From 7541460cf31da8eadcb5a0d2b11b52f13b8d02f7 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 8 Jul 2026 15:36:26 -0400 Subject: [PATCH 1/8] ref(integrations): route HTTP header filtering through data collection config `_filter_headers` previously used a hardcoded sensitive-header tuple and a `send_default_pii`/`use_annotated_value` toggle. It now delegates to `_apply_key_value_collection_filtering` from `sentry_sdk.data_collection`, so header scrubbing respects the new `data_collection.http_headers.request` allowlist/denylist/off configuration. Cookie and set-cookie headers are always redacted regardless of mode. Drops the now-unused `use_annotated_value` parameter from all call sites. Work to scrub cookies in a more granular way will be tackled as part of PY-2581/#6741. Fixes PY-2584 Fixes #6744 --- sentry_sdk/data_collection.py | 60 ++++++- sentry_sdk/integrations/_asgi_common.py | 2 +- sentry_sdk/integrations/_wsgi_common.py | 41 ++--- sentry_sdk/integrations/aiohttp.py | 2 +- sentry_sdk/integrations/aws_lambda.py | 2 +- sentry_sdk/integrations/chalice.py | 2 +- sentry_sdk/integrations/gcp.py | 2 +- sentry_sdk/integrations/quart.py | 2 +- sentry_sdk/integrations/sanic.py | 2 +- sentry_sdk/integrations/tornado.py | 2 +- sentry_sdk/integrations/wsgi.py | 2 +- tests/integrations/aiohttp/test_aiohttp.py | 160 +++++++++++++++++-- tests/integrations/quart/test_quart.py | 172 ++++++++++++++++----- 13 files changed, 361 insertions(+), 90 deletions(-) diff --git a/sentry_sdk/data_collection.py b/sentry_sdk/data_collection.py index e742f3c209..bde922f96e 100644 --- a/sentry_sdk/data_collection.py +++ b/sentry_sdk/data_collection.py @@ -23,7 +23,9 @@ """ import warnings -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, List, Mapping, Optional, cast + +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE if TYPE_CHECKING: from typing import Any, Dict, Literal @@ -77,6 +79,62 @@ ] +def _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: + """ + Return whether ``key`` matches the sensitive denylist using a partial, + case-insensitive substring match. + + :param extra_terms: additional deny terms (e.g. user-provided) to consider + alongside the built-in `_SENSITIVE_DENYLIST`. + """ + lowered = key.lower() + for term in _SENSITIVE_DENYLIST: + if term in lowered: + return True + if extra_terms: + for term in extra_terms: + if term and term.lower() in lowered: + return True + return False + + +def _apply_key_value_collection_filtering( + items: "Mapping[str, Any]", + behaviour: "KeyValueCollectionBehaviour", + substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, +) -> "Dict[str, Any]": + + if behaviour["mode"] == "off": + return {} + + result: "Dict[str, Any]" = {} + + if behaviour["mode"] == "allowlist": + for key, value in items.items(): + is_allowed = False + if isinstance(key, str): + lowered = key.lower() + is_allowed = any( + term and term.lower() in lowered + for term in behaviour.get("terms", []) + ) + if is_allowed and not _is_sensitive_key(key): + result[key] = value + else: + result[key] = substitute + return result + + # denylist behaviour + for key, value in items.items(): + if isinstance(key, str) and _is_sensitive_key( + key=key, extra_terms=behaviour.get("terms", []) + ): + result[key] = substitute + else: + result[key] = value + return result + + def _map_from_send_default_pii( *, send_default_pii: bool, diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index 85f8f11d6d..1a3ced948e 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -148,7 +148,7 @@ def _get_request_attributes( if asgi_scope.get("method"): attributes["http.request.method"] = asgi_scope["method"].upper() - headers = _filter_headers(_get_headers(asgi_scope), use_annotated_value=False) + headers = _filter_headers(_get_headers(asgi_scope)) for header, value in headers.items(): attributes[f"http.request.header.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index cf1a365209..8a931e2a38 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -4,6 +4,7 @@ import sentry_sdk from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.data_collection import _apply_key_value_collection_filtering from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import AnnotatedValue, logger @@ -23,22 +24,6 @@ from sentry_sdk._types import Event, HttpStatusCodeRange -SENSITIVE_ENV_KEYS = ( - "REMOTE_ADDR", - "HTTP_X_FORWARDED_FOR", - "HTTP_SET_COOKIE", - "HTTP_COOKIE", - "HTTP_AUTHORIZATION", - "HTTP_PROXY_AUTHORIZATION", - "HTTP_X_API_KEY", - "HTTP_X_FORWARDED_FOR", - "HTTP_X_REAL_IP", -) - -SENSITIVE_HEADERS = tuple( - x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") -) - DEFAULT_HTTP_METHODS_TO_CAPTURE = ( "CONNECT", "DELETE", @@ -211,21 +196,19 @@ def _is_json_content_type(ct: "Optional[str]") -> bool: def _filter_headers( headers: "Mapping[str, str]", - use_annotated_value: bool = True, -) -> "Mapping[str, Union[AnnotatedValue, str]]": - if should_send_default_pii(): - return headers - - substitute: "Union[AnnotatedValue, str]" = ( - SENSITIVE_DATA_SUBSTITUTE - if not use_annotated_value - else AnnotatedValue.removed_because_over_size_limit() +) -> "Mapping[str, str]": + data_collection_configuration = sentry_sdk.get_client().options["data_collection"] + + filtered = _apply_key_value_collection_filtering( + items=headers, + behaviour=data_collection_configuration["http_headers"]["request"], ) - return { - k: (v if k.upper().replace("-", "_") not in SENSITIVE_HEADERS else substitute) - for k, v in headers.items() - } + for key in filtered: + if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): + filtered[key] = SENSITIVE_DATA_SUBSTITUTE + + return filtered def _in_http_status_code_range( diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index d22f3a745b..e5c5769528 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -150,7 +150,7 @@ async def sentry_app_handle( header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers( - headers, use_annotated_value=False + headers ).items(): header_attributes[ f"http.request.header.{header.lower()}" diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index c7fe77714a..d4dd778504 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -152,7 +152,7 @@ def sentry_handler( header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers( - headers, use_annotated_value=False + headers ).items(): header_attributes[f"http.request.header.{header.lower()}"] = ( header_value diff --git a/sentry_sdk/integrations/chalice.py b/sentry_sdk/integrations/chalice.py index 9baa0e5cdd..579a7eacf8 100644 --- a/sentry_sdk/integrations/chalice.py +++ b/sentry_sdk/integrations/chalice.py @@ -92,7 +92,7 @@ def wrapped_view_function(**function_args: "Any") -> "Any": header_attrs: "Dict[str, Any]" = {} for header, value in _filter_headers( - headers, use_annotated_value=False + headers ).items(): header_attrs[f"http.request.header.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/gcp.py b/sentry_sdk/integrations/gcp.py index 91a62b3a81..d92f35e796 100644 --- a/sentry_sdk/integrations/gcp.py +++ b/sentry_sdk/integrations/gcp.py @@ -89,7 +89,7 @@ def sentry_func( if hasattr(gcp_event, "headers"): headers = gcp_event.headers for header, header_value in _filter_headers( - headers, use_annotated_value=False + headers ).items(): header_attributes[f"http.request.header.{header.lower()}"] = ( # header_value will always be a string because we set `use_annotated_value` to false above diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 6a5603d825..37b2b80e49 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -195,7 +195,7 @@ async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers( - dict(request_websocket.headers), use_annotated_value=False + dict(request_websocket.headers) ).items(): header_attributes[f"http.request.header.{header.lower()}"] = ( header_value diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 908fceb0cf..8ae313d282 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -374,7 +374,7 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]": if request.method: attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - headers = _filter_headers(dict(request.headers), use_annotated_value=False) + headers = _filter_headers(dict(request.headers)) for header, value in headers.items(): attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 859b0d0870..845cab66d4 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -193,7 +193,7 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": if request.method: attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - headers = _filter_headers(dict(request.headers), use_annotated_value=False) + headers = _filter_headers(dict(request.headers)) for header, value in headers.items(): attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index e776ed915a..4cb2cf7303 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -390,7 +390,7 @@ def _get_request_attributes( if method: attributes["http.request.method"] = method.upper() - headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) + headers = _filter_headers(dict(_get_headers(environ))) for header, value in headers.items(): attributes[f"http.request.header.{header.lower()}"] = value diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 583e7a50b6..4732f53f7b 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1258,14 +1258,128 @@ async def hello(request): ) +@pytest.mark.parametrize( + "options,expected", + [ + pytest.param( + { + "send_default_pii": True, + "data_collection": None, + }, + { + "authorization": "[Filtered]", + "custom": "foobar", + "cookie": "[Filtered]", + }, + id="enabled_send_default_pii_redacts_auth_header_due_to_data_collection_default_settings", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": None, + }, + { + "authorization": "[Filtered]", + "custom": "foobar", + "cookie": "[Filtered]", + }, + id="disabled_send_default_pii_redacts_auth_header_due_to_data_collection_default_settings", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": {"http_headers": {"request": {"mode": "off"}}}, + }, + None, + id="data_collection_off_does_not_add_headers", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": {"http_headers": {"request": {"mode": "allowlist"}}}, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_allow_list_redacts_terms_that_do_not_appear", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["Authorization"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_allow_list_redacts_sensitive_terms_even_when_provided_by_user", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["custom"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "foobar", + "cookie": "[Filtered]", + }, + id="data_collection_allow_list_does_not_redact_provided_term", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "denylist", "terms": ["custom"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_deny_list_redacts_sensitive_terms_when_provided_by_user", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["cookie"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_cookie_is_always_redacted_even_when_allow_listed", + ), + ], +) @pytest.mark.asyncio async def test_sensitive_header_passthrough_with_pii_span_streaming( - sentry_init, aiohttp_client, capture_items + sentry_init, aiohttp_client, capture_items, options, expected, request ): sentry_init( integrations=[AioHttpIntegration()], traces_sample_rate=1.0, - send_default_pii=True, + send_default_pii=options["send_default_pii"], + data_collection=options["data_collection"], _experiments={"trace_lifecycle": "stream"}, ) @@ -1278,21 +1392,45 @@ async def hello(request): items = capture_items("span") client = await aiohttp_client(app) - await client.get("/", headers={"Authorization": "Bearer secret-token"}) + await client.get( + "/", + headers={ + "Authorization": "Bearer secret-token", + "x-custom-header": "foobar", + "Cookie": "sessionid=secret", + }, + ) sentry_sdk.flush() server_span, _client_segment = [item.payload for item in items] - # With send_default_pii=True, _filter_headers is a no-op and the original - # value reaches the span attribute. - assert ( - server_span["attributes"]["http.request.header.authorization"] - == "Bearer secret-token" - ) + if request.node.callspec.id == "data_collection_off_does_not_add_headers": + assert "http.request.header.authorization" not in server_span["attributes"] + assert "http.request.header.cookie" not in server_span["attributes"] + else: + assert ( + server_span["attributes"]["http.request.header.authorization"] + == expected["authorization"] + ) + assert ( + server_span["attributes"]["http.request.header.x-custom-header"] + == expected["custom"] + ) + assert ( + server_span["attributes"]["http.request.header.cookie"] + == expected["cookie"] + ) + # client.address and user.ip_address is captured under send_default_pii=True. - assert server_span["attributes"]["client.address"] == "127.0.0.1" - assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + # TODO: This block will eventually need to be removed from this test into a separate + # test once data collection gating is introduced on these values + if options["send_default_pii"]: + assert server_span["attributes"]["client.address"] == "127.0.0.1" + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "user.ip_address" not in server_span["attributes"] + assert "client.address" not in server_span["attributes"] @pytest.mark.asyncio diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 730b24ef33..0e716698ca 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -14,7 +14,6 @@ set_tag, ) from sentry_sdk.integrations.logging import LoggingIntegration -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE def quart_app_factory(): @@ -861,12 +860,128 @@ async def test_span_streaming_request_attributes_with_pii(sentry_init, capture_i assert "user.ip_address" in segment["attributes"] +@pytest.mark.parametrize( + "options,expected", + [ + pytest.param( + { + "send_default_pii": True, + "data_collection": None, + }, + { + "authorization": "[Filtered]", + "custom": "passthrough", + "cookie": "[Filtered]", + }, + id="enabled_send_default_pii_redacts_auth_header_due_to_data_collection_default_settings", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": None, + }, + { + "authorization": "[Filtered]", + "custom": "passthrough", + "cookie": "[Filtered]", + }, + id="disabled_send_default_pii_redacts_auth_header_due_to_data_collection_default_settings", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": {"http_headers": {"request": {"mode": "off"}}}, + }, + None, + id="data_collection_off_does_not_add_headers", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": {"http_headers": {"request": {"mode": "allowlist"}}}, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_allow_list_redacts_terms_that_do_not_appear", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["Authorization"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_allow_list_redacts_sensitive_terms_even_when_provided_by_user", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["custom"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "passthrough", + "cookie": "[Filtered]", + }, + id="data_collection_allow_list_does_not_redact_provided_term", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "denylist", "terms": ["custom"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_deny_list_redacts_sensitive_terms_when_provided_by_user", + ), + pytest.param( + { + "send_default_pii": False, + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["cookie"]} + } + }, + }, + { + "authorization": "[Filtered]", + "custom": "[Filtered]", + "cookie": "[Filtered]", + }, + id="data_collection_cookie_is_always_redacted_even_when_allow_listed", + ), + ], +) @pytest.mark.asyncio -async def test_span_streaming_sensitive_header_scrubbing(sentry_init, capture_items): +async def test_span_streaming_sensitive_header_scrubbing( + sentry_init, capture_items, options, expected, request +): sentry_init( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, - send_default_pii=False, + send_default_pii=options["send_default_pii"], + data_collection=options["data_collection"], _experiments={"trace_lifecycle": "stream"}, ) items = capture_items("span") @@ -878,6 +993,7 @@ async def test_span_streaming_sensitive_header_scrubbing(sentry_init, capture_it headers={ "Authorization": "Bearer secret-token", "X-Custom-Header": "passthrough", + "Cookie": "sessionid=secret", }, ) assert response.status_code == 200 @@ -888,11 +1004,19 @@ async def test_span_streaming_sensitive_header_scrubbing(sentry_init, capture_it assert len(spans) == 1 segment = spans[0] - assert ( - segment["attributes"]["http.request.header.authorization"] - == SENSITIVE_DATA_SUBSTITUTE - ) - assert segment["attributes"]["http.request.header.x-custom-header"] == "passthrough" + if request.node.callspec.id == "data_collection_off_does_not_add_headers": + assert "http.request.header.authorization" not in segment["attributes"] + assert "http.request.header.cookie" not in segment["attributes"] + else: + assert ( + segment["attributes"]["http.request.header.authorization"] + == expected["authorization"] + ) + assert ( + segment["attributes"]["http.request.header.x-custom-header"] + == expected["custom"] + ) + assert segment["attributes"]["http.request.header.cookie"] == expected["cookie"] @pytest.mark.asyncio @@ -936,35 +1060,3 @@ async def login(): assert segment["attributes"]["user.id"] == user_id else: assert "user.id" not in segment.get("attributes", {}) - - -@pytest.mark.asyncio -async def test_span_streaming_sensitive_header_passthrough_with_pii( - sentry_init, capture_items -): - sentry_init( - integrations=[quart_sentry.QuartIntegration()], - traces_sample_rate=1.0, - send_default_pii=True, - _experiments={"trace_lifecycle": "stream"}, - ) - items = capture_items("span") - - app = quart_app_factory() - client = app.test_client() - response = await client.get( - "/message", - headers={"Authorization": "Bearer secret-token"}, - ) - assert response.status_code == 200 - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 1 - - segment = spans[0] - assert ( - segment["attributes"]["http.request.header.authorization"] - == "Bearer secret-token" - ) From a590c8ece1d8704f2ed91bf6c2bd9dc66170ae0e Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 10:45:38 -0400 Subject: [PATCH 2/8] Make changes to adjust to the data collection option being within the _experiments property in the client --- sentry_sdk/integrations/_asgi_common.py | 1 + sentry_sdk/integrations/_wsgi_common.py | 59 +++++++++++++---- sentry_sdk/integrations/aiohttp.py | 3 +- sentry_sdk/integrations/aws_lambda.py | 2 +- sentry_sdk/integrations/chalice.py | 2 +- sentry_sdk/integrations/gcp.py | 2 +- sentry_sdk/integrations/quart.py | 2 +- sentry_sdk/integrations/sanic.py | 2 +- sentry_sdk/integrations/tornado.py | 2 +- sentry_sdk/integrations/wsgi.py | 2 +- tests/integrations/aiohttp/test_aiohttp.py | 43 ++++++++++++- tests/integrations/quart/test_quart.py | 75 +++++++++++++++++++++- 12 files changed, 172 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index 1a3ced948e..b5b8565bf0 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -117,6 +117,7 @@ def _get_request_data( request_data["headers"] = headers = _filter_headers( _get_headers(asgi_scope), + use_annotated_value=False, ) request_data["query_string"] = _get_query(asgi_scope) diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 8a931e2a38..0ffeac688a 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -6,7 +6,7 @@ from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.data_collection import _apply_key_value_collection_filtering from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue, logger +from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger try: from django.http.request import RawPostDataException @@ -23,6 +23,21 @@ from sentry_sdk._types import Event, HttpStatusCodeRange +SENSITIVE_ENV_KEYS = ( + "REMOTE_ADDR", + "HTTP_X_FORWARDED_FOR", + "HTTP_SET_COOKIE", + "HTTP_COOKIE", + "HTTP_AUTHORIZATION", + "HTTP_PROXY_AUTHORIZATION", + "HTTP_X_API_KEY", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", +) + +SENSITIVE_HEADERS = tuple( + x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") +) DEFAULT_HTTP_METHODS_TO_CAPTURE = ( "CONNECT", @@ -196,19 +211,41 @@ def _is_json_content_type(ct: "Optional[str]") -> bool: def _filter_headers( headers: "Mapping[str, str]", -) -> "Mapping[str, str]": - data_collection_configuration = sentry_sdk.get_client().options["data_collection"] + use_annotated_value: bool = True, +) -> "Mapping[str, Union[str, AnnotatedValue]]": + client_options = sentry_sdk.get_client().options - filtered = _apply_key_value_collection_filtering( - items=headers, - behaviour=data_collection_configuration["http_headers"]["request"], - ) + if has_data_collection_enabled(client_options): + data_collection_configuration = client_options["data_collection"] - for key in filtered: - if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): - filtered[key] = SENSITIVE_DATA_SUBSTITUTE + filtered = _apply_key_value_collection_filtering( + items=headers, + behaviour=data_collection_configuration["http_headers"]["request"], + ) - return filtered + for key in filtered: + if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): + filtered[key] = SENSITIVE_DATA_SUBSTITUTE + + return filtered + else: + if should_send_default_pii(): + return headers + + substitute: "Union[AnnotatedValue, str]" = ( + SENSITIVE_DATA_SUBSTITUTE + if not use_annotated_value + else AnnotatedValue.removed_because_over_size_limit() + ) + + return { + k: ( + v + if k.upper().replace("-", "_") not in SENSITIVE_HEADERS + else substitute + ) + for k, v in headers.items() + } def _in_http_status_code_range( diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index e5c5769528..59bae92a60 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -150,7 +150,8 @@ async def sentry_app_handle( header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers( - headers + headers, + use_annotated_value=False, ).items(): header_attributes[ f"http.request.header.{header.lower()}" diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index d4dd778504..c7fe77714a 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -152,7 +152,7 @@ def sentry_handler( header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers( - headers + headers, use_annotated_value=False ).items(): header_attributes[f"http.request.header.{header.lower()}"] = ( header_value diff --git a/sentry_sdk/integrations/chalice.py b/sentry_sdk/integrations/chalice.py index 579a7eacf8..9baa0e5cdd 100644 --- a/sentry_sdk/integrations/chalice.py +++ b/sentry_sdk/integrations/chalice.py @@ -92,7 +92,7 @@ def wrapped_view_function(**function_args: "Any") -> "Any": header_attrs: "Dict[str, Any]" = {} for header, value in _filter_headers( - headers + headers, use_annotated_value=False ).items(): header_attrs[f"http.request.header.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/gcp.py b/sentry_sdk/integrations/gcp.py index d92f35e796..91a62b3a81 100644 --- a/sentry_sdk/integrations/gcp.py +++ b/sentry_sdk/integrations/gcp.py @@ -89,7 +89,7 @@ def sentry_func( if hasattr(gcp_event, "headers"): headers = gcp_event.headers for header, header_value in _filter_headers( - headers + headers, use_annotated_value=False ).items(): header_attributes[f"http.request.header.{header.lower()}"] = ( # header_value will always be a string because we set `use_annotated_value` to false above diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 37b2b80e49..6a5603d825 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -195,7 +195,7 @@ async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers( - dict(request_websocket.headers) + dict(request_websocket.headers), use_annotated_value=False ).items(): header_attributes[f"http.request.header.{header.lower()}"] = ( header_value diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 8ae313d282..908fceb0cf 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -374,7 +374,7 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]": if request.method: attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - headers = _filter_headers(dict(request.headers)) + headers = _filter_headers(dict(request.headers), use_annotated_value=False) for header, value in headers.items(): attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 845cab66d4..859b0d0870 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -193,7 +193,7 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": if request.method: attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - headers = _filter_headers(dict(request.headers)) + headers = _filter_headers(dict(request.headers), use_annotated_value=False) for header, value in headers.items(): attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 4cb2cf7303..e776ed915a 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -390,7 +390,7 @@ def _get_request_attributes( if method: attributes["http.request.method"] = method.upper() - headers = _filter_headers(dict(_get_headers(environ))) + headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) for header, value in headers.items(): attributes[f"http.request.header.{header.lower()}"] = value diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 4732f53f7b..3b5acece83 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1379,8 +1379,10 @@ async def test_sensitive_header_passthrough_with_pii_span_streaming( integrations=[AioHttpIntegration()], traces_sample_rate=1.0, send_default_pii=options["send_default_pii"], - data_collection=options["data_collection"], - _experiments={"trace_lifecycle": "stream"}, + _experiments={ + "trace_lifecycle": "stream", + "data_collection": options["data_collection"], + }, ) async def hello(request): @@ -1433,6 +1435,43 @@ async def hello(request): assert "client.address" not in server_span["attributes"] +@pytest.mark.asyncio +async def test_sensitive_header_passthrough_with_pii_span_streaming_without_data_collection( + sentry_init, aiohttp_client, capture_items +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + _experiments={"trace_lifecycle": "stream"}, + ) + + async def hello(request): + return web.Response(text="hello") + + app = web.Application() + app.router.add_get("/", hello) + + items = capture_items("span") + + client = await aiohttp_client(app) + await client.get("/", headers={"Authorization": "Bearer secret-token"}) + + sentry_sdk.flush() + + server_span, _client_segment = [item.payload for item in items] + + # With send_default_pii=True, _filter_headers is a no-op and the original + # value reaches the span attribute. + assert ( + server_span["attributes"]["http.request.header.authorization"] + == "Bearer secret-token" + ) + # client.address and user.ip_address is captured under send_default_pii=True. + assert server_span["attributes"]["client.address"] == "127.0.0.1" + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + + @pytest.mark.asyncio @pytest.mark.parametrize("send_pii", [True, False]) async def test_url_query_attribute_span_streaming( diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 0e716698ca..69b632806e 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -7,6 +7,7 @@ import pytest import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE import sentry_sdk.integrations.quart as quart_sentry from sentry_sdk import ( capture_exception, @@ -981,8 +982,10 @@ async def test_span_streaming_sensitive_header_scrubbing( integrations=[quart_sentry.QuartIntegration()], traces_sample_rate=1.0, send_default_pii=options["send_default_pii"], - data_collection=options["data_collection"], - _experiments={"trace_lifecycle": "stream"}, + _experiments={ + "trace_lifecycle": "stream", + "data_collection": options["data_collection"], + }, ) items = capture_items("span") @@ -1019,6 +1022,42 @@ async def test_span_streaming_sensitive_header_scrubbing( assert segment["attributes"]["http.request.header.cookie"] == expected["cookie"] +@pytest.mark.asyncio +async def test_span_streaming_sensitive_header_without_data_collection( + sentry_init, capture_items +): + sentry_init( + integrations=[quart_sentry.QuartIntegration()], + traces_sample_rate=1.0, + send_default_pii=False, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("span") + + app = quart_app_factory() + client = app.test_client() + response = await client.get( + "/message", + headers={ + "Authorization": "Bearer secret-token", + "X-Custom-Header": "passthrough", + }, + ) + assert response.status_code == 200 + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 1 + + segment = spans[0] + assert ( + segment["attributes"]["http.request.header.authorization"] + == SENSITIVE_DATA_SUBSTITUTE + ) + assert segment["attributes"]["http.request.header.x-custom-header"] == "passthrough" + + @pytest.mark.asyncio @pytest.mark.parametrize("send_default_pii", [True, False]) @pytest.mark.parametrize("user_id", [None, "42"]) @@ -1060,3 +1099,35 @@ async def login(): assert segment["attributes"]["user.id"] == user_id else: assert "user.id" not in segment.get("attributes", {}) + + +@pytest.mark.asyncio +async def test_span_streaming_sensitive_header_passthrough_with_pii_and_no_data_collection( + sentry_init, capture_items +): + sentry_init( + integrations=[quart_sentry.QuartIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("span") + + app = quart_app_factory() + client = app.test_client() + response = await client.get( + "/message", + headers={"Authorization": "Bearer secret-token"}, + ) + assert response.status_code == 200 + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 1 + + segment = spans[0] + assert ( + segment["attributes"]["http.request.header.authorization"] + == "Bearer secret-token" + ) From 16ed0c84c66d72df07564674efdb4818e056b8fa Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 11:03:26 -0400 Subject: [PATCH 3/8] fix test --- tests/integrations/aiohttp/test_aiohttp.py | 2 +- tests/integrations/quart/test_quart.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 3b5acece83..dc485954ec 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1407,7 +1407,7 @@ async def hello(request): server_span, _client_segment = [item.payload for item in items] - if request.node.callspec.id == "data_collection_off_does_not_add_headers": + if request.node.callspec.id.endswith("data_collection_off_does_not_add_headers"): assert "http.request.header.authorization" not in server_span["attributes"] assert "http.request.header.cookie" not in server_span["attributes"] else: diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 69b632806e..e381dedfe0 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -7,13 +7,13 @@ import pytest import sentry_sdk -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE import sentry_sdk.integrations.quart as quart_sentry from sentry_sdk import ( capture_exception, capture_message, set_tag, ) +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.integrations.logging import LoggingIntegration From 486f0bc18836673402f7882bb4e6b3e48392d099 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 14:41:22 -0400 Subject: [PATCH 4/8] make sure host header is not filtered when being added to the url --- sentry_sdk/integrations/_asgi_common.py | 13 +++-- sentry_sdk/integrations/_wsgi_common.py | 3 +- tests/integrations/asgi/test_asgi.py | 63 ++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index b5b8565bf0..7ff4657013 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -115,10 +115,13 @@ def _get_request_data( if ty in ("http", "websocket"): request_data["method"] = asgi_scope.get("method") - request_data["headers"] = headers = _filter_headers( - _get_headers(asgi_scope), + headers = _get_headers(asgi_scope) + + request_data["headers"] = _filter_headers( + headers, use_annotated_value=False, ) + request_data["query_string"] = _get_query(asgi_scope) request_data["url"] = _get_url( @@ -149,8 +152,10 @@ def _get_request_attributes( if asgi_scope.get("method"): attributes["http.request.method"] = asgi_scope["method"].upper() - headers = _filter_headers(_get_headers(asgi_scope)) - for header, value in headers.items(): + headers = _get_headers(asgi_scope) + + filtered_headers = _filter_headers(headers, use_annotated_value=False) + for header, value in filtered_headers.items(): attributes[f"http.request.header.{header.lower()}"] = value if should_send_default_pii(): diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 0ffeac688a..ad0ab7c734 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -23,6 +23,7 @@ from sentry_sdk._types import Event, HttpStatusCodeRange + SENSITIVE_ENV_KEYS = ( "REMOTE_ADDR", "HTTP_X_FORWARDED_FOR", @@ -212,7 +213,7 @@ def _is_json_content_type(ct: "Optional[str]") -> bool: def _filter_headers( headers: "Mapping[str, str]", use_annotated_value: bool = True, -) -> "Mapping[str, Union[str, AnnotatedValue]]": +) -> "Mapping[str, Union[AnnotatedValue, str]]": client_options = sentry_sdk.get_client().options if has_data_collection_enabled(client_options): diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 3bce0d1e10..4e43514188 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -5,7 +5,13 @@ import sentry_sdk from sentry_sdk import capture_message -from sentry_sdk.integrations._asgi_common import _get_headers, _get_ip +from sentry_sdk.integrations._asgi_common import ( + _get_headers, + _get_ip, + _get_request_attributes, + _get_request_data, + _RootPathInPath, +) from sentry_sdk.integrations.asgi import SentryAsgiMiddleware, _looks_like_asgi3 from sentry_sdk.tracing import TransactionSource @@ -831,6 +837,61 @@ def test_get_headers(): } +def test_get_request_data_url_with_filtered_host(sentry_init): + # allowlist mode in data collection that does not allow "host" scrubs the host header value, + # but the reported URL must still resolve via rather than embedding the substituted "[Filtered]" value. + sentry_init( + _experiments={ + "data_collection": { + "http_headers": {"request": {"mode": "allowlist", "terms": []}} + } + } + ) + + scope = { + "type": "http", + "method": "GET", + "scheme": "http", + "server": ("example.com", 80), + "path": "/foo", + "query_string": b"", + "headers": [(b"host", b"example.com")], + } + + request_data = _get_request_data(scope, _RootPathInPath.EXCLUDED) + + assert request_data["headers"]["host"] == "[Filtered]" + assert request_data["url"] == "http://example.com/foo" + + +def test_get_request_attributes_url_with_filtered_host(sentry_init): + # As with _get_request_data, an allowlist mode that does not allow "host" + # scrubs the host header value, but "url.full" must still resolve rather than embedding the substituted value. + sentry_init( + send_default_pii=True, + _experiments={ + "data_collection": { + "http_headers": {"request": {"mode": "allowlist", "terms": []}} + } + }, + ) + + scope = { + "type": "http", + "method": "GET", + "scheme": "http", + "server": ("example.com", 80), + "path": "/foo", + "query_string": b"somevalue=123", + "headers": [(b"host", b"example.com")], + } + + attributes = _get_request_attributes(scope, _RootPathInPath.EXCLUDED) + + assert attributes["http.request.header.host"] == "[Filtered]" + assert attributes["url.full"] == "http://example.com/foo?somevalue=123" + + @pytest.mark.asyncio @pytest.mark.parametrize( "request_url,transaction_style,expected_transaction_name,expected_transaction_source", From 7d14a8134dc4f0e4733815745eea1f65759c2b2e Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 15:37:09 -0400 Subject: [PATCH 5/8] add test coverage for edge case around host header being dropped but still being needed for the url attribute --- tests/integrations/asgi/test_asgi.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 4e43514188..e72cd317c2 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -892,6 +892,32 @@ def test_get_request_attributes_url_with_filtered_host(sentry_init): assert attributes["url.full"] == "http://example.com/foo?somevalue=123" +def test_get_request_attributes_url_with_headers_off(sentry_init): + # "off" mode in data collection captures no headers at all, but "url.full" + # must still resolve via the (uncaptured) host header rather than being dropped. + sentry_init( + send_default_pii=True, + _experiments={ + "data_collection": {"http_headers": {"request": {"mode": "off"}}} + }, + ) + + scope = { + "type": "http", + "method": "GET", + "scheme": "http", + "server": ("example.com", 80), + "path": "/foo", + "query_string": b"somevalue=123", + "headers": [(b"host", b"example.com")], + } + + attributes = _get_request_attributes(scope, _RootPathInPath.EXCLUDED) + + assert not any(key.startswith("http.request.header.") for key in attributes) + assert attributes["url.full"] == "http://example.com/foo?somevalue=123" + + @pytest.mark.asyncio @pytest.mark.parametrize( "request_url,transaction_style,expected_transaction_name,expected_transaction_source", From bb98334b6fec9feb3082f437150211b1f1e32893 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 16:03:03 -0400 Subject: [PATCH 6/8] add aws lambda test coverage --- .../BasicOkDataCollectionAllowlist/.lock | 0 .../certifi-2026.6.17.dist-info/INSTALLER | 1 + .../certifi-2026.6.17.dist-info/METADATA | 78 + .../certifi-2026.6.17.dist-info/RECORD | 12 + .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../certifi-2026.6.17.dist-info/top_level.txt | 1 + .../certifi/__init__.py | 4 + .../certifi/__main__.py | 12 + .../certifi/cacert.pem | 3863 +++++++++++++++++ .../certifi/core.py | 83 + .../certifi/py.typed | 0 .../BasicOkDataCollectionAllowlist/index.py | 27 + .../sentry_sdk/__init__.py | 70 + .../sentry_sdk/_batcher.py | 184 + .../sentry_sdk/_compat.py | 92 + .../sentry_sdk/_init_implementation.py | 78 + .../sentry_sdk/_log_batcher.py | 66 + .../sentry_sdk/_lru_cache.py | 43 + .../sentry_sdk/_metrics_batcher.py | 48 + .../sentry_sdk/_queue.py | 287 ++ .../sentry_sdk/_span_batcher.py | 234 + .../sentry_sdk/_types.py | 481 ++ .../sentry_sdk/_werkzeug.py | 100 + .../sentry_sdk/ai/__init__.py | 8 + .../sentry_sdk/ai/_openai_completions_api.py | 66 + .../sentry_sdk/ai/_openai_responses_api.py | 22 + .../sentry_sdk/ai/consts.py | 6 + .../sentry_sdk/ai/monitoring.py | 147 + .../sentry_sdk/ai/utils.py | 782 ++++ .../sentry_sdk/api.py | 573 +++ .../sentry_sdk/attachments.py | 71 + .../sentry_sdk/client.py | 1479 +++++++ .../sentry_sdk/consts.py | 1802 ++++++++ .../sentry_sdk/crons/__init__.py | 9 + .../sentry_sdk/crons/api.py | 60 + .../sentry_sdk/crons/consts.py | 4 + .../sentry_sdk/crons/decorator.py | 137 + .../sentry_sdk/data_collection.py | 319 ++ .../sentry_sdk/debug.py | 37 + .../sentry_sdk/envelope.py | 332 ++ .../sentry_sdk/feature_flags.py | 75 + .../sentry_sdk/hub.py | 741 ++++ .../sentry_sdk/integrations/__init__.py | 352 ++ .../sentry_sdk/integrations/_asgi_common.py | 187 + .../sentry_sdk/integrations/_wsgi_common.py | 282 ++ .../sentry_sdk/integrations/aiohttp.py | 509 +++ .../sentry_sdk/integrations/aiomysql.py | 275 ++ .../sentry_sdk/integrations/anthropic.py | 1154 +++++ .../sentry_sdk/integrations/argv.py | 28 + .../sentry_sdk/integrations/ariadne.py | 167 + .../sentry_sdk/integrations/arq.py | 277 ++ .../sentry_sdk/integrations/asgi.py | 543 +++ .../sentry_sdk/integrations/asyncio.py | 280 ++ .../sentry_sdk/integrations/asyncpg.py | 312 ++ .../sentry_sdk/integrations/atexit.py | 51 + .../sentry_sdk/integrations/aws_lambda.py | 542 +++ .../sentry_sdk/integrations/beam.py | 164 + .../sentry_sdk/integrations/boto3.py | 187 + .../sentry_sdk/integrations/bottle.py | 239 + .../integrations/celery/__init__.py | 612 +++ .../sentry_sdk/integrations/celery/beat.py | 291 ++ .../sentry_sdk/integrations/celery/utils.py | 32 + .../sentry_sdk/integrations/chalice.py | 188 + .../integrations/clickhouse_driver.py | 211 + .../integrations/cloud_resource_context.py | 273 ++ .../sentry_sdk/integrations/cohere.py | 303 ++ .../sentry_sdk/integrations/dedupe.py | 62 + .../integrations/django/__init__.py | 884 ++++ .../sentry_sdk/integrations/django/asgi.py | 262 ++ .../sentry_sdk/integrations/django/caching.py | 264 ++ .../integrations/django/middleware.py | 219 + .../integrations/django/signals_handlers.py | 105 + .../sentry_sdk/integrations/django/tasks.py | 52 + .../integrations/django/templates.py | 209 + .../integrations/django/transactions.py | 154 + .../sentry_sdk/integrations/django/views.py | 127 + .../sentry_sdk/integrations/dramatiq.py | 246 ++ .../sentry_sdk/integrations/excepthook.py | 76 + .../sentry_sdk/integrations/executing.py | 66 + .../sentry_sdk/integrations/falcon.py | 278 ++ .../sentry_sdk/integrations/fastapi.py | 201 + .../sentry_sdk/integrations/flask.py | 281 ++ .../sentry_sdk/integrations/gcp.py | 286 ++ .../sentry_sdk/integrations/gnu_backtrace.py | 96 + .../integrations/google_genai/__init__.py | 457 ++ .../integrations/google_genai/consts.py | 16 + .../integrations/google_genai/streaming.py | 172 + .../integrations/google_genai/utils.py | 1118 +++++ .../sentry_sdk/integrations/gql.py | 173 + .../sentry_sdk/integrations/graphene.py | 181 + .../sentry_sdk/integrations/grpc/__init__.py | 188 + .../integrations/grpc/aio/__init__.py | 7 + .../integrations/grpc/aio/client.py | 146 + .../integrations/grpc/aio/server.py | 134 + .../sentry_sdk/integrations/grpc/client.py | 149 + .../sentry_sdk/integrations/grpc/consts.py | 1 + .../sentry_sdk/integrations/grpc/server.py | 91 + .../sentry_sdk/integrations/httpx.py | 263 ++ .../sentry_sdk/integrations/httpx2.py | 263 ++ .../sentry_sdk/integrations/huey.py | 239 + .../integrations/huggingface_hub.py | 392 ++ .../sentry_sdk/integrations/langchain.py | 1412 ++++++ .../sentry_sdk/integrations/langgraph.py | 515 +++ .../sentry_sdk/integrations/launchdarkly.py | 63 + .../sentry_sdk/integrations/litellm.py | 376 ++ .../sentry_sdk/integrations/litestar.py | 364 ++ .../sentry_sdk/integrations/logging.py | 476 ++ .../sentry_sdk/integrations/loguru.py | 208 + .../sentry_sdk/integrations/mcp.py | 876 ++++ .../sentry_sdk/integrations/modules.py | 28 + .../sentry_sdk/integrations/openai.py | 1530 +++++++ .../integrations/openai_agents/__init__.py | 250 ++ .../integrations/openai_agents/consts.py | 1 + .../openai_agents/patches/__init__.py | 10 + .../openai_agents/patches/agent_run.py | 332 ++ .../openai_agents/patches/error_tracing.py | 74 + .../openai_agents/patches/models.py | 197 + .../openai_agents/patches/runner.py | 221 + .../openai_agents/patches/tools.py | 70 + .../openai_agents/spans/__init__.py | 8 + .../openai_agents/spans/agent_workflow.py | 32 + .../openai_agents/spans/ai_client.py | 84 + .../openai_agents/spans/execute_tool.py | 82 + .../openai_agents/spans/handoff.py | 41 + .../openai_agents/spans/invoke_agent.py | 122 + .../integrations/openai_agents/utils.py | 244 ++ .../sentry_sdk/integrations/openfeature.py | 34 + .../integrations/opentelemetry/__init__.py | 7 + .../integrations/opentelemetry/consts.py | 9 + .../integrations/opentelemetry/integration.py | 81 + .../integrations/opentelemetry/propagator.py | 128 + .../opentelemetry/span_processor.py | 407 ++ .../sentry_sdk/integrations/otlp.py | 223 + .../sentry_sdk/integrations/pure_eval.py | 134 + .../integrations/pydantic_ai/__init__.py | 187 + .../integrations/pydantic_ai/consts.py | 1 + .../pydantic_ai/patches/__init__.py | 3 + .../pydantic_ai/patches/agent_run.py | 198 + .../pydantic_ai/patches/graph_nodes.py | 106 + .../integrations/pydantic_ai/patches/tools.py | 177 + .../pydantic_ai/spans/__init__.py | 3 + .../pydantic_ai/spans/ai_client.py | 331 ++ .../pydantic_ai/spans/execute_tool.py | 84 + .../pydantic_ai/spans/invoke_agent.py | 184 + .../integrations/pydantic_ai/spans/utils.py | 87 + .../integrations/pydantic_ai/utils.py | 233 + .../sentry_sdk/integrations/pymongo.py | 262 ++ .../sentry_sdk/integrations/pyramid.py | 239 + .../sentry_sdk/integrations/pyreqwest.py | 197 + .../sentry_sdk/integrations/quart.py | 292 ++ .../sentry_sdk/integrations/ray.py | 240 + .../sentry_sdk/integrations/redis/__init__.py | 49 + .../integrations/redis/_async_common.py | 177 + .../integrations/redis/_sync_common.py | 176 + .../sentry_sdk/integrations/redis/consts.py | 19 + .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 + .../integrations/redis/modules/queries.py | 88 + .../sentry_sdk/integrations/redis/rb.py | 31 + .../sentry_sdk/integrations/redis/redis.py | 67 + .../integrations/redis/redis_cluster.py | 115 + .../redis/redis_py_cluster_legacy.py | 49 + .../sentry_sdk/integrations/redis/utils.py | 159 + .../sentry_sdk/integrations/rq.py | 225 + .../sentry_sdk/integrations/rust_tracing.py | 300 ++ .../sentry_sdk/integrations/sanic.py | 431 ++ .../sentry_sdk/integrations/serverless.py | 65 + .../sentry_sdk/integrations/socket.py | 142 + .../sentry_sdk/integrations/spark/__init__.py | 4 + .../integrations/spark/spark_driver.py | 278 ++ .../integrations/spark/spark_worker.py | 109 + .../sentry_sdk/integrations/sqlalchemy.py | 185 + .../sentry_sdk/integrations/starlette.py | 858 ++++ .../sentry_sdk/integrations/starlite.py | 314 ++ .../sentry_sdk/integrations/statsig.py | 37 + .../sentry_sdk/integrations/stdlib.py | 404 ++ .../sentry_sdk/integrations/strawberry.py | 493 +++ .../sentry_sdk/integrations/sys_exit.py | 65 + .../sentry_sdk/integrations/threading.py | 196 + .../sentry_sdk/integrations/tornado.py | 320 ++ .../sentry_sdk/integrations/trytond.py | 52 + .../sentry_sdk/integrations/typer.py | 58 + .../sentry_sdk/integrations/unleash.py | 33 + .../sentry_sdk/integrations/unraisablehook.py | 50 + .../sentry_sdk/integrations/wsgi.py | 427 ++ .../sentry_sdk/logger.py | 88 + .../sentry_sdk/metrics.py | 62 + .../sentry_sdk/monitor.py | 138 + .../sentry_sdk/profiler/__init__.py | 49 + .../profiler/continuous_profiler.py | 703 +++ .../profiler/transaction_profiler.py | 802 ++++ .../sentry_sdk/profiler/utils.py | 186 + .../sentry_sdk/py.typed | 0 .../sentry_sdk/scope.py | 2185 ++++++++++ .../sentry_sdk/scrubber.py | 176 + .../sentry_sdk/serializer.py | 418 ++ .../sentry_sdk/session.py | 165 + .../sentry_sdk/sessions.py | 262 ++ .../sentry_sdk/spotlight.py | 327 ++ .../sentry_sdk/traces.py | 843 ++++ .../sentry_sdk/tracing.py | 1475 +++++++ .../sentry_sdk/tracing_utils.py | 1694 ++++++++ .../sentry_sdk/transport.py | 1255 ++++++ .../sentry_sdk/types.py | 52 + .../sentry_sdk/utils.py | 2178 ++++++++++ .../sentry_sdk/worker.py | 308 ++ .../urllib3-2.7.0.dist-info/INSTALLER | 1 + .../urllib3-2.7.0.dist-info/METADATA | 163 + .../urllib3-2.7.0.dist-info/RECORD | 44 + .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 + .../licenses/LICENSE.txt | 21 + .../urllib3/__init__.py | 211 + .../urllib3/_base_connection.py | 167 + .../urllib3/_collections.py | 486 +++ .../urllib3/_request_methods.py | 278 ++ .../urllib3/_version.py | 24 + .../urllib3/connection.py | 1099 +++++ .../urllib3/connectionpool.py | 1191 +++++ .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 + .../urllib3/contrib/emscripten/connection.py | 260 ++ .../emscripten/emscripten_fetch_worker.js | 110 + .../urllib3/contrib/emscripten/fetch.py | 726 ++++ .../urllib3/contrib/emscripten/request.py | 22 + .../urllib3/contrib/emscripten/response.py | 281 ++ .../urllib3/contrib/pyopenssl.py | 563 +++ .../urllib3/contrib/socks.py | 228 + .../urllib3/exceptions.py | 335 ++ .../urllib3/fields.py | 341 ++ .../urllib3/filepost.py | 89 + .../urllib3/http2/__init__.py | 53 + .../urllib3/http2/connection.py | 356 ++ .../urllib3/http2/probe.py | 87 + .../urllib3/poolmanager.py | 653 +++ .../urllib3/py.typed | 2 + .../urllib3/response.py | 1493 +++++++ .../urllib3/util/__init__.py | 42 + .../urllib3/util/connection.py | 137 + .../urllib3/util/proxy.py | 43 + .../urllib3/util/request.py | 263 ++ .../urllib3/util/response.py | 101 + .../urllib3/util/retry.py | 557 +++ .../urllib3/util/ssl_.py | 477 ++ .../urllib3/util/ssl_match_hostname.py | 153 + .../urllib3/util/ssltransport.py | 271 ++ .../urllib3/util/timeout.py | 275 ++ .../urllib3/util/url.py | 469 ++ .../urllib3/util/util.py | 42 + .../urllib3/util/wait.py | 124 + .../BasicOkDataCollectionDenylist/.lock | 0 .../certifi-2026.6.17.dist-info/INSTALLER | 1 + .../certifi-2026.6.17.dist-info/METADATA | 78 + .../certifi-2026.6.17.dist-info/RECORD | 12 + .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../certifi-2026.6.17.dist-info/top_level.txt | 1 + .../certifi/__init__.py | 4 + .../certifi/__main__.py | 12 + .../certifi/cacert.pem | 3863 +++++++++++++++++ .../certifi/core.py | 83 + .../certifi/py.typed | 0 .../BasicOkDataCollectionDenylist/index.py | 26 + .../sentry_sdk/__init__.py | 70 + .../sentry_sdk/_batcher.py | 184 + .../sentry_sdk/_compat.py | 92 + .../sentry_sdk/_init_implementation.py | 78 + .../sentry_sdk/_log_batcher.py | 66 + .../sentry_sdk/_lru_cache.py | 43 + .../sentry_sdk/_metrics_batcher.py | 48 + .../sentry_sdk/_queue.py | 287 ++ .../sentry_sdk/_span_batcher.py | 234 + .../sentry_sdk/_types.py | 481 ++ .../sentry_sdk/_werkzeug.py | 100 + .../sentry_sdk/ai/__init__.py | 8 + .../sentry_sdk/ai/_openai_completions_api.py | 66 + .../sentry_sdk/ai/_openai_responses_api.py | 22 + .../sentry_sdk/ai/consts.py | 6 + .../sentry_sdk/ai/monitoring.py | 147 + .../sentry_sdk/ai/utils.py | 782 ++++ .../sentry_sdk/api.py | 573 +++ .../sentry_sdk/attachments.py | 71 + .../sentry_sdk/client.py | 1479 +++++++ .../sentry_sdk/consts.py | 1802 ++++++++ .../sentry_sdk/crons/__init__.py | 9 + .../sentry_sdk/crons/api.py | 60 + .../sentry_sdk/crons/consts.py | 4 + .../sentry_sdk/crons/decorator.py | 137 + .../sentry_sdk/data_collection.py | 319 ++ .../sentry_sdk/debug.py | 37 + .../sentry_sdk/envelope.py | 332 ++ .../sentry_sdk/feature_flags.py | 75 + .../sentry_sdk/hub.py | 741 ++++ .../sentry_sdk/integrations/__init__.py | 352 ++ .../sentry_sdk/integrations/_asgi_common.py | 187 + .../sentry_sdk/integrations/_wsgi_common.py | 282 ++ .../sentry_sdk/integrations/aiohttp.py | 509 +++ .../sentry_sdk/integrations/aiomysql.py | 275 ++ .../sentry_sdk/integrations/anthropic.py | 1154 +++++ .../sentry_sdk/integrations/argv.py | 28 + .../sentry_sdk/integrations/ariadne.py | 167 + .../sentry_sdk/integrations/arq.py | 277 ++ .../sentry_sdk/integrations/asgi.py | 543 +++ .../sentry_sdk/integrations/asyncio.py | 280 ++ .../sentry_sdk/integrations/asyncpg.py | 312 ++ .../sentry_sdk/integrations/atexit.py | 51 + .../sentry_sdk/integrations/aws_lambda.py | 542 +++ .../sentry_sdk/integrations/beam.py | 164 + .../sentry_sdk/integrations/boto3.py | 187 + .../sentry_sdk/integrations/bottle.py | 239 + .../integrations/celery/__init__.py | 612 +++ .../sentry_sdk/integrations/celery/beat.py | 291 ++ .../sentry_sdk/integrations/celery/utils.py | 32 + .../sentry_sdk/integrations/chalice.py | 188 + .../integrations/clickhouse_driver.py | 211 + .../integrations/cloud_resource_context.py | 273 ++ .../sentry_sdk/integrations/cohere.py | 303 ++ .../sentry_sdk/integrations/dedupe.py | 62 + .../integrations/django/__init__.py | 884 ++++ .../sentry_sdk/integrations/django/asgi.py | 262 ++ .../sentry_sdk/integrations/django/caching.py | 264 ++ .../integrations/django/middleware.py | 219 + .../integrations/django/signals_handlers.py | 105 + .../sentry_sdk/integrations/django/tasks.py | 52 + .../integrations/django/templates.py | 209 + .../integrations/django/transactions.py | 154 + .../sentry_sdk/integrations/django/views.py | 127 + .../sentry_sdk/integrations/dramatiq.py | 246 ++ .../sentry_sdk/integrations/excepthook.py | 76 + .../sentry_sdk/integrations/executing.py | 66 + .../sentry_sdk/integrations/falcon.py | 278 ++ .../sentry_sdk/integrations/fastapi.py | 201 + .../sentry_sdk/integrations/flask.py | 281 ++ .../sentry_sdk/integrations/gcp.py | 286 ++ .../sentry_sdk/integrations/gnu_backtrace.py | 96 + .../integrations/google_genai/__init__.py | 457 ++ .../integrations/google_genai/consts.py | 16 + .../integrations/google_genai/streaming.py | 172 + .../integrations/google_genai/utils.py | 1118 +++++ .../sentry_sdk/integrations/gql.py | 173 + .../sentry_sdk/integrations/graphene.py | 181 + .../sentry_sdk/integrations/grpc/__init__.py | 188 + .../integrations/grpc/aio/__init__.py | 7 + .../integrations/grpc/aio/client.py | 146 + .../integrations/grpc/aio/server.py | 134 + .../sentry_sdk/integrations/grpc/client.py | 149 + .../sentry_sdk/integrations/grpc/consts.py | 1 + .../sentry_sdk/integrations/grpc/server.py | 91 + .../sentry_sdk/integrations/httpx.py | 263 ++ .../sentry_sdk/integrations/httpx2.py | 263 ++ .../sentry_sdk/integrations/huey.py | 239 + .../integrations/huggingface_hub.py | 392 ++ .../sentry_sdk/integrations/langchain.py | 1412 ++++++ .../sentry_sdk/integrations/langgraph.py | 515 +++ .../sentry_sdk/integrations/launchdarkly.py | 63 + .../sentry_sdk/integrations/litellm.py | 376 ++ .../sentry_sdk/integrations/litestar.py | 364 ++ .../sentry_sdk/integrations/logging.py | 476 ++ .../sentry_sdk/integrations/loguru.py | 208 + .../sentry_sdk/integrations/mcp.py | 876 ++++ .../sentry_sdk/integrations/modules.py | 28 + .../sentry_sdk/integrations/openai.py | 1530 +++++++ .../integrations/openai_agents/__init__.py | 250 ++ .../integrations/openai_agents/consts.py | 1 + .../openai_agents/patches/__init__.py | 10 + .../openai_agents/patches/agent_run.py | 332 ++ .../openai_agents/patches/error_tracing.py | 74 + .../openai_agents/patches/models.py | 197 + .../openai_agents/patches/runner.py | 221 + .../openai_agents/patches/tools.py | 70 + .../openai_agents/spans/__init__.py | 8 + .../openai_agents/spans/agent_workflow.py | 32 + .../openai_agents/spans/ai_client.py | 84 + .../openai_agents/spans/execute_tool.py | 82 + .../openai_agents/spans/handoff.py | 41 + .../openai_agents/spans/invoke_agent.py | 122 + .../integrations/openai_agents/utils.py | 244 ++ .../sentry_sdk/integrations/openfeature.py | 34 + .../integrations/opentelemetry/__init__.py | 7 + .../integrations/opentelemetry/consts.py | 9 + .../integrations/opentelemetry/integration.py | 81 + .../integrations/opentelemetry/propagator.py | 128 + .../opentelemetry/span_processor.py | 407 ++ .../sentry_sdk/integrations/otlp.py | 223 + .../sentry_sdk/integrations/pure_eval.py | 134 + .../integrations/pydantic_ai/__init__.py | 187 + .../integrations/pydantic_ai/consts.py | 1 + .../pydantic_ai/patches/__init__.py | 3 + .../pydantic_ai/patches/agent_run.py | 198 + .../pydantic_ai/patches/graph_nodes.py | 106 + .../integrations/pydantic_ai/patches/tools.py | 177 + .../pydantic_ai/spans/__init__.py | 3 + .../pydantic_ai/spans/ai_client.py | 331 ++ .../pydantic_ai/spans/execute_tool.py | 84 + .../pydantic_ai/spans/invoke_agent.py | 184 + .../integrations/pydantic_ai/spans/utils.py | 87 + .../integrations/pydantic_ai/utils.py | 233 + .../sentry_sdk/integrations/pymongo.py | 262 ++ .../sentry_sdk/integrations/pyramid.py | 239 + .../sentry_sdk/integrations/pyreqwest.py | 197 + .../sentry_sdk/integrations/quart.py | 292 ++ .../sentry_sdk/integrations/ray.py | 240 + .../sentry_sdk/integrations/redis/__init__.py | 49 + .../integrations/redis/_async_common.py | 177 + .../integrations/redis/_sync_common.py | 176 + .../sentry_sdk/integrations/redis/consts.py | 19 + .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 + .../integrations/redis/modules/queries.py | 88 + .../sentry_sdk/integrations/redis/rb.py | 31 + .../sentry_sdk/integrations/redis/redis.py | 67 + .../integrations/redis/redis_cluster.py | 115 + .../redis/redis_py_cluster_legacy.py | 49 + .../sentry_sdk/integrations/redis/utils.py | 159 + .../sentry_sdk/integrations/rq.py | 225 + .../sentry_sdk/integrations/rust_tracing.py | 300 ++ .../sentry_sdk/integrations/sanic.py | 431 ++ .../sentry_sdk/integrations/serverless.py | 65 + .../sentry_sdk/integrations/socket.py | 142 + .../sentry_sdk/integrations/spark/__init__.py | 4 + .../integrations/spark/spark_driver.py | 278 ++ .../integrations/spark/spark_worker.py | 109 + .../sentry_sdk/integrations/sqlalchemy.py | 185 + .../sentry_sdk/integrations/starlette.py | 858 ++++ .../sentry_sdk/integrations/starlite.py | 314 ++ .../sentry_sdk/integrations/statsig.py | 37 + .../sentry_sdk/integrations/stdlib.py | 404 ++ .../sentry_sdk/integrations/strawberry.py | 493 +++ .../sentry_sdk/integrations/sys_exit.py | 65 + .../sentry_sdk/integrations/threading.py | 196 + .../sentry_sdk/integrations/tornado.py | 320 ++ .../sentry_sdk/integrations/trytond.py | 52 + .../sentry_sdk/integrations/typer.py | 58 + .../sentry_sdk/integrations/unleash.py | 33 + .../sentry_sdk/integrations/unraisablehook.py | 50 + .../sentry_sdk/integrations/wsgi.py | 427 ++ .../sentry_sdk/logger.py | 88 + .../sentry_sdk/metrics.py | 62 + .../sentry_sdk/monitor.py | 138 + .../sentry_sdk/profiler/__init__.py | 49 + .../profiler/continuous_profiler.py | 703 +++ .../profiler/transaction_profiler.py | 802 ++++ .../sentry_sdk/profiler/utils.py | 186 + .../sentry_sdk/py.typed | 0 .../sentry_sdk/scope.py | 2185 ++++++++++ .../sentry_sdk/scrubber.py | 176 + .../sentry_sdk/serializer.py | 418 ++ .../sentry_sdk/session.py | 165 + .../sentry_sdk/sessions.py | 262 ++ .../sentry_sdk/spotlight.py | 327 ++ .../sentry_sdk/traces.py | 843 ++++ .../sentry_sdk/tracing.py | 1475 +++++++ .../sentry_sdk/tracing_utils.py | 1694 ++++++++ .../sentry_sdk/transport.py | 1255 ++++++ .../sentry_sdk/types.py | 52 + .../sentry_sdk/utils.py | 2178 ++++++++++ .../sentry_sdk/worker.py | 308 ++ .../urllib3-2.7.0.dist-info/INSTALLER | 1 + .../urllib3-2.7.0.dist-info/METADATA | 163 + .../urllib3-2.7.0.dist-info/RECORD | 44 + .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 + .../licenses/LICENSE.txt | 21 + .../urllib3/__init__.py | 211 + .../urllib3/_base_connection.py | 167 + .../urllib3/_collections.py | 486 +++ .../urllib3/_request_methods.py | 278 ++ .../urllib3/_version.py | 24 + .../urllib3/connection.py | 1099 +++++ .../urllib3/connectionpool.py | 1191 +++++ .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 + .../urllib3/contrib/emscripten/connection.py | 260 ++ .../emscripten/emscripten_fetch_worker.js | 110 + .../urllib3/contrib/emscripten/fetch.py | 726 ++++ .../urllib3/contrib/emscripten/request.py | 22 + .../urllib3/contrib/emscripten/response.py | 281 ++ .../urllib3/contrib/pyopenssl.py | 563 +++ .../urllib3/contrib/socks.py | 228 + .../urllib3/exceptions.py | 335 ++ .../urllib3/fields.py | 341 ++ .../urllib3/filepost.py | 89 + .../urllib3/http2/__init__.py | 53 + .../urllib3/http2/connection.py | 356 ++ .../urllib3/http2/probe.py | 87 + .../urllib3/poolmanager.py | 653 +++ .../urllib3/py.typed | 2 + .../urllib3/response.py | 1493 +++++++ .../urllib3/util/__init__.py | 42 + .../urllib3/util/connection.py | 137 + .../urllib3/util/proxy.py | 43 + .../urllib3/util/request.py | 263 ++ .../urllib3/util/response.py | 101 + .../urllib3/util/retry.py | 557 +++ .../urllib3/util/ssl_.py | 477 ++ .../urllib3/util/ssl_match_hostname.py | 153 + .../urllib3/util/ssltransport.py | 271 ++ .../urllib3/util/timeout.py | 275 ++ .../urllib3/util/url.py | 469 ++ .../urllib3/util/util.py | 42 + .../urllib3/util/wait.py | 124 + .../BasicOkDataCollectionOff/.lock | 0 .../certifi-2026.6.17.dist-info/INSTALLER | 1 + .../certifi-2026.6.17.dist-info/METADATA | 78 + .../certifi-2026.6.17.dist-info/RECORD | 12 + .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../certifi-2026.6.17.dist-info/top_level.txt | 1 + .../certifi/__init__.py | 4 + .../certifi/__main__.py | 12 + .../certifi/cacert.pem | 3863 +++++++++++++++++ .../BasicOkDataCollectionOff/certifi/core.py | 83 + .../BasicOkDataCollectionOff/certifi/py.typed | 0 .../BasicOkDataCollectionOff/index.py | 21 + .../sentry_sdk/__init__.py | 70 + .../sentry_sdk/_batcher.py | 184 + .../sentry_sdk/_compat.py | 92 + .../sentry_sdk/_init_implementation.py | 78 + .../sentry_sdk/_log_batcher.py | 66 + .../sentry_sdk/_lru_cache.py | 43 + .../sentry_sdk/_metrics_batcher.py | 48 + .../sentry_sdk/_queue.py | 287 ++ .../sentry_sdk/_span_batcher.py | 234 + .../sentry_sdk/_types.py | 481 ++ .../sentry_sdk/_werkzeug.py | 100 + .../sentry_sdk/ai/__init__.py | 8 + .../sentry_sdk/ai/_openai_completions_api.py | 66 + .../sentry_sdk/ai/_openai_responses_api.py | 22 + .../sentry_sdk/ai/consts.py | 6 + .../sentry_sdk/ai/monitoring.py | 147 + .../sentry_sdk/ai/utils.py | 782 ++++ .../sentry_sdk/api.py | 573 +++ .../sentry_sdk/attachments.py | 71 + .../sentry_sdk/client.py | 1479 +++++++ .../sentry_sdk/consts.py | 1802 ++++++++ .../sentry_sdk/crons/__init__.py | 9 + .../sentry_sdk/crons/api.py | 60 + .../sentry_sdk/crons/consts.py | 4 + .../sentry_sdk/crons/decorator.py | 137 + .../sentry_sdk/data_collection.py | 319 ++ .../sentry_sdk/debug.py | 37 + .../sentry_sdk/envelope.py | 332 ++ .../sentry_sdk/feature_flags.py | 75 + .../sentry_sdk/hub.py | 741 ++++ .../sentry_sdk/integrations/__init__.py | 352 ++ .../sentry_sdk/integrations/_asgi_common.py | 187 + .../sentry_sdk/integrations/_wsgi_common.py | 282 ++ .../sentry_sdk/integrations/aiohttp.py | 509 +++ .../sentry_sdk/integrations/aiomysql.py | 275 ++ .../sentry_sdk/integrations/anthropic.py | 1154 +++++ .../sentry_sdk/integrations/argv.py | 28 + .../sentry_sdk/integrations/ariadne.py | 167 + .../sentry_sdk/integrations/arq.py | 277 ++ .../sentry_sdk/integrations/asgi.py | 543 +++ .../sentry_sdk/integrations/asyncio.py | 280 ++ .../sentry_sdk/integrations/asyncpg.py | 312 ++ .../sentry_sdk/integrations/atexit.py | 51 + .../sentry_sdk/integrations/aws_lambda.py | 542 +++ .../sentry_sdk/integrations/beam.py | 164 + .../sentry_sdk/integrations/boto3.py | 187 + .../sentry_sdk/integrations/bottle.py | 239 + .../integrations/celery/__init__.py | 612 +++ .../sentry_sdk/integrations/celery/beat.py | 291 ++ .../sentry_sdk/integrations/celery/utils.py | 32 + .../sentry_sdk/integrations/chalice.py | 188 + .../integrations/clickhouse_driver.py | 211 + .../integrations/cloud_resource_context.py | 273 ++ .../sentry_sdk/integrations/cohere.py | 303 ++ .../sentry_sdk/integrations/dedupe.py | 62 + .../integrations/django/__init__.py | 884 ++++ .../sentry_sdk/integrations/django/asgi.py | 262 ++ .../sentry_sdk/integrations/django/caching.py | 264 ++ .../integrations/django/middleware.py | 219 + .../integrations/django/signals_handlers.py | 105 + .../sentry_sdk/integrations/django/tasks.py | 52 + .../integrations/django/templates.py | 209 + .../integrations/django/transactions.py | 154 + .../sentry_sdk/integrations/django/views.py | 127 + .../sentry_sdk/integrations/dramatiq.py | 246 ++ .../sentry_sdk/integrations/excepthook.py | 76 + .../sentry_sdk/integrations/executing.py | 66 + .../sentry_sdk/integrations/falcon.py | 278 ++ .../sentry_sdk/integrations/fastapi.py | 201 + .../sentry_sdk/integrations/flask.py | 281 ++ .../sentry_sdk/integrations/gcp.py | 286 ++ .../sentry_sdk/integrations/gnu_backtrace.py | 96 + .../integrations/google_genai/__init__.py | 457 ++ .../integrations/google_genai/consts.py | 16 + .../integrations/google_genai/streaming.py | 172 + .../integrations/google_genai/utils.py | 1118 +++++ .../sentry_sdk/integrations/gql.py | 173 + .../sentry_sdk/integrations/graphene.py | 181 + .../sentry_sdk/integrations/grpc/__init__.py | 188 + .../integrations/grpc/aio/__init__.py | 7 + .../integrations/grpc/aio/client.py | 146 + .../integrations/grpc/aio/server.py | 134 + .../sentry_sdk/integrations/grpc/client.py | 149 + .../sentry_sdk/integrations/grpc/consts.py | 1 + .../sentry_sdk/integrations/grpc/server.py | 91 + .../sentry_sdk/integrations/httpx.py | 263 ++ .../sentry_sdk/integrations/httpx2.py | 263 ++ .../sentry_sdk/integrations/huey.py | 239 + .../integrations/huggingface_hub.py | 392 ++ .../sentry_sdk/integrations/langchain.py | 1412 ++++++ .../sentry_sdk/integrations/langgraph.py | 515 +++ .../sentry_sdk/integrations/launchdarkly.py | 63 + .../sentry_sdk/integrations/litellm.py | 376 ++ .../sentry_sdk/integrations/litestar.py | 364 ++ .../sentry_sdk/integrations/logging.py | 476 ++ .../sentry_sdk/integrations/loguru.py | 208 + .../sentry_sdk/integrations/mcp.py | 876 ++++ .../sentry_sdk/integrations/modules.py | 28 + .../sentry_sdk/integrations/openai.py | 1530 +++++++ .../integrations/openai_agents/__init__.py | 250 ++ .../integrations/openai_agents/consts.py | 1 + .../openai_agents/patches/__init__.py | 10 + .../openai_agents/patches/agent_run.py | 332 ++ .../openai_agents/patches/error_tracing.py | 74 + .../openai_agents/patches/models.py | 197 + .../openai_agents/patches/runner.py | 221 + .../openai_agents/patches/tools.py | 70 + .../openai_agents/spans/__init__.py | 8 + .../openai_agents/spans/agent_workflow.py | 32 + .../openai_agents/spans/ai_client.py | 84 + .../openai_agents/spans/execute_tool.py | 82 + .../openai_agents/spans/handoff.py | 41 + .../openai_agents/spans/invoke_agent.py | 122 + .../integrations/openai_agents/utils.py | 244 ++ .../sentry_sdk/integrations/openfeature.py | 34 + .../integrations/opentelemetry/__init__.py | 7 + .../integrations/opentelemetry/consts.py | 9 + .../integrations/opentelemetry/integration.py | 81 + .../integrations/opentelemetry/propagator.py | 128 + .../opentelemetry/span_processor.py | 407 ++ .../sentry_sdk/integrations/otlp.py | 223 + .../sentry_sdk/integrations/pure_eval.py | 134 + .../integrations/pydantic_ai/__init__.py | 187 + .../integrations/pydantic_ai/consts.py | 1 + .../pydantic_ai/patches/__init__.py | 3 + .../pydantic_ai/patches/agent_run.py | 198 + .../pydantic_ai/patches/graph_nodes.py | 106 + .../integrations/pydantic_ai/patches/tools.py | 177 + .../pydantic_ai/spans/__init__.py | 3 + .../pydantic_ai/spans/ai_client.py | 331 ++ .../pydantic_ai/spans/execute_tool.py | 84 + .../pydantic_ai/spans/invoke_agent.py | 184 + .../integrations/pydantic_ai/spans/utils.py | 87 + .../integrations/pydantic_ai/utils.py | 233 + .../sentry_sdk/integrations/pymongo.py | 262 ++ .../sentry_sdk/integrations/pyramid.py | 239 + .../sentry_sdk/integrations/pyreqwest.py | 197 + .../sentry_sdk/integrations/quart.py | 292 ++ .../sentry_sdk/integrations/ray.py | 240 + .../sentry_sdk/integrations/redis/__init__.py | 49 + .../integrations/redis/_async_common.py | 177 + .../integrations/redis/_sync_common.py | 176 + .../sentry_sdk/integrations/redis/consts.py | 19 + .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 + .../integrations/redis/modules/queries.py | 88 + .../sentry_sdk/integrations/redis/rb.py | 31 + .../sentry_sdk/integrations/redis/redis.py | 67 + .../integrations/redis/redis_cluster.py | 115 + .../redis/redis_py_cluster_legacy.py | 49 + .../sentry_sdk/integrations/redis/utils.py | 159 + .../sentry_sdk/integrations/rq.py | 225 + .../sentry_sdk/integrations/rust_tracing.py | 300 ++ .../sentry_sdk/integrations/sanic.py | 431 ++ .../sentry_sdk/integrations/serverless.py | 65 + .../sentry_sdk/integrations/socket.py | 142 + .../sentry_sdk/integrations/spark/__init__.py | 4 + .../integrations/spark/spark_driver.py | 278 ++ .../integrations/spark/spark_worker.py | 109 + .../sentry_sdk/integrations/sqlalchemy.py | 185 + .../sentry_sdk/integrations/starlette.py | 858 ++++ .../sentry_sdk/integrations/starlite.py | 314 ++ .../sentry_sdk/integrations/statsig.py | 37 + .../sentry_sdk/integrations/stdlib.py | 404 ++ .../sentry_sdk/integrations/strawberry.py | 493 +++ .../sentry_sdk/integrations/sys_exit.py | 65 + .../sentry_sdk/integrations/threading.py | 196 + .../sentry_sdk/integrations/tornado.py | 320 ++ .../sentry_sdk/integrations/trytond.py | 52 + .../sentry_sdk/integrations/typer.py | 58 + .../sentry_sdk/integrations/unleash.py | 33 + .../sentry_sdk/integrations/unraisablehook.py | 50 + .../sentry_sdk/integrations/wsgi.py | 427 ++ .../sentry_sdk/logger.py | 88 + .../sentry_sdk/metrics.py | 62 + .../sentry_sdk/monitor.py | 138 + .../sentry_sdk/profiler/__init__.py | 49 + .../profiler/continuous_profiler.py | 703 +++ .../profiler/transaction_profiler.py | 802 ++++ .../sentry_sdk/profiler/utils.py | 186 + .../sentry_sdk/py.typed | 0 .../sentry_sdk/scope.py | 2185 ++++++++++ .../sentry_sdk/scrubber.py | 176 + .../sentry_sdk/serializer.py | 418 ++ .../sentry_sdk/session.py | 165 + .../sentry_sdk/sessions.py | 262 ++ .../sentry_sdk/spotlight.py | 327 ++ .../sentry_sdk/traces.py | 843 ++++ .../sentry_sdk/tracing.py | 1475 +++++++ .../sentry_sdk/tracing_utils.py | 1694 ++++++++ .../sentry_sdk/transport.py | 1255 ++++++ .../sentry_sdk/types.py | 52 + .../sentry_sdk/utils.py | 2178 ++++++++++ .../sentry_sdk/worker.py | 308 ++ .../urllib3-2.7.0.dist-info/INSTALLER | 1 + .../urllib3-2.7.0.dist-info/METADATA | 163 + .../urllib3-2.7.0.dist-info/RECORD | 44 + .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 + .../licenses/LICENSE.txt | 21 + .../urllib3/__init__.py | 211 + .../urllib3/_base_connection.py | 167 + .../urllib3/_collections.py | 486 +++ .../urllib3/_request_methods.py | 278 ++ .../urllib3/_version.py | 24 + .../urllib3/connection.py | 1099 +++++ .../urllib3/connectionpool.py | 1191 +++++ .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 + .../urllib3/contrib/emscripten/connection.py | 260 ++ .../emscripten/emscripten_fetch_worker.js | 110 + .../urllib3/contrib/emscripten/fetch.py | 726 ++++ .../urllib3/contrib/emscripten/request.py | 22 + .../urllib3/contrib/emscripten/response.py | 281 ++ .../urllib3/contrib/pyopenssl.py | 563 +++ .../urllib3/contrib/socks.py | 228 + .../urllib3/exceptions.py | 335 ++ .../urllib3/fields.py | 341 ++ .../urllib3/filepost.py | 89 + .../urllib3/http2/__init__.py | 53 + .../urllib3/http2/connection.py | 356 ++ .../urllib3/http2/probe.py | 87 + .../urllib3/poolmanager.py | 653 +++ .../BasicOkDataCollectionOff/urllib3/py.typed | 2 + .../urllib3/response.py | 1493 +++++++ .../urllib3/util/__init__.py | 42 + .../urllib3/util/connection.py | 137 + .../urllib3/util/proxy.py | 43 + .../urllib3/util/request.py | 263 ++ .../urllib3/util/response.py | 101 + .../urllib3/util/retry.py | 557 +++ .../urllib3/util/ssl_.py | 477 ++ .../urllib3/util/ssl_match_hostname.py | 153 + .../urllib3/util/ssltransport.py | 271 ++ .../urllib3/util/timeout.py | 275 ++ .../urllib3/util/url.py | 469 ++ .../urllib3/util/util.py | 42 + .../urllib3/util/wait.py | 124 + .../BasicOkSendDefaultPii/.lock | 0 .../certifi-2026.6.17.dist-info/INSTALLER | 1 + .../certifi-2026.6.17.dist-info/METADATA | 78 + .../certifi-2026.6.17.dist-info/RECORD | 12 + .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../certifi-2026.6.17.dist-info/top_level.txt | 1 + .../BasicOkSendDefaultPii/certifi/__init__.py | 4 + .../BasicOkSendDefaultPii/certifi/__main__.py | 12 + .../BasicOkSendDefaultPii/certifi/cacert.pem | 3863 +++++++++++++++++ .../BasicOkSendDefaultPii/certifi/core.py | 83 + .../BasicOkSendDefaultPii/certifi/py.typed | 0 .../BasicOkSendDefaultPii/index.py | 15 + .../sentry_sdk/__init__.py | 70 + .../sentry_sdk/_batcher.py | 184 + .../sentry_sdk/_compat.py | 92 + .../sentry_sdk/_init_implementation.py | 78 + .../sentry_sdk/_log_batcher.py | 66 + .../sentry_sdk/_lru_cache.py | 43 + .../sentry_sdk/_metrics_batcher.py | 48 + .../sentry_sdk/_queue.py | 287 ++ .../sentry_sdk/_span_batcher.py | 234 + .../sentry_sdk/_types.py | 481 ++ .../sentry_sdk/_werkzeug.py | 100 + .../sentry_sdk/ai/__init__.py | 8 + .../sentry_sdk/ai/_openai_completions_api.py | 66 + .../sentry_sdk/ai/_openai_responses_api.py | 22 + .../sentry_sdk/ai/consts.py | 6 + .../sentry_sdk/ai/monitoring.py | 147 + .../sentry_sdk/ai/utils.py | 782 ++++ .../BasicOkSendDefaultPii/sentry_sdk/api.py | 573 +++ .../sentry_sdk/attachments.py | 71 + .../sentry_sdk/client.py | 1479 +++++++ .../sentry_sdk/consts.py | 1802 ++++++++ .../sentry_sdk/crons/__init__.py | 9 + .../sentry_sdk/crons/api.py | 60 + .../sentry_sdk/crons/consts.py | 4 + .../sentry_sdk/crons/decorator.py | 137 + .../sentry_sdk/data_collection.py | 319 ++ .../BasicOkSendDefaultPii/sentry_sdk/debug.py | 37 + .../sentry_sdk/envelope.py | 332 ++ .../sentry_sdk/feature_flags.py | 75 + .../BasicOkSendDefaultPii/sentry_sdk/hub.py | 741 ++++ .../sentry_sdk/integrations/__init__.py | 352 ++ .../sentry_sdk/integrations/_asgi_common.py | 187 + .../sentry_sdk/integrations/_wsgi_common.py | 282 ++ .../sentry_sdk/integrations/aiohttp.py | 509 +++ .../sentry_sdk/integrations/aiomysql.py | 275 ++ .../sentry_sdk/integrations/anthropic.py | 1154 +++++ .../sentry_sdk/integrations/argv.py | 28 + .../sentry_sdk/integrations/ariadne.py | 167 + .../sentry_sdk/integrations/arq.py | 277 ++ .../sentry_sdk/integrations/asgi.py | 543 +++ .../sentry_sdk/integrations/asyncio.py | 280 ++ .../sentry_sdk/integrations/asyncpg.py | 312 ++ .../sentry_sdk/integrations/atexit.py | 51 + .../sentry_sdk/integrations/aws_lambda.py | 542 +++ .../sentry_sdk/integrations/beam.py | 164 + .../sentry_sdk/integrations/boto3.py | 187 + .../sentry_sdk/integrations/bottle.py | 239 + .../integrations/celery/__init__.py | 612 +++ .../sentry_sdk/integrations/celery/beat.py | 291 ++ .../sentry_sdk/integrations/celery/utils.py | 32 + .../sentry_sdk/integrations/chalice.py | 188 + .../integrations/clickhouse_driver.py | 211 + .../integrations/cloud_resource_context.py | 273 ++ .../sentry_sdk/integrations/cohere.py | 303 ++ .../sentry_sdk/integrations/dedupe.py | 62 + .../integrations/django/__init__.py | 884 ++++ .../sentry_sdk/integrations/django/asgi.py | 262 ++ .../sentry_sdk/integrations/django/caching.py | 264 ++ .../integrations/django/middleware.py | 219 + .../integrations/django/signals_handlers.py | 105 + .../sentry_sdk/integrations/django/tasks.py | 52 + .../integrations/django/templates.py | 209 + .../integrations/django/transactions.py | 154 + .../sentry_sdk/integrations/django/views.py | 127 + .../sentry_sdk/integrations/dramatiq.py | 246 ++ .../sentry_sdk/integrations/excepthook.py | 76 + .../sentry_sdk/integrations/executing.py | 66 + .../sentry_sdk/integrations/falcon.py | 278 ++ .../sentry_sdk/integrations/fastapi.py | 201 + .../sentry_sdk/integrations/flask.py | 281 ++ .../sentry_sdk/integrations/gcp.py | 286 ++ .../sentry_sdk/integrations/gnu_backtrace.py | 96 + .../integrations/google_genai/__init__.py | 457 ++ .../integrations/google_genai/consts.py | 16 + .../integrations/google_genai/streaming.py | 172 + .../integrations/google_genai/utils.py | 1118 +++++ .../sentry_sdk/integrations/gql.py | 173 + .../sentry_sdk/integrations/graphene.py | 181 + .../sentry_sdk/integrations/grpc/__init__.py | 188 + .../integrations/grpc/aio/__init__.py | 7 + .../integrations/grpc/aio/client.py | 146 + .../integrations/grpc/aio/server.py | 134 + .../sentry_sdk/integrations/grpc/client.py | 149 + .../sentry_sdk/integrations/grpc/consts.py | 1 + .../sentry_sdk/integrations/grpc/server.py | 91 + .../sentry_sdk/integrations/httpx.py | 263 ++ .../sentry_sdk/integrations/httpx2.py | 263 ++ .../sentry_sdk/integrations/huey.py | 239 + .../integrations/huggingface_hub.py | 392 ++ .../sentry_sdk/integrations/langchain.py | 1412 ++++++ .../sentry_sdk/integrations/langgraph.py | 515 +++ .../sentry_sdk/integrations/launchdarkly.py | 63 + .../sentry_sdk/integrations/litellm.py | 376 ++ .../sentry_sdk/integrations/litestar.py | 364 ++ .../sentry_sdk/integrations/logging.py | 476 ++ .../sentry_sdk/integrations/loguru.py | 208 + .../sentry_sdk/integrations/mcp.py | 876 ++++ .../sentry_sdk/integrations/modules.py | 28 + .../sentry_sdk/integrations/openai.py | 1530 +++++++ .../integrations/openai_agents/__init__.py | 250 ++ .../integrations/openai_agents/consts.py | 1 + .../openai_agents/patches/__init__.py | 10 + .../openai_agents/patches/agent_run.py | 332 ++ .../openai_agents/patches/error_tracing.py | 74 + .../openai_agents/patches/models.py | 197 + .../openai_agents/patches/runner.py | 221 + .../openai_agents/patches/tools.py | 70 + .../openai_agents/spans/__init__.py | 8 + .../openai_agents/spans/agent_workflow.py | 32 + .../openai_agents/spans/ai_client.py | 84 + .../openai_agents/spans/execute_tool.py | 82 + .../openai_agents/spans/handoff.py | 41 + .../openai_agents/spans/invoke_agent.py | 122 + .../integrations/openai_agents/utils.py | 244 ++ .../sentry_sdk/integrations/openfeature.py | 34 + .../integrations/opentelemetry/__init__.py | 7 + .../integrations/opentelemetry/consts.py | 9 + .../integrations/opentelemetry/integration.py | 81 + .../integrations/opentelemetry/propagator.py | 128 + .../opentelemetry/span_processor.py | 407 ++ .../sentry_sdk/integrations/otlp.py | 223 + .../sentry_sdk/integrations/pure_eval.py | 134 + .../integrations/pydantic_ai/__init__.py | 187 + .../integrations/pydantic_ai/consts.py | 1 + .../pydantic_ai/patches/__init__.py | 3 + .../pydantic_ai/patches/agent_run.py | 198 + .../pydantic_ai/patches/graph_nodes.py | 106 + .../integrations/pydantic_ai/patches/tools.py | 177 + .../pydantic_ai/spans/__init__.py | 3 + .../pydantic_ai/spans/ai_client.py | 331 ++ .../pydantic_ai/spans/execute_tool.py | 84 + .../pydantic_ai/spans/invoke_agent.py | 184 + .../integrations/pydantic_ai/spans/utils.py | 87 + .../integrations/pydantic_ai/utils.py | 233 + .../sentry_sdk/integrations/pymongo.py | 262 ++ .../sentry_sdk/integrations/pyramid.py | 239 + .../sentry_sdk/integrations/pyreqwest.py | 197 + .../sentry_sdk/integrations/quart.py | 292 ++ .../sentry_sdk/integrations/ray.py | 240 + .../sentry_sdk/integrations/redis/__init__.py | 49 + .../integrations/redis/_async_common.py | 177 + .../integrations/redis/_sync_common.py | 176 + .../sentry_sdk/integrations/redis/consts.py | 19 + .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 + .../integrations/redis/modules/queries.py | 88 + .../sentry_sdk/integrations/redis/rb.py | 31 + .../sentry_sdk/integrations/redis/redis.py | 67 + .../integrations/redis/redis_cluster.py | 115 + .../redis/redis_py_cluster_legacy.py | 49 + .../sentry_sdk/integrations/redis/utils.py | 159 + .../sentry_sdk/integrations/rq.py | 225 + .../sentry_sdk/integrations/rust_tracing.py | 300 ++ .../sentry_sdk/integrations/sanic.py | 431 ++ .../sentry_sdk/integrations/serverless.py | 65 + .../sentry_sdk/integrations/socket.py | 142 + .../sentry_sdk/integrations/spark/__init__.py | 4 + .../integrations/spark/spark_driver.py | 278 ++ .../integrations/spark/spark_worker.py | 109 + .../sentry_sdk/integrations/sqlalchemy.py | 185 + .../sentry_sdk/integrations/starlette.py | 858 ++++ .../sentry_sdk/integrations/starlite.py | 314 ++ .../sentry_sdk/integrations/statsig.py | 37 + .../sentry_sdk/integrations/stdlib.py | 404 ++ .../sentry_sdk/integrations/strawberry.py | 493 +++ .../sentry_sdk/integrations/sys_exit.py | 65 + .../sentry_sdk/integrations/threading.py | 196 + .../sentry_sdk/integrations/tornado.py | 320 ++ .../sentry_sdk/integrations/trytond.py | 52 + .../sentry_sdk/integrations/typer.py | 58 + .../sentry_sdk/integrations/unleash.py | 33 + .../sentry_sdk/integrations/unraisablehook.py | 50 + .../sentry_sdk/integrations/wsgi.py | 427 ++ .../sentry_sdk/logger.py | 88 + .../sentry_sdk/metrics.py | 62 + .../sentry_sdk/monitor.py | 138 + .../sentry_sdk/profiler/__init__.py | 49 + .../profiler/continuous_profiler.py | 703 +++ .../profiler/transaction_profiler.py | 802 ++++ .../sentry_sdk/profiler/utils.py | 186 + .../BasicOkSendDefaultPii/sentry_sdk/py.typed | 0 .../BasicOkSendDefaultPii/sentry_sdk/scope.py | 2185 ++++++++++ .../sentry_sdk/scrubber.py | 176 + .../sentry_sdk/serializer.py | 418 ++ .../sentry_sdk/session.py | 165 + .../sentry_sdk/sessions.py | 262 ++ .../sentry_sdk/spotlight.py | 327 ++ .../sentry_sdk/traces.py | 843 ++++ .../sentry_sdk/tracing.py | 1475 +++++++ .../sentry_sdk/tracing_utils.py | 1694 ++++++++ .../sentry_sdk/transport.py | 1255 ++++++ .../BasicOkSendDefaultPii/sentry_sdk/types.py | 52 + .../BasicOkSendDefaultPii/sentry_sdk/utils.py | 2178 ++++++++++ .../sentry_sdk/worker.py | 308 ++ .../urllib3-2.7.0.dist-info/INSTALLER | 1 + .../urllib3-2.7.0.dist-info/METADATA | 163 + .../urllib3-2.7.0.dist-info/RECORD | 44 + .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 + .../licenses/LICENSE.txt | 21 + .../BasicOkSendDefaultPii/urllib3/__init__.py | 211 + .../urllib3/_base_connection.py | 167 + .../urllib3/_collections.py | 486 +++ .../urllib3/_request_methods.py | 278 ++ .../BasicOkSendDefaultPii/urllib3/_version.py | 24 + .../urllib3/connection.py | 1099 +++++ .../urllib3/connectionpool.py | 1191 +++++ .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 + .../urllib3/contrib/emscripten/connection.py | 260 ++ .../emscripten/emscripten_fetch_worker.js | 110 + .../urllib3/contrib/emscripten/fetch.py | 726 ++++ .../urllib3/contrib/emscripten/request.py | 22 + .../urllib3/contrib/emscripten/response.py | 281 ++ .../urllib3/contrib/pyopenssl.py | 563 +++ .../urllib3/contrib/socks.py | 228 + .../urllib3/exceptions.py | 335 ++ .../BasicOkSendDefaultPii/urllib3/fields.py | 341 ++ .../BasicOkSendDefaultPii/urllib3/filepost.py | 89 + .../urllib3/http2/__init__.py | 53 + .../urllib3/http2/connection.py | 356 ++ .../urllib3/http2/probe.py | 87 + .../urllib3/poolmanager.py | 653 +++ .../BasicOkSendDefaultPii/urllib3/py.typed | 2 + .../BasicOkSendDefaultPii/urllib3/response.py | 1493 +++++++ .../urllib3/util/__init__.py | 42 + .../urllib3/util/connection.py | 137 + .../urllib3/util/proxy.py | 43 + .../urllib3/util/request.py | 263 ++ .../urllib3/util/response.py | 101 + .../urllib3/util/retry.py | 557 +++ .../urllib3/util/ssl_.py | 477 ++ .../urllib3/util/ssl_match_hostname.py | 153 + .../urllib3/util/ssltransport.py | 271 ++ .../urllib3/util/timeout.py | 275 ++ .../BasicOkSendDefaultPii/urllib3/util/url.py | 469 ++ .../urllib3/util/util.py | 42 + .../urllib3/util/wait.py | 124 + .../aws_lambda/test_aws_lambda.py | 229 +- 1009 files changed, 284112 insertions(+), 2 deletions(-) create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.lock create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.lock create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.lock create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.lock create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/REQUESTED create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.lock b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER new file mode 100644 index 0000000000..5c69047b2e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA new file mode 100644 index 0000000000..0d4f101491 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.6.17 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/latest/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD new file mode 100644 index 0000000000..13b0ce7da4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD @@ -0,0 +1,12 @@ +certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 +certifi-2026.6.17.dist-info/RECORD,, +certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL new file mode 100644 index 0000000000..14a883f292 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE new file mode 100644 index 0000000000..62b076cdee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py new file mode 100644 index 0000000000..ed9a74b7a0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py new file mode 100644 index 0000000000..8945b5da85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem new file mode 100644 index 0000000000..1c2dbfeb68 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem @@ -0,0 +1,3863 @@ + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Label: "OISTE Server Root ECC G1" +# Serial: 47819833811561661340092227008453318557 +# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 +# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 +# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Label: "OISTE Server Root RSA G1" +# Serial: 113845518112613905024960613408179309848 +# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 +# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e +# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Label: "e-Szigno TLS Root CA 2023" +# Serial: 71934828665710877219916191754 +# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 +# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe +# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py new file mode 100644 index 0000000000..1c9661cc7c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/index.py new file mode 100644 index 0000000000..a79e1a03a7 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/index.py @@ -0,0 +1,27 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={ + "data_collection": { + "http_headers": { + "request": { + "mode": "allowlist", + # "authorization" is allowlisted on purpose to show that an + # allowlist entry cannot override the built-in sensitive + # denylist. + "terms": ["user-agent", "x-allow-me", "authorization"], + } + } + } + }, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py new file mode 100644 index 0000000000..8ce8d739c9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py @@ -0,0 +1,70 @@ +from sentry_sdk import metrics, profiler + +from sentry_sdk.scope import Scope # isort: skip +from sentry_sdk.client import Client # isort: skip +from sentry_sdk.consts import VERSION +from sentry_sdk.transport import HttpTransport, Transport + +from sentry_sdk.api import * # noqa # isort: skip + +__all__ = [ # noqa + "Hub", + "Scope", + "Client", + "Transport", + "HttpTransport", + "VERSION", + "integrations", + # From sentry_sdk.api + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "logger", + "metrics", + "profiler", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + +# Initialize the debug support after everything is loaded +from sentry_sdk.debug import init_debug_support + +init_debug_support() +del init_debug_support + +# circular imports +from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py new file mode 100644 index 0000000000..565fac2a2d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py @@ -0,0 +1,184 @@ +import os +import random +import threading +import weakref +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Generic, TypeVar + +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + +T = TypeVar("T") + + +class Batcher(Generic[T]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "" + CONTENT_TYPE = "" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + self._buffer: "list[T]" = [] + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._flush_event: "threading.Event" = threading.Event() + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._buffer = [] + self._running = True + self._lock = threading.Lock() + self._active = threading.local() + self._flush_event = threading.Event() + self._flusher = None + self._flusher_pid = None + + def _ensure_thread(self) -> bool: + """For forking processes we might need to restart this thread. + This ensures that our process actually has that thread running. + """ + if not self._running: + return False + + pid = os.getpid() + if self._flusher_pid == pid: + return True + + with self._lock: + # Recheck to make sure another thread didn't get here and start the + # the flusher in the meantime + if self._flusher_pid == pid: + return True + + self._flusher_pid = pid + + self._flusher = threading.Thread(target=self._flush_loop) + self._flusher.daemon = True + + try: + self._flusher.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return False + + return True + + def _flush_loop(self) -> None: + # Mark the flush-loop thread as active for its entire lifetime so + # that any re-entrant add() triggered by GC warnings during wait(), + # flush(), or Event operations is silently dropped instead of + # deadlocking on internal locks. + self._active.flag = True + while self._running: + self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) + self._flush_event.clear() + self._flush() + + def add(self, item: "T") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + if len(self._buffer) >= self.MAX_BEFORE_DROP: + self._record_lost(item) + return None + + self._buffer.append(item) + if len(self._buffer) >= self.MAX_BEFORE_FLUSH: + self._flush_event.set() + finally: + self._active.flag = False + + def kill(self) -> None: + if self._flusher is None: + return + + self._running = False + self._flush_event.set() + self._flusher = None + + def flush(self) -> None: + was_active = getattr(self._active, "flag", False) + self._active.flag = True + try: + self._flush() + finally: + self._active.flag = was_active + + def _add_to_envelope(self, envelope: "Envelope") -> None: + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": len(self._buffer), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(item) for item in self._buffer + ], + } + ), + ) + ) + + def _flush(self) -> "Optional[Envelope]": + envelope = Envelope( + headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + ) + with self._lock: + if len(self._buffer) == 0: + return None + + self._add_to_envelope(envelope) + self._buffer.clear() + + self._capture_func(envelope) + return envelope + + def _record_lost(self, item: "T") -> None: + pass + + @staticmethod + def _to_transport_format(item: "T") -> "Any": + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py new file mode 100644 index 0000000000..f62175c09f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py @@ -0,0 +1,92 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, TypeVar + + T = TypeVar("T") + + +PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 +PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 +PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 +PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 + + +def with_metaclass(meta: "Any", *bases: "Any") -> "Any": + class MetaClass(type): + def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": + return meta(name, bases, d) + + return type.__new__(MetaClass, "temporary_class", (), {}) + + +def check_uwsgi_thread_support() -> bool: + # We check two things here: + # + # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if + # that's the case. + # + # 2. Additionally, if uWSGI is running in preforking mode (default), it needs + # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This + # is because any background threads spawned before the main process is + # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if + # --enable-threads is on. One has to explicitly provide + # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython + # after-fork hooks that take care of cleaning up stale thread data. + try: + from uwsgi import opt # type: ignore + except ImportError: + return True + + from sentry_sdk.consts import FALSE_VALUES + + def enabled(option: str) -> bool: + value = opt.get(option, False) + if isinstance(value, bool): + return value + + if isinstance(value, bytes): + try: + value = value.decode() + except Exception: + pass + + return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] + + # When `threads` is passed in as a uwsgi option, + # `enable-threads` is implied on. + threads_enabled = "threads" in opt or enabled("enable-threads") + fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") + lazy_mode = enabled("lazy-apps") or enabled("lazy") + + if lazy_mode and not threads_enabled: + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI without thread support. " + "This might lead to unexpected issues. " + 'Please run uWSGI with "--enable-threads" for full support.' + ) + ) + + return False + + elif not lazy_mode and (not threads_enabled or not fork_hooks_on): + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI in preforking mode without " + "thread support. This might lead to crashing workers. " + 'Please run uWSGI with both "--enable-threads" and ' + '"--py-call-uwsgi-fork-hooks" for full support.' + ) + ) + + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py new file mode 100644 index 0000000000..923fcf6df8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py @@ -0,0 +1,78 @@ +import warnings +from typing import TYPE_CHECKING + +import sentry_sdk + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional + + import sentry_sdk.consts + + +class _InitGuard: + _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( + "Using the return value of sentry_sdk.init as a context manager " + "and manually calling the __enter__ and __exit__ methods on the " + "return value are deprecated. We are no longer maintaining this " + "functionality, and we will remove it in the next major release." + ) + + def __init__(self, client: "sentry_sdk.Client") -> None: + self._client = client + + def __enter__(self) -> "_InitGuard": + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + c = self._client + if c is not None: + c.close() + + +def _check_python_deprecations() -> None: + # Since we're likely to deprecate Python versions in the future, I'm keeping + # this handy function around. Use this to detect the Python version used and + # to output logger.warning()s if it's deprecated. + pass + + +def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": + """Initializes the SDK and optionally integrations. + + This takes the same arguments as the client constructor. + """ + client = sentry_sdk.Client(*args, **kwargs) + sentry_sdk.get_global_scope().set_client(client) + _check_python_deprecations() + rv = _InitGuard(client) + return rv + + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `init` is a type to + # have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `ContextManager[Any]` to tell static analyzers about the return type. + + class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 + pass + +else: + # Alias `init` for actual usage. Go through the lambda indirection to throw + # PyCharm off of the weakly typed signature (it would otherwise discover + # both the weakly typed signature of `_init` and our faked `init` type). + + init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py new file mode 100644 index 0000000000..0719932ee9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Item, PayloadRef +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Log + + +class LogBatcher(Batcher["Log"]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "log" + CONTENT_TYPE = "application/vnd.sentry.items.log+json" + + @staticmethod + def _to_transport_format(item: "Log") -> "Any": + if "sentry.severity_number" not in item["attributes"]: + item["attributes"]["sentry.severity_number"] = item["severity_number"] + if "sentry.severity_text" not in item["attributes"]: + item["attributes"]["sentry.severity_text"] = item["severity_text"] + + res = { + "timestamp": int(item["time_unix_nano"]) / 1.0e9, + "level": str(item["severity_text"]), + "body": str(item["body"]), + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + return res + + def _record_lost(self, item: "Log") -> None: + # Construct log envelope item without sending it to report lost bytes + log_item = Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": 1, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [self._to_transport_format(item)], + } + ), + ) + + self._record_lost_func( + reason="queue_overflow", + data_category="log_item", + item=log_item, + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py new file mode 100644 index 0000000000..16c238bcab --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py @@ -0,0 +1,43 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +_SENTINEL = object() + + +class LRUCache: + def __init__(self, max_size: int) -> None: + if max_size <= 0: + raise AssertionError(f"invalid max_size: {max_size}") + self.max_size = max_size + self._data: "dict[Any, Any]" = {} + self.hits = self.misses = 0 + self.full = False + + def set(self, key: "Any", value: "Any") -> None: + current = self._data.pop(key, _SENTINEL) + if current is not _SENTINEL: + self._data[key] = value + elif self.full: + self._data.pop(next(iter(self._data))) + self._data[key] = value + else: + self._data[key] = value + self.full = len(self._data) >= self.max_size + + def get(self, key: "Any", default: "Any" = None) -> "Any": + try: + ret = self._data.pop(key) + except KeyError: + self.misses += 1 + ret = default + else: + self.hits += 1 + self._data[key] = ret + + return ret + + def get_all(self) -> "list[tuple[Any, Any]]": + return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py new file mode 100644 index 0000000000..06bce1a282 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Metric + + +class MetricsBatcher(Batcher["Metric"]): + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 10_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "trace_metric" + CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" + + @staticmethod + def _to_transport_format(item: "Metric") -> "Any": + res = { + "timestamp": item["timestamp"], + "name": item["name"], + "type": item["type"], + "value": item["value"], + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + if item.get("unit") is not None: + res["unit"] = item["unit"] + + return res + + def _record_lost(self, item: "Metric") -> None: + self._record_lost_func( + reason="queue_overflow", + data_category="trace_metric", + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py new file mode 100644 index 0000000000..c28c8de9ac --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py @@ -0,0 +1,287 @@ +""" +A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') +with Lock swapped out for RLock to avoid a deadlock while garbage collecting. + +https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py + + +See also +https://codewithoutrules.com/2017/08/16/concurrency-python/ +https://bugs.python.org/issue14976 +https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 + +We also vendor the code to evade eventlet's broken monkeypatching, see +https://github.com/getsentry/sentry-python/pull/484 + + +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; + +All Rights Reserved + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + +""" + +import threading +from collections import deque +from time import time +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +__all__ = ["EmptyError", "FullError", "Queue"] + + +class EmptyError(Exception): + "Exception raised by Queue.get(block=0)/get_nowait()." + + pass + + +class FullError(Exception): + "Exception raised by Queue.put(block=0)/put_nowait()." + + pass + + +class Queue: + """Create a queue object with a given maximum size. + + If maxsize is <= 0, the queue size is infinite. + """ + + def __init__(self, maxsize=0): + self.maxsize = maxsize + self._init(maxsize) + + # mutex must be held whenever the queue is mutating. All methods + # that acquire mutex must release it before returning. mutex + # is shared between the three conditions, so acquiring and + # releasing the conditions also acquires and releases mutex. + self.mutex = threading.RLock() + + # Notify not_empty whenever an item is added to the queue; a + # thread waiting to get is notified then. + self.not_empty = threading.Condition(self.mutex) + + # Notify not_full whenever an item is removed from the queue; + # a thread waiting to put is notified then. + self.not_full = threading.Condition(self.mutex) + + # Notify all_tasks_done whenever the number of unfinished tasks + # drops to zero; thread waiting to join() is notified to resume + self.all_tasks_done = threading.Condition(self.mutex) + self.unfinished_tasks = 0 + + def task_done(self): + """Indicate that a formerly enqueued task is complete. + + Used by Queue consumer threads. For each get() used to fetch a task, + a subsequent call to task_done() tells the queue that the processing + on the task is complete. + + If a join() is currently blocking, it will resume when all items + have been processed (meaning that a task_done() call was received + for every item that had been put() into the queue). + + Raises a ValueError if called more times than there were items + placed in the queue. + """ + with self.all_tasks_done: + unfinished = self.unfinished_tasks - 1 + if unfinished <= 0: + if unfinished < 0: + raise ValueError("task_done() called too many times") + self.all_tasks_done.notify_all() + self.unfinished_tasks = unfinished + + def join(self): + """Blocks until all items in the Queue have been gotten and processed. + + The count of unfinished tasks goes up whenever an item is added to the + queue. The count goes down whenever a consumer thread calls task_done() + to indicate the item was retrieved and all work on it is complete. + + When the count of unfinished tasks drops to zero, join() unblocks. + """ + with self.all_tasks_done: + while self.unfinished_tasks: + self.all_tasks_done.wait() + + def qsize(self): + """Return the approximate size of the queue (not reliable!).""" + with self.mutex: + return self._qsize() + + def empty(self): + """Return True if the queue is empty, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() == 0 + as a direct substitute, but be aware that either approach risks a race + condition where a queue can grow before the result of empty() or + qsize() can be used. + + To create code that needs to wait for all queued tasks to be + completed, the preferred technique is to use the join() method. + """ + with self.mutex: + return not self._qsize() + + def full(self): + """Return True if the queue is full, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() >= n + as a direct substitute, but be aware that either approach risks a race + condition where a queue can shrink before the result of full() or + qsize() can be used. + """ + with self.mutex: + return 0 < self.maxsize <= self._qsize() + + def put(self, item, block=True, timeout=None): + """Put an item into the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until a free slot is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the FullError exception if no free slot was available within that time. + Otherwise ('block' is false), put an item on the queue if a free slot + is immediately available, else raise the FullError exception ('timeout' + is ignored in that case). + """ + with self.not_full: + if self.maxsize > 0: + if not block: + if self._qsize() >= self.maxsize: + raise FullError() + elif timeout is None: + while self._qsize() >= self.maxsize: + self.not_full.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while self._qsize() >= self.maxsize: + remaining = endtime - time() + if remaining <= 0.0: + raise FullError() + self.not_full.wait(remaining) + self._put(item) + self.unfinished_tasks += 1 + self.not_empty.notify() + + def get(self, block=True, timeout=None): + """Remove and return an item from the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until an item is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the EmptyError exception if no item was available within that time. + Otherwise ('block' is false), return an item if one is immediately + available, else raise the EmptyError exception ('timeout' is ignored + in that case). + """ + with self.not_empty: + if not block: + if not self._qsize(): + raise EmptyError() + elif timeout is None: + while not self._qsize(): + self.not_empty.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while not self._qsize(): + remaining = endtime - time() + if remaining <= 0.0: + raise EmptyError() + self.not_empty.wait(remaining) + item = self._get() + self.not_full.notify() + return item + + def put_nowait(self, item): + """Put an item into the queue without blocking. + + Only enqueue the item if a free slot is immediately available. + Otherwise raise the FullError exception. + """ + return self.put(item, block=False) + + def get_nowait(self): + """Remove and return an item from the queue without blocking. + + Only get an item if one is immediately available. Otherwise + raise the EmptyError exception. + """ + return self.get(block=False) + + # Override these methods to implement other queue organizations + # (e.g. stack or priority queue). + # These will only be called with appropriate locks held + + # Initialize the queue representation + def _init(self, maxsize): + self.queue: "Any" = deque() + + def _qsize(self): + return len(self.queue) + + # Put a new item in the queue + def _put(self, item): + self.queue.append(item) + + # Get an item from the queue + def _get(self): + return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py new file mode 100644 index 0000000000..79285c3386 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py @@ -0,0 +1,234 @@ +import os +import random +import threading +import time +import weakref +from collections import defaultdict +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp, serialize_attribute + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + from sentry_sdk._types import SpanJSON + + +class SpanBatcher(Batcher["SpanJSON"]): + # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is + # a bit of a buffer for spans that appear between the trigger to flush + # and actually flushing the buffer. + # + # The max limits are all per trace (per bucket). + MAX_ENVELOPE_SIZE = 1000 # spans + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 2000 + MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB + + FLUSH_WAIT_TIME = 5.0 + + TYPE = "span" + CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + # Spans from different traces cannot be emitted in the same envelope + # since the envelope contains a shared trace header. That's why we bucket + # by trace_id, so that we can then send the buckets each in its own + # envelope. + # trace_id -> span buffer + self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) + self._running_size: dict[str, int] = defaultdict(lambda: 0) + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._last_full_flush: float = time.monotonic() # drives time-based flushes + self._flush_event = threading.Event() + self._pending_flush: set[str] = set() # buckets to be flushed + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._span_buffer = defaultdict(list) + self._running_size = defaultdict(lambda: 0) + self._running = True + + self._lock = threading.Lock() + self._active = threading.local() + + self._last_full_flush = time.monotonic() + self._flush_event = threading.Event() + self._pending_flush = set() + + self._flusher = None + self._flusher_pid = None + + def _flush_loop(self) -> None: + self._active.flag = True + while self._running: + jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 + self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) + self._flush_event.clear() + + self._flush(only_pending=True) + + if ( + time.monotonic() - self._last_full_flush + >= self.FLUSH_WAIT_TIME + jitter + ): + self._flush() + self._last_full_flush = time.monotonic() + + def add(self, span: "SpanJSON") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + size = len(self._span_buffer[span["trace_id"]]) + if size >= self.MAX_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="span", + quantity=1, + ) + return None + + self._span_buffer[span["trace_id"]].append(span) + self._running_size[span["trace_id"]] += self._estimate_size(span) + + if ( + size + 1 >= self.MAX_BEFORE_FLUSH + or self._running_size[span["trace_id"]] + >= self.MAX_BYTES_BEFORE_FLUSH + ): + self._pending_flush.add(span["trace_id"]) + notify = True + else: + notify = False + + if notify: + self._flush_event.set() + finally: + self._active.flag = False + + @staticmethod + def _estimate_size(item: "SpanJSON") -> int: + # Rough estimate of serialized span size that's quick to compute. + # 210 is the rough size of the payload without attributes, and then we + # estimate the attributes separately. + estimate = 210 + for value in (item.get("attributes") or {}).values(): + estimate += 50 + + if isinstance(value, str): + estimate += len(value) + else: + estimate += len(str(value)) + + return estimate + + @staticmethod + def _to_transport_format(item: "SpanJSON") -> "Any": + res = {k: v for k, v in item.items() if k not in ("_segment_span",)} + + if item.get("attributes"): + res["attributes"] = { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + } + else: + del res["attributes"] + + return res + + def _flush(self, only_pending: bool = False) -> None: + with self._lock: + if only_pending: + buckets = list(self._pending_flush) + else: + # flush whole buffer, e.g. if the SDK is shutting down + buckets = list(self._span_buffer.keys()) + + self._pending_flush.clear() + + if not buckets: + return + + envelopes = [] + + for bucket_id in buckets: + spans = self._span_buffer.get(bucket_id) + if not spans: + continue + + dsc = spans[0]["_segment_span"]._dynamic_sampling_context() + + # Max per envelope is 1000, so if we happen to have more than + # 1000 spans in one bucket, we'll need to separate them. + for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): + end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) + + envelope = Envelope( + headers={ + "sent_at": format_timestamp(datetime.now(timezone.utc)), + "trace": dsc, + } + ) + + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": end - start, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(spans[j]) + for j in range(start, end) + ], + } + ), + ) + ) + + envelopes.append(envelope) + + del self._span_buffer[bucket_id] + del self._running_size[bucket_id] + + for envelope in envelopes: + self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py new file mode 100644 index 0000000000..fbd2578048 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py @@ -0,0 +1,481 @@ +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING, TypeVar, Union + +# Re-exported for compat, since code out there in the wild might use this variable. +MYPY = TYPE_CHECKING + + +SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" +BLOB_DATA_SUBSTITUTE = "[Blob substitute]" +OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" +UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" + + +class AnnotatedValue: + """ + Meta information for a data field in the event payload. + This is to tell Relay that we have tampered with the fields value. + See: + https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 + """ + + __slots__ = ("value", "metadata") + + def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: + self.value = value + self.metadata = metadata + + def __eq__(self, other: "Any") -> bool: + if not isinstance(other, AnnotatedValue): + return False + + return self.value == other.value and self.metadata == other.metadata + + def __str__(self: "AnnotatedValue") -> str: + return str({"value": str(self.value), "metadata": str(self.metadata)}) + + def __len__(self: "AnnotatedValue") -> int: + if self.value is not None: + return len(self.value) + else: + return 0 + + @classmethod + def removed_because_raw_data(cls) -> "AnnotatedValue": + """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" + # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition + # to span-first + return AnnotatedValue( + value="", + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_raw_data(cls) -> "AnnotatedValue": + """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" + return AnnotatedValue( + value=UNPARSABLE_RAW_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": + """ + The actual value was removed because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition + # to span-first + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_over_size_limit( + cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE + ) -> "AnnotatedValue": + """ + The actual value was replaced because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": + """The actual value was removed because it contained sensitive information.""" + return AnnotatedValue( + value=SENSITIVE_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) + "s", # The fields original value was substituted + ] + ] + }, + ) + + +T = TypeVar("T") +Annotated = Union[AnnotatedValue, T] + + +if TYPE_CHECKING: + from collections.abc import Container, MutableMapping, Sequence + from datetime import datetime + from types import TracebackType + from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + + class SDKInfo(TypedDict): + name: str + version: str + packages: "Sequence[Mapping[str, str]]" + + class KeyValueCollectionBehaviour(TypedDict): + mode: 'Literal["off", "denylist", "allowlist"]' + 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): + request: "KeyValueCollectionBehaviour" + + class HttpHeadersCollectionBehaviour(TypedDict): + request: "KeyValueCollectionBehaviour" + + class DataCollectionUserOptions(TypedDict, total=False): + user_info: bool + cookies: "KeyValueCollectionBehaviour" + http_headers: "HttpHeadersCollectionUserOptions" + http_bodies: "List[str]" + query_params: "KeyValueCollectionBehaviour" + graphql: "GraphQLCollectionUserOptions" + gen_ai: "GenAICollectionUserOptions" + database_query_data: bool + queues: bool + 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_query_data: bool + queues: bool + 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"] + + DurationUnit = Literal[ + "nanosecond", + "microsecond", + "millisecond", + "second", + "minute", + "hour", + "day", + "week", + ] + + InformationUnit = Literal[ + "bit", + "byte", + "kilobyte", + "kibibyte", + "megabyte", + "mebibyte", + "gigabyte", + "gibibyte", + "terabyte", + "tebibyte", + "petabyte", + "pebibyte", + "exabyte", + "exbibyte", + ] + + FractionUnit = Literal["ratio", "percent"] + MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] + + MeasurementValue = TypedDict( + "MeasurementValue", + { + "value": float, + "unit": NotRequired[Optional[MeasurementUnit]], + }, + ) + + Event = TypedDict( + "Event", + { + "breadcrumbs": Annotated[ + dict[Literal["values"], list[dict[str, Any]]] + ], # TODO: We can expand on this type + "check_in_id": str, + "contexts": dict[str, dict[str, object]], + "dist": str, + "duration": Optional[float], + "environment": Optional[str], + "errors": list[dict[str, Any]], # TODO: We can expand on this type + "event_id": str, + "exception": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "extra": MutableMapping[str, object], + "fingerprint": list[str], + "level": LogLevelStr, + "logentry": Mapping[str, object], + "logger": str, + "measurements": dict[str, MeasurementValue], + "message": str, + "modules": dict[str, str], + "monitor_config": Mapping[str, object], + "monitor_slug": Optional[str], + "platform": Literal["python"], + "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports + "release": Optional[str], + "request": dict[str, object], + "sdk": Mapping[str, object], + "server_name": str, + "spans": Annotated[list[dict[str, object]]], + "stacktrace": dict[ + str, object + ], # We access this key in the code, but I am unsure whether we ever set it + "start_timestamp": datetime, + "status": Optional[str], + "tags": MutableMapping[ + str, str + ], # Tags must be less than 200 characters each + "threads": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "timestamp": Optional[datetime], # Must be set before sending the event + "transaction": str, + "transaction_info": Mapping[str, Any], # TODO: We can expand on this type + "type": Literal["check_in", "transaction"], + "user": dict[str, object], + "_dropped_spans": int, + "_has_gen_ai_span": bool, + }, + total=False, + ) + + ExcInfo = Union[ + tuple[Type[BaseException], BaseException, Optional[TracebackType]], + tuple[None, None, None], + ] + + # TODO: Make a proper type definition for this (PRs welcome!) + Hint = Dict[str, Any] + + AttributeValue = ( + str + | bool + | float + | int + | list[str] + | list[bool] + | list[float] + | list[int] + | tuple[str, ...] + | tuple[bool, ...] + | tuple[float, ...] + | tuple[int, ...] + ) + Attributes = dict[str, AttributeValue] + + SerializedAttributeValue = TypedDict( + # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types + "SerializedAttributeValue", + { + "type": Literal[ + "string", + "boolean", + "double", + "integer", + "array", + ], + "value": AttributeValue, + }, + ) + + Log = TypedDict( + "Log", + { + "severity_text": str, + "severity_number": int, + "body": str, + "attributes": Attributes, + "time_unix_nano": int, + "trace_id": Optional[str], + "span_id": Optional[str], + }, + ) + + MetricType = Literal["counter", "gauge", "distribution"] + MetricUnit = Union[DurationUnit, InformationUnit, str] + + Metric = TypedDict( + "Metric", + { + "timestamp": float, + "trace_id": Optional[str], + "span_id": Optional[str], + "name": str, + "type": MetricType, + "value": float, + "unit": Optional[MetricUnit], + "attributes": Attributes, + }, + ) + + MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] + + SpanJSON = TypedDict( + "SpanJSON", + { + "trace_id": str, + "span_id": str, + "parent_span_id": NotRequired[str], + "name": str, + "status": str, + "is_segment": bool, + "start_timestamp": float, + "end_timestamp": NotRequired[float], + "attributes": NotRequired[Attributes], + "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], + }, + ) + + # TODO: Make a proper type definition for this (PRs welcome!) + Breadcrumb = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + BreadcrumbHint = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + SamplingContext = Dict[str, Any] + + EventProcessor = Callable[[Event, Hint], Optional[Event]] + ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] + BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] + TransactionProcessor = Callable[[Event, Hint], Optional[Event]] + LogProcessor = Callable[[Log, Hint], Optional[Log]] + + TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] + + # https://github.com/python/mypy/issues/5710 + NotImplementedType = Any + + EventDataCategory = Literal[ + "default", + "error", + "crash", + "transaction", + "security", + "attachment", + "session", + "internal", + "profile", + "profile_chunk", + "monitor", + "span", + "log_item", + "log_byte", + "trace_metric", + ] + SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] + + ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] + ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] + + MonitorConfigScheduleType = Literal["crontab", "interval"] + MonitorConfigScheduleUnit = Literal[ + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", # not supported in Sentry and will result in a warning + ] + + MonitorConfigSchedule = TypedDict( + "MonitorConfigSchedule", + { + "type": MonitorConfigScheduleType, + "value": Union[int, str], + "unit": MonitorConfigScheduleUnit, + }, + total=False, + ) + + MonitorConfig = TypedDict( + "MonitorConfig", + { + "schedule": MonitorConfigSchedule, + "timezone": str, + "checkin_margin": int, + "max_runtime": int, + "failure_issue_threshold": int, + "recovery_threshold": int, + "owner": str, + }, + total=False, + ) + + HttpStatusCodeRange = Union[int, Container[int]] + + class TextPart(TypedDict): + type: Literal["text"] + content: str + + IgnoreSpansName = Union[str, Pattern[str]] + IgnoreSpansContext = TypedDict( + "IgnoreSpansContext", + {"name": IgnoreSpansName, "attributes": Attributes}, + total=False, + ) + IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py new file mode 100644 index 0000000000..98f932267f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py @@ -0,0 +1,100 @@ +""" +Copyright (c) 2007 by the Pallets team. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Dict, Iterator, Optional, Tuple + + +# +# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` +# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 +# +# We need this function because Django does not give us a "pure" http header +# dict. So we might as well use it for all WSGI integrations. +# +def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns only proper HTTP headers. + """ + for key, value in environ.items(): + key = str(key) + if key.startswith("HTTP_") and key not in ( + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + ): + yield key[5:].replace("_", "-").title(), value + elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): + yield key.replace("_", "-").title(), value + + +def _strip_default_port(host: str, scheme: "Optional[str]") -> str: + """Strip the port from the host if it's the default for the scheme.""" + if scheme == "http" and host.endswith(":80"): + return host[:-3] + if scheme == "https" and host.endswith(":443"): + return host[:-4] + return host + + +# `get_host` comes from `werkzeug.wsgi.get_host` +# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 + + +def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: + """ + Return the host for the given WSGI environment. + """ + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: + return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) + elif environ.get("HTTP_HOST"): + return _strip_default_port(environ["HTTP_HOST"], scheme) + elif environ.get("SERVER_NAME"): + # SERVER_NAME/SERVER_PORT describe the internal server, so use + # wsgi.url_scheme (not the forwarded scheme) for port decisions. + rv = environ["SERVER_NAME"] + if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( + ("https", "443"), + ("http", "80"), + ): + rv += ":" + environ["SERVER_PORT"] + return rv + else: + # In spite of the WSGI spec, SERVER_NAME might not be present. + return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py new file mode 100644 index 0000000000..404e57ff1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py @@ -0,0 +1,8 @@ +from .utils import ( + GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 + GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 + normalize_message_role, # noqa: F401 + normalize_message_roles, # noqa: F401 + set_conversation_id, # noqa: F401 + set_data_normalized, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py new file mode 100644 index 0000000000..b5eb8c55ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py @@ -0,0 +1,66 @@ +from collections.abc import Iterable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.chat import ( + ChatCompletionContentPartParam, + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ) + + from sentry_sdk._types import TextPart + + +def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: + return isinstance(message, dict) and message.get("role") == "system" + + +def _get_system_instructions( + messages: "Iterable[ChatCompletionMessageParam]", +) -> "list[ChatCompletionMessageParam]": + if not isinstance(messages, Iterable): + return [] + + return [message for message in messages if _is_system_instruction(message)] + + +def _get_text_items( + content: "Union[str, Iterable[ChatCompletionContentPartParam]]", +) -> "list[str]": + if isinstance(content, str): + return [content] + + if not isinstance(content, Iterable): + return [] + + text_items = [] + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text", None) + if text is not None: + text_items.append(text) + + return text_items + + +def _transform_system_instructions( + system_instructions: "list[ChatCompletionSystemMessageParam]", +) -> "list[TextPart]": + instruction_text_parts: "list[TextPart]" = [] + + for instruction in system_instructions: + if not isinstance(instruction, dict): + continue + + content = instruction.get("content") + if content is None: + continue + + text_parts: "list[TextPart]" = [ + {"type": "text", "content": text} for text in _get_text_items(content) + ] + instruction_text_parts += text_parts + + return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py new file mode 100644 index 0000000000..8f751c3248 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.responses import ResponseInputItemParam, ResponseInputParam + + +def _is_system_instruction(message: "ResponseInputItemParam") -> bool: + if not isinstance(message, dict) or not message.get("role") == "system": + return False + + return "type" not in message or message["type"] == "message" + + +def _get_system_instructions( + messages: "Union[str, ResponseInputParam]", +) -> "list[ResponseInputItemParam]": + if not isinstance(messages, list): + return [] + + return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py new file mode 100644 index 0000000000..35ee4bd788 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py @@ -0,0 +1,6 @@ +import re + +# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." +DATA_URL_BASE64_REGEX = re.compile( + r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py new file mode 100644 index 0000000000..d8840ad451 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py @@ -0,0 +1,147 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk.utils +from sentry_sdk import start_span +from sentry_sdk.ai.utils import _set_span_data_attribute +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, TypeVar, Union + + F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) + +_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) + + +def set_ai_pipeline_name(name: "Optional[str]") -> None: + _ai_pipeline_name.set(name) + + +def get_ai_pipeline_name() -> "Optional[str]": + return _ai_pipeline_name.get() + + +def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": + def decorator(f: "F") -> "F": + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return await f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = await f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + if inspect.iscoroutinefunction(f): + return wraps(f)(async_wrapped) # type: ignore + else: + return wraps(f)(sync_wrapped) # type: ignore + + return decorator + + +def record_token_usage( + span: "Union[Span, StreamedSpan]", + input_tokens: "Optional[int]" = None, + input_tokens_cached: "Optional[int]" = None, + input_tokens_cache_write: "Optional[int]" = None, + output_tokens: "Optional[int]" = None, + output_tokens_reasoning: "Optional[int]" = None, + total_tokens: "Optional[int]" = None, +) -> None: + # TODO: move pipeline name elsewhere + ai_pipeline_name = get_ai_pipeline_name() + if ai_pipeline_name: + _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) + + if input_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if input_tokens_cached is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + input_tokens_cached, + ) + + if input_tokens_cache_write is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + input_tokens_cache_write, + ) + + if output_tokens is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + ) + + if output_tokens_reasoning is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + output_tokens_reasoning, + ) + + if total_tokens is None and input_tokens is not None and output_tokens is not None: + total_tokens = input_tokens + output_tokens + + if total_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py new file mode 100644 index 0000000000..7b1ca9324b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py @@ -0,0 +1,782 @@ +import inspect +import json +from copy import deepcopy +from typing import TYPE_CHECKING + +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import logger + +MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB +# Maximum characters when only a single message is left after bytes truncation +MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 + + +class GEN_AI_ALLOWED_MESSAGE_ROLES: + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { + GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], + GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], + GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], + GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], +} + +GEN_AI_MESSAGE_ROLE_MAPPING = {} +for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): + for source_role in source_roles: + GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role + + +def parse_data_uri(url: str) -> "Tuple[str, str]": + """ + Parse a data URI and return (mime_type, content). + + Data URI format (RFC 2397): data:[][;base64], + + Examples: + data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") + data:text/plain,Hello → ("text/plain", "Hello") + data:;base64,SGVsbG8= → ("", "SGVsbG8=") + + Raises: + ValueError: If the URL is not a valid data URI (missing comma separator) + """ + if "," not in url: + raise ValueError("Invalid data URI: missing comma separator") + + header, content = url.split(",", 1) + + # Extract mime type from header + # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" + # Remove "data:" prefix, then take everything before the first semicolon + if header.startswith("data:"): + mime_part = header[5:] # Remove "data:" prefix + else: + mime_part = header + + mime_type = mime_part.split(";")[0] + + return mime_type, content + + +def get_modality_from_mime_type(mime_type: str) -> str: + """ + Infer the content modality from a MIME type string. + + Args: + mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") + + Returns: + One of: "image", "audio", "video", or "document" + Defaults to "image" for unknown or empty MIME types. + + Examples: + "image/jpeg" -> "image" + "audio/mp3" -> "audio" + "video/mp4" -> "video" + "application/pdf" -> "document" + "text/plain" -> "document" + """ + if not mime_type: + return "image" # Default fallback + + mime_lower = mime_type.lower() + if mime_lower.startswith("image/"): + return "image" + elif mime_lower.startswith("audio/"): + return "audio" + elif mime_lower.startswith("video/"): + return "video" + elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): + return "document" + else: + return "image" # Default fallback for unknown types + + +def transform_openai_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an OpenAI/LiteLLM content part to Sentry's standardized format. + + This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. + + Input format: + - {"type": "image_url", "image_url": {"url": "..."}} + - {"type": "image_url", "image_url": "..."} (string shorthand) + + Output format (one of): + - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from OpenAI/LiteLLM + + Returns: + A transformed dictionary in standardized format, or None if the format + is not OpenAI image_url format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type != "image_url": + return None + + image_url_data = content_part.get("image_url") + if isinstance(image_url_data, str): + url = image_url_data + elif isinstance(image_url_data, dict): + url = image_url_data.get("url", "") + else: + return None + + if not url: + return None + + # Check if it's a data URI (base64 encoded) + if url.startswith("data:"): + try: + mime_type, content = parse_data_uri(url) + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": content, + } + except ValueError: + # If parsing fails, return as URI + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + else: + # Regular URL + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + + +def transform_anthropic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an Anthropic content part to Sentry's standardized format. + + This handles the Anthropic image and document formats with source dictionaries. + + Input format: + - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} + - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} + - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} + - {"type": "document", "source": {...}} (same source formats) + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from Anthropic + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Anthropic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "document") or "source" not in content_part: + return None + + source = content_part.get("source") + if not isinstance(source, dict): + return None + + source_type = source.get("type") + media_type = source.get("media_type", "") + modality = ( + "document" + if block_type == "document" + else get_modality_from_mime_type(media_type) + ) + + if source_type == "base64": + return { + "type": "blob", + "modality": modality, + "mime_type": media_type, + "content": source.get("data", ""), + } + elif source_type == "url": + return { + "type": "uri", + "modality": modality, + "mime_type": media_type, + "uri": source.get("url", ""), + } + elif source_type == "file": + return { + "type": "file", + "modality": modality, + "mime_type": media_type, + "file_id": source.get("file_id", ""), + } + + return None + + +def transform_google_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a Google GenAI content part to Sentry's standardized format. + + This handles the Google GenAI inline_data and file_data formats. + + Input format: + - {"inline_data": {"mime_type": "...", "data": "..."}} + - {"file_data": {"mime_type": "...", "file_uri": "..."}} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from Google GenAI + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Google format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Handle Google inline_data format + if "inline_data" in content_part: + inline_data = content_part.get("inline_data") + if isinstance(inline_data, dict): + mime_type = inline_data.get("mime_type", "") + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": inline_data.get("data", ""), + } + return None + + # Handle Google file_data format + if "file_data" in content_part: + file_data = content_part.get("file_data") + if isinstance(file_data, dict): + mime_type = file_data.get("mime_type", "") + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_data.get("file_uri", ""), + } + return None + + return None + + +def transform_generic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a generic/LangChain-style content part to Sentry's standardized format. + + This handles generic formats where the type indicates the modality and + the data is provided via direct base64, url, or file_id fields. + + Input format: + - {"type": "image", "base64": "...", "mime_type": "..."} + - {"type": "audio", "url": "...", "mime_type": "..."} + - {"type": "video", "base64": "...", "mime_type": "..."} + - {"type": "file", "file_id": "...", "mime_type": "..."} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part in generic format + + Returns: + A transformed dictionary in standardized format, or None if the format + is not generic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "audio", "video", "file"): + return None + + # Ensure it's not Anthropic format (which also uses type: "image") + if "source" in content_part: + return None + + mime_type = content_part.get("mime_type", "") + modality = block_type if block_type != "file" else "document" + + # Check for base64 encoded content + if "base64" in content_part: + return { + "type": "blob", + "modality": modality, + "mime_type": mime_type, + "content": content_part.get("base64", ""), + } + # Check for URL reference + elif "url" in content_part: + return { + "type": "uri", + "modality": modality, + "mime_type": mime_type, + "uri": content_part.get("url", ""), + } + # Check for file_id reference + elif "file_id" in content_part: + return { + "type": "file", + "modality": modality, + "mime_type": mime_type, + "file_id": content_part.get("file_id", ""), + } + + return None + + +def transform_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a content part from various AI SDK formats to Sentry's standardized format. + + This is a heuristic dispatcher that detects the format and delegates to the + appropriate SDK-specific transformer. For direct SDK integration, prefer using + the specific transformers directly: + - transform_openai_content_part() for OpenAI/LiteLLM + - transform_anthropic_content_part() for Anthropic + - transform_google_content_part() for Google GenAI + - transform_generic_content_part() for LangChain and other generic formats + + Detection order: + 1. OpenAI: type == "image_url" + 2. Google: "inline_data" or "file_data" keys present + 3. Anthropic: type in ("image", "document") with "source" key + 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from an AI SDK + + Returns: + A transformed dictionary in standardized format, or None if the format + is unrecognized or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Try OpenAI format first (most common, clear indicator) + result = transform_openai_content_part(content_part) + if result is not None: + return result + + # Try Google format (unique keys make it easy to detect) + result = transform_google_content_part(content_part) + if result is not None: + return result + + # Try Anthropic format (has "source" key) + result = transform_anthropic_content_part(content_part) + if result is not None: + return result + + # Try generic format as fallback + result = transform_generic_content_part(content_part) + if result is not None: + return result + + # Unrecognized format + return None + + +def transform_message_content(content: "Any") -> "Any": + """ + Transform message content, handling both string content and list of content blocks. + + For list content, each item is transformed using transform_content_part(). + Items that cannot be transformed (return None) are kept as-is. + + Args: + content: Message content - can be a string, list of content blocks, or other + + Returns: + - String content: returned as-is + - List content: list with each transformable item converted to standardized format + - Other: returned as-is + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + return transformed + + return content + + +def _normalize_data(data: "Any", unpack: bool = True) -> "Any": + # convert pydantic data (e.g. OpenAI v1+) to json compatible format + if hasattr(data, "model_dump"): + # Check if it's a class (type) rather than an instance + # Model classes can be passed as arguments (e.g., for schema definitions) + if inspect.isclass(data): + return f"" + + try: + return _normalize_data(data.model_dump(), unpack=unpack) + except Exception as e: + logger.warning("Could not convert pydantic data to JSON: %s", e) + return data if isinstance(data, (int, float, bool, str)) else str(data) + + if isinstance(data, list): + if unpack and len(data) == 1: + return _normalize_data(data[0], unpack=unpack) # remove empty dimensions + return list(_normalize_data(x, unpack=unpack) for x in data) + + if isinstance(data, dict): + return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} + + return data if isinstance(data, (int, float, bool, str)) else str(data) + + +def set_data_normalized( + span: "Union[Span, StreamedSpan]", + key: str, + value: "Any", + unpack: bool = True, +) -> None: + normalized = _normalize_data(value, unpack=unpack) + if isinstance(normalized, (int, float, bool, str)): + _set_span_data_attribute(span, key, normalized) + else: + _set_span_data_attribute(span, key, json.dumps(normalized)) + + +def _set_span_data_attribute( + span: "Union[Span, StreamedSpan]", key: str, value: "Any" +) -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(key, value) + else: + span.set_data(key, value) + + +def normalize_message_role(role: str) -> str: + """ + Normalize a message role to one of the 4 allowed gen_ai role values. + Maps "ai" -> "assistant" and keeps other standard roles unchanged. + """ + return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) + + +def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": + """ + Normalize roles in a list of messages to use standard gen_ai role values. + Creates a deep copy to avoid modifying the original messages. + """ + normalized_messages = [] + for message in messages: + if not isinstance(message, dict): + normalized_messages.append(message) + continue + normalized_message = message.copy() + if "role" in message: + normalized_message["role"] = normalize_message_role(message["role"]) + normalized_messages.append(normalized_message) + + return normalized_messages + + +def get_start_span_function() -> "Callable[..., Any]": + current_span = sentry_sdk.get_current_span() + + transaction_exists = ( + current_span is not None and current_span.containing_transaction is not None + ) + return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction + + +def _truncate_single_message_content_if_present( + message: "Dict[str, Any]", max_chars: int +) -> "Dict[str, Any]": + """ + Truncate a message's content to at most `max_chars` characters and append an + ellipsis if truncation occurs. + """ + if not isinstance(message, dict) or "content" not in message: + return message + content = message["content"] + + if isinstance(content, str): + if len(content) <= max_chars: + return message + message["content"] = content[:max_chars] + "..." + return message + + if isinstance(content, list): + remaining = max_chars + for item in content: + if isinstance(item, dict) and "text" in item: + text = item["text"] + if isinstance(text, str): + if len(text) > remaining: + item["text"] = text[:remaining] + "..." + remaining = 0 + else: + remaining -= len(text) + return message + + return message + + +def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: + """ + Find the index of the first message that would exceed the max bytes limit. + Compute the individual message sizes, and return the index of the first message from the back + of the list that would exceed the max bytes limit. + """ + running_sum = 0 + for idx in range(len(messages) - 1, -1, -1): + size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) + running_sum += size + if running_sum > max_bytes: + return idx + 1 + + return 0 + + +def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: + """ + Some content blocks contain an image_url property with base64 content as its value. + This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. + """ + if item.get("type") != "image_url": + return False + + image_url_val = item.get("image_url") + image_url = ( + image_url_val.get("url", "") + if isinstance(image_url_val, dict) + else (image_url_val or "") + ) + data_url_match = DATA_URL_BASE64_REGEX.match(image_url) + + return bool(data_url_match) + + +def redact_blob_message_parts( + messages: "List[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Redact blob message parts from the messages by replacing blob content with "[Filtered]". + + This function creates a deep copy of messages that contain blob content to avoid + mutating the original message dictionaries. Messages without blob content are + returned as-is to minimize copying overhead. + + e.g: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,..." + } + ] + } + becomes: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "[Filtered]" + } + ] + } + """ + + # First pass: check if any message contains blob content + has_blobs = False + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and ( + item.get("type") == "blob" or _is_image_type_with_blob_content(item) + ): + has_blobs = True + break + if has_blobs: + break + + # If no blobs found, return original messages to avoid unnecessary copying + if not has_blobs: + return messages + + # Deep copy messages to avoid mutating the original + messages_copy = deepcopy(messages) + + # Second pass: redact blob content in the copy + for message in messages_copy: + if not isinstance(message, dict): + continue + + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + if item.get("type") == "blob": + item["content"] = BLOB_DATA_SUBSTITUTE + elif _is_image_type_with_blob_content(item): + if isinstance(item["image_url"], dict): + item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE + else: + item["image_url"] = BLOB_DATA_SUBSTITUTE + + return messages_copy + + +def truncate_messages_by_size( + messages: "List[Dict[str, Any]]", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Tuple[List[Dict[str, Any]], int]": + """ + Returns a truncated messages list, consisting of + - the last message, with its content truncated to `max_single_message_chars` characters, + if the last message's size exceeds `max_bytes` bytes; otherwise, + - the maximum number of messages, starting from the end of the `messages` list, whose total + serialized size does not exceed `max_bytes` bytes. + + In the single message case, the serialized message size may exceed `max_bytes`, because + truncation is based only on character count in that case. + """ + serialized_json = json.dumps(messages, separators=(",", ":")) + current_size = len(serialized_json.encode("utf-8")) + + if current_size <= max_bytes: + return messages, 0 + + truncation_index = _find_truncation_index(messages, max_bytes) + if truncation_index < len(messages): + truncated_messages = messages[truncation_index:] + else: + truncation_index = len(messages) - 1 + truncated_messages = messages[-1:] + + if len(truncated_messages) == 1: + truncated_messages[0] = _truncate_single_message_content_if_present( + deepcopy(truncated_messages[0]), max_chars=max_single_message_chars + ) + + return truncated_messages, truncation_index + + +def truncate_and_annotate_messages( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_message = _truncate_single_message_content_if_present( + deepcopy(messages[-1]), max_chars=max_single_message_chars + ) + if len(messages) > 1: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return [truncated_message] + + +def truncate_and_annotate_embedding_inputs( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) + if removed_count > 0: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return truncated_messages + + +def set_conversation_id(conversation_id: str) -> None: + """ + Set the conversation_id in the scope. + """ + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py new file mode 100644 index 0000000000..5556b11ace --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py @@ -0,0 +1,573 @@ +import inspect +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import Client, tracing_utils +from sentry_sdk._init_implementation import init +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.crons import monitor +from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import get_current_span as _get_current_streamed_span +from sentry_sdk.tracing import NoOpSpan, Transaction, trace + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Optional, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + MeasurementUnit, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span, TransactionKwargs + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) +else: + + def overload(x: "T") -> "T": + return x + + +# When changing this, update __all__ in __init__.py too +__all__ = [ + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + + +def scopemethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, + inspect.getdoc(getattr(Scope, f.__name__)), + ) + return f + + +def clientmethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, + inspect.getdoc(getattr(Client, f.__name__)), + ) + return f + + +@scopemethod +def get_client() -> "BaseClient": + return Scope.get_client() + + +def is_initialized() -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether Sentry has been initialized or not. + + If a client is available and the client is active + (meaning it is configured to send data) then + Sentry is initialized. + """ + return get_client().is_active() + + +@scopemethod +def get_global_scope() -> "Scope": + return Scope.get_global_scope() + + +@scopemethod +def get_isolation_scope() -> "Scope": + return Scope.get_isolation_scope() + + +@scopemethod +def get_current_scope() -> "Scope": + return Scope.get_current_scope() + + +@scopemethod +def last_event_id() -> "Optional[str]": + """ + See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding + this method's limitations. + """ + return Scope.last_event_id() + + +@scopemethod +def capture_event( + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) + + +@scopemethod +def capture_message( + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_message( + message, level, scope=scope, **scope_kwargs + ) + + +@scopemethod +def capture_exception( + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) + + +@scopemethod +def add_attachment( + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, +) -> None: + return get_isolation_scope().add_attachment( + bytes, filename, path, content_type, add_to_transactions + ) + + +@scopemethod +def add_breadcrumb( + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", +) -> None: + return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + +@overload +def configure_scope() -> "ContextManager[Scope]": + pass + + +@overload +def configure_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def configure_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + warnings.warn( + "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", + DeprecationWarning, + stacklevel=2, + ) + + scope = get_isolation_scope() + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + +@overload +def push_scope() -> "ContextManager[Scope]": + pass + + +@overload +def push_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def push_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + warnings.warn( + "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", + DeprecationWarning, + stacklevel=2, + ) + + if callback is not None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with push_scope() as scope: + callback(scope) + return None + + return _ScopeManager() + + +@scopemethod +def set_attribute(attribute: str, value: "Any") -> None: + """ + Set an attribute. + + Any attributes-based telemetry (logs, metrics) captured in this scope will + include this attribute. + """ + return get_isolation_scope().set_attribute(attribute, value) + + +@scopemethod +def remove_attribute(attribute: str) -> None: + """ + Remove an attribute. + + If the attribute doesn't exist, this function will not have any effect and + it will also not raise an exception. + """ + return get_isolation_scope().remove_attribute(attribute) + + +@scopemethod +def set_tag(key: str, value: "Any") -> None: + return get_isolation_scope().set_tag(key, value) + + +@scopemethod +def set_tags(tags: "Mapping[str, object]") -> None: + return get_isolation_scope().set_tags(tags) + + +@scopemethod +def set_context(key: str, value: "Dict[str, Any]") -> None: + return get_isolation_scope().set_context(key, value) + + +@scopemethod +def set_extra(key: str, value: "Any") -> None: + return get_isolation_scope().set_extra(key, value) + + +@scopemethod +def set_user(value: "Optional[Dict[str, Any]]") -> None: + return get_isolation_scope().set_user(value) + + +@scopemethod +def set_level(value: "LogLevelStr") -> None: + return get_isolation_scope().set_level(value) + + +@clientmethod +def flush( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return get_client().flush(timeout=timeout, callback=callback) + + +@clientmethod +async def flush_async( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return await get_client().flush_async(timeout=timeout, callback=callback) + + +@scopemethod +def start_span( + **kwargs: "Any", +) -> "Span": + return get_current_scope().start_span(**kwargs) + + +@scopemethod +def start_transaction( + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", +) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction on the current scope. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + return get_current_scope().start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + +def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + transaction = get_current_scope().transaction + if transaction is not None: + transaction.set_measurement(name, value, unit) + + +def get_current_span( + scope: "Optional[Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + return tracing_utils.get_current_span(scope) + + +def get_traceparent() -> "Optional[str]": + """ + Returns the traceparent either from the active span or from the scope. + """ + return get_current_scope().get_traceparent() + + +def get_baggage() -> "Optional[str]": + """ + Returns Baggage either from the active span or from the scope. + """ + baggage = get_current_scope().get_baggage() + if baggage is not None: + return baggage.serialize() + + return None + + +def continue_trace( + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", +) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers, op, name, source, origin + ) + + +@scopemethod +def start_session( + session_mode: str = "application", +) -> None: + return get_isolation_scope().start_session(session_mode=session_mode) + + +@scopemethod +def end_session() -> None: + return get_isolation_scope().end_session() + + +@scopemethod +def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: + return get_current_scope().set_transaction_name(name, source) + + +def update_current_span( + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, + data: "Optional[dict[str, Any]]" = None, +) -> None: + """ + Update the current active span with the provided parameters. + + This function allows you to modify properties of the currently active span. + If no span is currently active, this function will do nothing. + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, the span's operation will + remain unchanged. + :type op: str or None + + :param name: The human-readable name/description for the span. This provides + more specific details about what the span represents (e.g., "GET /api/users", + "SELECT * FROM users"). If not provided, the span's name will remain unchanged. + :type name: str or None + + :param data: A dictionary of key-value pairs to add as data to the span. This + data will be merged with any existing span data. If not provided, + no data will be added. + + .. deprecated:: 2.35.0 + Use ``attributes`` instead. The ``data`` parameter will be removed + in a future version. + :type data: dict[str, Union[str, int, float, bool]] or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes will be merged with any existing span data. If not provided, + no attributes will be added. + :type attributes: dict[str, Union[str, int, float, bool]] or None + + :returns: None + + .. versionadded:: 2.35.0 + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP + + sentry_sdk.update_current_span( + op=OP.FUNCTION, + name="process_user_data", + attributes={"user_id": 123, "batch_size": 50} + ) + """ + if isinstance(_get_current_streamed_span(), StreamedSpan): + warnings.warn( + "The `update_current_span` API isn't available in streaming mode. " + "Retrieve the current span with get_current_span() and use its API " + "directly.", + DeprecationWarning, + stacklevel=2, + ) + return + + current_span = get_current_span() + + if current_span is None: + return + + if op is not None: + current_span.op = op + + if name is not None: + # internally it is still description + current_span.description = name + + if data is not None and attributes is not None: + raise ValueError( + "Cannot provide both `data` and `attributes`. Please use only `attributes`." + ) + + if data is not None: + warnings.warn( + "The `data` parameter is deprecated. Please use `attributes` instead.", + DeprecationWarning, + stacklevel=2, + ) + attributes = data + + if attributes is not None: + current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py new file mode 100644 index 0000000000..4d69d3acf2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py @@ -0,0 +1,71 @@ +import mimetypes +import os +from typing import TYPE_CHECKING + +from sentry_sdk.envelope import Item, PayloadRef + +if TYPE_CHECKING: + from typing import Callable, Optional, Union + + +class Attachment: + """Additional files/data to send along with an event. + + This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. + config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with + all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are + captured within the ``Scope``. + + To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for + ``add_attachment`` are the same as the parameters for this class's constructor. + + :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless + ``path`` is provided. + :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. + :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. + :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` + parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. + :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. + """ + + def __init__( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + if bytes is None and path is None: + raise TypeError("path or raw bytes required for attachment") + if filename is None and path is not None: + filename = os.path.basename(path) + if filename is None: + raise TypeError("filename is required for attachment") + if content_type is None: + content_type = mimetypes.guess_type(filename)[0] + self.bytes = bytes + self.filename = filename + self.path = path + self.content_type = content_type + self.add_to_transactions = add_to_transactions + + def to_envelope_item(self) -> "Item": + """Returns an envelope item for this attachment.""" + payload: "Union[None, PayloadRef, bytes]" = None + if self.bytes is not None: + if callable(self.bytes): + payload = self.bytes() + else: + payload = self.bytes + else: + payload = PayloadRef(path=self.path) + return Item( + payload=payload, + type="attachment", + content_type=self.content_type, + filename=self.filename, + ) + + def __repr__(self) -> str: + return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py new file mode 100644 index 0000000000..92cb42277e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py @@ -0,0 +1,1479 @@ +import json +import os +import platform +import random +import socket +import sys +import uuid +import warnings +from collections.abc import Iterable, Mapping +from datetime import datetime, timezone +from importlib import import_module +from typing import TYPE_CHECKING, Dict, List, cast, overload + +from sentry_sdk._compat import check_uwsgi_thread_support +from sentry_sdk._metrics_batcher import MetricsBatcher +from sentry_sdk._span_batcher import SpanBatcher +from sentry_sdk.consts import ( + DEFAULT_MAX_VALUE_LENGTH, + DEFAULT_OPTIONS, + INSTRUMENTER, + SPANDATA, + SPANSTATUS, + VERSION, + ClientConstructor, +) +from sentry_sdk.data_collection import ( + _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 +from sentry_sdk.monitor import Monitor +from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler +from sentry_sdk.profiler.transaction_profiler import ( + Profile, + has_profiling_enabled, + setup_profiler, +) +from sentry_sdk.scrubber import EventScrubber +from sentry_sdk.serializer import serialize +from sentry_sdk.sessions import SessionFlusher +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import trace +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import ( + AsyncHttpTransport, + HttpTransportCore, + make_transport, +) +from sentry_sdk.utils import ( + AnnotatedValue, + ContextVar, + capture_internal_exceptions, + current_stacktrace, + datetime_from_isoformat, + env_to_bool, + format_timestamp, + get_before_send_log, + get_before_send_metric, + get_before_send_span, + get_default_release, + get_sdk_name, + get_type_name, + handle_in_app, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union + + from sentry_sdk._log_batcher import LogBatcher + from sentry_sdk._metrics_batcher import MetricsBatcher + from sentry_sdk._types import ( + Event, + EventDataCategory, + Hint, + Log, + Metric, + SDKInfo, + SerializedAttributeValue, + ) + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.session import Session + from sentry_sdk.spotlight import SpotlightClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.transport import Item, Transport + from sentry_sdk.utils import Dsn + + I = TypeVar("I", bound=Integration) # noqa: E741 + +_client_init_debug = ContextVar("client_init_debug") + + +SDK_INFO: "SDKInfo" = { + "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() + "version": VERSION, + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], +} + + +def _serialized_v1_attribute_to_serialized_v2_attribute( + attribute_value: "Any", +) -> "Optional[SerializedAttributeValue]": + if isinstance(attribute_value, bool): + return { + "value": attribute_value, + "type": "boolean", + } + + if isinstance(attribute_value, int): + return { + "value": attribute_value, + "type": "integer", + } + + if isinstance(attribute_value, float): + return { + "value": attribute_value, + "type": "double", + } + + if isinstance(attribute_value, str): + return { + "value": attribute_value, + "type": "string", + } + + if isinstance(attribute_value, list): + if not attribute_value: + return {"value": [], "type": "array"} + + ty = type(attribute_value[0]) + if ty in (int, str, bool, float) and all( + type(v) is ty for v in attribute_value + ): + return { + "value": attribute_value, + "type": "array", + } + + # Types returned when the serializer for V1 span attributes recurses into some container types. + if isinstance(attribute_value, (dict, list)): + return { + "value": json.dumps(attribute_value), + "type": "string", + } + + return None + + +def _serialized_v1_span_to_serialized_v2_span( + span: "dict[str, Any]", event: "Event" +) -> "dict[str, Any]": + # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". + res: "dict[str, Any]" = { + "status": SpanStatus.OK.value, + "is_segment": False, + } + + if "trace_id" in span: + res["trace_id"] = span["trace_id"] + + if "span_id" in span: + res["span_id"] = span["span_id"] + + if "description" in span: + description = span["description"] + + if description is None and "op" in span: + description = span["op"] + + res["name"] = description + + if "start_timestamp" in span: + start_timestamp = None + try: + start_timestamp = datetime_from_isoformat(span["start_timestamp"]) + except Exception: + pass + + if start_timestamp is not None: + res["start_timestamp"] = start_timestamp.timestamp() + + if "timestamp" in span: + end_timestamp = None + try: + end_timestamp = datetime_from_isoformat(span["timestamp"]) + except Exception: + pass + + if end_timestamp is not None: + res["end_timestamp"] = end_timestamp.timestamp() + + if "parent_span_id" in span: + res["parent_span_id"] = span["parent_span_id"] + + if "status" in span and span["status"] != SPANSTATUS.OK: + res["status"] = "error" + + attributes: "Dict[str, Any]" = {} + + if "op" in span: + attributes["sentry.op"] = span["op"] + if "origin" in span: + attributes["sentry.origin"] = span["origin"] + + span_data = span.get("data") + if isinstance(span_data, dict): + attributes.update(span_data) + + span_tags = span.get("tags") + if isinstance(span_tags, dict): + attributes.update(span_tags) + + # See Scope._apply_user_attributes_to_telemetry() for user attributes. + user = event.get("user") + if isinstance(user, dict): + if "id" in user: + attributes["user.id"] = user["id"] + if "username" in user: + attributes["user.name"] = user["username"] + if "email" in user: + attributes["user.email"] = user["email"] + + # See Scope.set_global_attributes() for release, environment, and SDK metadata. + if "release" in event: + attributes["sentry.release"] = event["release"] + if "environment" in event: + attributes["sentry.environment"] = event["environment"] + if "server_name" in event: + attributes["server.address"] = event["server_name"] + if "transaction" in event: + attributes["sentry.segment.name"] = event["transaction"] + + trace_context = event.get("contexts", {}).get("trace", {}) + if "span_id" in trace_context: + attributes["sentry.segment.id"] = trace_context["span_id"] + + sdk_info = event.get("sdk") + if isinstance(sdk_info, dict): + if "name" in sdk_info: + attributes["sentry.sdk.name"] = sdk_info["name"] + if "version" in sdk_info: + attributes["sentry.sdk.version"] = sdk_info["version"] + + attributes["process.runtime.name"] = platform.python_implementation() + attributes["process.runtime.version"] = ( + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + ) + + if not attributes: + return res + + res["attributes"] = {} + for key, value in attributes.items(): + converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) + if converted_value is None: + continue + + res["attributes"][key] = converted_value + + # Remove redundant attribute, as status is stored in the status field. + if "status" in res["attributes"]: + del res["attributes"]["status"] + + return res + + +def _split_gen_ai_spans( + event_opt: "Event", +) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": + if "spans" not in event_opt: + return None + + spans: "Any" = event_opt["spans"] + if isinstance(spans, AnnotatedValue): + spans = spans.value + + if not isinstance(spans, Iterable): + return None + + non_gen_ai_spans = [] + gen_ai_spans = [] + for span in spans: + if not isinstance(span, dict): + non_gen_ai_spans.append(span) + continue + + span_op = span.get("op") + if isinstance(span_op, str) and span_op.startswith("gen_ai."): + gen_ai_spans.append(span) + else: + non_gen_ai_spans.append(span) + + return non_gen_ai_spans, gen_ai_spans + + +def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": + if args and (isinstance(args[0], (bytes, str)) or args[0] is None): + dsn: "Optional[str]" = args[0] + args = args[1:] + else: + dsn = None + + if len(args) > 1: + raise TypeError("Only single positional argument is expected") + + rv = dict(DEFAULT_OPTIONS) + options = dict(*args, **kwargs) + if dsn is not None and options.get("dsn") is None: + options["dsn"] = dsn + + for key, value in options.items(): + if key not in rv: + raise TypeError("Unknown option %r" % (key,)) + + rv[key] = value + + if rv["dsn"] is None: + rv["dsn"] = os.environ.get("SENTRY_DSN") + + if rv["release"] is None: + rv["release"] = get_default_release() + + if rv["environment"] is None: + rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" + + if rv["debug"] is None: + rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False + + if rv["server_name"] is None and hasattr(socket, "gethostname"): + rv["server_name"] = socket.gethostname() + + if rv["instrumenter"] is None: + rv["instrumenter"] = INSTRUMENTER.SENTRY + + if rv["project_root"] is None: + try: + project_root = os.getcwd() + except Exception: + project_root = None + + rv["project_root"] = project_root + + 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"] + ) + + if rv["socket_options"] and not isinstance(rv["socket_options"], list): + logger.warning( + "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." + ) + rv["socket_options"] = None + + if rv["keep_alive"] is None: + rv["keep_alive"] = ( + env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False + ) + + if rv["enable_tracing"] is not None: + warnings.warn( + "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", + DeprecationWarning, + stacklevel=2, + ) + + if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): + warnings.warn( + "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", + stacklevel=2, + ) + + return rv + + +try: + # Python 3.6+ + module_not_found_error = ModuleNotFoundError +except Exception: + # Older Python versions + module_not_found_error = ImportError # type: ignore + + +class BaseClient: + """ + .. versionadded:: 2.0.0 + + The basic definition of a client that is used for sending data to Sentry. + """ + + spotlight: "Optional[SpotlightClient]" = None + + def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: + self.options: "Dict[str, Any]" = ( + options if options is not None else DEFAULT_OPTIONS + ) + + self.transport: "Optional[Transport]" = None + self.monitor: "Optional[Monitor]" = None + self.log_batcher: "Optional[LogBatcher]" = None + self.metrics_batcher: "Optional[MetricsBatcher]" = None + self.span_batcher: "Optional[SpanBatcher]" = None + self.integrations: "dict[str, Integration]" = {} + + def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": + return {"options": {}} + + def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: + pass + + @property + def dsn(self) -> "Optional[str]": + return None + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + return None + + def should_send_default_pii(self) -> bool: + return False + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return False + + def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + return None + + def _capture_log(self, log: "Log", scope: "Scope") -> None: + pass + + def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: + pass + + def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: + pass + + def capture_session(self, *args: "Any", **kwargs: "Any") -> None: + return None + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, name_or_class: "Union[str, type[Integration]]" + ) -> "Optional[Integration]": + return None + + def close(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def flush(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def close_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def __enter__(self) -> "BaseClient": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + return None + + +class NonRecordingClient(BaseClient): + """ + .. versionadded:: 2.0.0 + + A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + """ + + pass + + +class _Client(BaseClient): + """ + The client is internally responsible for capturing the events and + forwarding them to sentry through the configured transport. It takes + the client options as keyword arguments and optionally the DSN as first + argument. + + Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) + """ + + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + super(_Client, self).__init__(options=get_options(*args, **kwargs)) + self._init_impl() + + def __getstate__(self) -> "Any": + return {"options": self.options} + + def __setstate__(self, state: "Any") -> None: + self.options = state["options"] + self._init_impl() + + def _setup_instrumentation( + self, functions_to_trace: "Sequence[Dict[str, str]]" + ) -> None: + """ + Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. + """ + for function in functions_to_trace: + class_name = None + function_qualname = function["qualified_name"] + + if "." not in function_qualname: + logger.warning( + "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", + function_qualname, + ) + continue + + module_name, function_name = function_qualname.rsplit(".", 1) + + try: + # Try to import module and function + # ex: "mymodule.submodule.funcname" + + module_obj = import_module(module_name) + function_obj = getattr(module_obj, function_name) + setattr(module_obj, function_name, trace(function_obj)) + logger.debug("Enabled tracing for %s", function_qualname) + except module_not_found_error: + try: + # Try to import a class + # ex: "mymodule.submodule.MyClassName.member_function" + + module_name, class_name = module_name.rsplit(".", 1) + module_obj = import_module(module_name) + class_obj = getattr(module_obj, class_name) + function_obj = getattr(class_obj, function_name) + function_type = type(class_obj.__dict__[function_name]) + traced_function = trace(function_obj) + + if function_type in (staticmethod, classmethod): + traced_function = staticmethod(traced_function) + + setattr(class_obj, function_name, traced_function) + setattr(module_obj, class_name, class_obj) + logger.debug("Enabled tracing for %s", function_qualname) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + def _init_impl(self) -> None: + old_debug = _client_init_debug.get(False) + + def _capture_envelope(envelope: "Envelope") -> None: + if self.spotlight is not None: + self.spotlight.capture_envelope(envelope) + if self.transport is not None: + self.transport.capture_envelope(envelope) + + def _record_lost_event( + reason: str, + data_category: "EventDataCategory", + item: "Optional[Item]" = None, + quantity: int = 1, + ) -> None: + if self.transport is not None: + self.transport.record_lost_event( + reason=reason, + data_category=data_category, + item=item, + quantity=quantity, + ) + + try: + _client_init_debug.set(self.options["debug"]) + self.transport = make_transport(self.options) + + self.monitor = None + if self.transport: + if self.options["enable_backpressure_handling"]: + self.monitor = Monitor(self.transport) + + # Setup Spotlight before creating batchers so _capture_envelope can use it. + # setup_spotlight handles all config/env var resolution per the SDK spec. + from sentry_sdk.spotlight import setup_spotlight + + self.spotlight = setup_spotlight(self.options) + if self.spotlight is not None and not self.options["dsn"]: + sample_all = lambda *_args, **_kwargs: 1.0 + self.options["send_default_pii"] = True + 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, + ) + + self.session_flusher = SessionFlusher(capture_func=_capture_envelope) + + self.log_batcher = None + + if has_logs_enabled(self.options): + from sentry_sdk._log_batcher import LogBatcher + + self.log_batcher = LogBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.metrics_batcher = None + if has_metrics_enabled(self.options): + self.metrics_batcher = MetricsBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.span_batcher = None + if has_span_streaming_enabled(self.options): + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + max_request_body_size = ("always", "never", "small", "medium") + if self.options["max_request_body_size"] not in max_request_body_size: + raise ValueError( + "Invalid value for max_request_body_size. Must be one of {}".format( + max_request_body_size + ) + ) + + if self.options["_experiments"].get("otel_powered_performance", False): + logger.debug( + "[OTel] Enabling experimental OTel-powered performance monitoring." + ) + self.options["instrumenter"] = INSTRUMENTER.OTEL + if ( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" + not in _DEFAULT_INTEGRATIONS + ): + _DEFAULT_INTEGRATIONS.append( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", + ) + + self.integrations = setup_integrations( + self.options["integrations"], + with_defaults=self.options["default_integrations"], + with_auto_enabling_integrations=self.options[ + "auto_enabling_integrations" + ], + disabled_integrations=self.options["disabled_integrations"], + options=self.options, + ) + + sdk_name = get_sdk_name(list(self.integrations.keys())) + SDK_INFO["name"] = sdk_name + logger.debug("Setting SDK name to '%s'", sdk_name) + + if has_profiling_enabled(self.options): + try: + setup_profiler(self.options) + except Exception as e: + logger.debug("Can not set up profiler. (%s)", e) + else: + try: + setup_continuous_profiler( + self.options, + sdk_info=SDK_INFO, + capture_func=_capture_envelope, + ) + except Exception as e: + logger.debug("Can not set up continuous profiler. (%s)", e) + + finally: + _client_init_debug.set(old_debug) + + self._setup_instrumentation(self.options.get("functions_to_trace", [])) + + if ( + self.monitor + or self.log_batcher + or self.metrics_batcher + or self.span_batcher + or has_profiling_enabled(self.options) + or isinstance(self.transport, HttpTransportCore) + ): + # If we have anything on that could spawn a background thread, we + # need to check if it's safe to use them. + check_uwsgi_thread_support() + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return True + + def should_send_default_pii(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. + """ + return self.options.get("send_default_pii") or False + + @property + def dsn(self) -> "Optional[str]": + """Returns the configured DSN as string.""" + return self.options["dsn"] + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + """Returns the configured parsed DSN object.""" + return self.transport.parsed_dsn if self.transport else None + + def _prepare_event( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]", + ) -> "Optional[Event]": + previous_total_spans: "Optional[int]" = None + previous_total_breadcrumbs: "Optional[int]" = None + + if event.get("timestamp") is None: + event["timestamp"] = datetime.now(timezone.utc) + + is_transaction = event.get("type") == "transaction" + + if scope is not None: + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + event_ = scope.apply_to_event(event, hint, self.options) + + # one of the event/error processors returned None + if event_ is None: + if self.transport: + self.transport.record_lost_event( + "event_processor", + data_category=("transaction" if is_transaction else "error"), + ) + if is_transaction: + self.transport.record_lost_event( + "event_processor", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + return None + + event = event_ + spans_delta = spans_before - len( + cast(List[Dict[str, object]], event.get("spans", [])) + ) + span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) + + if is_transaction and self.transport is not None: + if spans_delta > 0: + self.transport.record_lost_event( + "event_processor", data_category="span", quantity=spans_delta + ) + if span_recorder_dropped_spans > 0: + self.transport.record_lost_event( + "buffer_overflow", + data_category="span", + quantity=span_recorder_dropped_spans, + ) + + dropped_spans: int = span_recorder_dropped_spans + spans_delta + if dropped_spans > 0: + previous_total_spans = spans_before + dropped_spans + if scope._n_breadcrumbs_truncated > 0: + breadcrumbs = event.get("breadcrumbs", {}) + values = ( + breadcrumbs.get("values", []) + if not isinstance(breadcrumbs, AnnotatedValue) + else [] + ) + previous_total_breadcrumbs = ( + len(values) + scope._n_breadcrumbs_truncated + ) + + if ( + not is_transaction + and self.options["attach_stacktrace"] + and "exception" not in event + and "stacktrace" not in event + and "threads" not in event + ): + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=self.options.get( + "include_local_variables", True + ), + max_value_length=self.options.get( + "max_value_length", DEFAULT_MAX_VALUE_LENGTH + ), + ), + "crashed": False, + "current": True, + } + ] + } + + for key in "release", "environment", "server_name", "dist": + if event.get(key) is None and self.options[key] is not None: + event[key] = str(self.options[key]).strip() + if event.get("sdk") is None: + sdk_info = dict(SDK_INFO) + sdk_info["integrations"] = sorted(self.integrations.keys()) + event["sdk"] = sdk_info + + if event.get("platform") is None: + event["platform"] = "python" + + event = handle_in_app( + event, + self.options["in_app_exclude"], + self.options["in_app_include"], + self.options["project_root"], + ) + + if event is not None: + event_scrubber = self.options["event_scrubber"] + if event_scrubber: + event_scrubber.scrub_event(event) + + if scope is not None and scope._gen_ai_original_message_count: + spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) + if isinstance(spans, list): + for span in spans: + span_id = span.get("span_id", None) + span_data = span.get("data", {}) + if ( + span_id + and span_id in scope._gen_ai_original_message_count + and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data + ): + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], + {"len": scope._gen_ai_original_message_count[span_id]}, + ) + if previous_total_spans is not None: + event["spans"] = AnnotatedValue( + event.get("spans", []), {"len": previous_total_spans} + ) + if previous_total_breadcrumbs is not None: + event["breadcrumbs"] = AnnotatedValue( + event.get("breadcrumbs", {"values": []}), + {"len": previous_total_breadcrumbs}, + ) + + # Postprocess the event here so that annotated types do + # generally not surface in before_send + if event is not None: + event = cast( + "Event", + serialize( + cast("Dict[str, Any]", event), + max_request_body_size=self.options.get("max_request_body_size"), + max_value_length=self.options.get("max_value_length"), + custom_repr=self.options.get("custom_repr"), + ), + ) + + before_send = self.options["before_send"] + if ( + before_send is not None + and event is not None + and event.get("type") != "transaction" + ): + new_event = None + with capture_internal_exceptions(): + new_event = before_send(event, hint or {}) + if new_event is None: + logger.info("before send dropped event") + if self.transport: + self.transport.record_lost_event( + "before_send", data_category="error" + ) + + # If this is an exception, reset the DedupeIntegration. It still + # remembers the dropped exception as the last exception, meaning + # that if the same exception happens again and is not dropped + # in before_send, it'd get dropped by DedupeIntegration. + if event.get("exception"): + DedupeIntegration.reset_last_seen() + + event = new_event + + before_send_transaction = self.options["before_send_transaction"] + if ( + before_send_transaction is not None + and event is not None + and event.get("type") == "transaction" + ): + new_event = None + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + with capture_internal_exceptions(): + new_event = before_send_transaction(event, hint or {}) + if new_event is None: + logger.info("before send transaction dropped event") + if self.transport: + self.transport.record_lost_event( + reason="before_send", data_category="transaction" + ) + self.transport.record_lost_event( + reason="before_send", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + else: + spans_delta = spans_before - len(new_event.get("spans", [])) + if spans_delta > 0 and self.transport is not None: + self.transport.record_lost_event( + reason="before_send", data_category="span", quantity=spans_delta + ) + + event = new_event + + return event + + def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: + exc_info = hint.get("exc_info") + if exc_info is None: + return False + + error = exc_info[0] + error_type_name = get_type_name(exc_info[0]) + error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) + + for ignored_error in self.options["ignore_errors"]: + # String types are matched against the type name in the + # exception only + if isinstance(ignored_error, str): + if ignored_error == error_full_name or ignored_error == error_type_name: + return True + else: + if issubclass(error, ignored_error): + return True + + return False + + def _should_capture( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]" = None, + ) -> bool: + # Transactions are sampled independent of error events. + is_transaction = event.get("type") == "transaction" + if is_transaction: + return True + + ignoring_prevents_recursion = scope is not None and not scope._should_capture + if ignoring_prevents_recursion: + return False + + ignored_by_config_option = self._is_ignored_error(event, hint) + if ignored_by_config_option: + return False + + return True + + def _should_sample_error( + self, + event: "Event", + hint: "Hint", + ) -> bool: + error_sampler = self.options.get("error_sampler", None) + + if callable(error_sampler): + with capture_internal_exceptions(): + sample_rate = error_sampler(event, hint) + else: + sample_rate = self.options["sample_rate"] + + try: + not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate + except NameError: + logger.warning( + "The provided error_sampler raised an error. Defaulting to sampling the event." + ) + + # If the error_sampler raised an error, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + except TypeError: + parameter, verb = ( + ("error_sampler", "returned") + if callable(error_sampler) + else ("sample_rate", "contains") + ) + logger.warning( + "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." + % (parameter, verb, repr(sample_rate)) + ) + + # If the sample_rate has an invalid value, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + + if not_in_sample_rate: + # because we will not sample this event, record a "lost event". + if self.transport: + self.transport.record_lost_event("sample_rate", data_category="error") + + return False + + return True + + def _update_session_from_event( + self, + session: "Session", + event: "Event", + ) -> None: + crashed = False + errored = False + user_agent = None + + exceptions = (event.get("exception") or {}).get("values") + if exceptions: + errored = True + for error in exceptions: + if isinstance(error, AnnotatedValue): + error = error.value or {} + mechanism = error.get("mechanism") + if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: + crashed = True + break + + user = event.get("user") + + if session.user_agent is None: + headers = (event.get("request") or {}).get("headers") + headers_dict = headers if isinstance(headers, dict) else {} + for k, v in headers_dict.items(): + if k.lower() == "user-agent": + user_agent = v + break + + session.update( + status="crashed" if crashed else None, + user=user, + user_agent=user_agent, + errors=session.errors + (errored or crashed), + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + ) -> "Optional[str]": + """Captures an event. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + + :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. + """ + hint = dict(hint or ()) + + if not self._should_capture(event, hint, scope): + return None + + profile = event.pop("profile", None) + + event_id = event.get("event_id") + if event_id is None: + event["event_id"] = event_id = uuid.uuid4().hex + + span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) + event_opt = self._prepare_event(event, hint, scope) + if event_opt is None: + return None + + # whenever we capture an event we also check if the session needs + # to be updated based on that information. + session = scope._session if scope else None + if session: + self._update_session_from_event(session, event) + + is_transaction = event_opt.get("type") == "transaction" + is_checkin = event_opt.get("type") == "check_in" + + if ( + not is_transaction + and not is_checkin + and not self._should_sample_error(event, hint) + ): + return None + + attachments = hint.get("attachments") + + trace_context = event_opt.get("contexts", {}).get("trace") or {} + dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) + + headers: "dict[str, object]" = { + "event_id": event_opt["event_id"], + "sent_at": format_timestamp(datetime.now(timezone.utc)), + } + + if dynamic_sampling_context: + headers["trace"] = dynamic_sampling_context + + envelope = Envelope(headers=headers) + + if is_transaction and isinstance(profile, Profile): + envelope.add_profile(profile.to_json(event_opt, self.options)) + + if is_transaction and not span_recorder_has_gen_ai_span: + envelope.add_transaction(event_opt) + elif is_transaction: + split_spans = _split_gen_ai_spans(event_opt) + if split_spans is None or not split_spans[1]: + envelope.add_transaction(event_opt) + else: + non_gen_ai_spans, gen_ai_spans = split_spans + + event_opt["spans"] = non_gen_ai_spans + envelope.add_transaction(event_opt) + + converted_gen_ai_spans = [ + _serialized_v1_span_to_serialized_v2_span(span, event_opt) + for span in gen_ai_spans + if isinstance(span, dict) + ] + + envelope.add_item( + Item( + type=SpanBatcher.TYPE, + content_type=SpanBatcher.CONTENT_TYPE, + headers={ + "item_count": len(converted_gen_ai_spans), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": converted_gen_ai_spans, + }, + ), + ) + ) + + elif is_checkin: + envelope.add_checkin(event_opt) + else: + envelope.add_event(event_opt) + + for attachment in attachments or (): + envelope.add_item(attachment.to_envelope_item()) + + return_value = None + if self.spotlight: + self.spotlight.capture_envelope(envelope) + return_value = event_id + + if self.transport is not None: + self.transport.capture_envelope(envelope) + return_value = event_id + + return return_value + + def _capture_telemetry( + self, + telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", + ty: str, + scope: "Scope", + ) -> None: + """ + Capture attributes-based telemetry (logs, metrics, streamed spans). + + Apply any attributes set on the scope to it, and run the user's + before_send_{telemetry} on it, if applicable. + """ + if telemetry is None: + return + + scope.apply_to_telemetry(telemetry) + + before_send = None + + if ty == "log": + before_send = get_before_send_log(self.options) + serialized = telemetry + + elif ty == "metric": + before_send = get_before_send_metric(self.options) + serialized = telemetry + + elif ty == "span": + before_send = get_before_send_span(self.options) + serialized = telemetry._to_json() # type: ignore[union-attr] + + if before_send is not None: + serialized = before_send(serialized, {}) # type: ignore[arg-type] + + if ty in ("log", "metric"): + # Logs and metrics can be dropped in their respective + # before_send, so if we get None, don't queue them for sending. + if serialized is None: + return + + elif ty == "span" and isinstance(telemetry, StreamedSpan): + # Spans can't be dropped in before_send_span by design. They can + # be altered though (e.g. to sanitize). Only allow changes to + # name and attributes. + if isinstance(serialized, dict) and "name" in serialized: + telemetry.name = serialized["name"] # type: ignore[typeddict-item] + telemetry._attributes = {} + for k, v in (serialized.get("attributes") or {}).items(): + telemetry.set_attribute(k, v) + + else: + logger.debug( + "[Tracing] Invalid return value from before_send_span. Keeping original span." + ) + + serialized = telemetry._to_json() + + batcher = None + if ty == "log": + batcher = self.log_batcher + + elif ty == "metric": + batcher = self.metrics_batcher + + elif ty == "span": + # We need a reference to the segment span in the batcher to populate + # the dynamic sampling context (DSC) + serialized["_segment_span"] = telemetry._segment # type: ignore + batcher = self.span_batcher + + if batcher is not None: + batcher.add(serialized) # type: ignore + + def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: + self._capture_telemetry(log, "log", scope) + + def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: + self._capture_telemetry(metric, "metric", scope) + + def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: + self._capture_telemetry(span, "span", scope) + + def capture_session( + self, + session: "Session", + ) -> None: + if not session.release: + logger.info("Discarded session update because of missing release") + else: + self.session_flusher.add_session(session) + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Optional[Integration]": + """Returns the integration for this client by name or class. + If the client does not have that integration then `None` is returned. + """ + if isinstance(name_or_class, str): + integration_name = name_or_class + elif name_or_class.identifier is not None: + integration_name = name_or_class.identifier + else: + raise ValueError("Integration has no name") + + return self.integrations.get(integration_name) + + def _has_async_transport(self) -> bool: + """Check if the current transport is async.""" + return isinstance(self.transport, AsyncHttpTransport) + + @property + def _batchers(self) -> "tuple[Any, ...]": + return tuple( + b + for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) + if b is not None + ) + + def _close_components(self) -> None: + """Kill all client components in the correct order.""" + self.session_flusher.kill() + for b in self._batchers: + b.kill() + if self.monitor: + self.monitor.kill() + + def _flush_components(self) -> None: + """Flush all client components.""" + self.session_flusher.flush() + for b in self._batchers: + b.flush() + + def close( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush`. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "close() used with AsyncHttpTransport. Use close_async() instead.", + stacklevel=2, + ) + self._flush_components() + else: + self.flush(timeout=timeout, callback=callback) + self._close_components() + self.transport.kill() + self.transport = None + + async def close_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush_async`. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "close_async() used with non-async transport, aborting. Please use close() instead." + ) + return + await self.flush_async(timeout=timeout, callback=callback) + self._close_components() + kill_task = self.transport.kill() # type: ignore + if kill_task is not None: + await kill_task + self.transport = None + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "flush() used with AsyncHttpTransport. Use flush_async() instead.", + stacklevel=2, + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + + self.transport.flush(timeout=timeout, callback=callback) + + async def flush_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "flush_async() used with non-async transport, aborting. Please use flush() instead." + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore + if flush_task is not None: + await flush_task + + def __enter__(self) -> "_Client": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + self.close() + + async def __aenter__(self) -> "_Client": + return self + + async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + await self.close_async() + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `get_options` is a + # type to have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `Dict[str, Any]` to tell static analyzers about the return type. + + class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 + pass + + class Client(ClientConstructor, _Client): + pass + +else: + # Alias `get_options` for actual usage. Go through the lambda indirection + # to throw PyCharm off of the weakly typed signature (it would otherwise + # discover both the weakly typed signature of `_init` and our faked `init` + # type). + + get_options = (lambda: _get_options)() + Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py new file mode 100644 index 0000000000..759898f6ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py @@ -0,0 +1,1802 @@ +import itertools +from enum import Enum +from typing import TYPE_CHECKING + +DEFAULT_MAX_VALUE_LENGTH = None + +DEFAULT_MAX_STACK_FRAMES = 100 +DEFAULT_ADD_FULL_STACK = False + + +# Also needs to be at the top to prevent circular import +class EndpointType(Enum): + """ + The type of an endpoint. This is an enum, rather than a constant, for historical reasons + (the old /store endpoint). The enum also preserve future compatibility, in case we ever + have a new endpoint. + """ + + ENVELOPE = "envelope" + OTLP_TRACES = "integration/otlp/v1/traces" + + +class CompressionAlgo(Enum): + GZIP = "gzip" + BROTLI = "br" + + +if TYPE_CHECKING: + from typing import ( + AbstractSet, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + ) + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + from sentry_sdk._types import ( + BreadcrumbProcessor, + ContinuousProfilerMode, + DataCollectionUserOptions, + Event, + EventProcessor, + Hint, + IgnoreSpansConfig, + Log, + Metric, + ProfilerMode, + SpanJSON, + TracesSampler, + TransactionProcessor, + ) + + # Experiments are feature flags to enable and disable certain unstable SDK + # functionality. Changing them from the defaults (`None`) in production + # code is highly discouraged. They are not subject to any stability + # guarantees such as the ones from semantic versioning. + Experiments = TypedDict( + "Experiments", + { + "max_spans": Optional[int], + "max_flags": Optional[int], + "record_sql_params": Optional[bool], + "continuous_profiling_auto_start": Optional[bool], + "continuous_profiling_mode": Optional[ContinuousProfilerMode], + "otel_powered_performance": Optional[bool], + "transport_zlib_compression_level": Optional[int], + "transport_compression_level": Optional[int], + "transport_compression_algo": Optional[CompressionAlgo], + "transport_num_pools": Optional[int], + "transport_http2": Optional[bool], + "transport_async": Optional[bool], + "enable_logs": Optional[bool], + "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], + "enable_metrics": Optional[bool], + "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], + "trace_lifecycle": Optional[Literal["static", "stream"]], + "ignore_spans": Optional[IgnoreSpansConfig], + "before_send_span": Optional[ + Callable[[SpanJSON, Hint], Optional[SpanJSON]] + ], + "suppress_asgi_chained_exceptions": Optional[bool], + "data_collection": Optional[DataCollectionUserOptions], + }, + total=False, + ) + +DEFAULT_QUEUE_SIZE = 100 +DEFAULT_MAX_BREADCRUMBS = 100 +MATCH_ALL = r".*" + +FALSE_VALUES = [ + "false", + "no", + "off", + "n", + "0", +] + + +class SPANTEMPLATE(str, Enum): + DEFAULT = "default" + AI_AGENT = "ai_agent" + AI_TOOL = "ai_tool" + AI_CHAT = "ai_chat" + + def __str__(self) -> str: + return self.value + + +class INSTRUMENTER: + SENTRY = "sentry" + OTEL = "otel" + + +class SPANNAME: + DB_COMMIT = "COMMIT" + DB_ROLLBACK = "ROLLBACK" + + +class SPANDATA: + """ + Additional information describing the type of the span. + See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ + """ + + AI_CITATIONS = "ai.citations" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + References or sources cited by the AI model in its response. + Example: ["Smith et al. 2020", "Jones 2019"] + """ + + AI_DOCUMENTS = "ai.documents" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Documents or content chunks used as context for the AI model. + Example: ["doc1.txt", "doc2.pdf"] + """ + + AI_FINISH_REASON = "ai.finish_reason" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. + + The reason why the model stopped generating. + Example: "length" + """ + + AI_FREQUENCY_PENALTY = "ai.frequency_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_FUNCTION_CALL = "ai.function_call" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_GENERATION_ID = "ai.generation_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. + + Unique identifier for the completion. + Example: "gen_123abc" + """ + + AI_INPUT_MESSAGES = "ai.input_messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. + + The input messages to an LLM call. + Example: [{"role": "user", "message": "hello"}] + """ + + AI_LOGIT_BIAS = "ai.logit_bias" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the logit bias + """ + + AI_METADATA = "ai.metadata" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Extra metadata passed to an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_MODEL_ID = "ai.model_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. + + The unique descriptor of the model being executed. + Example: gpt-4 + """ + + AI_PIPELINE_NAME = "ai.pipeline.name" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. + + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + AI_PREAMBLE = "ai.preamble" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the preamble parameter. + Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. + Example: "You are now a clown." + """ + + AI_PRESENCE_PENALTY = "ai.presence_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_RAW_PROMPTING = "ai.raw_prompting" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Minimize pre-processing done to the prompt sent to the LLM. + Example: true + """ + + AI_RESPONSE_FORMAT = "ai.response_format" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the format of the response + """ + + AI_RESPONSES = "ai.responses" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. + + The responses to an AI model call. Always as a list. + Example: ["hello", "world"] + """ + + AI_SEARCH_QUERIES = "ai.search_queries" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Queries used to search for relevant context or documents. + Example: ["climate change effects", "renewable energy"] + """ + + AI_SEARCH_REQUIRED = "ai.is_search_required" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Boolean indicating if the model needs to perform a search. + Example: true + """ + + AI_SEARCH_RESULTS = "ai.search_results" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Results returned from search queries for context. + Example: ["Result 1", "Result 2"] + """ + + AI_SEED = "ai.seed" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. + + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: 123.45 + """ + + AI_STREAMING = "ai.streaming" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. + + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + AI_TAGS = "ai.tags" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Tags that describe an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_TEMPERATURE = "ai.temperature" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. + + For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. + Example: 0.5 + """ + + AI_TEXTS = "ai.texts" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Raw text inputs provided to the model. + Example: ["What is machine learning?"] + """ + + AI_TOP_K = "ai.top_k" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. + + For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. + Example: 35 + """ + + AI_TOP_P = "ai.top_p" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. + + For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. + Example: 0.5 + """ + + AI_TOOL_CALLS = "ai.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_TOOLS = "ai.tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. + + For an AI model call, the functions that are available + """ + + AI_WARNINGS = "ai.warnings" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Warning messages generated during model execution. + Example: ["Token limit exceeded"] + """ + + CACHE_HIT = "cache.hit" + """ + A boolean indicating whether the requested data was found in the cache. + Example: true + """ + + CACHE_ITEM_SIZE = "cache.item_size" + """ + The size of the requested data in bytes. + Example: 58 + """ + + CACHE_KEY = "cache.key" + """ + The key of the requested data. + Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 + """ + + CLIENT_ADDRESS = "client.address" + """ + Client address of the network connection - IP address or Unix domain socket name. + Example: "10.1.2.80" + """ + + CODE_FILEPATH = "code.filepath" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FILE_PATH instead. + + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FILE_PATH = "code.file.path" + """ + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FUNCTION = "code.function" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead. + + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_FUNCTION_NAME = "code.function.name" + """ + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_LINENO = "code.lineno" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_LINE_NUMBER instead. + + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + Example: 42 + """ + + CODE_LINE_NUMBER = "code.line.number" + """ + The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. + Example: 42 + """ + + CODE_NAMESPACE = "code.namespace" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. + + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + Example: "http.handler" + """ + + DB_MONGODB_COLLECTION = "db.mongodb.collection" + """ + The MongoDB collection being accessed within the database. + See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes + Example: public.users; customers + """ + + DB_NAME = "db.name" + """ + .. deprecated:: + This attribute is deprecated. Use DB_NAMESPACE instead. + + The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Example: myDatabase + """ + + DB_NAMESPACE = "db.namespace" + """ + The name of the database being accessed. + Example: "customers" + """ + + DB_DRIVER_NAME = "db.driver.name" + """ + The name of the database driver being used for the connection. + Example: "psycopg2" + """ + + DB_OPERATION = "db.operation" + """ + .. deprecated:: + This attribute is deprecated. Use DB_OPERATION_NAME instead. + + The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: findAndModify, HMSET, SELECT + """ + + DB_OPERATION_NAME = "db.operation.name" + """ + The name of the operation being executed. + Example: "SELECT" + """ + + DB_SYSTEM = "db.system" + """ + .. deprecated:: + This attribute is deprecated. Use DB_SYSTEM_NAME instead. + + An identifier for the database management system (DBMS) product being used. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: postgresql + """ + + DB_QUERY_TEXT = "db.query.text" + """ + The database query being executed. + Example: "SELECT * FROM users WHERE id = $1" + """ + + DB_SYSTEM_NAME = "db.system.name" + """ + An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. + Example: "postgresql" + """ + + DB_USER = "db.user" + """ + The name of the database user used for connecting to the database. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: my_user + """ + + GEN_AI_AGENT_NAME = "gen_ai.agent.name" + """ + The name of the agent being used. + Example: "ResearchAssistant" + """ + + GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" + """ + The unique identifier for the conversation/thread with the AI model. + Example: "conv_abc123" + """ + + GEN_AI_CHOICE = "gen_ai.choice" + """ + The model's response message. + Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" + """ + + GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" + """ + The input to the embeddings operation. + Example: "Hello!" + """ + + GEN_AI_FUNCTION_ID = "gen_ai.function_id" + """ + Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. + Example: "my-awesome-function" + """ + + GEN_AI_OPERATION_NAME = "gen_ai.operation.name" + """ + The name of the operation being performed. + Example: "chat" + """ + + GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" + """ + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" + """ + The reason why the model stopped generating. + Example: "COMPLETE" + """ + + GEN_AI_RESPONSE_ID = "gen_ai.response.id" + """ + Unique identifier for the completion. + Example: "gen_123abc" + """ + + GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" + """ + Exact model identifier used to generate the response + Example: gpt-4o-mini-2024-07-18 + """ + + GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" + """ + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The model's response text messages. + Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] + """ + + GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" + """ + The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. + Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] + """ + + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" + """ + The time it took to receive the first token from the model. + Example: 0.1 + """ + + GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The tool calls in the model's response. + Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] + """ + + GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. + + The available tools for the model. + Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] + """ + + GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + """ + The list of source system tool definitions available to the GenAI agent or model. + Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] + """ + + GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" + """ + The frequency penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" + """ + The maximum number of tokens to generate in the response. + Example: 2048 + """ + + GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" + """ + The system instructions passed to the model. + Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] + """ + + GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. + + The messages passed to the model. The "content" can be a string or an array of objects. + Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] + """ + + GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" + """ + The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". + Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] + """ + + GEN_AI_REQUEST_MODEL = "gen_ai.request.model" + """ + The model identifier being used for the request. + Example: "gpt-4-turbo" + """ + + GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" + """ + The presence penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_SEED = "gen_ai.request.seed" + """ + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: "1234567890" + """ + + GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" + """ + The temperature parameter used to control randomness in the output. + Example: 0.7 + """ + + GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" + """ + Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). + Example: 35 + """ + + GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" + """ + The top_p parameter used to control diversity via nucleus sampling. + Example: 1.0 + """ + + GEN_AI_SYSTEM = "gen_ai.system" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. + + The name of the AI system being used. + Example: "openai" + """ + + GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" + """ + The Generative AI provider as identified by the client or server instrumentation. + Example: "openai" + """ + + GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" + """ + The description of the tool being used. + Example: "Searches the web for current information about a topic" + """ + + GEN_AI_TOOL_INPUT = "gen_ai.tool.input" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. + + The input of the tool being used. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" + """ + The arguments of the tool call. It has to be a stringified version of the arguments to the tool. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_NAME = "gen_ai.tool.name" + """ + The name of the tool being used. + Example: "web_search" + """ + + GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. + + The output of the tool being used. + Example: "rainy, 57°F" + """ + + GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" + """ + The result of the tool call. It has to be a stringified version of the result of the tool. + Example: "rainy, 57°F" + """ + + GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + """ + The number of tokens in the input. + Example: 150 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" + """ + The number of cached tokens in the input. + Example: 50 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" + """ + The number of tokens written to the cache when processing the AI input (prompt). + Example: 100 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + """ + The number of tokens in the output. + Example: 250 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" + """ + The number of tokens used for reasoning in the output. + Example: 75 + """ + + GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" + """ + The total number of tokens used (input + output). + Example: 400 + """ + + GEN_AI_USER_MESSAGE = "gen_ai.user.message" + """ + The user message passed to the model. + Example: "What's the weather in Paris?" + """ + + HTTP_FRAGMENT = "http.fragment" + """ + The Fragments present in the URL. + Example: #foo=bar + """ + + HTTP_METHOD = "http.method" + """ + .. deprecated:: + This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. + + The HTTP method used. + Example: GET + """ + + HTTP_REQUEST_BODY_DATA = "http.request.body.data" + """ + HTTP request body data. Can be given as string or structural data of any format. + Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" + """ + + HTTP_REQUEST_HEADER = "http.request.header" + """ + Prefix for HTTP request header attributes. The header name (lowercased) is + appended to form the full attribute key. + Example: "http.request.header.content-type" + """ + + HTTP_REQUEST_METHOD = "http.request.method" + """ + The HTTP method used. + Example: GET + """ + + HTTP_QUERY = "http.query" + """ + The Query string present in the URL. + Example: ?foo=bar&bar=baz + """ + + HTTP_STATUS_CODE = "http.response.status_code" + """ + The HTTP status code as an integer. + Example: 418 + """ + + MESSAGING_DESTINATION_NAME = "messaging.destination.name" + """ + The destination name where the message is being consumed from, + e.g. the queue name or topic. + """ + + MESSAGING_MESSAGE_ID = "messaging.message.id" + """ + The message's identifier. + """ + + MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" + """ + The latency between when the task was enqueued and when it was started to be processed. + """ + + MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" + """ + Number of retries/attempts to process a message. + """ + + MESSAGING_SYSTEM = "messaging.system" + """ + The messaging system's name, e.g. `kafka`, `aws_sqs` + """ + + MIDDLEWARE_NAME = "middleware.name" + """ + The middleware's name, e.g. `AuthenticationMiddleware` + """ + + NETWORK_PROTOCOL_NAME = "network.protocol.name" + """ + The application layer protocol name used for the network connection. + Example: "http", "https" + """ + + NETWORK_PEER_ADDRESS = "network.peer.address" + """ + Peer address of the network connection - IP address or Unix domain socket name. + Example: 10.1.2.80, /tmp/my.sock, localhost + """ + + NETWORK_PEER_PORT = "network.peer.port" + """ + Peer port number of the network connection. + Example: 6379 + """ + + NETWORK_TRANSPORT = "network.transport" + """ + The transport protocol used for the network connection. + Example: "tcp", "udp", "unix" + """ + + PROCESS_PID = "process.pid" + """ + The process ID of the running process. + Example: 12345 + """ + + PROCESS_COMMAND_ARGS = "process.command_args" + """ + All the command arguments (including the command/executable itself) as received by the process. + Example: ["cmd/otecol","--config=config.yaml"] + """ + + PROFILER_ID = "profiler_id" + """ + Label identifying the profiler id that the span occurred in. This should be a string. + Example: "5249fbada8d5416482c2f6e47e337372" + """ + + RPC_METHOD = "rpc.method" + """ + The fully-qualified logical name of the method from the RPC interface perspective. + Example: "com.example.ExampleService/exampleMethod" + """ + + RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" + """ + Status code of the RPC returned by the RPC server or generated by the client. + Example: "DEADLINE_EXCEEDED" + """ + + SERVER_ADDRESS = "server.address" + """ + Name of the database host. + Example: example.com + """ + + SERVER_PORT = "server.port" + """ + Logical server port number + Example: 80; 8080; 443 + """ + + SERVER_SOCKET_ADDRESS = "server.socket.address" + """ + Physical server IP address or Unix socket address. + Example: 10.5.3.2 + """ + + SERVER_SOCKET_PORT = "server.socket.port" + """ + Physical server port. + Recommended: If different than server.port. + Example: 16456 + """ + + THREAD_ID = "thread.id" + """ + Identifier of a thread from where the span originated. This should be a string. + Example: "7972576320" + """ + + THREAD_NAME = "thread.name" + """ + Label identifying a thread from where the span originated. This should be a string. + Example: "MainThread" + """ + + USER_EMAIL = "user.email" + """ + User email address. + Example: "test@example.com" + """ + + USER_ID = "user.id" + """ + Unique identifier of the user. + Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" + """ + + USER_IP_ADDRESS = "user.ip_address" + """ + The IP address of the user that triggered the request. + Example: "10.1.2.80" + """ + + USER_NAME = "user.name" + """ + Short name or login/username of the user. + Example: "j.smith" + """ + + URL_FULL = "url.full" + """ + The URL of the resource that was fetched. + Example: "https://example.com/test?foo=bar#buzz" + """ + + URL_FRAGMENT = "url.fragment" + """ + The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. + Example: "details" + """ + + URL_PATH = "url.path" + """ + The URI path component. + Example: "/foo" + """ + + URL_QUERY = "url.query" + """ + The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. + Example: "foo=bar&bar=baz" + """ + + MCP_TOOL_NAME = "mcp.tool.name" + """ + The name of the MCP tool being called. + Example: "get_weather" + """ + + MCP_PROMPT_NAME = "mcp.prompt.name" + """ + The name of the MCP prompt being retrieved. + Example: "code_review" + """ + + MCP_RESOURCE_URI = "mcp.resource.uri" + """ + The URI of the MCP resource being accessed. + Example: "file:///path/to/resource" + """ + + MCP_METHOD_NAME = "mcp.method.name" + """ + The MCP protocol method name being called. + Example: "tools/call", "prompts/get", "resources/read" + """ + + MCP_REQUEST_ID = "mcp.request.id" + """ + The unique identifier for the MCP request. + Example: "req_123abc" + """ + + MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" + """ + The result/output content from an MCP tool execution. + Example: "The weather is sunny" + """ + + MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" + """ + The number of items/keys in the MCP tool result. + Example: 5 + """ + + MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" + """ + Whether the MCP tool execution resulted in an error. + Example: True + """ + + MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" + """ + The message content from an MCP prompt retrieval. + Example: "Review the following code..." + """ + + MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" + """ + The role of the message in an MCP prompt retrieval (only set for single-message prompts). + Example: "user", "assistant", "system" + """ + + MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" + """ + The number of messages in an MCP prompt result. + Example: 1, 3 + """ + + MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" + """ + The protocol/scheme of the MCP resource URI. + Example: "file", "http", "https" + """ + + MCP_TRANSPORT = "mcp.transport" + """ + The transport method used for MCP communication. + Example: "http", "sse", "stdio" + """ + + MCP_SESSION_ID = "mcp.session.id" + """ + The session identifier for the MCP connection. + Example: "a1b2c3d4e5f6" + """ + + SENTRY_DIST = "sentry.dist" + """ + The Sentry dist. + Example: "1.0" + """ + + SENTRY_ENVIRONMENT = "sentry.environment" + """ + The Sentry environment. + Example: "prod" + """ + + SENTRY_RELEASE = "sentry.release" + """ + The Sentry release. + Example: "1.2.3" + """ + + SENTRY_PLATFORM = "sentry.platform" + """ + The sdk platform that generated the event. + Example: "python" + """ + + SENTRY_SDK_NAME = "sentry.sdk.name" + """ + The name of the SDK. + Example: "python" + """ + + SENTRY_SDK_VERSION = "sentry.sdk.version" + """ + The SDK version. + Example: "1.2.3" + """ + + SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" + """ + A list of names identifying enabled integrations. + Example: ["AtexitIntegration", "StdlibIntegration"] + """ + + +class SPANSTATUS: + """ + The status of a Sentry span. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + + ABORTED = "aborted" + ALREADY_EXISTS = "already_exists" + CANCELLED = "cancelled" + DATA_LOSS = "data_loss" + DEADLINE_EXCEEDED = "deadline_exceeded" + FAILED_PRECONDITION = "failed_precondition" + INTERNAL_ERROR = "internal_error" + INVALID_ARGUMENT = "invalid_argument" + NOT_FOUND = "not_found" + OK = "ok" + OUT_OF_RANGE = "out_of_range" + PERMISSION_DENIED = "permission_denied" + RESOURCE_EXHAUSTED = "resource_exhausted" + UNAUTHENTICATED = "unauthenticated" + UNAVAILABLE = "unavailable" + UNIMPLEMENTED = "unimplemented" + UNKNOWN_ERROR = "unknown_error" + + +class OP: + ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" + CACHE_GET = "cache.get" + CACHE_PUT = "cache.put" + COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" + COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" + DB = "db" + DB_CURSOR_ITERATOR = "db.cursor.iter" + DB_CURSOR_FETCH = "db.cursor.fetch" + DB_REDIS = "db.redis" + EVENT_DJANGO = "event.django" + FUNCTION = "function" + FUNCTION_AWS = "function.aws" + FUNCTION_GCP = "function.gcp" + GEN_AI_CHAT = "gen_ai.chat" + GEN_AI_CREATE_AGENT = "gen_ai.create_agent" + GEN_AI_EMBEDDINGS = "gen_ai.embeddings" + GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" + GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" + GEN_AI_HANDOFF = "gen_ai.handoff" + GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" + GEN_AI_RESPONSES = "gen_ai.responses" + GRAPHQL_EXECUTE = "graphql.execute" + GRAPHQL_MUTATION = "graphql.mutation" + GRAPHQL_PARSE = "graphql.parse" + GRAPHQL_RESOLVE = "graphql.resolve" + GRAPHQL_SUBSCRIPTION = "graphql.subscription" + GRAPHQL_QUERY = "graphql.query" + GRAPHQL_VALIDATE = "graphql.validate" + GRPC_CLIENT = "grpc.client" + GRPC_SERVER = "grpc.server" + HTTP_CLIENT = "http.client" + HTTP_CLIENT_STREAM = "http.client.stream" + HTTP_SERVER = "http.server" + MIDDLEWARE_DJANGO = "middleware.django" + MIDDLEWARE_LITESTAR = "middleware.litestar" + MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" + MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" + MIDDLEWARE_STARLETTE = "middleware.starlette" + MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" + MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" + MIDDLEWARE_STARLITE = "middleware.starlite" + MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" + MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" + HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( + "ai.chat_completions.create.huggingface_hub" + ) + QUEUE_PROCESS = "queue.process" + QUEUE_PUBLISH = "queue.publish" + QUEUE_SUBMIT_ARQ = "queue.submit.arq" + QUEUE_TASK_ARQ = "queue.task.arq" + QUEUE_SUBMIT_CELERY = "queue.submit.celery" + QUEUE_TASK_CELERY = "queue.task.celery" + QUEUE_TASK_RQ = "queue.task.rq" + QUEUE_SUBMIT_HUEY = "queue.submit.huey" + QUEUE_TASK_HUEY = "queue.task.huey" + QUEUE_SUBMIT_RAY = "queue.submit.ray" + QUEUE_TASK_RAY = "queue.task.ray" + QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" + QUEUE_SUBMIT_DJANGO = "queue.submit.django" + SUBPROCESS = "subprocess" + SUBPROCESS_WAIT = "subprocess.wait" + SUBPROCESS_COMMUNICATE = "subprocess.communicate" + TEMPLATE_RENDER = "template.render" + VIEW_RENDER = "view.render" + VIEW_RESPONSE_RENDER = "view.response.render" + WEBSOCKET_SERVER = "websocket.server" + SOCKET_CONNECTION = "socket.connection" + SOCKET_DNS = "socket.dns" + MCP_SERVER = "mcp.server" + + +# This type exists to trick mypy and PyCharm into thinking `init` and `Client` +# take these arguments (even though they take opaque **kwargs) +class ClientConstructor: + def __init__( + self, + dsn: "Optional[str]" = None, + *, + max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + server_name: "Optional[str]" = None, + shutdown_timeout: float = 2, + integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 + in_app_include: "List[str]" = [], # noqa: B006 + in_app_exclude: "List[str]" = [], # noqa: B006 + default_integrations: bool = True, + dist: "Optional[str]" = None, + transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, + transport_queue_size: int = DEFAULT_QUEUE_SIZE, + sample_rate: float = 1.0, + send_default_pii: "Optional[bool]" = None, + http_proxy: "Optional[str]" = None, + https_proxy: "Optional[str]" = None, + ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 + max_request_body_size: str = "medium", + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, + keep_alive: "Optional[bool]" = None, + before_send: "Optional[EventProcessor]" = None, + before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, + debug: "Optional[bool]" = None, + attach_stacktrace: bool = False, + ca_certs: "Optional[str]" = None, + propagate_traces: bool = True, + traces_sample_rate: "Optional[float]" = None, + traces_sampler: "Optional[TracesSampler]" = None, + profiles_sample_rate: "Optional[float]" = None, + profiles_sampler: "Optional[TracesSampler]" = None, + profiler_mode: "Optional[ProfilerMode]" = None, + profile_lifecycle: 'Literal["manual", "trace"]' = "manual", + profile_session_sample_rate: "Optional[float]" = None, + auto_enabling_integrations: bool = True, + disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, + auto_session_tracking: bool = True, + send_client_reports: bool = True, + _experiments: "Experiments" = {}, # noqa: B006 + proxy_headers: "Optional[Dict[str, str]]" = None, + instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, + before_send_transaction: "Optional[TransactionProcessor]" = None, + project_root: "Optional[str]" = None, + enable_tracing: "Optional[bool]" = None, + include_local_variables: "Optional[bool]" = True, + include_source_context: "Optional[bool]" = True, + trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 + MATCH_ALL + ], + functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 + event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, + max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, + enable_backpressure_handling: bool = True, + error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, + enable_db_query_source: bool = True, + db_query_source_threshold_ms: int = 100, + enable_http_request_source: bool = True, + http_request_source_threshold_ms: int = 100, + spotlight: "Optional[Union[bool, str]]" = None, + cert_file: "Optional[str]" = None, + key_file: "Optional[str]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, + add_full_stack: bool = DEFAULT_ADD_FULL_STACK, + max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, + enable_logs: bool = False, + before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, + trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), + enable_metrics: bool = True, + before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, + org_id: "Optional[str]" = None, + strict_trace_continuation: bool = False, + stream_gen_ai_spans: bool = True, + ) -> None: + """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. + + :param dsn: The DSN tells the SDK where to send the events. + + If this option is not set, the SDK will just not send any data. + + The `dsn` config option takes precedence over the environment variable. + + Learn more about `DSN utilization `_. + + :param debug: Turns debug mode on or off. + + When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes + wrong with event sending. + + The default is always `False`. It's generally not recommended to turn it on in production because of the + increase in log output. + + The `debug` config option takes precedence over the environment variable. + + :param release: Sets the release. + + If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to + manually set it to guarantee that the release is in sync with your deploy integrations. + + Release names are strings, but some formats are detected by Sentry and might be rendered differently. + + See `the releases documentation `_ to learn how the SDK tries to + automatically configure a release. + + The `release` config option takes precedence over the environment variable. + + Learn more about how to send release data so Sentry can tell you about regressions between releases and + identify the potential source in `the product documentation `_. + + :param environment: Sets the environment. This string is freeform and set to `production` by default. + + A release can be associated with more than one environment to separate them in the UI (think `staging` vs + `production` or similar). + + The `environment` config option takes precedence over the environment variable. + + :param dist: The distribution of the application. + + Distributions are used to disambiguate build or deployment variants of the same release of an application. + + The dist can be for example a build number. + + :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. + + The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of + error events will be sent. + + Events are picked randomly. + + :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. + + This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and + which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number + between `0.0` and `1.0`, inclusive. + + The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to + send the event with the given probability. + + If this configuration option is specified, the `sample_rate` option is ignored. + + :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. + + Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're + sent to Sentry. + + By default, all errors are sent. + + :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. + + This defaults to `100`, but you can set this to any number. + + However, you should be aware that Sentry has a `maximum payload size `_ + and any events exceeding that payload size will be dropped. + + :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. + + Stack traces are always attached to exceptions; however, when this option is set, stack traces are also + sent with messages. + + This option means that stack traces appear next to all log messages. + + Grouping in Sentry is different for events with stack traces and without. As a result, you will get new + groups as you enable or disable this flag for certain events. + + :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) + `_ is added by active integrations. + + If you enable this option, be sure to manually remove what you don't want to send using our features for + managing `Sensitive Data `_. + + :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and + passwords from a `denylist`. + + It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. + + See how to `configure the scrubber here `_. + + :param include_source_context: When enabled, source context will be included in events sent to Sentry. + + This source context includes the five lines of code above and below the line of code where an error + happened. + + :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with + the event to help with debugging. + + :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the + moment an error occurs. + + But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will + be included in the stack trace sent to Sentry. + + :param max_stack_frames: This option limits the number of stack frames that will be captured when + `add_full_stack` is enabled. + + :param server_name: This option can be used to supply a server name. + + When provided, the name of the server is sent along and persisted in the event. + + For many integrations, the server name actually corresponds to the device hostname, even in situations + where the machine is not actually a server. + + :param project_root: The full path to the root directory of your application. + + The `project_root` is used to mark frames in a stack trace either as being in your application or outside + of the application. + + :param in_app_include: A list of string prefixes of module names that belong to the app. + + This option takes precedence over `in_app_exclude`. + + Sentry differentiates stack frames that are directly related to your application ("in application") from + stack frames that come from other packages such as the standard library, frameworks, or other dependencies. + + The application package is automatically marked as `inApp`. + + The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are + displayed by default. + + :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to + third-party packages. + + Modules considered not part of the app will be hidden from stack traces by default. + + This option can be overridden using `in_app_include`. + + :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. + It can be set to one of the following values: + + - `never`: Request bodies are never sent. + - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically + 4KB). + - `medium`: Medium and small requests will be captured (typically 10KB). + - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. + + Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ + expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of + how you configure this option. + + :param max_value_length: The number of characters after which the values containing text in the event payload + will be truncated. + + WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be + dropped by Sentry. + + :param ca_certs: A path to an alternative CA bundle file in PEM-format. + + :param send_client_reports: Set this boolean to `False` to disable sending of client reports. + + Client reports allow the client to send status reports about itself to Sentry, such as information about + events that were dropped before being sent. + + :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) + `_. + + This setting can be used to override the default config options for a specific auto-enabling integration + or to add an integration that is not auto-enabled. + + :param disabled_integrations: List of integrations that will be disabled. + + This setting can be used to explicitly turn off specific `auto-enabling integrations (list) + `_ or + `default `_ integrations. + + :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) + `_ should be enabled. + + When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding + framework/library is detected. + + :param default_integrations: Configures whether `default integrations + `_ should be enabled. + + Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling + integrations**, unless they are specifically added in the `integrations` option, described above. + + :param before_send: This function is called with an SDK-specific message or error event object, and can return + a modified event object, or `null` to skip reporting the event. + + This can be used, for instance, for manual PII stripping before sending. + + By the time `before_send` is executed, all scope data has already been applied to the event. Further + modification of the scope won't have any effect. + + :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can + return a modified transaction event object, or `null` to skip reporting the event. + + One way this might be used is for manual PII stripping before sending. + + :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb + is added to the scope. + + When nothing is returned from the function, the breadcrumb is dropped. + + To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. + + The callback typically gets a second argument (called a "hint") which contains the original object from + which the breadcrumb was created to further customize what the breadcrumb should look like. + + :param transport: Switches out the transport used to send events. + + How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to + send it through some more complex setup that requires proxy authentication. + + :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to + flush. + + :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. + + This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs + support a separate HTTPS proxy. + + SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix + systems, the `http_proxy` environment variable will be picked up. + + :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. + + This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for + HTTPS requests at all times. + + :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded + to `urllib3`'s `ProxyManager `_. + + :param shutdown_timeout: Controls how many seconds to wait before shutting down. + + Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending + events. The default is SDK specific but typically around two seconds. + + Setting this value too low may cause problems for sending events from command line applications. + + Setting the value too high will cause the application to block for a long time for users experiencing + network connectivity problems. + + :param keep_alive: Determines whether to keep the connection alive between requests. + + This can be useful in environments where you encounter frequent network issues such as connection resets. + + :param cert_file: Path to the client certificate to use. + + If set, supersedes the `CLIENT_CERT_FILE` environment variable. + + :param key_file: Path to the key file to use. + + If set, supersedes the `CLIENT_KEY_FILE` environment variable. + + :param socket_options: An optional list of socket options to use. + + These provide fine-grained, low-level control over the way the SDK connects to Sentry. + + If provided, the options will override the default `urllib3` `socket options + `_. + + :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction + will be sent to Sentry. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + Either this or `traces_sampler` must be defined to enable tracing. + + If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have + another service (for example a JS frontend) that makes requests to your service that include trace + information, those traces will be continued and thus transactions will be sent to Sentry. + + If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces + will be started and no incoming traces will be continued. + + :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be + sent to Sentry. + + It will automatically be passed information about the transaction and the context in which it's being + created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being + sent). + + Can also be used for filtering transactions, by returning `0` for those that are unwanted. + + Either this or `traces_sample_rate` must be defined to enable tracing. + + :param trace_propagation_targets: An optional property that controls which downstream services receive tracing + data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. + + The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. + + If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to + that request. + + String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ + a string provided through the option. + + If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the + instrumented client. + + :param functions_to_trace: An optional list of functions that should be set up for tracing. + + For each function in the list, a span will be created when the function is executed. + + Functions in the list are represented as strings containing the fully qualified name of the function. + + This is a convenient option, making it possible to have one central place for configuring what functions + to trace, instead of having custom instrumentation scattered all over your code base. + + To learn more, see the `Custom Instrumentation `_ documentation. + + :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health + checks on the SDK. + + If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second + intervals until recovery. + + This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. + + This option is enabled by default. + + :param enable_db_query_source: When enabled, the source location will be added to database queries. + + :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database + queries. + + The query location will be added to the query for queries slower than the specified threshold. + + :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. + + :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an + outgoing HTTP request. + + The request location will be added to the request for requests slower than the specified threshold. + + :param custom_repr: A custom `repr `_ function to run + while serializing an object. + + Use this to control how your custom objects and classes are visible in Sentry. + + Return a string for that repr value to be used or `None` to continue serializing how Sentry would have + done it anyway. + + :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled + transaction will be profiled. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be + profiled. + + :param profiles_sampler: + + :param profiler_mode: + + :param profile_lifecycle: + + :param profile_session_sample_rate: + + :param enable_tracing: + + :param propagate_traces: + + :param auto_session_tracking: + + :param spotlight: + + :param instrumenter: + + :param enable_logs: Set `enable_logs` to True to enable the SDK to emit + Sentry logs. Defaults to False. + + :param before_send_log: An optional function to modify or filter out logs + before they're sent to Sentry. Any modifications to the log in this + function will be retained. If the function returns None, the log will + not be sent to Sentry. + + :param trace_ignore_status_codes: An optional property that disables tracing for + HTTP requests with certain status codes. + + Requests are not traced if the status code is contained in the provided set. + + If `trace_ignore_status_codes` is not provided, requests with any status code + may be traced. + + This option has no effect in span streaming mode (`trace_lifecycle="stream"`). + + :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the + `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. + + If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. + + The client's organization ID is extracted from the DSN or can be set with the `org_id` option. + If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. + This is useful to prevent traces of unknown third-party services from being continued in your application. + + :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases + but you can provide it explicitly for self-hosted and Relay setups. This value is used for + trace propagation and for features like `strict_trace_continuation`. + + :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to + reduce downstream data loss. + + :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. + + ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations + collect automatically, superseding `send_default_pii`. Passing a dict under + `_experiments={"data_collection": {...}}` opts into the feature; 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 + changes nothing. Restrict collection per category (user identity, cookies, HTTP + headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source + context). If `send_default_pii` is also set, `data_collection` takes precedence. + + Example:: + + sentry_sdk.init( + dsn="...", + _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, + ) + + See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. + """ + pass + + +def _get_default_options() -> "dict[str, Any]": + import inspect + + a = inspect.getfullargspec(ClientConstructor.__init__) + defaults = a.defaults or () + kwonlydefaults = a.kwonlydefaults or {} + + return dict( + itertools.chain( + zip(a.args[-len(defaults) :], defaults), + kwonlydefaults.items(), + ) + ) + + +DEFAULT_OPTIONS = _get_default_options() +del _get_default_options + + +VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py new file mode 100644 index 0000000000..b3287703b9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py @@ -0,0 +1,9 @@ +from sentry_sdk.crons.api import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.crons.decorator import monitor + +__all__ = [ + "capture_checkin", + "MonitorStatus", + "monitor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py new file mode 100644 index 0000000000..6ea3e36b6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py @@ -0,0 +1,60 @@ +import uuid +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, MonitorConfig + + +def _create_check_in_event( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration_s: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> "Event": + options = sentry_sdk.get_client().options + check_in_id = check_in_id or uuid.uuid4().hex + + check_in: "Event" = { + "type": "check_in", + "monitor_slug": monitor_slug, + "check_in_id": check_in_id, + "status": status, + "duration": duration_s, + "environment": options.get("environment", None), + "release": options.get("release", None), + } + + if monitor_config: + check_in["monitor_config"] = monitor_config + + return check_in + + +def capture_checkin( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> str: + check_in_event = _create_check_in_event( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=status, + duration_s=duration, + monitor_config=monitor_config, + ) + + sentry_sdk.capture_event(check_in_event) + + logger.debug( + f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" + ) + + return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py new file mode 100644 index 0000000000..be686b4539 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py @@ -0,0 +1,4 @@ +class MonitorStatus: + IN_PROGRESS = "in_progress" + OK = "ok" + ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py new file mode 100644 index 0000000000..b13d350e15 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py @@ -0,0 +1,137 @@ +from functools import wraps +from inspect import iscoroutinefunction +from typing import TYPE_CHECKING + +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from types import TracebackType + from typing import ( + Any, + Optional, + ParamSpec, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from sentry_sdk._types import MonitorConfig + + P = ParamSpec("P") + R = TypeVar("R") + + +class monitor: # noqa: N801 + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + """ + + def __init__( + self, + monitor_slug: "Optional[str]" = None, + monitor_config: "Optional[MonitorConfig]" = None, + ) -> None: + self.monitor_slug = monitor_slug + self.monitor_config = monitor_config + + def __enter__(self) -> None: + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, + status=MonitorStatus.IN_PROGRESS, + monitor_config=self.monitor_config, + ) + + def __exit__( + self, + exc_type: "Optional[Type[BaseException]]", + exc_value: "Optional[BaseException]", + traceback: "Optional[TracebackType]", + ) -> None: + duration_s = now() - self.start_timestamp + + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR + + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + monitor_config=self.monitor_config, + ) + + if TYPE_CHECKING: + + @overload + def __call__( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + # Unfortunately, mypy does not give us any reliable way to type check the + # return value of an Awaitable (i.e. async function) for this overload, + # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. + ... + + @overload + def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... + + def __call__( + self, + fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", + ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": + if iscoroutinefunction(fn): + return self._async_wrapper(fn) + + else: + if TYPE_CHECKING: + fn = cast("Callable[P, R]", fn) + return self._sync_wrapper(fn) + + def _async_wrapper( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + @wraps(fn) + async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return await fn(*args, **kwargs) + + return inner + + def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": + @wraps(fn) + def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return fn(*args, **kwargs) + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py new file mode 100644 index 0000000000..bcdf767409 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py @@ -0,0 +1,319 @@ +""" +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, List, Mapping, Optional, cast + +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE + +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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: + """ + Return whether ``key`` matches the sensitive denylist using a partial, + case-insensitive substring match. + + :param extra_terms: additional deny terms (e.g. user-provided) to consider + alongside the built-in `_SENSITIVE_DENYLIST`. + """ + lowered = key.lower() + for term in _SENSITIVE_DENYLIST: + if term in lowered: + return True + if extra_terms: + for term in extra_terms: + if term and term.lower() in lowered: + return True + return False + + +def _apply_key_value_collection_filtering( + items: "Mapping[str, Any]", + behaviour: "KeyValueCollectionBehaviour", + substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, +) -> "Dict[str, Any]": + + if behaviour["mode"] == "off": + return {} + + result: "Dict[str, Any]" = {} + + if behaviour["mode"] == "allowlist": + for key, value in items.items(): + is_allowed = False + if isinstance(key, str): + lowered = key.lower() + is_allowed = any( + term and term.lower() in lowered + for term in behaviour.get("terms", []) + ) + if is_allowed and not _is_sensitive_key(key): + result[key] = value + else: + result[key] = substitute + return result + + # denylist behaviour + for key, value in items.items(): + if isinstance(key, str) and _is_sensitive_key( + key=key, extra_terms=behaviour.get("terms", []) + ): + result[key] = substitute + else: + result[key] = value + return result + + +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, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py new file mode 100644 index 0000000000..795882e9ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py @@ -0,0 +1,37 @@ +import logging +import sys +import warnings +from logging import LogRecord + +from sentry_sdk import get_client +from sentry_sdk.client import _client_init_debug +from sentry_sdk.utils import logger + + +class _DebugFilter(logging.Filter): + def filter(self, record: "LogRecord") -> bool: + if _client_init_debug.get(False): + return True + + return get_client().options["debug"] + + +def init_debug_support() -> None: + if not logger.handlers: + configure_logger() + + +def configure_logger() -> None: + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.DEBUG) + logger.addFilter(_DebugFilter()) + + +def configure_debug_hub() -> None: + warnings.warn( + "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py new file mode 100644 index 0000000000..d2d4aae31a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py @@ -0,0 +1,332 @@ +import io +import json +import mimetypes +from typing import TYPE_CHECKING + +from sentry_sdk.session import Session +from sentry_sdk.utils import capture_internal_exceptions, json_dumps + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, List, Optional, Union + + from sentry_sdk._types import Event, EventDataCategory + + +def parse_json(data: "Union[bytes, str]") -> "Any": + # on some python 3 versions this needs to be bytes + if isinstance(data, bytes): + data = data.decode("utf-8", "replace") + return json.loads(data) + + +class Envelope: + """ + Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints + documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, + each envelope may have at most one Item with type "event" or "transaction" (but not both). + """ + + def __init__( + self, + headers: "Optional[Dict[str, Any]]" = None, + items: "Optional[List[Item]]" = None, + ) -> None: + if headers is not None: + headers = dict(headers) + self.headers = headers or {} + if items is None: + items = [] + else: + items = list(items) + self.items = items + + @property + def description(self) -> str: + return "envelope with %s items (%s)" % ( + len(self.items), + ", ".join(x.data_category for x in self.items), + ) + + def add_event( + self, + event: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=event), type="event")) + + def add_transaction( + self, + transaction: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) + + def add_profile( + self, + profile: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) + + def add_profile_chunk( + self, + profile_chunk: "Any", + ) -> None: + self.add_item( + Item( + payload=PayloadRef(json=profile_chunk), + type="profile_chunk", + headers={"platform": profile_chunk.get("platform", "python")}, + ) + ) + + def add_checkin( + self, + checkin: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) + + def add_session( + self, + session: "Union[Session, Any]", + ) -> None: + if isinstance(session, Session): + session = session.to_json() + self.add_item(Item(payload=PayloadRef(json=session), type="session")) + + def add_sessions( + self, + sessions: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) + + def add_item( + self, + item: "Item", + ) -> None: + self.items.append(item) + + def get_event(self) -> "Optional[Event]": + for items in self.items: + event = items.get_event() + if event is not None: + return event + return None + + def get_transaction_event(self) -> "Optional[Event]": + for item in self.items: + event = item.get_transaction_event() + if event is not None: + return event + return None + + def __iter__(self) -> "Iterator[Item]": + return iter(self.items) + + def serialize_into( + self, + f: "Any", + ) -> None: + f.write(json_dumps(self.headers)) + f.write(b"\n") + for item in self.items: + item.serialize_into(f) + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Envelope": + headers = parse_json(f.readline()) + items = [] + while 1: + item = Item.deserialize_from(f) + if item is None: + break + items.append(item) + return cls(headers=headers, items=items) + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Envelope": + return cls.deserialize_from(io.BytesIO(bytes)) + + def __repr__(self) -> str: + return "" % (self.headers, self.items) + + +class PayloadRef: + def __init__( + self, + bytes: "Optional[bytes]" = None, + path: "Optional[Union[bytes, str]]" = None, + json: "Optional[Any]" = None, + ) -> None: + self.json = json + self.bytes = bytes + self.path = path + + def get_bytes(self) -> bytes: + if self.bytes is None: + if self.path is not None: + with capture_internal_exceptions(): + with open(self.path, "rb") as f: + self.bytes = f.read() + elif self.json is not None: + self.bytes = json_dumps(self.json) + return self.bytes or b"" + + @property + def inferred_content_type(self) -> str: + if self.json is not None: + return "application/json" + elif self.path is not None: + path = self.path + if isinstance(path, bytes): + path = path.decode("utf-8", "replace") + ty = mimetypes.guess_type(path)[0] + if ty: + return ty + return "application/octet-stream" + + def __repr__(self) -> str: + return "" % (self.inferred_content_type,) + + +class Item: + def __init__( + self, + payload: "Union[bytes, str, PayloadRef]", + headers: "Optional[Dict[str, Any]]" = None, + type: "Optional[str]" = None, + content_type: "Optional[str]" = None, + filename: "Optional[str]" = None, + ): + if headers is not None: + headers = dict(headers) + elif headers is None: + headers = {} + self.headers = headers + if isinstance(payload, bytes): + payload = PayloadRef(bytes=payload) + elif isinstance(payload, str): + payload = PayloadRef(bytes=payload.encode("utf-8")) + else: + payload = payload + + if filename is not None: + headers["filename"] = filename + if type is not None: + headers["type"] = type + if content_type is not None: + headers["content_type"] = content_type + elif "content_type" not in headers: + headers["content_type"] = payload.inferred_content_type + + self.payload = payload + + def __repr__(self) -> str: + return "" % ( + self.headers, + self.payload, + self.data_category, + ) + + @property + def type(self) -> "Optional[str]": + return self.headers.get("type") + + @property + def data_category(self) -> "EventDataCategory": + ty = self.headers.get("type") + if ty == "session" or ty == "sessions": + return "session" + elif ty == "attachment": + return "attachment" + elif ty == "transaction": + return "transaction" + elif ty == "span": + return "span" + elif ty == "event": + return "error" + elif ty == "log": + return "log_item" + elif ty == "trace_metric": + return "trace_metric" + elif ty == "client_report": + return "internal" + elif ty == "profile": + return "profile" + elif ty == "profile_chunk": + return "profile_chunk" + elif ty == "check_in": + return "monitor" + else: + return "default" + + def get_bytes(self) -> bytes: + return self.payload.get_bytes() + + def get_event(self) -> "Optional[Event]": + """ + Returns an error event if there is one. + """ + if self.type == "event" and self.payload.json is not None: + return self.payload.json + return None + + def get_transaction_event(self) -> "Optional[Event]": + if self.type == "transaction" and self.payload.json is not None: + return self.payload.json + return None + + def serialize_into( + self, + f: "Any", + ) -> None: + headers = dict(self.headers) + bytes = self.get_bytes() + headers["length"] = len(bytes) + f.write(json_dumps(headers)) + f.write(b"\n") + f.write(bytes) + f.write(b"\n") + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Optional[Item]": + line = f.readline().rstrip() + if not line: + return None + headers = parse_json(line) + length = headers.get("length") + if length is not None: + payload = f.read(length) + f.readline() + else: + # if no length was specified we need to read up to the end of line + # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) + payload = f.readline().rstrip(b"\n") + if headers.get("type") in ("event", "transaction"): + rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) + else: + rv = cls(headers=headers, payload=payload) + return rv + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Optional[Item]": + return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py new file mode 100644 index 0000000000..5eaa5e440b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py @@ -0,0 +1,75 @@ +import copy +from threading import Lock +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import TypedDict + + FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) + + +DEFAULT_FLAG_CAPACITY = 100 + + +class FlagBuffer: + def __init__(self, capacity: int) -> None: + self.capacity = capacity + self.lock = Lock() + + # Buffer is private. The name is mangled to discourage use. If you use this attribute + # directly you're on your own! + self.__buffer = LRUCache(capacity) + + def clear(self) -> None: + self.__buffer = LRUCache(self.capacity) + + def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": + with self.lock: + buffer = FlagBuffer(self.capacity) + buffer.__buffer = copy.deepcopy(self.__buffer, memo) + return buffer + + def get(self) -> "list[FlagData]": + with self.lock: + return [ + {"flag": key, "result": value} for key, value in self.__buffer.get_all() + ] + + def set(self, flag: str, result: bool) -> None: + if isinstance(result, FlagBuffer): + # If someone were to insert `self` into `self` this would create a circular dependency + # on the lock. This is of course a deadlock. However, this is far outside the expected + # usage of this class. We guard against it here for completeness and to document this + # expected failure mode. + raise ValueError( + "FlagBuffer instances can not be inserted into the dictionary." + ) + + with self.lock: + self.__buffer.set(flag, result) + + +def add_feature_flag(flag: str, result: bool) -> None: + """ + Records a flag and its value to be sent on subsequent error events. + We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. + """ + client = sentry_sdk.get_client() + + flags = sentry_sdk.get_isolation_scope().flags + flags.set(flag, result) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.get_current_span() + if span and isinstance(span, sentry_sdk.traces.StreamedSpan): + span.set_attribute(f"flag.evaluation.{flag}", result) + + else: + span = sentry_sdk.get_current_span() + if span and isinstance(span, Span): + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py new file mode 100644 index 0000000000..b17444d06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py @@ -0,0 +1,741 @@ +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import ( + get_client, + get_current_scope, + get_global_scope, + get_isolation_scope, +) +from sentry_sdk._compat import with_metaclass +from sentry_sdk.client import Client +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.scope import _ScopeManager +from sentry_sdk.tracing import ( + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.utils import ( + ContextVar, + logger, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.tracing import TransactionKwargs + + T = TypeVar("T") + +else: + + def overload(x: "T") -> "T": + return x + + +class SentryHubDeprecationWarning(DeprecationWarning): + """ + A custom deprecation warning to inform users that the Hub is deprecated. + """ + + _MESSAGE = ( + "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " + "Please consult our 1.x to 2.x migration guide for details on how to migrate " + "`Hub` usage to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" + ) + + def __init__(self, *_: object) -> None: + super().__init__(self._MESSAGE) + + +@contextmanager +def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": + """Utility function to suppress deprecation warnings for the Hub.""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) + yield + + +_local = ContextVar("sentry_current_hub") + + +class HubMeta(type): + @property + def current(cls) -> "Hub": + """Returns the current instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + rv = _local.get(None) + if rv is None: + with _suppress_hub_deprecation_warning(): + # This will raise a deprecation warning; suppress it since we already warned above. + rv = Hub(GLOBAL_HUB) + _local.set(rv) + return rv + + @property + def main(cls) -> "Hub": + """Returns the main instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + return GLOBAL_HUB + + +class Hub(with_metaclass(HubMeta)): # type: ignore + """ + .. deprecated:: 2.0.0 + The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. + + The hub wraps the concurrency management of the SDK. Each thread has + its own hub but the hub might transfer with the flow of execution if + context vars are available. + + If the hub is used with a with statement it's temporarily activated. + """ + + _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] + _scope: "Optional[Scope]" = None + + # Mypy doesn't pick up on the metaclass. + + if TYPE_CHECKING: + current: "Hub" = None # type: ignore[assignment] + main: "Optional[Hub]" = None + + def __init__( + self, + client_or_hub: "Optional[Union[Hub, Client]]" = None, + scope: "Optional[Any]" = None, + ) -> None: + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + + current_scope = None + + if isinstance(client_or_hub, Hub): + client = get_client() + if scope is None: + # hub cloning is going on, we use a fork of the current/isolation scope for context manager + scope = get_isolation_scope().fork() + current_scope = get_current_scope().fork() + else: + client = client_or_hub + get_global_scope().set_client(client) + + if scope is None: # so there is no Hub cloning going on + # just the current isolation scope is used for context manager + scope = get_isolation_scope() + current_scope = get_current_scope() + + if current_scope is None: + # just the current current scope is used for context manager + current_scope = get_current_scope() + + self._stack = [(client, scope)] # type: ignore + self._last_event_id: "Optional[str]" = None + self._old_hubs: "List[Hub]" = [] + + self._old_current_scopes: "List[Scope]" = [] + self._old_isolation_scopes: "List[Scope]" = [] + self._current_scope: "Scope" = current_scope + self._scope: "Scope" = scope + + def __enter__(self) -> "Hub": + self._old_hubs.append(Hub.current) + _local.set(self) + + current_scope = get_current_scope() + self._old_current_scopes.append(current_scope) + scope._current_scope.set(self._current_scope) + + isolation_scope = get_isolation_scope() + self._old_isolation_scopes.append(isolation_scope) + scope._isolation_scope.set(self._scope) + + return self + + def __exit__( + self, + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[Any]", + ) -> None: + old = self._old_hubs.pop() + _local.set(old) + + old_current_scope = self._old_current_scopes.pop() + scope._current_scope.set(old_current_scope) + + old_isolation_scope = self._old_isolation_scopes.pop() + scope._isolation_scope.set(old_isolation_scope) + + def run( + self, + callback: "Callable[[], T]", + ) -> "T": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Runs a callback in the context of the hub. Alternatively the + with statement can be used on the hub directly. + """ + with self: + return callback() + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Any": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. + + Returns the integration for this hub by name or class. If there + is no client bound or the client does not have that integration + then `None` is returned. + + If the return value is not `None` the hub is guaranteed to have a + client attached. + """ + return get_client().get_integration(name_or_class) + + @property + def client(self) -> "Optional[BaseClient]": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Please use :py:func:`sentry_sdk.api.get_client` instead. + + Returns the current client on the hub. + """ + client = get_client() + + if not client.is_active(): + return None + + return client + + @property + def scope(self) -> "Scope": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Returns the current scope on the hub. + """ + return get_isolation_scope() + + def last_event_id(self) -> "Optional[str]": + """ + Returns the last event ID. + + .. deprecated:: 1.40.5 + This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. + """ + logger.warning( + "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." + ) + return self._last_event_id + + def bind_client( + self, + new: "Optional[BaseClient]", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.set_client` instead. + + Binds a new client to the hub. + """ + get_global_scope().set_client(new) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. + + Captures an event. + + Alias of :py:meth:`sentry_sdk.Scope.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + """ + last_event_id = get_current_scope().capture_event( + event, hint, scope=scope, **scope_kwargs + ) + + is_transaction = event.get("type") == "transaction" + if last_event_id is not None and not is_transaction: + self._last_event_id = last_event_id + + return last_event_id + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. + + Captures a message. + + Alias of :py:meth:`sentry_sdk.Scope.capture_message`. + + :param message: The string to send as the message to Sentry. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_message( + message, level=level, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. + + Captures an exception. + + Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_exception( + error, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. + + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_span` instead. + + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + """ + scope = get_current_scope() + return scope.start_span(instrumenter=instrumenter, **kwargs) + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. + + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. + """ + scope = get_current_scope() + + # For backwards compatibility, we allow passing the scope as the hub. + # We need a major release to make this nice. (if someone searches the code: deprecated) + # Type checking disabled for this line because deprecated keys are not allowed in the type signature. + kwargs["hub"] = scope # type: ignore + + return scope.start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + ) -> "Transaction": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. + + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers=environ_or_headers, op=op, name=name, source=source + ) + + @overload + def push_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def push_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def push_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + if callback is not None: + with self.push_scope() as scope: + callback(scope) + return None + + return _ScopeManager(self) + + def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pops a scope layer from the stack. + + Try to use the context manager :py:meth:`push_scope` instead. + """ + rv = self._stack.pop() + assert self._stack, "stack must have at least one layer" + return rv + + @overload + def configure_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def configure_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def configure_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + scope = get_isolation_scope() + + if continue_trace: + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + def start_session( + self, + session_mode: str = "application", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_session` instead. + + Starts a new session. + """ + get_isolation_scope().start_session( + session_mode=session_mode, + ) + + def end_session(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.end_session` instead. + + Ends the current session if there is one. + """ + get_isolation_scope().end_session() + + def stop_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. + + Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + get_isolation_scope().stop_auto_session_tracking() + + def resume_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. + + Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + get_isolation_scope().resume_auto_session_tracking() + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.flush` instead. + + Alias for :py:meth:`sentry_sdk.client._Client.flush` + """ + return get_client().flush(timeout=timeout, callback=callback) + + def get_traceparent(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. + + Returns the traceparent either from the active span or from the scope. + """ + current_scope = get_current_scope() + traceparent = current_scope.get_traceparent() + + if traceparent is None: + isolation_scope = get_isolation_scope() + traceparent = isolation_scope.get_traceparent() + + return traceparent + + def get_baggage(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. + + Returns Baggage either from the active span or from the scope. + """ + current_scope = get_current_scope() + baggage = current_scope.get_baggage() + + if baggage is None: + isolation_scope = get_isolation_scope() + baggage = isolation_scope.get_baggage() + + if baggage is not None: + return baggage.serialize() + + return None + + def iter_trace_propagation_headers( + self, span: "Optional[Span]" = None + ) -> "Generator[Tuple[str, str], None, None]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. + + Return HTTP headers which allow propagation of trace data. Data taken + from the span representing the request, if available, or the current + span on the scope if not. + """ + return get_current_scope().iter_trace_propagation_headers( + span=span, + ) + + def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. + + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + return get_current_scope().trace_propagation_meta( + span=span, + ) + + +with _suppress_hub_deprecation_warning(): + # Suppress deprecation warning for the Hub here, since we still always + # import this module. + GLOBAL_HUB = Hub() +_local.set(GLOBAL_HUB) + + +# Circular imports +from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py new file mode 100644 index 0000000000..677d34a81e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +from threading import Lock +from typing import TYPE_CHECKING + +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union + + +_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) + + +_installer_lock = Lock() + +# Set of all integration identifiers we have attempted to install +_processed_integrations: "Set[str]" = set() + +# Set of all integration identifiers we have actually installed +_installed_integrations: "Set[str]" = set() + + +def _generate_default_integrations_iterator( + integrations: "List[str]", + auto_enabling_integrations: "List[str]", +) -> "Callable[[bool], Iterator[Type[Integration]]]": + def iter_default_integrations( + with_auto_enabling_integrations: bool, + ) -> "Iterator[Type[Integration]]": + """Returns an iterator of the default integration classes:""" + from importlib import import_module + + if with_auto_enabling_integrations: + all_import_strings = integrations + auto_enabling_integrations + else: + all_import_strings = integrations + + for import_string in all_import_strings: + try: + module, cls = import_string.rsplit(".", 1) + yield getattr(import_module(module), cls) + except (DidNotEnable, SyntaxError) as e: + logger.debug( + "Did not import default integration %s: %s", import_string, e + ) + + if isinstance(iter_default_integrations.__doc__, str): + for import_string in integrations: + iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) + + return iter_default_integrations + + +_DEFAULT_INTEGRATIONS = [ + # stdlib/base runtime integrations + "sentry_sdk.integrations.argv.ArgvIntegration", + "sentry_sdk.integrations.atexit.AtexitIntegration", + "sentry_sdk.integrations.dedupe.DedupeIntegration", + "sentry_sdk.integrations.excepthook.ExcepthookIntegration", + "sentry_sdk.integrations.logging.LoggingIntegration", + "sentry_sdk.integrations.modules.ModulesIntegration", + "sentry_sdk.integrations.stdlib.StdlibIntegration", + "sentry_sdk.integrations.threading.ThreadingIntegration", +] + +_AUTO_ENABLING_INTEGRATIONS = [ + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.anthropic.AnthropicIntegration", + "sentry_sdk.integrations.ariadne.AriadneIntegration", + "sentry_sdk.integrations.arq.ArqIntegration", + "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", + "sentry_sdk.integrations.boto3.Boto3Integration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.chalice.ChaliceIntegration", + "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", + "sentry_sdk.integrations.cohere.CohereIntegration", + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.fastapi.FastApiIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.gql.GQLIntegration", + "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", + "sentry_sdk.integrations.graphene.GrapheneIntegration", + "sentry_sdk.integrations.httpx.HttpxIntegration", + "sentry_sdk.integrations.httpx2.Httpx2Integration", + "sentry_sdk.integrations.huey.HueyIntegration", + "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", + "sentry_sdk.integrations.langchain.LangchainIntegration", + "sentry_sdk.integrations.langgraph.LanggraphIntegration", + "sentry_sdk.integrations.litestar.LitestarIntegration", + "sentry_sdk.integrations.loguru.LoguruIntegration", + "sentry_sdk.integrations.mcp.MCPIntegration", + "sentry_sdk.integrations.openai.OpenAIIntegration", + "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", + "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", + "sentry_sdk.integrations.pymongo.PyMongoIntegration", + "sentry_sdk.integrations.pyramid.PyramidIntegration", + "sentry_sdk.integrations.quart.QuartIntegration", + "sentry_sdk.integrations.redis.RedisIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", + "sentry_sdk.integrations.starlette.StarletteIntegration", + "sentry_sdk.integrations.starlite.StarliteIntegration", + "sentry_sdk.integrations.strawberry.StrawberryIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", +] + +iter_default_integrations = _generate_default_integrations_iterator( + integrations=_DEFAULT_INTEGRATIONS, + auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, +) + +del _generate_default_integrations_iterator + + +_MIN_VERSIONS = { + "aiohttp": (3, 4), + "aiomysql": (0, 3, 0), + "anthropic": (0, 16), + "ariadne": (0, 20), + "arq": (0, 23), + "asyncpg": (0, 23), + "beam": (2, 12), + "boto3": (1, 16), # botocore + "bottle": (0, 12), + "celery": (4, 4, 7), + "chalice": (1, 16, 0), + "clickhouse_driver": (0, 2, 0), + "cohere": (5, 4, 0), + "django": (1, 8), + "dramatiq": (1, 9), + "falcon": (1, 4), + "fastapi": (0, 79, 0), + "flask": (1, 1, 4), + "gql": (3, 4, 1), + "graphene": (3, 3), + "google_genai": (1, 29, 0), # google-genai + "grpc": (1, 32, 0), # grpcio + "httpx": (0, 16, 0), + "httpx2": (2, 0, 0), + "huggingface_hub": (0, 24, 7), + "langchain": (0, 1, 0), + "langgraph": (0, 6, 6), + "launchdarkly": (9, 8, 0), + "litellm": (1, 77, 5), + "loguru": (0, 7, 0), + "mcp": (1, 15, 0), + "openai": (1, 0, 0), + "openai_agents": (0, 0, 19), + "openfeature": (0, 7, 1), + "pydantic_ai": (1, 0, 0), + "pymongo": (3, 5, 0), + "pyreqwest": (0, 11, 6), + "quart": (0, 16, 0), + "ray": (2, 7, 0), + "requests": (2, 0, 0), + "rq": (0, 6), + "sanic": (0, 8), + "sqlalchemy": (1, 2), + "starlette": (0, 16), + "starlite": (1, 48), + "statsig": (0, 55, 3), + "strawberry": (0, 209, 5), + "tornado": (6, 0), + "typer": (0, 15), + "unleash": (6, 0, 1), +} + + +_INTEGRATION_DEACTIVATES = { + "langchain": {"openai", "anthropic", "google_genai"}, + "openai_agents": {"openai"}, + "pydantic_ai": {"openai", "anthropic"}, +} + + +def setup_integrations( + integrations: "Sequence[Integration]", + with_defaults: bool = True, + with_auto_enabling_integrations: bool = False, + disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, + options: "Optional[Dict[str, Any]]" = None, +) -> "Dict[str, Integration]": + """ + Given a list of integration instances, this installs them all. + + When `with_defaults` is set to `True` all default integrations are added + unless they were already provided before. + + `disabled_integrations` takes precedence over `with_defaults` and + `with_auto_enabling_integrations`. + + Some integrations are designed to automatically deactivate other integrations + in order to avoid conflicts and prevent duplicate telemetry from being collected. + For example, enabling the `langchain` integration will auto-deactivate both the + `openai` and `anthropic` integrations. + + Users can override this behavior by: + - Explicitly providing an integration in the `integrations=[]` list, or + - Disabling the higher-level integration via the `disabled_integrations` option. + """ + integrations = dict( + (integration.identifier, integration) for integration in integrations or () + ) + + logger.debug("Setting up integrations (with default = %s)", with_defaults) + + user_provided_integrations = set(integrations.keys()) + + # Integrations that will not be enabled + disabled_integrations = [ + integration if isinstance(integration, type) else type(integration) + for integration in disabled_integrations or [] + ] + + # Integrations that are not explicitly set up by the user. + used_as_default_integration = set() + + if with_defaults: + for integration_cls in iter_default_integrations( + with_auto_enabling_integrations + ): + if integration_cls.identifier not in integrations: + instance = integration_cls() + integrations[instance.identifier] = instance + used_as_default_integration.add(instance.identifier) + + disabled_integration_identifiers = { + integration.identifier for integration in disabled_integrations + } + + for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): + if ( + integration in integrations + and integration not in disabled_integration_identifiers + ): + for target in targets_to_deactivate: + if target not in user_provided_integrations: + for cls in iter_default_integrations(True): + if cls.identifier == target: + if cls not in disabled_integrations: + disabled_integrations.append(cls) + logger.debug( + "Auto-deactivating %s integration because %s integration is active", + target, + integration, + ) + + for identifier, integration in integrations.items(): + with _installer_lock: + if identifier not in _processed_integrations: + if type(integration) in disabled_integrations: + logger.debug("Ignoring integration %s", identifier) + else: + logger.debug( + "Setting up previously not enabled integration %s", identifier + ) + try: + type(integration).setup_once() + integration.setup_once_with_options(options) + except DidNotEnable as e: + if identifier not in used_as_default_integration: + raise + + logger.debug( + "Did not enable default integration %s: %s", identifier, e + ) + else: + _installed_integrations.add(identifier) + + _processed_integrations.add(identifier) + + integrations = { + identifier: integration + for identifier, integration in integrations.items() + if identifier in _installed_integrations + } + + for identifier in integrations: + logger.debug("Enabling integration %s", identifier) + + return integrations + + +def _check_minimum_version( + integration: "type[Integration]", + version: "Optional[tuple[int, ...]]", + package: "Optional[str]" = None, +) -> None: + package = package or integration.identifier + + if version is None: + raise DidNotEnable(f"Unparsable {package} version.") + + min_version = _MIN_VERSIONS.get(integration.identifier) + if min_version is None: + return + + if version < min_version: + raise DidNotEnable( + f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." + ) + + +class DidNotEnable(Exception): # noqa: N818 + """ + The integration could not be enabled due to a trivial user error like + `flask` not being installed for the `FlaskIntegration`. + + This exception is silently swallowed for default integrations, but reraised + for explicitly enabled integrations. + """ + + +class Integration(ABC): + """Baseclass for all integrations. + + To accept options for an integration, implement your own constructor that + saves those options on `self`. + """ + + install = None + """Legacy method, do not implement.""" + + identifier: "str" = None # type: ignore[assignment] + """String unique ID of integration type""" + + @staticmethod + @abstractmethod + def setup_once() -> None: + """ + Initialize the integration. + + This function is only called once, ever. Configuration is not available + at this point, so the only thing to do here is to hook into exception + handlers, and perhaps do monkeypatches. + + Inside those hooks `Integration.current` can be used to access the + instance again. + """ + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + """ + Called after setup_once in rare cases on the instance and with options since we don't have those available above. + """ + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py new file mode 100644 index 0000000000..7ff4657013 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py @@ -0,0 +1,187 @@ +import urllib +from enum import Enum +from typing import TYPE_CHECKING + +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.scope import should_send_default_pii + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from typing_extensions import Literal + + from sentry_sdk.utils import AnnotatedValue + + +class _RootPathInPath(Enum): + EXCLUDED = "excluded" + EITHER = "either" + + +def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": + """ + Extract headers from the ASGI scope, in the format that the Sentry protocol expects. + """ + headers: "Dict[str, str]" = {} + for raw_key, raw_value in asgi_scope["headers"]: + key = raw_key.decode("latin-1") + value = raw_value.decode("latin-1") + if key in headers: + headers[key] = headers[key] + ", " + value + else: + headers[key] = value + + return headers + + +def _get_path( + asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" +) -> "str": + if root_path_in_path is _RootPathInPath.EXCLUDED: + return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") + + # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 + path = asgi_scope["path"] + root_path = asgi_scope.get("root_path", "") + + if not root_path or path == root_path or path.startswith(root_path + "/"): + return path + + return root_path + path + + +def _get_url( + asgi_scope: "Dict[str, Any]", + default_scheme: "Literal['ws', 'http']", + host: "Optional[Union[AnnotatedValue, str]]", + path: str, +) -> str: + """ + Extract URL from the ASGI scope, without also including the querystring. + """ + scheme = asgi_scope.get("scheme", default_scheme) + + server = asgi_scope.get("server", None) + + if host: + return "%s://%s%s" % (scheme, host, path) + + if server is not None: + host, port = server + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) + if port != default_port: + return "%s://%s:%s%s" % (scheme, host, port, path) + return "%s://%s%s" % (scheme, host, path) + return path + + +def _get_query(asgi_scope: "Any") -> "Any": + """ + Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. + """ + qs = asgi_scope.get("query_string") + if not qs: + return None + return urllib.parse.unquote(qs.decode("latin-1")) + + +def _get_ip(asgi_scope: "Any") -> str: + """ + Extract IP Address from the ASGI scope based on request headers with fallback to scope client. + """ + headers = _get_headers(asgi_scope) + try: + return headers["x-forwarded-for"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return headers["x-real-ip"] + except KeyError: + pass + + return asgi_scope.get("client")[0] + + +def _get_request_data( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "Dict[str, Any]": + """ + Returns data related to the HTTP request from the ASGI scope. + """ + request_data: "Dict[str, Any]" = {} + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + request_data["method"] = asgi_scope.get("method") + + headers = _get_headers(asgi_scope) + + request_data["headers"] = _filter_headers( + headers, + use_annotated_value=False, + ) + + request_data["query_string"] = _get_query(asgi_scope) + + request_data["url"] = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} + + return request_data + + +def _get_request_attributes( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "dict[str, Any]": + """ + Return attributes related to the HTTP request from the ASGI scope. + """ + attributes: "dict[str, Any]" = {} + + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + if asgi_scope.get("method"): + attributes["http.request.method"] = asgi_scope["method"].upper() + + headers = _get_headers(asgi_scope) + + filtered_headers = _filter_headers(headers, use_annotated_value=False) + for header, value in filtered_headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + if should_send_default_pii(): + query = _get_query(asgi_scope) + if query: + attributes["http.query"] = query + + path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) + attributes["url.path"] = path + + url_without_query_string = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=path, + ) + query_string = _get_query(asgi_scope) + attributes["url.full"] = ( + f"{url_without_query_string}?{query_string}" + if query_string is not None + else url_without_query_string + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + ip = _get_ip(asgi_scope) + attributes["client.address"] = ip + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py new file mode 100644 index 0000000000..ad0ab7c734 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py @@ -0,0 +1,282 @@ +import json +from contextlib import contextmanager +from copy import deepcopy + +import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.data_collection import _apply_key_value_collection_filtering +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger + +try: + from django.http.request import RawPostDataException + + _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) +except ImportError: + RawPostDataException = None + _RAW_DATA_EXCEPTIONS = (ValueError,) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union + + from sentry_sdk._types import Event, HttpStatusCodeRange + + +SENSITIVE_ENV_KEYS = ( + "REMOTE_ADDR", + "HTTP_X_FORWARDED_FOR", + "HTTP_SET_COOKIE", + "HTTP_COOKIE", + "HTTP_AUTHORIZATION", + "HTTP_PROXY_AUTHORIZATION", + "HTTP_X_API_KEY", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", +) + +SENSITIVE_HEADERS = tuple( + x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") +) + +DEFAULT_HTTP_METHODS_TO_CAPTURE = ( + "CONNECT", + "DELETE", + "GET", + # "HEAD", # do not capture HEAD requests by default + # "OPTIONS", # do not capture OPTIONS requests by default + "PATCH", + "POST", + "PUT", + "TRACE", +) + + +# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support +@contextmanager +def nullcontext() -> "Iterator[None]": + yield + + +def request_body_within_bounds( + client: "Optional[sentry_sdk.client.BaseClient]", content_length: int +) -> bool: + if client is None: + return False + + bodies = client.options["max_request_body_size"] + return not ( + bodies == "never" + or (bodies == "small" and content_length > 10**3) + or (bodies == "medium" and content_length > 10**4) + ) + + +class RequestExtractor: + """ + Base class for request extraction. + """ + + # It does not make sense to make this class an ABC because it is not used + # for typing, only so that child classes can inherit common methods from + # it. Only some child classes implement all methods that raise + # NotImplementedError in this class. + + def __init__(self, request: "Any") -> None: + self.request = request + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + + content_length = self.content_length() + request_info = event.get("request", {}) + + if should_send_default_pii(): + request_info["cookies"] = dict(self.cookies()) + + if not request_body_within_bounds(client, content_length): + data = AnnotatedValue.removed_because_over_size_limit() + else: + # First read the raw body data + # It is important to read this first because if it is Django + # it will cache the body and then we can read the cached version + # again in parsed_body() (or json() or wherever). + raw_data = None + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # If DjangoRestFramework is used it already read the body for us + # so reading it here will fail. We can ignore this. + pass + + parsed_body = self.parsed_body() + if parsed_body is not None: + data = parsed_body + elif raw_data: + data = AnnotatedValue.removed_because_raw_data() + else: + data = None + + if data is not None: + request_info["data"] = data + + event["request"] = deepcopy(request_info) + + def content_length(self) -> int: + try: + return int(self.env().get("CONTENT_LENGTH", 0)) + except ValueError: + return 0 + + def cookies(self) -> "MutableMapping[str, Any]": + raise NotImplementedError() + + def raw_data(self) -> "Optional[Union[str, bytes]]": + raise NotImplementedError() + + def form(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + form = self.form() + except Exception: + form = None + try: + files = self.files() + except Exception: + files = None + + if form or files: + data = {} + if form: + data = dict(form.items()) + if files: + for key in files.keys(): + data[key] = AnnotatedValue.removed_because_raw_data() + + return data + + return self.json() + + def is_json(self) -> bool: + return _is_json_content_type(self.env().get("CONTENT_TYPE")) + + def json(self) -> "Optional[Any]": + try: + if not self.is_json(): + return None + + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # The body might have already been read, in which case this will + # fail + raw_data = None + + if raw_data is None: + return None + + if isinstance(raw_data, str): + return json.loads(raw_data) + else: + return json.loads(raw_data.decode("utf-8")) + except ValueError: + pass + + return None + + def files(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def size_of_file(self, file: "Any") -> int: + raise NotImplementedError() + + def env(self) -> "Dict[str, Any]": + raise NotImplementedError() + + +def _is_json_content_type(ct: "Optional[str]") -> bool: + mt = (ct or "").split(";", 1)[0] + return ( + mt == "application/json" + or (mt.startswith("application/")) + and mt.endswith("+json") + ) + + +def _filter_headers( + headers: "Mapping[str, str]", + use_annotated_value: bool = True, +) -> "Mapping[str, Union[AnnotatedValue, str]]": + client_options = sentry_sdk.get_client().options + + if has_data_collection_enabled(client_options): + data_collection_configuration = client_options["data_collection"] + + filtered = _apply_key_value_collection_filtering( + items=headers, + behaviour=data_collection_configuration["http_headers"]["request"], + ) + + for key in filtered: + if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): + filtered[key] = SENSITIVE_DATA_SUBSTITUTE + + return filtered + else: + if should_send_default_pii(): + return headers + + substitute: "Union[AnnotatedValue, str]" = ( + SENSITIVE_DATA_SUBSTITUTE + if not use_annotated_value + else AnnotatedValue.removed_because_over_size_limit() + ) + + return { + k: ( + v + if k.upper().replace("-", "_") not in SENSITIVE_HEADERS + else substitute + ) + for k, v in headers.items() + } + + +def _in_http_status_code_range( + code: object, code_ranges: "list[HttpStatusCodeRange]" +) -> bool: + for target in code_ranges: + if isinstance(target, int): + if code == target: + return True + continue + + try: + if code in target: + return True + except TypeError: + logger.warning( + "failed_request_status_codes has to be a list of integers or containers" + ) + + return False + + +class HttpCodeRangeContainer: + """ + Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. + Used for backwards compatibility with the old `failed_request_status_codes` option. + """ + + def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: + self._code_ranges = code_ranges + + def __contains__(self, item: object) -> bool: + return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..59bae92a60 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,509 @@ +import sys +import weakref +from functools import wraps + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import ( + _filter_headers, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + NoOpStreamedSpan, + SegmentSource, + SpanStatus, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import ( + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + parse_url, + parse_version, + reraise, + transaction_from_function, +) + +try: + import asyncio + + from aiohttp import ClientSession, TraceConfig + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Set + from types import SimpleNamespace + from typing import Any, ContextManager, Optional, Tuple, Union + + from aiohttp import TraceRequestEndParams, TraceRequestStartParams + from aiohttp.web_request import Request + from aiohttp.web_urldispatcher import UrlMappingMatchInfo + + from sentry_sdk._types import Attributes, Event, EventProcessor + from sentry_sdk.tracing import Span + from sentry_sdk.utils import ExcInfo + + +TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + origin = f"auto.http.{identifier}" + + def __init__( + self, + transaction_style: str = "handler_name", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self._failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(AIOHTTP_VERSION) + _check_minimum_version(AioHttpIntegration, version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + # In the aiohttp integration, all of their HTTP responses are Exceptions. + # Because they have to be raised and handled by the framework, we need to + # register the exceptions as control flow exceptions so that we don't + # accidentally overwrite a status of "ok" with "error". + _register_control_flow_exception(HTTPException) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(AioHttpIntegration) + if integration is None: + return await old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + # Scope data will not leak between requests because aiohttp + # create a task to wrap each request. + scope.generate_propagation_context() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + headers = dict(request.headers) + + span_ctx: "ContextManager[Union[Span, StreamedSpan]]" + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context({"aiohttp_request": request}) + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, + use_annotated_value=False, + ).items(): + header_attributes[ + f"http.request.header.{header.lower()}" + ] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + url_attributes = {} + if should_send_default_pii(): + url_attributes["url.full"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + url_attributes["url.path"] = request.path + + if request.query_string: + url_attributes["url.query"] = request.query_string + + client_address_attributes = {} + if should_send_default_pii() and request.remote: + client_address_attributes["client.address"] = request.remote + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) + + span_ctx = sentry_sdk.traces.start_span( + # If this name makes it to the UI, AIOHTTP's URL + # resolver did not find a route or died trying. + name="generic AIOHTTP request", + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": AioHttpIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE.value, + "http.request.method": request.method, + **url_attributes, + **client_address_attributes, + **header_attributes, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + name="generic AIOHTTP request", + source=TransactionSource.ROUTE, + origin=AioHttpIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"aiohttp_request": request}, + ) + + with span_ctx as span: + try: + response = await old_handle(self, request) + except HTTPException as e: + if isinstance(span, StreamedSpan) and not isinstance( + span, NoOpStreamedSpan + ): + span.set_attribute( + "http.response.status_code", e.status_code + ) + + if e.status_code >= 400: + span.status = SpanStatus.ERROR.value + else: + span.status = SpanStatus.OK.value + else: + # Since a NoOpStreamedSpan can end up here, we have to guard against it + # so this only gets set in the legacy transaction approach. + if not isinstance(span, NoOpStreamedSpan): + span.set_http_status(e.status_code) + + if ( + e.status_code + in integration._failed_request_status_codes + ): + _capture_exception() + raise + except (asyncio.CancelledError, ConnectionResetError): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR.value + else: + span.set_status(SPANSTATUS.CANCELLED) + raise + except Exception: + # This will probably map to a 500 but seems like we + # have no way to tell. Do not set span status. + reraise(*_capture_exception()) + + try: + # A valid response handler will return a valid response with a status. But, if the handler + # returns an invalid response (e.g. None), the line below will raise an AttributeError. + # Even though this is likely invalid, we need to handle this case to ensure we don't break + # the application. + response_status = response.status + except AttributeError: + pass + else: + if isinstance(span, StreamedSpan): + span.set_attribute( + "http.response.status_code", response_status + ) + span.status = ( + SpanStatus.ERROR.value + if response_status >= 400 + else SpanStatus.OK.value + ) + else: + span.set_http_status(response_status) + + return response + + Application._handle = sentry_app_handle + + old_urldispatcher_resolve = UrlDispatcher.resolve + + @wraps(old_urldispatcher_resolve) + async def sentry_urldispatcher_resolve( + self: "UrlDispatcher", request: "Request" + ) -> "UrlMappingMatchInfo": + rv = await old_urldispatcher_resolve(self, request) + + integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) + if integration is None: + return rv + + name = None + + try: + if integration.transaction_style == "handler_name": + name = transaction_from_function(rv.handler) + elif integration.transaction_style == "method_and_path_pattern": + route_info = rv.get_info() + pattern = route_info.get("path") or route_info.get("formatter") + name = "{} {}".format(request.method, pattern) + except Exception: + pass + + if name is not None: + current_span = sentry_sdk.get_current_span() + if isinstance(current_span, StreamedSpan) and not isinstance( + current_span, NoOpStreamedSpan + ): + current_span._segment.name = name + current_span._segment.set_attribute( + "sentry.span.source", + SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, + ) + else: + current_scope = sentry_sdk.get_current_scope() + current_scope.set_transaction_name( + name, + source=SOURCE_FOR_STYLE[integration.transaction_style], + ) + + return rv + + UrlDispatcher.resolve = sentry_urldispatcher_resolve + + old_client_session_init = ClientSession.__init__ + + @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) + def init(*args: "Any", **kwargs: "Any") -> None: + client_trace_configs = list(kwargs.get("trace_configs") or ()) + trace_config = create_trace_config() + client_trace_configs.append(trace_config) + + kwargs["trace_configs"] = client_trace_configs + return old_client_session_init(*args, **kwargs) + + ClientSession.__init__ = init + + +def create_trace_config() -> "TraceConfig": + async def on_request_start( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestStartParams", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(AioHttpIntegration) is None: + return + + method = params.method.upper() + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(params.url), sanitize=False) + + span_name = "%s %s" % ( + method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ) + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(client.options): + attributes: "Attributes" = { + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": AioHttpIntegration.origin, + "http.request.method": method, + } + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + attributes["url.path"] = params.url.path + + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) + else: + legacy_span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=span_name, + origin=AioHttpIntegration.origin, + ) + legacy_span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + legacy_span.set_data("url", parsed_url.url) + legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span = legacy_span + + if should_propagate_trace(client, str(params.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=params.url + ) + ) + if key == BAGGAGE_HEADER_NAME and params.headers.get( + BAGGAGE_HEADER_NAME + ): + # do not overwrite any existing baggage, just append to it + params.headers[key] += "," + value + else: + params.headers[key] = value + + trace_config_ctx.span = span + + async def on_request_end( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestEndParams", + ) -> None: + if trace_config_ctx.span is None: + return + + span = trace_config_ctx.span + status = int(params.response.status) + + if isinstance(span, StreamedSpan): + span.set_attribute("http.response.status_code", status) + span.status = ( + SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value + ) + + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.set_http_status(status) + span.set_data("reason", params.response.reason) + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + trace_config = TraceConfig() + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + + return trace_config + + +def _make_request_processor( + weak_request: "weakref.ReferenceType[Request]", +) -> "EventProcessor": + def aiohttp_processor( + event: "Event", + hint: "dict[str, Tuple[type, BaseException, Any]]", + ) -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + request_info = event.setdefault("request", {}) + + request_info["url"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote} + request_info["headers"] = _filter_headers(dict(request.headers)) + + # Just attach raw data here if it is within bounds, if available. + # Unfortunately there's no way to get structured data from aiohttp + # without awaiting on some coroutine. + request_info["data"] = get_aiohttp_request_data(request) + + return event + + return aiohttp_processor + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "aiohttp", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + return exc_info + + +BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" + + +def get_aiohttp_request_data( + request: "Request", +) -> "Union[Optional[str], AnnotatedValue]": + bytes_body = request._read_bytes + + if bytes_body is not None: + # we have body to show + if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): + return AnnotatedValue.substituted_because_over_size_limit() + + encoding = request.charset or "utf-8" + return bytes_body.decode(encoding, "replace") + + if request.can_read_body: + # body exists but we can't show it + return BODY_NOT_READ_MESSAGE + + # request has no body + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py new file mode 100644 index 0000000000..49459268e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Any, Awaitable, Callable, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import aiomysql # type: ignore[import-not-found] + from aiomysql.connection import Connection # type: ignore[import-not-found] + from aiomysql.cursors import Cursor # type: ignore[import-not-found] +except ImportError: + raise DidNotEnable("aiomysql not installed.") + + +class AioMySQLIntegration(Integration): + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) + + Cursor.execute = _wrap_execute(Cursor.execute) + Cursor.executemany = _wrap_executemany(Cursor.executemany) + + # Patch Connection._connect — this catches ALL connections: + # - aiomysql.connect() + # - aiomysql.create_pool() (pool.py does `from .connection import connect` + # which ultimately calls Connection._connect) + # - Reconnects + Connection._connect = _wrap_connect(Connection._connect) + + +T = TypeVar("T") + + +def _normalize_query(query: str | bytes | bytearray) -> str: + if isinstance(query, (bytes, bytearray)): + query = query.decode("utf-8", errors="replace") + return " ".join(query.split()) + + +def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.execute to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + + # Skip if flagged by executemany (avoids double-recording). + # Do NOT reset the flag here — it must stay True for the entire + # duration of executemany, which may call execute multiple times + # in a loop (non-INSERT fallback). Only _wrap_executemany's + # finally block should clear it. + if getattr(cursor, "_sentry_skip_next_execute", False): + return await f(*args, **kwargs) + + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = params if integration and integration._record_params else None + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.executemany to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = ( + seq_of_params if integration and integration._record_params else None + ) + param_style = "pyformat" if params_list else None + + # Prevent double-recording: _do_execute_many calls self.execute internally + cursor._sentry_skip_next_execute = True + try: + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=True, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + finally: + cursor._sentry_skip_next_execute = False + + return _inner + + +def _get_connection(cursor: Any) -> Any: + """Get the underlying connection from a cursor.""" + return getattr(cursor, "connection", None) + + +def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Connection._connect to capture connection spans.""" + + async def _inner(self: "Connection") -> T: + client = sentry_sdk.get_client() + if client.get_integration(AioMySQLIntegration) is None: + return await f(self) + + if has_span_streaming_enabled(client.options): + breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) + + span_attributes: dict[str, Any] = { + "sentry.op": OP.DB, + "sentry.origin": AioMySQLIntegration.origin, + } | breadcrumb_data + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=breadcrumb_data + ) + res = await f(self) + else: + connect_data = _get_connect_data(self) + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + _set_db_data(span, self) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", + category="query", + data=connect_data, + ) + res = await f(self) + + return res + + return _inner + + +def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: + if use_streaming_keys: + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + data: dict[str, Any] = { + db_system: "mysql", + SPANDATA.DB_DRIVER_NAME: "aiomysql", + } + + host = getattr(conn, "host", None) + if host is not None: + data[SPANDATA.SERVER_ADDRESS] = host + + port = getattr(conn, "port", None) + if port is not None: + data[SPANDATA.SERVER_PORT] = port + + database = getattr(conn, "db", None) + if database is not None: + data[db_name] = database + + user = getattr(conn, "user", None) + if user is not None: + data[SPANDATA.DB_USER] = user + + return data + + +def _set_db_data(span: Any, conn: Any) -> None: + """Set database-related span data from connection object.""" + if isinstance(span, StreamedSpan): + set_value = span.set_attribute + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + set_value = span.set_data + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + set_value(db_system, "mysql") + set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") + + host = getattr(conn, "host", None) + if host is not None: + set_value(SPANDATA.SERVER_ADDRESS, host) + + port = getattr(conn, "port", None) + if port is not None: + set_value(SPANDATA.SERVER_PORT, port) + + database = getattr(conn, "db", None) + if database is not None: + set_value(db_name, database) + + user = getattr(conn, "user", None) + if user is not None: + set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py new file mode 100644 index 0000000000..dfa4aef34c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py @@ -0,0 +1,1154 @@ +import json +import sys +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_anthropic_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + package_version, + reraise, + safe_serialize, +) + +try: + try: + from anthropic import NotGiven + except ImportError: + NotGiven = None + + try: + from anthropic import Omit + except ImportError: + Omit = None + + from anthropic import AsyncStream, Stream + from anthropic.lib.streaming import ( + AsyncMessageStream, + AsyncMessageStreamManager, + MessageStream, + MessageStreamManager, + ) + from anthropic.resources import AsyncMessages, Messages + from anthropic.types import ( + ContentBlockDeltaEvent, + ContentBlockStartEvent, + ContentBlockStopEvent, + MessageDeltaEvent, + MessageStartEvent, + MessageStopEvent, + ) + + if TYPE_CHECKING: + from anthropic.types import MessageStreamEvent, TextBlockParam +except ImportError: + raise DidNotEnable("Anthropic not installed") + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Iterator, + Optional, + Union, + ) + + from anthropic.types import ( + MessageParam, + ModelParam, + RawMessageStreamEvent, + TextBlockParam, + ToolUnionParam, + ) + + from sentry_sdk._types import TextPart + + +class _RecordedUsage: + output_tokens: int = 0 + input_tokens: int = 0 + cache_write_input_tokens: "Optional[int]" = 0 + cache_read_input_tokens: "Optional[int]" = 0 + + +class _StreamSpanContext: + """ + Sets accumulated data on the stream's span and finishes the span on exit. + Is a no-op if the stream has no span set, i.e., when the span has already been finished. + """ + + def __init__( + self, + stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", + # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). + guaranteed_streaming_state: bool = False, + ) -> None: + self._stream = stream + self._guaranteed_streaming_state = guaranteed_streaming_state + + def __enter__(self) -> "_StreamSpanContext": + return self + + def __exit__( + self, + exc_type: "Optional[type[BaseException]]", + exc_val: "Optional[BaseException]", + exc_tb: "Optional[Any]", + ) -> None: + with capture_internal_exceptions(): + if not hasattr(self._stream, "_span"): + return + + if not self._guaranteed_streaming_state and not hasattr( + self._stream, "_model" + ): + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + return + + _set_streaming_output_data( + span=self._stream._span, + integration=self._stream._integration, + model=self._stream._model, + usage=self._stream._usage, + content_blocks=self._stream._content_blocks, + response_id=self._stream._response_id, + finish_reason=self._stream._finish_reason, + ) + + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + + +class AnthropicIntegration(Integration): + identifier = "anthropic" + origin = f"auto.ai.{identifier}" + + def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + version = package_version("anthropic") + _check_minimum_version(AnthropicIntegration, version) + + """ + client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. + Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. + The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, + information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. + + The span can be finished in two places: + - When the user exits the context manager or directly calls close(), the patched close() finishes the span. + - When iteration ends, the finally block in the _iterator wrapper finishes the span. + + Both paths may run. For example, the context manager exit can follow iterator exhaustion. + """ + Messages.create = _wrap_message_create(Messages.create) + Stream.close = _wrap_close(Stream.close) + + AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) + AsyncStream.close = _wrap_async_close(AsyncStream.close) + + """ + client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. + """ + Messages.stream = _wrap_message_stream(Messages.stream) + MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( + MessageStreamManager.__enter__ + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. + if not issubclass(MessageStream, Stream): + MessageStream.close = _wrap_close(MessageStream.close) + + AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) + AsyncMessageStreamManager.__aenter__ = ( + _wrap_async_message_stream_manager_aenter( + AsyncMessageStreamManager.__aenter__ + ) + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. + if not issubclass(AsyncMessageStream, AsyncStream): + AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "anthropic", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": + """ + Get token usage from the Anthropic response. + Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) + """ + input_tokens = 0 + output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 + if hasattr(result, "usage"): + usage = result.usage + if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): + input_tokens = usage.input_tokens + if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): + output_tokens = usage.output_tokens + if hasattr(usage, "cache_read_input_tokens") and isinstance( + usage.cache_read_input_tokens, int + ): + cache_read_input_tokens = usage.cache_read_input_tokens + if hasattr(usage, "cache_creation_input_tokens") and isinstance( + usage.cache_creation_input_tokens, int + ): + cache_write_input_tokens = usage.cache_creation_input_tokens + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens so downstream cost calculations + # (input_tokens - cached) don't produce negative values. + input_tokens += cache_read_input_tokens + cache_write_input_tokens + + return ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) + + +def _collect_ai_data( + event: "MessageStreamEvent", + model: "str | None", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": + """ + Collect model information, token usage, and collect content blocks from the AI streaming response. + """ + with capture_internal_exceptions(): + if hasattr(event, "type"): + if event.type == "content_block_start": + pass + elif event.type == "content_block_delta": + if hasattr(event.delta, "text"): + content_blocks.append(event.delta.text) + elif hasattr(event.delta, "partial_json"): + content_blocks.append(event.delta.partial_json) + elif event.type == "content_block_stop": + pass + + # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. + # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 + if event.type == "message_start": + model = event.message.model or model + response_id = event.message.id + + incoming_usage = event.message.usage + usage.output_tokens = incoming_usage.output_tokens + usage.input_tokens = incoming_usage.input_tokens + + usage.cache_write_input_tokens = getattr( + incoming_usage, "cache_creation_input_tokens", None + ) + usage.cache_read_input_tokens = getattr( + incoming_usage, "cache_read_input_tokens", None + ) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + # Counterintuitive, but message_delta contains cumulative token counts :) + if event.type == "message_delta": + usage.output_tokens = event.usage.output_tokens + + # Update other usage fields if they exist in the event + input_tokens = getattr(event.usage, "input_tokens", None) + if input_tokens is not None: + usage.input_tokens = input_tokens + + cache_creation_input_tokens = getattr( + event.usage, "cache_creation_input_tokens", None + ) + if cache_creation_input_tokens is not None: + usage.cache_write_input_tokens = cache_creation_input_tokens + + cache_read_input_tokens = getattr( + event.usage, "cache_read_input_tokens", None + ) + if cache_read_input_tokens is not None: + usage.cache_read_input_tokens = cache_read_input_tokens + # TODO: Record event.usage.server_tool_use + + if event.delta.stop_reason is not None: + finish_reason = event.delta.stop_reason + + return (model, usage, content_blocks, response_id, finish_reason) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + +def _transform_anthropic_content_block( + content_block: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Transform an Anthropic content block using the Anthropic-specific transformer, + with special handling for Anthropic's text-type documents. + """ + # Handle Anthropic's text-type documents specially (not covered by shared function) + if content_block.get("type") == "document": + source = content_block.get("source") + if isinstance(source, dict) and source.get("type") == "text": + return { + "type": "text", + "text": source.get("data", ""), + } + + # Use Anthropic-specific transformation + result = transform_anthropic_content_part(content_block) + return result if result is not None else content_block + + +def _transform_system_instructions( + system_instructions: "Union[str, Iterable[TextBlockParam]]", +) -> "list[TextPart]": + if isinstance(system_instructions, str): + return [ + { + "type": "text", + "content": system_instructions, + } + ] + + return [ + { + "type": "text", + "content": instruction["text"], + } + for instruction in system_instructions + if isinstance(instruction, dict) and "text" in instruction + ] + + +def _set_common_input_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Optional[Union[str, Iterable[TextBlockParam]]]", + temperature: "Optional[float]", + top_k: "Optional[int]", + top_p: "Optional[float]", + tools: "Optional[Iterable[ToolUnionParam]]", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if ( + messages is not None + and len(messages) > 0 # type: ignore + and should_send_default_pii() + and integration.include_prompts + ): + if isinstance(system, str) or isinstance(system, Iterable): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system)), + ) + + normalized_messages = [] + for message in messages: + if ( + message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER + and "content" in message + and isinstance(message["content"], (list, tuple)) + ): + transformed_content = [] + for item in message["content"]: + # Skip tool_result items - they can contain images/documents + # with nested structures that are difficult to redact properly + if isinstance(item, dict) and item.get("type") == "tool_result": + continue + + # Transform content blocks (images, documents, etc.) + transformed_content.append( + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + ) + + # If there are non-tool-result items, add them as a message + if transformed_content: + normalized_messages.append( + { + "role": message.get("role"), + "content": transformed_content, + } + ) + else: + # Transform content for non-list messages or assistant messages + transformed_message = message.copy() + if "content" in transformed_message: + content = transformed_message["content"] + if isinstance(content, (list, tuple)): + transformed_message["content"] = [ + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + for item in content + ] + normalized_messages.append(transformed_message) + + role_normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(role_normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else role_normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + if model is not None and _is_given(model): + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + if top_k is not None and _is_given(top_k): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore + set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) + + +def _set_create_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "AnthropicIntegration", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=kwargs.get("max_tokens"), # type: ignore + messages=kwargs.get("messages"), # type: ignore + model=kwargs.get("model"), + system=kwargs.get("system"), + temperature=kwargs.get("temperature"), + top_k=kwargs.get("top_k"), + top_p=kwargs.get("top_p"), + tools=kwargs.get("tools"), + ) + + +def _wrap_synchronous_message_iterator( + stream: "Union[Stream, MessageStream]", + iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +async def _wrap_asynchronous_message_iterator( + stream: "Union[AsyncStream, AsyncMessageStream]", + iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + async for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +def _set_output_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + model: "str | None", + input_tokens: "int | None", + output_tokens: "int | None", + cache_read_input_tokens: "int | None", + cache_write_input_tokens: "int | None", + content_blocks: "list[Any]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> None: + """ + Set output data for the span based on the AI response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + if finish_reason is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in content_blocks: + if output["type"] == "text": + output_messages["response"].append(output["text"]) + elif output["type"] == "tool_use": + output_messages["tool"].append(output) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + record_token_usage( + span, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_tokens_cached=cache_read_input_tokens, + input_tokens_cache_write=cache_write_input_tokens, + ) + + +def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, Stream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_synchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +async def _sentry_patched_create_async( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return await f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, AsyncStream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_asynchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +def _wrap_message_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return _sentry_patched_create_sync(f, *args, **kwargs) + + return _sentry_wrapped_create_sync + + +def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: + """ + Initialize fields for accumulating output on the Stream instance. + """ + if not hasattr(stream, "_model"): + stream._model = None + stream._usage = _RecordedUsage() + stream._content_blocks = [] + stream._response_id = None + stream._finish_reason = None + + +def _accumulate_event_data( + stream: "Union[Stream, MessageStream]", + event: "Union[RawMessageStreamEvent, MessageStreamEvent]", +) -> None: + """ + Update accumulated output from a single stream event. + """ + (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( + event, + stream._model, + stream._usage, + stream._content_blocks, + stream._response_id, + stream._finish_reason, + ) + + stream._model = model + stream._usage = usage + stream._content_blocks = content_blocks + stream._response_id = response_id + stream._finish_reason = finish_reason + + +def _set_streaming_output_data( + span: "Span", + integration: "AnthropicIntegration", + model: "Optional[str]", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "Optional[str]", + finish_reason: "Optional[str]", +) -> None: + """ + Set output attributes on the AI Client Span. + """ + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + response_id=response_id, + finish_reason=finish_reason, + ) + + +def _wrap_close( + f: "Callable[..., None]", +) -> "Callable[..., None]": + """ + Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. + """ + + def close(self: "Union[Stream, MessageStream]") -> None: + with _StreamSpanContext(self): + return f(self) + + return close + + +def _wrap_message_create_async(f: "Any") -> "Any": + @wraps(f) + async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return await _sentry_patched_create_async(f, *args, **kwargs) + + return _sentry_wrapped_create_async + + +def _wrap_async_close( + f: "Callable[..., Awaitable[None]]", +) -> "Callable[..., Awaitable[None]]": + """ + Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. + """ + + async def close(self: "AsyncStream") -> None: + with _StreamSpanContext(self): + return await f(self) + + return close + + +def _wrap_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_message_stream_manager_enter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": + if not hasattr(self, "_max_tokens"): + return f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return f(self) + + if self._messages is None: + return f(self) + + try: + iter(self._messages) + except TypeError: + return f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_synchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_enter + + +def _wrap_async_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream( + *args: "Any", **kwargs: "Any" + ) -> "AsyncMessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + async def _sentry_patched_aenter( + self: "AsyncMessageStreamManager", + ) -> "AsyncMessageStream": + if not hasattr(self, "_max_tokens"): + return await f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return await f(self) + + if self._messages is None: + return await f(self) + + try: + iter(self._messages) + except TypeError: + return await f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = await f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_asynchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_aenter + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different anthropic versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py new file mode 100644 index 0000000000..0215ffa093 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py @@ -0,0 +1,28 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class ArgvIntegration(Integration): + identifier = "argv" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: + extra = event.setdefault("extra", {}) + # If some event processor decided to set extra to e.g. an + # `int`, don't crash. Not here. + if isinstance(extra, dict): + extra["sys.argv"] = sys.argv + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py new file mode 100644 index 0000000000..1ce228d3a5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py @@ -0,0 +1,167 @@ +from importlib import import_module + +import sentry_sdk +from sentry_sdk import capture_event, get_client +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + # importing like this is necessary due to name shadowing in ariadne + # (ariadne.graphql is also a function) + ariadne_graphql = import_module("ariadne.graphql") +except ImportError: + raise DidNotEnable("ariadne is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, List, Optional + + from ariadne.types import ( # type: ignore + GraphQLError, + GraphQLResult, + GraphQLSchema, + QueryParser, + ) + from graphql.language.ast import DocumentNode + + from sentry_sdk._types import Event, EventProcessor + + +class AriadneIntegration(Integration): + identifier = "ariadne" + + @staticmethod + def setup_once() -> None: + version = package_version("ariadne") + _check_minimum_version(AriadneIntegration, version) + + ignore_logger("ariadne") + + _patch_graphql() + + +def _patch_graphql() -> None: + old_parse_query = ariadne_graphql.parse_query + old_handle_errors = ariadne_graphql.handle_graphql_errors + old_handle_query_result = ariadne_graphql.handle_query_result + + @ensure_integration_enabled(AriadneIntegration, old_parse_query) + def _sentry_patched_parse_query( + context_value: "Optional[Any]", + query_parser: "Optional[QueryParser]", + data: "Any", + ) -> "DocumentNode": + event_processor = _make_request_event_processor(data) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + result = old_parse_query(context_value, query_parser, data) + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_errors) + def _sentry_patched_handle_graphql_errors( + errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + result = old_handle_errors(errors, *args, **kwargs) + + event_processor = _make_response_event_processor(result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) + def _sentry_patched_handle_query_result( + result: "Any", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + query_result = old_handle_query_result(result, *args, **kwargs) + + event_processor = _make_response_event_processor(query_result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return query_result + + ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore + ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore + ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore + + +def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": + """Add request data and api_target to events.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + if not isinstance(data, dict): + return event + + with capture_internal_exceptions(): + try: + content_length = int( + (data.get("headers") or {}).get("Content-Length", 0) + ) + except (TypeError, ValueError): + return event + + if should_send_default_pii() and request_body_within_bounds( + get_client(), content_length + ): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + request_info["data"] = data + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + return inner + + +def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": + """Add response data to the event's response context.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii() and response.get("errors"): + contexts = event.setdefault("contexts", {}) + contexts["response"] = { + "data": response, + } + + return event + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py new file mode 100644 index 0000000000..da03bafb8b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py @@ -0,0 +1,277 @@ +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Transaction, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +try: + import arq.worker + from arq.connections import ArqRedis + from arq.version import VERSION as ARQ_VERSION + from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker +except ImportError: + raise DidNotEnable("Arq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from arq.cron import CronJob + from arq.jobs import Job + from arq.typing import WorkerCoroutine + from arq.worker import Function + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) + + +class ArqIntegration(Integration): + identifier = "arq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + try: + if isinstance(ARQ_VERSION, str): + version = parse_version(ARQ_VERSION) + else: + version = ARQ_VERSION.version[:2] + + except (TypeError, ValueError): + version = None + + _check_minimum_version(ArqIntegration, version) + + patch_enqueue_job() + patch_run_job() + patch_create_worker() + + _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore + + ignore_logger("arq.worker") + + +def patch_enqueue_job() -> None: + old_enqueue_job = ArqRedis.enqueue_job + original_kwdefaults = old_enqueue_job.__kwdefaults__ + + async def _sentry_enqueue_job( + self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" + ) -> "Optional[Job]": + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_enqueue_job(self, function, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=function, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_ARQ, + "sentry.origin": ArqIntegration.origin, + }, + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults + ArqRedis.enqueue_job = _sentry_enqueue_job + + +def patch_run_job() -> None: + old_run_job = Worker.run_job + + async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_run_job(self, job_id, score) + + with sentry_sdk.isolation_scope() as scope: + scope._name = "arq" + scope.clear_breadcrumbs() + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name="unknown arq task", + attributes={ + "sentry.op": OP.QUEUE_TASK_ARQ, + "sentry.origin": ArqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job_id, + }, + parent_span=None, + ): + return await old_run_job(self, job_id, score) + + transaction = Transaction( + name="unknown arq task", + status="ok", + op=OP.QUEUE_TASK_ARQ, + source=TransactionSource.TASK, + origin=ArqIntegration.origin, + ) + + with sentry_sdk.start_transaction(transaction): + return await old_run_job(self, job_id, score) + + Worker.run_job = _sentry_run_job + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + + if scope.transaction is not None: + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + scope.transaction.set_status(SPANSTATUS.ABORTED) + return + + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": ArqIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + if scope.transaction is not None: + scope.transaction.name = ctx["job_name"] + event["transaction"] = ctx["job_name"] + + tags = event.setdefault("tags", {}) + tags["arq_task_id"] = ctx["job_id"] + tags["arq_task_retry"] = ctx["job_try"] > 1 + extra = event.setdefault("extra", {}) + extra["arq-job"] = { + "task": ctx["job_name"], + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": ctx["job_try"], + } + + return event + + return event_processor + + +def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": + async def _sentry_coroutine( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(ArqIntegration) + if integration is None: + return await coroutine(ctx, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + scope = sentry_sdk.get_current_scope() + span = scope.streamed_span + if span is not None: + span.name = name + + scope.set_transaction_name(name) + + sentry_sdk.get_isolation_scope().add_event_processor( + _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) + ) + + try: + result = await coroutine(ctx, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_coroutine + + +def patch_create_worker() -> None: + old_create_worker = arq.worker.create_worker + + @ensure_integration_enabled(ArqIntegration, old_create_worker) + def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": + settings_cls = args[0] if args else kwargs.get("settings_cls") + + if isinstance(settings_cls, dict): + if "functions" in settings_cls: + settings_cls["functions"] = [ + _get_arq_function(func) + for func in settings_cls.get("functions", []) + ] + if "cron_jobs" in settings_cls: + settings_cls["cron_jobs"] = [ + _get_arq_cron_job(cron_job) + for cron_job in settings_cls.get("cron_jobs", []) + ] + + if hasattr(settings_cls, "functions"): + settings_cls.functions = [ # type: ignore[union-attr] + _get_arq_function(func) + for func in settings_cls.functions # type: ignore[union-attr] + ] + if hasattr(settings_cls, "cron_jobs"): + settings_cls.cron_jobs = [ # type: ignore[union-attr] + _get_arq_cron_job(cron_job) + for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] + ] + + if "functions" in kwargs: + kwargs["functions"] = [ + _get_arq_function(func) for func in kwargs.get("functions", []) + ] + if "cron_jobs" in kwargs: + kwargs["cron_jobs"] = [ + _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) + ] + + return old_create_worker(*args, **kwargs) + + arq.worker.create_worker = _sentry_create_worker + + +def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": + arq_func = arq.worker.func(func) + arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) + + return arq_func + + +def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": + cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) + + return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py new file mode 100644 index 0000000000..8b1ff5e2a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py @@ -0,0 +1,543 @@ +""" +An ASGI middleware. + +Based on Tom Christie's `sentry-asgi `. +""" + +import inspect +import sys +from copy import deepcopy +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._asgi_common import ( + _get_headers, + _get_ip, + _get_path, + _get_request_attributes, + _get_request_data, + _get_url, + _RootPathInPath, +) +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + SegmentSource, + StreamedSpan, +) +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + Transaction, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + ContextVar, + _get_installed_modules, + capture_internal_exceptions, + event_from_exception, + logger, + qualname_from_function, + reraise, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, ContextManager, Dict, Optional, Tuple, Union + + from sentry_sdk._types import Attributes, Event, Hint + from sentry_sdk.tracing import Span + + +_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") + +_DEFAULT_TRANSACTION_NAME = "generic ASGI request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 +if sys.version_info >= (3, 14): + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": mechanism_type, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _looks_like_asgi3(app: "Any") -> bool: + """ + Try to figure out if an application object supports ASGI3. + + This is how uvicorn figures out the application version as well. + """ + if inspect.isclass(app): + return hasattr(app, "__await__") + elif inspect.isfunction(app): + return iscoroutinefunction(app) + else: + call = getattr(app, "__call__", None) # noqa + return iscoroutinefunction(call) + + +class SentryAsgiMiddleware: + __slots__ = ( + "app", + "__call__", + "transaction_style", + "mechanism_type", + "span_origin", + "http_methods_to_capture", + "root_path_in_path", + ) + + def __init__( + self, + app: "Any", + unsafe_context_data: bool = False, + transaction_style: str = "endpoint", + mechanism_type: str = "asgi", + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + asgi_version: "Optional[int]" = None, + root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, + ) -> None: + """ + Instrument an ASGI application with Sentry. Provides HTTP/websocket + data to sent events and basic handling for exceptions bubbling up + through the middleware. + + :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. + """ + if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The ASGI middleware for Sentry requires Python 3.7+ " + "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + + asgi_middleware_while_using_starlette_or_fastapi = ( + mechanism_type == "asgi" and "starlette" in _get_installed_modules() + ) + if asgi_middleware_while_using_starlette_or_fastapi: + logger.warning( + "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " + "Please remove 'SentryAsgiMiddleware' from your project. " + "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." + ) + + self.transaction_style = transaction_style + self.mechanism_type = mechanism_type + self.span_origin = span_origin + self.app = app + self.http_methods_to_capture = http_methods_to_capture + self.root_path_in_path = root_path_in_path + + if asgi_version is None: + if _looks_like_asgi3(app): + asgi_version = 3 + else: + asgi_version = 2 + + if asgi_version == 3: + self.__call__ = self._run_asgi3 + elif asgi_version == 2: + self.__call__ = self._run_asgi2 # type: ignore + + def _capture_lifespan_exception(self, exc: Exception) -> None: + """Capture exceptions raise in application lifespan handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _capture_request_exception(self, exc: Exception) -> None: + """Capture exceptions raised in incoming request handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _run_asgi2(self, scope: "Any") -> "Any": + async def inner(receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=2) + + return inner + + async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=3) + + async def _run_app( + self, scope: "Any", receive: "Any", send: "Any", asgi_version: int + ) -> "Any": + is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) + is_lifespan = scope["type"] == "lifespan" + if is_recursive_asgi_middleware or is_lifespan: + try: + if asgi_version == 2: + return await self.app(scope)(receive, send) + else: + return await self.app(scope, receive, send) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_lifespan_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_lifespan_exception(exc) + reraise(*exc_info) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _asgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as sentry_scope: + with track_session(sentry_scope, session_mode="request"): + sentry_scope.clear_breadcrumbs() + sentry_scope._name = "asgi" + processor = partial(self.event_processor, asgi_scope=scope) + sentry_scope.add_event_processor(processor) + + ty = scope["type"] + ( + transaction_name, + transaction_source, + ) = self._get_transaction_name_and_source( + self.transaction_style, + scope, + ) + + method = scope.get("method", "").upper() + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + if span_streaming: + segment: "Optional[StreamedSpan]" = None + attributes: "Attributes" = { + "sentry.span.source": getattr( + transaction_source, "value", transaction_source + ), + "sentry.origin": self.span_origin, + "network.protocol.name": ty, + } + + if scope.get("client") and should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) + + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + sentry_sdk.traces.continue_trace(_get_headers(scope)) + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = f"{ty}.server" + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + else: + sentry_sdk.traces.new_trace() + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = OP.HTTP_SERVER + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + + span_ctx = segment or nullcontext() + + else: + transaction = None + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + transaction = continue_trace( + _get_headers(scope), + op="{}.server".format(ty), + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + else: + transaction = Transaction( + op=OP.HTTP_SERVER, + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + + if transaction: + transaction.set_tag("asgi.type", ty) + + span_ctx = ( + sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"asgi_scope": scope}, + ) + if transaction is not None + else nullcontext() + ) + + with span_ctx as span: + if isinstance(span, StreamedSpan): + for attribute, value in _get_request_attributes( + scope, + root_path_in_path=self.root_path_in_path, + ).items(): + span.set_attribute(attribute, value) + + try: + + async def _sentry_wrapped_send( + event: "Dict[str, Any]", + ) -> "Any": + if span is not None: + is_http_response = ( + event.get("type") == "http.response.start" + and "status" in event + ) + if is_http_response: + if isinstance(span, StreamedSpan): + span.status = ( + "error" + if event["status"] >= 400 + else "ok" + ) + span.set_attribute( + "http.response.status_code", + event["status"], + ) + else: + span.set_http_status(event["status"]) + + return await send(event) + + if asgi_version == 2: + return await self.app(scope)( + receive, _sentry_wrapped_send + ) + else: + return await self.app( + scope, receive, _sentry_wrapped_send + ) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_request_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_request_exception(exc) + reraise(*exc_info) + + finally: + if isinstance(span, StreamedSpan): + already_set = ( + span is not None + and span.name != _DEFAULT_TRANSACTION_NAME + and span.get_attributes().get("sentry.span.source") + in [ + SegmentSource.COMPONENT.value, + SegmentSource.ROUTE.value, + SegmentSource.CUSTOM.value, + ] + ) + with capture_internal_exceptions(): + if not already_set: + name, source = ( + self._get_segment_name_and_source( + self.transaction_style, scope + ) + ) + span.name = name + span.set_attribute("sentry.span.source", source) + finally: + _asgi_middleware_applied.set(False) + + def event_processor( + self, event: "Event", hint: "Hint", asgi_scope: "Any" + ) -> "Optional[Event]": + request_data = event.get("request", {}) + request_data.update( + _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) + ) + event["request"] = deepcopy(request_data) + + # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) + transaction = event.get("transaction") + transaction_source = (event.get("transaction_info") or {}).get("source") + already_set = ( + transaction is not None + and transaction != _DEFAULT_TRANSACTION_NAME + and transaction_source + in [ + TransactionSource.COMPONENT, + TransactionSource.ROUTE, + TransactionSource.CUSTOM, + ] + ) + if not already_set: + name, source = self._get_transaction_name_and_source( + self.transaction_style, asgi_scope + ) + event["transaction"] = name + event["transaction_info"] = {"source": source} + + return event + + # Helper functions. + # + # Note: Those functions are not public API. If you want to mutate request + # data to your liking it's recommended to use the `before_send` callback + # for that. + + def _get_transaction_name_and_source( + self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SOURCE_FOR_STYLE[transaction_style] + ty = asgi_scope.get("type") + + if transaction_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = transaction_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + elif transaction_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + return name, source + + return name, source + + def _get_segment_name_and_source( + self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value + ty = asgi_scope.get("type") + + if segment_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = qualname_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + elif segment_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = SegmentSource.ROUTE.value + return name, source + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py new file mode 100644 index 0000000000..4b3f6d330e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py @@ -0,0 +1,280 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import AsyncHttpTransport +from sentry_sdk.utils import ( + event_from_exception, + is_internal_task, + logger, + reraise, +) + +try: + import asyncio + from asyncio.tasks import Task +except ImportError: + raise DidNotEnable("asyncio not available") + +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from collections.abc import Coroutine + from typing import Any, Callable, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T", bound=Callable[..., Any]) + + +def get_name(coro: "Any") -> str: + return ( + getattr(coro, "__qualname__", None) + or getattr(coro, "__name__", None) + or "coroutine without __name__" + ) + + +def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": + # Only __name__ and __qualname__ are copied from function to coroutine in CPython + return functools.partial( + functools.update_wrapper, + wrapped=wrapped, # type: ignore + assigned=("__name__", "__qualname__"), + updated=(), + ) + + +def patch_loop_close() -> None: + """Patch loop.close to flush pending events before shutdown.""" + # Atexit shutdown hook happens after the event loop is closed. + # Therefore, it is necessary to patch the loop.close method to ensure + # that pending events are flushed before the interpreter shuts down. + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No running loop → cannot patch now + return + + if getattr(loop, "_sentry_flush_patched", False): + return + + async def _flush() -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + try: + if not isinstance(client.transport, AsyncHttpTransport): + return + + await client.close_async() + except Exception: + logger.warning("Sentry flush failed during loop shutdown", exc_info=True) + + orig_close = loop.close + + def _patched_close() -> None: + try: + loop.run_until_complete(_flush()) + except Exception: + logger.debug( + "Could not flush Sentry events during loop close", exc_info=True + ) + finally: + orig_close() + + loop.close = _patched_close # type: ignore + loop._sentry_flush_patched = True # type: ignore + + +def _create_task_with_factory( + orig_task_factory: "Any", + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", +) -> "asyncio.Task[Any]": + task = None + + # Trying to use user set task factory (if there is one) + if orig_task_factory: + task = orig_task_factory(loop, coro, **kwargs) + + if task is None: + # The default task factory in `asyncio` does not have its own function + # but is just a couple of lines in `asyncio.base_events.create_task()` + # Those lines are copied here. + + # WARNING: + # If the default behavior of the task creation in asyncio changes, + # this will break! + task = Task(coro, loop=loop, **kwargs) + if task._source_traceback: # type: ignore + del task._source_traceback[-1] # type: ignore + + return task + + +def patch_asyncio() -> None: + orig_task_factory = None + try: + loop = asyncio.get_running_loop() + orig_task_factory = loop.get_task_factory() + + # Check if already patched + if getattr(orig_task_factory, "_is_sentry_task_factory", False): + return + + def _sentry_task_factory( + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", + ) -> "asyncio.Future[Any]": + # Check if this is an internal Sentry task + if is_internal_task(): + return _create_task_with_factory( + orig_task_factory, loop, coro, **kwargs + ) + + @_wrap_coroutine(coro) + async def _task_with_sentry_span_creation() -> "Any": + result = None + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncioIntegration) + task_spans = integration.task_spans if integration else False + + span_ctx: "Optional[Union[StreamedSpan, Span]]" = None + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope(): + if task_spans: + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=get_name(coro), + attributes={ + "sentry.op": OP.FUNCTION, + "sentry.origin": AsyncioIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.FUNCTION, + name=get_name(coro), + origin=AsyncioIntegration.origin, + ) + + with span_ctx if span_ctx else nullcontext(): + try: + result = await coro + except StopAsyncIteration as e: + raise e from None + except Exception: + reraise(*_capture_exception()) + + return result + + task = _create_task_with_factory( + orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs + ) + + # Set the task name to include the original coroutine's name + try: + task.set_name(f"{get_name(coro)} (Sentry-wrapped)") + except AttributeError: + # set_name might not be available in all Python versions + pass + + return task + + _sentry_task_factory._is_sentry_task_factory = True # type: ignore + loop.set_task_factory(_sentry_task_factory) # type: ignore + + except RuntimeError: + # When there is no running loop, we have nothing to patch. + logger.warning( + "There is no running asyncio loop so there is nothing Sentry can patch. " + "Please make sure you call sentry_sdk.init() within a running " + "asyncio loop for the AsyncioIntegration to work. " + "See https://docs.sentry.io/platforms/python/integrations/asyncio/" + ) + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + + integration = client.get_integration(AsyncioIntegration) + if integration is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "asyncio", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class AsyncioIntegration(Integration): + identifier = "asyncio" + origin = f"auto.function.{identifier}" + + def __init__(self, task_spans: bool = True) -> None: + self.task_spans = task_spans + + @staticmethod + def setup_once() -> None: + patch_asyncio() + patch_loop_close() + + +def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: + """ + Enable AsyncioIntegration with the provided options. + + This is useful in scenarios where Sentry needs to be initialized before + an event loop is set up, but you still want to instrument asyncio once there + is an event loop. In that case, you can sentry_sdk.init() early on without + the AsyncioIntegration and then, once the event loop has been set up, + execute: + + ```python + from sentry_sdk.integrations.asyncio import enable_asyncio_integration + + async def async_entrypoint(): + enable_asyncio_integration() + ``` + + Any arguments provided will be passed to AsyncioIntegration() as is. + + If AsyncioIntegration has already patched the current event loop, this + function won't have any effect. + + If AsyncioIntegration was provided in + sentry_sdk.init(disabled_integrations=[...]), this function will ignore that + and the integration will be enabled. + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # This function purposefully bypasses the integration machinery in + # integrations/__init__.py. _installed_integrations/_processed_integrations + # is used to prevent double patching the same module, but in the case of + # the AsyncioIntegration, we don't monkeypatch the standard library directly, + # we patch the currently running event loop, and we keep the record of doing + # that on the loop itself. + logger.debug("Setting up integration asyncio") + + integration = AsyncioIntegration(*args, **kwargs) + integration.setup_once() + + if "asyncio" not in client.integrations: + client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py new file mode 100644 index 0000000000..186176d268 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import contextlib +import re +from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import asyncpg # type: ignore + from asyncpg.cursor import ( # type: ignore + BaseCursor, + Cursor, + CursorIterator, + ) + +except ImportError: + raise DidNotEnable("asyncpg not installed.") + + +class AsyncPGIntegration(Integration): + identifier = "asyncpg" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AsyncPGIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + # asyncpg.__version__ is a string containing the semantic version in the form of ".." + asyncpg_version = parse_version(asyncpg.__version__) + _check_minimum_version(AsyncPGIntegration, asyncpg_version) + + asyncpg.Connection.execute = _wrap_execute( + asyncpg.Connection.execute, + ) + + asyncpg.Connection._execute = _wrap_connection_method( + asyncpg.Connection._execute + ) + asyncpg.Connection._executemany = _wrap_connection_method( + asyncpg.Connection._executemany, executemany=True + ) + asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) + + BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) + BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) + + asyncpg.connect_utils._connect_addr = _wrap_connect_addr( + asyncpg.connect_utils._connect_addr + ) + + +T = TypeVar("T") + + +def _normalize_query(query: str) -> str: + return re.sub(r"\s+", " ", query).strip() + + +def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + # Avoid recording calls to _execute twice. + # Calls to Connection.execute with args also call + # Connection._execute, which is recorded separately + # args[0] = the connection object, args[1] is the query + if len(args) > 2: + return await f(*args, **kwargs) + + query = _normalize_query(args[1]) + with record_sql_queries( + cursor=None, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + span_origin=AsyncPGIntegration.origin, + ) as span: + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +SubCursor = TypeVar("SubCursor", bound=BaseCursor) + + +@contextlib.contextmanager +def _record( + cursor: "SubCursor | None", + query: str, + params_list: "tuple[Any, ...] | None", + *, + executemany: bool = False, +) -> "Iterator[Union[Span, StreamedSpan]]": + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncPGIntegration) + if integration is not None and not integration._record_params: + params_list = None + + param_style = "pyformat" if params_list else None + + query = _normalize_query(query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=params_list, + paramstyle=param_style, + executemany=executemany, + record_cursor_repr=cursor is not None, + span_origin=AsyncPGIntegration.origin, + ) as span: + yield span + + +def _wrap_connection_method( + f: "Callable[..., Awaitable[T]]", *, executemany: bool = False +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + query = args[1] + params_list = args[2] if len(args) > 2 else None + with _record(None, query, params_list, executemany=executemany) as span: + _set_db_data(span, args[0]) + + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_cursor_method( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + if type(cursor) is CursorIterator: + span_op_override_value = OP.DB_CURSOR_ITERATOR + elif type(cursor) is Cursor: + span_op_override_value = OP.DB_CURSOR_FETCH + else: + span_op_override_value = None + + query = _normalize_query(cursor._query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + record_cursor_repr=True, + span_origin=AsyncPGIntegration.origin, + span_op_override_value=span_op_override_value, + ) as span: + _set_db_data(span, cursor._connection) + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_connect_addr( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + user = kwargs["params"].user + database = kwargs["params"].database + addr = kwargs.get("addr") + + if has_span_streaming_enabled(client.options): + span_attributes = { + "sentry.op": OP.DB, + "sentry.origin": AsyncPGIntegration.origin, + SPANDATA.DB_SYSTEM_NAME: "postgresql", + SPANDATA.DB_USER: user, + SPANDATA.DB_NAMESPACE: database, + SPANDATA.DB_DRIVER_NAME: "asyncpg", + } + if addr: + try: + span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] + span_attributes[SPANDATA.SERVER_PORT] = addr[1] + except IndexError: + pass + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span_attributes + ) + res = await f(*args, **kwargs) + + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AsyncPGIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + span.set_data(SPANDATA.DB_NAME, database) + span.set_data(SPANDATA.DB_USER, user) + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span._data + ) + res = await f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + addr = conn._addr + database = conn._params.database + user = conn._params.user + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") + if addr: + try: + span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_attribute(SPANDATA.DB_NAMESPACE, database) + + if user: + span.set_attribute(SPANDATA.DB_USER, user) + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_data(SPANDATA.DB_NAME, database) + + if user: + span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py new file mode 100644 index 0000000000..b573e76f09 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py @@ -0,0 +1,51 @@ +import atexit +import os +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Optional + + +def default_callback(pending: int, timeout: int) -> None: + """This is the default shutdown callback that is set on the options. + It prints out a message to stderr that informs the user that some events + are still pending and the process is waiting for them to flush out. + """ + + def echo(msg: str) -> None: + sys.stderr.write(msg + "\n") + + echo("Sentry is attempting to send %i pending events" % pending) + echo("Waiting up to %s seconds" % timeout) + echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) + sys.stderr.flush() + + +class AtexitIntegration(Integration): + identifier = "atexit" + + def __init__(self, callback: "Optional[Any]" = None) -> None: + if callback is None: + callback = default_callback + self.callback = callback + + @staticmethod + def setup_once() -> None: + @atexit.register + def _shutdown() -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(AtexitIntegration) + + if integration is None: + return + + logger.debug("atexit: got shutdown signal") + logger.debug("atexit: shutting down client") + sentry_sdk.get_isolation_scope().end_session() + + client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py new file mode 100644 index 0000000000..c7fe77714a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py @@ -0,0 +1,542 @@ +import functools +import json +import re +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING +from urllib.parse import urlencode + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.cloud_resource_context import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + +# Constants +TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + + +def _wrap_init_error(init_error: "F") -> "F": + @ensure_integration_enabled(AwsLambdaIntegration, init_error) + def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + with capture_internal_exceptions(): + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + exc_info = sys.exc_info() + if exc_info and all(exc_info): + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + + else: + # Fall back to AWS lambdas JSON representation of the error + error_info = args[1] + if isinstance(error_info, str): + error_info = json.loads(error_info) + sentry_event = _event_from_error_json(error_info) + sentry_sdk.capture_event(sentry_event) + + return init_error(*args, **kwargs) + + return sentry_init_error # type: ignore + + +def _wrap_handler(handler: "F") -> "F": + @functools.wraps(handler) + def sentry_handler( + aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, + # `event` here is *likely* a dictionary, but also might be a number of + # other types (str, int, float, None). + # + # In some cases, it is a list (if the user is batch-invoking their + # function, for example), in which case we'll use the first entry as a + # representative from which to try pulling request data. (Presumably it + # will be the same for all events in the list, since they're all hitting + # the lambda in the same request.) + + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + + if integration is None: + return handler(aws_event, aws_context, *args, **kwargs) + + if isinstance(aws_event, list) and len(aws_event) >= 1: + request_data = aws_event[0] + batch_size = len(aws_event) + else: + request_data = aws_event + batch_size = 1 + + if not isinstance(request_data, dict): + # If we're not dealing with a dictionary, we won't be able to get + # headers, path, http method, etc in any case, so it's fine that + # this is empty + request_data = {} + + configured_time = aws_context.get_remaining_time_in_millis() + aws_region = aws_context.invoked_function_arn.split(":")[3] + + with sentry_sdk.isolation_scope() as scope: + timeout_thread = None + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + request_data, aws_context, configured_time + ) + ) + scope.set_tag("aws_region", aws_region) + if batch_size > 1: + scope.set_tag("batch_request", True) + scope.set_tag("batch_size", batch_size) + + # Starting the Timeout thread only if the configured time is greater than Timeout warning + # buffer and timeout_warning parameter is set True. + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = ( + configured_time - TIMEOUT_WARNING_BUFFER + ) / MILLIS_TO_SECONDS + + timeout_thread = TimeoutThread( + waiting_time, + configured_time / MILLIS_TO_SECONDS, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = request_data.get("headers", {}) + # Some AWS Services (ie. EventBridge) set headers as a list + # or None, so we must ensure it is a dict + if not isinstance(headers, dict): + headers = {} + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + additional_attributes: "dict[str, Any]" = {} + if "httpMethod" in request_data: + additional_attributes["http.request.method"] = request_data[ + "httpMethod" + ] + + if should_send_default_pii() and "queryStringParameters" in request_data: + qs = request_data["queryStringParameters"] + if qs: + additional_attributes["url.query"] = urlencode(qs) + + sampling_context = { + "aws_event": aws_event, + "aws_context": aws_context, + } + + function_name = aws_context.function_name + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_AWS, + "sentry.origin": AwsLambdaIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.region": aws_region, + "cloud.resource_id": aws_context.invoked_function_arn, + "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, + "cloud.provider": CLOUD_PROVIDER.AWS, + "faas.name": function_name, + "faas.invocation_id": aws_context.aws_request_id, + "faas.version": aws_context.function_version, + "aws.lambda.invoked_arn": aws_context.invoked_function_arn, + "aws.log.group.names": [aws_context.log_group_name], + "aws.log.stream.names": [aws_context.log_stream_name], + "messaging.batch.message_count": batch_size, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_AWS, + name=function_name, + source=TransactionSource.COMPONENT, + origin=AwsLambdaIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return handler(aws_event, aws_context, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + + return sentry_handler # type: ignore + + +def _drain_queue() -> None: + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + if integration is not None: + # Flush out the event queue before AWS kills the + # process. + client.flush() + + +class AwsLambdaIntegration(Integration): + identifier = "aws_lambda" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + lambda_bootstrap = get_lambda_bootstrap() + if not lambda_bootstrap: + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find bootstrap module)" + ) + return + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find handle_event_request)" + ) + return + + pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 + + if pre_37: + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_event_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + old_handle_http_request = lambda_bootstrap.handle_http_request + + def sentry_handle_http_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_http_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_http_request = sentry_handle_http_request + + # Patch to_json to drain the queue. This should work even when the + # SDK is initialized inside of the handler + + old_to_json = lambda_bootstrap.to_json + + def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return old_to_json(*args, **kwargs) + + lambda_bootstrap.to_json = sentry_to_json + else: + lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( + lambda_bootstrap.LambdaRuntimeClient.post_init_error + ) + + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f: "F") -> "F": + def inner(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore + + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + ) + + +def get_lambda_bootstrap() -> "Optional[Any]": + # Python 3.7: If the bootstrap module is *already imported*, it is the + # one we actually want to use (no idea what's in __main__) + # + # Python 3.8: bootstrap is also importable, but will be the same file + # as __main__ imported under a different name: + # + # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ + # sys.modules['__main__'] is not sys.modules['bootstrap'] + # + # Python 3.9: bootstrap is in __main__.awslambdaricmain + # + # On container builds using the `aws-lambda-python-runtime-interface-client` + # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap + # + # Such a setup would then make all monkeypatches useless. + if "bootstrap" in sys.modules: + return sys.modules["bootstrap"] + elif "__main__" in sys.modules: + module = sys.modules["__main__"] + # python3.9 runtime + if hasattr(module, "awslambdaricmain") and hasattr( + module.awslambdaricmain, "bootstrap" + ): + return module.awslambdaricmain.bootstrap + elif hasattr(module, "bootstrap"): + # awslambdaric python module in container builds + return module.bootstrap + + # python3.8 runtime + return module + else: + return None + + +def _make_request_event_processor( + aws_event: "Any", aws_context: "Any", configured_timeout: "Any" +) -> "EventProcessor": + start_time = datetime.now(timezone.utc) + + def event_processor( + sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time + ) -> "Optional[Event]": + remaining_time_in_milis = aws_context.get_remaining_time_in_millis() + exec_duration = configured_timeout - remaining_time_in_milis + + extra = sentry_event.setdefault("extra", {}) + extra["lambda"] = { + "function_name": aws_context.function_name, + "function_version": aws_context.function_version, + "invoked_function_arn": aws_context.invoked_function_arn, + "aws_request_id": aws_context.aws_request_id, + "execution_duration_in_millis": exec_duration, + "remaining_time_in_millis": remaining_time_in_milis, + } + + extra["cloudwatch logs"] = { + "url": _get_cloudwatch_logs_url(aws_context, start_time), + "log_group": aws_context.log_group_name, + "log_stream": aws_context.log_stream_name, + } + + request = sentry_event.get("request", {}) + + if "httpMethod" in aws_event: + request["method"] = aws_event["httpMethod"] + + request["url"] = _get_url(aws_event, aws_context) + + if "queryStringParameters" in aws_event: + request["query_string"] = aws_event["queryStringParameters"] + + if "headers" in aws_event: + request["headers"] = _filter_headers(aws_event["headers"]) + + if should_send_default_pii(): + user_info = sentry_event.setdefault("user", {}) + + identity = aws_event.get("identity") + if identity is None: + identity = {} + + id = identity.get("userArn") + if id is not None: + user_info.setdefault("id", id) + + ip = identity.get("sourceIp") + if ip is not None: + user_info.setdefault("ip_address", ip) + + if "body" in aws_event: + request["data"] = aws_event.get("body", "") + else: + if aws_event.get("body", None): + # Unfortunately couldn't find a way to get structured body from AWS + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + sentry_event["request"] = deepcopy(request) + + return sentry_event + + return event_processor + + +def _get_url(aws_event: "Any", aws_context: "Any") -> str: + path = aws_event.get("path", None) + + headers = aws_event.get("headers") + if headers is None: + headers = {} + + host = headers.get("Host", None) + proto = headers.get("X-Forwarded-Proto", None) + if proto and host and path: + return "{}://{}{}".format(proto, host, path) + return "awslambda:///{}".format(aws_context.function_name) + + +def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: + """ + Generates a CloudWatchLogs console URL based on the context object + + Arguments: + aws_context {Any} -- context from lambda handler + + Returns: + str -- AWS Console URL to logs. + """ + formatstring = "%Y-%m-%dT%H:%M:%SZ" + region = environ.get("AWS_REGION", "") + + url = ( + "https://console.{domain}/cloudwatch/home?region={region}" + "#logEventViewer:group={log_group};stream={log_stream}" + ";start={start_time};end={end_time}" + ).format( + domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", + region=region, + log_group=aws_context.log_group_name, + log_stream=aws_context.log_stream_name, + start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), + end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( + formatstring + ), + ) + + return url + + +def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": + frames = [] + for frame in formatted_tb: + match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) + if match: + file_name, line_number, func_name = match.groups() + line_number = int(line_number) + frames.append( + { + "filename": file_name, + "function": func_name, + "lineno": line_number, + "vars": None, + "pre_context": None, + "context_line": None, + "post_context": None, + } + ) + return frames + + +def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": + """ + Converts the error JSON from AWS Lambda into a Sentry error event. + This is not a full fletched event, but better than nothing. + + This is an example of where AWS creates the error JSON: + https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 + """ + event: "Event" = { + "level": "error", + "exception": { + "values": [ + { + "type": error_json.get("errorType"), + "value": error_json.get("errorMessage"), + "stacktrace": { + "frames": _parse_formatted_traceback( + error_json.get("stackTrace", []) + ), + }, + "mechanism": { + "type": "aws_lambda", + "handled": False, + }, + } + ], + }, + } + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py new file mode 100644 index 0000000000..31f45f73de --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py @@ -0,0 +1,164 @@ +import sys +import types +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) + + +WRAPPED_FUNC = "_wrapped_{}_" +INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py +USED_FUNC = "_sentry_used_" + + +class BeamIntegration(Integration): + identifier = "beam" + + @staticmethod + def setup_once() -> None: + from apache_beam.transforms.core import DoFn, ParDo # type: ignore + + ignore_logger("root") + ignore_logger("bundle_processor.create") + + function_patches = ["process", "start_bundle", "finish_bundle", "setup"] + for func_name in function_patches: + setattr( + DoFn, + INSPECT_FUNC.format(func_name), + _wrap_inspect_call(DoFn, func_name), + ) + + old_init = ParDo.__init__ + + def sentry_init_pardo( + self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Do not monkey patch init twice + if not getattr(self, "_sentry_is_patched", False): + for func_name in function_patches: + if not hasattr(fn, func_name): + continue + wrapped_func = WRAPPED_FUNC.format(func_name) + + # Check to see if inspect is set and process is not + # to avoid monkey patching process twice. + # Check to see if function is part of object for + # backwards compatibility. + process_func = getattr(fn, func_name) + inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) + if not getattr(inspect_func, USED_FUNC, False) and not getattr( + process_func, USED_FUNC, False + ): + setattr(fn, wrapped_func, process_func) + setattr(fn, func_name, _wrap_task_call(process_func)) + + self._sentry_is_patched = True + old_init(self, fn, *args, **kwargs) + + ParDo.__init__ = sentry_init_pardo + + +def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": + if not hasattr(cls, func_name): + return None + + def _inspect(self: "Any") -> "Any": + """ + Inspect function overrides the way Beam gets argspec. + """ + wrapped_func = WRAPPED_FUNC.format(func_name) + if hasattr(self, wrapped_func): + process_func = getattr(self, wrapped_func) + else: + process_func = getattr(self, func_name) + setattr(self, func_name, _wrap_task_call(process_func)) + setattr(self, wrapped_func, process_func) + + # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults + # (which uses Signatures internally) should be used instead. + try: + from apache_beam.transforms.core import get_function_args_defaults + + return get_function_args_defaults(process_func) + except ImportError: + from apache_beam.typehints.decorators import getfullargspec # type: ignore + + return getfullargspec(process_func) + + setattr(_inspect, USED_FUNC, True) + return _inspect + + +def _wrap_task_call(func: "F") -> "F": + """ + Wrap task call with a try catch to get exceptions. + """ + + @wraps(func) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + try: + gen = func(*args, **kwargs) + except Exception: + raise_exception() + + if not isinstance(gen, types.GeneratorType): + return gen + return _wrap_generator_call(gen) + + setattr(_inner, USED_FUNC, True) + return _inner # type: ignore + + +@ensure_integration_enabled(BeamIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + """ + Send Beam exception to Sentry. + """ + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "beam", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def raise_exception() -> None: + """ + Raise an exception. + """ + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) + reraise(*exc_info) + + +def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": + """ + Wrap the generator to handle any failures. + """ + while True: + try: + yield next(gen) + except StopIteration: + break + except Exception: + raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py new file mode 100644 index 0000000000..a7fdd99b21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py @@ -0,0 +1,187 @@ +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_url, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Type, Union + + from botocore.model import ServiceId + +try: + from botocore import __version__ as BOTOCORE_VERSION + from botocore.awsrequest import AWSRequest + from botocore.client import BaseClient + from botocore.response import StreamingBody +except ImportError: + raise DidNotEnable("botocore is not installed") + + +class Boto3Integration(Integration): + identifier = "boto3" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTOCORE_VERSION) + _check_minimum_version(Boto3Integration, version, "botocore") + + orig_init = BaseClient.__init__ + + def sentry_patched_init( + self: "BaseClient", *args: "Any", **kwargs: "Any" + ) -> None: + orig_init(self, *args, **kwargs) + meta = self.meta + service_id = meta.service_model.service_id + meta.events.register( + "request-created", + partial(_sentry_request_created, service_id=service_id), + ) + meta.events.register("after-call", _sentry_after_call) + meta.events.register("after-call-error", _sentry_after_call_error) + + BaseClient.__init__ = sentry_patched_init # type: ignore + + +def _sentry_request_created( + service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" +) -> None: + description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) + + client = sentry_sdk.get_client() + if client.get_integration(Boto3Integration) is None: + return + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Boto3Integration.origin, + SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", + }, + ) + if request.url is not None and should_send_default_pii(): + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if request.method is not None: + span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=description, + origin=Boto3Integration.origin, + ) + + if request.url is not None: + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_data("aws.request.url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + span.set_tag("aws.service_id", service_id.hyphenize()) + span.set_tag("aws.operation_name", operation_name) + if request.method is not None: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + + # We do it in order for subsequent http calls/retries be + # attached to this span. + span.__enter__() + + # request.context is an open-ended data-structure + # where we can add anything useful in request life cycle. + request.context["_sentrysdk_span"] = span + + +def _sentry_after_call( + context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(None, None, None) + + body = parsed.get("Body") + if not isinstance(body, StreamingBody): + return + + streaming_span: "Union[Span, StreamedSpan]" + if isinstance(span, StreamedSpan): + streaming_span = sentry_sdk.traces.start_span( + name=span.name, + parent_span=span, + attributes={ + "sentry.op": OP.HTTP_CLIENT_STREAM, + "sentry.origin": Boto3Integration.origin, + }, + ) + else: + streaming_span = span.start_child( + op=OP.HTTP_CLIENT_STREAM, + name=span.description, + origin=Boto3Integration.origin, + ) + + orig_read = body.read + orig_close = body.close + + def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: + try: + ret = orig_read(*args, **kwargs) + if ret: + return ret + + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + return ret + except Exception: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + raise + + body.read = sentry_streaming_body_read # type: ignore + + def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + orig_close(*args, **kwargs) + + body.close = sentry_streaming_body_close # type: ignore + + +def _sentry_after_call_error( + context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py new file mode 100644 index 0000000000..50f6ca2e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py @@ -0,0 +1,239 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from collections.abc import Set + from typing import Any, Callable, Dict, Optional + + from bottle import FileUpload, FormsDict, LocalRequest # type: ignore + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + +try: + from bottle import ( + Bottle, + HTTPResponse, + Route, + ) + from bottle import ( + __version__ as BOTTLE_VERSION, + ) + from bottle import ( + request as bottle_request, + ) +except ImportError: + raise DidNotEnable("Bottle not installed") + + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class BottleIntegration(Integration): + identifier = "bottle" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTTLE_VERSION) + _check_minimum_version(BottleIntegration, version) + + old_app = Bottle.__call__ + + @ensure_integration_enabled(BottleIntegration, old_app) + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=BottleIntegration.origin, + ) + + return middleware(environ, start_response) + + Bottle.__call__ = sentry_patched_wsgi_app + + old_handle = Bottle._handle + + @functools.wraps(old_handle) + def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return old_handle(self, environ) + + scope = sentry_sdk.get_isolation_scope() + scope._name = "bottle" + scope.add_event_processor( + _make_request_event_processor(self, bottle_request, integration) + ) + res = old_handle(self, environ) + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + _set_segment_name_and_source( + transaction_style=integration.transaction_style + ) + + return res + + Bottle._handle = _patched_handle + + old_make_callback = Route._make_callback + + @functools.wraps(old_make_callback) + def patched_make_callback( + self: "Route", *args: object, **kwargs: object + ) -> "Any": + prepared_callback = old_make_callback(self, *args, **kwargs) + + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return prepared_callback + + def wrapped_callback(*args: object, **kwargs: object) -> "Any": + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) + + return res + + return wrapped_callback + + Route._make_callback = patched_make_callback + + +class BottleRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[str, str]": + return self.request.cookies + + def raw_data(self) -> bytes: + return self.request.body.read() + + def form(self) -> "FormsDict": + if self.is_json(): + return None + return self.request.forms.decode() + + def files(self) -> "Optional[Dict[str, str]]": + if self.is_json(): + return None + + return self.request.files + + def size_of_file(self, file: "FileUpload") -> int: + return file.content_length + + +def _set_segment_name_and_source(transaction_style: str) -> None: + try: + if transaction_style == "url": + name = bottle_request.route.rule or "bottle request" + else: + name = ( + bottle_request.route.name + or transaction_from_function(bottle_request.route.callback) + or "bottle request" + ) + + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], + ) + except RuntimeError: + pass + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "url": + try: + name = request.route.rule or "" + except RuntimeError: + pass + + elif transaction_style == "endpoint": + try: + name = ( + request.route.name + or transaction_from_function(request.route.callback) + or "" + ) + except RuntimeError: + pass + + event["transaction"] = name + event["transaction_info"] = { + "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + } + + +def _make_request_event_processor( + app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, request) + + with capture_internal_exceptions(): + BottleRequestExtractor(request).extract_into_event(event) + + return event + + return event_processor + + +def _capture_exception(exception: BaseException, handled: bool) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "bottle", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py new file mode 100644 index 0000000000..532b13539b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py @@ -0,0 +1,612 @@ +import sys +from collections.abc import Mapping +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import isolation_scope +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.celery.beat import ( + _patch_beat_apply_entry, + _patch_redbeat_apply_async, + _setup_celery_beat_signals, +) +from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource +from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, List, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import VERSION as CELERY_VERSION # type: ignore + from celery.app.task import Task # type: ignore + from celery.app.trace import task_has_custom + from celery.exceptions import ( # type: ignore + Ignore, + Reject, + Retry, + SoftTimeLimitExceeded, + ) + from kombu import Producer # type: ignore +except ImportError: + raise DidNotEnable("Celery not installed") + + +CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) + + +class CeleryIntegration(Integration): + identifier = "celery" + origin = f"auto.queue.{identifier}" + + def __init__( + self, + propagate_traces: bool = True, + monitor_beat_tasks: bool = False, + exclude_beat_tasks: "Optional[List[str]]" = None, + ) -> None: + self.propagate_traces = propagate_traces + self.monitor_beat_tasks = monitor_beat_tasks + self.exclude_beat_tasks = exclude_beat_tasks + + _patch_beat_apply_entry() + _patch_redbeat_apply_async() + _setup_celery_beat_signals(monitor_beat_tasks) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(CeleryIntegration, CELERY_VERSION) + + _patch_build_tracer() + _patch_task_apply_async() + _patch_celery_send_task() + _patch_worker_exit() + _patch_producer_publish() + + # This logger logs every status of every task that ran on the worker. + # Meaning that every task's breadcrumbs are full of stuff like "Task + # raised unexpected ". + ignore_logger("celery.worker.job") + ignore_logger("celery.app.trace") + + # This is stdout/err redirected to a logger, can't deal with this + # (need event_level=logging.WARN to reproduce) + ignore_logger("celery.redirected") + + +def _set_status(status: str) -> None: + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + + if span_streaming and scope.streamed_span is not None: + scope.streamed_span.status = "ok" if status == "ok" else "error" + elif not span_streaming and scope.span is not None: + scope.span.set_status(status) + + +def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return + + if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): + # ??? Doesn't map to anything + _set_status("aborted") + return + + _set_status("internal_error") + + if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): + return + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "celery", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + task: "Any", + uuid: "Any", + args: "Any", + kwargs: "Any", + request: "Optional[Any]" = None, +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["celery_task_id"] = uuid + extra = event.setdefault("extra", {}) + extra["celery-job"] = { + "task_name": task.name, + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + } + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): + event["fingerprint"] = [ + "celery", + "SoftTimeLimitExceeded", + getattr(task, "name", task), + ] + + return event + + return event_processor + + +def _update_celery_task_headers( + original_headers: "dict[str, Any]", + span: "Optional[Union[StreamedSpan, Span]]", + monitor_beat_tasks: bool, +) -> "dict[str, Any]": + """ + Updates the headers of the Celery task with the tracing information + and eventually Sentry Crons monitoring information for beat tasks. + """ + updated_headers = original_headers.copy() + with capture_internal_exceptions(): + # if span is None (when the task was started by Celery Beat) + # this will return the trace headers from the scope. + headers = dict( + sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) + ) + + if monitor_beat_tasks: + headers.update( + { + "sentry-monitor-start-timestamp-s": "%.9f" + % _now_seconds_since_epoch(), + } + ) + + # Add the time the task was enqueued to the headers + # This is used in the consumer to calculate the latency + updated_headers.update( + {"sentry-task-enqueued-time": _now_seconds_since_epoch()} + ) + + if headers: + existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) + sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) + + combined_baggage = sentry_baggage or existing_baggage + if sentry_baggage and existing_baggage: + # Merge incoming and sentry baggage, where the sentry trace information + # in the incoming baggage takes precedence and the third-party items + # are concatenated. + incoming = Baggage.from_incoming_header(existing_baggage) + combined = Baggage.from_incoming_header(sentry_baggage) + combined.sentry_items.update(incoming.sentry_items) + combined.third_party_items = ",".join( + [ + x + for x in [ + combined.third_party_items, + incoming.third_party_items, + ] + if x is not None and x != "" + ] + ) + combined_baggage = combined.serialize(include_third_party=True) + + updated_headers.update(headers) + if combined_baggage: + updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage + + # https://github.com/celery/celery/issues/4875 + # + # Need to setdefault the inner headers too since other + # tracing tools (dd-trace-py) also employ this exact + # workaround and we don't want to break them. + updated_headers.setdefault("headers", {}).update(headers) + if combined_baggage: + updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage + + # Add the Sentry options potentially added in `sentry_apply_entry` + # to the headers (done when auto-instrumenting Celery Beat tasks) + for key, value in updated_headers.items(): + if key.startswith("sentry-"): + updated_headers["headers"][key] = value + + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + for key, value in original_headers.items(): + if key != "headers" and key not in updated_headers["headers"]: + updated_headers["headers"][key] = value + + return updated_headers + + +class NoOpMgr: + def __enter__(self) -> None: + return None + + def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: + return None + + +def _wrap_task_run(f: "F") -> "F": + @wraps(f) + def apply_async(*args: "Any", **kwargs: "Any") -> "Any": + # Note: kwargs can contain headers=None, so no setdefault! + # Unsure which backend though. + client = sentry_sdk.get_client() + integration = client.get_integration(CeleryIntegration) + if integration is None: + return f(*args, **kwargs) + + kwarg_headers = kwargs.get("headers") or {} + propagate_traces = kwarg_headers.pop( + "sentry-propagate-traces", integration.propagate_traces + ) + + if not propagate_traces: + return f(*args, **kwargs) + + if isinstance(args[0], Task): + task_name: str = args[0].name + elif len(args) > 1 and isinstance(args[1], str): + task_name = args[1] + else: + task_name = "" + + span_streaming = has_span_streaming_enabled(client.options) + + task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" + + span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + if span_streaming: + if not task_started_from_beat and get_current_span() is not None: + span_mgr = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_CELERY, + "sentry.origin": CeleryIntegration.origin, + }, + ) + + else: + if not task_started_from_beat: + span_mgr = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_CELERY, + name=task_name, + origin=CeleryIntegration.origin, + ) + + with span_mgr as span: + kwargs["headers"] = _update_celery_task_headers( + kwarg_headers, span, integration.monitor_beat_tasks + ) + return f(*args, **kwargs) + + return apply_async # type: ignore + + +def _wrap_tracer(task: "Any", f: "F") -> "F": + # Need to wrap tracer for pushing the scope before prerun is sent, and + # popping it after postrun is sent. + # + # This is the reason we don't use signals for hooking in the first place. + # Also because in Celery 3, signal dispatch returns early if one handler + # crashes. + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + with isolation_scope() as scope: + scope._name = "celery" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) + + task_name = getattr(task, "name", "") + + custom_sampling_context = {} + with capture_internal_exceptions(): + custom_sampling_context = { + "celery_job": { + "task": task_name, + # for some reason, args[1] is a list if non-empty but a + # tuple if empty + "args": list(args[1]), + "kwargs": args[2], + } + } + + span: "Union[Span, StreamedSpan]" + span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + + # Celery task objects are not a thing to be trusted. Even + # something such as attribute access can fail. + with capture_internal_exceptions(): + headers = args[3].get("headers") or {} + if span_streaming: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(custom_sampling_context) + span = sentry_sdk.traces.start_span( + name=task_name, + parent_span=None, # make this a segment + attributes={ + "sentry.origin": CeleryIntegration.origin, + "sentry.span.source": TransactionSource.TASK.value, + "sentry.op": OP.QUEUE_TASK_CELERY, + }, + ) + + span_ctx = span + + else: + span = continue_trace( + headers, + op=OP.QUEUE_TASK_CELERY, + name=task_name, + source=TransactionSource.TASK, + origin=CeleryIntegration.origin, + ) + span.set_status(SPANSTATUS.OK) + + span_ctx = sentry_sdk.start_transaction( + span, + custom_sampling_context=custom_sampling_context, + ) + + with span_ctx: + return f(*args, **kwargs) + + return _inner # type: ignore + + +def _set_messaging_destination_name( + task: "Any", span: "Union[StreamedSpan, Span]" +) -> None: + """Set "messaging.destination.name" tag for span""" + with capture_internal_exceptions(): + delivery_info = task.request.delivery_info + if delivery_info: + routing_key = delivery_info.get("routing_key") + if delivery_info.get("exchange") == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning the tasks + # are sent to the queue with the same name as the routing key. + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + else: + span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + +def _wrap_task_call(task: "Any", f: "F") -> "F": + # Need to wrap task call because the exception is caught before we get to + # see it. Also celery's reported stacktrace is untrustworthy. + + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + try: + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_PROCESS, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PROCESS, + name=task.name, + origin=CeleryIntegration.origin, + ) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + _set_messaging_destination_name(task, span) + + latency = None + with capture_internal_exceptions(): + if ( + task.request.headers is not None + and "sentry-task-enqueued-time" in task.request.headers + ): + latency = _now_seconds_since_epoch() - task.request.headers.pop( + "sentry-task-enqueued-time" + ) + + if latency is not None: + latency *= 1000 # milliseconds + set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) + + with capture_internal_exceptions(): + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries + ) + + with capture_internal_exceptions(): + with task.app.connection() as conn: + set_on_span( + SPANDATA.MESSAGING_SYSTEM, + conn.transport.driver_type, + ) + + return f(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(task, exc_info) + reraise(*exc_info) + + return _inner # type: ignore + + +def _patch_build_tracer() -> None: + import celery.app.trace as trace # type: ignore + + original_build_tracer = trace.build_tracer + + def sentry_build_tracer( + name: "Any", task: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not getattr(task, "_sentry_is_patched", False): + # determine whether Celery will use __call__ or run and patch + # accordingly + if task_has_custom(task, "__call__"): + type(task).__call__ = _wrap_task_call(task, type(task).__call__) + else: + task.run = _wrap_task_call(task, task.run) + + # `build_tracer` is apparently called for every task + # invocation. Can't wrap every celery task for every invocation + # or we will get infinitely nested wrapper functions. + task._sentry_is_patched = True + + return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) + + trace.build_tracer = sentry_build_tracer + + +def _patch_task_apply_async() -> None: + Task.apply_async = _wrap_task_run(Task.apply_async) + + +def _patch_celery_send_task() -> None: + from celery import Celery + + Celery.send_task = _wrap_task_run(Celery.send_task) + + +def _patch_worker_exit() -> None: + # Need to flush queue before worker shutdown because a crashing worker will + # call os._exit + from billiard.pool import Worker # type: ignore + + original_workloop = Worker.workloop + + def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": + try: + return original_workloop(*args, **kwargs) + finally: + with capture_internal_exceptions(): + if ( + sentry_sdk.get_client().get_integration(CeleryIntegration) + is not None + ): + sentry_sdk.flush() + + Worker.workloop = sentry_workloop + + +def _patch_producer_publish() -> None: + original_publish = Producer.publish + + def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return original_publish(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + kwargs_headers = kwargs.get("headers", {}) + if not isinstance(kwargs_headers, Mapping): + # Ensure kwargs_headers is a Mapping, so we can safely call get(). + # We don't expect this to happen, but it's better to be safe. Even + # if it does happen, only our instrumentation breaks. This line + # does not overwrite kwargs["headers"], so the original publish + # method will still work. + kwargs_headers = {} + + task_name = kwargs_headers.get("task") or "" + task_id = kwargs_headers.get("id") + retries = kwargs_headers.get("retries") + + routing_key = kwargs.get("routing_key") + exchange = kwargs.get("exchange") + + span: "Union[StreamedSpan, Span, None]" = None + if span_streaming: + if get_current_span() is not None: + span = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_PUBLISH, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PUBLISH, + name=task_name, + origin=CeleryIntegration.origin, + ) + + if span is None: + return original_publish(self, *args, **kwargs) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + if task_id is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) + + if exchange == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning messages are + # routed to the queue with the same name as the routing key. + set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + if retries is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type + ) + + return original_publish(self, *args, **kwargs) + + Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py new file mode 100644 index 0000000000..b5027d212a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py @@ -0,0 +1,291 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.crons import MonitorStatus, capture_checkin +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.celery.utils import ( + _get_humanized_interval, + _now_seconds_since_epoch, +) +from sentry_sdk.utils import ( + logger, + match_regex_list, +) + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, TypeVar, Union + + from sentry_sdk._types import ( + MonitorConfig, + MonitorConfigScheduleType, + MonitorConfigScheduleUnit, + ) + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import Celery, Task # type: ignore + from celery.beat import Scheduler # type: ignore + from celery.schedules import crontab, schedule # type: ignore + from celery.signals import ( # type: ignore + task_failure, + task_retry, + task_success, + ) +except ImportError: + raise DidNotEnable("Celery not installed") + +try: + from redbeat.schedulers import RedBeatScheduler # type: ignore +except ImportError: + RedBeatScheduler = None + + +def _get_headers(task: "Task") -> "dict[str, Any]": + headers = task.request.get("headers") or {} + + # flatten nested headers + if "headers" in headers: + headers.update(headers["headers"]) + del headers["headers"] + + headers.update(task.request.get("properties") or {}) + + return headers + + +def _get_monitor_config( + celery_schedule: "Any", app: "Celery", monitor_name: str +) -> "MonitorConfig": + monitor_config: "MonitorConfig" = {} + schedule_type: "Optional[MonitorConfigScheduleType]" = None + schedule_value: "Optional[Union[str, int]]" = None + schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None + + if isinstance(celery_schedule, crontab): + schedule_type = "crontab" + schedule_value = ( + "{0._orig_minute} " + "{0._orig_hour} " + "{0._orig_day_of_month} " + "{0._orig_month_of_year} " + "{0._orig_day_of_week}".format(celery_schedule) + ) + elif isinstance(celery_schedule, schedule): + schedule_type = "interval" + (schedule_value, schedule_unit) = _get_humanized_interval( + celery_schedule.seconds + ) + + if schedule_unit == "second": + logger.warning( + "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", + monitor_name, + schedule_value, + ) + return {} + + else: + logger.warning( + "Celery schedule type '%s' not supported by Sentry Crons.", + type(celery_schedule), + ) + return {} + + monitor_config["schedule"] = {} + monitor_config["schedule"]["type"] = schedule_type + monitor_config["schedule"]["value"] = schedule_value + + if schedule_unit is not None: + monitor_config["schedule"]["unit"] = schedule_unit + + monitor_config["timezone"] = ( + ( + hasattr(celery_schedule, "tz") + and celery_schedule.tz is not None + and str(celery_schedule.tz) + ) + or app.timezone + or "UTC" + ) + + return monitor_config + + +def _apply_crons_data_to_schedule_entry( + scheduler: "Any", + schedule_entry: "Any", + integration: "sentry_sdk.integrations.celery.CeleryIntegration", +) -> None: + """ + Add Sentry Crons information to the schedule_entry headers. + """ + if not integration.monitor_beat_tasks: + return + + monitor_name = schedule_entry.name + + task_should_be_excluded = match_regex_list( + monitor_name, integration.exclude_beat_tasks + ) + if task_should_be_excluded: + return + + celery_schedule = schedule_entry.schedule + app = scheduler.app + + monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) + + is_supported_schedule = bool(monitor_config) + if not is_supported_schedule: + return + + headers = schedule_entry.options.pop("headers", {}) + headers.update( + { + "sentry-monitor-slug": monitor_name, + "sentry-monitor-config": monitor_config, + } + ) + + check_in_id = capture_checkin( + monitor_slug=monitor_name, + monitor_config=monitor_config, + status=MonitorStatus.IN_PROGRESS, + ) + headers.update({"sentry-monitor-check-in-id": check_in_id}) + + # Set the Sentry configuration in the options of the ScheduleEntry. + # Those will be picked up in `apply_async` and added to the headers. + schedule_entry.options["headers"] = headers + + +def _wrap_beat_scheduler( + original_function: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Makes sure that: + - a new Sentry trace is started for each task started by Celery Beat and + it is propagated to the task. + - the Sentry Crons information is set in the Celery Beat task's + headers so that is monitored with Sentry Crons. + + After the patched function is called, + Celery Beat will call apply_async to put the task in the queue. + """ + # Patch only once + # Can't use __name__ here, because some of our tests mock original_apply_entry + already_patched = "sentry_patched_scheduler" in str(original_function) + if already_patched: + return original_function + + from sentry_sdk.integrations.celery import CeleryIntegration + + def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(CeleryIntegration) + if integration is None: + return original_function(*args, **kwargs) + + # Tasks started by Celery Beat start a new Trace + scope = sentry_sdk.get_isolation_scope() + scope.set_new_propagation_context() + scope._name = "celery-beat" + + scheduler, schedule_entry = args + _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) + + return original_function(*args, **kwargs) + + return sentry_patched_scheduler + + +def _patch_beat_apply_entry() -> None: + Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) + + +def _patch_redbeat_apply_async() -> None: + if RedBeatScheduler is None: + return + + RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) + + +def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: + if monitor_beat_tasks: + task_success.connect(crons_task_success) + task_failure.connect(crons_task_failure) + task_retry.connect(crons_task_retry) + + +def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_success %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.OK, + ) + + +def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_failure %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) + + +def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_retry %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py new file mode 100644 index 0000000000..8d181f1f24 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py @@ -0,0 +1,32 @@ +import time +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from typing import Tuple + + from sentry_sdk._types import MonitorConfigScheduleUnit + + +def _now_seconds_since_epoch() -> float: + # We cannot use `time.perf_counter()` when dealing with the duration + # of a Celery task, because the start of a Celery task and + # the end are recorded in different processes. + # Start happens in the Celery Beat process, + # the end in a Celery Worker process. + return time.time() + + +def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": + TIME_UNITS = ( # noqa: N806 + ("day", 60 * 60 * 24.0), + ("hour", 60 * 60.0), + ("minute", 60.0), + ) + + seconds = float(seconds) + for unit, divider in TIME_UNITS: + if seconds >= divider: + interval = int(seconds / divider) + return (interval, cast("MonitorConfigScheduleUnit", unit)) + + return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py new file mode 100644 index 0000000000..9baa0e5cdd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py @@ -0,0 +1,188 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.aws_lambda import _make_request_event_processor +from sentry_sdk.traces import ( + SpanStatus, + StreamedSpan, + get_current_span, +) +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + parse_version, + reraise, +) + +try: + import chalice # type: ignore + from chalice import Chalice, ChaliceViewError + from chalice import __version__ as CHALICE_VERSION + from chalice.app import ( # type: ignore + EventSourceHandler as ChaliceEventSourceHandler, + ) +except ImportError: + raise DidNotEnable("Chalice is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) + + +class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore + def __call__(self, event: "Any", context: "Any") -> "Any": + client = sentry_sdk.get_client() + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor(event, context, configured_time) + ) + try: + return ChaliceEventSourceHandler.__call__(self, event, context) + except Exception: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + client.flush() + reraise(*exc_info) + + +def _get_view_function_response( + app: "Any", view_function: "F", function_args: "Any" +) -> "F": + @wraps(view_function) + def wrapped_view_function(**function_args: "Any") -> "Any": + client = sentry_sdk.get_client() + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = app.lambda_context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor( + app.current_request.to_dict(), + app.lambda_context, + configured_time, + ) + ) + + if has_span_streaming_enabled(client.options): + current_span = get_current_span() + segment = None + if type(current_span) is StreamedSpan: + # A segment already exists (created by the AWS Lambda + # integration), so decorate it with Chalice attributes + # The AWS Lambda integration owns the span lifecycle + # (end + flush), but Chalice converts unhandled view exceptions + # into 500 responses, so the error must be captured here. + request_dict = app.current_request.to_dict() + headers = request_dict.get("headers", {}) + + header_attrs: "Dict[str, Any]" = {} + for header, value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attrs[f"http.request.header.{header.lower()}"] = value + + additional_attrs: "Dict[str, Any]" = {} + if "method" in request_dict: + additional_attrs["http.request.method"] = request_dict["method"] + + attributes = { + "sentry.origin": ChaliceIntegration.origin, + **header_attrs, + **additional_attrs, + } + + segment = current_span._segment + segment.set_attributes(attributes) + + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + if segment: + segment.status = SpanStatus.ERROR.value + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + if segment is None: + client.flush() + raise + else: + scope.set_transaction_name( + app.lambda_context.function_name, + source=TransactionSource.COMPONENT, + ) + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + client.flush() + raise + + return wrapped_view_function # type: ignore + + +class ChaliceIntegration(Integration): + identifier = "chalice" + origin = f"auto.function.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(CHALICE_VERSION) + + if version is None: + raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) + + if version < (1, 20): + old_get_view_function_response = Chalice._get_view_function_response + else: + from chalice.app import RestAPIEventHandler + + old_get_view_function_response = ( + RestAPIEventHandler._get_view_function_response + ) + + def sentry_event_response( + app: "Any", view_function: "F", function_args: "Dict[str, Any]" + ) -> "Any": + wrapped_view_function = _get_view_function_response( + app, view_function, function_args + ) + + return old_get_view_function_response( + app, wrapped_view_function, function_args + ) + + if version < (1, 20): + Chalice._get_view_function_response = sentry_event_response + else: + RestAPIEventHandler._get_view_function_response = sentry_event_response + # for everything else (like events) + chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py new file mode 100644 index 0000000000..e6b3009548 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py @@ -0,0 +1,211 @@ +import functools +from typing import TYPE_CHECKING, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import Any, Callable, ParamSpec, Union +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + + +try: + from clickhouse_driver import VERSION # type: ignore[import-not-found] + from clickhouse_driver.client import Client # type: ignore[import-not-found] + from clickhouse_driver.connection import ( # type: ignore[import-not-found] + Connection, + ) + +except ImportError: + raise DidNotEnable("clickhouse-driver not installed.") + + +class ClickhouseDriverIntegration(Integration): + identifier = "clickhouse_driver" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(ClickhouseDriverIntegration, VERSION) + + # Every query is done using the Connection's `send_query` function + Connection.send_query = _wrap_start(Connection.send_query) + + # If the query contains parameters then the send_data function is used to send those parameters to clickhouse + _wrap_send_data() + + # Every query ends either with the Client's `receive_end_of_query` (no result expected) + # or its `receive_result` (result expected) + Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) + if hasattr(Client, "receive_end_of_insert_query"): + # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` + Client.receive_end_of_insert_query = _wrap_end( + Client.receive_end_of_insert_query + ) + Client.receive_result = _wrap_end(Client.receive_result) + + +P = ParamSpec("P") +T = TypeVar("T") + + +def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": + @functools.wraps(f) + def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(ClickhouseDriverIntegration) is None: + return f(*args, **kwargs) + + connection = args[0] + query = args[1] + query_id = args[2] if len(args) > 2 else kwargs.get("query_id") + params = args[3] if len(args) > 3 else kwargs.get("params") + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=query, # type: ignore + attributes={ + "sentry.op": OP.DB, + "sentry.origin": ClickhouseDriverIntegration.origin, + SPANDATA.DB_QUERY_TEXT: str(query), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=ClickhouseDriverIntegration.origin, + ) + + span.set_data("query", query) + + if query_id: + span.set_data("db.query_id", query_id) + + if params and should_send_default_pii(): + span.set_data("db.params", params) + + connection._sentry_span = span # type: ignore[attr-defined] + + _set_db_data(span, connection) + + # run the original code + ret = f(*args, **kwargs) + + return ret + + return _inner + + +def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": + def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": + res = f(*args, **kwargs) + instance = args[0] + span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] + + if span is None: + return res + + if isinstance(span, StreamedSpan): + span.end() + else: + if res is not None and should_send_default_pii(): + span.set_data("db.result", res) + + with capture_internal_exceptions(): + span.scope.add_breadcrumb( + message=span._data.pop("query"), category="query", data=span._data + ) + + span.finish() + + return res + + return _inner_end + + +def _wrap_send_data() -> None: + original_send_data = Client.send_data + + def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data + self, sample_block, data, types_check=False, columnar=False, *args, **kwargs + ): + span = getattr(self.connection, "_sentry_span", None) + + if isinstance(span, StreamedSpan): + _set_db_data(span, self.connection) + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + if span is not None: + _set_db_data(span, self.connection) + + if should_send_default_pii(): + db_params = span._data.get("db.params", []) + + if isinstance(data, (list, tuple)): + db_params.extend(data) + + else: # data is a generic iterator + orig_data = data + + # Wrap the generator to add items to db.params as they are yielded. + # This allows us to send the params to Sentry without needing to allocate + # memory for the entire generator at once. + def wrapped_generator() -> "Iterator[Any]": + for item in orig_data: + db_params.append(item) + yield item + + # Replace the original iterator with the wrapped one. + data = wrapped_generator() + + span.set_data("db.params", db_params) + + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + Client.send_data = _inner_send_data + + +def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") + span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) + + set_on_span = span.set_attribute + else: + span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") + span.set_data(SPANDATA.DB_NAME, connection.database) + + set_on_span = span.set_data + + set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") + set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) + set_on_span(SPANDATA.SERVER_PORT, connection.port) + set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py new file mode 100644 index 0000000000..f6285d0a9b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py @@ -0,0 +1,273 @@ +import json +from typing import TYPE_CHECKING + +import urllib3 + +from sentry_sdk.api import set_context +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Dict + + +CONTEXT_TYPE = "cloud_resource" + +HTTP_TIMEOUT = 2.0 + +AWS_METADATA_HOST = "169.254.169.254" +AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) +AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( + AWS_METADATA_HOST +) + +GCP_METADATA_HOST = "metadata.google.internal" +GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( + GCP_METADATA_HOST +) + + +class CLOUD_PROVIDER: # noqa: N801 + """ + Name of the cloud provider. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + ALIBABA = "alibaba_cloud" + AWS = "aws" + AZURE = "azure" + GCP = "gcp" + IBM = "ibm_cloud" + TENCENT = "tencent_cloud" + + +class CLOUD_PLATFORM: # noqa: N801 + """ + The cloud platform. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + AWS_EC2 = "aws_ec2" + AWS_LAMBDA = "aws_lambda" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" + + +class CloudResourceContextIntegration(Integration): + """ + Adds cloud resource context to the Senty scope + """ + + identifier = "cloudresourcecontext" + + cloud_provider = "" + + aws_token = "" + http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) + + gcp_metadata = None + + def __init__(self, cloud_provider: str = "") -> None: + CloudResourceContextIntegration.cloud_provider = cloud_provider + + @classmethod + def _is_aws(cls) -> bool: + try: + r = cls.http.request( + "PUT", + AWS_TOKEN_URL, + headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, + ) + + if r.status != 200: + return False + + cls.aws_token = r.data.decode() + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking AWS metadata service: %s", str(e)) + return False + + @classmethod + def _get_aws_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.AWS, + "cloud.platform": CLOUD_PLATFORM.AWS_EC2, + } + + try: + r = cls.http.request( + "GET", + AWS_METADATA_URL, + headers={"X-aws-ec2-metadata-token": cls.aws_token}, + ) + + if r.status != 200: + return ctx + + data = json.loads(r.data.decode("utf-8")) + + try: + ctx["cloud.account.id"] = data["accountId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = data["availabilityZone"] + except Exception: + pass + + try: + ctx["cloud.region"] = data["region"] + except Exception: + pass + + try: + ctx["host.id"] = data["instanceId"] + except Exception: + pass + + try: + ctx["host.type"] = data["instanceType"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching AWS metadata: %s", str(e)) + + return ctx + + @classmethod + def _is_gcp(cls) -> bool: + try: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return False + + cls.gcp_metadata = json.loads(r.data.decode("utf-8")) + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking GCP metadata service: %s", str(e)) + return False + + @classmethod + def _get_gcp_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.GCP, + "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, + } + + gcp_metadata = cls.gcp_metadata + try: + if cls.gcp_metadata is None: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return ctx + + gcp_metadata = json.loads(r.data.decode("utf-8")) + cls.gcp_metadata = gcp_metadata + + try: + ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( + "/" + )[-1] + except Exception: + pass + + try: + # only populated in google cloud run + ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] + except Exception: + pass + + try: + ctx["host.id"] = gcp_metadata["instance"]["id"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching GCP metadata: %s", str(e)) + + return ctx + + @classmethod + def _get_cloud_provider(cls) -> str: + if cls._is_aws(): + return CLOUD_PROVIDER.AWS + + if cls._is_gcp(): + return CLOUD_PROVIDER.GCP + + return "" + + @classmethod + def _get_cloud_resource_context(cls) -> "Dict[str, str]": + cloud_provider = ( + cls.cloud_provider + if cls.cloud_provider != "" + else CloudResourceContextIntegration._get_cloud_provider() + ) + if cloud_provider in context_getters.keys(): + return context_getters[cloud_provider]() + + return {} + + @staticmethod + def setup_once() -> None: + cloud_provider = CloudResourceContextIntegration.cloud_provider + unsupported_cloud_provider = ( + cloud_provider != "" and cloud_provider not in context_getters.keys() + ) + + if unsupported_cloud_provider: + logger.warning( + "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", + CloudResourceContextIntegration.cloud_provider, + list(context_getters.keys()), + ) + + context = CloudResourceContextIntegration._get_cloud_resource_context() + if context != {}: + set_context(CONTEXT_TYPE, context) + + +# Map with the currently supported cloud providers +# mapping to functions extracting the context +context_getters = { + CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, + CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, +} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py new file mode 100644 index 0000000000..7abf3f6808 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py @@ -0,0 +1,303 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise + +try: + from cohere import ( + ChatStreamEndEvent, + NonStreamedChatResponse, + ) + from cohere.base_client import BaseCohere + from cohere.client import Client + + if TYPE_CHECKING: + from cohere import StreamedChatResponse +except ImportError: + raise DidNotEnable("Cohere not installed") + +try: + # cohere 5.9.3+ + from cohere import StreamEndStreamedChatResponse +except ImportError: + from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse + + +COLLECTED_CHAT_PARAMS = { + "model": SPANDATA.AI_MODEL_ID, + "k": SPANDATA.AI_TOP_K, + "p": SPANDATA.AI_TOP_P, + "seed": SPANDATA.AI_SEED, + "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, + "raw_prompting": SPANDATA.AI_RAW_PROMPTING, +} + +COLLECTED_PII_CHAT_PARAMS = { + "tools": SPANDATA.AI_TOOLS, + "preamble": SPANDATA.AI_PREAMBLE, +} + +COLLECTED_CHAT_RESP_ATTRS = { + "generation_id": SPANDATA.AI_GENERATION_ID, + "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, + "finish_reason": SPANDATA.AI_FINISH_REASON, +} + +COLLECTED_PII_CHAT_RESP_ATTRS = { + "citations": SPANDATA.AI_CITATIONS, + "documents": SPANDATA.AI_DOCUMENTS, + "search_queries": SPANDATA.AI_SEARCH_QUERIES, + "search_results": SPANDATA.AI_SEARCH_RESULTS, + "tool_calls": SPANDATA.AI_TOOL_CALLS, +} + + +class CohereIntegration(Integration): + identifier = "cohere" + origin = f"auto.ai.{identifier}" + + def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) + Client.embed = _wrap_embed(Client.embed) + BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "cohere", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _end_span(span: "Any") -> None: + if isinstance(span, StreamedSpan): + span.end() + else: + span.__exit__(None, None, None) + + +def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": + def collect_chat_response_fields( + span: "Union[Span, StreamedSpan]", + res: "NonStreamedChatResponse", + include_pii: bool, + ) -> None: + if include_pii: + if hasattr(res, "text"): + set_data_normalized( + span, + SPANDATA.AI_RESPONSES, + [res.text], + ) + for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: + if hasattr(res, pii_attr): + set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) + + for attr in COLLECTED_CHAT_RESP_ATTRS: + if hasattr(res, attr): + set_data_normalized(span, "ai." + attr, getattr(res, attr)) + + if hasattr(res, "meta"): + if hasattr(res.meta, "billed_units"): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + output_tokens=res.meta.billed_units.output_tokens, + ) + elif hasattr(res.meta, "tokens"): + record_token_usage( + span, + input_tokens=res.meta.tokens.input_tokens, + output_tokens=res.meta.tokens.output_tokens, + ) + + if hasattr(res.meta, "warnings"): + set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) + + @wraps(f) + def new_chat(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if ( + integration is None + or "message" not in kwargs + or not isinstance(kwargs.get("message"), str) + ): + return f(*args, **kwargs) + + message = kwargs.get("message") + + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name="cohere.client.Chat", + attributes={ + "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + name="cohere.client.Chat", + origin=CohereIntegration.origin, + ) + span.__enter__() + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + + reraise(*exc_info) + + with capture_internal_exceptions(): + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, + SPANDATA.AI_INPUT_MESSAGES, + list( + map( + lambda x: { + "role": getattr(x, "role", "").lower(), + "content": getattr(x, "message", ""), + }, + kwargs.get("chat_history", []), + ) + ) + + [{"role": "user", "content": message}], + ) + for k, v in COLLECTED_PII_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + + for k, v in COLLECTED_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + set_data_normalized(span, SPANDATA.AI_STREAMING, False) + + if streaming: + old_iterator = res + + def new_iterator() -> "Iterator[StreamedChatResponse]": + with capture_internal_exceptions(): + for x in old_iterator: + if isinstance(x, ChatStreamEndEvent) or isinstance( + x, StreamEndStreamedChatResponse + ): + collect_chat_response_fields( + span, + x.response, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + yield x + _end_span(span) + + return new_iterator() + elif isinstance(res, NonStreamedChatResponse): + collect_chat_response_fields( + span, + res, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + _end_span(span) + else: + set_data_normalized(span, "unknown_response", True) + _end_span(span) + return res + + return new_chat + + +def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + if integration is None: + return f(*args, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name="Cohere Embedding Creation", + attributes={ + "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span_ctx = get_start_span_function()( + op=consts.OP.COHERE_EMBEDDINGS_CREATE, + name="Cohere Embedding Creation", + origin=CohereIntegration.origin, + ) + + with span_ctx as span: + if "texts" in kwargs and ( + should_send_default_pii() and integration.include_prompts + ): + if isinstance(kwargs["texts"], str): + set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) + elif ( + isinstance(kwargs["texts"], list) + and len(kwargs["texts"]) > 0 + and isinstance(kwargs["texts"][0], str) + ): + set_data_normalized( + span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] + ) + + if "model" in kwargs: + set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) + if ( + hasattr(res, "meta") + and hasattr(res.meta, "billed_units") + and hasattr(res.meta.billed_units, "input_tokens") + ): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + total_tokens=res.meta.billed_units.input_tokens, + ) + return res + + return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py new file mode 100644 index 0000000000..a0e9014666 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py @@ -0,0 +1,62 @@ +import weakref +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import ContextVar, logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class DedupeIntegration(Integration): + identifier = "dedupe" + + def __init__(self) -> None: + self._last_seen = ContextVar("last-seen") + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if hint is None: + return event + + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return event + + exc_info = hint.get("exc_info", None) + if exc_info is None: + return event + + last_seen = integration._last_seen.get(None) + if last_seen is not None: + # last_seen is either a weakref or the original instance + last_seen = ( + last_seen() if isinstance(last_seen, weakref.ref) else last_seen + ) + + exc = exc_info[1] + if last_seen is exc: + logger.info("DedupeIntegration dropped duplicated error event %s", exc) + return None + + # we can only weakref non builtin types + try: + integration._last_seen.set(weakref.ref(exc)) + except TypeError: + integration._last_seen.set(exc) + + return event + + @staticmethod + def reset_last_seen() -> None: + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return + + integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py new file mode 100644 index 0000000000..361b60079d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py @@ -0,0 +1,884 @@ +import inspect +import sys +import threading +import weakref +from importlib import import_module + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANNAME +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + transaction_from_function, + walk_exception_chain, +) + +try: + from django import VERSION as DJANGO_VERSION + from django.conf import settings + from django.conf import settings as django_settings + from django.core import signals + from django.utils.functional import SimpleLazyObject + + try: + from django.urls import resolve + except ImportError: + from django.core.urlresolvers import resolve + + try: + from django.urls import Resolver404 + except ImportError: + from django.core.urlresolvers import Resolver404 + + # Only available in Django 3.0+ + try: + from django.core.handlers.asgi import ASGIRequest + except Exception: + ASGIRequest = None + +except ImportError: + raise DidNotEnable("Django not installed") + +from sentry_sdk.integrations.django.middleware import patch_django_middlewares +from sentry_sdk.integrations.django.signals_handlers import patch_signals +from sentry_sdk.integrations.django.tasks import patch_tasks +from sentry_sdk.integrations.django.templates import ( + get_template_frame_from_exception, + patch_templates, +) +from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER +from sentry_sdk.integrations.django.views import patch_views + +if DJANGO_VERSION[:2] > (1, 8): + from sentry_sdk.integrations.django.caching import patch_caching +else: + patch_caching = None # type: ignore + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from django.core.handlers.wsgi import WSGIRequest + from django.http.request import QueryDict + from django.http.response import HttpResponse + from django.utils.datastructures import MultiValueDict + + from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +if DJANGO_VERSION < (1, 10): + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated() + +else: + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated + + +TRANSACTION_STYLE_VALUES = ("function_name", "url") + + +class DjangoIntegration(Integration): + """ + Auto instrument a Django application. + + :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. + :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. + :param signals_spans: Whether to create spans for signals. Defaults to `True`. + :param signals_denylist: A list of signals to ignore when creating spans. + :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. + """ + + identifier = "django" + origin = f"auto.http.{identifier}" + origin_db = f"auto.db.{identifier}" + + transaction_style = "" + middleware_spans: "Optional[bool]" = None + signals_spans: "Optional[bool]" = None + cache_spans: "Optional[bool]" = None + signals_denylist: "list[signals.Signal]" = [] + + def __init__( + self, + transaction_style: str = "url", + middleware_spans: bool = False, + signals_spans: bool = True, + cache_spans: bool = False, + db_transaction_spans: bool = False, + signals_denylist: "Optional[list[signals.Signal]]" = None, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + + self.signals_spans = signals_spans + self.signals_denylist = signals_denylist or [] + + self.cache_spans = cache_spans + self.db_transaction_spans = db_transaction_spans + + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(DjangoIntegration, DJANGO_VERSION) + + install_sql_hook() + # Patch in our custom middleware. + + # logs an error for every 500 + ignore_logger("django.server") + ignore_logger("django.request") + + from django.core.handlers.wsgi import WSGIHandler + + old_app = WSGIHandler.__call__ + + @ensure_integration_enabled(DjangoIntegration, old_app) + def sentry_patched_wsgi_handler( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + bound_old_app = old_app.__get__(self, WSGIHandler) + + from django.conf import settings + + use_x_forwarded_for = settings.USE_X_FORWARDED_HOST + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + middleware = SentryWsgiMiddleware( + bound_old_app, + use_x_forwarded_for, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + WSGIHandler.__call__ = sentry_patched_wsgi_handler + + _patch_get_response() + + _patch_django_asgi_handler() + + signals.got_request_exception.connect(_got_request_exception) + + @add_global_event_processor + def process_django_templates( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_, exc_value, _) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + frame = get_template_frame_from_exception(exc_value) + if frame is not None: + frames = exception.get("stacktrace", {}).get("frames", []) + + for i in reversed(range(len(frames))): + f = frames[i] + if ( + f.get("function") in ("Parser.parse", "parse", "render") + and f.get("module") == "django.template.base" + ): + i += 1 + break + else: + i = len(frames) + + frames.insert(i, frame) + + return event + + @add_global_repr_processor + def _django_queryset_repr( + value: "Any", hint: "Dict[str, Any]" + ) -> "Union[NotImplementedType, str]": + try: + # Django 1.6 can fail to import `QuerySet` when Django settings + # have not yet been initialized. + # + # If we fail to import, return `NotImplemented`. It's at least + # unlikely that we have a query set in `value` when importing + # `QuerySet` fails. + from django.db.models.query import QuerySet + except Exception: + return NotImplemented + + if not isinstance(value, QuerySet) or value._result_cache: + return NotImplemented + + return "<%s from %s at 0x%x>" % ( + value.__class__.__name__, + value.__module__, + id(value), + ) + + _patch_channels() + patch_django_middlewares() + patch_views() + patch_templates() + patch_signals() + patch_tasks() + add_template_context_repr_sequence() + + if patch_caching is not None: + patch_caching() + + +_DRF_PATCHED = False +_DRF_PATCH_LOCK = threading.Lock() + + +def _patch_drf() -> None: + """ + Patch Django Rest Framework for more/better request data. DRF's request + type is a wrapper around Django's request type. The attribute we're + interested in is `request.data`, which is a cached property containing a + parsed request body. Reading a request body from that property is more + reliable than reading from any of Django's own properties, as those don't + hold payloads in memory and therefore can only be accessed once. + + We patch the Django request object to include a weak backreference to the + DRF request object, such that we can later use either in + `DjangoRequestExtractor`. + + This function is not called directly on SDK setup, because importing almost + any part of Django Rest Framework will try to access Django settings (where + `sentry_sdk.init()` might be called from in the first place). Instead we + run this function on every request and do the patching on the first + request. + """ + + global _DRF_PATCHED + + if _DRF_PATCHED: + # Double-checked locking + return + + with _DRF_PATCH_LOCK: + if _DRF_PATCHED: + return + + # We set this regardless of whether the code below succeeds or fails. + # There is no point in trying to patch again on the next request. + _DRF_PATCHED = True + + with capture_internal_exceptions(): + try: + from rest_framework.views import APIView # type: ignore + except ImportError: + pass + else: + old_drf_initial = APIView.initial + + def sentry_patched_drf_initial( + self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + with capture_internal_exceptions(): + request._request._sentry_drf_request_backref = weakref.ref( + request + ) + pass + return old_drf_initial(self, request, *args, **kwargs) + + APIView.initial = sentry_patched_drf_initial + + +def _patch_channels() -> None: + try: + from channels.http import AsgiHandler # type: ignore + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because channels may not be used at all in + # the current process. That is the case when running traditional WSGI + # workers in gunicorn+gevent and the websocket stuff in a separate + # process. + logger.warning( + "We detected that you are using Django channels 2.0." + + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl + + patch_channels_asgi_handler_impl(AsgiHandler) + + +def _patch_django_asgi_handler() -> None: + try: + from django.core.handlers.asgi import ASGIHandler + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because Django's ASGI stuff may not be used + # at all. + logger.warning( + "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl + + patch_django_asgi_handler_impl(ASGIHandler) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" +) -> None: + try: + transaction_name = None + if transaction_style == "function_name": + fn = resolve(request.path).func + transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) + + elif transaction_style == "url": + if hasattr(request, "urlconf"): + transaction_name = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + else: + transaction_name = LEGACY_RESOLVER.resolve(request.path_info) + + if transaction_name is None: + transaction_name = request.path_info + source = TransactionSource.URL + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name( + transaction_name, + source=source, + ) + except Resolver404: + urlconf = import_module(settings.ROOT_URLCONF) + # This exception only gets thrown when transaction_style is `function_name` + # So we don't check here what style is configured + if hasattr(urlconf, "handler404"): + handler = urlconf.handler404 + if isinstance(handler, str): + scope.transaction = handler + else: + scope.transaction = transaction_from_function( + getattr(handler, "view_class", handler) + ) + except Exception: + pass + + +def _before_get_response(request: "WSGIRequest") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return + + _patch_drf() + + scope = sentry_sdk.get_current_scope() + # Rely on WSGI middleware to start a trace + _set_transaction_name_and_source(scope, integration.transaction_style, request) + + scope.add_event_processor( + _make_wsgi_request_event_processor(weakref.ref(request), integration) + ) + + +def _attempt_resolve_again( + request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str +) -> None: + """ + Some django middlewares overwrite request.urlconf + so we need to respect that contract, + so we try to resolve the url again. + """ + if not hasattr(request, "urlconf"): + return + + _set_transaction_name_and_source(scope, transaction_style, request) + + +def _after_get_response(request: "WSGIRequest") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming and should_send_default_pii(): + user = getattr(request, "user", None) + + # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. + # Exit early if the user has not been materialized yet. + is_lazy = isinstance(user, SimpleLazyObject) + if is_lazy and hasattr(request, "_cached_user"): + user = request._cached_user + elif is_lazy: + return + + if user is None or not is_authenticated(user): + return + + user_info = {} + try: + user_info["id"] = str(user.pk) + except Exception: + pass + + try: + user_info["email"] = user.email + except Exception: + pass + + try: + user_info["username"] = user.get_username() + except Exception: + pass + + sentry_sdk.set_user(user_info) + + +def _patch_get_response() -> None: + """ + patch get_response, because at that point we have the Django request object + """ + from django.core.handlers.base import BaseHandler + + old_get_response = BaseHandler.get_response + + def sentry_patched_get_response( + self: "Any", request: "WSGIRequest" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + rv = old_get_response(self, request) + _after_get_response(request) + return rv + + BaseHandler.get_response = sentry_patched_get_response + + if hasattr(BaseHandler, "get_response_async"): + from sentry_sdk.integrations.django.asgi import patch_get_response_async + + patch_get_response_async(BaseHandler, _before_get_response) + + +def _make_wsgi_request_event_processor( + weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" +) -> "EventProcessor": + def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + request = weak_request() + if request is None: + return event + + django_3 = ASGIRequest is not None + if django_3 and type(request) == ASGIRequest: + # We have a `asgi_request_event_processor` for this. + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return wsgi_request_event_processor + + +def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if request is not None and integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + event, hint = event_from_exception( + sys.exc_info(), + client_options=client.options, + mechanism={"type": "django", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class DjangoRequestExtractor(RequestExtractor): + def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: + try: + drf_request = request._sentry_drf_request_backref() + if drf_request is not None: + request = drf_request + except AttributeError: + pass + self.request = request + + def env(self) -> "Dict[str, str]": + return self.request.META + + def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": + privacy_cookies = [ + django_settings.CSRF_COOKIE_NAME, + django_settings.SESSION_COOKIE_NAME, + ] + + clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} + for key, val in self.request.COOKIES.items(): + if key in privacy_cookies: + clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE + else: + clean_cookies[key] = val + + return clean_cookies + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "QueryDict": + return self.request.POST + + def files(self) -> "MultiValueDict": + return self.request.FILES + + def size_of_file(self, file: "Any") -> int: + return file.size + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + return self.request.data + except Exception: + return RequestExtractor.parsed_body(self) + + +def _set_user_info(request: "WSGIRequest", event: "Event") -> None: + user_info = event.setdefault("user", {}) + + user = getattr(request, "user", None) + + if user is None or not is_authenticated(user): + return + + try: + user_info.setdefault("id", str(user.pk)) + except Exception: + pass + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.get_username()) + except Exception: + pass + + +def install_sql_hook() -> None: + """If installed this causes Django's queries to be captured.""" + try: + from django.db.backends.utils import CursorWrapper + except ImportError: + from django.db.backends.util import CursorWrapper + + try: + # django 1.6 and 1.7 compatability + from django.db.backends import BaseDatabaseWrapper + except ImportError: + # django 1.8 or later + from django.db.backends.base.base import BaseDatabaseWrapper + + try: + real_execute = CursorWrapper.execute + real_executemany = CursorWrapper.executemany + real_connect = BaseDatabaseWrapper.connect + real_commit = BaseDatabaseWrapper._commit + real_rollback = BaseDatabaseWrapper._rollback + except AttributeError: + # This won't work on Django versions < 1.6 + return + + @ensure_integration_enabled(DjangoIntegration, real_execute) + def execute( + self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=params, + paramstyle="format", + executemany=False, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + result = real_execute(self, sql, params) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_executemany) + def executemany( + self: "CursorWrapper", sql: "Any", param_list: "List[Any]" + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=param_list, + paramstyle="format", + executemany=True, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + + result = real_executemany(self, sql, param_list) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_connect) + def connect(self: "BaseDatabaseWrapper") -> None: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message="connect", category="query") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="connect", + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self) + return real_connect(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + return real_connect(self) + + def _commit(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_commit(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_COMMIT, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_COMMIT, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + + def _rollback(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_rollback(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_ROLLBACK, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_ROLLBACK, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + + CursorWrapper.execute = execute + CursorWrapper.executemany = executemany + BaseDatabaseWrapper.connect = connect + BaseDatabaseWrapper._commit = _commit + BaseDatabaseWrapper._rollback = _rollback + ignore_logger("django.db.backends") + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", + cursor_or_db: "Any", + db_operation: "Optional[str]" = None, +) -> None: + db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db + vendor = db.vendor + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) + + if db_operation is not None: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) + else: + span.set_data(SPANDATA.DB_SYSTEM, vendor) + + if db_operation is not None: + span.set_data(SPANDATA.DB_OPERATION, db_operation) + + # Some custom backends override `__getattr__`, making it look like `cursor_or_db` + # actually has a `connection` and the `connection` has a `get_dsn_parameters` + # attribute, only to throw an error once you actually want to call it. + # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable + # function. + is_psycopg2 = ( + hasattr(cursor_or_db, "connection") + and hasattr(cursor_or_db.connection, "get_dsn_parameters") + and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + ) + if is_psycopg2: + connection_params = cursor_or_db.connection.get_dsn_parameters() + else: + try: + # psycopg3, only extract needed params as get_parameters + # can be slow because of the additional logic to filter out default + # values + connection_params = { + "dbname": cursor_or_db.connection.info.dbname, + "port": cursor_or_db.connection.info.port, + } + # PGhost returns host or base dir of UNIX socket as an absolute path + # starting with /, use it only when it contains host + pg_host = cursor_or_db.connection.info.host + if pg_host and not pg_host.startswith("/"): + connection_params["host"] = pg_host + except Exception: + connection_params = db.get_connection_params() + + db_name = connection_params.get("dbname") or connection_params.get("database") + + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + + set_on_span = span.set_attribute + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + set_on_span = span.set_data + + server_address = connection_params.get("host") + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = connection_params.get("port") + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, str(server_port)) + + server_socket_address = connection_params.get("unix_socket") + if server_socket_address is not None: + set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) + + +def add_template_context_repr_sequence() -> None: + try: + from django.template.context import BaseContext + + add_repr_sequence_type(BaseContext) + except Exception: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py new file mode 100644 index 0000000000..43faffb5be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py @@ -0,0 +1,262 @@ +""" +Instrumentation for Django 3.0 + +Since this file contains `async def` it is conditionally imported in +`sentry_sdk.integrations.django` (depending on the existence of +`django.core.handlers.asgi`. +""" + +import asyncio +import functools +import inspect +from typing import TYPE_CHECKING + +from django.core.handlers.wsgi import WSGIRequest + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar, Union + + from django.core.handlers.asgi import ASGIRequest + from django.http.response import HttpResponse + + from sentry_sdk._types import Event, EventProcessor + + _F = TypeVar("_F", bound=Callable[..., Any]) + + +# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for +# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. +# The latter is replaced with the inspect.markcoroutinefunction decorator. +# Until 3.12 is the minimum supported Python version, provide a shim. +# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py +if hasattr(inspect, "markcoroutinefunction"): + iscoroutinefunction = inspect.iscoroutinefunction + markcoroutinefunction = inspect.markcoroutinefunction +else: + iscoroutinefunction = asyncio.iscoroutinefunction + + def markcoroutinefunction(func: "_F") -> "_F": + func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + return func + + +def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": + def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + from sentry_sdk.integrations.django import ( + DjangoRequestExtractor, + _set_user_info, + ) + + if request is None: + return event + + if type(request) == WSGIRequest: + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return asgi_request_event_processor + + +def patch_django_asgi_handler_impl(cls: "Any") -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + )._run_asgi3 + + return await middleware(scope, receive, send) + + cls.__call__ = sentry_patched_asgi_handler + + modern_django_asgi_support = hasattr(cls, "create_request") + if modern_django_asgi_support: + old_create_request = cls.create_request + + @ensure_integration_enabled(DjangoIntegration, old_create_request) + def sentry_patched_create_request( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request, error_response = old_create_request(self, *args, **kwargs) + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_make_asgi_request_event_processor(request)) + + return request, error_response + + cls.create_request = sentry_patched_create_request + + +def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: + old_get_response_async = cls.get_response_async + + async def sentry_patched_get_response_async( + self: "Any", request: "Any" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + return await old_get_response_async(self, request) + + cls.get_response_async = sentry_patched_get_response_async + + +def patch_channels_asgi_handler_impl(cls: "Any") -> None: + import channels # type: ignore + + from sentry_sdk.integrations.django import DjangoIntegration + + if channels.__version__ < "3.0.0": + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, receive, send) + + middleware = SentryAsgiMiddleware( + lambda _scope: old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + ) + + return await middleware(self.scope)(receive, send) # type: ignore + + cls.__call__ = sentry_patched_asgi_handler + + else: + # The ASGI handler in Channels >= 3 has the same signature as + # the Django handler. + patch_django_asgi_handler_impl(cls) + + +def wrap_async_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + async def sentry_wrapped_callback( + request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return await callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return await callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return await callback(request, *args, **kwargs) + + return sentry_wrapped_callback + + +def _asgi_middleware_mixin_factory( + _check_middleware_span: "Callable[..., Any]", +) -> "Any": + """ + Mixin class factory that generates a middleware mixin for handling requests + in async mode. + """ + + class SentryASGIMixin: + if TYPE_CHECKING: + _inner = None + + def __init__(self, get_response: "Callable[..., Any]") -> None: + self.get_response = get_response + self._acall_method = None + self._async_check() + + def _async_check(self) -> None: + """ + If get_response is a coroutine function, turns us into async mode so + a thread is not consumed during a whole request. + Taken from django.utils.deprecation::MiddlewareMixin._async_check + """ + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) + + def async_route_check(self) -> bool: + """ + Function that checks if we are in async mode, + and if we are forwards the handling of requests to __acall__ + """ + return iscoroutinefunction(self.get_response) + + async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": + f = self._acall_method + if f is None: + if hasattr(self._inner, "__acall__"): + self._acall_method = f = self._inner.__acall__ # type: ignore + else: + self._acall_method = f = self._inner + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return await f(*args, **kwargs) # type: ignore + + with middleware_span: + return await f(*args, **kwargs) # type: ignore + + return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py new file mode 100644 index 0000000000..faf1803c11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py @@ -0,0 +1,264 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.core.cache import CacheHandler +from urllib3.util import parse_url as urlparse + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +METHODS_TO_INSTRUMENT = [ + "set", + "set_many", + "get", + "get_many", +] + + +def _get_span_description( + method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" +) -> str: + return _key_as_string(_get_safe_key(method_name, args, kwargs)) + + +def _patch_cache_method( + cache: "CacheHandler", + method_name: str, + address: "Optional[str]", + port: "Optional[int]", +) -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + original_method = getattr(cache, method_name) + + @ensure_integration_enabled(DjangoIntegration, original_method) + def _instrument_call( + cache: "CacheHandler", + method_name: str, + original_method: "Callable[..., Any]", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + address: "Optional[str]", + port: "Optional[int]", + ) -> "Any": + is_set_operation = method_name.startswith("set") + is_get_method = method_name == "get" + is_get_many_method = method_name == "get_many" + + op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET + description = _get_span_description(method_name, args, kwargs) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": op, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_attribute(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + else: + with sentry_sdk.start_span( + op=op, + name=description, + origin=DjangoIntegration.origin, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_data(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_data(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + + @functools.wraps(original_method) + def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": + return _instrument_call( + cache, method_name, original_method, args, kwargs, address, port + ) + + setattr(cache, method_name, sentry_method) + + +def _patch_cache( + cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None +) -> None: + if not hasattr(cache, "_sentry_patched"): + for method_name in METHODS_TO_INSTRUMENT: + _patch_cache_method(cache, method_name, address, port) + cache._sentry_patched = True + + +def _get_address_port( + settings: "dict[str, Any]", +) -> "tuple[Optional[str], Optional[int]]": + location = settings.get("LOCATION") + + # TODO: location can also be an array of locations + # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis + # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 + if not isinstance(location, str): + return None, None + + if "://" in location: + parsed_url = urlparse(location) + # remove the username and password from URL to not leak sensitive data. + address = "{}://{}{}".format( + parsed_url.scheme or "", + parsed_url.hostname or "", + parsed_url.path or "", + ) + port = parsed_url.port + else: + address = location + port = None + + return address, int(port) if port is not None else None + + +def should_enable_cache_spans() -> bool: + from sentry_sdk.integrations.django import DjangoIntegration + + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + from django.conf import settings + + return integration is not None and ( + (client.spotlight is not None and settings.DEBUG is True) + or integration.cache_spans is True + ) + + +def patch_caching() -> None: + if not hasattr(CacheHandler, "_sentry_patched"): + if DJANGO_VERSION < (3, 2): + original_get_item = CacheHandler.__getitem__ + + @functools.wraps(original_get_item) + def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": + cache = original_get_item(self, alias) + + if should_enable_cache_spans(): + from django.conf import settings + + address, port = _get_address_port( + settings.CACHES[alias or "default"] + ) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.__getitem__ = sentry_get_item + CacheHandler._sentry_patched = True + + else: + original_create_connection = CacheHandler.create_connection + + @functools.wraps(original_create_connection) + def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": + cache = original_create_connection(self, alias) + + if should_enable_cache_spans(): + address, port = _get_address_port(self.settings[alias or "default"]) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.create_connection = sentry_create_connection + CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py new file mode 100644 index 0000000000..a14ec96ff5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py @@ -0,0 +1,219 @@ +""" +Create spans from Django middleware invocations +""" + +from functools import wraps +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + F = TypeVar("F", bound=Callable[..., Any]) + +_import_string_should_wrap_middleware = ContextVar( + "import_string_should_wrap_middleware" +) + +DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) + +if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: + _asgi_middleware_mixin_factory = lambda _: object + iscoroutinefunction = lambda _: False +else: + from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction + + +def patch_django_middlewares() -> None: + from django.core.handlers import base + + old_import_string = base.import_string + + def sentry_patched_import_string(dotted_path: str) -> "Any": + rv = old_import_string(dotted_path) + + if _import_string_should_wrap_middleware.get(None): + rv = _wrap_middleware(rv, dotted_path) + + return rv + + base.import_string = sentry_patched_import_string + + old_load_middleware = base.BaseHandler.load_middleware + + def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": + _import_string_should_wrap_middleware.set(True) + try: + return old_load_middleware(*args, **kwargs) + finally: + _import_string_should_wrap_middleware.set(False) + + base.BaseHandler.load_middleware = sentry_patched_load_middleware + + +def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + def _check_middleware_span( + old_method: "Callable[..., Any]", + ) -> "Optional[Union[Span, StreamedSpan]]": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None or not integration.middleware_spans: + return None + + function_name = transaction_from_function(old_method) + + description = middleware_name + function_basename = getattr(old_method, "__name__", None) + if function_basename: + description = "{}.{}".format(description, function_basename) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + middleware_span: "Union[Span, StreamedSpan]" + if span_streaming: + middleware_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.MIDDLEWARE_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + else: + middleware_span = sentry_sdk.start_span( + op=OP.MIDDLEWARE_DJANGO, + name=description, + origin=DjangoIntegration.origin, + ) + middleware_span.set_tag("django.function_name", function_name) + middleware_span.set_tag("django.middleware_name", middleware_name) + + return middleware_span + + def _get_wrapped_method(old_method: "F") -> "F": + with capture_internal_exceptions(): + # Middleware hooks (e.g. `process_view`, `process_exception`) may be + # `async def` when the middleware is async. A synchronous wrapper + # would hide the coroutine from Django's `iscoroutinefunction` check, + # causing Django to call the hook synchronously and never await the + # returned coroutine. Wrap async hooks with an async wrapper so the + # wrapped method continues to report as a coroutine function. + if iscoroutinefunction is not None and iscoroutinefunction(old_method): + + async def async_sentry_wrapped_method( + *args: "Any", **kwargs: "Any" + ) -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return await old_method(*args, **kwargs) + + with middleware_span: + return await old_method(*args, **kwargs) + + sentry_wrapped_method = async_sentry_wrapped_method + + else: + + def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return old_method(*args, **kwargs) + + with middleware_span: + return old_method(*args, **kwargs) + + sentry_wrapped_method = sync_sentry_wrapped_method + + try: + # fails for __call__ of function on Python 2 (see py2.7-django-1.11) + sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) + + # Necessary for Django 3.1 + sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore + except Exception: + pass + + return sentry_wrapped_method # type: ignore + + return old_method + + class SentryWrappingMiddleware( + _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore + ): + sync_capable = getattr(middleware, "sync_capable", True) + async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( + middleware, "async_capable", False + ) + + def __init__( + self, + get_response: "Optional[Callable[..., Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> None: + if get_response: + self._inner = middleware(get_response, *args, **kwargs) + else: + self._inner = middleware(*args, **kwargs) + self.get_response = get_response + self._call_method = None + if self.async_capable: + super().__init__(get_response) + + # We need correct behavior for `hasattr()`, which we can only determine + # when we have an instance of the middleware we're wrapping. + def __getattr__(self, method_name: str) -> "Any": + if method_name not in ( + "process_request", + "process_view", + "process_template_response", + "process_response", + "process_exception", + ): + raise AttributeError() + + old_method = getattr(self._inner, method_name) + rv = _get_wrapped_method(old_method) + self.__dict__[method_name] = rv + return rv + + def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": + if hasattr(self, "async_route_check") and self.async_route_check(): + return self.__acall__(*args, **kwargs) + + f = self._call_method + if f is None: + self._call_method = f = self._inner.__call__ + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return f(*args, **kwargs) + + with middleware_span: + return f(*args, **kwargs) + + for attr in ( + "__name__", + "__module__", + "__qualname__", + ): + if hasattr(middleware, attr): + setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) + + return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py new file mode 100644 index 0000000000..7140ead782 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py @@ -0,0 +1,105 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from django.dispatch import Signal + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.django import DJANGO_VERSION +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Union + + +def _get_receiver_name(receiver: "Callable[..., Any]") -> str: + name = "" + + if hasattr(receiver, "__qualname__"): + name = receiver.__qualname__ + elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ + name = receiver.__name__ + elif hasattr( + receiver, "func" + ): # certain functions (like partials) dont have a name + if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): + name = "partial()" + + if ( + name == "" + ): # In case nothing was found, return the string representation (this is the slowest case) + return str(receiver) + + if hasattr(receiver, "__module__"): # prepend with module, if there is one + name = receiver.__module__ + "." + name + + return name + + +def patch_signals() -> None: + """ + Patch django signal receivers to create a span. + + This only wraps sync receivers. Django>=5.0 introduced async receivers, but + since we don't create transactions for ASGI Django, we don't wrap them. + """ + from sentry_sdk.integrations.django import DjangoIntegration + + old_live_receivers = Signal._live_receivers + + def _sentry_live_receivers( + self: "Signal", sender: "Any" + ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": + if DJANGO_VERSION >= (5, 0): + sync_receivers, async_receivers = old_live_receivers(self, sender) + else: + sync_receivers = old_live_receivers(self, sender) + async_receivers = [] + + def sentry_sync_receiver_wrapper( + receiver: "Callable[..., Any]", + ) -> "Callable[..., Any]": + @wraps(receiver) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + signal_name = _get_receiver_name(receiver) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + with sentry_sdk.traces.start_span( + name=signal_name, + attributes={ + "sentry.op": OP.EVENT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.CODE_FUNCTION_NAME: signal_name, + }, + ) as span: + return receiver(*args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.EVENT_DJANGO, + name=signal_name, + origin=DjangoIntegration.origin, + ) as span: + span.set_data("signal", signal_name) + return receiver(*args, **kwargs) + + return wrapper + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if ( + integration + and integration.signals_spans + and self not in integration.signals_denylist + ): + for idx, receiver in enumerate(sync_receivers): + sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + + if DJANGO_VERSION >= (5, 0): + return sync_receivers, async_receivers + else: + return sync_receivers + + Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py new file mode 100644 index 0000000000..5e23c258fb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py @@ -0,0 +1,52 @@ +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import qualname_from_function + +try: + # django.tasks were added in Django 6.0 + from django.tasks.base import Task +except ImportError: + Task = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +def patch_tasks() -> None: + if Task is None: + return + + old_task_enqueue = Task.enqueue + + @wraps(old_task_enqueue) + def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return old_task_enqueue(self, *args, **kwargs) + + name = qualname_from_function(self.func) or "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_task_enqueue(self, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin + ): + return old_task_enqueue(self, *args, **kwargs) + + Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py new file mode 100644 index 0000000000..5ab89d4a74 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py @@ -0,0 +1,209 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.template import TemplateSyntaxError +from django.utils.safestring import mark_safe + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Optional, Tuple + +try: + # support Django 1.9 + from django.template.base import Origin +except ImportError: + # backward compatibility + from django.template.loader import LoaderOrigin as Origin + + +def get_template_frame_from_exception( + exc_value: "Optional[BaseException]", +) -> "Optional[Dict[str, Any]]": + # As of Django 1.9 or so the new template debug thing showed up. + if hasattr(exc_value, "template_debug"): + return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore + + # As of r16833 (Django) all exceptions may contain a + # ``django_template_source`` attribute (rather than the legacy + # ``TemplateSyntaxError.source`` check) + if hasattr(exc_value, "django_template_source"): + return _get_template_frame_from_source( + exc_value.django_template_source # type: ignore + ) + + if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): + source = exc_value.source + if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): + return _get_template_frame_from_source(source) # type: ignore + + return None + + +def _get_template_name_description(template_name: str) -> str: + if isinstance(template_name, (list, tuple)): + if template_name: + return "[{}, ...]".format(template_name[0]) + else: + return template_name + + +def patch_templates() -> None: + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + real_rendered_content = SimpleTemplateResponse.rendered_content + + @property # type: ignore + @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) + def rendered_content(self: "SimpleTemplateResponse") -> str: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(self.template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_rendered_content.fget(self) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(self.template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", self.context_data) + return real_rendered_content.fget(self) + + SimpleTemplateResponse.rendered_content = rendered_content + + if DJANGO_VERSION < (1, 7): + return + import django.shortcuts + + real_render = django.shortcuts.render + + @functools.wraps(real_render) + @ensure_integration_enabled(DjangoIntegration, real_render) + def render( + request: "django.http.HttpRequest", + template_name: str, + context: "Optional[Dict[str, Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> "django.http.HttpResponse": + # Inject trace meta tags into template context + context = context or {} + if "sentry_trace_meta" not in context: + context["sentry_trace_meta"] = mark_safe( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_render(request, template_name, context, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", context) + return real_render(request, template_name, context, *args, **kwargs) + + django.shortcuts.render = render + + +def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": + if debug is None: + return None + + lineno = debug["line"] + filename = debug["name"] + if filename is None: + filename = "" + + pre_context = [] + post_context = [] + context_line = None + + for i, line in debug["source_lines"]: + if i < lineno: + pre_context.append(line) + elif i > lineno: + post_context.append(line) + else: + context_line = line + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + "in_app": True, + } + + +def _linebreak_iter(template_source: str) -> "Iterator[int]": + yield 0 + p = template_source.find("\n") + while p >= 0: + yield p + 1 + p = template_source.find("\n", p + 1) + + +def _get_template_frame_from_source( + source: "Tuple[Origin, Tuple[int, int]]", +) -> "Optional[Dict[str, Any]]": + if not source: + return None + + origin, (start, end) = source + filename = getattr(origin, "loadname", None) + if filename is None: + filename = "" + template_source = origin.reload() + lineno = None + upto = 0 + pre_context = [] + post_context = [] + context_line = None + + for num, next in enumerate(_linebreak_iter(template_source)): + line = template_source[upto:next] + if start >= upto and end <= next: + lineno = num + context_line = line + elif lineno is None: + pre_context.append(line) + else: + post_context.append(line) + + upto = next + + if context_line is None or lineno is None: + return None + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py new file mode 100644 index 0000000000..192f0765e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py @@ -0,0 +1,154 @@ +""" +Copied from raven-python. + +Despite being called "legacy" in some places this resolver is very much still +in use. +""" + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from re import Pattern + from typing import Dict, List, Optional, Tuple, Union + + from django.urls.resolvers import URLPattern, URLResolver + +from django import VERSION as DJANGO_VERSION + +if DJANGO_VERSION >= (2, 0): + from django.urls.resolvers import RoutePattern +else: + RoutePattern = None + +try: + from django.urls import get_resolver +except ImportError: + from django.core.urlresolvers import get_resolver + + +def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": + """Utility method for django's deprecated resolver.regex""" + try: + regex = resolver_or_pattern.regex + except AttributeError: + regex = resolver_or_pattern.pattern.regex + return regex + + +class RavenResolver: + _new_style_group_matcher = re.compile( + r"<(?:([^>:]+):)?([^>]+)>" + ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 + _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") + _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") + _non_named_group_matcher = re.compile(r"\([^\)]+\)") + # [foo|bar|baz] + _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") + _camel_re = re.compile(r"([A-Z]+)([a-z])") + + _cache: "Dict[URLPattern, str]" = {} + + def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: + r""" + Clean up urlpattern regexes into something readable by humans: + + From: + > "^(?P\w+)/athletes/(?P\w+)/$" + + To: + > "{sport_slug}/athletes/{athlete_slug}/" + """ + # "new-style" path patterns can be parsed directly without turning them + # into regexes first + if ( + RoutePattern is not None + and hasattr(pattern, "pattern") + and isinstance(pattern.pattern, RoutePattern) + ): + return self._new_style_group_matcher.sub( + lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) + ) + + result = get_regex(pattern).pattern + + # remove optional params + # TODO(dcramer): it'd be nice to change these into [%s] but it currently + # conflicts with the other rules because we're doing regexp matches + # rather than parsing tokens + result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) + + # handle named groups first + result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) + + # handle non-named groups + result = self._non_named_group_matcher.sub("{var}", result) + + # handle optional params + result = self._either_option_matcher.sub(lambda m: m.group(1), result) + + # clean up any outstanding regex-y characters. + result = ( + result.replace("^", "") + .replace("$", "") + .replace("?", "") + .replace("\\A", "") + .replace("\\Z", "") + .replace("//", "/") + .replace("\\", "") + ) + + return result + + def _resolve( + self, + resolver: "URLResolver", + path: str, + parents: "Optional[List[URLResolver]]" = None, + ) -> "Optional[str]": + match = get_regex(resolver).search(path) # Django < 2.0 + + if not match: + return None + + if parents is None: + parents = [resolver] + elif resolver not in parents: + parents = parents + [resolver] + + new_path = path[match.end() :] + for pattern in resolver.url_patterns: + # this is an include() + if not pattern.callback: + match_ = self._resolve(pattern, new_path, parents) + if match_: + return match_ + continue + elif not get_regex(pattern).search(new_path): + continue + + try: + return self._cache[pattern] + except KeyError: + pass + + prefix = "".join(self._simplify(p) for p in parents) + result = prefix + self._simplify(pattern) + if not result.startswith("/"): + result = "/" + result + self._cache[pattern] = result + return result + + return None + + def resolve( + self, + path: str, + urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, + ) -> "Optional[str]": + resolver = get_resolver(urlconf) + match = self._resolve(resolver, path) + return match + + +LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py new file mode 100644 index 0000000000..cf3012a75e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py @@ -0,0 +1,127 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +try: + from asyncio import iscoroutinefunction +except ImportError: + iscoroutinefunction = None # type: ignore + + +try: + from sentry_sdk.integrations.django.asgi import wrap_async_view +except (ImportError, SyntaxError): + wrap_async_view = None # type: ignore + + +def patch_views() -> None: + from django.core.handlers.base import BaseHandler + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + old_make_view_atomic = BaseHandler.make_view_atomic + old_render = SimpleTemplateResponse.render + + def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="serialize response", + attributes={ + "sentry.op": OP.VIEW_RESPONSE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_render(self) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RESPONSE_RENDER, + name="serialize response", + origin=DjangoIntegration.origin, + ): + return old_render(self) + + @functools.wraps(old_make_view_atomic) + def sentry_patched_make_view_atomic( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + callback = old_make_view_atomic(self, *args, **kwargs) + + # XXX: The wrapper function is created for every request. Find more + # efficient way to wrap views (or build a cache?) + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is not None: + is_async_view = ( + iscoroutinefunction is not None + and wrap_async_view is not None + and iscoroutinefunction(callback) + ) + if is_async_view: + sentry_wrapped_callback = wrap_async_view(callback) + else: + sentry_wrapped_callback = _wrap_sync_view(callback) + + else: + sentry_wrapped_callback = callback + + return sentry_wrapped_callback + + SimpleTemplateResponse.render = sentry_patched_render + BaseHandler.make_view_atomic = sentry_patched_make_view_atomic + + +def _wrap_sync_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + # set the active thread id to the handler thread for sync views + # this isn't necessary for async views since that runs on main + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return callback(request, *args, **kwargs) + + return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py new file mode 100644 index 0000000000..310766ee3a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py @@ -0,0 +1,246 @@ +import json +from typing import TypeVar + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, +) + +R = TypeVar("R") + +try: + from dramatiq.broker import Broker + from dramatiq.errors import Retry + from dramatiq.message import Message + from dramatiq.middleware import Middleware, default_middleware +except ImportError: + raise DidNotEnable("Dramatiq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Union + + from sentry_sdk._types import Event, Hint + + +class DramatiqIntegration(Integration): + """ + Dramatiq integration for Sentry + + Please make sure that you call `sentry_sdk.init` *before* initializing + your broker, as it monkey patches `Broker.__init__`. + + This integration was originally developed and maintained + by https://github.com/jacobsvante and later donated to the Sentry + project. + """ + + identifier = "dramatiq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_dramatiq_broker() + + +def _patch_dramatiq_broker() -> None: + original_broker__init__ = Broker.__init__ + + def sentry_patched_broker__init__( + self: "Broker", *args: "Any", **kw: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + + try: + middleware = kw.pop("middleware") + except KeyError: + # Unfortunately Broker and StubBroker allows middleware to be + # passed in as positional arguments, whilst RabbitmqBroker and + # RedisBroker does not. + if len(args) == 1: + middleware = args[0] + args = () + else: + middleware = None + + if middleware is None: + middleware = list(m() for m in default_middleware) + else: + middleware = list(middleware) + + if integration is not None: + middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] + middleware.insert(0, SentryMiddleware()) + + kw["middleware"] = middleware + original_broker__init__(self, *args, **kw) + + Broker.__init__ = sentry_patched_broker__init__ + + +class SentryMiddleware(Middleware): # type: ignore[misc] + """ + A Dramatiq middleware that automatically captures and sends + exceptions to Sentry. + + This is automatically added to every instantiated broker via the + DramatiqIntegration. + """ + + SENTRY_HEADERS_NAME = "_sentry_headers" + + def before_enqueue( + self, broker: "Broker", message: "Message[R]", delay: int + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + message.options[self.SENTRY_HEADERS_NAME] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + + def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DramatiqIntegration) + if integration is None: + return + + message._scope_manager = sentry_sdk.isolation_scope() + scope = message._scope_manager.__enter__() + scope.clear_breadcrumbs() + scope.set_extra("dramatiq_message_id", message.message_id) + scope.add_event_processor(_make_message_event_processor(message, integration)) + + sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} + if "retries" in message.options: + # start new trace in case of retrying + sentry_headers = {} + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(sentry_headers) + span = sentry_sdk.traces.start_span( + name=message.actor_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_DRAMATIQ, + "sentry.origin": DramatiqIntegration.origin, + "sentry.span.source": SegmentSource.TASK.value, + }, + parent_span=None, + ) + message._sentry_span_ctx = span + else: + transaction = continue_trace( + sentry_headers, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + origin=DramatiqIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + sentry_sdk.start_transaction( + transaction, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + ) + transaction.__enter__() + message._sentry_span_ctx = transaction + + def after_process_message( + self, + broker: "Broker", + message: "Message[R]", + *, + result: "Optional[Any]" = None, + exception: "Optional[Exception]" = None, + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + actor = broker.get_actor(message.actor_name) + throws = message.options.get("throws") or actor.options.get("throws") + + scope_manager = message._scope_manager + span_ctx = getattr(message, "_sentry_span_ctx", None) + if span_ctx is None: + return None + + is_event_capture_required = ( + exception is not None + and not (throws and isinstance(exception, throws)) + and not isinstance(exception, Retry) + ) + if not is_event_capture_required: + # normal transaction finish + span_ctx.__exit__(None, None, None) + scope_manager.__exit__(None, None, None) + return + + event, hint = event_from_exception( + exception, # type: ignore[arg-type] + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": DramatiqIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + # transaction error + span_ctx.__exit__(type(exception), exception, None) + scope_manager.__exit__(type(exception), exception, None) + + after_skip_message = after_process_message + + +def _make_message_event_processor( + message: "Message[R]", integration: "DramatiqIntegration" +) -> "Callable[[Event, Hint], Optional[Event]]": + def inner(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + DramatiqMessageExtractor(message).extract_into_event(event) + + return event + + return inner + + +class DramatiqMessageExtractor: + def __init__(self, message: "Message[R]") -> None: + self.message_data = dict(message.asdict()) + + def content_length(self) -> int: + return len(json.dumps(self.message_data)) + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + contexts = event.setdefault("contexts", {}) + request_info = contexts.setdefault("dramatiq", {}) + request_info["type"] = "dramatiq" + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + if not request_body_within_bounds(client, self.content_length()): + data = AnnotatedValue.removed_because_over_size_limit() + else: + data = self.message_data + + request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py new file mode 100644 index 0000000000..6bbc61000d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py @@ -0,0 +1,76 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + + +class ExcepthookIntegration(Integration): + identifier = "excepthook" + + always_run = False + + def __init__(self, always_run: bool = False) -> None: + if not isinstance(always_run, bool): + raise ValueError( + "Invalid value for always_run: %s (must be type boolean)" + % (always_run,) + ) + self.always_run = always_run + + @staticmethod + def setup_once() -> None: + sys.excepthook = _make_excepthook(sys.excepthook) + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + if _should_send(integration.always_run): + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "excepthook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook + + +def _should_send(always_run: bool = False) -> bool: + if always_run: + return True + + if hasattr(sys, "ps1"): + # Disable the excepthook for interactive Python shells, otherwise + # every typo gets sent to Sentry. + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py new file mode 100644 index 0000000000..4473fcc435 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + + +class ExecutingIntegration(Integration): + identifier = "executing" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + frame = tb.tb_frame + source = Source.for_frame(frame) + sentry_frame["function"] = source.code_qualname(frame.f_code) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py new file mode 100644 index 0000000000..7a595bcf2a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + + from sentry_sdk._types import Event, EventProcessor + +# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` +# and `falcon.API` to `falcon.App` + +try: + import falcon # type: ignore + from falcon import __version__ as FALCON_VERSION +except ImportError: + raise DidNotEnable("Falcon not installed") + +try: + import falcon.app_helpers # type: ignore + + falcon_helpers = falcon.app_helpers + falcon_app_class = falcon.App + FALCON3 = True +except ImportError: + import falcon.api_helpers # type: ignore + + falcon_helpers = falcon.api_helpers + falcon_app_class = falcon.API + FALCON3 = False + + +_FALCON_UNSET: "Optional[object]" = None +if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ + with capture_internal_exceptions(): + from falcon.request import ( # type: ignore[import-not-found, no-redef] + _UNSET as _FALCON_UNSET, + ) + + +class FalconRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, Any]": + return self.request.env + + def cookies(self) -> "Dict[str, Any]": + return self.request.cookies + + def form(self) -> None: + return None # No such concept in Falcon + + def files(self) -> None: + return None # No such concept in Falcon + + def raw_data(self) -> "Optional[str]": + # As request data can only be read once we won't make this available + # to Sentry. Just send back a dummy string in case there was a + # content length. + # TODO(jmagnusson): Figure out if there's a way to support this + content_length = self.content_length() + if content_length > 0: + return "[REQUEST_CONTAINING_RAW_DATA]" + else: + return None + + def json(self) -> "Optional[Dict[str, Any]]": + # fallback to cached_media = None if self.request._media is not available + cached_media = None + with capture_internal_exceptions(): + # self.request._media is the cached self.request.media + # value. It is only available if self.request.media + # has already been accessed. Therefore, reading + # self.request._media will not exhaust the raw request + # stream (self.request.bounded_stream) because it has + # already been read if self.request._media is set. + cached_media = self.request._media + + if cached_media is not _FALCON_UNSET: + return cached_media + + return None + + +class SentryFalconMiddleware: + """Captures exceptions in Falcon requests and send to Sentry""" + + def process_request( + self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return + + scope = sentry_sdk.get_isolation_scope() + scope._name = "falcon" + scope.add_event_processor(_make_request_event_processor(req, integration)) + + def process_resource( + self, req: "Any", resp: "Any", resource: "Any", params: "Any" + ) -> None: + """ + Sets the segment name and source as the route is resolved when this runs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FalconIntegration) + if integration is None or not has_span_streaming_enabled(client.options): + return + + name_for_style = { + "uri_template": req.uri_template, + "path": req.path, + } + name = name_for_style[integration.transaction_style] + source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] + sentry_sdk.set_transaction_name(name, source) + + +TRANSACTION_STYLE_VALUES = ("uri_template", "path") + + +class FalconIntegration(Integration): + identifier = "falcon" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "uri_template") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + version = parse_version(FALCON_VERSION) + _check_minimum_version(FalconIntegration, version) + + _patch_wsgi_app() + _patch_handle_exception() + _patch_prepare_middleware() + + +def _patch_wsgi_app() -> None: + original_wsgi_app = falcon_app_class.__call__ + + def sentry_patched_wsgi_app( + self: "falcon.API", env: "Any", start_response: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return original_wsgi_app(self, env, start_response) + + sentry_wrapped = SentryWsgiMiddleware( + lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), + span_origin=FalconIntegration.origin, + ) + + return sentry_wrapped(env, start_response) + + falcon_app_class.__call__ = sentry_patched_wsgi_app + + +def _patch_handle_exception() -> None: + original_handle_exception = falcon_app_class._handle_exception + + @ensure_integration_enabled(FalconIntegration, original_handle_exception) + def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": + # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception + # method signature from `(ex, req, resp, params)` to + # `(req, resp, ex, params)` + ex = response = None + with capture_internal_exceptions(): + ex = next(argument for argument in args if isinstance(argument, Exception)) + response = next( + argument for argument in args if isinstance(argument, falcon.Response) + ) + + was_handled = original_handle_exception(self, *args) + + if ex is None or response is None: + # Both ex and response should have a non-None value at this point; otherwise, + # there is an error with the SDK that will have been captured in the + # capture_internal_exceptions block above. + return was_handled + + if _exception_leads_to_http_5xx(ex, response): + event, hint = event_from_exception( + ex, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "falcon", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return was_handled + + falcon_app_class._handle_exception = sentry_patched_handle_exception + + +def _patch_prepare_middleware() -> None: + original_prepare_middleware = falcon_helpers.prepare_middleware + + def sentry_patched_prepare_middleware( + middleware: "Any" = None, + independent_middleware: "Any" = False, + asgi: bool = False, + ) -> "Any": + if asgi: + # We don't support ASGI Falcon apps, so we don't patch anything here + return original_prepare_middleware(middleware, independent_middleware, asgi) + + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is not None: + middleware = [SentryFalconMiddleware()] + (middleware or []) + + # We intentionally omit the asgi argument here, since the default is False anyways, + # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. + return original_prepare_middleware(middleware, independent_middleware) + + falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware + + +def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: + is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( + "5" + ) + is_unhandled_error = not isinstance( + ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) + ) + + # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response + # at the stage where we capture it is listed as 200, even though we would expect to see a 500 + # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to + # only perform this check on Falcon 3+, despite the risk that some handled errors might be + # reported to Sentry as unhandled on Falcon 2. + return (is_server_error or is_unhandled_error) and ( + not FALCON3 or _has_http_5xx_status(response) + ) + + +def _has_http_5xx_status(response: "falcon.Response") -> bool: + return response.status.startswith("5") + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "falcon.Request" +) -> None: + name_for_style = { + "uri_template": request.uri_template, + "path": request.path, + } + event["transaction"] = name_for_style[transaction_style] + event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} + + +def _make_request_event_processor( + req: "falcon.Request", integration: "FalconIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, req) + + with capture_internal_exceptions(): + FalconRequestExtractor(req).extract_into_event(event) + + return event + + return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py new file mode 100644 index 0000000000..c7b97c88b1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py @@ -0,0 +1,201 @@ +import sys +from copy import deepcopy +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import transaction_from_function + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Dict + + from sentry_sdk._types import Event + +try: + from sentry_sdk.integrations.starlette import ( + StarletteIntegration, + StarletteRequestExtractor, + _get_cached_request_body_attribute, + ) +except DidNotEnable: + raise DidNotEnable("Starlette is not installed") + +try: + import fastapi # type: ignore +except ImportError: + raise DidNotEnable("FastAPI is not installed") + + +_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +class FastApiIntegration(StarletteIntegration): + identifier = "fastapi" + + @staticmethod + def setup_once() -> None: + patch_get_request_handler() + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or "" + + elif transaction_style == "url": + route = request.scope.get("route") + + if route: + # FastAPI >= 0.137 stores the prefix-resolved path on an + # effective_route_context in scope["fastapi"], while + # scope["route"].path holds the unprefixed original. + # Prefer the effective context path when available. + effective_route_context = request.scope.get("fastapi", {}).get( + "effective_route_context" + ) + context_path = getattr(effective_route_context, "path", None) + + if context_path: + name = context_path + else: + path = getattr(route, "path", None) + if path is not None: + name = path + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name(name, source=source) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FastApiIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, Dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if info: + if "cookies" in info and should_send_default_pii(): + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = FastApiIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_get_request_handler() -> None: + old_get_request_handler = fastapi.routing.get_request_handler + + def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": + dependant = kwargs.get("dependant") + if ( + dependant + and dependant.call is not None + and not iscoroutinefunction(dependant.call) + # FastAPI >= 0.137 calls get_request_handler() on every request + # (router-tree traversal) rather than once at registration. Guard + # against accumulating _sentry_call wrappers on the shared + # dependant object, which would cause a RecursionError after ~987 + # requests as the call chain grows past Python's recursion limit. + and not getattr(dependant.call, "_sentry_is_patched", False) + ): + old_call = dependant.call + + @wraps(old_call) + def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": + current_scope = sentry_sdk.get_current_scope() + + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_call(*args, **kwargs) + + _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] + dependant.call = _sentry_call + + old_app = old_get_request_handler(*args, **kwargs) + + async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_app, *args, **kwargs) + + return _sentry_app + + fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py new file mode 100644 index 0000000000..1902091fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py @@ -0,0 +1,281 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Union + + from werkzeug.datastructures import FileStorage, ImmutableMultiDict + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + + +try: + import flask_login # type: ignore +except ImportError: + flask_login = None + +try: + from flask import Flask, Request # type: ignore + from flask import request as flask_request + from flask.signals import ( + before_render_template, + got_request_exception, + request_started, + ) + from markupsafe import Markup +except ImportError: + raise DidNotEnable("Flask is not installed") + +try: + import blinker # noqa +except ImportError: + raise DidNotEnable("blinker is not installed") + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class FlaskIntegration(Integration): + identifier = "flask" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + try: + from quart import Quart # type: ignore + + if Flask == Quart: + # This is Quart masquerading as Flask, don't enable the Flask + # integration. See https://github.com/getsentry/sentry-python/issues/2709 + raise DidNotEnable( + "This is not a Flask app but rather Quart pretending to be Flask" + ) + except ImportError: + pass + + version = package_version("flask") + _check_minimum_version(FlaskIntegration, version) + + before_render_template.connect(_add_sentry_trace) + request_started.connect(_request_started) + got_request_exception.connect(_capture_exception) + + old_app = Flask.__call__ + + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return old_app(self, environ, start_response) + + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=FlaskIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + Flask.__call__ = sentry_patched_wsgi_app + + +def _add_sentry_trace( + sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" +) -> None: + if "sentry_trace" in context: + return + + scope = sentry_sdk.get_current_scope() + trace_meta = Markup(scope.trace_propagation_meta()) + context["sentry_trace"] = trace_meta # for backwards compatibility + context["sentry_trace_meta"] = trace_meta + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + scope.set_transaction_name( + name_for_style[transaction_style], + source=SOURCE_FOR_STYLE[transaction_style], + ) + except Exception: + pass + + +def _request_started(app: "Flask", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return + + request = flask_request._get_current_object() + + # Set the transaction name and source here, + # but rely on WSGI middleware to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + scope.set_user(user_properties) + + evt_processor = _make_request_event_processor(app, request, integration) + scope.add_event_processor(evt_processor) + + +class FlaskRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[Any, Any]": + return { + k: v[0] if isinstance(v, list) and len(v) == 1 else v + for k, v in self.request.cookies.items() + } + + def raw_data(self) -> bytes: + return self.request.get_data() + + def form(self) -> "ImmutableMultiDict[str, Any]": + return self.request.form + + def files(self) -> "ImmutableMultiDict[str, Any]": + return self.request.files + + def is_json(self) -> bool: + return self.request.is_json + + def json(self) -> "Any": + return self.request.get_json(silent=True) + + def size_of_file(self, file: "FileStorage") -> int: + return file.content_length + + +def _make_request_event_processor( + app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + FlaskRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _add_user_to_event(event) + + return event + + return inner + + +@ensure_integration_enabled(FlaskIntegration) +def _capture_exception( + sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "flask", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_flask_user_properties() -> "Dict[str, str]": + if flask_login is None: + return {} + + user = flask_login.current_user + if user is None: + return {} + + properties = {} + + try: + user_id = user.get_id() + if user_id is not None: + properties["id"] = user_id + except AttributeError: + # might happen if: + # - flask_login could not be imported + # - flask_login is not configured + # - no user is logged in + pass + + # The following attribute accesses are ineffective for the general + # Flask-Login case, because the User interface of Flask-Login does not + # care about anything but the ID. However, Flask-User (based on + # Flask-Login) documents a few optional extra attributes. + # + # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names + try: + if user.email is not None: + properties["email"] = user.email + except Exception: + pass + + try: + if user.username is not None: + properties["username"] = user.username + except Exception: + pass + + return properties + + +def _add_user_to_event(event: "Event") -> None: + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + user_info = event.setdefault("user", {}) + for key, value in user_properties.items(): + user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py new file mode 100644 index 0000000000..91a62b3a81 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py @@ -0,0 +1,286 @@ +import functools +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +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 +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +# Constants +TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_func(func: "F") -> "F": + @functools.wraps(func) + def sentry_func( + functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(GcpIntegration) + if integration is None: + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = environ.get("FUNCTION_TIMEOUT_SEC") + if not configured_time: + logger.debug( + "The configured timeout could not be fetched from Cloud Functions configuration." + ) + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = int(configured_time) + + initial_time = datetime.now(timezone.utc) + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + gcp_event, configured_time, initial_time + ) + ) + scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) + timeout_thread = None + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = configured_time - TIMEOUT_WARNING_BUFFER + + timeout_thread = TimeoutThread( + waiting_time, + configured_time, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = {} + header_attributes: "dict[str, Any]" = {} + if hasattr(gcp_event, "headers"): + headers = gcp_event.headers + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + additional_attributes = {} + 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" + ) + + sampling_context = { + "gcp_env": { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + }, + "gcp_event": gcp_event, + } + + function_name = environ.get("FUNCTION_NAME", "") + + if environ.get("GCP_PROJECT"): + additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") + + if environ.get("FUNCTION_IDENTITY"): + additional_attributes["faas.identity"] = environ.get( + "FUNCTION_IDENTITY" + ) + + if environ.get("ENTRY_POINT"): + additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_GCP, + "sentry.origin": GcpIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.provider": CLOUD_PROVIDER.GCP, + "faas.name": function_name, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_GCP, + name=environ.get("FUNCTION_NAME", ""), + source=TransactionSource.COMPONENT, + origin=GcpIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return func(functionhandler, gcp_event, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "gcp", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + # Flush out the event queue + client.flush() + + return sentry_func # type: ignore + + +class GcpIntegration(Integration): + identifier = "gcp" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + import __main__ as gcp_functions + + if not hasattr(gcp_functions, "worker_v1"): + logger.warning( + "GcpIntegration currently supports only Python 3.7 runtime environment." + ) + return + + worker1 = gcp_functions.worker_v1 + + worker1.FunctionHandler.invoke_user_function = _wrap_func( + worker1.FunctionHandler.invoke_user_function + ) + + +def _make_request_event_processor( + gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + final_time = datetime.now(timezone.utc) + time_diff = final_time - initial_time + + execution_duration_in_millis = time_diff / timedelta(milliseconds=1) + + extra = event.setdefault("extra", {}) + extra["google cloud functions"] = { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + "execution_duration_in_millis": execution_duration_in_millis, + "configured_timeout_in_seconds": configured_timeout, + } + + extra["google cloud logs"] = { + "url": _get_google_cloud_logs_url(final_time), + } + + request = event.get("request", {}) + + request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) + + if hasattr(gcp_event, "method"): + request["method"] = gcp_event.method + + if hasattr(gcp_event, "query_string"): + request["query_string"] = gcp_event.query_string.decode( + "utf-8", errors="replace" + ) + + if hasattr(gcp_event, "headers"): + request["headers"] = _filter_headers(gcp_event.headers) + + if should_send_default_pii(): + if hasattr(gcp_event, "data"): + request["data"] = gcp_event.data + else: + if hasattr(gcp_event, "data"): + # Unfortunately couldn't find a way to get structured body from GCP + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + event["request"] = deepcopy(request) + + return event + + return event_processor + + +def _get_google_cloud_logs_url(final_time: "datetime") -> str: + """ + Generates a Google Cloud Logs console URL based on the environment variables + Arguments: + final_time {datetime} -- Final time + Returns: + str -- Google Cloud Logs Console URL to logs. + """ + hour_ago = final_time - timedelta(hours=1) + formatstring = "%Y-%m-%dT%H:%M:%SZ" + + url = ( + "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" + "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" + "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" + "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" + "&interval=PT1H&scrollTimestamp={timestamp_end}" + ).format( + project=environ.get("GCP_PROJECT"), + function_name=environ.get("FUNCTION_NAME"), + region=environ.get("FUNCTION_REGION"), + timestamp_end=final_time.strftime(formatstring), + timestamp_start=hour_ago.strftime(formatstring), + ) + + return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py new file mode 100644 index 0000000000..4be0f479bc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py @@ -0,0 +1,96 @@ +import re +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + +# function is everything between index at @ +# and then we match on the @ plus the hex val +FUNCTION_RE = r"[^@]+?" +HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" + +_FRAME_RE_PATTERN = r""" +^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ +""".format( + FUNCTION_RE=FUNCTION_RE, + HEX_ADDRESS=HEX_ADDRESS, +) + +FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) + + +class GnuBacktraceIntegration(Integration): + identifier = "gnu_backtrace" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + return _process_gnu_backtrace(event, hint) + + +def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception in values: + frames = exception.get("stacktrace", {}).get("frames", []) + if not frames: + continue + + msg = exception.get("value", None) + if not msg: + continue + + additional_frames = [] + new_msg = [] + + for line in msg.splitlines(): + match = FRAME_RE.match(line) + if match: + additional_frames.append( + ( + int(match.group("index")), + { + "package": match.group("package") or None, + "function": match.group("function") or None, + "platform": "native", + }, + ) + ) + else: + # Put garbage lines back into message, not sure what to do with them. + new_msg.append(line) + + if additional_frames: + additional_frames.sort(key=lambda x: -x[0]) + for _, frame in additional_frames: + frames.append(frame) + + new_msg.append("") + exception["value"] = "\n".join(new_msg) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py new file mode 100644 index 0000000000..45652c3f71 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py @@ -0,0 +1,457 @@ +from functools import wraps +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + List, +) + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import SPANSTATUS +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.genai.models import AsyncModels, Models +except ImportError: + raise DidNotEnable("google-genai not installed") + + +from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN +from .streaming import ( + accumulate_streaming_response, + set_span_data_for_streaming_response, +) +from .utils import ( + _capture_exception, + prepare_embed_content_args, + prepare_generate_content_args, + set_span_data_for_embed_request, + set_span_data_for_embed_response, + set_span_data_for_request, + set_span_data_for_response, +) + + +class GoogleGenAIIntegration(Integration): + identifier = IDENTIFIER + origin = ORIGIN + + def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Patch sync methods + Models.generate_content = _wrap_generate_content(Models.generate_content) + Models.generate_content_stream = _wrap_generate_content_stream( + Models.generate_content_stream + ) + Models.embed_content = _wrap_embed_content(Models.embed_content) + + # Patch async methods + AsyncModels.generate_content = _wrap_async_generate_content( + AsyncModels.generate_content + ) + AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( + AsyncModels.generate_content_stream + ) + AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) + + +def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = f(self, *args, **kwargs) + + # Create wrapper iterator to accumulate responses + def new_iterator() -> "Iterator[Any]": + chunks: "List[Any]" = [] + try: + for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_generate_content_stream + + +def _wrap_async_generate_content_stream( + f: "Callable[..., Any]", +) -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = await f(self, *args, **kwargs) + + # Create wrapper async iterator to accumulate responses + async def new_async_iterator() -> "AsyncIterator[Any]": + chunks: "List[Any]" = [] + try: + async for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_async_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_async_generate_content_stream + + +def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_generate_content + + +def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_async_generate_content + + +def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_embed_content + + +def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_embed_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py new file mode 100644 index 0000000000..5b53ebf0e2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py @@ -0,0 +1,16 @@ +GEN_AI_SYSTEM = "gcp.gemini" + +# Mapping of tool attributes to their descriptions +# These are all tools that are available in the Google GenAI API +TOOL_ATTRIBUTES_MAP = { + "google_search_retrieval": "Google Search retrieval tool", + "google_search": "Google Search tool", + "retrieval": "Retrieval tool", + "enterprise_web_search": "Enterprise web search tool", + "google_maps": "Google Maps tool", + "code_execution": "Code execution tool", + "computer_use": "Computer use tool", +} + +IDENTIFIER = "google_genai" +ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py new file mode 100644 index 0000000000..8414ea4f21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py @@ -0,0 +1,172 @@ +from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union + +from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import ( + safe_serialize, +) + +from .utils import ( + UsageData, + extract_contents_text, + extract_finish_reasons, + extract_tool_calls, + extract_usage_data, +) + +if TYPE_CHECKING: + from google.genai.types import GenerateContentResponse + + from sentry_sdk.tracing import Span + + +class AccumulatedResponse(TypedDict): + id: "Optional[str]" + model: "Optional[str]" + text: str + finish_reasons: "List[str]" + tool_calls: "List[dict[str, Any]]" + usage_metadata: "Optional[UsageData]" + + +def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": + return UsageData( + input_tokens=max(self["input_tokens"], other["input_tokens"]), + output_tokens=max(self["output_tokens"], other["output_tokens"]), + input_tokens_cached=max( + self["input_tokens_cached"], other["input_tokens_cached"] + ), + output_tokens_reasoning=max( + self["output_tokens_reasoning"], other["output_tokens_reasoning"] + ), + total_tokens=max(self["total_tokens"], other["total_tokens"]), + ) + + +def accumulate_streaming_response( + chunks: "List[GenerateContentResponse]", +) -> "AccumulatedResponse": + """Accumulate streaming chunks into a single response-like object.""" + accumulated_text = [] + finish_reasons = [] + tool_calls = [] + usage_data = None + response_id = None + model = None + + for chunk in chunks: + # Extract text and tool calls + if getattr(chunk, "candidates", None): + for candidate in getattr(chunk, "candidates", []): + if hasattr(candidate, "content") and getattr( + candidate.content, "parts", [] + ): + extracted_text = extract_contents_text(candidate.content) + if extracted_text: + accumulated_text.append(extracted_text) + + extracted_finish_reasons = extract_finish_reasons(chunk) + if extracted_finish_reasons: + finish_reasons.extend(extracted_finish_reasons) + + extracted_tool_calls = extract_tool_calls(chunk) + if extracted_tool_calls: + tool_calls.extend(extracted_tool_calls) + + # Use last possible chunk, in case of interruption, and + # gracefully handle missing intermediate tokens by taking maximum + # with previous token reporting. + chunk_usage_data = extract_usage_data(chunk) + usage_data = ( + chunk_usage_data + if usage_data is None + else element_wise_usage_max(usage_data, chunk_usage_data) + ) + + accumulated_response = AccumulatedResponse( + text="".join(accumulated_text), + finish_reasons=finish_reasons, + tool_calls=tool_calls, + usage_metadata=usage_data, + id=response_id, + model=model, + ) + + return accumulated_response + + +def set_span_data_for_streaming_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + accumulated_response: "AccumulatedResponse", +) -> None: + """Set span data for accumulated streaming response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if ( + should_send_default_pii() + and integration.include_prompts + and accumulated_response.get("text") + ): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TEXT, + safe_serialize([accumulated_response["text"]]), + ) + + if accumulated_response.get("finish_reasons"): + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + accumulated_response["finish_reasons"], + ) + + if accumulated_response.get("tool_calls"): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(accumulated_response["tool_calls"]), + ) + + response_id = accumulated_response.get("id") + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + + response_model = accumulated_response.get("model") + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if accumulated_response["usage_metadata"] is None: + return + + if accumulated_response["usage_metadata"]["input_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + accumulated_response["usage_metadata"]["input_tokens"], + ) + + if accumulated_response["usage_metadata"]["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + accumulated_response["usage_metadata"]["input_tokens_cached"], + ) + + if accumulated_response["usage_metadata"]["output_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + accumulated_response["usage_metadata"]["output_tokens"], + ) + + if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + accumulated_response["usage_metadata"]["output_tokens_reasoning"], + ) + + if accumulated_response["usage_metadata"]["total_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + accumulated_response["usage_metadata"]["total_tokens"], + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py new file mode 100644 index 0000000000..464a812680 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py @@ -0,0 +1,1118 @@ +import copy +import inspect +import json +from functools import wraps +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + TypedDict, + Union, +) + +from google.genai.types import Content, GenerateContentConfig, Part, PartDict + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.utils import ( + get_modality_from_mime_type, + normalize_message_roles, + set_data_normalized, + transform_google_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + safe_serialize, +) + +from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP + +if TYPE_CHECKING: + from google.genai.types import ( + ContentListUnion, + ContentUnion, + ContentUnionDict, + EmbedContentResponse, + GenerateContentResponse, + Model, + Tool, + ) + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +_is_PIL_available = False +try: + from PIL import Image as PILImage # type: ignore[import-not-found] + + _is_PIL_available = True +except ImportError: + pass + +# Keys to use when checking to see if a dict provided by the user +# is Part-like (as opposed to a Content or multi-turn conversation entry). +_PART_DICT_KEYS = PartDict.__optional_keys__ + + +class UsageData(TypedDict): + """Structure for token usage data.""" + + input_tokens: int + input_tokens_cached: int + output_tokens: int + output_tokens_reasoning: int + total_tokens: int + + +def extract_usage_data( + response: "Union[GenerateContentResponse, dict[str, Any]]", +) -> "UsageData": + """Extract usage data from response into a structured format. + + Args: + response: The GenerateContentResponse object or dictionary containing usage metadata + + Returns: + UsageData: Dictionary with input_tokens, input_tokens_cached, + output_tokens, and output_tokens_reasoning fields + """ + usage_data = UsageData( + input_tokens=0, + input_tokens_cached=0, + output_tokens=0, + output_tokens_reasoning=0, + total_tokens=0, + ) + + # Handle dictionary response (from streaming) + if isinstance(response, dict): + usage = response.get("usage_metadata", {}) + if not usage: + return usage_data + + prompt_tokens = usage.get("prompt_token_count", 0) or 0 + tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + cached_tokens = usage.get("cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + candidates_tokens = usage.get("candidates_token_count", 0) or 0 + # python-genai reports output and reasoning tokens separately + # reasoning should be sub-category of output tokens + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = usage.get("total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + if not hasattr(response, "usage_metadata"): + return usage_data + + usage = response.usage_metadata + + # Input tokens include both prompt and tool use prompt tokens + prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 + tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + # Cached input tokens + cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + # Reasoning tokens + reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + # output_tokens = candidates_tokens + reasoning_tokens + # google-genai reports output and reasoning tokens separately + candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = getattr(usage, "total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + +def _capture_exception(exc: "Any") -> None: + """Capture exception with Google GenAI mechanism.""" + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "google_genai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def get_model_name(model: "Union[str, Model]") -> str: + """Extract model name from model parameter.""" + if isinstance(model, str): + return model + # Handle case where model might be an object with a name attribute + if hasattr(model, "name"): + return str(model.name) + return str(model) + + +def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": + """Extract messages from contents parameter which can have various formats. + + Returns a list of message dictionaries in the format: + - System: {"role": "system", "content": "string"} + - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} + """ + if contents is None: + return [] + + messages = [] + + # Handle string case + if isinstance(contents, str): + return [{"role": "user", "content": contents}] + + # Handle list case + if isinstance(contents, list): + if contents and all(_is_part_like(item) for item in contents): + # All items are parts — merge into a single multi-part user message + content_parts = [] + for item in contents: + part = _extract_part_from_item(item) + if part is not None: + content_parts.append(part) + + return [{"role": "user", "content": content_parts}] + else: + # Multi-turn conversation or mixed content types + for item in contents: + item_messages = extract_contents_messages(item) + messages.extend(item_messages) + return messages + + # Handle dictionary case (ContentDict) + if isinstance(contents, dict): + role = contents.get("role", "user") + parts = contents.get("parts") + + if parts: + content_parts = [] + tool_messages = [] + + for part in parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + # Tool message - add separately + tool_messages.append(part_result) + else: + # Regular content part + content_parts.append(part_result) + + # Add main message if we have content parts + if content_parts: + # Normalize role: "model" -> "assistant" + normalized_role = "assistant" if role == "model" else role or "user" + messages.append({"role": normalized_role, "content": content_parts}) + + # Add tool messages + messages.extend(tool_messages) + elif "text" in contents: + messages.append( + { + "role": role, + "content": [{"text": contents["text"], "type": "text"}], + } + ) + elif "inline_data" in contents: + # The "data" will always be bytes (or bytes within a string), + # so if this is present, it's safe to automatically substitute with the placeholder + messages.append( + { + "inline_data": { + "mime_type": contents["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + ) + + return messages + + # Handle Content object + if hasattr(contents, "parts") and contents.parts: + role = getattr(contents, "role", None) or "user" + content_parts = [] + tool_messages = [] + + for part in contents.parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + tool_messages.append(part_result) + else: + content_parts.append(part_result) + + if content_parts: + normalized_role = "assistant" if role == "model" else role + messages.append({"role": normalized_role, "content": content_parts}) + + messages.extend(tool_messages) + return messages + + # Handle Part object directly + part_result = _extract_part_content(contents) + if part_result: + if isinstance(part_result, dict) and part_result.get("role") == "tool": + return [part_result] + else: + return [{"role": "user", "content": [part_result]}] + + # Handle PIL.Image.Image + if _is_PIL_available and isinstance(contents, PILImage.Image): + blob_part = _extract_pil_image(contents) + if blob_part: + return [{"role": "user", "content": [blob_part]}] + + # Handle File object + if hasattr(contents, "uri") and hasattr(contents, "mime_type"): + # File object + file_uri = getattr(contents, "uri", None) + mime_type = getattr(contents, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing + if file_uri is not None: + # Default to empty string if mime_type is None + if mime_type is None: + mime_type = "" + + blob_part = { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + return [{"role": "user", "content": [blob_part]}] + + # Handle direct text attribute + if hasattr(contents, "text") and contents.text: + return [ + {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} + ] + + return [] + + +def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": + """Extract content from a Part object or dict. + + Returns: + - dict for content part (text/blob) or tool message + - None if part should be skipped + """ + if part is None: + return None + + # Handle dict Part + if isinstance(part, dict): + # Check for function_response first (tool message) + if "function_response" in part: + return _extract_tool_message_from_part(part) + + if part.get("text"): + return {"text": part["text"], "type": "text"} + + # Try using Google-specific transform for dict formats (inline_data, file_data) + result = transform_google_content_part(part) + if result is not None: + # For inline_data with bytes data, substitute the content + if "inline_data" in part: + # inline_data.data will always be bytes, or a string containing base64-encoded bytes, + # so can automatically substitute without further checks + result["content"] = BLOB_DATA_SUBSTITUTE + return result + + return None + + # Handle Part object + # Check for function_response (tool message) + if hasattr(part, "function_response") and part.function_response: + return _extract_tool_message_from_part(part) + + # Handle text + if hasattr(part, "text") and part.text: + return {"text": part.text, "type": "text"} + + # Handle file_data + if hasattr(part, "file_data") and part.file_data: + file_data = part.file_data + file_uri = getattr(file_data, "file_uri", None) + mime_type = getattr(file_data, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) + if file_uri is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + # Handle inline_data + if hasattr(part, "inline_data") and part.inline_data: + inline_data = part.inline_data + data = getattr(inline_data, "data", None) + mime_type = getattr(inline_data, "mime_type", None) + # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) + if data is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + return None + + +def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": + """Extract tool message from a Part with function_response. + + Returns: + {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} + or None if not a valid tool message + """ + function_response = None + + if isinstance(part, dict): + function_response = part.get("function_response") + elif hasattr(part, "function_response"): + function_response = part.function_response + + if not function_response: + return None + + # Extract fields from function_response + tool_call_id = None + tool_name = None + output = None + + if isinstance(function_response, dict): + tool_call_id = function_response.get("id") + tool_name = function_response.get("name") + response_dict = function_response.get("response", {}) + # Prefer "output" key if present, otherwise use entire response + output = response_dict.get("output", response_dict) + else: + # FunctionResponse object + tool_call_id = getattr(function_response, "id", None) + tool_name = getattr(function_response, "name", None) + response_obj = getattr(function_response, "response", None) + if response_obj is None: + response_obj = {} + if isinstance(response_obj, dict): + output = response_obj.get("output", response_obj) + else: + output = response_obj + + if not tool_name: + return None + + return { + "role": "tool", + "content": { + "toolCallId": str(tool_call_id) if tool_call_id else None, + "toolName": str(tool_name), + "output": safe_serialize(output) if output is not None else None, + }, + } + + +def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": + """Extract blob part from PIL.Image.Image.""" + if not _is_PIL_available or not isinstance(image, PILImage.Image): + return None + + # Get format, default to JPEG + format_str = image.format or "JPEG" + suffix = format_str.lower() + mime_type = f"image/{suffix}" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _is_part_like(item: "Any") -> bool: + """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" + if isinstance(item, (str, Part)): + return True + if isinstance(item, (list, Content)): + return False + if isinstance(item, dict): + if "role" in item or "parts" in item: + return False + # Part objects that came in as plain dicts + return bool(_PART_DICT_KEYS & item.keys()) + # File objects + if hasattr(item, "uri"): + return True + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return True + return False + + +def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": + """Convert a single part-like item to a content part dict.""" + if isinstance(item, str): + return {"text": item, "type": "text"} + + # Handle bare inline_data dicts directly to preserve the raw format + if isinstance(item, dict) and "inline_data" in item: + return { + "inline_data": { + "mime_type": item["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + + # For other dicts and Part objects, use existing _extract_part_content + result = _extract_part_content(item) + if result is not None: + return result + + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return _extract_pil_image(item) + + # File objects + if hasattr(item, "uri") and hasattr(item, "mime_type"): + file_uri = getattr(item, "uri", None) + mime_type = getattr(item, "mime_type", None) or "" + if file_uri is not None: + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + return None + + +def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": + """Extract text from contents parameter which can have various formats. + + This is a compatibility function that extracts text from messages. + For new code, use extract_contents_messages instead. + """ + messages = extract_contents_messages(contents) + if not messages: + return None + + texts = [] + for message in messages: + content = message.get("content") + if isinstance(content, str): + texts.append(content) + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + texts.append(part.get("text", "")) + + return " ".join(texts) if texts else None + + +def _format_tools_for_span( + tools: "Iterable[Tool | Callable[..., Any]]", +) -> "Optional[List[dict[str, Any]]]": + """Format tools parameter for span data.""" + formatted_tools = [] + for tool in tools: + if callable(tool): + # Handle callable functions passed directly + formatted_tools.append( + { + "name": getattr(tool, "__name__", "unknown"), + "description": getattr(tool, "__doc__", None), + } + ) + elif ( + hasattr(tool, "function_declarations") + and tool.function_declarations is not None + ): + # Tool object with function declarations + for func_decl in tool.function_declarations: + formatted_tools.append( + { + "name": getattr(func_decl, "name", None), + "description": getattr(func_decl, "description", None), + } + ) + else: + # Check for predefined tool attributes - each of these tools + # is an attribute of the tool object, by default set to None + for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): + if getattr(tool, attr_name, None): + formatted_tools.append( + { + "name": attr_name, + "description": description, + } + ) + break + + return formatted_tools if formatted_tools else None + + +def extract_tool_calls( + response: "GenerateContentResponse", +) -> "Optional[List[dict[str, Any]]]": + """Extract tool/function calls from response candidates and automatic function calling history.""" + + tool_calls = [] + + # Extract from candidates, sometimes tool calls are nested under the content.parts object + if getattr(response, "candidates", []): + for candidate in response.candidates: + if not hasattr(candidate, "content") or not getattr( + candidate.content, "parts", [] + ): + continue + + for part in candidate.content.parts: + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if getattr(function_call, "args", None): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + # Extract from automatic_function_calling_history + # This is the history of tool calls made by the model + if getattr(response, "automatic_function_calling_history", None): + for content in response.automatic_function_calling_history: + if not getattr(content, "parts", None): + continue + + for part in getattr(content, "parts", []): + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if hasattr(function_call, "args"): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + return tool_calls if tool_calls else None + + +def _capture_tool_input( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" +) -> "dict[str, Any]": + """Capture tool input from args and kwargs.""" + tool_input = kwargs.copy() if kwargs else {} + + # If we have positional args, try to map them to the function signature + if args: + try: + sig = inspect.signature(tool) + param_names = list(sig.parameters.keys()) + for i, arg in enumerate(args): + if i < len(param_names): + tool_input[param_names[i]] = arg + except Exception: + # Fallback if we can't get the signature + tool_input["args"] = args + + return tool_input + + +def _create_tool_span( + tool_name: str, tool_doc: "Optional[str]" +) -> "Union[Span, StreamedSpan]": + """Create a span for tool execution.""" + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + if tool_doc: + span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=ORIGIN, + ) + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + if tool_doc: + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + +def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": + """Wrap a tool to emit execute_tool spans when called.""" + if not callable(tool): + # Not a callable function, return as-is (predefined tools) + return tool + + tool_name = getattr(tool, "__name__", "unknown") + tool_doc = tool.__doc__ + + if inspect.iscoroutinefunction(tool): + # Async function + @wraps(tool) + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = await tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return async_wrapped + else: + # Sync function + @wraps(tool) + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return sync_wrapped + + +def wrapped_config_with_tools( + config: "GenerateContentConfig", +) -> "GenerateContentConfig": + """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as + callable functions as a part of the config object.""" + + if not config or not getattr(config, "tools", None): + return config + + result = copy.copy(config) + result.tools = [wrapped_tool(tool) for tool in config.tools] + + return result + + +def _extract_response_text( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract text from response candidates.""" + + if not response or not getattr(response, "candidates", []): + return None + + texts = [] + for candidate in response.candidates: + if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): + continue + + if candidate.content is None or candidate.content.parts is None: + continue + + for part in candidate.content.parts: + if getattr(part, "text", None): + texts.append(part.text) + + return texts if texts else None + + +def extract_finish_reasons( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract finish reasons from response candidates.""" + if not response or not getattr(response, "candidates", []): + return None + + finish_reasons = [] + for candidate in response.candidates: + if getattr(candidate, "finish_reason", None): + # Convert enum value to string if necessary + reason = str(candidate.finish_reason) + # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") + if "." in reason: + reason = reason.split(".")[-1] + finish_reasons.append(reason) + + return finish_reasons if finish_reasons else None + + +def _transform_system_instruction_one_level( + system_instructions: "Union[ContentUnionDict, ContentUnion]", + can_be_content: bool, +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, str): + return [{"type": "text", "content": system_instructions}] + + if isinstance(system_instructions, Part) and system_instructions.text: + return [{"type": "text", "content": system_instructions.text}] + + if can_be_content and isinstance(system_instructions, Content): + if isinstance(system_instructions.parts, list): + for part in system_instructions.parts: + if isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + return text_parts + + if isinstance(system_instructions, dict) and system_instructions.get("text"): + return [{"type": "text", "content": system_instructions["text"]}] + + elif can_be_content and isinstance(system_instructions, dict): + parts = system_instructions.get("parts", []) + for part in parts: + if isinstance(part, Part) and isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + elif isinstance(part, dict) and isinstance(part.get("text"), str): + text_parts.append({"type": "text", "content": part["text"]}) + return text_parts + + return text_parts + + +def _transform_system_instructions( + system_instructions: "Union[ContentUnionDict, ContentUnion]", +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, list): + text_parts = list( + chain.from_iterable( + _transform_system_instruction_one_level( + instructions, can_be_content=False + ) + for instructions in system_instructions + ) + ) + + return text_parts + + return _transform_system_instruction_one_level( + system_instructions, can_be_content=True + ) + + +def set_span_data_for_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + model: str, + contents: "ContentListUnion", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for the request.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if kwargs.get("stream", False): + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + config: "Optional[GenerateContentConfig]" = kwargs.get("config") + + # Set input messages/prompts if PII is allowed + if should_send_default_pii() and integration.include_prompts: + messages = [] + + # Add system instruction if present + system_instructions = None + if config and hasattr(config, "system_instruction"): + system_instructions = config.system_instruction + elif isinstance(config, dict) and "system_instruction" in config: + system_instructions = config.get("system_instruction") + + if system_instructions is not None: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + # Extract messages from contents + contents_messages = extract_contents_messages(contents) + messages.extend(contents_messages) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Extract parameters directly from config (not nested under generation_config) + for param, span_key in [ + ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), + ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), + ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), + ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), + ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), + ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), + ("seed", SPANDATA.GEN_AI_REQUEST_SEED), + ]: + if hasattr(config, param): + value = getattr(config, param) + if value is not None: + set_on_span(span_key, value) + + # Set tools if available + if config is not None and hasattr(config, "tools"): + tools = config.tools + if tools: + formatted_tools = _format_tools_for_span(tools) + if formatted_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + formatted_tools, + unpack=False, + ) + + +def set_span_data_for_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "GenerateContentResponse", +) -> None: + """Set span data for the response.""" + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if should_send_default_pii() and integration.include_prompts: + response_texts = _extract_response_text(response) + if response_texts: + # Format as JSON string array as per documentation + set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) + + tool_calls = extract_tool_calls(response) + if tool_calls: + # Tool calls should be JSON serialized + set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) + + finish_reasons = extract_finish_reasons(response) + if finish_reasons: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + + if getattr(response, "response_id", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) + + if getattr(response, "model_version", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) + + usage_data = extract_usage_data(response) + + if usage_data["input_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) + + if usage_data["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage_data["input_tokens_cached"], + ) + + if usage_data["output_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) + + if usage_data["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage_data["output_tokens_reasoning"], + ) + + if usage_data["total_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) + + +def prepare_generate_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[Any, Any, str]": + """Extract and prepare common arguments for generate_content methods.""" + model = args[0] if args else kwargs.get("model", "unknown") + contents = args[1] if len(args) > 1 else kwargs.get("contents") + model_name = get_model_name(model) + + config = kwargs.get("config") + wrapped_config = wrapped_config_with_tools(config) + if wrapped_config is not config: + kwargs["config"] = wrapped_config + + return model, contents, model_name + + +def prepare_embed_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[str, Any]": + """Extract and prepare common arguments for embed_content methods. + + Returns: + tuple: (model_name, contents) + """ + model = kwargs.get("model", "unknown") + contents = kwargs.get("contents") + model_name = get_model_name(model) + + return model_name, contents + + +def set_span_data_for_embed_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + contents: "Any", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for embedding request.""" + # Include input contents if PII is allowed + if should_send_default_pii() and integration.include_prompts: + if contents: + # For embeddings, contents is typically a list of strings/texts + input_texts = [] + + # Handle various content formats + if isinstance(contents, str): + input_texts = [contents] + elif isinstance(contents, list): + for item in contents: + text = extract_contents_text(item) + if text: + input_texts.append(text) + else: + text = extract_contents_text(contents) + if text: + input_texts = [text] + + if input_texts: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + input_texts, + unpack=False, + ) + + +def set_span_data_for_embed_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "EmbedContentResponse", +) -> None: + """Set span data for embedding response.""" + if not response: + return + + # Extract token counts from embeddings statistics (Vertex AI only) + # Each embedding has its own statistics with token_count + if hasattr(response, "embeddings") and response.embeddings: + total_tokens = 0 + + for embedding in response.embeddings: + if hasattr(embedding, "statistics") and embedding.statistics: + token_count = getattr(embedding.statistics, "token_count", None) + if token_count is not None: + total_tokens += int(token_count) + + # Set token count if we found any + if total_tokens > 0: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) + else: + span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py new file mode 100644 index 0000000000..dcb60e9561 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py @@ -0,0 +1,173 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +try: + import gql # type: ignore[import-not-found] + from gql.transport import ( # type: ignore[import-not-found] + AsyncTransport, + Transport, + ) + from gql.transport.exceptions import ( # type: ignore[import-not-found] + TransportQueryError, + ) + from graphql import ( + DocumentNode, + VariableDefinitionNode, + get_operation_ast, + print_ast, + ) + + try: + # gql 4.0+ + from gql import GraphQLRequest + except ImportError: + GraphQLRequest = None + +except ImportError: + raise DidNotEnable("gql is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Tuple, Union + + from sentry_sdk._types import Event, EventProcessor + + EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] + + +class GQLIntegration(Integration): + identifier = "gql" + + @staticmethod + def setup_once() -> None: + gql_version = parse_version(gql.__version__) + _check_minimum_version(GQLIntegration, gql_version) + + _patch_execute() + + +def _data_from_document(document: "DocumentNode") -> "EventDataType": + try: + operation_ast = get_operation_ast(document) + data: "EventDataType" = {"query": print_ast(document)} + + if operation_ast is not None: + data["variables"] = operation_ast.variable_definitions + if operation_ast.name is not None: + data["operationName"] = operation_ast.name.value + + return data + except (AttributeError, TypeError): + return dict() + + +def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: + """ + The RequestsHTTPTransport allows defining the HTTP method; all + other transports use POST. + """ + try: + return transport.method + except AttributeError: + return "POST" + + +def _request_info_from_transport( + transport: "Union[Transport, AsyncTransport, None]", +) -> "Dict[str, str]": + if transport is None: + return {} + + request_info = { + "method": _transport_method(transport), + } + + try: + request_info["url"] = transport.url + except AttributeError: + pass + + return request_info + + +def _patch_execute() -> None: + real_execute = gql.Client.execute + + # Maintain signature for backwards compatibility. + # gql.Client.execute() accepts a positional-only "request" + # parameter with version 4.0.0. + @ensure_integration_enabled(GQLIntegration, real_execute) + def sentry_patched_execute( + self: "gql.Client", + document: "DocumentNode", + *args: "Any", + **kwargs: "Any", + ) -> "Any": + scope = sentry_sdk.get_isolation_scope() + # document is a gql.GraphQLRequest with gql v4.0.0. + scope.add_event_processor(_make_gql_event_processor(self, document)) + + try: + # document is a gql.GraphQLRequest with gql v4.0.0. + return real_execute(self, document, *args, **kwargs) + except TransportQueryError as e: + event, hint = event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "gql", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint) + raise e + + gql.Client.execute = sentry_patched_execute + + +def _make_gql_event_processor( + client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" +) -> "EventProcessor": + def processor(event: "Event", hint: "dict[str, Any]") -> "Event": + try: + errors = hint["exc_info"][1].errors + except (AttributeError, KeyError): + errors = None + + request = event.setdefault("request", {}) + request.update( + { + "api_target": "graphql", + **_request_info_from_transport(client.transport), + } + ) + + if should_send_default_pii(): + if GraphQLRequest is not None and isinstance( + document_or_request, GraphQLRequest + ): + # In v4.0.0, gql moved to using GraphQLRequest instead of + # DocumentNode in execute + # https://github.com/graphql-python/gql/pull/556 + document = document_or_request.document + else: + document = document_or_request + + request["data"] = _data_from_document(document) + contexts = event.setdefault("contexts", {}) + response = contexts.setdefault("response", {}) + response.update( + { + "data": {"errors": errors}, + "type": response, + } + ) + + return event + + return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py new file mode 100644 index 0000000000..4938a5c0f3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py @@ -0,0 +1,181 @@ +from contextlib import contextmanager + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from graphene.types import schema as graphene_schema # type: ignore +except ImportError: + raise DidNotEnable("graphene is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Any, Dict, Union + + from graphene.language.source import Source # type: ignore + from graphql.execution import ExecutionResult + from graphql.type import GraphQLSchema + + from sentry_sdk._types import Event + + +class GrapheneIntegration(Integration): + identifier = "graphene" + + @staticmethod + def setup_once() -> None: + version = package_version("graphene") + _check_minimum_version(GrapheneIntegration, version) + + _patch_graphql() + + +def _patch_graphql() -> None: + old_graphql_sync = graphene_schema.graphql_sync + old_graphql_async = graphene_schema.graphql + + @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) + def _sentry_patched_graphql_sync( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = old_graphql_sync(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + async def _sentry_patched_graphql_async( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) + if integration is None: + return await old_graphql_async(schema, source, *args, **kwargs) + + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = await old_graphql_async(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + graphene_schema.graphql_sync = _sentry_patched_graphql_sync + graphene_schema.graphql = _sentry_patched_graphql_async + + +def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + if should_send_default_pii(): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + +@contextmanager +def graphql_span( + schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" +) -> "Generator[None, None, None]": + operation_name = kwargs.get("operation_name") or "" + + operation_type = "query" + op = OP.GRAPHQL_QUERY + if source.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif source.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + sentry_sdk.add_breadcrumb( + crumb={ + "data": { + "operation_name": operation_name, + "operation_type": operation_type, + }, + "category": "graphql.operation", + }, + ) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + additional_attributes = {} + if should_send_default_pii(): + additional_attributes["graphql.document"] = source + + _graphql_span = sentry_sdk.traces.start_span( + name=operation_name, + attributes={ + "sentry.op": op, + "graphql.operation.name": operation_name, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) + + if should_send_default_pii(): + _graphql_span.set_data("graphql.document", source) + _graphql_span.set_data("graphql.operation.name", operation_name) + _graphql_span.set_data("graphql.operation.type", operation_type) + + _graphql_span.__enter__() + + try: + yield + finally: + if is_span_streaming_enabled: + _graphql_span.end() # type: ignore + else: + _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py new file mode 100644 index 0000000000..bf74ff1351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py @@ -0,0 +1,188 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any, Optional, Sequence + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .client import ClientInterceptor +from .server import ServerInterceptor + +try: + import grpc + from grpc import Channel, Server, intercept_channel + from grpc.aio import Channel as AsyncChannel + from grpc.aio import Server as AsyncServer + + from .aio.client import ( + SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, + ) + from .aio.client import ( + SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, + ) + from .aio.server import ServerInterceptor as AsyncServerInterceptor +except ImportError: + raise DidNotEnable("grpcio is not installed.") + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from typing import Callable, ParamSpec +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + +P = ParamSpec("P") + +GRPC_VERSION = parse_version(grpc.__version__) + + +def _is_channel_intercepted(channel: "Channel") -> bool: + interceptor = getattr(channel, "_interceptor", None) + while interceptor is not None: + if isinstance(interceptor, ClientInterceptor): + return True + + inner_channel = getattr(channel, "_channel", None) + if inner_channel is None: + return False + + channel = inner_channel + interceptor = getattr(channel, "_interceptor", None) + + return False + + +def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + "Wrapper for synchronous secure and insecure channel." + + @wraps(func) + def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": + channel = func(*args, **kwargs) + if not _is_channel_intercepted(channel): + return intercept_channel(channel, ClientInterceptor()) + else: + return channel + + return patched_channel + + +def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + @wraps(func) + def patched_intercept_channel( + channel: "Channel", *interceptors: "grpc.ServerInterceptor" + ) -> "Channel": + if _is_channel_intercepted(channel): + interceptors = tuple( + [ + interceptor + for interceptor in interceptors + if not isinstance(interceptor, ClientInterceptor) + ] + ) + else: + interceptors = interceptors + return intercept_channel(channel, *interceptors) + + return patched_intercept_channel # type: ignore + + +def _wrap_channel_async( + func: "Callable[P, AsyncChannel]", +) -> "Callable[P, AsyncChannel]": + "Wrapper for asynchronous secure and insecure channel." + + @wraps(func) + def patched_channel( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Channel": + sentry_interceptors = [ + AsyncUnaryUnaryClientInterceptor(), + AsyncUnaryStreamClientIntercetor(), + ] + interceptors = [*sentry_interceptors, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_channel # type: ignore + + +def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": + """Wrapper for synchronous server.""" + + @wraps(func) + def patched_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + interceptors = [ + interceptor + for interceptor in interceptors or [] + if not isinstance(interceptor, ServerInterceptor) + ] + server_interceptor = ServerInterceptor() + interceptors = [server_interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_server # type: ignore + + +def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": + """Wrapper for asynchronous server.""" + + @wraps(func) + def patched_aio_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + server_interceptor = AsyncServerInterceptor() + interceptors = [ + server_interceptor, + *(interceptors or []), + ] + + try: + # We prefer interceptors as a list because of compatibility with + # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 + # However, prior to grpc 1.42.0, only tuples were accepted, so we + # have no choice there. + if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): + interceptors = tuple(interceptors) + except Exception: + pass + + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_aio_server # type: ignore + + +class GRPCIntegration(Integration): + identifier = "grpc" + + @staticmethod + def setup_once() -> None: + import grpc + + grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) + grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) + grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) + + grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) + grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) + + grpc.server = _wrap_sync_server(grpc.server) + grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py new file mode 100644 index 0000000000..4d21815254 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py @@ -0,0 +1,7 @@ +from .client import ClientInterceptor +from .server import ServerInterceptor + +__all__ = [ + "ClientInterceptor", + "ServerInterceptor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py new file mode 100644 index 0000000000..d07b7f19be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py @@ -0,0 +1,146 @@ +from typing import Any, AsyncIterable, Callable, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.protobuf.message import Message + from grpc.aio import ( + ClientCallDetails, + Metadata, + UnaryStreamCall, + UnaryStreamClientInterceptor, + UnaryUnaryCall, + UnaryUnaryClientInterceptor, + ) +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor: + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + if client_call_details.metadata is None: + client_call_details = client_call_details._replace(metadata=Metadata()) + elif not isinstance(client_call_details.metadata, Metadata): + # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 + # See https://github.com/grpc/grpc/issues/34298. + client_call_details = client_call_details._replace( + metadata=Metadata.from_tuple(client_call_details.metadata) + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + client_call_details.metadata.add(key, value) + return client_call_details + + +class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore + async def intercept_unary_unary( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[UnaryUnaryCall, Message]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_data("code", status_code.name) + + return response + + +class SentryUnaryStreamClientInterceptor( + ClientInterceptor, + UnaryStreamClientInterceptor, # type: ignore +): + async def intercept_unary_stream( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + # status_code = await response.code() + # span.set_data("code", status_code) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py new file mode 100644 index 0000000000..010337e98c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py @@ -0,0 +1,134 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from typing import Any, Optional + + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler + from grpc.aio import AbortError, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Callable[[ServicerContext], str] | None" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + async def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", + handler_call_details: "HandlerCallDetails", + ) -> "Optional[Awaitable[RpcMethodHandler]]": + handler = await continuation(handler_call_details) + if handler is None: + return None + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + if not handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.unary_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + with sentry_sdk.isolation_scope(): + name = ( + custom_find_name(context) if custom_find_name else method_name + ) + if not name: + return await handler(request, context) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + # What if the headers are empty? + sentry_sdk.traces.continue_trace( + dict(context.invocation_metadata()) + ) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + else: + # What if the headers are empty? + transaction = sentry_sdk.continue_trace( + dict(context.invocation_metadata()), + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + + elif not handler.request_streaming and handler.response_streaming: + handler_factory = grpc.unary_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.unary_stream(request, context): + yield r + + elif handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.stream_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + response = handler.stream_unary(request, context) + return await response + + elif handler.request_streaming and handler.response_streaming: + handler_factory = grpc.stream_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.stream_stream(request, context): + yield r + + return handler_factory( + wrapped, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py new file mode 100644 index 0000000000..5384a0a78f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py @@ -0,0 +1,149 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Iterator, Union + +try: + import grpc + from google.protobuf.message import Message + from grpc import Call, ClientCallDetails + from grpc._interceptor import _UnaryOutcome + from grpc.aio._interceptor import UnaryStreamCall +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor( + grpc.UnaryUnaryClientInterceptor, # type: ignore + grpc.UnaryStreamClientInterceptor, # type: ignore +): + def intercept_unary_unary( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "_UnaryOutcome": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_attribute( + SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name + ) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_data("code", response.code().name) + + return response + + def intercept_unary_stream( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[Iterator[Message], Call]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + response: "UnaryStreamCall" + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + metadata = ( + list(client_call_details.metadata) if client_call_details.metadata else [] + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + metadata.append((key, value)) + + client_call_details = grpc._interceptor._ClientCallDetails( + method=client_call_details.method, + timeout=client_call_details.timeout, + metadata=metadata, + credentials=client_call_details.credentials, + wait_for_ready=client_call_details.wait_for_ready, + compression=client_call_details.compression, + ) + + return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py new file mode 100644 index 0000000000..9fdb975caf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py new file mode 100644 index 0000000000..1cba1d4b85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py @@ -0,0 +1,91 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Callable, Optional + + from google.protobuf.message import Message + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Optional[Callable[[ServicerContext], str]]" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", + handler_call_details: "HandlerCallDetails", + ) -> "RpcMethodHandler": + handler = continuation(handler_call_details) + if not handler or not handler.unary_unary: + return handler + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + def behavior(request: "Message", context: "ServicerContext") -> "Message": + with sentry_sdk.isolation_scope(): + name = custom_find_name(context) if custom_find_name else method_name + + if name: + metadata = dict(context.invocation_metadata()) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(metadata) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + transaction = sentry_sdk.continue_trace( + metadata, + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + return handler.unary_unary(request, context) + + return grpc.unary_unary_rpc_method_handler( + behavior, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py new file mode 100644 index 0000000000..a68f20b299 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx is not installed") + +__all__ = ["HttpxIntegration"] + + +class HttpxIntegration(Integration): + identifier = "httpx" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx_client() + _install_httpx_async_client() + + +def _install_httpx_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(HttpxIntegration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(HttpxIntegration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py new file mode 100644 index 0000000000..25062aaa11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx2 import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx2 is not installed") + +__all__ = ["Httpx2Integration"] + + +class Httpx2Integration(Integration): + identifier = "httpx2" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx2 has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx2_client() + _install_httpx2_async_client() + + +def _install_httpx2_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(Httpx2Integration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx2_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(Httpx2Integration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py new file mode 100644 index 0000000000..c3bbc8abcf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py @@ -0,0 +1,239 @@ +import sys +from datetime import datetime +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, Hint + from sentry_sdk.utils import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + +try: + from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task + from huey.exceptions import CancelExecution, RetryTask, TaskLockedException +except ImportError: + raise DidNotEnable("Huey is not installed") + +try: + from huey.api import chord as HueyChord + from huey.api import group as HueyGroup +except ImportError: + HueyChord = None + HueyGroup = None + + +HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) + + +class HueyIntegration(Integration): + identifier = "huey" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_enqueue() + patch_execute() + _register_control_flow_exception( + [CancelExecution, RetryTask, TaskLockedException] + ) + + +def patch_enqueue() -> None: + old_enqueue = Huey.enqueue + + @ensure_integration_enabled(HueyIntegration, old_enqueue) + def _sentry_enqueue( + self: "Huey", item: "Any" + ) -> "Optional[Union[Result, ResultGroup]]": + if HueyChord is not None and isinstance(item, HueyChord): + span_name = "Huey Chord" + elif HueyGroup is not None and isinstance(item, HueyGroup): + span_name = "Huey Task Group" + else: + span_name = item.name + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + span_ctx = None + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_HUEY, + "sentry.origin": HueyIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_HUEY, + name=span_name, + origin=HueyIntegration.origin, + ) + + no_headers_types = (PeriodicTask,) + tuple( + t for t in [HueyGroup, HueyChord] if t is not None + ) + with span_ctx: + if not isinstance(item, no_headers_types): + # Attach trace propagation data to task kwargs. We do + # not do this for periodic tasks, as these don't + # really have an originating transaction. + # Additionally, we do not do this for Huey groups or chords, as enqueue will + # recursively call this method for each task within the list, resulting + # in the trace propagation data being attached to each task individually + # (which we want) + item.kwargs["sentry_headers"] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + return old_enqueue(self, item) + + Huey.enqueue = _sentry_enqueue + + +def _make_event_processor(task: "Any") -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["huey_task_id"] = task.id + tags["huey_task_retry"] = task.default_retries > task.retries + extra = event.setdefault("extra", {}) + extra["huey-job"] = { + "task": task.name, + "args": ( + task.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + task.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": (task.default_retries or 0) - task.retries, + } + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.ABORTED) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.OK + return + + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.ERROR + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": HueyIntegration.identifier, "handled": False}, + ) + scope.capture_event(event, hint=hint) + + +def _wrap_task_execute(func: "F") -> "F": + @ensure_integration_enabled(HueyIntegration, func) + def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": + try: + result = func(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_execute # type: ignore + + +def patch_execute() -> None: + old_execute = Huey._execute + + @ensure_integration_enabled(HueyIntegration, old_execute) + def _sentry_execute( + self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None + ) -> "Any": + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope._name = "huey" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task)) + + sentry_headers = task.kwargs.pop("sentry_headers", None) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + headers = sentry_headers or {} + sentry_sdk.traces.continue_trace(headers) + span_ctx = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_TASK_HUEY, + "sentry.origin": HueyIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + "messaging.message.id": task.id, + "messaging.message.system": "huey", + "messaging.message.retry.count": (task.default_retries or 0) + - task.retries, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + sentry_headers or {}, + name=task.name, + op=OP.QUEUE_TASK_HUEY, + source=TransactionSource.TASK, + origin=HueyIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + span_ctx = sentry_sdk.start_transaction(transaction) + + if not getattr(task, "_sentry_is_patched", False): + task.execute = _wrap_task_execute(task.execute) + task._sentry_is_patched = True + + with span_ctx: + return old_execute(self, task, timestamp) + + Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py new file mode 100644 index 0000000000..835acc7279 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py @@ -0,0 +1,392 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + _set_span_data_attribute, + get_start_span_function, + set_data_normalized, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Union + + from sentry_sdk.tracing import Span + +try: + import huggingface_hub.inference._client +except ImportError: + raise DidNotEnable("Huggingface not installed") + + +class HuggingfaceHubIntegration(Integration): + identifier = "huggingface_hub" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "HuggingfaceHubIntegration", include_prompts: bool = True + ) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks + huggingface_hub.inference._client.InferenceClient.text_generation = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.text_generation, + OP.GEN_AI_TEXT_COMPLETION, + ) + ) + huggingface_hub.inference._client.InferenceClient.chat_completion = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.chat_completion, + OP.GEN_AI_CHAT, + ) + ) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "huggingface_hub", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": + @wraps(f) + def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) + if integration is None: + return f(*args, **kwargs) + + prompt = None + if "prompt" in kwargs: + prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] + elif len(args) >= 2: + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error + return f(*args, **kwargs) + + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(sentry_sdk.get_client().options): + span = sentry_sdk.traces.start_span( + name=f"{operation_name} {model}", + attributes={ + "sentry.op": op, + "sentry.origin": HuggingfaceHubIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=op, + name=f"{operation_name} {model}", + origin=HuggingfaceHubIntegration.origin, + ) + span.__enter__() + + _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) + + if model: + _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) + + # Input attributes + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + ) + + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + _set_span_data_attribute(span, span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + tool_calls = None + usage = None + + with capture_internal_exceptions(): + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) + + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) + + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): + span.__exit__(None, None, None) + return res + + if kwargs.get("details", False): + # text-generation stream output + def new_details_iterator() -> "Iterable[Any]": + finish_reason = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + + with capture_internal_exceptions(): + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None + ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + span.__exit__(None, None, None) + + return new_details_iterator() + + else: + # chat-completion stream output + def new_iterator() -> "Iterable[str]": + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tool_calls = None + usage = None + + with capture_internal_exceptions(): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if hasattr(chunk, "choices") and chunk.choices is not None: + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content + ) + + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason + + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls + + yield chunk + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + + span.__exit__(None, None, None) + + return new_iterator() + + return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py new file mode 100644 index 0000000000..9dcbb189ce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py @@ -0,0 +1,1412 @@ +import itertools +import json +import sys +import warnings +from collections import OrderedDict +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + _get_value, + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import capture_internal_exceptions, logger + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Dict, + Iterator, + List, + Optional, + Union, + ) + from uuid import UUID + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + + +try: + from langchain_core.agents import AgentFinish + from langchain_core.callbacks import ( + BaseCallbackHandler, + BaseCallbackManager, + Callbacks, + manager, + ) + from langchain_core.messages import BaseMessage + from langchain_core.outputs import LLMResult + +except ImportError: + raise DidNotEnable("langchain not installed") + + +try: + # >=v1 + from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] +except ImportError: + try: + # "Optional[str]": + ai_type = all_params.get("_type") + + if not ai_type or not isinstance(ai_type, str): + return None + + return ai_type + + +DATA_FIELDS = { + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, +} + + +def _transform_langchain_content_block( + content_block: "Dict[str, Any]", +) -> "Dict[str, Any]": + """ + Transform a LangChain content block using the shared transform_content_part function. + + Returns the original content block if transformation is not applicable + (e.g., for text blocks or unrecognized formats). + """ + result = transform_content_part(content_block) + return result if result is not None else content_block + + +def _transform_langchain_message_content(content: "Any") -> "Any": + """ + Transform LangChain message content, handling both string content and + list of content blocks. + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for block in content: + if isinstance(block, dict): + transformed.append(_transform_langchain_content_block(block)) + else: + transformed.append(block) + return transformed + + return content + + +def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": + system_instructions = [] + + for list_ in messages: + for message in list_: + # type of content: str | list[str | dict] | None + if message.type == "system" and isinstance(message.content, str): + system_instructions.append(message.content) + + elif message.type == "system" and isinstance(message.content, list): + for item in message.content: + if isinstance(item, str): + system_instructions.append(item) + + elif isinstance(item, dict) and item.get("type") == "text": + instruction = item.get("text") + if isinstance(instruction, str): + system_instructions.append(instruction) + + return system_instructions + + +def _transform_system_instructions( + system_instructions: "List[str]", +) -> "List[TextPart]": + return [ + { + "type": "text", + "content": instruction, + } + for instruction in system_instructions + ] + + +class LangchainIntegration(Integration): + identifier = "langchain" + origin = f"auto.ai.{identifier}" + + _ignored_exceptions: "set[type[Exception]]" = set() + + def __init__( + self: "LangchainIntegration", + include_prompts: bool = True, + max_spans: "Optional[int]" = None, + ) -> None: + self.include_prompts = include_prompts + self.max_spans = max_spans + + if max_spans is not None: + warnings.warn( + "The `max_spans` parameter of `LangchainIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + manager._configure = _wrap_configure(manager._configure) + + if AgentExecutor is not None: + AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) + AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) + + # Patch embeddings providers + _patch_embeddings_provider(OpenAIEmbeddings) + _patch_embeddings_provider(AzureOpenAIEmbeddings) + _patch_embeddings_provider(VertexAIEmbeddings) + _patch_embeddings_provider(BedrockEmbeddings) + _patch_embeddings_provider(CohereEmbeddings) + _patch_embeddings_provider(MistralAIEmbeddings) + _patch_embeddings_provider(HuggingFaceEmbeddings) + _patch_embeddings_provider(OllamaEmbeddings) + + +class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] + """Callback handler that creates Sentry spans.""" + + def __init__( + self, max_span_map_size: "Optional[int]", include_prompts: bool + ) -> None: + self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() + self.max_span_map_size = max_span_map_size + self.include_prompts = include_prompts + + def gc_span_map(self) -> None: + if self.max_span_map_size is not None: + while len(self.span_map) > self.max_span_map_size: + run_id, span = self.span_map.popitem(last=False) + self._exit_span(span, run_id) + + def _handle_error(self, run_id: "UUID", error: "Any") -> None: + is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) + + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if is_ignored: + span.__exit__(None, None, None) + else: + sentry_sdk.capture_exception( + error, span._scope if isinstance(span, StreamedSpan) else span.scope + ) + span.__exit__(type(error), error, error.__traceback__) + + del self.span_map[run_id] + + def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": + # Transform content to handle multimodal data (images, audio, video, files) + transformed_content = _transform_langchain_message_content(message.content) + parsed = {"role": message.type, "content": transformed_content} + parsed.update(message.additional_kwargs) + return parsed + + def _create_span( + self: "SentryLangchainCallback", + run_id: "UUID", + parent_id: "Optional[Any]", + op: str, + name: str, + origin: str, + ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span = None + if parent_id: + parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( + self.span_map.get(parent_id) + ) + if parent_span: + span = ( + sentry_sdk.traces.start_span( + parent_span=parent_span, + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if isinstance(parent_span, StreamedSpan) + else parent_span.start_child(op=op, name=name, origin=origin) + ) + + if span is None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span = ( + sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if span_streaming + else sentry_sdk.start_span(op=op, name=name, origin=origin) + ) + + span.__enter__() + self.span_map[run_id] = span + self.gc_span_map() + return span + + def _exit_span( + self: "SentryLangchainCallback", + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + run_id: "UUID", + ) -> None: + span.__exit__(None, None, None) + del self.span_map[run_id] + + def on_llm_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + prompts: "List[str]", + *, + run_id: "UUID", + tags: "Optional[List[str]]" = None, + parent_run_id: "Optional[UUID]" = None, + metadata: "Optional[Dict[str, Any]]" = None, + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + parent_run_id, + op=OP.GEN_AI_TEXT_COMPLETION, + name=f"text_completion {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") + + run_name = kwargs.get("name") + if run_name: + set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + if model: + set_on_span( + SPANDATA.GEN_AI_REQUEST_MODEL, + model, + ) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + normalized_messages = [ + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, + "content": {"type": "text", "text": prompt}, + } + for prompt in prompts + ] + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + messages: "List[List[BaseMessage]]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if model: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + system_instructions = _get_system_instructions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + normalized_messages = [] + for list_ in messages: + for message in list_: + if message.type == "system": + continue + + normalized_messages.append( + self._normalize_langchain_message(message) + ) + normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + try: + generation = response.generations[0][0] + except IndexError: + generation = None + + if generation is not None: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + + try: + response_model = generation.message.response_metadata.get( + "model_name" + ) + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + except AttributeError: + pass + + try: + finish_reason = generation.generation_info.get("finish_reason") + if finish_reason is not None: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + [finish_reason], + ) + except AttributeError: + pass + + try: + if should_send_default_pii() and self.include_prompts: + tool_calls = getattr(generation.message, "tool_calls", None) + if tool_calls is not None and tool_calls != []: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + except AttributeError: + pass + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM errors.""" + self._handle_error(run_id, error) + + def on_chat_model_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model errors.""" + self._handle_error(run_id, error) + + def on_agent_finish( + self: "SentryLangchainCallback", + finish: "AgentFinish", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() + ) + + self._exit_span(span, run_id) + + def on_tool_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + input_str: str, + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + tool_name = serialized.get("name") or kwargs.get("name") or "" + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + tool_description = serialized.get("description") + if tool_description is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_TOOL_INPUT, + kwargs.get("inputs", [input_str]), + ) + + def on_tool_end( + self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" + ) -> "Any": + """Run when tool ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) + + self._exit_span(span, run_id) + + def on_tool_error( + self, + error: "SentryLangchainCallback", + *args: "Union[Exception, KeyboardInterrupt]", + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool errors.""" + self._handle_error(run_id, error) + + +def _extract_tokens( + token_usage: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + if not token_usage: + return None, None, None + + input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( + token_usage, "input_tokens" + ) + output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( + token_usage, "output_tokens" + ) + total_tokens = _get_value(token_usage, "total_tokens") + + return input_tokens, output_tokens, total_tokens + + +def _extract_tokens_from_generations( + generations: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + """Extract token usage from response.generations structure.""" + if not generations: + return None, None, None + + total_input = 0 + total_output = 0 + total_total = 0 + + for gen_list in generations: + for gen in gen_list: + token_usage = _get_token_usage(gen) + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + total_input += input_tokens if input_tokens is not None else 0 + total_output += output_tokens if output_tokens is not None else 0 + total_total += total_tokens if total_tokens is not None else 0 + + return ( + total_input if total_input > 0 else None, + total_output if total_output > 0 else None, + total_total if total_total > 0 else None, + ) + + +def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": + """ + Check multiple paths to extract token usage from different objects. + """ + possible_names = ("usage", "token_usage", "usage_metadata") + + message = _get_value(obj, "message") + if message is not None: + for name in possible_names: + usage = _get_value(message, name) + if usage is not None: + return usage + + llm_output = _get_value(obj, "llm_output") + if llm_output is not None: + for name in possible_names: + usage = _get_value(llm_output, name) + if usage is not None: + return usage + + for name in possible_names: + usage = _get_value(obj, name) + if usage is not None: + return usage + + return None + + +def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: + token_usage = _get_token_usage(response) + if token_usage: + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + else: + input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( + response.generations + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) + + +def _get_request_data( + obj: "Any", args: "Any", kwargs: "Any" +) -> "tuple[Optional[str], Optional[List[Any]]]": + """ + Get the agent name and available tools for the agent. + """ + agent = getattr(obj, "agent", None) + runnable = getattr(agent, "runnable", None) + runnable_config = getattr(runnable, "config", {}) + tools = ( + getattr(obj, "tools", None) + or getattr(agent, "tools", None) + or runnable_config.get("tools") + or runnable_config.get("available_tools") + ) + tools = tools if tools and len(tools) > 0 else None + + try: + agent_name = None + if len(args) > 1: + agent_name = args[1].get("run_name") + if agent_name is None: + agent_name = runnable_config.get("run_name") + except Exception: + pass + + return (agent_name, tools) + + +def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": + """Parse and simplify tools into a cleaner format.""" + if not tools: + return None + + if not isinstance(tools, (list, tuple)): + return None + + simplified_tools = [] + for tool in tools: + try: + if isinstance(tool, dict): + if "function" in tool and isinstance(tool["function"], dict): + func = tool["function"] + simplified_tool = { + "name": func.get("name"), + "description": func.get("description"), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif "name" in tool: + simplified_tool = { + "name": tool.get("name"), + "description": tool.get("description"), + } + simplified_tools.append(simplified_tool) + else: + name = ( + tool.get("name") + or tool.get("tool_name") + or tool.get("function_name") + ) + if name: + simplified_tools.append( + { + "name": name, + "description": tool.get("description") + or tool.get("desc"), + } + ) + elif hasattr(tool, "name"): + simplified_tool = { + "name": getattr(tool, "name", None), + "description": getattr(tool, "description", None) + or getattr(tool, "desc", None), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif hasattr(tool, "__name__"): + simplified_tools.append( + { + "name": tool.__name__, + "description": getattr(tool, "__doc__", None), + } + ) + else: + tool_str = str(tool) + if tool_str and tool_str != "": + simplified_tools.append({"name": tool_str, "description": None}) + except Exception: + continue + + return simplified_tools if simplified_tools else None + + +def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: + """Set available tools data on a span if tools are provided.""" + if tools is not None: + simplified_tools = _simplify_langchain_tools(tools) + if simplified_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + simplified_tools, + unpack=False, + ) + + +def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_configure( + callback_manager_cls: type, + inheritable_callbacks: "Callbacks" = None, + local_callbacks: "Callbacks" = None, + *args: "Any", + **kwargs: "Any", + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(LangchainIntegration) + if integration is None: + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + local_callbacks = local_callbacks or [] + + # Handle each possible type of local_callbacks. For each type, we + # extract the list of callbacks to check for SentryLangchainCallback, + # and define a function that would add the SentryLangchainCallback + # to the existing callbacks list. + if isinstance(local_callbacks, BaseCallbackManager): + callbacks_list = local_callbacks.handlers + elif isinstance(local_callbacks, BaseCallbackHandler): + callbacks_list = [local_callbacks] + elif isinstance(local_callbacks, list): + callbacks_list = local_callbacks + else: + logger.debug("Unknown callback type: %s", local_callbacks) + # Just proceed with original function call + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + # Handle each possible type of inheritable_callbacks. + if isinstance(inheritable_callbacks, BaseCallbackManager): + inheritable_callbacks_list = inheritable_callbacks.handlers + elif isinstance(inheritable_callbacks, list): + inheritable_callbacks_list = inheritable_callbacks + else: + inheritable_callbacks_list = [] + + if not any( + isinstance(cb, SentryLangchainCallback) + for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) + ): + sentry_handler = SentryLangchainCallback( + integration.max_spans, + integration.include_prompts, + ) + if isinstance(local_callbacks, BaseCallbackManager): + local_callbacks = local_callbacks.copy() + local_callbacks.handlers = [ + *local_callbacks.handlers, + sentry_handler, + ] + elif isinstance(local_callbacks, BaseCallbackHandler): + local_callbacks = [local_callbacks, sentry_handler] + else: + local_callbacks = [*local_callbacks, sentry_handler] + + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + return new_configure + + +def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: False, + }, + ) as span: + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + else: + start_span_function = get_start_span_function() + + with start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) as span: + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + + return new_invoke + + +def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + else: + start_span_function = get_start_span_function() + + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + input = args[0].get("input") if len(args) >= 1 else None + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Run the agent + result = f(self, *args, **kwargs) + + old_iterator = result + + def new_iterator() -> "Iterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + async def new_iterator_async() -> "AsyncIterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + async for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + if str(type(result)) == "": + result = new_iterator_async() + else: + result = new_iterator() + + return result + + return new_stream + + +def _patch_embeddings_provider(provider_class: "Any") -> None: + """Patch an embeddings provider class with monitoring wrappers.""" + if provider_class is None: + return + + if hasattr(provider_class, "embed_documents"): + provider_class.embed_documents = _wrap_embedding_method( + provider_class.embed_documents + ) + if hasattr(provider_class, "embed_query"): + provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) + if hasattr(provider_class, "aembed_documents"): + provider_class.aembed_documents = _wrap_async_embedding_method( + provider_class.aembed_documents + ) + if hasattr(provider_class, "aembed_query"): + provider_class.aembed_query = _wrap_async_embedding_method( + provider_class.aembed_query + ) + + +def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap sync embedding methods (embed_documents and embed_query).""" + + @wraps(f) + def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + + return new_embedding_method + + +def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap async embedding methods (aembed_documents and aembed_query).""" + + @wraps(f) + async def new_async_embedding_method( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + + return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py new file mode 100644 index 0000000000..3d3856a913 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py @@ -0,0 +1,515 @@ +from functools import wraps +from typing import Any, Callable, List, Optional + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration + +# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. +from sentry_sdk.integrations.langchain import LangchainIntegration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +try: + from langgraph.errors import GraphBubbleUp + from langgraph.graph import StateGraph + from langgraph.pregel import Pregel +except ImportError: + raise DidNotEnable("langgraph not installed") + + +class LanggraphIntegration(Integration): + identifier = "langgraph" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) + # LangGraph lets users create agents using a StateGraph or the Functional API. + # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and + # the functional API execute on a Pregel instance. Pregel is the runtime for the graph + # and the invocation happens on Pregel, so patching the invoke methods takes care of both. + # The streaming methods are not patched, because due to some internal reasons, LangGraph + # will automatically patch the streaming methods to run through invoke, and by doing this + # we prevent duplicate spans for invocations. + StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) + if hasattr(Pregel, "invoke"): + Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) + if hasattr(Pregel, "ainvoke"): + Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) + + +def _get_graph_name(graph_obj: "Any") -> "Optional[str]": + for attr in ["name", "graph_name", "__name__", "_name"]: + if hasattr(graph_obj, attr): + name = getattr(graph_obj, attr) + if name and isinstance(name, str): + return name + return None + + +def _normalize_langgraph_message(message: "Any") -> "Any": + if not hasattr(message, "content"): + return None + + parsed = {"role": getattr(message, "type", None), "content": message.content} + + for attr in [ + "name", + "tool_calls", + "function_call", + "tool_call_id", + "response_metadata", + ]: + if hasattr(message, attr): + value = getattr(message, attr) + if value is not None: + parsed[attr] = value + + return parsed + + +def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": + if not state: + return None + + messages = None + + if isinstance(state, dict): + messages = state.get("messages") + elif hasattr(state, "messages"): + messages = state.messages + elif hasattr(state, "get") and callable(state.get): + try: + messages = state.get("messages") + except Exception: + pass + + if not messages or not isinstance(messages, (list, tuple)): + return None + + normalized_messages = [] + for message in messages: + try: + normalized = _normalize_langgraph_message(message) + if normalized: + normalized_messages.append(normalized) + except Exception: + continue + + return normalized_messages if normalized_messages else None + + +def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None or has_span_streaming_enabled(client.options): + return f(self, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GEN_AI_CREATE_AGENT, + origin=LanggraphIntegration.origin, + ) as span: + compiled_graph = f(self, *args, **kwargs) + + compiled_graph_name = getattr(compiled_graph, "name", None) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) + + if compiled_graph_name: + span.description = f"create_agent {compiled_graph_name}" + else: + span.description = "create_agent" + + if kwargs.get("model", None) is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) + + tools = None + get_graph = getattr(compiled_graph, "get_graph", None) + if get_graph and callable(get_graph): + graph_obj = compiled_graph.get_graph() + nodes = getattr(graph_obj, "nodes", None) + if nodes and isinstance(nodes, dict): + tools_node = nodes.get("tools") + if tools_node: + data = getattr(tools_node, "data", None) + if data and hasattr(data, "tools_by_name"): + tools = list(data.tools_by_name.keys()) + + if tools is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) + + return compiled_graph + + return new_compile + + +def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + else: + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_invoke + + +def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles(input_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_ainvoke + + +def _get_new_messages( + input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" +) -> "Optional[List[Any]]": + """Extract only the new messages added during this invocation.""" + if not output_messages: + return None + + if not input_messages: + return output_messages + + # only return the new messages, aka the output messages that are not in the input messages + input_count = len(input_messages) + new_messages = ( + output_messages[input_count:] if len(output_messages) > input_count else [] + ) + + return new_messages if new_messages else None + + +def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": + if not messages: + return None + + for message in reversed(messages): + if isinstance(message, dict): + role = message.get("role") + if role in ["assistant", "ai"]: + content = message.get("content") + if content and isinstance(content, str): + return content + + return None + + +def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": + if not messages: + return None + + tool_calls = [] + for message in messages: + if isinstance(message, dict): + msg_tool_calls = message.get("tool_calls") + if msg_tool_calls and isinstance(msg_tool_calls, list): + tool_calls.extend(msg_tool_calls) + + return tool_calls if tool_calls else None + + +def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + input_tokens = 0 + output_tokens = 0 + total_tokens = 0 + + for message in messages: + response_metadata = message.get("response_metadata") + if response_metadata is None: + continue + + token_usage = response_metadata.get("token_usage") + if not token_usage: + continue + + input_tokens += int(token_usage.get("prompt_tokens", 0)) + output_tokens += int(token_usage.get("completion_tokens", 0)) + total_tokens += int(token_usage.get("total_tokens", 0)) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens > 0: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + total_tokens, + ) + + +def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + if len(messages) == 0: + return + + last_message = messages[-1] + response_metadata = last_message.get("response_metadata") + if response_metadata is None: + return + + model_name = response_metadata.get("model_name") + if model_name is None: + return + + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) + + +def _set_response_attributes( + span: "Any", + input_messages: "Optional[List[Any]]", + result: "Any", + integration: "LanggraphIntegration", +) -> None: + parsed_response_messages = _parse_langgraph_messages(result) + new_messages = _get_new_messages(input_messages, parsed_response_messages) + + if new_messages is None: + return + + _set_usage_data(span, new_messages) + _set_response_model_name(span, new_messages) + + if not (should_send_default_pii() and integration.include_prompts): + return + + llm_response_text = _extract_llm_response_text(new_messages) + if llm_response_text: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) + elif new_messages: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) + else: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) + + tool_calls = _extract_tool_calls(new_messages) + if tool_calls: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(tool_calls), + unpack=False, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py new file mode 100644 index 0000000000..3c2c76450c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + import ldclient + from ldclient.hook import Hook, Metadata + + if TYPE_CHECKING: + from typing import Any + + from ldclient import LDClient + from ldclient.evaluation import EvaluationDetail + from ldclient.hook import EvaluationSeriesContext +except ImportError: + raise DidNotEnable("LaunchDarkly is not installed") + + +class LaunchDarklyIntegration(Integration): + identifier = "launchdarkly" + + def __init__(self, ld_client: "LDClient | None" = None) -> None: + """ + :param client: An initialized LDClient instance. If a client is not provided, this + integration will attempt to use the shared global instance. + """ + try: + client = ld_client or ldclient.get() + except Exception as exc: + raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) + + if not client.is_initialized(): + raise DidNotEnable("LaunchDarkly client is not initialized.") + + # Register the flag collection hook with the LD client. + client.add_hook(LaunchDarklyHook()) + + @staticmethod + def setup_once() -> None: + pass + + +class LaunchDarklyHook(Hook): + @property + def metadata(self) -> "Metadata": + return Metadata(name="sentry-flag-auditor") + + def after_evaluation( + self, + series_context: "EvaluationSeriesContext", + data: "dict[Any, Any]", + detail: "EvaluationDetail", + ) -> "dict[Any, Any]": + if isinstance(detail.value, bool): + add_feature_flag(series_context.key, detail.value) + + return data + + def before_evaluation( + self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" + ) -> "dict[Any, Any]": + return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py new file mode 100644 index 0000000000..49ead6b068 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py @@ -0,0 +1,376 @@ +import copy +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + set_data_normalized, + transform_openai_content_part, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from datetime import datetime + from typing import Any, Dict, List + +try: + import litellm # type: ignore[import-not-found] + from litellm import failure_callback, input_callback, success_callback +except ImportError: + raise DidNotEnable("LiteLLM not installed") + + +# Stash the span on a top-level key of the per-request kwargs dict litellm passes +# to every callback, so it lives and dies with the request. +_SPAN_KEY = "_sentry_span" + + +def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: + kwargs[_SPAN_KEY] = span + + +def _peek_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.get(_SPAN_KEY) + + +def _pop_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.pop(_SPAN_KEY, None) + + +def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": + """ + Convert the message parts from OpenAI format to the `gen_ai.request.messages` format + using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). + + Deep copies messages to avoid mutating original kwargs. + """ + # Deep copy to avoid mutating original messages from kwargs + messages = copy.deepcopy(messages) + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_openai_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + message["content"] = transformed + return messages + + +def _input_callback(kwargs: "Dict[str, Any]") -> None: + """Handle the start of a request.""" + client = sentry_sdk.get_client() + integration = client.get_integration(LiteLLMIntegration) + + if integration is None: + return + + # Get key parameters + full_model = kwargs.get("model", "") + try: + model, provider, _, _ = litellm.get_llm_provider(full_model) + except Exception: + model = full_model + provider = "unknown" + + call_type = kwargs.get("call_type", None) + if call_type == "embedding" or call_type == "aembedding": + operation = "embeddings" + else: + operation = "chat" + + # Start a new span/transaction + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"{operation} {model}", + attributes={ + "sentry.op": ( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + "sentry.origin": LiteLLMIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + name=f"{operation} {model}", + origin=LiteLLMIntegration.origin, + ) + span.__enter__() + + _store_span(kwargs, span) + + # Set basic data + set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) + + # Record input/messages if allowed + if should_send_default_pii() and integration.include_prompts: + if operation == "embeddings": + # For embeddings, look for the 'input' parameter + embedding_input = kwargs.get("input") + if embedding_input: + scope = sentry_sdk.get_current_scope() + # Normalize to list format + input_list = ( + embedding_input + if isinstance(embedding_input, list) + else [embedding_input] + ) + client = sentry_sdk.get_client() + messages_data = ( + truncate_and_annotate_embedding_inputs(input_list, span, scope) + if should_truncate_gen_ai_input(client.options) + else input_list + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + messages_data, + unpack=False, + ) + else: + # For chat, look for the 'messages' parameter + messages = kwargs.get("messages", []) + if messages: + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages = _convert_message_parts(messages) + messages_data = ( + truncate_and_annotate_messages(messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Record other parameters + params = { + "model": SPANDATA.GEN_AI_REQUEST_MODEL, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + } + for key, attribute in params.items(): + value = kwargs.get(key) + if value is not None: + set_data_normalized(span, attribute, value) + + +async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: + return _input_callback(kwargs) + + +def _success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle successful completion.""" + + span = _peek_span(kwargs) + if span is None: + return + + integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) + if integration is None: + return + + try: + # Record model information + if hasattr(completion_response, "model"): + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model + ) + + # Record response content if allowed + if should_send_default_pii() and integration.include_prompts: + if hasattr(completion_response, "choices"): + response_messages = [] + for choice in completion_response.choices: + if hasattr(choice, "message"): + if hasattr(choice.message, "model_dump"): + response_messages.append(choice.message.model_dump()) + elif hasattr(choice.message, "dict"): + response_messages.append(choice.message.dict()) + else: + # Fallback for basic message objects + msg = {} + if hasattr(choice.message, "role"): + msg["role"] = choice.message.role + if hasattr(choice.message, "content"): + msg["content"] = choice.message.content + if hasattr(choice.message, "tool_calls"): + msg["tool_calls"] = choice.message.tool_calls + response_messages.append(msg) + + if response_messages: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages + ) + + # Record token usage + if hasattr(completion_response, "usage"): + usage = completion_response.usage + record_token_usage( + span, + input_tokens=getattr(usage, "prompt_tokens", None), + output_tokens=getattr(usage, "completion_tokens", None), + total_tokens=getattr(usage, "total_tokens", None), + ) + + finally: + is_streaming = kwargs.get("stream") + # Callback is fired multiple times when streaming a response. + # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 + if ( + is_streaming is not True + or "complete_streaming_response" in kwargs + or "async_complete_streaming_response" in kwargs + ): + span = _pop_span(kwargs) + if span is not None: + span.__exit__(None, None, None) + + +async def _async_success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + return _success_callback( + kwargs, + completion_response, + start_time, + end_time, + ) + + +def _failure_callback( + kwargs: "Dict[str, Any]", + exception: Exception, + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle request failure.""" + span = _pop_span(kwargs) + if span is None: + return + + try: + # Capture the exception + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "litellm", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + finally: + # Always finish the span and clean up + span.__exit__(type(exception), exception, None) + + +class LiteLLMIntegration(Integration): + """ + LiteLLM integration for Sentry. + + This integration automatically captures LiteLLM API calls and sends them to Sentry + for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM + supports, including OpenAI, Anthropic, Google, Cohere, and many others. + + Features: + - Automatic exception capture for all LiteLLM calls + - Token usage tracking across all providers + - Provider detection and attribution + - Input/output message capture (configurable) + - Streaming response support + - Cost tracking integration + + Usage: + + ```python + import litellm + import sentry_sdk + + # Initialize Sentry with the LiteLLM integration + sentry_sdk.init( + dsn="your-dsn", + send_default_pii=True + integrations=[ + sentry_sdk.integrations.LiteLLMIntegration( + include_prompts=True # Set to False to exclude message content + ) + ] + ) + + # All LiteLLM calls will now be monitored + response = litellm.completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Hello!"}] + ) + ``` + + Configuration: + - include_prompts (bool): Whether to include prompts and responses in spans. + Defaults to True. Set to False to exclude potentially sensitive data. + """ + + identifier = "litellm" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """Set up LiteLLM callbacks for monitoring.""" + litellm.input_callback = input_callback or [] + if _input_callback not in litellm.input_callback: + litellm.input_callback.append(_input_callback) + if _async_input_callback not in litellm.input_callback: + litellm.input_callback.append(_async_input_callback) + + litellm.success_callback = success_callback or [] + if _success_callback not in litellm.success_callback: + litellm.success_callback.append(_success_callback) + if _async_success_callback not in litellm.success_callback: + litellm.success_callback.append(_async_success_callback) + + litellm.failure_callback = failure_callback or [] + if _failure_callback not in litellm.failure_callback: + litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py new file mode 100644 index 0000000000..f0c90a7921 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py @@ -0,0 +1,364 @@ +from collections.abc import Set +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from litestar import Litestar, Request # type: ignore + from litestar.data_extractors import ConnectionDataExtractor # type: ignore + from litestar.exceptions import HTTPException # type: ignore + from litestar.handlers.base import BaseRouteHandler # type: ignore + from litestar.middleware import DefineMiddleware # type: ignore + from litestar.routes.http import HTTPRoute # type: ignore +except ImportError: + raise DidNotEnable("Litestar is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from litestar.middleware import MiddlewareProtocol + from litestar.types import ( # type: ignore + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from litestar.types import ( + Scope as LitestarScope, + ) + from litestar.types.asgi_types import ASGIApp # type: ignore + + from sentry_sdk._types import Event, Hint + +_DEFAULT_TRANSACTION_NAME = "generic Litestar request" + + +class LitestarIntegration(Integration): + identifier = "litestar" + origin = f"auto.http.{identifier}" + + def __init__( + self, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. + # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following + # (among other things): + # 1. Logs them, some at least (such as 500s) as errors + # 2. Calls after_exception hooks + # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event + # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will + # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. + ignore_logger("litestar") + + +class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + def _capture_request_exception(self, exc: Exception) -> None: + """Avoid catching exceptions from request handlers. + + Those exceptions are already handled in Litestar.after_exception handler. + We still catch exceptions from application lifespan handlers. + """ + pass + + +def patch_app_init() -> None: + """ + Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. + See: + - https://docs.litestar.dev/2/usage/applications.html#after-exception + - https://docs.litestar.dev/2/usage/middleware/using-middleware.html + """ + old__init__ = Litestar.__init__ + + @ensure_integration_enabled(LitestarIntegration, old__init__) + def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: + kwargs["after_exception"] = [ + exception_handler, + *(kwargs.get("after_exception") or []), + ] + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Litestar.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryLitestarASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "LitestarScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(LitestarIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=middleware_name, + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR, + "sentry.origin": LitestarIntegration.origin, + }, + ) as middleware_span: + middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.traces.start_span( + name=getattr(receive, "__qualname__", str(receive)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.traces.start_span( + name=getattr(send, "__qualname__", str(send)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + else: + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR, + name=middleware_name, + origin=LitestarIntegration.origin, + ) as middleware_span: + middleware_span.set_tag("litestar.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_SEND, + name=getattr(send, "__qualname__", str(send)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type + elif hasattr(route_handler.fn, "value"): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = LitestarIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if isinstance(scope_user, dict): + return scope_user + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + return None + + +@ensure_integration_enabled(LitestarIntegration) +def exception_handler(exc: Exception, scope: "LitestarScope") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + if isinstance(exc, HTTPException): + integration = sentry_sdk.get_client().get_integration(LitestarIntegration) + if ( + integration is not None + and exc.status_code not in integration.failed_request_status_codes + ): + return + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": LitestarIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py new file mode 100644 index 0000000000..a310a0ced6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py @@ -0,0 +1,476 @@ +import logging +import sys +from datetime import datetime, timezone +from fnmatch import fnmatch +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.client import BaseClient +from sentry_sdk.integrations import Integration +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import ( + capture_internal_exceptions, + current_stacktrace, + event_from_exception, + has_logs_enabled, + safe_repr, + to_string, +) + +if TYPE_CHECKING: + from collections.abc import MutableMapping + from logging import LogRecord + from typing import Any, Dict, Optional + +DEFAULT_LEVEL = logging.INFO +DEFAULT_EVENT_LEVEL = logging.ERROR +LOGGING_TO_EVENT_LEVEL = { + logging.NOTSET: "notset", + logging.DEBUG: "debug", + logging.INFO: "info", + logging.WARN: "warning", # WARN is same a WARNING + logging.WARNING: "warning", + logging.ERROR: "error", + logging.FATAL: "fatal", + logging.CRITICAL: "fatal", # CRITICAL is same as FATAL +} + +# Map logging level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + logging.CRITICAL: 21, # fatal + logging.ERROR: 17, # error + logging.WARNING: 13, # warn + logging.INFO: 9, # info + logging.DEBUG: 5, # debug +} + + +# Capturing events from those loggers causes recursion errors. We cannot allow +# the user to unconditionally create events from those loggers under any +# circumstances. +# +# Note: Ignoring by logger name here is better than mucking with thread-locals. +# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. +# +# Events/breadcrumbs and Sentry Logs have separate ignore lists so that +# framework loggers silenced for events (e.g. django.server) can still be +# captured as Sentry Logs. +_IGNORED_LOGGERS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + +_IGNORED_LOGGERS_SENTRY_LOGS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + + +def ignore_logger( + name: str, +) -> None: + """This disables recording (both in breadcrumbs and as events) calls to + a logger of a specific name. Among other uses, many of our integrations + use this to prevent their actions being recorded as breadcrumbs. Exposed + to users as a way to quiet spammy loggers. + + This does **not** affect Sentry Logs — use + :py:func:`ignore_logger_for_sentry_logs` for that. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS.add(name) + + +def ignore_logger_for_sentry_logs( + name: str, +) -> None: + """This disables recording as Sentry Logs calls to a logger of a + specific name. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS_SENTRY_LOGS.add(name) + + +def unignore_logger( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger` call, re-enabling + recording of breadcrumbs and events for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS.discard(name) + + +def unignore_logger_for_sentry_logs( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, + re-enabling recording of Sentry Logs for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) + + +class LoggingIntegration(Integration): + identifier = "logging" + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + self._handler = None + self._breadcrumb_handler = None + self._sentry_logs_handler = None + + if level is not None: + self._breadcrumb_handler = BreadcrumbHandler(level=level) + + if sentry_logs_level is not None: + self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) + + if event_level is not None: + self._handler = EventHandler(level=event_level) + + def _handle_record(self, record: "LogRecord") -> None: + if self._handler is not None and record.levelno >= self._handler.level: + self._handler.handle(record) + + if ( + self._breadcrumb_handler is not None + and record.levelno >= self._breadcrumb_handler.level + ): + self._breadcrumb_handler.handle(record) + + def _handle_sentry_logs_record(self, record: "LogRecord") -> None: + if ( + self._sentry_logs_handler is not None + and record.levelno >= self._sentry_logs_handler.level + ): + self._sentry_logs_handler.handle(record) + + @staticmethod + def setup_once() -> None: + old_callhandlers = logging.Logger.callHandlers + + def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": + # keeping a local reference because the + # global might be discarded on shutdown + ignored_loggers = _IGNORED_LOGGERS + ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS + + try: + return old_callhandlers(self, record) + finally: + # This check is done twice, once also here before we even get + # the integration. Otherwise we have a high chance of getting + # into a recursion error when the integration is resolved + # (this also is slower). + name = record.name.strip() + + handle_events = ( + ignored_loggers is not None and name not in ignored_loggers + ) + handle_sentry_logs = ( + ignored_loggers_sentry_logs is not None + and name not in ignored_loggers_sentry_logs + ) + + if handle_events or handle_sentry_logs: + integration = sentry_sdk.get_client().get_integration( + LoggingIntegration + ) + if integration is not None: + if handle_events: + integration._handle_record(record) + if handle_sentry_logs: + integration._handle_sentry_logs_record(record) + + logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore + + +class _BaseHandler(logging.Handler): + COMMON_RECORD_ATTRS = frozenset( + ( + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "linenno", + "lineno", + "message", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack", + "tags", + "taskName", + "thread", + "threadName", + "stack_info", + ) + ) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + return LOGGING_TO_EVENT_LEVEL.get( + record.levelno, record.levelname.lower() if record.levelname else "" + ) + + def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": + return { + k: v + for k, v in vars(record).items() + if k not in self.COMMON_RECORD_ATTRS + and (not isinstance(k, str) or not k.startswith("_")) + } + + +class EventHandler(_BaseHandler): + """ + A logging handler that emits Sentry events for each log record + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + client_options = client.options + + # exc_info might be None or (None, None, None) + # + # exc_info may also be any falsy value due to Python stdlib being + # liberal with what it receives and Celery's billiard being "liberal" + # with what it sends. See + # https://github.com/getsentry/sentry-python/issues/904 + if record.exc_info and record.exc_info[0] is not None: + event, hint = event_from_exception( + record.exc_info, + client_options=client_options, + mechanism={"type": "logging", "handled": True}, + ) + elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: + event = {} + hint = {} + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=client_options[ + "include_local_variables" + ], + max_value_length=client_options["max_value_length"], + ), + "crashed": False, + "current": True, + } + ] + } + else: + event = {} + hint = {} + + hint["log_record"] = record + + level = self._logging_to_event_level(record) + if level in {"debug", "info", "warning", "error", "critical", "fatal"}: + event["level"] = level # type: ignore[typeddict-item] + event["logger"] = record.name + + if ( + sys.version_info < (3, 11) + and record.name == "py.warnings" + and record.msg == "%s" + ): + # warnings module on Python 3.10 and below sets record.msg to "%s" + # and record.args[0] to the actual warning message. + # This was fixed in https://github.com/python/cpython/pull/30975. + message = record.args[0] + params = () + else: + message = record.msg + params = record.args + + event["logentry"] = { + "message": to_string(message), + "formatted": record.getMessage(), + "params": params, + } + + event["extra"] = self._extra_from_record(record) + + sentry_sdk.capture_event(event, hint=hint) + + +# Legacy name +SentryHandler = EventHandler + + +class BreadcrumbHandler(_BaseHandler): + """ + A logging handler that records breadcrumbs for each log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + sentry_sdk.add_breadcrumb( + self._breadcrumb_from_record(record), hint={"log_record": record} + ) + + def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": + return { + "type": "log", + "level": self._logging_to_event_level(record), + "category": record.name, + "message": record.message, + "timestamp": datetime.fromtimestamp(record.created, timezone.utc), + "data": self._extra_from_record(record), + } + + +class SentryLogsHandler(_BaseHandler): + """ + A logging handler that records Sentry logs for each Python log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS_SENTRY_LOGS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + self._capture_log_from_record(client, record) + + def _capture_log_from_record( + self, client: "BaseClient", record: "LogRecord" + ) -> None: + otel_severity_number, otel_severity_text = _log_level_to_otel( + record.levelno, SEVERITY_TO_OTEL_SEVERITY + ) + project_root = client.options["project_root"] + + attrs: "Any" = self._extra_from_record(record) + attrs["sentry.origin"] = "auto.log.stdlib" + + parameters_set = False + if record.args is not None: + if isinstance(record.args, tuple): + parameters_set = bool(record.args) + for i, arg in enumerate(record.args): + attrs[f"sentry.message.parameter.{i}"] = ( + arg + if isinstance(arg, (str, float, int, bool)) + else safe_repr(arg) + ) + elif isinstance(record.args, dict): + parameters_set = bool(record.args) + for key, value in record.args.items(): + attrs[f"sentry.message.parameter.{key}"] = ( + value + if isinstance(value, (str, float, int, bool)) + else safe_repr(value) + ) + + if parameters_set and isinstance(record.msg, str): + # only include template if there is at least one + # sentry.message.parameter.X set + attrs["sentry.message.template"] = record.msg + + if record.lineno: + attrs["code.line.number"] = record.lineno + + if record.pathname: + if project_root is not None and record.pathname.startswith(project_root): + attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record.pathname + + if record.funcName: + attrs["code.function.name"] = record.funcName + + if record.thread: + attrs["thread.id"] = record.thread + if record.threadName: + attrs["thread.name"] = record.threadName + + if record.process: + attrs["process.pid"] = record.process + if record.processName: + attrs["process.executable.name"] = record.processName + if record.name: + attrs["logger.name"] = record.name + + # noinspection PyProtectedMember + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record.message, + "attributes": attrs, + "time_unix_nano": int(record.created * 1e9), + "trace_id": None, + "span_id": None, + }, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py new file mode 100644 index 0000000000..dbb724d9a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py @@ -0,0 +1,208 @@ +import enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.logging import ( + BreadcrumbHandler, + EventHandler, + _BaseHandler, +) +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import has_logs_enabled, safe_repr + +if TYPE_CHECKING: + from logging import LogRecord + from typing import Any, Optional + +try: + import loguru + from loguru import logger + from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT + + if TYPE_CHECKING: + from loguru import Message +except ImportError: + raise DidNotEnable("LOGURU is not installed") + + +class LoggingLevels(enum.IntEnum): + TRACE = 5 + DEBUG = 10 + INFO = 20 + SUCCESS = 25 + WARNING = 30 + ERROR = 40 + CRITICAL = 50 + + +DEFAULT_LEVEL = LoggingLevels.INFO.value +DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value + + +SENTRY_LEVEL_FROM_LOGURU_LEVEL = { + "TRACE": "DEBUG", + "DEBUG": "DEBUG", + "INFO": "INFO", + "SUCCESS": "INFO", + "WARNING": "WARNING", + "ERROR": "ERROR", + "CRITICAL": "CRITICAL", +} + +# Map Loguru level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + LoggingLevels.CRITICAL: 21, # fatal + LoggingLevels.ERROR: 17, # error + LoggingLevels.WARNING: 13, # warn + LoggingLevels.SUCCESS: 11, # info + LoggingLevels.INFO: 9, # info + LoggingLevels.DEBUG: 5, # debug + LoggingLevels.TRACE: 1, # trace +} + + +class LoguruIntegration(Integration): + identifier = "loguru" + + level: "Optional[int]" = DEFAULT_LEVEL + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL + breadcrumb_format = DEFAULT_FORMAT + event_format = DEFAULT_FORMAT + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + LoguruIntegration.level = level + LoguruIntegration.event_level = event_level + LoguruIntegration.breadcrumb_format = breadcrumb_format + LoguruIntegration.event_format = event_format + LoguruIntegration.sentry_logs_level = sentry_logs_level + + @staticmethod + def setup_once() -> None: + if LoguruIntegration.level is not None: + logger.add( + LoguruBreadcrumbHandler(level=LoguruIntegration.level), + level=LoguruIntegration.level, + format=LoguruIntegration.breadcrumb_format, + ) + + if LoguruIntegration.event_level is not None: + logger.add( + LoguruEventHandler(level=LoguruIntegration.event_level), + level=LoguruIntegration.event_level, + format=LoguruIntegration.event_format, + ) + + if LoguruIntegration.sentry_logs_level is not None: + logger.add( + loguru_sentry_logs_handler, + level=LoguruIntegration.sentry_logs_level, + ) + + +class _LoguruBaseHandler(_BaseHandler): + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + if kwargs.get("level"): + kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( + kwargs.get("level", ""), DEFAULT_LEVEL + ) + + super().__init__(*args, **kwargs) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + try: + return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ + LoggingLevels(record.levelno).name + ].lower() + except (ValueError, KeyError): + return record.levelname.lower() if record.levelname else "" + + +class LoguruEventHandler(_LoguruBaseHandler, EventHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" + + pass + + +class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" + + pass + + +def loguru_sentry_logs_handler(message: "Message") -> None: + # This is intentionally a callable sink instead of a standard logging handler + # since otherwise we wouldn't get direct access to message.record + client = sentry_sdk.get_client() + + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + record = message.record + + if ( + LoguruIntegration.sentry_logs_level is None + or record["level"].no < LoguruIntegration.sentry_logs_level + ): + return + + otel_severity_number, otel_severity_text = _log_level_to_otel( + record["level"].no, SEVERITY_TO_OTEL_SEVERITY + ) + + attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} + + project_root = client.options["project_root"] + if record.get("file"): + if project_root is not None and record["file"].path.startswith(project_root): + attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record["file"].path + + if record.get("line") is not None: + attrs["code.line.number"] = record["line"] + + if record.get("function"): + attrs["code.function.name"] = record["function"] + + if record.get("thread"): + attrs["thread.name"] = record["thread"].name + attrs["thread.id"] = record["thread"].id + + if record.get("process"): + attrs["process.pid"] = record["process"].id + attrs["process.executable.name"] = record["process"].name + + if record.get("name"): + attrs["logger.name"] = record["name"] + + extra = record.get("extra") + if isinstance(extra, dict): + for key, value in extra.items(): + if isinstance(value, (str, int, float, bool)): + attrs[f"sentry.message.parameter.{key}"] = value + else: + attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record["message"], + "attributes": attrs, + "time_unix_nano": int(record["time"].timestamp() * 1e9), + "trace_id": None, + "span_id": None, + } + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py new file mode 100644 index 0000000000..79381fe06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py @@ -0,0 +1,876 @@ +""" +Sentry integration for MCP (Model Context Protocol) servers. + +This integration instruments MCP servers to create spans for tool, prompt, +and resource handler execution, and captures errors that occur during execution. + +Supports the low-level `mcp.server.lowlevel.Server` API. +""" + +import inspect +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import package_version, safe_serialize + +MCP_PACKAGE_VERSION = package_version("mcp") + +try: + from mcp.server.lowlevel import Server + from mcp.server.streamable_http import ( + StreamableHTTPServerTransport, + ) + + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] + request_ctx, + ) +except ImportError: + raise DidNotEnable("MCP SDK not installed") + +try: + from fastmcp import FastMCP # type: ignore[import-not-found] +except ImportError: + FastMCP = None + +if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + try: + from mcp.server.context import ( + ServerRequestContext, + ) + except ImportError: + ServerRequestContext = None # type: ignore[assignment,misc] +else: + ServerRequestContext = None # type: ignore[assignment,misc] + + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Optional, Tuple, Union + + from starlette.types import Receive, Scope, Send + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +class MCPIntegration(Integration): + identifier = "mcp" + origin = "auto.ai.mcp" + + def __init__(self, include_prompts: bool = True) -> None: + """ + Initialize the MCP integration. + + Args: + include_prompts: Whether to include prompts (tool results and prompt content) + in span data. Requires send_default_pii=True. Default is True. + """ + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """ + Patches MCP server classes to instrument handler execution. + """ + _patch_lowlevel_server() + _patch_handle_request() + + if FastMCP is not None: + _patch_fastmcp() + + +def _get_active_http_scopes( + ctx: "Optional[Any]" = None, +) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return None + + if ( + ctx is None + or not hasattr(ctx, "request") + or ctx.request is None + or "state" not in ctx.request.scope + ): + return None + + return ( + ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), + ctx.request.scope["state"].get("sentry_sdk.current_scope"), + ) + + +def _get_request_context_data( + ctx: "Optional[Any]" = None, +) -> "tuple[Optional[str], Optional[str], str]": + """ + Extract request ID, session ID, and MCP transport type from the request context. + + Returns: + Tuple of (request_id, session_id, mcp_transport). + - request_id: May be None if not available + - session_id: May be None if not available + - mcp_transport: "http", "sse", "stdio" + """ + request_id: "Optional[str]" = None + session_id: "Optional[str]" = None + mcp_transport: str = "stdio" + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return request_id, session_id, mcp_transport + + if ctx is not None: + request_id = ctx.request_id + if hasattr(ctx, "request") and ctx.request is not None: + request = ctx.request + # Detect transport type by checking request characteristics + if hasattr(request, "query_params") and request.query_params.get( + "session_id" + ): + # SSE transport uses query parameter + mcp_transport = "sse" + session_id = request.query_params.get("session_id") + elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): + # StreamableHTTP transport uses header + mcp_transport = "http" + session_id = request.headers.get("mcp-session-id") + + return request_id, session_id, mcp_transport + + +def _get_span_config( + handler_type: str, item_name: str +) -> "tuple[str, str, str, Optional[str]]": + """ + Get span configuration based on handler type. + + Returns: + Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) + Note: result_data_key is None for resources + """ + if handler_type == "tool": + span_data_key = SPANDATA.MCP_TOOL_NAME + mcp_method_name = "tools/call" + result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT + elif handler_type == "prompt": + span_data_key = SPANDATA.MCP_PROMPT_NAME + mcp_method_name = "prompts/get" + result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT + else: # resource + span_data_key = SPANDATA.MCP_RESOURCE_URI + mcp_method_name = "resources/read" + result_data_key = None # Resources don't capture result content + + span_name = f"{mcp_method_name} {item_name}" + return span_data_key, span_name, mcp_method_name, result_data_key + + +def _set_span_input_data( + span: "Union[StreamedSpan, Span]", + handler_name: str, + span_data_key: str, + mcp_method_name: str, + arguments: "dict[str, Any]", + request_id: "Optional[str]", + session_id: "Optional[str]", + mcp_transport: str, +) -> None: + """Set input span data for MCP handlers.""" + + # Set handler identifier + _set_span_data_attribute(span, span_data_key, handler_name) + _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) + + # Set transport/MCP transport type + _set_span_data_attribute( + span, + SPANDATA.NETWORK_TRANSPORT, + "pipe" if mcp_transport == "stdio" else "tcp", + ) + _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) + + # Set request_id if provided + if request_id: + _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) + + # Set session_id if provided + if session_id: + _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) + + # Set request arguments (excluding common request context objects) + for k, v in arguments.items(): + _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) + + +def _extract_tool_result_content(result: "Any") -> "Any": + """ + Extract meaningful content from MCP tool result. + + Tool handlers can return: + - CallToolResult (mcp v2+): Has .content list and optional .structured_content + - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) + - dict (StructuredContent): Return as-is + - list/Iterable (UnstructuredContent): Extract text from content blocks + """ + if result is None: + return None + + # Handle v2 CallToolResult-like objects (has .content list attribute) + if hasattr(result, "content") and isinstance( + getattr(result, "content", None), list + ): + # This is only present when a tool declares an output_schema + structured = getattr(result, "structured_content", None) + if structured is not None: + return structured + return _extract_text_from_content_blocks(result.content) + + # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) + if isinstance(result, tuple) and len(result) == 2: + # Return the structured content (2nd element) + return result[1] + + # Handle StructuredContent: dict + if isinstance(result, dict): + return result + + # Handle UnstructuredContent: iterable of ContentBlock objects + if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): + return _extract_text_from_content_blocks(result) + + return result + + +def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": + texts = [] + try: + for item in content_blocks: + if hasattr(item, "text"): + texts.append(item.text) + elif isinstance(item, dict) and "text" in item: + texts.append(item["text"]) + except Exception: + return content_blocks + return " ".join(texts) if texts else content_blocks + + +def _set_span_output_data( + span: "Union[StreamedSpan, Span]", + result: "Any", + result_data_key: "Optional[str]", + handler_type: str, +) -> None: + """Set output span data for MCP handlers.""" + if result is None: + return + + # Get integration to check PII settings + integration = sentry_sdk.get_client().get_integration(MCPIntegration) + if integration is None: + return + + # Check if we should include sensitive data + should_include_data = should_send_default_pii() and integration.include_prompts + + # For tools, extract the meaningful content + if handler_type == "tool": + extracted = _extract_tool_result_content(result) + if ( + extracted is not None + and should_include_data + and result_data_key is not None + ): + _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) + # Set content count if result is a dict + if isinstance(extracted, dict): + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) + ) + elif handler_type == "prompt": + # For prompts, count messages and set role/content only for single-message prompts + try: + messages: "Optional[list[str]]" = None + message_count = 0 + + # Check if result has messages attribute (GetPromptResult) + if hasattr(result, "messages") and result.messages: + messages = result.messages + message_count = len(messages) + # Also check if result is a dict with messages + elif isinstance(result, dict) and result.get("messages"): + messages = result["messages"] + message_count = len(messages) + + # Always set message count if we found messages + if message_count > 0: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count + ) + + # Only set role and content for single-message prompts if PII is allowed + if message_count == 1 and should_include_data and messages: + first_message = messages[0] + # Extract role + role = None + if hasattr(first_message, "role"): + role = first_message.role + elif isinstance(first_message, dict) and "role" in first_message: + role = first_message["role"] + + if role: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role + ) + + # Extract content text + content_text = None + if hasattr(first_message, "content"): + msg_content = first_message.content + # Content can be a TextContent object or similar + if hasattr(msg_content, "text"): + content_text = msg_content.text + elif isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + elif isinstance(first_message, dict) and "content" in first_message: + msg_content = first_message["content"] + if isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + + if content_text and result_data_key is not None: + _set_span_data_attribute(span, result_data_key, content_text) + except Exception: + # Silently ignore if we can't extract message info + pass + # Resources don't capture result content (result_data_key is None) + + +# Handler data preparation and wrapping + + +def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: + """Check if original_args contains a v2 ServerRequestContext as the first element.""" + return ( + ServerRequestContext is not None + and bool(original_args) + and isinstance(original_args[0], ServerRequestContext) + ) + + +def _extract_handler_data_from_params( + handler_type: str, + params: "Any", +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from a v2 typed params object. + + In MCP SDK v2, handlers receive (ctx, params) where params is a typed + Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). + """ + if handler_type == "tool": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + elif handler_type == "prompt": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + arguments = {"name": handler_name, **arguments} + else: # resource + handler_name = str(getattr(params, "uri", "unknown")) + arguments = {} + + return handler_name, arguments + + +def _extract_handler_data_from_args( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from v1 positional args. + + In MCP SDK v1, handlers receive positional args: + - Tool: (tool_name, arguments) + - Prompt: (name, arguments) + - Resource: (uri,) + """ + original_kwargs = original_kwargs or {} + + if handler_type == "tool": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + elif handler_type == "prompt": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + arguments = {"name": handler_name, **(arguments or {})} + + else: # resource + handler_name = "unknown" + if original_args: + handler_name = str(original_args[0]) + elif original_kwargs.get("uri"): + handler_name = str(original_kwargs["uri"]) + + arguments = {} + + return handler_name, arguments + + +def _prepare_handler_data( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + params: "Optional[Any]" = None, +) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": + """ + Prepare common handler data for both v1 and v2 MCP SDK. + + Args: + handler_type: "tool", "prompt", or "resource" + original_args: Original positional args (v1 path) + original_kwargs: Original keyword args (v1 path) + params: Typed params object from v2 ServerRequestContext path + + Returns: + Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) + """ + if params is not None: + handler_name, arguments = _extract_handler_data_from_params( + handler_type, params + ) + elif _is_v2_context(original_args): + handler_name = "unknown" + arguments = {} + else: + handler_name, arguments = _extract_handler_data_from_args( + handler_type, original_args, original_kwargs + ) + + span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( + handler_type, handler_name + ) + + return ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) + + +async def _handler_wrapper( + handler_type: str, + func: "Callable[..., Any]", + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + self: "Optional[Any]" = None, + force_await: bool = True, +) -> "Any": + """ + Wrapper for MCP handlers. + + Args: + handler_type: "tool", "prompt", or "resource" + func: The handler function to wrap + original_args: Original arguments passed to the handler + original_kwargs: Original keyword arguments passed to the handler + self: Optional instance for bound methods + """ + if original_kwargs is None: + original_kwargs = {} + + # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers + ctx: "Optional[Any]" = None + params: "Optional[Any]" = None + if ( + ServerRequestContext is not None + and original_args + and isinstance(original_args[0], ServerRequestContext) + ): + ctx = original_args[0] + params = original_args[1] if len(original_args) > 1 else None + + ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) = _prepare_handler_data( + handler_type, original_args, original_kwargs, params=params + ) + + scopes = _get_active_http_scopes(ctx=ctx) + + isolation_scope_context: "ContextManager[Any]" + current_scope_context: "ContextManager[Any]" + + if scopes is None: + isolation_scope_context = nullcontext() + current_scope_context = nullcontext() + else: + isolation_scope, current_scope = scopes + + isolation_scope_context = ( + nullcontext() + if isolation_scope is None + else sentry_sdk.scope.use_isolation_scope(isolation_scope) + ) + current_scope_context = ( + nullcontext() + if current_scope is None + else sentry_sdk.scope.use_scope(current_scope) + ) + + # Get request ID, session ID, and transport from context + request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + + # Start span and execute + with isolation_scope_context: + with current_scope_context: + span_mgr: "Union[Span, StreamedSpan]" + if span_streaming: + span_mgr = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.MCP_SERVER, + "sentry.origin": MCPIntegration.origin, + }, + ) + else: + span_mgr = get_start_span_function()( + op=OP.MCP_SERVER, + name=span_name, + origin=MCPIntegration.origin, + ) + + with span_mgr as span: + # Set input span data + _set_span_input_data( + span, + handler_name, + span_data_key, + mcp_method_name, + arguments, + request_id, + session_id, + mcp_transport, + ) + + # For resources, extract and set protocol + if handler_type == "resource": + uri = None + if params is not None: + uri = getattr(params, "uri", None) + + # v1 scenario + if ServerRequestContext is None: + if original_args: + uri = original_args[0] + else: + uri = original_kwargs.get("uri") + + protocol = None + if uri is not None and hasattr(uri, "scheme"): + protocol = uri.scheme + elif handler_name and "://" in handler_name: + protocol = handler_name.split("://")[0] + if protocol: + _set_span_data_attribute( + span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol + ) + + try: + # Execute the async handler + if self is not None: + original_args = (self, *original_args) + + result = func(*original_args, **original_kwargs) + if force_await or inspect.isawaitable(result): + result = await result + + except Exception as e: + # Set error flag for tools + if handler_type == "tool": + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True + ) + sentry_sdk.capture_exception(e) + raise + + _set_span_output_data(span, result, result_data_key, handler_type) + + return result + + +def _create_instrumented_decorator( + original_decorator: "Callable[..., Any]", + handler_type: str, + *decorator_args: "Any", + **decorator_kwargs: "Any", +) -> "Callable[..., Any]": + """ + Create an instrumented version of an MCP decorator. + + This function intercepts MCP decorators (like @server.call_tool()) and injects + Sentry instrumentation into the handler registration flow. The returned decorator + will: + 1. Receive the user's handler function + 2. Pass the instrumented version to the original MCP decorator + + This ensures that when the handler is called at runtime, it's already wrapped + with Sentry spans and metrics collection. + + Args: + original_decorator: The original MCP decorator method (e.g., Server.call_tool) + handler_type: "tool", "prompt", or "resource" - determines span configuration + decorator_args: Positional arguments to pass to the original decorator (e.g., self) + decorator_kwargs: Keyword arguments to pass to the original decorator + + Returns: + A decorator function that instruments handlers before registering them + """ + + def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(func) + async def wrapper(*args: "Any") -> "Any": + return await _handler_wrapper(handler_type, func, args, force_await=False) + + # Then register it with the original MCP decorator + return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) + + return instrumented_decorator + + +_METHOD_TO_HANDLER_TYPE = { + "tools/call": "tool", + "prompts/get": "prompt", + "resources/read": "resource", +} + +# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via +# the Server(...) constructor kwargs rather than add_request_handler. The in-tree +# high-level MCPServer also wires its handlers through these kwargs. +_KWARG_TO_HANDLER_TYPE = { + "on_call_tool": "tool", + "on_get_prompt": "prompt", + "on_read_resource": "resource", +} + + +def _patch_lowlevel_server() -> None: + """ + Patches the mcp.server.lowlevel.Server class to instrument handler execution. + """ + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + _patch_lowlevel_server_v2() + else: + _patch_lowlevel_server_v1() + + +def _patch_lowlevel_server_v1() -> None: + """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" + # Patch call_tool decorator + original_call_tool = Server.call_tool # type: ignore[attr-defined] + + def patched_call_tool( + self: "Server", **kwargs: "Any" + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.call_tool that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_call_tool, "tool", self, **kwargs + )(func) + + Server.call_tool = patched_call_tool # type: ignore[attr-defined] + + # Patch get_prompt decorator + original_get_prompt = Server.get_prompt # type: ignore[attr-defined] + + def patched_get_prompt( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.get_prompt that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_get_prompt, "prompt", self + )(func) + + Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] + + # Patch read_resource decorator + original_read_resource = Server.read_resource # type: ignore[attr-defined] + + def patched_read_resource( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.read_resource that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_read_resource, "resource", self + )(func) + + Server.read_resource = patched_read_resource # type: ignore[attr-defined] + + +def _wrap_v2_handler( + handler_type: str, handler: "Callable[..., Any]" +) -> "Callable[..., Any]": + """Wrap a v2 (ctx, params) handler with Sentry instrumentation. + + Idempotent: an already-wrapped handler is returned unchanged so handlers + registered through more than one path (e.g. MCPServer building a Server) + are not double-wrapped. + """ + if getattr(handler, "__sentry_mcp_wrapped__", False): + return handler + + @wraps(handler) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + return await _handler_wrapper( + handler_type, handler, args, kwargs, force_await=False + ) + + wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] + return wrapper + + +def _patch_lowlevel_server_v2() -> None: + """Patches the v2 Server to wrap tool/prompt/resource handlers. + + Handlers can be registered either via the Server(...) constructor kwargs + (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree + MCPServer and most lowlevel examples use — or via add_request_handler. + Both are patched. + """ + original_init = Server.__init__ + + @wraps(original_init) + def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: + for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): + handler = kwargs.get(kwarg) + if handler is not None: + kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) + original_init(self, *args, **kwargs) + + Server.__init__ = patched_init # type: ignore[method-assign] + + original_add_request_handler = Server.add_request_handler + + def patched_add_request_handler( + self: "Server", + method: str, + params_type: "Any", + handler: "Callable[..., Any]", + *args: "Any", + **kwargs: "Any", + ) -> None: + handler_type = _METHOD_TO_HANDLER_TYPE.get(method) + if handler_type is not None: + handler = _wrap_v2_handler(handler_type, handler) + + original_add_request_handler( + self, method, params_type, handler, *args, **kwargs + ) + + Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] + + +def _patch_handle_request() -> None: + original_handle_request = StreamableHTTPServerTransport.handle_request + + @wraps(original_handle_request) + async def patched_handle_request( + self: "StreamableHTTPServerTransport", + scope: "Scope", + receive: "Receive", + send: "Send", + ) -> None: + scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( + sentry_sdk.get_isolation_scope() + ) + scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() + await original_handle_request(self, scope, receive, send) + + StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] + + +def _patch_fastmcp() -> None: + """ + Patches the standalone fastmcp package's FastMCP class. + + The standalone fastmcp package (v2.14.0+) registers its own handlers for + prompts and resources directly, bypassing the Server decorators we patch. + This function patches the _get_prompt_mcp and _read_resource_mcp methods + to add instrumentation for those handlers. + """ + if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): + original_get_prompt_mcp = FastMCP._get_prompt_mcp + + @wraps(original_get_prompt_mcp) + async def patched_get_prompt_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "prompt", + original_get_prompt_mcp, + args, + kwargs, + self, + ) + + FastMCP._get_prompt_mcp = patched_get_prompt_mcp + + if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): + original_read_resource_mcp = FastMCP._read_resource_mcp + + @wraps(original_read_resource_mcp) + async def patched_read_resource_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "resource", + original_read_resource_mcp, + args, + kwargs, + self, + ) + + FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py new file mode 100644 index 0000000000..b6111492bb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import _get_installed_modules + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + + +class ModulesIntegration(Integration): + identifier = "modules" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Any") -> "Event": + if event.get("type") == "transaction": + return event + + if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: + return event + + event["modules"] = _get_installed_modules() + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py new file mode 100644 index 0000000000..186c665ed1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py @@ -0,0 +1,1530 @@ +import json +import sys +import time +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai._openai_completions_api import ( + _get_system_instructions as _get_system_instructions_completions, +) +from sentry_sdk.ai._openai_completions_api import ( + _get_text_items, + _transform_system_instructions, +) +from sentry_sdk.ai._openai_completions_api import ( + _is_system_instruction as _is_system_instruction_completions, +) +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions as _get_system_instructions_responses, +) +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction as _is_system_instruction_responses, +) +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, + safe_serialize, +) + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Iterable, + Iterator, + List, + Optional, + Union, + ) + + from openai import Omit + from openai.types import CompletionUsage + from openai.types.responses import ( + ResponseInputParam, + ResponseStreamEvent, + SequenceNotStr, + ) + from openai.types.responses.response_usage import ResponseUsage + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +try: + try: + from openai import NotGiven + except ImportError: + NotGiven = None + + try: + from openai import Omit + except ImportError: + Omit = None + + from openai import AsyncStream, Stream + from openai.resources import AsyncEmbeddings, Embeddings + from openai.resources.chat.completions import AsyncCompletions, Completions + + if TYPE_CHECKING: + from openai.types.chat import ( + ChatCompletionChunk, + ChatCompletionMessageParam, + ) +except ImportError: + raise DidNotEnable("OpenAI not installed") + +RESPONSES_API_ENABLED = True +try: + # responses API support was introduced in v1.66.0 + from openai.resources.responses import AsyncResponses, Responses + from openai.types.responses.response_completed_event import ResponseCompletedEvent +except ImportError: + RESPONSES_API_ENABLED = False + + +class OpenAIIntegration(Integration): + identifier = "openai" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "OpenAIIntegration", + include_prompts: bool = True, + tiktoken_encoding_name: "Optional[str]" = None, + ) -> None: + self.include_prompts = include_prompts + + self.tiktoken_encoding = None + if tiktoken_encoding_name is not None: + import tiktoken # type: ignore + + self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) + + @staticmethod + def setup_once() -> None: + Completions.create = _wrap_chat_completion_create(Completions.create) + AsyncCompletions.create = _wrap_async_chat_completion_create( + AsyncCompletions.create + ) + + Embeddings.create = _wrap_embeddings_create(Embeddings.create) + AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) + + if RESPONSES_API_ENABLED: + Responses.create = _wrap_responses_create(Responses.create) + AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) + + def count_tokens(self: "OpenAIIntegration", s: str) -> int: + if self.tiktoken_encoding is None: + return 0 + try: + return len(self.tiktoken_encoding.encode_ordinary(s)) + except Exception: + return 0 + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _has_attr_and_is_int( + token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str +) -> bool: + return hasattr(token_usage, attr_name) and isinstance( + getattr(token_usage, attr_name, None), int + ) + + +def _calculate_completions_token_usage( + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + streaming_message_total_token_usage: "Optional[CompletionUsage]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Chat Completions API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + usage = None + + if streaming_message_total_token_usage is not None: + usage = streaming_message_total_token_usage + elif hasattr(response, "usage"): + usage = response.usage + + if usage is not None: + if _has_attr_and_is_int(usage, "prompt_tokens"): + input_tokens = usage.prompt_tokens + if _has_attr_and_is_int(usage, "completion_tokens"): + output_tokens = usage.completion_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "prompt_tokens_details"): + cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "completion_tokens_details"): + reasoning = getattr( + usage.completion_tokens_details, "reasoning_tokens", None + ) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in messages or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "choices") and response.choices is not None: + for choice in response.choices: + if hasattr(choice, "message") and hasattr(choice.message, "content"): + output_tokens += count_tokens(choice.message.content) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _calculate_responses_token_usage( + input: "Any", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Responses API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + + if hasattr(response, "usage"): + usage = response.usage + + if _has_attr_and_is_int(usage, "input_tokens"): + input_tokens = usage.input_tokens + if _has_attr_and_is_int(usage, "output_tokens"): + output_tokens = usage.output_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "input_tokens_details"): + cached = getattr(usage.input_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "output_tokens_details"): + reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in input or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + # Deliberate use of Completions function for both Completions and Responses input format. + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "output"): + for output_item in response.output: + if hasattr(output_item, "content"): + for content_item in output_item.content: + if hasattr(content_item, "text"): + output_tokens += count_tokens(content_item.text) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _set_responses_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") + messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_output_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + conversation = kwargs.get("conversation") + if conversation is not None and _is_given(conversation): + conversation_id: "Optional[str]" = None + if isinstance(conversation, str): + conversation_id = conversation + elif isinstance(conversation, dict): + conversation_id = conversation.get("id") + if conversation_id is not None: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + if not should_send_default_pii() or not integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if ( + messages is None + and explicit_instructions is not None + and _is_given(explicit_instructions) + ): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + [ + { + "type": "text", + "content": explicit_instructions, + } + ] + ), + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if messages is None: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + instructions_text_parts: "list[TextPart]" = [] + if explicit_instructions is not None and _is_given(explicit_instructions): + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions_responses(messages) + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + non_system_messages = [ + message for message in messages if not _is_system_instruction_responses(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + + +def _set_completions_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( + "messages" + ) + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + presence_penalty = kwargs.get("presence_penalty") + if presence_penalty is not None and _is_given(presence_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) + + frequency_penalty = kwargs.get("frequency_penalty") + if frequency_penalty is not None and _is_given(frequency_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + messages = list(messages) + kwargs["messages"] = messages + + system_instructions = _get_system_instructions_completions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + non_system_messages = [ + message + for message in messages + if not _is_system_instruction_completions(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + +def _set_embeddings_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( + "input" + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + return + + if isinstance(messages, str): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + return + + messages = list(messages) + kwargs["input"] = messages + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + +def _set_common_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + input: "Any", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + if hasattr(response, "model"): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) + + # Chat Completions API + if hasattr(response, "choices") and response.choices is not None: + if should_send_default_pii() and integration.include_prompts: + response_text = [ + choice.message.model_dump() + for choice in response.choices + if choice.message is not None + ] + if len(response_text) > 0: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) + + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + # Responses API + elif hasattr(response, "output"): + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in response.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + # Embeddings API (fallback for responses with neither choices nor output) + else: + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_synchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_asynchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _set_completions_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + _set_common_output_data( + span, + response, + messages, + integration, + finish_span, + ) + + +def _wrap_synchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Stream[ChatCompletionChunk]", + old_iterator: "Iterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "Iterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "AsyncStream[ChatCompletionChunk]", + old_iterator: "AsyncIterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "AsyncIterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + async for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +def _wrap_synchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "Stream[ResponseStreamEvent]", + old_iterator: "Iterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "Iterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "AsyncStream[ResponseStreamEvent]", + old_iterator: "AsyncIterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "AsyncIterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft: "Optional[float]" = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + async for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _set_responses_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _set_embeddings_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + return _new_sync_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + return await _new_async_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +async def _new_async_embeddings_create( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +def _wrap_embeddings_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_embeddings_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_synchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_asynchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _wrap_responses_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_responses_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_responses_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_responses_create(f, *args, **kwargs) + + return _sentry_patched_responses_async + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different openai versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py new file mode 100644 index 0000000000..5895f53ad3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py @@ -0,0 +1,250 @@ +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .patches import ( + _create_run_streamed_wrapper, + _create_run_wrapper, + _execute_final_output, + _execute_handoffs, + _get_all_tools, + _get_model, + _patch_error_tracing, + _run_single_turn, + _run_single_turn_streamed, +) + +try: + # "agents" is too generic. If someone has an agents.py file in their project + # or another package that's importable via "agents", no ImportError would + # be thrown and the integration would enable itself even if openai-agents is + # not installed. That's why we're adding the second, more specific import + # after it, even if we don't use it. + import agents + from agents.run import AgentRunner + from agents.version import __version__ as OPENAI_AGENTS_VERSION + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +try: + # AgentRunner methods moved in v0.8 + # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 + from agents.run_internal import run_loop, turn_preparation, turn_resolution +except ImportError: + run_loop = None + turn_preparation = None + turn_resolution = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + from agents.run_internal.run_steps import SingleStepResult + + +def _patch_runner() -> None: + # Create the root span for one full agent run (including eventual handoffs) + # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around + # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. + agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run + ) + + # Patch streaming runner + agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run_streamed + ) + + +class OpenAIAgentsIntegration(Integration): + """ + NOTE: With version 0.8.0, the class methods below have been refactored to functions. + - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` + - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` + - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` + - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` + - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` + + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. + 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. + - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. + - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. + 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. + - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. + - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. + 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. + + Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. + Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. + An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. + Handoffs are a way to switch agent-wide configuration. + - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` + """ + + identifier = "openai_agents" + + @staticmethod + def setup_once() -> None: + _patch_error_tracing() + _patch_runner() + + library_version = parse_version(OPENAI_AGENTS_VERSION) + if library_version is not None and library_version >= ( + 0, + 8, + ): + if run_loop is not None: + + @wraps(run_loop.get_all_tools) + async def new_wrapped_get_all_tools( + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools( + run_loop.get_all_tools, agent, context_wrapper + ) + + agents.run.get_all_tools = new_wrapped_get_all_tools + + @wraps(run_loop.run_single_turn) + async def new_wrapped_run_single_turn( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn( + run_loop.run_single_turn, *args, **kwargs + ) + + agents.run.run_single_turn = new_wrapped_run_single_turn + + @wraps(run_loop.run_single_turn_streamed) + async def new_wrapped_run_single_turn_streamed( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + run_loop.run_single_turn_streamed, *args, **kwargs + ) + + agents.run.run_single_turn_streamed = ( + new_wrapped_run_single_turn_streamed + ) + + if turn_preparation is not None: + + @wraps(turn_preparation.get_model) + def new_wrapped_get_model( + agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(turn_preparation.get_model, agent, run_config) + + agents.run_internal.run_loop.get_model = new_wrapped_get_model + + if turn_resolution is not None: + original_execute_handoffs = turn_resolution.execute_handoffs + + @wraps(original_execute_handoffs) + async def new_wrapped_execute_handoffs( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs( + original_execute_handoffs, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_handoffs = ( + new_wrapped_execute_handoffs + ) + + original_execute_final_output = turn_resolution.execute_final_output + + @wraps(turn_resolution.execute_final_output) + async def new_wrapped_final_output( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_final_output = ( + new_wrapped_final_output + ) + + return + + original_get_all_tools = AgentRunner._get_all_tools + + @wraps(AgentRunner._get_all_tools.__func__) + async def old_wrapped_get_all_tools( + cls: "agents.Runner", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + + agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) + + original_get_model = AgentRunner._get_model + + @wraps(AgentRunner._get_model.__func__) + def old_wrapped_get_model( + cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(original_get_model, agent, run_config) + + agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) + + original_run_single_turn = AgentRunner._run_single_turn + + @wraps(AgentRunner._run_single_turn.__func__) + async def old_wrapped_run_single_turn( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn(original_run_single_turn, *args, **kwargs) + + agents.run.AgentRunner._run_single_turn = classmethod( + old_wrapped_run_single_turn + ) + + original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed + + @wraps(AgentRunner._run_single_turn_streamed.__func__) + async def old_wrapped_run_single_turn_streamed( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + original_run_single_turn_streamed, *args, **kwargs + ) + + agents.run.AgentRunner._run_single_turn_streamed = classmethod( + old_wrapped_run_single_turn_streamed + ) + + original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs + + @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) + async def old_wrapped_execute_handoffs( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) + + agents._run_impl.RunImpl.execute_handoffs = classmethod( + old_wrapped_execute_handoffs + ) + + original_execute_final_output = agents._run_impl.RunImpl.execute_final_output + + @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) + async def old_wrapped_final_output( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents._run_impl.RunImpl.execute_final_output = classmethod( + old_wrapped_final_output + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py new file mode 100644 index 0000000000..f5de978be0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py new file mode 100644 index 0000000000..85d48f2d41 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -0,0 +1,10 @@ +from .agent_run import ( + _execute_final_output, # noqa: F401 + _execute_handoffs, # noqa: F401 + _run_single_turn, # noqa: F401 + _run_single_turn_streamed, # noqa: F401 +) +from .error_tracing import _patch_error_tracing # noqa: F401 +from .models import _get_model # noqa: F401 +from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 +from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py new file mode 100644 index 0000000000..71883b2eef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -0,0 +1,332 @@ +import sys +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import ( + handoff_span, + invoke_agent_span, + update_invoke_agent_span, +) + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, Union + + from agents.run_internal.run_steps import SingleStepResult + + from sentry_sdk.tracing import Span + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: + """Check if there's an active agent span for this context""" + return getattr(context_wrapper, "_sentry_current_agent", None) is not None + + +def _get_current_agent( + context_wrapper: "agents.RunContextWrapper", +) -> "Optional[agents.Agent]": + """Get the current agent from context wrapper""" + return getattr(context_wrapper, "_sentry_current_agent", None) + + +def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: + """Close the workflow span for streaming executions if it exists.""" + if agent and hasattr(agent, "_sentry_workflow_span"): + workflow_span = agent._sentry_workflow_span + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + +def _maybe_start_agent_span( + context_wrapper: "agents.RunContextWrapper", + agent: "agents.Agent", + should_run_agent_start_hooks: bool, + span_kwargs: "dict[str, Any]", + is_streaming: bool = False, +) -> "Optional[Union[Span, StreamedSpan]]": + """ + Start an agent invocation span if conditions are met. + Handles ending any existing span for a different agent. + + Returns the new span if started, or the existing span if conditions aren't met. + """ + if not (should_run_agent_start_hooks and agent and context_wrapper): + return getattr(context_wrapper, "_sentry_agent_span", None) + + # End any existing span for a different agent + if _has_active_agent_span(context_wrapper): + current_agent = _get_current_agent(context_wrapper) + if current_agent and current_agent != agent: + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + # Store the agent on the context wrapper so we can access it later + context_wrapper._sentry_current_agent = agent + span = invoke_agent_span(context_wrapper, agent, span_kwargs) + context_wrapper._sentry_agent_span = span + agent._sentry_agent_span = span + + if not is_streaming: + return span + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + return span + + +async def _run_single_turn( + original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn that + - creates agent invocation spans if there is no already active agent invocation span. + - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. + """ + # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. + bindings = kwargs.get("bindings") + agent = ( + getattr(bindings, "public_agent", None) + if bindings is not None + else kwargs.get("agent") + ) + context_wrapper = kwargs.get("context_wrapper") + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) + + span = _maybe_start_agent_span( + context_wrapper, agent, should_run_agent_start_hooks, kwargs + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn(*args, **kwargs) + + try: + result = await original_run_single_turn(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + return result + + +async def _run_single_turn_streamed( + original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn_streamed that + - creates agent invocation spans for streaming if there is no already active agent invocation span. + - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. + + Note: Unlike _run_single_turn which uses keyword-only arguments (*,), + _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: + _run_single_turn_streamed( + streamed_result, # args[0] + bindings, # args[1] + hooks, # args[2] + context_wrapper, # args[3] + run_config, # args[4] + should_run_agent_start_hooks, # args[5] + tool_use_tracker, # args[6] + all_tools, # args[7] + server_conversation_tracker, # args[8] (optional) + ) + """ + streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") + # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. + agent_or_bindings = ( + args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) + ) + agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) + context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") + should_run_agent_start_hooks = bool( + args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) + ) + + span_kwargs: "dict[str, Any]" = {} + if streamed_result and hasattr(streamed_result, "input"): + span_kwargs["original_input"] = streamed_result.input + + span = _maybe_start_agent_span( + context_wrapper, + agent, + should_run_agent_start_hooks, + span_kwargs, + is_streaming=True, + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn_streamed(*args, **kwargs) + + try: + result = await original_run_single_turn_streamed(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + return result + + +async def _execute_handoffs( + original_execute_handoffs: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_handoffs that + - creates and manages handoff spans. + - ends the agent invocation span. + - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. + """ + + context_wrapper = kwargs.get("context_wrapper") + run_handoffs = kwargs.get("run_handoffs") + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + + # Create Sentry handoff span for the first handoff (agents library only processes the first one) + if run_handoffs: + first_handoff = run_handoffs[0] + handoff_agent_name = first_handoff.handoff.agent_name + handoff_span(context_wrapper, agent, handoff_agent_name) + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + # Call original method with all parameters + try: + return await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + # Call original method with all parameters + try: + result = await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result + + +async def _execute_final_output( + original_execute_final_output: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_final_output that + - ends the agent invocation span. + - ends the workflow span if the response is streamed. + """ + + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + context_wrapper = kwargs.get("context_wrapper") + final_output = kwargs.get("final_output") + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + try: + return await original_execute_final_output(*args, **kwargs) + finally: + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + + try: + result = await original_execute_final_output(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py new file mode 100644 index 0000000000..68dadb3101 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py @@ -0,0 +1,74 @@ +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANSTATUS +from sentry_sdk.traces import SpanStatus +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +def _patch_error_tracing() -> None: + """ + Patches agents error tracing function to inject our span error logic + when a tool execution fails. + + In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span + In older versions, it was at: agents._utils.attach_error_to_current_span + + This works even when the module or function doesn't exist. + """ + error_tracing_module = None + + # Try newer location first (agents.util._error_tracing) + try: + from agents.util import _error_tracing + + error_tracing_module = _error_tracing + except (ImportError, AttributeError): + pass + + # Try older location (agents._utils) + if error_tracing_module is None: + try: + import agents._utils + + error_tracing_module = agents._utils + except (ImportError, AttributeError): + # Module doesn't exist in either location, nothing to patch + return + + # Check if the function exists + if not hasattr(error_tracing_module, "attach_error_to_current_span"): + return + + original_attach_error = error_tracing_module.attach_error_to_current_span + + @wraps(original_attach_error) + def sentry_attach_error_to_current_span( + error: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Wraps agents' error attachment to also set Sentry span status to error. + This allows us to properly track tool execution errors even though + the agents library swallows exceptions. + """ + # Set the current Sentry span to errored + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + current_span = sentry_sdk.get_current_scope().streamed_span + if current_span is not None: + current_span.status = SpanStatus.ERROR + else: + current_span = sentry_sdk.get_current_span() + if current_span is not None: + current_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # Call the original function + return original_attach_error(error, *args, **kwargs) + + error_tracing_module.attach_error_to_current_span = ( + sentry_attach_error_to_current_span + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py new file mode 100644 index 0000000000..634c9fdca1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py @@ -0,0 +1,197 @@ +import copy +import time +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_sentry_baggage_to_headers, + should_propagate_trace, +) +from sentry_sdk.utils import logger + +from ..spans import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + from sentry_sdk.tracing import Span + +try: + import agents + from agents.tool import HostedMCPTool +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _set_response_model_on_agent_span( + agent: "agents.Agent", response_model: "Optional[str]" +) -> None: + """Set the response model on the agent's invoke_agent span if available.""" + if response_model: + agent_span = getattr(agent, "_sentry_agent_span", None) + if agent_span: + if isinstance(agent_span, StreamedSpan): + agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + else: + agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + +def _inject_trace_propagation_headers( + hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" +) -> None: + headers = hosted_tool.tool_config.get("headers") + if headers is None: + headers = {} + hosted_tool.tool_config["headers"] = headers + + mcp_url = hosted_tool.tool_config.get("server_url") + if not mcp_url: + return + + if should_propagate_trace(sentry_sdk.get_client(), mcp_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( + key=key, value=value, mcp_url=mcp_url + ) + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(headers, value) + else: + headers[key] = value + + +def _get_model( + original_get_model: "Callable[..., agents.Model]", + agent: "agents.Agent", + run_config: "agents.RunConfig", +) -> "agents.Model": + """ + Responsible for + - creating and managing AI client spans. + - adding trace propagation headers to tools with type HostedMCPTool. + - setting the response model on agent invocation spans. + """ + # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) + # because we only patch its direct methods, all underlying data can remain unchanged. + model = copy.copy(original_get_model(agent, run_config)) + + # Capture the request model name for spans (agent.model can be None when using defaults) + request_model_name = model.model if hasattr(model, "model") else str(model) + agent._sentry_request_model = request_model_name + + # Wrap _fetch_response if it exists (for OpenAI models) to capture response model + if hasattr(model, "_fetch_response"): + original_fetch_response = model._fetch_response + + @wraps(original_fetch_response) + async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": + response = await original_fetch_response(*args, **kwargs) + if hasattr(response, "model") and response.model: + agent._sentry_response_model = str(response.model) + return response + + model._fetch_response = wrapped_fetch_response + + original_get_response = model.get_response + + @wraps(original_get_response) + async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": + mcp_tools = kwargs.get("tools") + hosted_tools = [] + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + result = await original_get_response(*args, **kwargs) + + # Get response model captured from _fetch_response and clean up + response_model = getattr(agent, "_sentry_response_model", None) + if response_model: + delattr(agent, "_sentry_response_model") + + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, result, response_model, agent) + + return result + + model.get_response = wrapped_get_response + + # Also wrap stream_response for streaming support + if hasattr(model, "stream_response"): + original_stream_response = model.stream_response + + @wraps(original_stream_response) + async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": + span_kwargs = dict(kwargs) + if len(args) > 0: + span_kwargs["system_instructions"] = args[0] + if len(args) > 1: + span_kwargs["input"] = args[1] + + hosted_tools = [] + if len(args) > 3: + mcp_tools = args[3] + + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, span_kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + streaming_response = None + ttft_recorded = False + # Capture start time locally to avoid race conditions with concurrent requests + start_time = time.perf_counter() + + async for event in original_stream_response(*args, **kwargs): + # Detect first content token (text delta event) + if not ttft_recorded and hasattr(event, "delta"): + ttft = time.perf_counter() - start_time + set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) + ttft_recorded = True + + # Capture the full response from ResponseCompletedEvent + if hasattr(event, "response"): + streaming_response = event.response + yield event + + # Update span with response data (usage, output, model) + if streaming_response: + response_model = ( + str(streaming_response.model) + if hasattr(streaming_response, "model") + and streaming_response.model + else None + ) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span( + span, streaming_response, response_model, agent + ) + + model.stream_response = wrapped_stream_response + + return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py new file mode 100644 index 0000000000..5f9996595f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -0,0 +1,221 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import agent_workflow_span, update_invoke_agent_span +from ..utils import _capture_exception + +try: + from agents.exceptions import AgentsException +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, AsyncIterator, Callable + + +def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run methods to + - create and manage a root span for the agent workflow runs. + - end the agent invocation span if an `AgentsException` is raised in `run()`. + + Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), + so it does not need to be wrapped separately. + """ + + @wraps(original_func) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + with agent_workflow_span(agent) as workflow_span: + # Set conversation ID on workflow span early so it's captured even on errors + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + run_result = await original_func(*args, **kwargs) + except AgentsException as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + + context_wrapper = getattr(exc.run_data, "context_wrapper", None) + if context_wrapper is not None: + invoke_agent_span = getattr( + context_wrapper, "_sentry_agent_span", None + ) + + if invoke_agent_span is not None and ( + ( + isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.end_timestamp is None + ) + or ( + not isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.timestamp is None + ) + ): + update_invoke_agent_span( + span=invoke_agent_span, + context=context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Invoke agent span is not finished in this case. + # This is much less likely to occur than other cases because + # AgentRunner.run() is "just" a while loop around _run_single_turn. + _capture_exception(exc) + reraise(*exc_info) + + invoke_agent_span = getattr( + run_result.context_wrapper, "_sentry_agent_span", None + ) + if not invoke_agent_span: + return run_result + + update_invoke_agent_span( + span=invoke_agent_span, + context=run_result.context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(None, None, None) + delattr(run_result.context_wrapper, "_sentry_agent_span") + return run_result + + return wrapper + + +def _create_run_streamed_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run_streamed method to + - create a root span for streaming agent workflow runs. + - end the workflow span if and only if the response stream is consumed or cancelled. + + Unlike run(), run_streamed() returns immediately with a RunResultStreaming object + while execution continues in a background task. The workflow span must stay open + throughout the streaming operation and close when streaming completes or is abandoned. + + Note: We don't use isolation_scope() here because it uses context variables that + cannot span async boundaries (the __enter__ and __exit__ would be called from + different async contexts, causing ValueError). + """ + + @wraps(original_func) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + # Capture conversation_id from kwargs if provided + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + # Start workflow span immediately (before run_streamed returns) + workflow_span = agent_workflow_span(agent) + workflow_span.__enter__() + + # Set conversation ID on workflow span early so it's captured even on errors + if conversation_id: + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + # Store span on agent for cleanup + agent._sentry_workflow_span = workflow_span + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + # Call original function to get RunResultStreaming + run_result = original_func(*args, **kwargs) + except Exception as exc: + # If run_streamed itself fails (not the background task), clean up immediately + workflow_span.__exit__(*sys.exc_info()) + _capture_exception(exc) + raise + + def _close_workflow_span() -> None: + if hasattr(agent, "_sentry_workflow_span"): + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + if hasattr(run_result, "stream_events"): + original_stream_events = run_result.stream_events + + @wraps(original_stream_events) + async def wrapped_stream_events( + *stream_args: "Any", **stream_kwargs: "Any" + ) -> "AsyncIterator[Any]": + try: + async for event in original_stream_events( + *stream_args, **stream_kwargs + ): + yield event + finally: + _close_workflow_span() + + run_result.stream_events = wrapped_stream_events + + if hasattr(run_result, "cancel"): + original_cancel = run_result.cancel + + @wraps(original_cancel) + def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": + try: + return original_cancel(*cancel_args, **cancel_kwargs) + finally: + _close_workflow_span() + + run_result.cancel = wrapped_cancel + + return run_result + + return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py new file mode 100644 index 0000000000..bd13b9d61a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -0,0 +1,70 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import execute_tool_span, update_execute_tool_span + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +async def _get_all_tools( + original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", +) -> "list[agents.Tool]": + """ + Responsible for creating and managing `gen_ai.execute_tool` spans. + """ + # Get the original tools + tools = await original_get_all_tools(agent, context_wrapper) + + wrapped_tools = [] + for tool in tools: + # Wrap only the function tools (for now) + if tool.__class__.__name__ != "FunctionTool": + wrapped_tools.append(tool) + continue + + # Create a new FunctionTool with our wrapped invoke method + original_on_invoke = tool.on_invoke_tool + + def create_wrapped_invoke( + current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" + ) -> "Callable[..., Any]": + @wraps(current_on_invoke) + async def sentry_wrapped_on_invoke_tool( + *args: "Any", **kwargs: "Any" + ) -> "Any": + with execute_tool_span(current_tool, *args, **kwargs) as span: + # We can not capture exceptions in tool execution here because + # `_on_invoke_tool` is swallowing the exception here: + # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 + # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter + # I was unable to monkey patch it because those are evaluated at module import time + # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` + # because it is nested inside this import time code. As if they made it hard to patch on purpose... + result = await current_on_invoke(*args, **kwargs) + update_execute_tool_span(span, agent, current_tool, result) + + return result + + return sentry_wrapped_on_invoke_tool + + wrapped_tool = agents.FunctionTool( + name=tool.name, + description=tool.description, + params_json_schema=tool.params_json_schema, + on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), + strict_json_schema=tool.strict_json_schema, + is_enabled=tool.is_enabled, + ) + wrapped_tools.append(wrapped_tool) + + return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py new file mode 100644 index 0000000000..08802a87a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py @@ -0,0 +1,8 @@ +from .agent_workflow import agent_workflow_span # noqa: F401 +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .handoff import handoff_span # noqa: F401 +from .invoke_agent import ( + invoke_agent_span, # noqa: F401 + update_invoke_agent_span, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py new file mode 100644 index 0000000000..758f06db8d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + from typing import Union + + import agents + + +def agent_workflow_span( + agent: "agents.Agent", +) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": + # Create a transaction or a span if an transaction is already active + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} + ) + + return span + + span = get_start_span_function()( + name=f"{agent.name} workflow", + origin=SPAN_ORIGIN, + ) + + return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py new file mode 100644 index 0000000000..f4f02cb674 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_input_data, + _set_output_data, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from agents import Agent + + +def ai_client_span( + agent: "Agent", get_response_kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + # TODO-anton: implement other types of operations. Now "chat" is hardcoded. + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + _set_agent_data(span, agent) + _set_input_data(span, get_response_kwargs) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + response: "Any", + response_model: "Optional[str]" = None, + agent: "Optional[Agent]" = None, +) -> None: + """Update AI client span with response data (works for streaming and non-streaming).""" + if hasattr(response, "usage") and response.usage: + _set_usage_data(span, response.usage) + + if hasattr(response, "output") and response.output: + _set_output_data(span, response) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + elif hasattr(response, "model") and response.model: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) + + # Set conversation ID from agent if available + if agent: + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py new file mode 100644 index 0000000000..fd3a430951 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def execute_tool_span( + tool: "agents.Tool", *args: "Any", **kwargs: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool.name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool.name, + SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool.name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) + + set_on_span = span.set_data + + if should_send_default_pii(): + input = args[1] + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + agent: "agents.Agent", + tool: "agents.Tool", + result: "Any", +) -> None: + _set_agent_data(span, agent) + + if isinstance(result, str) and result.startswith( + "An error occurred while running the tool" + ): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if should_send_default_pii(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py new file mode 100644 index 0000000000..ea91464afb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + import agents + + +def handoff_span( + context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str +) -> None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"handoff from {from_agent.name} to {to_agent_name}", + attributes={ + "sentry.op": OP.GEN_AI_HANDOFF, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "handoff", + }, + ) as span: + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_HANDOFF, + name=f"handoff from {from_agent.name} to {to_agent_name}", + origin=SPAN_ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") + + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py new file mode 100644 index 0000000000..c21145ac4a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -0,0 +1,122 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _set_usage_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def invoke_agent_span( + context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {agent.name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + start_span_function = get_start_span_function() + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {agent.name}", + origin=SPAN_ORIGIN, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + if should_send_default_pii(): + messages = [] + if agent.instructions: + message = ( + agent.instructions + if isinstance(agent.instructions, str) + else safe_serialize(agent.instructions) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "system", + } + ) + + original_input = kwargs.get("original_input") + if original_input is not None: + message = ( + original_input + if isinstance(original_input, str) + else safe_serialize(original_input) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "user", + } + ) + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + _set_agent_data(span, agent) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + context: "agents.RunContextWrapper", + agent: "agents.Agent", + output: "Any" = None, +) -> None: + # Add aggregated usage data from context_wrapper + if hasattr(context, "usage"): + _set_usage_data(span, context.usage) + + if should_send_default_pii(): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py new file mode 100644 index 0000000000..224a5f66ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py @@ -0,0 +1,244 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai._openai_completions_api import _transform_system_instructions +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions, + _is_system_instruction, +) +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + normalize_message_role, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import should_truncate_gen_ai_input +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Union + + from agents import TResponseInputItem, Usage + + from sentry_sdk._types import TextPart + +try: + import agents + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai_agents", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span( + SPANDATA.GEN_AI_SYSTEM, "openai" + ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. + + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) + + if agent.model_settings.max_tokens: + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) + + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + if agent.model_settings.presence_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + agent.model_settings.presence_penalty, + ) + + if agent.model_settings.temperature: + set_on_span( + SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature + ) + + if agent.model_settings.top_p: + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) + + if agent.model_settings.frequency_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + agent.model_settings.frequency_penalty, + ) + + if len(agent.tools) > 0: + set_on_span( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + safe_serialize([vars(tool) for tool in agent.tools]), + ) + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage.input_tokens_details.cached_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage.output_tokens_details.reasoning_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) + + +def _set_input_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + get_response_kwargs: "dict[str, Any]", +) -> None: + if not should_send_default_pii(): + return + request_messages = [] + + messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) + + instructions_text_parts: "list[TextPart]" = [] + explicit_instructions = get_response_kwargs.get("system_instructions") + if explicit_instructions is not None: + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions(messages) + + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + non_system_messages = [ + message for message in messages if not _is_system_instruction(message) + ] + for message in non_system_messages: + if "role" in message: + normalized_role = normalize_message_role(message.get("role")) # type: ignore + content = message.get("content") # type: ignore + request_messages.append( + { + "role": normalized_role, + "content": ( + [{"type": "text", "text": content}] + if isinstance(content, str) + else content + ), + } + ) + else: + if message.get("type") == "function_call": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, + "content": [message], + } + ) + elif message.get("type") == "function_call_output": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, + "content": [message], + } + ) + + normalized_messages = normalize_message_roles(request_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + if not should_send_default_pii(): + return + + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in result.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py new file mode 100644 index 0000000000..281604fe38 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from openfeature import api + from openfeature.hook import Hook + + if TYPE_CHECKING: + from openfeature.hook import HookContext, HookHints +except ImportError: + raise DidNotEnable("OpenFeature is not installed") + + +class OpenFeatureIntegration(Integration): + identifier = "openfeature" + + @staticmethod + def setup_once() -> None: + # Register the hook within the global openfeature hooks list. + api.add_hooks(hooks=[OpenFeatureHook()]) + + +class OpenFeatureHook(Hook): + def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: + if isinstance(details.value, bool): + add_feature_flag(details.flag_key, details.value) + + def error( + self, hook_context: "HookContext", exception: Exception, hints: "HookHints" + ) -> None: + if isinstance(hook_context.default_value, bool): + add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py new file mode 100644 index 0000000000..d76b8058ee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py @@ -0,0 +1,7 @@ +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor + +__all__ = [ + "SentryPropagator", + "SentrySpanProcessor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py new file mode 100644 index 0000000000..d6733036ea --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py @@ -0,0 +1,9 @@ +from sentry_sdk.integrations import DidNotEnable + +try: + from opentelemetry.context import create_key +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +SENTRY_TRACE_KEY = create_key("sentry-trace") +SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py new file mode 100644 index 0000000000..472e8181f9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py @@ -0,0 +1,81 @@ +""" +IMPORTANT: The contents of this file are part of a proof of concept and as such +are experimental and not suitable for production use. They may be changed or +removed at any time without prior notice. +""" + +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + +try: + from opentelemetry import trace + from opentelemetry.propagate import set_global_textmap + from opentelemetry.sdk.trace import TracerProvider +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +try: + from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] + DjangoInstrumentor, + ) +except ImportError: + DjangoInstrumentor = None + + +CONFIGURABLE_INSTRUMENTATIONS = { + DjangoInstrumentor: {"is_sql_commentor_enabled": True}, +} + + +class OpenTelemetryIntegration(Integration): + identifier = "opentelemetry" + + @staticmethod + def setup_once() -> None: + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if has_span_streaming_enabled(options): + logger.warning( + "[OTel] OpenTelemetryIntegration is not compatible with span streaming " + "(trace_lifecycle='stream') and will be disabled." + ) + return + + warnings.warn( + "OpenTelemetryIntegration is deprecated. " + "Please use OTLPIntegration instead: " + "https://docs.sentry.io/platforms/python/integrations/otlp/", + DeprecationWarning, + stacklevel=2, + ) + + _setup_sentry_tracing() + # _setup_instrumentors() + + logger.debug("[OTel] Finished setting up OpenTelemetry integration") + + +def _setup_sentry_tracing() -> None: + provider = TracerProvider() + provider.add_span_processor(SentrySpanProcessor()) + trace.set_tracer_provider(provider) + set_global_textmap(SentryPropagator()) + + +def _setup_instrumentors() -> None: + for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): + if instrumentor is None: + continue + instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py new file mode 100644 index 0000000000..5b5f13861d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py @@ -0,0 +1,128 @@ +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.integrations.opentelemetry.span_processor import ( + SentrySpanProcessor, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data + +try: + from opentelemetry import trace + from opentelemetry.context import ( + Context, + get_current, + set_value, + ) + from opentelemetry.propagators.textmap import ( + CarrierT, + Getter, + Setter, + TextMapPropagator, + default_getter, + default_setter, + ) + from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional, Set + + +class SentryPropagator(TextMapPropagator): + """ + Propagates tracing headers for Sentry's tracing system in a way OTel understands. + """ + + def extract( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + getter: "Getter[CarrierT]" = default_getter, + ) -> "Context": + if context is None: + context = get_current() + + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + if not sentry_trace: + return context + + sentrytrace = extract_sentrytrace_data(sentry_trace[0]) + if not sentrytrace: + return context + + context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) + + trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] + + span_context = SpanContext( + trace_id=int(trace_id, 16), # type: ignore + span_id=int(span_id, 16), # type: ignore + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags=TraceFlags(TraceFlags.SAMPLED), + is_remote=True, + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + if baggage_header: + baggage = Baggage.from_incoming_header(baggage_header[0]) + else: + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + baggage = Baggage(sentry_items={}) + + baggage.freeze() + context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) + + span = NonRecordingSpan(span_context) + modified_context = trace.set_span_in_context(span, context) + return modified_context + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + if context is None: + context = get_current() + + current_span = trace.get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + span_id = trace.format_span_id(current_span_context.span_id) + + span_map = SentrySpanProcessor.otel_span_map + sentry_span = span_map.get(span_id, None) + if not sentry_span: + return + + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) + + if sentry_span.containing_transaction: + baggage = sentry_span.containing_transaction.get_baggage() + if baggage: + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + @property + def fields(self) -> "Set[str]": + return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py new file mode 100644 index 0000000000..1efd2ac455 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -0,0 +1,407 @@ +from datetime import datetime, timezone +from time import time +from typing import TYPE_CHECKING, cast + +from urllib3.util import parse_url as urlparse + +from sentry_sdk import get_client, start_transaction +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing import Transaction + +try: + from opentelemetry.context import get_value + from opentelemetry.sdk.trace import ReadableSpan as OTelSpan + from opentelemetry.sdk.trace import SpanProcessor + from opentelemetry.semconv.trace import SpanAttributes + from opentelemetry.trace import ( + SpanKind, + format_span_id, + format_trace_id, + get_current_span, + ) + from opentelemetry.trace.span import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from opentelemetry import context as context_api + from opentelemetry.trace import SpanContext + + from sentry_sdk._types import Event, Hint + +OPEN_TELEMETRY_CONTEXT = "otel" +SPAN_MAX_TIME_OPEN_MINUTES = 10 +SPAN_ORIGIN = "auto.otel" + + +def link_trace_context_to_error_event( + event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" +) -> "Event": + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return event + + if hasattr(event, "type") and event["type"] == "transaction": + return event + + otel_span = get_current_span() + if not otel_span: + return event + + ctx = otel_span.get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return event + + sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) + if not sentry_span: + return event + + contexts = event.setdefault("contexts", {}) + contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) + + return event + + +class SentrySpanProcessor(SpanProcessor): + """ + Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. + """ + + # The mapping from otel span ids to sentry spans + otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} + + # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES + open_spans: "dict[int, set[str]]" = {} + + initialized: "bool" = False + + def __new__(cls) -> "SentrySpanProcessor": + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + + # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). + return cls.instance # type: ignore[misc] + + def __init__(self) -> None: + if self.initialized: + return + + @add_global_event_processor + def global_event_processor(event: "Event", hint: "Hint") -> "Event": + return link_trace_context_to_error_event(event, self.otel_span_map) + + self.initialized = True + + def _prune_old_spans(self: "SentrySpanProcessor") -> None: + """ + Prune spans that have been open for too long. + """ + current_time_minutes = int(time() / 60) + for span_start_minutes in list( + self.open_spans.keys() + ): # making a list because we change the dict + # prune empty open spans buckets + if self.open_spans[span_start_minutes] == set(): + self.open_spans.pop(span_start_minutes) + + # prune old buckets + elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: + for span_id in self.open_spans.pop(span_start_minutes): + self.otel_span_map.pop(span_id, None) + + def on_start( + self, + otel_span: "OTelSpan", + parent_context: "Optional[context_api.Context]" = None, + ) -> None: + client = get_client() + + if not client.parsed_dsn: + return + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + if self._is_sentry_span(otel_span): + return + + trace_data = self._get_trace_data( + span_context, otel_span.parent, parent_context + ) + + parent_span_id = trace_data["parent_span_id"] + sentry_parent_span = ( + self.otel_span_map.get(parent_span_id) if parent_span_id else None + ) + + start_timestamp = None + if otel_span.start_time is not None: + start_timestamp = datetime.fromtimestamp( + otel_span.start_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span = None + if sentry_parent_span: + sentry_span = sentry_parent_span.start_child( + span_id=trace_data["span_id"], + name=otel_span.name, + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + else: + sentry_span = start_transaction( + name=otel_span.name, + span_id=trace_data["span_id"], + parent_span_id=parent_span_id, + trace_id=trace_data["trace_id"], + baggage=trace_data["baggage"], + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + + self.otel_span_map[trace_data["span_id"]] = sentry_span + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).add( + trace_data["span_id"] + ) + + self._prune_old_spans() + + def on_end(self, otel_span: "OTelSpan") -> None: + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + span_id = format_span_id(span_context.span_id) + sentry_span = self.otel_span_map.pop(span_id, None) + if not sentry_span: + return + + sentry_span.op = otel_span.name + + self._update_span_with_otel_status(sentry_span, otel_span) + + if isinstance(sentry_span, Transaction): + sentry_span.name = otel_span.name + sentry_span.set_context( + OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) + ) + self._update_transaction_with_otel_data(sentry_span, otel_span) + + else: + self._update_span_with_otel_data(sentry_span, otel_span) + + end_timestamp = None + if otel_span.end_time is not None: + end_timestamp = datetime.fromtimestamp( + otel_span.end_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span.finish(end_timestamp=end_timestamp) + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) + + self._prune_old_spans() + + def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: + """ + Break infinite loop: + HTTP requests to Sentry are caught by OTel and send again to Sentry. + """ + otel_span_url = None + if otel_span.attributes is not None: + otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) + otel_span_url = cast("Optional[str]", otel_span_url) + + parsed_dsn = get_client().parsed_dsn + dsn_url = parsed_dsn.netloc if parsed_dsn else None + + if otel_span_url and dsn_url and dsn_url in otel_span_url: + return True + + return False + + def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": + """ + Returns the OTel context for Sentry. + See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context + """ + ctx = {} + + if otel_span.attributes: + ctx["attributes"] = dict(otel_span.attributes) + + if otel_span.resource.attributes: + ctx["resource"] = dict(otel_span.resource.attributes) + + return ctx + + def _get_trace_data( + self, + span_context: "SpanContext", + parent_span_context: "Optional[SpanContext]", + parent_context: "Optional[context_api.Context]", + ) -> "dict[str, Any]": + """ + Extracts tracing information from one OTel span's context and its parent OTel context. + """ + trace_data: "dict[str, Any]" = {} + + span_id = format_span_id(span_context.span_id) + trace_data["span_id"] = span_id + + trace_id = format_trace_id(span_context.trace_id) + trace_data["trace_id"] = trace_id + + parent_span_id = ( + format_span_id(parent_span_context.span_id) if parent_span_context else None + ) + trace_data["parent_span_id"] = parent_span_id + + sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) + sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) + trace_data["parent_sampled"] = ( + sentry_trace_data["parent_sampled"] if sentry_trace_data else None + ) + + baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) + trace_data["baggage"] = baggage + + return trace_data + + def _update_span_with_otel_status( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Set the Sentry span status from the OTel span + """ + if otel_span.status.is_unset: + return + + if otel_span.status.is_ok: + sentry_span.set_status(SPANSTATUS.OK) + return + + sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + def _update_span_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Convert OTel span data and update the Sentry span with it. + This should eventually happen on the server when ingesting the spans. + """ + sentry_span.set_data("otel.kind", otel_span.kind) + + op = otel_span.name + description = otel_span.name + + if otel_span.attributes is not None: + for key, val in otel_span.attributes.items(): + sentry_span.set_data(key, val) + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + http_method = cast("Optional[str]", http_method) + + db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) + + if http_method: + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + description = http_method + + peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) + if peer_name: + description += " {}".format(peer_name) + + target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) + if target: + description += " {}".format(target) + + if not peer_name and not target: + url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) + url = cast("Optional[str]", url) + if url: + parsed_url = urlparse(url) + url = "{}://{}{}".format( + parsed_url.scheme, parsed_url.netloc, parsed_url.path + ) + description += " {}".format(url) + + status_code = otel_span.attributes.get( + SpanAttributes.HTTP_STATUS_CODE, None + ) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + elif db_query: + op = "db" + statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) + statement = cast("Optional[str]", statement) + if statement: + description = statement + + sentry_span.op = op + sentry_span.description = description + + def _update_transaction_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + if otel_span.attributes is None: + return + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + + if http_method: + status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py new file mode 100644 index 0000000000..b91f2cfd21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py @@ -0,0 +1,223 @@ +from sentry_sdk import capture_event, get_client +from sentry_sdk.consts import VERSION, EndpointType +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import register_external_propagation_context +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + event_from_exception, + logger, +) + +try: + from opentelemetry.context import ( + Context, + get_current, + get_value, + ) + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.propagate import set_global_textmap + from opentelemetry.propagators.textmap import ( + CarrierT, + Setter, + default_setter, + ) + from opentelemetry.sdk.trace import Span, TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + SpanContext, + format_span_id, + format_trace_id, + get_current_span, + get_tracer_provider, + set_tracer_provider, + ) + + from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY + from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +except ImportError: + raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Tuple + + +def otel_propagation_context() -> "Optional[Tuple[str, str]]": + """ + Get the (trace_id, span_id) from opentelemetry if exists. + """ + ctx = get_current_span().get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return None + + return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) + + +def setup_otlp_traces_exporter( + dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None +) -> None: + tracer_provider = get_tracer_provider() + + if not isinstance(tracer_provider, TracerProvider): + logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") + tracer_provider = TracerProvider() + set_tracer_provider(tracer_provider) + + endpoint = None + headers = None + if collector_url: + endpoint = collector_url + logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") + elif dsn: + auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") + endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) + headers = {"X-Sentry-Auth": auth.to_header()} + logger.debug(f"[OTLP] Sending traces to {endpoint}") + + otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + + +_sentry_patched_exception = False + + +def setup_capture_exceptions() -> None: + """ + Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. + """ + global _sentry_patched_exception + _original_record_exception = Span.record_exception + + if _sentry_patched_exception: + return + + def _sentry_patched_record_exception( + self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration and otlp_integration.capture_exceptions: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=get_client().options, + mechanism={"type": OTLPIntegration.identifier, "handled": False}, + ) + capture_event(event, hint=hint) + + _original_record_exception(self, exception, *args, **kwargs) + + Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] + _sentry_patched_exception = True + + +class SentryOTLPPropagator(SentryPropagator): + """ + We need to override the inject of the older propagator since that + is SpanProcessor based. + + !!! Note regarding baggage: + We cannot meaningfully populate a new baggage as a head SDK + when we are using OTLP since we don't have any sort of transaction semantic to + track state across a group of spans. + + For incoming baggage, we just pass it on as is so that case is correctly handled. + """ + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration is None: + return + + if context is None: + context = get_current() + + current_span = get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + sentry_trace = _to_traceparent(current_span_context) + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) + + baggage = get_value(SENTRY_BAGGAGE_KEY, context) + if baggage is not None and isinstance(baggage, Baggage): + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + +def _to_traceparent(span_context: "SpanContext") -> str: + """ + Helper method to generate the sentry-trace header. + """ + span_id = format_span_id(span_context.span_id) + trace_id = format_trace_id(span_context.trace_id) + sampled = span_context.trace_flags.sampled + + return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" + + +class OTLPIntegration(Integration): + """ + Automatically setup OTLP ingestion from the DSN. + + :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. + Set to False to setup the TracerProvider manually. + :param collector_url: URL of your own OpenTelemetry collector, defaults to None. + When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. + :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. + Set to False to configure propagators manually or to disable propagation. + :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. + Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. + """ + + identifier = "otlp" + + def __init__( + self, + setup_otlp_traces_exporter: bool = True, + collector_url: "Optional[str]" = None, + setup_propagator: bool = True, + capture_exceptions: bool = False, + ) -> None: + self.setup_otlp_traces_exporter = setup_otlp_traces_exporter + self.collector_url = collector_url + self.setup_propagator = setup_propagator + self.capture_exceptions = capture_exceptions + + @staticmethod + def setup_once() -> None: + logger.debug("[OTLP] Setting up trace linking for all events") + register_external_propagation_context(otel_propagation_context) + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if self.setup_otlp_traces_exporter: + logger.debug("[OTLP] Setting up OTLP exporter") + dsn: "Optional[str]" = options.get("dsn") if options else None + setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) + + if self.setup_propagator: + logger.debug("[OTLP] Setting up propagator for distributed tracing") + # TODO-neel better propagator support, chain with existing ones if possible instead of replacing + set_global_textmap(SentryOTLPPropagator()) + + setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py new file mode 100644 index 0000000000..569e3e1fa5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py @@ -0,0 +1,134 @@ +import ast +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import serializer +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, List, Optional, Tuple + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + +try: + from pure_eval import Evaluator +except ImportError: + raise DidNotEnable("pure_eval is not installed") + +try: + # Used implicitly, just testing it's available + import asttokens # noqa +except ImportError: + raise DidNotEnable("asttokens is not installed") + + +class PureEvalIntegration(Integration): + identifier = "pure_eval" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + sentry_frame["vars"] = ( + pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] + ) + return event + + +def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": + source = Source.for_frame(frame) + if not source.tree: + return {} + + statements = source.statements_at_line(frame.f_lineno) + if not statements: + return {} + + scope = stmt = list(statements)[0] + while True: + # Get the parent first in case the original statement is already + # a function definition, e.g. if we're calling a decorator + # In that case we still want the surrounding scope, not that function + scope = scope.parent + if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): + break + + evaluator = Evaluator.from_frame(frame) + expressions = evaluator.interesting_expressions_grouped(scope) + + def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": + # Prioritise expressions with a node closer to the statement executed + # without being after that statement + # A higher return value is better - the expression will appear + # earlier in the list of values and is less likely to be trimmed + nodes, _value = expression + + def start(n: "ast.expr") -> "Tuple[int, int]": + return (n.lineno, n.col_offset) + + nodes_before_stmt = [ + node for node in nodes if start(node) < stmt.last_token.end + ] + if nodes_before_stmt: + # The position of the last node before or in the statement + return max(start(node) for node in nodes_before_stmt) + else: + # The position of the first node after the statement + # Negative means it's always lower priority than nodes that come before + # Less negative means closer to the statement and higher priority + lineno, col_offset = min(start(node) for node in nodes) + return (-lineno, -col_offset) + + # This adds the first_token and last_token attributes to nodes + atok = source.asttokens() + + expressions.sort(key=closeness, reverse=True) + vars = { + atok.get_text(nodes[0]): value + for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] + } + return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py new file mode 100644 index 0000000000..81e7cf8090 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -0,0 +1,187 @@ +import functools + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import capture_internal_exceptions, parse_version + +try: + import pydantic_ai # type: ignore # noqa: F401 + from pydantic_ai import Agent +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +from importlib.metadata import PackageNotFoundError, version +from typing import TYPE_CHECKING + +from .patches import ( + _patch_agent_run, + _patch_graph_nodes, + _patch_tool_execution, +) +from .spans.ai_client import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any + + from pydantic_ai import ModelRequestContext, RunContext + from pydantic_ai.capabilities import Hooks # type: ignore + from pydantic_ai.messages import ModelResponse # type: ignore + + +def register_hooks(hooks: "Hooks") -> None: + """ + Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. + """ + + @hooks.on.before_model_request # type: ignore + async def on_request( + ctx: "RunContext[None]", request_context: "ModelRequestContext" + ) -> "ModelRequestContext": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return request_context + + span = ai_client_span( + messages=request_context.messages, + agent=None, + model=request_context.model, + model_settings=request_context.model_settings, + ) + + run_context_metadata["_sentry_span"] = span + span.__enter__() + + return request_context + + @hooks.on.after_model_request # type: ignore + async def on_response( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + response: "ModelResponse", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return response + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + return response + + update_ai_client_span(span, response) + span.__exit__(None, None, None) + + return response + + @hooks.on.model_request_error # type: ignore + async def on_error( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + error: "Exception", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + + if not isinstance(run_context_metadata, dict): + raise error + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + raise error + + with capture_internal_exceptions(): + span.__exit__(type(error), error, error.__traceback__) + + raise error + + original_init = Agent.__init__ + + @functools.wraps(original_init) + def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: + caps = list(kwargs.get("capabilities") or []) + caps.append(hooks) + kwargs["capabilities"] = caps + + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {} # Used as shared reference between hooks + + return original_init(self, *args, **kwargs) + + Agent.__init__ = patched_init + + +class PydanticAIIntegration(Integration): + """ + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. + 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. + - Each run invocation has `RunContext` objects that are passed to the library hooks. + 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + + Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. + Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). + The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. + + The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that + instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. + """ + + identifier = "pydantic_ai" + origin = f"auto.ai.{identifier}" + using_request_hooks = False + + def __init__( + self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True + ) -> None: + """ + Initialize the Pydantic AI integration. + + Args: + include_prompts: Whether to include prompts and messages in span data. + Requires send_default_pii=True. Defaults to True. + handled_tool_exceptions: Capture tool call exceptions that Pydantic AI + internally prevents from bubbling up. + """ + self.include_prompts = include_prompts + self.handled_tool_call_exceptions = handled_tool_call_exceptions + + @staticmethod + def setup_once() -> None: + """ + Set up the pydantic-ai integration. + + This patches the key methods in pydantic-ai to create Sentry spans for: + - Agent invocations (Agent.run methods) + - Model requests (AI client calls) + - Tool executions + """ + _patch_agent_run() + _patch_tool_execution() + + PydanticAIIntegration.using_request_hooks = False + try: + PYDANTIC_AI_VERSION = version("pydantic-ai-slim") + except PackageNotFoundError: + return + + PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) + if PYDANTIC_AI_VERSION is None: + return + + # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c + if PYDANTIC_AI_VERSION < ( + 1, + 73, + ): + _patch_graph_nodes() + return + + try: + from pydantic_ai.capabilities import Hooks + except ImportError: + return + + PydanticAIIntegration.using_request_hooks = True + hooks = Hooks() + register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py new file mode 100644 index 0000000000..afa66dc47d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py new file mode 100644 index 0000000000..d0ea6242b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py @@ -0,0 +1,3 @@ +from .agent_run import _patch_agent_run # noqa: F401 +from .graph_nodes import _patch_graph_nodes # noqa: F401 +from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py new file mode 100644 index 0000000000..3039e40698 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -0,0 +1,198 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import invoke_agent_span, update_invoke_agent_span +from ..utils import _capture_exception, pop_agent, push_agent + +try: + from pydantic_ai.agent import Agent # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + +class _StreamingContextManagerWrapper: + """Wrapper for streaming methods that return async context managers.""" + + def __init__( + self, + agent: "Any", + original_ctx_manager: "Any", + user_prompt: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = True, + ) -> None: + self.agent = agent + self.original_ctx_manager = original_ctx_manager + self.user_prompt = user_prompt + self.model = model + self.model_settings = model_settings + self.is_streaming = is_streaming + self._isolation_scope: "Any" = None + self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None + self._result: "Any" = None + + async def __aenter__(self) -> "Any": + # Set up isolation scope and invoke_agent span + self._isolation_scope = sentry_sdk.isolation_scope() + self._isolation_scope.__enter__() + + # Create invoke_agent span (will be closed in __aexit__) + self._span = invoke_agent_span( + self.user_prompt, + self.agent, + self.model, + self.model_settings, + self.is_streaming, + ) + self._span.__enter__() + + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur + push_agent(self.agent, self.is_streaming) + + # Enter the original context manager + result = await self.original_ctx_manager.__aenter__() + self._result = result + return result + + async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: + try: + # Exit the original context manager first + await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) + + # Update span with result if successful + if exc_type is None and self._result and self._span is not None: + update_invoke_agent_span(self._span, self._result) + finally: + # Pop agent from contextvar stack + pop_agent() + + # Clean up invoke span + if self._span: + self._span.__exit__(exc_type, exc_val, exc_tb) + + # Clean up isolation scope + if self._isolation_scope: + self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) + + +def _create_run_wrapper( + original_func: "Callable[..., Any]", is_streaming: bool = False +) -> "Callable[..., Any]": + """ + Wraps the Agent.run method to create an invoke_agent span. + + Args: + original_func: The original run method + is_streaming: Whether this is a streaming method (for future use) + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Create invoke_agent span + with invoke_agent_span( + user_prompt, self, model, model_settings, is_streaming + ) as span: + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in finally even if exceptions occur + push_agent(self, is_streaming) + + try: + result = await original_func(self, *args, **kwargs) + + # Update span with result + update_invoke_agent_span(span, result) + + return result + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + finally: + # Pop agent from contextvar stack + pop_agent() + + return wrapper + + +def _create_streaming_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps run_stream method that returns an async context manager. + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Call original function to get the context manager + original_ctx_manager = original_func(self, *args, **kwargs) + + # Wrap it with our instrumentation + return _StreamingContextManagerWrapper( + agent=self, + original_ctx_manager=original_ctx_manager, + user_prompt=user_prompt, + model=model, + model_settings=model_settings, + is_streaming=True, + ) + + return wrapper + + +def _patch_agent_run() -> None: + """ + Patches the Agent run methods to create spans for agent execution. + + This patches both non-streaming (run, run_sync) and streaming + (run_stream, run_stream_events) methods. + """ + + # Store original methods + original_run = Agent.run + original_run_stream = Agent.run_stream + + # Wrap and apply patches for non-streaming methods + Agent.run = _create_run_wrapper(original_run, is_streaming=False) + + # Wrap and apply patches for streaming methods + Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py new file mode 100644 index 0000000000..afb10395f4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py @@ -0,0 +1,106 @@ +from contextlib import asynccontextmanager +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import ( + ai_client_span, + update_ai_client_span, +) + +try: + from pydantic_ai._agent_graph import ModelRequestNode # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + +def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": + """Extract common data needed for creating chat spans. + + Returns: + Tuple of (messages, model, model_settings) + """ + # Extract model and settings from context + model = None + model_settings = None + if hasattr(ctx, "deps"): + model = getattr(ctx.deps, "model", None) + model_settings = getattr(ctx.deps, "model_settings", None) + + # Build full message list: history + current request + messages = [] + if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): + messages.extend(ctx.state.message_history) + + current_request = getattr(node, "request", None) + if current_request: + messages.append(current_request) + + return messages, model, model_settings + + +def _patch_graph_nodes() -> None: + """ + Patches the graph node execution to create appropriate spans. + + ModelRequestNode -> Creates ai_client span for model requests + CallToolsNode -> Handles tool calls (spans created in tool patching) + """ + + # Patch ModelRequestNode to create ai_client spans + original_model_request_run = ModelRequestNode.run + + @wraps(original_model_request_run) + async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + with ai_client_span(messages, None, model, model_settings) as span: + result = await original_model_request_run(self, ctx) + + # Extract response from result if available + model_response = None + if hasattr(result, "model_response"): + model_response = result.model_response + + update_ai_client_span(span, model_response) + return result + + ModelRequestNode.run = wrapped_model_request_run + + # Patch ModelRequestNode.stream for streaming requests + original_model_request_stream = ModelRequestNode.stream + + def create_wrapped_stream( + original_stream_method: "Callable[..., Any]", + ) -> "Callable[..., Any]": + """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" + + @asynccontextmanager + @wraps(original_stream_method) + async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + # Create chat span for streaming request + with ai_client_span(messages, None, model, model_settings) as span: + # Call the original stream method + async with original_stream_method(self, ctx) as stream: + yield stream + + # After streaming completes, update span with response data + # The ModelRequestNode stores the final response in _result + model_response = None + if hasattr(self, "_result") and self._result is not None: + # _result is a NextNode containing the model_response + if hasattr(self._result, "model_response"): + model_response = self._result.model_response + + update_ai_client_span(span, model_response) + + return wrapped_model_request_stream + + ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py new file mode 100644 index 0000000000..5646b5d47c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -0,0 +1,177 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import execute_tool_span, update_execute_tool_span +from ..utils import _capture_exception, get_current_agent + +if TYPE_CHECKING: + from typing import Any + +try: + try: + from pydantic_ai.tool_manager import ToolManager # type: ignore + except ImportError: + from pydantic_ai._tool_manager import ToolManager # type: ignore + + from pydantic_ai.exceptions import ToolRetryError # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +def _patch_tool_execution() -> None: + if hasattr(ToolManager, "execute_tool_call"): + _patch_execute_tool_call() + + elif hasattr(ToolManager, "_call_tool"): + # older versions + _patch_call_tool() + + +def _patch_execute_tool_call() -> None: + original_execute_tool_call = ToolManager.execute_tool_call + + @wraps(original_execute_tool_call) + async def wrapped_execute_tool_call( + self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not validated or not hasattr(validated, "call"): + return await original_execute_tool_call(self, validated, *args, **kwargs) + + # Extract tool info before calling original + call = validated.call + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_execute_tool_call( + self, + validated, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + return await original_execute_tool_call(self, validated, *args, **kwargs) + + ToolManager.execute_tool_call = wrapped_execute_tool_call + + +def _patch_call_tool() -> None: + """ + Patch ToolManager._call_tool to create execute_tool spans. + + This is the single point where ALL tool calls flow through in pydantic_ai, + regardless of toolset type (function, MCP, combined, wrapper, etc.). + + By patching here, we avoid: + - Patching multiple toolset classes + - Dealing with signature mismatches from instrumented MCP servers + - Complex nested toolset handling + """ + original_call_tool = ToolManager._call_tool + + @wraps(original_call_tool) + async def wrapped_call_tool( + self: "Any", call: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Extract tool info before calling original + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_call_tool( + self, + call, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + # No span context - just call original + return await original_call_tool( + self, + call, + *args, + **kwargs, + ) + + ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py new file mode 100644 index 0000000000..574046d645 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py @@ -0,0 +1,3 @@ +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py new file mode 100644 index 0000000000..27deb0c55c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -0,0 +1,331 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _get_model_name, + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, + get_current_agent, + get_is_streaming, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Dict, List, Union + + from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore + + from sentry_sdk._types import TextPart as SentryTextPart + +try: + from pydantic_ai.messages import ( + BaseToolCallPart, + BaseToolReturnPart, + BinaryContent, + ImageUrl, + SystemPromptPart, + TextPart, + ThinkingPart, + UserPromptPart, + ) +except ImportError: + # Fallback if these classes are not available + BaseToolCallPart = None + BaseToolReturnPart = None + SystemPromptPart = None + UserPromptPart = None + TextPart = None + ThinkingPart = None + BinaryContent = None + ImageUrl = None + + +def _transform_system_instructions( + permanent_instructions: "list[SystemPromptPart]", + current_instructions: "list[str]", +) -> "list[SentryTextPart]": + text_parts: "list[SentryTextPart]" = [ + { + "type": "text", + "content": instruction.content, + } + for instruction in permanent_instructions + ] + + text_parts.extend( + { + "type": "text", + "content": instruction, + } + for instruction in current_instructions + ) + + return text_parts + + +def _get_system_instructions( + messages: "list[ModelMessage]", +) -> "tuple[list[SystemPromptPart], list[str]]": + permanent_instructions = [] + current_instructions = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + if SystemPromptPart and isinstance(part, SystemPromptPart): + permanent_instructions.append(part) + + if hasattr(msg, "instructions") and msg.instructions is not None: + current_instructions.append(msg.instructions) + + return permanent_instructions, current_instructions + + +def _set_input_messages( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" +) -> None: + """Set input messages data on a span.""" + if not _should_send_prompts(): + return + + if not messages: + return + + permanent_instructions, current_instructions = _get_system_instructions(messages) + if len(permanent_instructions) > 0 or len(current_instructions) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + + try: + formatted_messages = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + role = "user" + # Use isinstance checks with proper base classes + if SystemPromptPart and isinstance(part, SystemPromptPart): + continue + elif ( + (TextPart and isinstance(part, TextPart)) + or (ThinkingPart and isinstance(part, ThinkingPart)) + or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) + ): + role = "assistant" + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + role = "tool" + + content: "List[Dict[str, Any] | str]" = [] + tool_calls = None + tool_call_id = None + + # Handle ToolCallPart (assistant requesting tool use) + if BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = {} + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + if tool_call_data: + tool_calls = [tool_call_data] + # Handle ToolReturnPart (tool result) + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + if hasattr(part, "tool_name"): + tool_call_id = part.tool_name + if hasattr(part, "content"): + content.append({"type": "text", "text": str(part.content)}) + # Handle regular content + elif hasattr(part, "content"): + if isinstance(part.content, str): + content.append({"type": "text", "text": part.content}) + elif isinstance(part.content, list): + for item in part.content: + if isinstance(item, str): + content.append({"type": "text", "text": item}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + else: + content.append(safe_serialize(item)) + else: + content.append({"type": "text", "text": str(part.content)}) + # Add message if we have content or tool calls + if content or tool_calls: + message: "Dict[str, Any]" = {"role": role} + if content: + message["content"] = content + if tool_calls: + message["tool_calls"] = tool_calls + if tool_call_id: + message["tool_call_id"] = tool_call_id + formatted_messages.append(message) + + if formatted_messages: + normalized_messages = normalize_message_roles(formatted_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + except Exception: + # If we fail to format messages, just skip it + pass + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" +) -> None: + """Set output data on a span.""" + if not _should_send_prompts(): + return + + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + + try: + # Extract text from ModelResponse + if hasattr(response, "parts"): + texts = [] + tool_calls = [] + + for part in response.parts: + if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): + texts.append(part.content) + elif BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = { + "type": "function", + } + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + tool_calls.append(tool_call_data) + + if texts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) + + if tool_calls: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) + ) + + except Exception: + # If we fail to format output, just skip it + pass + + +def ai_client_span( + messages: "Any", agent: "Any", model: "Any", model_settings: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for an AI client call (model request). + + Args: + messages: Full conversation history (list of messages) + agent: Agent object + model: Model object + model_settings: Model settings + """ + # Determine model name for span name + model_obj = model + if agent and hasattr(agent, "model"): + model_obj = agent.model + + model_name = _get_model_name(model_obj) or "unknown" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + # Set streaming flag from contextvar + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + + # Add available tools if agent is available + agent_obj = agent or get_current_agent() + _set_available_tools(span, agent_obj) + + # Set input messages (full conversation history) + if messages: + _set_input_messages(span, messages) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" +) -> None: + """Update the AI client span with response data.""" + if not span: + return + + # Set usage data if available + if model_response and hasattr(model_response, "usage"): + _set_usage_data(span, model_response.usage) + + # Set output data + _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py new file mode 100644 index 0000000000..7648c1418a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _should_send_prompts + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from pydantic_ai._tool_manager import ToolDefinition # type: ignore + + +def execute_tool_span( + tool_name: str, + tool_args: "Any", + agent: "Any", + tool_definition: "Optional[ToolDefinition]" = None, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for tool execution. + + Args: + tool_name: The name of the tool being executed + tool_args: The arguments passed to the tool + agent: The agent executing the tool + tool_definition: The definition of the tool, if available + """ + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + set_on_span = span.set_data + + if tool_definition is not None and hasattr(tool_definition, "description"): + set_on_span( + SPANDATA.GEN_AI_TOOL_DESCRIPTION, + tool_definition.description, + ) + + _set_agent_data(span, agent) + + if _should_send_prompts() and tool_args is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + """Update the execute tool span with the result.""" + if not span: + return + + if not _should_send_prompts() or result is None: + return + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + else: + span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py new file mode 100644 index 0000000000..f0c68e85ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -0,0 +1,184 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, +) + +if TYPE_CHECKING: + from typing import Any, Union + +try: + from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore +except ImportError: + BinaryContent = None + ImageUrl = None + + +def invoke_agent_span( + user_prompt: "Any", + agent: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = False, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for invoking the agent.""" + # Determine agent name for span + name = "agent" + if agent and getattr(agent, "name", None): + name = agent.name + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + _set_available_tools(span, agent) + + # Add user prompt and system prompts if available and prompts are enabled + if _should_send_prompts(): + messages = [] + + # Add system prompts (both instructions and system_prompt) + system_texts = [] + + if agent: + # Check for system_prompt + system_prompts = getattr(agent, "_system_prompts", None) or [] + for prompt in system_prompts: + if isinstance(prompt, str): + system_texts.append(prompt) + + # Check for instructions (stored in _instructions) + instructions = getattr(agent, "_instructions", None) + if instructions: + if isinstance(instructions, str): + system_texts.append(instructions) + elif isinstance(instructions, (list, tuple)): + for instr in instructions: + if isinstance(instr, str): + system_texts.append(instr) + elif callable(instr): + # Skip dynamic/callable instructions + pass + + # Add all system texts as system messages + for system_text in system_texts: + messages.append( + { + "content": [{"text": system_text, "type": "text"}], + "role": "system", + } + ) + + # Add user prompt + if user_prompt: + if isinstance(user_prompt, str): + messages.append( + { + "content": [{"text": user_prompt, "type": "text"}], + "role": "user", + } + ) + elif isinstance(user_prompt, list): + # Handle list of user content + content = [] + for item in user_prompt: + if isinstance(item, str): + content.append({"text": item, "type": "text"}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + if content: + messages.append( + { + "content": content, + "role": "user", + } + ) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + result: "Any", +) -> None: + """Update and close the invoke agent span.""" + if not span or not result: + return + + # Extract output from result + output = getattr(result, "output", None) + + # Set response text if prompts are enabled + if _should_send_prompts() and output: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False + ) + + # Set model name from response if available + if hasattr(result, "response"): + try: + response = result.response + if hasattr(response, "model_name") and response.model_name: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + except Exception: + # If response access fails, continue without setting model name + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py new file mode 100644 index 0000000000..330496c6b2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py @@ -0,0 +1,87 @@ +"""Utility functions for PydanticAI span instrumentation.""" + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX +from sentry_sdk.ai.utils import get_modality_from_mime_type +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore + + +def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": + """Serialize an ImageUrl content item for span data. + + For data URLs containing base64-encoded images, the content is redacted. + For regular HTTP URLs, the URL string is preserved. + """ + url = str(item.url) + data_url_match = DATA_URL_BASE64_REGEX.match(url) + + if data_url_match: + return { + "type": "image", + "content": BLOB_DATA_SUBSTITUTE, + } + + return { + "type": "image", + "content": url, + } + + +def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": + """Serialize a BinaryContent item for span data, redacting the blob data.""" + return { + "type": "blob", + "modality": get_modality_from_mime_type(item.media_type), + "mime_type": item.media_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + usage: "Union[RequestUsage, RunUsage]", +) -> None: + """Set token usage data on a span. + + This function works with both RequestUsage (single request) and + RunUsage (agent run) objects from pydantic_ai. + + Args: + span: The Sentry span to set data on. + usage: RequestUsage or RunUsage object containing token usage information. + """ + if usage is None: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if hasattr(usage, "input_tokens") and usage.input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + + # Pydantic AI uses cache_read_tokens (not input_tokens_cached) + if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) + + # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) + if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + usage.cache_write_tokens, + ) + + if hasattr(usage, "output_tokens") and usage.output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + + if hasattr(usage, "total_tokens") and usage.total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py new file mode 100644 index 0000000000..340dcf8953 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py @@ -0,0 +1,233 @@ +from contextvars import ContextVar +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + +# Store the current agent context in a contextvar for re-entrant safety +# Using a list as a stack to support nested agent calls +_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( + "pydantic_ai_agent_context_stack", default=[] +) + + +def push_agent(agent: "Any", is_streaming: bool = False) -> None: + """Push an agent context onto the stack along with its streaming flag.""" + stack = _agent_context_stack.get().copy() + stack.append({"agent": agent, "is_streaming": is_streaming}) + _agent_context_stack.set(stack) + + +def pop_agent() -> None: + """Pop an agent context from the stack.""" + stack = _agent_context_stack.get().copy() + if stack: + stack.pop() + _agent_context_stack.set(stack) + + +def get_current_agent() -> "Any": + """Get the current agent from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1]["agent"] + return None + + +def get_is_streaming() -> bool: + """Get the streaming flag from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1].get("is_streaming", False) + return False + + +def _should_send_prompts() -> bool: + """ + Check if prompts should be sent to Sentry. + + This checks both send_default_pii and the include_prompts integration setting. + """ + if not should_send_default_pii(): + return False + + from . import PydanticAIIntegration + + # Get the integration instance from the client + integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) + + if integration is None: + return False + + return getattr(integration, "include_prompts", False) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set agent-related data on a span. + + Args: + span: The span to set data on + agent: Agent object (can be None, will try to get from contextvar if not provided) + """ + # Extract agent name from agent object or contextvar + agent_obj = agent + if not agent_obj: + # Try to get from contextvar + agent_obj = get_current_agent() + + if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + else: + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + + +def _get_model_name(model_obj: "Any") -> "Optional[str]": + """Extract model name from a model object. + + Args: + model_obj: Model object to extract name from + + Returns: + Model name string or None if not found + """ + if not model_obj: + return None + + if hasattr(model_obj, "model_name"): + return model_obj.model_name + elif hasattr(model_obj, "name"): + try: + return model_obj.name() + except Exception: + return str(model_obj) + elif isinstance(model_obj, str): + return model_obj + else: + return str(model_obj) + + +def _set_model_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + model: "Any", + model_settings: "Any", +) -> None: + """Set model-related data on a span. + + Args: + span: The span to set data on + model: Model object (can be None, will try to get from agent if not provided) + model_settings: Model settings (can be None, will try to get from agent if not provided) + """ + # Try to get agent from contextvar if we need it + agent_obj = get_current_agent() + + # Extract model information + model_obj = model + if not model_obj and agent_obj and hasattr(agent_obj, "model"): + model_obj = agent_obj.model + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if model_obj: + # Set system from model + if hasattr(model_obj, "system"): + set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) + + # Set model name + model_name = _get_model_name(model_obj) + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Extract model settings + settings = model_settings + if not settings and agent_obj and hasattr(agent_obj, "model_settings"): + settings = agent_obj.model_settings + + if settings: + settings_map = { + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + } + + # ModelSettings is a TypedDict (dict at runtime), so use dict access + if isinstance(settings, dict): + for setting_name, spandata_key in settings_map.items(): + value = settings.get(setting_name) + if value is not None: + set_on_span(spandata_key, value) + else: + # Fallback for object-style settings + for setting_name, spandata_key in settings_map.items(): + if hasattr(settings, setting_name): + value = getattr(settings, setting_name) + if value is not None: + set_on_span(spandata_key, value) + + +def _set_available_tools( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set available tools data on a span from an agent's function toolset. + + Args: + span: The span to set data on + agent: Agent object with _function_toolset attribute + """ + if not agent or not hasattr(agent, "_function_toolset"): + return + + try: + tools = [] + # Get tools from the function toolset + if hasattr(agent._function_toolset, "tools"): + for tool_name, tool in agent._function_toolset.tools.items(): + tool_info = {"name": tool_name} + + # Add description from function_schema if available + if hasattr(tool, "function_schema"): + schema = tool.function_schema + if getattr(schema, "description", None): + tool_info["description"] = schema.description + + # Add parameters from json_schema + if getattr(schema, "json_schema", None): + tool_info["parameters"] = schema.json_schema + + tools.append(tool_info) + + if tools: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + else: + span.set_data( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + except Exception: + # If we can't extract tools, just skip it + pass + + +def _capture_exception(exc: "Any", handled: bool = False) -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pydantic_ai", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py new file mode 100644 index 0000000000..2616f4d5a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py @@ -0,0 +1,262 @@ +import copy +import json + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +try: + from pymongo import monitoring +except ImportError: + raise DidNotEnable("Pymongo not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pymongo.monitoring import ( + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + ) + + +SAFE_COMMAND_ATTRIBUTES = [ + "insert", + "ordered", + "find", + "limit", + "singleBatch", + "aggregate", + "createIndexes", + "indexes", + "delete", + "findAndModify", + "renameCollection", + "to", + "drop", +] + + +def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": + for key in command: + is_safe_field = key in SAFE_COMMAND_ATTRIBUTES + if is_safe_field: + # Skip if safe key + continue + + update_db_command = key == "update" and "findAndModify" not in command + if update_db_command: + # Also skip "update" db command because it is save. + # There is also an "update" key in the "findAndModify" command, which is NOT safe! + continue + + # Special stripping for documents + is_document = key == "documents" + if is_document: + for doc in command[key]: + for doc_key in doc: + doc[doc_key] = "%s" + continue + + # Special stripping for dict style fields + is_dict_field = key in ["filter", "query", "update"] + if is_dict_field: + for item_key in command[key]: + command[key][item_key] = "%s" + continue + + # For pipeline fields strip the `$match` dict + is_pipeline_field = key == "pipeline" + if is_pipeline_field: + for pipeline in command[key]: + for match_key in pipeline["$match"] if "$match" in pipeline else []: + pipeline["$match"][match_key] = "%s" + continue + + # Default stripping + command[key] = "%s" + + return command + + +def _get_db_data(event: "Any") -> "Dict[str, Any]": + data = {} + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + data[SPANDATA.DB_DRIVER_NAME] = "pymongo" + db_name = event.database_name + + server_address = event.connection_id[0] + if server_address is not None: + data[SPANDATA.SERVER_ADDRESS] = server_address + + server_port = event.connection_id[1] + if server_port is not None: + data[SPANDATA.SERVER_PORT] = server_port + + if is_span_streaming_enabled: + data["db.system.name"] = "mongodb" + + if db_name is not None: + data["db.namespace"] = db_name + else: + data[SPANDATA.DB_SYSTEM] = "mongodb" + + if db_name is not None: + data[SPANDATA.DB_NAME] = db_name + + return data + + +class CommandTracer(monitoring.CommandListener): + def __init__(self) -> None: + self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} + + def _operation_key( + self, + event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", + ) -> int: + return event.request_id + + def started(self, event: "CommandStartedEvent") -> None: + client = sentry_sdk.get_client() + if client.get_integration(PyMongoIntegration) is None: + return + + with capture_internal_exceptions(): + command = dict(copy.deepcopy(event.command)) + + command.pop("$db", None) + command.pop("$clusterTime", None) + command.pop("$signature", None) + + db_data = _get_db_data(event) + + collection_name = command.get(event.command_name) + operation_name = event.command_name + db_name = event.database_name + + lsid = command.pop("lsid", None) + if not should_send_default_pii(): + command = _strip_pii(command) + + query = json.dumps(command, default=str) + + if has_span_streaming_enabled(client.options): + span_first_data = { + "db.operation.name": operation_name, + "db.collection.name": collection_name, + SPANDATA.DB_QUERY_TEXT: query, + "sentry.op": OP.DB, + "sentry.origin": PyMongoIntegration.origin, + **db_data, + } + + span = sentry_sdk.traces.start_span( + name=query, attributes=span_first_data + ) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, + category="query", + type=OP.DB, + data=span_first_data, + ) + + else: + tags = { + "db.name": db_name, + SPANDATA.DB_SYSTEM: "mongodb", + SPANDATA.DB_DRIVER_NAME: "pymongo", + SPANDATA.DB_OPERATION: operation_name, + # The below is a deprecated field, but leaving for legacy reasons. + # The v2 spans will use `db.collection.name` instead. + SPANDATA.DB_MONGODB_COLLECTION: collection_name, + } + + try: + tags["net.peer.name"] = event.connection_id[0] + tags["net.peer.port"] = str(event.connection_id[1]) + except TypeError: + pass + + data: "Dict[str, Any]" = {"operation_ids": {}} + data["operation_ids"]["operation"] = event.operation_id + data["operation_ids"]["request"] = event.request_id + + data.update(db_data) + + try: + if lsid: + lsid_id = lsid["id"] + data["operation_ids"]["session"] = str(lsid_id) + except KeyError: + pass + + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=PyMongoIntegration.origin, + ) + + for tag, value in tags.items(): + # set the tag for backwards-compatibility. + # TODO: remove the set_tag call in the next major release! + span.set_tag(tag, value) + span.set_data(tag, value) + + for key, value in data.items(): + span.set_data(key, value) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, category="query", type=OP.DB, data=tags + ) + + self._ongoing_operations[self._operation_key(event)] = span.__enter__() + + def failed(self, event: "CommandFailedEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + # Ignoring NoOpStreamedSpan as it will always have a status of "ok" + if type(span) is StreamedSpan: + span.status = SpanStatus.ERROR + elif type(span) is Span: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + span.__exit__(None, None, None) + except KeyError: + return + + def succeeded(self, event: "CommandSucceededEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + if type(span) is Span: + span.set_status(SPANSTATUS.OK) + span.__exit__(None, None, None) + except KeyError: + pass + + +class PyMongoIntegration(Integration): + identifier = "pymongo" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py new file mode 100644 index 0000000000..6837d8345c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py @@ -0,0 +1,239 @@ +import functools +import os +import sys +import weakref + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +try: + from pyramid.httpexceptions import HTTPException + from pyramid.request import Request +except ImportError: + raise DidNotEnable("Pyramid not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional + + from pyramid.response import Response + from webob.cookies import RequestCookies + from webob.request import _FieldStorageWithFile + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.utils import ExcInfo + + +if getattr(Request, "authenticated_userid", None): + + def authenticated_userid(request: "Request") -> "Optional[Any]": + return request.authenticated_userid + +else: + # bw-compat for pyramid < 1.5 + from pyramid.security import authenticated_userid # type: ignore + + +TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") + + +class PyramidIntegration(Integration): + identifier = "pyramid" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "route_name") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + from pyramid import router + + old_call_view = router._call_view + + @functools.wraps(old_call_view) + def sentry_patched_call_view( + registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + integration = client.get_integration(PyramidIntegration) + if integration is None: + return old_call_view(registry, request, *args, **kwargs) + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii() and has_span_streaming_enabled(client.options): + user_id = authenticated_userid(request) + if user_id: + scope.set_user({"id": user_id}) + + scope.add_event_processor( + _make_event_processor(weakref.ref(request), integration) + ) + + return old_call_view(registry, request, *args, **kwargs) + + router._call_view = sentry_patched_call_view + + if hasattr(Request, "invoke_exception_view"): + old_invoke_exception_view = Request.invoke_exception_view + + def sentry_patched_invoke_exception_view( + self: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + rv = old_invoke_exception_view(self, *args, **kwargs) + + if ( + self.exc_info + and all(self.exc_info) + and rv.status_int == 500 + and sentry_sdk.get_client().get_integration(PyramidIntegration) + is not None + ): + _capture_exception(self.exc_info) + + return rv + + Request.invoke_exception_view = sentry_patched_invoke_exception_view + + old_wsgi_call = router.Router.__call__ + + @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) + def sentry_patched_wsgi_call( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + def sentry_patched_inner_wsgi_call( + environ: "Dict[str, Any]", start_response: "Callable[..., Any]" + ) -> "Any": + try: + return old_wsgi_call(self, environ, start_response) + except Exception: + einfo = sys.exc_info() + _capture_exception(einfo) + reraise(*einfo) + + middleware = SentryWsgiMiddleware( + sentry_patched_inner_wsgi_call, + span_origin=PyramidIntegration.origin, + ) + return middleware(environ, start_response) + + router.Router.__call__ = sentry_patched_wsgi_call + + +@ensure_integration_enabled(PyramidIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + if exc_info[0] is None or issubclass(exc_info[0], HTTPException): + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pyramid", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "route_name": request.matched_route.name, + "route_pattern": request.matched_route.pattern, + } + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if is_span_streaming_enabled + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + scope.set_transaction_name( + name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +class PyramidRequestExtractor(RequestExtractor): + def url(self) -> str: + return self.request.path_url + + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "RequestCookies": + return self.request.cookies + + def raw_data(self) -> str: + return self.request.text + + def form(self) -> "Dict[str, str]": + return { + key: value + for key, value in self.request.POST.items() + if not getattr(value, "filename", None) + } + + def files(self) -> "Dict[str, _FieldStorageWithFile]": + return { + key: value + for key, value in self.request.POST.items() + if getattr(value, "filename", None) + } + + def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: + file = postdata.file + try: + return os.fstat(file.fileno()).st_size + except Exception: + return 0 + + +def _make_event_processor( + weak_request: "Callable[[], Request]", integration: "PyramidIntegration" +) -> "EventProcessor": + def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + PyramidRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_info = event.setdefault("user", {}) + user_info.setdefault("id", authenticated_userid(request)) + + return event + + return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py new file mode 100644 index 0000000000..aae68c4c10 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py @@ -0,0 +1,197 @@ +from contextlib import contextmanager +from typing import Any, Generator + +import sentry_sdk +from sentry_sdk import start_span +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + logger, + parse_url, +) + +try: + from pyreqwest.client import ( # type: ignore[import-not-found] + ClientBuilder, + SyncClientBuilder, + ) + from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] + from pyreqwest.request import ( # type: ignore[import-not-found] + OneOffRequestBuilder, + Request, + SyncOneOffRequestBuilder, + ) + from pyreqwest.response import ( # type: ignore[import-not-found] + Response, + SyncResponse, + ) +except ImportError: + raise DidNotEnable("pyreqwest not installed or incompatible version installed") + + +class PyreqwestIntegration(Integration): + identifier = "pyreqwest" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_pyreqwest() + + +def _patch_pyreqwest() -> None: + # Patch Client Builders + _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) + _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) + + # Patch Request Builders + _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) + _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) + + +def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: + if not hasattr(cls, method_name): + return + + original_method = getattr(cls, method_name) + + def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + if not getattr(self, "_sentry_instrumented", False): + integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) + if integration is not None: + self.with_middleware(middleware) + try: + self._sentry_instrumented = True + except (TypeError, AttributeError): + # In case the instance itself is immutable or doesn't allow extra attributes + pass + return original_method(self, *args, **kwargs) + + setattr(cls, method_name, sentry_patched_method) + + +@contextmanager +def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": PyreqwestIntegration.origin, + SPANDATA.HTTP_REQUEST_METHOD: request.method, + }, + ) as span: + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + return + + with start_span( + op=OP.HTTP_CLIENT, + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + origin=PyreqwestIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + +async def sentry_async_middleware( + request: "Request", next_handler: "Next" +) -> "Response": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return await next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = await next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response + + +def sentry_sync_middleware( + request: "Request", next_handler: "SyncNext" +) -> "SyncResponse": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py new file mode 100644 index 0000000000..6a5603d825 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py @@ -0,0 +1,292 @@ +import asyncio +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Union + + from sentry_sdk._types import Event, EventProcessor + +try: + import quart_auth # type: ignore +except ImportError: + quart_auth = None + +try: + from quart import ( # type: ignore + Quart, + Request, + has_request_context, + has_websocket_context, + request, + websocket, + ) + from quart.signals import ( # type: ignore + got_background_exception, + got_request_exception, + got_websocket_exception, + request_started, + websocket_started, + ) +except ImportError: + raise DidNotEnable("Quart is not installed") +else: + # Quart 0.19 is based on Flask and hence no longer has a Scaffold + try: + from quart.scaffold import Scaffold # type: ignore + except ImportError: + from flask.sansio.scaffold import Scaffold # type: ignore + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class QuartIntegration(Integration): + identifier = "quart" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "endpoint") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + request_started.connect(_request_websocket_started) + websocket_started.connect(_request_websocket_started) + got_background_exception.connect(_capture_exception) + got_request_exception.connect(_capture_exception) + got_websocket_exception.connect(_capture_exception) + + patch_asgi_app() + patch_scaffold_route() + + +def patch_asgi_app() -> None: + old_app = Quart.__call__ + + async def sentry_patched_asgi_app( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + if sentry_sdk.get_client().get_integration(QuartIntegration) is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=QuartIntegration.origin, + asgi_version=3, + ) + return await middleware(scope, receive, send) + + Quart.__call__ = sentry_patched_asgi_app + + +def patch_scaffold_route() -> None: + # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 + if sys.version_info >= (3, 12): + iscoroutinefunction = inspect.iscoroutinefunction + else: + iscoroutinefunction = asyncio.iscoroutinefunction + + old_route = Scaffold.route + + def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": + old_decorator = old_route(*args, **kwargs) + + def decorator(old_func: "Any") -> "Any": + if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): + + @wraps(old_func) + @ensure_integration_enabled(QuartIntegration, old_func) + def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + span = get_current_span() + if span is not None and hasattr(span, "_segment"): + span._segment._update_active_thread() + else: + current_scope = sentry_sdk.get_current_scope() + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_func(*args, **kwargs) + + return old_decorator(_sentry_func) + + return old_decorator(old_func) + + return decorator + + Scaffold.route = _sentry_route + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if has_span_streaming_enabled(sentry_sdk.get_client().options) + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + + scope.set_transaction_name( + name=name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + if has_request_context(): + request_websocket = request._get_current_object() + if has_websocket_context(): + request_websocket = websocket._get_current_object() + + # Set the transaction name here, but rely on ASGI middleware + # to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket + ) + + scope = sentry_sdk.get_isolation_scope() + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + current_span = get_current_span() + if type(current_span) is StreamedSpan: + segment = current_span._segment + + segment.set_attribute("http.request.method", request_websocket.method) + header_attributes: "dict[str, Any]" = {} + + for header, header_value in _filter_headers( + dict(request_websocket.headers), use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + segment.set_attributes(header_attributes) + + if should_send_default_pii(): + segment.set_attribute("url.full", request_websocket.url) + segment.set_attribute( + "url.query", + request_websocket.query_string.decode("utf-8", errors="replace"), + ) + + user_properties = {} + if len(request_websocket.access_route) >= 1: + segment.set_attribute( + "client.address", request_websocket.access_route[0] + ) + user_properties["ip_address"] = request_websocket.access_route[0] + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_properties["id"] = current_user_id + + if user_properties: + existing_user_properties = scope._user or {} + scope.set_user({**existing_user_properties, **user_properties}) + + evt_processor = _make_request_event_processor(app, request_websocket, integration) + scope.add_event_processor(evt_processor) + + +def _make_request_event_processor( + app: "Quart", request: "Request", integration: "QuartIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + # TODO: Figure out what to do with request body. Methods on request + # are async, but event processors are not. + + request_info = event.setdefault("request", {}) + request_info["url"] = request.url + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + if len(request.access_route) >= 1: + request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_info = event.setdefault("user", {}) + user_info["id"] = current_user_id + + return event + + return inner + + +async def _capture_exception( + sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "quart", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_current_user_id_from_quart() -> "str | None": + if quart_auth is None: + return None + + if quart_auth.current_user is None: + return None + + try: + return quart_auth.current_user._auth_id + except Exception: + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py new file mode 100644 index 0000000000..f723a96f3c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py @@ -0,0 +1,240 @@ +import functools +import inspect +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + event_from_exception, + logger, + package_version, + qualname_from_function, + reraise, +) + +try: + import ray # type: ignore[import-not-found] + from ray import remote +except ImportError: + raise DidNotEnable("Ray not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional + + from sentry_sdk.utils import ExcInfo + + +def _check_sentry_initialized() -> None: + if sentry_sdk.get_client().is_active(): + return + + logger.debug( + "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." + ) + + +def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: + # Patching new_func signature to add the _sentry_tracing parameter to it + # Ray later inspects the signature and finds the unexpected parameter otherwise + signature = inspect.signature(func) + params = list(signature.parameters.values()) + sentry_tracing_param = inspect.Parameter( + "_sentry_tracing", + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + ) + + # Keyword only arguments are penultimate if function has variadic keyword arguments + if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: + params.insert(-1, sentry_tracing_param) + else: + params.append(sentry_tracing_param) + + func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] + + +def _patch_ray_remote() -> None: + old_remote = remote + + @functools.wraps(old_remote) + def new_remote( + f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" + ) -> "Callable[..., Any]": + if inspect.isclass(f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(f, *args, **kwargs) + + def wrapper(user_f: "Callable[..., Any]") -> "Any": + if inspect.isclass(user_f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(*args, **kwargs)(user_f) + + @functools.wraps(user_f) + def new_func( + *f_args: "Any", + _sentry_tracing: "Optional[dict[str, Any]]" = None, + **f_kwargs: "Any", + ) -> "Any": + _check_sentry_initialized() + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(_sentry_tracing or {}) + + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RAY, + "sentry.origin": RayIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + }, + parent_span=None, + ): + try: + result = user_f(*f_args, **f_kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + transaction = sentry_sdk.continue_trace( + _sentry_tracing or {}, + op=OP.QUEUE_TASK_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + source=TransactionSource.TASK, + ) + + with sentry_sdk.start_transaction(transaction) as transaction: + try: + result = user_f(*f_args, **f_kwargs) + transaction.set_status(SPANSTATUS.OK) + except Exception: + transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + _insert_sentry_tracing_in_signature(new_func) + + if f: + rv = old_remote(new_func) + else: + rv = old_remote(*args, **kwargs)(new_func) + old_remote_method = rv.remote + + def _remote_method_with_header_propagation( + *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Ray Client + """ + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_RAY, + "sentry.origin": RayIntegration.origin, + }, + ): + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + ) as span: + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + span.set_status(SPANSTATUS.OK) + except Exception: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + rv.remote = _remote_method_with_header_propagation + + return rv + + if f is not None: + return wrapper(f) + else: + return wrapper + + ray.remote = new_remote + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={ + "handled": False, + "type": RayIntegration.identifier, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class RayIntegration(Integration): + identifier = "ray" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = package_version("ray") + _check_minimum_version(RayIntegration, version) + + _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py new file mode 100644 index 0000000000..7095721ed2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py @@ -0,0 +1,49 @@ +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE +from sentry_sdk.integrations.redis.rb import _patch_rb +from sentry_sdk.integrations.redis.redis import _patch_redis +from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster +from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +class RedisIntegration(Integration): + identifier = "redis" + + def __init__( + self, + max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, + cache_prefixes: "Optional[list[str]]" = None, + ) -> None: + self.max_data_size = max_data_size + self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] + + if max_data_size is not None: + warnings.warn( + "The `max_data_size` parameter of `RedisIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + try: + from redis import StrictRedis, client + except ImportError: + raise DidNotEnable("Redis client not installed") + + _patch_redis(StrictRedis, client) + _patch_redis_cluster() + _patch_rb() + + try: + _patch_rediscluster() + except Exception: + logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py new file mode 100644 index 0000000000..8fc3d0c3a9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py @@ -0,0 +1,177 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from redis.asyncio.client import Pipeline, StrictRedis + from redis.asyncio.cluster import ClusterPipeline, RedisCluster + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_async_pipeline( + pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return await old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + try: + command_seq = self._execution_strategy._command_queue + except AttributeError: + if is_cluster: + command_seq = self._command_stack + else: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.is_transaction, + command_seq, + ) + + return await old_execute(self, *args, **kwargs) + + pipeline_cls.execute = _sentry_execute # type: ignore + + +def patch_redis_async_client( + cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return await old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = await old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py new file mode 100644 index 0000000000..58d686b099 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_pipeline( + pipeline_cls: "Any", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + command_seq = None + try: + command_seq = self._execution_strategy.command_queue + except AttributeError: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.transaction, + command_seq, + ) + + return old_execute(self, *args, **kwargs) + + pipeline_cls.execute = sentry_patched_execute + + +def patch_redis_client( + cls: "Any", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + """ + This function can be used to instrument custom redis client classes or + subclasses. + """ + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py new file mode 100644 index 0000000000..0822c2c930 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py @@ -0,0 +1,19 @@ +SPAN_ORIGIN = "auto.db.redis" + +_SINGLE_KEY_COMMANDS = frozenset( + ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], +) +_MULTI_KEY_COMMANDS = frozenset( + [ + "del", + "touch", + "unlink", + "mget", + ], +) +_COMMANDS_INCLUDING_SENSITIVE_DATA = [ + "auth", +] +_MAX_NUM_ARGS = 10 # Trim argument lists to this many values +_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values +_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py new file mode 100644 index 0000000000..35f20fddd9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py @@ -0,0 +1,136 @@ +""" +Code used for the Caches module in Sentry +""" + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +GET_COMMANDS = ("get", "mget") +SET_COMMANDS = ("set", "setex") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _get_op(name: str) -> "Optional[str]": + op = None + if name.lower() in GET_COMMANDS: + op = OP.CACHE_GET + elif name.lower() in SET_COMMANDS: + op = OP.CACHE_PUT + + return op + + +def _compile_cache_span_properties( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> "dict[str, Any]": + key = _get_safe_key(redis_command, args, kwargs) + key_as_string = _key_as_string(key) + keys_as_string = key_as_string.split(", ") + + is_cache_key = False + for prefix in integration.cache_prefixes: + for kee in keys_as_string: + if kee.startswith(prefix): + is_cache_key = True + break + if is_cache_key: + break + + value = None + if redis_command.lower() in SET_COMMANDS: + value = args[-1] + + properties = { + "op": _get_op(redis_command), + "description": _get_cache_span_description( + redis_command, args, kwargs, integration + ), + "key": key, + "key_as_string": key_as_string, + "redis_command": redis_command.lower(), + "is_cache_key": is_cache_key, + "value": value, + } + + return properties + + +def _get_cache_span_description( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> str: + description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_cache_data( + span: "Union[Span, StreamedSpan]", + redis_client: "Any", + properties: "dict[str, Any]", + return_value: "Optional[Any]", +) -> None: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + with capture_internal_exceptions(): + set_on_span(SPANDATA.CACHE_KEY, properties["key"]) + + if properties["redis_command"] in GET_COMMANDS: + if return_value is not None: + set_on_span(SPANDATA.CACHE_HIT, True) + size = ( + len(str(return_value).encode("utf-8")) + if not isinstance(return_value, bytes) + else len(return_value) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + else: + set_on_span(SPANDATA.CACHE_HIT, False) + + elif properties["redis_command"] in SET_COMMANDS: + if properties["value"] is not None: + size = ( + len(properties["value"].encode("utf-8")) + if not isinstance(properties["value"], bytes) + else len(properties["value"]) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + + try: + connection_params = redis_client.connection_pool.connection_kwargs + except AttributeError: + # If it is a cluster, there is no connection_pool attribute so we + # need to get the default node from the cluster instance + default_node = redis_client.get_default_node() + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + + host = connection_params.get("host") + if host is not None: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) + + port = connection_params.get("port") + if port is not None: + set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py new file mode 100644 index 0000000000..69207bf6f6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py @@ -0,0 +1,88 @@ +""" +Code used for the Queries module in Sentry +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_command +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import Redis + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _compile_db_span_properties( + integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" +) -> "dict[str, Any]": + description = _get_db_span_description(integration, redis_command, args) + + properties = { + "op": OP.DB_REDIS, + "description": description, + } + + return properties + + +def _get_db_span_description( + integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" +) -> str: + description = command_name + + with capture_internal_exceptions(): + description = _get_safe_command(command_name, args) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_db_data_on_span( + span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" +) -> None: + db = connection_params.get("db") + host = connection_params.get("host") + port = connection_params.get("port") + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) + + if host is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_attribute(SPANDATA.SERVER_PORT, port) + + else: + span.set_data(SPANDATA.DB_SYSTEM, "redis") + span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_data(SPANDATA.DB_NAME, str(db)) + + if host is not None: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_data(SPANDATA.SERVER_PORT, port) + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" +) -> None: + try: + _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) + except AttributeError: + pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py new file mode 100644 index 0000000000..e2ce863fe8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py @@ -0,0 +1,31 @@ +""" +Instrumentation for Redis Blaster (rb) + +https://github.com/getsentry/rb +""" + +from sentry_sdk.integrations.redis._sync_common import patch_redis_client +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + + +def _patch_rb() -> None: + try: + import rb.clients # type: ignore + except ImportError: + pass + else: + patch_redis_client( + rb.clients.FanoutClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.MappingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.RoutingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py new file mode 100644 index 0000000000..e704c9bc6a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py @@ -0,0 +1,67 @@ +""" +Instrumentation for Redis + +https://github.com/redis/redis-py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + +if TYPE_CHECKING: + from typing import Any, Sequence + + +def _get_redis_command_args(command: "Any") -> "Sequence[Any]": + return command[0] + + +def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 + patch_redis_client( + StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_pipeline( + client.Pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + try: + strict_pipeline = client.StrictPipeline + except AttributeError: + pass + else: + patch_redis_pipeline( + strict_pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + + try: + import redis.asyncio + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + redis.asyncio.client.StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_async_pipeline( + redis.asyncio.client.Pipeline, + False, + _get_redis_command_args, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py new file mode 100644 index 0000000000..b6c95e6abd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py @@ -0,0 +1,115 @@ +""" +Instrumentation for RedisCluster +This is part of the main redis-py client. + +https://github.com/redis/redis-py/blob/master/redis/cluster.py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import RedisCluster + from redis.asyncio.cluster import ( + ClusterPipeline as AsyncClusterPipeline, + ) + from redis.asyncio.cluster import ( + RedisCluster as AsyncRedisCluster, + ) + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +def _set_async_cluster_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_instance: "AsyncRedisCluster[Any]", +) -> None: + default_node = async_redis_cluster_instance.get_default_node() + if default_node is not None and default_node.connection_kwargs is not None: + _set_db_data_on_span(span, default_node.connection_kwargs) + + +def _set_async_cluster_pipeline_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", +) -> None: + with capture_internal_exceptions(): + client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) + if client is None: + # In older redis-py versions, the AsyncClusterPipeline had a `_client` + # attr but it is private so potentially problematic and mypy does not + # recognize it - see + # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 + client = ( + async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] + ) + + _set_async_cluster_db_data( + span, + client, + ) + + +def _set_cluster_db_data( + span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" +) -> None: + default_node = redis_cluster_instance.get_default_node() + + if default_node is not None: + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + _set_db_data_on_span(span, connection_params) + + +def _patch_redis_cluster() -> None: + """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" + try: + from redis import RedisCluster, cluster + except ImportError: + pass + else: + patch_redis_client( + RedisCluster, + is_cluster=True, + set_db_data_fn=_set_cluster_db_data, + ) + patch_redis_pipeline( + cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_cluster_db_data, + ) + + try: + from redis.asyncio import cluster as async_cluster + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + async_cluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_async_cluster_db_data, + ) + patch_redis_async_pipeline( + async_cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_async_cluster_pipeline_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py new file mode 100644 index 0000000000..3437aa1f2f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py @@ -0,0 +1,49 @@ +""" +Instrumentation for redis-py-cluster +The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). + +https://github.com/grokzen/redis-py-cluster +""" + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command + + +def _patch_rediscluster() -> None: + try: + import rediscluster # type: ignore + except ImportError: + return + + patch_redis_client( + rediscluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + + # up to v1.3.6, __version__ attribute is a tuple + # from v2.0.0, __version__ is a string and VERSION a tuple + version = getattr(rediscluster, "VERSION", rediscluster.__version__) + + # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 + # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst + if (0, 2, 0) < version < (2, 0, 0): + pipeline_cls = rediscluster.pipeline.StrictClusterPipeline + patch_redis_client( + rediscluster.StrictRedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + else: + pipeline_cls = rediscluster.pipeline.ClusterPipeline + + patch_redis_pipeline( + pipeline_cls, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py new file mode 100644 index 0000000000..7e04df9c69 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py @@ -0,0 +1,159 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations.redis.consts import ( + _COMMANDS_INCLUDING_SENSITIVE_DATA, + _MAX_NUM_ARGS, + _MAX_NUM_COMMANDS, + _MULTI_KEY_COMMANDS, + _SINGLE_KEY_COMMANDS, +) +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + +if TYPE_CHECKING: + from typing import Any, Optional, Sequence, Union + + +def _get_safe_command(name: str, args: "Sequence[Any]") -> str: + command_parts = [name] + + name_low = name.lower() + send_default_pii = should_send_default_pii() + + for i, arg in enumerate(args): + if i > _MAX_NUM_ARGS: + break + + if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + continue + + arg_is_the_key = i == 0 + if arg_is_the_key: + command_parts.append(repr(arg)) + else: + if send_default_pii: + command_parts.append(repr(arg)) + else: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + + command = " ".join(command_parts) + return command + + +def _safe_decode(key: "Any") -> str: + if isinstance(key, bytes): + try: + return key.decode() + except UnicodeDecodeError: + return "" + + return str(key) + + +def _key_as_string(key: "Any") -> str: + if isinstance(key, (dict, list, tuple)): + key = ", ".join(_safe_decode(x) for x in key) + elif isinstance(key, bytes): + key = _safe_decode(key) + elif key is None: + key = "" + else: + key = str(key) + + return key + + +def _get_safe_key( + method_name: str, + args: "Optional[tuple[Any, ...]]", + kwargs: "Optional[dict[str, Any]]", +) -> "Optional[tuple[str, ...]]": + """ + Gets the key (or keys) from the given method_name. + The method_name could be a redis command or a django caching command + """ + key = None + + if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: + # for example redis "mget" + key = tuple(args) + + elif args is not None and len(args) >= 1: + # for example django "set_many/get_many" or redis "get" + if isinstance(args[0], (dict, list, tuple)): + key = tuple(args[0]) + else: + key = (args[0],) + + elif kwargs is not None and "key" in kwargs: + # this is a legacy case for older versions of Django + if isinstance(kwargs["key"], (list, tuple)): + if len(kwargs["key"]) > 0: + key = tuple(kwargs["key"]) + else: + if kwargs["key"] is not None: + key = (kwargs["key"],) + + return key + + +def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": + return command.args + + +def _set_pipeline_data( + span: "Union[Span, StreamedSpan]", + is_cluster: bool, + get_command_args_fn: "Any", + is_transaction: bool, + commands_seq: "Sequence[Any]", +) -> None: + # TODO: Remove this whole function when removing transaction based tracing + if isinstance(span, StreamedSpan): + return + + span.set_tag("redis.is_cluster", is_cluster) + span.set_tag("redis.transaction", is_transaction) + + commands = [] + for i, arg in enumerate(commands_seq): + if i >= _MAX_NUM_COMMANDS: + break + + command = get_command_args_fn(arg) + commands.append(_get_safe_command(command[0], command[1:])) + + span.set_data( + "redis.commands", + { + "count": len(commands_seq), + "first_ten": commands, + }, + ) + + +def _set_client_data( + span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" +) -> None: + if isinstance(span, StreamedSpan): + if name: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) + else: + span.set_tag("redis.is_cluster", is_cluster) + if name: + span.set_tag("redis.command", name) + span.set_tag(SPANDATA.DB_OPERATION, name) + + if name and args: + name_low = name.lower() + if (name_low in _SINGLE_KEY_COMMANDS) or ( + name_low in _MULTI_KEY_COMMANDS and len(args) == 1 + ): + if isinstance(span, StreamedSpan): + span.set_attribute("db.redis.key", args[0]) + else: + span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py new file mode 100644 index 0000000000..edd48a9f67 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py @@ -0,0 +1,225 @@ +import functools +import weakref + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + format_timestamp, + parse_version, +) + +try: + from rq.job import JobStatus + from rq.queue import Queue + from rq.timeouts import JobTimeoutException + from rq.version import VERSION as RQ_VERSION + from rq.worker import Worker +except ImportError: + raise DidNotEnable("RQ not installed") + +try: + from rq.worker import BaseWorker + + if not hasattr(BaseWorker, "perform_job"): + BaseWorker = None +except ImportError: + BaseWorker = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + from rq.job import Job + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + +class RqIntegration(Integration): + identifier = "rq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(RQ_VERSION) + _check_minimum_version(RqIntegration, version) + + # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly + # instead of Worker, so we need to patch BaseWorker to cover both. + # For older versions where BaseWorker doesn't exist or doesn't have + # perform_job, we patch Worker. + worker_cls = BaseWorker if BaseWorker is not None else Worker + + old_perform_job = worker_cls.perform_job + + @functools.wraps(old_perform_job) + def sentry_patched_perform_job( + self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" + ) -> bool: + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_perform_job(self, job, *args, **kwargs) + + with sentry_sdk.new_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(weakref.ref(job))) + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace( + job.meta.get("_sentry_trace_headers") or {} + ) + + Scope.set_custom_sampling_context({"rq_job": job}) + + func_name = None + with capture_internal_exceptions(): + func_name = job.func_name + + with sentry_sdk.traces.start_span( + name="unknown RQ task" if func_name is None else func_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RQ, + "sentry.origin": RqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job.id, + }, + parent_span=None, + ) as span: + if func_name is not None: + span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) + + rv = old_perform_job(self, job, *args, **kwargs) + else: + transaction = continue_trace( + job.meta.get("_sentry_trace_headers") or {}, + op=OP.QUEUE_TASK_RQ, + name="unknown RQ task", + source=TransactionSource.TASK, + origin=RqIntegration.origin, + ) + + with capture_internal_exceptions(): + transaction.name = job.func_name + + with sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"rq_job": job}, + ): + rv = old_perform_job(self, job, *args, **kwargs) + + if self.is_horse: + # We're inside of a forked process and RQ is + # about to call `os._exit`. Make sure that our + # events get sent out. + sentry_sdk.get_client().flush() + + return rv + + worker_cls.perform_job = sentry_patched_perform_job + + old_handle_exception = worker_cls.handle_exception + + def sentry_patched_handle_exception( + self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" + ) -> "Any": + retry = ( + hasattr(job, "retries_left") + and job.retries_left + and job.retries_left > 0 + ) + failed = job._status == JobStatus.FAILED or job.is_failed + if failed and not retry: + _capture_exception(exc_info) + + return old_handle_exception(self, job, *exc_info, **kwargs) + + worker_cls.handle_exception = sentry_patched_handle_exception + + old_enqueue_job = Queue.enqueue_job + + @functools.wraps(old_enqueue_job) + def sentry_patched_enqueue_job( + self: "Queue", job: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_enqueue_job(self, job, **kwargs) + + scope = sentry_sdk.get_current_scope() + span = ( + scope.streamed_span + if has_span_streaming_enabled(client.options) + else scope.span + ) + if span is not None: + job.meta["_sentry_trace_headers"] = dict( + scope.iter_trace_propagation_headers() + ) + + return old_enqueue_job(self, job, **kwargs) + + Queue.enqueue_job = sentry_patched_enqueue_job + + ignore_logger("rq.worker") + + +def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + job = weak_job() + if job is not None: + with capture_internal_exceptions(): + extra = event.setdefault("extra", {}) + rq_job = { + "job_id": job.id, + "func": job.func_name, + "args": ( + job.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + job.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "description": job.description, + } + + if job.enqueued_at: + rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) + if job.started_at: + rq_job["started_at"] = format_timestamp(job.started_at) + + extra["rq-job"] = rq_job + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], JobTimeoutException): + event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "rq", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py new file mode 100644 index 0000000000..622e3c17af --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py @@ -0,0 +1,300 @@ +""" +This integration ingests tracing data from native extensions written in Rust. + +Using it requires additional setup on the Rust side to accept a +`RustTracingLayer` Python object and register it with the `tracing-subscriber` +using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: +```rust +#[pyfunction] +pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { + tracing_subscriber::registry() + .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) + .init(); +} +``` + +Usage in Python would then look like: +``` +sentry_sdk.init( + dsn=sentry_dsn, + integrations=[ + RustTracingIntegration( + "demo_rust_extension", + demo_rust_extension.initialize_tracing, + event_type_mapping=event_type_mapping, + ) + ], +) +``` + +Each native extension requires its own integration. +""" + +import json +from enum import Enum, auto +from typing import Any, Callable, Dict, Optional, Union + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + + +class RustTracingLevel(Enum): + Trace = "TRACE" + Debug = "DEBUG" + Info = "INFO" + Warn = "WARN" + Error = "ERROR" + + +class EventTypeMapping(Enum): + Ignore = auto() + Exc = auto() + Breadcrumb = auto() + Event = auto() + + +def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": + level = RustTracingLevel(level) + if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): + return "debug" + elif level == RustTracingLevel.Info: + return "info" + elif level == RustTracingLevel.Warn: + return "warning" + elif level == RustTracingLevel.Error: + return "error" + else: + # Better this than crashing + return "info" + + +def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": + metadata = event.get("metadata", {}) + contexts = {} + + location = {} + for field in ["module_path", "file", "line"]: + if field in metadata: + location[field] = metadata[field] + if len(location) > 0: + contexts["rust_tracing_location"] = location + + fields = {} + for field in metadata.get("fields", []): + fields[field] = event.get(field) + if len(fields) > 0: + contexts["rust_tracing_fields"] = fields + + return contexts + + +def process_event(event: "Dict[str, Any]") -> None: + metadata = event.get("metadata", {}) + + logger = metadata.get("target") + level = tracing_level_to_sentry_level(metadata.get("level")) + message: "sentry_sdk._types.Any" = event.get("message") + contexts = extract_contexts(event) + + sentry_event: "sentry_sdk._types.Event" = { + "logger": logger, + "level": level, + "message": message, + "contexts": contexts, + } + + sentry_sdk.capture_event(sentry_event) + + +def process_exception(event: "Dict[str, Any]") -> None: + process_event(event) + + +def process_breadcrumb(event: "Dict[str, Any]") -> None: + level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) + message = event.get("message") + + sentry_sdk.add_breadcrumb(level=level, message=message) + + +def default_span_filter(metadata: "Dict[str, Any]") -> bool: + return RustTracingLevel(metadata.get("level")) in ( + RustTracingLevel.Error, + RustTracingLevel.Warn, + RustTracingLevel.Info, + ) + + +def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": + level = RustTracingLevel(metadata.get("level")) + if level == RustTracingLevel.Error: + return EventTypeMapping.Exc + elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): + return EventTypeMapping.Breadcrumb + elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): + return EventTypeMapping.Ignore + else: + return EventTypeMapping.Ignore + + +class RustTracingLayer: + def __init__( + self, + origin: str, + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.origin = origin + self.event_type_mapping = event_type_mapping + self.span_filter = span_filter + self.include_tracing_fields = include_tracing_fields + + def _include_tracing_fields(self) -> bool: + """ + By default, the values of tracing fields are not included in case they + contain PII. A user may override that by passing `True` for the + `include_tracing_fields` keyword argument of this integration or by + setting `send_default_pii` to `True` in their Sentry client options. + """ + return ( + should_send_default_pii() + if self.include_tracing_fields is None + else self.include_tracing_fields + ) + + def on_event(self, event: str, sentry_span: "SentrySpan") -> None: + deserialized_event = json.loads(event) + metadata = deserialized_event.get("metadata", {}) + + event_type = self.event_type_mapping(metadata) + if event_type == EventTypeMapping.Ignore: + return + elif event_type == EventTypeMapping.Exc: + process_exception(deserialized_event) + elif event_type == EventTypeMapping.Breadcrumb: + process_breadcrumb(deserialized_event) + elif event_type == EventTypeMapping.Event: + process_event(deserialized_event) + + def on_new_span( + self, attrs: str, span_id: str + ) -> "Optional[Union[SentrySpan, StreamedSpan]]": + attrs = json.loads(attrs) + metadata = attrs.get("metadata", {}) + + if not self.span_filter(metadata): + return None + + module_path = metadata.get("module_path") + name = metadata.get("name") + message = attrs.get("message") + + if message is not None: + sentry_span_name = message + elif module_path is not None and name is not None: + sentry_span_name = f"{module_path}::{name}" # noqa: E231 + elif name is not None: + sentry_span_name = name + else: + sentry_span_name = "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + sentry_span = sentry_sdk.traces.start_span( + name=sentry_span_name, + attributes={ + "sentry.op": "function", + "sentry.origin": self.origin, + }, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_attribute(field, attrs.get(field)) + else: + sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) + + return sentry_span + + sentry_span = sentry_sdk.start_span( + op="function", + name=sentry_span_name, + origin=self.origin, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_data(field, attrs.get(field)) + else: + sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) + + sentry_span.__enter__() + return sentry_span + + def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: + if sentry_span is None: + return + + sentry_span.__exit__(None, None, None) + + def on_record( + self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" + ) -> None: + if sentry_span is None: + return + + set_on_span = ( + sentry_span.set_attribute + if isinstance(sentry_span, StreamedSpan) + else sentry_span.set_data + ) + + deserialized_values = json.loads(values) + for key, value in deserialized_values.items(): + if self._include_tracing_fields(): + set_on_span(key, value) + else: + set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) + + +class RustTracingIntegration(Integration): + """ + Ingests tracing data from a Rust native extension's `tracing` instrumentation. + + If a project uses more than one Rust native extension, each one will need + its own instance of `RustTracingIntegration` with an initializer function + specific to that extension. + + Since all of the setup for this integration requires instance-specific state + which is not available in `setup_once()`, setup instead happens in `__init__()`. + """ + + def __init__( + self, + identifier: str, + initializer: "Callable[[RustTracingLayer], None]", + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.identifier = identifier + origin = f"auto.function.rust_tracing.{identifier}" + self.tracing_layer = RustTracingLayer( + origin, event_type_mapping, span_filter, include_tracing_fields + ) + + initializer(self.tracing_layer) + + @staticmethod + def setup_once() -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py new file mode 100644 index 0000000000..908fceb0cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py @@ -0,0 +1,431 @@ +import sys +import warnings +import weakref +from inspect import isawaitable +from typing import TYPE_CHECKING +from urllib.parse import urlsplit + +import sentry_sdk +from sentry_sdk import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +if TYPE_CHECKING: + from collections.abc import Container + from typing import Any, Callable, Dict, Optional, Union + + from sanic.request import Request, RequestParameters + from sanic.response import BaseHTTPResponse + from sanic.router import Route + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +try: + from sanic import Sanic + from sanic import __version__ as SANIC_VERSION + from sanic.exceptions import SanicException + from sanic.handlers import ErrorHandler + from sanic.router import Router +except ImportError: + raise DidNotEnable("Sanic not installed") + +old_error_handler_lookup = ErrorHandler.lookup +old_handle_request = Sanic.handle_request +old_router_get = Router.get + +try: + # This method was introduced in Sanic v21.9 + old_startup = Sanic._startup +except AttributeError: + pass + + +class SanicIntegration(Integration): + identifier = "sanic" + origin = f"auto.http.{identifier}" + version: "Optional[tuple[int, ...]]" = None + + def __init__( + self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) + ) -> None: + """ + The unsampled_statuses parameter can be used to specify for which HTTP statuses the + transactions should not be sent to Sentry. By default, transactions are sent for all + HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all + HTTP statuses, including 404. + """ + self._unsampled_statuses = unsampled_statuses or set() + + @staticmethod + def setup_once() -> None: + SanicIntegration.version = parse_version(SANIC_VERSION) + _check_minimum_version(SanicIntegration, SanicIntegration.version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The sanic integration for Sentry requires Python 3.7+ " + " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + if SANIC_VERSION.startswith("0.8."): + # Sanic 0.8 and older creates a logger named "root" and puts a + # stringified version of every exception in there (without exc_info), + # which our error deduplication can't detect. + # + # We explicitly check the version here because it is a very + # invasive step to ignore this logger and not necessary in newer + # versions at all. + # + # https://github.com/huge-success/sanic/issues/1332 + ignore_logger("root") + + if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): + _setup_legacy_sanic() + return + + _setup_sanic() + + +class SanicRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return dict(self.request.cookies) + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "RequestParameters": + return self.request.form + + def is_json(self) -> bool: + raise NotImplementedError() + + def json(self) -> "Optional[Any]": + return self.request.json + + def files(self) -> "RequestParameters": + return self.request.files + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) + + +def _setup_sanic() -> None: + Sanic._startup = _startup + ErrorHandler.lookup = _sentry_error_handler_lookup + + +def _setup_legacy_sanic() -> None: + Sanic.handle_request = _legacy_handle_request + Router.get = _legacy_router_get + ErrorHandler.lookup = _sentry_error_handler_lookup + + +async def _startup(self: "Sanic") -> None: + # This happens about as early in the lifecycle as possible, just after the + # Request object is created. The body has not yet been consumed. + self.signal("http.lifecycle.request")(_context_enter) + + # This happens after the handler is complete. In v21.9 this signal is not + # dispatched when there is an exception. Therefore we need to close out + # and call _context_exit from the custom exception handler as well. + # See https://github.com/sanic-org/sanic/issues/2297 + self.signal("http.lifecycle.response")(_context_exit) + + # This happens inside of request handling immediately after the route + # has been identified by the router. + self.signal("http.routing.after")(_set_transaction) + + # The above signals need to be declared before this can be called. + await old_startup(self) + + +async def _context_enter(request: "Request") -> None: + request.ctx._sentry_do_integration = ( + sentry_sdk.get_client().get_integration(SanicIntegration) is not None + ) + + if not request.ctx._sentry_do_integration: + return + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + weak_request = weakref.ref(request) + request.ctx._sentry_scope = sentry_sdk.isolation_scope() + scope = request.ctx._sentry_scope.__enter__() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + if is_span_streaming_enabled: + integration = client.get_integration(SanicIntegration) + if ( + isinstance(integration, SanicIntegration) + and integration._unsampled_statuses + ): + warnings.warn( + "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", + stacklevel=2, + ) + + sentry_sdk.traces.continue_trace(dict(request.headers)) + scope.set_custom_sampling_context({"sanic_request": request}) + + if should_send_default_pii() and request.remote_addr: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + + span = sentry_sdk.traces.start_span( + # Unless the request results in a 404 error, the name and source + # will get overwritten in _set_transaction + name=request.path, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": SanicIntegration.origin, + "sentry.span.source": SegmentSource.URL.value, + }, + parent_span=None, + ) + request.ctx._sentry_root_span = span + else: + transaction = continue_trace( + dict(request.headers), + op=OP.HTTP_SERVER, + # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction + name=request.path, + source=TransactionSource.URL, + origin=SanicIntegration.origin, + ) + request.ctx._sentry_root_span = sentry_sdk.start_transaction( + transaction + ).__enter__() + + +async def _context_exit( + request: "Request", response: "Optional[BaseHTTPResponse]" = None +) -> None: + with capture_internal_exceptions(): + if not request.ctx._sentry_do_integration: + return + + integration = sentry_sdk.get_client().get_integration(SanicIntegration) + + response_status = None if response is None else response.status + + # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception + # happens while trying to end the transaction, we still attempt to exit the hub. + with capture_internal_exceptions(): + span = request.ctx._sentry_root_span + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes(request).items(): + span.set_attribute(attr, value) + if response_status is not None: + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) + span.status = "error" if response_status >= 400 else "ok" + + span.end() + + else: + span.set_http_status(response_status) + span.sampled &= ( + isinstance(integration, SanicIntegration) + and response_status not in integration._unsampled_statuses + ) + + span.__exit__(None, None, None) + + request.ctx._sentry_scope.__exit__(None, None, None) + + +async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: + if request.ctx._sentry_do_integration: + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + route_name = route.name.replace(request.app.name, "").strip(".") + scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) + + +def _sentry_error_handler_lookup( + self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" +) -> "Optional[object]": + _capture_exception(exception) + old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) + + if old_error_handler is None: + return None + + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return old_error_handler + + async def sentry_wrapped_error_handler( + request: "Request", exception: Exception + ) -> "Any": + try: + response = old_error_handler(request, exception) + if isawaitable(response): + response = await response + return response + except Exception: + # Report errors that occur in Sanic error handler. These + # exceptions will not even show up in Sanic's + # `sanic.exceptions` logger. + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + finally: + # As mentioned in previous comment in _startup, this can be removed + # after https://github.com/sanic-org/sanic/issues/2297 is resolved + if SanicIntegration.version and SanicIntegration.version == (21, 9): + await _context_exit(request) + + return sentry_wrapped_error_handler + + +async def _legacy_handle_request( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" +) -> "Any": + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return await old_handle_request(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + response = old_handle_request(self, request, *args, **kwargs) + if isawaitable(response): + response = await response + + return response + + +def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": + rv = old_router_get(self, *args) + if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: + with capture_internal_exceptions(): + scope = sentry_sdk.get_isolation_scope() + if SanicIntegration.version and SanicIntegration.version >= (21, 3): + # Sanic versions above and including 21.3 append the app name to the + # route name, and so we need to remove it from Route name so the + # transaction name is consistent across all versions + sanic_app_name = self.ctx.app.name + sanic_route = rv[0].name + + if sanic_route.startswith("%s." % sanic_app_name): + # We add a 1 to the len of the sanic_app_name because there is a dot + # that joins app name and the route name + # Format: app_name.route_name + sanic_route = sanic_route[len(sanic_app_name) + 1 :] + + scope.set_transaction_name( + sanic_route, source=TransactionSource.COMPONENT + ) + else: + scope.set_transaction_name( + rv[0].__name__, source=TransactionSource.COMPONENT + ) + + return rv + + +@ensure_integration_enabled(SanicIntegration) +def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "sanic", "handled": False}, + ) + + if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: + return + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_request_attributes(request: "Request") -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from a Sanic request. + """ + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + urlparts = urlsplit(request.url) + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.url + attributes["url.path"] = urlparts.path + + if urlparts.query: + attributes[SPANDATA.HTTP_QUERY] = urlparts.query + + if urlparts.scheme: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme + + if should_send_default_pii() and request.remote_addr: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr + + return attributes + + +def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": + def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + try: + if hint and issubclass(hint["exc_info"][0], SanicException): + return None + except KeyError: + pass + + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + extractor = SanicRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + urlparts = urlsplit(request.url) + + request_info["url"] = "%s://%s%s" % ( + urlparts.scheme, + urlparts.netloc, + urlparts.path, + ) + + request_info["query_string"] = urlparts.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_addr} + request_info["headers"] = _filter_headers(dict(request.headers)) + + return event + + return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py new file mode 100644 index 0000000000..16f91b28ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py @@ -0,0 +1,65 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import event_from_exception, reraise + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union, overload + + F = TypeVar("F", bound=Callable[..., Any]) + +else: + + def overload(x: "F") -> "F": + return x + + +@overload +def serverless_function(f: "F", flush: bool = True) -> "F": + pass + + +@overload +def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 + pass + + +def serverless_function( # noqa + f: "Optional[F]" = None, flush: bool = True +) -> "Union[F, Callable[[F], F]]": + def wrapper(f: "F") -> "F": + @wraps(f) + def inner(*args: "Any", **kwargs: "Any") -> "Any": + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + + try: + return f(*args, **kwargs) + except Exception: + _capture_and_reraise() + finally: + if flush: + sentry_sdk.flush() + + return inner # type: ignore + + if f is None: + return wrapper + else: + return wrapper(f) + + +def _capture_and_reraise() -> None: + exc_info = sys.exc_info() + client = sentry_sdk.get_client() + if client.is_active(): + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "serverless", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py new file mode 100644 index 0000000000..775170fb9f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py @@ -0,0 +1,142 @@ +import socket + +import sentry_sdk +from sentry_sdk._types import MYPY +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if MYPY: + from socket import AddressFamily, SocketKind + from typing import List, Optional, Tuple, Union + +__all__ = ["SocketIntegration"] + + +class SocketIntegration(Integration): + identifier = "socket" + origin = f"auto.socket.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) + """ + _patch_create_connection() + _patch_getaddrinfo() + + +def _get_span_description( + host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" +) -> str: + try: + host = host.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + try: + port = port.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + description = "%s:%s" % (host, port) # type: ignore + return description + + +def _patch_create_connection() -> None: + real_create_connection = socket.create_connection + + def create_connection( + address: "Tuple[Optional[str], int]", + timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore + source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, + ) -> "socket.socket": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_create_connection(address, timeout, source_address) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(address[0], address[1]), + attributes={ + "sentry.op": OP.SOCKET_CONNECTION, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if address[0] is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) + span.set_attribute(SPANDATA.SERVER_PORT, address[1]) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_CONNECTION, + name=_get_span_description(address[0], address[1]), + origin=SocketIntegration.origin, + ) as span: + span.set_data("address", address) + span.set_data("timeout", timeout) + span.set_data("source_address", source_address) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + + socket.create_connection = create_connection # type: ignore + + +def _patch_getaddrinfo() -> None: + real_getaddrinfo = socket.getaddrinfo + + def getaddrinfo( + host: "Union[bytes, str, None]", + port: "Union[bytes, str, int, None]", + family: int = 0, + type: int = 0, + proto: int = 0, + flags: int = 0, + ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_getaddrinfo(host, port, family, type, proto, flags) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(host, port), + attributes={ + "sentry.op": OP.SOCKET_DNS, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if isinstance(host, str): + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + elif isinstance(host, bytes): + span.set_attribute( + SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") + ) + + if isinstance(port, int): + span.set_attribute(SPANDATA.SERVER_PORT, port) + elif port is not None: + try: + span.set_attribute(SPANDATA.SERVER_PORT, int(port)) + except (ValueError, TypeError): + pass + + return real_getaddrinfo(host, port, family, type, proto, flags) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_DNS, + name=_get_span_description(host, port), + origin=SocketIntegration.origin, + ) as span: + span.set_data("host", host) + span.set_data("port", port) + + return real_getaddrinfo(host, port, family, type, proto, flags) + + socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py new file mode 100644 index 0000000000..10d94163c5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py @@ -0,0 +1,4 @@ +from sentry_sdk.integrations.spark.spark_driver import SparkIntegration +from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration + +__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py new file mode 100644 index 0000000000..a83532b6a6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Optional + + from pyspark import SparkContext + + from sentry_sdk._types import Event, Hint + + +class SparkIntegration(Integration): + identifier = "spark" + + @staticmethod + def setup_once() -> None: + _setup_sentry_tracing() + + +def _set_app_properties() -> None: + """ + Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. + This allows worker integration to have access to app_name and application_id. + """ + from pyspark import SparkContext + + spark_context = SparkContext._active_spark_context + if spark_context: + spark_context.setLocalProperty( + "sentry_app_name", + spark_context.appName, + ) + spark_context.setLocalProperty( + "sentry_application_id", + spark_context.applicationId, + ) + + +def _start_sentry_listener(sc: "SparkContext") -> None: + """ + Start java gateway server to add custom `SparkListener` + """ + from pyspark.java_gateway import ensure_callback_server_started + + gw = sc._gateway + ensure_callback_server_started(gw) + listener = SentryListener() + sc._jsc.sc().addSparkListener(listener) + + +def _add_event_processor(sc: "SparkContext") -> None: + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + if sentry_sdk.get_client().get_integration(SparkIntegration) is None: + return event + + if sc._active_spark_context is None: + return event + + event.setdefault("user", {}).setdefault("id", sc.sparkUser()) + + event.setdefault("tags", {}).setdefault( + "executor.id", sc._conf.get("spark.executor.id") + ) + event["tags"].setdefault( + "spark-submit.deployMode", + sc._conf.get("spark.submit.deployMode"), + ) + event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) + event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) + event["tags"].setdefault("spark_version", sc.version) + event["tags"].setdefault("app_name", sc.appName) + event["tags"].setdefault("application_id", sc.applicationId) + event["tags"].setdefault("master", sc.master) + event["tags"].setdefault("spark_home", sc.sparkHome) + + event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) + + return event + + +def _activate_integration(sc: "SparkContext") -> None: + _start_sentry_listener(sc) + _set_app_properties() + _add_event_processor(sc) + + +def _patch_spark_context_init() -> None: + from pyspark import SparkContext + + spark_context_init = SparkContext._do_init + + @ensure_integration_enabled(SparkIntegration, spark_context_init) + def _sentry_patched_spark_context_init( + self: "SparkContext", *args: "Any", **kwargs: "Any" + ) -> "Optional[Any]": + rv = spark_context_init(self, *args, **kwargs) + _activate_integration(self) + return rv + + SparkContext._do_init = _sentry_patched_spark_context_init + + +def _setup_sentry_tracing() -> None: + from pyspark import SparkContext + + if SparkContext._active_spark_context is not None: + _activate_integration(SparkContext._active_spark_context) + return + _patch_spark_context_init() + + +class SparkListener: + def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 + pass + + def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklistedForStage( # noqa: N802 + self, + executorBlacklistedForStage: "Any", # noqa: N803 + ) -> None: + pass + + def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 + pass + + def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 + pass + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + pass + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 + pass + + def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 + pass + + class Java: + implements = ["org.apache.spark.scheduler.SparkListenerInterface"] + + +class SentryListener(SparkListener): + def _add_breadcrumb( + self, + level: str, + message: str, + data: "Optional[dict[str, Any]]" = None, + ) -> None: + sentry_sdk.get_isolation_scope().add_breadcrumb( + level=level, message=message, data=data + ) + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + message = "Job {} Started".format(jobStart.jobId()) + self._add_breadcrumb(level="info", message=message) + _set_app_properties() + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + level = "" + message = "" + data = {"result": jobEnd.jobResult().toString()} + + if jobEnd.jobResult().toString() == "JobSucceeded": + level = "info" + message = "Job {} Ended".format(jobEnd.jobId()) + else: + level = "warning" + message = "Job {} Failed".format(jobEnd.jobId()) + + self._add_breadcrumb(level=level, message=message, data=data) + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + stage_info = stageSubmitted.stageInfo() + message = "Stage {} Submitted".format(stage_info.stageId()) + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + self._add_breadcrumb(level="info", message=message, data=data) + _set_app_properties() + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + from py4j.protocol import Py4JJavaError # type: ignore + + stage_info = stageCompleted.stageInfo() + message = "" + level = "" + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + # Have to Try Except because stageInfo.failureReason() is typed with Scala Option + try: + data["reason"] = stage_info.failureReason().get() + message = "Stage {} Failed".format(stage_info.stageId()) + level = "warning" + except Py4JJavaError: + message = "Stage {} Completed".format(stage_info.stageId()) + level = "info" + + self._add_breadcrumb(level=level, message=message, data=data) + + +def _get_attempt_id(stage_info: "Any") -> "Optional[int]": + try: + return stage_info.attemptId() + except Exception: + pass + + try: + return stage_info.attemptNumber() + except Exception: + pass + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py new file mode 100644 index 0000000000..5906472748 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py @@ -0,0 +1,109 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_hint_with_exc_info, + exc_info_from_error, + single_exception_from_error_tuple, + walk_exception_chain, +) + +if TYPE_CHECKING: + from typing import Any, Optional + + from sentry_sdk._types import Event, ExcInfo, Hint + + +class SparkWorkerIntegration(Integration): + identifier = "spark_worker" + + @staticmethod + def setup_once() -> None: + import pyspark.daemon as original_daemon + + original_daemon.worker_main = _sentry_worker_main + + +def _capture_exception(exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + + mechanism = {"type": "spark", "handled": False} + + exc_info = exc_info_from_error(exc_info) + + exc_type, exc_value, tb = exc_info + rv = [] + + # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + if exc_type not in (SystemExit, EOFError, ConnectionResetError): + rv.append( + single_exception_from_error_tuple( + exc_type, exc_value, tb, client.options, mechanism + ) + ) + + if rv: + rv.reverse() + hint = event_hint_with_exc_info(exc_info) + event: "Event" = {"level": "error", "exception": {"values": rv}} + + _tag_task_context() + + sentry_sdk.capture_event(event, hint=hint) + + +def _tag_task_context() -> None: + from pyspark.taskcontext import TaskContext + + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + integration = sentry_sdk.get_client().get_integration( + SparkWorkerIntegration + ) + task_context = TaskContext.get() + + if integration is None or task_context is None: + return event + + event.setdefault("tags", {}).setdefault( + "stageId", str(task_context.stageId()) + ) + event["tags"].setdefault("partitionId", str(task_context.partitionId())) + event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) + event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) + + if task_context._localProperties: + if "sentry_app_name" in task_context._localProperties: + event["tags"].setdefault( + "app_name", task_context._localProperties["sentry_app_name"] + ) + event["tags"].setdefault( + "application_id", + task_context._localProperties["sentry_application_id"], + ) + + if "callSite.short" in task_context._localProperties: + event.setdefault("extra", {}).setdefault( + "callSite", task_context._localProperties["callSite.short"] + ) + + return event + + +def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: + import pyspark.worker as original_worker + + try: + original_worker.main(*args, **kwargs) + except SystemExit: + if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py new file mode 100644 index 0000000000..962fe4ee53 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py @@ -0,0 +1,185 @@ +from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + parse_version, +) + +try: + from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore + from sqlalchemy.engine import Engine # type: ignore + from sqlalchemy.event import listen # type: ignore +except ImportError: + raise DidNotEnable("SQLAlchemy not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional, Union + + +class SqlalchemyIntegration(Integration): + identifier = "sqlalchemy" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(SQLALCHEMY_VERSION) + _check_minimum_version(SqlalchemyIntegration, version) + + listen(Engine, "before_cursor_execute", _before_cursor_execute) + listen(Engine, "after_cursor_execute", _after_cursor_execute) + listen(Engine, "handle_error", _handle_error) + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _before_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + executemany: bool, + *args: "Any", +) -> None: + ctx_mgr = record_sql_queries( + cursor, + statement, + parameters, + paramstyle=context and context.dialect and context.dialect.paramstyle or None, + executemany=executemany, + span_origin=SqlalchemyIntegration.origin, + ) + context._sentry_sql_span_manager = ctx_mgr + + span = ctx_mgr.__enter__() + + if span is not None: + _set_db_data(span, conn) + context._sentry_sql_span = span + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _after_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + *args: "Any", +) -> None: + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + context, "_sentry_sql_span_manager", None + ) + + # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. + span: "Optional[Union[Span, StreamedSpan]]" = getattr( + context, "_sentry_sql_span", None + ) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if ctx_mgr is not None: + context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + if isinstance(span, Span): + with capture_internal_exceptions(): + add_query_source(span) + + +def _handle_error(context: "Any", *args: "Any") -> None: + execution_context = context.execution_context + if execution_context is None: + return + + span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) + + if span is not None: + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # _after_cursor_execute does not get called for crashing SQL stmts. Judging + # from SQLAlchemy codebase it does seem like any error coming into this + # handler is going to be fatal. + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + execution_context, "_sentry_sql_span_manager", None + ) + + if ctx_mgr is not None: + execution_context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + +# See: https://docs.sqlalchemy.org/en/20/dialects/index.html +def _get_db_system(name: str) -> "Optional[str]": + name = str(name) + + if "sqlite" in name: + return "sqlite" + + if "postgres" in name: + return "postgresql" + + if "mariadb" in name: + return "mariadb" + + if "mysql" in name: + return "mysql" + + if "oracle" in name: + return "oracle" + + return None + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + db_system = _get_db_system(conn.engine.name) + + if isinstance(span, StreamedSpan): + if db_system is not None: + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) + else: + if db_system is not None: + span.set_data(SPANDATA.DB_SYSTEM, db_system) + + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + try: + driver = conn.dialect.driver + if driver: + set_on_span(SPANDATA.DB_DRIVER_NAME, driver) + except Exception: + pass + + if conn.engine.url is None: + return + + db_name = conn.engine.url.database + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + server_address = conn.engine.url.host + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = conn.engine.url.port + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py new file mode 100644 index 0000000000..3f9fbf4d8e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py @@ -0,0 +1,858 @@ +import functools +import json +import sys +import warnings +from collections.abc import Set +from copy import deepcopy +from json import JSONDecodeError +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations._asgi_common import _RootPathInPath +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + HttpCodeRangeContainer, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Awaitable, + Callable, + Container, + Dict, + Optional, + Tuple, + Union, + ) + + from sentry_sdk._types import Event, HttpStatusCodeRange +try: + import starlette + from starlette import __version__ as STARLETTE_VERSION + from starlette.applications import Starlette + from starlette.datastructures import ( + UploadFile, + ) + from starlette.middleware import Middleware + from starlette.middleware.authentication import ( + AuthenticationMiddleware, + ) + from starlette.requests import Request + from starlette.routing import Match + from starlette.types import ASGIApp, Receive, Send + from starlette.types import Scope as StarletteScope +except ImportError: + raise DidNotEnable("Starlette is not installed") + +try: + # Starlette 0.20 + from starlette.middleware.exceptions import ExceptionMiddleware +except ImportError: + # Startlette 0.19.1 + from starlette.exceptions import ExceptionMiddleware # type: ignore + +try: + # Optional dependency of Starlette to parse form data. + try: + # python-multipart 0.0.13 and later + import python_multipart as multipart + except ImportError: + # python-multipart 0.0.12 and earlier + import multipart # type: ignore +except ImportError: + multipart = None # type: ignore[assignment] + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +_DEFAULT_TRANSACTION_NAME = "generic Starlette request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class StarletteIntegration(Integration): + identifier = "starlette" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "url", + failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + middleware_spans: bool = False, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ): + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + if isinstance(failed_request_status_codes, Set): + self.failed_request_status_codes: "Container[int]" = ( + failed_request_status_codes + ) + else: + warnings.warn( + "Passing a list or None for failed_request_status_codes is deprecated. " + "Please pass a set of int instead.", + DeprecationWarning, + stacklevel=2, + ) + + if failed_request_status_codes is None: + self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES + else: + self.failed_request_status_codes = HttpCodeRangeContainer( + failed_request_status_codes + ) + + @staticmethod + def setup_once() -> None: + version = parse_version(STARLETTE_VERSION) + + if version is None: + raise DidNotEnable( + "Unparsable Starlette version: {}".format(STARLETTE_VERSION) + ) + + patch_middlewares() + # Starlette tolerates both starting with: + # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. + root_path_in_path = ( + _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED + ) + patch_asgi_app(root_path_in_path=root_path_in_path) + patch_request_response() + + if version >= (0, 24): + patch_templates() + + +def _enable_span_for_middleware( + middleware_class: "Any", +) -> "Any": + old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ + + async def _create_span_call( + app: "Any", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + **kwargs: "Any", + ) -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await old_call(app, scope, receive, send, **kwargs) + + # Update transaction name with middleware name + name, source = _get_transaction_from_middleware(app, scope, integration) + + if name is not None: + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=source, + ) + + if not integration.middleware_spans: + return await old_call(app, scope, receive, send, **kwargs) + + middleware_name = app.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarletteIntegration.origin, + "middleware.name": middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarletteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlette.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await send(*args, **kwargs) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(app, scope, new_receive, new_send, **kwargs) + + not_yet_patched = old_call.__name__ not in [ + "_create_span_call", + "_sentry_authenticationmiddleware_call", + "_sentry_exceptionmiddleware_call", + ] + + if not_yet_patched: + middleware_class.__call__ = _create_span_call + + return middleware_class + + +def _serialize_request_body_data(data: "Any") -> str: + # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values + def _default(value: "Any") -> "Any": + if isinstance(value, AnnotatedValue): + return value.value + return str(value) + + return json.dumps(data, default=_default) + + +@ensure_integration_enabled(StarletteIntegration) +def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarletteIntegration.identifier, "handled": handled}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def patch_exception_middleware(middleware_class: "Any") -> None: + """ + Capture all exceptions in Starlette app and + also extract user information. + """ + old_middleware_init = middleware_class.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + + if not_yet_patched: + + def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: + old_middleware_init(self, *args, **kwargs) + + # Patch existing exception handlers + old_handlers = self._exception_handlers.copy() + + async def _sentry_patched_exception_handler( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration( + StarletteIntegration + ) + + exp = args[0] + + if integration is not None: + is_http_server_error = ( + hasattr(exp, "status_code") + and isinstance(exp.status_code, int) + and exp.status_code in integration.failed_request_status_codes + ) + if is_http_server_error: + _capture_exception(exp, handled=True) + + # Find a matching handler + old_handler = None + for cls in type(exp).__mro__: + if cls in old_handlers: + old_handler = old_handlers[cls] + break + + if old_handler is None: + return + + if _is_async_callable(old_handler): + return await old_handler(self, *args, **kwargs) + else: + return old_handler(self, *args, **kwargs) + + for key in self._exception_handlers.keys(): + self._exception_handlers[key] = _sentry_patched_exception_handler + + middleware_class.__init__ = _sentry_middleware_init + + old_call = middleware_class.__call__ + + async def _sentry_exceptionmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + # Also add the user (that was eventually set by be Authentication middle + # that was called before this middleware). This is done because the authentication + # middleware sets the user in the scope and then (in the same function) + # calls this exception middelware. In case there is no exception (or no handler + # for the type of exception occuring) then the exception bubbles up and setting the + # user information into the sentry scope is done in auth middleware and the + # ASGI middleware will then send everything to Sentry and this is fine. + # But if there is an exception happening that the exception middleware here + # has a handler for, it will send the exception directly to Sentry, so we need + # the user information right now. + # This is why we do it here. + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_exceptionmiddleware_call + + +@ensure_integration_enabled(StarletteIntegration) +def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: + """ + Extracts user information from the ASGI scope and + adds it to Sentry's scope. + """ + if "user" not in scope: + return + + if not should_send_default_pii(): + return + + user_info: "Dict[str, Any]" = {} + starlette_user = scope["user"] + + username = getattr(starlette_user, "username", None) + if username: + user_info.setdefault("username", starlette_user.username) + + user_id = getattr(starlette_user, "id", None) + if user_id: + user_info.setdefault("id", starlette_user.id) + + email = getattr(starlette_user, "email", None) + if email: + user_info.setdefault("email", starlette_user.email) + + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + +def patch_authentication_middleware(middleware_class: "Any") -> None: + """ + Add user information to Sentry scope. + """ + old_call = middleware_class.__call__ + + not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) + + if not_yet_patched: + + async def _sentry_authenticationmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_authenticationmiddleware_call + + +def patch_middlewares() -> None: + """ + Patches Starlettes `Middleware` class to record + spans for every middleware invoked. + """ + old_middleware_init = Middleware.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + if not_yet_patched: + + def _sentry_middleware_init( + self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + if cls == SentryAsgiMiddleware: + return old_middleware_init(self, cls, *args, **kwargs) + + span_enabled_cls = _enable_span_for_middleware(cls) + old_middleware_init(self, span_enabled_cls, *args, **kwargs) + + if cls == AuthenticationMiddleware: + patch_authentication_middleware(cls) + + if cls == ExceptionMiddleware: + patch_exception_middleware(cls) + + Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] + + +def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: + """ + Instrument Starlette ASGI app using the SentryAsgiMiddleware. + """ + old_app = Starlette.__call__ + + async def _sentry_patched_asgi_app( + self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StarletteIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + mechanism_type=StarletteIntegration.identifier, + transaction_style=integration.transaction_style, + span_origin=StarletteIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + asgi_version=3, + root_path_in_path=root_path_in_path, + ) + + return await middleware(scope, receive, send) + + Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] + + +# This was vendored in from Starlette to support Starlette 0.19.1 because +# this function was only introduced in 0.20.x +def _is_async_callable(obj: "Any") -> bool: + while isinstance(obj, functools.partial): + obj = obj.func + + return iscoroutinefunction(obj) or ( + callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] + ) + + +def _get_cached_request_body_attribute( + client: "sentry_sdk.client.BaseClient", request: "Request" +) -> "Optional[str]": + """ + Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. + """ + if "content-length" not in request.headers: + return None + + try: + content_length = int(request.headers["content-length"]) + except ValueError: + return None + + if content_length and not request_body_within_bounds(client, content_length): + return OVER_SIZE_LIMIT_SUBSTITUTE + + if hasattr(request, "_json"): + return json.dumps(request._json) + + formdata_body = getattr(request, "_form", None) + if formdata_body is None: + return None + + form_data = {} + for key, val in formdata_body.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = val if not is_file else "[Unparsable]" + + return json.dumps(form_data) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), + integration.transaction_style, + request, + ) + + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Add info from request to event + request_info = event.get("request", {}) + if info: + if "cookies" in info: + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_request_response() -> None: + old_request_response = starlette.routing.request_response + + def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": + old_func = func + + is_coroutine = _is_async_callable(old_func) + if is_coroutine: + + async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_func, *args, **kwargs) + + func = _sentry_async_func + + else: + + @functools.wraps(old_func) + def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(StarletteIntegration) + if integration is None: + return old_func(*args, **kwargs) + + current_scope = sentry_sdk.get_current_scope() + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming: + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + current_span._segment._update_active_thread() + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + request = args[0] + + _set_transaction_name_and_source( + sentry_scope, integration.transaction_style, request + ) + + extractor = StarletteRequestExtractor(request) + cookies = extractor.extract_cookies_from_request() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor( + event: "Event", hint: "dict[str, Any]" + ) -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if cookies: + request_info["cookies"] = cookies + + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + return old_func(*args, **kwargs) + + func = _sentry_sync_func + + return old_request_response(func) + + starlette.routing.request_response = _sentry_request_response + + +def patch_templates() -> None: + # If markupsafe is not installed, then Jinja2 is not installed + # (markupsafe is a dependency of Jinja2) + # In this case we do not need to patch the Jinja2Templates class + try: + from markupsafe import Markup + except ImportError: + return # Nothing to do + + # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a + try: + from starlette.templating import Jinja2Templates + except ImportError: + return + + old_jinja2templates_init = Jinja2Templates.__init__ + + not_yet_patched = "_sentry_jinja2templates_init" not in str( + old_jinja2templates_init + ) + + if not_yet_patched: + + def _sentry_jinja2templates_init( + self: "Jinja2Templates", *args: "Any", **kwargs: "Any" + ) -> None: + def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": + trace_meta = Markup( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + return { + "sentry_trace_meta": trace_meta, + } + + kwargs.setdefault("context_processors", []) + + if add_sentry_trace_meta not in kwargs["context_processors"]: + kwargs["context_processors"].append(add_sentry_trace_meta) + + return old_jinja2templates_init(self, *args, **kwargs) + + Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] + + +class StarletteRequestExtractor: + """ + Extracts useful information from the Starlette request + (like form data or cookies) and adds it to the Sentry event. + """ + + def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: + self.request = request + + def extract_cookies_from_request( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + cookies: "Optional[Dict[str, Any]]" = None + if should_send_default_pii(): + cookies = self.cookies() + + return cookies + + async def extract_request_info( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + client = sentry_sdk.get_client() + + request_info: "Dict[str, Any]" = {} + + with capture_internal_exceptions(): + # Add cookies + if should_send_default_pii(): + request_info["cookies"] = self.cookies() + + # If there is no body, just return the cookies + content_length = await self.content_length() + if not content_length: + return request_info + + # Add annotation if body is too big + if content_length and not request_body_within_bounds( + client, content_length + ): + request_info["data"] = AnnotatedValue.removed_because_over_size_limit() + return request_info + + # Add JSON body, if it is a JSON request + json = await self.json() + if json: + request_info["data"] = json + return request_info + + # Add form as key/value pairs, if request has form data + form = await self.form() + if form: + form_data = {} + for key, val in form.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = ( + val + if not is_file + else AnnotatedValue.removed_because_raw_data() + ) + + request_info["data"] = form_data + return request_info + + # Raw data, do not add body just an annotation + request_info["data"] = AnnotatedValue.removed_because_raw_data() + return request_info + + async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": + if "content-length" in self.request.headers: + return int(self.request.headers["content-length"]) + + return None + + def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": + return self.request.cookies + + async def form(self: "StarletteRequestExtractor") -> "Any": + if multipart is None: + return None + + # Parse the body first to get it cached, as Starlette does not cache form() as it + # does with body() and json() https://github.com/encode/starlette/discussions/1933 + # Calling `.form()` without calling `.body()` first will + # potentially break the users project. + await self.request.body() + + return await self.request.form() + + def is_json(self: "StarletteRequestExtractor") -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": + if not self.is_json(): + return None + try: + return await self.request.json() + except JSONDecodeError: + return None + + +def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": + router = scope.get("router") + if not router: + return None + + for route in router.routes: + match = route.matches(scope) + if match[0] == Match.FULL: + try: + return route.path + except AttributeError: + # routes added via app.host() won't have a path attribute + return scope.get("path") + + return None + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = None + source = SOURCE_FOR_STYLE[transaction_style] + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or None + + elif transaction_style == "url": + name = _transaction_name_from_router(request.scope) + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + scope.set_transaction_name(name, source=source) + + +def _get_transaction_from_middleware( + app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" +) -> "Tuple[Optional[str], Optional[str]]": + name = None + source = None + + if integration.transaction_style == "endpoint": + name = transaction_from_function(app.__class__) + source = TransactionSource.COMPONENT + elif integration.transaction_style == "url": + name = _transaction_name_from_router(asgi_scope) + source = TransactionSource.ROUTE + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py new file mode 100644 index 0000000000..1eebd37e84 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py @@ -0,0 +1,314 @@ +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from pydantic import BaseModel + from starlite import Request, Starlite, State # type: ignore + from starlite.handlers.base import BaseRouteHandler # type: ignore + from starlite.middleware import DefineMiddleware # type: ignore + from starlite.plugins.base import get_plugin_for_value # type: ignore + from starlite.routes.http import HTTPRoute # type: ignore + from starlite.utils import ( # type: ignore + ConnectionDataExtractor, + Ref, + is_async_callable, + ) +except ImportError: + raise DidNotEnable("Starlite is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from starlite import MiddlewareProtocol + from starlite.types import ( # type: ignore + ASGIApp, + Hint, + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from starlite.types import ( + Scope as StarliteScope, + ) + + from sentry_sdk._types import Event + + +_DEFAULT_TRANSACTION_NAME = "generic Starlite request" + + +class StarliteIntegration(Integration): + identifier = "starlite" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + +class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + +def patch_app_init() -> None: + """ + Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. + See: + - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception + - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ + """ + old__init__ = Starlite.__init__ + + @ensure_integration_enabled(StarliteIntegration, old__init__) + def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: + after_exception = kwargs.pop("after_exception", []) + kwargs.update( + after_exception=[ + exception_handler, + *( + after_exception + if isinstance(after_exception, list) + else [after_exception] + ), + ] + ) + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Starlite.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryStarliteASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "StarliteScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(StarliteIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarliteIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarliteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlite.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await receive(*args, **kwargs) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await send(message) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + elif isinstance(route_handler.fn, Ref): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = StarliteIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if not scope_user: + return None + if isinstance(scope_user, dict): + return scope_user + if isinstance(scope_user, BaseModel): + return scope_user.dict() + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + plugin = get_plugin_for_value(scope_user) + if plugin and not is_async_callable(plugin.to_dict): + return plugin.to_dict(scope_user) + + return None + + +@ensure_integration_enabled(StarliteIntegration) +def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarliteIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py new file mode 100644 index 0000000000..746e09b36f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py @@ -0,0 +1,37 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.utils import parse_version + +try: + from statsig import statsig as statsig_module + from statsig.version import __version__ as STATSIG_VERSION +except ImportError: + raise DidNotEnable("statsig is not installed") + +if TYPE_CHECKING: + from statsig.statsig_user import StatsigUser + + +class StatsigIntegration(Integration): + identifier = "statsig" + + @staticmethod + def setup_once() -> None: + version = parse_version(STATSIG_VERSION) + _check_minimum_version(StatsigIntegration, version, "statsig") + + # Wrap and patch evaluation method(s) in the statsig module + old_check_gate = statsig_module.check_gate + + @wraps(old_check_gate) + def sentry_check_gate( + user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_check_gate(user, gate, *args, **kwargs) + add_feature_flag(gate, enabled) + return enabled + + statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py new file mode 100644 index 0000000000..82f30f2dda --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py @@ -0,0 +1,404 @@ +import os +import platform +import subprocess +import sys +from http.client import HTTPConnection, HTTPResponse +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + EnvironHeaders, + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + is_sentry_url, + logger, + parse_url, + safe_repr, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from sentry_sdk._types import Event, Hint + + +_RUNTIME_CONTEXT: "dict[str, object]" = { + "name": platform.python_implementation(), + "version": "%s.%s.%s" % (sys.version_info[:3]), + "build": sys.version, +} + + +class StdlibIntegration(Integration): + identifier = "stdlib" + + @staticmethod + def setup_once() -> None: + _install_httplib() + _install_subprocess() + + @add_global_event_processor + def add_python_runtime_context( + event: "Event", hint: "Hint" + ) -> "Optional[Event]": + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is not None: + contexts = event.setdefault("contexts", {}) + if isinstance(contexts, dict) and "runtime" not in contexts: + contexts["runtime"] = _RUNTIME_CONTEXT + + return event + + +def _complete_span(span: "Union[Span, StreamedSpan]") -> None: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + +def _install_httplib() -> None: + real_putrequest = HTTPConnection.putrequest + real_getresponse = HTTPConnection.getresponse + real_read = HTTPResponse.read + real_close = HTTPResponse.close + + def putrequest( + self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + default_port = self.default_port + + # proxies go through set_tunnel + tunnel_host = getattr(self, "_tunnel_host", None) + if tunnel_host: + host = tunnel_host + port = getattr(self, "_tunnel_port", default_port) + else: + host = self.host + port = self.port + + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is None or is_sentry_url( + client, host + ): + return real_putrequest(self, method, url, *args, **kwargs) + + real_url = url + if real_url is None or not real_url.startswith(("http://", "https://")): + real_url = "%s://%s%s%s" % ( + default_port == 443 and "https" or "http", + host, + port != default_port and ":%s" % port or "", + url, + ) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(real_url, sanitize=False) + + span_streaming = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + attributes={ + "sentry.origin": "auto.http.stdlib.httplib", + "sentry.op": OP.HTTP_CLIENT, + SPANDATA.HTTP_REQUEST_METHOD: method, + }, + ) + + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + + set_on_span = span.set_attribute + + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + origin="auto.http.stdlib.httplib", + ) + + span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + + set_on_span = span.set_data + + # for proxies, these point to the proxy host/port + if tunnel_host: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) + set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) + + rv = real_putrequest(self, method, url, *args, **kwargs) + + if should_propagate_trace(client, real_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=key, value=value, real_url=real_url + ) + ) + self.putheader(key, value) + + self._sentrysdk_span = span # type: ignore[attr-defined] + + return rv + + def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": + span = getattr(self, "_sentrysdk_span", None) + + if span is None: + return real_getresponse(self, *args, **kwargs) + + try: + rv = real_getresponse(self, *args, **kwargs) + except BaseException: + _complete_span(span) + raise + + if isinstance(span, StreamedSpan): + status_code = int(rv.status) + span.status = "error" if status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", status_code) + else: + span.set_http_status(int(rv.status)) + span.set_data("reason", rv.reason) + + # getresponse doesn't include actually reading the response body. This + # is done in read(). So if the metadata/headers suggest there's a body to + # read, don't finish the span just yet, but save it for ending it later. + has_body = rv.chunked or (rv.length is not None and rv.length > 0) + if has_body: + rv._sentrysdk_span = span # type: ignore[attr-defined] + else: + _complete_span(span) + + return rv + + def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": + try: + return real_read(self, *args, **kwargs) + finally: + span = getattr(self, "_sentrysdk_span", None) + # read() might be called multiple times to consume a single body, + # so we can't just end the span when read() is done. Instead, + # try to figure out whether the response body has been fully read. + if span and (self.fp is None or self.closed): + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + def close(self: "HTTPResponse") -> None: + # We patch close() as a best effort fallback in case the span is not + # ended yet in getresponse() or read(). + + try: + real_close(self) + finally: + span = getattr(self, "_sentrysdk_span", None) + if span is not None: + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + HTTPConnection.putrequest = putrequest # type: ignore[method-assign] + HTTPConnection.getresponse = getresponse # type: ignore[method-assign] + HTTPResponse.read = read # type: ignore[method-assign] + HTTPResponse.close = close # type: ignore[assignment,method-assign] + + +def _init_argument( + args: "List[Any]", + kwargs: "Dict[Any, Any]", + name: str, + position: int, + setdefault_callback: "Optional[Callable[[Any], Any]]" = None, +) -> "Any": + """ + given (*args, **kwargs) of a function call, retrieve (and optionally set a + default for) an argument by either name or position. + + This is useful for wrapping functions with complex type signatures and + extracting a few arguments without needing to redefine that function's + entire type signature. + """ + + if name in kwargs: + rv = kwargs[name] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + kwargs[name] = rv + elif position < len(args): + rv = args[position] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + args[position] = rv + else: + rv = setdefault_callback and setdefault_callback(None) + if rv is not None: + kwargs[name] = rv + + return rv + + +def _install_subprocess() -> None: + old_popen_init = subprocess.Popen.__init__ + + @ensure_integration_enabled(StdlibIntegration, old_popen_init) + def sentry_patched_popen_init( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> None: + # Convert from tuple to list to be able to set values. + a = list(a) + + args = _init_argument(a, kw, "args", 0) or [] + cwd = _init_argument(a, kw, "cwd", 9) + + # if args is not a list or tuple (and e.g. some iterator instead), + # let's not use it at all. There are too many things that can go wrong + # when trying to collect an iterator into a list and setting that list + # into `a` again. + # + # Also invocations where `args` is not a sequence are not actually + # legal. They just happen to work under CPython. + description = None + + if isinstance(args, (list, tuple)) and len(args) < 100: + with capture_internal_exceptions(): + description = " ".join(map(str, args)) + + if description is None: + description = safe_repr(args) + + env = None + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.SUBPROCESS, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.SUBPROCESS, + name=description, + origin="auto.subprocess.stdlib.subprocess", + ) + + with span: + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + if env is None: + env = _init_argument( + a, + kw, + "env", + 10, + lambda x: dict(x if x is not None else os.environ), + ) + env["SUBPROCESS_" + k.upper().replace("-", "_")] = v + + if cwd and isinstance(span, Span): + span.set_data("subprocess.cwd", cwd) + + rv = old_popen_init(self, *a, **kw) + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + else: + span.set_tag("subprocess.pid", self.pid) + + return rv + + subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore + + old_popen_wait = subprocess.Popen.wait + + @ensure_integration_enabled(StdlibIntegration, old_popen_wait) + def sentry_patched_popen_wait( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_WAIT, + attributes={ + "sentry.op": OP.SUBPROCESS_WAIT, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_wait(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_WAIT, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_wait(self, *a, **kw) + + subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore + + old_popen_communicate = subprocess.Popen.communicate + + @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) + def sentry_patched_popen_communicate( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_COMMUNICATE, + attributes={ + "sentry.op": OP.SUBPROCESS_COMMUNICATE, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_communicate(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_COMMUNICATE, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_communicate(self, *a, **kw) + + subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore + + +def get_subprocess_traceparent_headers() -> "EnvironHeaders": + return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py new file mode 100644 index 0000000000..5f00e8bf6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py @@ -0,0 +1,493 @@ +import functools +import hashlib +import warnings +from inspect import isawaitable + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from functools import cached_property +except ImportError: + # The strawberry integration requires Python 3.8+. functools.cached_property + # was added in 3.8, so this check is technically not needed, but since this + # is an auto-enabling integration, we might get to executing this import in + # lower Python versions, so we need to deal with it. + raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") + +try: + from strawberry import Schema + from strawberry.extensions import SchemaExtension + from strawberry.extensions.tracing.utils import ( + should_skip_tracing as strawberry_should_skip_tracing, + ) + from strawberry.http import async_base_view, sync_base_view +except ImportError: + raise DidNotEnable("strawberry-graphql is not installed") + +try: + from strawberry.extensions.tracing import ( + SentryTracingExtension as StrawberrySentryAsyncExtension, + ) + from strawberry.extensions.tracing import ( + SentryTracingExtensionSync as StrawberrySentrySyncExtension, + ) +except ImportError: + StrawberrySentryAsyncExtension = None + StrawberrySentrySyncExtension = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Generator, List, Optional + + from graphql import GraphQLError, GraphQLResolveInfo + from strawberry.http import GraphQLHTTPResponse + from strawberry.types import ExecutionContext + + from sentry_sdk._types import Event, EventProcessor + + +ignore_logger("strawberry.execution") + + +class StrawberryIntegration(Integration): + identifier = "strawberry" + origin = f"auto.graphql.{identifier}" + + def __init__(self, async_execution: "Optional[bool]" = None) -> None: + if async_execution not in (None, False, True): + raise ValueError( + 'Invalid value for async_execution: "{}" (must be bool)'.format( + async_execution + ) + ) + self.async_execution = async_execution + + @staticmethod + def setup_once() -> None: + version = package_version("strawberry-graphql") + _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") + + _patch_schema_init() + _patch_views() + + +def _patch_schema_init() -> None: + old_schema_init = Schema.__init__ + + @functools.wraps(old_schema_init) + def _sentry_patched_schema_init( + self: "Schema", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) + if integration is None: + return old_schema_init(self, *args, **kwargs) + + extensions = kwargs.get("extensions") or [] + + should_use_async_extension: "Optional[bool]" = None + if integration.async_execution is not None: + should_use_async_extension = integration.async_execution + else: + # try to figure it out ourselves + should_use_async_extension = _guess_if_using_async(extensions) + + if should_use_async_extension is None: + warnings.warn( + "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", + stacklevel=2, + ) + should_use_async_extension = False + + # remove the built in strawberry sentry extension, if present + extensions = [ + extension + for extension in extensions + if extension + not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) + ] + + # add our extension + extensions = [ + SentryAsyncExtension if should_use_async_extension else SentrySyncExtension + ] + extensions + + kwargs["extensions"] = extensions + + return old_schema_init(self, *args, **kwargs) + + Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] + + +class SentryAsyncExtension(SchemaExtension): + def __init__( + self: "Any", + *, + execution_context: "Optional[ExecutionContext]" = None, + ) -> None: + if execution_context: + self.execution_context = execution_context + + @cached_property + def _resource_name(self) -> str: + query_hash = self.hash_query(self.execution_context.query) # type: ignore + + if self.execution_context.operation_name: + return "{}:{}".format(self.execution_context.operation_name, query_hash) + + return query_hash + + def hash_query(self, query: str) -> str: + return hashlib.md5(query.encode("utf-8")).hexdigest() + + def on_operation(self) -> "Generator[None, None, None]": + operation_name = self.execution_context.operation_name + + operation_type = "query" + op = OP.GRAPHQL_QUERY + + if self.execution_context.query is None: + self.execution_context.query = "" + + if self.execution_context.query.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif self.execution_context.query.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + description = operation_type + if operation_name: + description += " {}".format(operation_name) + + sentry_sdk.add_breadcrumb( + category="graphql.operation", + data={ + "operation_name": operation_name, + "operation_type": operation_type, + }, + ) + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_request_event_processor(self.execution_context) + scope.add_event_processor(event_processor) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + additional_attributes: "dict[str, Any]" = {} + + if should_send_default_pii(): + additional_attributes["graphql.document"] = self.execution_context.query + + if operation_name: + additional_attributes["graphql.operation.name"] = operation_name + + graphql_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": op, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + graphql_span = sentry_sdk.start_span( + op=op, + name=description, + origin=StrawberryIntegration.origin, + ) + graphql_span.__enter__() + + if type(graphql_span) is Span: + if should_send_default_pii(): + graphql_span.set_data("graphql.document", self.execution_context.query) + + graphql_span.set_data("graphql.operation.type", operation_type) + graphql_span.set_data("graphql.operation.name", operation_name) + # This attribute is being removed in streamed spans + graphql_span.set_data("graphql.resource_name", self._resource_name) + + yield + + if type(graphql_span) is StreamedSpan: + if self.execution_context.operation_name: + segment = graphql_span._segment + segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) + segment.set_attribute("sentry.op", op) + segment.name = self.execution_context.operation_name + elif isinstance(graphql_span, Span): + transaction = graphql_span.containing_transaction + if transaction and self.execution_context.operation_name: + transaction.name = self.execution_context.operation_name + transaction.source = TransactionSource.COMPONENT + transaction.op = op + + graphql_span.__exit__(None, None, None) + + def on_validate(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + validation_span = sentry_sdk.traces.start_span( + name="validation", + attributes={ + "sentry.op": OP.GRAPHQL_VALIDATE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + validation_span = sentry_sdk.start_span( + op=OP.GRAPHQL_VALIDATE, + name="validation", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during validation, we still need to close the span + try: + yield + finally: + if isinstance(validation_span, StreamedSpan): + validation_span.end() + else: + validation_span.finish() + + def on_parse(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + parsing_span = sentry_sdk.traces.start_span( + name="parsing", + attributes={ + "sentry.op": OP.GRAPHQL_PARSE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + parsing_span = sentry_sdk.start_span( + op=OP.GRAPHQL_PARSE, + name="parsing", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during parsing, we still need to close the span + try: + yield + finally: + if isinstance(parsing_span, StreamedSpan): + parsing_span.end() + else: + parsing_span.finish() + + def should_skip_tracing( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + info: "GraphQLResolveInfo", + ) -> bool: + return strawberry_should_skip_tracing(_next, info) + + async def _resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + result = _next(root, info, *args, **kwargs) + + if isawaitable(result): + result = await result + + return result + + async def resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return await self._resolve(_next, root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return await self._resolve(_next, root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return await self._resolve(_next, root, info, *args, **kwargs) + + +class SentrySyncExtension(SentryAsyncExtension): + def resolve( + self, + _next: "Callable[[Any, Any, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return _next(root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return _next(root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return _next(root, info, *args, **kwargs) + + +def _patch_views() -> None: + old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors + old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors + + def _sentry_patched_async_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_async_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + def _sentry_patched_sync_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_sync_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + @ensure_integration_enabled(StrawberryIntegration) + def _sentry_patched_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + if not errors: + return + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_response_event_processor(response_data) + scope.add_event_processor(event_processor) + + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": StrawberryIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_async_view_handle_errors + ) + sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_sync_view_handle_errors + ) + + +def _make_request_event_processor( + execution_context: "ExecutionContext", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + request_data = event.setdefault("request", {}) + request_data["api_target"] = "graphql" + + if not request_data.get("data"): + data: "dict[str, Any]" = {"query": execution_context.query} + if execution_context.variables: + data["variables"] = execution_context.variables + if execution_context.operation_name: + data["operationName"] = execution_context.operation_name + + request_data["data"] = data + + else: + try: + del event["request"]["data"] + except (KeyError, TypeError): + pass + + return event + + return inner + + +def _make_response_event_processor( + response_data: "GraphQLHTTPResponse", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + contexts = event.setdefault("contexts", {}) + contexts["response"] = {"data": response_data} + + return event + + return inner + + +def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": + if StrawberrySentryAsyncExtension in extensions: + return True + elif StrawberrySentrySyncExtension in extensions: + return False + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py new file mode 100644 index 0000000000..4927c0e885 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py @@ -0,0 +1,65 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import NoReturn, Union + + +class SysExitIntegration(Integration): + """Captures sys.exit calls and sends them as events to Sentry. + + By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit + exceptions generated by sys.exit calls and send them to Sentry. + + This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and + non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. + Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. + """ + + identifier = "sys_exit" + + def __init__(self, *, capture_successful_exits: bool = False) -> None: + self._capture_successful_exits = capture_successful_exits + + @staticmethod + def setup_once() -> None: + SysExitIntegration._patch_sys_exit() + + @staticmethod + def _patch_sys_exit() -> None: + old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit + + @functools.wraps(old_exit) + def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": + # @ensure_integration_enabled ensures that this is non-None + integration = sentry_sdk.get_client().get_integration(SysExitIntegration) + if integration is None: + old_exit(__status) + + try: + old_exit(__status) + except SystemExit as e: + with capture_internal_exceptions(): + if integration._capture_successful_exits or __status not in ( + 0, + None, + ): + _capture_exception(e) + raise e + + sys.exit = sentry_patched_exit + + +def _capture_exception(exc: "SystemExit") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": SysExitIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py new file mode 100644 index 0000000000..f3ba046332 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py @@ -0,0 +1,196 @@ +import sys +import warnings +from concurrent.futures import Future, ThreadPoolExecutor +from functools import wraps +from threading import Thread, current_thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import use_isolation_scope, use_scope +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T", bound=Any) + + +class ThreadingIntegration(Integration): + identifier = "threading" + + def __init__( + self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True + ) -> None: + if propagate_hub is not None: + logger.warning( + "Deprecated: propagate_hub is deprecated. This will be removed in the future." + ) + + # Note: propagate_hub did not have any effect on propagation of scope data + # scope data was always propagated no matter what the value of propagate_hub was + # This is why the default for propagate_scope is True + + self.propagate_scope = propagate_scope + + if propagate_hub is not None: + self.propagate_scope = propagate_hub + + @staticmethod + def setup_once() -> None: + old_start = Thread.start + + try: + from django import VERSION as django_version # noqa: N811 + except ImportError: + django_version = None + + try: + import channels # type: ignore[import-untyped] + + channels_version = channels.__version__ + except (ImportError, AttributeError): + channels_version = None + + is_async_emulated_with_threads = ( + sys.version_info < (3, 9) + and channels_version is not None + and channels_version < "4.0.0" + and django_version is not None + and django_version >= (3, 0) + and django_version < (4, 0) + ) + + @wraps(old_start) + def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return old_start(self, *a, **kw) + + if integration.propagate_scope: + if is_async_emulated_with_threads: + warnings.warn( + "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " + "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " + "Please either upgrade to Django channels 4.0+, use Django's async features " + "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", + stacklevel=2, + ) + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + + else: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + # Patching instance methods in `start()` creates a reference cycle if + # done in a naive way. See + # https://github.com/getsentry/sentry-python/pull/434 + # + # In threading module, using current_thread API will access current thread instance + # without holding it to avoid a reference cycle in an easier way. + with capture_internal_exceptions(): + new_run = _wrap_run( + isolation_scope, + current_scope, + getattr(self.run, "__func__", self.run), + ) + self.run = new_run # type: ignore + + return old_start(self, *a, **kw) + + Thread.start = sentry_start # type: ignore + ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore + ThreadPoolExecutor.submit, is_async_emulated_with_threads + ) + + +def _wrap_run( + isolation_scope_to_use: "Optional[sentry_sdk.Scope]", + current_scope_to_use: "Optional[sentry_sdk.Scope]", + old_run_func: "F", +) -> "F": + @wraps(old_run_func) + def run(*a: "Any", **kw: "Any") -> "Any": + def _run_old_run_func() -> "Any": + try: + self = current_thread() + return old_run_func(self, *a[1:], **kw) + except Exception: + reraise(*_capture_exception()) + + if isolation_scope_to_use is not None and current_scope_to_use is not None: + with use_isolation_scope(isolation_scope_to_use): + with use_scope(current_scope_to_use): + return _run_old_run_func() + else: + return _run_old_run_func() + + return run # type: ignore + + +def _wrap_threadpool_executor_submit( + func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool +) -> "Callable[..., Future[T]]": + """ + Wrap submit call to propagate scopes on task submission. + """ + + @wraps(func) + def sentry_submit( + self: "ThreadPoolExecutor", + fn: "Callable[..., T]", + *args: "Any", + **kwargs: "Any", + ) -> "Future[T]": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return func(self, fn, *args, **kwargs) + + if integration.propagate_scope and is_async_emulated_with_threads: + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + elif integration.propagate_scope: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": + if isolation_scope is not None and current_scope is not None: + with use_isolation_scope(isolation_scope): + with use_scope(current_scope): + return fn(*args, **kwargs) + + return fn(*args, **kwargs) + + return func(self, wrapped_fn, *args, **kwargs) + + return sentry_submit + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + if client.get_integration(ThreadingIntegration) is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "threading", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py new file mode 100644 index 0000000000..859b0d0870 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py @@ -0,0 +1,320 @@ +import contextlib +import weakref +from inspect import iscoroutinefunction + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + RequestExtractor, + _filter_headers, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from tornado import version_info as TORNADO_VERSION + from tornado.gen import coroutine + from tornado.web import HTTPError, RequestHandler +except ImportError: + raise DidNotEnable("Tornado not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.tracing import Span + + +class TornadoIntegration(Integration): + identifier = "tornado" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(TornadoIntegration, TORNADO_VERSION) + + if not HAS_REAL_CONTEXTVARS: + # Tornado is async. We better have contextvars or we're going to leak + # state between requests. + raise DidNotEnable( + "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" + + CONTEXTVARS_ERROR_MESSAGE + ) + + ignore_logger("tornado.access") + + old_execute = RequestHandler._execute + + awaitable = iscoroutinefunction(old_execute) + + if awaitable: + # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) + # In that case our method should be a coroutine function too + async def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + return await old_execute(self, *args, **kwargs) + + else: + + @coroutine # type: ignore + def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + result = yield from old_execute(self, *args, **kwargs) + return result + + RequestHandler._execute = sentry_execute_request_handler + + old_log_exception = RequestHandler.log_exception + + def sentry_log_exception( + self: "Any", + ty: type, + value: BaseException, + tb: "Any", + *args: "Any", + **kwargs: "Any", + ) -> "Optional[Any]": + _capture_exception(ty, value, tb) + return old_log_exception(self, ty, value, tb, *args, **kwargs) + + RequestHandler.log_exception = sentry_log_exception + + +_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" + + +@contextlib.contextmanager +def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": + integration = sentry_sdk.get_client().get_integration(TornadoIntegration) + + if integration is None: + yield + return + + weak_handler = weakref.ref(self) + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + headers = self.request.headers + + scope.clear_breadcrumbs() + processor = _make_event_processor(weak_handler) + scope.add_event_processor(processor) + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(dict(headers)) + scope.set_custom_sampling_context({"tornado_request": self.request}) + + if should_send_default_pii() and self.request.remote_ip: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_ROOT_SPAN_NAME, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": TornadoIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # Like with all other integrations, this is our + # fallback transaction in case there is no route. + # sentry_urldispatcher_resolve is responsible for + # setting a transaction name later. + name=_DEFAULT_ROOT_SPAN_NAME, + source=TransactionSource.ROUTE, + origin=TornadoIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"tornado_request": self.request}, + ) + + with span_ctx as span: + try: + yield + finally: + if type(span) is StreamedSpan: + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + self.request + ).items(): + span.set_attribute(attr, value) + + with capture_internal_exceptions(): + method = getattr(self, self.request.method.lower(), None) + if method is not None: + span_name = transaction_from_function(method) + if span_name: + span.name = span_name + span.set_attribute( + "sentry.span.source", + SegmentSource.COMPONENT, + ) + + with capture_internal_exceptions(): + status_int = self.get_status() + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) + span.status = "error" if status_int >= 400 else "ok" + + +def _get_request_attributes(request: "Any") -> "Dict[str, Any]": + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.full_url() + attributes["url.path"] = request.path + + if request.query: + attributes[SPANDATA.URL_QUERY] = request.query + + if request.protocol: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol + + if should_send_default_pii() and request.remote_ip: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + + with capture_internal_exceptions(): + raw_data = _get_tornado_request_data(request) + body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data + if body_data is not None: + attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data + + return attributes + + +def _get_tornado_request_data( + request: "Any", +) -> "Union[Optional[str], AnnotatedValue]": + body = request.body + if not body: + return None + + if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): + return AnnotatedValue.substituted_because_over_size_limit() + + return body.decode("utf-8", "replace") + + +@ensure_integration_enabled(TornadoIntegration) +def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: + if isinstance(value, HTTPError): + return + + event, hint = event_from_exception( + (ty, value, tb), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "tornado", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + weak_handler: "Callable[[], RequestHandler]", +) -> "EventProcessor": + def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + handler = weak_handler() + if handler is None: + return event + + request = handler.request + + with capture_internal_exceptions(): + method = getattr(handler, handler.request.method.lower()) + event["transaction"] = transaction_from_function(method) or "" + event["transaction_info"] = {"source": TransactionSource.COMPONENT} + + with capture_internal_exceptions(): + extractor = TornadoRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + + request_info["url"] = "%s://%s%s" % ( + request.protocol, + request.host, + request.path, + ) + + request_info["query_string"] = request.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_ip} + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + try: + current_user = handler.current_user + except Exception: + current_user = None + + if current_user: + event.setdefault("user", {}).setdefault("is_authenticated", True) + + return event + + return tornado_processor + + +class TornadoRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return {k: v.value for k, v in self.request.cookies.items()} + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "Dict[str, Any]": + return { + k: [v.decode("latin1", "replace") for v in vs] + for k, vs in self.request.body_arguments.items() + } + + def is_json(self) -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + def files(self) -> "Dict[str, Any]": + return {k: v[0] for k, v in self.request.files.items() if v} + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py new file mode 100644 index 0000000000..0449a8f10c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py @@ -0,0 +1,52 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.utils import ensure_integration_enabled, event_from_exception + +try: + from trytond.exceptions import TrytonException # type: ignore + from trytond.wsgi import app # type: ignore +except ImportError: + raise DidNotEnable("Trytond is not installed.") + +# TODO: trytond-worker, trytond-cron and trytond-admin intergations + + +class TrytondWSGIIntegration(Integration): + identifier = "trytond_wsgi" + origin = f"auto.http.{identifier}" + + def __init__(self) -> None: + pass + + @staticmethod + def setup_once() -> None: + app.wsgi_app = SentryWsgiMiddleware( + app.wsgi_app, + span_origin=TrytondWSGIIntegration.origin, + ) + + @ensure_integration_enabled(TrytondWSGIIntegration) + def error_handler(e: Exception) -> None: + if isinstance(e, TrytonException): + return + else: + client = sentry_sdk.get_client() + event, hint = event_from_exception( + e, + client_options=client.options, + mechanism={"type": "trytond", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + # Expected error handlers signature was changed + # when the error_handler decorator was introduced + # in Tryton-5.4 + if hasattr(app, "error_handler"): + + @app.error_handler + def _(app, request, e): # type: ignore + error_handler(e) + + else: + app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py new file mode 100644 index 0000000000..497f0539ec --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + +try: + import typer + from typer.main import except_hook +except ImportError: + raise DidNotEnable("Typer not installed") + + +class TyperIntegration(Integration): + identifier = "typer" + + @staticmethod + def setup_once() -> None: + typer.main.except_hook = _make_excepthook(except_hook) # type: ignore + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(TyperIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "typer", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py new file mode 100644 index 0000000000..0316f6b88a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py @@ -0,0 +1,33 @@ +from functools import wraps +from typing import Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from UnleashClient import UnleashClient +except ImportError: + raise DidNotEnable("UnleashClient is not installed") + + +class UnleashIntegration(Integration): + identifier = "unleash" + + @staticmethod + def setup_once() -> None: + # Wrap and patch evaluation methods (class methods) + old_is_enabled = UnleashClient.is_enabled + + @wraps(old_is_enabled) + def sentry_is_enabled( + self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_is_enabled(self, feature, *args, **kwargs) + + # We have no way of knowing what type of unleash feature this is, so we have to treat + # it as a boolean / toggle feature. + add_feature_flag(feature, enabled) + + return enabled + + UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py new file mode 100644 index 0000000000..2c7280a1f2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py @@ -0,0 +1,50 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Callable + + +class UnraisablehookIntegration(Integration): + identifier = "unraisablehook" + + @staticmethod + def setup_once() -> None: + sys.unraisablehook = _make_unraisable(sys.unraisablehook) + + +def _make_unraisable( + old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", +) -> "Callable[[sys.UnraisableHookArgs], Any]": + def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: + integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_unraisablehook(unraisable) + + if unraisable.exc_value and unraisable.exc_traceback: + with capture_internal_exceptions(): + event, hint = event_from_exception( + ( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "unraisablehook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_unraisablehook(unraisable) + + return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py new file mode 100644 index 0000000000..e776ed915a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py @@ -0,0 +1,427 @@ +import sys +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._werkzeug import _get_headers, get_host +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + _filter_headers, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Iterator, + Optional, + Protocol, + Tuple, + TypeVar, + Union, + ) + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + WsgiResponseIter = TypeVar("WsgiResponseIter") + WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") + WsgiExcInfo = TypeVar("WsgiExcInfo") + + class StartResponse(Protocol): + def __call__( + self, + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, + ) -> "WsgiResponseIter": # type: ignore + pass + + +_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_DEFAULT_TRANSACTION_NAME = "generic WSGI request" + + +def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: + return s.encode("latin1").decode(charset, errors) + + +def get_request_url( + environ: "Dict[str, str]", use_x_forwarded_for: bool = False +) -> str: + """Return the absolute URL without query string for the given WSGI + environment.""" + script_name = environ.get("SCRIPT_NAME", "").rstrip("/") + path_info = environ.get("PATH_INFO", "").lstrip("/") + path = f"{script_name}/{path_info}" + + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + return "%s://%s/%s" % ( + scheme, + get_host(environ, use_x_forwarded_for), + wsgi_decoding_dance(path).lstrip("/"), + ) + + +class SentryWsgiMiddleware: + __slots__ = ( + "app", + "use_x_forwarded_for", + "span_origin", + "http_methods_to_capture", + ) + + def __init__( + self, + app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", + use_x_forwarded_for: bool = False, + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + self.app = app + self.use_x_forwarded_for = use_x_forwarded_for + self.span_origin = span_origin + self.http_methods_to_capture = http_methods_to_capture + + def __call__( + self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "Any": + if _wsgi_middleware_applied.get(False): + return self.app(environ, start_response) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _wsgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for + ) + ) + + method = environ.get("REQUEST_METHOD", "").upper() + + span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None + if method in self.http_methods_to_capture: + if span_streaming: + sentry_sdk.traces.continue_trace( + dict(_get_headers(environ)) + ) + Scope.set_custom_sampling_context({"wsgi_environ": environ}) + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, client_ip + ) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_TRANSACTION_NAME, + attributes={ + "sentry.span.source": SegmentSource.ROUTE, + "sentry.origin": self.span_origin, + "sentry.op": OP.HTTP_SERVER, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + environ, + op=OP.HTTP_SERVER, + name=_DEFAULT_TRANSACTION_NAME, + source=TransactionSource.ROUTE, + origin=self.span_origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"wsgi_environ": environ}, + ) + + span_ctx = span_ctx or nullcontext() + + with span_ctx as span: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + environ, self.use_x_forwarded_for + ).items(): + span.set_attribute(attr, value) + + try: + response = self.app( + environ, + partial(_sentry_start_response, start_response, span), + ) + except BaseException: + reraise(*_capture_exception()) + finally: + _wsgi_middleware_applied.set(False) + + # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses + # is determined by a pointer equality check on the response object + # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). + # + # If we were to return a _ScopedResponse, this would cause the check to always fail + # since it's checking the files are exactly the same. + # + # To avoid this and ensure that the offloading mechanism works as expected when it's + # enabled, we check if the response is a file-like object (determined by the presence + # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, + # it would've been used in handling the file in the response). + # + # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, + # so we want to make sure we don't interfere with those either. + # + # If all conditions are met, we return the original response object directly, + # allowing uWSGI to handle it as intended. + if ( + environ.get("wsgi.file_wrapper") + and getattr(response, "fileno", None) is not None + ): + return response + + return _ScopedResponse(scope, response) + + +def _sentry_start_response( + old_start_response: "StartResponse", + span: "Optional[Union[Span, StreamedSpan]]", + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, +) -> "WsgiResponseIter": # type: ignore[type-var] + with capture_internal_exceptions(): + status_int = int(status.split(" ", 1)[0]) + if span is not None: + if isinstance(span, StreamedSpan): + span.status = "error" if status_int >= 400 else "ok" + span.set_attribute("http.response.status_code", status_int) + else: + span.set_http_status(status_int) + + if exc_info is None: + # The Django Rest Framework WSGI test client, and likely other + # (incorrect) implementations, cannot deal with the exc_info argument + # if one is present. Avoid providing a third argument if not necessary. + return old_start_response(status, response_headers) + else: + return old_start_response(status, response_headers, exc_info) + + +def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns our explicitly included environment variables we want to + capture (server name, port and remote addr if pii is enabled). + """ + keys = ["SERVER_NAME", "SERVER_PORT"] + if should_send_default_pii(): + # make debugging of proxy setup easier. Proxy headers are + # in headers. + keys += ["REMOTE_ADDR"] + + for key in keys: + if key in environ: + yield key, environ[key] + + +def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": + """ + Infer the user IP address from various headers. This cannot be used in + security sensitive situations since the value may be forged from a client, + but it's good enough for the event payload. + """ + try: + return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return environ["HTTP_X_REAL_IP"] + except KeyError: + pass + + return environ.get("REMOTE_ADDR") + + +def _capture_exception() -> "ExcInfo": + """ + Captures the current exception and sends it to Sentry. + Returns the ExcInfo tuple to it can be reraised afterwards. + """ + exc_info = sys.exc_info() + e = exc_info[1] + + # SystemExit(0) is the only uncaught exception that is expected behavior + should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) + if not should_skip_capture: + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "wsgi", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class _ScopedResponse: + """ + Users a separate scope for each response chunk. + + This will make WSGI apps more tolerant against: + - WSGI servers streaming responses from a different thread/from + different threads than the one that called start_response + - close() not being called + - WSGI servers streaming responses interleaved from the same thread + """ + + __slots__ = ("_response", "_scope") + + def __init__( + self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" + ) -> None: + self._scope = scope + self._response = response + + def __iter__(self) -> "Iterator[bytes]": + iterator = iter(self._response) + + while True: + with use_isolation_scope(self._scope): + try: + chunk = next(iterator) + except StopIteration: + break + except BaseException: + reraise(*_capture_exception()) + + yield chunk + + def close(self) -> None: + with use_isolation_scope(self._scope): + try: + self._response.close() # type: ignore + except AttributeError: + pass + except BaseException: + reraise(*_capture_exception()) + + +def _make_wsgi_event_processor( + environ: "Dict[str, str]", use_x_forwarded_for: bool +) -> "EventProcessor": + # It's a bit unfortunate that we have to extract and parse the request data + # from the environ so eagerly, but there are a few good reasons for this. + # + # We might be in a situation where the scope never gets torn down + # properly. In that case we will have an unnecessary strong reference to + # all objects in the environ (some of which may take a lot of memory) when + # we're really just interested in a few of them. + # + # Keeping the environment around for longer than the request lifecycle is + # also not necessarily something uWSGI can deal with: + # https://github.com/unbit/uwsgi/issues/1950 + + client_ip = get_client_ip(environ) + request_url = get_request_url(environ, use_x_forwarded_for) + query_string = environ.get("QUERY_STRING") + method = environ.get("REQUEST_METHOD") + env = dict(_get_environ(environ)) + headers = _filter_headers(dict(_get_headers(environ))) + + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + # if the code below fails halfway through we at least have some data + request_info = event.setdefault("request", {}) + + if should_send_default_pii(): + user_info = event.setdefault("user", {}) + if client_ip: + user_info.setdefault("ip_address", client_ip) + + request_info["url"] = request_url + request_info["query_string"] = query_string + request_info["method"] = method + request_info["env"] = env + request_info["headers"] = headers + + return event + + return event_processor + + +def _get_request_attributes( + environ: "Dict[str, str]", + use_x_forwarded_for: bool = False, +) -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from the WSGI environ. + """ + attributes: "dict[str, Any]" = {} + + method = environ.get("REQUEST_METHOD") + if method: + attributes["http.request.method"] = method.upper() + + headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + url_scheme = environ.get("wsgi.url_scheme") + if url_scheme: + attributes["network.protocol.name"] = url_scheme + + server_name = environ.get("SERVER_NAME") + if server_name: + attributes["server.address"] = server_name + + server_port = environ.get("SERVER_PORT") + if server_port: + try: + attributes["server.port"] = int(server_port) + except ValueError: + pass + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + attributes["client.address"] = client_ip + + query_string = environ.get("QUERY_STRING") + if query_string: + attributes["http.query"] = query_string + + path = environ.get("PATH_INFO", "") + if path: + attributes["url.path"] = path + + attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py new file mode 100644 index 0000000000..d7f4425dfd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py @@ -0,0 +1,88 @@ +# NOTE: this is the logger sentry exposes to users, not some generic logger. +import functools +import time +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk.utils import capture_internal_exceptions, format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes + + +OTEL_RANGES = [ + # ((severity level range), severity text) + # https://opentelemetry.io/docs/specs/otel/logs/data-model + ((1, 4), "trace"), + ((5, 8), "debug"), + ((9, 12), "info"), + ((13, 16), "warn"), + ((17, 20), "error"), + ((21, 24), "fatal"), +] + + +class _dict_default_key(dict): # type: ignore[type-arg] + """dict that returns the key if missing.""" + + def __missing__(self, key: str) -> str: + return "{" + key + "}" + + +def _capture_log( + severity_text: str, severity_number: int, template: str, **kwargs: "Any" +) -> None: + body = template + + attributes: "Attributes" = {} + + if "attributes" in kwargs: + provided_attributes = kwargs.pop("attributes") or {} + for attribute, value in provided_attributes.items(): + attributes[attribute] = format_attribute(value) + + for k, v in kwargs.items(): + attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) + + if kwargs: + # only attach template if there are parameters + attributes["sentry.message.template"] = format_attribute(template) + + with capture_internal_exceptions(): + body = template.format_map(_dict_default_key(kwargs)) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": severity_text, + "severity_number": severity_number, + "attributes": attributes, + "body": body, + "time_unix_nano": time.time_ns(), + "trace_id": None, + "span_id": None, + } + ) + + +trace = functools.partial(_capture_log, "trace", 1) +debug = functools.partial(_capture_log, "debug", 5) +info = functools.partial(_capture_log, "info", 9) +warning = functools.partial(_capture_log, "warn", 13) +error = functools.partial(_capture_log, "error", 17) +fatal = functools.partial(_capture_log, "fatal", 21) + + +def _otel_severity_text(otel_severity_number: int) -> str: + for (lower, upper), severity in OTEL_RANGES: + if lower <= otel_severity_number <= upper: + return severity + + return "default" + + +def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": + for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): + if level >= py_level: + return otel_severity_number, _otel_severity_text(otel_severity_number) + + return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py new file mode 100644 index 0000000000..27b7468c0d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py @@ -0,0 +1,62 @@ +import time +from typing import TYPE_CHECKING, Any, Optional + +import sentry_sdk +from sentry_sdk.utils import format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes, Metric, MetricType + + +def _capture_metric( + name: str, + metric_type: "MetricType", + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[Attributes]" = None, +) -> None: + attrs: "Attributes" = {} + + if attributes: + for k, v in attributes.items(): + attrs[k] = format_attribute(v) + + metric: "Metric" = { + "timestamp": time.time(), + "trace_id": None, + "span_id": None, + "name": name, + "type": metric_type, + "value": float(value), + "unit": unit, + "attributes": attrs, + } + + sentry_sdk.get_current_scope()._capture_metric(metric) + + +def count( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "counter", value, unit, attributes) + + +def gauge( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "gauge", value, unit, attributes) + + +def distribution( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py new file mode 100644 index 0000000000..d2ba298c35 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py @@ -0,0 +1,138 @@ +import os +import time +import weakref +from threading import Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +MAX_DOWNSAMPLE_FACTOR = 10 + + +class Monitor: + """ + Performs health checks in a separate thread once every interval seconds + and updates the internal state. Other parts of the SDK only read this state + and act accordingly. + """ + + name = "sentry.monitor" + + _thread: "Optional[Thread]" + _thread_for_pid: "Optional[int]" + + def __init__( + self, transport: "sentry_sdk.transport.Transport", interval: float = 10 + ) -> None: + self.transport: "sentry_sdk.transport.Transport" = transport + self.interval: float = interval + + self._healthy = True + self._downsample_factor: int = 0 + self._running = True + self._reset_thread_state() + + # See https://github.com/getsentry/sentry-python/issues/6148. + # If os.fork() runs while another thread holds self._thread_lock, + # the child inherits the lock locked but the holding thread does + # not exist in the child, so the lock can never be released and + # _ensure_running deadlocks forever. Reinitialise the lock and + # cached thread/pid in the child so it starts clean regardless + # of inherited state. We bind via a WeakMethod so the + # permanently-registered fork handler does not pin this Monitor + # (and its Transport): register_at_fork has no unregister API. + # POSIX-only; Windows uses spawn. + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._thread = None + self._thread_lock = Lock() + self._thread_for_pid = None + + def _ensure_running(self) -> None: + """ + Check that the monitor has an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + while self._running: + time.sleep(self.interval) + if self._running: + self.run() + + thread = Thread(name=self.name, target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def run(self) -> None: + self.check_health() + self.set_downsample_factor() + + def set_downsample_factor(self) -> None: + if self._healthy: + if self._downsample_factor > 0: + logger.debug( + "[Monitor] health check positive, reverting to normal sampling" + ) + self._downsample_factor = 0 + else: + if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: + self._downsample_factor += 1 + logger.debug( + "[Monitor] health check negative, downsampling with a factor of %d", + self._downsample_factor, + ) + + def check_health(self) -> None: + """ + Perform the actual health checks, + currently only checks if the transport is rate-limited. + TODO: augment in the future with more checks. + """ + self._healthy = self.transport.is_healthy() + + def is_healthy(self) -> bool: + self._ensure_running() + return self._healthy + + @property + def downsample_factor(self) -> int: + self._ensure_running() + return self._downsample_factor + + def kill(self) -> None: + self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py new file mode 100644 index 0000000000..d562405295 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py @@ -0,0 +1,49 @@ +from sentry_sdk.profiler.continuous_profiler import ( + start_profile_session, + start_profiler, + stop_profile_session, + stop_profiler, +) +from sentry_sdk.profiler.transaction_profiler import ( + MAX_PROFILE_DURATION_NS, + PROFILE_MINIMUM_SAMPLES, + GeventScheduler, + Profile, + Scheduler, + ThreadScheduler, + has_profiling_enabled, + setup_profiler, + teardown_profiler, +) +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + MAX_STACK_DEPTH, + extract_frame, + extract_stack, + frame_id, + get_frame_name, +) + +__all__ = [ + "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` + "start_profiler", + "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` + "stop_profiler", + # DEPRECATED: The following was re-exported for backwards compatibility. It + # will be removed from sentry_sdk.profiler in a future release. + "MAX_PROFILE_DURATION_NS", + "PROFILE_MINIMUM_SAMPLES", + "Profile", + "Scheduler", + "ThreadScheduler", + "GeventScheduler", + "has_profiling_enabled", + "setup_profiler", + "teardown_profiler", + "DEFAULT_SAMPLING_FREQUENCY", + "MAX_STACK_DEPTH", + "get_frame_name", + "extract_frame", + "extract_stack", + "frame_id", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py new file mode 100644 index 0000000000..ed525f52bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py @@ -0,0 +1,703 @@ +import atexit +import os +import random +import sys +import threading +import time +import uuid +import warnings +from collections import deque +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.consts import VERSION +from sentry_sdk.envelope import Envelope +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + is_gevent, + logger, + now, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union + + from typing_extensions import TypedDict + + from sentry_sdk._types import ContinuousProfilerMode, SDKInfo + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "timestamp": float, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + ThreadPool = None + + +_scheduler: "Optional[ContinuousScheduler]" = None + + +def setup_continuous_profiler( + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", +) -> bool: + global _scheduler + + already_initialized = _scheduler is not None + + if already_initialized: + logger.debug("[Profiling] Continuous Profiler is already setup") + teardown_continuous_profiler() + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventContinuousScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventContinuousScheduler.mode + else: + default_profiler_mode = ThreadContinuousScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + # TODO: deprecate this and just use the existing `profiler_mode` + experiments = options.get("_experiments", {}) + + profiler_mode = ( + experiments.get("continuous_profiling_mode") or default_profiler_mode + ) + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if profiler_mode == ThreadContinuousScheduler.mode: + _scheduler = ThreadContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + elif profiler_mode == GeventContinuousScheduler.mode: + _scheduler = GeventContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + else: + raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up continuous profiler in {mode} mode".format( + mode=_scheduler.mode + ) + ) + + if not already_initialized: + atexit.register(teardown_continuous_profiler) + + return True + + +def is_profile_session_sampled() -> bool: + if _scheduler is None: + return False + return _scheduler.sampled + + +def try_autostart_continuous_profiler() -> None: + # TODO: deprecate this as it'll be replaced by the auto lifecycle option + + if _scheduler is None: + return + + if not _scheduler.is_auto_start_enabled(): + return + + _scheduler.manual_start() + + +def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": + if _scheduler is None: + return None + + return _scheduler.auto_start() + + +def start_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_start() + + +def start_profile_session() -> None: + warnings.warn( + "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + start_profiler() + + +def stop_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_stop() + + +def stop_profile_session() -> None: + warnings.warn( + "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + stop_profiler() + + +def teardown_continuous_profiler() -> None: + stop_profiler() + + global _scheduler + _scheduler = None + + +def get_profiler_id() -> "Union[str, None]": + if _scheduler is None: + return None + return _scheduler.profiler_id + + +def determine_profile_session_sampling_decision( + sample_rate: "Union[float, None]", +) -> bool: + # `None` is treated as `0.0` + if not sample_rate: + return False + + return random.random() < float(sample_rate) + + +class ContinuousProfile: + active: bool = True + + def stop(self) -> None: + self.active = False + + +class ContinuousScheduler: + mode: "ContinuousProfilerMode" = "unknown" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.interval = 1.0 / frequency + self.options = options + self.sdk_info = sdk_info + self.capture_func = capture_func + + self.lifecycle = self.options.get("profile_lifecycle") + profile_session_sample_rate = self.options.get("profile_session_sample_rate") + self.sampled = determine_profile_session_sampling_decision( + profile_session_sample_rate + ) + + self.sampler = self.make_sampler() + self.buffer: "Optional[ProfileBuffer]" = None + self.pid: "Optional[int]" = None + + self.running = False + self.soft_shutdown = False + + self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) + self.active_profiles: "Set[ContinuousProfile]" = set() + + def is_auto_start_enabled(self) -> bool: + # Ensure that the scheduler only autostarts once per process. + # This is necessary because many web servers use forks to spawn + # additional processes. And the profiler is only spawned on the + # master process, then it often only profiles the main process + # and not the ones where the requests are being handled. + if self.pid == os.getpid(): + return False + + experiments = self.options.get("_experiments") + if not experiments: + return False + + return experiments.get("continuous_profiling_auto_start") + + def auto_start(self) -> "Union[ContinuousProfile, None]": + if not self.sampled: + return None + + if self.lifecycle != "trace": + return None + + logger.debug("[Profiling] Auto starting profiler") + + profile = ContinuousProfile() + + self.new_profiles.append(profile) + self.ensure_running() + + return profile + + def manual_start(self) -> None: + if not self.sampled: + return + + if self.lifecycle != "manual": + return + + self.ensure_running() + + def manual_stop(self) -> None: + if self.lifecycle != "manual": + return + + self.teardown() + + def ensure_running(self) -> None: + raise NotImplementedError + + def teardown(self) -> None: + raise NotImplementedError + + def pause(self) -> None: + raise NotImplementedError + + def reset_buffer(self) -> None: + self.buffer = ProfileBuffer( + self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func + ) + + @property + def profiler_id(self) -> "Union[str, None]": + if not self.running or self.buffer is None: + return None + return self.buffer.profiler_id + + def make_sampler(self) -> "Callable[..., bool]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + if self.lifecycle == "trace": + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + return True + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + inactive_profiles = [] + + for profile in self.active_profiles: + if not profile.active: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + else: + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + return _sample_stack + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.soft_shutdown = self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # the soft shutdown happens here to give it a chance + # for the profiler to be reused + if self.soft_shutdown: + self.running = False + + # make sure to explicitly exit the profiler here or there might + # be multiple profilers at once + break + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + buffer = self.buffer + if buffer is not None: + buffer.flush() + + +class ThreadContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ContinuousProfilerMode" = "thread" + name = "sentry.profiler.ThreadContinuousScheduler" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[threading.Thread]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +class GeventContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ContinuousProfilerMode" = "gevent" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[_ThreadPool]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +PROFILE_BUFFER_SECONDS = 60 + + +class ProfileBuffer: + def __init__( + self, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + buffer_size: int, + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.options = options + self.sdk_info = sdk_info + self.buffer_size = buffer_size + self.capture_func = capture_func + + self.profiler_id = uuid.uuid4().hex + self.chunk = ProfileChunk() + + # Make sure to use the same clock to compute a sample's monotonic timestamp + # to ensure the timestamps are correctly aligned. + self.start_monotonic_time = now() + + # Make sure the start timestamp is defined only once per profiler id. + # This prevents issues with clock drift within a single profiler session. + # + # Subtracting the start_monotonic_time here to find a fixed starting position + # for relative monotonic timestamps for each sample. + self.start_timestamp = ( + datetime.now(timezone.utc).timestamp() - self.start_monotonic_time + ) + + def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: + if self.should_flush(monotonic_time): + self.flush() + self.chunk = ProfileChunk() + self.start_monotonic_time = now() + + self.chunk.write(self.start_timestamp + monotonic_time, sample) + + def should_flush(self, monotonic_time: float) -> bool: + # If the delta between the new monotonic time and the start monotonic time + # exceeds the buffer size, it means we should flush the chunk + return monotonic_time - self.start_monotonic_time >= self.buffer_size + + def flush(self) -> None: + chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) + envelope = Envelope() + envelope.add_profile_chunk(chunk) + self.capture_func(envelope) + + +class ProfileChunk: + def __init__(self) -> None: + self.chunk_id = uuid.uuid4().hex + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + def write(self, ts: float, sample: "ExtractedSample") -> None: + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "timestamp": ts, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def to_json( + self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" + ) -> "Dict[str, Any]": + profile = { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + }, + } + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + payload = { + "chunk_id": self.chunk_id, + "client_sdk": { + "name": sdk_info["name"], + "version": VERSION, + }, + "platform": "python", + "profile": profile, + "profiler_id": profiler_id, + "version": "2", + } + + for key in "release", "environment", "dist": + if options[key] is not None: + payload[key] = str(options[key]).strip() + + return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py new file mode 100644 index 0000000000..fe65774c0b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py @@ -0,0 +1,802 @@ +""" +This file is originally based on code from https://github.com/nylas/nylas-perftools, +which is published under the following license: + +The MIT License (MIT) + +Copyright (c) 2014 Nylas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import atexit +import os +import platform +import random +import sys +import threading +import time +import uuid +import warnings +from abc import ABC, abstractmethod +from collections import deque +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + capture_internal_exceptions, + get_current_thread_meta, + is_gevent, + is_valid_sample_rate, + logger, + nanosecond_time, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type + + from typing_extensions import TypedDict + + from sentry_sdk._types import Event, ProfilerMode, SamplingContext + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + ProcessedThreadMetadata, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "elapsed_since_start_ns": str, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + ProcessedProfile = TypedDict( + "ProcessedProfile", + { + "frames": List[ProcessedFrame], + "stacks": List[ProcessedStack], + "samples": List[ProcessedSample], + "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + + ThreadPool = None + + +_scheduler: "Optional[Scheduler]" = None + + +# The minimum number of unique samples that must exist in a profile to be +# considered valid. +PROFILE_MINIMUM_SAMPLES = 2 + + +def has_profiling_enabled(options: "Dict[str, Any]") -> bool: + profiles_sampler = options["profiles_sampler"] + if profiles_sampler is not None: + return True + + profiles_sample_rate = options["profiles_sample_rate"] + if profiles_sample_rate is not None and profiles_sample_rate > 0: + return True + + profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") + if profiles_sample_rate is not None: + logger.warning( + "_experiments['profiles_sample_rate'] is deprecated. " + "Please use the non-experimental profiles_sample_rate option " + "directly." + ) + if profiles_sample_rate > 0: + return True + + return False + + +def setup_profiler(options: "Dict[str, Any]") -> bool: + global _scheduler + + if _scheduler is not None: + logger.debug("[Profiling] Profiler is already setup") + return False + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventScheduler.mode + else: + default_profiler_mode = ThreadScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + profiler_mode = options.get("_experiments", {}).get("profiler_mode") + if profiler_mode is not None: + logger.warning( + "_experiments['profiler_mode'] is deprecated. Please use the " + "non-experimental profiler_mode option directly." + ) + profiler_mode = profiler_mode or default_profiler_mode + + if ( + profiler_mode == ThreadScheduler.mode + # for legacy reasons, we'll keep supporting sleep mode for this scheduler + or profiler_mode == "sleep" + ): + _scheduler = ThreadScheduler(frequency=frequency) + elif profiler_mode == GeventScheduler.mode: + _scheduler = GeventScheduler(frequency=frequency) + else: + raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) + ) + _scheduler.setup() + + atexit.register(teardown_profiler) + + return True + + +def teardown_profiler() -> None: + global _scheduler + + if _scheduler is not None: + _scheduler.teardown() + + _scheduler = None + + +MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds + + +class Profile: + def __init__( + self, + sampled: "Optional[bool]", + start_ns: int, + hub: "Optional[sentry_sdk.Hub]" = None, + scheduler: "Optional[Scheduler]" = None, + ) -> None: + self.scheduler = _scheduler if scheduler is None else scheduler + + self.event_id: str = uuid.uuid4().hex + + self.sampled: "Optional[bool]" = sampled + + # Various framework integrations are capable of overwriting the active thread id. + # If it is set to `None` at the end of the profile, we fall back to the default. + self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 + self.active_thread_id: "Optional[int]" = None + + try: + self.start_ns: int = start_ns + except AttributeError: + self.start_ns = 0 + + self.stop_ns: int = 0 + self.active: bool = False + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + self.unique_samples = 0 + + # Backwards compatibility with the old hub property + self._hub: "Optional[sentry_sdk.Hub]" = None + if hub is not None: + self._hub = hub + warnings.warn( + "The `hub` parameter is deprecated. Please do not use it.", + DeprecationWarning, + stacklevel=2, + ) + + def update_active_thread_id(self) -> None: + self.active_thread_id = get_current_thread_meta()[0] + logger.debug( + "[Profiling] updating active thread id to {tid}".format( + tid=self.active_thread_id + ) + ) + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the profile's sampling decision according to the following + precedence rules: + + 1. If the transaction to be profiled is not sampled, that decision + will be used, regardless of anything else. + + 2. Use `profiles_sample_rate` to decide. + """ + + # The corresponding transaction was not sampled, + # so don't generate a profile for it. + if not self.sampled: + logger.debug( + "[Profiling] Discarding profile because transaction is discarded." + ) + self.sampled = False + return + + # The profiler hasn't been properly initialized. + if self.scheduler is None: + logger.debug( + "[Profiling] Discarding profile because profiler was not started." + ) + self.sampled = False + return + + client = sentry_sdk.get_client() + if not client.is_active(): + self.sampled = False + return + + options = client.options + + if callable(options.get("profiles_sampler")): + sample_rate = options["profiles_sampler"](sampling_context) + elif options["profiles_sample_rate"] is not None: + sample_rate = options["profiles_sample_rate"] + else: + sample_rate = options["_experiments"].get("profiles_sample_rate") + + # The profiles_sample_rate option was not set, so profiling + # was never enabled. + if sample_rate is None: + logger.debug( + "[Profiling] Discarding profile because profiling was not enabled." + ) + self.sampled = False + return + + if not is_valid_sample_rate(sample_rate, source="Profiling"): + logger.warning( + "[Profiling] Discarding profile because of invalid sample rate." + ) + self.sampled = False + return + + # Now we roll the dice. random.random is inclusive of 0, but not of 1, + # so strict < is safe here. In case sample_rate is a boolean, cast it + # to a float (True becomes 1.0 and False becomes 0.0) + self.sampled = random.random() < float(sample_rate) + + if self.sampled: + logger.debug("[Profiling] Initializing profile") + else: + logger.debug( + "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( + sample_rate=float(sample_rate) + ) + ) + + def start(self) -> None: + if not self.sampled or self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Starting profile") + self.active = True + if not self.start_ns: + self.start_ns = nanosecond_time() + self.scheduler.start_profiling(self) + + def stop(self) -> None: + if not self.sampled or not self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Stopping profile") + self.active = False + self.stop_ns = nanosecond_time() + + def __enter__(self) -> "Profile": + scope = sentry_sdk.get_isolation_scope() + old_profile = scope.profile + scope.profile = self + + self._context_manager_state = (scope, old_profile) + + self.start() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + with capture_internal_exceptions(): + self.stop() + + scope, old_profile = self._context_manager_state + del self._context_manager_state + + scope.profile = old_profile + + def write(self, ts: int, sample: "ExtractedSample") -> None: + if not self.active: + return + + if ts < self.start_ns: + return + + offset = ts - self.start_ns + if offset > MAX_PROFILE_DURATION_NS: + self.stop() + return + + self.unique_samples += 1 + + elapsed_since_start_ns = str(offset) + + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "elapsed_since_start_ns": elapsed_since_start_ns, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def process(self) -> "ProcessedProfile": + # This collects the thread metadata at the end of a profile. Doing it + # this way means that any threads that terminate before the profile ends + # will not have any metadata associated with it. + thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + } + + return { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": thread_metadata, + } + + def to_json( + self, event_opt: "Event", options: "Dict[str, Any]" + ) -> "Dict[str, Any]": + profile = self.process() + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + return { + "environment": event_opt.get("environment"), + "event_id": self.event_id, + "platform": "python", + "profile": profile, + "release": event_opt.get("release", ""), + "timestamp": event_opt["start_timestamp"], + "version": "1", + "device": { + "architecture": platform.machine(), + }, + "os": { + "name": platform.system(), + "version": platform.release(), + }, + "runtime": { + "name": platform.python_implementation(), + "version": platform.python_version(), + }, + "transactions": [ + { + "id": event_opt["event_id"], + "name": event_opt["transaction"], + # we start the transaction before the profile and this is + # the transaction start time relative to the profile, so we + # hardcode it to 0 until we can start the profile before + "relative_start_ns": "0", + # use the duration of the profile instead of the transaction + # because we end the transaction after the profile + "relative_end_ns": str(self.stop_ns - self.start_ns), + "trace_id": event_opt["contexts"]["trace"]["trace_id"], + "active_thread_id": str( + self._default_active_thread_id + if self.active_thread_id is None + else self.active_thread_id + ), + } + ], + } + + def valid(self) -> bool: + client = sentry_sdk.get_client() + if not client.is_active(): + return False + + if not has_profiling_enabled(client.options): + return False + + if self.sampled is None or not self.sampled: + if client.transport: + client.transport.record_lost_event( + "sample_rate", data_category="profile" + ) + return False + + if self.unique_samples < PROFILE_MINIMUM_SAMPLES: + if client.transport: + client.transport.record_lost_event( + "insufficient_data", data_category="profile" + ) + logger.debug("[Profiling] Discarding profile because insufficient samples.") + return False + + return True + + @property + def hub(self) -> "Optional[sentry_sdk.Hub]": + warnings.warn( + "The `hub` attribute is deprecated. Please do not access it.", + DeprecationWarning, + stacklevel=2, + ) + return self._hub + + @hub.setter + def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: + warnings.warn( + "The `hub` attribute is deprecated. Please do not set it.", + DeprecationWarning, + stacklevel=2, + ) + self._hub = value + + +class Scheduler(ABC): + mode: "ProfilerMode" = "unknown" + + def __init__(self, frequency: int) -> None: + self.interval = 1.0 / frequency + + self.sampler = self.make_sampler() + + # cap the number of new profiles at any time so it does not grow infinitely + self.new_profiles: "Deque[Profile]" = deque(maxlen=128) + self.active_profiles: "Set[Profile]" = set() + + def __enter__(self) -> "Scheduler": + self.setup() + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self.teardown() + + @abstractmethod + def setup(self) -> None: + pass + + @abstractmethod + def teardown(self) -> None: + pass + + def ensure_running(self) -> None: + """ + Ensure the scheduler is running. By default, this method is a no-op. + The method should be overridden by any implementation for which it is + relevant. + """ + return None + + def start_profiling(self, profile: "Profile") -> None: + self.ensure_running() + self.new_profiles.append(profile) + + def make_sampler(self) -> "Callable[..., None]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + def _sample_stack(*args: "Any", **kwargs: "Any") -> None: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + # make sure to clear the cache if we're not profiling so we dont + # keep a reference to the last stack of frames around + return + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + now = nanosecond_time() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + + inactive_profiles = [] + + for profile in self.active_profiles: + if profile.active: + profile.write(now, sample) + else: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + return _sample_stack + + +class ThreadScheduler(Scheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ProfilerMode" = "thread" + name = "sentry.profiler.ThreadScheduler" + + def __init__(self, frequency: int) -> None: + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[threading.Thread]" = None + self.pid: "Optional[int]" = None + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + """ + Check that the profiler has an active thread to run in, and start one if + that's not the case. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self.running + will be False after running this function. + """ + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + +class GeventScheduler(Scheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ProfilerMode" = "gevent" + name = "sentry.profiler.GeventScheduler" + + def __init__(self, frequency: int) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[_ThreadPool]" = None + self.pid: "Optional[int]" = None + + # This intentionally uses the gevent patched threading.Lock. + # The lock will be required when first trying to start profiles + # as we need to spawn the profiler thread from the greenlets. + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py new file mode 100644 index 0000000000..e9f0fec07f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py @@ -0,0 +1,186 @@ +import os +from collections import deque +from typing import TYPE_CHECKING + +from sentry_sdk._compat import PY311 +from sentry_sdk.utils import filename_for_module + +if TYPE_CHECKING: + from types import FrameType + from typing import Deque, List, Optional, Sequence, Tuple + + from typing_extensions import TypedDict + + from sentry_sdk._lru_cache import LRUCache + + ThreadId = str + + ProcessedStack = List[int] + + ProcessedFrame = TypedDict( + "ProcessedFrame", + { + "abs_path": str, + "filename": Optional[str], + "function": str, + "lineno": int, + "module": Optional[str], + }, + ) + + ProcessedThreadMetadata = TypedDict( + "ProcessedThreadMetadata", + {"name": str}, + ) + + FrameId = Tuple[ + str, # abs_path + int, # lineno + str, # function + ] + FrameIds = Tuple[FrameId, ...] + + # The exact value of this id is not very meaningful. The purpose + # of this id is to give us a compact and unique identifier for a + # raw stack that can be used as a key to a dictionary so that it + # can be used during the sampled format generation. + StackId = Tuple[int, int] + + ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] + ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] + +# The default sampling frequency to use. This is set at 101 in order to +# mitigate the effects of lockstep sampling. +DEFAULT_SAMPLING_FREQUENCY = 101 + + +# We want to impose a stack depth limit so that samples aren't too large. +MAX_STACK_DEPTH = 128 + + +if PY311: + + def get_frame_name(frame: "FrameType") -> str: + return frame.f_code.co_qualname + +else: + + def get_frame_name(frame: "FrameType") -> str: + f_code = frame.f_code + co_varnames = f_code.co_varnames + + # co_name only contains the frame name. If the frame was a method, + # the class name will NOT be included. + name = f_code.co_name + + # if it was a method, we can get the class name by inspecting + # the f_locals for the `self` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `self` if its an instance method + co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals + ): + for cls in type(frame.f_locals["self"]).__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # if it was a class method, (decorated with `@classmethod`) + # we can get the class name by inspecting the f_locals for the `cls` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `cls` if its a class method + co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals + ): + for cls in frame.f_locals["cls"].__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # nothing we can do if it is a staticmethod (decorated with @staticmethod) + + # we've done all we can, time to give up and return what we have + return name + + +def frame_id(raw_frame: "FrameType") -> "FrameId": + return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) + + +def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": + abs_path = raw_frame.f_code.co_filename + + try: + module = raw_frame.f_globals["__name__"] + except Exception: + module = None + + # namedtuples can be many times slower when initialing + # and accessing attribute so we opt to use a tuple here instead + return { + # This originally was `os.path.abspath(abs_path)` but that had + # a large performance overhead. + # + # According to docs, this is equivalent to + # `os.path.normpath(os.path.join(os.getcwd(), path))`. + # The `os.getcwd()` call is slow here, so we precompute it. + # + # Additionally, since we are using normalized path already, + # we skip calling `os.path.normpath` entirely. + "abs_path": os.path.join(cwd, abs_path), + "module": module, + "filename": filename_for_module(module, abs_path) or None, + "function": fid[2], + "lineno": raw_frame.f_lineno, + } + + +def extract_stack( + raw_frame: "Optional[FrameType]", + cache: "LRUCache", + cwd: str, + max_stack_depth: int = MAX_STACK_DEPTH, +) -> "ExtractedStack": + """ + Extracts the stack starting the specified frame. The extracted stack + assumes the specified frame is the top of the stack, and works back + to the bottom of the stack. + + In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, + only the first `MAX_STACK_DEPTH` frames will be returned. + """ + + raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) + + while raw_frame is not None: + f_back = raw_frame.f_back + raw_frames.append(raw_frame) + raw_frame = f_back + + frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) + frames = [] + for i, fid in enumerate(frame_ids): + frame = cache.get(fid) + if frame is None: + frame = extract_frame(fid, raw_frames[i], cwd) + cache.set(fid, frame) + frames.append(frame) + + # Instead of mapping the stack into frame ids and hashing + # that as a tuple, we can directly hash the stack. + # This saves us from having to generate yet another list. + # Additionally, using the stack as the key directly is + # costly because the stack can be large, so we pre-hash + # the stack, and use the hash as the key as this will be + # needed a few times to improve performance. + # + # To Reduce the likelihood of hash collisions, we include + # the stack depth. This means that only stacks of the same + # depth can suffer from hash collisions. + stack_id = len(raw_frames), hash(frame_ids) + + return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py new file mode 100644 index 0000000000..4fd22714cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py @@ -0,0 +1,2185 @@ +import os +import platform +import sys +import warnings +from collections import deque +from contextlib import contextmanager +from copy import copy, deepcopy +from datetime import datetime, timezone +from enum import Enum +from functools import wraps +from itertools import chain +from typing import TYPE_CHECKING, cast + +import sentry_sdk +from sentry_sdk._types import AnnotatedValue +from sentry_sdk.attachments import Attachment +from sentry_sdk.consts import ( + DEFAULT_MAX_BREADCRUMBS, + FALSE_VALUES, + INSTRUMENTER, + SPANDATA, +) +from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.profiler.transaction_profiler import Profile +from sentry_sdk.session import Session +from sentry_sdk.traces import ( + _DEFAULT_PARENT_SPAN, + NoOpStreamedSpan, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.tracing_utils import ( + Baggage, + PropagationContext, + _make_sampling_decision, + has_span_streaming_enabled, + has_tracing_enabled, + is_ignored_span, +) +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exception, + capture_internal_exceptions, + datetime_from_isoformat, + disable_capture_event, + event_from_exception, + exc_info_from_error, + format_attribute, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + Deque, + Dict, + Generator, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + ) + + from typing_extensions import Unpack + + import sentry_sdk + from sentry_sdk._types import ( + Attributes, + AttributeValue, + Breadcrumb, + BreadcrumbHint, + ErrorProcessor, + Event, + EventProcessor, + ExcInfo, + Hint, + Log, + LogLevelStr, + Metric, + SamplingContext, + Type, + ) + from sentry_sdk.tracing import TransactionKwargs + + P = ParamSpec("P") + R = TypeVar("R") + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T") + + +# Holds data that will be added to **all** events sent by this process. +# In case this is a http server (think web framework) with multiple users +# the data will be added to events of all users. +# Typically this is used for process wide data such as the release. +_global_scope: "Optional[Scope]" = None + +# Holds data for the active request. +# This is used to isolate data for different requests or users. +# The isolation scope is usually created by integrations, but may also +# be created manually +_isolation_scope = ContextVar("isolation_scope", default=None) + +# Holds data for the active span. +# This can be used to manually add additional data to a span. +_current_scope = ContextVar("current_scope", default=None) + +global_event_processors: "List[EventProcessor]" = [] + +# A function returning a (trace_id, span_id) tuple +# from an external tracing source (such as otel) +_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None + + +class ScopeType(Enum): + CURRENT = "current" + ISOLATION = "isolation" + GLOBAL = "global" + MERGED = "merged" + + +class _ScopeManager: + def __init__(self, hub: "Optional[Any]" = None) -> None: + self._old_scopes: "List[Scope]" = [] + + def __enter__(self) -> "Scope": + isolation_scope = Scope.get_isolation_scope() + + self._old_scopes.append(isolation_scope) + + forked_scope = isolation_scope.fork() + _isolation_scope.set(forked_scope) + + return forked_scope + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + old_scope = self._old_scopes.pop() + _isolation_scope.set(old_scope) + + +def add_global_event_processor(processor: "EventProcessor") -> None: + global_event_processors.append(processor) + + +def register_external_propagation_context( + fn: "Callable[[], Optional[Tuple[str, str]]]", +) -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = fn + + +def remove_external_propagation_context() -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = None + + +def get_external_propagation_context() -> "Optional[Tuple[str, str]]": + return ( + _external_propagation_context_fn() if _external_propagation_context_fn else None + ) + + +def has_external_propagation_context() -> bool: + return _external_propagation_context_fn is not None + + +def _attr_setter(fn: "Any") -> "Any": + return property(fset=fn, doc=fn.__doc__) + + +def _disable_capture(fn: "F") -> "F": + @wraps(fn) + def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": + if not self._should_capture: + return + try: + self._should_capture = False + return fn(self, *args, **kwargs) + finally: + self._should_capture = True + + return wrapper # type: ignore + + +class Scope: + """The scope holds extra information that should be sent with all + events that belong to it. + """ + + # NOTE: Even though it should not happen, the scope needs to not crash when + # accessed by multiple threads. It's fine if it's full of races, but those + # races should never make the user application crash. + # + # The same needs to hold for any accesses of the scope the SDK makes. + + __slots__ = ( + "_level", + "_name", + "_fingerprint", + # note that for legacy reasons, _transaction is the transaction *name*, + # not a Transaction object (the object is stored in _span) + "_transaction", + "_transaction_info", + "_user", + "_tags", + "_contexts", + "_extras", + "_breadcrumbs", + "_n_breadcrumbs_truncated", + "_gen_ai_original_message_count", + "_gen_ai_conversation_id", + "_event_processors", + "_error_processors", + "_should_capture", + "_span", + "_session", + "_attachments", + "_force_auto_session_tracking", + "_profile", + "_propagation_context", + "client", + "_type", + "_last_event_id", + "_flags", + "_attributes", + ) + + def __init__( + self, + ty: "Optional[ScopeType]" = None, + client: "Optional[sentry_sdk.Client]" = None, + ) -> None: + self._type = ty + + self._event_processors: "List[EventProcessor]" = [] + self._error_processors: "List[ErrorProcessor]" = [] + + self._name: "Optional[str]" = None + self._propagation_context: "Optional[PropagationContext]" = None + self._n_breadcrumbs_truncated: int = 0 + self._gen_ai_original_message_count: "Dict[str, int]" = {} + + self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() + + if client is not None: + self.set_client(client) + + self.clear() + + incoming_trace_information = self._load_trace_data_from_env() + self.generate_propagation_context(incoming_data=incoming_trace_information) + + def __copy__(self) -> "Scope": + """ + Returns a copy of this scope. + This also creates a copy of all referenced data structures. + """ + rv: "Scope" = object.__new__(self.__class__) + + rv._type = self._type + rv.client = self.client + rv._level = self._level + rv._name = self._name + rv._fingerprint = self._fingerprint + rv._transaction = self._transaction + rv._transaction_info = self._transaction_info.copy() + rv._user = self._user + + rv._tags = self._tags.copy() + rv._contexts = self._contexts.copy() + rv._extras = self._extras.copy() + + rv._breadcrumbs = copy(self._breadcrumbs) + rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated + rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() + rv._event_processors = self._event_processors.copy() + rv._error_processors = self._error_processors.copy() + rv._propagation_context = self._propagation_context + + rv._should_capture = self._should_capture + rv._span = self._span + rv._session = self._session + rv._force_auto_session_tracking = self._force_auto_session_tracking + rv._attachments = self._attachments.copy() + + rv._profile = self._profile + + rv._last_event_id = self._last_event_id + + rv._flags = deepcopy(self._flags) + + rv._attributes = self._attributes.copy() + + rv._gen_ai_conversation_id = self._gen_ai_conversation_id + + return rv + + @classmethod + def get_current_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the current scope. + """ + current_scope = _current_scope.get() + if current_scope is None: + current_scope = Scope(ty=ScopeType.CURRENT) + _current_scope.set(current_scope) + + return current_scope + + @classmethod + def set_current_scope(cls, new_current_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new current scope overwriting the existing current scope. + :param new_current_scope: The scope to set as the new current scope. + """ + _current_scope.set(new_current_scope) + + @classmethod + def get_isolation_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the isolation scope. + """ + isolation_scope = _isolation_scope.get() + if isolation_scope is None: + isolation_scope = Scope(ty=ScopeType.ISOLATION) + _isolation_scope.set(isolation_scope) + + return isolation_scope + + @classmethod + def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new isolation scope overwriting the existing isolation scope. + :param new_isolation_scope: The scope to set as the new isolation scope. + """ + _isolation_scope.set(new_isolation_scope) + + @classmethod + def get_global_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the global scope. + """ + global _global_scope + if _global_scope is None: + _global_scope = Scope(ty=ScopeType.GLOBAL) + + return _global_scope + + def set_global_attributes(self) -> None: + from sentry_sdk.client import SDK_INFO + + self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) + self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) + + self.set_attribute( + "process.runtime.name", + platform.python_implementation(), + ) + self.set_attribute( + "process.runtime.version", + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + ) + + options = sentry_sdk.get_client().options + + server_name = options.get("server_name") + if server_name: + self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) + + environment = options.get("environment") + if environment: + self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) + + release = options.get("release") + if release: + self.set_attribute(SPANDATA.SENTRY_RELEASE, release) + + @classmethod + def last_event_id(cls) -> "Optional[str]": + """ + .. versionadded:: 2.2.0 + + Returns event ID of the event most recently captured by the isolation scope, or None if no event + has been captured. We do not consider events that are dropped, e.g. by a before_send hook. + Transactions also are not considered events in this context. + + The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; + whether the event is sent depends on the transport. The event could be sent later or not at all. + Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or + various other reasons. + """ + return cls.get_isolation_scope()._last_event_id + + def _merge_scopes( + self, + additional_scope: "Optional[Scope]" = None, + additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, + ) -> "Scope": + """ + Merges global, isolation and current scope into a new scope and + adds the given additional scope or additional scope kwargs to it. + """ + if additional_scope and additional_scope_kwargs: + raise TypeError("cannot provide scope and kwargs") + + final_scope = copy(_global_scope) if _global_scope is not None else Scope() + final_scope._type = ScopeType.MERGED + + isolation_scope = _isolation_scope.get() + if isolation_scope is not None: + final_scope.update_from_scope(isolation_scope) + + current_scope = _current_scope.get() + if current_scope is not None: + final_scope.update_from_scope(current_scope) + + if self != current_scope and self != isolation_scope: + final_scope.update_from_scope(self) + + if additional_scope is not None: + if callable(additional_scope): + additional_scope(final_scope) + else: + final_scope.update_from_scope(additional_scope) + + elif additional_scope_kwargs: + final_scope.update_from_kwargs(**additional_scope_kwargs) + + return final_scope + + @classmethod + def get_client(cls) -> "sentry_sdk.client.BaseClient": + """ + .. versionadded:: 2.0.0 + + Returns the currently used :py:class:`sentry_sdk.Client`. + This checks the current scope, the isolation scope and the global scope for a client. + If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. + """ + current_scope = _current_scope.get() + try: + client = current_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + isolation_scope = _isolation_scope.get() + try: + client = isolation_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + try: + client = _global_scope.client # type: ignore + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + return NonRecordingClient() + + def set_client( + self, client: "Optional[sentry_sdk.client.BaseClient]" = None + ) -> None: + """ + .. versionadded:: 2.0.0 + + Sets the client for this scope. + + :param client: The client to use in this scope. + If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. + + """ + if client is not None: + self.client = client + # We need a client to set the initial global attributes on the global + # scope since they mostly come from client options, so populate them + # as soon as a client is set + sentry_sdk.get_global_scope().set_global_attributes() + else: + self.client = NonRecordingClient() + + def fork(self) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns a fork of this scope. + """ + forked_scope = copy(self) + return forked_scope + + def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": + """ + Load Sentry trace id and baggage from environment variables. + Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". + """ + incoming_trace_information: "Optional[Dict[str, str]]" = None + + sentry_use_environment = ( + os.environ.get("SENTRY_USE_ENVIRONMENT") or "" + ).lower() + use_environment = sentry_use_environment not in FALSE_VALUES + if use_environment: + incoming_trace_information = {} + + if os.environ.get("SENTRY_TRACE"): + incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( + os.environ.get("SENTRY_TRACE") or "" + ) + + if os.environ.get("SENTRY_BAGGAGE"): + incoming_trace_information[BAGGAGE_HEADER_NAME] = ( + os.environ.get("SENTRY_BAGGAGE") or "" + ) + + return incoming_trace_information or None + + def set_new_propagation_context(self) -> None: + """ + Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. + """ + self._propagation_context = PropagationContext() + + def generate_propagation_context( + self, incoming_data: "Optional[Dict[str, str]]" = None + ) -> None: + """ + Makes sure the propagation context is set on the scope. + If there is `incoming_data` overwrite existing propagation context. + If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. + """ + if incoming_data is not None: + self._propagation_context = PropagationContext.from_incoming_data( + incoming_data + ) + + # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring + if self._type != ScopeType.CURRENT: + if self._propagation_context is None: + self.set_new_propagation_context() + + def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": + """ + Returns the Dynamic Sampling Context from the Propagation Context. + If not existing, creates a new one. + + Deprecated: Logic moved to PropagationContext, don't use directly. + """ + if self._propagation_context is None: + return None + + return self._propagation_context.dynamic_sampling_context + + def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + """ + Returns the Sentry "sentry-trace" header (aka the traceparent) from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().to_traceparent() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return traceparent from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_traceparent() + elif not span_streaming and self.span is not None: + return self.span._to_traceparent() + + # else return traceparent from the propagation context + return self.get_active_propagation_context().to_traceparent() + + def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": + """ + Returns the Sentry "baggage" header containing trace information from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().get_baggage() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return baggage from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_baggage() + elif not span_streaming and self.span is not None: + return self.span._to_baggage() + + # else return baggage from the propagation context + return self.get_active_propagation_context().get_baggage() + + def get_trace_context(self) -> "Dict[str, Any]": + """ + Returns the Sentry "trace" context from the Propagation Context. + """ + if ( + has_tracing_enabled(self.get_client().options) + and self._span is not None + and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) + ): + return self._span._get_trace_context() + + # if we are tracing externally (otel), those values take precedence + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + trace_id, span_id = external_propagation_context + return {"trace_id": trace_id, "span_id": span_id} + + propagation_context = self.get_active_propagation_context() + + return { + "trace_id": propagation_context.trace_id, + "span_id": propagation_context.span_id, + "parent_span_id": propagation_context.parent_span_id, + "dynamic_sampling_context": propagation_context.dynamic_sampling_context, + } + + def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: + """ + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + span = kwargs.pop("span", None) + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + meta = "" + + for name, content in self.iter_trace_propagation_headers(): + meta += f'' + + return meta + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. + Deprecated: use PropagationContext.iter_headers instead. + """ + if self._propagation_context is not None: + yield from self._propagation_context.iter_headers() + + def iter_trace_propagation_headers( + self, *args: "Any", **kwargs: "Any" + ) -> "Generator[Tuple[str, str], None, None]": + """ + Return HTTP headers which allow propagation of trace data. + + If a span is given, the trace data will taken from the span. + If no span is given, the trace data is taken from the scope. + """ + client = self.get_client() + if not client.options.get("propagate_traces"): + warnings.warn( + "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", + DeprecationWarning, + stacklevel=2, + ) + return + + span = kwargs.pop("span", None) + if not span: + span_streaming = has_span_streaming_enabled(client.options) + span = self.streamed_span if span_streaming else self.span + + if ( + has_tracing_enabled(client.options) + and span is not None + and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) + ): + for header in span._iter_headers(): + yield header + elif has_external_propagation_context(): + # when we have an external_propagation_context (otlp) + # we leave outgoing propagation to the propagator + return + else: + for header in self.get_active_propagation_context().iter_headers(): + yield header + + def get_active_propagation_context(self) -> "PropagationContext": + if self._propagation_context is not None: + return self._propagation_context + + current_scope = self.get_current_scope() + if current_scope._propagation_context is not None: + return current_scope._propagation_context + + isolation_scope = self.get_isolation_scope() + # should actually never happen, but just in case someone calls scope.clear + if isolation_scope._propagation_context is None: + isolation_scope._propagation_context = PropagationContext() + return isolation_scope._propagation_context + + @classmethod + def set_custom_sampling_context( + cls, custom_sampling_context: "dict[str, Any]" + ) -> None: + cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( + custom_sampling_context + ) + + def clear(self) -> None: + """Clears the entire scope.""" + self._level: "Optional[LogLevelStr]" = None + self._fingerprint: "Optional[List[str]]" = None + self._transaction: "Optional[str]" = None + self._transaction_info: "dict[str, str]" = {} + self._user: "Optional[Dict[str, Any]]" = None + + self._tags: "Dict[str, Any]" = {} + self._contexts: "Dict[str, Dict[str, Any]]" = {} + self._extras: "dict[str, Any]" = {} + self._attachments: "List[Attachment]" = [] + + self.clear_breadcrumbs() + self._should_capture: bool = True + + self._span: "Optional[Union[Span, StreamedSpan]]" = None + self._session: "Optional[Session]" = None + self._force_auto_session_tracking: "Optional[bool]" = None + + self._profile: "Optional[Profile]" = None + + self._propagation_context = None + + # self._last_event_id is only applicable to isolation scopes + self._last_event_id: "Optional[str]" = None + self._flags: "Optional[FlagBuffer]" = None + + self._attributes: "Attributes" = {} + + self._gen_ai_conversation_id: "Optional[str]" = None + + @_attr_setter + def level(self, value: "LogLevelStr") -> None: + """ + When set this overrides the level. + + .. deprecated:: 1.0.0 + Use :func:`set_level` instead. + + :param value: The level to set. + """ + logger.warning( + "Deprecated: use .set_level() instead. This will be removed in the future." + ) + + self._level = value + + def set_level(self, value: "LogLevelStr") -> None: + """ + Sets the level for the scope. + + :param value: The level to set. + """ + self._level = value + + @_attr_setter + def fingerprint(self, value: "Optional[List[str]]") -> None: + """When set this overrides the default fingerprint.""" + self._fingerprint = value + + @property + def transaction(self) -> "Any": + # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 + """Return the transaction (root span) in the scope, if any.""" + + # there is no span/transaction on the scope + if self._span is None: + return None + + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + # there is an orphan span on the scope + if self._span.containing_transaction is None: + return None + + # there is either a transaction (which is its own containing + # transaction) or a non-orphan span on the scope + return self._span.containing_transaction + + @transaction.setter + def transaction(self, value: "Any") -> None: + # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 + """When set this forces a specific transaction name to be set. + + Deprecated: use set_transaction_name instead.""" + + # XXX: the docstring above is misleading. The implementation of + # apply_to_event prefers an existing value of event.transaction over + # anything set in the scope. + # XXX: note that with the introduction of the Scope.transaction getter, + # there is a semantic and type mismatch between getter and setter. The + # getter returns a Transaction, the setter sets a transaction name. + # Without breaking version compatibility, we could make the setter set a + # transaction name or transaction (self._span) depending on the type of + # the value argument. + + logger.warning( + "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." + ) + self._transaction = value + if self._span: + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + if self._span.containing_transaction: + self._span.containing_transaction.name = value + + def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: + """Set the transaction name and optionally the transaction source.""" + self._transaction = name + if self._span: + if isinstance(self._span, NoOpStreamedSpan): + return + + elif isinstance(self._span, StreamedSpan): + self._span._segment.name = name + if source: + self._span._segment.set_attribute( + "sentry.span.source", getattr(source, "value", source) + ) + + elif self._span.containing_transaction: + self._span.containing_transaction.name = name + if source: + self._span.containing_transaction.source = source + + if source: + self._transaction_info["source"] = source + + @_attr_setter + def user(self, value: "Optional[Dict[str, Any]]") -> None: + """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" + warnings.warn( + "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", + DeprecationWarning, + stacklevel=2, + ) + self.set_user(value) + + def set_user(self, value: "Optional[Dict[str, Any]]") -> None: + """Sets a user for the scope.""" + self._user = value + + session = self.get_isolation_scope()._session + if session is not None: + session.update(user=value) + + @property + def span(self) -> "Optional[Span]": + """Get/set current tracing span or transaction.""" + return self._span if isinstance(self._span, Span) else None + + @span.setter + def span(self, span: "Optional[Span]") -> None: + self._span = span + # XXX: this differs from the implementation in JS, there Scope.setSpan + # does not set Scope._transactionName. + if isinstance(span, Transaction): + transaction = span + if transaction.name: + self._transaction = transaction.name + if transaction.source: + self._transaction_info["source"] = transaction.source + + @property + def streamed_span(self) -> "Optional[StreamedSpan]": + """Get/set current tracing span.""" + return self._span if isinstance(self._span, StreamedSpan) else None + + @streamed_span.setter + def streamed_span(self, span: "Optional[StreamedSpan]") -> None: + self._span = span + + # Also set _transaction and _transaction_info in streaming mode as this + # is used for populating events and linking them to segments + if type(span) is StreamedSpan and span._is_segment(): + self._transaction = span.name + if span._attributes.get("sentry.span.source"): + self._transaction_info["source"] = str( + span._attributes["sentry.span.source"] + ) + + @property + def profile(self) -> "Optional[Profile]": + return self._profile + + @profile.setter + def profile(self, profile: "Optional[Profile]") -> None: + self._profile = profile + + def set_tag(self, key: str, value: "Any") -> None: + """ + Sets a tag for a key to a specific value. + + :param key: Key of the tag to set. + + :param value: Value of the tag to set. + """ + self._tags[key] = value + + def set_tags(self, tags: "Mapping[str, object]") -> None: + """Sets multiple tags at once. + + This method updates multiple tags at once. The tags are passed as a dictionary + or other mapping type. + + Calling this method is equivalent to calling `set_tag` on each key-value pair + in the mapping. If a tag key already exists in the scope, its value will be + updated. If the tag key does not exist in the scope, the key-value pair will + be added to the scope. + + This method only modifies tag keys in the `tags` mapping passed to the method. + `scope.set_tags({})` is, therefore, a no-op. + + :param tags: A mapping of tag keys to tag values to set. + """ + self._tags.update(tags) + + def remove_tag(self, key: str) -> None: + """ + Removes a specific tag. + + :param key: Key of the tag to remove. + """ + self._tags.pop(key, None) + + def set_context( + self, + key: str, + value: "Dict[str, Any]", + ) -> None: + """ + Binds a context at a certain key to a specific value. + """ + self._contexts[key] = value + + def remove_context( + self, + key: str, + ) -> None: + """Removes a context.""" + self._contexts.pop(key, None) + + def set_extra( + self, + key: str, + value: "Any", + ) -> None: + """Sets an extra key to a specific value.""" + self._extras[key] = value + + def remove_extra( + self, + key: str, + ) -> None: + """Removes a specific extra key.""" + self._extras.pop(key, None) + + def set_conversation_id(self, conversation_id: str) -> None: + """ + Sets the conversation ID for gen_ai spans. + + :param conversation_id: The conversation ID to set. + """ + self._gen_ai_conversation_id = conversation_id + + def get_conversation_id(self) -> "Optional[str]": + """ + Gets the conversation ID for gen_ai spans. + + :returns: The conversation ID, or None if not set. + """ + return self._gen_ai_conversation_id + + def remove_conversation_id(self) -> None: + """Removes the conversation ID.""" + self._gen_ai_conversation_id = None + + def clear_breadcrumbs(self) -> None: + """Clears breadcrumb buffer.""" + self._breadcrumbs: "Deque[Breadcrumb]" = deque() + self._n_breadcrumbs_truncated = 0 + + def add_attachment( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + """Adds an attachment to future events sent from this scope. + + The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. + """ + self._attachments.append( + Attachment( + bytes=bytes, + path=path, + filename=filename, + content_type=content_type, + add_to_transactions=add_to_transactions, + ) + ) + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + client = self.get_client() + + if not client.is_active(): + logger.info("Dropped breadcrumb because no client bound") + return + + before_breadcrumb = client.options.get("before_breadcrumb") + max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) + + crumb = dict(crumb or ()) + crumb.update(kwargs) + if not crumb: + return + + hint = dict(hint or ()) + + if crumb.get("timestamp") is None: + crumb["timestamp"] = datetime.now(timezone.utc) + if crumb.get("type") is None: + crumb["type"] = "default" + + if before_breadcrumb is not None: + new_crumb = before_breadcrumb(crumb, hint) + else: + new_crumb = crumb + + if new_crumb is not None: + self._breadcrumbs.append(new_crumb) + else: + logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) + + while len(self._breadcrumbs) > max_breadcrumbs: + self._breadcrumbs.popleft() + self._n_breadcrumbs_truncated += 1 + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + try_autostart_continuous_profiler() + + custom_sampling_context = custom_sampling_context or {} + + # kwargs at this point has type TransactionKwargs, since we have removed + # the client and custom_sampling_context from it. + transaction_kwargs: "TransactionKwargs" = kwargs + + # if we haven't been given a transaction, make one + if transaction is None: + transaction = Transaction(**transaction_kwargs) + + # use traces_sample_rate, traces_sampler, and/or inheritance to make a + # sampling decision + sampling_context = { + "transaction_context": transaction.to_json(), + "parent_sampled": transaction.parent_sampled, + } + sampling_context.update(custom_sampling_context) + transaction._set_initial_sampling_decision(sampling_context=sampling_context) + + # update the sample rate in the dsc + if transaction.sample_rate is not None: + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction._baggage: + transaction._baggage.sentry_items["sample_rate"] = str( + transaction.sample_rate + ) + + if transaction.sampled: + profile = Profile( + transaction.sampled, transaction._start_timestamp_monotonic_ns + ) + profile._set_initial_sampling_decision(sampling_context=sampling_context) + + transaction._profile = profile + + transaction._continuous_profile = try_profile_lifecycle_trace_start() + + # Typically, the profiler is set when the transaction is created. But when + # using the auto lifecycle, the profiler isn't running when the first + # transaction is started. So make sure we update the profiler id on it. + if transaction._continuous_profile is not None: + transaction.set_profiler_id(get_profiler_id()) + + # we don't bother to keep spans if we already know we're not going to + # send the transaction + max_spans = (client.options["_experiments"].get("max_spans")) or 1000 + transaction.init_span_recorder(maxlen=max_spans) + + return transaction + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + warnings.warn( + "Scope.start_span is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return NoOpSpan() + + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + with new_scope(): + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + # get current span or transaction + span = self.span or self.get_isolation_scope().span + if isinstance(span, StreamedSpan): + # make mypy happy + return NoOpSpan() + + if span is None: + # New spans get the `trace_id` from the scope + if "trace_id" not in kwargs: + propagation_context = self.get_active_propagation_context() + kwargs["trace_id"] = propagation_context.trace_id + + span = Span(**kwargs) + else: + # Children take `trace_id`` from the parent span. + span = span.start_child(**kwargs) + + return span + + def start_streamed_span( + self, + name: str, + attributes: "Optional[Attributes]", + parent_span: "Optional[StreamedSpan]", + active: bool, + ) -> "StreamedSpan": + # TODO: rename to start_span once we drop the old API + if isinstance(parent_span, NoOpStreamedSpan): + # parent_span is only set if the user explicitly set it + logger.debug( + "Ignored parent span provided. Span will be parented to the " + "currently active span instead." + ) + + if parent_span is _DEFAULT_PARENT_SPAN or isinstance( + parent_span, NoOpStreamedSpan + ): + parent_span = self.streamed_span + + # If no eligible parent_span was provided and there is no currently + # active span, this is a segment + if parent_span is None: + propagation_context = self.get_active_propagation_context() + + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + scope=self, + unsampled_reason="ignored", + ) + + sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( + name, + attributes, + self, + ) + + if sample_rate is not None: + self._update_sample_rate(sample_rate) + + if sampled is False: + return NoOpStreamedSpan( + scope=self, + unsampled_reason=outcome, + ) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=None, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + parent_sampled=propagation_context.parent_sampled, + baggage=propagation_context.baggage, + sample_rand=sample_rand, + sample_rate=sample_rate, + ) + + # This is a child span; take propagation context from the parent span + with new_scope(): + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + unsampled_reason="ignored", + ) + + if isinstance(parent_span, NoOpStreamedSpan): + return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=parent_span._segment, + trace_id=parent_span.trace_id, + parent_span_id=parent_span.span_id, + parent_sampled=parent_span.sampled, + ) + + def _update_sample_rate(self, sample_rate: float) -> None: + # If we had to adjust the sample rate when setting the sampling decision + # for a span, it needs to be updated in the propagation context too + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(sample_rate) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", + ) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + self.generate_propagation_context(environ_or_headers) + + # generate_propagation_context ensures that the propagation_context is not None. + propagation_context = cast(PropagationContext, self._propagation_context) + + optional_kwargs = {} + if name: + optional_kwargs["name"] = name + if source: + optional_kwargs["source"] = source + + return Transaction( + op=op, + origin=origin, + baggage=propagation_context.baggage, + parent_sampled=propagation_context.parent_sampled, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + same_process_as_parent=False, + **optional_kwargs, + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures an event. + + Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + scope = self._merge_scopes(scope, scope_kwargs) + + event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) + + if event_id is not None and event.get("type") != "transaction": + self.get_isolation_scope()._last_event_id = event_id + + return event_id + + def _capture_log(self, log: "Optional[Log]") -> None: + if log is None: + return + + client = self.get_client() + if not has_logs_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" + ) + + client._capture_log(log, scope=merged_scope) + + def _capture_metric(self, metric: "Optional[Metric]") -> None: + if metric is None: + return + + client = self.get_client() + if not has_metrics_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" + ) + + client._capture_metric(metric, scope=merged_scope) + + def _capture_span(self, span: "Optional[StreamedSpan]") -> None: + if span is None: + return + + client = self.get_client() + if not has_span_streaming_enabled(client.options): + return + + merged_scope = self._merge_scopes() + client._capture_span(span, scope=merged_scope) + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures a message. + + :param message: The string to send as the message. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if level is None: + level = "info" + + event: "Event" = { + "message": message, + "level": level, + } + + return self.capture_event(event, scope=scope, **scope_kwargs) + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """Captures an exception. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if error is not None: + exc_info = exc_info_from_error(error) + else: + exc_info = sys.exc_info() + + event, hint = event_from_exception( + exc_info, client_options=self.get_client().options + ) + + try: + return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) + except Exception: + capture_internal_exception(sys.exc_info()) + + return None + + def start_session(self, *args: "Any", **kwargs: "Any") -> None: + """Starts a new session.""" + session_mode = kwargs.pop("session_mode", "application") + + self.end_session() + + client = self.get_client() + self._session = Session( + release=client.options.get("release"), + environment=client.options.get("environment"), + user=self._user, + session_mode=session_mode, + ) + + def end_session(self, *args: "Any", **kwargs: "Any") -> None: + """Ends the current session if there is one.""" + session = self._session + self._session = None + + if session is not None: + session.close() + self.get_client().capture_session(session) + + def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: + """Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + self.end_session() + self._force_auto_session_tracking = False + + def resume_auto_session_tracking(self) -> None: + """Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + self._force_auto_session_tracking = None + + def add_event_processor( + self, + func: "EventProcessor", + ) -> None: + """Register a scope local event processor on the scope. + + :param func: This function behaves like `before_send.` + """ + if len(self._event_processors) > 20: + logger.warning( + "Too many event processors on scope! Clearing list to free up some memory: %r", + self._event_processors, + ) + del self._event_processors[:] + + self._event_processors.append(func) + + def add_error_processor( + self, + func: "ErrorProcessor", + cls: "Optional[Type[BaseException]]" = None, + ) -> None: + """Register a scope local error processor on the scope. + + :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. + + :param cls: Optionally, only process exceptions of this type. + """ + if cls is not None: + cls_ = cls # For mypy. + real_func = func + + def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": + try: + is_inst = isinstance(exc_info[1], cls_) + except Exception: + is_inst = False + if is_inst: + return real_func(event, exc_info) + return event + + self._error_processors.append(func) + + def _apply_level_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._level is not None: + event["level"] = self._level + + def _apply_breadcrumbs_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + event.setdefault("breadcrumbs", {}) + + # This check is just for mypy - + if not isinstance(event["breadcrumbs"], AnnotatedValue): + event["breadcrumbs"].setdefault("values", []) + event["breadcrumbs"]["values"].extend(self._breadcrumbs) + + # Attempt to sort timestamps + try: + if not isinstance(event["breadcrumbs"], AnnotatedValue): + for crumb in event["breadcrumbs"]["values"]: + if isinstance(crumb["timestamp"], str): + crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) + + event["breadcrumbs"]["values"].sort( + key=lambda crumb: crumb["timestamp"] + ) + except Exception as err: + logger.debug("Error when sorting breadcrumbs", exc_info=err) + pass + + def _apply_user_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("user") is None and self._user is not None: + event["user"] = self._user + + def _apply_transaction_name_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction") is None and self._transaction is not None: + event["transaction"] = self._transaction + + def _apply_transaction_info_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction_info") is None and self._transaction_info is not None: + event["transaction_info"] = self._transaction_info + + def _apply_fingerprint_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("fingerprint") is None and self._fingerprint is not None: + event["fingerprint"] = self._fingerprint + + def _apply_extra_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._extras: + event.setdefault("extra", {}).update(self._extras) + + def _apply_tags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._tags: + event.setdefault("tags", {}).update(self._tags) + + def _apply_contexts_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._contexts: + event.setdefault("contexts", {}).update(self._contexts) + + contexts = event.setdefault("contexts", {}) + + # Add "trace" context + if contexts.get("trace") is None: + contexts["trace"] = self.get_trace_context() + + def _apply_flags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + flags = self.flags.get() + if len(flags) > 0: + event.setdefault("contexts", {}).setdefault("flags", {}).update( + {"values": flags} + ) + + def _apply_scope_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + # TODO: turn Logs, Metrics into actual classes + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + for attribute, value in self._attributes.items(): + if attribute not in attributes: + attributes[attribute] = value + + def _apply_user_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + if not should_send_default_pii() or self._user is None: + return + + for attribute_name, user_attribute in ( + ("user.id", "id"), + ("user.name", "username"), + ("user.email", "email"), + ("user.ip_address", "ip_address"), + ): + if ( + user_attribute in self._user + and attribute_name not in attributes + and self._user[user_attribute] is not None + ): + attributes[attribute_name] = self._user[user_attribute] + + def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": + logger.info("%s (%s) dropped event", ty, cause) + return None + + def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the error processors on the event and returns the modified event. + """ + exc_info = hint.get("exc_info") + if exc_info is not None: + error_processors = chain( + self.get_global_scope()._error_processors, + self.get_isolation_scope()._error_processors, + self.get_current_scope()._error_processors, + ) + + for error_processor in error_processors: + new_event = error_processor(event, exc_info) + if new_event is None: + return self._drop(error_processor, "error processor") + + event = new_event + + return event + + def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the event processors on the event and returns the modified event. + """ + ty = event.get("type") + is_check_in = ty == "check_in" + + if not is_check_in: + # Get scopes without creating them to prevent infinite recursion + isolation_scope = _isolation_scope.get() + current_scope = _current_scope.get() + + event_processors = chain( + global_event_processors, + _global_scope and _global_scope._event_processors or [], + isolation_scope and isolation_scope._event_processors or [], + current_scope and current_scope._event_processors or [], + ) + + for event_processor in event_processors: + new_event = event + with capture_internal_exceptions(): + new_event = event_processor(event, hint) + if new_event is None: + return self._drop(event_processor, "event processor") + event = new_event + + return event + + @_disable_capture + def apply_to_event( + self, + event: "Event", + hint: "Hint", + options: "Optional[Dict[str, Any]]" = None, + ) -> "Optional[Event]": + """Applies the information contained on the scope to the given event.""" + ty = event.get("type") + is_transaction = ty == "transaction" + is_check_in = ty == "check_in" + + # put all attachments into the hint. This lets callbacks play around + # with attachments. We also later pull this out of the hint when we + # create the envelope. + attachments_to_send = hint.get("attachments") or [] + for attachment in self._attachments: + if not is_transaction or attachment.add_to_transactions: + attachments_to_send.append(attachment) + hint["attachments"] = attachments_to_send + + self._apply_contexts_to_event(event, hint, options) + + if is_check_in: + # Check-ins only support the trace context, strip all others + event["contexts"] = { + "trace": event.setdefault("contexts", {}).get("trace", {}) + } + + if not is_check_in: + self._apply_level_to_event(event, hint, options) + self._apply_fingerprint_to_event(event, hint, options) + self._apply_user_to_event(event, hint, options) + self._apply_transaction_name_to_event(event, hint, options) + self._apply_transaction_info_to_event(event, hint, options) + self._apply_tags_to_event(event, hint, options) + self._apply_extra_to_event(event, hint, options) + + if not is_transaction and not is_check_in: + self._apply_breadcrumbs_to_event(event, hint, options) + self._apply_flags_to_event(event, hint, options) + + event = self.run_error_processors(event, hint) + if event is None: + return None + + event = self.run_event_processors(event, hint) + if event is None: + return None + + return event + + @_disable_capture + def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: + # Attributes-based events and telemetry go through here (logs, metrics, + # spansV2) + if not isinstance(telemetry, StreamedSpan): + trace_context = self.get_trace_context() + trace_id = trace_context.get("trace_id") + if telemetry.get("trace_id") is None and trace_id is not None: + telemetry["trace_id"] = trace_id + + # span_id should only be populated if there's an active span. We can't + # use the trace_context here because it synthesizes a span_id if there + # isn't one + if telemetry.get("span_id") is None: + if self._span is not None and not isinstance( + self._span, (NoOpStreamedSpan, NoOpSpan) + ): + telemetry["span_id"] = self._span.span_id + else: + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + _, span_id = external_propagation_context + if span_id is not None: + telemetry["span_id"] = span_id + + self._apply_scope_attributes_to_telemetry(telemetry) + self._apply_user_attributes_to_telemetry(telemetry) + + def update_from_scope(self, scope: "Scope") -> None: + """Update the scope with another scope's data.""" + if scope._level is not None: + self._level = scope._level + if scope._fingerprint is not None: + self._fingerprint = scope._fingerprint + if scope._transaction is not None: + self._transaction = scope._transaction + if scope._transaction_info is not None: + self._transaction_info.update(scope._transaction_info) + if scope._user is not None: + self._user = scope._user + if scope._tags: + self._tags.update(scope._tags) + if scope._contexts: + self._contexts.update(scope._contexts) + if scope._extras: + self._extras.update(scope._extras) + if scope._breadcrumbs: + self._breadcrumbs.extend(scope._breadcrumbs) + if scope._n_breadcrumbs_truncated: + self._n_breadcrumbs_truncated = ( + self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated + ) + if scope._gen_ai_original_message_count: + self._gen_ai_original_message_count.update( + scope._gen_ai_original_message_count + ) + if scope._gen_ai_conversation_id: + self._gen_ai_conversation_id = scope._gen_ai_conversation_id + if scope._span: + self._span = scope._span + if scope._attachments: + self._attachments.extend(scope._attachments) + if scope._profile: + self._profile = scope._profile + if scope._propagation_context: + self._propagation_context = scope._propagation_context + if scope._session: + self._session = scope._session + if scope._flags: + if not self._flags: + self._flags = deepcopy(scope._flags) + else: + for flag in scope._flags.get(): + self._flags.set(flag["flag"], flag["result"]) + if scope._attributes: + self._attributes.update(scope._attributes) + + def update_from_kwargs( + self, + user: "Optional[Any]" = None, + level: "Optional[LogLevelStr]" = None, + extras: "Optional[Dict[str, Any]]" = None, + contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, + tags: "Optional[Dict[str, str]]" = None, + fingerprint: "Optional[List[str]]" = None, + attributes: "Optional[Attributes]" = None, + ) -> None: + """Update the scope's attributes.""" + if level is not None: + self._level = level + if user is not None: + self._user = user + if extras is not None: + self._extras.update(extras) + if contexts is not None: + self._contexts.update(contexts) + if tags is not None: + self._tags.update(tags) + if fingerprint is not None: + self._fingerprint = fingerprint + if attributes is not None: + self._attributes.update(attributes) + + def __repr__(self) -> str: + return "<%s id=%s name=%s type=%s>" % ( + self.__class__.__name__, + hex(id(self)), + self._name, + self._type, + ) + + @property + def flags(self) -> "FlagBuffer": + if self._flags is None: + max_flags = ( + self.get_client().options["_experiments"].get("max_flags") + or DEFAULT_FLAG_CAPACITY + ) + self._flags = FlagBuffer(capacity=max_flags) + return self._flags + + def set_attribute(self, attribute: str, value: "AttributeValue") -> None: + """ + Set an attribute on the scope. + + Any attributes-based telemetry (logs, metrics) captured while this scope + is active will inherit attributes set on the scope. + """ + self._attributes[attribute] = format_attribute(value) + + def remove_attribute(self, attribute: str) -> None: + """Remove an attribute if set on the scope. No-op if there is no such attribute.""" + try: + del self._attributes[attribute] + except KeyError: + pass + + +@contextmanager +def new_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current scope and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.new_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + new_scope = current_scope.fork() + token = _current_scope.set(new_scope) + + try: + yield new_scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `scope` and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + Suppose the variable `scope` contains a `Scope` object, which is not currently + the active scope. + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.use_scope(scope): + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # set given scope as current scope + token = _current_scope.set(scope) + + try: + yield scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def isolation_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current isolation scope and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # fork isolation scope + isolation_scope = Scope.get_isolation_scope() + new_isolation_scope = isolation_scope.fork() + isolation_token = _isolation_scope.set(new_isolation_scope) + + try: + yield new_isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `isolation_scope` and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # set given scope as isolation scope + isolation_token = _isolation_scope.set(isolation_scope) + + try: + yield isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +def should_send_default_pii() -> bool: + """Shortcut for `Scope.get_client().should_send_default_pii()`.""" + return Scope.get_client().should_send_default_pii() + + +# Circular imports +from sentry_sdk.client import NonRecordingClient + +if TYPE_CHECKING: + import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py new file mode 100644 index 0000000000..6794491325 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING, Dict, List, cast + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + iter_event_frames, +) + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event + + +DEFAULT_DENYLIST = [ + # stolen from relay + "password", + "passwd", + "secret", + "api_key", + "apikey", + "auth", + "credentials", + "mysql_pwd", + "privatekey", + "private_key", + "token", + "session", + # django + "csrftoken", + "sessionid", + # wsgi + "x_csrftoken", + "x_forwarded_for", + "set_cookie", + "cookie", + "authorization", + "proxy-authorization", + "x_api_key", + # other common names used in the wild + "aiohttp_session", # aiohttp + "connect.sid", # Express + "csrf_token", # Pyramid + "csrf", # (this is a cookie name used in accepted answers on stack overflow) + "_csrf", # Express + "_csrf_token", # Bottle + "PHPSESSID", # PHP + "_session", # Sanic + "symfony", # Symfony + "user_session", # Vue + "_xsrf", # Tornado + "XSRF-TOKEN", # Angular, Laravel +] + +DEFAULT_PII_DENYLIST = [ + "x_forwarded_for", + "x_real_ip", + "ip_address", + "remote_addr", +] + + +class EventScrubber: + def __init__( + self, + denylist: "Optional[List[str]]" = None, + recursive: bool = False, + send_default_pii: bool = False, + pii_denylist: "Optional[List[str]]" = None, + ) -> None: + """ + A scrubber that goes through the event payload and removes sensitive data configured through denylists. + + :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. + :param recursive: Whether to scrub the event payload recursively, default False. + :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. + :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. + """ + self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist + + if not send_default_pii: + pii_denylist = ( + DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist + ) + self.denylist += pii_denylist + + self.denylist = [x.lower() for x in self.denylist] + self.recursive = recursive + + def scrub_list(self, lst: object) -> None: + """ + If a list is passed to this method, the method recursively searches the list and any + nested lists for any dictionaries. The method calls scrub_dict on all dictionaries + it finds. + If the parameter passed to this method is not a list, the method does nothing. + """ + if not isinstance(lst, list): + return + + for v in lst: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_dict(self, d: object) -> None: + """ + If a dictionary is passed to this method, the method scrubs the dictionary of any + sensitive data. The method calls itself recursively on any nested dictionaries ( + including dictionaries nested in lists) if self.recursive is True. + This method does nothing if the parameter passed to it is not a dictionary. + """ + if not isinstance(d, dict): + return + + for k, v in d.items(): + # The cast is needed because mypy is not smart enough to figure out that k must be a + # string after the isinstance check. + if isinstance(k, str) and k.lower() in self.denylist: + d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() + elif self.recursive: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_request(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "request" in event: + if "headers" in event["request"]: + self.scrub_dict(event["request"]["headers"]) + if "cookies" in event["request"]: + self.scrub_dict(event["request"]["cookies"]) + if "data" in event["request"]: + self.scrub_dict(event["request"]["data"]) + + def scrub_extra(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "extra" in event: + self.scrub_dict(event["extra"]) + + def scrub_user(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "user" in event: + user = event["user"] + if "ip_address" in self.denylist and isinstance(user, dict): + user.pop("ip_address", None) + self.scrub_dict(user) + + def scrub_breadcrumbs(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "breadcrumbs" in event: + if ( + not isinstance(event["breadcrumbs"], AnnotatedValue) + and "values" in event["breadcrumbs"] + ): + for value in event["breadcrumbs"]["values"]: + if "data" in value: + self.scrub_dict(value["data"]) + + def scrub_frames(self, event: "Event") -> None: + with capture_internal_exceptions(): + for frame in iter_event_frames(event): + if "vars" in frame: + self.scrub_dict(frame["vars"]) + + def scrub_spans(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "spans" in event: + for span in cast(List[Dict[str, object]], event["spans"]): + if "data" in span: + self.scrub_dict(span["data"]) + + def scrub_event(self, event: "Event") -> None: + self.scrub_request(event) + self.scrub_extra(event) + self.scrub_user(event) + self.scrub_breadcrumbs(event) + self.scrub_frames(event) + self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py new file mode 100644 index 0000000000..6bf6f6e70b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py @@ -0,0 +1,418 @@ +import math +import sys +from array import array +from collections.abc import Mapping +from datetime import datetime +from typing import TYPE_CHECKING + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exception, + disable_capture_event, + format_timestamp, + safe_repr, + strip_string, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union + + from sentry_sdk._types import NotImplementedType + + Span = Dict[str, Any] + + ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] + Segment = Union[str, int] + + +# Bytes are technically not strings in Python 3, but we can serialize them +serializable_str_types = (str, bytes, bytearray, memoryview) + + +# Maximum length of JSON-serialized event payloads that can be safely sent +# before the server may reject the event due to its size. This is not intended +# to reflect actual values defined server-side, but rather only be an upper +# bound for events sent by the SDK. +# +# Can be overwritten if wanting to send more bytes, e.g. with a custom server. +# When changing this, keep in mind that events may be a little bit larger than +# this value due to attached metadata, so keep the number conservative. +MAX_EVENT_BYTES = 10**6 + +# Maximum depth and breadth of databags. Excess data will be trimmed. If +# max_request_body_size is "always", request bodies won't be trimmed. +MAX_DATABAG_DEPTH = 5 +MAX_DATABAG_BREADTH = 10 +CYCLE_MARKER = "" + + +global_repr_processors: "List[ReprProcessor]" = [] + + +def add_global_repr_processor(processor: "ReprProcessor") -> None: + global_repr_processors.append(processor) + + +sequence_types: "List[type]" = [tuple, list, set, frozenset, array] + + +def add_repr_sequence_type(ty: type) -> None: + sequence_types.append(ty) + + +class Memo: + __slots__ = ("_ids", "_objs") + + def __init__(self) -> None: + self._ids: "Dict[int, Any]" = {} + self._objs: "List[Any]" = [] + + def memoize(self, obj: "Any") -> "ContextManager[bool]": + self._objs.append(obj) + return self + + def __enter__(self) -> bool: + obj = self._objs[-1] + if id(obj) in self._ids: + return True + else: + self._ids[id(obj)] = obj + return False + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> None: + self._ids.pop(id(self._objs.pop()), None) + + +class _Serializer: + """Holds the state of a single serialize() call.""" + + __slots__ = ( + "memo", + "path", + "meta_stack", + "keep_request_bodies", + "max_value_length", + "is_vars", + "custom_repr", + ) + + def __init__( + self, + keep_request_bodies: bool, + max_value_length: "Optional[int]", + is_vars: bool, + custom_repr: "Optional[Callable[..., Optional[str]]]", + ) -> None: + self.memo = Memo() + self.path: "List[Segment]" = [] + self.meta_stack: "List[Dict[str, Any]]" = [] + self.keep_request_bodies = keep_request_bodies + self.max_value_length = max_value_length + self.is_vars = is_vars + self.custom_repr = custom_repr + + def _safe_repr_wrapper(self, value: "Any") -> str: + try: + repr_value = None + if self.custom_repr is not None: + repr_value = self.custom_repr(value) + return repr_value or safe_repr(value) + except Exception: + return safe_repr(value) + + def _annotate(self, **meta: "Any") -> None: + while len(self.meta_stack) <= len(self.path): + try: + segment = self.path[len(self.meta_stack) - 1] + node = self.meta_stack[-1].setdefault(str(segment), {}) + except IndexError: + node = {} + + self.meta_stack.append(node) + + self.meta_stack[-1].setdefault("", {}).update(meta) + + def _is_databag(self) -> "Optional[bool]": + """ + A databag is any value that we need to trim. + True for stuff like vars, request bodies, breadcrumbs and extra. + + :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". + """ + try: + if self.is_vars: + return True + + is_request_body = self._is_request_body() + if is_request_body in (True, None): + return is_request_body + + p0 = self.path[0] + if p0 == "breadcrumbs" and self.path[1] == "values": + self.path[2] + return True + + if p0 == "extra": + return True + + except IndexError: + return None + + return False + + def _is_span_attribute(self) -> "Optional[bool]": + try: + if self.path[0] == "spans" and self.path[2] == "data": + return True + except IndexError: + return None + + return False + + def _is_request_body(self) -> "Optional[bool]": + try: + if self.path[0] == "request" and self.path[1] == "data": + return True + except IndexError: + return None + + return False + + def _serialize_node( + self, + obj: "Any", + is_databag: "Optional[bool]" = None, + is_request_body: "Optional[bool]" = None, + should_repr_strings: "Optional[bool]" = None, + segment: "Optional[Segment]" = None, + remaining_breadth: "Optional[Union[int, float]]" = None, + remaining_depth: "Optional[Union[int, float]]" = None, + ) -> "Any": + if segment is not None: + self.path.append(segment) + + try: + with self.memo.memoize(obj) as result: + if result: + return CYCLE_MARKER + + return self._serialize_node_impl( + obj, + is_databag=is_databag, + is_request_body=is_request_body, + should_repr_strings=should_repr_strings, + remaining_depth=remaining_depth, + remaining_breadth=remaining_breadth, + ) + except BaseException: + capture_internal_exception(sys.exc_info()) + + if is_databag: + return "" + + return None + finally: + if segment is not None: + self.path.pop() + del self.meta_stack[len(self.path) + 1 :] + + def _flatten_annotated(self, obj: "Any") -> "Any": + if isinstance(obj, AnnotatedValue): + self._annotate(**obj.metadata) + obj = obj.value + return obj + + def _serialize_node_impl( + self, + obj: "Any", + is_databag: "Optional[bool]", + is_request_body: "Optional[bool]", + should_repr_strings: "Optional[bool]", + remaining_depth: "Optional[Union[float, int]]", + remaining_breadth: "Optional[Union[float, int]]", + ) -> "Any": + if isinstance(obj, AnnotatedValue): + should_repr_strings = False + if should_repr_strings is None: + should_repr_strings = self.is_vars + + if is_databag is None: + is_databag = self._is_databag() + + if is_request_body is None: + is_request_body = self._is_request_body() + + if is_databag: + if is_request_body and self.keep_request_bodies: + remaining_depth = float("inf") + remaining_breadth = float("inf") + else: + if remaining_depth is None: + remaining_depth = MAX_DATABAG_DEPTH + if remaining_breadth is None: + remaining_breadth = MAX_DATABAG_BREADTH + + obj = self._flatten_annotated(obj) + + if remaining_depth is not None and remaining_depth <= 0: + self._annotate(rem=[["!limit", "x"]]) + if is_databag: + return self._flatten_annotated( + strip_string( + self._safe_repr_wrapper(obj), max_length=self.max_value_length + ) + ) + return None + + is_span_attribute = self._is_span_attribute() + if (is_databag or is_span_attribute) and global_repr_processors: + hints = {"memo": self.memo, "remaining_depth": remaining_depth} + for processor in global_repr_processors: + result = processor(obj, hints) + if result is not NotImplemented: + return self._flatten_annotated(result) + + sentry_repr = getattr(type(obj), "__sentry_repr__", None) + + if obj is None or isinstance(obj, (bool, int, float)): + if should_repr_strings or ( + isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) + ): + return self._safe_repr_wrapper(obj) + else: + return obj + + elif callable(sentry_repr): + return sentry_repr(obj) + + elif isinstance(obj, datetime): + return ( + str(format_timestamp(obj)) + if not should_repr_strings + else self._safe_repr_wrapper(obj) + ) + + elif isinstance(obj, Mapping): + # Create temporary copy here to avoid calling too much code that + # might mutate our dictionary while we're still iterating over it. + obj = dict(obj.items()) + + rv_dict: "Dict[str, Any]" = {} + i = 0 + + for k, v in obj.items(): + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) + break + + str_k = str(k) + v = self._serialize_node( + v, + segment=str_k, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + rv_dict[str_k] = v + i += 1 + + return rv_dict + + elif not isinstance(obj, serializable_str_types) and isinstance( + obj, tuple(sequence_types) + ): + rv_list = [] + + for i, v in enumerate(obj): # type: ignore[arg-type] + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) # type: ignore[arg-type] + break + + rv_list.append( + self._serialize_node( + v, + segment=i, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + ) + + return rv_list + + if should_repr_strings: + obj = self._safe_repr_wrapper(obj) + else: + if isinstance(obj, bytes) or isinstance(obj, bytearray): + obj = obj.decode("utf-8", "replace") + + if not isinstance(obj, str): + obj = self._safe_repr_wrapper(obj) + + is_span_description = ( + len(self.path) == 3 + and self.path[0] == "spans" + and self.path[-1] == "description" + ) + if is_span_description: + return obj + + return self._flatten_annotated( + strip_string(obj, max_length=self.max_value_length) + ) + + +def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": + """ + A very smart serializer that takes a dict and emits a json-friendly dict. + Currently used for serializing the final Event and also prematurely while fetching the stack + local variables for each frame in a stacktrace. + + It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. + The algorithm itself is a recursive graph walk down the data structures it encounters. + + It has the following responsibilities: + * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. + * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. + * Annotating the payload with the _meta field whenever trimming happens. + + :param max_request_body_size: If set to "always", will never trim request bodies. + :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. + :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. + :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. + + """ + serializer = _Serializer( + keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", + max_value_length=kwargs.pop("max_value_length", None), + is_vars=kwargs.pop("is_vars", False), + custom_repr=kwargs.pop("custom_repr", None), + ) + + disable_capture_event.set(True) + try: + serialized_event = serializer._serialize_node(event, **kwargs) + if ( + not serializer.is_vars + and serializer.meta_stack + and isinstance(serialized_event, dict) + ): + serialized_event["_meta"] = serializer.meta_stack[0] + + return serialized_event + finally: + disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py new file mode 100644 index 0000000000..3ffd071bbc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py @@ -0,0 +1,165 @@ +import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from sentry_sdk._types import SessionStatus + + +def _minute_trunc(ts: "datetime") -> "datetime": + return ts.replace(second=0, microsecond=0) + + +def _make_uuid( + val: "Union[str, uuid.UUID]", +) -> "uuid.UUID": + if isinstance(val, uuid.UUID): + return val + return uuid.UUID(val) + + +class Session: + def __init__( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + session_mode: str = "application", + ) -> None: + if sid is None: + sid = uuid.uuid4() + if started is None: + started = datetime.now(timezone.utc) + if status is None: + status = "ok" + self.status = status + self.did: "Optional[str]" = None + self.started = started + self.release: "Optional[str]" = None + self.environment: "Optional[str]" = None + self.duration: "Optional[float]" = None + self.user_agent: "Optional[str]" = None + self.ip_address: "Optional[str]" = None + self.session_mode: str = session_mode + self.errors = 0 + + self.update( + sid=sid, + did=did, + timestamp=timestamp, + duration=duration, + release=release, + environment=environment, + user_agent=user_agent, + ip_address=ip_address, + errors=errors, + user=user, + ) + + @property + def truncated_started(self) -> "datetime": + return _minute_trunc(self.started) + + def update( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + ) -> None: + # If a user is supplied we pull some data form it + if user: + if ip_address is None: + ip_address = user.get("ip_address") + if did is None: + did = user.get("id") or user.get("email") or user.get("username") + + if sid is not None: + self.sid = _make_uuid(sid) + if did is not None: + self.did = str(did) + if timestamp is None: + timestamp = datetime.now(timezone.utc) + self.timestamp = timestamp + if started is not None: + self.started = started + if duration is not None: + self.duration = duration + if release is not None: + self.release = release + if environment is not None: + self.environment = environment + if ip_address is not None: + self.ip_address = ip_address + if user_agent is not None: + self.user_agent = user_agent + if errors is not None: + self.errors = errors + + if status is not None: + self.status = status + + def close( + self, + status: "Optional[SessionStatus]" = None, + ) -> "Any": + if status is None and self.status == "ok": + status = "exited" + if status is not None: + self.update(status=status) + + def get_json_attrs( + self, + with_user_info: "Optional[bool]" = True, + ) -> "Any": + attrs = {} + if self.release is not None: + attrs["release"] = self.release + if self.environment is not None: + attrs["environment"] = self.environment + if with_user_info: + if self.ip_address is not None: + attrs["ip_address"] = self.ip_address + if self.user_agent is not None: + attrs["user_agent"] = self.user_agent + return attrs + + def to_json(self) -> "Any": + rv: "Dict[str, Any]" = { + "sid": str(self.sid), + "init": True, + "started": format_timestamp(self.started), + "timestamp": format_timestamp(self.timestamp), + "status": self.status, + } + if self.errors: + rv["errors"] = self.errors + if self.did is not None: + rv["did"] = self.did + if self.duration is not None: + rv["duration"] = self.duration + attrs = self.get_json_attrs() + if attrs: + rv["attrs"] = attrs + return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py new file mode 100644 index 0000000000..aabf874fcd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py @@ -0,0 +1,262 @@ +import os +import warnings +from contextlib import contextmanager +from threading import Event, Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.envelope import Envelope +from sentry_sdk.session import Session +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Generator, List, Optional, Union + + +def is_auto_session_tracking_enabled( + hub: "Optional[sentry_sdk.Hub]" = None, +) -> "Union[Any, bool, None]": + """DEPRECATED: Utility function to find out if session tracking is enabled.""" + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + + should_track = hub.scope._force_auto_session_tracking + + if should_track is None: + client_options = hub.client.options if hub.client else {} + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking( + hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: Use track_session instead + Starts and stops a session automatically around a block. + """ + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "Use track_session instead.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + should_track = is_auto_session_tracking_enabled(hub) + if should_track: + hub.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + hub.end_session() + + +def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: + """ + DEPRECATED: Utility function to find out if session tracking is enabled. + """ + + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + return _is_auto_session_tracking_enabled(scope) + + +def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: + """ + Utility function to find out if session tracking is enabled. + """ + + should_track = scope._force_auto_session_tracking + if should_track is None: + client_options = sentry_sdk.get_client().options + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking_scope( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: This function is a deprecated alias for track_session. + Starts and stops a session automatically around a block. + """ + + warnings.warn( + "This function is a deprecated alias for track_session and will be removed in the next major release.", + DeprecationWarning, + stacklevel=2, + ) + + with track_session(scope, session_mode=session_mode): + yield + + +@contextmanager +def track_session( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """ + Start a new session in the provided scope, assuming session tracking is enabled. + This is a no-op context manager if session tracking is not enabled. + """ + + should_track = _is_auto_session_tracking_enabled(scope) + if should_track: + scope.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + scope.end_session() + + +TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") +MAX_ENVELOPE_ITEMS = 100 + + +def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": + return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} + + +class SessionFlusher: + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + flush_interval: int = 60, + ) -> None: + self.capture_func = capture_func + self.flush_interval = flush_interval + self.pending_sessions: "List[Any]" = [] + self.pending_aggregates: "Dict[Any, Any]" = {} + self._thread: "Optional[Thread]" = None + self._thread_lock = Lock() + self._aggregate_lock = Lock() + self._thread_for_pid: "Optional[int]" = None + self.__shutdown_requested = Event() + + def flush(self) -> None: + pending_sessions = self.pending_sessions + self.pending_sessions = [] + + with self._aggregate_lock: + pending_aggregates = self.pending_aggregates + self.pending_aggregates = {} + + envelope = Envelope() + for session in pending_sessions: + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_session(session) + + for attrs, states in pending_aggregates.items(): + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_sessions(make_aggregate_envelope(states, attrs)) + + if len(envelope.items) > 0: + self.capture_func(envelope) + + def _ensure_running(self) -> None: + """ + Check that we have an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + running = True + while running: + running = not self.__shutdown_requested.wait(self.flush_interval) + self.flush() + + thread = Thread(target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.__shutdown_requested.set() + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def add_aggregate_session( + self, + session: "Session", + ) -> None: + # NOTE on `session.did`: + # the protocol can deal with buckets that have a distinct-id, however + # in practice we expect the python SDK to have an extremely high cardinality + # here, effectively making aggregation useless, therefore we do not + # aggregate per-did. + + # For this part we can get away with using the global interpreter lock + with self._aggregate_lock: + attrs = session.get_json_attrs(with_user_info=False) + primary_key = tuple(sorted(attrs.items())) + secondary_key = session.truncated_started # (, session.did) + states = self.pending_aggregates.setdefault(primary_key, {}) + state = states.setdefault(secondary_key, {}) + + if "started" not in state: + state["started"] = format_timestamp(session.truncated_started) + # if session.did is not None: + # state["did"] = session.did + if session.status == "crashed": + state["crashed"] = state.get("crashed", 0) + 1 + elif session.status == "abnormal": + state["abnormal"] = state.get("abnormal", 0) + 1 + elif session.errors > 0: + state["errored"] = state.get("errored", 0) + 1 + else: + state["exited"] = state.get("exited", 0) + 1 + + def add_session( + self, + session: "Session", + ) -> None: + if session.session_mode == "request": + self.add_aggregate_session(session) + else: + self.pending_sessions.append(session.to_json()) + self._ensure_running() + + def kill(self) -> None: + self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py new file mode 100644 index 0000000000..2dcc86bc47 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py @@ -0,0 +1,327 @@ +import io +import logging +import os +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from itertools import chain, product +from typing import TYPE_CHECKING + +import urllib3 + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Self + +from sentry_sdk.envelope import Envelope +from sentry_sdk.utils import ( + capture_internal_exceptions, + env_to_bool, +) +from sentry_sdk.utils import ( + logger as sentry_logger, +) + +logger = logging.getLogger("spotlight") + + +DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" +DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" + + +class SpotlightClient: + """ + A client for sending envelopes to Sentry Spotlight. + + Implements exponential backoff retry logic per the SDK spec: + - Logs error at least once when server is unreachable + - Does not log for every failed envelope + - Uses exponential backoff to avoid hammering an unavailable server + - Never blocks normal Sentry operation + """ + + # Exponential backoff settings + INITIAL_RETRY_DELAY = 1.0 # Start with 1 second + MAX_RETRY_DELAY = 60.0 # Max 60 seconds + + def __init__(self, url: str) -> None: + self.url = url + self.http = urllib3.PoolManager() + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time: float = 0.0 + + def capture_envelope(self, envelope: "Envelope") -> None: + # Check if we're in backoff period - skip sending to avoid blocking + if self._last_error_time > 0: + time_since_error = time.time() - self._last_error_time + if time_since_error < self._retry_delay: + # Still in backoff period, skip this envelope + return + + body = io.BytesIO() + envelope.serialize_into(body) + try: + req = self.http.request( + url=self.url, + body=body.getvalue(), + method="POST", + headers={ + "Content-Type": "application/x-sentry-envelope", + }, + ) + req.close() + # Success - reset backoff state + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time = 0.0 + except Exception as e: + self._last_error_time = time.time() + + # Increase backoff delay exponentially first, so logged value matches actual wait + self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) + + # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) + sentry_logger.warning( + "Failed to send envelope to Spotlight at %s: %s. " + "Will retry after %.1f seconds.", + self.url, + e, + self._retry_delay, + ) + + +try: + from django.conf import settings + from django.http import HttpRequest, HttpResponse, HttpResponseServerError + from django.utils.deprecation import MiddlewareMixin + + SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" + SPOTLIGHT_JS_SNIPPET_PATTERN = ( + "\n" + '\n' + ) + SPOTLIGHT_ERROR_PAGE_SNIPPET = ( + '\n' + '\n' + ) + CHARSET_PREFIX = "charset=" + BODY_TAG_NAME = "body" + BODY_CLOSE_TAG_POSSIBILITIES = tuple( + "".format("".join(chars)) + for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) + ) + + class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] + _spotlight_script: "Optional[str]" = None + _spotlight_url: "Optional[str]" = None + + def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: + super().__init__(get_response) + + import sentry_sdk.api + + self.sentry_sdk = sentry_sdk.api + + spotlight_client = self.sentry_sdk.get_client().spotlight + if spotlight_client is None: + sentry_logger.warning( + "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." + ) + return None + # Spotlight URL has a trailing `/stream` part at the end so split it off + self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") + + @property + def spotlight_script(self: "Self") -> "Optional[str]": + if self._spotlight_url is not None and self._spotlight_script is None: + try: + spotlight_js_url = urllib.parse.urljoin( + self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH + ) + req = urllib.request.Request( + spotlight_js_url, + method="HEAD", + ) + urllib.request.urlopen(req) + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_url=self._spotlight_url, + spotlight_js_url=spotlight_js_url, + ) + except urllib.error.URLError as err: + sentry_logger.debug( + "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", + spotlight_js_url, + exc_info=err, + ) + + return self._spotlight_script + + def process_response( + self: "Self", _request: "HttpRequest", response: "HttpResponse" + ) -> "Optional[HttpResponse]": + content_type_header = tuple( + p.strip() + for p in response.headers.get("Content-Type", "").lower().split(";") + ) + content_type = content_type_header[0] + if len(content_type_header) > 1 and content_type_header[1].startswith( + CHARSET_PREFIX + ): + encoding = content_type_header[1][len(CHARSET_PREFIX) :] + else: + encoding = "utf-8" + + if ( + self.spotlight_script is not None + and not response.streaming + and content_type == "text/html" + ): + content_length = len(response.content) + injection = self.spotlight_script.encode(encoding) + injection_site = next( + ( + idx + for idx in ( + response.content.rfind(body_variant.encode(encoding)) + for body_variant in BODY_CLOSE_TAG_POSSIBILITIES + ) + if idx > -1 + ), + content_length, + ) + + # This approach works even when we don't have a `` tag + response.content = ( + response.content[:injection_site] + + injection + + response.content[injection_site:] + ) + + if response.has_header("Content-Length"): + response.headers["Content-Length"] = content_length + len(injection) + + return response + + def process_exception( + self: "Self", _request: "HttpRequest", exception: Exception + ) -> "Optional[HttpResponseServerError]": + if not settings.DEBUG or not self._spotlight_url: + return None + + try: + spotlight = ( + urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") + ) + except urllib.error.URLError: + return None + else: + event_id = self.sentry_sdk.capture_exception(exception) + return HttpResponseServerError( + spotlight.replace( + "", + SPOTLIGHT_ERROR_PAGE_SNIPPET.format( + spotlight_url=self._spotlight_url, event_id=event_id + ), + ) + ) + +except ImportError: + settings = None + + +def _resolve_spotlight_url( + spotlight_config: "Any", sentry_logger: "Any" +) -> "Optional[str]": + """ + Resolve the Spotlight URL based on config and environment variable. + + Implements precedence rules per the SDK spec: + https://develop.sentry.dev/sdk/expected-features/spotlight/ + + Returns the resolved URL string, or None if Spotlight should be disabled. + """ + spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") + + # Parse env var to determine if it's a boolean or URL + spotlight_from_env: "Optional[bool]" = None + spotlight_env_url: "Optional[str]" = None + if spotlight_env_value: + parsed = env_to_bool(spotlight_env_value, strict=True) + if parsed is None: + # It's a URL string + spotlight_from_env = True + spotlight_env_url = spotlight_env_value + else: + spotlight_from_env = parsed + + # Apply precedence rules per spec: + # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules + if spotlight_config is False: + # Config explicitly disables spotlight - warn if env var was set + if spotlight_from_env: + sentry_logger.warning( + "Spotlight is disabled via spotlight=False config option, " + "ignoring SENTRY_SPOTLIGHT environment variable." + ) + return None + elif spotlight_config is True: + # Config enables spotlight with boolean true + # If env var has URL, use env var URL per spec + if spotlight_env_url: + return spotlight_env_url + else: + return DEFAULT_SPOTLIGHT_URL + elif isinstance(spotlight_config, str): + # Config has URL string - use config URL, warn if env var differs + if spotlight_env_value and spotlight_env_value != spotlight_config: + sentry_logger.warning( + "Spotlight URL from config (%s) takes precedence over " + "SENTRY_SPOTLIGHT environment variable (%s).", + spotlight_config, + spotlight_env_value, + ) + return spotlight_config + elif spotlight_config is None: + # No config - use env var + if spotlight_env_url: + return spotlight_env_url + elif spotlight_from_env: + return DEFAULT_SPOTLIGHT_URL + # else: stays None (disabled) + + return None + + +def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": + url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) + + if url is None: + return None + + # Only set up logging handler when spotlight is actually enabled + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.INFO) + + # Update options with resolved URL for consistency + options["spotlight"] = url + + with capture_internal_exceptions(): + if ( + settings is not None + and settings.DEBUG + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) + ): + middleware = settings.MIDDLEWARE + if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: + settings.MIDDLEWARE = type(middleware)( + chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) + ) + logger.info("Enabled Spotlight integration for Django") + + client = SpotlightClient(url) + logger.info("Enabled Spotlight using sidecar at %s", url) + + return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py new file mode 100644 index 0000000000..5ee7e8460b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py @@ -0,0 +1,843 @@ +""" +EXPERIMENTAL. Do not use in production. + +The API in this file is only meant to be used in span streaming mode. + +You can enable span streaming mode via +sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). +""" + +import sys +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + capture_internal_exceptions, + format_attribute, + get_current_thread_meta, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + Iterator, + Optional, + ParamSpec, + TypeVar, + Union, + overload, + ) + + from sentry_sdk._types import Attributes, AttributeValue, SpanJSON + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + + P = ParamSpec("P") + R = TypeVar("R") + + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +class SpanStatus(str, Enum): + OK = "ok" + ERROR = "error" + + def __str__(self) -> str: + return self.value + + +_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) + + +# Segment source, see +# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource +class SegmentSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_SEGMENT_SOURCES = [ + SegmentSource.URL, +] + + +SOURCE_FOR_STYLE = { + "endpoint": SegmentSource.COMPONENT, + "function_name": SegmentSource.COMPONENT, + "handler_name": SegmentSource.COMPONENT, + "method_and_path_pattern": SegmentSource.ROUTE, + "path": SegmentSource.URL, + "route_name": SegmentSource.COMPONENT, + "route_pattern": SegmentSource.ROUTE, + "uri_template": SegmentSource.ROUTE, + "url": SegmentSource.ROUTE, +} + + +# Sentinel value for an unset parent_span to be able to distinguish it from +# a None set by the user +_DEFAULT_PARENT_SPAN = object() + + +def start_span( + name: str, + attributes: "Optional[Attributes]" = None, + parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] + active: bool = True, +) -> "StreamedSpan": + """ + Start a span. + + EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() + instead. + + The span's parent, unless provided explicitly via the `parent_span` argument, + will be the current active span, if any. If there is none, this span will + become the root of a new span tree. If you explicitly want this span to be + top-level without a parent, set `parent_span=None`. + + `start_span()` can either be used as context manager or you can use the span + object it returns and explicitly end it via `span.end()`. The following is + equivalent: + + ```python + import sentry_sdk + + with sentry_sdk.traces.start_span(name="My Span"): + # do something + + # The span automatically finishes once the `with` block is exited + ``` + + ```python + import sentry_sdk + + span = sentry_sdk.traces.start_span(name="My Span") + # do something + span.end() + ``` + + To continue a trace from another service, call + `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. + + :param name: The name to identify this span by. + :type name: str + + :param attributes: Key-value attributes to set on the span from the start. + These will also be accessible in the traces sampler. + :type attributes: "Optional[Attributes]" + + :param parent_span: A span instance that the new span should consider its + parent. If not provided, the parent will be set to the currently active + span, if any. If set to `None`, this span will become a new root-level + span. + :type parent_span: "Optional[StreamedSpan]" + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to `False`. + :type active: bool + + :return: The span that has been started. + :rtype: StreamedSpan + """ + from sentry_sdk.tracing_utils import has_span_streaming_enabled + + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "sentry_sdk.start_transaction() and sentry_sdk.start_span() " + "instead.", + stacklevel=2, + ) + return NoOpStreamedSpan() + + return sentry_sdk.get_current_scope().start_streamed_span( + name, attributes, parent_span, active + ) + + +def continue_trace(incoming: "dict[str, Any]") -> None: + """ + Continue a trace from headers or environment variables. + + EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. + + This function sets the propagation context on the scope. Any span started + in the updated scope will belong under the trace extracted from the + provided propagation headers or environment variables. + + continue_trace() doesn't start any spans on its own. Use the start_span() + API for that. + """ + # This is set both on the isolation and the current scope for compatibility + # reasons. Conceptually, it belongs on the isolation scope, and it also + # used to be set there in non-span-first mode. But in span first mode, we + # start spans on the current scope, regardless of type, like JS does, so we + # need to set the propagation context there. + sentry_sdk.get_isolation_scope().generate_propagation_context( + incoming, + ) + sentry_sdk.get_current_scope().generate_propagation_context( + incoming, + ) + + +def new_trace() -> None: + """ + Resets the propagation context, forcing a new trace. + + EXPERIMENTAL. + + This function sets the propagation context on the scope. Any span started + in the updated scope will start its own trace. + + new_trace() doesn't start any spans on its own. Use the start_span() API + for that. + """ + sentry_sdk.get_isolation_scope().set_new_propagation_context() + sentry_sdk.get_current_scope().set_new_propagation_context() + + +class StreamedSpan: + """ + A span holds timing information of a block of code. + + Spans can have multiple child spans, thus forming a span tree. + + This is the Span First span implementation that streams spans. The original + transaction-based span implementation lives in tracing.Span. + """ + + __slots__ = ( + "_name", + "_attributes", + "_active", + "_span_id", + "_trace_id", + "_parent_span_id", + "_segment", + "_parent_sampled", + "_start_timestamp", + "_start_timestamp_monotonic_ns", + "_end_timestamp", + "_status", + "_scope", + "_previous_span_on_scope", + "_baggage", + "_sample_rand", + "_sample_rate", + "_continuous_profile", + ) + + def __init__( + self, + *, + name: str, + attributes: "Optional[Attributes]" = None, + active: bool = True, + scope: "sentry_sdk.Scope", + segment: "Optional[StreamedSpan]" = None, + trace_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + sample_rate: "Optional[float]" = None, + sample_rand: "Optional[float]" = None, + ): + self._name: str = name + self._active: bool = active + self._attributes: "Attributes" = { + "sentry.origin": "manual", + "sentry.trace_lifecycle": "stream", + } + + if attributes: + for attribute, value in attributes.items(): + self.set_attribute(attribute, value) + + self._scope = scope + + self._segment = segment or self + + self._trace_id: "Optional[str]" = trace_id + self._parent_span_id = parent_span_id + self._parent_sampled = parent_sampled + self._baggage = baggage + self._sample_rand = sample_rand + self._sample_rate = sample_rate + + self._start_timestamp = datetime.now(timezone.utc) + self._end_timestamp: "Optional[datetime]" = None + + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + + self._span_id: "Optional[str]" = None + + self._status = SpanStatus.OK.value + + self._update_active_thread() + + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._start_profile() + self._set_profile_id(get_profiler_id()) + + self._set_segment_attributes() + + self._start() + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__}(" + f"name={self._name}, " + f"trace_id={self.trace_id}, " + f"span_id={self.span_id}, " + f"parent_span_id={self._parent_span_id}, " + f"active={self._active})>" + ) + + def __enter__(self) -> "StreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore + return + + if value is not None and should_be_treated_as_error(ty, value): + self.status = SpanStatus.ERROR.value + + self._end() + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + """ + Finish this span and queue it for sending. + + :param end_timestamp: End timestamp to use instead of current time. + :type end_timestamp: "Optional[Union[float, datetime]]" + """ + self._end(end_timestamp) + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self.end(end_timestamp) + + def _start(self) -> None: + if self._active: + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore. + return + + # Stop the profiler + if self._is_segment() and self._continuous_profile is not None: + with capture_internal_exceptions(): + self._continuous_profile.stop() + + # Detach from scope + if self._active: + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + # Set attributes from the segment. These are set on span end on purpose + # so that we have the best chance to capture the segment's final name + # (since it might change during its lifetime) + self.set_attribute("sentry.segment.id", self._segment.span_id) + self.set_attribute("sentry.segment.name", self._segment.name) + + # Set the end timestamp + if end_timestamp is not None: + if isinstance(end_timestamp, (float, int)): + try: + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + except Exception: + pass + + if isinstance(end_timestamp, datetime): + self._end_timestamp = end_timestamp + else: + logger.debug( + "[Tracing] Failed to set end_timestamp. Using current time instead." + ) + + if self._end_timestamp is None: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self._end_timestamp = self._start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # Finally, queue the span for sending to Sentry + self._scope._capture_span(self) + + def get_attributes(self) -> "Attributes": + return self._attributes + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + self._attributes[key] = format_attribute(value) + + def set_attributes(self, attributes: "Attributes") -> None: + for key, value in attributes.items(): + self.set_attribute(key, value) + + def remove_attribute(self, key: str) -> None: + try: + del self._attributes[key] + except KeyError: + pass + + @property + def status(self) -> "str": + return self._status + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + if isinstance(status, Enum): + status = status.value + + if status not in _VALID_SPAN_STATUSES: + logger.debug( + f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' + ) + return + + self._status = status + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def active(self) -> bool: + return self._active + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @property + def sampled(self) -> "Optional[bool]": + return True + + @property + def start_timestamp(self) -> "Optional[datetime]": + return self._start_timestamp + + @property + def end_timestamp(self) -> "Optional[datetime]": + return self._end_timestamp + + def _is_segment(self) -> bool: + return self._segment is self + + def _update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + + if thread_id is not None: + self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_attribute(SPANDATA.THREAD_NAME, thread_name) + + def _dynamic_sampling_context(self) -> "dict[str, str]": + return self._segment._get_baggage().dynamic_sampling_context() + + def _to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def _to_baggage(self) -> "Optional[Baggage]": + if self._segment: + return self._segment._get_baggage() + return None + + def _get_baggage(self) -> "Baggage": + """ + Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with + the segment. + + The first time a new baggage with Sentry items is made, it will be frozen. + """ + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_segment(self) + + return self._baggage + + def _iter_headers(self) -> "Iterator[tuple[str, str]]": + if not self._segment: + return + + yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() + + baggage = self._segment._get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def _get_trace_context(self) -> "dict[str, Any]": + # Even if spans themselves are not event-based anymore, we need this + # to populate trace context on events + context: "dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self._parent_span_id, + "dynamic_sampling_context": self._dynamic_sampling_context(), + } + + if "sentry.op" in self._attributes: + context["op"] = self._attributes["sentry.op"] + if "sentry.origin" in self._attributes: + context["origin"] = self._attributes["sentry.origin"] + + return context + + def _set_profile_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_attribute("sentry.profiler_id", profiler_id) + + def _start_profile(self) -> None: + if not self._is_segment(): + return + + try_autostart_continuous_profiler() + + self._continuous_profile = try_profile_lifecycle_trace_start() + + def _set_segment_attributes(self) -> None: + if not self._is_segment(): + return + + client = sentry_sdk.get_client() + + self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") + self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) + self.set_attribute( + SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) + ) + + if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: + self.set_attribute( + SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() + ) + + def _to_json(self) -> "SpanJSON": + res: "SpanJSON" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "name": self._name if self._name is not None else "", + "status": self._status, + "is_segment": self._is_segment(), + "start_timestamp": self._start_timestamp.timestamp(), + } + + if self._end_timestamp: + res["end_timestamp"] = self._end_timestamp.timestamp() + + if self._parent_span_id: + res["parent_span_id"] = self._parent_span_id + + res["attributes"] = {k: v for k, v in self._attributes.items()} + + return res + + +class NoOpStreamedSpan(StreamedSpan): + __slots__ = ( + "_finished", + "_unsampled_reason", + ) + + def __init__( + self, + unsampled_reason: "Optional[str]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + ) -> None: + self._scope = scope # type: ignore[assignment] + self._unsampled_reason = unsampled_reason + + self._finished = False + + self._start() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(sampled={self.sampled})>" + + def __enter__(self) -> "NoOpStreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self._end() + + def _start(self) -> None: + if self._scope is None: + return + + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._finished: + return + + if self._unsampled_reason is not None: + client = sentry_sdk.get_client() + if client.is_active() and client.transport: + logger.debug( + f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" + ) + client.transport.record_lost_event( + reason=self._unsampled_reason, + data_category="span", + quantity=1, + ) + + if self._scope and hasattr(self, "_previous_span_on_scope"): + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + self._finished = True + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + self._end() + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self._end() + + def get_attributes(self) -> "Attributes": + return {} + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + pass + + def set_attributes(self, attributes: "Attributes") -> None: + pass + + def remove_attribute(self, key: str) -> None: + pass + + def _is_segment(self) -> bool: + return self._scope is not None + + @property + def status(self) -> "str": + return SpanStatus.OK.value + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + pass + + @property + def name(self) -> str: + return "" + + @name.setter + def name(self, value: str) -> None: + pass + + @property + def active(self) -> bool: + return True + + @property + def span_id(self) -> str: + return "0000000000000000" + + @property + def trace_id(self) -> str: + return "00000000000000000000000000000000" + + @property + def sampled(self) -> "Optional[bool]": + return False + + @property + def start_timestamp(self) -> "Optional[datetime]": + return None + + @property + def end_timestamp(self) -> "Optional[datetime]": + return None + + +if TYPE_CHECKING: + + @overload + def trace( + func: "Callable[P, R]", + ) -> "Callable[P, R]": ... + + @overload + def trace( + func: None = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a span around a function call. + + EXPERIMENTAL. Use @sentry_sdk.trace instead. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to False. + :type active: bool + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import ( + create_streaming_span_decorator, + ) + + decorator = create_streaming_span_decorator( + name=name, + attributes=attributes, + active=active, + ) + + if func: + return decorator(func) + else: + return decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[StreamedSpan]": + """ + Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. + + This function will only return a non-`None` value when the streaming trace lifecycle is enabled. + To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.streamed_span + return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py new file mode 100644 index 0000000000..1790e13fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py @@ -0,0 +1,1475 @@ +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.profiler.continuous_profiler import get_profiler_id +from sentry_sdk.utils import ( + capture_internal_exceptions, + get_current_thread_meta, + is_valid_sample_rate, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, MutableMapping + from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + overload, + ) + + from typing_extensions import TypedDict, Unpack + + P = ParamSpec("P") + R = TypeVar("R") + + from sentry_sdk._types import ( + Event, + MeasurementUnit, + MeasurementValue, + SamplingContext, + ) + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + from sentry_sdk.profiler.transaction_profiler import Profile + + class SpanKwargs(TypedDict, total=False): + trace_id: str + """ + The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + """ + + span_id: str + """The span ID of this span. If omitted, a new span ID will be generated.""" + + parent_span_id: str + """The span ID of the parent span, if applicable.""" + + same_process_as_parent: bool + """Whether this span is in the same process as the parent span.""" + + sampled: bool + """ + Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + """ + + op: str + """ + The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + """ + + description: str + """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" + + hub: "Optional[sentry_sdk.Hub]" + """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" + + status: str + """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" + + containing_transaction: "Optional[Transaction]" + """The transaction that this span belongs to.""" + + start_timestamp: "Optional[Union[datetime, float]]" + """ + The timestamp when the span started. If omitted, the current time + will be used. + """ + + scope: "sentry_sdk.Scope" + """The scope to use for this span. If not provided, we use the current scope.""" + + origin: str + """ + The origin of the span. + See https://develop.sentry.dev/sdk/performance/trace-origin/ + Default "manual". + """ + + name: str + """A string describing what operation is being performed within the span/transaction.""" + + class TransactionKwargs(SpanKwargs, total=False): + source: str + """ + A string describing the source of the transaction name. This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. + Default "custom". + """ + + parent_sampled: bool + """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" + + baggage: "Baggage" + """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" + + ProfileContext = TypedDict( + "ProfileContext", + { + "profiler_id": str, + }, + ) + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +# Transaction source +# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations +class TransactionSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_TRANSACTION_SOURCES = [ + TransactionSource.URL, +] + +SOURCE_FOR_STYLE = { + "endpoint": TransactionSource.COMPONENT, + "function_name": TransactionSource.COMPONENT, + "handler_name": TransactionSource.COMPONENT, + "method_and_path_pattern": TransactionSource.ROUTE, + "path": TransactionSource.URL, + "route_name": TransactionSource.COMPONENT, + "route_pattern": TransactionSource.ROUTE, + "uri_template": TransactionSource.ROUTE, + "url": TransactionSource.ROUTE, +} + + +def get_span_status_from_http_code(http_status_code: int) -> str: + """ + Returns the Sentry status corresponding to the given HTTP status code. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + if http_status_code < 400: + return SPANSTATUS.OK + + elif 400 <= http_status_code < 500: + if http_status_code == 403: + return SPANSTATUS.PERMISSION_DENIED + elif http_status_code == 404: + return SPANSTATUS.NOT_FOUND + elif http_status_code == 429: + return SPANSTATUS.RESOURCE_EXHAUSTED + elif http_status_code == 413: + return SPANSTATUS.FAILED_PRECONDITION + elif http_status_code == 401: + return SPANSTATUS.UNAUTHENTICATED + elif http_status_code == 409: + return SPANSTATUS.ALREADY_EXISTS + else: + return SPANSTATUS.INVALID_ARGUMENT + + elif 500 <= http_status_code < 600: + if http_status_code == 504: + return SPANSTATUS.DEADLINE_EXCEEDED + elif http_status_code == 501: + return SPANSTATUS.UNIMPLEMENTED + elif http_status_code == 503: + return SPANSTATUS.UNAVAILABLE + else: + return SPANSTATUS.INTERNAL_ERROR + + return SPANSTATUS.UNKNOWN_ERROR + + +class _SpanRecorder: + """Limits the number of spans recorded in a transaction.""" + + __slots__ = ("maxlen", "spans", "dropped_spans") + + def __init__(self, maxlen: int) -> None: + # FIXME: this is `maxlen - 1` only to preserve historical behavior + # enforced by tests. + # Either this should be changed to `maxlen` or the JS SDK implementation + # should be changed to match a consistent interpretation of what maxlen + # limits: either transaction+spans or only child spans. + self.maxlen = maxlen - 1 + self.spans: "List[Span]" = [] + self.dropped_spans: int = 0 + + def add(self, span: "Span") -> None: + if len(self.spans) > self.maxlen: + span._span_recorder = None + self.dropped_spans += 1 + else: + self.spans.append(span) + + +class Span: + """A span holds timing information of a block of code. + Spans can have multiple child spans thus forming a span tree. + + :param trace_id: The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + :param span_id: The span ID of this span. If omitted, a new span ID will be generated. + :param parent_span_id: The span ID of the parent span, if applicable. + :param same_process_as_parent: Whether this span is in the same process as the parent span. + :param sampled: Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + :param op: The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + :param description: A description of what operation is being performed within the span. + + .. deprecated:: 2.15.0 + Please use the `name` parameter, instead. + :param name: A string describing what operation is being performed within the span. + :param hub: The hub to use for this span. + + .. deprecated:: 2.0.0 + Please use the `scope` parameter, instead. + :param status: The span's status. Possible values are listed at + https://develop.sentry.dev/sdk/event-payloads/span/ + :param containing_transaction: The transaction that this span belongs to. + :param start_timestamp: The timestamp when the span started. If omitted, the current time + will be used. + :param scope: The scope to use for this span. If not provided, we use the current scope. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "same_process_as_parent", + "sampled", + "op", + "description", + "_measurements", + "start_timestamp", + "_start_timestamp_monotonic_ns", + "status", + "timestamp", + "_tags", + "_data", + "_span_recorder", + "hub", + "_context_manager_state", + "_containing_transaction", + "scope", + "origin", + "name", + "_flags", + "_flags_capacity", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + same_process_as_parent: bool = True, + sampled: "Optional[bool]" = None, + op: "Optional[str]" = None, + description: "Optional[str]" = None, + hub: "Optional[sentry_sdk.Hub]" = None, # deprecated + status: "Optional[str]" = None, + containing_transaction: "Optional[Transaction]" = None, + start_timestamp: "Optional[Union[datetime, float]]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + origin: str = "manual", + name: "Optional[str]" = None, + ) -> None: + self._trace_id = trace_id + self._span_id = span_id + self.parent_span_id = parent_span_id + self.same_process_as_parent = same_process_as_parent + self.sampled = sampled + self.op = op + self.description = name or description + self.status = status + self.hub = hub # backwards compatibility + self.scope = scope + self.origin = origin + self._measurements: "Dict[str, MeasurementValue]" = {} + self._tags: "MutableMapping[str, str]" = {} + self._data: "Dict[str, Any]" = {} + self._containing_transaction = containing_transaction + self._flags: "Dict[str, bool]" = {} + self._flags_capacity = 10 + + if hub is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use `scope` instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.scope = self.scope or hub.scope + + if start_timestamp is None: + start_timestamp = datetime.now(timezone.utc) + elif isinstance(start_timestamp, float): + start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) + self.start_timestamp = start_timestamp + try: + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + except AttributeError: + pass + + #: End timestamp of span + self.timestamp: "Optional[datetime]" = None + + self._span_recorder: "Optional[_SpanRecorder]" = None + + self.update_active_thread() + self.set_profiler_id(get_profiler_id()) + + # TODO this should really live on the Transaction class rather than the Span + # class + def init_span_recorder(self, maxlen: int) -> None: + if self._span_recorder is None: + self._span_recorder = _SpanRecorder(maxlen) + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + def __repr__(self) -> str: + return ( + "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.op, + self.description, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.origin, + ) + ) + + def __enter__(self) -> "Span": + scope = self.scope or sentry_sdk.get_current_scope() + old_span = scope.span + scope.span = self + self._context_manager_state = (scope, old_span) + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if value is not None and should_be_treated_as_error(ty, value): + self.set_status(SPANSTATUS.INTERNAL_ERROR) + + with capture_internal_exceptions(): + scope, old_span = self._context_manager_state + del self._context_manager_state + self.finish(scope) + scope.span = old_span + + @property + def containing_transaction(self) -> "Optional[Transaction]": + """The ``Transaction`` that this span belongs to. + The ``Transaction`` is the root of the span tree, + so one could also think of this ``Transaction`` as the "root span".""" + + # this is a getter rather than a regular attribute so that transactions + # can return `self` here instead (as a way to prevent them circularly + # referencing themselves) + return self._containing_transaction + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a sub-span from the current span or transaction. + + Takes the same arguments as the initializer of :py:class:`Span`. The + trace id, sampling decision, transaction pointer, and span recorder are + inherited from the current span/transaction. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + kwargs.setdefault("sampled", self.sampled) + + child = Span( + trace_id=self.trace_id, + parent_span_id=self.span_id, + containing_transaction=self.containing_transaction, + **kwargs, + ) + + span_recorder = ( + self.containing_transaction and self.containing_transaction._span_recorder + ) + if span_recorder: + span_recorder.add(child) + + return child + + @classmethod + def continue_from_environ( + cls, + environ: "Mapping[str, str]", + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a Transaction with the given params, then add in data pulled from + the ``sentry-trace`` and ``baggage`` headers from the environ (if any) + before returning the Transaction. + + This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` + in that it assumes header names in the form ``HTTP_HEADER_NAME`` - + such as you would get from a WSGI/ASGI environ - + rather than the form ``header-name``. + + :param environ: The ASGI/WSGI environ to pull information from. + """ + return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) + + @classmethod + def continue_from_headers( + cls, + headers: "Mapping[str, str]", + *, + _sample_rand: "Optional[str]" = None, + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a transaction with the given params (including any data pulled from + the ``sentry-trace`` and ``baggage`` headers). + + :param headers: The dictionary with the HTTP headers to pull information from. + :param _sample_rand: If provided, we override the sample_rand value from the + incoming headers with this value. (internal use only) + """ + logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") + + # TODO-neel move away from this kwargs stuff, it's confusing and opaque + # make more explicit + baggage = Baggage.from_incoming_header( + headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand + ) + kwargs.update({BAGGAGE_HEADER_NAME: baggage}) + + sentrytrace_kwargs = extract_sentrytrace_data( + headers.get(SENTRY_TRACE_HEADER_NAME) + ) + + if sentrytrace_kwargs is not None: + kwargs.update(sentrytrace_kwargs) + + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and immutable and won't be populated as head SDK. + baggage.freeze() + + transaction = Transaction(**kwargs) + transaction.same_process_as_parent = False + + return transaction + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. + If the span's containing transaction doesn't yet have a ``baggage`` value, + this will cause one to be generated and stored. + """ + if not self.containing_transaction: + # Do not propagate headers if there is no containing transaction. Otherwise, this + # span ends up being the root span of a new trace, and since it does not get sent + # to Sentry, the trace will be missing a root transaction. The dynamic sampling + # context will also be missing, breaking dynamic sampling & traces. + return + + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.containing_transaction.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + @classmethod + def from_traceparent( + cls, + traceparent: "Optional[str]", + **kwargs: "Any", + ) -> "Optional[Transaction]": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a ``Transaction`` with the given params, then add in data pulled from + the given ``sentry-trace`` header value before returning the ``Transaction``. + """ + if not traceparent: + return None + + return cls.continue_from_headers( + {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs + ) + + def to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def to_baggage(self) -> "Optional[Baggage]": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with this ``Span``, if any. (Taken from the root of the span tree.) + """ + if self.containing_transaction: + return self.containing_transaction.get_baggage() + return None + + def set_tag(self, key: str, value: "Any") -> None: + self._tags[key] = value + + def set_data(self, key: str, value: "Any") -> None: + self._data[key] = value + + def update_data(self, data: "Dict[str, Any]") -> None: + self._data.update(data) + + def set_flag(self, flag: str, result: bool) -> None: + if len(self._flags) < self._flags_capacity: + self._flags[flag] = result + + def set_status(self, value: str) -> None: + self.status = value + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_thread( + self, thread_id: "Optional[int]", thread_name: "Optional[str]" + ) -> None: + if thread_id is not None: + self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_data(SPANDATA.THREAD_NAME, thread_name) + + def set_profiler_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) + + def set_http_status(self, http_status: int) -> None: + self.set_tag( + "http.status_code", str(http_status) + ) # TODO-neel remove in major, we keep this for backwards compatibility + self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) + self.set_status(get_span_status_from_http_code(http_status)) + + def is_success(self) -> bool: + return self.status == "ok" + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + ) -> "Optional[str]": + """ + Sets the end timestamp of the span. + + Additionally it also creates a breadcrumb from the span, + if the span represents a database or HTTP request. + + :param scope: The scope to use for this transaction. + If not provided, the current scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + + :return: Always ``None``. The type is ``Optional[str]`` to match + the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. + """ + if self.timestamp is not None: + # This span is already finished, ignore. + return None + + try: + if end_timestamp: + if isinstance(end_timestamp, float): + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + self.timestamp = end_timestamp + else: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self.timestamp = self.start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + except AttributeError: + self.timestamp = datetime.now(timezone.utc) + + scope = scope or sentry_sdk.get_current_scope() + + # Copy conversation_id from scope to span data if this is an AI span + conversation_id = scope.get_conversation_id() + if conversation_id: + has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data + is_ai_span_op = self.op is not None and ( + self.op.startswith("ai.") or self.op.startswith("gen_ai.") + ) + if has_ai_op or is_ai_span_op: + self.set_data("gen_ai.conversation.id", conversation_id) + + maybe_create_breadcrumbs_from_span(scope, self) + + return None + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the span.""" + + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "same_process_as_parent": self.same_process_as_parent, + "op": self.op, + "description": self.description, + "start_timestamp": self.start_timestamp, + "timestamp": self.timestamp, + "origin": self.origin, + } + + if self.status: + rv["status"] = self.status + # TODO-neel remove redundant tag in major + self._tags["status"] = self.status + + if len(self._measurements) > 0: + rv["measurements"] = self._measurements + + tags = self._tags + if tags: + rv["tags"] = tags + + data = {} + data.update(self._flags) + data.update(self._data) + if data: + rv["data"] = data + + return rv + + def get_trace_context(self) -> "Any": + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "op": self.op, + "description": self.description, + "origin": self.origin, + } + if self.status: + rv["status"] = self.status + + if self.containing_transaction: + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) + + data = {} + + thread_id = self._data.get(SPANDATA.THREAD_ID) + if thread_id is not None: + data["thread.id"] = thread_id + + thread_name = self._data.get(SPANDATA.THREAD_NAME) + if thread_name is not None: + data["thread.name"] = thread_name + + if data: + rv["data"] = data + + return rv + + def get_profile_context(self) -> "Optional[ProfileContext]": + profiler_id = self._data.get(SPANDATA.PROFILER_ID) + if profiler_id is None: + return None + + return { + "profiler_id": profiler_id, + } + + def update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + self.set_thread(thread_id, thread_name) + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +class Transaction(Span): + """The Transaction is the root element that holds all the spans + for Sentry performance instrumentation. + + :param name: Identifier of the transaction. + Will show up in the Sentry UI. + :param parent_sampled: Whether the parent transaction was sampled. + If True this transaction will be kept, if False it will be discarded. + :param baggage: The W3C baggage header value. + (see https://www.w3.org/TR/baggage/) + :param source: A string describing the source of the transaction name. + This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations + for more information. Default "custom". + :param kwargs: Additional arguments to be passed to the Span constructor. + See :py:class:`sentry_sdk.tracing.Span` for available arguments. + """ + + __slots__ = ( + "name", + "source", + "parent_sampled", + # used to create baggage value for head SDKs in dynamic sampling + "sample_rate", + "_measurements", + "_contexts", + "_profile", + "_continuous_profile", + "_baggage", + "_sample_rand", + ) + + def __init__( # type: ignore[misc] + self, + name: str = "", + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + source: str = TransactionSource.CUSTOM, + **kwargs: "Unpack[SpanKwargs]", + ) -> None: + super().__init__(**kwargs) + + self.name = name + self.source = source + self.sample_rate: "Optional[float]" = None + self.parent_sampled = parent_sampled + self._measurements: "Dict[str, MeasurementValue]" = {} + self._contexts: "Dict[str, Any]" = {} + self._profile: "Optional[Profile]" = None + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._baggage = baggage + + baggage_sample_rand = ( + None if self._baggage is None else self._baggage._sample_rand() + ) + if baggage_sample_rand is not None: + self._sample_rand = baggage_sample_rand + else: + self._sample_rand = _generate_sample_rand(self.trace_id) + + def __repr__(self) -> str: + return ( + "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.name, + self.op, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.source, + self.origin, + ) + ) + + def _possibly_started(self) -> bool: + """Returns whether the transaction might have been started. + + If this returns False, we know that the transaction was not started + with sentry_sdk.start_transaction, and therefore the transaction will + be discarded. + """ + + # We must explicitly check self.sampled is False since self.sampled can be None + return self._span_recorder is not None or self.sampled is False + + def __enter__(self) -> "Transaction": + if not self._possibly_started(): + logger.debug( + "Transaction was entered without being started with sentry_sdk.start_transaction." + "The transaction will not be sent to Sentry. To fix, start the transaction by" + "passing it to sentry_sdk.start_transaction." + ) + + super().__enter__() + + if self._profile is not None: + self._profile.__enter__() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._profile is not None: + self._profile.__exit__(ty, value, tb) + + if self._continuous_profile is not None: + self._continuous_profile.stop() + + super().__exit__(ty, value, tb) + + @property + def containing_transaction(self) -> "Transaction": + """The root element of the span tree. + In the case of a transaction it is the transaction itself. + """ + + # Transactions (as spans) belong to themselves (as transactions). This + # is a getter rather than a regular attribute to avoid having a circular + # reference. + return self + + def _get_scope_from_finish_args( + self, + scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + ) -> "Optional[sentry_sdk.Scope]": + """ + Logic to get the scope from the arguments passed to finish. This + function exists for backwards compatibility with the old finish. + + TODO: Remove this function in the next major version. + """ + scope_or_hub = scope_arg + if hub_arg is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", + DeprecationWarning, + stacklevel=3, + ) + + scope_or_hub = hub_arg + + if isinstance(scope_or_hub, sentry_sdk.Hub): + warnings.warn( + "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", + DeprecationWarning, + stacklevel=3, + ) + + return scope_or_hub.scope + + return scope_or_hub + + def _get_log_representation(self) -> str: + return "{op}transaction <{name}>".format( + op=("<" + self.op + "> " if self.op else ""), name=self.name + ) + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """Finishes the transaction and sends it to Sentry. + All finished spans in the transaction will also be sent to Sentry. + + :param scope: The Scope to use for this transaction. + If not provided, the current Scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + :param hub: The hub to use for this transaction. + This argument is DEPRECATED. Please use the `scope` + parameter, instead. + + :return: The event ID if the transaction was sent to Sentry, + otherwise None. + """ + if self.timestamp is not None: + # This transaction is already finished, ignore. + return None + + # For backwards compatibility, we must handle the case where `scope` + # or `hub` could both either be a `Scope` or a `Hub`. + scope = self._get_scope_from_finish_args(scope, hub) + + scope = scope or self.scope or sentry_sdk.get_current_scope() + client = sentry_sdk.get_client() + + if not client.is_active(): + # We have no active client and therefore nowhere to send this transaction. + return None + + if self._span_recorder is None: + # Explicit check against False needed because self.sampled might be None + if self.sampled is False: + logger.debug("Discarding transaction because sampled = False") + else: + logger.debug( + "Discarding transaction because it was not started with sentry_sdk.start_transaction" + ) + + # This is not entirely accurate because discards here are not + # exclusively based on sample rate but also traces sampler, but + # we handle this the same here. + if client.transport and has_tracing_enabled(client.options): + if client.monitor and client.monitor.downsample_factor > 0: + reason = "backpressure" + else: + reason = "sample_rate" + + client.transport.record_lost_event(reason, data_category="transaction") + + # Only one span (the transaction itself) is discarded, since we did not record any spans here. + client.transport.record_lost_event(reason, data_category="span") + return None + + if not self.name: + logger.warning( + "Transaction has no name, falling back to ``." + ) + self.name = "" + + super().finish(scope, end_timestamp) + + status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) + if ( + status_code is not None + and status_code in client.options["trace_ignore_status_codes"] + ): + logger.debug( + "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( + transaction_description=self._get_log_representation(), + status_code=self._data[SPANDATA.HTTP_STATUS_CODE], + trace_ignore_status_codes=client.options[ + "trace_ignore_status_codes" + ], + ) + ) + if client.transport: + client.transport.record_lost_event( + "event_processor", data_category="transaction" + ) + + num_spans = len(self._span_recorder.spans) + 1 + client.transport.record_lost_event( + "event_processor", data_category="span", quantity=num_spans + ) + + self.sampled = False + + if not self.sampled: + # At this point a `sampled = None` should have already been resolved + # to a concrete decision. + if self.sampled is None: + logger.warning("Discarding transaction without sampling decision.") + + return None + + finished_spans = [] + has_gen_ai_span = False + if client.options.get("stream_gen_ai_spans", True): + for span in self._span_recorder.spans: + if span.timestamp is None: + continue + + if isinstance(span.op, str) and span.op.startswith("gen_ai."): + has_gen_ai_span = True + + finished_spans.append(span.to_json()) + else: + finished_spans = [ + span.to_json() + for span in self._span_recorder.spans + if span.timestamp is not None + ] + + len_diff = len(self._span_recorder.spans) - len(finished_spans) + dropped_spans = len_diff + self._span_recorder.dropped_spans + + # we do this to break the circular reference of transaction -> span + # recorder -> span -> containing transaction (which is where we started) + # before either the spans or the transaction goes out of scope and has + # to be garbage collected + self._span_recorder = None + + contexts = {} + contexts.update(self._contexts) + contexts.update({"trace": self.get_trace_context()}) + profile_context = self.get_profile_context() + if profile_context is not None: + contexts.update({"profile": profile_context}) + + event: "Event" = { + "type": "transaction", + "transaction": self.name, + "transaction_info": {"source": self.source}, + "contexts": contexts, + "tags": self._tags, + "timestamp": self.timestamp, + "start_timestamp": self.start_timestamp, + "spans": finished_spans, + } + + if dropped_spans > 0: + event["_dropped_spans"] = dropped_spans + + if has_gen_ai_span: + event["_has_gen_ai_span"] = True + + if self._profile is not None and self._profile.valid(): + event["profile"] = self._profile + self._profile = None + + event["measurements"] = self._measurements + + return scope.capture_event(event) + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + """Sets a context. Transactions can have multiple contexts + and they should follow the format described in the "Contexts Interface" + documentation. + + :param key: The name of the context. + :param value: The information about the context. + """ + self._contexts[key] = value + + def set_http_status(self, http_status: int) -> None: + """Sets the status of the Transaction according to the given HTTP status. + + :param http_status: The HTTP status code.""" + super().set_http_status(http_status) + self.set_context("response", {"status_code": http_status}) + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the transaction.""" + rv = super().to_json() + + rv["name"] = self.name + rv["source"] = self.source + rv["sampled"] = self.sampled + + return rv + + def get_trace_context(self) -> "Any": + trace_context = super().get_trace_context() + + if self._data: + trace_context["data"] = self._data + + return trace_context + + def get_baggage(self) -> "Baggage": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with the Transaction. + + The first time a new baggage with Sentry items is made, + it will be frozen.""" + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_transaction(self) + + return self._baggage + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the transaction's sampling decision, according to the following + precedence rules: + + 1. If a sampling decision is passed to `start_transaction` + (`start_transaction(name: "my transaction", sampled: True)`), that + decision will be used, regardless of anything else + + 2. If `traces_sampler` is defined, its decision will be used. It can + choose to keep or ignore any parent sampling decision, or use the + sampling context data to make its own decision or to choose a sample + rate for the transaction. + + 3. If `traces_sampler` is not defined, but there's a parent sampling + decision, the parent sampling decision will be used. + + 4. If `traces_sampler` is not defined and there's no parent sampling + decision, `traces_sample_rate` will be used. + """ + client = sentry_sdk.get_client() + + transaction_description = self._get_log_representation() + + # nothing to do if tracing is disabled + if not has_tracing_enabled(client.options): + self.sampled = False + return + + # if the user has forced a sampling decision by passing a `sampled` + # value when starting the transaction, go with that + if self.sampled is not None: + self.sample_rate = float(self.sampled) + return + + # we would have bailed already if neither `traces_sampler` nor + # `traces_sample_rate` were defined, so one of these should work; prefer + # the hook if so + sample_rate = ( + client.options["traces_sampler"](sampling_context) + if callable(client.options.get("traces_sampler")) + # default inheritance behavior + else ( + sampling_context["parent_sampled"] + if sampling_context["parent_sampled"] is not None + else client.options["traces_sample_rate"] + ) + ) + + # Since this is coming from the user (or from a function provided by the + # user), who knows what we might get. (The only valid values are + # booleans or numbers between 0 and 1.) + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning( + "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( + transaction_description=transaction_description, + ) + ) + self.sampled = False + return + + self.sample_rate = float(sample_rate) + + if client.monitor: + self.sample_rate /= 2**client.monitor.downsample_factor + + # if the function returned 0 (or false), or if `traces_sample_rate` is + # 0, it's a sign the transaction should be dropped + if not self.sample_rate: + logger.debug( + "[Tracing] Discarding {transaction_description} because {reason}".format( + transaction_description=transaction_description, + reason=( + "traces_sampler returned 0 or False" + if callable(client.options.get("traces_sampler")) + else "traces_sample_rate is set to 0" + ), + ) + ) + self.sampled = False + return + + # Now we roll the dice. + self.sampled = self._sample_rand < self.sample_rate + + if self.sampled: + logger.debug( + "[Tracing] Starting {transaction_description}".format( + transaction_description=transaction_description, + ) + ) + else: + logger.debug( + "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( + transaction_description=transaction_description, + sample_rate=self.sample_rate, + ) + ) + + # Private aliases matching StreamedSpan's private API + _get_baggage = get_baggage + _get_trace_context = get_trace_context + + +class NoOpSpan(Span): + def __repr__(self) -> str: + return "<%s>" % self.__class__.__name__ + + @property + def containing_transaction(self) -> "Optional[Transaction]": + return None + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "NoOpSpan": + return NoOpSpan() + + def to_traceparent(self) -> str: + return "" + + def to_baggage(self) -> "Optional[Baggage]": + return None + + def get_baggage(self) -> "Optional[Baggage]": + return None + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + return iter(()) + + def set_tag(self, key: str, value: "Any") -> None: + pass + + def set_data(self, key: str, value: "Any") -> None: + pass + + def update_data(self, data: "Dict[str, Any]") -> None: + pass + + def set_status(self, value: str) -> None: + pass + + def set_http_status(self, http_status: int) -> None: + pass + + def is_success(self) -> bool: + return True + + def to_json(self) -> "Dict[str, Any]": + return {} + + def get_trace_context(self) -> "Any": + return {} + + def get_profile_context(self) -> "Any": + return {} + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """ + The `hub` parameter is deprecated. Please use the `scope` parameter, instead. + """ + pass + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + pass + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + pass + + def init_span_recorder(self, maxlen: int) -> None: + pass + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + pass + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _get_baggage = get_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +if TYPE_CHECKING: + + @overload + def trace( + func: None = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": + # Handles: @trace() and @trace(op="custom") + pass + + @overload + def trace(func: "Callable[P, R]") -> "Callable[P, R]": + # Handles: @trace + pass + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a child span around a function call. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, a default operation will + be assigned based on the template. + :type op: str or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP, SPANTEMPLATE + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + op=OP.DB_QUERY, + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + + # With a custom template + @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) + def calculate_interest_rate(amount, rate, years): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import create_span_decorator + + decorator = create_span_decorator( + op=op, + name=name, + attributes=attributes, + template=template, + ) + + if func: + return decorator(func) + else: + return decorator + + +# Circular imports + +from sentry_sdk.tracing_utils import ( + Baggage, + EnvironHeaders, + _generate_sample_rand, + extract_sentrytrace_data, + has_tracing_enabled, + maybe_create_breadcrumbs_from_span, +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py new file mode 100644 index 0000000000..c2e34a795b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py @@ -0,0 +1,1694 @@ +import contextlib +import functools +import inspect +import os +import re +import sys +import uuid +import warnings +from collections.abc import Mapping, MutableMapping +from datetime import datetime, timedelta, timezone +from random import Random +from urllib.parse import quote, unquote + +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.utils import ( + _is_external_source, + _is_in_project_root, + _module_in_list, + capture_internal_exceptions, + filename_for_module, + is_sentry_url, + is_valid_sample_rate, + logger, + match_regex_list, + qualname_from_function, + safe_repr, + to_string, + try_convert, +) + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union + + from sentry_sdk._types import Attributes + + +SENTRY_TRACE_REGEX = re.compile( + "^[ \t]*" # whitespace + "([0-9a-f]{32})?" # trace_id + "-?([0-9a-f]{16})?" # span_id + "-?([01])?" # sampled + "[ \t]*$" # whitespace +) + + +# This is a normal base64 regex, modified to reflect that fact that we strip the +# trailing = or == off +base64_stripped = ( + # any of the characters in the base64 "alphabet", in multiples of 4 + "([a-zA-Z0-9+/]{4})*" + # either nothing or 2 or 3 base64-alphabet characters (see + # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for + # why there's never only 1 extra character) + "([a-zA-Z0-9+/]{2,3})?" +) + + +class EnvironHeaders(Mapping): # type: ignore + def __init__( + self, + environ: "Mapping[str, str]", + prefix: str = "HTTP_", + ) -> None: + self.environ = environ + self.prefix = prefix + + def __getitem__(self, key: str) -> "Optional[Any]": + return self.environ[self.prefix + key.replace("-", "_").upper()] + + def __len__(self) -> int: + return sum(1 for _ in iter(self)) + + def __iter__(self) -> "Generator[str, None, None]": + for k in self.environ: + if not isinstance(k, str): + continue + + k = k.replace("-", "_").upper() + if not k.startswith(self.prefix): + continue + + yield k[len(self.prefix) :] + + +def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: + """ + Returns True if either traces_sample_rate or traces_sampler is + defined and enable_tracing is set and not false. + """ + if options is None: + return False + + return bool( + options.get("enable_tracing") is not False + and ( + options.get("traces_sample_rate") is not None + or options.get("traces_sampler") is not None + ) + ) + + +def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return False + + return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" + + +def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return True + + return not options.get( + "stream_gen_ai_spans", True + ) and not has_span_streaming_enabled(options) + + +@contextlib.contextmanager +def record_sql_queries( + cursor: "Any", + query: "Any", + params_list: "Any", + paramstyle: "Optional[str]", + executemany: bool, + record_cursor_repr: bool = False, + span_origin: str = "manual", + span_op_override_value: "Optional[str]" = None, +) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": + # TODO: Bring back capturing of params by default + client = sentry_sdk.get_client() + if client.options["_experiments"].get("record_sql_params", False): + if not params_list or params_list == [None]: + params_list = None + + if paramstyle == "pyformat": + paramstyle = "format" + else: + params_list = None + paramstyle = None + + query = _format_sql(cursor, query) + + data = {} + if params_list is not None: + data["db.params"] = params_list + if paramstyle is not None: + data["db.paramstyle"] = paramstyle + if executemany: + data["db.executemany"] = True + if record_cursor_repr and cursor is not None: + data["db.cursor"] = cursor + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message=query, category="query", data=data) + + if has_span_streaming_enabled(client.options): + additional_attributes = {} + if query is not None: + additional_attributes["db.query.text"] = query + + with sentry_sdk.traces.start_span( + name="" if query is None else query, + attributes={ + "sentry.origin": span_origin, + "sentry.op": span_op_override_value + if span_op_override_value + else OP.DB, + **additional_attributes, + }, + ) as span: + yield span + else: + with sentry_sdk.start_span( + op=span_op_override_value if span_op_override_value is not None else OP.DB, + name=query, + origin=span_origin, + ) as span: + for k, v in data.items(): + span.set_data(k, v) + yield span + + +def maybe_create_breadcrumbs_from_span( + scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" +) -> None: + if span.op == OP.DB_REDIS: + scope.add_breadcrumb( + message=span.description, type="redis", category="redis", data=span._tags + ) + + elif span.op == OP.HTTP_CLIENT: + level = None + status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) + if status_code: + if 500 <= status_code <= 599: + level = "error" + elif 400 <= status_code <= 499: + level = "warning" + + if level: + scope.add_breadcrumb( + type="http", category="httplib", data=span._data, level=level + ) + else: + scope.add_breadcrumb(type="http", category="httplib", data=span._data) + + elif span.op == "subprocess": + scope.add_breadcrumb( + type="subprocess", + category="subprocess", + message=span.description, + data=span._data, + ) + + +def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": + try: + return frame.f_code.co_filename + except Exception: + return None + + +def _should_be_included( + is_sentry_sdk_frame: bool, + namespace: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", + abs_path: "Optional[str]", + project_root: "Optional[str]", +) -> bool: + # in_app_include takes precedence over in_app_exclude + should_be_included = _module_in_list(namespace, in_app_include) + should_be_excluded = _is_external_source(abs_path) or _module_in_list( + namespace, in_app_exclude + ) + return not is_sentry_sdk_frame and ( + should_be_included + or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) + ) + + +def add_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", + project_root: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", +) -> None: + """ + Adds OTel compatible source code information to the span + """ + # Find the correct frame + frame: "Union[FrameType, None]" = sys._getframe() + while frame is not None: + abs_path = _get_frame_module_abs_path(frame) + + try: + namespace: "Optional[str]" = frame.f_globals.get("__name__") + except Exception: + namespace = None + + is_sentry_sdk_frame = namespace is not None and namespace.startswith( + "sentry_sdk." + ) + + should_be_included = _should_be_included( + is_sentry_sdk_frame=is_sentry_sdk_frame, + namespace=namespace, + in_app_include=in_app_include, + in_app_exclude=in_app_exclude, + abs_path=abs_path, + project_root=project_root, + ) + if should_be_included: + break + + frame = frame.f_back + else: + frame = None + + # Set the data + if frame is not None: + try: + lineno = frame.f_lineno + except Exception: + lineno = None + if lineno is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_LINENO, lineno) + else: + span.set_attribute("code.line.number", lineno) + + try: + namespace = frame.f_globals.get("__name__") + except Exception: + namespace = None + if namespace is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_NAMESPACE, namespace) + else: + span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) + + filepath = _get_frame_module_abs_path(frame) + if filepath is not None: + if namespace is not None: + in_app_path = filename_for_module(namespace, filepath) + elif project_root is not None and filepath.startswith(project_root): + in_app_path = filepath.replace(project_root, "").lstrip(os.sep) + else: + in_app_path = filepath + + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) + else: + if in_app_path is not None: + span.set_attribute("code.file.path", in_app_path) + + try: + code_function = frame.f_code.co_name + except Exception: + code_function = None + + if code_function is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + else: + span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + + +def add_query_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a database query span + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_query_source = client.options.get("enable_db_query_source", True) + if not should_add_query_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("db_query_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def add_http_request_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a span for an outgoing HTTP request + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_request_source = client.options.get("enable_http_request_source", True) + if not should_add_request_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("http_request_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def extract_sentrytrace_data( + header: "Optional[str]", +) -> "Optional[Dict[str, Union[str, bool, None]]]": + """ + Given a `sentry-trace` header string, return a dictionary of data. + """ + if not header: + return None + + if "," in header: + # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. + parts = [part.strip() for part in header.split(",")] + header = next((part for part in parts if part), "") + + if not header: + return None + + if header.startswith("00-") and header.endswith("-00"): + header = header[3:-3] + + match = SENTRY_TRACE_REGEX.match(header) + if not match: + return None + + trace_id, parent_span_id, sampled_str = match.groups() + parent_sampled = None + + if trace_id: + trace_id = "{:032x}".format(int(trace_id, 16)) + if parent_span_id: + parent_span_id = "{:016x}".format(int(parent_span_id, 16)) + if sampled_str: + parent_sampled = sampled_str != "0" + + return { + "trace_id": trace_id, + "parent_span_id": parent_span_id, + "parent_sampled": parent_sampled, + } + + +def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": + real_sql = None + + # If we're using psycopg2, it could be that we're + # looking at a query that uses Composed objects. Use psycopg2's mogrify + # function to format the query. We lose per-parameter trimming but gain + # accuracy in formatting. + try: + if hasattr(cursor, "mogrify"): + real_sql = cursor.mogrify(sql) + if isinstance(real_sql, bytes): + real_sql = real_sql.decode(cursor.connection.encoding) + except Exception: + real_sql = None + + return real_sql or to_string(sql) + + +class PropagationContext: + """ + The PropagationContext represents the data of a trace in Sentry. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "parent_sampled", + "baggage", + "custom_sampling_context", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + dynamic_sampling_context: "Optional[Dict[str, str]]" = None, + baggage: "Optional[Baggage]" = None, + ) -> None: + self._trace_id = trace_id + """The trace id of the Sentry trace.""" + + self._span_id = span_id + """The span id of the currently executing span.""" + + self.parent_span_id = parent_span_id + """The id of the parent span that started this span. + The parent span could also be a span in an upstream service.""" + + self.parent_sampled = parent_sampled + """Boolean indicator if the parent span was sampled. + Important when the parent span originated in an upstream service, + because we want to sample the whole trace, or nothing from the trace.""" + + self.baggage = baggage + """Parsed baggage header that is used for dynamic sampling decisions.""" + + """DEPRECATED this only exists for backwards compat of constructor.""" + if baggage is None and dynamic_sampling_context is not None: + self.baggage = Baggage(dynamic_sampling_context) + + self.custom_sampling_context: "Optional[dict[str, Any]]" = None + + @classmethod + def from_incoming_data( + cls, incoming_data: "Dict[str, Any]" + ) -> "PropagationContext": + propagation_context = PropagationContext() + normalized_data = normalize_incoming_data(incoming_data) + + sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) + sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) + + # nothing to propagate if no sentry-trace + if sentrytrace_data is None: + return propagation_context + + baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) + baggage = ( + Baggage.from_incoming_header(baggage_header) if baggage_header else None + ) + + if not _should_continue_trace(baggage): + return propagation_context + + propagation_context.update(sentrytrace_data) + if baggage: + propagation_context.baggage = baggage + + propagation_context._fill_sample_rand() + + return propagation_context + + @property + def trace_id(self) -> str: + """The trace id of the Sentry trace.""" + if not self._trace_id: + # New trace, don't fill in sample_rand + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + """The span id of the currently executed span.""" + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + @property + def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": + return self.get_baggage().dynamic_sampling_context() + + def to_traceparent(self) -> str: + return f"{self.trace_id}-{self.span_id}" + + def get_baggage(self) -> "Baggage": + if self.baggage is None: + self.baggage = Baggage.populate_from_propagation_context(self) + return self.baggage + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. + """ + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def update(self, other_dict: "Dict[str, Any]") -> None: + """ + Updates the PropagationContext with data from the given dictionary. + """ + for key, value in other_dict.items(): + try: + setattr(self, key, value) + except AttributeError: + pass + + def _set_custom_sampling_context( + self, custom_sampling_context: "dict[str, Any]" + ) -> None: + self.custom_sampling_context = custom_sampling_context + + def __repr__(self) -> str: + return "".format( + self._trace_id, + self._span_id, + self.parent_span_id, + self.parent_sampled, + self.baggage, + ) + + def _fill_sample_rand(self) -> None: + """ + Ensure that there is a valid sample_rand value in the baggage. + + If there is a valid sample_rand value in the baggage, we keep it. + Otherwise, we generate a sample_rand value according to the following: + + - If we have a parent_sampled value and a sample_rate in the DSC, we compute + a sample_rand value randomly in the range: + - [0, sample_rate) if parent_sampled is True, + - or, in the range [sample_rate, 1) if parent_sampled is False. + + - If either parent_sampled or sample_rate is missing, we generate a random + value in the range [0, 1). + + The sample_rand is deterministically generated from the trace_id, if present. + + This function does nothing if there is no baggage. + """ + if self.baggage is None: + return + + sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) + if sample_rand is not None and 0 <= sample_rand < 1: + # sample_rand is present and valid, so don't overwrite it + return + + # Get the sample rate and compute the transformation that will map the random value + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). + sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) + + try: + sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) + except ValueError: + # ValueError is raised if the interval is invalid, i.e. lower >= upper. + # lower >= upper might happen if the incoming trace's sampled flag + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. + # We cannot generate a sensible sample_rand value in this case. + logger.debug( + f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " + f"and sample_rate={sample_rate}." + ) + return + + self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 + + def _sample_rand(self) -> "Optional[str]": + """Convenience method to get the sample_rand value from the baggage.""" + if self.baggage is None: + return None + + return self.baggage.sentry_items.get("sample_rand") + + +class Baggage: + """ + The W3C Baggage header information (see https://www.w3.org/TR/baggage/). + + Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. + Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but + it is the caller's responsibility to enforce this restriction. + """ + + __slots__ = ("sentry_items", "third_party_items", "mutable") + + SENTRY_PREFIX = "sentry-" + SENTRY_PREFIX_REGEX = re.compile("^sentry-") + + def __init__( + self, + sentry_items: "Dict[str, str]", + third_party_items: str = "", + mutable: bool = True, + ): + self.sentry_items = sentry_items + self.third_party_items = third_party_items + self.mutable = mutable + + @classmethod + def from_incoming_header( + cls, + header: "Optional[str]", + *, + _sample_rand: "Optional[str]" = None, + ) -> "Baggage": + """ + freeze if incoming header already has sentry baggage + """ + sentry_items = {} + third_party_items = "" + mutable = True + + if header: + for item in header.split(","): + if "=" not in item: + continue + + with capture_internal_exceptions(): + item = item.strip() + key, val = item.split("=", 1) + if Baggage.SENTRY_PREFIX_REGEX.match(key): + baggage_key = unquote(key.split("-")[1]) + sentry_items[baggage_key] = unquote(val) + mutable = False + else: + third_party_items += ("," if third_party_items else "") + item + + if _sample_rand is not None: + sentry_items["sample_rand"] = str(_sample_rand) + mutable = False + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": + """ + Deprecated: use populate_from_propagation_context + """ + if scope._propagation_context is None: + return Baggage({}) + + return Baggage.populate_from_propagation_context(scope._propagation_context) + + @classmethod + def populate_from_propagation_context( + cls, propagation_context: "PropagationContext" + ) -> "Baggage": + sentry_items: "Dict[str, str]" = {} + third_party_items = "" + mutable = False + + client = sentry_sdk.get_client() + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options + + sentry_items["trace_id"] = propagation_context.trace_id + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if options.get("traces_sample_rate"): + sentry_items["sample_rate"] = str(options["traces_sample_rate"]) + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def populate_from_transaction( + cls, transaction: "sentry_sdk.tracing.Transaction" + ) -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = transaction.trace_id + sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + transaction.name + and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES + ): + sentry_items["transaction"] = transaction.name + + if transaction.sample_rate is not None: + sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction.sampled is not None: + sentry_items["sampled"] = "true" if transaction.sampled else "false" + + # there's an existing baggage but it was mutable, + # which is why we are creating this new baggage. + # However, if by chance the user put some sentry items in there, give them precedence. + if transaction._baggage and transaction._baggage.sentry_items: + sentry_items.update(transaction._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + @classmethod + def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = segment.trace_id + sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + segment.get_attributes().get("sentry.span.source") + not in LOW_QUALITY_SEGMENT_SOURCES + ) and segment._name: + sentry_items["transaction"] = segment._name + + if segment._sample_rate is not None: + sentry_items["sample_rate"] = str(segment._sample_rate) + + if segment.sampled is not None: + sentry_items["sampled"] = "true" if segment.sampled else "false" + + # There's an existing baggage but it was mutable, which is why we are + # creating this new baggage. + # However, if by chance the user put some sentry items in there, give + # them precedence. + if segment._baggage and segment._baggage.sentry_items: + sentry_items.update(segment._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + def freeze(self) -> None: + self.mutable = False + + def dynamic_sampling_context(self) -> "Dict[str, str]": + header = {} + + for key, item in self.sentry_items.items(): + header[key] = item + + return header + + def serialize(self, include_third_party: bool = False) -> str: + items = [] + + for key, val in self.sentry_items.items(): + with capture_internal_exceptions(): + item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) + items.append(item) + + if include_third_party: + items.append(self.third_party_items) + + return ",".join(items) + + @staticmethod + def strip_sentry_baggage(header: str) -> str: + """Remove Sentry baggage from the given header. + + Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. + """ + return ",".join( + ( + item + for item in header.split(",") + if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) + ) + ) + + def _sample_rand(self) -> "Optional[float]": + """Convenience method to get the sample_rand value from the sentry_items. + + We validate the value and parse it as a float before returning it. The value is considered + valid if it is a float in the range [0, 1). + """ + sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) + + if sample_rand is not None and 0.0 <= sample_rand < 1.0: + return sample_rand + + return None + + def __repr__(self) -> str: + return f'' + + +def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: + """ + Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. + """ + trace_propagation_targets = client.options["trace_propagation_targets"] + + if is_sentry_url(client, url): + return False + + return match_regex_list(url, trace_propagation_targets, substring_matching=True) + + +def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": + """ + Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. + """ + data = {} + for key, value in incoming_data.items(): + if key.startswith("HTTP_"): + key = key[5:] + + key = key.replace("_", "-").lower() + data[key] = value + + return data + + +def create_span_decorator( + op: "Optional[Union[str, OP]]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Any": + """ + Create a span decorator that can wrap both sync and async functions. + + :param op: The operation type for the span. + :type op: str or :py:class:`sentry_sdk.consts.OP` or None + :param name: The name of the span. + :type name: str or None + :param attributes: Additional attributes to set on the span. + :type attributes: dict or None + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + """ + from sentry_sdk.scope import should_send_default_pii + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return await f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return await f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = await f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def create_streaming_span_decorator( + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Any": + """ + Create a span creating decorator that can wrap both sync and async functions. + """ + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + result = await f(*args, **kwargs) + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + return f(*args, **kwargs) + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.span + return current_span + + +def _generate_sample_rand( + trace_id: "Optional[str]", + *, + interval: "tuple[float, float]" = (0.0, 1.0), +) -> float: + """Generate a sample_rand value from a trace ID. + + The generated value will be pseudorandomly chosen from the provided + interval. Specifically, given (lower, upper) = interval, the generated + value will be in the range [lower, upper). The value has 6-digit precision, + so when printing with .6f, the value will never be rounded up. + + The pseudorandom number generator is seeded with the trace ID. + """ + lower, upper = interval + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly + raise ValueError("Invalid interval: lower must be less than upper") + + rng = Random(trace_id) + lower_scaled = int(lower * 1_000_000) + upper_scaled = int(upper * 1_000_000) + try: + sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) + except ValueError: + # In some corner cases it might happen that the range is too small + # In that case, just take the lower bound + sample_rand_scaled = lower_scaled + + return sample_rand_scaled / 1_000_000 + + +def _sample_rand_range( + parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" +) -> "tuple[float, float]": + """ + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values + that a generated sample_rand value must fall into, given the parent_sampled and + sample_rate values. + """ + if parent_sampled is None or sample_rate is None: + return 0.0, 1.0 + elif parent_sampled is True: + return 0.0, sample_rate + else: # parent_sampled is False + return sample_rate, 1.0 + + +def _get_value(source: "Any", key: str) -> "Optional[Any]": + """ + Gets a value from a source object. The source can be a dict or an object. + It is checked for dictionary keys and object attributes. + """ + value = None + if isinstance(source, dict): + value = source.get(key) + else: + if hasattr(source, key): + try: + value = getattr(source, key) + except Exception: + value = None + return value + + +def _get_span_name( + template: "Union[str, SPANTEMPLATE]", + name: str, + kwargs: "Optional[dict[str, Any]]" = None, +) -> str: + """ + Get the name of the span based on the template and the name. + """ + span_name = name + + if template == SPANTEMPLATE.AI_CHAT: + model = None + if kwargs: + for key in ("model", "model_name"): + if kwargs.get(key) and isinstance(kwargs[key], str): + model = kwargs[key] + break + + span_name = f"chat {model}" if model else "chat" + + elif template == SPANTEMPLATE.AI_AGENT: + span_name = f"invoke_agent {name}" + + elif template == SPANTEMPLATE.AI_TOOL: + span_name = f"execute_tool {name}" + + return span_name + + +def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: + """ + Get the operation of the span based on the template. + """ + mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { + SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, + SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, + SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, + } + op = mapping.get(template, OP.FUNCTION) + + return str(op) + + +def _get_input_attributes( + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Get input attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + mapping = { + "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), + "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), + "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), + "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), + "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), + "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), + "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), + "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), + } + + def _set_from_key(key: str, value: "Any") -> None: + if key in mapping: + (attribute, data_type) = mapping[key] + if value is not None and isinstance(value, data_type): + attributes[attribute] = value + + for key, value in list(kwargs.items()): + if key == "prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "user", "content": value} + ) + continue + + if key == "system_prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "system", "content": value} + ) + continue + + _set_from_key(key, value) + + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( + {"args": args, "kwargs": kwargs} + ) + + # Coerce to string + if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] + ) + + return attributes + + +def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": + """ + Get usage attributes. + """ + attributes = {} + + def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: + for key in keys: + value = _get_value(usage, key) + if value is not None and isinstance(value, int): + attributes[attribute] = value + + _set_from_keys( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + ("prompt_tokens", "input_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + ("completion_tokens", "output_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + ("total_tokens",), + ) + + return attributes + + +def _get_output_attributes( + template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> "dict[str, Any]": + """ + Get output attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + with capture_internal_exceptions(): + # Usage from result, result.usage, and result.metadata.usage + usage_candidates = [result] + + usage = _get_value(result, "usage") + usage_candidates.append(usage) + + meta = _get_value(result, "metadata") + usage = _get_value(meta, "usage") + usage_candidates.append(usage) + + for usage_candidate in usage_candidates: + if usage_candidate is not None: + attributes.update(_get_usage_attributes(usage_candidate)) + + # Response model + model_name = _get_value(result, "model") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + model_name = _get_value(result, "model_name") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + # Tool output + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) + + return attributes + + +def _set_input_attributes( + span: "Span", + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + name: str, + f: "Any", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> None: + """ + Set span input attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param f: The wrapped function. + :param args: The arguments to the wrapped function. + :param kwargs: The keyword arguments to the wrapped function. + """ + attributes: "dict[str, Any]" = {} + + if template == SPANTEMPLATE.AI_AGENT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_AGENT_NAME: name, + } + elif template == SPANTEMPLATE.AI_CHAT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + } + elif template == SPANTEMPLATE.AI_TOOL: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: name, + } + + docstring = f.__doc__ + if docstring is not None: + attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring + + attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) + span.update_data(attributes or {}) + + +def _set_output_attributes( + span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> None: + """ + Set span output attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param result: The result of the wrapped function. + """ + span.update_data(_get_output_attributes(template, send_pii, result) or {}) + + +def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: + """ + Check if we should continue the incoming trace according to the strict_trace_continuation spec. + https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation + """ + + client = sentry_sdk.get_client() + parsed_dsn = client.parsed_dsn + client_org_id = parsed_dsn.org_id if parsed_dsn else None + baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None + + if ( + client_org_id is not None + and baggage_org_id is not None + and client_org_id != baggage_org_id + ): + logger.debug( + f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + strict_trace_continuation: bool = client.options.get( + "strict_trace_continuation", False + ) + if strict_trace_continuation: + if (baggage_org_id is not None and client_org_id is None) or ( + baggage_org_id is None and client_org_id is not None + ): + logger.debug( + f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + return True + + +def add_sentry_baggage_to_headers( + headers: "MutableMapping[str, str]", sentry_baggage: str +) -> None: + """Add the Sentry baggage to the headers. + + This function directly mutates the provided headers. The provided sentry_baggage + is appended to the existing baggage. If the baggage already contains Sentry items, + they are stripped out first. + """ + existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") + stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) + + separator = "," if len(stripped_existing_baggage) > 0 else "" + + headers[BAGGAGE_HEADER_NAME] = ( + stripped_existing_baggage + separator + sentry_baggage + ) + + +def _make_sampling_decision( + name: str, + attributes: "Optional[Attributes]", + scope: "sentry_sdk.Scope", +) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": + """ + Decide whether a span should be sampled. + + Returns a tuple with: + 1. the sampling decision + 2. the effective sample rate + 3. the sample rand + 4. the reason for not sampling the span, if unsampled + """ + client = sentry_sdk.get_client() + + if not has_tracing_enabled(client.options): + return False, None, None, None + + propagation_context = scope.get_active_propagation_context() + + sample_rand = None + if propagation_context.baggage is not None: + sample_rand = propagation_context.baggage._sample_rand() + if sample_rand is None: + sample_rand = _generate_sample_rand(propagation_context.trace_id) + + # If there's a traces_sampler, use that; otherwise use traces_sample_rate + traces_sampler_defined = callable(client.options.get("traces_sampler")) + if traces_sampler_defined: + sampling_context = { + "span_context": { + "name": name, + "trace_id": propagation_context.trace_id, + "parent_span_id": propagation_context.parent_span_id, + "parent_sampled": propagation_context.parent_sampled, + "attributes": dict(attributes) if attributes else {}, + }, + } + + if propagation_context.custom_sampling_context: + sampling_context.update(propagation_context.custom_sampling_context) + + sample_rate = client.options["traces_sampler"](sampling_context) + else: + if propagation_context.parent_sampled is not None: + sample_rate = propagation_context.parent_sampled + else: + sample_rate = client.options["traces_sample_rate"] + + # Validate whether the sample_rate we got is actually valid. Since + # traces_sampler is user-provided, it could return anything. + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") + return False, None, None, "sample_rate" + + sample_rate = float(sample_rate) + if not sample_rate: + if traces_sampler_defined: + reason = "traces_sampler returned 0 or False" + else: + reason = "traces_sample_rate is set to 0" + + logger.debug(f"[Tracing] Discarding {name} because {reason}") + return False, 0.0, None, "sample_rate" + + # Adjust sample rate if we're under backpressure + sample_rate_before_backpressure = sample_rate + if client.monitor: + sample_rate /= 2**client.monitor.downsample_factor + + if not sample_rate: + logger.debug(f"[Tracing] Discarding {name} because backpressure") + return False, 0.0, None, "backpressure" + + # Make the actual decision + sampled = sample_rand < sample_rate + + if sampled: + logger.debug(f"[Tracing] Starting {name}") + outcome = None + + else: + # Determine why exactly the span will not be sampled. If we've lowered + # the effective sample_rate because of backpressure, check whether the + # span would've been sampled if backpressure wasn't active. If that's the + # case, backpressure is the actual reason, otherwise just pure sampling + # rate. + if ( + sample_rate_before_backpressure != sample_rate + and sample_rand < sample_rate_before_backpressure + ): + logger.debug(f"[Tracing] Discarding {name} because backpressure") + outcome = "backpressure" + + else: + logger.debug( + f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" + ) + outcome = "sample_rate" + + return sampled, sample_rate, sample_rand, outcome + + +def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: + """Determine if a span fits one of the rules in ignore_spans.""" + client = sentry_sdk.get_client() + ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") + + if not ignore_spans: + return False + + def _matches(rule: "Any", value: "Any") -> bool: + if isinstance(rule, Pattern): + if isinstance(value, str): + return bool(rule.fullmatch(value)) + else: + return False + + return rule == value + + for rule in ignore_spans: + if isinstance(rule, (str, Pattern)): + if _matches(rule, name): + return True + + elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): + name_matches = True + attributes_match = True + + if "name" in rule: + name_matches = _matches(rule["name"], name) + + if "attributes" in rule: + attributes = attributes or {} + + for attribute, value in rule["attributes"].items(): + if attribute not in attributes or not _matches( + value, attributes[attribute] + ): + attributes_match = False + break + + if name_matches and attributes_match: + return True + + return False + + +# Circular imports +from sentry_sdk.traces import ( + LOW_QUALITY_SEGMENT_SOURCES, + StreamedSpan, +) +from sentry_sdk.traces import ( + start_span as start_streaming_span, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + LOW_QUALITY_TRANSACTION_SOURCES, + SENTRY_TRACE_HEADER_NAME, + Span, +) + +if TYPE_CHECKING: + from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py new file mode 100644 index 0000000000..2b71fee429 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py @@ -0,0 +1,1255 @@ +import asyncio +import gzip +import io +import json +import logging +import os +import socket +import ssl +import time +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from urllib.request import getproxies + +try: + import brotli # type: ignore +except ImportError: + brotli = None + +try: + import httpcore +except ImportError: + httpcore = None # type: ignore[assignment,unused-ignore] + +try: + import h2 # noqa: F401 + + HTTP2_ENABLED = httpcore is not None +except ImportError: + HTTP2_ENABLED = False + +try: + import anyio # noqa: F401 + + ASYNC_TRANSPORT_AVAILABLE = httpcore is not None +except ImportError: + ASYNC_TRANSPORT_AVAILABLE = False + +from typing import TYPE_CHECKING, Dict, List, cast + +import certifi +import urllib3 + +import sentry_sdk +from sentry_sdk.consts import EndpointType +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + logger, + mark_sentry_task_internal, +) +from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + DefaultDict, + Iterable, + Mapping, + Optional, + Self, + Tuple, + Type, + Union, + ) + + from urllib3.poolmanager import PoolManager, ProxyManager + + from sentry_sdk._types import Event, EventDataCategory + +KEEP_ALIVE_SOCKET_OPTIONS = [] +for option in [ + (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 +]: + try: + KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) + except AttributeError: + # a specific option might not be available on specific systems, + # e.g. TCP_KEEPIDLE doesn't exist on macOS + pass + + +def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": + """Case-insensitive header lookup for httpcore-style responses.""" + header_lower = header.lower() + return next( + ( + val.decode("ascii") + for key, val in response.headers + if key.decode("ascii").lower() == header_lower + ), + None, + ) + + +class Transport(ABC): + """Baseclass for all transports. + + A transport is used to send an event to sentry. + """ + + parsed_dsn: "Optional[Dsn]" = None + + def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: + self.options = options + if options and options["dsn"]: + self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) + else: + self.parsed_dsn = None + + def capture_event(self: "Self", event: "Event") -> None: + """ + DEPRECATED: Please use capture_envelope instead. + + This gets invoked with the event dictionary when an event should + be sent to sentry. + """ + + warnings.warn( + "capture_event is deprecated, please use capture_envelope instead!", + DeprecationWarning, + stacklevel=2, + ) + + envelope = Envelope() + envelope.add_event(event) + self.capture_envelope(envelope) + + @abstractmethod + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + """ + Send an envelope to Sentry. + + Envelopes are a data container format that can hold any type of data + submitted to Sentry. We use it to send all event data (including errors, + transactions, crons check-ins, etc.) to Sentry. + """ + pass + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Any]" = None, + ) -> None: + """ + Wait `timeout` seconds for the current events to be sent out. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def kill(self: "Self") -> None: + """ + Forcefully kills the transport. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + """This increments a counter for event loss by reason and + data category by the given positive-int quantity (default 1). + + If an item is provided, the data category and quantity are + extracted from the item, and the values passed for + data_category and quantity are ignored. + + When recording a lost transaction via data_category="transaction", + the calling code should also record the lost spans via this method. + When recording lost spans, `quantity` should be set to the number + of contained spans, plus one for the transaction itself. When + passing an Item containing a transaction via the `item` parameter, + this method automatically records the lost spans. + """ + return None + + def is_healthy(self: "Self") -> bool: + return True + + +def _parse_rate_limits( + header: str, now: "Optional[datetime]" = None +) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": + if now is None: + now = datetime.now(timezone.utc) + + for limit in header.split(","): + try: + parameters = limit.strip().split(":") + retry_after_val, categories = parameters[:2] + + retry_after = now + timedelta(seconds=int(retry_after_val)) + for category in categories and categories.split(";") or (None,): + yield category, retry_after # type: ignore + except (LookupError, ValueError): + continue + + +class HttpTransportCore(Transport): + """Shared base class for sync and async transports.""" + + TIMEOUT = 30 # seconds + + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + from sentry_sdk.consts import VERSION + + Transport.__init__(self, options) + assert self.parsed_dsn is not None + self.options: "Dict[str, Any]" = options + self._worker = self._create_worker(options) + self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) + self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} + # We only use this Retry() class for the `get_retry_after` method it exposes + self._retry = urllib3.util.Retry() + self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( + defaultdict(int) + ) + self._last_client_report_sent = time.time() + + self._pool = self._make_pool() + + # Backwards compatibility for deprecated `self.hub_class` attribute + self._hub_cls = sentry_sdk.Hub + + experiments = options.get("_experiments", {}) + compression_level = experiments.get( + "transport_compression_level", + experiments.get("transport_zlib_compression_level"), + ) + compression_algo = experiments.get( + "transport_compression_algo", + ( + "gzip" + # if only compression level is set, assume gzip for backwards compatibility + # if we don't have brotli available, fallback to gzip + if compression_level is not None or brotli is None + else "br" + ), + ) + + if compression_algo == "br" and brotli is None: + logger.warning( + "You asked for brotli compression without the Brotli module, falling back to gzip -9" + ) + compression_algo = "gzip" + compression_level = None + + if compression_algo not in ("br", "gzip"): + logger.warning( + "Unknown compression algo %s, disabling compression", compression_algo + ) + self._compression_level = 0 + self._compression_algo = None + else: + self._compression_algo = compression_algo + + if compression_level is not None: + self._compression_level = compression_level + elif self._compression_algo == "gzip": + self._compression_level = 9 + elif self._compression_algo == "br": + self._compression_level = 4 + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + raise NotImplementedError() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + if not self.options["send_client_reports"]: + return + + if item is not None: + data_category = item.data_category + quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). + + if data_category == "transaction": + # Also record the lost spans + event = item.get_transaction_event() or {} + + # +1 for the transaction itself + span_count = ( + len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 + ) + self.record_lost_event(reason, "span", quantity=span_count) + + elif data_category == "log_item" and item: + # Also record size of lost logs in bytes + bytes_size = len(item.get_bytes()) + self.record_lost_event(reason, "log_byte", quantity=bytes_size) + + elif data_category == "attachment": + # quantity of 0 is actually 1 as we do not want to count + # empty attachments as actually empty. + quantity = len(item.get_bytes()) or 1 + + elif data_category is None: + raise TypeError("data category not provided") + + self._discarded_events[data_category, reason] += quantity + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return response.headers.get(header) + + def _update_rate_limits( + self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" + ) -> None: + # new sentries with more rate limit insights. We honor this header + # no matter of the status code to update our internal rate limits. + header = self._get_header_value(response, "x-sentry-rate-limits") + if header: + logger.warning("Rate-limited via x-sentry-rate-limits") + self._disabled_until.update(_parse_rate_limits(header)) + + # old sentries only communicate global rate limit hits via the + # retry-after header on 429. This header can also be emitted on new + # sentries if a proxy in front wants to globally slow things down. + elif response.status == 429: + logger.warning("Rate-limited via 429") + retry_after_value = self._get_header_value(response, "Retry-After") + retry_after = ( + self._retry.parse_retry_after(retry_after_value) + if retry_after_value is not None + else None + ) or 60 + self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( + seconds=retry_after + ) + + def _handle_request_error( + self: "Self", + envelope: "Optional[Envelope]", + loss_reason: str = "network", + record_reason: str = "network_error", + ) -> None: + def record_loss(reason: str) -> None: + if envelope is None: + self.record_lost_event(reason, data_category="error") + else: + for item in envelope.items: + self.record_lost_event(reason, item=item) + + self.on_dropped_event(loss_reason) + record_loss(record_reason) + + def _handle_response( + self: "Self", + response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", + envelope: "Optional[Envelope]", + ) -> None: + self._update_rate_limits(response) + + if response.status == 413: + size_exceeded_message = ( + "HTTP 413: Event dropped due to exceeded envelope size limit" + ) + response_message = getattr( + response, "data", getattr(response, "content", None) + ) + if response_message is not None: + size_exceeded_message += f" (body: {response_message})" + + logger.error(size_exceeded_message) + self._handle_request_error( + envelope=envelope, loss_reason="status_413", record_reason="send_error" + ) + + elif response.status == 429: + # if we hit a 429. Something was rate limited but we already + # acted on this in `self._update_rate_limits`. Note that we + # do not want to record event loss here as we will have recorded + # an outcome in relay already. + self.on_dropped_event("status_429") + pass + + elif response.status >= 300 or response.status < 200: + logger.error( + "Unexpected status code: %s (body: %s)", + response.status, + getattr(response, "data", getattr(response, "content", None)), + ) + self._handle_request_error( + envelope=envelope, loss_reason="status_{}".format(response.status) + ) + + def _update_headers( + self: "Self", + headers: "Dict[str, str]", + ) -> None: + headers.update( + { + "User-Agent": str(self._auth.client), + "X-Sentry-Auth": str(self._auth.to_header()), + } + ) + + def on_dropped_event(self: "Self", _reason: str) -> None: + return None + + def _fetch_pending_client_report( + self: "Self", force: bool = False, interval: int = 60 + ) -> "Optional[Item]": + if not self.options["send_client_reports"]: + return None + + if not (force or self._last_client_report_sent < time.time() - interval): + return None + + discarded_events = self._discarded_events + self._discarded_events = defaultdict(int) + self._last_client_report_sent = time.time() + + if not discarded_events: + return None + + return Item( + PayloadRef( + json={ + "timestamp": time.time(), + "discarded_events": [ + {"reason": reason, "category": category, "quantity": quantity} + for ( + (category, reason), + quantity, + ) in discarded_events.items() + ], + } + ), + type="client_report", + ) + + def _check_disabled(self, category: str) -> bool: + def _disabled(bucket: "Any") -> bool: + ts = self._disabled_until.get(bucket) + return ts is not None and ts > datetime.now(timezone.utc) + + return _disabled(category) or _disabled(None) + + def _is_rate_limited(self: "Self") -> bool: + return any( + ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() + ) + + def _is_worker_full(self: "Self") -> bool: + return self._worker.full() + + def is_healthy(self: "Self") -> bool: + return not (self._is_worker_full() or self._is_rate_limited()) + + def _prepare_envelope( + self: "Self", envelope: "Envelope" + ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": + # remove all items from the envelope which are over quota + new_items = [] + for item in envelope.items: + if self._check_disabled(item.data_category): + if item.data_category in ("transaction", "error", "default", "statsd"): + self.on_dropped_event("self_rate_limits") + self.record_lost_event("ratelimit_backoff", item=item) + else: + new_items.append(item) + + # Since we're modifying the envelope here make a copy so that others + # that hold references do not see their envelope modified. + envelope = Envelope(headers=envelope.headers, items=new_items) + + if not envelope.items: + return None + + # since we're already in the business of sending out an envelope here + # check if we have one pending for the stats session envelopes so we + # can attach it to this enveloped scheduled for sending. This will + # currently typically attach the client report to the most recent + # session update. + client_report_item = self._fetch_pending_client_report(interval=30) + if client_report_item is not None: + envelope.items.append(client_report_item) + + content_encoding, body = self._serialize_envelope(envelope) + + assert self.parsed_dsn is not None + logger.debug( + "Sending envelope [%s] project:%s host:%s", + envelope.description, + self.parsed_dsn.project_id, + self.parsed_dsn.host, + ) + + headers: "Dict[str, str]" = { + "Content-Type": "application/x-sentry-envelope", + } + if content_encoding: + headers["Content-Encoding"] = content_encoding + + return envelope, body, headers + + def _serialize_envelope( + self: "Self", envelope: "Envelope" + ) -> "tuple[Optional[str], io.BytesIO]": + content_encoding = None + body = io.BytesIO() + if self._compression_level == 0 or self._compression_algo is None: + envelope.serialize_into(body) + else: + content_encoding = self._compression_algo + if self._compression_algo == "br" and brotli is not None: + body.write( + brotli.compress( + envelope.serialize(), quality=self._compression_level + ) + ) + else: # assume gzip as we sanitize the algo value in init + with gzip.GzipFile( + fileobj=body, mode="w", compresslevel=self._compression_level + ) as f: + envelope.serialize_into(f) + + return content_encoding, body + + def _get_httpcore_pool_options( + self: "Self", http2: bool = False + ) -> "Dict[str, Any]": + """Shared pool options for httpcore-based transports (Http2 and Async).""" + options: "Dict[str, Any]" = { + "http2": http2, + "retries": 3, + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations( + self.options["ca_certs"] + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") + key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") + if cert_file is not None: + ssl_context.load_cert_chain(cert_file, key_file) + + options["ssl_context"] = ssl_context + return options + + def _resolve_proxy(self: "Self") -> "Optional[str]": + """Resolve proxy URL from options and environment. Returns proxy URL or None.""" + if self.parsed_dsn is None: + return None + + no_proxy = self._in_no_proxy(self.parsed_dsn) + proxy = None + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + return proxy or None + + @property + def _timeout_extensions(self: "Self") -> "Dict[str, Any]": + return { + "timeout": { + "pool": self.TIMEOUT, + "connect": self.TIMEOUT, + "write": self.TIMEOUT, + "read": self.TIMEOUT, + } + } + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + raise NotImplementedError() + + def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: + no_proxy = getproxies().get("no") + if not no_proxy: + return False + for host in no_proxy.split(","): + host = host.strip() + if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): + return True + return False + + def _make_pool( + self: "Self", + ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + raise NotImplementedError() + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": + raise NotImplementedError() + + def kill(self: "Self") -> None: + logger.debug("Killing HTTP transport") + self._worker.kill() + + +# Keep BaseHttpTransport as an alias for backwards compatibility +# and for the sync transport implementation +class BaseHttpTransport(HttpTransportCore): + """The base HTTP transport (synchronous).""" + + def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + response.close() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return BackgroundWorker(queue_size=options["transport_queue_size"]) + + def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def capture_envelope( + self, + envelope: "Envelope", + ) -> None: + def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + self._send_envelope(envelope) + self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + self._worker.flush(timeout, callback) + + @staticmethod + def _warn_hub_cls() -> None: + """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" + warnings.warn( + "The `hub_cls` attribute is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=3, + ) + + @property + def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + return self._hub_cls + + @hub_cls.setter + def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + self._hub_cls = value + + +class HttpTransport(BaseHttpTransport): + if TYPE_CHECKING: + _pool: "Union[PoolManager, ProxyManager]" + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + num_pools = self.options.get("_experiments", {}).get("transport_num_pools") + options = { + "num_pools": 2 if num_pools is None else int(num_pools), + "cert_reqs": "CERT_REQUIRED", + "timeout": urllib3.Timeout(total=self.TIMEOUT), + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if self.options["keep_alive"]: + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + options["ca_certs"] = ( + self.options["ca_certs"] # User-provided bundle from the SDK init + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + + options["cert_file"] = self.options["cert_file"] or os.environ.get( + "CLIENT_CERT_FILE" + ) + options["key_file"] = self.options["key_file"] or os.environ.get( + "CLIENT_KEY_FILE" + ) + + return options + + def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = None + no_proxy = self._in_no_proxy(self.parsed_dsn) + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + use_socks_proxy = True + try: + # Check if PySocks dependency is available + from urllib3.contrib.socks import SOCKSProxyManager + except ImportError: + use_socks_proxy = False + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", + proxy, + ) + + if use_socks_proxy: + return SOCKSProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + else: + return urllib3.ProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "urllib3.BaseHTTPResponse": + return self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + body=body, + headers=headers, + ) + + +class AsyncHttpTransport(HttpTransportCore): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + if not ASYNC_TRANSPORT_AVAILABLE: + raise RuntimeError( + "AsyncHttpTransport requires httpcore[asyncio]. " + "Install it with: pip install sentry-sdk[asyncio]" + ) + super().__init__(options) + # Requires event loop at init time + self.loop = asyncio.get_running_loop() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return AsyncWorker(queue_size=options["transport_queue_size"]) + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + async def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + await self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + async def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = await self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + await response.aclose() + + async def _request( # type: ignore[override] + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + return await self._pool.request( # type: ignore[misc,unused-ignore] + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + + async def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def _capture_envelope(self: "Self", envelope: "Envelope") -> None: + async def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + await self._send_envelope(envelope) + await self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + # Synchronous entry point + if self.loop and self.loop.is_running(): + self.loop.call_soon_threadsafe(self._capture_envelope, envelope) + else: + # The event loop is no longer running + logger.warning("Async Transport is not running in an event loop.") + self.on_dropped_event("internal_sdk_error") + for item in envelope.items: + self.record_lost_event("internal_sdk_error", item=item) + + def flush( # type: ignore[override] + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> "Optional[asyncio.Task[None]]": + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] + return None + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=HTTP2_ENABLED + and self.parsed_dsn is not None + and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + socks_opts = opts.copy() + if "socket_options" in socks_opts: + socket_options = socks_opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) + + return httpcore.AsyncConnectionPool(**opts) + + def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] + logger.debug("Killing HTTP transport") + self._worker.kill() + try: + # Return the pool cleanup task so caller can await it if needed + with mark_sentry_task_internal(): + return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] + except RuntimeError: + logger.warning("Event loop not running, aborting kill.") + return None + + +if not HTTP2_ENABLED: + # Sorry, no Http2Transport for you + class Http2Transport(HttpTransport): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + super().__init__(options) + logger.warning( + "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." + ) + +else: + + class Http2Transport(BaseHttpTransport): # type: ignore + """The HTTP2 transport based on httpcore.""" + + TIMEOUT = 15 + + if TYPE_CHECKING: + _pool: """Union[ + httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool + ]""" + + def _get_header_value( + self: "Self", response: "httpcore.Response", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + response = self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + return response + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + if "socket_options" in opts: + socket_options = opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.SOCKSProxy(proxy_url=proxy, **opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.HTTPProxy(proxy_url=proxy, **opts) + + return httpcore.ConnectionPool(**opts) + + +class _EnvelopePrinterTransport(Transport): + """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" + + def __init__(self, transport: "Transport") -> None: + Transport.__init__(self, options=transport.options) + self._inner = transport + self.parsed_dsn = transport.parsed_dsn + + self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") + self.envelope_logger.setLevel(logging.INFO) + self.envelope_logger.propagate = False + if not self.envelope_logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + self.envelope_logger.addHandler(handler) + + @property # type: ignore[misc] + def __class__(self) -> type: + return self._inner.__class__ + + def capture_envelope(self, envelope: "Envelope") -> None: + try: + self.envelope_logger.info("--- Sentry Envelope ---") + self.envelope_logger.info( + "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) + ) + for item in envelope.items: + self.envelope_logger.info(" Item type: %s", item.type) + self.envelope_logger.info( + " Item headers: %s", + json.dumps(item.headers, indent=2, default=str), + ) + try: + payload = json.loads(item.get_bytes()) + self.envelope_logger.info( + " Payload:\n%s", + json.dumps(payload, indent=2, default=str), + ) + except (ValueError, TypeError): + self.envelope_logger.info( + " Payload: ", + len(item.get_bytes()), + ) + self.envelope_logger.info("--- End Envelope ---") + except Exception: + pass + + self._inner.capture_envelope(envelope) + + def flush( + self, + timeout: float, + callback: "Optional[Any]" = None, + ) -> "Any": + return self._inner.flush(timeout, callback) + + def kill(self) -> "Any": + return self._inner.kill() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + self._inner.record_lost_event(reason, data_category, item, quantity=quantity) + + def is_healthy(self) -> bool: + return self._inner.is_healthy() + + def __getattr__(self, name: str) -> "Any": + return getattr(self._inner, name) + + +class _FunctionTransport(Transport): + """ + DEPRECATED: Users wishing to provide a custom transport should subclass + the Transport class, rather than providing a function. + """ + + def __init__( + self, + func: "Callable[[Event], None]", + ) -> None: + Transport.__init__(self) + self._func = func + + def capture_event( + self, + event: "Event", + ) -> None: + self._func(event) + return None + + def capture_envelope(self, envelope: "Envelope") -> None: + # Since function transports expect to be called with an event, we need + # to iterate over the envelope and call the function for each event, via + # the deprecated capture_event method. + event = envelope.get_event() + if event is not None: + self.capture_event(event) + + +def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": + ref_transport = options["transport"] + + use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) + use_async_transport = options.get("_experiments", {}).get("transport_async", False) + async_integration = any( + integration.__class__.__name__ == "AsyncioIntegration" + for integration in options.get("integrations") or [] + ) + + # By default, we use the http transport class + transport_cls: "Type[Transport]" = ( + Http2Transport if use_http2_transport else HttpTransport + ) + + if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: + try: + asyncio.get_running_loop() + if async_integration: + if use_http2_transport: + logger.warning( + "HTTP/2 transport is not supported with async transport. " + "Ignoring transport_http2 experiment." + ) + transport_cls = AsyncHttpTransport + else: + logger.warning( + "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." + ) + except RuntimeError: + # No event loop running, fall back to sync transport + logger.warning("No event loop running, falling back to sync transport.") + elif use_async_transport: + logger.warning( + "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." + ) + + transport: "Optional[Transport]" = None + + if isinstance(ref_transport, Transport): + transport = ref_transport + elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): + transport_cls = ref_transport + elif callable(ref_transport): + warnings.warn( + "Function transports are deprecated and will be removed in a future release." + "Please provide a Transport instance or subclass, instead.", + DeprecationWarning, + stacklevel=2, + ) + transport = _FunctionTransport(ref_transport) + + # if a transport class is given only instantiate it if the dsn is not + # empty or None + if transport is None and options["dsn"]: + transport = transport_cls(options) + + if transport is not None and os.environ.get( + "SENTRY_PRINT_ENVELOPES", "" + ).lower() in ("1", "true", "yes"): + transport = _EnvelopePrinterTransport(transport) + + return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py new file mode 100644 index 0000000000..dff91e3719 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py @@ -0,0 +1,52 @@ +""" +This module contains type definitions for the Sentry SDK's public API. +The types are re-exported from the internal module `sentry_sdk._types`. + +Disclaimer: Since types are a form of documentation, type definitions +may change in minor releases. Removing a type would be considered a +breaking change, and so we will only remove type definitions in major +releases. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Re-export types to make them available in the public API + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + EventDataCategory, + Hint, + Log, + Metric, + MonitorConfig, + SamplingContext, + ) +else: + from typing import Any + + # The lines below allow the types to be imported from outside `if TYPE_CHECKING` + # guards. The types in this module are only intended to be used for type hints. + Breadcrumb = Any + BreadcrumbHint = Any + Event = Any + EventDataCategory = Any + Hint = Any + Log = Any + MonitorConfig = Any + SamplingContext = Any + Metric = Any + + +__all__ = ( + "Breadcrumb", + "BreadcrumbHint", + "Event", + "EventDataCategory", + "Hint", + "Log", + "MonitorConfig", + "SamplingContext", + "Metric", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py new file mode 100644 index 0000000000..0963015351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py @@ -0,0 +1,2178 @@ +import base64 +import copy +import json +import linecache +import logging +import math +import os +import random +import re +import subprocess +import sys +import threading +import time +from collections import namedtuple +from contextlib import contextmanager +from datetime import datetime, timezone +from decimal import Decimal +from functools import partial, partialmethod, wraps +from numbers import Real +from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit + +try: + # Python 3.11 + from builtins import BaseExceptionGroup +except ImportError: + # Python 3.10 and below + BaseExceptionGroup = None # type: ignore + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._compat import PY37 +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue +from sentry_sdk.consts import ( + DEFAULT_ADD_FULL_STACK, + DEFAULT_MAX_STACK_FRAMES, + EndpointType, +) + +if TYPE_CHECKING: + from types import FrameType, TracebackType + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Iterator, + List, + NoReturn, + Optional, + ParamSpec, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from gevent.hub import Hub + + from sentry_sdk._types import ( + AttributeValue, + Event, + ExcInfo, + Hint, + Log, + Metric, + SerializedAttributeValue, + SpanJSON, + ) + + P = ParamSpec("P") + R = TypeVar("R") + + +epoch = datetime(1970, 1, 1) + +# The logger is created here but initialized in the debug support module +logger = logging.getLogger("sentry_sdk.errors") + +_installed_modules = None + +BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") + +FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) +TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) + +MAX_STACK_FRAMES = 2000 +"""Maximum number of stack frames to send to Sentry. + +If we have more than this number of stack frames, we will stop processing +the stacktrace to avoid getting stuck in a long-lasting loop. This value +exceeds the default sys.getrecursionlimit() of 1000, so users will only +be affected by this limit if they have a custom recursion limit. +""" + + +def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": + """Casts an ENV variable value to boolean using the constants defined above. + In strict mode, it may return None if the value doesn't match any of the predefined values. + """ + normalized = str(value).lower() if value is not None else None + + if normalized in FALSY_ENV_VALUES: + return False + + if normalized in TRUTHY_ENV_VALUES: + return True + + return None if strict else bool(value) + + +def json_dumps(data: "Any") -> bytes: + """Serialize data into a compact JSON representation encoded as UTF-8.""" + return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") + + +def get_git_revision() -> "Optional[str]": + try: + with open(os.path.devnull, "w+") as null: + # prevent command prompt windows from popping up on windows + startupinfo = None + if sys.platform == "win32" or sys.platform == "cygwin": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + revision = ( + subprocess.Popen( + ["git", "rev-parse", "HEAD"], + startupinfo=startupinfo, + stdout=subprocess.PIPE, + stderr=null, + stdin=null, + ) + .communicate()[0] + .strip() + .decode("utf-8") + ) + except (OSError, IOError, FileNotFoundError): + return None + + return revision + + +def get_default_release() -> "Optional[str]": + """Try to guess a default release.""" + release = os.environ.get("SENTRY_RELEASE") + if release: + return release + + release = get_git_revision() + if release: + return release + + for var in ( + "HEROKU_BUILD_COMMIT", + "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility + "SOURCE_VERSION", + "CODEBUILD_RESOLVED_SOURCE_VERSION", + "CIRCLE_SHA1", + "GAE_DEPLOYMENT_ID", + "K_REVISION", + ): + release = os.environ.get(var) + if release: + return release + return None + + +def get_sdk_name(installed_integrations: "List[str]") -> str: + """Return the SDK name including the name of the used web framework.""" + + # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier + # here because if django is not installed the integration is not accessible. + framework_integrations = [ + "django", + "flask", + "fastapi", + "bottle", + "falcon", + "quart", + "sanic", + "starlette", + "litestar", + "starlite", + "chalice", + "serverless", + "pyramid", + "tornado", + "aiohttp", + "aws_lambda", + "gcp", + "beam", + "asgi", + "wsgi", + ] + + for integration in framework_integrations: + if integration in installed_integrations: + return "sentry.python.{}".format(integration) + + return "sentry.python" + + +class CaptureInternalException: + __slots__ = () + + def __enter__(self) -> "ContextManager[Any]": + return self + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> bool: + if ty is not None and value is not None: + capture_internal_exception((ty, value, tb)) + + return True + + +_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() + + +def capture_internal_exceptions() -> "ContextManager[Any]": + return _CAPTURE_INTERNAL_EXCEPTION + + +def capture_internal_exception(exc_info: "ExcInfo") -> None: + """ + Capture an exception that is likely caused by a bug in the SDK + itself. + + These exceptions do not end up in Sentry and are just logged instead. + """ + if sentry_sdk.get_client().is_active(): + logger.error("Internal error in sentry_sdk", exc_info=exc_info) + + +def to_timestamp(value: "datetime") -> float: + return (value - epoch).total_seconds() + + +def format_timestamp(value: "datetime") -> str: + """Formats a timestamp in RFC 3339 format. + + Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. + """ + utctime = value.astimezone(timezone.utc) + + # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for + # several years now), and isoformat is slightly different. + return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +ISO_TZ_SEPARATORS = frozenset(("+", "-")) + + +def datetime_from_isoformat(value: str) -> "datetime": + try: + result = datetime.fromisoformat(value) + except (AttributeError, ValueError): + # py 3.6 + timestamp_format = ( + "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" + ) + if value.endswith("Z"): + value = value[:-1] + "+0000" + + if value[-6] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + value = value[:-3] + value[-2:] + elif value[-5] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + + result = datetime.strptime(value, timestamp_format) + return result.astimezone(timezone.utc) + + +def event_hint_with_exc_info( + exc_info: "Optional[ExcInfo]" = None, +) -> "Dict[str, Optional[ExcInfo]]": + """Creates a hint with the exc info filled in.""" + if exc_info is None: + exc_info = sys.exc_info() + else: + exc_info = exc_info_from_error(exc_info) + if exc_info[0] is None: + exc_info = None + return {"exc_info": exc_info} + + +class BadDsn(ValueError): + """Raised on invalid DSNs.""" + + +class Dsn: + """Represents a DSN.""" + + ORG_ID_REGEX = re.compile(r"^o(\d+)\.") + + def __init__( + self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None + ) -> None: + if isinstance(value, Dsn): + self.__dict__ = dict(value.__dict__) + return + parts = urlsplit(str(value)) + + if parts.scheme not in ("http", "https"): + raise BadDsn("Unsupported scheme %r" % parts.scheme) + self.scheme = parts.scheme + + if parts.hostname is None: + raise BadDsn("Missing hostname") + + self.host = parts.hostname + + if org_id is not None: + self.org_id: "Optional[str]" = org_id + else: + org_id_match = Dsn.ORG_ID_REGEX.match(self.host) + self.org_id = org_id_match.group(1) if org_id_match else None + + if parts.port is None: + self.port: int = self.scheme == "https" and 443 or 80 + else: + self.port = parts.port + + if not parts.username: + raise BadDsn("Missing public key") + + self.public_key = parts.username + self.secret_key = parts.password + + path = parts.path.rsplit("/", 1) + + try: + self.project_id = str(int(path.pop())) + except (ValueError, TypeError): + raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) + + self.path = "/".join(path) + "/" + + @property + def netloc(self) -> str: + """The netloc part of a DSN.""" + rv = self.host + if (self.scheme, self.port) not in (("http", 80), ("https", 443)): + rv = "%s:%s" % (rv, self.port) + return rv + + def to_auth(self, client: "Optional[Any]" = None) -> "Auth": + """Returns the auth info object for this dsn.""" + return Auth( + scheme=self.scheme, + host=self.netloc, + path=self.path, + project_id=self.project_id, + public_key=self.public_key, + secret_key=self.secret_key, + client=client, + ) + + def __str__(self) -> str: + return "%s://%s%s@%s%s%s" % ( + self.scheme, + self.public_key, + self.secret_key and "@" + self.secret_key or "", + self.netloc, + self.path, + self.project_id, + ) + + +class Auth: + """Helper object that represents the auth info.""" + + def __init__( + self, + scheme: str, + host: str, + project_id: str, + public_key: str, + secret_key: "Optional[str]" = None, + version: int = 7, + client: "Optional[Any]" = None, + path: str = "/", + ) -> None: + self.scheme = scheme + self.host = host + self.path = path + self.project_id = project_id + self.public_key = public_key + self.secret_key = secret_key + self.version = version + self.client = client + + def get_api_url( + self, + type: "EndpointType" = EndpointType.ENVELOPE, + ) -> str: + """Returns the API url for storing events.""" + return "%s://%s%sapi/%s/%s/" % ( + self.scheme, + self.host, + self.path, + self.project_id, + type.value, + ) + + def to_header(self) -> str: + """Returns the auth header a string.""" + rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] + if self.client is not None: + rv.append(("sentry_client", self.client)) + if self.secret_key is not None: + rv.append(("sentry_secret", self.secret_key)) + return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) + + +def get_type_name(cls: "Optional[type]") -> "Optional[str]": + return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) + + +def get_type_module(cls: "Optional[type]") -> "Optional[str]": + mod = getattr(cls, "__module__", None) + if mod not in (None, "builtins", "__builtins__"): + return mod + return None + + +def should_hide_frame(frame: "FrameType") -> bool: + try: + mod = frame.f_globals["__name__"] + if mod.startswith("sentry_sdk."): + return True + except (AttributeError, KeyError): + pass + + for flag_name in "__traceback_hide__", "__tracebackhide__": + try: + if frame.f_locals[flag_name]: + return True + except Exception: + pass + + return False + + +def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": + tb_: "Optional[TracebackType]" = tb + while tb_ is not None: + if not should_hide_frame(tb_.tb_frame): + yield tb_ + tb_ = tb_.tb_next + + +def get_lines_from_file( + filename: str, + lineno: int, + max_length: "Optional[int]" = None, + loader: "Optional[Any]" = None, + module: "Optional[str]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + context_lines = 5 + source = None + if loader is not None and hasattr(loader, "get_source"): + try: + source_str: "Optional[str]" = loader.get_source(module) + except (ImportError, IOError): + source_str = None + if source_str is not None: + source = source_str.splitlines() + + if source is None: + try: + source = linecache.getlines(filename) + except (OSError, IOError): + return [], None, [] + + if not source: + return [], None, [] + + lower_bound = max(0, lineno - context_lines) + upper_bound = min(lineno + 1 + context_lines, len(source)) + + try: + pre_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[lower_bound:lineno] + ] + context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) + post_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[(lineno + 1) : upper_bound] + ] + return pre_context, context_line, post_context + except IndexError: + # the file may have changed since it was loaded into memory + return [], None, [] + + +def get_source_context( + frame: "FrameType", + tb_lineno: "Optional[int]", + max_value_length: "Optional[int]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + try: + abs_path: "Optional[str]" = frame.f_code.co_filename + except Exception: + abs_path = None + try: + module = frame.f_globals["__name__"] + except Exception: + return [], None, [] + try: + loader = frame.f_globals["__loader__"] + except Exception: + loader = None + + if tb_lineno is not None and abs_path: + lineno = tb_lineno - 1 + return get_lines_from_file( + abs_path, lineno, max_value_length, loader=loader, module=module + ) + + return [], None, [] + + +def safe_str(value: "Any") -> str: + try: + return str(value) + except Exception: + return safe_repr(value) + + +def safe_repr(value: "Any") -> str: + try: + return repr(value) + except Exception: + return "" + + +def filename_for_module( + module: "Optional[str]", abs_path: "Optional[str]" +) -> "Optional[str]": + if not abs_path or not module: + return abs_path + + try: + if abs_path.endswith(".pyc"): + abs_path = abs_path[:-1] + + base_module = module.split(".", 1)[0] + if base_module == module: + return os.path.basename(abs_path) + + base_module_path = sys.modules[base_module].__file__ + if not base_module_path: + return abs_path + + return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( + os.sep + ) + except Exception: + return abs_path + + +def serialize_frame( + frame: "FrameType", + tb_lineno: "Optional[int]" = None, + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, +) -> "Dict[str, Any]": + f_code = getattr(frame, "f_code", None) + if not f_code: + abs_path = None + function = None + else: + abs_path = frame.f_code.co_filename + function = frame.f_code.co_name + try: + module = frame.f_globals["__name__"] + except Exception: + module = None + + if tb_lineno is None: + tb_lineno = frame.f_lineno + + try: + os_abs_path = os.path.abspath(abs_path) if abs_path else None + except Exception: + os_abs_path = None + + rv: "Dict[str, Any]" = { + "filename": filename_for_module(module, abs_path) or None, + "abs_path": os_abs_path, + "function": function or "", + "module": module, + "lineno": tb_lineno, + } + + if include_source_context: + rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( + frame, tb_lineno, max_value_length + ) + + if include_local_variables: + from sentry_sdk.serializer import serialize + + rv["vars"] = serialize( + dict(frame.f_locals), is_vars=True, custom_repr=custom_repr + ) + + return rv + + +def current_stacktrace( + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, +) -> "Dict[str, Any]": + __tracebackhide__ = True + frames = [] + + f: "Optional[FrameType]" = sys._getframe() + while f is not None: + if not should_hide_frame(f): + frames.append( + serialize_frame( + f, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + ) + ) + f = f.f_back + + frames.reverse() + + return {"frames": frames} + + +def get_errno(exc_value: BaseException) -> "Optional[Any]": + return getattr(exc_value, "errno", None) + + +def get_error_message(exc_value: "Optional[BaseException]") -> str: + message: str = safe_str( + getattr(exc_value, "message", "") + or getattr(exc_value, "detail", "") + or safe_str(exc_value) + ) + + # __notes__ should be a list of strings when notes are added + # via add_note, but can be anything else if __notes__ is set + # directly. We only support strings in __notes__, since that + # is the correct use. + notes: object = getattr(exc_value, "__notes__", None) + if isinstance(notes, list) and len(notes) > 0: + message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) + + return message + + +def single_exception_from_error_tuple( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: "Optional[int]" = None, + parent_id: "Optional[int]" = None, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "Dict[str, Any]": + """ + Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + """ + exception_value: "Dict[str, Any]" = {} + exception_value["mechanism"] = ( + mechanism.copy() if mechanism else {"type": "generic", "handled": True} + ) + if exception_id is not None: + exception_value["mechanism"]["exception_id"] = exception_id + + if exc_value is not None: + errno = get_errno(exc_value) + else: + errno = None + + if errno is not None: + exception_value["mechanism"].setdefault("meta", {}).setdefault( + "errno", {} + ).setdefault("number", errno) + + if source is not None: + exception_value["mechanism"]["source"] = source + + is_root_exception = exception_id == 0 + if not is_root_exception and parent_id is not None: + exception_value["mechanism"]["parent_id"] = parent_id + exception_value["mechanism"]["type"] = "chained" + + if is_root_exception and "type" not in exception_value["mechanism"]: + exception_value["mechanism"]["type"] = "generic" + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + if is_exception_group: + exception_value["mechanism"]["is_exception_group"] = True + + exception_value["module"] = get_type_module(exc_type) + exception_value["type"] = get_type_name(exc_type) + exception_value["value"] = get_error_message(exc_value) + + if client_options is None: + include_local_variables = True + include_source_context = True + max_value_length = None # fallback + custom_repr = None + else: + include_local_variables = client_options["include_local_variables"] + include_source_context = client_options["include_source_context"] + max_value_length = client_options["max_value_length"] + custom_repr = client_options.get("custom_repr") + + frames: "List[Dict[str, Any]]" = [ + serialize_frame( + tb.tb_frame, + tb_lineno=tb.tb_lineno, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + custom_repr=custom_repr, + ) + # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on + # processing a super-long stacktrace. + for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) + ] + + if len(frames) > MAX_STACK_FRAMES: + # If we have more frames than the limit, we remove the stacktrace completely. + # We don't trim the stacktrace here because we have not processed the whole + # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would + # intelligently trim by removing frames in the middle of the stacktrace, but + # since we don't have the whole stacktrace, we can't do that. Instead, we + # drop the entire stacktrace. + exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( + value=None + ) + + elif frames: + if not full_stack: + new_frames = frames + else: + new_frames = merge_stack_frames(frames, full_stack, client_options) + + exception_value["stacktrace"] = {"frames": new_frames} + + return exception_value + + +HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") + +if HAS_CHAINED_EXCEPTIONS: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + exc_type, exc_value, tb = exc_info + + seen_exceptions = [] + seen_exception_ids: "Set[int]" = set() + + while ( + exc_type is not None + and exc_value is not None + and id(exc_value) not in seen_exception_ids + ): + yield exc_type, exc_value, tb + + # Avoid hashing random types we don't know anything + # about. Use the list to keep a ref so that the `id` is + # not used for another object. + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + if exc_value.__suppress_context__: + cause = exc_value.__cause__ + else: + cause = exc_value.__context__ + if cause is None: + break + exc_type = type(cause) + exc_value = cause + tb = getattr(cause, "__traceback__", None) + +else: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + yield exc_info + + +def exceptions_from_error( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: int = 0, + parent_id: int = 0, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, + seen_exceptions: "Optional[list[BaseException]]" = None, + seen_exception_ids: "Optional[Set[int]]" = None, +) -> "Tuple[int, List[Dict[str, Any]]]": + """ + Creates the list of exceptions. + This can include chained exceptions and exceptions from an ExceptionGroup. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + + Args: + exception_id (int): + + Sequential counter for assigning ``mechanism.exception_id`` + to each processed exception. Is NOT the result of calling `id()` on the exception itself. + + parent_id (int): + + The ``mechanism.exception_id`` of the parent exception. + + Written into ``mechanism.parent_id`` in the event payload so Sentry can + reconstruct the exception tree. + + Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` + values for cycle detection. + """ + + if seen_exception_ids is None: + seen_exception_ids = set() + + if seen_exceptions is None: + seen_exceptions = [] + + if exc_value is not None and id(exc_value) in seen_exception_ids: + return (exception_id, []) + + if exc_value is not None: + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + parent = single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source=source, + full_stack=full_stack, + ) + exceptions = [parent] + + parent_id = exception_id + exception_id += 1 + + should_supress_context = ( + hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore + ) + if should_supress_context: + # Add direct cause. + # The field `__cause__` is set when raised with the exception (using the `from` keyword). + exception_has_cause = ( + exc_value + and hasattr(exc_value, "__cause__") + and exc_value.__cause__ is not None + ) + if exception_has_cause: + cause = exc_value.__cause__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(cause), + exc_value=cause, + tb=getattr(cause, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__cause__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + else: + # Add indirect cause. + # The field `__context__` is assigned if another exception occurs while handling the exception. + exception_has_content = ( + exc_value + and hasattr(exc_value, "__context__") + and exc_value.__context__ is not None + ) + if exception_has_content: + context = exc_value.__context__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(context), + exc_value=context, + tb=getattr(context, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__context__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + # Add exceptions from an ExceptionGroup. + is_exception_group = exc_value and hasattr(exc_value, "exceptions") + if is_exception_group: + for idx, e in enumerate(exc_value.exceptions): # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(e), + exc_value=e, + tb=getattr(e, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source="exceptions[%s]" % idx, + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + return (exception_id, exceptions) + + +def exceptions_from_error_tuple( + exc_info: "ExcInfo", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "List[Dict[str, Any]]": + exc_type, exc_value, tb = exc_info + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + + if is_exception_group: + (_, exceptions) = exceptions_from_error( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=0, + parent_id=0, + full_stack=full_stack, + ) + + else: + exceptions = [] + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + exceptions.append( + single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + full_stack=full_stack, + ) + ) + + exceptions.reverse() + + return exceptions + + +def to_string(value: str) -> str: + try: + return str(value) + except UnicodeDecodeError: + return repr(value)[1:-1] + + +def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": + if "stacktrace" in event: + yield event["stacktrace"] + if "threads" in event: + for thread in event["threads"].get("values") or (): + if "stacktrace" in thread: + yield thread["stacktrace"] + if "exception" in event: + for exception in event["exception"].get("values") or (): + if isinstance(exception, dict) and "stacktrace" in exception: + yield exception["stacktrace"] + + +def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + for frame in stacktrace.get("frames") or (): + yield frame + + +def handle_in_app( + event: "Event", + in_app_exclude: "Optional[List[str]]" = None, + in_app_include: "Optional[List[str]]" = None, + project_root: "Optional[str]" = None, +) -> "Event": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + set_in_app_in_frames( + stacktrace.get("frames"), + in_app_exclude=in_app_exclude, + in_app_include=in_app_include, + project_root=project_root, + ) + + return event + + +def set_in_app_in_frames( + frames: "Any", + in_app_exclude: "Optional[List[str]]", + in_app_include: "Optional[List[str]]", + project_root: "Optional[str]" = None, +) -> "Optional[Any]": + if not frames: + return None + + for frame in frames: + # if frame has already been marked as in_app, skip it + current_in_app = frame.get("in_app") + if current_in_app is not None: + continue + + module = frame.get("module") + + # check if module in frame is in the list of modules to include + if _module_in_list(module, in_app_include): + frame["in_app"] = True + continue + + # check if module in frame is in the list of modules to exclude + if _module_in_list(module, in_app_exclude): + frame["in_app"] = False + continue + + # if frame has no abs_path, skip further checks + abs_path = frame.get("abs_path") + if abs_path is None: + continue + + if _is_external_source(abs_path): + frame["in_app"] = False + continue + + if _is_in_project_root(abs_path, project_root): + frame["in_app"] = True + continue + + return frames + + +def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": + if isinstance(error, tuple) and len(error) == 3: + exc_type, exc_value, tb = error + elif isinstance(error, BaseException): + tb = getattr(error, "__traceback__", None) + if tb is not None: + exc_type = type(error) + exc_value = error + else: + exc_type, exc_value, tb = sys.exc_info() + if exc_value is not error: + tb = None + exc_value = error + exc_type = type(error) + + else: + raise ValueError("Expected Exception object to report, got %s!" % type(error)) + + exc_info = (exc_type, exc_value, tb) + + if TYPE_CHECKING: + # This cast is safe because exc_type and exc_value are either both + # None or both not None. + exc_info = cast(ExcInfo, exc_info) + + return exc_info + + +def merge_stack_frames( + frames: "List[Dict[str, Any]]", + full_stack: "List[Dict[str, Any]]", + client_options: "Optional[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Add the missing frames from full_stack to frames and return the merged list. + """ + frame_ids = { + ( + frame["abs_path"], + frame["context_line"], + frame["lineno"], + frame["function"], + ) + for frame in frames + } + + new_frames = [ + stackframe + for stackframe in full_stack + if ( + stackframe["abs_path"], + stackframe["context_line"], + stackframe["lineno"], + stackframe["function"], + ) + not in frame_ids + ] + new_frames.extend(frames) + + # Limit the number of frames + max_stack_frames = ( + client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) + if client_options + else None + ) + if max_stack_frames is not None: + new_frames = new_frames[len(new_frames) - max_stack_frames :] + + return new_frames + + +def event_from_exception( + exc_info: "Union[BaseException, ExcInfo]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, +) -> "Tuple[Event, Dict[str, Any]]": + exc_info = exc_info_from_error(exc_info) + hint = event_hint_with_exc_info(exc_info) + + if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): + full_stack = current_stacktrace( + include_local_variables=client_options["include_local_variables"], + max_value_length=client_options["max_value_length"], + )["frames"] + else: + full_stack = None + + return ( + { + "level": "error", + "exception": { + "values": exceptions_from_error_tuple( + exc_info, client_options, mechanism, full_stack + ) + }, + }, + hint, + ) + + +def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: + if name is None: + return False + + if not items: + return False + + for item in items: + if item == name or name.startswith(item + "."): + return True + + return False + + +def _is_external_source(abs_path: "Optional[str]") -> bool: + # check if frame is in 'site-packages' or 'dist-packages' + if abs_path is None: + return False + + external_source = ( + re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None + ) + return external_source + + +def _is_in_project_root( + abs_path: "Optional[str]", project_root: "Optional[str]" +) -> bool: + if abs_path is None or project_root is None: + return False + + # check if path is in the project root + if abs_path.startswith(project_root): + return True + + return False + + +def _truncate_by_bytes(string: str, max_bytes: int) -> str: + """ + Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. + """ + truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") + + return truncated + "..." + + +def _get_size_in_bytes(value: str) -> "Optional[int]": + try: + return len(value.encode("utf-8")) + except (UnicodeEncodeError, UnicodeDecodeError): + return None + + +def strip_string( + value: str, max_length: "Optional[int]" = None +) -> "Union[AnnotatedValue, str]": + if not value or max_length is None: + return value + + byte_size = _get_size_in_bytes(value) + text_size = len(value) + + if byte_size is not None and byte_size > max_length: + # truncate to max_length bytes, preserving code points + truncated_value = _truncate_by_bytes(value, max_length) + elif text_size is not None and text_size > max_length: + # fallback to truncating by string length + truncated_value = value[: max_length - 3] + "..." + else: + return value + + return AnnotatedValue( + value=truncated_value, + metadata={ + "len": byte_size or text_size, + "rem": [["!limit", "x", max_length - 3, max_length]], + }, + ) + + +def parse_version(version: str) -> "Optional[Tuple[int, ...]]": + """ + Parses a version string into a tuple of integers. + This uses the parsing loging from PEP 440: + https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + """ + VERSION_PATTERN = r""" # noqa: N806 + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+                [-_\.]?
+                (?P(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+            (?P                                         # post release
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?Ppost|rev|r)
+                    [-_\.]?
+                    (?P[0-9]+)?
+                )
+            )?
+            (?P                                          # dev release
+                [-_\.]?
+                (?Pdev)
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+        )
+        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+    """
+
+    pattern = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    try:
+        release = pattern.match(version).groupdict()["release"]  # type: ignore
+        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
+    except (TypeError, ValueError, AttributeError):
+        return None
+
+    return release_tuple
+
+
+def _is_contextvars_broken() -> bool:
+    """
+    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
+    """
+    try:
+        import gevent
+        from gevent.monkey import is_object_patched
+
+        # Get the MAJOR and MINOR version numbers of Gevent
+        version_tuple = tuple(
+            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
+        )
+        if is_object_patched("threading", "local"):
+            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
+            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
+            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
+            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
+            # for contextvars, is able to patch both thread locals and contextvars, in
+            # that case, check if contextvars are effectively patched.
+            if (
+                # Gevent 20.9.0+
+                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
+                # Gevent 20.5.0+ or Python < 3.7
+                or (is_object_patched("contextvars", "ContextVar"))
+            ):
+                return False
+
+            return True
+    except ImportError:
+        pass
+
+    try:
+        import greenlet
+        from eventlet.patcher import is_monkey_patched  # type: ignore
+
+        greenlet_version = parse_version(greenlet.__version__)
+
+        if greenlet_version is None:
+            logger.error(
+                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
+            )
+            return False
+
+        if is_monkey_patched("thread") and greenlet_version < (0, 5):
+            return True
+    except ImportError:
+        pass
+
+    return False
+
+
+def _make_threadlocal_contextvars(local: type) -> type:
+    class ContextVar:
+        # Super-limited impl of ContextVar
+
+        def __init__(self, name: str, default: "Any" = None) -> None:
+            self._name = name
+            self._default = default
+            self._local = local()
+            self._original_local = local()
+
+        def get(self, default: "Any" = None) -> "Any":
+            return getattr(self._local, "value", default or self._default)
+
+        def set(self, value: "Any") -> "Any":
+            token = str(random.getrandbits(64))
+            original_value = self.get()
+            setattr(self._original_local, token, original_value)
+            self._local.value = value
+            return token
+
+        def reset(self, token: "Any") -> None:
+            self._local.value = getattr(self._original_local, token)
+            # delete the original value (this way it works in Python 3.6+)
+            del self._original_local.__dict__[token]
+
+    return ContextVar
+
+
+def _get_contextvars() -> "Tuple[bool, type]":
+    """
+    Figure out the "right" contextvars installation to use. Returns a
+    `contextvars.ContextVar`-like class with a limited API.
+
+    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
+    """
+    if not _is_contextvars_broken():
+        # aiocontextvars is a PyPI package that ensures that the contextvars
+        # backport (also a PyPI package) works with asyncio under Python 3.6
+        #
+        # Import it if available.
+        if sys.version_info < (3, 7):
+            # `aiocontextvars` is absolutely required for functional
+            # contextvars on Python 3.6.
+            try:
+                from aiocontextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+        else:
+            # On Python 3.7 contextvars are functional.
+            try:
+                from contextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+
+    # Fall back to basic thread-local usage.
+
+    from threading import local
+
+    return False, _make_threadlocal_contextvars(local)
+
+
+HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
+
+CONTEXTVARS_ERROR_MESSAGE = """
+
+With asyncio/ASGI applications, the Sentry SDK requires a functional
+installation of `contextvars` to avoid leaking scope/context data across
+requests.
+
+Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
+"""
+
+_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
+
+# These exceptions won't set the span status to error if they occur. Use
+# register_control_flow_exception to add to this list
+_control_flow_exception_classes: "set[type]" = set()
+
+
+def is_internal_task() -> bool:
+    return _is_sentry_internal_task.get()
+
+
+@contextmanager
+def mark_sentry_task_internal() -> "Generator[None, None, None]":
+    """Context manager to mark a task as Sentry internal."""
+    token = _is_sentry_internal_task.set(True)
+    try:
+        yield
+    finally:
+        _is_sentry_internal_task.reset(token)
+
+
+def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
+    func_qualname: "Optional[str]" = None
+
+    prefix, suffix = "", ""
+
+    if isinstance(func, partial) and hasattr(func.func, "__name__"):
+        prefix, suffix = "partial()"
+        func = func.func
+    else:
+        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
+        # https://github.com/python/cpython/pull/16600
+        partial_method = getattr(func, "_partialmethod", None) or getattr(
+            func, "__partialmethod__", None
+        )
+        if isinstance(partial_method, partialmethod):
+            prefix, suffix = "partialmethod()"
+            func = partial_method.func
+
+    if hasattr(func, "__qualname__"):
+        func_qualname = func.__qualname__
+    elif hasattr(func, "__name__"):
+        func_qualname = func.__name__
+
+    if func_qualname is not None:
+        if hasattr(func, "__module__") and isinstance(func.__module__, str):
+            func_qualname = func.__module__ + "." + func_qualname
+        func_qualname = prefix + func_qualname + suffix
+
+    return func_qualname
+
+
+def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    return qualname_from_function(func)
+
+
+disable_capture_event = ContextVar("disable_capture_event")
+
+
+class ServerlessTimeoutWarning(Exception):  # noqa: N818
+    """Raised when a serverless method is about to reach its timeout."""
+
+    pass
+
+
+class TimeoutThread(threading.Thread):
+    """Creates a Thread which runs (sleeps) for a time duration equal to
+    waiting_time and raises a custom ServerlessTimeout exception.
+    """
+
+    def __init__(
+        self,
+        waiting_time: float,
+        configured_timeout: int,
+        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
+        current_scope: "Optional[sentry_sdk.Scope]" = None,
+    ) -> None:
+        threading.Thread.__init__(self)
+        self.waiting_time = waiting_time
+        self.configured_timeout = configured_timeout
+
+        self.isolation_scope = isolation_scope
+        self.current_scope = current_scope
+
+        self._stop_event = threading.Event()
+
+    def stop(self) -> None:
+        self._stop_event.set()
+
+    def _capture_exception(self) -> "ExcInfo":
+        exc_info = sys.exc_info()
+
+        client = sentry_sdk.get_client()
+        event, hint = event_from_exception(
+            exc_info,
+            client_options=client.options,
+            mechanism={"type": "threading", "handled": False},
+        )
+        sentry_sdk.capture_event(event, hint=hint)
+
+        return exc_info
+
+    def run(self) -> None:
+        self._stop_event.wait(self.waiting_time)
+
+        if self._stop_event.is_set():
+            return
+
+        integer_configured_timeout = int(self.configured_timeout)
+
+        # Setting up the exact integer value of configured time(in seconds)
+        if integer_configured_timeout < self.configured_timeout:
+            integer_configured_timeout = integer_configured_timeout + 1
+
+        # Raising Exception after timeout duration is reached
+        if self.isolation_scope is not None and self.current_scope is not None:
+            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
+                with sentry_sdk.scope.use_scope(self.current_scope):
+                    try:
+                        raise ServerlessTimeoutWarning(
+                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                                integer_configured_timeout
+                            )
+                        )
+                    except Exception:
+                        reraise(*self._capture_exception())
+
+        raise ServerlessTimeoutWarning(
+            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                integer_configured_timeout
+            )
+        )
+
+
+def to_base64(original: str) -> "Optional[str]":
+    """
+    Convert a string to base64, via UTF-8. Returns None on invalid input.
+    """
+    base64_string = None
+
+    try:
+        utf8_bytes = original.encode("UTF-8")
+        base64_bytes = base64.b64encode(utf8_bytes)
+        base64_string = base64_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
+
+    return base64_string
+
+
+def from_base64(base64_string: str) -> "Optional[str]":
+    """
+    Convert a string from base64, via UTF-8. Returns None on invalid input.
+    """
+    utf8_string = None
+
+    try:
+        only_valid_chars = BASE64_ALPHABET.match(base64_string)
+        assert only_valid_chars
+
+        base64_bytes = base64_string.encode("UTF-8")
+        utf8_bytes = base64.b64decode(base64_bytes)
+        utf8_string = utf8_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning(
+            "Unable to decode {b64} from base64:".format(b64=base64_string), err
+        )
+
+    return utf8_string
+
+
+Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
+
+
+def sanitize_url(
+    url: str,
+    remove_authority: bool = True,
+    remove_query_values: bool = True,
+    split: bool = False,
+) -> "Union[str, Components]":
+    """
+    Removes the authority and query parameter values from a given URL.
+    """
+    parsed_url = urlsplit(url)
+    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
+
+    # strip username:password (netloc can be usr:pwd@example.com)
+    if remove_authority:
+        netloc_parts = parsed_url.netloc.split("@")
+        if len(netloc_parts) > 1:
+            netloc = "%s:%s@%s" % (
+                SENSITIVE_DATA_SUBSTITUTE,
+                SENSITIVE_DATA_SUBSTITUTE,
+                netloc_parts[-1],
+            )
+        else:
+            netloc = parsed_url.netloc
+    else:
+        netloc = parsed_url.netloc
+
+    # strip values from query string
+    if remove_query_values:
+        query_string = unquote(
+            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
+        )
+    else:
+        query_string = parsed_url.query
+
+    components = Components(
+        scheme=parsed_url.scheme,
+        netloc=netloc,
+        query=query_string,
+        path=parsed_url.path,
+        fragment=parsed_url.fragment,
+    )
+
+    if split:
+        return components
+    else:
+        return urlunsplit(components)
+
+
+ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
+
+
+def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
+    """
+    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
+    parameters will be sanitized to remove sensitive data. The autority (username and password)
+    in the URL will always be removed.
+    """
+    parsed_url = sanitize_url(
+        url, remove_authority=True, remove_query_values=sanitize, split=True
+    )
+
+    base_url = urlunsplit(
+        Components(
+            scheme=parsed_url.scheme,  # type: ignore
+            netloc=parsed_url.netloc,  # type: ignore
+            query="",
+            path=parsed_url.path,  # type: ignore
+            fragment="",
+        )
+    )
+
+    return ParsedUrl(
+        url=base_url,
+        query=parsed_url.query,  # type: ignore
+        fragment=parsed_url.fragment,  # type: ignore
+    )
+
+
+def is_valid_sample_rate(rate: "Any", source: str) -> bool:
+    """
+    Checks the given sample rate to make sure it is valid type and value (a
+    boolean or a number between 0 and 1, inclusive).
+    """
+
+    # both booleans and NaN are instances of Real, so a) checking for Real
+    # checks for the possibility of a boolean also, and b) we have to check
+    # separately for NaN and Decimal does not derive from Real so need to check that too
+    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
+                source=source, rate=rate, type=type(rate)
+            )
+        )
+        return False
+
+    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
+    rate = float(rate)
+    if rate < 0 or rate > 1:
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
+                source=source, rate=rate
+            )
+        )
+        return False
+
+    return True
+
+
+def match_regex_list(
+    item: str,
+    regex_list: "Optional[List[str]]" = None,
+    substring_matching: bool = False,
+) -> bool:
+    if regex_list is None:
+        return False
+
+    for item_matcher in regex_list:
+        if not substring_matching and item_matcher[-1] != "$":
+            item_matcher += "$"
+
+        matched = re.search(item_matcher, item)
+        if matched:
+            return True
+
+    return False
+
+
+def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
+    """
+    Determines whether the given URL matches the Sentry DSN.
+    """
+    return (
+        client is not None
+        and client.transport is not None
+        and client.transport.parsed_dsn is not None
+        and client.transport.parsed_dsn.netloc in url
+    )
+
+
+def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
+    try:
+        from importlib import metadata
+
+        yielded = set()
+        for dist in metadata.distributions():
+            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
+            # `metadata` values may be `None`, see:
+            # https://github.com/python/cpython/issues/91216
+            # and
+            # https://github.com/python/importlib_metadata/issues/371
+            if name is not None:
+                normalized_name = _normalize_module_name(name)
+                if dist.version is not None and normalized_name not in yielded:
+                    yield normalized_name, dist.version
+                    yielded.add(normalized_name)
+
+    except ImportError:
+        # < py3.8
+        try:
+            import pkg_resources
+        except ImportError:
+            return
+
+        for info in pkg_resources.working_set:
+            yield _normalize_module_name(info.key), info.version
+
+
+def _normalize_module_name(name: str) -> str:
+    return name.lower()
+
+
+def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
+    # https://peps.python.org/pep-0503/#normalized-names
+    return re.sub(r"[-_.]+", "-", name)
+
+
+def _get_installed_modules() -> "Dict[str, str]":
+    global _installed_modules
+    if _installed_modules is None:
+        _installed_modules = dict(_generate_installed_modules())
+    return _installed_modules
+
+
+def package_version(package: str) -> "Optional[Tuple[int, ...]]":
+    normalized_package = _normalize_module_name(
+        _replace_hyphens_dots_and_underscores_with_dashes(package)
+    )
+
+    installed_packages = {
+        _replace_hyphens_dots_and_underscores_with_dashes(module): v
+        for module, v in _get_installed_modules().items()
+    }
+    version = installed_packages.get(normalized_package)
+    if version is None:
+        return None
+
+    return parse_version(version)
+
+
+def reraise(
+    tp: "Optional[Type[BaseException]]",
+    value: "Optional[BaseException]",
+    tb: "Optional[Any]" = None,
+) -> "NoReturn":
+    assert value is not None
+    if value.__traceback__ is not tb:
+        raise value.with_traceback(tb)
+    raise value
+
+
+def _no_op(*_a: "Any", **_k: "Any") -> None:
+    """No-op function for ensure_integration_enabled."""
+    pass
+
+
+if TYPE_CHECKING:
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+        original_function: "Callable[P, R]",
+    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
+
+
+def ensure_integration_enabled(
+    integration: "type[sentry_sdk.integrations.Integration]",
+    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
+) -> "Callable[[Callable[P, R]], Callable[P, R]]":
+    """
+    Ensures a given integration is enabled prior to calling a Sentry-patched function.
+
+    The function takes as its parameters the integration that must be enabled and the original
+    function that the SDK is patching. The function returns a function that takes the
+    decorated (Sentry-patched) function as its parameter, and returns a function that, when
+    called, checks whether the given integration is enabled. If the integration is enabled, the
+    function calls the decorated, Sentry-patched function. If the integration is not enabled,
+    the original function is called.
+
+    The function also takes care of preserving the original function's signature and docstring.
+
+    Example usage:
+
+    ```python
+    @ensure_integration_enabled(MyIntegration, my_function)
+    def patch_my_function():
+        with sentry_sdk.start_transaction(...):
+            return my_function()
+    ```
+    """
+    if TYPE_CHECKING:
+        # Type hint to ensure the default function has the right typing. The overloads
+        # ensure the default _no_op function is only used when R is None.
+        original_function = cast(Callable[P, R], original_function)
+
+    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
+        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
+            if sentry_sdk.get_client().get_integration(integration) is None:
+                return original_function(*args, **kwargs)
+
+            return sentry_patched_function(*args, **kwargs)
+
+        if original_function is _no_op:
+            return wraps(sentry_patched_function)(runner)
+
+        return wraps(original_function)(runner)
+
+    return patcher
+
+
+if PY37:
+
+    def nanosecond_time() -> int:
+        return time.perf_counter_ns()
+
+else:
+
+    def nanosecond_time() -> int:
+        return int(time.perf_counter() * 1e9)
+
+
+def now() -> float:
+    return time.perf_counter()
+
+
+try:
+    from gevent import get_hub as get_gevent_hub
+    from gevent.monkey import is_module_patched
+except ImportError:
+    # it's not great that the signatures are different, get_hub can't return None
+    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
+    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
+        return None
+
+    def is_module_patched(mod_name: str) -> bool:
+        # unable to import from gevent means no modules have been patched
+        return False
+
+
+def is_gevent() -> bool:
+    return is_module_patched("threading") or is_module_patched("_thread")
+
+
+def get_current_thread_meta(
+    thread: "Optional[threading.Thread]" = None,
+) -> "Tuple[Optional[int], Optional[str]]":
+    """
+    Try to get the id of the current thread, with various fall backs.
+    """
+
+    # if a thread is specified, that takes priority
+    if thread is not None:
+        try:
+            thread_id = thread.ident
+            thread_name = thread.name
+            if thread_id is not None:
+                return thread_id, thread_name
+        except AttributeError:
+            pass
+
+    # if the app is using gevent, we should look at the gevent hub first
+    # as the id there differs from what the threading module reports
+    if is_gevent():
+        gevent_hub = get_gevent_hub()
+        if gevent_hub is not None:
+            try:
+                # this is undocumented, so wrap it in try except to be safe
+                return gevent_hub.thread_ident, None
+            except AttributeError:
+                pass
+
+    # use the current thread's id if possible
+    try:
+        thread = threading.current_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # if we can't get the current thread id, fall back to the main thread id
+    try:
+        thread = threading.main_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # we've tried everything, time to give up
+    return None, None
+
+
+def _register_control_flow_exception(
+    exc_type: "Union[type, list[type], tuple[type], set[type]]",
+) -> None:
+    if isinstance(exc_type, (list, tuple, set)):
+        _control_flow_exception_classes.update(exc_type)
+    else:
+        _control_flow_exception_classes.add(exc_type)
+
+
+def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
+    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
+        # https://docs.python.org/3/library/exceptions.html#SystemExit
+        return False
+
+    if issubclass(ty, tuple(_control_flow_exception_classes)):
+        return False
+
+    return True
+
+
+if TYPE_CHECKING:
+    T = TypeVar("T")
+
+
+def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
+    """
+    Attempt to convert from an unknown type to a specific type, using the
+    given function. Return None if the conversion fails, i.e. if the function
+    raises an exception.
+    """
+    try:
+        if isinstance(value, convert_func):  # type: ignore
+            return value
+    except TypeError:
+        pass
+
+    try:
+        return convert_func(value)
+    except Exception:
+        return None
+
+
+def safe_serialize(data: "Any") -> str:
+    """Safely serialize to a readable string."""
+
+    def serialize_item(
+        item: "Any",
+    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
+        if callable(item):
+            try:
+                module = getattr(item, "__module__", None)
+                qualname = getattr(item, "__qualname__", None)
+                name = getattr(item, "__name__", "anonymous")
+
+                if module and qualname:
+                    full_path = f"{module}.{qualname}"
+                elif module and name:
+                    full_path = f"{module}.{name}"
+                else:
+                    full_path = name
+
+                return f""
+            except Exception:
+                return f""
+        elif isinstance(item, dict):
+            return {k: serialize_item(v) for k, v in item.items()}
+        elif isinstance(item, (list, tuple)):
+            return [serialize_item(x) for x in item]
+        elif hasattr(item, "__dict__"):
+            try:
+                attrs = {
+                    k: serialize_item(v)
+                    for k, v in vars(item).items()
+                    if not k.startswith("_")
+                }
+                return f"<{type(item).__name__} {attrs}>"
+            except Exception:
+                return repr(item)
+        else:
+            return item
+
+    try:
+        serialized = serialize_item(data)
+        return (
+            json.dumps(serialized, default=str)
+            if not isinstance(serialized, str)
+            else serialized
+        )
+    except Exception:
+        return str(data)
+
+
+def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(
+        options.get("enable_logs", False)
+        or options["_experiments"].get("enable_logs", False)
+    )
+
+
+def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return "data_collection" in options.get("_experiments", {})
+
+
+def get_before_send_log(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_log") or options["_experiments"].get(
+        "before_send_log"
+    )
+
+
+def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(options.get("enable_metrics", True))
+
+
+def get_before_send_metric(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_metric") or options["_experiments"].get(
+        "before_send_metric"
+    )
+
+
+def get_before_send_span(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
+    if options is None:
+        return None
+
+    return options["_experiments"].get("before_send_span")
+
+
+def format_attribute(val: "Any") -> "AttributeValue":
+    """
+    Turn unsupported attribute value types into an AttributeValue.
+
+    We do this as soon as a user-provided attribute is set, to prevent spans,
+    logs, metrics and similar from having live references to various objects.
+
+    Note: This is not the final attribute value format. Before they're sent,
+    they're serialized further into the actual format the protocol expects:
+    https://develop.sentry.dev/sdk/telemetry/attributes/
+    """
+    if isinstance(val, (bool, int, float, str)):
+        return val
+
+    if isinstance(val, (list, tuple)) and not val:
+        return []
+    elif isinstance(val, list):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return copy.deepcopy(val)
+    elif isinstance(val, tuple):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return list(val)
+
+    return safe_repr(val)
+
+
+def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
+    """Serialize attribute value to the transport format."""
+    if isinstance(val, bool):
+        return {"value": val, "type": "boolean"}
+    if isinstance(val, int):
+        return {"value": val, "type": "integer"}
+    if isinstance(val, float):
+        return {"value": val, "type": "double"}
+    if isinstance(val, str):
+        return {"value": val, "type": "string"}
+
+    if isinstance(val, list):
+        if not val:
+            return {"value": [], "type": "array"}
+
+        # Only lists of elements of a single type are supported
+        ty = type(val[0])
+        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
+            return {"value": val, "type": "array"}
+
+    # Coerce to string if we don't know what to do with the value. This should
+    # never happen as we pre-format early in format_attribute, but let's be safe.
+    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py
new file mode 100644
index 0000000000..5eb9b23130
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py
@@ -0,0 +1,308 @@
+import asyncio
+import os
+import threading
+from abc import ABC, abstractmethod
+from time import sleep, time
+from typing import TYPE_CHECKING
+
+from sentry_sdk._queue import FullError, Queue
+from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
+from sentry_sdk.utils import logger, mark_sentry_task_internal
+
+if TYPE_CHECKING:
+    from typing import Any, Callable, Optional
+
+
+_TERMINATOR = object()
+
+
+class Worker(ABC):
+    """Base class for all workers."""
+
+    @property
+    @abstractmethod
+    def is_alive(self) -> bool:
+        """Whether the worker is alive and running."""
+        pass
+
+    @abstractmethod
+    def kill(self) -> None:
+        """Kill the worker. It will not process any more events."""
+        pass
+
+    def flush(
+        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
+    ) -> None:
+        """Flush the worker, blocking until done or timeout is reached."""
+        return None
+
+    @abstractmethod
+    def full(self) -> bool:
+        """Whether the worker's queue is full."""
+        pass
+
+    @abstractmethod
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        """Schedule a callback. Returns True if queued, False if full."""
+        pass
+
+
+class BackgroundWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Queue" = Queue(queue_size)
+        self._lock = threading.Lock()
+        self._thread: "Optional[threading.Thread]" = None
+        self._thread_for_pid: "Optional[int]" = None
+
+    @property
+    def is_alive(self) -> bool:
+        if self._thread_for_pid != os.getpid():
+            return False
+        if not self._thread:
+            return False
+        return self._thread.is_alive()
+
+    def _ensure_thread(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    def _timed_queue_join(self, timeout: float) -> bool:
+        deadline = time() + timeout
+        queue = self._queue
+
+        queue.all_tasks_done.acquire()
+
+        try:
+            while queue.unfinished_tasks:
+                delay = deadline - time()
+                if delay <= 0:
+                    return False
+                queue.all_tasks_done.wait(timeout=delay)
+
+            return True
+        finally:
+            queue.all_tasks_done.release()
+
+    def start(self) -> None:
+        with self._lock:
+            if not self.is_alive:
+                self._thread = threading.Thread(
+                    target=self._target, name="sentry-sdk.BackgroundWorker"
+                )
+                self._thread.daemon = True
+                try:
+                    self._thread.start()
+                    self._thread_for_pid = os.getpid()
+                except RuntimeError:
+                    # At this point we can no longer start because the interpreter
+                    # is already shutting down.  Sadly at this point we can no longer
+                    # send out events.
+                    self._thread = None
+
+    def kill(self) -> None:
+        """
+        Kill worker thread. Returns immediately. Not useful for
+        waiting on shutdown for events, use `flush` for that.
+        """
+        logger.debug("background worker got kill request")
+        with self._lock:
+            if self._thread:
+                try:
+                    self._queue.put_nowait(_TERMINATOR)
+                except FullError:
+                    logger.debug("background worker queue full, kill failed")
+
+                self._thread = None
+                self._thread_for_pid = None
+
+    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
+        logger.debug("background worker got flush request")
+        with self._lock:
+            if self.is_alive and timeout > 0.0:
+                self._wait_flush(timeout, callback)
+        logger.debug("background worker flushed")
+
+    def full(self) -> bool:
+        return self._queue.full()
+
+    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
+        initial_timeout = min(0.1, timeout)
+        if not self._timed_queue_join(initial_timeout):
+            pending = self._queue.qsize() + 1
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            if not self._timed_queue_join(timeout - initial_timeout):
+                pending = self._queue.qsize() + 1
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_thread()
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except FullError:
+            return False
+
+    def _target(self) -> None:
+        while True:
+            callback = self._queue.get()
+            try:
+                if callback is _TERMINATOR:
+                    break
+                try:
+                    callback()
+                except Exception:
+                    logger.error("Failed processing job", exc_info=True)
+            finally:
+                self._queue.task_done()
+            sleep(0)
+
+
+class AsyncWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Optional[asyncio.Queue[Any]]" = None
+        self._queue_size = queue_size
+        self._task: "Optional[asyncio.Task[None]]" = None
+        # Event loop needs to remain in the same process
+        self._task_for_pid: "Optional[int]" = None
+        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
+        # Track active callback tasks so they have a strong reference and can be cancelled on kill
+        self._active_tasks: "set[asyncio.Task[None]]" = set()
+
+    @property
+    def is_alive(self) -> bool:
+        if self._task_for_pid != os.getpid():
+            return False
+        if not self._task or not self._loop:
+            return False
+        return self._loop.is_running() and not self._task.done()
+
+    def kill(self) -> None:
+        if self._task:
+            # Cancel the main consumer task to prevent duplicate consumers
+            self._task.cancel()
+            # Also cancel any active callback tasks
+            # Avoid modifying the set while cancelling tasks
+            tasks_to_cancel = set(self._active_tasks)
+            for task in tasks_to_cancel:
+                task.cancel()
+            self._active_tasks.clear()
+            self._loop = None
+            self._task = None
+            self._task_for_pid = None
+
+    def start(self) -> None:
+        if not self.is_alive:
+            try:
+                self._loop = asyncio.get_running_loop()
+                # Always create a fresh queue on start to avoid stale items
+                self._queue = asyncio.Queue(maxsize=self._queue_size)
+                with mark_sentry_task_internal():
+                    self._task = self._loop.create_task(self._target())
+                self._task_for_pid = os.getpid()
+            except RuntimeError:
+                # There is no event loop running
+                logger.warning("No event loop running, async worker not started")
+                self._loop = None
+                self._task = None
+                self._task_for_pid = None
+
+    def full(self) -> bool:
+        if self._queue is None:
+            return True
+        return self._queue.full()
+
+    def _ensure_task(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    async def _wait_flush(
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> None:
+        if not self._loop or not self._loop.is_running() or self._queue is None:
+            return
+
+        initial_timeout = min(0.1, timeout)
+
+        # Timeout on the join
+        try:
+            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
+        except asyncio.TimeoutError:
+            pending = self._queue.qsize() + len(self._active_tasks)
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            try:
+                remaining_timeout = timeout - initial_timeout
+                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
+            except asyncio.TimeoutError:
+                pending = self._queue.qsize() + len(self._active_tasks)
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def flush(  # type: ignore[override]
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> "Optional[asyncio.Task[None]]":
+        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
+            with mark_sentry_task_internal():
+                return self._loop.create_task(self._wait_flush(timeout, callback))
+        return None
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_task()
+        if self._queue is None:
+            return False
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except asyncio.QueueFull:
+            return False
+
+    async def _target(self) -> None:
+        if self._queue is None:
+            return
+        try:
+            while True:
+                callback = await self._queue.get()
+                if callback is _TERMINATOR:
+                    self._queue.task_done()
+                    break
+                # Firing tasks instead of awaiting them allows for concurrent requests
+                with mark_sentry_task_internal():
+                    task = asyncio.create_task(self._process_callback(callback))
+                # Create a strong reference to the task so it can be cancelled on kill
+                # and does not get garbage collected while running
+                self._active_tasks.add(task)
+                # Capture queue ref at dispatch time so done callbacks use the
+                # correct queue even if kill()/start() replace self._queue.
+                queue_ref = self._queue
+                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
+                # Yield to let the event loop run other tasks
+                await asyncio.sleep(0)
+        except asyncio.CancelledError:
+            pass  # Expected during kill()
+
+    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
+        # Callback is an async coroutine, need to await it
+        await callback()
+
+    def _on_task_complete(
+        self,
+        task: "asyncio.Task[None]",
+        queue: "Optional[asyncio.Queue[Any]]" = None,
+    ) -> None:
+        try:
+            task.result()
+        except asyncio.CancelledError:
+            pass  # Task was cancelled, expected during shutdown
+        except Exception:
+            logger.error("Failed processing job", exc_info=True)
+        finally:
+            # Mark the task as done and remove it from the active tasks set
+            # Use the queue reference captured at dispatch time, not self._queue,
+            # to avoid calling task_done() on a different queue after kill()/start().
+            if queue is not None:
+                queue.task_done()
+            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER
new file mode 100644
index 0000000000..5c69047b2e
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA
new file mode 100644
index 0000000000..9c7a4703f8
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA
@@ -0,0 +1,163 @@
+Metadata-Version: 2.4
+Name: urllib3
+Version: 2.7.0
+Summary: HTTP library with thread-safe connection pooling, file post, and more.
+Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
+Project-URL: Documentation, https://urllib3.readthedocs.io
+Project-URL: Code, https://github.com/urllib3/urllib3
+Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
+Author-email: Andrey Petrov 
+Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
+License-Expression: MIT
+License-File: LICENSE.txt
+Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Software Development :: Libraries
+Requires-Python: >=3.10
+Provides-Extra: brotli
+Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
+Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
+Provides-Extra: h2
+Requires-Dist: h2<5,>=4; extra == 'h2'
+Provides-Extra: socks
+Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
+Provides-Extra: zstd
+Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
+Description-Content-Type: text/markdown
+
+

+ +![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) + +

+ +

+ PyPI Version + Python Versions + Join our Discord + Coverage Status + Build Status on GitHub + Documentation Status
+ OpenSSF Scorecard + SLSA 3 + CII Best Practices +

+ +urllib3 is a powerful, *user-friendly* HTTP client for Python. +urllib3 brings many critical features that are missing from the Python +standard libraries: + +- Thread safety. +- Connection pooling. +- Client-side SSL/TLS verification. +- File uploads with multipart encoding. +- Helpers for retrying requests and dealing with HTTP redirects. +- Support for gzip, deflate, brotli, and zstd encoding. +- Proxy support for HTTP and SOCKS. +- 100% test coverage. + +... and many more features, but most importantly: Our maintainers have a 15+ +year track record of maintaining urllib3 with the highest code standards and +attention to security and safety. + +[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) +and you should too. + + +## Installing + +urllib3 can be installed with [pip](https://pip.pypa.io): + +```bash +$ python -m pip install urllib3 +``` + +Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): + +```bash +$ git clone https://github.com/urllib3/urllib3.git +$ cd urllib3 +$ pip install . +``` + +## Getting Started + +urllib3 is easy to use: + +```python3 +>>> import urllib3 +>>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") +>>> resp.status +200 +>>> resp.data +b"User-agent: *\nDisallow: /deny\n" +``` + +urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). + + +## Community + +urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and +collaborating with other contributors. Drop by and say hello 👋 + + +## Contributing + +urllib3 happily accepts contributions. Please see our +[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) +for some tips on getting started. + + +## Security Disclosures + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure with maintainers. + + +## Maintainers + +Meet our maintainers since 2008: + +- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) +- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) +- [@pquentin](https://github.com/pquentin) (Quentin Pradet) +- [@theacodes](https://github.com/theacodes) (Thea Flowers) +- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) +- [@lukasa](https://github.com/lukasa) (Cory Benfield) +- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) +- [@shazow](https://github.com/shazow) (Andrey Petrov) + +👋 + + +## Sponsorship + +If your company benefits from this library, please consider [sponsoring its +development](https://urllib3.readthedocs.io/en/latest/sponsors.html). + + +## For Enterprise + +Professional support for urllib3 is available as part of the [Tidelift +Subscription][1]. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD new file mode 100644 index 0000000000..8c4f9c0b4b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD @@ -0,0 +1,44 @@ +urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 +urllib3-2.7.0.dist-info/RECORD,, +urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 +urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 +urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 +urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 +urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 +urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 +urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 +urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 +urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 +urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 +urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 +urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 +urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 +urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 +urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 +urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 +urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 +urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 +urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 +urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 +urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 +urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 +urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 +urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 +urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 +urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 +urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 +urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 +urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 +urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 +urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 +urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 +urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 +urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 +urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 +urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 +urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 +urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 +urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL new file mode 100644 index 0000000000..b1b94fd58e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000..e6183d0276 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py new file mode 100644 index 0000000000..3fe782c8a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py @@ -0,0 +1,211 @@ +""" +Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more +""" + +from __future__ import annotations + +# Set default logging handler to avoid "No handler found" warnings. +import logging +import sys +import typing +import warnings +from logging import NullHandler + +from . import exceptions +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._version import __version__ +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import BaseHTTPResponse, HTTPResponse +from .util.request import make_headers +from .util.retry import Retry +from .util.timeout import Timeout + +# Ensure that Python is compiled with OpenSSL 1.1.1+ +# If the 'ssl' module isn't available at all that's +# fine, we only care if the module is available. +try: + import ssl +except ImportError: + pass +else: + if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: + warnings.warn( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/3020", + exceptions.NotOpenSSLWarning, + ) + elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: + raise ImportError( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/2168" + ) + +__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" +__license__ = "MIT" +__version__ = __version__ + +__all__ = ( + "HTTPConnectionPool", + "HTTPHeaderDict", + "HTTPSConnectionPool", + "PoolManager", + "ProxyManager", + "HTTPResponse", + "Retry", + "Timeout", + "add_stderr_logger", + "connection_from_url", + "disable_warnings", + "encode_multipart_formdata", + "make_headers", + "proxy_from_url", + "request", + "BaseHTTPResponse", +) + +logging.getLogger(__name__).addHandler(NullHandler()) + + +def add_stderr_logger( + level: int = logging.DEBUG, +) -> logging.StreamHandler[typing.TextIO]: + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug("Added a stderr logging handler to logger: %s", __name__) + return handler + + +# ... Clean up. +del NullHandler + + +# All warning filters *must* be appended unless you're really certain that they +# shouldn't be: otherwise, it's very hard for users to use most Python +# mechanisms to silence them. +# SecurityWarning's always go off by default. +warnings.simplefilter("always", exceptions.SecurityWarning, append=True) +# InsecurePlatformWarning's don't vary between requests, so we keep it default. +warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) + + +def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter("ignore", category) + + +_DEFAULT_POOL = PoolManager() + + +def request( + method: str, + url: str, + *, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + preload_content: bool | None = True, + decode_content: bool | None = True, + redirect: bool | None = True, + retries: Retry | bool | int | None = None, + timeout: Timeout | float | int | None = 3, + json: typing.Any | None = None, +) -> BaseHTTPResponse: + """ + A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. + Therefore, its side effects could be shared across dependencies relying on it. + To avoid side effects create a new ``PoolManager`` instance and use it instead. + The method does not accept low-level ``**urlopen_kw`` keyword arguments. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + + return _DEFAULT_POOL.request( + method, + url, + body=body, + fields=fields, + headers=headers, + preload_content=preload_content, + decode_content=decode_content, + redirect=redirect, + retries=retries, + timeout=timeout, + json=json, + ) + + +if sys.platform == "emscripten": + from .contrib.emscripten import inject_into_urllib3 # noqa: 401 + + inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py new file mode 100644 index 0000000000..992ec1657a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import typing + +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from .util.url import Url + +_TYPE_BODY = typing.Union[ + bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str +] + + +class ProxyConfig(typing.NamedTuple): + ssl_context: ssl.SSLContext | None + use_forwarding_for_https: bool + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + + +class _ResponseOptions(typing.NamedTuple): + # TODO: Remove this in favor of a better + # HTTP request/response lifecycle tracking. + request_method: str + request_url: str + preload_content: bool + decode_content: bool + enforce_content_length: bool + + +if typing.TYPE_CHECKING: + import ssl + from typing import Protocol + + from .response import BaseHTTPResponse + + class BaseHTTPConnection(Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + host: str + port: int + timeout: None | ( + float + ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool + proxy_is_verified: bool | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: ... + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: ... + + def connect(self) -> None: ... + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: ... + + def getresponse(self) -> BaseHTTPResponse: ... + + def close(self) -> None: ... + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + + class BaseHTTPSConnection(BaseHTTPConnection, Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + # Certificate verification methods + cert_reqs: int | str | None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + ssl_context: ssl.SSLContext | None + + # Trusted CAs + ca_certs: str | None + ca_cert_dir: str | None + ca_cert_data: None | str | bytes + + # TLS version + ssl_minimum_version: int | None + ssl_maximum_version: int | None + ssl_version: int | str | None # Deprecated + + # Client certificates + cert_file: str | None + key_file: str | None + key_password: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py new file mode 100644 index 0000000000..ee9ca662b6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py @@ -0,0 +1,486 @@ +from __future__ import annotations + +import typing +from collections import OrderedDict +from enum import Enum, auto +from threading import RLock + +if typing.TYPE_CHECKING: + # We can only import Protocol if TYPE_CHECKING because it's a development + # dependency, and is not available at runtime. + from typing import Protocol + + from typing_extensions import Self + + class HasGettableStringKeys(Protocol): + def keys(self) -> typing.Iterator[str]: ... + + def __getitem__(self, key: str) -> str: ... + + +__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] + + +# Key type +_KT = typing.TypeVar("_KT") +# Value type +_VT = typing.TypeVar("_VT") +# Default type +_DT = typing.TypeVar("_DT") + +ValidHTTPHeaderSource = typing.Union[ + "HTTPHeaderDict", + typing.Mapping[str, str], + typing.Iterable[tuple[str, str]], + "HasGettableStringKeys", +] + + +class _Sentinel(Enum): + not_passed = auto() + + +def ensure_can_construct_http_header_dict( + potential: object, +) -> ValidHTTPHeaderSource | None: + if isinstance(potential, HTTPHeaderDict): + return potential + elif isinstance(potential, typing.Mapping): + # Full runtime checking of the contents of a Mapping is expensive, so for the + # purposes of typechecking, we assume that any Mapping is the right shape. + return typing.cast(typing.Mapping[str, str], potential) + elif isinstance(potential, typing.Iterable): + # Similarly to Mapping, full runtime checking of the contents of an Iterable is + # expensive, so for the purposes of typechecking, we assume that any Iterable + # is the right shape. + return typing.cast(typing.Iterable[tuple[str, str]], potential) + elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): + return typing.cast("HasGettableStringKeys", potential) + else: + return None + + +class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + _container: typing.OrderedDict[_KT, _VT] + _maxsize: int + dispose_func: typing.Callable[[_VT], None] | None + lock: RLock + + def __init__( + self, + maxsize: int = 10, + dispose_func: typing.Callable[[_VT], None] | None = None, + ) -> None: + super().__init__() + self._maxsize = maxsize + self.dispose_func = dispose_func + self._container = OrderedDict() + self.lock = RLock() + + def __getitem__(self, key: _KT) -> _VT: + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key: _KT, value: _VT) -> None: + evicted_item = None + with self.lock: + # Possibly evict the existing value of 'key' + try: + # If the key exists, we'll overwrite it, which won't change the + # size of the pool. Because accessing a key should move it to + # the end of the eviction line, we pop it out first. + evicted_item = key, self._container.pop(key) + self._container[key] = value + except KeyError: + # When the key does not exist, we insert the value first so that + # evicting works in all cases, including when self._maxsize is 0 + self._container[key] = value + if len(self._container) > self._maxsize: + # If we didn't evict an existing value, and we've hit our maximum + # size, then we have to evict the least recently used item from + # the beginning of the container. + evicted_item = self._container.popitem(last=False) + + # After releasing the lock on the pool, dispose of any evicted value. + if evicted_item is not None and self.dispose_func: + _, evicted_value = evicted_item + self.dispose_func(evicted_value) + + def __delitem__(self, key: _KT) -> None: + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self) -> int: + with self.lock: + return len(self._container) + + def __iter__(self) -> typing.NoReturn: + raise NotImplementedError( + "Iteration over this class is unlikely to be threadsafe." + ) + + def clear(self) -> None: + with self.lock: + # Copy pointers to all values, then wipe the mapping + values = list(self._container.values()) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self) -> set[_KT]: # type: ignore[override] + with self.lock: + return set(self._container.keys()) + + +class HTTPHeaderDictItemView(set[tuple[str, str]]): + """ + HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of + address. + + If we directly try to get an item with a particular name, we will get a string + back that is the concatenated version of all the values: + + >>> d['X-Header-Name'] + 'Value1, Value2, Value3' + + However, if we iterate over an HTTPHeaderDict's items, we will optionally combine + these values based on whether combine=True was called when building up the dictionary + + >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) + >>> d.add("A", "2", combine=True) + >>> d.add("B", "bar") + >>> list(d.items()) + [ + ('A', '1, 2'), + ('B', 'foo'), + ('B', 'bar'), + ] + + This class conforms to the interface required by the MutableMapping ABC while + also giving us the nonstandard iteration behavior we want; items with duplicate + keys, ordered by time of first insertion. + """ + + _headers: HTTPHeaderDict + + def __init__(self, headers: HTTPHeaderDict) -> None: + self._headers = headers + + def __len__(self) -> int: + return len(list(self._headers.iteritems())) + + def __iter__(self) -> typing.Iterator[tuple[str, str]]: + return self._headers.iteritems() + + def __contains__(self, item: object) -> bool: + if isinstance(item, tuple) and len(item) == 2: + passed_key, passed_val = item + if isinstance(passed_key, str) and isinstance(passed_val, str): + return self._headers._has_value_for_header(passed_key, passed_val) + return False + + +class HTTPHeaderDict(typing.MutableMapping[str, str]): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 7230. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + """ + + _container: typing.MutableMapping[str, list[str]] + + def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): + super().__init__() + self._container = {} # 'dict' is insert-ordered + if headers is not None: + if isinstance(headers, HTTPHeaderDict): + self._copy_from(headers) + else: + self.extend(headers) + if kwargs: + self.extend(kwargs) + + def __setitem__(self, key: str, val: str) -> None: + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + self._container[key.lower()] = [key, val] + + def __getitem__(self, key: str) -> str: + if isinstance(key, bytes): + key = key.decode("latin-1") + val = self._container[key.lower()] + return ", ".join(val[1:]) + + def __delitem__(self, key: str) -> None: + if isinstance(key, bytes): + key = key.decode("latin-1") + del self._container[key.lower()] + + def __contains__(self, key: object) -> bool: + if isinstance(key, bytes): + key = key.decode("latin-1") + if isinstance(key, str): + return key.lower() in self._container + return False + + def setdefault(self, key: str, default: str = "") -> str: + return super().setdefault(key, default) + + def __eq__(self, other: object) -> bool: + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return False + else: + other_as_http_header_dict = type(self)(maybe_constructable) + + return {k.lower(): v for k, v in self.itermerged()} == { + k.lower(): v for k, v in other_as_http_header_dict.itermerged() + } + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __len__(self) -> int: + return len(self._container) + + def __iter__(self) -> typing.Iterator[str]: + # Only provide the originally cased names + for vals in self._container.values(): + yield vals[0] + + def discard(self, key: str) -> None: + try: + del self[key] + except KeyError: + pass + + def add(self, key: str, val: str, *, combine: bool = False) -> None: + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + If this is called with combine=True, instead of adding a new header value + as a distinct item during iteration, this will instead append the value to + any existing header value with a comma. If no existing header value exists + for the key, then the value will simply be added, ignoring the combine parameter. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + >>> list(headers.items()) + [('foo', 'bar'), ('foo', 'baz')] + >>> headers.add('foo', 'quz', combine=True) + >>> list(headers.items()) + [('foo', 'bar, baz, quz')] + """ + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + key_lower = key.lower() + new_vals = [key, val] + # Keep the common case aka no item present as fast as possible + vals = self._container.setdefault(key_lower, new_vals) + if new_vals is not vals: + # if there are values here, then there is at least the initial + # key/value pair + assert len(vals) >= 2 + if combine: + vals[-1] = vals[-1] + ", " + val + else: + vals.append(val) + + def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: + """Generic import function for any type of header-like object. + Adapted version of MutableMapping.update in order to insert items + with self.add instead of self.__setitem__ + """ + if len(args) > 1: + raise TypeError( + f"extend() takes at most 1 positional arguments ({len(args)} given)" + ) + other = args[0] if len(args) >= 1 else () + + if isinstance(other, HTTPHeaderDict): + for key, val in other.iteritems(): + self.add(key, val) + elif isinstance(other, typing.Mapping): + for key, val in other.items(): + self.add(key, val) + elif isinstance(other, typing.Iterable): + for key, value in other: + self.add(key, value) + elif hasattr(other, "keys") and hasattr(other, "__getitem__"): + # THIS IS NOT A TYPESAFE BRANCH + # In this branch, the object has a `keys` attr but is not a Mapping or any of + # the other types indicated in the method signature. We do some stuff with + # it as though it partially implements the Mapping interface, but we're not + # doing that stuff safely AT ALL. + for key in other.keys(): + self.add(key, other[key]) + + for key, value in kwargs.items(): + self.add(key, value) + + @typing.overload + def getlist(self, key: str) -> list[str]: ... + + @typing.overload + def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... + + def getlist( + self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed + ) -> list[str] | _DT: + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + if isinstance(key, bytes): + key = key.decode("latin-1") + try: + vals = self._container[key.lower()] + except KeyError: + if default is _Sentinel.not_passed: + # _DT is unbound; empty list is instance of List[str] + return [] + # _DT is bound; default is instance of _DT + return default + else: + # _DT may or may not be bound; vals[1:] is instance of List[str], which + # meets our external interface requirement of `Union[List[str], _DT]`. + return vals[1:] + + def _prepare_for_method_change(self) -> Self: + """ + Remove content-specific header fields before changing the request + method to GET or HEAD according to RFC 9110, Section 15.4. + """ + content_specific_headers = [ + "Content-Encoding", + "Content-Language", + "Content-Location", + "Content-Type", + "Content-Length", + "Digest", + "Last-Modified", + ] + for header in content_specific_headers: + self.discard(header) + return self + + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist + iget = getlist + + # Backwards compatibility for http.cookiejar + get_all = getlist + + def __repr__(self) -> str: + return f"{type(self).__name__}({dict(self.itermerged())})" + + def _copy_from(self, other: HTTPHeaderDict) -> None: + for key in other: + val = other.getlist(key) + self._container[key.lower()] = [key, *val] + + def copy(self) -> Self: + clone = type(self)() + clone._copy_from(self) + return clone + + def iteritems(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all header lines, including duplicate ones.""" + for key in self: + vals = self._container[key.lower()] + for val in vals[1:]: + yield vals[0], val + + def itermerged(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all headers, merging duplicate ones together.""" + for key in self: + val = self._container[key.lower()] + yield val[0], ", ".join(val[1:]) + + def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] + return HTTPHeaderDictItemView(self) + + def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: + if header_name in self: + return potential_value in self._container[header_name.lower()][1:] + return False + + def __ior__(self, other: object) -> HTTPHeaderDict: + # Supports extending a header dict in-place using operator |= + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + self.extend(maybe_constructable) + return self + + def __or__(self, other: object) -> Self: + # Supports merging header dicts using operator | + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = self.copy() + result.extend(maybe_constructable) + return result + + def __ror__(self, other: object) -> Self: + # Supports merging header dicts using operator | when other is on left side + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = type(self)(maybe_constructable) + result.extend(self) + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py new file mode 100644 index 0000000000..297c271bf4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import json as _json +import typing +from urllib.parse import urlencode + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .response import BaseHTTPResponse + +__all__ = ["RequestMethods"] + +_TYPE_ENCODE_URL_FIELDS = typing.Union[ + typing.Sequence[tuple[str, typing.Union[str, bytes]]], + typing.Mapping[str, typing.Union[str, bytes]], +] + + +class RequestMethods: + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`urllib3.HTTPConnectionPool` and + :class:`urllib3.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are + encoded in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} + + def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: + self.headers = headers or {} + + def urlopen( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **kw: typing.Any, + ) -> BaseHTTPResponse: # Abstract + raise NotImplementedError( + "Classes extending RequestMethods must implement " + "their own ``urlopen`` method." + ) + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + json: typing.Any | None = None, + **urlopen_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the + option to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the URL or request body, depending on ``method``. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + method = method.upper() + + if json is not None and body is not None: + raise TypeError( + "request got values for both 'body' and 'json' parameters which are mutually exclusive" + ) + + if json is not None: + if headers is None: + headers = self.headers + + if not ("content-type" in map(str.lower, headers.keys())): + headers = HTTPHeaderDict(headers) + headers["Content-Type"] = "application/json" + + body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + + if body is not None: + urlopen_kw["body"] = body + + if method in self._encode_url_methods: + return self.request_encode_url( + method, + url, + fields=fields, # type: ignore[arg-type] + headers=headers, + **urlopen_kw, + ) + else: + return self.request_encode_body( + method, url, fields=fields, headers=headers, **urlopen_kw + ) + + def request_encode_url( + self, + method: str, + url: str, + fields: _TYPE_ENCODE_URL_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the URL. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": headers} + extra_kw.update(urlopen_kw) + + if fields: + url += "?" + urlencode(fields) + + return self.urlopen(method, url, **extra_kw) + + def request_encode_body( + self, + method: str, + url: str, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :func:`urllib3.encode_multipart_formdata` is used to encode + the payload with the appropriate content type. Otherwise + :func:`urllib.parse.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request + signing, such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example:: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimic behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will + be overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param encode_multipart: + If True, encode the ``fields`` using the multipart/form-data MIME + format. + + :param multipart_boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} + body: bytes | str + + if fields: + if "body" in urlopen_kw: + raise TypeError( + "request got values for both 'fields' and 'body', can only specify one." + ) + + if encode_multipart: + body, content_type = encode_multipart_formdata( + fields, boundary=multipart_boundary + ) + else: + body, content_type = ( + urlencode(fields), # type: ignore[arg-type] + "application/x-www-form-urlencoded", + ) + + extra_kw["body"] = body + extra_kw["headers"].setdefault("Content-Type", content_type) + + extra_kw.update(urlopen_kw) + + return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py new file mode 100644 index 0000000000..bfed03985b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '2.7.0' +__version_tuple__ = version_tuple = (2, 7, 0) + +__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py new file mode 100644 index 0000000000..84e1dab945 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py @@ -0,0 +1,1099 @@ +from __future__ import annotations + +import datetime +import http.client +import logging +import os +import re +import socket +import sys +import threading +import typing +import warnings +from http.client import HTTPConnection as _HTTPConnection +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from .response import HTTPResponse + from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT + from .util.ssltransport import SSLTransport + +from ._collections import HTTPHeaderDict +from .http2 import probe as http2_probe +from .util.response import assert_header_parsing +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout +from .util.util import to_str +from .util.wait import wait_for_read + +try: # Compiled with SSL? + import ssl + + BaseSSLError = ssl.SSLError +except (ImportError, AttributeError): + ssl = None # type: ignore[assignment] + + class BaseSSLError(BaseException): # type: ignore[no-redef] + pass + + +from ._base_connection import _TYPE_BODY +from ._base_connection import ProxyConfig as ProxyConfig +from ._base_connection import _ResponseOptions as _ResponseOptions +from ._version import __version__ +from .exceptions import ( + ConnectTimeoutError, + HeaderParsingError, + NameResolutionError, + NewConnectionError, + ProxyError, + SystemTimeWarning, +) +from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ +from .util.request import body_to_chunks +from .util.ssl_ import assert_fingerprint as _assert_fingerprint +from .util.ssl_ import ( + create_urllib3_context, + is_ipaddress, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .util.ssl_match_hostname import CertificateError, match_hostname +from .util.url import Url + +# Not a no-op, we're adding this to the namespace so it can be imported. +ConnectionError = ConnectionError +BrokenPipeError = BrokenPipeError + + +log = logging.getLogger(__name__) + +port_by_scheme = {"http": 80, "https": 443} + +# When it comes time to update this value as a part of regular maintenance +# (ie test_recent_date is failing) update it to ~6 months before the current date. +RECENT_DATE = datetime.date(2025, 1, 1) + +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") + + +class HTTPConnection(_HTTPConnection): + """ + Based on :class:`http.client.HTTPConnection` but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + + Additional keyword parameters are used to configure attributes of the connection. + Accepted parameters include: + + - ``source_address``: Set the source address for the current connection. + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass: + + .. code-block:: python + + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + """ + + default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] + + #: Disable Nagle's algorithm by default. + #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` + default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + ] + + #: Whether this connection verifies the host's certificate. + is_verified: bool = False + + #: Whether this proxy connection verified the proxy host's certificate. + # If no proxy is currently connected to the value will be ``None``. + proxy_is_verified: bool | None = None + + blocksize: int + source_address: tuple[str, int] | None + socket_options: connection._TYPE_SOCKET_OPTIONS | None + + _has_connected_to_proxy: bool + _response_options: _ResponseOptions | None + _tunnel_host: str | None + _tunnel_port: int | None + _tunnel_scheme: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + super().__init__( + host=host, + port=port, + timeout=Timeout.resolve_default_timeout(timeout), + source_address=source_address, + blocksize=blocksize, + ) + self.socket_options = socket_options + self.proxy = proxy + self.proxy_config = proxy_config + + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host: str | None = None + self._tunnel_port: int | None = None + self._tunnel_scheme: str | None = None + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __repr__(self) -> str: + return f"<{self} at {id(self):#x}>" + + @property + def host(self) -> str: + """ + Getter method to remove any trailing dots that indicate the hostname is an FQDN. + + In general, SSL certificates don't include the trailing dot indicating a + fully-qualified domain name, and thus, they don't validate properly when + checked against a domain name that includes the dot. In addition, some + servers may not expect to receive the trailing dot when provided. + + However, the hostname with trailing dot is critical to DNS resolution; doing a + lookup with the trailing dot will properly only resolve the appropriate FQDN, + whereas a lookup without a trailing dot will search the system's search domain + list. Thus, it's important to keep the original host around for use only in + those cases where it's appropriate (i.e., when doing DNS lookup to establish the + actual TCP connection across which we're going to send HTTP requests). + """ + return self._dns_host.rstrip(".") + + @host.setter + def host(self, value: str) -> None: + """ + Setter for the `host` property. + + We assume that only urllib3 uses the _dns_host attribute; httplib itself + only uses `host`, and it seems reasonable that other libraries follow suit. + """ + self._dns_host = value + + def _new_conn(self) -> socket.socket: + """Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + try: + sock = connection.create_connection( + (self._dns_host, self.port), + self.timeout, + source_address=self.source_address, + socket_options=self.socket_options, + ) + except socket.gaierror as e: + raise NameResolutionError(self.host, self, e) from e + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except OSError as e: + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + sys.audit("http.client.connect", self, self.host, self.port) + + return sock + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + if scheme not in ("http", "https"): + raise ValueError( + f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" + ) + super().set_tunnel(host, port=port, headers=headers) + self._tunnel_scheme = scheme + + if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): + # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. + # When using connection_from_host, host will come without brackets. + def _wrap_ipv6(self, ip: bytes) -> bytes: + if b":" in ip and ip[0] != b"["[0]: + return b"[" + ip + b"]" + return ip + + if sys.version_info < (3, 11, 9): + # `_tunnel` copied from 3.11.13 backporting + # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 + # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 + def _tunnel(self) -> None: + _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] + connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + while True: + line = response.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise http.client.LineTooLong("header line") + if not line: + # for sites which EOF without sending a trailer + break + if line in (b"\r\n", b"\n", b""): + break + + if self.debuglevel > 0: + print("header:", line.decode()) + finally: + response.close() + + elif (3, 12) <= sys.version_info < (3, 12, 3): + # `_tunnel` copied from 3.12.11 backporting + # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 + def _tunnel(self) -> None: # noqa: F811 + connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] + + if self.debuglevel > 0: + for header in self._raw_proxy_headers: + print("header:", header.decode()) + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + + finally: + response.close() + + def connect(self) -> None: + self.sock = self._new_conn() + if self._tunnel_host: + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + if self._has_connected_to_proxy: + self.proxy_is_verified = False + + @property + def is_closed(self) -> bool: + return self.sock is None + + @property + def is_connected(self) -> bool: + if self.sock is None: + return False + return not wait_for_read(self.sock, timeout=0.0) + + @property + def has_connected_to_proxy(self) -> bool: + return self._has_connected_to_proxy + + @property + def proxy_is_forwarding(self) -> bool: + """ + Return True if a forwarding proxy is configured, else return False + """ + return bool(self.proxy) and self._tunnel_host is None + + @property + def proxy_is_tunneling(self) -> bool: + """ + Return True if a tunneling proxy is configured, else return False + """ + return self._tunnel_host is not None + + def close(self) -> None: + try: + super().close() + finally: + # Reset all stateful properties so connection + # can be re-used without leaking prior configs. + self.sock = None + self.is_verified = False + self.proxy_is_verified = None + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_scheme = None + + def putrequest( + self, + method: str, + url: str, + skip_host: bool = False, + skip_accept_encoding: bool = False, + ) -> None: + """""" + # Empty docstring because the indentation of CPython's implementation + # is broken but we don't want this method in our documentation. + match = _CONTAINS_CONTROL_CHAR_RE.search(method) + if match: + raise ValueError( + f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" + ) + + return super().putrequest( + method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding + ) + + def putheader(self, header: str, *values: str) -> None: # type: ignore[override] + """""" + if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): + super().putheader(header, *values) + elif to_str(header.lower()) not in SKIPPABLE_HEADERS: + skippable_headers = "', '".join( + [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] + ) + raise ValueError( + f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" + ) + + # `request` method's signature intentionally violates LSP. + # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + # Update the inner socket's timeout value to send the request. + # This only triggers if the connection is re-used. + if self.sock is not None: + self.sock.settimeout(self.timeout) + + # Store these values to be fed into the HTTPResponse + # object later. TODO: Remove this in favor of a real + # HTTP lifecycle mechanism. + + # We have to store these before we call .request() + # because sometimes we can still salvage a response + # off the wire even if we aren't able to completely + # send the request body. + self._response_options = _ResponseOptions( + request_method=method, + request_url=url, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + if headers is None: + headers = {} + header_keys = frozenset(to_str(k.lower()) for k in headers) + skip_accept_encoding = "accept-encoding" in header_keys + skip_host = "host" in header_keys + self.putrequest( + method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host + ) + + # Transform the body into an iterable of sendall()-able chunks + # and detect if an explicit Content-Length is doable. + chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) + chunks = chunks_and_cl.chunks + content_length = chunks_and_cl.content_length + + # When chunked is explicit set to 'True' we respect that. + if chunked: + if "transfer-encoding" not in header_keys: + self.putheader("Transfer-Encoding", "chunked") + else: + # Detect whether a framing mechanism is already in use. If so + # we respect that value, otherwise we pick chunked vs content-length + # depending on the type of 'body'. + if "content-length" in header_keys: + chunked = False + elif "transfer-encoding" in header_keys: + chunked = True + + # Otherwise we go off the recommendation of 'body_to_chunks()'. + else: + chunked = False + if content_length is None: + if chunks is not None: + chunked = True + self.putheader("Transfer-Encoding", "chunked") + else: + self.putheader("Content-Length", str(content_length)) + + # Now that framing headers are out of the way we send all the other headers. + if "user-agent" not in header_keys: + self.putheader("User-Agent", _get_default_user_agent()) + for header, value in headers.items(): + self.putheader(header, value) + self.endheaders() + + # If we're given a body we start sending that in chunks. + if chunks is not None: + for chunk in chunks: + # Sending empty chunks isn't allowed for TE: chunked + # as it indicates the end of the body. + if not chunk: + continue + if isinstance(chunk, str): + chunk = chunk.encode("utf-8") + if chunked: + self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) + else: + self.send(chunk) + + # Regardless of whether we have a body or not, if we're in + # chunked mode we want to send an explicit empty chunk. + if chunked: + self.send(b"0\r\n\r\n") + + def request_chunked( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + ) -> None: + """ + Alternative to the common request method, which sends the + body with chunked encoding and not as one block + """ + warnings.warn( + "HTTPConnection.request_chunked() is deprecated and will be removed " + "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", + category=FutureWarning, + stacklevel=2, + ) + self.request(method, url, body=body, headers=headers, chunked=True) + + def getresponse( # type: ignore[override] + self, + ) -> HTTPResponse: + """ + Get the response from the server. + + If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. + + If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. + """ + # Raise the same error as http.client.HTTPConnection + if self._response_options is None: + raise ResponseNotReady() + + # Reset this attribute for being used again. + resp_options = self._response_options + self._response_options = None + + # Since the connection's timeout value may have been updated + # we need to set the timeout on the socket. + self.sock.settimeout(self.timeout) + + # This is needed here to avoid circular import errors + from .response import HTTPResponse + + # Save a reference to the shutdown function before ownership is passed + # to httplib_response + # TODO should we implement it everywhere? + _shutdown = getattr(self.sock, "shutdown", None) + + # Get the response from http.client.HTTPConnection + httplib_response = super().getresponse() + + try: + assert_header_parsing(httplib_response.msg) + except (HeaderParsingError, TypeError) as hpe: + log.warning( + "Failed to parse headers (url=%s): %s", + _url_from_connection(self, resp_options.request_url), + hpe, + exc_info=True, + ) + + headers = HTTPHeaderDict(httplib_response.msg.items()) + + response = HTTPResponse( + body=httplib_response, + headers=headers, + status=httplib_response.status, + version=httplib_response.version, + version_string=getattr(self, "_http_vsn_str", "HTTP/?"), + reason=httplib_response.reason, + preload_content=resp_options.preload_content, + decode_content=resp_options.decode_content, + original_response=httplib_response, + enforce_content_length=resp_options.enforce_content_length, + request_method=resp_options.request_method, + request_url=resp_options.request_url, + sock_shutdown=_shutdown, + ) + return response + + +class HTTPSConnection(HTTPConnection): + """ + Many of the parameters to this constructor are passed to the underlying SSL + socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. + """ + + default_port = port_by_scheme["https"] # type: ignore[misc] + + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_fingerprint: str | None = None + _connect_callback: typing.Callable[..., None] | None = None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + # cert_reqs depends on ssl_context so calculate last. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + self.cert_reqs = cert_reqs + self._connect_callback = None + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + """ + This method should only be called once, before the connection is used. + """ + warnings.warn( + "HTTPSConnection.set_cert() is deprecated and will be removed " + "in urllib3 v3.0. Instead provide the parameters to the " + "HTTPSConnection constructor.", + category=FutureWarning, + stacklevel=2, + ) + + # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also + # have an SSLContext object in which case we'll use its verify_mode. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + def connect(self) -> None: + # Today we don't need to be doing this step before the /actual/ socket + # connection, however in the future we'll need to decide whether to + # create a new socket or re-use an existing "shared" socket as a part + # of the HTTP/2 handshake dance. + if self._tunnel_host is not None and self._tunnel_port is not None: + probe_http2_host = self._tunnel_host + probe_http2_port = self._tunnel_port + else: + probe_http2_host = self.host + probe_http2_port = self.port + + # Check if the target origin supports HTTP/2. + # If the value comes back as 'None' it means that the current thread + # is probing for HTTP/2 support. Otherwise, we're waiting for another + # probe to complete, or we get a value right away. + target_supports_http2: bool | None + if "h2" in ssl_.ALPN_PROTOCOLS: + target_supports_http2 = http2_probe.acquire_and_get( + host=probe_http2_host, port=probe_http2_port + ) + else: + # If HTTP/2 isn't going to be offered it doesn't matter if + # the target supports HTTP/2. Don't want to make a probe. + target_supports_http2 = False + + if self._connect_callback is not None: + self._connect_callback( + "before connect", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + try: + sock: socket.socket | ssl.SSLSocket + self.sock = sock = self._new_conn() + server_hostname: str = self.host + tls_in_tls = False + + # Do we need to establish a tunnel? + if self.proxy_is_tunneling: + # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. + if self._tunnel_scheme == "https": + # _connect_tls_proxy will verify and assign proxy_is_verified + self.sock = sock = self._connect_tls_proxy(self.host, sock) + tls_in_tls = True + elif self._tunnel_scheme == "http": + self.proxy_is_verified = False + + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + self._tunnel() + # Override the host with the one we're requesting data from. + server_hostname = typing.cast(str, self._tunnel_host) + + if self.server_hostname is not None: + server_hostname = self.server_hostname + + is_time_off = datetime.date.today() < RECENT_DATE + if is_time_off: + warnings.warn( + ( + f"System time is way off (before {RECENT_DATE}). This will probably " + "lead to SSL verification errors" + ), + SystemTimeWarning, + ) + + # Remove trailing '.' from fqdn hostnames to allow certificate validation + server_hostname_rm_dot = server_hostname.rstrip(".") + + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock=sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + server_hostname=server_hostname_rm_dot, + ssl_context=self.ssl_context, + tls_in_tls=tls_in_tls, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ) + self.sock = sock_and_verified.socket + + # If an error occurs during connection/handshake we may need to release + # our lock so another connection can probe the origin. + except BaseException: + if self._connect_callback is not None: + self._connect_callback( + "after connect failure", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + if target_supports_http2 is None: + http2_probe.set_and_release( + host=probe_http2_host, port=probe_http2_port, supports_http2=None + ) + raise + + # If this connection doesn't know if the origin supports HTTP/2 + # we report back to the HTTP/2 probe our result. + if target_supports_http2 is None: + supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" + http2_probe.set_and_release( + host=probe_http2_host, + port=probe_http2_port, + supports_http2=supports_http2, + ) + + # Forwarding proxies can never have a verified target since + # the proxy is the one doing the verification. Should instead + # use a CONNECT tunnel in order to verify the target. + # See: https://github.com/urllib3/urllib3/issues/3267. + if self.proxy_is_forwarding: + self.is_verified = False + else: + self.is_verified = sock_and_verified.is_verified + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + # Set `self.proxy_is_verified` unless it's already set while + # establishing a tunnel. + if self._has_connected_to_proxy and self.proxy_is_verified is None: + self.proxy_is_verified = sock_and_verified.is_verified + + def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: + """ + Establish a TLS connection to the proxy using the provided SSL context. + """ + # `_connect_tls_proxy` is called when self._tunnel_host is truthy. + proxy_config = typing.cast(ProxyConfig, self.proxy_config) + ssl_context = proxy_config.ssl_context + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + server_hostname=hostname, + ssl_context=ssl_context, + assert_hostname=proxy_config.assert_hostname, + assert_fingerprint=proxy_config.assert_fingerprint, + # Features that aren't implemented for proxies yet: + cert_file=None, + key_file=None, + key_password=None, + tls_in_tls=False, + ) + self.proxy_is_verified = sock_and_verified.is_verified + return sock_and_verified.socket # type: ignore[return-value] + + +class _WrappedAndVerifiedSocket(typing.NamedTuple): + """ + Wrapped socket and whether the connection is + verified after the TLS handshake + """ + + socket: ssl.SSLSocket | SSLTransport + is_verified: bool + + +def _ssl_wrap_socket_and_match_hostname( + sock: socket.socket, + *, + cert_reqs: None | str | int, + ssl_version: None | str | int, + ssl_minimum_version: int | None, + ssl_maximum_version: int | None, + cert_file: str | None, + key_file: str | None, + key_password: str | None, + ca_certs: str | None, + ca_cert_dir: str | None, + ca_cert_data: None | str | bytes, + assert_hostname: None | str | typing.Literal[False], + assert_fingerprint: str | None, + server_hostname: str | None, + ssl_context: ssl.SSLContext | None, + tls_in_tls: bool = False, +) -> _WrappedAndVerifiedSocket: + """Logic for constructing an SSLContext from all TLS parameters, passing + that down into ssl_wrap_socket, and then doing certificate verification + either via hostname or fingerprint. This function exists to guarantee + that both proxies and targets have the same behavior when connecting via TLS. + """ + default_ssl_context = False + if ssl_context is None: + default_ssl_context = True + context = create_urllib3_context( + ssl_version=resolve_ssl_version(ssl_version), + ssl_minimum_version=ssl_minimum_version, + ssl_maximum_version=ssl_maximum_version, + cert_reqs=resolve_cert_reqs(cert_reqs), + ) + else: + context = ssl_context + + context.verify_mode = resolve_cert_reqs(cert_reqs) + + # In some cases, we want to verify hostnames ourselves + if ( + # `ssl` can't verify fingerprints or alternate hostnames + assert_fingerprint + or assert_hostname + # assert_hostname can be set to False to disable hostname checking + or assert_hostname is False + # We still support OpenSSL 1.0.2, which prevents us from verifying + # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 + or ssl_.IS_PYOPENSSL + or not ssl_.HAS_NEVER_CHECK_COMMON_NAME + ): + context.check_hostname = False + + # Try to load OS default certs if none are given. We need to do the hasattr() check + # for custom pyOpenSSL SSLContext objects because they don't support + # load_default_certs(). + if ( + not ca_certs + and not ca_cert_dir + and not ca_cert_data + and default_ssl_context + and hasattr(context, "load_default_certs") + ): + context.load_default_certs() + + # Ensure that IPv6 addresses are in the proper format and don't have a + # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses + # and interprets them as DNS hostnames. + if server_hostname is not None: + normalized = server_hostname.strip("[]") + if "%" in normalized: + normalized = normalized[: normalized.rfind("%")] + if is_ipaddress(normalized): + server_hostname = normalized + + ssl_sock = ssl_wrap_socket( + sock=sock, + keyfile=key_file, + certfile=cert_file, + key_password=key_password, + ca_certs=ca_certs, + ca_cert_dir=ca_cert_dir, + ca_cert_data=ca_cert_data, + server_hostname=server_hostname, + ssl_context=context, + tls_in_tls=tls_in_tls, + ) + + try: + if assert_fingerprint: + _assert_fingerprint( + ssl_sock.getpeercert(binary_form=True), assert_fingerprint + ) + elif ( + context.verify_mode != ssl.CERT_NONE + and not context.check_hostname + and assert_hostname is not False + ): + cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] + + # Need to signal to our match_hostname whether to use 'commonName' or not. + # If we're using our own constructed SSLContext we explicitly set 'False' + # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. + if default_ssl_context: + hostname_checks_common_name = False + else: + hostname_checks_common_name = ( + getattr(context, "hostname_checks_common_name", False) or False + ) + + _match_hostname( + cert, + assert_hostname or server_hostname, # type: ignore[arg-type] + hostname_checks_common_name, + ) + + return _WrappedAndVerifiedSocket( + socket=ssl_sock, + is_verified=context.verify_mode == ssl.CERT_REQUIRED + or bool(assert_fingerprint), + ) + except BaseException: + ssl_sock.close() + raise + + +def _match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + asserted_hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + # Our upstream implementation of ssl.match_hostname() + # only applies this normalization to IP addresses so it doesn't + # match DNS SANs so we do the same thing! + stripped_hostname = asserted_hostname.strip("[]") + if is_ipaddress(stripped_hostname): + asserted_hostname = stripped_hostname + + try: + match_hostname(cert, asserted_hostname, hostname_checks_common_name) + except CertificateError as e: + log.warning( + "Certificate did not match expected hostname: %s. Certificate: %s", + asserted_hostname, + cert, + ) + # Add cert to exception and reraise so client code can inspect + # the cert when catching the exception, if they want to + e._peer_cert = cert # type: ignore[attr-defined] + raise + + +def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: + # Look for the phrase 'wrong version number', if found + # then we should warn the user that we're very sure that + # this proxy is HTTP-only and they have a configuration issue. + error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) + is_likely_http_proxy = ( + "wrong version number" in error_normalized + or "unknown protocol" in error_normalized + or "record layer failure" in error_normalized + ) + http_proxy_warning = ( + ". Your proxy appears to only use HTTP and not HTTPS, " + "try changing your proxy URL to be HTTP. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#https-proxy-error-http-proxy" + ) + new_err = ProxyError( + f"Unable to connect to proxy" + f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", + err, + ) + new_err.__cause__ = err + return new_err + + +def _get_default_user_agent() -> str: + return f"python-urllib3/{__version__}" + + +class DummyConnection: + """Used to detect a failed ConnectionCls import.""" + + +if not ssl: + HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 + + +VerifiedHTTPSConnection = HTTPSConnection + + +def _url_from_connection( + conn: HTTPConnection | HTTPSConnection, path: str | None = None +) -> str: + """Returns the URL from a given connection. This is mainly used for testing and logging.""" + + scheme = "https" if isinstance(conn, HTTPSConnection) else "http" + + return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py new file mode 100644 index 0000000000..70fbc5e725 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py @@ -0,0 +1,1191 @@ +from __future__ import annotations + +import errno +import logging +import queue +import sys +import typing +import warnings +import weakref +from socket import timeout as SocketTimeout +from types import TracebackType + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._request_methods import RequestMethods +from .connection import ( + BaseSSLError, + BrokenPipeError, + DummyConnection, + HTTPConnection, + HTTPException, + HTTPSConnection, + ProxyConfig, + _wrap_proxy_error, +) +from .connection import port_by_scheme as port_by_scheme +from .exceptions import ( + ClosedPoolError, + EmptyPoolError, + FullPoolError, + HostChangedError, + InsecureRequestWarning, + LocationValueError, + MaxRetryError, + NewConnectionError, + ProtocolError, + ProxyError, + ReadTimeoutError, + SSLError, + TimeoutError, +) +from .response import BaseHTTPResponse +from .util.connection import is_connection_dropped +from .util.proxy import connection_requires_http_tunnel +from .util.request import _TYPE_BODY_POSITION, set_file_position +from .util.retry import Retry +from .util.ssl_match_hostname import CertificateError +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout +from .util.url import Url, _encode_target +from .util.url import _normalize_host as normalize_host +from .util.url import parse_url +from .util.util import to_str + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + + from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + +_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] + + +# Pool objects +class ConnectionPool: + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + + .. note:: + ConnectionPool.urlopen() does not normalize or percent-encode target URIs + which is useful if your target server doesn't support percent-encoded + target URIs. + """ + + scheme: str | None = None + QueueCls = queue.LifoQueue + + def __init__(self, host: str, port: int | None = None) -> None: + if not host: + raise LocationValueError("No host specified.") + + self.host = _normalize_host(host, scheme=self.scheme) + self.port = port + + # This property uses 'normalize_host()' (not '_normalize_host()') + # to avoid removing square braces around IPv6 addresses. + # This value is sent to `HTTPConnection.set_tunnel()` if called + # because square braces are required for HTTP CONNECT tunneling. + self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.close() + # Return False to re-raise any potential exceptions + return False + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} + + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`http.client.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`http.client.HTTPConnection`. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to False, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param retries: + Retry configuration to use by default with requests in this pool. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.ProxyManager` + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.ProxyManager` + + :param \\**conn_kw: + Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, + :class:`urllib3.connection.HTTPSConnection` instances. + """ + + scheme = "http" + ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + _proxy_config: ProxyConfig | None = None, + **conn_kw: typing.Any, + ): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + if retries is None: + retries = Retry.DEFAULT + + self.timeout = timeout + self.retries = retries + + self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + self.proxy_config = _proxy_config + + # Fill the queue up so that doing get() on it will block properly + for _ in range(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + self.conn_kw = conn_kw + + if self.proxy: + # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. + # Defaulting `socket_options` to an empty list avoids it defaulting to + # ``HTTPConnection.default_socket_options``. + self.conn_kw.setdefault("socket_options", []) + + self.conn_kw["proxy"] = self.proxy + self.conn_kw["proxy_config"] = self.proxy_config + + # Do not pass 'self' as callback to 'finalize'. + # Then the 'finalize' would keep an endless living (leak) to self. + # By just passing a reference to the pool allows the garbage collector + # to free self if nobody else has a reference to it. + pool = self.pool + + # Close all the HTTPConnections in the pool before the + # HTTPConnectionPool object is garbage collected. + weakref.finalize(self, _close_pool_connections, pool) + + def _new_conn(self) -> BaseHTTPConnection: + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTP connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "80", + ) + + conn = self.ConnectionCls( + host=self.host, + port=self.port, + timeout=self.timeout.connect_timeout, + **self.conn_kw, + ) + return conn + + def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + + if self.pool is None: + raise ClosedPoolError(self, "Pool is closed.") + + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: + + except queue.Empty: + if self.block: + raise EmptyPoolError( + self, + "Pool is empty and a new connection can't be opened due to blocking mode.", + ) from None + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.debug("Resetting dropped connection: %s", self.host) + conn.close() + + return conn or self._new_conn() + + def _put_conn(self, conn: BaseHTTPConnection | None) -> None: + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + if self.pool is not None: + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except queue.Full: + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + if self.block: + # This should never happen if you got the conn from self._get_conn + raise FullPoolError( + self, + "Pool reached maximum size and no more connections are allowed.", + ) from None + + log.warning( + "Connection pool is full, discarding connection: %s. Connection pool size: %s", + self.host, + self.pool.qsize(), + ) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + + def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: + # Nothing to do for HTTP connections. + pass + + def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: + """Helper that always returns a :class:`urllib3.util.Timeout`""" + if timeout is _DEFAULT_TIMEOUT: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _raise_timeout( + self, + err: BaseSSLError | OSError | SocketTimeout, + url: str, + timeout_value: _TYPE_TIMEOUT | None, + ) -> None: + """Is the error actually a timeout? Will raise a ReadTimeout or pass""" + + if isinstance(err, SocketTimeout): + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + # See the above comment about EAGAIN in Python 3. + if hasattr(err, "errno") and err.errno in _blocking_errnos: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + def _make_request( + self, + conn: BaseHTTPConnection, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | None = None, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + chunked: bool = False, + response_conn: BaseHTTPConnection | None = None, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> BaseHTTPResponse: + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param response_conn: + Set this to ``None`` if you will handle releasing the connection or + set the connection to have the response release it. + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + timeout_obj.start_connect() + conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) + + try: + # Trigger any extra validation we need to do. + try: + self._validate_conn(conn) + except (SocketTimeout, BaseSSLError) as e: + self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) + raise + + # _validate_conn() starts the connection to an HTTPS proxy + # so we need to wrap errors with 'ProxyError' here too. + except ( + OSError, + NewConnectionError, + TimeoutError, + BaseSSLError, + CertificateError, + SSLError, + ) as e: + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + # If the connection didn't successfully connect to it's proxy + # then there + if isinstance( + new_e, (OSError, NewConnectionError, TimeoutError, SSLError) + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + raise new_e + + # conn.request() calls http.client.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + try: + conn.request( + method, + url, + body=body, + headers=headers, + chunked=chunked, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + # We are swallowing BrokenPipeError (errno.EPIPE) since the server is + # legitimately able to close the connection after sending a valid response. + # With this behaviour, the received response is still readable. + except BrokenPipeError: + pass + except OSError as e: + # MacOS/Linux + # EPROTOTYPE and ECONNRESET are needed on macOS + # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ + # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. + if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: + raise + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + if not conn.is_closed: + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={read_timeout})" + ) + conn.timeout = read_timeout + + # Receive the response from the server + try: + response = conn.getresponse() + except (BaseSSLError, OSError) as e: + self._raise_timeout(err=e, url=url, timeout_value=read_timeout) + raise + + # Set properties that are used by the pooling layer. + response.retries = retries + response._connection = response_conn # type: ignore[attr-defined] + response._pool = self # type: ignore[attr-defined] + + log.debug( + '%s://%s:%s "%s %s %s" %s %s', + self.scheme, + self.host, + self.port, + method, + url, + response.version_string, + response.status, + response.length_remaining, + ) + + return response + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + if self.pool is None: + return + # Disable access to the pool + old_pool, self.pool = self.pool, None + + # Close all the HTTPConnections in the pool. + _close_pool_connections(old_pool) + + def is_same_host(self, url: str) -> bool: + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith("/"): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + if host is not None: + host = _normalize_host(host, scheme=scheme) + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + redirect: bool = True, + assert_same_host: bool = True, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + pool_timeout: int | None = None, + release_conn: bool | None = None, + chunked: bool = False, + body_pos: _TYPE_BODY_POSITION | None = None, + preload_content: bool = True, + decode_content: bool = True, + **response_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method + such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When ``False``, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of ``preload_content`` + which defaults to ``True``. + + :param bool chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param int body_pos: + Position to seek to in file-like body in the event of a retry or + redirect. Typically this won't need to be set because urllib3 will + auto-populate the value when needed. + """ + # Ensure that the URL we're connecting to is properly encoded + if url.startswith("/"): + # URLs starting with / are inherently schemeless. + url = to_str(_encode_target(url)) + destination_scheme = None + else: + parsed_url = parse_url(url) + destination_scheme = parsed_url.scheme + url = to_str(parsed_url.url) + + if headers is None: + headers = self.headers + + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) + + if release_conn is None: + release_conn = preload_content + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries) + + conn = None + + # Track whether `conn` needs to be released before + # returning/raising/recursing. Update this variable if necessary, and + # leave `release_conn` constant throughout the function. That way, if + # the function recurses, the original value of `release_conn` will be + # passed down into the recursive call, and its value will be respected. + # + # See issue #651 [1] for details. + # + # [1] + release_this_conn = release_conn + + http_tunnel_required = connection_requires_http_tunnel( + self.proxy, self.proxy_config, destination_scheme + ) + + # Merge the proxy headers. Only done when not using HTTP CONNECT. We + # have to copy the headers dict so we can safely change it without those + # changes being reflected in anyone else's copy. + if not http_tunnel_required: + headers = headers.copy() # type: ignore[attr-defined] + headers.update(self.proxy_headers) # type: ignore[union-attr] + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + # Keep track of whether we cleanly exited the except block. This + # ensures we do proper cleanup in finally. + clean_exit = False + + # Rewind body position, if needed. Record current position + # for future rewinds in the event of a redirect/retry. + body_pos = set_file_position(body, body_pos) + + try: + # Request a connection from the queue. + timeout_obj = self._get_timeout(timeout) + conn = self._get_conn(timeout=pool_timeout) + + conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] + + # Is this a closed/new connection that requires CONNECT tunnelling? + if self.proxy is not None and http_tunnel_required and conn.is_closed: + try: + self._prepare_proxy(conn) + except (BaseSSLError, OSError, SocketTimeout) as e: + self._raise_timeout( + err=e, url=self.proxy.url, timeout_value=conn.timeout + ) + raise + + # If we're going to release the connection in ``finally:``, then + # the response doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = conn if not release_conn else None + + # Make the request on the HTTPConnection object + response = self._make_request( + conn, + method, + url, + timeout=timeout_obj, + body=body, + headers=headers, + chunked=chunked, + retries=retries, + response_conn=response_conn, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Everything went great! + clean_exit = True + + except EmptyPoolError: + # Didn't get a connection from the pool, no need to clean up + clean_exit = True + release_this_conn = False + raise + + except ( + TimeoutError, + HTTPException, + OSError, + ProtocolError, + BaseSSLError, + SSLError, + CertificateError, + ProxyError, + ) as e: + # Discard the connection for these exceptions. It will be + # replaced during the next _get_conn() call. + clean_exit = False + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + if isinstance( + new_e, + ( + OSError, + NewConnectionError, + TimeoutError, + SSLError, + HTTPException, + ), + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + elif isinstance(new_e, (OSError, HTTPException)): + new_e = ProtocolError("Connection aborted.", new_e) + + retries = retries.increment( + method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] + ) + retries.sleep() + + # Keep track of the error for the retry warning. + err = e + + finally: + if not clean_exit: + # We hit some kind of exception, handled or otherwise. We need + # to throw the connection away unless explicitly told not to. + # Close the connection, set the variable to None, and make sure + # we put the None back in the pool to avoid leaking it. + if conn: + conn.close() + conn = None + release_this_conn = True + + if release_this_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning( + "Retrying (%r) after connection broken by '%r': %s", retries, err, url + ) + return self.urlopen( + method, + url, + body, + headers, + retries, + redirect, + assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + body = None + headers = HTTPHeaderDict(headers)._prepare_for_method_change() + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # self.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not self.is_same_host( + redirect_location + ): + new_headers = headers.copy() # type: ignore[union-attr] + for header in headers: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + headers = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep_for_retry(response) + log.debug("Redirecting %s -> %s", url, redirect_location) + return self.urlopen( + method, + redirect_location, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Check if we should retry the HTTP response. + has_retry_after = bool(response.headers.get("Retry-After")) + if retries.is_retry(method, response.status, has_retry_after): + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_status: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep(response) + log.debug("Retry: %s", url) + return self.urlopen( + method, + url, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, + ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` + is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade + the connection socket into an SSL socket. + """ + + scheme = "https" + ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + ssl_version: int | str | None = None, + ssl_minimum_version: ssl.TLSVersion | None = None, + ssl_maximum_version: ssl.TLSVersion | None = None, + assert_hostname: str | typing.Literal[False] | None = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + **conn_kw: typing.Any, + ) -> None: + super().__init__( + host, + port, + timeout, + maxsize, + block, + headers, + retries, + _proxy, + _proxy_headers, + **conn_kw, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.ca_certs = ca_certs + self.ca_cert_dir = ca_cert_dir + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] + """Establishes a tunnel connection through HTTP CONNECT.""" + if self.proxy and self.proxy.scheme == "https": + tunnel_scheme = "https" + else: + tunnel_scheme = "http" + + conn.set_tunnel( + scheme=tunnel_scheme, + host=self._tunnel_host, + port=self.port, + headers=self.proxy_headers, + ) + conn.connect() + + def _new_conn(self) -> BaseHTTPSConnection: + """ + Return a fresh :class:`urllib3.connection.HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTPS connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "443", + ) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] + raise ImportError( + "Can't connect to HTTPS URL because the SSL module is not available." + ) + + actual_host: str = self.host + actual_port = self.port + if self.proxy is not None and self.proxy.host is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + return self.ConnectionCls( + host=actual_host, + port=actual_port, + timeout=self.timeout.connect_timeout, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + **self.conn_kw, + ) + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + super()._validate_conn(conn) + + # Force connect early to allow us to validate the connection. + if conn.is_closed: + conn.connect() + + # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 + if not conn.is_verified and not conn.proxy_is_verified: + warnings.warn( + ( + f"Unverified HTTPS request is being made to host '{conn.host}'. " + "Adding certificate verification is strongly advised. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#tls-warnings" + ), + InsecureRequestWarning, + ) + + +def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \\**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example:: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + port = port or port_by_scheme.get(scheme, 80) + if scheme == "https": + return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + else: + return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + """ + Normalize hosts for comparisons and use with sockets. + """ + + host = normalize_host(host, scheme) + + # httplib doesn't like it when we include brackets in IPv6 addresses + # Specifically, if we include brackets but also pass the port then + # httplib crazily doubles up the square brackets on the Host header. + # Instead, we need to make sure we never pass ``None`` as the port. + # However, for backward compatibility reasons we can't actually + # *assert* that. See http://bugs.python.org/issue28539 + if host and host.startswith("[") and host.endswith("]"): + host = host[1:-1] + return host + + +def _url_from_pool( + pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None +) -> str: + """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" + return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url + + +def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: + """Drains a queue of connections and closes each one.""" + try: + while True: + conn = pool.get(block=False) + if conn: + conn.close() + except queue.Empty: + pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py new file mode 100644 index 0000000000..e5b62b25e9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import urllib3.connection + +from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection + + +def inject_into_urllib3() -> None: + # override connection classes to use emscripten specific classes + # n.b. mypy complains about the overriding of classes below + # if it isn't ignored + HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection + HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection + urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] + urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] + urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py new file mode 100644 index 0000000000..63f79dd3be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import os +import typing + +# use http.client.HTTPException for consistency with non-emscripten +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady + +from ..._base_connection import _TYPE_BODY +from ...connection import HTTPConnection, ProxyConfig, port_by_scheme +from ...exceptions import TimeoutError +from ...response import BaseHTTPResponse +from ...util.connection import _TYPE_SOCKET_OPTIONS +from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from ...util.url import Url +from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request +from .request import EmscriptenRequest +from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + + +class EmscriptenHTTPConnection: + default_port: typing.ClassVar[int] = port_by_scheme["http"] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + timeout: None | (float) + + host: str + port: int + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool = False + proxy_is_verified: bool | None = None + + response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper + _response: EmscriptenResponse | None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + self.host = host + self.port = port + self.timeout = timeout if isinstance(timeout, float) else 0.0 + self.scheme = "http" + self._closed = True + self._response = None + # ignore these things because we don't + # have control over that stuff + self.proxy = None + self.proxy_config = None + self.blocksize = blocksize + self.source_address = None + self.socket_options = None + self.is_verified = False + + def set_tunnel( + self, + host: str, + port: int | None = 0, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + pass + + def connect(self) -> None: + pass + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + self._closed = False + if url.startswith("/"): + if self.port is not None: + port = f":{self.port}" + else: + port = "" + # no scheme / host / port included, make a full url + url = f"{self.scheme}://{self.host}{port}{url}" + request = EmscriptenRequest( + url=url, + method=method, + timeout=self.timeout if self.timeout else 0, + decode_content=decode_content, + ) + request.set_body(body) + if headers: + for k, v in headers.items(): + request.set_header(k, v) + self._response = None + try: + if not preload_content: + self._response = send_streaming_request(request) + if self._response is None: + self._response = send_request(request) + except _TimeoutError as e: + raise TimeoutError(e.message) from e + except _RequestError as e: + raise HTTPException(e.message) from e + + def getresponse(self) -> BaseHTTPResponse: + if self._response is not None: + return EmscriptenHttpResponseWrapper( + internal_response=self._response, + url=self._response.request.url, + connection=self, + ) + else: + raise ResponseNotReady() + + def close(self) -> None: + self._closed = True + self._response = None + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + return self._closed + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + return True + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + return False + + +class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): + default_port = port_by_scheme["https"] + # all this is basically ignored, as browser handles https + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + cert_file: str | None + key_file: str | None + key_password: str | None + ssl_context: typing.Any | None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None = None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: ( + None | _TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: typing.Any | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + self.scheme = "https" + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + self.cert_reqs = None + + # The browser will automatically verify all requests. + # We have no control over that setting. + self.is_verified = True + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + pass + + +# verify that this class implements BaseHTTP(s) connection correctly +if typing.TYPE_CHECKING: + _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) + _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js new file mode 100644 index 0000000000..faf141e1fa --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js @@ -0,0 +1,110 @@ +let Status = { + SUCCESS_HEADER: -1, + SUCCESS_EOF: -2, + ERROR_TIMEOUT: -3, + ERROR_EXCEPTION: -4, +}; + +let connections = new Map(); +let nextConnectionID = 1; +const encoder = new TextEncoder(); + +self.addEventListener("message", async function (event) { + if (event.data.close) { + let connectionID = event.data.close; + connections.delete(connectionID); + return; + } else if (event.data.getMore) { + let connectionID = event.data.getMore; + let { curOffset, value, reader, intBuffer, byteBuffer } = + connections.get(connectionID); + // if we still have some in buffer, then just send it back straight away + if (!value || curOffset >= value.length) { + // read another buffer if required + try { + let readResponse = await reader.read(); + + if (readResponse.done) { + // read everything - clear connection and return + connections.delete(connectionID); + Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); + Atomics.notify(intBuffer, 0); + // finished reading successfully + // return from event handler + return; + } + curOffset = 0; + connections.get(connectionID).value = readResponse.value; + value = readResponse.value; + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } + + // send as much buffer as we can + let curLen = value.length - curOffset; + if (curLen > byteBuffer.length) { + curLen = byteBuffer.length; + } + byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); + + Atomics.store(intBuffer, 0, curLen); // store current length in bytes + Atomics.notify(intBuffer, 0); + curOffset += curLen; + connections.get(connectionID).curOffset = curOffset; + + return; + } else { + // start fetch + let connectionID = nextConnectionID; + nextConnectionID += 1; + const intBuffer = new Int32Array(event.data.buffer); + const byteBuffer = new Uint8Array(event.data.buffer, 8); + try { + const response = await fetch(event.data.url, event.data.fetchParams); + // return the headers first via textencoder + var headers = []; + for (const pair of response.headers.entries()) { + headers.push([pair[0], pair[1]]); + } + let headerObj = { + headers: headers, + status: response.status, + connectionID, + }; + const headerText = JSON.stringify(headerObj); + let headerBytes = encoder.encode(headerText); + let written = headerBytes.length; + byteBuffer.set(headerBytes); + intBuffer[1] = written; + // make a connection + connections.set(connectionID, { + reader: response.body.getReader(), + intBuffer: intBuffer, + byteBuffer: byteBuffer, + value: undefined, + curOffset: 0, + }); + // set header ready + Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); + Atomics.notify(intBuffer, 0); + // all fetching after this goes through a new postmessage call with getMore + // this allows for parallel requests + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } +}); +self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py new file mode 100644 index 0000000000..612cfddc4c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py @@ -0,0 +1,726 @@ +""" +Support for streaming http requests in emscripten. + +A few caveats - + +If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled +https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md +*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the +JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case +timeouts and streaming should just work. + +Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. + +This approach has several caveats: + +Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. +Streaming only works if you're running pyodide in a web worker. + +Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch +operation, so it requires that you have crossOriginIsolation enabled, by serving over https +(or from localhost) with the two headers below set: + + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp + +You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in +JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole +request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. + +Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once +control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. + +NB: in this code, there are a lot of JavaScript objects. They are named js_* +to make it clear what type of object they are. +""" + +from __future__ import annotations + +import io +import json +from email.parser import Parser +from importlib.resources import files +from typing import TYPE_CHECKING, Any + +import js # type: ignore[import-not-found] +from pyodide.ffi import ( # type: ignore[import-not-found] + JsArray, + JsException, + JsProxy, + to_js, +) + +if TYPE_CHECKING: + from typing_extensions import Buffer + +from .request import EmscriptenRequest +from .response import EmscriptenResponse + +""" +There are some headers that trigger unintended CORS preflight requests. +See also https://github.com/koenvo/pyodide-http/issues/22 +""" +HEADERS_TO_IGNORE = ("user-agent",) + +SUCCESS_HEADER = -1 +SUCCESS_EOF = -2 +ERROR_TIMEOUT = -3 +ERROR_EXCEPTION = -4 + + +class _RequestError(Exception): + def __init__( + self, + message: str | None = None, + *, + request: EmscriptenRequest | None = None, + response: EmscriptenResponse | None = None, + ): + self.request = request + self.response = response + self.message = message + super().__init__(self.message) + + +class _StreamingError(_RequestError): + pass + + +class _TimeoutError(_RequestError): + pass + + +def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: + return to_js(dict_val, dict_converter=js.Object.fromEntries) + + +class _ReadStream(io.RawIOBase): + def __init__( + self, + int_buffer: JsArray, + byte_buffer: JsArray, + timeout: float, + worker: JsProxy, + connection_id: int, + request: EmscriptenRequest, + ): + self.int_buffer = int_buffer + self.byte_buffer = byte_buffer + self.read_pos = 0 + self.read_len = 0 + self.connection_id = connection_id + self.worker = worker + self.timeout = int(1000 * timeout) if timeout > 0 else None + self.is_live = True + self._is_closed = False + self.request: EmscriptenRequest | None = request + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.int_buffer = None + self.byte_buffer = None + self._is_closed = True + self.request = None + if self.is_live: + self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) + self.is_live = False + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def readinto(self, byte_obj: Buffer) -> int: + if not self.int_buffer: + raise _StreamingError( + "No buffer for stream in _ReadStream.readinto", + request=self.request, + response=None, + ) + if self.read_len == 0: + # wait for the worker to send something + js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) + self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) + if ( + js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) + == "timed-out" + ): + raise _TimeoutError + data_len = self.int_buffer[0] + if data_len > 0: + self.read_len = data_len + self.read_pos = 0 + elif data_len == ERROR_EXCEPTION: + string_len = self.int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", + request=self.request, + response=None, + ) + else: + # EOF, free the buffers and return zero + # and free the request + self.is_live = False + self.close() + return 0 + # copy from int32array to python bytes + ret_length = min(self.read_len, len(memoryview(byte_obj))) + subarray = self.byte_buffer.subarray( + self.read_pos, self.read_pos + ret_length + ).to_py() + memoryview(byte_obj)[0:ret_length] = subarray + self.read_len -= ret_length + self.read_pos += ret_length + return ret_length + + +class _StreamingFetcher: + def __init__(self) -> None: + # make web-worker and data buffer on startup + self.streaming_ready = False + streaming_worker_code = ( + files(__package__) + .joinpath("emscripten_fetch_worker.js") + .read_text(encoding="utf-8") + ) + js_data_blob = js.Blob.new( + to_js([streaming_worker_code], create_pyproxies=False), + _obj_from_dict({"type": "application/javascript"}), + ) + + def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: + def onMsg(e: JsProxy) -> None: + self.streaming_ready = True + js_resolve_fn(e) + + def onErr(e: JsProxy) -> None: + js_reject_fn(e) # Defensive: never happens in ci + + self.js_worker.onmessage = onMsg + self.js_worker.onerror = onErr + + js_data_url = js.URL.createObjectURL(js_data_blob) + self.js_worker = js.globalThis.Worker.new(js_data_url) + self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) + + def send(self, request: EmscriptenRequest) -> EmscriptenResponse: + headers = { + k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE + } + + body = request.body + fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} + # start the request off in the worker + timeout = int(1000 * request.timeout) if request.timeout > 0 else None + js_shared_buffer = js.SharedArrayBuffer.new(1048576) + js_int_buffer = js.Int32Array.new(js_shared_buffer) + js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) + + js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) + js.Atomics.notify(js_int_buffer, 0) + js_absolute_url = js.URL.new(request.url, js.location).href + self.js_worker.postMessage( + _obj_from_dict( + { + "buffer": js_shared_buffer, + "url": js_absolute_url, + "fetchParams": fetch_data, + } + ) + ) + # wait for the worker to send something + js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) + if js_int_buffer[0] == ERROR_TIMEOUT: + raise _TimeoutError( + "Timeout connecting to streaming request", + request=request, + response=None, + ) + elif js_int_buffer[0] == SUCCESS_HEADER: + # got response + # header length is in second int of intBuffer + string_len = js_int_buffer[1] + # decode the rest to a JSON string + js_decoder = js.TextDecoder.new() + # this does a copy (the slice) because decode can't work on shared array + # for some silly reason + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + # get it as an object + response_obj = json.loads(json_str) + return EmscriptenResponse( + request=request, + status_code=response_obj["status"], + headers=response_obj["headers"], + body=_ReadStream( + js_int_buffer, + js_byte_buffer, + request.timeout, + self.js_worker, + response_obj["connectionID"], + request, + ), + ) + elif js_int_buffer[0] == ERROR_EXCEPTION: + string_len = js_int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", request=request, response=None + ) + else: + raise _StreamingError( + f"Unknown status from worker in fetch: {js_int_buffer[0]}", + request=request, + response=None, + ) + + +class _JSPIReadStream(io.RawIOBase): + """ + A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch + response. This requires support for WebAssembly JavaScript Promise Integration + in the containing browser, and for pyodide to be launched via runPythonAsync. + + :param js_read_stream: + The JavaScript stream reader + + :param timeout: + Timeout in seconds + + :param request: + The request we're handling + + :param response: + The response this stream relates to + + :param js_abort_controller: + A JavaScript AbortController object, used for timeouts + """ + + def __init__( + self, + js_read_stream: Any, + timeout: float, + request: EmscriptenRequest, + response: EmscriptenResponse, + js_abort_controller: Any, # JavaScript AbortController for timeouts + ): + self.js_read_stream = js_read_stream + self.timeout = timeout + self._is_closed = False + self._is_done = False + self.request: EmscriptenRequest | None = request + self.response: EmscriptenResponse | None = response + self.current_buffer = None + self.current_buffer_pos = 0 + self.js_abort_controller = js_abort_controller + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.js_read_stream.cancel() + self.js_read_stream = None + self._is_closed = True + self._is_done = True + self.request = None + self.response = None + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def _get_next_buffer(self) -> bool: + result_js = _run_sync_with_timeout( + self.js_read_stream.read(), + self.timeout, + self.js_abort_controller, + request=self.request, + response=self.response, + ) + if result_js.done: + self._is_done = True + return False + else: + self.current_buffer = result_js.value.to_py() + self.current_buffer_pos = 0 + return True + + def readinto(self, byte_obj: Buffer) -> int: + if self.current_buffer is None: + if not self._get_next_buffer() or self.current_buffer is None: + self.close() + return 0 + ret_length = min( + len(byte_obj), len(self.current_buffer) - self.current_buffer_pos + ) + byte_obj[0:ret_length] = self.current_buffer[ + self.current_buffer_pos : self.current_buffer_pos + ret_length + ] + self.current_buffer_pos += ret_length + if self.current_buffer_pos == len(self.current_buffer): + self.current_buffer = None + return ret_length + + +# check if we are in a worker or not +def is_in_browser_main_thread() -> bool: + return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window + + +def is_cross_origin_isolated() -> bool: + return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated + + +def is_in_node() -> bool: + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + and hasattr(js.process.release, "name") + and js.process.release.name == "node" + ) + + +def is_worker_available() -> bool: + return hasattr(js, "Worker") and hasattr(js, "Blob") + + +_fetcher: _StreamingFetcher | None = None + +if is_worker_available() and ( + (is_cross_origin_isolated() and not is_in_browser_main_thread()) + and (not is_in_node()) +): + _fetcher = _StreamingFetcher() +else: + _fetcher = None + + +NODE_JSPI_ERROR = ( + "urllib3 only works in Node.js with pyodide.runPythonAsync" + " and requires the flag --experimental-wasm-stack-switching in " + " versions of node <24." +) + + +def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: + if has_jspi(): + return send_jspi_request(request, True) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + + if _fetcher and streaming_ready(): + return _fetcher.send(request) + else: + _show_streaming_warning() + return None + + +_SHOWN_TIMEOUT_WARNING = False + + +def _show_timeout_warning() -> None: + global _SHOWN_TIMEOUT_WARNING + if not _SHOWN_TIMEOUT_WARNING: + _SHOWN_TIMEOUT_WARNING = True + message = "Warning: Timeout is not available on main browser thread" + js.console.warn(message) + + +_SHOWN_STREAMING_WARNING = False + + +def _show_streaming_warning() -> None: + global _SHOWN_STREAMING_WARNING + if not _SHOWN_STREAMING_WARNING: + _SHOWN_STREAMING_WARNING = True + message = "Can't stream HTTP requests because: \n" + if not is_cross_origin_isolated(): + message += " Page is not cross-origin isolated\n" + if is_in_browser_main_thread(): + message += " Python is running in main browser thread\n" + if not is_worker_available(): + message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in + if streaming_ready() is False: + message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch +is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" + from js import console + + console.warn(message) + + +def send_request(request: EmscriptenRequest) -> EmscriptenResponse: + if has_jspi(): + return send_jspi_request(request, False) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + try: + js_xhr = js.XMLHttpRequest.new() + + if not is_in_browser_main_thread(): + js_xhr.responseType = "arraybuffer" + if request.timeout: + js_xhr.timeout = int(request.timeout * 1000) + else: + js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") + if request.timeout: + # timeout isn't available on the main thread - show a warning in console + # if it is set + _show_timeout_warning() + + js_xhr.open(request.method, request.url, False) + for name, value in request.headers.items(): + if name.lower() not in HEADERS_TO_IGNORE: + js_xhr.setRequestHeader(name, value) + + js_xhr.send(to_js(request.body)) + + headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) + + if not is_in_browser_main_thread(): + body = js_xhr.response.to_py().tobytes() + else: + body = js_xhr.response.encode("ISO-8859-15") + return EmscriptenResponse( + status_code=js_xhr.status, headers=headers, body=body, request=request + ) + except JsException as err: + if err.name == "TimeoutError": + raise _TimeoutError(err.message, request=request) + elif err.name == "NetworkError": + raise _RequestError(err.message, request=request) + else: + # general http error + raise _RequestError(err.message, request=request) + + +def send_jspi_request( + request: EmscriptenRequest, streaming: bool +) -> EmscriptenResponse: + """ + Send a request using WebAssembly JavaScript Promise Integration + to wrap the asynchronous JavaScript fetch api (experimental). + + :param request: + Request to send + + :param streaming: + Whether to stream the response + + :return: The response object + :rtype: EmscriptenResponse + """ + timeout = request.timeout + js_abort_controller = js.AbortController.new() + headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} + req_body = request.body + fetch_data = { + "headers": headers, + "body": to_js(req_body), + "method": request.method, + "signal": js_abort_controller.signal, + } + # Node.js returns the whole response (unlike opaqueredirect in browsers), + # so urllib3 can set `redirect: manual` to control redirects itself. + # https://stackoverflow.com/a/78524615 + if _is_node_js(): + fetch_data["redirect"] = "manual" + # Call JavaScript fetch (async api, returns a promise) + fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) + # Now suspend WebAssembly until we resolve that promise + # or time out. + response_js = _run_sync_with_timeout( + fetcher_promise_js, + timeout, + js_abort_controller, + request=request, + response=None, + ) + headers = {} + header_iter = response_js.headers.entries() + while True: + iter_value_js = header_iter.next() + if getattr(iter_value_js, "done", False): + break + else: + headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) + status_code = response_js.status + body: bytes | io.RawIOBase = b"" + + response = EmscriptenResponse( + status_code=status_code, headers=headers, body=b"", request=request + ) + if streaming: + # get via inputstream + if response_js.body is not None: + # get a reader from the fetch response + body_stream_js = response_js.body.getReader() + body = _JSPIReadStream( + body_stream_js, timeout, request, response, js_abort_controller + ) + else: + # get directly via arraybuffer + # n.b. this is another async JavaScript call. + body = _run_sync_with_timeout( + response_js.arrayBuffer(), + timeout, + js_abort_controller, + request=request, + response=response, + ).to_py() + response.body = body + return response + + +def _run_sync_with_timeout( + promise: Any, + timeout: float, + js_abort_controller: Any, + request: EmscriptenRequest | None, + response: EmscriptenResponse | None, +) -> Any: + """ + Await a JavaScript promise synchronously with a timeout which is implemented + via the AbortController + + :param promise: + Javascript promise to await + + :param timeout: + Timeout in seconds + + :param js_abort_controller: + A JavaScript AbortController object, used on timeout + + :param request: + The request being handled + + :param response: + The response being handled (if it exists yet) + + :raises _TimeoutError: If the request times out + :raises _RequestError: If the request raises a JavaScript exception + + :return: The result of awaiting the promise. + """ + timer_id = None + if timeout > 0: + timer_id = js.setTimeout( + js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) + ) + try: + from pyodide.ffi import run_sync + + # run_sync here uses WebAssembly JavaScript Promise Integration to + # suspend python until the JavaScript promise resolves. + return run_sync(promise) + except JsException as err: + if err.name == "AbortError": + raise _TimeoutError( + message="Request timed out", request=request, response=response + ) + else: + raise _RequestError(message=err.message, request=request, response=response) + finally: + if timer_id is not None: + js.clearTimeout(timer_id) + + +def has_jspi() -> bool: + """ + Return true if jspi can be used. + + This requires both browser support and also WebAssembly + to be in the correct state - i.e. that the javascript + call into python was async not sync. + + :return: True if jspi can be used. + :rtype: bool + """ + try: + from pyodide.ffi import can_run_sync, run_sync # noqa: F401 + + return bool(can_run_sync()) + except ImportError: + return False + + +def _is_node_js() -> bool: + """ + Check if we are in Node.js. + + :return: True if we are in Node.js. + :rtype: bool + """ + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + # According to the Node.js documentation, the release name is always "node". + and js.process.release.name == "node" + ) + + +def streaming_ready() -> bool | None: + if _fetcher: + return _fetcher.streaming_ready + else: + return None # no fetcher, return None to signify that + + +async def wait_for_streaming_ready() -> bool: + if _fetcher: + await _fetcher.js_worker_ready_promise + return True + else: + return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py new file mode 100644 index 0000000000..e692e692bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..._base_connection import _TYPE_BODY + + +@dataclass +class EmscriptenRequest: + method: str + url: str + params: dict[str, str] | None = None + body: _TYPE_BODY | None = None + headers: dict[str, str] = field(default_factory=dict) + timeout: float = 0 + decode_content: bool = True + + def set_header(self, name: str, value: str) -> None: + self.headers[name.capitalize()] = value + + def set_body(self, body: _TYPE_BODY | None) -> None: + self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py new file mode 100644 index 0000000000..ec1e1dbe83 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import json as _json +import logging +import typing +from contextlib import contextmanager +from dataclasses import dataclass +from http.client import HTTPException as HTTPException +from io import BytesIO, IOBase + +from ...exceptions import InvalidHeader, TimeoutError +from ...response import BaseHTTPResponse +from ...util.retry import Retry +from .request import EmscriptenRequest + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + + +@dataclass +class EmscriptenResponse: + status_code: int + headers: dict[str, str] + body: IOBase | bytes + request: EmscriptenRequest + + +class EmscriptenHttpResponseWrapper(BaseHTTPResponse): + def __init__( + self, + internal_response: EmscriptenResponse, + url: str | None = None, + connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, + ): + self._pool = None # set by pool class + self._body = None + self._uncached_read_occurred = False + self._response = internal_response + self._url = url + self._connection = connection + self._closed = False + super().__init__( + headers=internal_response.headers, + status=internal_response.status_code, + request_url=url, + version=0, + version_string="HTTP/?", + reason="", + decode_content=True, + ) + self.length_remaining = self._init_length(self._response.request.method) + self.length_is_certain = False + + @property + def url(self) -> str | None: + return self._url + + @url.setter + def url(self, url: str | None) -> None: + self._url = url + + @property + def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: + return self._connection + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + while True: + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + else: + break + + def _init_length(self, request_method: str | None) -> int | None: + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Check for responses that shouldn't include a body + if ( + self.status in (204, 304) + or 100 <= self.status < 200 + or request_method == "HEAD" + ): + length = 0 + + return length + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, # ignored because browser decodes always + cache_content: bool = False, + ) -> bytes: + if ( + self._closed + or self._response is None + or (isinstance(self._response.body, IOBase) and self._response.body.closed) + ): + return b"" + + with self._error_catcher(): + # body has been preloaded as a string by XmlHttpRequest + if not isinstance(self._response.body, IOBase): + self.length_remaining = len(self._response.body) + self.length_is_certain = True + # wrap body in IOStream + self._response.body = BytesIO(self._response.body) + if amt is not None and amt >= 0: + # don't cache partial content + cache_content = False + data = self._response.body.read(amt) + self._uncached_read_occurred = True + else: # read all we can (and cache it) + data = self._response.body.read() + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + self._uncached_read_occurred = True + if self.length_remaining is not None: + self.length_remaining = max(self.length_remaining - len(data), 0) + if len(data) == 0 or ( + self.length_is_certain and self.length_remaining == 0 + ): + # definitely finished reading, close response stream + self._response.body.close() + return typing.cast(bytes, data) + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Generator[bytes]: + # chunked is handled by browser + while True: + bytes = self.read(amt, decode_content) + if not bytes: + break + yield bytes + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + self.close() + + @property + def data(self) -> bytes: + if self._body: + return self._body + else: + return self.read(cache_content=True) + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + def close(self) -> None: + if not self._closed: + if isinstance(self._response.body, IOBase): + self._response.body.close() + if self._connection: + self._connection.close() + self._connection = None + self._closed = True + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch Emscripten specific exceptions thrown by fetch.py, + instead re-raising urllib3 variants, so that low-level exceptions + are not leaked in the high-level api. + + On exit, release the connection back to the pool. + """ + from .fetch import _RequestError, _TimeoutError # avoid circular import + + clean_exit = False + + try: + yield + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + except _TimeoutError as e: + raise TimeoutError(str(e)) + except _RequestError as e: + raise HTTPException(str(e)) + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now + if ( + isinstance(self._response.body, IOBase) + and not self._response.body.closed + ): + self._response.body.close() + # release the connection back to the pool + self.release_conn() + else: + # If we have read everything from the response stream, + # return the connection back to the pool. + if ( + isinstance(self._response.body, IOBase) + and self._response.body.closed + ): + self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py new file mode 100644 index 0000000000..f06b859992 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py @@ -0,0 +1,563 @@ +""" +Module for using pyOpenSSL as a TLS backend. This module was relevant before +the standard library ``ssl`` module supported SNI, but now that we've dropped +support for Python 2.7 all relevant Python versions support SNI so +**this module is no longer recommended**. + +This needs the following packages installed: + +* `pyOpenSSL`_ (tested with 19.0.0) +* `cryptography`_ (minimum 2.3, from pyopenssl) +* `idna`_ (minimum 2.1, from cryptography) + +However, pyOpenSSL depends on cryptography, so while we use all three directly here we +end up having relatively few packages required. + +You can install them with the following command: + +.. code-block:: bash + + $ python -m pip install pyopenssl cryptography idna + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this: + +.. code-block:: python + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +.. _pyopenssl: https://www.pyopenssl.org +.. _cryptography: https://cryptography.io +.. _idna: https://github.com/kjd/idna +""" + +from __future__ import annotations + +import OpenSSL.SSL # type: ignore[import-not-found] +from cryptography import x509 + +try: + from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] +except ImportError: + # UnsupportedExtension is gone in cryptography >= 2.1.0 + class UnsupportedExtension(Exception): # type: ignore[no-redef] + pass + + +import logging +import ssl +import typing +from io import BytesIO +from socket import socket as socket_cls + +from .. import util + +if typing.TYPE_CHECKING: + from OpenSSL.crypto import X509 # type: ignore[import-not-found] + + +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions: dict[int, int] = { + util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} +_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} + +# The SSLvX values are the most likely to be missing in the future +# but we check them all just to be sure. +_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( + OpenSSL.SSL, "OP_NO_SSLv3", 0 +) +_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) +_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) +_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) +_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) + +_openssl_to_ssl_minimum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, + ssl.TLSVersion.TLSv1_3: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), + ssl.TLSVersion.MAXIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), +} +_openssl_to_ssl_maximum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 + | _OP_NO_TLSv1 + | _OP_NO_TLSv1_1 + | _OP_NO_TLSv1_2 + | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, +} + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_SSLContext = util.ssl_.SSLContext + + +log = logging.getLogger(__name__) + + +def inject_into_urllib3() -> None: + "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." + + _validate_dependencies_met() + + util.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.IS_PYOPENSSL = True + util.ssl_.IS_PYOPENSSL = True + + +def extract_from_urllib3() -> None: + "Undo monkey-patching by :func:`inject_into_urllib3`." + + util.SSLContext = orig_util_SSLContext + util.ssl_.SSLContext = orig_util_SSLContext + util.IS_PYOPENSSL = False + util.ssl_.IS_PYOPENSSL = False + + +def _validate_dependencies_met() -> None: + """ + Verifies that PyOpenSSL's package-level dependencies have been met. + Throws `ImportError` if they are not met. + """ + # Method added in `cryptography==1.1`; not available in older versions + from cryptography.x509.extensions import Extensions + + if getattr(Extensions, "get_extension_for_class", None) is None: + raise ImportError( + "'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer." + ) + + # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 + # attribute is only present on those versions. + from OpenSSL.crypto import X509 + + x509 = X509() + if getattr(x509, "_x509", None) is None: + raise ImportError( + "'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer." + ) + + +def _dnsname_to_stdlib(name: str) -> str | None: + """ + Converts a dNSName SubjectAlternativeName field to the form used by the + standard library on the given Python version. + + Cryptography produces a dNSName as a unicode string that was idna-decoded + from ASCII bytes. We need to idna-encode that string to get it back, and + then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib + uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). + + If the name cannot be idna-encoded then we return None signalling that + the name given should be skipped. + """ + + def idna_encode(name: str) -> bytes | None: + """ + Borrowed wholesale from the Python Cryptography Project. It turns out + that we can't just safely call `idna.encode`: it can explode for + wildcard names. This avoids that problem. + """ + import idna + + try: + for prefix in ["*.", "."]: + if name.startswith(prefix): + name = name[len(prefix) :] + return prefix.encode("ascii") + idna.encode(name) + return idna.encode(name) + except idna.core.IDNAError: + return None + + # Don't send IPv6 addresses through the IDNA encoder. + if ":" in name: + return name + + encoded_name = idna_encode(name) + if encoded_name is None: + return None + return encoded_name.decode("utf-8") + + +def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: + """ + Given an PyOpenSSL certificate, provides all the subject alternative names. + """ + cert = peer_cert.to_cryptography() + + # We want to find the SAN extension. Ask Cryptography to locate it (it's + # faster than looping in Python) + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + except x509.ExtensionNotFound: + # No such extension, return the empty list. + return [] + except ( + x509.DuplicateExtension, + UnsupportedExtension, + x509.UnsupportedGeneralNameType, + UnicodeError, + ) as e: + # A problem has been found with the quality of the certificate. Assume + # no SAN field is present. + log.warning( + "A problem was encountered with the certificate that prevented " + "urllib3 from finding the SubjectAlternativeName field. This can " + "affect certificate validation. The error was %s", + e, + ) + return [] + + # We want to return dNSName and iPAddress fields. We need to cast the IPs + # back to strings because the match_hostname function wants them as + # strings. + # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 + # decoded. This is pretty frustrating, but that's what the standard library + # does with certificates, and so we need to attempt to do the same. + # We also want to skip over names which cannot be idna encoded. + names = [ + ("DNS", name) + for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) + if name is not None + ] + names.extend( + ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) + ) + + return names + + +class WrappedSocket: + """API-compatibility wrapper for Python OpenSSL's Connection-class.""" + + def __init__( + self, + connection: OpenSSL.SSL.Connection, + socket: socket_cls, + suppress_ragged_eofs: bool = True, + ) -> None: + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._io_refs = 0 + self._closed = False + + def fileno(self) -> int: + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self) -> None: + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return b"" + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b"" + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + else: + return data # type: ignore[no-any-return] + + def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: + try: + return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return 0 + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return 0 + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv_into(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + + def settimeout(self, timeout: float) -> None: + return self.socket.settimeout(timeout) + + def _send_until_done(self, data: bytes) -> int: + while True: + try: + return self.connection.send(data) # type: ignore[no-any-return] + except OpenSSL.SSL.WantWriteError as e: + if not util.wait_for_write(self.socket, self.socket.gettimeout()): + raise TimeoutError() from e + continue + except OpenSSL.SSL.SysCallError as e: + raise OSError(e.args[0], str(e)) from e + + def sendall(self, data: bytes) -> None: + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done( + data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] + ) + total_sent += sent + + def shutdown(self, how: int) -> None: + try: + self.connection.shutdown() + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"shutdown error: {e!r}") from e + + def close(self) -> None: + self._closed = True + if self._io_refs <= 0: + self._real_close() + + def _real_close(self) -> None: + try: + return self.connection.close() # type: ignore[no-any-return] + except OpenSSL.SSL.Error: + return + + def getpeercert( + self, binary_form: bool = False + ) -> dict[str, list[typing.Any]] | None: + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 # type: ignore[no-any-return] + + if binary_form: + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] + + return { + "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] + "subjectAltName": get_subj_alt_name(x509), + } + + def version(self) -> str: + return self.connection.get_protocol_version_name() # type: ignore[no-any-return] + + def selected_alpn_protocol(self) -> str | None: + alpn_proto = self.connection.get_alpn_proto_negotiated() + return alpn_proto.decode() if alpn_proto else None + + +WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] + + +class PyOpenSSLContext: + """ + I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible + for translating the interface of the standard library ``SSLContext`` object + to calls into PyOpenSSL. + """ + + def __init__(self, protocol: int) -> None: + self.protocol = _openssl_versions[protocol] + self._ctx = OpenSSL.SSL.Context(self.protocol) + self._options = 0 + self.check_hostname = False + self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED + self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED + self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST + + @property + def options(self) -> int: + return self._options + + @options.setter + def options(self, value: int) -> None: + self._options = value + self._set_ctx_options() + + @property + def verify_flags(self) -> int: + return self._verify_flags + + @verify_flags.setter + def verify_flags(self, value: int) -> None: + self._verify_flags = value + self._ctx.get_cert_store().set_flags(self._verify_flags) + + @property + def verify_mode(self) -> int: + return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] + + @verify_mode.setter + def verify_mode(self, value: ssl.VerifyMode) -> None: + self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) + + def set_default_verify_paths(self) -> None: + self._ctx.set_default_verify_paths() + + def set_ciphers(self, ciphers: bytes | str) -> None: + if isinstance(ciphers, str): + ciphers = ciphers.encode("utf-8") + self._ctx.set_cipher_list(ciphers) + + def load_verify_locations( + self, + cafile: str | None = None, + capath: str | None = None, + cadata: bytes | None = None, + ) -> None: + if cafile is not None: + cafile = cafile.encode("utf-8") # type: ignore[assignment] + if capath is not None: + capath = capath.encode("utf-8") # type: ignore[assignment] + try: + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e + + def load_cert_chain( + self, + certfile: str, + keyfile: str | None = None, + password: str | None = None, + ) -> None: + try: + self._ctx.use_certificate_chain_file(certfile) + if password is not None: + if not isinstance(password, bytes): + password = password.encode("utf-8") # type: ignore[assignment] + self._ctx.set_passwd_cb(lambda *_: password) + self._ctx.use_privatekey_file(keyfile or certfile) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e + + def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: + protocols = [util.util.to_bytes(p, "ascii") for p in protocols] + return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] + + def wrap_socket( + self, + sock: socket_cls, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bytes | str | None = None, + ) -> WrappedSocket: + cnx = OpenSSL.SSL.Connection(self._ctx, sock) + + # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 + if server_hostname and not util.ssl_.is_ipaddress(server_hostname): + if isinstance(server_hostname, str): + server_hostname = server_hostname.encode("utf-8") + cnx.set_tlsext_host_name(server_hostname) + + cnx.set_connect_state() + + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(sock, sock.gettimeout()): + raise TimeoutError("select timed out") from e + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"bad handshake: {e!r}") from e + break + + return WrappedSocket(cnx, sock) + + def _set_ctx_options(self) -> None: + self._ctx.set_options( + self._options + | _openssl_to_ssl_minimum_version[self._minimum_version] + | _openssl_to_ssl_maximum_version[self._maximum_version] + ) + + @property + def minimum_version(self) -> int: + return self._minimum_version + + @minimum_version.setter + def minimum_version(self, minimum_version: int) -> None: + self._minimum_version = minimum_version + self._set_ctx_options() + + @property + def maximum_version(self) -> int: + return self._maximum_version + + @maximum_version.setter + def maximum_version(self, maximum_version: int) -> None: + self._maximum_version = maximum_version + self._set_ctx_options() + + +def _verify_callback( + cnx: OpenSSL.SSL.Connection, + x509: X509, + err_no: int, + err_depth: int, + return_code: int, +) -> bool: + return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py new file mode 100644 index 0000000000..d37da8fc20 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py @@ -0,0 +1,228 @@ +""" +This module contains provisional support for SOCKS proxies from within +urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and +SOCKS5. To enable its functionality, either install PySocks or install this +module with the ``socks`` extra. + +The SOCKS implementation supports the full range of urllib3 features. It also +supports the following SOCKS features: + +- SOCKS4A (``proxy_url='socks4a://...``) +- SOCKS4 (``proxy_url='socks4://...``) +- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) +- SOCKS5 with local DNS (``proxy_url='socks5://...``) +- Usernames and passwords for the SOCKS proxy + +.. note:: + It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in + your ``proxy_url`` to ensure that DNS resolution is done from the remote + server instead of client-side when connecting to a domain name. + +SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 +supports IPv4, IPv6, and domain names. + +When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` +will be sent as the ``userid`` section of the SOCKS request: + +.. code-block:: python + + proxy_url="socks4a://@proxy-host" + +When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion +of the ``proxy_url`` will be sent as the username/password to authenticate +with the proxy: + +.. code-block:: python + + proxy_url="socks5h://:@proxy-host" + +""" + +from __future__ import annotations + +try: + import socks # type: ignore[import-untyped] +except ImportError: + import warnings + + from ..exceptions import DependencyWarning + + warnings.warn( + ( + "SOCKS support in urllib3 requires the installation of optional " + "dependencies: specifically, PySocks. For more information, see " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" + ), + DependencyWarning, + ) + raise + +import typing +from socket import timeout as SocketTimeout + +from ..connection import HTTPConnection, HTTPSConnection +from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from ..exceptions import ConnectTimeoutError, NewConnectionError +from ..poolmanager import PoolManager +from ..util.url import parse_url + +try: + import ssl +except ImportError: + ssl = None # type: ignore[assignment] + + +class _TYPE_SOCKS_OPTIONS(typing.TypedDict): + socks_version: int + proxy_host: str | None + proxy_port: str | None + username: str | None + password: str | None + rdns: bool + + +class SOCKSConnection(HTTPConnection): + """ + A plain-text HTTP connection that connects via a SOCKS proxy. + """ + + def __init__( + self, + _socks_options: _TYPE_SOCKS_OPTIONS, + *args: typing.Any, + **kwargs: typing.Any, + ) -> None: + self._socks_options = _socks_options + super().__init__(*args, **kwargs) + + def _new_conn(self) -> socks.socksocket: + """ + Establish a new connection via the SOCKS proxy. + """ + extra_kw: dict[str, typing.Any] = {} + if self.source_address: + extra_kw["source_address"] = self.source_address + + if self.socket_options: + extra_kw["socket_options"] = self.socket_options + + try: + conn = socks.create_connection( + (self.host, self.port), + proxy_type=self._socks_options["socks_version"], + proxy_addr=self._socks_options["proxy_host"], + proxy_port=self._socks_options["proxy_port"], + proxy_username=self._socks_options["username"], + proxy_password=self._socks_options["password"], + proxy_rdns=self._socks_options["rdns"], + timeout=self.timeout, + **extra_kw, + ) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except socks.ProxyError as e: + # This is fragile as hell, but it seems to be the only way to raise + # useful errors here. + if e.socket_err: + error = e.socket_err + if isinstance(error, SocketTimeout): + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + else: + # Adding `from e` messes with coverage somehow, so it's omitted. + # See #2386. + raise NewConnectionError( + self, f"Failed to establish a new connection: {error}" + ) + else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + except OSError as e: # Defensive: PySocks should catch all these. + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + return conn + + +# We don't need to duplicate the Verified/Unverified distinction from +# urllib3/connection.py here because the HTTPSConnection will already have been +# correctly set to either the Verified or Unverified form by that module. This +# means the SOCKSHTTPSConnection will automatically be the correct type. +class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): + pass + + +class SOCKSHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = SOCKSConnection + + +class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): + ConnectionCls = SOCKSHTTPSConnection + + +class SOCKSProxyManager(PoolManager): + """ + A version of the urllib3 ProxyManager that routes connections via the + defined SOCKS proxy. + """ + + pool_classes_by_scheme = { + "http": SOCKSHTTPConnectionPool, + "https": SOCKSHTTPSConnectionPool, + } + + def __init__( + self, + proxy_url: str, + username: str | None = None, + password: str | None = None, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ): + parsed = parse_url(proxy_url) + + if username is None and password is None and parsed.auth is not None: + split = parsed.auth.split(":") + if len(split) == 2: + username, password = split + if parsed.scheme == "socks5": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = False + elif parsed.scheme == "socks5h": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = True + elif parsed.scheme == "socks4": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = False + elif parsed.scheme == "socks4a": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = True + else: + raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") + + self.proxy_url = proxy_url + + socks_options = { + "socks_version": socks_version, + "proxy_host": parsed.host, + "proxy_port": parsed.port, + "username": username, + "password": password, + "rdns": rdns, + } + connection_pool_kw["_socks_options"] = socks_options + + super().__init__(num_pools, headers, **connection_pool_kw) + + self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py new file mode 100644 index 0000000000..3d7e9b93d3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import socket +import typing +import warnings +from email.errors import MessageDefect +from http.client import IncompleteRead as httplib_IncompleteRead + +if typing.TYPE_CHECKING: + from .connection import HTTPConnection + from .connectionpool import ConnectionPool + from .response import HTTPResponse + from .util.retry import Retry + +# Base Exceptions + + +class HTTPError(Exception): + """Base exception used by this module.""" + + +class HTTPWarning(Warning): + """Base warning used by this module.""" + + +_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] + + +class PoolError(HTTPError): + """Base exception for errors caused within a pool.""" + + def __init__(self, pool: ConnectionPool, message: str) -> None: + self.pool = pool + self._message = message + super().__init__(f"{pool}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + +class RequestError(PoolError): + """Base exception for PoolErrors that have associated URLs.""" + + def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: + self.url = url + super().__init__(pool, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self._message) + + +class SSLError(HTTPError): + """Raised when SSL certificate fails in an HTTPS connection.""" + + +class ProxyError(HTTPError): + """Raised when the connection to a proxy fails.""" + + # The original error is also available as __cause__. + original_error: Exception + + def __init__(self, message: str, error: Exception) -> None: + super().__init__(message, error) + self.original_error = error + + +class DecodeError(HTTPError): + """Raised when automatic decoding based on Content-Type fails.""" + + +class ProtocolError(HTTPError): + """Raised when something unexpected happens mid-request/response.""" + + +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + +# Leaf Exceptions + + +class MaxRetryError(RequestError): + """Raised when the maximum number of retries is exceeded. + + :param pool: The connection pool + :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` + :param str url: The requested Url + :param reason: The underlying error + :type reason: :class:`Exception` + + """ + + def __init__( + self, pool: ConnectionPool, url: str | None, reason: Exception | None = None + ) -> None: + self.reason = reason + + message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" + + super().__init__(pool, url, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self.reason) + + +class HostChangedError(RequestError): + """Raised when an existing pool gets a request for a foreign host.""" + + def __init__( + self, pool: ConnectionPool, url: str, retries: Retry | int = 3 + ) -> None: + message = f"Tried to open a foreign host with url: {url}" + super().__init__(pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """Raised when passing an invalid state to a timeout""" + + +class TimeoutError(HTTPError): + """Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + ` and :exc:`ConnectTimeoutErrors `. + """ + + +class ReadTimeoutError(TimeoutError, RequestError): + """Raised when a socket timeout occurs while receiving data from a server""" + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + """Raised when a socket timeout occurs while connecting to a server""" + + +class NewConnectionError(ConnectTimeoutError, HTTPError): + """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" + + def __init__(self, conn: HTTPConnection, message: str) -> None: + self.conn = conn + self._message = message + super().__init__(f"{conn}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + @property + def pool(self) -> HTTPConnection: + warnings.warn( + "The 'pool' property is deprecated and will be removed " + "in urllib3 v3.0. Use 'conn' instead.", + FutureWarning, + stacklevel=2, + ) + + return self.conn + + +class NameResolutionError(NewConnectionError): + """Raised when host name resolution fails.""" + + def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): + message = f"Failed to resolve '{host}' ({reason})" + self._host = host + self._reason = reason + super().__init__(conn, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (self._host, None, self._reason) + + +class EmptyPoolError(PoolError): + """Raised when a pool runs out of connections and no more are allowed.""" + + +class FullPoolError(PoolError): + """Raised when we try to add a connection to a full pool in blocking mode.""" + + +class ClosedPoolError(PoolError): + """Raised when a request enters a pool after the pool has been closed.""" + + +class LocationValueError(ValueError, HTTPError): + """Raised when there is something wrong with a given URL input.""" + + +class LocationParseError(LocationValueError): + """Raised when get_host or similar fails to parse the URL input.""" + + def __init__(self, location: str) -> None: + message = f"Failed to parse: {location}" + super().__init__(message) + + self.location = location + + +class URLSchemeUnknown(LocationValueError): + """Raised when a URL input has an unsupported scheme.""" + + def __init__(self, scheme: str): + message = f"Not supported URL scheme {scheme}" + super().__init__(message) + + self.scheme = scheme + + +class ResponseError(HTTPError): + """Used as a container for an error reason supplied in a MaxRetryError.""" + + GENERIC_ERROR = "too many error responses" + SPECIFIC_ERROR = "too many {status_code} error responses" + + +class SecurityWarning(HTTPWarning): + """Warned when performing security reducing actions""" + + +class InsecureRequestWarning(SecurityWarning): + """Warned when making an unverified HTTPS request.""" + + +class NotOpenSSLWarning(SecurityWarning): + """Warned when using unsupported SSL library""" + + +class SystemTimeWarning(SecurityWarning): + """Warned when system time is suspected to be wrong""" + + +class InsecurePlatformWarning(SecurityWarning): + """Warned when certain TLS/SSL configuration is not available on a platform.""" + + +class DependencyWarning(HTTPWarning): + """ + Warned when an attempt is made to import a module with missing optional + dependencies. + """ + + +class ResponseNotChunked(ProtocolError, ValueError): + """Response needs to be chunked in order to read it as chunks.""" + + +class BodyNotHttplibCompatible(HTTPError): + """ + Body should be :class:`http.client.HTTPResponse` like + (have an fp attribute which returns raw chunks) for read_chunked(). + """ + + +class IncompleteRead(HTTPError, httplib_IncompleteRead): + """ + Response length doesn't match expected Content-Length + + Subclass of :class:`http.client.IncompleteRead` to allow int value + for ``partial`` to avoid creating large objects on streamed reads. + """ + + partial: int # type: ignore[assignment] + expected: int + + def __init__(self, partial: int, expected: int) -> None: + self.partial = partial + self.expected = expected + + def __repr__(self) -> str: + return "IncompleteRead(%i bytes read, %i more expected)" % ( + self.partial, + self.expected, + ) + + +class InvalidChunkLength(HTTPError, httplib_IncompleteRead): + """Invalid chunk length in a chunked response.""" + + def __init__(self, response: HTTPResponse, length: bytes) -> None: + self.partial: int = response.tell() # type: ignore[assignment] + self.expected: int | None = response.length_remaining + self.response = response + self.length = length + + def __repr__(self) -> str: + return "InvalidChunkLength(got length %r, %i bytes read)" % ( + self.length, + self.partial, + ) + + +class InvalidHeader(HTTPError): + """The header provided was somehow invalid.""" + + +class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): + """ProxyManager does not support the supplied scheme""" + + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. + + def __init__(self, scheme: str | None) -> None: + # 'localhost' is here because our URL parser parses + # localhost:8080 -> scheme=localhost, remove if we fix this. + if scheme == "localhost": + scheme = None + if scheme is None: + message = "Proxy URL had no scheme, should start with http:// or https://" + else: + message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" + super().__init__(message) + + +class ProxySchemeUnsupported(ValueError): + """Fetching HTTPS resources through HTTPS proxies is unsupported""" + + +class HeaderParsingError(HTTPError): + """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" + + def __init__( + self, defects: list[MessageDefect], unparsed_data: bytes | str | None + ) -> None: + message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" + super().__init__(message) + + +class UnrewindableBodyError(HTTPError): + """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py new file mode 100644 index 0000000000..fe68e17732 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import email.utils +import mimetypes +import typing + +_TYPE_FIELD_VALUE = typing.Union[str, bytes] +_TYPE_FIELD_VALUE_TUPLE = typing.Union[ + _TYPE_FIELD_VALUE, + tuple[str, _TYPE_FIELD_VALUE], + tuple[str, _TYPE_FIELD_VALUE, str], +] + + +def guess_content_type( + filename: str | None, default: str = "application/octet-stream" +) -> str: + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetypes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Helper function to format and quote a single header parameter using the + strategy defined in RFC 2231. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows + `RFC 2388 Section 4.4 `_. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as ``bytes`` or `str``. + :returns: + An RFC-2231-formatted unicode string. + + .. deprecated:: 2.0.0 + Will be removed in urllib3 v3.0. This is not valid for + ``multipart/form-data`` header parameters. + """ + import warnings + + warnings.warn( + "'format_header_param_rfc2231' is insecure, deprecated and will be " + "removed in urllib3 v3.0. This is not valid for " + "multipart/form-data header parameters.", + FutureWarning, + stacklevel=2, + ) + + if isinstance(value, bytes): + value = value.decode("utf-8") + + if not any(ch in value for ch in '"\\\r\n'): + result = f'{name}="{value}"' + try: + result.encode("ascii") + except (UnicodeEncodeError, UnicodeDecodeError): + pass + else: + return result + + value = email.utils.encode_rfc2231(value, "utf-8") + value = f"{name}*={value}" + + return value + + +def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Format and quote a single multipart header parameter. + + This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching + the behavior of current browser and curl versions. Values are + assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are + percent encoded. + + .. _WHATWG HTML Standard: + https://html.spec.whatwg.org/multipage/ + form-control-infrastructure.html#multipart-form-data + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + :returns: + A string ``name="value"`` with the escaped value. + + .. versionchanged:: 2.0.0 + Matches the WHATWG HTML Standard as of 2021/06/10. Control + characters are no longer percent encoded. + + .. versionchanged:: 2.0.0 + Renamed from ``format_header_param_html5`` and + ``format_header_param``. The old names will be removed in + urllib3 v3.0. + """ + if isinstance(value, bytes): + value = value.decode("utf-8") + + # percent encode \n \r " + value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) + return f'{name}="{value}"' + + +def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param_html5' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +class RequestField: + """ + A data container for request body parameters. + + :param name: + The name of this request field. Must be unicode. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. Must be unicode. + :param headers: + An optional dict-like object of headers to initially use for the field. + + .. versionchanged:: 2.0.0 + The ``header_formatter`` parameter is deprecated and will + be removed in urllib3 v3.0. + """ + + def __init__( + self, + name: str, + data: _TYPE_FIELD_VALUE, + filename: str | None = None, + headers: typing.Mapping[str, str] | None = None, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ): + self._name = name + self._filename = filename + self.data = data + self.headers: dict[str, str | None] = {} + if headers: + self.headers = dict(headers) + + if header_formatter is not None: + import warnings + + warnings.warn( + "The 'header_formatter' parameter is deprecated and " + "will be removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + self.header_formatter = header_formatter + else: + self.header_formatter = format_multipart_header_param + + @classmethod + def from_tuples( + cls, + fieldname: str, + value: _TYPE_FIELD_VALUE_TUPLE, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ) -> RequestField: + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from + parameter of key/value strings AND key/filetuple. A filetuple is a + (filename, data, MIME type) tuple where the MIME type is optional. + For example:: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + filename: str | None + content_type: str | None + data: _TYPE_FIELD_VALUE + + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls( + fieldname, data, filename=filename, header_formatter=header_formatter + ) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Override this method to change how each multipart header + parameter is formatted. By default, this calls + :func:`format_multipart_header_param`. + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + + :meta public: + """ + return self.header_formatter(name, value) + + def _render_parts( + self, + header_parts: ( + dict[str, _TYPE_FIELD_VALUE | None] + | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] + ), + ) -> str: + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format + as `k1="v1"; k2="v2"; ...`. + """ + iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] + + parts = [] + if isinstance(header_parts, dict): + iterable = header_parts.items() + else: + iterable = header_parts + + for name, value in iterable: + if value is not None: + parts.append(self._render_part(name, value)) + + return "; ".join(parts) + + def render_headers(self) -> str: + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append(f"{sort_key}: {self.headers[sort_key]}") + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append(f"{header_name}: {header_value}") + + lines.append("\r\n") + return "\r\n".join(lines) + + def make_multipart( + self, + content_disposition: str | None = None, + content_type: str | None = None, + content_location: str | None = None, + ) -> None: + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_disposition: + The 'Content-Disposition' of the request body. Defaults to 'form-data' + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + content_disposition = (content_disposition or "form-data") + "; ".join( + [ + "", + self._render_parts( + (("name", self._name), ("filename", self._filename)) + ), + ] + ) + + self.headers["Content-Disposition"] = content_disposition + self.headers["Content-Type"] = content_type + self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py new file mode 100644 index 0000000000..14f70b05b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import binascii +import codecs +import os +import typing +from io import BytesIO + +from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField + +writer = codecs.lookup("utf-8")[3] + +_TYPE_FIELDS_SEQUENCE = typing.Sequence[ + typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] +] +_TYPE_FIELDS = typing.Union[ + _TYPE_FIELDS_SEQUENCE, + typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], +] + + +def choose_boundary() -> str: + """ + Our embarrassingly-simple replacement for mimetools.choose_boundary. + """ + return binascii.hexlify(os.urandom(16)).decode() + + +def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] + + if isinstance(fields, typing.Mapping): + iterable = fields.items() + else: + iterable = fields + + for field in iterable: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def encode_multipart_formdata( + fields: _TYPE_FIELDS, boundary: str | None = None +) -> tuple[bytes, str]: + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(f"--{boundary}\r\n".encode("latin-1")) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, str): + writer(body).write(data) + else: + body.write(data) + + body.write(b"\r\n") + + body.write(f"--{boundary}--\r\n".encode("latin-1")) + + content_type = f"multipart/form-data; boundary={boundary}" + + return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py new file mode 100644 index 0000000000..133e1d8f23 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from importlib.metadata import version + +__all__ = [ + "inject_into_urllib3", + "extract_from_urllib3", +] + +import typing + +orig_HTTPSConnection: typing.Any = None + + +def inject_into_urllib3() -> None: + # First check if h2 version is valid + h2_version = version("h2") + if not h2_version.startswith("4."): + raise ImportError( + "urllib3 v2 supports h2 version 4.x.x, currently " + f"the 'h2' module is compiled with {h2_version!r}. " + "See: https://github.com/urllib3/urllib3/issues/3290" + ) + + # Import here to avoid circular dependencies. + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + from .connection import HTTP2Connection + + global orig_HTTPSConnection + orig_HTTPSConnection = urllib3_connection.HTTPSConnection + + HTTPSConnectionPool.ConnectionCls = HTTP2Connection + urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] + + # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. + urllib3_util.ALPN_PROTOCOLS = ["h2"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] + + +def extract_from_urllib3() -> None: + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + + HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection + urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] + + urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py new file mode 100644 index 0000000000..0a026da0a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py @@ -0,0 +1,356 @@ +from __future__ import annotations + +import logging +import re +import threading +import types +import typing + +import h2.config +import h2.connection +import h2.events + +from .._base_connection import _TYPE_BODY +from .._collections import HTTPHeaderDict +from ..connection import HTTPSConnection, _get_default_user_agent +from ..exceptions import ConnectionError +from ..response import BaseHTTPResponse + +orig_HTTPSConnection = HTTPSConnection + +T = typing.TypeVar("T") + +log = logging.getLogger(__name__) + +RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") +RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") + + +def _is_legal_header_name(name: bytes) -> bool: + """ + "An implementation that validates fields according to the definitions in Sections + 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not + include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + + `http.client._is_legal_header_name` does not validate the field name according to the + HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. + + This does not allow for the `:` character in the header name, so should not + be used to validate pseudo-headers. + """ + return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) + + +def _is_illegal_header_value(value: bytes) -> bool: + """ + "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed + (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field + value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, + 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + """ + return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) + + +class _LockedObject(typing.Generic[T]): + """ + A wrapper class that hides a specific object behind a lock. + The goal here is to provide a simple way to protect access to an object + that cannot safely be simultaneously accessed from multiple threads. The + intended use of this class is simple: take hold of it with a context + manager, which returns the protected object. + """ + + __slots__ = ( + "lock", + "_obj", + ) + + def __init__(self, obj: T): + self.lock = threading.RLock() + self._obj = obj + + def __enter__(self) -> T: + self.lock.acquire() + return self._obj + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + self.lock.release() + + +class HTTP2Connection(HTTPSConnection): + def __init__( + self, host: str, port: int | None = None, **kwargs: typing.Any + ) -> None: + self._h2_conn = self._new_h2_conn() + self._h2_stream: int | None = None + self._headers: list[tuple[bytes, bytes]] = [] + + if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: + raise NotImplementedError("Proxies aren't supported with HTTP/2") + + super().__init__(host, port, **kwargs) + + if self._tunnel_host is not None: + raise NotImplementedError("Tunneling isn't supported with HTTP/2") + + def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: + config = h2.config.H2Configuration(client_side=True) + return _LockedObject(h2.connection.H2Connection(config=config)) + + def connect(self) -> None: + super().connect() + with self._h2_conn as conn: + conn.initiate_connection() + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + def putrequest( # type: ignore[override] + self, + method: str, + url: str, + **kwargs: typing.Any, + ) -> None: + """putrequest + This deviates from the HTTPConnection method signature since we never need to override + sending accept-encoding headers or the host header. + """ + if "skip_host" in kwargs: + raise NotImplementedError("`skip_host` isn't supported") + if "skip_accept_encoding" in kwargs: + raise NotImplementedError("`skip_accept_encoding` isn't supported") + + self._request_url = url or "/" + self._validate_path(url) # type: ignore[attr-defined] + + if ":" in self.host: + authority = f"[{self.host}]:{self.port or 443}" + else: + authority = f"{self.host}:{self.port or 443}" + + self._headers.append((b":scheme", b"https")) + self._headers.append((b":method", method.encode())) + self._headers.append((b":authority", authority.encode())) + self._headers.append((b":path", url.encode())) + + with self._h2_conn as conn: + self._h2_stream = conn.get_next_available_stream_id() + + def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] + # TODO SKIPPABLE_HEADERS from urllib3 are ignored. + header = header.encode() if isinstance(header, str) else header + header = header.lower() # A lot of upstream code uses capitalized headers. + if not _is_legal_header_name(header): + raise ValueError(f"Illegal header name {str(header)}") + + for value in values: + value = value.encode() if isinstance(value, str) else value + if _is_illegal_header_value(value): + raise ValueError(f"Illegal header value {str(value)}") + self._headers.append((header, value)) + + def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + conn.send_headers( + stream_id=self._h2_stream, + headers=self._headers, + end_stream=(message_body is None), + ) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + self._headers = [] # Reset headers for the next request. + + def send(self, data: typing.Any) -> None: + """Send data to the server. + `data` can be: `str`, `bytes`, an iterable, or file-like objects + that support a .read() method. + """ + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + if hasattr(data, "read"): # file-like objects + while True: + chunk = data.read(self.blocksize) + if not chunk: + break + if isinstance(chunk, str): + chunk = chunk.encode() + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + return + + if isinstance(data, str): # str -> bytes + data = data.encode() + + try: + if isinstance(data, bytes): + conn.send_data(self._h2_stream, data, end_stream=True) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + else: + for chunk in data: + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + except TypeError: + raise TypeError( + "`data` should be str, bytes, iterable, or file. got %r" + % type(data) + ) + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + raise NotImplementedError( + "HTTP/2 does not support setting up a tunnel through a proxy" + ) + + def getresponse( # type: ignore[override] + self, + ) -> HTTP2Response: + status = None + data = bytearray() + with self._h2_conn as conn: + end_stream = False + while not end_stream: + # TODO: Arbitrary read value. + if received_data := self.sock.recv(65535): + events = conn.receive_data(received_data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + headers = HTTPHeaderDict() + for header, value in event.headers: + if header == b":status": + status = int(value.decode()) + else: + headers.add( + header.decode("ascii"), value.decode("ascii") + ) + + elif isinstance(event, h2.events.DataReceived): + data += event.data + conn.acknowledge_received_data( + event.flow_controlled_length, event.stream_id + ) + + elif isinstance(event, h2.events.StreamEnded): + end_stream = True + + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + assert status is not None + return HTTP2Response( + status=status, + headers=headers, + request_url=self._request_url, + data=bytes(data), + ) + + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + **kwargs: typing.Any, + ) -> None: + """Send an HTTP/2 request""" + if "chunked" in kwargs: + # TODO this is often present from upstream. + # raise NotImplementedError("`chunked` isn't supported with HTTP/2") + pass + + if self.sock is not None: + self.sock.settimeout(self.timeout) + + self.putrequest(method, url) + + headers = headers or {} + for k, v in headers.items(): + if k.lower() == "transfer-encoding" and v == "chunked": + continue + else: + self.putheader(k, v) + + if b"user-agent" not in dict(self._headers): + self.putheader(b"user-agent", _get_default_user_agent()) + + if body: + self.endheaders(message_body=body) + self.send(body) + else: + self.endheaders() + + def close(self) -> None: + with self._h2_conn as conn: + try: + conn.close_connection() + if data := conn.data_to_send(): + self.sock.sendall(data) + except Exception: + pass + + # Reset all our HTTP/2 connection state. + self._h2_conn = self._new_h2_conn() + self._h2_stream = None + self._headers = [] + + super().close() + + +class HTTP2Response(BaseHTTPResponse): + # TODO: This is a woefully incomplete response object, but works for non-streaming. + def __init__( + self, + status: int, + headers: HTTPHeaderDict, + request_url: str, + data: bytes, + decode_content: bool = False, # TODO: support decoding + ) -> None: + super().__init__( + status=status, + headers=headers, + # Following CPython, we map HTTP versions to major * 10 + minor integers + version=20, + version_string="HTTP/2", + # No reason phrase in HTTP/2 + reason=None, + decode_content=decode_content, + request_url=request_url, + ) + self._data = data + self.length_remaining = 0 + + @property + def data(self) -> bytes: + return self._data + + def get_redirect_location(self) -> None: + return None + + def close(self) -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py new file mode 100644 index 0000000000..9ea900764f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import threading + + +class _HTTP2ProbeCache: + __slots__ = ( + "_lock", + "_cache_locks", + "_cache_values", + ) + + def __init__(self) -> None: + self._lock = threading.Lock() + self._cache_locks: dict[tuple[str, int], threading.RLock] = {} + self._cache_values: dict[tuple[str, int], bool | None] = {} + + def acquire_and_get(self, host: str, port: int) -> bool | None: + # By the end of this block we know that + # _cache_[values,locks] is available. + value = None + with self._lock: + key = (host, port) + try: + value = self._cache_values[key] + # If it's a known value we return right away. + if value is not None: + return value + except KeyError: + self._cache_locks[key] = threading.RLock() + self._cache_values[key] = None + + # If the value is unknown, we acquire the lock to signal + # to the requesting thread that the probe is in progress + # or that the current thread needs to return their findings. + key_lock = self._cache_locks[key] + key_lock.acquire() + try: + # If the by the time we get the lock the value has been + # updated we want to return the updated value. + value = self._cache_values[key] + + # In case an exception like KeyboardInterrupt is raised here. + except BaseException as e: # Defensive: + assert not isinstance(e, KeyError) # KeyError shouldn't be possible. + key_lock.release() + raise + + return value + + def set_and_release( + self, host: str, port: int, supports_http2: bool | None + ) -> None: + key = (host, port) + key_lock = self._cache_locks[key] + with key_lock: # Uses an RLock, so can be locked again from same thread. + if supports_http2 is None and self._cache_values[key] is not None: + raise ValueError( + "Cannot reset HTTP/2 support for origin after value has been set." + ) # Defensive: not expected in normal usage + + self._cache_values[key] = supports_http2 + key_lock.release() + + def _values(self) -> dict[tuple[str, int], bool | None]: + """This function is for testing purposes only. Gets the current state of the probe cache""" + with self._lock: + return {k: v for k, v in self._cache_values.items()} + + def _reset(self) -> None: + """This function is for testing purposes only. Reset the cache values""" + with self._lock: + self._cache_locks = {} + self._cache_values = {} + + +_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() + +set_and_release = _HTTP2_PROBE_CACHE.set_and_release +acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get +_values = _HTTP2_PROBE_CACHE._values +_reset = _HTTP2_PROBE_CACHE._reset + +__all__ = [ + "set_and_release", + "acquire_and_get", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py new file mode 100644 index 0000000000..8f2c56745c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py @@ -0,0 +1,653 @@ +from __future__ import annotations + +import functools +import logging +import typing +import warnings +from types import TracebackType +from urllib.parse import urljoin + +from ._collections import HTTPHeaderDict, RecentlyUsedContainer +from ._request_methods import RequestMethods +from .connection import ProxyConfig +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme +from .exceptions import ( + LocationValueError, + MaxRetryError, + ProxySchemeUnknown, + URLSchemeUnknown, +) +from .response import BaseHTTPResponse +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.proxy import connection_requires_http_tunnel +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import Url, parse_url + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + +__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] + + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ( + "key_file", + "cert_file", + "cert_reqs", + "ca_certs", + "ca_cert_data", + "ssl_version", + "ssl_minimum_version", + "ssl_maximum_version", + "ca_cert_dir", + "ssl_context", + "key_password", + "server_hostname", +) +# Default value for `blocksize` - a new parameter introduced to +# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 +_DEFAULT_BLOCKSIZE = 16384 + + +class PoolKey(typing.NamedTuple): + """ + All known keyword arguments that could be provided to the pool manager, its + pools, or the underlying connections. + + All custom key schemes should include the fields in this key at a minimum. + """ + + key_scheme: str + key_host: str + key_port: int | None + key_timeout: Timeout | float | int | None + key_retries: Retry | bool | int | None + key_block: bool | None + key_source_address: tuple[str, int] | None + key_key_file: str | None + key_key_password: str | None + key_cert_file: str | None + key_cert_reqs: str | None + key_ca_certs: str | None + key_ca_cert_data: str | bytes | None + key_ssl_version: int | str | None + key_ssl_minimum_version: ssl.TLSVersion | None + key_ssl_maximum_version: ssl.TLSVersion | None + key_ca_cert_dir: str | None + key_ssl_context: ssl.SSLContext | None + key_maxsize: int | None + key_headers: frozenset[tuple[str, str]] | None + key__proxy: Url | None + key__proxy_headers: frozenset[tuple[str, str]] | None + key__proxy_config: ProxyConfig | None + key_socket_options: _TYPE_SOCKET_OPTIONS | None + key__socks_options: frozenset[tuple[str, str]] | None + key_assert_hostname: bool | str | None + key_assert_fingerprint: str | None + key_server_hostname: str | None + key_blocksize: int | None + + +def _default_key_normalizer( + key_class: type[PoolKey], request_context: dict[str, typing.Any] +) -> PoolKey: + """ + Create a pool key out of a request context dictionary. + + According to RFC 3986, both the scheme and host are case-insensitive. + Therefore, this function normalizes both before constructing the pool + key for an HTTPS request. If you wish to change this behaviour, provide + alternate callables to ``key_fn_by_scheme``. + + :param key_class: + The class to use when constructing the key. This should be a namedtuple + with the ``scheme`` and ``host`` keys at a minimum. + :type key_class: namedtuple + :param request_context: + A dictionary-like object that contain the context for a request. + :type request_context: dict + + :return: A namedtuple that can be used as a connection pool key. + :rtype: PoolKey + """ + # Since we mutate the dictionary, make a copy first + context = request_context.copy() + context["scheme"] = context["scheme"].lower() + context["host"] = context["host"].lower() + + # These are both dictionaries and need to be transformed into frozensets + for key in ("headers", "_proxy_headers", "_socks_options"): + if key in context and context[key] is not None: + context[key] = frozenset(context[key].items()) + + # The socket_options key may be a list and needs to be transformed into a + # tuple. + socket_opts = context.get("socket_options") + if socket_opts is not None: + context["socket_options"] = tuple(socket_opts) + + # Map the kwargs to the names in the namedtuple - this is necessary since + # namedtuples can't have fields starting with '_'. + for key in list(context.keys()): + context["key_" + key] = context.pop(key) + + # Default to ``None`` for keys missing from the context + for field in key_class._fields: + if field not in context: + context[field] = None + + # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context + if context.get("key_blocksize") is None: + context["key_blocksize"] = _DEFAULT_BLOCKSIZE + + return key_class(**context) + + +#: A dictionary that maps a scheme to a callable that creates a pool key. +#: This can be used to alter the way pool keys are constructed, if desired. +#: Each PoolManager makes a copy of this dictionary so they can be configured +#: globally here, or individually on the instance. +key_fn_by_scheme = { + "http": functools.partial(_default_key_normalizer, PoolKey), + "https": functools.partial(_default_key_normalizer, PoolKey), +} + +pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \\**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example: + + .. code-block:: python + + import urllib3 + + http = urllib3.PoolManager(num_pools=2) + + resp1 = http.request("GET", "https://google.com/") + resp2 = http.request("GET", "https://google.com/mail") + resp3 = http.request("GET", "https://yahoo.com/") + + print(len(http.pools)) + # 2 + + """ + + proxy: Url | None = None + proxy_config: ProxyConfig | None = None + + def __init__( + self, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + super().__init__(headers) + # PoolManager handles redirects itself in PoolManager.urlopen(). + # It always passes redirect=False to the underlying connection pool to + # suppress per-pool redirect handling. If the user supplied a non-Retry + # value (int/bool/etc) for retries and we let the pool normalize it + # while redirect=False, the resulting Retry object would have redirect + # handling disabled, which can interfere with PoolManager's own + # redirect logic. Normalize here so redirects remain governed solely by + # PoolManager logic. + if "retries" in connection_pool_kw: + retries = connection_pool_kw["retries"] + if not isinstance(retries, Retry): + retries = Retry.from_int(retries) + connection_pool_kw = connection_pool_kw.copy() + connection_pool_kw["retries"] = retries + self.connection_pool_kw = connection_pool_kw + + self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] + self.pools = RecentlyUsedContainer(num_pools) + + # Locally set the pool classes and keys so other PoolManagers can + # override them. + self.pool_classes_by_scheme = pool_classes_by_scheme + self.key_fn_by_scheme = key_fn_by_scheme.copy() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.clear() + # Return False to re-raise any potential exceptions + return False + + def _new_pool( + self, + scheme: str, + host: str, + port: int, + request_context: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and + any additional pool keyword arguments. + + If ``request_context`` is provided, it is provided as keyword arguments + to the pool class used. This method is used to actually create the + connection pools handed out by :meth:`connection_from_url` and + companion methods. It is intended to be overridden for customization. + """ + pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] + if request_context is None: + request_context = self.connection_pool_kw.copy() + + # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly + # set to 'None' in the request_context. + if request_context.get("blocksize") is None: + request_context["blocksize"] = _DEFAULT_BLOCKSIZE + + # Although the context has everything necessary to create the pool, + # this function has historically only used the scheme, host, and port + # in the positional args. When an API change is acceptable these can + # be removed. + for key in ("scheme", "host", "port"): + request_context.pop(key, None) + + if scheme == "http": + for kw in SSL_KEYWORDS: + request_context.pop(kw, None) + + return pool_cls(host, port, **request_context) + + def clear(self) -> None: + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is + provided, it is merged with the instance's ``connection_pool_kw`` + variable and used to create the new connection pool, if one is + needed. + """ + + if not host: + raise LocationValueError("No host specified.") + + request_context = self._merge_pool_kwargs(pool_kwargs) + request_context["scheme"] = scheme or "http" + if not port: + port = port_by_scheme.get(request_context["scheme"].lower(), 80) + request_context["port"] = port + request_context["host"] = host + + return self.connection_from_context(request_context) + + def connection_from_context( + self, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. + + ``request_context`` must at least contain the ``scheme`` key and its + value must be a key in ``key_fn_by_scheme`` instance variable. + """ + if "strict" in request_context: + warnings.warn( + "The 'strict' parameter is no longer needed on Python 3+. " + "This will raise an error in urllib3 v3.0.", + FutureWarning, + ) + request_context.pop("strict") + + scheme = request_context["scheme"].lower() + pool_key_constructor = self.key_fn_by_scheme.get(scheme) + if not pool_key_constructor: + raise URLSchemeUnknown(scheme) + pool_key = pool_key_constructor(request_context) + + return self.connection_from_pool_key(pool_key, request_context=request_context) + + def connection_from_pool_key( + self, pool_key: PoolKey, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. + + ``pool_key`` should be a namedtuple that only contains immutable + objects. At a minimum it must have the ``scheme``, ``host``, and + ``port`` fields. + """ + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + scheme = request_context["scheme"] + host = request_context["host"] + port = request_context["port"] + pool = self._new_pool(scheme, host, port, request_context=request_context) + self.pools[pool_key] = pool + + return pool + + def connection_from_url( + self, url: str, pool_kwargs: dict[str, typing.Any] | None = None + ) -> HTTPConnectionPool: + """ + Similar to :func:`urllib3.connectionpool.connection_from_url`. + + If ``pool_kwargs`` is not provided and a new pool needs to be + constructed, ``self.connection_pool_kw`` is used to initialize + the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` + is provided, it is used instead. Note that if a new pool does not + need to be created for the request, the provided ``pool_kwargs`` are + not used. + """ + u = parse_url(url) + return self.connection_from_host( + u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs + ) + + def _merge_pool_kwargs( + self, override: dict[str, typing.Any] | None + ) -> dict[str, typing.Any]: + """ + Merge a dictionary of override values for self.connection_pool_kw. + + This does not modify self.connection_pool_kw and returns a new dict. + Any keys in the override dictionary with a value of ``None`` are + removed from the merged dictionary. + """ + base_pool_kwargs = self.connection_pool_kw.copy() + if override: + for key, value in override.items(): + if value is None: + try: + del base_pool_kwargs[key] + except KeyError: + pass + else: + base_pool_kwargs[key] = value + return base_pool_kwargs + + def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: + """ + Indicates if the proxy requires the complete destination URL in the + request. Normally this is only needed when not using an HTTP CONNECT + tunnel. + """ + if self.proxy is None: + return False + + return not connection_requires_http_tunnel( + self.proxy, self.proxy_config, parsed_url.scheme + ) + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + """ + Same as :meth:`urllib3.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + + if u.scheme is None: + warnings.warn( + "URLs without a scheme (ie 'https://') are deprecated and will raise an error " + "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " + "start with 'https://' or 'http://'. Read more in this issue: " + "https://github.com/urllib3/urllib3/issues/2920", + category=FutureWarning, + stacklevel=2, + ) + + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw["assert_same_host"] = False + kw["redirect"] = False + + if "headers" not in kw: + kw["headers"] = self.headers + + if self._proxy_requires_url_absolute_form(u): + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + kw["body"] = None + kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + + retries = kw.get("retries", response.retries) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # conn.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not conn.is_same_host( + redirect_location + ): + new_headers = kw["headers"].copy() + for header in kw["headers"]: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + kw["headers"] = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + kw["retries"] = retries + kw["redirect"] = redirect + + log.info("Redirecting %s -> %s", url, redirect_location) + + response.drain_conn() + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary containing headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + :param proxy_ssl_context: + The proxy SSL context is used to establish the TLS connection to the + proxy when using HTTPS proxies. + + :param use_forwarding_for_https: + (Defaults to False) If set to True will forward requests to the HTTPS + proxy to be made on behalf of the client instead of creating a TLS + tunnel via the CONNECT method. **Enabling this flag means that request + and response headers and content will be visible from the HTTPS proxy** + whereas tunneling keeps request and response headers and content + private. IP address, target hostname, SNI, and port are always visible + to an HTTPS proxy even when this flag is disabled. + + :param proxy_assert_hostname: + The hostname of the certificate to verify against. + + :param proxy_assert_fingerprint: + The fingerprint of the certificate to verify against. + + Example: + + .. code-block:: python + + import urllib3 + + proxy = urllib3.ProxyManager("https://localhost:3128/") + + resp1 = proxy.request("GET", "http://google.com/") + resp2 = proxy.request("GET", "http://httpbin.org/") + + # One pool was shared by both plain HTTP requests. + print(len(proxy.pools)) + # 1 + + resp3 = proxy.request("GET", "https://httpbin.org/") + resp4 = proxy.request("GET", "https://twitter.com/") + + # A separate pool was added for each HTTPS target. + print(len(proxy.pools)) + # 3 + + """ + + def __init__( + self, + proxy_url: str, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + proxy_headers: typing.Mapping[str, str] | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + use_forwarding_for_https: bool = False, + proxy_assert_hostname: None | str | typing.Literal[False] = None, + proxy_assert_fingerprint: str | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + if isinstance(proxy_url, HTTPConnectionPool): + str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" + else: + str_proxy_url = proxy_url + proxy = parse_url(str_proxy_url) + + if proxy.scheme not in ("http", "https"): + raise ProxySchemeUnknown(proxy.scheme) + + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + self.proxy_ssl_context = proxy_ssl_context + self.proxy_config = ProxyConfig( + proxy_ssl_context, + use_forwarding_for_https, + proxy_assert_hostname, + proxy_assert_fingerprint, + ) + + connection_pool_kw["_proxy"] = self.proxy + connection_pool_kw["_proxy_headers"] = self.proxy_headers + connection_pool_kw["_proxy_config"] = self.proxy_config + + super().__init__(num_pools, headers, **connection_pool_kw) + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + if scheme == "https": + return super().connection_from_host( + host, port, scheme, pool_kwargs=pool_kwargs + ) + + return super().connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] + ) + + def _set_proxy_headers( + self, url: str, headers: typing.Mapping[str, str] | None = None + ) -> typing.Mapping[str, str]: + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {"Accept": "*/*"} + + netloc = parse_url(url).netloc + if netloc: + headers_["Host"] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): + # For connections using HTTP CONNECT, httplib sets the necessary + # headers on the CONNECT to the proxy. If we're not using CONNECT, + # we'll definitely need to set 'Host' at the very least. + headers = kw.get("headers", self.headers) + kw["headers"] = self._set_proxy_headers(url, headers) + + return super().urlopen(method, url, redirect=redirect, **kw) + + +def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: + return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed new file mode 100644 index 0000000000..5f3ea3d919 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed @@ -0,0 +1,2 @@ +# Instruct type checkers to look for inline type annotations in this package. +# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py new file mode 100644 index 0000000000..e9246b75e3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py @@ -0,0 +1,1493 @@ +from __future__ import annotations + +import collections +import io +import json as _json +import logging +import socket +import sys +import typing +import warnings +import zlib +from contextlib import contextmanager +from http.client import HTTPMessage as _HttplibHTTPMessage +from http.client import HTTPResponse as _HttplibHTTPResponse +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from ._base_connection import BaseHTTPConnection + +try: + try: + import brotlicffi as brotli # type: ignore[import-not-found] + except ImportError: + import brotli # type: ignore[import-not-found] +except ImportError: + brotli = None + +from . import util +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .connection import BaseSSLError, HTTPConnection, HTTPException +from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, + DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, + InvalidHeader, + ProtocolError, + ReadTimeoutError, + ResponseNotChunked, + SSLError, +) +from .util.response import is_fp_closed, is_response_to_head +from .util.retry import Retry + +if typing.TYPE_CHECKING: + from .connectionpool import HTTPConnectionPool + +log = logging.getLogger(__name__) + + +class ContentDecoder: + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + raise NotImplementedError() + + @property + def has_unconsumed_tail(self) -> bool: + raise NotImplementedError() + + def flush(self) -> bytes: + raise NotImplementedError() + + +class DeflateDecoder(ContentDecoder): + def __init__(self) -> None: + self._first_try = True + self._first_try_data = b"" + self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + data = self._unfed_data + data + self._unfed_data = b"" + if not data and not self._obj.unconsumed_tail: + return data + original_max_length = max_length + if original_max_length < 0: + max_length = 0 + elif original_max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unfed_data = data + return b"" + + # Subsequent calls always reuse `self._obj`. zlib requires + # passing the unconsumed tail if decompression is to continue. + if not self._first_try: + return self._obj.decompress( + self._obj.unconsumed_tail + data, max_length=max_length + ) + + # First call tries with RFC 1950 ZLIB format. + self._first_try_data += data + try: + decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False + self._first_try_data = b"" + return decompressed + # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress( + self._first_try_data, max_length=original_max_length + ) + finally: + self._first_try_data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unfed_data) or ( + bool(self._obj.unconsumed_tail) and not self._first_try + ) + + def flush(self) -> bytes: + return self._obj.flush() + + +class GzipDecoderState: + FIRST_MEMBER = 0 + OTHER_MEMBERS = 1 + SWALLOW_DATA = 2 + + +class GzipDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER + self._unconsumed_tail = b"" + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() + if self._state == GzipDecoderState.SWALLOW_DATA: + return bytes(ret) + + if max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unconsumed_tail += data + return b"" + + # zlib requires passing the unconsumed tail to the subsequent + # call if decompression is to continue. + data = self._unconsumed_tail + data + if not data and self._obj.eof: + return bytes(ret) + + while True: + try: + ret += self._obj.decompress( + data, max_length=max(max_length - len(ret), 0) + ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA + self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise + + self._unconsumed_tail = data = ( + self._obj.unconsumed_tail or self._obj.unused_data + ) + if max_length > 0 and len(ret) >= max_length: + break + + if not data: + return bytes(ret) + # When the end of a gzip member is reached, a new decompressor + # must be created for unused (possibly future) data. + if self._obj.eof: + self._state = GzipDecoderState.OTHER_MEMBERS + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + + return bytes(ret) + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unconsumed_tail) + + def flush(self) -> bytes: + return self._obj.flush() + + +if brotli is not None: + + class BrotliDecoder(ContentDecoder): + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' + def __init__(self) -> None: + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): + setattr(self, "_decompress", self._obj.decompress) + else: + setattr(self, "_decompress", self._obj.process) + + # Requires Brotli >= 1.2.0 for `output_buffer_limit`. + def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: + raise NotImplementedError() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + try: + if max_length > 0: + return self._decompress(data, output_buffer_limit=max_length) + else: + return self._decompress(data) + except TypeError: + # Fallback for Brotli/brotlicffi/brotlipy versions without + # the `output_buffer_limit` parameter. + warnings.warn( + "Brotli >= 1.2.0 is required to prevent decompression bombs.", + DependencyWarning, + ) + return self._decompress(data) + + @property + def has_unconsumed_tail(self) -> bool: + try: + return not self._obj.can_accept_more_data() + except AttributeError: + return False + + def flush(self) -> bytes: + if hasattr(self._obj, "flush"): + return self._obj.flush() # type: ignore[no-any-return] + return b"" + + +try: + if sys.version_info >= (3, 14): + from compression import zstd + else: + from backports import zstd +except ImportError: + HAS_ZSTD = False +else: + HAS_ZSTD = True + + class ZstdDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zstd.ZstdDecompressor() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if not data and not self.has_unconsumed_tail: + return b"" + if self._obj.eof: + data = self._obj.unused_data + data + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress(data, max_length=max_length) + length = len(part) + data_parts = [part] + # Every loop iteration is supposed to read data from a separate frame. + # The loop breaks when: + # - enough data is read; + # - no more unused data is available; + # - end of the last read frame has not been reached (i.e., + # more data has to be fed). + while ( + self._obj.eof + and self._obj.unused_data + and (max_length < 0 or length < max_length) + ): + unused_data = self._obj.unused_data + if not self._obj.needs_input: + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress( + unused_data, + max_length=(max_length - length) if max_length > 0 else -1, + ) + if part_length := len(part): + data_parts.append(part) + length += part_length + elif self._obj.needs_input: + break + return b"".join(data_parts) + + @property + def has_unconsumed_tail(self) -> bool: + return not (self._obj.needs_input or self._obj.eof) or bool( + self._obj.unused_data + ) + + def flush(self) -> bytes: + if not self._obj.eof: + raise DecodeError("Zstandard data is incomplete") + return b"" + + +class MultiDecoder(ContentDecoder): + """ + From RFC7231: + If one or more encodings have been applied to a representation, the + sender that applied the encodings MUST generate a Content-Encoding + header field that lists the content codings in the order in which + they were applied. + """ + + # Maximum allowed number of chained HTTP encodings in the + # Content-Encoding header. + max_decode_links = 5 + + def __init__(self, modes: str) -> None: + encodings = [m.strip() for m in modes.split(",")] + if len(encodings) > self.max_decode_links: + raise DecodeError( + "Too many content encodings in the chain: " + f"{len(encodings)} > {self.max_decode_links}" + ) + self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self) -> bytes: + return self._decoders[0].flush() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if max_length <= 0: + for d in reversed(self._decoders): + data = d.decompress(data) + return data + + ret = bytearray() + # Every while loop iteration goes through all decoders once. + # It exits when enough data is read or no more data can be read. + # It is possible that the while loop iteration does not produce + # any data because we retrieve up to `max_length` from every + # decoder, and the amount of bytes may be insufficient for the + # next decoder to produce enough/any output. + while True: + any_data = False + for d in reversed(self._decoders): + data = d.decompress(data, max_length=max_length - len(ret)) + if data: + any_data = True + # We should not break when no data is returned because + # next decoders may produce data even with empty input. + ret += data + if not any_data or len(ret) >= max_length: + return bytes(ret) + data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return any(d.has_unconsumed_tail for d in self._decoders) + + +def _get_decoder(mode: str) -> ContentDecoder: + if "," in mode: + return MultiDecoder(mode) + + # According to RFC 9110 section 8.4.1.3, recipients should + # consider x-gzip equivalent to gzip + if mode in ("gzip", "x-gzip"): + return GzipDecoder() + + if brotli is not None and mode == "br": + return BrotliDecoder() + + if HAS_ZSTD and mode == "zstd": + return ZstdDecoder() + + return DeflateDecoder() + + +class BytesQueueBuffer: + """Memory-efficient bytes buffer + + To return decoded data in read() and still follow the BufferedIOBase API, we need a + buffer to always return the correct amount of bytes. + + This buffer should be filled using calls to put() + + Our maximum memory usage is determined by the sum of the size of: + + * self.buffer, which contains the full data + * the largest chunk that we will copy in get() + """ + + def __init__(self) -> None: + self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() + self._size: int = 0 + + def __len__(self) -> int: + return self._size + + def put(self, data: bytes) -> None: + self.buffer.append(data) + self._size += len(data) + + def get(self, n: int) -> bytes: + if n == 0: + return b"" + elif not self.buffer: + raise RuntimeError("buffer is empty") + elif n < 0: + raise ValueError("n should be > 0") + + if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): + self._size -= n + return self.buffer.popleft() + + fetched = 0 + ret = io.BytesIO() + while fetched < n: + remaining = n - fetched + chunk = self.buffer.popleft() + chunk_length = len(chunk) + if remaining < chunk_length: + chunk = memoryview(chunk) + left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] + ret.write(left_chunk) + self.buffer.appendleft(right_chunk) + self._size -= remaining + break + else: + ret.write(chunk) + self._size -= chunk_length + fetched += chunk_length + + if not self.buffer: + break + + return ret.getvalue() + + def get_all(self) -> bytes: + buffer = self.buffer + if not buffer: + assert self._size == 0 + return b"" + if len(buffer) == 1: + result = buffer.pop() + if isinstance(result, memoryview): + result = result.tobytes() + else: + ret = io.BytesIO() + ret.writelines(buffer.popleft() for _ in range(len(buffer))) + result = ret.getvalue() + self._size = 0 + return result + + +class BaseHTTPResponse(io.IOBase): + CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] + if brotli is not None: + CONTENT_DECODERS += ["br"] + if HAS_ZSTD: + CONTENT_DECODERS += ["zstd"] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + + if HAS_ZSTD: + DECODER_ERROR_CLASSES += (zstd.ZstdError,) + + def __init__( + self, + *, + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int, + version: int, + version_string: str, + reason: str | None, + decode_content: bool, + request_url: str | None, + retries: Retry | None = None, + ) -> None: + if isinstance(headers, HTTPHeaderDict): + self.headers = headers + else: + self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] + self.status = status + self.version = version + self.version_string = version_string + self.reason = reason + self.decode_content = decode_content + self._has_decoded_content = False + self._request_url: str | None = request_url + self.retries = retries + + self.chunked = False + tr_enc = self.headers.get("transfer-encoding", "").lower() + # Don't incur the penalty of creating a list and then discarding it + encodings = (enc.strip() for enc in tr_enc.split(",")) + if "chunked" in encodings: + self.chunked = True + + self._decoder: ContentDecoder | None = None + self.length_remaining: int | None + + def get_redirect_location(self) -> str | None | typing.Literal[False]: + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get("location") + return False + + @property + def data(self) -> bytes: + raise NotImplementedError() + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + @property + def url(self) -> str | None: + raise NotImplementedError() + + @url.setter + def url(self, url: str | None) -> None: + raise NotImplementedError() + + @property + def connection(self) -> BaseHTTPConnection | None: + raise NotImplementedError() + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + if retries is not None and retries.history: + self.url = retries.history[-1].redirect_location + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + raise NotImplementedError() + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + raise NotImplementedError() + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def release_conn(self) -> None: + raise NotImplementedError() + + def drain_conn(self) -> None: + raise NotImplementedError() + + def shutdown(self) -> None: + raise NotImplementedError() + + def close(self) -> None: + raise NotImplementedError() + + def _init_decoder(self) -> None: + """ + Set-up the _decoder attribute if necessary. + """ + # Note: content-encoding value should be case-insensitive, per RFC 7230 + # Section 3.2 + content_encoding = self.headers.get("content-encoding", "").lower() + if self._decoder is None: + if content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + elif "," in content_encoding: + encodings = [ + e.strip() + for e in content_encoding.split(",") + if e.strip() in self.CONTENT_DECODERS + ] + if encodings: + self._decoder = _get_decoder(content_encoding) + + def _decode( + self, + data: bytes, + decode_content: bool | None, + flush_decoder: bool, + max_length: int | None = None, + ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + if max_length is None or flush_decoder: + max_length = -1 + + try: + if self._decoder: + data = self._decoder.decompress(data, max_length=max_length) + self._has_decoded_content = True + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, + e, + ) from e + if flush_decoder: + data += self._flush_decoder() + + return data + + def _flush_decoder(self) -> bytes: + """ + Flushes the decoder. Should only be called if the decoder is actually + being used. + """ + if self._decoder: + return self._decoder.decompress(b"") + self._decoder.flush() + return b"" + + # Compatibility methods for `io` module + def readinto(self, b: bytearray | memoryview[int]) -> int: + temp = self.read(len(b)) + if len(temp) == 0: + return 0 + else: + b[: len(temp)] = temp + return len(temp) + + # Methods used by dependent libraries + def getheaders(self) -> HTTPHeaderDict: + return self.headers + + def getheader(self, name: str, default: str | None = None) -> str | None: + return self.headers.get(name, default) + + # Compatibility method for http.cookiejar + def info(self) -> HTTPHeaderDict: + return self.headers + + def geturl(self) -> str | None: + return self.url + + +class HTTPResponse(BaseHTTPResponse): + """ + HTTP Response container. + + Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. This + class is also compatible with the Python standard library's :mod:`io` + module, and can hence be treated as a readable object in the context of that + framework. + + Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param original_response: + When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + + :param retries: + The retries contains the last :class:`~urllib3.util.retry.Retry` that + was used during the request. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + + def __init__( + self, + body: _TYPE_BODY = "", + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int = 0, + version: int = 0, + version_string: str = "HTTP/?", + reason: str | None = None, + preload_content: bool = True, + decode_content: bool = True, + original_response: _HttplibHTTPResponse | None = None, + pool: HTTPConnectionPool | None = None, + connection: HTTPConnection | None = None, + msg: _HttplibHTTPMessage | None = None, + retries: Retry | None = None, + enforce_content_length: bool = True, + request_method: str | None = None, + request_url: str | None = None, + auto_close: bool = True, + sock_shutdown: typing.Callable[[int], None] | None = None, + ) -> None: + super().__init__( + headers=headers, + status=status, + version=version, + version_string=version_string, + reason=reason, + decode_content=decode_content, + request_url=request_url, + retries=retries, + ) + + self.enforce_content_length = enforce_content_length + self.auto_close = auto_close + + self._body = None + self._uncached_read_occurred = False + self._fp: _HttplibHTTPResponse | None = None + self._original_response = original_response + self._fp_bytes_read = 0 + self.msg = msg + + if body and isinstance(body, (str, bytes)): + self._body = body + + self._pool = pool + self._connection = connection + + if hasattr(body, "read"): + self._fp = body # type: ignore[assignment] + self._sock_shutdown = sock_shutdown + + # Are we using the chunked-style of transfer encoding? + self.chunk_left: int | None = None + + # Determine length of response + self.length_remaining = self._init_length(request_method) + + # Used to return the correct amount of bytes for partial read()s + self._decoded_buffer = BytesQueueBuffer() + + # If requested, preload the body. + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + """ + Read and discard any remaining HTTP response data in the response connection. + + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: + self._raw_read() + except (HTTPError, OSError, BaseSSLError, HTTPException): + pass + if self._has_decoded_content: + # `_raw_read` skips decompression, so we should clean up the + # decoder to avoid keeping unnecessary data in memory. + self._decoded_buffer = BytesQueueBuffer() + self._decoder = None + + @property + def data(self) -> bytes: + # For backwards-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body # type: ignore[return-value] + + if self._fp: + return self.read(cache_content=True) + + return None # type: ignore[return-value] + + @property + def connection(self) -> HTTPConnection | None: + return self._connection + + def isclosed(self) -> bool: + return is_fp_closed(self._fp) + + def tell(self) -> int: + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:`HTTPResponse.read` + if bytes are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def _init_length(self, request_method: str | None) -> int | None: + """ + Set initial length value for Response content if available. + """ + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + if self.chunked: + # This Response will fail with an IncompleteRead if it can't be + # received as chunked. This method falls back to attempt reading + # the response before raising an exception. + log.warning( + "Received response with both Content-Length and " + "Transfer-Encoding set. This is expressly forbidden " + "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " + "attempting to process response as Transfer-Encoding: " + "chunked." + ) + return None + + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Convert status to int for comparison + # In some cases, httplib returns a status of "_UNKNOWN" + try: + status = int(self.status) + except ValueError: + status = 0 + + # Check for responses that shouldn't include a body + if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": + length = 0 + + return length + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch low-level python exceptions, instead re-raising urllib3 + variants, so that low-level exceptions are not leaked in the + high-level api. + + On exit, release the connection back to the pool. + """ + clean_exit = False + + try: + try: + yield + + except SocketTimeout as e: + # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but + # there is yet no clean way to get at it from this context. + raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + + except BaseSSLError as e: + # SSL errors related to framing/MAC get wrapped and reraised here + raise SSLError(e) from e + + except IncompleteRead as e: + if ( + e.expected is not None + and e.partial is not None + and e.expected == -e.partial + ): + arg = "Response may not contain content." + else: + arg = f"Connection broken: {e!r}" + raise ProtocolError(arg, e) from e + + except (HTTPException, OSError) as e: + raise ProtocolError(f"Connection broken: {e!r}", e) from e + + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now to ensure that the connection is + # released back to the pool. + if self._original_response: + self._original_response.close() + + # Closing the response may not actually be sufficient to close + # everything, so if we have a hold of the connection close that + # too. + if self._connection: + self._connection.close() + + # If we hold the original response but it's closed now, we should + # return the connection back to the pool. + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def _fp_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Read a response with the thought that reading the number of bytes + larger than can fit in a 32-bit int at a time via SSL in some + known cases leads to an overflow error that has to be prevented + if `amt` or `self.length_remaining` indicate that a problem may + happen. + + This happens to urllib3 injected with pyOpenSSL-backed SSL-support. + """ + assert self._fp + c_int_max = 2**31 - 1 + if ( + (amt and amt > c_int_max) + or ( + amt is None + and self.length_remaining + and self.length_remaining > c_int_max + ) + ) and util.IS_PYOPENSSL: + if read1: + return self._fp.read1(c_int_max) + buffer = io.BytesIO() + # Besides `max_chunk_amt` being a maximum chunk size, it + # affects memory overhead of reading a response by this + # method in CPython. + # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum + # chunk size that does not lead to an overflow error, but + # 256 MiB is a compromise. + max_chunk_amt = 2**28 + while amt is None or amt != 0: + if amt is not None: + chunk_amt = min(amt, max_chunk_amt) + amt -= chunk_amt + else: + chunk_amt = max_chunk_amt + data = self._fp.read(chunk_amt) + if not data: + break + buffer.write(data) + del data # to reduce peak memory usage by `max_chunk_amt`. + return buffer.getvalue() + elif read1: + return self._fp.read1(amt) if amt is not None else self._fp.read1() + else: + # StringIO doesn't like amt=None + return self._fp.read(amt) if amt is not None else self._fp.read() + + def _raw_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Reads `amt` of bytes from the socket. + """ + if self._fp is None: + return None # type: ignore[return-value] + + fp_closed = getattr(self._fp, "closed", False) + + with self._error_catcher(): + data = self._fp_read(amt, read1=read1) if not fp_closed else b"" + if amt is not None and amt != 0 and not data: + # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do + # not properly close the connection in all cases. There is + # no harm in redundantly calling close. + self._fp.close() + if ( + self.enforce_content_length + and self.length_remaining is not None + and self.length_remaining != 0 + ): + # This is an edge case that httplib failed to cover due + # to concerns of backward compatibility. We're + # addressing it here to make sure IncompleteRead is + # raised during streaming, so all calls with incorrect + # Content-Length are caught. + raise IncompleteRead(self._fp_bytes_read, self.length_remaining) + elif read1 and ( + (amt != 0 and not data) or self.length_remaining == len(data) + ): + # All data has been read, but `self._fp.read1` in + # CPython 3.12 and older doesn't always close + # `http.client.HTTPResponse`, so we close it here. + # See https://github.com/python/cpython/issues/113199 + self._fp.close() + + if data: + self._fp_bytes_read += len(data) + if self.length_remaining is not None: + self.length_remaining -= len(data) + return data + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + """ + Similar to :meth:`http.client.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + self._init_decoder() + if decode_content is None: + decode_content = self.decode_content + + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + elif amt is not None: + cache_content = False + + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and len(self._decoded_buffer) < amt + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) >= amt: + return self._decoded_buffer.get(amt) + + data = self._raw_read(amt) + if not cache_content: + self._uncached_read_occurred = True + + flush_decoder = amt is None or (amt != 0 and not data) + + if ( + not data + and len(self._decoded_buffer) == 0 + and not (self._decoder and self._decoder.has_unconsumed_tail) + ): + return data + + if amt is None: + data = self._decode(data, decode_content, flush_decoder) + # It's possible that there is buffered decoded data after a + # partial read. + if decode_content and len(self._decoded_buffer) > 0: + self._decoded_buffer.put(data) + data = self._decoded_buffer.get_all() + + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + # do not waste memory on buffer when not decoding + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + + while len(self._decoded_buffer) < amt and data: + # TODO make sure to initially read enough data to get past the headers + # For example, the GZ file header takes 10 bytes, we don't want to read + # it one byte at a time + data = self._raw_read(amt) + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + data = self._decoded_buffer.get(amt) + + return data + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + """ + Similar to ``http.client.HTTPResponse.read1`` and documented + in :meth:`io.BufferedReader.read1`, but with an additional parameter: + ``decode_content``. + + :param amt: + How much of the content to read. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if decode_content is None: + decode_content = self.decode_content + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + # try and respond without going to the network + if self._has_decoded_content: + if not decode_content: + raise RuntimeError( + "Calling read1(decode_content=False) is not supported after " + "read1(decode_content=True) was called." + ) + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and (amt is None or len(self._decoded_buffer) < amt) + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=( + amt - len(self._decoded_buffer) if amt is not None else None + ), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) > 0: + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + if amt == 0: + return b"" + + # FIXME, this method's type doesn't say returning None is possible + data = self._raw_read(amt, read1=True) + self._uncached_read_occurred = True + if not decode_content or data is None: + return data + + self._init_decoder() + while True: + flush_decoder = not data + decoded_data = self._decode( + data, decode_content, flush_decoder, max_length=amt + ) + self._decoded_buffer.put(decoded_data) + if decoded_data or flush_decoder: + break + data = self._raw_read(8192, read1=True) + + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if amt == 0: + return + + if self.chunked and self.supports_chunked_reads(): + yield from self.read_chunked(amt, decode_content=decode_content) + else: + while ( + not is_fp_closed(self._fp) + or len(self._decoded_buffer) > 0 + or (self._decoder and self._decoder.has_unconsumed_tail) + ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + # Overrides from io.IOBase + def readable(self) -> bool: + return True + + def shutdown(self) -> None: + if not self._sock_shutdown: + raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") + if self._connection is None: + raise RuntimeError( + "Cannot shutdown as connection has already been released to the pool" + ) + self._sock_shutdown(socket.SHUT_RD) + + def close(self) -> None: + self._sock_shutdown = None + + if not self.closed and self._fp: + self._fp.close() + + if self._connection: + self._connection.close() + + if not self.auto_close: + io.IOBase.close(self) + + @property + def closed(self) -> bool: + if not self.auto_close: + return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] + elif self._fp is None: + return True + elif hasattr(self._fp, "isclosed"): + return self._fp.isclosed() + elif hasattr(self._fp, "closed"): + return self._fp.closed + else: + return True + + def fileno(self) -> int: + if self._fp is None: + raise OSError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise OSError( + "The file-like object this HTTPResponse is wrapped " + "around has no file descriptor" + ) + + def flush(self) -> None: + if ( + self._fp is not None + and hasattr(self._fp, "flush") + and not getattr(self._fp, "closed", False) + ): + return self._fp.flush() + + def supports_chunked_reads(self) -> bool: + """ + Checks if the underlying file-like object looks like a + :class:`http.client.HTTPResponse` object. We do this by testing for + the fp attribute. If it is present we assume it returns raw chunks as + processed by read_chunked(). + """ + return hasattr(self._fp, "fp") + + def _update_chunk_length(self) -> None: + # First, we'll figure out length of a chunk and then + # we'll try to read it from socket. + if self.chunk_left is not None: + return None + line = self._fp.fp.readline() # type: ignore[union-attr] + line = line.split(b";", 1)[0] + try: + self.chunk_left = int(line, 16) + except ValueError: + self.close() + if line: + # Invalid chunked protocol response, abort. + raise InvalidChunkLength(self, line) from None + else: + # Truncated at start of next chunk + raise ProtocolError("Response ended prematurely") from None + + def _handle_chunk(self, amt: int | None) -> bytes: + returned_chunk = None + if amt is None: + chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + returned_chunk = chunk + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + elif self.chunk_left is not None and amt < self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self.chunk_left = self.chunk_left - amt + returned_chunk = value + elif amt == self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + returned_chunk = value + else: # amt > self.chunk_left + returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + return returned_chunk # type: ignore[no-any-return] + + def read_chunked( + self, amt: int | None = None, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + Similar to :meth:`HTTPResponse.read`, but with an additional + parameter: ``decode_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + self._init_decoder() + # FIXME: Rewrite this method and make it a class with a better structured logic. + if not self.chunked: + raise ResponseNotChunked( + "Response is not chunked. " + "Header 'transfer-encoding: chunked' is missing." + ) + if not self.supports_chunked_reads(): + raise BodyNotHttplibCompatible( + "Body should be http.client.HTTPResponse like. " + "It should have have an fp attribute which returns raw chunks." + ) + + with self._error_catcher(): + # Don't bother reading the body of a HEAD request. + if self._original_response and is_response_to_head(self._original_response): + self._original_response.close() + return None + + # If a response is already read and closed + # then return immediately. + if self._fp.fp is None: # type: ignore[union-attr] + return None + + if amt == 0: + return + elif amt and amt < 0: + # Negative numbers and `None` should be treated the same, + # but httplib handles only `None` correctly. + amt = None + + while True: + # First, check if any data is left in the decoder's buffer. + if self._decoder and self._decoder.has_unconsumed_tail: + chunk = b"" + else: + self._update_chunk_length() + self._uncached_read_occurred = True + if self.chunk_left == 0: + break + chunk = self._handle_chunk(amt) + decoded = self._decode( + chunk, + decode_content=decode_content, + flush_decoder=False, + max_length=amt, + ) + if decoded: + yield decoded + + if decode_content: + # On CPython and PyPy, we should never need to flush the + # decoder. However, on Jython we *might* need to, so + # lets defensively do it anyway. + decoded = self._flush_decoder() + if decoded: # Platform-specific: Jython. + yield decoded + + # Chunk content ends with \r\n: discard it. + while self._fp is not None: + line = self._fp.fp.readline() + if not line: + # Some sites may not end with '\r\n'. + break + if line == b"\r\n": + break + + # We read everything; close the "file". + if self._original_response: + self._original_response.close() + + @property + def url(self) -> str | None: + """ + Returns the URL that was the source of this response. + If the request that generated this response redirected, this method + will return the final redirect location. + """ + return self._request_url + + @url.setter + def url(self, url: str | None) -> None: + self._request_url = url + + def __iter__(self) -> typing.Iterator[bytes]: + buffer: list[bytes] = [] + for chunk in self.stream(decode_content=True): + if b"\n" in chunk: + chunks = chunk.split(b"\n") + yield b"".join(buffer) + chunks[0] + b"\n" + for x in chunks[1:-1]: + yield x + b"\n" + if chunks[-1]: + buffer = [chunks[-1]] + else: + buffer = [] + else: + buffer.append(chunk) + if buffer: + yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py new file mode 100644 index 0000000000..534126033c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py @@ -0,0 +1,42 @@ +# For backwards compatibility, provide imports that used to be here. +from __future__ import annotations + +from .connection import is_connection_dropped +from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers +from .response import is_fp_closed +from .retry import Retry +from .ssl_ import ( + ALPN_PROTOCOLS, + IS_PYOPENSSL, + SSLContext, + assert_fingerprint, + create_urllib3_context, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import Timeout +from .url import Url, parse_url +from .wait import wait_for_read, wait_for_write + +__all__ = ( + "IS_PYOPENSSL", + "SSLContext", + "ALPN_PROTOCOLS", + "Retry", + "Timeout", + "Url", + "assert_fingerprint", + "create_urllib3_context", + "is_connection_dropped", + "is_fp_closed", + "parse_url", + "make_headers", + "resolve_cert_reqs", + "resolve_ssl_version", + "ssl_wrap_socket", + "wait_for_read", + "wait_for_write", + "SKIP_HEADER", + "SKIPPABLE_HEADERS", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py new file mode 100644 index 0000000000..f92519ee91 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import socket +import typing + +from ..exceptions import LocationParseError +from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT + +_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] + +if typing.TYPE_CHECKING: + from .._base_connection import BaseHTTPConnection + + +def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + :param conn: :class:`urllib3.connection.HTTPConnection` object. + """ + return not conn.is_connected + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +# One additional modification is that we avoid binding to IPv6 servers +# discovered in DNS if the system doesn't have IPv6 functionality. +def create_connection( + address: tuple[str, int], + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, +) -> socket.socket: + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`socket.getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith("["): + host = host.strip("[]") + err = None + + # Using the value from allowed_gai_family() in the context of getaddrinfo lets + # us select whether to work with IPv4 DNS records, IPv6 records, or both. + # The original create_connection function always returns all records. + family = allowed_gai_family() + + try: + host.encode("idna") + except UnicodeError: + raise LocationParseError(f"'{host}', label empty or too long") from None + + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + _set_socket_options(sock, socket_options) + + if timeout is not _DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + # Break explicitly a reference cycle + err = None + return sock + + except OSError as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + try: + raise err + finally: + # Break explicitly a reference cycle + err = None + else: + raise OSError("getaddrinfo returns an empty list") + + +def _set_socket_options( + sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None +) -> None: + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) + + +def allowed_gai_family() -> socket.AddressFamily: + """This function is designed to work in the context of + getaddrinfo, where family=socket.AF_UNSPEC is the default and + will perform a DNS search for both IPv6 and IPv4 records.""" + + family = socket.AF_INET + if HAS_IPV6: + family = socket.AF_UNSPEC + return family + + +def _has_ipv6(host: str) -> bool: + """Returns True if the system can bind an IPv6 address.""" + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/urllib3/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = True + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py new file mode 100644 index 0000000000..908fc6621d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .url import Url + +if typing.TYPE_CHECKING: + from ..connection import ProxyConfig + + +def connection_requires_http_tunnel( + proxy_url: Url | None = None, + proxy_config: ProxyConfig | None = None, + destination_scheme: str | None = None, +) -> bool: + """ + Returns True if the connection requires an HTTP CONNECT through the proxy. + + :param URL proxy_url: + URL of the proxy. + :param ProxyConfig proxy_config: + Proxy configuration from poolmanager.py + :param str destination_scheme: + The scheme of the destination. (i.e https, http, etc) + """ + # If we're not using a proxy, no way to use a tunnel. + if proxy_url is None: + return False + + # HTTP destinations never require tunneling, we always forward. + if destination_scheme == "http": + return False + + # Support for forwarding with HTTPS proxies and HTTPS destinations. + if ( + proxy_url.scheme == "https" + and proxy_config + and proxy_config.use_forwarding_for_https + ): + return False + + # Otherwise always use a tunnel. + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py new file mode 100644 index 0000000000..6c2372ba7e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +import io +import sys +import typing +from base64 import b64encode +from enum import Enum + +from ..exceptions import UnrewindableBodyError +from .util import to_bytes + +if typing.TYPE_CHECKING: + from typing import Final + +# Pass as a value within ``headers`` to skip +# emitting some HTTP headers that are added automatically. +# The only headers that are supported are ``Accept-Encoding``, +# ``Host``, and ``User-Agent``. +SKIP_HEADER = "@@@SKIP_HEADER@@@" +SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) + +ACCEPT_ENCODING = "gzip,deflate" +try: + try: + import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 + except ImportError: + import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",br" + +try: + if sys.version_info >= (3, 14): + from compression import zstd as _unused_module_zstd # noqa: F401 + else: + from backports import zstd as _unused_module_zstd # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",zstd" + + +class _TYPE_FAILEDTELL(Enum): + token = 0 + + +_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token + +_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] + +# When sending a request with these methods we aren't expecting +# a body so don't need to set an explicit 'Content-Length: 0' +# The reason we do this in the negative instead of tracking methods +# which 'should' have a body is because unknown methods should be +# treated as if they were 'POST' which *does* expect a body. +_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} + + +def make_headers( + keep_alive: bool | None = None, + accept_encoding: bool | list[str] | str | None = None, + user_agent: str | None = None, + basic_auth: str | None = None, + proxy_basic_auth: str | None = None, + disable_cache: bool | None = None, +) -> dict[str, str]: + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. If the dependencies for + Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or + Zstandard (the ``backports.zstd`` package for Python before 3.14) + algorithms are installed, then their encodings are + included in the string ('br' and 'zstd', respectively). + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example: + + .. code-block:: python + + import urllib3 + + print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) + # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + print(urllib3.util.make_headers(accept_encoding=True)) + # {'accept-encoding': 'gzip,deflate'} + """ + headers: dict[str, str] = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ",".join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers["accept-encoding"] = accept_encoding + + if user_agent: + headers["user-agent"] = user_agent + + if keep_alive: + headers["connection"] = "keep-alive" + + if basic_auth: + headers["authorization"] = ( + f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" + ) + + if proxy_basic_auth: + headers["proxy-authorization"] = ( + f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" + ) + + if disable_cache: + headers["cache-control"] = "no-cache" + + return headers + + +def set_file_position( + body: typing.Any, pos: _TYPE_BODY_POSITION | None +) -> _TYPE_BODY_POSITION | None: + """ + If a position is provided, move file to that point. + Otherwise, we'll attempt to record a position for future use. + """ + if pos is not None: + rewind_body(body, pos) + elif getattr(body, "tell", None) is not None: + try: + pos = body.tell() + except OSError: + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body. + pos = _FAILEDTELL + + return pos + + +def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: + """ + Attempt to rewind body to a certain position. + Primarily used for request redirects and retries. + + :param body: + File-like object that supports seek. + + :param int pos: + Position to seek to in file. + """ + body_seek = getattr(body, "seek", None) + if body_seek is not None and isinstance(body_pos, int): + try: + body_seek(body_pos) + except OSError as e: + raise UnrewindableBodyError( + "An error occurred when rewinding request body for redirect/retry." + ) from e + elif body_pos is _FAILEDTELL: + raise UnrewindableBodyError( + "Unable to record file position for rewinding " + "request body during a redirect/retry." + ) + else: + raise ValueError( + f"body_pos must be of type integer, instead it was {type(body_pos)}." + ) + + +class ChunksAndContentLength(typing.NamedTuple): + chunks: typing.Iterable[bytes] | None + content_length: int | None + + +def body_to_chunks( + body: typing.Any | None, method: str, blocksize: int +) -> ChunksAndContentLength: + """Takes the HTTP request method, body, and blocksize and + transforms them into an iterable of chunks to pass to + socket.sendall() and an optional 'Content-Length' header. + + A 'Content-Length' of 'None' indicates the length of the body + can't be determined so should use 'Transfer-Encoding: chunked' + for framing instead. + """ + + chunks: typing.Iterable[bytes] | None + content_length: int | None + + # No body, we need to make a recommendation on 'Content-Length' + # based on whether that request method is expected to have + # a body or not. + if body is None: + chunks = None + if method.upper() not in _METHODS_NOT_EXPECTING_BODY: + content_length = 0 + else: + content_length = None + + # Bytes or strings become bytes + elif isinstance(body, (str, bytes)): + chunks = (to_bytes(body),) + content_length = len(chunks[0]) + + # File-like object, TODO: use seek() and tell() for length? + elif hasattr(body, "read"): + + def chunk_readable() -> typing.Iterable[bytes]: + encode = isinstance(body, io.TextIOBase) + while True: + datablock = body.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("utf-8") + yield datablock + + chunks = chunk_readable() + content_length = None + + # Otherwise we need to start checking via duck-typing. + else: + try: + # Check if the body implements the buffer API. + mv = memoryview(body) + except TypeError: + try: + # Check if the body is an iterable + chunks = iter(body) + content_length = None + except TypeError: + raise TypeError( + f"'body' must be a bytes-like object, file-like " + f"object, or iterable. Instead was {body!r}" + ) from None + else: + # Since it implements the buffer API can be passed directly to socket.sendall() + chunks = (body,) + content_length = mv.nbytes + + return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py new file mode 100644 index 0000000000..0f4578696f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import http.client as httplib +from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj: object) -> bool: + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check `isclosed()` first, in case Python3 doesn't set `closed`. + # GH Issue #928 + return obj.isclosed() # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check via the official file-like-object way. + return obj.closed # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None # type: ignore[attr-defined] + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers: httplib.HTTPMessage) -> None: + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param http.client.HTTPMessage headers: Headers to verify. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError(f"expected httplib.Message, got {type(headers)}.") + + unparsed_data = None + + # get_payload is actually email.message.Message.get_payload; + # we're only interested in the result if it's not a multipart message + if not headers.is_multipart(): + payload = headers.get_payload() + + if isinstance(payload, (bytes, str)): + unparsed_data = payload + + # httplib is assuming a response body is available + # when parsing headers even when httplib only sends + # header data to parse_headers() This results in + # defects on multipart responses in particular. + # See: https://github.com/urllib3/urllib3/issues/800 + + # So we ignore the following defects: + # - StartBoundaryNotFoundDefect: + # The claimed start boundary was never found. + # - MultipartInvariantViolationDefect: + # A message claimed to be a multipart but no subparts were found. + defects = [ + defect + for defect in headers.defects + if not isinstance( + defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) + ) + ] + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response: httplib.HTTPResponse) -> bool: + """ + Checks whether the request of a response has been a HEAD-request. + + :param http.client.HTTPResponse response: + Response to check if the originating request + used 'HEAD' as a method. + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method_str = response._method # type: str # type: ignore[attr-defined] + return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py new file mode 100644 index 0000000000..7649898e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py @@ -0,0 +1,557 @@ +from __future__ import annotations + +import email +import logging +import random +import re +import time +import typing +from itertools import takewhile +from types import TracebackType + +from ..exceptions import ( + ConnectTimeoutError, + InvalidHeader, + MaxRetryError, + ProtocolError, + ProxyError, + ReadTimeoutError, + ResponseError, +) +from .util import reraise + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from ..connectionpool import ConnectionPool + from ..response import BaseHTTPResponse + +log = logging.getLogger(__name__) + + +# Data structure for representing the metadata of requests that result in a retry. +class RequestHistory(typing.NamedTuple): + method: str | None + url: str | None + error: Exception | None + status: int | None + redirect_location: str | None + + +class Retry: + """Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool: + + .. code-block:: python + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request("GET", "https://example.com/") + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=Retry(10)) + + Retries can be disabled by passing ``False``: + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int status: + How many times to retry on bad status codes. + + These are retries made on responses, where status code matches + ``status_forcelist``. + + Set to ``0`` to fail on the first retry of this type. + + :param int other: + How many times to retry on other errors. + + Other errors are errors that are not connect, read, redirect or status errors. + These errors might be raised after the request was sent to the server, so the + request might have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + If ``total`` is not set, it's a good idea to set this to 0 to account + for unexpected edge cases and avoid infinite retry loops. + + :param Collection allowed_methods: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + idempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. + + Set to a ``None`` value to retry on any verb. + + :param Collection status_forcelist: + A set of integer HTTP status codes that we should force a retry on. + A retry is initiated if the request method is in ``allowed_methods`` + and the response status code is in ``status_forcelist``. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts after the second try + (most errors are resolved immediately by a second try without a + delay). urllib3 will sleep for:: + + {backoff factor} * (2 ** ({number of previous retries})) + + seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: + + random.uniform(0, {backoff jitter}) + + seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will + sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever + be longer than `backoff_max`. + + By default, backoff is disabled (factor set to 0). + + :param float backoff_max: + The maximum backoff time (in seconds) between retry attempts. + This value caps the computed backoff from `backoff_factor`. + + :param float backoff_jitter: + Random jitter amount (in seconds) added to the computed backoff. + Jitter is sampled uniformly from `0` to `backoff_jitter`. + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have + been exhausted. + + :param tuple history: The history of the request encountered during + each call to :meth:`~Retry.increment`. The list is in the order + the requests occurred. Each list item is of class :class:`RequestHistory`. + + :param bool respect_retry_after_header: + Whether to respect Retry-After header on status codes defined as + :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. + + :param Collection remove_headers_on_redirect: + Sequence of headers to remove from the request when a response + indicating a redirect is returned before firing off the redirected + request. + + :param int retry_after_max: Number of seconds to allow as the maximum for + Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. + Any Retry-After headers larger than this value will be limited to this + value. + """ + + #: Default methods to be used for ``allowed_methods`` + DEFAULT_ALLOWED_METHODS = frozenset( + ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) + + #: Default status codes to be used for ``status_forcelist`` + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Default headers to be used for ``remove_headers_on_redirect`` + DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( + ["Cookie", "Authorization", "Proxy-Authorization"] + ) + + #: Default maximum backoff time. + DEFAULT_BACKOFF_MAX = 120 + + # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. + #: Default maximum allowed value for Retry-After headers in seconds + DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 + + # Backward compatibility; assigned outside of the class. + DEFAULT: typing.ClassVar[Retry] + + def __init__( + self, + total: bool | int | None = 10, + connect: int | None = None, + read: int | None = None, + redirect: bool | int | None = None, + status: int | None = None, + other: int | None = None, + allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, + status_forcelist: typing.Collection[int] | None = None, + backoff_factor: float = 0, + backoff_max: float = DEFAULT_BACKOFF_MAX, + raise_on_redirect: bool = True, + raise_on_status: bool = True, + history: tuple[RequestHistory, ...] | None = None, + respect_retry_after_header: bool = True, + remove_headers_on_redirect: typing.Collection[ + str + ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, + backoff_jitter: float = 0.0, + retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, + ) -> None: + self.total = total + self.connect = connect + self.read = read + self.status = status + self.other = other + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.allowed_methods = allowed_methods + self.backoff_factor = backoff_factor + self.backoff_max = backoff_max + self.retry_after_max = retry_after_max + self.raise_on_redirect = raise_on_redirect + self.raise_on_status = raise_on_status + self.history = history or () + self.respect_retry_after_header = respect_retry_after_header + self.remove_headers_on_redirect = frozenset( + h.lower() for h in remove_headers_on_redirect + ) + self.backoff_jitter = backoff_jitter + + def new(self, **kw: typing.Any) -> Self: + params = dict( + total=self.total, + connect=self.connect, + read=self.read, + redirect=self.redirect, + status=self.status, + other=self.other, + allowed_methods=self.allowed_methods, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + backoff_max=self.backoff_max, + retry_after_max=self.retry_after_max, + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, + remove_headers_on_redirect=self.remove_headers_on_redirect, + respect_retry_after_header=self.respect_retry_after_header, + backoff_jitter=self.backoff_jitter, + ) + + params.update(kw) + return type(self)(**params) # type: ignore[arg-type] + + @classmethod + def from_int( + cls, + retries: Retry | bool | int | None, + redirect: bool | int | None = True, + default: Retry | bool | int | None = None, + ) -> Retry: + """Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r", retries, new_retries) + return new_retries + + def get_backoff_time(self) -> float: + """Formula for computing the current backoff + + :rtype: float + """ + # We want to consider only the last consecutive errors sequence (Ignore redirects). + consecutive_errors_len = len( + list( + takewhile(lambda x: x.redirect_location is None, reversed(self.history)) + ) + ) + if consecutive_errors_len <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) + if self.backoff_jitter != 0.0: + backoff_value += random.random() * self.backoff_jitter + return float(max(0, min(self.backoff_max, backoff_value))) + + def parse_retry_after(self, retry_after: str) -> float: + seconds: float + # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = int(retry_after) + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + seconds = max(seconds, 0) + + # Check the seconds do not exceed the specified maximum + if seconds > self.retry_after_max: + seconds = self.retry_after_max + + return seconds + + def get_retry_after(self, response: BaseHTTPResponse) -> float | None: + """Get the value of Retry-After in seconds.""" + + retry_after = response.headers.get("Retry-After") + + if retry_after is None: + return None + + return self.parse_retry_after(retry_after) + + def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: + retry_after = self.get_retry_after(response) + if retry_after: + time.sleep(retry_after) + return True + + return False + + def _sleep_backoff(self) -> None: + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def sleep(self, response: BaseHTTPResponse | None = None) -> None: + """Sleep between retry attempts. + + This method will respect a server's ``Retry-After`` response header + and sleep the duration of the time requested. If that is not present, it + will use an exponential backoff. By default, the backoff factor is 0 and + this method will return immediately. + """ + + if self.respect_retry_after_header and response: + slept = self.sleep_for_retry(response) + if slept: + return + + self._sleep_backoff() + + def _is_connection_error(self, err: Exception) -> bool: + """Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + if isinstance(err, ProxyError): + err = err.original_error + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err: Exception) -> bool: + """Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def _is_method_retryable(self, method: str) -> bool: + """Checks if a given HTTP method should be retried upon, depending if + it is included in the allowed_methods + """ + if self.allowed_methods and method.upper() not in self.allowed_methods: + return False + return True + + def is_retry( + self, method: str, status_code: int, has_retry_after: bool = False + ) -> bool: + """Is this method/status code retryable? (Based on allowlists and control + variables such as the number of total retries to allow, whether to + respect the Retry-After header, whether this header is present, and + whether the returned status code is on the list of status codes to + be retried upon on the presence of the aforementioned header) + """ + if not self._is_method_retryable(method): + return False + + if self.status_forcelist and status_code in self.status_forcelist: + return True + + return bool( + self.total + and self.respect_retry_after_header + and has_retry_after + and (status_code in self.RETRY_AFTER_STATUS_CODES) + ) + + def is_exhausted(self) -> bool: + """Are we out of retries?""" + retry_counts = [ + x + for x in ( + self.total, + self.connect, + self.read, + self.redirect, + self.status, + self.other, + ) + if x + ] + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment( + self, + method: str | None = None, + url: str | None = None, + response: BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, + ) -> Self: + """Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.BaseHTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + connect = self.connect + read = self.read + redirect = self.redirect + status_count = self.status + other = self.other + cause = "unknown" + status = None + redirect_location = None + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False or method is None or not self._is_method_retryable(method): + raise reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + + elif error: + # Other retry? + if other is not None: + other -= 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = "too many redirects" + response_redirect_location = response.get_redirect_location() + if response_redirect_location: + redirect_location = response_redirect_location + status = response.status + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and the given method is in the allowed_methods + cause = ResponseError.GENERIC_ERROR + if response and response.status: + if status_count is not None: + status_count -= 1 + cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) + status = response.status + + history = self.history + ( + RequestHistory(method, url, error, status, redirect_location), + ) + + new_retry = self.new( + total=total, + connect=connect, + read=read, + redirect=redirect, + status=status_count, + other=other, + history=history, + ) + + if new_retry.is_exhausted(): + reason = error or ResponseError(cause) + raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] + + log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) + + return new_retry + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(total={self.total}, connect={self.connect}, " + f"read={self.read}, redirect={self.redirect}, status={self.status})" + ) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py new file mode 100644 index 0000000000..e66549a76c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +import hashlib +import hmac +import os +import socket +import sys +import typing +import warnings +from binascii import unhexlify + +from ..exceptions import ProxySchemeUnsupported, SSLError +from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE + +SSLContext = None +SSLTransport = None +HAS_NEVER_CHECK_COMMON_NAME = False +IS_PYOPENSSL = False +ALPN_PROTOCOLS = ["http/1.1"] + +_TYPE_VERSION_INFO = tuple[int, int, int, str, int] + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = { + length: getattr(hashlib, algorithm, None) + for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) +} + + +def _is_has_never_check_common_name_reliable( + openssl_version: str, +) -> bool: + # As of May 2023, all released versions of LibreSSL fail to reject certificates with + # only common names, see https://github.com/urllib3/urllib3/pull/3024 + is_openssl = openssl_version.startswith("OpenSSL ") + + return is_openssl + + +if typing.TYPE_CHECKING: + from ssl import VerifyMode + from typing import TypedDict + + from .ssltransport import SSLTransport as SSLTransportType + + class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): + subjectAltName: tuple[tuple[str, str], ...] + subject: tuple[tuple[tuple[str, str], ...], ...] + serialNumber: str + + +# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' +_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} + +try: # Do we have ssl at all? + import ssl + from ssl import ( # type: ignore[assignment] + CERT_REQUIRED, + HAS_NEVER_CHECK_COMMON_NAME, + OP_NO_COMPRESSION, + OP_NO_TICKET, + OPENSSL_VERSION, + PROTOCOL_TLS, + PROTOCOL_TLS_CLIENT, + VERIFY_X509_PARTIAL_CHAIN, + VERIFY_X509_STRICT, + OP_NO_SSLv2, + OP_NO_SSLv3, + SSLContext, + TLSVersion, + ) + + PROTOCOL_SSLv23 = PROTOCOL_TLS + + # Setting SSLContext.hostname_checks_common_name = False didn't work with + # LibreSSL, check details in the used function. + if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( + OPENSSL_VERSION, + ): # Defensive: + HAS_NEVER_CHECK_COMMON_NAME = False + + # Need to be careful here in case old TLS versions get + # removed in future 'ssl' module implementations. + for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): + try: + _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( + TLSVersion, attr + ) + except AttributeError: # Defensive: + continue + + from .ssltransport import SSLTransport # type: ignore[assignment] +except ImportError: + OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] + OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] + OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] + OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] + PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] + PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] + VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] + VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] + + +_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] + + +def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + if cert is None: + raise SSLError("No certificate for the peer.") + + fingerprint = fingerprint.replace(":", "").lower() + digest_length = len(fingerprint) + if digest_length not in HASHFUNC_MAP: + raise SSLError(f"Fingerprint of invalid length: {fingerprint}") + hashfunc = HASHFUNC_MAP.get(digest_length) + if hashfunc is None: + raise SSLError( + f"Hash function implementation unavailable for fingerprint length: {digest_length}" + ) + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not hmac.compare_digest(cert_digest, fingerprint_bytes): + raise SSLError( + f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' + ) + + +def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_REQUIRED`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbreviation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_REQUIRED + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "CERT_" + candidate) + return res # type: ignore[no-any-return] + + return candidate # type: ignore[return-value] + + +def resolve_ssl_version(candidate: None | int | str) -> int: + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_TLS + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "PROTOCOL_" + candidate) + return typing.cast(int, res) + + return candidate + + +def create_urllib3_context( + ssl_version: int | None = None, + cert_reqs: int | None = None, + options: int | None = None, + ciphers: str | None = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + verify_flags: int | None = None, +) -> ssl.SSLContext: + """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + + This parameter is deprecated instead use 'ssl_minimum_version'. + :param ssl_minimum_version: + The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + :param ssl_maximum_version: + The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the + default value. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. + :param ciphers: + Which cipher suites to allow the server to select. Defaults to either system configured + ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. + :param verify_flags: + The flags for certificate verification operations. These default to + ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + if SSLContext is None: + raise TypeError("Can't create an SSLContext object without an ssl module") + + # This means 'ssl_version' was specified as an exact value. + if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): + # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' + # to avoid conflicts. + if ssl_minimum_version is not None or ssl_maximum_version is not None: + raise ValueError( + "Can't specify both 'ssl_version' and either " + "'ssl_minimum_version' or 'ssl_maximum_version'" + ) + + # 'ssl_version' is deprecated and will be removed in the future. + else: + # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. + ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MINIMUM_SUPPORTED + ) + ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MAXIMUM_SUPPORTED + ) + + # This warning message is pushing users to use 'ssl_minimum_version' + # instead of both min/max. Best practice is to only set the minimum version and + # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' + warnings.warn( + "'ssl_version' option is deprecated and will be " + "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", + category=FutureWarning, + stacklevel=2, + ) + + context = SSLContext(PROTOCOL_TLS_CLIENT) + if ssl_minimum_version is not None: + context.minimum_version = ssl_minimum_version + else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here + context.minimum_version = TLSVersion.TLSv1_2 + + if ssl_maximum_version is not None: + context.maximum_version = ssl_maximum_version + + # Unless we're given ciphers defer to either system ciphers in + # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. + if ciphers: + context.set_ciphers(ciphers) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + # TLSv1.2 only. Unless set explicitly, do not request tickets. + # This may save some bandwidth on wire, and although the ticket is encrypted, + # there is a risk associated with it being on wire, + # if the server is not rotating its ticketing keys properly. + options |= OP_NO_TICKET + + context.options |= options + + if verify_flags is None: + verify_flags = 0 + # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN + # and VERIFY_X509_STRICT so we do the same + if sys.version_info >= (3, 13): + verify_flags |= VERIFY_X509_PARTIAL_CHAIN + verify_flags |= VERIFY_X509_STRICT + + context.verify_flags |= verify_flags + + # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is + # necessary for conditional client cert authentication with TLS 1.3. + # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using + # an SSLContext created by pyOpenSSL. + if getattr(context, "post_handshake_auth", None) is not None: + context.post_handshake_auth = True + + # The order of the below lines setting verify_mode and check_hostname + # matter due to safe-guards SSLContext has to prevent an SSLContext with + # check_hostname=True, verify_mode=NONE/OPTIONAL. + # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own + # 'ssl.match_hostname()' implementation. + if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: + context.verify_mode = cert_reqs + context.check_hostname = True + else: + context.check_hostname = False + context.verify_mode = cert_reqs + + context.hostname_checks_common_name = False + + if "SSLKEYLOGFILE" in os.environ: + sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) + else: + sslkeylogfile = None + if sslkeylogfile: + context.keylog_filename = sslkeylogfile + + return context + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: typing.Literal[False] = ..., +) -> ssl.SSLSocket: ... + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: bool = ..., +) -> ssl.SSLSocket | SSLTransportType: ... + + +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = None, + certfile: str | None = None, + cert_reqs: int | None = None, + ca_certs: str | None = None, + server_hostname: str | None = None, + ssl_version: int | None = None, + ciphers: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_cert_dir: str | None = None, + key_password: str | None = None, + ca_cert_data: None | str | bytes = None, + tls_in_tls: bool = False, +) -> ssl.SSLSocket | SSLTransportType: + """ + All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and + ca_cert_dir have the same meaning as they do when using + :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, + :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + :param key_password: + Optional password if the keyfile is encrypted. + :param ca_cert_data: + Optional string containing CA certificates in PEM format suitable for + passing as the cadata parameter to SSLContext.load_verify_locations() + :param tls_in_tls: + Use SSLTransport to wrap the existing socket. + """ + context = ssl_context + if context is None: + # Note: This branch of code and all the variables in it are only used in tests. + # We should consider deprecating and removing this code. + context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) + + if ca_certs or ca_cert_dir or ca_cert_data: + try: + context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) + except OSError as e: + raise SSLError(e) from e + + elif ssl_context is None and hasattr(context, "load_default_certs"): + # try to load OS default certs; works well on Windows. + context.load_default_certs() + + # Attempt to detect if we get the goofy behavior of the + # keyfile being encrypted and OpenSSL asking for the + # passphrase via the terminal and instead error out. + if keyfile and key_password is None and _is_key_file_encrypted(keyfile): + raise SSLError("Client private key is encrypted, password is required") + + if certfile: + if key_password is None: + context.load_cert_chain(certfile, keyfile) + else: + context.load_cert_chain(certfile, keyfile, key_password) + + context.set_alpn_protocols(ALPN_PROTOCOLS) + + ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) + return ssl_sock + + +def is_ipaddress(hostname: str | bytes) -> bool: + """Detects whether the hostname given is an IPv4 or IPv6 address. + Also detects IPv6 addresses with Zone IDs. + + :param str hostname: Hostname to examine. + :return: True if the hostname is an IP address, False otherwise. + """ + if isinstance(hostname, bytes): + # IDN A-label bytes are ASCII compatible. + hostname = hostname.decode("ascii") + return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) + + +def _is_key_file_encrypted(key_file: str) -> bool: + """Detects if a key file is encrypted or not.""" + with open(key_file) as f: + for line in f: + # Look for Proc-Type: 4,ENCRYPTED + if "ENCRYPTED" in line: + return True + + return False + + +def _ssl_wrap_socket_impl( + sock: socket.socket, + ssl_context: ssl.SSLContext, + tls_in_tls: bool, + server_hostname: str | None = None, +) -> ssl.SSLSocket | SSLTransportType: + if tls_in_tls: + if not SSLTransport: + # Import error, ssl is not available. + raise ProxySchemeUnsupported( + "TLS in TLS requires support for the 'ssl' module" + ) + + SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) + return SSLTransport(sock, ssl_context, server_hostname) + + return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py new file mode 100644 index 0000000000..94994f25ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py @@ -0,0 +1,153 @@ +"""The match_hostname() function from Python 3.5, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html +# It is modified to remove commonName support. + +from __future__ import annotations + +import ipaddress +import re +import typing +from ipaddress import IPv4Address, IPv6Address + +if typing.TYPE_CHECKING: + from .ssl_ import _TYPE_PEER_CERT_RET_DICT + +__version__ = "3.5.0.1" + + +class CertificateError(ValueError): + pass + + +def _dnsname_match( + dn: typing.Any, hostname: str, max_wildcards: int = 1 +) -> typing.Match[str] | None | bool: + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r".") + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count("*") + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn) + ) + + # speed up common case w/o wildcards + if not wildcards: + return bool(dn.lower() == hostname.lower()) + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == "*": + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append("[^.]+") + elif leftmost.startswith("xn--") or hostname.startswith("xn--"): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) + return pat.match(hostname) + + +def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: + """Exact matching of IP addresses. + + RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded + bytes of the IP address. An IP version 4 address is 4 octets, and an IP + version 6 address is 16 octets. [...] A reference identity of type IP-ID + matches if the address is identical to an iPAddress value of the + subjectAltName extension of the certificate." + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(ipname.rstrip()) + return bool(ip.packed == host_ip.packed) + + +def match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError( + "empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED" + ) + + try: + host_ip = ipaddress.ip_address(hostname) + except ValueError: + # Not an IP address (common case) + host_ip = None + dnsnames = [] + san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) + key: str + value: str + for key, value in san: + if key == "DNS": + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == "IP Address": + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + + # We only check 'commonName' if it's enabled and we're not verifying + # an IP address. IP addresses aren't valid within 'commonName'. + if hostname_checks_common_name and host_ip is None and not dnsnames: + for sub in cert.get("subject", ()): + for key, value in sub: + if key == "commonName": + if _dnsname_match(value, hostname): + return + dnsnames.append( + value + ) # Defensive: for older PyPy and OpenSSL versions + + if len(dnsnames) > 1: + raise CertificateError( + "hostname %r " + "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) + ) + elif len(dnsnames) == 1: + raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") + else: + raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py new file mode 100644 index 0000000000..6d59bc3bce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +import io +import socket +import ssl +import typing + +from ..exceptions import ProxySchemeUnsupported + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT + + +_WriteBuffer = typing.Union[bytearray, memoryview] +_ReturnValue = typing.TypeVar("_ReturnValue") + +SSL_BLOCKSIZE = 16384 + + +class SSLTransport: + """ + The SSLTransport wraps an existing socket and establishes an SSL connection. + + Contrary to Python's implementation of SSLSocket, it allows you to chain + multiple TLS connections together. It's particularly useful if you need to + implement TLS within TLS. + + The class supports most of the socket API operations. + """ + + @staticmethod + def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: + """ + Raises a ProxySchemeUnsupported if the provided ssl_context can't be used + for TLS in TLS. + + The only requirement is that the ssl_context provides the 'wrap_bio' + methods. + """ + + if not hasattr(ssl_context, "wrap_bio"): + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "available on non-native SSLContext" + ) + + def __init__( + self, + socket: socket.socket, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + suppress_ragged_eofs: bool = True, + ) -> None: + """ + Create an SSLTransport around socket using the provided ssl_context. + """ + self.incoming = ssl.MemoryBIO() + self.outgoing = ssl.MemoryBIO() + + self.suppress_ragged_eofs = suppress_ragged_eofs + self.socket = socket + + self.sslobj = ssl_context.wrap_bio( + self.incoming, self.outgoing, server_hostname=server_hostname + ) + + # Perform initial handshake. + self._ssl_io_loop(self.sslobj.do_handshake) + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: typing.Any) -> None: + self.close() + + def fileno(self) -> int: + return self.socket.fileno() + + def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: + return self._wrap_ssl_read(len, buffer) + + def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv") + return self._wrap_ssl_read(buflen) + + def recv_into( + self, + buffer: _WriteBuffer, + nbytes: int | None = None, + flags: int = 0, + ) -> None | int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv_into") + if nbytes is None: + nbytes = len(buffer) + return self.read(nbytes, buffer) + + def sendall(self, data: bytes, flags: int = 0) -> None: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall") + count = 0 + with memoryview(data) as view, view.cast("B") as byte_view: + amount = len(byte_view) + while count < amount: + v = self.send(byte_view[count:]) + count += v + + def send(self, data: bytes, flags: int = 0) -> int: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send") + return self._ssl_io_loop(self.sslobj.write, data) + + def makefile( + self, + mode: str, + buffering: int | None = None, + *, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: + """ + Python's httpclient uses makefile and buffered io when reading HTTP + messages and we need to support it. + + This is unfortunately a copy and paste of socket.py makefile with small + changes to point to the socket directly. + """ + if not set(mode) <= {"r", "w", "b"}: + raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") + + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] + self.socket._io_refs += 1 # type: ignore[attr-defined] + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + buffer: typing.BinaryIO + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode # type: ignore[misc] + return text + + def unwrap(self) -> None: + self._ssl_io_loop(self.sslobj.unwrap) + + def close(self) -> None: + self.socket.close() + + @typing.overload + def getpeercert( + self, binary_form: typing.Literal[False] = ... + ) -> _TYPE_PEER_CERT_RET_DICT | None: ... + + @typing.overload + def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... + + def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: + return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] + + def version(self) -> str | None: + return self.sslobj.version() + + def cipher(self) -> tuple[str, str, int] | None: + return self.sslobj.cipher() + + def selected_alpn_protocol(self) -> str | None: + return self.sslobj.selected_alpn_protocol() + + def shared_ciphers(self) -> list[tuple[str, str, int]] | None: + return self.sslobj.shared_ciphers() + + def compression(self) -> str | None: + return self.sslobj.compression() + + def settimeout(self, value: float | None) -> None: + self.socket.settimeout(value) + + def gettimeout(self) -> float | None: + return self.socket.gettimeout() + + def _decref_socketios(self) -> None: + self.socket._decref_socketios() # type: ignore[attr-defined] + + def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: + try: + return self._ssl_io_loop(self.sslobj.read, len, buffer) + except ssl.SSLError as e: + if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: + return 0 # eof, return 0. + else: + raise + + # func is sslobj.do_handshake or sslobj.unwrap + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... + + # func is sslobj.write, arg1 is data + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... + + # func is sslobj.read, arg1 is len, arg2 is buffer + @typing.overload + def _ssl_io_loop( + self, + func: typing.Callable[[int, bytearray | None], bytes], + arg1: int, + arg2: bytearray | None, + ) -> bytes: ... + + def _ssl_io_loop( + self, + func: typing.Callable[..., _ReturnValue], + arg1: None | bytes | int = None, + arg2: bytearray | None = None, + ) -> _ReturnValue: + """Performs an I/O loop between incoming/outgoing and the socket.""" + should_loop = True + ret = None + + while should_loop: + errno = None + try: + if arg1 is None and arg2 is None: + ret = func() + elif arg2 is None: + ret = func(arg1) + else: + ret = func(arg1, arg2) + except ssl.SSLError as e: + if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): + # WANT_READ, and WANT_WRITE are expected, others are not. + raise e + errno = e.errno + + buf = self.outgoing.read() + self.socket.sendall(buf) + + if errno is None: + should_loop = False + elif errno == ssl.SSL_ERROR_WANT_READ: + buf = self.socket.recv(SSL_BLOCKSIZE) + if buf: + self.incoming.write(buf) + else: + self.incoming.write_eof() + return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py new file mode 100644 index 0000000000..4bb1be11d9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import time +import typing +from enum import Enum +from socket import getdefaulttimeout + +from ..exceptions import TimeoutStateError + +if typing.TYPE_CHECKING: + from typing import Final + + +class _TYPE_DEFAULT(Enum): + # This value should never be passed to socket.settimeout() so for safety we use a -1. + # socket.settimout() raises a ValueError for negative values. + token = -1 + + +_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token + +_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] + + +class Timeout: + """Timeout configuration. + + Timeouts can be defined as a default for a pool: + + .. code-block:: python + + import urllib3 + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + + http = urllib3.PoolManager(timeout=timeout) + + resp = http.request("GET", "https://example.com/") + + print(resp.status) + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``: + + .. code-block:: python + + no_timeout = Timeout(connect=None, read=None) + response = http.request("GET", "https://example.com/", timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: int, float, or None + + :param connect: + The maximum amount of time (in seconds) to wait for a connection + attempt to a server to succeed. Omitting the parameter will default the + connect timeout to the system default, probably `the global default + timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: int, float, or None + + :param read: + The maximum amount of time (in seconds) to wait between consecutive + read operations for a response from the server. Omitting the parameter + will default the read timeout to the system default, probably `the + global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: int, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT + + def __init__( + self, + total: _TYPE_TIMEOUT = None, + connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + ) -> None: + self._connect = self._validate_timeout(connect, "connect") + self._read = self._validate_timeout(read, "read") + self.total = self._validate_timeout(total, "total") + self._start_connect: float | None = None + + def __repr__(self) -> str: + return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" + + # __str__ provided for backwards compatibility + __str__ = __repr__ + + @staticmethod + def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: + return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout + + @classmethod + def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: + """Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If it is a numeric value less than or equal to + zero, or the type is not an integer, float, or None. + """ + if value is None or value is _DEFAULT_TIMEOUT: + return value + + if isinstance(value, bool): + raise ValueError( + "Timeout cannot be a boolean value. It must " + "be an int, float or None." + ) + try: + float(value) + except (TypeError, ValueError): + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + try: + if value <= 0: + raise ValueError( + "Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value) + ) + except TypeError: + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + return value + + @classmethod + def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: + """Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self) -> Timeout: + """Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, total=self.total) + + def start_connect(self) -> float: + """Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = time.monotonic() + return self._start_connect + + def get_connect_duration(self) -> float: + """Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time in seconds. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError( + "Can't get connect duration for timer that has not started." + ) + return time.monotonic() - self._start_connect + + @property + def connect_timeout(self) -> _TYPE_TIMEOUT: + """Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is _DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) # type: ignore[type-var] + + @property + def read_timeout(self) -> float | None: + """Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if ( + self.total is not None + and self.total is not _DEFAULT_TIMEOUT + and self._read is not None + and self._read is not _DEFAULT_TIMEOUT + ): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), self._read)) + elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py new file mode 100644 index 0000000000..db057f17be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import re +import typing + +from ..exceptions import LocationParseError +from .util import to_str + +# We only want to normalize urls with an HTTP(S) scheme. +# urllib3 infers URLs without a scheme (None) to be http. +_NORMALIZABLE_SCHEMES = ("http", "https", None) + +# Almost all of these patterns were derived from the +# 'rfc3986' module: https://github.com/python-hyper/rfc3986 +_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") +_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") +_URI_RE = re.compile( + r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" + r"(?://([^\\/?#]*))?" + r"([^?#]*)" + r"(?:\?([^#]*))?" + r"(?:#(.*))?$", + re.UNICODE | re.DOTALL, +) + +_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" +_HEX_PAT = "[0-9A-Fa-f]{1,4}" +_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) +_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} +_variations = [ + # 6( h16 ":" ) ls32 + "(?:%(hex)s:){6}%(ls32)s", + # "::" 5( h16 ":" ) ls32 + "::(?:%(hex)s:){5}%(ls32)s", + # [ h16 ] "::" 4( h16 ":" ) ls32 + "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", + # [ *4( h16 ":" ) h16 ] "::" ls32 + "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", + # [ *5( h16 ":" ) h16 ] "::" h16 + "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", + # [ *6( h16 ":" ) h16 ] "::" + "(?:(?:%(hex)s:){0,6}%(hex)s)?::", +] + +_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" +_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" +_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" +_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" +_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" +_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") + +_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") +_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") +_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") +_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") +_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") + +_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( + _REG_NAME_PAT, + _IPV4_PAT, + _IPV6_ADDRZ_PAT, +) +_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) + +_UNRESERVED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" +) +_SUB_DELIM_CHARS = set("!$&'()*+,;=") +_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} +_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} +_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} + + +class Url( + typing.NamedTuple( + "Url", + [ + ("scheme", typing.Optional[str]), + ("auth", typing.Optional[str]), + ("host", typing.Optional[str]), + ("port", typing.Optional[int]), + ("path", typing.Optional[str]), + ("query", typing.Optional[str]), + ("fragment", typing.Optional[str]), + ], + ) +): + """ + Data structure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. Both the scheme and host are normalized as they are + both case-insensitive according to RFC 3986. + """ + + def __new__( # type: ignore[no-untyped-def] + cls, + scheme: str | None = None, + auth: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ): + if path and not path.startswith("/"): + path = "/" + path + if scheme is not None: + scheme = scheme.lower() + return super().__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self) -> str | None: + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self) -> str: + """Absolute path including the query string.""" + uri = self.path or "/" + + if self.query is not None: + uri += "?" + self.query + + return uri + + @property + def authority(self) -> str | None: + """ + Authority component as defined in RFC 3986 3.2. + This includes userinfo (auth), host and port. + + i.e. + userinfo@host:port + """ + userinfo = self.auth + netloc = self.netloc + if netloc is None or userinfo is None: + return netloc + else: + return f"{userinfo}@{netloc}" + + @property + def netloc(self) -> str | None: + """ + Network location including host and port. + + If you need the equivalent of urllib.parse's ``netloc``, + use the ``authority`` property instead. + """ + if self.host is None: + return None + if self.port: + return f"{self.host}:{self.port}" + return self.host + + @property + def url(self) -> str: + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: + + .. code-block:: python + + import urllib3 + + U = urllib3.util.parse_url("https://google.com/mail/") + + print(U.url) + # "https://google.com/mail/" + + print( urllib3.util.Url("https", "username:password", + "host.com", 80, "/path", "query", "fragment" + ).url + ) + # "https://username:password@host.com:80/path?query#fragment" + """ + scheme, auth, host, port, path, query, fragment = self + url = "" + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + "://" + if auth is not None: + url += auth + "@" + if host is not None: + url += host + if port is not None: + url += ":" + str(port) + if path is not None: + url += path + if query is not None: + url += "?" + query + if fragment is not None: + url += "#" + fragment + + return url + + def __str__(self) -> str: + return self.url + + +@typing.overload +def _encode_invalid_chars( + component: str, allowed_chars: typing.Container[str] +) -> str: # Abstract + ... + + +@typing.overload +def _encode_invalid_chars( + component: None, allowed_chars: typing.Container[str] +) -> None: # Abstract + ... + + +def _encode_invalid_chars( + component: str | None, allowed_chars: typing.Container[str] +) -> str | None: + """Percent-encodes a URI component without reapplying + onto an already percent-encoded component. + """ + if component is None: + return component + + component = to_str(component) + + # Normalize existing percent-encoded bytes. + # Try to see if the component we're encoding is already percent-encoded + # so we can skip all '%' characters but still encode all others. + component, percent_encodings = _PERCENT_RE.subn( + lambda match: match.group(0).upper(), component + ) + + uri_bytes = component.encode("utf-8", "surrogatepass") + is_percent_encoded = percent_encodings == uri_bytes.count(b"%") + encoded_component = bytearray() + + for i in range(0, len(uri_bytes)): + # Will return a single character bytestring + byte = uri_bytes[i : i + 1] + byte_ord = ord(byte) + if (is_percent_encoded and byte == b"%") or ( + byte_ord < 128 and byte.decode() in allowed_chars + ): + encoded_component += byte + continue + encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) + + return encoded_component.decode() + + +def _remove_path_dot_segments(path: str) -> str: + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = path.split("/") # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == ".": + continue + # Anything other than '..', should be appended to the output + if segment != "..": + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if path.startswith("/") and (not output or output[0]): + output.insert(0, "") + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if path.endswith(("/.", "/..")): + output.append("") + + return "/".join(output) + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + if host: + if scheme in _NORMALIZABLE_SCHEMES: + is_ipv6 = _IPV6_ADDRZ_RE.match(host) + if is_ipv6: + # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as + # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID + # separator as necessary to return a valid RFC 4007 scoped IP. + match = _ZONE_ID_RE.search(host) + if match: + start, end = match.span(1) + zone_id = host[start:end] + + if zone_id.startswith("%25") and zone_id != "%25": + zone_id = zone_id[3:] + else: + zone_id = zone_id[1:] + zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) + return f"{host[:start].lower()}%{zone_id}{host[end:]}" + else: + return host.lower() + elif not _IPV4_RE.match(host): + return to_str( + b".".join([_idna_encode(label) for label in host.split(".")]), + "ascii", + ) + return host + + +def _idna_encode(name: str) -> bytes: + if not name.isascii(): + try: + import idna + except ImportError: + raise LocationParseError( + "Unable to parse URL without the 'idna' module" + ) from None + + try: + return idna.encode(name.lower(), strict=True, std3_rules=True) + except idna.IDNAError: + raise LocationParseError( + f"Name '{name}' is not a valid IDNA label" + ) from None + + return name.lower().encode("ascii") + + +def _encode_target(target: str) -> str: + """Percent-encodes a request target so that there are no invalid characters + + Pre-condition for this function is that 'target' must start with '/'. + If that is the case then _TARGET_RE will always produce a match. + """ + match = _TARGET_RE.match(target) + if not match: # Defensive: + raise LocationParseError(f"{target!r} is not a valid request URI") + + path, query = match.groups() + encoded_target = _encode_invalid_chars(path, _PATH_CHARS) + if query is not None: + query = _encode_invalid_chars(query, _QUERY_CHARS) + encoded_target += "?" + query + return encoded_target + + +def parse_url(url: str) -> Url: + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + This parser is RFC 3986 and RFC 6874 compliant. + + The parser logic and helper functions are based heavily on + work done in the ``rfc3986`` module. + + :param str url: URL to parse into a :class:`.Url` namedtuple. + + Partly backwards-compatible with :mod:`urllib.parse`. + + Example: + + .. code-block:: python + + import urllib3 + + print( urllib3.util.parse_url('http://google.com/mail/')) + # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + + print( urllib3.util.parse_url('google.com:80')) + # Url(scheme=None, host='google.com', port=80, path=None, ...) + + print( urllib3.util.parse_url('/foo?bar')) + # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + if not url: + # Empty + return Url() + + source_url = url + if not _SCHEME_RE.search(url): + url = "//" + url + + scheme: str | None + authority: str | None + auth: str | None + host: str | None + port: str | None + port_int: int | None + path: str | None + query: str | None + fragment: str | None + + try: + scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] + normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES + + if scheme: + scheme = scheme.lower() + + if authority: + auth, _, host_port = authority.rpartition("@") + auth = auth or None + host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] + if auth and normalize_uri: + auth = _encode_invalid_chars(auth, _USERINFO_CHARS) + if port == "": + port = None + else: + auth, host, port = None, None, None + + if port is not None: + port_int = int(port) + if not (0 <= port_int <= 65535): + raise LocationParseError(url) + else: + port_int = None + + host = _normalize_host(host, scheme) + + if normalize_uri and path: + path = _remove_path_dot_segments(path) + path = _encode_invalid_chars(path, _PATH_CHARS) + if normalize_uri and query: + query = _encode_invalid_chars(query, _QUERY_CHARS) + if normalize_uri and fragment: + fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) + + except (ValueError, AttributeError) as e: + raise LocationParseError(source_url) from e + + # For the sake of backwards compatibility we put empty + # string values for path if there are any defined values + # beyond the path in the URL. + # TODO: Remove this when we break backwards compatibility. + if not path: + if query is not None or fragment is not None: + path = "" + else: + path = None + + return Url( + scheme=scheme, + auth=auth, + host=host, + port=port_int, + path=path, + query=query, + fragment=fragment, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py new file mode 100644 index 0000000000..35c77e4025 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import typing +from types import TracebackType + + +def to_bytes( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> bytes: + if isinstance(x, bytes): + return x + elif not isinstance(x, str): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.encode(encoding or "utf-8", errors=errors or "strict") + return x.encode() + + +def to_str( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> str: + if isinstance(x, str): + return x + elif not isinstance(x, bytes): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.decode(encoding or "utf-8", errors=errors or "strict") + return x.decode() + + +def reraise( + tp: type[BaseException] | None, + value: BaseException, + tb: TracebackType | None = None, +) -> typing.NoReturn: + try: + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None # type: ignore[assignment] + tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py new file mode 100644 index 0000000000..aeca0c7ad5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import select +import socket +from functools import partial + +__all__ = ["wait_for_read", "wait_for_write"] + + +# How should we wait on sockets? +# +# There are two types of APIs you can use for waiting on sockets: the fancy +# modern stateful APIs like epoll/kqueue, and the older stateless APIs like +# select/poll. The stateful APIs are more efficient when you have a lots of +# sockets to keep track of, because you can set them up once and then use them +# lots of times. But we only ever want to wait on a single socket at a time +# and don't want to keep track of state, so the stateless APIs are actually +# more efficient. So we want to use select() or poll(). +# +# Now, how do we choose between select() and poll()? On traditional Unixes, +# select() has a strange calling convention that makes it slow, or fail +# altogether, for high-numbered file descriptors. The point of poll() is to fix +# that, so on Unixes, we prefer poll(). +# +# On Windows, there is no poll() (or at least Python doesn't provide a wrapper +# for it), but that's OK, because on Windows, select() doesn't have this +# strange calling convention; plain select() works fine. +# +# So: on Windows we use select(), and everywhere else we use poll(). We also +# fall back to select() in case poll() is somehow broken or missing. + + +def select_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + rcheck = [] + wcheck = [] + if read: + rcheck.append(sock) + if write: + wcheck.append(sock) + # When doing a non-blocking connect, most systems signal success by + # marking the socket writable. Windows, though, signals success by marked + # it as "exceptional". We paper over the difference by checking the write + # sockets for both conditions. (The stdlib selectors module does the same + # thing.) + fn = partial(select.select, rcheck, wcheck, wcheck) + rready, wready, xready = fn(timeout) + return bool(rready or wready or xready) + + +def poll_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + mask = 0 + if read: + mask |= select.POLLIN + if write: + mask |= select.POLLOUT + poll_obj = select.poll() + poll_obj.register(sock, mask) + + # For some reason, poll() takes timeout in milliseconds + def do_poll(t: float | None) -> list[tuple[int, int]]: + if t is not None: + t *= 1000 + return poll_obj.poll(t) + + return bool(do_poll(timeout)) + + +def _have_working_poll() -> bool: + # Apparently some systems have a select.poll that fails as soon as you try + # to use it, either due to strange configuration or broken monkeypatching + # from libraries like eventlet/greenlet. + try: + poll_obj = select.poll() + poll_obj.poll(0) + except (AttributeError, OSError): + return False + else: + return True + + +def wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + # We delay choosing which implementation to use until the first time we're + # called. We could do it at import time, but then we might make the wrong + # decision if someone goes wild with monkeypatching select.poll after + # we're imported. + global wait_for_socket + if _have_working_poll(): + wait_for_socket = poll_wait_for_socket + elif hasattr(select, "select"): + wait_for_socket = select_wait_for_socket + return wait_for_socket(sock, read, write, timeout) + + +def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for reading to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, read=True, timeout=timeout) + + +def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for writing to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.lock b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER new file mode 100644 index 0000000000..5c69047b2e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA new file mode 100644 index 0000000000..0d4f101491 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.6.17 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/latest/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD new file mode 100644 index 0000000000..13b0ce7da4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD @@ -0,0 +1,12 @@ +certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 +certifi-2026.6.17.dist-info/RECORD,, +certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL new file mode 100644 index 0000000000..14a883f292 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE new file mode 100644 index 0000000000..62b076cdee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py new file mode 100644 index 0000000000..ed9a74b7a0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py new file mode 100644 index 0000000000..8945b5da85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem new file mode 100644 index 0000000000..1c2dbfeb68 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem @@ -0,0 +1,3863 @@ + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Label: "OISTE Server Root ECC G1" +# Serial: 47819833811561661340092227008453318557 +# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 +# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 +# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Label: "OISTE Server Root RSA G1" +# Serial: 113845518112613905024960613408179309848 +# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 +# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e +# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Label: "e-Szigno TLS Root CA 2023" +# Serial: 71934828665710877219916191754 +# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 +# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe +# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py new file mode 100644 index 0000000000..1c9661cc7c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/index.py new file mode 100644 index 0000000000..4c59de9cb4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/index.py @@ -0,0 +1,26 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={ + "data_collection": { + "http_headers": { + "request": { + "mode": "denylist", + # Custom terms deny otherwise non-sensitive headers on top + # of the built-in sensitive denylist. + "terms": ["x-forwarded", "user-agent"], + } + } + } + }, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py new file mode 100644 index 0000000000..8ce8d739c9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py @@ -0,0 +1,70 @@ +from sentry_sdk import metrics, profiler + +from sentry_sdk.scope import Scope # isort: skip +from sentry_sdk.client import Client # isort: skip +from sentry_sdk.consts import VERSION +from sentry_sdk.transport import HttpTransport, Transport + +from sentry_sdk.api import * # noqa # isort: skip + +__all__ = [ # noqa + "Hub", + "Scope", + "Client", + "Transport", + "HttpTransport", + "VERSION", + "integrations", + # From sentry_sdk.api + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "logger", + "metrics", + "profiler", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + +# Initialize the debug support after everything is loaded +from sentry_sdk.debug import init_debug_support + +init_debug_support() +del init_debug_support + +# circular imports +from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py new file mode 100644 index 0000000000..565fac2a2d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py @@ -0,0 +1,184 @@ +import os +import random +import threading +import weakref +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Generic, TypeVar + +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + +T = TypeVar("T") + + +class Batcher(Generic[T]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "" + CONTENT_TYPE = "" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + self._buffer: "list[T]" = [] + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._flush_event: "threading.Event" = threading.Event() + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._buffer = [] + self._running = True + self._lock = threading.Lock() + self._active = threading.local() + self._flush_event = threading.Event() + self._flusher = None + self._flusher_pid = None + + def _ensure_thread(self) -> bool: + """For forking processes we might need to restart this thread. + This ensures that our process actually has that thread running. + """ + if not self._running: + return False + + pid = os.getpid() + if self._flusher_pid == pid: + return True + + with self._lock: + # Recheck to make sure another thread didn't get here and start the + # the flusher in the meantime + if self._flusher_pid == pid: + return True + + self._flusher_pid = pid + + self._flusher = threading.Thread(target=self._flush_loop) + self._flusher.daemon = True + + try: + self._flusher.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return False + + return True + + def _flush_loop(self) -> None: + # Mark the flush-loop thread as active for its entire lifetime so + # that any re-entrant add() triggered by GC warnings during wait(), + # flush(), or Event operations is silently dropped instead of + # deadlocking on internal locks. + self._active.flag = True + while self._running: + self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) + self._flush_event.clear() + self._flush() + + def add(self, item: "T") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + if len(self._buffer) >= self.MAX_BEFORE_DROP: + self._record_lost(item) + return None + + self._buffer.append(item) + if len(self._buffer) >= self.MAX_BEFORE_FLUSH: + self._flush_event.set() + finally: + self._active.flag = False + + def kill(self) -> None: + if self._flusher is None: + return + + self._running = False + self._flush_event.set() + self._flusher = None + + def flush(self) -> None: + was_active = getattr(self._active, "flag", False) + self._active.flag = True + try: + self._flush() + finally: + self._active.flag = was_active + + def _add_to_envelope(self, envelope: "Envelope") -> None: + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": len(self._buffer), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(item) for item in self._buffer + ], + } + ), + ) + ) + + def _flush(self) -> "Optional[Envelope]": + envelope = Envelope( + headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + ) + with self._lock: + if len(self._buffer) == 0: + return None + + self._add_to_envelope(envelope) + self._buffer.clear() + + self._capture_func(envelope) + return envelope + + def _record_lost(self, item: "T") -> None: + pass + + @staticmethod + def _to_transport_format(item: "T") -> "Any": + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py new file mode 100644 index 0000000000..f62175c09f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py @@ -0,0 +1,92 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, TypeVar + + T = TypeVar("T") + + +PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 +PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 +PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 +PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 + + +def with_metaclass(meta: "Any", *bases: "Any") -> "Any": + class MetaClass(type): + def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": + return meta(name, bases, d) + + return type.__new__(MetaClass, "temporary_class", (), {}) + + +def check_uwsgi_thread_support() -> bool: + # We check two things here: + # + # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if + # that's the case. + # + # 2. Additionally, if uWSGI is running in preforking mode (default), it needs + # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This + # is because any background threads spawned before the main process is + # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if + # --enable-threads is on. One has to explicitly provide + # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython + # after-fork hooks that take care of cleaning up stale thread data. + try: + from uwsgi import opt # type: ignore + except ImportError: + return True + + from sentry_sdk.consts import FALSE_VALUES + + def enabled(option: str) -> bool: + value = opt.get(option, False) + if isinstance(value, bool): + return value + + if isinstance(value, bytes): + try: + value = value.decode() + except Exception: + pass + + return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] + + # When `threads` is passed in as a uwsgi option, + # `enable-threads` is implied on. + threads_enabled = "threads" in opt or enabled("enable-threads") + fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") + lazy_mode = enabled("lazy-apps") or enabled("lazy") + + if lazy_mode and not threads_enabled: + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI without thread support. " + "This might lead to unexpected issues. " + 'Please run uWSGI with "--enable-threads" for full support.' + ) + ) + + return False + + elif not lazy_mode and (not threads_enabled or not fork_hooks_on): + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI in preforking mode without " + "thread support. This might lead to crashing workers. " + 'Please run uWSGI with both "--enable-threads" and ' + '"--py-call-uwsgi-fork-hooks" for full support.' + ) + ) + + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py new file mode 100644 index 0000000000..923fcf6df8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py @@ -0,0 +1,78 @@ +import warnings +from typing import TYPE_CHECKING + +import sentry_sdk + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional + + import sentry_sdk.consts + + +class _InitGuard: + _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( + "Using the return value of sentry_sdk.init as a context manager " + "and manually calling the __enter__ and __exit__ methods on the " + "return value are deprecated. We are no longer maintaining this " + "functionality, and we will remove it in the next major release." + ) + + def __init__(self, client: "sentry_sdk.Client") -> None: + self._client = client + + def __enter__(self) -> "_InitGuard": + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + c = self._client + if c is not None: + c.close() + + +def _check_python_deprecations() -> None: + # Since we're likely to deprecate Python versions in the future, I'm keeping + # this handy function around. Use this to detect the Python version used and + # to output logger.warning()s if it's deprecated. + pass + + +def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": + """Initializes the SDK and optionally integrations. + + This takes the same arguments as the client constructor. + """ + client = sentry_sdk.Client(*args, **kwargs) + sentry_sdk.get_global_scope().set_client(client) + _check_python_deprecations() + rv = _InitGuard(client) + return rv + + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `init` is a type to + # have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `ContextManager[Any]` to tell static analyzers about the return type. + + class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 + pass + +else: + # Alias `init` for actual usage. Go through the lambda indirection to throw + # PyCharm off of the weakly typed signature (it would otherwise discover + # both the weakly typed signature of `_init` and our faked `init` type). + + init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py new file mode 100644 index 0000000000..0719932ee9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Item, PayloadRef +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Log + + +class LogBatcher(Batcher["Log"]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "log" + CONTENT_TYPE = "application/vnd.sentry.items.log+json" + + @staticmethod + def _to_transport_format(item: "Log") -> "Any": + if "sentry.severity_number" not in item["attributes"]: + item["attributes"]["sentry.severity_number"] = item["severity_number"] + if "sentry.severity_text" not in item["attributes"]: + item["attributes"]["sentry.severity_text"] = item["severity_text"] + + res = { + "timestamp": int(item["time_unix_nano"]) / 1.0e9, + "level": str(item["severity_text"]), + "body": str(item["body"]), + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + return res + + def _record_lost(self, item: "Log") -> None: + # Construct log envelope item without sending it to report lost bytes + log_item = Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": 1, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [self._to_transport_format(item)], + } + ), + ) + + self._record_lost_func( + reason="queue_overflow", + data_category="log_item", + item=log_item, + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py new file mode 100644 index 0000000000..16c238bcab --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py @@ -0,0 +1,43 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +_SENTINEL = object() + + +class LRUCache: + def __init__(self, max_size: int) -> None: + if max_size <= 0: + raise AssertionError(f"invalid max_size: {max_size}") + self.max_size = max_size + self._data: "dict[Any, Any]" = {} + self.hits = self.misses = 0 + self.full = False + + def set(self, key: "Any", value: "Any") -> None: + current = self._data.pop(key, _SENTINEL) + if current is not _SENTINEL: + self._data[key] = value + elif self.full: + self._data.pop(next(iter(self._data))) + self._data[key] = value + else: + self._data[key] = value + self.full = len(self._data) >= self.max_size + + def get(self, key: "Any", default: "Any" = None) -> "Any": + try: + ret = self._data.pop(key) + except KeyError: + self.misses += 1 + ret = default + else: + self.hits += 1 + self._data[key] = ret + + return ret + + def get_all(self) -> "list[tuple[Any, Any]]": + return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py new file mode 100644 index 0000000000..06bce1a282 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Metric + + +class MetricsBatcher(Batcher["Metric"]): + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 10_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "trace_metric" + CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" + + @staticmethod + def _to_transport_format(item: "Metric") -> "Any": + res = { + "timestamp": item["timestamp"], + "name": item["name"], + "type": item["type"], + "value": item["value"], + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + if item.get("unit") is not None: + res["unit"] = item["unit"] + + return res + + def _record_lost(self, item: "Metric") -> None: + self._record_lost_func( + reason="queue_overflow", + data_category="trace_metric", + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py new file mode 100644 index 0000000000..c28c8de9ac --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py @@ -0,0 +1,287 @@ +""" +A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') +with Lock swapped out for RLock to avoid a deadlock while garbage collecting. + +https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py + + +See also +https://codewithoutrules.com/2017/08/16/concurrency-python/ +https://bugs.python.org/issue14976 +https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 + +We also vendor the code to evade eventlet's broken monkeypatching, see +https://github.com/getsentry/sentry-python/pull/484 + + +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; + +All Rights Reserved + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + +""" + +import threading +from collections import deque +from time import time +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +__all__ = ["EmptyError", "FullError", "Queue"] + + +class EmptyError(Exception): + "Exception raised by Queue.get(block=0)/get_nowait()." + + pass + + +class FullError(Exception): + "Exception raised by Queue.put(block=0)/put_nowait()." + + pass + + +class Queue: + """Create a queue object with a given maximum size. + + If maxsize is <= 0, the queue size is infinite. + """ + + def __init__(self, maxsize=0): + self.maxsize = maxsize + self._init(maxsize) + + # mutex must be held whenever the queue is mutating. All methods + # that acquire mutex must release it before returning. mutex + # is shared between the three conditions, so acquiring and + # releasing the conditions also acquires and releases mutex. + self.mutex = threading.RLock() + + # Notify not_empty whenever an item is added to the queue; a + # thread waiting to get is notified then. + self.not_empty = threading.Condition(self.mutex) + + # Notify not_full whenever an item is removed from the queue; + # a thread waiting to put is notified then. + self.not_full = threading.Condition(self.mutex) + + # Notify all_tasks_done whenever the number of unfinished tasks + # drops to zero; thread waiting to join() is notified to resume + self.all_tasks_done = threading.Condition(self.mutex) + self.unfinished_tasks = 0 + + def task_done(self): + """Indicate that a formerly enqueued task is complete. + + Used by Queue consumer threads. For each get() used to fetch a task, + a subsequent call to task_done() tells the queue that the processing + on the task is complete. + + If a join() is currently blocking, it will resume when all items + have been processed (meaning that a task_done() call was received + for every item that had been put() into the queue). + + Raises a ValueError if called more times than there were items + placed in the queue. + """ + with self.all_tasks_done: + unfinished = self.unfinished_tasks - 1 + if unfinished <= 0: + if unfinished < 0: + raise ValueError("task_done() called too many times") + self.all_tasks_done.notify_all() + self.unfinished_tasks = unfinished + + def join(self): + """Blocks until all items in the Queue have been gotten and processed. + + The count of unfinished tasks goes up whenever an item is added to the + queue. The count goes down whenever a consumer thread calls task_done() + to indicate the item was retrieved and all work on it is complete. + + When the count of unfinished tasks drops to zero, join() unblocks. + """ + with self.all_tasks_done: + while self.unfinished_tasks: + self.all_tasks_done.wait() + + def qsize(self): + """Return the approximate size of the queue (not reliable!).""" + with self.mutex: + return self._qsize() + + def empty(self): + """Return True if the queue is empty, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() == 0 + as a direct substitute, but be aware that either approach risks a race + condition where a queue can grow before the result of empty() or + qsize() can be used. + + To create code that needs to wait for all queued tasks to be + completed, the preferred technique is to use the join() method. + """ + with self.mutex: + return not self._qsize() + + def full(self): + """Return True if the queue is full, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() >= n + as a direct substitute, but be aware that either approach risks a race + condition where a queue can shrink before the result of full() or + qsize() can be used. + """ + with self.mutex: + return 0 < self.maxsize <= self._qsize() + + def put(self, item, block=True, timeout=None): + """Put an item into the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until a free slot is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the FullError exception if no free slot was available within that time. + Otherwise ('block' is false), put an item on the queue if a free slot + is immediately available, else raise the FullError exception ('timeout' + is ignored in that case). + """ + with self.not_full: + if self.maxsize > 0: + if not block: + if self._qsize() >= self.maxsize: + raise FullError() + elif timeout is None: + while self._qsize() >= self.maxsize: + self.not_full.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while self._qsize() >= self.maxsize: + remaining = endtime - time() + if remaining <= 0.0: + raise FullError() + self.not_full.wait(remaining) + self._put(item) + self.unfinished_tasks += 1 + self.not_empty.notify() + + def get(self, block=True, timeout=None): + """Remove and return an item from the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until an item is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the EmptyError exception if no item was available within that time. + Otherwise ('block' is false), return an item if one is immediately + available, else raise the EmptyError exception ('timeout' is ignored + in that case). + """ + with self.not_empty: + if not block: + if not self._qsize(): + raise EmptyError() + elif timeout is None: + while not self._qsize(): + self.not_empty.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while not self._qsize(): + remaining = endtime - time() + if remaining <= 0.0: + raise EmptyError() + self.not_empty.wait(remaining) + item = self._get() + self.not_full.notify() + return item + + def put_nowait(self, item): + """Put an item into the queue without blocking. + + Only enqueue the item if a free slot is immediately available. + Otherwise raise the FullError exception. + """ + return self.put(item, block=False) + + def get_nowait(self): + """Remove and return an item from the queue without blocking. + + Only get an item if one is immediately available. Otherwise + raise the EmptyError exception. + """ + return self.get(block=False) + + # Override these methods to implement other queue organizations + # (e.g. stack or priority queue). + # These will only be called with appropriate locks held + + # Initialize the queue representation + def _init(self, maxsize): + self.queue: "Any" = deque() + + def _qsize(self): + return len(self.queue) + + # Put a new item in the queue + def _put(self, item): + self.queue.append(item) + + # Get an item from the queue + def _get(self): + return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py new file mode 100644 index 0000000000..79285c3386 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py @@ -0,0 +1,234 @@ +import os +import random +import threading +import time +import weakref +from collections import defaultdict +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp, serialize_attribute + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + from sentry_sdk._types import SpanJSON + + +class SpanBatcher(Batcher["SpanJSON"]): + # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is + # a bit of a buffer for spans that appear between the trigger to flush + # and actually flushing the buffer. + # + # The max limits are all per trace (per bucket). + MAX_ENVELOPE_SIZE = 1000 # spans + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 2000 + MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB + + FLUSH_WAIT_TIME = 5.0 + + TYPE = "span" + CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + # Spans from different traces cannot be emitted in the same envelope + # since the envelope contains a shared trace header. That's why we bucket + # by trace_id, so that we can then send the buckets each in its own + # envelope. + # trace_id -> span buffer + self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) + self._running_size: dict[str, int] = defaultdict(lambda: 0) + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._last_full_flush: float = time.monotonic() # drives time-based flushes + self._flush_event = threading.Event() + self._pending_flush: set[str] = set() # buckets to be flushed + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._span_buffer = defaultdict(list) + self._running_size = defaultdict(lambda: 0) + self._running = True + + self._lock = threading.Lock() + self._active = threading.local() + + self._last_full_flush = time.monotonic() + self._flush_event = threading.Event() + self._pending_flush = set() + + self._flusher = None + self._flusher_pid = None + + def _flush_loop(self) -> None: + self._active.flag = True + while self._running: + jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 + self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) + self._flush_event.clear() + + self._flush(only_pending=True) + + if ( + time.monotonic() - self._last_full_flush + >= self.FLUSH_WAIT_TIME + jitter + ): + self._flush() + self._last_full_flush = time.monotonic() + + def add(self, span: "SpanJSON") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + size = len(self._span_buffer[span["trace_id"]]) + if size >= self.MAX_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="span", + quantity=1, + ) + return None + + self._span_buffer[span["trace_id"]].append(span) + self._running_size[span["trace_id"]] += self._estimate_size(span) + + if ( + size + 1 >= self.MAX_BEFORE_FLUSH + or self._running_size[span["trace_id"]] + >= self.MAX_BYTES_BEFORE_FLUSH + ): + self._pending_flush.add(span["trace_id"]) + notify = True + else: + notify = False + + if notify: + self._flush_event.set() + finally: + self._active.flag = False + + @staticmethod + def _estimate_size(item: "SpanJSON") -> int: + # Rough estimate of serialized span size that's quick to compute. + # 210 is the rough size of the payload without attributes, and then we + # estimate the attributes separately. + estimate = 210 + for value in (item.get("attributes") or {}).values(): + estimate += 50 + + if isinstance(value, str): + estimate += len(value) + else: + estimate += len(str(value)) + + return estimate + + @staticmethod + def _to_transport_format(item: "SpanJSON") -> "Any": + res = {k: v for k, v in item.items() if k not in ("_segment_span",)} + + if item.get("attributes"): + res["attributes"] = { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + } + else: + del res["attributes"] + + return res + + def _flush(self, only_pending: bool = False) -> None: + with self._lock: + if only_pending: + buckets = list(self._pending_flush) + else: + # flush whole buffer, e.g. if the SDK is shutting down + buckets = list(self._span_buffer.keys()) + + self._pending_flush.clear() + + if not buckets: + return + + envelopes = [] + + for bucket_id in buckets: + spans = self._span_buffer.get(bucket_id) + if not spans: + continue + + dsc = spans[0]["_segment_span"]._dynamic_sampling_context() + + # Max per envelope is 1000, so if we happen to have more than + # 1000 spans in one bucket, we'll need to separate them. + for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): + end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) + + envelope = Envelope( + headers={ + "sent_at": format_timestamp(datetime.now(timezone.utc)), + "trace": dsc, + } + ) + + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": end - start, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(spans[j]) + for j in range(start, end) + ], + } + ), + ) + ) + + envelopes.append(envelope) + + del self._span_buffer[bucket_id] + del self._running_size[bucket_id] + + for envelope in envelopes: + self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py new file mode 100644 index 0000000000..fbd2578048 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py @@ -0,0 +1,481 @@ +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING, TypeVar, Union + +# Re-exported for compat, since code out there in the wild might use this variable. +MYPY = TYPE_CHECKING + + +SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" +BLOB_DATA_SUBSTITUTE = "[Blob substitute]" +OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" +UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" + + +class AnnotatedValue: + """ + Meta information for a data field in the event payload. + This is to tell Relay that we have tampered with the fields value. + See: + https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 + """ + + __slots__ = ("value", "metadata") + + def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: + self.value = value + self.metadata = metadata + + def __eq__(self, other: "Any") -> bool: + if not isinstance(other, AnnotatedValue): + return False + + return self.value == other.value and self.metadata == other.metadata + + def __str__(self: "AnnotatedValue") -> str: + return str({"value": str(self.value), "metadata": str(self.metadata)}) + + def __len__(self: "AnnotatedValue") -> int: + if self.value is not None: + return len(self.value) + else: + return 0 + + @classmethod + def removed_because_raw_data(cls) -> "AnnotatedValue": + """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" + # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition + # to span-first + return AnnotatedValue( + value="", + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_raw_data(cls) -> "AnnotatedValue": + """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" + return AnnotatedValue( + value=UNPARSABLE_RAW_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": + """ + The actual value was removed because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition + # to span-first + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_over_size_limit( + cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE + ) -> "AnnotatedValue": + """ + The actual value was replaced because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": + """The actual value was removed because it contained sensitive information.""" + return AnnotatedValue( + value=SENSITIVE_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) + "s", # The fields original value was substituted + ] + ] + }, + ) + + +T = TypeVar("T") +Annotated = Union[AnnotatedValue, T] + + +if TYPE_CHECKING: + from collections.abc import Container, MutableMapping, Sequence + from datetime import datetime + from types import TracebackType + from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + + class SDKInfo(TypedDict): + name: str + version: str + packages: "Sequence[Mapping[str, str]]" + + class KeyValueCollectionBehaviour(TypedDict): + mode: 'Literal["off", "denylist", "allowlist"]' + 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): + request: "KeyValueCollectionBehaviour" + + class HttpHeadersCollectionBehaviour(TypedDict): + request: "KeyValueCollectionBehaviour" + + class DataCollectionUserOptions(TypedDict, total=False): + user_info: bool + cookies: "KeyValueCollectionBehaviour" + http_headers: "HttpHeadersCollectionUserOptions" + http_bodies: "List[str]" + query_params: "KeyValueCollectionBehaviour" + graphql: "GraphQLCollectionUserOptions" + gen_ai: "GenAICollectionUserOptions" + database_query_data: bool + queues: bool + 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_query_data: bool + queues: bool + 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"] + + DurationUnit = Literal[ + "nanosecond", + "microsecond", + "millisecond", + "second", + "minute", + "hour", + "day", + "week", + ] + + InformationUnit = Literal[ + "bit", + "byte", + "kilobyte", + "kibibyte", + "megabyte", + "mebibyte", + "gigabyte", + "gibibyte", + "terabyte", + "tebibyte", + "petabyte", + "pebibyte", + "exabyte", + "exbibyte", + ] + + FractionUnit = Literal["ratio", "percent"] + MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] + + MeasurementValue = TypedDict( + "MeasurementValue", + { + "value": float, + "unit": NotRequired[Optional[MeasurementUnit]], + }, + ) + + Event = TypedDict( + "Event", + { + "breadcrumbs": Annotated[ + dict[Literal["values"], list[dict[str, Any]]] + ], # TODO: We can expand on this type + "check_in_id": str, + "contexts": dict[str, dict[str, object]], + "dist": str, + "duration": Optional[float], + "environment": Optional[str], + "errors": list[dict[str, Any]], # TODO: We can expand on this type + "event_id": str, + "exception": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "extra": MutableMapping[str, object], + "fingerprint": list[str], + "level": LogLevelStr, + "logentry": Mapping[str, object], + "logger": str, + "measurements": dict[str, MeasurementValue], + "message": str, + "modules": dict[str, str], + "monitor_config": Mapping[str, object], + "monitor_slug": Optional[str], + "platform": Literal["python"], + "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports + "release": Optional[str], + "request": dict[str, object], + "sdk": Mapping[str, object], + "server_name": str, + "spans": Annotated[list[dict[str, object]]], + "stacktrace": dict[ + str, object + ], # We access this key in the code, but I am unsure whether we ever set it + "start_timestamp": datetime, + "status": Optional[str], + "tags": MutableMapping[ + str, str + ], # Tags must be less than 200 characters each + "threads": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "timestamp": Optional[datetime], # Must be set before sending the event + "transaction": str, + "transaction_info": Mapping[str, Any], # TODO: We can expand on this type + "type": Literal["check_in", "transaction"], + "user": dict[str, object], + "_dropped_spans": int, + "_has_gen_ai_span": bool, + }, + total=False, + ) + + ExcInfo = Union[ + tuple[Type[BaseException], BaseException, Optional[TracebackType]], + tuple[None, None, None], + ] + + # TODO: Make a proper type definition for this (PRs welcome!) + Hint = Dict[str, Any] + + AttributeValue = ( + str + | bool + | float + | int + | list[str] + | list[bool] + | list[float] + | list[int] + | tuple[str, ...] + | tuple[bool, ...] + | tuple[float, ...] + | tuple[int, ...] + ) + Attributes = dict[str, AttributeValue] + + SerializedAttributeValue = TypedDict( + # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types + "SerializedAttributeValue", + { + "type": Literal[ + "string", + "boolean", + "double", + "integer", + "array", + ], + "value": AttributeValue, + }, + ) + + Log = TypedDict( + "Log", + { + "severity_text": str, + "severity_number": int, + "body": str, + "attributes": Attributes, + "time_unix_nano": int, + "trace_id": Optional[str], + "span_id": Optional[str], + }, + ) + + MetricType = Literal["counter", "gauge", "distribution"] + MetricUnit = Union[DurationUnit, InformationUnit, str] + + Metric = TypedDict( + "Metric", + { + "timestamp": float, + "trace_id": Optional[str], + "span_id": Optional[str], + "name": str, + "type": MetricType, + "value": float, + "unit": Optional[MetricUnit], + "attributes": Attributes, + }, + ) + + MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] + + SpanJSON = TypedDict( + "SpanJSON", + { + "trace_id": str, + "span_id": str, + "parent_span_id": NotRequired[str], + "name": str, + "status": str, + "is_segment": bool, + "start_timestamp": float, + "end_timestamp": NotRequired[float], + "attributes": NotRequired[Attributes], + "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], + }, + ) + + # TODO: Make a proper type definition for this (PRs welcome!) + Breadcrumb = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + BreadcrumbHint = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + SamplingContext = Dict[str, Any] + + EventProcessor = Callable[[Event, Hint], Optional[Event]] + ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] + BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] + TransactionProcessor = Callable[[Event, Hint], Optional[Event]] + LogProcessor = Callable[[Log, Hint], Optional[Log]] + + TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] + + # https://github.com/python/mypy/issues/5710 + NotImplementedType = Any + + EventDataCategory = Literal[ + "default", + "error", + "crash", + "transaction", + "security", + "attachment", + "session", + "internal", + "profile", + "profile_chunk", + "monitor", + "span", + "log_item", + "log_byte", + "trace_metric", + ] + SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] + + ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] + ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] + + MonitorConfigScheduleType = Literal["crontab", "interval"] + MonitorConfigScheduleUnit = Literal[ + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", # not supported in Sentry and will result in a warning + ] + + MonitorConfigSchedule = TypedDict( + "MonitorConfigSchedule", + { + "type": MonitorConfigScheduleType, + "value": Union[int, str], + "unit": MonitorConfigScheduleUnit, + }, + total=False, + ) + + MonitorConfig = TypedDict( + "MonitorConfig", + { + "schedule": MonitorConfigSchedule, + "timezone": str, + "checkin_margin": int, + "max_runtime": int, + "failure_issue_threshold": int, + "recovery_threshold": int, + "owner": str, + }, + total=False, + ) + + HttpStatusCodeRange = Union[int, Container[int]] + + class TextPart(TypedDict): + type: Literal["text"] + content: str + + IgnoreSpansName = Union[str, Pattern[str]] + IgnoreSpansContext = TypedDict( + "IgnoreSpansContext", + {"name": IgnoreSpansName, "attributes": Attributes}, + total=False, + ) + IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py new file mode 100644 index 0000000000..98f932267f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py @@ -0,0 +1,100 @@ +""" +Copyright (c) 2007 by the Pallets team. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Dict, Iterator, Optional, Tuple + + +# +# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` +# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 +# +# We need this function because Django does not give us a "pure" http header +# dict. So we might as well use it for all WSGI integrations. +# +def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns only proper HTTP headers. + """ + for key, value in environ.items(): + key = str(key) + if key.startswith("HTTP_") and key not in ( + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + ): + yield key[5:].replace("_", "-").title(), value + elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): + yield key.replace("_", "-").title(), value + + +def _strip_default_port(host: str, scheme: "Optional[str]") -> str: + """Strip the port from the host if it's the default for the scheme.""" + if scheme == "http" and host.endswith(":80"): + return host[:-3] + if scheme == "https" and host.endswith(":443"): + return host[:-4] + return host + + +# `get_host` comes from `werkzeug.wsgi.get_host` +# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 + + +def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: + """ + Return the host for the given WSGI environment. + """ + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: + return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) + elif environ.get("HTTP_HOST"): + return _strip_default_port(environ["HTTP_HOST"], scheme) + elif environ.get("SERVER_NAME"): + # SERVER_NAME/SERVER_PORT describe the internal server, so use + # wsgi.url_scheme (not the forwarded scheme) for port decisions. + rv = environ["SERVER_NAME"] + if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( + ("https", "443"), + ("http", "80"), + ): + rv += ":" + environ["SERVER_PORT"] + return rv + else: + # In spite of the WSGI spec, SERVER_NAME might not be present. + return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py new file mode 100644 index 0000000000..404e57ff1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py @@ -0,0 +1,8 @@ +from .utils import ( + GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 + GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 + normalize_message_role, # noqa: F401 + normalize_message_roles, # noqa: F401 + set_conversation_id, # noqa: F401 + set_data_normalized, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py new file mode 100644 index 0000000000..b5eb8c55ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py @@ -0,0 +1,66 @@ +from collections.abc import Iterable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.chat import ( + ChatCompletionContentPartParam, + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ) + + from sentry_sdk._types import TextPart + + +def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: + return isinstance(message, dict) and message.get("role") == "system" + + +def _get_system_instructions( + messages: "Iterable[ChatCompletionMessageParam]", +) -> "list[ChatCompletionMessageParam]": + if not isinstance(messages, Iterable): + return [] + + return [message for message in messages if _is_system_instruction(message)] + + +def _get_text_items( + content: "Union[str, Iterable[ChatCompletionContentPartParam]]", +) -> "list[str]": + if isinstance(content, str): + return [content] + + if not isinstance(content, Iterable): + return [] + + text_items = [] + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text", None) + if text is not None: + text_items.append(text) + + return text_items + + +def _transform_system_instructions( + system_instructions: "list[ChatCompletionSystemMessageParam]", +) -> "list[TextPart]": + instruction_text_parts: "list[TextPart]" = [] + + for instruction in system_instructions: + if not isinstance(instruction, dict): + continue + + content = instruction.get("content") + if content is None: + continue + + text_parts: "list[TextPart]" = [ + {"type": "text", "content": text} for text in _get_text_items(content) + ] + instruction_text_parts += text_parts + + return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py new file mode 100644 index 0000000000..8f751c3248 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.responses import ResponseInputItemParam, ResponseInputParam + + +def _is_system_instruction(message: "ResponseInputItemParam") -> bool: + if not isinstance(message, dict) or not message.get("role") == "system": + return False + + return "type" not in message or message["type"] == "message" + + +def _get_system_instructions( + messages: "Union[str, ResponseInputParam]", +) -> "list[ResponseInputItemParam]": + if not isinstance(messages, list): + return [] + + return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py new file mode 100644 index 0000000000..35ee4bd788 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py @@ -0,0 +1,6 @@ +import re + +# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." +DATA_URL_BASE64_REGEX = re.compile( + r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py new file mode 100644 index 0000000000..d8840ad451 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py @@ -0,0 +1,147 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk.utils +from sentry_sdk import start_span +from sentry_sdk.ai.utils import _set_span_data_attribute +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, TypeVar, Union + + F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) + +_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) + + +def set_ai_pipeline_name(name: "Optional[str]") -> None: + _ai_pipeline_name.set(name) + + +def get_ai_pipeline_name() -> "Optional[str]": + return _ai_pipeline_name.get() + + +def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": + def decorator(f: "F") -> "F": + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return await f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = await f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + if inspect.iscoroutinefunction(f): + return wraps(f)(async_wrapped) # type: ignore + else: + return wraps(f)(sync_wrapped) # type: ignore + + return decorator + + +def record_token_usage( + span: "Union[Span, StreamedSpan]", + input_tokens: "Optional[int]" = None, + input_tokens_cached: "Optional[int]" = None, + input_tokens_cache_write: "Optional[int]" = None, + output_tokens: "Optional[int]" = None, + output_tokens_reasoning: "Optional[int]" = None, + total_tokens: "Optional[int]" = None, +) -> None: + # TODO: move pipeline name elsewhere + ai_pipeline_name = get_ai_pipeline_name() + if ai_pipeline_name: + _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) + + if input_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if input_tokens_cached is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + input_tokens_cached, + ) + + if input_tokens_cache_write is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + input_tokens_cache_write, + ) + + if output_tokens is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + ) + + if output_tokens_reasoning is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + output_tokens_reasoning, + ) + + if total_tokens is None and input_tokens is not None and output_tokens is not None: + total_tokens = input_tokens + output_tokens + + if total_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py new file mode 100644 index 0000000000..7b1ca9324b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py @@ -0,0 +1,782 @@ +import inspect +import json +from copy import deepcopy +from typing import TYPE_CHECKING + +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import logger + +MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB +# Maximum characters when only a single message is left after bytes truncation +MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 + + +class GEN_AI_ALLOWED_MESSAGE_ROLES: + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { + GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], + GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], + GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], + GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], +} + +GEN_AI_MESSAGE_ROLE_MAPPING = {} +for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): + for source_role in source_roles: + GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role + + +def parse_data_uri(url: str) -> "Tuple[str, str]": + """ + Parse a data URI and return (mime_type, content). + + Data URI format (RFC 2397): data:[][;base64], + + Examples: + data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") + data:text/plain,Hello → ("text/plain", "Hello") + data:;base64,SGVsbG8= → ("", "SGVsbG8=") + + Raises: + ValueError: If the URL is not a valid data URI (missing comma separator) + """ + if "," not in url: + raise ValueError("Invalid data URI: missing comma separator") + + header, content = url.split(",", 1) + + # Extract mime type from header + # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" + # Remove "data:" prefix, then take everything before the first semicolon + if header.startswith("data:"): + mime_part = header[5:] # Remove "data:" prefix + else: + mime_part = header + + mime_type = mime_part.split(";")[0] + + return mime_type, content + + +def get_modality_from_mime_type(mime_type: str) -> str: + """ + Infer the content modality from a MIME type string. + + Args: + mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") + + Returns: + One of: "image", "audio", "video", or "document" + Defaults to "image" for unknown or empty MIME types. + + Examples: + "image/jpeg" -> "image" + "audio/mp3" -> "audio" + "video/mp4" -> "video" + "application/pdf" -> "document" + "text/plain" -> "document" + """ + if not mime_type: + return "image" # Default fallback + + mime_lower = mime_type.lower() + if mime_lower.startswith("image/"): + return "image" + elif mime_lower.startswith("audio/"): + return "audio" + elif mime_lower.startswith("video/"): + return "video" + elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): + return "document" + else: + return "image" # Default fallback for unknown types + + +def transform_openai_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an OpenAI/LiteLLM content part to Sentry's standardized format. + + This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. + + Input format: + - {"type": "image_url", "image_url": {"url": "..."}} + - {"type": "image_url", "image_url": "..."} (string shorthand) + + Output format (one of): + - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from OpenAI/LiteLLM + + Returns: + A transformed dictionary in standardized format, or None if the format + is not OpenAI image_url format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type != "image_url": + return None + + image_url_data = content_part.get("image_url") + if isinstance(image_url_data, str): + url = image_url_data + elif isinstance(image_url_data, dict): + url = image_url_data.get("url", "") + else: + return None + + if not url: + return None + + # Check if it's a data URI (base64 encoded) + if url.startswith("data:"): + try: + mime_type, content = parse_data_uri(url) + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": content, + } + except ValueError: + # If parsing fails, return as URI + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + else: + # Regular URL + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + + +def transform_anthropic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an Anthropic content part to Sentry's standardized format. + + This handles the Anthropic image and document formats with source dictionaries. + + Input format: + - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} + - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} + - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} + - {"type": "document", "source": {...}} (same source formats) + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from Anthropic + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Anthropic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "document") or "source" not in content_part: + return None + + source = content_part.get("source") + if not isinstance(source, dict): + return None + + source_type = source.get("type") + media_type = source.get("media_type", "") + modality = ( + "document" + if block_type == "document" + else get_modality_from_mime_type(media_type) + ) + + if source_type == "base64": + return { + "type": "blob", + "modality": modality, + "mime_type": media_type, + "content": source.get("data", ""), + } + elif source_type == "url": + return { + "type": "uri", + "modality": modality, + "mime_type": media_type, + "uri": source.get("url", ""), + } + elif source_type == "file": + return { + "type": "file", + "modality": modality, + "mime_type": media_type, + "file_id": source.get("file_id", ""), + } + + return None + + +def transform_google_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a Google GenAI content part to Sentry's standardized format. + + This handles the Google GenAI inline_data and file_data formats. + + Input format: + - {"inline_data": {"mime_type": "...", "data": "..."}} + - {"file_data": {"mime_type": "...", "file_uri": "..."}} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from Google GenAI + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Google format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Handle Google inline_data format + if "inline_data" in content_part: + inline_data = content_part.get("inline_data") + if isinstance(inline_data, dict): + mime_type = inline_data.get("mime_type", "") + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": inline_data.get("data", ""), + } + return None + + # Handle Google file_data format + if "file_data" in content_part: + file_data = content_part.get("file_data") + if isinstance(file_data, dict): + mime_type = file_data.get("mime_type", "") + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_data.get("file_uri", ""), + } + return None + + return None + + +def transform_generic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a generic/LangChain-style content part to Sentry's standardized format. + + This handles generic formats where the type indicates the modality and + the data is provided via direct base64, url, or file_id fields. + + Input format: + - {"type": "image", "base64": "...", "mime_type": "..."} + - {"type": "audio", "url": "...", "mime_type": "..."} + - {"type": "video", "base64": "...", "mime_type": "..."} + - {"type": "file", "file_id": "...", "mime_type": "..."} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part in generic format + + Returns: + A transformed dictionary in standardized format, or None if the format + is not generic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "audio", "video", "file"): + return None + + # Ensure it's not Anthropic format (which also uses type: "image") + if "source" in content_part: + return None + + mime_type = content_part.get("mime_type", "") + modality = block_type if block_type != "file" else "document" + + # Check for base64 encoded content + if "base64" in content_part: + return { + "type": "blob", + "modality": modality, + "mime_type": mime_type, + "content": content_part.get("base64", ""), + } + # Check for URL reference + elif "url" in content_part: + return { + "type": "uri", + "modality": modality, + "mime_type": mime_type, + "uri": content_part.get("url", ""), + } + # Check for file_id reference + elif "file_id" in content_part: + return { + "type": "file", + "modality": modality, + "mime_type": mime_type, + "file_id": content_part.get("file_id", ""), + } + + return None + + +def transform_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a content part from various AI SDK formats to Sentry's standardized format. + + This is a heuristic dispatcher that detects the format and delegates to the + appropriate SDK-specific transformer. For direct SDK integration, prefer using + the specific transformers directly: + - transform_openai_content_part() for OpenAI/LiteLLM + - transform_anthropic_content_part() for Anthropic + - transform_google_content_part() for Google GenAI + - transform_generic_content_part() for LangChain and other generic formats + + Detection order: + 1. OpenAI: type == "image_url" + 2. Google: "inline_data" or "file_data" keys present + 3. Anthropic: type in ("image", "document") with "source" key + 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from an AI SDK + + Returns: + A transformed dictionary in standardized format, or None if the format + is unrecognized or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Try OpenAI format first (most common, clear indicator) + result = transform_openai_content_part(content_part) + if result is not None: + return result + + # Try Google format (unique keys make it easy to detect) + result = transform_google_content_part(content_part) + if result is not None: + return result + + # Try Anthropic format (has "source" key) + result = transform_anthropic_content_part(content_part) + if result is not None: + return result + + # Try generic format as fallback + result = transform_generic_content_part(content_part) + if result is not None: + return result + + # Unrecognized format + return None + + +def transform_message_content(content: "Any") -> "Any": + """ + Transform message content, handling both string content and list of content blocks. + + For list content, each item is transformed using transform_content_part(). + Items that cannot be transformed (return None) are kept as-is. + + Args: + content: Message content - can be a string, list of content blocks, or other + + Returns: + - String content: returned as-is + - List content: list with each transformable item converted to standardized format + - Other: returned as-is + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + return transformed + + return content + + +def _normalize_data(data: "Any", unpack: bool = True) -> "Any": + # convert pydantic data (e.g. OpenAI v1+) to json compatible format + if hasattr(data, "model_dump"): + # Check if it's a class (type) rather than an instance + # Model classes can be passed as arguments (e.g., for schema definitions) + if inspect.isclass(data): + return f"" + + try: + return _normalize_data(data.model_dump(), unpack=unpack) + except Exception as e: + logger.warning("Could not convert pydantic data to JSON: %s", e) + return data if isinstance(data, (int, float, bool, str)) else str(data) + + if isinstance(data, list): + if unpack and len(data) == 1: + return _normalize_data(data[0], unpack=unpack) # remove empty dimensions + return list(_normalize_data(x, unpack=unpack) for x in data) + + if isinstance(data, dict): + return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} + + return data if isinstance(data, (int, float, bool, str)) else str(data) + + +def set_data_normalized( + span: "Union[Span, StreamedSpan]", + key: str, + value: "Any", + unpack: bool = True, +) -> None: + normalized = _normalize_data(value, unpack=unpack) + if isinstance(normalized, (int, float, bool, str)): + _set_span_data_attribute(span, key, normalized) + else: + _set_span_data_attribute(span, key, json.dumps(normalized)) + + +def _set_span_data_attribute( + span: "Union[Span, StreamedSpan]", key: str, value: "Any" +) -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(key, value) + else: + span.set_data(key, value) + + +def normalize_message_role(role: str) -> str: + """ + Normalize a message role to one of the 4 allowed gen_ai role values. + Maps "ai" -> "assistant" and keeps other standard roles unchanged. + """ + return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) + + +def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": + """ + Normalize roles in a list of messages to use standard gen_ai role values. + Creates a deep copy to avoid modifying the original messages. + """ + normalized_messages = [] + for message in messages: + if not isinstance(message, dict): + normalized_messages.append(message) + continue + normalized_message = message.copy() + if "role" in message: + normalized_message["role"] = normalize_message_role(message["role"]) + normalized_messages.append(normalized_message) + + return normalized_messages + + +def get_start_span_function() -> "Callable[..., Any]": + current_span = sentry_sdk.get_current_span() + + transaction_exists = ( + current_span is not None and current_span.containing_transaction is not None + ) + return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction + + +def _truncate_single_message_content_if_present( + message: "Dict[str, Any]", max_chars: int +) -> "Dict[str, Any]": + """ + Truncate a message's content to at most `max_chars` characters and append an + ellipsis if truncation occurs. + """ + if not isinstance(message, dict) or "content" not in message: + return message + content = message["content"] + + if isinstance(content, str): + if len(content) <= max_chars: + return message + message["content"] = content[:max_chars] + "..." + return message + + if isinstance(content, list): + remaining = max_chars + for item in content: + if isinstance(item, dict) and "text" in item: + text = item["text"] + if isinstance(text, str): + if len(text) > remaining: + item["text"] = text[:remaining] + "..." + remaining = 0 + else: + remaining -= len(text) + return message + + return message + + +def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: + """ + Find the index of the first message that would exceed the max bytes limit. + Compute the individual message sizes, and return the index of the first message from the back + of the list that would exceed the max bytes limit. + """ + running_sum = 0 + for idx in range(len(messages) - 1, -1, -1): + size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) + running_sum += size + if running_sum > max_bytes: + return idx + 1 + + return 0 + + +def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: + """ + Some content blocks contain an image_url property with base64 content as its value. + This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. + """ + if item.get("type") != "image_url": + return False + + image_url_val = item.get("image_url") + image_url = ( + image_url_val.get("url", "") + if isinstance(image_url_val, dict) + else (image_url_val or "") + ) + data_url_match = DATA_URL_BASE64_REGEX.match(image_url) + + return bool(data_url_match) + + +def redact_blob_message_parts( + messages: "List[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Redact blob message parts from the messages by replacing blob content with "[Filtered]". + + This function creates a deep copy of messages that contain blob content to avoid + mutating the original message dictionaries. Messages without blob content are + returned as-is to minimize copying overhead. + + e.g: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,..." + } + ] + } + becomes: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "[Filtered]" + } + ] + } + """ + + # First pass: check if any message contains blob content + has_blobs = False + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and ( + item.get("type") == "blob" or _is_image_type_with_blob_content(item) + ): + has_blobs = True + break + if has_blobs: + break + + # If no blobs found, return original messages to avoid unnecessary copying + if not has_blobs: + return messages + + # Deep copy messages to avoid mutating the original + messages_copy = deepcopy(messages) + + # Second pass: redact blob content in the copy + for message in messages_copy: + if not isinstance(message, dict): + continue + + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + if item.get("type") == "blob": + item["content"] = BLOB_DATA_SUBSTITUTE + elif _is_image_type_with_blob_content(item): + if isinstance(item["image_url"], dict): + item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE + else: + item["image_url"] = BLOB_DATA_SUBSTITUTE + + return messages_copy + + +def truncate_messages_by_size( + messages: "List[Dict[str, Any]]", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Tuple[List[Dict[str, Any]], int]": + """ + Returns a truncated messages list, consisting of + - the last message, with its content truncated to `max_single_message_chars` characters, + if the last message's size exceeds `max_bytes` bytes; otherwise, + - the maximum number of messages, starting from the end of the `messages` list, whose total + serialized size does not exceed `max_bytes` bytes. + + In the single message case, the serialized message size may exceed `max_bytes`, because + truncation is based only on character count in that case. + """ + serialized_json = json.dumps(messages, separators=(",", ":")) + current_size = len(serialized_json.encode("utf-8")) + + if current_size <= max_bytes: + return messages, 0 + + truncation_index = _find_truncation_index(messages, max_bytes) + if truncation_index < len(messages): + truncated_messages = messages[truncation_index:] + else: + truncation_index = len(messages) - 1 + truncated_messages = messages[-1:] + + if len(truncated_messages) == 1: + truncated_messages[0] = _truncate_single_message_content_if_present( + deepcopy(truncated_messages[0]), max_chars=max_single_message_chars + ) + + return truncated_messages, truncation_index + + +def truncate_and_annotate_messages( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_message = _truncate_single_message_content_if_present( + deepcopy(messages[-1]), max_chars=max_single_message_chars + ) + if len(messages) > 1: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return [truncated_message] + + +def truncate_and_annotate_embedding_inputs( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) + if removed_count > 0: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return truncated_messages + + +def set_conversation_id(conversation_id: str) -> None: + """ + Set the conversation_id in the scope. + """ + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py new file mode 100644 index 0000000000..5556b11ace --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py @@ -0,0 +1,573 @@ +import inspect +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import Client, tracing_utils +from sentry_sdk._init_implementation import init +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.crons import monitor +from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import get_current_span as _get_current_streamed_span +from sentry_sdk.tracing import NoOpSpan, Transaction, trace + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Optional, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + MeasurementUnit, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span, TransactionKwargs + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) +else: + + def overload(x: "T") -> "T": + return x + + +# When changing this, update __all__ in __init__.py too +__all__ = [ + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + + +def scopemethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, + inspect.getdoc(getattr(Scope, f.__name__)), + ) + return f + + +def clientmethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, + inspect.getdoc(getattr(Client, f.__name__)), + ) + return f + + +@scopemethod +def get_client() -> "BaseClient": + return Scope.get_client() + + +def is_initialized() -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether Sentry has been initialized or not. + + If a client is available and the client is active + (meaning it is configured to send data) then + Sentry is initialized. + """ + return get_client().is_active() + + +@scopemethod +def get_global_scope() -> "Scope": + return Scope.get_global_scope() + + +@scopemethod +def get_isolation_scope() -> "Scope": + return Scope.get_isolation_scope() + + +@scopemethod +def get_current_scope() -> "Scope": + return Scope.get_current_scope() + + +@scopemethod +def last_event_id() -> "Optional[str]": + """ + See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding + this method's limitations. + """ + return Scope.last_event_id() + + +@scopemethod +def capture_event( + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) + + +@scopemethod +def capture_message( + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_message( + message, level, scope=scope, **scope_kwargs + ) + + +@scopemethod +def capture_exception( + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) + + +@scopemethod +def add_attachment( + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, +) -> None: + return get_isolation_scope().add_attachment( + bytes, filename, path, content_type, add_to_transactions + ) + + +@scopemethod +def add_breadcrumb( + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", +) -> None: + return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + +@overload +def configure_scope() -> "ContextManager[Scope]": + pass + + +@overload +def configure_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def configure_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + warnings.warn( + "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", + DeprecationWarning, + stacklevel=2, + ) + + scope = get_isolation_scope() + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + +@overload +def push_scope() -> "ContextManager[Scope]": + pass + + +@overload +def push_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def push_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + warnings.warn( + "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", + DeprecationWarning, + stacklevel=2, + ) + + if callback is not None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with push_scope() as scope: + callback(scope) + return None + + return _ScopeManager() + + +@scopemethod +def set_attribute(attribute: str, value: "Any") -> None: + """ + Set an attribute. + + Any attributes-based telemetry (logs, metrics) captured in this scope will + include this attribute. + """ + return get_isolation_scope().set_attribute(attribute, value) + + +@scopemethod +def remove_attribute(attribute: str) -> None: + """ + Remove an attribute. + + If the attribute doesn't exist, this function will not have any effect and + it will also not raise an exception. + """ + return get_isolation_scope().remove_attribute(attribute) + + +@scopemethod +def set_tag(key: str, value: "Any") -> None: + return get_isolation_scope().set_tag(key, value) + + +@scopemethod +def set_tags(tags: "Mapping[str, object]") -> None: + return get_isolation_scope().set_tags(tags) + + +@scopemethod +def set_context(key: str, value: "Dict[str, Any]") -> None: + return get_isolation_scope().set_context(key, value) + + +@scopemethod +def set_extra(key: str, value: "Any") -> None: + return get_isolation_scope().set_extra(key, value) + + +@scopemethod +def set_user(value: "Optional[Dict[str, Any]]") -> None: + return get_isolation_scope().set_user(value) + + +@scopemethod +def set_level(value: "LogLevelStr") -> None: + return get_isolation_scope().set_level(value) + + +@clientmethod +def flush( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return get_client().flush(timeout=timeout, callback=callback) + + +@clientmethod +async def flush_async( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return await get_client().flush_async(timeout=timeout, callback=callback) + + +@scopemethod +def start_span( + **kwargs: "Any", +) -> "Span": + return get_current_scope().start_span(**kwargs) + + +@scopemethod +def start_transaction( + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", +) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction on the current scope. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + return get_current_scope().start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + +def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + transaction = get_current_scope().transaction + if transaction is not None: + transaction.set_measurement(name, value, unit) + + +def get_current_span( + scope: "Optional[Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + return tracing_utils.get_current_span(scope) + + +def get_traceparent() -> "Optional[str]": + """ + Returns the traceparent either from the active span or from the scope. + """ + return get_current_scope().get_traceparent() + + +def get_baggage() -> "Optional[str]": + """ + Returns Baggage either from the active span or from the scope. + """ + baggage = get_current_scope().get_baggage() + if baggage is not None: + return baggage.serialize() + + return None + + +def continue_trace( + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", +) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers, op, name, source, origin + ) + + +@scopemethod +def start_session( + session_mode: str = "application", +) -> None: + return get_isolation_scope().start_session(session_mode=session_mode) + + +@scopemethod +def end_session() -> None: + return get_isolation_scope().end_session() + + +@scopemethod +def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: + return get_current_scope().set_transaction_name(name, source) + + +def update_current_span( + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, + data: "Optional[dict[str, Any]]" = None, +) -> None: + """ + Update the current active span with the provided parameters. + + This function allows you to modify properties of the currently active span. + If no span is currently active, this function will do nothing. + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, the span's operation will + remain unchanged. + :type op: str or None + + :param name: The human-readable name/description for the span. This provides + more specific details about what the span represents (e.g., "GET /api/users", + "SELECT * FROM users"). If not provided, the span's name will remain unchanged. + :type name: str or None + + :param data: A dictionary of key-value pairs to add as data to the span. This + data will be merged with any existing span data. If not provided, + no data will be added. + + .. deprecated:: 2.35.0 + Use ``attributes`` instead. The ``data`` parameter will be removed + in a future version. + :type data: dict[str, Union[str, int, float, bool]] or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes will be merged with any existing span data. If not provided, + no attributes will be added. + :type attributes: dict[str, Union[str, int, float, bool]] or None + + :returns: None + + .. versionadded:: 2.35.0 + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP + + sentry_sdk.update_current_span( + op=OP.FUNCTION, + name="process_user_data", + attributes={"user_id": 123, "batch_size": 50} + ) + """ + if isinstance(_get_current_streamed_span(), StreamedSpan): + warnings.warn( + "The `update_current_span` API isn't available in streaming mode. " + "Retrieve the current span with get_current_span() and use its API " + "directly.", + DeprecationWarning, + stacklevel=2, + ) + return + + current_span = get_current_span() + + if current_span is None: + return + + if op is not None: + current_span.op = op + + if name is not None: + # internally it is still description + current_span.description = name + + if data is not None and attributes is not None: + raise ValueError( + "Cannot provide both `data` and `attributes`. Please use only `attributes`." + ) + + if data is not None: + warnings.warn( + "The `data` parameter is deprecated. Please use `attributes` instead.", + DeprecationWarning, + stacklevel=2, + ) + attributes = data + + if attributes is not None: + current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py new file mode 100644 index 0000000000..4d69d3acf2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py @@ -0,0 +1,71 @@ +import mimetypes +import os +from typing import TYPE_CHECKING + +from sentry_sdk.envelope import Item, PayloadRef + +if TYPE_CHECKING: + from typing import Callable, Optional, Union + + +class Attachment: + """Additional files/data to send along with an event. + + This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. + config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with + all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are + captured within the ``Scope``. + + To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for + ``add_attachment`` are the same as the parameters for this class's constructor. + + :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless + ``path`` is provided. + :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. + :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. + :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` + parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. + :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. + """ + + def __init__( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + if bytes is None and path is None: + raise TypeError("path or raw bytes required for attachment") + if filename is None and path is not None: + filename = os.path.basename(path) + if filename is None: + raise TypeError("filename is required for attachment") + if content_type is None: + content_type = mimetypes.guess_type(filename)[0] + self.bytes = bytes + self.filename = filename + self.path = path + self.content_type = content_type + self.add_to_transactions = add_to_transactions + + def to_envelope_item(self) -> "Item": + """Returns an envelope item for this attachment.""" + payload: "Union[None, PayloadRef, bytes]" = None + if self.bytes is not None: + if callable(self.bytes): + payload = self.bytes() + else: + payload = self.bytes + else: + payload = PayloadRef(path=self.path) + return Item( + payload=payload, + type="attachment", + content_type=self.content_type, + filename=self.filename, + ) + + def __repr__(self) -> str: + return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py new file mode 100644 index 0000000000..92cb42277e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py @@ -0,0 +1,1479 @@ +import json +import os +import platform +import random +import socket +import sys +import uuid +import warnings +from collections.abc import Iterable, Mapping +from datetime import datetime, timezone +from importlib import import_module +from typing import TYPE_CHECKING, Dict, List, cast, overload + +from sentry_sdk._compat import check_uwsgi_thread_support +from sentry_sdk._metrics_batcher import MetricsBatcher +from sentry_sdk._span_batcher import SpanBatcher +from sentry_sdk.consts import ( + DEFAULT_MAX_VALUE_LENGTH, + DEFAULT_OPTIONS, + INSTRUMENTER, + SPANDATA, + SPANSTATUS, + VERSION, + ClientConstructor, +) +from sentry_sdk.data_collection import ( + _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 +from sentry_sdk.monitor import Monitor +from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler +from sentry_sdk.profiler.transaction_profiler import ( + Profile, + has_profiling_enabled, + setup_profiler, +) +from sentry_sdk.scrubber import EventScrubber +from sentry_sdk.serializer import serialize +from sentry_sdk.sessions import SessionFlusher +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import trace +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import ( + AsyncHttpTransport, + HttpTransportCore, + make_transport, +) +from sentry_sdk.utils import ( + AnnotatedValue, + ContextVar, + capture_internal_exceptions, + current_stacktrace, + datetime_from_isoformat, + env_to_bool, + format_timestamp, + get_before_send_log, + get_before_send_metric, + get_before_send_span, + get_default_release, + get_sdk_name, + get_type_name, + handle_in_app, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union + + from sentry_sdk._log_batcher import LogBatcher + from sentry_sdk._metrics_batcher import MetricsBatcher + from sentry_sdk._types import ( + Event, + EventDataCategory, + Hint, + Log, + Metric, + SDKInfo, + SerializedAttributeValue, + ) + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.session import Session + from sentry_sdk.spotlight import SpotlightClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.transport import Item, Transport + from sentry_sdk.utils import Dsn + + I = TypeVar("I", bound=Integration) # noqa: E741 + +_client_init_debug = ContextVar("client_init_debug") + + +SDK_INFO: "SDKInfo" = { + "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() + "version": VERSION, + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], +} + + +def _serialized_v1_attribute_to_serialized_v2_attribute( + attribute_value: "Any", +) -> "Optional[SerializedAttributeValue]": + if isinstance(attribute_value, bool): + return { + "value": attribute_value, + "type": "boolean", + } + + if isinstance(attribute_value, int): + return { + "value": attribute_value, + "type": "integer", + } + + if isinstance(attribute_value, float): + return { + "value": attribute_value, + "type": "double", + } + + if isinstance(attribute_value, str): + return { + "value": attribute_value, + "type": "string", + } + + if isinstance(attribute_value, list): + if not attribute_value: + return {"value": [], "type": "array"} + + ty = type(attribute_value[0]) + if ty in (int, str, bool, float) and all( + type(v) is ty for v in attribute_value + ): + return { + "value": attribute_value, + "type": "array", + } + + # Types returned when the serializer for V1 span attributes recurses into some container types. + if isinstance(attribute_value, (dict, list)): + return { + "value": json.dumps(attribute_value), + "type": "string", + } + + return None + + +def _serialized_v1_span_to_serialized_v2_span( + span: "dict[str, Any]", event: "Event" +) -> "dict[str, Any]": + # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". + res: "dict[str, Any]" = { + "status": SpanStatus.OK.value, + "is_segment": False, + } + + if "trace_id" in span: + res["trace_id"] = span["trace_id"] + + if "span_id" in span: + res["span_id"] = span["span_id"] + + if "description" in span: + description = span["description"] + + if description is None and "op" in span: + description = span["op"] + + res["name"] = description + + if "start_timestamp" in span: + start_timestamp = None + try: + start_timestamp = datetime_from_isoformat(span["start_timestamp"]) + except Exception: + pass + + if start_timestamp is not None: + res["start_timestamp"] = start_timestamp.timestamp() + + if "timestamp" in span: + end_timestamp = None + try: + end_timestamp = datetime_from_isoformat(span["timestamp"]) + except Exception: + pass + + if end_timestamp is not None: + res["end_timestamp"] = end_timestamp.timestamp() + + if "parent_span_id" in span: + res["parent_span_id"] = span["parent_span_id"] + + if "status" in span and span["status"] != SPANSTATUS.OK: + res["status"] = "error" + + attributes: "Dict[str, Any]" = {} + + if "op" in span: + attributes["sentry.op"] = span["op"] + if "origin" in span: + attributes["sentry.origin"] = span["origin"] + + span_data = span.get("data") + if isinstance(span_data, dict): + attributes.update(span_data) + + span_tags = span.get("tags") + if isinstance(span_tags, dict): + attributes.update(span_tags) + + # See Scope._apply_user_attributes_to_telemetry() for user attributes. + user = event.get("user") + if isinstance(user, dict): + if "id" in user: + attributes["user.id"] = user["id"] + if "username" in user: + attributes["user.name"] = user["username"] + if "email" in user: + attributes["user.email"] = user["email"] + + # See Scope.set_global_attributes() for release, environment, and SDK metadata. + if "release" in event: + attributes["sentry.release"] = event["release"] + if "environment" in event: + attributes["sentry.environment"] = event["environment"] + if "server_name" in event: + attributes["server.address"] = event["server_name"] + if "transaction" in event: + attributes["sentry.segment.name"] = event["transaction"] + + trace_context = event.get("contexts", {}).get("trace", {}) + if "span_id" in trace_context: + attributes["sentry.segment.id"] = trace_context["span_id"] + + sdk_info = event.get("sdk") + if isinstance(sdk_info, dict): + if "name" in sdk_info: + attributes["sentry.sdk.name"] = sdk_info["name"] + if "version" in sdk_info: + attributes["sentry.sdk.version"] = sdk_info["version"] + + attributes["process.runtime.name"] = platform.python_implementation() + attributes["process.runtime.version"] = ( + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + ) + + if not attributes: + return res + + res["attributes"] = {} + for key, value in attributes.items(): + converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) + if converted_value is None: + continue + + res["attributes"][key] = converted_value + + # Remove redundant attribute, as status is stored in the status field. + if "status" in res["attributes"]: + del res["attributes"]["status"] + + return res + + +def _split_gen_ai_spans( + event_opt: "Event", +) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": + if "spans" not in event_opt: + return None + + spans: "Any" = event_opt["spans"] + if isinstance(spans, AnnotatedValue): + spans = spans.value + + if not isinstance(spans, Iterable): + return None + + non_gen_ai_spans = [] + gen_ai_spans = [] + for span in spans: + if not isinstance(span, dict): + non_gen_ai_spans.append(span) + continue + + span_op = span.get("op") + if isinstance(span_op, str) and span_op.startswith("gen_ai."): + gen_ai_spans.append(span) + else: + non_gen_ai_spans.append(span) + + return non_gen_ai_spans, gen_ai_spans + + +def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": + if args and (isinstance(args[0], (bytes, str)) or args[0] is None): + dsn: "Optional[str]" = args[0] + args = args[1:] + else: + dsn = None + + if len(args) > 1: + raise TypeError("Only single positional argument is expected") + + rv = dict(DEFAULT_OPTIONS) + options = dict(*args, **kwargs) + if dsn is not None and options.get("dsn") is None: + options["dsn"] = dsn + + for key, value in options.items(): + if key not in rv: + raise TypeError("Unknown option %r" % (key,)) + + rv[key] = value + + if rv["dsn"] is None: + rv["dsn"] = os.environ.get("SENTRY_DSN") + + if rv["release"] is None: + rv["release"] = get_default_release() + + if rv["environment"] is None: + rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" + + if rv["debug"] is None: + rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False + + if rv["server_name"] is None and hasattr(socket, "gethostname"): + rv["server_name"] = socket.gethostname() + + if rv["instrumenter"] is None: + rv["instrumenter"] = INSTRUMENTER.SENTRY + + if rv["project_root"] is None: + try: + project_root = os.getcwd() + except Exception: + project_root = None + + rv["project_root"] = project_root + + 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"] + ) + + if rv["socket_options"] and not isinstance(rv["socket_options"], list): + logger.warning( + "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." + ) + rv["socket_options"] = None + + if rv["keep_alive"] is None: + rv["keep_alive"] = ( + env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False + ) + + if rv["enable_tracing"] is not None: + warnings.warn( + "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", + DeprecationWarning, + stacklevel=2, + ) + + if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): + warnings.warn( + "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", + stacklevel=2, + ) + + return rv + + +try: + # Python 3.6+ + module_not_found_error = ModuleNotFoundError +except Exception: + # Older Python versions + module_not_found_error = ImportError # type: ignore + + +class BaseClient: + """ + .. versionadded:: 2.0.0 + + The basic definition of a client that is used for sending data to Sentry. + """ + + spotlight: "Optional[SpotlightClient]" = None + + def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: + self.options: "Dict[str, Any]" = ( + options if options is not None else DEFAULT_OPTIONS + ) + + self.transport: "Optional[Transport]" = None + self.monitor: "Optional[Monitor]" = None + self.log_batcher: "Optional[LogBatcher]" = None + self.metrics_batcher: "Optional[MetricsBatcher]" = None + self.span_batcher: "Optional[SpanBatcher]" = None + self.integrations: "dict[str, Integration]" = {} + + def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": + return {"options": {}} + + def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: + pass + + @property + def dsn(self) -> "Optional[str]": + return None + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + return None + + def should_send_default_pii(self) -> bool: + return False + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return False + + def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + return None + + def _capture_log(self, log: "Log", scope: "Scope") -> None: + pass + + def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: + pass + + def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: + pass + + def capture_session(self, *args: "Any", **kwargs: "Any") -> None: + return None + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, name_or_class: "Union[str, type[Integration]]" + ) -> "Optional[Integration]": + return None + + def close(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def flush(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def close_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def __enter__(self) -> "BaseClient": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + return None + + +class NonRecordingClient(BaseClient): + """ + .. versionadded:: 2.0.0 + + A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + """ + + pass + + +class _Client(BaseClient): + """ + The client is internally responsible for capturing the events and + forwarding them to sentry through the configured transport. It takes + the client options as keyword arguments and optionally the DSN as first + argument. + + Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) + """ + + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + super(_Client, self).__init__(options=get_options(*args, **kwargs)) + self._init_impl() + + def __getstate__(self) -> "Any": + return {"options": self.options} + + def __setstate__(self, state: "Any") -> None: + self.options = state["options"] + self._init_impl() + + def _setup_instrumentation( + self, functions_to_trace: "Sequence[Dict[str, str]]" + ) -> None: + """ + Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. + """ + for function in functions_to_trace: + class_name = None + function_qualname = function["qualified_name"] + + if "." not in function_qualname: + logger.warning( + "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", + function_qualname, + ) + continue + + module_name, function_name = function_qualname.rsplit(".", 1) + + try: + # Try to import module and function + # ex: "mymodule.submodule.funcname" + + module_obj = import_module(module_name) + function_obj = getattr(module_obj, function_name) + setattr(module_obj, function_name, trace(function_obj)) + logger.debug("Enabled tracing for %s", function_qualname) + except module_not_found_error: + try: + # Try to import a class + # ex: "mymodule.submodule.MyClassName.member_function" + + module_name, class_name = module_name.rsplit(".", 1) + module_obj = import_module(module_name) + class_obj = getattr(module_obj, class_name) + function_obj = getattr(class_obj, function_name) + function_type = type(class_obj.__dict__[function_name]) + traced_function = trace(function_obj) + + if function_type in (staticmethod, classmethod): + traced_function = staticmethod(traced_function) + + setattr(class_obj, function_name, traced_function) + setattr(module_obj, class_name, class_obj) + logger.debug("Enabled tracing for %s", function_qualname) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + def _init_impl(self) -> None: + old_debug = _client_init_debug.get(False) + + def _capture_envelope(envelope: "Envelope") -> None: + if self.spotlight is not None: + self.spotlight.capture_envelope(envelope) + if self.transport is not None: + self.transport.capture_envelope(envelope) + + def _record_lost_event( + reason: str, + data_category: "EventDataCategory", + item: "Optional[Item]" = None, + quantity: int = 1, + ) -> None: + if self.transport is not None: + self.transport.record_lost_event( + reason=reason, + data_category=data_category, + item=item, + quantity=quantity, + ) + + try: + _client_init_debug.set(self.options["debug"]) + self.transport = make_transport(self.options) + + self.monitor = None + if self.transport: + if self.options["enable_backpressure_handling"]: + self.monitor = Monitor(self.transport) + + # Setup Spotlight before creating batchers so _capture_envelope can use it. + # setup_spotlight handles all config/env var resolution per the SDK spec. + from sentry_sdk.spotlight import setup_spotlight + + self.spotlight = setup_spotlight(self.options) + if self.spotlight is not None and not self.options["dsn"]: + sample_all = lambda *_args, **_kwargs: 1.0 + self.options["send_default_pii"] = True + 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, + ) + + self.session_flusher = SessionFlusher(capture_func=_capture_envelope) + + self.log_batcher = None + + if has_logs_enabled(self.options): + from sentry_sdk._log_batcher import LogBatcher + + self.log_batcher = LogBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.metrics_batcher = None + if has_metrics_enabled(self.options): + self.metrics_batcher = MetricsBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.span_batcher = None + if has_span_streaming_enabled(self.options): + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + max_request_body_size = ("always", "never", "small", "medium") + if self.options["max_request_body_size"] not in max_request_body_size: + raise ValueError( + "Invalid value for max_request_body_size. Must be one of {}".format( + max_request_body_size + ) + ) + + if self.options["_experiments"].get("otel_powered_performance", False): + logger.debug( + "[OTel] Enabling experimental OTel-powered performance monitoring." + ) + self.options["instrumenter"] = INSTRUMENTER.OTEL + if ( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" + not in _DEFAULT_INTEGRATIONS + ): + _DEFAULT_INTEGRATIONS.append( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", + ) + + self.integrations = setup_integrations( + self.options["integrations"], + with_defaults=self.options["default_integrations"], + with_auto_enabling_integrations=self.options[ + "auto_enabling_integrations" + ], + disabled_integrations=self.options["disabled_integrations"], + options=self.options, + ) + + sdk_name = get_sdk_name(list(self.integrations.keys())) + SDK_INFO["name"] = sdk_name + logger.debug("Setting SDK name to '%s'", sdk_name) + + if has_profiling_enabled(self.options): + try: + setup_profiler(self.options) + except Exception as e: + logger.debug("Can not set up profiler. (%s)", e) + else: + try: + setup_continuous_profiler( + self.options, + sdk_info=SDK_INFO, + capture_func=_capture_envelope, + ) + except Exception as e: + logger.debug("Can not set up continuous profiler. (%s)", e) + + finally: + _client_init_debug.set(old_debug) + + self._setup_instrumentation(self.options.get("functions_to_trace", [])) + + if ( + self.monitor + or self.log_batcher + or self.metrics_batcher + or self.span_batcher + or has_profiling_enabled(self.options) + or isinstance(self.transport, HttpTransportCore) + ): + # If we have anything on that could spawn a background thread, we + # need to check if it's safe to use them. + check_uwsgi_thread_support() + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return True + + def should_send_default_pii(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. + """ + return self.options.get("send_default_pii") or False + + @property + def dsn(self) -> "Optional[str]": + """Returns the configured DSN as string.""" + return self.options["dsn"] + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + """Returns the configured parsed DSN object.""" + return self.transport.parsed_dsn if self.transport else None + + def _prepare_event( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]", + ) -> "Optional[Event]": + previous_total_spans: "Optional[int]" = None + previous_total_breadcrumbs: "Optional[int]" = None + + if event.get("timestamp") is None: + event["timestamp"] = datetime.now(timezone.utc) + + is_transaction = event.get("type") == "transaction" + + if scope is not None: + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + event_ = scope.apply_to_event(event, hint, self.options) + + # one of the event/error processors returned None + if event_ is None: + if self.transport: + self.transport.record_lost_event( + "event_processor", + data_category=("transaction" if is_transaction else "error"), + ) + if is_transaction: + self.transport.record_lost_event( + "event_processor", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + return None + + event = event_ + spans_delta = spans_before - len( + cast(List[Dict[str, object]], event.get("spans", [])) + ) + span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) + + if is_transaction and self.transport is not None: + if spans_delta > 0: + self.transport.record_lost_event( + "event_processor", data_category="span", quantity=spans_delta + ) + if span_recorder_dropped_spans > 0: + self.transport.record_lost_event( + "buffer_overflow", + data_category="span", + quantity=span_recorder_dropped_spans, + ) + + dropped_spans: int = span_recorder_dropped_spans + spans_delta + if dropped_spans > 0: + previous_total_spans = spans_before + dropped_spans + if scope._n_breadcrumbs_truncated > 0: + breadcrumbs = event.get("breadcrumbs", {}) + values = ( + breadcrumbs.get("values", []) + if not isinstance(breadcrumbs, AnnotatedValue) + else [] + ) + previous_total_breadcrumbs = ( + len(values) + scope._n_breadcrumbs_truncated + ) + + if ( + not is_transaction + and self.options["attach_stacktrace"] + and "exception" not in event + and "stacktrace" not in event + and "threads" not in event + ): + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=self.options.get( + "include_local_variables", True + ), + max_value_length=self.options.get( + "max_value_length", DEFAULT_MAX_VALUE_LENGTH + ), + ), + "crashed": False, + "current": True, + } + ] + } + + for key in "release", "environment", "server_name", "dist": + if event.get(key) is None and self.options[key] is not None: + event[key] = str(self.options[key]).strip() + if event.get("sdk") is None: + sdk_info = dict(SDK_INFO) + sdk_info["integrations"] = sorted(self.integrations.keys()) + event["sdk"] = sdk_info + + if event.get("platform") is None: + event["platform"] = "python" + + event = handle_in_app( + event, + self.options["in_app_exclude"], + self.options["in_app_include"], + self.options["project_root"], + ) + + if event is not None: + event_scrubber = self.options["event_scrubber"] + if event_scrubber: + event_scrubber.scrub_event(event) + + if scope is not None and scope._gen_ai_original_message_count: + spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) + if isinstance(spans, list): + for span in spans: + span_id = span.get("span_id", None) + span_data = span.get("data", {}) + if ( + span_id + and span_id in scope._gen_ai_original_message_count + and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data + ): + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], + {"len": scope._gen_ai_original_message_count[span_id]}, + ) + if previous_total_spans is not None: + event["spans"] = AnnotatedValue( + event.get("spans", []), {"len": previous_total_spans} + ) + if previous_total_breadcrumbs is not None: + event["breadcrumbs"] = AnnotatedValue( + event.get("breadcrumbs", {"values": []}), + {"len": previous_total_breadcrumbs}, + ) + + # Postprocess the event here so that annotated types do + # generally not surface in before_send + if event is not None: + event = cast( + "Event", + serialize( + cast("Dict[str, Any]", event), + max_request_body_size=self.options.get("max_request_body_size"), + max_value_length=self.options.get("max_value_length"), + custom_repr=self.options.get("custom_repr"), + ), + ) + + before_send = self.options["before_send"] + if ( + before_send is not None + and event is not None + and event.get("type") != "transaction" + ): + new_event = None + with capture_internal_exceptions(): + new_event = before_send(event, hint or {}) + if new_event is None: + logger.info("before send dropped event") + if self.transport: + self.transport.record_lost_event( + "before_send", data_category="error" + ) + + # If this is an exception, reset the DedupeIntegration. It still + # remembers the dropped exception as the last exception, meaning + # that if the same exception happens again and is not dropped + # in before_send, it'd get dropped by DedupeIntegration. + if event.get("exception"): + DedupeIntegration.reset_last_seen() + + event = new_event + + before_send_transaction = self.options["before_send_transaction"] + if ( + before_send_transaction is not None + and event is not None + and event.get("type") == "transaction" + ): + new_event = None + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + with capture_internal_exceptions(): + new_event = before_send_transaction(event, hint or {}) + if new_event is None: + logger.info("before send transaction dropped event") + if self.transport: + self.transport.record_lost_event( + reason="before_send", data_category="transaction" + ) + self.transport.record_lost_event( + reason="before_send", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + else: + spans_delta = spans_before - len(new_event.get("spans", [])) + if spans_delta > 0 and self.transport is not None: + self.transport.record_lost_event( + reason="before_send", data_category="span", quantity=spans_delta + ) + + event = new_event + + return event + + def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: + exc_info = hint.get("exc_info") + if exc_info is None: + return False + + error = exc_info[0] + error_type_name = get_type_name(exc_info[0]) + error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) + + for ignored_error in self.options["ignore_errors"]: + # String types are matched against the type name in the + # exception only + if isinstance(ignored_error, str): + if ignored_error == error_full_name or ignored_error == error_type_name: + return True + else: + if issubclass(error, ignored_error): + return True + + return False + + def _should_capture( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]" = None, + ) -> bool: + # Transactions are sampled independent of error events. + is_transaction = event.get("type") == "transaction" + if is_transaction: + return True + + ignoring_prevents_recursion = scope is not None and not scope._should_capture + if ignoring_prevents_recursion: + return False + + ignored_by_config_option = self._is_ignored_error(event, hint) + if ignored_by_config_option: + return False + + return True + + def _should_sample_error( + self, + event: "Event", + hint: "Hint", + ) -> bool: + error_sampler = self.options.get("error_sampler", None) + + if callable(error_sampler): + with capture_internal_exceptions(): + sample_rate = error_sampler(event, hint) + else: + sample_rate = self.options["sample_rate"] + + try: + not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate + except NameError: + logger.warning( + "The provided error_sampler raised an error. Defaulting to sampling the event." + ) + + # If the error_sampler raised an error, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + except TypeError: + parameter, verb = ( + ("error_sampler", "returned") + if callable(error_sampler) + else ("sample_rate", "contains") + ) + logger.warning( + "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." + % (parameter, verb, repr(sample_rate)) + ) + + # If the sample_rate has an invalid value, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + + if not_in_sample_rate: + # because we will not sample this event, record a "lost event". + if self.transport: + self.transport.record_lost_event("sample_rate", data_category="error") + + return False + + return True + + def _update_session_from_event( + self, + session: "Session", + event: "Event", + ) -> None: + crashed = False + errored = False + user_agent = None + + exceptions = (event.get("exception") or {}).get("values") + if exceptions: + errored = True + for error in exceptions: + if isinstance(error, AnnotatedValue): + error = error.value or {} + mechanism = error.get("mechanism") + if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: + crashed = True + break + + user = event.get("user") + + if session.user_agent is None: + headers = (event.get("request") or {}).get("headers") + headers_dict = headers if isinstance(headers, dict) else {} + for k, v in headers_dict.items(): + if k.lower() == "user-agent": + user_agent = v + break + + session.update( + status="crashed" if crashed else None, + user=user, + user_agent=user_agent, + errors=session.errors + (errored or crashed), + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + ) -> "Optional[str]": + """Captures an event. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + + :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. + """ + hint = dict(hint or ()) + + if not self._should_capture(event, hint, scope): + return None + + profile = event.pop("profile", None) + + event_id = event.get("event_id") + if event_id is None: + event["event_id"] = event_id = uuid.uuid4().hex + + span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) + event_opt = self._prepare_event(event, hint, scope) + if event_opt is None: + return None + + # whenever we capture an event we also check if the session needs + # to be updated based on that information. + session = scope._session if scope else None + if session: + self._update_session_from_event(session, event) + + is_transaction = event_opt.get("type") == "transaction" + is_checkin = event_opt.get("type") == "check_in" + + if ( + not is_transaction + and not is_checkin + and not self._should_sample_error(event, hint) + ): + return None + + attachments = hint.get("attachments") + + trace_context = event_opt.get("contexts", {}).get("trace") or {} + dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) + + headers: "dict[str, object]" = { + "event_id": event_opt["event_id"], + "sent_at": format_timestamp(datetime.now(timezone.utc)), + } + + if dynamic_sampling_context: + headers["trace"] = dynamic_sampling_context + + envelope = Envelope(headers=headers) + + if is_transaction and isinstance(profile, Profile): + envelope.add_profile(profile.to_json(event_opt, self.options)) + + if is_transaction and not span_recorder_has_gen_ai_span: + envelope.add_transaction(event_opt) + elif is_transaction: + split_spans = _split_gen_ai_spans(event_opt) + if split_spans is None or not split_spans[1]: + envelope.add_transaction(event_opt) + else: + non_gen_ai_spans, gen_ai_spans = split_spans + + event_opt["spans"] = non_gen_ai_spans + envelope.add_transaction(event_opt) + + converted_gen_ai_spans = [ + _serialized_v1_span_to_serialized_v2_span(span, event_opt) + for span in gen_ai_spans + if isinstance(span, dict) + ] + + envelope.add_item( + Item( + type=SpanBatcher.TYPE, + content_type=SpanBatcher.CONTENT_TYPE, + headers={ + "item_count": len(converted_gen_ai_spans), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": converted_gen_ai_spans, + }, + ), + ) + ) + + elif is_checkin: + envelope.add_checkin(event_opt) + else: + envelope.add_event(event_opt) + + for attachment in attachments or (): + envelope.add_item(attachment.to_envelope_item()) + + return_value = None + if self.spotlight: + self.spotlight.capture_envelope(envelope) + return_value = event_id + + if self.transport is not None: + self.transport.capture_envelope(envelope) + return_value = event_id + + return return_value + + def _capture_telemetry( + self, + telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", + ty: str, + scope: "Scope", + ) -> None: + """ + Capture attributes-based telemetry (logs, metrics, streamed spans). + + Apply any attributes set on the scope to it, and run the user's + before_send_{telemetry} on it, if applicable. + """ + if telemetry is None: + return + + scope.apply_to_telemetry(telemetry) + + before_send = None + + if ty == "log": + before_send = get_before_send_log(self.options) + serialized = telemetry + + elif ty == "metric": + before_send = get_before_send_metric(self.options) + serialized = telemetry + + elif ty == "span": + before_send = get_before_send_span(self.options) + serialized = telemetry._to_json() # type: ignore[union-attr] + + if before_send is not None: + serialized = before_send(serialized, {}) # type: ignore[arg-type] + + if ty in ("log", "metric"): + # Logs and metrics can be dropped in their respective + # before_send, so if we get None, don't queue them for sending. + if serialized is None: + return + + elif ty == "span" and isinstance(telemetry, StreamedSpan): + # Spans can't be dropped in before_send_span by design. They can + # be altered though (e.g. to sanitize). Only allow changes to + # name and attributes. + if isinstance(serialized, dict) and "name" in serialized: + telemetry.name = serialized["name"] # type: ignore[typeddict-item] + telemetry._attributes = {} + for k, v in (serialized.get("attributes") or {}).items(): + telemetry.set_attribute(k, v) + + else: + logger.debug( + "[Tracing] Invalid return value from before_send_span. Keeping original span." + ) + + serialized = telemetry._to_json() + + batcher = None + if ty == "log": + batcher = self.log_batcher + + elif ty == "metric": + batcher = self.metrics_batcher + + elif ty == "span": + # We need a reference to the segment span in the batcher to populate + # the dynamic sampling context (DSC) + serialized["_segment_span"] = telemetry._segment # type: ignore + batcher = self.span_batcher + + if batcher is not None: + batcher.add(serialized) # type: ignore + + def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: + self._capture_telemetry(log, "log", scope) + + def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: + self._capture_telemetry(metric, "metric", scope) + + def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: + self._capture_telemetry(span, "span", scope) + + def capture_session( + self, + session: "Session", + ) -> None: + if not session.release: + logger.info("Discarded session update because of missing release") + else: + self.session_flusher.add_session(session) + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Optional[Integration]": + """Returns the integration for this client by name or class. + If the client does not have that integration then `None` is returned. + """ + if isinstance(name_or_class, str): + integration_name = name_or_class + elif name_or_class.identifier is not None: + integration_name = name_or_class.identifier + else: + raise ValueError("Integration has no name") + + return self.integrations.get(integration_name) + + def _has_async_transport(self) -> bool: + """Check if the current transport is async.""" + return isinstance(self.transport, AsyncHttpTransport) + + @property + def _batchers(self) -> "tuple[Any, ...]": + return tuple( + b + for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) + if b is not None + ) + + def _close_components(self) -> None: + """Kill all client components in the correct order.""" + self.session_flusher.kill() + for b in self._batchers: + b.kill() + if self.monitor: + self.monitor.kill() + + def _flush_components(self) -> None: + """Flush all client components.""" + self.session_flusher.flush() + for b in self._batchers: + b.flush() + + def close( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush`. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "close() used with AsyncHttpTransport. Use close_async() instead.", + stacklevel=2, + ) + self._flush_components() + else: + self.flush(timeout=timeout, callback=callback) + self._close_components() + self.transport.kill() + self.transport = None + + async def close_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush_async`. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "close_async() used with non-async transport, aborting. Please use close() instead." + ) + return + await self.flush_async(timeout=timeout, callback=callback) + self._close_components() + kill_task = self.transport.kill() # type: ignore + if kill_task is not None: + await kill_task + self.transport = None + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "flush() used with AsyncHttpTransport. Use flush_async() instead.", + stacklevel=2, + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + + self.transport.flush(timeout=timeout, callback=callback) + + async def flush_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "flush_async() used with non-async transport, aborting. Please use flush() instead." + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore + if flush_task is not None: + await flush_task + + def __enter__(self) -> "_Client": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + self.close() + + async def __aenter__(self) -> "_Client": + return self + + async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + await self.close_async() + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `get_options` is a + # type to have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `Dict[str, Any]` to tell static analyzers about the return type. + + class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 + pass + + class Client(ClientConstructor, _Client): + pass + +else: + # Alias `get_options` for actual usage. Go through the lambda indirection + # to throw PyCharm off of the weakly typed signature (it would otherwise + # discover both the weakly typed signature of `_init` and our faked `init` + # type). + + get_options = (lambda: _get_options)() + Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py new file mode 100644 index 0000000000..759898f6ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py @@ -0,0 +1,1802 @@ +import itertools +from enum import Enum +from typing import TYPE_CHECKING + +DEFAULT_MAX_VALUE_LENGTH = None + +DEFAULT_MAX_STACK_FRAMES = 100 +DEFAULT_ADD_FULL_STACK = False + + +# Also needs to be at the top to prevent circular import +class EndpointType(Enum): + """ + The type of an endpoint. This is an enum, rather than a constant, for historical reasons + (the old /store endpoint). The enum also preserve future compatibility, in case we ever + have a new endpoint. + """ + + ENVELOPE = "envelope" + OTLP_TRACES = "integration/otlp/v1/traces" + + +class CompressionAlgo(Enum): + GZIP = "gzip" + BROTLI = "br" + + +if TYPE_CHECKING: + from typing import ( + AbstractSet, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + ) + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + from sentry_sdk._types import ( + BreadcrumbProcessor, + ContinuousProfilerMode, + DataCollectionUserOptions, + Event, + EventProcessor, + Hint, + IgnoreSpansConfig, + Log, + Metric, + ProfilerMode, + SpanJSON, + TracesSampler, + TransactionProcessor, + ) + + # Experiments are feature flags to enable and disable certain unstable SDK + # functionality. Changing them from the defaults (`None`) in production + # code is highly discouraged. They are not subject to any stability + # guarantees such as the ones from semantic versioning. + Experiments = TypedDict( + "Experiments", + { + "max_spans": Optional[int], + "max_flags": Optional[int], + "record_sql_params": Optional[bool], + "continuous_profiling_auto_start": Optional[bool], + "continuous_profiling_mode": Optional[ContinuousProfilerMode], + "otel_powered_performance": Optional[bool], + "transport_zlib_compression_level": Optional[int], + "transport_compression_level": Optional[int], + "transport_compression_algo": Optional[CompressionAlgo], + "transport_num_pools": Optional[int], + "transport_http2": Optional[bool], + "transport_async": Optional[bool], + "enable_logs": Optional[bool], + "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], + "enable_metrics": Optional[bool], + "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], + "trace_lifecycle": Optional[Literal["static", "stream"]], + "ignore_spans": Optional[IgnoreSpansConfig], + "before_send_span": Optional[ + Callable[[SpanJSON, Hint], Optional[SpanJSON]] + ], + "suppress_asgi_chained_exceptions": Optional[bool], + "data_collection": Optional[DataCollectionUserOptions], + }, + total=False, + ) + +DEFAULT_QUEUE_SIZE = 100 +DEFAULT_MAX_BREADCRUMBS = 100 +MATCH_ALL = r".*" + +FALSE_VALUES = [ + "false", + "no", + "off", + "n", + "0", +] + + +class SPANTEMPLATE(str, Enum): + DEFAULT = "default" + AI_AGENT = "ai_agent" + AI_TOOL = "ai_tool" + AI_CHAT = "ai_chat" + + def __str__(self) -> str: + return self.value + + +class INSTRUMENTER: + SENTRY = "sentry" + OTEL = "otel" + + +class SPANNAME: + DB_COMMIT = "COMMIT" + DB_ROLLBACK = "ROLLBACK" + + +class SPANDATA: + """ + Additional information describing the type of the span. + See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ + """ + + AI_CITATIONS = "ai.citations" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + References or sources cited by the AI model in its response. + Example: ["Smith et al. 2020", "Jones 2019"] + """ + + AI_DOCUMENTS = "ai.documents" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Documents or content chunks used as context for the AI model. + Example: ["doc1.txt", "doc2.pdf"] + """ + + AI_FINISH_REASON = "ai.finish_reason" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. + + The reason why the model stopped generating. + Example: "length" + """ + + AI_FREQUENCY_PENALTY = "ai.frequency_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_FUNCTION_CALL = "ai.function_call" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_GENERATION_ID = "ai.generation_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. + + Unique identifier for the completion. + Example: "gen_123abc" + """ + + AI_INPUT_MESSAGES = "ai.input_messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. + + The input messages to an LLM call. + Example: [{"role": "user", "message": "hello"}] + """ + + AI_LOGIT_BIAS = "ai.logit_bias" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the logit bias + """ + + AI_METADATA = "ai.metadata" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Extra metadata passed to an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_MODEL_ID = "ai.model_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. + + The unique descriptor of the model being executed. + Example: gpt-4 + """ + + AI_PIPELINE_NAME = "ai.pipeline.name" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. + + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + AI_PREAMBLE = "ai.preamble" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the preamble parameter. + Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. + Example: "You are now a clown." + """ + + AI_PRESENCE_PENALTY = "ai.presence_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_RAW_PROMPTING = "ai.raw_prompting" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Minimize pre-processing done to the prompt sent to the LLM. + Example: true + """ + + AI_RESPONSE_FORMAT = "ai.response_format" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the format of the response + """ + + AI_RESPONSES = "ai.responses" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. + + The responses to an AI model call. Always as a list. + Example: ["hello", "world"] + """ + + AI_SEARCH_QUERIES = "ai.search_queries" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Queries used to search for relevant context or documents. + Example: ["climate change effects", "renewable energy"] + """ + + AI_SEARCH_REQUIRED = "ai.is_search_required" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Boolean indicating if the model needs to perform a search. + Example: true + """ + + AI_SEARCH_RESULTS = "ai.search_results" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Results returned from search queries for context. + Example: ["Result 1", "Result 2"] + """ + + AI_SEED = "ai.seed" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. + + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: 123.45 + """ + + AI_STREAMING = "ai.streaming" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. + + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + AI_TAGS = "ai.tags" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Tags that describe an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_TEMPERATURE = "ai.temperature" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. + + For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. + Example: 0.5 + """ + + AI_TEXTS = "ai.texts" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Raw text inputs provided to the model. + Example: ["What is machine learning?"] + """ + + AI_TOP_K = "ai.top_k" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. + + For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. + Example: 35 + """ + + AI_TOP_P = "ai.top_p" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. + + For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. + Example: 0.5 + """ + + AI_TOOL_CALLS = "ai.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_TOOLS = "ai.tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. + + For an AI model call, the functions that are available + """ + + AI_WARNINGS = "ai.warnings" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Warning messages generated during model execution. + Example: ["Token limit exceeded"] + """ + + CACHE_HIT = "cache.hit" + """ + A boolean indicating whether the requested data was found in the cache. + Example: true + """ + + CACHE_ITEM_SIZE = "cache.item_size" + """ + The size of the requested data in bytes. + Example: 58 + """ + + CACHE_KEY = "cache.key" + """ + The key of the requested data. + Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 + """ + + CLIENT_ADDRESS = "client.address" + """ + Client address of the network connection - IP address or Unix domain socket name. + Example: "10.1.2.80" + """ + + CODE_FILEPATH = "code.filepath" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FILE_PATH instead. + + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FILE_PATH = "code.file.path" + """ + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FUNCTION = "code.function" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead. + + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_FUNCTION_NAME = "code.function.name" + """ + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_LINENO = "code.lineno" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_LINE_NUMBER instead. + + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + Example: 42 + """ + + CODE_LINE_NUMBER = "code.line.number" + """ + The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. + Example: 42 + """ + + CODE_NAMESPACE = "code.namespace" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. + + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + Example: "http.handler" + """ + + DB_MONGODB_COLLECTION = "db.mongodb.collection" + """ + The MongoDB collection being accessed within the database. + See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes + Example: public.users; customers + """ + + DB_NAME = "db.name" + """ + .. deprecated:: + This attribute is deprecated. Use DB_NAMESPACE instead. + + The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Example: myDatabase + """ + + DB_NAMESPACE = "db.namespace" + """ + The name of the database being accessed. + Example: "customers" + """ + + DB_DRIVER_NAME = "db.driver.name" + """ + The name of the database driver being used for the connection. + Example: "psycopg2" + """ + + DB_OPERATION = "db.operation" + """ + .. deprecated:: + This attribute is deprecated. Use DB_OPERATION_NAME instead. + + The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: findAndModify, HMSET, SELECT + """ + + DB_OPERATION_NAME = "db.operation.name" + """ + The name of the operation being executed. + Example: "SELECT" + """ + + DB_SYSTEM = "db.system" + """ + .. deprecated:: + This attribute is deprecated. Use DB_SYSTEM_NAME instead. + + An identifier for the database management system (DBMS) product being used. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: postgresql + """ + + DB_QUERY_TEXT = "db.query.text" + """ + The database query being executed. + Example: "SELECT * FROM users WHERE id = $1" + """ + + DB_SYSTEM_NAME = "db.system.name" + """ + An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. + Example: "postgresql" + """ + + DB_USER = "db.user" + """ + The name of the database user used for connecting to the database. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: my_user + """ + + GEN_AI_AGENT_NAME = "gen_ai.agent.name" + """ + The name of the agent being used. + Example: "ResearchAssistant" + """ + + GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" + """ + The unique identifier for the conversation/thread with the AI model. + Example: "conv_abc123" + """ + + GEN_AI_CHOICE = "gen_ai.choice" + """ + The model's response message. + Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" + """ + + GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" + """ + The input to the embeddings operation. + Example: "Hello!" + """ + + GEN_AI_FUNCTION_ID = "gen_ai.function_id" + """ + Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. + Example: "my-awesome-function" + """ + + GEN_AI_OPERATION_NAME = "gen_ai.operation.name" + """ + The name of the operation being performed. + Example: "chat" + """ + + GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" + """ + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" + """ + The reason why the model stopped generating. + Example: "COMPLETE" + """ + + GEN_AI_RESPONSE_ID = "gen_ai.response.id" + """ + Unique identifier for the completion. + Example: "gen_123abc" + """ + + GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" + """ + Exact model identifier used to generate the response + Example: gpt-4o-mini-2024-07-18 + """ + + GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" + """ + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The model's response text messages. + Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] + """ + + GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" + """ + The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. + Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] + """ + + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" + """ + The time it took to receive the first token from the model. + Example: 0.1 + """ + + GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The tool calls in the model's response. + Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] + """ + + GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. + + The available tools for the model. + Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] + """ + + GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + """ + The list of source system tool definitions available to the GenAI agent or model. + Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] + """ + + GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" + """ + The frequency penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" + """ + The maximum number of tokens to generate in the response. + Example: 2048 + """ + + GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" + """ + The system instructions passed to the model. + Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] + """ + + GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. + + The messages passed to the model. The "content" can be a string or an array of objects. + Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] + """ + + GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" + """ + The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". + Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] + """ + + GEN_AI_REQUEST_MODEL = "gen_ai.request.model" + """ + The model identifier being used for the request. + Example: "gpt-4-turbo" + """ + + GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" + """ + The presence penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_SEED = "gen_ai.request.seed" + """ + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: "1234567890" + """ + + GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" + """ + The temperature parameter used to control randomness in the output. + Example: 0.7 + """ + + GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" + """ + Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). + Example: 35 + """ + + GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" + """ + The top_p parameter used to control diversity via nucleus sampling. + Example: 1.0 + """ + + GEN_AI_SYSTEM = "gen_ai.system" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. + + The name of the AI system being used. + Example: "openai" + """ + + GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" + """ + The Generative AI provider as identified by the client or server instrumentation. + Example: "openai" + """ + + GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" + """ + The description of the tool being used. + Example: "Searches the web for current information about a topic" + """ + + GEN_AI_TOOL_INPUT = "gen_ai.tool.input" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. + + The input of the tool being used. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" + """ + The arguments of the tool call. It has to be a stringified version of the arguments to the tool. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_NAME = "gen_ai.tool.name" + """ + The name of the tool being used. + Example: "web_search" + """ + + GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. + + The output of the tool being used. + Example: "rainy, 57°F" + """ + + GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" + """ + The result of the tool call. It has to be a stringified version of the result of the tool. + Example: "rainy, 57°F" + """ + + GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + """ + The number of tokens in the input. + Example: 150 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" + """ + The number of cached tokens in the input. + Example: 50 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" + """ + The number of tokens written to the cache when processing the AI input (prompt). + Example: 100 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + """ + The number of tokens in the output. + Example: 250 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" + """ + The number of tokens used for reasoning in the output. + Example: 75 + """ + + GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" + """ + The total number of tokens used (input + output). + Example: 400 + """ + + GEN_AI_USER_MESSAGE = "gen_ai.user.message" + """ + The user message passed to the model. + Example: "What's the weather in Paris?" + """ + + HTTP_FRAGMENT = "http.fragment" + """ + The Fragments present in the URL. + Example: #foo=bar + """ + + HTTP_METHOD = "http.method" + """ + .. deprecated:: + This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. + + The HTTP method used. + Example: GET + """ + + HTTP_REQUEST_BODY_DATA = "http.request.body.data" + """ + HTTP request body data. Can be given as string or structural data of any format. + Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" + """ + + HTTP_REQUEST_HEADER = "http.request.header" + """ + Prefix for HTTP request header attributes. The header name (lowercased) is + appended to form the full attribute key. + Example: "http.request.header.content-type" + """ + + HTTP_REQUEST_METHOD = "http.request.method" + """ + The HTTP method used. + Example: GET + """ + + HTTP_QUERY = "http.query" + """ + The Query string present in the URL. + Example: ?foo=bar&bar=baz + """ + + HTTP_STATUS_CODE = "http.response.status_code" + """ + The HTTP status code as an integer. + Example: 418 + """ + + MESSAGING_DESTINATION_NAME = "messaging.destination.name" + """ + The destination name where the message is being consumed from, + e.g. the queue name or topic. + """ + + MESSAGING_MESSAGE_ID = "messaging.message.id" + """ + The message's identifier. + """ + + MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" + """ + The latency between when the task was enqueued and when it was started to be processed. + """ + + MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" + """ + Number of retries/attempts to process a message. + """ + + MESSAGING_SYSTEM = "messaging.system" + """ + The messaging system's name, e.g. `kafka`, `aws_sqs` + """ + + MIDDLEWARE_NAME = "middleware.name" + """ + The middleware's name, e.g. `AuthenticationMiddleware` + """ + + NETWORK_PROTOCOL_NAME = "network.protocol.name" + """ + The application layer protocol name used for the network connection. + Example: "http", "https" + """ + + NETWORK_PEER_ADDRESS = "network.peer.address" + """ + Peer address of the network connection - IP address or Unix domain socket name. + Example: 10.1.2.80, /tmp/my.sock, localhost + """ + + NETWORK_PEER_PORT = "network.peer.port" + """ + Peer port number of the network connection. + Example: 6379 + """ + + NETWORK_TRANSPORT = "network.transport" + """ + The transport protocol used for the network connection. + Example: "tcp", "udp", "unix" + """ + + PROCESS_PID = "process.pid" + """ + The process ID of the running process. + Example: 12345 + """ + + PROCESS_COMMAND_ARGS = "process.command_args" + """ + All the command arguments (including the command/executable itself) as received by the process. + Example: ["cmd/otecol","--config=config.yaml"] + """ + + PROFILER_ID = "profiler_id" + """ + Label identifying the profiler id that the span occurred in. This should be a string. + Example: "5249fbada8d5416482c2f6e47e337372" + """ + + RPC_METHOD = "rpc.method" + """ + The fully-qualified logical name of the method from the RPC interface perspective. + Example: "com.example.ExampleService/exampleMethod" + """ + + RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" + """ + Status code of the RPC returned by the RPC server or generated by the client. + Example: "DEADLINE_EXCEEDED" + """ + + SERVER_ADDRESS = "server.address" + """ + Name of the database host. + Example: example.com + """ + + SERVER_PORT = "server.port" + """ + Logical server port number + Example: 80; 8080; 443 + """ + + SERVER_SOCKET_ADDRESS = "server.socket.address" + """ + Physical server IP address or Unix socket address. + Example: 10.5.3.2 + """ + + SERVER_SOCKET_PORT = "server.socket.port" + """ + Physical server port. + Recommended: If different than server.port. + Example: 16456 + """ + + THREAD_ID = "thread.id" + """ + Identifier of a thread from where the span originated. This should be a string. + Example: "7972576320" + """ + + THREAD_NAME = "thread.name" + """ + Label identifying a thread from where the span originated. This should be a string. + Example: "MainThread" + """ + + USER_EMAIL = "user.email" + """ + User email address. + Example: "test@example.com" + """ + + USER_ID = "user.id" + """ + Unique identifier of the user. + Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" + """ + + USER_IP_ADDRESS = "user.ip_address" + """ + The IP address of the user that triggered the request. + Example: "10.1.2.80" + """ + + USER_NAME = "user.name" + """ + Short name or login/username of the user. + Example: "j.smith" + """ + + URL_FULL = "url.full" + """ + The URL of the resource that was fetched. + Example: "https://example.com/test?foo=bar#buzz" + """ + + URL_FRAGMENT = "url.fragment" + """ + The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. + Example: "details" + """ + + URL_PATH = "url.path" + """ + The URI path component. + Example: "/foo" + """ + + URL_QUERY = "url.query" + """ + The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. + Example: "foo=bar&bar=baz" + """ + + MCP_TOOL_NAME = "mcp.tool.name" + """ + The name of the MCP tool being called. + Example: "get_weather" + """ + + MCP_PROMPT_NAME = "mcp.prompt.name" + """ + The name of the MCP prompt being retrieved. + Example: "code_review" + """ + + MCP_RESOURCE_URI = "mcp.resource.uri" + """ + The URI of the MCP resource being accessed. + Example: "file:///path/to/resource" + """ + + MCP_METHOD_NAME = "mcp.method.name" + """ + The MCP protocol method name being called. + Example: "tools/call", "prompts/get", "resources/read" + """ + + MCP_REQUEST_ID = "mcp.request.id" + """ + The unique identifier for the MCP request. + Example: "req_123abc" + """ + + MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" + """ + The result/output content from an MCP tool execution. + Example: "The weather is sunny" + """ + + MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" + """ + The number of items/keys in the MCP tool result. + Example: 5 + """ + + MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" + """ + Whether the MCP tool execution resulted in an error. + Example: True + """ + + MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" + """ + The message content from an MCP prompt retrieval. + Example: "Review the following code..." + """ + + MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" + """ + The role of the message in an MCP prompt retrieval (only set for single-message prompts). + Example: "user", "assistant", "system" + """ + + MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" + """ + The number of messages in an MCP prompt result. + Example: 1, 3 + """ + + MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" + """ + The protocol/scheme of the MCP resource URI. + Example: "file", "http", "https" + """ + + MCP_TRANSPORT = "mcp.transport" + """ + The transport method used for MCP communication. + Example: "http", "sse", "stdio" + """ + + MCP_SESSION_ID = "mcp.session.id" + """ + The session identifier for the MCP connection. + Example: "a1b2c3d4e5f6" + """ + + SENTRY_DIST = "sentry.dist" + """ + The Sentry dist. + Example: "1.0" + """ + + SENTRY_ENVIRONMENT = "sentry.environment" + """ + The Sentry environment. + Example: "prod" + """ + + SENTRY_RELEASE = "sentry.release" + """ + The Sentry release. + Example: "1.2.3" + """ + + SENTRY_PLATFORM = "sentry.platform" + """ + The sdk platform that generated the event. + Example: "python" + """ + + SENTRY_SDK_NAME = "sentry.sdk.name" + """ + The name of the SDK. + Example: "python" + """ + + SENTRY_SDK_VERSION = "sentry.sdk.version" + """ + The SDK version. + Example: "1.2.3" + """ + + SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" + """ + A list of names identifying enabled integrations. + Example: ["AtexitIntegration", "StdlibIntegration"] + """ + + +class SPANSTATUS: + """ + The status of a Sentry span. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + + ABORTED = "aborted" + ALREADY_EXISTS = "already_exists" + CANCELLED = "cancelled" + DATA_LOSS = "data_loss" + DEADLINE_EXCEEDED = "deadline_exceeded" + FAILED_PRECONDITION = "failed_precondition" + INTERNAL_ERROR = "internal_error" + INVALID_ARGUMENT = "invalid_argument" + NOT_FOUND = "not_found" + OK = "ok" + OUT_OF_RANGE = "out_of_range" + PERMISSION_DENIED = "permission_denied" + RESOURCE_EXHAUSTED = "resource_exhausted" + UNAUTHENTICATED = "unauthenticated" + UNAVAILABLE = "unavailable" + UNIMPLEMENTED = "unimplemented" + UNKNOWN_ERROR = "unknown_error" + + +class OP: + ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" + CACHE_GET = "cache.get" + CACHE_PUT = "cache.put" + COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" + COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" + DB = "db" + DB_CURSOR_ITERATOR = "db.cursor.iter" + DB_CURSOR_FETCH = "db.cursor.fetch" + DB_REDIS = "db.redis" + EVENT_DJANGO = "event.django" + FUNCTION = "function" + FUNCTION_AWS = "function.aws" + FUNCTION_GCP = "function.gcp" + GEN_AI_CHAT = "gen_ai.chat" + GEN_AI_CREATE_AGENT = "gen_ai.create_agent" + GEN_AI_EMBEDDINGS = "gen_ai.embeddings" + GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" + GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" + GEN_AI_HANDOFF = "gen_ai.handoff" + GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" + GEN_AI_RESPONSES = "gen_ai.responses" + GRAPHQL_EXECUTE = "graphql.execute" + GRAPHQL_MUTATION = "graphql.mutation" + GRAPHQL_PARSE = "graphql.parse" + GRAPHQL_RESOLVE = "graphql.resolve" + GRAPHQL_SUBSCRIPTION = "graphql.subscription" + GRAPHQL_QUERY = "graphql.query" + GRAPHQL_VALIDATE = "graphql.validate" + GRPC_CLIENT = "grpc.client" + GRPC_SERVER = "grpc.server" + HTTP_CLIENT = "http.client" + HTTP_CLIENT_STREAM = "http.client.stream" + HTTP_SERVER = "http.server" + MIDDLEWARE_DJANGO = "middleware.django" + MIDDLEWARE_LITESTAR = "middleware.litestar" + MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" + MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" + MIDDLEWARE_STARLETTE = "middleware.starlette" + MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" + MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" + MIDDLEWARE_STARLITE = "middleware.starlite" + MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" + MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" + HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( + "ai.chat_completions.create.huggingface_hub" + ) + QUEUE_PROCESS = "queue.process" + QUEUE_PUBLISH = "queue.publish" + QUEUE_SUBMIT_ARQ = "queue.submit.arq" + QUEUE_TASK_ARQ = "queue.task.arq" + QUEUE_SUBMIT_CELERY = "queue.submit.celery" + QUEUE_TASK_CELERY = "queue.task.celery" + QUEUE_TASK_RQ = "queue.task.rq" + QUEUE_SUBMIT_HUEY = "queue.submit.huey" + QUEUE_TASK_HUEY = "queue.task.huey" + QUEUE_SUBMIT_RAY = "queue.submit.ray" + QUEUE_TASK_RAY = "queue.task.ray" + QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" + QUEUE_SUBMIT_DJANGO = "queue.submit.django" + SUBPROCESS = "subprocess" + SUBPROCESS_WAIT = "subprocess.wait" + SUBPROCESS_COMMUNICATE = "subprocess.communicate" + TEMPLATE_RENDER = "template.render" + VIEW_RENDER = "view.render" + VIEW_RESPONSE_RENDER = "view.response.render" + WEBSOCKET_SERVER = "websocket.server" + SOCKET_CONNECTION = "socket.connection" + SOCKET_DNS = "socket.dns" + MCP_SERVER = "mcp.server" + + +# This type exists to trick mypy and PyCharm into thinking `init` and `Client` +# take these arguments (even though they take opaque **kwargs) +class ClientConstructor: + def __init__( + self, + dsn: "Optional[str]" = None, + *, + max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + server_name: "Optional[str]" = None, + shutdown_timeout: float = 2, + integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 + in_app_include: "List[str]" = [], # noqa: B006 + in_app_exclude: "List[str]" = [], # noqa: B006 + default_integrations: bool = True, + dist: "Optional[str]" = None, + transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, + transport_queue_size: int = DEFAULT_QUEUE_SIZE, + sample_rate: float = 1.0, + send_default_pii: "Optional[bool]" = None, + http_proxy: "Optional[str]" = None, + https_proxy: "Optional[str]" = None, + ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 + max_request_body_size: str = "medium", + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, + keep_alive: "Optional[bool]" = None, + before_send: "Optional[EventProcessor]" = None, + before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, + debug: "Optional[bool]" = None, + attach_stacktrace: bool = False, + ca_certs: "Optional[str]" = None, + propagate_traces: bool = True, + traces_sample_rate: "Optional[float]" = None, + traces_sampler: "Optional[TracesSampler]" = None, + profiles_sample_rate: "Optional[float]" = None, + profiles_sampler: "Optional[TracesSampler]" = None, + profiler_mode: "Optional[ProfilerMode]" = None, + profile_lifecycle: 'Literal["manual", "trace"]' = "manual", + profile_session_sample_rate: "Optional[float]" = None, + auto_enabling_integrations: bool = True, + disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, + auto_session_tracking: bool = True, + send_client_reports: bool = True, + _experiments: "Experiments" = {}, # noqa: B006 + proxy_headers: "Optional[Dict[str, str]]" = None, + instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, + before_send_transaction: "Optional[TransactionProcessor]" = None, + project_root: "Optional[str]" = None, + enable_tracing: "Optional[bool]" = None, + include_local_variables: "Optional[bool]" = True, + include_source_context: "Optional[bool]" = True, + trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 + MATCH_ALL + ], + functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 + event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, + max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, + enable_backpressure_handling: bool = True, + error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, + enable_db_query_source: bool = True, + db_query_source_threshold_ms: int = 100, + enable_http_request_source: bool = True, + http_request_source_threshold_ms: int = 100, + spotlight: "Optional[Union[bool, str]]" = None, + cert_file: "Optional[str]" = None, + key_file: "Optional[str]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, + add_full_stack: bool = DEFAULT_ADD_FULL_STACK, + max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, + enable_logs: bool = False, + before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, + trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), + enable_metrics: bool = True, + before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, + org_id: "Optional[str]" = None, + strict_trace_continuation: bool = False, + stream_gen_ai_spans: bool = True, + ) -> None: + """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. + + :param dsn: The DSN tells the SDK where to send the events. + + If this option is not set, the SDK will just not send any data. + + The `dsn` config option takes precedence over the environment variable. + + Learn more about `DSN utilization `_. + + :param debug: Turns debug mode on or off. + + When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes + wrong with event sending. + + The default is always `False`. It's generally not recommended to turn it on in production because of the + increase in log output. + + The `debug` config option takes precedence over the environment variable. + + :param release: Sets the release. + + If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to + manually set it to guarantee that the release is in sync with your deploy integrations. + + Release names are strings, but some formats are detected by Sentry and might be rendered differently. + + See `the releases documentation `_ to learn how the SDK tries to + automatically configure a release. + + The `release` config option takes precedence over the environment variable. + + Learn more about how to send release data so Sentry can tell you about regressions between releases and + identify the potential source in `the product documentation `_. + + :param environment: Sets the environment. This string is freeform and set to `production` by default. + + A release can be associated with more than one environment to separate them in the UI (think `staging` vs + `production` or similar). + + The `environment` config option takes precedence over the environment variable. + + :param dist: The distribution of the application. + + Distributions are used to disambiguate build or deployment variants of the same release of an application. + + The dist can be for example a build number. + + :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. + + The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of + error events will be sent. + + Events are picked randomly. + + :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. + + This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and + which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number + between `0.0` and `1.0`, inclusive. + + The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to + send the event with the given probability. + + If this configuration option is specified, the `sample_rate` option is ignored. + + :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. + + Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're + sent to Sentry. + + By default, all errors are sent. + + :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. + + This defaults to `100`, but you can set this to any number. + + However, you should be aware that Sentry has a `maximum payload size `_ + and any events exceeding that payload size will be dropped. + + :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. + + Stack traces are always attached to exceptions; however, when this option is set, stack traces are also + sent with messages. + + This option means that stack traces appear next to all log messages. + + Grouping in Sentry is different for events with stack traces and without. As a result, you will get new + groups as you enable or disable this flag for certain events. + + :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) + `_ is added by active integrations. + + If you enable this option, be sure to manually remove what you don't want to send using our features for + managing `Sensitive Data `_. + + :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and + passwords from a `denylist`. + + It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. + + See how to `configure the scrubber here `_. + + :param include_source_context: When enabled, source context will be included in events sent to Sentry. + + This source context includes the five lines of code above and below the line of code where an error + happened. + + :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with + the event to help with debugging. + + :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the + moment an error occurs. + + But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will + be included in the stack trace sent to Sentry. + + :param max_stack_frames: This option limits the number of stack frames that will be captured when + `add_full_stack` is enabled. + + :param server_name: This option can be used to supply a server name. + + When provided, the name of the server is sent along and persisted in the event. + + For many integrations, the server name actually corresponds to the device hostname, even in situations + where the machine is not actually a server. + + :param project_root: The full path to the root directory of your application. + + The `project_root` is used to mark frames in a stack trace either as being in your application or outside + of the application. + + :param in_app_include: A list of string prefixes of module names that belong to the app. + + This option takes precedence over `in_app_exclude`. + + Sentry differentiates stack frames that are directly related to your application ("in application") from + stack frames that come from other packages such as the standard library, frameworks, or other dependencies. + + The application package is automatically marked as `inApp`. + + The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are + displayed by default. + + :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to + third-party packages. + + Modules considered not part of the app will be hidden from stack traces by default. + + This option can be overridden using `in_app_include`. + + :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. + It can be set to one of the following values: + + - `never`: Request bodies are never sent. + - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically + 4KB). + - `medium`: Medium and small requests will be captured (typically 10KB). + - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. + + Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ + expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of + how you configure this option. + + :param max_value_length: The number of characters after which the values containing text in the event payload + will be truncated. + + WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be + dropped by Sentry. + + :param ca_certs: A path to an alternative CA bundle file in PEM-format. + + :param send_client_reports: Set this boolean to `False` to disable sending of client reports. + + Client reports allow the client to send status reports about itself to Sentry, such as information about + events that were dropped before being sent. + + :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) + `_. + + This setting can be used to override the default config options for a specific auto-enabling integration + or to add an integration that is not auto-enabled. + + :param disabled_integrations: List of integrations that will be disabled. + + This setting can be used to explicitly turn off specific `auto-enabling integrations (list) + `_ or + `default `_ integrations. + + :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) + `_ should be enabled. + + When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding + framework/library is detected. + + :param default_integrations: Configures whether `default integrations + `_ should be enabled. + + Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling + integrations**, unless they are specifically added in the `integrations` option, described above. + + :param before_send: This function is called with an SDK-specific message or error event object, and can return + a modified event object, or `null` to skip reporting the event. + + This can be used, for instance, for manual PII stripping before sending. + + By the time `before_send` is executed, all scope data has already been applied to the event. Further + modification of the scope won't have any effect. + + :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can + return a modified transaction event object, or `null` to skip reporting the event. + + One way this might be used is for manual PII stripping before sending. + + :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb + is added to the scope. + + When nothing is returned from the function, the breadcrumb is dropped. + + To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. + + The callback typically gets a second argument (called a "hint") which contains the original object from + which the breadcrumb was created to further customize what the breadcrumb should look like. + + :param transport: Switches out the transport used to send events. + + How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to + send it through some more complex setup that requires proxy authentication. + + :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to + flush. + + :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. + + This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs + support a separate HTTPS proxy. + + SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix + systems, the `http_proxy` environment variable will be picked up. + + :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. + + This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for + HTTPS requests at all times. + + :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded + to `urllib3`'s `ProxyManager `_. + + :param shutdown_timeout: Controls how many seconds to wait before shutting down. + + Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending + events. The default is SDK specific but typically around two seconds. + + Setting this value too low may cause problems for sending events from command line applications. + + Setting the value too high will cause the application to block for a long time for users experiencing + network connectivity problems. + + :param keep_alive: Determines whether to keep the connection alive between requests. + + This can be useful in environments where you encounter frequent network issues such as connection resets. + + :param cert_file: Path to the client certificate to use. + + If set, supersedes the `CLIENT_CERT_FILE` environment variable. + + :param key_file: Path to the key file to use. + + If set, supersedes the `CLIENT_KEY_FILE` environment variable. + + :param socket_options: An optional list of socket options to use. + + These provide fine-grained, low-level control over the way the SDK connects to Sentry. + + If provided, the options will override the default `urllib3` `socket options + `_. + + :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction + will be sent to Sentry. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + Either this or `traces_sampler` must be defined to enable tracing. + + If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have + another service (for example a JS frontend) that makes requests to your service that include trace + information, those traces will be continued and thus transactions will be sent to Sentry. + + If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces + will be started and no incoming traces will be continued. + + :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be + sent to Sentry. + + It will automatically be passed information about the transaction and the context in which it's being + created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being + sent). + + Can also be used for filtering transactions, by returning `0` for those that are unwanted. + + Either this or `traces_sample_rate` must be defined to enable tracing. + + :param trace_propagation_targets: An optional property that controls which downstream services receive tracing + data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. + + The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. + + If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to + that request. + + String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ + a string provided through the option. + + If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the + instrumented client. + + :param functions_to_trace: An optional list of functions that should be set up for tracing. + + For each function in the list, a span will be created when the function is executed. + + Functions in the list are represented as strings containing the fully qualified name of the function. + + This is a convenient option, making it possible to have one central place for configuring what functions + to trace, instead of having custom instrumentation scattered all over your code base. + + To learn more, see the `Custom Instrumentation `_ documentation. + + :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health + checks on the SDK. + + If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second + intervals until recovery. + + This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. + + This option is enabled by default. + + :param enable_db_query_source: When enabled, the source location will be added to database queries. + + :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database + queries. + + The query location will be added to the query for queries slower than the specified threshold. + + :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. + + :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an + outgoing HTTP request. + + The request location will be added to the request for requests slower than the specified threshold. + + :param custom_repr: A custom `repr `_ function to run + while serializing an object. + + Use this to control how your custom objects and classes are visible in Sentry. + + Return a string for that repr value to be used or `None` to continue serializing how Sentry would have + done it anyway. + + :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled + transaction will be profiled. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be + profiled. + + :param profiles_sampler: + + :param profiler_mode: + + :param profile_lifecycle: + + :param profile_session_sample_rate: + + :param enable_tracing: + + :param propagate_traces: + + :param auto_session_tracking: + + :param spotlight: + + :param instrumenter: + + :param enable_logs: Set `enable_logs` to True to enable the SDK to emit + Sentry logs. Defaults to False. + + :param before_send_log: An optional function to modify or filter out logs + before they're sent to Sentry. Any modifications to the log in this + function will be retained. If the function returns None, the log will + not be sent to Sentry. + + :param trace_ignore_status_codes: An optional property that disables tracing for + HTTP requests with certain status codes. + + Requests are not traced if the status code is contained in the provided set. + + If `trace_ignore_status_codes` is not provided, requests with any status code + may be traced. + + This option has no effect in span streaming mode (`trace_lifecycle="stream"`). + + :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the + `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. + + If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. + + The client's organization ID is extracted from the DSN or can be set with the `org_id` option. + If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. + This is useful to prevent traces of unknown third-party services from being continued in your application. + + :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases + but you can provide it explicitly for self-hosted and Relay setups. This value is used for + trace propagation and for features like `strict_trace_continuation`. + + :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to + reduce downstream data loss. + + :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. + + ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations + collect automatically, superseding `send_default_pii`. Passing a dict under + `_experiments={"data_collection": {...}}` opts into the feature; 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 + changes nothing. Restrict collection per category (user identity, cookies, HTTP + headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source + context). If `send_default_pii` is also set, `data_collection` takes precedence. + + Example:: + + sentry_sdk.init( + dsn="...", + _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, + ) + + See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. + """ + pass + + +def _get_default_options() -> "dict[str, Any]": + import inspect + + a = inspect.getfullargspec(ClientConstructor.__init__) + defaults = a.defaults or () + kwonlydefaults = a.kwonlydefaults or {} + + return dict( + itertools.chain( + zip(a.args[-len(defaults) :], defaults), + kwonlydefaults.items(), + ) + ) + + +DEFAULT_OPTIONS = _get_default_options() +del _get_default_options + + +VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py new file mode 100644 index 0000000000..b3287703b9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py @@ -0,0 +1,9 @@ +from sentry_sdk.crons.api import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.crons.decorator import monitor + +__all__ = [ + "capture_checkin", + "MonitorStatus", + "monitor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py new file mode 100644 index 0000000000..6ea3e36b6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py @@ -0,0 +1,60 @@ +import uuid +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, MonitorConfig + + +def _create_check_in_event( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration_s: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> "Event": + options = sentry_sdk.get_client().options + check_in_id = check_in_id or uuid.uuid4().hex + + check_in: "Event" = { + "type": "check_in", + "monitor_slug": monitor_slug, + "check_in_id": check_in_id, + "status": status, + "duration": duration_s, + "environment": options.get("environment", None), + "release": options.get("release", None), + } + + if monitor_config: + check_in["monitor_config"] = monitor_config + + return check_in + + +def capture_checkin( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> str: + check_in_event = _create_check_in_event( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=status, + duration_s=duration, + monitor_config=monitor_config, + ) + + sentry_sdk.capture_event(check_in_event) + + logger.debug( + f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" + ) + + return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py new file mode 100644 index 0000000000..be686b4539 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py @@ -0,0 +1,4 @@ +class MonitorStatus: + IN_PROGRESS = "in_progress" + OK = "ok" + ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py new file mode 100644 index 0000000000..b13d350e15 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py @@ -0,0 +1,137 @@ +from functools import wraps +from inspect import iscoroutinefunction +from typing import TYPE_CHECKING + +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from types import TracebackType + from typing import ( + Any, + Optional, + ParamSpec, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from sentry_sdk._types import MonitorConfig + + P = ParamSpec("P") + R = TypeVar("R") + + +class monitor: # noqa: N801 + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + """ + + def __init__( + self, + monitor_slug: "Optional[str]" = None, + monitor_config: "Optional[MonitorConfig]" = None, + ) -> None: + self.monitor_slug = monitor_slug + self.monitor_config = monitor_config + + def __enter__(self) -> None: + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, + status=MonitorStatus.IN_PROGRESS, + monitor_config=self.monitor_config, + ) + + def __exit__( + self, + exc_type: "Optional[Type[BaseException]]", + exc_value: "Optional[BaseException]", + traceback: "Optional[TracebackType]", + ) -> None: + duration_s = now() - self.start_timestamp + + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR + + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + monitor_config=self.monitor_config, + ) + + if TYPE_CHECKING: + + @overload + def __call__( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + # Unfortunately, mypy does not give us any reliable way to type check the + # return value of an Awaitable (i.e. async function) for this overload, + # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. + ... + + @overload + def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... + + def __call__( + self, + fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", + ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": + if iscoroutinefunction(fn): + return self._async_wrapper(fn) + + else: + if TYPE_CHECKING: + fn = cast("Callable[P, R]", fn) + return self._sync_wrapper(fn) + + def _async_wrapper( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + @wraps(fn) + async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return await fn(*args, **kwargs) + + return inner + + def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": + @wraps(fn) + def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return fn(*args, **kwargs) + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py new file mode 100644 index 0000000000..bcdf767409 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py @@ -0,0 +1,319 @@ +""" +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, List, Mapping, Optional, cast + +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE + +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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: + """ + Return whether ``key`` matches the sensitive denylist using a partial, + case-insensitive substring match. + + :param extra_terms: additional deny terms (e.g. user-provided) to consider + alongside the built-in `_SENSITIVE_DENYLIST`. + """ + lowered = key.lower() + for term in _SENSITIVE_DENYLIST: + if term in lowered: + return True + if extra_terms: + for term in extra_terms: + if term and term.lower() in lowered: + return True + return False + + +def _apply_key_value_collection_filtering( + items: "Mapping[str, Any]", + behaviour: "KeyValueCollectionBehaviour", + substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, +) -> "Dict[str, Any]": + + if behaviour["mode"] == "off": + return {} + + result: "Dict[str, Any]" = {} + + if behaviour["mode"] == "allowlist": + for key, value in items.items(): + is_allowed = False + if isinstance(key, str): + lowered = key.lower() + is_allowed = any( + term and term.lower() in lowered + for term in behaviour.get("terms", []) + ) + if is_allowed and not _is_sensitive_key(key): + result[key] = value + else: + result[key] = substitute + return result + + # denylist behaviour + for key, value in items.items(): + if isinstance(key, str) and _is_sensitive_key( + key=key, extra_terms=behaviour.get("terms", []) + ): + result[key] = substitute + else: + result[key] = value + return result + + +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, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py new file mode 100644 index 0000000000..795882e9ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py @@ -0,0 +1,37 @@ +import logging +import sys +import warnings +from logging import LogRecord + +from sentry_sdk import get_client +from sentry_sdk.client import _client_init_debug +from sentry_sdk.utils import logger + + +class _DebugFilter(logging.Filter): + def filter(self, record: "LogRecord") -> bool: + if _client_init_debug.get(False): + return True + + return get_client().options["debug"] + + +def init_debug_support() -> None: + if not logger.handlers: + configure_logger() + + +def configure_logger() -> None: + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.DEBUG) + logger.addFilter(_DebugFilter()) + + +def configure_debug_hub() -> None: + warnings.warn( + "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py new file mode 100644 index 0000000000..d2d4aae31a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py @@ -0,0 +1,332 @@ +import io +import json +import mimetypes +from typing import TYPE_CHECKING + +from sentry_sdk.session import Session +from sentry_sdk.utils import capture_internal_exceptions, json_dumps + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, List, Optional, Union + + from sentry_sdk._types import Event, EventDataCategory + + +def parse_json(data: "Union[bytes, str]") -> "Any": + # on some python 3 versions this needs to be bytes + if isinstance(data, bytes): + data = data.decode("utf-8", "replace") + return json.loads(data) + + +class Envelope: + """ + Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints + documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, + each envelope may have at most one Item with type "event" or "transaction" (but not both). + """ + + def __init__( + self, + headers: "Optional[Dict[str, Any]]" = None, + items: "Optional[List[Item]]" = None, + ) -> None: + if headers is not None: + headers = dict(headers) + self.headers = headers or {} + if items is None: + items = [] + else: + items = list(items) + self.items = items + + @property + def description(self) -> str: + return "envelope with %s items (%s)" % ( + len(self.items), + ", ".join(x.data_category for x in self.items), + ) + + def add_event( + self, + event: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=event), type="event")) + + def add_transaction( + self, + transaction: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) + + def add_profile( + self, + profile: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) + + def add_profile_chunk( + self, + profile_chunk: "Any", + ) -> None: + self.add_item( + Item( + payload=PayloadRef(json=profile_chunk), + type="profile_chunk", + headers={"platform": profile_chunk.get("platform", "python")}, + ) + ) + + def add_checkin( + self, + checkin: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) + + def add_session( + self, + session: "Union[Session, Any]", + ) -> None: + if isinstance(session, Session): + session = session.to_json() + self.add_item(Item(payload=PayloadRef(json=session), type="session")) + + def add_sessions( + self, + sessions: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) + + def add_item( + self, + item: "Item", + ) -> None: + self.items.append(item) + + def get_event(self) -> "Optional[Event]": + for items in self.items: + event = items.get_event() + if event is not None: + return event + return None + + def get_transaction_event(self) -> "Optional[Event]": + for item in self.items: + event = item.get_transaction_event() + if event is not None: + return event + return None + + def __iter__(self) -> "Iterator[Item]": + return iter(self.items) + + def serialize_into( + self, + f: "Any", + ) -> None: + f.write(json_dumps(self.headers)) + f.write(b"\n") + for item in self.items: + item.serialize_into(f) + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Envelope": + headers = parse_json(f.readline()) + items = [] + while 1: + item = Item.deserialize_from(f) + if item is None: + break + items.append(item) + return cls(headers=headers, items=items) + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Envelope": + return cls.deserialize_from(io.BytesIO(bytes)) + + def __repr__(self) -> str: + return "" % (self.headers, self.items) + + +class PayloadRef: + def __init__( + self, + bytes: "Optional[bytes]" = None, + path: "Optional[Union[bytes, str]]" = None, + json: "Optional[Any]" = None, + ) -> None: + self.json = json + self.bytes = bytes + self.path = path + + def get_bytes(self) -> bytes: + if self.bytes is None: + if self.path is not None: + with capture_internal_exceptions(): + with open(self.path, "rb") as f: + self.bytes = f.read() + elif self.json is not None: + self.bytes = json_dumps(self.json) + return self.bytes or b"" + + @property + def inferred_content_type(self) -> str: + if self.json is not None: + return "application/json" + elif self.path is not None: + path = self.path + if isinstance(path, bytes): + path = path.decode("utf-8", "replace") + ty = mimetypes.guess_type(path)[0] + if ty: + return ty + return "application/octet-stream" + + def __repr__(self) -> str: + return "" % (self.inferred_content_type,) + + +class Item: + def __init__( + self, + payload: "Union[bytes, str, PayloadRef]", + headers: "Optional[Dict[str, Any]]" = None, + type: "Optional[str]" = None, + content_type: "Optional[str]" = None, + filename: "Optional[str]" = None, + ): + if headers is not None: + headers = dict(headers) + elif headers is None: + headers = {} + self.headers = headers + if isinstance(payload, bytes): + payload = PayloadRef(bytes=payload) + elif isinstance(payload, str): + payload = PayloadRef(bytes=payload.encode("utf-8")) + else: + payload = payload + + if filename is not None: + headers["filename"] = filename + if type is not None: + headers["type"] = type + if content_type is not None: + headers["content_type"] = content_type + elif "content_type" not in headers: + headers["content_type"] = payload.inferred_content_type + + self.payload = payload + + def __repr__(self) -> str: + return "" % ( + self.headers, + self.payload, + self.data_category, + ) + + @property + def type(self) -> "Optional[str]": + return self.headers.get("type") + + @property + def data_category(self) -> "EventDataCategory": + ty = self.headers.get("type") + if ty == "session" or ty == "sessions": + return "session" + elif ty == "attachment": + return "attachment" + elif ty == "transaction": + return "transaction" + elif ty == "span": + return "span" + elif ty == "event": + return "error" + elif ty == "log": + return "log_item" + elif ty == "trace_metric": + return "trace_metric" + elif ty == "client_report": + return "internal" + elif ty == "profile": + return "profile" + elif ty == "profile_chunk": + return "profile_chunk" + elif ty == "check_in": + return "monitor" + else: + return "default" + + def get_bytes(self) -> bytes: + return self.payload.get_bytes() + + def get_event(self) -> "Optional[Event]": + """ + Returns an error event if there is one. + """ + if self.type == "event" and self.payload.json is not None: + return self.payload.json + return None + + def get_transaction_event(self) -> "Optional[Event]": + if self.type == "transaction" and self.payload.json is not None: + return self.payload.json + return None + + def serialize_into( + self, + f: "Any", + ) -> None: + headers = dict(self.headers) + bytes = self.get_bytes() + headers["length"] = len(bytes) + f.write(json_dumps(headers)) + f.write(b"\n") + f.write(bytes) + f.write(b"\n") + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Optional[Item]": + line = f.readline().rstrip() + if not line: + return None + headers = parse_json(line) + length = headers.get("length") + if length is not None: + payload = f.read(length) + f.readline() + else: + # if no length was specified we need to read up to the end of line + # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) + payload = f.readline().rstrip(b"\n") + if headers.get("type") in ("event", "transaction"): + rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) + else: + rv = cls(headers=headers, payload=payload) + return rv + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Optional[Item]": + return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py new file mode 100644 index 0000000000..5eaa5e440b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py @@ -0,0 +1,75 @@ +import copy +from threading import Lock +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import TypedDict + + FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) + + +DEFAULT_FLAG_CAPACITY = 100 + + +class FlagBuffer: + def __init__(self, capacity: int) -> None: + self.capacity = capacity + self.lock = Lock() + + # Buffer is private. The name is mangled to discourage use. If you use this attribute + # directly you're on your own! + self.__buffer = LRUCache(capacity) + + def clear(self) -> None: + self.__buffer = LRUCache(self.capacity) + + def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": + with self.lock: + buffer = FlagBuffer(self.capacity) + buffer.__buffer = copy.deepcopy(self.__buffer, memo) + return buffer + + def get(self) -> "list[FlagData]": + with self.lock: + return [ + {"flag": key, "result": value} for key, value in self.__buffer.get_all() + ] + + def set(self, flag: str, result: bool) -> None: + if isinstance(result, FlagBuffer): + # If someone were to insert `self` into `self` this would create a circular dependency + # on the lock. This is of course a deadlock. However, this is far outside the expected + # usage of this class. We guard against it here for completeness and to document this + # expected failure mode. + raise ValueError( + "FlagBuffer instances can not be inserted into the dictionary." + ) + + with self.lock: + self.__buffer.set(flag, result) + + +def add_feature_flag(flag: str, result: bool) -> None: + """ + Records a flag and its value to be sent on subsequent error events. + We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. + """ + client = sentry_sdk.get_client() + + flags = sentry_sdk.get_isolation_scope().flags + flags.set(flag, result) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.get_current_span() + if span and isinstance(span, sentry_sdk.traces.StreamedSpan): + span.set_attribute(f"flag.evaluation.{flag}", result) + + else: + span = sentry_sdk.get_current_span() + if span and isinstance(span, Span): + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py new file mode 100644 index 0000000000..b17444d06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py @@ -0,0 +1,741 @@ +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import ( + get_client, + get_current_scope, + get_global_scope, + get_isolation_scope, +) +from sentry_sdk._compat import with_metaclass +from sentry_sdk.client import Client +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.scope import _ScopeManager +from sentry_sdk.tracing import ( + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.utils import ( + ContextVar, + logger, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.tracing import TransactionKwargs + + T = TypeVar("T") + +else: + + def overload(x: "T") -> "T": + return x + + +class SentryHubDeprecationWarning(DeprecationWarning): + """ + A custom deprecation warning to inform users that the Hub is deprecated. + """ + + _MESSAGE = ( + "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " + "Please consult our 1.x to 2.x migration guide for details on how to migrate " + "`Hub` usage to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" + ) + + def __init__(self, *_: object) -> None: + super().__init__(self._MESSAGE) + + +@contextmanager +def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": + """Utility function to suppress deprecation warnings for the Hub.""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) + yield + + +_local = ContextVar("sentry_current_hub") + + +class HubMeta(type): + @property + def current(cls) -> "Hub": + """Returns the current instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + rv = _local.get(None) + if rv is None: + with _suppress_hub_deprecation_warning(): + # This will raise a deprecation warning; suppress it since we already warned above. + rv = Hub(GLOBAL_HUB) + _local.set(rv) + return rv + + @property + def main(cls) -> "Hub": + """Returns the main instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + return GLOBAL_HUB + + +class Hub(with_metaclass(HubMeta)): # type: ignore + """ + .. deprecated:: 2.0.0 + The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. + + The hub wraps the concurrency management of the SDK. Each thread has + its own hub but the hub might transfer with the flow of execution if + context vars are available. + + If the hub is used with a with statement it's temporarily activated. + """ + + _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] + _scope: "Optional[Scope]" = None + + # Mypy doesn't pick up on the metaclass. + + if TYPE_CHECKING: + current: "Hub" = None # type: ignore[assignment] + main: "Optional[Hub]" = None + + def __init__( + self, + client_or_hub: "Optional[Union[Hub, Client]]" = None, + scope: "Optional[Any]" = None, + ) -> None: + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + + current_scope = None + + if isinstance(client_or_hub, Hub): + client = get_client() + if scope is None: + # hub cloning is going on, we use a fork of the current/isolation scope for context manager + scope = get_isolation_scope().fork() + current_scope = get_current_scope().fork() + else: + client = client_or_hub + get_global_scope().set_client(client) + + if scope is None: # so there is no Hub cloning going on + # just the current isolation scope is used for context manager + scope = get_isolation_scope() + current_scope = get_current_scope() + + if current_scope is None: + # just the current current scope is used for context manager + current_scope = get_current_scope() + + self._stack = [(client, scope)] # type: ignore + self._last_event_id: "Optional[str]" = None + self._old_hubs: "List[Hub]" = [] + + self._old_current_scopes: "List[Scope]" = [] + self._old_isolation_scopes: "List[Scope]" = [] + self._current_scope: "Scope" = current_scope + self._scope: "Scope" = scope + + def __enter__(self) -> "Hub": + self._old_hubs.append(Hub.current) + _local.set(self) + + current_scope = get_current_scope() + self._old_current_scopes.append(current_scope) + scope._current_scope.set(self._current_scope) + + isolation_scope = get_isolation_scope() + self._old_isolation_scopes.append(isolation_scope) + scope._isolation_scope.set(self._scope) + + return self + + def __exit__( + self, + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[Any]", + ) -> None: + old = self._old_hubs.pop() + _local.set(old) + + old_current_scope = self._old_current_scopes.pop() + scope._current_scope.set(old_current_scope) + + old_isolation_scope = self._old_isolation_scopes.pop() + scope._isolation_scope.set(old_isolation_scope) + + def run( + self, + callback: "Callable[[], T]", + ) -> "T": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Runs a callback in the context of the hub. Alternatively the + with statement can be used on the hub directly. + """ + with self: + return callback() + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Any": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. + + Returns the integration for this hub by name or class. If there + is no client bound or the client does not have that integration + then `None` is returned. + + If the return value is not `None` the hub is guaranteed to have a + client attached. + """ + return get_client().get_integration(name_or_class) + + @property + def client(self) -> "Optional[BaseClient]": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Please use :py:func:`sentry_sdk.api.get_client` instead. + + Returns the current client on the hub. + """ + client = get_client() + + if not client.is_active(): + return None + + return client + + @property + def scope(self) -> "Scope": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Returns the current scope on the hub. + """ + return get_isolation_scope() + + def last_event_id(self) -> "Optional[str]": + """ + Returns the last event ID. + + .. deprecated:: 1.40.5 + This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. + """ + logger.warning( + "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." + ) + return self._last_event_id + + def bind_client( + self, + new: "Optional[BaseClient]", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.set_client` instead. + + Binds a new client to the hub. + """ + get_global_scope().set_client(new) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. + + Captures an event. + + Alias of :py:meth:`sentry_sdk.Scope.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + """ + last_event_id = get_current_scope().capture_event( + event, hint, scope=scope, **scope_kwargs + ) + + is_transaction = event.get("type") == "transaction" + if last_event_id is not None and not is_transaction: + self._last_event_id = last_event_id + + return last_event_id + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. + + Captures a message. + + Alias of :py:meth:`sentry_sdk.Scope.capture_message`. + + :param message: The string to send as the message to Sentry. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_message( + message, level=level, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. + + Captures an exception. + + Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_exception( + error, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. + + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_span` instead. + + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + """ + scope = get_current_scope() + return scope.start_span(instrumenter=instrumenter, **kwargs) + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. + + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. + """ + scope = get_current_scope() + + # For backwards compatibility, we allow passing the scope as the hub. + # We need a major release to make this nice. (if someone searches the code: deprecated) + # Type checking disabled for this line because deprecated keys are not allowed in the type signature. + kwargs["hub"] = scope # type: ignore + + return scope.start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + ) -> "Transaction": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. + + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers=environ_or_headers, op=op, name=name, source=source + ) + + @overload + def push_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def push_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def push_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + if callback is not None: + with self.push_scope() as scope: + callback(scope) + return None + + return _ScopeManager(self) + + def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pops a scope layer from the stack. + + Try to use the context manager :py:meth:`push_scope` instead. + """ + rv = self._stack.pop() + assert self._stack, "stack must have at least one layer" + return rv + + @overload + def configure_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def configure_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def configure_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + scope = get_isolation_scope() + + if continue_trace: + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + def start_session( + self, + session_mode: str = "application", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_session` instead. + + Starts a new session. + """ + get_isolation_scope().start_session( + session_mode=session_mode, + ) + + def end_session(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.end_session` instead. + + Ends the current session if there is one. + """ + get_isolation_scope().end_session() + + def stop_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. + + Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + get_isolation_scope().stop_auto_session_tracking() + + def resume_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. + + Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + get_isolation_scope().resume_auto_session_tracking() + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.flush` instead. + + Alias for :py:meth:`sentry_sdk.client._Client.flush` + """ + return get_client().flush(timeout=timeout, callback=callback) + + def get_traceparent(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. + + Returns the traceparent either from the active span or from the scope. + """ + current_scope = get_current_scope() + traceparent = current_scope.get_traceparent() + + if traceparent is None: + isolation_scope = get_isolation_scope() + traceparent = isolation_scope.get_traceparent() + + return traceparent + + def get_baggage(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. + + Returns Baggage either from the active span or from the scope. + """ + current_scope = get_current_scope() + baggage = current_scope.get_baggage() + + if baggage is None: + isolation_scope = get_isolation_scope() + baggage = isolation_scope.get_baggage() + + if baggage is not None: + return baggage.serialize() + + return None + + def iter_trace_propagation_headers( + self, span: "Optional[Span]" = None + ) -> "Generator[Tuple[str, str], None, None]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. + + Return HTTP headers which allow propagation of trace data. Data taken + from the span representing the request, if available, or the current + span on the scope if not. + """ + return get_current_scope().iter_trace_propagation_headers( + span=span, + ) + + def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. + + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + return get_current_scope().trace_propagation_meta( + span=span, + ) + + +with _suppress_hub_deprecation_warning(): + # Suppress deprecation warning for the Hub here, since we still always + # import this module. + GLOBAL_HUB = Hub() +_local.set(GLOBAL_HUB) + + +# Circular imports +from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py new file mode 100644 index 0000000000..677d34a81e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +from threading import Lock +from typing import TYPE_CHECKING + +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union + + +_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) + + +_installer_lock = Lock() + +# Set of all integration identifiers we have attempted to install +_processed_integrations: "Set[str]" = set() + +# Set of all integration identifiers we have actually installed +_installed_integrations: "Set[str]" = set() + + +def _generate_default_integrations_iterator( + integrations: "List[str]", + auto_enabling_integrations: "List[str]", +) -> "Callable[[bool], Iterator[Type[Integration]]]": + def iter_default_integrations( + with_auto_enabling_integrations: bool, + ) -> "Iterator[Type[Integration]]": + """Returns an iterator of the default integration classes:""" + from importlib import import_module + + if with_auto_enabling_integrations: + all_import_strings = integrations + auto_enabling_integrations + else: + all_import_strings = integrations + + for import_string in all_import_strings: + try: + module, cls = import_string.rsplit(".", 1) + yield getattr(import_module(module), cls) + except (DidNotEnable, SyntaxError) as e: + logger.debug( + "Did not import default integration %s: %s", import_string, e + ) + + if isinstance(iter_default_integrations.__doc__, str): + for import_string in integrations: + iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) + + return iter_default_integrations + + +_DEFAULT_INTEGRATIONS = [ + # stdlib/base runtime integrations + "sentry_sdk.integrations.argv.ArgvIntegration", + "sentry_sdk.integrations.atexit.AtexitIntegration", + "sentry_sdk.integrations.dedupe.DedupeIntegration", + "sentry_sdk.integrations.excepthook.ExcepthookIntegration", + "sentry_sdk.integrations.logging.LoggingIntegration", + "sentry_sdk.integrations.modules.ModulesIntegration", + "sentry_sdk.integrations.stdlib.StdlibIntegration", + "sentry_sdk.integrations.threading.ThreadingIntegration", +] + +_AUTO_ENABLING_INTEGRATIONS = [ + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.anthropic.AnthropicIntegration", + "sentry_sdk.integrations.ariadne.AriadneIntegration", + "sentry_sdk.integrations.arq.ArqIntegration", + "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", + "sentry_sdk.integrations.boto3.Boto3Integration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.chalice.ChaliceIntegration", + "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", + "sentry_sdk.integrations.cohere.CohereIntegration", + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.fastapi.FastApiIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.gql.GQLIntegration", + "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", + "sentry_sdk.integrations.graphene.GrapheneIntegration", + "sentry_sdk.integrations.httpx.HttpxIntegration", + "sentry_sdk.integrations.httpx2.Httpx2Integration", + "sentry_sdk.integrations.huey.HueyIntegration", + "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", + "sentry_sdk.integrations.langchain.LangchainIntegration", + "sentry_sdk.integrations.langgraph.LanggraphIntegration", + "sentry_sdk.integrations.litestar.LitestarIntegration", + "sentry_sdk.integrations.loguru.LoguruIntegration", + "sentry_sdk.integrations.mcp.MCPIntegration", + "sentry_sdk.integrations.openai.OpenAIIntegration", + "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", + "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", + "sentry_sdk.integrations.pymongo.PyMongoIntegration", + "sentry_sdk.integrations.pyramid.PyramidIntegration", + "sentry_sdk.integrations.quart.QuartIntegration", + "sentry_sdk.integrations.redis.RedisIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", + "sentry_sdk.integrations.starlette.StarletteIntegration", + "sentry_sdk.integrations.starlite.StarliteIntegration", + "sentry_sdk.integrations.strawberry.StrawberryIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", +] + +iter_default_integrations = _generate_default_integrations_iterator( + integrations=_DEFAULT_INTEGRATIONS, + auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, +) + +del _generate_default_integrations_iterator + + +_MIN_VERSIONS = { + "aiohttp": (3, 4), + "aiomysql": (0, 3, 0), + "anthropic": (0, 16), + "ariadne": (0, 20), + "arq": (0, 23), + "asyncpg": (0, 23), + "beam": (2, 12), + "boto3": (1, 16), # botocore + "bottle": (0, 12), + "celery": (4, 4, 7), + "chalice": (1, 16, 0), + "clickhouse_driver": (0, 2, 0), + "cohere": (5, 4, 0), + "django": (1, 8), + "dramatiq": (1, 9), + "falcon": (1, 4), + "fastapi": (0, 79, 0), + "flask": (1, 1, 4), + "gql": (3, 4, 1), + "graphene": (3, 3), + "google_genai": (1, 29, 0), # google-genai + "grpc": (1, 32, 0), # grpcio + "httpx": (0, 16, 0), + "httpx2": (2, 0, 0), + "huggingface_hub": (0, 24, 7), + "langchain": (0, 1, 0), + "langgraph": (0, 6, 6), + "launchdarkly": (9, 8, 0), + "litellm": (1, 77, 5), + "loguru": (0, 7, 0), + "mcp": (1, 15, 0), + "openai": (1, 0, 0), + "openai_agents": (0, 0, 19), + "openfeature": (0, 7, 1), + "pydantic_ai": (1, 0, 0), + "pymongo": (3, 5, 0), + "pyreqwest": (0, 11, 6), + "quart": (0, 16, 0), + "ray": (2, 7, 0), + "requests": (2, 0, 0), + "rq": (0, 6), + "sanic": (0, 8), + "sqlalchemy": (1, 2), + "starlette": (0, 16), + "starlite": (1, 48), + "statsig": (0, 55, 3), + "strawberry": (0, 209, 5), + "tornado": (6, 0), + "typer": (0, 15), + "unleash": (6, 0, 1), +} + + +_INTEGRATION_DEACTIVATES = { + "langchain": {"openai", "anthropic", "google_genai"}, + "openai_agents": {"openai"}, + "pydantic_ai": {"openai", "anthropic"}, +} + + +def setup_integrations( + integrations: "Sequence[Integration]", + with_defaults: bool = True, + with_auto_enabling_integrations: bool = False, + disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, + options: "Optional[Dict[str, Any]]" = None, +) -> "Dict[str, Integration]": + """ + Given a list of integration instances, this installs them all. + + When `with_defaults` is set to `True` all default integrations are added + unless they were already provided before. + + `disabled_integrations` takes precedence over `with_defaults` and + `with_auto_enabling_integrations`. + + Some integrations are designed to automatically deactivate other integrations + in order to avoid conflicts and prevent duplicate telemetry from being collected. + For example, enabling the `langchain` integration will auto-deactivate both the + `openai` and `anthropic` integrations. + + Users can override this behavior by: + - Explicitly providing an integration in the `integrations=[]` list, or + - Disabling the higher-level integration via the `disabled_integrations` option. + """ + integrations = dict( + (integration.identifier, integration) for integration in integrations or () + ) + + logger.debug("Setting up integrations (with default = %s)", with_defaults) + + user_provided_integrations = set(integrations.keys()) + + # Integrations that will not be enabled + disabled_integrations = [ + integration if isinstance(integration, type) else type(integration) + for integration in disabled_integrations or [] + ] + + # Integrations that are not explicitly set up by the user. + used_as_default_integration = set() + + if with_defaults: + for integration_cls in iter_default_integrations( + with_auto_enabling_integrations + ): + if integration_cls.identifier not in integrations: + instance = integration_cls() + integrations[instance.identifier] = instance + used_as_default_integration.add(instance.identifier) + + disabled_integration_identifiers = { + integration.identifier for integration in disabled_integrations + } + + for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): + if ( + integration in integrations + and integration not in disabled_integration_identifiers + ): + for target in targets_to_deactivate: + if target not in user_provided_integrations: + for cls in iter_default_integrations(True): + if cls.identifier == target: + if cls not in disabled_integrations: + disabled_integrations.append(cls) + logger.debug( + "Auto-deactivating %s integration because %s integration is active", + target, + integration, + ) + + for identifier, integration in integrations.items(): + with _installer_lock: + if identifier not in _processed_integrations: + if type(integration) in disabled_integrations: + logger.debug("Ignoring integration %s", identifier) + else: + logger.debug( + "Setting up previously not enabled integration %s", identifier + ) + try: + type(integration).setup_once() + integration.setup_once_with_options(options) + except DidNotEnable as e: + if identifier not in used_as_default_integration: + raise + + logger.debug( + "Did not enable default integration %s: %s", identifier, e + ) + else: + _installed_integrations.add(identifier) + + _processed_integrations.add(identifier) + + integrations = { + identifier: integration + for identifier, integration in integrations.items() + if identifier in _installed_integrations + } + + for identifier in integrations: + logger.debug("Enabling integration %s", identifier) + + return integrations + + +def _check_minimum_version( + integration: "type[Integration]", + version: "Optional[tuple[int, ...]]", + package: "Optional[str]" = None, +) -> None: + package = package or integration.identifier + + if version is None: + raise DidNotEnable(f"Unparsable {package} version.") + + min_version = _MIN_VERSIONS.get(integration.identifier) + if min_version is None: + return + + if version < min_version: + raise DidNotEnable( + f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." + ) + + +class DidNotEnable(Exception): # noqa: N818 + """ + The integration could not be enabled due to a trivial user error like + `flask` not being installed for the `FlaskIntegration`. + + This exception is silently swallowed for default integrations, but reraised + for explicitly enabled integrations. + """ + + +class Integration(ABC): + """Baseclass for all integrations. + + To accept options for an integration, implement your own constructor that + saves those options on `self`. + """ + + install = None + """Legacy method, do not implement.""" + + identifier: "str" = None # type: ignore[assignment] + """String unique ID of integration type""" + + @staticmethod + @abstractmethod + def setup_once() -> None: + """ + Initialize the integration. + + This function is only called once, ever. Configuration is not available + at this point, so the only thing to do here is to hook into exception + handlers, and perhaps do monkeypatches. + + Inside those hooks `Integration.current` can be used to access the + instance again. + """ + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + """ + Called after setup_once in rare cases on the instance and with options since we don't have those available above. + """ + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py new file mode 100644 index 0000000000..7ff4657013 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py @@ -0,0 +1,187 @@ +import urllib +from enum import Enum +from typing import TYPE_CHECKING + +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.scope import should_send_default_pii + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from typing_extensions import Literal + + from sentry_sdk.utils import AnnotatedValue + + +class _RootPathInPath(Enum): + EXCLUDED = "excluded" + EITHER = "either" + + +def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": + """ + Extract headers from the ASGI scope, in the format that the Sentry protocol expects. + """ + headers: "Dict[str, str]" = {} + for raw_key, raw_value in asgi_scope["headers"]: + key = raw_key.decode("latin-1") + value = raw_value.decode("latin-1") + if key in headers: + headers[key] = headers[key] + ", " + value + else: + headers[key] = value + + return headers + + +def _get_path( + asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" +) -> "str": + if root_path_in_path is _RootPathInPath.EXCLUDED: + return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") + + # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 + path = asgi_scope["path"] + root_path = asgi_scope.get("root_path", "") + + if not root_path or path == root_path or path.startswith(root_path + "/"): + return path + + return root_path + path + + +def _get_url( + asgi_scope: "Dict[str, Any]", + default_scheme: "Literal['ws', 'http']", + host: "Optional[Union[AnnotatedValue, str]]", + path: str, +) -> str: + """ + Extract URL from the ASGI scope, without also including the querystring. + """ + scheme = asgi_scope.get("scheme", default_scheme) + + server = asgi_scope.get("server", None) + + if host: + return "%s://%s%s" % (scheme, host, path) + + if server is not None: + host, port = server + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) + if port != default_port: + return "%s://%s:%s%s" % (scheme, host, port, path) + return "%s://%s%s" % (scheme, host, path) + return path + + +def _get_query(asgi_scope: "Any") -> "Any": + """ + Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. + """ + qs = asgi_scope.get("query_string") + if not qs: + return None + return urllib.parse.unquote(qs.decode("latin-1")) + + +def _get_ip(asgi_scope: "Any") -> str: + """ + Extract IP Address from the ASGI scope based on request headers with fallback to scope client. + """ + headers = _get_headers(asgi_scope) + try: + return headers["x-forwarded-for"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return headers["x-real-ip"] + except KeyError: + pass + + return asgi_scope.get("client")[0] + + +def _get_request_data( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "Dict[str, Any]": + """ + Returns data related to the HTTP request from the ASGI scope. + """ + request_data: "Dict[str, Any]" = {} + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + request_data["method"] = asgi_scope.get("method") + + headers = _get_headers(asgi_scope) + + request_data["headers"] = _filter_headers( + headers, + use_annotated_value=False, + ) + + request_data["query_string"] = _get_query(asgi_scope) + + request_data["url"] = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} + + return request_data + + +def _get_request_attributes( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "dict[str, Any]": + """ + Return attributes related to the HTTP request from the ASGI scope. + """ + attributes: "dict[str, Any]" = {} + + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + if asgi_scope.get("method"): + attributes["http.request.method"] = asgi_scope["method"].upper() + + headers = _get_headers(asgi_scope) + + filtered_headers = _filter_headers(headers, use_annotated_value=False) + for header, value in filtered_headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + if should_send_default_pii(): + query = _get_query(asgi_scope) + if query: + attributes["http.query"] = query + + path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) + attributes["url.path"] = path + + url_without_query_string = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=path, + ) + query_string = _get_query(asgi_scope) + attributes["url.full"] = ( + f"{url_without_query_string}?{query_string}" + if query_string is not None + else url_without_query_string + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + ip = _get_ip(asgi_scope) + attributes["client.address"] = ip + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py new file mode 100644 index 0000000000..ad0ab7c734 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py @@ -0,0 +1,282 @@ +import json +from contextlib import contextmanager +from copy import deepcopy + +import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.data_collection import _apply_key_value_collection_filtering +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger + +try: + from django.http.request import RawPostDataException + + _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) +except ImportError: + RawPostDataException = None + _RAW_DATA_EXCEPTIONS = (ValueError,) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union + + from sentry_sdk._types import Event, HttpStatusCodeRange + + +SENSITIVE_ENV_KEYS = ( + "REMOTE_ADDR", + "HTTP_X_FORWARDED_FOR", + "HTTP_SET_COOKIE", + "HTTP_COOKIE", + "HTTP_AUTHORIZATION", + "HTTP_PROXY_AUTHORIZATION", + "HTTP_X_API_KEY", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", +) + +SENSITIVE_HEADERS = tuple( + x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") +) + +DEFAULT_HTTP_METHODS_TO_CAPTURE = ( + "CONNECT", + "DELETE", + "GET", + # "HEAD", # do not capture HEAD requests by default + # "OPTIONS", # do not capture OPTIONS requests by default + "PATCH", + "POST", + "PUT", + "TRACE", +) + + +# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support +@contextmanager +def nullcontext() -> "Iterator[None]": + yield + + +def request_body_within_bounds( + client: "Optional[sentry_sdk.client.BaseClient]", content_length: int +) -> bool: + if client is None: + return False + + bodies = client.options["max_request_body_size"] + return not ( + bodies == "never" + or (bodies == "small" and content_length > 10**3) + or (bodies == "medium" and content_length > 10**4) + ) + + +class RequestExtractor: + """ + Base class for request extraction. + """ + + # It does not make sense to make this class an ABC because it is not used + # for typing, only so that child classes can inherit common methods from + # it. Only some child classes implement all methods that raise + # NotImplementedError in this class. + + def __init__(self, request: "Any") -> None: + self.request = request + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + + content_length = self.content_length() + request_info = event.get("request", {}) + + if should_send_default_pii(): + request_info["cookies"] = dict(self.cookies()) + + if not request_body_within_bounds(client, content_length): + data = AnnotatedValue.removed_because_over_size_limit() + else: + # First read the raw body data + # It is important to read this first because if it is Django + # it will cache the body and then we can read the cached version + # again in parsed_body() (or json() or wherever). + raw_data = None + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # If DjangoRestFramework is used it already read the body for us + # so reading it here will fail. We can ignore this. + pass + + parsed_body = self.parsed_body() + if parsed_body is not None: + data = parsed_body + elif raw_data: + data = AnnotatedValue.removed_because_raw_data() + else: + data = None + + if data is not None: + request_info["data"] = data + + event["request"] = deepcopy(request_info) + + def content_length(self) -> int: + try: + return int(self.env().get("CONTENT_LENGTH", 0)) + except ValueError: + return 0 + + def cookies(self) -> "MutableMapping[str, Any]": + raise NotImplementedError() + + def raw_data(self) -> "Optional[Union[str, bytes]]": + raise NotImplementedError() + + def form(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + form = self.form() + except Exception: + form = None + try: + files = self.files() + except Exception: + files = None + + if form or files: + data = {} + if form: + data = dict(form.items()) + if files: + for key in files.keys(): + data[key] = AnnotatedValue.removed_because_raw_data() + + return data + + return self.json() + + def is_json(self) -> bool: + return _is_json_content_type(self.env().get("CONTENT_TYPE")) + + def json(self) -> "Optional[Any]": + try: + if not self.is_json(): + return None + + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # The body might have already been read, in which case this will + # fail + raw_data = None + + if raw_data is None: + return None + + if isinstance(raw_data, str): + return json.loads(raw_data) + else: + return json.loads(raw_data.decode("utf-8")) + except ValueError: + pass + + return None + + def files(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def size_of_file(self, file: "Any") -> int: + raise NotImplementedError() + + def env(self) -> "Dict[str, Any]": + raise NotImplementedError() + + +def _is_json_content_type(ct: "Optional[str]") -> bool: + mt = (ct or "").split(";", 1)[0] + return ( + mt == "application/json" + or (mt.startswith("application/")) + and mt.endswith("+json") + ) + + +def _filter_headers( + headers: "Mapping[str, str]", + use_annotated_value: bool = True, +) -> "Mapping[str, Union[AnnotatedValue, str]]": + client_options = sentry_sdk.get_client().options + + if has_data_collection_enabled(client_options): + data_collection_configuration = client_options["data_collection"] + + filtered = _apply_key_value_collection_filtering( + items=headers, + behaviour=data_collection_configuration["http_headers"]["request"], + ) + + for key in filtered: + if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): + filtered[key] = SENSITIVE_DATA_SUBSTITUTE + + return filtered + else: + if should_send_default_pii(): + return headers + + substitute: "Union[AnnotatedValue, str]" = ( + SENSITIVE_DATA_SUBSTITUTE + if not use_annotated_value + else AnnotatedValue.removed_because_over_size_limit() + ) + + return { + k: ( + v + if k.upper().replace("-", "_") not in SENSITIVE_HEADERS + else substitute + ) + for k, v in headers.items() + } + + +def _in_http_status_code_range( + code: object, code_ranges: "list[HttpStatusCodeRange]" +) -> bool: + for target in code_ranges: + if isinstance(target, int): + if code == target: + return True + continue + + try: + if code in target: + return True + except TypeError: + logger.warning( + "failed_request_status_codes has to be a list of integers or containers" + ) + + return False + + +class HttpCodeRangeContainer: + """ + Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. + Used for backwards compatibility with the old `failed_request_status_codes` option. + """ + + def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: + self._code_ranges = code_ranges + + def __contains__(self, item: object) -> bool: + return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..59bae92a60 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,509 @@ +import sys +import weakref +from functools import wraps + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import ( + _filter_headers, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + NoOpStreamedSpan, + SegmentSource, + SpanStatus, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import ( + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + parse_url, + parse_version, + reraise, + transaction_from_function, +) + +try: + import asyncio + + from aiohttp import ClientSession, TraceConfig + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Set + from types import SimpleNamespace + from typing import Any, ContextManager, Optional, Tuple, Union + + from aiohttp import TraceRequestEndParams, TraceRequestStartParams + from aiohttp.web_request import Request + from aiohttp.web_urldispatcher import UrlMappingMatchInfo + + from sentry_sdk._types import Attributes, Event, EventProcessor + from sentry_sdk.tracing import Span + from sentry_sdk.utils import ExcInfo + + +TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + origin = f"auto.http.{identifier}" + + def __init__( + self, + transaction_style: str = "handler_name", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self._failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(AIOHTTP_VERSION) + _check_minimum_version(AioHttpIntegration, version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + # In the aiohttp integration, all of their HTTP responses are Exceptions. + # Because they have to be raised and handled by the framework, we need to + # register the exceptions as control flow exceptions so that we don't + # accidentally overwrite a status of "ok" with "error". + _register_control_flow_exception(HTTPException) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(AioHttpIntegration) + if integration is None: + return await old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + # Scope data will not leak between requests because aiohttp + # create a task to wrap each request. + scope.generate_propagation_context() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + headers = dict(request.headers) + + span_ctx: "ContextManager[Union[Span, StreamedSpan]]" + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context({"aiohttp_request": request}) + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, + use_annotated_value=False, + ).items(): + header_attributes[ + f"http.request.header.{header.lower()}" + ] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + url_attributes = {} + if should_send_default_pii(): + url_attributes["url.full"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + url_attributes["url.path"] = request.path + + if request.query_string: + url_attributes["url.query"] = request.query_string + + client_address_attributes = {} + if should_send_default_pii() and request.remote: + client_address_attributes["client.address"] = request.remote + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) + + span_ctx = sentry_sdk.traces.start_span( + # If this name makes it to the UI, AIOHTTP's URL + # resolver did not find a route or died trying. + name="generic AIOHTTP request", + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": AioHttpIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE.value, + "http.request.method": request.method, + **url_attributes, + **client_address_attributes, + **header_attributes, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + name="generic AIOHTTP request", + source=TransactionSource.ROUTE, + origin=AioHttpIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"aiohttp_request": request}, + ) + + with span_ctx as span: + try: + response = await old_handle(self, request) + except HTTPException as e: + if isinstance(span, StreamedSpan) and not isinstance( + span, NoOpStreamedSpan + ): + span.set_attribute( + "http.response.status_code", e.status_code + ) + + if e.status_code >= 400: + span.status = SpanStatus.ERROR.value + else: + span.status = SpanStatus.OK.value + else: + # Since a NoOpStreamedSpan can end up here, we have to guard against it + # so this only gets set in the legacy transaction approach. + if not isinstance(span, NoOpStreamedSpan): + span.set_http_status(e.status_code) + + if ( + e.status_code + in integration._failed_request_status_codes + ): + _capture_exception() + raise + except (asyncio.CancelledError, ConnectionResetError): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR.value + else: + span.set_status(SPANSTATUS.CANCELLED) + raise + except Exception: + # This will probably map to a 500 but seems like we + # have no way to tell. Do not set span status. + reraise(*_capture_exception()) + + try: + # A valid response handler will return a valid response with a status. But, if the handler + # returns an invalid response (e.g. None), the line below will raise an AttributeError. + # Even though this is likely invalid, we need to handle this case to ensure we don't break + # the application. + response_status = response.status + except AttributeError: + pass + else: + if isinstance(span, StreamedSpan): + span.set_attribute( + "http.response.status_code", response_status + ) + span.status = ( + SpanStatus.ERROR.value + if response_status >= 400 + else SpanStatus.OK.value + ) + else: + span.set_http_status(response_status) + + return response + + Application._handle = sentry_app_handle + + old_urldispatcher_resolve = UrlDispatcher.resolve + + @wraps(old_urldispatcher_resolve) + async def sentry_urldispatcher_resolve( + self: "UrlDispatcher", request: "Request" + ) -> "UrlMappingMatchInfo": + rv = await old_urldispatcher_resolve(self, request) + + integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) + if integration is None: + return rv + + name = None + + try: + if integration.transaction_style == "handler_name": + name = transaction_from_function(rv.handler) + elif integration.transaction_style == "method_and_path_pattern": + route_info = rv.get_info() + pattern = route_info.get("path") or route_info.get("formatter") + name = "{} {}".format(request.method, pattern) + except Exception: + pass + + if name is not None: + current_span = sentry_sdk.get_current_span() + if isinstance(current_span, StreamedSpan) and not isinstance( + current_span, NoOpStreamedSpan + ): + current_span._segment.name = name + current_span._segment.set_attribute( + "sentry.span.source", + SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, + ) + else: + current_scope = sentry_sdk.get_current_scope() + current_scope.set_transaction_name( + name, + source=SOURCE_FOR_STYLE[integration.transaction_style], + ) + + return rv + + UrlDispatcher.resolve = sentry_urldispatcher_resolve + + old_client_session_init = ClientSession.__init__ + + @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) + def init(*args: "Any", **kwargs: "Any") -> None: + client_trace_configs = list(kwargs.get("trace_configs") or ()) + trace_config = create_trace_config() + client_trace_configs.append(trace_config) + + kwargs["trace_configs"] = client_trace_configs + return old_client_session_init(*args, **kwargs) + + ClientSession.__init__ = init + + +def create_trace_config() -> "TraceConfig": + async def on_request_start( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestStartParams", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(AioHttpIntegration) is None: + return + + method = params.method.upper() + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(params.url), sanitize=False) + + span_name = "%s %s" % ( + method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ) + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(client.options): + attributes: "Attributes" = { + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": AioHttpIntegration.origin, + "http.request.method": method, + } + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + attributes["url.path"] = params.url.path + + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) + else: + legacy_span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=span_name, + origin=AioHttpIntegration.origin, + ) + legacy_span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + legacy_span.set_data("url", parsed_url.url) + legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span = legacy_span + + if should_propagate_trace(client, str(params.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=params.url + ) + ) + if key == BAGGAGE_HEADER_NAME and params.headers.get( + BAGGAGE_HEADER_NAME + ): + # do not overwrite any existing baggage, just append to it + params.headers[key] += "," + value + else: + params.headers[key] = value + + trace_config_ctx.span = span + + async def on_request_end( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestEndParams", + ) -> None: + if trace_config_ctx.span is None: + return + + span = trace_config_ctx.span + status = int(params.response.status) + + if isinstance(span, StreamedSpan): + span.set_attribute("http.response.status_code", status) + span.status = ( + SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value + ) + + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.set_http_status(status) + span.set_data("reason", params.response.reason) + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + trace_config = TraceConfig() + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + + return trace_config + + +def _make_request_processor( + weak_request: "weakref.ReferenceType[Request]", +) -> "EventProcessor": + def aiohttp_processor( + event: "Event", + hint: "dict[str, Tuple[type, BaseException, Any]]", + ) -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + request_info = event.setdefault("request", {}) + + request_info["url"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote} + request_info["headers"] = _filter_headers(dict(request.headers)) + + # Just attach raw data here if it is within bounds, if available. + # Unfortunately there's no way to get structured data from aiohttp + # without awaiting on some coroutine. + request_info["data"] = get_aiohttp_request_data(request) + + return event + + return aiohttp_processor + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "aiohttp", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + return exc_info + + +BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" + + +def get_aiohttp_request_data( + request: "Request", +) -> "Union[Optional[str], AnnotatedValue]": + bytes_body = request._read_bytes + + if bytes_body is not None: + # we have body to show + if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): + return AnnotatedValue.substituted_because_over_size_limit() + + encoding = request.charset or "utf-8" + return bytes_body.decode(encoding, "replace") + + if request.can_read_body: + # body exists but we can't show it + return BODY_NOT_READ_MESSAGE + + # request has no body + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py new file mode 100644 index 0000000000..49459268e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Any, Awaitable, Callable, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import aiomysql # type: ignore[import-not-found] + from aiomysql.connection import Connection # type: ignore[import-not-found] + from aiomysql.cursors import Cursor # type: ignore[import-not-found] +except ImportError: + raise DidNotEnable("aiomysql not installed.") + + +class AioMySQLIntegration(Integration): + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) + + Cursor.execute = _wrap_execute(Cursor.execute) + Cursor.executemany = _wrap_executemany(Cursor.executemany) + + # Patch Connection._connect — this catches ALL connections: + # - aiomysql.connect() + # - aiomysql.create_pool() (pool.py does `from .connection import connect` + # which ultimately calls Connection._connect) + # - Reconnects + Connection._connect = _wrap_connect(Connection._connect) + + +T = TypeVar("T") + + +def _normalize_query(query: str | bytes | bytearray) -> str: + if isinstance(query, (bytes, bytearray)): + query = query.decode("utf-8", errors="replace") + return " ".join(query.split()) + + +def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.execute to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + + # Skip if flagged by executemany (avoids double-recording). + # Do NOT reset the flag here — it must stay True for the entire + # duration of executemany, which may call execute multiple times + # in a loop (non-INSERT fallback). Only _wrap_executemany's + # finally block should clear it. + if getattr(cursor, "_sentry_skip_next_execute", False): + return await f(*args, **kwargs) + + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = params if integration and integration._record_params else None + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.executemany to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = ( + seq_of_params if integration and integration._record_params else None + ) + param_style = "pyformat" if params_list else None + + # Prevent double-recording: _do_execute_many calls self.execute internally + cursor._sentry_skip_next_execute = True + try: + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=True, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + finally: + cursor._sentry_skip_next_execute = False + + return _inner + + +def _get_connection(cursor: Any) -> Any: + """Get the underlying connection from a cursor.""" + return getattr(cursor, "connection", None) + + +def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Connection._connect to capture connection spans.""" + + async def _inner(self: "Connection") -> T: + client = sentry_sdk.get_client() + if client.get_integration(AioMySQLIntegration) is None: + return await f(self) + + if has_span_streaming_enabled(client.options): + breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) + + span_attributes: dict[str, Any] = { + "sentry.op": OP.DB, + "sentry.origin": AioMySQLIntegration.origin, + } | breadcrumb_data + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=breadcrumb_data + ) + res = await f(self) + else: + connect_data = _get_connect_data(self) + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + _set_db_data(span, self) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", + category="query", + data=connect_data, + ) + res = await f(self) + + return res + + return _inner + + +def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: + if use_streaming_keys: + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + data: dict[str, Any] = { + db_system: "mysql", + SPANDATA.DB_DRIVER_NAME: "aiomysql", + } + + host = getattr(conn, "host", None) + if host is not None: + data[SPANDATA.SERVER_ADDRESS] = host + + port = getattr(conn, "port", None) + if port is not None: + data[SPANDATA.SERVER_PORT] = port + + database = getattr(conn, "db", None) + if database is not None: + data[db_name] = database + + user = getattr(conn, "user", None) + if user is not None: + data[SPANDATA.DB_USER] = user + + return data + + +def _set_db_data(span: Any, conn: Any) -> None: + """Set database-related span data from connection object.""" + if isinstance(span, StreamedSpan): + set_value = span.set_attribute + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + set_value = span.set_data + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + set_value(db_system, "mysql") + set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") + + host = getattr(conn, "host", None) + if host is not None: + set_value(SPANDATA.SERVER_ADDRESS, host) + + port = getattr(conn, "port", None) + if port is not None: + set_value(SPANDATA.SERVER_PORT, port) + + database = getattr(conn, "db", None) + if database is not None: + set_value(db_name, database) + + user = getattr(conn, "user", None) + if user is not None: + set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py new file mode 100644 index 0000000000..dfa4aef34c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py @@ -0,0 +1,1154 @@ +import json +import sys +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_anthropic_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + package_version, + reraise, + safe_serialize, +) + +try: + try: + from anthropic import NotGiven + except ImportError: + NotGiven = None + + try: + from anthropic import Omit + except ImportError: + Omit = None + + from anthropic import AsyncStream, Stream + from anthropic.lib.streaming import ( + AsyncMessageStream, + AsyncMessageStreamManager, + MessageStream, + MessageStreamManager, + ) + from anthropic.resources import AsyncMessages, Messages + from anthropic.types import ( + ContentBlockDeltaEvent, + ContentBlockStartEvent, + ContentBlockStopEvent, + MessageDeltaEvent, + MessageStartEvent, + MessageStopEvent, + ) + + if TYPE_CHECKING: + from anthropic.types import MessageStreamEvent, TextBlockParam +except ImportError: + raise DidNotEnable("Anthropic not installed") + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Iterator, + Optional, + Union, + ) + + from anthropic.types import ( + MessageParam, + ModelParam, + RawMessageStreamEvent, + TextBlockParam, + ToolUnionParam, + ) + + from sentry_sdk._types import TextPart + + +class _RecordedUsage: + output_tokens: int = 0 + input_tokens: int = 0 + cache_write_input_tokens: "Optional[int]" = 0 + cache_read_input_tokens: "Optional[int]" = 0 + + +class _StreamSpanContext: + """ + Sets accumulated data on the stream's span and finishes the span on exit. + Is a no-op if the stream has no span set, i.e., when the span has already been finished. + """ + + def __init__( + self, + stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", + # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). + guaranteed_streaming_state: bool = False, + ) -> None: + self._stream = stream + self._guaranteed_streaming_state = guaranteed_streaming_state + + def __enter__(self) -> "_StreamSpanContext": + return self + + def __exit__( + self, + exc_type: "Optional[type[BaseException]]", + exc_val: "Optional[BaseException]", + exc_tb: "Optional[Any]", + ) -> None: + with capture_internal_exceptions(): + if not hasattr(self._stream, "_span"): + return + + if not self._guaranteed_streaming_state and not hasattr( + self._stream, "_model" + ): + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + return + + _set_streaming_output_data( + span=self._stream._span, + integration=self._stream._integration, + model=self._stream._model, + usage=self._stream._usage, + content_blocks=self._stream._content_blocks, + response_id=self._stream._response_id, + finish_reason=self._stream._finish_reason, + ) + + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + + +class AnthropicIntegration(Integration): + identifier = "anthropic" + origin = f"auto.ai.{identifier}" + + def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + version = package_version("anthropic") + _check_minimum_version(AnthropicIntegration, version) + + """ + client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. + Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. + The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, + information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. + + The span can be finished in two places: + - When the user exits the context manager or directly calls close(), the patched close() finishes the span. + - When iteration ends, the finally block in the _iterator wrapper finishes the span. + + Both paths may run. For example, the context manager exit can follow iterator exhaustion. + """ + Messages.create = _wrap_message_create(Messages.create) + Stream.close = _wrap_close(Stream.close) + + AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) + AsyncStream.close = _wrap_async_close(AsyncStream.close) + + """ + client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. + """ + Messages.stream = _wrap_message_stream(Messages.stream) + MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( + MessageStreamManager.__enter__ + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. + if not issubclass(MessageStream, Stream): + MessageStream.close = _wrap_close(MessageStream.close) + + AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) + AsyncMessageStreamManager.__aenter__ = ( + _wrap_async_message_stream_manager_aenter( + AsyncMessageStreamManager.__aenter__ + ) + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. + if not issubclass(AsyncMessageStream, AsyncStream): + AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "anthropic", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": + """ + Get token usage from the Anthropic response. + Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) + """ + input_tokens = 0 + output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 + if hasattr(result, "usage"): + usage = result.usage + if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): + input_tokens = usage.input_tokens + if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): + output_tokens = usage.output_tokens + if hasattr(usage, "cache_read_input_tokens") and isinstance( + usage.cache_read_input_tokens, int + ): + cache_read_input_tokens = usage.cache_read_input_tokens + if hasattr(usage, "cache_creation_input_tokens") and isinstance( + usage.cache_creation_input_tokens, int + ): + cache_write_input_tokens = usage.cache_creation_input_tokens + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens so downstream cost calculations + # (input_tokens - cached) don't produce negative values. + input_tokens += cache_read_input_tokens + cache_write_input_tokens + + return ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) + + +def _collect_ai_data( + event: "MessageStreamEvent", + model: "str | None", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": + """ + Collect model information, token usage, and collect content blocks from the AI streaming response. + """ + with capture_internal_exceptions(): + if hasattr(event, "type"): + if event.type == "content_block_start": + pass + elif event.type == "content_block_delta": + if hasattr(event.delta, "text"): + content_blocks.append(event.delta.text) + elif hasattr(event.delta, "partial_json"): + content_blocks.append(event.delta.partial_json) + elif event.type == "content_block_stop": + pass + + # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. + # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 + if event.type == "message_start": + model = event.message.model or model + response_id = event.message.id + + incoming_usage = event.message.usage + usage.output_tokens = incoming_usage.output_tokens + usage.input_tokens = incoming_usage.input_tokens + + usage.cache_write_input_tokens = getattr( + incoming_usage, "cache_creation_input_tokens", None + ) + usage.cache_read_input_tokens = getattr( + incoming_usage, "cache_read_input_tokens", None + ) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + # Counterintuitive, but message_delta contains cumulative token counts :) + if event.type == "message_delta": + usage.output_tokens = event.usage.output_tokens + + # Update other usage fields if they exist in the event + input_tokens = getattr(event.usage, "input_tokens", None) + if input_tokens is not None: + usage.input_tokens = input_tokens + + cache_creation_input_tokens = getattr( + event.usage, "cache_creation_input_tokens", None + ) + if cache_creation_input_tokens is not None: + usage.cache_write_input_tokens = cache_creation_input_tokens + + cache_read_input_tokens = getattr( + event.usage, "cache_read_input_tokens", None + ) + if cache_read_input_tokens is not None: + usage.cache_read_input_tokens = cache_read_input_tokens + # TODO: Record event.usage.server_tool_use + + if event.delta.stop_reason is not None: + finish_reason = event.delta.stop_reason + + return (model, usage, content_blocks, response_id, finish_reason) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + +def _transform_anthropic_content_block( + content_block: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Transform an Anthropic content block using the Anthropic-specific transformer, + with special handling for Anthropic's text-type documents. + """ + # Handle Anthropic's text-type documents specially (not covered by shared function) + if content_block.get("type") == "document": + source = content_block.get("source") + if isinstance(source, dict) and source.get("type") == "text": + return { + "type": "text", + "text": source.get("data", ""), + } + + # Use Anthropic-specific transformation + result = transform_anthropic_content_part(content_block) + return result if result is not None else content_block + + +def _transform_system_instructions( + system_instructions: "Union[str, Iterable[TextBlockParam]]", +) -> "list[TextPart]": + if isinstance(system_instructions, str): + return [ + { + "type": "text", + "content": system_instructions, + } + ] + + return [ + { + "type": "text", + "content": instruction["text"], + } + for instruction in system_instructions + if isinstance(instruction, dict) and "text" in instruction + ] + + +def _set_common_input_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Optional[Union[str, Iterable[TextBlockParam]]]", + temperature: "Optional[float]", + top_k: "Optional[int]", + top_p: "Optional[float]", + tools: "Optional[Iterable[ToolUnionParam]]", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if ( + messages is not None + and len(messages) > 0 # type: ignore + and should_send_default_pii() + and integration.include_prompts + ): + if isinstance(system, str) or isinstance(system, Iterable): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system)), + ) + + normalized_messages = [] + for message in messages: + if ( + message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER + and "content" in message + and isinstance(message["content"], (list, tuple)) + ): + transformed_content = [] + for item in message["content"]: + # Skip tool_result items - they can contain images/documents + # with nested structures that are difficult to redact properly + if isinstance(item, dict) and item.get("type") == "tool_result": + continue + + # Transform content blocks (images, documents, etc.) + transformed_content.append( + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + ) + + # If there are non-tool-result items, add them as a message + if transformed_content: + normalized_messages.append( + { + "role": message.get("role"), + "content": transformed_content, + } + ) + else: + # Transform content for non-list messages or assistant messages + transformed_message = message.copy() + if "content" in transformed_message: + content = transformed_message["content"] + if isinstance(content, (list, tuple)): + transformed_message["content"] = [ + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + for item in content + ] + normalized_messages.append(transformed_message) + + role_normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(role_normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else role_normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + if model is not None and _is_given(model): + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + if top_k is not None and _is_given(top_k): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore + set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) + + +def _set_create_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "AnthropicIntegration", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=kwargs.get("max_tokens"), # type: ignore + messages=kwargs.get("messages"), # type: ignore + model=kwargs.get("model"), + system=kwargs.get("system"), + temperature=kwargs.get("temperature"), + top_k=kwargs.get("top_k"), + top_p=kwargs.get("top_p"), + tools=kwargs.get("tools"), + ) + + +def _wrap_synchronous_message_iterator( + stream: "Union[Stream, MessageStream]", + iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +async def _wrap_asynchronous_message_iterator( + stream: "Union[AsyncStream, AsyncMessageStream]", + iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + async for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +def _set_output_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + model: "str | None", + input_tokens: "int | None", + output_tokens: "int | None", + cache_read_input_tokens: "int | None", + cache_write_input_tokens: "int | None", + content_blocks: "list[Any]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> None: + """ + Set output data for the span based on the AI response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + if finish_reason is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in content_blocks: + if output["type"] == "text": + output_messages["response"].append(output["text"]) + elif output["type"] == "tool_use": + output_messages["tool"].append(output) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + record_token_usage( + span, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_tokens_cached=cache_read_input_tokens, + input_tokens_cache_write=cache_write_input_tokens, + ) + + +def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, Stream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_synchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +async def _sentry_patched_create_async( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return await f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, AsyncStream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_asynchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +def _wrap_message_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return _sentry_patched_create_sync(f, *args, **kwargs) + + return _sentry_wrapped_create_sync + + +def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: + """ + Initialize fields for accumulating output on the Stream instance. + """ + if not hasattr(stream, "_model"): + stream._model = None + stream._usage = _RecordedUsage() + stream._content_blocks = [] + stream._response_id = None + stream._finish_reason = None + + +def _accumulate_event_data( + stream: "Union[Stream, MessageStream]", + event: "Union[RawMessageStreamEvent, MessageStreamEvent]", +) -> None: + """ + Update accumulated output from a single stream event. + """ + (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( + event, + stream._model, + stream._usage, + stream._content_blocks, + stream._response_id, + stream._finish_reason, + ) + + stream._model = model + stream._usage = usage + stream._content_blocks = content_blocks + stream._response_id = response_id + stream._finish_reason = finish_reason + + +def _set_streaming_output_data( + span: "Span", + integration: "AnthropicIntegration", + model: "Optional[str]", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "Optional[str]", + finish_reason: "Optional[str]", +) -> None: + """ + Set output attributes on the AI Client Span. + """ + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + response_id=response_id, + finish_reason=finish_reason, + ) + + +def _wrap_close( + f: "Callable[..., None]", +) -> "Callable[..., None]": + """ + Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. + """ + + def close(self: "Union[Stream, MessageStream]") -> None: + with _StreamSpanContext(self): + return f(self) + + return close + + +def _wrap_message_create_async(f: "Any") -> "Any": + @wraps(f) + async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return await _sentry_patched_create_async(f, *args, **kwargs) + + return _sentry_wrapped_create_async + + +def _wrap_async_close( + f: "Callable[..., Awaitable[None]]", +) -> "Callable[..., Awaitable[None]]": + """ + Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. + """ + + async def close(self: "AsyncStream") -> None: + with _StreamSpanContext(self): + return await f(self) + + return close + + +def _wrap_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_message_stream_manager_enter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": + if not hasattr(self, "_max_tokens"): + return f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return f(self) + + if self._messages is None: + return f(self) + + try: + iter(self._messages) + except TypeError: + return f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_synchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_enter + + +def _wrap_async_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream( + *args: "Any", **kwargs: "Any" + ) -> "AsyncMessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + async def _sentry_patched_aenter( + self: "AsyncMessageStreamManager", + ) -> "AsyncMessageStream": + if not hasattr(self, "_max_tokens"): + return await f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return await f(self) + + if self._messages is None: + return await f(self) + + try: + iter(self._messages) + except TypeError: + return await f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = await f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_asynchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_aenter + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different anthropic versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py new file mode 100644 index 0000000000..0215ffa093 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py @@ -0,0 +1,28 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class ArgvIntegration(Integration): + identifier = "argv" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: + extra = event.setdefault("extra", {}) + # If some event processor decided to set extra to e.g. an + # `int`, don't crash. Not here. + if isinstance(extra, dict): + extra["sys.argv"] = sys.argv + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py new file mode 100644 index 0000000000..1ce228d3a5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py @@ -0,0 +1,167 @@ +from importlib import import_module + +import sentry_sdk +from sentry_sdk import capture_event, get_client +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + # importing like this is necessary due to name shadowing in ariadne + # (ariadne.graphql is also a function) + ariadne_graphql = import_module("ariadne.graphql") +except ImportError: + raise DidNotEnable("ariadne is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, List, Optional + + from ariadne.types import ( # type: ignore + GraphQLError, + GraphQLResult, + GraphQLSchema, + QueryParser, + ) + from graphql.language.ast import DocumentNode + + from sentry_sdk._types import Event, EventProcessor + + +class AriadneIntegration(Integration): + identifier = "ariadne" + + @staticmethod + def setup_once() -> None: + version = package_version("ariadne") + _check_minimum_version(AriadneIntegration, version) + + ignore_logger("ariadne") + + _patch_graphql() + + +def _patch_graphql() -> None: + old_parse_query = ariadne_graphql.parse_query + old_handle_errors = ariadne_graphql.handle_graphql_errors + old_handle_query_result = ariadne_graphql.handle_query_result + + @ensure_integration_enabled(AriadneIntegration, old_parse_query) + def _sentry_patched_parse_query( + context_value: "Optional[Any]", + query_parser: "Optional[QueryParser]", + data: "Any", + ) -> "DocumentNode": + event_processor = _make_request_event_processor(data) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + result = old_parse_query(context_value, query_parser, data) + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_errors) + def _sentry_patched_handle_graphql_errors( + errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + result = old_handle_errors(errors, *args, **kwargs) + + event_processor = _make_response_event_processor(result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) + def _sentry_patched_handle_query_result( + result: "Any", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + query_result = old_handle_query_result(result, *args, **kwargs) + + event_processor = _make_response_event_processor(query_result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return query_result + + ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore + ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore + ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore + + +def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": + """Add request data and api_target to events.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + if not isinstance(data, dict): + return event + + with capture_internal_exceptions(): + try: + content_length = int( + (data.get("headers") or {}).get("Content-Length", 0) + ) + except (TypeError, ValueError): + return event + + if should_send_default_pii() and request_body_within_bounds( + get_client(), content_length + ): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + request_info["data"] = data + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + return inner + + +def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": + """Add response data to the event's response context.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii() and response.get("errors"): + contexts = event.setdefault("contexts", {}) + contexts["response"] = { + "data": response, + } + + return event + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py new file mode 100644 index 0000000000..da03bafb8b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py @@ -0,0 +1,277 @@ +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Transaction, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +try: + import arq.worker + from arq.connections import ArqRedis + from arq.version import VERSION as ARQ_VERSION + from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker +except ImportError: + raise DidNotEnable("Arq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from arq.cron import CronJob + from arq.jobs import Job + from arq.typing import WorkerCoroutine + from arq.worker import Function + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) + + +class ArqIntegration(Integration): + identifier = "arq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + try: + if isinstance(ARQ_VERSION, str): + version = parse_version(ARQ_VERSION) + else: + version = ARQ_VERSION.version[:2] + + except (TypeError, ValueError): + version = None + + _check_minimum_version(ArqIntegration, version) + + patch_enqueue_job() + patch_run_job() + patch_create_worker() + + _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore + + ignore_logger("arq.worker") + + +def patch_enqueue_job() -> None: + old_enqueue_job = ArqRedis.enqueue_job + original_kwdefaults = old_enqueue_job.__kwdefaults__ + + async def _sentry_enqueue_job( + self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" + ) -> "Optional[Job]": + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_enqueue_job(self, function, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=function, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_ARQ, + "sentry.origin": ArqIntegration.origin, + }, + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults + ArqRedis.enqueue_job = _sentry_enqueue_job + + +def patch_run_job() -> None: + old_run_job = Worker.run_job + + async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_run_job(self, job_id, score) + + with sentry_sdk.isolation_scope() as scope: + scope._name = "arq" + scope.clear_breadcrumbs() + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name="unknown arq task", + attributes={ + "sentry.op": OP.QUEUE_TASK_ARQ, + "sentry.origin": ArqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job_id, + }, + parent_span=None, + ): + return await old_run_job(self, job_id, score) + + transaction = Transaction( + name="unknown arq task", + status="ok", + op=OP.QUEUE_TASK_ARQ, + source=TransactionSource.TASK, + origin=ArqIntegration.origin, + ) + + with sentry_sdk.start_transaction(transaction): + return await old_run_job(self, job_id, score) + + Worker.run_job = _sentry_run_job + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + + if scope.transaction is not None: + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + scope.transaction.set_status(SPANSTATUS.ABORTED) + return + + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": ArqIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + if scope.transaction is not None: + scope.transaction.name = ctx["job_name"] + event["transaction"] = ctx["job_name"] + + tags = event.setdefault("tags", {}) + tags["arq_task_id"] = ctx["job_id"] + tags["arq_task_retry"] = ctx["job_try"] > 1 + extra = event.setdefault("extra", {}) + extra["arq-job"] = { + "task": ctx["job_name"], + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": ctx["job_try"], + } + + return event + + return event_processor + + +def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": + async def _sentry_coroutine( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(ArqIntegration) + if integration is None: + return await coroutine(ctx, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + scope = sentry_sdk.get_current_scope() + span = scope.streamed_span + if span is not None: + span.name = name + + scope.set_transaction_name(name) + + sentry_sdk.get_isolation_scope().add_event_processor( + _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) + ) + + try: + result = await coroutine(ctx, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_coroutine + + +def patch_create_worker() -> None: + old_create_worker = arq.worker.create_worker + + @ensure_integration_enabled(ArqIntegration, old_create_worker) + def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": + settings_cls = args[0] if args else kwargs.get("settings_cls") + + if isinstance(settings_cls, dict): + if "functions" in settings_cls: + settings_cls["functions"] = [ + _get_arq_function(func) + for func in settings_cls.get("functions", []) + ] + if "cron_jobs" in settings_cls: + settings_cls["cron_jobs"] = [ + _get_arq_cron_job(cron_job) + for cron_job in settings_cls.get("cron_jobs", []) + ] + + if hasattr(settings_cls, "functions"): + settings_cls.functions = [ # type: ignore[union-attr] + _get_arq_function(func) + for func in settings_cls.functions # type: ignore[union-attr] + ] + if hasattr(settings_cls, "cron_jobs"): + settings_cls.cron_jobs = [ # type: ignore[union-attr] + _get_arq_cron_job(cron_job) + for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] + ] + + if "functions" in kwargs: + kwargs["functions"] = [ + _get_arq_function(func) for func in kwargs.get("functions", []) + ] + if "cron_jobs" in kwargs: + kwargs["cron_jobs"] = [ + _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) + ] + + return old_create_worker(*args, **kwargs) + + arq.worker.create_worker = _sentry_create_worker + + +def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": + arq_func = arq.worker.func(func) + arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) + + return arq_func + + +def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": + cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) + + return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py new file mode 100644 index 0000000000..8b1ff5e2a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py @@ -0,0 +1,543 @@ +""" +An ASGI middleware. + +Based on Tom Christie's `sentry-asgi `. +""" + +import inspect +import sys +from copy import deepcopy +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._asgi_common import ( + _get_headers, + _get_ip, + _get_path, + _get_request_attributes, + _get_request_data, + _get_url, + _RootPathInPath, +) +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + SegmentSource, + StreamedSpan, +) +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + Transaction, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + ContextVar, + _get_installed_modules, + capture_internal_exceptions, + event_from_exception, + logger, + qualname_from_function, + reraise, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, ContextManager, Dict, Optional, Tuple, Union + + from sentry_sdk._types import Attributes, Event, Hint + from sentry_sdk.tracing import Span + + +_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") + +_DEFAULT_TRANSACTION_NAME = "generic ASGI request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 +if sys.version_info >= (3, 14): + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": mechanism_type, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _looks_like_asgi3(app: "Any") -> bool: + """ + Try to figure out if an application object supports ASGI3. + + This is how uvicorn figures out the application version as well. + """ + if inspect.isclass(app): + return hasattr(app, "__await__") + elif inspect.isfunction(app): + return iscoroutinefunction(app) + else: + call = getattr(app, "__call__", None) # noqa + return iscoroutinefunction(call) + + +class SentryAsgiMiddleware: + __slots__ = ( + "app", + "__call__", + "transaction_style", + "mechanism_type", + "span_origin", + "http_methods_to_capture", + "root_path_in_path", + ) + + def __init__( + self, + app: "Any", + unsafe_context_data: bool = False, + transaction_style: str = "endpoint", + mechanism_type: str = "asgi", + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + asgi_version: "Optional[int]" = None, + root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, + ) -> None: + """ + Instrument an ASGI application with Sentry. Provides HTTP/websocket + data to sent events and basic handling for exceptions bubbling up + through the middleware. + + :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. + """ + if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The ASGI middleware for Sentry requires Python 3.7+ " + "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + + asgi_middleware_while_using_starlette_or_fastapi = ( + mechanism_type == "asgi" and "starlette" in _get_installed_modules() + ) + if asgi_middleware_while_using_starlette_or_fastapi: + logger.warning( + "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " + "Please remove 'SentryAsgiMiddleware' from your project. " + "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." + ) + + self.transaction_style = transaction_style + self.mechanism_type = mechanism_type + self.span_origin = span_origin + self.app = app + self.http_methods_to_capture = http_methods_to_capture + self.root_path_in_path = root_path_in_path + + if asgi_version is None: + if _looks_like_asgi3(app): + asgi_version = 3 + else: + asgi_version = 2 + + if asgi_version == 3: + self.__call__ = self._run_asgi3 + elif asgi_version == 2: + self.__call__ = self._run_asgi2 # type: ignore + + def _capture_lifespan_exception(self, exc: Exception) -> None: + """Capture exceptions raise in application lifespan handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _capture_request_exception(self, exc: Exception) -> None: + """Capture exceptions raised in incoming request handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _run_asgi2(self, scope: "Any") -> "Any": + async def inner(receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=2) + + return inner + + async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=3) + + async def _run_app( + self, scope: "Any", receive: "Any", send: "Any", asgi_version: int + ) -> "Any": + is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) + is_lifespan = scope["type"] == "lifespan" + if is_recursive_asgi_middleware or is_lifespan: + try: + if asgi_version == 2: + return await self.app(scope)(receive, send) + else: + return await self.app(scope, receive, send) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_lifespan_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_lifespan_exception(exc) + reraise(*exc_info) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _asgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as sentry_scope: + with track_session(sentry_scope, session_mode="request"): + sentry_scope.clear_breadcrumbs() + sentry_scope._name = "asgi" + processor = partial(self.event_processor, asgi_scope=scope) + sentry_scope.add_event_processor(processor) + + ty = scope["type"] + ( + transaction_name, + transaction_source, + ) = self._get_transaction_name_and_source( + self.transaction_style, + scope, + ) + + method = scope.get("method", "").upper() + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + if span_streaming: + segment: "Optional[StreamedSpan]" = None + attributes: "Attributes" = { + "sentry.span.source": getattr( + transaction_source, "value", transaction_source + ), + "sentry.origin": self.span_origin, + "network.protocol.name": ty, + } + + if scope.get("client") and should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) + + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + sentry_sdk.traces.continue_trace(_get_headers(scope)) + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = f"{ty}.server" + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + else: + sentry_sdk.traces.new_trace() + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = OP.HTTP_SERVER + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + + span_ctx = segment or nullcontext() + + else: + transaction = None + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + transaction = continue_trace( + _get_headers(scope), + op="{}.server".format(ty), + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + else: + transaction = Transaction( + op=OP.HTTP_SERVER, + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + + if transaction: + transaction.set_tag("asgi.type", ty) + + span_ctx = ( + sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"asgi_scope": scope}, + ) + if transaction is not None + else nullcontext() + ) + + with span_ctx as span: + if isinstance(span, StreamedSpan): + for attribute, value in _get_request_attributes( + scope, + root_path_in_path=self.root_path_in_path, + ).items(): + span.set_attribute(attribute, value) + + try: + + async def _sentry_wrapped_send( + event: "Dict[str, Any]", + ) -> "Any": + if span is not None: + is_http_response = ( + event.get("type") == "http.response.start" + and "status" in event + ) + if is_http_response: + if isinstance(span, StreamedSpan): + span.status = ( + "error" + if event["status"] >= 400 + else "ok" + ) + span.set_attribute( + "http.response.status_code", + event["status"], + ) + else: + span.set_http_status(event["status"]) + + return await send(event) + + if asgi_version == 2: + return await self.app(scope)( + receive, _sentry_wrapped_send + ) + else: + return await self.app( + scope, receive, _sentry_wrapped_send + ) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_request_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_request_exception(exc) + reraise(*exc_info) + + finally: + if isinstance(span, StreamedSpan): + already_set = ( + span is not None + and span.name != _DEFAULT_TRANSACTION_NAME + and span.get_attributes().get("sentry.span.source") + in [ + SegmentSource.COMPONENT.value, + SegmentSource.ROUTE.value, + SegmentSource.CUSTOM.value, + ] + ) + with capture_internal_exceptions(): + if not already_set: + name, source = ( + self._get_segment_name_and_source( + self.transaction_style, scope + ) + ) + span.name = name + span.set_attribute("sentry.span.source", source) + finally: + _asgi_middleware_applied.set(False) + + def event_processor( + self, event: "Event", hint: "Hint", asgi_scope: "Any" + ) -> "Optional[Event]": + request_data = event.get("request", {}) + request_data.update( + _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) + ) + event["request"] = deepcopy(request_data) + + # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) + transaction = event.get("transaction") + transaction_source = (event.get("transaction_info") or {}).get("source") + already_set = ( + transaction is not None + and transaction != _DEFAULT_TRANSACTION_NAME + and transaction_source + in [ + TransactionSource.COMPONENT, + TransactionSource.ROUTE, + TransactionSource.CUSTOM, + ] + ) + if not already_set: + name, source = self._get_transaction_name_and_source( + self.transaction_style, asgi_scope + ) + event["transaction"] = name + event["transaction_info"] = {"source": source} + + return event + + # Helper functions. + # + # Note: Those functions are not public API. If you want to mutate request + # data to your liking it's recommended to use the `before_send` callback + # for that. + + def _get_transaction_name_and_source( + self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SOURCE_FOR_STYLE[transaction_style] + ty = asgi_scope.get("type") + + if transaction_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = transaction_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + elif transaction_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + return name, source + + return name, source + + def _get_segment_name_and_source( + self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value + ty = asgi_scope.get("type") + + if segment_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = qualname_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + elif segment_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = SegmentSource.ROUTE.value + return name, source + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py new file mode 100644 index 0000000000..4b3f6d330e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py @@ -0,0 +1,280 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import AsyncHttpTransport +from sentry_sdk.utils import ( + event_from_exception, + is_internal_task, + logger, + reraise, +) + +try: + import asyncio + from asyncio.tasks import Task +except ImportError: + raise DidNotEnable("asyncio not available") + +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from collections.abc import Coroutine + from typing import Any, Callable, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T", bound=Callable[..., Any]) + + +def get_name(coro: "Any") -> str: + return ( + getattr(coro, "__qualname__", None) + or getattr(coro, "__name__", None) + or "coroutine without __name__" + ) + + +def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": + # Only __name__ and __qualname__ are copied from function to coroutine in CPython + return functools.partial( + functools.update_wrapper, + wrapped=wrapped, # type: ignore + assigned=("__name__", "__qualname__"), + updated=(), + ) + + +def patch_loop_close() -> None: + """Patch loop.close to flush pending events before shutdown.""" + # Atexit shutdown hook happens after the event loop is closed. + # Therefore, it is necessary to patch the loop.close method to ensure + # that pending events are flushed before the interpreter shuts down. + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No running loop → cannot patch now + return + + if getattr(loop, "_sentry_flush_patched", False): + return + + async def _flush() -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + try: + if not isinstance(client.transport, AsyncHttpTransport): + return + + await client.close_async() + except Exception: + logger.warning("Sentry flush failed during loop shutdown", exc_info=True) + + orig_close = loop.close + + def _patched_close() -> None: + try: + loop.run_until_complete(_flush()) + except Exception: + logger.debug( + "Could not flush Sentry events during loop close", exc_info=True + ) + finally: + orig_close() + + loop.close = _patched_close # type: ignore + loop._sentry_flush_patched = True # type: ignore + + +def _create_task_with_factory( + orig_task_factory: "Any", + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", +) -> "asyncio.Task[Any]": + task = None + + # Trying to use user set task factory (if there is one) + if orig_task_factory: + task = orig_task_factory(loop, coro, **kwargs) + + if task is None: + # The default task factory in `asyncio` does not have its own function + # but is just a couple of lines in `asyncio.base_events.create_task()` + # Those lines are copied here. + + # WARNING: + # If the default behavior of the task creation in asyncio changes, + # this will break! + task = Task(coro, loop=loop, **kwargs) + if task._source_traceback: # type: ignore + del task._source_traceback[-1] # type: ignore + + return task + + +def patch_asyncio() -> None: + orig_task_factory = None + try: + loop = asyncio.get_running_loop() + orig_task_factory = loop.get_task_factory() + + # Check if already patched + if getattr(orig_task_factory, "_is_sentry_task_factory", False): + return + + def _sentry_task_factory( + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", + ) -> "asyncio.Future[Any]": + # Check if this is an internal Sentry task + if is_internal_task(): + return _create_task_with_factory( + orig_task_factory, loop, coro, **kwargs + ) + + @_wrap_coroutine(coro) + async def _task_with_sentry_span_creation() -> "Any": + result = None + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncioIntegration) + task_spans = integration.task_spans if integration else False + + span_ctx: "Optional[Union[StreamedSpan, Span]]" = None + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope(): + if task_spans: + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=get_name(coro), + attributes={ + "sentry.op": OP.FUNCTION, + "sentry.origin": AsyncioIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.FUNCTION, + name=get_name(coro), + origin=AsyncioIntegration.origin, + ) + + with span_ctx if span_ctx else nullcontext(): + try: + result = await coro + except StopAsyncIteration as e: + raise e from None + except Exception: + reraise(*_capture_exception()) + + return result + + task = _create_task_with_factory( + orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs + ) + + # Set the task name to include the original coroutine's name + try: + task.set_name(f"{get_name(coro)} (Sentry-wrapped)") + except AttributeError: + # set_name might not be available in all Python versions + pass + + return task + + _sentry_task_factory._is_sentry_task_factory = True # type: ignore + loop.set_task_factory(_sentry_task_factory) # type: ignore + + except RuntimeError: + # When there is no running loop, we have nothing to patch. + logger.warning( + "There is no running asyncio loop so there is nothing Sentry can patch. " + "Please make sure you call sentry_sdk.init() within a running " + "asyncio loop for the AsyncioIntegration to work. " + "See https://docs.sentry.io/platforms/python/integrations/asyncio/" + ) + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + + integration = client.get_integration(AsyncioIntegration) + if integration is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "asyncio", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class AsyncioIntegration(Integration): + identifier = "asyncio" + origin = f"auto.function.{identifier}" + + def __init__(self, task_spans: bool = True) -> None: + self.task_spans = task_spans + + @staticmethod + def setup_once() -> None: + patch_asyncio() + patch_loop_close() + + +def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: + """ + Enable AsyncioIntegration with the provided options. + + This is useful in scenarios where Sentry needs to be initialized before + an event loop is set up, but you still want to instrument asyncio once there + is an event loop. In that case, you can sentry_sdk.init() early on without + the AsyncioIntegration and then, once the event loop has been set up, + execute: + + ```python + from sentry_sdk.integrations.asyncio import enable_asyncio_integration + + async def async_entrypoint(): + enable_asyncio_integration() + ``` + + Any arguments provided will be passed to AsyncioIntegration() as is. + + If AsyncioIntegration has already patched the current event loop, this + function won't have any effect. + + If AsyncioIntegration was provided in + sentry_sdk.init(disabled_integrations=[...]), this function will ignore that + and the integration will be enabled. + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # This function purposefully bypasses the integration machinery in + # integrations/__init__.py. _installed_integrations/_processed_integrations + # is used to prevent double patching the same module, but in the case of + # the AsyncioIntegration, we don't monkeypatch the standard library directly, + # we patch the currently running event loop, and we keep the record of doing + # that on the loop itself. + logger.debug("Setting up integration asyncio") + + integration = AsyncioIntegration(*args, **kwargs) + integration.setup_once() + + if "asyncio" not in client.integrations: + client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py new file mode 100644 index 0000000000..186176d268 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import contextlib +import re +from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import asyncpg # type: ignore + from asyncpg.cursor import ( # type: ignore + BaseCursor, + Cursor, + CursorIterator, + ) + +except ImportError: + raise DidNotEnable("asyncpg not installed.") + + +class AsyncPGIntegration(Integration): + identifier = "asyncpg" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AsyncPGIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + # asyncpg.__version__ is a string containing the semantic version in the form of ".." + asyncpg_version = parse_version(asyncpg.__version__) + _check_minimum_version(AsyncPGIntegration, asyncpg_version) + + asyncpg.Connection.execute = _wrap_execute( + asyncpg.Connection.execute, + ) + + asyncpg.Connection._execute = _wrap_connection_method( + asyncpg.Connection._execute + ) + asyncpg.Connection._executemany = _wrap_connection_method( + asyncpg.Connection._executemany, executemany=True + ) + asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) + + BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) + BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) + + asyncpg.connect_utils._connect_addr = _wrap_connect_addr( + asyncpg.connect_utils._connect_addr + ) + + +T = TypeVar("T") + + +def _normalize_query(query: str) -> str: + return re.sub(r"\s+", " ", query).strip() + + +def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + # Avoid recording calls to _execute twice. + # Calls to Connection.execute with args also call + # Connection._execute, which is recorded separately + # args[0] = the connection object, args[1] is the query + if len(args) > 2: + return await f(*args, **kwargs) + + query = _normalize_query(args[1]) + with record_sql_queries( + cursor=None, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + span_origin=AsyncPGIntegration.origin, + ) as span: + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +SubCursor = TypeVar("SubCursor", bound=BaseCursor) + + +@contextlib.contextmanager +def _record( + cursor: "SubCursor | None", + query: str, + params_list: "tuple[Any, ...] | None", + *, + executemany: bool = False, +) -> "Iterator[Union[Span, StreamedSpan]]": + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncPGIntegration) + if integration is not None and not integration._record_params: + params_list = None + + param_style = "pyformat" if params_list else None + + query = _normalize_query(query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=params_list, + paramstyle=param_style, + executemany=executemany, + record_cursor_repr=cursor is not None, + span_origin=AsyncPGIntegration.origin, + ) as span: + yield span + + +def _wrap_connection_method( + f: "Callable[..., Awaitable[T]]", *, executemany: bool = False +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + query = args[1] + params_list = args[2] if len(args) > 2 else None + with _record(None, query, params_list, executemany=executemany) as span: + _set_db_data(span, args[0]) + + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_cursor_method( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + if type(cursor) is CursorIterator: + span_op_override_value = OP.DB_CURSOR_ITERATOR + elif type(cursor) is Cursor: + span_op_override_value = OP.DB_CURSOR_FETCH + else: + span_op_override_value = None + + query = _normalize_query(cursor._query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + record_cursor_repr=True, + span_origin=AsyncPGIntegration.origin, + span_op_override_value=span_op_override_value, + ) as span: + _set_db_data(span, cursor._connection) + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_connect_addr( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + user = kwargs["params"].user + database = kwargs["params"].database + addr = kwargs.get("addr") + + if has_span_streaming_enabled(client.options): + span_attributes = { + "sentry.op": OP.DB, + "sentry.origin": AsyncPGIntegration.origin, + SPANDATA.DB_SYSTEM_NAME: "postgresql", + SPANDATA.DB_USER: user, + SPANDATA.DB_NAMESPACE: database, + SPANDATA.DB_DRIVER_NAME: "asyncpg", + } + if addr: + try: + span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] + span_attributes[SPANDATA.SERVER_PORT] = addr[1] + except IndexError: + pass + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span_attributes + ) + res = await f(*args, **kwargs) + + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AsyncPGIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + span.set_data(SPANDATA.DB_NAME, database) + span.set_data(SPANDATA.DB_USER, user) + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span._data + ) + res = await f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + addr = conn._addr + database = conn._params.database + user = conn._params.user + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") + if addr: + try: + span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_attribute(SPANDATA.DB_NAMESPACE, database) + + if user: + span.set_attribute(SPANDATA.DB_USER, user) + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_data(SPANDATA.DB_NAME, database) + + if user: + span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py new file mode 100644 index 0000000000..b573e76f09 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py @@ -0,0 +1,51 @@ +import atexit +import os +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Optional + + +def default_callback(pending: int, timeout: int) -> None: + """This is the default shutdown callback that is set on the options. + It prints out a message to stderr that informs the user that some events + are still pending and the process is waiting for them to flush out. + """ + + def echo(msg: str) -> None: + sys.stderr.write(msg + "\n") + + echo("Sentry is attempting to send %i pending events" % pending) + echo("Waiting up to %s seconds" % timeout) + echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) + sys.stderr.flush() + + +class AtexitIntegration(Integration): + identifier = "atexit" + + def __init__(self, callback: "Optional[Any]" = None) -> None: + if callback is None: + callback = default_callback + self.callback = callback + + @staticmethod + def setup_once() -> None: + @atexit.register + def _shutdown() -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(AtexitIntegration) + + if integration is None: + return + + logger.debug("atexit: got shutdown signal") + logger.debug("atexit: shutting down client") + sentry_sdk.get_isolation_scope().end_session() + + client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py new file mode 100644 index 0000000000..c7fe77714a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py @@ -0,0 +1,542 @@ +import functools +import json +import re +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING +from urllib.parse import urlencode + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.cloud_resource_context import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + +# Constants +TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + + +def _wrap_init_error(init_error: "F") -> "F": + @ensure_integration_enabled(AwsLambdaIntegration, init_error) + def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + with capture_internal_exceptions(): + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + exc_info = sys.exc_info() + if exc_info and all(exc_info): + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + + else: + # Fall back to AWS lambdas JSON representation of the error + error_info = args[1] + if isinstance(error_info, str): + error_info = json.loads(error_info) + sentry_event = _event_from_error_json(error_info) + sentry_sdk.capture_event(sentry_event) + + return init_error(*args, **kwargs) + + return sentry_init_error # type: ignore + + +def _wrap_handler(handler: "F") -> "F": + @functools.wraps(handler) + def sentry_handler( + aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, + # `event` here is *likely* a dictionary, but also might be a number of + # other types (str, int, float, None). + # + # In some cases, it is a list (if the user is batch-invoking their + # function, for example), in which case we'll use the first entry as a + # representative from which to try pulling request data. (Presumably it + # will be the same for all events in the list, since they're all hitting + # the lambda in the same request.) + + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + + if integration is None: + return handler(aws_event, aws_context, *args, **kwargs) + + if isinstance(aws_event, list) and len(aws_event) >= 1: + request_data = aws_event[0] + batch_size = len(aws_event) + else: + request_data = aws_event + batch_size = 1 + + if not isinstance(request_data, dict): + # If we're not dealing with a dictionary, we won't be able to get + # headers, path, http method, etc in any case, so it's fine that + # this is empty + request_data = {} + + configured_time = aws_context.get_remaining_time_in_millis() + aws_region = aws_context.invoked_function_arn.split(":")[3] + + with sentry_sdk.isolation_scope() as scope: + timeout_thread = None + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + request_data, aws_context, configured_time + ) + ) + scope.set_tag("aws_region", aws_region) + if batch_size > 1: + scope.set_tag("batch_request", True) + scope.set_tag("batch_size", batch_size) + + # Starting the Timeout thread only if the configured time is greater than Timeout warning + # buffer and timeout_warning parameter is set True. + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = ( + configured_time - TIMEOUT_WARNING_BUFFER + ) / MILLIS_TO_SECONDS + + timeout_thread = TimeoutThread( + waiting_time, + configured_time / MILLIS_TO_SECONDS, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = request_data.get("headers", {}) + # Some AWS Services (ie. EventBridge) set headers as a list + # or None, so we must ensure it is a dict + if not isinstance(headers, dict): + headers = {} + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + additional_attributes: "dict[str, Any]" = {} + if "httpMethod" in request_data: + additional_attributes["http.request.method"] = request_data[ + "httpMethod" + ] + + if should_send_default_pii() and "queryStringParameters" in request_data: + qs = request_data["queryStringParameters"] + if qs: + additional_attributes["url.query"] = urlencode(qs) + + sampling_context = { + "aws_event": aws_event, + "aws_context": aws_context, + } + + function_name = aws_context.function_name + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_AWS, + "sentry.origin": AwsLambdaIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.region": aws_region, + "cloud.resource_id": aws_context.invoked_function_arn, + "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, + "cloud.provider": CLOUD_PROVIDER.AWS, + "faas.name": function_name, + "faas.invocation_id": aws_context.aws_request_id, + "faas.version": aws_context.function_version, + "aws.lambda.invoked_arn": aws_context.invoked_function_arn, + "aws.log.group.names": [aws_context.log_group_name], + "aws.log.stream.names": [aws_context.log_stream_name], + "messaging.batch.message_count": batch_size, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_AWS, + name=function_name, + source=TransactionSource.COMPONENT, + origin=AwsLambdaIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return handler(aws_event, aws_context, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + + return sentry_handler # type: ignore + + +def _drain_queue() -> None: + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + if integration is not None: + # Flush out the event queue before AWS kills the + # process. + client.flush() + + +class AwsLambdaIntegration(Integration): + identifier = "aws_lambda" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + lambda_bootstrap = get_lambda_bootstrap() + if not lambda_bootstrap: + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find bootstrap module)" + ) + return + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find handle_event_request)" + ) + return + + pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 + + if pre_37: + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_event_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + old_handle_http_request = lambda_bootstrap.handle_http_request + + def sentry_handle_http_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_http_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_http_request = sentry_handle_http_request + + # Patch to_json to drain the queue. This should work even when the + # SDK is initialized inside of the handler + + old_to_json = lambda_bootstrap.to_json + + def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return old_to_json(*args, **kwargs) + + lambda_bootstrap.to_json = sentry_to_json + else: + lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( + lambda_bootstrap.LambdaRuntimeClient.post_init_error + ) + + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f: "F") -> "F": + def inner(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore + + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + ) + + +def get_lambda_bootstrap() -> "Optional[Any]": + # Python 3.7: If the bootstrap module is *already imported*, it is the + # one we actually want to use (no idea what's in __main__) + # + # Python 3.8: bootstrap is also importable, but will be the same file + # as __main__ imported under a different name: + # + # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ + # sys.modules['__main__'] is not sys.modules['bootstrap'] + # + # Python 3.9: bootstrap is in __main__.awslambdaricmain + # + # On container builds using the `aws-lambda-python-runtime-interface-client` + # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap + # + # Such a setup would then make all monkeypatches useless. + if "bootstrap" in sys.modules: + return sys.modules["bootstrap"] + elif "__main__" in sys.modules: + module = sys.modules["__main__"] + # python3.9 runtime + if hasattr(module, "awslambdaricmain") and hasattr( + module.awslambdaricmain, "bootstrap" + ): + return module.awslambdaricmain.bootstrap + elif hasattr(module, "bootstrap"): + # awslambdaric python module in container builds + return module.bootstrap + + # python3.8 runtime + return module + else: + return None + + +def _make_request_event_processor( + aws_event: "Any", aws_context: "Any", configured_timeout: "Any" +) -> "EventProcessor": + start_time = datetime.now(timezone.utc) + + def event_processor( + sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time + ) -> "Optional[Event]": + remaining_time_in_milis = aws_context.get_remaining_time_in_millis() + exec_duration = configured_timeout - remaining_time_in_milis + + extra = sentry_event.setdefault("extra", {}) + extra["lambda"] = { + "function_name": aws_context.function_name, + "function_version": aws_context.function_version, + "invoked_function_arn": aws_context.invoked_function_arn, + "aws_request_id": aws_context.aws_request_id, + "execution_duration_in_millis": exec_duration, + "remaining_time_in_millis": remaining_time_in_milis, + } + + extra["cloudwatch logs"] = { + "url": _get_cloudwatch_logs_url(aws_context, start_time), + "log_group": aws_context.log_group_name, + "log_stream": aws_context.log_stream_name, + } + + request = sentry_event.get("request", {}) + + if "httpMethod" in aws_event: + request["method"] = aws_event["httpMethod"] + + request["url"] = _get_url(aws_event, aws_context) + + if "queryStringParameters" in aws_event: + request["query_string"] = aws_event["queryStringParameters"] + + if "headers" in aws_event: + request["headers"] = _filter_headers(aws_event["headers"]) + + if should_send_default_pii(): + user_info = sentry_event.setdefault("user", {}) + + identity = aws_event.get("identity") + if identity is None: + identity = {} + + id = identity.get("userArn") + if id is not None: + user_info.setdefault("id", id) + + ip = identity.get("sourceIp") + if ip is not None: + user_info.setdefault("ip_address", ip) + + if "body" in aws_event: + request["data"] = aws_event.get("body", "") + else: + if aws_event.get("body", None): + # Unfortunately couldn't find a way to get structured body from AWS + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + sentry_event["request"] = deepcopy(request) + + return sentry_event + + return event_processor + + +def _get_url(aws_event: "Any", aws_context: "Any") -> str: + path = aws_event.get("path", None) + + headers = aws_event.get("headers") + if headers is None: + headers = {} + + host = headers.get("Host", None) + proto = headers.get("X-Forwarded-Proto", None) + if proto and host and path: + return "{}://{}{}".format(proto, host, path) + return "awslambda:///{}".format(aws_context.function_name) + + +def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: + """ + Generates a CloudWatchLogs console URL based on the context object + + Arguments: + aws_context {Any} -- context from lambda handler + + Returns: + str -- AWS Console URL to logs. + """ + formatstring = "%Y-%m-%dT%H:%M:%SZ" + region = environ.get("AWS_REGION", "") + + url = ( + "https://console.{domain}/cloudwatch/home?region={region}" + "#logEventViewer:group={log_group};stream={log_stream}" + ";start={start_time};end={end_time}" + ).format( + domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", + region=region, + log_group=aws_context.log_group_name, + log_stream=aws_context.log_stream_name, + start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), + end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( + formatstring + ), + ) + + return url + + +def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": + frames = [] + for frame in formatted_tb: + match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) + if match: + file_name, line_number, func_name = match.groups() + line_number = int(line_number) + frames.append( + { + "filename": file_name, + "function": func_name, + "lineno": line_number, + "vars": None, + "pre_context": None, + "context_line": None, + "post_context": None, + } + ) + return frames + + +def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": + """ + Converts the error JSON from AWS Lambda into a Sentry error event. + This is not a full fletched event, but better than nothing. + + This is an example of where AWS creates the error JSON: + https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 + """ + event: "Event" = { + "level": "error", + "exception": { + "values": [ + { + "type": error_json.get("errorType"), + "value": error_json.get("errorMessage"), + "stacktrace": { + "frames": _parse_formatted_traceback( + error_json.get("stackTrace", []) + ), + }, + "mechanism": { + "type": "aws_lambda", + "handled": False, + }, + } + ], + }, + } + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py new file mode 100644 index 0000000000..31f45f73de --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py @@ -0,0 +1,164 @@ +import sys +import types +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) + + +WRAPPED_FUNC = "_wrapped_{}_" +INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py +USED_FUNC = "_sentry_used_" + + +class BeamIntegration(Integration): + identifier = "beam" + + @staticmethod + def setup_once() -> None: + from apache_beam.transforms.core import DoFn, ParDo # type: ignore + + ignore_logger("root") + ignore_logger("bundle_processor.create") + + function_patches = ["process", "start_bundle", "finish_bundle", "setup"] + for func_name in function_patches: + setattr( + DoFn, + INSPECT_FUNC.format(func_name), + _wrap_inspect_call(DoFn, func_name), + ) + + old_init = ParDo.__init__ + + def sentry_init_pardo( + self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Do not monkey patch init twice + if not getattr(self, "_sentry_is_patched", False): + for func_name in function_patches: + if not hasattr(fn, func_name): + continue + wrapped_func = WRAPPED_FUNC.format(func_name) + + # Check to see if inspect is set and process is not + # to avoid monkey patching process twice. + # Check to see if function is part of object for + # backwards compatibility. + process_func = getattr(fn, func_name) + inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) + if not getattr(inspect_func, USED_FUNC, False) and not getattr( + process_func, USED_FUNC, False + ): + setattr(fn, wrapped_func, process_func) + setattr(fn, func_name, _wrap_task_call(process_func)) + + self._sentry_is_patched = True + old_init(self, fn, *args, **kwargs) + + ParDo.__init__ = sentry_init_pardo + + +def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": + if not hasattr(cls, func_name): + return None + + def _inspect(self: "Any") -> "Any": + """ + Inspect function overrides the way Beam gets argspec. + """ + wrapped_func = WRAPPED_FUNC.format(func_name) + if hasattr(self, wrapped_func): + process_func = getattr(self, wrapped_func) + else: + process_func = getattr(self, func_name) + setattr(self, func_name, _wrap_task_call(process_func)) + setattr(self, wrapped_func, process_func) + + # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults + # (which uses Signatures internally) should be used instead. + try: + from apache_beam.transforms.core import get_function_args_defaults + + return get_function_args_defaults(process_func) + except ImportError: + from apache_beam.typehints.decorators import getfullargspec # type: ignore + + return getfullargspec(process_func) + + setattr(_inspect, USED_FUNC, True) + return _inspect + + +def _wrap_task_call(func: "F") -> "F": + """ + Wrap task call with a try catch to get exceptions. + """ + + @wraps(func) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + try: + gen = func(*args, **kwargs) + except Exception: + raise_exception() + + if not isinstance(gen, types.GeneratorType): + return gen + return _wrap_generator_call(gen) + + setattr(_inner, USED_FUNC, True) + return _inner # type: ignore + + +@ensure_integration_enabled(BeamIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + """ + Send Beam exception to Sentry. + """ + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "beam", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def raise_exception() -> None: + """ + Raise an exception. + """ + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) + reraise(*exc_info) + + +def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": + """ + Wrap the generator to handle any failures. + """ + while True: + try: + yield next(gen) + except StopIteration: + break + except Exception: + raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py new file mode 100644 index 0000000000..a7fdd99b21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py @@ -0,0 +1,187 @@ +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_url, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Type, Union + + from botocore.model import ServiceId + +try: + from botocore import __version__ as BOTOCORE_VERSION + from botocore.awsrequest import AWSRequest + from botocore.client import BaseClient + from botocore.response import StreamingBody +except ImportError: + raise DidNotEnable("botocore is not installed") + + +class Boto3Integration(Integration): + identifier = "boto3" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTOCORE_VERSION) + _check_minimum_version(Boto3Integration, version, "botocore") + + orig_init = BaseClient.__init__ + + def sentry_patched_init( + self: "BaseClient", *args: "Any", **kwargs: "Any" + ) -> None: + orig_init(self, *args, **kwargs) + meta = self.meta + service_id = meta.service_model.service_id + meta.events.register( + "request-created", + partial(_sentry_request_created, service_id=service_id), + ) + meta.events.register("after-call", _sentry_after_call) + meta.events.register("after-call-error", _sentry_after_call_error) + + BaseClient.__init__ = sentry_patched_init # type: ignore + + +def _sentry_request_created( + service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" +) -> None: + description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) + + client = sentry_sdk.get_client() + if client.get_integration(Boto3Integration) is None: + return + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Boto3Integration.origin, + SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", + }, + ) + if request.url is not None and should_send_default_pii(): + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if request.method is not None: + span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=description, + origin=Boto3Integration.origin, + ) + + if request.url is not None: + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_data("aws.request.url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + span.set_tag("aws.service_id", service_id.hyphenize()) + span.set_tag("aws.operation_name", operation_name) + if request.method is not None: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + + # We do it in order for subsequent http calls/retries be + # attached to this span. + span.__enter__() + + # request.context is an open-ended data-structure + # where we can add anything useful in request life cycle. + request.context["_sentrysdk_span"] = span + + +def _sentry_after_call( + context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(None, None, None) + + body = parsed.get("Body") + if not isinstance(body, StreamingBody): + return + + streaming_span: "Union[Span, StreamedSpan]" + if isinstance(span, StreamedSpan): + streaming_span = sentry_sdk.traces.start_span( + name=span.name, + parent_span=span, + attributes={ + "sentry.op": OP.HTTP_CLIENT_STREAM, + "sentry.origin": Boto3Integration.origin, + }, + ) + else: + streaming_span = span.start_child( + op=OP.HTTP_CLIENT_STREAM, + name=span.description, + origin=Boto3Integration.origin, + ) + + orig_read = body.read + orig_close = body.close + + def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: + try: + ret = orig_read(*args, **kwargs) + if ret: + return ret + + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + return ret + except Exception: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + raise + + body.read = sentry_streaming_body_read # type: ignore + + def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + orig_close(*args, **kwargs) + + body.close = sentry_streaming_body_close # type: ignore + + +def _sentry_after_call_error( + context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py new file mode 100644 index 0000000000..50f6ca2e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py @@ -0,0 +1,239 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from collections.abc import Set + from typing import Any, Callable, Dict, Optional + + from bottle import FileUpload, FormsDict, LocalRequest # type: ignore + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + +try: + from bottle import ( + Bottle, + HTTPResponse, + Route, + ) + from bottle import ( + __version__ as BOTTLE_VERSION, + ) + from bottle import ( + request as bottle_request, + ) +except ImportError: + raise DidNotEnable("Bottle not installed") + + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class BottleIntegration(Integration): + identifier = "bottle" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTTLE_VERSION) + _check_minimum_version(BottleIntegration, version) + + old_app = Bottle.__call__ + + @ensure_integration_enabled(BottleIntegration, old_app) + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=BottleIntegration.origin, + ) + + return middleware(environ, start_response) + + Bottle.__call__ = sentry_patched_wsgi_app + + old_handle = Bottle._handle + + @functools.wraps(old_handle) + def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return old_handle(self, environ) + + scope = sentry_sdk.get_isolation_scope() + scope._name = "bottle" + scope.add_event_processor( + _make_request_event_processor(self, bottle_request, integration) + ) + res = old_handle(self, environ) + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + _set_segment_name_and_source( + transaction_style=integration.transaction_style + ) + + return res + + Bottle._handle = _patched_handle + + old_make_callback = Route._make_callback + + @functools.wraps(old_make_callback) + def patched_make_callback( + self: "Route", *args: object, **kwargs: object + ) -> "Any": + prepared_callback = old_make_callback(self, *args, **kwargs) + + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return prepared_callback + + def wrapped_callback(*args: object, **kwargs: object) -> "Any": + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) + + return res + + return wrapped_callback + + Route._make_callback = patched_make_callback + + +class BottleRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[str, str]": + return self.request.cookies + + def raw_data(self) -> bytes: + return self.request.body.read() + + def form(self) -> "FormsDict": + if self.is_json(): + return None + return self.request.forms.decode() + + def files(self) -> "Optional[Dict[str, str]]": + if self.is_json(): + return None + + return self.request.files + + def size_of_file(self, file: "FileUpload") -> int: + return file.content_length + + +def _set_segment_name_and_source(transaction_style: str) -> None: + try: + if transaction_style == "url": + name = bottle_request.route.rule or "bottle request" + else: + name = ( + bottle_request.route.name + or transaction_from_function(bottle_request.route.callback) + or "bottle request" + ) + + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], + ) + except RuntimeError: + pass + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "url": + try: + name = request.route.rule or "" + except RuntimeError: + pass + + elif transaction_style == "endpoint": + try: + name = ( + request.route.name + or transaction_from_function(request.route.callback) + or "" + ) + except RuntimeError: + pass + + event["transaction"] = name + event["transaction_info"] = { + "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + } + + +def _make_request_event_processor( + app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, request) + + with capture_internal_exceptions(): + BottleRequestExtractor(request).extract_into_event(event) + + return event + + return event_processor + + +def _capture_exception(exception: BaseException, handled: bool) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "bottle", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py new file mode 100644 index 0000000000..532b13539b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py @@ -0,0 +1,612 @@ +import sys +from collections.abc import Mapping +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import isolation_scope +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.celery.beat import ( + _patch_beat_apply_entry, + _patch_redbeat_apply_async, + _setup_celery_beat_signals, +) +from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource +from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, List, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import VERSION as CELERY_VERSION # type: ignore + from celery.app.task import Task # type: ignore + from celery.app.trace import task_has_custom + from celery.exceptions import ( # type: ignore + Ignore, + Reject, + Retry, + SoftTimeLimitExceeded, + ) + from kombu import Producer # type: ignore +except ImportError: + raise DidNotEnable("Celery not installed") + + +CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) + + +class CeleryIntegration(Integration): + identifier = "celery" + origin = f"auto.queue.{identifier}" + + def __init__( + self, + propagate_traces: bool = True, + monitor_beat_tasks: bool = False, + exclude_beat_tasks: "Optional[List[str]]" = None, + ) -> None: + self.propagate_traces = propagate_traces + self.monitor_beat_tasks = monitor_beat_tasks + self.exclude_beat_tasks = exclude_beat_tasks + + _patch_beat_apply_entry() + _patch_redbeat_apply_async() + _setup_celery_beat_signals(monitor_beat_tasks) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(CeleryIntegration, CELERY_VERSION) + + _patch_build_tracer() + _patch_task_apply_async() + _patch_celery_send_task() + _patch_worker_exit() + _patch_producer_publish() + + # This logger logs every status of every task that ran on the worker. + # Meaning that every task's breadcrumbs are full of stuff like "Task + # raised unexpected ". + ignore_logger("celery.worker.job") + ignore_logger("celery.app.trace") + + # This is stdout/err redirected to a logger, can't deal with this + # (need event_level=logging.WARN to reproduce) + ignore_logger("celery.redirected") + + +def _set_status(status: str) -> None: + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + + if span_streaming and scope.streamed_span is not None: + scope.streamed_span.status = "ok" if status == "ok" else "error" + elif not span_streaming and scope.span is not None: + scope.span.set_status(status) + + +def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return + + if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): + # ??? Doesn't map to anything + _set_status("aborted") + return + + _set_status("internal_error") + + if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): + return + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "celery", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + task: "Any", + uuid: "Any", + args: "Any", + kwargs: "Any", + request: "Optional[Any]" = None, +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["celery_task_id"] = uuid + extra = event.setdefault("extra", {}) + extra["celery-job"] = { + "task_name": task.name, + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + } + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): + event["fingerprint"] = [ + "celery", + "SoftTimeLimitExceeded", + getattr(task, "name", task), + ] + + return event + + return event_processor + + +def _update_celery_task_headers( + original_headers: "dict[str, Any]", + span: "Optional[Union[StreamedSpan, Span]]", + monitor_beat_tasks: bool, +) -> "dict[str, Any]": + """ + Updates the headers of the Celery task with the tracing information + and eventually Sentry Crons monitoring information for beat tasks. + """ + updated_headers = original_headers.copy() + with capture_internal_exceptions(): + # if span is None (when the task was started by Celery Beat) + # this will return the trace headers from the scope. + headers = dict( + sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) + ) + + if monitor_beat_tasks: + headers.update( + { + "sentry-monitor-start-timestamp-s": "%.9f" + % _now_seconds_since_epoch(), + } + ) + + # Add the time the task was enqueued to the headers + # This is used in the consumer to calculate the latency + updated_headers.update( + {"sentry-task-enqueued-time": _now_seconds_since_epoch()} + ) + + if headers: + existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) + sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) + + combined_baggage = sentry_baggage or existing_baggage + if sentry_baggage and existing_baggage: + # Merge incoming and sentry baggage, where the sentry trace information + # in the incoming baggage takes precedence and the third-party items + # are concatenated. + incoming = Baggage.from_incoming_header(existing_baggage) + combined = Baggage.from_incoming_header(sentry_baggage) + combined.sentry_items.update(incoming.sentry_items) + combined.third_party_items = ",".join( + [ + x + for x in [ + combined.third_party_items, + incoming.third_party_items, + ] + if x is not None and x != "" + ] + ) + combined_baggage = combined.serialize(include_third_party=True) + + updated_headers.update(headers) + if combined_baggage: + updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage + + # https://github.com/celery/celery/issues/4875 + # + # Need to setdefault the inner headers too since other + # tracing tools (dd-trace-py) also employ this exact + # workaround and we don't want to break them. + updated_headers.setdefault("headers", {}).update(headers) + if combined_baggage: + updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage + + # Add the Sentry options potentially added in `sentry_apply_entry` + # to the headers (done when auto-instrumenting Celery Beat tasks) + for key, value in updated_headers.items(): + if key.startswith("sentry-"): + updated_headers["headers"][key] = value + + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + for key, value in original_headers.items(): + if key != "headers" and key not in updated_headers["headers"]: + updated_headers["headers"][key] = value + + return updated_headers + + +class NoOpMgr: + def __enter__(self) -> None: + return None + + def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: + return None + + +def _wrap_task_run(f: "F") -> "F": + @wraps(f) + def apply_async(*args: "Any", **kwargs: "Any") -> "Any": + # Note: kwargs can contain headers=None, so no setdefault! + # Unsure which backend though. + client = sentry_sdk.get_client() + integration = client.get_integration(CeleryIntegration) + if integration is None: + return f(*args, **kwargs) + + kwarg_headers = kwargs.get("headers") or {} + propagate_traces = kwarg_headers.pop( + "sentry-propagate-traces", integration.propagate_traces + ) + + if not propagate_traces: + return f(*args, **kwargs) + + if isinstance(args[0], Task): + task_name: str = args[0].name + elif len(args) > 1 and isinstance(args[1], str): + task_name = args[1] + else: + task_name = "" + + span_streaming = has_span_streaming_enabled(client.options) + + task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" + + span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + if span_streaming: + if not task_started_from_beat and get_current_span() is not None: + span_mgr = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_CELERY, + "sentry.origin": CeleryIntegration.origin, + }, + ) + + else: + if not task_started_from_beat: + span_mgr = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_CELERY, + name=task_name, + origin=CeleryIntegration.origin, + ) + + with span_mgr as span: + kwargs["headers"] = _update_celery_task_headers( + kwarg_headers, span, integration.monitor_beat_tasks + ) + return f(*args, **kwargs) + + return apply_async # type: ignore + + +def _wrap_tracer(task: "Any", f: "F") -> "F": + # Need to wrap tracer for pushing the scope before prerun is sent, and + # popping it after postrun is sent. + # + # This is the reason we don't use signals for hooking in the first place. + # Also because in Celery 3, signal dispatch returns early if one handler + # crashes. + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + with isolation_scope() as scope: + scope._name = "celery" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) + + task_name = getattr(task, "name", "") + + custom_sampling_context = {} + with capture_internal_exceptions(): + custom_sampling_context = { + "celery_job": { + "task": task_name, + # for some reason, args[1] is a list if non-empty but a + # tuple if empty + "args": list(args[1]), + "kwargs": args[2], + } + } + + span: "Union[Span, StreamedSpan]" + span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + + # Celery task objects are not a thing to be trusted. Even + # something such as attribute access can fail. + with capture_internal_exceptions(): + headers = args[3].get("headers") or {} + if span_streaming: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(custom_sampling_context) + span = sentry_sdk.traces.start_span( + name=task_name, + parent_span=None, # make this a segment + attributes={ + "sentry.origin": CeleryIntegration.origin, + "sentry.span.source": TransactionSource.TASK.value, + "sentry.op": OP.QUEUE_TASK_CELERY, + }, + ) + + span_ctx = span + + else: + span = continue_trace( + headers, + op=OP.QUEUE_TASK_CELERY, + name=task_name, + source=TransactionSource.TASK, + origin=CeleryIntegration.origin, + ) + span.set_status(SPANSTATUS.OK) + + span_ctx = sentry_sdk.start_transaction( + span, + custom_sampling_context=custom_sampling_context, + ) + + with span_ctx: + return f(*args, **kwargs) + + return _inner # type: ignore + + +def _set_messaging_destination_name( + task: "Any", span: "Union[StreamedSpan, Span]" +) -> None: + """Set "messaging.destination.name" tag for span""" + with capture_internal_exceptions(): + delivery_info = task.request.delivery_info + if delivery_info: + routing_key = delivery_info.get("routing_key") + if delivery_info.get("exchange") == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning the tasks + # are sent to the queue with the same name as the routing key. + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + else: + span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + +def _wrap_task_call(task: "Any", f: "F") -> "F": + # Need to wrap task call because the exception is caught before we get to + # see it. Also celery's reported stacktrace is untrustworthy. + + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + try: + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_PROCESS, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PROCESS, + name=task.name, + origin=CeleryIntegration.origin, + ) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + _set_messaging_destination_name(task, span) + + latency = None + with capture_internal_exceptions(): + if ( + task.request.headers is not None + and "sentry-task-enqueued-time" in task.request.headers + ): + latency = _now_seconds_since_epoch() - task.request.headers.pop( + "sentry-task-enqueued-time" + ) + + if latency is not None: + latency *= 1000 # milliseconds + set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) + + with capture_internal_exceptions(): + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries + ) + + with capture_internal_exceptions(): + with task.app.connection() as conn: + set_on_span( + SPANDATA.MESSAGING_SYSTEM, + conn.transport.driver_type, + ) + + return f(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(task, exc_info) + reraise(*exc_info) + + return _inner # type: ignore + + +def _patch_build_tracer() -> None: + import celery.app.trace as trace # type: ignore + + original_build_tracer = trace.build_tracer + + def sentry_build_tracer( + name: "Any", task: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not getattr(task, "_sentry_is_patched", False): + # determine whether Celery will use __call__ or run and patch + # accordingly + if task_has_custom(task, "__call__"): + type(task).__call__ = _wrap_task_call(task, type(task).__call__) + else: + task.run = _wrap_task_call(task, task.run) + + # `build_tracer` is apparently called for every task + # invocation. Can't wrap every celery task for every invocation + # or we will get infinitely nested wrapper functions. + task._sentry_is_patched = True + + return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) + + trace.build_tracer = sentry_build_tracer + + +def _patch_task_apply_async() -> None: + Task.apply_async = _wrap_task_run(Task.apply_async) + + +def _patch_celery_send_task() -> None: + from celery import Celery + + Celery.send_task = _wrap_task_run(Celery.send_task) + + +def _patch_worker_exit() -> None: + # Need to flush queue before worker shutdown because a crashing worker will + # call os._exit + from billiard.pool import Worker # type: ignore + + original_workloop = Worker.workloop + + def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": + try: + return original_workloop(*args, **kwargs) + finally: + with capture_internal_exceptions(): + if ( + sentry_sdk.get_client().get_integration(CeleryIntegration) + is not None + ): + sentry_sdk.flush() + + Worker.workloop = sentry_workloop + + +def _patch_producer_publish() -> None: + original_publish = Producer.publish + + def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return original_publish(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + kwargs_headers = kwargs.get("headers", {}) + if not isinstance(kwargs_headers, Mapping): + # Ensure kwargs_headers is a Mapping, so we can safely call get(). + # We don't expect this to happen, but it's better to be safe. Even + # if it does happen, only our instrumentation breaks. This line + # does not overwrite kwargs["headers"], so the original publish + # method will still work. + kwargs_headers = {} + + task_name = kwargs_headers.get("task") or "" + task_id = kwargs_headers.get("id") + retries = kwargs_headers.get("retries") + + routing_key = kwargs.get("routing_key") + exchange = kwargs.get("exchange") + + span: "Union[StreamedSpan, Span, None]" = None + if span_streaming: + if get_current_span() is not None: + span = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_PUBLISH, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PUBLISH, + name=task_name, + origin=CeleryIntegration.origin, + ) + + if span is None: + return original_publish(self, *args, **kwargs) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + if task_id is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) + + if exchange == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning messages are + # routed to the queue with the same name as the routing key. + set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + if retries is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type + ) + + return original_publish(self, *args, **kwargs) + + Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py new file mode 100644 index 0000000000..b5027d212a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py @@ -0,0 +1,291 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.crons import MonitorStatus, capture_checkin +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.celery.utils import ( + _get_humanized_interval, + _now_seconds_since_epoch, +) +from sentry_sdk.utils import ( + logger, + match_regex_list, +) + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, TypeVar, Union + + from sentry_sdk._types import ( + MonitorConfig, + MonitorConfigScheduleType, + MonitorConfigScheduleUnit, + ) + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import Celery, Task # type: ignore + from celery.beat import Scheduler # type: ignore + from celery.schedules import crontab, schedule # type: ignore + from celery.signals import ( # type: ignore + task_failure, + task_retry, + task_success, + ) +except ImportError: + raise DidNotEnable("Celery not installed") + +try: + from redbeat.schedulers import RedBeatScheduler # type: ignore +except ImportError: + RedBeatScheduler = None + + +def _get_headers(task: "Task") -> "dict[str, Any]": + headers = task.request.get("headers") or {} + + # flatten nested headers + if "headers" in headers: + headers.update(headers["headers"]) + del headers["headers"] + + headers.update(task.request.get("properties") or {}) + + return headers + + +def _get_monitor_config( + celery_schedule: "Any", app: "Celery", monitor_name: str +) -> "MonitorConfig": + monitor_config: "MonitorConfig" = {} + schedule_type: "Optional[MonitorConfigScheduleType]" = None + schedule_value: "Optional[Union[str, int]]" = None + schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None + + if isinstance(celery_schedule, crontab): + schedule_type = "crontab" + schedule_value = ( + "{0._orig_minute} " + "{0._orig_hour} " + "{0._orig_day_of_month} " + "{0._orig_month_of_year} " + "{0._orig_day_of_week}".format(celery_schedule) + ) + elif isinstance(celery_schedule, schedule): + schedule_type = "interval" + (schedule_value, schedule_unit) = _get_humanized_interval( + celery_schedule.seconds + ) + + if schedule_unit == "second": + logger.warning( + "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", + monitor_name, + schedule_value, + ) + return {} + + else: + logger.warning( + "Celery schedule type '%s' not supported by Sentry Crons.", + type(celery_schedule), + ) + return {} + + monitor_config["schedule"] = {} + monitor_config["schedule"]["type"] = schedule_type + monitor_config["schedule"]["value"] = schedule_value + + if schedule_unit is not None: + monitor_config["schedule"]["unit"] = schedule_unit + + monitor_config["timezone"] = ( + ( + hasattr(celery_schedule, "tz") + and celery_schedule.tz is not None + and str(celery_schedule.tz) + ) + or app.timezone + or "UTC" + ) + + return monitor_config + + +def _apply_crons_data_to_schedule_entry( + scheduler: "Any", + schedule_entry: "Any", + integration: "sentry_sdk.integrations.celery.CeleryIntegration", +) -> None: + """ + Add Sentry Crons information to the schedule_entry headers. + """ + if not integration.monitor_beat_tasks: + return + + monitor_name = schedule_entry.name + + task_should_be_excluded = match_regex_list( + monitor_name, integration.exclude_beat_tasks + ) + if task_should_be_excluded: + return + + celery_schedule = schedule_entry.schedule + app = scheduler.app + + monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) + + is_supported_schedule = bool(monitor_config) + if not is_supported_schedule: + return + + headers = schedule_entry.options.pop("headers", {}) + headers.update( + { + "sentry-monitor-slug": monitor_name, + "sentry-monitor-config": monitor_config, + } + ) + + check_in_id = capture_checkin( + monitor_slug=monitor_name, + monitor_config=monitor_config, + status=MonitorStatus.IN_PROGRESS, + ) + headers.update({"sentry-monitor-check-in-id": check_in_id}) + + # Set the Sentry configuration in the options of the ScheduleEntry. + # Those will be picked up in `apply_async` and added to the headers. + schedule_entry.options["headers"] = headers + + +def _wrap_beat_scheduler( + original_function: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Makes sure that: + - a new Sentry trace is started for each task started by Celery Beat and + it is propagated to the task. + - the Sentry Crons information is set in the Celery Beat task's + headers so that is monitored with Sentry Crons. + + After the patched function is called, + Celery Beat will call apply_async to put the task in the queue. + """ + # Patch only once + # Can't use __name__ here, because some of our tests mock original_apply_entry + already_patched = "sentry_patched_scheduler" in str(original_function) + if already_patched: + return original_function + + from sentry_sdk.integrations.celery import CeleryIntegration + + def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(CeleryIntegration) + if integration is None: + return original_function(*args, **kwargs) + + # Tasks started by Celery Beat start a new Trace + scope = sentry_sdk.get_isolation_scope() + scope.set_new_propagation_context() + scope._name = "celery-beat" + + scheduler, schedule_entry = args + _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) + + return original_function(*args, **kwargs) + + return sentry_patched_scheduler + + +def _patch_beat_apply_entry() -> None: + Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) + + +def _patch_redbeat_apply_async() -> None: + if RedBeatScheduler is None: + return + + RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) + + +def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: + if monitor_beat_tasks: + task_success.connect(crons_task_success) + task_failure.connect(crons_task_failure) + task_retry.connect(crons_task_retry) + + +def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_success %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.OK, + ) + + +def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_failure %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) + + +def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_retry %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py new file mode 100644 index 0000000000..8d181f1f24 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py @@ -0,0 +1,32 @@ +import time +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from typing import Tuple + + from sentry_sdk._types import MonitorConfigScheduleUnit + + +def _now_seconds_since_epoch() -> float: + # We cannot use `time.perf_counter()` when dealing with the duration + # of a Celery task, because the start of a Celery task and + # the end are recorded in different processes. + # Start happens in the Celery Beat process, + # the end in a Celery Worker process. + return time.time() + + +def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": + TIME_UNITS = ( # noqa: N806 + ("day", 60 * 60 * 24.0), + ("hour", 60 * 60.0), + ("minute", 60.0), + ) + + seconds = float(seconds) + for unit, divider in TIME_UNITS: + if seconds >= divider: + interval = int(seconds / divider) + return (interval, cast("MonitorConfigScheduleUnit", unit)) + + return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py new file mode 100644 index 0000000000..9baa0e5cdd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py @@ -0,0 +1,188 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.aws_lambda import _make_request_event_processor +from sentry_sdk.traces import ( + SpanStatus, + StreamedSpan, + get_current_span, +) +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + parse_version, + reraise, +) + +try: + import chalice # type: ignore + from chalice import Chalice, ChaliceViewError + from chalice import __version__ as CHALICE_VERSION + from chalice.app import ( # type: ignore + EventSourceHandler as ChaliceEventSourceHandler, + ) +except ImportError: + raise DidNotEnable("Chalice is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) + + +class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore + def __call__(self, event: "Any", context: "Any") -> "Any": + client = sentry_sdk.get_client() + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor(event, context, configured_time) + ) + try: + return ChaliceEventSourceHandler.__call__(self, event, context) + except Exception: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + client.flush() + reraise(*exc_info) + + +def _get_view_function_response( + app: "Any", view_function: "F", function_args: "Any" +) -> "F": + @wraps(view_function) + def wrapped_view_function(**function_args: "Any") -> "Any": + client = sentry_sdk.get_client() + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = app.lambda_context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor( + app.current_request.to_dict(), + app.lambda_context, + configured_time, + ) + ) + + if has_span_streaming_enabled(client.options): + current_span = get_current_span() + segment = None + if type(current_span) is StreamedSpan: + # A segment already exists (created by the AWS Lambda + # integration), so decorate it with Chalice attributes + # The AWS Lambda integration owns the span lifecycle + # (end + flush), but Chalice converts unhandled view exceptions + # into 500 responses, so the error must be captured here. + request_dict = app.current_request.to_dict() + headers = request_dict.get("headers", {}) + + header_attrs: "Dict[str, Any]" = {} + for header, value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attrs[f"http.request.header.{header.lower()}"] = value + + additional_attrs: "Dict[str, Any]" = {} + if "method" in request_dict: + additional_attrs["http.request.method"] = request_dict["method"] + + attributes = { + "sentry.origin": ChaliceIntegration.origin, + **header_attrs, + **additional_attrs, + } + + segment = current_span._segment + segment.set_attributes(attributes) + + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + if segment: + segment.status = SpanStatus.ERROR.value + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + if segment is None: + client.flush() + raise + else: + scope.set_transaction_name( + app.lambda_context.function_name, + source=TransactionSource.COMPONENT, + ) + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + client.flush() + raise + + return wrapped_view_function # type: ignore + + +class ChaliceIntegration(Integration): + identifier = "chalice" + origin = f"auto.function.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(CHALICE_VERSION) + + if version is None: + raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) + + if version < (1, 20): + old_get_view_function_response = Chalice._get_view_function_response + else: + from chalice.app import RestAPIEventHandler + + old_get_view_function_response = ( + RestAPIEventHandler._get_view_function_response + ) + + def sentry_event_response( + app: "Any", view_function: "F", function_args: "Dict[str, Any]" + ) -> "Any": + wrapped_view_function = _get_view_function_response( + app, view_function, function_args + ) + + return old_get_view_function_response( + app, wrapped_view_function, function_args + ) + + if version < (1, 20): + Chalice._get_view_function_response = sentry_event_response + else: + RestAPIEventHandler._get_view_function_response = sentry_event_response + # for everything else (like events) + chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py new file mode 100644 index 0000000000..e6b3009548 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py @@ -0,0 +1,211 @@ +import functools +from typing import TYPE_CHECKING, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import Any, Callable, ParamSpec, Union +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + + +try: + from clickhouse_driver import VERSION # type: ignore[import-not-found] + from clickhouse_driver.client import Client # type: ignore[import-not-found] + from clickhouse_driver.connection import ( # type: ignore[import-not-found] + Connection, + ) + +except ImportError: + raise DidNotEnable("clickhouse-driver not installed.") + + +class ClickhouseDriverIntegration(Integration): + identifier = "clickhouse_driver" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(ClickhouseDriverIntegration, VERSION) + + # Every query is done using the Connection's `send_query` function + Connection.send_query = _wrap_start(Connection.send_query) + + # If the query contains parameters then the send_data function is used to send those parameters to clickhouse + _wrap_send_data() + + # Every query ends either with the Client's `receive_end_of_query` (no result expected) + # or its `receive_result` (result expected) + Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) + if hasattr(Client, "receive_end_of_insert_query"): + # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` + Client.receive_end_of_insert_query = _wrap_end( + Client.receive_end_of_insert_query + ) + Client.receive_result = _wrap_end(Client.receive_result) + + +P = ParamSpec("P") +T = TypeVar("T") + + +def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": + @functools.wraps(f) + def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(ClickhouseDriverIntegration) is None: + return f(*args, **kwargs) + + connection = args[0] + query = args[1] + query_id = args[2] if len(args) > 2 else kwargs.get("query_id") + params = args[3] if len(args) > 3 else kwargs.get("params") + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=query, # type: ignore + attributes={ + "sentry.op": OP.DB, + "sentry.origin": ClickhouseDriverIntegration.origin, + SPANDATA.DB_QUERY_TEXT: str(query), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=ClickhouseDriverIntegration.origin, + ) + + span.set_data("query", query) + + if query_id: + span.set_data("db.query_id", query_id) + + if params and should_send_default_pii(): + span.set_data("db.params", params) + + connection._sentry_span = span # type: ignore[attr-defined] + + _set_db_data(span, connection) + + # run the original code + ret = f(*args, **kwargs) + + return ret + + return _inner + + +def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": + def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": + res = f(*args, **kwargs) + instance = args[0] + span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] + + if span is None: + return res + + if isinstance(span, StreamedSpan): + span.end() + else: + if res is not None and should_send_default_pii(): + span.set_data("db.result", res) + + with capture_internal_exceptions(): + span.scope.add_breadcrumb( + message=span._data.pop("query"), category="query", data=span._data + ) + + span.finish() + + return res + + return _inner_end + + +def _wrap_send_data() -> None: + original_send_data = Client.send_data + + def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data + self, sample_block, data, types_check=False, columnar=False, *args, **kwargs + ): + span = getattr(self.connection, "_sentry_span", None) + + if isinstance(span, StreamedSpan): + _set_db_data(span, self.connection) + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + if span is not None: + _set_db_data(span, self.connection) + + if should_send_default_pii(): + db_params = span._data.get("db.params", []) + + if isinstance(data, (list, tuple)): + db_params.extend(data) + + else: # data is a generic iterator + orig_data = data + + # Wrap the generator to add items to db.params as they are yielded. + # This allows us to send the params to Sentry without needing to allocate + # memory for the entire generator at once. + def wrapped_generator() -> "Iterator[Any]": + for item in orig_data: + db_params.append(item) + yield item + + # Replace the original iterator with the wrapped one. + data = wrapped_generator() + + span.set_data("db.params", db_params) + + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + Client.send_data = _inner_send_data + + +def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") + span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) + + set_on_span = span.set_attribute + else: + span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") + span.set_data(SPANDATA.DB_NAME, connection.database) + + set_on_span = span.set_data + + set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") + set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) + set_on_span(SPANDATA.SERVER_PORT, connection.port) + set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py new file mode 100644 index 0000000000..f6285d0a9b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py @@ -0,0 +1,273 @@ +import json +from typing import TYPE_CHECKING + +import urllib3 + +from sentry_sdk.api import set_context +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Dict + + +CONTEXT_TYPE = "cloud_resource" + +HTTP_TIMEOUT = 2.0 + +AWS_METADATA_HOST = "169.254.169.254" +AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) +AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( + AWS_METADATA_HOST +) + +GCP_METADATA_HOST = "metadata.google.internal" +GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( + GCP_METADATA_HOST +) + + +class CLOUD_PROVIDER: # noqa: N801 + """ + Name of the cloud provider. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + ALIBABA = "alibaba_cloud" + AWS = "aws" + AZURE = "azure" + GCP = "gcp" + IBM = "ibm_cloud" + TENCENT = "tencent_cloud" + + +class CLOUD_PLATFORM: # noqa: N801 + """ + The cloud platform. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + AWS_EC2 = "aws_ec2" + AWS_LAMBDA = "aws_lambda" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" + + +class CloudResourceContextIntegration(Integration): + """ + Adds cloud resource context to the Senty scope + """ + + identifier = "cloudresourcecontext" + + cloud_provider = "" + + aws_token = "" + http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) + + gcp_metadata = None + + def __init__(self, cloud_provider: str = "") -> None: + CloudResourceContextIntegration.cloud_provider = cloud_provider + + @classmethod + def _is_aws(cls) -> bool: + try: + r = cls.http.request( + "PUT", + AWS_TOKEN_URL, + headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, + ) + + if r.status != 200: + return False + + cls.aws_token = r.data.decode() + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking AWS metadata service: %s", str(e)) + return False + + @classmethod + def _get_aws_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.AWS, + "cloud.platform": CLOUD_PLATFORM.AWS_EC2, + } + + try: + r = cls.http.request( + "GET", + AWS_METADATA_URL, + headers={"X-aws-ec2-metadata-token": cls.aws_token}, + ) + + if r.status != 200: + return ctx + + data = json.loads(r.data.decode("utf-8")) + + try: + ctx["cloud.account.id"] = data["accountId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = data["availabilityZone"] + except Exception: + pass + + try: + ctx["cloud.region"] = data["region"] + except Exception: + pass + + try: + ctx["host.id"] = data["instanceId"] + except Exception: + pass + + try: + ctx["host.type"] = data["instanceType"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching AWS metadata: %s", str(e)) + + return ctx + + @classmethod + def _is_gcp(cls) -> bool: + try: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return False + + cls.gcp_metadata = json.loads(r.data.decode("utf-8")) + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking GCP metadata service: %s", str(e)) + return False + + @classmethod + def _get_gcp_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.GCP, + "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, + } + + gcp_metadata = cls.gcp_metadata + try: + if cls.gcp_metadata is None: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return ctx + + gcp_metadata = json.loads(r.data.decode("utf-8")) + cls.gcp_metadata = gcp_metadata + + try: + ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( + "/" + )[-1] + except Exception: + pass + + try: + # only populated in google cloud run + ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] + except Exception: + pass + + try: + ctx["host.id"] = gcp_metadata["instance"]["id"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching GCP metadata: %s", str(e)) + + return ctx + + @classmethod + def _get_cloud_provider(cls) -> str: + if cls._is_aws(): + return CLOUD_PROVIDER.AWS + + if cls._is_gcp(): + return CLOUD_PROVIDER.GCP + + return "" + + @classmethod + def _get_cloud_resource_context(cls) -> "Dict[str, str]": + cloud_provider = ( + cls.cloud_provider + if cls.cloud_provider != "" + else CloudResourceContextIntegration._get_cloud_provider() + ) + if cloud_provider in context_getters.keys(): + return context_getters[cloud_provider]() + + return {} + + @staticmethod + def setup_once() -> None: + cloud_provider = CloudResourceContextIntegration.cloud_provider + unsupported_cloud_provider = ( + cloud_provider != "" and cloud_provider not in context_getters.keys() + ) + + if unsupported_cloud_provider: + logger.warning( + "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", + CloudResourceContextIntegration.cloud_provider, + list(context_getters.keys()), + ) + + context = CloudResourceContextIntegration._get_cloud_resource_context() + if context != {}: + set_context(CONTEXT_TYPE, context) + + +# Map with the currently supported cloud providers +# mapping to functions extracting the context +context_getters = { + CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, + CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, +} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py new file mode 100644 index 0000000000..7abf3f6808 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py @@ -0,0 +1,303 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise + +try: + from cohere import ( + ChatStreamEndEvent, + NonStreamedChatResponse, + ) + from cohere.base_client import BaseCohere + from cohere.client import Client + + if TYPE_CHECKING: + from cohere import StreamedChatResponse +except ImportError: + raise DidNotEnable("Cohere not installed") + +try: + # cohere 5.9.3+ + from cohere import StreamEndStreamedChatResponse +except ImportError: + from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse + + +COLLECTED_CHAT_PARAMS = { + "model": SPANDATA.AI_MODEL_ID, + "k": SPANDATA.AI_TOP_K, + "p": SPANDATA.AI_TOP_P, + "seed": SPANDATA.AI_SEED, + "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, + "raw_prompting": SPANDATA.AI_RAW_PROMPTING, +} + +COLLECTED_PII_CHAT_PARAMS = { + "tools": SPANDATA.AI_TOOLS, + "preamble": SPANDATA.AI_PREAMBLE, +} + +COLLECTED_CHAT_RESP_ATTRS = { + "generation_id": SPANDATA.AI_GENERATION_ID, + "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, + "finish_reason": SPANDATA.AI_FINISH_REASON, +} + +COLLECTED_PII_CHAT_RESP_ATTRS = { + "citations": SPANDATA.AI_CITATIONS, + "documents": SPANDATA.AI_DOCUMENTS, + "search_queries": SPANDATA.AI_SEARCH_QUERIES, + "search_results": SPANDATA.AI_SEARCH_RESULTS, + "tool_calls": SPANDATA.AI_TOOL_CALLS, +} + + +class CohereIntegration(Integration): + identifier = "cohere" + origin = f"auto.ai.{identifier}" + + def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) + Client.embed = _wrap_embed(Client.embed) + BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "cohere", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _end_span(span: "Any") -> None: + if isinstance(span, StreamedSpan): + span.end() + else: + span.__exit__(None, None, None) + + +def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": + def collect_chat_response_fields( + span: "Union[Span, StreamedSpan]", + res: "NonStreamedChatResponse", + include_pii: bool, + ) -> None: + if include_pii: + if hasattr(res, "text"): + set_data_normalized( + span, + SPANDATA.AI_RESPONSES, + [res.text], + ) + for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: + if hasattr(res, pii_attr): + set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) + + for attr in COLLECTED_CHAT_RESP_ATTRS: + if hasattr(res, attr): + set_data_normalized(span, "ai." + attr, getattr(res, attr)) + + if hasattr(res, "meta"): + if hasattr(res.meta, "billed_units"): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + output_tokens=res.meta.billed_units.output_tokens, + ) + elif hasattr(res.meta, "tokens"): + record_token_usage( + span, + input_tokens=res.meta.tokens.input_tokens, + output_tokens=res.meta.tokens.output_tokens, + ) + + if hasattr(res.meta, "warnings"): + set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) + + @wraps(f) + def new_chat(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if ( + integration is None + or "message" not in kwargs + or not isinstance(kwargs.get("message"), str) + ): + return f(*args, **kwargs) + + message = kwargs.get("message") + + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name="cohere.client.Chat", + attributes={ + "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + name="cohere.client.Chat", + origin=CohereIntegration.origin, + ) + span.__enter__() + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + + reraise(*exc_info) + + with capture_internal_exceptions(): + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, + SPANDATA.AI_INPUT_MESSAGES, + list( + map( + lambda x: { + "role": getattr(x, "role", "").lower(), + "content": getattr(x, "message", ""), + }, + kwargs.get("chat_history", []), + ) + ) + + [{"role": "user", "content": message}], + ) + for k, v in COLLECTED_PII_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + + for k, v in COLLECTED_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + set_data_normalized(span, SPANDATA.AI_STREAMING, False) + + if streaming: + old_iterator = res + + def new_iterator() -> "Iterator[StreamedChatResponse]": + with capture_internal_exceptions(): + for x in old_iterator: + if isinstance(x, ChatStreamEndEvent) or isinstance( + x, StreamEndStreamedChatResponse + ): + collect_chat_response_fields( + span, + x.response, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + yield x + _end_span(span) + + return new_iterator() + elif isinstance(res, NonStreamedChatResponse): + collect_chat_response_fields( + span, + res, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + _end_span(span) + else: + set_data_normalized(span, "unknown_response", True) + _end_span(span) + return res + + return new_chat + + +def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + if integration is None: + return f(*args, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name="Cohere Embedding Creation", + attributes={ + "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span_ctx = get_start_span_function()( + op=consts.OP.COHERE_EMBEDDINGS_CREATE, + name="Cohere Embedding Creation", + origin=CohereIntegration.origin, + ) + + with span_ctx as span: + if "texts" in kwargs and ( + should_send_default_pii() and integration.include_prompts + ): + if isinstance(kwargs["texts"], str): + set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) + elif ( + isinstance(kwargs["texts"], list) + and len(kwargs["texts"]) > 0 + and isinstance(kwargs["texts"][0], str) + ): + set_data_normalized( + span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] + ) + + if "model" in kwargs: + set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) + if ( + hasattr(res, "meta") + and hasattr(res.meta, "billed_units") + and hasattr(res.meta.billed_units, "input_tokens") + ): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + total_tokens=res.meta.billed_units.input_tokens, + ) + return res + + return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py new file mode 100644 index 0000000000..a0e9014666 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py @@ -0,0 +1,62 @@ +import weakref +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import ContextVar, logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class DedupeIntegration(Integration): + identifier = "dedupe" + + def __init__(self) -> None: + self._last_seen = ContextVar("last-seen") + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if hint is None: + return event + + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return event + + exc_info = hint.get("exc_info", None) + if exc_info is None: + return event + + last_seen = integration._last_seen.get(None) + if last_seen is not None: + # last_seen is either a weakref or the original instance + last_seen = ( + last_seen() if isinstance(last_seen, weakref.ref) else last_seen + ) + + exc = exc_info[1] + if last_seen is exc: + logger.info("DedupeIntegration dropped duplicated error event %s", exc) + return None + + # we can only weakref non builtin types + try: + integration._last_seen.set(weakref.ref(exc)) + except TypeError: + integration._last_seen.set(exc) + + return event + + @staticmethod + def reset_last_seen() -> None: + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return + + integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py new file mode 100644 index 0000000000..361b60079d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py @@ -0,0 +1,884 @@ +import inspect +import sys +import threading +import weakref +from importlib import import_module + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANNAME +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + transaction_from_function, + walk_exception_chain, +) + +try: + from django import VERSION as DJANGO_VERSION + from django.conf import settings + from django.conf import settings as django_settings + from django.core import signals + from django.utils.functional import SimpleLazyObject + + try: + from django.urls import resolve + except ImportError: + from django.core.urlresolvers import resolve + + try: + from django.urls import Resolver404 + except ImportError: + from django.core.urlresolvers import Resolver404 + + # Only available in Django 3.0+ + try: + from django.core.handlers.asgi import ASGIRequest + except Exception: + ASGIRequest = None + +except ImportError: + raise DidNotEnable("Django not installed") + +from sentry_sdk.integrations.django.middleware import patch_django_middlewares +from sentry_sdk.integrations.django.signals_handlers import patch_signals +from sentry_sdk.integrations.django.tasks import patch_tasks +from sentry_sdk.integrations.django.templates import ( + get_template_frame_from_exception, + patch_templates, +) +from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER +from sentry_sdk.integrations.django.views import patch_views + +if DJANGO_VERSION[:2] > (1, 8): + from sentry_sdk.integrations.django.caching import patch_caching +else: + patch_caching = None # type: ignore + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from django.core.handlers.wsgi import WSGIRequest + from django.http.request import QueryDict + from django.http.response import HttpResponse + from django.utils.datastructures import MultiValueDict + + from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +if DJANGO_VERSION < (1, 10): + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated() + +else: + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated + + +TRANSACTION_STYLE_VALUES = ("function_name", "url") + + +class DjangoIntegration(Integration): + """ + Auto instrument a Django application. + + :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. + :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. + :param signals_spans: Whether to create spans for signals. Defaults to `True`. + :param signals_denylist: A list of signals to ignore when creating spans. + :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. + """ + + identifier = "django" + origin = f"auto.http.{identifier}" + origin_db = f"auto.db.{identifier}" + + transaction_style = "" + middleware_spans: "Optional[bool]" = None + signals_spans: "Optional[bool]" = None + cache_spans: "Optional[bool]" = None + signals_denylist: "list[signals.Signal]" = [] + + def __init__( + self, + transaction_style: str = "url", + middleware_spans: bool = False, + signals_spans: bool = True, + cache_spans: bool = False, + db_transaction_spans: bool = False, + signals_denylist: "Optional[list[signals.Signal]]" = None, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + + self.signals_spans = signals_spans + self.signals_denylist = signals_denylist or [] + + self.cache_spans = cache_spans + self.db_transaction_spans = db_transaction_spans + + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(DjangoIntegration, DJANGO_VERSION) + + install_sql_hook() + # Patch in our custom middleware. + + # logs an error for every 500 + ignore_logger("django.server") + ignore_logger("django.request") + + from django.core.handlers.wsgi import WSGIHandler + + old_app = WSGIHandler.__call__ + + @ensure_integration_enabled(DjangoIntegration, old_app) + def sentry_patched_wsgi_handler( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + bound_old_app = old_app.__get__(self, WSGIHandler) + + from django.conf import settings + + use_x_forwarded_for = settings.USE_X_FORWARDED_HOST + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + middleware = SentryWsgiMiddleware( + bound_old_app, + use_x_forwarded_for, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + WSGIHandler.__call__ = sentry_patched_wsgi_handler + + _patch_get_response() + + _patch_django_asgi_handler() + + signals.got_request_exception.connect(_got_request_exception) + + @add_global_event_processor + def process_django_templates( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_, exc_value, _) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + frame = get_template_frame_from_exception(exc_value) + if frame is not None: + frames = exception.get("stacktrace", {}).get("frames", []) + + for i in reversed(range(len(frames))): + f = frames[i] + if ( + f.get("function") in ("Parser.parse", "parse", "render") + and f.get("module") == "django.template.base" + ): + i += 1 + break + else: + i = len(frames) + + frames.insert(i, frame) + + return event + + @add_global_repr_processor + def _django_queryset_repr( + value: "Any", hint: "Dict[str, Any]" + ) -> "Union[NotImplementedType, str]": + try: + # Django 1.6 can fail to import `QuerySet` when Django settings + # have not yet been initialized. + # + # If we fail to import, return `NotImplemented`. It's at least + # unlikely that we have a query set in `value` when importing + # `QuerySet` fails. + from django.db.models.query import QuerySet + except Exception: + return NotImplemented + + if not isinstance(value, QuerySet) or value._result_cache: + return NotImplemented + + return "<%s from %s at 0x%x>" % ( + value.__class__.__name__, + value.__module__, + id(value), + ) + + _patch_channels() + patch_django_middlewares() + patch_views() + patch_templates() + patch_signals() + patch_tasks() + add_template_context_repr_sequence() + + if patch_caching is not None: + patch_caching() + + +_DRF_PATCHED = False +_DRF_PATCH_LOCK = threading.Lock() + + +def _patch_drf() -> None: + """ + Patch Django Rest Framework for more/better request data. DRF's request + type is a wrapper around Django's request type. The attribute we're + interested in is `request.data`, which is a cached property containing a + parsed request body. Reading a request body from that property is more + reliable than reading from any of Django's own properties, as those don't + hold payloads in memory and therefore can only be accessed once. + + We patch the Django request object to include a weak backreference to the + DRF request object, such that we can later use either in + `DjangoRequestExtractor`. + + This function is not called directly on SDK setup, because importing almost + any part of Django Rest Framework will try to access Django settings (where + `sentry_sdk.init()` might be called from in the first place). Instead we + run this function on every request and do the patching on the first + request. + """ + + global _DRF_PATCHED + + if _DRF_PATCHED: + # Double-checked locking + return + + with _DRF_PATCH_LOCK: + if _DRF_PATCHED: + return + + # We set this regardless of whether the code below succeeds or fails. + # There is no point in trying to patch again on the next request. + _DRF_PATCHED = True + + with capture_internal_exceptions(): + try: + from rest_framework.views import APIView # type: ignore + except ImportError: + pass + else: + old_drf_initial = APIView.initial + + def sentry_patched_drf_initial( + self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + with capture_internal_exceptions(): + request._request._sentry_drf_request_backref = weakref.ref( + request + ) + pass + return old_drf_initial(self, request, *args, **kwargs) + + APIView.initial = sentry_patched_drf_initial + + +def _patch_channels() -> None: + try: + from channels.http import AsgiHandler # type: ignore + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because channels may not be used at all in + # the current process. That is the case when running traditional WSGI + # workers in gunicorn+gevent and the websocket stuff in a separate + # process. + logger.warning( + "We detected that you are using Django channels 2.0." + + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl + + patch_channels_asgi_handler_impl(AsgiHandler) + + +def _patch_django_asgi_handler() -> None: + try: + from django.core.handlers.asgi import ASGIHandler + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because Django's ASGI stuff may not be used + # at all. + logger.warning( + "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl + + patch_django_asgi_handler_impl(ASGIHandler) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" +) -> None: + try: + transaction_name = None + if transaction_style == "function_name": + fn = resolve(request.path).func + transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) + + elif transaction_style == "url": + if hasattr(request, "urlconf"): + transaction_name = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + else: + transaction_name = LEGACY_RESOLVER.resolve(request.path_info) + + if transaction_name is None: + transaction_name = request.path_info + source = TransactionSource.URL + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name( + transaction_name, + source=source, + ) + except Resolver404: + urlconf = import_module(settings.ROOT_URLCONF) + # This exception only gets thrown when transaction_style is `function_name` + # So we don't check here what style is configured + if hasattr(urlconf, "handler404"): + handler = urlconf.handler404 + if isinstance(handler, str): + scope.transaction = handler + else: + scope.transaction = transaction_from_function( + getattr(handler, "view_class", handler) + ) + except Exception: + pass + + +def _before_get_response(request: "WSGIRequest") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return + + _patch_drf() + + scope = sentry_sdk.get_current_scope() + # Rely on WSGI middleware to start a trace + _set_transaction_name_and_source(scope, integration.transaction_style, request) + + scope.add_event_processor( + _make_wsgi_request_event_processor(weakref.ref(request), integration) + ) + + +def _attempt_resolve_again( + request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str +) -> None: + """ + Some django middlewares overwrite request.urlconf + so we need to respect that contract, + so we try to resolve the url again. + """ + if not hasattr(request, "urlconf"): + return + + _set_transaction_name_and_source(scope, transaction_style, request) + + +def _after_get_response(request: "WSGIRequest") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming and should_send_default_pii(): + user = getattr(request, "user", None) + + # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. + # Exit early if the user has not been materialized yet. + is_lazy = isinstance(user, SimpleLazyObject) + if is_lazy and hasattr(request, "_cached_user"): + user = request._cached_user + elif is_lazy: + return + + if user is None or not is_authenticated(user): + return + + user_info = {} + try: + user_info["id"] = str(user.pk) + except Exception: + pass + + try: + user_info["email"] = user.email + except Exception: + pass + + try: + user_info["username"] = user.get_username() + except Exception: + pass + + sentry_sdk.set_user(user_info) + + +def _patch_get_response() -> None: + """ + patch get_response, because at that point we have the Django request object + """ + from django.core.handlers.base import BaseHandler + + old_get_response = BaseHandler.get_response + + def sentry_patched_get_response( + self: "Any", request: "WSGIRequest" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + rv = old_get_response(self, request) + _after_get_response(request) + return rv + + BaseHandler.get_response = sentry_patched_get_response + + if hasattr(BaseHandler, "get_response_async"): + from sentry_sdk.integrations.django.asgi import patch_get_response_async + + patch_get_response_async(BaseHandler, _before_get_response) + + +def _make_wsgi_request_event_processor( + weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" +) -> "EventProcessor": + def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + request = weak_request() + if request is None: + return event + + django_3 = ASGIRequest is not None + if django_3 and type(request) == ASGIRequest: + # We have a `asgi_request_event_processor` for this. + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return wsgi_request_event_processor + + +def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if request is not None and integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + event, hint = event_from_exception( + sys.exc_info(), + client_options=client.options, + mechanism={"type": "django", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class DjangoRequestExtractor(RequestExtractor): + def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: + try: + drf_request = request._sentry_drf_request_backref() + if drf_request is not None: + request = drf_request + except AttributeError: + pass + self.request = request + + def env(self) -> "Dict[str, str]": + return self.request.META + + def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": + privacy_cookies = [ + django_settings.CSRF_COOKIE_NAME, + django_settings.SESSION_COOKIE_NAME, + ] + + clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} + for key, val in self.request.COOKIES.items(): + if key in privacy_cookies: + clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE + else: + clean_cookies[key] = val + + return clean_cookies + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "QueryDict": + return self.request.POST + + def files(self) -> "MultiValueDict": + return self.request.FILES + + def size_of_file(self, file: "Any") -> int: + return file.size + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + return self.request.data + except Exception: + return RequestExtractor.parsed_body(self) + + +def _set_user_info(request: "WSGIRequest", event: "Event") -> None: + user_info = event.setdefault("user", {}) + + user = getattr(request, "user", None) + + if user is None or not is_authenticated(user): + return + + try: + user_info.setdefault("id", str(user.pk)) + except Exception: + pass + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.get_username()) + except Exception: + pass + + +def install_sql_hook() -> None: + """If installed this causes Django's queries to be captured.""" + try: + from django.db.backends.utils import CursorWrapper + except ImportError: + from django.db.backends.util import CursorWrapper + + try: + # django 1.6 and 1.7 compatability + from django.db.backends import BaseDatabaseWrapper + except ImportError: + # django 1.8 or later + from django.db.backends.base.base import BaseDatabaseWrapper + + try: + real_execute = CursorWrapper.execute + real_executemany = CursorWrapper.executemany + real_connect = BaseDatabaseWrapper.connect + real_commit = BaseDatabaseWrapper._commit + real_rollback = BaseDatabaseWrapper._rollback + except AttributeError: + # This won't work on Django versions < 1.6 + return + + @ensure_integration_enabled(DjangoIntegration, real_execute) + def execute( + self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=params, + paramstyle="format", + executemany=False, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + result = real_execute(self, sql, params) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_executemany) + def executemany( + self: "CursorWrapper", sql: "Any", param_list: "List[Any]" + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=param_list, + paramstyle="format", + executemany=True, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + + result = real_executemany(self, sql, param_list) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_connect) + def connect(self: "BaseDatabaseWrapper") -> None: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message="connect", category="query") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="connect", + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self) + return real_connect(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + return real_connect(self) + + def _commit(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_commit(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_COMMIT, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_COMMIT, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + + def _rollback(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_rollback(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_ROLLBACK, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_ROLLBACK, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + + CursorWrapper.execute = execute + CursorWrapper.executemany = executemany + BaseDatabaseWrapper.connect = connect + BaseDatabaseWrapper._commit = _commit + BaseDatabaseWrapper._rollback = _rollback + ignore_logger("django.db.backends") + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", + cursor_or_db: "Any", + db_operation: "Optional[str]" = None, +) -> None: + db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db + vendor = db.vendor + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) + + if db_operation is not None: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) + else: + span.set_data(SPANDATA.DB_SYSTEM, vendor) + + if db_operation is not None: + span.set_data(SPANDATA.DB_OPERATION, db_operation) + + # Some custom backends override `__getattr__`, making it look like `cursor_or_db` + # actually has a `connection` and the `connection` has a `get_dsn_parameters` + # attribute, only to throw an error once you actually want to call it. + # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable + # function. + is_psycopg2 = ( + hasattr(cursor_or_db, "connection") + and hasattr(cursor_or_db.connection, "get_dsn_parameters") + and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + ) + if is_psycopg2: + connection_params = cursor_or_db.connection.get_dsn_parameters() + else: + try: + # psycopg3, only extract needed params as get_parameters + # can be slow because of the additional logic to filter out default + # values + connection_params = { + "dbname": cursor_or_db.connection.info.dbname, + "port": cursor_or_db.connection.info.port, + } + # PGhost returns host or base dir of UNIX socket as an absolute path + # starting with /, use it only when it contains host + pg_host = cursor_or_db.connection.info.host + if pg_host and not pg_host.startswith("/"): + connection_params["host"] = pg_host + except Exception: + connection_params = db.get_connection_params() + + db_name = connection_params.get("dbname") or connection_params.get("database") + + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + + set_on_span = span.set_attribute + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + set_on_span = span.set_data + + server_address = connection_params.get("host") + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = connection_params.get("port") + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, str(server_port)) + + server_socket_address = connection_params.get("unix_socket") + if server_socket_address is not None: + set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) + + +def add_template_context_repr_sequence() -> None: + try: + from django.template.context import BaseContext + + add_repr_sequence_type(BaseContext) + except Exception: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py new file mode 100644 index 0000000000..43faffb5be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py @@ -0,0 +1,262 @@ +""" +Instrumentation for Django 3.0 + +Since this file contains `async def` it is conditionally imported in +`sentry_sdk.integrations.django` (depending on the existence of +`django.core.handlers.asgi`. +""" + +import asyncio +import functools +import inspect +from typing import TYPE_CHECKING + +from django.core.handlers.wsgi import WSGIRequest + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar, Union + + from django.core.handlers.asgi import ASGIRequest + from django.http.response import HttpResponse + + from sentry_sdk._types import Event, EventProcessor + + _F = TypeVar("_F", bound=Callable[..., Any]) + + +# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for +# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. +# The latter is replaced with the inspect.markcoroutinefunction decorator. +# Until 3.12 is the minimum supported Python version, provide a shim. +# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py +if hasattr(inspect, "markcoroutinefunction"): + iscoroutinefunction = inspect.iscoroutinefunction + markcoroutinefunction = inspect.markcoroutinefunction +else: + iscoroutinefunction = asyncio.iscoroutinefunction + + def markcoroutinefunction(func: "_F") -> "_F": + func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + return func + + +def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": + def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + from sentry_sdk.integrations.django import ( + DjangoRequestExtractor, + _set_user_info, + ) + + if request is None: + return event + + if type(request) == WSGIRequest: + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return asgi_request_event_processor + + +def patch_django_asgi_handler_impl(cls: "Any") -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + )._run_asgi3 + + return await middleware(scope, receive, send) + + cls.__call__ = sentry_patched_asgi_handler + + modern_django_asgi_support = hasattr(cls, "create_request") + if modern_django_asgi_support: + old_create_request = cls.create_request + + @ensure_integration_enabled(DjangoIntegration, old_create_request) + def sentry_patched_create_request( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request, error_response = old_create_request(self, *args, **kwargs) + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_make_asgi_request_event_processor(request)) + + return request, error_response + + cls.create_request = sentry_patched_create_request + + +def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: + old_get_response_async = cls.get_response_async + + async def sentry_patched_get_response_async( + self: "Any", request: "Any" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + return await old_get_response_async(self, request) + + cls.get_response_async = sentry_patched_get_response_async + + +def patch_channels_asgi_handler_impl(cls: "Any") -> None: + import channels # type: ignore + + from sentry_sdk.integrations.django import DjangoIntegration + + if channels.__version__ < "3.0.0": + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, receive, send) + + middleware = SentryAsgiMiddleware( + lambda _scope: old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + ) + + return await middleware(self.scope)(receive, send) # type: ignore + + cls.__call__ = sentry_patched_asgi_handler + + else: + # The ASGI handler in Channels >= 3 has the same signature as + # the Django handler. + patch_django_asgi_handler_impl(cls) + + +def wrap_async_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + async def sentry_wrapped_callback( + request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return await callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return await callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return await callback(request, *args, **kwargs) + + return sentry_wrapped_callback + + +def _asgi_middleware_mixin_factory( + _check_middleware_span: "Callable[..., Any]", +) -> "Any": + """ + Mixin class factory that generates a middleware mixin for handling requests + in async mode. + """ + + class SentryASGIMixin: + if TYPE_CHECKING: + _inner = None + + def __init__(self, get_response: "Callable[..., Any]") -> None: + self.get_response = get_response + self._acall_method = None + self._async_check() + + def _async_check(self) -> None: + """ + If get_response is a coroutine function, turns us into async mode so + a thread is not consumed during a whole request. + Taken from django.utils.deprecation::MiddlewareMixin._async_check + """ + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) + + def async_route_check(self) -> bool: + """ + Function that checks if we are in async mode, + and if we are forwards the handling of requests to __acall__ + """ + return iscoroutinefunction(self.get_response) + + async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": + f = self._acall_method + if f is None: + if hasattr(self._inner, "__acall__"): + self._acall_method = f = self._inner.__acall__ # type: ignore + else: + self._acall_method = f = self._inner + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return await f(*args, **kwargs) # type: ignore + + with middleware_span: + return await f(*args, **kwargs) # type: ignore + + return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py new file mode 100644 index 0000000000..faf1803c11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py @@ -0,0 +1,264 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.core.cache import CacheHandler +from urllib3.util import parse_url as urlparse + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +METHODS_TO_INSTRUMENT = [ + "set", + "set_many", + "get", + "get_many", +] + + +def _get_span_description( + method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" +) -> str: + return _key_as_string(_get_safe_key(method_name, args, kwargs)) + + +def _patch_cache_method( + cache: "CacheHandler", + method_name: str, + address: "Optional[str]", + port: "Optional[int]", +) -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + original_method = getattr(cache, method_name) + + @ensure_integration_enabled(DjangoIntegration, original_method) + def _instrument_call( + cache: "CacheHandler", + method_name: str, + original_method: "Callable[..., Any]", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + address: "Optional[str]", + port: "Optional[int]", + ) -> "Any": + is_set_operation = method_name.startswith("set") + is_get_method = method_name == "get" + is_get_many_method = method_name == "get_many" + + op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET + description = _get_span_description(method_name, args, kwargs) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": op, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_attribute(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + else: + with sentry_sdk.start_span( + op=op, + name=description, + origin=DjangoIntegration.origin, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_data(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_data(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + + @functools.wraps(original_method) + def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": + return _instrument_call( + cache, method_name, original_method, args, kwargs, address, port + ) + + setattr(cache, method_name, sentry_method) + + +def _patch_cache( + cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None +) -> None: + if not hasattr(cache, "_sentry_patched"): + for method_name in METHODS_TO_INSTRUMENT: + _patch_cache_method(cache, method_name, address, port) + cache._sentry_patched = True + + +def _get_address_port( + settings: "dict[str, Any]", +) -> "tuple[Optional[str], Optional[int]]": + location = settings.get("LOCATION") + + # TODO: location can also be an array of locations + # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis + # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 + if not isinstance(location, str): + return None, None + + if "://" in location: + parsed_url = urlparse(location) + # remove the username and password from URL to not leak sensitive data. + address = "{}://{}{}".format( + parsed_url.scheme or "", + parsed_url.hostname or "", + parsed_url.path or "", + ) + port = parsed_url.port + else: + address = location + port = None + + return address, int(port) if port is not None else None + + +def should_enable_cache_spans() -> bool: + from sentry_sdk.integrations.django import DjangoIntegration + + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + from django.conf import settings + + return integration is not None and ( + (client.spotlight is not None and settings.DEBUG is True) + or integration.cache_spans is True + ) + + +def patch_caching() -> None: + if not hasattr(CacheHandler, "_sentry_patched"): + if DJANGO_VERSION < (3, 2): + original_get_item = CacheHandler.__getitem__ + + @functools.wraps(original_get_item) + def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": + cache = original_get_item(self, alias) + + if should_enable_cache_spans(): + from django.conf import settings + + address, port = _get_address_port( + settings.CACHES[alias or "default"] + ) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.__getitem__ = sentry_get_item + CacheHandler._sentry_patched = True + + else: + original_create_connection = CacheHandler.create_connection + + @functools.wraps(original_create_connection) + def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": + cache = original_create_connection(self, alias) + + if should_enable_cache_spans(): + address, port = _get_address_port(self.settings[alias or "default"]) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.create_connection = sentry_create_connection + CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py new file mode 100644 index 0000000000..a14ec96ff5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py @@ -0,0 +1,219 @@ +""" +Create spans from Django middleware invocations +""" + +from functools import wraps +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + F = TypeVar("F", bound=Callable[..., Any]) + +_import_string_should_wrap_middleware = ContextVar( + "import_string_should_wrap_middleware" +) + +DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) + +if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: + _asgi_middleware_mixin_factory = lambda _: object + iscoroutinefunction = lambda _: False +else: + from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction + + +def patch_django_middlewares() -> None: + from django.core.handlers import base + + old_import_string = base.import_string + + def sentry_patched_import_string(dotted_path: str) -> "Any": + rv = old_import_string(dotted_path) + + if _import_string_should_wrap_middleware.get(None): + rv = _wrap_middleware(rv, dotted_path) + + return rv + + base.import_string = sentry_patched_import_string + + old_load_middleware = base.BaseHandler.load_middleware + + def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": + _import_string_should_wrap_middleware.set(True) + try: + return old_load_middleware(*args, **kwargs) + finally: + _import_string_should_wrap_middleware.set(False) + + base.BaseHandler.load_middleware = sentry_patched_load_middleware + + +def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + def _check_middleware_span( + old_method: "Callable[..., Any]", + ) -> "Optional[Union[Span, StreamedSpan]]": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None or not integration.middleware_spans: + return None + + function_name = transaction_from_function(old_method) + + description = middleware_name + function_basename = getattr(old_method, "__name__", None) + if function_basename: + description = "{}.{}".format(description, function_basename) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + middleware_span: "Union[Span, StreamedSpan]" + if span_streaming: + middleware_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.MIDDLEWARE_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + else: + middleware_span = sentry_sdk.start_span( + op=OP.MIDDLEWARE_DJANGO, + name=description, + origin=DjangoIntegration.origin, + ) + middleware_span.set_tag("django.function_name", function_name) + middleware_span.set_tag("django.middleware_name", middleware_name) + + return middleware_span + + def _get_wrapped_method(old_method: "F") -> "F": + with capture_internal_exceptions(): + # Middleware hooks (e.g. `process_view`, `process_exception`) may be + # `async def` when the middleware is async. A synchronous wrapper + # would hide the coroutine from Django's `iscoroutinefunction` check, + # causing Django to call the hook synchronously and never await the + # returned coroutine. Wrap async hooks with an async wrapper so the + # wrapped method continues to report as a coroutine function. + if iscoroutinefunction is not None and iscoroutinefunction(old_method): + + async def async_sentry_wrapped_method( + *args: "Any", **kwargs: "Any" + ) -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return await old_method(*args, **kwargs) + + with middleware_span: + return await old_method(*args, **kwargs) + + sentry_wrapped_method = async_sentry_wrapped_method + + else: + + def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return old_method(*args, **kwargs) + + with middleware_span: + return old_method(*args, **kwargs) + + sentry_wrapped_method = sync_sentry_wrapped_method + + try: + # fails for __call__ of function on Python 2 (see py2.7-django-1.11) + sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) + + # Necessary for Django 3.1 + sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore + except Exception: + pass + + return sentry_wrapped_method # type: ignore + + return old_method + + class SentryWrappingMiddleware( + _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore + ): + sync_capable = getattr(middleware, "sync_capable", True) + async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( + middleware, "async_capable", False + ) + + def __init__( + self, + get_response: "Optional[Callable[..., Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> None: + if get_response: + self._inner = middleware(get_response, *args, **kwargs) + else: + self._inner = middleware(*args, **kwargs) + self.get_response = get_response + self._call_method = None + if self.async_capable: + super().__init__(get_response) + + # We need correct behavior for `hasattr()`, which we can only determine + # when we have an instance of the middleware we're wrapping. + def __getattr__(self, method_name: str) -> "Any": + if method_name not in ( + "process_request", + "process_view", + "process_template_response", + "process_response", + "process_exception", + ): + raise AttributeError() + + old_method = getattr(self._inner, method_name) + rv = _get_wrapped_method(old_method) + self.__dict__[method_name] = rv + return rv + + def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": + if hasattr(self, "async_route_check") and self.async_route_check(): + return self.__acall__(*args, **kwargs) + + f = self._call_method + if f is None: + self._call_method = f = self._inner.__call__ + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return f(*args, **kwargs) + + with middleware_span: + return f(*args, **kwargs) + + for attr in ( + "__name__", + "__module__", + "__qualname__", + ): + if hasattr(middleware, attr): + setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) + + return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py new file mode 100644 index 0000000000..7140ead782 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py @@ -0,0 +1,105 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from django.dispatch import Signal + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.django import DJANGO_VERSION +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Union + + +def _get_receiver_name(receiver: "Callable[..., Any]") -> str: + name = "" + + if hasattr(receiver, "__qualname__"): + name = receiver.__qualname__ + elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ + name = receiver.__name__ + elif hasattr( + receiver, "func" + ): # certain functions (like partials) dont have a name + if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): + name = "partial()" + + if ( + name == "" + ): # In case nothing was found, return the string representation (this is the slowest case) + return str(receiver) + + if hasattr(receiver, "__module__"): # prepend with module, if there is one + name = receiver.__module__ + "." + name + + return name + + +def patch_signals() -> None: + """ + Patch django signal receivers to create a span. + + This only wraps sync receivers. Django>=5.0 introduced async receivers, but + since we don't create transactions for ASGI Django, we don't wrap them. + """ + from sentry_sdk.integrations.django import DjangoIntegration + + old_live_receivers = Signal._live_receivers + + def _sentry_live_receivers( + self: "Signal", sender: "Any" + ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": + if DJANGO_VERSION >= (5, 0): + sync_receivers, async_receivers = old_live_receivers(self, sender) + else: + sync_receivers = old_live_receivers(self, sender) + async_receivers = [] + + def sentry_sync_receiver_wrapper( + receiver: "Callable[..., Any]", + ) -> "Callable[..., Any]": + @wraps(receiver) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + signal_name = _get_receiver_name(receiver) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + with sentry_sdk.traces.start_span( + name=signal_name, + attributes={ + "sentry.op": OP.EVENT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.CODE_FUNCTION_NAME: signal_name, + }, + ) as span: + return receiver(*args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.EVENT_DJANGO, + name=signal_name, + origin=DjangoIntegration.origin, + ) as span: + span.set_data("signal", signal_name) + return receiver(*args, **kwargs) + + return wrapper + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if ( + integration + and integration.signals_spans + and self not in integration.signals_denylist + ): + for idx, receiver in enumerate(sync_receivers): + sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + + if DJANGO_VERSION >= (5, 0): + return sync_receivers, async_receivers + else: + return sync_receivers + + Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py new file mode 100644 index 0000000000..5e23c258fb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py @@ -0,0 +1,52 @@ +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import qualname_from_function + +try: + # django.tasks were added in Django 6.0 + from django.tasks.base import Task +except ImportError: + Task = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +def patch_tasks() -> None: + if Task is None: + return + + old_task_enqueue = Task.enqueue + + @wraps(old_task_enqueue) + def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return old_task_enqueue(self, *args, **kwargs) + + name = qualname_from_function(self.func) or "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_task_enqueue(self, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin + ): + return old_task_enqueue(self, *args, **kwargs) + + Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py new file mode 100644 index 0000000000..5ab89d4a74 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py @@ -0,0 +1,209 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.template import TemplateSyntaxError +from django.utils.safestring import mark_safe + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Optional, Tuple + +try: + # support Django 1.9 + from django.template.base import Origin +except ImportError: + # backward compatibility + from django.template.loader import LoaderOrigin as Origin + + +def get_template_frame_from_exception( + exc_value: "Optional[BaseException]", +) -> "Optional[Dict[str, Any]]": + # As of Django 1.9 or so the new template debug thing showed up. + if hasattr(exc_value, "template_debug"): + return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore + + # As of r16833 (Django) all exceptions may contain a + # ``django_template_source`` attribute (rather than the legacy + # ``TemplateSyntaxError.source`` check) + if hasattr(exc_value, "django_template_source"): + return _get_template_frame_from_source( + exc_value.django_template_source # type: ignore + ) + + if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): + source = exc_value.source + if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): + return _get_template_frame_from_source(source) # type: ignore + + return None + + +def _get_template_name_description(template_name: str) -> str: + if isinstance(template_name, (list, tuple)): + if template_name: + return "[{}, ...]".format(template_name[0]) + else: + return template_name + + +def patch_templates() -> None: + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + real_rendered_content = SimpleTemplateResponse.rendered_content + + @property # type: ignore + @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) + def rendered_content(self: "SimpleTemplateResponse") -> str: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(self.template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_rendered_content.fget(self) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(self.template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", self.context_data) + return real_rendered_content.fget(self) + + SimpleTemplateResponse.rendered_content = rendered_content + + if DJANGO_VERSION < (1, 7): + return + import django.shortcuts + + real_render = django.shortcuts.render + + @functools.wraps(real_render) + @ensure_integration_enabled(DjangoIntegration, real_render) + def render( + request: "django.http.HttpRequest", + template_name: str, + context: "Optional[Dict[str, Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> "django.http.HttpResponse": + # Inject trace meta tags into template context + context = context or {} + if "sentry_trace_meta" not in context: + context["sentry_trace_meta"] = mark_safe( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_render(request, template_name, context, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", context) + return real_render(request, template_name, context, *args, **kwargs) + + django.shortcuts.render = render + + +def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": + if debug is None: + return None + + lineno = debug["line"] + filename = debug["name"] + if filename is None: + filename = "" + + pre_context = [] + post_context = [] + context_line = None + + for i, line in debug["source_lines"]: + if i < lineno: + pre_context.append(line) + elif i > lineno: + post_context.append(line) + else: + context_line = line + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + "in_app": True, + } + + +def _linebreak_iter(template_source: str) -> "Iterator[int]": + yield 0 + p = template_source.find("\n") + while p >= 0: + yield p + 1 + p = template_source.find("\n", p + 1) + + +def _get_template_frame_from_source( + source: "Tuple[Origin, Tuple[int, int]]", +) -> "Optional[Dict[str, Any]]": + if not source: + return None + + origin, (start, end) = source + filename = getattr(origin, "loadname", None) + if filename is None: + filename = "" + template_source = origin.reload() + lineno = None + upto = 0 + pre_context = [] + post_context = [] + context_line = None + + for num, next in enumerate(_linebreak_iter(template_source)): + line = template_source[upto:next] + if start >= upto and end <= next: + lineno = num + context_line = line + elif lineno is None: + pre_context.append(line) + else: + post_context.append(line) + + upto = next + + if context_line is None or lineno is None: + return None + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py new file mode 100644 index 0000000000..192f0765e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py @@ -0,0 +1,154 @@ +""" +Copied from raven-python. + +Despite being called "legacy" in some places this resolver is very much still +in use. +""" + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from re import Pattern + from typing import Dict, List, Optional, Tuple, Union + + from django.urls.resolvers import URLPattern, URLResolver + +from django import VERSION as DJANGO_VERSION + +if DJANGO_VERSION >= (2, 0): + from django.urls.resolvers import RoutePattern +else: + RoutePattern = None + +try: + from django.urls import get_resolver +except ImportError: + from django.core.urlresolvers import get_resolver + + +def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": + """Utility method for django's deprecated resolver.regex""" + try: + regex = resolver_or_pattern.regex + except AttributeError: + regex = resolver_or_pattern.pattern.regex + return regex + + +class RavenResolver: + _new_style_group_matcher = re.compile( + r"<(?:([^>:]+):)?([^>]+)>" + ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 + _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") + _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") + _non_named_group_matcher = re.compile(r"\([^\)]+\)") + # [foo|bar|baz] + _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") + _camel_re = re.compile(r"([A-Z]+)([a-z])") + + _cache: "Dict[URLPattern, str]" = {} + + def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: + r""" + Clean up urlpattern regexes into something readable by humans: + + From: + > "^(?P\w+)/athletes/(?P\w+)/$" + + To: + > "{sport_slug}/athletes/{athlete_slug}/" + """ + # "new-style" path patterns can be parsed directly without turning them + # into regexes first + if ( + RoutePattern is not None + and hasattr(pattern, "pattern") + and isinstance(pattern.pattern, RoutePattern) + ): + return self._new_style_group_matcher.sub( + lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) + ) + + result = get_regex(pattern).pattern + + # remove optional params + # TODO(dcramer): it'd be nice to change these into [%s] but it currently + # conflicts with the other rules because we're doing regexp matches + # rather than parsing tokens + result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) + + # handle named groups first + result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) + + # handle non-named groups + result = self._non_named_group_matcher.sub("{var}", result) + + # handle optional params + result = self._either_option_matcher.sub(lambda m: m.group(1), result) + + # clean up any outstanding regex-y characters. + result = ( + result.replace("^", "") + .replace("$", "") + .replace("?", "") + .replace("\\A", "") + .replace("\\Z", "") + .replace("//", "/") + .replace("\\", "") + ) + + return result + + def _resolve( + self, + resolver: "URLResolver", + path: str, + parents: "Optional[List[URLResolver]]" = None, + ) -> "Optional[str]": + match = get_regex(resolver).search(path) # Django < 2.0 + + if not match: + return None + + if parents is None: + parents = [resolver] + elif resolver not in parents: + parents = parents + [resolver] + + new_path = path[match.end() :] + for pattern in resolver.url_patterns: + # this is an include() + if not pattern.callback: + match_ = self._resolve(pattern, new_path, parents) + if match_: + return match_ + continue + elif not get_regex(pattern).search(new_path): + continue + + try: + return self._cache[pattern] + except KeyError: + pass + + prefix = "".join(self._simplify(p) for p in parents) + result = prefix + self._simplify(pattern) + if not result.startswith("/"): + result = "/" + result + self._cache[pattern] = result + return result + + return None + + def resolve( + self, + path: str, + urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, + ) -> "Optional[str]": + resolver = get_resolver(urlconf) + match = self._resolve(resolver, path) + return match + + +LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py new file mode 100644 index 0000000000..cf3012a75e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py @@ -0,0 +1,127 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +try: + from asyncio import iscoroutinefunction +except ImportError: + iscoroutinefunction = None # type: ignore + + +try: + from sentry_sdk.integrations.django.asgi import wrap_async_view +except (ImportError, SyntaxError): + wrap_async_view = None # type: ignore + + +def patch_views() -> None: + from django.core.handlers.base import BaseHandler + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + old_make_view_atomic = BaseHandler.make_view_atomic + old_render = SimpleTemplateResponse.render + + def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="serialize response", + attributes={ + "sentry.op": OP.VIEW_RESPONSE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_render(self) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RESPONSE_RENDER, + name="serialize response", + origin=DjangoIntegration.origin, + ): + return old_render(self) + + @functools.wraps(old_make_view_atomic) + def sentry_patched_make_view_atomic( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + callback = old_make_view_atomic(self, *args, **kwargs) + + # XXX: The wrapper function is created for every request. Find more + # efficient way to wrap views (or build a cache?) + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is not None: + is_async_view = ( + iscoroutinefunction is not None + and wrap_async_view is not None + and iscoroutinefunction(callback) + ) + if is_async_view: + sentry_wrapped_callback = wrap_async_view(callback) + else: + sentry_wrapped_callback = _wrap_sync_view(callback) + + else: + sentry_wrapped_callback = callback + + return sentry_wrapped_callback + + SimpleTemplateResponse.render = sentry_patched_render + BaseHandler.make_view_atomic = sentry_patched_make_view_atomic + + +def _wrap_sync_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + # set the active thread id to the handler thread for sync views + # this isn't necessary for async views since that runs on main + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return callback(request, *args, **kwargs) + + return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py new file mode 100644 index 0000000000..310766ee3a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py @@ -0,0 +1,246 @@ +import json +from typing import TypeVar + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, +) + +R = TypeVar("R") + +try: + from dramatiq.broker import Broker + from dramatiq.errors import Retry + from dramatiq.message import Message + from dramatiq.middleware import Middleware, default_middleware +except ImportError: + raise DidNotEnable("Dramatiq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Union + + from sentry_sdk._types import Event, Hint + + +class DramatiqIntegration(Integration): + """ + Dramatiq integration for Sentry + + Please make sure that you call `sentry_sdk.init` *before* initializing + your broker, as it monkey patches `Broker.__init__`. + + This integration was originally developed and maintained + by https://github.com/jacobsvante and later donated to the Sentry + project. + """ + + identifier = "dramatiq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_dramatiq_broker() + + +def _patch_dramatiq_broker() -> None: + original_broker__init__ = Broker.__init__ + + def sentry_patched_broker__init__( + self: "Broker", *args: "Any", **kw: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + + try: + middleware = kw.pop("middleware") + except KeyError: + # Unfortunately Broker and StubBroker allows middleware to be + # passed in as positional arguments, whilst RabbitmqBroker and + # RedisBroker does not. + if len(args) == 1: + middleware = args[0] + args = () + else: + middleware = None + + if middleware is None: + middleware = list(m() for m in default_middleware) + else: + middleware = list(middleware) + + if integration is not None: + middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] + middleware.insert(0, SentryMiddleware()) + + kw["middleware"] = middleware + original_broker__init__(self, *args, **kw) + + Broker.__init__ = sentry_patched_broker__init__ + + +class SentryMiddleware(Middleware): # type: ignore[misc] + """ + A Dramatiq middleware that automatically captures and sends + exceptions to Sentry. + + This is automatically added to every instantiated broker via the + DramatiqIntegration. + """ + + SENTRY_HEADERS_NAME = "_sentry_headers" + + def before_enqueue( + self, broker: "Broker", message: "Message[R]", delay: int + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + message.options[self.SENTRY_HEADERS_NAME] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + + def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DramatiqIntegration) + if integration is None: + return + + message._scope_manager = sentry_sdk.isolation_scope() + scope = message._scope_manager.__enter__() + scope.clear_breadcrumbs() + scope.set_extra("dramatiq_message_id", message.message_id) + scope.add_event_processor(_make_message_event_processor(message, integration)) + + sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} + if "retries" in message.options: + # start new trace in case of retrying + sentry_headers = {} + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(sentry_headers) + span = sentry_sdk.traces.start_span( + name=message.actor_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_DRAMATIQ, + "sentry.origin": DramatiqIntegration.origin, + "sentry.span.source": SegmentSource.TASK.value, + }, + parent_span=None, + ) + message._sentry_span_ctx = span + else: + transaction = continue_trace( + sentry_headers, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + origin=DramatiqIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + sentry_sdk.start_transaction( + transaction, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + ) + transaction.__enter__() + message._sentry_span_ctx = transaction + + def after_process_message( + self, + broker: "Broker", + message: "Message[R]", + *, + result: "Optional[Any]" = None, + exception: "Optional[Exception]" = None, + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + actor = broker.get_actor(message.actor_name) + throws = message.options.get("throws") or actor.options.get("throws") + + scope_manager = message._scope_manager + span_ctx = getattr(message, "_sentry_span_ctx", None) + if span_ctx is None: + return None + + is_event_capture_required = ( + exception is not None + and not (throws and isinstance(exception, throws)) + and not isinstance(exception, Retry) + ) + if not is_event_capture_required: + # normal transaction finish + span_ctx.__exit__(None, None, None) + scope_manager.__exit__(None, None, None) + return + + event, hint = event_from_exception( + exception, # type: ignore[arg-type] + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": DramatiqIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + # transaction error + span_ctx.__exit__(type(exception), exception, None) + scope_manager.__exit__(type(exception), exception, None) + + after_skip_message = after_process_message + + +def _make_message_event_processor( + message: "Message[R]", integration: "DramatiqIntegration" +) -> "Callable[[Event, Hint], Optional[Event]]": + def inner(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + DramatiqMessageExtractor(message).extract_into_event(event) + + return event + + return inner + + +class DramatiqMessageExtractor: + def __init__(self, message: "Message[R]") -> None: + self.message_data = dict(message.asdict()) + + def content_length(self) -> int: + return len(json.dumps(self.message_data)) + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + contexts = event.setdefault("contexts", {}) + request_info = contexts.setdefault("dramatiq", {}) + request_info["type"] = "dramatiq" + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + if not request_body_within_bounds(client, self.content_length()): + data = AnnotatedValue.removed_because_over_size_limit() + else: + data = self.message_data + + request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py new file mode 100644 index 0000000000..6bbc61000d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py @@ -0,0 +1,76 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + + +class ExcepthookIntegration(Integration): + identifier = "excepthook" + + always_run = False + + def __init__(self, always_run: bool = False) -> None: + if not isinstance(always_run, bool): + raise ValueError( + "Invalid value for always_run: %s (must be type boolean)" + % (always_run,) + ) + self.always_run = always_run + + @staticmethod + def setup_once() -> None: + sys.excepthook = _make_excepthook(sys.excepthook) + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + if _should_send(integration.always_run): + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "excepthook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook + + +def _should_send(always_run: bool = False) -> bool: + if always_run: + return True + + if hasattr(sys, "ps1"): + # Disable the excepthook for interactive Python shells, otherwise + # every typo gets sent to Sentry. + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py new file mode 100644 index 0000000000..4473fcc435 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + + +class ExecutingIntegration(Integration): + identifier = "executing" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + frame = tb.tb_frame + source = Source.for_frame(frame) + sentry_frame["function"] = source.code_qualname(frame.f_code) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py new file mode 100644 index 0000000000..7a595bcf2a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + + from sentry_sdk._types import Event, EventProcessor + +# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` +# and `falcon.API` to `falcon.App` + +try: + import falcon # type: ignore + from falcon import __version__ as FALCON_VERSION +except ImportError: + raise DidNotEnable("Falcon not installed") + +try: + import falcon.app_helpers # type: ignore + + falcon_helpers = falcon.app_helpers + falcon_app_class = falcon.App + FALCON3 = True +except ImportError: + import falcon.api_helpers # type: ignore + + falcon_helpers = falcon.api_helpers + falcon_app_class = falcon.API + FALCON3 = False + + +_FALCON_UNSET: "Optional[object]" = None +if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ + with capture_internal_exceptions(): + from falcon.request import ( # type: ignore[import-not-found, no-redef] + _UNSET as _FALCON_UNSET, + ) + + +class FalconRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, Any]": + return self.request.env + + def cookies(self) -> "Dict[str, Any]": + return self.request.cookies + + def form(self) -> None: + return None # No such concept in Falcon + + def files(self) -> None: + return None # No such concept in Falcon + + def raw_data(self) -> "Optional[str]": + # As request data can only be read once we won't make this available + # to Sentry. Just send back a dummy string in case there was a + # content length. + # TODO(jmagnusson): Figure out if there's a way to support this + content_length = self.content_length() + if content_length > 0: + return "[REQUEST_CONTAINING_RAW_DATA]" + else: + return None + + def json(self) -> "Optional[Dict[str, Any]]": + # fallback to cached_media = None if self.request._media is not available + cached_media = None + with capture_internal_exceptions(): + # self.request._media is the cached self.request.media + # value. It is only available if self.request.media + # has already been accessed. Therefore, reading + # self.request._media will not exhaust the raw request + # stream (self.request.bounded_stream) because it has + # already been read if self.request._media is set. + cached_media = self.request._media + + if cached_media is not _FALCON_UNSET: + return cached_media + + return None + + +class SentryFalconMiddleware: + """Captures exceptions in Falcon requests and send to Sentry""" + + def process_request( + self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return + + scope = sentry_sdk.get_isolation_scope() + scope._name = "falcon" + scope.add_event_processor(_make_request_event_processor(req, integration)) + + def process_resource( + self, req: "Any", resp: "Any", resource: "Any", params: "Any" + ) -> None: + """ + Sets the segment name and source as the route is resolved when this runs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FalconIntegration) + if integration is None or not has_span_streaming_enabled(client.options): + return + + name_for_style = { + "uri_template": req.uri_template, + "path": req.path, + } + name = name_for_style[integration.transaction_style] + source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] + sentry_sdk.set_transaction_name(name, source) + + +TRANSACTION_STYLE_VALUES = ("uri_template", "path") + + +class FalconIntegration(Integration): + identifier = "falcon" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "uri_template") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + version = parse_version(FALCON_VERSION) + _check_minimum_version(FalconIntegration, version) + + _patch_wsgi_app() + _patch_handle_exception() + _patch_prepare_middleware() + + +def _patch_wsgi_app() -> None: + original_wsgi_app = falcon_app_class.__call__ + + def sentry_patched_wsgi_app( + self: "falcon.API", env: "Any", start_response: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return original_wsgi_app(self, env, start_response) + + sentry_wrapped = SentryWsgiMiddleware( + lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), + span_origin=FalconIntegration.origin, + ) + + return sentry_wrapped(env, start_response) + + falcon_app_class.__call__ = sentry_patched_wsgi_app + + +def _patch_handle_exception() -> None: + original_handle_exception = falcon_app_class._handle_exception + + @ensure_integration_enabled(FalconIntegration, original_handle_exception) + def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": + # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception + # method signature from `(ex, req, resp, params)` to + # `(req, resp, ex, params)` + ex = response = None + with capture_internal_exceptions(): + ex = next(argument for argument in args if isinstance(argument, Exception)) + response = next( + argument for argument in args if isinstance(argument, falcon.Response) + ) + + was_handled = original_handle_exception(self, *args) + + if ex is None or response is None: + # Both ex and response should have a non-None value at this point; otherwise, + # there is an error with the SDK that will have been captured in the + # capture_internal_exceptions block above. + return was_handled + + if _exception_leads_to_http_5xx(ex, response): + event, hint = event_from_exception( + ex, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "falcon", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return was_handled + + falcon_app_class._handle_exception = sentry_patched_handle_exception + + +def _patch_prepare_middleware() -> None: + original_prepare_middleware = falcon_helpers.prepare_middleware + + def sentry_patched_prepare_middleware( + middleware: "Any" = None, + independent_middleware: "Any" = False, + asgi: bool = False, + ) -> "Any": + if asgi: + # We don't support ASGI Falcon apps, so we don't patch anything here + return original_prepare_middleware(middleware, independent_middleware, asgi) + + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is not None: + middleware = [SentryFalconMiddleware()] + (middleware or []) + + # We intentionally omit the asgi argument here, since the default is False anyways, + # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. + return original_prepare_middleware(middleware, independent_middleware) + + falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware + + +def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: + is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( + "5" + ) + is_unhandled_error = not isinstance( + ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) + ) + + # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response + # at the stage where we capture it is listed as 200, even though we would expect to see a 500 + # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to + # only perform this check on Falcon 3+, despite the risk that some handled errors might be + # reported to Sentry as unhandled on Falcon 2. + return (is_server_error or is_unhandled_error) and ( + not FALCON3 or _has_http_5xx_status(response) + ) + + +def _has_http_5xx_status(response: "falcon.Response") -> bool: + return response.status.startswith("5") + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "falcon.Request" +) -> None: + name_for_style = { + "uri_template": request.uri_template, + "path": request.path, + } + event["transaction"] = name_for_style[transaction_style] + event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} + + +def _make_request_event_processor( + req: "falcon.Request", integration: "FalconIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, req) + + with capture_internal_exceptions(): + FalconRequestExtractor(req).extract_into_event(event) + + return event + + return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py new file mode 100644 index 0000000000..c7b97c88b1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py @@ -0,0 +1,201 @@ +import sys +from copy import deepcopy +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import transaction_from_function + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Dict + + from sentry_sdk._types import Event + +try: + from sentry_sdk.integrations.starlette import ( + StarletteIntegration, + StarletteRequestExtractor, + _get_cached_request_body_attribute, + ) +except DidNotEnable: + raise DidNotEnable("Starlette is not installed") + +try: + import fastapi # type: ignore +except ImportError: + raise DidNotEnable("FastAPI is not installed") + + +_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +class FastApiIntegration(StarletteIntegration): + identifier = "fastapi" + + @staticmethod + def setup_once() -> None: + patch_get_request_handler() + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or "" + + elif transaction_style == "url": + route = request.scope.get("route") + + if route: + # FastAPI >= 0.137 stores the prefix-resolved path on an + # effective_route_context in scope["fastapi"], while + # scope["route"].path holds the unprefixed original. + # Prefer the effective context path when available. + effective_route_context = request.scope.get("fastapi", {}).get( + "effective_route_context" + ) + context_path = getattr(effective_route_context, "path", None) + + if context_path: + name = context_path + else: + path = getattr(route, "path", None) + if path is not None: + name = path + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name(name, source=source) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FastApiIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, Dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if info: + if "cookies" in info and should_send_default_pii(): + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = FastApiIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_get_request_handler() -> None: + old_get_request_handler = fastapi.routing.get_request_handler + + def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": + dependant = kwargs.get("dependant") + if ( + dependant + and dependant.call is not None + and not iscoroutinefunction(dependant.call) + # FastAPI >= 0.137 calls get_request_handler() on every request + # (router-tree traversal) rather than once at registration. Guard + # against accumulating _sentry_call wrappers on the shared + # dependant object, which would cause a RecursionError after ~987 + # requests as the call chain grows past Python's recursion limit. + and not getattr(dependant.call, "_sentry_is_patched", False) + ): + old_call = dependant.call + + @wraps(old_call) + def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": + current_scope = sentry_sdk.get_current_scope() + + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_call(*args, **kwargs) + + _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] + dependant.call = _sentry_call + + old_app = old_get_request_handler(*args, **kwargs) + + async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_app, *args, **kwargs) + + return _sentry_app + + fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py new file mode 100644 index 0000000000..1902091fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py @@ -0,0 +1,281 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Union + + from werkzeug.datastructures import FileStorage, ImmutableMultiDict + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + + +try: + import flask_login # type: ignore +except ImportError: + flask_login = None + +try: + from flask import Flask, Request # type: ignore + from flask import request as flask_request + from flask.signals import ( + before_render_template, + got_request_exception, + request_started, + ) + from markupsafe import Markup +except ImportError: + raise DidNotEnable("Flask is not installed") + +try: + import blinker # noqa +except ImportError: + raise DidNotEnable("blinker is not installed") + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class FlaskIntegration(Integration): + identifier = "flask" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + try: + from quart import Quart # type: ignore + + if Flask == Quart: + # This is Quart masquerading as Flask, don't enable the Flask + # integration. See https://github.com/getsentry/sentry-python/issues/2709 + raise DidNotEnable( + "This is not a Flask app but rather Quart pretending to be Flask" + ) + except ImportError: + pass + + version = package_version("flask") + _check_minimum_version(FlaskIntegration, version) + + before_render_template.connect(_add_sentry_trace) + request_started.connect(_request_started) + got_request_exception.connect(_capture_exception) + + old_app = Flask.__call__ + + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return old_app(self, environ, start_response) + + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=FlaskIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + Flask.__call__ = sentry_patched_wsgi_app + + +def _add_sentry_trace( + sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" +) -> None: + if "sentry_trace" in context: + return + + scope = sentry_sdk.get_current_scope() + trace_meta = Markup(scope.trace_propagation_meta()) + context["sentry_trace"] = trace_meta # for backwards compatibility + context["sentry_trace_meta"] = trace_meta + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + scope.set_transaction_name( + name_for_style[transaction_style], + source=SOURCE_FOR_STYLE[transaction_style], + ) + except Exception: + pass + + +def _request_started(app: "Flask", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return + + request = flask_request._get_current_object() + + # Set the transaction name and source here, + # but rely on WSGI middleware to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + scope.set_user(user_properties) + + evt_processor = _make_request_event_processor(app, request, integration) + scope.add_event_processor(evt_processor) + + +class FlaskRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[Any, Any]": + return { + k: v[0] if isinstance(v, list) and len(v) == 1 else v + for k, v in self.request.cookies.items() + } + + def raw_data(self) -> bytes: + return self.request.get_data() + + def form(self) -> "ImmutableMultiDict[str, Any]": + return self.request.form + + def files(self) -> "ImmutableMultiDict[str, Any]": + return self.request.files + + def is_json(self) -> bool: + return self.request.is_json + + def json(self) -> "Any": + return self.request.get_json(silent=True) + + def size_of_file(self, file: "FileStorage") -> int: + return file.content_length + + +def _make_request_event_processor( + app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + FlaskRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _add_user_to_event(event) + + return event + + return inner + + +@ensure_integration_enabled(FlaskIntegration) +def _capture_exception( + sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "flask", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_flask_user_properties() -> "Dict[str, str]": + if flask_login is None: + return {} + + user = flask_login.current_user + if user is None: + return {} + + properties = {} + + try: + user_id = user.get_id() + if user_id is not None: + properties["id"] = user_id + except AttributeError: + # might happen if: + # - flask_login could not be imported + # - flask_login is not configured + # - no user is logged in + pass + + # The following attribute accesses are ineffective for the general + # Flask-Login case, because the User interface of Flask-Login does not + # care about anything but the ID. However, Flask-User (based on + # Flask-Login) documents a few optional extra attributes. + # + # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names + try: + if user.email is not None: + properties["email"] = user.email + except Exception: + pass + + try: + if user.username is not None: + properties["username"] = user.username + except Exception: + pass + + return properties + + +def _add_user_to_event(event: "Event") -> None: + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + user_info = event.setdefault("user", {}) + for key, value in user_properties.items(): + user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py new file mode 100644 index 0000000000..91a62b3a81 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py @@ -0,0 +1,286 @@ +import functools +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +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 +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +# Constants +TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_func(func: "F") -> "F": + @functools.wraps(func) + def sentry_func( + functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(GcpIntegration) + if integration is None: + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = environ.get("FUNCTION_TIMEOUT_SEC") + if not configured_time: + logger.debug( + "The configured timeout could not be fetched from Cloud Functions configuration." + ) + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = int(configured_time) + + initial_time = datetime.now(timezone.utc) + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + gcp_event, configured_time, initial_time + ) + ) + scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) + timeout_thread = None + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = configured_time - TIMEOUT_WARNING_BUFFER + + timeout_thread = TimeoutThread( + waiting_time, + configured_time, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = {} + header_attributes: "dict[str, Any]" = {} + if hasattr(gcp_event, "headers"): + headers = gcp_event.headers + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + additional_attributes = {} + 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" + ) + + sampling_context = { + "gcp_env": { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + }, + "gcp_event": gcp_event, + } + + function_name = environ.get("FUNCTION_NAME", "") + + if environ.get("GCP_PROJECT"): + additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") + + if environ.get("FUNCTION_IDENTITY"): + additional_attributes["faas.identity"] = environ.get( + "FUNCTION_IDENTITY" + ) + + if environ.get("ENTRY_POINT"): + additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_GCP, + "sentry.origin": GcpIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.provider": CLOUD_PROVIDER.GCP, + "faas.name": function_name, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_GCP, + name=environ.get("FUNCTION_NAME", ""), + source=TransactionSource.COMPONENT, + origin=GcpIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return func(functionhandler, gcp_event, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "gcp", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + # Flush out the event queue + client.flush() + + return sentry_func # type: ignore + + +class GcpIntegration(Integration): + identifier = "gcp" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + import __main__ as gcp_functions + + if not hasattr(gcp_functions, "worker_v1"): + logger.warning( + "GcpIntegration currently supports only Python 3.7 runtime environment." + ) + return + + worker1 = gcp_functions.worker_v1 + + worker1.FunctionHandler.invoke_user_function = _wrap_func( + worker1.FunctionHandler.invoke_user_function + ) + + +def _make_request_event_processor( + gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + final_time = datetime.now(timezone.utc) + time_diff = final_time - initial_time + + execution_duration_in_millis = time_diff / timedelta(milliseconds=1) + + extra = event.setdefault("extra", {}) + extra["google cloud functions"] = { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + "execution_duration_in_millis": execution_duration_in_millis, + "configured_timeout_in_seconds": configured_timeout, + } + + extra["google cloud logs"] = { + "url": _get_google_cloud_logs_url(final_time), + } + + request = event.get("request", {}) + + request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) + + if hasattr(gcp_event, "method"): + request["method"] = gcp_event.method + + if hasattr(gcp_event, "query_string"): + request["query_string"] = gcp_event.query_string.decode( + "utf-8", errors="replace" + ) + + if hasattr(gcp_event, "headers"): + request["headers"] = _filter_headers(gcp_event.headers) + + if should_send_default_pii(): + if hasattr(gcp_event, "data"): + request["data"] = gcp_event.data + else: + if hasattr(gcp_event, "data"): + # Unfortunately couldn't find a way to get structured body from GCP + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + event["request"] = deepcopy(request) + + return event + + return event_processor + + +def _get_google_cloud_logs_url(final_time: "datetime") -> str: + """ + Generates a Google Cloud Logs console URL based on the environment variables + Arguments: + final_time {datetime} -- Final time + Returns: + str -- Google Cloud Logs Console URL to logs. + """ + hour_ago = final_time - timedelta(hours=1) + formatstring = "%Y-%m-%dT%H:%M:%SZ" + + url = ( + "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" + "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" + "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" + "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" + "&interval=PT1H&scrollTimestamp={timestamp_end}" + ).format( + project=environ.get("GCP_PROJECT"), + function_name=environ.get("FUNCTION_NAME"), + region=environ.get("FUNCTION_REGION"), + timestamp_end=final_time.strftime(formatstring), + timestamp_start=hour_ago.strftime(formatstring), + ) + + return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py new file mode 100644 index 0000000000..4be0f479bc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py @@ -0,0 +1,96 @@ +import re +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + +# function is everything between index at @ +# and then we match on the @ plus the hex val +FUNCTION_RE = r"[^@]+?" +HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" + +_FRAME_RE_PATTERN = r""" +^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ +""".format( + FUNCTION_RE=FUNCTION_RE, + HEX_ADDRESS=HEX_ADDRESS, +) + +FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) + + +class GnuBacktraceIntegration(Integration): + identifier = "gnu_backtrace" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + return _process_gnu_backtrace(event, hint) + + +def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception in values: + frames = exception.get("stacktrace", {}).get("frames", []) + if not frames: + continue + + msg = exception.get("value", None) + if not msg: + continue + + additional_frames = [] + new_msg = [] + + for line in msg.splitlines(): + match = FRAME_RE.match(line) + if match: + additional_frames.append( + ( + int(match.group("index")), + { + "package": match.group("package") or None, + "function": match.group("function") or None, + "platform": "native", + }, + ) + ) + else: + # Put garbage lines back into message, not sure what to do with them. + new_msg.append(line) + + if additional_frames: + additional_frames.sort(key=lambda x: -x[0]) + for _, frame in additional_frames: + frames.append(frame) + + new_msg.append("") + exception["value"] = "\n".join(new_msg) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py new file mode 100644 index 0000000000..45652c3f71 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py @@ -0,0 +1,457 @@ +from functools import wraps +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + List, +) + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import SPANSTATUS +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.genai.models import AsyncModels, Models +except ImportError: + raise DidNotEnable("google-genai not installed") + + +from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN +from .streaming import ( + accumulate_streaming_response, + set_span_data_for_streaming_response, +) +from .utils import ( + _capture_exception, + prepare_embed_content_args, + prepare_generate_content_args, + set_span_data_for_embed_request, + set_span_data_for_embed_response, + set_span_data_for_request, + set_span_data_for_response, +) + + +class GoogleGenAIIntegration(Integration): + identifier = IDENTIFIER + origin = ORIGIN + + def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Patch sync methods + Models.generate_content = _wrap_generate_content(Models.generate_content) + Models.generate_content_stream = _wrap_generate_content_stream( + Models.generate_content_stream + ) + Models.embed_content = _wrap_embed_content(Models.embed_content) + + # Patch async methods + AsyncModels.generate_content = _wrap_async_generate_content( + AsyncModels.generate_content + ) + AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( + AsyncModels.generate_content_stream + ) + AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) + + +def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = f(self, *args, **kwargs) + + # Create wrapper iterator to accumulate responses + def new_iterator() -> "Iterator[Any]": + chunks: "List[Any]" = [] + try: + for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_generate_content_stream + + +def _wrap_async_generate_content_stream( + f: "Callable[..., Any]", +) -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = await f(self, *args, **kwargs) + + # Create wrapper async iterator to accumulate responses + async def new_async_iterator() -> "AsyncIterator[Any]": + chunks: "List[Any]" = [] + try: + async for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_async_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_async_generate_content_stream + + +def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_generate_content + + +def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_async_generate_content + + +def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_embed_content + + +def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_embed_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py new file mode 100644 index 0000000000..5b53ebf0e2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py @@ -0,0 +1,16 @@ +GEN_AI_SYSTEM = "gcp.gemini" + +# Mapping of tool attributes to their descriptions +# These are all tools that are available in the Google GenAI API +TOOL_ATTRIBUTES_MAP = { + "google_search_retrieval": "Google Search retrieval tool", + "google_search": "Google Search tool", + "retrieval": "Retrieval tool", + "enterprise_web_search": "Enterprise web search tool", + "google_maps": "Google Maps tool", + "code_execution": "Code execution tool", + "computer_use": "Computer use tool", +} + +IDENTIFIER = "google_genai" +ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py new file mode 100644 index 0000000000..8414ea4f21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py @@ -0,0 +1,172 @@ +from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union + +from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import ( + safe_serialize, +) + +from .utils import ( + UsageData, + extract_contents_text, + extract_finish_reasons, + extract_tool_calls, + extract_usage_data, +) + +if TYPE_CHECKING: + from google.genai.types import GenerateContentResponse + + from sentry_sdk.tracing import Span + + +class AccumulatedResponse(TypedDict): + id: "Optional[str]" + model: "Optional[str]" + text: str + finish_reasons: "List[str]" + tool_calls: "List[dict[str, Any]]" + usage_metadata: "Optional[UsageData]" + + +def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": + return UsageData( + input_tokens=max(self["input_tokens"], other["input_tokens"]), + output_tokens=max(self["output_tokens"], other["output_tokens"]), + input_tokens_cached=max( + self["input_tokens_cached"], other["input_tokens_cached"] + ), + output_tokens_reasoning=max( + self["output_tokens_reasoning"], other["output_tokens_reasoning"] + ), + total_tokens=max(self["total_tokens"], other["total_tokens"]), + ) + + +def accumulate_streaming_response( + chunks: "List[GenerateContentResponse]", +) -> "AccumulatedResponse": + """Accumulate streaming chunks into a single response-like object.""" + accumulated_text = [] + finish_reasons = [] + tool_calls = [] + usage_data = None + response_id = None + model = None + + for chunk in chunks: + # Extract text and tool calls + if getattr(chunk, "candidates", None): + for candidate in getattr(chunk, "candidates", []): + if hasattr(candidate, "content") and getattr( + candidate.content, "parts", [] + ): + extracted_text = extract_contents_text(candidate.content) + if extracted_text: + accumulated_text.append(extracted_text) + + extracted_finish_reasons = extract_finish_reasons(chunk) + if extracted_finish_reasons: + finish_reasons.extend(extracted_finish_reasons) + + extracted_tool_calls = extract_tool_calls(chunk) + if extracted_tool_calls: + tool_calls.extend(extracted_tool_calls) + + # Use last possible chunk, in case of interruption, and + # gracefully handle missing intermediate tokens by taking maximum + # with previous token reporting. + chunk_usage_data = extract_usage_data(chunk) + usage_data = ( + chunk_usage_data + if usage_data is None + else element_wise_usage_max(usage_data, chunk_usage_data) + ) + + accumulated_response = AccumulatedResponse( + text="".join(accumulated_text), + finish_reasons=finish_reasons, + tool_calls=tool_calls, + usage_metadata=usage_data, + id=response_id, + model=model, + ) + + return accumulated_response + + +def set_span_data_for_streaming_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + accumulated_response: "AccumulatedResponse", +) -> None: + """Set span data for accumulated streaming response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if ( + should_send_default_pii() + and integration.include_prompts + and accumulated_response.get("text") + ): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TEXT, + safe_serialize([accumulated_response["text"]]), + ) + + if accumulated_response.get("finish_reasons"): + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + accumulated_response["finish_reasons"], + ) + + if accumulated_response.get("tool_calls"): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(accumulated_response["tool_calls"]), + ) + + response_id = accumulated_response.get("id") + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + + response_model = accumulated_response.get("model") + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if accumulated_response["usage_metadata"] is None: + return + + if accumulated_response["usage_metadata"]["input_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + accumulated_response["usage_metadata"]["input_tokens"], + ) + + if accumulated_response["usage_metadata"]["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + accumulated_response["usage_metadata"]["input_tokens_cached"], + ) + + if accumulated_response["usage_metadata"]["output_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + accumulated_response["usage_metadata"]["output_tokens"], + ) + + if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + accumulated_response["usage_metadata"]["output_tokens_reasoning"], + ) + + if accumulated_response["usage_metadata"]["total_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + accumulated_response["usage_metadata"]["total_tokens"], + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py new file mode 100644 index 0000000000..464a812680 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py @@ -0,0 +1,1118 @@ +import copy +import inspect +import json +from functools import wraps +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + TypedDict, + Union, +) + +from google.genai.types import Content, GenerateContentConfig, Part, PartDict + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.utils import ( + get_modality_from_mime_type, + normalize_message_roles, + set_data_normalized, + transform_google_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + safe_serialize, +) + +from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP + +if TYPE_CHECKING: + from google.genai.types import ( + ContentListUnion, + ContentUnion, + ContentUnionDict, + EmbedContentResponse, + GenerateContentResponse, + Model, + Tool, + ) + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +_is_PIL_available = False +try: + from PIL import Image as PILImage # type: ignore[import-not-found] + + _is_PIL_available = True +except ImportError: + pass + +# Keys to use when checking to see if a dict provided by the user +# is Part-like (as opposed to a Content or multi-turn conversation entry). +_PART_DICT_KEYS = PartDict.__optional_keys__ + + +class UsageData(TypedDict): + """Structure for token usage data.""" + + input_tokens: int + input_tokens_cached: int + output_tokens: int + output_tokens_reasoning: int + total_tokens: int + + +def extract_usage_data( + response: "Union[GenerateContentResponse, dict[str, Any]]", +) -> "UsageData": + """Extract usage data from response into a structured format. + + Args: + response: The GenerateContentResponse object or dictionary containing usage metadata + + Returns: + UsageData: Dictionary with input_tokens, input_tokens_cached, + output_tokens, and output_tokens_reasoning fields + """ + usage_data = UsageData( + input_tokens=0, + input_tokens_cached=0, + output_tokens=0, + output_tokens_reasoning=0, + total_tokens=0, + ) + + # Handle dictionary response (from streaming) + if isinstance(response, dict): + usage = response.get("usage_metadata", {}) + if not usage: + return usage_data + + prompt_tokens = usage.get("prompt_token_count", 0) or 0 + tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + cached_tokens = usage.get("cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + candidates_tokens = usage.get("candidates_token_count", 0) or 0 + # python-genai reports output and reasoning tokens separately + # reasoning should be sub-category of output tokens + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = usage.get("total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + if not hasattr(response, "usage_metadata"): + return usage_data + + usage = response.usage_metadata + + # Input tokens include both prompt and tool use prompt tokens + prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 + tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + # Cached input tokens + cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + # Reasoning tokens + reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + # output_tokens = candidates_tokens + reasoning_tokens + # google-genai reports output and reasoning tokens separately + candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = getattr(usage, "total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + +def _capture_exception(exc: "Any") -> None: + """Capture exception with Google GenAI mechanism.""" + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "google_genai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def get_model_name(model: "Union[str, Model]") -> str: + """Extract model name from model parameter.""" + if isinstance(model, str): + return model + # Handle case where model might be an object with a name attribute + if hasattr(model, "name"): + return str(model.name) + return str(model) + + +def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": + """Extract messages from contents parameter which can have various formats. + + Returns a list of message dictionaries in the format: + - System: {"role": "system", "content": "string"} + - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} + """ + if contents is None: + return [] + + messages = [] + + # Handle string case + if isinstance(contents, str): + return [{"role": "user", "content": contents}] + + # Handle list case + if isinstance(contents, list): + if contents and all(_is_part_like(item) for item in contents): + # All items are parts — merge into a single multi-part user message + content_parts = [] + for item in contents: + part = _extract_part_from_item(item) + if part is not None: + content_parts.append(part) + + return [{"role": "user", "content": content_parts}] + else: + # Multi-turn conversation or mixed content types + for item in contents: + item_messages = extract_contents_messages(item) + messages.extend(item_messages) + return messages + + # Handle dictionary case (ContentDict) + if isinstance(contents, dict): + role = contents.get("role", "user") + parts = contents.get("parts") + + if parts: + content_parts = [] + tool_messages = [] + + for part in parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + # Tool message - add separately + tool_messages.append(part_result) + else: + # Regular content part + content_parts.append(part_result) + + # Add main message if we have content parts + if content_parts: + # Normalize role: "model" -> "assistant" + normalized_role = "assistant" if role == "model" else role or "user" + messages.append({"role": normalized_role, "content": content_parts}) + + # Add tool messages + messages.extend(tool_messages) + elif "text" in contents: + messages.append( + { + "role": role, + "content": [{"text": contents["text"], "type": "text"}], + } + ) + elif "inline_data" in contents: + # The "data" will always be bytes (or bytes within a string), + # so if this is present, it's safe to automatically substitute with the placeholder + messages.append( + { + "inline_data": { + "mime_type": contents["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + ) + + return messages + + # Handle Content object + if hasattr(contents, "parts") and contents.parts: + role = getattr(contents, "role", None) or "user" + content_parts = [] + tool_messages = [] + + for part in contents.parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + tool_messages.append(part_result) + else: + content_parts.append(part_result) + + if content_parts: + normalized_role = "assistant" if role == "model" else role + messages.append({"role": normalized_role, "content": content_parts}) + + messages.extend(tool_messages) + return messages + + # Handle Part object directly + part_result = _extract_part_content(contents) + if part_result: + if isinstance(part_result, dict) and part_result.get("role") == "tool": + return [part_result] + else: + return [{"role": "user", "content": [part_result]}] + + # Handle PIL.Image.Image + if _is_PIL_available and isinstance(contents, PILImage.Image): + blob_part = _extract_pil_image(contents) + if blob_part: + return [{"role": "user", "content": [blob_part]}] + + # Handle File object + if hasattr(contents, "uri") and hasattr(contents, "mime_type"): + # File object + file_uri = getattr(contents, "uri", None) + mime_type = getattr(contents, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing + if file_uri is not None: + # Default to empty string if mime_type is None + if mime_type is None: + mime_type = "" + + blob_part = { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + return [{"role": "user", "content": [blob_part]}] + + # Handle direct text attribute + if hasattr(contents, "text") and contents.text: + return [ + {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} + ] + + return [] + + +def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": + """Extract content from a Part object or dict. + + Returns: + - dict for content part (text/blob) or tool message + - None if part should be skipped + """ + if part is None: + return None + + # Handle dict Part + if isinstance(part, dict): + # Check for function_response first (tool message) + if "function_response" in part: + return _extract_tool_message_from_part(part) + + if part.get("text"): + return {"text": part["text"], "type": "text"} + + # Try using Google-specific transform for dict formats (inline_data, file_data) + result = transform_google_content_part(part) + if result is not None: + # For inline_data with bytes data, substitute the content + if "inline_data" in part: + # inline_data.data will always be bytes, or a string containing base64-encoded bytes, + # so can automatically substitute without further checks + result["content"] = BLOB_DATA_SUBSTITUTE + return result + + return None + + # Handle Part object + # Check for function_response (tool message) + if hasattr(part, "function_response") and part.function_response: + return _extract_tool_message_from_part(part) + + # Handle text + if hasattr(part, "text") and part.text: + return {"text": part.text, "type": "text"} + + # Handle file_data + if hasattr(part, "file_data") and part.file_data: + file_data = part.file_data + file_uri = getattr(file_data, "file_uri", None) + mime_type = getattr(file_data, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) + if file_uri is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + # Handle inline_data + if hasattr(part, "inline_data") and part.inline_data: + inline_data = part.inline_data + data = getattr(inline_data, "data", None) + mime_type = getattr(inline_data, "mime_type", None) + # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) + if data is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + return None + + +def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": + """Extract tool message from a Part with function_response. + + Returns: + {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} + or None if not a valid tool message + """ + function_response = None + + if isinstance(part, dict): + function_response = part.get("function_response") + elif hasattr(part, "function_response"): + function_response = part.function_response + + if not function_response: + return None + + # Extract fields from function_response + tool_call_id = None + tool_name = None + output = None + + if isinstance(function_response, dict): + tool_call_id = function_response.get("id") + tool_name = function_response.get("name") + response_dict = function_response.get("response", {}) + # Prefer "output" key if present, otherwise use entire response + output = response_dict.get("output", response_dict) + else: + # FunctionResponse object + tool_call_id = getattr(function_response, "id", None) + tool_name = getattr(function_response, "name", None) + response_obj = getattr(function_response, "response", None) + if response_obj is None: + response_obj = {} + if isinstance(response_obj, dict): + output = response_obj.get("output", response_obj) + else: + output = response_obj + + if not tool_name: + return None + + return { + "role": "tool", + "content": { + "toolCallId": str(tool_call_id) if tool_call_id else None, + "toolName": str(tool_name), + "output": safe_serialize(output) if output is not None else None, + }, + } + + +def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": + """Extract blob part from PIL.Image.Image.""" + if not _is_PIL_available or not isinstance(image, PILImage.Image): + return None + + # Get format, default to JPEG + format_str = image.format or "JPEG" + suffix = format_str.lower() + mime_type = f"image/{suffix}" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _is_part_like(item: "Any") -> bool: + """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" + if isinstance(item, (str, Part)): + return True + if isinstance(item, (list, Content)): + return False + if isinstance(item, dict): + if "role" in item or "parts" in item: + return False + # Part objects that came in as plain dicts + return bool(_PART_DICT_KEYS & item.keys()) + # File objects + if hasattr(item, "uri"): + return True + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return True + return False + + +def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": + """Convert a single part-like item to a content part dict.""" + if isinstance(item, str): + return {"text": item, "type": "text"} + + # Handle bare inline_data dicts directly to preserve the raw format + if isinstance(item, dict) and "inline_data" in item: + return { + "inline_data": { + "mime_type": item["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + + # For other dicts and Part objects, use existing _extract_part_content + result = _extract_part_content(item) + if result is not None: + return result + + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return _extract_pil_image(item) + + # File objects + if hasattr(item, "uri") and hasattr(item, "mime_type"): + file_uri = getattr(item, "uri", None) + mime_type = getattr(item, "mime_type", None) or "" + if file_uri is not None: + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + return None + + +def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": + """Extract text from contents parameter which can have various formats. + + This is a compatibility function that extracts text from messages. + For new code, use extract_contents_messages instead. + """ + messages = extract_contents_messages(contents) + if not messages: + return None + + texts = [] + for message in messages: + content = message.get("content") + if isinstance(content, str): + texts.append(content) + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + texts.append(part.get("text", "")) + + return " ".join(texts) if texts else None + + +def _format_tools_for_span( + tools: "Iterable[Tool | Callable[..., Any]]", +) -> "Optional[List[dict[str, Any]]]": + """Format tools parameter for span data.""" + formatted_tools = [] + for tool in tools: + if callable(tool): + # Handle callable functions passed directly + formatted_tools.append( + { + "name": getattr(tool, "__name__", "unknown"), + "description": getattr(tool, "__doc__", None), + } + ) + elif ( + hasattr(tool, "function_declarations") + and tool.function_declarations is not None + ): + # Tool object with function declarations + for func_decl in tool.function_declarations: + formatted_tools.append( + { + "name": getattr(func_decl, "name", None), + "description": getattr(func_decl, "description", None), + } + ) + else: + # Check for predefined tool attributes - each of these tools + # is an attribute of the tool object, by default set to None + for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): + if getattr(tool, attr_name, None): + formatted_tools.append( + { + "name": attr_name, + "description": description, + } + ) + break + + return formatted_tools if formatted_tools else None + + +def extract_tool_calls( + response: "GenerateContentResponse", +) -> "Optional[List[dict[str, Any]]]": + """Extract tool/function calls from response candidates and automatic function calling history.""" + + tool_calls = [] + + # Extract from candidates, sometimes tool calls are nested under the content.parts object + if getattr(response, "candidates", []): + for candidate in response.candidates: + if not hasattr(candidate, "content") or not getattr( + candidate.content, "parts", [] + ): + continue + + for part in candidate.content.parts: + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if getattr(function_call, "args", None): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + # Extract from automatic_function_calling_history + # This is the history of tool calls made by the model + if getattr(response, "automatic_function_calling_history", None): + for content in response.automatic_function_calling_history: + if not getattr(content, "parts", None): + continue + + for part in getattr(content, "parts", []): + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if hasattr(function_call, "args"): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + return tool_calls if tool_calls else None + + +def _capture_tool_input( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" +) -> "dict[str, Any]": + """Capture tool input from args and kwargs.""" + tool_input = kwargs.copy() if kwargs else {} + + # If we have positional args, try to map them to the function signature + if args: + try: + sig = inspect.signature(tool) + param_names = list(sig.parameters.keys()) + for i, arg in enumerate(args): + if i < len(param_names): + tool_input[param_names[i]] = arg + except Exception: + # Fallback if we can't get the signature + tool_input["args"] = args + + return tool_input + + +def _create_tool_span( + tool_name: str, tool_doc: "Optional[str]" +) -> "Union[Span, StreamedSpan]": + """Create a span for tool execution.""" + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + if tool_doc: + span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=ORIGIN, + ) + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + if tool_doc: + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + +def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": + """Wrap a tool to emit execute_tool spans when called.""" + if not callable(tool): + # Not a callable function, return as-is (predefined tools) + return tool + + tool_name = getattr(tool, "__name__", "unknown") + tool_doc = tool.__doc__ + + if inspect.iscoroutinefunction(tool): + # Async function + @wraps(tool) + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = await tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return async_wrapped + else: + # Sync function + @wraps(tool) + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return sync_wrapped + + +def wrapped_config_with_tools( + config: "GenerateContentConfig", +) -> "GenerateContentConfig": + """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as + callable functions as a part of the config object.""" + + if not config or not getattr(config, "tools", None): + return config + + result = copy.copy(config) + result.tools = [wrapped_tool(tool) for tool in config.tools] + + return result + + +def _extract_response_text( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract text from response candidates.""" + + if not response or not getattr(response, "candidates", []): + return None + + texts = [] + for candidate in response.candidates: + if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): + continue + + if candidate.content is None or candidate.content.parts is None: + continue + + for part in candidate.content.parts: + if getattr(part, "text", None): + texts.append(part.text) + + return texts if texts else None + + +def extract_finish_reasons( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract finish reasons from response candidates.""" + if not response or not getattr(response, "candidates", []): + return None + + finish_reasons = [] + for candidate in response.candidates: + if getattr(candidate, "finish_reason", None): + # Convert enum value to string if necessary + reason = str(candidate.finish_reason) + # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") + if "." in reason: + reason = reason.split(".")[-1] + finish_reasons.append(reason) + + return finish_reasons if finish_reasons else None + + +def _transform_system_instruction_one_level( + system_instructions: "Union[ContentUnionDict, ContentUnion]", + can_be_content: bool, +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, str): + return [{"type": "text", "content": system_instructions}] + + if isinstance(system_instructions, Part) and system_instructions.text: + return [{"type": "text", "content": system_instructions.text}] + + if can_be_content and isinstance(system_instructions, Content): + if isinstance(system_instructions.parts, list): + for part in system_instructions.parts: + if isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + return text_parts + + if isinstance(system_instructions, dict) and system_instructions.get("text"): + return [{"type": "text", "content": system_instructions["text"]}] + + elif can_be_content and isinstance(system_instructions, dict): + parts = system_instructions.get("parts", []) + for part in parts: + if isinstance(part, Part) and isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + elif isinstance(part, dict) and isinstance(part.get("text"), str): + text_parts.append({"type": "text", "content": part["text"]}) + return text_parts + + return text_parts + + +def _transform_system_instructions( + system_instructions: "Union[ContentUnionDict, ContentUnion]", +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, list): + text_parts = list( + chain.from_iterable( + _transform_system_instruction_one_level( + instructions, can_be_content=False + ) + for instructions in system_instructions + ) + ) + + return text_parts + + return _transform_system_instruction_one_level( + system_instructions, can_be_content=True + ) + + +def set_span_data_for_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + model: str, + contents: "ContentListUnion", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for the request.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if kwargs.get("stream", False): + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + config: "Optional[GenerateContentConfig]" = kwargs.get("config") + + # Set input messages/prompts if PII is allowed + if should_send_default_pii() and integration.include_prompts: + messages = [] + + # Add system instruction if present + system_instructions = None + if config and hasattr(config, "system_instruction"): + system_instructions = config.system_instruction + elif isinstance(config, dict) and "system_instruction" in config: + system_instructions = config.get("system_instruction") + + if system_instructions is not None: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + # Extract messages from contents + contents_messages = extract_contents_messages(contents) + messages.extend(contents_messages) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Extract parameters directly from config (not nested under generation_config) + for param, span_key in [ + ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), + ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), + ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), + ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), + ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), + ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), + ("seed", SPANDATA.GEN_AI_REQUEST_SEED), + ]: + if hasattr(config, param): + value = getattr(config, param) + if value is not None: + set_on_span(span_key, value) + + # Set tools if available + if config is not None and hasattr(config, "tools"): + tools = config.tools + if tools: + formatted_tools = _format_tools_for_span(tools) + if formatted_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + formatted_tools, + unpack=False, + ) + + +def set_span_data_for_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "GenerateContentResponse", +) -> None: + """Set span data for the response.""" + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if should_send_default_pii() and integration.include_prompts: + response_texts = _extract_response_text(response) + if response_texts: + # Format as JSON string array as per documentation + set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) + + tool_calls = extract_tool_calls(response) + if tool_calls: + # Tool calls should be JSON serialized + set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) + + finish_reasons = extract_finish_reasons(response) + if finish_reasons: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + + if getattr(response, "response_id", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) + + if getattr(response, "model_version", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) + + usage_data = extract_usage_data(response) + + if usage_data["input_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) + + if usage_data["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage_data["input_tokens_cached"], + ) + + if usage_data["output_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) + + if usage_data["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage_data["output_tokens_reasoning"], + ) + + if usage_data["total_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) + + +def prepare_generate_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[Any, Any, str]": + """Extract and prepare common arguments for generate_content methods.""" + model = args[0] if args else kwargs.get("model", "unknown") + contents = args[1] if len(args) > 1 else kwargs.get("contents") + model_name = get_model_name(model) + + config = kwargs.get("config") + wrapped_config = wrapped_config_with_tools(config) + if wrapped_config is not config: + kwargs["config"] = wrapped_config + + return model, contents, model_name + + +def prepare_embed_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[str, Any]": + """Extract and prepare common arguments for embed_content methods. + + Returns: + tuple: (model_name, contents) + """ + model = kwargs.get("model", "unknown") + contents = kwargs.get("contents") + model_name = get_model_name(model) + + return model_name, contents + + +def set_span_data_for_embed_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + contents: "Any", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for embedding request.""" + # Include input contents if PII is allowed + if should_send_default_pii() and integration.include_prompts: + if contents: + # For embeddings, contents is typically a list of strings/texts + input_texts = [] + + # Handle various content formats + if isinstance(contents, str): + input_texts = [contents] + elif isinstance(contents, list): + for item in contents: + text = extract_contents_text(item) + if text: + input_texts.append(text) + else: + text = extract_contents_text(contents) + if text: + input_texts = [text] + + if input_texts: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + input_texts, + unpack=False, + ) + + +def set_span_data_for_embed_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "EmbedContentResponse", +) -> None: + """Set span data for embedding response.""" + if not response: + return + + # Extract token counts from embeddings statistics (Vertex AI only) + # Each embedding has its own statistics with token_count + if hasattr(response, "embeddings") and response.embeddings: + total_tokens = 0 + + for embedding in response.embeddings: + if hasattr(embedding, "statistics") and embedding.statistics: + token_count = getattr(embedding.statistics, "token_count", None) + if token_count is not None: + total_tokens += int(token_count) + + # Set token count if we found any + if total_tokens > 0: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) + else: + span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py new file mode 100644 index 0000000000..dcb60e9561 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py @@ -0,0 +1,173 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +try: + import gql # type: ignore[import-not-found] + from gql.transport import ( # type: ignore[import-not-found] + AsyncTransport, + Transport, + ) + from gql.transport.exceptions import ( # type: ignore[import-not-found] + TransportQueryError, + ) + from graphql import ( + DocumentNode, + VariableDefinitionNode, + get_operation_ast, + print_ast, + ) + + try: + # gql 4.0+ + from gql import GraphQLRequest + except ImportError: + GraphQLRequest = None + +except ImportError: + raise DidNotEnable("gql is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Tuple, Union + + from sentry_sdk._types import Event, EventProcessor + + EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] + + +class GQLIntegration(Integration): + identifier = "gql" + + @staticmethod + def setup_once() -> None: + gql_version = parse_version(gql.__version__) + _check_minimum_version(GQLIntegration, gql_version) + + _patch_execute() + + +def _data_from_document(document: "DocumentNode") -> "EventDataType": + try: + operation_ast = get_operation_ast(document) + data: "EventDataType" = {"query": print_ast(document)} + + if operation_ast is not None: + data["variables"] = operation_ast.variable_definitions + if operation_ast.name is not None: + data["operationName"] = operation_ast.name.value + + return data + except (AttributeError, TypeError): + return dict() + + +def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: + """ + The RequestsHTTPTransport allows defining the HTTP method; all + other transports use POST. + """ + try: + return transport.method + except AttributeError: + return "POST" + + +def _request_info_from_transport( + transport: "Union[Transport, AsyncTransport, None]", +) -> "Dict[str, str]": + if transport is None: + return {} + + request_info = { + "method": _transport_method(transport), + } + + try: + request_info["url"] = transport.url + except AttributeError: + pass + + return request_info + + +def _patch_execute() -> None: + real_execute = gql.Client.execute + + # Maintain signature for backwards compatibility. + # gql.Client.execute() accepts a positional-only "request" + # parameter with version 4.0.0. + @ensure_integration_enabled(GQLIntegration, real_execute) + def sentry_patched_execute( + self: "gql.Client", + document: "DocumentNode", + *args: "Any", + **kwargs: "Any", + ) -> "Any": + scope = sentry_sdk.get_isolation_scope() + # document is a gql.GraphQLRequest with gql v4.0.0. + scope.add_event_processor(_make_gql_event_processor(self, document)) + + try: + # document is a gql.GraphQLRequest with gql v4.0.0. + return real_execute(self, document, *args, **kwargs) + except TransportQueryError as e: + event, hint = event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "gql", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint) + raise e + + gql.Client.execute = sentry_patched_execute + + +def _make_gql_event_processor( + client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" +) -> "EventProcessor": + def processor(event: "Event", hint: "dict[str, Any]") -> "Event": + try: + errors = hint["exc_info"][1].errors + except (AttributeError, KeyError): + errors = None + + request = event.setdefault("request", {}) + request.update( + { + "api_target": "graphql", + **_request_info_from_transport(client.transport), + } + ) + + if should_send_default_pii(): + if GraphQLRequest is not None and isinstance( + document_or_request, GraphQLRequest + ): + # In v4.0.0, gql moved to using GraphQLRequest instead of + # DocumentNode in execute + # https://github.com/graphql-python/gql/pull/556 + document = document_or_request.document + else: + document = document_or_request + + request["data"] = _data_from_document(document) + contexts = event.setdefault("contexts", {}) + response = contexts.setdefault("response", {}) + response.update( + { + "data": {"errors": errors}, + "type": response, + } + ) + + return event + + return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py new file mode 100644 index 0000000000..4938a5c0f3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py @@ -0,0 +1,181 @@ +from contextlib import contextmanager + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from graphene.types import schema as graphene_schema # type: ignore +except ImportError: + raise DidNotEnable("graphene is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Any, Dict, Union + + from graphene.language.source import Source # type: ignore + from graphql.execution import ExecutionResult + from graphql.type import GraphQLSchema + + from sentry_sdk._types import Event + + +class GrapheneIntegration(Integration): + identifier = "graphene" + + @staticmethod + def setup_once() -> None: + version = package_version("graphene") + _check_minimum_version(GrapheneIntegration, version) + + _patch_graphql() + + +def _patch_graphql() -> None: + old_graphql_sync = graphene_schema.graphql_sync + old_graphql_async = graphene_schema.graphql + + @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) + def _sentry_patched_graphql_sync( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = old_graphql_sync(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + async def _sentry_patched_graphql_async( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) + if integration is None: + return await old_graphql_async(schema, source, *args, **kwargs) + + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = await old_graphql_async(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + graphene_schema.graphql_sync = _sentry_patched_graphql_sync + graphene_schema.graphql = _sentry_patched_graphql_async + + +def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + if should_send_default_pii(): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + +@contextmanager +def graphql_span( + schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" +) -> "Generator[None, None, None]": + operation_name = kwargs.get("operation_name") or "" + + operation_type = "query" + op = OP.GRAPHQL_QUERY + if source.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif source.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + sentry_sdk.add_breadcrumb( + crumb={ + "data": { + "operation_name": operation_name, + "operation_type": operation_type, + }, + "category": "graphql.operation", + }, + ) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + additional_attributes = {} + if should_send_default_pii(): + additional_attributes["graphql.document"] = source + + _graphql_span = sentry_sdk.traces.start_span( + name=operation_name, + attributes={ + "sentry.op": op, + "graphql.operation.name": operation_name, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) + + if should_send_default_pii(): + _graphql_span.set_data("graphql.document", source) + _graphql_span.set_data("graphql.operation.name", operation_name) + _graphql_span.set_data("graphql.operation.type", operation_type) + + _graphql_span.__enter__() + + try: + yield + finally: + if is_span_streaming_enabled: + _graphql_span.end() # type: ignore + else: + _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py new file mode 100644 index 0000000000..bf74ff1351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py @@ -0,0 +1,188 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any, Optional, Sequence + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .client import ClientInterceptor +from .server import ServerInterceptor + +try: + import grpc + from grpc import Channel, Server, intercept_channel + from grpc.aio import Channel as AsyncChannel + from grpc.aio import Server as AsyncServer + + from .aio.client import ( + SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, + ) + from .aio.client import ( + SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, + ) + from .aio.server import ServerInterceptor as AsyncServerInterceptor +except ImportError: + raise DidNotEnable("grpcio is not installed.") + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from typing import Callable, ParamSpec +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + +P = ParamSpec("P") + +GRPC_VERSION = parse_version(grpc.__version__) + + +def _is_channel_intercepted(channel: "Channel") -> bool: + interceptor = getattr(channel, "_interceptor", None) + while interceptor is not None: + if isinstance(interceptor, ClientInterceptor): + return True + + inner_channel = getattr(channel, "_channel", None) + if inner_channel is None: + return False + + channel = inner_channel + interceptor = getattr(channel, "_interceptor", None) + + return False + + +def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + "Wrapper for synchronous secure and insecure channel." + + @wraps(func) + def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": + channel = func(*args, **kwargs) + if not _is_channel_intercepted(channel): + return intercept_channel(channel, ClientInterceptor()) + else: + return channel + + return patched_channel + + +def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + @wraps(func) + def patched_intercept_channel( + channel: "Channel", *interceptors: "grpc.ServerInterceptor" + ) -> "Channel": + if _is_channel_intercepted(channel): + interceptors = tuple( + [ + interceptor + for interceptor in interceptors + if not isinstance(interceptor, ClientInterceptor) + ] + ) + else: + interceptors = interceptors + return intercept_channel(channel, *interceptors) + + return patched_intercept_channel # type: ignore + + +def _wrap_channel_async( + func: "Callable[P, AsyncChannel]", +) -> "Callable[P, AsyncChannel]": + "Wrapper for asynchronous secure and insecure channel." + + @wraps(func) + def patched_channel( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Channel": + sentry_interceptors = [ + AsyncUnaryUnaryClientInterceptor(), + AsyncUnaryStreamClientIntercetor(), + ] + interceptors = [*sentry_interceptors, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_channel # type: ignore + + +def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": + """Wrapper for synchronous server.""" + + @wraps(func) + def patched_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + interceptors = [ + interceptor + for interceptor in interceptors or [] + if not isinstance(interceptor, ServerInterceptor) + ] + server_interceptor = ServerInterceptor() + interceptors = [server_interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_server # type: ignore + + +def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": + """Wrapper for asynchronous server.""" + + @wraps(func) + def patched_aio_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + server_interceptor = AsyncServerInterceptor() + interceptors = [ + server_interceptor, + *(interceptors or []), + ] + + try: + # We prefer interceptors as a list because of compatibility with + # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 + # However, prior to grpc 1.42.0, only tuples were accepted, so we + # have no choice there. + if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): + interceptors = tuple(interceptors) + except Exception: + pass + + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_aio_server # type: ignore + + +class GRPCIntegration(Integration): + identifier = "grpc" + + @staticmethod + def setup_once() -> None: + import grpc + + grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) + grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) + grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) + + grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) + grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) + + grpc.server = _wrap_sync_server(grpc.server) + grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py new file mode 100644 index 0000000000..4d21815254 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py @@ -0,0 +1,7 @@ +from .client import ClientInterceptor +from .server import ServerInterceptor + +__all__ = [ + "ClientInterceptor", + "ServerInterceptor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py new file mode 100644 index 0000000000..d07b7f19be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py @@ -0,0 +1,146 @@ +from typing import Any, AsyncIterable, Callable, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.protobuf.message import Message + from grpc.aio import ( + ClientCallDetails, + Metadata, + UnaryStreamCall, + UnaryStreamClientInterceptor, + UnaryUnaryCall, + UnaryUnaryClientInterceptor, + ) +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor: + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + if client_call_details.metadata is None: + client_call_details = client_call_details._replace(metadata=Metadata()) + elif not isinstance(client_call_details.metadata, Metadata): + # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 + # See https://github.com/grpc/grpc/issues/34298. + client_call_details = client_call_details._replace( + metadata=Metadata.from_tuple(client_call_details.metadata) + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + client_call_details.metadata.add(key, value) + return client_call_details + + +class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore + async def intercept_unary_unary( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[UnaryUnaryCall, Message]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_data("code", status_code.name) + + return response + + +class SentryUnaryStreamClientInterceptor( + ClientInterceptor, + UnaryStreamClientInterceptor, # type: ignore +): + async def intercept_unary_stream( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + # status_code = await response.code() + # span.set_data("code", status_code) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py new file mode 100644 index 0000000000..010337e98c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py @@ -0,0 +1,134 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from typing import Any, Optional + + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler + from grpc.aio import AbortError, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Callable[[ServicerContext], str] | None" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + async def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", + handler_call_details: "HandlerCallDetails", + ) -> "Optional[Awaitable[RpcMethodHandler]]": + handler = await continuation(handler_call_details) + if handler is None: + return None + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + if not handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.unary_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + with sentry_sdk.isolation_scope(): + name = ( + custom_find_name(context) if custom_find_name else method_name + ) + if not name: + return await handler(request, context) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + # What if the headers are empty? + sentry_sdk.traces.continue_trace( + dict(context.invocation_metadata()) + ) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + else: + # What if the headers are empty? + transaction = sentry_sdk.continue_trace( + dict(context.invocation_metadata()), + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + + elif not handler.request_streaming and handler.response_streaming: + handler_factory = grpc.unary_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.unary_stream(request, context): + yield r + + elif handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.stream_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + response = handler.stream_unary(request, context) + return await response + + elif handler.request_streaming and handler.response_streaming: + handler_factory = grpc.stream_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.stream_stream(request, context): + yield r + + return handler_factory( + wrapped, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py new file mode 100644 index 0000000000..5384a0a78f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py @@ -0,0 +1,149 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Iterator, Union + +try: + import grpc + from google.protobuf.message import Message + from grpc import Call, ClientCallDetails + from grpc._interceptor import _UnaryOutcome + from grpc.aio._interceptor import UnaryStreamCall +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor( + grpc.UnaryUnaryClientInterceptor, # type: ignore + grpc.UnaryStreamClientInterceptor, # type: ignore +): + def intercept_unary_unary( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "_UnaryOutcome": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_attribute( + SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name + ) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_data("code", response.code().name) + + return response + + def intercept_unary_stream( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[Iterator[Message], Call]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + response: "UnaryStreamCall" + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + metadata = ( + list(client_call_details.metadata) if client_call_details.metadata else [] + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + metadata.append((key, value)) + + client_call_details = grpc._interceptor._ClientCallDetails( + method=client_call_details.method, + timeout=client_call_details.timeout, + metadata=metadata, + credentials=client_call_details.credentials, + wait_for_ready=client_call_details.wait_for_ready, + compression=client_call_details.compression, + ) + + return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py new file mode 100644 index 0000000000..9fdb975caf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py new file mode 100644 index 0000000000..1cba1d4b85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py @@ -0,0 +1,91 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Callable, Optional + + from google.protobuf.message import Message + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Optional[Callable[[ServicerContext], str]]" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", + handler_call_details: "HandlerCallDetails", + ) -> "RpcMethodHandler": + handler = continuation(handler_call_details) + if not handler or not handler.unary_unary: + return handler + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + def behavior(request: "Message", context: "ServicerContext") -> "Message": + with sentry_sdk.isolation_scope(): + name = custom_find_name(context) if custom_find_name else method_name + + if name: + metadata = dict(context.invocation_metadata()) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(metadata) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + transaction = sentry_sdk.continue_trace( + metadata, + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + return handler.unary_unary(request, context) + + return grpc.unary_unary_rpc_method_handler( + behavior, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py new file mode 100644 index 0000000000..a68f20b299 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx is not installed") + +__all__ = ["HttpxIntegration"] + + +class HttpxIntegration(Integration): + identifier = "httpx" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx_client() + _install_httpx_async_client() + + +def _install_httpx_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(HttpxIntegration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(HttpxIntegration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py new file mode 100644 index 0000000000..25062aaa11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx2 import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx2 is not installed") + +__all__ = ["Httpx2Integration"] + + +class Httpx2Integration(Integration): + identifier = "httpx2" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx2 has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx2_client() + _install_httpx2_async_client() + + +def _install_httpx2_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(Httpx2Integration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx2_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(Httpx2Integration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py new file mode 100644 index 0000000000..c3bbc8abcf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py @@ -0,0 +1,239 @@ +import sys +from datetime import datetime +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, Hint + from sentry_sdk.utils import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + +try: + from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task + from huey.exceptions import CancelExecution, RetryTask, TaskLockedException +except ImportError: + raise DidNotEnable("Huey is not installed") + +try: + from huey.api import chord as HueyChord + from huey.api import group as HueyGroup +except ImportError: + HueyChord = None + HueyGroup = None + + +HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) + + +class HueyIntegration(Integration): + identifier = "huey" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_enqueue() + patch_execute() + _register_control_flow_exception( + [CancelExecution, RetryTask, TaskLockedException] + ) + + +def patch_enqueue() -> None: + old_enqueue = Huey.enqueue + + @ensure_integration_enabled(HueyIntegration, old_enqueue) + def _sentry_enqueue( + self: "Huey", item: "Any" + ) -> "Optional[Union[Result, ResultGroup]]": + if HueyChord is not None and isinstance(item, HueyChord): + span_name = "Huey Chord" + elif HueyGroup is not None and isinstance(item, HueyGroup): + span_name = "Huey Task Group" + else: + span_name = item.name + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + span_ctx = None + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_HUEY, + "sentry.origin": HueyIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_HUEY, + name=span_name, + origin=HueyIntegration.origin, + ) + + no_headers_types = (PeriodicTask,) + tuple( + t for t in [HueyGroup, HueyChord] if t is not None + ) + with span_ctx: + if not isinstance(item, no_headers_types): + # Attach trace propagation data to task kwargs. We do + # not do this for periodic tasks, as these don't + # really have an originating transaction. + # Additionally, we do not do this for Huey groups or chords, as enqueue will + # recursively call this method for each task within the list, resulting + # in the trace propagation data being attached to each task individually + # (which we want) + item.kwargs["sentry_headers"] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + return old_enqueue(self, item) + + Huey.enqueue = _sentry_enqueue + + +def _make_event_processor(task: "Any") -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["huey_task_id"] = task.id + tags["huey_task_retry"] = task.default_retries > task.retries + extra = event.setdefault("extra", {}) + extra["huey-job"] = { + "task": task.name, + "args": ( + task.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + task.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": (task.default_retries or 0) - task.retries, + } + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.ABORTED) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.OK + return + + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.ERROR + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": HueyIntegration.identifier, "handled": False}, + ) + scope.capture_event(event, hint=hint) + + +def _wrap_task_execute(func: "F") -> "F": + @ensure_integration_enabled(HueyIntegration, func) + def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": + try: + result = func(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_execute # type: ignore + + +def patch_execute() -> None: + old_execute = Huey._execute + + @ensure_integration_enabled(HueyIntegration, old_execute) + def _sentry_execute( + self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None + ) -> "Any": + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope._name = "huey" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task)) + + sentry_headers = task.kwargs.pop("sentry_headers", None) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + headers = sentry_headers or {} + sentry_sdk.traces.continue_trace(headers) + span_ctx = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_TASK_HUEY, + "sentry.origin": HueyIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + "messaging.message.id": task.id, + "messaging.message.system": "huey", + "messaging.message.retry.count": (task.default_retries or 0) + - task.retries, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + sentry_headers or {}, + name=task.name, + op=OP.QUEUE_TASK_HUEY, + source=TransactionSource.TASK, + origin=HueyIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + span_ctx = sentry_sdk.start_transaction(transaction) + + if not getattr(task, "_sentry_is_patched", False): + task.execute = _wrap_task_execute(task.execute) + task._sentry_is_patched = True + + with span_ctx: + return old_execute(self, task, timestamp) + + Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py new file mode 100644 index 0000000000..835acc7279 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py @@ -0,0 +1,392 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + _set_span_data_attribute, + get_start_span_function, + set_data_normalized, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Union + + from sentry_sdk.tracing import Span + +try: + import huggingface_hub.inference._client +except ImportError: + raise DidNotEnable("Huggingface not installed") + + +class HuggingfaceHubIntegration(Integration): + identifier = "huggingface_hub" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "HuggingfaceHubIntegration", include_prompts: bool = True + ) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks + huggingface_hub.inference._client.InferenceClient.text_generation = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.text_generation, + OP.GEN_AI_TEXT_COMPLETION, + ) + ) + huggingface_hub.inference._client.InferenceClient.chat_completion = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.chat_completion, + OP.GEN_AI_CHAT, + ) + ) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "huggingface_hub", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": + @wraps(f) + def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) + if integration is None: + return f(*args, **kwargs) + + prompt = None + if "prompt" in kwargs: + prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] + elif len(args) >= 2: + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error + return f(*args, **kwargs) + + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(sentry_sdk.get_client().options): + span = sentry_sdk.traces.start_span( + name=f"{operation_name} {model}", + attributes={ + "sentry.op": op, + "sentry.origin": HuggingfaceHubIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=op, + name=f"{operation_name} {model}", + origin=HuggingfaceHubIntegration.origin, + ) + span.__enter__() + + _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) + + if model: + _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) + + # Input attributes + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + ) + + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + _set_span_data_attribute(span, span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + tool_calls = None + usage = None + + with capture_internal_exceptions(): + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) + + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) + + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): + span.__exit__(None, None, None) + return res + + if kwargs.get("details", False): + # text-generation stream output + def new_details_iterator() -> "Iterable[Any]": + finish_reason = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + + with capture_internal_exceptions(): + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None + ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + span.__exit__(None, None, None) + + return new_details_iterator() + + else: + # chat-completion stream output + def new_iterator() -> "Iterable[str]": + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tool_calls = None + usage = None + + with capture_internal_exceptions(): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if hasattr(chunk, "choices") and chunk.choices is not None: + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content + ) + + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason + + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls + + yield chunk + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + + span.__exit__(None, None, None) + + return new_iterator() + + return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py new file mode 100644 index 0000000000..9dcbb189ce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py @@ -0,0 +1,1412 @@ +import itertools +import json +import sys +import warnings +from collections import OrderedDict +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + _get_value, + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import capture_internal_exceptions, logger + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Dict, + Iterator, + List, + Optional, + Union, + ) + from uuid import UUID + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + + +try: + from langchain_core.agents import AgentFinish + from langchain_core.callbacks import ( + BaseCallbackHandler, + BaseCallbackManager, + Callbacks, + manager, + ) + from langchain_core.messages import BaseMessage + from langchain_core.outputs import LLMResult + +except ImportError: + raise DidNotEnable("langchain not installed") + + +try: + # >=v1 + from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] +except ImportError: + try: + # "Optional[str]": + ai_type = all_params.get("_type") + + if not ai_type or not isinstance(ai_type, str): + return None + + return ai_type + + +DATA_FIELDS = { + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, +} + + +def _transform_langchain_content_block( + content_block: "Dict[str, Any]", +) -> "Dict[str, Any]": + """ + Transform a LangChain content block using the shared transform_content_part function. + + Returns the original content block if transformation is not applicable + (e.g., for text blocks or unrecognized formats). + """ + result = transform_content_part(content_block) + return result if result is not None else content_block + + +def _transform_langchain_message_content(content: "Any") -> "Any": + """ + Transform LangChain message content, handling both string content and + list of content blocks. + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for block in content: + if isinstance(block, dict): + transformed.append(_transform_langchain_content_block(block)) + else: + transformed.append(block) + return transformed + + return content + + +def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": + system_instructions = [] + + for list_ in messages: + for message in list_: + # type of content: str | list[str | dict] | None + if message.type == "system" and isinstance(message.content, str): + system_instructions.append(message.content) + + elif message.type == "system" and isinstance(message.content, list): + for item in message.content: + if isinstance(item, str): + system_instructions.append(item) + + elif isinstance(item, dict) and item.get("type") == "text": + instruction = item.get("text") + if isinstance(instruction, str): + system_instructions.append(instruction) + + return system_instructions + + +def _transform_system_instructions( + system_instructions: "List[str]", +) -> "List[TextPart]": + return [ + { + "type": "text", + "content": instruction, + } + for instruction in system_instructions + ] + + +class LangchainIntegration(Integration): + identifier = "langchain" + origin = f"auto.ai.{identifier}" + + _ignored_exceptions: "set[type[Exception]]" = set() + + def __init__( + self: "LangchainIntegration", + include_prompts: bool = True, + max_spans: "Optional[int]" = None, + ) -> None: + self.include_prompts = include_prompts + self.max_spans = max_spans + + if max_spans is not None: + warnings.warn( + "The `max_spans` parameter of `LangchainIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + manager._configure = _wrap_configure(manager._configure) + + if AgentExecutor is not None: + AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) + AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) + + # Patch embeddings providers + _patch_embeddings_provider(OpenAIEmbeddings) + _patch_embeddings_provider(AzureOpenAIEmbeddings) + _patch_embeddings_provider(VertexAIEmbeddings) + _patch_embeddings_provider(BedrockEmbeddings) + _patch_embeddings_provider(CohereEmbeddings) + _patch_embeddings_provider(MistralAIEmbeddings) + _patch_embeddings_provider(HuggingFaceEmbeddings) + _patch_embeddings_provider(OllamaEmbeddings) + + +class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] + """Callback handler that creates Sentry spans.""" + + def __init__( + self, max_span_map_size: "Optional[int]", include_prompts: bool + ) -> None: + self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() + self.max_span_map_size = max_span_map_size + self.include_prompts = include_prompts + + def gc_span_map(self) -> None: + if self.max_span_map_size is not None: + while len(self.span_map) > self.max_span_map_size: + run_id, span = self.span_map.popitem(last=False) + self._exit_span(span, run_id) + + def _handle_error(self, run_id: "UUID", error: "Any") -> None: + is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) + + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if is_ignored: + span.__exit__(None, None, None) + else: + sentry_sdk.capture_exception( + error, span._scope if isinstance(span, StreamedSpan) else span.scope + ) + span.__exit__(type(error), error, error.__traceback__) + + del self.span_map[run_id] + + def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": + # Transform content to handle multimodal data (images, audio, video, files) + transformed_content = _transform_langchain_message_content(message.content) + parsed = {"role": message.type, "content": transformed_content} + parsed.update(message.additional_kwargs) + return parsed + + def _create_span( + self: "SentryLangchainCallback", + run_id: "UUID", + parent_id: "Optional[Any]", + op: str, + name: str, + origin: str, + ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span = None + if parent_id: + parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( + self.span_map.get(parent_id) + ) + if parent_span: + span = ( + sentry_sdk.traces.start_span( + parent_span=parent_span, + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if isinstance(parent_span, StreamedSpan) + else parent_span.start_child(op=op, name=name, origin=origin) + ) + + if span is None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span = ( + sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if span_streaming + else sentry_sdk.start_span(op=op, name=name, origin=origin) + ) + + span.__enter__() + self.span_map[run_id] = span + self.gc_span_map() + return span + + def _exit_span( + self: "SentryLangchainCallback", + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + run_id: "UUID", + ) -> None: + span.__exit__(None, None, None) + del self.span_map[run_id] + + def on_llm_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + prompts: "List[str]", + *, + run_id: "UUID", + tags: "Optional[List[str]]" = None, + parent_run_id: "Optional[UUID]" = None, + metadata: "Optional[Dict[str, Any]]" = None, + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + parent_run_id, + op=OP.GEN_AI_TEXT_COMPLETION, + name=f"text_completion {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") + + run_name = kwargs.get("name") + if run_name: + set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + if model: + set_on_span( + SPANDATA.GEN_AI_REQUEST_MODEL, + model, + ) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + normalized_messages = [ + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, + "content": {"type": "text", "text": prompt}, + } + for prompt in prompts + ] + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + messages: "List[List[BaseMessage]]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if model: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + system_instructions = _get_system_instructions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + normalized_messages = [] + for list_ in messages: + for message in list_: + if message.type == "system": + continue + + normalized_messages.append( + self._normalize_langchain_message(message) + ) + normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + try: + generation = response.generations[0][0] + except IndexError: + generation = None + + if generation is not None: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + + try: + response_model = generation.message.response_metadata.get( + "model_name" + ) + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + except AttributeError: + pass + + try: + finish_reason = generation.generation_info.get("finish_reason") + if finish_reason is not None: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + [finish_reason], + ) + except AttributeError: + pass + + try: + if should_send_default_pii() and self.include_prompts: + tool_calls = getattr(generation.message, "tool_calls", None) + if tool_calls is not None and tool_calls != []: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + except AttributeError: + pass + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM errors.""" + self._handle_error(run_id, error) + + def on_chat_model_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model errors.""" + self._handle_error(run_id, error) + + def on_agent_finish( + self: "SentryLangchainCallback", + finish: "AgentFinish", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() + ) + + self._exit_span(span, run_id) + + def on_tool_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + input_str: str, + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + tool_name = serialized.get("name") or kwargs.get("name") or "" + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + tool_description = serialized.get("description") + if tool_description is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_TOOL_INPUT, + kwargs.get("inputs", [input_str]), + ) + + def on_tool_end( + self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" + ) -> "Any": + """Run when tool ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) + + self._exit_span(span, run_id) + + def on_tool_error( + self, + error: "SentryLangchainCallback", + *args: "Union[Exception, KeyboardInterrupt]", + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool errors.""" + self._handle_error(run_id, error) + + +def _extract_tokens( + token_usage: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + if not token_usage: + return None, None, None + + input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( + token_usage, "input_tokens" + ) + output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( + token_usage, "output_tokens" + ) + total_tokens = _get_value(token_usage, "total_tokens") + + return input_tokens, output_tokens, total_tokens + + +def _extract_tokens_from_generations( + generations: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + """Extract token usage from response.generations structure.""" + if not generations: + return None, None, None + + total_input = 0 + total_output = 0 + total_total = 0 + + for gen_list in generations: + for gen in gen_list: + token_usage = _get_token_usage(gen) + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + total_input += input_tokens if input_tokens is not None else 0 + total_output += output_tokens if output_tokens is not None else 0 + total_total += total_tokens if total_tokens is not None else 0 + + return ( + total_input if total_input > 0 else None, + total_output if total_output > 0 else None, + total_total if total_total > 0 else None, + ) + + +def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": + """ + Check multiple paths to extract token usage from different objects. + """ + possible_names = ("usage", "token_usage", "usage_metadata") + + message = _get_value(obj, "message") + if message is not None: + for name in possible_names: + usage = _get_value(message, name) + if usage is not None: + return usage + + llm_output = _get_value(obj, "llm_output") + if llm_output is not None: + for name in possible_names: + usage = _get_value(llm_output, name) + if usage is not None: + return usage + + for name in possible_names: + usage = _get_value(obj, name) + if usage is not None: + return usage + + return None + + +def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: + token_usage = _get_token_usage(response) + if token_usage: + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + else: + input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( + response.generations + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) + + +def _get_request_data( + obj: "Any", args: "Any", kwargs: "Any" +) -> "tuple[Optional[str], Optional[List[Any]]]": + """ + Get the agent name and available tools for the agent. + """ + agent = getattr(obj, "agent", None) + runnable = getattr(agent, "runnable", None) + runnable_config = getattr(runnable, "config", {}) + tools = ( + getattr(obj, "tools", None) + or getattr(agent, "tools", None) + or runnable_config.get("tools") + or runnable_config.get("available_tools") + ) + tools = tools if tools and len(tools) > 0 else None + + try: + agent_name = None + if len(args) > 1: + agent_name = args[1].get("run_name") + if agent_name is None: + agent_name = runnable_config.get("run_name") + except Exception: + pass + + return (agent_name, tools) + + +def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": + """Parse and simplify tools into a cleaner format.""" + if not tools: + return None + + if not isinstance(tools, (list, tuple)): + return None + + simplified_tools = [] + for tool in tools: + try: + if isinstance(tool, dict): + if "function" in tool and isinstance(tool["function"], dict): + func = tool["function"] + simplified_tool = { + "name": func.get("name"), + "description": func.get("description"), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif "name" in tool: + simplified_tool = { + "name": tool.get("name"), + "description": tool.get("description"), + } + simplified_tools.append(simplified_tool) + else: + name = ( + tool.get("name") + or tool.get("tool_name") + or tool.get("function_name") + ) + if name: + simplified_tools.append( + { + "name": name, + "description": tool.get("description") + or tool.get("desc"), + } + ) + elif hasattr(tool, "name"): + simplified_tool = { + "name": getattr(tool, "name", None), + "description": getattr(tool, "description", None) + or getattr(tool, "desc", None), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif hasattr(tool, "__name__"): + simplified_tools.append( + { + "name": tool.__name__, + "description": getattr(tool, "__doc__", None), + } + ) + else: + tool_str = str(tool) + if tool_str and tool_str != "": + simplified_tools.append({"name": tool_str, "description": None}) + except Exception: + continue + + return simplified_tools if simplified_tools else None + + +def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: + """Set available tools data on a span if tools are provided.""" + if tools is not None: + simplified_tools = _simplify_langchain_tools(tools) + if simplified_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + simplified_tools, + unpack=False, + ) + + +def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_configure( + callback_manager_cls: type, + inheritable_callbacks: "Callbacks" = None, + local_callbacks: "Callbacks" = None, + *args: "Any", + **kwargs: "Any", + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(LangchainIntegration) + if integration is None: + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + local_callbacks = local_callbacks or [] + + # Handle each possible type of local_callbacks. For each type, we + # extract the list of callbacks to check for SentryLangchainCallback, + # and define a function that would add the SentryLangchainCallback + # to the existing callbacks list. + if isinstance(local_callbacks, BaseCallbackManager): + callbacks_list = local_callbacks.handlers + elif isinstance(local_callbacks, BaseCallbackHandler): + callbacks_list = [local_callbacks] + elif isinstance(local_callbacks, list): + callbacks_list = local_callbacks + else: + logger.debug("Unknown callback type: %s", local_callbacks) + # Just proceed with original function call + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + # Handle each possible type of inheritable_callbacks. + if isinstance(inheritable_callbacks, BaseCallbackManager): + inheritable_callbacks_list = inheritable_callbacks.handlers + elif isinstance(inheritable_callbacks, list): + inheritable_callbacks_list = inheritable_callbacks + else: + inheritable_callbacks_list = [] + + if not any( + isinstance(cb, SentryLangchainCallback) + for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) + ): + sentry_handler = SentryLangchainCallback( + integration.max_spans, + integration.include_prompts, + ) + if isinstance(local_callbacks, BaseCallbackManager): + local_callbacks = local_callbacks.copy() + local_callbacks.handlers = [ + *local_callbacks.handlers, + sentry_handler, + ] + elif isinstance(local_callbacks, BaseCallbackHandler): + local_callbacks = [local_callbacks, sentry_handler] + else: + local_callbacks = [*local_callbacks, sentry_handler] + + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + return new_configure + + +def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: False, + }, + ) as span: + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + else: + start_span_function = get_start_span_function() + + with start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) as span: + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + + return new_invoke + + +def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + else: + start_span_function = get_start_span_function() + + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + input = args[0].get("input") if len(args) >= 1 else None + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Run the agent + result = f(self, *args, **kwargs) + + old_iterator = result + + def new_iterator() -> "Iterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + async def new_iterator_async() -> "AsyncIterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + async for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + if str(type(result)) == "": + result = new_iterator_async() + else: + result = new_iterator() + + return result + + return new_stream + + +def _patch_embeddings_provider(provider_class: "Any") -> None: + """Patch an embeddings provider class with monitoring wrappers.""" + if provider_class is None: + return + + if hasattr(provider_class, "embed_documents"): + provider_class.embed_documents = _wrap_embedding_method( + provider_class.embed_documents + ) + if hasattr(provider_class, "embed_query"): + provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) + if hasattr(provider_class, "aembed_documents"): + provider_class.aembed_documents = _wrap_async_embedding_method( + provider_class.aembed_documents + ) + if hasattr(provider_class, "aembed_query"): + provider_class.aembed_query = _wrap_async_embedding_method( + provider_class.aembed_query + ) + + +def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap sync embedding methods (embed_documents and embed_query).""" + + @wraps(f) + def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + + return new_embedding_method + + +def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap async embedding methods (aembed_documents and aembed_query).""" + + @wraps(f) + async def new_async_embedding_method( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + + return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py new file mode 100644 index 0000000000..3d3856a913 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py @@ -0,0 +1,515 @@ +from functools import wraps +from typing import Any, Callable, List, Optional + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration + +# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. +from sentry_sdk.integrations.langchain import LangchainIntegration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +try: + from langgraph.errors import GraphBubbleUp + from langgraph.graph import StateGraph + from langgraph.pregel import Pregel +except ImportError: + raise DidNotEnable("langgraph not installed") + + +class LanggraphIntegration(Integration): + identifier = "langgraph" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) + # LangGraph lets users create agents using a StateGraph or the Functional API. + # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and + # the functional API execute on a Pregel instance. Pregel is the runtime for the graph + # and the invocation happens on Pregel, so patching the invoke methods takes care of both. + # The streaming methods are not patched, because due to some internal reasons, LangGraph + # will automatically patch the streaming methods to run through invoke, and by doing this + # we prevent duplicate spans for invocations. + StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) + if hasattr(Pregel, "invoke"): + Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) + if hasattr(Pregel, "ainvoke"): + Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) + + +def _get_graph_name(graph_obj: "Any") -> "Optional[str]": + for attr in ["name", "graph_name", "__name__", "_name"]: + if hasattr(graph_obj, attr): + name = getattr(graph_obj, attr) + if name and isinstance(name, str): + return name + return None + + +def _normalize_langgraph_message(message: "Any") -> "Any": + if not hasattr(message, "content"): + return None + + parsed = {"role": getattr(message, "type", None), "content": message.content} + + for attr in [ + "name", + "tool_calls", + "function_call", + "tool_call_id", + "response_metadata", + ]: + if hasattr(message, attr): + value = getattr(message, attr) + if value is not None: + parsed[attr] = value + + return parsed + + +def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": + if not state: + return None + + messages = None + + if isinstance(state, dict): + messages = state.get("messages") + elif hasattr(state, "messages"): + messages = state.messages + elif hasattr(state, "get") and callable(state.get): + try: + messages = state.get("messages") + except Exception: + pass + + if not messages or not isinstance(messages, (list, tuple)): + return None + + normalized_messages = [] + for message in messages: + try: + normalized = _normalize_langgraph_message(message) + if normalized: + normalized_messages.append(normalized) + except Exception: + continue + + return normalized_messages if normalized_messages else None + + +def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None or has_span_streaming_enabled(client.options): + return f(self, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GEN_AI_CREATE_AGENT, + origin=LanggraphIntegration.origin, + ) as span: + compiled_graph = f(self, *args, **kwargs) + + compiled_graph_name = getattr(compiled_graph, "name", None) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) + + if compiled_graph_name: + span.description = f"create_agent {compiled_graph_name}" + else: + span.description = "create_agent" + + if kwargs.get("model", None) is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) + + tools = None + get_graph = getattr(compiled_graph, "get_graph", None) + if get_graph and callable(get_graph): + graph_obj = compiled_graph.get_graph() + nodes = getattr(graph_obj, "nodes", None) + if nodes and isinstance(nodes, dict): + tools_node = nodes.get("tools") + if tools_node: + data = getattr(tools_node, "data", None) + if data and hasattr(data, "tools_by_name"): + tools = list(data.tools_by_name.keys()) + + if tools is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) + + return compiled_graph + + return new_compile + + +def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + else: + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_invoke + + +def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles(input_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_ainvoke + + +def _get_new_messages( + input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" +) -> "Optional[List[Any]]": + """Extract only the new messages added during this invocation.""" + if not output_messages: + return None + + if not input_messages: + return output_messages + + # only return the new messages, aka the output messages that are not in the input messages + input_count = len(input_messages) + new_messages = ( + output_messages[input_count:] if len(output_messages) > input_count else [] + ) + + return new_messages if new_messages else None + + +def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": + if not messages: + return None + + for message in reversed(messages): + if isinstance(message, dict): + role = message.get("role") + if role in ["assistant", "ai"]: + content = message.get("content") + if content and isinstance(content, str): + return content + + return None + + +def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": + if not messages: + return None + + tool_calls = [] + for message in messages: + if isinstance(message, dict): + msg_tool_calls = message.get("tool_calls") + if msg_tool_calls and isinstance(msg_tool_calls, list): + tool_calls.extend(msg_tool_calls) + + return tool_calls if tool_calls else None + + +def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + input_tokens = 0 + output_tokens = 0 + total_tokens = 0 + + for message in messages: + response_metadata = message.get("response_metadata") + if response_metadata is None: + continue + + token_usage = response_metadata.get("token_usage") + if not token_usage: + continue + + input_tokens += int(token_usage.get("prompt_tokens", 0)) + output_tokens += int(token_usage.get("completion_tokens", 0)) + total_tokens += int(token_usage.get("total_tokens", 0)) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens > 0: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + total_tokens, + ) + + +def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + if len(messages) == 0: + return + + last_message = messages[-1] + response_metadata = last_message.get("response_metadata") + if response_metadata is None: + return + + model_name = response_metadata.get("model_name") + if model_name is None: + return + + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) + + +def _set_response_attributes( + span: "Any", + input_messages: "Optional[List[Any]]", + result: "Any", + integration: "LanggraphIntegration", +) -> None: + parsed_response_messages = _parse_langgraph_messages(result) + new_messages = _get_new_messages(input_messages, parsed_response_messages) + + if new_messages is None: + return + + _set_usage_data(span, new_messages) + _set_response_model_name(span, new_messages) + + if not (should_send_default_pii() and integration.include_prompts): + return + + llm_response_text = _extract_llm_response_text(new_messages) + if llm_response_text: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) + elif new_messages: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) + else: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) + + tool_calls = _extract_tool_calls(new_messages) + if tool_calls: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(tool_calls), + unpack=False, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py new file mode 100644 index 0000000000..3c2c76450c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + import ldclient + from ldclient.hook import Hook, Metadata + + if TYPE_CHECKING: + from typing import Any + + from ldclient import LDClient + from ldclient.evaluation import EvaluationDetail + from ldclient.hook import EvaluationSeriesContext +except ImportError: + raise DidNotEnable("LaunchDarkly is not installed") + + +class LaunchDarklyIntegration(Integration): + identifier = "launchdarkly" + + def __init__(self, ld_client: "LDClient | None" = None) -> None: + """ + :param client: An initialized LDClient instance. If a client is not provided, this + integration will attempt to use the shared global instance. + """ + try: + client = ld_client or ldclient.get() + except Exception as exc: + raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) + + if not client.is_initialized(): + raise DidNotEnable("LaunchDarkly client is not initialized.") + + # Register the flag collection hook with the LD client. + client.add_hook(LaunchDarklyHook()) + + @staticmethod + def setup_once() -> None: + pass + + +class LaunchDarklyHook(Hook): + @property + def metadata(self) -> "Metadata": + return Metadata(name="sentry-flag-auditor") + + def after_evaluation( + self, + series_context: "EvaluationSeriesContext", + data: "dict[Any, Any]", + detail: "EvaluationDetail", + ) -> "dict[Any, Any]": + if isinstance(detail.value, bool): + add_feature_flag(series_context.key, detail.value) + + return data + + def before_evaluation( + self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" + ) -> "dict[Any, Any]": + return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py new file mode 100644 index 0000000000..49ead6b068 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py @@ -0,0 +1,376 @@ +import copy +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + set_data_normalized, + transform_openai_content_part, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from datetime import datetime + from typing import Any, Dict, List + +try: + import litellm # type: ignore[import-not-found] + from litellm import failure_callback, input_callback, success_callback +except ImportError: + raise DidNotEnable("LiteLLM not installed") + + +# Stash the span on a top-level key of the per-request kwargs dict litellm passes +# to every callback, so it lives and dies with the request. +_SPAN_KEY = "_sentry_span" + + +def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: + kwargs[_SPAN_KEY] = span + + +def _peek_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.get(_SPAN_KEY) + + +def _pop_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.pop(_SPAN_KEY, None) + + +def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": + """ + Convert the message parts from OpenAI format to the `gen_ai.request.messages` format + using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). + + Deep copies messages to avoid mutating original kwargs. + """ + # Deep copy to avoid mutating original messages from kwargs + messages = copy.deepcopy(messages) + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_openai_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + message["content"] = transformed + return messages + + +def _input_callback(kwargs: "Dict[str, Any]") -> None: + """Handle the start of a request.""" + client = sentry_sdk.get_client() + integration = client.get_integration(LiteLLMIntegration) + + if integration is None: + return + + # Get key parameters + full_model = kwargs.get("model", "") + try: + model, provider, _, _ = litellm.get_llm_provider(full_model) + except Exception: + model = full_model + provider = "unknown" + + call_type = kwargs.get("call_type", None) + if call_type == "embedding" or call_type == "aembedding": + operation = "embeddings" + else: + operation = "chat" + + # Start a new span/transaction + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"{operation} {model}", + attributes={ + "sentry.op": ( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + "sentry.origin": LiteLLMIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + name=f"{operation} {model}", + origin=LiteLLMIntegration.origin, + ) + span.__enter__() + + _store_span(kwargs, span) + + # Set basic data + set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) + + # Record input/messages if allowed + if should_send_default_pii() and integration.include_prompts: + if operation == "embeddings": + # For embeddings, look for the 'input' parameter + embedding_input = kwargs.get("input") + if embedding_input: + scope = sentry_sdk.get_current_scope() + # Normalize to list format + input_list = ( + embedding_input + if isinstance(embedding_input, list) + else [embedding_input] + ) + client = sentry_sdk.get_client() + messages_data = ( + truncate_and_annotate_embedding_inputs(input_list, span, scope) + if should_truncate_gen_ai_input(client.options) + else input_list + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + messages_data, + unpack=False, + ) + else: + # For chat, look for the 'messages' parameter + messages = kwargs.get("messages", []) + if messages: + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages = _convert_message_parts(messages) + messages_data = ( + truncate_and_annotate_messages(messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Record other parameters + params = { + "model": SPANDATA.GEN_AI_REQUEST_MODEL, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + } + for key, attribute in params.items(): + value = kwargs.get(key) + if value is not None: + set_data_normalized(span, attribute, value) + + +async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: + return _input_callback(kwargs) + + +def _success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle successful completion.""" + + span = _peek_span(kwargs) + if span is None: + return + + integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) + if integration is None: + return + + try: + # Record model information + if hasattr(completion_response, "model"): + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model + ) + + # Record response content if allowed + if should_send_default_pii() and integration.include_prompts: + if hasattr(completion_response, "choices"): + response_messages = [] + for choice in completion_response.choices: + if hasattr(choice, "message"): + if hasattr(choice.message, "model_dump"): + response_messages.append(choice.message.model_dump()) + elif hasattr(choice.message, "dict"): + response_messages.append(choice.message.dict()) + else: + # Fallback for basic message objects + msg = {} + if hasattr(choice.message, "role"): + msg["role"] = choice.message.role + if hasattr(choice.message, "content"): + msg["content"] = choice.message.content + if hasattr(choice.message, "tool_calls"): + msg["tool_calls"] = choice.message.tool_calls + response_messages.append(msg) + + if response_messages: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages + ) + + # Record token usage + if hasattr(completion_response, "usage"): + usage = completion_response.usage + record_token_usage( + span, + input_tokens=getattr(usage, "prompt_tokens", None), + output_tokens=getattr(usage, "completion_tokens", None), + total_tokens=getattr(usage, "total_tokens", None), + ) + + finally: + is_streaming = kwargs.get("stream") + # Callback is fired multiple times when streaming a response. + # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 + if ( + is_streaming is not True + or "complete_streaming_response" in kwargs + or "async_complete_streaming_response" in kwargs + ): + span = _pop_span(kwargs) + if span is not None: + span.__exit__(None, None, None) + + +async def _async_success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + return _success_callback( + kwargs, + completion_response, + start_time, + end_time, + ) + + +def _failure_callback( + kwargs: "Dict[str, Any]", + exception: Exception, + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle request failure.""" + span = _pop_span(kwargs) + if span is None: + return + + try: + # Capture the exception + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "litellm", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + finally: + # Always finish the span and clean up + span.__exit__(type(exception), exception, None) + + +class LiteLLMIntegration(Integration): + """ + LiteLLM integration for Sentry. + + This integration automatically captures LiteLLM API calls and sends them to Sentry + for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM + supports, including OpenAI, Anthropic, Google, Cohere, and many others. + + Features: + - Automatic exception capture for all LiteLLM calls + - Token usage tracking across all providers + - Provider detection and attribution + - Input/output message capture (configurable) + - Streaming response support + - Cost tracking integration + + Usage: + + ```python + import litellm + import sentry_sdk + + # Initialize Sentry with the LiteLLM integration + sentry_sdk.init( + dsn="your-dsn", + send_default_pii=True + integrations=[ + sentry_sdk.integrations.LiteLLMIntegration( + include_prompts=True # Set to False to exclude message content + ) + ] + ) + + # All LiteLLM calls will now be monitored + response = litellm.completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Hello!"}] + ) + ``` + + Configuration: + - include_prompts (bool): Whether to include prompts and responses in spans. + Defaults to True. Set to False to exclude potentially sensitive data. + """ + + identifier = "litellm" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """Set up LiteLLM callbacks for monitoring.""" + litellm.input_callback = input_callback or [] + if _input_callback not in litellm.input_callback: + litellm.input_callback.append(_input_callback) + if _async_input_callback not in litellm.input_callback: + litellm.input_callback.append(_async_input_callback) + + litellm.success_callback = success_callback or [] + if _success_callback not in litellm.success_callback: + litellm.success_callback.append(_success_callback) + if _async_success_callback not in litellm.success_callback: + litellm.success_callback.append(_async_success_callback) + + litellm.failure_callback = failure_callback or [] + if _failure_callback not in litellm.failure_callback: + litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py new file mode 100644 index 0000000000..f0c90a7921 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py @@ -0,0 +1,364 @@ +from collections.abc import Set +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from litestar import Litestar, Request # type: ignore + from litestar.data_extractors import ConnectionDataExtractor # type: ignore + from litestar.exceptions import HTTPException # type: ignore + from litestar.handlers.base import BaseRouteHandler # type: ignore + from litestar.middleware import DefineMiddleware # type: ignore + from litestar.routes.http import HTTPRoute # type: ignore +except ImportError: + raise DidNotEnable("Litestar is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from litestar.middleware import MiddlewareProtocol + from litestar.types import ( # type: ignore + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from litestar.types import ( + Scope as LitestarScope, + ) + from litestar.types.asgi_types import ASGIApp # type: ignore + + from sentry_sdk._types import Event, Hint + +_DEFAULT_TRANSACTION_NAME = "generic Litestar request" + + +class LitestarIntegration(Integration): + identifier = "litestar" + origin = f"auto.http.{identifier}" + + def __init__( + self, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. + # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following + # (among other things): + # 1. Logs them, some at least (such as 500s) as errors + # 2. Calls after_exception hooks + # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event + # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will + # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. + ignore_logger("litestar") + + +class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + def _capture_request_exception(self, exc: Exception) -> None: + """Avoid catching exceptions from request handlers. + + Those exceptions are already handled in Litestar.after_exception handler. + We still catch exceptions from application lifespan handlers. + """ + pass + + +def patch_app_init() -> None: + """ + Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. + See: + - https://docs.litestar.dev/2/usage/applications.html#after-exception + - https://docs.litestar.dev/2/usage/middleware/using-middleware.html + """ + old__init__ = Litestar.__init__ + + @ensure_integration_enabled(LitestarIntegration, old__init__) + def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: + kwargs["after_exception"] = [ + exception_handler, + *(kwargs.get("after_exception") or []), + ] + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Litestar.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryLitestarASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "LitestarScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(LitestarIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=middleware_name, + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR, + "sentry.origin": LitestarIntegration.origin, + }, + ) as middleware_span: + middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.traces.start_span( + name=getattr(receive, "__qualname__", str(receive)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.traces.start_span( + name=getattr(send, "__qualname__", str(send)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + else: + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR, + name=middleware_name, + origin=LitestarIntegration.origin, + ) as middleware_span: + middleware_span.set_tag("litestar.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_SEND, + name=getattr(send, "__qualname__", str(send)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type + elif hasattr(route_handler.fn, "value"): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = LitestarIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if isinstance(scope_user, dict): + return scope_user + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + return None + + +@ensure_integration_enabled(LitestarIntegration) +def exception_handler(exc: Exception, scope: "LitestarScope") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + if isinstance(exc, HTTPException): + integration = sentry_sdk.get_client().get_integration(LitestarIntegration) + if ( + integration is not None + and exc.status_code not in integration.failed_request_status_codes + ): + return + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": LitestarIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py new file mode 100644 index 0000000000..a310a0ced6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py @@ -0,0 +1,476 @@ +import logging +import sys +from datetime import datetime, timezone +from fnmatch import fnmatch +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.client import BaseClient +from sentry_sdk.integrations import Integration +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import ( + capture_internal_exceptions, + current_stacktrace, + event_from_exception, + has_logs_enabled, + safe_repr, + to_string, +) + +if TYPE_CHECKING: + from collections.abc import MutableMapping + from logging import LogRecord + from typing import Any, Dict, Optional + +DEFAULT_LEVEL = logging.INFO +DEFAULT_EVENT_LEVEL = logging.ERROR +LOGGING_TO_EVENT_LEVEL = { + logging.NOTSET: "notset", + logging.DEBUG: "debug", + logging.INFO: "info", + logging.WARN: "warning", # WARN is same a WARNING + logging.WARNING: "warning", + logging.ERROR: "error", + logging.FATAL: "fatal", + logging.CRITICAL: "fatal", # CRITICAL is same as FATAL +} + +# Map logging level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + logging.CRITICAL: 21, # fatal + logging.ERROR: 17, # error + logging.WARNING: 13, # warn + logging.INFO: 9, # info + logging.DEBUG: 5, # debug +} + + +# Capturing events from those loggers causes recursion errors. We cannot allow +# the user to unconditionally create events from those loggers under any +# circumstances. +# +# Note: Ignoring by logger name here is better than mucking with thread-locals. +# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. +# +# Events/breadcrumbs and Sentry Logs have separate ignore lists so that +# framework loggers silenced for events (e.g. django.server) can still be +# captured as Sentry Logs. +_IGNORED_LOGGERS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + +_IGNORED_LOGGERS_SENTRY_LOGS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + + +def ignore_logger( + name: str, +) -> None: + """This disables recording (both in breadcrumbs and as events) calls to + a logger of a specific name. Among other uses, many of our integrations + use this to prevent their actions being recorded as breadcrumbs. Exposed + to users as a way to quiet spammy loggers. + + This does **not** affect Sentry Logs — use + :py:func:`ignore_logger_for_sentry_logs` for that. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS.add(name) + + +def ignore_logger_for_sentry_logs( + name: str, +) -> None: + """This disables recording as Sentry Logs calls to a logger of a + specific name. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS_SENTRY_LOGS.add(name) + + +def unignore_logger( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger` call, re-enabling + recording of breadcrumbs and events for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS.discard(name) + + +def unignore_logger_for_sentry_logs( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, + re-enabling recording of Sentry Logs for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) + + +class LoggingIntegration(Integration): + identifier = "logging" + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + self._handler = None + self._breadcrumb_handler = None + self._sentry_logs_handler = None + + if level is not None: + self._breadcrumb_handler = BreadcrumbHandler(level=level) + + if sentry_logs_level is not None: + self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) + + if event_level is not None: + self._handler = EventHandler(level=event_level) + + def _handle_record(self, record: "LogRecord") -> None: + if self._handler is not None and record.levelno >= self._handler.level: + self._handler.handle(record) + + if ( + self._breadcrumb_handler is not None + and record.levelno >= self._breadcrumb_handler.level + ): + self._breadcrumb_handler.handle(record) + + def _handle_sentry_logs_record(self, record: "LogRecord") -> None: + if ( + self._sentry_logs_handler is not None + and record.levelno >= self._sentry_logs_handler.level + ): + self._sentry_logs_handler.handle(record) + + @staticmethod + def setup_once() -> None: + old_callhandlers = logging.Logger.callHandlers + + def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": + # keeping a local reference because the + # global might be discarded on shutdown + ignored_loggers = _IGNORED_LOGGERS + ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS + + try: + return old_callhandlers(self, record) + finally: + # This check is done twice, once also here before we even get + # the integration. Otherwise we have a high chance of getting + # into a recursion error when the integration is resolved + # (this also is slower). + name = record.name.strip() + + handle_events = ( + ignored_loggers is not None and name not in ignored_loggers + ) + handle_sentry_logs = ( + ignored_loggers_sentry_logs is not None + and name not in ignored_loggers_sentry_logs + ) + + if handle_events or handle_sentry_logs: + integration = sentry_sdk.get_client().get_integration( + LoggingIntegration + ) + if integration is not None: + if handle_events: + integration._handle_record(record) + if handle_sentry_logs: + integration._handle_sentry_logs_record(record) + + logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore + + +class _BaseHandler(logging.Handler): + COMMON_RECORD_ATTRS = frozenset( + ( + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "linenno", + "lineno", + "message", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack", + "tags", + "taskName", + "thread", + "threadName", + "stack_info", + ) + ) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + return LOGGING_TO_EVENT_LEVEL.get( + record.levelno, record.levelname.lower() if record.levelname else "" + ) + + def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": + return { + k: v + for k, v in vars(record).items() + if k not in self.COMMON_RECORD_ATTRS + and (not isinstance(k, str) or not k.startswith("_")) + } + + +class EventHandler(_BaseHandler): + """ + A logging handler that emits Sentry events for each log record + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + client_options = client.options + + # exc_info might be None or (None, None, None) + # + # exc_info may also be any falsy value due to Python stdlib being + # liberal with what it receives and Celery's billiard being "liberal" + # with what it sends. See + # https://github.com/getsentry/sentry-python/issues/904 + if record.exc_info and record.exc_info[0] is not None: + event, hint = event_from_exception( + record.exc_info, + client_options=client_options, + mechanism={"type": "logging", "handled": True}, + ) + elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: + event = {} + hint = {} + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=client_options[ + "include_local_variables" + ], + max_value_length=client_options["max_value_length"], + ), + "crashed": False, + "current": True, + } + ] + } + else: + event = {} + hint = {} + + hint["log_record"] = record + + level = self._logging_to_event_level(record) + if level in {"debug", "info", "warning", "error", "critical", "fatal"}: + event["level"] = level # type: ignore[typeddict-item] + event["logger"] = record.name + + if ( + sys.version_info < (3, 11) + and record.name == "py.warnings" + and record.msg == "%s" + ): + # warnings module on Python 3.10 and below sets record.msg to "%s" + # and record.args[0] to the actual warning message. + # This was fixed in https://github.com/python/cpython/pull/30975. + message = record.args[0] + params = () + else: + message = record.msg + params = record.args + + event["logentry"] = { + "message": to_string(message), + "formatted": record.getMessage(), + "params": params, + } + + event["extra"] = self._extra_from_record(record) + + sentry_sdk.capture_event(event, hint=hint) + + +# Legacy name +SentryHandler = EventHandler + + +class BreadcrumbHandler(_BaseHandler): + """ + A logging handler that records breadcrumbs for each log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + sentry_sdk.add_breadcrumb( + self._breadcrumb_from_record(record), hint={"log_record": record} + ) + + def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": + return { + "type": "log", + "level": self._logging_to_event_level(record), + "category": record.name, + "message": record.message, + "timestamp": datetime.fromtimestamp(record.created, timezone.utc), + "data": self._extra_from_record(record), + } + + +class SentryLogsHandler(_BaseHandler): + """ + A logging handler that records Sentry logs for each Python log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS_SENTRY_LOGS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + self._capture_log_from_record(client, record) + + def _capture_log_from_record( + self, client: "BaseClient", record: "LogRecord" + ) -> None: + otel_severity_number, otel_severity_text = _log_level_to_otel( + record.levelno, SEVERITY_TO_OTEL_SEVERITY + ) + project_root = client.options["project_root"] + + attrs: "Any" = self._extra_from_record(record) + attrs["sentry.origin"] = "auto.log.stdlib" + + parameters_set = False + if record.args is not None: + if isinstance(record.args, tuple): + parameters_set = bool(record.args) + for i, arg in enumerate(record.args): + attrs[f"sentry.message.parameter.{i}"] = ( + arg + if isinstance(arg, (str, float, int, bool)) + else safe_repr(arg) + ) + elif isinstance(record.args, dict): + parameters_set = bool(record.args) + for key, value in record.args.items(): + attrs[f"sentry.message.parameter.{key}"] = ( + value + if isinstance(value, (str, float, int, bool)) + else safe_repr(value) + ) + + if parameters_set and isinstance(record.msg, str): + # only include template if there is at least one + # sentry.message.parameter.X set + attrs["sentry.message.template"] = record.msg + + if record.lineno: + attrs["code.line.number"] = record.lineno + + if record.pathname: + if project_root is not None and record.pathname.startswith(project_root): + attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record.pathname + + if record.funcName: + attrs["code.function.name"] = record.funcName + + if record.thread: + attrs["thread.id"] = record.thread + if record.threadName: + attrs["thread.name"] = record.threadName + + if record.process: + attrs["process.pid"] = record.process + if record.processName: + attrs["process.executable.name"] = record.processName + if record.name: + attrs["logger.name"] = record.name + + # noinspection PyProtectedMember + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record.message, + "attributes": attrs, + "time_unix_nano": int(record.created * 1e9), + "trace_id": None, + "span_id": None, + }, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py new file mode 100644 index 0000000000..dbb724d9a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py @@ -0,0 +1,208 @@ +import enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.logging import ( + BreadcrumbHandler, + EventHandler, + _BaseHandler, +) +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import has_logs_enabled, safe_repr + +if TYPE_CHECKING: + from logging import LogRecord + from typing import Any, Optional + +try: + import loguru + from loguru import logger + from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT + + if TYPE_CHECKING: + from loguru import Message +except ImportError: + raise DidNotEnable("LOGURU is not installed") + + +class LoggingLevels(enum.IntEnum): + TRACE = 5 + DEBUG = 10 + INFO = 20 + SUCCESS = 25 + WARNING = 30 + ERROR = 40 + CRITICAL = 50 + + +DEFAULT_LEVEL = LoggingLevels.INFO.value +DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value + + +SENTRY_LEVEL_FROM_LOGURU_LEVEL = { + "TRACE": "DEBUG", + "DEBUG": "DEBUG", + "INFO": "INFO", + "SUCCESS": "INFO", + "WARNING": "WARNING", + "ERROR": "ERROR", + "CRITICAL": "CRITICAL", +} + +# Map Loguru level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + LoggingLevels.CRITICAL: 21, # fatal + LoggingLevels.ERROR: 17, # error + LoggingLevels.WARNING: 13, # warn + LoggingLevels.SUCCESS: 11, # info + LoggingLevels.INFO: 9, # info + LoggingLevels.DEBUG: 5, # debug + LoggingLevels.TRACE: 1, # trace +} + + +class LoguruIntegration(Integration): + identifier = "loguru" + + level: "Optional[int]" = DEFAULT_LEVEL + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL + breadcrumb_format = DEFAULT_FORMAT + event_format = DEFAULT_FORMAT + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + LoguruIntegration.level = level + LoguruIntegration.event_level = event_level + LoguruIntegration.breadcrumb_format = breadcrumb_format + LoguruIntegration.event_format = event_format + LoguruIntegration.sentry_logs_level = sentry_logs_level + + @staticmethod + def setup_once() -> None: + if LoguruIntegration.level is not None: + logger.add( + LoguruBreadcrumbHandler(level=LoguruIntegration.level), + level=LoguruIntegration.level, + format=LoguruIntegration.breadcrumb_format, + ) + + if LoguruIntegration.event_level is not None: + logger.add( + LoguruEventHandler(level=LoguruIntegration.event_level), + level=LoguruIntegration.event_level, + format=LoguruIntegration.event_format, + ) + + if LoguruIntegration.sentry_logs_level is not None: + logger.add( + loguru_sentry_logs_handler, + level=LoguruIntegration.sentry_logs_level, + ) + + +class _LoguruBaseHandler(_BaseHandler): + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + if kwargs.get("level"): + kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( + kwargs.get("level", ""), DEFAULT_LEVEL + ) + + super().__init__(*args, **kwargs) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + try: + return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ + LoggingLevels(record.levelno).name + ].lower() + except (ValueError, KeyError): + return record.levelname.lower() if record.levelname else "" + + +class LoguruEventHandler(_LoguruBaseHandler, EventHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" + + pass + + +class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" + + pass + + +def loguru_sentry_logs_handler(message: "Message") -> None: + # This is intentionally a callable sink instead of a standard logging handler + # since otherwise we wouldn't get direct access to message.record + client = sentry_sdk.get_client() + + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + record = message.record + + if ( + LoguruIntegration.sentry_logs_level is None + or record["level"].no < LoguruIntegration.sentry_logs_level + ): + return + + otel_severity_number, otel_severity_text = _log_level_to_otel( + record["level"].no, SEVERITY_TO_OTEL_SEVERITY + ) + + attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} + + project_root = client.options["project_root"] + if record.get("file"): + if project_root is not None and record["file"].path.startswith(project_root): + attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record["file"].path + + if record.get("line") is not None: + attrs["code.line.number"] = record["line"] + + if record.get("function"): + attrs["code.function.name"] = record["function"] + + if record.get("thread"): + attrs["thread.name"] = record["thread"].name + attrs["thread.id"] = record["thread"].id + + if record.get("process"): + attrs["process.pid"] = record["process"].id + attrs["process.executable.name"] = record["process"].name + + if record.get("name"): + attrs["logger.name"] = record["name"] + + extra = record.get("extra") + if isinstance(extra, dict): + for key, value in extra.items(): + if isinstance(value, (str, int, float, bool)): + attrs[f"sentry.message.parameter.{key}"] = value + else: + attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record["message"], + "attributes": attrs, + "time_unix_nano": int(record["time"].timestamp() * 1e9), + "trace_id": None, + "span_id": None, + } + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py new file mode 100644 index 0000000000..79381fe06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py @@ -0,0 +1,876 @@ +""" +Sentry integration for MCP (Model Context Protocol) servers. + +This integration instruments MCP servers to create spans for tool, prompt, +and resource handler execution, and captures errors that occur during execution. + +Supports the low-level `mcp.server.lowlevel.Server` API. +""" + +import inspect +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import package_version, safe_serialize + +MCP_PACKAGE_VERSION = package_version("mcp") + +try: + from mcp.server.lowlevel import Server + from mcp.server.streamable_http import ( + StreamableHTTPServerTransport, + ) + + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] + request_ctx, + ) +except ImportError: + raise DidNotEnable("MCP SDK not installed") + +try: + from fastmcp import FastMCP # type: ignore[import-not-found] +except ImportError: + FastMCP = None + +if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + try: + from mcp.server.context import ( + ServerRequestContext, + ) + except ImportError: + ServerRequestContext = None # type: ignore[assignment,misc] +else: + ServerRequestContext = None # type: ignore[assignment,misc] + + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Optional, Tuple, Union + + from starlette.types import Receive, Scope, Send + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +class MCPIntegration(Integration): + identifier = "mcp" + origin = "auto.ai.mcp" + + def __init__(self, include_prompts: bool = True) -> None: + """ + Initialize the MCP integration. + + Args: + include_prompts: Whether to include prompts (tool results and prompt content) + in span data. Requires send_default_pii=True. Default is True. + """ + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """ + Patches MCP server classes to instrument handler execution. + """ + _patch_lowlevel_server() + _patch_handle_request() + + if FastMCP is not None: + _patch_fastmcp() + + +def _get_active_http_scopes( + ctx: "Optional[Any]" = None, +) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return None + + if ( + ctx is None + or not hasattr(ctx, "request") + or ctx.request is None + or "state" not in ctx.request.scope + ): + return None + + return ( + ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), + ctx.request.scope["state"].get("sentry_sdk.current_scope"), + ) + + +def _get_request_context_data( + ctx: "Optional[Any]" = None, +) -> "tuple[Optional[str], Optional[str], str]": + """ + Extract request ID, session ID, and MCP transport type from the request context. + + Returns: + Tuple of (request_id, session_id, mcp_transport). + - request_id: May be None if not available + - session_id: May be None if not available + - mcp_transport: "http", "sse", "stdio" + """ + request_id: "Optional[str]" = None + session_id: "Optional[str]" = None + mcp_transport: str = "stdio" + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return request_id, session_id, mcp_transport + + if ctx is not None: + request_id = ctx.request_id + if hasattr(ctx, "request") and ctx.request is not None: + request = ctx.request + # Detect transport type by checking request characteristics + if hasattr(request, "query_params") and request.query_params.get( + "session_id" + ): + # SSE transport uses query parameter + mcp_transport = "sse" + session_id = request.query_params.get("session_id") + elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): + # StreamableHTTP transport uses header + mcp_transport = "http" + session_id = request.headers.get("mcp-session-id") + + return request_id, session_id, mcp_transport + + +def _get_span_config( + handler_type: str, item_name: str +) -> "tuple[str, str, str, Optional[str]]": + """ + Get span configuration based on handler type. + + Returns: + Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) + Note: result_data_key is None for resources + """ + if handler_type == "tool": + span_data_key = SPANDATA.MCP_TOOL_NAME + mcp_method_name = "tools/call" + result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT + elif handler_type == "prompt": + span_data_key = SPANDATA.MCP_PROMPT_NAME + mcp_method_name = "prompts/get" + result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT + else: # resource + span_data_key = SPANDATA.MCP_RESOURCE_URI + mcp_method_name = "resources/read" + result_data_key = None # Resources don't capture result content + + span_name = f"{mcp_method_name} {item_name}" + return span_data_key, span_name, mcp_method_name, result_data_key + + +def _set_span_input_data( + span: "Union[StreamedSpan, Span]", + handler_name: str, + span_data_key: str, + mcp_method_name: str, + arguments: "dict[str, Any]", + request_id: "Optional[str]", + session_id: "Optional[str]", + mcp_transport: str, +) -> None: + """Set input span data for MCP handlers.""" + + # Set handler identifier + _set_span_data_attribute(span, span_data_key, handler_name) + _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) + + # Set transport/MCP transport type + _set_span_data_attribute( + span, + SPANDATA.NETWORK_TRANSPORT, + "pipe" if mcp_transport == "stdio" else "tcp", + ) + _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) + + # Set request_id if provided + if request_id: + _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) + + # Set session_id if provided + if session_id: + _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) + + # Set request arguments (excluding common request context objects) + for k, v in arguments.items(): + _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) + + +def _extract_tool_result_content(result: "Any") -> "Any": + """ + Extract meaningful content from MCP tool result. + + Tool handlers can return: + - CallToolResult (mcp v2+): Has .content list and optional .structured_content + - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) + - dict (StructuredContent): Return as-is + - list/Iterable (UnstructuredContent): Extract text from content blocks + """ + if result is None: + return None + + # Handle v2 CallToolResult-like objects (has .content list attribute) + if hasattr(result, "content") and isinstance( + getattr(result, "content", None), list + ): + # This is only present when a tool declares an output_schema + structured = getattr(result, "structured_content", None) + if structured is not None: + return structured + return _extract_text_from_content_blocks(result.content) + + # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) + if isinstance(result, tuple) and len(result) == 2: + # Return the structured content (2nd element) + return result[1] + + # Handle StructuredContent: dict + if isinstance(result, dict): + return result + + # Handle UnstructuredContent: iterable of ContentBlock objects + if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): + return _extract_text_from_content_blocks(result) + + return result + + +def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": + texts = [] + try: + for item in content_blocks: + if hasattr(item, "text"): + texts.append(item.text) + elif isinstance(item, dict) and "text" in item: + texts.append(item["text"]) + except Exception: + return content_blocks + return " ".join(texts) if texts else content_blocks + + +def _set_span_output_data( + span: "Union[StreamedSpan, Span]", + result: "Any", + result_data_key: "Optional[str]", + handler_type: str, +) -> None: + """Set output span data for MCP handlers.""" + if result is None: + return + + # Get integration to check PII settings + integration = sentry_sdk.get_client().get_integration(MCPIntegration) + if integration is None: + return + + # Check if we should include sensitive data + should_include_data = should_send_default_pii() and integration.include_prompts + + # For tools, extract the meaningful content + if handler_type == "tool": + extracted = _extract_tool_result_content(result) + if ( + extracted is not None + and should_include_data + and result_data_key is not None + ): + _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) + # Set content count if result is a dict + if isinstance(extracted, dict): + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) + ) + elif handler_type == "prompt": + # For prompts, count messages and set role/content only for single-message prompts + try: + messages: "Optional[list[str]]" = None + message_count = 0 + + # Check if result has messages attribute (GetPromptResult) + if hasattr(result, "messages") and result.messages: + messages = result.messages + message_count = len(messages) + # Also check if result is a dict with messages + elif isinstance(result, dict) and result.get("messages"): + messages = result["messages"] + message_count = len(messages) + + # Always set message count if we found messages + if message_count > 0: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count + ) + + # Only set role and content for single-message prompts if PII is allowed + if message_count == 1 and should_include_data and messages: + first_message = messages[0] + # Extract role + role = None + if hasattr(first_message, "role"): + role = first_message.role + elif isinstance(first_message, dict) and "role" in first_message: + role = first_message["role"] + + if role: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role + ) + + # Extract content text + content_text = None + if hasattr(first_message, "content"): + msg_content = first_message.content + # Content can be a TextContent object or similar + if hasattr(msg_content, "text"): + content_text = msg_content.text + elif isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + elif isinstance(first_message, dict) and "content" in first_message: + msg_content = first_message["content"] + if isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + + if content_text and result_data_key is not None: + _set_span_data_attribute(span, result_data_key, content_text) + except Exception: + # Silently ignore if we can't extract message info + pass + # Resources don't capture result content (result_data_key is None) + + +# Handler data preparation and wrapping + + +def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: + """Check if original_args contains a v2 ServerRequestContext as the first element.""" + return ( + ServerRequestContext is not None + and bool(original_args) + and isinstance(original_args[0], ServerRequestContext) + ) + + +def _extract_handler_data_from_params( + handler_type: str, + params: "Any", +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from a v2 typed params object. + + In MCP SDK v2, handlers receive (ctx, params) where params is a typed + Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). + """ + if handler_type == "tool": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + elif handler_type == "prompt": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + arguments = {"name": handler_name, **arguments} + else: # resource + handler_name = str(getattr(params, "uri", "unknown")) + arguments = {} + + return handler_name, arguments + + +def _extract_handler_data_from_args( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from v1 positional args. + + In MCP SDK v1, handlers receive positional args: + - Tool: (tool_name, arguments) + - Prompt: (name, arguments) + - Resource: (uri,) + """ + original_kwargs = original_kwargs or {} + + if handler_type == "tool": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + elif handler_type == "prompt": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + arguments = {"name": handler_name, **(arguments or {})} + + else: # resource + handler_name = "unknown" + if original_args: + handler_name = str(original_args[0]) + elif original_kwargs.get("uri"): + handler_name = str(original_kwargs["uri"]) + + arguments = {} + + return handler_name, arguments + + +def _prepare_handler_data( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + params: "Optional[Any]" = None, +) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": + """ + Prepare common handler data for both v1 and v2 MCP SDK. + + Args: + handler_type: "tool", "prompt", or "resource" + original_args: Original positional args (v1 path) + original_kwargs: Original keyword args (v1 path) + params: Typed params object from v2 ServerRequestContext path + + Returns: + Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) + """ + if params is not None: + handler_name, arguments = _extract_handler_data_from_params( + handler_type, params + ) + elif _is_v2_context(original_args): + handler_name = "unknown" + arguments = {} + else: + handler_name, arguments = _extract_handler_data_from_args( + handler_type, original_args, original_kwargs + ) + + span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( + handler_type, handler_name + ) + + return ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) + + +async def _handler_wrapper( + handler_type: str, + func: "Callable[..., Any]", + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + self: "Optional[Any]" = None, + force_await: bool = True, +) -> "Any": + """ + Wrapper for MCP handlers. + + Args: + handler_type: "tool", "prompt", or "resource" + func: The handler function to wrap + original_args: Original arguments passed to the handler + original_kwargs: Original keyword arguments passed to the handler + self: Optional instance for bound methods + """ + if original_kwargs is None: + original_kwargs = {} + + # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers + ctx: "Optional[Any]" = None + params: "Optional[Any]" = None + if ( + ServerRequestContext is not None + and original_args + and isinstance(original_args[0], ServerRequestContext) + ): + ctx = original_args[0] + params = original_args[1] if len(original_args) > 1 else None + + ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) = _prepare_handler_data( + handler_type, original_args, original_kwargs, params=params + ) + + scopes = _get_active_http_scopes(ctx=ctx) + + isolation_scope_context: "ContextManager[Any]" + current_scope_context: "ContextManager[Any]" + + if scopes is None: + isolation_scope_context = nullcontext() + current_scope_context = nullcontext() + else: + isolation_scope, current_scope = scopes + + isolation_scope_context = ( + nullcontext() + if isolation_scope is None + else sentry_sdk.scope.use_isolation_scope(isolation_scope) + ) + current_scope_context = ( + nullcontext() + if current_scope is None + else sentry_sdk.scope.use_scope(current_scope) + ) + + # Get request ID, session ID, and transport from context + request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + + # Start span and execute + with isolation_scope_context: + with current_scope_context: + span_mgr: "Union[Span, StreamedSpan]" + if span_streaming: + span_mgr = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.MCP_SERVER, + "sentry.origin": MCPIntegration.origin, + }, + ) + else: + span_mgr = get_start_span_function()( + op=OP.MCP_SERVER, + name=span_name, + origin=MCPIntegration.origin, + ) + + with span_mgr as span: + # Set input span data + _set_span_input_data( + span, + handler_name, + span_data_key, + mcp_method_name, + arguments, + request_id, + session_id, + mcp_transport, + ) + + # For resources, extract and set protocol + if handler_type == "resource": + uri = None + if params is not None: + uri = getattr(params, "uri", None) + + # v1 scenario + if ServerRequestContext is None: + if original_args: + uri = original_args[0] + else: + uri = original_kwargs.get("uri") + + protocol = None + if uri is not None and hasattr(uri, "scheme"): + protocol = uri.scheme + elif handler_name and "://" in handler_name: + protocol = handler_name.split("://")[0] + if protocol: + _set_span_data_attribute( + span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol + ) + + try: + # Execute the async handler + if self is not None: + original_args = (self, *original_args) + + result = func(*original_args, **original_kwargs) + if force_await or inspect.isawaitable(result): + result = await result + + except Exception as e: + # Set error flag for tools + if handler_type == "tool": + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True + ) + sentry_sdk.capture_exception(e) + raise + + _set_span_output_data(span, result, result_data_key, handler_type) + + return result + + +def _create_instrumented_decorator( + original_decorator: "Callable[..., Any]", + handler_type: str, + *decorator_args: "Any", + **decorator_kwargs: "Any", +) -> "Callable[..., Any]": + """ + Create an instrumented version of an MCP decorator. + + This function intercepts MCP decorators (like @server.call_tool()) and injects + Sentry instrumentation into the handler registration flow. The returned decorator + will: + 1. Receive the user's handler function + 2. Pass the instrumented version to the original MCP decorator + + This ensures that when the handler is called at runtime, it's already wrapped + with Sentry spans and metrics collection. + + Args: + original_decorator: The original MCP decorator method (e.g., Server.call_tool) + handler_type: "tool", "prompt", or "resource" - determines span configuration + decorator_args: Positional arguments to pass to the original decorator (e.g., self) + decorator_kwargs: Keyword arguments to pass to the original decorator + + Returns: + A decorator function that instruments handlers before registering them + """ + + def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(func) + async def wrapper(*args: "Any") -> "Any": + return await _handler_wrapper(handler_type, func, args, force_await=False) + + # Then register it with the original MCP decorator + return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) + + return instrumented_decorator + + +_METHOD_TO_HANDLER_TYPE = { + "tools/call": "tool", + "prompts/get": "prompt", + "resources/read": "resource", +} + +# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via +# the Server(...) constructor kwargs rather than add_request_handler. The in-tree +# high-level MCPServer also wires its handlers through these kwargs. +_KWARG_TO_HANDLER_TYPE = { + "on_call_tool": "tool", + "on_get_prompt": "prompt", + "on_read_resource": "resource", +} + + +def _patch_lowlevel_server() -> None: + """ + Patches the mcp.server.lowlevel.Server class to instrument handler execution. + """ + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + _patch_lowlevel_server_v2() + else: + _patch_lowlevel_server_v1() + + +def _patch_lowlevel_server_v1() -> None: + """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" + # Patch call_tool decorator + original_call_tool = Server.call_tool # type: ignore[attr-defined] + + def patched_call_tool( + self: "Server", **kwargs: "Any" + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.call_tool that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_call_tool, "tool", self, **kwargs + )(func) + + Server.call_tool = patched_call_tool # type: ignore[attr-defined] + + # Patch get_prompt decorator + original_get_prompt = Server.get_prompt # type: ignore[attr-defined] + + def patched_get_prompt( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.get_prompt that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_get_prompt, "prompt", self + )(func) + + Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] + + # Patch read_resource decorator + original_read_resource = Server.read_resource # type: ignore[attr-defined] + + def patched_read_resource( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.read_resource that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_read_resource, "resource", self + )(func) + + Server.read_resource = patched_read_resource # type: ignore[attr-defined] + + +def _wrap_v2_handler( + handler_type: str, handler: "Callable[..., Any]" +) -> "Callable[..., Any]": + """Wrap a v2 (ctx, params) handler with Sentry instrumentation. + + Idempotent: an already-wrapped handler is returned unchanged so handlers + registered through more than one path (e.g. MCPServer building a Server) + are not double-wrapped. + """ + if getattr(handler, "__sentry_mcp_wrapped__", False): + return handler + + @wraps(handler) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + return await _handler_wrapper( + handler_type, handler, args, kwargs, force_await=False + ) + + wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] + return wrapper + + +def _patch_lowlevel_server_v2() -> None: + """Patches the v2 Server to wrap tool/prompt/resource handlers. + + Handlers can be registered either via the Server(...) constructor kwargs + (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree + MCPServer and most lowlevel examples use — or via add_request_handler. + Both are patched. + """ + original_init = Server.__init__ + + @wraps(original_init) + def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: + for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): + handler = kwargs.get(kwarg) + if handler is not None: + kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) + original_init(self, *args, **kwargs) + + Server.__init__ = patched_init # type: ignore[method-assign] + + original_add_request_handler = Server.add_request_handler + + def patched_add_request_handler( + self: "Server", + method: str, + params_type: "Any", + handler: "Callable[..., Any]", + *args: "Any", + **kwargs: "Any", + ) -> None: + handler_type = _METHOD_TO_HANDLER_TYPE.get(method) + if handler_type is not None: + handler = _wrap_v2_handler(handler_type, handler) + + original_add_request_handler( + self, method, params_type, handler, *args, **kwargs + ) + + Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] + + +def _patch_handle_request() -> None: + original_handle_request = StreamableHTTPServerTransport.handle_request + + @wraps(original_handle_request) + async def patched_handle_request( + self: "StreamableHTTPServerTransport", + scope: "Scope", + receive: "Receive", + send: "Send", + ) -> None: + scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( + sentry_sdk.get_isolation_scope() + ) + scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() + await original_handle_request(self, scope, receive, send) + + StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] + + +def _patch_fastmcp() -> None: + """ + Patches the standalone fastmcp package's FastMCP class. + + The standalone fastmcp package (v2.14.0+) registers its own handlers for + prompts and resources directly, bypassing the Server decorators we patch. + This function patches the _get_prompt_mcp and _read_resource_mcp methods + to add instrumentation for those handlers. + """ + if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): + original_get_prompt_mcp = FastMCP._get_prompt_mcp + + @wraps(original_get_prompt_mcp) + async def patched_get_prompt_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "prompt", + original_get_prompt_mcp, + args, + kwargs, + self, + ) + + FastMCP._get_prompt_mcp = patched_get_prompt_mcp + + if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): + original_read_resource_mcp = FastMCP._read_resource_mcp + + @wraps(original_read_resource_mcp) + async def patched_read_resource_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "resource", + original_read_resource_mcp, + args, + kwargs, + self, + ) + + FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py new file mode 100644 index 0000000000..b6111492bb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import _get_installed_modules + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + + +class ModulesIntegration(Integration): + identifier = "modules" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Any") -> "Event": + if event.get("type") == "transaction": + return event + + if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: + return event + + event["modules"] = _get_installed_modules() + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py new file mode 100644 index 0000000000..186c665ed1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py @@ -0,0 +1,1530 @@ +import json +import sys +import time +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai._openai_completions_api import ( + _get_system_instructions as _get_system_instructions_completions, +) +from sentry_sdk.ai._openai_completions_api import ( + _get_text_items, + _transform_system_instructions, +) +from sentry_sdk.ai._openai_completions_api import ( + _is_system_instruction as _is_system_instruction_completions, +) +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions as _get_system_instructions_responses, +) +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction as _is_system_instruction_responses, +) +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, + safe_serialize, +) + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Iterable, + Iterator, + List, + Optional, + Union, + ) + + from openai import Omit + from openai.types import CompletionUsage + from openai.types.responses import ( + ResponseInputParam, + ResponseStreamEvent, + SequenceNotStr, + ) + from openai.types.responses.response_usage import ResponseUsage + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +try: + try: + from openai import NotGiven + except ImportError: + NotGiven = None + + try: + from openai import Omit + except ImportError: + Omit = None + + from openai import AsyncStream, Stream + from openai.resources import AsyncEmbeddings, Embeddings + from openai.resources.chat.completions import AsyncCompletions, Completions + + if TYPE_CHECKING: + from openai.types.chat import ( + ChatCompletionChunk, + ChatCompletionMessageParam, + ) +except ImportError: + raise DidNotEnable("OpenAI not installed") + +RESPONSES_API_ENABLED = True +try: + # responses API support was introduced in v1.66.0 + from openai.resources.responses import AsyncResponses, Responses + from openai.types.responses.response_completed_event import ResponseCompletedEvent +except ImportError: + RESPONSES_API_ENABLED = False + + +class OpenAIIntegration(Integration): + identifier = "openai" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "OpenAIIntegration", + include_prompts: bool = True, + tiktoken_encoding_name: "Optional[str]" = None, + ) -> None: + self.include_prompts = include_prompts + + self.tiktoken_encoding = None + if tiktoken_encoding_name is not None: + import tiktoken # type: ignore + + self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) + + @staticmethod + def setup_once() -> None: + Completions.create = _wrap_chat_completion_create(Completions.create) + AsyncCompletions.create = _wrap_async_chat_completion_create( + AsyncCompletions.create + ) + + Embeddings.create = _wrap_embeddings_create(Embeddings.create) + AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) + + if RESPONSES_API_ENABLED: + Responses.create = _wrap_responses_create(Responses.create) + AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) + + def count_tokens(self: "OpenAIIntegration", s: str) -> int: + if self.tiktoken_encoding is None: + return 0 + try: + return len(self.tiktoken_encoding.encode_ordinary(s)) + except Exception: + return 0 + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _has_attr_and_is_int( + token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str +) -> bool: + return hasattr(token_usage, attr_name) and isinstance( + getattr(token_usage, attr_name, None), int + ) + + +def _calculate_completions_token_usage( + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + streaming_message_total_token_usage: "Optional[CompletionUsage]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Chat Completions API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + usage = None + + if streaming_message_total_token_usage is not None: + usage = streaming_message_total_token_usage + elif hasattr(response, "usage"): + usage = response.usage + + if usage is not None: + if _has_attr_and_is_int(usage, "prompt_tokens"): + input_tokens = usage.prompt_tokens + if _has_attr_and_is_int(usage, "completion_tokens"): + output_tokens = usage.completion_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "prompt_tokens_details"): + cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "completion_tokens_details"): + reasoning = getattr( + usage.completion_tokens_details, "reasoning_tokens", None + ) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in messages or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "choices") and response.choices is not None: + for choice in response.choices: + if hasattr(choice, "message") and hasattr(choice.message, "content"): + output_tokens += count_tokens(choice.message.content) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _calculate_responses_token_usage( + input: "Any", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Responses API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + + if hasattr(response, "usage"): + usage = response.usage + + if _has_attr_and_is_int(usage, "input_tokens"): + input_tokens = usage.input_tokens + if _has_attr_and_is_int(usage, "output_tokens"): + output_tokens = usage.output_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "input_tokens_details"): + cached = getattr(usage.input_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "output_tokens_details"): + reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in input or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + # Deliberate use of Completions function for both Completions and Responses input format. + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "output"): + for output_item in response.output: + if hasattr(output_item, "content"): + for content_item in output_item.content: + if hasattr(content_item, "text"): + output_tokens += count_tokens(content_item.text) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _set_responses_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") + messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_output_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + conversation = kwargs.get("conversation") + if conversation is not None and _is_given(conversation): + conversation_id: "Optional[str]" = None + if isinstance(conversation, str): + conversation_id = conversation + elif isinstance(conversation, dict): + conversation_id = conversation.get("id") + if conversation_id is not None: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + if not should_send_default_pii() or not integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if ( + messages is None + and explicit_instructions is not None + and _is_given(explicit_instructions) + ): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + [ + { + "type": "text", + "content": explicit_instructions, + } + ] + ), + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if messages is None: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + instructions_text_parts: "list[TextPart]" = [] + if explicit_instructions is not None and _is_given(explicit_instructions): + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions_responses(messages) + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + non_system_messages = [ + message for message in messages if not _is_system_instruction_responses(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + + +def _set_completions_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( + "messages" + ) + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + presence_penalty = kwargs.get("presence_penalty") + if presence_penalty is not None and _is_given(presence_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) + + frequency_penalty = kwargs.get("frequency_penalty") + if frequency_penalty is not None and _is_given(frequency_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + messages = list(messages) + kwargs["messages"] = messages + + system_instructions = _get_system_instructions_completions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + non_system_messages = [ + message + for message in messages + if not _is_system_instruction_completions(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + +def _set_embeddings_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( + "input" + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + return + + if isinstance(messages, str): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + return + + messages = list(messages) + kwargs["input"] = messages + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + +def _set_common_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + input: "Any", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + if hasattr(response, "model"): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) + + # Chat Completions API + if hasattr(response, "choices") and response.choices is not None: + if should_send_default_pii() and integration.include_prompts: + response_text = [ + choice.message.model_dump() + for choice in response.choices + if choice.message is not None + ] + if len(response_text) > 0: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) + + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + # Responses API + elif hasattr(response, "output"): + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in response.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + # Embeddings API (fallback for responses with neither choices nor output) + else: + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_synchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_asynchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _set_completions_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + _set_common_output_data( + span, + response, + messages, + integration, + finish_span, + ) + + +def _wrap_synchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Stream[ChatCompletionChunk]", + old_iterator: "Iterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "Iterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "AsyncStream[ChatCompletionChunk]", + old_iterator: "AsyncIterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "AsyncIterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + async for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +def _wrap_synchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "Stream[ResponseStreamEvent]", + old_iterator: "Iterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "Iterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "AsyncStream[ResponseStreamEvent]", + old_iterator: "AsyncIterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "AsyncIterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft: "Optional[float]" = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + async for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _set_responses_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _set_embeddings_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + return _new_sync_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + return await _new_async_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +async def _new_async_embeddings_create( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +def _wrap_embeddings_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_embeddings_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_synchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_asynchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _wrap_responses_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_responses_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_responses_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_responses_create(f, *args, **kwargs) + + return _sentry_patched_responses_async + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different openai versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py new file mode 100644 index 0000000000..5895f53ad3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py @@ -0,0 +1,250 @@ +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .patches import ( + _create_run_streamed_wrapper, + _create_run_wrapper, + _execute_final_output, + _execute_handoffs, + _get_all_tools, + _get_model, + _patch_error_tracing, + _run_single_turn, + _run_single_turn_streamed, +) + +try: + # "agents" is too generic. If someone has an agents.py file in their project + # or another package that's importable via "agents", no ImportError would + # be thrown and the integration would enable itself even if openai-agents is + # not installed. That's why we're adding the second, more specific import + # after it, even if we don't use it. + import agents + from agents.run import AgentRunner + from agents.version import __version__ as OPENAI_AGENTS_VERSION + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +try: + # AgentRunner methods moved in v0.8 + # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 + from agents.run_internal import run_loop, turn_preparation, turn_resolution +except ImportError: + run_loop = None + turn_preparation = None + turn_resolution = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + from agents.run_internal.run_steps import SingleStepResult + + +def _patch_runner() -> None: + # Create the root span for one full agent run (including eventual handoffs) + # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around + # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. + agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run + ) + + # Patch streaming runner + agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run_streamed + ) + + +class OpenAIAgentsIntegration(Integration): + """ + NOTE: With version 0.8.0, the class methods below have been refactored to functions. + - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` + - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` + - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` + - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` + - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` + + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. + 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. + - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. + - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. + 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. + - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. + - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. + 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. + + Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. + Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. + An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. + Handoffs are a way to switch agent-wide configuration. + - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` + """ + + identifier = "openai_agents" + + @staticmethod + def setup_once() -> None: + _patch_error_tracing() + _patch_runner() + + library_version = parse_version(OPENAI_AGENTS_VERSION) + if library_version is not None and library_version >= ( + 0, + 8, + ): + if run_loop is not None: + + @wraps(run_loop.get_all_tools) + async def new_wrapped_get_all_tools( + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools( + run_loop.get_all_tools, agent, context_wrapper + ) + + agents.run.get_all_tools = new_wrapped_get_all_tools + + @wraps(run_loop.run_single_turn) + async def new_wrapped_run_single_turn( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn( + run_loop.run_single_turn, *args, **kwargs + ) + + agents.run.run_single_turn = new_wrapped_run_single_turn + + @wraps(run_loop.run_single_turn_streamed) + async def new_wrapped_run_single_turn_streamed( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + run_loop.run_single_turn_streamed, *args, **kwargs + ) + + agents.run.run_single_turn_streamed = ( + new_wrapped_run_single_turn_streamed + ) + + if turn_preparation is not None: + + @wraps(turn_preparation.get_model) + def new_wrapped_get_model( + agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(turn_preparation.get_model, agent, run_config) + + agents.run_internal.run_loop.get_model = new_wrapped_get_model + + if turn_resolution is not None: + original_execute_handoffs = turn_resolution.execute_handoffs + + @wraps(original_execute_handoffs) + async def new_wrapped_execute_handoffs( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs( + original_execute_handoffs, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_handoffs = ( + new_wrapped_execute_handoffs + ) + + original_execute_final_output = turn_resolution.execute_final_output + + @wraps(turn_resolution.execute_final_output) + async def new_wrapped_final_output( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_final_output = ( + new_wrapped_final_output + ) + + return + + original_get_all_tools = AgentRunner._get_all_tools + + @wraps(AgentRunner._get_all_tools.__func__) + async def old_wrapped_get_all_tools( + cls: "agents.Runner", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + + agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) + + original_get_model = AgentRunner._get_model + + @wraps(AgentRunner._get_model.__func__) + def old_wrapped_get_model( + cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(original_get_model, agent, run_config) + + agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) + + original_run_single_turn = AgentRunner._run_single_turn + + @wraps(AgentRunner._run_single_turn.__func__) + async def old_wrapped_run_single_turn( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn(original_run_single_turn, *args, **kwargs) + + agents.run.AgentRunner._run_single_turn = classmethod( + old_wrapped_run_single_turn + ) + + original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed + + @wraps(AgentRunner._run_single_turn_streamed.__func__) + async def old_wrapped_run_single_turn_streamed( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + original_run_single_turn_streamed, *args, **kwargs + ) + + agents.run.AgentRunner._run_single_turn_streamed = classmethod( + old_wrapped_run_single_turn_streamed + ) + + original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs + + @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) + async def old_wrapped_execute_handoffs( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) + + agents._run_impl.RunImpl.execute_handoffs = classmethod( + old_wrapped_execute_handoffs + ) + + original_execute_final_output = agents._run_impl.RunImpl.execute_final_output + + @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) + async def old_wrapped_final_output( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents._run_impl.RunImpl.execute_final_output = classmethod( + old_wrapped_final_output + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py new file mode 100644 index 0000000000..f5de978be0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py new file mode 100644 index 0000000000..85d48f2d41 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -0,0 +1,10 @@ +from .agent_run import ( + _execute_final_output, # noqa: F401 + _execute_handoffs, # noqa: F401 + _run_single_turn, # noqa: F401 + _run_single_turn_streamed, # noqa: F401 +) +from .error_tracing import _patch_error_tracing # noqa: F401 +from .models import _get_model # noqa: F401 +from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 +from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py new file mode 100644 index 0000000000..71883b2eef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -0,0 +1,332 @@ +import sys +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import ( + handoff_span, + invoke_agent_span, + update_invoke_agent_span, +) + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, Union + + from agents.run_internal.run_steps import SingleStepResult + + from sentry_sdk.tracing import Span + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: + """Check if there's an active agent span for this context""" + return getattr(context_wrapper, "_sentry_current_agent", None) is not None + + +def _get_current_agent( + context_wrapper: "agents.RunContextWrapper", +) -> "Optional[agents.Agent]": + """Get the current agent from context wrapper""" + return getattr(context_wrapper, "_sentry_current_agent", None) + + +def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: + """Close the workflow span for streaming executions if it exists.""" + if agent and hasattr(agent, "_sentry_workflow_span"): + workflow_span = agent._sentry_workflow_span + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + +def _maybe_start_agent_span( + context_wrapper: "agents.RunContextWrapper", + agent: "agents.Agent", + should_run_agent_start_hooks: bool, + span_kwargs: "dict[str, Any]", + is_streaming: bool = False, +) -> "Optional[Union[Span, StreamedSpan]]": + """ + Start an agent invocation span if conditions are met. + Handles ending any existing span for a different agent. + + Returns the new span if started, or the existing span if conditions aren't met. + """ + if not (should_run_agent_start_hooks and agent and context_wrapper): + return getattr(context_wrapper, "_sentry_agent_span", None) + + # End any existing span for a different agent + if _has_active_agent_span(context_wrapper): + current_agent = _get_current_agent(context_wrapper) + if current_agent and current_agent != agent: + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + # Store the agent on the context wrapper so we can access it later + context_wrapper._sentry_current_agent = agent + span = invoke_agent_span(context_wrapper, agent, span_kwargs) + context_wrapper._sentry_agent_span = span + agent._sentry_agent_span = span + + if not is_streaming: + return span + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + return span + + +async def _run_single_turn( + original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn that + - creates agent invocation spans if there is no already active agent invocation span. + - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. + """ + # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. + bindings = kwargs.get("bindings") + agent = ( + getattr(bindings, "public_agent", None) + if bindings is not None + else kwargs.get("agent") + ) + context_wrapper = kwargs.get("context_wrapper") + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) + + span = _maybe_start_agent_span( + context_wrapper, agent, should_run_agent_start_hooks, kwargs + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn(*args, **kwargs) + + try: + result = await original_run_single_turn(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + return result + + +async def _run_single_turn_streamed( + original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn_streamed that + - creates agent invocation spans for streaming if there is no already active agent invocation span. + - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. + + Note: Unlike _run_single_turn which uses keyword-only arguments (*,), + _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: + _run_single_turn_streamed( + streamed_result, # args[0] + bindings, # args[1] + hooks, # args[2] + context_wrapper, # args[3] + run_config, # args[4] + should_run_agent_start_hooks, # args[5] + tool_use_tracker, # args[6] + all_tools, # args[7] + server_conversation_tracker, # args[8] (optional) + ) + """ + streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") + # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. + agent_or_bindings = ( + args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) + ) + agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) + context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") + should_run_agent_start_hooks = bool( + args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) + ) + + span_kwargs: "dict[str, Any]" = {} + if streamed_result and hasattr(streamed_result, "input"): + span_kwargs["original_input"] = streamed_result.input + + span = _maybe_start_agent_span( + context_wrapper, + agent, + should_run_agent_start_hooks, + span_kwargs, + is_streaming=True, + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn_streamed(*args, **kwargs) + + try: + result = await original_run_single_turn_streamed(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + return result + + +async def _execute_handoffs( + original_execute_handoffs: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_handoffs that + - creates and manages handoff spans. + - ends the agent invocation span. + - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. + """ + + context_wrapper = kwargs.get("context_wrapper") + run_handoffs = kwargs.get("run_handoffs") + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + + # Create Sentry handoff span for the first handoff (agents library only processes the first one) + if run_handoffs: + first_handoff = run_handoffs[0] + handoff_agent_name = first_handoff.handoff.agent_name + handoff_span(context_wrapper, agent, handoff_agent_name) + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + # Call original method with all parameters + try: + return await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + # Call original method with all parameters + try: + result = await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result + + +async def _execute_final_output( + original_execute_final_output: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_final_output that + - ends the agent invocation span. + - ends the workflow span if the response is streamed. + """ + + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + context_wrapper = kwargs.get("context_wrapper") + final_output = kwargs.get("final_output") + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + try: + return await original_execute_final_output(*args, **kwargs) + finally: + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + + try: + result = await original_execute_final_output(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py new file mode 100644 index 0000000000..68dadb3101 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py @@ -0,0 +1,74 @@ +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANSTATUS +from sentry_sdk.traces import SpanStatus +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +def _patch_error_tracing() -> None: + """ + Patches agents error tracing function to inject our span error logic + when a tool execution fails. + + In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span + In older versions, it was at: agents._utils.attach_error_to_current_span + + This works even when the module or function doesn't exist. + """ + error_tracing_module = None + + # Try newer location first (agents.util._error_tracing) + try: + from agents.util import _error_tracing + + error_tracing_module = _error_tracing + except (ImportError, AttributeError): + pass + + # Try older location (agents._utils) + if error_tracing_module is None: + try: + import agents._utils + + error_tracing_module = agents._utils + except (ImportError, AttributeError): + # Module doesn't exist in either location, nothing to patch + return + + # Check if the function exists + if not hasattr(error_tracing_module, "attach_error_to_current_span"): + return + + original_attach_error = error_tracing_module.attach_error_to_current_span + + @wraps(original_attach_error) + def sentry_attach_error_to_current_span( + error: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Wraps agents' error attachment to also set Sentry span status to error. + This allows us to properly track tool execution errors even though + the agents library swallows exceptions. + """ + # Set the current Sentry span to errored + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + current_span = sentry_sdk.get_current_scope().streamed_span + if current_span is not None: + current_span.status = SpanStatus.ERROR + else: + current_span = sentry_sdk.get_current_span() + if current_span is not None: + current_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # Call the original function + return original_attach_error(error, *args, **kwargs) + + error_tracing_module.attach_error_to_current_span = ( + sentry_attach_error_to_current_span + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py new file mode 100644 index 0000000000..634c9fdca1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py @@ -0,0 +1,197 @@ +import copy +import time +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_sentry_baggage_to_headers, + should_propagate_trace, +) +from sentry_sdk.utils import logger + +from ..spans import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + from sentry_sdk.tracing import Span + +try: + import agents + from agents.tool import HostedMCPTool +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _set_response_model_on_agent_span( + agent: "agents.Agent", response_model: "Optional[str]" +) -> None: + """Set the response model on the agent's invoke_agent span if available.""" + if response_model: + agent_span = getattr(agent, "_sentry_agent_span", None) + if agent_span: + if isinstance(agent_span, StreamedSpan): + agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + else: + agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + +def _inject_trace_propagation_headers( + hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" +) -> None: + headers = hosted_tool.tool_config.get("headers") + if headers is None: + headers = {} + hosted_tool.tool_config["headers"] = headers + + mcp_url = hosted_tool.tool_config.get("server_url") + if not mcp_url: + return + + if should_propagate_trace(sentry_sdk.get_client(), mcp_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( + key=key, value=value, mcp_url=mcp_url + ) + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(headers, value) + else: + headers[key] = value + + +def _get_model( + original_get_model: "Callable[..., agents.Model]", + agent: "agents.Agent", + run_config: "agents.RunConfig", +) -> "agents.Model": + """ + Responsible for + - creating and managing AI client spans. + - adding trace propagation headers to tools with type HostedMCPTool. + - setting the response model on agent invocation spans. + """ + # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) + # because we only patch its direct methods, all underlying data can remain unchanged. + model = copy.copy(original_get_model(agent, run_config)) + + # Capture the request model name for spans (agent.model can be None when using defaults) + request_model_name = model.model if hasattr(model, "model") else str(model) + agent._sentry_request_model = request_model_name + + # Wrap _fetch_response if it exists (for OpenAI models) to capture response model + if hasattr(model, "_fetch_response"): + original_fetch_response = model._fetch_response + + @wraps(original_fetch_response) + async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": + response = await original_fetch_response(*args, **kwargs) + if hasattr(response, "model") and response.model: + agent._sentry_response_model = str(response.model) + return response + + model._fetch_response = wrapped_fetch_response + + original_get_response = model.get_response + + @wraps(original_get_response) + async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": + mcp_tools = kwargs.get("tools") + hosted_tools = [] + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + result = await original_get_response(*args, **kwargs) + + # Get response model captured from _fetch_response and clean up + response_model = getattr(agent, "_sentry_response_model", None) + if response_model: + delattr(agent, "_sentry_response_model") + + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, result, response_model, agent) + + return result + + model.get_response = wrapped_get_response + + # Also wrap stream_response for streaming support + if hasattr(model, "stream_response"): + original_stream_response = model.stream_response + + @wraps(original_stream_response) + async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": + span_kwargs = dict(kwargs) + if len(args) > 0: + span_kwargs["system_instructions"] = args[0] + if len(args) > 1: + span_kwargs["input"] = args[1] + + hosted_tools = [] + if len(args) > 3: + mcp_tools = args[3] + + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, span_kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + streaming_response = None + ttft_recorded = False + # Capture start time locally to avoid race conditions with concurrent requests + start_time = time.perf_counter() + + async for event in original_stream_response(*args, **kwargs): + # Detect first content token (text delta event) + if not ttft_recorded and hasattr(event, "delta"): + ttft = time.perf_counter() - start_time + set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) + ttft_recorded = True + + # Capture the full response from ResponseCompletedEvent + if hasattr(event, "response"): + streaming_response = event.response + yield event + + # Update span with response data (usage, output, model) + if streaming_response: + response_model = ( + str(streaming_response.model) + if hasattr(streaming_response, "model") + and streaming_response.model + else None + ) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span( + span, streaming_response, response_model, agent + ) + + model.stream_response = wrapped_stream_response + + return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py new file mode 100644 index 0000000000..5f9996595f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -0,0 +1,221 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import agent_workflow_span, update_invoke_agent_span +from ..utils import _capture_exception + +try: + from agents.exceptions import AgentsException +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, AsyncIterator, Callable + + +def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run methods to + - create and manage a root span for the agent workflow runs. + - end the agent invocation span if an `AgentsException` is raised in `run()`. + + Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), + so it does not need to be wrapped separately. + """ + + @wraps(original_func) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + with agent_workflow_span(agent) as workflow_span: + # Set conversation ID on workflow span early so it's captured even on errors + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + run_result = await original_func(*args, **kwargs) + except AgentsException as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + + context_wrapper = getattr(exc.run_data, "context_wrapper", None) + if context_wrapper is not None: + invoke_agent_span = getattr( + context_wrapper, "_sentry_agent_span", None + ) + + if invoke_agent_span is not None and ( + ( + isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.end_timestamp is None + ) + or ( + not isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.timestamp is None + ) + ): + update_invoke_agent_span( + span=invoke_agent_span, + context=context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Invoke agent span is not finished in this case. + # This is much less likely to occur than other cases because + # AgentRunner.run() is "just" a while loop around _run_single_turn. + _capture_exception(exc) + reraise(*exc_info) + + invoke_agent_span = getattr( + run_result.context_wrapper, "_sentry_agent_span", None + ) + if not invoke_agent_span: + return run_result + + update_invoke_agent_span( + span=invoke_agent_span, + context=run_result.context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(None, None, None) + delattr(run_result.context_wrapper, "_sentry_agent_span") + return run_result + + return wrapper + + +def _create_run_streamed_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run_streamed method to + - create a root span for streaming agent workflow runs. + - end the workflow span if and only if the response stream is consumed or cancelled. + + Unlike run(), run_streamed() returns immediately with a RunResultStreaming object + while execution continues in a background task. The workflow span must stay open + throughout the streaming operation and close when streaming completes or is abandoned. + + Note: We don't use isolation_scope() here because it uses context variables that + cannot span async boundaries (the __enter__ and __exit__ would be called from + different async contexts, causing ValueError). + """ + + @wraps(original_func) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + # Capture conversation_id from kwargs if provided + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + # Start workflow span immediately (before run_streamed returns) + workflow_span = agent_workflow_span(agent) + workflow_span.__enter__() + + # Set conversation ID on workflow span early so it's captured even on errors + if conversation_id: + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + # Store span on agent for cleanup + agent._sentry_workflow_span = workflow_span + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + # Call original function to get RunResultStreaming + run_result = original_func(*args, **kwargs) + except Exception as exc: + # If run_streamed itself fails (not the background task), clean up immediately + workflow_span.__exit__(*sys.exc_info()) + _capture_exception(exc) + raise + + def _close_workflow_span() -> None: + if hasattr(agent, "_sentry_workflow_span"): + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + if hasattr(run_result, "stream_events"): + original_stream_events = run_result.stream_events + + @wraps(original_stream_events) + async def wrapped_stream_events( + *stream_args: "Any", **stream_kwargs: "Any" + ) -> "AsyncIterator[Any]": + try: + async for event in original_stream_events( + *stream_args, **stream_kwargs + ): + yield event + finally: + _close_workflow_span() + + run_result.stream_events = wrapped_stream_events + + if hasattr(run_result, "cancel"): + original_cancel = run_result.cancel + + @wraps(original_cancel) + def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": + try: + return original_cancel(*cancel_args, **cancel_kwargs) + finally: + _close_workflow_span() + + run_result.cancel = wrapped_cancel + + return run_result + + return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py new file mode 100644 index 0000000000..bd13b9d61a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -0,0 +1,70 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import execute_tool_span, update_execute_tool_span + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +async def _get_all_tools( + original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", +) -> "list[agents.Tool]": + """ + Responsible for creating and managing `gen_ai.execute_tool` spans. + """ + # Get the original tools + tools = await original_get_all_tools(agent, context_wrapper) + + wrapped_tools = [] + for tool in tools: + # Wrap only the function tools (for now) + if tool.__class__.__name__ != "FunctionTool": + wrapped_tools.append(tool) + continue + + # Create a new FunctionTool with our wrapped invoke method + original_on_invoke = tool.on_invoke_tool + + def create_wrapped_invoke( + current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" + ) -> "Callable[..., Any]": + @wraps(current_on_invoke) + async def sentry_wrapped_on_invoke_tool( + *args: "Any", **kwargs: "Any" + ) -> "Any": + with execute_tool_span(current_tool, *args, **kwargs) as span: + # We can not capture exceptions in tool execution here because + # `_on_invoke_tool` is swallowing the exception here: + # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 + # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter + # I was unable to monkey patch it because those are evaluated at module import time + # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` + # because it is nested inside this import time code. As if they made it hard to patch on purpose... + result = await current_on_invoke(*args, **kwargs) + update_execute_tool_span(span, agent, current_tool, result) + + return result + + return sentry_wrapped_on_invoke_tool + + wrapped_tool = agents.FunctionTool( + name=tool.name, + description=tool.description, + params_json_schema=tool.params_json_schema, + on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), + strict_json_schema=tool.strict_json_schema, + is_enabled=tool.is_enabled, + ) + wrapped_tools.append(wrapped_tool) + + return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py new file mode 100644 index 0000000000..08802a87a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py @@ -0,0 +1,8 @@ +from .agent_workflow import agent_workflow_span # noqa: F401 +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .handoff import handoff_span # noqa: F401 +from .invoke_agent import ( + invoke_agent_span, # noqa: F401 + update_invoke_agent_span, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py new file mode 100644 index 0000000000..758f06db8d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + from typing import Union + + import agents + + +def agent_workflow_span( + agent: "agents.Agent", +) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": + # Create a transaction or a span if an transaction is already active + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} + ) + + return span + + span = get_start_span_function()( + name=f"{agent.name} workflow", + origin=SPAN_ORIGIN, + ) + + return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py new file mode 100644 index 0000000000..f4f02cb674 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_input_data, + _set_output_data, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from agents import Agent + + +def ai_client_span( + agent: "Agent", get_response_kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + # TODO-anton: implement other types of operations. Now "chat" is hardcoded. + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + _set_agent_data(span, agent) + _set_input_data(span, get_response_kwargs) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + response: "Any", + response_model: "Optional[str]" = None, + agent: "Optional[Agent]" = None, +) -> None: + """Update AI client span with response data (works for streaming and non-streaming).""" + if hasattr(response, "usage") and response.usage: + _set_usage_data(span, response.usage) + + if hasattr(response, "output") and response.output: + _set_output_data(span, response) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + elif hasattr(response, "model") and response.model: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) + + # Set conversation ID from agent if available + if agent: + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py new file mode 100644 index 0000000000..fd3a430951 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def execute_tool_span( + tool: "agents.Tool", *args: "Any", **kwargs: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool.name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool.name, + SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool.name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) + + set_on_span = span.set_data + + if should_send_default_pii(): + input = args[1] + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + agent: "agents.Agent", + tool: "agents.Tool", + result: "Any", +) -> None: + _set_agent_data(span, agent) + + if isinstance(result, str) and result.startswith( + "An error occurred while running the tool" + ): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if should_send_default_pii(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py new file mode 100644 index 0000000000..ea91464afb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + import agents + + +def handoff_span( + context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str +) -> None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"handoff from {from_agent.name} to {to_agent_name}", + attributes={ + "sentry.op": OP.GEN_AI_HANDOFF, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "handoff", + }, + ) as span: + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_HANDOFF, + name=f"handoff from {from_agent.name} to {to_agent_name}", + origin=SPAN_ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") + + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py new file mode 100644 index 0000000000..c21145ac4a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -0,0 +1,122 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _set_usage_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def invoke_agent_span( + context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {agent.name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + start_span_function = get_start_span_function() + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {agent.name}", + origin=SPAN_ORIGIN, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + if should_send_default_pii(): + messages = [] + if agent.instructions: + message = ( + agent.instructions + if isinstance(agent.instructions, str) + else safe_serialize(agent.instructions) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "system", + } + ) + + original_input = kwargs.get("original_input") + if original_input is not None: + message = ( + original_input + if isinstance(original_input, str) + else safe_serialize(original_input) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "user", + } + ) + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + _set_agent_data(span, agent) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + context: "agents.RunContextWrapper", + agent: "agents.Agent", + output: "Any" = None, +) -> None: + # Add aggregated usage data from context_wrapper + if hasattr(context, "usage"): + _set_usage_data(span, context.usage) + + if should_send_default_pii(): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py new file mode 100644 index 0000000000..224a5f66ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py @@ -0,0 +1,244 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai._openai_completions_api import _transform_system_instructions +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions, + _is_system_instruction, +) +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + normalize_message_role, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import should_truncate_gen_ai_input +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Union + + from agents import TResponseInputItem, Usage + + from sentry_sdk._types import TextPart + +try: + import agents + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai_agents", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span( + SPANDATA.GEN_AI_SYSTEM, "openai" + ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. + + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) + + if agent.model_settings.max_tokens: + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) + + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + if agent.model_settings.presence_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + agent.model_settings.presence_penalty, + ) + + if agent.model_settings.temperature: + set_on_span( + SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature + ) + + if agent.model_settings.top_p: + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) + + if agent.model_settings.frequency_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + agent.model_settings.frequency_penalty, + ) + + if len(agent.tools) > 0: + set_on_span( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + safe_serialize([vars(tool) for tool in agent.tools]), + ) + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage.input_tokens_details.cached_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage.output_tokens_details.reasoning_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) + + +def _set_input_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + get_response_kwargs: "dict[str, Any]", +) -> None: + if not should_send_default_pii(): + return + request_messages = [] + + messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) + + instructions_text_parts: "list[TextPart]" = [] + explicit_instructions = get_response_kwargs.get("system_instructions") + if explicit_instructions is not None: + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions(messages) + + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + non_system_messages = [ + message for message in messages if not _is_system_instruction(message) + ] + for message in non_system_messages: + if "role" in message: + normalized_role = normalize_message_role(message.get("role")) # type: ignore + content = message.get("content") # type: ignore + request_messages.append( + { + "role": normalized_role, + "content": ( + [{"type": "text", "text": content}] + if isinstance(content, str) + else content + ), + } + ) + else: + if message.get("type") == "function_call": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, + "content": [message], + } + ) + elif message.get("type") == "function_call_output": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, + "content": [message], + } + ) + + normalized_messages = normalize_message_roles(request_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + if not should_send_default_pii(): + return + + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in result.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py new file mode 100644 index 0000000000..281604fe38 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from openfeature import api + from openfeature.hook import Hook + + if TYPE_CHECKING: + from openfeature.hook import HookContext, HookHints +except ImportError: + raise DidNotEnable("OpenFeature is not installed") + + +class OpenFeatureIntegration(Integration): + identifier = "openfeature" + + @staticmethod + def setup_once() -> None: + # Register the hook within the global openfeature hooks list. + api.add_hooks(hooks=[OpenFeatureHook()]) + + +class OpenFeatureHook(Hook): + def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: + if isinstance(details.value, bool): + add_feature_flag(details.flag_key, details.value) + + def error( + self, hook_context: "HookContext", exception: Exception, hints: "HookHints" + ) -> None: + if isinstance(hook_context.default_value, bool): + add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py new file mode 100644 index 0000000000..d76b8058ee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py @@ -0,0 +1,7 @@ +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor + +__all__ = [ + "SentryPropagator", + "SentrySpanProcessor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py new file mode 100644 index 0000000000..d6733036ea --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py @@ -0,0 +1,9 @@ +from sentry_sdk.integrations import DidNotEnable + +try: + from opentelemetry.context import create_key +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +SENTRY_TRACE_KEY = create_key("sentry-trace") +SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py new file mode 100644 index 0000000000..472e8181f9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py @@ -0,0 +1,81 @@ +""" +IMPORTANT: The contents of this file are part of a proof of concept and as such +are experimental and not suitable for production use. They may be changed or +removed at any time without prior notice. +""" + +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + +try: + from opentelemetry import trace + from opentelemetry.propagate import set_global_textmap + from opentelemetry.sdk.trace import TracerProvider +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +try: + from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] + DjangoInstrumentor, + ) +except ImportError: + DjangoInstrumentor = None + + +CONFIGURABLE_INSTRUMENTATIONS = { + DjangoInstrumentor: {"is_sql_commentor_enabled": True}, +} + + +class OpenTelemetryIntegration(Integration): + identifier = "opentelemetry" + + @staticmethod + def setup_once() -> None: + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if has_span_streaming_enabled(options): + logger.warning( + "[OTel] OpenTelemetryIntegration is not compatible with span streaming " + "(trace_lifecycle='stream') and will be disabled." + ) + return + + warnings.warn( + "OpenTelemetryIntegration is deprecated. " + "Please use OTLPIntegration instead: " + "https://docs.sentry.io/platforms/python/integrations/otlp/", + DeprecationWarning, + stacklevel=2, + ) + + _setup_sentry_tracing() + # _setup_instrumentors() + + logger.debug("[OTel] Finished setting up OpenTelemetry integration") + + +def _setup_sentry_tracing() -> None: + provider = TracerProvider() + provider.add_span_processor(SentrySpanProcessor()) + trace.set_tracer_provider(provider) + set_global_textmap(SentryPropagator()) + + +def _setup_instrumentors() -> None: + for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): + if instrumentor is None: + continue + instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py new file mode 100644 index 0000000000..5b5f13861d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py @@ -0,0 +1,128 @@ +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.integrations.opentelemetry.span_processor import ( + SentrySpanProcessor, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data + +try: + from opentelemetry import trace + from opentelemetry.context import ( + Context, + get_current, + set_value, + ) + from opentelemetry.propagators.textmap import ( + CarrierT, + Getter, + Setter, + TextMapPropagator, + default_getter, + default_setter, + ) + from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional, Set + + +class SentryPropagator(TextMapPropagator): + """ + Propagates tracing headers for Sentry's tracing system in a way OTel understands. + """ + + def extract( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + getter: "Getter[CarrierT]" = default_getter, + ) -> "Context": + if context is None: + context = get_current() + + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + if not sentry_trace: + return context + + sentrytrace = extract_sentrytrace_data(sentry_trace[0]) + if not sentrytrace: + return context + + context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) + + trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] + + span_context = SpanContext( + trace_id=int(trace_id, 16), # type: ignore + span_id=int(span_id, 16), # type: ignore + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags=TraceFlags(TraceFlags.SAMPLED), + is_remote=True, + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + if baggage_header: + baggage = Baggage.from_incoming_header(baggage_header[0]) + else: + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + baggage = Baggage(sentry_items={}) + + baggage.freeze() + context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) + + span = NonRecordingSpan(span_context) + modified_context = trace.set_span_in_context(span, context) + return modified_context + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + if context is None: + context = get_current() + + current_span = trace.get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + span_id = trace.format_span_id(current_span_context.span_id) + + span_map = SentrySpanProcessor.otel_span_map + sentry_span = span_map.get(span_id, None) + if not sentry_span: + return + + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) + + if sentry_span.containing_transaction: + baggage = sentry_span.containing_transaction.get_baggage() + if baggage: + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + @property + def fields(self) -> "Set[str]": + return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py new file mode 100644 index 0000000000..1efd2ac455 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -0,0 +1,407 @@ +from datetime import datetime, timezone +from time import time +from typing import TYPE_CHECKING, cast + +from urllib3.util import parse_url as urlparse + +from sentry_sdk import get_client, start_transaction +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing import Transaction + +try: + from opentelemetry.context import get_value + from opentelemetry.sdk.trace import ReadableSpan as OTelSpan + from opentelemetry.sdk.trace import SpanProcessor + from opentelemetry.semconv.trace import SpanAttributes + from opentelemetry.trace import ( + SpanKind, + format_span_id, + format_trace_id, + get_current_span, + ) + from opentelemetry.trace.span import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from opentelemetry import context as context_api + from opentelemetry.trace import SpanContext + + from sentry_sdk._types import Event, Hint + +OPEN_TELEMETRY_CONTEXT = "otel" +SPAN_MAX_TIME_OPEN_MINUTES = 10 +SPAN_ORIGIN = "auto.otel" + + +def link_trace_context_to_error_event( + event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" +) -> "Event": + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return event + + if hasattr(event, "type") and event["type"] == "transaction": + return event + + otel_span = get_current_span() + if not otel_span: + return event + + ctx = otel_span.get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return event + + sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) + if not sentry_span: + return event + + contexts = event.setdefault("contexts", {}) + contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) + + return event + + +class SentrySpanProcessor(SpanProcessor): + """ + Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. + """ + + # The mapping from otel span ids to sentry spans + otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} + + # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES + open_spans: "dict[int, set[str]]" = {} + + initialized: "bool" = False + + def __new__(cls) -> "SentrySpanProcessor": + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + + # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). + return cls.instance # type: ignore[misc] + + def __init__(self) -> None: + if self.initialized: + return + + @add_global_event_processor + def global_event_processor(event: "Event", hint: "Hint") -> "Event": + return link_trace_context_to_error_event(event, self.otel_span_map) + + self.initialized = True + + def _prune_old_spans(self: "SentrySpanProcessor") -> None: + """ + Prune spans that have been open for too long. + """ + current_time_minutes = int(time() / 60) + for span_start_minutes in list( + self.open_spans.keys() + ): # making a list because we change the dict + # prune empty open spans buckets + if self.open_spans[span_start_minutes] == set(): + self.open_spans.pop(span_start_minutes) + + # prune old buckets + elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: + for span_id in self.open_spans.pop(span_start_minutes): + self.otel_span_map.pop(span_id, None) + + def on_start( + self, + otel_span: "OTelSpan", + parent_context: "Optional[context_api.Context]" = None, + ) -> None: + client = get_client() + + if not client.parsed_dsn: + return + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + if self._is_sentry_span(otel_span): + return + + trace_data = self._get_trace_data( + span_context, otel_span.parent, parent_context + ) + + parent_span_id = trace_data["parent_span_id"] + sentry_parent_span = ( + self.otel_span_map.get(parent_span_id) if parent_span_id else None + ) + + start_timestamp = None + if otel_span.start_time is not None: + start_timestamp = datetime.fromtimestamp( + otel_span.start_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span = None + if sentry_parent_span: + sentry_span = sentry_parent_span.start_child( + span_id=trace_data["span_id"], + name=otel_span.name, + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + else: + sentry_span = start_transaction( + name=otel_span.name, + span_id=trace_data["span_id"], + parent_span_id=parent_span_id, + trace_id=trace_data["trace_id"], + baggage=trace_data["baggage"], + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + + self.otel_span_map[trace_data["span_id"]] = sentry_span + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).add( + trace_data["span_id"] + ) + + self._prune_old_spans() + + def on_end(self, otel_span: "OTelSpan") -> None: + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + span_id = format_span_id(span_context.span_id) + sentry_span = self.otel_span_map.pop(span_id, None) + if not sentry_span: + return + + sentry_span.op = otel_span.name + + self._update_span_with_otel_status(sentry_span, otel_span) + + if isinstance(sentry_span, Transaction): + sentry_span.name = otel_span.name + sentry_span.set_context( + OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) + ) + self._update_transaction_with_otel_data(sentry_span, otel_span) + + else: + self._update_span_with_otel_data(sentry_span, otel_span) + + end_timestamp = None + if otel_span.end_time is not None: + end_timestamp = datetime.fromtimestamp( + otel_span.end_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span.finish(end_timestamp=end_timestamp) + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) + + self._prune_old_spans() + + def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: + """ + Break infinite loop: + HTTP requests to Sentry are caught by OTel and send again to Sentry. + """ + otel_span_url = None + if otel_span.attributes is not None: + otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) + otel_span_url = cast("Optional[str]", otel_span_url) + + parsed_dsn = get_client().parsed_dsn + dsn_url = parsed_dsn.netloc if parsed_dsn else None + + if otel_span_url and dsn_url and dsn_url in otel_span_url: + return True + + return False + + def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": + """ + Returns the OTel context for Sentry. + See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context + """ + ctx = {} + + if otel_span.attributes: + ctx["attributes"] = dict(otel_span.attributes) + + if otel_span.resource.attributes: + ctx["resource"] = dict(otel_span.resource.attributes) + + return ctx + + def _get_trace_data( + self, + span_context: "SpanContext", + parent_span_context: "Optional[SpanContext]", + parent_context: "Optional[context_api.Context]", + ) -> "dict[str, Any]": + """ + Extracts tracing information from one OTel span's context and its parent OTel context. + """ + trace_data: "dict[str, Any]" = {} + + span_id = format_span_id(span_context.span_id) + trace_data["span_id"] = span_id + + trace_id = format_trace_id(span_context.trace_id) + trace_data["trace_id"] = trace_id + + parent_span_id = ( + format_span_id(parent_span_context.span_id) if parent_span_context else None + ) + trace_data["parent_span_id"] = parent_span_id + + sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) + sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) + trace_data["parent_sampled"] = ( + sentry_trace_data["parent_sampled"] if sentry_trace_data else None + ) + + baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) + trace_data["baggage"] = baggage + + return trace_data + + def _update_span_with_otel_status( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Set the Sentry span status from the OTel span + """ + if otel_span.status.is_unset: + return + + if otel_span.status.is_ok: + sentry_span.set_status(SPANSTATUS.OK) + return + + sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + def _update_span_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Convert OTel span data and update the Sentry span with it. + This should eventually happen on the server when ingesting the spans. + """ + sentry_span.set_data("otel.kind", otel_span.kind) + + op = otel_span.name + description = otel_span.name + + if otel_span.attributes is not None: + for key, val in otel_span.attributes.items(): + sentry_span.set_data(key, val) + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + http_method = cast("Optional[str]", http_method) + + db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) + + if http_method: + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + description = http_method + + peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) + if peer_name: + description += " {}".format(peer_name) + + target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) + if target: + description += " {}".format(target) + + if not peer_name and not target: + url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) + url = cast("Optional[str]", url) + if url: + parsed_url = urlparse(url) + url = "{}://{}{}".format( + parsed_url.scheme, parsed_url.netloc, parsed_url.path + ) + description += " {}".format(url) + + status_code = otel_span.attributes.get( + SpanAttributes.HTTP_STATUS_CODE, None + ) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + elif db_query: + op = "db" + statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) + statement = cast("Optional[str]", statement) + if statement: + description = statement + + sentry_span.op = op + sentry_span.description = description + + def _update_transaction_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + if otel_span.attributes is None: + return + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + + if http_method: + status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py new file mode 100644 index 0000000000..b91f2cfd21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py @@ -0,0 +1,223 @@ +from sentry_sdk import capture_event, get_client +from sentry_sdk.consts import VERSION, EndpointType +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import register_external_propagation_context +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + event_from_exception, + logger, +) + +try: + from opentelemetry.context import ( + Context, + get_current, + get_value, + ) + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.propagate import set_global_textmap + from opentelemetry.propagators.textmap import ( + CarrierT, + Setter, + default_setter, + ) + from opentelemetry.sdk.trace import Span, TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + SpanContext, + format_span_id, + format_trace_id, + get_current_span, + get_tracer_provider, + set_tracer_provider, + ) + + from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY + from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +except ImportError: + raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Tuple + + +def otel_propagation_context() -> "Optional[Tuple[str, str]]": + """ + Get the (trace_id, span_id) from opentelemetry if exists. + """ + ctx = get_current_span().get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return None + + return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) + + +def setup_otlp_traces_exporter( + dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None +) -> None: + tracer_provider = get_tracer_provider() + + if not isinstance(tracer_provider, TracerProvider): + logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") + tracer_provider = TracerProvider() + set_tracer_provider(tracer_provider) + + endpoint = None + headers = None + if collector_url: + endpoint = collector_url + logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") + elif dsn: + auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") + endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) + headers = {"X-Sentry-Auth": auth.to_header()} + logger.debug(f"[OTLP] Sending traces to {endpoint}") + + otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + + +_sentry_patched_exception = False + + +def setup_capture_exceptions() -> None: + """ + Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. + """ + global _sentry_patched_exception + _original_record_exception = Span.record_exception + + if _sentry_patched_exception: + return + + def _sentry_patched_record_exception( + self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration and otlp_integration.capture_exceptions: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=get_client().options, + mechanism={"type": OTLPIntegration.identifier, "handled": False}, + ) + capture_event(event, hint=hint) + + _original_record_exception(self, exception, *args, **kwargs) + + Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] + _sentry_patched_exception = True + + +class SentryOTLPPropagator(SentryPropagator): + """ + We need to override the inject of the older propagator since that + is SpanProcessor based. + + !!! Note regarding baggage: + We cannot meaningfully populate a new baggage as a head SDK + when we are using OTLP since we don't have any sort of transaction semantic to + track state across a group of spans. + + For incoming baggage, we just pass it on as is so that case is correctly handled. + """ + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration is None: + return + + if context is None: + context = get_current() + + current_span = get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + sentry_trace = _to_traceparent(current_span_context) + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) + + baggage = get_value(SENTRY_BAGGAGE_KEY, context) + if baggage is not None and isinstance(baggage, Baggage): + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + +def _to_traceparent(span_context: "SpanContext") -> str: + """ + Helper method to generate the sentry-trace header. + """ + span_id = format_span_id(span_context.span_id) + trace_id = format_trace_id(span_context.trace_id) + sampled = span_context.trace_flags.sampled + + return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" + + +class OTLPIntegration(Integration): + """ + Automatically setup OTLP ingestion from the DSN. + + :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. + Set to False to setup the TracerProvider manually. + :param collector_url: URL of your own OpenTelemetry collector, defaults to None. + When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. + :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. + Set to False to configure propagators manually or to disable propagation. + :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. + Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. + """ + + identifier = "otlp" + + def __init__( + self, + setup_otlp_traces_exporter: bool = True, + collector_url: "Optional[str]" = None, + setup_propagator: bool = True, + capture_exceptions: bool = False, + ) -> None: + self.setup_otlp_traces_exporter = setup_otlp_traces_exporter + self.collector_url = collector_url + self.setup_propagator = setup_propagator + self.capture_exceptions = capture_exceptions + + @staticmethod + def setup_once() -> None: + logger.debug("[OTLP] Setting up trace linking for all events") + register_external_propagation_context(otel_propagation_context) + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if self.setup_otlp_traces_exporter: + logger.debug("[OTLP] Setting up OTLP exporter") + dsn: "Optional[str]" = options.get("dsn") if options else None + setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) + + if self.setup_propagator: + logger.debug("[OTLP] Setting up propagator for distributed tracing") + # TODO-neel better propagator support, chain with existing ones if possible instead of replacing + set_global_textmap(SentryOTLPPropagator()) + + setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py new file mode 100644 index 0000000000..569e3e1fa5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py @@ -0,0 +1,134 @@ +import ast +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import serializer +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, List, Optional, Tuple + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + +try: + from pure_eval import Evaluator +except ImportError: + raise DidNotEnable("pure_eval is not installed") + +try: + # Used implicitly, just testing it's available + import asttokens # noqa +except ImportError: + raise DidNotEnable("asttokens is not installed") + + +class PureEvalIntegration(Integration): + identifier = "pure_eval" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + sentry_frame["vars"] = ( + pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] + ) + return event + + +def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": + source = Source.for_frame(frame) + if not source.tree: + return {} + + statements = source.statements_at_line(frame.f_lineno) + if not statements: + return {} + + scope = stmt = list(statements)[0] + while True: + # Get the parent first in case the original statement is already + # a function definition, e.g. if we're calling a decorator + # In that case we still want the surrounding scope, not that function + scope = scope.parent + if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): + break + + evaluator = Evaluator.from_frame(frame) + expressions = evaluator.interesting_expressions_grouped(scope) + + def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": + # Prioritise expressions with a node closer to the statement executed + # without being after that statement + # A higher return value is better - the expression will appear + # earlier in the list of values and is less likely to be trimmed + nodes, _value = expression + + def start(n: "ast.expr") -> "Tuple[int, int]": + return (n.lineno, n.col_offset) + + nodes_before_stmt = [ + node for node in nodes if start(node) < stmt.last_token.end + ] + if nodes_before_stmt: + # The position of the last node before or in the statement + return max(start(node) for node in nodes_before_stmt) + else: + # The position of the first node after the statement + # Negative means it's always lower priority than nodes that come before + # Less negative means closer to the statement and higher priority + lineno, col_offset = min(start(node) for node in nodes) + return (-lineno, -col_offset) + + # This adds the first_token and last_token attributes to nodes + atok = source.asttokens() + + expressions.sort(key=closeness, reverse=True) + vars = { + atok.get_text(nodes[0]): value + for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] + } + return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py new file mode 100644 index 0000000000..81e7cf8090 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -0,0 +1,187 @@ +import functools + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import capture_internal_exceptions, parse_version + +try: + import pydantic_ai # type: ignore # noqa: F401 + from pydantic_ai import Agent +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +from importlib.metadata import PackageNotFoundError, version +from typing import TYPE_CHECKING + +from .patches import ( + _patch_agent_run, + _patch_graph_nodes, + _patch_tool_execution, +) +from .spans.ai_client import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any + + from pydantic_ai import ModelRequestContext, RunContext + from pydantic_ai.capabilities import Hooks # type: ignore + from pydantic_ai.messages import ModelResponse # type: ignore + + +def register_hooks(hooks: "Hooks") -> None: + """ + Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. + """ + + @hooks.on.before_model_request # type: ignore + async def on_request( + ctx: "RunContext[None]", request_context: "ModelRequestContext" + ) -> "ModelRequestContext": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return request_context + + span = ai_client_span( + messages=request_context.messages, + agent=None, + model=request_context.model, + model_settings=request_context.model_settings, + ) + + run_context_metadata["_sentry_span"] = span + span.__enter__() + + return request_context + + @hooks.on.after_model_request # type: ignore + async def on_response( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + response: "ModelResponse", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return response + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + return response + + update_ai_client_span(span, response) + span.__exit__(None, None, None) + + return response + + @hooks.on.model_request_error # type: ignore + async def on_error( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + error: "Exception", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + + if not isinstance(run_context_metadata, dict): + raise error + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + raise error + + with capture_internal_exceptions(): + span.__exit__(type(error), error, error.__traceback__) + + raise error + + original_init = Agent.__init__ + + @functools.wraps(original_init) + def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: + caps = list(kwargs.get("capabilities") or []) + caps.append(hooks) + kwargs["capabilities"] = caps + + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {} # Used as shared reference between hooks + + return original_init(self, *args, **kwargs) + + Agent.__init__ = patched_init + + +class PydanticAIIntegration(Integration): + """ + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. + 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. + - Each run invocation has `RunContext` objects that are passed to the library hooks. + 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + + Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. + Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). + The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. + + The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that + instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. + """ + + identifier = "pydantic_ai" + origin = f"auto.ai.{identifier}" + using_request_hooks = False + + def __init__( + self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True + ) -> None: + """ + Initialize the Pydantic AI integration. + + Args: + include_prompts: Whether to include prompts and messages in span data. + Requires send_default_pii=True. Defaults to True. + handled_tool_exceptions: Capture tool call exceptions that Pydantic AI + internally prevents from bubbling up. + """ + self.include_prompts = include_prompts + self.handled_tool_call_exceptions = handled_tool_call_exceptions + + @staticmethod + def setup_once() -> None: + """ + Set up the pydantic-ai integration. + + This patches the key methods in pydantic-ai to create Sentry spans for: + - Agent invocations (Agent.run methods) + - Model requests (AI client calls) + - Tool executions + """ + _patch_agent_run() + _patch_tool_execution() + + PydanticAIIntegration.using_request_hooks = False + try: + PYDANTIC_AI_VERSION = version("pydantic-ai-slim") + except PackageNotFoundError: + return + + PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) + if PYDANTIC_AI_VERSION is None: + return + + # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c + if PYDANTIC_AI_VERSION < ( + 1, + 73, + ): + _patch_graph_nodes() + return + + try: + from pydantic_ai.capabilities import Hooks + except ImportError: + return + + PydanticAIIntegration.using_request_hooks = True + hooks = Hooks() + register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py new file mode 100644 index 0000000000..afa66dc47d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py new file mode 100644 index 0000000000..d0ea6242b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py @@ -0,0 +1,3 @@ +from .agent_run import _patch_agent_run # noqa: F401 +from .graph_nodes import _patch_graph_nodes # noqa: F401 +from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py new file mode 100644 index 0000000000..3039e40698 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -0,0 +1,198 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import invoke_agent_span, update_invoke_agent_span +from ..utils import _capture_exception, pop_agent, push_agent + +try: + from pydantic_ai.agent import Agent # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + +class _StreamingContextManagerWrapper: + """Wrapper for streaming methods that return async context managers.""" + + def __init__( + self, + agent: "Any", + original_ctx_manager: "Any", + user_prompt: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = True, + ) -> None: + self.agent = agent + self.original_ctx_manager = original_ctx_manager + self.user_prompt = user_prompt + self.model = model + self.model_settings = model_settings + self.is_streaming = is_streaming + self._isolation_scope: "Any" = None + self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None + self._result: "Any" = None + + async def __aenter__(self) -> "Any": + # Set up isolation scope and invoke_agent span + self._isolation_scope = sentry_sdk.isolation_scope() + self._isolation_scope.__enter__() + + # Create invoke_agent span (will be closed in __aexit__) + self._span = invoke_agent_span( + self.user_prompt, + self.agent, + self.model, + self.model_settings, + self.is_streaming, + ) + self._span.__enter__() + + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur + push_agent(self.agent, self.is_streaming) + + # Enter the original context manager + result = await self.original_ctx_manager.__aenter__() + self._result = result + return result + + async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: + try: + # Exit the original context manager first + await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) + + # Update span with result if successful + if exc_type is None and self._result and self._span is not None: + update_invoke_agent_span(self._span, self._result) + finally: + # Pop agent from contextvar stack + pop_agent() + + # Clean up invoke span + if self._span: + self._span.__exit__(exc_type, exc_val, exc_tb) + + # Clean up isolation scope + if self._isolation_scope: + self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) + + +def _create_run_wrapper( + original_func: "Callable[..., Any]", is_streaming: bool = False +) -> "Callable[..., Any]": + """ + Wraps the Agent.run method to create an invoke_agent span. + + Args: + original_func: The original run method + is_streaming: Whether this is a streaming method (for future use) + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Create invoke_agent span + with invoke_agent_span( + user_prompt, self, model, model_settings, is_streaming + ) as span: + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in finally even if exceptions occur + push_agent(self, is_streaming) + + try: + result = await original_func(self, *args, **kwargs) + + # Update span with result + update_invoke_agent_span(span, result) + + return result + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + finally: + # Pop agent from contextvar stack + pop_agent() + + return wrapper + + +def _create_streaming_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps run_stream method that returns an async context manager. + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Call original function to get the context manager + original_ctx_manager = original_func(self, *args, **kwargs) + + # Wrap it with our instrumentation + return _StreamingContextManagerWrapper( + agent=self, + original_ctx_manager=original_ctx_manager, + user_prompt=user_prompt, + model=model, + model_settings=model_settings, + is_streaming=True, + ) + + return wrapper + + +def _patch_agent_run() -> None: + """ + Patches the Agent run methods to create spans for agent execution. + + This patches both non-streaming (run, run_sync) and streaming + (run_stream, run_stream_events) methods. + """ + + # Store original methods + original_run = Agent.run + original_run_stream = Agent.run_stream + + # Wrap and apply patches for non-streaming methods + Agent.run = _create_run_wrapper(original_run, is_streaming=False) + + # Wrap and apply patches for streaming methods + Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py new file mode 100644 index 0000000000..afb10395f4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py @@ -0,0 +1,106 @@ +from contextlib import asynccontextmanager +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import ( + ai_client_span, + update_ai_client_span, +) + +try: + from pydantic_ai._agent_graph import ModelRequestNode # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + +def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": + """Extract common data needed for creating chat spans. + + Returns: + Tuple of (messages, model, model_settings) + """ + # Extract model and settings from context + model = None + model_settings = None + if hasattr(ctx, "deps"): + model = getattr(ctx.deps, "model", None) + model_settings = getattr(ctx.deps, "model_settings", None) + + # Build full message list: history + current request + messages = [] + if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): + messages.extend(ctx.state.message_history) + + current_request = getattr(node, "request", None) + if current_request: + messages.append(current_request) + + return messages, model, model_settings + + +def _patch_graph_nodes() -> None: + """ + Patches the graph node execution to create appropriate spans. + + ModelRequestNode -> Creates ai_client span for model requests + CallToolsNode -> Handles tool calls (spans created in tool patching) + """ + + # Patch ModelRequestNode to create ai_client spans + original_model_request_run = ModelRequestNode.run + + @wraps(original_model_request_run) + async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + with ai_client_span(messages, None, model, model_settings) as span: + result = await original_model_request_run(self, ctx) + + # Extract response from result if available + model_response = None + if hasattr(result, "model_response"): + model_response = result.model_response + + update_ai_client_span(span, model_response) + return result + + ModelRequestNode.run = wrapped_model_request_run + + # Patch ModelRequestNode.stream for streaming requests + original_model_request_stream = ModelRequestNode.stream + + def create_wrapped_stream( + original_stream_method: "Callable[..., Any]", + ) -> "Callable[..., Any]": + """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" + + @asynccontextmanager + @wraps(original_stream_method) + async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + # Create chat span for streaming request + with ai_client_span(messages, None, model, model_settings) as span: + # Call the original stream method + async with original_stream_method(self, ctx) as stream: + yield stream + + # After streaming completes, update span with response data + # The ModelRequestNode stores the final response in _result + model_response = None + if hasattr(self, "_result") and self._result is not None: + # _result is a NextNode containing the model_response + if hasattr(self._result, "model_response"): + model_response = self._result.model_response + + update_ai_client_span(span, model_response) + + return wrapped_model_request_stream + + ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py new file mode 100644 index 0000000000..5646b5d47c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -0,0 +1,177 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import execute_tool_span, update_execute_tool_span +from ..utils import _capture_exception, get_current_agent + +if TYPE_CHECKING: + from typing import Any + +try: + try: + from pydantic_ai.tool_manager import ToolManager # type: ignore + except ImportError: + from pydantic_ai._tool_manager import ToolManager # type: ignore + + from pydantic_ai.exceptions import ToolRetryError # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +def _patch_tool_execution() -> None: + if hasattr(ToolManager, "execute_tool_call"): + _patch_execute_tool_call() + + elif hasattr(ToolManager, "_call_tool"): + # older versions + _patch_call_tool() + + +def _patch_execute_tool_call() -> None: + original_execute_tool_call = ToolManager.execute_tool_call + + @wraps(original_execute_tool_call) + async def wrapped_execute_tool_call( + self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not validated or not hasattr(validated, "call"): + return await original_execute_tool_call(self, validated, *args, **kwargs) + + # Extract tool info before calling original + call = validated.call + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_execute_tool_call( + self, + validated, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + return await original_execute_tool_call(self, validated, *args, **kwargs) + + ToolManager.execute_tool_call = wrapped_execute_tool_call + + +def _patch_call_tool() -> None: + """ + Patch ToolManager._call_tool to create execute_tool spans. + + This is the single point where ALL tool calls flow through in pydantic_ai, + regardless of toolset type (function, MCP, combined, wrapper, etc.). + + By patching here, we avoid: + - Patching multiple toolset classes + - Dealing with signature mismatches from instrumented MCP servers + - Complex nested toolset handling + """ + original_call_tool = ToolManager._call_tool + + @wraps(original_call_tool) + async def wrapped_call_tool( + self: "Any", call: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Extract tool info before calling original + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_call_tool( + self, + call, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + # No span context - just call original + return await original_call_tool( + self, + call, + *args, + **kwargs, + ) + + ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py new file mode 100644 index 0000000000..574046d645 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py @@ -0,0 +1,3 @@ +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py new file mode 100644 index 0000000000..27deb0c55c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -0,0 +1,331 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _get_model_name, + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, + get_current_agent, + get_is_streaming, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Dict, List, Union + + from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore + + from sentry_sdk._types import TextPart as SentryTextPart + +try: + from pydantic_ai.messages import ( + BaseToolCallPart, + BaseToolReturnPart, + BinaryContent, + ImageUrl, + SystemPromptPart, + TextPart, + ThinkingPart, + UserPromptPart, + ) +except ImportError: + # Fallback if these classes are not available + BaseToolCallPart = None + BaseToolReturnPart = None + SystemPromptPart = None + UserPromptPart = None + TextPart = None + ThinkingPart = None + BinaryContent = None + ImageUrl = None + + +def _transform_system_instructions( + permanent_instructions: "list[SystemPromptPart]", + current_instructions: "list[str]", +) -> "list[SentryTextPart]": + text_parts: "list[SentryTextPart]" = [ + { + "type": "text", + "content": instruction.content, + } + for instruction in permanent_instructions + ] + + text_parts.extend( + { + "type": "text", + "content": instruction, + } + for instruction in current_instructions + ) + + return text_parts + + +def _get_system_instructions( + messages: "list[ModelMessage]", +) -> "tuple[list[SystemPromptPart], list[str]]": + permanent_instructions = [] + current_instructions = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + if SystemPromptPart and isinstance(part, SystemPromptPart): + permanent_instructions.append(part) + + if hasattr(msg, "instructions") and msg.instructions is not None: + current_instructions.append(msg.instructions) + + return permanent_instructions, current_instructions + + +def _set_input_messages( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" +) -> None: + """Set input messages data on a span.""" + if not _should_send_prompts(): + return + + if not messages: + return + + permanent_instructions, current_instructions = _get_system_instructions(messages) + if len(permanent_instructions) > 0 or len(current_instructions) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + + try: + formatted_messages = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + role = "user" + # Use isinstance checks with proper base classes + if SystemPromptPart and isinstance(part, SystemPromptPart): + continue + elif ( + (TextPart and isinstance(part, TextPart)) + or (ThinkingPart and isinstance(part, ThinkingPart)) + or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) + ): + role = "assistant" + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + role = "tool" + + content: "List[Dict[str, Any] | str]" = [] + tool_calls = None + tool_call_id = None + + # Handle ToolCallPart (assistant requesting tool use) + if BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = {} + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + if tool_call_data: + tool_calls = [tool_call_data] + # Handle ToolReturnPart (tool result) + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + if hasattr(part, "tool_name"): + tool_call_id = part.tool_name + if hasattr(part, "content"): + content.append({"type": "text", "text": str(part.content)}) + # Handle regular content + elif hasattr(part, "content"): + if isinstance(part.content, str): + content.append({"type": "text", "text": part.content}) + elif isinstance(part.content, list): + for item in part.content: + if isinstance(item, str): + content.append({"type": "text", "text": item}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + else: + content.append(safe_serialize(item)) + else: + content.append({"type": "text", "text": str(part.content)}) + # Add message if we have content or tool calls + if content or tool_calls: + message: "Dict[str, Any]" = {"role": role} + if content: + message["content"] = content + if tool_calls: + message["tool_calls"] = tool_calls + if tool_call_id: + message["tool_call_id"] = tool_call_id + formatted_messages.append(message) + + if formatted_messages: + normalized_messages = normalize_message_roles(formatted_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + except Exception: + # If we fail to format messages, just skip it + pass + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" +) -> None: + """Set output data on a span.""" + if not _should_send_prompts(): + return + + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + + try: + # Extract text from ModelResponse + if hasattr(response, "parts"): + texts = [] + tool_calls = [] + + for part in response.parts: + if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): + texts.append(part.content) + elif BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = { + "type": "function", + } + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + tool_calls.append(tool_call_data) + + if texts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) + + if tool_calls: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) + ) + + except Exception: + # If we fail to format output, just skip it + pass + + +def ai_client_span( + messages: "Any", agent: "Any", model: "Any", model_settings: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for an AI client call (model request). + + Args: + messages: Full conversation history (list of messages) + agent: Agent object + model: Model object + model_settings: Model settings + """ + # Determine model name for span name + model_obj = model + if agent and hasattr(agent, "model"): + model_obj = agent.model + + model_name = _get_model_name(model_obj) or "unknown" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + # Set streaming flag from contextvar + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + + # Add available tools if agent is available + agent_obj = agent or get_current_agent() + _set_available_tools(span, agent_obj) + + # Set input messages (full conversation history) + if messages: + _set_input_messages(span, messages) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" +) -> None: + """Update the AI client span with response data.""" + if not span: + return + + # Set usage data if available + if model_response and hasattr(model_response, "usage"): + _set_usage_data(span, model_response.usage) + + # Set output data + _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py new file mode 100644 index 0000000000..7648c1418a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _should_send_prompts + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from pydantic_ai._tool_manager import ToolDefinition # type: ignore + + +def execute_tool_span( + tool_name: str, + tool_args: "Any", + agent: "Any", + tool_definition: "Optional[ToolDefinition]" = None, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for tool execution. + + Args: + tool_name: The name of the tool being executed + tool_args: The arguments passed to the tool + agent: The agent executing the tool + tool_definition: The definition of the tool, if available + """ + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + set_on_span = span.set_data + + if tool_definition is not None and hasattr(tool_definition, "description"): + set_on_span( + SPANDATA.GEN_AI_TOOL_DESCRIPTION, + tool_definition.description, + ) + + _set_agent_data(span, agent) + + if _should_send_prompts() and tool_args is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + """Update the execute tool span with the result.""" + if not span: + return + + if not _should_send_prompts() or result is None: + return + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + else: + span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py new file mode 100644 index 0000000000..f0c68e85ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -0,0 +1,184 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, +) + +if TYPE_CHECKING: + from typing import Any, Union + +try: + from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore +except ImportError: + BinaryContent = None + ImageUrl = None + + +def invoke_agent_span( + user_prompt: "Any", + agent: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = False, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for invoking the agent.""" + # Determine agent name for span + name = "agent" + if agent and getattr(agent, "name", None): + name = agent.name + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + _set_available_tools(span, agent) + + # Add user prompt and system prompts if available and prompts are enabled + if _should_send_prompts(): + messages = [] + + # Add system prompts (both instructions and system_prompt) + system_texts = [] + + if agent: + # Check for system_prompt + system_prompts = getattr(agent, "_system_prompts", None) or [] + for prompt in system_prompts: + if isinstance(prompt, str): + system_texts.append(prompt) + + # Check for instructions (stored in _instructions) + instructions = getattr(agent, "_instructions", None) + if instructions: + if isinstance(instructions, str): + system_texts.append(instructions) + elif isinstance(instructions, (list, tuple)): + for instr in instructions: + if isinstance(instr, str): + system_texts.append(instr) + elif callable(instr): + # Skip dynamic/callable instructions + pass + + # Add all system texts as system messages + for system_text in system_texts: + messages.append( + { + "content": [{"text": system_text, "type": "text"}], + "role": "system", + } + ) + + # Add user prompt + if user_prompt: + if isinstance(user_prompt, str): + messages.append( + { + "content": [{"text": user_prompt, "type": "text"}], + "role": "user", + } + ) + elif isinstance(user_prompt, list): + # Handle list of user content + content = [] + for item in user_prompt: + if isinstance(item, str): + content.append({"text": item, "type": "text"}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + if content: + messages.append( + { + "content": content, + "role": "user", + } + ) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + result: "Any", +) -> None: + """Update and close the invoke agent span.""" + if not span or not result: + return + + # Extract output from result + output = getattr(result, "output", None) + + # Set response text if prompts are enabled + if _should_send_prompts() and output: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False + ) + + # Set model name from response if available + if hasattr(result, "response"): + try: + response = result.response + if hasattr(response, "model_name") and response.model_name: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + except Exception: + # If response access fails, continue without setting model name + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py new file mode 100644 index 0000000000..330496c6b2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py @@ -0,0 +1,87 @@ +"""Utility functions for PydanticAI span instrumentation.""" + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX +from sentry_sdk.ai.utils import get_modality_from_mime_type +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore + + +def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": + """Serialize an ImageUrl content item for span data. + + For data URLs containing base64-encoded images, the content is redacted. + For regular HTTP URLs, the URL string is preserved. + """ + url = str(item.url) + data_url_match = DATA_URL_BASE64_REGEX.match(url) + + if data_url_match: + return { + "type": "image", + "content": BLOB_DATA_SUBSTITUTE, + } + + return { + "type": "image", + "content": url, + } + + +def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": + """Serialize a BinaryContent item for span data, redacting the blob data.""" + return { + "type": "blob", + "modality": get_modality_from_mime_type(item.media_type), + "mime_type": item.media_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + usage: "Union[RequestUsage, RunUsage]", +) -> None: + """Set token usage data on a span. + + This function works with both RequestUsage (single request) and + RunUsage (agent run) objects from pydantic_ai. + + Args: + span: The Sentry span to set data on. + usage: RequestUsage or RunUsage object containing token usage information. + """ + if usage is None: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if hasattr(usage, "input_tokens") and usage.input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + + # Pydantic AI uses cache_read_tokens (not input_tokens_cached) + if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) + + # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) + if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + usage.cache_write_tokens, + ) + + if hasattr(usage, "output_tokens") and usage.output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + + if hasattr(usage, "total_tokens") and usage.total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py new file mode 100644 index 0000000000..340dcf8953 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py @@ -0,0 +1,233 @@ +from contextvars import ContextVar +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + +# Store the current agent context in a contextvar for re-entrant safety +# Using a list as a stack to support nested agent calls +_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( + "pydantic_ai_agent_context_stack", default=[] +) + + +def push_agent(agent: "Any", is_streaming: bool = False) -> None: + """Push an agent context onto the stack along with its streaming flag.""" + stack = _agent_context_stack.get().copy() + stack.append({"agent": agent, "is_streaming": is_streaming}) + _agent_context_stack.set(stack) + + +def pop_agent() -> None: + """Pop an agent context from the stack.""" + stack = _agent_context_stack.get().copy() + if stack: + stack.pop() + _agent_context_stack.set(stack) + + +def get_current_agent() -> "Any": + """Get the current agent from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1]["agent"] + return None + + +def get_is_streaming() -> bool: + """Get the streaming flag from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1].get("is_streaming", False) + return False + + +def _should_send_prompts() -> bool: + """ + Check if prompts should be sent to Sentry. + + This checks both send_default_pii and the include_prompts integration setting. + """ + if not should_send_default_pii(): + return False + + from . import PydanticAIIntegration + + # Get the integration instance from the client + integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) + + if integration is None: + return False + + return getattr(integration, "include_prompts", False) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set agent-related data on a span. + + Args: + span: The span to set data on + agent: Agent object (can be None, will try to get from contextvar if not provided) + """ + # Extract agent name from agent object or contextvar + agent_obj = agent + if not agent_obj: + # Try to get from contextvar + agent_obj = get_current_agent() + + if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + else: + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + + +def _get_model_name(model_obj: "Any") -> "Optional[str]": + """Extract model name from a model object. + + Args: + model_obj: Model object to extract name from + + Returns: + Model name string or None if not found + """ + if not model_obj: + return None + + if hasattr(model_obj, "model_name"): + return model_obj.model_name + elif hasattr(model_obj, "name"): + try: + return model_obj.name() + except Exception: + return str(model_obj) + elif isinstance(model_obj, str): + return model_obj + else: + return str(model_obj) + + +def _set_model_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + model: "Any", + model_settings: "Any", +) -> None: + """Set model-related data on a span. + + Args: + span: The span to set data on + model: Model object (can be None, will try to get from agent if not provided) + model_settings: Model settings (can be None, will try to get from agent if not provided) + """ + # Try to get agent from contextvar if we need it + agent_obj = get_current_agent() + + # Extract model information + model_obj = model + if not model_obj and agent_obj and hasattr(agent_obj, "model"): + model_obj = agent_obj.model + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if model_obj: + # Set system from model + if hasattr(model_obj, "system"): + set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) + + # Set model name + model_name = _get_model_name(model_obj) + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Extract model settings + settings = model_settings + if not settings and agent_obj and hasattr(agent_obj, "model_settings"): + settings = agent_obj.model_settings + + if settings: + settings_map = { + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + } + + # ModelSettings is a TypedDict (dict at runtime), so use dict access + if isinstance(settings, dict): + for setting_name, spandata_key in settings_map.items(): + value = settings.get(setting_name) + if value is not None: + set_on_span(spandata_key, value) + else: + # Fallback for object-style settings + for setting_name, spandata_key in settings_map.items(): + if hasattr(settings, setting_name): + value = getattr(settings, setting_name) + if value is not None: + set_on_span(spandata_key, value) + + +def _set_available_tools( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set available tools data on a span from an agent's function toolset. + + Args: + span: The span to set data on + agent: Agent object with _function_toolset attribute + """ + if not agent or not hasattr(agent, "_function_toolset"): + return + + try: + tools = [] + # Get tools from the function toolset + if hasattr(agent._function_toolset, "tools"): + for tool_name, tool in agent._function_toolset.tools.items(): + tool_info = {"name": tool_name} + + # Add description from function_schema if available + if hasattr(tool, "function_schema"): + schema = tool.function_schema + if getattr(schema, "description", None): + tool_info["description"] = schema.description + + # Add parameters from json_schema + if getattr(schema, "json_schema", None): + tool_info["parameters"] = schema.json_schema + + tools.append(tool_info) + + if tools: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + else: + span.set_data( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + except Exception: + # If we can't extract tools, just skip it + pass + + +def _capture_exception(exc: "Any", handled: bool = False) -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pydantic_ai", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py new file mode 100644 index 0000000000..2616f4d5a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py @@ -0,0 +1,262 @@ +import copy +import json + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +try: + from pymongo import monitoring +except ImportError: + raise DidNotEnable("Pymongo not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pymongo.monitoring import ( + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + ) + + +SAFE_COMMAND_ATTRIBUTES = [ + "insert", + "ordered", + "find", + "limit", + "singleBatch", + "aggregate", + "createIndexes", + "indexes", + "delete", + "findAndModify", + "renameCollection", + "to", + "drop", +] + + +def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": + for key in command: + is_safe_field = key in SAFE_COMMAND_ATTRIBUTES + if is_safe_field: + # Skip if safe key + continue + + update_db_command = key == "update" and "findAndModify" not in command + if update_db_command: + # Also skip "update" db command because it is save. + # There is also an "update" key in the "findAndModify" command, which is NOT safe! + continue + + # Special stripping for documents + is_document = key == "documents" + if is_document: + for doc in command[key]: + for doc_key in doc: + doc[doc_key] = "%s" + continue + + # Special stripping for dict style fields + is_dict_field = key in ["filter", "query", "update"] + if is_dict_field: + for item_key in command[key]: + command[key][item_key] = "%s" + continue + + # For pipeline fields strip the `$match` dict + is_pipeline_field = key == "pipeline" + if is_pipeline_field: + for pipeline in command[key]: + for match_key in pipeline["$match"] if "$match" in pipeline else []: + pipeline["$match"][match_key] = "%s" + continue + + # Default stripping + command[key] = "%s" + + return command + + +def _get_db_data(event: "Any") -> "Dict[str, Any]": + data = {} + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + data[SPANDATA.DB_DRIVER_NAME] = "pymongo" + db_name = event.database_name + + server_address = event.connection_id[0] + if server_address is not None: + data[SPANDATA.SERVER_ADDRESS] = server_address + + server_port = event.connection_id[1] + if server_port is not None: + data[SPANDATA.SERVER_PORT] = server_port + + if is_span_streaming_enabled: + data["db.system.name"] = "mongodb" + + if db_name is not None: + data["db.namespace"] = db_name + else: + data[SPANDATA.DB_SYSTEM] = "mongodb" + + if db_name is not None: + data[SPANDATA.DB_NAME] = db_name + + return data + + +class CommandTracer(monitoring.CommandListener): + def __init__(self) -> None: + self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} + + def _operation_key( + self, + event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", + ) -> int: + return event.request_id + + def started(self, event: "CommandStartedEvent") -> None: + client = sentry_sdk.get_client() + if client.get_integration(PyMongoIntegration) is None: + return + + with capture_internal_exceptions(): + command = dict(copy.deepcopy(event.command)) + + command.pop("$db", None) + command.pop("$clusterTime", None) + command.pop("$signature", None) + + db_data = _get_db_data(event) + + collection_name = command.get(event.command_name) + operation_name = event.command_name + db_name = event.database_name + + lsid = command.pop("lsid", None) + if not should_send_default_pii(): + command = _strip_pii(command) + + query = json.dumps(command, default=str) + + if has_span_streaming_enabled(client.options): + span_first_data = { + "db.operation.name": operation_name, + "db.collection.name": collection_name, + SPANDATA.DB_QUERY_TEXT: query, + "sentry.op": OP.DB, + "sentry.origin": PyMongoIntegration.origin, + **db_data, + } + + span = sentry_sdk.traces.start_span( + name=query, attributes=span_first_data + ) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, + category="query", + type=OP.DB, + data=span_first_data, + ) + + else: + tags = { + "db.name": db_name, + SPANDATA.DB_SYSTEM: "mongodb", + SPANDATA.DB_DRIVER_NAME: "pymongo", + SPANDATA.DB_OPERATION: operation_name, + # The below is a deprecated field, but leaving for legacy reasons. + # The v2 spans will use `db.collection.name` instead. + SPANDATA.DB_MONGODB_COLLECTION: collection_name, + } + + try: + tags["net.peer.name"] = event.connection_id[0] + tags["net.peer.port"] = str(event.connection_id[1]) + except TypeError: + pass + + data: "Dict[str, Any]" = {"operation_ids": {}} + data["operation_ids"]["operation"] = event.operation_id + data["operation_ids"]["request"] = event.request_id + + data.update(db_data) + + try: + if lsid: + lsid_id = lsid["id"] + data["operation_ids"]["session"] = str(lsid_id) + except KeyError: + pass + + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=PyMongoIntegration.origin, + ) + + for tag, value in tags.items(): + # set the tag for backwards-compatibility. + # TODO: remove the set_tag call in the next major release! + span.set_tag(tag, value) + span.set_data(tag, value) + + for key, value in data.items(): + span.set_data(key, value) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, category="query", type=OP.DB, data=tags + ) + + self._ongoing_operations[self._operation_key(event)] = span.__enter__() + + def failed(self, event: "CommandFailedEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + # Ignoring NoOpStreamedSpan as it will always have a status of "ok" + if type(span) is StreamedSpan: + span.status = SpanStatus.ERROR + elif type(span) is Span: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + span.__exit__(None, None, None) + except KeyError: + return + + def succeeded(self, event: "CommandSucceededEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + if type(span) is Span: + span.set_status(SPANSTATUS.OK) + span.__exit__(None, None, None) + except KeyError: + pass + + +class PyMongoIntegration(Integration): + identifier = "pymongo" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py new file mode 100644 index 0000000000..6837d8345c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py @@ -0,0 +1,239 @@ +import functools +import os +import sys +import weakref + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +try: + from pyramid.httpexceptions import HTTPException + from pyramid.request import Request +except ImportError: + raise DidNotEnable("Pyramid not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional + + from pyramid.response import Response + from webob.cookies import RequestCookies + from webob.request import _FieldStorageWithFile + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.utils import ExcInfo + + +if getattr(Request, "authenticated_userid", None): + + def authenticated_userid(request: "Request") -> "Optional[Any]": + return request.authenticated_userid + +else: + # bw-compat for pyramid < 1.5 + from pyramid.security import authenticated_userid # type: ignore + + +TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") + + +class PyramidIntegration(Integration): + identifier = "pyramid" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "route_name") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + from pyramid import router + + old_call_view = router._call_view + + @functools.wraps(old_call_view) + def sentry_patched_call_view( + registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + integration = client.get_integration(PyramidIntegration) + if integration is None: + return old_call_view(registry, request, *args, **kwargs) + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii() and has_span_streaming_enabled(client.options): + user_id = authenticated_userid(request) + if user_id: + scope.set_user({"id": user_id}) + + scope.add_event_processor( + _make_event_processor(weakref.ref(request), integration) + ) + + return old_call_view(registry, request, *args, **kwargs) + + router._call_view = sentry_patched_call_view + + if hasattr(Request, "invoke_exception_view"): + old_invoke_exception_view = Request.invoke_exception_view + + def sentry_patched_invoke_exception_view( + self: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + rv = old_invoke_exception_view(self, *args, **kwargs) + + if ( + self.exc_info + and all(self.exc_info) + and rv.status_int == 500 + and sentry_sdk.get_client().get_integration(PyramidIntegration) + is not None + ): + _capture_exception(self.exc_info) + + return rv + + Request.invoke_exception_view = sentry_patched_invoke_exception_view + + old_wsgi_call = router.Router.__call__ + + @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) + def sentry_patched_wsgi_call( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + def sentry_patched_inner_wsgi_call( + environ: "Dict[str, Any]", start_response: "Callable[..., Any]" + ) -> "Any": + try: + return old_wsgi_call(self, environ, start_response) + except Exception: + einfo = sys.exc_info() + _capture_exception(einfo) + reraise(*einfo) + + middleware = SentryWsgiMiddleware( + sentry_patched_inner_wsgi_call, + span_origin=PyramidIntegration.origin, + ) + return middleware(environ, start_response) + + router.Router.__call__ = sentry_patched_wsgi_call + + +@ensure_integration_enabled(PyramidIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + if exc_info[0] is None or issubclass(exc_info[0], HTTPException): + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pyramid", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "route_name": request.matched_route.name, + "route_pattern": request.matched_route.pattern, + } + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if is_span_streaming_enabled + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + scope.set_transaction_name( + name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +class PyramidRequestExtractor(RequestExtractor): + def url(self) -> str: + return self.request.path_url + + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "RequestCookies": + return self.request.cookies + + def raw_data(self) -> str: + return self.request.text + + def form(self) -> "Dict[str, str]": + return { + key: value + for key, value in self.request.POST.items() + if not getattr(value, "filename", None) + } + + def files(self) -> "Dict[str, _FieldStorageWithFile]": + return { + key: value + for key, value in self.request.POST.items() + if getattr(value, "filename", None) + } + + def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: + file = postdata.file + try: + return os.fstat(file.fileno()).st_size + except Exception: + return 0 + + +def _make_event_processor( + weak_request: "Callable[[], Request]", integration: "PyramidIntegration" +) -> "EventProcessor": + def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + PyramidRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_info = event.setdefault("user", {}) + user_info.setdefault("id", authenticated_userid(request)) + + return event + + return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py new file mode 100644 index 0000000000..aae68c4c10 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py @@ -0,0 +1,197 @@ +from contextlib import contextmanager +from typing import Any, Generator + +import sentry_sdk +from sentry_sdk import start_span +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + logger, + parse_url, +) + +try: + from pyreqwest.client import ( # type: ignore[import-not-found] + ClientBuilder, + SyncClientBuilder, + ) + from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] + from pyreqwest.request import ( # type: ignore[import-not-found] + OneOffRequestBuilder, + Request, + SyncOneOffRequestBuilder, + ) + from pyreqwest.response import ( # type: ignore[import-not-found] + Response, + SyncResponse, + ) +except ImportError: + raise DidNotEnable("pyreqwest not installed or incompatible version installed") + + +class PyreqwestIntegration(Integration): + identifier = "pyreqwest" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_pyreqwest() + + +def _patch_pyreqwest() -> None: + # Patch Client Builders + _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) + _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) + + # Patch Request Builders + _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) + _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) + + +def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: + if not hasattr(cls, method_name): + return + + original_method = getattr(cls, method_name) + + def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + if not getattr(self, "_sentry_instrumented", False): + integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) + if integration is not None: + self.with_middleware(middleware) + try: + self._sentry_instrumented = True + except (TypeError, AttributeError): + # In case the instance itself is immutable or doesn't allow extra attributes + pass + return original_method(self, *args, **kwargs) + + setattr(cls, method_name, sentry_patched_method) + + +@contextmanager +def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": PyreqwestIntegration.origin, + SPANDATA.HTTP_REQUEST_METHOD: request.method, + }, + ) as span: + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + return + + with start_span( + op=OP.HTTP_CLIENT, + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + origin=PyreqwestIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + +async def sentry_async_middleware( + request: "Request", next_handler: "Next" +) -> "Response": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return await next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = await next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response + + +def sentry_sync_middleware( + request: "Request", next_handler: "SyncNext" +) -> "SyncResponse": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py new file mode 100644 index 0000000000..6a5603d825 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py @@ -0,0 +1,292 @@ +import asyncio +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Union + + from sentry_sdk._types import Event, EventProcessor + +try: + import quart_auth # type: ignore +except ImportError: + quart_auth = None + +try: + from quart import ( # type: ignore + Quart, + Request, + has_request_context, + has_websocket_context, + request, + websocket, + ) + from quart.signals import ( # type: ignore + got_background_exception, + got_request_exception, + got_websocket_exception, + request_started, + websocket_started, + ) +except ImportError: + raise DidNotEnable("Quart is not installed") +else: + # Quart 0.19 is based on Flask and hence no longer has a Scaffold + try: + from quart.scaffold import Scaffold # type: ignore + except ImportError: + from flask.sansio.scaffold import Scaffold # type: ignore + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class QuartIntegration(Integration): + identifier = "quart" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "endpoint") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + request_started.connect(_request_websocket_started) + websocket_started.connect(_request_websocket_started) + got_background_exception.connect(_capture_exception) + got_request_exception.connect(_capture_exception) + got_websocket_exception.connect(_capture_exception) + + patch_asgi_app() + patch_scaffold_route() + + +def patch_asgi_app() -> None: + old_app = Quart.__call__ + + async def sentry_patched_asgi_app( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + if sentry_sdk.get_client().get_integration(QuartIntegration) is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=QuartIntegration.origin, + asgi_version=3, + ) + return await middleware(scope, receive, send) + + Quart.__call__ = sentry_patched_asgi_app + + +def patch_scaffold_route() -> None: + # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 + if sys.version_info >= (3, 12): + iscoroutinefunction = inspect.iscoroutinefunction + else: + iscoroutinefunction = asyncio.iscoroutinefunction + + old_route = Scaffold.route + + def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": + old_decorator = old_route(*args, **kwargs) + + def decorator(old_func: "Any") -> "Any": + if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): + + @wraps(old_func) + @ensure_integration_enabled(QuartIntegration, old_func) + def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + span = get_current_span() + if span is not None and hasattr(span, "_segment"): + span._segment._update_active_thread() + else: + current_scope = sentry_sdk.get_current_scope() + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_func(*args, **kwargs) + + return old_decorator(_sentry_func) + + return old_decorator(old_func) + + return decorator + + Scaffold.route = _sentry_route + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if has_span_streaming_enabled(sentry_sdk.get_client().options) + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + + scope.set_transaction_name( + name=name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + if has_request_context(): + request_websocket = request._get_current_object() + if has_websocket_context(): + request_websocket = websocket._get_current_object() + + # Set the transaction name here, but rely on ASGI middleware + # to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket + ) + + scope = sentry_sdk.get_isolation_scope() + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + current_span = get_current_span() + if type(current_span) is StreamedSpan: + segment = current_span._segment + + segment.set_attribute("http.request.method", request_websocket.method) + header_attributes: "dict[str, Any]" = {} + + for header, header_value in _filter_headers( + dict(request_websocket.headers), use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + segment.set_attributes(header_attributes) + + if should_send_default_pii(): + segment.set_attribute("url.full", request_websocket.url) + segment.set_attribute( + "url.query", + request_websocket.query_string.decode("utf-8", errors="replace"), + ) + + user_properties = {} + if len(request_websocket.access_route) >= 1: + segment.set_attribute( + "client.address", request_websocket.access_route[0] + ) + user_properties["ip_address"] = request_websocket.access_route[0] + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_properties["id"] = current_user_id + + if user_properties: + existing_user_properties = scope._user or {} + scope.set_user({**existing_user_properties, **user_properties}) + + evt_processor = _make_request_event_processor(app, request_websocket, integration) + scope.add_event_processor(evt_processor) + + +def _make_request_event_processor( + app: "Quart", request: "Request", integration: "QuartIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + # TODO: Figure out what to do with request body. Methods on request + # are async, but event processors are not. + + request_info = event.setdefault("request", {}) + request_info["url"] = request.url + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + if len(request.access_route) >= 1: + request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_info = event.setdefault("user", {}) + user_info["id"] = current_user_id + + return event + + return inner + + +async def _capture_exception( + sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "quart", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_current_user_id_from_quart() -> "str | None": + if quart_auth is None: + return None + + if quart_auth.current_user is None: + return None + + try: + return quart_auth.current_user._auth_id + except Exception: + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py new file mode 100644 index 0000000000..f723a96f3c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py @@ -0,0 +1,240 @@ +import functools +import inspect +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + event_from_exception, + logger, + package_version, + qualname_from_function, + reraise, +) + +try: + import ray # type: ignore[import-not-found] + from ray import remote +except ImportError: + raise DidNotEnable("Ray not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional + + from sentry_sdk.utils import ExcInfo + + +def _check_sentry_initialized() -> None: + if sentry_sdk.get_client().is_active(): + return + + logger.debug( + "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." + ) + + +def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: + # Patching new_func signature to add the _sentry_tracing parameter to it + # Ray later inspects the signature and finds the unexpected parameter otherwise + signature = inspect.signature(func) + params = list(signature.parameters.values()) + sentry_tracing_param = inspect.Parameter( + "_sentry_tracing", + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + ) + + # Keyword only arguments are penultimate if function has variadic keyword arguments + if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: + params.insert(-1, sentry_tracing_param) + else: + params.append(sentry_tracing_param) + + func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] + + +def _patch_ray_remote() -> None: + old_remote = remote + + @functools.wraps(old_remote) + def new_remote( + f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" + ) -> "Callable[..., Any]": + if inspect.isclass(f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(f, *args, **kwargs) + + def wrapper(user_f: "Callable[..., Any]") -> "Any": + if inspect.isclass(user_f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(*args, **kwargs)(user_f) + + @functools.wraps(user_f) + def new_func( + *f_args: "Any", + _sentry_tracing: "Optional[dict[str, Any]]" = None, + **f_kwargs: "Any", + ) -> "Any": + _check_sentry_initialized() + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(_sentry_tracing or {}) + + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RAY, + "sentry.origin": RayIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + }, + parent_span=None, + ): + try: + result = user_f(*f_args, **f_kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + transaction = sentry_sdk.continue_trace( + _sentry_tracing or {}, + op=OP.QUEUE_TASK_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + source=TransactionSource.TASK, + ) + + with sentry_sdk.start_transaction(transaction) as transaction: + try: + result = user_f(*f_args, **f_kwargs) + transaction.set_status(SPANSTATUS.OK) + except Exception: + transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + _insert_sentry_tracing_in_signature(new_func) + + if f: + rv = old_remote(new_func) + else: + rv = old_remote(*args, **kwargs)(new_func) + old_remote_method = rv.remote + + def _remote_method_with_header_propagation( + *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Ray Client + """ + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_RAY, + "sentry.origin": RayIntegration.origin, + }, + ): + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + ) as span: + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + span.set_status(SPANSTATUS.OK) + except Exception: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + rv.remote = _remote_method_with_header_propagation + + return rv + + if f is not None: + return wrapper(f) + else: + return wrapper + + ray.remote = new_remote + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={ + "handled": False, + "type": RayIntegration.identifier, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class RayIntegration(Integration): + identifier = "ray" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = package_version("ray") + _check_minimum_version(RayIntegration, version) + + _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py new file mode 100644 index 0000000000..7095721ed2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py @@ -0,0 +1,49 @@ +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE +from sentry_sdk.integrations.redis.rb import _patch_rb +from sentry_sdk.integrations.redis.redis import _patch_redis +from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster +from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +class RedisIntegration(Integration): + identifier = "redis" + + def __init__( + self, + max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, + cache_prefixes: "Optional[list[str]]" = None, + ) -> None: + self.max_data_size = max_data_size + self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] + + if max_data_size is not None: + warnings.warn( + "The `max_data_size` parameter of `RedisIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + try: + from redis import StrictRedis, client + except ImportError: + raise DidNotEnable("Redis client not installed") + + _patch_redis(StrictRedis, client) + _patch_redis_cluster() + _patch_rb() + + try: + _patch_rediscluster() + except Exception: + logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py new file mode 100644 index 0000000000..8fc3d0c3a9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py @@ -0,0 +1,177 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from redis.asyncio.client import Pipeline, StrictRedis + from redis.asyncio.cluster import ClusterPipeline, RedisCluster + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_async_pipeline( + pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return await old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + try: + command_seq = self._execution_strategy._command_queue + except AttributeError: + if is_cluster: + command_seq = self._command_stack + else: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.is_transaction, + command_seq, + ) + + return await old_execute(self, *args, **kwargs) + + pipeline_cls.execute = _sentry_execute # type: ignore + + +def patch_redis_async_client( + cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return await old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = await old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py new file mode 100644 index 0000000000..58d686b099 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_pipeline( + pipeline_cls: "Any", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + command_seq = None + try: + command_seq = self._execution_strategy.command_queue + except AttributeError: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.transaction, + command_seq, + ) + + return old_execute(self, *args, **kwargs) + + pipeline_cls.execute = sentry_patched_execute + + +def patch_redis_client( + cls: "Any", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + """ + This function can be used to instrument custom redis client classes or + subclasses. + """ + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py new file mode 100644 index 0000000000..0822c2c930 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py @@ -0,0 +1,19 @@ +SPAN_ORIGIN = "auto.db.redis" + +_SINGLE_KEY_COMMANDS = frozenset( + ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], +) +_MULTI_KEY_COMMANDS = frozenset( + [ + "del", + "touch", + "unlink", + "mget", + ], +) +_COMMANDS_INCLUDING_SENSITIVE_DATA = [ + "auth", +] +_MAX_NUM_ARGS = 10 # Trim argument lists to this many values +_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values +_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py new file mode 100644 index 0000000000..35f20fddd9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py @@ -0,0 +1,136 @@ +""" +Code used for the Caches module in Sentry +""" + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +GET_COMMANDS = ("get", "mget") +SET_COMMANDS = ("set", "setex") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _get_op(name: str) -> "Optional[str]": + op = None + if name.lower() in GET_COMMANDS: + op = OP.CACHE_GET + elif name.lower() in SET_COMMANDS: + op = OP.CACHE_PUT + + return op + + +def _compile_cache_span_properties( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> "dict[str, Any]": + key = _get_safe_key(redis_command, args, kwargs) + key_as_string = _key_as_string(key) + keys_as_string = key_as_string.split(", ") + + is_cache_key = False + for prefix in integration.cache_prefixes: + for kee in keys_as_string: + if kee.startswith(prefix): + is_cache_key = True + break + if is_cache_key: + break + + value = None + if redis_command.lower() in SET_COMMANDS: + value = args[-1] + + properties = { + "op": _get_op(redis_command), + "description": _get_cache_span_description( + redis_command, args, kwargs, integration + ), + "key": key, + "key_as_string": key_as_string, + "redis_command": redis_command.lower(), + "is_cache_key": is_cache_key, + "value": value, + } + + return properties + + +def _get_cache_span_description( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> str: + description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_cache_data( + span: "Union[Span, StreamedSpan]", + redis_client: "Any", + properties: "dict[str, Any]", + return_value: "Optional[Any]", +) -> None: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + with capture_internal_exceptions(): + set_on_span(SPANDATA.CACHE_KEY, properties["key"]) + + if properties["redis_command"] in GET_COMMANDS: + if return_value is not None: + set_on_span(SPANDATA.CACHE_HIT, True) + size = ( + len(str(return_value).encode("utf-8")) + if not isinstance(return_value, bytes) + else len(return_value) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + else: + set_on_span(SPANDATA.CACHE_HIT, False) + + elif properties["redis_command"] in SET_COMMANDS: + if properties["value"] is not None: + size = ( + len(properties["value"].encode("utf-8")) + if not isinstance(properties["value"], bytes) + else len(properties["value"]) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + + try: + connection_params = redis_client.connection_pool.connection_kwargs + except AttributeError: + # If it is a cluster, there is no connection_pool attribute so we + # need to get the default node from the cluster instance + default_node = redis_client.get_default_node() + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + + host = connection_params.get("host") + if host is not None: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) + + port = connection_params.get("port") + if port is not None: + set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py new file mode 100644 index 0000000000..69207bf6f6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py @@ -0,0 +1,88 @@ +""" +Code used for the Queries module in Sentry +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_command +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import Redis + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _compile_db_span_properties( + integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" +) -> "dict[str, Any]": + description = _get_db_span_description(integration, redis_command, args) + + properties = { + "op": OP.DB_REDIS, + "description": description, + } + + return properties + + +def _get_db_span_description( + integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" +) -> str: + description = command_name + + with capture_internal_exceptions(): + description = _get_safe_command(command_name, args) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_db_data_on_span( + span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" +) -> None: + db = connection_params.get("db") + host = connection_params.get("host") + port = connection_params.get("port") + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) + + if host is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_attribute(SPANDATA.SERVER_PORT, port) + + else: + span.set_data(SPANDATA.DB_SYSTEM, "redis") + span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_data(SPANDATA.DB_NAME, str(db)) + + if host is not None: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_data(SPANDATA.SERVER_PORT, port) + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" +) -> None: + try: + _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) + except AttributeError: + pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py new file mode 100644 index 0000000000..e2ce863fe8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py @@ -0,0 +1,31 @@ +""" +Instrumentation for Redis Blaster (rb) + +https://github.com/getsentry/rb +""" + +from sentry_sdk.integrations.redis._sync_common import patch_redis_client +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + + +def _patch_rb() -> None: + try: + import rb.clients # type: ignore + except ImportError: + pass + else: + patch_redis_client( + rb.clients.FanoutClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.MappingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.RoutingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py new file mode 100644 index 0000000000..e704c9bc6a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py @@ -0,0 +1,67 @@ +""" +Instrumentation for Redis + +https://github.com/redis/redis-py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + +if TYPE_CHECKING: + from typing import Any, Sequence + + +def _get_redis_command_args(command: "Any") -> "Sequence[Any]": + return command[0] + + +def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 + patch_redis_client( + StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_pipeline( + client.Pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + try: + strict_pipeline = client.StrictPipeline + except AttributeError: + pass + else: + patch_redis_pipeline( + strict_pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + + try: + import redis.asyncio + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + redis.asyncio.client.StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_async_pipeline( + redis.asyncio.client.Pipeline, + False, + _get_redis_command_args, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py new file mode 100644 index 0000000000..b6c95e6abd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py @@ -0,0 +1,115 @@ +""" +Instrumentation for RedisCluster +This is part of the main redis-py client. + +https://github.com/redis/redis-py/blob/master/redis/cluster.py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import RedisCluster + from redis.asyncio.cluster import ( + ClusterPipeline as AsyncClusterPipeline, + ) + from redis.asyncio.cluster import ( + RedisCluster as AsyncRedisCluster, + ) + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +def _set_async_cluster_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_instance: "AsyncRedisCluster[Any]", +) -> None: + default_node = async_redis_cluster_instance.get_default_node() + if default_node is not None and default_node.connection_kwargs is not None: + _set_db_data_on_span(span, default_node.connection_kwargs) + + +def _set_async_cluster_pipeline_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", +) -> None: + with capture_internal_exceptions(): + client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) + if client is None: + # In older redis-py versions, the AsyncClusterPipeline had a `_client` + # attr but it is private so potentially problematic and mypy does not + # recognize it - see + # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 + client = ( + async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] + ) + + _set_async_cluster_db_data( + span, + client, + ) + + +def _set_cluster_db_data( + span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" +) -> None: + default_node = redis_cluster_instance.get_default_node() + + if default_node is not None: + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + _set_db_data_on_span(span, connection_params) + + +def _patch_redis_cluster() -> None: + """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" + try: + from redis import RedisCluster, cluster + except ImportError: + pass + else: + patch_redis_client( + RedisCluster, + is_cluster=True, + set_db_data_fn=_set_cluster_db_data, + ) + patch_redis_pipeline( + cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_cluster_db_data, + ) + + try: + from redis.asyncio import cluster as async_cluster + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + async_cluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_async_cluster_db_data, + ) + patch_redis_async_pipeline( + async_cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_async_cluster_pipeline_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py new file mode 100644 index 0000000000..3437aa1f2f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py @@ -0,0 +1,49 @@ +""" +Instrumentation for redis-py-cluster +The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). + +https://github.com/grokzen/redis-py-cluster +""" + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command + + +def _patch_rediscluster() -> None: + try: + import rediscluster # type: ignore + except ImportError: + return + + patch_redis_client( + rediscluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + + # up to v1.3.6, __version__ attribute is a tuple + # from v2.0.0, __version__ is a string and VERSION a tuple + version = getattr(rediscluster, "VERSION", rediscluster.__version__) + + # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 + # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst + if (0, 2, 0) < version < (2, 0, 0): + pipeline_cls = rediscluster.pipeline.StrictClusterPipeline + patch_redis_client( + rediscluster.StrictRedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + else: + pipeline_cls = rediscluster.pipeline.ClusterPipeline + + patch_redis_pipeline( + pipeline_cls, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py new file mode 100644 index 0000000000..7e04df9c69 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py @@ -0,0 +1,159 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations.redis.consts import ( + _COMMANDS_INCLUDING_SENSITIVE_DATA, + _MAX_NUM_ARGS, + _MAX_NUM_COMMANDS, + _MULTI_KEY_COMMANDS, + _SINGLE_KEY_COMMANDS, +) +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + +if TYPE_CHECKING: + from typing import Any, Optional, Sequence, Union + + +def _get_safe_command(name: str, args: "Sequence[Any]") -> str: + command_parts = [name] + + name_low = name.lower() + send_default_pii = should_send_default_pii() + + for i, arg in enumerate(args): + if i > _MAX_NUM_ARGS: + break + + if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + continue + + arg_is_the_key = i == 0 + if arg_is_the_key: + command_parts.append(repr(arg)) + else: + if send_default_pii: + command_parts.append(repr(arg)) + else: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + + command = " ".join(command_parts) + return command + + +def _safe_decode(key: "Any") -> str: + if isinstance(key, bytes): + try: + return key.decode() + except UnicodeDecodeError: + return "" + + return str(key) + + +def _key_as_string(key: "Any") -> str: + if isinstance(key, (dict, list, tuple)): + key = ", ".join(_safe_decode(x) for x in key) + elif isinstance(key, bytes): + key = _safe_decode(key) + elif key is None: + key = "" + else: + key = str(key) + + return key + + +def _get_safe_key( + method_name: str, + args: "Optional[tuple[Any, ...]]", + kwargs: "Optional[dict[str, Any]]", +) -> "Optional[tuple[str, ...]]": + """ + Gets the key (or keys) from the given method_name. + The method_name could be a redis command or a django caching command + """ + key = None + + if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: + # for example redis "mget" + key = tuple(args) + + elif args is not None and len(args) >= 1: + # for example django "set_many/get_many" or redis "get" + if isinstance(args[0], (dict, list, tuple)): + key = tuple(args[0]) + else: + key = (args[0],) + + elif kwargs is not None and "key" in kwargs: + # this is a legacy case for older versions of Django + if isinstance(kwargs["key"], (list, tuple)): + if len(kwargs["key"]) > 0: + key = tuple(kwargs["key"]) + else: + if kwargs["key"] is not None: + key = (kwargs["key"],) + + return key + + +def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": + return command.args + + +def _set_pipeline_data( + span: "Union[Span, StreamedSpan]", + is_cluster: bool, + get_command_args_fn: "Any", + is_transaction: bool, + commands_seq: "Sequence[Any]", +) -> None: + # TODO: Remove this whole function when removing transaction based tracing + if isinstance(span, StreamedSpan): + return + + span.set_tag("redis.is_cluster", is_cluster) + span.set_tag("redis.transaction", is_transaction) + + commands = [] + for i, arg in enumerate(commands_seq): + if i >= _MAX_NUM_COMMANDS: + break + + command = get_command_args_fn(arg) + commands.append(_get_safe_command(command[0], command[1:])) + + span.set_data( + "redis.commands", + { + "count": len(commands_seq), + "first_ten": commands, + }, + ) + + +def _set_client_data( + span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" +) -> None: + if isinstance(span, StreamedSpan): + if name: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) + else: + span.set_tag("redis.is_cluster", is_cluster) + if name: + span.set_tag("redis.command", name) + span.set_tag(SPANDATA.DB_OPERATION, name) + + if name and args: + name_low = name.lower() + if (name_low in _SINGLE_KEY_COMMANDS) or ( + name_low in _MULTI_KEY_COMMANDS and len(args) == 1 + ): + if isinstance(span, StreamedSpan): + span.set_attribute("db.redis.key", args[0]) + else: + span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py new file mode 100644 index 0000000000..edd48a9f67 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py @@ -0,0 +1,225 @@ +import functools +import weakref + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + format_timestamp, + parse_version, +) + +try: + from rq.job import JobStatus + from rq.queue import Queue + from rq.timeouts import JobTimeoutException + from rq.version import VERSION as RQ_VERSION + from rq.worker import Worker +except ImportError: + raise DidNotEnable("RQ not installed") + +try: + from rq.worker import BaseWorker + + if not hasattr(BaseWorker, "perform_job"): + BaseWorker = None +except ImportError: + BaseWorker = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + from rq.job import Job + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + +class RqIntegration(Integration): + identifier = "rq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(RQ_VERSION) + _check_minimum_version(RqIntegration, version) + + # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly + # instead of Worker, so we need to patch BaseWorker to cover both. + # For older versions where BaseWorker doesn't exist or doesn't have + # perform_job, we patch Worker. + worker_cls = BaseWorker if BaseWorker is not None else Worker + + old_perform_job = worker_cls.perform_job + + @functools.wraps(old_perform_job) + def sentry_patched_perform_job( + self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" + ) -> bool: + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_perform_job(self, job, *args, **kwargs) + + with sentry_sdk.new_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(weakref.ref(job))) + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace( + job.meta.get("_sentry_trace_headers") or {} + ) + + Scope.set_custom_sampling_context({"rq_job": job}) + + func_name = None + with capture_internal_exceptions(): + func_name = job.func_name + + with sentry_sdk.traces.start_span( + name="unknown RQ task" if func_name is None else func_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RQ, + "sentry.origin": RqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job.id, + }, + parent_span=None, + ) as span: + if func_name is not None: + span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) + + rv = old_perform_job(self, job, *args, **kwargs) + else: + transaction = continue_trace( + job.meta.get("_sentry_trace_headers") or {}, + op=OP.QUEUE_TASK_RQ, + name="unknown RQ task", + source=TransactionSource.TASK, + origin=RqIntegration.origin, + ) + + with capture_internal_exceptions(): + transaction.name = job.func_name + + with sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"rq_job": job}, + ): + rv = old_perform_job(self, job, *args, **kwargs) + + if self.is_horse: + # We're inside of a forked process and RQ is + # about to call `os._exit`. Make sure that our + # events get sent out. + sentry_sdk.get_client().flush() + + return rv + + worker_cls.perform_job = sentry_patched_perform_job + + old_handle_exception = worker_cls.handle_exception + + def sentry_patched_handle_exception( + self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" + ) -> "Any": + retry = ( + hasattr(job, "retries_left") + and job.retries_left + and job.retries_left > 0 + ) + failed = job._status == JobStatus.FAILED or job.is_failed + if failed and not retry: + _capture_exception(exc_info) + + return old_handle_exception(self, job, *exc_info, **kwargs) + + worker_cls.handle_exception = sentry_patched_handle_exception + + old_enqueue_job = Queue.enqueue_job + + @functools.wraps(old_enqueue_job) + def sentry_patched_enqueue_job( + self: "Queue", job: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_enqueue_job(self, job, **kwargs) + + scope = sentry_sdk.get_current_scope() + span = ( + scope.streamed_span + if has_span_streaming_enabled(client.options) + else scope.span + ) + if span is not None: + job.meta["_sentry_trace_headers"] = dict( + scope.iter_trace_propagation_headers() + ) + + return old_enqueue_job(self, job, **kwargs) + + Queue.enqueue_job = sentry_patched_enqueue_job + + ignore_logger("rq.worker") + + +def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + job = weak_job() + if job is not None: + with capture_internal_exceptions(): + extra = event.setdefault("extra", {}) + rq_job = { + "job_id": job.id, + "func": job.func_name, + "args": ( + job.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + job.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "description": job.description, + } + + if job.enqueued_at: + rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) + if job.started_at: + rq_job["started_at"] = format_timestamp(job.started_at) + + extra["rq-job"] = rq_job + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], JobTimeoutException): + event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "rq", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py new file mode 100644 index 0000000000..622e3c17af --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py @@ -0,0 +1,300 @@ +""" +This integration ingests tracing data from native extensions written in Rust. + +Using it requires additional setup on the Rust side to accept a +`RustTracingLayer` Python object and register it with the `tracing-subscriber` +using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: +```rust +#[pyfunction] +pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { + tracing_subscriber::registry() + .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) + .init(); +} +``` + +Usage in Python would then look like: +``` +sentry_sdk.init( + dsn=sentry_dsn, + integrations=[ + RustTracingIntegration( + "demo_rust_extension", + demo_rust_extension.initialize_tracing, + event_type_mapping=event_type_mapping, + ) + ], +) +``` + +Each native extension requires its own integration. +""" + +import json +from enum import Enum, auto +from typing import Any, Callable, Dict, Optional, Union + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + + +class RustTracingLevel(Enum): + Trace = "TRACE" + Debug = "DEBUG" + Info = "INFO" + Warn = "WARN" + Error = "ERROR" + + +class EventTypeMapping(Enum): + Ignore = auto() + Exc = auto() + Breadcrumb = auto() + Event = auto() + + +def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": + level = RustTracingLevel(level) + if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): + return "debug" + elif level == RustTracingLevel.Info: + return "info" + elif level == RustTracingLevel.Warn: + return "warning" + elif level == RustTracingLevel.Error: + return "error" + else: + # Better this than crashing + return "info" + + +def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": + metadata = event.get("metadata", {}) + contexts = {} + + location = {} + for field in ["module_path", "file", "line"]: + if field in metadata: + location[field] = metadata[field] + if len(location) > 0: + contexts["rust_tracing_location"] = location + + fields = {} + for field in metadata.get("fields", []): + fields[field] = event.get(field) + if len(fields) > 0: + contexts["rust_tracing_fields"] = fields + + return contexts + + +def process_event(event: "Dict[str, Any]") -> None: + metadata = event.get("metadata", {}) + + logger = metadata.get("target") + level = tracing_level_to_sentry_level(metadata.get("level")) + message: "sentry_sdk._types.Any" = event.get("message") + contexts = extract_contexts(event) + + sentry_event: "sentry_sdk._types.Event" = { + "logger": logger, + "level": level, + "message": message, + "contexts": contexts, + } + + sentry_sdk.capture_event(sentry_event) + + +def process_exception(event: "Dict[str, Any]") -> None: + process_event(event) + + +def process_breadcrumb(event: "Dict[str, Any]") -> None: + level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) + message = event.get("message") + + sentry_sdk.add_breadcrumb(level=level, message=message) + + +def default_span_filter(metadata: "Dict[str, Any]") -> bool: + return RustTracingLevel(metadata.get("level")) in ( + RustTracingLevel.Error, + RustTracingLevel.Warn, + RustTracingLevel.Info, + ) + + +def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": + level = RustTracingLevel(metadata.get("level")) + if level == RustTracingLevel.Error: + return EventTypeMapping.Exc + elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): + return EventTypeMapping.Breadcrumb + elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): + return EventTypeMapping.Ignore + else: + return EventTypeMapping.Ignore + + +class RustTracingLayer: + def __init__( + self, + origin: str, + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.origin = origin + self.event_type_mapping = event_type_mapping + self.span_filter = span_filter + self.include_tracing_fields = include_tracing_fields + + def _include_tracing_fields(self) -> bool: + """ + By default, the values of tracing fields are not included in case they + contain PII. A user may override that by passing `True` for the + `include_tracing_fields` keyword argument of this integration or by + setting `send_default_pii` to `True` in their Sentry client options. + """ + return ( + should_send_default_pii() + if self.include_tracing_fields is None + else self.include_tracing_fields + ) + + def on_event(self, event: str, sentry_span: "SentrySpan") -> None: + deserialized_event = json.loads(event) + metadata = deserialized_event.get("metadata", {}) + + event_type = self.event_type_mapping(metadata) + if event_type == EventTypeMapping.Ignore: + return + elif event_type == EventTypeMapping.Exc: + process_exception(deserialized_event) + elif event_type == EventTypeMapping.Breadcrumb: + process_breadcrumb(deserialized_event) + elif event_type == EventTypeMapping.Event: + process_event(deserialized_event) + + def on_new_span( + self, attrs: str, span_id: str + ) -> "Optional[Union[SentrySpan, StreamedSpan]]": + attrs = json.loads(attrs) + metadata = attrs.get("metadata", {}) + + if not self.span_filter(metadata): + return None + + module_path = metadata.get("module_path") + name = metadata.get("name") + message = attrs.get("message") + + if message is not None: + sentry_span_name = message + elif module_path is not None and name is not None: + sentry_span_name = f"{module_path}::{name}" # noqa: E231 + elif name is not None: + sentry_span_name = name + else: + sentry_span_name = "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + sentry_span = sentry_sdk.traces.start_span( + name=sentry_span_name, + attributes={ + "sentry.op": "function", + "sentry.origin": self.origin, + }, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_attribute(field, attrs.get(field)) + else: + sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) + + return sentry_span + + sentry_span = sentry_sdk.start_span( + op="function", + name=sentry_span_name, + origin=self.origin, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_data(field, attrs.get(field)) + else: + sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) + + sentry_span.__enter__() + return sentry_span + + def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: + if sentry_span is None: + return + + sentry_span.__exit__(None, None, None) + + def on_record( + self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" + ) -> None: + if sentry_span is None: + return + + set_on_span = ( + sentry_span.set_attribute + if isinstance(sentry_span, StreamedSpan) + else sentry_span.set_data + ) + + deserialized_values = json.loads(values) + for key, value in deserialized_values.items(): + if self._include_tracing_fields(): + set_on_span(key, value) + else: + set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) + + +class RustTracingIntegration(Integration): + """ + Ingests tracing data from a Rust native extension's `tracing` instrumentation. + + If a project uses more than one Rust native extension, each one will need + its own instance of `RustTracingIntegration` with an initializer function + specific to that extension. + + Since all of the setup for this integration requires instance-specific state + which is not available in `setup_once()`, setup instead happens in `__init__()`. + """ + + def __init__( + self, + identifier: str, + initializer: "Callable[[RustTracingLayer], None]", + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.identifier = identifier + origin = f"auto.function.rust_tracing.{identifier}" + self.tracing_layer = RustTracingLayer( + origin, event_type_mapping, span_filter, include_tracing_fields + ) + + initializer(self.tracing_layer) + + @staticmethod + def setup_once() -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py new file mode 100644 index 0000000000..908fceb0cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py @@ -0,0 +1,431 @@ +import sys +import warnings +import weakref +from inspect import isawaitable +from typing import TYPE_CHECKING +from urllib.parse import urlsplit + +import sentry_sdk +from sentry_sdk import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +if TYPE_CHECKING: + from collections.abc import Container + from typing import Any, Callable, Dict, Optional, Union + + from sanic.request import Request, RequestParameters + from sanic.response import BaseHTTPResponse + from sanic.router import Route + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +try: + from sanic import Sanic + from sanic import __version__ as SANIC_VERSION + from sanic.exceptions import SanicException + from sanic.handlers import ErrorHandler + from sanic.router import Router +except ImportError: + raise DidNotEnable("Sanic not installed") + +old_error_handler_lookup = ErrorHandler.lookup +old_handle_request = Sanic.handle_request +old_router_get = Router.get + +try: + # This method was introduced in Sanic v21.9 + old_startup = Sanic._startup +except AttributeError: + pass + + +class SanicIntegration(Integration): + identifier = "sanic" + origin = f"auto.http.{identifier}" + version: "Optional[tuple[int, ...]]" = None + + def __init__( + self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) + ) -> None: + """ + The unsampled_statuses parameter can be used to specify for which HTTP statuses the + transactions should not be sent to Sentry. By default, transactions are sent for all + HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all + HTTP statuses, including 404. + """ + self._unsampled_statuses = unsampled_statuses or set() + + @staticmethod + def setup_once() -> None: + SanicIntegration.version = parse_version(SANIC_VERSION) + _check_minimum_version(SanicIntegration, SanicIntegration.version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The sanic integration for Sentry requires Python 3.7+ " + " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + if SANIC_VERSION.startswith("0.8."): + # Sanic 0.8 and older creates a logger named "root" and puts a + # stringified version of every exception in there (without exc_info), + # which our error deduplication can't detect. + # + # We explicitly check the version here because it is a very + # invasive step to ignore this logger and not necessary in newer + # versions at all. + # + # https://github.com/huge-success/sanic/issues/1332 + ignore_logger("root") + + if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): + _setup_legacy_sanic() + return + + _setup_sanic() + + +class SanicRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return dict(self.request.cookies) + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "RequestParameters": + return self.request.form + + def is_json(self) -> bool: + raise NotImplementedError() + + def json(self) -> "Optional[Any]": + return self.request.json + + def files(self) -> "RequestParameters": + return self.request.files + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) + + +def _setup_sanic() -> None: + Sanic._startup = _startup + ErrorHandler.lookup = _sentry_error_handler_lookup + + +def _setup_legacy_sanic() -> None: + Sanic.handle_request = _legacy_handle_request + Router.get = _legacy_router_get + ErrorHandler.lookup = _sentry_error_handler_lookup + + +async def _startup(self: "Sanic") -> None: + # This happens about as early in the lifecycle as possible, just after the + # Request object is created. The body has not yet been consumed. + self.signal("http.lifecycle.request")(_context_enter) + + # This happens after the handler is complete. In v21.9 this signal is not + # dispatched when there is an exception. Therefore we need to close out + # and call _context_exit from the custom exception handler as well. + # See https://github.com/sanic-org/sanic/issues/2297 + self.signal("http.lifecycle.response")(_context_exit) + + # This happens inside of request handling immediately after the route + # has been identified by the router. + self.signal("http.routing.after")(_set_transaction) + + # The above signals need to be declared before this can be called. + await old_startup(self) + + +async def _context_enter(request: "Request") -> None: + request.ctx._sentry_do_integration = ( + sentry_sdk.get_client().get_integration(SanicIntegration) is not None + ) + + if not request.ctx._sentry_do_integration: + return + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + weak_request = weakref.ref(request) + request.ctx._sentry_scope = sentry_sdk.isolation_scope() + scope = request.ctx._sentry_scope.__enter__() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + if is_span_streaming_enabled: + integration = client.get_integration(SanicIntegration) + if ( + isinstance(integration, SanicIntegration) + and integration._unsampled_statuses + ): + warnings.warn( + "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", + stacklevel=2, + ) + + sentry_sdk.traces.continue_trace(dict(request.headers)) + scope.set_custom_sampling_context({"sanic_request": request}) + + if should_send_default_pii() and request.remote_addr: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + + span = sentry_sdk.traces.start_span( + # Unless the request results in a 404 error, the name and source + # will get overwritten in _set_transaction + name=request.path, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": SanicIntegration.origin, + "sentry.span.source": SegmentSource.URL.value, + }, + parent_span=None, + ) + request.ctx._sentry_root_span = span + else: + transaction = continue_trace( + dict(request.headers), + op=OP.HTTP_SERVER, + # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction + name=request.path, + source=TransactionSource.URL, + origin=SanicIntegration.origin, + ) + request.ctx._sentry_root_span = sentry_sdk.start_transaction( + transaction + ).__enter__() + + +async def _context_exit( + request: "Request", response: "Optional[BaseHTTPResponse]" = None +) -> None: + with capture_internal_exceptions(): + if not request.ctx._sentry_do_integration: + return + + integration = sentry_sdk.get_client().get_integration(SanicIntegration) + + response_status = None if response is None else response.status + + # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception + # happens while trying to end the transaction, we still attempt to exit the hub. + with capture_internal_exceptions(): + span = request.ctx._sentry_root_span + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes(request).items(): + span.set_attribute(attr, value) + if response_status is not None: + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) + span.status = "error" if response_status >= 400 else "ok" + + span.end() + + else: + span.set_http_status(response_status) + span.sampled &= ( + isinstance(integration, SanicIntegration) + and response_status not in integration._unsampled_statuses + ) + + span.__exit__(None, None, None) + + request.ctx._sentry_scope.__exit__(None, None, None) + + +async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: + if request.ctx._sentry_do_integration: + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + route_name = route.name.replace(request.app.name, "").strip(".") + scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) + + +def _sentry_error_handler_lookup( + self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" +) -> "Optional[object]": + _capture_exception(exception) + old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) + + if old_error_handler is None: + return None + + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return old_error_handler + + async def sentry_wrapped_error_handler( + request: "Request", exception: Exception + ) -> "Any": + try: + response = old_error_handler(request, exception) + if isawaitable(response): + response = await response + return response + except Exception: + # Report errors that occur in Sanic error handler. These + # exceptions will not even show up in Sanic's + # `sanic.exceptions` logger. + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + finally: + # As mentioned in previous comment in _startup, this can be removed + # after https://github.com/sanic-org/sanic/issues/2297 is resolved + if SanicIntegration.version and SanicIntegration.version == (21, 9): + await _context_exit(request) + + return sentry_wrapped_error_handler + + +async def _legacy_handle_request( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" +) -> "Any": + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return await old_handle_request(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + response = old_handle_request(self, request, *args, **kwargs) + if isawaitable(response): + response = await response + + return response + + +def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": + rv = old_router_get(self, *args) + if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: + with capture_internal_exceptions(): + scope = sentry_sdk.get_isolation_scope() + if SanicIntegration.version and SanicIntegration.version >= (21, 3): + # Sanic versions above and including 21.3 append the app name to the + # route name, and so we need to remove it from Route name so the + # transaction name is consistent across all versions + sanic_app_name = self.ctx.app.name + sanic_route = rv[0].name + + if sanic_route.startswith("%s." % sanic_app_name): + # We add a 1 to the len of the sanic_app_name because there is a dot + # that joins app name and the route name + # Format: app_name.route_name + sanic_route = sanic_route[len(sanic_app_name) + 1 :] + + scope.set_transaction_name( + sanic_route, source=TransactionSource.COMPONENT + ) + else: + scope.set_transaction_name( + rv[0].__name__, source=TransactionSource.COMPONENT + ) + + return rv + + +@ensure_integration_enabled(SanicIntegration) +def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "sanic", "handled": False}, + ) + + if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: + return + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_request_attributes(request: "Request") -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from a Sanic request. + """ + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + urlparts = urlsplit(request.url) + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.url + attributes["url.path"] = urlparts.path + + if urlparts.query: + attributes[SPANDATA.HTTP_QUERY] = urlparts.query + + if urlparts.scheme: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme + + if should_send_default_pii() and request.remote_addr: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr + + return attributes + + +def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": + def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + try: + if hint and issubclass(hint["exc_info"][0], SanicException): + return None + except KeyError: + pass + + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + extractor = SanicRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + urlparts = urlsplit(request.url) + + request_info["url"] = "%s://%s%s" % ( + urlparts.scheme, + urlparts.netloc, + urlparts.path, + ) + + request_info["query_string"] = urlparts.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_addr} + request_info["headers"] = _filter_headers(dict(request.headers)) + + return event + + return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py new file mode 100644 index 0000000000..16f91b28ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py @@ -0,0 +1,65 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import event_from_exception, reraise + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union, overload + + F = TypeVar("F", bound=Callable[..., Any]) + +else: + + def overload(x: "F") -> "F": + return x + + +@overload +def serverless_function(f: "F", flush: bool = True) -> "F": + pass + + +@overload +def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 + pass + + +def serverless_function( # noqa + f: "Optional[F]" = None, flush: bool = True +) -> "Union[F, Callable[[F], F]]": + def wrapper(f: "F") -> "F": + @wraps(f) + def inner(*args: "Any", **kwargs: "Any") -> "Any": + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + + try: + return f(*args, **kwargs) + except Exception: + _capture_and_reraise() + finally: + if flush: + sentry_sdk.flush() + + return inner # type: ignore + + if f is None: + return wrapper + else: + return wrapper(f) + + +def _capture_and_reraise() -> None: + exc_info = sys.exc_info() + client = sentry_sdk.get_client() + if client.is_active(): + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "serverless", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py new file mode 100644 index 0000000000..775170fb9f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py @@ -0,0 +1,142 @@ +import socket + +import sentry_sdk +from sentry_sdk._types import MYPY +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if MYPY: + from socket import AddressFamily, SocketKind + from typing import List, Optional, Tuple, Union + +__all__ = ["SocketIntegration"] + + +class SocketIntegration(Integration): + identifier = "socket" + origin = f"auto.socket.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) + """ + _patch_create_connection() + _patch_getaddrinfo() + + +def _get_span_description( + host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" +) -> str: + try: + host = host.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + try: + port = port.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + description = "%s:%s" % (host, port) # type: ignore + return description + + +def _patch_create_connection() -> None: + real_create_connection = socket.create_connection + + def create_connection( + address: "Tuple[Optional[str], int]", + timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore + source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, + ) -> "socket.socket": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_create_connection(address, timeout, source_address) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(address[0], address[1]), + attributes={ + "sentry.op": OP.SOCKET_CONNECTION, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if address[0] is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) + span.set_attribute(SPANDATA.SERVER_PORT, address[1]) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_CONNECTION, + name=_get_span_description(address[0], address[1]), + origin=SocketIntegration.origin, + ) as span: + span.set_data("address", address) + span.set_data("timeout", timeout) + span.set_data("source_address", source_address) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + + socket.create_connection = create_connection # type: ignore + + +def _patch_getaddrinfo() -> None: + real_getaddrinfo = socket.getaddrinfo + + def getaddrinfo( + host: "Union[bytes, str, None]", + port: "Union[bytes, str, int, None]", + family: int = 0, + type: int = 0, + proto: int = 0, + flags: int = 0, + ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_getaddrinfo(host, port, family, type, proto, flags) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(host, port), + attributes={ + "sentry.op": OP.SOCKET_DNS, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if isinstance(host, str): + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + elif isinstance(host, bytes): + span.set_attribute( + SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") + ) + + if isinstance(port, int): + span.set_attribute(SPANDATA.SERVER_PORT, port) + elif port is not None: + try: + span.set_attribute(SPANDATA.SERVER_PORT, int(port)) + except (ValueError, TypeError): + pass + + return real_getaddrinfo(host, port, family, type, proto, flags) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_DNS, + name=_get_span_description(host, port), + origin=SocketIntegration.origin, + ) as span: + span.set_data("host", host) + span.set_data("port", port) + + return real_getaddrinfo(host, port, family, type, proto, flags) + + socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py new file mode 100644 index 0000000000..10d94163c5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py @@ -0,0 +1,4 @@ +from sentry_sdk.integrations.spark.spark_driver import SparkIntegration +from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration + +__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py new file mode 100644 index 0000000000..a83532b6a6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Optional + + from pyspark import SparkContext + + from sentry_sdk._types import Event, Hint + + +class SparkIntegration(Integration): + identifier = "spark" + + @staticmethod + def setup_once() -> None: + _setup_sentry_tracing() + + +def _set_app_properties() -> None: + """ + Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. + This allows worker integration to have access to app_name and application_id. + """ + from pyspark import SparkContext + + spark_context = SparkContext._active_spark_context + if spark_context: + spark_context.setLocalProperty( + "sentry_app_name", + spark_context.appName, + ) + spark_context.setLocalProperty( + "sentry_application_id", + spark_context.applicationId, + ) + + +def _start_sentry_listener(sc: "SparkContext") -> None: + """ + Start java gateway server to add custom `SparkListener` + """ + from pyspark.java_gateway import ensure_callback_server_started + + gw = sc._gateway + ensure_callback_server_started(gw) + listener = SentryListener() + sc._jsc.sc().addSparkListener(listener) + + +def _add_event_processor(sc: "SparkContext") -> None: + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + if sentry_sdk.get_client().get_integration(SparkIntegration) is None: + return event + + if sc._active_spark_context is None: + return event + + event.setdefault("user", {}).setdefault("id", sc.sparkUser()) + + event.setdefault("tags", {}).setdefault( + "executor.id", sc._conf.get("spark.executor.id") + ) + event["tags"].setdefault( + "spark-submit.deployMode", + sc._conf.get("spark.submit.deployMode"), + ) + event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) + event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) + event["tags"].setdefault("spark_version", sc.version) + event["tags"].setdefault("app_name", sc.appName) + event["tags"].setdefault("application_id", sc.applicationId) + event["tags"].setdefault("master", sc.master) + event["tags"].setdefault("spark_home", sc.sparkHome) + + event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) + + return event + + +def _activate_integration(sc: "SparkContext") -> None: + _start_sentry_listener(sc) + _set_app_properties() + _add_event_processor(sc) + + +def _patch_spark_context_init() -> None: + from pyspark import SparkContext + + spark_context_init = SparkContext._do_init + + @ensure_integration_enabled(SparkIntegration, spark_context_init) + def _sentry_patched_spark_context_init( + self: "SparkContext", *args: "Any", **kwargs: "Any" + ) -> "Optional[Any]": + rv = spark_context_init(self, *args, **kwargs) + _activate_integration(self) + return rv + + SparkContext._do_init = _sentry_patched_spark_context_init + + +def _setup_sentry_tracing() -> None: + from pyspark import SparkContext + + if SparkContext._active_spark_context is not None: + _activate_integration(SparkContext._active_spark_context) + return + _patch_spark_context_init() + + +class SparkListener: + def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 + pass + + def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklistedForStage( # noqa: N802 + self, + executorBlacklistedForStage: "Any", # noqa: N803 + ) -> None: + pass + + def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 + pass + + def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 + pass + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + pass + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 + pass + + def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 + pass + + class Java: + implements = ["org.apache.spark.scheduler.SparkListenerInterface"] + + +class SentryListener(SparkListener): + def _add_breadcrumb( + self, + level: str, + message: str, + data: "Optional[dict[str, Any]]" = None, + ) -> None: + sentry_sdk.get_isolation_scope().add_breadcrumb( + level=level, message=message, data=data + ) + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + message = "Job {} Started".format(jobStart.jobId()) + self._add_breadcrumb(level="info", message=message) + _set_app_properties() + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + level = "" + message = "" + data = {"result": jobEnd.jobResult().toString()} + + if jobEnd.jobResult().toString() == "JobSucceeded": + level = "info" + message = "Job {} Ended".format(jobEnd.jobId()) + else: + level = "warning" + message = "Job {} Failed".format(jobEnd.jobId()) + + self._add_breadcrumb(level=level, message=message, data=data) + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + stage_info = stageSubmitted.stageInfo() + message = "Stage {} Submitted".format(stage_info.stageId()) + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + self._add_breadcrumb(level="info", message=message, data=data) + _set_app_properties() + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + from py4j.protocol import Py4JJavaError # type: ignore + + stage_info = stageCompleted.stageInfo() + message = "" + level = "" + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + # Have to Try Except because stageInfo.failureReason() is typed with Scala Option + try: + data["reason"] = stage_info.failureReason().get() + message = "Stage {} Failed".format(stage_info.stageId()) + level = "warning" + except Py4JJavaError: + message = "Stage {} Completed".format(stage_info.stageId()) + level = "info" + + self._add_breadcrumb(level=level, message=message, data=data) + + +def _get_attempt_id(stage_info: "Any") -> "Optional[int]": + try: + return stage_info.attemptId() + except Exception: + pass + + try: + return stage_info.attemptNumber() + except Exception: + pass + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py new file mode 100644 index 0000000000..5906472748 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py @@ -0,0 +1,109 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_hint_with_exc_info, + exc_info_from_error, + single_exception_from_error_tuple, + walk_exception_chain, +) + +if TYPE_CHECKING: + from typing import Any, Optional + + from sentry_sdk._types import Event, ExcInfo, Hint + + +class SparkWorkerIntegration(Integration): + identifier = "spark_worker" + + @staticmethod + def setup_once() -> None: + import pyspark.daemon as original_daemon + + original_daemon.worker_main = _sentry_worker_main + + +def _capture_exception(exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + + mechanism = {"type": "spark", "handled": False} + + exc_info = exc_info_from_error(exc_info) + + exc_type, exc_value, tb = exc_info + rv = [] + + # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + if exc_type not in (SystemExit, EOFError, ConnectionResetError): + rv.append( + single_exception_from_error_tuple( + exc_type, exc_value, tb, client.options, mechanism + ) + ) + + if rv: + rv.reverse() + hint = event_hint_with_exc_info(exc_info) + event: "Event" = {"level": "error", "exception": {"values": rv}} + + _tag_task_context() + + sentry_sdk.capture_event(event, hint=hint) + + +def _tag_task_context() -> None: + from pyspark.taskcontext import TaskContext + + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + integration = sentry_sdk.get_client().get_integration( + SparkWorkerIntegration + ) + task_context = TaskContext.get() + + if integration is None or task_context is None: + return event + + event.setdefault("tags", {}).setdefault( + "stageId", str(task_context.stageId()) + ) + event["tags"].setdefault("partitionId", str(task_context.partitionId())) + event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) + event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) + + if task_context._localProperties: + if "sentry_app_name" in task_context._localProperties: + event["tags"].setdefault( + "app_name", task_context._localProperties["sentry_app_name"] + ) + event["tags"].setdefault( + "application_id", + task_context._localProperties["sentry_application_id"], + ) + + if "callSite.short" in task_context._localProperties: + event.setdefault("extra", {}).setdefault( + "callSite", task_context._localProperties["callSite.short"] + ) + + return event + + +def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: + import pyspark.worker as original_worker + + try: + original_worker.main(*args, **kwargs) + except SystemExit: + if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py new file mode 100644 index 0000000000..962fe4ee53 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py @@ -0,0 +1,185 @@ +from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + parse_version, +) + +try: + from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore + from sqlalchemy.engine import Engine # type: ignore + from sqlalchemy.event import listen # type: ignore +except ImportError: + raise DidNotEnable("SQLAlchemy not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional, Union + + +class SqlalchemyIntegration(Integration): + identifier = "sqlalchemy" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(SQLALCHEMY_VERSION) + _check_minimum_version(SqlalchemyIntegration, version) + + listen(Engine, "before_cursor_execute", _before_cursor_execute) + listen(Engine, "after_cursor_execute", _after_cursor_execute) + listen(Engine, "handle_error", _handle_error) + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _before_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + executemany: bool, + *args: "Any", +) -> None: + ctx_mgr = record_sql_queries( + cursor, + statement, + parameters, + paramstyle=context and context.dialect and context.dialect.paramstyle or None, + executemany=executemany, + span_origin=SqlalchemyIntegration.origin, + ) + context._sentry_sql_span_manager = ctx_mgr + + span = ctx_mgr.__enter__() + + if span is not None: + _set_db_data(span, conn) + context._sentry_sql_span = span + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _after_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + *args: "Any", +) -> None: + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + context, "_sentry_sql_span_manager", None + ) + + # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. + span: "Optional[Union[Span, StreamedSpan]]" = getattr( + context, "_sentry_sql_span", None + ) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if ctx_mgr is not None: + context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + if isinstance(span, Span): + with capture_internal_exceptions(): + add_query_source(span) + + +def _handle_error(context: "Any", *args: "Any") -> None: + execution_context = context.execution_context + if execution_context is None: + return + + span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) + + if span is not None: + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # _after_cursor_execute does not get called for crashing SQL stmts. Judging + # from SQLAlchemy codebase it does seem like any error coming into this + # handler is going to be fatal. + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + execution_context, "_sentry_sql_span_manager", None + ) + + if ctx_mgr is not None: + execution_context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + +# See: https://docs.sqlalchemy.org/en/20/dialects/index.html +def _get_db_system(name: str) -> "Optional[str]": + name = str(name) + + if "sqlite" in name: + return "sqlite" + + if "postgres" in name: + return "postgresql" + + if "mariadb" in name: + return "mariadb" + + if "mysql" in name: + return "mysql" + + if "oracle" in name: + return "oracle" + + return None + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + db_system = _get_db_system(conn.engine.name) + + if isinstance(span, StreamedSpan): + if db_system is not None: + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) + else: + if db_system is not None: + span.set_data(SPANDATA.DB_SYSTEM, db_system) + + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + try: + driver = conn.dialect.driver + if driver: + set_on_span(SPANDATA.DB_DRIVER_NAME, driver) + except Exception: + pass + + if conn.engine.url is None: + return + + db_name = conn.engine.url.database + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + server_address = conn.engine.url.host + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = conn.engine.url.port + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py new file mode 100644 index 0000000000..3f9fbf4d8e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py @@ -0,0 +1,858 @@ +import functools +import json +import sys +import warnings +from collections.abc import Set +from copy import deepcopy +from json import JSONDecodeError +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations._asgi_common import _RootPathInPath +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + HttpCodeRangeContainer, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Awaitable, + Callable, + Container, + Dict, + Optional, + Tuple, + Union, + ) + + from sentry_sdk._types import Event, HttpStatusCodeRange +try: + import starlette + from starlette import __version__ as STARLETTE_VERSION + from starlette.applications import Starlette + from starlette.datastructures import ( + UploadFile, + ) + from starlette.middleware import Middleware + from starlette.middleware.authentication import ( + AuthenticationMiddleware, + ) + from starlette.requests import Request + from starlette.routing import Match + from starlette.types import ASGIApp, Receive, Send + from starlette.types import Scope as StarletteScope +except ImportError: + raise DidNotEnable("Starlette is not installed") + +try: + # Starlette 0.20 + from starlette.middleware.exceptions import ExceptionMiddleware +except ImportError: + # Startlette 0.19.1 + from starlette.exceptions import ExceptionMiddleware # type: ignore + +try: + # Optional dependency of Starlette to parse form data. + try: + # python-multipart 0.0.13 and later + import python_multipart as multipart + except ImportError: + # python-multipart 0.0.12 and earlier + import multipart # type: ignore +except ImportError: + multipart = None # type: ignore[assignment] + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +_DEFAULT_TRANSACTION_NAME = "generic Starlette request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class StarletteIntegration(Integration): + identifier = "starlette" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "url", + failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + middleware_spans: bool = False, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ): + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + if isinstance(failed_request_status_codes, Set): + self.failed_request_status_codes: "Container[int]" = ( + failed_request_status_codes + ) + else: + warnings.warn( + "Passing a list or None for failed_request_status_codes is deprecated. " + "Please pass a set of int instead.", + DeprecationWarning, + stacklevel=2, + ) + + if failed_request_status_codes is None: + self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES + else: + self.failed_request_status_codes = HttpCodeRangeContainer( + failed_request_status_codes + ) + + @staticmethod + def setup_once() -> None: + version = parse_version(STARLETTE_VERSION) + + if version is None: + raise DidNotEnable( + "Unparsable Starlette version: {}".format(STARLETTE_VERSION) + ) + + patch_middlewares() + # Starlette tolerates both starting with: + # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. + root_path_in_path = ( + _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED + ) + patch_asgi_app(root_path_in_path=root_path_in_path) + patch_request_response() + + if version >= (0, 24): + patch_templates() + + +def _enable_span_for_middleware( + middleware_class: "Any", +) -> "Any": + old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ + + async def _create_span_call( + app: "Any", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + **kwargs: "Any", + ) -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await old_call(app, scope, receive, send, **kwargs) + + # Update transaction name with middleware name + name, source = _get_transaction_from_middleware(app, scope, integration) + + if name is not None: + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=source, + ) + + if not integration.middleware_spans: + return await old_call(app, scope, receive, send, **kwargs) + + middleware_name = app.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarletteIntegration.origin, + "middleware.name": middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarletteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlette.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await send(*args, **kwargs) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(app, scope, new_receive, new_send, **kwargs) + + not_yet_patched = old_call.__name__ not in [ + "_create_span_call", + "_sentry_authenticationmiddleware_call", + "_sentry_exceptionmiddleware_call", + ] + + if not_yet_patched: + middleware_class.__call__ = _create_span_call + + return middleware_class + + +def _serialize_request_body_data(data: "Any") -> str: + # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values + def _default(value: "Any") -> "Any": + if isinstance(value, AnnotatedValue): + return value.value + return str(value) + + return json.dumps(data, default=_default) + + +@ensure_integration_enabled(StarletteIntegration) +def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarletteIntegration.identifier, "handled": handled}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def patch_exception_middleware(middleware_class: "Any") -> None: + """ + Capture all exceptions in Starlette app and + also extract user information. + """ + old_middleware_init = middleware_class.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + + if not_yet_patched: + + def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: + old_middleware_init(self, *args, **kwargs) + + # Patch existing exception handlers + old_handlers = self._exception_handlers.copy() + + async def _sentry_patched_exception_handler( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration( + StarletteIntegration + ) + + exp = args[0] + + if integration is not None: + is_http_server_error = ( + hasattr(exp, "status_code") + and isinstance(exp.status_code, int) + and exp.status_code in integration.failed_request_status_codes + ) + if is_http_server_error: + _capture_exception(exp, handled=True) + + # Find a matching handler + old_handler = None + for cls in type(exp).__mro__: + if cls in old_handlers: + old_handler = old_handlers[cls] + break + + if old_handler is None: + return + + if _is_async_callable(old_handler): + return await old_handler(self, *args, **kwargs) + else: + return old_handler(self, *args, **kwargs) + + for key in self._exception_handlers.keys(): + self._exception_handlers[key] = _sentry_patched_exception_handler + + middleware_class.__init__ = _sentry_middleware_init + + old_call = middleware_class.__call__ + + async def _sentry_exceptionmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + # Also add the user (that was eventually set by be Authentication middle + # that was called before this middleware). This is done because the authentication + # middleware sets the user in the scope and then (in the same function) + # calls this exception middelware. In case there is no exception (or no handler + # for the type of exception occuring) then the exception bubbles up and setting the + # user information into the sentry scope is done in auth middleware and the + # ASGI middleware will then send everything to Sentry and this is fine. + # But if there is an exception happening that the exception middleware here + # has a handler for, it will send the exception directly to Sentry, so we need + # the user information right now. + # This is why we do it here. + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_exceptionmiddleware_call + + +@ensure_integration_enabled(StarletteIntegration) +def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: + """ + Extracts user information from the ASGI scope and + adds it to Sentry's scope. + """ + if "user" not in scope: + return + + if not should_send_default_pii(): + return + + user_info: "Dict[str, Any]" = {} + starlette_user = scope["user"] + + username = getattr(starlette_user, "username", None) + if username: + user_info.setdefault("username", starlette_user.username) + + user_id = getattr(starlette_user, "id", None) + if user_id: + user_info.setdefault("id", starlette_user.id) + + email = getattr(starlette_user, "email", None) + if email: + user_info.setdefault("email", starlette_user.email) + + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + +def patch_authentication_middleware(middleware_class: "Any") -> None: + """ + Add user information to Sentry scope. + """ + old_call = middleware_class.__call__ + + not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) + + if not_yet_patched: + + async def _sentry_authenticationmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_authenticationmiddleware_call + + +def patch_middlewares() -> None: + """ + Patches Starlettes `Middleware` class to record + spans for every middleware invoked. + """ + old_middleware_init = Middleware.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + if not_yet_patched: + + def _sentry_middleware_init( + self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + if cls == SentryAsgiMiddleware: + return old_middleware_init(self, cls, *args, **kwargs) + + span_enabled_cls = _enable_span_for_middleware(cls) + old_middleware_init(self, span_enabled_cls, *args, **kwargs) + + if cls == AuthenticationMiddleware: + patch_authentication_middleware(cls) + + if cls == ExceptionMiddleware: + patch_exception_middleware(cls) + + Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] + + +def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: + """ + Instrument Starlette ASGI app using the SentryAsgiMiddleware. + """ + old_app = Starlette.__call__ + + async def _sentry_patched_asgi_app( + self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StarletteIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + mechanism_type=StarletteIntegration.identifier, + transaction_style=integration.transaction_style, + span_origin=StarletteIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + asgi_version=3, + root_path_in_path=root_path_in_path, + ) + + return await middleware(scope, receive, send) + + Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] + + +# This was vendored in from Starlette to support Starlette 0.19.1 because +# this function was only introduced in 0.20.x +def _is_async_callable(obj: "Any") -> bool: + while isinstance(obj, functools.partial): + obj = obj.func + + return iscoroutinefunction(obj) or ( + callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] + ) + + +def _get_cached_request_body_attribute( + client: "sentry_sdk.client.BaseClient", request: "Request" +) -> "Optional[str]": + """ + Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. + """ + if "content-length" not in request.headers: + return None + + try: + content_length = int(request.headers["content-length"]) + except ValueError: + return None + + if content_length and not request_body_within_bounds(client, content_length): + return OVER_SIZE_LIMIT_SUBSTITUTE + + if hasattr(request, "_json"): + return json.dumps(request._json) + + formdata_body = getattr(request, "_form", None) + if formdata_body is None: + return None + + form_data = {} + for key, val in formdata_body.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = val if not is_file else "[Unparsable]" + + return json.dumps(form_data) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), + integration.transaction_style, + request, + ) + + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Add info from request to event + request_info = event.get("request", {}) + if info: + if "cookies" in info: + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_request_response() -> None: + old_request_response = starlette.routing.request_response + + def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": + old_func = func + + is_coroutine = _is_async_callable(old_func) + if is_coroutine: + + async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_func, *args, **kwargs) + + func = _sentry_async_func + + else: + + @functools.wraps(old_func) + def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(StarletteIntegration) + if integration is None: + return old_func(*args, **kwargs) + + current_scope = sentry_sdk.get_current_scope() + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming: + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + current_span._segment._update_active_thread() + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + request = args[0] + + _set_transaction_name_and_source( + sentry_scope, integration.transaction_style, request + ) + + extractor = StarletteRequestExtractor(request) + cookies = extractor.extract_cookies_from_request() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor( + event: "Event", hint: "dict[str, Any]" + ) -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if cookies: + request_info["cookies"] = cookies + + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + return old_func(*args, **kwargs) + + func = _sentry_sync_func + + return old_request_response(func) + + starlette.routing.request_response = _sentry_request_response + + +def patch_templates() -> None: + # If markupsafe is not installed, then Jinja2 is not installed + # (markupsafe is a dependency of Jinja2) + # In this case we do not need to patch the Jinja2Templates class + try: + from markupsafe import Markup + except ImportError: + return # Nothing to do + + # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a + try: + from starlette.templating import Jinja2Templates + except ImportError: + return + + old_jinja2templates_init = Jinja2Templates.__init__ + + not_yet_patched = "_sentry_jinja2templates_init" not in str( + old_jinja2templates_init + ) + + if not_yet_patched: + + def _sentry_jinja2templates_init( + self: "Jinja2Templates", *args: "Any", **kwargs: "Any" + ) -> None: + def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": + trace_meta = Markup( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + return { + "sentry_trace_meta": trace_meta, + } + + kwargs.setdefault("context_processors", []) + + if add_sentry_trace_meta not in kwargs["context_processors"]: + kwargs["context_processors"].append(add_sentry_trace_meta) + + return old_jinja2templates_init(self, *args, **kwargs) + + Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] + + +class StarletteRequestExtractor: + """ + Extracts useful information from the Starlette request + (like form data or cookies) and adds it to the Sentry event. + """ + + def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: + self.request = request + + def extract_cookies_from_request( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + cookies: "Optional[Dict[str, Any]]" = None + if should_send_default_pii(): + cookies = self.cookies() + + return cookies + + async def extract_request_info( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + client = sentry_sdk.get_client() + + request_info: "Dict[str, Any]" = {} + + with capture_internal_exceptions(): + # Add cookies + if should_send_default_pii(): + request_info["cookies"] = self.cookies() + + # If there is no body, just return the cookies + content_length = await self.content_length() + if not content_length: + return request_info + + # Add annotation if body is too big + if content_length and not request_body_within_bounds( + client, content_length + ): + request_info["data"] = AnnotatedValue.removed_because_over_size_limit() + return request_info + + # Add JSON body, if it is a JSON request + json = await self.json() + if json: + request_info["data"] = json + return request_info + + # Add form as key/value pairs, if request has form data + form = await self.form() + if form: + form_data = {} + for key, val in form.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = ( + val + if not is_file + else AnnotatedValue.removed_because_raw_data() + ) + + request_info["data"] = form_data + return request_info + + # Raw data, do not add body just an annotation + request_info["data"] = AnnotatedValue.removed_because_raw_data() + return request_info + + async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": + if "content-length" in self.request.headers: + return int(self.request.headers["content-length"]) + + return None + + def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": + return self.request.cookies + + async def form(self: "StarletteRequestExtractor") -> "Any": + if multipart is None: + return None + + # Parse the body first to get it cached, as Starlette does not cache form() as it + # does with body() and json() https://github.com/encode/starlette/discussions/1933 + # Calling `.form()` without calling `.body()` first will + # potentially break the users project. + await self.request.body() + + return await self.request.form() + + def is_json(self: "StarletteRequestExtractor") -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": + if not self.is_json(): + return None + try: + return await self.request.json() + except JSONDecodeError: + return None + + +def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": + router = scope.get("router") + if not router: + return None + + for route in router.routes: + match = route.matches(scope) + if match[0] == Match.FULL: + try: + return route.path + except AttributeError: + # routes added via app.host() won't have a path attribute + return scope.get("path") + + return None + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = None + source = SOURCE_FOR_STYLE[transaction_style] + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or None + + elif transaction_style == "url": + name = _transaction_name_from_router(request.scope) + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + scope.set_transaction_name(name, source=source) + + +def _get_transaction_from_middleware( + app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" +) -> "Tuple[Optional[str], Optional[str]]": + name = None + source = None + + if integration.transaction_style == "endpoint": + name = transaction_from_function(app.__class__) + source = TransactionSource.COMPONENT + elif integration.transaction_style == "url": + name = _transaction_name_from_router(asgi_scope) + source = TransactionSource.ROUTE + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py new file mode 100644 index 0000000000..1eebd37e84 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py @@ -0,0 +1,314 @@ +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from pydantic import BaseModel + from starlite import Request, Starlite, State # type: ignore + from starlite.handlers.base import BaseRouteHandler # type: ignore + from starlite.middleware import DefineMiddleware # type: ignore + from starlite.plugins.base import get_plugin_for_value # type: ignore + from starlite.routes.http import HTTPRoute # type: ignore + from starlite.utils import ( # type: ignore + ConnectionDataExtractor, + Ref, + is_async_callable, + ) +except ImportError: + raise DidNotEnable("Starlite is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from starlite import MiddlewareProtocol + from starlite.types import ( # type: ignore + ASGIApp, + Hint, + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from starlite.types import ( + Scope as StarliteScope, + ) + + from sentry_sdk._types import Event + + +_DEFAULT_TRANSACTION_NAME = "generic Starlite request" + + +class StarliteIntegration(Integration): + identifier = "starlite" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + +class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + +def patch_app_init() -> None: + """ + Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. + See: + - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception + - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ + """ + old__init__ = Starlite.__init__ + + @ensure_integration_enabled(StarliteIntegration, old__init__) + def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: + after_exception = kwargs.pop("after_exception", []) + kwargs.update( + after_exception=[ + exception_handler, + *( + after_exception + if isinstance(after_exception, list) + else [after_exception] + ), + ] + ) + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Starlite.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryStarliteASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "StarliteScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(StarliteIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarliteIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarliteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlite.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await receive(*args, **kwargs) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await send(message) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + elif isinstance(route_handler.fn, Ref): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = StarliteIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if not scope_user: + return None + if isinstance(scope_user, dict): + return scope_user + if isinstance(scope_user, BaseModel): + return scope_user.dict() + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + plugin = get_plugin_for_value(scope_user) + if plugin and not is_async_callable(plugin.to_dict): + return plugin.to_dict(scope_user) + + return None + + +@ensure_integration_enabled(StarliteIntegration) +def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarliteIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py new file mode 100644 index 0000000000..746e09b36f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py @@ -0,0 +1,37 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.utils import parse_version + +try: + from statsig import statsig as statsig_module + from statsig.version import __version__ as STATSIG_VERSION +except ImportError: + raise DidNotEnable("statsig is not installed") + +if TYPE_CHECKING: + from statsig.statsig_user import StatsigUser + + +class StatsigIntegration(Integration): + identifier = "statsig" + + @staticmethod + def setup_once() -> None: + version = parse_version(STATSIG_VERSION) + _check_minimum_version(StatsigIntegration, version, "statsig") + + # Wrap and patch evaluation method(s) in the statsig module + old_check_gate = statsig_module.check_gate + + @wraps(old_check_gate) + def sentry_check_gate( + user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_check_gate(user, gate, *args, **kwargs) + add_feature_flag(gate, enabled) + return enabled + + statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py new file mode 100644 index 0000000000..82f30f2dda --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py @@ -0,0 +1,404 @@ +import os +import platform +import subprocess +import sys +from http.client import HTTPConnection, HTTPResponse +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + EnvironHeaders, + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + is_sentry_url, + logger, + parse_url, + safe_repr, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from sentry_sdk._types import Event, Hint + + +_RUNTIME_CONTEXT: "dict[str, object]" = { + "name": platform.python_implementation(), + "version": "%s.%s.%s" % (sys.version_info[:3]), + "build": sys.version, +} + + +class StdlibIntegration(Integration): + identifier = "stdlib" + + @staticmethod + def setup_once() -> None: + _install_httplib() + _install_subprocess() + + @add_global_event_processor + def add_python_runtime_context( + event: "Event", hint: "Hint" + ) -> "Optional[Event]": + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is not None: + contexts = event.setdefault("contexts", {}) + if isinstance(contexts, dict) and "runtime" not in contexts: + contexts["runtime"] = _RUNTIME_CONTEXT + + return event + + +def _complete_span(span: "Union[Span, StreamedSpan]") -> None: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + +def _install_httplib() -> None: + real_putrequest = HTTPConnection.putrequest + real_getresponse = HTTPConnection.getresponse + real_read = HTTPResponse.read + real_close = HTTPResponse.close + + def putrequest( + self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + default_port = self.default_port + + # proxies go through set_tunnel + tunnel_host = getattr(self, "_tunnel_host", None) + if tunnel_host: + host = tunnel_host + port = getattr(self, "_tunnel_port", default_port) + else: + host = self.host + port = self.port + + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is None or is_sentry_url( + client, host + ): + return real_putrequest(self, method, url, *args, **kwargs) + + real_url = url + if real_url is None or not real_url.startswith(("http://", "https://")): + real_url = "%s://%s%s%s" % ( + default_port == 443 and "https" or "http", + host, + port != default_port and ":%s" % port or "", + url, + ) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(real_url, sanitize=False) + + span_streaming = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + attributes={ + "sentry.origin": "auto.http.stdlib.httplib", + "sentry.op": OP.HTTP_CLIENT, + SPANDATA.HTTP_REQUEST_METHOD: method, + }, + ) + + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + + set_on_span = span.set_attribute + + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + origin="auto.http.stdlib.httplib", + ) + + span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + + set_on_span = span.set_data + + # for proxies, these point to the proxy host/port + if tunnel_host: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) + set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) + + rv = real_putrequest(self, method, url, *args, **kwargs) + + if should_propagate_trace(client, real_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=key, value=value, real_url=real_url + ) + ) + self.putheader(key, value) + + self._sentrysdk_span = span # type: ignore[attr-defined] + + return rv + + def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": + span = getattr(self, "_sentrysdk_span", None) + + if span is None: + return real_getresponse(self, *args, **kwargs) + + try: + rv = real_getresponse(self, *args, **kwargs) + except BaseException: + _complete_span(span) + raise + + if isinstance(span, StreamedSpan): + status_code = int(rv.status) + span.status = "error" if status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", status_code) + else: + span.set_http_status(int(rv.status)) + span.set_data("reason", rv.reason) + + # getresponse doesn't include actually reading the response body. This + # is done in read(). So if the metadata/headers suggest there's a body to + # read, don't finish the span just yet, but save it for ending it later. + has_body = rv.chunked or (rv.length is not None and rv.length > 0) + if has_body: + rv._sentrysdk_span = span # type: ignore[attr-defined] + else: + _complete_span(span) + + return rv + + def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": + try: + return real_read(self, *args, **kwargs) + finally: + span = getattr(self, "_sentrysdk_span", None) + # read() might be called multiple times to consume a single body, + # so we can't just end the span when read() is done. Instead, + # try to figure out whether the response body has been fully read. + if span and (self.fp is None or self.closed): + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + def close(self: "HTTPResponse") -> None: + # We patch close() as a best effort fallback in case the span is not + # ended yet in getresponse() or read(). + + try: + real_close(self) + finally: + span = getattr(self, "_sentrysdk_span", None) + if span is not None: + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + HTTPConnection.putrequest = putrequest # type: ignore[method-assign] + HTTPConnection.getresponse = getresponse # type: ignore[method-assign] + HTTPResponse.read = read # type: ignore[method-assign] + HTTPResponse.close = close # type: ignore[assignment,method-assign] + + +def _init_argument( + args: "List[Any]", + kwargs: "Dict[Any, Any]", + name: str, + position: int, + setdefault_callback: "Optional[Callable[[Any], Any]]" = None, +) -> "Any": + """ + given (*args, **kwargs) of a function call, retrieve (and optionally set a + default for) an argument by either name or position. + + This is useful for wrapping functions with complex type signatures and + extracting a few arguments without needing to redefine that function's + entire type signature. + """ + + if name in kwargs: + rv = kwargs[name] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + kwargs[name] = rv + elif position < len(args): + rv = args[position] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + args[position] = rv + else: + rv = setdefault_callback and setdefault_callback(None) + if rv is not None: + kwargs[name] = rv + + return rv + + +def _install_subprocess() -> None: + old_popen_init = subprocess.Popen.__init__ + + @ensure_integration_enabled(StdlibIntegration, old_popen_init) + def sentry_patched_popen_init( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> None: + # Convert from tuple to list to be able to set values. + a = list(a) + + args = _init_argument(a, kw, "args", 0) or [] + cwd = _init_argument(a, kw, "cwd", 9) + + # if args is not a list or tuple (and e.g. some iterator instead), + # let's not use it at all. There are too many things that can go wrong + # when trying to collect an iterator into a list and setting that list + # into `a` again. + # + # Also invocations where `args` is not a sequence are not actually + # legal. They just happen to work under CPython. + description = None + + if isinstance(args, (list, tuple)) and len(args) < 100: + with capture_internal_exceptions(): + description = " ".join(map(str, args)) + + if description is None: + description = safe_repr(args) + + env = None + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.SUBPROCESS, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.SUBPROCESS, + name=description, + origin="auto.subprocess.stdlib.subprocess", + ) + + with span: + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + if env is None: + env = _init_argument( + a, + kw, + "env", + 10, + lambda x: dict(x if x is not None else os.environ), + ) + env["SUBPROCESS_" + k.upper().replace("-", "_")] = v + + if cwd and isinstance(span, Span): + span.set_data("subprocess.cwd", cwd) + + rv = old_popen_init(self, *a, **kw) + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + else: + span.set_tag("subprocess.pid", self.pid) + + return rv + + subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore + + old_popen_wait = subprocess.Popen.wait + + @ensure_integration_enabled(StdlibIntegration, old_popen_wait) + def sentry_patched_popen_wait( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_WAIT, + attributes={ + "sentry.op": OP.SUBPROCESS_WAIT, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_wait(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_WAIT, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_wait(self, *a, **kw) + + subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore + + old_popen_communicate = subprocess.Popen.communicate + + @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) + def sentry_patched_popen_communicate( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_COMMUNICATE, + attributes={ + "sentry.op": OP.SUBPROCESS_COMMUNICATE, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_communicate(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_COMMUNICATE, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_communicate(self, *a, **kw) + + subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore + + +def get_subprocess_traceparent_headers() -> "EnvironHeaders": + return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py new file mode 100644 index 0000000000..5f00e8bf6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py @@ -0,0 +1,493 @@ +import functools +import hashlib +import warnings +from inspect import isawaitable + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from functools import cached_property +except ImportError: + # The strawberry integration requires Python 3.8+. functools.cached_property + # was added in 3.8, so this check is technically not needed, but since this + # is an auto-enabling integration, we might get to executing this import in + # lower Python versions, so we need to deal with it. + raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") + +try: + from strawberry import Schema + from strawberry.extensions import SchemaExtension + from strawberry.extensions.tracing.utils import ( + should_skip_tracing as strawberry_should_skip_tracing, + ) + from strawberry.http import async_base_view, sync_base_view +except ImportError: + raise DidNotEnable("strawberry-graphql is not installed") + +try: + from strawberry.extensions.tracing import ( + SentryTracingExtension as StrawberrySentryAsyncExtension, + ) + from strawberry.extensions.tracing import ( + SentryTracingExtensionSync as StrawberrySentrySyncExtension, + ) +except ImportError: + StrawberrySentryAsyncExtension = None + StrawberrySentrySyncExtension = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Generator, List, Optional + + from graphql import GraphQLError, GraphQLResolveInfo + from strawberry.http import GraphQLHTTPResponse + from strawberry.types import ExecutionContext + + from sentry_sdk._types import Event, EventProcessor + + +ignore_logger("strawberry.execution") + + +class StrawberryIntegration(Integration): + identifier = "strawberry" + origin = f"auto.graphql.{identifier}" + + def __init__(self, async_execution: "Optional[bool]" = None) -> None: + if async_execution not in (None, False, True): + raise ValueError( + 'Invalid value for async_execution: "{}" (must be bool)'.format( + async_execution + ) + ) + self.async_execution = async_execution + + @staticmethod + def setup_once() -> None: + version = package_version("strawberry-graphql") + _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") + + _patch_schema_init() + _patch_views() + + +def _patch_schema_init() -> None: + old_schema_init = Schema.__init__ + + @functools.wraps(old_schema_init) + def _sentry_patched_schema_init( + self: "Schema", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) + if integration is None: + return old_schema_init(self, *args, **kwargs) + + extensions = kwargs.get("extensions") or [] + + should_use_async_extension: "Optional[bool]" = None + if integration.async_execution is not None: + should_use_async_extension = integration.async_execution + else: + # try to figure it out ourselves + should_use_async_extension = _guess_if_using_async(extensions) + + if should_use_async_extension is None: + warnings.warn( + "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", + stacklevel=2, + ) + should_use_async_extension = False + + # remove the built in strawberry sentry extension, if present + extensions = [ + extension + for extension in extensions + if extension + not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) + ] + + # add our extension + extensions = [ + SentryAsyncExtension if should_use_async_extension else SentrySyncExtension + ] + extensions + + kwargs["extensions"] = extensions + + return old_schema_init(self, *args, **kwargs) + + Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] + + +class SentryAsyncExtension(SchemaExtension): + def __init__( + self: "Any", + *, + execution_context: "Optional[ExecutionContext]" = None, + ) -> None: + if execution_context: + self.execution_context = execution_context + + @cached_property + def _resource_name(self) -> str: + query_hash = self.hash_query(self.execution_context.query) # type: ignore + + if self.execution_context.operation_name: + return "{}:{}".format(self.execution_context.operation_name, query_hash) + + return query_hash + + def hash_query(self, query: str) -> str: + return hashlib.md5(query.encode("utf-8")).hexdigest() + + def on_operation(self) -> "Generator[None, None, None]": + operation_name = self.execution_context.operation_name + + operation_type = "query" + op = OP.GRAPHQL_QUERY + + if self.execution_context.query is None: + self.execution_context.query = "" + + if self.execution_context.query.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif self.execution_context.query.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + description = operation_type + if operation_name: + description += " {}".format(operation_name) + + sentry_sdk.add_breadcrumb( + category="graphql.operation", + data={ + "operation_name": operation_name, + "operation_type": operation_type, + }, + ) + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_request_event_processor(self.execution_context) + scope.add_event_processor(event_processor) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + additional_attributes: "dict[str, Any]" = {} + + if should_send_default_pii(): + additional_attributes["graphql.document"] = self.execution_context.query + + if operation_name: + additional_attributes["graphql.operation.name"] = operation_name + + graphql_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": op, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + graphql_span = sentry_sdk.start_span( + op=op, + name=description, + origin=StrawberryIntegration.origin, + ) + graphql_span.__enter__() + + if type(graphql_span) is Span: + if should_send_default_pii(): + graphql_span.set_data("graphql.document", self.execution_context.query) + + graphql_span.set_data("graphql.operation.type", operation_type) + graphql_span.set_data("graphql.operation.name", operation_name) + # This attribute is being removed in streamed spans + graphql_span.set_data("graphql.resource_name", self._resource_name) + + yield + + if type(graphql_span) is StreamedSpan: + if self.execution_context.operation_name: + segment = graphql_span._segment + segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) + segment.set_attribute("sentry.op", op) + segment.name = self.execution_context.operation_name + elif isinstance(graphql_span, Span): + transaction = graphql_span.containing_transaction + if transaction and self.execution_context.operation_name: + transaction.name = self.execution_context.operation_name + transaction.source = TransactionSource.COMPONENT + transaction.op = op + + graphql_span.__exit__(None, None, None) + + def on_validate(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + validation_span = sentry_sdk.traces.start_span( + name="validation", + attributes={ + "sentry.op": OP.GRAPHQL_VALIDATE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + validation_span = sentry_sdk.start_span( + op=OP.GRAPHQL_VALIDATE, + name="validation", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during validation, we still need to close the span + try: + yield + finally: + if isinstance(validation_span, StreamedSpan): + validation_span.end() + else: + validation_span.finish() + + def on_parse(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + parsing_span = sentry_sdk.traces.start_span( + name="parsing", + attributes={ + "sentry.op": OP.GRAPHQL_PARSE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + parsing_span = sentry_sdk.start_span( + op=OP.GRAPHQL_PARSE, + name="parsing", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during parsing, we still need to close the span + try: + yield + finally: + if isinstance(parsing_span, StreamedSpan): + parsing_span.end() + else: + parsing_span.finish() + + def should_skip_tracing( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + info: "GraphQLResolveInfo", + ) -> bool: + return strawberry_should_skip_tracing(_next, info) + + async def _resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + result = _next(root, info, *args, **kwargs) + + if isawaitable(result): + result = await result + + return result + + async def resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return await self._resolve(_next, root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return await self._resolve(_next, root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return await self._resolve(_next, root, info, *args, **kwargs) + + +class SentrySyncExtension(SentryAsyncExtension): + def resolve( + self, + _next: "Callable[[Any, Any, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return _next(root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return _next(root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return _next(root, info, *args, **kwargs) + + +def _patch_views() -> None: + old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors + old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors + + def _sentry_patched_async_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_async_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + def _sentry_patched_sync_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_sync_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + @ensure_integration_enabled(StrawberryIntegration) + def _sentry_patched_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + if not errors: + return + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_response_event_processor(response_data) + scope.add_event_processor(event_processor) + + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": StrawberryIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_async_view_handle_errors + ) + sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_sync_view_handle_errors + ) + + +def _make_request_event_processor( + execution_context: "ExecutionContext", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + request_data = event.setdefault("request", {}) + request_data["api_target"] = "graphql" + + if not request_data.get("data"): + data: "dict[str, Any]" = {"query": execution_context.query} + if execution_context.variables: + data["variables"] = execution_context.variables + if execution_context.operation_name: + data["operationName"] = execution_context.operation_name + + request_data["data"] = data + + else: + try: + del event["request"]["data"] + except (KeyError, TypeError): + pass + + return event + + return inner + + +def _make_response_event_processor( + response_data: "GraphQLHTTPResponse", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + contexts = event.setdefault("contexts", {}) + contexts["response"] = {"data": response_data} + + return event + + return inner + + +def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": + if StrawberrySentryAsyncExtension in extensions: + return True + elif StrawberrySentrySyncExtension in extensions: + return False + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py new file mode 100644 index 0000000000..4927c0e885 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py @@ -0,0 +1,65 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import NoReturn, Union + + +class SysExitIntegration(Integration): + """Captures sys.exit calls and sends them as events to Sentry. + + By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit + exceptions generated by sys.exit calls and send them to Sentry. + + This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and + non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. + Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. + """ + + identifier = "sys_exit" + + def __init__(self, *, capture_successful_exits: bool = False) -> None: + self._capture_successful_exits = capture_successful_exits + + @staticmethod + def setup_once() -> None: + SysExitIntegration._patch_sys_exit() + + @staticmethod + def _patch_sys_exit() -> None: + old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit + + @functools.wraps(old_exit) + def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": + # @ensure_integration_enabled ensures that this is non-None + integration = sentry_sdk.get_client().get_integration(SysExitIntegration) + if integration is None: + old_exit(__status) + + try: + old_exit(__status) + except SystemExit as e: + with capture_internal_exceptions(): + if integration._capture_successful_exits or __status not in ( + 0, + None, + ): + _capture_exception(e) + raise e + + sys.exit = sentry_patched_exit + + +def _capture_exception(exc: "SystemExit") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": SysExitIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py new file mode 100644 index 0000000000..f3ba046332 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py @@ -0,0 +1,196 @@ +import sys +import warnings +from concurrent.futures import Future, ThreadPoolExecutor +from functools import wraps +from threading import Thread, current_thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import use_isolation_scope, use_scope +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T", bound=Any) + + +class ThreadingIntegration(Integration): + identifier = "threading" + + def __init__( + self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True + ) -> None: + if propagate_hub is not None: + logger.warning( + "Deprecated: propagate_hub is deprecated. This will be removed in the future." + ) + + # Note: propagate_hub did not have any effect on propagation of scope data + # scope data was always propagated no matter what the value of propagate_hub was + # This is why the default for propagate_scope is True + + self.propagate_scope = propagate_scope + + if propagate_hub is not None: + self.propagate_scope = propagate_hub + + @staticmethod + def setup_once() -> None: + old_start = Thread.start + + try: + from django import VERSION as django_version # noqa: N811 + except ImportError: + django_version = None + + try: + import channels # type: ignore[import-untyped] + + channels_version = channels.__version__ + except (ImportError, AttributeError): + channels_version = None + + is_async_emulated_with_threads = ( + sys.version_info < (3, 9) + and channels_version is not None + and channels_version < "4.0.0" + and django_version is not None + and django_version >= (3, 0) + and django_version < (4, 0) + ) + + @wraps(old_start) + def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return old_start(self, *a, **kw) + + if integration.propagate_scope: + if is_async_emulated_with_threads: + warnings.warn( + "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " + "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " + "Please either upgrade to Django channels 4.0+, use Django's async features " + "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", + stacklevel=2, + ) + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + + else: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + # Patching instance methods in `start()` creates a reference cycle if + # done in a naive way. See + # https://github.com/getsentry/sentry-python/pull/434 + # + # In threading module, using current_thread API will access current thread instance + # without holding it to avoid a reference cycle in an easier way. + with capture_internal_exceptions(): + new_run = _wrap_run( + isolation_scope, + current_scope, + getattr(self.run, "__func__", self.run), + ) + self.run = new_run # type: ignore + + return old_start(self, *a, **kw) + + Thread.start = sentry_start # type: ignore + ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore + ThreadPoolExecutor.submit, is_async_emulated_with_threads + ) + + +def _wrap_run( + isolation_scope_to_use: "Optional[sentry_sdk.Scope]", + current_scope_to_use: "Optional[sentry_sdk.Scope]", + old_run_func: "F", +) -> "F": + @wraps(old_run_func) + def run(*a: "Any", **kw: "Any") -> "Any": + def _run_old_run_func() -> "Any": + try: + self = current_thread() + return old_run_func(self, *a[1:], **kw) + except Exception: + reraise(*_capture_exception()) + + if isolation_scope_to_use is not None and current_scope_to_use is not None: + with use_isolation_scope(isolation_scope_to_use): + with use_scope(current_scope_to_use): + return _run_old_run_func() + else: + return _run_old_run_func() + + return run # type: ignore + + +def _wrap_threadpool_executor_submit( + func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool +) -> "Callable[..., Future[T]]": + """ + Wrap submit call to propagate scopes on task submission. + """ + + @wraps(func) + def sentry_submit( + self: "ThreadPoolExecutor", + fn: "Callable[..., T]", + *args: "Any", + **kwargs: "Any", + ) -> "Future[T]": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return func(self, fn, *args, **kwargs) + + if integration.propagate_scope and is_async_emulated_with_threads: + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + elif integration.propagate_scope: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": + if isolation_scope is not None and current_scope is not None: + with use_isolation_scope(isolation_scope): + with use_scope(current_scope): + return fn(*args, **kwargs) + + return fn(*args, **kwargs) + + return func(self, wrapped_fn, *args, **kwargs) + + return sentry_submit + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + if client.get_integration(ThreadingIntegration) is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "threading", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py new file mode 100644 index 0000000000..859b0d0870 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py @@ -0,0 +1,320 @@ +import contextlib +import weakref +from inspect import iscoroutinefunction + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + RequestExtractor, + _filter_headers, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from tornado import version_info as TORNADO_VERSION + from tornado.gen import coroutine + from tornado.web import HTTPError, RequestHandler +except ImportError: + raise DidNotEnable("Tornado not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.tracing import Span + + +class TornadoIntegration(Integration): + identifier = "tornado" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(TornadoIntegration, TORNADO_VERSION) + + if not HAS_REAL_CONTEXTVARS: + # Tornado is async. We better have contextvars or we're going to leak + # state between requests. + raise DidNotEnable( + "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" + + CONTEXTVARS_ERROR_MESSAGE + ) + + ignore_logger("tornado.access") + + old_execute = RequestHandler._execute + + awaitable = iscoroutinefunction(old_execute) + + if awaitable: + # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) + # In that case our method should be a coroutine function too + async def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + return await old_execute(self, *args, **kwargs) + + else: + + @coroutine # type: ignore + def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + result = yield from old_execute(self, *args, **kwargs) + return result + + RequestHandler._execute = sentry_execute_request_handler + + old_log_exception = RequestHandler.log_exception + + def sentry_log_exception( + self: "Any", + ty: type, + value: BaseException, + tb: "Any", + *args: "Any", + **kwargs: "Any", + ) -> "Optional[Any]": + _capture_exception(ty, value, tb) + return old_log_exception(self, ty, value, tb, *args, **kwargs) + + RequestHandler.log_exception = sentry_log_exception + + +_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" + + +@contextlib.contextmanager +def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": + integration = sentry_sdk.get_client().get_integration(TornadoIntegration) + + if integration is None: + yield + return + + weak_handler = weakref.ref(self) + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + headers = self.request.headers + + scope.clear_breadcrumbs() + processor = _make_event_processor(weak_handler) + scope.add_event_processor(processor) + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(dict(headers)) + scope.set_custom_sampling_context({"tornado_request": self.request}) + + if should_send_default_pii() and self.request.remote_ip: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_ROOT_SPAN_NAME, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": TornadoIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # Like with all other integrations, this is our + # fallback transaction in case there is no route. + # sentry_urldispatcher_resolve is responsible for + # setting a transaction name later. + name=_DEFAULT_ROOT_SPAN_NAME, + source=TransactionSource.ROUTE, + origin=TornadoIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"tornado_request": self.request}, + ) + + with span_ctx as span: + try: + yield + finally: + if type(span) is StreamedSpan: + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + self.request + ).items(): + span.set_attribute(attr, value) + + with capture_internal_exceptions(): + method = getattr(self, self.request.method.lower(), None) + if method is not None: + span_name = transaction_from_function(method) + if span_name: + span.name = span_name + span.set_attribute( + "sentry.span.source", + SegmentSource.COMPONENT, + ) + + with capture_internal_exceptions(): + status_int = self.get_status() + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) + span.status = "error" if status_int >= 400 else "ok" + + +def _get_request_attributes(request: "Any") -> "Dict[str, Any]": + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.full_url() + attributes["url.path"] = request.path + + if request.query: + attributes[SPANDATA.URL_QUERY] = request.query + + if request.protocol: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol + + if should_send_default_pii() and request.remote_ip: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + + with capture_internal_exceptions(): + raw_data = _get_tornado_request_data(request) + body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data + if body_data is not None: + attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data + + return attributes + + +def _get_tornado_request_data( + request: "Any", +) -> "Union[Optional[str], AnnotatedValue]": + body = request.body + if not body: + return None + + if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): + return AnnotatedValue.substituted_because_over_size_limit() + + return body.decode("utf-8", "replace") + + +@ensure_integration_enabled(TornadoIntegration) +def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: + if isinstance(value, HTTPError): + return + + event, hint = event_from_exception( + (ty, value, tb), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "tornado", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + weak_handler: "Callable[[], RequestHandler]", +) -> "EventProcessor": + def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + handler = weak_handler() + if handler is None: + return event + + request = handler.request + + with capture_internal_exceptions(): + method = getattr(handler, handler.request.method.lower()) + event["transaction"] = transaction_from_function(method) or "" + event["transaction_info"] = {"source": TransactionSource.COMPONENT} + + with capture_internal_exceptions(): + extractor = TornadoRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + + request_info["url"] = "%s://%s%s" % ( + request.protocol, + request.host, + request.path, + ) + + request_info["query_string"] = request.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_ip} + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + try: + current_user = handler.current_user + except Exception: + current_user = None + + if current_user: + event.setdefault("user", {}).setdefault("is_authenticated", True) + + return event + + return tornado_processor + + +class TornadoRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return {k: v.value for k, v in self.request.cookies.items()} + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "Dict[str, Any]": + return { + k: [v.decode("latin1", "replace") for v in vs] + for k, vs in self.request.body_arguments.items() + } + + def is_json(self) -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + def files(self) -> "Dict[str, Any]": + return {k: v[0] for k, v in self.request.files.items() if v} + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py new file mode 100644 index 0000000000..0449a8f10c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py @@ -0,0 +1,52 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.utils import ensure_integration_enabled, event_from_exception + +try: + from trytond.exceptions import TrytonException # type: ignore + from trytond.wsgi import app # type: ignore +except ImportError: + raise DidNotEnable("Trytond is not installed.") + +# TODO: trytond-worker, trytond-cron and trytond-admin intergations + + +class TrytondWSGIIntegration(Integration): + identifier = "trytond_wsgi" + origin = f"auto.http.{identifier}" + + def __init__(self) -> None: + pass + + @staticmethod + def setup_once() -> None: + app.wsgi_app = SentryWsgiMiddleware( + app.wsgi_app, + span_origin=TrytondWSGIIntegration.origin, + ) + + @ensure_integration_enabled(TrytondWSGIIntegration) + def error_handler(e: Exception) -> None: + if isinstance(e, TrytonException): + return + else: + client = sentry_sdk.get_client() + event, hint = event_from_exception( + e, + client_options=client.options, + mechanism={"type": "trytond", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + # Expected error handlers signature was changed + # when the error_handler decorator was introduced + # in Tryton-5.4 + if hasattr(app, "error_handler"): + + @app.error_handler + def _(app, request, e): # type: ignore + error_handler(e) + + else: + app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py new file mode 100644 index 0000000000..497f0539ec --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + +try: + import typer + from typer.main import except_hook +except ImportError: + raise DidNotEnable("Typer not installed") + + +class TyperIntegration(Integration): + identifier = "typer" + + @staticmethod + def setup_once() -> None: + typer.main.except_hook = _make_excepthook(except_hook) # type: ignore + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(TyperIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "typer", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py new file mode 100644 index 0000000000..0316f6b88a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py @@ -0,0 +1,33 @@ +from functools import wraps +from typing import Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from UnleashClient import UnleashClient +except ImportError: + raise DidNotEnable("UnleashClient is not installed") + + +class UnleashIntegration(Integration): + identifier = "unleash" + + @staticmethod + def setup_once() -> None: + # Wrap and patch evaluation methods (class methods) + old_is_enabled = UnleashClient.is_enabled + + @wraps(old_is_enabled) + def sentry_is_enabled( + self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_is_enabled(self, feature, *args, **kwargs) + + # We have no way of knowing what type of unleash feature this is, so we have to treat + # it as a boolean / toggle feature. + add_feature_flag(feature, enabled) + + return enabled + + UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py new file mode 100644 index 0000000000..2c7280a1f2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py @@ -0,0 +1,50 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Callable + + +class UnraisablehookIntegration(Integration): + identifier = "unraisablehook" + + @staticmethod + def setup_once() -> None: + sys.unraisablehook = _make_unraisable(sys.unraisablehook) + + +def _make_unraisable( + old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", +) -> "Callable[[sys.UnraisableHookArgs], Any]": + def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: + integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_unraisablehook(unraisable) + + if unraisable.exc_value and unraisable.exc_traceback: + with capture_internal_exceptions(): + event, hint = event_from_exception( + ( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "unraisablehook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_unraisablehook(unraisable) + + return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py new file mode 100644 index 0000000000..e776ed915a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py @@ -0,0 +1,427 @@ +import sys +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._werkzeug import _get_headers, get_host +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + _filter_headers, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Iterator, + Optional, + Protocol, + Tuple, + TypeVar, + Union, + ) + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + WsgiResponseIter = TypeVar("WsgiResponseIter") + WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") + WsgiExcInfo = TypeVar("WsgiExcInfo") + + class StartResponse(Protocol): + def __call__( + self, + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, + ) -> "WsgiResponseIter": # type: ignore + pass + + +_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_DEFAULT_TRANSACTION_NAME = "generic WSGI request" + + +def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: + return s.encode("latin1").decode(charset, errors) + + +def get_request_url( + environ: "Dict[str, str]", use_x_forwarded_for: bool = False +) -> str: + """Return the absolute URL without query string for the given WSGI + environment.""" + script_name = environ.get("SCRIPT_NAME", "").rstrip("/") + path_info = environ.get("PATH_INFO", "").lstrip("/") + path = f"{script_name}/{path_info}" + + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + return "%s://%s/%s" % ( + scheme, + get_host(environ, use_x_forwarded_for), + wsgi_decoding_dance(path).lstrip("/"), + ) + + +class SentryWsgiMiddleware: + __slots__ = ( + "app", + "use_x_forwarded_for", + "span_origin", + "http_methods_to_capture", + ) + + def __init__( + self, + app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", + use_x_forwarded_for: bool = False, + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + self.app = app + self.use_x_forwarded_for = use_x_forwarded_for + self.span_origin = span_origin + self.http_methods_to_capture = http_methods_to_capture + + def __call__( + self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "Any": + if _wsgi_middleware_applied.get(False): + return self.app(environ, start_response) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _wsgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for + ) + ) + + method = environ.get("REQUEST_METHOD", "").upper() + + span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None + if method in self.http_methods_to_capture: + if span_streaming: + sentry_sdk.traces.continue_trace( + dict(_get_headers(environ)) + ) + Scope.set_custom_sampling_context({"wsgi_environ": environ}) + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, client_ip + ) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_TRANSACTION_NAME, + attributes={ + "sentry.span.source": SegmentSource.ROUTE, + "sentry.origin": self.span_origin, + "sentry.op": OP.HTTP_SERVER, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + environ, + op=OP.HTTP_SERVER, + name=_DEFAULT_TRANSACTION_NAME, + source=TransactionSource.ROUTE, + origin=self.span_origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"wsgi_environ": environ}, + ) + + span_ctx = span_ctx or nullcontext() + + with span_ctx as span: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + environ, self.use_x_forwarded_for + ).items(): + span.set_attribute(attr, value) + + try: + response = self.app( + environ, + partial(_sentry_start_response, start_response, span), + ) + except BaseException: + reraise(*_capture_exception()) + finally: + _wsgi_middleware_applied.set(False) + + # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses + # is determined by a pointer equality check on the response object + # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). + # + # If we were to return a _ScopedResponse, this would cause the check to always fail + # since it's checking the files are exactly the same. + # + # To avoid this and ensure that the offloading mechanism works as expected when it's + # enabled, we check if the response is a file-like object (determined by the presence + # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, + # it would've been used in handling the file in the response). + # + # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, + # so we want to make sure we don't interfere with those either. + # + # If all conditions are met, we return the original response object directly, + # allowing uWSGI to handle it as intended. + if ( + environ.get("wsgi.file_wrapper") + and getattr(response, "fileno", None) is not None + ): + return response + + return _ScopedResponse(scope, response) + + +def _sentry_start_response( + old_start_response: "StartResponse", + span: "Optional[Union[Span, StreamedSpan]]", + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, +) -> "WsgiResponseIter": # type: ignore[type-var] + with capture_internal_exceptions(): + status_int = int(status.split(" ", 1)[0]) + if span is not None: + if isinstance(span, StreamedSpan): + span.status = "error" if status_int >= 400 else "ok" + span.set_attribute("http.response.status_code", status_int) + else: + span.set_http_status(status_int) + + if exc_info is None: + # The Django Rest Framework WSGI test client, and likely other + # (incorrect) implementations, cannot deal with the exc_info argument + # if one is present. Avoid providing a third argument if not necessary. + return old_start_response(status, response_headers) + else: + return old_start_response(status, response_headers, exc_info) + + +def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns our explicitly included environment variables we want to + capture (server name, port and remote addr if pii is enabled). + """ + keys = ["SERVER_NAME", "SERVER_PORT"] + if should_send_default_pii(): + # make debugging of proxy setup easier. Proxy headers are + # in headers. + keys += ["REMOTE_ADDR"] + + for key in keys: + if key in environ: + yield key, environ[key] + + +def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": + """ + Infer the user IP address from various headers. This cannot be used in + security sensitive situations since the value may be forged from a client, + but it's good enough for the event payload. + """ + try: + return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return environ["HTTP_X_REAL_IP"] + except KeyError: + pass + + return environ.get("REMOTE_ADDR") + + +def _capture_exception() -> "ExcInfo": + """ + Captures the current exception and sends it to Sentry. + Returns the ExcInfo tuple to it can be reraised afterwards. + """ + exc_info = sys.exc_info() + e = exc_info[1] + + # SystemExit(0) is the only uncaught exception that is expected behavior + should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) + if not should_skip_capture: + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "wsgi", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class _ScopedResponse: + """ + Users a separate scope for each response chunk. + + This will make WSGI apps more tolerant against: + - WSGI servers streaming responses from a different thread/from + different threads than the one that called start_response + - close() not being called + - WSGI servers streaming responses interleaved from the same thread + """ + + __slots__ = ("_response", "_scope") + + def __init__( + self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" + ) -> None: + self._scope = scope + self._response = response + + def __iter__(self) -> "Iterator[bytes]": + iterator = iter(self._response) + + while True: + with use_isolation_scope(self._scope): + try: + chunk = next(iterator) + except StopIteration: + break + except BaseException: + reraise(*_capture_exception()) + + yield chunk + + def close(self) -> None: + with use_isolation_scope(self._scope): + try: + self._response.close() # type: ignore + except AttributeError: + pass + except BaseException: + reraise(*_capture_exception()) + + +def _make_wsgi_event_processor( + environ: "Dict[str, str]", use_x_forwarded_for: bool +) -> "EventProcessor": + # It's a bit unfortunate that we have to extract and parse the request data + # from the environ so eagerly, but there are a few good reasons for this. + # + # We might be in a situation where the scope never gets torn down + # properly. In that case we will have an unnecessary strong reference to + # all objects in the environ (some of which may take a lot of memory) when + # we're really just interested in a few of them. + # + # Keeping the environment around for longer than the request lifecycle is + # also not necessarily something uWSGI can deal with: + # https://github.com/unbit/uwsgi/issues/1950 + + client_ip = get_client_ip(environ) + request_url = get_request_url(environ, use_x_forwarded_for) + query_string = environ.get("QUERY_STRING") + method = environ.get("REQUEST_METHOD") + env = dict(_get_environ(environ)) + headers = _filter_headers(dict(_get_headers(environ))) + + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + # if the code below fails halfway through we at least have some data + request_info = event.setdefault("request", {}) + + if should_send_default_pii(): + user_info = event.setdefault("user", {}) + if client_ip: + user_info.setdefault("ip_address", client_ip) + + request_info["url"] = request_url + request_info["query_string"] = query_string + request_info["method"] = method + request_info["env"] = env + request_info["headers"] = headers + + return event + + return event_processor + + +def _get_request_attributes( + environ: "Dict[str, str]", + use_x_forwarded_for: bool = False, +) -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from the WSGI environ. + """ + attributes: "dict[str, Any]" = {} + + method = environ.get("REQUEST_METHOD") + if method: + attributes["http.request.method"] = method.upper() + + headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + url_scheme = environ.get("wsgi.url_scheme") + if url_scheme: + attributes["network.protocol.name"] = url_scheme + + server_name = environ.get("SERVER_NAME") + if server_name: + attributes["server.address"] = server_name + + server_port = environ.get("SERVER_PORT") + if server_port: + try: + attributes["server.port"] = int(server_port) + except ValueError: + pass + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + attributes["client.address"] = client_ip + + query_string = environ.get("QUERY_STRING") + if query_string: + attributes["http.query"] = query_string + + path = environ.get("PATH_INFO", "") + if path: + attributes["url.path"] = path + + attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py new file mode 100644 index 0000000000..d7f4425dfd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py @@ -0,0 +1,88 @@ +# NOTE: this is the logger sentry exposes to users, not some generic logger. +import functools +import time +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk.utils import capture_internal_exceptions, format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes + + +OTEL_RANGES = [ + # ((severity level range), severity text) + # https://opentelemetry.io/docs/specs/otel/logs/data-model + ((1, 4), "trace"), + ((5, 8), "debug"), + ((9, 12), "info"), + ((13, 16), "warn"), + ((17, 20), "error"), + ((21, 24), "fatal"), +] + + +class _dict_default_key(dict): # type: ignore[type-arg] + """dict that returns the key if missing.""" + + def __missing__(self, key: str) -> str: + return "{" + key + "}" + + +def _capture_log( + severity_text: str, severity_number: int, template: str, **kwargs: "Any" +) -> None: + body = template + + attributes: "Attributes" = {} + + if "attributes" in kwargs: + provided_attributes = kwargs.pop("attributes") or {} + for attribute, value in provided_attributes.items(): + attributes[attribute] = format_attribute(value) + + for k, v in kwargs.items(): + attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) + + if kwargs: + # only attach template if there are parameters + attributes["sentry.message.template"] = format_attribute(template) + + with capture_internal_exceptions(): + body = template.format_map(_dict_default_key(kwargs)) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": severity_text, + "severity_number": severity_number, + "attributes": attributes, + "body": body, + "time_unix_nano": time.time_ns(), + "trace_id": None, + "span_id": None, + } + ) + + +trace = functools.partial(_capture_log, "trace", 1) +debug = functools.partial(_capture_log, "debug", 5) +info = functools.partial(_capture_log, "info", 9) +warning = functools.partial(_capture_log, "warn", 13) +error = functools.partial(_capture_log, "error", 17) +fatal = functools.partial(_capture_log, "fatal", 21) + + +def _otel_severity_text(otel_severity_number: int) -> str: + for (lower, upper), severity in OTEL_RANGES: + if lower <= otel_severity_number <= upper: + return severity + + return "default" + + +def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": + for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): + if level >= py_level: + return otel_severity_number, _otel_severity_text(otel_severity_number) + + return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py new file mode 100644 index 0000000000..27b7468c0d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py @@ -0,0 +1,62 @@ +import time +from typing import TYPE_CHECKING, Any, Optional + +import sentry_sdk +from sentry_sdk.utils import format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes, Metric, MetricType + + +def _capture_metric( + name: str, + metric_type: "MetricType", + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[Attributes]" = None, +) -> None: + attrs: "Attributes" = {} + + if attributes: + for k, v in attributes.items(): + attrs[k] = format_attribute(v) + + metric: "Metric" = { + "timestamp": time.time(), + "trace_id": None, + "span_id": None, + "name": name, + "type": metric_type, + "value": float(value), + "unit": unit, + "attributes": attrs, + } + + sentry_sdk.get_current_scope()._capture_metric(metric) + + +def count( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "counter", value, unit, attributes) + + +def gauge( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "gauge", value, unit, attributes) + + +def distribution( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py new file mode 100644 index 0000000000..d2ba298c35 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py @@ -0,0 +1,138 @@ +import os +import time +import weakref +from threading import Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +MAX_DOWNSAMPLE_FACTOR = 10 + + +class Monitor: + """ + Performs health checks in a separate thread once every interval seconds + and updates the internal state. Other parts of the SDK only read this state + and act accordingly. + """ + + name = "sentry.monitor" + + _thread: "Optional[Thread]" + _thread_for_pid: "Optional[int]" + + def __init__( + self, transport: "sentry_sdk.transport.Transport", interval: float = 10 + ) -> None: + self.transport: "sentry_sdk.transport.Transport" = transport + self.interval: float = interval + + self._healthy = True + self._downsample_factor: int = 0 + self._running = True + self._reset_thread_state() + + # See https://github.com/getsentry/sentry-python/issues/6148. + # If os.fork() runs while another thread holds self._thread_lock, + # the child inherits the lock locked but the holding thread does + # not exist in the child, so the lock can never be released and + # _ensure_running deadlocks forever. Reinitialise the lock and + # cached thread/pid in the child so it starts clean regardless + # of inherited state. We bind via a WeakMethod so the + # permanently-registered fork handler does not pin this Monitor + # (and its Transport): register_at_fork has no unregister API. + # POSIX-only; Windows uses spawn. + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._thread = None + self._thread_lock = Lock() + self._thread_for_pid = None + + def _ensure_running(self) -> None: + """ + Check that the monitor has an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + while self._running: + time.sleep(self.interval) + if self._running: + self.run() + + thread = Thread(name=self.name, target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def run(self) -> None: + self.check_health() + self.set_downsample_factor() + + def set_downsample_factor(self) -> None: + if self._healthy: + if self._downsample_factor > 0: + logger.debug( + "[Monitor] health check positive, reverting to normal sampling" + ) + self._downsample_factor = 0 + else: + if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: + self._downsample_factor += 1 + logger.debug( + "[Monitor] health check negative, downsampling with a factor of %d", + self._downsample_factor, + ) + + def check_health(self) -> None: + """ + Perform the actual health checks, + currently only checks if the transport is rate-limited. + TODO: augment in the future with more checks. + """ + self._healthy = self.transport.is_healthy() + + def is_healthy(self) -> bool: + self._ensure_running() + return self._healthy + + @property + def downsample_factor(self) -> int: + self._ensure_running() + return self._downsample_factor + + def kill(self) -> None: + self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py new file mode 100644 index 0000000000..d562405295 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py @@ -0,0 +1,49 @@ +from sentry_sdk.profiler.continuous_profiler import ( + start_profile_session, + start_profiler, + stop_profile_session, + stop_profiler, +) +from sentry_sdk.profiler.transaction_profiler import ( + MAX_PROFILE_DURATION_NS, + PROFILE_MINIMUM_SAMPLES, + GeventScheduler, + Profile, + Scheduler, + ThreadScheduler, + has_profiling_enabled, + setup_profiler, + teardown_profiler, +) +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + MAX_STACK_DEPTH, + extract_frame, + extract_stack, + frame_id, + get_frame_name, +) + +__all__ = [ + "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` + "start_profiler", + "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` + "stop_profiler", + # DEPRECATED: The following was re-exported for backwards compatibility. It + # will be removed from sentry_sdk.profiler in a future release. + "MAX_PROFILE_DURATION_NS", + "PROFILE_MINIMUM_SAMPLES", + "Profile", + "Scheduler", + "ThreadScheduler", + "GeventScheduler", + "has_profiling_enabled", + "setup_profiler", + "teardown_profiler", + "DEFAULT_SAMPLING_FREQUENCY", + "MAX_STACK_DEPTH", + "get_frame_name", + "extract_frame", + "extract_stack", + "frame_id", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py new file mode 100644 index 0000000000..ed525f52bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py @@ -0,0 +1,703 @@ +import atexit +import os +import random +import sys +import threading +import time +import uuid +import warnings +from collections import deque +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.consts import VERSION +from sentry_sdk.envelope import Envelope +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + is_gevent, + logger, + now, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union + + from typing_extensions import TypedDict + + from sentry_sdk._types import ContinuousProfilerMode, SDKInfo + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "timestamp": float, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + ThreadPool = None + + +_scheduler: "Optional[ContinuousScheduler]" = None + + +def setup_continuous_profiler( + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", +) -> bool: + global _scheduler + + already_initialized = _scheduler is not None + + if already_initialized: + logger.debug("[Profiling] Continuous Profiler is already setup") + teardown_continuous_profiler() + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventContinuousScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventContinuousScheduler.mode + else: + default_profiler_mode = ThreadContinuousScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + # TODO: deprecate this and just use the existing `profiler_mode` + experiments = options.get("_experiments", {}) + + profiler_mode = ( + experiments.get("continuous_profiling_mode") or default_profiler_mode + ) + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if profiler_mode == ThreadContinuousScheduler.mode: + _scheduler = ThreadContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + elif profiler_mode == GeventContinuousScheduler.mode: + _scheduler = GeventContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + else: + raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up continuous profiler in {mode} mode".format( + mode=_scheduler.mode + ) + ) + + if not already_initialized: + atexit.register(teardown_continuous_profiler) + + return True + + +def is_profile_session_sampled() -> bool: + if _scheduler is None: + return False + return _scheduler.sampled + + +def try_autostart_continuous_profiler() -> None: + # TODO: deprecate this as it'll be replaced by the auto lifecycle option + + if _scheduler is None: + return + + if not _scheduler.is_auto_start_enabled(): + return + + _scheduler.manual_start() + + +def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": + if _scheduler is None: + return None + + return _scheduler.auto_start() + + +def start_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_start() + + +def start_profile_session() -> None: + warnings.warn( + "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + start_profiler() + + +def stop_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_stop() + + +def stop_profile_session() -> None: + warnings.warn( + "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + stop_profiler() + + +def teardown_continuous_profiler() -> None: + stop_profiler() + + global _scheduler + _scheduler = None + + +def get_profiler_id() -> "Union[str, None]": + if _scheduler is None: + return None + return _scheduler.profiler_id + + +def determine_profile_session_sampling_decision( + sample_rate: "Union[float, None]", +) -> bool: + # `None` is treated as `0.0` + if not sample_rate: + return False + + return random.random() < float(sample_rate) + + +class ContinuousProfile: + active: bool = True + + def stop(self) -> None: + self.active = False + + +class ContinuousScheduler: + mode: "ContinuousProfilerMode" = "unknown" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.interval = 1.0 / frequency + self.options = options + self.sdk_info = sdk_info + self.capture_func = capture_func + + self.lifecycle = self.options.get("profile_lifecycle") + profile_session_sample_rate = self.options.get("profile_session_sample_rate") + self.sampled = determine_profile_session_sampling_decision( + profile_session_sample_rate + ) + + self.sampler = self.make_sampler() + self.buffer: "Optional[ProfileBuffer]" = None + self.pid: "Optional[int]" = None + + self.running = False + self.soft_shutdown = False + + self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) + self.active_profiles: "Set[ContinuousProfile]" = set() + + def is_auto_start_enabled(self) -> bool: + # Ensure that the scheduler only autostarts once per process. + # This is necessary because many web servers use forks to spawn + # additional processes. And the profiler is only spawned on the + # master process, then it often only profiles the main process + # and not the ones where the requests are being handled. + if self.pid == os.getpid(): + return False + + experiments = self.options.get("_experiments") + if not experiments: + return False + + return experiments.get("continuous_profiling_auto_start") + + def auto_start(self) -> "Union[ContinuousProfile, None]": + if not self.sampled: + return None + + if self.lifecycle != "trace": + return None + + logger.debug("[Profiling] Auto starting profiler") + + profile = ContinuousProfile() + + self.new_profiles.append(profile) + self.ensure_running() + + return profile + + def manual_start(self) -> None: + if not self.sampled: + return + + if self.lifecycle != "manual": + return + + self.ensure_running() + + def manual_stop(self) -> None: + if self.lifecycle != "manual": + return + + self.teardown() + + def ensure_running(self) -> None: + raise NotImplementedError + + def teardown(self) -> None: + raise NotImplementedError + + def pause(self) -> None: + raise NotImplementedError + + def reset_buffer(self) -> None: + self.buffer = ProfileBuffer( + self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func + ) + + @property + def profiler_id(self) -> "Union[str, None]": + if not self.running or self.buffer is None: + return None + return self.buffer.profiler_id + + def make_sampler(self) -> "Callable[..., bool]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + if self.lifecycle == "trace": + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + return True + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + inactive_profiles = [] + + for profile in self.active_profiles: + if not profile.active: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + else: + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + return _sample_stack + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.soft_shutdown = self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # the soft shutdown happens here to give it a chance + # for the profiler to be reused + if self.soft_shutdown: + self.running = False + + # make sure to explicitly exit the profiler here or there might + # be multiple profilers at once + break + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + buffer = self.buffer + if buffer is not None: + buffer.flush() + + +class ThreadContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ContinuousProfilerMode" = "thread" + name = "sentry.profiler.ThreadContinuousScheduler" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[threading.Thread]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +class GeventContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ContinuousProfilerMode" = "gevent" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[_ThreadPool]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +PROFILE_BUFFER_SECONDS = 60 + + +class ProfileBuffer: + def __init__( + self, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + buffer_size: int, + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.options = options + self.sdk_info = sdk_info + self.buffer_size = buffer_size + self.capture_func = capture_func + + self.profiler_id = uuid.uuid4().hex + self.chunk = ProfileChunk() + + # Make sure to use the same clock to compute a sample's monotonic timestamp + # to ensure the timestamps are correctly aligned. + self.start_monotonic_time = now() + + # Make sure the start timestamp is defined only once per profiler id. + # This prevents issues with clock drift within a single profiler session. + # + # Subtracting the start_monotonic_time here to find a fixed starting position + # for relative monotonic timestamps for each sample. + self.start_timestamp = ( + datetime.now(timezone.utc).timestamp() - self.start_monotonic_time + ) + + def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: + if self.should_flush(monotonic_time): + self.flush() + self.chunk = ProfileChunk() + self.start_monotonic_time = now() + + self.chunk.write(self.start_timestamp + monotonic_time, sample) + + def should_flush(self, monotonic_time: float) -> bool: + # If the delta between the new monotonic time and the start monotonic time + # exceeds the buffer size, it means we should flush the chunk + return monotonic_time - self.start_monotonic_time >= self.buffer_size + + def flush(self) -> None: + chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) + envelope = Envelope() + envelope.add_profile_chunk(chunk) + self.capture_func(envelope) + + +class ProfileChunk: + def __init__(self) -> None: + self.chunk_id = uuid.uuid4().hex + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + def write(self, ts: float, sample: "ExtractedSample") -> None: + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "timestamp": ts, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def to_json( + self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" + ) -> "Dict[str, Any]": + profile = { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + }, + } + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + payload = { + "chunk_id": self.chunk_id, + "client_sdk": { + "name": sdk_info["name"], + "version": VERSION, + }, + "platform": "python", + "profile": profile, + "profiler_id": profiler_id, + "version": "2", + } + + for key in "release", "environment", "dist": + if options[key] is not None: + payload[key] = str(options[key]).strip() + + return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py new file mode 100644 index 0000000000..fe65774c0b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py @@ -0,0 +1,802 @@ +""" +This file is originally based on code from https://github.com/nylas/nylas-perftools, +which is published under the following license: + +The MIT License (MIT) + +Copyright (c) 2014 Nylas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import atexit +import os +import platform +import random +import sys +import threading +import time +import uuid +import warnings +from abc import ABC, abstractmethod +from collections import deque +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + capture_internal_exceptions, + get_current_thread_meta, + is_gevent, + is_valid_sample_rate, + logger, + nanosecond_time, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type + + from typing_extensions import TypedDict + + from sentry_sdk._types import Event, ProfilerMode, SamplingContext + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + ProcessedThreadMetadata, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "elapsed_since_start_ns": str, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + ProcessedProfile = TypedDict( + "ProcessedProfile", + { + "frames": List[ProcessedFrame], + "stacks": List[ProcessedStack], + "samples": List[ProcessedSample], + "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + + ThreadPool = None + + +_scheduler: "Optional[Scheduler]" = None + + +# The minimum number of unique samples that must exist in a profile to be +# considered valid. +PROFILE_MINIMUM_SAMPLES = 2 + + +def has_profiling_enabled(options: "Dict[str, Any]") -> bool: + profiles_sampler = options["profiles_sampler"] + if profiles_sampler is not None: + return True + + profiles_sample_rate = options["profiles_sample_rate"] + if profiles_sample_rate is not None and profiles_sample_rate > 0: + return True + + profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") + if profiles_sample_rate is not None: + logger.warning( + "_experiments['profiles_sample_rate'] is deprecated. " + "Please use the non-experimental profiles_sample_rate option " + "directly." + ) + if profiles_sample_rate > 0: + return True + + return False + + +def setup_profiler(options: "Dict[str, Any]") -> bool: + global _scheduler + + if _scheduler is not None: + logger.debug("[Profiling] Profiler is already setup") + return False + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventScheduler.mode + else: + default_profiler_mode = ThreadScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + profiler_mode = options.get("_experiments", {}).get("profiler_mode") + if profiler_mode is not None: + logger.warning( + "_experiments['profiler_mode'] is deprecated. Please use the " + "non-experimental profiler_mode option directly." + ) + profiler_mode = profiler_mode or default_profiler_mode + + if ( + profiler_mode == ThreadScheduler.mode + # for legacy reasons, we'll keep supporting sleep mode for this scheduler + or profiler_mode == "sleep" + ): + _scheduler = ThreadScheduler(frequency=frequency) + elif profiler_mode == GeventScheduler.mode: + _scheduler = GeventScheduler(frequency=frequency) + else: + raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) + ) + _scheduler.setup() + + atexit.register(teardown_profiler) + + return True + + +def teardown_profiler() -> None: + global _scheduler + + if _scheduler is not None: + _scheduler.teardown() + + _scheduler = None + + +MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds + + +class Profile: + def __init__( + self, + sampled: "Optional[bool]", + start_ns: int, + hub: "Optional[sentry_sdk.Hub]" = None, + scheduler: "Optional[Scheduler]" = None, + ) -> None: + self.scheduler = _scheduler if scheduler is None else scheduler + + self.event_id: str = uuid.uuid4().hex + + self.sampled: "Optional[bool]" = sampled + + # Various framework integrations are capable of overwriting the active thread id. + # If it is set to `None` at the end of the profile, we fall back to the default. + self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 + self.active_thread_id: "Optional[int]" = None + + try: + self.start_ns: int = start_ns + except AttributeError: + self.start_ns = 0 + + self.stop_ns: int = 0 + self.active: bool = False + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + self.unique_samples = 0 + + # Backwards compatibility with the old hub property + self._hub: "Optional[sentry_sdk.Hub]" = None + if hub is not None: + self._hub = hub + warnings.warn( + "The `hub` parameter is deprecated. Please do not use it.", + DeprecationWarning, + stacklevel=2, + ) + + def update_active_thread_id(self) -> None: + self.active_thread_id = get_current_thread_meta()[0] + logger.debug( + "[Profiling] updating active thread id to {tid}".format( + tid=self.active_thread_id + ) + ) + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the profile's sampling decision according to the following + precedence rules: + + 1. If the transaction to be profiled is not sampled, that decision + will be used, regardless of anything else. + + 2. Use `profiles_sample_rate` to decide. + """ + + # The corresponding transaction was not sampled, + # so don't generate a profile for it. + if not self.sampled: + logger.debug( + "[Profiling] Discarding profile because transaction is discarded." + ) + self.sampled = False + return + + # The profiler hasn't been properly initialized. + if self.scheduler is None: + logger.debug( + "[Profiling] Discarding profile because profiler was not started." + ) + self.sampled = False + return + + client = sentry_sdk.get_client() + if not client.is_active(): + self.sampled = False + return + + options = client.options + + if callable(options.get("profiles_sampler")): + sample_rate = options["profiles_sampler"](sampling_context) + elif options["profiles_sample_rate"] is not None: + sample_rate = options["profiles_sample_rate"] + else: + sample_rate = options["_experiments"].get("profiles_sample_rate") + + # The profiles_sample_rate option was not set, so profiling + # was never enabled. + if sample_rate is None: + logger.debug( + "[Profiling] Discarding profile because profiling was not enabled." + ) + self.sampled = False + return + + if not is_valid_sample_rate(sample_rate, source="Profiling"): + logger.warning( + "[Profiling] Discarding profile because of invalid sample rate." + ) + self.sampled = False + return + + # Now we roll the dice. random.random is inclusive of 0, but not of 1, + # so strict < is safe here. In case sample_rate is a boolean, cast it + # to a float (True becomes 1.0 and False becomes 0.0) + self.sampled = random.random() < float(sample_rate) + + if self.sampled: + logger.debug("[Profiling] Initializing profile") + else: + logger.debug( + "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( + sample_rate=float(sample_rate) + ) + ) + + def start(self) -> None: + if not self.sampled or self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Starting profile") + self.active = True + if not self.start_ns: + self.start_ns = nanosecond_time() + self.scheduler.start_profiling(self) + + def stop(self) -> None: + if not self.sampled or not self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Stopping profile") + self.active = False + self.stop_ns = nanosecond_time() + + def __enter__(self) -> "Profile": + scope = sentry_sdk.get_isolation_scope() + old_profile = scope.profile + scope.profile = self + + self._context_manager_state = (scope, old_profile) + + self.start() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + with capture_internal_exceptions(): + self.stop() + + scope, old_profile = self._context_manager_state + del self._context_manager_state + + scope.profile = old_profile + + def write(self, ts: int, sample: "ExtractedSample") -> None: + if not self.active: + return + + if ts < self.start_ns: + return + + offset = ts - self.start_ns + if offset > MAX_PROFILE_DURATION_NS: + self.stop() + return + + self.unique_samples += 1 + + elapsed_since_start_ns = str(offset) + + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "elapsed_since_start_ns": elapsed_since_start_ns, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def process(self) -> "ProcessedProfile": + # This collects the thread metadata at the end of a profile. Doing it + # this way means that any threads that terminate before the profile ends + # will not have any metadata associated with it. + thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + } + + return { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": thread_metadata, + } + + def to_json( + self, event_opt: "Event", options: "Dict[str, Any]" + ) -> "Dict[str, Any]": + profile = self.process() + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + return { + "environment": event_opt.get("environment"), + "event_id": self.event_id, + "platform": "python", + "profile": profile, + "release": event_opt.get("release", ""), + "timestamp": event_opt["start_timestamp"], + "version": "1", + "device": { + "architecture": platform.machine(), + }, + "os": { + "name": platform.system(), + "version": platform.release(), + }, + "runtime": { + "name": platform.python_implementation(), + "version": platform.python_version(), + }, + "transactions": [ + { + "id": event_opt["event_id"], + "name": event_opt["transaction"], + # we start the transaction before the profile and this is + # the transaction start time relative to the profile, so we + # hardcode it to 0 until we can start the profile before + "relative_start_ns": "0", + # use the duration of the profile instead of the transaction + # because we end the transaction after the profile + "relative_end_ns": str(self.stop_ns - self.start_ns), + "trace_id": event_opt["contexts"]["trace"]["trace_id"], + "active_thread_id": str( + self._default_active_thread_id + if self.active_thread_id is None + else self.active_thread_id + ), + } + ], + } + + def valid(self) -> bool: + client = sentry_sdk.get_client() + if not client.is_active(): + return False + + if not has_profiling_enabled(client.options): + return False + + if self.sampled is None or not self.sampled: + if client.transport: + client.transport.record_lost_event( + "sample_rate", data_category="profile" + ) + return False + + if self.unique_samples < PROFILE_MINIMUM_SAMPLES: + if client.transport: + client.transport.record_lost_event( + "insufficient_data", data_category="profile" + ) + logger.debug("[Profiling] Discarding profile because insufficient samples.") + return False + + return True + + @property + def hub(self) -> "Optional[sentry_sdk.Hub]": + warnings.warn( + "The `hub` attribute is deprecated. Please do not access it.", + DeprecationWarning, + stacklevel=2, + ) + return self._hub + + @hub.setter + def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: + warnings.warn( + "The `hub` attribute is deprecated. Please do not set it.", + DeprecationWarning, + stacklevel=2, + ) + self._hub = value + + +class Scheduler(ABC): + mode: "ProfilerMode" = "unknown" + + def __init__(self, frequency: int) -> None: + self.interval = 1.0 / frequency + + self.sampler = self.make_sampler() + + # cap the number of new profiles at any time so it does not grow infinitely + self.new_profiles: "Deque[Profile]" = deque(maxlen=128) + self.active_profiles: "Set[Profile]" = set() + + def __enter__(self) -> "Scheduler": + self.setup() + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self.teardown() + + @abstractmethod + def setup(self) -> None: + pass + + @abstractmethod + def teardown(self) -> None: + pass + + def ensure_running(self) -> None: + """ + Ensure the scheduler is running. By default, this method is a no-op. + The method should be overridden by any implementation for which it is + relevant. + """ + return None + + def start_profiling(self, profile: "Profile") -> None: + self.ensure_running() + self.new_profiles.append(profile) + + def make_sampler(self) -> "Callable[..., None]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + def _sample_stack(*args: "Any", **kwargs: "Any") -> None: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + # make sure to clear the cache if we're not profiling so we dont + # keep a reference to the last stack of frames around + return + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + now = nanosecond_time() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + + inactive_profiles = [] + + for profile in self.active_profiles: + if profile.active: + profile.write(now, sample) + else: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + return _sample_stack + + +class ThreadScheduler(Scheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ProfilerMode" = "thread" + name = "sentry.profiler.ThreadScheduler" + + def __init__(self, frequency: int) -> None: + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[threading.Thread]" = None + self.pid: "Optional[int]" = None + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + """ + Check that the profiler has an active thread to run in, and start one if + that's not the case. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self.running + will be False after running this function. + """ + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + +class GeventScheduler(Scheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ProfilerMode" = "gevent" + name = "sentry.profiler.GeventScheduler" + + def __init__(self, frequency: int) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[_ThreadPool]" = None + self.pid: "Optional[int]" = None + + # This intentionally uses the gevent patched threading.Lock. + # The lock will be required when first trying to start profiles + # as we need to spawn the profiler thread from the greenlets. + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py new file mode 100644 index 0000000000..e9f0fec07f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py @@ -0,0 +1,186 @@ +import os +from collections import deque +from typing import TYPE_CHECKING + +from sentry_sdk._compat import PY311 +from sentry_sdk.utils import filename_for_module + +if TYPE_CHECKING: + from types import FrameType + from typing import Deque, List, Optional, Sequence, Tuple + + from typing_extensions import TypedDict + + from sentry_sdk._lru_cache import LRUCache + + ThreadId = str + + ProcessedStack = List[int] + + ProcessedFrame = TypedDict( + "ProcessedFrame", + { + "abs_path": str, + "filename": Optional[str], + "function": str, + "lineno": int, + "module": Optional[str], + }, + ) + + ProcessedThreadMetadata = TypedDict( + "ProcessedThreadMetadata", + {"name": str}, + ) + + FrameId = Tuple[ + str, # abs_path + int, # lineno + str, # function + ] + FrameIds = Tuple[FrameId, ...] + + # The exact value of this id is not very meaningful. The purpose + # of this id is to give us a compact and unique identifier for a + # raw stack that can be used as a key to a dictionary so that it + # can be used during the sampled format generation. + StackId = Tuple[int, int] + + ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] + ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] + +# The default sampling frequency to use. This is set at 101 in order to +# mitigate the effects of lockstep sampling. +DEFAULT_SAMPLING_FREQUENCY = 101 + + +# We want to impose a stack depth limit so that samples aren't too large. +MAX_STACK_DEPTH = 128 + + +if PY311: + + def get_frame_name(frame: "FrameType") -> str: + return frame.f_code.co_qualname + +else: + + def get_frame_name(frame: "FrameType") -> str: + f_code = frame.f_code + co_varnames = f_code.co_varnames + + # co_name only contains the frame name. If the frame was a method, + # the class name will NOT be included. + name = f_code.co_name + + # if it was a method, we can get the class name by inspecting + # the f_locals for the `self` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `self` if its an instance method + co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals + ): + for cls in type(frame.f_locals["self"]).__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # if it was a class method, (decorated with `@classmethod`) + # we can get the class name by inspecting the f_locals for the `cls` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `cls` if its a class method + co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals + ): + for cls in frame.f_locals["cls"].__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # nothing we can do if it is a staticmethod (decorated with @staticmethod) + + # we've done all we can, time to give up and return what we have + return name + + +def frame_id(raw_frame: "FrameType") -> "FrameId": + return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) + + +def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": + abs_path = raw_frame.f_code.co_filename + + try: + module = raw_frame.f_globals["__name__"] + except Exception: + module = None + + # namedtuples can be many times slower when initialing + # and accessing attribute so we opt to use a tuple here instead + return { + # This originally was `os.path.abspath(abs_path)` but that had + # a large performance overhead. + # + # According to docs, this is equivalent to + # `os.path.normpath(os.path.join(os.getcwd(), path))`. + # The `os.getcwd()` call is slow here, so we precompute it. + # + # Additionally, since we are using normalized path already, + # we skip calling `os.path.normpath` entirely. + "abs_path": os.path.join(cwd, abs_path), + "module": module, + "filename": filename_for_module(module, abs_path) or None, + "function": fid[2], + "lineno": raw_frame.f_lineno, + } + + +def extract_stack( + raw_frame: "Optional[FrameType]", + cache: "LRUCache", + cwd: str, + max_stack_depth: int = MAX_STACK_DEPTH, +) -> "ExtractedStack": + """ + Extracts the stack starting the specified frame. The extracted stack + assumes the specified frame is the top of the stack, and works back + to the bottom of the stack. + + In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, + only the first `MAX_STACK_DEPTH` frames will be returned. + """ + + raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) + + while raw_frame is not None: + f_back = raw_frame.f_back + raw_frames.append(raw_frame) + raw_frame = f_back + + frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) + frames = [] + for i, fid in enumerate(frame_ids): + frame = cache.get(fid) + if frame is None: + frame = extract_frame(fid, raw_frames[i], cwd) + cache.set(fid, frame) + frames.append(frame) + + # Instead of mapping the stack into frame ids and hashing + # that as a tuple, we can directly hash the stack. + # This saves us from having to generate yet another list. + # Additionally, using the stack as the key directly is + # costly because the stack can be large, so we pre-hash + # the stack, and use the hash as the key as this will be + # needed a few times to improve performance. + # + # To Reduce the likelihood of hash collisions, we include + # the stack depth. This means that only stacks of the same + # depth can suffer from hash collisions. + stack_id = len(raw_frames), hash(frame_ids) + + return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py new file mode 100644 index 0000000000..4fd22714cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py @@ -0,0 +1,2185 @@ +import os +import platform +import sys +import warnings +from collections import deque +from contextlib import contextmanager +from copy import copy, deepcopy +from datetime import datetime, timezone +from enum import Enum +from functools import wraps +from itertools import chain +from typing import TYPE_CHECKING, cast + +import sentry_sdk +from sentry_sdk._types import AnnotatedValue +from sentry_sdk.attachments import Attachment +from sentry_sdk.consts import ( + DEFAULT_MAX_BREADCRUMBS, + FALSE_VALUES, + INSTRUMENTER, + SPANDATA, +) +from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.profiler.transaction_profiler import Profile +from sentry_sdk.session import Session +from sentry_sdk.traces import ( + _DEFAULT_PARENT_SPAN, + NoOpStreamedSpan, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.tracing_utils import ( + Baggage, + PropagationContext, + _make_sampling_decision, + has_span_streaming_enabled, + has_tracing_enabled, + is_ignored_span, +) +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exception, + capture_internal_exceptions, + datetime_from_isoformat, + disable_capture_event, + event_from_exception, + exc_info_from_error, + format_attribute, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + Deque, + Dict, + Generator, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + ) + + from typing_extensions import Unpack + + import sentry_sdk + from sentry_sdk._types import ( + Attributes, + AttributeValue, + Breadcrumb, + BreadcrumbHint, + ErrorProcessor, + Event, + EventProcessor, + ExcInfo, + Hint, + Log, + LogLevelStr, + Metric, + SamplingContext, + Type, + ) + from sentry_sdk.tracing import TransactionKwargs + + P = ParamSpec("P") + R = TypeVar("R") + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T") + + +# Holds data that will be added to **all** events sent by this process. +# In case this is a http server (think web framework) with multiple users +# the data will be added to events of all users. +# Typically this is used for process wide data such as the release. +_global_scope: "Optional[Scope]" = None + +# Holds data for the active request. +# This is used to isolate data for different requests or users. +# The isolation scope is usually created by integrations, but may also +# be created manually +_isolation_scope = ContextVar("isolation_scope", default=None) + +# Holds data for the active span. +# This can be used to manually add additional data to a span. +_current_scope = ContextVar("current_scope", default=None) + +global_event_processors: "List[EventProcessor]" = [] + +# A function returning a (trace_id, span_id) tuple +# from an external tracing source (such as otel) +_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None + + +class ScopeType(Enum): + CURRENT = "current" + ISOLATION = "isolation" + GLOBAL = "global" + MERGED = "merged" + + +class _ScopeManager: + def __init__(self, hub: "Optional[Any]" = None) -> None: + self._old_scopes: "List[Scope]" = [] + + def __enter__(self) -> "Scope": + isolation_scope = Scope.get_isolation_scope() + + self._old_scopes.append(isolation_scope) + + forked_scope = isolation_scope.fork() + _isolation_scope.set(forked_scope) + + return forked_scope + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + old_scope = self._old_scopes.pop() + _isolation_scope.set(old_scope) + + +def add_global_event_processor(processor: "EventProcessor") -> None: + global_event_processors.append(processor) + + +def register_external_propagation_context( + fn: "Callable[[], Optional[Tuple[str, str]]]", +) -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = fn + + +def remove_external_propagation_context() -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = None + + +def get_external_propagation_context() -> "Optional[Tuple[str, str]]": + return ( + _external_propagation_context_fn() if _external_propagation_context_fn else None + ) + + +def has_external_propagation_context() -> bool: + return _external_propagation_context_fn is not None + + +def _attr_setter(fn: "Any") -> "Any": + return property(fset=fn, doc=fn.__doc__) + + +def _disable_capture(fn: "F") -> "F": + @wraps(fn) + def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": + if not self._should_capture: + return + try: + self._should_capture = False + return fn(self, *args, **kwargs) + finally: + self._should_capture = True + + return wrapper # type: ignore + + +class Scope: + """The scope holds extra information that should be sent with all + events that belong to it. + """ + + # NOTE: Even though it should not happen, the scope needs to not crash when + # accessed by multiple threads. It's fine if it's full of races, but those + # races should never make the user application crash. + # + # The same needs to hold for any accesses of the scope the SDK makes. + + __slots__ = ( + "_level", + "_name", + "_fingerprint", + # note that for legacy reasons, _transaction is the transaction *name*, + # not a Transaction object (the object is stored in _span) + "_transaction", + "_transaction_info", + "_user", + "_tags", + "_contexts", + "_extras", + "_breadcrumbs", + "_n_breadcrumbs_truncated", + "_gen_ai_original_message_count", + "_gen_ai_conversation_id", + "_event_processors", + "_error_processors", + "_should_capture", + "_span", + "_session", + "_attachments", + "_force_auto_session_tracking", + "_profile", + "_propagation_context", + "client", + "_type", + "_last_event_id", + "_flags", + "_attributes", + ) + + def __init__( + self, + ty: "Optional[ScopeType]" = None, + client: "Optional[sentry_sdk.Client]" = None, + ) -> None: + self._type = ty + + self._event_processors: "List[EventProcessor]" = [] + self._error_processors: "List[ErrorProcessor]" = [] + + self._name: "Optional[str]" = None + self._propagation_context: "Optional[PropagationContext]" = None + self._n_breadcrumbs_truncated: int = 0 + self._gen_ai_original_message_count: "Dict[str, int]" = {} + + self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() + + if client is not None: + self.set_client(client) + + self.clear() + + incoming_trace_information = self._load_trace_data_from_env() + self.generate_propagation_context(incoming_data=incoming_trace_information) + + def __copy__(self) -> "Scope": + """ + Returns a copy of this scope. + This also creates a copy of all referenced data structures. + """ + rv: "Scope" = object.__new__(self.__class__) + + rv._type = self._type + rv.client = self.client + rv._level = self._level + rv._name = self._name + rv._fingerprint = self._fingerprint + rv._transaction = self._transaction + rv._transaction_info = self._transaction_info.copy() + rv._user = self._user + + rv._tags = self._tags.copy() + rv._contexts = self._contexts.copy() + rv._extras = self._extras.copy() + + rv._breadcrumbs = copy(self._breadcrumbs) + rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated + rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() + rv._event_processors = self._event_processors.copy() + rv._error_processors = self._error_processors.copy() + rv._propagation_context = self._propagation_context + + rv._should_capture = self._should_capture + rv._span = self._span + rv._session = self._session + rv._force_auto_session_tracking = self._force_auto_session_tracking + rv._attachments = self._attachments.copy() + + rv._profile = self._profile + + rv._last_event_id = self._last_event_id + + rv._flags = deepcopy(self._flags) + + rv._attributes = self._attributes.copy() + + rv._gen_ai_conversation_id = self._gen_ai_conversation_id + + return rv + + @classmethod + def get_current_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the current scope. + """ + current_scope = _current_scope.get() + if current_scope is None: + current_scope = Scope(ty=ScopeType.CURRENT) + _current_scope.set(current_scope) + + return current_scope + + @classmethod + def set_current_scope(cls, new_current_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new current scope overwriting the existing current scope. + :param new_current_scope: The scope to set as the new current scope. + """ + _current_scope.set(new_current_scope) + + @classmethod + def get_isolation_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the isolation scope. + """ + isolation_scope = _isolation_scope.get() + if isolation_scope is None: + isolation_scope = Scope(ty=ScopeType.ISOLATION) + _isolation_scope.set(isolation_scope) + + return isolation_scope + + @classmethod + def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new isolation scope overwriting the existing isolation scope. + :param new_isolation_scope: The scope to set as the new isolation scope. + """ + _isolation_scope.set(new_isolation_scope) + + @classmethod + def get_global_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the global scope. + """ + global _global_scope + if _global_scope is None: + _global_scope = Scope(ty=ScopeType.GLOBAL) + + return _global_scope + + def set_global_attributes(self) -> None: + from sentry_sdk.client import SDK_INFO + + self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) + self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) + + self.set_attribute( + "process.runtime.name", + platform.python_implementation(), + ) + self.set_attribute( + "process.runtime.version", + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + ) + + options = sentry_sdk.get_client().options + + server_name = options.get("server_name") + if server_name: + self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) + + environment = options.get("environment") + if environment: + self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) + + release = options.get("release") + if release: + self.set_attribute(SPANDATA.SENTRY_RELEASE, release) + + @classmethod + def last_event_id(cls) -> "Optional[str]": + """ + .. versionadded:: 2.2.0 + + Returns event ID of the event most recently captured by the isolation scope, or None if no event + has been captured. We do not consider events that are dropped, e.g. by a before_send hook. + Transactions also are not considered events in this context. + + The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; + whether the event is sent depends on the transport. The event could be sent later or not at all. + Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or + various other reasons. + """ + return cls.get_isolation_scope()._last_event_id + + def _merge_scopes( + self, + additional_scope: "Optional[Scope]" = None, + additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, + ) -> "Scope": + """ + Merges global, isolation and current scope into a new scope and + adds the given additional scope or additional scope kwargs to it. + """ + if additional_scope and additional_scope_kwargs: + raise TypeError("cannot provide scope and kwargs") + + final_scope = copy(_global_scope) if _global_scope is not None else Scope() + final_scope._type = ScopeType.MERGED + + isolation_scope = _isolation_scope.get() + if isolation_scope is not None: + final_scope.update_from_scope(isolation_scope) + + current_scope = _current_scope.get() + if current_scope is not None: + final_scope.update_from_scope(current_scope) + + if self != current_scope and self != isolation_scope: + final_scope.update_from_scope(self) + + if additional_scope is not None: + if callable(additional_scope): + additional_scope(final_scope) + else: + final_scope.update_from_scope(additional_scope) + + elif additional_scope_kwargs: + final_scope.update_from_kwargs(**additional_scope_kwargs) + + return final_scope + + @classmethod + def get_client(cls) -> "sentry_sdk.client.BaseClient": + """ + .. versionadded:: 2.0.0 + + Returns the currently used :py:class:`sentry_sdk.Client`. + This checks the current scope, the isolation scope and the global scope for a client. + If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. + """ + current_scope = _current_scope.get() + try: + client = current_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + isolation_scope = _isolation_scope.get() + try: + client = isolation_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + try: + client = _global_scope.client # type: ignore + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + return NonRecordingClient() + + def set_client( + self, client: "Optional[sentry_sdk.client.BaseClient]" = None + ) -> None: + """ + .. versionadded:: 2.0.0 + + Sets the client for this scope. + + :param client: The client to use in this scope. + If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. + + """ + if client is not None: + self.client = client + # We need a client to set the initial global attributes on the global + # scope since they mostly come from client options, so populate them + # as soon as a client is set + sentry_sdk.get_global_scope().set_global_attributes() + else: + self.client = NonRecordingClient() + + def fork(self) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns a fork of this scope. + """ + forked_scope = copy(self) + return forked_scope + + def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": + """ + Load Sentry trace id and baggage from environment variables. + Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". + """ + incoming_trace_information: "Optional[Dict[str, str]]" = None + + sentry_use_environment = ( + os.environ.get("SENTRY_USE_ENVIRONMENT") or "" + ).lower() + use_environment = sentry_use_environment not in FALSE_VALUES + if use_environment: + incoming_trace_information = {} + + if os.environ.get("SENTRY_TRACE"): + incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( + os.environ.get("SENTRY_TRACE") or "" + ) + + if os.environ.get("SENTRY_BAGGAGE"): + incoming_trace_information[BAGGAGE_HEADER_NAME] = ( + os.environ.get("SENTRY_BAGGAGE") or "" + ) + + return incoming_trace_information or None + + def set_new_propagation_context(self) -> None: + """ + Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. + """ + self._propagation_context = PropagationContext() + + def generate_propagation_context( + self, incoming_data: "Optional[Dict[str, str]]" = None + ) -> None: + """ + Makes sure the propagation context is set on the scope. + If there is `incoming_data` overwrite existing propagation context. + If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. + """ + if incoming_data is not None: + self._propagation_context = PropagationContext.from_incoming_data( + incoming_data + ) + + # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring + if self._type != ScopeType.CURRENT: + if self._propagation_context is None: + self.set_new_propagation_context() + + def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": + """ + Returns the Dynamic Sampling Context from the Propagation Context. + If not existing, creates a new one. + + Deprecated: Logic moved to PropagationContext, don't use directly. + """ + if self._propagation_context is None: + return None + + return self._propagation_context.dynamic_sampling_context + + def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + """ + Returns the Sentry "sentry-trace" header (aka the traceparent) from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().to_traceparent() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return traceparent from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_traceparent() + elif not span_streaming and self.span is not None: + return self.span._to_traceparent() + + # else return traceparent from the propagation context + return self.get_active_propagation_context().to_traceparent() + + def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": + """ + Returns the Sentry "baggage" header containing trace information from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().get_baggage() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return baggage from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_baggage() + elif not span_streaming and self.span is not None: + return self.span._to_baggage() + + # else return baggage from the propagation context + return self.get_active_propagation_context().get_baggage() + + def get_trace_context(self) -> "Dict[str, Any]": + """ + Returns the Sentry "trace" context from the Propagation Context. + """ + if ( + has_tracing_enabled(self.get_client().options) + and self._span is not None + and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) + ): + return self._span._get_trace_context() + + # if we are tracing externally (otel), those values take precedence + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + trace_id, span_id = external_propagation_context + return {"trace_id": trace_id, "span_id": span_id} + + propagation_context = self.get_active_propagation_context() + + return { + "trace_id": propagation_context.trace_id, + "span_id": propagation_context.span_id, + "parent_span_id": propagation_context.parent_span_id, + "dynamic_sampling_context": propagation_context.dynamic_sampling_context, + } + + def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: + """ + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + span = kwargs.pop("span", None) + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + meta = "" + + for name, content in self.iter_trace_propagation_headers(): + meta += f'' + + return meta + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. + Deprecated: use PropagationContext.iter_headers instead. + """ + if self._propagation_context is not None: + yield from self._propagation_context.iter_headers() + + def iter_trace_propagation_headers( + self, *args: "Any", **kwargs: "Any" + ) -> "Generator[Tuple[str, str], None, None]": + """ + Return HTTP headers which allow propagation of trace data. + + If a span is given, the trace data will taken from the span. + If no span is given, the trace data is taken from the scope. + """ + client = self.get_client() + if not client.options.get("propagate_traces"): + warnings.warn( + "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", + DeprecationWarning, + stacklevel=2, + ) + return + + span = kwargs.pop("span", None) + if not span: + span_streaming = has_span_streaming_enabled(client.options) + span = self.streamed_span if span_streaming else self.span + + if ( + has_tracing_enabled(client.options) + and span is not None + and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) + ): + for header in span._iter_headers(): + yield header + elif has_external_propagation_context(): + # when we have an external_propagation_context (otlp) + # we leave outgoing propagation to the propagator + return + else: + for header in self.get_active_propagation_context().iter_headers(): + yield header + + def get_active_propagation_context(self) -> "PropagationContext": + if self._propagation_context is not None: + return self._propagation_context + + current_scope = self.get_current_scope() + if current_scope._propagation_context is not None: + return current_scope._propagation_context + + isolation_scope = self.get_isolation_scope() + # should actually never happen, but just in case someone calls scope.clear + if isolation_scope._propagation_context is None: + isolation_scope._propagation_context = PropagationContext() + return isolation_scope._propagation_context + + @classmethod + def set_custom_sampling_context( + cls, custom_sampling_context: "dict[str, Any]" + ) -> None: + cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( + custom_sampling_context + ) + + def clear(self) -> None: + """Clears the entire scope.""" + self._level: "Optional[LogLevelStr]" = None + self._fingerprint: "Optional[List[str]]" = None + self._transaction: "Optional[str]" = None + self._transaction_info: "dict[str, str]" = {} + self._user: "Optional[Dict[str, Any]]" = None + + self._tags: "Dict[str, Any]" = {} + self._contexts: "Dict[str, Dict[str, Any]]" = {} + self._extras: "dict[str, Any]" = {} + self._attachments: "List[Attachment]" = [] + + self.clear_breadcrumbs() + self._should_capture: bool = True + + self._span: "Optional[Union[Span, StreamedSpan]]" = None + self._session: "Optional[Session]" = None + self._force_auto_session_tracking: "Optional[bool]" = None + + self._profile: "Optional[Profile]" = None + + self._propagation_context = None + + # self._last_event_id is only applicable to isolation scopes + self._last_event_id: "Optional[str]" = None + self._flags: "Optional[FlagBuffer]" = None + + self._attributes: "Attributes" = {} + + self._gen_ai_conversation_id: "Optional[str]" = None + + @_attr_setter + def level(self, value: "LogLevelStr") -> None: + """ + When set this overrides the level. + + .. deprecated:: 1.0.0 + Use :func:`set_level` instead. + + :param value: The level to set. + """ + logger.warning( + "Deprecated: use .set_level() instead. This will be removed in the future." + ) + + self._level = value + + def set_level(self, value: "LogLevelStr") -> None: + """ + Sets the level for the scope. + + :param value: The level to set. + """ + self._level = value + + @_attr_setter + def fingerprint(self, value: "Optional[List[str]]") -> None: + """When set this overrides the default fingerprint.""" + self._fingerprint = value + + @property + def transaction(self) -> "Any": + # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 + """Return the transaction (root span) in the scope, if any.""" + + # there is no span/transaction on the scope + if self._span is None: + return None + + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + # there is an orphan span on the scope + if self._span.containing_transaction is None: + return None + + # there is either a transaction (which is its own containing + # transaction) or a non-orphan span on the scope + return self._span.containing_transaction + + @transaction.setter + def transaction(self, value: "Any") -> None: + # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 + """When set this forces a specific transaction name to be set. + + Deprecated: use set_transaction_name instead.""" + + # XXX: the docstring above is misleading. The implementation of + # apply_to_event prefers an existing value of event.transaction over + # anything set in the scope. + # XXX: note that with the introduction of the Scope.transaction getter, + # there is a semantic and type mismatch between getter and setter. The + # getter returns a Transaction, the setter sets a transaction name. + # Without breaking version compatibility, we could make the setter set a + # transaction name or transaction (self._span) depending on the type of + # the value argument. + + logger.warning( + "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." + ) + self._transaction = value + if self._span: + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + if self._span.containing_transaction: + self._span.containing_transaction.name = value + + def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: + """Set the transaction name and optionally the transaction source.""" + self._transaction = name + if self._span: + if isinstance(self._span, NoOpStreamedSpan): + return + + elif isinstance(self._span, StreamedSpan): + self._span._segment.name = name + if source: + self._span._segment.set_attribute( + "sentry.span.source", getattr(source, "value", source) + ) + + elif self._span.containing_transaction: + self._span.containing_transaction.name = name + if source: + self._span.containing_transaction.source = source + + if source: + self._transaction_info["source"] = source + + @_attr_setter + def user(self, value: "Optional[Dict[str, Any]]") -> None: + """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" + warnings.warn( + "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", + DeprecationWarning, + stacklevel=2, + ) + self.set_user(value) + + def set_user(self, value: "Optional[Dict[str, Any]]") -> None: + """Sets a user for the scope.""" + self._user = value + + session = self.get_isolation_scope()._session + if session is not None: + session.update(user=value) + + @property + def span(self) -> "Optional[Span]": + """Get/set current tracing span or transaction.""" + return self._span if isinstance(self._span, Span) else None + + @span.setter + def span(self, span: "Optional[Span]") -> None: + self._span = span + # XXX: this differs from the implementation in JS, there Scope.setSpan + # does not set Scope._transactionName. + if isinstance(span, Transaction): + transaction = span + if transaction.name: + self._transaction = transaction.name + if transaction.source: + self._transaction_info["source"] = transaction.source + + @property + def streamed_span(self) -> "Optional[StreamedSpan]": + """Get/set current tracing span.""" + return self._span if isinstance(self._span, StreamedSpan) else None + + @streamed_span.setter + def streamed_span(self, span: "Optional[StreamedSpan]") -> None: + self._span = span + + # Also set _transaction and _transaction_info in streaming mode as this + # is used for populating events and linking them to segments + if type(span) is StreamedSpan and span._is_segment(): + self._transaction = span.name + if span._attributes.get("sentry.span.source"): + self._transaction_info["source"] = str( + span._attributes["sentry.span.source"] + ) + + @property + def profile(self) -> "Optional[Profile]": + return self._profile + + @profile.setter + def profile(self, profile: "Optional[Profile]") -> None: + self._profile = profile + + def set_tag(self, key: str, value: "Any") -> None: + """ + Sets a tag for a key to a specific value. + + :param key: Key of the tag to set. + + :param value: Value of the tag to set. + """ + self._tags[key] = value + + def set_tags(self, tags: "Mapping[str, object]") -> None: + """Sets multiple tags at once. + + This method updates multiple tags at once. The tags are passed as a dictionary + or other mapping type. + + Calling this method is equivalent to calling `set_tag` on each key-value pair + in the mapping. If a tag key already exists in the scope, its value will be + updated. If the tag key does not exist in the scope, the key-value pair will + be added to the scope. + + This method only modifies tag keys in the `tags` mapping passed to the method. + `scope.set_tags({})` is, therefore, a no-op. + + :param tags: A mapping of tag keys to tag values to set. + """ + self._tags.update(tags) + + def remove_tag(self, key: str) -> None: + """ + Removes a specific tag. + + :param key: Key of the tag to remove. + """ + self._tags.pop(key, None) + + def set_context( + self, + key: str, + value: "Dict[str, Any]", + ) -> None: + """ + Binds a context at a certain key to a specific value. + """ + self._contexts[key] = value + + def remove_context( + self, + key: str, + ) -> None: + """Removes a context.""" + self._contexts.pop(key, None) + + def set_extra( + self, + key: str, + value: "Any", + ) -> None: + """Sets an extra key to a specific value.""" + self._extras[key] = value + + def remove_extra( + self, + key: str, + ) -> None: + """Removes a specific extra key.""" + self._extras.pop(key, None) + + def set_conversation_id(self, conversation_id: str) -> None: + """ + Sets the conversation ID for gen_ai spans. + + :param conversation_id: The conversation ID to set. + """ + self._gen_ai_conversation_id = conversation_id + + def get_conversation_id(self) -> "Optional[str]": + """ + Gets the conversation ID for gen_ai spans. + + :returns: The conversation ID, or None if not set. + """ + return self._gen_ai_conversation_id + + def remove_conversation_id(self) -> None: + """Removes the conversation ID.""" + self._gen_ai_conversation_id = None + + def clear_breadcrumbs(self) -> None: + """Clears breadcrumb buffer.""" + self._breadcrumbs: "Deque[Breadcrumb]" = deque() + self._n_breadcrumbs_truncated = 0 + + def add_attachment( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + """Adds an attachment to future events sent from this scope. + + The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. + """ + self._attachments.append( + Attachment( + bytes=bytes, + path=path, + filename=filename, + content_type=content_type, + add_to_transactions=add_to_transactions, + ) + ) + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + client = self.get_client() + + if not client.is_active(): + logger.info("Dropped breadcrumb because no client bound") + return + + before_breadcrumb = client.options.get("before_breadcrumb") + max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) + + crumb = dict(crumb or ()) + crumb.update(kwargs) + if not crumb: + return + + hint = dict(hint or ()) + + if crumb.get("timestamp") is None: + crumb["timestamp"] = datetime.now(timezone.utc) + if crumb.get("type") is None: + crumb["type"] = "default" + + if before_breadcrumb is not None: + new_crumb = before_breadcrumb(crumb, hint) + else: + new_crumb = crumb + + if new_crumb is not None: + self._breadcrumbs.append(new_crumb) + else: + logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) + + while len(self._breadcrumbs) > max_breadcrumbs: + self._breadcrumbs.popleft() + self._n_breadcrumbs_truncated += 1 + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + try_autostart_continuous_profiler() + + custom_sampling_context = custom_sampling_context or {} + + # kwargs at this point has type TransactionKwargs, since we have removed + # the client and custom_sampling_context from it. + transaction_kwargs: "TransactionKwargs" = kwargs + + # if we haven't been given a transaction, make one + if transaction is None: + transaction = Transaction(**transaction_kwargs) + + # use traces_sample_rate, traces_sampler, and/or inheritance to make a + # sampling decision + sampling_context = { + "transaction_context": transaction.to_json(), + "parent_sampled": transaction.parent_sampled, + } + sampling_context.update(custom_sampling_context) + transaction._set_initial_sampling_decision(sampling_context=sampling_context) + + # update the sample rate in the dsc + if transaction.sample_rate is not None: + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction._baggage: + transaction._baggage.sentry_items["sample_rate"] = str( + transaction.sample_rate + ) + + if transaction.sampled: + profile = Profile( + transaction.sampled, transaction._start_timestamp_monotonic_ns + ) + profile._set_initial_sampling_decision(sampling_context=sampling_context) + + transaction._profile = profile + + transaction._continuous_profile = try_profile_lifecycle_trace_start() + + # Typically, the profiler is set when the transaction is created. But when + # using the auto lifecycle, the profiler isn't running when the first + # transaction is started. So make sure we update the profiler id on it. + if transaction._continuous_profile is not None: + transaction.set_profiler_id(get_profiler_id()) + + # we don't bother to keep spans if we already know we're not going to + # send the transaction + max_spans = (client.options["_experiments"].get("max_spans")) or 1000 + transaction.init_span_recorder(maxlen=max_spans) + + return transaction + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + warnings.warn( + "Scope.start_span is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return NoOpSpan() + + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + with new_scope(): + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + # get current span or transaction + span = self.span or self.get_isolation_scope().span + if isinstance(span, StreamedSpan): + # make mypy happy + return NoOpSpan() + + if span is None: + # New spans get the `trace_id` from the scope + if "trace_id" not in kwargs: + propagation_context = self.get_active_propagation_context() + kwargs["trace_id"] = propagation_context.trace_id + + span = Span(**kwargs) + else: + # Children take `trace_id`` from the parent span. + span = span.start_child(**kwargs) + + return span + + def start_streamed_span( + self, + name: str, + attributes: "Optional[Attributes]", + parent_span: "Optional[StreamedSpan]", + active: bool, + ) -> "StreamedSpan": + # TODO: rename to start_span once we drop the old API + if isinstance(parent_span, NoOpStreamedSpan): + # parent_span is only set if the user explicitly set it + logger.debug( + "Ignored parent span provided. Span will be parented to the " + "currently active span instead." + ) + + if parent_span is _DEFAULT_PARENT_SPAN or isinstance( + parent_span, NoOpStreamedSpan + ): + parent_span = self.streamed_span + + # If no eligible parent_span was provided and there is no currently + # active span, this is a segment + if parent_span is None: + propagation_context = self.get_active_propagation_context() + + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + scope=self, + unsampled_reason="ignored", + ) + + sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( + name, + attributes, + self, + ) + + if sample_rate is not None: + self._update_sample_rate(sample_rate) + + if sampled is False: + return NoOpStreamedSpan( + scope=self, + unsampled_reason=outcome, + ) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=None, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + parent_sampled=propagation_context.parent_sampled, + baggage=propagation_context.baggage, + sample_rand=sample_rand, + sample_rate=sample_rate, + ) + + # This is a child span; take propagation context from the parent span + with new_scope(): + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + unsampled_reason="ignored", + ) + + if isinstance(parent_span, NoOpStreamedSpan): + return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=parent_span._segment, + trace_id=parent_span.trace_id, + parent_span_id=parent_span.span_id, + parent_sampled=parent_span.sampled, + ) + + def _update_sample_rate(self, sample_rate: float) -> None: + # If we had to adjust the sample rate when setting the sampling decision + # for a span, it needs to be updated in the propagation context too + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(sample_rate) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", + ) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + self.generate_propagation_context(environ_or_headers) + + # generate_propagation_context ensures that the propagation_context is not None. + propagation_context = cast(PropagationContext, self._propagation_context) + + optional_kwargs = {} + if name: + optional_kwargs["name"] = name + if source: + optional_kwargs["source"] = source + + return Transaction( + op=op, + origin=origin, + baggage=propagation_context.baggage, + parent_sampled=propagation_context.parent_sampled, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + same_process_as_parent=False, + **optional_kwargs, + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures an event. + + Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + scope = self._merge_scopes(scope, scope_kwargs) + + event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) + + if event_id is not None and event.get("type") != "transaction": + self.get_isolation_scope()._last_event_id = event_id + + return event_id + + def _capture_log(self, log: "Optional[Log]") -> None: + if log is None: + return + + client = self.get_client() + if not has_logs_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" + ) + + client._capture_log(log, scope=merged_scope) + + def _capture_metric(self, metric: "Optional[Metric]") -> None: + if metric is None: + return + + client = self.get_client() + if not has_metrics_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" + ) + + client._capture_metric(metric, scope=merged_scope) + + def _capture_span(self, span: "Optional[StreamedSpan]") -> None: + if span is None: + return + + client = self.get_client() + if not has_span_streaming_enabled(client.options): + return + + merged_scope = self._merge_scopes() + client._capture_span(span, scope=merged_scope) + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures a message. + + :param message: The string to send as the message. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if level is None: + level = "info" + + event: "Event" = { + "message": message, + "level": level, + } + + return self.capture_event(event, scope=scope, **scope_kwargs) + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """Captures an exception. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if error is not None: + exc_info = exc_info_from_error(error) + else: + exc_info = sys.exc_info() + + event, hint = event_from_exception( + exc_info, client_options=self.get_client().options + ) + + try: + return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) + except Exception: + capture_internal_exception(sys.exc_info()) + + return None + + def start_session(self, *args: "Any", **kwargs: "Any") -> None: + """Starts a new session.""" + session_mode = kwargs.pop("session_mode", "application") + + self.end_session() + + client = self.get_client() + self._session = Session( + release=client.options.get("release"), + environment=client.options.get("environment"), + user=self._user, + session_mode=session_mode, + ) + + def end_session(self, *args: "Any", **kwargs: "Any") -> None: + """Ends the current session if there is one.""" + session = self._session + self._session = None + + if session is not None: + session.close() + self.get_client().capture_session(session) + + def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: + """Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + self.end_session() + self._force_auto_session_tracking = False + + def resume_auto_session_tracking(self) -> None: + """Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + self._force_auto_session_tracking = None + + def add_event_processor( + self, + func: "EventProcessor", + ) -> None: + """Register a scope local event processor on the scope. + + :param func: This function behaves like `before_send.` + """ + if len(self._event_processors) > 20: + logger.warning( + "Too many event processors on scope! Clearing list to free up some memory: %r", + self._event_processors, + ) + del self._event_processors[:] + + self._event_processors.append(func) + + def add_error_processor( + self, + func: "ErrorProcessor", + cls: "Optional[Type[BaseException]]" = None, + ) -> None: + """Register a scope local error processor on the scope. + + :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. + + :param cls: Optionally, only process exceptions of this type. + """ + if cls is not None: + cls_ = cls # For mypy. + real_func = func + + def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": + try: + is_inst = isinstance(exc_info[1], cls_) + except Exception: + is_inst = False + if is_inst: + return real_func(event, exc_info) + return event + + self._error_processors.append(func) + + def _apply_level_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._level is not None: + event["level"] = self._level + + def _apply_breadcrumbs_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + event.setdefault("breadcrumbs", {}) + + # This check is just for mypy - + if not isinstance(event["breadcrumbs"], AnnotatedValue): + event["breadcrumbs"].setdefault("values", []) + event["breadcrumbs"]["values"].extend(self._breadcrumbs) + + # Attempt to sort timestamps + try: + if not isinstance(event["breadcrumbs"], AnnotatedValue): + for crumb in event["breadcrumbs"]["values"]: + if isinstance(crumb["timestamp"], str): + crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) + + event["breadcrumbs"]["values"].sort( + key=lambda crumb: crumb["timestamp"] + ) + except Exception as err: + logger.debug("Error when sorting breadcrumbs", exc_info=err) + pass + + def _apply_user_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("user") is None and self._user is not None: + event["user"] = self._user + + def _apply_transaction_name_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction") is None and self._transaction is not None: + event["transaction"] = self._transaction + + def _apply_transaction_info_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction_info") is None and self._transaction_info is not None: + event["transaction_info"] = self._transaction_info + + def _apply_fingerprint_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("fingerprint") is None and self._fingerprint is not None: + event["fingerprint"] = self._fingerprint + + def _apply_extra_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._extras: + event.setdefault("extra", {}).update(self._extras) + + def _apply_tags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._tags: + event.setdefault("tags", {}).update(self._tags) + + def _apply_contexts_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._contexts: + event.setdefault("contexts", {}).update(self._contexts) + + contexts = event.setdefault("contexts", {}) + + # Add "trace" context + if contexts.get("trace") is None: + contexts["trace"] = self.get_trace_context() + + def _apply_flags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + flags = self.flags.get() + if len(flags) > 0: + event.setdefault("contexts", {}).setdefault("flags", {}).update( + {"values": flags} + ) + + def _apply_scope_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + # TODO: turn Logs, Metrics into actual classes + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + for attribute, value in self._attributes.items(): + if attribute not in attributes: + attributes[attribute] = value + + def _apply_user_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + if not should_send_default_pii() or self._user is None: + return + + for attribute_name, user_attribute in ( + ("user.id", "id"), + ("user.name", "username"), + ("user.email", "email"), + ("user.ip_address", "ip_address"), + ): + if ( + user_attribute in self._user + and attribute_name not in attributes + and self._user[user_attribute] is not None + ): + attributes[attribute_name] = self._user[user_attribute] + + def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": + logger.info("%s (%s) dropped event", ty, cause) + return None + + def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the error processors on the event and returns the modified event. + """ + exc_info = hint.get("exc_info") + if exc_info is not None: + error_processors = chain( + self.get_global_scope()._error_processors, + self.get_isolation_scope()._error_processors, + self.get_current_scope()._error_processors, + ) + + for error_processor in error_processors: + new_event = error_processor(event, exc_info) + if new_event is None: + return self._drop(error_processor, "error processor") + + event = new_event + + return event + + def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the event processors on the event and returns the modified event. + """ + ty = event.get("type") + is_check_in = ty == "check_in" + + if not is_check_in: + # Get scopes without creating them to prevent infinite recursion + isolation_scope = _isolation_scope.get() + current_scope = _current_scope.get() + + event_processors = chain( + global_event_processors, + _global_scope and _global_scope._event_processors or [], + isolation_scope and isolation_scope._event_processors or [], + current_scope and current_scope._event_processors or [], + ) + + for event_processor in event_processors: + new_event = event + with capture_internal_exceptions(): + new_event = event_processor(event, hint) + if new_event is None: + return self._drop(event_processor, "event processor") + event = new_event + + return event + + @_disable_capture + def apply_to_event( + self, + event: "Event", + hint: "Hint", + options: "Optional[Dict[str, Any]]" = None, + ) -> "Optional[Event]": + """Applies the information contained on the scope to the given event.""" + ty = event.get("type") + is_transaction = ty == "transaction" + is_check_in = ty == "check_in" + + # put all attachments into the hint. This lets callbacks play around + # with attachments. We also later pull this out of the hint when we + # create the envelope. + attachments_to_send = hint.get("attachments") or [] + for attachment in self._attachments: + if not is_transaction or attachment.add_to_transactions: + attachments_to_send.append(attachment) + hint["attachments"] = attachments_to_send + + self._apply_contexts_to_event(event, hint, options) + + if is_check_in: + # Check-ins only support the trace context, strip all others + event["contexts"] = { + "trace": event.setdefault("contexts", {}).get("trace", {}) + } + + if not is_check_in: + self._apply_level_to_event(event, hint, options) + self._apply_fingerprint_to_event(event, hint, options) + self._apply_user_to_event(event, hint, options) + self._apply_transaction_name_to_event(event, hint, options) + self._apply_transaction_info_to_event(event, hint, options) + self._apply_tags_to_event(event, hint, options) + self._apply_extra_to_event(event, hint, options) + + if not is_transaction and not is_check_in: + self._apply_breadcrumbs_to_event(event, hint, options) + self._apply_flags_to_event(event, hint, options) + + event = self.run_error_processors(event, hint) + if event is None: + return None + + event = self.run_event_processors(event, hint) + if event is None: + return None + + return event + + @_disable_capture + def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: + # Attributes-based events and telemetry go through here (logs, metrics, + # spansV2) + if not isinstance(telemetry, StreamedSpan): + trace_context = self.get_trace_context() + trace_id = trace_context.get("trace_id") + if telemetry.get("trace_id") is None and trace_id is not None: + telemetry["trace_id"] = trace_id + + # span_id should only be populated if there's an active span. We can't + # use the trace_context here because it synthesizes a span_id if there + # isn't one + if telemetry.get("span_id") is None: + if self._span is not None and not isinstance( + self._span, (NoOpStreamedSpan, NoOpSpan) + ): + telemetry["span_id"] = self._span.span_id + else: + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + _, span_id = external_propagation_context + if span_id is not None: + telemetry["span_id"] = span_id + + self._apply_scope_attributes_to_telemetry(telemetry) + self._apply_user_attributes_to_telemetry(telemetry) + + def update_from_scope(self, scope: "Scope") -> None: + """Update the scope with another scope's data.""" + if scope._level is not None: + self._level = scope._level + if scope._fingerprint is not None: + self._fingerprint = scope._fingerprint + if scope._transaction is not None: + self._transaction = scope._transaction + if scope._transaction_info is not None: + self._transaction_info.update(scope._transaction_info) + if scope._user is not None: + self._user = scope._user + if scope._tags: + self._tags.update(scope._tags) + if scope._contexts: + self._contexts.update(scope._contexts) + if scope._extras: + self._extras.update(scope._extras) + if scope._breadcrumbs: + self._breadcrumbs.extend(scope._breadcrumbs) + if scope._n_breadcrumbs_truncated: + self._n_breadcrumbs_truncated = ( + self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated + ) + if scope._gen_ai_original_message_count: + self._gen_ai_original_message_count.update( + scope._gen_ai_original_message_count + ) + if scope._gen_ai_conversation_id: + self._gen_ai_conversation_id = scope._gen_ai_conversation_id + if scope._span: + self._span = scope._span + if scope._attachments: + self._attachments.extend(scope._attachments) + if scope._profile: + self._profile = scope._profile + if scope._propagation_context: + self._propagation_context = scope._propagation_context + if scope._session: + self._session = scope._session + if scope._flags: + if not self._flags: + self._flags = deepcopy(scope._flags) + else: + for flag in scope._flags.get(): + self._flags.set(flag["flag"], flag["result"]) + if scope._attributes: + self._attributes.update(scope._attributes) + + def update_from_kwargs( + self, + user: "Optional[Any]" = None, + level: "Optional[LogLevelStr]" = None, + extras: "Optional[Dict[str, Any]]" = None, + contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, + tags: "Optional[Dict[str, str]]" = None, + fingerprint: "Optional[List[str]]" = None, + attributes: "Optional[Attributes]" = None, + ) -> None: + """Update the scope's attributes.""" + if level is not None: + self._level = level + if user is not None: + self._user = user + if extras is not None: + self._extras.update(extras) + if contexts is not None: + self._contexts.update(contexts) + if tags is not None: + self._tags.update(tags) + if fingerprint is not None: + self._fingerprint = fingerprint + if attributes is not None: + self._attributes.update(attributes) + + def __repr__(self) -> str: + return "<%s id=%s name=%s type=%s>" % ( + self.__class__.__name__, + hex(id(self)), + self._name, + self._type, + ) + + @property + def flags(self) -> "FlagBuffer": + if self._flags is None: + max_flags = ( + self.get_client().options["_experiments"].get("max_flags") + or DEFAULT_FLAG_CAPACITY + ) + self._flags = FlagBuffer(capacity=max_flags) + return self._flags + + def set_attribute(self, attribute: str, value: "AttributeValue") -> None: + """ + Set an attribute on the scope. + + Any attributes-based telemetry (logs, metrics) captured while this scope + is active will inherit attributes set on the scope. + """ + self._attributes[attribute] = format_attribute(value) + + def remove_attribute(self, attribute: str) -> None: + """Remove an attribute if set on the scope. No-op if there is no such attribute.""" + try: + del self._attributes[attribute] + except KeyError: + pass + + +@contextmanager +def new_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current scope and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.new_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + new_scope = current_scope.fork() + token = _current_scope.set(new_scope) + + try: + yield new_scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `scope` and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + Suppose the variable `scope` contains a `Scope` object, which is not currently + the active scope. + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.use_scope(scope): + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # set given scope as current scope + token = _current_scope.set(scope) + + try: + yield scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def isolation_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current isolation scope and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # fork isolation scope + isolation_scope = Scope.get_isolation_scope() + new_isolation_scope = isolation_scope.fork() + isolation_token = _isolation_scope.set(new_isolation_scope) + + try: + yield new_isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `isolation_scope` and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # set given scope as isolation scope + isolation_token = _isolation_scope.set(isolation_scope) + + try: + yield isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +def should_send_default_pii() -> bool: + """Shortcut for `Scope.get_client().should_send_default_pii()`.""" + return Scope.get_client().should_send_default_pii() + + +# Circular imports +from sentry_sdk.client import NonRecordingClient + +if TYPE_CHECKING: + import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py new file mode 100644 index 0000000000..6794491325 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING, Dict, List, cast + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + iter_event_frames, +) + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event + + +DEFAULT_DENYLIST = [ + # stolen from relay + "password", + "passwd", + "secret", + "api_key", + "apikey", + "auth", + "credentials", + "mysql_pwd", + "privatekey", + "private_key", + "token", + "session", + # django + "csrftoken", + "sessionid", + # wsgi + "x_csrftoken", + "x_forwarded_for", + "set_cookie", + "cookie", + "authorization", + "proxy-authorization", + "x_api_key", + # other common names used in the wild + "aiohttp_session", # aiohttp + "connect.sid", # Express + "csrf_token", # Pyramid + "csrf", # (this is a cookie name used in accepted answers on stack overflow) + "_csrf", # Express + "_csrf_token", # Bottle + "PHPSESSID", # PHP + "_session", # Sanic + "symfony", # Symfony + "user_session", # Vue + "_xsrf", # Tornado + "XSRF-TOKEN", # Angular, Laravel +] + +DEFAULT_PII_DENYLIST = [ + "x_forwarded_for", + "x_real_ip", + "ip_address", + "remote_addr", +] + + +class EventScrubber: + def __init__( + self, + denylist: "Optional[List[str]]" = None, + recursive: bool = False, + send_default_pii: bool = False, + pii_denylist: "Optional[List[str]]" = None, + ) -> None: + """ + A scrubber that goes through the event payload and removes sensitive data configured through denylists. + + :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. + :param recursive: Whether to scrub the event payload recursively, default False. + :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. + :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. + """ + self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist + + if not send_default_pii: + pii_denylist = ( + DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist + ) + self.denylist += pii_denylist + + self.denylist = [x.lower() for x in self.denylist] + self.recursive = recursive + + def scrub_list(self, lst: object) -> None: + """ + If a list is passed to this method, the method recursively searches the list and any + nested lists for any dictionaries. The method calls scrub_dict on all dictionaries + it finds. + If the parameter passed to this method is not a list, the method does nothing. + """ + if not isinstance(lst, list): + return + + for v in lst: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_dict(self, d: object) -> None: + """ + If a dictionary is passed to this method, the method scrubs the dictionary of any + sensitive data. The method calls itself recursively on any nested dictionaries ( + including dictionaries nested in lists) if self.recursive is True. + This method does nothing if the parameter passed to it is not a dictionary. + """ + if not isinstance(d, dict): + return + + for k, v in d.items(): + # The cast is needed because mypy is not smart enough to figure out that k must be a + # string after the isinstance check. + if isinstance(k, str) and k.lower() in self.denylist: + d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() + elif self.recursive: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_request(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "request" in event: + if "headers" in event["request"]: + self.scrub_dict(event["request"]["headers"]) + if "cookies" in event["request"]: + self.scrub_dict(event["request"]["cookies"]) + if "data" in event["request"]: + self.scrub_dict(event["request"]["data"]) + + def scrub_extra(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "extra" in event: + self.scrub_dict(event["extra"]) + + def scrub_user(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "user" in event: + user = event["user"] + if "ip_address" in self.denylist and isinstance(user, dict): + user.pop("ip_address", None) + self.scrub_dict(user) + + def scrub_breadcrumbs(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "breadcrumbs" in event: + if ( + not isinstance(event["breadcrumbs"], AnnotatedValue) + and "values" in event["breadcrumbs"] + ): + for value in event["breadcrumbs"]["values"]: + if "data" in value: + self.scrub_dict(value["data"]) + + def scrub_frames(self, event: "Event") -> None: + with capture_internal_exceptions(): + for frame in iter_event_frames(event): + if "vars" in frame: + self.scrub_dict(frame["vars"]) + + def scrub_spans(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "spans" in event: + for span in cast(List[Dict[str, object]], event["spans"]): + if "data" in span: + self.scrub_dict(span["data"]) + + def scrub_event(self, event: "Event") -> None: + self.scrub_request(event) + self.scrub_extra(event) + self.scrub_user(event) + self.scrub_breadcrumbs(event) + self.scrub_frames(event) + self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py new file mode 100644 index 0000000000..6bf6f6e70b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py @@ -0,0 +1,418 @@ +import math +import sys +from array import array +from collections.abc import Mapping +from datetime import datetime +from typing import TYPE_CHECKING + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exception, + disable_capture_event, + format_timestamp, + safe_repr, + strip_string, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union + + from sentry_sdk._types import NotImplementedType + + Span = Dict[str, Any] + + ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] + Segment = Union[str, int] + + +# Bytes are technically not strings in Python 3, but we can serialize them +serializable_str_types = (str, bytes, bytearray, memoryview) + + +# Maximum length of JSON-serialized event payloads that can be safely sent +# before the server may reject the event due to its size. This is not intended +# to reflect actual values defined server-side, but rather only be an upper +# bound for events sent by the SDK. +# +# Can be overwritten if wanting to send more bytes, e.g. with a custom server. +# When changing this, keep in mind that events may be a little bit larger than +# this value due to attached metadata, so keep the number conservative. +MAX_EVENT_BYTES = 10**6 + +# Maximum depth and breadth of databags. Excess data will be trimmed. If +# max_request_body_size is "always", request bodies won't be trimmed. +MAX_DATABAG_DEPTH = 5 +MAX_DATABAG_BREADTH = 10 +CYCLE_MARKER = "" + + +global_repr_processors: "List[ReprProcessor]" = [] + + +def add_global_repr_processor(processor: "ReprProcessor") -> None: + global_repr_processors.append(processor) + + +sequence_types: "List[type]" = [tuple, list, set, frozenset, array] + + +def add_repr_sequence_type(ty: type) -> None: + sequence_types.append(ty) + + +class Memo: + __slots__ = ("_ids", "_objs") + + def __init__(self) -> None: + self._ids: "Dict[int, Any]" = {} + self._objs: "List[Any]" = [] + + def memoize(self, obj: "Any") -> "ContextManager[bool]": + self._objs.append(obj) + return self + + def __enter__(self) -> bool: + obj = self._objs[-1] + if id(obj) in self._ids: + return True + else: + self._ids[id(obj)] = obj + return False + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> None: + self._ids.pop(id(self._objs.pop()), None) + + +class _Serializer: + """Holds the state of a single serialize() call.""" + + __slots__ = ( + "memo", + "path", + "meta_stack", + "keep_request_bodies", + "max_value_length", + "is_vars", + "custom_repr", + ) + + def __init__( + self, + keep_request_bodies: bool, + max_value_length: "Optional[int]", + is_vars: bool, + custom_repr: "Optional[Callable[..., Optional[str]]]", + ) -> None: + self.memo = Memo() + self.path: "List[Segment]" = [] + self.meta_stack: "List[Dict[str, Any]]" = [] + self.keep_request_bodies = keep_request_bodies + self.max_value_length = max_value_length + self.is_vars = is_vars + self.custom_repr = custom_repr + + def _safe_repr_wrapper(self, value: "Any") -> str: + try: + repr_value = None + if self.custom_repr is not None: + repr_value = self.custom_repr(value) + return repr_value or safe_repr(value) + except Exception: + return safe_repr(value) + + def _annotate(self, **meta: "Any") -> None: + while len(self.meta_stack) <= len(self.path): + try: + segment = self.path[len(self.meta_stack) - 1] + node = self.meta_stack[-1].setdefault(str(segment), {}) + except IndexError: + node = {} + + self.meta_stack.append(node) + + self.meta_stack[-1].setdefault("", {}).update(meta) + + def _is_databag(self) -> "Optional[bool]": + """ + A databag is any value that we need to trim. + True for stuff like vars, request bodies, breadcrumbs and extra. + + :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". + """ + try: + if self.is_vars: + return True + + is_request_body = self._is_request_body() + if is_request_body in (True, None): + return is_request_body + + p0 = self.path[0] + if p0 == "breadcrumbs" and self.path[1] == "values": + self.path[2] + return True + + if p0 == "extra": + return True + + except IndexError: + return None + + return False + + def _is_span_attribute(self) -> "Optional[bool]": + try: + if self.path[0] == "spans" and self.path[2] == "data": + return True + except IndexError: + return None + + return False + + def _is_request_body(self) -> "Optional[bool]": + try: + if self.path[0] == "request" and self.path[1] == "data": + return True + except IndexError: + return None + + return False + + def _serialize_node( + self, + obj: "Any", + is_databag: "Optional[bool]" = None, + is_request_body: "Optional[bool]" = None, + should_repr_strings: "Optional[bool]" = None, + segment: "Optional[Segment]" = None, + remaining_breadth: "Optional[Union[int, float]]" = None, + remaining_depth: "Optional[Union[int, float]]" = None, + ) -> "Any": + if segment is not None: + self.path.append(segment) + + try: + with self.memo.memoize(obj) as result: + if result: + return CYCLE_MARKER + + return self._serialize_node_impl( + obj, + is_databag=is_databag, + is_request_body=is_request_body, + should_repr_strings=should_repr_strings, + remaining_depth=remaining_depth, + remaining_breadth=remaining_breadth, + ) + except BaseException: + capture_internal_exception(sys.exc_info()) + + if is_databag: + return "" + + return None + finally: + if segment is not None: + self.path.pop() + del self.meta_stack[len(self.path) + 1 :] + + def _flatten_annotated(self, obj: "Any") -> "Any": + if isinstance(obj, AnnotatedValue): + self._annotate(**obj.metadata) + obj = obj.value + return obj + + def _serialize_node_impl( + self, + obj: "Any", + is_databag: "Optional[bool]", + is_request_body: "Optional[bool]", + should_repr_strings: "Optional[bool]", + remaining_depth: "Optional[Union[float, int]]", + remaining_breadth: "Optional[Union[float, int]]", + ) -> "Any": + if isinstance(obj, AnnotatedValue): + should_repr_strings = False + if should_repr_strings is None: + should_repr_strings = self.is_vars + + if is_databag is None: + is_databag = self._is_databag() + + if is_request_body is None: + is_request_body = self._is_request_body() + + if is_databag: + if is_request_body and self.keep_request_bodies: + remaining_depth = float("inf") + remaining_breadth = float("inf") + else: + if remaining_depth is None: + remaining_depth = MAX_DATABAG_DEPTH + if remaining_breadth is None: + remaining_breadth = MAX_DATABAG_BREADTH + + obj = self._flatten_annotated(obj) + + if remaining_depth is not None and remaining_depth <= 0: + self._annotate(rem=[["!limit", "x"]]) + if is_databag: + return self._flatten_annotated( + strip_string( + self._safe_repr_wrapper(obj), max_length=self.max_value_length + ) + ) + return None + + is_span_attribute = self._is_span_attribute() + if (is_databag or is_span_attribute) and global_repr_processors: + hints = {"memo": self.memo, "remaining_depth": remaining_depth} + for processor in global_repr_processors: + result = processor(obj, hints) + if result is not NotImplemented: + return self._flatten_annotated(result) + + sentry_repr = getattr(type(obj), "__sentry_repr__", None) + + if obj is None or isinstance(obj, (bool, int, float)): + if should_repr_strings or ( + isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) + ): + return self._safe_repr_wrapper(obj) + else: + return obj + + elif callable(sentry_repr): + return sentry_repr(obj) + + elif isinstance(obj, datetime): + return ( + str(format_timestamp(obj)) + if not should_repr_strings + else self._safe_repr_wrapper(obj) + ) + + elif isinstance(obj, Mapping): + # Create temporary copy here to avoid calling too much code that + # might mutate our dictionary while we're still iterating over it. + obj = dict(obj.items()) + + rv_dict: "Dict[str, Any]" = {} + i = 0 + + for k, v in obj.items(): + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) + break + + str_k = str(k) + v = self._serialize_node( + v, + segment=str_k, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + rv_dict[str_k] = v + i += 1 + + return rv_dict + + elif not isinstance(obj, serializable_str_types) and isinstance( + obj, tuple(sequence_types) + ): + rv_list = [] + + for i, v in enumerate(obj): # type: ignore[arg-type] + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) # type: ignore[arg-type] + break + + rv_list.append( + self._serialize_node( + v, + segment=i, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + ) + + return rv_list + + if should_repr_strings: + obj = self._safe_repr_wrapper(obj) + else: + if isinstance(obj, bytes) or isinstance(obj, bytearray): + obj = obj.decode("utf-8", "replace") + + if not isinstance(obj, str): + obj = self._safe_repr_wrapper(obj) + + is_span_description = ( + len(self.path) == 3 + and self.path[0] == "spans" + and self.path[-1] == "description" + ) + if is_span_description: + return obj + + return self._flatten_annotated( + strip_string(obj, max_length=self.max_value_length) + ) + + +def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": + """ + A very smart serializer that takes a dict and emits a json-friendly dict. + Currently used for serializing the final Event and also prematurely while fetching the stack + local variables for each frame in a stacktrace. + + It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. + The algorithm itself is a recursive graph walk down the data structures it encounters. + + It has the following responsibilities: + * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. + * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. + * Annotating the payload with the _meta field whenever trimming happens. + + :param max_request_body_size: If set to "always", will never trim request bodies. + :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. + :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. + :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. + + """ + serializer = _Serializer( + keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", + max_value_length=kwargs.pop("max_value_length", None), + is_vars=kwargs.pop("is_vars", False), + custom_repr=kwargs.pop("custom_repr", None), + ) + + disable_capture_event.set(True) + try: + serialized_event = serializer._serialize_node(event, **kwargs) + if ( + not serializer.is_vars + and serializer.meta_stack + and isinstance(serialized_event, dict) + ): + serialized_event["_meta"] = serializer.meta_stack[0] + + return serialized_event + finally: + disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py new file mode 100644 index 0000000000..3ffd071bbc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py @@ -0,0 +1,165 @@ +import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from sentry_sdk._types import SessionStatus + + +def _minute_trunc(ts: "datetime") -> "datetime": + return ts.replace(second=0, microsecond=0) + + +def _make_uuid( + val: "Union[str, uuid.UUID]", +) -> "uuid.UUID": + if isinstance(val, uuid.UUID): + return val + return uuid.UUID(val) + + +class Session: + def __init__( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + session_mode: str = "application", + ) -> None: + if sid is None: + sid = uuid.uuid4() + if started is None: + started = datetime.now(timezone.utc) + if status is None: + status = "ok" + self.status = status + self.did: "Optional[str]" = None + self.started = started + self.release: "Optional[str]" = None + self.environment: "Optional[str]" = None + self.duration: "Optional[float]" = None + self.user_agent: "Optional[str]" = None + self.ip_address: "Optional[str]" = None + self.session_mode: str = session_mode + self.errors = 0 + + self.update( + sid=sid, + did=did, + timestamp=timestamp, + duration=duration, + release=release, + environment=environment, + user_agent=user_agent, + ip_address=ip_address, + errors=errors, + user=user, + ) + + @property + def truncated_started(self) -> "datetime": + return _minute_trunc(self.started) + + def update( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + ) -> None: + # If a user is supplied we pull some data form it + if user: + if ip_address is None: + ip_address = user.get("ip_address") + if did is None: + did = user.get("id") or user.get("email") or user.get("username") + + if sid is not None: + self.sid = _make_uuid(sid) + if did is not None: + self.did = str(did) + if timestamp is None: + timestamp = datetime.now(timezone.utc) + self.timestamp = timestamp + if started is not None: + self.started = started + if duration is not None: + self.duration = duration + if release is not None: + self.release = release + if environment is not None: + self.environment = environment + if ip_address is not None: + self.ip_address = ip_address + if user_agent is not None: + self.user_agent = user_agent + if errors is not None: + self.errors = errors + + if status is not None: + self.status = status + + def close( + self, + status: "Optional[SessionStatus]" = None, + ) -> "Any": + if status is None and self.status == "ok": + status = "exited" + if status is not None: + self.update(status=status) + + def get_json_attrs( + self, + with_user_info: "Optional[bool]" = True, + ) -> "Any": + attrs = {} + if self.release is not None: + attrs["release"] = self.release + if self.environment is not None: + attrs["environment"] = self.environment + if with_user_info: + if self.ip_address is not None: + attrs["ip_address"] = self.ip_address + if self.user_agent is not None: + attrs["user_agent"] = self.user_agent + return attrs + + def to_json(self) -> "Any": + rv: "Dict[str, Any]" = { + "sid": str(self.sid), + "init": True, + "started": format_timestamp(self.started), + "timestamp": format_timestamp(self.timestamp), + "status": self.status, + } + if self.errors: + rv["errors"] = self.errors + if self.did is not None: + rv["did"] = self.did + if self.duration is not None: + rv["duration"] = self.duration + attrs = self.get_json_attrs() + if attrs: + rv["attrs"] = attrs + return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py new file mode 100644 index 0000000000..aabf874fcd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py @@ -0,0 +1,262 @@ +import os +import warnings +from contextlib import contextmanager +from threading import Event, Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.envelope import Envelope +from sentry_sdk.session import Session +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Generator, List, Optional, Union + + +def is_auto_session_tracking_enabled( + hub: "Optional[sentry_sdk.Hub]" = None, +) -> "Union[Any, bool, None]": + """DEPRECATED: Utility function to find out if session tracking is enabled.""" + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + + should_track = hub.scope._force_auto_session_tracking + + if should_track is None: + client_options = hub.client.options if hub.client else {} + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking( + hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: Use track_session instead + Starts and stops a session automatically around a block. + """ + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "Use track_session instead.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + should_track = is_auto_session_tracking_enabled(hub) + if should_track: + hub.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + hub.end_session() + + +def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: + """ + DEPRECATED: Utility function to find out if session tracking is enabled. + """ + + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + return _is_auto_session_tracking_enabled(scope) + + +def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: + """ + Utility function to find out if session tracking is enabled. + """ + + should_track = scope._force_auto_session_tracking + if should_track is None: + client_options = sentry_sdk.get_client().options + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking_scope( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: This function is a deprecated alias for track_session. + Starts and stops a session automatically around a block. + """ + + warnings.warn( + "This function is a deprecated alias for track_session and will be removed in the next major release.", + DeprecationWarning, + stacklevel=2, + ) + + with track_session(scope, session_mode=session_mode): + yield + + +@contextmanager +def track_session( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """ + Start a new session in the provided scope, assuming session tracking is enabled. + This is a no-op context manager if session tracking is not enabled. + """ + + should_track = _is_auto_session_tracking_enabled(scope) + if should_track: + scope.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + scope.end_session() + + +TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") +MAX_ENVELOPE_ITEMS = 100 + + +def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": + return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} + + +class SessionFlusher: + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + flush_interval: int = 60, + ) -> None: + self.capture_func = capture_func + self.flush_interval = flush_interval + self.pending_sessions: "List[Any]" = [] + self.pending_aggregates: "Dict[Any, Any]" = {} + self._thread: "Optional[Thread]" = None + self._thread_lock = Lock() + self._aggregate_lock = Lock() + self._thread_for_pid: "Optional[int]" = None + self.__shutdown_requested = Event() + + def flush(self) -> None: + pending_sessions = self.pending_sessions + self.pending_sessions = [] + + with self._aggregate_lock: + pending_aggregates = self.pending_aggregates + self.pending_aggregates = {} + + envelope = Envelope() + for session in pending_sessions: + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_session(session) + + for attrs, states in pending_aggregates.items(): + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_sessions(make_aggregate_envelope(states, attrs)) + + if len(envelope.items) > 0: + self.capture_func(envelope) + + def _ensure_running(self) -> None: + """ + Check that we have an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + running = True + while running: + running = not self.__shutdown_requested.wait(self.flush_interval) + self.flush() + + thread = Thread(target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.__shutdown_requested.set() + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def add_aggregate_session( + self, + session: "Session", + ) -> None: + # NOTE on `session.did`: + # the protocol can deal with buckets that have a distinct-id, however + # in practice we expect the python SDK to have an extremely high cardinality + # here, effectively making aggregation useless, therefore we do not + # aggregate per-did. + + # For this part we can get away with using the global interpreter lock + with self._aggregate_lock: + attrs = session.get_json_attrs(with_user_info=False) + primary_key = tuple(sorted(attrs.items())) + secondary_key = session.truncated_started # (, session.did) + states = self.pending_aggregates.setdefault(primary_key, {}) + state = states.setdefault(secondary_key, {}) + + if "started" not in state: + state["started"] = format_timestamp(session.truncated_started) + # if session.did is not None: + # state["did"] = session.did + if session.status == "crashed": + state["crashed"] = state.get("crashed", 0) + 1 + elif session.status == "abnormal": + state["abnormal"] = state.get("abnormal", 0) + 1 + elif session.errors > 0: + state["errored"] = state.get("errored", 0) + 1 + else: + state["exited"] = state.get("exited", 0) + 1 + + def add_session( + self, + session: "Session", + ) -> None: + if session.session_mode == "request": + self.add_aggregate_session(session) + else: + self.pending_sessions.append(session.to_json()) + self._ensure_running() + + def kill(self) -> None: + self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py new file mode 100644 index 0000000000..2dcc86bc47 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py @@ -0,0 +1,327 @@ +import io +import logging +import os +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from itertools import chain, product +from typing import TYPE_CHECKING + +import urllib3 + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Self + +from sentry_sdk.envelope import Envelope +from sentry_sdk.utils import ( + capture_internal_exceptions, + env_to_bool, +) +from sentry_sdk.utils import ( + logger as sentry_logger, +) + +logger = logging.getLogger("spotlight") + + +DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" +DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" + + +class SpotlightClient: + """ + A client for sending envelopes to Sentry Spotlight. + + Implements exponential backoff retry logic per the SDK spec: + - Logs error at least once when server is unreachable + - Does not log for every failed envelope + - Uses exponential backoff to avoid hammering an unavailable server + - Never blocks normal Sentry operation + """ + + # Exponential backoff settings + INITIAL_RETRY_DELAY = 1.0 # Start with 1 second + MAX_RETRY_DELAY = 60.0 # Max 60 seconds + + def __init__(self, url: str) -> None: + self.url = url + self.http = urllib3.PoolManager() + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time: float = 0.0 + + def capture_envelope(self, envelope: "Envelope") -> None: + # Check if we're in backoff period - skip sending to avoid blocking + if self._last_error_time > 0: + time_since_error = time.time() - self._last_error_time + if time_since_error < self._retry_delay: + # Still in backoff period, skip this envelope + return + + body = io.BytesIO() + envelope.serialize_into(body) + try: + req = self.http.request( + url=self.url, + body=body.getvalue(), + method="POST", + headers={ + "Content-Type": "application/x-sentry-envelope", + }, + ) + req.close() + # Success - reset backoff state + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time = 0.0 + except Exception as e: + self._last_error_time = time.time() + + # Increase backoff delay exponentially first, so logged value matches actual wait + self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) + + # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) + sentry_logger.warning( + "Failed to send envelope to Spotlight at %s: %s. " + "Will retry after %.1f seconds.", + self.url, + e, + self._retry_delay, + ) + + +try: + from django.conf import settings + from django.http import HttpRequest, HttpResponse, HttpResponseServerError + from django.utils.deprecation import MiddlewareMixin + + SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" + SPOTLIGHT_JS_SNIPPET_PATTERN = ( + "\n" + '\n' + ) + SPOTLIGHT_ERROR_PAGE_SNIPPET = ( + '\n' + '\n' + ) + CHARSET_PREFIX = "charset=" + BODY_TAG_NAME = "body" + BODY_CLOSE_TAG_POSSIBILITIES = tuple( + "".format("".join(chars)) + for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) + ) + + class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] + _spotlight_script: "Optional[str]" = None + _spotlight_url: "Optional[str]" = None + + def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: + super().__init__(get_response) + + import sentry_sdk.api + + self.sentry_sdk = sentry_sdk.api + + spotlight_client = self.sentry_sdk.get_client().spotlight + if spotlight_client is None: + sentry_logger.warning( + "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." + ) + return None + # Spotlight URL has a trailing `/stream` part at the end so split it off + self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") + + @property + def spotlight_script(self: "Self") -> "Optional[str]": + if self._spotlight_url is not None and self._spotlight_script is None: + try: + spotlight_js_url = urllib.parse.urljoin( + self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH + ) + req = urllib.request.Request( + spotlight_js_url, + method="HEAD", + ) + urllib.request.urlopen(req) + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_url=self._spotlight_url, + spotlight_js_url=spotlight_js_url, + ) + except urllib.error.URLError as err: + sentry_logger.debug( + "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", + spotlight_js_url, + exc_info=err, + ) + + return self._spotlight_script + + def process_response( + self: "Self", _request: "HttpRequest", response: "HttpResponse" + ) -> "Optional[HttpResponse]": + content_type_header = tuple( + p.strip() + for p in response.headers.get("Content-Type", "").lower().split(";") + ) + content_type = content_type_header[0] + if len(content_type_header) > 1 and content_type_header[1].startswith( + CHARSET_PREFIX + ): + encoding = content_type_header[1][len(CHARSET_PREFIX) :] + else: + encoding = "utf-8" + + if ( + self.spotlight_script is not None + and not response.streaming + and content_type == "text/html" + ): + content_length = len(response.content) + injection = self.spotlight_script.encode(encoding) + injection_site = next( + ( + idx + for idx in ( + response.content.rfind(body_variant.encode(encoding)) + for body_variant in BODY_CLOSE_TAG_POSSIBILITIES + ) + if idx > -1 + ), + content_length, + ) + + # This approach works even when we don't have a `` tag + response.content = ( + response.content[:injection_site] + + injection + + response.content[injection_site:] + ) + + if response.has_header("Content-Length"): + response.headers["Content-Length"] = content_length + len(injection) + + return response + + def process_exception( + self: "Self", _request: "HttpRequest", exception: Exception + ) -> "Optional[HttpResponseServerError]": + if not settings.DEBUG or not self._spotlight_url: + return None + + try: + spotlight = ( + urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") + ) + except urllib.error.URLError: + return None + else: + event_id = self.sentry_sdk.capture_exception(exception) + return HttpResponseServerError( + spotlight.replace( + "", + SPOTLIGHT_ERROR_PAGE_SNIPPET.format( + spotlight_url=self._spotlight_url, event_id=event_id + ), + ) + ) + +except ImportError: + settings = None + + +def _resolve_spotlight_url( + spotlight_config: "Any", sentry_logger: "Any" +) -> "Optional[str]": + """ + Resolve the Spotlight URL based on config and environment variable. + + Implements precedence rules per the SDK spec: + https://develop.sentry.dev/sdk/expected-features/spotlight/ + + Returns the resolved URL string, or None if Spotlight should be disabled. + """ + spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") + + # Parse env var to determine if it's a boolean or URL + spotlight_from_env: "Optional[bool]" = None + spotlight_env_url: "Optional[str]" = None + if spotlight_env_value: + parsed = env_to_bool(spotlight_env_value, strict=True) + if parsed is None: + # It's a URL string + spotlight_from_env = True + spotlight_env_url = spotlight_env_value + else: + spotlight_from_env = parsed + + # Apply precedence rules per spec: + # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules + if spotlight_config is False: + # Config explicitly disables spotlight - warn if env var was set + if spotlight_from_env: + sentry_logger.warning( + "Spotlight is disabled via spotlight=False config option, " + "ignoring SENTRY_SPOTLIGHT environment variable." + ) + return None + elif spotlight_config is True: + # Config enables spotlight with boolean true + # If env var has URL, use env var URL per spec + if spotlight_env_url: + return spotlight_env_url + else: + return DEFAULT_SPOTLIGHT_URL + elif isinstance(spotlight_config, str): + # Config has URL string - use config URL, warn if env var differs + if spotlight_env_value and spotlight_env_value != spotlight_config: + sentry_logger.warning( + "Spotlight URL from config (%s) takes precedence over " + "SENTRY_SPOTLIGHT environment variable (%s).", + spotlight_config, + spotlight_env_value, + ) + return spotlight_config + elif spotlight_config is None: + # No config - use env var + if spotlight_env_url: + return spotlight_env_url + elif spotlight_from_env: + return DEFAULT_SPOTLIGHT_URL + # else: stays None (disabled) + + return None + + +def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": + url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) + + if url is None: + return None + + # Only set up logging handler when spotlight is actually enabled + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.INFO) + + # Update options with resolved URL for consistency + options["spotlight"] = url + + with capture_internal_exceptions(): + if ( + settings is not None + and settings.DEBUG + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) + ): + middleware = settings.MIDDLEWARE + if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: + settings.MIDDLEWARE = type(middleware)( + chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) + ) + logger.info("Enabled Spotlight integration for Django") + + client = SpotlightClient(url) + logger.info("Enabled Spotlight using sidecar at %s", url) + + return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py new file mode 100644 index 0000000000..5ee7e8460b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py @@ -0,0 +1,843 @@ +""" +EXPERIMENTAL. Do not use in production. + +The API in this file is only meant to be used in span streaming mode. + +You can enable span streaming mode via +sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). +""" + +import sys +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + capture_internal_exceptions, + format_attribute, + get_current_thread_meta, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + Iterator, + Optional, + ParamSpec, + TypeVar, + Union, + overload, + ) + + from sentry_sdk._types import Attributes, AttributeValue, SpanJSON + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + + P = ParamSpec("P") + R = TypeVar("R") + + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +class SpanStatus(str, Enum): + OK = "ok" + ERROR = "error" + + def __str__(self) -> str: + return self.value + + +_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) + + +# Segment source, see +# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource +class SegmentSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_SEGMENT_SOURCES = [ + SegmentSource.URL, +] + + +SOURCE_FOR_STYLE = { + "endpoint": SegmentSource.COMPONENT, + "function_name": SegmentSource.COMPONENT, + "handler_name": SegmentSource.COMPONENT, + "method_and_path_pattern": SegmentSource.ROUTE, + "path": SegmentSource.URL, + "route_name": SegmentSource.COMPONENT, + "route_pattern": SegmentSource.ROUTE, + "uri_template": SegmentSource.ROUTE, + "url": SegmentSource.ROUTE, +} + + +# Sentinel value for an unset parent_span to be able to distinguish it from +# a None set by the user +_DEFAULT_PARENT_SPAN = object() + + +def start_span( + name: str, + attributes: "Optional[Attributes]" = None, + parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] + active: bool = True, +) -> "StreamedSpan": + """ + Start a span. + + EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() + instead. + + The span's parent, unless provided explicitly via the `parent_span` argument, + will be the current active span, if any. If there is none, this span will + become the root of a new span tree. If you explicitly want this span to be + top-level without a parent, set `parent_span=None`. + + `start_span()` can either be used as context manager or you can use the span + object it returns and explicitly end it via `span.end()`. The following is + equivalent: + + ```python + import sentry_sdk + + with sentry_sdk.traces.start_span(name="My Span"): + # do something + + # The span automatically finishes once the `with` block is exited + ``` + + ```python + import sentry_sdk + + span = sentry_sdk.traces.start_span(name="My Span") + # do something + span.end() + ``` + + To continue a trace from another service, call + `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. + + :param name: The name to identify this span by. + :type name: str + + :param attributes: Key-value attributes to set on the span from the start. + These will also be accessible in the traces sampler. + :type attributes: "Optional[Attributes]" + + :param parent_span: A span instance that the new span should consider its + parent. If not provided, the parent will be set to the currently active + span, if any. If set to `None`, this span will become a new root-level + span. + :type parent_span: "Optional[StreamedSpan]" + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to `False`. + :type active: bool + + :return: The span that has been started. + :rtype: StreamedSpan + """ + from sentry_sdk.tracing_utils import has_span_streaming_enabled + + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "sentry_sdk.start_transaction() and sentry_sdk.start_span() " + "instead.", + stacklevel=2, + ) + return NoOpStreamedSpan() + + return sentry_sdk.get_current_scope().start_streamed_span( + name, attributes, parent_span, active + ) + + +def continue_trace(incoming: "dict[str, Any]") -> None: + """ + Continue a trace from headers or environment variables. + + EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. + + This function sets the propagation context on the scope. Any span started + in the updated scope will belong under the trace extracted from the + provided propagation headers or environment variables. + + continue_trace() doesn't start any spans on its own. Use the start_span() + API for that. + """ + # This is set both on the isolation and the current scope for compatibility + # reasons. Conceptually, it belongs on the isolation scope, and it also + # used to be set there in non-span-first mode. But in span first mode, we + # start spans on the current scope, regardless of type, like JS does, so we + # need to set the propagation context there. + sentry_sdk.get_isolation_scope().generate_propagation_context( + incoming, + ) + sentry_sdk.get_current_scope().generate_propagation_context( + incoming, + ) + + +def new_trace() -> None: + """ + Resets the propagation context, forcing a new trace. + + EXPERIMENTAL. + + This function sets the propagation context on the scope. Any span started + in the updated scope will start its own trace. + + new_trace() doesn't start any spans on its own. Use the start_span() API + for that. + """ + sentry_sdk.get_isolation_scope().set_new_propagation_context() + sentry_sdk.get_current_scope().set_new_propagation_context() + + +class StreamedSpan: + """ + A span holds timing information of a block of code. + + Spans can have multiple child spans, thus forming a span tree. + + This is the Span First span implementation that streams spans. The original + transaction-based span implementation lives in tracing.Span. + """ + + __slots__ = ( + "_name", + "_attributes", + "_active", + "_span_id", + "_trace_id", + "_parent_span_id", + "_segment", + "_parent_sampled", + "_start_timestamp", + "_start_timestamp_monotonic_ns", + "_end_timestamp", + "_status", + "_scope", + "_previous_span_on_scope", + "_baggage", + "_sample_rand", + "_sample_rate", + "_continuous_profile", + ) + + def __init__( + self, + *, + name: str, + attributes: "Optional[Attributes]" = None, + active: bool = True, + scope: "sentry_sdk.Scope", + segment: "Optional[StreamedSpan]" = None, + trace_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + sample_rate: "Optional[float]" = None, + sample_rand: "Optional[float]" = None, + ): + self._name: str = name + self._active: bool = active + self._attributes: "Attributes" = { + "sentry.origin": "manual", + "sentry.trace_lifecycle": "stream", + } + + if attributes: + for attribute, value in attributes.items(): + self.set_attribute(attribute, value) + + self._scope = scope + + self._segment = segment or self + + self._trace_id: "Optional[str]" = trace_id + self._parent_span_id = parent_span_id + self._parent_sampled = parent_sampled + self._baggage = baggage + self._sample_rand = sample_rand + self._sample_rate = sample_rate + + self._start_timestamp = datetime.now(timezone.utc) + self._end_timestamp: "Optional[datetime]" = None + + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + + self._span_id: "Optional[str]" = None + + self._status = SpanStatus.OK.value + + self._update_active_thread() + + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._start_profile() + self._set_profile_id(get_profiler_id()) + + self._set_segment_attributes() + + self._start() + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__}(" + f"name={self._name}, " + f"trace_id={self.trace_id}, " + f"span_id={self.span_id}, " + f"parent_span_id={self._parent_span_id}, " + f"active={self._active})>" + ) + + def __enter__(self) -> "StreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore + return + + if value is not None and should_be_treated_as_error(ty, value): + self.status = SpanStatus.ERROR.value + + self._end() + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + """ + Finish this span and queue it for sending. + + :param end_timestamp: End timestamp to use instead of current time. + :type end_timestamp: "Optional[Union[float, datetime]]" + """ + self._end(end_timestamp) + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self.end(end_timestamp) + + def _start(self) -> None: + if self._active: + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore. + return + + # Stop the profiler + if self._is_segment() and self._continuous_profile is not None: + with capture_internal_exceptions(): + self._continuous_profile.stop() + + # Detach from scope + if self._active: + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + # Set attributes from the segment. These are set on span end on purpose + # so that we have the best chance to capture the segment's final name + # (since it might change during its lifetime) + self.set_attribute("sentry.segment.id", self._segment.span_id) + self.set_attribute("sentry.segment.name", self._segment.name) + + # Set the end timestamp + if end_timestamp is not None: + if isinstance(end_timestamp, (float, int)): + try: + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + except Exception: + pass + + if isinstance(end_timestamp, datetime): + self._end_timestamp = end_timestamp + else: + logger.debug( + "[Tracing] Failed to set end_timestamp. Using current time instead." + ) + + if self._end_timestamp is None: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self._end_timestamp = self._start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # Finally, queue the span for sending to Sentry + self._scope._capture_span(self) + + def get_attributes(self) -> "Attributes": + return self._attributes + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + self._attributes[key] = format_attribute(value) + + def set_attributes(self, attributes: "Attributes") -> None: + for key, value in attributes.items(): + self.set_attribute(key, value) + + def remove_attribute(self, key: str) -> None: + try: + del self._attributes[key] + except KeyError: + pass + + @property + def status(self) -> "str": + return self._status + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + if isinstance(status, Enum): + status = status.value + + if status not in _VALID_SPAN_STATUSES: + logger.debug( + f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' + ) + return + + self._status = status + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def active(self) -> bool: + return self._active + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @property + def sampled(self) -> "Optional[bool]": + return True + + @property + def start_timestamp(self) -> "Optional[datetime]": + return self._start_timestamp + + @property + def end_timestamp(self) -> "Optional[datetime]": + return self._end_timestamp + + def _is_segment(self) -> bool: + return self._segment is self + + def _update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + + if thread_id is not None: + self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_attribute(SPANDATA.THREAD_NAME, thread_name) + + def _dynamic_sampling_context(self) -> "dict[str, str]": + return self._segment._get_baggage().dynamic_sampling_context() + + def _to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def _to_baggage(self) -> "Optional[Baggage]": + if self._segment: + return self._segment._get_baggage() + return None + + def _get_baggage(self) -> "Baggage": + """ + Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with + the segment. + + The first time a new baggage with Sentry items is made, it will be frozen. + """ + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_segment(self) + + return self._baggage + + def _iter_headers(self) -> "Iterator[tuple[str, str]]": + if not self._segment: + return + + yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() + + baggage = self._segment._get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def _get_trace_context(self) -> "dict[str, Any]": + # Even if spans themselves are not event-based anymore, we need this + # to populate trace context on events + context: "dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self._parent_span_id, + "dynamic_sampling_context": self._dynamic_sampling_context(), + } + + if "sentry.op" in self._attributes: + context["op"] = self._attributes["sentry.op"] + if "sentry.origin" in self._attributes: + context["origin"] = self._attributes["sentry.origin"] + + return context + + def _set_profile_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_attribute("sentry.profiler_id", profiler_id) + + def _start_profile(self) -> None: + if not self._is_segment(): + return + + try_autostart_continuous_profiler() + + self._continuous_profile = try_profile_lifecycle_trace_start() + + def _set_segment_attributes(self) -> None: + if not self._is_segment(): + return + + client = sentry_sdk.get_client() + + self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") + self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) + self.set_attribute( + SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) + ) + + if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: + self.set_attribute( + SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() + ) + + def _to_json(self) -> "SpanJSON": + res: "SpanJSON" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "name": self._name if self._name is not None else "", + "status": self._status, + "is_segment": self._is_segment(), + "start_timestamp": self._start_timestamp.timestamp(), + } + + if self._end_timestamp: + res["end_timestamp"] = self._end_timestamp.timestamp() + + if self._parent_span_id: + res["parent_span_id"] = self._parent_span_id + + res["attributes"] = {k: v for k, v in self._attributes.items()} + + return res + + +class NoOpStreamedSpan(StreamedSpan): + __slots__ = ( + "_finished", + "_unsampled_reason", + ) + + def __init__( + self, + unsampled_reason: "Optional[str]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + ) -> None: + self._scope = scope # type: ignore[assignment] + self._unsampled_reason = unsampled_reason + + self._finished = False + + self._start() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(sampled={self.sampled})>" + + def __enter__(self) -> "NoOpStreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self._end() + + def _start(self) -> None: + if self._scope is None: + return + + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._finished: + return + + if self._unsampled_reason is not None: + client = sentry_sdk.get_client() + if client.is_active() and client.transport: + logger.debug( + f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" + ) + client.transport.record_lost_event( + reason=self._unsampled_reason, + data_category="span", + quantity=1, + ) + + if self._scope and hasattr(self, "_previous_span_on_scope"): + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + self._finished = True + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + self._end() + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self._end() + + def get_attributes(self) -> "Attributes": + return {} + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + pass + + def set_attributes(self, attributes: "Attributes") -> None: + pass + + def remove_attribute(self, key: str) -> None: + pass + + def _is_segment(self) -> bool: + return self._scope is not None + + @property + def status(self) -> "str": + return SpanStatus.OK.value + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + pass + + @property + def name(self) -> str: + return "" + + @name.setter + def name(self, value: str) -> None: + pass + + @property + def active(self) -> bool: + return True + + @property + def span_id(self) -> str: + return "0000000000000000" + + @property + def trace_id(self) -> str: + return "00000000000000000000000000000000" + + @property + def sampled(self) -> "Optional[bool]": + return False + + @property + def start_timestamp(self) -> "Optional[datetime]": + return None + + @property + def end_timestamp(self) -> "Optional[datetime]": + return None + + +if TYPE_CHECKING: + + @overload + def trace( + func: "Callable[P, R]", + ) -> "Callable[P, R]": ... + + @overload + def trace( + func: None = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a span around a function call. + + EXPERIMENTAL. Use @sentry_sdk.trace instead. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to False. + :type active: bool + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import ( + create_streaming_span_decorator, + ) + + decorator = create_streaming_span_decorator( + name=name, + attributes=attributes, + active=active, + ) + + if func: + return decorator(func) + else: + return decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[StreamedSpan]": + """ + Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. + + This function will only return a non-`None` value when the streaming trace lifecycle is enabled. + To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.streamed_span + return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py new file mode 100644 index 0000000000..1790e13fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py @@ -0,0 +1,1475 @@ +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.profiler.continuous_profiler import get_profiler_id +from sentry_sdk.utils import ( + capture_internal_exceptions, + get_current_thread_meta, + is_valid_sample_rate, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, MutableMapping + from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + overload, + ) + + from typing_extensions import TypedDict, Unpack + + P = ParamSpec("P") + R = TypeVar("R") + + from sentry_sdk._types import ( + Event, + MeasurementUnit, + MeasurementValue, + SamplingContext, + ) + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + from sentry_sdk.profiler.transaction_profiler import Profile + + class SpanKwargs(TypedDict, total=False): + trace_id: str + """ + The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + """ + + span_id: str + """The span ID of this span. If omitted, a new span ID will be generated.""" + + parent_span_id: str + """The span ID of the parent span, if applicable.""" + + same_process_as_parent: bool + """Whether this span is in the same process as the parent span.""" + + sampled: bool + """ + Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + """ + + op: str + """ + The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + """ + + description: str + """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" + + hub: "Optional[sentry_sdk.Hub]" + """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" + + status: str + """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" + + containing_transaction: "Optional[Transaction]" + """The transaction that this span belongs to.""" + + start_timestamp: "Optional[Union[datetime, float]]" + """ + The timestamp when the span started. If omitted, the current time + will be used. + """ + + scope: "sentry_sdk.Scope" + """The scope to use for this span. If not provided, we use the current scope.""" + + origin: str + """ + The origin of the span. + See https://develop.sentry.dev/sdk/performance/trace-origin/ + Default "manual". + """ + + name: str + """A string describing what operation is being performed within the span/transaction.""" + + class TransactionKwargs(SpanKwargs, total=False): + source: str + """ + A string describing the source of the transaction name. This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. + Default "custom". + """ + + parent_sampled: bool + """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" + + baggage: "Baggage" + """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" + + ProfileContext = TypedDict( + "ProfileContext", + { + "profiler_id": str, + }, + ) + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +# Transaction source +# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations +class TransactionSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_TRANSACTION_SOURCES = [ + TransactionSource.URL, +] + +SOURCE_FOR_STYLE = { + "endpoint": TransactionSource.COMPONENT, + "function_name": TransactionSource.COMPONENT, + "handler_name": TransactionSource.COMPONENT, + "method_and_path_pattern": TransactionSource.ROUTE, + "path": TransactionSource.URL, + "route_name": TransactionSource.COMPONENT, + "route_pattern": TransactionSource.ROUTE, + "uri_template": TransactionSource.ROUTE, + "url": TransactionSource.ROUTE, +} + + +def get_span_status_from_http_code(http_status_code: int) -> str: + """ + Returns the Sentry status corresponding to the given HTTP status code. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + if http_status_code < 400: + return SPANSTATUS.OK + + elif 400 <= http_status_code < 500: + if http_status_code == 403: + return SPANSTATUS.PERMISSION_DENIED + elif http_status_code == 404: + return SPANSTATUS.NOT_FOUND + elif http_status_code == 429: + return SPANSTATUS.RESOURCE_EXHAUSTED + elif http_status_code == 413: + return SPANSTATUS.FAILED_PRECONDITION + elif http_status_code == 401: + return SPANSTATUS.UNAUTHENTICATED + elif http_status_code == 409: + return SPANSTATUS.ALREADY_EXISTS + else: + return SPANSTATUS.INVALID_ARGUMENT + + elif 500 <= http_status_code < 600: + if http_status_code == 504: + return SPANSTATUS.DEADLINE_EXCEEDED + elif http_status_code == 501: + return SPANSTATUS.UNIMPLEMENTED + elif http_status_code == 503: + return SPANSTATUS.UNAVAILABLE + else: + return SPANSTATUS.INTERNAL_ERROR + + return SPANSTATUS.UNKNOWN_ERROR + + +class _SpanRecorder: + """Limits the number of spans recorded in a transaction.""" + + __slots__ = ("maxlen", "spans", "dropped_spans") + + def __init__(self, maxlen: int) -> None: + # FIXME: this is `maxlen - 1` only to preserve historical behavior + # enforced by tests. + # Either this should be changed to `maxlen` or the JS SDK implementation + # should be changed to match a consistent interpretation of what maxlen + # limits: either transaction+spans or only child spans. + self.maxlen = maxlen - 1 + self.spans: "List[Span]" = [] + self.dropped_spans: int = 0 + + def add(self, span: "Span") -> None: + if len(self.spans) > self.maxlen: + span._span_recorder = None + self.dropped_spans += 1 + else: + self.spans.append(span) + + +class Span: + """A span holds timing information of a block of code. + Spans can have multiple child spans thus forming a span tree. + + :param trace_id: The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + :param span_id: The span ID of this span. If omitted, a new span ID will be generated. + :param parent_span_id: The span ID of the parent span, if applicable. + :param same_process_as_parent: Whether this span is in the same process as the parent span. + :param sampled: Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + :param op: The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + :param description: A description of what operation is being performed within the span. + + .. deprecated:: 2.15.0 + Please use the `name` parameter, instead. + :param name: A string describing what operation is being performed within the span. + :param hub: The hub to use for this span. + + .. deprecated:: 2.0.0 + Please use the `scope` parameter, instead. + :param status: The span's status. Possible values are listed at + https://develop.sentry.dev/sdk/event-payloads/span/ + :param containing_transaction: The transaction that this span belongs to. + :param start_timestamp: The timestamp when the span started. If omitted, the current time + will be used. + :param scope: The scope to use for this span. If not provided, we use the current scope. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "same_process_as_parent", + "sampled", + "op", + "description", + "_measurements", + "start_timestamp", + "_start_timestamp_monotonic_ns", + "status", + "timestamp", + "_tags", + "_data", + "_span_recorder", + "hub", + "_context_manager_state", + "_containing_transaction", + "scope", + "origin", + "name", + "_flags", + "_flags_capacity", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + same_process_as_parent: bool = True, + sampled: "Optional[bool]" = None, + op: "Optional[str]" = None, + description: "Optional[str]" = None, + hub: "Optional[sentry_sdk.Hub]" = None, # deprecated + status: "Optional[str]" = None, + containing_transaction: "Optional[Transaction]" = None, + start_timestamp: "Optional[Union[datetime, float]]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + origin: str = "manual", + name: "Optional[str]" = None, + ) -> None: + self._trace_id = trace_id + self._span_id = span_id + self.parent_span_id = parent_span_id + self.same_process_as_parent = same_process_as_parent + self.sampled = sampled + self.op = op + self.description = name or description + self.status = status + self.hub = hub # backwards compatibility + self.scope = scope + self.origin = origin + self._measurements: "Dict[str, MeasurementValue]" = {} + self._tags: "MutableMapping[str, str]" = {} + self._data: "Dict[str, Any]" = {} + self._containing_transaction = containing_transaction + self._flags: "Dict[str, bool]" = {} + self._flags_capacity = 10 + + if hub is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use `scope` instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.scope = self.scope or hub.scope + + if start_timestamp is None: + start_timestamp = datetime.now(timezone.utc) + elif isinstance(start_timestamp, float): + start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) + self.start_timestamp = start_timestamp + try: + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + except AttributeError: + pass + + #: End timestamp of span + self.timestamp: "Optional[datetime]" = None + + self._span_recorder: "Optional[_SpanRecorder]" = None + + self.update_active_thread() + self.set_profiler_id(get_profiler_id()) + + # TODO this should really live on the Transaction class rather than the Span + # class + def init_span_recorder(self, maxlen: int) -> None: + if self._span_recorder is None: + self._span_recorder = _SpanRecorder(maxlen) + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + def __repr__(self) -> str: + return ( + "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.op, + self.description, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.origin, + ) + ) + + def __enter__(self) -> "Span": + scope = self.scope or sentry_sdk.get_current_scope() + old_span = scope.span + scope.span = self + self._context_manager_state = (scope, old_span) + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if value is not None and should_be_treated_as_error(ty, value): + self.set_status(SPANSTATUS.INTERNAL_ERROR) + + with capture_internal_exceptions(): + scope, old_span = self._context_manager_state + del self._context_manager_state + self.finish(scope) + scope.span = old_span + + @property + def containing_transaction(self) -> "Optional[Transaction]": + """The ``Transaction`` that this span belongs to. + The ``Transaction`` is the root of the span tree, + so one could also think of this ``Transaction`` as the "root span".""" + + # this is a getter rather than a regular attribute so that transactions + # can return `self` here instead (as a way to prevent them circularly + # referencing themselves) + return self._containing_transaction + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a sub-span from the current span or transaction. + + Takes the same arguments as the initializer of :py:class:`Span`. The + trace id, sampling decision, transaction pointer, and span recorder are + inherited from the current span/transaction. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + kwargs.setdefault("sampled", self.sampled) + + child = Span( + trace_id=self.trace_id, + parent_span_id=self.span_id, + containing_transaction=self.containing_transaction, + **kwargs, + ) + + span_recorder = ( + self.containing_transaction and self.containing_transaction._span_recorder + ) + if span_recorder: + span_recorder.add(child) + + return child + + @classmethod + def continue_from_environ( + cls, + environ: "Mapping[str, str]", + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a Transaction with the given params, then add in data pulled from + the ``sentry-trace`` and ``baggage`` headers from the environ (if any) + before returning the Transaction. + + This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` + in that it assumes header names in the form ``HTTP_HEADER_NAME`` - + such as you would get from a WSGI/ASGI environ - + rather than the form ``header-name``. + + :param environ: The ASGI/WSGI environ to pull information from. + """ + return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) + + @classmethod + def continue_from_headers( + cls, + headers: "Mapping[str, str]", + *, + _sample_rand: "Optional[str]" = None, + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a transaction with the given params (including any data pulled from + the ``sentry-trace`` and ``baggage`` headers). + + :param headers: The dictionary with the HTTP headers to pull information from. + :param _sample_rand: If provided, we override the sample_rand value from the + incoming headers with this value. (internal use only) + """ + logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") + + # TODO-neel move away from this kwargs stuff, it's confusing and opaque + # make more explicit + baggage = Baggage.from_incoming_header( + headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand + ) + kwargs.update({BAGGAGE_HEADER_NAME: baggage}) + + sentrytrace_kwargs = extract_sentrytrace_data( + headers.get(SENTRY_TRACE_HEADER_NAME) + ) + + if sentrytrace_kwargs is not None: + kwargs.update(sentrytrace_kwargs) + + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and immutable and won't be populated as head SDK. + baggage.freeze() + + transaction = Transaction(**kwargs) + transaction.same_process_as_parent = False + + return transaction + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. + If the span's containing transaction doesn't yet have a ``baggage`` value, + this will cause one to be generated and stored. + """ + if not self.containing_transaction: + # Do not propagate headers if there is no containing transaction. Otherwise, this + # span ends up being the root span of a new trace, and since it does not get sent + # to Sentry, the trace will be missing a root transaction. The dynamic sampling + # context will also be missing, breaking dynamic sampling & traces. + return + + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.containing_transaction.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + @classmethod + def from_traceparent( + cls, + traceparent: "Optional[str]", + **kwargs: "Any", + ) -> "Optional[Transaction]": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a ``Transaction`` with the given params, then add in data pulled from + the given ``sentry-trace`` header value before returning the ``Transaction``. + """ + if not traceparent: + return None + + return cls.continue_from_headers( + {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs + ) + + def to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def to_baggage(self) -> "Optional[Baggage]": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with this ``Span``, if any. (Taken from the root of the span tree.) + """ + if self.containing_transaction: + return self.containing_transaction.get_baggage() + return None + + def set_tag(self, key: str, value: "Any") -> None: + self._tags[key] = value + + def set_data(self, key: str, value: "Any") -> None: + self._data[key] = value + + def update_data(self, data: "Dict[str, Any]") -> None: + self._data.update(data) + + def set_flag(self, flag: str, result: bool) -> None: + if len(self._flags) < self._flags_capacity: + self._flags[flag] = result + + def set_status(self, value: str) -> None: + self.status = value + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_thread( + self, thread_id: "Optional[int]", thread_name: "Optional[str]" + ) -> None: + if thread_id is not None: + self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_data(SPANDATA.THREAD_NAME, thread_name) + + def set_profiler_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) + + def set_http_status(self, http_status: int) -> None: + self.set_tag( + "http.status_code", str(http_status) + ) # TODO-neel remove in major, we keep this for backwards compatibility + self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) + self.set_status(get_span_status_from_http_code(http_status)) + + def is_success(self) -> bool: + return self.status == "ok" + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + ) -> "Optional[str]": + """ + Sets the end timestamp of the span. + + Additionally it also creates a breadcrumb from the span, + if the span represents a database or HTTP request. + + :param scope: The scope to use for this transaction. + If not provided, the current scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + + :return: Always ``None``. The type is ``Optional[str]`` to match + the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. + """ + if self.timestamp is not None: + # This span is already finished, ignore. + return None + + try: + if end_timestamp: + if isinstance(end_timestamp, float): + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + self.timestamp = end_timestamp + else: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self.timestamp = self.start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + except AttributeError: + self.timestamp = datetime.now(timezone.utc) + + scope = scope or sentry_sdk.get_current_scope() + + # Copy conversation_id from scope to span data if this is an AI span + conversation_id = scope.get_conversation_id() + if conversation_id: + has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data + is_ai_span_op = self.op is not None and ( + self.op.startswith("ai.") or self.op.startswith("gen_ai.") + ) + if has_ai_op or is_ai_span_op: + self.set_data("gen_ai.conversation.id", conversation_id) + + maybe_create_breadcrumbs_from_span(scope, self) + + return None + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the span.""" + + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "same_process_as_parent": self.same_process_as_parent, + "op": self.op, + "description": self.description, + "start_timestamp": self.start_timestamp, + "timestamp": self.timestamp, + "origin": self.origin, + } + + if self.status: + rv["status"] = self.status + # TODO-neel remove redundant tag in major + self._tags["status"] = self.status + + if len(self._measurements) > 0: + rv["measurements"] = self._measurements + + tags = self._tags + if tags: + rv["tags"] = tags + + data = {} + data.update(self._flags) + data.update(self._data) + if data: + rv["data"] = data + + return rv + + def get_trace_context(self) -> "Any": + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "op": self.op, + "description": self.description, + "origin": self.origin, + } + if self.status: + rv["status"] = self.status + + if self.containing_transaction: + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) + + data = {} + + thread_id = self._data.get(SPANDATA.THREAD_ID) + if thread_id is not None: + data["thread.id"] = thread_id + + thread_name = self._data.get(SPANDATA.THREAD_NAME) + if thread_name is not None: + data["thread.name"] = thread_name + + if data: + rv["data"] = data + + return rv + + def get_profile_context(self) -> "Optional[ProfileContext]": + profiler_id = self._data.get(SPANDATA.PROFILER_ID) + if profiler_id is None: + return None + + return { + "profiler_id": profiler_id, + } + + def update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + self.set_thread(thread_id, thread_name) + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +class Transaction(Span): + """The Transaction is the root element that holds all the spans + for Sentry performance instrumentation. + + :param name: Identifier of the transaction. + Will show up in the Sentry UI. + :param parent_sampled: Whether the parent transaction was sampled. + If True this transaction will be kept, if False it will be discarded. + :param baggage: The W3C baggage header value. + (see https://www.w3.org/TR/baggage/) + :param source: A string describing the source of the transaction name. + This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations + for more information. Default "custom". + :param kwargs: Additional arguments to be passed to the Span constructor. + See :py:class:`sentry_sdk.tracing.Span` for available arguments. + """ + + __slots__ = ( + "name", + "source", + "parent_sampled", + # used to create baggage value for head SDKs in dynamic sampling + "sample_rate", + "_measurements", + "_contexts", + "_profile", + "_continuous_profile", + "_baggage", + "_sample_rand", + ) + + def __init__( # type: ignore[misc] + self, + name: str = "", + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + source: str = TransactionSource.CUSTOM, + **kwargs: "Unpack[SpanKwargs]", + ) -> None: + super().__init__(**kwargs) + + self.name = name + self.source = source + self.sample_rate: "Optional[float]" = None + self.parent_sampled = parent_sampled + self._measurements: "Dict[str, MeasurementValue]" = {} + self._contexts: "Dict[str, Any]" = {} + self._profile: "Optional[Profile]" = None + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._baggage = baggage + + baggage_sample_rand = ( + None if self._baggage is None else self._baggage._sample_rand() + ) + if baggage_sample_rand is not None: + self._sample_rand = baggage_sample_rand + else: + self._sample_rand = _generate_sample_rand(self.trace_id) + + def __repr__(self) -> str: + return ( + "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.name, + self.op, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.source, + self.origin, + ) + ) + + def _possibly_started(self) -> bool: + """Returns whether the transaction might have been started. + + If this returns False, we know that the transaction was not started + with sentry_sdk.start_transaction, and therefore the transaction will + be discarded. + """ + + # We must explicitly check self.sampled is False since self.sampled can be None + return self._span_recorder is not None or self.sampled is False + + def __enter__(self) -> "Transaction": + if not self._possibly_started(): + logger.debug( + "Transaction was entered without being started with sentry_sdk.start_transaction." + "The transaction will not be sent to Sentry. To fix, start the transaction by" + "passing it to sentry_sdk.start_transaction." + ) + + super().__enter__() + + if self._profile is not None: + self._profile.__enter__() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._profile is not None: + self._profile.__exit__(ty, value, tb) + + if self._continuous_profile is not None: + self._continuous_profile.stop() + + super().__exit__(ty, value, tb) + + @property + def containing_transaction(self) -> "Transaction": + """The root element of the span tree. + In the case of a transaction it is the transaction itself. + """ + + # Transactions (as spans) belong to themselves (as transactions). This + # is a getter rather than a regular attribute to avoid having a circular + # reference. + return self + + def _get_scope_from_finish_args( + self, + scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + ) -> "Optional[sentry_sdk.Scope]": + """ + Logic to get the scope from the arguments passed to finish. This + function exists for backwards compatibility with the old finish. + + TODO: Remove this function in the next major version. + """ + scope_or_hub = scope_arg + if hub_arg is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", + DeprecationWarning, + stacklevel=3, + ) + + scope_or_hub = hub_arg + + if isinstance(scope_or_hub, sentry_sdk.Hub): + warnings.warn( + "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", + DeprecationWarning, + stacklevel=3, + ) + + return scope_or_hub.scope + + return scope_or_hub + + def _get_log_representation(self) -> str: + return "{op}transaction <{name}>".format( + op=("<" + self.op + "> " if self.op else ""), name=self.name + ) + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """Finishes the transaction and sends it to Sentry. + All finished spans in the transaction will also be sent to Sentry. + + :param scope: The Scope to use for this transaction. + If not provided, the current Scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + :param hub: The hub to use for this transaction. + This argument is DEPRECATED. Please use the `scope` + parameter, instead. + + :return: The event ID if the transaction was sent to Sentry, + otherwise None. + """ + if self.timestamp is not None: + # This transaction is already finished, ignore. + return None + + # For backwards compatibility, we must handle the case where `scope` + # or `hub` could both either be a `Scope` or a `Hub`. + scope = self._get_scope_from_finish_args(scope, hub) + + scope = scope or self.scope or sentry_sdk.get_current_scope() + client = sentry_sdk.get_client() + + if not client.is_active(): + # We have no active client and therefore nowhere to send this transaction. + return None + + if self._span_recorder is None: + # Explicit check against False needed because self.sampled might be None + if self.sampled is False: + logger.debug("Discarding transaction because sampled = False") + else: + logger.debug( + "Discarding transaction because it was not started with sentry_sdk.start_transaction" + ) + + # This is not entirely accurate because discards here are not + # exclusively based on sample rate but also traces sampler, but + # we handle this the same here. + if client.transport and has_tracing_enabled(client.options): + if client.monitor and client.monitor.downsample_factor > 0: + reason = "backpressure" + else: + reason = "sample_rate" + + client.transport.record_lost_event(reason, data_category="transaction") + + # Only one span (the transaction itself) is discarded, since we did not record any spans here. + client.transport.record_lost_event(reason, data_category="span") + return None + + if not self.name: + logger.warning( + "Transaction has no name, falling back to ``." + ) + self.name = "" + + super().finish(scope, end_timestamp) + + status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) + if ( + status_code is not None + and status_code in client.options["trace_ignore_status_codes"] + ): + logger.debug( + "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( + transaction_description=self._get_log_representation(), + status_code=self._data[SPANDATA.HTTP_STATUS_CODE], + trace_ignore_status_codes=client.options[ + "trace_ignore_status_codes" + ], + ) + ) + if client.transport: + client.transport.record_lost_event( + "event_processor", data_category="transaction" + ) + + num_spans = len(self._span_recorder.spans) + 1 + client.transport.record_lost_event( + "event_processor", data_category="span", quantity=num_spans + ) + + self.sampled = False + + if not self.sampled: + # At this point a `sampled = None` should have already been resolved + # to a concrete decision. + if self.sampled is None: + logger.warning("Discarding transaction without sampling decision.") + + return None + + finished_spans = [] + has_gen_ai_span = False + if client.options.get("stream_gen_ai_spans", True): + for span in self._span_recorder.spans: + if span.timestamp is None: + continue + + if isinstance(span.op, str) and span.op.startswith("gen_ai."): + has_gen_ai_span = True + + finished_spans.append(span.to_json()) + else: + finished_spans = [ + span.to_json() + for span in self._span_recorder.spans + if span.timestamp is not None + ] + + len_diff = len(self._span_recorder.spans) - len(finished_spans) + dropped_spans = len_diff + self._span_recorder.dropped_spans + + # we do this to break the circular reference of transaction -> span + # recorder -> span -> containing transaction (which is where we started) + # before either the spans or the transaction goes out of scope and has + # to be garbage collected + self._span_recorder = None + + contexts = {} + contexts.update(self._contexts) + contexts.update({"trace": self.get_trace_context()}) + profile_context = self.get_profile_context() + if profile_context is not None: + contexts.update({"profile": profile_context}) + + event: "Event" = { + "type": "transaction", + "transaction": self.name, + "transaction_info": {"source": self.source}, + "contexts": contexts, + "tags": self._tags, + "timestamp": self.timestamp, + "start_timestamp": self.start_timestamp, + "spans": finished_spans, + } + + if dropped_spans > 0: + event["_dropped_spans"] = dropped_spans + + if has_gen_ai_span: + event["_has_gen_ai_span"] = True + + if self._profile is not None and self._profile.valid(): + event["profile"] = self._profile + self._profile = None + + event["measurements"] = self._measurements + + return scope.capture_event(event) + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + """Sets a context. Transactions can have multiple contexts + and they should follow the format described in the "Contexts Interface" + documentation. + + :param key: The name of the context. + :param value: The information about the context. + """ + self._contexts[key] = value + + def set_http_status(self, http_status: int) -> None: + """Sets the status of the Transaction according to the given HTTP status. + + :param http_status: The HTTP status code.""" + super().set_http_status(http_status) + self.set_context("response", {"status_code": http_status}) + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the transaction.""" + rv = super().to_json() + + rv["name"] = self.name + rv["source"] = self.source + rv["sampled"] = self.sampled + + return rv + + def get_trace_context(self) -> "Any": + trace_context = super().get_trace_context() + + if self._data: + trace_context["data"] = self._data + + return trace_context + + def get_baggage(self) -> "Baggage": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with the Transaction. + + The first time a new baggage with Sentry items is made, + it will be frozen.""" + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_transaction(self) + + return self._baggage + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the transaction's sampling decision, according to the following + precedence rules: + + 1. If a sampling decision is passed to `start_transaction` + (`start_transaction(name: "my transaction", sampled: True)`), that + decision will be used, regardless of anything else + + 2. If `traces_sampler` is defined, its decision will be used. It can + choose to keep or ignore any parent sampling decision, or use the + sampling context data to make its own decision or to choose a sample + rate for the transaction. + + 3. If `traces_sampler` is not defined, but there's a parent sampling + decision, the parent sampling decision will be used. + + 4. If `traces_sampler` is not defined and there's no parent sampling + decision, `traces_sample_rate` will be used. + """ + client = sentry_sdk.get_client() + + transaction_description = self._get_log_representation() + + # nothing to do if tracing is disabled + if not has_tracing_enabled(client.options): + self.sampled = False + return + + # if the user has forced a sampling decision by passing a `sampled` + # value when starting the transaction, go with that + if self.sampled is not None: + self.sample_rate = float(self.sampled) + return + + # we would have bailed already if neither `traces_sampler` nor + # `traces_sample_rate` were defined, so one of these should work; prefer + # the hook if so + sample_rate = ( + client.options["traces_sampler"](sampling_context) + if callable(client.options.get("traces_sampler")) + # default inheritance behavior + else ( + sampling_context["parent_sampled"] + if sampling_context["parent_sampled"] is not None + else client.options["traces_sample_rate"] + ) + ) + + # Since this is coming from the user (or from a function provided by the + # user), who knows what we might get. (The only valid values are + # booleans or numbers between 0 and 1.) + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning( + "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( + transaction_description=transaction_description, + ) + ) + self.sampled = False + return + + self.sample_rate = float(sample_rate) + + if client.monitor: + self.sample_rate /= 2**client.monitor.downsample_factor + + # if the function returned 0 (or false), or if `traces_sample_rate` is + # 0, it's a sign the transaction should be dropped + if not self.sample_rate: + logger.debug( + "[Tracing] Discarding {transaction_description} because {reason}".format( + transaction_description=transaction_description, + reason=( + "traces_sampler returned 0 or False" + if callable(client.options.get("traces_sampler")) + else "traces_sample_rate is set to 0" + ), + ) + ) + self.sampled = False + return + + # Now we roll the dice. + self.sampled = self._sample_rand < self.sample_rate + + if self.sampled: + logger.debug( + "[Tracing] Starting {transaction_description}".format( + transaction_description=transaction_description, + ) + ) + else: + logger.debug( + "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( + transaction_description=transaction_description, + sample_rate=self.sample_rate, + ) + ) + + # Private aliases matching StreamedSpan's private API + _get_baggage = get_baggage + _get_trace_context = get_trace_context + + +class NoOpSpan(Span): + def __repr__(self) -> str: + return "<%s>" % self.__class__.__name__ + + @property + def containing_transaction(self) -> "Optional[Transaction]": + return None + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "NoOpSpan": + return NoOpSpan() + + def to_traceparent(self) -> str: + return "" + + def to_baggage(self) -> "Optional[Baggage]": + return None + + def get_baggage(self) -> "Optional[Baggage]": + return None + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + return iter(()) + + def set_tag(self, key: str, value: "Any") -> None: + pass + + def set_data(self, key: str, value: "Any") -> None: + pass + + def update_data(self, data: "Dict[str, Any]") -> None: + pass + + def set_status(self, value: str) -> None: + pass + + def set_http_status(self, http_status: int) -> None: + pass + + def is_success(self) -> bool: + return True + + def to_json(self) -> "Dict[str, Any]": + return {} + + def get_trace_context(self) -> "Any": + return {} + + def get_profile_context(self) -> "Any": + return {} + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """ + The `hub` parameter is deprecated. Please use the `scope` parameter, instead. + """ + pass + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + pass + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + pass + + def init_span_recorder(self, maxlen: int) -> None: + pass + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + pass + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _get_baggage = get_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +if TYPE_CHECKING: + + @overload + def trace( + func: None = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": + # Handles: @trace() and @trace(op="custom") + pass + + @overload + def trace(func: "Callable[P, R]") -> "Callable[P, R]": + # Handles: @trace + pass + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a child span around a function call. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, a default operation will + be assigned based on the template. + :type op: str or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP, SPANTEMPLATE + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + op=OP.DB_QUERY, + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + + # With a custom template + @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) + def calculate_interest_rate(amount, rate, years): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import create_span_decorator + + decorator = create_span_decorator( + op=op, + name=name, + attributes=attributes, + template=template, + ) + + if func: + return decorator(func) + else: + return decorator + + +# Circular imports + +from sentry_sdk.tracing_utils import ( + Baggage, + EnvironHeaders, + _generate_sample_rand, + extract_sentrytrace_data, + has_tracing_enabled, + maybe_create_breadcrumbs_from_span, +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py new file mode 100644 index 0000000000..c2e34a795b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py @@ -0,0 +1,1694 @@ +import contextlib +import functools +import inspect +import os +import re +import sys +import uuid +import warnings +from collections.abc import Mapping, MutableMapping +from datetime import datetime, timedelta, timezone +from random import Random +from urllib.parse import quote, unquote + +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.utils import ( + _is_external_source, + _is_in_project_root, + _module_in_list, + capture_internal_exceptions, + filename_for_module, + is_sentry_url, + is_valid_sample_rate, + logger, + match_regex_list, + qualname_from_function, + safe_repr, + to_string, + try_convert, +) + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union + + from sentry_sdk._types import Attributes + + +SENTRY_TRACE_REGEX = re.compile( + "^[ \t]*" # whitespace + "([0-9a-f]{32})?" # trace_id + "-?([0-9a-f]{16})?" # span_id + "-?([01])?" # sampled + "[ \t]*$" # whitespace +) + + +# This is a normal base64 regex, modified to reflect that fact that we strip the +# trailing = or == off +base64_stripped = ( + # any of the characters in the base64 "alphabet", in multiples of 4 + "([a-zA-Z0-9+/]{4})*" + # either nothing or 2 or 3 base64-alphabet characters (see + # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for + # why there's never only 1 extra character) + "([a-zA-Z0-9+/]{2,3})?" +) + + +class EnvironHeaders(Mapping): # type: ignore + def __init__( + self, + environ: "Mapping[str, str]", + prefix: str = "HTTP_", + ) -> None: + self.environ = environ + self.prefix = prefix + + def __getitem__(self, key: str) -> "Optional[Any]": + return self.environ[self.prefix + key.replace("-", "_").upper()] + + def __len__(self) -> int: + return sum(1 for _ in iter(self)) + + def __iter__(self) -> "Generator[str, None, None]": + for k in self.environ: + if not isinstance(k, str): + continue + + k = k.replace("-", "_").upper() + if not k.startswith(self.prefix): + continue + + yield k[len(self.prefix) :] + + +def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: + """ + Returns True if either traces_sample_rate or traces_sampler is + defined and enable_tracing is set and not false. + """ + if options is None: + return False + + return bool( + options.get("enable_tracing") is not False + and ( + options.get("traces_sample_rate") is not None + or options.get("traces_sampler") is not None + ) + ) + + +def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return False + + return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" + + +def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return True + + return not options.get( + "stream_gen_ai_spans", True + ) and not has_span_streaming_enabled(options) + + +@contextlib.contextmanager +def record_sql_queries( + cursor: "Any", + query: "Any", + params_list: "Any", + paramstyle: "Optional[str]", + executemany: bool, + record_cursor_repr: bool = False, + span_origin: str = "manual", + span_op_override_value: "Optional[str]" = None, +) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": + # TODO: Bring back capturing of params by default + client = sentry_sdk.get_client() + if client.options["_experiments"].get("record_sql_params", False): + if not params_list or params_list == [None]: + params_list = None + + if paramstyle == "pyformat": + paramstyle = "format" + else: + params_list = None + paramstyle = None + + query = _format_sql(cursor, query) + + data = {} + if params_list is not None: + data["db.params"] = params_list + if paramstyle is not None: + data["db.paramstyle"] = paramstyle + if executemany: + data["db.executemany"] = True + if record_cursor_repr and cursor is not None: + data["db.cursor"] = cursor + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message=query, category="query", data=data) + + if has_span_streaming_enabled(client.options): + additional_attributes = {} + if query is not None: + additional_attributes["db.query.text"] = query + + with sentry_sdk.traces.start_span( + name="" if query is None else query, + attributes={ + "sentry.origin": span_origin, + "sentry.op": span_op_override_value + if span_op_override_value + else OP.DB, + **additional_attributes, + }, + ) as span: + yield span + else: + with sentry_sdk.start_span( + op=span_op_override_value if span_op_override_value is not None else OP.DB, + name=query, + origin=span_origin, + ) as span: + for k, v in data.items(): + span.set_data(k, v) + yield span + + +def maybe_create_breadcrumbs_from_span( + scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" +) -> None: + if span.op == OP.DB_REDIS: + scope.add_breadcrumb( + message=span.description, type="redis", category="redis", data=span._tags + ) + + elif span.op == OP.HTTP_CLIENT: + level = None + status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) + if status_code: + if 500 <= status_code <= 599: + level = "error" + elif 400 <= status_code <= 499: + level = "warning" + + if level: + scope.add_breadcrumb( + type="http", category="httplib", data=span._data, level=level + ) + else: + scope.add_breadcrumb(type="http", category="httplib", data=span._data) + + elif span.op == "subprocess": + scope.add_breadcrumb( + type="subprocess", + category="subprocess", + message=span.description, + data=span._data, + ) + + +def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": + try: + return frame.f_code.co_filename + except Exception: + return None + + +def _should_be_included( + is_sentry_sdk_frame: bool, + namespace: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", + abs_path: "Optional[str]", + project_root: "Optional[str]", +) -> bool: + # in_app_include takes precedence over in_app_exclude + should_be_included = _module_in_list(namespace, in_app_include) + should_be_excluded = _is_external_source(abs_path) or _module_in_list( + namespace, in_app_exclude + ) + return not is_sentry_sdk_frame and ( + should_be_included + or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) + ) + + +def add_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", + project_root: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", +) -> None: + """ + Adds OTel compatible source code information to the span + """ + # Find the correct frame + frame: "Union[FrameType, None]" = sys._getframe() + while frame is not None: + abs_path = _get_frame_module_abs_path(frame) + + try: + namespace: "Optional[str]" = frame.f_globals.get("__name__") + except Exception: + namespace = None + + is_sentry_sdk_frame = namespace is not None and namespace.startswith( + "sentry_sdk." + ) + + should_be_included = _should_be_included( + is_sentry_sdk_frame=is_sentry_sdk_frame, + namespace=namespace, + in_app_include=in_app_include, + in_app_exclude=in_app_exclude, + abs_path=abs_path, + project_root=project_root, + ) + if should_be_included: + break + + frame = frame.f_back + else: + frame = None + + # Set the data + if frame is not None: + try: + lineno = frame.f_lineno + except Exception: + lineno = None + if lineno is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_LINENO, lineno) + else: + span.set_attribute("code.line.number", lineno) + + try: + namespace = frame.f_globals.get("__name__") + except Exception: + namespace = None + if namespace is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_NAMESPACE, namespace) + else: + span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) + + filepath = _get_frame_module_abs_path(frame) + if filepath is not None: + if namespace is not None: + in_app_path = filename_for_module(namespace, filepath) + elif project_root is not None and filepath.startswith(project_root): + in_app_path = filepath.replace(project_root, "").lstrip(os.sep) + else: + in_app_path = filepath + + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) + else: + if in_app_path is not None: + span.set_attribute("code.file.path", in_app_path) + + try: + code_function = frame.f_code.co_name + except Exception: + code_function = None + + if code_function is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + else: + span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + + +def add_query_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a database query span + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_query_source = client.options.get("enable_db_query_source", True) + if not should_add_query_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("db_query_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def add_http_request_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a span for an outgoing HTTP request + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_request_source = client.options.get("enable_http_request_source", True) + if not should_add_request_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("http_request_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def extract_sentrytrace_data( + header: "Optional[str]", +) -> "Optional[Dict[str, Union[str, bool, None]]]": + """ + Given a `sentry-trace` header string, return a dictionary of data. + """ + if not header: + return None + + if "," in header: + # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. + parts = [part.strip() for part in header.split(",")] + header = next((part for part in parts if part), "") + + if not header: + return None + + if header.startswith("00-") and header.endswith("-00"): + header = header[3:-3] + + match = SENTRY_TRACE_REGEX.match(header) + if not match: + return None + + trace_id, parent_span_id, sampled_str = match.groups() + parent_sampled = None + + if trace_id: + trace_id = "{:032x}".format(int(trace_id, 16)) + if parent_span_id: + parent_span_id = "{:016x}".format(int(parent_span_id, 16)) + if sampled_str: + parent_sampled = sampled_str != "0" + + return { + "trace_id": trace_id, + "parent_span_id": parent_span_id, + "parent_sampled": parent_sampled, + } + + +def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": + real_sql = None + + # If we're using psycopg2, it could be that we're + # looking at a query that uses Composed objects. Use psycopg2's mogrify + # function to format the query. We lose per-parameter trimming but gain + # accuracy in formatting. + try: + if hasattr(cursor, "mogrify"): + real_sql = cursor.mogrify(sql) + if isinstance(real_sql, bytes): + real_sql = real_sql.decode(cursor.connection.encoding) + except Exception: + real_sql = None + + return real_sql or to_string(sql) + + +class PropagationContext: + """ + The PropagationContext represents the data of a trace in Sentry. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "parent_sampled", + "baggage", + "custom_sampling_context", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + dynamic_sampling_context: "Optional[Dict[str, str]]" = None, + baggage: "Optional[Baggage]" = None, + ) -> None: + self._trace_id = trace_id + """The trace id of the Sentry trace.""" + + self._span_id = span_id + """The span id of the currently executing span.""" + + self.parent_span_id = parent_span_id + """The id of the parent span that started this span. + The parent span could also be a span in an upstream service.""" + + self.parent_sampled = parent_sampled + """Boolean indicator if the parent span was sampled. + Important when the parent span originated in an upstream service, + because we want to sample the whole trace, or nothing from the trace.""" + + self.baggage = baggage + """Parsed baggage header that is used for dynamic sampling decisions.""" + + """DEPRECATED this only exists for backwards compat of constructor.""" + if baggage is None and dynamic_sampling_context is not None: + self.baggage = Baggage(dynamic_sampling_context) + + self.custom_sampling_context: "Optional[dict[str, Any]]" = None + + @classmethod + def from_incoming_data( + cls, incoming_data: "Dict[str, Any]" + ) -> "PropagationContext": + propagation_context = PropagationContext() + normalized_data = normalize_incoming_data(incoming_data) + + sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) + sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) + + # nothing to propagate if no sentry-trace + if sentrytrace_data is None: + return propagation_context + + baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) + baggage = ( + Baggage.from_incoming_header(baggage_header) if baggage_header else None + ) + + if not _should_continue_trace(baggage): + return propagation_context + + propagation_context.update(sentrytrace_data) + if baggage: + propagation_context.baggage = baggage + + propagation_context._fill_sample_rand() + + return propagation_context + + @property + def trace_id(self) -> str: + """The trace id of the Sentry trace.""" + if not self._trace_id: + # New trace, don't fill in sample_rand + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + """The span id of the currently executed span.""" + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + @property + def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": + return self.get_baggage().dynamic_sampling_context() + + def to_traceparent(self) -> str: + return f"{self.trace_id}-{self.span_id}" + + def get_baggage(self) -> "Baggage": + if self.baggage is None: + self.baggage = Baggage.populate_from_propagation_context(self) + return self.baggage + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. + """ + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def update(self, other_dict: "Dict[str, Any]") -> None: + """ + Updates the PropagationContext with data from the given dictionary. + """ + for key, value in other_dict.items(): + try: + setattr(self, key, value) + except AttributeError: + pass + + def _set_custom_sampling_context( + self, custom_sampling_context: "dict[str, Any]" + ) -> None: + self.custom_sampling_context = custom_sampling_context + + def __repr__(self) -> str: + return "".format( + self._trace_id, + self._span_id, + self.parent_span_id, + self.parent_sampled, + self.baggage, + ) + + def _fill_sample_rand(self) -> None: + """ + Ensure that there is a valid sample_rand value in the baggage. + + If there is a valid sample_rand value in the baggage, we keep it. + Otherwise, we generate a sample_rand value according to the following: + + - If we have a parent_sampled value and a sample_rate in the DSC, we compute + a sample_rand value randomly in the range: + - [0, sample_rate) if parent_sampled is True, + - or, in the range [sample_rate, 1) if parent_sampled is False. + + - If either parent_sampled or sample_rate is missing, we generate a random + value in the range [0, 1). + + The sample_rand is deterministically generated from the trace_id, if present. + + This function does nothing if there is no baggage. + """ + if self.baggage is None: + return + + sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) + if sample_rand is not None and 0 <= sample_rand < 1: + # sample_rand is present and valid, so don't overwrite it + return + + # Get the sample rate and compute the transformation that will map the random value + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). + sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) + + try: + sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) + except ValueError: + # ValueError is raised if the interval is invalid, i.e. lower >= upper. + # lower >= upper might happen if the incoming trace's sampled flag + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. + # We cannot generate a sensible sample_rand value in this case. + logger.debug( + f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " + f"and sample_rate={sample_rate}." + ) + return + + self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 + + def _sample_rand(self) -> "Optional[str]": + """Convenience method to get the sample_rand value from the baggage.""" + if self.baggage is None: + return None + + return self.baggage.sentry_items.get("sample_rand") + + +class Baggage: + """ + The W3C Baggage header information (see https://www.w3.org/TR/baggage/). + + Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. + Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but + it is the caller's responsibility to enforce this restriction. + """ + + __slots__ = ("sentry_items", "third_party_items", "mutable") + + SENTRY_PREFIX = "sentry-" + SENTRY_PREFIX_REGEX = re.compile("^sentry-") + + def __init__( + self, + sentry_items: "Dict[str, str]", + third_party_items: str = "", + mutable: bool = True, + ): + self.sentry_items = sentry_items + self.third_party_items = third_party_items + self.mutable = mutable + + @classmethod + def from_incoming_header( + cls, + header: "Optional[str]", + *, + _sample_rand: "Optional[str]" = None, + ) -> "Baggage": + """ + freeze if incoming header already has sentry baggage + """ + sentry_items = {} + third_party_items = "" + mutable = True + + if header: + for item in header.split(","): + if "=" not in item: + continue + + with capture_internal_exceptions(): + item = item.strip() + key, val = item.split("=", 1) + if Baggage.SENTRY_PREFIX_REGEX.match(key): + baggage_key = unquote(key.split("-")[1]) + sentry_items[baggage_key] = unquote(val) + mutable = False + else: + third_party_items += ("," if third_party_items else "") + item + + if _sample_rand is not None: + sentry_items["sample_rand"] = str(_sample_rand) + mutable = False + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": + """ + Deprecated: use populate_from_propagation_context + """ + if scope._propagation_context is None: + return Baggage({}) + + return Baggage.populate_from_propagation_context(scope._propagation_context) + + @classmethod + def populate_from_propagation_context( + cls, propagation_context: "PropagationContext" + ) -> "Baggage": + sentry_items: "Dict[str, str]" = {} + third_party_items = "" + mutable = False + + client = sentry_sdk.get_client() + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options + + sentry_items["trace_id"] = propagation_context.trace_id + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if options.get("traces_sample_rate"): + sentry_items["sample_rate"] = str(options["traces_sample_rate"]) + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def populate_from_transaction( + cls, transaction: "sentry_sdk.tracing.Transaction" + ) -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = transaction.trace_id + sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + transaction.name + and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES + ): + sentry_items["transaction"] = transaction.name + + if transaction.sample_rate is not None: + sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction.sampled is not None: + sentry_items["sampled"] = "true" if transaction.sampled else "false" + + # there's an existing baggage but it was mutable, + # which is why we are creating this new baggage. + # However, if by chance the user put some sentry items in there, give them precedence. + if transaction._baggage and transaction._baggage.sentry_items: + sentry_items.update(transaction._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + @classmethod + def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = segment.trace_id + sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + segment.get_attributes().get("sentry.span.source") + not in LOW_QUALITY_SEGMENT_SOURCES + ) and segment._name: + sentry_items["transaction"] = segment._name + + if segment._sample_rate is not None: + sentry_items["sample_rate"] = str(segment._sample_rate) + + if segment.sampled is not None: + sentry_items["sampled"] = "true" if segment.sampled else "false" + + # There's an existing baggage but it was mutable, which is why we are + # creating this new baggage. + # However, if by chance the user put some sentry items in there, give + # them precedence. + if segment._baggage and segment._baggage.sentry_items: + sentry_items.update(segment._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + def freeze(self) -> None: + self.mutable = False + + def dynamic_sampling_context(self) -> "Dict[str, str]": + header = {} + + for key, item in self.sentry_items.items(): + header[key] = item + + return header + + def serialize(self, include_third_party: bool = False) -> str: + items = [] + + for key, val in self.sentry_items.items(): + with capture_internal_exceptions(): + item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) + items.append(item) + + if include_third_party: + items.append(self.third_party_items) + + return ",".join(items) + + @staticmethod + def strip_sentry_baggage(header: str) -> str: + """Remove Sentry baggage from the given header. + + Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. + """ + return ",".join( + ( + item + for item in header.split(",") + if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) + ) + ) + + def _sample_rand(self) -> "Optional[float]": + """Convenience method to get the sample_rand value from the sentry_items. + + We validate the value and parse it as a float before returning it. The value is considered + valid if it is a float in the range [0, 1). + """ + sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) + + if sample_rand is not None and 0.0 <= sample_rand < 1.0: + return sample_rand + + return None + + def __repr__(self) -> str: + return f'' + + +def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: + """ + Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. + """ + trace_propagation_targets = client.options["trace_propagation_targets"] + + if is_sentry_url(client, url): + return False + + return match_regex_list(url, trace_propagation_targets, substring_matching=True) + + +def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": + """ + Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. + """ + data = {} + for key, value in incoming_data.items(): + if key.startswith("HTTP_"): + key = key[5:] + + key = key.replace("_", "-").lower() + data[key] = value + + return data + + +def create_span_decorator( + op: "Optional[Union[str, OP]]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Any": + """ + Create a span decorator that can wrap both sync and async functions. + + :param op: The operation type for the span. + :type op: str or :py:class:`sentry_sdk.consts.OP` or None + :param name: The name of the span. + :type name: str or None + :param attributes: Additional attributes to set on the span. + :type attributes: dict or None + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + """ + from sentry_sdk.scope import should_send_default_pii + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return await f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return await f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = await f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def create_streaming_span_decorator( + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Any": + """ + Create a span creating decorator that can wrap both sync and async functions. + """ + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + result = await f(*args, **kwargs) + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + return f(*args, **kwargs) + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.span + return current_span + + +def _generate_sample_rand( + trace_id: "Optional[str]", + *, + interval: "tuple[float, float]" = (0.0, 1.0), +) -> float: + """Generate a sample_rand value from a trace ID. + + The generated value will be pseudorandomly chosen from the provided + interval. Specifically, given (lower, upper) = interval, the generated + value will be in the range [lower, upper). The value has 6-digit precision, + so when printing with .6f, the value will never be rounded up. + + The pseudorandom number generator is seeded with the trace ID. + """ + lower, upper = interval + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly + raise ValueError("Invalid interval: lower must be less than upper") + + rng = Random(trace_id) + lower_scaled = int(lower * 1_000_000) + upper_scaled = int(upper * 1_000_000) + try: + sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) + except ValueError: + # In some corner cases it might happen that the range is too small + # In that case, just take the lower bound + sample_rand_scaled = lower_scaled + + return sample_rand_scaled / 1_000_000 + + +def _sample_rand_range( + parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" +) -> "tuple[float, float]": + """ + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values + that a generated sample_rand value must fall into, given the parent_sampled and + sample_rate values. + """ + if parent_sampled is None or sample_rate is None: + return 0.0, 1.0 + elif parent_sampled is True: + return 0.0, sample_rate + else: # parent_sampled is False + return sample_rate, 1.0 + + +def _get_value(source: "Any", key: str) -> "Optional[Any]": + """ + Gets a value from a source object. The source can be a dict or an object. + It is checked for dictionary keys and object attributes. + """ + value = None + if isinstance(source, dict): + value = source.get(key) + else: + if hasattr(source, key): + try: + value = getattr(source, key) + except Exception: + value = None + return value + + +def _get_span_name( + template: "Union[str, SPANTEMPLATE]", + name: str, + kwargs: "Optional[dict[str, Any]]" = None, +) -> str: + """ + Get the name of the span based on the template and the name. + """ + span_name = name + + if template == SPANTEMPLATE.AI_CHAT: + model = None + if kwargs: + for key in ("model", "model_name"): + if kwargs.get(key) and isinstance(kwargs[key], str): + model = kwargs[key] + break + + span_name = f"chat {model}" if model else "chat" + + elif template == SPANTEMPLATE.AI_AGENT: + span_name = f"invoke_agent {name}" + + elif template == SPANTEMPLATE.AI_TOOL: + span_name = f"execute_tool {name}" + + return span_name + + +def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: + """ + Get the operation of the span based on the template. + """ + mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { + SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, + SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, + SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, + } + op = mapping.get(template, OP.FUNCTION) + + return str(op) + + +def _get_input_attributes( + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Get input attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + mapping = { + "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), + "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), + "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), + "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), + "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), + "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), + "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), + "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), + } + + def _set_from_key(key: str, value: "Any") -> None: + if key in mapping: + (attribute, data_type) = mapping[key] + if value is not None and isinstance(value, data_type): + attributes[attribute] = value + + for key, value in list(kwargs.items()): + if key == "prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "user", "content": value} + ) + continue + + if key == "system_prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "system", "content": value} + ) + continue + + _set_from_key(key, value) + + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( + {"args": args, "kwargs": kwargs} + ) + + # Coerce to string + if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] + ) + + return attributes + + +def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": + """ + Get usage attributes. + """ + attributes = {} + + def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: + for key in keys: + value = _get_value(usage, key) + if value is not None and isinstance(value, int): + attributes[attribute] = value + + _set_from_keys( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + ("prompt_tokens", "input_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + ("completion_tokens", "output_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + ("total_tokens",), + ) + + return attributes + + +def _get_output_attributes( + template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> "dict[str, Any]": + """ + Get output attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + with capture_internal_exceptions(): + # Usage from result, result.usage, and result.metadata.usage + usage_candidates = [result] + + usage = _get_value(result, "usage") + usage_candidates.append(usage) + + meta = _get_value(result, "metadata") + usage = _get_value(meta, "usage") + usage_candidates.append(usage) + + for usage_candidate in usage_candidates: + if usage_candidate is not None: + attributes.update(_get_usage_attributes(usage_candidate)) + + # Response model + model_name = _get_value(result, "model") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + model_name = _get_value(result, "model_name") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + # Tool output + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) + + return attributes + + +def _set_input_attributes( + span: "Span", + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + name: str, + f: "Any", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> None: + """ + Set span input attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param f: The wrapped function. + :param args: The arguments to the wrapped function. + :param kwargs: The keyword arguments to the wrapped function. + """ + attributes: "dict[str, Any]" = {} + + if template == SPANTEMPLATE.AI_AGENT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_AGENT_NAME: name, + } + elif template == SPANTEMPLATE.AI_CHAT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + } + elif template == SPANTEMPLATE.AI_TOOL: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: name, + } + + docstring = f.__doc__ + if docstring is not None: + attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring + + attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) + span.update_data(attributes or {}) + + +def _set_output_attributes( + span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> None: + """ + Set span output attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param result: The result of the wrapped function. + """ + span.update_data(_get_output_attributes(template, send_pii, result) or {}) + + +def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: + """ + Check if we should continue the incoming trace according to the strict_trace_continuation spec. + https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation + """ + + client = sentry_sdk.get_client() + parsed_dsn = client.parsed_dsn + client_org_id = parsed_dsn.org_id if parsed_dsn else None + baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None + + if ( + client_org_id is not None + and baggage_org_id is not None + and client_org_id != baggage_org_id + ): + logger.debug( + f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + strict_trace_continuation: bool = client.options.get( + "strict_trace_continuation", False + ) + if strict_trace_continuation: + if (baggage_org_id is not None and client_org_id is None) or ( + baggage_org_id is None and client_org_id is not None + ): + logger.debug( + f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + return True + + +def add_sentry_baggage_to_headers( + headers: "MutableMapping[str, str]", sentry_baggage: str +) -> None: + """Add the Sentry baggage to the headers. + + This function directly mutates the provided headers. The provided sentry_baggage + is appended to the existing baggage. If the baggage already contains Sentry items, + they are stripped out first. + """ + existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") + stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) + + separator = "," if len(stripped_existing_baggage) > 0 else "" + + headers[BAGGAGE_HEADER_NAME] = ( + stripped_existing_baggage + separator + sentry_baggage + ) + + +def _make_sampling_decision( + name: str, + attributes: "Optional[Attributes]", + scope: "sentry_sdk.Scope", +) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": + """ + Decide whether a span should be sampled. + + Returns a tuple with: + 1. the sampling decision + 2. the effective sample rate + 3. the sample rand + 4. the reason for not sampling the span, if unsampled + """ + client = sentry_sdk.get_client() + + if not has_tracing_enabled(client.options): + return False, None, None, None + + propagation_context = scope.get_active_propagation_context() + + sample_rand = None + if propagation_context.baggage is not None: + sample_rand = propagation_context.baggage._sample_rand() + if sample_rand is None: + sample_rand = _generate_sample_rand(propagation_context.trace_id) + + # If there's a traces_sampler, use that; otherwise use traces_sample_rate + traces_sampler_defined = callable(client.options.get("traces_sampler")) + if traces_sampler_defined: + sampling_context = { + "span_context": { + "name": name, + "trace_id": propagation_context.trace_id, + "parent_span_id": propagation_context.parent_span_id, + "parent_sampled": propagation_context.parent_sampled, + "attributes": dict(attributes) if attributes else {}, + }, + } + + if propagation_context.custom_sampling_context: + sampling_context.update(propagation_context.custom_sampling_context) + + sample_rate = client.options["traces_sampler"](sampling_context) + else: + if propagation_context.parent_sampled is not None: + sample_rate = propagation_context.parent_sampled + else: + sample_rate = client.options["traces_sample_rate"] + + # Validate whether the sample_rate we got is actually valid. Since + # traces_sampler is user-provided, it could return anything. + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") + return False, None, None, "sample_rate" + + sample_rate = float(sample_rate) + if not sample_rate: + if traces_sampler_defined: + reason = "traces_sampler returned 0 or False" + else: + reason = "traces_sample_rate is set to 0" + + logger.debug(f"[Tracing] Discarding {name} because {reason}") + return False, 0.0, None, "sample_rate" + + # Adjust sample rate if we're under backpressure + sample_rate_before_backpressure = sample_rate + if client.monitor: + sample_rate /= 2**client.monitor.downsample_factor + + if not sample_rate: + logger.debug(f"[Tracing] Discarding {name} because backpressure") + return False, 0.0, None, "backpressure" + + # Make the actual decision + sampled = sample_rand < sample_rate + + if sampled: + logger.debug(f"[Tracing] Starting {name}") + outcome = None + + else: + # Determine why exactly the span will not be sampled. If we've lowered + # the effective sample_rate because of backpressure, check whether the + # span would've been sampled if backpressure wasn't active. If that's the + # case, backpressure is the actual reason, otherwise just pure sampling + # rate. + if ( + sample_rate_before_backpressure != sample_rate + and sample_rand < sample_rate_before_backpressure + ): + logger.debug(f"[Tracing] Discarding {name} because backpressure") + outcome = "backpressure" + + else: + logger.debug( + f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" + ) + outcome = "sample_rate" + + return sampled, sample_rate, sample_rand, outcome + + +def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: + """Determine if a span fits one of the rules in ignore_spans.""" + client = sentry_sdk.get_client() + ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") + + if not ignore_spans: + return False + + def _matches(rule: "Any", value: "Any") -> bool: + if isinstance(rule, Pattern): + if isinstance(value, str): + return bool(rule.fullmatch(value)) + else: + return False + + return rule == value + + for rule in ignore_spans: + if isinstance(rule, (str, Pattern)): + if _matches(rule, name): + return True + + elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): + name_matches = True + attributes_match = True + + if "name" in rule: + name_matches = _matches(rule["name"], name) + + if "attributes" in rule: + attributes = attributes or {} + + for attribute, value in rule["attributes"].items(): + if attribute not in attributes or not _matches( + value, attributes[attribute] + ): + attributes_match = False + break + + if name_matches and attributes_match: + return True + + return False + + +# Circular imports +from sentry_sdk.traces import ( + LOW_QUALITY_SEGMENT_SOURCES, + StreamedSpan, +) +from sentry_sdk.traces import ( + start_span as start_streaming_span, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + LOW_QUALITY_TRANSACTION_SOURCES, + SENTRY_TRACE_HEADER_NAME, + Span, +) + +if TYPE_CHECKING: + from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py new file mode 100644 index 0000000000..2b71fee429 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py @@ -0,0 +1,1255 @@ +import asyncio +import gzip +import io +import json +import logging +import os +import socket +import ssl +import time +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from urllib.request import getproxies + +try: + import brotli # type: ignore +except ImportError: + brotli = None + +try: + import httpcore +except ImportError: + httpcore = None # type: ignore[assignment,unused-ignore] + +try: + import h2 # noqa: F401 + + HTTP2_ENABLED = httpcore is not None +except ImportError: + HTTP2_ENABLED = False + +try: + import anyio # noqa: F401 + + ASYNC_TRANSPORT_AVAILABLE = httpcore is not None +except ImportError: + ASYNC_TRANSPORT_AVAILABLE = False + +from typing import TYPE_CHECKING, Dict, List, cast + +import certifi +import urllib3 + +import sentry_sdk +from sentry_sdk.consts import EndpointType +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + logger, + mark_sentry_task_internal, +) +from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + DefaultDict, + Iterable, + Mapping, + Optional, + Self, + Tuple, + Type, + Union, + ) + + from urllib3.poolmanager import PoolManager, ProxyManager + + from sentry_sdk._types import Event, EventDataCategory + +KEEP_ALIVE_SOCKET_OPTIONS = [] +for option in [ + (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 +]: + try: + KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) + except AttributeError: + # a specific option might not be available on specific systems, + # e.g. TCP_KEEPIDLE doesn't exist on macOS + pass + + +def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": + """Case-insensitive header lookup for httpcore-style responses.""" + header_lower = header.lower() + return next( + ( + val.decode("ascii") + for key, val in response.headers + if key.decode("ascii").lower() == header_lower + ), + None, + ) + + +class Transport(ABC): + """Baseclass for all transports. + + A transport is used to send an event to sentry. + """ + + parsed_dsn: "Optional[Dsn]" = None + + def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: + self.options = options + if options and options["dsn"]: + self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) + else: + self.parsed_dsn = None + + def capture_event(self: "Self", event: "Event") -> None: + """ + DEPRECATED: Please use capture_envelope instead. + + This gets invoked with the event dictionary when an event should + be sent to sentry. + """ + + warnings.warn( + "capture_event is deprecated, please use capture_envelope instead!", + DeprecationWarning, + stacklevel=2, + ) + + envelope = Envelope() + envelope.add_event(event) + self.capture_envelope(envelope) + + @abstractmethod + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + """ + Send an envelope to Sentry. + + Envelopes are a data container format that can hold any type of data + submitted to Sentry. We use it to send all event data (including errors, + transactions, crons check-ins, etc.) to Sentry. + """ + pass + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Any]" = None, + ) -> None: + """ + Wait `timeout` seconds for the current events to be sent out. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def kill(self: "Self") -> None: + """ + Forcefully kills the transport. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + """This increments a counter for event loss by reason and + data category by the given positive-int quantity (default 1). + + If an item is provided, the data category and quantity are + extracted from the item, and the values passed for + data_category and quantity are ignored. + + When recording a lost transaction via data_category="transaction", + the calling code should also record the lost spans via this method. + When recording lost spans, `quantity` should be set to the number + of contained spans, plus one for the transaction itself. When + passing an Item containing a transaction via the `item` parameter, + this method automatically records the lost spans. + """ + return None + + def is_healthy(self: "Self") -> bool: + return True + + +def _parse_rate_limits( + header: str, now: "Optional[datetime]" = None +) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": + if now is None: + now = datetime.now(timezone.utc) + + for limit in header.split(","): + try: + parameters = limit.strip().split(":") + retry_after_val, categories = parameters[:2] + + retry_after = now + timedelta(seconds=int(retry_after_val)) + for category in categories and categories.split(";") or (None,): + yield category, retry_after # type: ignore + except (LookupError, ValueError): + continue + + +class HttpTransportCore(Transport): + """Shared base class for sync and async transports.""" + + TIMEOUT = 30 # seconds + + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + from sentry_sdk.consts import VERSION + + Transport.__init__(self, options) + assert self.parsed_dsn is not None + self.options: "Dict[str, Any]" = options + self._worker = self._create_worker(options) + self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) + self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} + # We only use this Retry() class for the `get_retry_after` method it exposes + self._retry = urllib3.util.Retry() + self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( + defaultdict(int) + ) + self._last_client_report_sent = time.time() + + self._pool = self._make_pool() + + # Backwards compatibility for deprecated `self.hub_class` attribute + self._hub_cls = sentry_sdk.Hub + + experiments = options.get("_experiments", {}) + compression_level = experiments.get( + "transport_compression_level", + experiments.get("transport_zlib_compression_level"), + ) + compression_algo = experiments.get( + "transport_compression_algo", + ( + "gzip" + # if only compression level is set, assume gzip for backwards compatibility + # if we don't have brotli available, fallback to gzip + if compression_level is not None or brotli is None + else "br" + ), + ) + + if compression_algo == "br" and brotli is None: + logger.warning( + "You asked for brotli compression without the Brotli module, falling back to gzip -9" + ) + compression_algo = "gzip" + compression_level = None + + if compression_algo not in ("br", "gzip"): + logger.warning( + "Unknown compression algo %s, disabling compression", compression_algo + ) + self._compression_level = 0 + self._compression_algo = None + else: + self._compression_algo = compression_algo + + if compression_level is not None: + self._compression_level = compression_level + elif self._compression_algo == "gzip": + self._compression_level = 9 + elif self._compression_algo == "br": + self._compression_level = 4 + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + raise NotImplementedError() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + if not self.options["send_client_reports"]: + return + + if item is not None: + data_category = item.data_category + quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). + + if data_category == "transaction": + # Also record the lost spans + event = item.get_transaction_event() or {} + + # +1 for the transaction itself + span_count = ( + len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 + ) + self.record_lost_event(reason, "span", quantity=span_count) + + elif data_category == "log_item" and item: + # Also record size of lost logs in bytes + bytes_size = len(item.get_bytes()) + self.record_lost_event(reason, "log_byte", quantity=bytes_size) + + elif data_category == "attachment": + # quantity of 0 is actually 1 as we do not want to count + # empty attachments as actually empty. + quantity = len(item.get_bytes()) or 1 + + elif data_category is None: + raise TypeError("data category not provided") + + self._discarded_events[data_category, reason] += quantity + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return response.headers.get(header) + + def _update_rate_limits( + self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" + ) -> None: + # new sentries with more rate limit insights. We honor this header + # no matter of the status code to update our internal rate limits. + header = self._get_header_value(response, "x-sentry-rate-limits") + if header: + logger.warning("Rate-limited via x-sentry-rate-limits") + self._disabled_until.update(_parse_rate_limits(header)) + + # old sentries only communicate global rate limit hits via the + # retry-after header on 429. This header can also be emitted on new + # sentries if a proxy in front wants to globally slow things down. + elif response.status == 429: + logger.warning("Rate-limited via 429") + retry_after_value = self._get_header_value(response, "Retry-After") + retry_after = ( + self._retry.parse_retry_after(retry_after_value) + if retry_after_value is not None + else None + ) or 60 + self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( + seconds=retry_after + ) + + def _handle_request_error( + self: "Self", + envelope: "Optional[Envelope]", + loss_reason: str = "network", + record_reason: str = "network_error", + ) -> None: + def record_loss(reason: str) -> None: + if envelope is None: + self.record_lost_event(reason, data_category="error") + else: + for item in envelope.items: + self.record_lost_event(reason, item=item) + + self.on_dropped_event(loss_reason) + record_loss(record_reason) + + def _handle_response( + self: "Self", + response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", + envelope: "Optional[Envelope]", + ) -> None: + self._update_rate_limits(response) + + if response.status == 413: + size_exceeded_message = ( + "HTTP 413: Event dropped due to exceeded envelope size limit" + ) + response_message = getattr( + response, "data", getattr(response, "content", None) + ) + if response_message is not None: + size_exceeded_message += f" (body: {response_message})" + + logger.error(size_exceeded_message) + self._handle_request_error( + envelope=envelope, loss_reason="status_413", record_reason="send_error" + ) + + elif response.status == 429: + # if we hit a 429. Something was rate limited but we already + # acted on this in `self._update_rate_limits`. Note that we + # do not want to record event loss here as we will have recorded + # an outcome in relay already. + self.on_dropped_event("status_429") + pass + + elif response.status >= 300 or response.status < 200: + logger.error( + "Unexpected status code: %s (body: %s)", + response.status, + getattr(response, "data", getattr(response, "content", None)), + ) + self._handle_request_error( + envelope=envelope, loss_reason="status_{}".format(response.status) + ) + + def _update_headers( + self: "Self", + headers: "Dict[str, str]", + ) -> None: + headers.update( + { + "User-Agent": str(self._auth.client), + "X-Sentry-Auth": str(self._auth.to_header()), + } + ) + + def on_dropped_event(self: "Self", _reason: str) -> None: + return None + + def _fetch_pending_client_report( + self: "Self", force: bool = False, interval: int = 60 + ) -> "Optional[Item]": + if not self.options["send_client_reports"]: + return None + + if not (force or self._last_client_report_sent < time.time() - interval): + return None + + discarded_events = self._discarded_events + self._discarded_events = defaultdict(int) + self._last_client_report_sent = time.time() + + if not discarded_events: + return None + + return Item( + PayloadRef( + json={ + "timestamp": time.time(), + "discarded_events": [ + {"reason": reason, "category": category, "quantity": quantity} + for ( + (category, reason), + quantity, + ) in discarded_events.items() + ], + } + ), + type="client_report", + ) + + def _check_disabled(self, category: str) -> bool: + def _disabled(bucket: "Any") -> bool: + ts = self._disabled_until.get(bucket) + return ts is not None and ts > datetime.now(timezone.utc) + + return _disabled(category) or _disabled(None) + + def _is_rate_limited(self: "Self") -> bool: + return any( + ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() + ) + + def _is_worker_full(self: "Self") -> bool: + return self._worker.full() + + def is_healthy(self: "Self") -> bool: + return not (self._is_worker_full() or self._is_rate_limited()) + + def _prepare_envelope( + self: "Self", envelope: "Envelope" + ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": + # remove all items from the envelope which are over quota + new_items = [] + for item in envelope.items: + if self._check_disabled(item.data_category): + if item.data_category in ("transaction", "error", "default", "statsd"): + self.on_dropped_event("self_rate_limits") + self.record_lost_event("ratelimit_backoff", item=item) + else: + new_items.append(item) + + # Since we're modifying the envelope here make a copy so that others + # that hold references do not see their envelope modified. + envelope = Envelope(headers=envelope.headers, items=new_items) + + if not envelope.items: + return None + + # since we're already in the business of sending out an envelope here + # check if we have one pending for the stats session envelopes so we + # can attach it to this enveloped scheduled for sending. This will + # currently typically attach the client report to the most recent + # session update. + client_report_item = self._fetch_pending_client_report(interval=30) + if client_report_item is not None: + envelope.items.append(client_report_item) + + content_encoding, body = self._serialize_envelope(envelope) + + assert self.parsed_dsn is not None + logger.debug( + "Sending envelope [%s] project:%s host:%s", + envelope.description, + self.parsed_dsn.project_id, + self.parsed_dsn.host, + ) + + headers: "Dict[str, str]" = { + "Content-Type": "application/x-sentry-envelope", + } + if content_encoding: + headers["Content-Encoding"] = content_encoding + + return envelope, body, headers + + def _serialize_envelope( + self: "Self", envelope: "Envelope" + ) -> "tuple[Optional[str], io.BytesIO]": + content_encoding = None + body = io.BytesIO() + if self._compression_level == 0 or self._compression_algo is None: + envelope.serialize_into(body) + else: + content_encoding = self._compression_algo + if self._compression_algo == "br" and brotli is not None: + body.write( + brotli.compress( + envelope.serialize(), quality=self._compression_level + ) + ) + else: # assume gzip as we sanitize the algo value in init + with gzip.GzipFile( + fileobj=body, mode="w", compresslevel=self._compression_level + ) as f: + envelope.serialize_into(f) + + return content_encoding, body + + def _get_httpcore_pool_options( + self: "Self", http2: bool = False + ) -> "Dict[str, Any]": + """Shared pool options for httpcore-based transports (Http2 and Async).""" + options: "Dict[str, Any]" = { + "http2": http2, + "retries": 3, + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations( + self.options["ca_certs"] + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") + key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") + if cert_file is not None: + ssl_context.load_cert_chain(cert_file, key_file) + + options["ssl_context"] = ssl_context + return options + + def _resolve_proxy(self: "Self") -> "Optional[str]": + """Resolve proxy URL from options and environment. Returns proxy URL or None.""" + if self.parsed_dsn is None: + return None + + no_proxy = self._in_no_proxy(self.parsed_dsn) + proxy = None + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + return proxy or None + + @property + def _timeout_extensions(self: "Self") -> "Dict[str, Any]": + return { + "timeout": { + "pool": self.TIMEOUT, + "connect": self.TIMEOUT, + "write": self.TIMEOUT, + "read": self.TIMEOUT, + } + } + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + raise NotImplementedError() + + def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: + no_proxy = getproxies().get("no") + if not no_proxy: + return False + for host in no_proxy.split(","): + host = host.strip() + if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): + return True + return False + + def _make_pool( + self: "Self", + ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + raise NotImplementedError() + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": + raise NotImplementedError() + + def kill(self: "Self") -> None: + logger.debug("Killing HTTP transport") + self._worker.kill() + + +# Keep BaseHttpTransport as an alias for backwards compatibility +# and for the sync transport implementation +class BaseHttpTransport(HttpTransportCore): + """The base HTTP transport (synchronous).""" + + def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + response.close() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return BackgroundWorker(queue_size=options["transport_queue_size"]) + + def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def capture_envelope( + self, + envelope: "Envelope", + ) -> None: + def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + self._send_envelope(envelope) + self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + self._worker.flush(timeout, callback) + + @staticmethod + def _warn_hub_cls() -> None: + """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" + warnings.warn( + "The `hub_cls` attribute is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=3, + ) + + @property + def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + return self._hub_cls + + @hub_cls.setter + def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + self._hub_cls = value + + +class HttpTransport(BaseHttpTransport): + if TYPE_CHECKING: + _pool: "Union[PoolManager, ProxyManager]" + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + num_pools = self.options.get("_experiments", {}).get("transport_num_pools") + options = { + "num_pools": 2 if num_pools is None else int(num_pools), + "cert_reqs": "CERT_REQUIRED", + "timeout": urllib3.Timeout(total=self.TIMEOUT), + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if self.options["keep_alive"]: + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + options["ca_certs"] = ( + self.options["ca_certs"] # User-provided bundle from the SDK init + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + + options["cert_file"] = self.options["cert_file"] or os.environ.get( + "CLIENT_CERT_FILE" + ) + options["key_file"] = self.options["key_file"] or os.environ.get( + "CLIENT_KEY_FILE" + ) + + return options + + def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = None + no_proxy = self._in_no_proxy(self.parsed_dsn) + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + use_socks_proxy = True + try: + # Check if PySocks dependency is available + from urllib3.contrib.socks import SOCKSProxyManager + except ImportError: + use_socks_proxy = False + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", + proxy, + ) + + if use_socks_proxy: + return SOCKSProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + else: + return urllib3.ProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "urllib3.BaseHTTPResponse": + return self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + body=body, + headers=headers, + ) + + +class AsyncHttpTransport(HttpTransportCore): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + if not ASYNC_TRANSPORT_AVAILABLE: + raise RuntimeError( + "AsyncHttpTransport requires httpcore[asyncio]. " + "Install it with: pip install sentry-sdk[asyncio]" + ) + super().__init__(options) + # Requires event loop at init time + self.loop = asyncio.get_running_loop() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return AsyncWorker(queue_size=options["transport_queue_size"]) + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + async def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + await self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + async def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = await self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + await response.aclose() + + async def _request( # type: ignore[override] + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + return await self._pool.request( # type: ignore[misc,unused-ignore] + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + + async def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def _capture_envelope(self: "Self", envelope: "Envelope") -> None: + async def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + await self._send_envelope(envelope) + await self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + # Synchronous entry point + if self.loop and self.loop.is_running(): + self.loop.call_soon_threadsafe(self._capture_envelope, envelope) + else: + # The event loop is no longer running + logger.warning("Async Transport is not running in an event loop.") + self.on_dropped_event("internal_sdk_error") + for item in envelope.items: + self.record_lost_event("internal_sdk_error", item=item) + + def flush( # type: ignore[override] + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> "Optional[asyncio.Task[None]]": + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] + return None + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=HTTP2_ENABLED + and self.parsed_dsn is not None + and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + socks_opts = opts.copy() + if "socket_options" in socks_opts: + socket_options = socks_opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) + + return httpcore.AsyncConnectionPool(**opts) + + def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] + logger.debug("Killing HTTP transport") + self._worker.kill() + try: + # Return the pool cleanup task so caller can await it if needed + with mark_sentry_task_internal(): + return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] + except RuntimeError: + logger.warning("Event loop not running, aborting kill.") + return None + + +if not HTTP2_ENABLED: + # Sorry, no Http2Transport for you + class Http2Transport(HttpTransport): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + super().__init__(options) + logger.warning( + "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." + ) + +else: + + class Http2Transport(BaseHttpTransport): # type: ignore + """The HTTP2 transport based on httpcore.""" + + TIMEOUT = 15 + + if TYPE_CHECKING: + _pool: """Union[ + httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool + ]""" + + def _get_header_value( + self: "Self", response: "httpcore.Response", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + response = self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + return response + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + if "socket_options" in opts: + socket_options = opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.SOCKSProxy(proxy_url=proxy, **opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.HTTPProxy(proxy_url=proxy, **opts) + + return httpcore.ConnectionPool(**opts) + + +class _EnvelopePrinterTransport(Transport): + """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" + + def __init__(self, transport: "Transport") -> None: + Transport.__init__(self, options=transport.options) + self._inner = transport + self.parsed_dsn = transport.parsed_dsn + + self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") + self.envelope_logger.setLevel(logging.INFO) + self.envelope_logger.propagate = False + if not self.envelope_logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + self.envelope_logger.addHandler(handler) + + @property # type: ignore[misc] + def __class__(self) -> type: + return self._inner.__class__ + + def capture_envelope(self, envelope: "Envelope") -> None: + try: + self.envelope_logger.info("--- Sentry Envelope ---") + self.envelope_logger.info( + "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) + ) + for item in envelope.items: + self.envelope_logger.info(" Item type: %s", item.type) + self.envelope_logger.info( + " Item headers: %s", + json.dumps(item.headers, indent=2, default=str), + ) + try: + payload = json.loads(item.get_bytes()) + self.envelope_logger.info( + " Payload:\n%s", + json.dumps(payload, indent=2, default=str), + ) + except (ValueError, TypeError): + self.envelope_logger.info( + " Payload: ", + len(item.get_bytes()), + ) + self.envelope_logger.info("--- End Envelope ---") + except Exception: + pass + + self._inner.capture_envelope(envelope) + + def flush( + self, + timeout: float, + callback: "Optional[Any]" = None, + ) -> "Any": + return self._inner.flush(timeout, callback) + + def kill(self) -> "Any": + return self._inner.kill() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + self._inner.record_lost_event(reason, data_category, item, quantity=quantity) + + def is_healthy(self) -> bool: + return self._inner.is_healthy() + + def __getattr__(self, name: str) -> "Any": + return getattr(self._inner, name) + + +class _FunctionTransport(Transport): + """ + DEPRECATED: Users wishing to provide a custom transport should subclass + the Transport class, rather than providing a function. + """ + + def __init__( + self, + func: "Callable[[Event], None]", + ) -> None: + Transport.__init__(self) + self._func = func + + def capture_event( + self, + event: "Event", + ) -> None: + self._func(event) + return None + + def capture_envelope(self, envelope: "Envelope") -> None: + # Since function transports expect to be called with an event, we need + # to iterate over the envelope and call the function for each event, via + # the deprecated capture_event method. + event = envelope.get_event() + if event is not None: + self.capture_event(event) + + +def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": + ref_transport = options["transport"] + + use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) + use_async_transport = options.get("_experiments", {}).get("transport_async", False) + async_integration = any( + integration.__class__.__name__ == "AsyncioIntegration" + for integration in options.get("integrations") or [] + ) + + # By default, we use the http transport class + transport_cls: "Type[Transport]" = ( + Http2Transport if use_http2_transport else HttpTransport + ) + + if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: + try: + asyncio.get_running_loop() + if async_integration: + if use_http2_transport: + logger.warning( + "HTTP/2 transport is not supported with async transport. " + "Ignoring transport_http2 experiment." + ) + transport_cls = AsyncHttpTransport + else: + logger.warning( + "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." + ) + except RuntimeError: + # No event loop running, fall back to sync transport + logger.warning("No event loop running, falling back to sync transport.") + elif use_async_transport: + logger.warning( + "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." + ) + + transport: "Optional[Transport]" = None + + if isinstance(ref_transport, Transport): + transport = ref_transport + elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): + transport_cls = ref_transport + elif callable(ref_transport): + warnings.warn( + "Function transports are deprecated and will be removed in a future release." + "Please provide a Transport instance or subclass, instead.", + DeprecationWarning, + stacklevel=2, + ) + transport = _FunctionTransport(ref_transport) + + # if a transport class is given only instantiate it if the dsn is not + # empty or None + if transport is None and options["dsn"]: + transport = transport_cls(options) + + if transport is not None and os.environ.get( + "SENTRY_PRINT_ENVELOPES", "" + ).lower() in ("1", "true", "yes"): + transport = _EnvelopePrinterTransport(transport) + + return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py new file mode 100644 index 0000000000..dff91e3719 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py @@ -0,0 +1,52 @@ +""" +This module contains type definitions for the Sentry SDK's public API. +The types are re-exported from the internal module `sentry_sdk._types`. + +Disclaimer: Since types are a form of documentation, type definitions +may change in minor releases. Removing a type would be considered a +breaking change, and so we will only remove type definitions in major +releases. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Re-export types to make them available in the public API + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + EventDataCategory, + Hint, + Log, + Metric, + MonitorConfig, + SamplingContext, + ) +else: + from typing import Any + + # The lines below allow the types to be imported from outside `if TYPE_CHECKING` + # guards. The types in this module are only intended to be used for type hints. + Breadcrumb = Any + BreadcrumbHint = Any + Event = Any + EventDataCategory = Any + Hint = Any + Log = Any + MonitorConfig = Any + SamplingContext = Any + Metric = Any + + +__all__ = ( + "Breadcrumb", + "BreadcrumbHint", + "Event", + "EventDataCategory", + "Hint", + "Log", + "MonitorConfig", + "SamplingContext", + "Metric", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py new file mode 100644 index 0000000000..0963015351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py @@ -0,0 +1,2178 @@ +import base64 +import copy +import json +import linecache +import logging +import math +import os +import random +import re +import subprocess +import sys +import threading +import time +from collections import namedtuple +from contextlib import contextmanager +from datetime import datetime, timezone +from decimal import Decimal +from functools import partial, partialmethod, wraps +from numbers import Real +from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit + +try: + # Python 3.11 + from builtins import BaseExceptionGroup +except ImportError: + # Python 3.10 and below + BaseExceptionGroup = None # type: ignore + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._compat import PY37 +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue +from sentry_sdk.consts import ( + DEFAULT_ADD_FULL_STACK, + DEFAULT_MAX_STACK_FRAMES, + EndpointType, +) + +if TYPE_CHECKING: + from types import FrameType, TracebackType + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Iterator, + List, + NoReturn, + Optional, + ParamSpec, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from gevent.hub import Hub + + from sentry_sdk._types import ( + AttributeValue, + Event, + ExcInfo, + Hint, + Log, + Metric, + SerializedAttributeValue, + SpanJSON, + ) + + P = ParamSpec("P") + R = TypeVar("R") + + +epoch = datetime(1970, 1, 1) + +# The logger is created here but initialized in the debug support module +logger = logging.getLogger("sentry_sdk.errors") + +_installed_modules = None + +BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") + +FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) +TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) + +MAX_STACK_FRAMES = 2000 +"""Maximum number of stack frames to send to Sentry. + +If we have more than this number of stack frames, we will stop processing +the stacktrace to avoid getting stuck in a long-lasting loop. This value +exceeds the default sys.getrecursionlimit() of 1000, so users will only +be affected by this limit if they have a custom recursion limit. +""" + + +def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": + """Casts an ENV variable value to boolean using the constants defined above. + In strict mode, it may return None if the value doesn't match any of the predefined values. + """ + normalized = str(value).lower() if value is not None else None + + if normalized in FALSY_ENV_VALUES: + return False + + if normalized in TRUTHY_ENV_VALUES: + return True + + return None if strict else bool(value) + + +def json_dumps(data: "Any") -> bytes: + """Serialize data into a compact JSON representation encoded as UTF-8.""" + return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") + + +def get_git_revision() -> "Optional[str]": + try: + with open(os.path.devnull, "w+") as null: + # prevent command prompt windows from popping up on windows + startupinfo = None + if sys.platform == "win32" or sys.platform == "cygwin": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + revision = ( + subprocess.Popen( + ["git", "rev-parse", "HEAD"], + startupinfo=startupinfo, + stdout=subprocess.PIPE, + stderr=null, + stdin=null, + ) + .communicate()[0] + .strip() + .decode("utf-8") + ) + except (OSError, IOError, FileNotFoundError): + return None + + return revision + + +def get_default_release() -> "Optional[str]": + """Try to guess a default release.""" + release = os.environ.get("SENTRY_RELEASE") + if release: + return release + + release = get_git_revision() + if release: + return release + + for var in ( + "HEROKU_BUILD_COMMIT", + "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility + "SOURCE_VERSION", + "CODEBUILD_RESOLVED_SOURCE_VERSION", + "CIRCLE_SHA1", + "GAE_DEPLOYMENT_ID", + "K_REVISION", + ): + release = os.environ.get(var) + if release: + return release + return None + + +def get_sdk_name(installed_integrations: "List[str]") -> str: + """Return the SDK name including the name of the used web framework.""" + + # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier + # here because if django is not installed the integration is not accessible. + framework_integrations = [ + "django", + "flask", + "fastapi", + "bottle", + "falcon", + "quart", + "sanic", + "starlette", + "litestar", + "starlite", + "chalice", + "serverless", + "pyramid", + "tornado", + "aiohttp", + "aws_lambda", + "gcp", + "beam", + "asgi", + "wsgi", + ] + + for integration in framework_integrations: + if integration in installed_integrations: + return "sentry.python.{}".format(integration) + + return "sentry.python" + + +class CaptureInternalException: + __slots__ = () + + def __enter__(self) -> "ContextManager[Any]": + return self + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> bool: + if ty is not None and value is not None: + capture_internal_exception((ty, value, tb)) + + return True + + +_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() + + +def capture_internal_exceptions() -> "ContextManager[Any]": + return _CAPTURE_INTERNAL_EXCEPTION + + +def capture_internal_exception(exc_info: "ExcInfo") -> None: + """ + Capture an exception that is likely caused by a bug in the SDK + itself. + + These exceptions do not end up in Sentry and are just logged instead. + """ + if sentry_sdk.get_client().is_active(): + logger.error("Internal error in sentry_sdk", exc_info=exc_info) + + +def to_timestamp(value: "datetime") -> float: + return (value - epoch).total_seconds() + + +def format_timestamp(value: "datetime") -> str: + """Formats a timestamp in RFC 3339 format. + + Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. + """ + utctime = value.astimezone(timezone.utc) + + # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for + # several years now), and isoformat is slightly different. + return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +ISO_TZ_SEPARATORS = frozenset(("+", "-")) + + +def datetime_from_isoformat(value: str) -> "datetime": + try: + result = datetime.fromisoformat(value) + except (AttributeError, ValueError): + # py 3.6 + timestamp_format = ( + "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" + ) + if value.endswith("Z"): + value = value[:-1] + "+0000" + + if value[-6] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + value = value[:-3] + value[-2:] + elif value[-5] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + + result = datetime.strptime(value, timestamp_format) + return result.astimezone(timezone.utc) + + +def event_hint_with_exc_info( + exc_info: "Optional[ExcInfo]" = None, +) -> "Dict[str, Optional[ExcInfo]]": + """Creates a hint with the exc info filled in.""" + if exc_info is None: + exc_info = sys.exc_info() + else: + exc_info = exc_info_from_error(exc_info) + if exc_info[0] is None: + exc_info = None + return {"exc_info": exc_info} + + +class BadDsn(ValueError): + """Raised on invalid DSNs.""" + + +class Dsn: + """Represents a DSN.""" + + ORG_ID_REGEX = re.compile(r"^o(\d+)\.") + + def __init__( + self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None + ) -> None: + if isinstance(value, Dsn): + self.__dict__ = dict(value.__dict__) + return + parts = urlsplit(str(value)) + + if parts.scheme not in ("http", "https"): + raise BadDsn("Unsupported scheme %r" % parts.scheme) + self.scheme = parts.scheme + + if parts.hostname is None: + raise BadDsn("Missing hostname") + + self.host = parts.hostname + + if org_id is not None: + self.org_id: "Optional[str]" = org_id + else: + org_id_match = Dsn.ORG_ID_REGEX.match(self.host) + self.org_id = org_id_match.group(1) if org_id_match else None + + if parts.port is None: + self.port: int = self.scheme == "https" and 443 or 80 + else: + self.port = parts.port + + if not parts.username: + raise BadDsn("Missing public key") + + self.public_key = parts.username + self.secret_key = parts.password + + path = parts.path.rsplit("/", 1) + + try: + self.project_id = str(int(path.pop())) + except (ValueError, TypeError): + raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) + + self.path = "/".join(path) + "/" + + @property + def netloc(self) -> str: + """The netloc part of a DSN.""" + rv = self.host + if (self.scheme, self.port) not in (("http", 80), ("https", 443)): + rv = "%s:%s" % (rv, self.port) + return rv + + def to_auth(self, client: "Optional[Any]" = None) -> "Auth": + """Returns the auth info object for this dsn.""" + return Auth( + scheme=self.scheme, + host=self.netloc, + path=self.path, + project_id=self.project_id, + public_key=self.public_key, + secret_key=self.secret_key, + client=client, + ) + + def __str__(self) -> str: + return "%s://%s%s@%s%s%s" % ( + self.scheme, + self.public_key, + self.secret_key and "@" + self.secret_key or "", + self.netloc, + self.path, + self.project_id, + ) + + +class Auth: + """Helper object that represents the auth info.""" + + def __init__( + self, + scheme: str, + host: str, + project_id: str, + public_key: str, + secret_key: "Optional[str]" = None, + version: int = 7, + client: "Optional[Any]" = None, + path: str = "/", + ) -> None: + self.scheme = scheme + self.host = host + self.path = path + self.project_id = project_id + self.public_key = public_key + self.secret_key = secret_key + self.version = version + self.client = client + + def get_api_url( + self, + type: "EndpointType" = EndpointType.ENVELOPE, + ) -> str: + """Returns the API url for storing events.""" + return "%s://%s%sapi/%s/%s/" % ( + self.scheme, + self.host, + self.path, + self.project_id, + type.value, + ) + + def to_header(self) -> str: + """Returns the auth header a string.""" + rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] + if self.client is not None: + rv.append(("sentry_client", self.client)) + if self.secret_key is not None: + rv.append(("sentry_secret", self.secret_key)) + return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) + + +def get_type_name(cls: "Optional[type]") -> "Optional[str]": + return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) + + +def get_type_module(cls: "Optional[type]") -> "Optional[str]": + mod = getattr(cls, "__module__", None) + if mod not in (None, "builtins", "__builtins__"): + return mod + return None + + +def should_hide_frame(frame: "FrameType") -> bool: + try: + mod = frame.f_globals["__name__"] + if mod.startswith("sentry_sdk."): + return True + except (AttributeError, KeyError): + pass + + for flag_name in "__traceback_hide__", "__tracebackhide__": + try: + if frame.f_locals[flag_name]: + return True + except Exception: + pass + + return False + + +def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": + tb_: "Optional[TracebackType]" = tb + while tb_ is not None: + if not should_hide_frame(tb_.tb_frame): + yield tb_ + tb_ = tb_.tb_next + + +def get_lines_from_file( + filename: str, + lineno: int, + max_length: "Optional[int]" = None, + loader: "Optional[Any]" = None, + module: "Optional[str]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + context_lines = 5 + source = None + if loader is not None and hasattr(loader, "get_source"): + try: + source_str: "Optional[str]" = loader.get_source(module) + except (ImportError, IOError): + source_str = None + if source_str is not None: + source = source_str.splitlines() + + if source is None: + try: + source = linecache.getlines(filename) + except (OSError, IOError): + return [], None, [] + + if not source: + return [], None, [] + + lower_bound = max(0, lineno - context_lines) + upper_bound = min(lineno + 1 + context_lines, len(source)) + + try: + pre_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[lower_bound:lineno] + ] + context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) + post_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[(lineno + 1) : upper_bound] + ] + return pre_context, context_line, post_context + except IndexError: + # the file may have changed since it was loaded into memory + return [], None, [] + + +def get_source_context( + frame: "FrameType", + tb_lineno: "Optional[int]", + max_value_length: "Optional[int]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + try: + abs_path: "Optional[str]" = frame.f_code.co_filename + except Exception: + abs_path = None + try: + module = frame.f_globals["__name__"] + except Exception: + return [], None, [] + try: + loader = frame.f_globals["__loader__"] + except Exception: + loader = None + + if tb_lineno is not None and abs_path: + lineno = tb_lineno - 1 + return get_lines_from_file( + abs_path, lineno, max_value_length, loader=loader, module=module + ) + + return [], None, [] + + +def safe_str(value: "Any") -> str: + try: + return str(value) + except Exception: + return safe_repr(value) + + +def safe_repr(value: "Any") -> str: + try: + return repr(value) + except Exception: + return "" + + +def filename_for_module( + module: "Optional[str]", abs_path: "Optional[str]" +) -> "Optional[str]": + if not abs_path or not module: + return abs_path + + try: + if abs_path.endswith(".pyc"): + abs_path = abs_path[:-1] + + base_module = module.split(".", 1)[0] + if base_module == module: + return os.path.basename(abs_path) + + base_module_path = sys.modules[base_module].__file__ + if not base_module_path: + return abs_path + + return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( + os.sep + ) + except Exception: + return abs_path + + +def serialize_frame( + frame: "FrameType", + tb_lineno: "Optional[int]" = None, + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, +) -> "Dict[str, Any]": + f_code = getattr(frame, "f_code", None) + if not f_code: + abs_path = None + function = None + else: + abs_path = frame.f_code.co_filename + function = frame.f_code.co_name + try: + module = frame.f_globals["__name__"] + except Exception: + module = None + + if tb_lineno is None: + tb_lineno = frame.f_lineno + + try: + os_abs_path = os.path.abspath(abs_path) if abs_path else None + except Exception: + os_abs_path = None + + rv: "Dict[str, Any]" = { + "filename": filename_for_module(module, abs_path) or None, + "abs_path": os_abs_path, + "function": function or "", + "module": module, + "lineno": tb_lineno, + } + + if include_source_context: + rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( + frame, tb_lineno, max_value_length + ) + + if include_local_variables: + from sentry_sdk.serializer import serialize + + rv["vars"] = serialize( + dict(frame.f_locals), is_vars=True, custom_repr=custom_repr + ) + + return rv + + +def current_stacktrace( + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, +) -> "Dict[str, Any]": + __tracebackhide__ = True + frames = [] + + f: "Optional[FrameType]" = sys._getframe() + while f is not None: + if not should_hide_frame(f): + frames.append( + serialize_frame( + f, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + ) + ) + f = f.f_back + + frames.reverse() + + return {"frames": frames} + + +def get_errno(exc_value: BaseException) -> "Optional[Any]": + return getattr(exc_value, "errno", None) + + +def get_error_message(exc_value: "Optional[BaseException]") -> str: + message: str = safe_str( + getattr(exc_value, "message", "") + or getattr(exc_value, "detail", "") + or safe_str(exc_value) + ) + + # __notes__ should be a list of strings when notes are added + # via add_note, but can be anything else if __notes__ is set + # directly. We only support strings in __notes__, since that + # is the correct use. + notes: object = getattr(exc_value, "__notes__", None) + if isinstance(notes, list) and len(notes) > 0: + message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) + + return message + + +def single_exception_from_error_tuple( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: "Optional[int]" = None, + parent_id: "Optional[int]" = None, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "Dict[str, Any]": + """ + Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + """ + exception_value: "Dict[str, Any]" = {} + exception_value["mechanism"] = ( + mechanism.copy() if mechanism else {"type": "generic", "handled": True} + ) + if exception_id is not None: + exception_value["mechanism"]["exception_id"] = exception_id + + if exc_value is not None: + errno = get_errno(exc_value) + else: + errno = None + + if errno is not None: + exception_value["mechanism"].setdefault("meta", {}).setdefault( + "errno", {} + ).setdefault("number", errno) + + if source is not None: + exception_value["mechanism"]["source"] = source + + is_root_exception = exception_id == 0 + if not is_root_exception and parent_id is not None: + exception_value["mechanism"]["parent_id"] = parent_id + exception_value["mechanism"]["type"] = "chained" + + if is_root_exception and "type" not in exception_value["mechanism"]: + exception_value["mechanism"]["type"] = "generic" + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + if is_exception_group: + exception_value["mechanism"]["is_exception_group"] = True + + exception_value["module"] = get_type_module(exc_type) + exception_value["type"] = get_type_name(exc_type) + exception_value["value"] = get_error_message(exc_value) + + if client_options is None: + include_local_variables = True + include_source_context = True + max_value_length = None # fallback + custom_repr = None + else: + include_local_variables = client_options["include_local_variables"] + include_source_context = client_options["include_source_context"] + max_value_length = client_options["max_value_length"] + custom_repr = client_options.get("custom_repr") + + frames: "List[Dict[str, Any]]" = [ + serialize_frame( + tb.tb_frame, + tb_lineno=tb.tb_lineno, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + custom_repr=custom_repr, + ) + # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on + # processing a super-long stacktrace. + for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) + ] + + if len(frames) > MAX_STACK_FRAMES: + # If we have more frames than the limit, we remove the stacktrace completely. + # We don't trim the stacktrace here because we have not processed the whole + # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would + # intelligently trim by removing frames in the middle of the stacktrace, but + # since we don't have the whole stacktrace, we can't do that. Instead, we + # drop the entire stacktrace. + exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( + value=None + ) + + elif frames: + if not full_stack: + new_frames = frames + else: + new_frames = merge_stack_frames(frames, full_stack, client_options) + + exception_value["stacktrace"] = {"frames": new_frames} + + return exception_value + + +HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") + +if HAS_CHAINED_EXCEPTIONS: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + exc_type, exc_value, tb = exc_info + + seen_exceptions = [] + seen_exception_ids: "Set[int]" = set() + + while ( + exc_type is not None + and exc_value is not None + and id(exc_value) not in seen_exception_ids + ): + yield exc_type, exc_value, tb + + # Avoid hashing random types we don't know anything + # about. Use the list to keep a ref so that the `id` is + # not used for another object. + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + if exc_value.__suppress_context__: + cause = exc_value.__cause__ + else: + cause = exc_value.__context__ + if cause is None: + break + exc_type = type(cause) + exc_value = cause + tb = getattr(cause, "__traceback__", None) + +else: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + yield exc_info + + +def exceptions_from_error( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: int = 0, + parent_id: int = 0, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, + seen_exceptions: "Optional[list[BaseException]]" = None, + seen_exception_ids: "Optional[Set[int]]" = None, +) -> "Tuple[int, List[Dict[str, Any]]]": + """ + Creates the list of exceptions. + This can include chained exceptions and exceptions from an ExceptionGroup. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + + Args: + exception_id (int): + + Sequential counter for assigning ``mechanism.exception_id`` + to each processed exception. Is NOT the result of calling `id()` on the exception itself. + + parent_id (int): + + The ``mechanism.exception_id`` of the parent exception. + + Written into ``mechanism.parent_id`` in the event payload so Sentry can + reconstruct the exception tree. + + Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` + values for cycle detection. + """ + + if seen_exception_ids is None: + seen_exception_ids = set() + + if seen_exceptions is None: + seen_exceptions = [] + + if exc_value is not None and id(exc_value) in seen_exception_ids: + return (exception_id, []) + + if exc_value is not None: + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + parent = single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source=source, + full_stack=full_stack, + ) + exceptions = [parent] + + parent_id = exception_id + exception_id += 1 + + should_supress_context = ( + hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore + ) + if should_supress_context: + # Add direct cause. + # The field `__cause__` is set when raised with the exception (using the `from` keyword). + exception_has_cause = ( + exc_value + and hasattr(exc_value, "__cause__") + and exc_value.__cause__ is not None + ) + if exception_has_cause: + cause = exc_value.__cause__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(cause), + exc_value=cause, + tb=getattr(cause, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__cause__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + else: + # Add indirect cause. + # The field `__context__` is assigned if another exception occurs while handling the exception. + exception_has_content = ( + exc_value + and hasattr(exc_value, "__context__") + and exc_value.__context__ is not None + ) + if exception_has_content: + context = exc_value.__context__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(context), + exc_value=context, + tb=getattr(context, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__context__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + # Add exceptions from an ExceptionGroup. + is_exception_group = exc_value and hasattr(exc_value, "exceptions") + if is_exception_group: + for idx, e in enumerate(exc_value.exceptions): # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(e), + exc_value=e, + tb=getattr(e, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source="exceptions[%s]" % idx, + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + return (exception_id, exceptions) + + +def exceptions_from_error_tuple( + exc_info: "ExcInfo", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "List[Dict[str, Any]]": + exc_type, exc_value, tb = exc_info + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + + if is_exception_group: + (_, exceptions) = exceptions_from_error( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=0, + parent_id=0, + full_stack=full_stack, + ) + + else: + exceptions = [] + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + exceptions.append( + single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + full_stack=full_stack, + ) + ) + + exceptions.reverse() + + return exceptions + + +def to_string(value: str) -> str: + try: + return str(value) + except UnicodeDecodeError: + return repr(value)[1:-1] + + +def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": + if "stacktrace" in event: + yield event["stacktrace"] + if "threads" in event: + for thread in event["threads"].get("values") or (): + if "stacktrace" in thread: + yield thread["stacktrace"] + if "exception" in event: + for exception in event["exception"].get("values") or (): + if isinstance(exception, dict) and "stacktrace" in exception: + yield exception["stacktrace"] + + +def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + for frame in stacktrace.get("frames") or (): + yield frame + + +def handle_in_app( + event: "Event", + in_app_exclude: "Optional[List[str]]" = None, + in_app_include: "Optional[List[str]]" = None, + project_root: "Optional[str]" = None, +) -> "Event": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + set_in_app_in_frames( + stacktrace.get("frames"), + in_app_exclude=in_app_exclude, + in_app_include=in_app_include, + project_root=project_root, + ) + + return event + + +def set_in_app_in_frames( + frames: "Any", + in_app_exclude: "Optional[List[str]]", + in_app_include: "Optional[List[str]]", + project_root: "Optional[str]" = None, +) -> "Optional[Any]": + if not frames: + return None + + for frame in frames: + # if frame has already been marked as in_app, skip it + current_in_app = frame.get("in_app") + if current_in_app is not None: + continue + + module = frame.get("module") + + # check if module in frame is in the list of modules to include + if _module_in_list(module, in_app_include): + frame["in_app"] = True + continue + + # check if module in frame is in the list of modules to exclude + if _module_in_list(module, in_app_exclude): + frame["in_app"] = False + continue + + # if frame has no abs_path, skip further checks + abs_path = frame.get("abs_path") + if abs_path is None: + continue + + if _is_external_source(abs_path): + frame["in_app"] = False + continue + + if _is_in_project_root(abs_path, project_root): + frame["in_app"] = True + continue + + return frames + + +def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": + if isinstance(error, tuple) and len(error) == 3: + exc_type, exc_value, tb = error + elif isinstance(error, BaseException): + tb = getattr(error, "__traceback__", None) + if tb is not None: + exc_type = type(error) + exc_value = error + else: + exc_type, exc_value, tb = sys.exc_info() + if exc_value is not error: + tb = None + exc_value = error + exc_type = type(error) + + else: + raise ValueError("Expected Exception object to report, got %s!" % type(error)) + + exc_info = (exc_type, exc_value, tb) + + if TYPE_CHECKING: + # This cast is safe because exc_type and exc_value are either both + # None or both not None. + exc_info = cast(ExcInfo, exc_info) + + return exc_info + + +def merge_stack_frames( + frames: "List[Dict[str, Any]]", + full_stack: "List[Dict[str, Any]]", + client_options: "Optional[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Add the missing frames from full_stack to frames and return the merged list. + """ + frame_ids = { + ( + frame["abs_path"], + frame["context_line"], + frame["lineno"], + frame["function"], + ) + for frame in frames + } + + new_frames = [ + stackframe + for stackframe in full_stack + if ( + stackframe["abs_path"], + stackframe["context_line"], + stackframe["lineno"], + stackframe["function"], + ) + not in frame_ids + ] + new_frames.extend(frames) + + # Limit the number of frames + max_stack_frames = ( + client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) + if client_options + else None + ) + if max_stack_frames is not None: + new_frames = new_frames[len(new_frames) - max_stack_frames :] + + return new_frames + + +def event_from_exception( + exc_info: "Union[BaseException, ExcInfo]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, +) -> "Tuple[Event, Dict[str, Any]]": + exc_info = exc_info_from_error(exc_info) + hint = event_hint_with_exc_info(exc_info) + + if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): + full_stack = current_stacktrace( + include_local_variables=client_options["include_local_variables"], + max_value_length=client_options["max_value_length"], + )["frames"] + else: + full_stack = None + + return ( + { + "level": "error", + "exception": { + "values": exceptions_from_error_tuple( + exc_info, client_options, mechanism, full_stack + ) + }, + }, + hint, + ) + + +def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: + if name is None: + return False + + if not items: + return False + + for item in items: + if item == name or name.startswith(item + "."): + return True + + return False + + +def _is_external_source(abs_path: "Optional[str]") -> bool: + # check if frame is in 'site-packages' or 'dist-packages' + if abs_path is None: + return False + + external_source = ( + re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None + ) + return external_source + + +def _is_in_project_root( + abs_path: "Optional[str]", project_root: "Optional[str]" +) -> bool: + if abs_path is None or project_root is None: + return False + + # check if path is in the project root + if abs_path.startswith(project_root): + return True + + return False + + +def _truncate_by_bytes(string: str, max_bytes: int) -> str: + """ + Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. + """ + truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") + + return truncated + "..." + + +def _get_size_in_bytes(value: str) -> "Optional[int]": + try: + return len(value.encode("utf-8")) + except (UnicodeEncodeError, UnicodeDecodeError): + return None + + +def strip_string( + value: str, max_length: "Optional[int]" = None +) -> "Union[AnnotatedValue, str]": + if not value or max_length is None: + return value + + byte_size = _get_size_in_bytes(value) + text_size = len(value) + + if byte_size is not None and byte_size > max_length: + # truncate to max_length bytes, preserving code points + truncated_value = _truncate_by_bytes(value, max_length) + elif text_size is not None and text_size > max_length: + # fallback to truncating by string length + truncated_value = value[: max_length - 3] + "..." + else: + return value + + return AnnotatedValue( + value=truncated_value, + metadata={ + "len": byte_size or text_size, + "rem": [["!limit", "x", max_length - 3, max_length]], + }, + ) + + +def parse_version(version: str) -> "Optional[Tuple[int, ...]]": + """ + Parses a version string into a tuple of integers. + This uses the parsing loging from PEP 440: + https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + """ + VERSION_PATTERN = r""" # noqa: N806 + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+                [-_\.]?
+                (?P(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+            (?P                                         # post release
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?Ppost|rev|r)
+                    [-_\.]?
+                    (?P[0-9]+)?
+                )
+            )?
+            (?P                                          # dev release
+                [-_\.]?
+                (?Pdev)
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+        )
+        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+    """
+
+    pattern = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    try:
+        release = pattern.match(version).groupdict()["release"]  # type: ignore
+        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
+    except (TypeError, ValueError, AttributeError):
+        return None
+
+    return release_tuple
+
+
+def _is_contextvars_broken() -> bool:
+    """
+    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
+    """
+    try:
+        import gevent
+        from gevent.monkey import is_object_patched
+
+        # Get the MAJOR and MINOR version numbers of Gevent
+        version_tuple = tuple(
+            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
+        )
+        if is_object_patched("threading", "local"):
+            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
+            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
+            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
+            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
+            # for contextvars, is able to patch both thread locals and contextvars, in
+            # that case, check if contextvars are effectively patched.
+            if (
+                # Gevent 20.9.0+
+                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
+                # Gevent 20.5.0+ or Python < 3.7
+                or (is_object_patched("contextvars", "ContextVar"))
+            ):
+                return False
+
+            return True
+    except ImportError:
+        pass
+
+    try:
+        import greenlet
+        from eventlet.patcher import is_monkey_patched  # type: ignore
+
+        greenlet_version = parse_version(greenlet.__version__)
+
+        if greenlet_version is None:
+            logger.error(
+                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
+            )
+            return False
+
+        if is_monkey_patched("thread") and greenlet_version < (0, 5):
+            return True
+    except ImportError:
+        pass
+
+    return False
+
+
+def _make_threadlocal_contextvars(local: type) -> type:
+    class ContextVar:
+        # Super-limited impl of ContextVar
+
+        def __init__(self, name: str, default: "Any" = None) -> None:
+            self._name = name
+            self._default = default
+            self._local = local()
+            self._original_local = local()
+
+        def get(self, default: "Any" = None) -> "Any":
+            return getattr(self._local, "value", default or self._default)
+
+        def set(self, value: "Any") -> "Any":
+            token = str(random.getrandbits(64))
+            original_value = self.get()
+            setattr(self._original_local, token, original_value)
+            self._local.value = value
+            return token
+
+        def reset(self, token: "Any") -> None:
+            self._local.value = getattr(self._original_local, token)
+            # delete the original value (this way it works in Python 3.6+)
+            del self._original_local.__dict__[token]
+
+    return ContextVar
+
+
+def _get_contextvars() -> "Tuple[bool, type]":
+    """
+    Figure out the "right" contextvars installation to use. Returns a
+    `contextvars.ContextVar`-like class with a limited API.
+
+    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
+    """
+    if not _is_contextvars_broken():
+        # aiocontextvars is a PyPI package that ensures that the contextvars
+        # backport (also a PyPI package) works with asyncio under Python 3.6
+        #
+        # Import it if available.
+        if sys.version_info < (3, 7):
+            # `aiocontextvars` is absolutely required for functional
+            # contextvars on Python 3.6.
+            try:
+                from aiocontextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+        else:
+            # On Python 3.7 contextvars are functional.
+            try:
+                from contextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+
+    # Fall back to basic thread-local usage.
+
+    from threading import local
+
+    return False, _make_threadlocal_contextvars(local)
+
+
+HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
+
+CONTEXTVARS_ERROR_MESSAGE = """
+
+With asyncio/ASGI applications, the Sentry SDK requires a functional
+installation of `contextvars` to avoid leaking scope/context data across
+requests.
+
+Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
+"""
+
+_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
+
+# These exceptions won't set the span status to error if they occur. Use
+# register_control_flow_exception to add to this list
+_control_flow_exception_classes: "set[type]" = set()
+
+
+def is_internal_task() -> bool:
+    return _is_sentry_internal_task.get()
+
+
+@contextmanager
+def mark_sentry_task_internal() -> "Generator[None, None, None]":
+    """Context manager to mark a task as Sentry internal."""
+    token = _is_sentry_internal_task.set(True)
+    try:
+        yield
+    finally:
+        _is_sentry_internal_task.reset(token)
+
+
+def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
+    func_qualname: "Optional[str]" = None
+
+    prefix, suffix = "", ""
+
+    if isinstance(func, partial) and hasattr(func.func, "__name__"):
+        prefix, suffix = "partial()"
+        func = func.func
+    else:
+        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
+        # https://github.com/python/cpython/pull/16600
+        partial_method = getattr(func, "_partialmethod", None) or getattr(
+            func, "__partialmethod__", None
+        )
+        if isinstance(partial_method, partialmethod):
+            prefix, suffix = "partialmethod()"
+            func = partial_method.func
+
+    if hasattr(func, "__qualname__"):
+        func_qualname = func.__qualname__
+    elif hasattr(func, "__name__"):
+        func_qualname = func.__name__
+
+    if func_qualname is not None:
+        if hasattr(func, "__module__") and isinstance(func.__module__, str):
+            func_qualname = func.__module__ + "." + func_qualname
+        func_qualname = prefix + func_qualname + suffix
+
+    return func_qualname
+
+
+def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    return qualname_from_function(func)
+
+
+disable_capture_event = ContextVar("disable_capture_event")
+
+
+class ServerlessTimeoutWarning(Exception):  # noqa: N818
+    """Raised when a serverless method is about to reach its timeout."""
+
+    pass
+
+
+class TimeoutThread(threading.Thread):
+    """Creates a Thread which runs (sleeps) for a time duration equal to
+    waiting_time and raises a custom ServerlessTimeout exception.
+    """
+
+    def __init__(
+        self,
+        waiting_time: float,
+        configured_timeout: int,
+        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
+        current_scope: "Optional[sentry_sdk.Scope]" = None,
+    ) -> None:
+        threading.Thread.__init__(self)
+        self.waiting_time = waiting_time
+        self.configured_timeout = configured_timeout
+
+        self.isolation_scope = isolation_scope
+        self.current_scope = current_scope
+
+        self._stop_event = threading.Event()
+
+    def stop(self) -> None:
+        self._stop_event.set()
+
+    def _capture_exception(self) -> "ExcInfo":
+        exc_info = sys.exc_info()
+
+        client = sentry_sdk.get_client()
+        event, hint = event_from_exception(
+            exc_info,
+            client_options=client.options,
+            mechanism={"type": "threading", "handled": False},
+        )
+        sentry_sdk.capture_event(event, hint=hint)
+
+        return exc_info
+
+    def run(self) -> None:
+        self._stop_event.wait(self.waiting_time)
+
+        if self._stop_event.is_set():
+            return
+
+        integer_configured_timeout = int(self.configured_timeout)
+
+        # Setting up the exact integer value of configured time(in seconds)
+        if integer_configured_timeout < self.configured_timeout:
+            integer_configured_timeout = integer_configured_timeout + 1
+
+        # Raising Exception after timeout duration is reached
+        if self.isolation_scope is not None and self.current_scope is not None:
+            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
+                with sentry_sdk.scope.use_scope(self.current_scope):
+                    try:
+                        raise ServerlessTimeoutWarning(
+                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                                integer_configured_timeout
+                            )
+                        )
+                    except Exception:
+                        reraise(*self._capture_exception())
+
+        raise ServerlessTimeoutWarning(
+            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                integer_configured_timeout
+            )
+        )
+
+
+def to_base64(original: str) -> "Optional[str]":
+    """
+    Convert a string to base64, via UTF-8. Returns None on invalid input.
+    """
+    base64_string = None
+
+    try:
+        utf8_bytes = original.encode("UTF-8")
+        base64_bytes = base64.b64encode(utf8_bytes)
+        base64_string = base64_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
+
+    return base64_string
+
+
+def from_base64(base64_string: str) -> "Optional[str]":
+    """
+    Convert a string from base64, via UTF-8. Returns None on invalid input.
+    """
+    utf8_string = None
+
+    try:
+        only_valid_chars = BASE64_ALPHABET.match(base64_string)
+        assert only_valid_chars
+
+        base64_bytes = base64_string.encode("UTF-8")
+        utf8_bytes = base64.b64decode(base64_bytes)
+        utf8_string = utf8_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning(
+            "Unable to decode {b64} from base64:".format(b64=base64_string), err
+        )
+
+    return utf8_string
+
+
+Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
+
+
+def sanitize_url(
+    url: str,
+    remove_authority: bool = True,
+    remove_query_values: bool = True,
+    split: bool = False,
+) -> "Union[str, Components]":
+    """
+    Removes the authority and query parameter values from a given URL.
+    """
+    parsed_url = urlsplit(url)
+    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
+
+    # strip username:password (netloc can be usr:pwd@example.com)
+    if remove_authority:
+        netloc_parts = parsed_url.netloc.split("@")
+        if len(netloc_parts) > 1:
+            netloc = "%s:%s@%s" % (
+                SENSITIVE_DATA_SUBSTITUTE,
+                SENSITIVE_DATA_SUBSTITUTE,
+                netloc_parts[-1],
+            )
+        else:
+            netloc = parsed_url.netloc
+    else:
+        netloc = parsed_url.netloc
+
+    # strip values from query string
+    if remove_query_values:
+        query_string = unquote(
+            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
+        )
+    else:
+        query_string = parsed_url.query
+
+    components = Components(
+        scheme=parsed_url.scheme,
+        netloc=netloc,
+        query=query_string,
+        path=parsed_url.path,
+        fragment=parsed_url.fragment,
+    )
+
+    if split:
+        return components
+    else:
+        return urlunsplit(components)
+
+
+ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
+
+
+def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
+    """
+    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
+    parameters will be sanitized to remove sensitive data. The autority (username and password)
+    in the URL will always be removed.
+    """
+    parsed_url = sanitize_url(
+        url, remove_authority=True, remove_query_values=sanitize, split=True
+    )
+
+    base_url = urlunsplit(
+        Components(
+            scheme=parsed_url.scheme,  # type: ignore
+            netloc=parsed_url.netloc,  # type: ignore
+            query="",
+            path=parsed_url.path,  # type: ignore
+            fragment="",
+        )
+    )
+
+    return ParsedUrl(
+        url=base_url,
+        query=parsed_url.query,  # type: ignore
+        fragment=parsed_url.fragment,  # type: ignore
+    )
+
+
+def is_valid_sample_rate(rate: "Any", source: str) -> bool:
+    """
+    Checks the given sample rate to make sure it is valid type and value (a
+    boolean or a number between 0 and 1, inclusive).
+    """
+
+    # both booleans and NaN are instances of Real, so a) checking for Real
+    # checks for the possibility of a boolean also, and b) we have to check
+    # separately for NaN and Decimal does not derive from Real so need to check that too
+    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
+                source=source, rate=rate, type=type(rate)
+            )
+        )
+        return False
+
+    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
+    rate = float(rate)
+    if rate < 0 or rate > 1:
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
+                source=source, rate=rate
+            )
+        )
+        return False
+
+    return True
+
+
+def match_regex_list(
+    item: str,
+    regex_list: "Optional[List[str]]" = None,
+    substring_matching: bool = False,
+) -> bool:
+    if regex_list is None:
+        return False
+
+    for item_matcher in regex_list:
+        if not substring_matching and item_matcher[-1] != "$":
+            item_matcher += "$"
+
+        matched = re.search(item_matcher, item)
+        if matched:
+            return True
+
+    return False
+
+
+def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
+    """
+    Determines whether the given URL matches the Sentry DSN.
+    """
+    return (
+        client is not None
+        and client.transport is not None
+        and client.transport.parsed_dsn is not None
+        and client.transport.parsed_dsn.netloc in url
+    )
+
+
+def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
+    try:
+        from importlib import metadata
+
+        yielded = set()
+        for dist in metadata.distributions():
+            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
+            # `metadata` values may be `None`, see:
+            # https://github.com/python/cpython/issues/91216
+            # and
+            # https://github.com/python/importlib_metadata/issues/371
+            if name is not None:
+                normalized_name = _normalize_module_name(name)
+                if dist.version is not None and normalized_name not in yielded:
+                    yield normalized_name, dist.version
+                    yielded.add(normalized_name)
+
+    except ImportError:
+        # < py3.8
+        try:
+            import pkg_resources
+        except ImportError:
+            return
+
+        for info in pkg_resources.working_set:
+            yield _normalize_module_name(info.key), info.version
+
+
+def _normalize_module_name(name: str) -> str:
+    return name.lower()
+
+
+def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
+    # https://peps.python.org/pep-0503/#normalized-names
+    return re.sub(r"[-_.]+", "-", name)
+
+
+def _get_installed_modules() -> "Dict[str, str]":
+    global _installed_modules
+    if _installed_modules is None:
+        _installed_modules = dict(_generate_installed_modules())
+    return _installed_modules
+
+
+def package_version(package: str) -> "Optional[Tuple[int, ...]]":
+    normalized_package = _normalize_module_name(
+        _replace_hyphens_dots_and_underscores_with_dashes(package)
+    )
+
+    installed_packages = {
+        _replace_hyphens_dots_and_underscores_with_dashes(module): v
+        for module, v in _get_installed_modules().items()
+    }
+    version = installed_packages.get(normalized_package)
+    if version is None:
+        return None
+
+    return parse_version(version)
+
+
+def reraise(
+    tp: "Optional[Type[BaseException]]",
+    value: "Optional[BaseException]",
+    tb: "Optional[Any]" = None,
+) -> "NoReturn":
+    assert value is not None
+    if value.__traceback__ is not tb:
+        raise value.with_traceback(tb)
+    raise value
+
+
+def _no_op(*_a: "Any", **_k: "Any") -> None:
+    """No-op function for ensure_integration_enabled."""
+    pass
+
+
+if TYPE_CHECKING:
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+        original_function: "Callable[P, R]",
+    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
+
+
+def ensure_integration_enabled(
+    integration: "type[sentry_sdk.integrations.Integration]",
+    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
+) -> "Callable[[Callable[P, R]], Callable[P, R]]":
+    """
+    Ensures a given integration is enabled prior to calling a Sentry-patched function.
+
+    The function takes as its parameters the integration that must be enabled and the original
+    function that the SDK is patching. The function returns a function that takes the
+    decorated (Sentry-patched) function as its parameter, and returns a function that, when
+    called, checks whether the given integration is enabled. If the integration is enabled, the
+    function calls the decorated, Sentry-patched function. If the integration is not enabled,
+    the original function is called.
+
+    The function also takes care of preserving the original function's signature and docstring.
+
+    Example usage:
+
+    ```python
+    @ensure_integration_enabled(MyIntegration, my_function)
+    def patch_my_function():
+        with sentry_sdk.start_transaction(...):
+            return my_function()
+    ```
+    """
+    if TYPE_CHECKING:
+        # Type hint to ensure the default function has the right typing. The overloads
+        # ensure the default _no_op function is only used when R is None.
+        original_function = cast(Callable[P, R], original_function)
+
+    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
+        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
+            if sentry_sdk.get_client().get_integration(integration) is None:
+                return original_function(*args, **kwargs)
+
+            return sentry_patched_function(*args, **kwargs)
+
+        if original_function is _no_op:
+            return wraps(sentry_patched_function)(runner)
+
+        return wraps(original_function)(runner)
+
+    return patcher
+
+
+if PY37:
+
+    def nanosecond_time() -> int:
+        return time.perf_counter_ns()
+
+else:
+
+    def nanosecond_time() -> int:
+        return int(time.perf_counter() * 1e9)
+
+
+def now() -> float:
+    return time.perf_counter()
+
+
+try:
+    from gevent import get_hub as get_gevent_hub
+    from gevent.monkey import is_module_patched
+except ImportError:
+    # it's not great that the signatures are different, get_hub can't return None
+    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
+    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
+        return None
+
+    def is_module_patched(mod_name: str) -> bool:
+        # unable to import from gevent means no modules have been patched
+        return False
+
+
+def is_gevent() -> bool:
+    return is_module_patched("threading") or is_module_patched("_thread")
+
+
+def get_current_thread_meta(
+    thread: "Optional[threading.Thread]" = None,
+) -> "Tuple[Optional[int], Optional[str]]":
+    """
+    Try to get the id of the current thread, with various fall backs.
+    """
+
+    # if a thread is specified, that takes priority
+    if thread is not None:
+        try:
+            thread_id = thread.ident
+            thread_name = thread.name
+            if thread_id is not None:
+                return thread_id, thread_name
+        except AttributeError:
+            pass
+
+    # if the app is using gevent, we should look at the gevent hub first
+    # as the id there differs from what the threading module reports
+    if is_gevent():
+        gevent_hub = get_gevent_hub()
+        if gevent_hub is not None:
+            try:
+                # this is undocumented, so wrap it in try except to be safe
+                return gevent_hub.thread_ident, None
+            except AttributeError:
+                pass
+
+    # use the current thread's id if possible
+    try:
+        thread = threading.current_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # if we can't get the current thread id, fall back to the main thread id
+    try:
+        thread = threading.main_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # we've tried everything, time to give up
+    return None, None
+
+
+def _register_control_flow_exception(
+    exc_type: "Union[type, list[type], tuple[type], set[type]]",
+) -> None:
+    if isinstance(exc_type, (list, tuple, set)):
+        _control_flow_exception_classes.update(exc_type)
+    else:
+        _control_flow_exception_classes.add(exc_type)
+
+
+def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
+    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
+        # https://docs.python.org/3/library/exceptions.html#SystemExit
+        return False
+
+    if issubclass(ty, tuple(_control_flow_exception_classes)):
+        return False
+
+    return True
+
+
+if TYPE_CHECKING:
+    T = TypeVar("T")
+
+
+def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
+    """
+    Attempt to convert from an unknown type to a specific type, using the
+    given function. Return None if the conversion fails, i.e. if the function
+    raises an exception.
+    """
+    try:
+        if isinstance(value, convert_func):  # type: ignore
+            return value
+    except TypeError:
+        pass
+
+    try:
+        return convert_func(value)
+    except Exception:
+        return None
+
+
+def safe_serialize(data: "Any") -> str:
+    """Safely serialize to a readable string."""
+
+    def serialize_item(
+        item: "Any",
+    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
+        if callable(item):
+            try:
+                module = getattr(item, "__module__", None)
+                qualname = getattr(item, "__qualname__", None)
+                name = getattr(item, "__name__", "anonymous")
+
+                if module and qualname:
+                    full_path = f"{module}.{qualname}"
+                elif module and name:
+                    full_path = f"{module}.{name}"
+                else:
+                    full_path = name
+
+                return f""
+            except Exception:
+                return f""
+        elif isinstance(item, dict):
+            return {k: serialize_item(v) for k, v in item.items()}
+        elif isinstance(item, (list, tuple)):
+            return [serialize_item(x) for x in item]
+        elif hasattr(item, "__dict__"):
+            try:
+                attrs = {
+                    k: serialize_item(v)
+                    for k, v in vars(item).items()
+                    if not k.startswith("_")
+                }
+                return f"<{type(item).__name__} {attrs}>"
+            except Exception:
+                return repr(item)
+        else:
+            return item
+
+    try:
+        serialized = serialize_item(data)
+        return (
+            json.dumps(serialized, default=str)
+            if not isinstance(serialized, str)
+            else serialized
+        )
+    except Exception:
+        return str(data)
+
+
+def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(
+        options.get("enable_logs", False)
+        or options["_experiments"].get("enable_logs", False)
+    )
+
+
+def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return "data_collection" in options.get("_experiments", {})
+
+
+def get_before_send_log(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_log") or options["_experiments"].get(
+        "before_send_log"
+    )
+
+
+def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(options.get("enable_metrics", True))
+
+
+def get_before_send_metric(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_metric") or options["_experiments"].get(
+        "before_send_metric"
+    )
+
+
+def get_before_send_span(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
+    if options is None:
+        return None
+
+    return options["_experiments"].get("before_send_span")
+
+
+def format_attribute(val: "Any") -> "AttributeValue":
+    """
+    Turn unsupported attribute value types into an AttributeValue.
+
+    We do this as soon as a user-provided attribute is set, to prevent spans,
+    logs, metrics and similar from having live references to various objects.
+
+    Note: This is not the final attribute value format. Before they're sent,
+    they're serialized further into the actual format the protocol expects:
+    https://develop.sentry.dev/sdk/telemetry/attributes/
+    """
+    if isinstance(val, (bool, int, float, str)):
+        return val
+
+    if isinstance(val, (list, tuple)) and not val:
+        return []
+    elif isinstance(val, list):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return copy.deepcopy(val)
+    elif isinstance(val, tuple):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return list(val)
+
+    return safe_repr(val)
+
+
+def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
+    """Serialize attribute value to the transport format."""
+    if isinstance(val, bool):
+        return {"value": val, "type": "boolean"}
+    if isinstance(val, int):
+        return {"value": val, "type": "integer"}
+    if isinstance(val, float):
+        return {"value": val, "type": "double"}
+    if isinstance(val, str):
+        return {"value": val, "type": "string"}
+
+    if isinstance(val, list):
+        if not val:
+            return {"value": [], "type": "array"}
+
+        # Only lists of elements of a single type are supported
+        ty = type(val[0])
+        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
+            return {"value": val, "type": "array"}
+
+    # Coerce to string if we don't know what to do with the value. This should
+    # never happen as we pre-format early in format_attribute, but let's be safe.
+    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py
new file mode 100644
index 0000000000..5eb9b23130
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py
@@ -0,0 +1,308 @@
+import asyncio
+import os
+import threading
+from abc import ABC, abstractmethod
+from time import sleep, time
+from typing import TYPE_CHECKING
+
+from sentry_sdk._queue import FullError, Queue
+from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
+from sentry_sdk.utils import logger, mark_sentry_task_internal
+
+if TYPE_CHECKING:
+    from typing import Any, Callable, Optional
+
+
+_TERMINATOR = object()
+
+
+class Worker(ABC):
+    """Base class for all workers."""
+
+    @property
+    @abstractmethod
+    def is_alive(self) -> bool:
+        """Whether the worker is alive and running."""
+        pass
+
+    @abstractmethod
+    def kill(self) -> None:
+        """Kill the worker. It will not process any more events."""
+        pass
+
+    def flush(
+        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
+    ) -> None:
+        """Flush the worker, blocking until done or timeout is reached."""
+        return None
+
+    @abstractmethod
+    def full(self) -> bool:
+        """Whether the worker's queue is full."""
+        pass
+
+    @abstractmethod
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        """Schedule a callback. Returns True if queued, False if full."""
+        pass
+
+
+class BackgroundWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Queue" = Queue(queue_size)
+        self._lock = threading.Lock()
+        self._thread: "Optional[threading.Thread]" = None
+        self._thread_for_pid: "Optional[int]" = None
+
+    @property
+    def is_alive(self) -> bool:
+        if self._thread_for_pid != os.getpid():
+            return False
+        if not self._thread:
+            return False
+        return self._thread.is_alive()
+
+    def _ensure_thread(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    def _timed_queue_join(self, timeout: float) -> bool:
+        deadline = time() + timeout
+        queue = self._queue
+
+        queue.all_tasks_done.acquire()
+
+        try:
+            while queue.unfinished_tasks:
+                delay = deadline - time()
+                if delay <= 0:
+                    return False
+                queue.all_tasks_done.wait(timeout=delay)
+
+            return True
+        finally:
+            queue.all_tasks_done.release()
+
+    def start(self) -> None:
+        with self._lock:
+            if not self.is_alive:
+                self._thread = threading.Thread(
+                    target=self._target, name="sentry-sdk.BackgroundWorker"
+                )
+                self._thread.daemon = True
+                try:
+                    self._thread.start()
+                    self._thread_for_pid = os.getpid()
+                except RuntimeError:
+                    # At this point we can no longer start because the interpreter
+                    # is already shutting down.  Sadly at this point we can no longer
+                    # send out events.
+                    self._thread = None
+
+    def kill(self) -> None:
+        """
+        Kill worker thread. Returns immediately. Not useful for
+        waiting on shutdown for events, use `flush` for that.
+        """
+        logger.debug("background worker got kill request")
+        with self._lock:
+            if self._thread:
+                try:
+                    self._queue.put_nowait(_TERMINATOR)
+                except FullError:
+                    logger.debug("background worker queue full, kill failed")
+
+                self._thread = None
+                self._thread_for_pid = None
+
+    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
+        logger.debug("background worker got flush request")
+        with self._lock:
+            if self.is_alive and timeout > 0.0:
+                self._wait_flush(timeout, callback)
+        logger.debug("background worker flushed")
+
+    def full(self) -> bool:
+        return self._queue.full()
+
+    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
+        initial_timeout = min(0.1, timeout)
+        if not self._timed_queue_join(initial_timeout):
+            pending = self._queue.qsize() + 1
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            if not self._timed_queue_join(timeout - initial_timeout):
+                pending = self._queue.qsize() + 1
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_thread()
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except FullError:
+            return False
+
+    def _target(self) -> None:
+        while True:
+            callback = self._queue.get()
+            try:
+                if callback is _TERMINATOR:
+                    break
+                try:
+                    callback()
+                except Exception:
+                    logger.error("Failed processing job", exc_info=True)
+            finally:
+                self._queue.task_done()
+            sleep(0)
+
+
+class AsyncWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Optional[asyncio.Queue[Any]]" = None
+        self._queue_size = queue_size
+        self._task: "Optional[asyncio.Task[None]]" = None
+        # Event loop needs to remain in the same process
+        self._task_for_pid: "Optional[int]" = None
+        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
+        # Track active callback tasks so they have a strong reference and can be cancelled on kill
+        self._active_tasks: "set[asyncio.Task[None]]" = set()
+
+    @property
+    def is_alive(self) -> bool:
+        if self._task_for_pid != os.getpid():
+            return False
+        if not self._task or not self._loop:
+            return False
+        return self._loop.is_running() and not self._task.done()
+
+    def kill(self) -> None:
+        if self._task:
+            # Cancel the main consumer task to prevent duplicate consumers
+            self._task.cancel()
+            # Also cancel any active callback tasks
+            # Avoid modifying the set while cancelling tasks
+            tasks_to_cancel = set(self._active_tasks)
+            for task in tasks_to_cancel:
+                task.cancel()
+            self._active_tasks.clear()
+            self._loop = None
+            self._task = None
+            self._task_for_pid = None
+
+    def start(self) -> None:
+        if not self.is_alive:
+            try:
+                self._loop = asyncio.get_running_loop()
+                # Always create a fresh queue on start to avoid stale items
+                self._queue = asyncio.Queue(maxsize=self._queue_size)
+                with mark_sentry_task_internal():
+                    self._task = self._loop.create_task(self._target())
+                self._task_for_pid = os.getpid()
+            except RuntimeError:
+                # There is no event loop running
+                logger.warning("No event loop running, async worker not started")
+                self._loop = None
+                self._task = None
+                self._task_for_pid = None
+
+    def full(self) -> bool:
+        if self._queue is None:
+            return True
+        return self._queue.full()
+
+    def _ensure_task(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    async def _wait_flush(
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> None:
+        if not self._loop or not self._loop.is_running() or self._queue is None:
+            return
+
+        initial_timeout = min(0.1, timeout)
+
+        # Timeout on the join
+        try:
+            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
+        except asyncio.TimeoutError:
+            pending = self._queue.qsize() + len(self._active_tasks)
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            try:
+                remaining_timeout = timeout - initial_timeout
+                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
+            except asyncio.TimeoutError:
+                pending = self._queue.qsize() + len(self._active_tasks)
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def flush(  # type: ignore[override]
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> "Optional[asyncio.Task[None]]":
+        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
+            with mark_sentry_task_internal():
+                return self._loop.create_task(self._wait_flush(timeout, callback))
+        return None
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_task()
+        if self._queue is None:
+            return False
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except asyncio.QueueFull:
+            return False
+
+    async def _target(self) -> None:
+        if self._queue is None:
+            return
+        try:
+            while True:
+                callback = await self._queue.get()
+                if callback is _TERMINATOR:
+                    self._queue.task_done()
+                    break
+                # Firing tasks instead of awaiting them allows for concurrent requests
+                with mark_sentry_task_internal():
+                    task = asyncio.create_task(self._process_callback(callback))
+                # Create a strong reference to the task so it can be cancelled on kill
+                # and does not get garbage collected while running
+                self._active_tasks.add(task)
+                # Capture queue ref at dispatch time so done callbacks use the
+                # correct queue even if kill()/start() replace self._queue.
+                queue_ref = self._queue
+                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
+                # Yield to let the event loop run other tasks
+                await asyncio.sleep(0)
+        except asyncio.CancelledError:
+            pass  # Expected during kill()
+
+    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
+        # Callback is an async coroutine, need to await it
+        await callback()
+
+    def _on_task_complete(
+        self,
+        task: "asyncio.Task[None]",
+        queue: "Optional[asyncio.Queue[Any]]" = None,
+    ) -> None:
+        try:
+            task.result()
+        except asyncio.CancelledError:
+            pass  # Task was cancelled, expected during shutdown
+        except Exception:
+            logger.error("Failed processing job", exc_info=True)
+        finally:
+            # Mark the task as done and remove it from the active tasks set
+            # Use the queue reference captured at dispatch time, not self._queue,
+            # to avoid calling task_done() on a different queue after kill()/start().
+            if queue is not None:
+                queue.task_done()
+            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER
new file mode 100644
index 0000000000..5c69047b2e
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA
new file mode 100644
index 0000000000..9c7a4703f8
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA
@@ -0,0 +1,163 @@
+Metadata-Version: 2.4
+Name: urllib3
+Version: 2.7.0
+Summary: HTTP library with thread-safe connection pooling, file post, and more.
+Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
+Project-URL: Documentation, https://urllib3.readthedocs.io
+Project-URL: Code, https://github.com/urllib3/urllib3
+Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
+Author-email: Andrey Petrov 
+Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
+License-Expression: MIT
+License-File: LICENSE.txt
+Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Software Development :: Libraries
+Requires-Python: >=3.10
+Provides-Extra: brotli
+Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
+Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
+Provides-Extra: h2
+Requires-Dist: h2<5,>=4; extra == 'h2'
+Provides-Extra: socks
+Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
+Provides-Extra: zstd
+Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
+Description-Content-Type: text/markdown
+
+

+ +![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) + +

+ +

+ PyPI Version + Python Versions + Join our Discord + Coverage Status + Build Status on GitHub + Documentation Status
+ OpenSSF Scorecard + SLSA 3 + CII Best Practices +

+ +urllib3 is a powerful, *user-friendly* HTTP client for Python. +urllib3 brings many critical features that are missing from the Python +standard libraries: + +- Thread safety. +- Connection pooling. +- Client-side SSL/TLS verification. +- File uploads with multipart encoding. +- Helpers for retrying requests and dealing with HTTP redirects. +- Support for gzip, deflate, brotli, and zstd encoding. +- Proxy support for HTTP and SOCKS. +- 100% test coverage. + +... and many more features, but most importantly: Our maintainers have a 15+ +year track record of maintaining urllib3 with the highest code standards and +attention to security and safety. + +[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) +and you should too. + + +## Installing + +urllib3 can be installed with [pip](https://pip.pypa.io): + +```bash +$ python -m pip install urllib3 +``` + +Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): + +```bash +$ git clone https://github.com/urllib3/urllib3.git +$ cd urllib3 +$ pip install . +``` + +## Getting Started + +urllib3 is easy to use: + +```python3 +>>> import urllib3 +>>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") +>>> resp.status +200 +>>> resp.data +b"User-agent: *\nDisallow: /deny\n" +``` + +urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). + + +## Community + +urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and +collaborating with other contributors. Drop by and say hello 👋 + + +## Contributing + +urllib3 happily accepts contributions. Please see our +[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) +for some tips on getting started. + + +## Security Disclosures + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure with maintainers. + + +## Maintainers + +Meet our maintainers since 2008: + +- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) +- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) +- [@pquentin](https://github.com/pquentin) (Quentin Pradet) +- [@theacodes](https://github.com/theacodes) (Thea Flowers) +- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) +- [@lukasa](https://github.com/lukasa) (Cory Benfield) +- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) +- [@shazow](https://github.com/shazow) (Andrey Petrov) + +👋 + + +## Sponsorship + +If your company benefits from this library, please consider [sponsoring its +development](https://urllib3.readthedocs.io/en/latest/sponsors.html). + + +## For Enterprise + +Professional support for urllib3 is available as part of the [Tidelift +Subscription][1]. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD new file mode 100644 index 0000000000..8c4f9c0b4b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD @@ -0,0 +1,44 @@ +urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 +urllib3-2.7.0.dist-info/RECORD,, +urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 +urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 +urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 +urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 +urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 +urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 +urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 +urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 +urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 +urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 +urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 +urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 +urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 +urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 +urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 +urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 +urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 +urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 +urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 +urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 +urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 +urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 +urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 +urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 +urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 +urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 +urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 +urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 +urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 +urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 +urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 +urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 +urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 +urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 +urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 +urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 +urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 +urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 +urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL new file mode 100644 index 0000000000..b1b94fd58e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000..e6183d0276 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py new file mode 100644 index 0000000000..3fe782c8a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py @@ -0,0 +1,211 @@ +""" +Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more +""" + +from __future__ import annotations + +# Set default logging handler to avoid "No handler found" warnings. +import logging +import sys +import typing +import warnings +from logging import NullHandler + +from . import exceptions +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._version import __version__ +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import BaseHTTPResponse, HTTPResponse +from .util.request import make_headers +from .util.retry import Retry +from .util.timeout import Timeout + +# Ensure that Python is compiled with OpenSSL 1.1.1+ +# If the 'ssl' module isn't available at all that's +# fine, we only care if the module is available. +try: + import ssl +except ImportError: + pass +else: + if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: + warnings.warn( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/3020", + exceptions.NotOpenSSLWarning, + ) + elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: + raise ImportError( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/2168" + ) + +__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" +__license__ = "MIT" +__version__ = __version__ + +__all__ = ( + "HTTPConnectionPool", + "HTTPHeaderDict", + "HTTPSConnectionPool", + "PoolManager", + "ProxyManager", + "HTTPResponse", + "Retry", + "Timeout", + "add_stderr_logger", + "connection_from_url", + "disable_warnings", + "encode_multipart_formdata", + "make_headers", + "proxy_from_url", + "request", + "BaseHTTPResponse", +) + +logging.getLogger(__name__).addHandler(NullHandler()) + + +def add_stderr_logger( + level: int = logging.DEBUG, +) -> logging.StreamHandler[typing.TextIO]: + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug("Added a stderr logging handler to logger: %s", __name__) + return handler + + +# ... Clean up. +del NullHandler + + +# All warning filters *must* be appended unless you're really certain that they +# shouldn't be: otherwise, it's very hard for users to use most Python +# mechanisms to silence them. +# SecurityWarning's always go off by default. +warnings.simplefilter("always", exceptions.SecurityWarning, append=True) +# InsecurePlatformWarning's don't vary between requests, so we keep it default. +warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) + + +def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter("ignore", category) + + +_DEFAULT_POOL = PoolManager() + + +def request( + method: str, + url: str, + *, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + preload_content: bool | None = True, + decode_content: bool | None = True, + redirect: bool | None = True, + retries: Retry | bool | int | None = None, + timeout: Timeout | float | int | None = 3, + json: typing.Any | None = None, +) -> BaseHTTPResponse: + """ + A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. + Therefore, its side effects could be shared across dependencies relying on it. + To avoid side effects create a new ``PoolManager`` instance and use it instead. + The method does not accept low-level ``**urlopen_kw`` keyword arguments. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + + return _DEFAULT_POOL.request( + method, + url, + body=body, + fields=fields, + headers=headers, + preload_content=preload_content, + decode_content=decode_content, + redirect=redirect, + retries=retries, + timeout=timeout, + json=json, + ) + + +if sys.platform == "emscripten": + from .contrib.emscripten import inject_into_urllib3 # noqa: 401 + + inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py new file mode 100644 index 0000000000..992ec1657a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import typing + +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from .util.url import Url + +_TYPE_BODY = typing.Union[ + bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str +] + + +class ProxyConfig(typing.NamedTuple): + ssl_context: ssl.SSLContext | None + use_forwarding_for_https: bool + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + + +class _ResponseOptions(typing.NamedTuple): + # TODO: Remove this in favor of a better + # HTTP request/response lifecycle tracking. + request_method: str + request_url: str + preload_content: bool + decode_content: bool + enforce_content_length: bool + + +if typing.TYPE_CHECKING: + import ssl + from typing import Protocol + + from .response import BaseHTTPResponse + + class BaseHTTPConnection(Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + host: str + port: int + timeout: None | ( + float + ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool + proxy_is_verified: bool | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: ... + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: ... + + def connect(self) -> None: ... + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: ... + + def getresponse(self) -> BaseHTTPResponse: ... + + def close(self) -> None: ... + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + + class BaseHTTPSConnection(BaseHTTPConnection, Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + # Certificate verification methods + cert_reqs: int | str | None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + ssl_context: ssl.SSLContext | None + + # Trusted CAs + ca_certs: str | None + ca_cert_dir: str | None + ca_cert_data: None | str | bytes + + # TLS version + ssl_minimum_version: int | None + ssl_maximum_version: int | None + ssl_version: int | str | None # Deprecated + + # Client certificates + cert_file: str | None + key_file: str | None + key_password: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py new file mode 100644 index 0000000000..ee9ca662b6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py @@ -0,0 +1,486 @@ +from __future__ import annotations + +import typing +from collections import OrderedDict +from enum import Enum, auto +from threading import RLock + +if typing.TYPE_CHECKING: + # We can only import Protocol if TYPE_CHECKING because it's a development + # dependency, and is not available at runtime. + from typing import Protocol + + from typing_extensions import Self + + class HasGettableStringKeys(Protocol): + def keys(self) -> typing.Iterator[str]: ... + + def __getitem__(self, key: str) -> str: ... + + +__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] + + +# Key type +_KT = typing.TypeVar("_KT") +# Value type +_VT = typing.TypeVar("_VT") +# Default type +_DT = typing.TypeVar("_DT") + +ValidHTTPHeaderSource = typing.Union[ + "HTTPHeaderDict", + typing.Mapping[str, str], + typing.Iterable[tuple[str, str]], + "HasGettableStringKeys", +] + + +class _Sentinel(Enum): + not_passed = auto() + + +def ensure_can_construct_http_header_dict( + potential: object, +) -> ValidHTTPHeaderSource | None: + if isinstance(potential, HTTPHeaderDict): + return potential + elif isinstance(potential, typing.Mapping): + # Full runtime checking of the contents of a Mapping is expensive, so for the + # purposes of typechecking, we assume that any Mapping is the right shape. + return typing.cast(typing.Mapping[str, str], potential) + elif isinstance(potential, typing.Iterable): + # Similarly to Mapping, full runtime checking of the contents of an Iterable is + # expensive, so for the purposes of typechecking, we assume that any Iterable + # is the right shape. + return typing.cast(typing.Iterable[tuple[str, str]], potential) + elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): + return typing.cast("HasGettableStringKeys", potential) + else: + return None + + +class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + _container: typing.OrderedDict[_KT, _VT] + _maxsize: int + dispose_func: typing.Callable[[_VT], None] | None + lock: RLock + + def __init__( + self, + maxsize: int = 10, + dispose_func: typing.Callable[[_VT], None] | None = None, + ) -> None: + super().__init__() + self._maxsize = maxsize + self.dispose_func = dispose_func + self._container = OrderedDict() + self.lock = RLock() + + def __getitem__(self, key: _KT) -> _VT: + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key: _KT, value: _VT) -> None: + evicted_item = None + with self.lock: + # Possibly evict the existing value of 'key' + try: + # If the key exists, we'll overwrite it, which won't change the + # size of the pool. Because accessing a key should move it to + # the end of the eviction line, we pop it out first. + evicted_item = key, self._container.pop(key) + self._container[key] = value + except KeyError: + # When the key does not exist, we insert the value first so that + # evicting works in all cases, including when self._maxsize is 0 + self._container[key] = value + if len(self._container) > self._maxsize: + # If we didn't evict an existing value, and we've hit our maximum + # size, then we have to evict the least recently used item from + # the beginning of the container. + evicted_item = self._container.popitem(last=False) + + # After releasing the lock on the pool, dispose of any evicted value. + if evicted_item is not None and self.dispose_func: + _, evicted_value = evicted_item + self.dispose_func(evicted_value) + + def __delitem__(self, key: _KT) -> None: + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self) -> int: + with self.lock: + return len(self._container) + + def __iter__(self) -> typing.NoReturn: + raise NotImplementedError( + "Iteration over this class is unlikely to be threadsafe." + ) + + def clear(self) -> None: + with self.lock: + # Copy pointers to all values, then wipe the mapping + values = list(self._container.values()) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self) -> set[_KT]: # type: ignore[override] + with self.lock: + return set(self._container.keys()) + + +class HTTPHeaderDictItemView(set[tuple[str, str]]): + """ + HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of + address. + + If we directly try to get an item with a particular name, we will get a string + back that is the concatenated version of all the values: + + >>> d['X-Header-Name'] + 'Value1, Value2, Value3' + + However, if we iterate over an HTTPHeaderDict's items, we will optionally combine + these values based on whether combine=True was called when building up the dictionary + + >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) + >>> d.add("A", "2", combine=True) + >>> d.add("B", "bar") + >>> list(d.items()) + [ + ('A', '1, 2'), + ('B', 'foo'), + ('B', 'bar'), + ] + + This class conforms to the interface required by the MutableMapping ABC while + also giving us the nonstandard iteration behavior we want; items with duplicate + keys, ordered by time of first insertion. + """ + + _headers: HTTPHeaderDict + + def __init__(self, headers: HTTPHeaderDict) -> None: + self._headers = headers + + def __len__(self) -> int: + return len(list(self._headers.iteritems())) + + def __iter__(self) -> typing.Iterator[tuple[str, str]]: + return self._headers.iteritems() + + def __contains__(self, item: object) -> bool: + if isinstance(item, tuple) and len(item) == 2: + passed_key, passed_val = item + if isinstance(passed_key, str) and isinstance(passed_val, str): + return self._headers._has_value_for_header(passed_key, passed_val) + return False + + +class HTTPHeaderDict(typing.MutableMapping[str, str]): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 7230. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + """ + + _container: typing.MutableMapping[str, list[str]] + + def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): + super().__init__() + self._container = {} # 'dict' is insert-ordered + if headers is not None: + if isinstance(headers, HTTPHeaderDict): + self._copy_from(headers) + else: + self.extend(headers) + if kwargs: + self.extend(kwargs) + + def __setitem__(self, key: str, val: str) -> None: + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + self._container[key.lower()] = [key, val] + + def __getitem__(self, key: str) -> str: + if isinstance(key, bytes): + key = key.decode("latin-1") + val = self._container[key.lower()] + return ", ".join(val[1:]) + + def __delitem__(self, key: str) -> None: + if isinstance(key, bytes): + key = key.decode("latin-1") + del self._container[key.lower()] + + def __contains__(self, key: object) -> bool: + if isinstance(key, bytes): + key = key.decode("latin-1") + if isinstance(key, str): + return key.lower() in self._container + return False + + def setdefault(self, key: str, default: str = "") -> str: + return super().setdefault(key, default) + + def __eq__(self, other: object) -> bool: + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return False + else: + other_as_http_header_dict = type(self)(maybe_constructable) + + return {k.lower(): v for k, v in self.itermerged()} == { + k.lower(): v for k, v in other_as_http_header_dict.itermerged() + } + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __len__(self) -> int: + return len(self._container) + + def __iter__(self) -> typing.Iterator[str]: + # Only provide the originally cased names + for vals in self._container.values(): + yield vals[0] + + def discard(self, key: str) -> None: + try: + del self[key] + except KeyError: + pass + + def add(self, key: str, val: str, *, combine: bool = False) -> None: + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + If this is called with combine=True, instead of adding a new header value + as a distinct item during iteration, this will instead append the value to + any existing header value with a comma. If no existing header value exists + for the key, then the value will simply be added, ignoring the combine parameter. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + >>> list(headers.items()) + [('foo', 'bar'), ('foo', 'baz')] + >>> headers.add('foo', 'quz', combine=True) + >>> list(headers.items()) + [('foo', 'bar, baz, quz')] + """ + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + key_lower = key.lower() + new_vals = [key, val] + # Keep the common case aka no item present as fast as possible + vals = self._container.setdefault(key_lower, new_vals) + if new_vals is not vals: + # if there are values here, then there is at least the initial + # key/value pair + assert len(vals) >= 2 + if combine: + vals[-1] = vals[-1] + ", " + val + else: + vals.append(val) + + def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: + """Generic import function for any type of header-like object. + Adapted version of MutableMapping.update in order to insert items + with self.add instead of self.__setitem__ + """ + if len(args) > 1: + raise TypeError( + f"extend() takes at most 1 positional arguments ({len(args)} given)" + ) + other = args[0] if len(args) >= 1 else () + + if isinstance(other, HTTPHeaderDict): + for key, val in other.iteritems(): + self.add(key, val) + elif isinstance(other, typing.Mapping): + for key, val in other.items(): + self.add(key, val) + elif isinstance(other, typing.Iterable): + for key, value in other: + self.add(key, value) + elif hasattr(other, "keys") and hasattr(other, "__getitem__"): + # THIS IS NOT A TYPESAFE BRANCH + # In this branch, the object has a `keys` attr but is not a Mapping or any of + # the other types indicated in the method signature. We do some stuff with + # it as though it partially implements the Mapping interface, but we're not + # doing that stuff safely AT ALL. + for key in other.keys(): + self.add(key, other[key]) + + for key, value in kwargs.items(): + self.add(key, value) + + @typing.overload + def getlist(self, key: str) -> list[str]: ... + + @typing.overload + def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... + + def getlist( + self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed + ) -> list[str] | _DT: + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + if isinstance(key, bytes): + key = key.decode("latin-1") + try: + vals = self._container[key.lower()] + except KeyError: + if default is _Sentinel.not_passed: + # _DT is unbound; empty list is instance of List[str] + return [] + # _DT is bound; default is instance of _DT + return default + else: + # _DT may or may not be bound; vals[1:] is instance of List[str], which + # meets our external interface requirement of `Union[List[str], _DT]`. + return vals[1:] + + def _prepare_for_method_change(self) -> Self: + """ + Remove content-specific header fields before changing the request + method to GET or HEAD according to RFC 9110, Section 15.4. + """ + content_specific_headers = [ + "Content-Encoding", + "Content-Language", + "Content-Location", + "Content-Type", + "Content-Length", + "Digest", + "Last-Modified", + ] + for header in content_specific_headers: + self.discard(header) + return self + + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist + iget = getlist + + # Backwards compatibility for http.cookiejar + get_all = getlist + + def __repr__(self) -> str: + return f"{type(self).__name__}({dict(self.itermerged())})" + + def _copy_from(self, other: HTTPHeaderDict) -> None: + for key in other: + val = other.getlist(key) + self._container[key.lower()] = [key, *val] + + def copy(self) -> Self: + clone = type(self)() + clone._copy_from(self) + return clone + + def iteritems(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all header lines, including duplicate ones.""" + for key in self: + vals = self._container[key.lower()] + for val in vals[1:]: + yield vals[0], val + + def itermerged(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all headers, merging duplicate ones together.""" + for key in self: + val = self._container[key.lower()] + yield val[0], ", ".join(val[1:]) + + def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] + return HTTPHeaderDictItemView(self) + + def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: + if header_name in self: + return potential_value in self._container[header_name.lower()][1:] + return False + + def __ior__(self, other: object) -> HTTPHeaderDict: + # Supports extending a header dict in-place using operator |= + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + self.extend(maybe_constructable) + return self + + def __or__(self, other: object) -> Self: + # Supports merging header dicts using operator | + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = self.copy() + result.extend(maybe_constructable) + return result + + def __ror__(self, other: object) -> Self: + # Supports merging header dicts using operator | when other is on left side + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = type(self)(maybe_constructable) + result.extend(self) + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py new file mode 100644 index 0000000000..297c271bf4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import json as _json +import typing +from urllib.parse import urlencode + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .response import BaseHTTPResponse + +__all__ = ["RequestMethods"] + +_TYPE_ENCODE_URL_FIELDS = typing.Union[ + typing.Sequence[tuple[str, typing.Union[str, bytes]]], + typing.Mapping[str, typing.Union[str, bytes]], +] + + +class RequestMethods: + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`urllib3.HTTPConnectionPool` and + :class:`urllib3.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are + encoded in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} + + def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: + self.headers = headers or {} + + def urlopen( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **kw: typing.Any, + ) -> BaseHTTPResponse: # Abstract + raise NotImplementedError( + "Classes extending RequestMethods must implement " + "their own ``urlopen`` method." + ) + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + json: typing.Any | None = None, + **urlopen_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the + option to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the URL or request body, depending on ``method``. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + method = method.upper() + + if json is not None and body is not None: + raise TypeError( + "request got values for both 'body' and 'json' parameters which are mutually exclusive" + ) + + if json is not None: + if headers is None: + headers = self.headers + + if not ("content-type" in map(str.lower, headers.keys())): + headers = HTTPHeaderDict(headers) + headers["Content-Type"] = "application/json" + + body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + + if body is not None: + urlopen_kw["body"] = body + + if method in self._encode_url_methods: + return self.request_encode_url( + method, + url, + fields=fields, # type: ignore[arg-type] + headers=headers, + **urlopen_kw, + ) + else: + return self.request_encode_body( + method, url, fields=fields, headers=headers, **urlopen_kw + ) + + def request_encode_url( + self, + method: str, + url: str, + fields: _TYPE_ENCODE_URL_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the URL. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": headers} + extra_kw.update(urlopen_kw) + + if fields: + url += "?" + urlencode(fields) + + return self.urlopen(method, url, **extra_kw) + + def request_encode_body( + self, + method: str, + url: str, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :func:`urllib3.encode_multipart_formdata` is used to encode + the payload with the appropriate content type. Otherwise + :func:`urllib.parse.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request + signing, such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example:: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimic behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will + be overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param encode_multipart: + If True, encode the ``fields`` using the multipart/form-data MIME + format. + + :param multipart_boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} + body: bytes | str + + if fields: + if "body" in urlopen_kw: + raise TypeError( + "request got values for both 'fields' and 'body', can only specify one." + ) + + if encode_multipart: + body, content_type = encode_multipart_formdata( + fields, boundary=multipart_boundary + ) + else: + body, content_type = ( + urlencode(fields), # type: ignore[arg-type] + "application/x-www-form-urlencoded", + ) + + extra_kw["body"] = body + extra_kw["headers"].setdefault("Content-Type", content_type) + + extra_kw.update(urlopen_kw) + + return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py new file mode 100644 index 0000000000..bfed03985b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '2.7.0' +__version_tuple__ = version_tuple = (2, 7, 0) + +__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py new file mode 100644 index 0000000000..84e1dab945 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py @@ -0,0 +1,1099 @@ +from __future__ import annotations + +import datetime +import http.client +import logging +import os +import re +import socket +import sys +import threading +import typing +import warnings +from http.client import HTTPConnection as _HTTPConnection +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from .response import HTTPResponse + from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT + from .util.ssltransport import SSLTransport + +from ._collections import HTTPHeaderDict +from .http2 import probe as http2_probe +from .util.response import assert_header_parsing +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout +from .util.util import to_str +from .util.wait import wait_for_read + +try: # Compiled with SSL? + import ssl + + BaseSSLError = ssl.SSLError +except (ImportError, AttributeError): + ssl = None # type: ignore[assignment] + + class BaseSSLError(BaseException): # type: ignore[no-redef] + pass + + +from ._base_connection import _TYPE_BODY +from ._base_connection import ProxyConfig as ProxyConfig +from ._base_connection import _ResponseOptions as _ResponseOptions +from ._version import __version__ +from .exceptions import ( + ConnectTimeoutError, + HeaderParsingError, + NameResolutionError, + NewConnectionError, + ProxyError, + SystemTimeWarning, +) +from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ +from .util.request import body_to_chunks +from .util.ssl_ import assert_fingerprint as _assert_fingerprint +from .util.ssl_ import ( + create_urllib3_context, + is_ipaddress, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .util.ssl_match_hostname import CertificateError, match_hostname +from .util.url import Url + +# Not a no-op, we're adding this to the namespace so it can be imported. +ConnectionError = ConnectionError +BrokenPipeError = BrokenPipeError + + +log = logging.getLogger(__name__) + +port_by_scheme = {"http": 80, "https": 443} + +# When it comes time to update this value as a part of regular maintenance +# (ie test_recent_date is failing) update it to ~6 months before the current date. +RECENT_DATE = datetime.date(2025, 1, 1) + +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") + + +class HTTPConnection(_HTTPConnection): + """ + Based on :class:`http.client.HTTPConnection` but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + + Additional keyword parameters are used to configure attributes of the connection. + Accepted parameters include: + + - ``source_address``: Set the source address for the current connection. + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass: + + .. code-block:: python + + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + """ + + default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] + + #: Disable Nagle's algorithm by default. + #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` + default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + ] + + #: Whether this connection verifies the host's certificate. + is_verified: bool = False + + #: Whether this proxy connection verified the proxy host's certificate. + # If no proxy is currently connected to the value will be ``None``. + proxy_is_verified: bool | None = None + + blocksize: int + source_address: tuple[str, int] | None + socket_options: connection._TYPE_SOCKET_OPTIONS | None + + _has_connected_to_proxy: bool + _response_options: _ResponseOptions | None + _tunnel_host: str | None + _tunnel_port: int | None + _tunnel_scheme: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + super().__init__( + host=host, + port=port, + timeout=Timeout.resolve_default_timeout(timeout), + source_address=source_address, + blocksize=blocksize, + ) + self.socket_options = socket_options + self.proxy = proxy + self.proxy_config = proxy_config + + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host: str | None = None + self._tunnel_port: int | None = None + self._tunnel_scheme: str | None = None + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __repr__(self) -> str: + return f"<{self} at {id(self):#x}>" + + @property + def host(self) -> str: + """ + Getter method to remove any trailing dots that indicate the hostname is an FQDN. + + In general, SSL certificates don't include the trailing dot indicating a + fully-qualified domain name, and thus, they don't validate properly when + checked against a domain name that includes the dot. In addition, some + servers may not expect to receive the trailing dot when provided. + + However, the hostname with trailing dot is critical to DNS resolution; doing a + lookup with the trailing dot will properly only resolve the appropriate FQDN, + whereas a lookup without a trailing dot will search the system's search domain + list. Thus, it's important to keep the original host around for use only in + those cases where it's appropriate (i.e., when doing DNS lookup to establish the + actual TCP connection across which we're going to send HTTP requests). + """ + return self._dns_host.rstrip(".") + + @host.setter + def host(self, value: str) -> None: + """ + Setter for the `host` property. + + We assume that only urllib3 uses the _dns_host attribute; httplib itself + only uses `host`, and it seems reasonable that other libraries follow suit. + """ + self._dns_host = value + + def _new_conn(self) -> socket.socket: + """Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + try: + sock = connection.create_connection( + (self._dns_host, self.port), + self.timeout, + source_address=self.source_address, + socket_options=self.socket_options, + ) + except socket.gaierror as e: + raise NameResolutionError(self.host, self, e) from e + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except OSError as e: + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + sys.audit("http.client.connect", self, self.host, self.port) + + return sock + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + if scheme not in ("http", "https"): + raise ValueError( + f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" + ) + super().set_tunnel(host, port=port, headers=headers) + self._tunnel_scheme = scheme + + if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): + # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. + # When using connection_from_host, host will come without brackets. + def _wrap_ipv6(self, ip: bytes) -> bytes: + if b":" in ip and ip[0] != b"["[0]: + return b"[" + ip + b"]" + return ip + + if sys.version_info < (3, 11, 9): + # `_tunnel` copied from 3.11.13 backporting + # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 + # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 + def _tunnel(self) -> None: + _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] + connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + while True: + line = response.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise http.client.LineTooLong("header line") + if not line: + # for sites which EOF without sending a trailer + break + if line in (b"\r\n", b"\n", b""): + break + + if self.debuglevel > 0: + print("header:", line.decode()) + finally: + response.close() + + elif (3, 12) <= sys.version_info < (3, 12, 3): + # `_tunnel` copied from 3.12.11 backporting + # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 + def _tunnel(self) -> None: # noqa: F811 + connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] + + if self.debuglevel > 0: + for header in self._raw_proxy_headers: + print("header:", header.decode()) + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + + finally: + response.close() + + def connect(self) -> None: + self.sock = self._new_conn() + if self._tunnel_host: + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + if self._has_connected_to_proxy: + self.proxy_is_verified = False + + @property + def is_closed(self) -> bool: + return self.sock is None + + @property + def is_connected(self) -> bool: + if self.sock is None: + return False + return not wait_for_read(self.sock, timeout=0.0) + + @property + def has_connected_to_proxy(self) -> bool: + return self._has_connected_to_proxy + + @property + def proxy_is_forwarding(self) -> bool: + """ + Return True if a forwarding proxy is configured, else return False + """ + return bool(self.proxy) and self._tunnel_host is None + + @property + def proxy_is_tunneling(self) -> bool: + """ + Return True if a tunneling proxy is configured, else return False + """ + return self._tunnel_host is not None + + def close(self) -> None: + try: + super().close() + finally: + # Reset all stateful properties so connection + # can be re-used without leaking prior configs. + self.sock = None + self.is_verified = False + self.proxy_is_verified = None + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_scheme = None + + def putrequest( + self, + method: str, + url: str, + skip_host: bool = False, + skip_accept_encoding: bool = False, + ) -> None: + """""" + # Empty docstring because the indentation of CPython's implementation + # is broken but we don't want this method in our documentation. + match = _CONTAINS_CONTROL_CHAR_RE.search(method) + if match: + raise ValueError( + f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" + ) + + return super().putrequest( + method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding + ) + + def putheader(self, header: str, *values: str) -> None: # type: ignore[override] + """""" + if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): + super().putheader(header, *values) + elif to_str(header.lower()) not in SKIPPABLE_HEADERS: + skippable_headers = "', '".join( + [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] + ) + raise ValueError( + f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" + ) + + # `request` method's signature intentionally violates LSP. + # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + # Update the inner socket's timeout value to send the request. + # This only triggers if the connection is re-used. + if self.sock is not None: + self.sock.settimeout(self.timeout) + + # Store these values to be fed into the HTTPResponse + # object later. TODO: Remove this in favor of a real + # HTTP lifecycle mechanism. + + # We have to store these before we call .request() + # because sometimes we can still salvage a response + # off the wire even if we aren't able to completely + # send the request body. + self._response_options = _ResponseOptions( + request_method=method, + request_url=url, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + if headers is None: + headers = {} + header_keys = frozenset(to_str(k.lower()) for k in headers) + skip_accept_encoding = "accept-encoding" in header_keys + skip_host = "host" in header_keys + self.putrequest( + method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host + ) + + # Transform the body into an iterable of sendall()-able chunks + # and detect if an explicit Content-Length is doable. + chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) + chunks = chunks_and_cl.chunks + content_length = chunks_and_cl.content_length + + # When chunked is explicit set to 'True' we respect that. + if chunked: + if "transfer-encoding" not in header_keys: + self.putheader("Transfer-Encoding", "chunked") + else: + # Detect whether a framing mechanism is already in use. If so + # we respect that value, otherwise we pick chunked vs content-length + # depending on the type of 'body'. + if "content-length" in header_keys: + chunked = False + elif "transfer-encoding" in header_keys: + chunked = True + + # Otherwise we go off the recommendation of 'body_to_chunks()'. + else: + chunked = False + if content_length is None: + if chunks is not None: + chunked = True + self.putheader("Transfer-Encoding", "chunked") + else: + self.putheader("Content-Length", str(content_length)) + + # Now that framing headers are out of the way we send all the other headers. + if "user-agent" not in header_keys: + self.putheader("User-Agent", _get_default_user_agent()) + for header, value in headers.items(): + self.putheader(header, value) + self.endheaders() + + # If we're given a body we start sending that in chunks. + if chunks is not None: + for chunk in chunks: + # Sending empty chunks isn't allowed for TE: chunked + # as it indicates the end of the body. + if not chunk: + continue + if isinstance(chunk, str): + chunk = chunk.encode("utf-8") + if chunked: + self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) + else: + self.send(chunk) + + # Regardless of whether we have a body or not, if we're in + # chunked mode we want to send an explicit empty chunk. + if chunked: + self.send(b"0\r\n\r\n") + + def request_chunked( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + ) -> None: + """ + Alternative to the common request method, which sends the + body with chunked encoding and not as one block + """ + warnings.warn( + "HTTPConnection.request_chunked() is deprecated and will be removed " + "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", + category=FutureWarning, + stacklevel=2, + ) + self.request(method, url, body=body, headers=headers, chunked=True) + + def getresponse( # type: ignore[override] + self, + ) -> HTTPResponse: + """ + Get the response from the server. + + If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. + + If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. + """ + # Raise the same error as http.client.HTTPConnection + if self._response_options is None: + raise ResponseNotReady() + + # Reset this attribute for being used again. + resp_options = self._response_options + self._response_options = None + + # Since the connection's timeout value may have been updated + # we need to set the timeout on the socket. + self.sock.settimeout(self.timeout) + + # This is needed here to avoid circular import errors + from .response import HTTPResponse + + # Save a reference to the shutdown function before ownership is passed + # to httplib_response + # TODO should we implement it everywhere? + _shutdown = getattr(self.sock, "shutdown", None) + + # Get the response from http.client.HTTPConnection + httplib_response = super().getresponse() + + try: + assert_header_parsing(httplib_response.msg) + except (HeaderParsingError, TypeError) as hpe: + log.warning( + "Failed to parse headers (url=%s): %s", + _url_from_connection(self, resp_options.request_url), + hpe, + exc_info=True, + ) + + headers = HTTPHeaderDict(httplib_response.msg.items()) + + response = HTTPResponse( + body=httplib_response, + headers=headers, + status=httplib_response.status, + version=httplib_response.version, + version_string=getattr(self, "_http_vsn_str", "HTTP/?"), + reason=httplib_response.reason, + preload_content=resp_options.preload_content, + decode_content=resp_options.decode_content, + original_response=httplib_response, + enforce_content_length=resp_options.enforce_content_length, + request_method=resp_options.request_method, + request_url=resp_options.request_url, + sock_shutdown=_shutdown, + ) + return response + + +class HTTPSConnection(HTTPConnection): + """ + Many of the parameters to this constructor are passed to the underlying SSL + socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. + """ + + default_port = port_by_scheme["https"] # type: ignore[misc] + + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_fingerprint: str | None = None + _connect_callback: typing.Callable[..., None] | None = None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + # cert_reqs depends on ssl_context so calculate last. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + self.cert_reqs = cert_reqs + self._connect_callback = None + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + """ + This method should only be called once, before the connection is used. + """ + warnings.warn( + "HTTPSConnection.set_cert() is deprecated and will be removed " + "in urllib3 v3.0. Instead provide the parameters to the " + "HTTPSConnection constructor.", + category=FutureWarning, + stacklevel=2, + ) + + # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also + # have an SSLContext object in which case we'll use its verify_mode. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + def connect(self) -> None: + # Today we don't need to be doing this step before the /actual/ socket + # connection, however in the future we'll need to decide whether to + # create a new socket or re-use an existing "shared" socket as a part + # of the HTTP/2 handshake dance. + if self._tunnel_host is not None and self._tunnel_port is not None: + probe_http2_host = self._tunnel_host + probe_http2_port = self._tunnel_port + else: + probe_http2_host = self.host + probe_http2_port = self.port + + # Check if the target origin supports HTTP/2. + # If the value comes back as 'None' it means that the current thread + # is probing for HTTP/2 support. Otherwise, we're waiting for another + # probe to complete, or we get a value right away. + target_supports_http2: bool | None + if "h2" in ssl_.ALPN_PROTOCOLS: + target_supports_http2 = http2_probe.acquire_and_get( + host=probe_http2_host, port=probe_http2_port + ) + else: + # If HTTP/2 isn't going to be offered it doesn't matter if + # the target supports HTTP/2. Don't want to make a probe. + target_supports_http2 = False + + if self._connect_callback is not None: + self._connect_callback( + "before connect", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + try: + sock: socket.socket | ssl.SSLSocket + self.sock = sock = self._new_conn() + server_hostname: str = self.host + tls_in_tls = False + + # Do we need to establish a tunnel? + if self.proxy_is_tunneling: + # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. + if self._tunnel_scheme == "https": + # _connect_tls_proxy will verify and assign proxy_is_verified + self.sock = sock = self._connect_tls_proxy(self.host, sock) + tls_in_tls = True + elif self._tunnel_scheme == "http": + self.proxy_is_verified = False + + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + self._tunnel() + # Override the host with the one we're requesting data from. + server_hostname = typing.cast(str, self._tunnel_host) + + if self.server_hostname is not None: + server_hostname = self.server_hostname + + is_time_off = datetime.date.today() < RECENT_DATE + if is_time_off: + warnings.warn( + ( + f"System time is way off (before {RECENT_DATE}). This will probably " + "lead to SSL verification errors" + ), + SystemTimeWarning, + ) + + # Remove trailing '.' from fqdn hostnames to allow certificate validation + server_hostname_rm_dot = server_hostname.rstrip(".") + + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock=sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + server_hostname=server_hostname_rm_dot, + ssl_context=self.ssl_context, + tls_in_tls=tls_in_tls, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ) + self.sock = sock_and_verified.socket + + # If an error occurs during connection/handshake we may need to release + # our lock so another connection can probe the origin. + except BaseException: + if self._connect_callback is not None: + self._connect_callback( + "after connect failure", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + if target_supports_http2 is None: + http2_probe.set_and_release( + host=probe_http2_host, port=probe_http2_port, supports_http2=None + ) + raise + + # If this connection doesn't know if the origin supports HTTP/2 + # we report back to the HTTP/2 probe our result. + if target_supports_http2 is None: + supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" + http2_probe.set_and_release( + host=probe_http2_host, + port=probe_http2_port, + supports_http2=supports_http2, + ) + + # Forwarding proxies can never have a verified target since + # the proxy is the one doing the verification. Should instead + # use a CONNECT tunnel in order to verify the target. + # See: https://github.com/urllib3/urllib3/issues/3267. + if self.proxy_is_forwarding: + self.is_verified = False + else: + self.is_verified = sock_and_verified.is_verified + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + # Set `self.proxy_is_verified` unless it's already set while + # establishing a tunnel. + if self._has_connected_to_proxy and self.proxy_is_verified is None: + self.proxy_is_verified = sock_and_verified.is_verified + + def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: + """ + Establish a TLS connection to the proxy using the provided SSL context. + """ + # `_connect_tls_proxy` is called when self._tunnel_host is truthy. + proxy_config = typing.cast(ProxyConfig, self.proxy_config) + ssl_context = proxy_config.ssl_context + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + server_hostname=hostname, + ssl_context=ssl_context, + assert_hostname=proxy_config.assert_hostname, + assert_fingerprint=proxy_config.assert_fingerprint, + # Features that aren't implemented for proxies yet: + cert_file=None, + key_file=None, + key_password=None, + tls_in_tls=False, + ) + self.proxy_is_verified = sock_and_verified.is_verified + return sock_and_verified.socket # type: ignore[return-value] + + +class _WrappedAndVerifiedSocket(typing.NamedTuple): + """ + Wrapped socket and whether the connection is + verified after the TLS handshake + """ + + socket: ssl.SSLSocket | SSLTransport + is_verified: bool + + +def _ssl_wrap_socket_and_match_hostname( + sock: socket.socket, + *, + cert_reqs: None | str | int, + ssl_version: None | str | int, + ssl_minimum_version: int | None, + ssl_maximum_version: int | None, + cert_file: str | None, + key_file: str | None, + key_password: str | None, + ca_certs: str | None, + ca_cert_dir: str | None, + ca_cert_data: None | str | bytes, + assert_hostname: None | str | typing.Literal[False], + assert_fingerprint: str | None, + server_hostname: str | None, + ssl_context: ssl.SSLContext | None, + tls_in_tls: bool = False, +) -> _WrappedAndVerifiedSocket: + """Logic for constructing an SSLContext from all TLS parameters, passing + that down into ssl_wrap_socket, and then doing certificate verification + either via hostname or fingerprint. This function exists to guarantee + that both proxies and targets have the same behavior when connecting via TLS. + """ + default_ssl_context = False + if ssl_context is None: + default_ssl_context = True + context = create_urllib3_context( + ssl_version=resolve_ssl_version(ssl_version), + ssl_minimum_version=ssl_minimum_version, + ssl_maximum_version=ssl_maximum_version, + cert_reqs=resolve_cert_reqs(cert_reqs), + ) + else: + context = ssl_context + + context.verify_mode = resolve_cert_reqs(cert_reqs) + + # In some cases, we want to verify hostnames ourselves + if ( + # `ssl` can't verify fingerprints or alternate hostnames + assert_fingerprint + or assert_hostname + # assert_hostname can be set to False to disable hostname checking + or assert_hostname is False + # We still support OpenSSL 1.0.2, which prevents us from verifying + # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 + or ssl_.IS_PYOPENSSL + or not ssl_.HAS_NEVER_CHECK_COMMON_NAME + ): + context.check_hostname = False + + # Try to load OS default certs if none are given. We need to do the hasattr() check + # for custom pyOpenSSL SSLContext objects because they don't support + # load_default_certs(). + if ( + not ca_certs + and not ca_cert_dir + and not ca_cert_data + and default_ssl_context + and hasattr(context, "load_default_certs") + ): + context.load_default_certs() + + # Ensure that IPv6 addresses are in the proper format and don't have a + # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses + # and interprets them as DNS hostnames. + if server_hostname is not None: + normalized = server_hostname.strip("[]") + if "%" in normalized: + normalized = normalized[: normalized.rfind("%")] + if is_ipaddress(normalized): + server_hostname = normalized + + ssl_sock = ssl_wrap_socket( + sock=sock, + keyfile=key_file, + certfile=cert_file, + key_password=key_password, + ca_certs=ca_certs, + ca_cert_dir=ca_cert_dir, + ca_cert_data=ca_cert_data, + server_hostname=server_hostname, + ssl_context=context, + tls_in_tls=tls_in_tls, + ) + + try: + if assert_fingerprint: + _assert_fingerprint( + ssl_sock.getpeercert(binary_form=True), assert_fingerprint + ) + elif ( + context.verify_mode != ssl.CERT_NONE + and not context.check_hostname + and assert_hostname is not False + ): + cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] + + # Need to signal to our match_hostname whether to use 'commonName' or not. + # If we're using our own constructed SSLContext we explicitly set 'False' + # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. + if default_ssl_context: + hostname_checks_common_name = False + else: + hostname_checks_common_name = ( + getattr(context, "hostname_checks_common_name", False) or False + ) + + _match_hostname( + cert, + assert_hostname or server_hostname, # type: ignore[arg-type] + hostname_checks_common_name, + ) + + return _WrappedAndVerifiedSocket( + socket=ssl_sock, + is_verified=context.verify_mode == ssl.CERT_REQUIRED + or bool(assert_fingerprint), + ) + except BaseException: + ssl_sock.close() + raise + + +def _match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + asserted_hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + # Our upstream implementation of ssl.match_hostname() + # only applies this normalization to IP addresses so it doesn't + # match DNS SANs so we do the same thing! + stripped_hostname = asserted_hostname.strip("[]") + if is_ipaddress(stripped_hostname): + asserted_hostname = stripped_hostname + + try: + match_hostname(cert, asserted_hostname, hostname_checks_common_name) + except CertificateError as e: + log.warning( + "Certificate did not match expected hostname: %s. Certificate: %s", + asserted_hostname, + cert, + ) + # Add cert to exception and reraise so client code can inspect + # the cert when catching the exception, if they want to + e._peer_cert = cert # type: ignore[attr-defined] + raise + + +def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: + # Look for the phrase 'wrong version number', if found + # then we should warn the user that we're very sure that + # this proxy is HTTP-only and they have a configuration issue. + error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) + is_likely_http_proxy = ( + "wrong version number" in error_normalized + or "unknown protocol" in error_normalized + or "record layer failure" in error_normalized + ) + http_proxy_warning = ( + ". Your proxy appears to only use HTTP and not HTTPS, " + "try changing your proxy URL to be HTTP. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#https-proxy-error-http-proxy" + ) + new_err = ProxyError( + f"Unable to connect to proxy" + f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", + err, + ) + new_err.__cause__ = err + return new_err + + +def _get_default_user_agent() -> str: + return f"python-urllib3/{__version__}" + + +class DummyConnection: + """Used to detect a failed ConnectionCls import.""" + + +if not ssl: + HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 + + +VerifiedHTTPSConnection = HTTPSConnection + + +def _url_from_connection( + conn: HTTPConnection | HTTPSConnection, path: str | None = None +) -> str: + """Returns the URL from a given connection. This is mainly used for testing and logging.""" + + scheme = "https" if isinstance(conn, HTTPSConnection) else "http" + + return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py new file mode 100644 index 0000000000..70fbc5e725 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py @@ -0,0 +1,1191 @@ +from __future__ import annotations + +import errno +import logging +import queue +import sys +import typing +import warnings +import weakref +from socket import timeout as SocketTimeout +from types import TracebackType + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._request_methods import RequestMethods +from .connection import ( + BaseSSLError, + BrokenPipeError, + DummyConnection, + HTTPConnection, + HTTPException, + HTTPSConnection, + ProxyConfig, + _wrap_proxy_error, +) +from .connection import port_by_scheme as port_by_scheme +from .exceptions import ( + ClosedPoolError, + EmptyPoolError, + FullPoolError, + HostChangedError, + InsecureRequestWarning, + LocationValueError, + MaxRetryError, + NewConnectionError, + ProtocolError, + ProxyError, + ReadTimeoutError, + SSLError, + TimeoutError, +) +from .response import BaseHTTPResponse +from .util.connection import is_connection_dropped +from .util.proxy import connection_requires_http_tunnel +from .util.request import _TYPE_BODY_POSITION, set_file_position +from .util.retry import Retry +from .util.ssl_match_hostname import CertificateError +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout +from .util.url import Url, _encode_target +from .util.url import _normalize_host as normalize_host +from .util.url import parse_url +from .util.util import to_str + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + + from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + +_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] + + +# Pool objects +class ConnectionPool: + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + + .. note:: + ConnectionPool.urlopen() does not normalize or percent-encode target URIs + which is useful if your target server doesn't support percent-encoded + target URIs. + """ + + scheme: str | None = None + QueueCls = queue.LifoQueue + + def __init__(self, host: str, port: int | None = None) -> None: + if not host: + raise LocationValueError("No host specified.") + + self.host = _normalize_host(host, scheme=self.scheme) + self.port = port + + # This property uses 'normalize_host()' (not '_normalize_host()') + # to avoid removing square braces around IPv6 addresses. + # This value is sent to `HTTPConnection.set_tunnel()` if called + # because square braces are required for HTTP CONNECT tunneling. + self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.close() + # Return False to re-raise any potential exceptions + return False + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} + + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`http.client.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`http.client.HTTPConnection`. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to False, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param retries: + Retry configuration to use by default with requests in this pool. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.ProxyManager` + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.ProxyManager` + + :param \\**conn_kw: + Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, + :class:`urllib3.connection.HTTPSConnection` instances. + """ + + scheme = "http" + ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + _proxy_config: ProxyConfig | None = None, + **conn_kw: typing.Any, + ): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + if retries is None: + retries = Retry.DEFAULT + + self.timeout = timeout + self.retries = retries + + self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + self.proxy_config = _proxy_config + + # Fill the queue up so that doing get() on it will block properly + for _ in range(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + self.conn_kw = conn_kw + + if self.proxy: + # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. + # Defaulting `socket_options` to an empty list avoids it defaulting to + # ``HTTPConnection.default_socket_options``. + self.conn_kw.setdefault("socket_options", []) + + self.conn_kw["proxy"] = self.proxy + self.conn_kw["proxy_config"] = self.proxy_config + + # Do not pass 'self' as callback to 'finalize'. + # Then the 'finalize' would keep an endless living (leak) to self. + # By just passing a reference to the pool allows the garbage collector + # to free self if nobody else has a reference to it. + pool = self.pool + + # Close all the HTTPConnections in the pool before the + # HTTPConnectionPool object is garbage collected. + weakref.finalize(self, _close_pool_connections, pool) + + def _new_conn(self) -> BaseHTTPConnection: + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTP connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "80", + ) + + conn = self.ConnectionCls( + host=self.host, + port=self.port, + timeout=self.timeout.connect_timeout, + **self.conn_kw, + ) + return conn + + def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + + if self.pool is None: + raise ClosedPoolError(self, "Pool is closed.") + + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: + + except queue.Empty: + if self.block: + raise EmptyPoolError( + self, + "Pool is empty and a new connection can't be opened due to blocking mode.", + ) from None + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.debug("Resetting dropped connection: %s", self.host) + conn.close() + + return conn or self._new_conn() + + def _put_conn(self, conn: BaseHTTPConnection | None) -> None: + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + if self.pool is not None: + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except queue.Full: + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + if self.block: + # This should never happen if you got the conn from self._get_conn + raise FullPoolError( + self, + "Pool reached maximum size and no more connections are allowed.", + ) from None + + log.warning( + "Connection pool is full, discarding connection: %s. Connection pool size: %s", + self.host, + self.pool.qsize(), + ) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + + def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: + # Nothing to do for HTTP connections. + pass + + def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: + """Helper that always returns a :class:`urllib3.util.Timeout`""" + if timeout is _DEFAULT_TIMEOUT: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _raise_timeout( + self, + err: BaseSSLError | OSError | SocketTimeout, + url: str, + timeout_value: _TYPE_TIMEOUT | None, + ) -> None: + """Is the error actually a timeout? Will raise a ReadTimeout or pass""" + + if isinstance(err, SocketTimeout): + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + # See the above comment about EAGAIN in Python 3. + if hasattr(err, "errno") and err.errno in _blocking_errnos: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + def _make_request( + self, + conn: BaseHTTPConnection, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | None = None, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + chunked: bool = False, + response_conn: BaseHTTPConnection | None = None, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> BaseHTTPResponse: + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param response_conn: + Set this to ``None`` if you will handle releasing the connection or + set the connection to have the response release it. + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + timeout_obj.start_connect() + conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) + + try: + # Trigger any extra validation we need to do. + try: + self._validate_conn(conn) + except (SocketTimeout, BaseSSLError) as e: + self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) + raise + + # _validate_conn() starts the connection to an HTTPS proxy + # so we need to wrap errors with 'ProxyError' here too. + except ( + OSError, + NewConnectionError, + TimeoutError, + BaseSSLError, + CertificateError, + SSLError, + ) as e: + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + # If the connection didn't successfully connect to it's proxy + # then there + if isinstance( + new_e, (OSError, NewConnectionError, TimeoutError, SSLError) + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + raise new_e + + # conn.request() calls http.client.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + try: + conn.request( + method, + url, + body=body, + headers=headers, + chunked=chunked, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + # We are swallowing BrokenPipeError (errno.EPIPE) since the server is + # legitimately able to close the connection after sending a valid response. + # With this behaviour, the received response is still readable. + except BrokenPipeError: + pass + except OSError as e: + # MacOS/Linux + # EPROTOTYPE and ECONNRESET are needed on macOS + # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ + # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. + if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: + raise + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + if not conn.is_closed: + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={read_timeout})" + ) + conn.timeout = read_timeout + + # Receive the response from the server + try: + response = conn.getresponse() + except (BaseSSLError, OSError) as e: + self._raise_timeout(err=e, url=url, timeout_value=read_timeout) + raise + + # Set properties that are used by the pooling layer. + response.retries = retries + response._connection = response_conn # type: ignore[attr-defined] + response._pool = self # type: ignore[attr-defined] + + log.debug( + '%s://%s:%s "%s %s %s" %s %s', + self.scheme, + self.host, + self.port, + method, + url, + response.version_string, + response.status, + response.length_remaining, + ) + + return response + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + if self.pool is None: + return + # Disable access to the pool + old_pool, self.pool = self.pool, None + + # Close all the HTTPConnections in the pool. + _close_pool_connections(old_pool) + + def is_same_host(self, url: str) -> bool: + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith("/"): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + if host is not None: + host = _normalize_host(host, scheme=scheme) + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + redirect: bool = True, + assert_same_host: bool = True, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + pool_timeout: int | None = None, + release_conn: bool | None = None, + chunked: bool = False, + body_pos: _TYPE_BODY_POSITION | None = None, + preload_content: bool = True, + decode_content: bool = True, + **response_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method + such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When ``False``, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of ``preload_content`` + which defaults to ``True``. + + :param bool chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param int body_pos: + Position to seek to in file-like body in the event of a retry or + redirect. Typically this won't need to be set because urllib3 will + auto-populate the value when needed. + """ + # Ensure that the URL we're connecting to is properly encoded + if url.startswith("/"): + # URLs starting with / are inherently schemeless. + url = to_str(_encode_target(url)) + destination_scheme = None + else: + parsed_url = parse_url(url) + destination_scheme = parsed_url.scheme + url = to_str(parsed_url.url) + + if headers is None: + headers = self.headers + + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) + + if release_conn is None: + release_conn = preload_content + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries) + + conn = None + + # Track whether `conn` needs to be released before + # returning/raising/recursing. Update this variable if necessary, and + # leave `release_conn` constant throughout the function. That way, if + # the function recurses, the original value of `release_conn` will be + # passed down into the recursive call, and its value will be respected. + # + # See issue #651 [1] for details. + # + # [1] + release_this_conn = release_conn + + http_tunnel_required = connection_requires_http_tunnel( + self.proxy, self.proxy_config, destination_scheme + ) + + # Merge the proxy headers. Only done when not using HTTP CONNECT. We + # have to copy the headers dict so we can safely change it without those + # changes being reflected in anyone else's copy. + if not http_tunnel_required: + headers = headers.copy() # type: ignore[attr-defined] + headers.update(self.proxy_headers) # type: ignore[union-attr] + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + # Keep track of whether we cleanly exited the except block. This + # ensures we do proper cleanup in finally. + clean_exit = False + + # Rewind body position, if needed. Record current position + # for future rewinds in the event of a redirect/retry. + body_pos = set_file_position(body, body_pos) + + try: + # Request a connection from the queue. + timeout_obj = self._get_timeout(timeout) + conn = self._get_conn(timeout=pool_timeout) + + conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] + + # Is this a closed/new connection that requires CONNECT tunnelling? + if self.proxy is not None and http_tunnel_required and conn.is_closed: + try: + self._prepare_proxy(conn) + except (BaseSSLError, OSError, SocketTimeout) as e: + self._raise_timeout( + err=e, url=self.proxy.url, timeout_value=conn.timeout + ) + raise + + # If we're going to release the connection in ``finally:``, then + # the response doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = conn if not release_conn else None + + # Make the request on the HTTPConnection object + response = self._make_request( + conn, + method, + url, + timeout=timeout_obj, + body=body, + headers=headers, + chunked=chunked, + retries=retries, + response_conn=response_conn, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Everything went great! + clean_exit = True + + except EmptyPoolError: + # Didn't get a connection from the pool, no need to clean up + clean_exit = True + release_this_conn = False + raise + + except ( + TimeoutError, + HTTPException, + OSError, + ProtocolError, + BaseSSLError, + SSLError, + CertificateError, + ProxyError, + ) as e: + # Discard the connection for these exceptions. It will be + # replaced during the next _get_conn() call. + clean_exit = False + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + if isinstance( + new_e, + ( + OSError, + NewConnectionError, + TimeoutError, + SSLError, + HTTPException, + ), + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + elif isinstance(new_e, (OSError, HTTPException)): + new_e = ProtocolError("Connection aborted.", new_e) + + retries = retries.increment( + method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] + ) + retries.sleep() + + # Keep track of the error for the retry warning. + err = e + + finally: + if not clean_exit: + # We hit some kind of exception, handled or otherwise. We need + # to throw the connection away unless explicitly told not to. + # Close the connection, set the variable to None, and make sure + # we put the None back in the pool to avoid leaking it. + if conn: + conn.close() + conn = None + release_this_conn = True + + if release_this_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning( + "Retrying (%r) after connection broken by '%r': %s", retries, err, url + ) + return self.urlopen( + method, + url, + body, + headers, + retries, + redirect, + assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + body = None + headers = HTTPHeaderDict(headers)._prepare_for_method_change() + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # self.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not self.is_same_host( + redirect_location + ): + new_headers = headers.copy() # type: ignore[union-attr] + for header in headers: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + headers = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep_for_retry(response) + log.debug("Redirecting %s -> %s", url, redirect_location) + return self.urlopen( + method, + redirect_location, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Check if we should retry the HTTP response. + has_retry_after = bool(response.headers.get("Retry-After")) + if retries.is_retry(method, response.status, has_retry_after): + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_status: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep(response) + log.debug("Retry: %s", url) + return self.urlopen( + method, + url, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, + ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` + is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade + the connection socket into an SSL socket. + """ + + scheme = "https" + ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + ssl_version: int | str | None = None, + ssl_minimum_version: ssl.TLSVersion | None = None, + ssl_maximum_version: ssl.TLSVersion | None = None, + assert_hostname: str | typing.Literal[False] | None = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + **conn_kw: typing.Any, + ) -> None: + super().__init__( + host, + port, + timeout, + maxsize, + block, + headers, + retries, + _proxy, + _proxy_headers, + **conn_kw, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.ca_certs = ca_certs + self.ca_cert_dir = ca_cert_dir + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] + """Establishes a tunnel connection through HTTP CONNECT.""" + if self.proxy and self.proxy.scheme == "https": + tunnel_scheme = "https" + else: + tunnel_scheme = "http" + + conn.set_tunnel( + scheme=tunnel_scheme, + host=self._tunnel_host, + port=self.port, + headers=self.proxy_headers, + ) + conn.connect() + + def _new_conn(self) -> BaseHTTPSConnection: + """ + Return a fresh :class:`urllib3.connection.HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTPS connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "443", + ) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] + raise ImportError( + "Can't connect to HTTPS URL because the SSL module is not available." + ) + + actual_host: str = self.host + actual_port = self.port + if self.proxy is not None and self.proxy.host is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + return self.ConnectionCls( + host=actual_host, + port=actual_port, + timeout=self.timeout.connect_timeout, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + **self.conn_kw, + ) + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + super()._validate_conn(conn) + + # Force connect early to allow us to validate the connection. + if conn.is_closed: + conn.connect() + + # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 + if not conn.is_verified and not conn.proxy_is_verified: + warnings.warn( + ( + f"Unverified HTTPS request is being made to host '{conn.host}'. " + "Adding certificate verification is strongly advised. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#tls-warnings" + ), + InsecureRequestWarning, + ) + + +def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \\**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example:: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + port = port or port_by_scheme.get(scheme, 80) + if scheme == "https": + return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + else: + return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + """ + Normalize hosts for comparisons and use with sockets. + """ + + host = normalize_host(host, scheme) + + # httplib doesn't like it when we include brackets in IPv6 addresses + # Specifically, if we include brackets but also pass the port then + # httplib crazily doubles up the square brackets on the Host header. + # Instead, we need to make sure we never pass ``None`` as the port. + # However, for backward compatibility reasons we can't actually + # *assert* that. See http://bugs.python.org/issue28539 + if host and host.startswith("[") and host.endswith("]"): + host = host[1:-1] + return host + + +def _url_from_pool( + pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None +) -> str: + """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" + return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url + + +def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: + """Drains a queue of connections and closes each one.""" + try: + while True: + conn = pool.get(block=False) + if conn: + conn.close() + except queue.Empty: + pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py new file mode 100644 index 0000000000..e5b62b25e9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import urllib3.connection + +from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection + + +def inject_into_urllib3() -> None: + # override connection classes to use emscripten specific classes + # n.b. mypy complains about the overriding of classes below + # if it isn't ignored + HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection + HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection + urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] + urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] + urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py new file mode 100644 index 0000000000..63f79dd3be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import os +import typing + +# use http.client.HTTPException for consistency with non-emscripten +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady + +from ..._base_connection import _TYPE_BODY +from ...connection import HTTPConnection, ProxyConfig, port_by_scheme +from ...exceptions import TimeoutError +from ...response import BaseHTTPResponse +from ...util.connection import _TYPE_SOCKET_OPTIONS +from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from ...util.url import Url +from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request +from .request import EmscriptenRequest +from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + + +class EmscriptenHTTPConnection: + default_port: typing.ClassVar[int] = port_by_scheme["http"] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + timeout: None | (float) + + host: str + port: int + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool = False + proxy_is_verified: bool | None = None + + response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper + _response: EmscriptenResponse | None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + self.host = host + self.port = port + self.timeout = timeout if isinstance(timeout, float) else 0.0 + self.scheme = "http" + self._closed = True + self._response = None + # ignore these things because we don't + # have control over that stuff + self.proxy = None + self.proxy_config = None + self.blocksize = blocksize + self.source_address = None + self.socket_options = None + self.is_verified = False + + def set_tunnel( + self, + host: str, + port: int | None = 0, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + pass + + def connect(self) -> None: + pass + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + self._closed = False + if url.startswith("/"): + if self.port is not None: + port = f":{self.port}" + else: + port = "" + # no scheme / host / port included, make a full url + url = f"{self.scheme}://{self.host}{port}{url}" + request = EmscriptenRequest( + url=url, + method=method, + timeout=self.timeout if self.timeout else 0, + decode_content=decode_content, + ) + request.set_body(body) + if headers: + for k, v in headers.items(): + request.set_header(k, v) + self._response = None + try: + if not preload_content: + self._response = send_streaming_request(request) + if self._response is None: + self._response = send_request(request) + except _TimeoutError as e: + raise TimeoutError(e.message) from e + except _RequestError as e: + raise HTTPException(e.message) from e + + def getresponse(self) -> BaseHTTPResponse: + if self._response is not None: + return EmscriptenHttpResponseWrapper( + internal_response=self._response, + url=self._response.request.url, + connection=self, + ) + else: + raise ResponseNotReady() + + def close(self) -> None: + self._closed = True + self._response = None + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + return self._closed + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + return True + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + return False + + +class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): + default_port = port_by_scheme["https"] + # all this is basically ignored, as browser handles https + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + cert_file: str | None + key_file: str | None + key_password: str | None + ssl_context: typing.Any | None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None = None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: ( + None | _TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: typing.Any | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + self.scheme = "https" + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + self.cert_reqs = None + + # The browser will automatically verify all requests. + # We have no control over that setting. + self.is_verified = True + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + pass + + +# verify that this class implements BaseHTTP(s) connection correctly +if typing.TYPE_CHECKING: + _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) + _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js new file mode 100644 index 0000000000..faf141e1fa --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js @@ -0,0 +1,110 @@ +let Status = { + SUCCESS_HEADER: -1, + SUCCESS_EOF: -2, + ERROR_TIMEOUT: -3, + ERROR_EXCEPTION: -4, +}; + +let connections = new Map(); +let nextConnectionID = 1; +const encoder = new TextEncoder(); + +self.addEventListener("message", async function (event) { + if (event.data.close) { + let connectionID = event.data.close; + connections.delete(connectionID); + return; + } else if (event.data.getMore) { + let connectionID = event.data.getMore; + let { curOffset, value, reader, intBuffer, byteBuffer } = + connections.get(connectionID); + // if we still have some in buffer, then just send it back straight away + if (!value || curOffset >= value.length) { + // read another buffer if required + try { + let readResponse = await reader.read(); + + if (readResponse.done) { + // read everything - clear connection and return + connections.delete(connectionID); + Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); + Atomics.notify(intBuffer, 0); + // finished reading successfully + // return from event handler + return; + } + curOffset = 0; + connections.get(connectionID).value = readResponse.value; + value = readResponse.value; + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } + + // send as much buffer as we can + let curLen = value.length - curOffset; + if (curLen > byteBuffer.length) { + curLen = byteBuffer.length; + } + byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); + + Atomics.store(intBuffer, 0, curLen); // store current length in bytes + Atomics.notify(intBuffer, 0); + curOffset += curLen; + connections.get(connectionID).curOffset = curOffset; + + return; + } else { + // start fetch + let connectionID = nextConnectionID; + nextConnectionID += 1; + const intBuffer = new Int32Array(event.data.buffer); + const byteBuffer = new Uint8Array(event.data.buffer, 8); + try { + const response = await fetch(event.data.url, event.data.fetchParams); + // return the headers first via textencoder + var headers = []; + for (const pair of response.headers.entries()) { + headers.push([pair[0], pair[1]]); + } + let headerObj = { + headers: headers, + status: response.status, + connectionID, + }; + const headerText = JSON.stringify(headerObj); + let headerBytes = encoder.encode(headerText); + let written = headerBytes.length; + byteBuffer.set(headerBytes); + intBuffer[1] = written; + // make a connection + connections.set(connectionID, { + reader: response.body.getReader(), + intBuffer: intBuffer, + byteBuffer: byteBuffer, + value: undefined, + curOffset: 0, + }); + // set header ready + Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); + Atomics.notify(intBuffer, 0); + // all fetching after this goes through a new postmessage call with getMore + // this allows for parallel requests + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } +}); +self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py new file mode 100644 index 0000000000..612cfddc4c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py @@ -0,0 +1,726 @@ +""" +Support for streaming http requests in emscripten. + +A few caveats - + +If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled +https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md +*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the +JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case +timeouts and streaming should just work. + +Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. + +This approach has several caveats: + +Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. +Streaming only works if you're running pyodide in a web worker. + +Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch +operation, so it requires that you have crossOriginIsolation enabled, by serving over https +(or from localhost) with the two headers below set: + + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp + +You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in +JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole +request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. + +Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once +control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. + +NB: in this code, there are a lot of JavaScript objects. They are named js_* +to make it clear what type of object they are. +""" + +from __future__ import annotations + +import io +import json +from email.parser import Parser +from importlib.resources import files +from typing import TYPE_CHECKING, Any + +import js # type: ignore[import-not-found] +from pyodide.ffi import ( # type: ignore[import-not-found] + JsArray, + JsException, + JsProxy, + to_js, +) + +if TYPE_CHECKING: + from typing_extensions import Buffer + +from .request import EmscriptenRequest +from .response import EmscriptenResponse + +""" +There are some headers that trigger unintended CORS preflight requests. +See also https://github.com/koenvo/pyodide-http/issues/22 +""" +HEADERS_TO_IGNORE = ("user-agent",) + +SUCCESS_HEADER = -1 +SUCCESS_EOF = -2 +ERROR_TIMEOUT = -3 +ERROR_EXCEPTION = -4 + + +class _RequestError(Exception): + def __init__( + self, + message: str | None = None, + *, + request: EmscriptenRequest | None = None, + response: EmscriptenResponse | None = None, + ): + self.request = request + self.response = response + self.message = message + super().__init__(self.message) + + +class _StreamingError(_RequestError): + pass + + +class _TimeoutError(_RequestError): + pass + + +def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: + return to_js(dict_val, dict_converter=js.Object.fromEntries) + + +class _ReadStream(io.RawIOBase): + def __init__( + self, + int_buffer: JsArray, + byte_buffer: JsArray, + timeout: float, + worker: JsProxy, + connection_id: int, + request: EmscriptenRequest, + ): + self.int_buffer = int_buffer + self.byte_buffer = byte_buffer + self.read_pos = 0 + self.read_len = 0 + self.connection_id = connection_id + self.worker = worker + self.timeout = int(1000 * timeout) if timeout > 0 else None + self.is_live = True + self._is_closed = False + self.request: EmscriptenRequest | None = request + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.int_buffer = None + self.byte_buffer = None + self._is_closed = True + self.request = None + if self.is_live: + self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) + self.is_live = False + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def readinto(self, byte_obj: Buffer) -> int: + if not self.int_buffer: + raise _StreamingError( + "No buffer for stream in _ReadStream.readinto", + request=self.request, + response=None, + ) + if self.read_len == 0: + # wait for the worker to send something + js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) + self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) + if ( + js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) + == "timed-out" + ): + raise _TimeoutError + data_len = self.int_buffer[0] + if data_len > 0: + self.read_len = data_len + self.read_pos = 0 + elif data_len == ERROR_EXCEPTION: + string_len = self.int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", + request=self.request, + response=None, + ) + else: + # EOF, free the buffers and return zero + # and free the request + self.is_live = False + self.close() + return 0 + # copy from int32array to python bytes + ret_length = min(self.read_len, len(memoryview(byte_obj))) + subarray = self.byte_buffer.subarray( + self.read_pos, self.read_pos + ret_length + ).to_py() + memoryview(byte_obj)[0:ret_length] = subarray + self.read_len -= ret_length + self.read_pos += ret_length + return ret_length + + +class _StreamingFetcher: + def __init__(self) -> None: + # make web-worker and data buffer on startup + self.streaming_ready = False + streaming_worker_code = ( + files(__package__) + .joinpath("emscripten_fetch_worker.js") + .read_text(encoding="utf-8") + ) + js_data_blob = js.Blob.new( + to_js([streaming_worker_code], create_pyproxies=False), + _obj_from_dict({"type": "application/javascript"}), + ) + + def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: + def onMsg(e: JsProxy) -> None: + self.streaming_ready = True + js_resolve_fn(e) + + def onErr(e: JsProxy) -> None: + js_reject_fn(e) # Defensive: never happens in ci + + self.js_worker.onmessage = onMsg + self.js_worker.onerror = onErr + + js_data_url = js.URL.createObjectURL(js_data_blob) + self.js_worker = js.globalThis.Worker.new(js_data_url) + self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) + + def send(self, request: EmscriptenRequest) -> EmscriptenResponse: + headers = { + k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE + } + + body = request.body + fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} + # start the request off in the worker + timeout = int(1000 * request.timeout) if request.timeout > 0 else None + js_shared_buffer = js.SharedArrayBuffer.new(1048576) + js_int_buffer = js.Int32Array.new(js_shared_buffer) + js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) + + js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) + js.Atomics.notify(js_int_buffer, 0) + js_absolute_url = js.URL.new(request.url, js.location).href + self.js_worker.postMessage( + _obj_from_dict( + { + "buffer": js_shared_buffer, + "url": js_absolute_url, + "fetchParams": fetch_data, + } + ) + ) + # wait for the worker to send something + js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) + if js_int_buffer[0] == ERROR_TIMEOUT: + raise _TimeoutError( + "Timeout connecting to streaming request", + request=request, + response=None, + ) + elif js_int_buffer[0] == SUCCESS_HEADER: + # got response + # header length is in second int of intBuffer + string_len = js_int_buffer[1] + # decode the rest to a JSON string + js_decoder = js.TextDecoder.new() + # this does a copy (the slice) because decode can't work on shared array + # for some silly reason + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + # get it as an object + response_obj = json.loads(json_str) + return EmscriptenResponse( + request=request, + status_code=response_obj["status"], + headers=response_obj["headers"], + body=_ReadStream( + js_int_buffer, + js_byte_buffer, + request.timeout, + self.js_worker, + response_obj["connectionID"], + request, + ), + ) + elif js_int_buffer[0] == ERROR_EXCEPTION: + string_len = js_int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", request=request, response=None + ) + else: + raise _StreamingError( + f"Unknown status from worker in fetch: {js_int_buffer[0]}", + request=request, + response=None, + ) + + +class _JSPIReadStream(io.RawIOBase): + """ + A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch + response. This requires support for WebAssembly JavaScript Promise Integration + in the containing browser, and for pyodide to be launched via runPythonAsync. + + :param js_read_stream: + The JavaScript stream reader + + :param timeout: + Timeout in seconds + + :param request: + The request we're handling + + :param response: + The response this stream relates to + + :param js_abort_controller: + A JavaScript AbortController object, used for timeouts + """ + + def __init__( + self, + js_read_stream: Any, + timeout: float, + request: EmscriptenRequest, + response: EmscriptenResponse, + js_abort_controller: Any, # JavaScript AbortController for timeouts + ): + self.js_read_stream = js_read_stream + self.timeout = timeout + self._is_closed = False + self._is_done = False + self.request: EmscriptenRequest | None = request + self.response: EmscriptenResponse | None = response + self.current_buffer = None + self.current_buffer_pos = 0 + self.js_abort_controller = js_abort_controller + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.js_read_stream.cancel() + self.js_read_stream = None + self._is_closed = True + self._is_done = True + self.request = None + self.response = None + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def _get_next_buffer(self) -> bool: + result_js = _run_sync_with_timeout( + self.js_read_stream.read(), + self.timeout, + self.js_abort_controller, + request=self.request, + response=self.response, + ) + if result_js.done: + self._is_done = True + return False + else: + self.current_buffer = result_js.value.to_py() + self.current_buffer_pos = 0 + return True + + def readinto(self, byte_obj: Buffer) -> int: + if self.current_buffer is None: + if not self._get_next_buffer() or self.current_buffer is None: + self.close() + return 0 + ret_length = min( + len(byte_obj), len(self.current_buffer) - self.current_buffer_pos + ) + byte_obj[0:ret_length] = self.current_buffer[ + self.current_buffer_pos : self.current_buffer_pos + ret_length + ] + self.current_buffer_pos += ret_length + if self.current_buffer_pos == len(self.current_buffer): + self.current_buffer = None + return ret_length + + +# check if we are in a worker or not +def is_in_browser_main_thread() -> bool: + return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window + + +def is_cross_origin_isolated() -> bool: + return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated + + +def is_in_node() -> bool: + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + and hasattr(js.process.release, "name") + and js.process.release.name == "node" + ) + + +def is_worker_available() -> bool: + return hasattr(js, "Worker") and hasattr(js, "Blob") + + +_fetcher: _StreamingFetcher | None = None + +if is_worker_available() and ( + (is_cross_origin_isolated() and not is_in_browser_main_thread()) + and (not is_in_node()) +): + _fetcher = _StreamingFetcher() +else: + _fetcher = None + + +NODE_JSPI_ERROR = ( + "urllib3 only works in Node.js with pyodide.runPythonAsync" + " and requires the flag --experimental-wasm-stack-switching in " + " versions of node <24." +) + + +def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: + if has_jspi(): + return send_jspi_request(request, True) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + + if _fetcher and streaming_ready(): + return _fetcher.send(request) + else: + _show_streaming_warning() + return None + + +_SHOWN_TIMEOUT_WARNING = False + + +def _show_timeout_warning() -> None: + global _SHOWN_TIMEOUT_WARNING + if not _SHOWN_TIMEOUT_WARNING: + _SHOWN_TIMEOUT_WARNING = True + message = "Warning: Timeout is not available on main browser thread" + js.console.warn(message) + + +_SHOWN_STREAMING_WARNING = False + + +def _show_streaming_warning() -> None: + global _SHOWN_STREAMING_WARNING + if not _SHOWN_STREAMING_WARNING: + _SHOWN_STREAMING_WARNING = True + message = "Can't stream HTTP requests because: \n" + if not is_cross_origin_isolated(): + message += " Page is not cross-origin isolated\n" + if is_in_browser_main_thread(): + message += " Python is running in main browser thread\n" + if not is_worker_available(): + message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in + if streaming_ready() is False: + message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch +is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" + from js import console + + console.warn(message) + + +def send_request(request: EmscriptenRequest) -> EmscriptenResponse: + if has_jspi(): + return send_jspi_request(request, False) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + try: + js_xhr = js.XMLHttpRequest.new() + + if not is_in_browser_main_thread(): + js_xhr.responseType = "arraybuffer" + if request.timeout: + js_xhr.timeout = int(request.timeout * 1000) + else: + js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") + if request.timeout: + # timeout isn't available on the main thread - show a warning in console + # if it is set + _show_timeout_warning() + + js_xhr.open(request.method, request.url, False) + for name, value in request.headers.items(): + if name.lower() not in HEADERS_TO_IGNORE: + js_xhr.setRequestHeader(name, value) + + js_xhr.send(to_js(request.body)) + + headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) + + if not is_in_browser_main_thread(): + body = js_xhr.response.to_py().tobytes() + else: + body = js_xhr.response.encode("ISO-8859-15") + return EmscriptenResponse( + status_code=js_xhr.status, headers=headers, body=body, request=request + ) + except JsException as err: + if err.name == "TimeoutError": + raise _TimeoutError(err.message, request=request) + elif err.name == "NetworkError": + raise _RequestError(err.message, request=request) + else: + # general http error + raise _RequestError(err.message, request=request) + + +def send_jspi_request( + request: EmscriptenRequest, streaming: bool +) -> EmscriptenResponse: + """ + Send a request using WebAssembly JavaScript Promise Integration + to wrap the asynchronous JavaScript fetch api (experimental). + + :param request: + Request to send + + :param streaming: + Whether to stream the response + + :return: The response object + :rtype: EmscriptenResponse + """ + timeout = request.timeout + js_abort_controller = js.AbortController.new() + headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} + req_body = request.body + fetch_data = { + "headers": headers, + "body": to_js(req_body), + "method": request.method, + "signal": js_abort_controller.signal, + } + # Node.js returns the whole response (unlike opaqueredirect in browsers), + # so urllib3 can set `redirect: manual` to control redirects itself. + # https://stackoverflow.com/a/78524615 + if _is_node_js(): + fetch_data["redirect"] = "manual" + # Call JavaScript fetch (async api, returns a promise) + fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) + # Now suspend WebAssembly until we resolve that promise + # or time out. + response_js = _run_sync_with_timeout( + fetcher_promise_js, + timeout, + js_abort_controller, + request=request, + response=None, + ) + headers = {} + header_iter = response_js.headers.entries() + while True: + iter_value_js = header_iter.next() + if getattr(iter_value_js, "done", False): + break + else: + headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) + status_code = response_js.status + body: bytes | io.RawIOBase = b"" + + response = EmscriptenResponse( + status_code=status_code, headers=headers, body=b"", request=request + ) + if streaming: + # get via inputstream + if response_js.body is not None: + # get a reader from the fetch response + body_stream_js = response_js.body.getReader() + body = _JSPIReadStream( + body_stream_js, timeout, request, response, js_abort_controller + ) + else: + # get directly via arraybuffer + # n.b. this is another async JavaScript call. + body = _run_sync_with_timeout( + response_js.arrayBuffer(), + timeout, + js_abort_controller, + request=request, + response=response, + ).to_py() + response.body = body + return response + + +def _run_sync_with_timeout( + promise: Any, + timeout: float, + js_abort_controller: Any, + request: EmscriptenRequest | None, + response: EmscriptenResponse | None, +) -> Any: + """ + Await a JavaScript promise synchronously with a timeout which is implemented + via the AbortController + + :param promise: + Javascript promise to await + + :param timeout: + Timeout in seconds + + :param js_abort_controller: + A JavaScript AbortController object, used on timeout + + :param request: + The request being handled + + :param response: + The response being handled (if it exists yet) + + :raises _TimeoutError: If the request times out + :raises _RequestError: If the request raises a JavaScript exception + + :return: The result of awaiting the promise. + """ + timer_id = None + if timeout > 0: + timer_id = js.setTimeout( + js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) + ) + try: + from pyodide.ffi import run_sync + + # run_sync here uses WebAssembly JavaScript Promise Integration to + # suspend python until the JavaScript promise resolves. + return run_sync(promise) + except JsException as err: + if err.name == "AbortError": + raise _TimeoutError( + message="Request timed out", request=request, response=response + ) + else: + raise _RequestError(message=err.message, request=request, response=response) + finally: + if timer_id is not None: + js.clearTimeout(timer_id) + + +def has_jspi() -> bool: + """ + Return true if jspi can be used. + + This requires both browser support and also WebAssembly + to be in the correct state - i.e. that the javascript + call into python was async not sync. + + :return: True if jspi can be used. + :rtype: bool + """ + try: + from pyodide.ffi import can_run_sync, run_sync # noqa: F401 + + return bool(can_run_sync()) + except ImportError: + return False + + +def _is_node_js() -> bool: + """ + Check if we are in Node.js. + + :return: True if we are in Node.js. + :rtype: bool + """ + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + # According to the Node.js documentation, the release name is always "node". + and js.process.release.name == "node" + ) + + +def streaming_ready() -> bool | None: + if _fetcher: + return _fetcher.streaming_ready + else: + return None # no fetcher, return None to signify that + + +async def wait_for_streaming_ready() -> bool: + if _fetcher: + await _fetcher.js_worker_ready_promise + return True + else: + return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py new file mode 100644 index 0000000000..e692e692bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..._base_connection import _TYPE_BODY + + +@dataclass +class EmscriptenRequest: + method: str + url: str + params: dict[str, str] | None = None + body: _TYPE_BODY | None = None + headers: dict[str, str] = field(default_factory=dict) + timeout: float = 0 + decode_content: bool = True + + def set_header(self, name: str, value: str) -> None: + self.headers[name.capitalize()] = value + + def set_body(self, body: _TYPE_BODY | None) -> None: + self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py new file mode 100644 index 0000000000..ec1e1dbe83 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import json as _json +import logging +import typing +from contextlib import contextmanager +from dataclasses import dataclass +from http.client import HTTPException as HTTPException +from io import BytesIO, IOBase + +from ...exceptions import InvalidHeader, TimeoutError +from ...response import BaseHTTPResponse +from ...util.retry import Retry +from .request import EmscriptenRequest + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + + +@dataclass +class EmscriptenResponse: + status_code: int + headers: dict[str, str] + body: IOBase | bytes + request: EmscriptenRequest + + +class EmscriptenHttpResponseWrapper(BaseHTTPResponse): + def __init__( + self, + internal_response: EmscriptenResponse, + url: str | None = None, + connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, + ): + self._pool = None # set by pool class + self._body = None + self._uncached_read_occurred = False + self._response = internal_response + self._url = url + self._connection = connection + self._closed = False + super().__init__( + headers=internal_response.headers, + status=internal_response.status_code, + request_url=url, + version=0, + version_string="HTTP/?", + reason="", + decode_content=True, + ) + self.length_remaining = self._init_length(self._response.request.method) + self.length_is_certain = False + + @property + def url(self) -> str | None: + return self._url + + @url.setter + def url(self, url: str | None) -> None: + self._url = url + + @property + def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: + return self._connection + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + while True: + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + else: + break + + def _init_length(self, request_method: str | None) -> int | None: + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Check for responses that shouldn't include a body + if ( + self.status in (204, 304) + or 100 <= self.status < 200 + or request_method == "HEAD" + ): + length = 0 + + return length + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, # ignored because browser decodes always + cache_content: bool = False, + ) -> bytes: + if ( + self._closed + or self._response is None + or (isinstance(self._response.body, IOBase) and self._response.body.closed) + ): + return b"" + + with self._error_catcher(): + # body has been preloaded as a string by XmlHttpRequest + if not isinstance(self._response.body, IOBase): + self.length_remaining = len(self._response.body) + self.length_is_certain = True + # wrap body in IOStream + self._response.body = BytesIO(self._response.body) + if amt is not None and amt >= 0: + # don't cache partial content + cache_content = False + data = self._response.body.read(amt) + self._uncached_read_occurred = True + else: # read all we can (and cache it) + data = self._response.body.read() + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + self._uncached_read_occurred = True + if self.length_remaining is not None: + self.length_remaining = max(self.length_remaining - len(data), 0) + if len(data) == 0 or ( + self.length_is_certain and self.length_remaining == 0 + ): + # definitely finished reading, close response stream + self._response.body.close() + return typing.cast(bytes, data) + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Generator[bytes]: + # chunked is handled by browser + while True: + bytes = self.read(amt, decode_content) + if not bytes: + break + yield bytes + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + self.close() + + @property + def data(self) -> bytes: + if self._body: + return self._body + else: + return self.read(cache_content=True) + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + def close(self) -> None: + if not self._closed: + if isinstance(self._response.body, IOBase): + self._response.body.close() + if self._connection: + self._connection.close() + self._connection = None + self._closed = True + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch Emscripten specific exceptions thrown by fetch.py, + instead re-raising urllib3 variants, so that low-level exceptions + are not leaked in the high-level api. + + On exit, release the connection back to the pool. + """ + from .fetch import _RequestError, _TimeoutError # avoid circular import + + clean_exit = False + + try: + yield + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + except _TimeoutError as e: + raise TimeoutError(str(e)) + except _RequestError as e: + raise HTTPException(str(e)) + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now + if ( + isinstance(self._response.body, IOBase) + and not self._response.body.closed + ): + self._response.body.close() + # release the connection back to the pool + self.release_conn() + else: + # If we have read everything from the response stream, + # return the connection back to the pool. + if ( + isinstance(self._response.body, IOBase) + and self._response.body.closed + ): + self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py new file mode 100644 index 0000000000..f06b859992 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py @@ -0,0 +1,563 @@ +""" +Module for using pyOpenSSL as a TLS backend. This module was relevant before +the standard library ``ssl`` module supported SNI, but now that we've dropped +support for Python 2.7 all relevant Python versions support SNI so +**this module is no longer recommended**. + +This needs the following packages installed: + +* `pyOpenSSL`_ (tested with 19.0.0) +* `cryptography`_ (minimum 2.3, from pyopenssl) +* `idna`_ (minimum 2.1, from cryptography) + +However, pyOpenSSL depends on cryptography, so while we use all three directly here we +end up having relatively few packages required. + +You can install them with the following command: + +.. code-block:: bash + + $ python -m pip install pyopenssl cryptography idna + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this: + +.. code-block:: python + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +.. _pyopenssl: https://www.pyopenssl.org +.. _cryptography: https://cryptography.io +.. _idna: https://github.com/kjd/idna +""" + +from __future__ import annotations + +import OpenSSL.SSL # type: ignore[import-not-found] +from cryptography import x509 + +try: + from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] +except ImportError: + # UnsupportedExtension is gone in cryptography >= 2.1.0 + class UnsupportedExtension(Exception): # type: ignore[no-redef] + pass + + +import logging +import ssl +import typing +from io import BytesIO +from socket import socket as socket_cls + +from .. import util + +if typing.TYPE_CHECKING: + from OpenSSL.crypto import X509 # type: ignore[import-not-found] + + +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions: dict[int, int] = { + util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} +_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} + +# The SSLvX values are the most likely to be missing in the future +# but we check them all just to be sure. +_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( + OpenSSL.SSL, "OP_NO_SSLv3", 0 +) +_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) +_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) +_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) +_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) + +_openssl_to_ssl_minimum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, + ssl.TLSVersion.TLSv1_3: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), + ssl.TLSVersion.MAXIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), +} +_openssl_to_ssl_maximum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 + | _OP_NO_TLSv1 + | _OP_NO_TLSv1_1 + | _OP_NO_TLSv1_2 + | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, +} + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_SSLContext = util.ssl_.SSLContext + + +log = logging.getLogger(__name__) + + +def inject_into_urllib3() -> None: + "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." + + _validate_dependencies_met() + + util.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.IS_PYOPENSSL = True + util.ssl_.IS_PYOPENSSL = True + + +def extract_from_urllib3() -> None: + "Undo monkey-patching by :func:`inject_into_urllib3`." + + util.SSLContext = orig_util_SSLContext + util.ssl_.SSLContext = orig_util_SSLContext + util.IS_PYOPENSSL = False + util.ssl_.IS_PYOPENSSL = False + + +def _validate_dependencies_met() -> None: + """ + Verifies that PyOpenSSL's package-level dependencies have been met. + Throws `ImportError` if they are not met. + """ + # Method added in `cryptography==1.1`; not available in older versions + from cryptography.x509.extensions import Extensions + + if getattr(Extensions, "get_extension_for_class", None) is None: + raise ImportError( + "'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer." + ) + + # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 + # attribute is only present on those versions. + from OpenSSL.crypto import X509 + + x509 = X509() + if getattr(x509, "_x509", None) is None: + raise ImportError( + "'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer." + ) + + +def _dnsname_to_stdlib(name: str) -> str | None: + """ + Converts a dNSName SubjectAlternativeName field to the form used by the + standard library on the given Python version. + + Cryptography produces a dNSName as a unicode string that was idna-decoded + from ASCII bytes. We need to idna-encode that string to get it back, and + then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib + uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). + + If the name cannot be idna-encoded then we return None signalling that + the name given should be skipped. + """ + + def idna_encode(name: str) -> bytes | None: + """ + Borrowed wholesale from the Python Cryptography Project. It turns out + that we can't just safely call `idna.encode`: it can explode for + wildcard names. This avoids that problem. + """ + import idna + + try: + for prefix in ["*.", "."]: + if name.startswith(prefix): + name = name[len(prefix) :] + return prefix.encode("ascii") + idna.encode(name) + return idna.encode(name) + except idna.core.IDNAError: + return None + + # Don't send IPv6 addresses through the IDNA encoder. + if ":" in name: + return name + + encoded_name = idna_encode(name) + if encoded_name is None: + return None + return encoded_name.decode("utf-8") + + +def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: + """ + Given an PyOpenSSL certificate, provides all the subject alternative names. + """ + cert = peer_cert.to_cryptography() + + # We want to find the SAN extension. Ask Cryptography to locate it (it's + # faster than looping in Python) + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + except x509.ExtensionNotFound: + # No such extension, return the empty list. + return [] + except ( + x509.DuplicateExtension, + UnsupportedExtension, + x509.UnsupportedGeneralNameType, + UnicodeError, + ) as e: + # A problem has been found with the quality of the certificate. Assume + # no SAN field is present. + log.warning( + "A problem was encountered with the certificate that prevented " + "urllib3 from finding the SubjectAlternativeName field. This can " + "affect certificate validation. The error was %s", + e, + ) + return [] + + # We want to return dNSName and iPAddress fields. We need to cast the IPs + # back to strings because the match_hostname function wants them as + # strings. + # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 + # decoded. This is pretty frustrating, but that's what the standard library + # does with certificates, and so we need to attempt to do the same. + # We also want to skip over names which cannot be idna encoded. + names = [ + ("DNS", name) + for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) + if name is not None + ] + names.extend( + ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) + ) + + return names + + +class WrappedSocket: + """API-compatibility wrapper for Python OpenSSL's Connection-class.""" + + def __init__( + self, + connection: OpenSSL.SSL.Connection, + socket: socket_cls, + suppress_ragged_eofs: bool = True, + ) -> None: + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._io_refs = 0 + self._closed = False + + def fileno(self) -> int: + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self) -> None: + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return b"" + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b"" + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + else: + return data # type: ignore[no-any-return] + + def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: + try: + return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return 0 + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return 0 + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv_into(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + + def settimeout(self, timeout: float) -> None: + return self.socket.settimeout(timeout) + + def _send_until_done(self, data: bytes) -> int: + while True: + try: + return self.connection.send(data) # type: ignore[no-any-return] + except OpenSSL.SSL.WantWriteError as e: + if not util.wait_for_write(self.socket, self.socket.gettimeout()): + raise TimeoutError() from e + continue + except OpenSSL.SSL.SysCallError as e: + raise OSError(e.args[0], str(e)) from e + + def sendall(self, data: bytes) -> None: + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done( + data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] + ) + total_sent += sent + + def shutdown(self, how: int) -> None: + try: + self.connection.shutdown() + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"shutdown error: {e!r}") from e + + def close(self) -> None: + self._closed = True + if self._io_refs <= 0: + self._real_close() + + def _real_close(self) -> None: + try: + return self.connection.close() # type: ignore[no-any-return] + except OpenSSL.SSL.Error: + return + + def getpeercert( + self, binary_form: bool = False + ) -> dict[str, list[typing.Any]] | None: + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 # type: ignore[no-any-return] + + if binary_form: + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] + + return { + "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] + "subjectAltName": get_subj_alt_name(x509), + } + + def version(self) -> str: + return self.connection.get_protocol_version_name() # type: ignore[no-any-return] + + def selected_alpn_protocol(self) -> str | None: + alpn_proto = self.connection.get_alpn_proto_negotiated() + return alpn_proto.decode() if alpn_proto else None + + +WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] + + +class PyOpenSSLContext: + """ + I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible + for translating the interface of the standard library ``SSLContext`` object + to calls into PyOpenSSL. + """ + + def __init__(self, protocol: int) -> None: + self.protocol = _openssl_versions[protocol] + self._ctx = OpenSSL.SSL.Context(self.protocol) + self._options = 0 + self.check_hostname = False + self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED + self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED + self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST + + @property + def options(self) -> int: + return self._options + + @options.setter + def options(self, value: int) -> None: + self._options = value + self._set_ctx_options() + + @property + def verify_flags(self) -> int: + return self._verify_flags + + @verify_flags.setter + def verify_flags(self, value: int) -> None: + self._verify_flags = value + self._ctx.get_cert_store().set_flags(self._verify_flags) + + @property + def verify_mode(self) -> int: + return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] + + @verify_mode.setter + def verify_mode(self, value: ssl.VerifyMode) -> None: + self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) + + def set_default_verify_paths(self) -> None: + self._ctx.set_default_verify_paths() + + def set_ciphers(self, ciphers: bytes | str) -> None: + if isinstance(ciphers, str): + ciphers = ciphers.encode("utf-8") + self._ctx.set_cipher_list(ciphers) + + def load_verify_locations( + self, + cafile: str | None = None, + capath: str | None = None, + cadata: bytes | None = None, + ) -> None: + if cafile is not None: + cafile = cafile.encode("utf-8") # type: ignore[assignment] + if capath is not None: + capath = capath.encode("utf-8") # type: ignore[assignment] + try: + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e + + def load_cert_chain( + self, + certfile: str, + keyfile: str | None = None, + password: str | None = None, + ) -> None: + try: + self._ctx.use_certificate_chain_file(certfile) + if password is not None: + if not isinstance(password, bytes): + password = password.encode("utf-8") # type: ignore[assignment] + self._ctx.set_passwd_cb(lambda *_: password) + self._ctx.use_privatekey_file(keyfile or certfile) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e + + def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: + protocols = [util.util.to_bytes(p, "ascii") for p in protocols] + return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] + + def wrap_socket( + self, + sock: socket_cls, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bytes | str | None = None, + ) -> WrappedSocket: + cnx = OpenSSL.SSL.Connection(self._ctx, sock) + + # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 + if server_hostname and not util.ssl_.is_ipaddress(server_hostname): + if isinstance(server_hostname, str): + server_hostname = server_hostname.encode("utf-8") + cnx.set_tlsext_host_name(server_hostname) + + cnx.set_connect_state() + + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(sock, sock.gettimeout()): + raise TimeoutError("select timed out") from e + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"bad handshake: {e!r}") from e + break + + return WrappedSocket(cnx, sock) + + def _set_ctx_options(self) -> None: + self._ctx.set_options( + self._options + | _openssl_to_ssl_minimum_version[self._minimum_version] + | _openssl_to_ssl_maximum_version[self._maximum_version] + ) + + @property + def minimum_version(self) -> int: + return self._minimum_version + + @minimum_version.setter + def minimum_version(self, minimum_version: int) -> None: + self._minimum_version = minimum_version + self._set_ctx_options() + + @property + def maximum_version(self) -> int: + return self._maximum_version + + @maximum_version.setter + def maximum_version(self, maximum_version: int) -> None: + self._maximum_version = maximum_version + self._set_ctx_options() + + +def _verify_callback( + cnx: OpenSSL.SSL.Connection, + x509: X509, + err_no: int, + err_depth: int, + return_code: int, +) -> bool: + return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py new file mode 100644 index 0000000000..d37da8fc20 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py @@ -0,0 +1,228 @@ +""" +This module contains provisional support for SOCKS proxies from within +urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and +SOCKS5. To enable its functionality, either install PySocks or install this +module with the ``socks`` extra. + +The SOCKS implementation supports the full range of urllib3 features. It also +supports the following SOCKS features: + +- SOCKS4A (``proxy_url='socks4a://...``) +- SOCKS4 (``proxy_url='socks4://...``) +- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) +- SOCKS5 with local DNS (``proxy_url='socks5://...``) +- Usernames and passwords for the SOCKS proxy + +.. note:: + It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in + your ``proxy_url`` to ensure that DNS resolution is done from the remote + server instead of client-side when connecting to a domain name. + +SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 +supports IPv4, IPv6, and domain names. + +When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` +will be sent as the ``userid`` section of the SOCKS request: + +.. code-block:: python + + proxy_url="socks4a://@proxy-host" + +When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion +of the ``proxy_url`` will be sent as the username/password to authenticate +with the proxy: + +.. code-block:: python + + proxy_url="socks5h://:@proxy-host" + +""" + +from __future__ import annotations + +try: + import socks # type: ignore[import-untyped] +except ImportError: + import warnings + + from ..exceptions import DependencyWarning + + warnings.warn( + ( + "SOCKS support in urllib3 requires the installation of optional " + "dependencies: specifically, PySocks. For more information, see " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" + ), + DependencyWarning, + ) + raise + +import typing +from socket import timeout as SocketTimeout + +from ..connection import HTTPConnection, HTTPSConnection +from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from ..exceptions import ConnectTimeoutError, NewConnectionError +from ..poolmanager import PoolManager +from ..util.url import parse_url + +try: + import ssl +except ImportError: + ssl = None # type: ignore[assignment] + + +class _TYPE_SOCKS_OPTIONS(typing.TypedDict): + socks_version: int + proxy_host: str | None + proxy_port: str | None + username: str | None + password: str | None + rdns: bool + + +class SOCKSConnection(HTTPConnection): + """ + A plain-text HTTP connection that connects via a SOCKS proxy. + """ + + def __init__( + self, + _socks_options: _TYPE_SOCKS_OPTIONS, + *args: typing.Any, + **kwargs: typing.Any, + ) -> None: + self._socks_options = _socks_options + super().__init__(*args, **kwargs) + + def _new_conn(self) -> socks.socksocket: + """ + Establish a new connection via the SOCKS proxy. + """ + extra_kw: dict[str, typing.Any] = {} + if self.source_address: + extra_kw["source_address"] = self.source_address + + if self.socket_options: + extra_kw["socket_options"] = self.socket_options + + try: + conn = socks.create_connection( + (self.host, self.port), + proxy_type=self._socks_options["socks_version"], + proxy_addr=self._socks_options["proxy_host"], + proxy_port=self._socks_options["proxy_port"], + proxy_username=self._socks_options["username"], + proxy_password=self._socks_options["password"], + proxy_rdns=self._socks_options["rdns"], + timeout=self.timeout, + **extra_kw, + ) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except socks.ProxyError as e: + # This is fragile as hell, but it seems to be the only way to raise + # useful errors here. + if e.socket_err: + error = e.socket_err + if isinstance(error, SocketTimeout): + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + else: + # Adding `from e` messes with coverage somehow, so it's omitted. + # See #2386. + raise NewConnectionError( + self, f"Failed to establish a new connection: {error}" + ) + else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + except OSError as e: # Defensive: PySocks should catch all these. + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + return conn + + +# We don't need to duplicate the Verified/Unverified distinction from +# urllib3/connection.py here because the HTTPSConnection will already have been +# correctly set to either the Verified or Unverified form by that module. This +# means the SOCKSHTTPSConnection will automatically be the correct type. +class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): + pass + + +class SOCKSHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = SOCKSConnection + + +class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): + ConnectionCls = SOCKSHTTPSConnection + + +class SOCKSProxyManager(PoolManager): + """ + A version of the urllib3 ProxyManager that routes connections via the + defined SOCKS proxy. + """ + + pool_classes_by_scheme = { + "http": SOCKSHTTPConnectionPool, + "https": SOCKSHTTPSConnectionPool, + } + + def __init__( + self, + proxy_url: str, + username: str | None = None, + password: str | None = None, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ): + parsed = parse_url(proxy_url) + + if username is None and password is None and parsed.auth is not None: + split = parsed.auth.split(":") + if len(split) == 2: + username, password = split + if parsed.scheme == "socks5": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = False + elif parsed.scheme == "socks5h": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = True + elif parsed.scheme == "socks4": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = False + elif parsed.scheme == "socks4a": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = True + else: + raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") + + self.proxy_url = proxy_url + + socks_options = { + "socks_version": socks_version, + "proxy_host": parsed.host, + "proxy_port": parsed.port, + "username": username, + "password": password, + "rdns": rdns, + } + connection_pool_kw["_socks_options"] = socks_options + + super().__init__(num_pools, headers, **connection_pool_kw) + + self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py new file mode 100644 index 0000000000..3d7e9b93d3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import socket +import typing +import warnings +from email.errors import MessageDefect +from http.client import IncompleteRead as httplib_IncompleteRead + +if typing.TYPE_CHECKING: + from .connection import HTTPConnection + from .connectionpool import ConnectionPool + from .response import HTTPResponse + from .util.retry import Retry + +# Base Exceptions + + +class HTTPError(Exception): + """Base exception used by this module.""" + + +class HTTPWarning(Warning): + """Base warning used by this module.""" + + +_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] + + +class PoolError(HTTPError): + """Base exception for errors caused within a pool.""" + + def __init__(self, pool: ConnectionPool, message: str) -> None: + self.pool = pool + self._message = message + super().__init__(f"{pool}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + +class RequestError(PoolError): + """Base exception for PoolErrors that have associated URLs.""" + + def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: + self.url = url + super().__init__(pool, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self._message) + + +class SSLError(HTTPError): + """Raised when SSL certificate fails in an HTTPS connection.""" + + +class ProxyError(HTTPError): + """Raised when the connection to a proxy fails.""" + + # The original error is also available as __cause__. + original_error: Exception + + def __init__(self, message: str, error: Exception) -> None: + super().__init__(message, error) + self.original_error = error + + +class DecodeError(HTTPError): + """Raised when automatic decoding based on Content-Type fails.""" + + +class ProtocolError(HTTPError): + """Raised when something unexpected happens mid-request/response.""" + + +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + +# Leaf Exceptions + + +class MaxRetryError(RequestError): + """Raised when the maximum number of retries is exceeded. + + :param pool: The connection pool + :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` + :param str url: The requested Url + :param reason: The underlying error + :type reason: :class:`Exception` + + """ + + def __init__( + self, pool: ConnectionPool, url: str | None, reason: Exception | None = None + ) -> None: + self.reason = reason + + message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" + + super().__init__(pool, url, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self.reason) + + +class HostChangedError(RequestError): + """Raised when an existing pool gets a request for a foreign host.""" + + def __init__( + self, pool: ConnectionPool, url: str, retries: Retry | int = 3 + ) -> None: + message = f"Tried to open a foreign host with url: {url}" + super().__init__(pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """Raised when passing an invalid state to a timeout""" + + +class TimeoutError(HTTPError): + """Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + ` and :exc:`ConnectTimeoutErrors `. + """ + + +class ReadTimeoutError(TimeoutError, RequestError): + """Raised when a socket timeout occurs while receiving data from a server""" + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + """Raised when a socket timeout occurs while connecting to a server""" + + +class NewConnectionError(ConnectTimeoutError, HTTPError): + """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" + + def __init__(self, conn: HTTPConnection, message: str) -> None: + self.conn = conn + self._message = message + super().__init__(f"{conn}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + @property + def pool(self) -> HTTPConnection: + warnings.warn( + "The 'pool' property is deprecated and will be removed " + "in urllib3 v3.0. Use 'conn' instead.", + FutureWarning, + stacklevel=2, + ) + + return self.conn + + +class NameResolutionError(NewConnectionError): + """Raised when host name resolution fails.""" + + def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): + message = f"Failed to resolve '{host}' ({reason})" + self._host = host + self._reason = reason + super().__init__(conn, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (self._host, None, self._reason) + + +class EmptyPoolError(PoolError): + """Raised when a pool runs out of connections and no more are allowed.""" + + +class FullPoolError(PoolError): + """Raised when we try to add a connection to a full pool in blocking mode.""" + + +class ClosedPoolError(PoolError): + """Raised when a request enters a pool after the pool has been closed.""" + + +class LocationValueError(ValueError, HTTPError): + """Raised when there is something wrong with a given URL input.""" + + +class LocationParseError(LocationValueError): + """Raised when get_host or similar fails to parse the URL input.""" + + def __init__(self, location: str) -> None: + message = f"Failed to parse: {location}" + super().__init__(message) + + self.location = location + + +class URLSchemeUnknown(LocationValueError): + """Raised when a URL input has an unsupported scheme.""" + + def __init__(self, scheme: str): + message = f"Not supported URL scheme {scheme}" + super().__init__(message) + + self.scheme = scheme + + +class ResponseError(HTTPError): + """Used as a container for an error reason supplied in a MaxRetryError.""" + + GENERIC_ERROR = "too many error responses" + SPECIFIC_ERROR = "too many {status_code} error responses" + + +class SecurityWarning(HTTPWarning): + """Warned when performing security reducing actions""" + + +class InsecureRequestWarning(SecurityWarning): + """Warned when making an unverified HTTPS request.""" + + +class NotOpenSSLWarning(SecurityWarning): + """Warned when using unsupported SSL library""" + + +class SystemTimeWarning(SecurityWarning): + """Warned when system time is suspected to be wrong""" + + +class InsecurePlatformWarning(SecurityWarning): + """Warned when certain TLS/SSL configuration is not available on a platform.""" + + +class DependencyWarning(HTTPWarning): + """ + Warned when an attempt is made to import a module with missing optional + dependencies. + """ + + +class ResponseNotChunked(ProtocolError, ValueError): + """Response needs to be chunked in order to read it as chunks.""" + + +class BodyNotHttplibCompatible(HTTPError): + """ + Body should be :class:`http.client.HTTPResponse` like + (have an fp attribute which returns raw chunks) for read_chunked(). + """ + + +class IncompleteRead(HTTPError, httplib_IncompleteRead): + """ + Response length doesn't match expected Content-Length + + Subclass of :class:`http.client.IncompleteRead` to allow int value + for ``partial`` to avoid creating large objects on streamed reads. + """ + + partial: int # type: ignore[assignment] + expected: int + + def __init__(self, partial: int, expected: int) -> None: + self.partial = partial + self.expected = expected + + def __repr__(self) -> str: + return "IncompleteRead(%i bytes read, %i more expected)" % ( + self.partial, + self.expected, + ) + + +class InvalidChunkLength(HTTPError, httplib_IncompleteRead): + """Invalid chunk length in a chunked response.""" + + def __init__(self, response: HTTPResponse, length: bytes) -> None: + self.partial: int = response.tell() # type: ignore[assignment] + self.expected: int | None = response.length_remaining + self.response = response + self.length = length + + def __repr__(self) -> str: + return "InvalidChunkLength(got length %r, %i bytes read)" % ( + self.length, + self.partial, + ) + + +class InvalidHeader(HTTPError): + """The header provided was somehow invalid.""" + + +class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): + """ProxyManager does not support the supplied scheme""" + + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. + + def __init__(self, scheme: str | None) -> None: + # 'localhost' is here because our URL parser parses + # localhost:8080 -> scheme=localhost, remove if we fix this. + if scheme == "localhost": + scheme = None + if scheme is None: + message = "Proxy URL had no scheme, should start with http:// or https://" + else: + message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" + super().__init__(message) + + +class ProxySchemeUnsupported(ValueError): + """Fetching HTTPS resources through HTTPS proxies is unsupported""" + + +class HeaderParsingError(HTTPError): + """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" + + def __init__( + self, defects: list[MessageDefect], unparsed_data: bytes | str | None + ) -> None: + message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" + super().__init__(message) + + +class UnrewindableBodyError(HTTPError): + """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py new file mode 100644 index 0000000000..fe68e17732 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import email.utils +import mimetypes +import typing + +_TYPE_FIELD_VALUE = typing.Union[str, bytes] +_TYPE_FIELD_VALUE_TUPLE = typing.Union[ + _TYPE_FIELD_VALUE, + tuple[str, _TYPE_FIELD_VALUE], + tuple[str, _TYPE_FIELD_VALUE, str], +] + + +def guess_content_type( + filename: str | None, default: str = "application/octet-stream" +) -> str: + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetypes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Helper function to format and quote a single header parameter using the + strategy defined in RFC 2231. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows + `RFC 2388 Section 4.4 `_. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as ``bytes`` or `str``. + :returns: + An RFC-2231-formatted unicode string. + + .. deprecated:: 2.0.0 + Will be removed in urllib3 v3.0. This is not valid for + ``multipart/form-data`` header parameters. + """ + import warnings + + warnings.warn( + "'format_header_param_rfc2231' is insecure, deprecated and will be " + "removed in urllib3 v3.0. This is not valid for " + "multipart/form-data header parameters.", + FutureWarning, + stacklevel=2, + ) + + if isinstance(value, bytes): + value = value.decode("utf-8") + + if not any(ch in value for ch in '"\\\r\n'): + result = f'{name}="{value}"' + try: + result.encode("ascii") + except (UnicodeEncodeError, UnicodeDecodeError): + pass + else: + return result + + value = email.utils.encode_rfc2231(value, "utf-8") + value = f"{name}*={value}" + + return value + + +def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Format and quote a single multipart header parameter. + + This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching + the behavior of current browser and curl versions. Values are + assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are + percent encoded. + + .. _WHATWG HTML Standard: + https://html.spec.whatwg.org/multipage/ + form-control-infrastructure.html#multipart-form-data + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + :returns: + A string ``name="value"`` with the escaped value. + + .. versionchanged:: 2.0.0 + Matches the WHATWG HTML Standard as of 2021/06/10. Control + characters are no longer percent encoded. + + .. versionchanged:: 2.0.0 + Renamed from ``format_header_param_html5`` and + ``format_header_param``. The old names will be removed in + urllib3 v3.0. + """ + if isinstance(value, bytes): + value = value.decode("utf-8") + + # percent encode \n \r " + value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) + return f'{name}="{value}"' + + +def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param_html5' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +class RequestField: + """ + A data container for request body parameters. + + :param name: + The name of this request field. Must be unicode. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. Must be unicode. + :param headers: + An optional dict-like object of headers to initially use for the field. + + .. versionchanged:: 2.0.0 + The ``header_formatter`` parameter is deprecated and will + be removed in urllib3 v3.0. + """ + + def __init__( + self, + name: str, + data: _TYPE_FIELD_VALUE, + filename: str | None = None, + headers: typing.Mapping[str, str] | None = None, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ): + self._name = name + self._filename = filename + self.data = data + self.headers: dict[str, str | None] = {} + if headers: + self.headers = dict(headers) + + if header_formatter is not None: + import warnings + + warnings.warn( + "The 'header_formatter' parameter is deprecated and " + "will be removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + self.header_formatter = header_formatter + else: + self.header_formatter = format_multipart_header_param + + @classmethod + def from_tuples( + cls, + fieldname: str, + value: _TYPE_FIELD_VALUE_TUPLE, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ) -> RequestField: + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from + parameter of key/value strings AND key/filetuple. A filetuple is a + (filename, data, MIME type) tuple where the MIME type is optional. + For example:: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + filename: str | None + content_type: str | None + data: _TYPE_FIELD_VALUE + + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls( + fieldname, data, filename=filename, header_formatter=header_formatter + ) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Override this method to change how each multipart header + parameter is formatted. By default, this calls + :func:`format_multipart_header_param`. + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + + :meta public: + """ + return self.header_formatter(name, value) + + def _render_parts( + self, + header_parts: ( + dict[str, _TYPE_FIELD_VALUE | None] + | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] + ), + ) -> str: + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format + as `k1="v1"; k2="v2"; ...`. + """ + iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] + + parts = [] + if isinstance(header_parts, dict): + iterable = header_parts.items() + else: + iterable = header_parts + + for name, value in iterable: + if value is not None: + parts.append(self._render_part(name, value)) + + return "; ".join(parts) + + def render_headers(self) -> str: + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append(f"{sort_key}: {self.headers[sort_key]}") + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append(f"{header_name}: {header_value}") + + lines.append("\r\n") + return "\r\n".join(lines) + + def make_multipart( + self, + content_disposition: str | None = None, + content_type: str | None = None, + content_location: str | None = None, + ) -> None: + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_disposition: + The 'Content-Disposition' of the request body. Defaults to 'form-data' + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + content_disposition = (content_disposition or "form-data") + "; ".join( + [ + "", + self._render_parts( + (("name", self._name), ("filename", self._filename)) + ), + ] + ) + + self.headers["Content-Disposition"] = content_disposition + self.headers["Content-Type"] = content_type + self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py new file mode 100644 index 0000000000..14f70b05b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import binascii +import codecs +import os +import typing +from io import BytesIO + +from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField + +writer = codecs.lookup("utf-8")[3] + +_TYPE_FIELDS_SEQUENCE = typing.Sequence[ + typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] +] +_TYPE_FIELDS = typing.Union[ + _TYPE_FIELDS_SEQUENCE, + typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], +] + + +def choose_boundary() -> str: + """ + Our embarrassingly-simple replacement for mimetools.choose_boundary. + """ + return binascii.hexlify(os.urandom(16)).decode() + + +def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] + + if isinstance(fields, typing.Mapping): + iterable = fields.items() + else: + iterable = fields + + for field in iterable: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def encode_multipart_formdata( + fields: _TYPE_FIELDS, boundary: str | None = None +) -> tuple[bytes, str]: + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(f"--{boundary}\r\n".encode("latin-1")) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, str): + writer(body).write(data) + else: + body.write(data) + + body.write(b"\r\n") + + body.write(f"--{boundary}--\r\n".encode("latin-1")) + + content_type = f"multipart/form-data; boundary={boundary}" + + return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py new file mode 100644 index 0000000000..133e1d8f23 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from importlib.metadata import version + +__all__ = [ + "inject_into_urllib3", + "extract_from_urllib3", +] + +import typing + +orig_HTTPSConnection: typing.Any = None + + +def inject_into_urllib3() -> None: + # First check if h2 version is valid + h2_version = version("h2") + if not h2_version.startswith("4."): + raise ImportError( + "urllib3 v2 supports h2 version 4.x.x, currently " + f"the 'h2' module is compiled with {h2_version!r}. " + "See: https://github.com/urllib3/urllib3/issues/3290" + ) + + # Import here to avoid circular dependencies. + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + from .connection import HTTP2Connection + + global orig_HTTPSConnection + orig_HTTPSConnection = urllib3_connection.HTTPSConnection + + HTTPSConnectionPool.ConnectionCls = HTTP2Connection + urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] + + # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. + urllib3_util.ALPN_PROTOCOLS = ["h2"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] + + +def extract_from_urllib3() -> None: + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + + HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection + urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] + + urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py new file mode 100644 index 0000000000..0a026da0a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py @@ -0,0 +1,356 @@ +from __future__ import annotations + +import logging +import re +import threading +import types +import typing + +import h2.config +import h2.connection +import h2.events + +from .._base_connection import _TYPE_BODY +from .._collections import HTTPHeaderDict +from ..connection import HTTPSConnection, _get_default_user_agent +from ..exceptions import ConnectionError +from ..response import BaseHTTPResponse + +orig_HTTPSConnection = HTTPSConnection + +T = typing.TypeVar("T") + +log = logging.getLogger(__name__) + +RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") +RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") + + +def _is_legal_header_name(name: bytes) -> bool: + """ + "An implementation that validates fields according to the definitions in Sections + 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not + include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + + `http.client._is_legal_header_name` does not validate the field name according to the + HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. + + This does not allow for the `:` character in the header name, so should not + be used to validate pseudo-headers. + """ + return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) + + +def _is_illegal_header_value(value: bytes) -> bool: + """ + "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed + (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field + value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, + 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + """ + return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) + + +class _LockedObject(typing.Generic[T]): + """ + A wrapper class that hides a specific object behind a lock. + The goal here is to provide a simple way to protect access to an object + that cannot safely be simultaneously accessed from multiple threads. The + intended use of this class is simple: take hold of it with a context + manager, which returns the protected object. + """ + + __slots__ = ( + "lock", + "_obj", + ) + + def __init__(self, obj: T): + self.lock = threading.RLock() + self._obj = obj + + def __enter__(self) -> T: + self.lock.acquire() + return self._obj + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + self.lock.release() + + +class HTTP2Connection(HTTPSConnection): + def __init__( + self, host: str, port: int | None = None, **kwargs: typing.Any + ) -> None: + self._h2_conn = self._new_h2_conn() + self._h2_stream: int | None = None + self._headers: list[tuple[bytes, bytes]] = [] + + if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: + raise NotImplementedError("Proxies aren't supported with HTTP/2") + + super().__init__(host, port, **kwargs) + + if self._tunnel_host is not None: + raise NotImplementedError("Tunneling isn't supported with HTTP/2") + + def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: + config = h2.config.H2Configuration(client_side=True) + return _LockedObject(h2.connection.H2Connection(config=config)) + + def connect(self) -> None: + super().connect() + with self._h2_conn as conn: + conn.initiate_connection() + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + def putrequest( # type: ignore[override] + self, + method: str, + url: str, + **kwargs: typing.Any, + ) -> None: + """putrequest + This deviates from the HTTPConnection method signature since we never need to override + sending accept-encoding headers or the host header. + """ + if "skip_host" in kwargs: + raise NotImplementedError("`skip_host` isn't supported") + if "skip_accept_encoding" in kwargs: + raise NotImplementedError("`skip_accept_encoding` isn't supported") + + self._request_url = url or "/" + self._validate_path(url) # type: ignore[attr-defined] + + if ":" in self.host: + authority = f"[{self.host}]:{self.port or 443}" + else: + authority = f"{self.host}:{self.port or 443}" + + self._headers.append((b":scheme", b"https")) + self._headers.append((b":method", method.encode())) + self._headers.append((b":authority", authority.encode())) + self._headers.append((b":path", url.encode())) + + with self._h2_conn as conn: + self._h2_stream = conn.get_next_available_stream_id() + + def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] + # TODO SKIPPABLE_HEADERS from urllib3 are ignored. + header = header.encode() if isinstance(header, str) else header + header = header.lower() # A lot of upstream code uses capitalized headers. + if not _is_legal_header_name(header): + raise ValueError(f"Illegal header name {str(header)}") + + for value in values: + value = value.encode() if isinstance(value, str) else value + if _is_illegal_header_value(value): + raise ValueError(f"Illegal header value {str(value)}") + self._headers.append((header, value)) + + def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + conn.send_headers( + stream_id=self._h2_stream, + headers=self._headers, + end_stream=(message_body is None), + ) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + self._headers = [] # Reset headers for the next request. + + def send(self, data: typing.Any) -> None: + """Send data to the server. + `data` can be: `str`, `bytes`, an iterable, or file-like objects + that support a .read() method. + """ + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + if hasattr(data, "read"): # file-like objects + while True: + chunk = data.read(self.blocksize) + if not chunk: + break + if isinstance(chunk, str): + chunk = chunk.encode() + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + return + + if isinstance(data, str): # str -> bytes + data = data.encode() + + try: + if isinstance(data, bytes): + conn.send_data(self._h2_stream, data, end_stream=True) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + else: + for chunk in data: + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + except TypeError: + raise TypeError( + "`data` should be str, bytes, iterable, or file. got %r" + % type(data) + ) + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + raise NotImplementedError( + "HTTP/2 does not support setting up a tunnel through a proxy" + ) + + def getresponse( # type: ignore[override] + self, + ) -> HTTP2Response: + status = None + data = bytearray() + with self._h2_conn as conn: + end_stream = False + while not end_stream: + # TODO: Arbitrary read value. + if received_data := self.sock.recv(65535): + events = conn.receive_data(received_data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + headers = HTTPHeaderDict() + for header, value in event.headers: + if header == b":status": + status = int(value.decode()) + else: + headers.add( + header.decode("ascii"), value.decode("ascii") + ) + + elif isinstance(event, h2.events.DataReceived): + data += event.data + conn.acknowledge_received_data( + event.flow_controlled_length, event.stream_id + ) + + elif isinstance(event, h2.events.StreamEnded): + end_stream = True + + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + assert status is not None + return HTTP2Response( + status=status, + headers=headers, + request_url=self._request_url, + data=bytes(data), + ) + + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + **kwargs: typing.Any, + ) -> None: + """Send an HTTP/2 request""" + if "chunked" in kwargs: + # TODO this is often present from upstream. + # raise NotImplementedError("`chunked` isn't supported with HTTP/2") + pass + + if self.sock is not None: + self.sock.settimeout(self.timeout) + + self.putrequest(method, url) + + headers = headers or {} + for k, v in headers.items(): + if k.lower() == "transfer-encoding" and v == "chunked": + continue + else: + self.putheader(k, v) + + if b"user-agent" not in dict(self._headers): + self.putheader(b"user-agent", _get_default_user_agent()) + + if body: + self.endheaders(message_body=body) + self.send(body) + else: + self.endheaders() + + def close(self) -> None: + with self._h2_conn as conn: + try: + conn.close_connection() + if data := conn.data_to_send(): + self.sock.sendall(data) + except Exception: + pass + + # Reset all our HTTP/2 connection state. + self._h2_conn = self._new_h2_conn() + self._h2_stream = None + self._headers = [] + + super().close() + + +class HTTP2Response(BaseHTTPResponse): + # TODO: This is a woefully incomplete response object, but works for non-streaming. + def __init__( + self, + status: int, + headers: HTTPHeaderDict, + request_url: str, + data: bytes, + decode_content: bool = False, # TODO: support decoding + ) -> None: + super().__init__( + status=status, + headers=headers, + # Following CPython, we map HTTP versions to major * 10 + minor integers + version=20, + version_string="HTTP/2", + # No reason phrase in HTTP/2 + reason=None, + decode_content=decode_content, + request_url=request_url, + ) + self._data = data + self.length_remaining = 0 + + @property + def data(self) -> bytes: + return self._data + + def get_redirect_location(self) -> None: + return None + + def close(self) -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py new file mode 100644 index 0000000000..9ea900764f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import threading + + +class _HTTP2ProbeCache: + __slots__ = ( + "_lock", + "_cache_locks", + "_cache_values", + ) + + def __init__(self) -> None: + self._lock = threading.Lock() + self._cache_locks: dict[tuple[str, int], threading.RLock] = {} + self._cache_values: dict[tuple[str, int], bool | None] = {} + + def acquire_and_get(self, host: str, port: int) -> bool | None: + # By the end of this block we know that + # _cache_[values,locks] is available. + value = None + with self._lock: + key = (host, port) + try: + value = self._cache_values[key] + # If it's a known value we return right away. + if value is not None: + return value + except KeyError: + self._cache_locks[key] = threading.RLock() + self._cache_values[key] = None + + # If the value is unknown, we acquire the lock to signal + # to the requesting thread that the probe is in progress + # or that the current thread needs to return their findings. + key_lock = self._cache_locks[key] + key_lock.acquire() + try: + # If the by the time we get the lock the value has been + # updated we want to return the updated value. + value = self._cache_values[key] + + # In case an exception like KeyboardInterrupt is raised here. + except BaseException as e: # Defensive: + assert not isinstance(e, KeyError) # KeyError shouldn't be possible. + key_lock.release() + raise + + return value + + def set_and_release( + self, host: str, port: int, supports_http2: bool | None + ) -> None: + key = (host, port) + key_lock = self._cache_locks[key] + with key_lock: # Uses an RLock, so can be locked again from same thread. + if supports_http2 is None and self._cache_values[key] is not None: + raise ValueError( + "Cannot reset HTTP/2 support for origin after value has been set." + ) # Defensive: not expected in normal usage + + self._cache_values[key] = supports_http2 + key_lock.release() + + def _values(self) -> dict[tuple[str, int], bool | None]: + """This function is for testing purposes only. Gets the current state of the probe cache""" + with self._lock: + return {k: v for k, v in self._cache_values.items()} + + def _reset(self) -> None: + """This function is for testing purposes only. Reset the cache values""" + with self._lock: + self._cache_locks = {} + self._cache_values = {} + + +_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() + +set_and_release = _HTTP2_PROBE_CACHE.set_and_release +acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get +_values = _HTTP2_PROBE_CACHE._values +_reset = _HTTP2_PROBE_CACHE._reset + +__all__ = [ + "set_and_release", + "acquire_and_get", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py new file mode 100644 index 0000000000..8f2c56745c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py @@ -0,0 +1,653 @@ +from __future__ import annotations + +import functools +import logging +import typing +import warnings +from types import TracebackType +from urllib.parse import urljoin + +from ._collections import HTTPHeaderDict, RecentlyUsedContainer +from ._request_methods import RequestMethods +from .connection import ProxyConfig +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme +from .exceptions import ( + LocationValueError, + MaxRetryError, + ProxySchemeUnknown, + URLSchemeUnknown, +) +from .response import BaseHTTPResponse +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.proxy import connection_requires_http_tunnel +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import Url, parse_url + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + +__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] + + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ( + "key_file", + "cert_file", + "cert_reqs", + "ca_certs", + "ca_cert_data", + "ssl_version", + "ssl_minimum_version", + "ssl_maximum_version", + "ca_cert_dir", + "ssl_context", + "key_password", + "server_hostname", +) +# Default value for `blocksize` - a new parameter introduced to +# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 +_DEFAULT_BLOCKSIZE = 16384 + + +class PoolKey(typing.NamedTuple): + """ + All known keyword arguments that could be provided to the pool manager, its + pools, or the underlying connections. + + All custom key schemes should include the fields in this key at a minimum. + """ + + key_scheme: str + key_host: str + key_port: int | None + key_timeout: Timeout | float | int | None + key_retries: Retry | bool | int | None + key_block: bool | None + key_source_address: tuple[str, int] | None + key_key_file: str | None + key_key_password: str | None + key_cert_file: str | None + key_cert_reqs: str | None + key_ca_certs: str | None + key_ca_cert_data: str | bytes | None + key_ssl_version: int | str | None + key_ssl_minimum_version: ssl.TLSVersion | None + key_ssl_maximum_version: ssl.TLSVersion | None + key_ca_cert_dir: str | None + key_ssl_context: ssl.SSLContext | None + key_maxsize: int | None + key_headers: frozenset[tuple[str, str]] | None + key__proxy: Url | None + key__proxy_headers: frozenset[tuple[str, str]] | None + key__proxy_config: ProxyConfig | None + key_socket_options: _TYPE_SOCKET_OPTIONS | None + key__socks_options: frozenset[tuple[str, str]] | None + key_assert_hostname: bool | str | None + key_assert_fingerprint: str | None + key_server_hostname: str | None + key_blocksize: int | None + + +def _default_key_normalizer( + key_class: type[PoolKey], request_context: dict[str, typing.Any] +) -> PoolKey: + """ + Create a pool key out of a request context dictionary. + + According to RFC 3986, both the scheme and host are case-insensitive. + Therefore, this function normalizes both before constructing the pool + key for an HTTPS request. If you wish to change this behaviour, provide + alternate callables to ``key_fn_by_scheme``. + + :param key_class: + The class to use when constructing the key. This should be a namedtuple + with the ``scheme`` and ``host`` keys at a minimum. + :type key_class: namedtuple + :param request_context: + A dictionary-like object that contain the context for a request. + :type request_context: dict + + :return: A namedtuple that can be used as a connection pool key. + :rtype: PoolKey + """ + # Since we mutate the dictionary, make a copy first + context = request_context.copy() + context["scheme"] = context["scheme"].lower() + context["host"] = context["host"].lower() + + # These are both dictionaries and need to be transformed into frozensets + for key in ("headers", "_proxy_headers", "_socks_options"): + if key in context and context[key] is not None: + context[key] = frozenset(context[key].items()) + + # The socket_options key may be a list and needs to be transformed into a + # tuple. + socket_opts = context.get("socket_options") + if socket_opts is not None: + context["socket_options"] = tuple(socket_opts) + + # Map the kwargs to the names in the namedtuple - this is necessary since + # namedtuples can't have fields starting with '_'. + for key in list(context.keys()): + context["key_" + key] = context.pop(key) + + # Default to ``None`` for keys missing from the context + for field in key_class._fields: + if field not in context: + context[field] = None + + # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context + if context.get("key_blocksize") is None: + context["key_blocksize"] = _DEFAULT_BLOCKSIZE + + return key_class(**context) + + +#: A dictionary that maps a scheme to a callable that creates a pool key. +#: This can be used to alter the way pool keys are constructed, if desired. +#: Each PoolManager makes a copy of this dictionary so they can be configured +#: globally here, or individually on the instance. +key_fn_by_scheme = { + "http": functools.partial(_default_key_normalizer, PoolKey), + "https": functools.partial(_default_key_normalizer, PoolKey), +} + +pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \\**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example: + + .. code-block:: python + + import urllib3 + + http = urllib3.PoolManager(num_pools=2) + + resp1 = http.request("GET", "https://google.com/") + resp2 = http.request("GET", "https://google.com/mail") + resp3 = http.request("GET", "https://yahoo.com/") + + print(len(http.pools)) + # 2 + + """ + + proxy: Url | None = None + proxy_config: ProxyConfig | None = None + + def __init__( + self, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + super().__init__(headers) + # PoolManager handles redirects itself in PoolManager.urlopen(). + # It always passes redirect=False to the underlying connection pool to + # suppress per-pool redirect handling. If the user supplied a non-Retry + # value (int/bool/etc) for retries and we let the pool normalize it + # while redirect=False, the resulting Retry object would have redirect + # handling disabled, which can interfere with PoolManager's own + # redirect logic. Normalize here so redirects remain governed solely by + # PoolManager logic. + if "retries" in connection_pool_kw: + retries = connection_pool_kw["retries"] + if not isinstance(retries, Retry): + retries = Retry.from_int(retries) + connection_pool_kw = connection_pool_kw.copy() + connection_pool_kw["retries"] = retries + self.connection_pool_kw = connection_pool_kw + + self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] + self.pools = RecentlyUsedContainer(num_pools) + + # Locally set the pool classes and keys so other PoolManagers can + # override them. + self.pool_classes_by_scheme = pool_classes_by_scheme + self.key_fn_by_scheme = key_fn_by_scheme.copy() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.clear() + # Return False to re-raise any potential exceptions + return False + + def _new_pool( + self, + scheme: str, + host: str, + port: int, + request_context: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and + any additional pool keyword arguments. + + If ``request_context`` is provided, it is provided as keyword arguments + to the pool class used. This method is used to actually create the + connection pools handed out by :meth:`connection_from_url` and + companion methods. It is intended to be overridden for customization. + """ + pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] + if request_context is None: + request_context = self.connection_pool_kw.copy() + + # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly + # set to 'None' in the request_context. + if request_context.get("blocksize") is None: + request_context["blocksize"] = _DEFAULT_BLOCKSIZE + + # Although the context has everything necessary to create the pool, + # this function has historically only used the scheme, host, and port + # in the positional args. When an API change is acceptable these can + # be removed. + for key in ("scheme", "host", "port"): + request_context.pop(key, None) + + if scheme == "http": + for kw in SSL_KEYWORDS: + request_context.pop(kw, None) + + return pool_cls(host, port, **request_context) + + def clear(self) -> None: + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is + provided, it is merged with the instance's ``connection_pool_kw`` + variable and used to create the new connection pool, if one is + needed. + """ + + if not host: + raise LocationValueError("No host specified.") + + request_context = self._merge_pool_kwargs(pool_kwargs) + request_context["scheme"] = scheme or "http" + if not port: + port = port_by_scheme.get(request_context["scheme"].lower(), 80) + request_context["port"] = port + request_context["host"] = host + + return self.connection_from_context(request_context) + + def connection_from_context( + self, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. + + ``request_context`` must at least contain the ``scheme`` key and its + value must be a key in ``key_fn_by_scheme`` instance variable. + """ + if "strict" in request_context: + warnings.warn( + "The 'strict' parameter is no longer needed on Python 3+. " + "This will raise an error in urllib3 v3.0.", + FutureWarning, + ) + request_context.pop("strict") + + scheme = request_context["scheme"].lower() + pool_key_constructor = self.key_fn_by_scheme.get(scheme) + if not pool_key_constructor: + raise URLSchemeUnknown(scheme) + pool_key = pool_key_constructor(request_context) + + return self.connection_from_pool_key(pool_key, request_context=request_context) + + def connection_from_pool_key( + self, pool_key: PoolKey, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. + + ``pool_key`` should be a namedtuple that only contains immutable + objects. At a minimum it must have the ``scheme``, ``host``, and + ``port`` fields. + """ + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + scheme = request_context["scheme"] + host = request_context["host"] + port = request_context["port"] + pool = self._new_pool(scheme, host, port, request_context=request_context) + self.pools[pool_key] = pool + + return pool + + def connection_from_url( + self, url: str, pool_kwargs: dict[str, typing.Any] | None = None + ) -> HTTPConnectionPool: + """ + Similar to :func:`urllib3.connectionpool.connection_from_url`. + + If ``pool_kwargs`` is not provided and a new pool needs to be + constructed, ``self.connection_pool_kw`` is used to initialize + the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` + is provided, it is used instead. Note that if a new pool does not + need to be created for the request, the provided ``pool_kwargs`` are + not used. + """ + u = parse_url(url) + return self.connection_from_host( + u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs + ) + + def _merge_pool_kwargs( + self, override: dict[str, typing.Any] | None + ) -> dict[str, typing.Any]: + """ + Merge a dictionary of override values for self.connection_pool_kw. + + This does not modify self.connection_pool_kw and returns a new dict. + Any keys in the override dictionary with a value of ``None`` are + removed from the merged dictionary. + """ + base_pool_kwargs = self.connection_pool_kw.copy() + if override: + for key, value in override.items(): + if value is None: + try: + del base_pool_kwargs[key] + except KeyError: + pass + else: + base_pool_kwargs[key] = value + return base_pool_kwargs + + def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: + """ + Indicates if the proxy requires the complete destination URL in the + request. Normally this is only needed when not using an HTTP CONNECT + tunnel. + """ + if self.proxy is None: + return False + + return not connection_requires_http_tunnel( + self.proxy, self.proxy_config, parsed_url.scheme + ) + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + """ + Same as :meth:`urllib3.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + + if u.scheme is None: + warnings.warn( + "URLs without a scheme (ie 'https://') are deprecated and will raise an error " + "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " + "start with 'https://' or 'http://'. Read more in this issue: " + "https://github.com/urllib3/urllib3/issues/2920", + category=FutureWarning, + stacklevel=2, + ) + + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw["assert_same_host"] = False + kw["redirect"] = False + + if "headers" not in kw: + kw["headers"] = self.headers + + if self._proxy_requires_url_absolute_form(u): + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + kw["body"] = None + kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + + retries = kw.get("retries", response.retries) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # conn.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not conn.is_same_host( + redirect_location + ): + new_headers = kw["headers"].copy() + for header in kw["headers"]: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + kw["headers"] = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + kw["retries"] = retries + kw["redirect"] = redirect + + log.info("Redirecting %s -> %s", url, redirect_location) + + response.drain_conn() + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary containing headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + :param proxy_ssl_context: + The proxy SSL context is used to establish the TLS connection to the + proxy when using HTTPS proxies. + + :param use_forwarding_for_https: + (Defaults to False) If set to True will forward requests to the HTTPS + proxy to be made on behalf of the client instead of creating a TLS + tunnel via the CONNECT method. **Enabling this flag means that request + and response headers and content will be visible from the HTTPS proxy** + whereas tunneling keeps request and response headers and content + private. IP address, target hostname, SNI, and port are always visible + to an HTTPS proxy even when this flag is disabled. + + :param proxy_assert_hostname: + The hostname of the certificate to verify against. + + :param proxy_assert_fingerprint: + The fingerprint of the certificate to verify against. + + Example: + + .. code-block:: python + + import urllib3 + + proxy = urllib3.ProxyManager("https://localhost:3128/") + + resp1 = proxy.request("GET", "http://google.com/") + resp2 = proxy.request("GET", "http://httpbin.org/") + + # One pool was shared by both plain HTTP requests. + print(len(proxy.pools)) + # 1 + + resp3 = proxy.request("GET", "https://httpbin.org/") + resp4 = proxy.request("GET", "https://twitter.com/") + + # A separate pool was added for each HTTPS target. + print(len(proxy.pools)) + # 3 + + """ + + def __init__( + self, + proxy_url: str, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + proxy_headers: typing.Mapping[str, str] | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + use_forwarding_for_https: bool = False, + proxy_assert_hostname: None | str | typing.Literal[False] = None, + proxy_assert_fingerprint: str | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + if isinstance(proxy_url, HTTPConnectionPool): + str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" + else: + str_proxy_url = proxy_url + proxy = parse_url(str_proxy_url) + + if proxy.scheme not in ("http", "https"): + raise ProxySchemeUnknown(proxy.scheme) + + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + self.proxy_ssl_context = proxy_ssl_context + self.proxy_config = ProxyConfig( + proxy_ssl_context, + use_forwarding_for_https, + proxy_assert_hostname, + proxy_assert_fingerprint, + ) + + connection_pool_kw["_proxy"] = self.proxy + connection_pool_kw["_proxy_headers"] = self.proxy_headers + connection_pool_kw["_proxy_config"] = self.proxy_config + + super().__init__(num_pools, headers, **connection_pool_kw) + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + if scheme == "https": + return super().connection_from_host( + host, port, scheme, pool_kwargs=pool_kwargs + ) + + return super().connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] + ) + + def _set_proxy_headers( + self, url: str, headers: typing.Mapping[str, str] | None = None + ) -> typing.Mapping[str, str]: + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {"Accept": "*/*"} + + netloc = parse_url(url).netloc + if netloc: + headers_["Host"] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): + # For connections using HTTP CONNECT, httplib sets the necessary + # headers on the CONNECT to the proxy. If we're not using CONNECT, + # we'll definitely need to set 'Host' at the very least. + headers = kw.get("headers", self.headers) + kw["headers"] = self._set_proxy_headers(url, headers) + + return super().urlopen(method, url, redirect=redirect, **kw) + + +def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: + return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed new file mode 100644 index 0000000000..5f3ea3d919 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed @@ -0,0 +1,2 @@ +# Instruct type checkers to look for inline type annotations in this package. +# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py new file mode 100644 index 0000000000..e9246b75e3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py @@ -0,0 +1,1493 @@ +from __future__ import annotations + +import collections +import io +import json as _json +import logging +import socket +import sys +import typing +import warnings +import zlib +from contextlib import contextmanager +from http.client import HTTPMessage as _HttplibHTTPMessage +from http.client import HTTPResponse as _HttplibHTTPResponse +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from ._base_connection import BaseHTTPConnection + +try: + try: + import brotlicffi as brotli # type: ignore[import-not-found] + except ImportError: + import brotli # type: ignore[import-not-found] +except ImportError: + brotli = None + +from . import util +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .connection import BaseSSLError, HTTPConnection, HTTPException +from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, + DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, + InvalidHeader, + ProtocolError, + ReadTimeoutError, + ResponseNotChunked, + SSLError, +) +from .util.response import is_fp_closed, is_response_to_head +from .util.retry import Retry + +if typing.TYPE_CHECKING: + from .connectionpool import HTTPConnectionPool + +log = logging.getLogger(__name__) + + +class ContentDecoder: + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + raise NotImplementedError() + + @property + def has_unconsumed_tail(self) -> bool: + raise NotImplementedError() + + def flush(self) -> bytes: + raise NotImplementedError() + + +class DeflateDecoder(ContentDecoder): + def __init__(self) -> None: + self._first_try = True + self._first_try_data = b"" + self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + data = self._unfed_data + data + self._unfed_data = b"" + if not data and not self._obj.unconsumed_tail: + return data + original_max_length = max_length + if original_max_length < 0: + max_length = 0 + elif original_max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unfed_data = data + return b"" + + # Subsequent calls always reuse `self._obj`. zlib requires + # passing the unconsumed tail if decompression is to continue. + if not self._first_try: + return self._obj.decompress( + self._obj.unconsumed_tail + data, max_length=max_length + ) + + # First call tries with RFC 1950 ZLIB format. + self._first_try_data += data + try: + decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False + self._first_try_data = b"" + return decompressed + # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress( + self._first_try_data, max_length=original_max_length + ) + finally: + self._first_try_data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unfed_data) or ( + bool(self._obj.unconsumed_tail) and not self._first_try + ) + + def flush(self) -> bytes: + return self._obj.flush() + + +class GzipDecoderState: + FIRST_MEMBER = 0 + OTHER_MEMBERS = 1 + SWALLOW_DATA = 2 + + +class GzipDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER + self._unconsumed_tail = b"" + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() + if self._state == GzipDecoderState.SWALLOW_DATA: + return bytes(ret) + + if max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unconsumed_tail += data + return b"" + + # zlib requires passing the unconsumed tail to the subsequent + # call if decompression is to continue. + data = self._unconsumed_tail + data + if not data and self._obj.eof: + return bytes(ret) + + while True: + try: + ret += self._obj.decompress( + data, max_length=max(max_length - len(ret), 0) + ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA + self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise + + self._unconsumed_tail = data = ( + self._obj.unconsumed_tail or self._obj.unused_data + ) + if max_length > 0 and len(ret) >= max_length: + break + + if not data: + return bytes(ret) + # When the end of a gzip member is reached, a new decompressor + # must be created for unused (possibly future) data. + if self._obj.eof: + self._state = GzipDecoderState.OTHER_MEMBERS + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + + return bytes(ret) + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unconsumed_tail) + + def flush(self) -> bytes: + return self._obj.flush() + + +if brotli is not None: + + class BrotliDecoder(ContentDecoder): + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' + def __init__(self) -> None: + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): + setattr(self, "_decompress", self._obj.decompress) + else: + setattr(self, "_decompress", self._obj.process) + + # Requires Brotli >= 1.2.0 for `output_buffer_limit`. + def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: + raise NotImplementedError() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + try: + if max_length > 0: + return self._decompress(data, output_buffer_limit=max_length) + else: + return self._decompress(data) + except TypeError: + # Fallback for Brotli/brotlicffi/brotlipy versions without + # the `output_buffer_limit` parameter. + warnings.warn( + "Brotli >= 1.2.0 is required to prevent decompression bombs.", + DependencyWarning, + ) + return self._decompress(data) + + @property + def has_unconsumed_tail(self) -> bool: + try: + return not self._obj.can_accept_more_data() + except AttributeError: + return False + + def flush(self) -> bytes: + if hasattr(self._obj, "flush"): + return self._obj.flush() # type: ignore[no-any-return] + return b"" + + +try: + if sys.version_info >= (3, 14): + from compression import zstd + else: + from backports import zstd +except ImportError: + HAS_ZSTD = False +else: + HAS_ZSTD = True + + class ZstdDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zstd.ZstdDecompressor() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if not data and not self.has_unconsumed_tail: + return b"" + if self._obj.eof: + data = self._obj.unused_data + data + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress(data, max_length=max_length) + length = len(part) + data_parts = [part] + # Every loop iteration is supposed to read data from a separate frame. + # The loop breaks when: + # - enough data is read; + # - no more unused data is available; + # - end of the last read frame has not been reached (i.e., + # more data has to be fed). + while ( + self._obj.eof + and self._obj.unused_data + and (max_length < 0 or length < max_length) + ): + unused_data = self._obj.unused_data + if not self._obj.needs_input: + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress( + unused_data, + max_length=(max_length - length) if max_length > 0 else -1, + ) + if part_length := len(part): + data_parts.append(part) + length += part_length + elif self._obj.needs_input: + break + return b"".join(data_parts) + + @property + def has_unconsumed_tail(self) -> bool: + return not (self._obj.needs_input or self._obj.eof) or bool( + self._obj.unused_data + ) + + def flush(self) -> bytes: + if not self._obj.eof: + raise DecodeError("Zstandard data is incomplete") + return b"" + + +class MultiDecoder(ContentDecoder): + """ + From RFC7231: + If one or more encodings have been applied to a representation, the + sender that applied the encodings MUST generate a Content-Encoding + header field that lists the content codings in the order in which + they were applied. + """ + + # Maximum allowed number of chained HTTP encodings in the + # Content-Encoding header. + max_decode_links = 5 + + def __init__(self, modes: str) -> None: + encodings = [m.strip() for m in modes.split(",")] + if len(encodings) > self.max_decode_links: + raise DecodeError( + "Too many content encodings in the chain: " + f"{len(encodings)} > {self.max_decode_links}" + ) + self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self) -> bytes: + return self._decoders[0].flush() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if max_length <= 0: + for d in reversed(self._decoders): + data = d.decompress(data) + return data + + ret = bytearray() + # Every while loop iteration goes through all decoders once. + # It exits when enough data is read or no more data can be read. + # It is possible that the while loop iteration does not produce + # any data because we retrieve up to `max_length` from every + # decoder, and the amount of bytes may be insufficient for the + # next decoder to produce enough/any output. + while True: + any_data = False + for d in reversed(self._decoders): + data = d.decompress(data, max_length=max_length - len(ret)) + if data: + any_data = True + # We should not break when no data is returned because + # next decoders may produce data even with empty input. + ret += data + if not any_data or len(ret) >= max_length: + return bytes(ret) + data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return any(d.has_unconsumed_tail for d in self._decoders) + + +def _get_decoder(mode: str) -> ContentDecoder: + if "," in mode: + return MultiDecoder(mode) + + # According to RFC 9110 section 8.4.1.3, recipients should + # consider x-gzip equivalent to gzip + if mode in ("gzip", "x-gzip"): + return GzipDecoder() + + if brotli is not None and mode == "br": + return BrotliDecoder() + + if HAS_ZSTD and mode == "zstd": + return ZstdDecoder() + + return DeflateDecoder() + + +class BytesQueueBuffer: + """Memory-efficient bytes buffer + + To return decoded data in read() and still follow the BufferedIOBase API, we need a + buffer to always return the correct amount of bytes. + + This buffer should be filled using calls to put() + + Our maximum memory usage is determined by the sum of the size of: + + * self.buffer, which contains the full data + * the largest chunk that we will copy in get() + """ + + def __init__(self) -> None: + self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() + self._size: int = 0 + + def __len__(self) -> int: + return self._size + + def put(self, data: bytes) -> None: + self.buffer.append(data) + self._size += len(data) + + def get(self, n: int) -> bytes: + if n == 0: + return b"" + elif not self.buffer: + raise RuntimeError("buffer is empty") + elif n < 0: + raise ValueError("n should be > 0") + + if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): + self._size -= n + return self.buffer.popleft() + + fetched = 0 + ret = io.BytesIO() + while fetched < n: + remaining = n - fetched + chunk = self.buffer.popleft() + chunk_length = len(chunk) + if remaining < chunk_length: + chunk = memoryview(chunk) + left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] + ret.write(left_chunk) + self.buffer.appendleft(right_chunk) + self._size -= remaining + break + else: + ret.write(chunk) + self._size -= chunk_length + fetched += chunk_length + + if not self.buffer: + break + + return ret.getvalue() + + def get_all(self) -> bytes: + buffer = self.buffer + if not buffer: + assert self._size == 0 + return b"" + if len(buffer) == 1: + result = buffer.pop() + if isinstance(result, memoryview): + result = result.tobytes() + else: + ret = io.BytesIO() + ret.writelines(buffer.popleft() for _ in range(len(buffer))) + result = ret.getvalue() + self._size = 0 + return result + + +class BaseHTTPResponse(io.IOBase): + CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] + if brotli is not None: + CONTENT_DECODERS += ["br"] + if HAS_ZSTD: + CONTENT_DECODERS += ["zstd"] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + + if HAS_ZSTD: + DECODER_ERROR_CLASSES += (zstd.ZstdError,) + + def __init__( + self, + *, + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int, + version: int, + version_string: str, + reason: str | None, + decode_content: bool, + request_url: str | None, + retries: Retry | None = None, + ) -> None: + if isinstance(headers, HTTPHeaderDict): + self.headers = headers + else: + self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] + self.status = status + self.version = version + self.version_string = version_string + self.reason = reason + self.decode_content = decode_content + self._has_decoded_content = False + self._request_url: str | None = request_url + self.retries = retries + + self.chunked = False + tr_enc = self.headers.get("transfer-encoding", "").lower() + # Don't incur the penalty of creating a list and then discarding it + encodings = (enc.strip() for enc in tr_enc.split(",")) + if "chunked" in encodings: + self.chunked = True + + self._decoder: ContentDecoder | None = None + self.length_remaining: int | None + + def get_redirect_location(self) -> str | None | typing.Literal[False]: + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get("location") + return False + + @property + def data(self) -> bytes: + raise NotImplementedError() + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + @property + def url(self) -> str | None: + raise NotImplementedError() + + @url.setter + def url(self, url: str | None) -> None: + raise NotImplementedError() + + @property + def connection(self) -> BaseHTTPConnection | None: + raise NotImplementedError() + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + if retries is not None and retries.history: + self.url = retries.history[-1].redirect_location + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + raise NotImplementedError() + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + raise NotImplementedError() + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def release_conn(self) -> None: + raise NotImplementedError() + + def drain_conn(self) -> None: + raise NotImplementedError() + + def shutdown(self) -> None: + raise NotImplementedError() + + def close(self) -> None: + raise NotImplementedError() + + def _init_decoder(self) -> None: + """ + Set-up the _decoder attribute if necessary. + """ + # Note: content-encoding value should be case-insensitive, per RFC 7230 + # Section 3.2 + content_encoding = self.headers.get("content-encoding", "").lower() + if self._decoder is None: + if content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + elif "," in content_encoding: + encodings = [ + e.strip() + for e in content_encoding.split(",") + if e.strip() in self.CONTENT_DECODERS + ] + if encodings: + self._decoder = _get_decoder(content_encoding) + + def _decode( + self, + data: bytes, + decode_content: bool | None, + flush_decoder: bool, + max_length: int | None = None, + ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + if max_length is None or flush_decoder: + max_length = -1 + + try: + if self._decoder: + data = self._decoder.decompress(data, max_length=max_length) + self._has_decoded_content = True + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, + e, + ) from e + if flush_decoder: + data += self._flush_decoder() + + return data + + def _flush_decoder(self) -> bytes: + """ + Flushes the decoder. Should only be called if the decoder is actually + being used. + """ + if self._decoder: + return self._decoder.decompress(b"") + self._decoder.flush() + return b"" + + # Compatibility methods for `io` module + def readinto(self, b: bytearray | memoryview[int]) -> int: + temp = self.read(len(b)) + if len(temp) == 0: + return 0 + else: + b[: len(temp)] = temp + return len(temp) + + # Methods used by dependent libraries + def getheaders(self) -> HTTPHeaderDict: + return self.headers + + def getheader(self, name: str, default: str | None = None) -> str | None: + return self.headers.get(name, default) + + # Compatibility method for http.cookiejar + def info(self) -> HTTPHeaderDict: + return self.headers + + def geturl(self) -> str | None: + return self.url + + +class HTTPResponse(BaseHTTPResponse): + """ + HTTP Response container. + + Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. This + class is also compatible with the Python standard library's :mod:`io` + module, and can hence be treated as a readable object in the context of that + framework. + + Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param original_response: + When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + + :param retries: + The retries contains the last :class:`~urllib3.util.retry.Retry` that + was used during the request. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + + def __init__( + self, + body: _TYPE_BODY = "", + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int = 0, + version: int = 0, + version_string: str = "HTTP/?", + reason: str | None = None, + preload_content: bool = True, + decode_content: bool = True, + original_response: _HttplibHTTPResponse | None = None, + pool: HTTPConnectionPool | None = None, + connection: HTTPConnection | None = None, + msg: _HttplibHTTPMessage | None = None, + retries: Retry | None = None, + enforce_content_length: bool = True, + request_method: str | None = None, + request_url: str | None = None, + auto_close: bool = True, + sock_shutdown: typing.Callable[[int], None] | None = None, + ) -> None: + super().__init__( + headers=headers, + status=status, + version=version, + version_string=version_string, + reason=reason, + decode_content=decode_content, + request_url=request_url, + retries=retries, + ) + + self.enforce_content_length = enforce_content_length + self.auto_close = auto_close + + self._body = None + self._uncached_read_occurred = False + self._fp: _HttplibHTTPResponse | None = None + self._original_response = original_response + self._fp_bytes_read = 0 + self.msg = msg + + if body and isinstance(body, (str, bytes)): + self._body = body + + self._pool = pool + self._connection = connection + + if hasattr(body, "read"): + self._fp = body # type: ignore[assignment] + self._sock_shutdown = sock_shutdown + + # Are we using the chunked-style of transfer encoding? + self.chunk_left: int | None = None + + # Determine length of response + self.length_remaining = self._init_length(request_method) + + # Used to return the correct amount of bytes for partial read()s + self._decoded_buffer = BytesQueueBuffer() + + # If requested, preload the body. + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + """ + Read and discard any remaining HTTP response data in the response connection. + + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: + self._raw_read() + except (HTTPError, OSError, BaseSSLError, HTTPException): + pass + if self._has_decoded_content: + # `_raw_read` skips decompression, so we should clean up the + # decoder to avoid keeping unnecessary data in memory. + self._decoded_buffer = BytesQueueBuffer() + self._decoder = None + + @property + def data(self) -> bytes: + # For backwards-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body # type: ignore[return-value] + + if self._fp: + return self.read(cache_content=True) + + return None # type: ignore[return-value] + + @property + def connection(self) -> HTTPConnection | None: + return self._connection + + def isclosed(self) -> bool: + return is_fp_closed(self._fp) + + def tell(self) -> int: + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:`HTTPResponse.read` + if bytes are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def _init_length(self, request_method: str | None) -> int | None: + """ + Set initial length value for Response content if available. + """ + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + if self.chunked: + # This Response will fail with an IncompleteRead if it can't be + # received as chunked. This method falls back to attempt reading + # the response before raising an exception. + log.warning( + "Received response with both Content-Length and " + "Transfer-Encoding set. This is expressly forbidden " + "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " + "attempting to process response as Transfer-Encoding: " + "chunked." + ) + return None + + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Convert status to int for comparison + # In some cases, httplib returns a status of "_UNKNOWN" + try: + status = int(self.status) + except ValueError: + status = 0 + + # Check for responses that shouldn't include a body + if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": + length = 0 + + return length + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch low-level python exceptions, instead re-raising urllib3 + variants, so that low-level exceptions are not leaked in the + high-level api. + + On exit, release the connection back to the pool. + """ + clean_exit = False + + try: + try: + yield + + except SocketTimeout as e: + # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but + # there is yet no clean way to get at it from this context. + raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + + except BaseSSLError as e: + # SSL errors related to framing/MAC get wrapped and reraised here + raise SSLError(e) from e + + except IncompleteRead as e: + if ( + e.expected is not None + and e.partial is not None + and e.expected == -e.partial + ): + arg = "Response may not contain content." + else: + arg = f"Connection broken: {e!r}" + raise ProtocolError(arg, e) from e + + except (HTTPException, OSError) as e: + raise ProtocolError(f"Connection broken: {e!r}", e) from e + + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now to ensure that the connection is + # released back to the pool. + if self._original_response: + self._original_response.close() + + # Closing the response may not actually be sufficient to close + # everything, so if we have a hold of the connection close that + # too. + if self._connection: + self._connection.close() + + # If we hold the original response but it's closed now, we should + # return the connection back to the pool. + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def _fp_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Read a response with the thought that reading the number of bytes + larger than can fit in a 32-bit int at a time via SSL in some + known cases leads to an overflow error that has to be prevented + if `amt` or `self.length_remaining` indicate that a problem may + happen. + + This happens to urllib3 injected with pyOpenSSL-backed SSL-support. + """ + assert self._fp + c_int_max = 2**31 - 1 + if ( + (amt and amt > c_int_max) + or ( + amt is None + and self.length_remaining + and self.length_remaining > c_int_max + ) + ) and util.IS_PYOPENSSL: + if read1: + return self._fp.read1(c_int_max) + buffer = io.BytesIO() + # Besides `max_chunk_amt` being a maximum chunk size, it + # affects memory overhead of reading a response by this + # method in CPython. + # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum + # chunk size that does not lead to an overflow error, but + # 256 MiB is a compromise. + max_chunk_amt = 2**28 + while amt is None or amt != 0: + if amt is not None: + chunk_amt = min(amt, max_chunk_amt) + amt -= chunk_amt + else: + chunk_amt = max_chunk_amt + data = self._fp.read(chunk_amt) + if not data: + break + buffer.write(data) + del data # to reduce peak memory usage by `max_chunk_amt`. + return buffer.getvalue() + elif read1: + return self._fp.read1(amt) if amt is not None else self._fp.read1() + else: + # StringIO doesn't like amt=None + return self._fp.read(amt) if amt is not None else self._fp.read() + + def _raw_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Reads `amt` of bytes from the socket. + """ + if self._fp is None: + return None # type: ignore[return-value] + + fp_closed = getattr(self._fp, "closed", False) + + with self._error_catcher(): + data = self._fp_read(amt, read1=read1) if not fp_closed else b"" + if amt is not None and amt != 0 and not data: + # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do + # not properly close the connection in all cases. There is + # no harm in redundantly calling close. + self._fp.close() + if ( + self.enforce_content_length + and self.length_remaining is not None + and self.length_remaining != 0 + ): + # This is an edge case that httplib failed to cover due + # to concerns of backward compatibility. We're + # addressing it here to make sure IncompleteRead is + # raised during streaming, so all calls with incorrect + # Content-Length are caught. + raise IncompleteRead(self._fp_bytes_read, self.length_remaining) + elif read1 and ( + (amt != 0 and not data) or self.length_remaining == len(data) + ): + # All data has been read, but `self._fp.read1` in + # CPython 3.12 and older doesn't always close + # `http.client.HTTPResponse`, so we close it here. + # See https://github.com/python/cpython/issues/113199 + self._fp.close() + + if data: + self._fp_bytes_read += len(data) + if self.length_remaining is not None: + self.length_remaining -= len(data) + return data + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + """ + Similar to :meth:`http.client.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + self._init_decoder() + if decode_content is None: + decode_content = self.decode_content + + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + elif amt is not None: + cache_content = False + + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and len(self._decoded_buffer) < amt + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) >= amt: + return self._decoded_buffer.get(amt) + + data = self._raw_read(amt) + if not cache_content: + self._uncached_read_occurred = True + + flush_decoder = amt is None or (amt != 0 and not data) + + if ( + not data + and len(self._decoded_buffer) == 0 + and not (self._decoder and self._decoder.has_unconsumed_tail) + ): + return data + + if amt is None: + data = self._decode(data, decode_content, flush_decoder) + # It's possible that there is buffered decoded data after a + # partial read. + if decode_content and len(self._decoded_buffer) > 0: + self._decoded_buffer.put(data) + data = self._decoded_buffer.get_all() + + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + # do not waste memory on buffer when not decoding + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + + while len(self._decoded_buffer) < amt and data: + # TODO make sure to initially read enough data to get past the headers + # For example, the GZ file header takes 10 bytes, we don't want to read + # it one byte at a time + data = self._raw_read(amt) + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + data = self._decoded_buffer.get(amt) + + return data + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + """ + Similar to ``http.client.HTTPResponse.read1`` and documented + in :meth:`io.BufferedReader.read1`, but with an additional parameter: + ``decode_content``. + + :param amt: + How much of the content to read. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if decode_content is None: + decode_content = self.decode_content + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + # try and respond without going to the network + if self._has_decoded_content: + if not decode_content: + raise RuntimeError( + "Calling read1(decode_content=False) is not supported after " + "read1(decode_content=True) was called." + ) + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and (amt is None or len(self._decoded_buffer) < amt) + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=( + amt - len(self._decoded_buffer) if amt is not None else None + ), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) > 0: + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + if amt == 0: + return b"" + + # FIXME, this method's type doesn't say returning None is possible + data = self._raw_read(amt, read1=True) + self._uncached_read_occurred = True + if not decode_content or data is None: + return data + + self._init_decoder() + while True: + flush_decoder = not data + decoded_data = self._decode( + data, decode_content, flush_decoder, max_length=amt + ) + self._decoded_buffer.put(decoded_data) + if decoded_data or flush_decoder: + break + data = self._raw_read(8192, read1=True) + + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if amt == 0: + return + + if self.chunked and self.supports_chunked_reads(): + yield from self.read_chunked(amt, decode_content=decode_content) + else: + while ( + not is_fp_closed(self._fp) + or len(self._decoded_buffer) > 0 + or (self._decoder and self._decoder.has_unconsumed_tail) + ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + # Overrides from io.IOBase + def readable(self) -> bool: + return True + + def shutdown(self) -> None: + if not self._sock_shutdown: + raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") + if self._connection is None: + raise RuntimeError( + "Cannot shutdown as connection has already been released to the pool" + ) + self._sock_shutdown(socket.SHUT_RD) + + def close(self) -> None: + self._sock_shutdown = None + + if not self.closed and self._fp: + self._fp.close() + + if self._connection: + self._connection.close() + + if not self.auto_close: + io.IOBase.close(self) + + @property + def closed(self) -> bool: + if not self.auto_close: + return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] + elif self._fp is None: + return True + elif hasattr(self._fp, "isclosed"): + return self._fp.isclosed() + elif hasattr(self._fp, "closed"): + return self._fp.closed + else: + return True + + def fileno(self) -> int: + if self._fp is None: + raise OSError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise OSError( + "The file-like object this HTTPResponse is wrapped " + "around has no file descriptor" + ) + + def flush(self) -> None: + if ( + self._fp is not None + and hasattr(self._fp, "flush") + and not getattr(self._fp, "closed", False) + ): + return self._fp.flush() + + def supports_chunked_reads(self) -> bool: + """ + Checks if the underlying file-like object looks like a + :class:`http.client.HTTPResponse` object. We do this by testing for + the fp attribute. If it is present we assume it returns raw chunks as + processed by read_chunked(). + """ + return hasattr(self._fp, "fp") + + def _update_chunk_length(self) -> None: + # First, we'll figure out length of a chunk and then + # we'll try to read it from socket. + if self.chunk_left is not None: + return None + line = self._fp.fp.readline() # type: ignore[union-attr] + line = line.split(b";", 1)[0] + try: + self.chunk_left = int(line, 16) + except ValueError: + self.close() + if line: + # Invalid chunked protocol response, abort. + raise InvalidChunkLength(self, line) from None + else: + # Truncated at start of next chunk + raise ProtocolError("Response ended prematurely") from None + + def _handle_chunk(self, amt: int | None) -> bytes: + returned_chunk = None + if amt is None: + chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + returned_chunk = chunk + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + elif self.chunk_left is not None and amt < self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self.chunk_left = self.chunk_left - amt + returned_chunk = value + elif amt == self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + returned_chunk = value + else: # amt > self.chunk_left + returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + return returned_chunk # type: ignore[no-any-return] + + def read_chunked( + self, amt: int | None = None, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + Similar to :meth:`HTTPResponse.read`, but with an additional + parameter: ``decode_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + self._init_decoder() + # FIXME: Rewrite this method and make it a class with a better structured logic. + if not self.chunked: + raise ResponseNotChunked( + "Response is not chunked. " + "Header 'transfer-encoding: chunked' is missing." + ) + if not self.supports_chunked_reads(): + raise BodyNotHttplibCompatible( + "Body should be http.client.HTTPResponse like. " + "It should have have an fp attribute which returns raw chunks." + ) + + with self._error_catcher(): + # Don't bother reading the body of a HEAD request. + if self._original_response and is_response_to_head(self._original_response): + self._original_response.close() + return None + + # If a response is already read and closed + # then return immediately. + if self._fp.fp is None: # type: ignore[union-attr] + return None + + if amt == 0: + return + elif amt and amt < 0: + # Negative numbers and `None` should be treated the same, + # but httplib handles only `None` correctly. + amt = None + + while True: + # First, check if any data is left in the decoder's buffer. + if self._decoder and self._decoder.has_unconsumed_tail: + chunk = b"" + else: + self._update_chunk_length() + self._uncached_read_occurred = True + if self.chunk_left == 0: + break + chunk = self._handle_chunk(amt) + decoded = self._decode( + chunk, + decode_content=decode_content, + flush_decoder=False, + max_length=amt, + ) + if decoded: + yield decoded + + if decode_content: + # On CPython and PyPy, we should never need to flush the + # decoder. However, on Jython we *might* need to, so + # lets defensively do it anyway. + decoded = self._flush_decoder() + if decoded: # Platform-specific: Jython. + yield decoded + + # Chunk content ends with \r\n: discard it. + while self._fp is not None: + line = self._fp.fp.readline() + if not line: + # Some sites may not end with '\r\n'. + break + if line == b"\r\n": + break + + # We read everything; close the "file". + if self._original_response: + self._original_response.close() + + @property + def url(self) -> str | None: + """ + Returns the URL that was the source of this response. + If the request that generated this response redirected, this method + will return the final redirect location. + """ + return self._request_url + + @url.setter + def url(self, url: str | None) -> None: + self._request_url = url + + def __iter__(self) -> typing.Iterator[bytes]: + buffer: list[bytes] = [] + for chunk in self.stream(decode_content=True): + if b"\n" in chunk: + chunks = chunk.split(b"\n") + yield b"".join(buffer) + chunks[0] + b"\n" + for x in chunks[1:-1]: + yield x + b"\n" + if chunks[-1]: + buffer = [chunks[-1]] + else: + buffer = [] + else: + buffer.append(chunk) + if buffer: + yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py new file mode 100644 index 0000000000..534126033c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py @@ -0,0 +1,42 @@ +# For backwards compatibility, provide imports that used to be here. +from __future__ import annotations + +from .connection import is_connection_dropped +from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers +from .response import is_fp_closed +from .retry import Retry +from .ssl_ import ( + ALPN_PROTOCOLS, + IS_PYOPENSSL, + SSLContext, + assert_fingerprint, + create_urllib3_context, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import Timeout +from .url import Url, parse_url +from .wait import wait_for_read, wait_for_write + +__all__ = ( + "IS_PYOPENSSL", + "SSLContext", + "ALPN_PROTOCOLS", + "Retry", + "Timeout", + "Url", + "assert_fingerprint", + "create_urllib3_context", + "is_connection_dropped", + "is_fp_closed", + "parse_url", + "make_headers", + "resolve_cert_reqs", + "resolve_ssl_version", + "ssl_wrap_socket", + "wait_for_read", + "wait_for_write", + "SKIP_HEADER", + "SKIPPABLE_HEADERS", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py new file mode 100644 index 0000000000..f92519ee91 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import socket +import typing + +from ..exceptions import LocationParseError +from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT + +_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] + +if typing.TYPE_CHECKING: + from .._base_connection import BaseHTTPConnection + + +def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + :param conn: :class:`urllib3.connection.HTTPConnection` object. + """ + return not conn.is_connected + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +# One additional modification is that we avoid binding to IPv6 servers +# discovered in DNS if the system doesn't have IPv6 functionality. +def create_connection( + address: tuple[str, int], + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, +) -> socket.socket: + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`socket.getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith("["): + host = host.strip("[]") + err = None + + # Using the value from allowed_gai_family() in the context of getaddrinfo lets + # us select whether to work with IPv4 DNS records, IPv6 records, or both. + # The original create_connection function always returns all records. + family = allowed_gai_family() + + try: + host.encode("idna") + except UnicodeError: + raise LocationParseError(f"'{host}', label empty or too long") from None + + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + _set_socket_options(sock, socket_options) + + if timeout is not _DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + # Break explicitly a reference cycle + err = None + return sock + + except OSError as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + try: + raise err + finally: + # Break explicitly a reference cycle + err = None + else: + raise OSError("getaddrinfo returns an empty list") + + +def _set_socket_options( + sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None +) -> None: + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) + + +def allowed_gai_family() -> socket.AddressFamily: + """This function is designed to work in the context of + getaddrinfo, where family=socket.AF_UNSPEC is the default and + will perform a DNS search for both IPv6 and IPv4 records.""" + + family = socket.AF_INET + if HAS_IPV6: + family = socket.AF_UNSPEC + return family + + +def _has_ipv6(host: str) -> bool: + """Returns True if the system can bind an IPv6 address.""" + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/urllib3/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = True + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py new file mode 100644 index 0000000000..908fc6621d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .url import Url + +if typing.TYPE_CHECKING: + from ..connection import ProxyConfig + + +def connection_requires_http_tunnel( + proxy_url: Url | None = None, + proxy_config: ProxyConfig | None = None, + destination_scheme: str | None = None, +) -> bool: + """ + Returns True if the connection requires an HTTP CONNECT through the proxy. + + :param URL proxy_url: + URL of the proxy. + :param ProxyConfig proxy_config: + Proxy configuration from poolmanager.py + :param str destination_scheme: + The scheme of the destination. (i.e https, http, etc) + """ + # If we're not using a proxy, no way to use a tunnel. + if proxy_url is None: + return False + + # HTTP destinations never require tunneling, we always forward. + if destination_scheme == "http": + return False + + # Support for forwarding with HTTPS proxies and HTTPS destinations. + if ( + proxy_url.scheme == "https" + and proxy_config + and proxy_config.use_forwarding_for_https + ): + return False + + # Otherwise always use a tunnel. + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py new file mode 100644 index 0000000000..6c2372ba7e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +import io +import sys +import typing +from base64 import b64encode +from enum import Enum + +from ..exceptions import UnrewindableBodyError +from .util import to_bytes + +if typing.TYPE_CHECKING: + from typing import Final + +# Pass as a value within ``headers`` to skip +# emitting some HTTP headers that are added automatically. +# The only headers that are supported are ``Accept-Encoding``, +# ``Host``, and ``User-Agent``. +SKIP_HEADER = "@@@SKIP_HEADER@@@" +SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) + +ACCEPT_ENCODING = "gzip,deflate" +try: + try: + import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 + except ImportError: + import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",br" + +try: + if sys.version_info >= (3, 14): + from compression import zstd as _unused_module_zstd # noqa: F401 + else: + from backports import zstd as _unused_module_zstd # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",zstd" + + +class _TYPE_FAILEDTELL(Enum): + token = 0 + + +_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token + +_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] + +# When sending a request with these methods we aren't expecting +# a body so don't need to set an explicit 'Content-Length: 0' +# The reason we do this in the negative instead of tracking methods +# which 'should' have a body is because unknown methods should be +# treated as if they were 'POST' which *does* expect a body. +_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} + + +def make_headers( + keep_alive: bool | None = None, + accept_encoding: bool | list[str] | str | None = None, + user_agent: str | None = None, + basic_auth: str | None = None, + proxy_basic_auth: str | None = None, + disable_cache: bool | None = None, +) -> dict[str, str]: + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. If the dependencies for + Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or + Zstandard (the ``backports.zstd`` package for Python before 3.14) + algorithms are installed, then their encodings are + included in the string ('br' and 'zstd', respectively). + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example: + + .. code-block:: python + + import urllib3 + + print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) + # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + print(urllib3.util.make_headers(accept_encoding=True)) + # {'accept-encoding': 'gzip,deflate'} + """ + headers: dict[str, str] = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ",".join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers["accept-encoding"] = accept_encoding + + if user_agent: + headers["user-agent"] = user_agent + + if keep_alive: + headers["connection"] = "keep-alive" + + if basic_auth: + headers["authorization"] = ( + f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" + ) + + if proxy_basic_auth: + headers["proxy-authorization"] = ( + f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" + ) + + if disable_cache: + headers["cache-control"] = "no-cache" + + return headers + + +def set_file_position( + body: typing.Any, pos: _TYPE_BODY_POSITION | None +) -> _TYPE_BODY_POSITION | None: + """ + If a position is provided, move file to that point. + Otherwise, we'll attempt to record a position for future use. + """ + if pos is not None: + rewind_body(body, pos) + elif getattr(body, "tell", None) is not None: + try: + pos = body.tell() + except OSError: + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body. + pos = _FAILEDTELL + + return pos + + +def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: + """ + Attempt to rewind body to a certain position. + Primarily used for request redirects and retries. + + :param body: + File-like object that supports seek. + + :param int pos: + Position to seek to in file. + """ + body_seek = getattr(body, "seek", None) + if body_seek is not None and isinstance(body_pos, int): + try: + body_seek(body_pos) + except OSError as e: + raise UnrewindableBodyError( + "An error occurred when rewinding request body for redirect/retry." + ) from e + elif body_pos is _FAILEDTELL: + raise UnrewindableBodyError( + "Unable to record file position for rewinding " + "request body during a redirect/retry." + ) + else: + raise ValueError( + f"body_pos must be of type integer, instead it was {type(body_pos)}." + ) + + +class ChunksAndContentLength(typing.NamedTuple): + chunks: typing.Iterable[bytes] | None + content_length: int | None + + +def body_to_chunks( + body: typing.Any | None, method: str, blocksize: int +) -> ChunksAndContentLength: + """Takes the HTTP request method, body, and blocksize and + transforms them into an iterable of chunks to pass to + socket.sendall() and an optional 'Content-Length' header. + + A 'Content-Length' of 'None' indicates the length of the body + can't be determined so should use 'Transfer-Encoding: chunked' + for framing instead. + """ + + chunks: typing.Iterable[bytes] | None + content_length: int | None + + # No body, we need to make a recommendation on 'Content-Length' + # based on whether that request method is expected to have + # a body or not. + if body is None: + chunks = None + if method.upper() not in _METHODS_NOT_EXPECTING_BODY: + content_length = 0 + else: + content_length = None + + # Bytes or strings become bytes + elif isinstance(body, (str, bytes)): + chunks = (to_bytes(body),) + content_length = len(chunks[0]) + + # File-like object, TODO: use seek() and tell() for length? + elif hasattr(body, "read"): + + def chunk_readable() -> typing.Iterable[bytes]: + encode = isinstance(body, io.TextIOBase) + while True: + datablock = body.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("utf-8") + yield datablock + + chunks = chunk_readable() + content_length = None + + # Otherwise we need to start checking via duck-typing. + else: + try: + # Check if the body implements the buffer API. + mv = memoryview(body) + except TypeError: + try: + # Check if the body is an iterable + chunks = iter(body) + content_length = None + except TypeError: + raise TypeError( + f"'body' must be a bytes-like object, file-like " + f"object, or iterable. Instead was {body!r}" + ) from None + else: + # Since it implements the buffer API can be passed directly to socket.sendall() + chunks = (body,) + content_length = mv.nbytes + + return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py new file mode 100644 index 0000000000..0f4578696f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import http.client as httplib +from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj: object) -> bool: + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check `isclosed()` first, in case Python3 doesn't set `closed`. + # GH Issue #928 + return obj.isclosed() # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check via the official file-like-object way. + return obj.closed # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None # type: ignore[attr-defined] + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers: httplib.HTTPMessage) -> None: + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param http.client.HTTPMessage headers: Headers to verify. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError(f"expected httplib.Message, got {type(headers)}.") + + unparsed_data = None + + # get_payload is actually email.message.Message.get_payload; + # we're only interested in the result if it's not a multipart message + if not headers.is_multipart(): + payload = headers.get_payload() + + if isinstance(payload, (bytes, str)): + unparsed_data = payload + + # httplib is assuming a response body is available + # when parsing headers even when httplib only sends + # header data to parse_headers() This results in + # defects on multipart responses in particular. + # See: https://github.com/urllib3/urllib3/issues/800 + + # So we ignore the following defects: + # - StartBoundaryNotFoundDefect: + # The claimed start boundary was never found. + # - MultipartInvariantViolationDefect: + # A message claimed to be a multipart but no subparts were found. + defects = [ + defect + for defect in headers.defects + if not isinstance( + defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) + ) + ] + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response: httplib.HTTPResponse) -> bool: + """ + Checks whether the request of a response has been a HEAD-request. + + :param http.client.HTTPResponse response: + Response to check if the originating request + used 'HEAD' as a method. + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method_str = response._method # type: str # type: ignore[attr-defined] + return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py new file mode 100644 index 0000000000..7649898e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py @@ -0,0 +1,557 @@ +from __future__ import annotations + +import email +import logging +import random +import re +import time +import typing +from itertools import takewhile +from types import TracebackType + +from ..exceptions import ( + ConnectTimeoutError, + InvalidHeader, + MaxRetryError, + ProtocolError, + ProxyError, + ReadTimeoutError, + ResponseError, +) +from .util import reraise + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from ..connectionpool import ConnectionPool + from ..response import BaseHTTPResponse + +log = logging.getLogger(__name__) + + +# Data structure for representing the metadata of requests that result in a retry. +class RequestHistory(typing.NamedTuple): + method: str | None + url: str | None + error: Exception | None + status: int | None + redirect_location: str | None + + +class Retry: + """Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool: + + .. code-block:: python + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request("GET", "https://example.com/") + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=Retry(10)) + + Retries can be disabled by passing ``False``: + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int status: + How many times to retry on bad status codes. + + These are retries made on responses, where status code matches + ``status_forcelist``. + + Set to ``0`` to fail on the first retry of this type. + + :param int other: + How many times to retry on other errors. + + Other errors are errors that are not connect, read, redirect or status errors. + These errors might be raised after the request was sent to the server, so the + request might have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + If ``total`` is not set, it's a good idea to set this to 0 to account + for unexpected edge cases and avoid infinite retry loops. + + :param Collection allowed_methods: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + idempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. + + Set to a ``None`` value to retry on any verb. + + :param Collection status_forcelist: + A set of integer HTTP status codes that we should force a retry on. + A retry is initiated if the request method is in ``allowed_methods`` + and the response status code is in ``status_forcelist``. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts after the second try + (most errors are resolved immediately by a second try without a + delay). urllib3 will sleep for:: + + {backoff factor} * (2 ** ({number of previous retries})) + + seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: + + random.uniform(0, {backoff jitter}) + + seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will + sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever + be longer than `backoff_max`. + + By default, backoff is disabled (factor set to 0). + + :param float backoff_max: + The maximum backoff time (in seconds) between retry attempts. + This value caps the computed backoff from `backoff_factor`. + + :param float backoff_jitter: + Random jitter amount (in seconds) added to the computed backoff. + Jitter is sampled uniformly from `0` to `backoff_jitter`. + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have + been exhausted. + + :param tuple history: The history of the request encountered during + each call to :meth:`~Retry.increment`. The list is in the order + the requests occurred. Each list item is of class :class:`RequestHistory`. + + :param bool respect_retry_after_header: + Whether to respect Retry-After header on status codes defined as + :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. + + :param Collection remove_headers_on_redirect: + Sequence of headers to remove from the request when a response + indicating a redirect is returned before firing off the redirected + request. + + :param int retry_after_max: Number of seconds to allow as the maximum for + Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. + Any Retry-After headers larger than this value will be limited to this + value. + """ + + #: Default methods to be used for ``allowed_methods`` + DEFAULT_ALLOWED_METHODS = frozenset( + ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) + + #: Default status codes to be used for ``status_forcelist`` + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Default headers to be used for ``remove_headers_on_redirect`` + DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( + ["Cookie", "Authorization", "Proxy-Authorization"] + ) + + #: Default maximum backoff time. + DEFAULT_BACKOFF_MAX = 120 + + # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. + #: Default maximum allowed value for Retry-After headers in seconds + DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 + + # Backward compatibility; assigned outside of the class. + DEFAULT: typing.ClassVar[Retry] + + def __init__( + self, + total: bool | int | None = 10, + connect: int | None = None, + read: int | None = None, + redirect: bool | int | None = None, + status: int | None = None, + other: int | None = None, + allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, + status_forcelist: typing.Collection[int] | None = None, + backoff_factor: float = 0, + backoff_max: float = DEFAULT_BACKOFF_MAX, + raise_on_redirect: bool = True, + raise_on_status: bool = True, + history: tuple[RequestHistory, ...] | None = None, + respect_retry_after_header: bool = True, + remove_headers_on_redirect: typing.Collection[ + str + ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, + backoff_jitter: float = 0.0, + retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, + ) -> None: + self.total = total + self.connect = connect + self.read = read + self.status = status + self.other = other + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.allowed_methods = allowed_methods + self.backoff_factor = backoff_factor + self.backoff_max = backoff_max + self.retry_after_max = retry_after_max + self.raise_on_redirect = raise_on_redirect + self.raise_on_status = raise_on_status + self.history = history or () + self.respect_retry_after_header = respect_retry_after_header + self.remove_headers_on_redirect = frozenset( + h.lower() for h in remove_headers_on_redirect + ) + self.backoff_jitter = backoff_jitter + + def new(self, **kw: typing.Any) -> Self: + params = dict( + total=self.total, + connect=self.connect, + read=self.read, + redirect=self.redirect, + status=self.status, + other=self.other, + allowed_methods=self.allowed_methods, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + backoff_max=self.backoff_max, + retry_after_max=self.retry_after_max, + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, + remove_headers_on_redirect=self.remove_headers_on_redirect, + respect_retry_after_header=self.respect_retry_after_header, + backoff_jitter=self.backoff_jitter, + ) + + params.update(kw) + return type(self)(**params) # type: ignore[arg-type] + + @classmethod + def from_int( + cls, + retries: Retry | bool | int | None, + redirect: bool | int | None = True, + default: Retry | bool | int | None = None, + ) -> Retry: + """Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r", retries, new_retries) + return new_retries + + def get_backoff_time(self) -> float: + """Formula for computing the current backoff + + :rtype: float + """ + # We want to consider only the last consecutive errors sequence (Ignore redirects). + consecutive_errors_len = len( + list( + takewhile(lambda x: x.redirect_location is None, reversed(self.history)) + ) + ) + if consecutive_errors_len <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) + if self.backoff_jitter != 0.0: + backoff_value += random.random() * self.backoff_jitter + return float(max(0, min(self.backoff_max, backoff_value))) + + def parse_retry_after(self, retry_after: str) -> float: + seconds: float + # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = int(retry_after) + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + seconds = max(seconds, 0) + + # Check the seconds do not exceed the specified maximum + if seconds > self.retry_after_max: + seconds = self.retry_after_max + + return seconds + + def get_retry_after(self, response: BaseHTTPResponse) -> float | None: + """Get the value of Retry-After in seconds.""" + + retry_after = response.headers.get("Retry-After") + + if retry_after is None: + return None + + return self.parse_retry_after(retry_after) + + def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: + retry_after = self.get_retry_after(response) + if retry_after: + time.sleep(retry_after) + return True + + return False + + def _sleep_backoff(self) -> None: + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def sleep(self, response: BaseHTTPResponse | None = None) -> None: + """Sleep between retry attempts. + + This method will respect a server's ``Retry-After`` response header + and sleep the duration of the time requested. If that is not present, it + will use an exponential backoff. By default, the backoff factor is 0 and + this method will return immediately. + """ + + if self.respect_retry_after_header and response: + slept = self.sleep_for_retry(response) + if slept: + return + + self._sleep_backoff() + + def _is_connection_error(self, err: Exception) -> bool: + """Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + if isinstance(err, ProxyError): + err = err.original_error + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err: Exception) -> bool: + """Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def _is_method_retryable(self, method: str) -> bool: + """Checks if a given HTTP method should be retried upon, depending if + it is included in the allowed_methods + """ + if self.allowed_methods and method.upper() not in self.allowed_methods: + return False + return True + + def is_retry( + self, method: str, status_code: int, has_retry_after: bool = False + ) -> bool: + """Is this method/status code retryable? (Based on allowlists and control + variables such as the number of total retries to allow, whether to + respect the Retry-After header, whether this header is present, and + whether the returned status code is on the list of status codes to + be retried upon on the presence of the aforementioned header) + """ + if not self._is_method_retryable(method): + return False + + if self.status_forcelist and status_code in self.status_forcelist: + return True + + return bool( + self.total + and self.respect_retry_after_header + and has_retry_after + and (status_code in self.RETRY_AFTER_STATUS_CODES) + ) + + def is_exhausted(self) -> bool: + """Are we out of retries?""" + retry_counts = [ + x + for x in ( + self.total, + self.connect, + self.read, + self.redirect, + self.status, + self.other, + ) + if x + ] + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment( + self, + method: str | None = None, + url: str | None = None, + response: BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, + ) -> Self: + """Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.BaseHTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + connect = self.connect + read = self.read + redirect = self.redirect + status_count = self.status + other = self.other + cause = "unknown" + status = None + redirect_location = None + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False or method is None or not self._is_method_retryable(method): + raise reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + + elif error: + # Other retry? + if other is not None: + other -= 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = "too many redirects" + response_redirect_location = response.get_redirect_location() + if response_redirect_location: + redirect_location = response_redirect_location + status = response.status + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and the given method is in the allowed_methods + cause = ResponseError.GENERIC_ERROR + if response and response.status: + if status_count is not None: + status_count -= 1 + cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) + status = response.status + + history = self.history + ( + RequestHistory(method, url, error, status, redirect_location), + ) + + new_retry = self.new( + total=total, + connect=connect, + read=read, + redirect=redirect, + status=status_count, + other=other, + history=history, + ) + + if new_retry.is_exhausted(): + reason = error or ResponseError(cause) + raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] + + log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) + + return new_retry + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(total={self.total}, connect={self.connect}, " + f"read={self.read}, redirect={self.redirect}, status={self.status})" + ) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py new file mode 100644 index 0000000000..e66549a76c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +import hashlib +import hmac +import os +import socket +import sys +import typing +import warnings +from binascii import unhexlify + +from ..exceptions import ProxySchemeUnsupported, SSLError +from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE + +SSLContext = None +SSLTransport = None +HAS_NEVER_CHECK_COMMON_NAME = False +IS_PYOPENSSL = False +ALPN_PROTOCOLS = ["http/1.1"] + +_TYPE_VERSION_INFO = tuple[int, int, int, str, int] + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = { + length: getattr(hashlib, algorithm, None) + for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) +} + + +def _is_has_never_check_common_name_reliable( + openssl_version: str, +) -> bool: + # As of May 2023, all released versions of LibreSSL fail to reject certificates with + # only common names, see https://github.com/urllib3/urllib3/pull/3024 + is_openssl = openssl_version.startswith("OpenSSL ") + + return is_openssl + + +if typing.TYPE_CHECKING: + from ssl import VerifyMode + from typing import TypedDict + + from .ssltransport import SSLTransport as SSLTransportType + + class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): + subjectAltName: tuple[tuple[str, str], ...] + subject: tuple[tuple[tuple[str, str], ...], ...] + serialNumber: str + + +# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' +_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} + +try: # Do we have ssl at all? + import ssl + from ssl import ( # type: ignore[assignment] + CERT_REQUIRED, + HAS_NEVER_CHECK_COMMON_NAME, + OP_NO_COMPRESSION, + OP_NO_TICKET, + OPENSSL_VERSION, + PROTOCOL_TLS, + PROTOCOL_TLS_CLIENT, + VERIFY_X509_PARTIAL_CHAIN, + VERIFY_X509_STRICT, + OP_NO_SSLv2, + OP_NO_SSLv3, + SSLContext, + TLSVersion, + ) + + PROTOCOL_SSLv23 = PROTOCOL_TLS + + # Setting SSLContext.hostname_checks_common_name = False didn't work with + # LibreSSL, check details in the used function. + if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( + OPENSSL_VERSION, + ): # Defensive: + HAS_NEVER_CHECK_COMMON_NAME = False + + # Need to be careful here in case old TLS versions get + # removed in future 'ssl' module implementations. + for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): + try: + _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( + TLSVersion, attr + ) + except AttributeError: # Defensive: + continue + + from .ssltransport import SSLTransport # type: ignore[assignment] +except ImportError: + OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] + OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] + OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] + OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] + PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] + PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] + VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] + VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] + + +_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] + + +def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + if cert is None: + raise SSLError("No certificate for the peer.") + + fingerprint = fingerprint.replace(":", "").lower() + digest_length = len(fingerprint) + if digest_length not in HASHFUNC_MAP: + raise SSLError(f"Fingerprint of invalid length: {fingerprint}") + hashfunc = HASHFUNC_MAP.get(digest_length) + if hashfunc is None: + raise SSLError( + f"Hash function implementation unavailable for fingerprint length: {digest_length}" + ) + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not hmac.compare_digest(cert_digest, fingerprint_bytes): + raise SSLError( + f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' + ) + + +def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_REQUIRED`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbreviation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_REQUIRED + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "CERT_" + candidate) + return res # type: ignore[no-any-return] + + return candidate # type: ignore[return-value] + + +def resolve_ssl_version(candidate: None | int | str) -> int: + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_TLS + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "PROTOCOL_" + candidate) + return typing.cast(int, res) + + return candidate + + +def create_urllib3_context( + ssl_version: int | None = None, + cert_reqs: int | None = None, + options: int | None = None, + ciphers: str | None = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + verify_flags: int | None = None, +) -> ssl.SSLContext: + """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + + This parameter is deprecated instead use 'ssl_minimum_version'. + :param ssl_minimum_version: + The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + :param ssl_maximum_version: + The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the + default value. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. + :param ciphers: + Which cipher suites to allow the server to select. Defaults to either system configured + ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. + :param verify_flags: + The flags for certificate verification operations. These default to + ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + if SSLContext is None: + raise TypeError("Can't create an SSLContext object without an ssl module") + + # This means 'ssl_version' was specified as an exact value. + if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): + # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' + # to avoid conflicts. + if ssl_minimum_version is not None or ssl_maximum_version is not None: + raise ValueError( + "Can't specify both 'ssl_version' and either " + "'ssl_minimum_version' or 'ssl_maximum_version'" + ) + + # 'ssl_version' is deprecated and will be removed in the future. + else: + # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. + ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MINIMUM_SUPPORTED + ) + ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MAXIMUM_SUPPORTED + ) + + # This warning message is pushing users to use 'ssl_minimum_version' + # instead of both min/max. Best practice is to only set the minimum version and + # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' + warnings.warn( + "'ssl_version' option is deprecated and will be " + "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", + category=FutureWarning, + stacklevel=2, + ) + + context = SSLContext(PROTOCOL_TLS_CLIENT) + if ssl_minimum_version is not None: + context.minimum_version = ssl_minimum_version + else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here + context.minimum_version = TLSVersion.TLSv1_2 + + if ssl_maximum_version is not None: + context.maximum_version = ssl_maximum_version + + # Unless we're given ciphers defer to either system ciphers in + # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. + if ciphers: + context.set_ciphers(ciphers) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + # TLSv1.2 only. Unless set explicitly, do not request tickets. + # This may save some bandwidth on wire, and although the ticket is encrypted, + # there is a risk associated with it being on wire, + # if the server is not rotating its ticketing keys properly. + options |= OP_NO_TICKET + + context.options |= options + + if verify_flags is None: + verify_flags = 0 + # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN + # and VERIFY_X509_STRICT so we do the same + if sys.version_info >= (3, 13): + verify_flags |= VERIFY_X509_PARTIAL_CHAIN + verify_flags |= VERIFY_X509_STRICT + + context.verify_flags |= verify_flags + + # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is + # necessary for conditional client cert authentication with TLS 1.3. + # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using + # an SSLContext created by pyOpenSSL. + if getattr(context, "post_handshake_auth", None) is not None: + context.post_handshake_auth = True + + # The order of the below lines setting verify_mode and check_hostname + # matter due to safe-guards SSLContext has to prevent an SSLContext with + # check_hostname=True, verify_mode=NONE/OPTIONAL. + # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own + # 'ssl.match_hostname()' implementation. + if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: + context.verify_mode = cert_reqs + context.check_hostname = True + else: + context.check_hostname = False + context.verify_mode = cert_reqs + + context.hostname_checks_common_name = False + + if "SSLKEYLOGFILE" in os.environ: + sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) + else: + sslkeylogfile = None + if sslkeylogfile: + context.keylog_filename = sslkeylogfile + + return context + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: typing.Literal[False] = ..., +) -> ssl.SSLSocket: ... + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: bool = ..., +) -> ssl.SSLSocket | SSLTransportType: ... + + +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = None, + certfile: str | None = None, + cert_reqs: int | None = None, + ca_certs: str | None = None, + server_hostname: str | None = None, + ssl_version: int | None = None, + ciphers: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_cert_dir: str | None = None, + key_password: str | None = None, + ca_cert_data: None | str | bytes = None, + tls_in_tls: bool = False, +) -> ssl.SSLSocket | SSLTransportType: + """ + All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and + ca_cert_dir have the same meaning as they do when using + :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, + :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + :param key_password: + Optional password if the keyfile is encrypted. + :param ca_cert_data: + Optional string containing CA certificates in PEM format suitable for + passing as the cadata parameter to SSLContext.load_verify_locations() + :param tls_in_tls: + Use SSLTransport to wrap the existing socket. + """ + context = ssl_context + if context is None: + # Note: This branch of code and all the variables in it are only used in tests. + # We should consider deprecating and removing this code. + context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) + + if ca_certs or ca_cert_dir or ca_cert_data: + try: + context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) + except OSError as e: + raise SSLError(e) from e + + elif ssl_context is None and hasattr(context, "load_default_certs"): + # try to load OS default certs; works well on Windows. + context.load_default_certs() + + # Attempt to detect if we get the goofy behavior of the + # keyfile being encrypted and OpenSSL asking for the + # passphrase via the terminal and instead error out. + if keyfile and key_password is None and _is_key_file_encrypted(keyfile): + raise SSLError("Client private key is encrypted, password is required") + + if certfile: + if key_password is None: + context.load_cert_chain(certfile, keyfile) + else: + context.load_cert_chain(certfile, keyfile, key_password) + + context.set_alpn_protocols(ALPN_PROTOCOLS) + + ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) + return ssl_sock + + +def is_ipaddress(hostname: str | bytes) -> bool: + """Detects whether the hostname given is an IPv4 or IPv6 address. + Also detects IPv6 addresses with Zone IDs. + + :param str hostname: Hostname to examine. + :return: True if the hostname is an IP address, False otherwise. + """ + if isinstance(hostname, bytes): + # IDN A-label bytes are ASCII compatible. + hostname = hostname.decode("ascii") + return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) + + +def _is_key_file_encrypted(key_file: str) -> bool: + """Detects if a key file is encrypted or not.""" + with open(key_file) as f: + for line in f: + # Look for Proc-Type: 4,ENCRYPTED + if "ENCRYPTED" in line: + return True + + return False + + +def _ssl_wrap_socket_impl( + sock: socket.socket, + ssl_context: ssl.SSLContext, + tls_in_tls: bool, + server_hostname: str | None = None, +) -> ssl.SSLSocket | SSLTransportType: + if tls_in_tls: + if not SSLTransport: + # Import error, ssl is not available. + raise ProxySchemeUnsupported( + "TLS in TLS requires support for the 'ssl' module" + ) + + SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) + return SSLTransport(sock, ssl_context, server_hostname) + + return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py new file mode 100644 index 0000000000..94994f25ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py @@ -0,0 +1,153 @@ +"""The match_hostname() function from Python 3.5, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html +# It is modified to remove commonName support. + +from __future__ import annotations + +import ipaddress +import re +import typing +from ipaddress import IPv4Address, IPv6Address + +if typing.TYPE_CHECKING: + from .ssl_ import _TYPE_PEER_CERT_RET_DICT + +__version__ = "3.5.0.1" + + +class CertificateError(ValueError): + pass + + +def _dnsname_match( + dn: typing.Any, hostname: str, max_wildcards: int = 1 +) -> typing.Match[str] | None | bool: + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r".") + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count("*") + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn) + ) + + # speed up common case w/o wildcards + if not wildcards: + return bool(dn.lower() == hostname.lower()) + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == "*": + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append("[^.]+") + elif leftmost.startswith("xn--") or hostname.startswith("xn--"): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) + return pat.match(hostname) + + +def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: + """Exact matching of IP addresses. + + RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded + bytes of the IP address. An IP version 4 address is 4 octets, and an IP + version 6 address is 16 octets. [...] A reference identity of type IP-ID + matches if the address is identical to an iPAddress value of the + subjectAltName extension of the certificate." + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(ipname.rstrip()) + return bool(ip.packed == host_ip.packed) + + +def match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError( + "empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED" + ) + + try: + host_ip = ipaddress.ip_address(hostname) + except ValueError: + # Not an IP address (common case) + host_ip = None + dnsnames = [] + san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) + key: str + value: str + for key, value in san: + if key == "DNS": + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == "IP Address": + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + + # We only check 'commonName' if it's enabled and we're not verifying + # an IP address. IP addresses aren't valid within 'commonName'. + if hostname_checks_common_name and host_ip is None and not dnsnames: + for sub in cert.get("subject", ()): + for key, value in sub: + if key == "commonName": + if _dnsname_match(value, hostname): + return + dnsnames.append( + value + ) # Defensive: for older PyPy and OpenSSL versions + + if len(dnsnames) > 1: + raise CertificateError( + "hostname %r " + "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) + ) + elif len(dnsnames) == 1: + raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") + else: + raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py new file mode 100644 index 0000000000..6d59bc3bce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +import io +import socket +import ssl +import typing + +from ..exceptions import ProxySchemeUnsupported + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT + + +_WriteBuffer = typing.Union[bytearray, memoryview] +_ReturnValue = typing.TypeVar("_ReturnValue") + +SSL_BLOCKSIZE = 16384 + + +class SSLTransport: + """ + The SSLTransport wraps an existing socket and establishes an SSL connection. + + Contrary to Python's implementation of SSLSocket, it allows you to chain + multiple TLS connections together. It's particularly useful if you need to + implement TLS within TLS. + + The class supports most of the socket API operations. + """ + + @staticmethod + def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: + """ + Raises a ProxySchemeUnsupported if the provided ssl_context can't be used + for TLS in TLS. + + The only requirement is that the ssl_context provides the 'wrap_bio' + methods. + """ + + if not hasattr(ssl_context, "wrap_bio"): + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "available on non-native SSLContext" + ) + + def __init__( + self, + socket: socket.socket, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + suppress_ragged_eofs: bool = True, + ) -> None: + """ + Create an SSLTransport around socket using the provided ssl_context. + """ + self.incoming = ssl.MemoryBIO() + self.outgoing = ssl.MemoryBIO() + + self.suppress_ragged_eofs = suppress_ragged_eofs + self.socket = socket + + self.sslobj = ssl_context.wrap_bio( + self.incoming, self.outgoing, server_hostname=server_hostname + ) + + # Perform initial handshake. + self._ssl_io_loop(self.sslobj.do_handshake) + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: typing.Any) -> None: + self.close() + + def fileno(self) -> int: + return self.socket.fileno() + + def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: + return self._wrap_ssl_read(len, buffer) + + def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv") + return self._wrap_ssl_read(buflen) + + def recv_into( + self, + buffer: _WriteBuffer, + nbytes: int | None = None, + flags: int = 0, + ) -> None | int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv_into") + if nbytes is None: + nbytes = len(buffer) + return self.read(nbytes, buffer) + + def sendall(self, data: bytes, flags: int = 0) -> None: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall") + count = 0 + with memoryview(data) as view, view.cast("B") as byte_view: + amount = len(byte_view) + while count < amount: + v = self.send(byte_view[count:]) + count += v + + def send(self, data: bytes, flags: int = 0) -> int: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send") + return self._ssl_io_loop(self.sslobj.write, data) + + def makefile( + self, + mode: str, + buffering: int | None = None, + *, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: + """ + Python's httpclient uses makefile and buffered io when reading HTTP + messages and we need to support it. + + This is unfortunately a copy and paste of socket.py makefile with small + changes to point to the socket directly. + """ + if not set(mode) <= {"r", "w", "b"}: + raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") + + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] + self.socket._io_refs += 1 # type: ignore[attr-defined] + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + buffer: typing.BinaryIO + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode # type: ignore[misc] + return text + + def unwrap(self) -> None: + self._ssl_io_loop(self.sslobj.unwrap) + + def close(self) -> None: + self.socket.close() + + @typing.overload + def getpeercert( + self, binary_form: typing.Literal[False] = ... + ) -> _TYPE_PEER_CERT_RET_DICT | None: ... + + @typing.overload + def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... + + def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: + return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] + + def version(self) -> str | None: + return self.sslobj.version() + + def cipher(self) -> tuple[str, str, int] | None: + return self.sslobj.cipher() + + def selected_alpn_protocol(self) -> str | None: + return self.sslobj.selected_alpn_protocol() + + def shared_ciphers(self) -> list[tuple[str, str, int]] | None: + return self.sslobj.shared_ciphers() + + def compression(self) -> str | None: + return self.sslobj.compression() + + def settimeout(self, value: float | None) -> None: + self.socket.settimeout(value) + + def gettimeout(self) -> float | None: + return self.socket.gettimeout() + + def _decref_socketios(self) -> None: + self.socket._decref_socketios() # type: ignore[attr-defined] + + def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: + try: + return self._ssl_io_loop(self.sslobj.read, len, buffer) + except ssl.SSLError as e: + if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: + return 0 # eof, return 0. + else: + raise + + # func is sslobj.do_handshake or sslobj.unwrap + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... + + # func is sslobj.write, arg1 is data + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... + + # func is sslobj.read, arg1 is len, arg2 is buffer + @typing.overload + def _ssl_io_loop( + self, + func: typing.Callable[[int, bytearray | None], bytes], + arg1: int, + arg2: bytearray | None, + ) -> bytes: ... + + def _ssl_io_loop( + self, + func: typing.Callable[..., _ReturnValue], + arg1: None | bytes | int = None, + arg2: bytearray | None = None, + ) -> _ReturnValue: + """Performs an I/O loop between incoming/outgoing and the socket.""" + should_loop = True + ret = None + + while should_loop: + errno = None + try: + if arg1 is None and arg2 is None: + ret = func() + elif arg2 is None: + ret = func(arg1) + else: + ret = func(arg1, arg2) + except ssl.SSLError as e: + if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): + # WANT_READ, and WANT_WRITE are expected, others are not. + raise e + errno = e.errno + + buf = self.outgoing.read() + self.socket.sendall(buf) + + if errno is None: + should_loop = False + elif errno == ssl.SSL_ERROR_WANT_READ: + buf = self.socket.recv(SSL_BLOCKSIZE) + if buf: + self.incoming.write(buf) + else: + self.incoming.write_eof() + return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py new file mode 100644 index 0000000000..4bb1be11d9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import time +import typing +from enum import Enum +from socket import getdefaulttimeout + +from ..exceptions import TimeoutStateError + +if typing.TYPE_CHECKING: + from typing import Final + + +class _TYPE_DEFAULT(Enum): + # This value should never be passed to socket.settimeout() so for safety we use a -1. + # socket.settimout() raises a ValueError for negative values. + token = -1 + + +_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token + +_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] + + +class Timeout: + """Timeout configuration. + + Timeouts can be defined as a default for a pool: + + .. code-block:: python + + import urllib3 + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + + http = urllib3.PoolManager(timeout=timeout) + + resp = http.request("GET", "https://example.com/") + + print(resp.status) + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``: + + .. code-block:: python + + no_timeout = Timeout(connect=None, read=None) + response = http.request("GET", "https://example.com/", timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: int, float, or None + + :param connect: + The maximum amount of time (in seconds) to wait for a connection + attempt to a server to succeed. Omitting the parameter will default the + connect timeout to the system default, probably `the global default + timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: int, float, or None + + :param read: + The maximum amount of time (in seconds) to wait between consecutive + read operations for a response from the server. Omitting the parameter + will default the read timeout to the system default, probably `the + global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: int, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT + + def __init__( + self, + total: _TYPE_TIMEOUT = None, + connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + ) -> None: + self._connect = self._validate_timeout(connect, "connect") + self._read = self._validate_timeout(read, "read") + self.total = self._validate_timeout(total, "total") + self._start_connect: float | None = None + + def __repr__(self) -> str: + return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" + + # __str__ provided for backwards compatibility + __str__ = __repr__ + + @staticmethod + def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: + return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout + + @classmethod + def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: + """Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If it is a numeric value less than or equal to + zero, or the type is not an integer, float, or None. + """ + if value is None or value is _DEFAULT_TIMEOUT: + return value + + if isinstance(value, bool): + raise ValueError( + "Timeout cannot be a boolean value. It must " + "be an int, float or None." + ) + try: + float(value) + except (TypeError, ValueError): + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + try: + if value <= 0: + raise ValueError( + "Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value) + ) + except TypeError: + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + return value + + @classmethod + def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: + """Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self) -> Timeout: + """Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, total=self.total) + + def start_connect(self) -> float: + """Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = time.monotonic() + return self._start_connect + + def get_connect_duration(self) -> float: + """Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time in seconds. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError( + "Can't get connect duration for timer that has not started." + ) + return time.monotonic() - self._start_connect + + @property + def connect_timeout(self) -> _TYPE_TIMEOUT: + """Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is _DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) # type: ignore[type-var] + + @property + def read_timeout(self) -> float | None: + """Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if ( + self.total is not None + and self.total is not _DEFAULT_TIMEOUT + and self._read is not None + and self._read is not _DEFAULT_TIMEOUT + ): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), self._read)) + elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py new file mode 100644 index 0000000000..db057f17be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import re +import typing + +from ..exceptions import LocationParseError +from .util import to_str + +# We only want to normalize urls with an HTTP(S) scheme. +# urllib3 infers URLs without a scheme (None) to be http. +_NORMALIZABLE_SCHEMES = ("http", "https", None) + +# Almost all of these patterns were derived from the +# 'rfc3986' module: https://github.com/python-hyper/rfc3986 +_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") +_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") +_URI_RE = re.compile( + r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" + r"(?://([^\\/?#]*))?" + r"([^?#]*)" + r"(?:\?([^#]*))?" + r"(?:#(.*))?$", + re.UNICODE | re.DOTALL, +) + +_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" +_HEX_PAT = "[0-9A-Fa-f]{1,4}" +_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) +_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} +_variations = [ + # 6( h16 ":" ) ls32 + "(?:%(hex)s:){6}%(ls32)s", + # "::" 5( h16 ":" ) ls32 + "::(?:%(hex)s:){5}%(ls32)s", + # [ h16 ] "::" 4( h16 ":" ) ls32 + "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", + # [ *4( h16 ":" ) h16 ] "::" ls32 + "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", + # [ *5( h16 ":" ) h16 ] "::" h16 + "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", + # [ *6( h16 ":" ) h16 ] "::" + "(?:(?:%(hex)s:){0,6}%(hex)s)?::", +] + +_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" +_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" +_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" +_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" +_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" +_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") + +_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") +_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") +_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") +_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") +_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") + +_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( + _REG_NAME_PAT, + _IPV4_PAT, + _IPV6_ADDRZ_PAT, +) +_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) + +_UNRESERVED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" +) +_SUB_DELIM_CHARS = set("!$&'()*+,;=") +_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} +_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} +_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} + + +class Url( + typing.NamedTuple( + "Url", + [ + ("scheme", typing.Optional[str]), + ("auth", typing.Optional[str]), + ("host", typing.Optional[str]), + ("port", typing.Optional[int]), + ("path", typing.Optional[str]), + ("query", typing.Optional[str]), + ("fragment", typing.Optional[str]), + ], + ) +): + """ + Data structure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. Both the scheme and host are normalized as they are + both case-insensitive according to RFC 3986. + """ + + def __new__( # type: ignore[no-untyped-def] + cls, + scheme: str | None = None, + auth: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ): + if path and not path.startswith("/"): + path = "/" + path + if scheme is not None: + scheme = scheme.lower() + return super().__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self) -> str | None: + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self) -> str: + """Absolute path including the query string.""" + uri = self.path or "/" + + if self.query is not None: + uri += "?" + self.query + + return uri + + @property + def authority(self) -> str | None: + """ + Authority component as defined in RFC 3986 3.2. + This includes userinfo (auth), host and port. + + i.e. + userinfo@host:port + """ + userinfo = self.auth + netloc = self.netloc + if netloc is None or userinfo is None: + return netloc + else: + return f"{userinfo}@{netloc}" + + @property + def netloc(self) -> str | None: + """ + Network location including host and port. + + If you need the equivalent of urllib.parse's ``netloc``, + use the ``authority`` property instead. + """ + if self.host is None: + return None + if self.port: + return f"{self.host}:{self.port}" + return self.host + + @property + def url(self) -> str: + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: + + .. code-block:: python + + import urllib3 + + U = urllib3.util.parse_url("https://google.com/mail/") + + print(U.url) + # "https://google.com/mail/" + + print( urllib3.util.Url("https", "username:password", + "host.com", 80, "/path", "query", "fragment" + ).url + ) + # "https://username:password@host.com:80/path?query#fragment" + """ + scheme, auth, host, port, path, query, fragment = self + url = "" + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + "://" + if auth is not None: + url += auth + "@" + if host is not None: + url += host + if port is not None: + url += ":" + str(port) + if path is not None: + url += path + if query is not None: + url += "?" + query + if fragment is not None: + url += "#" + fragment + + return url + + def __str__(self) -> str: + return self.url + + +@typing.overload +def _encode_invalid_chars( + component: str, allowed_chars: typing.Container[str] +) -> str: # Abstract + ... + + +@typing.overload +def _encode_invalid_chars( + component: None, allowed_chars: typing.Container[str] +) -> None: # Abstract + ... + + +def _encode_invalid_chars( + component: str | None, allowed_chars: typing.Container[str] +) -> str | None: + """Percent-encodes a URI component without reapplying + onto an already percent-encoded component. + """ + if component is None: + return component + + component = to_str(component) + + # Normalize existing percent-encoded bytes. + # Try to see if the component we're encoding is already percent-encoded + # so we can skip all '%' characters but still encode all others. + component, percent_encodings = _PERCENT_RE.subn( + lambda match: match.group(0).upper(), component + ) + + uri_bytes = component.encode("utf-8", "surrogatepass") + is_percent_encoded = percent_encodings == uri_bytes.count(b"%") + encoded_component = bytearray() + + for i in range(0, len(uri_bytes)): + # Will return a single character bytestring + byte = uri_bytes[i : i + 1] + byte_ord = ord(byte) + if (is_percent_encoded and byte == b"%") or ( + byte_ord < 128 and byte.decode() in allowed_chars + ): + encoded_component += byte + continue + encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) + + return encoded_component.decode() + + +def _remove_path_dot_segments(path: str) -> str: + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = path.split("/") # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == ".": + continue + # Anything other than '..', should be appended to the output + if segment != "..": + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if path.startswith("/") and (not output or output[0]): + output.insert(0, "") + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if path.endswith(("/.", "/..")): + output.append("") + + return "/".join(output) + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + if host: + if scheme in _NORMALIZABLE_SCHEMES: + is_ipv6 = _IPV6_ADDRZ_RE.match(host) + if is_ipv6: + # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as + # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID + # separator as necessary to return a valid RFC 4007 scoped IP. + match = _ZONE_ID_RE.search(host) + if match: + start, end = match.span(1) + zone_id = host[start:end] + + if zone_id.startswith("%25") and zone_id != "%25": + zone_id = zone_id[3:] + else: + zone_id = zone_id[1:] + zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) + return f"{host[:start].lower()}%{zone_id}{host[end:]}" + else: + return host.lower() + elif not _IPV4_RE.match(host): + return to_str( + b".".join([_idna_encode(label) for label in host.split(".")]), + "ascii", + ) + return host + + +def _idna_encode(name: str) -> bytes: + if not name.isascii(): + try: + import idna + except ImportError: + raise LocationParseError( + "Unable to parse URL without the 'idna' module" + ) from None + + try: + return idna.encode(name.lower(), strict=True, std3_rules=True) + except idna.IDNAError: + raise LocationParseError( + f"Name '{name}' is not a valid IDNA label" + ) from None + + return name.lower().encode("ascii") + + +def _encode_target(target: str) -> str: + """Percent-encodes a request target so that there are no invalid characters + + Pre-condition for this function is that 'target' must start with '/'. + If that is the case then _TARGET_RE will always produce a match. + """ + match = _TARGET_RE.match(target) + if not match: # Defensive: + raise LocationParseError(f"{target!r} is not a valid request URI") + + path, query = match.groups() + encoded_target = _encode_invalid_chars(path, _PATH_CHARS) + if query is not None: + query = _encode_invalid_chars(query, _QUERY_CHARS) + encoded_target += "?" + query + return encoded_target + + +def parse_url(url: str) -> Url: + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + This parser is RFC 3986 and RFC 6874 compliant. + + The parser logic and helper functions are based heavily on + work done in the ``rfc3986`` module. + + :param str url: URL to parse into a :class:`.Url` namedtuple. + + Partly backwards-compatible with :mod:`urllib.parse`. + + Example: + + .. code-block:: python + + import urllib3 + + print( urllib3.util.parse_url('http://google.com/mail/')) + # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + + print( urllib3.util.parse_url('google.com:80')) + # Url(scheme=None, host='google.com', port=80, path=None, ...) + + print( urllib3.util.parse_url('/foo?bar')) + # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + if not url: + # Empty + return Url() + + source_url = url + if not _SCHEME_RE.search(url): + url = "//" + url + + scheme: str | None + authority: str | None + auth: str | None + host: str | None + port: str | None + port_int: int | None + path: str | None + query: str | None + fragment: str | None + + try: + scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] + normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES + + if scheme: + scheme = scheme.lower() + + if authority: + auth, _, host_port = authority.rpartition("@") + auth = auth or None + host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] + if auth and normalize_uri: + auth = _encode_invalid_chars(auth, _USERINFO_CHARS) + if port == "": + port = None + else: + auth, host, port = None, None, None + + if port is not None: + port_int = int(port) + if not (0 <= port_int <= 65535): + raise LocationParseError(url) + else: + port_int = None + + host = _normalize_host(host, scheme) + + if normalize_uri and path: + path = _remove_path_dot_segments(path) + path = _encode_invalid_chars(path, _PATH_CHARS) + if normalize_uri and query: + query = _encode_invalid_chars(query, _QUERY_CHARS) + if normalize_uri and fragment: + fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) + + except (ValueError, AttributeError) as e: + raise LocationParseError(source_url) from e + + # For the sake of backwards compatibility we put empty + # string values for path if there are any defined values + # beyond the path in the URL. + # TODO: Remove this when we break backwards compatibility. + if not path: + if query is not None or fragment is not None: + path = "" + else: + path = None + + return Url( + scheme=scheme, + auth=auth, + host=host, + port=port_int, + path=path, + query=query, + fragment=fragment, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py new file mode 100644 index 0000000000..35c77e4025 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import typing +from types import TracebackType + + +def to_bytes( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> bytes: + if isinstance(x, bytes): + return x + elif not isinstance(x, str): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.encode(encoding or "utf-8", errors=errors or "strict") + return x.encode() + + +def to_str( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> str: + if isinstance(x, str): + return x + elif not isinstance(x, bytes): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.decode(encoding or "utf-8", errors=errors or "strict") + return x.decode() + + +def reraise( + tp: type[BaseException] | None, + value: BaseException, + tb: TracebackType | None = None, +) -> typing.NoReturn: + try: + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None # type: ignore[assignment] + tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py new file mode 100644 index 0000000000..aeca0c7ad5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import select +import socket +from functools import partial + +__all__ = ["wait_for_read", "wait_for_write"] + + +# How should we wait on sockets? +# +# There are two types of APIs you can use for waiting on sockets: the fancy +# modern stateful APIs like epoll/kqueue, and the older stateless APIs like +# select/poll. The stateful APIs are more efficient when you have a lots of +# sockets to keep track of, because you can set them up once and then use them +# lots of times. But we only ever want to wait on a single socket at a time +# and don't want to keep track of state, so the stateless APIs are actually +# more efficient. So we want to use select() or poll(). +# +# Now, how do we choose between select() and poll()? On traditional Unixes, +# select() has a strange calling convention that makes it slow, or fail +# altogether, for high-numbered file descriptors. The point of poll() is to fix +# that, so on Unixes, we prefer poll(). +# +# On Windows, there is no poll() (or at least Python doesn't provide a wrapper +# for it), but that's OK, because on Windows, select() doesn't have this +# strange calling convention; plain select() works fine. +# +# So: on Windows we use select(), and everywhere else we use poll(). We also +# fall back to select() in case poll() is somehow broken or missing. + + +def select_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + rcheck = [] + wcheck = [] + if read: + rcheck.append(sock) + if write: + wcheck.append(sock) + # When doing a non-blocking connect, most systems signal success by + # marking the socket writable. Windows, though, signals success by marked + # it as "exceptional". We paper over the difference by checking the write + # sockets for both conditions. (The stdlib selectors module does the same + # thing.) + fn = partial(select.select, rcheck, wcheck, wcheck) + rready, wready, xready = fn(timeout) + return bool(rready or wready or xready) + + +def poll_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + mask = 0 + if read: + mask |= select.POLLIN + if write: + mask |= select.POLLOUT + poll_obj = select.poll() + poll_obj.register(sock, mask) + + # For some reason, poll() takes timeout in milliseconds + def do_poll(t: float | None) -> list[tuple[int, int]]: + if t is not None: + t *= 1000 + return poll_obj.poll(t) + + return bool(do_poll(timeout)) + + +def _have_working_poll() -> bool: + # Apparently some systems have a select.poll that fails as soon as you try + # to use it, either due to strange configuration or broken monkeypatching + # from libraries like eventlet/greenlet. + try: + poll_obj = select.poll() + poll_obj.poll(0) + except (AttributeError, OSError): + return False + else: + return True + + +def wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + # We delay choosing which implementation to use until the first time we're + # called. We could do it at import time, but then we might make the wrong + # decision if someone goes wild with monkeypatching select.poll after + # we're imported. + global wait_for_socket + if _have_working_poll(): + wait_for_socket = poll_wait_for_socket + elif hasattr(select, "select"): + wait_for_socket = select_wait_for_socket + return wait_for_socket(sock, read, write, timeout) + + +def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for reading to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, read=True, timeout=timeout) + + +def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for writing to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.lock b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER new file mode 100644 index 0000000000..5c69047b2e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA new file mode 100644 index 0000000000..0d4f101491 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.6.17 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/latest/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD new file mode 100644 index 0000000000..13b0ce7da4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD @@ -0,0 +1,12 @@ +certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 +certifi-2026.6.17.dist-info/RECORD,, +certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL new file mode 100644 index 0000000000..14a883f292 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE new file mode 100644 index 0000000000..62b076cdee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py new file mode 100644 index 0000000000..ed9a74b7a0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py new file mode 100644 index 0000000000..8945b5da85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem new file mode 100644 index 0000000000..1c2dbfeb68 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem @@ -0,0 +1,3863 @@ + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Label: "OISTE Server Root ECC G1" +# Serial: 47819833811561661340092227008453318557 +# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 +# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 +# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Label: "OISTE Server Root RSA G1" +# Serial: 113845518112613905024960613408179309848 +# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 +# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e +# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Label: "e-Szigno TLS Root CA 2023" +# Serial: 71934828665710877219916191754 +# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 +# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe +# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py new file mode 100644 index 0000000000..1c9661cc7c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/index.py new file mode 100644 index 0000000000..6f3a7a8126 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/index.py @@ -0,0 +1,21 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={ + "data_collection": { + "http_headers": { + "request": {"mode": "off"}, + } + } + }, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py new file mode 100644 index 0000000000..8ce8d739c9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py @@ -0,0 +1,70 @@ +from sentry_sdk import metrics, profiler + +from sentry_sdk.scope import Scope # isort: skip +from sentry_sdk.client import Client # isort: skip +from sentry_sdk.consts import VERSION +from sentry_sdk.transport import HttpTransport, Transport + +from sentry_sdk.api import * # noqa # isort: skip + +__all__ = [ # noqa + "Hub", + "Scope", + "Client", + "Transport", + "HttpTransport", + "VERSION", + "integrations", + # From sentry_sdk.api + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "logger", + "metrics", + "profiler", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + +# Initialize the debug support after everything is loaded +from sentry_sdk.debug import init_debug_support + +init_debug_support() +del init_debug_support + +# circular imports +from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py new file mode 100644 index 0000000000..565fac2a2d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py @@ -0,0 +1,184 @@ +import os +import random +import threading +import weakref +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Generic, TypeVar + +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + +T = TypeVar("T") + + +class Batcher(Generic[T]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "" + CONTENT_TYPE = "" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + self._buffer: "list[T]" = [] + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._flush_event: "threading.Event" = threading.Event() + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._buffer = [] + self._running = True + self._lock = threading.Lock() + self._active = threading.local() + self._flush_event = threading.Event() + self._flusher = None + self._flusher_pid = None + + def _ensure_thread(self) -> bool: + """For forking processes we might need to restart this thread. + This ensures that our process actually has that thread running. + """ + if not self._running: + return False + + pid = os.getpid() + if self._flusher_pid == pid: + return True + + with self._lock: + # Recheck to make sure another thread didn't get here and start the + # the flusher in the meantime + if self._flusher_pid == pid: + return True + + self._flusher_pid = pid + + self._flusher = threading.Thread(target=self._flush_loop) + self._flusher.daemon = True + + try: + self._flusher.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return False + + return True + + def _flush_loop(self) -> None: + # Mark the flush-loop thread as active for its entire lifetime so + # that any re-entrant add() triggered by GC warnings during wait(), + # flush(), or Event operations is silently dropped instead of + # deadlocking on internal locks. + self._active.flag = True + while self._running: + self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) + self._flush_event.clear() + self._flush() + + def add(self, item: "T") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + if len(self._buffer) >= self.MAX_BEFORE_DROP: + self._record_lost(item) + return None + + self._buffer.append(item) + if len(self._buffer) >= self.MAX_BEFORE_FLUSH: + self._flush_event.set() + finally: + self._active.flag = False + + def kill(self) -> None: + if self._flusher is None: + return + + self._running = False + self._flush_event.set() + self._flusher = None + + def flush(self) -> None: + was_active = getattr(self._active, "flag", False) + self._active.flag = True + try: + self._flush() + finally: + self._active.flag = was_active + + def _add_to_envelope(self, envelope: "Envelope") -> None: + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": len(self._buffer), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(item) for item in self._buffer + ], + } + ), + ) + ) + + def _flush(self) -> "Optional[Envelope]": + envelope = Envelope( + headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + ) + with self._lock: + if len(self._buffer) == 0: + return None + + self._add_to_envelope(envelope) + self._buffer.clear() + + self._capture_func(envelope) + return envelope + + def _record_lost(self, item: "T") -> None: + pass + + @staticmethod + def _to_transport_format(item: "T") -> "Any": + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py new file mode 100644 index 0000000000..f62175c09f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py @@ -0,0 +1,92 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, TypeVar + + T = TypeVar("T") + + +PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 +PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 +PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 +PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 + + +def with_metaclass(meta: "Any", *bases: "Any") -> "Any": + class MetaClass(type): + def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": + return meta(name, bases, d) + + return type.__new__(MetaClass, "temporary_class", (), {}) + + +def check_uwsgi_thread_support() -> bool: + # We check two things here: + # + # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if + # that's the case. + # + # 2. Additionally, if uWSGI is running in preforking mode (default), it needs + # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This + # is because any background threads spawned before the main process is + # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if + # --enable-threads is on. One has to explicitly provide + # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython + # after-fork hooks that take care of cleaning up stale thread data. + try: + from uwsgi import opt # type: ignore + except ImportError: + return True + + from sentry_sdk.consts import FALSE_VALUES + + def enabled(option: str) -> bool: + value = opt.get(option, False) + if isinstance(value, bool): + return value + + if isinstance(value, bytes): + try: + value = value.decode() + except Exception: + pass + + return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] + + # When `threads` is passed in as a uwsgi option, + # `enable-threads` is implied on. + threads_enabled = "threads" in opt or enabled("enable-threads") + fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") + lazy_mode = enabled("lazy-apps") or enabled("lazy") + + if lazy_mode and not threads_enabled: + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI without thread support. " + "This might lead to unexpected issues. " + 'Please run uWSGI with "--enable-threads" for full support.' + ) + ) + + return False + + elif not lazy_mode and (not threads_enabled or not fork_hooks_on): + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI in preforking mode without " + "thread support. This might lead to crashing workers. " + 'Please run uWSGI with both "--enable-threads" and ' + '"--py-call-uwsgi-fork-hooks" for full support.' + ) + ) + + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py new file mode 100644 index 0000000000..923fcf6df8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py @@ -0,0 +1,78 @@ +import warnings +from typing import TYPE_CHECKING + +import sentry_sdk + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional + + import sentry_sdk.consts + + +class _InitGuard: + _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( + "Using the return value of sentry_sdk.init as a context manager " + "and manually calling the __enter__ and __exit__ methods on the " + "return value are deprecated. We are no longer maintaining this " + "functionality, and we will remove it in the next major release." + ) + + def __init__(self, client: "sentry_sdk.Client") -> None: + self._client = client + + def __enter__(self) -> "_InitGuard": + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + c = self._client + if c is not None: + c.close() + + +def _check_python_deprecations() -> None: + # Since we're likely to deprecate Python versions in the future, I'm keeping + # this handy function around. Use this to detect the Python version used and + # to output logger.warning()s if it's deprecated. + pass + + +def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": + """Initializes the SDK and optionally integrations. + + This takes the same arguments as the client constructor. + """ + client = sentry_sdk.Client(*args, **kwargs) + sentry_sdk.get_global_scope().set_client(client) + _check_python_deprecations() + rv = _InitGuard(client) + return rv + + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `init` is a type to + # have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `ContextManager[Any]` to tell static analyzers about the return type. + + class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 + pass + +else: + # Alias `init` for actual usage. Go through the lambda indirection to throw + # PyCharm off of the weakly typed signature (it would otherwise discover + # both the weakly typed signature of `_init` and our faked `init` type). + + init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py new file mode 100644 index 0000000000..0719932ee9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Item, PayloadRef +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Log + + +class LogBatcher(Batcher["Log"]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "log" + CONTENT_TYPE = "application/vnd.sentry.items.log+json" + + @staticmethod + def _to_transport_format(item: "Log") -> "Any": + if "sentry.severity_number" not in item["attributes"]: + item["attributes"]["sentry.severity_number"] = item["severity_number"] + if "sentry.severity_text" not in item["attributes"]: + item["attributes"]["sentry.severity_text"] = item["severity_text"] + + res = { + "timestamp": int(item["time_unix_nano"]) / 1.0e9, + "level": str(item["severity_text"]), + "body": str(item["body"]), + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + return res + + def _record_lost(self, item: "Log") -> None: + # Construct log envelope item without sending it to report lost bytes + log_item = Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": 1, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [self._to_transport_format(item)], + } + ), + ) + + self._record_lost_func( + reason="queue_overflow", + data_category="log_item", + item=log_item, + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py new file mode 100644 index 0000000000..16c238bcab --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py @@ -0,0 +1,43 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +_SENTINEL = object() + + +class LRUCache: + def __init__(self, max_size: int) -> None: + if max_size <= 0: + raise AssertionError(f"invalid max_size: {max_size}") + self.max_size = max_size + self._data: "dict[Any, Any]" = {} + self.hits = self.misses = 0 + self.full = False + + def set(self, key: "Any", value: "Any") -> None: + current = self._data.pop(key, _SENTINEL) + if current is not _SENTINEL: + self._data[key] = value + elif self.full: + self._data.pop(next(iter(self._data))) + self._data[key] = value + else: + self._data[key] = value + self.full = len(self._data) >= self.max_size + + def get(self, key: "Any", default: "Any" = None) -> "Any": + try: + ret = self._data.pop(key) + except KeyError: + self.misses += 1 + ret = default + else: + self.hits += 1 + self._data[key] = ret + + return ret + + def get_all(self) -> "list[tuple[Any, Any]]": + return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py new file mode 100644 index 0000000000..06bce1a282 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Metric + + +class MetricsBatcher(Batcher["Metric"]): + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 10_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "trace_metric" + CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" + + @staticmethod + def _to_transport_format(item: "Metric") -> "Any": + res = { + "timestamp": item["timestamp"], + "name": item["name"], + "type": item["type"], + "value": item["value"], + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + if item.get("unit") is not None: + res["unit"] = item["unit"] + + return res + + def _record_lost(self, item: "Metric") -> None: + self._record_lost_func( + reason="queue_overflow", + data_category="trace_metric", + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py new file mode 100644 index 0000000000..c28c8de9ac --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py @@ -0,0 +1,287 @@ +""" +A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') +with Lock swapped out for RLock to avoid a deadlock while garbage collecting. + +https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py + + +See also +https://codewithoutrules.com/2017/08/16/concurrency-python/ +https://bugs.python.org/issue14976 +https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 + +We also vendor the code to evade eventlet's broken monkeypatching, see +https://github.com/getsentry/sentry-python/pull/484 + + +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; + +All Rights Reserved + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + +""" + +import threading +from collections import deque +from time import time +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +__all__ = ["EmptyError", "FullError", "Queue"] + + +class EmptyError(Exception): + "Exception raised by Queue.get(block=0)/get_nowait()." + + pass + + +class FullError(Exception): + "Exception raised by Queue.put(block=0)/put_nowait()." + + pass + + +class Queue: + """Create a queue object with a given maximum size. + + If maxsize is <= 0, the queue size is infinite. + """ + + def __init__(self, maxsize=0): + self.maxsize = maxsize + self._init(maxsize) + + # mutex must be held whenever the queue is mutating. All methods + # that acquire mutex must release it before returning. mutex + # is shared between the three conditions, so acquiring and + # releasing the conditions also acquires and releases mutex. + self.mutex = threading.RLock() + + # Notify not_empty whenever an item is added to the queue; a + # thread waiting to get is notified then. + self.not_empty = threading.Condition(self.mutex) + + # Notify not_full whenever an item is removed from the queue; + # a thread waiting to put is notified then. + self.not_full = threading.Condition(self.mutex) + + # Notify all_tasks_done whenever the number of unfinished tasks + # drops to zero; thread waiting to join() is notified to resume + self.all_tasks_done = threading.Condition(self.mutex) + self.unfinished_tasks = 0 + + def task_done(self): + """Indicate that a formerly enqueued task is complete. + + Used by Queue consumer threads. For each get() used to fetch a task, + a subsequent call to task_done() tells the queue that the processing + on the task is complete. + + If a join() is currently blocking, it will resume when all items + have been processed (meaning that a task_done() call was received + for every item that had been put() into the queue). + + Raises a ValueError if called more times than there were items + placed in the queue. + """ + with self.all_tasks_done: + unfinished = self.unfinished_tasks - 1 + if unfinished <= 0: + if unfinished < 0: + raise ValueError("task_done() called too many times") + self.all_tasks_done.notify_all() + self.unfinished_tasks = unfinished + + def join(self): + """Blocks until all items in the Queue have been gotten and processed. + + The count of unfinished tasks goes up whenever an item is added to the + queue. The count goes down whenever a consumer thread calls task_done() + to indicate the item was retrieved and all work on it is complete. + + When the count of unfinished tasks drops to zero, join() unblocks. + """ + with self.all_tasks_done: + while self.unfinished_tasks: + self.all_tasks_done.wait() + + def qsize(self): + """Return the approximate size of the queue (not reliable!).""" + with self.mutex: + return self._qsize() + + def empty(self): + """Return True if the queue is empty, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() == 0 + as a direct substitute, but be aware that either approach risks a race + condition where a queue can grow before the result of empty() or + qsize() can be used. + + To create code that needs to wait for all queued tasks to be + completed, the preferred technique is to use the join() method. + """ + with self.mutex: + return not self._qsize() + + def full(self): + """Return True if the queue is full, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() >= n + as a direct substitute, but be aware that either approach risks a race + condition where a queue can shrink before the result of full() or + qsize() can be used. + """ + with self.mutex: + return 0 < self.maxsize <= self._qsize() + + def put(self, item, block=True, timeout=None): + """Put an item into the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until a free slot is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the FullError exception if no free slot was available within that time. + Otherwise ('block' is false), put an item on the queue if a free slot + is immediately available, else raise the FullError exception ('timeout' + is ignored in that case). + """ + with self.not_full: + if self.maxsize > 0: + if not block: + if self._qsize() >= self.maxsize: + raise FullError() + elif timeout is None: + while self._qsize() >= self.maxsize: + self.not_full.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while self._qsize() >= self.maxsize: + remaining = endtime - time() + if remaining <= 0.0: + raise FullError() + self.not_full.wait(remaining) + self._put(item) + self.unfinished_tasks += 1 + self.not_empty.notify() + + def get(self, block=True, timeout=None): + """Remove and return an item from the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until an item is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the EmptyError exception if no item was available within that time. + Otherwise ('block' is false), return an item if one is immediately + available, else raise the EmptyError exception ('timeout' is ignored + in that case). + """ + with self.not_empty: + if not block: + if not self._qsize(): + raise EmptyError() + elif timeout is None: + while not self._qsize(): + self.not_empty.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while not self._qsize(): + remaining = endtime - time() + if remaining <= 0.0: + raise EmptyError() + self.not_empty.wait(remaining) + item = self._get() + self.not_full.notify() + return item + + def put_nowait(self, item): + """Put an item into the queue without blocking. + + Only enqueue the item if a free slot is immediately available. + Otherwise raise the FullError exception. + """ + return self.put(item, block=False) + + def get_nowait(self): + """Remove and return an item from the queue without blocking. + + Only get an item if one is immediately available. Otherwise + raise the EmptyError exception. + """ + return self.get(block=False) + + # Override these methods to implement other queue organizations + # (e.g. stack or priority queue). + # These will only be called with appropriate locks held + + # Initialize the queue representation + def _init(self, maxsize): + self.queue: "Any" = deque() + + def _qsize(self): + return len(self.queue) + + # Put a new item in the queue + def _put(self, item): + self.queue.append(item) + + # Get an item from the queue + def _get(self): + return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py new file mode 100644 index 0000000000..79285c3386 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py @@ -0,0 +1,234 @@ +import os +import random +import threading +import time +import weakref +from collections import defaultdict +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp, serialize_attribute + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + from sentry_sdk._types import SpanJSON + + +class SpanBatcher(Batcher["SpanJSON"]): + # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is + # a bit of a buffer for spans that appear between the trigger to flush + # and actually flushing the buffer. + # + # The max limits are all per trace (per bucket). + MAX_ENVELOPE_SIZE = 1000 # spans + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 2000 + MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB + + FLUSH_WAIT_TIME = 5.0 + + TYPE = "span" + CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + # Spans from different traces cannot be emitted in the same envelope + # since the envelope contains a shared trace header. That's why we bucket + # by trace_id, so that we can then send the buckets each in its own + # envelope. + # trace_id -> span buffer + self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) + self._running_size: dict[str, int] = defaultdict(lambda: 0) + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._last_full_flush: float = time.monotonic() # drives time-based flushes + self._flush_event = threading.Event() + self._pending_flush: set[str] = set() # buckets to be flushed + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._span_buffer = defaultdict(list) + self._running_size = defaultdict(lambda: 0) + self._running = True + + self._lock = threading.Lock() + self._active = threading.local() + + self._last_full_flush = time.monotonic() + self._flush_event = threading.Event() + self._pending_flush = set() + + self._flusher = None + self._flusher_pid = None + + def _flush_loop(self) -> None: + self._active.flag = True + while self._running: + jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 + self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) + self._flush_event.clear() + + self._flush(only_pending=True) + + if ( + time.monotonic() - self._last_full_flush + >= self.FLUSH_WAIT_TIME + jitter + ): + self._flush() + self._last_full_flush = time.monotonic() + + def add(self, span: "SpanJSON") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + size = len(self._span_buffer[span["trace_id"]]) + if size >= self.MAX_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="span", + quantity=1, + ) + return None + + self._span_buffer[span["trace_id"]].append(span) + self._running_size[span["trace_id"]] += self._estimate_size(span) + + if ( + size + 1 >= self.MAX_BEFORE_FLUSH + or self._running_size[span["trace_id"]] + >= self.MAX_BYTES_BEFORE_FLUSH + ): + self._pending_flush.add(span["trace_id"]) + notify = True + else: + notify = False + + if notify: + self._flush_event.set() + finally: + self._active.flag = False + + @staticmethod + def _estimate_size(item: "SpanJSON") -> int: + # Rough estimate of serialized span size that's quick to compute. + # 210 is the rough size of the payload without attributes, and then we + # estimate the attributes separately. + estimate = 210 + for value in (item.get("attributes") or {}).values(): + estimate += 50 + + if isinstance(value, str): + estimate += len(value) + else: + estimate += len(str(value)) + + return estimate + + @staticmethod + def _to_transport_format(item: "SpanJSON") -> "Any": + res = {k: v for k, v in item.items() if k not in ("_segment_span",)} + + if item.get("attributes"): + res["attributes"] = { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + } + else: + del res["attributes"] + + return res + + def _flush(self, only_pending: bool = False) -> None: + with self._lock: + if only_pending: + buckets = list(self._pending_flush) + else: + # flush whole buffer, e.g. if the SDK is shutting down + buckets = list(self._span_buffer.keys()) + + self._pending_flush.clear() + + if not buckets: + return + + envelopes = [] + + for bucket_id in buckets: + spans = self._span_buffer.get(bucket_id) + if not spans: + continue + + dsc = spans[0]["_segment_span"]._dynamic_sampling_context() + + # Max per envelope is 1000, so if we happen to have more than + # 1000 spans in one bucket, we'll need to separate them. + for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): + end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) + + envelope = Envelope( + headers={ + "sent_at": format_timestamp(datetime.now(timezone.utc)), + "trace": dsc, + } + ) + + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": end - start, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(spans[j]) + for j in range(start, end) + ], + } + ), + ) + ) + + envelopes.append(envelope) + + del self._span_buffer[bucket_id] + del self._running_size[bucket_id] + + for envelope in envelopes: + self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py new file mode 100644 index 0000000000..fbd2578048 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py @@ -0,0 +1,481 @@ +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING, TypeVar, Union + +# Re-exported for compat, since code out there in the wild might use this variable. +MYPY = TYPE_CHECKING + + +SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" +BLOB_DATA_SUBSTITUTE = "[Blob substitute]" +OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" +UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" + + +class AnnotatedValue: + """ + Meta information for a data field in the event payload. + This is to tell Relay that we have tampered with the fields value. + See: + https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 + """ + + __slots__ = ("value", "metadata") + + def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: + self.value = value + self.metadata = metadata + + def __eq__(self, other: "Any") -> bool: + if not isinstance(other, AnnotatedValue): + return False + + return self.value == other.value and self.metadata == other.metadata + + def __str__(self: "AnnotatedValue") -> str: + return str({"value": str(self.value), "metadata": str(self.metadata)}) + + def __len__(self: "AnnotatedValue") -> int: + if self.value is not None: + return len(self.value) + else: + return 0 + + @classmethod + def removed_because_raw_data(cls) -> "AnnotatedValue": + """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" + # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition + # to span-first + return AnnotatedValue( + value="", + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_raw_data(cls) -> "AnnotatedValue": + """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" + return AnnotatedValue( + value=UNPARSABLE_RAW_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": + """ + The actual value was removed because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition + # to span-first + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_over_size_limit( + cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE + ) -> "AnnotatedValue": + """ + The actual value was replaced because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": + """The actual value was removed because it contained sensitive information.""" + return AnnotatedValue( + value=SENSITIVE_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) + "s", # The fields original value was substituted + ] + ] + }, + ) + + +T = TypeVar("T") +Annotated = Union[AnnotatedValue, T] + + +if TYPE_CHECKING: + from collections.abc import Container, MutableMapping, Sequence + from datetime import datetime + from types import TracebackType + from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + + class SDKInfo(TypedDict): + name: str + version: str + packages: "Sequence[Mapping[str, str]]" + + class KeyValueCollectionBehaviour(TypedDict): + mode: 'Literal["off", "denylist", "allowlist"]' + 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): + request: "KeyValueCollectionBehaviour" + + class HttpHeadersCollectionBehaviour(TypedDict): + request: "KeyValueCollectionBehaviour" + + class DataCollectionUserOptions(TypedDict, total=False): + user_info: bool + cookies: "KeyValueCollectionBehaviour" + http_headers: "HttpHeadersCollectionUserOptions" + http_bodies: "List[str]" + query_params: "KeyValueCollectionBehaviour" + graphql: "GraphQLCollectionUserOptions" + gen_ai: "GenAICollectionUserOptions" + database_query_data: bool + queues: bool + 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_query_data: bool + queues: bool + 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"] + + DurationUnit = Literal[ + "nanosecond", + "microsecond", + "millisecond", + "second", + "minute", + "hour", + "day", + "week", + ] + + InformationUnit = Literal[ + "bit", + "byte", + "kilobyte", + "kibibyte", + "megabyte", + "mebibyte", + "gigabyte", + "gibibyte", + "terabyte", + "tebibyte", + "petabyte", + "pebibyte", + "exabyte", + "exbibyte", + ] + + FractionUnit = Literal["ratio", "percent"] + MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] + + MeasurementValue = TypedDict( + "MeasurementValue", + { + "value": float, + "unit": NotRequired[Optional[MeasurementUnit]], + }, + ) + + Event = TypedDict( + "Event", + { + "breadcrumbs": Annotated[ + dict[Literal["values"], list[dict[str, Any]]] + ], # TODO: We can expand on this type + "check_in_id": str, + "contexts": dict[str, dict[str, object]], + "dist": str, + "duration": Optional[float], + "environment": Optional[str], + "errors": list[dict[str, Any]], # TODO: We can expand on this type + "event_id": str, + "exception": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "extra": MutableMapping[str, object], + "fingerprint": list[str], + "level": LogLevelStr, + "logentry": Mapping[str, object], + "logger": str, + "measurements": dict[str, MeasurementValue], + "message": str, + "modules": dict[str, str], + "monitor_config": Mapping[str, object], + "monitor_slug": Optional[str], + "platform": Literal["python"], + "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports + "release": Optional[str], + "request": dict[str, object], + "sdk": Mapping[str, object], + "server_name": str, + "spans": Annotated[list[dict[str, object]]], + "stacktrace": dict[ + str, object + ], # We access this key in the code, but I am unsure whether we ever set it + "start_timestamp": datetime, + "status": Optional[str], + "tags": MutableMapping[ + str, str + ], # Tags must be less than 200 characters each + "threads": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "timestamp": Optional[datetime], # Must be set before sending the event + "transaction": str, + "transaction_info": Mapping[str, Any], # TODO: We can expand on this type + "type": Literal["check_in", "transaction"], + "user": dict[str, object], + "_dropped_spans": int, + "_has_gen_ai_span": bool, + }, + total=False, + ) + + ExcInfo = Union[ + tuple[Type[BaseException], BaseException, Optional[TracebackType]], + tuple[None, None, None], + ] + + # TODO: Make a proper type definition for this (PRs welcome!) + Hint = Dict[str, Any] + + AttributeValue = ( + str + | bool + | float + | int + | list[str] + | list[bool] + | list[float] + | list[int] + | tuple[str, ...] + | tuple[bool, ...] + | tuple[float, ...] + | tuple[int, ...] + ) + Attributes = dict[str, AttributeValue] + + SerializedAttributeValue = TypedDict( + # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types + "SerializedAttributeValue", + { + "type": Literal[ + "string", + "boolean", + "double", + "integer", + "array", + ], + "value": AttributeValue, + }, + ) + + Log = TypedDict( + "Log", + { + "severity_text": str, + "severity_number": int, + "body": str, + "attributes": Attributes, + "time_unix_nano": int, + "trace_id": Optional[str], + "span_id": Optional[str], + }, + ) + + MetricType = Literal["counter", "gauge", "distribution"] + MetricUnit = Union[DurationUnit, InformationUnit, str] + + Metric = TypedDict( + "Metric", + { + "timestamp": float, + "trace_id": Optional[str], + "span_id": Optional[str], + "name": str, + "type": MetricType, + "value": float, + "unit": Optional[MetricUnit], + "attributes": Attributes, + }, + ) + + MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] + + SpanJSON = TypedDict( + "SpanJSON", + { + "trace_id": str, + "span_id": str, + "parent_span_id": NotRequired[str], + "name": str, + "status": str, + "is_segment": bool, + "start_timestamp": float, + "end_timestamp": NotRequired[float], + "attributes": NotRequired[Attributes], + "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], + }, + ) + + # TODO: Make a proper type definition for this (PRs welcome!) + Breadcrumb = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + BreadcrumbHint = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + SamplingContext = Dict[str, Any] + + EventProcessor = Callable[[Event, Hint], Optional[Event]] + ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] + BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] + TransactionProcessor = Callable[[Event, Hint], Optional[Event]] + LogProcessor = Callable[[Log, Hint], Optional[Log]] + + TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] + + # https://github.com/python/mypy/issues/5710 + NotImplementedType = Any + + EventDataCategory = Literal[ + "default", + "error", + "crash", + "transaction", + "security", + "attachment", + "session", + "internal", + "profile", + "profile_chunk", + "monitor", + "span", + "log_item", + "log_byte", + "trace_metric", + ] + SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] + + ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] + ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] + + MonitorConfigScheduleType = Literal["crontab", "interval"] + MonitorConfigScheduleUnit = Literal[ + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", # not supported in Sentry and will result in a warning + ] + + MonitorConfigSchedule = TypedDict( + "MonitorConfigSchedule", + { + "type": MonitorConfigScheduleType, + "value": Union[int, str], + "unit": MonitorConfigScheduleUnit, + }, + total=False, + ) + + MonitorConfig = TypedDict( + "MonitorConfig", + { + "schedule": MonitorConfigSchedule, + "timezone": str, + "checkin_margin": int, + "max_runtime": int, + "failure_issue_threshold": int, + "recovery_threshold": int, + "owner": str, + }, + total=False, + ) + + HttpStatusCodeRange = Union[int, Container[int]] + + class TextPart(TypedDict): + type: Literal["text"] + content: str + + IgnoreSpansName = Union[str, Pattern[str]] + IgnoreSpansContext = TypedDict( + "IgnoreSpansContext", + {"name": IgnoreSpansName, "attributes": Attributes}, + total=False, + ) + IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py new file mode 100644 index 0000000000..98f932267f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py @@ -0,0 +1,100 @@ +""" +Copyright (c) 2007 by the Pallets team. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Dict, Iterator, Optional, Tuple + + +# +# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` +# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 +# +# We need this function because Django does not give us a "pure" http header +# dict. So we might as well use it for all WSGI integrations. +# +def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns only proper HTTP headers. + """ + for key, value in environ.items(): + key = str(key) + if key.startswith("HTTP_") and key not in ( + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + ): + yield key[5:].replace("_", "-").title(), value + elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): + yield key.replace("_", "-").title(), value + + +def _strip_default_port(host: str, scheme: "Optional[str]") -> str: + """Strip the port from the host if it's the default for the scheme.""" + if scheme == "http" and host.endswith(":80"): + return host[:-3] + if scheme == "https" and host.endswith(":443"): + return host[:-4] + return host + + +# `get_host` comes from `werkzeug.wsgi.get_host` +# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 + + +def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: + """ + Return the host for the given WSGI environment. + """ + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: + return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) + elif environ.get("HTTP_HOST"): + return _strip_default_port(environ["HTTP_HOST"], scheme) + elif environ.get("SERVER_NAME"): + # SERVER_NAME/SERVER_PORT describe the internal server, so use + # wsgi.url_scheme (not the forwarded scheme) for port decisions. + rv = environ["SERVER_NAME"] + if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( + ("https", "443"), + ("http", "80"), + ): + rv += ":" + environ["SERVER_PORT"] + return rv + else: + # In spite of the WSGI spec, SERVER_NAME might not be present. + return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py new file mode 100644 index 0000000000..404e57ff1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py @@ -0,0 +1,8 @@ +from .utils import ( + GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 + GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 + normalize_message_role, # noqa: F401 + normalize_message_roles, # noqa: F401 + set_conversation_id, # noqa: F401 + set_data_normalized, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py new file mode 100644 index 0000000000..b5eb8c55ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py @@ -0,0 +1,66 @@ +from collections.abc import Iterable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.chat import ( + ChatCompletionContentPartParam, + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ) + + from sentry_sdk._types import TextPart + + +def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: + return isinstance(message, dict) and message.get("role") == "system" + + +def _get_system_instructions( + messages: "Iterable[ChatCompletionMessageParam]", +) -> "list[ChatCompletionMessageParam]": + if not isinstance(messages, Iterable): + return [] + + return [message for message in messages if _is_system_instruction(message)] + + +def _get_text_items( + content: "Union[str, Iterable[ChatCompletionContentPartParam]]", +) -> "list[str]": + if isinstance(content, str): + return [content] + + if not isinstance(content, Iterable): + return [] + + text_items = [] + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text", None) + if text is not None: + text_items.append(text) + + return text_items + + +def _transform_system_instructions( + system_instructions: "list[ChatCompletionSystemMessageParam]", +) -> "list[TextPart]": + instruction_text_parts: "list[TextPart]" = [] + + for instruction in system_instructions: + if not isinstance(instruction, dict): + continue + + content = instruction.get("content") + if content is None: + continue + + text_parts: "list[TextPart]" = [ + {"type": "text", "content": text} for text in _get_text_items(content) + ] + instruction_text_parts += text_parts + + return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py new file mode 100644 index 0000000000..8f751c3248 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.responses import ResponseInputItemParam, ResponseInputParam + + +def _is_system_instruction(message: "ResponseInputItemParam") -> bool: + if not isinstance(message, dict) or not message.get("role") == "system": + return False + + return "type" not in message or message["type"] == "message" + + +def _get_system_instructions( + messages: "Union[str, ResponseInputParam]", +) -> "list[ResponseInputItemParam]": + if not isinstance(messages, list): + return [] + + return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py new file mode 100644 index 0000000000..35ee4bd788 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py @@ -0,0 +1,6 @@ +import re + +# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." +DATA_URL_BASE64_REGEX = re.compile( + r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py new file mode 100644 index 0000000000..d8840ad451 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py @@ -0,0 +1,147 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk.utils +from sentry_sdk import start_span +from sentry_sdk.ai.utils import _set_span_data_attribute +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, TypeVar, Union + + F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) + +_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) + + +def set_ai_pipeline_name(name: "Optional[str]") -> None: + _ai_pipeline_name.set(name) + + +def get_ai_pipeline_name() -> "Optional[str]": + return _ai_pipeline_name.get() + + +def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": + def decorator(f: "F") -> "F": + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return await f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = await f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + if inspect.iscoroutinefunction(f): + return wraps(f)(async_wrapped) # type: ignore + else: + return wraps(f)(sync_wrapped) # type: ignore + + return decorator + + +def record_token_usage( + span: "Union[Span, StreamedSpan]", + input_tokens: "Optional[int]" = None, + input_tokens_cached: "Optional[int]" = None, + input_tokens_cache_write: "Optional[int]" = None, + output_tokens: "Optional[int]" = None, + output_tokens_reasoning: "Optional[int]" = None, + total_tokens: "Optional[int]" = None, +) -> None: + # TODO: move pipeline name elsewhere + ai_pipeline_name = get_ai_pipeline_name() + if ai_pipeline_name: + _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) + + if input_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if input_tokens_cached is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + input_tokens_cached, + ) + + if input_tokens_cache_write is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + input_tokens_cache_write, + ) + + if output_tokens is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + ) + + if output_tokens_reasoning is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + output_tokens_reasoning, + ) + + if total_tokens is None and input_tokens is not None and output_tokens is not None: + total_tokens = input_tokens + output_tokens + + if total_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py new file mode 100644 index 0000000000..7b1ca9324b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py @@ -0,0 +1,782 @@ +import inspect +import json +from copy import deepcopy +from typing import TYPE_CHECKING + +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import logger + +MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB +# Maximum characters when only a single message is left after bytes truncation +MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 + + +class GEN_AI_ALLOWED_MESSAGE_ROLES: + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { + GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], + GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], + GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], + GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], +} + +GEN_AI_MESSAGE_ROLE_MAPPING = {} +for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): + for source_role in source_roles: + GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role + + +def parse_data_uri(url: str) -> "Tuple[str, str]": + """ + Parse a data URI and return (mime_type, content). + + Data URI format (RFC 2397): data:[][;base64], + + Examples: + data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") + data:text/plain,Hello → ("text/plain", "Hello") + data:;base64,SGVsbG8= → ("", "SGVsbG8=") + + Raises: + ValueError: If the URL is not a valid data URI (missing comma separator) + """ + if "," not in url: + raise ValueError("Invalid data URI: missing comma separator") + + header, content = url.split(",", 1) + + # Extract mime type from header + # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" + # Remove "data:" prefix, then take everything before the first semicolon + if header.startswith("data:"): + mime_part = header[5:] # Remove "data:" prefix + else: + mime_part = header + + mime_type = mime_part.split(";")[0] + + return mime_type, content + + +def get_modality_from_mime_type(mime_type: str) -> str: + """ + Infer the content modality from a MIME type string. + + Args: + mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") + + Returns: + One of: "image", "audio", "video", or "document" + Defaults to "image" for unknown or empty MIME types. + + Examples: + "image/jpeg" -> "image" + "audio/mp3" -> "audio" + "video/mp4" -> "video" + "application/pdf" -> "document" + "text/plain" -> "document" + """ + if not mime_type: + return "image" # Default fallback + + mime_lower = mime_type.lower() + if mime_lower.startswith("image/"): + return "image" + elif mime_lower.startswith("audio/"): + return "audio" + elif mime_lower.startswith("video/"): + return "video" + elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): + return "document" + else: + return "image" # Default fallback for unknown types + + +def transform_openai_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an OpenAI/LiteLLM content part to Sentry's standardized format. + + This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. + + Input format: + - {"type": "image_url", "image_url": {"url": "..."}} + - {"type": "image_url", "image_url": "..."} (string shorthand) + + Output format (one of): + - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from OpenAI/LiteLLM + + Returns: + A transformed dictionary in standardized format, or None if the format + is not OpenAI image_url format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type != "image_url": + return None + + image_url_data = content_part.get("image_url") + if isinstance(image_url_data, str): + url = image_url_data + elif isinstance(image_url_data, dict): + url = image_url_data.get("url", "") + else: + return None + + if not url: + return None + + # Check if it's a data URI (base64 encoded) + if url.startswith("data:"): + try: + mime_type, content = parse_data_uri(url) + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": content, + } + except ValueError: + # If parsing fails, return as URI + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + else: + # Regular URL + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + + +def transform_anthropic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an Anthropic content part to Sentry's standardized format. + + This handles the Anthropic image and document formats with source dictionaries. + + Input format: + - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} + - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} + - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} + - {"type": "document", "source": {...}} (same source formats) + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from Anthropic + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Anthropic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "document") or "source" not in content_part: + return None + + source = content_part.get("source") + if not isinstance(source, dict): + return None + + source_type = source.get("type") + media_type = source.get("media_type", "") + modality = ( + "document" + if block_type == "document" + else get_modality_from_mime_type(media_type) + ) + + if source_type == "base64": + return { + "type": "blob", + "modality": modality, + "mime_type": media_type, + "content": source.get("data", ""), + } + elif source_type == "url": + return { + "type": "uri", + "modality": modality, + "mime_type": media_type, + "uri": source.get("url", ""), + } + elif source_type == "file": + return { + "type": "file", + "modality": modality, + "mime_type": media_type, + "file_id": source.get("file_id", ""), + } + + return None + + +def transform_google_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a Google GenAI content part to Sentry's standardized format. + + This handles the Google GenAI inline_data and file_data formats. + + Input format: + - {"inline_data": {"mime_type": "...", "data": "..."}} + - {"file_data": {"mime_type": "...", "file_uri": "..."}} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from Google GenAI + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Google format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Handle Google inline_data format + if "inline_data" in content_part: + inline_data = content_part.get("inline_data") + if isinstance(inline_data, dict): + mime_type = inline_data.get("mime_type", "") + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": inline_data.get("data", ""), + } + return None + + # Handle Google file_data format + if "file_data" in content_part: + file_data = content_part.get("file_data") + if isinstance(file_data, dict): + mime_type = file_data.get("mime_type", "") + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_data.get("file_uri", ""), + } + return None + + return None + + +def transform_generic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a generic/LangChain-style content part to Sentry's standardized format. + + This handles generic formats where the type indicates the modality and + the data is provided via direct base64, url, or file_id fields. + + Input format: + - {"type": "image", "base64": "...", "mime_type": "..."} + - {"type": "audio", "url": "...", "mime_type": "..."} + - {"type": "video", "base64": "...", "mime_type": "..."} + - {"type": "file", "file_id": "...", "mime_type": "..."} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part in generic format + + Returns: + A transformed dictionary in standardized format, or None if the format + is not generic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "audio", "video", "file"): + return None + + # Ensure it's not Anthropic format (which also uses type: "image") + if "source" in content_part: + return None + + mime_type = content_part.get("mime_type", "") + modality = block_type if block_type != "file" else "document" + + # Check for base64 encoded content + if "base64" in content_part: + return { + "type": "blob", + "modality": modality, + "mime_type": mime_type, + "content": content_part.get("base64", ""), + } + # Check for URL reference + elif "url" in content_part: + return { + "type": "uri", + "modality": modality, + "mime_type": mime_type, + "uri": content_part.get("url", ""), + } + # Check for file_id reference + elif "file_id" in content_part: + return { + "type": "file", + "modality": modality, + "mime_type": mime_type, + "file_id": content_part.get("file_id", ""), + } + + return None + + +def transform_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a content part from various AI SDK formats to Sentry's standardized format. + + This is a heuristic dispatcher that detects the format and delegates to the + appropriate SDK-specific transformer. For direct SDK integration, prefer using + the specific transformers directly: + - transform_openai_content_part() for OpenAI/LiteLLM + - transform_anthropic_content_part() for Anthropic + - transform_google_content_part() for Google GenAI + - transform_generic_content_part() for LangChain and other generic formats + + Detection order: + 1. OpenAI: type == "image_url" + 2. Google: "inline_data" or "file_data" keys present + 3. Anthropic: type in ("image", "document") with "source" key + 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from an AI SDK + + Returns: + A transformed dictionary in standardized format, or None if the format + is unrecognized or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Try OpenAI format first (most common, clear indicator) + result = transform_openai_content_part(content_part) + if result is not None: + return result + + # Try Google format (unique keys make it easy to detect) + result = transform_google_content_part(content_part) + if result is not None: + return result + + # Try Anthropic format (has "source" key) + result = transform_anthropic_content_part(content_part) + if result is not None: + return result + + # Try generic format as fallback + result = transform_generic_content_part(content_part) + if result is not None: + return result + + # Unrecognized format + return None + + +def transform_message_content(content: "Any") -> "Any": + """ + Transform message content, handling both string content and list of content blocks. + + For list content, each item is transformed using transform_content_part(). + Items that cannot be transformed (return None) are kept as-is. + + Args: + content: Message content - can be a string, list of content blocks, or other + + Returns: + - String content: returned as-is + - List content: list with each transformable item converted to standardized format + - Other: returned as-is + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + return transformed + + return content + + +def _normalize_data(data: "Any", unpack: bool = True) -> "Any": + # convert pydantic data (e.g. OpenAI v1+) to json compatible format + if hasattr(data, "model_dump"): + # Check if it's a class (type) rather than an instance + # Model classes can be passed as arguments (e.g., for schema definitions) + if inspect.isclass(data): + return f"" + + try: + return _normalize_data(data.model_dump(), unpack=unpack) + except Exception as e: + logger.warning("Could not convert pydantic data to JSON: %s", e) + return data if isinstance(data, (int, float, bool, str)) else str(data) + + if isinstance(data, list): + if unpack and len(data) == 1: + return _normalize_data(data[0], unpack=unpack) # remove empty dimensions + return list(_normalize_data(x, unpack=unpack) for x in data) + + if isinstance(data, dict): + return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} + + return data if isinstance(data, (int, float, bool, str)) else str(data) + + +def set_data_normalized( + span: "Union[Span, StreamedSpan]", + key: str, + value: "Any", + unpack: bool = True, +) -> None: + normalized = _normalize_data(value, unpack=unpack) + if isinstance(normalized, (int, float, bool, str)): + _set_span_data_attribute(span, key, normalized) + else: + _set_span_data_attribute(span, key, json.dumps(normalized)) + + +def _set_span_data_attribute( + span: "Union[Span, StreamedSpan]", key: str, value: "Any" +) -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(key, value) + else: + span.set_data(key, value) + + +def normalize_message_role(role: str) -> str: + """ + Normalize a message role to one of the 4 allowed gen_ai role values. + Maps "ai" -> "assistant" and keeps other standard roles unchanged. + """ + return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) + + +def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": + """ + Normalize roles in a list of messages to use standard gen_ai role values. + Creates a deep copy to avoid modifying the original messages. + """ + normalized_messages = [] + for message in messages: + if not isinstance(message, dict): + normalized_messages.append(message) + continue + normalized_message = message.copy() + if "role" in message: + normalized_message["role"] = normalize_message_role(message["role"]) + normalized_messages.append(normalized_message) + + return normalized_messages + + +def get_start_span_function() -> "Callable[..., Any]": + current_span = sentry_sdk.get_current_span() + + transaction_exists = ( + current_span is not None and current_span.containing_transaction is not None + ) + return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction + + +def _truncate_single_message_content_if_present( + message: "Dict[str, Any]", max_chars: int +) -> "Dict[str, Any]": + """ + Truncate a message's content to at most `max_chars` characters and append an + ellipsis if truncation occurs. + """ + if not isinstance(message, dict) or "content" not in message: + return message + content = message["content"] + + if isinstance(content, str): + if len(content) <= max_chars: + return message + message["content"] = content[:max_chars] + "..." + return message + + if isinstance(content, list): + remaining = max_chars + for item in content: + if isinstance(item, dict) and "text" in item: + text = item["text"] + if isinstance(text, str): + if len(text) > remaining: + item["text"] = text[:remaining] + "..." + remaining = 0 + else: + remaining -= len(text) + return message + + return message + + +def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: + """ + Find the index of the first message that would exceed the max bytes limit. + Compute the individual message sizes, and return the index of the first message from the back + of the list that would exceed the max bytes limit. + """ + running_sum = 0 + for idx in range(len(messages) - 1, -1, -1): + size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) + running_sum += size + if running_sum > max_bytes: + return idx + 1 + + return 0 + + +def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: + """ + Some content blocks contain an image_url property with base64 content as its value. + This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. + """ + if item.get("type") != "image_url": + return False + + image_url_val = item.get("image_url") + image_url = ( + image_url_val.get("url", "") + if isinstance(image_url_val, dict) + else (image_url_val or "") + ) + data_url_match = DATA_URL_BASE64_REGEX.match(image_url) + + return bool(data_url_match) + + +def redact_blob_message_parts( + messages: "List[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Redact blob message parts from the messages by replacing blob content with "[Filtered]". + + This function creates a deep copy of messages that contain blob content to avoid + mutating the original message dictionaries. Messages without blob content are + returned as-is to minimize copying overhead. + + e.g: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,..." + } + ] + } + becomes: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "[Filtered]" + } + ] + } + """ + + # First pass: check if any message contains blob content + has_blobs = False + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and ( + item.get("type") == "blob" or _is_image_type_with_blob_content(item) + ): + has_blobs = True + break + if has_blobs: + break + + # If no blobs found, return original messages to avoid unnecessary copying + if not has_blobs: + return messages + + # Deep copy messages to avoid mutating the original + messages_copy = deepcopy(messages) + + # Second pass: redact blob content in the copy + for message in messages_copy: + if not isinstance(message, dict): + continue + + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + if item.get("type") == "blob": + item["content"] = BLOB_DATA_SUBSTITUTE + elif _is_image_type_with_blob_content(item): + if isinstance(item["image_url"], dict): + item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE + else: + item["image_url"] = BLOB_DATA_SUBSTITUTE + + return messages_copy + + +def truncate_messages_by_size( + messages: "List[Dict[str, Any]]", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Tuple[List[Dict[str, Any]], int]": + """ + Returns a truncated messages list, consisting of + - the last message, with its content truncated to `max_single_message_chars` characters, + if the last message's size exceeds `max_bytes` bytes; otherwise, + - the maximum number of messages, starting from the end of the `messages` list, whose total + serialized size does not exceed `max_bytes` bytes. + + In the single message case, the serialized message size may exceed `max_bytes`, because + truncation is based only on character count in that case. + """ + serialized_json = json.dumps(messages, separators=(",", ":")) + current_size = len(serialized_json.encode("utf-8")) + + if current_size <= max_bytes: + return messages, 0 + + truncation_index = _find_truncation_index(messages, max_bytes) + if truncation_index < len(messages): + truncated_messages = messages[truncation_index:] + else: + truncation_index = len(messages) - 1 + truncated_messages = messages[-1:] + + if len(truncated_messages) == 1: + truncated_messages[0] = _truncate_single_message_content_if_present( + deepcopy(truncated_messages[0]), max_chars=max_single_message_chars + ) + + return truncated_messages, truncation_index + + +def truncate_and_annotate_messages( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_message = _truncate_single_message_content_if_present( + deepcopy(messages[-1]), max_chars=max_single_message_chars + ) + if len(messages) > 1: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return [truncated_message] + + +def truncate_and_annotate_embedding_inputs( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) + if removed_count > 0: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return truncated_messages + + +def set_conversation_id(conversation_id: str) -> None: + """ + Set the conversation_id in the scope. + """ + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py new file mode 100644 index 0000000000..5556b11ace --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py @@ -0,0 +1,573 @@ +import inspect +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import Client, tracing_utils +from sentry_sdk._init_implementation import init +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.crons import monitor +from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import get_current_span as _get_current_streamed_span +from sentry_sdk.tracing import NoOpSpan, Transaction, trace + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Optional, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + MeasurementUnit, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span, TransactionKwargs + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) +else: + + def overload(x: "T") -> "T": + return x + + +# When changing this, update __all__ in __init__.py too +__all__ = [ + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + + +def scopemethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, + inspect.getdoc(getattr(Scope, f.__name__)), + ) + return f + + +def clientmethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, + inspect.getdoc(getattr(Client, f.__name__)), + ) + return f + + +@scopemethod +def get_client() -> "BaseClient": + return Scope.get_client() + + +def is_initialized() -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether Sentry has been initialized or not. + + If a client is available and the client is active + (meaning it is configured to send data) then + Sentry is initialized. + """ + return get_client().is_active() + + +@scopemethod +def get_global_scope() -> "Scope": + return Scope.get_global_scope() + + +@scopemethod +def get_isolation_scope() -> "Scope": + return Scope.get_isolation_scope() + + +@scopemethod +def get_current_scope() -> "Scope": + return Scope.get_current_scope() + + +@scopemethod +def last_event_id() -> "Optional[str]": + """ + See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding + this method's limitations. + """ + return Scope.last_event_id() + + +@scopemethod +def capture_event( + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) + + +@scopemethod +def capture_message( + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_message( + message, level, scope=scope, **scope_kwargs + ) + + +@scopemethod +def capture_exception( + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) + + +@scopemethod +def add_attachment( + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, +) -> None: + return get_isolation_scope().add_attachment( + bytes, filename, path, content_type, add_to_transactions + ) + + +@scopemethod +def add_breadcrumb( + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", +) -> None: + return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + +@overload +def configure_scope() -> "ContextManager[Scope]": + pass + + +@overload +def configure_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def configure_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + warnings.warn( + "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", + DeprecationWarning, + stacklevel=2, + ) + + scope = get_isolation_scope() + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + +@overload +def push_scope() -> "ContextManager[Scope]": + pass + + +@overload +def push_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def push_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + warnings.warn( + "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", + DeprecationWarning, + stacklevel=2, + ) + + if callback is not None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with push_scope() as scope: + callback(scope) + return None + + return _ScopeManager() + + +@scopemethod +def set_attribute(attribute: str, value: "Any") -> None: + """ + Set an attribute. + + Any attributes-based telemetry (logs, metrics) captured in this scope will + include this attribute. + """ + return get_isolation_scope().set_attribute(attribute, value) + + +@scopemethod +def remove_attribute(attribute: str) -> None: + """ + Remove an attribute. + + If the attribute doesn't exist, this function will not have any effect and + it will also not raise an exception. + """ + return get_isolation_scope().remove_attribute(attribute) + + +@scopemethod +def set_tag(key: str, value: "Any") -> None: + return get_isolation_scope().set_tag(key, value) + + +@scopemethod +def set_tags(tags: "Mapping[str, object]") -> None: + return get_isolation_scope().set_tags(tags) + + +@scopemethod +def set_context(key: str, value: "Dict[str, Any]") -> None: + return get_isolation_scope().set_context(key, value) + + +@scopemethod +def set_extra(key: str, value: "Any") -> None: + return get_isolation_scope().set_extra(key, value) + + +@scopemethod +def set_user(value: "Optional[Dict[str, Any]]") -> None: + return get_isolation_scope().set_user(value) + + +@scopemethod +def set_level(value: "LogLevelStr") -> None: + return get_isolation_scope().set_level(value) + + +@clientmethod +def flush( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return get_client().flush(timeout=timeout, callback=callback) + + +@clientmethod +async def flush_async( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return await get_client().flush_async(timeout=timeout, callback=callback) + + +@scopemethod +def start_span( + **kwargs: "Any", +) -> "Span": + return get_current_scope().start_span(**kwargs) + + +@scopemethod +def start_transaction( + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", +) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction on the current scope. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + return get_current_scope().start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + +def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + transaction = get_current_scope().transaction + if transaction is not None: + transaction.set_measurement(name, value, unit) + + +def get_current_span( + scope: "Optional[Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + return tracing_utils.get_current_span(scope) + + +def get_traceparent() -> "Optional[str]": + """ + Returns the traceparent either from the active span or from the scope. + """ + return get_current_scope().get_traceparent() + + +def get_baggage() -> "Optional[str]": + """ + Returns Baggage either from the active span or from the scope. + """ + baggage = get_current_scope().get_baggage() + if baggage is not None: + return baggage.serialize() + + return None + + +def continue_trace( + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", +) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers, op, name, source, origin + ) + + +@scopemethod +def start_session( + session_mode: str = "application", +) -> None: + return get_isolation_scope().start_session(session_mode=session_mode) + + +@scopemethod +def end_session() -> None: + return get_isolation_scope().end_session() + + +@scopemethod +def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: + return get_current_scope().set_transaction_name(name, source) + + +def update_current_span( + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, + data: "Optional[dict[str, Any]]" = None, +) -> None: + """ + Update the current active span with the provided parameters. + + This function allows you to modify properties of the currently active span. + If no span is currently active, this function will do nothing. + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, the span's operation will + remain unchanged. + :type op: str or None + + :param name: The human-readable name/description for the span. This provides + more specific details about what the span represents (e.g., "GET /api/users", + "SELECT * FROM users"). If not provided, the span's name will remain unchanged. + :type name: str or None + + :param data: A dictionary of key-value pairs to add as data to the span. This + data will be merged with any existing span data. If not provided, + no data will be added. + + .. deprecated:: 2.35.0 + Use ``attributes`` instead. The ``data`` parameter will be removed + in a future version. + :type data: dict[str, Union[str, int, float, bool]] or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes will be merged with any existing span data. If not provided, + no attributes will be added. + :type attributes: dict[str, Union[str, int, float, bool]] or None + + :returns: None + + .. versionadded:: 2.35.0 + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP + + sentry_sdk.update_current_span( + op=OP.FUNCTION, + name="process_user_data", + attributes={"user_id": 123, "batch_size": 50} + ) + """ + if isinstance(_get_current_streamed_span(), StreamedSpan): + warnings.warn( + "The `update_current_span` API isn't available in streaming mode. " + "Retrieve the current span with get_current_span() and use its API " + "directly.", + DeprecationWarning, + stacklevel=2, + ) + return + + current_span = get_current_span() + + if current_span is None: + return + + if op is not None: + current_span.op = op + + if name is not None: + # internally it is still description + current_span.description = name + + if data is not None and attributes is not None: + raise ValueError( + "Cannot provide both `data` and `attributes`. Please use only `attributes`." + ) + + if data is not None: + warnings.warn( + "The `data` parameter is deprecated. Please use `attributes` instead.", + DeprecationWarning, + stacklevel=2, + ) + attributes = data + + if attributes is not None: + current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py new file mode 100644 index 0000000000..4d69d3acf2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py @@ -0,0 +1,71 @@ +import mimetypes +import os +from typing import TYPE_CHECKING + +from sentry_sdk.envelope import Item, PayloadRef + +if TYPE_CHECKING: + from typing import Callable, Optional, Union + + +class Attachment: + """Additional files/data to send along with an event. + + This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. + config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with + all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are + captured within the ``Scope``. + + To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for + ``add_attachment`` are the same as the parameters for this class's constructor. + + :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless + ``path`` is provided. + :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. + :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. + :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` + parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. + :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. + """ + + def __init__( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + if bytes is None and path is None: + raise TypeError("path or raw bytes required for attachment") + if filename is None and path is not None: + filename = os.path.basename(path) + if filename is None: + raise TypeError("filename is required for attachment") + if content_type is None: + content_type = mimetypes.guess_type(filename)[0] + self.bytes = bytes + self.filename = filename + self.path = path + self.content_type = content_type + self.add_to_transactions = add_to_transactions + + def to_envelope_item(self) -> "Item": + """Returns an envelope item for this attachment.""" + payload: "Union[None, PayloadRef, bytes]" = None + if self.bytes is not None: + if callable(self.bytes): + payload = self.bytes() + else: + payload = self.bytes + else: + payload = PayloadRef(path=self.path) + return Item( + payload=payload, + type="attachment", + content_type=self.content_type, + filename=self.filename, + ) + + def __repr__(self) -> str: + return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py new file mode 100644 index 0000000000..92cb42277e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py @@ -0,0 +1,1479 @@ +import json +import os +import platform +import random +import socket +import sys +import uuid +import warnings +from collections.abc import Iterable, Mapping +from datetime import datetime, timezone +from importlib import import_module +from typing import TYPE_CHECKING, Dict, List, cast, overload + +from sentry_sdk._compat import check_uwsgi_thread_support +from sentry_sdk._metrics_batcher import MetricsBatcher +from sentry_sdk._span_batcher import SpanBatcher +from sentry_sdk.consts import ( + DEFAULT_MAX_VALUE_LENGTH, + DEFAULT_OPTIONS, + INSTRUMENTER, + SPANDATA, + SPANSTATUS, + VERSION, + ClientConstructor, +) +from sentry_sdk.data_collection import ( + _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 +from sentry_sdk.monitor import Monitor +from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler +from sentry_sdk.profiler.transaction_profiler import ( + Profile, + has_profiling_enabled, + setup_profiler, +) +from sentry_sdk.scrubber import EventScrubber +from sentry_sdk.serializer import serialize +from sentry_sdk.sessions import SessionFlusher +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import trace +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import ( + AsyncHttpTransport, + HttpTransportCore, + make_transport, +) +from sentry_sdk.utils import ( + AnnotatedValue, + ContextVar, + capture_internal_exceptions, + current_stacktrace, + datetime_from_isoformat, + env_to_bool, + format_timestamp, + get_before_send_log, + get_before_send_metric, + get_before_send_span, + get_default_release, + get_sdk_name, + get_type_name, + handle_in_app, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union + + from sentry_sdk._log_batcher import LogBatcher + from sentry_sdk._metrics_batcher import MetricsBatcher + from sentry_sdk._types import ( + Event, + EventDataCategory, + Hint, + Log, + Metric, + SDKInfo, + SerializedAttributeValue, + ) + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.session import Session + from sentry_sdk.spotlight import SpotlightClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.transport import Item, Transport + from sentry_sdk.utils import Dsn + + I = TypeVar("I", bound=Integration) # noqa: E741 + +_client_init_debug = ContextVar("client_init_debug") + + +SDK_INFO: "SDKInfo" = { + "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() + "version": VERSION, + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], +} + + +def _serialized_v1_attribute_to_serialized_v2_attribute( + attribute_value: "Any", +) -> "Optional[SerializedAttributeValue]": + if isinstance(attribute_value, bool): + return { + "value": attribute_value, + "type": "boolean", + } + + if isinstance(attribute_value, int): + return { + "value": attribute_value, + "type": "integer", + } + + if isinstance(attribute_value, float): + return { + "value": attribute_value, + "type": "double", + } + + if isinstance(attribute_value, str): + return { + "value": attribute_value, + "type": "string", + } + + if isinstance(attribute_value, list): + if not attribute_value: + return {"value": [], "type": "array"} + + ty = type(attribute_value[0]) + if ty in (int, str, bool, float) and all( + type(v) is ty for v in attribute_value + ): + return { + "value": attribute_value, + "type": "array", + } + + # Types returned when the serializer for V1 span attributes recurses into some container types. + if isinstance(attribute_value, (dict, list)): + return { + "value": json.dumps(attribute_value), + "type": "string", + } + + return None + + +def _serialized_v1_span_to_serialized_v2_span( + span: "dict[str, Any]", event: "Event" +) -> "dict[str, Any]": + # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". + res: "dict[str, Any]" = { + "status": SpanStatus.OK.value, + "is_segment": False, + } + + if "trace_id" in span: + res["trace_id"] = span["trace_id"] + + if "span_id" in span: + res["span_id"] = span["span_id"] + + if "description" in span: + description = span["description"] + + if description is None and "op" in span: + description = span["op"] + + res["name"] = description + + if "start_timestamp" in span: + start_timestamp = None + try: + start_timestamp = datetime_from_isoformat(span["start_timestamp"]) + except Exception: + pass + + if start_timestamp is not None: + res["start_timestamp"] = start_timestamp.timestamp() + + if "timestamp" in span: + end_timestamp = None + try: + end_timestamp = datetime_from_isoformat(span["timestamp"]) + except Exception: + pass + + if end_timestamp is not None: + res["end_timestamp"] = end_timestamp.timestamp() + + if "parent_span_id" in span: + res["parent_span_id"] = span["parent_span_id"] + + if "status" in span and span["status"] != SPANSTATUS.OK: + res["status"] = "error" + + attributes: "Dict[str, Any]" = {} + + if "op" in span: + attributes["sentry.op"] = span["op"] + if "origin" in span: + attributes["sentry.origin"] = span["origin"] + + span_data = span.get("data") + if isinstance(span_data, dict): + attributes.update(span_data) + + span_tags = span.get("tags") + if isinstance(span_tags, dict): + attributes.update(span_tags) + + # See Scope._apply_user_attributes_to_telemetry() for user attributes. + user = event.get("user") + if isinstance(user, dict): + if "id" in user: + attributes["user.id"] = user["id"] + if "username" in user: + attributes["user.name"] = user["username"] + if "email" in user: + attributes["user.email"] = user["email"] + + # See Scope.set_global_attributes() for release, environment, and SDK metadata. + if "release" in event: + attributes["sentry.release"] = event["release"] + if "environment" in event: + attributes["sentry.environment"] = event["environment"] + if "server_name" in event: + attributes["server.address"] = event["server_name"] + if "transaction" in event: + attributes["sentry.segment.name"] = event["transaction"] + + trace_context = event.get("contexts", {}).get("trace", {}) + if "span_id" in trace_context: + attributes["sentry.segment.id"] = trace_context["span_id"] + + sdk_info = event.get("sdk") + if isinstance(sdk_info, dict): + if "name" in sdk_info: + attributes["sentry.sdk.name"] = sdk_info["name"] + if "version" in sdk_info: + attributes["sentry.sdk.version"] = sdk_info["version"] + + attributes["process.runtime.name"] = platform.python_implementation() + attributes["process.runtime.version"] = ( + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + ) + + if not attributes: + return res + + res["attributes"] = {} + for key, value in attributes.items(): + converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) + if converted_value is None: + continue + + res["attributes"][key] = converted_value + + # Remove redundant attribute, as status is stored in the status field. + if "status" in res["attributes"]: + del res["attributes"]["status"] + + return res + + +def _split_gen_ai_spans( + event_opt: "Event", +) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": + if "spans" not in event_opt: + return None + + spans: "Any" = event_opt["spans"] + if isinstance(spans, AnnotatedValue): + spans = spans.value + + if not isinstance(spans, Iterable): + return None + + non_gen_ai_spans = [] + gen_ai_spans = [] + for span in spans: + if not isinstance(span, dict): + non_gen_ai_spans.append(span) + continue + + span_op = span.get("op") + if isinstance(span_op, str) and span_op.startswith("gen_ai."): + gen_ai_spans.append(span) + else: + non_gen_ai_spans.append(span) + + return non_gen_ai_spans, gen_ai_spans + + +def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": + if args and (isinstance(args[0], (bytes, str)) or args[0] is None): + dsn: "Optional[str]" = args[0] + args = args[1:] + else: + dsn = None + + if len(args) > 1: + raise TypeError("Only single positional argument is expected") + + rv = dict(DEFAULT_OPTIONS) + options = dict(*args, **kwargs) + if dsn is not None and options.get("dsn") is None: + options["dsn"] = dsn + + for key, value in options.items(): + if key not in rv: + raise TypeError("Unknown option %r" % (key,)) + + rv[key] = value + + if rv["dsn"] is None: + rv["dsn"] = os.environ.get("SENTRY_DSN") + + if rv["release"] is None: + rv["release"] = get_default_release() + + if rv["environment"] is None: + rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" + + if rv["debug"] is None: + rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False + + if rv["server_name"] is None and hasattr(socket, "gethostname"): + rv["server_name"] = socket.gethostname() + + if rv["instrumenter"] is None: + rv["instrumenter"] = INSTRUMENTER.SENTRY + + if rv["project_root"] is None: + try: + project_root = os.getcwd() + except Exception: + project_root = None + + rv["project_root"] = project_root + + 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"] + ) + + if rv["socket_options"] and not isinstance(rv["socket_options"], list): + logger.warning( + "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." + ) + rv["socket_options"] = None + + if rv["keep_alive"] is None: + rv["keep_alive"] = ( + env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False + ) + + if rv["enable_tracing"] is not None: + warnings.warn( + "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", + DeprecationWarning, + stacklevel=2, + ) + + if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): + warnings.warn( + "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", + stacklevel=2, + ) + + return rv + + +try: + # Python 3.6+ + module_not_found_error = ModuleNotFoundError +except Exception: + # Older Python versions + module_not_found_error = ImportError # type: ignore + + +class BaseClient: + """ + .. versionadded:: 2.0.0 + + The basic definition of a client that is used for sending data to Sentry. + """ + + spotlight: "Optional[SpotlightClient]" = None + + def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: + self.options: "Dict[str, Any]" = ( + options if options is not None else DEFAULT_OPTIONS + ) + + self.transport: "Optional[Transport]" = None + self.monitor: "Optional[Monitor]" = None + self.log_batcher: "Optional[LogBatcher]" = None + self.metrics_batcher: "Optional[MetricsBatcher]" = None + self.span_batcher: "Optional[SpanBatcher]" = None + self.integrations: "dict[str, Integration]" = {} + + def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": + return {"options": {}} + + def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: + pass + + @property + def dsn(self) -> "Optional[str]": + return None + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + return None + + def should_send_default_pii(self) -> bool: + return False + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return False + + def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + return None + + def _capture_log(self, log: "Log", scope: "Scope") -> None: + pass + + def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: + pass + + def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: + pass + + def capture_session(self, *args: "Any", **kwargs: "Any") -> None: + return None + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, name_or_class: "Union[str, type[Integration]]" + ) -> "Optional[Integration]": + return None + + def close(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def flush(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def close_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def __enter__(self) -> "BaseClient": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + return None + + +class NonRecordingClient(BaseClient): + """ + .. versionadded:: 2.0.0 + + A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + """ + + pass + + +class _Client(BaseClient): + """ + The client is internally responsible for capturing the events and + forwarding them to sentry through the configured transport. It takes + the client options as keyword arguments and optionally the DSN as first + argument. + + Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) + """ + + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + super(_Client, self).__init__(options=get_options(*args, **kwargs)) + self._init_impl() + + def __getstate__(self) -> "Any": + return {"options": self.options} + + def __setstate__(self, state: "Any") -> None: + self.options = state["options"] + self._init_impl() + + def _setup_instrumentation( + self, functions_to_trace: "Sequence[Dict[str, str]]" + ) -> None: + """ + Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. + """ + for function in functions_to_trace: + class_name = None + function_qualname = function["qualified_name"] + + if "." not in function_qualname: + logger.warning( + "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", + function_qualname, + ) + continue + + module_name, function_name = function_qualname.rsplit(".", 1) + + try: + # Try to import module and function + # ex: "mymodule.submodule.funcname" + + module_obj = import_module(module_name) + function_obj = getattr(module_obj, function_name) + setattr(module_obj, function_name, trace(function_obj)) + logger.debug("Enabled tracing for %s", function_qualname) + except module_not_found_error: + try: + # Try to import a class + # ex: "mymodule.submodule.MyClassName.member_function" + + module_name, class_name = module_name.rsplit(".", 1) + module_obj = import_module(module_name) + class_obj = getattr(module_obj, class_name) + function_obj = getattr(class_obj, function_name) + function_type = type(class_obj.__dict__[function_name]) + traced_function = trace(function_obj) + + if function_type in (staticmethod, classmethod): + traced_function = staticmethod(traced_function) + + setattr(class_obj, function_name, traced_function) + setattr(module_obj, class_name, class_obj) + logger.debug("Enabled tracing for %s", function_qualname) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + def _init_impl(self) -> None: + old_debug = _client_init_debug.get(False) + + def _capture_envelope(envelope: "Envelope") -> None: + if self.spotlight is not None: + self.spotlight.capture_envelope(envelope) + if self.transport is not None: + self.transport.capture_envelope(envelope) + + def _record_lost_event( + reason: str, + data_category: "EventDataCategory", + item: "Optional[Item]" = None, + quantity: int = 1, + ) -> None: + if self.transport is not None: + self.transport.record_lost_event( + reason=reason, + data_category=data_category, + item=item, + quantity=quantity, + ) + + try: + _client_init_debug.set(self.options["debug"]) + self.transport = make_transport(self.options) + + self.monitor = None + if self.transport: + if self.options["enable_backpressure_handling"]: + self.monitor = Monitor(self.transport) + + # Setup Spotlight before creating batchers so _capture_envelope can use it. + # setup_spotlight handles all config/env var resolution per the SDK spec. + from sentry_sdk.spotlight import setup_spotlight + + self.spotlight = setup_spotlight(self.options) + if self.spotlight is not None and not self.options["dsn"]: + sample_all = lambda *_args, **_kwargs: 1.0 + self.options["send_default_pii"] = True + 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, + ) + + self.session_flusher = SessionFlusher(capture_func=_capture_envelope) + + self.log_batcher = None + + if has_logs_enabled(self.options): + from sentry_sdk._log_batcher import LogBatcher + + self.log_batcher = LogBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.metrics_batcher = None + if has_metrics_enabled(self.options): + self.metrics_batcher = MetricsBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.span_batcher = None + if has_span_streaming_enabled(self.options): + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + max_request_body_size = ("always", "never", "small", "medium") + if self.options["max_request_body_size"] not in max_request_body_size: + raise ValueError( + "Invalid value for max_request_body_size. Must be one of {}".format( + max_request_body_size + ) + ) + + if self.options["_experiments"].get("otel_powered_performance", False): + logger.debug( + "[OTel] Enabling experimental OTel-powered performance monitoring." + ) + self.options["instrumenter"] = INSTRUMENTER.OTEL + if ( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" + not in _DEFAULT_INTEGRATIONS + ): + _DEFAULT_INTEGRATIONS.append( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", + ) + + self.integrations = setup_integrations( + self.options["integrations"], + with_defaults=self.options["default_integrations"], + with_auto_enabling_integrations=self.options[ + "auto_enabling_integrations" + ], + disabled_integrations=self.options["disabled_integrations"], + options=self.options, + ) + + sdk_name = get_sdk_name(list(self.integrations.keys())) + SDK_INFO["name"] = sdk_name + logger.debug("Setting SDK name to '%s'", sdk_name) + + if has_profiling_enabled(self.options): + try: + setup_profiler(self.options) + except Exception as e: + logger.debug("Can not set up profiler. (%s)", e) + else: + try: + setup_continuous_profiler( + self.options, + sdk_info=SDK_INFO, + capture_func=_capture_envelope, + ) + except Exception as e: + logger.debug("Can not set up continuous profiler. (%s)", e) + + finally: + _client_init_debug.set(old_debug) + + self._setup_instrumentation(self.options.get("functions_to_trace", [])) + + if ( + self.monitor + or self.log_batcher + or self.metrics_batcher + or self.span_batcher + or has_profiling_enabled(self.options) + or isinstance(self.transport, HttpTransportCore) + ): + # If we have anything on that could spawn a background thread, we + # need to check if it's safe to use them. + check_uwsgi_thread_support() + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return True + + def should_send_default_pii(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. + """ + return self.options.get("send_default_pii") or False + + @property + def dsn(self) -> "Optional[str]": + """Returns the configured DSN as string.""" + return self.options["dsn"] + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + """Returns the configured parsed DSN object.""" + return self.transport.parsed_dsn if self.transport else None + + def _prepare_event( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]", + ) -> "Optional[Event]": + previous_total_spans: "Optional[int]" = None + previous_total_breadcrumbs: "Optional[int]" = None + + if event.get("timestamp") is None: + event["timestamp"] = datetime.now(timezone.utc) + + is_transaction = event.get("type") == "transaction" + + if scope is not None: + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + event_ = scope.apply_to_event(event, hint, self.options) + + # one of the event/error processors returned None + if event_ is None: + if self.transport: + self.transport.record_lost_event( + "event_processor", + data_category=("transaction" if is_transaction else "error"), + ) + if is_transaction: + self.transport.record_lost_event( + "event_processor", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + return None + + event = event_ + spans_delta = spans_before - len( + cast(List[Dict[str, object]], event.get("spans", [])) + ) + span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) + + if is_transaction and self.transport is not None: + if spans_delta > 0: + self.transport.record_lost_event( + "event_processor", data_category="span", quantity=spans_delta + ) + if span_recorder_dropped_spans > 0: + self.transport.record_lost_event( + "buffer_overflow", + data_category="span", + quantity=span_recorder_dropped_spans, + ) + + dropped_spans: int = span_recorder_dropped_spans + spans_delta + if dropped_spans > 0: + previous_total_spans = spans_before + dropped_spans + if scope._n_breadcrumbs_truncated > 0: + breadcrumbs = event.get("breadcrumbs", {}) + values = ( + breadcrumbs.get("values", []) + if not isinstance(breadcrumbs, AnnotatedValue) + else [] + ) + previous_total_breadcrumbs = ( + len(values) + scope._n_breadcrumbs_truncated + ) + + if ( + not is_transaction + and self.options["attach_stacktrace"] + and "exception" not in event + and "stacktrace" not in event + and "threads" not in event + ): + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=self.options.get( + "include_local_variables", True + ), + max_value_length=self.options.get( + "max_value_length", DEFAULT_MAX_VALUE_LENGTH + ), + ), + "crashed": False, + "current": True, + } + ] + } + + for key in "release", "environment", "server_name", "dist": + if event.get(key) is None and self.options[key] is not None: + event[key] = str(self.options[key]).strip() + if event.get("sdk") is None: + sdk_info = dict(SDK_INFO) + sdk_info["integrations"] = sorted(self.integrations.keys()) + event["sdk"] = sdk_info + + if event.get("platform") is None: + event["platform"] = "python" + + event = handle_in_app( + event, + self.options["in_app_exclude"], + self.options["in_app_include"], + self.options["project_root"], + ) + + if event is not None: + event_scrubber = self.options["event_scrubber"] + if event_scrubber: + event_scrubber.scrub_event(event) + + if scope is not None and scope._gen_ai_original_message_count: + spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) + if isinstance(spans, list): + for span in spans: + span_id = span.get("span_id", None) + span_data = span.get("data", {}) + if ( + span_id + and span_id in scope._gen_ai_original_message_count + and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data + ): + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], + {"len": scope._gen_ai_original_message_count[span_id]}, + ) + if previous_total_spans is not None: + event["spans"] = AnnotatedValue( + event.get("spans", []), {"len": previous_total_spans} + ) + if previous_total_breadcrumbs is not None: + event["breadcrumbs"] = AnnotatedValue( + event.get("breadcrumbs", {"values": []}), + {"len": previous_total_breadcrumbs}, + ) + + # Postprocess the event here so that annotated types do + # generally not surface in before_send + if event is not None: + event = cast( + "Event", + serialize( + cast("Dict[str, Any]", event), + max_request_body_size=self.options.get("max_request_body_size"), + max_value_length=self.options.get("max_value_length"), + custom_repr=self.options.get("custom_repr"), + ), + ) + + before_send = self.options["before_send"] + if ( + before_send is not None + and event is not None + and event.get("type") != "transaction" + ): + new_event = None + with capture_internal_exceptions(): + new_event = before_send(event, hint or {}) + if new_event is None: + logger.info("before send dropped event") + if self.transport: + self.transport.record_lost_event( + "before_send", data_category="error" + ) + + # If this is an exception, reset the DedupeIntegration. It still + # remembers the dropped exception as the last exception, meaning + # that if the same exception happens again and is not dropped + # in before_send, it'd get dropped by DedupeIntegration. + if event.get("exception"): + DedupeIntegration.reset_last_seen() + + event = new_event + + before_send_transaction = self.options["before_send_transaction"] + if ( + before_send_transaction is not None + and event is not None + and event.get("type") == "transaction" + ): + new_event = None + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + with capture_internal_exceptions(): + new_event = before_send_transaction(event, hint or {}) + if new_event is None: + logger.info("before send transaction dropped event") + if self.transport: + self.transport.record_lost_event( + reason="before_send", data_category="transaction" + ) + self.transport.record_lost_event( + reason="before_send", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + else: + spans_delta = spans_before - len(new_event.get("spans", [])) + if spans_delta > 0 and self.transport is not None: + self.transport.record_lost_event( + reason="before_send", data_category="span", quantity=spans_delta + ) + + event = new_event + + return event + + def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: + exc_info = hint.get("exc_info") + if exc_info is None: + return False + + error = exc_info[0] + error_type_name = get_type_name(exc_info[0]) + error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) + + for ignored_error in self.options["ignore_errors"]: + # String types are matched against the type name in the + # exception only + if isinstance(ignored_error, str): + if ignored_error == error_full_name or ignored_error == error_type_name: + return True + else: + if issubclass(error, ignored_error): + return True + + return False + + def _should_capture( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]" = None, + ) -> bool: + # Transactions are sampled independent of error events. + is_transaction = event.get("type") == "transaction" + if is_transaction: + return True + + ignoring_prevents_recursion = scope is not None and not scope._should_capture + if ignoring_prevents_recursion: + return False + + ignored_by_config_option = self._is_ignored_error(event, hint) + if ignored_by_config_option: + return False + + return True + + def _should_sample_error( + self, + event: "Event", + hint: "Hint", + ) -> bool: + error_sampler = self.options.get("error_sampler", None) + + if callable(error_sampler): + with capture_internal_exceptions(): + sample_rate = error_sampler(event, hint) + else: + sample_rate = self.options["sample_rate"] + + try: + not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate + except NameError: + logger.warning( + "The provided error_sampler raised an error. Defaulting to sampling the event." + ) + + # If the error_sampler raised an error, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + except TypeError: + parameter, verb = ( + ("error_sampler", "returned") + if callable(error_sampler) + else ("sample_rate", "contains") + ) + logger.warning( + "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." + % (parameter, verb, repr(sample_rate)) + ) + + # If the sample_rate has an invalid value, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + + if not_in_sample_rate: + # because we will not sample this event, record a "lost event". + if self.transport: + self.transport.record_lost_event("sample_rate", data_category="error") + + return False + + return True + + def _update_session_from_event( + self, + session: "Session", + event: "Event", + ) -> None: + crashed = False + errored = False + user_agent = None + + exceptions = (event.get("exception") or {}).get("values") + if exceptions: + errored = True + for error in exceptions: + if isinstance(error, AnnotatedValue): + error = error.value or {} + mechanism = error.get("mechanism") + if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: + crashed = True + break + + user = event.get("user") + + if session.user_agent is None: + headers = (event.get("request") or {}).get("headers") + headers_dict = headers if isinstance(headers, dict) else {} + for k, v in headers_dict.items(): + if k.lower() == "user-agent": + user_agent = v + break + + session.update( + status="crashed" if crashed else None, + user=user, + user_agent=user_agent, + errors=session.errors + (errored or crashed), + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + ) -> "Optional[str]": + """Captures an event. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + + :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. + """ + hint = dict(hint or ()) + + if not self._should_capture(event, hint, scope): + return None + + profile = event.pop("profile", None) + + event_id = event.get("event_id") + if event_id is None: + event["event_id"] = event_id = uuid.uuid4().hex + + span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) + event_opt = self._prepare_event(event, hint, scope) + if event_opt is None: + return None + + # whenever we capture an event we also check if the session needs + # to be updated based on that information. + session = scope._session if scope else None + if session: + self._update_session_from_event(session, event) + + is_transaction = event_opt.get("type") == "transaction" + is_checkin = event_opt.get("type") == "check_in" + + if ( + not is_transaction + and not is_checkin + and not self._should_sample_error(event, hint) + ): + return None + + attachments = hint.get("attachments") + + trace_context = event_opt.get("contexts", {}).get("trace") or {} + dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) + + headers: "dict[str, object]" = { + "event_id": event_opt["event_id"], + "sent_at": format_timestamp(datetime.now(timezone.utc)), + } + + if dynamic_sampling_context: + headers["trace"] = dynamic_sampling_context + + envelope = Envelope(headers=headers) + + if is_transaction and isinstance(profile, Profile): + envelope.add_profile(profile.to_json(event_opt, self.options)) + + if is_transaction and not span_recorder_has_gen_ai_span: + envelope.add_transaction(event_opt) + elif is_transaction: + split_spans = _split_gen_ai_spans(event_opt) + if split_spans is None or not split_spans[1]: + envelope.add_transaction(event_opt) + else: + non_gen_ai_spans, gen_ai_spans = split_spans + + event_opt["spans"] = non_gen_ai_spans + envelope.add_transaction(event_opt) + + converted_gen_ai_spans = [ + _serialized_v1_span_to_serialized_v2_span(span, event_opt) + for span in gen_ai_spans + if isinstance(span, dict) + ] + + envelope.add_item( + Item( + type=SpanBatcher.TYPE, + content_type=SpanBatcher.CONTENT_TYPE, + headers={ + "item_count": len(converted_gen_ai_spans), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": converted_gen_ai_spans, + }, + ), + ) + ) + + elif is_checkin: + envelope.add_checkin(event_opt) + else: + envelope.add_event(event_opt) + + for attachment in attachments or (): + envelope.add_item(attachment.to_envelope_item()) + + return_value = None + if self.spotlight: + self.spotlight.capture_envelope(envelope) + return_value = event_id + + if self.transport is not None: + self.transport.capture_envelope(envelope) + return_value = event_id + + return return_value + + def _capture_telemetry( + self, + telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", + ty: str, + scope: "Scope", + ) -> None: + """ + Capture attributes-based telemetry (logs, metrics, streamed spans). + + Apply any attributes set on the scope to it, and run the user's + before_send_{telemetry} on it, if applicable. + """ + if telemetry is None: + return + + scope.apply_to_telemetry(telemetry) + + before_send = None + + if ty == "log": + before_send = get_before_send_log(self.options) + serialized = telemetry + + elif ty == "metric": + before_send = get_before_send_metric(self.options) + serialized = telemetry + + elif ty == "span": + before_send = get_before_send_span(self.options) + serialized = telemetry._to_json() # type: ignore[union-attr] + + if before_send is not None: + serialized = before_send(serialized, {}) # type: ignore[arg-type] + + if ty in ("log", "metric"): + # Logs and metrics can be dropped in their respective + # before_send, so if we get None, don't queue them for sending. + if serialized is None: + return + + elif ty == "span" and isinstance(telemetry, StreamedSpan): + # Spans can't be dropped in before_send_span by design. They can + # be altered though (e.g. to sanitize). Only allow changes to + # name and attributes. + if isinstance(serialized, dict) and "name" in serialized: + telemetry.name = serialized["name"] # type: ignore[typeddict-item] + telemetry._attributes = {} + for k, v in (serialized.get("attributes") or {}).items(): + telemetry.set_attribute(k, v) + + else: + logger.debug( + "[Tracing] Invalid return value from before_send_span. Keeping original span." + ) + + serialized = telemetry._to_json() + + batcher = None + if ty == "log": + batcher = self.log_batcher + + elif ty == "metric": + batcher = self.metrics_batcher + + elif ty == "span": + # We need a reference to the segment span in the batcher to populate + # the dynamic sampling context (DSC) + serialized["_segment_span"] = telemetry._segment # type: ignore + batcher = self.span_batcher + + if batcher is not None: + batcher.add(serialized) # type: ignore + + def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: + self._capture_telemetry(log, "log", scope) + + def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: + self._capture_telemetry(metric, "metric", scope) + + def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: + self._capture_telemetry(span, "span", scope) + + def capture_session( + self, + session: "Session", + ) -> None: + if not session.release: + logger.info("Discarded session update because of missing release") + else: + self.session_flusher.add_session(session) + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Optional[Integration]": + """Returns the integration for this client by name or class. + If the client does not have that integration then `None` is returned. + """ + if isinstance(name_or_class, str): + integration_name = name_or_class + elif name_or_class.identifier is not None: + integration_name = name_or_class.identifier + else: + raise ValueError("Integration has no name") + + return self.integrations.get(integration_name) + + def _has_async_transport(self) -> bool: + """Check if the current transport is async.""" + return isinstance(self.transport, AsyncHttpTransport) + + @property + def _batchers(self) -> "tuple[Any, ...]": + return tuple( + b + for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) + if b is not None + ) + + def _close_components(self) -> None: + """Kill all client components in the correct order.""" + self.session_flusher.kill() + for b in self._batchers: + b.kill() + if self.monitor: + self.monitor.kill() + + def _flush_components(self) -> None: + """Flush all client components.""" + self.session_flusher.flush() + for b in self._batchers: + b.flush() + + def close( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush`. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "close() used with AsyncHttpTransport. Use close_async() instead.", + stacklevel=2, + ) + self._flush_components() + else: + self.flush(timeout=timeout, callback=callback) + self._close_components() + self.transport.kill() + self.transport = None + + async def close_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush_async`. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "close_async() used with non-async transport, aborting. Please use close() instead." + ) + return + await self.flush_async(timeout=timeout, callback=callback) + self._close_components() + kill_task = self.transport.kill() # type: ignore + if kill_task is not None: + await kill_task + self.transport = None + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "flush() used with AsyncHttpTransport. Use flush_async() instead.", + stacklevel=2, + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + + self.transport.flush(timeout=timeout, callback=callback) + + async def flush_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "flush_async() used with non-async transport, aborting. Please use flush() instead." + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore + if flush_task is not None: + await flush_task + + def __enter__(self) -> "_Client": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + self.close() + + async def __aenter__(self) -> "_Client": + return self + + async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + await self.close_async() + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `get_options` is a + # type to have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `Dict[str, Any]` to tell static analyzers about the return type. + + class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 + pass + + class Client(ClientConstructor, _Client): + pass + +else: + # Alias `get_options` for actual usage. Go through the lambda indirection + # to throw PyCharm off of the weakly typed signature (it would otherwise + # discover both the weakly typed signature of `_init` and our faked `init` + # type). + + get_options = (lambda: _get_options)() + Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py new file mode 100644 index 0000000000..759898f6ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py @@ -0,0 +1,1802 @@ +import itertools +from enum import Enum +from typing import TYPE_CHECKING + +DEFAULT_MAX_VALUE_LENGTH = None + +DEFAULT_MAX_STACK_FRAMES = 100 +DEFAULT_ADD_FULL_STACK = False + + +# Also needs to be at the top to prevent circular import +class EndpointType(Enum): + """ + The type of an endpoint. This is an enum, rather than a constant, for historical reasons + (the old /store endpoint). The enum also preserve future compatibility, in case we ever + have a new endpoint. + """ + + ENVELOPE = "envelope" + OTLP_TRACES = "integration/otlp/v1/traces" + + +class CompressionAlgo(Enum): + GZIP = "gzip" + BROTLI = "br" + + +if TYPE_CHECKING: + from typing import ( + AbstractSet, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + ) + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + from sentry_sdk._types import ( + BreadcrumbProcessor, + ContinuousProfilerMode, + DataCollectionUserOptions, + Event, + EventProcessor, + Hint, + IgnoreSpansConfig, + Log, + Metric, + ProfilerMode, + SpanJSON, + TracesSampler, + TransactionProcessor, + ) + + # Experiments are feature flags to enable and disable certain unstable SDK + # functionality. Changing them from the defaults (`None`) in production + # code is highly discouraged. They are not subject to any stability + # guarantees such as the ones from semantic versioning. + Experiments = TypedDict( + "Experiments", + { + "max_spans": Optional[int], + "max_flags": Optional[int], + "record_sql_params": Optional[bool], + "continuous_profiling_auto_start": Optional[bool], + "continuous_profiling_mode": Optional[ContinuousProfilerMode], + "otel_powered_performance": Optional[bool], + "transport_zlib_compression_level": Optional[int], + "transport_compression_level": Optional[int], + "transport_compression_algo": Optional[CompressionAlgo], + "transport_num_pools": Optional[int], + "transport_http2": Optional[bool], + "transport_async": Optional[bool], + "enable_logs": Optional[bool], + "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], + "enable_metrics": Optional[bool], + "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], + "trace_lifecycle": Optional[Literal["static", "stream"]], + "ignore_spans": Optional[IgnoreSpansConfig], + "before_send_span": Optional[ + Callable[[SpanJSON, Hint], Optional[SpanJSON]] + ], + "suppress_asgi_chained_exceptions": Optional[bool], + "data_collection": Optional[DataCollectionUserOptions], + }, + total=False, + ) + +DEFAULT_QUEUE_SIZE = 100 +DEFAULT_MAX_BREADCRUMBS = 100 +MATCH_ALL = r".*" + +FALSE_VALUES = [ + "false", + "no", + "off", + "n", + "0", +] + + +class SPANTEMPLATE(str, Enum): + DEFAULT = "default" + AI_AGENT = "ai_agent" + AI_TOOL = "ai_tool" + AI_CHAT = "ai_chat" + + def __str__(self) -> str: + return self.value + + +class INSTRUMENTER: + SENTRY = "sentry" + OTEL = "otel" + + +class SPANNAME: + DB_COMMIT = "COMMIT" + DB_ROLLBACK = "ROLLBACK" + + +class SPANDATA: + """ + Additional information describing the type of the span. + See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ + """ + + AI_CITATIONS = "ai.citations" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + References or sources cited by the AI model in its response. + Example: ["Smith et al. 2020", "Jones 2019"] + """ + + AI_DOCUMENTS = "ai.documents" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Documents or content chunks used as context for the AI model. + Example: ["doc1.txt", "doc2.pdf"] + """ + + AI_FINISH_REASON = "ai.finish_reason" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. + + The reason why the model stopped generating. + Example: "length" + """ + + AI_FREQUENCY_PENALTY = "ai.frequency_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_FUNCTION_CALL = "ai.function_call" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_GENERATION_ID = "ai.generation_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. + + Unique identifier for the completion. + Example: "gen_123abc" + """ + + AI_INPUT_MESSAGES = "ai.input_messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. + + The input messages to an LLM call. + Example: [{"role": "user", "message": "hello"}] + """ + + AI_LOGIT_BIAS = "ai.logit_bias" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the logit bias + """ + + AI_METADATA = "ai.metadata" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Extra metadata passed to an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_MODEL_ID = "ai.model_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. + + The unique descriptor of the model being executed. + Example: gpt-4 + """ + + AI_PIPELINE_NAME = "ai.pipeline.name" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. + + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + AI_PREAMBLE = "ai.preamble" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the preamble parameter. + Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. + Example: "You are now a clown." + """ + + AI_PRESENCE_PENALTY = "ai.presence_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_RAW_PROMPTING = "ai.raw_prompting" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Minimize pre-processing done to the prompt sent to the LLM. + Example: true + """ + + AI_RESPONSE_FORMAT = "ai.response_format" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the format of the response + """ + + AI_RESPONSES = "ai.responses" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. + + The responses to an AI model call. Always as a list. + Example: ["hello", "world"] + """ + + AI_SEARCH_QUERIES = "ai.search_queries" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Queries used to search for relevant context or documents. + Example: ["climate change effects", "renewable energy"] + """ + + AI_SEARCH_REQUIRED = "ai.is_search_required" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Boolean indicating if the model needs to perform a search. + Example: true + """ + + AI_SEARCH_RESULTS = "ai.search_results" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Results returned from search queries for context. + Example: ["Result 1", "Result 2"] + """ + + AI_SEED = "ai.seed" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. + + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: 123.45 + """ + + AI_STREAMING = "ai.streaming" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. + + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + AI_TAGS = "ai.tags" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Tags that describe an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_TEMPERATURE = "ai.temperature" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. + + For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. + Example: 0.5 + """ + + AI_TEXTS = "ai.texts" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Raw text inputs provided to the model. + Example: ["What is machine learning?"] + """ + + AI_TOP_K = "ai.top_k" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. + + For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. + Example: 35 + """ + + AI_TOP_P = "ai.top_p" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. + + For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. + Example: 0.5 + """ + + AI_TOOL_CALLS = "ai.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_TOOLS = "ai.tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. + + For an AI model call, the functions that are available + """ + + AI_WARNINGS = "ai.warnings" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Warning messages generated during model execution. + Example: ["Token limit exceeded"] + """ + + CACHE_HIT = "cache.hit" + """ + A boolean indicating whether the requested data was found in the cache. + Example: true + """ + + CACHE_ITEM_SIZE = "cache.item_size" + """ + The size of the requested data in bytes. + Example: 58 + """ + + CACHE_KEY = "cache.key" + """ + The key of the requested data. + Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 + """ + + CLIENT_ADDRESS = "client.address" + """ + Client address of the network connection - IP address or Unix domain socket name. + Example: "10.1.2.80" + """ + + CODE_FILEPATH = "code.filepath" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FILE_PATH instead. + + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FILE_PATH = "code.file.path" + """ + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FUNCTION = "code.function" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead. + + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_FUNCTION_NAME = "code.function.name" + """ + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_LINENO = "code.lineno" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_LINE_NUMBER instead. + + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + Example: 42 + """ + + CODE_LINE_NUMBER = "code.line.number" + """ + The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. + Example: 42 + """ + + CODE_NAMESPACE = "code.namespace" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. + + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + Example: "http.handler" + """ + + DB_MONGODB_COLLECTION = "db.mongodb.collection" + """ + The MongoDB collection being accessed within the database. + See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes + Example: public.users; customers + """ + + DB_NAME = "db.name" + """ + .. deprecated:: + This attribute is deprecated. Use DB_NAMESPACE instead. + + The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Example: myDatabase + """ + + DB_NAMESPACE = "db.namespace" + """ + The name of the database being accessed. + Example: "customers" + """ + + DB_DRIVER_NAME = "db.driver.name" + """ + The name of the database driver being used for the connection. + Example: "psycopg2" + """ + + DB_OPERATION = "db.operation" + """ + .. deprecated:: + This attribute is deprecated. Use DB_OPERATION_NAME instead. + + The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: findAndModify, HMSET, SELECT + """ + + DB_OPERATION_NAME = "db.operation.name" + """ + The name of the operation being executed. + Example: "SELECT" + """ + + DB_SYSTEM = "db.system" + """ + .. deprecated:: + This attribute is deprecated. Use DB_SYSTEM_NAME instead. + + An identifier for the database management system (DBMS) product being used. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: postgresql + """ + + DB_QUERY_TEXT = "db.query.text" + """ + The database query being executed. + Example: "SELECT * FROM users WHERE id = $1" + """ + + DB_SYSTEM_NAME = "db.system.name" + """ + An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. + Example: "postgresql" + """ + + DB_USER = "db.user" + """ + The name of the database user used for connecting to the database. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: my_user + """ + + GEN_AI_AGENT_NAME = "gen_ai.agent.name" + """ + The name of the agent being used. + Example: "ResearchAssistant" + """ + + GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" + """ + The unique identifier for the conversation/thread with the AI model. + Example: "conv_abc123" + """ + + GEN_AI_CHOICE = "gen_ai.choice" + """ + The model's response message. + Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" + """ + + GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" + """ + The input to the embeddings operation. + Example: "Hello!" + """ + + GEN_AI_FUNCTION_ID = "gen_ai.function_id" + """ + Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. + Example: "my-awesome-function" + """ + + GEN_AI_OPERATION_NAME = "gen_ai.operation.name" + """ + The name of the operation being performed. + Example: "chat" + """ + + GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" + """ + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" + """ + The reason why the model stopped generating. + Example: "COMPLETE" + """ + + GEN_AI_RESPONSE_ID = "gen_ai.response.id" + """ + Unique identifier for the completion. + Example: "gen_123abc" + """ + + GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" + """ + Exact model identifier used to generate the response + Example: gpt-4o-mini-2024-07-18 + """ + + GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" + """ + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The model's response text messages. + Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] + """ + + GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" + """ + The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. + Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] + """ + + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" + """ + The time it took to receive the first token from the model. + Example: 0.1 + """ + + GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The tool calls in the model's response. + Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] + """ + + GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. + + The available tools for the model. + Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] + """ + + GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + """ + The list of source system tool definitions available to the GenAI agent or model. + Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] + """ + + GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" + """ + The frequency penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" + """ + The maximum number of tokens to generate in the response. + Example: 2048 + """ + + GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" + """ + The system instructions passed to the model. + Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] + """ + + GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. + + The messages passed to the model. The "content" can be a string or an array of objects. + Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] + """ + + GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" + """ + The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". + Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] + """ + + GEN_AI_REQUEST_MODEL = "gen_ai.request.model" + """ + The model identifier being used for the request. + Example: "gpt-4-turbo" + """ + + GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" + """ + The presence penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_SEED = "gen_ai.request.seed" + """ + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: "1234567890" + """ + + GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" + """ + The temperature parameter used to control randomness in the output. + Example: 0.7 + """ + + GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" + """ + Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). + Example: 35 + """ + + GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" + """ + The top_p parameter used to control diversity via nucleus sampling. + Example: 1.0 + """ + + GEN_AI_SYSTEM = "gen_ai.system" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. + + The name of the AI system being used. + Example: "openai" + """ + + GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" + """ + The Generative AI provider as identified by the client or server instrumentation. + Example: "openai" + """ + + GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" + """ + The description of the tool being used. + Example: "Searches the web for current information about a topic" + """ + + GEN_AI_TOOL_INPUT = "gen_ai.tool.input" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. + + The input of the tool being used. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" + """ + The arguments of the tool call. It has to be a stringified version of the arguments to the tool. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_NAME = "gen_ai.tool.name" + """ + The name of the tool being used. + Example: "web_search" + """ + + GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. + + The output of the tool being used. + Example: "rainy, 57°F" + """ + + GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" + """ + The result of the tool call. It has to be a stringified version of the result of the tool. + Example: "rainy, 57°F" + """ + + GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + """ + The number of tokens in the input. + Example: 150 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" + """ + The number of cached tokens in the input. + Example: 50 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" + """ + The number of tokens written to the cache when processing the AI input (prompt). + Example: 100 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + """ + The number of tokens in the output. + Example: 250 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" + """ + The number of tokens used for reasoning in the output. + Example: 75 + """ + + GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" + """ + The total number of tokens used (input + output). + Example: 400 + """ + + GEN_AI_USER_MESSAGE = "gen_ai.user.message" + """ + The user message passed to the model. + Example: "What's the weather in Paris?" + """ + + HTTP_FRAGMENT = "http.fragment" + """ + The Fragments present in the URL. + Example: #foo=bar + """ + + HTTP_METHOD = "http.method" + """ + .. deprecated:: + This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. + + The HTTP method used. + Example: GET + """ + + HTTP_REQUEST_BODY_DATA = "http.request.body.data" + """ + HTTP request body data. Can be given as string or structural data of any format. + Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" + """ + + HTTP_REQUEST_HEADER = "http.request.header" + """ + Prefix for HTTP request header attributes. The header name (lowercased) is + appended to form the full attribute key. + Example: "http.request.header.content-type" + """ + + HTTP_REQUEST_METHOD = "http.request.method" + """ + The HTTP method used. + Example: GET + """ + + HTTP_QUERY = "http.query" + """ + The Query string present in the URL. + Example: ?foo=bar&bar=baz + """ + + HTTP_STATUS_CODE = "http.response.status_code" + """ + The HTTP status code as an integer. + Example: 418 + """ + + MESSAGING_DESTINATION_NAME = "messaging.destination.name" + """ + The destination name where the message is being consumed from, + e.g. the queue name or topic. + """ + + MESSAGING_MESSAGE_ID = "messaging.message.id" + """ + The message's identifier. + """ + + MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" + """ + The latency between when the task was enqueued and when it was started to be processed. + """ + + MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" + """ + Number of retries/attempts to process a message. + """ + + MESSAGING_SYSTEM = "messaging.system" + """ + The messaging system's name, e.g. `kafka`, `aws_sqs` + """ + + MIDDLEWARE_NAME = "middleware.name" + """ + The middleware's name, e.g. `AuthenticationMiddleware` + """ + + NETWORK_PROTOCOL_NAME = "network.protocol.name" + """ + The application layer protocol name used for the network connection. + Example: "http", "https" + """ + + NETWORK_PEER_ADDRESS = "network.peer.address" + """ + Peer address of the network connection - IP address or Unix domain socket name. + Example: 10.1.2.80, /tmp/my.sock, localhost + """ + + NETWORK_PEER_PORT = "network.peer.port" + """ + Peer port number of the network connection. + Example: 6379 + """ + + NETWORK_TRANSPORT = "network.transport" + """ + The transport protocol used for the network connection. + Example: "tcp", "udp", "unix" + """ + + PROCESS_PID = "process.pid" + """ + The process ID of the running process. + Example: 12345 + """ + + PROCESS_COMMAND_ARGS = "process.command_args" + """ + All the command arguments (including the command/executable itself) as received by the process. + Example: ["cmd/otecol","--config=config.yaml"] + """ + + PROFILER_ID = "profiler_id" + """ + Label identifying the profiler id that the span occurred in. This should be a string. + Example: "5249fbada8d5416482c2f6e47e337372" + """ + + RPC_METHOD = "rpc.method" + """ + The fully-qualified logical name of the method from the RPC interface perspective. + Example: "com.example.ExampleService/exampleMethod" + """ + + RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" + """ + Status code of the RPC returned by the RPC server or generated by the client. + Example: "DEADLINE_EXCEEDED" + """ + + SERVER_ADDRESS = "server.address" + """ + Name of the database host. + Example: example.com + """ + + SERVER_PORT = "server.port" + """ + Logical server port number + Example: 80; 8080; 443 + """ + + SERVER_SOCKET_ADDRESS = "server.socket.address" + """ + Physical server IP address or Unix socket address. + Example: 10.5.3.2 + """ + + SERVER_SOCKET_PORT = "server.socket.port" + """ + Physical server port. + Recommended: If different than server.port. + Example: 16456 + """ + + THREAD_ID = "thread.id" + """ + Identifier of a thread from where the span originated. This should be a string. + Example: "7972576320" + """ + + THREAD_NAME = "thread.name" + """ + Label identifying a thread from where the span originated. This should be a string. + Example: "MainThread" + """ + + USER_EMAIL = "user.email" + """ + User email address. + Example: "test@example.com" + """ + + USER_ID = "user.id" + """ + Unique identifier of the user. + Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" + """ + + USER_IP_ADDRESS = "user.ip_address" + """ + The IP address of the user that triggered the request. + Example: "10.1.2.80" + """ + + USER_NAME = "user.name" + """ + Short name or login/username of the user. + Example: "j.smith" + """ + + URL_FULL = "url.full" + """ + The URL of the resource that was fetched. + Example: "https://example.com/test?foo=bar#buzz" + """ + + URL_FRAGMENT = "url.fragment" + """ + The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. + Example: "details" + """ + + URL_PATH = "url.path" + """ + The URI path component. + Example: "/foo" + """ + + URL_QUERY = "url.query" + """ + The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. + Example: "foo=bar&bar=baz" + """ + + MCP_TOOL_NAME = "mcp.tool.name" + """ + The name of the MCP tool being called. + Example: "get_weather" + """ + + MCP_PROMPT_NAME = "mcp.prompt.name" + """ + The name of the MCP prompt being retrieved. + Example: "code_review" + """ + + MCP_RESOURCE_URI = "mcp.resource.uri" + """ + The URI of the MCP resource being accessed. + Example: "file:///path/to/resource" + """ + + MCP_METHOD_NAME = "mcp.method.name" + """ + The MCP protocol method name being called. + Example: "tools/call", "prompts/get", "resources/read" + """ + + MCP_REQUEST_ID = "mcp.request.id" + """ + The unique identifier for the MCP request. + Example: "req_123abc" + """ + + MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" + """ + The result/output content from an MCP tool execution. + Example: "The weather is sunny" + """ + + MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" + """ + The number of items/keys in the MCP tool result. + Example: 5 + """ + + MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" + """ + Whether the MCP tool execution resulted in an error. + Example: True + """ + + MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" + """ + The message content from an MCP prompt retrieval. + Example: "Review the following code..." + """ + + MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" + """ + The role of the message in an MCP prompt retrieval (only set for single-message prompts). + Example: "user", "assistant", "system" + """ + + MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" + """ + The number of messages in an MCP prompt result. + Example: 1, 3 + """ + + MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" + """ + The protocol/scheme of the MCP resource URI. + Example: "file", "http", "https" + """ + + MCP_TRANSPORT = "mcp.transport" + """ + The transport method used for MCP communication. + Example: "http", "sse", "stdio" + """ + + MCP_SESSION_ID = "mcp.session.id" + """ + The session identifier for the MCP connection. + Example: "a1b2c3d4e5f6" + """ + + SENTRY_DIST = "sentry.dist" + """ + The Sentry dist. + Example: "1.0" + """ + + SENTRY_ENVIRONMENT = "sentry.environment" + """ + The Sentry environment. + Example: "prod" + """ + + SENTRY_RELEASE = "sentry.release" + """ + The Sentry release. + Example: "1.2.3" + """ + + SENTRY_PLATFORM = "sentry.platform" + """ + The sdk platform that generated the event. + Example: "python" + """ + + SENTRY_SDK_NAME = "sentry.sdk.name" + """ + The name of the SDK. + Example: "python" + """ + + SENTRY_SDK_VERSION = "sentry.sdk.version" + """ + The SDK version. + Example: "1.2.3" + """ + + SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" + """ + A list of names identifying enabled integrations. + Example: ["AtexitIntegration", "StdlibIntegration"] + """ + + +class SPANSTATUS: + """ + The status of a Sentry span. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + + ABORTED = "aborted" + ALREADY_EXISTS = "already_exists" + CANCELLED = "cancelled" + DATA_LOSS = "data_loss" + DEADLINE_EXCEEDED = "deadline_exceeded" + FAILED_PRECONDITION = "failed_precondition" + INTERNAL_ERROR = "internal_error" + INVALID_ARGUMENT = "invalid_argument" + NOT_FOUND = "not_found" + OK = "ok" + OUT_OF_RANGE = "out_of_range" + PERMISSION_DENIED = "permission_denied" + RESOURCE_EXHAUSTED = "resource_exhausted" + UNAUTHENTICATED = "unauthenticated" + UNAVAILABLE = "unavailable" + UNIMPLEMENTED = "unimplemented" + UNKNOWN_ERROR = "unknown_error" + + +class OP: + ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" + CACHE_GET = "cache.get" + CACHE_PUT = "cache.put" + COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" + COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" + DB = "db" + DB_CURSOR_ITERATOR = "db.cursor.iter" + DB_CURSOR_FETCH = "db.cursor.fetch" + DB_REDIS = "db.redis" + EVENT_DJANGO = "event.django" + FUNCTION = "function" + FUNCTION_AWS = "function.aws" + FUNCTION_GCP = "function.gcp" + GEN_AI_CHAT = "gen_ai.chat" + GEN_AI_CREATE_AGENT = "gen_ai.create_agent" + GEN_AI_EMBEDDINGS = "gen_ai.embeddings" + GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" + GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" + GEN_AI_HANDOFF = "gen_ai.handoff" + GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" + GEN_AI_RESPONSES = "gen_ai.responses" + GRAPHQL_EXECUTE = "graphql.execute" + GRAPHQL_MUTATION = "graphql.mutation" + GRAPHQL_PARSE = "graphql.parse" + GRAPHQL_RESOLVE = "graphql.resolve" + GRAPHQL_SUBSCRIPTION = "graphql.subscription" + GRAPHQL_QUERY = "graphql.query" + GRAPHQL_VALIDATE = "graphql.validate" + GRPC_CLIENT = "grpc.client" + GRPC_SERVER = "grpc.server" + HTTP_CLIENT = "http.client" + HTTP_CLIENT_STREAM = "http.client.stream" + HTTP_SERVER = "http.server" + MIDDLEWARE_DJANGO = "middleware.django" + MIDDLEWARE_LITESTAR = "middleware.litestar" + MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" + MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" + MIDDLEWARE_STARLETTE = "middleware.starlette" + MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" + MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" + MIDDLEWARE_STARLITE = "middleware.starlite" + MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" + MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" + HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( + "ai.chat_completions.create.huggingface_hub" + ) + QUEUE_PROCESS = "queue.process" + QUEUE_PUBLISH = "queue.publish" + QUEUE_SUBMIT_ARQ = "queue.submit.arq" + QUEUE_TASK_ARQ = "queue.task.arq" + QUEUE_SUBMIT_CELERY = "queue.submit.celery" + QUEUE_TASK_CELERY = "queue.task.celery" + QUEUE_TASK_RQ = "queue.task.rq" + QUEUE_SUBMIT_HUEY = "queue.submit.huey" + QUEUE_TASK_HUEY = "queue.task.huey" + QUEUE_SUBMIT_RAY = "queue.submit.ray" + QUEUE_TASK_RAY = "queue.task.ray" + QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" + QUEUE_SUBMIT_DJANGO = "queue.submit.django" + SUBPROCESS = "subprocess" + SUBPROCESS_WAIT = "subprocess.wait" + SUBPROCESS_COMMUNICATE = "subprocess.communicate" + TEMPLATE_RENDER = "template.render" + VIEW_RENDER = "view.render" + VIEW_RESPONSE_RENDER = "view.response.render" + WEBSOCKET_SERVER = "websocket.server" + SOCKET_CONNECTION = "socket.connection" + SOCKET_DNS = "socket.dns" + MCP_SERVER = "mcp.server" + + +# This type exists to trick mypy and PyCharm into thinking `init` and `Client` +# take these arguments (even though they take opaque **kwargs) +class ClientConstructor: + def __init__( + self, + dsn: "Optional[str]" = None, + *, + max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + server_name: "Optional[str]" = None, + shutdown_timeout: float = 2, + integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 + in_app_include: "List[str]" = [], # noqa: B006 + in_app_exclude: "List[str]" = [], # noqa: B006 + default_integrations: bool = True, + dist: "Optional[str]" = None, + transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, + transport_queue_size: int = DEFAULT_QUEUE_SIZE, + sample_rate: float = 1.0, + send_default_pii: "Optional[bool]" = None, + http_proxy: "Optional[str]" = None, + https_proxy: "Optional[str]" = None, + ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 + max_request_body_size: str = "medium", + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, + keep_alive: "Optional[bool]" = None, + before_send: "Optional[EventProcessor]" = None, + before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, + debug: "Optional[bool]" = None, + attach_stacktrace: bool = False, + ca_certs: "Optional[str]" = None, + propagate_traces: bool = True, + traces_sample_rate: "Optional[float]" = None, + traces_sampler: "Optional[TracesSampler]" = None, + profiles_sample_rate: "Optional[float]" = None, + profiles_sampler: "Optional[TracesSampler]" = None, + profiler_mode: "Optional[ProfilerMode]" = None, + profile_lifecycle: 'Literal["manual", "trace"]' = "manual", + profile_session_sample_rate: "Optional[float]" = None, + auto_enabling_integrations: bool = True, + disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, + auto_session_tracking: bool = True, + send_client_reports: bool = True, + _experiments: "Experiments" = {}, # noqa: B006 + proxy_headers: "Optional[Dict[str, str]]" = None, + instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, + before_send_transaction: "Optional[TransactionProcessor]" = None, + project_root: "Optional[str]" = None, + enable_tracing: "Optional[bool]" = None, + include_local_variables: "Optional[bool]" = True, + include_source_context: "Optional[bool]" = True, + trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 + MATCH_ALL + ], + functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 + event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, + max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, + enable_backpressure_handling: bool = True, + error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, + enable_db_query_source: bool = True, + db_query_source_threshold_ms: int = 100, + enable_http_request_source: bool = True, + http_request_source_threshold_ms: int = 100, + spotlight: "Optional[Union[bool, str]]" = None, + cert_file: "Optional[str]" = None, + key_file: "Optional[str]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, + add_full_stack: bool = DEFAULT_ADD_FULL_STACK, + max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, + enable_logs: bool = False, + before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, + trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), + enable_metrics: bool = True, + before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, + org_id: "Optional[str]" = None, + strict_trace_continuation: bool = False, + stream_gen_ai_spans: bool = True, + ) -> None: + """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. + + :param dsn: The DSN tells the SDK where to send the events. + + If this option is not set, the SDK will just not send any data. + + The `dsn` config option takes precedence over the environment variable. + + Learn more about `DSN utilization `_. + + :param debug: Turns debug mode on or off. + + When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes + wrong with event sending. + + The default is always `False`. It's generally not recommended to turn it on in production because of the + increase in log output. + + The `debug` config option takes precedence over the environment variable. + + :param release: Sets the release. + + If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to + manually set it to guarantee that the release is in sync with your deploy integrations. + + Release names are strings, but some formats are detected by Sentry and might be rendered differently. + + See `the releases documentation `_ to learn how the SDK tries to + automatically configure a release. + + The `release` config option takes precedence over the environment variable. + + Learn more about how to send release data so Sentry can tell you about regressions between releases and + identify the potential source in `the product documentation `_. + + :param environment: Sets the environment. This string is freeform and set to `production` by default. + + A release can be associated with more than one environment to separate them in the UI (think `staging` vs + `production` or similar). + + The `environment` config option takes precedence over the environment variable. + + :param dist: The distribution of the application. + + Distributions are used to disambiguate build or deployment variants of the same release of an application. + + The dist can be for example a build number. + + :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. + + The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of + error events will be sent. + + Events are picked randomly. + + :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. + + This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and + which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number + between `0.0` and `1.0`, inclusive. + + The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to + send the event with the given probability. + + If this configuration option is specified, the `sample_rate` option is ignored. + + :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. + + Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're + sent to Sentry. + + By default, all errors are sent. + + :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. + + This defaults to `100`, but you can set this to any number. + + However, you should be aware that Sentry has a `maximum payload size `_ + and any events exceeding that payload size will be dropped. + + :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. + + Stack traces are always attached to exceptions; however, when this option is set, stack traces are also + sent with messages. + + This option means that stack traces appear next to all log messages. + + Grouping in Sentry is different for events with stack traces and without. As a result, you will get new + groups as you enable or disable this flag for certain events. + + :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) + `_ is added by active integrations. + + If you enable this option, be sure to manually remove what you don't want to send using our features for + managing `Sensitive Data `_. + + :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and + passwords from a `denylist`. + + It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. + + See how to `configure the scrubber here `_. + + :param include_source_context: When enabled, source context will be included in events sent to Sentry. + + This source context includes the five lines of code above and below the line of code where an error + happened. + + :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with + the event to help with debugging. + + :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the + moment an error occurs. + + But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will + be included in the stack trace sent to Sentry. + + :param max_stack_frames: This option limits the number of stack frames that will be captured when + `add_full_stack` is enabled. + + :param server_name: This option can be used to supply a server name. + + When provided, the name of the server is sent along and persisted in the event. + + For many integrations, the server name actually corresponds to the device hostname, even in situations + where the machine is not actually a server. + + :param project_root: The full path to the root directory of your application. + + The `project_root` is used to mark frames in a stack trace either as being in your application or outside + of the application. + + :param in_app_include: A list of string prefixes of module names that belong to the app. + + This option takes precedence over `in_app_exclude`. + + Sentry differentiates stack frames that are directly related to your application ("in application") from + stack frames that come from other packages such as the standard library, frameworks, or other dependencies. + + The application package is automatically marked as `inApp`. + + The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are + displayed by default. + + :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to + third-party packages. + + Modules considered not part of the app will be hidden from stack traces by default. + + This option can be overridden using `in_app_include`. + + :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. + It can be set to one of the following values: + + - `never`: Request bodies are never sent. + - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically + 4KB). + - `medium`: Medium and small requests will be captured (typically 10KB). + - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. + + Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ + expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of + how you configure this option. + + :param max_value_length: The number of characters after which the values containing text in the event payload + will be truncated. + + WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be + dropped by Sentry. + + :param ca_certs: A path to an alternative CA bundle file in PEM-format. + + :param send_client_reports: Set this boolean to `False` to disable sending of client reports. + + Client reports allow the client to send status reports about itself to Sentry, such as information about + events that were dropped before being sent. + + :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) + `_. + + This setting can be used to override the default config options for a specific auto-enabling integration + or to add an integration that is not auto-enabled. + + :param disabled_integrations: List of integrations that will be disabled. + + This setting can be used to explicitly turn off specific `auto-enabling integrations (list) + `_ or + `default `_ integrations. + + :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) + `_ should be enabled. + + When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding + framework/library is detected. + + :param default_integrations: Configures whether `default integrations + `_ should be enabled. + + Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling + integrations**, unless they are specifically added in the `integrations` option, described above. + + :param before_send: This function is called with an SDK-specific message or error event object, and can return + a modified event object, or `null` to skip reporting the event. + + This can be used, for instance, for manual PII stripping before sending. + + By the time `before_send` is executed, all scope data has already been applied to the event. Further + modification of the scope won't have any effect. + + :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can + return a modified transaction event object, or `null` to skip reporting the event. + + One way this might be used is for manual PII stripping before sending. + + :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb + is added to the scope. + + When nothing is returned from the function, the breadcrumb is dropped. + + To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. + + The callback typically gets a second argument (called a "hint") which contains the original object from + which the breadcrumb was created to further customize what the breadcrumb should look like. + + :param transport: Switches out the transport used to send events. + + How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to + send it through some more complex setup that requires proxy authentication. + + :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to + flush. + + :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. + + This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs + support a separate HTTPS proxy. + + SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix + systems, the `http_proxy` environment variable will be picked up. + + :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. + + This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for + HTTPS requests at all times. + + :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded + to `urllib3`'s `ProxyManager `_. + + :param shutdown_timeout: Controls how many seconds to wait before shutting down. + + Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending + events. The default is SDK specific but typically around two seconds. + + Setting this value too low may cause problems for sending events from command line applications. + + Setting the value too high will cause the application to block for a long time for users experiencing + network connectivity problems. + + :param keep_alive: Determines whether to keep the connection alive between requests. + + This can be useful in environments where you encounter frequent network issues such as connection resets. + + :param cert_file: Path to the client certificate to use. + + If set, supersedes the `CLIENT_CERT_FILE` environment variable. + + :param key_file: Path to the key file to use. + + If set, supersedes the `CLIENT_KEY_FILE` environment variable. + + :param socket_options: An optional list of socket options to use. + + These provide fine-grained, low-level control over the way the SDK connects to Sentry. + + If provided, the options will override the default `urllib3` `socket options + `_. + + :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction + will be sent to Sentry. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + Either this or `traces_sampler` must be defined to enable tracing. + + If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have + another service (for example a JS frontend) that makes requests to your service that include trace + information, those traces will be continued and thus transactions will be sent to Sentry. + + If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces + will be started and no incoming traces will be continued. + + :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be + sent to Sentry. + + It will automatically be passed information about the transaction and the context in which it's being + created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being + sent). + + Can also be used for filtering transactions, by returning `0` for those that are unwanted. + + Either this or `traces_sample_rate` must be defined to enable tracing. + + :param trace_propagation_targets: An optional property that controls which downstream services receive tracing + data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. + + The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. + + If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to + that request. + + String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ + a string provided through the option. + + If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the + instrumented client. + + :param functions_to_trace: An optional list of functions that should be set up for tracing. + + For each function in the list, a span will be created when the function is executed. + + Functions in the list are represented as strings containing the fully qualified name of the function. + + This is a convenient option, making it possible to have one central place for configuring what functions + to trace, instead of having custom instrumentation scattered all over your code base. + + To learn more, see the `Custom Instrumentation `_ documentation. + + :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health + checks on the SDK. + + If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second + intervals until recovery. + + This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. + + This option is enabled by default. + + :param enable_db_query_source: When enabled, the source location will be added to database queries. + + :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database + queries. + + The query location will be added to the query for queries slower than the specified threshold. + + :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. + + :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an + outgoing HTTP request. + + The request location will be added to the request for requests slower than the specified threshold. + + :param custom_repr: A custom `repr `_ function to run + while serializing an object. + + Use this to control how your custom objects and classes are visible in Sentry. + + Return a string for that repr value to be used or `None` to continue serializing how Sentry would have + done it anyway. + + :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled + transaction will be profiled. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be + profiled. + + :param profiles_sampler: + + :param profiler_mode: + + :param profile_lifecycle: + + :param profile_session_sample_rate: + + :param enable_tracing: + + :param propagate_traces: + + :param auto_session_tracking: + + :param spotlight: + + :param instrumenter: + + :param enable_logs: Set `enable_logs` to True to enable the SDK to emit + Sentry logs. Defaults to False. + + :param before_send_log: An optional function to modify or filter out logs + before they're sent to Sentry. Any modifications to the log in this + function will be retained. If the function returns None, the log will + not be sent to Sentry. + + :param trace_ignore_status_codes: An optional property that disables tracing for + HTTP requests with certain status codes. + + Requests are not traced if the status code is contained in the provided set. + + If `trace_ignore_status_codes` is not provided, requests with any status code + may be traced. + + This option has no effect in span streaming mode (`trace_lifecycle="stream"`). + + :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the + `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. + + If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. + + The client's organization ID is extracted from the DSN or can be set with the `org_id` option. + If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. + This is useful to prevent traces of unknown third-party services from being continued in your application. + + :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases + but you can provide it explicitly for self-hosted and Relay setups. This value is used for + trace propagation and for features like `strict_trace_continuation`. + + :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to + reduce downstream data loss. + + :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. + + ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations + collect automatically, superseding `send_default_pii`. Passing a dict under + `_experiments={"data_collection": {...}}` opts into the feature; 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 + changes nothing. Restrict collection per category (user identity, cookies, HTTP + headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source + context). If `send_default_pii` is also set, `data_collection` takes precedence. + + Example:: + + sentry_sdk.init( + dsn="...", + _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, + ) + + See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. + """ + pass + + +def _get_default_options() -> "dict[str, Any]": + import inspect + + a = inspect.getfullargspec(ClientConstructor.__init__) + defaults = a.defaults or () + kwonlydefaults = a.kwonlydefaults or {} + + return dict( + itertools.chain( + zip(a.args[-len(defaults) :], defaults), + kwonlydefaults.items(), + ) + ) + + +DEFAULT_OPTIONS = _get_default_options() +del _get_default_options + + +VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py new file mode 100644 index 0000000000..b3287703b9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py @@ -0,0 +1,9 @@ +from sentry_sdk.crons.api import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.crons.decorator import monitor + +__all__ = [ + "capture_checkin", + "MonitorStatus", + "monitor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py new file mode 100644 index 0000000000..6ea3e36b6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py @@ -0,0 +1,60 @@ +import uuid +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, MonitorConfig + + +def _create_check_in_event( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration_s: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> "Event": + options = sentry_sdk.get_client().options + check_in_id = check_in_id or uuid.uuid4().hex + + check_in: "Event" = { + "type": "check_in", + "monitor_slug": monitor_slug, + "check_in_id": check_in_id, + "status": status, + "duration": duration_s, + "environment": options.get("environment", None), + "release": options.get("release", None), + } + + if monitor_config: + check_in["monitor_config"] = monitor_config + + return check_in + + +def capture_checkin( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> str: + check_in_event = _create_check_in_event( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=status, + duration_s=duration, + monitor_config=monitor_config, + ) + + sentry_sdk.capture_event(check_in_event) + + logger.debug( + f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" + ) + + return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py new file mode 100644 index 0000000000..be686b4539 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py @@ -0,0 +1,4 @@ +class MonitorStatus: + IN_PROGRESS = "in_progress" + OK = "ok" + ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py new file mode 100644 index 0000000000..b13d350e15 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py @@ -0,0 +1,137 @@ +from functools import wraps +from inspect import iscoroutinefunction +from typing import TYPE_CHECKING + +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from types import TracebackType + from typing import ( + Any, + Optional, + ParamSpec, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from sentry_sdk._types import MonitorConfig + + P = ParamSpec("P") + R = TypeVar("R") + + +class monitor: # noqa: N801 + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + """ + + def __init__( + self, + monitor_slug: "Optional[str]" = None, + monitor_config: "Optional[MonitorConfig]" = None, + ) -> None: + self.monitor_slug = monitor_slug + self.monitor_config = monitor_config + + def __enter__(self) -> None: + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, + status=MonitorStatus.IN_PROGRESS, + monitor_config=self.monitor_config, + ) + + def __exit__( + self, + exc_type: "Optional[Type[BaseException]]", + exc_value: "Optional[BaseException]", + traceback: "Optional[TracebackType]", + ) -> None: + duration_s = now() - self.start_timestamp + + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR + + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + monitor_config=self.monitor_config, + ) + + if TYPE_CHECKING: + + @overload + def __call__( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + # Unfortunately, mypy does not give us any reliable way to type check the + # return value of an Awaitable (i.e. async function) for this overload, + # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. + ... + + @overload + def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... + + def __call__( + self, + fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", + ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": + if iscoroutinefunction(fn): + return self._async_wrapper(fn) + + else: + if TYPE_CHECKING: + fn = cast("Callable[P, R]", fn) + return self._sync_wrapper(fn) + + def _async_wrapper( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + @wraps(fn) + async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return await fn(*args, **kwargs) + + return inner + + def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": + @wraps(fn) + def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return fn(*args, **kwargs) + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py new file mode 100644 index 0000000000..bcdf767409 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py @@ -0,0 +1,319 @@ +""" +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, List, Mapping, Optional, cast + +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE + +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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: + """ + Return whether ``key`` matches the sensitive denylist using a partial, + case-insensitive substring match. + + :param extra_terms: additional deny terms (e.g. user-provided) to consider + alongside the built-in `_SENSITIVE_DENYLIST`. + """ + lowered = key.lower() + for term in _SENSITIVE_DENYLIST: + if term in lowered: + return True + if extra_terms: + for term in extra_terms: + if term and term.lower() in lowered: + return True + return False + + +def _apply_key_value_collection_filtering( + items: "Mapping[str, Any]", + behaviour: "KeyValueCollectionBehaviour", + substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, +) -> "Dict[str, Any]": + + if behaviour["mode"] == "off": + return {} + + result: "Dict[str, Any]" = {} + + if behaviour["mode"] == "allowlist": + for key, value in items.items(): + is_allowed = False + if isinstance(key, str): + lowered = key.lower() + is_allowed = any( + term and term.lower() in lowered + for term in behaviour.get("terms", []) + ) + if is_allowed and not _is_sensitive_key(key): + result[key] = value + else: + result[key] = substitute + return result + + # denylist behaviour + for key, value in items.items(): + if isinstance(key, str) and _is_sensitive_key( + key=key, extra_terms=behaviour.get("terms", []) + ): + result[key] = substitute + else: + result[key] = value + return result + + +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, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py new file mode 100644 index 0000000000..795882e9ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py @@ -0,0 +1,37 @@ +import logging +import sys +import warnings +from logging import LogRecord + +from sentry_sdk import get_client +from sentry_sdk.client import _client_init_debug +from sentry_sdk.utils import logger + + +class _DebugFilter(logging.Filter): + def filter(self, record: "LogRecord") -> bool: + if _client_init_debug.get(False): + return True + + return get_client().options["debug"] + + +def init_debug_support() -> None: + if not logger.handlers: + configure_logger() + + +def configure_logger() -> None: + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.DEBUG) + logger.addFilter(_DebugFilter()) + + +def configure_debug_hub() -> None: + warnings.warn( + "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py new file mode 100644 index 0000000000..d2d4aae31a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py @@ -0,0 +1,332 @@ +import io +import json +import mimetypes +from typing import TYPE_CHECKING + +from sentry_sdk.session import Session +from sentry_sdk.utils import capture_internal_exceptions, json_dumps + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, List, Optional, Union + + from sentry_sdk._types import Event, EventDataCategory + + +def parse_json(data: "Union[bytes, str]") -> "Any": + # on some python 3 versions this needs to be bytes + if isinstance(data, bytes): + data = data.decode("utf-8", "replace") + return json.loads(data) + + +class Envelope: + """ + Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints + documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, + each envelope may have at most one Item with type "event" or "transaction" (but not both). + """ + + def __init__( + self, + headers: "Optional[Dict[str, Any]]" = None, + items: "Optional[List[Item]]" = None, + ) -> None: + if headers is not None: + headers = dict(headers) + self.headers = headers or {} + if items is None: + items = [] + else: + items = list(items) + self.items = items + + @property + def description(self) -> str: + return "envelope with %s items (%s)" % ( + len(self.items), + ", ".join(x.data_category for x in self.items), + ) + + def add_event( + self, + event: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=event), type="event")) + + def add_transaction( + self, + transaction: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) + + def add_profile( + self, + profile: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) + + def add_profile_chunk( + self, + profile_chunk: "Any", + ) -> None: + self.add_item( + Item( + payload=PayloadRef(json=profile_chunk), + type="profile_chunk", + headers={"platform": profile_chunk.get("platform", "python")}, + ) + ) + + def add_checkin( + self, + checkin: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) + + def add_session( + self, + session: "Union[Session, Any]", + ) -> None: + if isinstance(session, Session): + session = session.to_json() + self.add_item(Item(payload=PayloadRef(json=session), type="session")) + + def add_sessions( + self, + sessions: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) + + def add_item( + self, + item: "Item", + ) -> None: + self.items.append(item) + + def get_event(self) -> "Optional[Event]": + for items in self.items: + event = items.get_event() + if event is not None: + return event + return None + + def get_transaction_event(self) -> "Optional[Event]": + for item in self.items: + event = item.get_transaction_event() + if event is not None: + return event + return None + + def __iter__(self) -> "Iterator[Item]": + return iter(self.items) + + def serialize_into( + self, + f: "Any", + ) -> None: + f.write(json_dumps(self.headers)) + f.write(b"\n") + for item in self.items: + item.serialize_into(f) + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Envelope": + headers = parse_json(f.readline()) + items = [] + while 1: + item = Item.deserialize_from(f) + if item is None: + break + items.append(item) + return cls(headers=headers, items=items) + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Envelope": + return cls.deserialize_from(io.BytesIO(bytes)) + + def __repr__(self) -> str: + return "" % (self.headers, self.items) + + +class PayloadRef: + def __init__( + self, + bytes: "Optional[bytes]" = None, + path: "Optional[Union[bytes, str]]" = None, + json: "Optional[Any]" = None, + ) -> None: + self.json = json + self.bytes = bytes + self.path = path + + def get_bytes(self) -> bytes: + if self.bytes is None: + if self.path is not None: + with capture_internal_exceptions(): + with open(self.path, "rb") as f: + self.bytes = f.read() + elif self.json is not None: + self.bytes = json_dumps(self.json) + return self.bytes or b"" + + @property + def inferred_content_type(self) -> str: + if self.json is not None: + return "application/json" + elif self.path is not None: + path = self.path + if isinstance(path, bytes): + path = path.decode("utf-8", "replace") + ty = mimetypes.guess_type(path)[0] + if ty: + return ty + return "application/octet-stream" + + def __repr__(self) -> str: + return "" % (self.inferred_content_type,) + + +class Item: + def __init__( + self, + payload: "Union[bytes, str, PayloadRef]", + headers: "Optional[Dict[str, Any]]" = None, + type: "Optional[str]" = None, + content_type: "Optional[str]" = None, + filename: "Optional[str]" = None, + ): + if headers is not None: + headers = dict(headers) + elif headers is None: + headers = {} + self.headers = headers + if isinstance(payload, bytes): + payload = PayloadRef(bytes=payload) + elif isinstance(payload, str): + payload = PayloadRef(bytes=payload.encode("utf-8")) + else: + payload = payload + + if filename is not None: + headers["filename"] = filename + if type is not None: + headers["type"] = type + if content_type is not None: + headers["content_type"] = content_type + elif "content_type" not in headers: + headers["content_type"] = payload.inferred_content_type + + self.payload = payload + + def __repr__(self) -> str: + return "" % ( + self.headers, + self.payload, + self.data_category, + ) + + @property + def type(self) -> "Optional[str]": + return self.headers.get("type") + + @property + def data_category(self) -> "EventDataCategory": + ty = self.headers.get("type") + if ty == "session" or ty == "sessions": + return "session" + elif ty == "attachment": + return "attachment" + elif ty == "transaction": + return "transaction" + elif ty == "span": + return "span" + elif ty == "event": + return "error" + elif ty == "log": + return "log_item" + elif ty == "trace_metric": + return "trace_metric" + elif ty == "client_report": + return "internal" + elif ty == "profile": + return "profile" + elif ty == "profile_chunk": + return "profile_chunk" + elif ty == "check_in": + return "monitor" + else: + return "default" + + def get_bytes(self) -> bytes: + return self.payload.get_bytes() + + def get_event(self) -> "Optional[Event]": + """ + Returns an error event if there is one. + """ + if self.type == "event" and self.payload.json is not None: + return self.payload.json + return None + + def get_transaction_event(self) -> "Optional[Event]": + if self.type == "transaction" and self.payload.json is not None: + return self.payload.json + return None + + def serialize_into( + self, + f: "Any", + ) -> None: + headers = dict(self.headers) + bytes = self.get_bytes() + headers["length"] = len(bytes) + f.write(json_dumps(headers)) + f.write(b"\n") + f.write(bytes) + f.write(b"\n") + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Optional[Item]": + line = f.readline().rstrip() + if not line: + return None + headers = parse_json(line) + length = headers.get("length") + if length is not None: + payload = f.read(length) + f.readline() + else: + # if no length was specified we need to read up to the end of line + # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) + payload = f.readline().rstrip(b"\n") + if headers.get("type") in ("event", "transaction"): + rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) + else: + rv = cls(headers=headers, payload=payload) + return rv + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Optional[Item]": + return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py new file mode 100644 index 0000000000..5eaa5e440b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py @@ -0,0 +1,75 @@ +import copy +from threading import Lock +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import TypedDict + + FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) + + +DEFAULT_FLAG_CAPACITY = 100 + + +class FlagBuffer: + def __init__(self, capacity: int) -> None: + self.capacity = capacity + self.lock = Lock() + + # Buffer is private. The name is mangled to discourage use. If you use this attribute + # directly you're on your own! + self.__buffer = LRUCache(capacity) + + def clear(self) -> None: + self.__buffer = LRUCache(self.capacity) + + def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": + with self.lock: + buffer = FlagBuffer(self.capacity) + buffer.__buffer = copy.deepcopy(self.__buffer, memo) + return buffer + + def get(self) -> "list[FlagData]": + with self.lock: + return [ + {"flag": key, "result": value} for key, value in self.__buffer.get_all() + ] + + def set(self, flag: str, result: bool) -> None: + if isinstance(result, FlagBuffer): + # If someone were to insert `self` into `self` this would create a circular dependency + # on the lock. This is of course a deadlock. However, this is far outside the expected + # usage of this class. We guard against it here for completeness and to document this + # expected failure mode. + raise ValueError( + "FlagBuffer instances can not be inserted into the dictionary." + ) + + with self.lock: + self.__buffer.set(flag, result) + + +def add_feature_flag(flag: str, result: bool) -> None: + """ + Records a flag and its value to be sent on subsequent error events. + We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. + """ + client = sentry_sdk.get_client() + + flags = sentry_sdk.get_isolation_scope().flags + flags.set(flag, result) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.get_current_span() + if span and isinstance(span, sentry_sdk.traces.StreamedSpan): + span.set_attribute(f"flag.evaluation.{flag}", result) + + else: + span = sentry_sdk.get_current_span() + if span and isinstance(span, Span): + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py new file mode 100644 index 0000000000..b17444d06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py @@ -0,0 +1,741 @@ +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import ( + get_client, + get_current_scope, + get_global_scope, + get_isolation_scope, +) +from sentry_sdk._compat import with_metaclass +from sentry_sdk.client import Client +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.scope import _ScopeManager +from sentry_sdk.tracing import ( + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.utils import ( + ContextVar, + logger, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.tracing import TransactionKwargs + + T = TypeVar("T") + +else: + + def overload(x: "T") -> "T": + return x + + +class SentryHubDeprecationWarning(DeprecationWarning): + """ + A custom deprecation warning to inform users that the Hub is deprecated. + """ + + _MESSAGE = ( + "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " + "Please consult our 1.x to 2.x migration guide for details on how to migrate " + "`Hub` usage to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" + ) + + def __init__(self, *_: object) -> None: + super().__init__(self._MESSAGE) + + +@contextmanager +def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": + """Utility function to suppress deprecation warnings for the Hub.""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) + yield + + +_local = ContextVar("sentry_current_hub") + + +class HubMeta(type): + @property + def current(cls) -> "Hub": + """Returns the current instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + rv = _local.get(None) + if rv is None: + with _suppress_hub_deprecation_warning(): + # This will raise a deprecation warning; suppress it since we already warned above. + rv = Hub(GLOBAL_HUB) + _local.set(rv) + return rv + + @property + def main(cls) -> "Hub": + """Returns the main instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + return GLOBAL_HUB + + +class Hub(with_metaclass(HubMeta)): # type: ignore + """ + .. deprecated:: 2.0.0 + The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. + + The hub wraps the concurrency management of the SDK. Each thread has + its own hub but the hub might transfer with the flow of execution if + context vars are available. + + If the hub is used with a with statement it's temporarily activated. + """ + + _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] + _scope: "Optional[Scope]" = None + + # Mypy doesn't pick up on the metaclass. + + if TYPE_CHECKING: + current: "Hub" = None # type: ignore[assignment] + main: "Optional[Hub]" = None + + def __init__( + self, + client_or_hub: "Optional[Union[Hub, Client]]" = None, + scope: "Optional[Any]" = None, + ) -> None: + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + + current_scope = None + + if isinstance(client_or_hub, Hub): + client = get_client() + if scope is None: + # hub cloning is going on, we use a fork of the current/isolation scope for context manager + scope = get_isolation_scope().fork() + current_scope = get_current_scope().fork() + else: + client = client_or_hub + get_global_scope().set_client(client) + + if scope is None: # so there is no Hub cloning going on + # just the current isolation scope is used for context manager + scope = get_isolation_scope() + current_scope = get_current_scope() + + if current_scope is None: + # just the current current scope is used for context manager + current_scope = get_current_scope() + + self._stack = [(client, scope)] # type: ignore + self._last_event_id: "Optional[str]" = None + self._old_hubs: "List[Hub]" = [] + + self._old_current_scopes: "List[Scope]" = [] + self._old_isolation_scopes: "List[Scope]" = [] + self._current_scope: "Scope" = current_scope + self._scope: "Scope" = scope + + def __enter__(self) -> "Hub": + self._old_hubs.append(Hub.current) + _local.set(self) + + current_scope = get_current_scope() + self._old_current_scopes.append(current_scope) + scope._current_scope.set(self._current_scope) + + isolation_scope = get_isolation_scope() + self._old_isolation_scopes.append(isolation_scope) + scope._isolation_scope.set(self._scope) + + return self + + def __exit__( + self, + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[Any]", + ) -> None: + old = self._old_hubs.pop() + _local.set(old) + + old_current_scope = self._old_current_scopes.pop() + scope._current_scope.set(old_current_scope) + + old_isolation_scope = self._old_isolation_scopes.pop() + scope._isolation_scope.set(old_isolation_scope) + + def run( + self, + callback: "Callable[[], T]", + ) -> "T": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Runs a callback in the context of the hub. Alternatively the + with statement can be used on the hub directly. + """ + with self: + return callback() + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Any": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. + + Returns the integration for this hub by name or class. If there + is no client bound or the client does not have that integration + then `None` is returned. + + If the return value is not `None` the hub is guaranteed to have a + client attached. + """ + return get_client().get_integration(name_or_class) + + @property + def client(self) -> "Optional[BaseClient]": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Please use :py:func:`sentry_sdk.api.get_client` instead. + + Returns the current client on the hub. + """ + client = get_client() + + if not client.is_active(): + return None + + return client + + @property + def scope(self) -> "Scope": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Returns the current scope on the hub. + """ + return get_isolation_scope() + + def last_event_id(self) -> "Optional[str]": + """ + Returns the last event ID. + + .. deprecated:: 1.40.5 + This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. + """ + logger.warning( + "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." + ) + return self._last_event_id + + def bind_client( + self, + new: "Optional[BaseClient]", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.set_client` instead. + + Binds a new client to the hub. + """ + get_global_scope().set_client(new) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. + + Captures an event. + + Alias of :py:meth:`sentry_sdk.Scope.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + """ + last_event_id = get_current_scope().capture_event( + event, hint, scope=scope, **scope_kwargs + ) + + is_transaction = event.get("type") == "transaction" + if last_event_id is not None and not is_transaction: + self._last_event_id = last_event_id + + return last_event_id + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. + + Captures a message. + + Alias of :py:meth:`sentry_sdk.Scope.capture_message`. + + :param message: The string to send as the message to Sentry. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_message( + message, level=level, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. + + Captures an exception. + + Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_exception( + error, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. + + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_span` instead. + + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + """ + scope = get_current_scope() + return scope.start_span(instrumenter=instrumenter, **kwargs) + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. + + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. + """ + scope = get_current_scope() + + # For backwards compatibility, we allow passing the scope as the hub. + # We need a major release to make this nice. (if someone searches the code: deprecated) + # Type checking disabled for this line because deprecated keys are not allowed in the type signature. + kwargs["hub"] = scope # type: ignore + + return scope.start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + ) -> "Transaction": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. + + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers=environ_or_headers, op=op, name=name, source=source + ) + + @overload + def push_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def push_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def push_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + if callback is not None: + with self.push_scope() as scope: + callback(scope) + return None + + return _ScopeManager(self) + + def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pops a scope layer from the stack. + + Try to use the context manager :py:meth:`push_scope` instead. + """ + rv = self._stack.pop() + assert self._stack, "stack must have at least one layer" + return rv + + @overload + def configure_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def configure_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def configure_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + scope = get_isolation_scope() + + if continue_trace: + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + def start_session( + self, + session_mode: str = "application", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_session` instead. + + Starts a new session. + """ + get_isolation_scope().start_session( + session_mode=session_mode, + ) + + def end_session(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.end_session` instead. + + Ends the current session if there is one. + """ + get_isolation_scope().end_session() + + def stop_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. + + Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + get_isolation_scope().stop_auto_session_tracking() + + def resume_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. + + Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + get_isolation_scope().resume_auto_session_tracking() + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.flush` instead. + + Alias for :py:meth:`sentry_sdk.client._Client.flush` + """ + return get_client().flush(timeout=timeout, callback=callback) + + def get_traceparent(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. + + Returns the traceparent either from the active span or from the scope. + """ + current_scope = get_current_scope() + traceparent = current_scope.get_traceparent() + + if traceparent is None: + isolation_scope = get_isolation_scope() + traceparent = isolation_scope.get_traceparent() + + return traceparent + + def get_baggage(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. + + Returns Baggage either from the active span or from the scope. + """ + current_scope = get_current_scope() + baggage = current_scope.get_baggage() + + if baggage is None: + isolation_scope = get_isolation_scope() + baggage = isolation_scope.get_baggage() + + if baggage is not None: + return baggage.serialize() + + return None + + def iter_trace_propagation_headers( + self, span: "Optional[Span]" = None + ) -> "Generator[Tuple[str, str], None, None]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. + + Return HTTP headers which allow propagation of trace data. Data taken + from the span representing the request, if available, or the current + span on the scope if not. + """ + return get_current_scope().iter_trace_propagation_headers( + span=span, + ) + + def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. + + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + return get_current_scope().trace_propagation_meta( + span=span, + ) + + +with _suppress_hub_deprecation_warning(): + # Suppress deprecation warning for the Hub here, since we still always + # import this module. + GLOBAL_HUB = Hub() +_local.set(GLOBAL_HUB) + + +# Circular imports +from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py new file mode 100644 index 0000000000..677d34a81e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +from threading import Lock +from typing import TYPE_CHECKING + +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union + + +_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) + + +_installer_lock = Lock() + +# Set of all integration identifiers we have attempted to install +_processed_integrations: "Set[str]" = set() + +# Set of all integration identifiers we have actually installed +_installed_integrations: "Set[str]" = set() + + +def _generate_default_integrations_iterator( + integrations: "List[str]", + auto_enabling_integrations: "List[str]", +) -> "Callable[[bool], Iterator[Type[Integration]]]": + def iter_default_integrations( + with_auto_enabling_integrations: bool, + ) -> "Iterator[Type[Integration]]": + """Returns an iterator of the default integration classes:""" + from importlib import import_module + + if with_auto_enabling_integrations: + all_import_strings = integrations + auto_enabling_integrations + else: + all_import_strings = integrations + + for import_string in all_import_strings: + try: + module, cls = import_string.rsplit(".", 1) + yield getattr(import_module(module), cls) + except (DidNotEnable, SyntaxError) as e: + logger.debug( + "Did not import default integration %s: %s", import_string, e + ) + + if isinstance(iter_default_integrations.__doc__, str): + for import_string in integrations: + iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) + + return iter_default_integrations + + +_DEFAULT_INTEGRATIONS = [ + # stdlib/base runtime integrations + "sentry_sdk.integrations.argv.ArgvIntegration", + "sentry_sdk.integrations.atexit.AtexitIntegration", + "sentry_sdk.integrations.dedupe.DedupeIntegration", + "sentry_sdk.integrations.excepthook.ExcepthookIntegration", + "sentry_sdk.integrations.logging.LoggingIntegration", + "sentry_sdk.integrations.modules.ModulesIntegration", + "sentry_sdk.integrations.stdlib.StdlibIntegration", + "sentry_sdk.integrations.threading.ThreadingIntegration", +] + +_AUTO_ENABLING_INTEGRATIONS = [ + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.anthropic.AnthropicIntegration", + "sentry_sdk.integrations.ariadne.AriadneIntegration", + "sentry_sdk.integrations.arq.ArqIntegration", + "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", + "sentry_sdk.integrations.boto3.Boto3Integration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.chalice.ChaliceIntegration", + "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", + "sentry_sdk.integrations.cohere.CohereIntegration", + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.fastapi.FastApiIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.gql.GQLIntegration", + "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", + "sentry_sdk.integrations.graphene.GrapheneIntegration", + "sentry_sdk.integrations.httpx.HttpxIntegration", + "sentry_sdk.integrations.httpx2.Httpx2Integration", + "sentry_sdk.integrations.huey.HueyIntegration", + "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", + "sentry_sdk.integrations.langchain.LangchainIntegration", + "sentry_sdk.integrations.langgraph.LanggraphIntegration", + "sentry_sdk.integrations.litestar.LitestarIntegration", + "sentry_sdk.integrations.loguru.LoguruIntegration", + "sentry_sdk.integrations.mcp.MCPIntegration", + "sentry_sdk.integrations.openai.OpenAIIntegration", + "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", + "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", + "sentry_sdk.integrations.pymongo.PyMongoIntegration", + "sentry_sdk.integrations.pyramid.PyramidIntegration", + "sentry_sdk.integrations.quart.QuartIntegration", + "sentry_sdk.integrations.redis.RedisIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", + "sentry_sdk.integrations.starlette.StarletteIntegration", + "sentry_sdk.integrations.starlite.StarliteIntegration", + "sentry_sdk.integrations.strawberry.StrawberryIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", +] + +iter_default_integrations = _generate_default_integrations_iterator( + integrations=_DEFAULT_INTEGRATIONS, + auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, +) + +del _generate_default_integrations_iterator + + +_MIN_VERSIONS = { + "aiohttp": (3, 4), + "aiomysql": (0, 3, 0), + "anthropic": (0, 16), + "ariadne": (0, 20), + "arq": (0, 23), + "asyncpg": (0, 23), + "beam": (2, 12), + "boto3": (1, 16), # botocore + "bottle": (0, 12), + "celery": (4, 4, 7), + "chalice": (1, 16, 0), + "clickhouse_driver": (0, 2, 0), + "cohere": (5, 4, 0), + "django": (1, 8), + "dramatiq": (1, 9), + "falcon": (1, 4), + "fastapi": (0, 79, 0), + "flask": (1, 1, 4), + "gql": (3, 4, 1), + "graphene": (3, 3), + "google_genai": (1, 29, 0), # google-genai + "grpc": (1, 32, 0), # grpcio + "httpx": (0, 16, 0), + "httpx2": (2, 0, 0), + "huggingface_hub": (0, 24, 7), + "langchain": (0, 1, 0), + "langgraph": (0, 6, 6), + "launchdarkly": (9, 8, 0), + "litellm": (1, 77, 5), + "loguru": (0, 7, 0), + "mcp": (1, 15, 0), + "openai": (1, 0, 0), + "openai_agents": (0, 0, 19), + "openfeature": (0, 7, 1), + "pydantic_ai": (1, 0, 0), + "pymongo": (3, 5, 0), + "pyreqwest": (0, 11, 6), + "quart": (0, 16, 0), + "ray": (2, 7, 0), + "requests": (2, 0, 0), + "rq": (0, 6), + "sanic": (0, 8), + "sqlalchemy": (1, 2), + "starlette": (0, 16), + "starlite": (1, 48), + "statsig": (0, 55, 3), + "strawberry": (0, 209, 5), + "tornado": (6, 0), + "typer": (0, 15), + "unleash": (6, 0, 1), +} + + +_INTEGRATION_DEACTIVATES = { + "langchain": {"openai", "anthropic", "google_genai"}, + "openai_agents": {"openai"}, + "pydantic_ai": {"openai", "anthropic"}, +} + + +def setup_integrations( + integrations: "Sequence[Integration]", + with_defaults: bool = True, + with_auto_enabling_integrations: bool = False, + disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, + options: "Optional[Dict[str, Any]]" = None, +) -> "Dict[str, Integration]": + """ + Given a list of integration instances, this installs them all. + + When `with_defaults` is set to `True` all default integrations are added + unless they were already provided before. + + `disabled_integrations` takes precedence over `with_defaults` and + `with_auto_enabling_integrations`. + + Some integrations are designed to automatically deactivate other integrations + in order to avoid conflicts and prevent duplicate telemetry from being collected. + For example, enabling the `langchain` integration will auto-deactivate both the + `openai` and `anthropic` integrations. + + Users can override this behavior by: + - Explicitly providing an integration in the `integrations=[]` list, or + - Disabling the higher-level integration via the `disabled_integrations` option. + """ + integrations = dict( + (integration.identifier, integration) for integration in integrations or () + ) + + logger.debug("Setting up integrations (with default = %s)", with_defaults) + + user_provided_integrations = set(integrations.keys()) + + # Integrations that will not be enabled + disabled_integrations = [ + integration if isinstance(integration, type) else type(integration) + for integration in disabled_integrations or [] + ] + + # Integrations that are not explicitly set up by the user. + used_as_default_integration = set() + + if with_defaults: + for integration_cls in iter_default_integrations( + with_auto_enabling_integrations + ): + if integration_cls.identifier not in integrations: + instance = integration_cls() + integrations[instance.identifier] = instance + used_as_default_integration.add(instance.identifier) + + disabled_integration_identifiers = { + integration.identifier for integration in disabled_integrations + } + + for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): + if ( + integration in integrations + and integration not in disabled_integration_identifiers + ): + for target in targets_to_deactivate: + if target not in user_provided_integrations: + for cls in iter_default_integrations(True): + if cls.identifier == target: + if cls not in disabled_integrations: + disabled_integrations.append(cls) + logger.debug( + "Auto-deactivating %s integration because %s integration is active", + target, + integration, + ) + + for identifier, integration in integrations.items(): + with _installer_lock: + if identifier not in _processed_integrations: + if type(integration) in disabled_integrations: + logger.debug("Ignoring integration %s", identifier) + else: + logger.debug( + "Setting up previously not enabled integration %s", identifier + ) + try: + type(integration).setup_once() + integration.setup_once_with_options(options) + except DidNotEnable as e: + if identifier not in used_as_default_integration: + raise + + logger.debug( + "Did not enable default integration %s: %s", identifier, e + ) + else: + _installed_integrations.add(identifier) + + _processed_integrations.add(identifier) + + integrations = { + identifier: integration + for identifier, integration in integrations.items() + if identifier in _installed_integrations + } + + for identifier in integrations: + logger.debug("Enabling integration %s", identifier) + + return integrations + + +def _check_minimum_version( + integration: "type[Integration]", + version: "Optional[tuple[int, ...]]", + package: "Optional[str]" = None, +) -> None: + package = package or integration.identifier + + if version is None: + raise DidNotEnable(f"Unparsable {package} version.") + + min_version = _MIN_VERSIONS.get(integration.identifier) + if min_version is None: + return + + if version < min_version: + raise DidNotEnable( + f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." + ) + + +class DidNotEnable(Exception): # noqa: N818 + """ + The integration could not be enabled due to a trivial user error like + `flask` not being installed for the `FlaskIntegration`. + + This exception is silently swallowed for default integrations, but reraised + for explicitly enabled integrations. + """ + + +class Integration(ABC): + """Baseclass for all integrations. + + To accept options for an integration, implement your own constructor that + saves those options on `self`. + """ + + install = None + """Legacy method, do not implement.""" + + identifier: "str" = None # type: ignore[assignment] + """String unique ID of integration type""" + + @staticmethod + @abstractmethod + def setup_once() -> None: + """ + Initialize the integration. + + This function is only called once, ever. Configuration is not available + at this point, so the only thing to do here is to hook into exception + handlers, and perhaps do monkeypatches. + + Inside those hooks `Integration.current` can be used to access the + instance again. + """ + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + """ + Called after setup_once in rare cases on the instance and with options since we don't have those available above. + """ + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py new file mode 100644 index 0000000000..7ff4657013 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py @@ -0,0 +1,187 @@ +import urllib +from enum import Enum +from typing import TYPE_CHECKING + +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.scope import should_send_default_pii + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from typing_extensions import Literal + + from sentry_sdk.utils import AnnotatedValue + + +class _RootPathInPath(Enum): + EXCLUDED = "excluded" + EITHER = "either" + + +def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": + """ + Extract headers from the ASGI scope, in the format that the Sentry protocol expects. + """ + headers: "Dict[str, str]" = {} + for raw_key, raw_value in asgi_scope["headers"]: + key = raw_key.decode("latin-1") + value = raw_value.decode("latin-1") + if key in headers: + headers[key] = headers[key] + ", " + value + else: + headers[key] = value + + return headers + + +def _get_path( + asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" +) -> "str": + if root_path_in_path is _RootPathInPath.EXCLUDED: + return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") + + # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 + path = asgi_scope["path"] + root_path = asgi_scope.get("root_path", "") + + if not root_path or path == root_path or path.startswith(root_path + "/"): + return path + + return root_path + path + + +def _get_url( + asgi_scope: "Dict[str, Any]", + default_scheme: "Literal['ws', 'http']", + host: "Optional[Union[AnnotatedValue, str]]", + path: str, +) -> str: + """ + Extract URL from the ASGI scope, without also including the querystring. + """ + scheme = asgi_scope.get("scheme", default_scheme) + + server = asgi_scope.get("server", None) + + if host: + return "%s://%s%s" % (scheme, host, path) + + if server is not None: + host, port = server + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) + if port != default_port: + return "%s://%s:%s%s" % (scheme, host, port, path) + return "%s://%s%s" % (scheme, host, path) + return path + + +def _get_query(asgi_scope: "Any") -> "Any": + """ + Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. + """ + qs = asgi_scope.get("query_string") + if not qs: + return None + return urllib.parse.unquote(qs.decode("latin-1")) + + +def _get_ip(asgi_scope: "Any") -> str: + """ + Extract IP Address from the ASGI scope based on request headers with fallback to scope client. + """ + headers = _get_headers(asgi_scope) + try: + return headers["x-forwarded-for"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return headers["x-real-ip"] + except KeyError: + pass + + return asgi_scope.get("client")[0] + + +def _get_request_data( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "Dict[str, Any]": + """ + Returns data related to the HTTP request from the ASGI scope. + """ + request_data: "Dict[str, Any]" = {} + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + request_data["method"] = asgi_scope.get("method") + + headers = _get_headers(asgi_scope) + + request_data["headers"] = _filter_headers( + headers, + use_annotated_value=False, + ) + + request_data["query_string"] = _get_query(asgi_scope) + + request_data["url"] = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} + + return request_data + + +def _get_request_attributes( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "dict[str, Any]": + """ + Return attributes related to the HTTP request from the ASGI scope. + """ + attributes: "dict[str, Any]" = {} + + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + if asgi_scope.get("method"): + attributes["http.request.method"] = asgi_scope["method"].upper() + + headers = _get_headers(asgi_scope) + + filtered_headers = _filter_headers(headers, use_annotated_value=False) + for header, value in filtered_headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + if should_send_default_pii(): + query = _get_query(asgi_scope) + if query: + attributes["http.query"] = query + + path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) + attributes["url.path"] = path + + url_without_query_string = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=path, + ) + query_string = _get_query(asgi_scope) + attributes["url.full"] = ( + f"{url_without_query_string}?{query_string}" + if query_string is not None + else url_without_query_string + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + ip = _get_ip(asgi_scope) + attributes["client.address"] = ip + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py new file mode 100644 index 0000000000..ad0ab7c734 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py @@ -0,0 +1,282 @@ +import json +from contextlib import contextmanager +from copy import deepcopy + +import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.data_collection import _apply_key_value_collection_filtering +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger + +try: + from django.http.request import RawPostDataException + + _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) +except ImportError: + RawPostDataException = None + _RAW_DATA_EXCEPTIONS = (ValueError,) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union + + from sentry_sdk._types import Event, HttpStatusCodeRange + + +SENSITIVE_ENV_KEYS = ( + "REMOTE_ADDR", + "HTTP_X_FORWARDED_FOR", + "HTTP_SET_COOKIE", + "HTTP_COOKIE", + "HTTP_AUTHORIZATION", + "HTTP_PROXY_AUTHORIZATION", + "HTTP_X_API_KEY", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", +) + +SENSITIVE_HEADERS = tuple( + x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") +) + +DEFAULT_HTTP_METHODS_TO_CAPTURE = ( + "CONNECT", + "DELETE", + "GET", + # "HEAD", # do not capture HEAD requests by default + # "OPTIONS", # do not capture OPTIONS requests by default + "PATCH", + "POST", + "PUT", + "TRACE", +) + + +# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support +@contextmanager +def nullcontext() -> "Iterator[None]": + yield + + +def request_body_within_bounds( + client: "Optional[sentry_sdk.client.BaseClient]", content_length: int +) -> bool: + if client is None: + return False + + bodies = client.options["max_request_body_size"] + return not ( + bodies == "never" + or (bodies == "small" and content_length > 10**3) + or (bodies == "medium" and content_length > 10**4) + ) + + +class RequestExtractor: + """ + Base class for request extraction. + """ + + # It does not make sense to make this class an ABC because it is not used + # for typing, only so that child classes can inherit common methods from + # it. Only some child classes implement all methods that raise + # NotImplementedError in this class. + + def __init__(self, request: "Any") -> None: + self.request = request + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + + content_length = self.content_length() + request_info = event.get("request", {}) + + if should_send_default_pii(): + request_info["cookies"] = dict(self.cookies()) + + if not request_body_within_bounds(client, content_length): + data = AnnotatedValue.removed_because_over_size_limit() + else: + # First read the raw body data + # It is important to read this first because if it is Django + # it will cache the body and then we can read the cached version + # again in parsed_body() (or json() or wherever). + raw_data = None + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # If DjangoRestFramework is used it already read the body for us + # so reading it here will fail. We can ignore this. + pass + + parsed_body = self.parsed_body() + if parsed_body is not None: + data = parsed_body + elif raw_data: + data = AnnotatedValue.removed_because_raw_data() + else: + data = None + + if data is not None: + request_info["data"] = data + + event["request"] = deepcopy(request_info) + + def content_length(self) -> int: + try: + return int(self.env().get("CONTENT_LENGTH", 0)) + except ValueError: + return 0 + + def cookies(self) -> "MutableMapping[str, Any]": + raise NotImplementedError() + + def raw_data(self) -> "Optional[Union[str, bytes]]": + raise NotImplementedError() + + def form(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + form = self.form() + except Exception: + form = None + try: + files = self.files() + except Exception: + files = None + + if form or files: + data = {} + if form: + data = dict(form.items()) + if files: + for key in files.keys(): + data[key] = AnnotatedValue.removed_because_raw_data() + + return data + + return self.json() + + def is_json(self) -> bool: + return _is_json_content_type(self.env().get("CONTENT_TYPE")) + + def json(self) -> "Optional[Any]": + try: + if not self.is_json(): + return None + + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # The body might have already been read, in which case this will + # fail + raw_data = None + + if raw_data is None: + return None + + if isinstance(raw_data, str): + return json.loads(raw_data) + else: + return json.loads(raw_data.decode("utf-8")) + except ValueError: + pass + + return None + + def files(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def size_of_file(self, file: "Any") -> int: + raise NotImplementedError() + + def env(self) -> "Dict[str, Any]": + raise NotImplementedError() + + +def _is_json_content_type(ct: "Optional[str]") -> bool: + mt = (ct or "").split(";", 1)[0] + return ( + mt == "application/json" + or (mt.startswith("application/")) + and mt.endswith("+json") + ) + + +def _filter_headers( + headers: "Mapping[str, str]", + use_annotated_value: bool = True, +) -> "Mapping[str, Union[AnnotatedValue, str]]": + client_options = sentry_sdk.get_client().options + + if has_data_collection_enabled(client_options): + data_collection_configuration = client_options["data_collection"] + + filtered = _apply_key_value_collection_filtering( + items=headers, + behaviour=data_collection_configuration["http_headers"]["request"], + ) + + for key in filtered: + if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): + filtered[key] = SENSITIVE_DATA_SUBSTITUTE + + return filtered + else: + if should_send_default_pii(): + return headers + + substitute: "Union[AnnotatedValue, str]" = ( + SENSITIVE_DATA_SUBSTITUTE + if not use_annotated_value + else AnnotatedValue.removed_because_over_size_limit() + ) + + return { + k: ( + v + if k.upper().replace("-", "_") not in SENSITIVE_HEADERS + else substitute + ) + for k, v in headers.items() + } + + +def _in_http_status_code_range( + code: object, code_ranges: "list[HttpStatusCodeRange]" +) -> bool: + for target in code_ranges: + if isinstance(target, int): + if code == target: + return True + continue + + try: + if code in target: + return True + except TypeError: + logger.warning( + "failed_request_status_codes has to be a list of integers or containers" + ) + + return False + + +class HttpCodeRangeContainer: + """ + Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. + Used for backwards compatibility with the old `failed_request_status_codes` option. + """ + + def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: + self._code_ranges = code_ranges + + def __contains__(self, item: object) -> bool: + return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..59bae92a60 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,509 @@ +import sys +import weakref +from functools import wraps + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import ( + _filter_headers, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + NoOpStreamedSpan, + SegmentSource, + SpanStatus, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import ( + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + parse_url, + parse_version, + reraise, + transaction_from_function, +) + +try: + import asyncio + + from aiohttp import ClientSession, TraceConfig + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Set + from types import SimpleNamespace + from typing import Any, ContextManager, Optional, Tuple, Union + + from aiohttp import TraceRequestEndParams, TraceRequestStartParams + from aiohttp.web_request import Request + from aiohttp.web_urldispatcher import UrlMappingMatchInfo + + from sentry_sdk._types import Attributes, Event, EventProcessor + from sentry_sdk.tracing import Span + from sentry_sdk.utils import ExcInfo + + +TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + origin = f"auto.http.{identifier}" + + def __init__( + self, + transaction_style: str = "handler_name", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self._failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(AIOHTTP_VERSION) + _check_minimum_version(AioHttpIntegration, version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + # In the aiohttp integration, all of their HTTP responses are Exceptions. + # Because they have to be raised and handled by the framework, we need to + # register the exceptions as control flow exceptions so that we don't + # accidentally overwrite a status of "ok" with "error". + _register_control_flow_exception(HTTPException) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(AioHttpIntegration) + if integration is None: + return await old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + # Scope data will not leak between requests because aiohttp + # create a task to wrap each request. + scope.generate_propagation_context() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + headers = dict(request.headers) + + span_ctx: "ContextManager[Union[Span, StreamedSpan]]" + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context({"aiohttp_request": request}) + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, + use_annotated_value=False, + ).items(): + header_attributes[ + f"http.request.header.{header.lower()}" + ] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + url_attributes = {} + if should_send_default_pii(): + url_attributes["url.full"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + url_attributes["url.path"] = request.path + + if request.query_string: + url_attributes["url.query"] = request.query_string + + client_address_attributes = {} + if should_send_default_pii() and request.remote: + client_address_attributes["client.address"] = request.remote + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) + + span_ctx = sentry_sdk.traces.start_span( + # If this name makes it to the UI, AIOHTTP's URL + # resolver did not find a route or died trying. + name="generic AIOHTTP request", + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": AioHttpIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE.value, + "http.request.method": request.method, + **url_attributes, + **client_address_attributes, + **header_attributes, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + name="generic AIOHTTP request", + source=TransactionSource.ROUTE, + origin=AioHttpIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"aiohttp_request": request}, + ) + + with span_ctx as span: + try: + response = await old_handle(self, request) + except HTTPException as e: + if isinstance(span, StreamedSpan) and not isinstance( + span, NoOpStreamedSpan + ): + span.set_attribute( + "http.response.status_code", e.status_code + ) + + if e.status_code >= 400: + span.status = SpanStatus.ERROR.value + else: + span.status = SpanStatus.OK.value + else: + # Since a NoOpStreamedSpan can end up here, we have to guard against it + # so this only gets set in the legacy transaction approach. + if not isinstance(span, NoOpStreamedSpan): + span.set_http_status(e.status_code) + + if ( + e.status_code + in integration._failed_request_status_codes + ): + _capture_exception() + raise + except (asyncio.CancelledError, ConnectionResetError): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR.value + else: + span.set_status(SPANSTATUS.CANCELLED) + raise + except Exception: + # This will probably map to a 500 but seems like we + # have no way to tell. Do not set span status. + reraise(*_capture_exception()) + + try: + # A valid response handler will return a valid response with a status. But, if the handler + # returns an invalid response (e.g. None), the line below will raise an AttributeError. + # Even though this is likely invalid, we need to handle this case to ensure we don't break + # the application. + response_status = response.status + except AttributeError: + pass + else: + if isinstance(span, StreamedSpan): + span.set_attribute( + "http.response.status_code", response_status + ) + span.status = ( + SpanStatus.ERROR.value + if response_status >= 400 + else SpanStatus.OK.value + ) + else: + span.set_http_status(response_status) + + return response + + Application._handle = sentry_app_handle + + old_urldispatcher_resolve = UrlDispatcher.resolve + + @wraps(old_urldispatcher_resolve) + async def sentry_urldispatcher_resolve( + self: "UrlDispatcher", request: "Request" + ) -> "UrlMappingMatchInfo": + rv = await old_urldispatcher_resolve(self, request) + + integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) + if integration is None: + return rv + + name = None + + try: + if integration.transaction_style == "handler_name": + name = transaction_from_function(rv.handler) + elif integration.transaction_style == "method_and_path_pattern": + route_info = rv.get_info() + pattern = route_info.get("path") or route_info.get("formatter") + name = "{} {}".format(request.method, pattern) + except Exception: + pass + + if name is not None: + current_span = sentry_sdk.get_current_span() + if isinstance(current_span, StreamedSpan) and not isinstance( + current_span, NoOpStreamedSpan + ): + current_span._segment.name = name + current_span._segment.set_attribute( + "sentry.span.source", + SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, + ) + else: + current_scope = sentry_sdk.get_current_scope() + current_scope.set_transaction_name( + name, + source=SOURCE_FOR_STYLE[integration.transaction_style], + ) + + return rv + + UrlDispatcher.resolve = sentry_urldispatcher_resolve + + old_client_session_init = ClientSession.__init__ + + @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) + def init(*args: "Any", **kwargs: "Any") -> None: + client_trace_configs = list(kwargs.get("trace_configs") or ()) + trace_config = create_trace_config() + client_trace_configs.append(trace_config) + + kwargs["trace_configs"] = client_trace_configs + return old_client_session_init(*args, **kwargs) + + ClientSession.__init__ = init + + +def create_trace_config() -> "TraceConfig": + async def on_request_start( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestStartParams", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(AioHttpIntegration) is None: + return + + method = params.method.upper() + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(params.url), sanitize=False) + + span_name = "%s %s" % ( + method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ) + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(client.options): + attributes: "Attributes" = { + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": AioHttpIntegration.origin, + "http.request.method": method, + } + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + attributes["url.path"] = params.url.path + + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) + else: + legacy_span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=span_name, + origin=AioHttpIntegration.origin, + ) + legacy_span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + legacy_span.set_data("url", parsed_url.url) + legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span = legacy_span + + if should_propagate_trace(client, str(params.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=params.url + ) + ) + if key == BAGGAGE_HEADER_NAME and params.headers.get( + BAGGAGE_HEADER_NAME + ): + # do not overwrite any existing baggage, just append to it + params.headers[key] += "," + value + else: + params.headers[key] = value + + trace_config_ctx.span = span + + async def on_request_end( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestEndParams", + ) -> None: + if trace_config_ctx.span is None: + return + + span = trace_config_ctx.span + status = int(params.response.status) + + if isinstance(span, StreamedSpan): + span.set_attribute("http.response.status_code", status) + span.status = ( + SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value + ) + + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.set_http_status(status) + span.set_data("reason", params.response.reason) + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + trace_config = TraceConfig() + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + + return trace_config + + +def _make_request_processor( + weak_request: "weakref.ReferenceType[Request]", +) -> "EventProcessor": + def aiohttp_processor( + event: "Event", + hint: "dict[str, Tuple[type, BaseException, Any]]", + ) -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + request_info = event.setdefault("request", {}) + + request_info["url"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote} + request_info["headers"] = _filter_headers(dict(request.headers)) + + # Just attach raw data here if it is within bounds, if available. + # Unfortunately there's no way to get structured data from aiohttp + # without awaiting on some coroutine. + request_info["data"] = get_aiohttp_request_data(request) + + return event + + return aiohttp_processor + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "aiohttp", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + return exc_info + + +BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" + + +def get_aiohttp_request_data( + request: "Request", +) -> "Union[Optional[str], AnnotatedValue]": + bytes_body = request._read_bytes + + if bytes_body is not None: + # we have body to show + if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): + return AnnotatedValue.substituted_because_over_size_limit() + + encoding = request.charset or "utf-8" + return bytes_body.decode(encoding, "replace") + + if request.can_read_body: + # body exists but we can't show it + return BODY_NOT_READ_MESSAGE + + # request has no body + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py new file mode 100644 index 0000000000..49459268e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Any, Awaitable, Callable, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import aiomysql # type: ignore[import-not-found] + from aiomysql.connection import Connection # type: ignore[import-not-found] + from aiomysql.cursors import Cursor # type: ignore[import-not-found] +except ImportError: + raise DidNotEnable("aiomysql not installed.") + + +class AioMySQLIntegration(Integration): + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) + + Cursor.execute = _wrap_execute(Cursor.execute) + Cursor.executemany = _wrap_executemany(Cursor.executemany) + + # Patch Connection._connect — this catches ALL connections: + # - aiomysql.connect() + # - aiomysql.create_pool() (pool.py does `from .connection import connect` + # which ultimately calls Connection._connect) + # - Reconnects + Connection._connect = _wrap_connect(Connection._connect) + + +T = TypeVar("T") + + +def _normalize_query(query: str | bytes | bytearray) -> str: + if isinstance(query, (bytes, bytearray)): + query = query.decode("utf-8", errors="replace") + return " ".join(query.split()) + + +def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.execute to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + + # Skip if flagged by executemany (avoids double-recording). + # Do NOT reset the flag here — it must stay True for the entire + # duration of executemany, which may call execute multiple times + # in a loop (non-INSERT fallback). Only _wrap_executemany's + # finally block should clear it. + if getattr(cursor, "_sentry_skip_next_execute", False): + return await f(*args, **kwargs) + + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = params if integration and integration._record_params else None + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.executemany to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = ( + seq_of_params if integration and integration._record_params else None + ) + param_style = "pyformat" if params_list else None + + # Prevent double-recording: _do_execute_many calls self.execute internally + cursor._sentry_skip_next_execute = True + try: + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=True, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + finally: + cursor._sentry_skip_next_execute = False + + return _inner + + +def _get_connection(cursor: Any) -> Any: + """Get the underlying connection from a cursor.""" + return getattr(cursor, "connection", None) + + +def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Connection._connect to capture connection spans.""" + + async def _inner(self: "Connection") -> T: + client = sentry_sdk.get_client() + if client.get_integration(AioMySQLIntegration) is None: + return await f(self) + + if has_span_streaming_enabled(client.options): + breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) + + span_attributes: dict[str, Any] = { + "sentry.op": OP.DB, + "sentry.origin": AioMySQLIntegration.origin, + } | breadcrumb_data + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=breadcrumb_data + ) + res = await f(self) + else: + connect_data = _get_connect_data(self) + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + _set_db_data(span, self) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", + category="query", + data=connect_data, + ) + res = await f(self) + + return res + + return _inner + + +def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: + if use_streaming_keys: + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + data: dict[str, Any] = { + db_system: "mysql", + SPANDATA.DB_DRIVER_NAME: "aiomysql", + } + + host = getattr(conn, "host", None) + if host is not None: + data[SPANDATA.SERVER_ADDRESS] = host + + port = getattr(conn, "port", None) + if port is not None: + data[SPANDATA.SERVER_PORT] = port + + database = getattr(conn, "db", None) + if database is not None: + data[db_name] = database + + user = getattr(conn, "user", None) + if user is not None: + data[SPANDATA.DB_USER] = user + + return data + + +def _set_db_data(span: Any, conn: Any) -> None: + """Set database-related span data from connection object.""" + if isinstance(span, StreamedSpan): + set_value = span.set_attribute + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + set_value = span.set_data + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + set_value(db_system, "mysql") + set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") + + host = getattr(conn, "host", None) + if host is not None: + set_value(SPANDATA.SERVER_ADDRESS, host) + + port = getattr(conn, "port", None) + if port is not None: + set_value(SPANDATA.SERVER_PORT, port) + + database = getattr(conn, "db", None) + if database is not None: + set_value(db_name, database) + + user = getattr(conn, "user", None) + if user is not None: + set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py new file mode 100644 index 0000000000..dfa4aef34c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py @@ -0,0 +1,1154 @@ +import json +import sys +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_anthropic_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + package_version, + reraise, + safe_serialize, +) + +try: + try: + from anthropic import NotGiven + except ImportError: + NotGiven = None + + try: + from anthropic import Omit + except ImportError: + Omit = None + + from anthropic import AsyncStream, Stream + from anthropic.lib.streaming import ( + AsyncMessageStream, + AsyncMessageStreamManager, + MessageStream, + MessageStreamManager, + ) + from anthropic.resources import AsyncMessages, Messages + from anthropic.types import ( + ContentBlockDeltaEvent, + ContentBlockStartEvent, + ContentBlockStopEvent, + MessageDeltaEvent, + MessageStartEvent, + MessageStopEvent, + ) + + if TYPE_CHECKING: + from anthropic.types import MessageStreamEvent, TextBlockParam +except ImportError: + raise DidNotEnable("Anthropic not installed") + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Iterator, + Optional, + Union, + ) + + from anthropic.types import ( + MessageParam, + ModelParam, + RawMessageStreamEvent, + TextBlockParam, + ToolUnionParam, + ) + + from sentry_sdk._types import TextPart + + +class _RecordedUsage: + output_tokens: int = 0 + input_tokens: int = 0 + cache_write_input_tokens: "Optional[int]" = 0 + cache_read_input_tokens: "Optional[int]" = 0 + + +class _StreamSpanContext: + """ + Sets accumulated data on the stream's span and finishes the span on exit. + Is a no-op if the stream has no span set, i.e., when the span has already been finished. + """ + + def __init__( + self, + stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", + # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). + guaranteed_streaming_state: bool = False, + ) -> None: + self._stream = stream + self._guaranteed_streaming_state = guaranteed_streaming_state + + def __enter__(self) -> "_StreamSpanContext": + return self + + def __exit__( + self, + exc_type: "Optional[type[BaseException]]", + exc_val: "Optional[BaseException]", + exc_tb: "Optional[Any]", + ) -> None: + with capture_internal_exceptions(): + if not hasattr(self._stream, "_span"): + return + + if not self._guaranteed_streaming_state and not hasattr( + self._stream, "_model" + ): + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + return + + _set_streaming_output_data( + span=self._stream._span, + integration=self._stream._integration, + model=self._stream._model, + usage=self._stream._usage, + content_blocks=self._stream._content_blocks, + response_id=self._stream._response_id, + finish_reason=self._stream._finish_reason, + ) + + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + + +class AnthropicIntegration(Integration): + identifier = "anthropic" + origin = f"auto.ai.{identifier}" + + def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + version = package_version("anthropic") + _check_minimum_version(AnthropicIntegration, version) + + """ + client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. + Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. + The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, + information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. + + The span can be finished in two places: + - When the user exits the context manager or directly calls close(), the patched close() finishes the span. + - When iteration ends, the finally block in the _iterator wrapper finishes the span. + + Both paths may run. For example, the context manager exit can follow iterator exhaustion. + """ + Messages.create = _wrap_message_create(Messages.create) + Stream.close = _wrap_close(Stream.close) + + AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) + AsyncStream.close = _wrap_async_close(AsyncStream.close) + + """ + client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. + """ + Messages.stream = _wrap_message_stream(Messages.stream) + MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( + MessageStreamManager.__enter__ + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. + if not issubclass(MessageStream, Stream): + MessageStream.close = _wrap_close(MessageStream.close) + + AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) + AsyncMessageStreamManager.__aenter__ = ( + _wrap_async_message_stream_manager_aenter( + AsyncMessageStreamManager.__aenter__ + ) + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. + if not issubclass(AsyncMessageStream, AsyncStream): + AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "anthropic", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": + """ + Get token usage from the Anthropic response. + Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) + """ + input_tokens = 0 + output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 + if hasattr(result, "usage"): + usage = result.usage + if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): + input_tokens = usage.input_tokens + if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): + output_tokens = usage.output_tokens + if hasattr(usage, "cache_read_input_tokens") and isinstance( + usage.cache_read_input_tokens, int + ): + cache_read_input_tokens = usage.cache_read_input_tokens + if hasattr(usage, "cache_creation_input_tokens") and isinstance( + usage.cache_creation_input_tokens, int + ): + cache_write_input_tokens = usage.cache_creation_input_tokens + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens so downstream cost calculations + # (input_tokens - cached) don't produce negative values. + input_tokens += cache_read_input_tokens + cache_write_input_tokens + + return ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) + + +def _collect_ai_data( + event: "MessageStreamEvent", + model: "str | None", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": + """ + Collect model information, token usage, and collect content blocks from the AI streaming response. + """ + with capture_internal_exceptions(): + if hasattr(event, "type"): + if event.type == "content_block_start": + pass + elif event.type == "content_block_delta": + if hasattr(event.delta, "text"): + content_blocks.append(event.delta.text) + elif hasattr(event.delta, "partial_json"): + content_blocks.append(event.delta.partial_json) + elif event.type == "content_block_stop": + pass + + # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. + # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 + if event.type == "message_start": + model = event.message.model or model + response_id = event.message.id + + incoming_usage = event.message.usage + usage.output_tokens = incoming_usage.output_tokens + usage.input_tokens = incoming_usage.input_tokens + + usage.cache_write_input_tokens = getattr( + incoming_usage, "cache_creation_input_tokens", None + ) + usage.cache_read_input_tokens = getattr( + incoming_usage, "cache_read_input_tokens", None + ) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + # Counterintuitive, but message_delta contains cumulative token counts :) + if event.type == "message_delta": + usage.output_tokens = event.usage.output_tokens + + # Update other usage fields if they exist in the event + input_tokens = getattr(event.usage, "input_tokens", None) + if input_tokens is not None: + usage.input_tokens = input_tokens + + cache_creation_input_tokens = getattr( + event.usage, "cache_creation_input_tokens", None + ) + if cache_creation_input_tokens is not None: + usage.cache_write_input_tokens = cache_creation_input_tokens + + cache_read_input_tokens = getattr( + event.usage, "cache_read_input_tokens", None + ) + if cache_read_input_tokens is not None: + usage.cache_read_input_tokens = cache_read_input_tokens + # TODO: Record event.usage.server_tool_use + + if event.delta.stop_reason is not None: + finish_reason = event.delta.stop_reason + + return (model, usage, content_blocks, response_id, finish_reason) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + +def _transform_anthropic_content_block( + content_block: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Transform an Anthropic content block using the Anthropic-specific transformer, + with special handling for Anthropic's text-type documents. + """ + # Handle Anthropic's text-type documents specially (not covered by shared function) + if content_block.get("type") == "document": + source = content_block.get("source") + if isinstance(source, dict) and source.get("type") == "text": + return { + "type": "text", + "text": source.get("data", ""), + } + + # Use Anthropic-specific transformation + result = transform_anthropic_content_part(content_block) + return result if result is not None else content_block + + +def _transform_system_instructions( + system_instructions: "Union[str, Iterable[TextBlockParam]]", +) -> "list[TextPart]": + if isinstance(system_instructions, str): + return [ + { + "type": "text", + "content": system_instructions, + } + ] + + return [ + { + "type": "text", + "content": instruction["text"], + } + for instruction in system_instructions + if isinstance(instruction, dict) and "text" in instruction + ] + + +def _set_common_input_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Optional[Union[str, Iterable[TextBlockParam]]]", + temperature: "Optional[float]", + top_k: "Optional[int]", + top_p: "Optional[float]", + tools: "Optional[Iterable[ToolUnionParam]]", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if ( + messages is not None + and len(messages) > 0 # type: ignore + and should_send_default_pii() + and integration.include_prompts + ): + if isinstance(system, str) or isinstance(system, Iterable): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system)), + ) + + normalized_messages = [] + for message in messages: + if ( + message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER + and "content" in message + and isinstance(message["content"], (list, tuple)) + ): + transformed_content = [] + for item in message["content"]: + # Skip tool_result items - they can contain images/documents + # with nested structures that are difficult to redact properly + if isinstance(item, dict) and item.get("type") == "tool_result": + continue + + # Transform content blocks (images, documents, etc.) + transformed_content.append( + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + ) + + # If there are non-tool-result items, add them as a message + if transformed_content: + normalized_messages.append( + { + "role": message.get("role"), + "content": transformed_content, + } + ) + else: + # Transform content for non-list messages or assistant messages + transformed_message = message.copy() + if "content" in transformed_message: + content = transformed_message["content"] + if isinstance(content, (list, tuple)): + transformed_message["content"] = [ + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + for item in content + ] + normalized_messages.append(transformed_message) + + role_normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(role_normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else role_normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + if model is not None and _is_given(model): + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + if top_k is not None and _is_given(top_k): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore + set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) + + +def _set_create_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "AnthropicIntegration", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=kwargs.get("max_tokens"), # type: ignore + messages=kwargs.get("messages"), # type: ignore + model=kwargs.get("model"), + system=kwargs.get("system"), + temperature=kwargs.get("temperature"), + top_k=kwargs.get("top_k"), + top_p=kwargs.get("top_p"), + tools=kwargs.get("tools"), + ) + + +def _wrap_synchronous_message_iterator( + stream: "Union[Stream, MessageStream]", + iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +async def _wrap_asynchronous_message_iterator( + stream: "Union[AsyncStream, AsyncMessageStream]", + iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + async for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +def _set_output_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + model: "str | None", + input_tokens: "int | None", + output_tokens: "int | None", + cache_read_input_tokens: "int | None", + cache_write_input_tokens: "int | None", + content_blocks: "list[Any]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> None: + """ + Set output data for the span based on the AI response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + if finish_reason is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in content_blocks: + if output["type"] == "text": + output_messages["response"].append(output["text"]) + elif output["type"] == "tool_use": + output_messages["tool"].append(output) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + record_token_usage( + span, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_tokens_cached=cache_read_input_tokens, + input_tokens_cache_write=cache_write_input_tokens, + ) + + +def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, Stream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_synchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +async def _sentry_patched_create_async( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return await f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, AsyncStream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_asynchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +def _wrap_message_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return _sentry_patched_create_sync(f, *args, **kwargs) + + return _sentry_wrapped_create_sync + + +def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: + """ + Initialize fields for accumulating output on the Stream instance. + """ + if not hasattr(stream, "_model"): + stream._model = None + stream._usage = _RecordedUsage() + stream._content_blocks = [] + stream._response_id = None + stream._finish_reason = None + + +def _accumulate_event_data( + stream: "Union[Stream, MessageStream]", + event: "Union[RawMessageStreamEvent, MessageStreamEvent]", +) -> None: + """ + Update accumulated output from a single stream event. + """ + (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( + event, + stream._model, + stream._usage, + stream._content_blocks, + stream._response_id, + stream._finish_reason, + ) + + stream._model = model + stream._usage = usage + stream._content_blocks = content_blocks + stream._response_id = response_id + stream._finish_reason = finish_reason + + +def _set_streaming_output_data( + span: "Span", + integration: "AnthropicIntegration", + model: "Optional[str]", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "Optional[str]", + finish_reason: "Optional[str]", +) -> None: + """ + Set output attributes on the AI Client Span. + """ + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + response_id=response_id, + finish_reason=finish_reason, + ) + + +def _wrap_close( + f: "Callable[..., None]", +) -> "Callable[..., None]": + """ + Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. + """ + + def close(self: "Union[Stream, MessageStream]") -> None: + with _StreamSpanContext(self): + return f(self) + + return close + + +def _wrap_message_create_async(f: "Any") -> "Any": + @wraps(f) + async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return await _sentry_patched_create_async(f, *args, **kwargs) + + return _sentry_wrapped_create_async + + +def _wrap_async_close( + f: "Callable[..., Awaitable[None]]", +) -> "Callable[..., Awaitable[None]]": + """ + Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. + """ + + async def close(self: "AsyncStream") -> None: + with _StreamSpanContext(self): + return await f(self) + + return close + + +def _wrap_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_message_stream_manager_enter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": + if not hasattr(self, "_max_tokens"): + return f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return f(self) + + if self._messages is None: + return f(self) + + try: + iter(self._messages) + except TypeError: + return f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_synchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_enter + + +def _wrap_async_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream( + *args: "Any", **kwargs: "Any" + ) -> "AsyncMessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + async def _sentry_patched_aenter( + self: "AsyncMessageStreamManager", + ) -> "AsyncMessageStream": + if not hasattr(self, "_max_tokens"): + return await f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return await f(self) + + if self._messages is None: + return await f(self) + + try: + iter(self._messages) + except TypeError: + return await f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = await f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_asynchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_aenter + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different anthropic versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py new file mode 100644 index 0000000000..0215ffa093 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py @@ -0,0 +1,28 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class ArgvIntegration(Integration): + identifier = "argv" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: + extra = event.setdefault("extra", {}) + # If some event processor decided to set extra to e.g. an + # `int`, don't crash. Not here. + if isinstance(extra, dict): + extra["sys.argv"] = sys.argv + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py new file mode 100644 index 0000000000..1ce228d3a5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py @@ -0,0 +1,167 @@ +from importlib import import_module + +import sentry_sdk +from sentry_sdk import capture_event, get_client +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + # importing like this is necessary due to name shadowing in ariadne + # (ariadne.graphql is also a function) + ariadne_graphql = import_module("ariadne.graphql") +except ImportError: + raise DidNotEnable("ariadne is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, List, Optional + + from ariadne.types import ( # type: ignore + GraphQLError, + GraphQLResult, + GraphQLSchema, + QueryParser, + ) + from graphql.language.ast import DocumentNode + + from sentry_sdk._types import Event, EventProcessor + + +class AriadneIntegration(Integration): + identifier = "ariadne" + + @staticmethod + def setup_once() -> None: + version = package_version("ariadne") + _check_minimum_version(AriadneIntegration, version) + + ignore_logger("ariadne") + + _patch_graphql() + + +def _patch_graphql() -> None: + old_parse_query = ariadne_graphql.parse_query + old_handle_errors = ariadne_graphql.handle_graphql_errors + old_handle_query_result = ariadne_graphql.handle_query_result + + @ensure_integration_enabled(AriadneIntegration, old_parse_query) + def _sentry_patched_parse_query( + context_value: "Optional[Any]", + query_parser: "Optional[QueryParser]", + data: "Any", + ) -> "DocumentNode": + event_processor = _make_request_event_processor(data) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + result = old_parse_query(context_value, query_parser, data) + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_errors) + def _sentry_patched_handle_graphql_errors( + errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + result = old_handle_errors(errors, *args, **kwargs) + + event_processor = _make_response_event_processor(result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) + def _sentry_patched_handle_query_result( + result: "Any", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + query_result = old_handle_query_result(result, *args, **kwargs) + + event_processor = _make_response_event_processor(query_result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return query_result + + ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore + ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore + ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore + + +def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": + """Add request data and api_target to events.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + if not isinstance(data, dict): + return event + + with capture_internal_exceptions(): + try: + content_length = int( + (data.get("headers") or {}).get("Content-Length", 0) + ) + except (TypeError, ValueError): + return event + + if should_send_default_pii() and request_body_within_bounds( + get_client(), content_length + ): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + request_info["data"] = data + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + return inner + + +def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": + """Add response data to the event's response context.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii() and response.get("errors"): + contexts = event.setdefault("contexts", {}) + contexts["response"] = { + "data": response, + } + + return event + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py new file mode 100644 index 0000000000..da03bafb8b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py @@ -0,0 +1,277 @@ +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Transaction, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +try: + import arq.worker + from arq.connections import ArqRedis + from arq.version import VERSION as ARQ_VERSION + from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker +except ImportError: + raise DidNotEnable("Arq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from arq.cron import CronJob + from arq.jobs import Job + from arq.typing import WorkerCoroutine + from arq.worker import Function + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) + + +class ArqIntegration(Integration): + identifier = "arq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + try: + if isinstance(ARQ_VERSION, str): + version = parse_version(ARQ_VERSION) + else: + version = ARQ_VERSION.version[:2] + + except (TypeError, ValueError): + version = None + + _check_minimum_version(ArqIntegration, version) + + patch_enqueue_job() + patch_run_job() + patch_create_worker() + + _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore + + ignore_logger("arq.worker") + + +def patch_enqueue_job() -> None: + old_enqueue_job = ArqRedis.enqueue_job + original_kwdefaults = old_enqueue_job.__kwdefaults__ + + async def _sentry_enqueue_job( + self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" + ) -> "Optional[Job]": + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_enqueue_job(self, function, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=function, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_ARQ, + "sentry.origin": ArqIntegration.origin, + }, + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults + ArqRedis.enqueue_job = _sentry_enqueue_job + + +def patch_run_job() -> None: + old_run_job = Worker.run_job + + async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_run_job(self, job_id, score) + + with sentry_sdk.isolation_scope() as scope: + scope._name = "arq" + scope.clear_breadcrumbs() + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name="unknown arq task", + attributes={ + "sentry.op": OP.QUEUE_TASK_ARQ, + "sentry.origin": ArqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job_id, + }, + parent_span=None, + ): + return await old_run_job(self, job_id, score) + + transaction = Transaction( + name="unknown arq task", + status="ok", + op=OP.QUEUE_TASK_ARQ, + source=TransactionSource.TASK, + origin=ArqIntegration.origin, + ) + + with sentry_sdk.start_transaction(transaction): + return await old_run_job(self, job_id, score) + + Worker.run_job = _sentry_run_job + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + + if scope.transaction is not None: + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + scope.transaction.set_status(SPANSTATUS.ABORTED) + return + + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": ArqIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + if scope.transaction is not None: + scope.transaction.name = ctx["job_name"] + event["transaction"] = ctx["job_name"] + + tags = event.setdefault("tags", {}) + tags["arq_task_id"] = ctx["job_id"] + tags["arq_task_retry"] = ctx["job_try"] > 1 + extra = event.setdefault("extra", {}) + extra["arq-job"] = { + "task": ctx["job_name"], + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": ctx["job_try"], + } + + return event + + return event_processor + + +def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": + async def _sentry_coroutine( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(ArqIntegration) + if integration is None: + return await coroutine(ctx, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + scope = sentry_sdk.get_current_scope() + span = scope.streamed_span + if span is not None: + span.name = name + + scope.set_transaction_name(name) + + sentry_sdk.get_isolation_scope().add_event_processor( + _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) + ) + + try: + result = await coroutine(ctx, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_coroutine + + +def patch_create_worker() -> None: + old_create_worker = arq.worker.create_worker + + @ensure_integration_enabled(ArqIntegration, old_create_worker) + def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": + settings_cls = args[0] if args else kwargs.get("settings_cls") + + if isinstance(settings_cls, dict): + if "functions" in settings_cls: + settings_cls["functions"] = [ + _get_arq_function(func) + for func in settings_cls.get("functions", []) + ] + if "cron_jobs" in settings_cls: + settings_cls["cron_jobs"] = [ + _get_arq_cron_job(cron_job) + for cron_job in settings_cls.get("cron_jobs", []) + ] + + if hasattr(settings_cls, "functions"): + settings_cls.functions = [ # type: ignore[union-attr] + _get_arq_function(func) + for func in settings_cls.functions # type: ignore[union-attr] + ] + if hasattr(settings_cls, "cron_jobs"): + settings_cls.cron_jobs = [ # type: ignore[union-attr] + _get_arq_cron_job(cron_job) + for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] + ] + + if "functions" in kwargs: + kwargs["functions"] = [ + _get_arq_function(func) for func in kwargs.get("functions", []) + ] + if "cron_jobs" in kwargs: + kwargs["cron_jobs"] = [ + _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) + ] + + return old_create_worker(*args, **kwargs) + + arq.worker.create_worker = _sentry_create_worker + + +def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": + arq_func = arq.worker.func(func) + arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) + + return arq_func + + +def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": + cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) + + return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py new file mode 100644 index 0000000000..8b1ff5e2a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py @@ -0,0 +1,543 @@ +""" +An ASGI middleware. + +Based on Tom Christie's `sentry-asgi `. +""" + +import inspect +import sys +from copy import deepcopy +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._asgi_common import ( + _get_headers, + _get_ip, + _get_path, + _get_request_attributes, + _get_request_data, + _get_url, + _RootPathInPath, +) +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + SegmentSource, + StreamedSpan, +) +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + Transaction, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + ContextVar, + _get_installed_modules, + capture_internal_exceptions, + event_from_exception, + logger, + qualname_from_function, + reraise, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, ContextManager, Dict, Optional, Tuple, Union + + from sentry_sdk._types import Attributes, Event, Hint + from sentry_sdk.tracing import Span + + +_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") + +_DEFAULT_TRANSACTION_NAME = "generic ASGI request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 +if sys.version_info >= (3, 14): + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": mechanism_type, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _looks_like_asgi3(app: "Any") -> bool: + """ + Try to figure out if an application object supports ASGI3. + + This is how uvicorn figures out the application version as well. + """ + if inspect.isclass(app): + return hasattr(app, "__await__") + elif inspect.isfunction(app): + return iscoroutinefunction(app) + else: + call = getattr(app, "__call__", None) # noqa + return iscoroutinefunction(call) + + +class SentryAsgiMiddleware: + __slots__ = ( + "app", + "__call__", + "transaction_style", + "mechanism_type", + "span_origin", + "http_methods_to_capture", + "root_path_in_path", + ) + + def __init__( + self, + app: "Any", + unsafe_context_data: bool = False, + transaction_style: str = "endpoint", + mechanism_type: str = "asgi", + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + asgi_version: "Optional[int]" = None, + root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, + ) -> None: + """ + Instrument an ASGI application with Sentry. Provides HTTP/websocket + data to sent events and basic handling for exceptions bubbling up + through the middleware. + + :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. + """ + if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The ASGI middleware for Sentry requires Python 3.7+ " + "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + + asgi_middleware_while_using_starlette_or_fastapi = ( + mechanism_type == "asgi" and "starlette" in _get_installed_modules() + ) + if asgi_middleware_while_using_starlette_or_fastapi: + logger.warning( + "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " + "Please remove 'SentryAsgiMiddleware' from your project. " + "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." + ) + + self.transaction_style = transaction_style + self.mechanism_type = mechanism_type + self.span_origin = span_origin + self.app = app + self.http_methods_to_capture = http_methods_to_capture + self.root_path_in_path = root_path_in_path + + if asgi_version is None: + if _looks_like_asgi3(app): + asgi_version = 3 + else: + asgi_version = 2 + + if asgi_version == 3: + self.__call__ = self._run_asgi3 + elif asgi_version == 2: + self.__call__ = self._run_asgi2 # type: ignore + + def _capture_lifespan_exception(self, exc: Exception) -> None: + """Capture exceptions raise in application lifespan handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _capture_request_exception(self, exc: Exception) -> None: + """Capture exceptions raised in incoming request handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _run_asgi2(self, scope: "Any") -> "Any": + async def inner(receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=2) + + return inner + + async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=3) + + async def _run_app( + self, scope: "Any", receive: "Any", send: "Any", asgi_version: int + ) -> "Any": + is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) + is_lifespan = scope["type"] == "lifespan" + if is_recursive_asgi_middleware or is_lifespan: + try: + if asgi_version == 2: + return await self.app(scope)(receive, send) + else: + return await self.app(scope, receive, send) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_lifespan_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_lifespan_exception(exc) + reraise(*exc_info) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _asgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as sentry_scope: + with track_session(sentry_scope, session_mode="request"): + sentry_scope.clear_breadcrumbs() + sentry_scope._name = "asgi" + processor = partial(self.event_processor, asgi_scope=scope) + sentry_scope.add_event_processor(processor) + + ty = scope["type"] + ( + transaction_name, + transaction_source, + ) = self._get_transaction_name_and_source( + self.transaction_style, + scope, + ) + + method = scope.get("method", "").upper() + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + if span_streaming: + segment: "Optional[StreamedSpan]" = None + attributes: "Attributes" = { + "sentry.span.source": getattr( + transaction_source, "value", transaction_source + ), + "sentry.origin": self.span_origin, + "network.protocol.name": ty, + } + + if scope.get("client") and should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) + + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + sentry_sdk.traces.continue_trace(_get_headers(scope)) + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = f"{ty}.server" + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + else: + sentry_sdk.traces.new_trace() + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = OP.HTTP_SERVER + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + + span_ctx = segment or nullcontext() + + else: + transaction = None + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + transaction = continue_trace( + _get_headers(scope), + op="{}.server".format(ty), + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + else: + transaction = Transaction( + op=OP.HTTP_SERVER, + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + + if transaction: + transaction.set_tag("asgi.type", ty) + + span_ctx = ( + sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"asgi_scope": scope}, + ) + if transaction is not None + else nullcontext() + ) + + with span_ctx as span: + if isinstance(span, StreamedSpan): + for attribute, value in _get_request_attributes( + scope, + root_path_in_path=self.root_path_in_path, + ).items(): + span.set_attribute(attribute, value) + + try: + + async def _sentry_wrapped_send( + event: "Dict[str, Any]", + ) -> "Any": + if span is not None: + is_http_response = ( + event.get("type") == "http.response.start" + and "status" in event + ) + if is_http_response: + if isinstance(span, StreamedSpan): + span.status = ( + "error" + if event["status"] >= 400 + else "ok" + ) + span.set_attribute( + "http.response.status_code", + event["status"], + ) + else: + span.set_http_status(event["status"]) + + return await send(event) + + if asgi_version == 2: + return await self.app(scope)( + receive, _sentry_wrapped_send + ) + else: + return await self.app( + scope, receive, _sentry_wrapped_send + ) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_request_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_request_exception(exc) + reraise(*exc_info) + + finally: + if isinstance(span, StreamedSpan): + already_set = ( + span is not None + and span.name != _DEFAULT_TRANSACTION_NAME + and span.get_attributes().get("sentry.span.source") + in [ + SegmentSource.COMPONENT.value, + SegmentSource.ROUTE.value, + SegmentSource.CUSTOM.value, + ] + ) + with capture_internal_exceptions(): + if not already_set: + name, source = ( + self._get_segment_name_and_source( + self.transaction_style, scope + ) + ) + span.name = name + span.set_attribute("sentry.span.source", source) + finally: + _asgi_middleware_applied.set(False) + + def event_processor( + self, event: "Event", hint: "Hint", asgi_scope: "Any" + ) -> "Optional[Event]": + request_data = event.get("request", {}) + request_data.update( + _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) + ) + event["request"] = deepcopy(request_data) + + # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) + transaction = event.get("transaction") + transaction_source = (event.get("transaction_info") or {}).get("source") + already_set = ( + transaction is not None + and transaction != _DEFAULT_TRANSACTION_NAME + and transaction_source + in [ + TransactionSource.COMPONENT, + TransactionSource.ROUTE, + TransactionSource.CUSTOM, + ] + ) + if not already_set: + name, source = self._get_transaction_name_and_source( + self.transaction_style, asgi_scope + ) + event["transaction"] = name + event["transaction_info"] = {"source": source} + + return event + + # Helper functions. + # + # Note: Those functions are not public API. If you want to mutate request + # data to your liking it's recommended to use the `before_send` callback + # for that. + + def _get_transaction_name_and_source( + self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SOURCE_FOR_STYLE[transaction_style] + ty = asgi_scope.get("type") + + if transaction_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = transaction_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + elif transaction_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + return name, source + + return name, source + + def _get_segment_name_and_source( + self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value + ty = asgi_scope.get("type") + + if segment_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = qualname_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + elif segment_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = SegmentSource.ROUTE.value + return name, source + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py new file mode 100644 index 0000000000..4b3f6d330e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py @@ -0,0 +1,280 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import AsyncHttpTransport +from sentry_sdk.utils import ( + event_from_exception, + is_internal_task, + logger, + reraise, +) + +try: + import asyncio + from asyncio.tasks import Task +except ImportError: + raise DidNotEnable("asyncio not available") + +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from collections.abc import Coroutine + from typing import Any, Callable, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T", bound=Callable[..., Any]) + + +def get_name(coro: "Any") -> str: + return ( + getattr(coro, "__qualname__", None) + or getattr(coro, "__name__", None) + or "coroutine without __name__" + ) + + +def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": + # Only __name__ and __qualname__ are copied from function to coroutine in CPython + return functools.partial( + functools.update_wrapper, + wrapped=wrapped, # type: ignore + assigned=("__name__", "__qualname__"), + updated=(), + ) + + +def patch_loop_close() -> None: + """Patch loop.close to flush pending events before shutdown.""" + # Atexit shutdown hook happens after the event loop is closed. + # Therefore, it is necessary to patch the loop.close method to ensure + # that pending events are flushed before the interpreter shuts down. + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No running loop → cannot patch now + return + + if getattr(loop, "_sentry_flush_patched", False): + return + + async def _flush() -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + try: + if not isinstance(client.transport, AsyncHttpTransport): + return + + await client.close_async() + except Exception: + logger.warning("Sentry flush failed during loop shutdown", exc_info=True) + + orig_close = loop.close + + def _patched_close() -> None: + try: + loop.run_until_complete(_flush()) + except Exception: + logger.debug( + "Could not flush Sentry events during loop close", exc_info=True + ) + finally: + orig_close() + + loop.close = _patched_close # type: ignore + loop._sentry_flush_patched = True # type: ignore + + +def _create_task_with_factory( + orig_task_factory: "Any", + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", +) -> "asyncio.Task[Any]": + task = None + + # Trying to use user set task factory (if there is one) + if orig_task_factory: + task = orig_task_factory(loop, coro, **kwargs) + + if task is None: + # The default task factory in `asyncio` does not have its own function + # but is just a couple of lines in `asyncio.base_events.create_task()` + # Those lines are copied here. + + # WARNING: + # If the default behavior of the task creation in asyncio changes, + # this will break! + task = Task(coro, loop=loop, **kwargs) + if task._source_traceback: # type: ignore + del task._source_traceback[-1] # type: ignore + + return task + + +def patch_asyncio() -> None: + orig_task_factory = None + try: + loop = asyncio.get_running_loop() + orig_task_factory = loop.get_task_factory() + + # Check if already patched + if getattr(orig_task_factory, "_is_sentry_task_factory", False): + return + + def _sentry_task_factory( + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", + ) -> "asyncio.Future[Any]": + # Check if this is an internal Sentry task + if is_internal_task(): + return _create_task_with_factory( + orig_task_factory, loop, coro, **kwargs + ) + + @_wrap_coroutine(coro) + async def _task_with_sentry_span_creation() -> "Any": + result = None + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncioIntegration) + task_spans = integration.task_spans if integration else False + + span_ctx: "Optional[Union[StreamedSpan, Span]]" = None + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope(): + if task_spans: + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=get_name(coro), + attributes={ + "sentry.op": OP.FUNCTION, + "sentry.origin": AsyncioIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.FUNCTION, + name=get_name(coro), + origin=AsyncioIntegration.origin, + ) + + with span_ctx if span_ctx else nullcontext(): + try: + result = await coro + except StopAsyncIteration as e: + raise e from None + except Exception: + reraise(*_capture_exception()) + + return result + + task = _create_task_with_factory( + orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs + ) + + # Set the task name to include the original coroutine's name + try: + task.set_name(f"{get_name(coro)} (Sentry-wrapped)") + except AttributeError: + # set_name might not be available in all Python versions + pass + + return task + + _sentry_task_factory._is_sentry_task_factory = True # type: ignore + loop.set_task_factory(_sentry_task_factory) # type: ignore + + except RuntimeError: + # When there is no running loop, we have nothing to patch. + logger.warning( + "There is no running asyncio loop so there is nothing Sentry can patch. " + "Please make sure you call sentry_sdk.init() within a running " + "asyncio loop for the AsyncioIntegration to work. " + "See https://docs.sentry.io/platforms/python/integrations/asyncio/" + ) + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + + integration = client.get_integration(AsyncioIntegration) + if integration is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "asyncio", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class AsyncioIntegration(Integration): + identifier = "asyncio" + origin = f"auto.function.{identifier}" + + def __init__(self, task_spans: bool = True) -> None: + self.task_spans = task_spans + + @staticmethod + def setup_once() -> None: + patch_asyncio() + patch_loop_close() + + +def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: + """ + Enable AsyncioIntegration with the provided options. + + This is useful in scenarios where Sentry needs to be initialized before + an event loop is set up, but you still want to instrument asyncio once there + is an event loop. In that case, you can sentry_sdk.init() early on without + the AsyncioIntegration and then, once the event loop has been set up, + execute: + + ```python + from sentry_sdk.integrations.asyncio import enable_asyncio_integration + + async def async_entrypoint(): + enable_asyncio_integration() + ``` + + Any arguments provided will be passed to AsyncioIntegration() as is. + + If AsyncioIntegration has already patched the current event loop, this + function won't have any effect. + + If AsyncioIntegration was provided in + sentry_sdk.init(disabled_integrations=[...]), this function will ignore that + and the integration will be enabled. + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # This function purposefully bypasses the integration machinery in + # integrations/__init__.py. _installed_integrations/_processed_integrations + # is used to prevent double patching the same module, but in the case of + # the AsyncioIntegration, we don't monkeypatch the standard library directly, + # we patch the currently running event loop, and we keep the record of doing + # that on the loop itself. + logger.debug("Setting up integration asyncio") + + integration = AsyncioIntegration(*args, **kwargs) + integration.setup_once() + + if "asyncio" not in client.integrations: + client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py new file mode 100644 index 0000000000..186176d268 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import contextlib +import re +from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import asyncpg # type: ignore + from asyncpg.cursor import ( # type: ignore + BaseCursor, + Cursor, + CursorIterator, + ) + +except ImportError: + raise DidNotEnable("asyncpg not installed.") + + +class AsyncPGIntegration(Integration): + identifier = "asyncpg" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AsyncPGIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + # asyncpg.__version__ is a string containing the semantic version in the form of ".." + asyncpg_version = parse_version(asyncpg.__version__) + _check_minimum_version(AsyncPGIntegration, asyncpg_version) + + asyncpg.Connection.execute = _wrap_execute( + asyncpg.Connection.execute, + ) + + asyncpg.Connection._execute = _wrap_connection_method( + asyncpg.Connection._execute + ) + asyncpg.Connection._executemany = _wrap_connection_method( + asyncpg.Connection._executemany, executemany=True + ) + asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) + + BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) + BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) + + asyncpg.connect_utils._connect_addr = _wrap_connect_addr( + asyncpg.connect_utils._connect_addr + ) + + +T = TypeVar("T") + + +def _normalize_query(query: str) -> str: + return re.sub(r"\s+", " ", query).strip() + + +def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + # Avoid recording calls to _execute twice. + # Calls to Connection.execute with args also call + # Connection._execute, which is recorded separately + # args[0] = the connection object, args[1] is the query + if len(args) > 2: + return await f(*args, **kwargs) + + query = _normalize_query(args[1]) + with record_sql_queries( + cursor=None, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + span_origin=AsyncPGIntegration.origin, + ) as span: + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +SubCursor = TypeVar("SubCursor", bound=BaseCursor) + + +@contextlib.contextmanager +def _record( + cursor: "SubCursor | None", + query: str, + params_list: "tuple[Any, ...] | None", + *, + executemany: bool = False, +) -> "Iterator[Union[Span, StreamedSpan]]": + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncPGIntegration) + if integration is not None and not integration._record_params: + params_list = None + + param_style = "pyformat" if params_list else None + + query = _normalize_query(query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=params_list, + paramstyle=param_style, + executemany=executemany, + record_cursor_repr=cursor is not None, + span_origin=AsyncPGIntegration.origin, + ) as span: + yield span + + +def _wrap_connection_method( + f: "Callable[..., Awaitable[T]]", *, executemany: bool = False +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + query = args[1] + params_list = args[2] if len(args) > 2 else None + with _record(None, query, params_list, executemany=executemany) as span: + _set_db_data(span, args[0]) + + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_cursor_method( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + if type(cursor) is CursorIterator: + span_op_override_value = OP.DB_CURSOR_ITERATOR + elif type(cursor) is Cursor: + span_op_override_value = OP.DB_CURSOR_FETCH + else: + span_op_override_value = None + + query = _normalize_query(cursor._query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + record_cursor_repr=True, + span_origin=AsyncPGIntegration.origin, + span_op_override_value=span_op_override_value, + ) as span: + _set_db_data(span, cursor._connection) + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_connect_addr( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + user = kwargs["params"].user + database = kwargs["params"].database + addr = kwargs.get("addr") + + if has_span_streaming_enabled(client.options): + span_attributes = { + "sentry.op": OP.DB, + "sentry.origin": AsyncPGIntegration.origin, + SPANDATA.DB_SYSTEM_NAME: "postgresql", + SPANDATA.DB_USER: user, + SPANDATA.DB_NAMESPACE: database, + SPANDATA.DB_DRIVER_NAME: "asyncpg", + } + if addr: + try: + span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] + span_attributes[SPANDATA.SERVER_PORT] = addr[1] + except IndexError: + pass + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span_attributes + ) + res = await f(*args, **kwargs) + + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AsyncPGIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + span.set_data(SPANDATA.DB_NAME, database) + span.set_data(SPANDATA.DB_USER, user) + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span._data + ) + res = await f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + addr = conn._addr + database = conn._params.database + user = conn._params.user + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") + if addr: + try: + span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_attribute(SPANDATA.DB_NAMESPACE, database) + + if user: + span.set_attribute(SPANDATA.DB_USER, user) + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_data(SPANDATA.DB_NAME, database) + + if user: + span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py new file mode 100644 index 0000000000..b573e76f09 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py @@ -0,0 +1,51 @@ +import atexit +import os +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Optional + + +def default_callback(pending: int, timeout: int) -> None: + """This is the default shutdown callback that is set on the options. + It prints out a message to stderr that informs the user that some events + are still pending and the process is waiting for them to flush out. + """ + + def echo(msg: str) -> None: + sys.stderr.write(msg + "\n") + + echo("Sentry is attempting to send %i pending events" % pending) + echo("Waiting up to %s seconds" % timeout) + echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) + sys.stderr.flush() + + +class AtexitIntegration(Integration): + identifier = "atexit" + + def __init__(self, callback: "Optional[Any]" = None) -> None: + if callback is None: + callback = default_callback + self.callback = callback + + @staticmethod + def setup_once() -> None: + @atexit.register + def _shutdown() -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(AtexitIntegration) + + if integration is None: + return + + logger.debug("atexit: got shutdown signal") + logger.debug("atexit: shutting down client") + sentry_sdk.get_isolation_scope().end_session() + + client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py new file mode 100644 index 0000000000..c7fe77714a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py @@ -0,0 +1,542 @@ +import functools +import json +import re +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING +from urllib.parse import urlencode + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.cloud_resource_context import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + +# Constants +TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + + +def _wrap_init_error(init_error: "F") -> "F": + @ensure_integration_enabled(AwsLambdaIntegration, init_error) + def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + with capture_internal_exceptions(): + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + exc_info = sys.exc_info() + if exc_info and all(exc_info): + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + + else: + # Fall back to AWS lambdas JSON representation of the error + error_info = args[1] + if isinstance(error_info, str): + error_info = json.loads(error_info) + sentry_event = _event_from_error_json(error_info) + sentry_sdk.capture_event(sentry_event) + + return init_error(*args, **kwargs) + + return sentry_init_error # type: ignore + + +def _wrap_handler(handler: "F") -> "F": + @functools.wraps(handler) + def sentry_handler( + aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, + # `event` here is *likely* a dictionary, but also might be a number of + # other types (str, int, float, None). + # + # In some cases, it is a list (if the user is batch-invoking their + # function, for example), in which case we'll use the first entry as a + # representative from which to try pulling request data. (Presumably it + # will be the same for all events in the list, since they're all hitting + # the lambda in the same request.) + + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + + if integration is None: + return handler(aws_event, aws_context, *args, **kwargs) + + if isinstance(aws_event, list) and len(aws_event) >= 1: + request_data = aws_event[0] + batch_size = len(aws_event) + else: + request_data = aws_event + batch_size = 1 + + if not isinstance(request_data, dict): + # If we're not dealing with a dictionary, we won't be able to get + # headers, path, http method, etc in any case, so it's fine that + # this is empty + request_data = {} + + configured_time = aws_context.get_remaining_time_in_millis() + aws_region = aws_context.invoked_function_arn.split(":")[3] + + with sentry_sdk.isolation_scope() as scope: + timeout_thread = None + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + request_data, aws_context, configured_time + ) + ) + scope.set_tag("aws_region", aws_region) + if batch_size > 1: + scope.set_tag("batch_request", True) + scope.set_tag("batch_size", batch_size) + + # Starting the Timeout thread only if the configured time is greater than Timeout warning + # buffer and timeout_warning parameter is set True. + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = ( + configured_time - TIMEOUT_WARNING_BUFFER + ) / MILLIS_TO_SECONDS + + timeout_thread = TimeoutThread( + waiting_time, + configured_time / MILLIS_TO_SECONDS, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = request_data.get("headers", {}) + # Some AWS Services (ie. EventBridge) set headers as a list + # or None, so we must ensure it is a dict + if not isinstance(headers, dict): + headers = {} + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + additional_attributes: "dict[str, Any]" = {} + if "httpMethod" in request_data: + additional_attributes["http.request.method"] = request_data[ + "httpMethod" + ] + + if should_send_default_pii() and "queryStringParameters" in request_data: + qs = request_data["queryStringParameters"] + if qs: + additional_attributes["url.query"] = urlencode(qs) + + sampling_context = { + "aws_event": aws_event, + "aws_context": aws_context, + } + + function_name = aws_context.function_name + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_AWS, + "sentry.origin": AwsLambdaIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.region": aws_region, + "cloud.resource_id": aws_context.invoked_function_arn, + "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, + "cloud.provider": CLOUD_PROVIDER.AWS, + "faas.name": function_name, + "faas.invocation_id": aws_context.aws_request_id, + "faas.version": aws_context.function_version, + "aws.lambda.invoked_arn": aws_context.invoked_function_arn, + "aws.log.group.names": [aws_context.log_group_name], + "aws.log.stream.names": [aws_context.log_stream_name], + "messaging.batch.message_count": batch_size, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_AWS, + name=function_name, + source=TransactionSource.COMPONENT, + origin=AwsLambdaIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return handler(aws_event, aws_context, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + + return sentry_handler # type: ignore + + +def _drain_queue() -> None: + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + if integration is not None: + # Flush out the event queue before AWS kills the + # process. + client.flush() + + +class AwsLambdaIntegration(Integration): + identifier = "aws_lambda" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + lambda_bootstrap = get_lambda_bootstrap() + if not lambda_bootstrap: + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find bootstrap module)" + ) + return + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find handle_event_request)" + ) + return + + pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 + + if pre_37: + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_event_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + old_handle_http_request = lambda_bootstrap.handle_http_request + + def sentry_handle_http_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_http_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_http_request = sentry_handle_http_request + + # Patch to_json to drain the queue. This should work even when the + # SDK is initialized inside of the handler + + old_to_json = lambda_bootstrap.to_json + + def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return old_to_json(*args, **kwargs) + + lambda_bootstrap.to_json = sentry_to_json + else: + lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( + lambda_bootstrap.LambdaRuntimeClient.post_init_error + ) + + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f: "F") -> "F": + def inner(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore + + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + ) + + +def get_lambda_bootstrap() -> "Optional[Any]": + # Python 3.7: If the bootstrap module is *already imported*, it is the + # one we actually want to use (no idea what's in __main__) + # + # Python 3.8: bootstrap is also importable, but will be the same file + # as __main__ imported under a different name: + # + # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ + # sys.modules['__main__'] is not sys.modules['bootstrap'] + # + # Python 3.9: bootstrap is in __main__.awslambdaricmain + # + # On container builds using the `aws-lambda-python-runtime-interface-client` + # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap + # + # Such a setup would then make all monkeypatches useless. + if "bootstrap" in sys.modules: + return sys.modules["bootstrap"] + elif "__main__" in sys.modules: + module = sys.modules["__main__"] + # python3.9 runtime + if hasattr(module, "awslambdaricmain") and hasattr( + module.awslambdaricmain, "bootstrap" + ): + return module.awslambdaricmain.bootstrap + elif hasattr(module, "bootstrap"): + # awslambdaric python module in container builds + return module.bootstrap + + # python3.8 runtime + return module + else: + return None + + +def _make_request_event_processor( + aws_event: "Any", aws_context: "Any", configured_timeout: "Any" +) -> "EventProcessor": + start_time = datetime.now(timezone.utc) + + def event_processor( + sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time + ) -> "Optional[Event]": + remaining_time_in_milis = aws_context.get_remaining_time_in_millis() + exec_duration = configured_timeout - remaining_time_in_milis + + extra = sentry_event.setdefault("extra", {}) + extra["lambda"] = { + "function_name": aws_context.function_name, + "function_version": aws_context.function_version, + "invoked_function_arn": aws_context.invoked_function_arn, + "aws_request_id": aws_context.aws_request_id, + "execution_duration_in_millis": exec_duration, + "remaining_time_in_millis": remaining_time_in_milis, + } + + extra["cloudwatch logs"] = { + "url": _get_cloudwatch_logs_url(aws_context, start_time), + "log_group": aws_context.log_group_name, + "log_stream": aws_context.log_stream_name, + } + + request = sentry_event.get("request", {}) + + if "httpMethod" in aws_event: + request["method"] = aws_event["httpMethod"] + + request["url"] = _get_url(aws_event, aws_context) + + if "queryStringParameters" in aws_event: + request["query_string"] = aws_event["queryStringParameters"] + + if "headers" in aws_event: + request["headers"] = _filter_headers(aws_event["headers"]) + + if should_send_default_pii(): + user_info = sentry_event.setdefault("user", {}) + + identity = aws_event.get("identity") + if identity is None: + identity = {} + + id = identity.get("userArn") + if id is not None: + user_info.setdefault("id", id) + + ip = identity.get("sourceIp") + if ip is not None: + user_info.setdefault("ip_address", ip) + + if "body" in aws_event: + request["data"] = aws_event.get("body", "") + else: + if aws_event.get("body", None): + # Unfortunately couldn't find a way to get structured body from AWS + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + sentry_event["request"] = deepcopy(request) + + return sentry_event + + return event_processor + + +def _get_url(aws_event: "Any", aws_context: "Any") -> str: + path = aws_event.get("path", None) + + headers = aws_event.get("headers") + if headers is None: + headers = {} + + host = headers.get("Host", None) + proto = headers.get("X-Forwarded-Proto", None) + if proto and host and path: + return "{}://{}{}".format(proto, host, path) + return "awslambda:///{}".format(aws_context.function_name) + + +def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: + """ + Generates a CloudWatchLogs console URL based on the context object + + Arguments: + aws_context {Any} -- context from lambda handler + + Returns: + str -- AWS Console URL to logs. + """ + formatstring = "%Y-%m-%dT%H:%M:%SZ" + region = environ.get("AWS_REGION", "") + + url = ( + "https://console.{domain}/cloudwatch/home?region={region}" + "#logEventViewer:group={log_group};stream={log_stream}" + ";start={start_time};end={end_time}" + ).format( + domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", + region=region, + log_group=aws_context.log_group_name, + log_stream=aws_context.log_stream_name, + start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), + end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( + formatstring + ), + ) + + return url + + +def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": + frames = [] + for frame in formatted_tb: + match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) + if match: + file_name, line_number, func_name = match.groups() + line_number = int(line_number) + frames.append( + { + "filename": file_name, + "function": func_name, + "lineno": line_number, + "vars": None, + "pre_context": None, + "context_line": None, + "post_context": None, + } + ) + return frames + + +def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": + """ + Converts the error JSON from AWS Lambda into a Sentry error event. + This is not a full fletched event, but better than nothing. + + This is an example of where AWS creates the error JSON: + https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 + """ + event: "Event" = { + "level": "error", + "exception": { + "values": [ + { + "type": error_json.get("errorType"), + "value": error_json.get("errorMessage"), + "stacktrace": { + "frames": _parse_formatted_traceback( + error_json.get("stackTrace", []) + ), + }, + "mechanism": { + "type": "aws_lambda", + "handled": False, + }, + } + ], + }, + } + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py new file mode 100644 index 0000000000..31f45f73de --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py @@ -0,0 +1,164 @@ +import sys +import types +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) + + +WRAPPED_FUNC = "_wrapped_{}_" +INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py +USED_FUNC = "_sentry_used_" + + +class BeamIntegration(Integration): + identifier = "beam" + + @staticmethod + def setup_once() -> None: + from apache_beam.transforms.core import DoFn, ParDo # type: ignore + + ignore_logger("root") + ignore_logger("bundle_processor.create") + + function_patches = ["process", "start_bundle", "finish_bundle", "setup"] + for func_name in function_patches: + setattr( + DoFn, + INSPECT_FUNC.format(func_name), + _wrap_inspect_call(DoFn, func_name), + ) + + old_init = ParDo.__init__ + + def sentry_init_pardo( + self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Do not monkey patch init twice + if not getattr(self, "_sentry_is_patched", False): + for func_name in function_patches: + if not hasattr(fn, func_name): + continue + wrapped_func = WRAPPED_FUNC.format(func_name) + + # Check to see if inspect is set and process is not + # to avoid monkey patching process twice. + # Check to see if function is part of object for + # backwards compatibility. + process_func = getattr(fn, func_name) + inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) + if not getattr(inspect_func, USED_FUNC, False) and not getattr( + process_func, USED_FUNC, False + ): + setattr(fn, wrapped_func, process_func) + setattr(fn, func_name, _wrap_task_call(process_func)) + + self._sentry_is_patched = True + old_init(self, fn, *args, **kwargs) + + ParDo.__init__ = sentry_init_pardo + + +def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": + if not hasattr(cls, func_name): + return None + + def _inspect(self: "Any") -> "Any": + """ + Inspect function overrides the way Beam gets argspec. + """ + wrapped_func = WRAPPED_FUNC.format(func_name) + if hasattr(self, wrapped_func): + process_func = getattr(self, wrapped_func) + else: + process_func = getattr(self, func_name) + setattr(self, func_name, _wrap_task_call(process_func)) + setattr(self, wrapped_func, process_func) + + # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults + # (which uses Signatures internally) should be used instead. + try: + from apache_beam.transforms.core import get_function_args_defaults + + return get_function_args_defaults(process_func) + except ImportError: + from apache_beam.typehints.decorators import getfullargspec # type: ignore + + return getfullargspec(process_func) + + setattr(_inspect, USED_FUNC, True) + return _inspect + + +def _wrap_task_call(func: "F") -> "F": + """ + Wrap task call with a try catch to get exceptions. + """ + + @wraps(func) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + try: + gen = func(*args, **kwargs) + except Exception: + raise_exception() + + if not isinstance(gen, types.GeneratorType): + return gen + return _wrap_generator_call(gen) + + setattr(_inner, USED_FUNC, True) + return _inner # type: ignore + + +@ensure_integration_enabled(BeamIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + """ + Send Beam exception to Sentry. + """ + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "beam", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def raise_exception() -> None: + """ + Raise an exception. + """ + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) + reraise(*exc_info) + + +def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": + """ + Wrap the generator to handle any failures. + """ + while True: + try: + yield next(gen) + except StopIteration: + break + except Exception: + raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py new file mode 100644 index 0000000000..a7fdd99b21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py @@ -0,0 +1,187 @@ +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_url, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Type, Union + + from botocore.model import ServiceId + +try: + from botocore import __version__ as BOTOCORE_VERSION + from botocore.awsrequest import AWSRequest + from botocore.client import BaseClient + from botocore.response import StreamingBody +except ImportError: + raise DidNotEnable("botocore is not installed") + + +class Boto3Integration(Integration): + identifier = "boto3" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTOCORE_VERSION) + _check_minimum_version(Boto3Integration, version, "botocore") + + orig_init = BaseClient.__init__ + + def sentry_patched_init( + self: "BaseClient", *args: "Any", **kwargs: "Any" + ) -> None: + orig_init(self, *args, **kwargs) + meta = self.meta + service_id = meta.service_model.service_id + meta.events.register( + "request-created", + partial(_sentry_request_created, service_id=service_id), + ) + meta.events.register("after-call", _sentry_after_call) + meta.events.register("after-call-error", _sentry_after_call_error) + + BaseClient.__init__ = sentry_patched_init # type: ignore + + +def _sentry_request_created( + service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" +) -> None: + description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) + + client = sentry_sdk.get_client() + if client.get_integration(Boto3Integration) is None: + return + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Boto3Integration.origin, + SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", + }, + ) + if request.url is not None and should_send_default_pii(): + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if request.method is not None: + span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=description, + origin=Boto3Integration.origin, + ) + + if request.url is not None: + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_data("aws.request.url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + span.set_tag("aws.service_id", service_id.hyphenize()) + span.set_tag("aws.operation_name", operation_name) + if request.method is not None: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + + # We do it in order for subsequent http calls/retries be + # attached to this span. + span.__enter__() + + # request.context is an open-ended data-structure + # where we can add anything useful in request life cycle. + request.context["_sentrysdk_span"] = span + + +def _sentry_after_call( + context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(None, None, None) + + body = parsed.get("Body") + if not isinstance(body, StreamingBody): + return + + streaming_span: "Union[Span, StreamedSpan]" + if isinstance(span, StreamedSpan): + streaming_span = sentry_sdk.traces.start_span( + name=span.name, + parent_span=span, + attributes={ + "sentry.op": OP.HTTP_CLIENT_STREAM, + "sentry.origin": Boto3Integration.origin, + }, + ) + else: + streaming_span = span.start_child( + op=OP.HTTP_CLIENT_STREAM, + name=span.description, + origin=Boto3Integration.origin, + ) + + orig_read = body.read + orig_close = body.close + + def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: + try: + ret = orig_read(*args, **kwargs) + if ret: + return ret + + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + return ret + except Exception: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + raise + + body.read = sentry_streaming_body_read # type: ignore + + def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + orig_close(*args, **kwargs) + + body.close = sentry_streaming_body_close # type: ignore + + +def _sentry_after_call_error( + context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py new file mode 100644 index 0000000000..50f6ca2e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py @@ -0,0 +1,239 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from collections.abc import Set + from typing import Any, Callable, Dict, Optional + + from bottle import FileUpload, FormsDict, LocalRequest # type: ignore + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + +try: + from bottle import ( + Bottle, + HTTPResponse, + Route, + ) + from bottle import ( + __version__ as BOTTLE_VERSION, + ) + from bottle import ( + request as bottle_request, + ) +except ImportError: + raise DidNotEnable("Bottle not installed") + + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class BottleIntegration(Integration): + identifier = "bottle" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTTLE_VERSION) + _check_minimum_version(BottleIntegration, version) + + old_app = Bottle.__call__ + + @ensure_integration_enabled(BottleIntegration, old_app) + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=BottleIntegration.origin, + ) + + return middleware(environ, start_response) + + Bottle.__call__ = sentry_patched_wsgi_app + + old_handle = Bottle._handle + + @functools.wraps(old_handle) + def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return old_handle(self, environ) + + scope = sentry_sdk.get_isolation_scope() + scope._name = "bottle" + scope.add_event_processor( + _make_request_event_processor(self, bottle_request, integration) + ) + res = old_handle(self, environ) + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + _set_segment_name_and_source( + transaction_style=integration.transaction_style + ) + + return res + + Bottle._handle = _patched_handle + + old_make_callback = Route._make_callback + + @functools.wraps(old_make_callback) + def patched_make_callback( + self: "Route", *args: object, **kwargs: object + ) -> "Any": + prepared_callback = old_make_callback(self, *args, **kwargs) + + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return prepared_callback + + def wrapped_callback(*args: object, **kwargs: object) -> "Any": + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) + + return res + + return wrapped_callback + + Route._make_callback = patched_make_callback + + +class BottleRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[str, str]": + return self.request.cookies + + def raw_data(self) -> bytes: + return self.request.body.read() + + def form(self) -> "FormsDict": + if self.is_json(): + return None + return self.request.forms.decode() + + def files(self) -> "Optional[Dict[str, str]]": + if self.is_json(): + return None + + return self.request.files + + def size_of_file(self, file: "FileUpload") -> int: + return file.content_length + + +def _set_segment_name_and_source(transaction_style: str) -> None: + try: + if transaction_style == "url": + name = bottle_request.route.rule or "bottle request" + else: + name = ( + bottle_request.route.name + or transaction_from_function(bottle_request.route.callback) + or "bottle request" + ) + + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], + ) + except RuntimeError: + pass + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "url": + try: + name = request.route.rule or "" + except RuntimeError: + pass + + elif transaction_style == "endpoint": + try: + name = ( + request.route.name + or transaction_from_function(request.route.callback) + or "" + ) + except RuntimeError: + pass + + event["transaction"] = name + event["transaction_info"] = { + "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + } + + +def _make_request_event_processor( + app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, request) + + with capture_internal_exceptions(): + BottleRequestExtractor(request).extract_into_event(event) + + return event + + return event_processor + + +def _capture_exception(exception: BaseException, handled: bool) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "bottle", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py new file mode 100644 index 0000000000..532b13539b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py @@ -0,0 +1,612 @@ +import sys +from collections.abc import Mapping +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import isolation_scope +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.celery.beat import ( + _patch_beat_apply_entry, + _patch_redbeat_apply_async, + _setup_celery_beat_signals, +) +from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource +from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, List, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import VERSION as CELERY_VERSION # type: ignore + from celery.app.task import Task # type: ignore + from celery.app.trace import task_has_custom + from celery.exceptions import ( # type: ignore + Ignore, + Reject, + Retry, + SoftTimeLimitExceeded, + ) + from kombu import Producer # type: ignore +except ImportError: + raise DidNotEnable("Celery not installed") + + +CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) + + +class CeleryIntegration(Integration): + identifier = "celery" + origin = f"auto.queue.{identifier}" + + def __init__( + self, + propagate_traces: bool = True, + monitor_beat_tasks: bool = False, + exclude_beat_tasks: "Optional[List[str]]" = None, + ) -> None: + self.propagate_traces = propagate_traces + self.monitor_beat_tasks = monitor_beat_tasks + self.exclude_beat_tasks = exclude_beat_tasks + + _patch_beat_apply_entry() + _patch_redbeat_apply_async() + _setup_celery_beat_signals(monitor_beat_tasks) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(CeleryIntegration, CELERY_VERSION) + + _patch_build_tracer() + _patch_task_apply_async() + _patch_celery_send_task() + _patch_worker_exit() + _patch_producer_publish() + + # This logger logs every status of every task that ran on the worker. + # Meaning that every task's breadcrumbs are full of stuff like "Task + # raised unexpected ". + ignore_logger("celery.worker.job") + ignore_logger("celery.app.trace") + + # This is stdout/err redirected to a logger, can't deal with this + # (need event_level=logging.WARN to reproduce) + ignore_logger("celery.redirected") + + +def _set_status(status: str) -> None: + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + + if span_streaming and scope.streamed_span is not None: + scope.streamed_span.status = "ok" if status == "ok" else "error" + elif not span_streaming and scope.span is not None: + scope.span.set_status(status) + + +def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return + + if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): + # ??? Doesn't map to anything + _set_status("aborted") + return + + _set_status("internal_error") + + if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): + return + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "celery", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + task: "Any", + uuid: "Any", + args: "Any", + kwargs: "Any", + request: "Optional[Any]" = None, +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["celery_task_id"] = uuid + extra = event.setdefault("extra", {}) + extra["celery-job"] = { + "task_name": task.name, + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + } + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): + event["fingerprint"] = [ + "celery", + "SoftTimeLimitExceeded", + getattr(task, "name", task), + ] + + return event + + return event_processor + + +def _update_celery_task_headers( + original_headers: "dict[str, Any]", + span: "Optional[Union[StreamedSpan, Span]]", + monitor_beat_tasks: bool, +) -> "dict[str, Any]": + """ + Updates the headers of the Celery task with the tracing information + and eventually Sentry Crons monitoring information for beat tasks. + """ + updated_headers = original_headers.copy() + with capture_internal_exceptions(): + # if span is None (when the task was started by Celery Beat) + # this will return the trace headers from the scope. + headers = dict( + sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) + ) + + if monitor_beat_tasks: + headers.update( + { + "sentry-monitor-start-timestamp-s": "%.9f" + % _now_seconds_since_epoch(), + } + ) + + # Add the time the task was enqueued to the headers + # This is used in the consumer to calculate the latency + updated_headers.update( + {"sentry-task-enqueued-time": _now_seconds_since_epoch()} + ) + + if headers: + existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) + sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) + + combined_baggage = sentry_baggage or existing_baggage + if sentry_baggage and existing_baggage: + # Merge incoming and sentry baggage, where the sentry trace information + # in the incoming baggage takes precedence and the third-party items + # are concatenated. + incoming = Baggage.from_incoming_header(existing_baggage) + combined = Baggage.from_incoming_header(sentry_baggage) + combined.sentry_items.update(incoming.sentry_items) + combined.third_party_items = ",".join( + [ + x + for x in [ + combined.third_party_items, + incoming.third_party_items, + ] + if x is not None and x != "" + ] + ) + combined_baggage = combined.serialize(include_third_party=True) + + updated_headers.update(headers) + if combined_baggage: + updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage + + # https://github.com/celery/celery/issues/4875 + # + # Need to setdefault the inner headers too since other + # tracing tools (dd-trace-py) also employ this exact + # workaround and we don't want to break them. + updated_headers.setdefault("headers", {}).update(headers) + if combined_baggage: + updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage + + # Add the Sentry options potentially added in `sentry_apply_entry` + # to the headers (done when auto-instrumenting Celery Beat tasks) + for key, value in updated_headers.items(): + if key.startswith("sentry-"): + updated_headers["headers"][key] = value + + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + for key, value in original_headers.items(): + if key != "headers" and key not in updated_headers["headers"]: + updated_headers["headers"][key] = value + + return updated_headers + + +class NoOpMgr: + def __enter__(self) -> None: + return None + + def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: + return None + + +def _wrap_task_run(f: "F") -> "F": + @wraps(f) + def apply_async(*args: "Any", **kwargs: "Any") -> "Any": + # Note: kwargs can contain headers=None, so no setdefault! + # Unsure which backend though. + client = sentry_sdk.get_client() + integration = client.get_integration(CeleryIntegration) + if integration is None: + return f(*args, **kwargs) + + kwarg_headers = kwargs.get("headers") or {} + propagate_traces = kwarg_headers.pop( + "sentry-propagate-traces", integration.propagate_traces + ) + + if not propagate_traces: + return f(*args, **kwargs) + + if isinstance(args[0], Task): + task_name: str = args[0].name + elif len(args) > 1 and isinstance(args[1], str): + task_name = args[1] + else: + task_name = "" + + span_streaming = has_span_streaming_enabled(client.options) + + task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" + + span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + if span_streaming: + if not task_started_from_beat and get_current_span() is not None: + span_mgr = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_CELERY, + "sentry.origin": CeleryIntegration.origin, + }, + ) + + else: + if not task_started_from_beat: + span_mgr = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_CELERY, + name=task_name, + origin=CeleryIntegration.origin, + ) + + with span_mgr as span: + kwargs["headers"] = _update_celery_task_headers( + kwarg_headers, span, integration.monitor_beat_tasks + ) + return f(*args, **kwargs) + + return apply_async # type: ignore + + +def _wrap_tracer(task: "Any", f: "F") -> "F": + # Need to wrap tracer for pushing the scope before prerun is sent, and + # popping it after postrun is sent. + # + # This is the reason we don't use signals for hooking in the first place. + # Also because in Celery 3, signal dispatch returns early if one handler + # crashes. + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + with isolation_scope() as scope: + scope._name = "celery" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) + + task_name = getattr(task, "name", "") + + custom_sampling_context = {} + with capture_internal_exceptions(): + custom_sampling_context = { + "celery_job": { + "task": task_name, + # for some reason, args[1] is a list if non-empty but a + # tuple if empty + "args": list(args[1]), + "kwargs": args[2], + } + } + + span: "Union[Span, StreamedSpan]" + span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + + # Celery task objects are not a thing to be trusted. Even + # something such as attribute access can fail. + with capture_internal_exceptions(): + headers = args[3].get("headers") or {} + if span_streaming: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(custom_sampling_context) + span = sentry_sdk.traces.start_span( + name=task_name, + parent_span=None, # make this a segment + attributes={ + "sentry.origin": CeleryIntegration.origin, + "sentry.span.source": TransactionSource.TASK.value, + "sentry.op": OP.QUEUE_TASK_CELERY, + }, + ) + + span_ctx = span + + else: + span = continue_trace( + headers, + op=OP.QUEUE_TASK_CELERY, + name=task_name, + source=TransactionSource.TASK, + origin=CeleryIntegration.origin, + ) + span.set_status(SPANSTATUS.OK) + + span_ctx = sentry_sdk.start_transaction( + span, + custom_sampling_context=custom_sampling_context, + ) + + with span_ctx: + return f(*args, **kwargs) + + return _inner # type: ignore + + +def _set_messaging_destination_name( + task: "Any", span: "Union[StreamedSpan, Span]" +) -> None: + """Set "messaging.destination.name" tag for span""" + with capture_internal_exceptions(): + delivery_info = task.request.delivery_info + if delivery_info: + routing_key = delivery_info.get("routing_key") + if delivery_info.get("exchange") == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning the tasks + # are sent to the queue with the same name as the routing key. + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + else: + span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + +def _wrap_task_call(task: "Any", f: "F") -> "F": + # Need to wrap task call because the exception is caught before we get to + # see it. Also celery's reported stacktrace is untrustworthy. + + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + try: + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_PROCESS, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PROCESS, + name=task.name, + origin=CeleryIntegration.origin, + ) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + _set_messaging_destination_name(task, span) + + latency = None + with capture_internal_exceptions(): + if ( + task.request.headers is not None + and "sentry-task-enqueued-time" in task.request.headers + ): + latency = _now_seconds_since_epoch() - task.request.headers.pop( + "sentry-task-enqueued-time" + ) + + if latency is not None: + latency *= 1000 # milliseconds + set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) + + with capture_internal_exceptions(): + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries + ) + + with capture_internal_exceptions(): + with task.app.connection() as conn: + set_on_span( + SPANDATA.MESSAGING_SYSTEM, + conn.transport.driver_type, + ) + + return f(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(task, exc_info) + reraise(*exc_info) + + return _inner # type: ignore + + +def _patch_build_tracer() -> None: + import celery.app.trace as trace # type: ignore + + original_build_tracer = trace.build_tracer + + def sentry_build_tracer( + name: "Any", task: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not getattr(task, "_sentry_is_patched", False): + # determine whether Celery will use __call__ or run and patch + # accordingly + if task_has_custom(task, "__call__"): + type(task).__call__ = _wrap_task_call(task, type(task).__call__) + else: + task.run = _wrap_task_call(task, task.run) + + # `build_tracer` is apparently called for every task + # invocation. Can't wrap every celery task for every invocation + # or we will get infinitely nested wrapper functions. + task._sentry_is_patched = True + + return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) + + trace.build_tracer = sentry_build_tracer + + +def _patch_task_apply_async() -> None: + Task.apply_async = _wrap_task_run(Task.apply_async) + + +def _patch_celery_send_task() -> None: + from celery import Celery + + Celery.send_task = _wrap_task_run(Celery.send_task) + + +def _patch_worker_exit() -> None: + # Need to flush queue before worker shutdown because a crashing worker will + # call os._exit + from billiard.pool import Worker # type: ignore + + original_workloop = Worker.workloop + + def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": + try: + return original_workloop(*args, **kwargs) + finally: + with capture_internal_exceptions(): + if ( + sentry_sdk.get_client().get_integration(CeleryIntegration) + is not None + ): + sentry_sdk.flush() + + Worker.workloop = sentry_workloop + + +def _patch_producer_publish() -> None: + original_publish = Producer.publish + + def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return original_publish(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + kwargs_headers = kwargs.get("headers", {}) + if not isinstance(kwargs_headers, Mapping): + # Ensure kwargs_headers is a Mapping, so we can safely call get(). + # We don't expect this to happen, but it's better to be safe. Even + # if it does happen, only our instrumentation breaks. This line + # does not overwrite kwargs["headers"], so the original publish + # method will still work. + kwargs_headers = {} + + task_name = kwargs_headers.get("task") or "" + task_id = kwargs_headers.get("id") + retries = kwargs_headers.get("retries") + + routing_key = kwargs.get("routing_key") + exchange = kwargs.get("exchange") + + span: "Union[StreamedSpan, Span, None]" = None + if span_streaming: + if get_current_span() is not None: + span = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_PUBLISH, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PUBLISH, + name=task_name, + origin=CeleryIntegration.origin, + ) + + if span is None: + return original_publish(self, *args, **kwargs) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + if task_id is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) + + if exchange == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning messages are + # routed to the queue with the same name as the routing key. + set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + if retries is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type + ) + + return original_publish(self, *args, **kwargs) + + Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py new file mode 100644 index 0000000000..b5027d212a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py @@ -0,0 +1,291 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.crons import MonitorStatus, capture_checkin +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.celery.utils import ( + _get_humanized_interval, + _now_seconds_since_epoch, +) +from sentry_sdk.utils import ( + logger, + match_regex_list, +) + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, TypeVar, Union + + from sentry_sdk._types import ( + MonitorConfig, + MonitorConfigScheduleType, + MonitorConfigScheduleUnit, + ) + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import Celery, Task # type: ignore + from celery.beat import Scheduler # type: ignore + from celery.schedules import crontab, schedule # type: ignore + from celery.signals import ( # type: ignore + task_failure, + task_retry, + task_success, + ) +except ImportError: + raise DidNotEnable("Celery not installed") + +try: + from redbeat.schedulers import RedBeatScheduler # type: ignore +except ImportError: + RedBeatScheduler = None + + +def _get_headers(task: "Task") -> "dict[str, Any]": + headers = task.request.get("headers") or {} + + # flatten nested headers + if "headers" in headers: + headers.update(headers["headers"]) + del headers["headers"] + + headers.update(task.request.get("properties") or {}) + + return headers + + +def _get_monitor_config( + celery_schedule: "Any", app: "Celery", monitor_name: str +) -> "MonitorConfig": + monitor_config: "MonitorConfig" = {} + schedule_type: "Optional[MonitorConfigScheduleType]" = None + schedule_value: "Optional[Union[str, int]]" = None + schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None + + if isinstance(celery_schedule, crontab): + schedule_type = "crontab" + schedule_value = ( + "{0._orig_minute} " + "{0._orig_hour} " + "{0._orig_day_of_month} " + "{0._orig_month_of_year} " + "{0._orig_day_of_week}".format(celery_schedule) + ) + elif isinstance(celery_schedule, schedule): + schedule_type = "interval" + (schedule_value, schedule_unit) = _get_humanized_interval( + celery_schedule.seconds + ) + + if schedule_unit == "second": + logger.warning( + "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", + monitor_name, + schedule_value, + ) + return {} + + else: + logger.warning( + "Celery schedule type '%s' not supported by Sentry Crons.", + type(celery_schedule), + ) + return {} + + monitor_config["schedule"] = {} + monitor_config["schedule"]["type"] = schedule_type + monitor_config["schedule"]["value"] = schedule_value + + if schedule_unit is not None: + monitor_config["schedule"]["unit"] = schedule_unit + + monitor_config["timezone"] = ( + ( + hasattr(celery_schedule, "tz") + and celery_schedule.tz is not None + and str(celery_schedule.tz) + ) + or app.timezone + or "UTC" + ) + + return monitor_config + + +def _apply_crons_data_to_schedule_entry( + scheduler: "Any", + schedule_entry: "Any", + integration: "sentry_sdk.integrations.celery.CeleryIntegration", +) -> None: + """ + Add Sentry Crons information to the schedule_entry headers. + """ + if not integration.monitor_beat_tasks: + return + + monitor_name = schedule_entry.name + + task_should_be_excluded = match_regex_list( + monitor_name, integration.exclude_beat_tasks + ) + if task_should_be_excluded: + return + + celery_schedule = schedule_entry.schedule + app = scheduler.app + + monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) + + is_supported_schedule = bool(monitor_config) + if not is_supported_schedule: + return + + headers = schedule_entry.options.pop("headers", {}) + headers.update( + { + "sentry-monitor-slug": monitor_name, + "sentry-monitor-config": monitor_config, + } + ) + + check_in_id = capture_checkin( + monitor_slug=monitor_name, + monitor_config=monitor_config, + status=MonitorStatus.IN_PROGRESS, + ) + headers.update({"sentry-monitor-check-in-id": check_in_id}) + + # Set the Sentry configuration in the options of the ScheduleEntry. + # Those will be picked up in `apply_async` and added to the headers. + schedule_entry.options["headers"] = headers + + +def _wrap_beat_scheduler( + original_function: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Makes sure that: + - a new Sentry trace is started for each task started by Celery Beat and + it is propagated to the task. + - the Sentry Crons information is set in the Celery Beat task's + headers so that is monitored with Sentry Crons. + + After the patched function is called, + Celery Beat will call apply_async to put the task in the queue. + """ + # Patch only once + # Can't use __name__ here, because some of our tests mock original_apply_entry + already_patched = "sentry_patched_scheduler" in str(original_function) + if already_patched: + return original_function + + from sentry_sdk.integrations.celery import CeleryIntegration + + def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(CeleryIntegration) + if integration is None: + return original_function(*args, **kwargs) + + # Tasks started by Celery Beat start a new Trace + scope = sentry_sdk.get_isolation_scope() + scope.set_new_propagation_context() + scope._name = "celery-beat" + + scheduler, schedule_entry = args + _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) + + return original_function(*args, **kwargs) + + return sentry_patched_scheduler + + +def _patch_beat_apply_entry() -> None: + Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) + + +def _patch_redbeat_apply_async() -> None: + if RedBeatScheduler is None: + return + + RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) + + +def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: + if monitor_beat_tasks: + task_success.connect(crons_task_success) + task_failure.connect(crons_task_failure) + task_retry.connect(crons_task_retry) + + +def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_success %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.OK, + ) + + +def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_failure %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) + + +def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_retry %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py new file mode 100644 index 0000000000..8d181f1f24 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py @@ -0,0 +1,32 @@ +import time +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from typing import Tuple + + from sentry_sdk._types import MonitorConfigScheduleUnit + + +def _now_seconds_since_epoch() -> float: + # We cannot use `time.perf_counter()` when dealing with the duration + # of a Celery task, because the start of a Celery task and + # the end are recorded in different processes. + # Start happens in the Celery Beat process, + # the end in a Celery Worker process. + return time.time() + + +def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": + TIME_UNITS = ( # noqa: N806 + ("day", 60 * 60 * 24.0), + ("hour", 60 * 60.0), + ("minute", 60.0), + ) + + seconds = float(seconds) + for unit, divider in TIME_UNITS: + if seconds >= divider: + interval = int(seconds / divider) + return (interval, cast("MonitorConfigScheduleUnit", unit)) + + return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py new file mode 100644 index 0000000000..9baa0e5cdd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py @@ -0,0 +1,188 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.aws_lambda import _make_request_event_processor +from sentry_sdk.traces import ( + SpanStatus, + StreamedSpan, + get_current_span, +) +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + parse_version, + reraise, +) + +try: + import chalice # type: ignore + from chalice import Chalice, ChaliceViewError + from chalice import __version__ as CHALICE_VERSION + from chalice.app import ( # type: ignore + EventSourceHandler as ChaliceEventSourceHandler, + ) +except ImportError: + raise DidNotEnable("Chalice is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) + + +class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore + def __call__(self, event: "Any", context: "Any") -> "Any": + client = sentry_sdk.get_client() + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor(event, context, configured_time) + ) + try: + return ChaliceEventSourceHandler.__call__(self, event, context) + except Exception: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + client.flush() + reraise(*exc_info) + + +def _get_view_function_response( + app: "Any", view_function: "F", function_args: "Any" +) -> "F": + @wraps(view_function) + def wrapped_view_function(**function_args: "Any") -> "Any": + client = sentry_sdk.get_client() + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = app.lambda_context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor( + app.current_request.to_dict(), + app.lambda_context, + configured_time, + ) + ) + + if has_span_streaming_enabled(client.options): + current_span = get_current_span() + segment = None + if type(current_span) is StreamedSpan: + # A segment already exists (created by the AWS Lambda + # integration), so decorate it with Chalice attributes + # The AWS Lambda integration owns the span lifecycle + # (end + flush), but Chalice converts unhandled view exceptions + # into 500 responses, so the error must be captured here. + request_dict = app.current_request.to_dict() + headers = request_dict.get("headers", {}) + + header_attrs: "Dict[str, Any]" = {} + for header, value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attrs[f"http.request.header.{header.lower()}"] = value + + additional_attrs: "Dict[str, Any]" = {} + if "method" in request_dict: + additional_attrs["http.request.method"] = request_dict["method"] + + attributes = { + "sentry.origin": ChaliceIntegration.origin, + **header_attrs, + **additional_attrs, + } + + segment = current_span._segment + segment.set_attributes(attributes) + + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + if segment: + segment.status = SpanStatus.ERROR.value + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + if segment is None: + client.flush() + raise + else: + scope.set_transaction_name( + app.lambda_context.function_name, + source=TransactionSource.COMPONENT, + ) + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + client.flush() + raise + + return wrapped_view_function # type: ignore + + +class ChaliceIntegration(Integration): + identifier = "chalice" + origin = f"auto.function.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(CHALICE_VERSION) + + if version is None: + raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) + + if version < (1, 20): + old_get_view_function_response = Chalice._get_view_function_response + else: + from chalice.app import RestAPIEventHandler + + old_get_view_function_response = ( + RestAPIEventHandler._get_view_function_response + ) + + def sentry_event_response( + app: "Any", view_function: "F", function_args: "Dict[str, Any]" + ) -> "Any": + wrapped_view_function = _get_view_function_response( + app, view_function, function_args + ) + + return old_get_view_function_response( + app, wrapped_view_function, function_args + ) + + if version < (1, 20): + Chalice._get_view_function_response = sentry_event_response + else: + RestAPIEventHandler._get_view_function_response = sentry_event_response + # for everything else (like events) + chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py new file mode 100644 index 0000000000..e6b3009548 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py @@ -0,0 +1,211 @@ +import functools +from typing import TYPE_CHECKING, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import Any, Callable, ParamSpec, Union +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + + +try: + from clickhouse_driver import VERSION # type: ignore[import-not-found] + from clickhouse_driver.client import Client # type: ignore[import-not-found] + from clickhouse_driver.connection import ( # type: ignore[import-not-found] + Connection, + ) + +except ImportError: + raise DidNotEnable("clickhouse-driver not installed.") + + +class ClickhouseDriverIntegration(Integration): + identifier = "clickhouse_driver" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(ClickhouseDriverIntegration, VERSION) + + # Every query is done using the Connection's `send_query` function + Connection.send_query = _wrap_start(Connection.send_query) + + # If the query contains parameters then the send_data function is used to send those parameters to clickhouse + _wrap_send_data() + + # Every query ends either with the Client's `receive_end_of_query` (no result expected) + # or its `receive_result` (result expected) + Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) + if hasattr(Client, "receive_end_of_insert_query"): + # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` + Client.receive_end_of_insert_query = _wrap_end( + Client.receive_end_of_insert_query + ) + Client.receive_result = _wrap_end(Client.receive_result) + + +P = ParamSpec("P") +T = TypeVar("T") + + +def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": + @functools.wraps(f) + def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(ClickhouseDriverIntegration) is None: + return f(*args, **kwargs) + + connection = args[0] + query = args[1] + query_id = args[2] if len(args) > 2 else kwargs.get("query_id") + params = args[3] if len(args) > 3 else kwargs.get("params") + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=query, # type: ignore + attributes={ + "sentry.op": OP.DB, + "sentry.origin": ClickhouseDriverIntegration.origin, + SPANDATA.DB_QUERY_TEXT: str(query), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=ClickhouseDriverIntegration.origin, + ) + + span.set_data("query", query) + + if query_id: + span.set_data("db.query_id", query_id) + + if params and should_send_default_pii(): + span.set_data("db.params", params) + + connection._sentry_span = span # type: ignore[attr-defined] + + _set_db_data(span, connection) + + # run the original code + ret = f(*args, **kwargs) + + return ret + + return _inner + + +def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": + def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": + res = f(*args, **kwargs) + instance = args[0] + span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] + + if span is None: + return res + + if isinstance(span, StreamedSpan): + span.end() + else: + if res is not None and should_send_default_pii(): + span.set_data("db.result", res) + + with capture_internal_exceptions(): + span.scope.add_breadcrumb( + message=span._data.pop("query"), category="query", data=span._data + ) + + span.finish() + + return res + + return _inner_end + + +def _wrap_send_data() -> None: + original_send_data = Client.send_data + + def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data + self, sample_block, data, types_check=False, columnar=False, *args, **kwargs + ): + span = getattr(self.connection, "_sentry_span", None) + + if isinstance(span, StreamedSpan): + _set_db_data(span, self.connection) + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + if span is not None: + _set_db_data(span, self.connection) + + if should_send_default_pii(): + db_params = span._data.get("db.params", []) + + if isinstance(data, (list, tuple)): + db_params.extend(data) + + else: # data is a generic iterator + orig_data = data + + # Wrap the generator to add items to db.params as they are yielded. + # This allows us to send the params to Sentry without needing to allocate + # memory for the entire generator at once. + def wrapped_generator() -> "Iterator[Any]": + for item in orig_data: + db_params.append(item) + yield item + + # Replace the original iterator with the wrapped one. + data = wrapped_generator() + + span.set_data("db.params", db_params) + + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + Client.send_data = _inner_send_data + + +def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") + span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) + + set_on_span = span.set_attribute + else: + span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") + span.set_data(SPANDATA.DB_NAME, connection.database) + + set_on_span = span.set_data + + set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") + set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) + set_on_span(SPANDATA.SERVER_PORT, connection.port) + set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py new file mode 100644 index 0000000000..f6285d0a9b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py @@ -0,0 +1,273 @@ +import json +from typing import TYPE_CHECKING + +import urllib3 + +from sentry_sdk.api import set_context +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Dict + + +CONTEXT_TYPE = "cloud_resource" + +HTTP_TIMEOUT = 2.0 + +AWS_METADATA_HOST = "169.254.169.254" +AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) +AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( + AWS_METADATA_HOST +) + +GCP_METADATA_HOST = "metadata.google.internal" +GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( + GCP_METADATA_HOST +) + + +class CLOUD_PROVIDER: # noqa: N801 + """ + Name of the cloud provider. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + ALIBABA = "alibaba_cloud" + AWS = "aws" + AZURE = "azure" + GCP = "gcp" + IBM = "ibm_cloud" + TENCENT = "tencent_cloud" + + +class CLOUD_PLATFORM: # noqa: N801 + """ + The cloud platform. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + AWS_EC2 = "aws_ec2" + AWS_LAMBDA = "aws_lambda" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" + + +class CloudResourceContextIntegration(Integration): + """ + Adds cloud resource context to the Senty scope + """ + + identifier = "cloudresourcecontext" + + cloud_provider = "" + + aws_token = "" + http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) + + gcp_metadata = None + + def __init__(self, cloud_provider: str = "") -> None: + CloudResourceContextIntegration.cloud_provider = cloud_provider + + @classmethod + def _is_aws(cls) -> bool: + try: + r = cls.http.request( + "PUT", + AWS_TOKEN_URL, + headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, + ) + + if r.status != 200: + return False + + cls.aws_token = r.data.decode() + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking AWS metadata service: %s", str(e)) + return False + + @classmethod + def _get_aws_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.AWS, + "cloud.platform": CLOUD_PLATFORM.AWS_EC2, + } + + try: + r = cls.http.request( + "GET", + AWS_METADATA_URL, + headers={"X-aws-ec2-metadata-token": cls.aws_token}, + ) + + if r.status != 200: + return ctx + + data = json.loads(r.data.decode("utf-8")) + + try: + ctx["cloud.account.id"] = data["accountId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = data["availabilityZone"] + except Exception: + pass + + try: + ctx["cloud.region"] = data["region"] + except Exception: + pass + + try: + ctx["host.id"] = data["instanceId"] + except Exception: + pass + + try: + ctx["host.type"] = data["instanceType"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching AWS metadata: %s", str(e)) + + return ctx + + @classmethod + def _is_gcp(cls) -> bool: + try: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return False + + cls.gcp_metadata = json.loads(r.data.decode("utf-8")) + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking GCP metadata service: %s", str(e)) + return False + + @classmethod + def _get_gcp_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.GCP, + "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, + } + + gcp_metadata = cls.gcp_metadata + try: + if cls.gcp_metadata is None: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return ctx + + gcp_metadata = json.loads(r.data.decode("utf-8")) + cls.gcp_metadata = gcp_metadata + + try: + ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( + "/" + )[-1] + except Exception: + pass + + try: + # only populated in google cloud run + ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] + except Exception: + pass + + try: + ctx["host.id"] = gcp_metadata["instance"]["id"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching GCP metadata: %s", str(e)) + + return ctx + + @classmethod + def _get_cloud_provider(cls) -> str: + if cls._is_aws(): + return CLOUD_PROVIDER.AWS + + if cls._is_gcp(): + return CLOUD_PROVIDER.GCP + + return "" + + @classmethod + def _get_cloud_resource_context(cls) -> "Dict[str, str]": + cloud_provider = ( + cls.cloud_provider + if cls.cloud_provider != "" + else CloudResourceContextIntegration._get_cloud_provider() + ) + if cloud_provider in context_getters.keys(): + return context_getters[cloud_provider]() + + return {} + + @staticmethod + def setup_once() -> None: + cloud_provider = CloudResourceContextIntegration.cloud_provider + unsupported_cloud_provider = ( + cloud_provider != "" and cloud_provider not in context_getters.keys() + ) + + if unsupported_cloud_provider: + logger.warning( + "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", + CloudResourceContextIntegration.cloud_provider, + list(context_getters.keys()), + ) + + context = CloudResourceContextIntegration._get_cloud_resource_context() + if context != {}: + set_context(CONTEXT_TYPE, context) + + +# Map with the currently supported cloud providers +# mapping to functions extracting the context +context_getters = { + CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, + CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, +} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py new file mode 100644 index 0000000000..7abf3f6808 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py @@ -0,0 +1,303 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise + +try: + from cohere import ( + ChatStreamEndEvent, + NonStreamedChatResponse, + ) + from cohere.base_client import BaseCohere + from cohere.client import Client + + if TYPE_CHECKING: + from cohere import StreamedChatResponse +except ImportError: + raise DidNotEnable("Cohere not installed") + +try: + # cohere 5.9.3+ + from cohere import StreamEndStreamedChatResponse +except ImportError: + from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse + + +COLLECTED_CHAT_PARAMS = { + "model": SPANDATA.AI_MODEL_ID, + "k": SPANDATA.AI_TOP_K, + "p": SPANDATA.AI_TOP_P, + "seed": SPANDATA.AI_SEED, + "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, + "raw_prompting": SPANDATA.AI_RAW_PROMPTING, +} + +COLLECTED_PII_CHAT_PARAMS = { + "tools": SPANDATA.AI_TOOLS, + "preamble": SPANDATA.AI_PREAMBLE, +} + +COLLECTED_CHAT_RESP_ATTRS = { + "generation_id": SPANDATA.AI_GENERATION_ID, + "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, + "finish_reason": SPANDATA.AI_FINISH_REASON, +} + +COLLECTED_PII_CHAT_RESP_ATTRS = { + "citations": SPANDATA.AI_CITATIONS, + "documents": SPANDATA.AI_DOCUMENTS, + "search_queries": SPANDATA.AI_SEARCH_QUERIES, + "search_results": SPANDATA.AI_SEARCH_RESULTS, + "tool_calls": SPANDATA.AI_TOOL_CALLS, +} + + +class CohereIntegration(Integration): + identifier = "cohere" + origin = f"auto.ai.{identifier}" + + def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) + Client.embed = _wrap_embed(Client.embed) + BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "cohere", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _end_span(span: "Any") -> None: + if isinstance(span, StreamedSpan): + span.end() + else: + span.__exit__(None, None, None) + + +def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": + def collect_chat_response_fields( + span: "Union[Span, StreamedSpan]", + res: "NonStreamedChatResponse", + include_pii: bool, + ) -> None: + if include_pii: + if hasattr(res, "text"): + set_data_normalized( + span, + SPANDATA.AI_RESPONSES, + [res.text], + ) + for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: + if hasattr(res, pii_attr): + set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) + + for attr in COLLECTED_CHAT_RESP_ATTRS: + if hasattr(res, attr): + set_data_normalized(span, "ai." + attr, getattr(res, attr)) + + if hasattr(res, "meta"): + if hasattr(res.meta, "billed_units"): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + output_tokens=res.meta.billed_units.output_tokens, + ) + elif hasattr(res.meta, "tokens"): + record_token_usage( + span, + input_tokens=res.meta.tokens.input_tokens, + output_tokens=res.meta.tokens.output_tokens, + ) + + if hasattr(res.meta, "warnings"): + set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) + + @wraps(f) + def new_chat(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if ( + integration is None + or "message" not in kwargs + or not isinstance(kwargs.get("message"), str) + ): + return f(*args, **kwargs) + + message = kwargs.get("message") + + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name="cohere.client.Chat", + attributes={ + "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + name="cohere.client.Chat", + origin=CohereIntegration.origin, + ) + span.__enter__() + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + + reraise(*exc_info) + + with capture_internal_exceptions(): + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, + SPANDATA.AI_INPUT_MESSAGES, + list( + map( + lambda x: { + "role": getattr(x, "role", "").lower(), + "content": getattr(x, "message", ""), + }, + kwargs.get("chat_history", []), + ) + ) + + [{"role": "user", "content": message}], + ) + for k, v in COLLECTED_PII_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + + for k, v in COLLECTED_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + set_data_normalized(span, SPANDATA.AI_STREAMING, False) + + if streaming: + old_iterator = res + + def new_iterator() -> "Iterator[StreamedChatResponse]": + with capture_internal_exceptions(): + for x in old_iterator: + if isinstance(x, ChatStreamEndEvent) or isinstance( + x, StreamEndStreamedChatResponse + ): + collect_chat_response_fields( + span, + x.response, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + yield x + _end_span(span) + + return new_iterator() + elif isinstance(res, NonStreamedChatResponse): + collect_chat_response_fields( + span, + res, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + _end_span(span) + else: + set_data_normalized(span, "unknown_response", True) + _end_span(span) + return res + + return new_chat + + +def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + if integration is None: + return f(*args, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name="Cohere Embedding Creation", + attributes={ + "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span_ctx = get_start_span_function()( + op=consts.OP.COHERE_EMBEDDINGS_CREATE, + name="Cohere Embedding Creation", + origin=CohereIntegration.origin, + ) + + with span_ctx as span: + if "texts" in kwargs and ( + should_send_default_pii() and integration.include_prompts + ): + if isinstance(kwargs["texts"], str): + set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) + elif ( + isinstance(kwargs["texts"], list) + and len(kwargs["texts"]) > 0 + and isinstance(kwargs["texts"][0], str) + ): + set_data_normalized( + span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] + ) + + if "model" in kwargs: + set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) + if ( + hasattr(res, "meta") + and hasattr(res.meta, "billed_units") + and hasattr(res.meta.billed_units, "input_tokens") + ): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + total_tokens=res.meta.billed_units.input_tokens, + ) + return res + + return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py new file mode 100644 index 0000000000..a0e9014666 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py @@ -0,0 +1,62 @@ +import weakref +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import ContextVar, logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class DedupeIntegration(Integration): + identifier = "dedupe" + + def __init__(self) -> None: + self._last_seen = ContextVar("last-seen") + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if hint is None: + return event + + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return event + + exc_info = hint.get("exc_info", None) + if exc_info is None: + return event + + last_seen = integration._last_seen.get(None) + if last_seen is not None: + # last_seen is either a weakref or the original instance + last_seen = ( + last_seen() if isinstance(last_seen, weakref.ref) else last_seen + ) + + exc = exc_info[1] + if last_seen is exc: + logger.info("DedupeIntegration dropped duplicated error event %s", exc) + return None + + # we can only weakref non builtin types + try: + integration._last_seen.set(weakref.ref(exc)) + except TypeError: + integration._last_seen.set(exc) + + return event + + @staticmethod + def reset_last_seen() -> None: + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return + + integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py new file mode 100644 index 0000000000..361b60079d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py @@ -0,0 +1,884 @@ +import inspect +import sys +import threading +import weakref +from importlib import import_module + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANNAME +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + transaction_from_function, + walk_exception_chain, +) + +try: + from django import VERSION as DJANGO_VERSION + from django.conf import settings + from django.conf import settings as django_settings + from django.core import signals + from django.utils.functional import SimpleLazyObject + + try: + from django.urls import resolve + except ImportError: + from django.core.urlresolvers import resolve + + try: + from django.urls import Resolver404 + except ImportError: + from django.core.urlresolvers import Resolver404 + + # Only available in Django 3.0+ + try: + from django.core.handlers.asgi import ASGIRequest + except Exception: + ASGIRequest = None + +except ImportError: + raise DidNotEnable("Django not installed") + +from sentry_sdk.integrations.django.middleware import patch_django_middlewares +from sentry_sdk.integrations.django.signals_handlers import patch_signals +from sentry_sdk.integrations.django.tasks import patch_tasks +from sentry_sdk.integrations.django.templates import ( + get_template_frame_from_exception, + patch_templates, +) +from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER +from sentry_sdk.integrations.django.views import patch_views + +if DJANGO_VERSION[:2] > (1, 8): + from sentry_sdk.integrations.django.caching import patch_caching +else: + patch_caching = None # type: ignore + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from django.core.handlers.wsgi import WSGIRequest + from django.http.request import QueryDict + from django.http.response import HttpResponse + from django.utils.datastructures import MultiValueDict + + from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +if DJANGO_VERSION < (1, 10): + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated() + +else: + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated + + +TRANSACTION_STYLE_VALUES = ("function_name", "url") + + +class DjangoIntegration(Integration): + """ + Auto instrument a Django application. + + :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. + :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. + :param signals_spans: Whether to create spans for signals. Defaults to `True`. + :param signals_denylist: A list of signals to ignore when creating spans. + :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. + """ + + identifier = "django" + origin = f"auto.http.{identifier}" + origin_db = f"auto.db.{identifier}" + + transaction_style = "" + middleware_spans: "Optional[bool]" = None + signals_spans: "Optional[bool]" = None + cache_spans: "Optional[bool]" = None + signals_denylist: "list[signals.Signal]" = [] + + def __init__( + self, + transaction_style: str = "url", + middleware_spans: bool = False, + signals_spans: bool = True, + cache_spans: bool = False, + db_transaction_spans: bool = False, + signals_denylist: "Optional[list[signals.Signal]]" = None, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + + self.signals_spans = signals_spans + self.signals_denylist = signals_denylist or [] + + self.cache_spans = cache_spans + self.db_transaction_spans = db_transaction_spans + + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(DjangoIntegration, DJANGO_VERSION) + + install_sql_hook() + # Patch in our custom middleware. + + # logs an error for every 500 + ignore_logger("django.server") + ignore_logger("django.request") + + from django.core.handlers.wsgi import WSGIHandler + + old_app = WSGIHandler.__call__ + + @ensure_integration_enabled(DjangoIntegration, old_app) + def sentry_patched_wsgi_handler( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + bound_old_app = old_app.__get__(self, WSGIHandler) + + from django.conf import settings + + use_x_forwarded_for = settings.USE_X_FORWARDED_HOST + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + middleware = SentryWsgiMiddleware( + bound_old_app, + use_x_forwarded_for, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + WSGIHandler.__call__ = sentry_patched_wsgi_handler + + _patch_get_response() + + _patch_django_asgi_handler() + + signals.got_request_exception.connect(_got_request_exception) + + @add_global_event_processor + def process_django_templates( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_, exc_value, _) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + frame = get_template_frame_from_exception(exc_value) + if frame is not None: + frames = exception.get("stacktrace", {}).get("frames", []) + + for i in reversed(range(len(frames))): + f = frames[i] + if ( + f.get("function") in ("Parser.parse", "parse", "render") + and f.get("module") == "django.template.base" + ): + i += 1 + break + else: + i = len(frames) + + frames.insert(i, frame) + + return event + + @add_global_repr_processor + def _django_queryset_repr( + value: "Any", hint: "Dict[str, Any]" + ) -> "Union[NotImplementedType, str]": + try: + # Django 1.6 can fail to import `QuerySet` when Django settings + # have not yet been initialized. + # + # If we fail to import, return `NotImplemented`. It's at least + # unlikely that we have a query set in `value` when importing + # `QuerySet` fails. + from django.db.models.query import QuerySet + except Exception: + return NotImplemented + + if not isinstance(value, QuerySet) or value._result_cache: + return NotImplemented + + return "<%s from %s at 0x%x>" % ( + value.__class__.__name__, + value.__module__, + id(value), + ) + + _patch_channels() + patch_django_middlewares() + patch_views() + patch_templates() + patch_signals() + patch_tasks() + add_template_context_repr_sequence() + + if patch_caching is not None: + patch_caching() + + +_DRF_PATCHED = False +_DRF_PATCH_LOCK = threading.Lock() + + +def _patch_drf() -> None: + """ + Patch Django Rest Framework for more/better request data. DRF's request + type is a wrapper around Django's request type. The attribute we're + interested in is `request.data`, which is a cached property containing a + parsed request body. Reading a request body from that property is more + reliable than reading from any of Django's own properties, as those don't + hold payloads in memory and therefore can only be accessed once. + + We patch the Django request object to include a weak backreference to the + DRF request object, such that we can later use either in + `DjangoRequestExtractor`. + + This function is not called directly on SDK setup, because importing almost + any part of Django Rest Framework will try to access Django settings (where + `sentry_sdk.init()` might be called from in the first place). Instead we + run this function on every request and do the patching on the first + request. + """ + + global _DRF_PATCHED + + if _DRF_PATCHED: + # Double-checked locking + return + + with _DRF_PATCH_LOCK: + if _DRF_PATCHED: + return + + # We set this regardless of whether the code below succeeds or fails. + # There is no point in trying to patch again on the next request. + _DRF_PATCHED = True + + with capture_internal_exceptions(): + try: + from rest_framework.views import APIView # type: ignore + except ImportError: + pass + else: + old_drf_initial = APIView.initial + + def sentry_patched_drf_initial( + self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + with capture_internal_exceptions(): + request._request._sentry_drf_request_backref = weakref.ref( + request + ) + pass + return old_drf_initial(self, request, *args, **kwargs) + + APIView.initial = sentry_patched_drf_initial + + +def _patch_channels() -> None: + try: + from channels.http import AsgiHandler # type: ignore + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because channels may not be used at all in + # the current process. That is the case when running traditional WSGI + # workers in gunicorn+gevent and the websocket stuff in a separate + # process. + logger.warning( + "We detected that you are using Django channels 2.0." + + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl + + patch_channels_asgi_handler_impl(AsgiHandler) + + +def _patch_django_asgi_handler() -> None: + try: + from django.core.handlers.asgi import ASGIHandler + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because Django's ASGI stuff may not be used + # at all. + logger.warning( + "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl + + patch_django_asgi_handler_impl(ASGIHandler) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" +) -> None: + try: + transaction_name = None + if transaction_style == "function_name": + fn = resolve(request.path).func + transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) + + elif transaction_style == "url": + if hasattr(request, "urlconf"): + transaction_name = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + else: + transaction_name = LEGACY_RESOLVER.resolve(request.path_info) + + if transaction_name is None: + transaction_name = request.path_info + source = TransactionSource.URL + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name( + transaction_name, + source=source, + ) + except Resolver404: + urlconf = import_module(settings.ROOT_URLCONF) + # This exception only gets thrown when transaction_style is `function_name` + # So we don't check here what style is configured + if hasattr(urlconf, "handler404"): + handler = urlconf.handler404 + if isinstance(handler, str): + scope.transaction = handler + else: + scope.transaction = transaction_from_function( + getattr(handler, "view_class", handler) + ) + except Exception: + pass + + +def _before_get_response(request: "WSGIRequest") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return + + _patch_drf() + + scope = sentry_sdk.get_current_scope() + # Rely on WSGI middleware to start a trace + _set_transaction_name_and_source(scope, integration.transaction_style, request) + + scope.add_event_processor( + _make_wsgi_request_event_processor(weakref.ref(request), integration) + ) + + +def _attempt_resolve_again( + request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str +) -> None: + """ + Some django middlewares overwrite request.urlconf + so we need to respect that contract, + so we try to resolve the url again. + """ + if not hasattr(request, "urlconf"): + return + + _set_transaction_name_and_source(scope, transaction_style, request) + + +def _after_get_response(request: "WSGIRequest") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming and should_send_default_pii(): + user = getattr(request, "user", None) + + # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. + # Exit early if the user has not been materialized yet. + is_lazy = isinstance(user, SimpleLazyObject) + if is_lazy and hasattr(request, "_cached_user"): + user = request._cached_user + elif is_lazy: + return + + if user is None or not is_authenticated(user): + return + + user_info = {} + try: + user_info["id"] = str(user.pk) + except Exception: + pass + + try: + user_info["email"] = user.email + except Exception: + pass + + try: + user_info["username"] = user.get_username() + except Exception: + pass + + sentry_sdk.set_user(user_info) + + +def _patch_get_response() -> None: + """ + patch get_response, because at that point we have the Django request object + """ + from django.core.handlers.base import BaseHandler + + old_get_response = BaseHandler.get_response + + def sentry_patched_get_response( + self: "Any", request: "WSGIRequest" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + rv = old_get_response(self, request) + _after_get_response(request) + return rv + + BaseHandler.get_response = sentry_patched_get_response + + if hasattr(BaseHandler, "get_response_async"): + from sentry_sdk.integrations.django.asgi import patch_get_response_async + + patch_get_response_async(BaseHandler, _before_get_response) + + +def _make_wsgi_request_event_processor( + weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" +) -> "EventProcessor": + def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + request = weak_request() + if request is None: + return event + + django_3 = ASGIRequest is not None + if django_3 and type(request) == ASGIRequest: + # We have a `asgi_request_event_processor` for this. + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return wsgi_request_event_processor + + +def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if request is not None and integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + event, hint = event_from_exception( + sys.exc_info(), + client_options=client.options, + mechanism={"type": "django", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class DjangoRequestExtractor(RequestExtractor): + def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: + try: + drf_request = request._sentry_drf_request_backref() + if drf_request is not None: + request = drf_request + except AttributeError: + pass + self.request = request + + def env(self) -> "Dict[str, str]": + return self.request.META + + def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": + privacy_cookies = [ + django_settings.CSRF_COOKIE_NAME, + django_settings.SESSION_COOKIE_NAME, + ] + + clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} + for key, val in self.request.COOKIES.items(): + if key in privacy_cookies: + clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE + else: + clean_cookies[key] = val + + return clean_cookies + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "QueryDict": + return self.request.POST + + def files(self) -> "MultiValueDict": + return self.request.FILES + + def size_of_file(self, file: "Any") -> int: + return file.size + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + return self.request.data + except Exception: + return RequestExtractor.parsed_body(self) + + +def _set_user_info(request: "WSGIRequest", event: "Event") -> None: + user_info = event.setdefault("user", {}) + + user = getattr(request, "user", None) + + if user is None or not is_authenticated(user): + return + + try: + user_info.setdefault("id", str(user.pk)) + except Exception: + pass + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.get_username()) + except Exception: + pass + + +def install_sql_hook() -> None: + """If installed this causes Django's queries to be captured.""" + try: + from django.db.backends.utils import CursorWrapper + except ImportError: + from django.db.backends.util import CursorWrapper + + try: + # django 1.6 and 1.7 compatability + from django.db.backends import BaseDatabaseWrapper + except ImportError: + # django 1.8 or later + from django.db.backends.base.base import BaseDatabaseWrapper + + try: + real_execute = CursorWrapper.execute + real_executemany = CursorWrapper.executemany + real_connect = BaseDatabaseWrapper.connect + real_commit = BaseDatabaseWrapper._commit + real_rollback = BaseDatabaseWrapper._rollback + except AttributeError: + # This won't work on Django versions < 1.6 + return + + @ensure_integration_enabled(DjangoIntegration, real_execute) + def execute( + self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=params, + paramstyle="format", + executemany=False, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + result = real_execute(self, sql, params) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_executemany) + def executemany( + self: "CursorWrapper", sql: "Any", param_list: "List[Any]" + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=param_list, + paramstyle="format", + executemany=True, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + + result = real_executemany(self, sql, param_list) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_connect) + def connect(self: "BaseDatabaseWrapper") -> None: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message="connect", category="query") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="connect", + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self) + return real_connect(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + return real_connect(self) + + def _commit(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_commit(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_COMMIT, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_COMMIT, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + + def _rollback(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_rollback(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_ROLLBACK, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_ROLLBACK, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + + CursorWrapper.execute = execute + CursorWrapper.executemany = executemany + BaseDatabaseWrapper.connect = connect + BaseDatabaseWrapper._commit = _commit + BaseDatabaseWrapper._rollback = _rollback + ignore_logger("django.db.backends") + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", + cursor_or_db: "Any", + db_operation: "Optional[str]" = None, +) -> None: + db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db + vendor = db.vendor + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) + + if db_operation is not None: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) + else: + span.set_data(SPANDATA.DB_SYSTEM, vendor) + + if db_operation is not None: + span.set_data(SPANDATA.DB_OPERATION, db_operation) + + # Some custom backends override `__getattr__`, making it look like `cursor_or_db` + # actually has a `connection` and the `connection` has a `get_dsn_parameters` + # attribute, only to throw an error once you actually want to call it. + # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable + # function. + is_psycopg2 = ( + hasattr(cursor_or_db, "connection") + and hasattr(cursor_or_db.connection, "get_dsn_parameters") + and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + ) + if is_psycopg2: + connection_params = cursor_or_db.connection.get_dsn_parameters() + else: + try: + # psycopg3, only extract needed params as get_parameters + # can be slow because of the additional logic to filter out default + # values + connection_params = { + "dbname": cursor_or_db.connection.info.dbname, + "port": cursor_or_db.connection.info.port, + } + # PGhost returns host or base dir of UNIX socket as an absolute path + # starting with /, use it only when it contains host + pg_host = cursor_or_db.connection.info.host + if pg_host and not pg_host.startswith("/"): + connection_params["host"] = pg_host + except Exception: + connection_params = db.get_connection_params() + + db_name = connection_params.get("dbname") or connection_params.get("database") + + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + + set_on_span = span.set_attribute + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + set_on_span = span.set_data + + server_address = connection_params.get("host") + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = connection_params.get("port") + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, str(server_port)) + + server_socket_address = connection_params.get("unix_socket") + if server_socket_address is not None: + set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) + + +def add_template_context_repr_sequence() -> None: + try: + from django.template.context import BaseContext + + add_repr_sequence_type(BaseContext) + except Exception: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py new file mode 100644 index 0000000000..43faffb5be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py @@ -0,0 +1,262 @@ +""" +Instrumentation for Django 3.0 + +Since this file contains `async def` it is conditionally imported in +`sentry_sdk.integrations.django` (depending on the existence of +`django.core.handlers.asgi`. +""" + +import asyncio +import functools +import inspect +from typing import TYPE_CHECKING + +from django.core.handlers.wsgi import WSGIRequest + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar, Union + + from django.core.handlers.asgi import ASGIRequest + from django.http.response import HttpResponse + + from sentry_sdk._types import Event, EventProcessor + + _F = TypeVar("_F", bound=Callable[..., Any]) + + +# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for +# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. +# The latter is replaced with the inspect.markcoroutinefunction decorator. +# Until 3.12 is the minimum supported Python version, provide a shim. +# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py +if hasattr(inspect, "markcoroutinefunction"): + iscoroutinefunction = inspect.iscoroutinefunction + markcoroutinefunction = inspect.markcoroutinefunction +else: + iscoroutinefunction = asyncio.iscoroutinefunction + + def markcoroutinefunction(func: "_F") -> "_F": + func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + return func + + +def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": + def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + from sentry_sdk.integrations.django import ( + DjangoRequestExtractor, + _set_user_info, + ) + + if request is None: + return event + + if type(request) == WSGIRequest: + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return asgi_request_event_processor + + +def patch_django_asgi_handler_impl(cls: "Any") -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + )._run_asgi3 + + return await middleware(scope, receive, send) + + cls.__call__ = sentry_patched_asgi_handler + + modern_django_asgi_support = hasattr(cls, "create_request") + if modern_django_asgi_support: + old_create_request = cls.create_request + + @ensure_integration_enabled(DjangoIntegration, old_create_request) + def sentry_patched_create_request( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request, error_response = old_create_request(self, *args, **kwargs) + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_make_asgi_request_event_processor(request)) + + return request, error_response + + cls.create_request = sentry_patched_create_request + + +def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: + old_get_response_async = cls.get_response_async + + async def sentry_patched_get_response_async( + self: "Any", request: "Any" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + return await old_get_response_async(self, request) + + cls.get_response_async = sentry_patched_get_response_async + + +def patch_channels_asgi_handler_impl(cls: "Any") -> None: + import channels # type: ignore + + from sentry_sdk.integrations.django import DjangoIntegration + + if channels.__version__ < "3.0.0": + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, receive, send) + + middleware = SentryAsgiMiddleware( + lambda _scope: old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + ) + + return await middleware(self.scope)(receive, send) # type: ignore + + cls.__call__ = sentry_patched_asgi_handler + + else: + # The ASGI handler in Channels >= 3 has the same signature as + # the Django handler. + patch_django_asgi_handler_impl(cls) + + +def wrap_async_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + async def sentry_wrapped_callback( + request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return await callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return await callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return await callback(request, *args, **kwargs) + + return sentry_wrapped_callback + + +def _asgi_middleware_mixin_factory( + _check_middleware_span: "Callable[..., Any]", +) -> "Any": + """ + Mixin class factory that generates a middleware mixin for handling requests + in async mode. + """ + + class SentryASGIMixin: + if TYPE_CHECKING: + _inner = None + + def __init__(self, get_response: "Callable[..., Any]") -> None: + self.get_response = get_response + self._acall_method = None + self._async_check() + + def _async_check(self) -> None: + """ + If get_response is a coroutine function, turns us into async mode so + a thread is not consumed during a whole request. + Taken from django.utils.deprecation::MiddlewareMixin._async_check + """ + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) + + def async_route_check(self) -> bool: + """ + Function that checks if we are in async mode, + and if we are forwards the handling of requests to __acall__ + """ + return iscoroutinefunction(self.get_response) + + async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": + f = self._acall_method + if f is None: + if hasattr(self._inner, "__acall__"): + self._acall_method = f = self._inner.__acall__ # type: ignore + else: + self._acall_method = f = self._inner + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return await f(*args, **kwargs) # type: ignore + + with middleware_span: + return await f(*args, **kwargs) # type: ignore + + return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py new file mode 100644 index 0000000000..faf1803c11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py @@ -0,0 +1,264 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.core.cache import CacheHandler +from urllib3.util import parse_url as urlparse + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +METHODS_TO_INSTRUMENT = [ + "set", + "set_many", + "get", + "get_many", +] + + +def _get_span_description( + method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" +) -> str: + return _key_as_string(_get_safe_key(method_name, args, kwargs)) + + +def _patch_cache_method( + cache: "CacheHandler", + method_name: str, + address: "Optional[str]", + port: "Optional[int]", +) -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + original_method = getattr(cache, method_name) + + @ensure_integration_enabled(DjangoIntegration, original_method) + def _instrument_call( + cache: "CacheHandler", + method_name: str, + original_method: "Callable[..., Any]", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + address: "Optional[str]", + port: "Optional[int]", + ) -> "Any": + is_set_operation = method_name.startswith("set") + is_get_method = method_name == "get" + is_get_many_method = method_name == "get_many" + + op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET + description = _get_span_description(method_name, args, kwargs) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": op, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_attribute(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + else: + with sentry_sdk.start_span( + op=op, + name=description, + origin=DjangoIntegration.origin, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_data(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_data(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + + @functools.wraps(original_method) + def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": + return _instrument_call( + cache, method_name, original_method, args, kwargs, address, port + ) + + setattr(cache, method_name, sentry_method) + + +def _patch_cache( + cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None +) -> None: + if not hasattr(cache, "_sentry_patched"): + for method_name in METHODS_TO_INSTRUMENT: + _patch_cache_method(cache, method_name, address, port) + cache._sentry_patched = True + + +def _get_address_port( + settings: "dict[str, Any]", +) -> "tuple[Optional[str], Optional[int]]": + location = settings.get("LOCATION") + + # TODO: location can also be an array of locations + # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis + # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 + if not isinstance(location, str): + return None, None + + if "://" in location: + parsed_url = urlparse(location) + # remove the username and password from URL to not leak sensitive data. + address = "{}://{}{}".format( + parsed_url.scheme or "", + parsed_url.hostname or "", + parsed_url.path or "", + ) + port = parsed_url.port + else: + address = location + port = None + + return address, int(port) if port is not None else None + + +def should_enable_cache_spans() -> bool: + from sentry_sdk.integrations.django import DjangoIntegration + + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + from django.conf import settings + + return integration is not None and ( + (client.spotlight is not None and settings.DEBUG is True) + or integration.cache_spans is True + ) + + +def patch_caching() -> None: + if not hasattr(CacheHandler, "_sentry_patched"): + if DJANGO_VERSION < (3, 2): + original_get_item = CacheHandler.__getitem__ + + @functools.wraps(original_get_item) + def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": + cache = original_get_item(self, alias) + + if should_enable_cache_spans(): + from django.conf import settings + + address, port = _get_address_port( + settings.CACHES[alias or "default"] + ) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.__getitem__ = sentry_get_item + CacheHandler._sentry_patched = True + + else: + original_create_connection = CacheHandler.create_connection + + @functools.wraps(original_create_connection) + def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": + cache = original_create_connection(self, alias) + + if should_enable_cache_spans(): + address, port = _get_address_port(self.settings[alias or "default"]) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.create_connection = sentry_create_connection + CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py new file mode 100644 index 0000000000..a14ec96ff5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py @@ -0,0 +1,219 @@ +""" +Create spans from Django middleware invocations +""" + +from functools import wraps +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + F = TypeVar("F", bound=Callable[..., Any]) + +_import_string_should_wrap_middleware = ContextVar( + "import_string_should_wrap_middleware" +) + +DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) + +if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: + _asgi_middleware_mixin_factory = lambda _: object + iscoroutinefunction = lambda _: False +else: + from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction + + +def patch_django_middlewares() -> None: + from django.core.handlers import base + + old_import_string = base.import_string + + def sentry_patched_import_string(dotted_path: str) -> "Any": + rv = old_import_string(dotted_path) + + if _import_string_should_wrap_middleware.get(None): + rv = _wrap_middleware(rv, dotted_path) + + return rv + + base.import_string = sentry_patched_import_string + + old_load_middleware = base.BaseHandler.load_middleware + + def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": + _import_string_should_wrap_middleware.set(True) + try: + return old_load_middleware(*args, **kwargs) + finally: + _import_string_should_wrap_middleware.set(False) + + base.BaseHandler.load_middleware = sentry_patched_load_middleware + + +def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + def _check_middleware_span( + old_method: "Callable[..., Any]", + ) -> "Optional[Union[Span, StreamedSpan]]": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None or not integration.middleware_spans: + return None + + function_name = transaction_from_function(old_method) + + description = middleware_name + function_basename = getattr(old_method, "__name__", None) + if function_basename: + description = "{}.{}".format(description, function_basename) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + middleware_span: "Union[Span, StreamedSpan]" + if span_streaming: + middleware_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.MIDDLEWARE_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + else: + middleware_span = sentry_sdk.start_span( + op=OP.MIDDLEWARE_DJANGO, + name=description, + origin=DjangoIntegration.origin, + ) + middleware_span.set_tag("django.function_name", function_name) + middleware_span.set_tag("django.middleware_name", middleware_name) + + return middleware_span + + def _get_wrapped_method(old_method: "F") -> "F": + with capture_internal_exceptions(): + # Middleware hooks (e.g. `process_view`, `process_exception`) may be + # `async def` when the middleware is async. A synchronous wrapper + # would hide the coroutine from Django's `iscoroutinefunction` check, + # causing Django to call the hook synchronously and never await the + # returned coroutine. Wrap async hooks with an async wrapper so the + # wrapped method continues to report as a coroutine function. + if iscoroutinefunction is not None and iscoroutinefunction(old_method): + + async def async_sentry_wrapped_method( + *args: "Any", **kwargs: "Any" + ) -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return await old_method(*args, **kwargs) + + with middleware_span: + return await old_method(*args, **kwargs) + + sentry_wrapped_method = async_sentry_wrapped_method + + else: + + def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return old_method(*args, **kwargs) + + with middleware_span: + return old_method(*args, **kwargs) + + sentry_wrapped_method = sync_sentry_wrapped_method + + try: + # fails for __call__ of function on Python 2 (see py2.7-django-1.11) + sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) + + # Necessary for Django 3.1 + sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore + except Exception: + pass + + return sentry_wrapped_method # type: ignore + + return old_method + + class SentryWrappingMiddleware( + _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore + ): + sync_capable = getattr(middleware, "sync_capable", True) + async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( + middleware, "async_capable", False + ) + + def __init__( + self, + get_response: "Optional[Callable[..., Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> None: + if get_response: + self._inner = middleware(get_response, *args, **kwargs) + else: + self._inner = middleware(*args, **kwargs) + self.get_response = get_response + self._call_method = None + if self.async_capable: + super().__init__(get_response) + + # We need correct behavior for `hasattr()`, which we can only determine + # when we have an instance of the middleware we're wrapping. + def __getattr__(self, method_name: str) -> "Any": + if method_name not in ( + "process_request", + "process_view", + "process_template_response", + "process_response", + "process_exception", + ): + raise AttributeError() + + old_method = getattr(self._inner, method_name) + rv = _get_wrapped_method(old_method) + self.__dict__[method_name] = rv + return rv + + def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": + if hasattr(self, "async_route_check") and self.async_route_check(): + return self.__acall__(*args, **kwargs) + + f = self._call_method + if f is None: + self._call_method = f = self._inner.__call__ + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return f(*args, **kwargs) + + with middleware_span: + return f(*args, **kwargs) + + for attr in ( + "__name__", + "__module__", + "__qualname__", + ): + if hasattr(middleware, attr): + setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) + + return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py new file mode 100644 index 0000000000..7140ead782 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py @@ -0,0 +1,105 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from django.dispatch import Signal + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.django import DJANGO_VERSION +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Union + + +def _get_receiver_name(receiver: "Callable[..., Any]") -> str: + name = "" + + if hasattr(receiver, "__qualname__"): + name = receiver.__qualname__ + elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ + name = receiver.__name__ + elif hasattr( + receiver, "func" + ): # certain functions (like partials) dont have a name + if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): + name = "partial()" + + if ( + name == "" + ): # In case nothing was found, return the string representation (this is the slowest case) + return str(receiver) + + if hasattr(receiver, "__module__"): # prepend with module, if there is one + name = receiver.__module__ + "." + name + + return name + + +def patch_signals() -> None: + """ + Patch django signal receivers to create a span. + + This only wraps sync receivers. Django>=5.0 introduced async receivers, but + since we don't create transactions for ASGI Django, we don't wrap them. + """ + from sentry_sdk.integrations.django import DjangoIntegration + + old_live_receivers = Signal._live_receivers + + def _sentry_live_receivers( + self: "Signal", sender: "Any" + ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": + if DJANGO_VERSION >= (5, 0): + sync_receivers, async_receivers = old_live_receivers(self, sender) + else: + sync_receivers = old_live_receivers(self, sender) + async_receivers = [] + + def sentry_sync_receiver_wrapper( + receiver: "Callable[..., Any]", + ) -> "Callable[..., Any]": + @wraps(receiver) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + signal_name = _get_receiver_name(receiver) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + with sentry_sdk.traces.start_span( + name=signal_name, + attributes={ + "sentry.op": OP.EVENT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.CODE_FUNCTION_NAME: signal_name, + }, + ) as span: + return receiver(*args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.EVENT_DJANGO, + name=signal_name, + origin=DjangoIntegration.origin, + ) as span: + span.set_data("signal", signal_name) + return receiver(*args, **kwargs) + + return wrapper + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if ( + integration + and integration.signals_spans + and self not in integration.signals_denylist + ): + for idx, receiver in enumerate(sync_receivers): + sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + + if DJANGO_VERSION >= (5, 0): + return sync_receivers, async_receivers + else: + return sync_receivers + + Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py new file mode 100644 index 0000000000..5e23c258fb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py @@ -0,0 +1,52 @@ +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import qualname_from_function + +try: + # django.tasks were added in Django 6.0 + from django.tasks.base import Task +except ImportError: + Task = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +def patch_tasks() -> None: + if Task is None: + return + + old_task_enqueue = Task.enqueue + + @wraps(old_task_enqueue) + def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return old_task_enqueue(self, *args, **kwargs) + + name = qualname_from_function(self.func) or "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_task_enqueue(self, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin + ): + return old_task_enqueue(self, *args, **kwargs) + + Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py new file mode 100644 index 0000000000..5ab89d4a74 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py @@ -0,0 +1,209 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.template import TemplateSyntaxError +from django.utils.safestring import mark_safe + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Optional, Tuple + +try: + # support Django 1.9 + from django.template.base import Origin +except ImportError: + # backward compatibility + from django.template.loader import LoaderOrigin as Origin + + +def get_template_frame_from_exception( + exc_value: "Optional[BaseException]", +) -> "Optional[Dict[str, Any]]": + # As of Django 1.9 or so the new template debug thing showed up. + if hasattr(exc_value, "template_debug"): + return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore + + # As of r16833 (Django) all exceptions may contain a + # ``django_template_source`` attribute (rather than the legacy + # ``TemplateSyntaxError.source`` check) + if hasattr(exc_value, "django_template_source"): + return _get_template_frame_from_source( + exc_value.django_template_source # type: ignore + ) + + if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): + source = exc_value.source + if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): + return _get_template_frame_from_source(source) # type: ignore + + return None + + +def _get_template_name_description(template_name: str) -> str: + if isinstance(template_name, (list, tuple)): + if template_name: + return "[{}, ...]".format(template_name[0]) + else: + return template_name + + +def patch_templates() -> None: + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + real_rendered_content = SimpleTemplateResponse.rendered_content + + @property # type: ignore + @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) + def rendered_content(self: "SimpleTemplateResponse") -> str: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(self.template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_rendered_content.fget(self) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(self.template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", self.context_data) + return real_rendered_content.fget(self) + + SimpleTemplateResponse.rendered_content = rendered_content + + if DJANGO_VERSION < (1, 7): + return + import django.shortcuts + + real_render = django.shortcuts.render + + @functools.wraps(real_render) + @ensure_integration_enabled(DjangoIntegration, real_render) + def render( + request: "django.http.HttpRequest", + template_name: str, + context: "Optional[Dict[str, Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> "django.http.HttpResponse": + # Inject trace meta tags into template context + context = context or {} + if "sentry_trace_meta" not in context: + context["sentry_trace_meta"] = mark_safe( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_render(request, template_name, context, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", context) + return real_render(request, template_name, context, *args, **kwargs) + + django.shortcuts.render = render + + +def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": + if debug is None: + return None + + lineno = debug["line"] + filename = debug["name"] + if filename is None: + filename = "" + + pre_context = [] + post_context = [] + context_line = None + + for i, line in debug["source_lines"]: + if i < lineno: + pre_context.append(line) + elif i > lineno: + post_context.append(line) + else: + context_line = line + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + "in_app": True, + } + + +def _linebreak_iter(template_source: str) -> "Iterator[int]": + yield 0 + p = template_source.find("\n") + while p >= 0: + yield p + 1 + p = template_source.find("\n", p + 1) + + +def _get_template_frame_from_source( + source: "Tuple[Origin, Tuple[int, int]]", +) -> "Optional[Dict[str, Any]]": + if not source: + return None + + origin, (start, end) = source + filename = getattr(origin, "loadname", None) + if filename is None: + filename = "" + template_source = origin.reload() + lineno = None + upto = 0 + pre_context = [] + post_context = [] + context_line = None + + for num, next in enumerate(_linebreak_iter(template_source)): + line = template_source[upto:next] + if start >= upto and end <= next: + lineno = num + context_line = line + elif lineno is None: + pre_context.append(line) + else: + post_context.append(line) + + upto = next + + if context_line is None or lineno is None: + return None + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py new file mode 100644 index 0000000000..192f0765e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py @@ -0,0 +1,154 @@ +""" +Copied from raven-python. + +Despite being called "legacy" in some places this resolver is very much still +in use. +""" + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from re import Pattern + from typing import Dict, List, Optional, Tuple, Union + + from django.urls.resolvers import URLPattern, URLResolver + +from django import VERSION as DJANGO_VERSION + +if DJANGO_VERSION >= (2, 0): + from django.urls.resolvers import RoutePattern +else: + RoutePattern = None + +try: + from django.urls import get_resolver +except ImportError: + from django.core.urlresolvers import get_resolver + + +def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": + """Utility method for django's deprecated resolver.regex""" + try: + regex = resolver_or_pattern.regex + except AttributeError: + regex = resolver_or_pattern.pattern.regex + return regex + + +class RavenResolver: + _new_style_group_matcher = re.compile( + r"<(?:([^>:]+):)?([^>]+)>" + ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 + _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") + _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") + _non_named_group_matcher = re.compile(r"\([^\)]+\)") + # [foo|bar|baz] + _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") + _camel_re = re.compile(r"([A-Z]+)([a-z])") + + _cache: "Dict[URLPattern, str]" = {} + + def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: + r""" + Clean up urlpattern regexes into something readable by humans: + + From: + > "^(?P\w+)/athletes/(?P\w+)/$" + + To: + > "{sport_slug}/athletes/{athlete_slug}/" + """ + # "new-style" path patterns can be parsed directly without turning them + # into regexes first + if ( + RoutePattern is not None + and hasattr(pattern, "pattern") + and isinstance(pattern.pattern, RoutePattern) + ): + return self._new_style_group_matcher.sub( + lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) + ) + + result = get_regex(pattern).pattern + + # remove optional params + # TODO(dcramer): it'd be nice to change these into [%s] but it currently + # conflicts with the other rules because we're doing regexp matches + # rather than parsing tokens + result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) + + # handle named groups first + result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) + + # handle non-named groups + result = self._non_named_group_matcher.sub("{var}", result) + + # handle optional params + result = self._either_option_matcher.sub(lambda m: m.group(1), result) + + # clean up any outstanding regex-y characters. + result = ( + result.replace("^", "") + .replace("$", "") + .replace("?", "") + .replace("\\A", "") + .replace("\\Z", "") + .replace("//", "/") + .replace("\\", "") + ) + + return result + + def _resolve( + self, + resolver: "URLResolver", + path: str, + parents: "Optional[List[URLResolver]]" = None, + ) -> "Optional[str]": + match = get_regex(resolver).search(path) # Django < 2.0 + + if not match: + return None + + if parents is None: + parents = [resolver] + elif resolver not in parents: + parents = parents + [resolver] + + new_path = path[match.end() :] + for pattern in resolver.url_patterns: + # this is an include() + if not pattern.callback: + match_ = self._resolve(pattern, new_path, parents) + if match_: + return match_ + continue + elif not get_regex(pattern).search(new_path): + continue + + try: + return self._cache[pattern] + except KeyError: + pass + + prefix = "".join(self._simplify(p) for p in parents) + result = prefix + self._simplify(pattern) + if not result.startswith("/"): + result = "/" + result + self._cache[pattern] = result + return result + + return None + + def resolve( + self, + path: str, + urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, + ) -> "Optional[str]": + resolver = get_resolver(urlconf) + match = self._resolve(resolver, path) + return match + + +LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py new file mode 100644 index 0000000000..cf3012a75e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py @@ -0,0 +1,127 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +try: + from asyncio import iscoroutinefunction +except ImportError: + iscoroutinefunction = None # type: ignore + + +try: + from sentry_sdk.integrations.django.asgi import wrap_async_view +except (ImportError, SyntaxError): + wrap_async_view = None # type: ignore + + +def patch_views() -> None: + from django.core.handlers.base import BaseHandler + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + old_make_view_atomic = BaseHandler.make_view_atomic + old_render = SimpleTemplateResponse.render + + def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="serialize response", + attributes={ + "sentry.op": OP.VIEW_RESPONSE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_render(self) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RESPONSE_RENDER, + name="serialize response", + origin=DjangoIntegration.origin, + ): + return old_render(self) + + @functools.wraps(old_make_view_atomic) + def sentry_patched_make_view_atomic( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + callback = old_make_view_atomic(self, *args, **kwargs) + + # XXX: The wrapper function is created for every request. Find more + # efficient way to wrap views (or build a cache?) + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is not None: + is_async_view = ( + iscoroutinefunction is not None + and wrap_async_view is not None + and iscoroutinefunction(callback) + ) + if is_async_view: + sentry_wrapped_callback = wrap_async_view(callback) + else: + sentry_wrapped_callback = _wrap_sync_view(callback) + + else: + sentry_wrapped_callback = callback + + return sentry_wrapped_callback + + SimpleTemplateResponse.render = sentry_patched_render + BaseHandler.make_view_atomic = sentry_patched_make_view_atomic + + +def _wrap_sync_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + # set the active thread id to the handler thread for sync views + # this isn't necessary for async views since that runs on main + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return callback(request, *args, **kwargs) + + return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py new file mode 100644 index 0000000000..310766ee3a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py @@ -0,0 +1,246 @@ +import json +from typing import TypeVar + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, +) + +R = TypeVar("R") + +try: + from dramatiq.broker import Broker + from dramatiq.errors import Retry + from dramatiq.message import Message + from dramatiq.middleware import Middleware, default_middleware +except ImportError: + raise DidNotEnable("Dramatiq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Union + + from sentry_sdk._types import Event, Hint + + +class DramatiqIntegration(Integration): + """ + Dramatiq integration for Sentry + + Please make sure that you call `sentry_sdk.init` *before* initializing + your broker, as it monkey patches `Broker.__init__`. + + This integration was originally developed and maintained + by https://github.com/jacobsvante and later donated to the Sentry + project. + """ + + identifier = "dramatiq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_dramatiq_broker() + + +def _patch_dramatiq_broker() -> None: + original_broker__init__ = Broker.__init__ + + def sentry_patched_broker__init__( + self: "Broker", *args: "Any", **kw: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + + try: + middleware = kw.pop("middleware") + except KeyError: + # Unfortunately Broker and StubBroker allows middleware to be + # passed in as positional arguments, whilst RabbitmqBroker and + # RedisBroker does not. + if len(args) == 1: + middleware = args[0] + args = () + else: + middleware = None + + if middleware is None: + middleware = list(m() for m in default_middleware) + else: + middleware = list(middleware) + + if integration is not None: + middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] + middleware.insert(0, SentryMiddleware()) + + kw["middleware"] = middleware + original_broker__init__(self, *args, **kw) + + Broker.__init__ = sentry_patched_broker__init__ + + +class SentryMiddleware(Middleware): # type: ignore[misc] + """ + A Dramatiq middleware that automatically captures and sends + exceptions to Sentry. + + This is automatically added to every instantiated broker via the + DramatiqIntegration. + """ + + SENTRY_HEADERS_NAME = "_sentry_headers" + + def before_enqueue( + self, broker: "Broker", message: "Message[R]", delay: int + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + message.options[self.SENTRY_HEADERS_NAME] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + + def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DramatiqIntegration) + if integration is None: + return + + message._scope_manager = sentry_sdk.isolation_scope() + scope = message._scope_manager.__enter__() + scope.clear_breadcrumbs() + scope.set_extra("dramatiq_message_id", message.message_id) + scope.add_event_processor(_make_message_event_processor(message, integration)) + + sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} + if "retries" in message.options: + # start new trace in case of retrying + sentry_headers = {} + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(sentry_headers) + span = sentry_sdk.traces.start_span( + name=message.actor_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_DRAMATIQ, + "sentry.origin": DramatiqIntegration.origin, + "sentry.span.source": SegmentSource.TASK.value, + }, + parent_span=None, + ) + message._sentry_span_ctx = span + else: + transaction = continue_trace( + sentry_headers, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + origin=DramatiqIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + sentry_sdk.start_transaction( + transaction, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + ) + transaction.__enter__() + message._sentry_span_ctx = transaction + + def after_process_message( + self, + broker: "Broker", + message: "Message[R]", + *, + result: "Optional[Any]" = None, + exception: "Optional[Exception]" = None, + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + actor = broker.get_actor(message.actor_name) + throws = message.options.get("throws") or actor.options.get("throws") + + scope_manager = message._scope_manager + span_ctx = getattr(message, "_sentry_span_ctx", None) + if span_ctx is None: + return None + + is_event_capture_required = ( + exception is not None + and not (throws and isinstance(exception, throws)) + and not isinstance(exception, Retry) + ) + if not is_event_capture_required: + # normal transaction finish + span_ctx.__exit__(None, None, None) + scope_manager.__exit__(None, None, None) + return + + event, hint = event_from_exception( + exception, # type: ignore[arg-type] + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": DramatiqIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + # transaction error + span_ctx.__exit__(type(exception), exception, None) + scope_manager.__exit__(type(exception), exception, None) + + after_skip_message = after_process_message + + +def _make_message_event_processor( + message: "Message[R]", integration: "DramatiqIntegration" +) -> "Callable[[Event, Hint], Optional[Event]]": + def inner(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + DramatiqMessageExtractor(message).extract_into_event(event) + + return event + + return inner + + +class DramatiqMessageExtractor: + def __init__(self, message: "Message[R]") -> None: + self.message_data = dict(message.asdict()) + + def content_length(self) -> int: + return len(json.dumps(self.message_data)) + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + contexts = event.setdefault("contexts", {}) + request_info = contexts.setdefault("dramatiq", {}) + request_info["type"] = "dramatiq" + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + if not request_body_within_bounds(client, self.content_length()): + data = AnnotatedValue.removed_because_over_size_limit() + else: + data = self.message_data + + request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py new file mode 100644 index 0000000000..6bbc61000d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py @@ -0,0 +1,76 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + + +class ExcepthookIntegration(Integration): + identifier = "excepthook" + + always_run = False + + def __init__(self, always_run: bool = False) -> None: + if not isinstance(always_run, bool): + raise ValueError( + "Invalid value for always_run: %s (must be type boolean)" + % (always_run,) + ) + self.always_run = always_run + + @staticmethod + def setup_once() -> None: + sys.excepthook = _make_excepthook(sys.excepthook) + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + if _should_send(integration.always_run): + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "excepthook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook + + +def _should_send(always_run: bool = False) -> bool: + if always_run: + return True + + if hasattr(sys, "ps1"): + # Disable the excepthook for interactive Python shells, otherwise + # every typo gets sent to Sentry. + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py new file mode 100644 index 0000000000..4473fcc435 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + + +class ExecutingIntegration(Integration): + identifier = "executing" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + frame = tb.tb_frame + source = Source.for_frame(frame) + sentry_frame["function"] = source.code_qualname(frame.f_code) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py new file mode 100644 index 0000000000..7a595bcf2a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + + from sentry_sdk._types import Event, EventProcessor + +# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` +# and `falcon.API` to `falcon.App` + +try: + import falcon # type: ignore + from falcon import __version__ as FALCON_VERSION +except ImportError: + raise DidNotEnable("Falcon not installed") + +try: + import falcon.app_helpers # type: ignore + + falcon_helpers = falcon.app_helpers + falcon_app_class = falcon.App + FALCON3 = True +except ImportError: + import falcon.api_helpers # type: ignore + + falcon_helpers = falcon.api_helpers + falcon_app_class = falcon.API + FALCON3 = False + + +_FALCON_UNSET: "Optional[object]" = None +if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ + with capture_internal_exceptions(): + from falcon.request import ( # type: ignore[import-not-found, no-redef] + _UNSET as _FALCON_UNSET, + ) + + +class FalconRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, Any]": + return self.request.env + + def cookies(self) -> "Dict[str, Any]": + return self.request.cookies + + def form(self) -> None: + return None # No such concept in Falcon + + def files(self) -> None: + return None # No such concept in Falcon + + def raw_data(self) -> "Optional[str]": + # As request data can only be read once we won't make this available + # to Sentry. Just send back a dummy string in case there was a + # content length. + # TODO(jmagnusson): Figure out if there's a way to support this + content_length = self.content_length() + if content_length > 0: + return "[REQUEST_CONTAINING_RAW_DATA]" + else: + return None + + def json(self) -> "Optional[Dict[str, Any]]": + # fallback to cached_media = None if self.request._media is not available + cached_media = None + with capture_internal_exceptions(): + # self.request._media is the cached self.request.media + # value. It is only available if self.request.media + # has already been accessed. Therefore, reading + # self.request._media will not exhaust the raw request + # stream (self.request.bounded_stream) because it has + # already been read if self.request._media is set. + cached_media = self.request._media + + if cached_media is not _FALCON_UNSET: + return cached_media + + return None + + +class SentryFalconMiddleware: + """Captures exceptions in Falcon requests and send to Sentry""" + + def process_request( + self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return + + scope = sentry_sdk.get_isolation_scope() + scope._name = "falcon" + scope.add_event_processor(_make_request_event_processor(req, integration)) + + def process_resource( + self, req: "Any", resp: "Any", resource: "Any", params: "Any" + ) -> None: + """ + Sets the segment name and source as the route is resolved when this runs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FalconIntegration) + if integration is None or not has_span_streaming_enabled(client.options): + return + + name_for_style = { + "uri_template": req.uri_template, + "path": req.path, + } + name = name_for_style[integration.transaction_style] + source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] + sentry_sdk.set_transaction_name(name, source) + + +TRANSACTION_STYLE_VALUES = ("uri_template", "path") + + +class FalconIntegration(Integration): + identifier = "falcon" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "uri_template") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + version = parse_version(FALCON_VERSION) + _check_minimum_version(FalconIntegration, version) + + _patch_wsgi_app() + _patch_handle_exception() + _patch_prepare_middleware() + + +def _patch_wsgi_app() -> None: + original_wsgi_app = falcon_app_class.__call__ + + def sentry_patched_wsgi_app( + self: "falcon.API", env: "Any", start_response: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return original_wsgi_app(self, env, start_response) + + sentry_wrapped = SentryWsgiMiddleware( + lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), + span_origin=FalconIntegration.origin, + ) + + return sentry_wrapped(env, start_response) + + falcon_app_class.__call__ = sentry_patched_wsgi_app + + +def _patch_handle_exception() -> None: + original_handle_exception = falcon_app_class._handle_exception + + @ensure_integration_enabled(FalconIntegration, original_handle_exception) + def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": + # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception + # method signature from `(ex, req, resp, params)` to + # `(req, resp, ex, params)` + ex = response = None + with capture_internal_exceptions(): + ex = next(argument for argument in args if isinstance(argument, Exception)) + response = next( + argument for argument in args if isinstance(argument, falcon.Response) + ) + + was_handled = original_handle_exception(self, *args) + + if ex is None or response is None: + # Both ex and response should have a non-None value at this point; otherwise, + # there is an error with the SDK that will have been captured in the + # capture_internal_exceptions block above. + return was_handled + + if _exception_leads_to_http_5xx(ex, response): + event, hint = event_from_exception( + ex, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "falcon", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return was_handled + + falcon_app_class._handle_exception = sentry_patched_handle_exception + + +def _patch_prepare_middleware() -> None: + original_prepare_middleware = falcon_helpers.prepare_middleware + + def sentry_patched_prepare_middleware( + middleware: "Any" = None, + independent_middleware: "Any" = False, + asgi: bool = False, + ) -> "Any": + if asgi: + # We don't support ASGI Falcon apps, so we don't patch anything here + return original_prepare_middleware(middleware, independent_middleware, asgi) + + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is not None: + middleware = [SentryFalconMiddleware()] + (middleware or []) + + # We intentionally omit the asgi argument here, since the default is False anyways, + # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. + return original_prepare_middleware(middleware, independent_middleware) + + falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware + + +def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: + is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( + "5" + ) + is_unhandled_error = not isinstance( + ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) + ) + + # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response + # at the stage where we capture it is listed as 200, even though we would expect to see a 500 + # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to + # only perform this check on Falcon 3+, despite the risk that some handled errors might be + # reported to Sentry as unhandled on Falcon 2. + return (is_server_error or is_unhandled_error) and ( + not FALCON3 or _has_http_5xx_status(response) + ) + + +def _has_http_5xx_status(response: "falcon.Response") -> bool: + return response.status.startswith("5") + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "falcon.Request" +) -> None: + name_for_style = { + "uri_template": request.uri_template, + "path": request.path, + } + event["transaction"] = name_for_style[transaction_style] + event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} + + +def _make_request_event_processor( + req: "falcon.Request", integration: "FalconIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, req) + + with capture_internal_exceptions(): + FalconRequestExtractor(req).extract_into_event(event) + + return event + + return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py new file mode 100644 index 0000000000..c7b97c88b1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py @@ -0,0 +1,201 @@ +import sys +from copy import deepcopy +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import transaction_from_function + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Dict + + from sentry_sdk._types import Event + +try: + from sentry_sdk.integrations.starlette import ( + StarletteIntegration, + StarletteRequestExtractor, + _get_cached_request_body_attribute, + ) +except DidNotEnable: + raise DidNotEnable("Starlette is not installed") + +try: + import fastapi # type: ignore +except ImportError: + raise DidNotEnable("FastAPI is not installed") + + +_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +class FastApiIntegration(StarletteIntegration): + identifier = "fastapi" + + @staticmethod + def setup_once() -> None: + patch_get_request_handler() + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or "" + + elif transaction_style == "url": + route = request.scope.get("route") + + if route: + # FastAPI >= 0.137 stores the prefix-resolved path on an + # effective_route_context in scope["fastapi"], while + # scope["route"].path holds the unprefixed original. + # Prefer the effective context path when available. + effective_route_context = request.scope.get("fastapi", {}).get( + "effective_route_context" + ) + context_path = getattr(effective_route_context, "path", None) + + if context_path: + name = context_path + else: + path = getattr(route, "path", None) + if path is not None: + name = path + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name(name, source=source) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FastApiIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, Dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if info: + if "cookies" in info and should_send_default_pii(): + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = FastApiIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_get_request_handler() -> None: + old_get_request_handler = fastapi.routing.get_request_handler + + def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": + dependant = kwargs.get("dependant") + if ( + dependant + and dependant.call is not None + and not iscoroutinefunction(dependant.call) + # FastAPI >= 0.137 calls get_request_handler() on every request + # (router-tree traversal) rather than once at registration. Guard + # against accumulating _sentry_call wrappers on the shared + # dependant object, which would cause a RecursionError after ~987 + # requests as the call chain grows past Python's recursion limit. + and not getattr(dependant.call, "_sentry_is_patched", False) + ): + old_call = dependant.call + + @wraps(old_call) + def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": + current_scope = sentry_sdk.get_current_scope() + + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_call(*args, **kwargs) + + _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] + dependant.call = _sentry_call + + old_app = old_get_request_handler(*args, **kwargs) + + async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_app, *args, **kwargs) + + return _sentry_app + + fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py new file mode 100644 index 0000000000..1902091fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py @@ -0,0 +1,281 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Union + + from werkzeug.datastructures import FileStorage, ImmutableMultiDict + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + + +try: + import flask_login # type: ignore +except ImportError: + flask_login = None + +try: + from flask import Flask, Request # type: ignore + from flask import request as flask_request + from flask.signals import ( + before_render_template, + got_request_exception, + request_started, + ) + from markupsafe import Markup +except ImportError: + raise DidNotEnable("Flask is not installed") + +try: + import blinker # noqa +except ImportError: + raise DidNotEnable("blinker is not installed") + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class FlaskIntegration(Integration): + identifier = "flask" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + try: + from quart import Quart # type: ignore + + if Flask == Quart: + # This is Quart masquerading as Flask, don't enable the Flask + # integration. See https://github.com/getsentry/sentry-python/issues/2709 + raise DidNotEnable( + "This is not a Flask app but rather Quart pretending to be Flask" + ) + except ImportError: + pass + + version = package_version("flask") + _check_minimum_version(FlaskIntegration, version) + + before_render_template.connect(_add_sentry_trace) + request_started.connect(_request_started) + got_request_exception.connect(_capture_exception) + + old_app = Flask.__call__ + + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return old_app(self, environ, start_response) + + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=FlaskIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + Flask.__call__ = sentry_patched_wsgi_app + + +def _add_sentry_trace( + sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" +) -> None: + if "sentry_trace" in context: + return + + scope = sentry_sdk.get_current_scope() + trace_meta = Markup(scope.trace_propagation_meta()) + context["sentry_trace"] = trace_meta # for backwards compatibility + context["sentry_trace_meta"] = trace_meta + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + scope.set_transaction_name( + name_for_style[transaction_style], + source=SOURCE_FOR_STYLE[transaction_style], + ) + except Exception: + pass + + +def _request_started(app: "Flask", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return + + request = flask_request._get_current_object() + + # Set the transaction name and source here, + # but rely on WSGI middleware to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + scope.set_user(user_properties) + + evt_processor = _make_request_event_processor(app, request, integration) + scope.add_event_processor(evt_processor) + + +class FlaskRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[Any, Any]": + return { + k: v[0] if isinstance(v, list) and len(v) == 1 else v + for k, v in self.request.cookies.items() + } + + def raw_data(self) -> bytes: + return self.request.get_data() + + def form(self) -> "ImmutableMultiDict[str, Any]": + return self.request.form + + def files(self) -> "ImmutableMultiDict[str, Any]": + return self.request.files + + def is_json(self) -> bool: + return self.request.is_json + + def json(self) -> "Any": + return self.request.get_json(silent=True) + + def size_of_file(self, file: "FileStorage") -> int: + return file.content_length + + +def _make_request_event_processor( + app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + FlaskRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _add_user_to_event(event) + + return event + + return inner + + +@ensure_integration_enabled(FlaskIntegration) +def _capture_exception( + sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "flask", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_flask_user_properties() -> "Dict[str, str]": + if flask_login is None: + return {} + + user = flask_login.current_user + if user is None: + return {} + + properties = {} + + try: + user_id = user.get_id() + if user_id is not None: + properties["id"] = user_id + except AttributeError: + # might happen if: + # - flask_login could not be imported + # - flask_login is not configured + # - no user is logged in + pass + + # The following attribute accesses are ineffective for the general + # Flask-Login case, because the User interface of Flask-Login does not + # care about anything but the ID. However, Flask-User (based on + # Flask-Login) documents a few optional extra attributes. + # + # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names + try: + if user.email is not None: + properties["email"] = user.email + except Exception: + pass + + try: + if user.username is not None: + properties["username"] = user.username + except Exception: + pass + + return properties + + +def _add_user_to_event(event: "Event") -> None: + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + user_info = event.setdefault("user", {}) + for key, value in user_properties.items(): + user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py new file mode 100644 index 0000000000..91a62b3a81 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py @@ -0,0 +1,286 @@ +import functools +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +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 +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +# Constants +TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_func(func: "F") -> "F": + @functools.wraps(func) + def sentry_func( + functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(GcpIntegration) + if integration is None: + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = environ.get("FUNCTION_TIMEOUT_SEC") + if not configured_time: + logger.debug( + "The configured timeout could not be fetched from Cloud Functions configuration." + ) + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = int(configured_time) + + initial_time = datetime.now(timezone.utc) + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + gcp_event, configured_time, initial_time + ) + ) + scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) + timeout_thread = None + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = configured_time - TIMEOUT_WARNING_BUFFER + + timeout_thread = TimeoutThread( + waiting_time, + configured_time, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = {} + header_attributes: "dict[str, Any]" = {} + if hasattr(gcp_event, "headers"): + headers = gcp_event.headers + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + additional_attributes = {} + 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" + ) + + sampling_context = { + "gcp_env": { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + }, + "gcp_event": gcp_event, + } + + function_name = environ.get("FUNCTION_NAME", "") + + if environ.get("GCP_PROJECT"): + additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") + + if environ.get("FUNCTION_IDENTITY"): + additional_attributes["faas.identity"] = environ.get( + "FUNCTION_IDENTITY" + ) + + if environ.get("ENTRY_POINT"): + additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_GCP, + "sentry.origin": GcpIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.provider": CLOUD_PROVIDER.GCP, + "faas.name": function_name, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_GCP, + name=environ.get("FUNCTION_NAME", ""), + source=TransactionSource.COMPONENT, + origin=GcpIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return func(functionhandler, gcp_event, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "gcp", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + # Flush out the event queue + client.flush() + + return sentry_func # type: ignore + + +class GcpIntegration(Integration): + identifier = "gcp" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + import __main__ as gcp_functions + + if not hasattr(gcp_functions, "worker_v1"): + logger.warning( + "GcpIntegration currently supports only Python 3.7 runtime environment." + ) + return + + worker1 = gcp_functions.worker_v1 + + worker1.FunctionHandler.invoke_user_function = _wrap_func( + worker1.FunctionHandler.invoke_user_function + ) + + +def _make_request_event_processor( + gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + final_time = datetime.now(timezone.utc) + time_diff = final_time - initial_time + + execution_duration_in_millis = time_diff / timedelta(milliseconds=1) + + extra = event.setdefault("extra", {}) + extra["google cloud functions"] = { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + "execution_duration_in_millis": execution_duration_in_millis, + "configured_timeout_in_seconds": configured_timeout, + } + + extra["google cloud logs"] = { + "url": _get_google_cloud_logs_url(final_time), + } + + request = event.get("request", {}) + + request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) + + if hasattr(gcp_event, "method"): + request["method"] = gcp_event.method + + if hasattr(gcp_event, "query_string"): + request["query_string"] = gcp_event.query_string.decode( + "utf-8", errors="replace" + ) + + if hasattr(gcp_event, "headers"): + request["headers"] = _filter_headers(gcp_event.headers) + + if should_send_default_pii(): + if hasattr(gcp_event, "data"): + request["data"] = gcp_event.data + else: + if hasattr(gcp_event, "data"): + # Unfortunately couldn't find a way to get structured body from GCP + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + event["request"] = deepcopy(request) + + return event + + return event_processor + + +def _get_google_cloud_logs_url(final_time: "datetime") -> str: + """ + Generates a Google Cloud Logs console URL based on the environment variables + Arguments: + final_time {datetime} -- Final time + Returns: + str -- Google Cloud Logs Console URL to logs. + """ + hour_ago = final_time - timedelta(hours=1) + formatstring = "%Y-%m-%dT%H:%M:%SZ" + + url = ( + "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" + "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" + "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" + "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" + "&interval=PT1H&scrollTimestamp={timestamp_end}" + ).format( + project=environ.get("GCP_PROJECT"), + function_name=environ.get("FUNCTION_NAME"), + region=environ.get("FUNCTION_REGION"), + timestamp_end=final_time.strftime(formatstring), + timestamp_start=hour_ago.strftime(formatstring), + ) + + return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py new file mode 100644 index 0000000000..4be0f479bc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py @@ -0,0 +1,96 @@ +import re +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + +# function is everything between index at @ +# and then we match on the @ plus the hex val +FUNCTION_RE = r"[^@]+?" +HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" + +_FRAME_RE_PATTERN = r""" +^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ +""".format( + FUNCTION_RE=FUNCTION_RE, + HEX_ADDRESS=HEX_ADDRESS, +) + +FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) + + +class GnuBacktraceIntegration(Integration): + identifier = "gnu_backtrace" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + return _process_gnu_backtrace(event, hint) + + +def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception in values: + frames = exception.get("stacktrace", {}).get("frames", []) + if not frames: + continue + + msg = exception.get("value", None) + if not msg: + continue + + additional_frames = [] + new_msg = [] + + for line in msg.splitlines(): + match = FRAME_RE.match(line) + if match: + additional_frames.append( + ( + int(match.group("index")), + { + "package": match.group("package") or None, + "function": match.group("function") or None, + "platform": "native", + }, + ) + ) + else: + # Put garbage lines back into message, not sure what to do with them. + new_msg.append(line) + + if additional_frames: + additional_frames.sort(key=lambda x: -x[0]) + for _, frame in additional_frames: + frames.append(frame) + + new_msg.append("") + exception["value"] = "\n".join(new_msg) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py new file mode 100644 index 0000000000..45652c3f71 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py @@ -0,0 +1,457 @@ +from functools import wraps +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + List, +) + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import SPANSTATUS +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.genai.models import AsyncModels, Models +except ImportError: + raise DidNotEnable("google-genai not installed") + + +from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN +from .streaming import ( + accumulate_streaming_response, + set_span_data_for_streaming_response, +) +from .utils import ( + _capture_exception, + prepare_embed_content_args, + prepare_generate_content_args, + set_span_data_for_embed_request, + set_span_data_for_embed_response, + set_span_data_for_request, + set_span_data_for_response, +) + + +class GoogleGenAIIntegration(Integration): + identifier = IDENTIFIER + origin = ORIGIN + + def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Patch sync methods + Models.generate_content = _wrap_generate_content(Models.generate_content) + Models.generate_content_stream = _wrap_generate_content_stream( + Models.generate_content_stream + ) + Models.embed_content = _wrap_embed_content(Models.embed_content) + + # Patch async methods + AsyncModels.generate_content = _wrap_async_generate_content( + AsyncModels.generate_content + ) + AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( + AsyncModels.generate_content_stream + ) + AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) + + +def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = f(self, *args, **kwargs) + + # Create wrapper iterator to accumulate responses + def new_iterator() -> "Iterator[Any]": + chunks: "List[Any]" = [] + try: + for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_generate_content_stream + + +def _wrap_async_generate_content_stream( + f: "Callable[..., Any]", +) -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = await f(self, *args, **kwargs) + + # Create wrapper async iterator to accumulate responses + async def new_async_iterator() -> "AsyncIterator[Any]": + chunks: "List[Any]" = [] + try: + async for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_async_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_async_generate_content_stream + + +def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_generate_content + + +def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_async_generate_content + + +def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_embed_content + + +def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_embed_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py new file mode 100644 index 0000000000..5b53ebf0e2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py @@ -0,0 +1,16 @@ +GEN_AI_SYSTEM = "gcp.gemini" + +# Mapping of tool attributes to their descriptions +# These are all tools that are available in the Google GenAI API +TOOL_ATTRIBUTES_MAP = { + "google_search_retrieval": "Google Search retrieval tool", + "google_search": "Google Search tool", + "retrieval": "Retrieval tool", + "enterprise_web_search": "Enterprise web search tool", + "google_maps": "Google Maps tool", + "code_execution": "Code execution tool", + "computer_use": "Computer use tool", +} + +IDENTIFIER = "google_genai" +ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py new file mode 100644 index 0000000000..8414ea4f21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py @@ -0,0 +1,172 @@ +from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union + +from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import ( + safe_serialize, +) + +from .utils import ( + UsageData, + extract_contents_text, + extract_finish_reasons, + extract_tool_calls, + extract_usage_data, +) + +if TYPE_CHECKING: + from google.genai.types import GenerateContentResponse + + from sentry_sdk.tracing import Span + + +class AccumulatedResponse(TypedDict): + id: "Optional[str]" + model: "Optional[str]" + text: str + finish_reasons: "List[str]" + tool_calls: "List[dict[str, Any]]" + usage_metadata: "Optional[UsageData]" + + +def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": + return UsageData( + input_tokens=max(self["input_tokens"], other["input_tokens"]), + output_tokens=max(self["output_tokens"], other["output_tokens"]), + input_tokens_cached=max( + self["input_tokens_cached"], other["input_tokens_cached"] + ), + output_tokens_reasoning=max( + self["output_tokens_reasoning"], other["output_tokens_reasoning"] + ), + total_tokens=max(self["total_tokens"], other["total_tokens"]), + ) + + +def accumulate_streaming_response( + chunks: "List[GenerateContentResponse]", +) -> "AccumulatedResponse": + """Accumulate streaming chunks into a single response-like object.""" + accumulated_text = [] + finish_reasons = [] + tool_calls = [] + usage_data = None + response_id = None + model = None + + for chunk in chunks: + # Extract text and tool calls + if getattr(chunk, "candidates", None): + for candidate in getattr(chunk, "candidates", []): + if hasattr(candidate, "content") and getattr( + candidate.content, "parts", [] + ): + extracted_text = extract_contents_text(candidate.content) + if extracted_text: + accumulated_text.append(extracted_text) + + extracted_finish_reasons = extract_finish_reasons(chunk) + if extracted_finish_reasons: + finish_reasons.extend(extracted_finish_reasons) + + extracted_tool_calls = extract_tool_calls(chunk) + if extracted_tool_calls: + tool_calls.extend(extracted_tool_calls) + + # Use last possible chunk, in case of interruption, and + # gracefully handle missing intermediate tokens by taking maximum + # with previous token reporting. + chunk_usage_data = extract_usage_data(chunk) + usage_data = ( + chunk_usage_data + if usage_data is None + else element_wise_usage_max(usage_data, chunk_usage_data) + ) + + accumulated_response = AccumulatedResponse( + text="".join(accumulated_text), + finish_reasons=finish_reasons, + tool_calls=tool_calls, + usage_metadata=usage_data, + id=response_id, + model=model, + ) + + return accumulated_response + + +def set_span_data_for_streaming_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + accumulated_response: "AccumulatedResponse", +) -> None: + """Set span data for accumulated streaming response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if ( + should_send_default_pii() + and integration.include_prompts + and accumulated_response.get("text") + ): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TEXT, + safe_serialize([accumulated_response["text"]]), + ) + + if accumulated_response.get("finish_reasons"): + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + accumulated_response["finish_reasons"], + ) + + if accumulated_response.get("tool_calls"): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(accumulated_response["tool_calls"]), + ) + + response_id = accumulated_response.get("id") + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + + response_model = accumulated_response.get("model") + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if accumulated_response["usage_metadata"] is None: + return + + if accumulated_response["usage_metadata"]["input_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + accumulated_response["usage_metadata"]["input_tokens"], + ) + + if accumulated_response["usage_metadata"]["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + accumulated_response["usage_metadata"]["input_tokens_cached"], + ) + + if accumulated_response["usage_metadata"]["output_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + accumulated_response["usage_metadata"]["output_tokens"], + ) + + if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + accumulated_response["usage_metadata"]["output_tokens_reasoning"], + ) + + if accumulated_response["usage_metadata"]["total_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + accumulated_response["usage_metadata"]["total_tokens"], + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py new file mode 100644 index 0000000000..464a812680 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py @@ -0,0 +1,1118 @@ +import copy +import inspect +import json +from functools import wraps +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + TypedDict, + Union, +) + +from google.genai.types import Content, GenerateContentConfig, Part, PartDict + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.utils import ( + get_modality_from_mime_type, + normalize_message_roles, + set_data_normalized, + transform_google_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + safe_serialize, +) + +from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP + +if TYPE_CHECKING: + from google.genai.types import ( + ContentListUnion, + ContentUnion, + ContentUnionDict, + EmbedContentResponse, + GenerateContentResponse, + Model, + Tool, + ) + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +_is_PIL_available = False +try: + from PIL import Image as PILImage # type: ignore[import-not-found] + + _is_PIL_available = True +except ImportError: + pass + +# Keys to use when checking to see if a dict provided by the user +# is Part-like (as opposed to a Content or multi-turn conversation entry). +_PART_DICT_KEYS = PartDict.__optional_keys__ + + +class UsageData(TypedDict): + """Structure for token usage data.""" + + input_tokens: int + input_tokens_cached: int + output_tokens: int + output_tokens_reasoning: int + total_tokens: int + + +def extract_usage_data( + response: "Union[GenerateContentResponse, dict[str, Any]]", +) -> "UsageData": + """Extract usage data from response into a structured format. + + Args: + response: The GenerateContentResponse object or dictionary containing usage metadata + + Returns: + UsageData: Dictionary with input_tokens, input_tokens_cached, + output_tokens, and output_tokens_reasoning fields + """ + usage_data = UsageData( + input_tokens=0, + input_tokens_cached=0, + output_tokens=0, + output_tokens_reasoning=0, + total_tokens=0, + ) + + # Handle dictionary response (from streaming) + if isinstance(response, dict): + usage = response.get("usage_metadata", {}) + if not usage: + return usage_data + + prompt_tokens = usage.get("prompt_token_count", 0) or 0 + tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + cached_tokens = usage.get("cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + candidates_tokens = usage.get("candidates_token_count", 0) or 0 + # python-genai reports output and reasoning tokens separately + # reasoning should be sub-category of output tokens + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = usage.get("total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + if not hasattr(response, "usage_metadata"): + return usage_data + + usage = response.usage_metadata + + # Input tokens include both prompt and tool use prompt tokens + prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 + tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + # Cached input tokens + cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + # Reasoning tokens + reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + # output_tokens = candidates_tokens + reasoning_tokens + # google-genai reports output and reasoning tokens separately + candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = getattr(usage, "total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + +def _capture_exception(exc: "Any") -> None: + """Capture exception with Google GenAI mechanism.""" + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "google_genai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def get_model_name(model: "Union[str, Model]") -> str: + """Extract model name from model parameter.""" + if isinstance(model, str): + return model + # Handle case where model might be an object with a name attribute + if hasattr(model, "name"): + return str(model.name) + return str(model) + + +def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": + """Extract messages from contents parameter which can have various formats. + + Returns a list of message dictionaries in the format: + - System: {"role": "system", "content": "string"} + - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} + """ + if contents is None: + return [] + + messages = [] + + # Handle string case + if isinstance(contents, str): + return [{"role": "user", "content": contents}] + + # Handle list case + if isinstance(contents, list): + if contents and all(_is_part_like(item) for item in contents): + # All items are parts — merge into a single multi-part user message + content_parts = [] + for item in contents: + part = _extract_part_from_item(item) + if part is not None: + content_parts.append(part) + + return [{"role": "user", "content": content_parts}] + else: + # Multi-turn conversation or mixed content types + for item in contents: + item_messages = extract_contents_messages(item) + messages.extend(item_messages) + return messages + + # Handle dictionary case (ContentDict) + if isinstance(contents, dict): + role = contents.get("role", "user") + parts = contents.get("parts") + + if parts: + content_parts = [] + tool_messages = [] + + for part in parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + # Tool message - add separately + tool_messages.append(part_result) + else: + # Regular content part + content_parts.append(part_result) + + # Add main message if we have content parts + if content_parts: + # Normalize role: "model" -> "assistant" + normalized_role = "assistant" if role == "model" else role or "user" + messages.append({"role": normalized_role, "content": content_parts}) + + # Add tool messages + messages.extend(tool_messages) + elif "text" in contents: + messages.append( + { + "role": role, + "content": [{"text": contents["text"], "type": "text"}], + } + ) + elif "inline_data" in contents: + # The "data" will always be bytes (or bytes within a string), + # so if this is present, it's safe to automatically substitute with the placeholder + messages.append( + { + "inline_data": { + "mime_type": contents["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + ) + + return messages + + # Handle Content object + if hasattr(contents, "parts") and contents.parts: + role = getattr(contents, "role", None) or "user" + content_parts = [] + tool_messages = [] + + for part in contents.parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + tool_messages.append(part_result) + else: + content_parts.append(part_result) + + if content_parts: + normalized_role = "assistant" if role == "model" else role + messages.append({"role": normalized_role, "content": content_parts}) + + messages.extend(tool_messages) + return messages + + # Handle Part object directly + part_result = _extract_part_content(contents) + if part_result: + if isinstance(part_result, dict) and part_result.get("role") == "tool": + return [part_result] + else: + return [{"role": "user", "content": [part_result]}] + + # Handle PIL.Image.Image + if _is_PIL_available and isinstance(contents, PILImage.Image): + blob_part = _extract_pil_image(contents) + if blob_part: + return [{"role": "user", "content": [blob_part]}] + + # Handle File object + if hasattr(contents, "uri") and hasattr(contents, "mime_type"): + # File object + file_uri = getattr(contents, "uri", None) + mime_type = getattr(contents, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing + if file_uri is not None: + # Default to empty string if mime_type is None + if mime_type is None: + mime_type = "" + + blob_part = { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + return [{"role": "user", "content": [blob_part]}] + + # Handle direct text attribute + if hasattr(contents, "text") and contents.text: + return [ + {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} + ] + + return [] + + +def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": + """Extract content from a Part object or dict. + + Returns: + - dict for content part (text/blob) or tool message + - None if part should be skipped + """ + if part is None: + return None + + # Handle dict Part + if isinstance(part, dict): + # Check for function_response first (tool message) + if "function_response" in part: + return _extract_tool_message_from_part(part) + + if part.get("text"): + return {"text": part["text"], "type": "text"} + + # Try using Google-specific transform for dict formats (inline_data, file_data) + result = transform_google_content_part(part) + if result is not None: + # For inline_data with bytes data, substitute the content + if "inline_data" in part: + # inline_data.data will always be bytes, or a string containing base64-encoded bytes, + # so can automatically substitute without further checks + result["content"] = BLOB_DATA_SUBSTITUTE + return result + + return None + + # Handle Part object + # Check for function_response (tool message) + if hasattr(part, "function_response") and part.function_response: + return _extract_tool_message_from_part(part) + + # Handle text + if hasattr(part, "text") and part.text: + return {"text": part.text, "type": "text"} + + # Handle file_data + if hasattr(part, "file_data") and part.file_data: + file_data = part.file_data + file_uri = getattr(file_data, "file_uri", None) + mime_type = getattr(file_data, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) + if file_uri is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + # Handle inline_data + if hasattr(part, "inline_data") and part.inline_data: + inline_data = part.inline_data + data = getattr(inline_data, "data", None) + mime_type = getattr(inline_data, "mime_type", None) + # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) + if data is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + return None + + +def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": + """Extract tool message from a Part with function_response. + + Returns: + {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} + or None if not a valid tool message + """ + function_response = None + + if isinstance(part, dict): + function_response = part.get("function_response") + elif hasattr(part, "function_response"): + function_response = part.function_response + + if not function_response: + return None + + # Extract fields from function_response + tool_call_id = None + tool_name = None + output = None + + if isinstance(function_response, dict): + tool_call_id = function_response.get("id") + tool_name = function_response.get("name") + response_dict = function_response.get("response", {}) + # Prefer "output" key if present, otherwise use entire response + output = response_dict.get("output", response_dict) + else: + # FunctionResponse object + tool_call_id = getattr(function_response, "id", None) + tool_name = getattr(function_response, "name", None) + response_obj = getattr(function_response, "response", None) + if response_obj is None: + response_obj = {} + if isinstance(response_obj, dict): + output = response_obj.get("output", response_obj) + else: + output = response_obj + + if not tool_name: + return None + + return { + "role": "tool", + "content": { + "toolCallId": str(tool_call_id) if tool_call_id else None, + "toolName": str(tool_name), + "output": safe_serialize(output) if output is not None else None, + }, + } + + +def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": + """Extract blob part from PIL.Image.Image.""" + if not _is_PIL_available or not isinstance(image, PILImage.Image): + return None + + # Get format, default to JPEG + format_str = image.format or "JPEG" + suffix = format_str.lower() + mime_type = f"image/{suffix}" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _is_part_like(item: "Any") -> bool: + """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" + if isinstance(item, (str, Part)): + return True + if isinstance(item, (list, Content)): + return False + if isinstance(item, dict): + if "role" in item or "parts" in item: + return False + # Part objects that came in as plain dicts + return bool(_PART_DICT_KEYS & item.keys()) + # File objects + if hasattr(item, "uri"): + return True + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return True + return False + + +def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": + """Convert a single part-like item to a content part dict.""" + if isinstance(item, str): + return {"text": item, "type": "text"} + + # Handle bare inline_data dicts directly to preserve the raw format + if isinstance(item, dict) and "inline_data" in item: + return { + "inline_data": { + "mime_type": item["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + + # For other dicts and Part objects, use existing _extract_part_content + result = _extract_part_content(item) + if result is not None: + return result + + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return _extract_pil_image(item) + + # File objects + if hasattr(item, "uri") and hasattr(item, "mime_type"): + file_uri = getattr(item, "uri", None) + mime_type = getattr(item, "mime_type", None) or "" + if file_uri is not None: + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + return None + + +def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": + """Extract text from contents parameter which can have various formats. + + This is a compatibility function that extracts text from messages. + For new code, use extract_contents_messages instead. + """ + messages = extract_contents_messages(contents) + if not messages: + return None + + texts = [] + for message in messages: + content = message.get("content") + if isinstance(content, str): + texts.append(content) + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + texts.append(part.get("text", "")) + + return " ".join(texts) if texts else None + + +def _format_tools_for_span( + tools: "Iterable[Tool | Callable[..., Any]]", +) -> "Optional[List[dict[str, Any]]]": + """Format tools parameter for span data.""" + formatted_tools = [] + for tool in tools: + if callable(tool): + # Handle callable functions passed directly + formatted_tools.append( + { + "name": getattr(tool, "__name__", "unknown"), + "description": getattr(tool, "__doc__", None), + } + ) + elif ( + hasattr(tool, "function_declarations") + and tool.function_declarations is not None + ): + # Tool object with function declarations + for func_decl in tool.function_declarations: + formatted_tools.append( + { + "name": getattr(func_decl, "name", None), + "description": getattr(func_decl, "description", None), + } + ) + else: + # Check for predefined tool attributes - each of these tools + # is an attribute of the tool object, by default set to None + for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): + if getattr(tool, attr_name, None): + formatted_tools.append( + { + "name": attr_name, + "description": description, + } + ) + break + + return formatted_tools if formatted_tools else None + + +def extract_tool_calls( + response: "GenerateContentResponse", +) -> "Optional[List[dict[str, Any]]]": + """Extract tool/function calls from response candidates and automatic function calling history.""" + + tool_calls = [] + + # Extract from candidates, sometimes tool calls are nested under the content.parts object + if getattr(response, "candidates", []): + for candidate in response.candidates: + if not hasattr(candidate, "content") or not getattr( + candidate.content, "parts", [] + ): + continue + + for part in candidate.content.parts: + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if getattr(function_call, "args", None): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + # Extract from automatic_function_calling_history + # This is the history of tool calls made by the model + if getattr(response, "automatic_function_calling_history", None): + for content in response.automatic_function_calling_history: + if not getattr(content, "parts", None): + continue + + for part in getattr(content, "parts", []): + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if hasattr(function_call, "args"): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + return tool_calls if tool_calls else None + + +def _capture_tool_input( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" +) -> "dict[str, Any]": + """Capture tool input from args and kwargs.""" + tool_input = kwargs.copy() if kwargs else {} + + # If we have positional args, try to map them to the function signature + if args: + try: + sig = inspect.signature(tool) + param_names = list(sig.parameters.keys()) + for i, arg in enumerate(args): + if i < len(param_names): + tool_input[param_names[i]] = arg + except Exception: + # Fallback if we can't get the signature + tool_input["args"] = args + + return tool_input + + +def _create_tool_span( + tool_name: str, tool_doc: "Optional[str]" +) -> "Union[Span, StreamedSpan]": + """Create a span for tool execution.""" + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + if tool_doc: + span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=ORIGIN, + ) + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + if tool_doc: + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + +def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": + """Wrap a tool to emit execute_tool spans when called.""" + if not callable(tool): + # Not a callable function, return as-is (predefined tools) + return tool + + tool_name = getattr(tool, "__name__", "unknown") + tool_doc = tool.__doc__ + + if inspect.iscoroutinefunction(tool): + # Async function + @wraps(tool) + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = await tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return async_wrapped + else: + # Sync function + @wraps(tool) + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return sync_wrapped + + +def wrapped_config_with_tools( + config: "GenerateContentConfig", +) -> "GenerateContentConfig": + """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as + callable functions as a part of the config object.""" + + if not config or not getattr(config, "tools", None): + return config + + result = copy.copy(config) + result.tools = [wrapped_tool(tool) for tool in config.tools] + + return result + + +def _extract_response_text( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract text from response candidates.""" + + if not response or not getattr(response, "candidates", []): + return None + + texts = [] + for candidate in response.candidates: + if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): + continue + + if candidate.content is None or candidate.content.parts is None: + continue + + for part in candidate.content.parts: + if getattr(part, "text", None): + texts.append(part.text) + + return texts if texts else None + + +def extract_finish_reasons( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract finish reasons from response candidates.""" + if not response or not getattr(response, "candidates", []): + return None + + finish_reasons = [] + for candidate in response.candidates: + if getattr(candidate, "finish_reason", None): + # Convert enum value to string if necessary + reason = str(candidate.finish_reason) + # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") + if "." in reason: + reason = reason.split(".")[-1] + finish_reasons.append(reason) + + return finish_reasons if finish_reasons else None + + +def _transform_system_instruction_one_level( + system_instructions: "Union[ContentUnionDict, ContentUnion]", + can_be_content: bool, +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, str): + return [{"type": "text", "content": system_instructions}] + + if isinstance(system_instructions, Part) and system_instructions.text: + return [{"type": "text", "content": system_instructions.text}] + + if can_be_content and isinstance(system_instructions, Content): + if isinstance(system_instructions.parts, list): + for part in system_instructions.parts: + if isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + return text_parts + + if isinstance(system_instructions, dict) and system_instructions.get("text"): + return [{"type": "text", "content": system_instructions["text"]}] + + elif can_be_content and isinstance(system_instructions, dict): + parts = system_instructions.get("parts", []) + for part in parts: + if isinstance(part, Part) and isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + elif isinstance(part, dict) and isinstance(part.get("text"), str): + text_parts.append({"type": "text", "content": part["text"]}) + return text_parts + + return text_parts + + +def _transform_system_instructions( + system_instructions: "Union[ContentUnionDict, ContentUnion]", +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, list): + text_parts = list( + chain.from_iterable( + _transform_system_instruction_one_level( + instructions, can_be_content=False + ) + for instructions in system_instructions + ) + ) + + return text_parts + + return _transform_system_instruction_one_level( + system_instructions, can_be_content=True + ) + + +def set_span_data_for_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + model: str, + contents: "ContentListUnion", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for the request.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if kwargs.get("stream", False): + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + config: "Optional[GenerateContentConfig]" = kwargs.get("config") + + # Set input messages/prompts if PII is allowed + if should_send_default_pii() and integration.include_prompts: + messages = [] + + # Add system instruction if present + system_instructions = None + if config and hasattr(config, "system_instruction"): + system_instructions = config.system_instruction + elif isinstance(config, dict) and "system_instruction" in config: + system_instructions = config.get("system_instruction") + + if system_instructions is not None: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + # Extract messages from contents + contents_messages = extract_contents_messages(contents) + messages.extend(contents_messages) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Extract parameters directly from config (not nested under generation_config) + for param, span_key in [ + ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), + ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), + ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), + ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), + ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), + ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), + ("seed", SPANDATA.GEN_AI_REQUEST_SEED), + ]: + if hasattr(config, param): + value = getattr(config, param) + if value is not None: + set_on_span(span_key, value) + + # Set tools if available + if config is not None and hasattr(config, "tools"): + tools = config.tools + if tools: + formatted_tools = _format_tools_for_span(tools) + if formatted_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + formatted_tools, + unpack=False, + ) + + +def set_span_data_for_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "GenerateContentResponse", +) -> None: + """Set span data for the response.""" + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if should_send_default_pii() and integration.include_prompts: + response_texts = _extract_response_text(response) + if response_texts: + # Format as JSON string array as per documentation + set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) + + tool_calls = extract_tool_calls(response) + if tool_calls: + # Tool calls should be JSON serialized + set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) + + finish_reasons = extract_finish_reasons(response) + if finish_reasons: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + + if getattr(response, "response_id", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) + + if getattr(response, "model_version", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) + + usage_data = extract_usage_data(response) + + if usage_data["input_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) + + if usage_data["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage_data["input_tokens_cached"], + ) + + if usage_data["output_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) + + if usage_data["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage_data["output_tokens_reasoning"], + ) + + if usage_data["total_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) + + +def prepare_generate_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[Any, Any, str]": + """Extract and prepare common arguments for generate_content methods.""" + model = args[0] if args else kwargs.get("model", "unknown") + contents = args[1] if len(args) > 1 else kwargs.get("contents") + model_name = get_model_name(model) + + config = kwargs.get("config") + wrapped_config = wrapped_config_with_tools(config) + if wrapped_config is not config: + kwargs["config"] = wrapped_config + + return model, contents, model_name + + +def prepare_embed_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[str, Any]": + """Extract and prepare common arguments for embed_content methods. + + Returns: + tuple: (model_name, contents) + """ + model = kwargs.get("model", "unknown") + contents = kwargs.get("contents") + model_name = get_model_name(model) + + return model_name, contents + + +def set_span_data_for_embed_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + contents: "Any", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for embedding request.""" + # Include input contents if PII is allowed + if should_send_default_pii() and integration.include_prompts: + if contents: + # For embeddings, contents is typically a list of strings/texts + input_texts = [] + + # Handle various content formats + if isinstance(contents, str): + input_texts = [contents] + elif isinstance(contents, list): + for item in contents: + text = extract_contents_text(item) + if text: + input_texts.append(text) + else: + text = extract_contents_text(contents) + if text: + input_texts = [text] + + if input_texts: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + input_texts, + unpack=False, + ) + + +def set_span_data_for_embed_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "EmbedContentResponse", +) -> None: + """Set span data for embedding response.""" + if not response: + return + + # Extract token counts from embeddings statistics (Vertex AI only) + # Each embedding has its own statistics with token_count + if hasattr(response, "embeddings") and response.embeddings: + total_tokens = 0 + + for embedding in response.embeddings: + if hasattr(embedding, "statistics") and embedding.statistics: + token_count = getattr(embedding.statistics, "token_count", None) + if token_count is not None: + total_tokens += int(token_count) + + # Set token count if we found any + if total_tokens > 0: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) + else: + span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py new file mode 100644 index 0000000000..dcb60e9561 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py @@ -0,0 +1,173 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +try: + import gql # type: ignore[import-not-found] + from gql.transport import ( # type: ignore[import-not-found] + AsyncTransport, + Transport, + ) + from gql.transport.exceptions import ( # type: ignore[import-not-found] + TransportQueryError, + ) + from graphql import ( + DocumentNode, + VariableDefinitionNode, + get_operation_ast, + print_ast, + ) + + try: + # gql 4.0+ + from gql import GraphQLRequest + except ImportError: + GraphQLRequest = None + +except ImportError: + raise DidNotEnable("gql is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Tuple, Union + + from sentry_sdk._types import Event, EventProcessor + + EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] + + +class GQLIntegration(Integration): + identifier = "gql" + + @staticmethod + def setup_once() -> None: + gql_version = parse_version(gql.__version__) + _check_minimum_version(GQLIntegration, gql_version) + + _patch_execute() + + +def _data_from_document(document: "DocumentNode") -> "EventDataType": + try: + operation_ast = get_operation_ast(document) + data: "EventDataType" = {"query": print_ast(document)} + + if operation_ast is not None: + data["variables"] = operation_ast.variable_definitions + if operation_ast.name is not None: + data["operationName"] = operation_ast.name.value + + return data + except (AttributeError, TypeError): + return dict() + + +def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: + """ + The RequestsHTTPTransport allows defining the HTTP method; all + other transports use POST. + """ + try: + return transport.method + except AttributeError: + return "POST" + + +def _request_info_from_transport( + transport: "Union[Transport, AsyncTransport, None]", +) -> "Dict[str, str]": + if transport is None: + return {} + + request_info = { + "method": _transport_method(transport), + } + + try: + request_info["url"] = transport.url + except AttributeError: + pass + + return request_info + + +def _patch_execute() -> None: + real_execute = gql.Client.execute + + # Maintain signature for backwards compatibility. + # gql.Client.execute() accepts a positional-only "request" + # parameter with version 4.0.0. + @ensure_integration_enabled(GQLIntegration, real_execute) + def sentry_patched_execute( + self: "gql.Client", + document: "DocumentNode", + *args: "Any", + **kwargs: "Any", + ) -> "Any": + scope = sentry_sdk.get_isolation_scope() + # document is a gql.GraphQLRequest with gql v4.0.0. + scope.add_event_processor(_make_gql_event_processor(self, document)) + + try: + # document is a gql.GraphQLRequest with gql v4.0.0. + return real_execute(self, document, *args, **kwargs) + except TransportQueryError as e: + event, hint = event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "gql", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint) + raise e + + gql.Client.execute = sentry_patched_execute + + +def _make_gql_event_processor( + client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" +) -> "EventProcessor": + def processor(event: "Event", hint: "dict[str, Any]") -> "Event": + try: + errors = hint["exc_info"][1].errors + except (AttributeError, KeyError): + errors = None + + request = event.setdefault("request", {}) + request.update( + { + "api_target": "graphql", + **_request_info_from_transport(client.transport), + } + ) + + if should_send_default_pii(): + if GraphQLRequest is not None and isinstance( + document_or_request, GraphQLRequest + ): + # In v4.0.0, gql moved to using GraphQLRequest instead of + # DocumentNode in execute + # https://github.com/graphql-python/gql/pull/556 + document = document_or_request.document + else: + document = document_or_request + + request["data"] = _data_from_document(document) + contexts = event.setdefault("contexts", {}) + response = contexts.setdefault("response", {}) + response.update( + { + "data": {"errors": errors}, + "type": response, + } + ) + + return event + + return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py new file mode 100644 index 0000000000..4938a5c0f3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py @@ -0,0 +1,181 @@ +from contextlib import contextmanager + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from graphene.types import schema as graphene_schema # type: ignore +except ImportError: + raise DidNotEnable("graphene is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Any, Dict, Union + + from graphene.language.source import Source # type: ignore + from graphql.execution import ExecutionResult + from graphql.type import GraphQLSchema + + from sentry_sdk._types import Event + + +class GrapheneIntegration(Integration): + identifier = "graphene" + + @staticmethod + def setup_once() -> None: + version = package_version("graphene") + _check_minimum_version(GrapheneIntegration, version) + + _patch_graphql() + + +def _patch_graphql() -> None: + old_graphql_sync = graphene_schema.graphql_sync + old_graphql_async = graphene_schema.graphql + + @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) + def _sentry_patched_graphql_sync( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = old_graphql_sync(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + async def _sentry_patched_graphql_async( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) + if integration is None: + return await old_graphql_async(schema, source, *args, **kwargs) + + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = await old_graphql_async(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + graphene_schema.graphql_sync = _sentry_patched_graphql_sync + graphene_schema.graphql = _sentry_patched_graphql_async + + +def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + if should_send_default_pii(): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + +@contextmanager +def graphql_span( + schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" +) -> "Generator[None, None, None]": + operation_name = kwargs.get("operation_name") or "" + + operation_type = "query" + op = OP.GRAPHQL_QUERY + if source.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif source.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + sentry_sdk.add_breadcrumb( + crumb={ + "data": { + "operation_name": operation_name, + "operation_type": operation_type, + }, + "category": "graphql.operation", + }, + ) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + additional_attributes = {} + if should_send_default_pii(): + additional_attributes["graphql.document"] = source + + _graphql_span = sentry_sdk.traces.start_span( + name=operation_name, + attributes={ + "sentry.op": op, + "graphql.operation.name": operation_name, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) + + if should_send_default_pii(): + _graphql_span.set_data("graphql.document", source) + _graphql_span.set_data("graphql.operation.name", operation_name) + _graphql_span.set_data("graphql.operation.type", operation_type) + + _graphql_span.__enter__() + + try: + yield + finally: + if is_span_streaming_enabled: + _graphql_span.end() # type: ignore + else: + _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py new file mode 100644 index 0000000000..bf74ff1351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py @@ -0,0 +1,188 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any, Optional, Sequence + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .client import ClientInterceptor +from .server import ServerInterceptor + +try: + import grpc + from grpc import Channel, Server, intercept_channel + from grpc.aio import Channel as AsyncChannel + from grpc.aio import Server as AsyncServer + + from .aio.client import ( + SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, + ) + from .aio.client import ( + SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, + ) + from .aio.server import ServerInterceptor as AsyncServerInterceptor +except ImportError: + raise DidNotEnable("grpcio is not installed.") + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from typing import Callable, ParamSpec +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + +P = ParamSpec("P") + +GRPC_VERSION = parse_version(grpc.__version__) + + +def _is_channel_intercepted(channel: "Channel") -> bool: + interceptor = getattr(channel, "_interceptor", None) + while interceptor is not None: + if isinstance(interceptor, ClientInterceptor): + return True + + inner_channel = getattr(channel, "_channel", None) + if inner_channel is None: + return False + + channel = inner_channel + interceptor = getattr(channel, "_interceptor", None) + + return False + + +def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + "Wrapper for synchronous secure and insecure channel." + + @wraps(func) + def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": + channel = func(*args, **kwargs) + if not _is_channel_intercepted(channel): + return intercept_channel(channel, ClientInterceptor()) + else: + return channel + + return patched_channel + + +def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + @wraps(func) + def patched_intercept_channel( + channel: "Channel", *interceptors: "grpc.ServerInterceptor" + ) -> "Channel": + if _is_channel_intercepted(channel): + interceptors = tuple( + [ + interceptor + for interceptor in interceptors + if not isinstance(interceptor, ClientInterceptor) + ] + ) + else: + interceptors = interceptors + return intercept_channel(channel, *interceptors) + + return patched_intercept_channel # type: ignore + + +def _wrap_channel_async( + func: "Callable[P, AsyncChannel]", +) -> "Callable[P, AsyncChannel]": + "Wrapper for asynchronous secure and insecure channel." + + @wraps(func) + def patched_channel( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Channel": + sentry_interceptors = [ + AsyncUnaryUnaryClientInterceptor(), + AsyncUnaryStreamClientIntercetor(), + ] + interceptors = [*sentry_interceptors, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_channel # type: ignore + + +def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": + """Wrapper for synchronous server.""" + + @wraps(func) + def patched_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + interceptors = [ + interceptor + for interceptor in interceptors or [] + if not isinstance(interceptor, ServerInterceptor) + ] + server_interceptor = ServerInterceptor() + interceptors = [server_interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_server # type: ignore + + +def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": + """Wrapper for asynchronous server.""" + + @wraps(func) + def patched_aio_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + server_interceptor = AsyncServerInterceptor() + interceptors = [ + server_interceptor, + *(interceptors or []), + ] + + try: + # We prefer interceptors as a list because of compatibility with + # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 + # However, prior to grpc 1.42.0, only tuples were accepted, so we + # have no choice there. + if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): + interceptors = tuple(interceptors) + except Exception: + pass + + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_aio_server # type: ignore + + +class GRPCIntegration(Integration): + identifier = "grpc" + + @staticmethod + def setup_once() -> None: + import grpc + + grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) + grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) + grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) + + grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) + grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) + + grpc.server = _wrap_sync_server(grpc.server) + grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py new file mode 100644 index 0000000000..4d21815254 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py @@ -0,0 +1,7 @@ +from .client import ClientInterceptor +from .server import ServerInterceptor + +__all__ = [ + "ClientInterceptor", + "ServerInterceptor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py new file mode 100644 index 0000000000..d07b7f19be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py @@ -0,0 +1,146 @@ +from typing import Any, AsyncIterable, Callable, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.protobuf.message import Message + from grpc.aio import ( + ClientCallDetails, + Metadata, + UnaryStreamCall, + UnaryStreamClientInterceptor, + UnaryUnaryCall, + UnaryUnaryClientInterceptor, + ) +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor: + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + if client_call_details.metadata is None: + client_call_details = client_call_details._replace(metadata=Metadata()) + elif not isinstance(client_call_details.metadata, Metadata): + # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 + # See https://github.com/grpc/grpc/issues/34298. + client_call_details = client_call_details._replace( + metadata=Metadata.from_tuple(client_call_details.metadata) + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + client_call_details.metadata.add(key, value) + return client_call_details + + +class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore + async def intercept_unary_unary( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[UnaryUnaryCall, Message]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_data("code", status_code.name) + + return response + + +class SentryUnaryStreamClientInterceptor( + ClientInterceptor, + UnaryStreamClientInterceptor, # type: ignore +): + async def intercept_unary_stream( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + # status_code = await response.code() + # span.set_data("code", status_code) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py new file mode 100644 index 0000000000..010337e98c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py @@ -0,0 +1,134 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from typing import Any, Optional + + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler + from grpc.aio import AbortError, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Callable[[ServicerContext], str] | None" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + async def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", + handler_call_details: "HandlerCallDetails", + ) -> "Optional[Awaitable[RpcMethodHandler]]": + handler = await continuation(handler_call_details) + if handler is None: + return None + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + if not handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.unary_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + with sentry_sdk.isolation_scope(): + name = ( + custom_find_name(context) if custom_find_name else method_name + ) + if not name: + return await handler(request, context) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + # What if the headers are empty? + sentry_sdk.traces.continue_trace( + dict(context.invocation_metadata()) + ) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + else: + # What if the headers are empty? + transaction = sentry_sdk.continue_trace( + dict(context.invocation_metadata()), + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + + elif not handler.request_streaming and handler.response_streaming: + handler_factory = grpc.unary_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.unary_stream(request, context): + yield r + + elif handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.stream_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + response = handler.stream_unary(request, context) + return await response + + elif handler.request_streaming and handler.response_streaming: + handler_factory = grpc.stream_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.stream_stream(request, context): + yield r + + return handler_factory( + wrapped, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py new file mode 100644 index 0000000000..5384a0a78f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py @@ -0,0 +1,149 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Iterator, Union + +try: + import grpc + from google.protobuf.message import Message + from grpc import Call, ClientCallDetails + from grpc._interceptor import _UnaryOutcome + from grpc.aio._interceptor import UnaryStreamCall +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor( + grpc.UnaryUnaryClientInterceptor, # type: ignore + grpc.UnaryStreamClientInterceptor, # type: ignore +): + def intercept_unary_unary( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "_UnaryOutcome": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_attribute( + SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name + ) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_data("code", response.code().name) + + return response + + def intercept_unary_stream( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[Iterator[Message], Call]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + response: "UnaryStreamCall" + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + metadata = ( + list(client_call_details.metadata) if client_call_details.metadata else [] + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + metadata.append((key, value)) + + client_call_details = grpc._interceptor._ClientCallDetails( + method=client_call_details.method, + timeout=client_call_details.timeout, + metadata=metadata, + credentials=client_call_details.credentials, + wait_for_ready=client_call_details.wait_for_ready, + compression=client_call_details.compression, + ) + + return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py new file mode 100644 index 0000000000..9fdb975caf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py new file mode 100644 index 0000000000..1cba1d4b85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py @@ -0,0 +1,91 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Callable, Optional + + from google.protobuf.message import Message + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Optional[Callable[[ServicerContext], str]]" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", + handler_call_details: "HandlerCallDetails", + ) -> "RpcMethodHandler": + handler = continuation(handler_call_details) + if not handler or not handler.unary_unary: + return handler + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + def behavior(request: "Message", context: "ServicerContext") -> "Message": + with sentry_sdk.isolation_scope(): + name = custom_find_name(context) if custom_find_name else method_name + + if name: + metadata = dict(context.invocation_metadata()) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(metadata) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + transaction = sentry_sdk.continue_trace( + metadata, + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + return handler.unary_unary(request, context) + + return grpc.unary_unary_rpc_method_handler( + behavior, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py new file mode 100644 index 0000000000..a68f20b299 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx is not installed") + +__all__ = ["HttpxIntegration"] + + +class HttpxIntegration(Integration): + identifier = "httpx" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx_client() + _install_httpx_async_client() + + +def _install_httpx_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(HttpxIntegration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(HttpxIntegration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py new file mode 100644 index 0000000000..25062aaa11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx2 import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx2 is not installed") + +__all__ = ["Httpx2Integration"] + + +class Httpx2Integration(Integration): + identifier = "httpx2" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx2 has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx2_client() + _install_httpx2_async_client() + + +def _install_httpx2_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(Httpx2Integration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx2_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(Httpx2Integration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py new file mode 100644 index 0000000000..c3bbc8abcf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py @@ -0,0 +1,239 @@ +import sys +from datetime import datetime +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, Hint + from sentry_sdk.utils import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + +try: + from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task + from huey.exceptions import CancelExecution, RetryTask, TaskLockedException +except ImportError: + raise DidNotEnable("Huey is not installed") + +try: + from huey.api import chord as HueyChord + from huey.api import group as HueyGroup +except ImportError: + HueyChord = None + HueyGroup = None + + +HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) + + +class HueyIntegration(Integration): + identifier = "huey" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_enqueue() + patch_execute() + _register_control_flow_exception( + [CancelExecution, RetryTask, TaskLockedException] + ) + + +def patch_enqueue() -> None: + old_enqueue = Huey.enqueue + + @ensure_integration_enabled(HueyIntegration, old_enqueue) + def _sentry_enqueue( + self: "Huey", item: "Any" + ) -> "Optional[Union[Result, ResultGroup]]": + if HueyChord is not None and isinstance(item, HueyChord): + span_name = "Huey Chord" + elif HueyGroup is not None and isinstance(item, HueyGroup): + span_name = "Huey Task Group" + else: + span_name = item.name + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + span_ctx = None + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_HUEY, + "sentry.origin": HueyIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_HUEY, + name=span_name, + origin=HueyIntegration.origin, + ) + + no_headers_types = (PeriodicTask,) + tuple( + t for t in [HueyGroup, HueyChord] if t is not None + ) + with span_ctx: + if not isinstance(item, no_headers_types): + # Attach trace propagation data to task kwargs. We do + # not do this for periodic tasks, as these don't + # really have an originating transaction. + # Additionally, we do not do this for Huey groups or chords, as enqueue will + # recursively call this method for each task within the list, resulting + # in the trace propagation data being attached to each task individually + # (which we want) + item.kwargs["sentry_headers"] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + return old_enqueue(self, item) + + Huey.enqueue = _sentry_enqueue + + +def _make_event_processor(task: "Any") -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["huey_task_id"] = task.id + tags["huey_task_retry"] = task.default_retries > task.retries + extra = event.setdefault("extra", {}) + extra["huey-job"] = { + "task": task.name, + "args": ( + task.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + task.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": (task.default_retries or 0) - task.retries, + } + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.ABORTED) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.OK + return + + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.ERROR + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": HueyIntegration.identifier, "handled": False}, + ) + scope.capture_event(event, hint=hint) + + +def _wrap_task_execute(func: "F") -> "F": + @ensure_integration_enabled(HueyIntegration, func) + def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": + try: + result = func(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_execute # type: ignore + + +def patch_execute() -> None: + old_execute = Huey._execute + + @ensure_integration_enabled(HueyIntegration, old_execute) + def _sentry_execute( + self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None + ) -> "Any": + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope._name = "huey" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task)) + + sentry_headers = task.kwargs.pop("sentry_headers", None) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + headers = sentry_headers or {} + sentry_sdk.traces.continue_trace(headers) + span_ctx = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_TASK_HUEY, + "sentry.origin": HueyIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + "messaging.message.id": task.id, + "messaging.message.system": "huey", + "messaging.message.retry.count": (task.default_retries or 0) + - task.retries, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + sentry_headers or {}, + name=task.name, + op=OP.QUEUE_TASK_HUEY, + source=TransactionSource.TASK, + origin=HueyIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + span_ctx = sentry_sdk.start_transaction(transaction) + + if not getattr(task, "_sentry_is_patched", False): + task.execute = _wrap_task_execute(task.execute) + task._sentry_is_patched = True + + with span_ctx: + return old_execute(self, task, timestamp) + + Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py new file mode 100644 index 0000000000..835acc7279 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py @@ -0,0 +1,392 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + _set_span_data_attribute, + get_start_span_function, + set_data_normalized, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Union + + from sentry_sdk.tracing import Span + +try: + import huggingface_hub.inference._client +except ImportError: + raise DidNotEnable("Huggingface not installed") + + +class HuggingfaceHubIntegration(Integration): + identifier = "huggingface_hub" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "HuggingfaceHubIntegration", include_prompts: bool = True + ) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks + huggingface_hub.inference._client.InferenceClient.text_generation = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.text_generation, + OP.GEN_AI_TEXT_COMPLETION, + ) + ) + huggingface_hub.inference._client.InferenceClient.chat_completion = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.chat_completion, + OP.GEN_AI_CHAT, + ) + ) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "huggingface_hub", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": + @wraps(f) + def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) + if integration is None: + return f(*args, **kwargs) + + prompt = None + if "prompt" in kwargs: + prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] + elif len(args) >= 2: + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error + return f(*args, **kwargs) + + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(sentry_sdk.get_client().options): + span = sentry_sdk.traces.start_span( + name=f"{operation_name} {model}", + attributes={ + "sentry.op": op, + "sentry.origin": HuggingfaceHubIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=op, + name=f"{operation_name} {model}", + origin=HuggingfaceHubIntegration.origin, + ) + span.__enter__() + + _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) + + if model: + _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) + + # Input attributes + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + ) + + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + _set_span_data_attribute(span, span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + tool_calls = None + usage = None + + with capture_internal_exceptions(): + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) + + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) + + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): + span.__exit__(None, None, None) + return res + + if kwargs.get("details", False): + # text-generation stream output + def new_details_iterator() -> "Iterable[Any]": + finish_reason = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + + with capture_internal_exceptions(): + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None + ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + span.__exit__(None, None, None) + + return new_details_iterator() + + else: + # chat-completion stream output + def new_iterator() -> "Iterable[str]": + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tool_calls = None + usage = None + + with capture_internal_exceptions(): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if hasattr(chunk, "choices") and chunk.choices is not None: + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content + ) + + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason + + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls + + yield chunk + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + + span.__exit__(None, None, None) + + return new_iterator() + + return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py new file mode 100644 index 0000000000..9dcbb189ce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py @@ -0,0 +1,1412 @@ +import itertools +import json +import sys +import warnings +from collections import OrderedDict +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + _get_value, + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import capture_internal_exceptions, logger + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Dict, + Iterator, + List, + Optional, + Union, + ) + from uuid import UUID + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + + +try: + from langchain_core.agents import AgentFinish + from langchain_core.callbacks import ( + BaseCallbackHandler, + BaseCallbackManager, + Callbacks, + manager, + ) + from langchain_core.messages import BaseMessage + from langchain_core.outputs import LLMResult + +except ImportError: + raise DidNotEnable("langchain not installed") + + +try: + # >=v1 + from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] +except ImportError: + try: + # "Optional[str]": + ai_type = all_params.get("_type") + + if not ai_type or not isinstance(ai_type, str): + return None + + return ai_type + + +DATA_FIELDS = { + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, +} + + +def _transform_langchain_content_block( + content_block: "Dict[str, Any]", +) -> "Dict[str, Any]": + """ + Transform a LangChain content block using the shared transform_content_part function. + + Returns the original content block if transformation is not applicable + (e.g., for text blocks or unrecognized formats). + """ + result = transform_content_part(content_block) + return result if result is not None else content_block + + +def _transform_langchain_message_content(content: "Any") -> "Any": + """ + Transform LangChain message content, handling both string content and + list of content blocks. + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for block in content: + if isinstance(block, dict): + transformed.append(_transform_langchain_content_block(block)) + else: + transformed.append(block) + return transformed + + return content + + +def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": + system_instructions = [] + + for list_ in messages: + for message in list_: + # type of content: str | list[str | dict] | None + if message.type == "system" and isinstance(message.content, str): + system_instructions.append(message.content) + + elif message.type == "system" and isinstance(message.content, list): + for item in message.content: + if isinstance(item, str): + system_instructions.append(item) + + elif isinstance(item, dict) and item.get("type") == "text": + instruction = item.get("text") + if isinstance(instruction, str): + system_instructions.append(instruction) + + return system_instructions + + +def _transform_system_instructions( + system_instructions: "List[str]", +) -> "List[TextPart]": + return [ + { + "type": "text", + "content": instruction, + } + for instruction in system_instructions + ] + + +class LangchainIntegration(Integration): + identifier = "langchain" + origin = f"auto.ai.{identifier}" + + _ignored_exceptions: "set[type[Exception]]" = set() + + def __init__( + self: "LangchainIntegration", + include_prompts: bool = True, + max_spans: "Optional[int]" = None, + ) -> None: + self.include_prompts = include_prompts + self.max_spans = max_spans + + if max_spans is not None: + warnings.warn( + "The `max_spans` parameter of `LangchainIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + manager._configure = _wrap_configure(manager._configure) + + if AgentExecutor is not None: + AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) + AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) + + # Patch embeddings providers + _patch_embeddings_provider(OpenAIEmbeddings) + _patch_embeddings_provider(AzureOpenAIEmbeddings) + _patch_embeddings_provider(VertexAIEmbeddings) + _patch_embeddings_provider(BedrockEmbeddings) + _patch_embeddings_provider(CohereEmbeddings) + _patch_embeddings_provider(MistralAIEmbeddings) + _patch_embeddings_provider(HuggingFaceEmbeddings) + _patch_embeddings_provider(OllamaEmbeddings) + + +class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] + """Callback handler that creates Sentry spans.""" + + def __init__( + self, max_span_map_size: "Optional[int]", include_prompts: bool + ) -> None: + self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() + self.max_span_map_size = max_span_map_size + self.include_prompts = include_prompts + + def gc_span_map(self) -> None: + if self.max_span_map_size is not None: + while len(self.span_map) > self.max_span_map_size: + run_id, span = self.span_map.popitem(last=False) + self._exit_span(span, run_id) + + def _handle_error(self, run_id: "UUID", error: "Any") -> None: + is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) + + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if is_ignored: + span.__exit__(None, None, None) + else: + sentry_sdk.capture_exception( + error, span._scope if isinstance(span, StreamedSpan) else span.scope + ) + span.__exit__(type(error), error, error.__traceback__) + + del self.span_map[run_id] + + def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": + # Transform content to handle multimodal data (images, audio, video, files) + transformed_content = _transform_langchain_message_content(message.content) + parsed = {"role": message.type, "content": transformed_content} + parsed.update(message.additional_kwargs) + return parsed + + def _create_span( + self: "SentryLangchainCallback", + run_id: "UUID", + parent_id: "Optional[Any]", + op: str, + name: str, + origin: str, + ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span = None + if parent_id: + parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( + self.span_map.get(parent_id) + ) + if parent_span: + span = ( + sentry_sdk.traces.start_span( + parent_span=parent_span, + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if isinstance(parent_span, StreamedSpan) + else parent_span.start_child(op=op, name=name, origin=origin) + ) + + if span is None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span = ( + sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if span_streaming + else sentry_sdk.start_span(op=op, name=name, origin=origin) + ) + + span.__enter__() + self.span_map[run_id] = span + self.gc_span_map() + return span + + def _exit_span( + self: "SentryLangchainCallback", + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + run_id: "UUID", + ) -> None: + span.__exit__(None, None, None) + del self.span_map[run_id] + + def on_llm_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + prompts: "List[str]", + *, + run_id: "UUID", + tags: "Optional[List[str]]" = None, + parent_run_id: "Optional[UUID]" = None, + metadata: "Optional[Dict[str, Any]]" = None, + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + parent_run_id, + op=OP.GEN_AI_TEXT_COMPLETION, + name=f"text_completion {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") + + run_name = kwargs.get("name") + if run_name: + set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + if model: + set_on_span( + SPANDATA.GEN_AI_REQUEST_MODEL, + model, + ) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + normalized_messages = [ + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, + "content": {"type": "text", "text": prompt}, + } + for prompt in prompts + ] + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + messages: "List[List[BaseMessage]]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if model: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + system_instructions = _get_system_instructions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + normalized_messages = [] + for list_ in messages: + for message in list_: + if message.type == "system": + continue + + normalized_messages.append( + self._normalize_langchain_message(message) + ) + normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + try: + generation = response.generations[0][0] + except IndexError: + generation = None + + if generation is not None: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + + try: + response_model = generation.message.response_metadata.get( + "model_name" + ) + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + except AttributeError: + pass + + try: + finish_reason = generation.generation_info.get("finish_reason") + if finish_reason is not None: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + [finish_reason], + ) + except AttributeError: + pass + + try: + if should_send_default_pii() and self.include_prompts: + tool_calls = getattr(generation.message, "tool_calls", None) + if tool_calls is not None and tool_calls != []: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + except AttributeError: + pass + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM errors.""" + self._handle_error(run_id, error) + + def on_chat_model_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model errors.""" + self._handle_error(run_id, error) + + def on_agent_finish( + self: "SentryLangchainCallback", + finish: "AgentFinish", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() + ) + + self._exit_span(span, run_id) + + def on_tool_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + input_str: str, + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + tool_name = serialized.get("name") or kwargs.get("name") or "" + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + tool_description = serialized.get("description") + if tool_description is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_TOOL_INPUT, + kwargs.get("inputs", [input_str]), + ) + + def on_tool_end( + self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" + ) -> "Any": + """Run when tool ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) + + self._exit_span(span, run_id) + + def on_tool_error( + self, + error: "SentryLangchainCallback", + *args: "Union[Exception, KeyboardInterrupt]", + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool errors.""" + self._handle_error(run_id, error) + + +def _extract_tokens( + token_usage: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + if not token_usage: + return None, None, None + + input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( + token_usage, "input_tokens" + ) + output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( + token_usage, "output_tokens" + ) + total_tokens = _get_value(token_usage, "total_tokens") + + return input_tokens, output_tokens, total_tokens + + +def _extract_tokens_from_generations( + generations: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + """Extract token usage from response.generations structure.""" + if not generations: + return None, None, None + + total_input = 0 + total_output = 0 + total_total = 0 + + for gen_list in generations: + for gen in gen_list: + token_usage = _get_token_usage(gen) + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + total_input += input_tokens if input_tokens is not None else 0 + total_output += output_tokens if output_tokens is not None else 0 + total_total += total_tokens if total_tokens is not None else 0 + + return ( + total_input if total_input > 0 else None, + total_output if total_output > 0 else None, + total_total if total_total > 0 else None, + ) + + +def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": + """ + Check multiple paths to extract token usage from different objects. + """ + possible_names = ("usage", "token_usage", "usage_metadata") + + message = _get_value(obj, "message") + if message is not None: + for name in possible_names: + usage = _get_value(message, name) + if usage is not None: + return usage + + llm_output = _get_value(obj, "llm_output") + if llm_output is not None: + for name in possible_names: + usage = _get_value(llm_output, name) + if usage is not None: + return usage + + for name in possible_names: + usage = _get_value(obj, name) + if usage is not None: + return usage + + return None + + +def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: + token_usage = _get_token_usage(response) + if token_usage: + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + else: + input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( + response.generations + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) + + +def _get_request_data( + obj: "Any", args: "Any", kwargs: "Any" +) -> "tuple[Optional[str], Optional[List[Any]]]": + """ + Get the agent name and available tools for the agent. + """ + agent = getattr(obj, "agent", None) + runnable = getattr(agent, "runnable", None) + runnable_config = getattr(runnable, "config", {}) + tools = ( + getattr(obj, "tools", None) + or getattr(agent, "tools", None) + or runnable_config.get("tools") + or runnable_config.get("available_tools") + ) + tools = tools if tools and len(tools) > 0 else None + + try: + agent_name = None + if len(args) > 1: + agent_name = args[1].get("run_name") + if agent_name is None: + agent_name = runnable_config.get("run_name") + except Exception: + pass + + return (agent_name, tools) + + +def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": + """Parse and simplify tools into a cleaner format.""" + if not tools: + return None + + if not isinstance(tools, (list, tuple)): + return None + + simplified_tools = [] + for tool in tools: + try: + if isinstance(tool, dict): + if "function" in tool and isinstance(tool["function"], dict): + func = tool["function"] + simplified_tool = { + "name": func.get("name"), + "description": func.get("description"), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif "name" in tool: + simplified_tool = { + "name": tool.get("name"), + "description": tool.get("description"), + } + simplified_tools.append(simplified_tool) + else: + name = ( + tool.get("name") + or tool.get("tool_name") + or tool.get("function_name") + ) + if name: + simplified_tools.append( + { + "name": name, + "description": tool.get("description") + or tool.get("desc"), + } + ) + elif hasattr(tool, "name"): + simplified_tool = { + "name": getattr(tool, "name", None), + "description": getattr(tool, "description", None) + or getattr(tool, "desc", None), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif hasattr(tool, "__name__"): + simplified_tools.append( + { + "name": tool.__name__, + "description": getattr(tool, "__doc__", None), + } + ) + else: + tool_str = str(tool) + if tool_str and tool_str != "": + simplified_tools.append({"name": tool_str, "description": None}) + except Exception: + continue + + return simplified_tools if simplified_tools else None + + +def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: + """Set available tools data on a span if tools are provided.""" + if tools is not None: + simplified_tools = _simplify_langchain_tools(tools) + if simplified_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + simplified_tools, + unpack=False, + ) + + +def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_configure( + callback_manager_cls: type, + inheritable_callbacks: "Callbacks" = None, + local_callbacks: "Callbacks" = None, + *args: "Any", + **kwargs: "Any", + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(LangchainIntegration) + if integration is None: + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + local_callbacks = local_callbacks or [] + + # Handle each possible type of local_callbacks. For each type, we + # extract the list of callbacks to check for SentryLangchainCallback, + # and define a function that would add the SentryLangchainCallback + # to the existing callbacks list. + if isinstance(local_callbacks, BaseCallbackManager): + callbacks_list = local_callbacks.handlers + elif isinstance(local_callbacks, BaseCallbackHandler): + callbacks_list = [local_callbacks] + elif isinstance(local_callbacks, list): + callbacks_list = local_callbacks + else: + logger.debug("Unknown callback type: %s", local_callbacks) + # Just proceed with original function call + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + # Handle each possible type of inheritable_callbacks. + if isinstance(inheritable_callbacks, BaseCallbackManager): + inheritable_callbacks_list = inheritable_callbacks.handlers + elif isinstance(inheritable_callbacks, list): + inheritable_callbacks_list = inheritable_callbacks + else: + inheritable_callbacks_list = [] + + if not any( + isinstance(cb, SentryLangchainCallback) + for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) + ): + sentry_handler = SentryLangchainCallback( + integration.max_spans, + integration.include_prompts, + ) + if isinstance(local_callbacks, BaseCallbackManager): + local_callbacks = local_callbacks.copy() + local_callbacks.handlers = [ + *local_callbacks.handlers, + sentry_handler, + ] + elif isinstance(local_callbacks, BaseCallbackHandler): + local_callbacks = [local_callbacks, sentry_handler] + else: + local_callbacks = [*local_callbacks, sentry_handler] + + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + return new_configure + + +def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: False, + }, + ) as span: + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + else: + start_span_function = get_start_span_function() + + with start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) as span: + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + + return new_invoke + + +def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + else: + start_span_function = get_start_span_function() + + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + input = args[0].get("input") if len(args) >= 1 else None + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Run the agent + result = f(self, *args, **kwargs) + + old_iterator = result + + def new_iterator() -> "Iterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + async def new_iterator_async() -> "AsyncIterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + async for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + if str(type(result)) == "": + result = new_iterator_async() + else: + result = new_iterator() + + return result + + return new_stream + + +def _patch_embeddings_provider(provider_class: "Any") -> None: + """Patch an embeddings provider class with monitoring wrappers.""" + if provider_class is None: + return + + if hasattr(provider_class, "embed_documents"): + provider_class.embed_documents = _wrap_embedding_method( + provider_class.embed_documents + ) + if hasattr(provider_class, "embed_query"): + provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) + if hasattr(provider_class, "aembed_documents"): + provider_class.aembed_documents = _wrap_async_embedding_method( + provider_class.aembed_documents + ) + if hasattr(provider_class, "aembed_query"): + provider_class.aembed_query = _wrap_async_embedding_method( + provider_class.aembed_query + ) + + +def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap sync embedding methods (embed_documents and embed_query).""" + + @wraps(f) + def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + + return new_embedding_method + + +def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap async embedding methods (aembed_documents and aembed_query).""" + + @wraps(f) + async def new_async_embedding_method( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + + return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py new file mode 100644 index 0000000000..3d3856a913 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py @@ -0,0 +1,515 @@ +from functools import wraps +from typing import Any, Callable, List, Optional + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration + +# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. +from sentry_sdk.integrations.langchain import LangchainIntegration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +try: + from langgraph.errors import GraphBubbleUp + from langgraph.graph import StateGraph + from langgraph.pregel import Pregel +except ImportError: + raise DidNotEnable("langgraph not installed") + + +class LanggraphIntegration(Integration): + identifier = "langgraph" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) + # LangGraph lets users create agents using a StateGraph or the Functional API. + # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and + # the functional API execute on a Pregel instance. Pregel is the runtime for the graph + # and the invocation happens on Pregel, so patching the invoke methods takes care of both. + # The streaming methods are not patched, because due to some internal reasons, LangGraph + # will automatically patch the streaming methods to run through invoke, and by doing this + # we prevent duplicate spans for invocations. + StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) + if hasattr(Pregel, "invoke"): + Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) + if hasattr(Pregel, "ainvoke"): + Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) + + +def _get_graph_name(graph_obj: "Any") -> "Optional[str]": + for attr in ["name", "graph_name", "__name__", "_name"]: + if hasattr(graph_obj, attr): + name = getattr(graph_obj, attr) + if name and isinstance(name, str): + return name + return None + + +def _normalize_langgraph_message(message: "Any") -> "Any": + if not hasattr(message, "content"): + return None + + parsed = {"role": getattr(message, "type", None), "content": message.content} + + for attr in [ + "name", + "tool_calls", + "function_call", + "tool_call_id", + "response_metadata", + ]: + if hasattr(message, attr): + value = getattr(message, attr) + if value is not None: + parsed[attr] = value + + return parsed + + +def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": + if not state: + return None + + messages = None + + if isinstance(state, dict): + messages = state.get("messages") + elif hasattr(state, "messages"): + messages = state.messages + elif hasattr(state, "get") and callable(state.get): + try: + messages = state.get("messages") + except Exception: + pass + + if not messages or not isinstance(messages, (list, tuple)): + return None + + normalized_messages = [] + for message in messages: + try: + normalized = _normalize_langgraph_message(message) + if normalized: + normalized_messages.append(normalized) + except Exception: + continue + + return normalized_messages if normalized_messages else None + + +def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None or has_span_streaming_enabled(client.options): + return f(self, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GEN_AI_CREATE_AGENT, + origin=LanggraphIntegration.origin, + ) as span: + compiled_graph = f(self, *args, **kwargs) + + compiled_graph_name = getattr(compiled_graph, "name", None) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) + + if compiled_graph_name: + span.description = f"create_agent {compiled_graph_name}" + else: + span.description = "create_agent" + + if kwargs.get("model", None) is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) + + tools = None + get_graph = getattr(compiled_graph, "get_graph", None) + if get_graph and callable(get_graph): + graph_obj = compiled_graph.get_graph() + nodes = getattr(graph_obj, "nodes", None) + if nodes and isinstance(nodes, dict): + tools_node = nodes.get("tools") + if tools_node: + data = getattr(tools_node, "data", None) + if data and hasattr(data, "tools_by_name"): + tools = list(data.tools_by_name.keys()) + + if tools is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) + + return compiled_graph + + return new_compile + + +def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + else: + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_invoke + + +def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles(input_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_ainvoke + + +def _get_new_messages( + input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" +) -> "Optional[List[Any]]": + """Extract only the new messages added during this invocation.""" + if not output_messages: + return None + + if not input_messages: + return output_messages + + # only return the new messages, aka the output messages that are not in the input messages + input_count = len(input_messages) + new_messages = ( + output_messages[input_count:] if len(output_messages) > input_count else [] + ) + + return new_messages if new_messages else None + + +def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": + if not messages: + return None + + for message in reversed(messages): + if isinstance(message, dict): + role = message.get("role") + if role in ["assistant", "ai"]: + content = message.get("content") + if content and isinstance(content, str): + return content + + return None + + +def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": + if not messages: + return None + + tool_calls = [] + for message in messages: + if isinstance(message, dict): + msg_tool_calls = message.get("tool_calls") + if msg_tool_calls and isinstance(msg_tool_calls, list): + tool_calls.extend(msg_tool_calls) + + return tool_calls if tool_calls else None + + +def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + input_tokens = 0 + output_tokens = 0 + total_tokens = 0 + + for message in messages: + response_metadata = message.get("response_metadata") + if response_metadata is None: + continue + + token_usage = response_metadata.get("token_usage") + if not token_usage: + continue + + input_tokens += int(token_usage.get("prompt_tokens", 0)) + output_tokens += int(token_usage.get("completion_tokens", 0)) + total_tokens += int(token_usage.get("total_tokens", 0)) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens > 0: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + total_tokens, + ) + + +def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + if len(messages) == 0: + return + + last_message = messages[-1] + response_metadata = last_message.get("response_metadata") + if response_metadata is None: + return + + model_name = response_metadata.get("model_name") + if model_name is None: + return + + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) + + +def _set_response_attributes( + span: "Any", + input_messages: "Optional[List[Any]]", + result: "Any", + integration: "LanggraphIntegration", +) -> None: + parsed_response_messages = _parse_langgraph_messages(result) + new_messages = _get_new_messages(input_messages, parsed_response_messages) + + if new_messages is None: + return + + _set_usage_data(span, new_messages) + _set_response_model_name(span, new_messages) + + if not (should_send_default_pii() and integration.include_prompts): + return + + llm_response_text = _extract_llm_response_text(new_messages) + if llm_response_text: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) + elif new_messages: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) + else: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) + + tool_calls = _extract_tool_calls(new_messages) + if tool_calls: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(tool_calls), + unpack=False, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py new file mode 100644 index 0000000000..3c2c76450c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + import ldclient + from ldclient.hook import Hook, Metadata + + if TYPE_CHECKING: + from typing import Any + + from ldclient import LDClient + from ldclient.evaluation import EvaluationDetail + from ldclient.hook import EvaluationSeriesContext +except ImportError: + raise DidNotEnable("LaunchDarkly is not installed") + + +class LaunchDarklyIntegration(Integration): + identifier = "launchdarkly" + + def __init__(self, ld_client: "LDClient | None" = None) -> None: + """ + :param client: An initialized LDClient instance. If a client is not provided, this + integration will attempt to use the shared global instance. + """ + try: + client = ld_client or ldclient.get() + except Exception as exc: + raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) + + if not client.is_initialized(): + raise DidNotEnable("LaunchDarkly client is not initialized.") + + # Register the flag collection hook with the LD client. + client.add_hook(LaunchDarklyHook()) + + @staticmethod + def setup_once() -> None: + pass + + +class LaunchDarklyHook(Hook): + @property + def metadata(self) -> "Metadata": + return Metadata(name="sentry-flag-auditor") + + def after_evaluation( + self, + series_context: "EvaluationSeriesContext", + data: "dict[Any, Any]", + detail: "EvaluationDetail", + ) -> "dict[Any, Any]": + if isinstance(detail.value, bool): + add_feature_flag(series_context.key, detail.value) + + return data + + def before_evaluation( + self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" + ) -> "dict[Any, Any]": + return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py new file mode 100644 index 0000000000..49ead6b068 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py @@ -0,0 +1,376 @@ +import copy +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + set_data_normalized, + transform_openai_content_part, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from datetime import datetime + from typing import Any, Dict, List + +try: + import litellm # type: ignore[import-not-found] + from litellm import failure_callback, input_callback, success_callback +except ImportError: + raise DidNotEnable("LiteLLM not installed") + + +# Stash the span on a top-level key of the per-request kwargs dict litellm passes +# to every callback, so it lives and dies with the request. +_SPAN_KEY = "_sentry_span" + + +def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: + kwargs[_SPAN_KEY] = span + + +def _peek_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.get(_SPAN_KEY) + + +def _pop_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.pop(_SPAN_KEY, None) + + +def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": + """ + Convert the message parts from OpenAI format to the `gen_ai.request.messages` format + using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). + + Deep copies messages to avoid mutating original kwargs. + """ + # Deep copy to avoid mutating original messages from kwargs + messages = copy.deepcopy(messages) + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_openai_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + message["content"] = transformed + return messages + + +def _input_callback(kwargs: "Dict[str, Any]") -> None: + """Handle the start of a request.""" + client = sentry_sdk.get_client() + integration = client.get_integration(LiteLLMIntegration) + + if integration is None: + return + + # Get key parameters + full_model = kwargs.get("model", "") + try: + model, provider, _, _ = litellm.get_llm_provider(full_model) + except Exception: + model = full_model + provider = "unknown" + + call_type = kwargs.get("call_type", None) + if call_type == "embedding" or call_type == "aembedding": + operation = "embeddings" + else: + operation = "chat" + + # Start a new span/transaction + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"{operation} {model}", + attributes={ + "sentry.op": ( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + "sentry.origin": LiteLLMIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + name=f"{operation} {model}", + origin=LiteLLMIntegration.origin, + ) + span.__enter__() + + _store_span(kwargs, span) + + # Set basic data + set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) + + # Record input/messages if allowed + if should_send_default_pii() and integration.include_prompts: + if operation == "embeddings": + # For embeddings, look for the 'input' parameter + embedding_input = kwargs.get("input") + if embedding_input: + scope = sentry_sdk.get_current_scope() + # Normalize to list format + input_list = ( + embedding_input + if isinstance(embedding_input, list) + else [embedding_input] + ) + client = sentry_sdk.get_client() + messages_data = ( + truncate_and_annotate_embedding_inputs(input_list, span, scope) + if should_truncate_gen_ai_input(client.options) + else input_list + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + messages_data, + unpack=False, + ) + else: + # For chat, look for the 'messages' parameter + messages = kwargs.get("messages", []) + if messages: + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages = _convert_message_parts(messages) + messages_data = ( + truncate_and_annotate_messages(messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Record other parameters + params = { + "model": SPANDATA.GEN_AI_REQUEST_MODEL, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + } + for key, attribute in params.items(): + value = kwargs.get(key) + if value is not None: + set_data_normalized(span, attribute, value) + + +async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: + return _input_callback(kwargs) + + +def _success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle successful completion.""" + + span = _peek_span(kwargs) + if span is None: + return + + integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) + if integration is None: + return + + try: + # Record model information + if hasattr(completion_response, "model"): + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model + ) + + # Record response content if allowed + if should_send_default_pii() and integration.include_prompts: + if hasattr(completion_response, "choices"): + response_messages = [] + for choice in completion_response.choices: + if hasattr(choice, "message"): + if hasattr(choice.message, "model_dump"): + response_messages.append(choice.message.model_dump()) + elif hasattr(choice.message, "dict"): + response_messages.append(choice.message.dict()) + else: + # Fallback for basic message objects + msg = {} + if hasattr(choice.message, "role"): + msg["role"] = choice.message.role + if hasattr(choice.message, "content"): + msg["content"] = choice.message.content + if hasattr(choice.message, "tool_calls"): + msg["tool_calls"] = choice.message.tool_calls + response_messages.append(msg) + + if response_messages: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages + ) + + # Record token usage + if hasattr(completion_response, "usage"): + usage = completion_response.usage + record_token_usage( + span, + input_tokens=getattr(usage, "prompt_tokens", None), + output_tokens=getattr(usage, "completion_tokens", None), + total_tokens=getattr(usage, "total_tokens", None), + ) + + finally: + is_streaming = kwargs.get("stream") + # Callback is fired multiple times when streaming a response. + # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 + if ( + is_streaming is not True + or "complete_streaming_response" in kwargs + or "async_complete_streaming_response" in kwargs + ): + span = _pop_span(kwargs) + if span is not None: + span.__exit__(None, None, None) + + +async def _async_success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + return _success_callback( + kwargs, + completion_response, + start_time, + end_time, + ) + + +def _failure_callback( + kwargs: "Dict[str, Any]", + exception: Exception, + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle request failure.""" + span = _pop_span(kwargs) + if span is None: + return + + try: + # Capture the exception + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "litellm", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + finally: + # Always finish the span and clean up + span.__exit__(type(exception), exception, None) + + +class LiteLLMIntegration(Integration): + """ + LiteLLM integration for Sentry. + + This integration automatically captures LiteLLM API calls and sends them to Sentry + for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM + supports, including OpenAI, Anthropic, Google, Cohere, and many others. + + Features: + - Automatic exception capture for all LiteLLM calls + - Token usage tracking across all providers + - Provider detection and attribution + - Input/output message capture (configurable) + - Streaming response support + - Cost tracking integration + + Usage: + + ```python + import litellm + import sentry_sdk + + # Initialize Sentry with the LiteLLM integration + sentry_sdk.init( + dsn="your-dsn", + send_default_pii=True + integrations=[ + sentry_sdk.integrations.LiteLLMIntegration( + include_prompts=True # Set to False to exclude message content + ) + ] + ) + + # All LiteLLM calls will now be monitored + response = litellm.completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Hello!"}] + ) + ``` + + Configuration: + - include_prompts (bool): Whether to include prompts and responses in spans. + Defaults to True. Set to False to exclude potentially sensitive data. + """ + + identifier = "litellm" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """Set up LiteLLM callbacks for monitoring.""" + litellm.input_callback = input_callback or [] + if _input_callback not in litellm.input_callback: + litellm.input_callback.append(_input_callback) + if _async_input_callback not in litellm.input_callback: + litellm.input_callback.append(_async_input_callback) + + litellm.success_callback = success_callback or [] + if _success_callback not in litellm.success_callback: + litellm.success_callback.append(_success_callback) + if _async_success_callback not in litellm.success_callback: + litellm.success_callback.append(_async_success_callback) + + litellm.failure_callback = failure_callback or [] + if _failure_callback not in litellm.failure_callback: + litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py new file mode 100644 index 0000000000..f0c90a7921 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py @@ -0,0 +1,364 @@ +from collections.abc import Set +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from litestar import Litestar, Request # type: ignore + from litestar.data_extractors import ConnectionDataExtractor # type: ignore + from litestar.exceptions import HTTPException # type: ignore + from litestar.handlers.base import BaseRouteHandler # type: ignore + from litestar.middleware import DefineMiddleware # type: ignore + from litestar.routes.http import HTTPRoute # type: ignore +except ImportError: + raise DidNotEnable("Litestar is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from litestar.middleware import MiddlewareProtocol + from litestar.types import ( # type: ignore + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from litestar.types import ( + Scope as LitestarScope, + ) + from litestar.types.asgi_types import ASGIApp # type: ignore + + from sentry_sdk._types import Event, Hint + +_DEFAULT_TRANSACTION_NAME = "generic Litestar request" + + +class LitestarIntegration(Integration): + identifier = "litestar" + origin = f"auto.http.{identifier}" + + def __init__( + self, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. + # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following + # (among other things): + # 1. Logs them, some at least (such as 500s) as errors + # 2. Calls after_exception hooks + # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event + # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will + # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. + ignore_logger("litestar") + + +class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + def _capture_request_exception(self, exc: Exception) -> None: + """Avoid catching exceptions from request handlers. + + Those exceptions are already handled in Litestar.after_exception handler. + We still catch exceptions from application lifespan handlers. + """ + pass + + +def patch_app_init() -> None: + """ + Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. + See: + - https://docs.litestar.dev/2/usage/applications.html#after-exception + - https://docs.litestar.dev/2/usage/middleware/using-middleware.html + """ + old__init__ = Litestar.__init__ + + @ensure_integration_enabled(LitestarIntegration, old__init__) + def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: + kwargs["after_exception"] = [ + exception_handler, + *(kwargs.get("after_exception") or []), + ] + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Litestar.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryLitestarASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "LitestarScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(LitestarIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=middleware_name, + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR, + "sentry.origin": LitestarIntegration.origin, + }, + ) as middleware_span: + middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.traces.start_span( + name=getattr(receive, "__qualname__", str(receive)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.traces.start_span( + name=getattr(send, "__qualname__", str(send)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + else: + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR, + name=middleware_name, + origin=LitestarIntegration.origin, + ) as middleware_span: + middleware_span.set_tag("litestar.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_SEND, + name=getattr(send, "__qualname__", str(send)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type + elif hasattr(route_handler.fn, "value"): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = LitestarIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if isinstance(scope_user, dict): + return scope_user + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + return None + + +@ensure_integration_enabled(LitestarIntegration) +def exception_handler(exc: Exception, scope: "LitestarScope") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + if isinstance(exc, HTTPException): + integration = sentry_sdk.get_client().get_integration(LitestarIntegration) + if ( + integration is not None + and exc.status_code not in integration.failed_request_status_codes + ): + return + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": LitestarIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py new file mode 100644 index 0000000000..a310a0ced6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py @@ -0,0 +1,476 @@ +import logging +import sys +from datetime import datetime, timezone +from fnmatch import fnmatch +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.client import BaseClient +from sentry_sdk.integrations import Integration +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import ( + capture_internal_exceptions, + current_stacktrace, + event_from_exception, + has_logs_enabled, + safe_repr, + to_string, +) + +if TYPE_CHECKING: + from collections.abc import MutableMapping + from logging import LogRecord + from typing import Any, Dict, Optional + +DEFAULT_LEVEL = logging.INFO +DEFAULT_EVENT_LEVEL = logging.ERROR +LOGGING_TO_EVENT_LEVEL = { + logging.NOTSET: "notset", + logging.DEBUG: "debug", + logging.INFO: "info", + logging.WARN: "warning", # WARN is same a WARNING + logging.WARNING: "warning", + logging.ERROR: "error", + logging.FATAL: "fatal", + logging.CRITICAL: "fatal", # CRITICAL is same as FATAL +} + +# Map logging level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + logging.CRITICAL: 21, # fatal + logging.ERROR: 17, # error + logging.WARNING: 13, # warn + logging.INFO: 9, # info + logging.DEBUG: 5, # debug +} + + +# Capturing events from those loggers causes recursion errors. We cannot allow +# the user to unconditionally create events from those loggers under any +# circumstances. +# +# Note: Ignoring by logger name here is better than mucking with thread-locals. +# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. +# +# Events/breadcrumbs and Sentry Logs have separate ignore lists so that +# framework loggers silenced for events (e.g. django.server) can still be +# captured as Sentry Logs. +_IGNORED_LOGGERS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + +_IGNORED_LOGGERS_SENTRY_LOGS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + + +def ignore_logger( + name: str, +) -> None: + """This disables recording (both in breadcrumbs and as events) calls to + a logger of a specific name. Among other uses, many of our integrations + use this to prevent their actions being recorded as breadcrumbs. Exposed + to users as a way to quiet spammy loggers. + + This does **not** affect Sentry Logs — use + :py:func:`ignore_logger_for_sentry_logs` for that. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS.add(name) + + +def ignore_logger_for_sentry_logs( + name: str, +) -> None: + """This disables recording as Sentry Logs calls to a logger of a + specific name. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS_SENTRY_LOGS.add(name) + + +def unignore_logger( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger` call, re-enabling + recording of breadcrumbs and events for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS.discard(name) + + +def unignore_logger_for_sentry_logs( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, + re-enabling recording of Sentry Logs for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) + + +class LoggingIntegration(Integration): + identifier = "logging" + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + self._handler = None + self._breadcrumb_handler = None + self._sentry_logs_handler = None + + if level is not None: + self._breadcrumb_handler = BreadcrumbHandler(level=level) + + if sentry_logs_level is not None: + self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) + + if event_level is not None: + self._handler = EventHandler(level=event_level) + + def _handle_record(self, record: "LogRecord") -> None: + if self._handler is not None and record.levelno >= self._handler.level: + self._handler.handle(record) + + if ( + self._breadcrumb_handler is not None + and record.levelno >= self._breadcrumb_handler.level + ): + self._breadcrumb_handler.handle(record) + + def _handle_sentry_logs_record(self, record: "LogRecord") -> None: + if ( + self._sentry_logs_handler is not None + and record.levelno >= self._sentry_logs_handler.level + ): + self._sentry_logs_handler.handle(record) + + @staticmethod + def setup_once() -> None: + old_callhandlers = logging.Logger.callHandlers + + def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": + # keeping a local reference because the + # global might be discarded on shutdown + ignored_loggers = _IGNORED_LOGGERS + ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS + + try: + return old_callhandlers(self, record) + finally: + # This check is done twice, once also here before we even get + # the integration. Otherwise we have a high chance of getting + # into a recursion error when the integration is resolved + # (this also is slower). + name = record.name.strip() + + handle_events = ( + ignored_loggers is not None and name not in ignored_loggers + ) + handle_sentry_logs = ( + ignored_loggers_sentry_logs is not None + and name not in ignored_loggers_sentry_logs + ) + + if handle_events or handle_sentry_logs: + integration = sentry_sdk.get_client().get_integration( + LoggingIntegration + ) + if integration is not None: + if handle_events: + integration._handle_record(record) + if handle_sentry_logs: + integration._handle_sentry_logs_record(record) + + logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore + + +class _BaseHandler(logging.Handler): + COMMON_RECORD_ATTRS = frozenset( + ( + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "linenno", + "lineno", + "message", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack", + "tags", + "taskName", + "thread", + "threadName", + "stack_info", + ) + ) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + return LOGGING_TO_EVENT_LEVEL.get( + record.levelno, record.levelname.lower() if record.levelname else "" + ) + + def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": + return { + k: v + for k, v in vars(record).items() + if k not in self.COMMON_RECORD_ATTRS + and (not isinstance(k, str) or not k.startswith("_")) + } + + +class EventHandler(_BaseHandler): + """ + A logging handler that emits Sentry events for each log record + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + client_options = client.options + + # exc_info might be None or (None, None, None) + # + # exc_info may also be any falsy value due to Python stdlib being + # liberal with what it receives and Celery's billiard being "liberal" + # with what it sends. See + # https://github.com/getsentry/sentry-python/issues/904 + if record.exc_info and record.exc_info[0] is not None: + event, hint = event_from_exception( + record.exc_info, + client_options=client_options, + mechanism={"type": "logging", "handled": True}, + ) + elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: + event = {} + hint = {} + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=client_options[ + "include_local_variables" + ], + max_value_length=client_options["max_value_length"], + ), + "crashed": False, + "current": True, + } + ] + } + else: + event = {} + hint = {} + + hint["log_record"] = record + + level = self._logging_to_event_level(record) + if level in {"debug", "info", "warning", "error", "critical", "fatal"}: + event["level"] = level # type: ignore[typeddict-item] + event["logger"] = record.name + + if ( + sys.version_info < (3, 11) + and record.name == "py.warnings" + and record.msg == "%s" + ): + # warnings module on Python 3.10 and below sets record.msg to "%s" + # and record.args[0] to the actual warning message. + # This was fixed in https://github.com/python/cpython/pull/30975. + message = record.args[0] + params = () + else: + message = record.msg + params = record.args + + event["logentry"] = { + "message": to_string(message), + "formatted": record.getMessage(), + "params": params, + } + + event["extra"] = self._extra_from_record(record) + + sentry_sdk.capture_event(event, hint=hint) + + +# Legacy name +SentryHandler = EventHandler + + +class BreadcrumbHandler(_BaseHandler): + """ + A logging handler that records breadcrumbs for each log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + sentry_sdk.add_breadcrumb( + self._breadcrumb_from_record(record), hint={"log_record": record} + ) + + def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": + return { + "type": "log", + "level": self._logging_to_event_level(record), + "category": record.name, + "message": record.message, + "timestamp": datetime.fromtimestamp(record.created, timezone.utc), + "data": self._extra_from_record(record), + } + + +class SentryLogsHandler(_BaseHandler): + """ + A logging handler that records Sentry logs for each Python log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS_SENTRY_LOGS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + self._capture_log_from_record(client, record) + + def _capture_log_from_record( + self, client: "BaseClient", record: "LogRecord" + ) -> None: + otel_severity_number, otel_severity_text = _log_level_to_otel( + record.levelno, SEVERITY_TO_OTEL_SEVERITY + ) + project_root = client.options["project_root"] + + attrs: "Any" = self._extra_from_record(record) + attrs["sentry.origin"] = "auto.log.stdlib" + + parameters_set = False + if record.args is not None: + if isinstance(record.args, tuple): + parameters_set = bool(record.args) + for i, arg in enumerate(record.args): + attrs[f"sentry.message.parameter.{i}"] = ( + arg + if isinstance(arg, (str, float, int, bool)) + else safe_repr(arg) + ) + elif isinstance(record.args, dict): + parameters_set = bool(record.args) + for key, value in record.args.items(): + attrs[f"sentry.message.parameter.{key}"] = ( + value + if isinstance(value, (str, float, int, bool)) + else safe_repr(value) + ) + + if parameters_set and isinstance(record.msg, str): + # only include template if there is at least one + # sentry.message.parameter.X set + attrs["sentry.message.template"] = record.msg + + if record.lineno: + attrs["code.line.number"] = record.lineno + + if record.pathname: + if project_root is not None and record.pathname.startswith(project_root): + attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record.pathname + + if record.funcName: + attrs["code.function.name"] = record.funcName + + if record.thread: + attrs["thread.id"] = record.thread + if record.threadName: + attrs["thread.name"] = record.threadName + + if record.process: + attrs["process.pid"] = record.process + if record.processName: + attrs["process.executable.name"] = record.processName + if record.name: + attrs["logger.name"] = record.name + + # noinspection PyProtectedMember + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record.message, + "attributes": attrs, + "time_unix_nano": int(record.created * 1e9), + "trace_id": None, + "span_id": None, + }, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py new file mode 100644 index 0000000000..dbb724d9a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py @@ -0,0 +1,208 @@ +import enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.logging import ( + BreadcrumbHandler, + EventHandler, + _BaseHandler, +) +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import has_logs_enabled, safe_repr + +if TYPE_CHECKING: + from logging import LogRecord + from typing import Any, Optional + +try: + import loguru + from loguru import logger + from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT + + if TYPE_CHECKING: + from loguru import Message +except ImportError: + raise DidNotEnable("LOGURU is not installed") + + +class LoggingLevels(enum.IntEnum): + TRACE = 5 + DEBUG = 10 + INFO = 20 + SUCCESS = 25 + WARNING = 30 + ERROR = 40 + CRITICAL = 50 + + +DEFAULT_LEVEL = LoggingLevels.INFO.value +DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value + + +SENTRY_LEVEL_FROM_LOGURU_LEVEL = { + "TRACE": "DEBUG", + "DEBUG": "DEBUG", + "INFO": "INFO", + "SUCCESS": "INFO", + "WARNING": "WARNING", + "ERROR": "ERROR", + "CRITICAL": "CRITICAL", +} + +# Map Loguru level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + LoggingLevels.CRITICAL: 21, # fatal + LoggingLevels.ERROR: 17, # error + LoggingLevels.WARNING: 13, # warn + LoggingLevels.SUCCESS: 11, # info + LoggingLevels.INFO: 9, # info + LoggingLevels.DEBUG: 5, # debug + LoggingLevels.TRACE: 1, # trace +} + + +class LoguruIntegration(Integration): + identifier = "loguru" + + level: "Optional[int]" = DEFAULT_LEVEL + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL + breadcrumb_format = DEFAULT_FORMAT + event_format = DEFAULT_FORMAT + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + LoguruIntegration.level = level + LoguruIntegration.event_level = event_level + LoguruIntegration.breadcrumb_format = breadcrumb_format + LoguruIntegration.event_format = event_format + LoguruIntegration.sentry_logs_level = sentry_logs_level + + @staticmethod + def setup_once() -> None: + if LoguruIntegration.level is not None: + logger.add( + LoguruBreadcrumbHandler(level=LoguruIntegration.level), + level=LoguruIntegration.level, + format=LoguruIntegration.breadcrumb_format, + ) + + if LoguruIntegration.event_level is not None: + logger.add( + LoguruEventHandler(level=LoguruIntegration.event_level), + level=LoguruIntegration.event_level, + format=LoguruIntegration.event_format, + ) + + if LoguruIntegration.sentry_logs_level is not None: + logger.add( + loguru_sentry_logs_handler, + level=LoguruIntegration.sentry_logs_level, + ) + + +class _LoguruBaseHandler(_BaseHandler): + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + if kwargs.get("level"): + kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( + kwargs.get("level", ""), DEFAULT_LEVEL + ) + + super().__init__(*args, **kwargs) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + try: + return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ + LoggingLevels(record.levelno).name + ].lower() + except (ValueError, KeyError): + return record.levelname.lower() if record.levelname else "" + + +class LoguruEventHandler(_LoguruBaseHandler, EventHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" + + pass + + +class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" + + pass + + +def loguru_sentry_logs_handler(message: "Message") -> None: + # This is intentionally a callable sink instead of a standard logging handler + # since otherwise we wouldn't get direct access to message.record + client = sentry_sdk.get_client() + + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + record = message.record + + if ( + LoguruIntegration.sentry_logs_level is None + or record["level"].no < LoguruIntegration.sentry_logs_level + ): + return + + otel_severity_number, otel_severity_text = _log_level_to_otel( + record["level"].no, SEVERITY_TO_OTEL_SEVERITY + ) + + attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} + + project_root = client.options["project_root"] + if record.get("file"): + if project_root is not None and record["file"].path.startswith(project_root): + attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record["file"].path + + if record.get("line") is not None: + attrs["code.line.number"] = record["line"] + + if record.get("function"): + attrs["code.function.name"] = record["function"] + + if record.get("thread"): + attrs["thread.name"] = record["thread"].name + attrs["thread.id"] = record["thread"].id + + if record.get("process"): + attrs["process.pid"] = record["process"].id + attrs["process.executable.name"] = record["process"].name + + if record.get("name"): + attrs["logger.name"] = record["name"] + + extra = record.get("extra") + if isinstance(extra, dict): + for key, value in extra.items(): + if isinstance(value, (str, int, float, bool)): + attrs[f"sentry.message.parameter.{key}"] = value + else: + attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record["message"], + "attributes": attrs, + "time_unix_nano": int(record["time"].timestamp() * 1e9), + "trace_id": None, + "span_id": None, + } + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py new file mode 100644 index 0000000000..79381fe06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py @@ -0,0 +1,876 @@ +""" +Sentry integration for MCP (Model Context Protocol) servers. + +This integration instruments MCP servers to create spans for tool, prompt, +and resource handler execution, and captures errors that occur during execution. + +Supports the low-level `mcp.server.lowlevel.Server` API. +""" + +import inspect +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import package_version, safe_serialize + +MCP_PACKAGE_VERSION = package_version("mcp") + +try: + from mcp.server.lowlevel import Server + from mcp.server.streamable_http import ( + StreamableHTTPServerTransport, + ) + + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] + request_ctx, + ) +except ImportError: + raise DidNotEnable("MCP SDK not installed") + +try: + from fastmcp import FastMCP # type: ignore[import-not-found] +except ImportError: + FastMCP = None + +if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + try: + from mcp.server.context import ( + ServerRequestContext, + ) + except ImportError: + ServerRequestContext = None # type: ignore[assignment,misc] +else: + ServerRequestContext = None # type: ignore[assignment,misc] + + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Optional, Tuple, Union + + from starlette.types import Receive, Scope, Send + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +class MCPIntegration(Integration): + identifier = "mcp" + origin = "auto.ai.mcp" + + def __init__(self, include_prompts: bool = True) -> None: + """ + Initialize the MCP integration. + + Args: + include_prompts: Whether to include prompts (tool results and prompt content) + in span data. Requires send_default_pii=True. Default is True. + """ + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """ + Patches MCP server classes to instrument handler execution. + """ + _patch_lowlevel_server() + _patch_handle_request() + + if FastMCP is not None: + _patch_fastmcp() + + +def _get_active_http_scopes( + ctx: "Optional[Any]" = None, +) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return None + + if ( + ctx is None + or not hasattr(ctx, "request") + or ctx.request is None + or "state" not in ctx.request.scope + ): + return None + + return ( + ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), + ctx.request.scope["state"].get("sentry_sdk.current_scope"), + ) + + +def _get_request_context_data( + ctx: "Optional[Any]" = None, +) -> "tuple[Optional[str], Optional[str], str]": + """ + Extract request ID, session ID, and MCP transport type from the request context. + + Returns: + Tuple of (request_id, session_id, mcp_transport). + - request_id: May be None if not available + - session_id: May be None if not available + - mcp_transport: "http", "sse", "stdio" + """ + request_id: "Optional[str]" = None + session_id: "Optional[str]" = None + mcp_transport: str = "stdio" + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return request_id, session_id, mcp_transport + + if ctx is not None: + request_id = ctx.request_id + if hasattr(ctx, "request") and ctx.request is not None: + request = ctx.request + # Detect transport type by checking request characteristics + if hasattr(request, "query_params") and request.query_params.get( + "session_id" + ): + # SSE transport uses query parameter + mcp_transport = "sse" + session_id = request.query_params.get("session_id") + elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): + # StreamableHTTP transport uses header + mcp_transport = "http" + session_id = request.headers.get("mcp-session-id") + + return request_id, session_id, mcp_transport + + +def _get_span_config( + handler_type: str, item_name: str +) -> "tuple[str, str, str, Optional[str]]": + """ + Get span configuration based on handler type. + + Returns: + Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) + Note: result_data_key is None for resources + """ + if handler_type == "tool": + span_data_key = SPANDATA.MCP_TOOL_NAME + mcp_method_name = "tools/call" + result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT + elif handler_type == "prompt": + span_data_key = SPANDATA.MCP_PROMPT_NAME + mcp_method_name = "prompts/get" + result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT + else: # resource + span_data_key = SPANDATA.MCP_RESOURCE_URI + mcp_method_name = "resources/read" + result_data_key = None # Resources don't capture result content + + span_name = f"{mcp_method_name} {item_name}" + return span_data_key, span_name, mcp_method_name, result_data_key + + +def _set_span_input_data( + span: "Union[StreamedSpan, Span]", + handler_name: str, + span_data_key: str, + mcp_method_name: str, + arguments: "dict[str, Any]", + request_id: "Optional[str]", + session_id: "Optional[str]", + mcp_transport: str, +) -> None: + """Set input span data for MCP handlers.""" + + # Set handler identifier + _set_span_data_attribute(span, span_data_key, handler_name) + _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) + + # Set transport/MCP transport type + _set_span_data_attribute( + span, + SPANDATA.NETWORK_TRANSPORT, + "pipe" if mcp_transport == "stdio" else "tcp", + ) + _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) + + # Set request_id if provided + if request_id: + _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) + + # Set session_id if provided + if session_id: + _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) + + # Set request arguments (excluding common request context objects) + for k, v in arguments.items(): + _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) + + +def _extract_tool_result_content(result: "Any") -> "Any": + """ + Extract meaningful content from MCP tool result. + + Tool handlers can return: + - CallToolResult (mcp v2+): Has .content list and optional .structured_content + - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) + - dict (StructuredContent): Return as-is + - list/Iterable (UnstructuredContent): Extract text from content blocks + """ + if result is None: + return None + + # Handle v2 CallToolResult-like objects (has .content list attribute) + if hasattr(result, "content") and isinstance( + getattr(result, "content", None), list + ): + # This is only present when a tool declares an output_schema + structured = getattr(result, "structured_content", None) + if structured is not None: + return structured + return _extract_text_from_content_blocks(result.content) + + # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) + if isinstance(result, tuple) and len(result) == 2: + # Return the structured content (2nd element) + return result[1] + + # Handle StructuredContent: dict + if isinstance(result, dict): + return result + + # Handle UnstructuredContent: iterable of ContentBlock objects + if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): + return _extract_text_from_content_blocks(result) + + return result + + +def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": + texts = [] + try: + for item in content_blocks: + if hasattr(item, "text"): + texts.append(item.text) + elif isinstance(item, dict) and "text" in item: + texts.append(item["text"]) + except Exception: + return content_blocks + return " ".join(texts) if texts else content_blocks + + +def _set_span_output_data( + span: "Union[StreamedSpan, Span]", + result: "Any", + result_data_key: "Optional[str]", + handler_type: str, +) -> None: + """Set output span data for MCP handlers.""" + if result is None: + return + + # Get integration to check PII settings + integration = sentry_sdk.get_client().get_integration(MCPIntegration) + if integration is None: + return + + # Check if we should include sensitive data + should_include_data = should_send_default_pii() and integration.include_prompts + + # For tools, extract the meaningful content + if handler_type == "tool": + extracted = _extract_tool_result_content(result) + if ( + extracted is not None + and should_include_data + and result_data_key is not None + ): + _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) + # Set content count if result is a dict + if isinstance(extracted, dict): + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) + ) + elif handler_type == "prompt": + # For prompts, count messages and set role/content only for single-message prompts + try: + messages: "Optional[list[str]]" = None + message_count = 0 + + # Check if result has messages attribute (GetPromptResult) + if hasattr(result, "messages") and result.messages: + messages = result.messages + message_count = len(messages) + # Also check if result is a dict with messages + elif isinstance(result, dict) and result.get("messages"): + messages = result["messages"] + message_count = len(messages) + + # Always set message count if we found messages + if message_count > 0: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count + ) + + # Only set role and content for single-message prompts if PII is allowed + if message_count == 1 and should_include_data and messages: + first_message = messages[0] + # Extract role + role = None + if hasattr(first_message, "role"): + role = first_message.role + elif isinstance(first_message, dict) and "role" in first_message: + role = first_message["role"] + + if role: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role + ) + + # Extract content text + content_text = None + if hasattr(first_message, "content"): + msg_content = first_message.content + # Content can be a TextContent object or similar + if hasattr(msg_content, "text"): + content_text = msg_content.text + elif isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + elif isinstance(first_message, dict) and "content" in first_message: + msg_content = first_message["content"] + if isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + + if content_text and result_data_key is not None: + _set_span_data_attribute(span, result_data_key, content_text) + except Exception: + # Silently ignore if we can't extract message info + pass + # Resources don't capture result content (result_data_key is None) + + +# Handler data preparation and wrapping + + +def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: + """Check if original_args contains a v2 ServerRequestContext as the first element.""" + return ( + ServerRequestContext is not None + and bool(original_args) + and isinstance(original_args[0], ServerRequestContext) + ) + + +def _extract_handler_data_from_params( + handler_type: str, + params: "Any", +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from a v2 typed params object. + + In MCP SDK v2, handlers receive (ctx, params) where params is a typed + Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). + """ + if handler_type == "tool": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + elif handler_type == "prompt": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + arguments = {"name": handler_name, **arguments} + else: # resource + handler_name = str(getattr(params, "uri", "unknown")) + arguments = {} + + return handler_name, arguments + + +def _extract_handler_data_from_args( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from v1 positional args. + + In MCP SDK v1, handlers receive positional args: + - Tool: (tool_name, arguments) + - Prompt: (name, arguments) + - Resource: (uri,) + """ + original_kwargs = original_kwargs or {} + + if handler_type == "tool": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + elif handler_type == "prompt": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + arguments = {"name": handler_name, **(arguments or {})} + + else: # resource + handler_name = "unknown" + if original_args: + handler_name = str(original_args[0]) + elif original_kwargs.get("uri"): + handler_name = str(original_kwargs["uri"]) + + arguments = {} + + return handler_name, arguments + + +def _prepare_handler_data( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + params: "Optional[Any]" = None, +) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": + """ + Prepare common handler data for both v1 and v2 MCP SDK. + + Args: + handler_type: "tool", "prompt", or "resource" + original_args: Original positional args (v1 path) + original_kwargs: Original keyword args (v1 path) + params: Typed params object from v2 ServerRequestContext path + + Returns: + Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) + """ + if params is not None: + handler_name, arguments = _extract_handler_data_from_params( + handler_type, params + ) + elif _is_v2_context(original_args): + handler_name = "unknown" + arguments = {} + else: + handler_name, arguments = _extract_handler_data_from_args( + handler_type, original_args, original_kwargs + ) + + span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( + handler_type, handler_name + ) + + return ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) + + +async def _handler_wrapper( + handler_type: str, + func: "Callable[..., Any]", + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + self: "Optional[Any]" = None, + force_await: bool = True, +) -> "Any": + """ + Wrapper for MCP handlers. + + Args: + handler_type: "tool", "prompt", or "resource" + func: The handler function to wrap + original_args: Original arguments passed to the handler + original_kwargs: Original keyword arguments passed to the handler + self: Optional instance for bound methods + """ + if original_kwargs is None: + original_kwargs = {} + + # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers + ctx: "Optional[Any]" = None + params: "Optional[Any]" = None + if ( + ServerRequestContext is not None + and original_args + and isinstance(original_args[0], ServerRequestContext) + ): + ctx = original_args[0] + params = original_args[1] if len(original_args) > 1 else None + + ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) = _prepare_handler_data( + handler_type, original_args, original_kwargs, params=params + ) + + scopes = _get_active_http_scopes(ctx=ctx) + + isolation_scope_context: "ContextManager[Any]" + current_scope_context: "ContextManager[Any]" + + if scopes is None: + isolation_scope_context = nullcontext() + current_scope_context = nullcontext() + else: + isolation_scope, current_scope = scopes + + isolation_scope_context = ( + nullcontext() + if isolation_scope is None + else sentry_sdk.scope.use_isolation_scope(isolation_scope) + ) + current_scope_context = ( + nullcontext() + if current_scope is None + else sentry_sdk.scope.use_scope(current_scope) + ) + + # Get request ID, session ID, and transport from context + request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + + # Start span and execute + with isolation_scope_context: + with current_scope_context: + span_mgr: "Union[Span, StreamedSpan]" + if span_streaming: + span_mgr = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.MCP_SERVER, + "sentry.origin": MCPIntegration.origin, + }, + ) + else: + span_mgr = get_start_span_function()( + op=OP.MCP_SERVER, + name=span_name, + origin=MCPIntegration.origin, + ) + + with span_mgr as span: + # Set input span data + _set_span_input_data( + span, + handler_name, + span_data_key, + mcp_method_name, + arguments, + request_id, + session_id, + mcp_transport, + ) + + # For resources, extract and set protocol + if handler_type == "resource": + uri = None + if params is not None: + uri = getattr(params, "uri", None) + + # v1 scenario + if ServerRequestContext is None: + if original_args: + uri = original_args[0] + else: + uri = original_kwargs.get("uri") + + protocol = None + if uri is not None and hasattr(uri, "scheme"): + protocol = uri.scheme + elif handler_name and "://" in handler_name: + protocol = handler_name.split("://")[0] + if protocol: + _set_span_data_attribute( + span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol + ) + + try: + # Execute the async handler + if self is not None: + original_args = (self, *original_args) + + result = func(*original_args, **original_kwargs) + if force_await or inspect.isawaitable(result): + result = await result + + except Exception as e: + # Set error flag for tools + if handler_type == "tool": + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True + ) + sentry_sdk.capture_exception(e) + raise + + _set_span_output_data(span, result, result_data_key, handler_type) + + return result + + +def _create_instrumented_decorator( + original_decorator: "Callable[..., Any]", + handler_type: str, + *decorator_args: "Any", + **decorator_kwargs: "Any", +) -> "Callable[..., Any]": + """ + Create an instrumented version of an MCP decorator. + + This function intercepts MCP decorators (like @server.call_tool()) and injects + Sentry instrumentation into the handler registration flow. The returned decorator + will: + 1. Receive the user's handler function + 2. Pass the instrumented version to the original MCP decorator + + This ensures that when the handler is called at runtime, it's already wrapped + with Sentry spans and metrics collection. + + Args: + original_decorator: The original MCP decorator method (e.g., Server.call_tool) + handler_type: "tool", "prompt", or "resource" - determines span configuration + decorator_args: Positional arguments to pass to the original decorator (e.g., self) + decorator_kwargs: Keyword arguments to pass to the original decorator + + Returns: + A decorator function that instruments handlers before registering them + """ + + def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(func) + async def wrapper(*args: "Any") -> "Any": + return await _handler_wrapper(handler_type, func, args, force_await=False) + + # Then register it with the original MCP decorator + return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) + + return instrumented_decorator + + +_METHOD_TO_HANDLER_TYPE = { + "tools/call": "tool", + "prompts/get": "prompt", + "resources/read": "resource", +} + +# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via +# the Server(...) constructor kwargs rather than add_request_handler. The in-tree +# high-level MCPServer also wires its handlers through these kwargs. +_KWARG_TO_HANDLER_TYPE = { + "on_call_tool": "tool", + "on_get_prompt": "prompt", + "on_read_resource": "resource", +} + + +def _patch_lowlevel_server() -> None: + """ + Patches the mcp.server.lowlevel.Server class to instrument handler execution. + """ + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + _patch_lowlevel_server_v2() + else: + _patch_lowlevel_server_v1() + + +def _patch_lowlevel_server_v1() -> None: + """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" + # Patch call_tool decorator + original_call_tool = Server.call_tool # type: ignore[attr-defined] + + def patched_call_tool( + self: "Server", **kwargs: "Any" + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.call_tool that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_call_tool, "tool", self, **kwargs + )(func) + + Server.call_tool = patched_call_tool # type: ignore[attr-defined] + + # Patch get_prompt decorator + original_get_prompt = Server.get_prompt # type: ignore[attr-defined] + + def patched_get_prompt( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.get_prompt that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_get_prompt, "prompt", self + )(func) + + Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] + + # Patch read_resource decorator + original_read_resource = Server.read_resource # type: ignore[attr-defined] + + def patched_read_resource( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.read_resource that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_read_resource, "resource", self + )(func) + + Server.read_resource = patched_read_resource # type: ignore[attr-defined] + + +def _wrap_v2_handler( + handler_type: str, handler: "Callable[..., Any]" +) -> "Callable[..., Any]": + """Wrap a v2 (ctx, params) handler with Sentry instrumentation. + + Idempotent: an already-wrapped handler is returned unchanged so handlers + registered through more than one path (e.g. MCPServer building a Server) + are not double-wrapped. + """ + if getattr(handler, "__sentry_mcp_wrapped__", False): + return handler + + @wraps(handler) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + return await _handler_wrapper( + handler_type, handler, args, kwargs, force_await=False + ) + + wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] + return wrapper + + +def _patch_lowlevel_server_v2() -> None: + """Patches the v2 Server to wrap tool/prompt/resource handlers. + + Handlers can be registered either via the Server(...) constructor kwargs + (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree + MCPServer and most lowlevel examples use — or via add_request_handler. + Both are patched. + """ + original_init = Server.__init__ + + @wraps(original_init) + def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: + for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): + handler = kwargs.get(kwarg) + if handler is not None: + kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) + original_init(self, *args, **kwargs) + + Server.__init__ = patched_init # type: ignore[method-assign] + + original_add_request_handler = Server.add_request_handler + + def patched_add_request_handler( + self: "Server", + method: str, + params_type: "Any", + handler: "Callable[..., Any]", + *args: "Any", + **kwargs: "Any", + ) -> None: + handler_type = _METHOD_TO_HANDLER_TYPE.get(method) + if handler_type is not None: + handler = _wrap_v2_handler(handler_type, handler) + + original_add_request_handler( + self, method, params_type, handler, *args, **kwargs + ) + + Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] + + +def _patch_handle_request() -> None: + original_handle_request = StreamableHTTPServerTransport.handle_request + + @wraps(original_handle_request) + async def patched_handle_request( + self: "StreamableHTTPServerTransport", + scope: "Scope", + receive: "Receive", + send: "Send", + ) -> None: + scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( + sentry_sdk.get_isolation_scope() + ) + scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() + await original_handle_request(self, scope, receive, send) + + StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] + + +def _patch_fastmcp() -> None: + """ + Patches the standalone fastmcp package's FastMCP class. + + The standalone fastmcp package (v2.14.0+) registers its own handlers for + prompts and resources directly, bypassing the Server decorators we patch. + This function patches the _get_prompt_mcp and _read_resource_mcp methods + to add instrumentation for those handlers. + """ + if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): + original_get_prompt_mcp = FastMCP._get_prompt_mcp + + @wraps(original_get_prompt_mcp) + async def patched_get_prompt_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "prompt", + original_get_prompt_mcp, + args, + kwargs, + self, + ) + + FastMCP._get_prompt_mcp = patched_get_prompt_mcp + + if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): + original_read_resource_mcp = FastMCP._read_resource_mcp + + @wraps(original_read_resource_mcp) + async def patched_read_resource_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "resource", + original_read_resource_mcp, + args, + kwargs, + self, + ) + + FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py new file mode 100644 index 0000000000..b6111492bb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import _get_installed_modules + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + + +class ModulesIntegration(Integration): + identifier = "modules" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Any") -> "Event": + if event.get("type") == "transaction": + return event + + if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: + return event + + event["modules"] = _get_installed_modules() + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py new file mode 100644 index 0000000000..186c665ed1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py @@ -0,0 +1,1530 @@ +import json +import sys +import time +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai._openai_completions_api import ( + _get_system_instructions as _get_system_instructions_completions, +) +from sentry_sdk.ai._openai_completions_api import ( + _get_text_items, + _transform_system_instructions, +) +from sentry_sdk.ai._openai_completions_api import ( + _is_system_instruction as _is_system_instruction_completions, +) +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions as _get_system_instructions_responses, +) +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction as _is_system_instruction_responses, +) +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, + safe_serialize, +) + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Iterable, + Iterator, + List, + Optional, + Union, + ) + + from openai import Omit + from openai.types import CompletionUsage + from openai.types.responses import ( + ResponseInputParam, + ResponseStreamEvent, + SequenceNotStr, + ) + from openai.types.responses.response_usage import ResponseUsage + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +try: + try: + from openai import NotGiven + except ImportError: + NotGiven = None + + try: + from openai import Omit + except ImportError: + Omit = None + + from openai import AsyncStream, Stream + from openai.resources import AsyncEmbeddings, Embeddings + from openai.resources.chat.completions import AsyncCompletions, Completions + + if TYPE_CHECKING: + from openai.types.chat import ( + ChatCompletionChunk, + ChatCompletionMessageParam, + ) +except ImportError: + raise DidNotEnable("OpenAI not installed") + +RESPONSES_API_ENABLED = True +try: + # responses API support was introduced in v1.66.0 + from openai.resources.responses import AsyncResponses, Responses + from openai.types.responses.response_completed_event import ResponseCompletedEvent +except ImportError: + RESPONSES_API_ENABLED = False + + +class OpenAIIntegration(Integration): + identifier = "openai" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "OpenAIIntegration", + include_prompts: bool = True, + tiktoken_encoding_name: "Optional[str]" = None, + ) -> None: + self.include_prompts = include_prompts + + self.tiktoken_encoding = None + if tiktoken_encoding_name is not None: + import tiktoken # type: ignore + + self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) + + @staticmethod + def setup_once() -> None: + Completions.create = _wrap_chat_completion_create(Completions.create) + AsyncCompletions.create = _wrap_async_chat_completion_create( + AsyncCompletions.create + ) + + Embeddings.create = _wrap_embeddings_create(Embeddings.create) + AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) + + if RESPONSES_API_ENABLED: + Responses.create = _wrap_responses_create(Responses.create) + AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) + + def count_tokens(self: "OpenAIIntegration", s: str) -> int: + if self.tiktoken_encoding is None: + return 0 + try: + return len(self.tiktoken_encoding.encode_ordinary(s)) + except Exception: + return 0 + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _has_attr_and_is_int( + token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str +) -> bool: + return hasattr(token_usage, attr_name) and isinstance( + getattr(token_usage, attr_name, None), int + ) + + +def _calculate_completions_token_usage( + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + streaming_message_total_token_usage: "Optional[CompletionUsage]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Chat Completions API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + usage = None + + if streaming_message_total_token_usage is not None: + usage = streaming_message_total_token_usage + elif hasattr(response, "usage"): + usage = response.usage + + if usage is not None: + if _has_attr_and_is_int(usage, "prompt_tokens"): + input_tokens = usage.prompt_tokens + if _has_attr_and_is_int(usage, "completion_tokens"): + output_tokens = usage.completion_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "prompt_tokens_details"): + cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "completion_tokens_details"): + reasoning = getattr( + usage.completion_tokens_details, "reasoning_tokens", None + ) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in messages or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "choices") and response.choices is not None: + for choice in response.choices: + if hasattr(choice, "message") and hasattr(choice.message, "content"): + output_tokens += count_tokens(choice.message.content) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _calculate_responses_token_usage( + input: "Any", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Responses API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + + if hasattr(response, "usage"): + usage = response.usage + + if _has_attr_and_is_int(usage, "input_tokens"): + input_tokens = usage.input_tokens + if _has_attr_and_is_int(usage, "output_tokens"): + output_tokens = usage.output_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "input_tokens_details"): + cached = getattr(usage.input_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "output_tokens_details"): + reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in input or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + # Deliberate use of Completions function for both Completions and Responses input format. + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "output"): + for output_item in response.output: + if hasattr(output_item, "content"): + for content_item in output_item.content: + if hasattr(content_item, "text"): + output_tokens += count_tokens(content_item.text) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _set_responses_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") + messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_output_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + conversation = kwargs.get("conversation") + if conversation is not None and _is_given(conversation): + conversation_id: "Optional[str]" = None + if isinstance(conversation, str): + conversation_id = conversation + elif isinstance(conversation, dict): + conversation_id = conversation.get("id") + if conversation_id is not None: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + if not should_send_default_pii() or not integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if ( + messages is None + and explicit_instructions is not None + and _is_given(explicit_instructions) + ): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + [ + { + "type": "text", + "content": explicit_instructions, + } + ] + ), + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if messages is None: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + instructions_text_parts: "list[TextPart]" = [] + if explicit_instructions is not None and _is_given(explicit_instructions): + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions_responses(messages) + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + non_system_messages = [ + message for message in messages if not _is_system_instruction_responses(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + + +def _set_completions_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( + "messages" + ) + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + presence_penalty = kwargs.get("presence_penalty") + if presence_penalty is not None and _is_given(presence_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) + + frequency_penalty = kwargs.get("frequency_penalty") + if frequency_penalty is not None and _is_given(frequency_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + messages = list(messages) + kwargs["messages"] = messages + + system_instructions = _get_system_instructions_completions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + non_system_messages = [ + message + for message in messages + if not _is_system_instruction_completions(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + +def _set_embeddings_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( + "input" + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + return + + if isinstance(messages, str): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + return + + messages = list(messages) + kwargs["input"] = messages + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + +def _set_common_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + input: "Any", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + if hasattr(response, "model"): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) + + # Chat Completions API + if hasattr(response, "choices") and response.choices is not None: + if should_send_default_pii() and integration.include_prompts: + response_text = [ + choice.message.model_dump() + for choice in response.choices + if choice.message is not None + ] + if len(response_text) > 0: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) + + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + # Responses API + elif hasattr(response, "output"): + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in response.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + # Embeddings API (fallback for responses with neither choices nor output) + else: + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_synchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_asynchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _set_completions_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + _set_common_output_data( + span, + response, + messages, + integration, + finish_span, + ) + + +def _wrap_synchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Stream[ChatCompletionChunk]", + old_iterator: "Iterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "Iterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "AsyncStream[ChatCompletionChunk]", + old_iterator: "AsyncIterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "AsyncIterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + async for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +def _wrap_synchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "Stream[ResponseStreamEvent]", + old_iterator: "Iterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "Iterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "AsyncStream[ResponseStreamEvent]", + old_iterator: "AsyncIterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "AsyncIterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft: "Optional[float]" = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + async for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _set_responses_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _set_embeddings_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + return _new_sync_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + return await _new_async_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +async def _new_async_embeddings_create( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +def _wrap_embeddings_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_embeddings_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_synchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_asynchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _wrap_responses_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_responses_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_responses_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_responses_create(f, *args, **kwargs) + + return _sentry_patched_responses_async + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different openai versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py new file mode 100644 index 0000000000..5895f53ad3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py @@ -0,0 +1,250 @@ +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .patches import ( + _create_run_streamed_wrapper, + _create_run_wrapper, + _execute_final_output, + _execute_handoffs, + _get_all_tools, + _get_model, + _patch_error_tracing, + _run_single_turn, + _run_single_turn_streamed, +) + +try: + # "agents" is too generic. If someone has an agents.py file in their project + # or another package that's importable via "agents", no ImportError would + # be thrown and the integration would enable itself even if openai-agents is + # not installed. That's why we're adding the second, more specific import + # after it, even if we don't use it. + import agents + from agents.run import AgentRunner + from agents.version import __version__ as OPENAI_AGENTS_VERSION + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +try: + # AgentRunner methods moved in v0.8 + # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 + from agents.run_internal import run_loop, turn_preparation, turn_resolution +except ImportError: + run_loop = None + turn_preparation = None + turn_resolution = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + from agents.run_internal.run_steps import SingleStepResult + + +def _patch_runner() -> None: + # Create the root span for one full agent run (including eventual handoffs) + # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around + # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. + agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run + ) + + # Patch streaming runner + agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run_streamed + ) + + +class OpenAIAgentsIntegration(Integration): + """ + NOTE: With version 0.8.0, the class methods below have been refactored to functions. + - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` + - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` + - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` + - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` + - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` + + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. + 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. + - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. + - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. + 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. + - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. + - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. + 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. + + Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. + Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. + An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. + Handoffs are a way to switch agent-wide configuration. + - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` + """ + + identifier = "openai_agents" + + @staticmethod + def setup_once() -> None: + _patch_error_tracing() + _patch_runner() + + library_version = parse_version(OPENAI_AGENTS_VERSION) + if library_version is not None and library_version >= ( + 0, + 8, + ): + if run_loop is not None: + + @wraps(run_loop.get_all_tools) + async def new_wrapped_get_all_tools( + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools( + run_loop.get_all_tools, agent, context_wrapper + ) + + agents.run.get_all_tools = new_wrapped_get_all_tools + + @wraps(run_loop.run_single_turn) + async def new_wrapped_run_single_turn( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn( + run_loop.run_single_turn, *args, **kwargs + ) + + agents.run.run_single_turn = new_wrapped_run_single_turn + + @wraps(run_loop.run_single_turn_streamed) + async def new_wrapped_run_single_turn_streamed( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + run_loop.run_single_turn_streamed, *args, **kwargs + ) + + agents.run.run_single_turn_streamed = ( + new_wrapped_run_single_turn_streamed + ) + + if turn_preparation is not None: + + @wraps(turn_preparation.get_model) + def new_wrapped_get_model( + agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(turn_preparation.get_model, agent, run_config) + + agents.run_internal.run_loop.get_model = new_wrapped_get_model + + if turn_resolution is not None: + original_execute_handoffs = turn_resolution.execute_handoffs + + @wraps(original_execute_handoffs) + async def new_wrapped_execute_handoffs( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs( + original_execute_handoffs, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_handoffs = ( + new_wrapped_execute_handoffs + ) + + original_execute_final_output = turn_resolution.execute_final_output + + @wraps(turn_resolution.execute_final_output) + async def new_wrapped_final_output( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_final_output = ( + new_wrapped_final_output + ) + + return + + original_get_all_tools = AgentRunner._get_all_tools + + @wraps(AgentRunner._get_all_tools.__func__) + async def old_wrapped_get_all_tools( + cls: "agents.Runner", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + + agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) + + original_get_model = AgentRunner._get_model + + @wraps(AgentRunner._get_model.__func__) + def old_wrapped_get_model( + cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(original_get_model, agent, run_config) + + agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) + + original_run_single_turn = AgentRunner._run_single_turn + + @wraps(AgentRunner._run_single_turn.__func__) + async def old_wrapped_run_single_turn( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn(original_run_single_turn, *args, **kwargs) + + agents.run.AgentRunner._run_single_turn = classmethod( + old_wrapped_run_single_turn + ) + + original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed + + @wraps(AgentRunner._run_single_turn_streamed.__func__) + async def old_wrapped_run_single_turn_streamed( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + original_run_single_turn_streamed, *args, **kwargs + ) + + agents.run.AgentRunner._run_single_turn_streamed = classmethod( + old_wrapped_run_single_turn_streamed + ) + + original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs + + @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) + async def old_wrapped_execute_handoffs( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) + + agents._run_impl.RunImpl.execute_handoffs = classmethod( + old_wrapped_execute_handoffs + ) + + original_execute_final_output = agents._run_impl.RunImpl.execute_final_output + + @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) + async def old_wrapped_final_output( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents._run_impl.RunImpl.execute_final_output = classmethod( + old_wrapped_final_output + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py new file mode 100644 index 0000000000..f5de978be0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py new file mode 100644 index 0000000000..85d48f2d41 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -0,0 +1,10 @@ +from .agent_run import ( + _execute_final_output, # noqa: F401 + _execute_handoffs, # noqa: F401 + _run_single_turn, # noqa: F401 + _run_single_turn_streamed, # noqa: F401 +) +from .error_tracing import _patch_error_tracing # noqa: F401 +from .models import _get_model # noqa: F401 +from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 +from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py new file mode 100644 index 0000000000..71883b2eef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -0,0 +1,332 @@ +import sys +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import ( + handoff_span, + invoke_agent_span, + update_invoke_agent_span, +) + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, Union + + from agents.run_internal.run_steps import SingleStepResult + + from sentry_sdk.tracing import Span + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: + """Check if there's an active agent span for this context""" + return getattr(context_wrapper, "_sentry_current_agent", None) is not None + + +def _get_current_agent( + context_wrapper: "agents.RunContextWrapper", +) -> "Optional[agents.Agent]": + """Get the current agent from context wrapper""" + return getattr(context_wrapper, "_sentry_current_agent", None) + + +def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: + """Close the workflow span for streaming executions if it exists.""" + if agent and hasattr(agent, "_sentry_workflow_span"): + workflow_span = agent._sentry_workflow_span + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + +def _maybe_start_agent_span( + context_wrapper: "agents.RunContextWrapper", + agent: "agents.Agent", + should_run_agent_start_hooks: bool, + span_kwargs: "dict[str, Any]", + is_streaming: bool = False, +) -> "Optional[Union[Span, StreamedSpan]]": + """ + Start an agent invocation span if conditions are met. + Handles ending any existing span for a different agent. + + Returns the new span if started, or the existing span if conditions aren't met. + """ + if not (should_run_agent_start_hooks and agent and context_wrapper): + return getattr(context_wrapper, "_sentry_agent_span", None) + + # End any existing span for a different agent + if _has_active_agent_span(context_wrapper): + current_agent = _get_current_agent(context_wrapper) + if current_agent and current_agent != agent: + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + # Store the agent on the context wrapper so we can access it later + context_wrapper._sentry_current_agent = agent + span = invoke_agent_span(context_wrapper, agent, span_kwargs) + context_wrapper._sentry_agent_span = span + agent._sentry_agent_span = span + + if not is_streaming: + return span + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + return span + + +async def _run_single_turn( + original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn that + - creates agent invocation spans if there is no already active agent invocation span. + - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. + """ + # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. + bindings = kwargs.get("bindings") + agent = ( + getattr(bindings, "public_agent", None) + if bindings is not None + else kwargs.get("agent") + ) + context_wrapper = kwargs.get("context_wrapper") + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) + + span = _maybe_start_agent_span( + context_wrapper, agent, should_run_agent_start_hooks, kwargs + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn(*args, **kwargs) + + try: + result = await original_run_single_turn(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + return result + + +async def _run_single_turn_streamed( + original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn_streamed that + - creates agent invocation spans for streaming if there is no already active agent invocation span. + - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. + + Note: Unlike _run_single_turn which uses keyword-only arguments (*,), + _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: + _run_single_turn_streamed( + streamed_result, # args[0] + bindings, # args[1] + hooks, # args[2] + context_wrapper, # args[3] + run_config, # args[4] + should_run_agent_start_hooks, # args[5] + tool_use_tracker, # args[6] + all_tools, # args[7] + server_conversation_tracker, # args[8] (optional) + ) + """ + streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") + # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. + agent_or_bindings = ( + args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) + ) + agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) + context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") + should_run_agent_start_hooks = bool( + args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) + ) + + span_kwargs: "dict[str, Any]" = {} + if streamed_result and hasattr(streamed_result, "input"): + span_kwargs["original_input"] = streamed_result.input + + span = _maybe_start_agent_span( + context_wrapper, + agent, + should_run_agent_start_hooks, + span_kwargs, + is_streaming=True, + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn_streamed(*args, **kwargs) + + try: + result = await original_run_single_turn_streamed(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + return result + + +async def _execute_handoffs( + original_execute_handoffs: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_handoffs that + - creates and manages handoff spans. + - ends the agent invocation span. + - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. + """ + + context_wrapper = kwargs.get("context_wrapper") + run_handoffs = kwargs.get("run_handoffs") + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + + # Create Sentry handoff span for the first handoff (agents library only processes the first one) + if run_handoffs: + first_handoff = run_handoffs[0] + handoff_agent_name = first_handoff.handoff.agent_name + handoff_span(context_wrapper, agent, handoff_agent_name) + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + # Call original method with all parameters + try: + return await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + # Call original method with all parameters + try: + result = await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result + + +async def _execute_final_output( + original_execute_final_output: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_final_output that + - ends the agent invocation span. + - ends the workflow span if the response is streamed. + """ + + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + context_wrapper = kwargs.get("context_wrapper") + final_output = kwargs.get("final_output") + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + try: + return await original_execute_final_output(*args, **kwargs) + finally: + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + + try: + result = await original_execute_final_output(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py new file mode 100644 index 0000000000..68dadb3101 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py @@ -0,0 +1,74 @@ +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANSTATUS +from sentry_sdk.traces import SpanStatus +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +def _patch_error_tracing() -> None: + """ + Patches agents error tracing function to inject our span error logic + when a tool execution fails. + + In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span + In older versions, it was at: agents._utils.attach_error_to_current_span + + This works even when the module or function doesn't exist. + """ + error_tracing_module = None + + # Try newer location first (agents.util._error_tracing) + try: + from agents.util import _error_tracing + + error_tracing_module = _error_tracing + except (ImportError, AttributeError): + pass + + # Try older location (agents._utils) + if error_tracing_module is None: + try: + import agents._utils + + error_tracing_module = agents._utils + except (ImportError, AttributeError): + # Module doesn't exist in either location, nothing to patch + return + + # Check if the function exists + if not hasattr(error_tracing_module, "attach_error_to_current_span"): + return + + original_attach_error = error_tracing_module.attach_error_to_current_span + + @wraps(original_attach_error) + def sentry_attach_error_to_current_span( + error: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Wraps agents' error attachment to also set Sentry span status to error. + This allows us to properly track tool execution errors even though + the agents library swallows exceptions. + """ + # Set the current Sentry span to errored + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + current_span = sentry_sdk.get_current_scope().streamed_span + if current_span is not None: + current_span.status = SpanStatus.ERROR + else: + current_span = sentry_sdk.get_current_span() + if current_span is not None: + current_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # Call the original function + return original_attach_error(error, *args, **kwargs) + + error_tracing_module.attach_error_to_current_span = ( + sentry_attach_error_to_current_span + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py new file mode 100644 index 0000000000..634c9fdca1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py @@ -0,0 +1,197 @@ +import copy +import time +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_sentry_baggage_to_headers, + should_propagate_trace, +) +from sentry_sdk.utils import logger + +from ..spans import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + from sentry_sdk.tracing import Span + +try: + import agents + from agents.tool import HostedMCPTool +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _set_response_model_on_agent_span( + agent: "agents.Agent", response_model: "Optional[str]" +) -> None: + """Set the response model on the agent's invoke_agent span if available.""" + if response_model: + agent_span = getattr(agent, "_sentry_agent_span", None) + if agent_span: + if isinstance(agent_span, StreamedSpan): + agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + else: + agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + +def _inject_trace_propagation_headers( + hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" +) -> None: + headers = hosted_tool.tool_config.get("headers") + if headers is None: + headers = {} + hosted_tool.tool_config["headers"] = headers + + mcp_url = hosted_tool.tool_config.get("server_url") + if not mcp_url: + return + + if should_propagate_trace(sentry_sdk.get_client(), mcp_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( + key=key, value=value, mcp_url=mcp_url + ) + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(headers, value) + else: + headers[key] = value + + +def _get_model( + original_get_model: "Callable[..., agents.Model]", + agent: "agents.Agent", + run_config: "agents.RunConfig", +) -> "agents.Model": + """ + Responsible for + - creating and managing AI client spans. + - adding trace propagation headers to tools with type HostedMCPTool. + - setting the response model on agent invocation spans. + """ + # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) + # because we only patch its direct methods, all underlying data can remain unchanged. + model = copy.copy(original_get_model(agent, run_config)) + + # Capture the request model name for spans (agent.model can be None when using defaults) + request_model_name = model.model if hasattr(model, "model") else str(model) + agent._sentry_request_model = request_model_name + + # Wrap _fetch_response if it exists (for OpenAI models) to capture response model + if hasattr(model, "_fetch_response"): + original_fetch_response = model._fetch_response + + @wraps(original_fetch_response) + async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": + response = await original_fetch_response(*args, **kwargs) + if hasattr(response, "model") and response.model: + agent._sentry_response_model = str(response.model) + return response + + model._fetch_response = wrapped_fetch_response + + original_get_response = model.get_response + + @wraps(original_get_response) + async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": + mcp_tools = kwargs.get("tools") + hosted_tools = [] + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + result = await original_get_response(*args, **kwargs) + + # Get response model captured from _fetch_response and clean up + response_model = getattr(agent, "_sentry_response_model", None) + if response_model: + delattr(agent, "_sentry_response_model") + + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, result, response_model, agent) + + return result + + model.get_response = wrapped_get_response + + # Also wrap stream_response for streaming support + if hasattr(model, "stream_response"): + original_stream_response = model.stream_response + + @wraps(original_stream_response) + async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": + span_kwargs = dict(kwargs) + if len(args) > 0: + span_kwargs["system_instructions"] = args[0] + if len(args) > 1: + span_kwargs["input"] = args[1] + + hosted_tools = [] + if len(args) > 3: + mcp_tools = args[3] + + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, span_kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + streaming_response = None + ttft_recorded = False + # Capture start time locally to avoid race conditions with concurrent requests + start_time = time.perf_counter() + + async for event in original_stream_response(*args, **kwargs): + # Detect first content token (text delta event) + if not ttft_recorded and hasattr(event, "delta"): + ttft = time.perf_counter() - start_time + set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) + ttft_recorded = True + + # Capture the full response from ResponseCompletedEvent + if hasattr(event, "response"): + streaming_response = event.response + yield event + + # Update span with response data (usage, output, model) + if streaming_response: + response_model = ( + str(streaming_response.model) + if hasattr(streaming_response, "model") + and streaming_response.model + else None + ) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span( + span, streaming_response, response_model, agent + ) + + model.stream_response = wrapped_stream_response + + return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py new file mode 100644 index 0000000000..5f9996595f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -0,0 +1,221 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import agent_workflow_span, update_invoke_agent_span +from ..utils import _capture_exception + +try: + from agents.exceptions import AgentsException +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, AsyncIterator, Callable + + +def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run methods to + - create and manage a root span for the agent workflow runs. + - end the agent invocation span if an `AgentsException` is raised in `run()`. + + Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), + so it does not need to be wrapped separately. + """ + + @wraps(original_func) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + with agent_workflow_span(agent) as workflow_span: + # Set conversation ID on workflow span early so it's captured even on errors + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + run_result = await original_func(*args, **kwargs) + except AgentsException as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + + context_wrapper = getattr(exc.run_data, "context_wrapper", None) + if context_wrapper is not None: + invoke_agent_span = getattr( + context_wrapper, "_sentry_agent_span", None + ) + + if invoke_agent_span is not None and ( + ( + isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.end_timestamp is None + ) + or ( + not isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.timestamp is None + ) + ): + update_invoke_agent_span( + span=invoke_agent_span, + context=context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Invoke agent span is not finished in this case. + # This is much less likely to occur than other cases because + # AgentRunner.run() is "just" a while loop around _run_single_turn. + _capture_exception(exc) + reraise(*exc_info) + + invoke_agent_span = getattr( + run_result.context_wrapper, "_sentry_agent_span", None + ) + if not invoke_agent_span: + return run_result + + update_invoke_agent_span( + span=invoke_agent_span, + context=run_result.context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(None, None, None) + delattr(run_result.context_wrapper, "_sentry_agent_span") + return run_result + + return wrapper + + +def _create_run_streamed_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run_streamed method to + - create a root span for streaming agent workflow runs. + - end the workflow span if and only if the response stream is consumed or cancelled. + + Unlike run(), run_streamed() returns immediately with a RunResultStreaming object + while execution continues in a background task. The workflow span must stay open + throughout the streaming operation and close when streaming completes or is abandoned. + + Note: We don't use isolation_scope() here because it uses context variables that + cannot span async boundaries (the __enter__ and __exit__ would be called from + different async contexts, causing ValueError). + """ + + @wraps(original_func) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + # Capture conversation_id from kwargs if provided + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + # Start workflow span immediately (before run_streamed returns) + workflow_span = agent_workflow_span(agent) + workflow_span.__enter__() + + # Set conversation ID on workflow span early so it's captured even on errors + if conversation_id: + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + # Store span on agent for cleanup + agent._sentry_workflow_span = workflow_span + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + # Call original function to get RunResultStreaming + run_result = original_func(*args, **kwargs) + except Exception as exc: + # If run_streamed itself fails (not the background task), clean up immediately + workflow_span.__exit__(*sys.exc_info()) + _capture_exception(exc) + raise + + def _close_workflow_span() -> None: + if hasattr(agent, "_sentry_workflow_span"): + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + if hasattr(run_result, "stream_events"): + original_stream_events = run_result.stream_events + + @wraps(original_stream_events) + async def wrapped_stream_events( + *stream_args: "Any", **stream_kwargs: "Any" + ) -> "AsyncIterator[Any]": + try: + async for event in original_stream_events( + *stream_args, **stream_kwargs + ): + yield event + finally: + _close_workflow_span() + + run_result.stream_events = wrapped_stream_events + + if hasattr(run_result, "cancel"): + original_cancel = run_result.cancel + + @wraps(original_cancel) + def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": + try: + return original_cancel(*cancel_args, **cancel_kwargs) + finally: + _close_workflow_span() + + run_result.cancel = wrapped_cancel + + return run_result + + return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py new file mode 100644 index 0000000000..bd13b9d61a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -0,0 +1,70 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import execute_tool_span, update_execute_tool_span + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +async def _get_all_tools( + original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", +) -> "list[agents.Tool]": + """ + Responsible for creating and managing `gen_ai.execute_tool` spans. + """ + # Get the original tools + tools = await original_get_all_tools(agent, context_wrapper) + + wrapped_tools = [] + for tool in tools: + # Wrap only the function tools (for now) + if tool.__class__.__name__ != "FunctionTool": + wrapped_tools.append(tool) + continue + + # Create a new FunctionTool with our wrapped invoke method + original_on_invoke = tool.on_invoke_tool + + def create_wrapped_invoke( + current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" + ) -> "Callable[..., Any]": + @wraps(current_on_invoke) + async def sentry_wrapped_on_invoke_tool( + *args: "Any", **kwargs: "Any" + ) -> "Any": + with execute_tool_span(current_tool, *args, **kwargs) as span: + # We can not capture exceptions in tool execution here because + # `_on_invoke_tool` is swallowing the exception here: + # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 + # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter + # I was unable to monkey patch it because those are evaluated at module import time + # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` + # because it is nested inside this import time code. As if they made it hard to patch on purpose... + result = await current_on_invoke(*args, **kwargs) + update_execute_tool_span(span, agent, current_tool, result) + + return result + + return sentry_wrapped_on_invoke_tool + + wrapped_tool = agents.FunctionTool( + name=tool.name, + description=tool.description, + params_json_schema=tool.params_json_schema, + on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), + strict_json_schema=tool.strict_json_schema, + is_enabled=tool.is_enabled, + ) + wrapped_tools.append(wrapped_tool) + + return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py new file mode 100644 index 0000000000..08802a87a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py @@ -0,0 +1,8 @@ +from .agent_workflow import agent_workflow_span # noqa: F401 +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .handoff import handoff_span # noqa: F401 +from .invoke_agent import ( + invoke_agent_span, # noqa: F401 + update_invoke_agent_span, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py new file mode 100644 index 0000000000..758f06db8d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + from typing import Union + + import agents + + +def agent_workflow_span( + agent: "agents.Agent", +) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": + # Create a transaction or a span if an transaction is already active + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} + ) + + return span + + span = get_start_span_function()( + name=f"{agent.name} workflow", + origin=SPAN_ORIGIN, + ) + + return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py new file mode 100644 index 0000000000..f4f02cb674 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_input_data, + _set_output_data, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from agents import Agent + + +def ai_client_span( + agent: "Agent", get_response_kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + # TODO-anton: implement other types of operations. Now "chat" is hardcoded. + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + _set_agent_data(span, agent) + _set_input_data(span, get_response_kwargs) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + response: "Any", + response_model: "Optional[str]" = None, + agent: "Optional[Agent]" = None, +) -> None: + """Update AI client span with response data (works for streaming and non-streaming).""" + if hasattr(response, "usage") and response.usage: + _set_usage_data(span, response.usage) + + if hasattr(response, "output") and response.output: + _set_output_data(span, response) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + elif hasattr(response, "model") and response.model: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) + + # Set conversation ID from agent if available + if agent: + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py new file mode 100644 index 0000000000..fd3a430951 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def execute_tool_span( + tool: "agents.Tool", *args: "Any", **kwargs: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool.name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool.name, + SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool.name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) + + set_on_span = span.set_data + + if should_send_default_pii(): + input = args[1] + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + agent: "agents.Agent", + tool: "agents.Tool", + result: "Any", +) -> None: + _set_agent_data(span, agent) + + if isinstance(result, str) and result.startswith( + "An error occurred while running the tool" + ): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if should_send_default_pii(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py new file mode 100644 index 0000000000..ea91464afb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + import agents + + +def handoff_span( + context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str +) -> None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"handoff from {from_agent.name} to {to_agent_name}", + attributes={ + "sentry.op": OP.GEN_AI_HANDOFF, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "handoff", + }, + ) as span: + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_HANDOFF, + name=f"handoff from {from_agent.name} to {to_agent_name}", + origin=SPAN_ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") + + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py new file mode 100644 index 0000000000..c21145ac4a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -0,0 +1,122 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _set_usage_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def invoke_agent_span( + context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {agent.name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + start_span_function = get_start_span_function() + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {agent.name}", + origin=SPAN_ORIGIN, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + if should_send_default_pii(): + messages = [] + if agent.instructions: + message = ( + agent.instructions + if isinstance(agent.instructions, str) + else safe_serialize(agent.instructions) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "system", + } + ) + + original_input = kwargs.get("original_input") + if original_input is not None: + message = ( + original_input + if isinstance(original_input, str) + else safe_serialize(original_input) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "user", + } + ) + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + _set_agent_data(span, agent) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + context: "agents.RunContextWrapper", + agent: "agents.Agent", + output: "Any" = None, +) -> None: + # Add aggregated usage data from context_wrapper + if hasattr(context, "usage"): + _set_usage_data(span, context.usage) + + if should_send_default_pii(): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py new file mode 100644 index 0000000000..224a5f66ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py @@ -0,0 +1,244 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai._openai_completions_api import _transform_system_instructions +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions, + _is_system_instruction, +) +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + normalize_message_role, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import should_truncate_gen_ai_input +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Union + + from agents import TResponseInputItem, Usage + + from sentry_sdk._types import TextPart + +try: + import agents + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai_agents", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span( + SPANDATA.GEN_AI_SYSTEM, "openai" + ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. + + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) + + if agent.model_settings.max_tokens: + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) + + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + if agent.model_settings.presence_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + agent.model_settings.presence_penalty, + ) + + if agent.model_settings.temperature: + set_on_span( + SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature + ) + + if agent.model_settings.top_p: + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) + + if agent.model_settings.frequency_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + agent.model_settings.frequency_penalty, + ) + + if len(agent.tools) > 0: + set_on_span( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + safe_serialize([vars(tool) for tool in agent.tools]), + ) + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage.input_tokens_details.cached_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage.output_tokens_details.reasoning_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) + + +def _set_input_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + get_response_kwargs: "dict[str, Any]", +) -> None: + if not should_send_default_pii(): + return + request_messages = [] + + messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) + + instructions_text_parts: "list[TextPart]" = [] + explicit_instructions = get_response_kwargs.get("system_instructions") + if explicit_instructions is not None: + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions(messages) + + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + non_system_messages = [ + message for message in messages if not _is_system_instruction(message) + ] + for message in non_system_messages: + if "role" in message: + normalized_role = normalize_message_role(message.get("role")) # type: ignore + content = message.get("content") # type: ignore + request_messages.append( + { + "role": normalized_role, + "content": ( + [{"type": "text", "text": content}] + if isinstance(content, str) + else content + ), + } + ) + else: + if message.get("type") == "function_call": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, + "content": [message], + } + ) + elif message.get("type") == "function_call_output": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, + "content": [message], + } + ) + + normalized_messages = normalize_message_roles(request_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + if not should_send_default_pii(): + return + + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in result.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py new file mode 100644 index 0000000000..281604fe38 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from openfeature import api + from openfeature.hook import Hook + + if TYPE_CHECKING: + from openfeature.hook import HookContext, HookHints +except ImportError: + raise DidNotEnable("OpenFeature is not installed") + + +class OpenFeatureIntegration(Integration): + identifier = "openfeature" + + @staticmethod + def setup_once() -> None: + # Register the hook within the global openfeature hooks list. + api.add_hooks(hooks=[OpenFeatureHook()]) + + +class OpenFeatureHook(Hook): + def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: + if isinstance(details.value, bool): + add_feature_flag(details.flag_key, details.value) + + def error( + self, hook_context: "HookContext", exception: Exception, hints: "HookHints" + ) -> None: + if isinstance(hook_context.default_value, bool): + add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py new file mode 100644 index 0000000000..d76b8058ee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py @@ -0,0 +1,7 @@ +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor + +__all__ = [ + "SentryPropagator", + "SentrySpanProcessor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py new file mode 100644 index 0000000000..d6733036ea --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py @@ -0,0 +1,9 @@ +from sentry_sdk.integrations import DidNotEnable + +try: + from opentelemetry.context import create_key +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +SENTRY_TRACE_KEY = create_key("sentry-trace") +SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py new file mode 100644 index 0000000000..472e8181f9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py @@ -0,0 +1,81 @@ +""" +IMPORTANT: The contents of this file are part of a proof of concept and as such +are experimental and not suitable for production use. They may be changed or +removed at any time without prior notice. +""" + +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + +try: + from opentelemetry import trace + from opentelemetry.propagate import set_global_textmap + from opentelemetry.sdk.trace import TracerProvider +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +try: + from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] + DjangoInstrumentor, + ) +except ImportError: + DjangoInstrumentor = None + + +CONFIGURABLE_INSTRUMENTATIONS = { + DjangoInstrumentor: {"is_sql_commentor_enabled": True}, +} + + +class OpenTelemetryIntegration(Integration): + identifier = "opentelemetry" + + @staticmethod + def setup_once() -> None: + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if has_span_streaming_enabled(options): + logger.warning( + "[OTel] OpenTelemetryIntegration is not compatible with span streaming " + "(trace_lifecycle='stream') and will be disabled." + ) + return + + warnings.warn( + "OpenTelemetryIntegration is deprecated. " + "Please use OTLPIntegration instead: " + "https://docs.sentry.io/platforms/python/integrations/otlp/", + DeprecationWarning, + stacklevel=2, + ) + + _setup_sentry_tracing() + # _setup_instrumentors() + + logger.debug("[OTel] Finished setting up OpenTelemetry integration") + + +def _setup_sentry_tracing() -> None: + provider = TracerProvider() + provider.add_span_processor(SentrySpanProcessor()) + trace.set_tracer_provider(provider) + set_global_textmap(SentryPropagator()) + + +def _setup_instrumentors() -> None: + for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): + if instrumentor is None: + continue + instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py new file mode 100644 index 0000000000..5b5f13861d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py @@ -0,0 +1,128 @@ +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.integrations.opentelemetry.span_processor import ( + SentrySpanProcessor, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data + +try: + from opentelemetry import trace + from opentelemetry.context import ( + Context, + get_current, + set_value, + ) + from opentelemetry.propagators.textmap import ( + CarrierT, + Getter, + Setter, + TextMapPropagator, + default_getter, + default_setter, + ) + from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional, Set + + +class SentryPropagator(TextMapPropagator): + """ + Propagates tracing headers for Sentry's tracing system in a way OTel understands. + """ + + def extract( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + getter: "Getter[CarrierT]" = default_getter, + ) -> "Context": + if context is None: + context = get_current() + + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + if not sentry_trace: + return context + + sentrytrace = extract_sentrytrace_data(sentry_trace[0]) + if not sentrytrace: + return context + + context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) + + trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] + + span_context = SpanContext( + trace_id=int(trace_id, 16), # type: ignore + span_id=int(span_id, 16), # type: ignore + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags=TraceFlags(TraceFlags.SAMPLED), + is_remote=True, + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + if baggage_header: + baggage = Baggage.from_incoming_header(baggage_header[0]) + else: + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + baggage = Baggage(sentry_items={}) + + baggage.freeze() + context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) + + span = NonRecordingSpan(span_context) + modified_context = trace.set_span_in_context(span, context) + return modified_context + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + if context is None: + context = get_current() + + current_span = trace.get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + span_id = trace.format_span_id(current_span_context.span_id) + + span_map = SentrySpanProcessor.otel_span_map + sentry_span = span_map.get(span_id, None) + if not sentry_span: + return + + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) + + if sentry_span.containing_transaction: + baggage = sentry_span.containing_transaction.get_baggage() + if baggage: + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + @property + def fields(self) -> "Set[str]": + return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py new file mode 100644 index 0000000000..1efd2ac455 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -0,0 +1,407 @@ +from datetime import datetime, timezone +from time import time +from typing import TYPE_CHECKING, cast + +from urllib3.util import parse_url as urlparse + +from sentry_sdk import get_client, start_transaction +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing import Transaction + +try: + from opentelemetry.context import get_value + from opentelemetry.sdk.trace import ReadableSpan as OTelSpan + from opentelemetry.sdk.trace import SpanProcessor + from opentelemetry.semconv.trace import SpanAttributes + from opentelemetry.trace import ( + SpanKind, + format_span_id, + format_trace_id, + get_current_span, + ) + from opentelemetry.trace.span import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from opentelemetry import context as context_api + from opentelemetry.trace import SpanContext + + from sentry_sdk._types import Event, Hint + +OPEN_TELEMETRY_CONTEXT = "otel" +SPAN_MAX_TIME_OPEN_MINUTES = 10 +SPAN_ORIGIN = "auto.otel" + + +def link_trace_context_to_error_event( + event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" +) -> "Event": + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return event + + if hasattr(event, "type") and event["type"] == "transaction": + return event + + otel_span = get_current_span() + if not otel_span: + return event + + ctx = otel_span.get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return event + + sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) + if not sentry_span: + return event + + contexts = event.setdefault("contexts", {}) + contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) + + return event + + +class SentrySpanProcessor(SpanProcessor): + """ + Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. + """ + + # The mapping from otel span ids to sentry spans + otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} + + # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES + open_spans: "dict[int, set[str]]" = {} + + initialized: "bool" = False + + def __new__(cls) -> "SentrySpanProcessor": + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + + # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). + return cls.instance # type: ignore[misc] + + def __init__(self) -> None: + if self.initialized: + return + + @add_global_event_processor + def global_event_processor(event: "Event", hint: "Hint") -> "Event": + return link_trace_context_to_error_event(event, self.otel_span_map) + + self.initialized = True + + def _prune_old_spans(self: "SentrySpanProcessor") -> None: + """ + Prune spans that have been open for too long. + """ + current_time_minutes = int(time() / 60) + for span_start_minutes in list( + self.open_spans.keys() + ): # making a list because we change the dict + # prune empty open spans buckets + if self.open_spans[span_start_minutes] == set(): + self.open_spans.pop(span_start_minutes) + + # prune old buckets + elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: + for span_id in self.open_spans.pop(span_start_minutes): + self.otel_span_map.pop(span_id, None) + + def on_start( + self, + otel_span: "OTelSpan", + parent_context: "Optional[context_api.Context]" = None, + ) -> None: + client = get_client() + + if not client.parsed_dsn: + return + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + if self._is_sentry_span(otel_span): + return + + trace_data = self._get_trace_data( + span_context, otel_span.parent, parent_context + ) + + parent_span_id = trace_data["parent_span_id"] + sentry_parent_span = ( + self.otel_span_map.get(parent_span_id) if parent_span_id else None + ) + + start_timestamp = None + if otel_span.start_time is not None: + start_timestamp = datetime.fromtimestamp( + otel_span.start_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span = None + if sentry_parent_span: + sentry_span = sentry_parent_span.start_child( + span_id=trace_data["span_id"], + name=otel_span.name, + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + else: + sentry_span = start_transaction( + name=otel_span.name, + span_id=trace_data["span_id"], + parent_span_id=parent_span_id, + trace_id=trace_data["trace_id"], + baggage=trace_data["baggage"], + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + + self.otel_span_map[trace_data["span_id"]] = sentry_span + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).add( + trace_data["span_id"] + ) + + self._prune_old_spans() + + def on_end(self, otel_span: "OTelSpan") -> None: + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + span_id = format_span_id(span_context.span_id) + sentry_span = self.otel_span_map.pop(span_id, None) + if not sentry_span: + return + + sentry_span.op = otel_span.name + + self._update_span_with_otel_status(sentry_span, otel_span) + + if isinstance(sentry_span, Transaction): + sentry_span.name = otel_span.name + sentry_span.set_context( + OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) + ) + self._update_transaction_with_otel_data(sentry_span, otel_span) + + else: + self._update_span_with_otel_data(sentry_span, otel_span) + + end_timestamp = None + if otel_span.end_time is not None: + end_timestamp = datetime.fromtimestamp( + otel_span.end_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span.finish(end_timestamp=end_timestamp) + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) + + self._prune_old_spans() + + def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: + """ + Break infinite loop: + HTTP requests to Sentry are caught by OTel and send again to Sentry. + """ + otel_span_url = None + if otel_span.attributes is not None: + otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) + otel_span_url = cast("Optional[str]", otel_span_url) + + parsed_dsn = get_client().parsed_dsn + dsn_url = parsed_dsn.netloc if parsed_dsn else None + + if otel_span_url and dsn_url and dsn_url in otel_span_url: + return True + + return False + + def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": + """ + Returns the OTel context for Sentry. + See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context + """ + ctx = {} + + if otel_span.attributes: + ctx["attributes"] = dict(otel_span.attributes) + + if otel_span.resource.attributes: + ctx["resource"] = dict(otel_span.resource.attributes) + + return ctx + + def _get_trace_data( + self, + span_context: "SpanContext", + parent_span_context: "Optional[SpanContext]", + parent_context: "Optional[context_api.Context]", + ) -> "dict[str, Any]": + """ + Extracts tracing information from one OTel span's context and its parent OTel context. + """ + trace_data: "dict[str, Any]" = {} + + span_id = format_span_id(span_context.span_id) + trace_data["span_id"] = span_id + + trace_id = format_trace_id(span_context.trace_id) + trace_data["trace_id"] = trace_id + + parent_span_id = ( + format_span_id(parent_span_context.span_id) if parent_span_context else None + ) + trace_data["parent_span_id"] = parent_span_id + + sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) + sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) + trace_data["parent_sampled"] = ( + sentry_trace_data["parent_sampled"] if sentry_trace_data else None + ) + + baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) + trace_data["baggage"] = baggage + + return trace_data + + def _update_span_with_otel_status( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Set the Sentry span status from the OTel span + """ + if otel_span.status.is_unset: + return + + if otel_span.status.is_ok: + sentry_span.set_status(SPANSTATUS.OK) + return + + sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + def _update_span_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Convert OTel span data and update the Sentry span with it. + This should eventually happen on the server when ingesting the spans. + """ + sentry_span.set_data("otel.kind", otel_span.kind) + + op = otel_span.name + description = otel_span.name + + if otel_span.attributes is not None: + for key, val in otel_span.attributes.items(): + sentry_span.set_data(key, val) + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + http_method = cast("Optional[str]", http_method) + + db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) + + if http_method: + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + description = http_method + + peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) + if peer_name: + description += " {}".format(peer_name) + + target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) + if target: + description += " {}".format(target) + + if not peer_name and not target: + url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) + url = cast("Optional[str]", url) + if url: + parsed_url = urlparse(url) + url = "{}://{}{}".format( + parsed_url.scheme, parsed_url.netloc, parsed_url.path + ) + description += " {}".format(url) + + status_code = otel_span.attributes.get( + SpanAttributes.HTTP_STATUS_CODE, None + ) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + elif db_query: + op = "db" + statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) + statement = cast("Optional[str]", statement) + if statement: + description = statement + + sentry_span.op = op + sentry_span.description = description + + def _update_transaction_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + if otel_span.attributes is None: + return + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + + if http_method: + status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py new file mode 100644 index 0000000000..b91f2cfd21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py @@ -0,0 +1,223 @@ +from sentry_sdk import capture_event, get_client +from sentry_sdk.consts import VERSION, EndpointType +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import register_external_propagation_context +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + event_from_exception, + logger, +) + +try: + from opentelemetry.context import ( + Context, + get_current, + get_value, + ) + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.propagate import set_global_textmap + from opentelemetry.propagators.textmap import ( + CarrierT, + Setter, + default_setter, + ) + from opentelemetry.sdk.trace import Span, TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + SpanContext, + format_span_id, + format_trace_id, + get_current_span, + get_tracer_provider, + set_tracer_provider, + ) + + from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY + from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +except ImportError: + raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Tuple + + +def otel_propagation_context() -> "Optional[Tuple[str, str]]": + """ + Get the (trace_id, span_id) from opentelemetry if exists. + """ + ctx = get_current_span().get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return None + + return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) + + +def setup_otlp_traces_exporter( + dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None +) -> None: + tracer_provider = get_tracer_provider() + + if not isinstance(tracer_provider, TracerProvider): + logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") + tracer_provider = TracerProvider() + set_tracer_provider(tracer_provider) + + endpoint = None + headers = None + if collector_url: + endpoint = collector_url + logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") + elif dsn: + auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") + endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) + headers = {"X-Sentry-Auth": auth.to_header()} + logger.debug(f"[OTLP] Sending traces to {endpoint}") + + otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + + +_sentry_patched_exception = False + + +def setup_capture_exceptions() -> None: + """ + Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. + """ + global _sentry_patched_exception + _original_record_exception = Span.record_exception + + if _sentry_patched_exception: + return + + def _sentry_patched_record_exception( + self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration and otlp_integration.capture_exceptions: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=get_client().options, + mechanism={"type": OTLPIntegration.identifier, "handled": False}, + ) + capture_event(event, hint=hint) + + _original_record_exception(self, exception, *args, **kwargs) + + Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] + _sentry_patched_exception = True + + +class SentryOTLPPropagator(SentryPropagator): + """ + We need to override the inject of the older propagator since that + is SpanProcessor based. + + !!! Note regarding baggage: + We cannot meaningfully populate a new baggage as a head SDK + when we are using OTLP since we don't have any sort of transaction semantic to + track state across a group of spans. + + For incoming baggage, we just pass it on as is so that case is correctly handled. + """ + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration is None: + return + + if context is None: + context = get_current() + + current_span = get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + sentry_trace = _to_traceparent(current_span_context) + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) + + baggage = get_value(SENTRY_BAGGAGE_KEY, context) + if baggage is not None and isinstance(baggage, Baggage): + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + +def _to_traceparent(span_context: "SpanContext") -> str: + """ + Helper method to generate the sentry-trace header. + """ + span_id = format_span_id(span_context.span_id) + trace_id = format_trace_id(span_context.trace_id) + sampled = span_context.trace_flags.sampled + + return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" + + +class OTLPIntegration(Integration): + """ + Automatically setup OTLP ingestion from the DSN. + + :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. + Set to False to setup the TracerProvider manually. + :param collector_url: URL of your own OpenTelemetry collector, defaults to None. + When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. + :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. + Set to False to configure propagators manually or to disable propagation. + :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. + Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. + """ + + identifier = "otlp" + + def __init__( + self, + setup_otlp_traces_exporter: bool = True, + collector_url: "Optional[str]" = None, + setup_propagator: bool = True, + capture_exceptions: bool = False, + ) -> None: + self.setup_otlp_traces_exporter = setup_otlp_traces_exporter + self.collector_url = collector_url + self.setup_propagator = setup_propagator + self.capture_exceptions = capture_exceptions + + @staticmethod + def setup_once() -> None: + logger.debug("[OTLP] Setting up trace linking for all events") + register_external_propagation_context(otel_propagation_context) + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if self.setup_otlp_traces_exporter: + logger.debug("[OTLP] Setting up OTLP exporter") + dsn: "Optional[str]" = options.get("dsn") if options else None + setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) + + if self.setup_propagator: + logger.debug("[OTLP] Setting up propagator for distributed tracing") + # TODO-neel better propagator support, chain with existing ones if possible instead of replacing + set_global_textmap(SentryOTLPPropagator()) + + setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py new file mode 100644 index 0000000000..569e3e1fa5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py @@ -0,0 +1,134 @@ +import ast +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import serializer +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, List, Optional, Tuple + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + +try: + from pure_eval import Evaluator +except ImportError: + raise DidNotEnable("pure_eval is not installed") + +try: + # Used implicitly, just testing it's available + import asttokens # noqa +except ImportError: + raise DidNotEnable("asttokens is not installed") + + +class PureEvalIntegration(Integration): + identifier = "pure_eval" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + sentry_frame["vars"] = ( + pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] + ) + return event + + +def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": + source = Source.for_frame(frame) + if not source.tree: + return {} + + statements = source.statements_at_line(frame.f_lineno) + if not statements: + return {} + + scope = stmt = list(statements)[0] + while True: + # Get the parent first in case the original statement is already + # a function definition, e.g. if we're calling a decorator + # In that case we still want the surrounding scope, not that function + scope = scope.parent + if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): + break + + evaluator = Evaluator.from_frame(frame) + expressions = evaluator.interesting_expressions_grouped(scope) + + def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": + # Prioritise expressions with a node closer to the statement executed + # without being after that statement + # A higher return value is better - the expression will appear + # earlier in the list of values and is less likely to be trimmed + nodes, _value = expression + + def start(n: "ast.expr") -> "Tuple[int, int]": + return (n.lineno, n.col_offset) + + nodes_before_stmt = [ + node for node in nodes if start(node) < stmt.last_token.end + ] + if nodes_before_stmt: + # The position of the last node before or in the statement + return max(start(node) for node in nodes_before_stmt) + else: + # The position of the first node after the statement + # Negative means it's always lower priority than nodes that come before + # Less negative means closer to the statement and higher priority + lineno, col_offset = min(start(node) for node in nodes) + return (-lineno, -col_offset) + + # This adds the first_token and last_token attributes to nodes + atok = source.asttokens() + + expressions.sort(key=closeness, reverse=True) + vars = { + atok.get_text(nodes[0]): value + for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] + } + return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py new file mode 100644 index 0000000000..81e7cf8090 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -0,0 +1,187 @@ +import functools + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import capture_internal_exceptions, parse_version + +try: + import pydantic_ai # type: ignore # noqa: F401 + from pydantic_ai import Agent +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +from importlib.metadata import PackageNotFoundError, version +from typing import TYPE_CHECKING + +from .patches import ( + _patch_agent_run, + _patch_graph_nodes, + _patch_tool_execution, +) +from .spans.ai_client import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any + + from pydantic_ai import ModelRequestContext, RunContext + from pydantic_ai.capabilities import Hooks # type: ignore + from pydantic_ai.messages import ModelResponse # type: ignore + + +def register_hooks(hooks: "Hooks") -> None: + """ + Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. + """ + + @hooks.on.before_model_request # type: ignore + async def on_request( + ctx: "RunContext[None]", request_context: "ModelRequestContext" + ) -> "ModelRequestContext": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return request_context + + span = ai_client_span( + messages=request_context.messages, + agent=None, + model=request_context.model, + model_settings=request_context.model_settings, + ) + + run_context_metadata["_sentry_span"] = span + span.__enter__() + + return request_context + + @hooks.on.after_model_request # type: ignore + async def on_response( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + response: "ModelResponse", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return response + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + return response + + update_ai_client_span(span, response) + span.__exit__(None, None, None) + + return response + + @hooks.on.model_request_error # type: ignore + async def on_error( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + error: "Exception", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + + if not isinstance(run_context_metadata, dict): + raise error + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + raise error + + with capture_internal_exceptions(): + span.__exit__(type(error), error, error.__traceback__) + + raise error + + original_init = Agent.__init__ + + @functools.wraps(original_init) + def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: + caps = list(kwargs.get("capabilities") or []) + caps.append(hooks) + kwargs["capabilities"] = caps + + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {} # Used as shared reference between hooks + + return original_init(self, *args, **kwargs) + + Agent.__init__ = patched_init + + +class PydanticAIIntegration(Integration): + """ + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. + 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. + - Each run invocation has `RunContext` objects that are passed to the library hooks. + 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + + Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. + Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). + The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. + + The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that + instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. + """ + + identifier = "pydantic_ai" + origin = f"auto.ai.{identifier}" + using_request_hooks = False + + def __init__( + self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True + ) -> None: + """ + Initialize the Pydantic AI integration. + + Args: + include_prompts: Whether to include prompts and messages in span data. + Requires send_default_pii=True. Defaults to True. + handled_tool_exceptions: Capture tool call exceptions that Pydantic AI + internally prevents from bubbling up. + """ + self.include_prompts = include_prompts + self.handled_tool_call_exceptions = handled_tool_call_exceptions + + @staticmethod + def setup_once() -> None: + """ + Set up the pydantic-ai integration. + + This patches the key methods in pydantic-ai to create Sentry spans for: + - Agent invocations (Agent.run methods) + - Model requests (AI client calls) + - Tool executions + """ + _patch_agent_run() + _patch_tool_execution() + + PydanticAIIntegration.using_request_hooks = False + try: + PYDANTIC_AI_VERSION = version("pydantic-ai-slim") + except PackageNotFoundError: + return + + PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) + if PYDANTIC_AI_VERSION is None: + return + + # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c + if PYDANTIC_AI_VERSION < ( + 1, + 73, + ): + _patch_graph_nodes() + return + + try: + from pydantic_ai.capabilities import Hooks + except ImportError: + return + + PydanticAIIntegration.using_request_hooks = True + hooks = Hooks() + register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py new file mode 100644 index 0000000000..afa66dc47d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py new file mode 100644 index 0000000000..d0ea6242b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py @@ -0,0 +1,3 @@ +from .agent_run import _patch_agent_run # noqa: F401 +from .graph_nodes import _patch_graph_nodes # noqa: F401 +from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py new file mode 100644 index 0000000000..3039e40698 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -0,0 +1,198 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import invoke_agent_span, update_invoke_agent_span +from ..utils import _capture_exception, pop_agent, push_agent + +try: + from pydantic_ai.agent import Agent # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + +class _StreamingContextManagerWrapper: + """Wrapper for streaming methods that return async context managers.""" + + def __init__( + self, + agent: "Any", + original_ctx_manager: "Any", + user_prompt: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = True, + ) -> None: + self.agent = agent + self.original_ctx_manager = original_ctx_manager + self.user_prompt = user_prompt + self.model = model + self.model_settings = model_settings + self.is_streaming = is_streaming + self._isolation_scope: "Any" = None + self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None + self._result: "Any" = None + + async def __aenter__(self) -> "Any": + # Set up isolation scope and invoke_agent span + self._isolation_scope = sentry_sdk.isolation_scope() + self._isolation_scope.__enter__() + + # Create invoke_agent span (will be closed in __aexit__) + self._span = invoke_agent_span( + self.user_prompt, + self.agent, + self.model, + self.model_settings, + self.is_streaming, + ) + self._span.__enter__() + + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur + push_agent(self.agent, self.is_streaming) + + # Enter the original context manager + result = await self.original_ctx_manager.__aenter__() + self._result = result + return result + + async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: + try: + # Exit the original context manager first + await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) + + # Update span with result if successful + if exc_type is None and self._result and self._span is not None: + update_invoke_agent_span(self._span, self._result) + finally: + # Pop agent from contextvar stack + pop_agent() + + # Clean up invoke span + if self._span: + self._span.__exit__(exc_type, exc_val, exc_tb) + + # Clean up isolation scope + if self._isolation_scope: + self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) + + +def _create_run_wrapper( + original_func: "Callable[..., Any]", is_streaming: bool = False +) -> "Callable[..., Any]": + """ + Wraps the Agent.run method to create an invoke_agent span. + + Args: + original_func: The original run method + is_streaming: Whether this is a streaming method (for future use) + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Create invoke_agent span + with invoke_agent_span( + user_prompt, self, model, model_settings, is_streaming + ) as span: + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in finally even if exceptions occur + push_agent(self, is_streaming) + + try: + result = await original_func(self, *args, **kwargs) + + # Update span with result + update_invoke_agent_span(span, result) + + return result + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + finally: + # Pop agent from contextvar stack + pop_agent() + + return wrapper + + +def _create_streaming_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps run_stream method that returns an async context manager. + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Call original function to get the context manager + original_ctx_manager = original_func(self, *args, **kwargs) + + # Wrap it with our instrumentation + return _StreamingContextManagerWrapper( + agent=self, + original_ctx_manager=original_ctx_manager, + user_prompt=user_prompt, + model=model, + model_settings=model_settings, + is_streaming=True, + ) + + return wrapper + + +def _patch_agent_run() -> None: + """ + Patches the Agent run methods to create spans for agent execution. + + This patches both non-streaming (run, run_sync) and streaming + (run_stream, run_stream_events) methods. + """ + + # Store original methods + original_run = Agent.run + original_run_stream = Agent.run_stream + + # Wrap and apply patches for non-streaming methods + Agent.run = _create_run_wrapper(original_run, is_streaming=False) + + # Wrap and apply patches for streaming methods + Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py new file mode 100644 index 0000000000..afb10395f4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py @@ -0,0 +1,106 @@ +from contextlib import asynccontextmanager +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import ( + ai_client_span, + update_ai_client_span, +) + +try: + from pydantic_ai._agent_graph import ModelRequestNode # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + +def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": + """Extract common data needed for creating chat spans. + + Returns: + Tuple of (messages, model, model_settings) + """ + # Extract model and settings from context + model = None + model_settings = None + if hasattr(ctx, "deps"): + model = getattr(ctx.deps, "model", None) + model_settings = getattr(ctx.deps, "model_settings", None) + + # Build full message list: history + current request + messages = [] + if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): + messages.extend(ctx.state.message_history) + + current_request = getattr(node, "request", None) + if current_request: + messages.append(current_request) + + return messages, model, model_settings + + +def _patch_graph_nodes() -> None: + """ + Patches the graph node execution to create appropriate spans. + + ModelRequestNode -> Creates ai_client span for model requests + CallToolsNode -> Handles tool calls (spans created in tool patching) + """ + + # Patch ModelRequestNode to create ai_client spans + original_model_request_run = ModelRequestNode.run + + @wraps(original_model_request_run) + async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + with ai_client_span(messages, None, model, model_settings) as span: + result = await original_model_request_run(self, ctx) + + # Extract response from result if available + model_response = None + if hasattr(result, "model_response"): + model_response = result.model_response + + update_ai_client_span(span, model_response) + return result + + ModelRequestNode.run = wrapped_model_request_run + + # Patch ModelRequestNode.stream for streaming requests + original_model_request_stream = ModelRequestNode.stream + + def create_wrapped_stream( + original_stream_method: "Callable[..., Any]", + ) -> "Callable[..., Any]": + """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" + + @asynccontextmanager + @wraps(original_stream_method) + async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + # Create chat span for streaming request + with ai_client_span(messages, None, model, model_settings) as span: + # Call the original stream method + async with original_stream_method(self, ctx) as stream: + yield stream + + # After streaming completes, update span with response data + # The ModelRequestNode stores the final response in _result + model_response = None + if hasattr(self, "_result") and self._result is not None: + # _result is a NextNode containing the model_response + if hasattr(self._result, "model_response"): + model_response = self._result.model_response + + update_ai_client_span(span, model_response) + + return wrapped_model_request_stream + + ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py new file mode 100644 index 0000000000..5646b5d47c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -0,0 +1,177 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import execute_tool_span, update_execute_tool_span +from ..utils import _capture_exception, get_current_agent + +if TYPE_CHECKING: + from typing import Any + +try: + try: + from pydantic_ai.tool_manager import ToolManager # type: ignore + except ImportError: + from pydantic_ai._tool_manager import ToolManager # type: ignore + + from pydantic_ai.exceptions import ToolRetryError # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +def _patch_tool_execution() -> None: + if hasattr(ToolManager, "execute_tool_call"): + _patch_execute_tool_call() + + elif hasattr(ToolManager, "_call_tool"): + # older versions + _patch_call_tool() + + +def _patch_execute_tool_call() -> None: + original_execute_tool_call = ToolManager.execute_tool_call + + @wraps(original_execute_tool_call) + async def wrapped_execute_tool_call( + self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not validated or not hasattr(validated, "call"): + return await original_execute_tool_call(self, validated, *args, **kwargs) + + # Extract tool info before calling original + call = validated.call + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_execute_tool_call( + self, + validated, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + return await original_execute_tool_call(self, validated, *args, **kwargs) + + ToolManager.execute_tool_call = wrapped_execute_tool_call + + +def _patch_call_tool() -> None: + """ + Patch ToolManager._call_tool to create execute_tool spans. + + This is the single point where ALL tool calls flow through in pydantic_ai, + regardless of toolset type (function, MCP, combined, wrapper, etc.). + + By patching here, we avoid: + - Patching multiple toolset classes + - Dealing with signature mismatches from instrumented MCP servers + - Complex nested toolset handling + """ + original_call_tool = ToolManager._call_tool + + @wraps(original_call_tool) + async def wrapped_call_tool( + self: "Any", call: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Extract tool info before calling original + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_call_tool( + self, + call, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + # No span context - just call original + return await original_call_tool( + self, + call, + *args, + **kwargs, + ) + + ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py new file mode 100644 index 0000000000..574046d645 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py @@ -0,0 +1,3 @@ +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py new file mode 100644 index 0000000000..27deb0c55c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -0,0 +1,331 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _get_model_name, + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, + get_current_agent, + get_is_streaming, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Dict, List, Union + + from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore + + from sentry_sdk._types import TextPart as SentryTextPart + +try: + from pydantic_ai.messages import ( + BaseToolCallPart, + BaseToolReturnPart, + BinaryContent, + ImageUrl, + SystemPromptPart, + TextPart, + ThinkingPart, + UserPromptPart, + ) +except ImportError: + # Fallback if these classes are not available + BaseToolCallPart = None + BaseToolReturnPart = None + SystemPromptPart = None + UserPromptPart = None + TextPart = None + ThinkingPart = None + BinaryContent = None + ImageUrl = None + + +def _transform_system_instructions( + permanent_instructions: "list[SystemPromptPart]", + current_instructions: "list[str]", +) -> "list[SentryTextPart]": + text_parts: "list[SentryTextPart]" = [ + { + "type": "text", + "content": instruction.content, + } + for instruction in permanent_instructions + ] + + text_parts.extend( + { + "type": "text", + "content": instruction, + } + for instruction in current_instructions + ) + + return text_parts + + +def _get_system_instructions( + messages: "list[ModelMessage]", +) -> "tuple[list[SystemPromptPart], list[str]]": + permanent_instructions = [] + current_instructions = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + if SystemPromptPart and isinstance(part, SystemPromptPart): + permanent_instructions.append(part) + + if hasattr(msg, "instructions") and msg.instructions is not None: + current_instructions.append(msg.instructions) + + return permanent_instructions, current_instructions + + +def _set_input_messages( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" +) -> None: + """Set input messages data on a span.""" + if not _should_send_prompts(): + return + + if not messages: + return + + permanent_instructions, current_instructions = _get_system_instructions(messages) + if len(permanent_instructions) > 0 or len(current_instructions) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + + try: + formatted_messages = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + role = "user" + # Use isinstance checks with proper base classes + if SystemPromptPart and isinstance(part, SystemPromptPart): + continue + elif ( + (TextPart and isinstance(part, TextPart)) + or (ThinkingPart and isinstance(part, ThinkingPart)) + or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) + ): + role = "assistant" + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + role = "tool" + + content: "List[Dict[str, Any] | str]" = [] + tool_calls = None + tool_call_id = None + + # Handle ToolCallPart (assistant requesting tool use) + if BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = {} + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + if tool_call_data: + tool_calls = [tool_call_data] + # Handle ToolReturnPart (tool result) + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + if hasattr(part, "tool_name"): + tool_call_id = part.tool_name + if hasattr(part, "content"): + content.append({"type": "text", "text": str(part.content)}) + # Handle regular content + elif hasattr(part, "content"): + if isinstance(part.content, str): + content.append({"type": "text", "text": part.content}) + elif isinstance(part.content, list): + for item in part.content: + if isinstance(item, str): + content.append({"type": "text", "text": item}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + else: + content.append(safe_serialize(item)) + else: + content.append({"type": "text", "text": str(part.content)}) + # Add message if we have content or tool calls + if content or tool_calls: + message: "Dict[str, Any]" = {"role": role} + if content: + message["content"] = content + if tool_calls: + message["tool_calls"] = tool_calls + if tool_call_id: + message["tool_call_id"] = tool_call_id + formatted_messages.append(message) + + if formatted_messages: + normalized_messages = normalize_message_roles(formatted_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + except Exception: + # If we fail to format messages, just skip it + pass + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" +) -> None: + """Set output data on a span.""" + if not _should_send_prompts(): + return + + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + + try: + # Extract text from ModelResponse + if hasattr(response, "parts"): + texts = [] + tool_calls = [] + + for part in response.parts: + if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): + texts.append(part.content) + elif BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = { + "type": "function", + } + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + tool_calls.append(tool_call_data) + + if texts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) + + if tool_calls: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) + ) + + except Exception: + # If we fail to format output, just skip it + pass + + +def ai_client_span( + messages: "Any", agent: "Any", model: "Any", model_settings: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for an AI client call (model request). + + Args: + messages: Full conversation history (list of messages) + agent: Agent object + model: Model object + model_settings: Model settings + """ + # Determine model name for span name + model_obj = model + if agent and hasattr(agent, "model"): + model_obj = agent.model + + model_name = _get_model_name(model_obj) or "unknown" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + # Set streaming flag from contextvar + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + + # Add available tools if agent is available + agent_obj = agent or get_current_agent() + _set_available_tools(span, agent_obj) + + # Set input messages (full conversation history) + if messages: + _set_input_messages(span, messages) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" +) -> None: + """Update the AI client span with response data.""" + if not span: + return + + # Set usage data if available + if model_response and hasattr(model_response, "usage"): + _set_usage_data(span, model_response.usage) + + # Set output data + _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py new file mode 100644 index 0000000000..7648c1418a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _should_send_prompts + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from pydantic_ai._tool_manager import ToolDefinition # type: ignore + + +def execute_tool_span( + tool_name: str, + tool_args: "Any", + agent: "Any", + tool_definition: "Optional[ToolDefinition]" = None, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for tool execution. + + Args: + tool_name: The name of the tool being executed + tool_args: The arguments passed to the tool + agent: The agent executing the tool + tool_definition: The definition of the tool, if available + """ + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + set_on_span = span.set_data + + if tool_definition is not None and hasattr(tool_definition, "description"): + set_on_span( + SPANDATA.GEN_AI_TOOL_DESCRIPTION, + tool_definition.description, + ) + + _set_agent_data(span, agent) + + if _should_send_prompts() and tool_args is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + """Update the execute tool span with the result.""" + if not span: + return + + if not _should_send_prompts() or result is None: + return + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + else: + span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py new file mode 100644 index 0000000000..f0c68e85ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -0,0 +1,184 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, +) + +if TYPE_CHECKING: + from typing import Any, Union + +try: + from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore +except ImportError: + BinaryContent = None + ImageUrl = None + + +def invoke_agent_span( + user_prompt: "Any", + agent: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = False, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for invoking the agent.""" + # Determine agent name for span + name = "agent" + if agent and getattr(agent, "name", None): + name = agent.name + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + _set_available_tools(span, agent) + + # Add user prompt and system prompts if available and prompts are enabled + if _should_send_prompts(): + messages = [] + + # Add system prompts (both instructions and system_prompt) + system_texts = [] + + if agent: + # Check for system_prompt + system_prompts = getattr(agent, "_system_prompts", None) or [] + for prompt in system_prompts: + if isinstance(prompt, str): + system_texts.append(prompt) + + # Check for instructions (stored in _instructions) + instructions = getattr(agent, "_instructions", None) + if instructions: + if isinstance(instructions, str): + system_texts.append(instructions) + elif isinstance(instructions, (list, tuple)): + for instr in instructions: + if isinstance(instr, str): + system_texts.append(instr) + elif callable(instr): + # Skip dynamic/callable instructions + pass + + # Add all system texts as system messages + for system_text in system_texts: + messages.append( + { + "content": [{"text": system_text, "type": "text"}], + "role": "system", + } + ) + + # Add user prompt + if user_prompt: + if isinstance(user_prompt, str): + messages.append( + { + "content": [{"text": user_prompt, "type": "text"}], + "role": "user", + } + ) + elif isinstance(user_prompt, list): + # Handle list of user content + content = [] + for item in user_prompt: + if isinstance(item, str): + content.append({"text": item, "type": "text"}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + if content: + messages.append( + { + "content": content, + "role": "user", + } + ) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + result: "Any", +) -> None: + """Update and close the invoke agent span.""" + if not span or not result: + return + + # Extract output from result + output = getattr(result, "output", None) + + # Set response text if prompts are enabled + if _should_send_prompts() and output: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False + ) + + # Set model name from response if available + if hasattr(result, "response"): + try: + response = result.response + if hasattr(response, "model_name") and response.model_name: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + except Exception: + # If response access fails, continue without setting model name + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py new file mode 100644 index 0000000000..330496c6b2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py @@ -0,0 +1,87 @@ +"""Utility functions for PydanticAI span instrumentation.""" + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX +from sentry_sdk.ai.utils import get_modality_from_mime_type +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore + + +def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": + """Serialize an ImageUrl content item for span data. + + For data URLs containing base64-encoded images, the content is redacted. + For regular HTTP URLs, the URL string is preserved. + """ + url = str(item.url) + data_url_match = DATA_URL_BASE64_REGEX.match(url) + + if data_url_match: + return { + "type": "image", + "content": BLOB_DATA_SUBSTITUTE, + } + + return { + "type": "image", + "content": url, + } + + +def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": + """Serialize a BinaryContent item for span data, redacting the blob data.""" + return { + "type": "blob", + "modality": get_modality_from_mime_type(item.media_type), + "mime_type": item.media_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + usage: "Union[RequestUsage, RunUsage]", +) -> None: + """Set token usage data on a span. + + This function works with both RequestUsage (single request) and + RunUsage (agent run) objects from pydantic_ai. + + Args: + span: The Sentry span to set data on. + usage: RequestUsage or RunUsage object containing token usage information. + """ + if usage is None: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if hasattr(usage, "input_tokens") and usage.input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + + # Pydantic AI uses cache_read_tokens (not input_tokens_cached) + if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) + + # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) + if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + usage.cache_write_tokens, + ) + + if hasattr(usage, "output_tokens") and usage.output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + + if hasattr(usage, "total_tokens") and usage.total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py new file mode 100644 index 0000000000..340dcf8953 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py @@ -0,0 +1,233 @@ +from contextvars import ContextVar +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + +# Store the current agent context in a contextvar for re-entrant safety +# Using a list as a stack to support nested agent calls +_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( + "pydantic_ai_agent_context_stack", default=[] +) + + +def push_agent(agent: "Any", is_streaming: bool = False) -> None: + """Push an agent context onto the stack along with its streaming flag.""" + stack = _agent_context_stack.get().copy() + stack.append({"agent": agent, "is_streaming": is_streaming}) + _agent_context_stack.set(stack) + + +def pop_agent() -> None: + """Pop an agent context from the stack.""" + stack = _agent_context_stack.get().copy() + if stack: + stack.pop() + _agent_context_stack.set(stack) + + +def get_current_agent() -> "Any": + """Get the current agent from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1]["agent"] + return None + + +def get_is_streaming() -> bool: + """Get the streaming flag from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1].get("is_streaming", False) + return False + + +def _should_send_prompts() -> bool: + """ + Check if prompts should be sent to Sentry. + + This checks both send_default_pii and the include_prompts integration setting. + """ + if not should_send_default_pii(): + return False + + from . import PydanticAIIntegration + + # Get the integration instance from the client + integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) + + if integration is None: + return False + + return getattr(integration, "include_prompts", False) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set agent-related data on a span. + + Args: + span: The span to set data on + agent: Agent object (can be None, will try to get from contextvar if not provided) + """ + # Extract agent name from agent object or contextvar + agent_obj = agent + if not agent_obj: + # Try to get from contextvar + agent_obj = get_current_agent() + + if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + else: + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + + +def _get_model_name(model_obj: "Any") -> "Optional[str]": + """Extract model name from a model object. + + Args: + model_obj: Model object to extract name from + + Returns: + Model name string or None if not found + """ + if not model_obj: + return None + + if hasattr(model_obj, "model_name"): + return model_obj.model_name + elif hasattr(model_obj, "name"): + try: + return model_obj.name() + except Exception: + return str(model_obj) + elif isinstance(model_obj, str): + return model_obj + else: + return str(model_obj) + + +def _set_model_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + model: "Any", + model_settings: "Any", +) -> None: + """Set model-related data on a span. + + Args: + span: The span to set data on + model: Model object (can be None, will try to get from agent if not provided) + model_settings: Model settings (can be None, will try to get from agent if not provided) + """ + # Try to get agent from contextvar if we need it + agent_obj = get_current_agent() + + # Extract model information + model_obj = model + if not model_obj and agent_obj and hasattr(agent_obj, "model"): + model_obj = agent_obj.model + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if model_obj: + # Set system from model + if hasattr(model_obj, "system"): + set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) + + # Set model name + model_name = _get_model_name(model_obj) + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Extract model settings + settings = model_settings + if not settings and agent_obj and hasattr(agent_obj, "model_settings"): + settings = agent_obj.model_settings + + if settings: + settings_map = { + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + } + + # ModelSettings is a TypedDict (dict at runtime), so use dict access + if isinstance(settings, dict): + for setting_name, spandata_key in settings_map.items(): + value = settings.get(setting_name) + if value is not None: + set_on_span(spandata_key, value) + else: + # Fallback for object-style settings + for setting_name, spandata_key in settings_map.items(): + if hasattr(settings, setting_name): + value = getattr(settings, setting_name) + if value is not None: + set_on_span(spandata_key, value) + + +def _set_available_tools( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set available tools data on a span from an agent's function toolset. + + Args: + span: The span to set data on + agent: Agent object with _function_toolset attribute + """ + if not agent or not hasattr(agent, "_function_toolset"): + return + + try: + tools = [] + # Get tools from the function toolset + if hasattr(agent._function_toolset, "tools"): + for tool_name, tool in agent._function_toolset.tools.items(): + tool_info = {"name": tool_name} + + # Add description from function_schema if available + if hasattr(tool, "function_schema"): + schema = tool.function_schema + if getattr(schema, "description", None): + tool_info["description"] = schema.description + + # Add parameters from json_schema + if getattr(schema, "json_schema", None): + tool_info["parameters"] = schema.json_schema + + tools.append(tool_info) + + if tools: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + else: + span.set_data( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + except Exception: + # If we can't extract tools, just skip it + pass + + +def _capture_exception(exc: "Any", handled: bool = False) -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pydantic_ai", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py new file mode 100644 index 0000000000..2616f4d5a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py @@ -0,0 +1,262 @@ +import copy +import json + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +try: + from pymongo import monitoring +except ImportError: + raise DidNotEnable("Pymongo not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pymongo.monitoring import ( + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + ) + + +SAFE_COMMAND_ATTRIBUTES = [ + "insert", + "ordered", + "find", + "limit", + "singleBatch", + "aggregate", + "createIndexes", + "indexes", + "delete", + "findAndModify", + "renameCollection", + "to", + "drop", +] + + +def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": + for key in command: + is_safe_field = key in SAFE_COMMAND_ATTRIBUTES + if is_safe_field: + # Skip if safe key + continue + + update_db_command = key == "update" and "findAndModify" not in command + if update_db_command: + # Also skip "update" db command because it is save. + # There is also an "update" key in the "findAndModify" command, which is NOT safe! + continue + + # Special stripping for documents + is_document = key == "documents" + if is_document: + for doc in command[key]: + for doc_key in doc: + doc[doc_key] = "%s" + continue + + # Special stripping for dict style fields + is_dict_field = key in ["filter", "query", "update"] + if is_dict_field: + for item_key in command[key]: + command[key][item_key] = "%s" + continue + + # For pipeline fields strip the `$match` dict + is_pipeline_field = key == "pipeline" + if is_pipeline_field: + for pipeline in command[key]: + for match_key in pipeline["$match"] if "$match" in pipeline else []: + pipeline["$match"][match_key] = "%s" + continue + + # Default stripping + command[key] = "%s" + + return command + + +def _get_db_data(event: "Any") -> "Dict[str, Any]": + data = {} + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + data[SPANDATA.DB_DRIVER_NAME] = "pymongo" + db_name = event.database_name + + server_address = event.connection_id[0] + if server_address is not None: + data[SPANDATA.SERVER_ADDRESS] = server_address + + server_port = event.connection_id[1] + if server_port is not None: + data[SPANDATA.SERVER_PORT] = server_port + + if is_span_streaming_enabled: + data["db.system.name"] = "mongodb" + + if db_name is not None: + data["db.namespace"] = db_name + else: + data[SPANDATA.DB_SYSTEM] = "mongodb" + + if db_name is not None: + data[SPANDATA.DB_NAME] = db_name + + return data + + +class CommandTracer(monitoring.CommandListener): + def __init__(self) -> None: + self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} + + def _operation_key( + self, + event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", + ) -> int: + return event.request_id + + def started(self, event: "CommandStartedEvent") -> None: + client = sentry_sdk.get_client() + if client.get_integration(PyMongoIntegration) is None: + return + + with capture_internal_exceptions(): + command = dict(copy.deepcopy(event.command)) + + command.pop("$db", None) + command.pop("$clusterTime", None) + command.pop("$signature", None) + + db_data = _get_db_data(event) + + collection_name = command.get(event.command_name) + operation_name = event.command_name + db_name = event.database_name + + lsid = command.pop("lsid", None) + if not should_send_default_pii(): + command = _strip_pii(command) + + query = json.dumps(command, default=str) + + if has_span_streaming_enabled(client.options): + span_first_data = { + "db.operation.name": operation_name, + "db.collection.name": collection_name, + SPANDATA.DB_QUERY_TEXT: query, + "sentry.op": OP.DB, + "sentry.origin": PyMongoIntegration.origin, + **db_data, + } + + span = sentry_sdk.traces.start_span( + name=query, attributes=span_first_data + ) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, + category="query", + type=OP.DB, + data=span_first_data, + ) + + else: + tags = { + "db.name": db_name, + SPANDATA.DB_SYSTEM: "mongodb", + SPANDATA.DB_DRIVER_NAME: "pymongo", + SPANDATA.DB_OPERATION: operation_name, + # The below is a deprecated field, but leaving for legacy reasons. + # The v2 spans will use `db.collection.name` instead. + SPANDATA.DB_MONGODB_COLLECTION: collection_name, + } + + try: + tags["net.peer.name"] = event.connection_id[0] + tags["net.peer.port"] = str(event.connection_id[1]) + except TypeError: + pass + + data: "Dict[str, Any]" = {"operation_ids": {}} + data["operation_ids"]["operation"] = event.operation_id + data["operation_ids"]["request"] = event.request_id + + data.update(db_data) + + try: + if lsid: + lsid_id = lsid["id"] + data["operation_ids"]["session"] = str(lsid_id) + except KeyError: + pass + + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=PyMongoIntegration.origin, + ) + + for tag, value in tags.items(): + # set the tag for backwards-compatibility. + # TODO: remove the set_tag call in the next major release! + span.set_tag(tag, value) + span.set_data(tag, value) + + for key, value in data.items(): + span.set_data(key, value) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, category="query", type=OP.DB, data=tags + ) + + self._ongoing_operations[self._operation_key(event)] = span.__enter__() + + def failed(self, event: "CommandFailedEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + # Ignoring NoOpStreamedSpan as it will always have a status of "ok" + if type(span) is StreamedSpan: + span.status = SpanStatus.ERROR + elif type(span) is Span: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + span.__exit__(None, None, None) + except KeyError: + return + + def succeeded(self, event: "CommandSucceededEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + if type(span) is Span: + span.set_status(SPANSTATUS.OK) + span.__exit__(None, None, None) + except KeyError: + pass + + +class PyMongoIntegration(Integration): + identifier = "pymongo" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py new file mode 100644 index 0000000000..6837d8345c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py @@ -0,0 +1,239 @@ +import functools +import os +import sys +import weakref + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +try: + from pyramid.httpexceptions import HTTPException + from pyramid.request import Request +except ImportError: + raise DidNotEnable("Pyramid not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional + + from pyramid.response import Response + from webob.cookies import RequestCookies + from webob.request import _FieldStorageWithFile + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.utils import ExcInfo + + +if getattr(Request, "authenticated_userid", None): + + def authenticated_userid(request: "Request") -> "Optional[Any]": + return request.authenticated_userid + +else: + # bw-compat for pyramid < 1.5 + from pyramid.security import authenticated_userid # type: ignore + + +TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") + + +class PyramidIntegration(Integration): + identifier = "pyramid" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "route_name") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + from pyramid import router + + old_call_view = router._call_view + + @functools.wraps(old_call_view) + def sentry_patched_call_view( + registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + integration = client.get_integration(PyramidIntegration) + if integration is None: + return old_call_view(registry, request, *args, **kwargs) + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii() and has_span_streaming_enabled(client.options): + user_id = authenticated_userid(request) + if user_id: + scope.set_user({"id": user_id}) + + scope.add_event_processor( + _make_event_processor(weakref.ref(request), integration) + ) + + return old_call_view(registry, request, *args, **kwargs) + + router._call_view = sentry_patched_call_view + + if hasattr(Request, "invoke_exception_view"): + old_invoke_exception_view = Request.invoke_exception_view + + def sentry_patched_invoke_exception_view( + self: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + rv = old_invoke_exception_view(self, *args, **kwargs) + + if ( + self.exc_info + and all(self.exc_info) + and rv.status_int == 500 + and sentry_sdk.get_client().get_integration(PyramidIntegration) + is not None + ): + _capture_exception(self.exc_info) + + return rv + + Request.invoke_exception_view = sentry_patched_invoke_exception_view + + old_wsgi_call = router.Router.__call__ + + @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) + def sentry_patched_wsgi_call( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + def sentry_patched_inner_wsgi_call( + environ: "Dict[str, Any]", start_response: "Callable[..., Any]" + ) -> "Any": + try: + return old_wsgi_call(self, environ, start_response) + except Exception: + einfo = sys.exc_info() + _capture_exception(einfo) + reraise(*einfo) + + middleware = SentryWsgiMiddleware( + sentry_patched_inner_wsgi_call, + span_origin=PyramidIntegration.origin, + ) + return middleware(environ, start_response) + + router.Router.__call__ = sentry_patched_wsgi_call + + +@ensure_integration_enabled(PyramidIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + if exc_info[0] is None or issubclass(exc_info[0], HTTPException): + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pyramid", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "route_name": request.matched_route.name, + "route_pattern": request.matched_route.pattern, + } + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if is_span_streaming_enabled + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + scope.set_transaction_name( + name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +class PyramidRequestExtractor(RequestExtractor): + def url(self) -> str: + return self.request.path_url + + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "RequestCookies": + return self.request.cookies + + def raw_data(self) -> str: + return self.request.text + + def form(self) -> "Dict[str, str]": + return { + key: value + for key, value in self.request.POST.items() + if not getattr(value, "filename", None) + } + + def files(self) -> "Dict[str, _FieldStorageWithFile]": + return { + key: value + for key, value in self.request.POST.items() + if getattr(value, "filename", None) + } + + def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: + file = postdata.file + try: + return os.fstat(file.fileno()).st_size + except Exception: + return 0 + + +def _make_event_processor( + weak_request: "Callable[[], Request]", integration: "PyramidIntegration" +) -> "EventProcessor": + def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + PyramidRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_info = event.setdefault("user", {}) + user_info.setdefault("id", authenticated_userid(request)) + + return event + + return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py new file mode 100644 index 0000000000..aae68c4c10 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py @@ -0,0 +1,197 @@ +from contextlib import contextmanager +from typing import Any, Generator + +import sentry_sdk +from sentry_sdk import start_span +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + logger, + parse_url, +) + +try: + from pyreqwest.client import ( # type: ignore[import-not-found] + ClientBuilder, + SyncClientBuilder, + ) + from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] + from pyreqwest.request import ( # type: ignore[import-not-found] + OneOffRequestBuilder, + Request, + SyncOneOffRequestBuilder, + ) + from pyreqwest.response import ( # type: ignore[import-not-found] + Response, + SyncResponse, + ) +except ImportError: + raise DidNotEnable("pyreqwest not installed or incompatible version installed") + + +class PyreqwestIntegration(Integration): + identifier = "pyreqwest" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_pyreqwest() + + +def _patch_pyreqwest() -> None: + # Patch Client Builders + _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) + _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) + + # Patch Request Builders + _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) + _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) + + +def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: + if not hasattr(cls, method_name): + return + + original_method = getattr(cls, method_name) + + def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + if not getattr(self, "_sentry_instrumented", False): + integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) + if integration is not None: + self.with_middleware(middleware) + try: + self._sentry_instrumented = True + except (TypeError, AttributeError): + # In case the instance itself is immutable or doesn't allow extra attributes + pass + return original_method(self, *args, **kwargs) + + setattr(cls, method_name, sentry_patched_method) + + +@contextmanager +def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": PyreqwestIntegration.origin, + SPANDATA.HTTP_REQUEST_METHOD: request.method, + }, + ) as span: + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + return + + with start_span( + op=OP.HTTP_CLIENT, + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + origin=PyreqwestIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + +async def sentry_async_middleware( + request: "Request", next_handler: "Next" +) -> "Response": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return await next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = await next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response + + +def sentry_sync_middleware( + request: "Request", next_handler: "SyncNext" +) -> "SyncResponse": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py new file mode 100644 index 0000000000..6a5603d825 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py @@ -0,0 +1,292 @@ +import asyncio +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Union + + from sentry_sdk._types import Event, EventProcessor + +try: + import quart_auth # type: ignore +except ImportError: + quart_auth = None + +try: + from quart import ( # type: ignore + Quart, + Request, + has_request_context, + has_websocket_context, + request, + websocket, + ) + from quart.signals import ( # type: ignore + got_background_exception, + got_request_exception, + got_websocket_exception, + request_started, + websocket_started, + ) +except ImportError: + raise DidNotEnable("Quart is not installed") +else: + # Quart 0.19 is based on Flask and hence no longer has a Scaffold + try: + from quart.scaffold import Scaffold # type: ignore + except ImportError: + from flask.sansio.scaffold import Scaffold # type: ignore + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class QuartIntegration(Integration): + identifier = "quart" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "endpoint") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + request_started.connect(_request_websocket_started) + websocket_started.connect(_request_websocket_started) + got_background_exception.connect(_capture_exception) + got_request_exception.connect(_capture_exception) + got_websocket_exception.connect(_capture_exception) + + patch_asgi_app() + patch_scaffold_route() + + +def patch_asgi_app() -> None: + old_app = Quart.__call__ + + async def sentry_patched_asgi_app( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + if sentry_sdk.get_client().get_integration(QuartIntegration) is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=QuartIntegration.origin, + asgi_version=3, + ) + return await middleware(scope, receive, send) + + Quart.__call__ = sentry_patched_asgi_app + + +def patch_scaffold_route() -> None: + # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 + if sys.version_info >= (3, 12): + iscoroutinefunction = inspect.iscoroutinefunction + else: + iscoroutinefunction = asyncio.iscoroutinefunction + + old_route = Scaffold.route + + def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": + old_decorator = old_route(*args, **kwargs) + + def decorator(old_func: "Any") -> "Any": + if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): + + @wraps(old_func) + @ensure_integration_enabled(QuartIntegration, old_func) + def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + span = get_current_span() + if span is not None and hasattr(span, "_segment"): + span._segment._update_active_thread() + else: + current_scope = sentry_sdk.get_current_scope() + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_func(*args, **kwargs) + + return old_decorator(_sentry_func) + + return old_decorator(old_func) + + return decorator + + Scaffold.route = _sentry_route + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if has_span_streaming_enabled(sentry_sdk.get_client().options) + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + + scope.set_transaction_name( + name=name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + if has_request_context(): + request_websocket = request._get_current_object() + if has_websocket_context(): + request_websocket = websocket._get_current_object() + + # Set the transaction name here, but rely on ASGI middleware + # to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket + ) + + scope = sentry_sdk.get_isolation_scope() + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + current_span = get_current_span() + if type(current_span) is StreamedSpan: + segment = current_span._segment + + segment.set_attribute("http.request.method", request_websocket.method) + header_attributes: "dict[str, Any]" = {} + + for header, header_value in _filter_headers( + dict(request_websocket.headers), use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + segment.set_attributes(header_attributes) + + if should_send_default_pii(): + segment.set_attribute("url.full", request_websocket.url) + segment.set_attribute( + "url.query", + request_websocket.query_string.decode("utf-8", errors="replace"), + ) + + user_properties = {} + if len(request_websocket.access_route) >= 1: + segment.set_attribute( + "client.address", request_websocket.access_route[0] + ) + user_properties["ip_address"] = request_websocket.access_route[0] + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_properties["id"] = current_user_id + + if user_properties: + existing_user_properties = scope._user or {} + scope.set_user({**existing_user_properties, **user_properties}) + + evt_processor = _make_request_event_processor(app, request_websocket, integration) + scope.add_event_processor(evt_processor) + + +def _make_request_event_processor( + app: "Quart", request: "Request", integration: "QuartIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + # TODO: Figure out what to do with request body. Methods on request + # are async, but event processors are not. + + request_info = event.setdefault("request", {}) + request_info["url"] = request.url + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + if len(request.access_route) >= 1: + request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_info = event.setdefault("user", {}) + user_info["id"] = current_user_id + + return event + + return inner + + +async def _capture_exception( + sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "quart", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_current_user_id_from_quart() -> "str | None": + if quart_auth is None: + return None + + if quart_auth.current_user is None: + return None + + try: + return quart_auth.current_user._auth_id + except Exception: + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py new file mode 100644 index 0000000000..f723a96f3c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py @@ -0,0 +1,240 @@ +import functools +import inspect +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + event_from_exception, + logger, + package_version, + qualname_from_function, + reraise, +) + +try: + import ray # type: ignore[import-not-found] + from ray import remote +except ImportError: + raise DidNotEnable("Ray not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional + + from sentry_sdk.utils import ExcInfo + + +def _check_sentry_initialized() -> None: + if sentry_sdk.get_client().is_active(): + return + + logger.debug( + "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." + ) + + +def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: + # Patching new_func signature to add the _sentry_tracing parameter to it + # Ray later inspects the signature and finds the unexpected parameter otherwise + signature = inspect.signature(func) + params = list(signature.parameters.values()) + sentry_tracing_param = inspect.Parameter( + "_sentry_tracing", + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + ) + + # Keyword only arguments are penultimate if function has variadic keyword arguments + if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: + params.insert(-1, sentry_tracing_param) + else: + params.append(sentry_tracing_param) + + func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] + + +def _patch_ray_remote() -> None: + old_remote = remote + + @functools.wraps(old_remote) + def new_remote( + f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" + ) -> "Callable[..., Any]": + if inspect.isclass(f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(f, *args, **kwargs) + + def wrapper(user_f: "Callable[..., Any]") -> "Any": + if inspect.isclass(user_f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(*args, **kwargs)(user_f) + + @functools.wraps(user_f) + def new_func( + *f_args: "Any", + _sentry_tracing: "Optional[dict[str, Any]]" = None, + **f_kwargs: "Any", + ) -> "Any": + _check_sentry_initialized() + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(_sentry_tracing or {}) + + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RAY, + "sentry.origin": RayIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + }, + parent_span=None, + ): + try: + result = user_f(*f_args, **f_kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + transaction = sentry_sdk.continue_trace( + _sentry_tracing or {}, + op=OP.QUEUE_TASK_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + source=TransactionSource.TASK, + ) + + with sentry_sdk.start_transaction(transaction) as transaction: + try: + result = user_f(*f_args, **f_kwargs) + transaction.set_status(SPANSTATUS.OK) + except Exception: + transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + _insert_sentry_tracing_in_signature(new_func) + + if f: + rv = old_remote(new_func) + else: + rv = old_remote(*args, **kwargs)(new_func) + old_remote_method = rv.remote + + def _remote_method_with_header_propagation( + *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Ray Client + """ + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_RAY, + "sentry.origin": RayIntegration.origin, + }, + ): + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + ) as span: + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + span.set_status(SPANSTATUS.OK) + except Exception: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + rv.remote = _remote_method_with_header_propagation + + return rv + + if f is not None: + return wrapper(f) + else: + return wrapper + + ray.remote = new_remote + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={ + "handled": False, + "type": RayIntegration.identifier, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class RayIntegration(Integration): + identifier = "ray" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = package_version("ray") + _check_minimum_version(RayIntegration, version) + + _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py new file mode 100644 index 0000000000..7095721ed2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py @@ -0,0 +1,49 @@ +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE +from sentry_sdk.integrations.redis.rb import _patch_rb +from sentry_sdk.integrations.redis.redis import _patch_redis +from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster +from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +class RedisIntegration(Integration): + identifier = "redis" + + def __init__( + self, + max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, + cache_prefixes: "Optional[list[str]]" = None, + ) -> None: + self.max_data_size = max_data_size + self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] + + if max_data_size is not None: + warnings.warn( + "The `max_data_size` parameter of `RedisIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + try: + from redis import StrictRedis, client + except ImportError: + raise DidNotEnable("Redis client not installed") + + _patch_redis(StrictRedis, client) + _patch_redis_cluster() + _patch_rb() + + try: + _patch_rediscluster() + except Exception: + logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py new file mode 100644 index 0000000000..8fc3d0c3a9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py @@ -0,0 +1,177 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from redis.asyncio.client import Pipeline, StrictRedis + from redis.asyncio.cluster import ClusterPipeline, RedisCluster + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_async_pipeline( + pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return await old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + try: + command_seq = self._execution_strategy._command_queue + except AttributeError: + if is_cluster: + command_seq = self._command_stack + else: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.is_transaction, + command_seq, + ) + + return await old_execute(self, *args, **kwargs) + + pipeline_cls.execute = _sentry_execute # type: ignore + + +def patch_redis_async_client( + cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return await old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = await old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py new file mode 100644 index 0000000000..58d686b099 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_pipeline( + pipeline_cls: "Any", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + command_seq = None + try: + command_seq = self._execution_strategy.command_queue + except AttributeError: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.transaction, + command_seq, + ) + + return old_execute(self, *args, **kwargs) + + pipeline_cls.execute = sentry_patched_execute + + +def patch_redis_client( + cls: "Any", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + """ + This function can be used to instrument custom redis client classes or + subclasses. + """ + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py new file mode 100644 index 0000000000..0822c2c930 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py @@ -0,0 +1,19 @@ +SPAN_ORIGIN = "auto.db.redis" + +_SINGLE_KEY_COMMANDS = frozenset( + ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], +) +_MULTI_KEY_COMMANDS = frozenset( + [ + "del", + "touch", + "unlink", + "mget", + ], +) +_COMMANDS_INCLUDING_SENSITIVE_DATA = [ + "auth", +] +_MAX_NUM_ARGS = 10 # Trim argument lists to this many values +_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values +_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py new file mode 100644 index 0000000000..35f20fddd9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py @@ -0,0 +1,136 @@ +""" +Code used for the Caches module in Sentry +""" + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +GET_COMMANDS = ("get", "mget") +SET_COMMANDS = ("set", "setex") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _get_op(name: str) -> "Optional[str]": + op = None + if name.lower() in GET_COMMANDS: + op = OP.CACHE_GET + elif name.lower() in SET_COMMANDS: + op = OP.CACHE_PUT + + return op + + +def _compile_cache_span_properties( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> "dict[str, Any]": + key = _get_safe_key(redis_command, args, kwargs) + key_as_string = _key_as_string(key) + keys_as_string = key_as_string.split(", ") + + is_cache_key = False + for prefix in integration.cache_prefixes: + for kee in keys_as_string: + if kee.startswith(prefix): + is_cache_key = True + break + if is_cache_key: + break + + value = None + if redis_command.lower() in SET_COMMANDS: + value = args[-1] + + properties = { + "op": _get_op(redis_command), + "description": _get_cache_span_description( + redis_command, args, kwargs, integration + ), + "key": key, + "key_as_string": key_as_string, + "redis_command": redis_command.lower(), + "is_cache_key": is_cache_key, + "value": value, + } + + return properties + + +def _get_cache_span_description( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> str: + description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_cache_data( + span: "Union[Span, StreamedSpan]", + redis_client: "Any", + properties: "dict[str, Any]", + return_value: "Optional[Any]", +) -> None: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + with capture_internal_exceptions(): + set_on_span(SPANDATA.CACHE_KEY, properties["key"]) + + if properties["redis_command"] in GET_COMMANDS: + if return_value is not None: + set_on_span(SPANDATA.CACHE_HIT, True) + size = ( + len(str(return_value).encode("utf-8")) + if not isinstance(return_value, bytes) + else len(return_value) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + else: + set_on_span(SPANDATA.CACHE_HIT, False) + + elif properties["redis_command"] in SET_COMMANDS: + if properties["value"] is not None: + size = ( + len(properties["value"].encode("utf-8")) + if not isinstance(properties["value"], bytes) + else len(properties["value"]) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + + try: + connection_params = redis_client.connection_pool.connection_kwargs + except AttributeError: + # If it is a cluster, there is no connection_pool attribute so we + # need to get the default node from the cluster instance + default_node = redis_client.get_default_node() + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + + host = connection_params.get("host") + if host is not None: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) + + port = connection_params.get("port") + if port is not None: + set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py new file mode 100644 index 0000000000..69207bf6f6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py @@ -0,0 +1,88 @@ +""" +Code used for the Queries module in Sentry +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_command +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import Redis + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _compile_db_span_properties( + integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" +) -> "dict[str, Any]": + description = _get_db_span_description(integration, redis_command, args) + + properties = { + "op": OP.DB_REDIS, + "description": description, + } + + return properties + + +def _get_db_span_description( + integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" +) -> str: + description = command_name + + with capture_internal_exceptions(): + description = _get_safe_command(command_name, args) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_db_data_on_span( + span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" +) -> None: + db = connection_params.get("db") + host = connection_params.get("host") + port = connection_params.get("port") + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) + + if host is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_attribute(SPANDATA.SERVER_PORT, port) + + else: + span.set_data(SPANDATA.DB_SYSTEM, "redis") + span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_data(SPANDATA.DB_NAME, str(db)) + + if host is not None: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_data(SPANDATA.SERVER_PORT, port) + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" +) -> None: + try: + _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) + except AttributeError: + pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py new file mode 100644 index 0000000000..e2ce863fe8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py @@ -0,0 +1,31 @@ +""" +Instrumentation for Redis Blaster (rb) + +https://github.com/getsentry/rb +""" + +from sentry_sdk.integrations.redis._sync_common import patch_redis_client +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + + +def _patch_rb() -> None: + try: + import rb.clients # type: ignore + except ImportError: + pass + else: + patch_redis_client( + rb.clients.FanoutClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.MappingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.RoutingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py new file mode 100644 index 0000000000..e704c9bc6a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py @@ -0,0 +1,67 @@ +""" +Instrumentation for Redis + +https://github.com/redis/redis-py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + +if TYPE_CHECKING: + from typing import Any, Sequence + + +def _get_redis_command_args(command: "Any") -> "Sequence[Any]": + return command[0] + + +def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 + patch_redis_client( + StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_pipeline( + client.Pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + try: + strict_pipeline = client.StrictPipeline + except AttributeError: + pass + else: + patch_redis_pipeline( + strict_pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + + try: + import redis.asyncio + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + redis.asyncio.client.StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_async_pipeline( + redis.asyncio.client.Pipeline, + False, + _get_redis_command_args, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py new file mode 100644 index 0000000000..b6c95e6abd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py @@ -0,0 +1,115 @@ +""" +Instrumentation for RedisCluster +This is part of the main redis-py client. + +https://github.com/redis/redis-py/blob/master/redis/cluster.py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import RedisCluster + from redis.asyncio.cluster import ( + ClusterPipeline as AsyncClusterPipeline, + ) + from redis.asyncio.cluster import ( + RedisCluster as AsyncRedisCluster, + ) + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +def _set_async_cluster_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_instance: "AsyncRedisCluster[Any]", +) -> None: + default_node = async_redis_cluster_instance.get_default_node() + if default_node is not None and default_node.connection_kwargs is not None: + _set_db_data_on_span(span, default_node.connection_kwargs) + + +def _set_async_cluster_pipeline_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", +) -> None: + with capture_internal_exceptions(): + client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) + if client is None: + # In older redis-py versions, the AsyncClusterPipeline had a `_client` + # attr but it is private so potentially problematic and mypy does not + # recognize it - see + # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 + client = ( + async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] + ) + + _set_async_cluster_db_data( + span, + client, + ) + + +def _set_cluster_db_data( + span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" +) -> None: + default_node = redis_cluster_instance.get_default_node() + + if default_node is not None: + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + _set_db_data_on_span(span, connection_params) + + +def _patch_redis_cluster() -> None: + """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" + try: + from redis import RedisCluster, cluster + except ImportError: + pass + else: + patch_redis_client( + RedisCluster, + is_cluster=True, + set_db_data_fn=_set_cluster_db_data, + ) + patch_redis_pipeline( + cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_cluster_db_data, + ) + + try: + from redis.asyncio import cluster as async_cluster + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + async_cluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_async_cluster_db_data, + ) + patch_redis_async_pipeline( + async_cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_async_cluster_pipeline_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py new file mode 100644 index 0000000000..3437aa1f2f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py @@ -0,0 +1,49 @@ +""" +Instrumentation for redis-py-cluster +The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). + +https://github.com/grokzen/redis-py-cluster +""" + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command + + +def _patch_rediscluster() -> None: + try: + import rediscluster # type: ignore + except ImportError: + return + + patch_redis_client( + rediscluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + + # up to v1.3.6, __version__ attribute is a tuple + # from v2.0.0, __version__ is a string and VERSION a tuple + version = getattr(rediscluster, "VERSION", rediscluster.__version__) + + # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 + # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst + if (0, 2, 0) < version < (2, 0, 0): + pipeline_cls = rediscluster.pipeline.StrictClusterPipeline + patch_redis_client( + rediscluster.StrictRedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + else: + pipeline_cls = rediscluster.pipeline.ClusterPipeline + + patch_redis_pipeline( + pipeline_cls, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py new file mode 100644 index 0000000000..7e04df9c69 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py @@ -0,0 +1,159 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations.redis.consts import ( + _COMMANDS_INCLUDING_SENSITIVE_DATA, + _MAX_NUM_ARGS, + _MAX_NUM_COMMANDS, + _MULTI_KEY_COMMANDS, + _SINGLE_KEY_COMMANDS, +) +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + +if TYPE_CHECKING: + from typing import Any, Optional, Sequence, Union + + +def _get_safe_command(name: str, args: "Sequence[Any]") -> str: + command_parts = [name] + + name_low = name.lower() + send_default_pii = should_send_default_pii() + + for i, arg in enumerate(args): + if i > _MAX_NUM_ARGS: + break + + if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + continue + + arg_is_the_key = i == 0 + if arg_is_the_key: + command_parts.append(repr(arg)) + else: + if send_default_pii: + command_parts.append(repr(arg)) + else: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + + command = " ".join(command_parts) + return command + + +def _safe_decode(key: "Any") -> str: + if isinstance(key, bytes): + try: + return key.decode() + except UnicodeDecodeError: + return "" + + return str(key) + + +def _key_as_string(key: "Any") -> str: + if isinstance(key, (dict, list, tuple)): + key = ", ".join(_safe_decode(x) for x in key) + elif isinstance(key, bytes): + key = _safe_decode(key) + elif key is None: + key = "" + else: + key = str(key) + + return key + + +def _get_safe_key( + method_name: str, + args: "Optional[tuple[Any, ...]]", + kwargs: "Optional[dict[str, Any]]", +) -> "Optional[tuple[str, ...]]": + """ + Gets the key (or keys) from the given method_name. + The method_name could be a redis command or a django caching command + """ + key = None + + if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: + # for example redis "mget" + key = tuple(args) + + elif args is not None and len(args) >= 1: + # for example django "set_many/get_many" or redis "get" + if isinstance(args[0], (dict, list, tuple)): + key = tuple(args[0]) + else: + key = (args[0],) + + elif kwargs is not None and "key" in kwargs: + # this is a legacy case for older versions of Django + if isinstance(kwargs["key"], (list, tuple)): + if len(kwargs["key"]) > 0: + key = tuple(kwargs["key"]) + else: + if kwargs["key"] is not None: + key = (kwargs["key"],) + + return key + + +def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": + return command.args + + +def _set_pipeline_data( + span: "Union[Span, StreamedSpan]", + is_cluster: bool, + get_command_args_fn: "Any", + is_transaction: bool, + commands_seq: "Sequence[Any]", +) -> None: + # TODO: Remove this whole function when removing transaction based tracing + if isinstance(span, StreamedSpan): + return + + span.set_tag("redis.is_cluster", is_cluster) + span.set_tag("redis.transaction", is_transaction) + + commands = [] + for i, arg in enumerate(commands_seq): + if i >= _MAX_NUM_COMMANDS: + break + + command = get_command_args_fn(arg) + commands.append(_get_safe_command(command[0], command[1:])) + + span.set_data( + "redis.commands", + { + "count": len(commands_seq), + "first_ten": commands, + }, + ) + + +def _set_client_data( + span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" +) -> None: + if isinstance(span, StreamedSpan): + if name: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) + else: + span.set_tag("redis.is_cluster", is_cluster) + if name: + span.set_tag("redis.command", name) + span.set_tag(SPANDATA.DB_OPERATION, name) + + if name and args: + name_low = name.lower() + if (name_low in _SINGLE_KEY_COMMANDS) or ( + name_low in _MULTI_KEY_COMMANDS and len(args) == 1 + ): + if isinstance(span, StreamedSpan): + span.set_attribute("db.redis.key", args[0]) + else: + span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py new file mode 100644 index 0000000000..edd48a9f67 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py @@ -0,0 +1,225 @@ +import functools +import weakref + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + format_timestamp, + parse_version, +) + +try: + from rq.job import JobStatus + from rq.queue import Queue + from rq.timeouts import JobTimeoutException + from rq.version import VERSION as RQ_VERSION + from rq.worker import Worker +except ImportError: + raise DidNotEnable("RQ not installed") + +try: + from rq.worker import BaseWorker + + if not hasattr(BaseWorker, "perform_job"): + BaseWorker = None +except ImportError: + BaseWorker = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + from rq.job import Job + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + +class RqIntegration(Integration): + identifier = "rq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(RQ_VERSION) + _check_minimum_version(RqIntegration, version) + + # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly + # instead of Worker, so we need to patch BaseWorker to cover both. + # For older versions where BaseWorker doesn't exist or doesn't have + # perform_job, we patch Worker. + worker_cls = BaseWorker if BaseWorker is not None else Worker + + old_perform_job = worker_cls.perform_job + + @functools.wraps(old_perform_job) + def sentry_patched_perform_job( + self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" + ) -> bool: + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_perform_job(self, job, *args, **kwargs) + + with sentry_sdk.new_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(weakref.ref(job))) + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace( + job.meta.get("_sentry_trace_headers") or {} + ) + + Scope.set_custom_sampling_context({"rq_job": job}) + + func_name = None + with capture_internal_exceptions(): + func_name = job.func_name + + with sentry_sdk.traces.start_span( + name="unknown RQ task" if func_name is None else func_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RQ, + "sentry.origin": RqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job.id, + }, + parent_span=None, + ) as span: + if func_name is not None: + span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) + + rv = old_perform_job(self, job, *args, **kwargs) + else: + transaction = continue_trace( + job.meta.get("_sentry_trace_headers") or {}, + op=OP.QUEUE_TASK_RQ, + name="unknown RQ task", + source=TransactionSource.TASK, + origin=RqIntegration.origin, + ) + + with capture_internal_exceptions(): + transaction.name = job.func_name + + with sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"rq_job": job}, + ): + rv = old_perform_job(self, job, *args, **kwargs) + + if self.is_horse: + # We're inside of a forked process and RQ is + # about to call `os._exit`. Make sure that our + # events get sent out. + sentry_sdk.get_client().flush() + + return rv + + worker_cls.perform_job = sentry_patched_perform_job + + old_handle_exception = worker_cls.handle_exception + + def sentry_patched_handle_exception( + self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" + ) -> "Any": + retry = ( + hasattr(job, "retries_left") + and job.retries_left + and job.retries_left > 0 + ) + failed = job._status == JobStatus.FAILED or job.is_failed + if failed and not retry: + _capture_exception(exc_info) + + return old_handle_exception(self, job, *exc_info, **kwargs) + + worker_cls.handle_exception = sentry_patched_handle_exception + + old_enqueue_job = Queue.enqueue_job + + @functools.wraps(old_enqueue_job) + def sentry_patched_enqueue_job( + self: "Queue", job: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_enqueue_job(self, job, **kwargs) + + scope = sentry_sdk.get_current_scope() + span = ( + scope.streamed_span + if has_span_streaming_enabled(client.options) + else scope.span + ) + if span is not None: + job.meta["_sentry_trace_headers"] = dict( + scope.iter_trace_propagation_headers() + ) + + return old_enqueue_job(self, job, **kwargs) + + Queue.enqueue_job = sentry_patched_enqueue_job + + ignore_logger("rq.worker") + + +def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + job = weak_job() + if job is not None: + with capture_internal_exceptions(): + extra = event.setdefault("extra", {}) + rq_job = { + "job_id": job.id, + "func": job.func_name, + "args": ( + job.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + job.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "description": job.description, + } + + if job.enqueued_at: + rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) + if job.started_at: + rq_job["started_at"] = format_timestamp(job.started_at) + + extra["rq-job"] = rq_job + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], JobTimeoutException): + event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "rq", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py new file mode 100644 index 0000000000..622e3c17af --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py @@ -0,0 +1,300 @@ +""" +This integration ingests tracing data from native extensions written in Rust. + +Using it requires additional setup on the Rust side to accept a +`RustTracingLayer` Python object and register it with the `tracing-subscriber` +using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: +```rust +#[pyfunction] +pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { + tracing_subscriber::registry() + .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) + .init(); +} +``` + +Usage in Python would then look like: +``` +sentry_sdk.init( + dsn=sentry_dsn, + integrations=[ + RustTracingIntegration( + "demo_rust_extension", + demo_rust_extension.initialize_tracing, + event_type_mapping=event_type_mapping, + ) + ], +) +``` + +Each native extension requires its own integration. +""" + +import json +from enum import Enum, auto +from typing import Any, Callable, Dict, Optional, Union + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + + +class RustTracingLevel(Enum): + Trace = "TRACE" + Debug = "DEBUG" + Info = "INFO" + Warn = "WARN" + Error = "ERROR" + + +class EventTypeMapping(Enum): + Ignore = auto() + Exc = auto() + Breadcrumb = auto() + Event = auto() + + +def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": + level = RustTracingLevel(level) + if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): + return "debug" + elif level == RustTracingLevel.Info: + return "info" + elif level == RustTracingLevel.Warn: + return "warning" + elif level == RustTracingLevel.Error: + return "error" + else: + # Better this than crashing + return "info" + + +def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": + metadata = event.get("metadata", {}) + contexts = {} + + location = {} + for field in ["module_path", "file", "line"]: + if field in metadata: + location[field] = metadata[field] + if len(location) > 0: + contexts["rust_tracing_location"] = location + + fields = {} + for field in metadata.get("fields", []): + fields[field] = event.get(field) + if len(fields) > 0: + contexts["rust_tracing_fields"] = fields + + return contexts + + +def process_event(event: "Dict[str, Any]") -> None: + metadata = event.get("metadata", {}) + + logger = metadata.get("target") + level = tracing_level_to_sentry_level(metadata.get("level")) + message: "sentry_sdk._types.Any" = event.get("message") + contexts = extract_contexts(event) + + sentry_event: "sentry_sdk._types.Event" = { + "logger": logger, + "level": level, + "message": message, + "contexts": contexts, + } + + sentry_sdk.capture_event(sentry_event) + + +def process_exception(event: "Dict[str, Any]") -> None: + process_event(event) + + +def process_breadcrumb(event: "Dict[str, Any]") -> None: + level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) + message = event.get("message") + + sentry_sdk.add_breadcrumb(level=level, message=message) + + +def default_span_filter(metadata: "Dict[str, Any]") -> bool: + return RustTracingLevel(metadata.get("level")) in ( + RustTracingLevel.Error, + RustTracingLevel.Warn, + RustTracingLevel.Info, + ) + + +def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": + level = RustTracingLevel(metadata.get("level")) + if level == RustTracingLevel.Error: + return EventTypeMapping.Exc + elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): + return EventTypeMapping.Breadcrumb + elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): + return EventTypeMapping.Ignore + else: + return EventTypeMapping.Ignore + + +class RustTracingLayer: + def __init__( + self, + origin: str, + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.origin = origin + self.event_type_mapping = event_type_mapping + self.span_filter = span_filter + self.include_tracing_fields = include_tracing_fields + + def _include_tracing_fields(self) -> bool: + """ + By default, the values of tracing fields are not included in case they + contain PII. A user may override that by passing `True` for the + `include_tracing_fields` keyword argument of this integration or by + setting `send_default_pii` to `True` in their Sentry client options. + """ + return ( + should_send_default_pii() + if self.include_tracing_fields is None + else self.include_tracing_fields + ) + + def on_event(self, event: str, sentry_span: "SentrySpan") -> None: + deserialized_event = json.loads(event) + metadata = deserialized_event.get("metadata", {}) + + event_type = self.event_type_mapping(metadata) + if event_type == EventTypeMapping.Ignore: + return + elif event_type == EventTypeMapping.Exc: + process_exception(deserialized_event) + elif event_type == EventTypeMapping.Breadcrumb: + process_breadcrumb(deserialized_event) + elif event_type == EventTypeMapping.Event: + process_event(deserialized_event) + + def on_new_span( + self, attrs: str, span_id: str + ) -> "Optional[Union[SentrySpan, StreamedSpan]]": + attrs = json.loads(attrs) + metadata = attrs.get("metadata", {}) + + if not self.span_filter(metadata): + return None + + module_path = metadata.get("module_path") + name = metadata.get("name") + message = attrs.get("message") + + if message is not None: + sentry_span_name = message + elif module_path is not None and name is not None: + sentry_span_name = f"{module_path}::{name}" # noqa: E231 + elif name is not None: + sentry_span_name = name + else: + sentry_span_name = "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + sentry_span = sentry_sdk.traces.start_span( + name=sentry_span_name, + attributes={ + "sentry.op": "function", + "sentry.origin": self.origin, + }, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_attribute(field, attrs.get(field)) + else: + sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) + + return sentry_span + + sentry_span = sentry_sdk.start_span( + op="function", + name=sentry_span_name, + origin=self.origin, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_data(field, attrs.get(field)) + else: + sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) + + sentry_span.__enter__() + return sentry_span + + def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: + if sentry_span is None: + return + + sentry_span.__exit__(None, None, None) + + def on_record( + self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" + ) -> None: + if sentry_span is None: + return + + set_on_span = ( + sentry_span.set_attribute + if isinstance(sentry_span, StreamedSpan) + else sentry_span.set_data + ) + + deserialized_values = json.loads(values) + for key, value in deserialized_values.items(): + if self._include_tracing_fields(): + set_on_span(key, value) + else: + set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) + + +class RustTracingIntegration(Integration): + """ + Ingests tracing data from a Rust native extension's `tracing` instrumentation. + + If a project uses more than one Rust native extension, each one will need + its own instance of `RustTracingIntegration` with an initializer function + specific to that extension. + + Since all of the setup for this integration requires instance-specific state + which is not available in `setup_once()`, setup instead happens in `__init__()`. + """ + + def __init__( + self, + identifier: str, + initializer: "Callable[[RustTracingLayer], None]", + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.identifier = identifier + origin = f"auto.function.rust_tracing.{identifier}" + self.tracing_layer = RustTracingLayer( + origin, event_type_mapping, span_filter, include_tracing_fields + ) + + initializer(self.tracing_layer) + + @staticmethod + def setup_once() -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py new file mode 100644 index 0000000000..908fceb0cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py @@ -0,0 +1,431 @@ +import sys +import warnings +import weakref +from inspect import isawaitable +from typing import TYPE_CHECKING +from urllib.parse import urlsplit + +import sentry_sdk +from sentry_sdk import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +if TYPE_CHECKING: + from collections.abc import Container + from typing import Any, Callable, Dict, Optional, Union + + from sanic.request import Request, RequestParameters + from sanic.response import BaseHTTPResponse + from sanic.router import Route + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +try: + from sanic import Sanic + from sanic import __version__ as SANIC_VERSION + from sanic.exceptions import SanicException + from sanic.handlers import ErrorHandler + from sanic.router import Router +except ImportError: + raise DidNotEnable("Sanic not installed") + +old_error_handler_lookup = ErrorHandler.lookup +old_handle_request = Sanic.handle_request +old_router_get = Router.get + +try: + # This method was introduced in Sanic v21.9 + old_startup = Sanic._startup +except AttributeError: + pass + + +class SanicIntegration(Integration): + identifier = "sanic" + origin = f"auto.http.{identifier}" + version: "Optional[tuple[int, ...]]" = None + + def __init__( + self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) + ) -> None: + """ + The unsampled_statuses parameter can be used to specify for which HTTP statuses the + transactions should not be sent to Sentry. By default, transactions are sent for all + HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all + HTTP statuses, including 404. + """ + self._unsampled_statuses = unsampled_statuses or set() + + @staticmethod + def setup_once() -> None: + SanicIntegration.version = parse_version(SANIC_VERSION) + _check_minimum_version(SanicIntegration, SanicIntegration.version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The sanic integration for Sentry requires Python 3.7+ " + " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + if SANIC_VERSION.startswith("0.8."): + # Sanic 0.8 and older creates a logger named "root" and puts a + # stringified version of every exception in there (without exc_info), + # which our error deduplication can't detect. + # + # We explicitly check the version here because it is a very + # invasive step to ignore this logger and not necessary in newer + # versions at all. + # + # https://github.com/huge-success/sanic/issues/1332 + ignore_logger("root") + + if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): + _setup_legacy_sanic() + return + + _setup_sanic() + + +class SanicRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return dict(self.request.cookies) + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "RequestParameters": + return self.request.form + + def is_json(self) -> bool: + raise NotImplementedError() + + def json(self) -> "Optional[Any]": + return self.request.json + + def files(self) -> "RequestParameters": + return self.request.files + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) + + +def _setup_sanic() -> None: + Sanic._startup = _startup + ErrorHandler.lookup = _sentry_error_handler_lookup + + +def _setup_legacy_sanic() -> None: + Sanic.handle_request = _legacy_handle_request + Router.get = _legacy_router_get + ErrorHandler.lookup = _sentry_error_handler_lookup + + +async def _startup(self: "Sanic") -> None: + # This happens about as early in the lifecycle as possible, just after the + # Request object is created. The body has not yet been consumed. + self.signal("http.lifecycle.request")(_context_enter) + + # This happens after the handler is complete. In v21.9 this signal is not + # dispatched when there is an exception. Therefore we need to close out + # and call _context_exit from the custom exception handler as well. + # See https://github.com/sanic-org/sanic/issues/2297 + self.signal("http.lifecycle.response")(_context_exit) + + # This happens inside of request handling immediately after the route + # has been identified by the router. + self.signal("http.routing.after")(_set_transaction) + + # The above signals need to be declared before this can be called. + await old_startup(self) + + +async def _context_enter(request: "Request") -> None: + request.ctx._sentry_do_integration = ( + sentry_sdk.get_client().get_integration(SanicIntegration) is not None + ) + + if not request.ctx._sentry_do_integration: + return + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + weak_request = weakref.ref(request) + request.ctx._sentry_scope = sentry_sdk.isolation_scope() + scope = request.ctx._sentry_scope.__enter__() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + if is_span_streaming_enabled: + integration = client.get_integration(SanicIntegration) + if ( + isinstance(integration, SanicIntegration) + and integration._unsampled_statuses + ): + warnings.warn( + "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", + stacklevel=2, + ) + + sentry_sdk.traces.continue_trace(dict(request.headers)) + scope.set_custom_sampling_context({"sanic_request": request}) + + if should_send_default_pii() and request.remote_addr: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + + span = sentry_sdk.traces.start_span( + # Unless the request results in a 404 error, the name and source + # will get overwritten in _set_transaction + name=request.path, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": SanicIntegration.origin, + "sentry.span.source": SegmentSource.URL.value, + }, + parent_span=None, + ) + request.ctx._sentry_root_span = span + else: + transaction = continue_trace( + dict(request.headers), + op=OP.HTTP_SERVER, + # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction + name=request.path, + source=TransactionSource.URL, + origin=SanicIntegration.origin, + ) + request.ctx._sentry_root_span = sentry_sdk.start_transaction( + transaction + ).__enter__() + + +async def _context_exit( + request: "Request", response: "Optional[BaseHTTPResponse]" = None +) -> None: + with capture_internal_exceptions(): + if not request.ctx._sentry_do_integration: + return + + integration = sentry_sdk.get_client().get_integration(SanicIntegration) + + response_status = None if response is None else response.status + + # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception + # happens while trying to end the transaction, we still attempt to exit the hub. + with capture_internal_exceptions(): + span = request.ctx._sentry_root_span + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes(request).items(): + span.set_attribute(attr, value) + if response_status is not None: + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) + span.status = "error" if response_status >= 400 else "ok" + + span.end() + + else: + span.set_http_status(response_status) + span.sampled &= ( + isinstance(integration, SanicIntegration) + and response_status not in integration._unsampled_statuses + ) + + span.__exit__(None, None, None) + + request.ctx._sentry_scope.__exit__(None, None, None) + + +async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: + if request.ctx._sentry_do_integration: + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + route_name = route.name.replace(request.app.name, "").strip(".") + scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) + + +def _sentry_error_handler_lookup( + self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" +) -> "Optional[object]": + _capture_exception(exception) + old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) + + if old_error_handler is None: + return None + + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return old_error_handler + + async def sentry_wrapped_error_handler( + request: "Request", exception: Exception + ) -> "Any": + try: + response = old_error_handler(request, exception) + if isawaitable(response): + response = await response + return response + except Exception: + # Report errors that occur in Sanic error handler. These + # exceptions will not even show up in Sanic's + # `sanic.exceptions` logger. + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + finally: + # As mentioned in previous comment in _startup, this can be removed + # after https://github.com/sanic-org/sanic/issues/2297 is resolved + if SanicIntegration.version and SanicIntegration.version == (21, 9): + await _context_exit(request) + + return sentry_wrapped_error_handler + + +async def _legacy_handle_request( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" +) -> "Any": + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return await old_handle_request(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + response = old_handle_request(self, request, *args, **kwargs) + if isawaitable(response): + response = await response + + return response + + +def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": + rv = old_router_get(self, *args) + if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: + with capture_internal_exceptions(): + scope = sentry_sdk.get_isolation_scope() + if SanicIntegration.version and SanicIntegration.version >= (21, 3): + # Sanic versions above and including 21.3 append the app name to the + # route name, and so we need to remove it from Route name so the + # transaction name is consistent across all versions + sanic_app_name = self.ctx.app.name + sanic_route = rv[0].name + + if sanic_route.startswith("%s." % sanic_app_name): + # We add a 1 to the len of the sanic_app_name because there is a dot + # that joins app name and the route name + # Format: app_name.route_name + sanic_route = sanic_route[len(sanic_app_name) + 1 :] + + scope.set_transaction_name( + sanic_route, source=TransactionSource.COMPONENT + ) + else: + scope.set_transaction_name( + rv[0].__name__, source=TransactionSource.COMPONENT + ) + + return rv + + +@ensure_integration_enabled(SanicIntegration) +def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "sanic", "handled": False}, + ) + + if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: + return + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_request_attributes(request: "Request") -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from a Sanic request. + """ + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + urlparts = urlsplit(request.url) + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.url + attributes["url.path"] = urlparts.path + + if urlparts.query: + attributes[SPANDATA.HTTP_QUERY] = urlparts.query + + if urlparts.scheme: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme + + if should_send_default_pii() and request.remote_addr: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr + + return attributes + + +def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": + def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + try: + if hint and issubclass(hint["exc_info"][0], SanicException): + return None + except KeyError: + pass + + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + extractor = SanicRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + urlparts = urlsplit(request.url) + + request_info["url"] = "%s://%s%s" % ( + urlparts.scheme, + urlparts.netloc, + urlparts.path, + ) + + request_info["query_string"] = urlparts.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_addr} + request_info["headers"] = _filter_headers(dict(request.headers)) + + return event + + return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py new file mode 100644 index 0000000000..16f91b28ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py @@ -0,0 +1,65 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import event_from_exception, reraise + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union, overload + + F = TypeVar("F", bound=Callable[..., Any]) + +else: + + def overload(x: "F") -> "F": + return x + + +@overload +def serverless_function(f: "F", flush: bool = True) -> "F": + pass + + +@overload +def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 + pass + + +def serverless_function( # noqa + f: "Optional[F]" = None, flush: bool = True +) -> "Union[F, Callable[[F], F]]": + def wrapper(f: "F") -> "F": + @wraps(f) + def inner(*args: "Any", **kwargs: "Any") -> "Any": + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + + try: + return f(*args, **kwargs) + except Exception: + _capture_and_reraise() + finally: + if flush: + sentry_sdk.flush() + + return inner # type: ignore + + if f is None: + return wrapper + else: + return wrapper(f) + + +def _capture_and_reraise() -> None: + exc_info = sys.exc_info() + client = sentry_sdk.get_client() + if client.is_active(): + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "serverless", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py new file mode 100644 index 0000000000..775170fb9f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py @@ -0,0 +1,142 @@ +import socket + +import sentry_sdk +from sentry_sdk._types import MYPY +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if MYPY: + from socket import AddressFamily, SocketKind + from typing import List, Optional, Tuple, Union + +__all__ = ["SocketIntegration"] + + +class SocketIntegration(Integration): + identifier = "socket" + origin = f"auto.socket.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) + """ + _patch_create_connection() + _patch_getaddrinfo() + + +def _get_span_description( + host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" +) -> str: + try: + host = host.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + try: + port = port.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + description = "%s:%s" % (host, port) # type: ignore + return description + + +def _patch_create_connection() -> None: + real_create_connection = socket.create_connection + + def create_connection( + address: "Tuple[Optional[str], int]", + timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore + source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, + ) -> "socket.socket": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_create_connection(address, timeout, source_address) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(address[0], address[1]), + attributes={ + "sentry.op": OP.SOCKET_CONNECTION, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if address[0] is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) + span.set_attribute(SPANDATA.SERVER_PORT, address[1]) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_CONNECTION, + name=_get_span_description(address[0], address[1]), + origin=SocketIntegration.origin, + ) as span: + span.set_data("address", address) + span.set_data("timeout", timeout) + span.set_data("source_address", source_address) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + + socket.create_connection = create_connection # type: ignore + + +def _patch_getaddrinfo() -> None: + real_getaddrinfo = socket.getaddrinfo + + def getaddrinfo( + host: "Union[bytes, str, None]", + port: "Union[bytes, str, int, None]", + family: int = 0, + type: int = 0, + proto: int = 0, + flags: int = 0, + ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_getaddrinfo(host, port, family, type, proto, flags) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(host, port), + attributes={ + "sentry.op": OP.SOCKET_DNS, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if isinstance(host, str): + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + elif isinstance(host, bytes): + span.set_attribute( + SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") + ) + + if isinstance(port, int): + span.set_attribute(SPANDATA.SERVER_PORT, port) + elif port is not None: + try: + span.set_attribute(SPANDATA.SERVER_PORT, int(port)) + except (ValueError, TypeError): + pass + + return real_getaddrinfo(host, port, family, type, proto, flags) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_DNS, + name=_get_span_description(host, port), + origin=SocketIntegration.origin, + ) as span: + span.set_data("host", host) + span.set_data("port", port) + + return real_getaddrinfo(host, port, family, type, proto, flags) + + socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py new file mode 100644 index 0000000000..10d94163c5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py @@ -0,0 +1,4 @@ +from sentry_sdk.integrations.spark.spark_driver import SparkIntegration +from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration + +__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py new file mode 100644 index 0000000000..a83532b6a6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Optional + + from pyspark import SparkContext + + from sentry_sdk._types import Event, Hint + + +class SparkIntegration(Integration): + identifier = "spark" + + @staticmethod + def setup_once() -> None: + _setup_sentry_tracing() + + +def _set_app_properties() -> None: + """ + Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. + This allows worker integration to have access to app_name and application_id. + """ + from pyspark import SparkContext + + spark_context = SparkContext._active_spark_context + if spark_context: + spark_context.setLocalProperty( + "sentry_app_name", + spark_context.appName, + ) + spark_context.setLocalProperty( + "sentry_application_id", + spark_context.applicationId, + ) + + +def _start_sentry_listener(sc: "SparkContext") -> None: + """ + Start java gateway server to add custom `SparkListener` + """ + from pyspark.java_gateway import ensure_callback_server_started + + gw = sc._gateway + ensure_callback_server_started(gw) + listener = SentryListener() + sc._jsc.sc().addSparkListener(listener) + + +def _add_event_processor(sc: "SparkContext") -> None: + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + if sentry_sdk.get_client().get_integration(SparkIntegration) is None: + return event + + if sc._active_spark_context is None: + return event + + event.setdefault("user", {}).setdefault("id", sc.sparkUser()) + + event.setdefault("tags", {}).setdefault( + "executor.id", sc._conf.get("spark.executor.id") + ) + event["tags"].setdefault( + "spark-submit.deployMode", + sc._conf.get("spark.submit.deployMode"), + ) + event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) + event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) + event["tags"].setdefault("spark_version", sc.version) + event["tags"].setdefault("app_name", sc.appName) + event["tags"].setdefault("application_id", sc.applicationId) + event["tags"].setdefault("master", sc.master) + event["tags"].setdefault("spark_home", sc.sparkHome) + + event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) + + return event + + +def _activate_integration(sc: "SparkContext") -> None: + _start_sentry_listener(sc) + _set_app_properties() + _add_event_processor(sc) + + +def _patch_spark_context_init() -> None: + from pyspark import SparkContext + + spark_context_init = SparkContext._do_init + + @ensure_integration_enabled(SparkIntegration, spark_context_init) + def _sentry_patched_spark_context_init( + self: "SparkContext", *args: "Any", **kwargs: "Any" + ) -> "Optional[Any]": + rv = spark_context_init(self, *args, **kwargs) + _activate_integration(self) + return rv + + SparkContext._do_init = _sentry_patched_spark_context_init + + +def _setup_sentry_tracing() -> None: + from pyspark import SparkContext + + if SparkContext._active_spark_context is not None: + _activate_integration(SparkContext._active_spark_context) + return + _patch_spark_context_init() + + +class SparkListener: + def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 + pass + + def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklistedForStage( # noqa: N802 + self, + executorBlacklistedForStage: "Any", # noqa: N803 + ) -> None: + pass + + def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 + pass + + def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 + pass + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + pass + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 + pass + + def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 + pass + + class Java: + implements = ["org.apache.spark.scheduler.SparkListenerInterface"] + + +class SentryListener(SparkListener): + def _add_breadcrumb( + self, + level: str, + message: str, + data: "Optional[dict[str, Any]]" = None, + ) -> None: + sentry_sdk.get_isolation_scope().add_breadcrumb( + level=level, message=message, data=data + ) + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + message = "Job {} Started".format(jobStart.jobId()) + self._add_breadcrumb(level="info", message=message) + _set_app_properties() + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + level = "" + message = "" + data = {"result": jobEnd.jobResult().toString()} + + if jobEnd.jobResult().toString() == "JobSucceeded": + level = "info" + message = "Job {} Ended".format(jobEnd.jobId()) + else: + level = "warning" + message = "Job {} Failed".format(jobEnd.jobId()) + + self._add_breadcrumb(level=level, message=message, data=data) + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + stage_info = stageSubmitted.stageInfo() + message = "Stage {} Submitted".format(stage_info.stageId()) + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + self._add_breadcrumb(level="info", message=message, data=data) + _set_app_properties() + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + from py4j.protocol import Py4JJavaError # type: ignore + + stage_info = stageCompleted.stageInfo() + message = "" + level = "" + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + # Have to Try Except because stageInfo.failureReason() is typed with Scala Option + try: + data["reason"] = stage_info.failureReason().get() + message = "Stage {} Failed".format(stage_info.stageId()) + level = "warning" + except Py4JJavaError: + message = "Stage {} Completed".format(stage_info.stageId()) + level = "info" + + self._add_breadcrumb(level=level, message=message, data=data) + + +def _get_attempt_id(stage_info: "Any") -> "Optional[int]": + try: + return stage_info.attemptId() + except Exception: + pass + + try: + return stage_info.attemptNumber() + except Exception: + pass + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py new file mode 100644 index 0000000000..5906472748 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py @@ -0,0 +1,109 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_hint_with_exc_info, + exc_info_from_error, + single_exception_from_error_tuple, + walk_exception_chain, +) + +if TYPE_CHECKING: + from typing import Any, Optional + + from sentry_sdk._types import Event, ExcInfo, Hint + + +class SparkWorkerIntegration(Integration): + identifier = "spark_worker" + + @staticmethod + def setup_once() -> None: + import pyspark.daemon as original_daemon + + original_daemon.worker_main = _sentry_worker_main + + +def _capture_exception(exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + + mechanism = {"type": "spark", "handled": False} + + exc_info = exc_info_from_error(exc_info) + + exc_type, exc_value, tb = exc_info + rv = [] + + # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + if exc_type not in (SystemExit, EOFError, ConnectionResetError): + rv.append( + single_exception_from_error_tuple( + exc_type, exc_value, tb, client.options, mechanism + ) + ) + + if rv: + rv.reverse() + hint = event_hint_with_exc_info(exc_info) + event: "Event" = {"level": "error", "exception": {"values": rv}} + + _tag_task_context() + + sentry_sdk.capture_event(event, hint=hint) + + +def _tag_task_context() -> None: + from pyspark.taskcontext import TaskContext + + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + integration = sentry_sdk.get_client().get_integration( + SparkWorkerIntegration + ) + task_context = TaskContext.get() + + if integration is None or task_context is None: + return event + + event.setdefault("tags", {}).setdefault( + "stageId", str(task_context.stageId()) + ) + event["tags"].setdefault("partitionId", str(task_context.partitionId())) + event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) + event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) + + if task_context._localProperties: + if "sentry_app_name" in task_context._localProperties: + event["tags"].setdefault( + "app_name", task_context._localProperties["sentry_app_name"] + ) + event["tags"].setdefault( + "application_id", + task_context._localProperties["sentry_application_id"], + ) + + if "callSite.short" in task_context._localProperties: + event.setdefault("extra", {}).setdefault( + "callSite", task_context._localProperties["callSite.short"] + ) + + return event + + +def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: + import pyspark.worker as original_worker + + try: + original_worker.main(*args, **kwargs) + except SystemExit: + if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py new file mode 100644 index 0000000000..962fe4ee53 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py @@ -0,0 +1,185 @@ +from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + parse_version, +) + +try: + from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore + from sqlalchemy.engine import Engine # type: ignore + from sqlalchemy.event import listen # type: ignore +except ImportError: + raise DidNotEnable("SQLAlchemy not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional, Union + + +class SqlalchemyIntegration(Integration): + identifier = "sqlalchemy" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(SQLALCHEMY_VERSION) + _check_minimum_version(SqlalchemyIntegration, version) + + listen(Engine, "before_cursor_execute", _before_cursor_execute) + listen(Engine, "after_cursor_execute", _after_cursor_execute) + listen(Engine, "handle_error", _handle_error) + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _before_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + executemany: bool, + *args: "Any", +) -> None: + ctx_mgr = record_sql_queries( + cursor, + statement, + parameters, + paramstyle=context and context.dialect and context.dialect.paramstyle or None, + executemany=executemany, + span_origin=SqlalchemyIntegration.origin, + ) + context._sentry_sql_span_manager = ctx_mgr + + span = ctx_mgr.__enter__() + + if span is not None: + _set_db_data(span, conn) + context._sentry_sql_span = span + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _after_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + *args: "Any", +) -> None: + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + context, "_sentry_sql_span_manager", None + ) + + # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. + span: "Optional[Union[Span, StreamedSpan]]" = getattr( + context, "_sentry_sql_span", None + ) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if ctx_mgr is not None: + context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + if isinstance(span, Span): + with capture_internal_exceptions(): + add_query_source(span) + + +def _handle_error(context: "Any", *args: "Any") -> None: + execution_context = context.execution_context + if execution_context is None: + return + + span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) + + if span is not None: + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # _after_cursor_execute does not get called for crashing SQL stmts. Judging + # from SQLAlchemy codebase it does seem like any error coming into this + # handler is going to be fatal. + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + execution_context, "_sentry_sql_span_manager", None + ) + + if ctx_mgr is not None: + execution_context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + +# See: https://docs.sqlalchemy.org/en/20/dialects/index.html +def _get_db_system(name: str) -> "Optional[str]": + name = str(name) + + if "sqlite" in name: + return "sqlite" + + if "postgres" in name: + return "postgresql" + + if "mariadb" in name: + return "mariadb" + + if "mysql" in name: + return "mysql" + + if "oracle" in name: + return "oracle" + + return None + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + db_system = _get_db_system(conn.engine.name) + + if isinstance(span, StreamedSpan): + if db_system is not None: + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) + else: + if db_system is not None: + span.set_data(SPANDATA.DB_SYSTEM, db_system) + + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + try: + driver = conn.dialect.driver + if driver: + set_on_span(SPANDATA.DB_DRIVER_NAME, driver) + except Exception: + pass + + if conn.engine.url is None: + return + + db_name = conn.engine.url.database + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + server_address = conn.engine.url.host + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = conn.engine.url.port + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py new file mode 100644 index 0000000000..3f9fbf4d8e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py @@ -0,0 +1,858 @@ +import functools +import json +import sys +import warnings +from collections.abc import Set +from copy import deepcopy +from json import JSONDecodeError +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations._asgi_common import _RootPathInPath +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + HttpCodeRangeContainer, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Awaitable, + Callable, + Container, + Dict, + Optional, + Tuple, + Union, + ) + + from sentry_sdk._types import Event, HttpStatusCodeRange +try: + import starlette + from starlette import __version__ as STARLETTE_VERSION + from starlette.applications import Starlette + from starlette.datastructures import ( + UploadFile, + ) + from starlette.middleware import Middleware + from starlette.middleware.authentication import ( + AuthenticationMiddleware, + ) + from starlette.requests import Request + from starlette.routing import Match + from starlette.types import ASGIApp, Receive, Send + from starlette.types import Scope as StarletteScope +except ImportError: + raise DidNotEnable("Starlette is not installed") + +try: + # Starlette 0.20 + from starlette.middleware.exceptions import ExceptionMiddleware +except ImportError: + # Startlette 0.19.1 + from starlette.exceptions import ExceptionMiddleware # type: ignore + +try: + # Optional dependency of Starlette to parse form data. + try: + # python-multipart 0.0.13 and later + import python_multipart as multipart + except ImportError: + # python-multipart 0.0.12 and earlier + import multipart # type: ignore +except ImportError: + multipart = None # type: ignore[assignment] + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +_DEFAULT_TRANSACTION_NAME = "generic Starlette request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class StarletteIntegration(Integration): + identifier = "starlette" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "url", + failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + middleware_spans: bool = False, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ): + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + if isinstance(failed_request_status_codes, Set): + self.failed_request_status_codes: "Container[int]" = ( + failed_request_status_codes + ) + else: + warnings.warn( + "Passing a list or None for failed_request_status_codes is deprecated. " + "Please pass a set of int instead.", + DeprecationWarning, + stacklevel=2, + ) + + if failed_request_status_codes is None: + self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES + else: + self.failed_request_status_codes = HttpCodeRangeContainer( + failed_request_status_codes + ) + + @staticmethod + def setup_once() -> None: + version = parse_version(STARLETTE_VERSION) + + if version is None: + raise DidNotEnable( + "Unparsable Starlette version: {}".format(STARLETTE_VERSION) + ) + + patch_middlewares() + # Starlette tolerates both starting with: + # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. + root_path_in_path = ( + _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED + ) + patch_asgi_app(root_path_in_path=root_path_in_path) + patch_request_response() + + if version >= (0, 24): + patch_templates() + + +def _enable_span_for_middleware( + middleware_class: "Any", +) -> "Any": + old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ + + async def _create_span_call( + app: "Any", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + **kwargs: "Any", + ) -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await old_call(app, scope, receive, send, **kwargs) + + # Update transaction name with middleware name + name, source = _get_transaction_from_middleware(app, scope, integration) + + if name is not None: + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=source, + ) + + if not integration.middleware_spans: + return await old_call(app, scope, receive, send, **kwargs) + + middleware_name = app.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarletteIntegration.origin, + "middleware.name": middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarletteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlette.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await send(*args, **kwargs) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(app, scope, new_receive, new_send, **kwargs) + + not_yet_patched = old_call.__name__ not in [ + "_create_span_call", + "_sentry_authenticationmiddleware_call", + "_sentry_exceptionmiddleware_call", + ] + + if not_yet_patched: + middleware_class.__call__ = _create_span_call + + return middleware_class + + +def _serialize_request_body_data(data: "Any") -> str: + # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values + def _default(value: "Any") -> "Any": + if isinstance(value, AnnotatedValue): + return value.value + return str(value) + + return json.dumps(data, default=_default) + + +@ensure_integration_enabled(StarletteIntegration) +def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarletteIntegration.identifier, "handled": handled}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def patch_exception_middleware(middleware_class: "Any") -> None: + """ + Capture all exceptions in Starlette app and + also extract user information. + """ + old_middleware_init = middleware_class.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + + if not_yet_patched: + + def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: + old_middleware_init(self, *args, **kwargs) + + # Patch existing exception handlers + old_handlers = self._exception_handlers.copy() + + async def _sentry_patched_exception_handler( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration( + StarletteIntegration + ) + + exp = args[0] + + if integration is not None: + is_http_server_error = ( + hasattr(exp, "status_code") + and isinstance(exp.status_code, int) + and exp.status_code in integration.failed_request_status_codes + ) + if is_http_server_error: + _capture_exception(exp, handled=True) + + # Find a matching handler + old_handler = None + for cls in type(exp).__mro__: + if cls in old_handlers: + old_handler = old_handlers[cls] + break + + if old_handler is None: + return + + if _is_async_callable(old_handler): + return await old_handler(self, *args, **kwargs) + else: + return old_handler(self, *args, **kwargs) + + for key in self._exception_handlers.keys(): + self._exception_handlers[key] = _sentry_patched_exception_handler + + middleware_class.__init__ = _sentry_middleware_init + + old_call = middleware_class.__call__ + + async def _sentry_exceptionmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + # Also add the user (that was eventually set by be Authentication middle + # that was called before this middleware). This is done because the authentication + # middleware sets the user in the scope and then (in the same function) + # calls this exception middelware. In case there is no exception (or no handler + # for the type of exception occuring) then the exception bubbles up and setting the + # user information into the sentry scope is done in auth middleware and the + # ASGI middleware will then send everything to Sentry and this is fine. + # But if there is an exception happening that the exception middleware here + # has a handler for, it will send the exception directly to Sentry, so we need + # the user information right now. + # This is why we do it here. + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_exceptionmiddleware_call + + +@ensure_integration_enabled(StarletteIntegration) +def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: + """ + Extracts user information from the ASGI scope and + adds it to Sentry's scope. + """ + if "user" not in scope: + return + + if not should_send_default_pii(): + return + + user_info: "Dict[str, Any]" = {} + starlette_user = scope["user"] + + username = getattr(starlette_user, "username", None) + if username: + user_info.setdefault("username", starlette_user.username) + + user_id = getattr(starlette_user, "id", None) + if user_id: + user_info.setdefault("id", starlette_user.id) + + email = getattr(starlette_user, "email", None) + if email: + user_info.setdefault("email", starlette_user.email) + + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + +def patch_authentication_middleware(middleware_class: "Any") -> None: + """ + Add user information to Sentry scope. + """ + old_call = middleware_class.__call__ + + not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) + + if not_yet_patched: + + async def _sentry_authenticationmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_authenticationmiddleware_call + + +def patch_middlewares() -> None: + """ + Patches Starlettes `Middleware` class to record + spans for every middleware invoked. + """ + old_middleware_init = Middleware.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + if not_yet_patched: + + def _sentry_middleware_init( + self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + if cls == SentryAsgiMiddleware: + return old_middleware_init(self, cls, *args, **kwargs) + + span_enabled_cls = _enable_span_for_middleware(cls) + old_middleware_init(self, span_enabled_cls, *args, **kwargs) + + if cls == AuthenticationMiddleware: + patch_authentication_middleware(cls) + + if cls == ExceptionMiddleware: + patch_exception_middleware(cls) + + Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] + + +def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: + """ + Instrument Starlette ASGI app using the SentryAsgiMiddleware. + """ + old_app = Starlette.__call__ + + async def _sentry_patched_asgi_app( + self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StarletteIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + mechanism_type=StarletteIntegration.identifier, + transaction_style=integration.transaction_style, + span_origin=StarletteIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + asgi_version=3, + root_path_in_path=root_path_in_path, + ) + + return await middleware(scope, receive, send) + + Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] + + +# This was vendored in from Starlette to support Starlette 0.19.1 because +# this function was only introduced in 0.20.x +def _is_async_callable(obj: "Any") -> bool: + while isinstance(obj, functools.partial): + obj = obj.func + + return iscoroutinefunction(obj) or ( + callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] + ) + + +def _get_cached_request_body_attribute( + client: "sentry_sdk.client.BaseClient", request: "Request" +) -> "Optional[str]": + """ + Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. + """ + if "content-length" not in request.headers: + return None + + try: + content_length = int(request.headers["content-length"]) + except ValueError: + return None + + if content_length and not request_body_within_bounds(client, content_length): + return OVER_SIZE_LIMIT_SUBSTITUTE + + if hasattr(request, "_json"): + return json.dumps(request._json) + + formdata_body = getattr(request, "_form", None) + if formdata_body is None: + return None + + form_data = {} + for key, val in formdata_body.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = val if not is_file else "[Unparsable]" + + return json.dumps(form_data) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), + integration.transaction_style, + request, + ) + + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Add info from request to event + request_info = event.get("request", {}) + if info: + if "cookies" in info: + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_request_response() -> None: + old_request_response = starlette.routing.request_response + + def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": + old_func = func + + is_coroutine = _is_async_callable(old_func) + if is_coroutine: + + async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_func, *args, **kwargs) + + func = _sentry_async_func + + else: + + @functools.wraps(old_func) + def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(StarletteIntegration) + if integration is None: + return old_func(*args, **kwargs) + + current_scope = sentry_sdk.get_current_scope() + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming: + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + current_span._segment._update_active_thread() + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + request = args[0] + + _set_transaction_name_and_source( + sentry_scope, integration.transaction_style, request + ) + + extractor = StarletteRequestExtractor(request) + cookies = extractor.extract_cookies_from_request() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor( + event: "Event", hint: "dict[str, Any]" + ) -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if cookies: + request_info["cookies"] = cookies + + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + return old_func(*args, **kwargs) + + func = _sentry_sync_func + + return old_request_response(func) + + starlette.routing.request_response = _sentry_request_response + + +def patch_templates() -> None: + # If markupsafe is not installed, then Jinja2 is not installed + # (markupsafe is a dependency of Jinja2) + # In this case we do not need to patch the Jinja2Templates class + try: + from markupsafe import Markup + except ImportError: + return # Nothing to do + + # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a + try: + from starlette.templating import Jinja2Templates + except ImportError: + return + + old_jinja2templates_init = Jinja2Templates.__init__ + + not_yet_patched = "_sentry_jinja2templates_init" not in str( + old_jinja2templates_init + ) + + if not_yet_patched: + + def _sentry_jinja2templates_init( + self: "Jinja2Templates", *args: "Any", **kwargs: "Any" + ) -> None: + def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": + trace_meta = Markup( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + return { + "sentry_trace_meta": trace_meta, + } + + kwargs.setdefault("context_processors", []) + + if add_sentry_trace_meta not in kwargs["context_processors"]: + kwargs["context_processors"].append(add_sentry_trace_meta) + + return old_jinja2templates_init(self, *args, **kwargs) + + Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] + + +class StarletteRequestExtractor: + """ + Extracts useful information from the Starlette request + (like form data or cookies) and adds it to the Sentry event. + """ + + def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: + self.request = request + + def extract_cookies_from_request( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + cookies: "Optional[Dict[str, Any]]" = None + if should_send_default_pii(): + cookies = self.cookies() + + return cookies + + async def extract_request_info( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + client = sentry_sdk.get_client() + + request_info: "Dict[str, Any]" = {} + + with capture_internal_exceptions(): + # Add cookies + if should_send_default_pii(): + request_info["cookies"] = self.cookies() + + # If there is no body, just return the cookies + content_length = await self.content_length() + if not content_length: + return request_info + + # Add annotation if body is too big + if content_length and not request_body_within_bounds( + client, content_length + ): + request_info["data"] = AnnotatedValue.removed_because_over_size_limit() + return request_info + + # Add JSON body, if it is a JSON request + json = await self.json() + if json: + request_info["data"] = json + return request_info + + # Add form as key/value pairs, if request has form data + form = await self.form() + if form: + form_data = {} + for key, val in form.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = ( + val + if not is_file + else AnnotatedValue.removed_because_raw_data() + ) + + request_info["data"] = form_data + return request_info + + # Raw data, do not add body just an annotation + request_info["data"] = AnnotatedValue.removed_because_raw_data() + return request_info + + async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": + if "content-length" in self.request.headers: + return int(self.request.headers["content-length"]) + + return None + + def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": + return self.request.cookies + + async def form(self: "StarletteRequestExtractor") -> "Any": + if multipart is None: + return None + + # Parse the body first to get it cached, as Starlette does not cache form() as it + # does with body() and json() https://github.com/encode/starlette/discussions/1933 + # Calling `.form()` without calling `.body()` first will + # potentially break the users project. + await self.request.body() + + return await self.request.form() + + def is_json(self: "StarletteRequestExtractor") -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": + if not self.is_json(): + return None + try: + return await self.request.json() + except JSONDecodeError: + return None + + +def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": + router = scope.get("router") + if not router: + return None + + for route in router.routes: + match = route.matches(scope) + if match[0] == Match.FULL: + try: + return route.path + except AttributeError: + # routes added via app.host() won't have a path attribute + return scope.get("path") + + return None + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = None + source = SOURCE_FOR_STYLE[transaction_style] + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or None + + elif transaction_style == "url": + name = _transaction_name_from_router(request.scope) + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + scope.set_transaction_name(name, source=source) + + +def _get_transaction_from_middleware( + app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" +) -> "Tuple[Optional[str], Optional[str]]": + name = None + source = None + + if integration.transaction_style == "endpoint": + name = transaction_from_function(app.__class__) + source = TransactionSource.COMPONENT + elif integration.transaction_style == "url": + name = _transaction_name_from_router(asgi_scope) + source = TransactionSource.ROUTE + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py new file mode 100644 index 0000000000..1eebd37e84 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py @@ -0,0 +1,314 @@ +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from pydantic import BaseModel + from starlite import Request, Starlite, State # type: ignore + from starlite.handlers.base import BaseRouteHandler # type: ignore + from starlite.middleware import DefineMiddleware # type: ignore + from starlite.plugins.base import get_plugin_for_value # type: ignore + from starlite.routes.http import HTTPRoute # type: ignore + from starlite.utils import ( # type: ignore + ConnectionDataExtractor, + Ref, + is_async_callable, + ) +except ImportError: + raise DidNotEnable("Starlite is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from starlite import MiddlewareProtocol + from starlite.types import ( # type: ignore + ASGIApp, + Hint, + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from starlite.types import ( + Scope as StarliteScope, + ) + + from sentry_sdk._types import Event + + +_DEFAULT_TRANSACTION_NAME = "generic Starlite request" + + +class StarliteIntegration(Integration): + identifier = "starlite" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + +class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + +def patch_app_init() -> None: + """ + Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. + See: + - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception + - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ + """ + old__init__ = Starlite.__init__ + + @ensure_integration_enabled(StarliteIntegration, old__init__) + def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: + after_exception = kwargs.pop("after_exception", []) + kwargs.update( + after_exception=[ + exception_handler, + *( + after_exception + if isinstance(after_exception, list) + else [after_exception] + ), + ] + ) + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Starlite.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryStarliteASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "StarliteScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(StarliteIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarliteIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarliteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlite.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await receive(*args, **kwargs) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await send(message) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + elif isinstance(route_handler.fn, Ref): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = StarliteIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if not scope_user: + return None + if isinstance(scope_user, dict): + return scope_user + if isinstance(scope_user, BaseModel): + return scope_user.dict() + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + plugin = get_plugin_for_value(scope_user) + if plugin and not is_async_callable(plugin.to_dict): + return plugin.to_dict(scope_user) + + return None + + +@ensure_integration_enabled(StarliteIntegration) +def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarliteIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py new file mode 100644 index 0000000000..746e09b36f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py @@ -0,0 +1,37 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.utils import parse_version + +try: + from statsig import statsig as statsig_module + from statsig.version import __version__ as STATSIG_VERSION +except ImportError: + raise DidNotEnable("statsig is not installed") + +if TYPE_CHECKING: + from statsig.statsig_user import StatsigUser + + +class StatsigIntegration(Integration): + identifier = "statsig" + + @staticmethod + def setup_once() -> None: + version = parse_version(STATSIG_VERSION) + _check_minimum_version(StatsigIntegration, version, "statsig") + + # Wrap and patch evaluation method(s) in the statsig module + old_check_gate = statsig_module.check_gate + + @wraps(old_check_gate) + def sentry_check_gate( + user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_check_gate(user, gate, *args, **kwargs) + add_feature_flag(gate, enabled) + return enabled + + statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py new file mode 100644 index 0000000000..82f30f2dda --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py @@ -0,0 +1,404 @@ +import os +import platform +import subprocess +import sys +from http.client import HTTPConnection, HTTPResponse +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + EnvironHeaders, + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + is_sentry_url, + logger, + parse_url, + safe_repr, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from sentry_sdk._types import Event, Hint + + +_RUNTIME_CONTEXT: "dict[str, object]" = { + "name": platform.python_implementation(), + "version": "%s.%s.%s" % (sys.version_info[:3]), + "build": sys.version, +} + + +class StdlibIntegration(Integration): + identifier = "stdlib" + + @staticmethod + def setup_once() -> None: + _install_httplib() + _install_subprocess() + + @add_global_event_processor + def add_python_runtime_context( + event: "Event", hint: "Hint" + ) -> "Optional[Event]": + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is not None: + contexts = event.setdefault("contexts", {}) + if isinstance(contexts, dict) and "runtime" not in contexts: + contexts["runtime"] = _RUNTIME_CONTEXT + + return event + + +def _complete_span(span: "Union[Span, StreamedSpan]") -> None: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + +def _install_httplib() -> None: + real_putrequest = HTTPConnection.putrequest + real_getresponse = HTTPConnection.getresponse + real_read = HTTPResponse.read + real_close = HTTPResponse.close + + def putrequest( + self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + default_port = self.default_port + + # proxies go through set_tunnel + tunnel_host = getattr(self, "_tunnel_host", None) + if tunnel_host: + host = tunnel_host + port = getattr(self, "_tunnel_port", default_port) + else: + host = self.host + port = self.port + + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is None or is_sentry_url( + client, host + ): + return real_putrequest(self, method, url, *args, **kwargs) + + real_url = url + if real_url is None or not real_url.startswith(("http://", "https://")): + real_url = "%s://%s%s%s" % ( + default_port == 443 and "https" or "http", + host, + port != default_port and ":%s" % port or "", + url, + ) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(real_url, sanitize=False) + + span_streaming = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + attributes={ + "sentry.origin": "auto.http.stdlib.httplib", + "sentry.op": OP.HTTP_CLIENT, + SPANDATA.HTTP_REQUEST_METHOD: method, + }, + ) + + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + + set_on_span = span.set_attribute + + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + origin="auto.http.stdlib.httplib", + ) + + span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + + set_on_span = span.set_data + + # for proxies, these point to the proxy host/port + if tunnel_host: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) + set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) + + rv = real_putrequest(self, method, url, *args, **kwargs) + + if should_propagate_trace(client, real_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=key, value=value, real_url=real_url + ) + ) + self.putheader(key, value) + + self._sentrysdk_span = span # type: ignore[attr-defined] + + return rv + + def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": + span = getattr(self, "_sentrysdk_span", None) + + if span is None: + return real_getresponse(self, *args, **kwargs) + + try: + rv = real_getresponse(self, *args, **kwargs) + except BaseException: + _complete_span(span) + raise + + if isinstance(span, StreamedSpan): + status_code = int(rv.status) + span.status = "error" if status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", status_code) + else: + span.set_http_status(int(rv.status)) + span.set_data("reason", rv.reason) + + # getresponse doesn't include actually reading the response body. This + # is done in read(). So if the metadata/headers suggest there's a body to + # read, don't finish the span just yet, but save it for ending it later. + has_body = rv.chunked or (rv.length is not None and rv.length > 0) + if has_body: + rv._sentrysdk_span = span # type: ignore[attr-defined] + else: + _complete_span(span) + + return rv + + def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": + try: + return real_read(self, *args, **kwargs) + finally: + span = getattr(self, "_sentrysdk_span", None) + # read() might be called multiple times to consume a single body, + # so we can't just end the span when read() is done. Instead, + # try to figure out whether the response body has been fully read. + if span and (self.fp is None or self.closed): + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + def close(self: "HTTPResponse") -> None: + # We patch close() as a best effort fallback in case the span is not + # ended yet in getresponse() or read(). + + try: + real_close(self) + finally: + span = getattr(self, "_sentrysdk_span", None) + if span is not None: + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + HTTPConnection.putrequest = putrequest # type: ignore[method-assign] + HTTPConnection.getresponse = getresponse # type: ignore[method-assign] + HTTPResponse.read = read # type: ignore[method-assign] + HTTPResponse.close = close # type: ignore[assignment,method-assign] + + +def _init_argument( + args: "List[Any]", + kwargs: "Dict[Any, Any]", + name: str, + position: int, + setdefault_callback: "Optional[Callable[[Any], Any]]" = None, +) -> "Any": + """ + given (*args, **kwargs) of a function call, retrieve (and optionally set a + default for) an argument by either name or position. + + This is useful for wrapping functions with complex type signatures and + extracting a few arguments without needing to redefine that function's + entire type signature. + """ + + if name in kwargs: + rv = kwargs[name] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + kwargs[name] = rv + elif position < len(args): + rv = args[position] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + args[position] = rv + else: + rv = setdefault_callback and setdefault_callback(None) + if rv is not None: + kwargs[name] = rv + + return rv + + +def _install_subprocess() -> None: + old_popen_init = subprocess.Popen.__init__ + + @ensure_integration_enabled(StdlibIntegration, old_popen_init) + def sentry_patched_popen_init( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> None: + # Convert from tuple to list to be able to set values. + a = list(a) + + args = _init_argument(a, kw, "args", 0) or [] + cwd = _init_argument(a, kw, "cwd", 9) + + # if args is not a list or tuple (and e.g. some iterator instead), + # let's not use it at all. There are too many things that can go wrong + # when trying to collect an iterator into a list and setting that list + # into `a` again. + # + # Also invocations where `args` is not a sequence are not actually + # legal. They just happen to work under CPython. + description = None + + if isinstance(args, (list, tuple)) and len(args) < 100: + with capture_internal_exceptions(): + description = " ".join(map(str, args)) + + if description is None: + description = safe_repr(args) + + env = None + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.SUBPROCESS, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.SUBPROCESS, + name=description, + origin="auto.subprocess.stdlib.subprocess", + ) + + with span: + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + if env is None: + env = _init_argument( + a, + kw, + "env", + 10, + lambda x: dict(x if x is not None else os.environ), + ) + env["SUBPROCESS_" + k.upper().replace("-", "_")] = v + + if cwd and isinstance(span, Span): + span.set_data("subprocess.cwd", cwd) + + rv = old_popen_init(self, *a, **kw) + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + else: + span.set_tag("subprocess.pid", self.pid) + + return rv + + subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore + + old_popen_wait = subprocess.Popen.wait + + @ensure_integration_enabled(StdlibIntegration, old_popen_wait) + def sentry_patched_popen_wait( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_WAIT, + attributes={ + "sentry.op": OP.SUBPROCESS_WAIT, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_wait(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_WAIT, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_wait(self, *a, **kw) + + subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore + + old_popen_communicate = subprocess.Popen.communicate + + @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) + def sentry_patched_popen_communicate( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_COMMUNICATE, + attributes={ + "sentry.op": OP.SUBPROCESS_COMMUNICATE, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_communicate(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_COMMUNICATE, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_communicate(self, *a, **kw) + + subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore + + +def get_subprocess_traceparent_headers() -> "EnvironHeaders": + return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py new file mode 100644 index 0000000000..5f00e8bf6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py @@ -0,0 +1,493 @@ +import functools +import hashlib +import warnings +from inspect import isawaitable + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from functools import cached_property +except ImportError: + # The strawberry integration requires Python 3.8+. functools.cached_property + # was added in 3.8, so this check is technically not needed, but since this + # is an auto-enabling integration, we might get to executing this import in + # lower Python versions, so we need to deal with it. + raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") + +try: + from strawberry import Schema + from strawberry.extensions import SchemaExtension + from strawberry.extensions.tracing.utils import ( + should_skip_tracing as strawberry_should_skip_tracing, + ) + from strawberry.http import async_base_view, sync_base_view +except ImportError: + raise DidNotEnable("strawberry-graphql is not installed") + +try: + from strawberry.extensions.tracing import ( + SentryTracingExtension as StrawberrySentryAsyncExtension, + ) + from strawberry.extensions.tracing import ( + SentryTracingExtensionSync as StrawberrySentrySyncExtension, + ) +except ImportError: + StrawberrySentryAsyncExtension = None + StrawberrySentrySyncExtension = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Generator, List, Optional + + from graphql import GraphQLError, GraphQLResolveInfo + from strawberry.http import GraphQLHTTPResponse + from strawberry.types import ExecutionContext + + from sentry_sdk._types import Event, EventProcessor + + +ignore_logger("strawberry.execution") + + +class StrawberryIntegration(Integration): + identifier = "strawberry" + origin = f"auto.graphql.{identifier}" + + def __init__(self, async_execution: "Optional[bool]" = None) -> None: + if async_execution not in (None, False, True): + raise ValueError( + 'Invalid value for async_execution: "{}" (must be bool)'.format( + async_execution + ) + ) + self.async_execution = async_execution + + @staticmethod + def setup_once() -> None: + version = package_version("strawberry-graphql") + _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") + + _patch_schema_init() + _patch_views() + + +def _patch_schema_init() -> None: + old_schema_init = Schema.__init__ + + @functools.wraps(old_schema_init) + def _sentry_patched_schema_init( + self: "Schema", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) + if integration is None: + return old_schema_init(self, *args, **kwargs) + + extensions = kwargs.get("extensions") or [] + + should_use_async_extension: "Optional[bool]" = None + if integration.async_execution is not None: + should_use_async_extension = integration.async_execution + else: + # try to figure it out ourselves + should_use_async_extension = _guess_if_using_async(extensions) + + if should_use_async_extension is None: + warnings.warn( + "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", + stacklevel=2, + ) + should_use_async_extension = False + + # remove the built in strawberry sentry extension, if present + extensions = [ + extension + for extension in extensions + if extension + not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) + ] + + # add our extension + extensions = [ + SentryAsyncExtension if should_use_async_extension else SentrySyncExtension + ] + extensions + + kwargs["extensions"] = extensions + + return old_schema_init(self, *args, **kwargs) + + Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] + + +class SentryAsyncExtension(SchemaExtension): + def __init__( + self: "Any", + *, + execution_context: "Optional[ExecutionContext]" = None, + ) -> None: + if execution_context: + self.execution_context = execution_context + + @cached_property + def _resource_name(self) -> str: + query_hash = self.hash_query(self.execution_context.query) # type: ignore + + if self.execution_context.operation_name: + return "{}:{}".format(self.execution_context.operation_name, query_hash) + + return query_hash + + def hash_query(self, query: str) -> str: + return hashlib.md5(query.encode("utf-8")).hexdigest() + + def on_operation(self) -> "Generator[None, None, None]": + operation_name = self.execution_context.operation_name + + operation_type = "query" + op = OP.GRAPHQL_QUERY + + if self.execution_context.query is None: + self.execution_context.query = "" + + if self.execution_context.query.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif self.execution_context.query.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + description = operation_type + if operation_name: + description += " {}".format(operation_name) + + sentry_sdk.add_breadcrumb( + category="graphql.operation", + data={ + "operation_name": operation_name, + "operation_type": operation_type, + }, + ) + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_request_event_processor(self.execution_context) + scope.add_event_processor(event_processor) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + additional_attributes: "dict[str, Any]" = {} + + if should_send_default_pii(): + additional_attributes["graphql.document"] = self.execution_context.query + + if operation_name: + additional_attributes["graphql.operation.name"] = operation_name + + graphql_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": op, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + graphql_span = sentry_sdk.start_span( + op=op, + name=description, + origin=StrawberryIntegration.origin, + ) + graphql_span.__enter__() + + if type(graphql_span) is Span: + if should_send_default_pii(): + graphql_span.set_data("graphql.document", self.execution_context.query) + + graphql_span.set_data("graphql.operation.type", operation_type) + graphql_span.set_data("graphql.operation.name", operation_name) + # This attribute is being removed in streamed spans + graphql_span.set_data("graphql.resource_name", self._resource_name) + + yield + + if type(graphql_span) is StreamedSpan: + if self.execution_context.operation_name: + segment = graphql_span._segment + segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) + segment.set_attribute("sentry.op", op) + segment.name = self.execution_context.operation_name + elif isinstance(graphql_span, Span): + transaction = graphql_span.containing_transaction + if transaction and self.execution_context.operation_name: + transaction.name = self.execution_context.operation_name + transaction.source = TransactionSource.COMPONENT + transaction.op = op + + graphql_span.__exit__(None, None, None) + + def on_validate(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + validation_span = sentry_sdk.traces.start_span( + name="validation", + attributes={ + "sentry.op": OP.GRAPHQL_VALIDATE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + validation_span = sentry_sdk.start_span( + op=OP.GRAPHQL_VALIDATE, + name="validation", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during validation, we still need to close the span + try: + yield + finally: + if isinstance(validation_span, StreamedSpan): + validation_span.end() + else: + validation_span.finish() + + def on_parse(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + parsing_span = sentry_sdk.traces.start_span( + name="parsing", + attributes={ + "sentry.op": OP.GRAPHQL_PARSE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + parsing_span = sentry_sdk.start_span( + op=OP.GRAPHQL_PARSE, + name="parsing", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during parsing, we still need to close the span + try: + yield + finally: + if isinstance(parsing_span, StreamedSpan): + parsing_span.end() + else: + parsing_span.finish() + + def should_skip_tracing( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + info: "GraphQLResolveInfo", + ) -> bool: + return strawberry_should_skip_tracing(_next, info) + + async def _resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + result = _next(root, info, *args, **kwargs) + + if isawaitable(result): + result = await result + + return result + + async def resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return await self._resolve(_next, root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return await self._resolve(_next, root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return await self._resolve(_next, root, info, *args, **kwargs) + + +class SentrySyncExtension(SentryAsyncExtension): + def resolve( + self, + _next: "Callable[[Any, Any, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return _next(root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return _next(root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return _next(root, info, *args, **kwargs) + + +def _patch_views() -> None: + old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors + old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors + + def _sentry_patched_async_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_async_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + def _sentry_patched_sync_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_sync_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + @ensure_integration_enabled(StrawberryIntegration) + def _sentry_patched_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + if not errors: + return + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_response_event_processor(response_data) + scope.add_event_processor(event_processor) + + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": StrawberryIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_async_view_handle_errors + ) + sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_sync_view_handle_errors + ) + + +def _make_request_event_processor( + execution_context: "ExecutionContext", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + request_data = event.setdefault("request", {}) + request_data["api_target"] = "graphql" + + if not request_data.get("data"): + data: "dict[str, Any]" = {"query": execution_context.query} + if execution_context.variables: + data["variables"] = execution_context.variables + if execution_context.operation_name: + data["operationName"] = execution_context.operation_name + + request_data["data"] = data + + else: + try: + del event["request"]["data"] + except (KeyError, TypeError): + pass + + return event + + return inner + + +def _make_response_event_processor( + response_data: "GraphQLHTTPResponse", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + contexts = event.setdefault("contexts", {}) + contexts["response"] = {"data": response_data} + + return event + + return inner + + +def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": + if StrawberrySentryAsyncExtension in extensions: + return True + elif StrawberrySentrySyncExtension in extensions: + return False + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py new file mode 100644 index 0000000000..4927c0e885 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py @@ -0,0 +1,65 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import NoReturn, Union + + +class SysExitIntegration(Integration): + """Captures sys.exit calls and sends them as events to Sentry. + + By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit + exceptions generated by sys.exit calls and send them to Sentry. + + This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and + non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. + Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. + """ + + identifier = "sys_exit" + + def __init__(self, *, capture_successful_exits: bool = False) -> None: + self._capture_successful_exits = capture_successful_exits + + @staticmethod + def setup_once() -> None: + SysExitIntegration._patch_sys_exit() + + @staticmethod + def _patch_sys_exit() -> None: + old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit + + @functools.wraps(old_exit) + def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": + # @ensure_integration_enabled ensures that this is non-None + integration = sentry_sdk.get_client().get_integration(SysExitIntegration) + if integration is None: + old_exit(__status) + + try: + old_exit(__status) + except SystemExit as e: + with capture_internal_exceptions(): + if integration._capture_successful_exits or __status not in ( + 0, + None, + ): + _capture_exception(e) + raise e + + sys.exit = sentry_patched_exit + + +def _capture_exception(exc: "SystemExit") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": SysExitIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py new file mode 100644 index 0000000000..f3ba046332 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py @@ -0,0 +1,196 @@ +import sys +import warnings +from concurrent.futures import Future, ThreadPoolExecutor +from functools import wraps +from threading import Thread, current_thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import use_isolation_scope, use_scope +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T", bound=Any) + + +class ThreadingIntegration(Integration): + identifier = "threading" + + def __init__( + self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True + ) -> None: + if propagate_hub is not None: + logger.warning( + "Deprecated: propagate_hub is deprecated. This will be removed in the future." + ) + + # Note: propagate_hub did not have any effect on propagation of scope data + # scope data was always propagated no matter what the value of propagate_hub was + # This is why the default for propagate_scope is True + + self.propagate_scope = propagate_scope + + if propagate_hub is not None: + self.propagate_scope = propagate_hub + + @staticmethod + def setup_once() -> None: + old_start = Thread.start + + try: + from django import VERSION as django_version # noqa: N811 + except ImportError: + django_version = None + + try: + import channels # type: ignore[import-untyped] + + channels_version = channels.__version__ + except (ImportError, AttributeError): + channels_version = None + + is_async_emulated_with_threads = ( + sys.version_info < (3, 9) + and channels_version is not None + and channels_version < "4.0.0" + and django_version is not None + and django_version >= (3, 0) + and django_version < (4, 0) + ) + + @wraps(old_start) + def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return old_start(self, *a, **kw) + + if integration.propagate_scope: + if is_async_emulated_with_threads: + warnings.warn( + "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " + "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " + "Please either upgrade to Django channels 4.0+, use Django's async features " + "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", + stacklevel=2, + ) + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + + else: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + # Patching instance methods in `start()` creates a reference cycle if + # done in a naive way. See + # https://github.com/getsentry/sentry-python/pull/434 + # + # In threading module, using current_thread API will access current thread instance + # without holding it to avoid a reference cycle in an easier way. + with capture_internal_exceptions(): + new_run = _wrap_run( + isolation_scope, + current_scope, + getattr(self.run, "__func__", self.run), + ) + self.run = new_run # type: ignore + + return old_start(self, *a, **kw) + + Thread.start = sentry_start # type: ignore + ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore + ThreadPoolExecutor.submit, is_async_emulated_with_threads + ) + + +def _wrap_run( + isolation_scope_to_use: "Optional[sentry_sdk.Scope]", + current_scope_to_use: "Optional[sentry_sdk.Scope]", + old_run_func: "F", +) -> "F": + @wraps(old_run_func) + def run(*a: "Any", **kw: "Any") -> "Any": + def _run_old_run_func() -> "Any": + try: + self = current_thread() + return old_run_func(self, *a[1:], **kw) + except Exception: + reraise(*_capture_exception()) + + if isolation_scope_to_use is not None and current_scope_to_use is not None: + with use_isolation_scope(isolation_scope_to_use): + with use_scope(current_scope_to_use): + return _run_old_run_func() + else: + return _run_old_run_func() + + return run # type: ignore + + +def _wrap_threadpool_executor_submit( + func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool +) -> "Callable[..., Future[T]]": + """ + Wrap submit call to propagate scopes on task submission. + """ + + @wraps(func) + def sentry_submit( + self: "ThreadPoolExecutor", + fn: "Callable[..., T]", + *args: "Any", + **kwargs: "Any", + ) -> "Future[T]": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return func(self, fn, *args, **kwargs) + + if integration.propagate_scope and is_async_emulated_with_threads: + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + elif integration.propagate_scope: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": + if isolation_scope is not None and current_scope is not None: + with use_isolation_scope(isolation_scope): + with use_scope(current_scope): + return fn(*args, **kwargs) + + return fn(*args, **kwargs) + + return func(self, wrapped_fn, *args, **kwargs) + + return sentry_submit + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + if client.get_integration(ThreadingIntegration) is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "threading", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py new file mode 100644 index 0000000000..859b0d0870 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py @@ -0,0 +1,320 @@ +import contextlib +import weakref +from inspect import iscoroutinefunction + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + RequestExtractor, + _filter_headers, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from tornado import version_info as TORNADO_VERSION + from tornado.gen import coroutine + from tornado.web import HTTPError, RequestHandler +except ImportError: + raise DidNotEnable("Tornado not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.tracing import Span + + +class TornadoIntegration(Integration): + identifier = "tornado" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(TornadoIntegration, TORNADO_VERSION) + + if not HAS_REAL_CONTEXTVARS: + # Tornado is async. We better have contextvars or we're going to leak + # state between requests. + raise DidNotEnable( + "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" + + CONTEXTVARS_ERROR_MESSAGE + ) + + ignore_logger("tornado.access") + + old_execute = RequestHandler._execute + + awaitable = iscoroutinefunction(old_execute) + + if awaitable: + # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) + # In that case our method should be a coroutine function too + async def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + return await old_execute(self, *args, **kwargs) + + else: + + @coroutine # type: ignore + def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + result = yield from old_execute(self, *args, **kwargs) + return result + + RequestHandler._execute = sentry_execute_request_handler + + old_log_exception = RequestHandler.log_exception + + def sentry_log_exception( + self: "Any", + ty: type, + value: BaseException, + tb: "Any", + *args: "Any", + **kwargs: "Any", + ) -> "Optional[Any]": + _capture_exception(ty, value, tb) + return old_log_exception(self, ty, value, tb, *args, **kwargs) + + RequestHandler.log_exception = sentry_log_exception + + +_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" + + +@contextlib.contextmanager +def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": + integration = sentry_sdk.get_client().get_integration(TornadoIntegration) + + if integration is None: + yield + return + + weak_handler = weakref.ref(self) + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + headers = self.request.headers + + scope.clear_breadcrumbs() + processor = _make_event_processor(weak_handler) + scope.add_event_processor(processor) + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(dict(headers)) + scope.set_custom_sampling_context({"tornado_request": self.request}) + + if should_send_default_pii() and self.request.remote_ip: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_ROOT_SPAN_NAME, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": TornadoIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # Like with all other integrations, this is our + # fallback transaction in case there is no route. + # sentry_urldispatcher_resolve is responsible for + # setting a transaction name later. + name=_DEFAULT_ROOT_SPAN_NAME, + source=TransactionSource.ROUTE, + origin=TornadoIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"tornado_request": self.request}, + ) + + with span_ctx as span: + try: + yield + finally: + if type(span) is StreamedSpan: + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + self.request + ).items(): + span.set_attribute(attr, value) + + with capture_internal_exceptions(): + method = getattr(self, self.request.method.lower(), None) + if method is not None: + span_name = transaction_from_function(method) + if span_name: + span.name = span_name + span.set_attribute( + "sentry.span.source", + SegmentSource.COMPONENT, + ) + + with capture_internal_exceptions(): + status_int = self.get_status() + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) + span.status = "error" if status_int >= 400 else "ok" + + +def _get_request_attributes(request: "Any") -> "Dict[str, Any]": + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.full_url() + attributes["url.path"] = request.path + + if request.query: + attributes[SPANDATA.URL_QUERY] = request.query + + if request.protocol: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol + + if should_send_default_pii() and request.remote_ip: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + + with capture_internal_exceptions(): + raw_data = _get_tornado_request_data(request) + body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data + if body_data is not None: + attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data + + return attributes + + +def _get_tornado_request_data( + request: "Any", +) -> "Union[Optional[str], AnnotatedValue]": + body = request.body + if not body: + return None + + if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): + return AnnotatedValue.substituted_because_over_size_limit() + + return body.decode("utf-8", "replace") + + +@ensure_integration_enabled(TornadoIntegration) +def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: + if isinstance(value, HTTPError): + return + + event, hint = event_from_exception( + (ty, value, tb), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "tornado", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + weak_handler: "Callable[[], RequestHandler]", +) -> "EventProcessor": + def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + handler = weak_handler() + if handler is None: + return event + + request = handler.request + + with capture_internal_exceptions(): + method = getattr(handler, handler.request.method.lower()) + event["transaction"] = transaction_from_function(method) or "" + event["transaction_info"] = {"source": TransactionSource.COMPONENT} + + with capture_internal_exceptions(): + extractor = TornadoRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + + request_info["url"] = "%s://%s%s" % ( + request.protocol, + request.host, + request.path, + ) + + request_info["query_string"] = request.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_ip} + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + try: + current_user = handler.current_user + except Exception: + current_user = None + + if current_user: + event.setdefault("user", {}).setdefault("is_authenticated", True) + + return event + + return tornado_processor + + +class TornadoRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return {k: v.value for k, v in self.request.cookies.items()} + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "Dict[str, Any]": + return { + k: [v.decode("latin1", "replace") for v in vs] + for k, vs in self.request.body_arguments.items() + } + + def is_json(self) -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + def files(self) -> "Dict[str, Any]": + return {k: v[0] for k, v in self.request.files.items() if v} + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py new file mode 100644 index 0000000000..0449a8f10c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py @@ -0,0 +1,52 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.utils import ensure_integration_enabled, event_from_exception + +try: + from trytond.exceptions import TrytonException # type: ignore + from trytond.wsgi import app # type: ignore +except ImportError: + raise DidNotEnable("Trytond is not installed.") + +# TODO: trytond-worker, trytond-cron and trytond-admin intergations + + +class TrytondWSGIIntegration(Integration): + identifier = "trytond_wsgi" + origin = f"auto.http.{identifier}" + + def __init__(self) -> None: + pass + + @staticmethod + def setup_once() -> None: + app.wsgi_app = SentryWsgiMiddleware( + app.wsgi_app, + span_origin=TrytondWSGIIntegration.origin, + ) + + @ensure_integration_enabled(TrytondWSGIIntegration) + def error_handler(e: Exception) -> None: + if isinstance(e, TrytonException): + return + else: + client = sentry_sdk.get_client() + event, hint = event_from_exception( + e, + client_options=client.options, + mechanism={"type": "trytond", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + # Expected error handlers signature was changed + # when the error_handler decorator was introduced + # in Tryton-5.4 + if hasattr(app, "error_handler"): + + @app.error_handler + def _(app, request, e): # type: ignore + error_handler(e) + + else: + app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py new file mode 100644 index 0000000000..497f0539ec --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + +try: + import typer + from typer.main import except_hook +except ImportError: + raise DidNotEnable("Typer not installed") + + +class TyperIntegration(Integration): + identifier = "typer" + + @staticmethod + def setup_once() -> None: + typer.main.except_hook = _make_excepthook(except_hook) # type: ignore + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(TyperIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "typer", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py new file mode 100644 index 0000000000..0316f6b88a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py @@ -0,0 +1,33 @@ +from functools import wraps +from typing import Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from UnleashClient import UnleashClient +except ImportError: + raise DidNotEnable("UnleashClient is not installed") + + +class UnleashIntegration(Integration): + identifier = "unleash" + + @staticmethod + def setup_once() -> None: + # Wrap and patch evaluation methods (class methods) + old_is_enabled = UnleashClient.is_enabled + + @wraps(old_is_enabled) + def sentry_is_enabled( + self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_is_enabled(self, feature, *args, **kwargs) + + # We have no way of knowing what type of unleash feature this is, so we have to treat + # it as a boolean / toggle feature. + add_feature_flag(feature, enabled) + + return enabled + + UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py new file mode 100644 index 0000000000..2c7280a1f2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py @@ -0,0 +1,50 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Callable + + +class UnraisablehookIntegration(Integration): + identifier = "unraisablehook" + + @staticmethod + def setup_once() -> None: + sys.unraisablehook = _make_unraisable(sys.unraisablehook) + + +def _make_unraisable( + old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", +) -> "Callable[[sys.UnraisableHookArgs], Any]": + def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: + integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_unraisablehook(unraisable) + + if unraisable.exc_value and unraisable.exc_traceback: + with capture_internal_exceptions(): + event, hint = event_from_exception( + ( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "unraisablehook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_unraisablehook(unraisable) + + return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py new file mode 100644 index 0000000000..e776ed915a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py @@ -0,0 +1,427 @@ +import sys +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._werkzeug import _get_headers, get_host +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + _filter_headers, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Iterator, + Optional, + Protocol, + Tuple, + TypeVar, + Union, + ) + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + WsgiResponseIter = TypeVar("WsgiResponseIter") + WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") + WsgiExcInfo = TypeVar("WsgiExcInfo") + + class StartResponse(Protocol): + def __call__( + self, + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, + ) -> "WsgiResponseIter": # type: ignore + pass + + +_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_DEFAULT_TRANSACTION_NAME = "generic WSGI request" + + +def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: + return s.encode("latin1").decode(charset, errors) + + +def get_request_url( + environ: "Dict[str, str]", use_x_forwarded_for: bool = False +) -> str: + """Return the absolute URL without query string for the given WSGI + environment.""" + script_name = environ.get("SCRIPT_NAME", "").rstrip("/") + path_info = environ.get("PATH_INFO", "").lstrip("/") + path = f"{script_name}/{path_info}" + + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + return "%s://%s/%s" % ( + scheme, + get_host(environ, use_x_forwarded_for), + wsgi_decoding_dance(path).lstrip("/"), + ) + + +class SentryWsgiMiddleware: + __slots__ = ( + "app", + "use_x_forwarded_for", + "span_origin", + "http_methods_to_capture", + ) + + def __init__( + self, + app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", + use_x_forwarded_for: bool = False, + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + self.app = app + self.use_x_forwarded_for = use_x_forwarded_for + self.span_origin = span_origin + self.http_methods_to_capture = http_methods_to_capture + + def __call__( + self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "Any": + if _wsgi_middleware_applied.get(False): + return self.app(environ, start_response) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _wsgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for + ) + ) + + method = environ.get("REQUEST_METHOD", "").upper() + + span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None + if method in self.http_methods_to_capture: + if span_streaming: + sentry_sdk.traces.continue_trace( + dict(_get_headers(environ)) + ) + Scope.set_custom_sampling_context({"wsgi_environ": environ}) + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, client_ip + ) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_TRANSACTION_NAME, + attributes={ + "sentry.span.source": SegmentSource.ROUTE, + "sentry.origin": self.span_origin, + "sentry.op": OP.HTTP_SERVER, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + environ, + op=OP.HTTP_SERVER, + name=_DEFAULT_TRANSACTION_NAME, + source=TransactionSource.ROUTE, + origin=self.span_origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"wsgi_environ": environ}, + ) + + span_ctx = span_ctx or nullcontext() + + with span_ctx as span: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + environ, self.use_x_forwarded_for + ).items(): + span.set_attribute(attr, value) + + try: + response = self.app( + environ, + partial(_sentry_start_response, start_response, span), + ) + except BaseException: + reraise(*_capture_exception()) + finally: + _wsgi_middleware_applied.set(False) + + # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses + # is determined by a pointer equality check on the response object + # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). + # + # If we were to return a _ScopedResponse, this would cause the check to always fail + # since it's checking the files are exactly the same. + # + # To avoid this and ensure that the offloading mechanism works as expected when it's + # enabled, we check if the response is a file-like object (determined by the presence + # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, + # it would've been used in handling the file in the response). + # + # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, + # so we want to make sure we don't interfere with those either. + # + # If all conditions are met, we return the original response object directly, + # allowing uWSGI to handle it as intended. + if ( + environ.get("wsgi.file_wrapper") + and getattr(response, "fileno", None) is not None + ): + return response + + return _ScopedResponse(scope, response) + + +def _sentry_start_response( + old_start_response: "StartResponse", + span: "Optional[Union[Span, StreamedSpan]]", + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, +) -> "WsgiResponseIter": # type: ignore[type-var] + with capture_internal_exceptions(): + status_int = int(status.split(" ", 1)[0]) + if span is not None: + if isinstance(span, StreamedSpan): + span.status = "error" if status_int >= 400 else "ok" + span.set_attribute("http.response.status_code", status_int) + else: + span.set_http_status(status_int) + + if exc_info is None: + # The Django Rest Framework WSGI test client, and likely other + # (incorrect) implementations, cannot deal with the exc_info argument + # if one is present. Avoid providing a third argument if not necessary. + return old_start_response(status, response_headers) + else: + return old_start_response(status, response_headers, exc_info) + + +def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns our explicitly included environment variables we want to + capture (server name, port and remote addr if pii is enabled). + """ + keys = ["SERVER_NAME", "SERVER_PORT"] + if should_send_default_pii(): + # make debugging of proxy setup easier. Proxy headers are + # in headers. + keys += ["REMOTE_ADDR"] + + for key in keys: + if key in environ: + yield key, environ[key] + + +def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": + """ + Infer the user IP address from various headers. This cannot be used in + security sensitive situations since the value may be forged from a client, + but it's good enough for the event payload. + """ + try: + return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return environ["HTTP_X_REAL_IP"] + except KeyError: + pass + + return environ.get("REMOTE_ADDR") + + +def _capture_exception() -> "ExcInfo": + """ + Captures the current exception and sends it to Sentry. + Returns the ExcInfo tuple to it can be reraised afterwards. + """ + exc_info = sys.exc_info() + e = exc_info[1] + + # SystemExit(0) is the only uncaught exception that is expected behavior + should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) + if not should_skip_capture: + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "wsgi", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class _ScopedResponse: + """ + Users a separate scope for each response chunk. + + This will make WSGI apps more tolerant against: + - WSGI servers streaming responses from a different thread/from + different threads than the one that called start_response + - close() not being called + - WSGI servers streaming responses interleaved from the same thread + """ + + __slots__ = ("_response", "_scope") + + def __init__( + self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" + ) -> None: + self._scope = scope + self._response = response + + def __iter__(self) -> "Iterator[bytes]": + iterator = iter(self._response) + + while True: + with use_isolation_scope(self._scope): + try: + chunk = next(iterator) + except StopIteration: + break + except BaseException: + reraise(*_capture_exception()) + + yield chunk + + def close(self) -> None: + with use_isolation_scope(self._scope): + try: + self._response.close() # type: ignore + except AttributeError: + pass + except BaseException: + reraise(*_capture_exception()) + + +def _make_wsgi_event_processor( + environ: "Dict[str, str]", use_x_forwarded_for: bool +) -> "EventProcessor": + # It's a bit unfortunate that we have to extract and parse the request data + # from the environ so eagerly, but there are a few good reasons for this. + # + # We might be in a situation where the scope never gets torn down + # properly. In that case we will have an unnecessary strong reference to + # all objects in the environ (some of which may take a lot of memory) when + # we're really just interested in a few of them. + # + # Keeping the environment around for longer than the request lifecycle is + # also not necessarily something uWSGI can deal with: + # https://github.com/unbit/uwsgi/issues/1950 + + client_ip = get_client_ip(environ) + request_url = get_request_url(environ, use_x_forwarded_for) + query_string = environ.get("QUERY_STRING") + method = environ.get("REQUEST_METHOD") + env = dict(_get_environ(environ)) + headers = _filter_headers(dict(_get_headers(environ))) + + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + # if the code below fails halfway through we at least have some data + request_info = event.setdefault("request", {}) + + if should_send_default_pii(): + user_info = event.setdefault("user", {}) + if client_ip: + user_info.setdefault("ip_address", client_ip) + + request_info["url"] = request_url + request_info["query_string"] = query_string + request_info["method"] = method + request_info["env"] = env + request_info["headers"] = headers + + return event + + return event_processor + + +def _get_request_attributes( + environ: "Dict[str, str]", + use_x_forwarded_for: bool = False, +) -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from the WSGI environ. + """ + attributes: "dict[str, Any]" = {} + + method = environ.get("REQUEST_METHOD") + if method: + attributes["http.request.method"] = method.upper() + + headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + url_scheme = environ.get("wsgi.url_scheme") + if url_scheme: + attributes["network.protocol.name"] = url_scheme + + server_name = environ.get("SERVER_NAME") + if server_name: + attributes["server.address"] = server_name + + server_port = environ.get("SERVER_PORT") + if server_port: + try: + attributes["server.port"] = int(server_port) + except ValueError: + pass + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + attributes["client.address"] = client_ip + + query_string = environ.get("QUERY_STRING") + if query_string: + attributes["http.query"] = query_string + + path = environ.get("PATH_INFO", "") + if path: + attributes["url.path"] = path + + attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py new file mode 100644 index 0000000000..d7f4425dfd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py @@ -0,0 +1,88 @@ +# NOTE: this is the logger sentry exposes to users, not some generic logger. +import functools +import time +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk.utils import capture_internal_exceptions, format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes + + +OTEL_RANGES = [ + # ((severity level range), severity text) + # https://opentelemetry.io/docs/specs/otel/logs/data-model + ((1, 4), "trace"), + ((5, 8), "debug"), + ((9, 12), "info"), + ((13, 16), "warn"), + ((17, 20), "error"), + ((21, 24), "fatal"), +] + + +class _dict_default_key(dict): # type: ignore[type-arg] + """dict that returns the key if missing.""" + + def __missing__(self, key: str) -> str: + return "{" + key + "}" + + +def _capture_log( + severity_text: str, severity_number: int, template: str, **kwargs: "Any" +) -> None: + body = template + + attributes: "Attributes" = {} + + if "attributes" in kwargs: + provided_attributes = kwargs.pop("attributes") or {} + for attribute, value in provided_attributes.items(): + attributes[attribute] = format_attribute(value) + + for k, v in kwargs.items(): + attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) + + if kwargs: + # only attach template if there are parameters + attributes["sentry.message.template"] = format_attribute(template) + + with capture_internal_exceptions(): + body = template.format_map(_dict_default_key(kwargs)) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": severity_text, + "severity_number": severity_number, + "attributes": attributes, + "body": body, + "time_unix_nano": time.time_ns(), + "trace_id": None, + "span_id": None, + } + ) + + +trace = functools.partial(_capture_log, "trace", 1) +debug = functools.partial(_capture_log, "debug", 5) +info = functools.partial(_capture_log, "info", 9) +warning = functools.partial(_capture_log, "warn", 13) +error = functools.partial(_capture_log, "error", 17) +fatal = functools.partial(_capture_log, "fatal", 21) + + +def _otel_severity_text(otel_severity_number: int) -> str: + for (lower, upper), severity in OTEL_RANGES: + if lower <= otel_severity_number <= upper: + return severity + + return "default" + + +def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": + for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): + if level >= py_level: + return otel_severity_number, _otel_severity_text(otel_severity_number) + + return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py new file mode 100644 index 0000000000..27b7468c0d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py @@ -0,0 +1,62 @@ +import time +from typing import TYPE_CHECKING, Any, Optional + +import sentry_sdk +from sentry_sdk.utils import format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes, Metric, MetricType + + +def _capture_metric( + name: str, + metric_type: "MetricType", + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[Attributes]" = None, +) -> None: + attrs: "Attributes" = {} + + if attributes: + for k, v in attributes.items(): + attrs[k] = format_attribute(v) + + metric: "Metric" = { + "timestamp": time.time(), + "trace_id": None, + "span_id": None, + "name": name, + "type": metric_type, + "value": float(value), + "unit": unit, + "attributes": attrs, + } + + sentry_sdk.get_current_scope()._capture_metric(metric) + + +def count( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "counter", value, unit, attributes) + + +def gauge( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "gauge", value, unit, attributes) + + +def distribution( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py new file mode 100644 index 0000000000..d2ba298c35 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py @@ -0,0 +1,138 @@ +import os +import time +import weakref +from threading import Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +MAX_DOWNSAMPLE_FACTOR = 10 + + +class Monitor: + """ + Performs health checks in a separate thread once every interval seconds + and updates the internal state. Other parts of the SDK only read this state + and act accordingly. + """ + + name = "sentry.monitor" + + _thread: "Optional[Thread]" + _thread_for_pid: "Optional[int]" + + def __init__( + self, transport: "sentry_sdk.transport.Transport", interval: float = 10 + ) -> None: + self.transport: "sentry_sdk.transport.Transport" = transport + self.interval: float = interval + + self._healthy = True + self._downsample_factor: int = 0 + self._running = True + self._reset_thread_state() + + # See https://github.com/getsentry/sentry-python/issues/6148. + # If os.fork() runs while another thread holds self._thread_lock, + # the child inherits the lock locked but the holding thread does + # not exist in the child, so the lock can never be released and + # _ensure_running deadlocks forever. Reinitialise the lock and + # cached thread/pid in the child so it starts clean regardless + # of inherited state. We bind via a WeakMethod so the + # permanently-registered fork handler does not pin this Monitor + # (and its Transport): register_at_fork has no unregister API. + # POSIX-only; Windows uses spawn. + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._thread = None + self._thread_lock = Lock() + self._thread_for_pid = None + + def _ensure_running(self) -> None: + """ + Check that the monitor has an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + while self._running: + time.sleep(self.interval) + if self._running: + self.run() + + thread = Thread(name=self.name, target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def run(self) -> None: + self.check_health() + self.set_downsample_factor() + + def set_downsample_factor(self) -> None: + if self._healthy: + if self._downsample_factor > 0: + logger.debug( + "[Monitor] health check positive, reverting to normal sampling" + ) + self._downsample_factor = 0 + else: + if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: + self._downsample_factor += 1 + logger.debug( + "[Monitor] health check negative, downsampling with a factor of %d", + self._downsample_factor, + ) + + def check_health(self) -> None: + """ + Perform the actual health checks, + currently only checks if the transport is rate-limited. + TODO: augment in the future with more checks. + """ + self._healthy = self.transport.is_healthy() + + def is_healthy(self) -> bool: + self._ensure_running() + return self._healthy + + @property + def downsample_factor(self) -> int: + self._ensure_running() + return self._downsample_factor + + def kill(self) -> None: + self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py new file mode 100644 index 0000000000..d562405295 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py @@ -0,0 +1,49 @@ +from sentry_sdk.profiler.continuous_profiler import ( + start_profile_session, + start_profiler, + stop_profile_session, + stop_profiler, +) +from sentry_sdk.profiler.transaction_profiler import ( + MAX_PROFILE_DURATION_NS, + PROFILE_MINIMUM_SAMPLES, + GeventScheduler, + Profile, + Scheduler, + ThreadScheduler, + has_profiling_enabled, + setup_profiler, + teardown_profiler, +) +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + MAX_STACK_DEPTH, + extract_frame, + extract_stack, + frame_id, + get_frame_name, +) + +__all__ = [ + "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` + "start_profiler", + "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` + "stop_profiler", + # DEPRECATED: The following was re-exported for backwards compatibility. It + # will be removed from sentry_sdk.profiler in a future release. + "MAX_PROFILE_DURATION_NS", + "PROFILE_MINIMUM_SAMPLES", + "Profile", + "Scheduler", + "ThreadScheduler", + "GeventScheduler", + "has_profiling_enabled", + "setup_profiler", + "teardown_profiler", + "DEFAULT_SAMPLING_FREQUENCY", + "MAX_STACK_DEPTH", + "get_frame_name", + "extract_frame", + "extract_stack", + "frame_id", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py new file mode 100644 index 0000000000..ed525f52bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py @@ -0,0 +1,703 @@ +import atexit +import os +import random +import sys +import threading +import time +import uuid +import warnings +from collections import deque +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.consts import VERSION +from sentry_sdk.envelope import Envelope +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + is_gevent, + logger, + now, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union + + from typing_extensions import TypedDict + + from sentry_sdk._types import ContinuousProfilerMode, SDKInfo + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "timestamp": float, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + ThreadPool = None + + +_scheduler: "Optional[ContinuousScheduler]" = None + + +def setup_continuous_profiler( + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", +) -> bool: + global _scheduler + + already_initialized = _scheduler is not None + + if already_initialized: + logger.debug("[Profiling] Continuous Profiler is already setup") + teardown_continuous_profiler() + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventContinuousScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventContinuousScheduler.mode + else: + default_profiler_mode = ThreadContinuousScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + # TODO: deprecate this and just use the existing `profiler_mode` + experiments = options.get("_experiments", {}) + + profiler_mode = ( + experiments.get("continuous_profiling_mode") or default_profiler_mode + ) + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if profiler_mode == ThreadContinuousScheduler.mode: + _scheduler = ThreadContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + elif profiler_mode == GeventContinuousScheduler.mode: + _scheduler = GeventContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + else: + raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up continuous profiler in {mode} mode".format( + mode=_scheduler.mode + ) + ) + + if not already_initialized: + atexit.register(teardown_continuous_profiler) + + return True + + +def is_profile_session_sampled() -> bool: + if _scheduler is None: + return False + return _scheduler.sampled + + +def try_autostart_continuous_profiler() -> None: + # TODO: deprecate this as it'll be replaced by the auto lifecycle option + + if _scheduler is None: + return + + if not _scheduler.is_auto_start_enabled(): + return + + _scheduler.manual_start() + + +def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": + if _scheduler is None: + return None + + return _scheduler.auto_start() + + +def start_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_start() + + +def start_profile_session() -> None: + warnings.warn( + "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + start_profiler() + + +def stop_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_stop() + + +def stop_profile_session() -> None: + warnings.warn( + "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + stop_profiler() + + +def teardown_continuous_profiler() -> None: + stop_profiler() + + global _scheduler + _scheduler = None + + +def get_profiler_id() -> "Union[str, None]": + if _scheduler is None: + return None + return _scheduler.profiler_id + + +def determine_profile_session_sampling_decision( + sample_rate: "Union[float, None]", +) -> bool: + # `None` is treated as `0.0` + if not sample_rate: + return False + + return random.random() < float(sample_rate) + + +class ContinuousProfile: + active: bool = True + + def stop(self) -> None: + self.active = False + + +class ContinuousScheduler: + mode: "ContinuousProfilerMode" = "unknown" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.interval = 1.0 / frequency + self.options = options + self.sdk_info = sdk_info + self.capture_func = capture_func + + self.lifecycle = self.options.get("profile_lifecycle") + profile_session_sample_rate = self.options.get("profile_session_sample_rate") + self.sampled = determine_profile_session_sampling_decision( + profile_session_sample_rate + ) + + self.sampler = self.make_sampler() + self.buffer: "Optional[ProfileBuffer]" = None + self.pid: "Optional[int]" = None + + self.running = False + self.soft_shutdown = False + + self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) + self.active_profiles: "Set[ContinuousProfile]" = set() + + def is_auto_start_enabled(self) -> bool: + # Ensure that the scheduler only autostarts once per process. + # This is necessary because many web servers use forks to spawn + # additional processes. And the profiler is only spawned on the + # master process, then it often only profiles the main process + # and not the ones where the requests are being handled. + if self.pid == os.getpid(): + return False + + experiments = self.options.get("_experiments") + if not experiments: + return False + + return experiments.get("continuous_profiling_auto_start") + + def auto_start(self) -> "Union[ContinuousProfile, None]": + if not self.sampled: + return None + + if self.lifecycle != "trace": + return None + + logger.debug("[Profiling] Auto starting profiler") + + profile = ContinuousProfile() + + self.new_profiles.append(profile) + self.ensure_running() + + return profile + + def manual_start(self) -> None: + if not self.sampled: + return + + if self.lifecycle != "manual": + return + + self.ensure_running() + + def manual_stop(self) -> None: + if self.lifecycle != "manual": + return + + self.teardown() + + def ensure_running(self) -> None: + raise NotImplementedError + + def teardown(self) -> None: + raise NotImplementedError + + def pause(self) -> None: + raise NotImplementedError + + def reset_buffer(self) -> None: + self.buffer = ProfileBuffer( + self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func + ) + + @property + def profiler_id(self) -> "Union[str, None]": + if not self.running or self.buffer is None: + return None + return self.buffer.profiler_id + + def make_sampler(self) -> "Callable[..., bool]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + if self.lifecycle == "trace": + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + return True + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + inactive_profiles = [] + + for profile in self.active_profiles: + if not profile.active: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + else: + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + return _sample_stack + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.soft_shutdown = self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # the soft shutdown happens here to give it a chance + # for the profiler to be reused + if self.soft_shutdown: + self.running = False + + # make sure to explicitly exit the profiler here or there might + # be multiple profilers at once + break + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + buffer = self.buffer + if buffer is not None: + buffer.flush() + + +class ThreadContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ContinuousProfilerMode" = "thread" + name = "sentry.profiler.ThreadContinuousScheduler" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[threading.Thread]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +class GeventContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ContinuousProfilerMode" = "gevent" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[_ThreadPool]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +PROFILE_BUFFER_SECONDS = 60 + + +class ProfileBuffer: + def __init__( + self, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + buffer_size: int, + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.options = options + self.sdk_info = sdk_info + self.buffer_size = buffer_size + self.capture_func = capture_func + + self.profiler_id = uuid.uuid4().hex + self.chunk = ProfileChunk() + + # Make sure to use the same clock to compute a sample's monotonic timestamp + # to ensure the timestamps are correctly aligned. + self.start_monotonic_time = now() + + # Make sure the start timestamp is defined only once per profiler id. + # This prevents issues with clock drift within a single profiler session. + # + # Subtracting the start_monotonic_time here to find a fixed starting position + # for relative monotonic timestamps for each sample. + self.start_timestamp = ( + datetime.now(timezone.utc).timestamp() - self.start_monotonic_time + ) + + def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: + if self.should_flush(monotonic_time): + self.flush() + self.chunk = ProfileChunk() + self.start_monotonic_time = now() + + self.chunk.write(self.start_timestamp + monotonic_time, sample) + + def should_flush(self, monotonic_time: float) -> bool: + # If the delta between the new monotonic time and the start monotonic time + # exceeds the buffer size, it means we should flush the chunk + return monotonic_time - self.start_monotonic_time >= self.buffer_size + + def flush(self) -> None: + chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) + envelope = Envelope() + envelope.add_profile_chunk(chunk) + self.capture_func(envelope) + + +class ProfileChunk: + def __init__(self) -> None: + self.chunk_id = uuid.uuid4().hex + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + def write(self, ts: float, sample: "ExtractedSample") -> None: + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "timestamp": ts, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def to_json( + self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" + ) -> "Dict[str, Any]": + profile = { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + }, + } + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + payload = { + "chunk_id": self.chunk_id, + "client_sdk": { + "name": sdk_info["name"], + "version": VERSION, + }, + "platform": "python", + "profile": profile, + "profiler_id": profiler_id, + "version": "2", + } + + for key in "release", "environment", "dist": + if options[key] is not None: + payload[key] = str(options[key]).strip() + + return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py new file mode 100644 index 0000000000..fe65774c0b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py @@ -0,0 +1,802 @@ +""" +This file is originally based on code from https://github.com/nylas/nylas-perftools, +which is published under the following license: + +The MIT License (MIT) + +Copyright (c) 2014 Nylas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import atexit +import os +import platform +import random +import sys +import threading +import time +import uuid +import warnings +from abc import ABC, abstractmethod +from collections import deque +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + capture_internal_exceptions, + get_current_thread_meta, + is_gevent, + is_valid_sample_rate, + logger, + nanosecond_time, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type + + from typing_extensions import TypedDict + + from sentry_sdk._types import Event, ProfilerMode, SamplingContext + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + ProcessedThreadMetadata, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "elapsed_since_start_ns": str, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + ProcessedProfile = TypedDict( + "ProcessedProfile", + { + "frames": List[ProcessedFrame], + "stacks": List[ProcessedStack], + "samples": List[ProcessedSample], + "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + + ThreadPool = None + + +_scheduler: "Optional[Scheduler]" = None + + +# The minimum number of unique samples that must exist in a profile to be +# considered valid. +PROFILE_MINIMUM_SAMPLES = 2 + + +def has_profiling_enabled(options: "Dict[str, Any]") -> bool: + profiles_sampler = options["profiles_sampler"] + if profiles_sampler is not None: + return True + + profiles_sample_rate = options["profiles_sample_rate"] + if profiles_sample_rate is not None and profiles_sample_rate > 0: + return True + + profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") + if profiles_sample_rate is not None: + logger.warning( + "_experiments['profiles_sample_rate'] is deprecated. " + "Please use the non-experimental profiles_sample_rate option " + "directly." + ) + if profiles_sample_rate > 0: + return True + + return False + + +def setup_profiler(options: "Dict[str, Any]") -> bool: + global _scheduler + + if _scheduler is not None: + logger.debug("[Profiling] Profiler is already setup") + return False + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventScheduler.mode + else: + default_profiler_mode = ThreadScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + profiler_mode = options.get("_experiments", {}).get("profiler_mode") + if profiler_mode is not None: + logger.warning( + "_experiments['profiler_mode'] is deprecated. Please use the " + "non-experimental profiler_mode option directly." + ) + profiler_mode = profiler_mode or default_profiler_mode + + if ( + profiler_mode == ThreadScheduler.mode + # for legacy reasons, we'll keep supporting sleep mode for this scheduler + or profiler_mode == "sleep" + ): + _scheduler = ThreadScheduler(frequency=frequency) + elif profiler_mode == GeventScheduler.mode: + _scheduler = GeventScheduler(frequency=frequency) + else: + raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) + ) + _scheduler.setup() + + atexit.register(teardown_profiler) + + return True + + +def teardown_profiler() -> None: + global _scheduler + + if _scheduler is not None: + _scheduler.teardown() + + _scheduler = None + + +MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds + + +class Profile: + def __init__( + self, + sampled: "Optional[bool]", + start_ns: int, + hub: "Optional[sentry_sdk.Hub]" = None, + scheduler: "Optional[Scheduler]" = None, + ) -> None: + self.scheduler = _scheduler if scheduler is None else scheduler + + self.event_id: str = uuid.uuid4().hex + + self.sampled: "Optional[bool]" = sampled + + # Various framework integrations are capable of overwriting the active thread id. + # If it is set to `None` at the end of the profile, we fall back to the default. + self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 + self.active_thread_id: "Optional[int]" = None + + try: + self.start_ns: int = start_ns + except AttributeError: + self.start_ns = 0 + + self.stop_ns: int = 0 + self.active: bool = False + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + self.unique_samples = 0 + + # Backwards compatibility with the old hub property + self._hub: "Optional[sentry_sdk.Hub]" = None + if hub is not None: + self._hub = hub + warnings.warn( + "The `hub` parameter is deprecated. Please do not use it.", + DeprecationWarning, + stacklevel=2, + ) + + def update_active_thread_id(self) -> None: + self.active_thread_id = get_current_thread_meta()[0] + logger.debug( + "[Profiling] updating active thread id to {tid}".format( + tid=self.active_thread_id + ) + ) + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the profile's sampling decision according to the following + precedence rules: + + 1. If the transaction to be profiled is not sampled, that decision + will be used, regardless of anything else. + + 2. Use `profiles_sample_rate` to decide. + """ + + # The corresponding transaction was not sampled, + # so don't generate a profile for it. + if not self.sampled: + logger.debug( + "[Profiling] Discarding profile because transaction is discarded." + ) + self.sampled = False + return + + # The profiler hasn't been properly initialized. + if self.scheduler is None: + logger.debug( + "[Profiling] Discarding profile because profiler was not started." + ) + self.sampled = False + return + + client = sentry_sdk.get_client() + if not client.is_active(): + self.sampled = False + return + + options = client.options + + if callable(options.get("profiles_sampler")): + sample_rate = options["profiles_sampler"](sampling_context) + elif options["profiles_sample_rate"] is not None: + sample_rate = options["profiles_sample_rate"] + else: + sample_rate = options["_experiments"].get("profiles_sample_rate") + + # The profiles_sample_rate option was not set, so profiling + # was never enabled. + if sample_rate is None: + logger.debug( + "[Profiling] Discarding profile because profiling was not enabled." + ) + self.sampled = False + return + + if not is_valid_sample_rate(sample_rate, source="Profiling"): + logger.warning( + "[Profiling] Discarding profile because of invalid sample rate." + ) + self.sampled = False + return + + # Now we roll the dice. random.random is inclusive of 0, but not of 1, + # so strict < is safe here. In case sample_rate is a boolean, cast it + # to a float (True becomes 1.0 and False becomes 0.0) + self.sampled = random.random() < float(sample_rate) + + if self.sampled: + logger.debug("[Profiling] Initializing profile") + else: + logger.debug( + "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( + sample_rate=float(sample_rate) + ) + ) + + def start(self) -> None: + if not self.sampled or self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Starting profile") + self.active = True + if not self.start_ns: + self.start_ns = nanosecond_time() + self.scheduler.start_profiling(self) + + def stop(self) -> None: + if not self.sampled or not self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Stopping profile") + self.active = False + self.stop_ns = nanosecond_time() + + def __enter__(self) -> "Profile": + scope = sentry_sdk.get_isolation_scope() + old_profile = scope.profile + scope.profile = self + + self._context_manager_state = (scope, old_profile) + + self.start() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + with capture_internal_exceptions(): + self.stop() + + scope, old_profile = self._context_manager_state + del self._context_manager_state + + scope.profile = old_profile + + def write(self, ts: int, sample: "ExtractedSample") -> None: + if not self.active: + return + + if ts < self.start_ns: + return + + offset = ts - self.start_ns + if offset > MAX_PROFILE_DURATION_NS: + self.stop() + return + + self.unique_samples += 1 + + elapsed_since_start_ns = str(offset) + + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "elapsed_since_start_ns": elapsed_since_start_ns, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def process(self) -> "ProcessedProfile": + # This collects the thread metadata at the end of a profile. Doing it + # this way means that any threads that terminate before the profile ends + # will not have any metadata associated with it. + thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + } + + return { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": thread_metadata, + } + + def to_json( + self, event_opt: "Event", options: "Dict[str, Any]" + ) -> "Dict[str, Any]": + profile = self.process() + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + return { + "environment": event_opt.get("environment"), + "event_id": self.event_id, + "platform": "python", + "profile": profile, + "release": event_opt.get("release", ""), + "timestamp": event_opt["start_timestamp"], + "version": "1", + "device": { + "architecture": platform.machine(), + }, + "os": { + "name": platform.system(), + "version": platform.release(), + }, + "runtime": { + "name": platform.python_implementation(), + "version": platform.python_version(), + }, + "transactions": [ + { + "id": event_opt["event_id"], + "name": event_opt["transaction"], + # we start the transaction before the profile and this is + # the transaction start time relative to the profile, so we + # hardcode it to 0 until we can start the profile before + "relative_start_ns": "0", + # use the duration of the profile instead of the transaction + # because we end the transaction after the profile + "relative_end_ns": str(self.stop_ns - self.start_ns), + "trace_id": event_opt["contexts"]["trace"]["trace_id"], + "active_thread_id": str( + self._default_active_thread_id + if self.active_thread_id is None + else self.active_thread_id + ), + } + ], + } + + def valid(self) -> bool: + client = sentry_sdk.get_client() + if not client.is_active(): + return False + + if not has_profiling_enabled(client.options): + return False + + if self.sampled is None or not self.sampled: + if client.transport: + client.transport.record_lost_event( + "sample_rate", data_category="profile" + ) + return False + + if self.unique_samples < PROFILE_MINIMUM_SAMPLES: + if client.transport: + client.transport.record_lost_event( + "insufficient_data", data_category="profile" + ) + logger.debug("[Profiling] Discarding profile because insufficient samples.") + return False + + return True + + @property + def hub(self) -> "Optional[sentry_sdk.Hub]": + warnings.warn( + "The `hub` attribute is deprecated. Please do not access it.", + DeprecationWarning, + stacklevel=2, + ) + return self._hub + + @hub.setter + def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: + warnings.warn( + "The `hub` attribute is deprecated. Please do not set it.", + DeprecationWarning, + stacklevel=2, + ) + self._hub = value + + +class Scheduler(ABC): + mode: "ProfilerMode" = "unknown" + + def __init__(self, frequency: int) -> None: + self.interval = 1.0 / frequency + + self.sampler = self.make_sampler() + + # cap the number of new profiles at any time so it does not grow infinitely + self.new_profiles: "Deque[Profile]" = deque(maxlen=128) + self.active_profiles: "Set[Profile]" = set() + + def __enter__(self) -> "Scheduler": + self.setup() + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self.teardown() + + @abstractmethod + def setup(self) -> None: + pass + + @abstractmethod + def teardown(self) -> None: + pass + + def ensure_running(self) -> None: + """ + Ensure the scheduler is running. By default, this method is a no-op. + The method should be overridden by any implementation for which it is + relevant. + """ + return None + + def start_profiling(self, profile: "Profile") -> None: + self.ensure_running() + self.new_profiles.append(profile) + + def make_sampler(self) -> "Callable[..., None]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + def _sample_stack(*args: "Any", **kwargs: "Any") -> None: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + # make sure to clear the cache if we're not profiling so we dont + # keep a reference to the last stack of frames around + return + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + now = nanosecond_time() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + + inactive_profiles = [] + + for profile in self.active_profiles: + if profile.active: + profile.write(now, sample) + else: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + return _sample_stack + + +class ThreadScheduler(Scheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ProfilerMode" = "thread" + name = "sentry.profiler.ThreadScheduler" + + def __init__(self, frequency: int) -> None: + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[threading.Thread]" = None + self.pid: "Optional[int]" = None + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + """ + Check that the profiler has an active thread to run in, and start one if + that's not the case. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self.running + will be False after running this function. + """ + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + +class GeventScheduler(Scheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ProfilerMode" = "gevent" + name = "sentry.profiler.GeventScheduler" + + def __init__(self, frequency: int) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[_ThreadPool]" = None + self.pid: "Optional[int]" = None + + # This intentionally uses the gevent patched threading.Lock. + # The lock will be required when first trying to start profiles + # as we need to spawn the profiler thread from the greenlets. + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py new file mode 100644 index 0000000000..e9f0fec07f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py @@ -0,0 +1,186 @@ +import os +from collections import deque +from typing import TYPE_CHECKING + +from sentry_sdk._compat import PY311 +from sentry_sdk.utils import filename_for_module + +if TYPE_CHECKING: + from types import FrameType + from typing import Deque, List, Optional, Sequence, Tuple + + from typing_extensions import TypedDict + + from sentry_sdk._lru_cache import LRUCache + + ThreadId = str + + ProcessedStack = List[int] + + ProcessedFrame = TypedDict( + "ProcessedFrame", + { + "abs_path": str, + "filename": Optional[str], + "function": str, + "lineno": int, + "module": Optional[str], + }, + ) + + ProcessedThreadMetadata = TypedDict( + "ProcessedThreadMetadata", + {"name": str}, + ) + + FrameId = Tuple[ + str, # abs_path + int, # lineno + str, # function + ] + FrameIds = Tuple[FrameId, ...] + + # The exact value of this id is not very meaningful. The purpose + # of this id is to give us a compact and unique identifier for a + # raw stack that can be used as a key to a dictionary so that it + # can be used during the sampled format generation. + StackId = Tuple[int, int] + + ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] + ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] + +# The default sampling frequency to use. This is set at 101 in order to +# mitigate the effects of lockstep sampling. +DEFAULT_SAMPLING_FREQUENCY = 101 + + +# We want to impose a stack depth limit so that samples aren't too large. +MAX_STACK_DEPTH = 128 + + +if PY311: + + def get_frame_name(frame: "FrameType") -> str: + return frame.f_code.co_qualname + +else: + + def get_frame_name(frame: "FrameType") -> str: + f_code = frame.f_code + co_varnames = f_code.co_varnames + + # co_name only contains the frame name. If the frame was a method, + # the class name will NOT be included. + name = f_code.co_name + + # if it was a method, we can get the class name by inspecting + # the f_locals for the `self` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `self` if its an instance method + co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals + ): + for cls in type(frame.f_locals["self"]).__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # if it was a class method, (decorated with `@classmethod`) + # we can get the class name by inspecting the f_locals for the `cls` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `cls` if its a class method + co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals + ): + for cls in frame.f_locals["cls"].__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # nothing we can do if it is a staticmethod (decorated with @staticmethod) + + # we've done all we can, time to give up and return what we have + return name + + +def frame_id(raw_frame: "FrameType") -> "FrameId": + return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) + + +def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": + abs_path = raw_frame.f_code.co_filename + + try: + module = raw_frame.f_globals["__name__"] + except Exception: + module = None + + # namedtuples can be many times slower when initialing + # and accessing attribute so we opt to use a tuple here instead + return { + # This originally was `os.path.abspath(abs_path)` but that had + # a large performance overhead. + # + # According to docs, this is equivalent to + # `os.path.normpath(os.path.join(os.getcwd(), path))`. + # The `os.getcwd()` call is slow here, so we precompute it. + # + # Additionally, since we are using normalized path already, + # we skip calling `os.path.normpath` entirely. + "abs_path": os.path.join(cwd, abs_path), + "module": module, + "filename": filename_for_module(module, abs_path) or None, + "function": fid[2], + "lineno": raw_frame.f_lineno, + } + + +def extract_stack( + raw_frame: "Optional[FrameType]", + cache: "LRUCache", + cwd: str, + max_stack_depth: int = MAX_STACK_DEPTH, +) -> "ExtractedStack": + """ + Extracts the stack starting the specified frame. The extracted stack + assumes the specified frame is the top of the stack, and works back + to the bottom of the stack. + + In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, + only the first `MAX_STACK_DEPTH` frames will be returned. + """ + + raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) + + while raw_frame is not None: + f_back = raw_frame.f_back + raw_frames.append(raw_frame) + raw_frame = f_back + + frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) + frames = [] + for i, fid in enumerate(frame_ids): + frame = cache.get(fid) + if frame is None: + frame = extract_frame(fid, raw_frames[i], cwd) + cache.set(fid, frame) + frames.append(frame) + + # Instead of mapping the stack into frame ids and hashing + # that as a tuple, we can directly hash the stack. + # This saves us from having to generate yet another list. + # Additionally, using the stack as the key directly is + # costly because the stack can be large, so we pre-hash + # the stack, and use the hash as the key as this will be + # needed a few times to improve performance. + # + # To Reduce the likelihood of hash collisions, we include + # the stack depth. This means that only stacks of the same + # depth can suffer from hash collisions. + stack_id = len(raw_frames), hash(frame_ids) + + return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py new file mode 100644 index 0000000000..4fd22714cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py @@ -0,0 +1,2185 @@ +import os +import platform +import sys +import warnings +from collections import deque +from contextlib import contextmanager +from copy import copy, deepcopy +from datetime import datetime, timezone +from enum import Enum +from functools import wraps +from itertools import chain +from typing import TYPE_CHECKING, cast + +import sentry_sdk +from sentry_sdk._types import AnnotatedValue +from sentry_sdk.attachments import Attachment +from sentry_sdk.consts import ( + DEFAULT_MAX_BREADCRUMBS, + FALSE_VALUES, + INSTRUMENTER, + SPANDATA, +) +from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.profiler.transaction_profiler import Profile +from sentry_sdk.session import Session +from sentry_sdk.traces import ( + _DEFAULT_PARENT_SPAN, + NoOpStreamedSpan, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.tracing_utils import ( + Baggage, + PropagationContext, + _make_sampling_decision, + has_span_streaming_enabled, + has_tracing_enabled, + is_ignored_span, +) +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exception, + capture_internal_exceptions, + datetime_from_isoformat, + disable_capture_event, + event_from_exception, + exc_info_from_error, + format_attribute, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + Deque, + Dict, + Generator, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + ) + + from typing_extensions import Unpack + + import sentry_sdk + from sentry_sdk._types import ( + Attributes, + AttributeValue, + Breadcrumb, + BreadcrumbHint, + ErrorProcessor, + Event, + EventProcessor, + ExcInfo, + Hint, + Log, + LogLevelStr, + Metric, + SamplingContext, + Type, + ) + from sentry_sdk.tracing import TransactionKwargs + + P = ParamSpec("P") + R = TypeVar("R") + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T") + + +# Holds data that will be added to **all** events sent by this process. +# In case this is a http server (think web framework) with multiple users +# the data will be added to events of all users. +# Typically this is used for process wide data such as the release. +_global_scope: "Optional[Scope]" = None + +# Holds data for the active request. +# This is used to isolate data for different requests or users. +# The isolation scope is usually created by integrations, but may also +# be created manually +_isolation_scope = ContextVar("isolation_scope", default=None) + +# Holds data for the active span. +# This can be used to manually add additional data to a span. +_current_scope = ContextVar("current_scope", default=None) + +global_event_processors: "List[EventProcessor]" = [] + +# A function returning a (trace_id, span_id) tuple +# from an external tracing source (such as otel) +_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None + + +class ScopeType(Enum): + CURRENT = "current" + ISOLATION = "isolation" + GLOBAL = "global" + MERGED = "merged" + + +class _ScopeManager: + def __init__(self, hub: "Optional[Any]" = None) -> None: + self._old_scopes: "List[Scope]" = [] + + def __enter__(self) -> "Scope": + isolation_scope = Scope.get_isolation_scope() + + self._old_scopes.append(isolation_scope) + + forked_scope = isolation_scope.fork() + _isolation_scope.set(forked_scope) + + return forked_scope + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + old_scope = self._old_scopes.pop() + _isolation_scope.set(old_scope) + + +def add_global_event_processor(processor: "EventProcessor") -> None: + global_event_processors.append(processor) + + +def register_external_propagation_context( + fn: "Callable[[], Optional[Tuple[str, str]]]", +) -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = fn + + +def remove_external_propagation_context() -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = None + + +def get_external_propagation_context() -> "Optional[Tuple[str, str]]": + return ( + _external_propagation_context_fn() if _external_propagation_context_fn else None + ) + + +def has_external_propagation_context() -> bool: + return _external_propagation_context_fn is not None + + +def _attr_setter(fn: "Any") -> "Any": + return property(fset=fn, doc=fn.__doc__) + + +def _disable_capture(fn: "F") -> "F": + @wraps(fn) + def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": + if not self._should_capture: + return + try: + self._should_capture = False + return fn(self, *args, **kwargs) + finally: + self._should_capture = True + + return wrapper # type: ignore + + +class Scope: + """The scope holds extra information that should be sent with all + events that belong to it. + """ + + # NOTE: Even though it should not happen, the scope needs to not crash when + # accessed by multiple threads. It's fine if it's full of races, but those + # races should never make the user application crash. + # + # The same needs to hold for any accesses of the scope the SDK makes. + + __slots__ = ( + "_level", + "_name", + "_fingerprint", + # note that for legacy reasons, _transaction is the transaction *name*, + # not a Transaction object (the object is stored in _span) + "_transaction", + "_transaction_info", + "_user", + "_tags", + "_contexts", + "_extras", + "_breadcrumbs", + "_n_breadcrumbs_truncated", + "_gen_ai_original_message_count", + "_gen_ai_conversation_id", + "_event_processors", + "_error_processors", + "_should_capture", + "_span", + "_session", + "_attachments", + "_force_auto_session_tracking", + "_profile", + "_propagation_context", + "client", + "_type", + "_last_event_id", + "_flags", + "_attributes", + ) + + def __init__( + self, + ty: "Optional[ScopeType]" = None, + client: "Optional[sentry_sdk.Client]" = None, + ) -> None: + self._type = ty + + self._event_processors: "List[EventProcessor]" = [] + self._error_processors: "List[ErrorProcessor]" = [] + + self._name: "Optional[str]" = None + self._propagation_context: "Optional[PropagationContext]" = None + self._n_breadcrumbs_truncated: int = 0 + self._gen_ai_original_message_count: "Dict[str, int]" = {} + + self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() + + if client is not None: + self.set_client(client) + + self.clear() + + incoming_trace_information = self._load_trace_data_from_env() + self.generate_propagation_context(incoming_data=incoming_trace_information) + + def __copy__(self) -> "Scope": + """ + Returns a copy of this scope. + This also creates a copy of all referenced data structures. + """ + rv: "Scope" = object.__new__(self.__class__) + + rv._type = self._type + rv.client = self.client + rv._level = self._level + rv._name = self._name + rv._fingerprint = self._fingerprint + rv._transaction = self._transaction + rv._transaction_info = self._transaction_info.copy() + rv._user = self._user + + rv._tags = self._tags.copy() + rv._contexts = self._contexts.copy() + rv._extras = self._extras.copy() + + rv._breadcrumbs = copy(self._breadcrumbs) + rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated + rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() + rv._event_processors = self._event_processors.copy() + rv._error_processors = self._error_processors.copy() + rv._propagation_context = self._propagation_context + + rv._should_capture = self._should_capture + rv._span = self._span + rv._session = self._session + rv._force_auto_session_tracking = self._force_auto_session_tracking + rv._attachments = self._attachments.copy() + + rv._profile = self._profile + + rv._last_event_id = self._last_event_id + + rv._flags = deepcopy(self._flags) + + rv._attributes = self._attributes.copy() + + rv._gen_ai_conversation_id = self._gen_ai_conversation_id + + return rv + + @classmethod + def get_current_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the current scope. + """ + current_scope = _current_scope.get() + if current_scope is None: + current_scope = Scope(ty=ScopeType.CURRENT) + _current_scope.set(current_scope) + + return current_scope + + @classmethod + def set_current_scope(cls, new_current_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new current scope overwriting the existing current scope. + :param new_current_scope: The scope to set as the new current scope. + """ + _current_scope.set(new_current_scope) + + @classmethod + def get_isolation_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the isolation scope. + """ + isolation_scope = _isolation_scope.get() + if isolation_scope is None: + isolation_scope = Scope(ty=ScopeType.ISOLATION) + _isolation_scope.set(isolation_scope) + + return isolation_scope + + @classmethod + def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new isolation scope overwriting the existing isolation scope. + :param new_isolation_scope: The scope to set as the new isolation scope. + """ + _isolation_scope.set(new_isolation_scope) + + @classmethod + def get_global_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the global scope. + """ + global _global_scope + if _global_scope is None: + _global_scope = Scope(ty=ScopeType.GLOBAL) + + return _global_scope + + def set_global_attributes(self) -> None: + from sentry_sdk.client import SDK_INFO + + self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) + self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) + + self.set_attribute( + "process.runtime.name", + platform.python_implementation(), + ) + self.set_attribute( + "process.runtime.version", + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + ) + + options = sentry_sdk.get_client().options + + server_name = options.get("server_name") + if server_name: + self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) + + environment = options.get("environment") + if environment: + self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) + + release = options.get("release") + if release: + self.set_attribute(SPANDATA.SENTRY_RELEASE, release) + + @classmethod + def last_event_id(cls) -> "Optional[str]": + """ + .. versionadded:: 2.2.0 + + Returns event ID of the event most recently captured by the isolation scope, or None if no event + has been captured. We do not consider events that are dropped, e.g. by a before_send hook. + Transactions also are not considered events in this context. + + The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; + whether the event is sent depends on the transport. The event could be sent later or not at all. + Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or + various other reasons. + """ + return cls.get_isolation_scope()._last_event_id + + def _merge_scopes( + self, + additional_scope: "Optional[Scope]" = None, + additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, + ) -> "Scope": + """ + Merges global, isolation and current scope into a new scope and + adds the given additional scope or additional scope kwargs to it. + """ + if additional_scope and additional_scope_kwargs: + raise TypeError("cannot provide scope and kwargs") + + final_scope = copy(_global_scope) if _global_scope is not None else Scope() + final_scope._type = ScopeType.MERGED + + isolation_scope = _isolation_scope.get() + if isolation_scope is not None: + final_scope.update_from_scope(isolation_scope) + + current_scope = _current_scope.get() + if current_scope is not None: + final_scope.update_from_scope(current_scope) + + if self != current_scope and self != isolation_scope: + final_scope.update_from_scope(self) + + if additional_scope is not None: + if callable(additional_scope): + additional_scope(final_scope) + else: + final_scope.update_from_scope(additional_scope) + + elif additional_scope_kwargs: + final_scope.update_from_kwargs(**additional_scope_kwargs) + + return final_scope + + @classmethod + def get_client(cls) -> "sentry_sdk.client.BaseClient": + """ + .. versionadded:: 2.0.0 + + Returns the currently used :py:class:`sentry_sdk.Client`. + This checks the current scope, the isolation scope and the global scope for a client. + If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. + """ + current_scope = _current_scope.get() + try: + client = current_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + isolation_scope = _isolation_scope.get() + try: + client = isolation_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + try: + client = _global_scope.client # type: ignore + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + return NonRecordingClient() + + def set_client( + self, client: "Optional[sentry_sdk.client.BaseClient]" = None + ) -> None: + """ + .. versionadded:: 2.0.0 + + Sets the client for this scope. + + :param client: The client to use in this scope. + If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. + + """ + if client is not None: + self.client = client + # We need a client to set the initial global attributes on the global + # scope since they mostly come from client options, so populate them + # as soon as a client is set + sentry_sdk.get_global_scope().set_global_attributes() + else: + self.client = NonRecordingClient() + + def fork(self) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns a fork of this scope. + """ + forked_scope = copy(self) + return forked_scope + + def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": + """ + Load Sentry trace id and baggage from environment variables. + Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". + """ + incoming_trace_information: "Optional[Dict[str, str]]" = None + + sentry_use_environment = ( + os.environ.get("SENTRY_USE_ENVIRONMENT") or "" + ).lower() + use_environment = sentry_use_environment not in FALSE_VALUES + if use_environment: + incoming_trace_information = {} + + if os.environ.get("SENTRY_TRACE"): + incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( + os.environ.get("SENTRY_TRACE") or "" + ) + + if os.environ.get("SENTRY_BAGGAGE"): + incoming_trace_information[BAGGAGE_HEADER_NAME] = ( + os.environ.get("SENTRY_BAGGAGE") or "" + ) + + return incoming_trace_information or None + + def set_new_propagation_context(self) -> None: + """ + Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. + """ + self._propagation_context = PropagationContext() + + def generate_propagation_context( + self, incoming_data: "Optional[Dict[str, str]]" = None + ) -> None: + """ + Makes sure the propagation context is set on the scope. + If there is `incoming_data` overwrite existing propagation context. + If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. + """ + if incoming_data is not None: + self._propagation_context = PropagationContext.from_incoming_data( + incoming_data + ) + + # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring + if self._type != ScopeType.CURRENT: + if self._propagation_context is None: + self.set_new_propagation_context() + + def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": + """ + Returns the Dynamic Sampling Context from the Propagation Context. + If not existing, creates a new one. + + Deprecated: Logic moved to PropagationContext, don't use directly. + """ + if self._propagation_context is None: + return None + + return self._propagation_context.dynamic_sampling_context + + def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + """ + Returns the Sentry "sentry-trace" header (aka the traceparent) from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().to_traceparent() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return traceparent from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_traceparent() + elif not span_streaming and self.span is not None: + return self.span._to_traceparent() + + # else return traceparent from the propagation context + return self.get_active_propagation_context().to_traceparent() + + def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": + """ + Returns the Sentry "baggage" header containing trace information from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().get_baggage() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return baggage from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_baggage() + elif not span_streaming and self.span is not None: + return self.span._to_baggage() + + # else return baggage from the propagation context + return self.get_active_propagation_context().get_baggage() + + def get_trace_context(self) -> "Dict[str, Any]": + """ + Returns the Sentry "trace" context from the Propagation Context. + """ + if ( + has_tracing_enabled(self.get_client().options) + and self._span is not None + and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) + ): + return self._span._get_trace_context() + + # if we are tracing externally (otel), those values take precedence + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + trace_id, span_id = external_propagation_context + return {"trace_id": trace_id, "span_id": span_id} + + propagation_context = self.get_active_propagation_context() + + return { + "trace_id": propagation_context.trace_id, + "span_id": propagation_context.span_id, + "parent_span_id": propagation_context.parent_span_id, + "dynamic_sampling_context": propagation_context.dynamic_sampling_context, + } + + def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: + """ + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + span = kwargs.pop("span", None) + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + meta = "" + + for name, content in self.iter_trace_propagation_headers(): + meta += f'' + + return meta + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. + Deprecated: use PropagationContext.iter_headers instead. + """ + if self._propagation_context is not None: + yield from self._propagation_context.iter_headers() + + def iter_trace_propagation_headers( + self, *args: "Any", **kwargs: "Any" + ) -> "Generator[Tuple[str, str], None, None]": + """ + Return HTTP headers which allow propagation of trace data. + + If a span is given, the trace data will taken from the span. + If no span is given, the trace data is taken from the scope. + """ + client = self.get_client() + if not client.options.get("propagate_traces"): + warnings.warn( + "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", + DeprecationWarning, + stacklevel=2, + ) + return + + span = kwargs.pop("span", None) + if not span: + span_streaming = has_span_streaming_enabled(client.options) + span = self.streamed_span if span_streaming else self.span + + if ( + has_tracing_enabled(client.options) + and span is not None + and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) + ): + for header in span._iter_headers(): + yield header + elif has_external_propagation_context(): + # when we have an external_propagation_context (otlp) + # we leave outgoing propagation to the propagator + return + else: + for header in self.get_active_propagation_context().iter_headers(): + yield header + + def get_active_propagation_context(self) -> "PropagationContext": + if self._propagation_context is not None: + return self._propagation_context + + current_scope = self.get_current_scope() + if current_scope._propagation_context is not None: + return current_scope._propagation_context + + isolation_scope = self.get_isolation_scope() + # should actually never happen, but just in case someone calls scope.clear + if isolation_scope._propagation_context is None: + isolation_scope._propagation_context = PropagationContext() + return isolation_scope._propagation_context + + @classmethod + def set_custom_sampling_context( + cls, custom_sampling_context: "dict[str, Any]" + ) -> None: + cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( + custom_sampling_context + ) + + def clear(self) -> None: + """Clears the entire scope.""" + self._level: "Optional[LogLevelStr]" = None + self._fingerprint: "Optional[List[str]]" = None + self._transaction: "Optional[str]" = None + self._transaction_info: "dict[str, str]" = {} + self._user: "Optional[Dict[str, Any]]" = None + + self._tags: "Dict[str, Any]" = {} + self._contexts: "Dict[str, Dict[str, Any]]" = {} + self._extras: "dict[str, Any]" = {} + self._attachments: "List[Attachment]" = [] + + self.clear_breadcrumbs() + self._should_capture: bool = True + + self._span: "Optional[Union[Span, StreamedSpan]]" = None + self._session: "Optional[Session]" = None + self._force_auto_session_tracking: "Optional[bool]" = None + + self._profile: "Optional[Profile]" = None + + self._propagation_context = None + + # self._last_event_id is only applicable to isolation scopes + self._last_event_id: "Optional[str]" = None + self._flags: "Optional[FlagBuffer]" = None + + self._attributes: "Attributes" = {} + + self._gen_ai_conversation_id: "Optional[str]" = None + + @_attr_setter + def level(self, value: "LogLevelStr") -> None: + """ + When set this overrides the level. + + .. deprecated:: 1.0.0 + Use :func:`set_level` instead. + + :param value: The level to set. + """ + logger.warning( + "Deprecated: use .set_level() instead. This will be removed in the future." + ) + + self._level = value + + def set_level(self, value: "LogLevelStr") -> None: + """ + Sets the level for the scope. + + :param value: The level to set. + """ + self._level = value + + @_attr_setter + def fingerprint(self, value: "Optional[List[str]]") -> None: + """When set this overrides the default fingerprint.""" + self._fingerprint = value + + @property + def transaction(self) -> "Any": + # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 + """Return the transaction (root span) in the scope, if any.""" + + # there is no span/transaction on the scope + if self._span is None: + return None + + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + # there is an orphan span on the scope + if self._span.containing_transaction is None: + return None + + # there is either a transaction (which is its own containing + # transaction) or a non-orphan span on the scope + return self._span.containing_transaction + + @transaction.setter + def transaction(self, value: "Any") -> None: + # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 + """When set this forces a specific transaction name to be set. + + Deprecated: use set_transaction_name instead.""" + + # XXX: the docstring above is misleading. The implementation of + # apply_to_event prefers an existing value of event.transaction over + # anything set in the scope. + # XXX: note that with the introduction of the Scope.transaction getter, + # there is a semantic and type mismatch between getter and setter. The + # getter returns a Transaction, the setter sets a transaction name. + # Without breaking version compatibility, we could make the setter set a + # transaction name or transaction (self._span) depending on the type of + # the value argument. + + logger.warning( + "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." + ) + self._transaction = value + if self._span: + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + if self._span.containing_transaction: + self._span.containing_transaction.name = value + + def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: + """Set the transaction name and optionally the transaction source.""" + self._transaction = name + if self._span: + if isinstance(self._span, NoOpStreamedSpan): + return + + elif isinstance(self._span, StreamedSpan): + self._span._segment.name = name + if source: + self._span._segment.set_attribute( + "sentry.span.source", getattr(source, "value", source) + ) + + elif self._span.containing_transaction: + self._span.containing_transaction.name = name + if source: + self._span.containing_transaction.source = source + + if source: + self._transaction_info["source"] = source + + @_attr_setter + def user(self, value: "Optional[Dict[str, Any]]") -> None: + """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" + warnings.warn( + "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", + DeprecationWarning, + stacklevel=2, + ) + self.set_user(value) + + def set_user(self, value: "Optional[Dict[str, Any]]") -> None: + """Sets a user for the scope.""" + self._user = value + + session = self.get_isolation_scope()._session + if session is not None: + session.update(user=value) + + @property + def span(self) -> "Optional[Span]": + """Get/set current tracing span or transaction.""" + return self._span if isinstance(self._span, Span) else None + + @span.setter + def span(self, span: "Optional[Span]") -> None: + self._span = span + # XXX: this differs from the implementation in JS, there Scope.setSpan + # does not set Scope._transactionName. + if isinstance(span, Transaction): + transaction = span + if transaction.name: + self._transaction = transaction.name + if transaction.source: + self._transaction_info["source"] = transaction.source + + @property + def streamed_span(self) -> "Optional[StreamedSpan]": + """Get/set current tracing span.""" + return self._span if isinstance(self._span, StreamedSpan) else None + + @streamed_span.setter + def streamed_span(self, span: "Optional[StreamedSpan]") -> None: + self._span = span + + # Also set _transaction and _transaction_info in streaming mode as this + # is used for populating events and linking them to segments + if type(span) is StreamedSpan and span._is_segment(): + self._transaction = span.name + if span._attributes.get("sentry.span.source"): + self._transaction_info["source"] = str( + span._attributes["sentry.span.source"] + ) + + @property + def profile(self) -> "Optional[Profile]": + return self._profile + + @profile.setter + def profile(self, profile: "Optional[Profile]") -> None: + self._profile = profile + + def set_tag(self, key: str, value: "Any") -> None: + """ + Sets a tag for a key to a specific value. + + :param key: Key of the tag to set. + + :param value: Value of the tag to set. + """ + self._tags[key] = value + + def set_tags(self, tags: "Mapping[str, object]") -> None: + """Sets multiple tags at once. + + This method updates multiple tags at once. The tags are passed as a dictionary + or other mapping type. + + Calling this method is equivalent to calling `set_tag` on each key-value pair + in the mapping. If a tag key already exists in the scope, its value will be + updated. If the tag key does not exist in the scope, the key-value pair will + be added to the scope. + + This method only modifies tag keys in the `tags` mapping passed to the method. + `scope.set_tags({})` is, therefore, a no-op. + + :param tags: A mapping of tag keys to tag values to set. + """ + self._tags.update(tags) + + def remove_tag(self, key: str) -> None: + """ + Removes a specific tag. + + :param key: Key of the tag to remove. + """ + self._tags.pop(key, None) + + def set_context( + self, + key: str, + value: "Dict[str, Any]", + ) -> None: + """ + Binds a context at a certain key to a specific value. + """ + self._contexts[key] = value + + def remove_context( + self, + key: str, + ) -> None: + """Removes a context.""" + self._contexts.pop(key, None) + + def set_extra( + self, + key: str, + value: "Any", + ) -> None: + """Sets an extra key to a specific value.""" + self._extras[key] = value + + def remove_extra( + self, + key: str, + ) -> None: + """Removes a specific extra key.""" + self._extras.pop(key, None) + + def set_conversation_id(self, conversation_id: str) -> None: + """ + Sets the conversation ID for gen_ai spans. + + :param conversation_id: The conversation ID to set. + """ + self._gen_ai_conversation_id = conversation_id + + def get_conversation_id(self) -> "Optional[str]": + """ + Gets the conversation ID for gen_ai spans. + + :returns: The conversation ID, or None if not set. + """ + return self._gen_ai_conversation_id + + def remove_conversation_id(self) -> None: + """Removes the conversation ID.""" + self._gen_ai_conversation_id = None + + def clear_breadcrumbs(self) -> None: + """Clears breadcrumb buffer.""" + self._breadcrumbs: "Deque[Breadcrumb]" = deque() + self._n_breadcrumbs_truncated = 0 + + def add_attachment( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + """Adds an attachment to future events sent from this scope. + + The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. + """ + self._attachments.append( + Attachment( + bytes=bytes, + path=path, + filename=filename, + content_type=content_type, + add_to_transactions=add_to_transactions, + ) + ) + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + client = self.get_client() + + if not client.is_active(): + logger.info("Dropped breadcrumb because no client bound") + return + + before_breadcrumb = client.options.get("before_breadcrumb") + max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) + + crumb = dict(crumb or ()) + crumb.update(kwargs) + if not crumb: + return + + hint = dict(hint or ()) + + if crumb.get("timestamp") is None: + crumb["timestamp"] = datetime.now(timezone.utc) + if crumb.get("type") is None: + crumb["type"] = "default" + + if before_breadcrumb is not None: + new_crumb = before_breadcrumb(crumb, hint) + else: + new_crumb = crumb + + if new_crumb is not None: + self._breadcrumbs.append(new_crumb) + else: + logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) + + while len(self._breadcrumbs) > max_breadcrumbs: + self._breadcrumbs.popleft() + self._n_breadcrumbs_truncated += 1 + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + try_autostart_continuous_profiler() + + custom_sampling_context = custom_sampling_context or {} + + # kwargs at this point has type TransactionKwargs, since we have removed + # the client and custom_sampling_context from it. + transaction_kwargs: "TransactionKwargs" = kwargs + + # if we haven't been given a transaction, make one + if transaction is None: + transaction = Transaction(**transaction_kwargs) + + # use traces_sample_rate, traces_sampler, and/or inheritance to make a + # sampling decision + sampling_context = { + "transaction_context": transaction.to_json(), + "parent_sampled": transaction.parent_sampled, + } + sampling_context.update(custom_sampling_context) + transaction._set_initial_sampling_decision(sampling_context=sampling_context) + + # update the sample rate in the dsc + if transaction.sample_rate is not None: + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction._baggage: + transaction._baggage.sentry_items["sample_rate"] = str( + transaction.sample_rate + ) + + if transaction.sampled: + profile = Profile( + transaction.sampled, transaction._start_timestamp_monotonic_ns + ) + profile._set_initial_sampling_decision(sampling_context=sampling_context) + + transaction._profile = profile + + transaction._continuous_profile = try_profile_lifecycle_trace_start() + + # Typically, the profiler is set when the transaction is created. But when + # using the auto lifecycle, the profiler isn't running when the first + # transaction is started. So make sure we update the profiler id on it. + if transaction._continuous_profile is not None: + transaction.set_profiler_id(get_profiler_id()) + + # we don't bother to keep spans if we already know we're not going to + # send the transaction + max_spans = (client.options["_experiments"].get("max_spans")) or 1000 + transaction.init_span_recorder(maxlen=max_spans) + + return transaction + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + warnings.warn( + "Scope.start_span is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return NoOpSpan() + + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + with new_scope(): + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + # get current span or transaction + span = self.span or self.get_isolation_scope().span + if isinstance(span, StreamedSpan): + # make mypy happy + return NoOpSpan() + + if span is None: + # New spans get the `trace_id` from the scope + if "trace_id" not in kwargs: + propagation_context = self.get_active_propagation_context() + kwargs["trace_id"] = propagation_context.trace_id + + span = Span(**kwargs) + else: + # Children take `trace_id`` from the parent span. + span = span.start_child(**kwargs) + + return span + + def start_streamed_span( + self, + name: str, + attributes: "Optional[Attributes]", + parent_span: "Optional[StreamedSpan]", + active: bool, + ) -> "StreamedSpan": + # TODO: rename to start_span once we drop the old API + if isinstance(parent_span, NoOpStreamedSpan): + # parent_span is only set if the user explicitly set it + logger.debug( + "Ignored parent span provided. Span will be parented to the " + "currently active span instead." + ) + + if parent_span is _DEFAULT_PARENT_SPAN or isinstance( + parent_span, NoOpStreamedSpan + ): + parent_span = self.streamed_span + + # If no eligible parent_span was provided and there is no currently + # active span, this is a segment + if parent_span is None: + propagation_context = self.get_active_propagation_context() + + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + scope=self, + unsampled_reason="ignored", + ) + + sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( + name, + attributes, + self, + ) + + if sample_rate is not None: + self._update_sample_rate(sample_rate) + + if sampled is False: + return NoOpStreamedSpan( + scope=self, + unsampled_reason=outcome, + ) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=None, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + parent_sampled=propagation_context.parent_sampled, + baggage=propagation_context.baggage, + sample_rand=sample_rand, + sample_rate=sample_rate, + ) + + # This is a child span; take propagation context from the parent span + with new_scope(): + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + unsampled_reason="ignored", + ) + + if isinstance(parent_span, NoOpStreamedSpan): + return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=parent_span._segment, + trace_id=parent_span.trace_id, + parent_span_id=parent_span.span_id, + parent_sampled=parent_span.sampled, + ) + + def _update_sample_rate(self, sample_rate: float) -> None: + # If we had to adjust the sample rate when setting the sampling decision + # for a span, it needs to be updated in the propagation context too + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(sample_rate) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", + ) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + self.generate_propagation_context(environ_or_headers) + + # generate_propagation_context ensures that the propagation_context is not None. + propagation_context = cast(PropagationContext, self._propagation_context) + + optional_kwargs = {} + if name: + optional_kwargs["name"] = name + if source: + optional_kwargs["source"] = source + + return Transaction( + op=op, + origin=origin, + baggage=propagation_context.baggage, + parent_sampled=propagation_context.parent_sampled, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + same_process_as_parent=False, + **optional_kwargs, + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures an event. + + Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + scope = self._merge_scopes(scope, scope_kwargs) + + event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) + + if event_id is not None and event.get("type") != "transaction": + self.get_isolation_scope()._last_event_id = event_id + + return event_id + + def _capture_log(self, log: "Optional[Log]") -> None: + if log is None: + return + + client = self.get_client() + if not has_logs_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" + ) + + client._capture_log(log, scope=merged_scope) + + def _capture_metric(self, metric: "Optional[Metric]") -> None: + if metric is None: + return + + client = self.get_client() + if not has_metrics_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" + ) + + client._capture_metric(metric, scope=merged_scope) + + def _capture_span(self, span: "Optional[StreamedSpan]") -> None: + if span is None: + return + + client = self.get_client() + if not has_span_streaming_enabled(client.options): + return + + merged_scope = self._merge_scopes() + client._capture_span(span, scope=merged_scope) + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures a message. + + :param message: The string to send as the message. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if level is None: + level = "info" + + event: "Event" = { + "message": message, + "level": level, + } + + return self.capture_event(event, scope=scope, **scope_kwargs) + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """Captures an exception. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if error is not None: + exc_info = exc_info_from_error(error) + else: + exc_info = sys.exc_info() + + event, hint = event_from_exception( + exc_info, client_options=self.get_client().options + ) + + try: + return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) + except Exception: + capture_internal_exception(sys.exc_info()) + + return None + + def start_session(self, *args: "Any", **kwargs: "Any") -> None: + """Starts a new session.""" + session_mode = kwargs.pop("session_mode", "application") + + self.end_session() + + client = self.get_client() + self._session = Session( + release=client.options.get("release"), + environment=client.options.get("environment"), + user=self._user, + session_mode=session_mode, + ) + + def end_session(self, *args: "Any", **kwargs: "Any") -> None: + """Ends the current session if there is one.""" + session = self._session + self._session = None + + if session is not None: + session.close() + self.get_client().capture_session(session) + + def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: + """Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + self.end_session() + self._force_auto_session_tracking = False + + def resume_auto_session_tracking(self) -> None: + """Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + self._force_auto_session_tracking = None + + def add_event_processor( + self, + func: "EventProcessor", + ) -> None: + """Register a scope local event processor on the scope. + + :param func: This function behaves like `before_send.` + """ + if len(self._event_processors) > 20: + logger.warning( + "Too many event processors on scope! Clearing list to free up some memory: %r", + self._event_processors, + ) + del self._event_processors[:] + + self._event_processors.append(func) + + def add_error_processor( + self, + func: "ErrorProcessor", + cls: "Optional[Type[BaseException]]" = None, + ) -> None: + """Register a scope local error processor on the scope. + + :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. + + :param cls: Optionally, only process exceptions of this type. + """ + if cls is not None: + cls_ = cls # For mypy. + real_func = func + + def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": + try: + is_inst = isinstance(exc_info[1], cls_) + except Exception: + is_inst = False + if is_inst: + return real_func(event, exc_info) + return event + + self._error_processors.append(func) + + def _apply_level_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._level is not None: + event["level"] = self._level + + def _apply_breadcrumbs_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + event.setdefault("breadcrumbs", {}) + + # This check is just for mypy - + if not isinstance(event["breadcrumbs"], AnnotatedValue): + event["breadcrumbs"].setdefault("values", []) + event["breadcrumbs"]["values"].extend(self._breadcrumbs) + + # Attempt to sort timestamps + try: + if not isinstance(event["breadcrumbs"], AnnotatedValue): + for crumb in event["breadcrumbs"]["values"]: + if isinstance(crumb["timestamp"], str): + crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) + + event["breadcrumbs"]["values"].sort( + key=lambda crumb: crumb["timestamp"] + ) + except Exception as err: + logger.debug("Error when sorting breadcrumbs", exc_info=err) + pass + + def _apply_user_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("user") is None and self._user is not None: + event["user"] = self._user + + def _apply_transaction_name_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction") is None and self._transaction is not None: + event["transaction"] = self._transaction + + def _apply_transaction_info_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction_info") is None and self._transaction_info is not None: + event["transaction_info"] = self._transaction_info + + def _apply_fingerprint_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("fingerprint") is None and self._fingerprint is not None: + event["fingerprint"] = self._fingerprint + + def _apply_extra_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._extras: + event.setdefault("extra", {}).update(self._extras) + + def _apply_tags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._tags: + event.setdefault("tags", {}).update(self._tags) + + def _apply_contexts_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._contexts: + event.setdefault("contexts", {}).update(self._contexts) + + contexts = event.setdefault("contexts", {}) + + # Add "trace" context + if contexts.get("trace") is None: + contexts["trace"] = self.get_trace_context() + + def _apply_flags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + flags = self.flags.get() + if len(flags) > 0: + event.setdefault("contexts", {}).setdefault("flags", {}).update( + {"values": flags} + ) + + def _apply_scope_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + # TODO: turn Logs, Metrics into actual classes + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + for attribute, value in self._attributes.items(): + if attribute not in attributes: + attributes[attribute] = value + + def _apply_user_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + if not should_send_default_pii() or self._user is None: + return + + for attribute_name, user_attribute in ( + ("user.id", "id"), + ("user.name", "username"), + ("user.email", "email"), + ("user.ip_address", "ip_address"), + ): + if ( + user_attribute in self._user + and attribute_name not in attributes + and self._user[user_attribute] is not None + ): + attributes[attribute_name] = self._user[user_attribute] + + def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": + logger.info("%s (%s) dropped event", ty, cause) + return None + + def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the error processors on the event and returns the modified event. + """ + exc_info = hint.get("exc_info") + if exc_info is not None: + error_processors = chain( + self.get_global_scope()._error_processors, + self.get_isolation_scope()._error_processors, + self.get_current_scope()._error_processors, + ) + + for error_processor in error_processors: + new_event = error_processor(event, exc_info) + if new_event is None: + return self._drop(error_processor, "error processor") + + event = new_event + + return event + + def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the event processors on the event and returns the modified event. + """ + ty = event.get("type") + is_check_in = ty == "check_in" + + if not is_check_in: + # Get scopes without creating them to prevent infinite recursion + isolation_scope = _isolation_scope.get() + current_scope = _current_scope.get() + + event_processors = chain( + global_event_processors, + _global_scope and _global_scope._event_processors or [], + isolation_scope and isolation_scope._event_processors or [], + current_scope and current_scope._event_processors or [], + ) + + for event_processor in event_processors: + new_event = event + with capture_internal_exceptions(): + new_event = event_processor(event, hint) + if new_event is None: + return self._drop(event_processor, "event processor") + event = new_event + + return event + + @_disable_capture + def apply_to_event( + self, + event: "Event", + hint: "Hint", + options: "Optional[Dict[str, Any]]" = None, + ) -> "Optional[Event]": + """Applies the information contained on the scope to the given event.""" + ty = event.get("type") + is_transaction = ty == "transaction" + is_check_in = ty == "check_in" + + # put all attachments into the hint. This lets callbacks play around + # with attachments. We also later pull this out of the hint when we + # create the envelope. + attachments_to_send = hint.get("attachments") or [] + for attachment in self._attachments: + if not is_transaction or attachment.add_to_transactions: + attachments_to_send.append(attachment) + hint["attachments"] = attachments_to_send + + self._apply_contexts_to_event(event, hint, options) + + if is_check_in: + # Check-ins only support the trace context, strip all others + event["contexts"] = { + "trace": event.setdefault("contexts", {}).get("trace", {}) + } + + if not is_check_in: + self._apply_level_to_event(event, hint, options) + self._apply_fingerprint_to_event(event, hint, options) + self._apply_user_to_event(event, hint, options) + self._apply_transaction_name_to_event(event, hint, options) + self._apply_transaction_info_to_event(event, hint, options) + self._apply_tags_to_event(event, hint, options) + self._apply_extra_to_event(event, hint, options) + + if not is_transaction and not is_check_in: + self._apply_breadcrumbs_to_event(event, hint, options) + self._apply_flags_to_event(event, hint, options) + + event = self.run_error_processors(event, hint) + if event is None: + return None + + event = self.run_event_processors(event, hint) + if event is None: + return None + + return event + + @_disable_capture + def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: + # Attributes-based events and telemetry go through here (logs, metrics, + # spansV2) + if not isinstance(telemetry, StreamedSpan): + trace_context = self.get_trace_context() + trace_id = trace_context.get("trace_id") + if telemetry.get("trace_id") is None and trace_id is not None: + telemetry["trace_id"] = trace_id + + # span_id should only be populated if there's an active span. We can't + # use the trace_context here because it synthesizes a span_id if there + # isn't one + if telemetry.get("span_id") is None: + if self._span is not None and not isinstance( + self._span, (NoOpStreamedSpan, NoOpSpan) + ): + telemetry["span_id"] = self._span.span_id + else: + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + _, span_id = external_propagation_context + if span_id is not None: + telemetry["span_id"] = span_id + + self._apply_scope_attributes_to_telemetry(telemetry) + self._apply_user_attributes_to_telemetry(telemetry) + + def update_from_scope(self, scope: "Scope") -> None: + """Update the scope with another scope's data.""" + if scope._level is not None: + self._level = scope._level + if scope._fingerprint is not None: + self._fingerprint = scope._fingerprint + if scope._transaction is not None: + self._transaction = scope._transaction + if scope._transaction_info is not None: + self._transaction_info.update(scope._transaction_info) + if scope._user is not None: + self._user = scope._user + if scope._tags: + self._tags.update(scope._tags) + if scope._contexts: + self._contexts.update(scope._contexts) + if scope._extras: + self._extras.update(scope._extras) + if scope._breadcrumbs: + self._breadcrumbs.extend(scope._breadcrumbs) + if scope._n_breadcrumbs_truncated: + self._n_breadcrumbs_truncated = ( + self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated + ) + if scope._gen_ai_original_message_count: + self._gen_ai_original_message_count.update( + scope._gen_ai_original_message_count + ) + if scope._gen_ai_conversation_id: + self._gen_ai_conversation_id = scope._gen_ai_conversation_id + if scope._span: + self._span = scope._span + if scope._attachments: + self._attachments.extend(scope._attachments) + if scope._profile: + self._profile = scope._profile + if scope._propagation_context: + self._propagation_context = scope._propagation_context + if scope._session: + self._session = scope._session + if scope._flags: + if not self._flags: + self._flags = deepcopy(scope._flags) + else: + for flag in scope._flags.get(): + self._flags.set(flag["flag"], flag["result"]) + if scope._attributes: + self._attributes.update(scope._attributes) + + def update_from_kwargs( + self, + user: "Optional[Any]" = None, + level: "Optional[LogLevelStr]" = None, + extras: "Optional[Dict[str, Any]]" = None, + contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, + tags: "Optional[Dict[str, str]]" = None, + fingerprint: "Optional[List[str]]" = None, + attributes: "Optional[Attributes]" = None, + ) -> None: + """Update the scope's attributes.""" + if level is not None: + self._level = level + if user is not None: + self._user = user + if extras is not None: + self._extras.update(extras) + if contexts is not None: + self._contexts.update(contexts) + if tags is not None: + self._tags.update(tags) + if fingerprint is not None: + self._fingerprint = fingerprint + if attributes is not None: + self._attributes.update(attributes) + + def __repr__(self) -> str: + return "<%s id=%s name=%s type=%s>" % ( + self.__class__.__name__, + hex(id(self)), + self._name, + self._type, + ) + + @property + def flags(self) -> "FlagBuffer": + if self._flags is None: + max_flags = ( + self.get_client().options["_experiments"].get("max_flags") + or DEFAULT_FLAG_CAPACITY + ) + self._flags = FlagBuffer(capacity=max_flags) + return self._flags + + def set_attribute(self, attribute: str, value: "AttributeValue") -> None: + """ + Set an attribute on the scope. + + Any attributes-based telemetry (logs, metrics) captured while this scope + is active will inherit attributes set on the scope. + """ + self._attributes[attribute] = format_attribute(value) + + def remove_attribute(self, attribute: str) -> None: + """Remove an attribute if set on the scope. No-op if there is no such attribute.""" + try: + del self._attributes[attribute] + except KeyError: + pass + + +@contextmanager +def new_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current scope and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.new_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + new_scope = current_scope.fork() + token = _current_scope.set(new_scope) + + try: + yield new_scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `scope` and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + Suppose the variable `scope` contains a `Scope` object, which is not currently + the active scope. + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.use_scope(scope): + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # set given scope as current scope + token = _current_scope.set(scope) + + try: + yield scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def isolation_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current isolation scope and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # fork isolation scope + isolation_scope = Scope.get_isolation_scope() + new_isolation_scope = isolation_scope.fork() + isolation_token = _isolation_scope.set(new_isolation_scope) + + try: + yield new_isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `isolation_scope` and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # set given scope as isolation scope + isolation_token = _isolation_scope.set(isolation_scope) + + try: + yield isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +def should_send_default_pii() -> bool: + """Shortcut for `Scope.get_client().should_send_default_pii()`.""" + return Scope.get_client().should_send_default_pii() + + +# Circular imports +from sentry_sdk.client import NonRecordingClient + +if TYPE_CHECKING: + import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py new file mode 100644 index 0000000000..6794491325 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING, Dict, List, cast + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + iter_event_frames, +) + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event + + +DEFAULT_DENYLIST = [ + # stolen from relay + "password", + "passwd", + "secret", + "api_key", + "apikey", + "auth", + "credentials", + "mysql_pwd", + "privatekey", + "private_key", + "token", + "session", + # django + "csrftoken", + "sessionid", + # wsgi + "x_csrftoken", + "x_forwarded_for", + "set_cookie", + "cookie", + "authorization", + "proxy-authorization", + "x_api_key", + # other common names used in the wild + "aiohttp_session", # aiohttp + "connect.sid", # Express + "csrf_token", # Pyramid + "csrf", # (this is a cookie name used in accepted answers on stack overflow) + "_csrf", # Express + "_csrf_token", # Bottle + "PHPSESSID", # PHP + "_session", # Sanic + "symfony", # Symfony + "user_session", # Vue + "_xsrf", # Tornado + "XSRF-TOKEN", # Angular, Laravel +] + +DEFAULT_PII_DENYLIST = [ + "x_forwarded_for", + "x_real_ip", + "ip_address", + "remote_addr", +] + + +class EventScrubber: + def __init__( + self, + denylist: "Optional[List[str]]" = None, + recursive: bool = False, + send_default_pii: bool = False, + pii_denylist: "Optional[List[str]]" = None, + ) -> None: + """ + A scrubber that goes through the event payload and removes sensitive data configured through denylists. + + :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. + :param recursive: Whether to scrub the event payload recursively, default False. + :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. + :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. + """ + self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist + + if not send_default_pii: + pii_denylist = ( + DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist + ) + self.denylist += pii_denylist + + self.denylist = [x.lower() for x in self.denylist] + self.recursive = recursive + + def scrub_list(self, lst: object) -> None: + """ + If a list is passed to this method, the method recursively searches the list and any + nested lists for any dictionaries. The method calls scrub_dict on all dictionaries + it finds. + If the parameter passed to this method is not a list, the method does nothing. + """ + if not isinstance(lst, list): + return + + for v in lst: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_dict(self, d: object) -> None: + """ + If a dictionary is passed to this method, the method scrubs the dictionary of any + sensitive data. The method calls itself recursively on any nested dictionaries ( + including dictionaries nested in lists) if self.recursive is True. + This method does nothing if the parameter passed to it is not a dictionary. + """ + if not isinstance(d, dict): + return + + for k, v in d.items(): + # The cast is needed because mypy is not smart enough to figure out that k must be a + # string after the isinstance check. + if isinstance(k, str) and k.lower() in self.denylist: + d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() + elif self.recursive: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_request(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "request" in event: + if "headers" in event["request"]: + self.scrub_dict(event["request"]["headers"]) + if "cookies" in event["request"]: + self.scrub_dict(event["request"]["cookies"]) + if "data" in event["request"]: + self.scrub_dict(event["request"]["data"]) + + def scrub_extra(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "extra" in event: + self.scrub_dict(event["extra"]) + + def scrub_user(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "user" in event: + user = event["user"] + if "ip_address" in self.denylist and isinstance(user, dict): + user.pop("ip_address", None) + self.scrub_dict(user) + + def scrub_breadcrumbs(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "breadcrumbs" in event: + if ( + not isinstance(event["breadcrumbs"], AnnotatedValue) + and "values" in event["breadcrumbs"] + ): + for value in event["breadcrumbs"]["values"]: + if "data" in value: + self.scrub_dict(value["data"]) + + def scrub_frames(self, event: "Event") -> None: + with capture_internal_exceptions(): + for frame in iter_event_frames(event): + if "vars" in frame: + self.scrub_dict(frame["vars"]) + + def scrub_spans(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "spans" in event: + for span in cast(List[Dict[str, object]], event["spans"]): + if "data" in span: + self.scrub_dict(span["data"]) + + def scrub_event(self, event: "Event") -> None: + self.scrub_request(event) + self.scrub_extra(event) + self.scrub_user(event) + self.scrub_breadcrumbs(event) + self.scrub_frames(event) + self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py new file mode 100644 index 0000000000..6bf6f6e70b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py @@ -0,0 +1,418 @@ +import math +import sys +from array import array +from collections.abc import Mapping +from datetime import datetime +from typing import TYPE_CHECKING + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exception, + disable_capture_event, + format_timestamp, + safe_repr, + strip_string, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union + + from sentry_sdk._types import NotImplementedType + + Span = Dict[str, Any] + + ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] + Segment = Union[str, int] + + +# Bytes are technically not strings in Python 3, but we can serialize them +serializable_str_types = (str, bytes, bytearray, memoryview) + + +# Maximum length of JSON-serialized event payloads that can be safely sent +# before the server may reject the event due to its size. This is not intended +# to reflect actual values defined server-side, but rather only be an upper +# bound for events sent by the SDK. +# +# Can be overwritten if wanting to send more bytes, e.g. with a custom server. +# When changing this, keep in mind that events may be a little bit larger than +# this value due to attached metadata, so keep the number conservative. +MAX_EVENT_BYTES = 10**6 + +# Maximum depth and breadth of databags. Excess data will be trimmed. If +# max_request_body_size is "always", request bodies won't be trimmed. +MAX_DATABAG_DEPTH = 5 +MAX_DATABAG_BREADTH = 10 +CYCLE_MARKER = "" + + +global_repr_processors: "List[ReprProcessor]" = [] + + +def add_global_repr_processor(processor: "ReprProcessor") -> None: + global_repr_processors.append(processor) + + +sequence_types: "List[type]" = [tuple, list, set, frozenset, array] + + +def add_repr_sequence_type(ty: type) -> None: + sequence_types.append(ty) + + +class Memo: + __slots__ = ("_ids", "_objs") + + def __init__(self) -> None: + self._ids: "Dict[int, Any]" = {} + self._objs: "List[Any]" = [] + + def memoize(self, obj: "Any") -> "ContextManager[bool]": + self._objs.append(obj) + return self + + def __enter__(self) -> bool: + obj = self._objs[-1] + if id(obj) in self._ids: + return True + else: + self._ids[id(obj)] = obj + return False + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> None: + self._ids.pop(id(self._objs.pop()), None) + + +class _Serializer: + """Holds the state of a single serialize() call.""" + + __slots__ = ( + "memo", + "path", + "meta_stack", + "keep_request_bodies", + "max_value_length", + "is_vars", + "custom_repr", + ) + + def __init__( + self, + keep_request_bodies: bool, + max_value_length: "Optional[int]", + is_vars: bool, + custom_repr: "Optional[Callable[..., Optional[str]]]", + ) -> None: + self.memo = Memo() + self.path: "List[Segment]" = [] + self.meta_stack: "List[Dict[str, Any]]" = [] + self.keep_request_bodies = keep_request_bodies + self.max_value_length = max_value_length + self.is_vars = is_vars + self.custom_repr = custom_repr + + def _safe_repr_wrapper(self, value: "Any") -> str: + try: + repr_value = None + if self.custom_repr is not None: + repr_value = self.custom_repr(value) + return repr_value or safe_repr(value) + except Exception: + return safe_repr(value) + + def _annotate(self, **meta: "Any") -> None: + while len(self.meta_stack) <= len(self.path): + try: + segment = self.path[len(self.meta_stack) - 1] + node = self.meta_stack[-1].setdefault(str(segment), {}) + except IndexError: + node = {} + + self.meta_stack.append(node) + + self.meta_stack[-1].setdefault("", {}).update(meta) + + def _is_databag(self) -> "Optional[bool]": + """ + A databag is any value that we need to trim. + True for stuff like vars, request bodies, breadcrumbs and extra. + + :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". + """ + try: + if self.is_vars: + return True + + is_request_body = self._is_request_body() + if is_request_body in (True, None): + return is_request_body + + p0 = self.path[0] + if p0 == "breadcrumbs" and self.path[1] == "values": + self.path[2] + return True + + if p0 == "extra": + return True + + except IndexError: + return None + + return False + + def _is_span_attribute(self) -> "Optional[bool]": + try: + if self.path[0] == "spans" and self.path[2] == "data": + return True + except IndexError: + return None + + return False + + def _is_request_body(self) -> "Optional[bool]": + try: + if self.path[0] == "request" and self.path[1] == "data": + return True + except IndexError: + return None + + return False + + def _serialize_node( + self, + obj: "Any", + is_databag: "Optional[bool]" = None, + is_request_body: "Optional[bool]" = None, + should_repr_strings: "Optional[bool]" = None, + segment: "Optional[Segment]" = None, + remaining_breadth: "Optional[Union[int, float]]" = None, + remaining_depth: "Optional[Union[int, float]]" = None, + ) -> "Any": + if segment is not None: + self.path.append(segment) + + try: + with self.memo.memoize(obj) as result: + if result: + return CYCLE_MARKER + + return self._serialize_node_impl( + obj, + is_databag=is_databag, + is_request_body=is_request_body, + should_repr_strings=should_repr_strings, + remaining_depth=remaining_depth, + remaining_breadth=remaining_breadth, + ) + except BaseException: + capture_internal_exception(sys.exc_info()) + + if is_databag: + return "" + + return None + finally: + if segment is not None: + self.path.pop() + del self.meta_stack[len(self.path) + 1 :] + + def _flatten_annotated(self, obj: "Any") -> "Any": + if isinstance(obj, AnnotatedValue): + self._annotate(**obj.metadata) + obj = obj.value + return obj + + def _serialize_node_impl( + self, + obj: "Any", + is_databag: "Optional[bool]", + is_request_body: "Optional[bool]", + should_repr_strings: "Optional[bool]", + remaining_depth: "Optional[Union[float, int]]", + remaining_breadth: "Optional[Union[float, int]]", + ) -> "Any": + if isinstance(obj, AnnotatedValue): + should_repr_strings = False + if should_repr_strings is None: + should_repr_strings = self.is_vars + + if is_databag is None: + is_databag = self._is_databag() + + if is_request_body is None: + is_request_body = self._is_request_body() + + if is_databag: + if is_request_body and self.keep_request_bodies: + remaining_depth = float("inf") + remaining_breadth = float("inf") + else: + if remaining_depth is None: + remaining_depth = MAX_DATABAG_DEPTH + if remaining_breadth is None: + remaining_breadth = MAX_DATABAG_BREADTH + + obj = self._flatten_annotated(obj) + + if remaining_depth is not None and remaining_depth <= 0: + self._annotate(rem=[["!limit", "x"]]) + if is_databag: + return self._flatten_annotated( + strip_string( + self._safe_repr_wrapper(obj), max_length=self.max_value_length + ) + ) + return None + + is_span_attribute = self._is_span_attribute() + if (is_databag or is_span_attribute) and global_repr_processors: + hints = {"memo": self.memo, "remaining_depth": remaining_depth} + for processor in global_repr_processors: + result = processor(obj, hints) + if result is not NotImplemented: + return self._flatten_annotated(result) + + sentry_repr = getattr(type(obj), "__sentry_repr__", None) + + if obj is None or isinstance(obj, (bool, int, float)): + if should_repr_strings or ( + isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) + ): + return self._safe_repr_wrapper(obj) + else: + return obj + + elif callable(sentry_repr): + return sentry_repr(obj) + + elif isinstance(obj, datetime): + return ( + str(format_timestamp(obj)) + if not should_repr_strings + else self._safe_repr_wrapper(obj) + ) + + elif isinstance(obj, Mapping): + # Create temporary copy here to avoid calling too much code that + # might mutate our dictionary while we're still iterating over it. + obj = dict(obj.items()) + + rv_dict: "Dict[str, Any]" = {} + i = 0 + + for k, v in obj.items(): + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) + break + + str_k = str(k) + v = self._serialize_node( + v, + segment=str_k, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + rv_dict[str_k] = v + i += 1 + + return rv_dict + + elif not isinstance(obj, serializable_str_types) and isinstance( + obj, tuple(sequence_types) + ): + rv_list = [] + + for i, v in enumerate(obj): # type: ignore[arg-type] + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) # type: ignore[arg-type] + break + + rv_list.append( + self._serialize_node( + v, + segment=i, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + ) + + return rv_list + + if should_repr_strings: + obj = self._safe_repr_wrapper(obj) + else: + if isinstance(obj, bytes) or isinstance(obj, bytearray): + obj = obj.decode("utf-8", "replace") + + if not isinstance(obj, str): + obj = self._safe_repr_wrapper(obj) + + is_span_description = ( + len(self.path) == 3 + and self.path[0] == "spans" + and self.path[-1] == "description" + ) + if is_span_description: + return obj + + return self._flatten_annotated( + strip_string(obj, max_length=self.max_value_length) + ) + + +def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": + """ + A very smart serializer that takes a dict and emits a json-friendly dict. + Currently used for serializing the final Event and also prematurely while fetching the stack + local variables for each frame in a stacktrace. + + It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. + The algorithm itself is a recursive graph walk down the data structures it encounters. + + It has the following responsibilities: + * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. + * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. + * Annotating the payload with the _meta field whenever trimming happens. + + :param max_request_body_size: If set to "always", will never trim request bodies. + :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. + :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. + :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. + + """ + serializer = _Serializer( + keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", + max_value_length=kwargs.pop("max_value_length", None), + is_vars=kwargs.pop("is_vars", False), + custom_repr=kwargs.pop("custom_repr", None), + ) + + disable_capture_event.set(True) + try: + serialized_event = serializer._serialize_node(event, **kwargs) + if ( + not serializer.is_vars + and serializer.meta_stack + and isinstance(serialized_event, dict) + ): + serialized_event["_meta"] = serializer.meta_stack[0] + + return serialized_event + finally: + disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py new file mode 100644 index 0000000000..3ffd071bbc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py @@ -0,0 +1,165 @@ +import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from sentry_sdk._types import SessionStatus + + +def _minute_trunc(ts: "datetime") -> "datetime": + return ts.replace(second=0, microsecond=0) + + +def _make_uuid( + val: "Union[str, uuid.UUID]", +) -> "uuid.UUID": + if isinstance(val, uuid.UUID): + return val + return uuid.UUID(val) + + +class Session: + def __init__( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + session_mode: str = "application", + ) -> None: + if sid is None: + sid = uuid.uuid4() + if started is None: + started = datetime.now(timezone.utc) + if status is None: + status = "ok" + self.status = status + self.did: "Optional[str]" = None + self.started = started + self.release: "Optional[str]" = None + self.environment: "Optional[str]" = None + self.duration: "Optional[float]" = None + self.user_agent: "Optional[str]" = None + self.ip_address: "Optional[str]" = None + self.session_mode: str = session_mode + self.errors = 0 + + self.update( + sid=sid, + did=did, + timestamp=timestamp, + duration=duration, + release=release, + environment=environment, + user_agent=user_agent, + ip_address=ip_address, + errors=errors, + user=user, + ) + + @property + def truncated_started(self) -> "datetime": + return _minute_trunc(self.started) + + def update( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + ) -> None: + # If a user is supplied we pull some data form it + if user: + if ip_address is None: + ip_address = user.get("ip_address") + if did is None: + did = user.get("id") or user.get("email") or user.get("username") + + if sid is not None: + self.sid = _make_uuid(sid) + if did is not None: + self.did = str(did) + if timestamp is None: + timestamp = datetime.now(timezone.utc) + self.timestamp = timestamp + if started is not None: + self.started = started + if duration is not None: + self.duration = duration + if release is not None: + self.release = release + if environment is not None: + self.environment = environment + if ip_address is not None: + self.ip_address = ip_address + if user_agent is not None: + self.user_agent = user_agent + if errors is not None: + self.errors = errors + + if status is not None: + self.status = status + + def close( + self, + status: "Optional[SessionStatus]" = None, + ) -> "Any": + if status is None and self.status == "ok": + status = "exited" + if status is not None: + self.update(status=status) + + def get_json_attrs( + self, + with_user_info: "Optional[bool]" = True, + ) -> "Any": + attrs = {} + if self.release is not None: + attrs["release"] = self.release + if self.environment is not None: + attrs["environment"] = self.environment + if with_user_info: + if self.ip_address is not None: + attrs["ip_address"] = self.ip_address + if self.user_agent is not None: + attrs["user_agent"] = self.user_agent + return attrs + + def to_json(self) -> "Any": + rv: "Dict[str, Any]" = { + "sid": str(self.sid), + "init": True, + "started": format_timestamp(self.started), + "timestamp": format_timestamp(self.timestamp), + "status": self.status, + } + if self.errors: + rv["errors"] = self.errors + if self.did is not None: + rv["did"] = self.did + if self.duration is not None: + rv["duration"] = self.duration + attrs = self.get_json_attrs() + if attrs: + rv["attrs"] = attrs + return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py new file mode 100644 index 0000000000..aabf874fcd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py @@ -0,0 +1,262 @@ +import os +import warnings +from contextlib import contextmanager +from threading import Event, Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.envelope import Envelope +from sentry_sdk.session import Session +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Generator, List, Optional, Union + + +def is_auto_session_tracking_enabled( + hub: "Optional[sentry_sdk.Hub]" = None, +) -> "Union[Any, bool, None]": + """DEPRECATED: Utility function to find out if session tracking is enabled.""" + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + + should_track = hub.scope._force_auto_session_tracking + + if should_track is None: + client_options = hub.client.options if hub.client else {} + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking( + hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: Use track_session instead + Starts and stops a session automatically around a block. + """ + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "Use track_session instead.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + should_track = is_auto_session_tracking_enabled(hub) + if should_track: + hub.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + hub.end_session() + + +def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: + """ + DEPRECATED: Utility function to find out if session tracking is enabled. + """ + + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + return _is_auto_session_tracking_enabled(scope) + + +def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: + """ + Utility function to find out if session tracking is enabled. + """ + + should_track = scope._force_auto_session_tracking + if should_track is None: + client_options = sentry_sdk.get_client().options + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking_scope( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: This function is a deprecated alias for track_session. + Starts and stops a session automatically around a block. + """ + + warnings.warn( + "This function is a deprecated alias for track_session and will be removed in the next major release.", + DeprecationWarning, + stacklevel=2, + ) + + with track_session(scope, session_mode=session_mode): + yield + + +@contextmanager +def track_session( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """ + Start a new session in the provided scope, assuming session tracking is enabled. + This is a no-op context manager if session tracking is not enabled. + """ + + should_track = _is_auto_session_tracking_enabled(scope) + if should_track: + scope.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + scope.end_session() + + +TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") +MAX_ENVELOPE_ITEMS = 100 + + +def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": + return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} + + +class SessionFlusher: + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + flush_interval: int = 60, + ) -> None: + self.capture_func = capture_func + self.flush_interval = flush_interval + self.pending_sessions: "List[Any]" = [] + self.pending_aggregates: "Dict[Any, Any]" = {} + self._thread: "Optional[Thread]" = None + self._thread_lock = Lock() + self._aggregate_lock = Lock() + self._thread_for_pid: "Optional[int]" = None + self.__shutdown_requested = Event() + + def flush(self) -> None: + pending_sessions = self.pending_sessions + self.pending_sessions = [] + + with self._aggregate_lock: + pending_aggregates = self.pending_aggregates + self.pending_aggregates = {} + + envelope = Envelope() + for session in pending_sessions: + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_session(session) + + for attrs, states in pending_aggregates.items(): + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_sessions(make_aggregate_envelope(states, attrs)) + + if len(envelope.items) > 0: + self.capture_func(envelope) + + def _ensure_running(self) -> None: + """ + Check that we have an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + running = True + while running: + running = not self.__shutdown_requested.wait(self.flush_interval) + self.flush() + + thread = Thread(target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.__shutdown_requested.set() + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def add_aggregate_session( + self, + session: "Session", + ) -> None: + # NOTE on `session.did`: + # the protocol can deal with buckets that have a distinct-id, however + # in practice we expect the python SDK to have an extremely high cardinality + # here, effectively making aggregation useless, therefore we do not + # aggregate per-did. + + # For this part we can get away with using the global interpreter lock + with self._aggregate_lock: + attrs = session.get_json_attrs(with_user_info=False) + primary_key = tuple(sorted(attrs.items())) + secondary_key = session.truncated_started # (, session.did) + states = self.pending_aggregates.setdefault(primary_key, {}) + state = states.setdefault(secondary_key, {}) + + if "started" not in state: + state["started"] = format_timestamp(session.truncated_started) + # if session.did is not None: + # state["did"] = session.did + if session.status == "crashed": + state["crashed"] = state.get("crashed", 0) + 1 + elif session.status == "abnormal": + state["abnormal"] = state.get("abnormal", 0) + 1 + elif session.errors > 0: + state["errored"] = state.get("errored", 0) + 1 + else: + state["exited"] = state.get("exited", 0) + 1 + + def add_session( + self, + session: "Session", + ) -> None: + if session.session_mode == "request": + self.add_aggregate_session(session) + else: + self.pending_sessions.append(session.to_json()) + self._ensure_running() + + def kill(self) -> None: + self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py new file mode 100644 index 0000000000..2dcc86bc47 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py @@ -0,0 +1,327 @@ +import io +import logging +import os +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from itertools import chain, product +from typing import TYPE_CHECKING + +import urllib3 + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Self + +from sentry_sdk.envelope import Envelope +from sentry_sdk.utils import ( + capture_internal_exceptions, + env_to_bool, +) +from sentry_sdk.utils import ( + logger as sentry_logger, +) + +logger = logging.getLogger("spotlight") + + +DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" +DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" + + +class SpotlightClient: + """ + A client for sending envelopes to Sentry Spotlight. + + Implements exponential backoff retry logic per the SDK spec: + - Logs error at least once when server is unreachable + - Does not log for every failed envelope + - Uses exponential backoff to avoid hammering an unavailable server + - Never blocks normal Sentry operation + """ + + # Exponential backoff settings + INITIAL_RETRY_DELAY = 1.0 # Start with 1 second + MAX_RETRY_DELAY = 60.0 # Max 60 seconds + + def __init__(self, url: str) -> None: + self.url = url + self.http = urllib3.PoolManager() + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time: float = 0.0 + + def capture_envelope(self, envelope: "Envelope") -> None: + # Check if we're in backoff period - skip sending to avoid blocking + if self._last_error_time > 0: + time_since_error = time.time() - self._last_error_time + if time_since_error < self._retry_delay: + # Still in backoff period, skip this envelope + return + + body = io.BytesIO() + envelope.serialize_into(body) + try: + req = self.http.request( + url=self.url, + body=body.getvalue(), + method="POST", + headers={ + "Content-Type": "application/x-sentry-envelope", + }, + ) + req.close() + # Success - reset backoff state + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time = 0.0 + except Exception as e: + self._last_error_time = time.time() + + # Increase backoff delay exponentially first, so logged value matches actual wait + self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) + + # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) + sentry_logger.warning( + "Failed to send envelope to Spotlight at %s: %s. " + "Will retry after %.1f seconds.", + self.url, + e, + self._retry_delay, + ) + + +try: + from django.conf import settings + from django.http import HttpRequest, HttpResponse, HttpResponseServerError + from django.utils.deprecation import MiddlewareMixin + + SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" + SPOTLIGHT_JS_SNIPPET_PATTERN = ( + "\n" + '\n' + ) + SPOTLIGHT_ERROR_PAGE_SNIPPET = ( + '\n' + '\n' + ) + CHARSET_PREFIX = "charset=" + BODY_TAG_NAME = "body" + BODY_CLOSE_TAG_POSSIBILITIES = tuple( + "".format("".join(chars)) + for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) + ) + + class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] + _spotlight_script: "Optional[str]" = None + _spotlight_url: "Optional[str]" = None + + def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: + super().__init__(get_response) + + import sentry_sdk.api + + self.sentry_sdk = sentry_sdk.api + + spotlight_client = self.sentry_sdk.get_client().spotlight + if spotlight_client is None: + sentry_logger.warning( + "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." + ) + return None + # Spotlight URL has a trailing `/stream` part at the end so split it off + self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") + + @property + def spotlight_script(self: "Self") -> "Optional[str]": + if self._spotlight_url is not None and self._spotlight_script is None: + try: + spotlight_js_url = urllib.parse.urljoin( + self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH + ) + req = urllib.request.Request( + spotlight_js_url, + method="HEAD", + ) + urllib.request.urlopen(req) + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_url=self._spotlight_url, + spotlight_js_url=spotlight_js_url, + ) + except urllib.error.URLError as err: + sentry_logger.debug( + "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", + spotlight_js_url, + exc_info=err, + ) + + return self._spotlight_script + + def process_response( + self: "Self", _request: "HttpRequest", response: "HttpResponse" + ) -> "Optional[HttpResponse]": + content_type_header = tuple( + p.strip() + for p in response.headers.get("Content-Type", "").lower().split(";") + ) + content_type = content_type_header[0] + if len(content_type_header) > 1 and content_type_header[1].startswith( + CHARSET_PREFIX + ): + encoding = content_type_header[1][len(CHARSET_PREFIX) :] + else: + encoding = "utf-8" + + if ( + self.spotlight_script is not None + and not response.streaming + and content_type == "text/html" + ): + content_length = len(response.content) + injection = self.spotlight_script.encode(encoding) + injection_site = next( + ( + idx + for idx in ( + response.content.rfind(body_variant.encode(encoding)) + for body_variant in BODY_CLOSE_TAG_POSSIBILITIES + ) + if idx > -1 + ), + content_length, + ) + + # This approach works even when we don't have a `` tag + response.content = ( + response.content[:injection_site] + + injection + + response.content[injection_site:] + ) + + if response.has_header("Content-Length"): + response.headers["Content-Length"] = content_length + len(injection) + + return response + + def process_exception( + self: "Self", _request: "HttpRequest", exception: Exception + ) -> "Optional[HttpResponseServerError]": + if not settings.DEBUG or not self._spotlight_url: + return None + + try: + spotlight = ( + urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") + ) + except urllib.error.URLError: + return None + else: + event_id = self.sentry_sdk.capture_exception(exception) + return HttpResponseServerError( + spotlight.replace( + "", + SPOTLIGHT_ERROR_PAGE_SNIPPET.format( + spotlight_url=self._spotlight_url, event_id=event_id + ), + ) + ) + +except ImportError: + settings = None + + +def _resolve_spotlight_url( + spotlight_config: "Any", sentry_logger: "Any" +) -> "Optional[str]": + """ + Resolve the Spotlight URL based on config and environment variable. + + Implements precedence rules per the SDK spec: + https://develop.sentry.dev/sdk/expected-features/spotlight/ + + Returns the resolved URL string, or None if Spotlight should be disabled. + """ + spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") + + # Parse env var to determine if it's a boolean or URL + spotlight_from_env: "Optional[bool]" = None + spotlight_env_url: "Optional[str]" = None + if spotlight_env_value: + parsed = env_to_bool(spotlight_env_value, strict=True) + if parsed is None: + # It's a URL string + spotlight_from_env = True + spotlight_env_url = spotlight_env_value + else: + spotlight_from_env = parsed + + # Apply precedence rules per spec: + # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules + if spotlight_config is False: + # Config explicitly disables spotlight - warn if env var was set + if spotlight_from_env: + sentry_logger.warning( + "Spotlight is disabled via spotlight=False config option, " + "ignoring SENTRY_SPOTLIGHT environment variable." + ) + return None + elif spotlight_config is True: + # Config enables spotlight with boolean true + # If env var has URL, use env var URL per spec + if spotlight_env_url: + return spotlight_env_url + else: + return DEFAULT_SPOTLIGHT_URL + elif isinstance(spotlight_config, str): + # Config has URL string - use config URL, warn if env var differs + if spotlight_env_value and spotlight_env_value != spotlight_config: + sentry_logger.warning( + "Spotlight URL from config (%s) takes precedence over " + "SENTRY_SPOTLIGHT environment variable (%s).", + spotlight_config, + spotlight_env_value, + ) + return spotlight_config + elif spotlight_config is None: + # No config - use env var + if spotlight_env_url: + return spotlight_env_url + elif spotlight_from_env: + return DEFAULT_SPOTLIGHT_URL + # else: stays None (disabled) + + return None + + +def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": + url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) + + if url is None: + return None + + # Only set up logging handler when spotlight is actually enabled + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.INFO) + + # Update options with resolved URL for consistency + options["spotlight"] = url + + with capture_internal_exceptions(): + if ( + settings is not None + and settings.DEBUG + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) + ): + middleware = settings.MIDDLEWARE + if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: + settings.MIDDLEWARE = type(middleware)( + chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) + ) + logger.info("Enabled Spotlight integration for Django") + + client = SpotlightClient(url) + logger.info("Enabled Spotlight using sidecar at %s", url) + + return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py new file mode 100644 index 0000000000..5ee7e8460b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py @@ -0,0 +1,843 @@ +""" +EXPERIMENTAL. Do not use in production. + +The API in this file is only meant to be used in span streaming mode. + +You can enable span streaming mode via +sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). +""" + +import sys +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + capture_internal_exceptions, + format_attribute, + get_current_thread_meta, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + Iterator, + Optional, + ParamSpec, + TypeVar, + Union, + overload, + ) + + from sentry_sdk._types import Attributes, AttributeValue, SpanJSON + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + + P = ParamSpec("P") + R = TypeVar("R") + + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +class SpanStatus(str, Enum): + OK = "ok" + ERROR = "error" + + def __str__(self) -> str: + return self.value + + +_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) + + +# Segment source, see +# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource +class SegmentSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_SEGMENT_SOURCES = [ + SegmentSource.URL, +] + + +SOURCE_FOR_STYLE = { + "endpoint": SegmentSource.COMPONENT, + "function_name": SegmentSource.COMPONENT, + "handler_name": SegmentSource.COMPONENT, + "method_and_path_pattern": SegmentSource.ROUTE, + "path": SegmentSource.URL, + "route_name": SegmentSource.COMPONENT, + "route_pattern": SegmentSource.ROUTE, + "uri_template": SegmentSource.ROUTE, + "url": SegmentSource.ROUTE, +} + + +# Sentinel value for an unset parent_span to be able to distinguish it from +# a None set by the user +_DEFAULT_PARENT_SPAN = object() + + +def start_span( + name: str, + attributes: "Optional[Attributes]" = None, + parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] + active: bool = True, +) -> "StreamedSpan": + """ + Start a span. + + EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() + instead. + + The span's parent, unless provided explicitly via the `parent_span` argument, + will be the current active span, if any. If there is none, this span will + become the root of a new span tree. If you explicitly want this span to be + top-level without a parent, set `parent_span=None`. + + `start_span()` can either be used as context manager or you can use the span + object it returns and explicitly end it via `span.end()`. The following is + equivalent: + + ```python + import sentry_sdk + + with sentry_sdk.traces.start_span(name="My Span"): + # do something + + # The span automatically finishes once the `with` block is exited + ``` + + ```python + import sentry_sdk + + span = sentry_sdk.traces.start_span(name="My Span") + # do something + span.end() + ``` + + To continue a trace from another service, call + `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. + + :param name: The name to identify this span by. + :type name: str + + :param attributes: Key-value attributes to set on the span from the start. + These will also be accessible in the traces sampler. + :type attributes: "Optional[Attributes]" + + :param parent_span: A span instance that the new span should consider its + parent. If not provided, the parent will be set to the currently active + span, if any. If set to `None`, this span will become a new root-level + span. + :type parent_span: "Optional[StreamedSpan]" + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to `False`. + :type active: bool + + :return: The span that has been started. + :rtype: StreamedSpan + """ + from sentry_sdk.tracing_utils import has_span_streaming_enabled + + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "sentry_sdk.start_transaction() and sentry_sdk.start_span() " + "instead.", + stacklevel=2, + ) + return NoOpStreamedSpan() + + return sentry_sdk.get_current_scope().start_streamed_span( + name, attributes, parent_span, active + ) + + +def continue_trace(incoming: "dict[str, Any]") -> None: + """ + Continue a trace from headers or environment variables. + + EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. + + This function sets the propagation context on the scope. Any span started + in the updated scope will belong under the trace extracted from the + provided propagation headers or environment variables. + + continue_trace() doesn't start any spans on its own. Use the start_span() + API for that. + """ + # This is set both on the isolation and the current scope for compatibility + # reasons. Conceptually, it belongs on the isolation scope, and it also + # used to be set there in non-span-first mode. But in span first mode, we + # start spans on the current scope, regardless of type, like JS does, so we + # need to set the propagation context there. + sentry_sdk.get_isolation_scope().generate_propagation_context( + incoming, + ) + sentry_sdk.get_current_scope().generate_propagation_context( + incoming, + ) + + +def new_trace() -> None: + """ + Resets the propagation context, forcing a new trace. + + EXPERIMENTAL. + + This function sets the propagation context on the scope. Any span started + in the updated scope will start its own trace. + + new_trace() doesn't start any spans on its own. Use the start_span() API + for that. + """ + sentry_sdk.get_isolation_scope().set_new_propagation_context() + sentry_sdk.get_current_scope().set_new_propagation_context() + + +class StreamedSpan: + """ + A span holds timing information of a block of code. + + Spans can have multiple child spans, thus forming a span tree. + + This is the Span First span implementation that streams spans. The original + transaction-based span implementation lives in tracing.Span. + """ + + __slots__ = ( + "_name", + "_attributes", + "_active", + "_span_id", + "_trace_id", + "_parent_span_id", + "_segment", + "_parent_sampled", + "_start_timestamp", + "_start_timestamp_monotonic_ns", + "_end_timestamp", + "_status", + "_scope", + "_previous_span_on_scope", + "_baggage", + "_sample_rand", + "_sample_rate", + "_continuous_profile", + ) + + def __init__( + self, + *, + name: str, + attributes: "Optional[Attributes]" = None, + active: bool = True, + scope: "sentry_sdk.Scope", + segment: "Optional[StreamedSpan]" = None, + trace_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + sample_rate: "Optional[float]" = None, + sample_rand: "Optional[float]" = None, + ): + self._name: str = name + self._active: bool = active + self._attributes: "Attributes" = { + "sentry.origin": "manual", + "sentry.trace_lifecycle": "stream", + } + + if attributes: + for attribute, value in attributes.items(): + self.set_attribute(attribute, value) + + self._scope = scope + + self._segment = segment or self + + self._trace_id: "Optional[str]" = trace_id + self._parent_span_id = parent_span_id + self._parent_sampled = parent_sampled + self._baggage = baggage + self._sample_rand = sample_rand + self._sample_rate = sample_rate + + self._start_timestamp = datetime.now(timezone.utc) + self._end_timestamp: "Optional[datetime]" = None + + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + + self._span_id: "Optional[str]" = None + + self._status = SpanStatus.OK.value + + self._update_active_thread() + + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._start_profile() + self._set_profile_id(get_profiler_id()) + + self._set_segment_attributes() + + self._start() + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__}(" + f"name={self._name}, " + f"trace_id={self.trace_id}, " + f"span_id={self.span_id}, " + f"parent_span_id={self._parent_span_id}, " + f"active={self._active})>" + ) + + def __enter__(self) -> "StreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore + return + + if value is not None and should_be_treated_as_error(ty, value): + self.status = SpanStatus.ERROR.value + + self._end() + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + """ + Finish this span and queue it for sending. + + :param end_timestamp: End timestamp to use instead of current time. + :type end_timestamp: "Optional[Union[float, datetime]]" + """ + self._end(end_timestamp) + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self.end(end_timestamp) + + def _start(self) -> None: + if self._active: + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore. + return + + # Stop the profiler + if self._is_segment() and self._continuous_profile is not None: + with capture_internal_exceptions(): + self._continuous_profile.stop() + + # Detach from scope + if self._active: + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + # Set attributes from the segment. These are set on span end on purpose + # so that we have the best chance to capture the segment's final name + # (since it might change during its lifetime) + self.set_attribute("sentry.segment.id", self._segment.span_id) + self.set_attribute("sentry.segment.name", self._segment.name) + + # Set the end timestamp + if end_timestamp is not None: + if isinstance(end_timestamp, (float, int)): + try: + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + except Exception: + pass + + if isinstance(end_timestamp, datetime): + self._end_timestamp = end_timestamp + else: + logger.debug( + "[Tracing] Failed to set end_timestamp. Using current time instead." + ) + + if self._end_timestamp is None: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self._end_timestamp = self._start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # Finally, queue the span for sending to Sentry + self._scope._capture_span(self) + + def get_attributes(self) -> "Attributes": + return self._attributes + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + self._attributes[key] = format_attribute(value) + + def set_attributes(self, attributes: "Attributes") -> None: + for key, value in attributes.items(): + self.set_attribute(key, value) + + def remove_attribute(self, key: str) -> None: + try: + del self._attributes[key] + except KeyError: + pass + + @property + def status(self) -> "str": + return self._status + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + if isinstance(status, Enum): + status = status.value + + if status not in _VALID_SPAN_STATUSES: + logger.debug( + f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' + ) + return + + self._status = status + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def active(self) -> bool: + return self._active + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @property + def sampled(self) -> "Optional[bool]": + return True + + @property + def start_timestamp(self) -> "Optional[datetime]": + return self._start_timestamp + + @property + def end_timestamp(self) -> "Optional[datetime]": + return self._end_timestamp + + def _is_segment(self) -> bool: + return self._segment is self + + def _update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + + if thread_id is not None: + self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_attribute(SPANDATA.THREAD_NAME, thread_name) + + def _dynamic_sampling_context(self) -> "dict[str, str]": + return self._segment._get_baggage().dynamic_sampling_context() + + def _to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def _to_baggage(self) -> "Optional[Baggage]": + if self._segment: + return self._segment._get_baggage() + return None + + def _get_baggage(self) -> "Baggage": + """ + Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with + the segment. + + The first time a new baggage with Sentry items is made, it will be frozen. + """ + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_segment(self) + + return self._baggage + + def _iter_headers(self) -> "Iterator[tuple[str, str]]": + if not self._segment: + return + + yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() + + baggage = self._segment._get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def _get_trace_context(self) -> "dict[str, Any]": + # Even if spans themselves are not event-based anymore, we need this + # to populate trace context on events + context: "dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self._parent_span_id, + "dynamic_sampling_context": self._dynamic_sampling_context(), + } + + if "sentry.op" in self._attributes: + context["op"] = self._attributes["sentry.op"] + if "sentry.origin" in self._attributes: + context["origin"] = self._attributes["sentry.origin"] + + return context + + def _set_profile_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_attribute("sentry.profiler_id", profiler_id) + + def _start_profile(self) -> None: + if not self._is_segment(): + return + + try_autostart_continuous_profiler() + + self._continuous_profile = try_profile_lifecycle_trace_start() + + def _set_segment_attributes(self) -> None: + if not self._is_segment(): + return + + client = sentry_sdk.get_client() + + self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") + self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) + self.set_attribute( + SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) + ) + + if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: + self.set_attribute( + SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() + ) + + def _to_json(self) -> "SpanJSON": + res: "SpanJSON" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "name": self._name if self._name is not None else "", + "status": self._status, + "is_segment": self._is_segment(), + "start_timestamp": self._start_timestamp.timestamp(), + } + + if self._end_timestamp: + res["end_timestamp"] = self._end_timestamp.timestamp() + + if self._parent_span_id: + res["parent_span_id"] = self._parent_span_id + + res["attributes"] = {k: v for k, v in self._attributes.items()} + + return res + + +class NoOpStreamedSpan(StreamedSpan): + __slots__ = ( + "_finished", + "_unsampled_reason", + ) + + def __init__( + self, + unsampled_reason: "Optional[str]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + ) -> None: + self._scope = scope # type: ignore[assignment] + self._unsampled_reason = unsampled_reason + + self._finished = False + + self._start() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(sampled={self.sampled})>" + + def __enter__(self) -> "NoOpStreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self._end() + + def _start(self) -> None: + if self._scope is None: + return + + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._finished: + return + + if self._unsampled_reason is not None: + client = sentry_sdk.get_client() + if client.is_active() and client.transport: + logger.debug( + f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" + ) + client.transport.record_lost_event( + reason=self._unsampled_reason, + data_category="span", + quantity=1, + ) + + if self._scope and hasattr(self, "_previous_span_on_scope"): + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + self._finished = True + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + self._end() + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self._end() + + def get_attributes(self) -> "Attributes": + return {} + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + pass + + def set_attributes(self, attributes: "Attributes") -> None: + pass + + def remove_attribute(self, key: str) -> None: + pass + + def _is_segment(self) -> bool: + return self._scope is not None + + @property + def status(self) -> "str": + return SpanStatus.OK.value + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + pass + + @property + def name(self) -> str: + return "" + + @name.setter + def name(self, value: str) -> None: + pass + + @property + def active(self) -> bool: + return True + + @property + def span_id(self) -> str: + return "0000000000000000" + + @property + def trace_id(self) -> str: + return "00000000000000000000000000000000" + + @property + def sampled(self) -> "Optional[bool]": + return False + + @property + def start_timestamp(self) -> "Optional[datetime]": + return None + + @property + def end_timestamp(self) -> "Optional[datetime]": + return None + + +if TYPE_CHECKING: + + @overload + def trace( + func: "Callable[P, R]", + ) -> "Callable[P, R]": ... + + @overload + def trace( + func: None = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a span around a function call. + + EXPERIMENTAL. Use @sentry_sdk.trace instead. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to False. + :type active: bool + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import ( + create_streaming_span_decorator, + ) + + decorator = create_streaming_span_decorator( + name=name, + attributes=attributes, + active=active, + ) + + if func: + return decorator(func) + else: + return decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[StreamedSpan]": + """ + Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. + + This function will only return a non-`None` value when the streaming trace lifecycle is enabled. + To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.streamed_span + return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py new file mode 100644 index 0000000000..1790e13fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py @@ -0,0 +1,1475 @@ +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.profiler.continuous_profiler import get_profiler_id +from sentry_sdk.utils import ( + capture_internal_exceptions, + get_current_thread_meta, + is_valid_sample_rate, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, MutableMapping + from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + overload, + ) + + from typing_extensions import TypedDict, Unpack + + P = ParamSpec("P") + R = TypeVar("R") + + from sentry_sdk._types import ( + Event, + MeasurementUnit, + MeasurementValue, + SamplingContext, + ) + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + from sentry_sdk.profiler.transaction_profiler import Profile + + class SpanKwargs(TypedDict, total=False): + trace_id: str + """ + The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + """ + + span_id: str + """The span ID of this span. If omitted, a new span ID will be generated.""" + + parent_span_id: str + """The span ID of the parent span, if applicable.""" + + same_process_as_parent: bool + """Whether this span is in the same process as the parent span.""" + + sampled: bool + """ + Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + """ + + op: str + """ + The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + """ + + description: str + """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" + + hub: "Optional[sentry_sdk.Hub]" + """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" + + status: str + """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" + + containing_transaction: "Optional[Transaction]" + """The transaction that this span belongs to.""" + + start_timestamp: "Optional[Union[datetime, float]]" + """ + The timestamp when the span started. If omitted, the current time + will be used. + """ + + scope: "sentry_sdk.Scope" + """The scope to use for this span. If not provided, we use the current scope.""" + + origin: str + """ + The origin of the span. + See https://develop.sentry.dev/sdk/performance/trace-origin/ + Default "manual". + """ + + name: str + """A string describing what operation is being performed within the span/transaction.""" + + class TransactionKwargs(SpanKwargs, total=False): + source: str + """ + A string describing the source of the transaction name. This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. + Default "custom". + """ + + parent_sampled: bool + """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" + + baggage: "Baggage" + """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" + + ProfileContext = TypedDict( + "ProfileContext", + { + "profiler_id": str, + }, + ) + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +# Transaction source +# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations +class TransactionSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_TRANSACTION_SOURCES = [ + TransactionSource.URL, +] + +SOURCE_FOR_STYLE = { + "endpoint": TransactionSource.COMPONENT, + "function_name": TransactionSource.COMPONENT, + "handler_name": TransactionSource.COMPONENT, + "method_and_path_pattern": TransactionSource.ROUTE, + "path": TransactionSource.URL, + "route_name": TransactionSource.COMPONENT, + "route_pattern": TransactionSource.ROUTE, + "uri_template": TransactionSource.ROUTE, + "url": TransactionSource.ROUTE, +} + + +def get_span_status_from_http_code(http_status_code: int) -> str: + """ + Returns the Sentry status corresponding to the given HTTP status code. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + if http_status_code < 400: + return SPANSTATUS.OK + + elif 400 <= http_status_code < 500: + if http_status_code == 403: + return SPANSTATUS.PERMISSION_DENIED + elif http_status_code == 404: + return SPANSTATUS.NOT_FOUND + elif http_status_code == 429: + return SPANSTATUS.RESOURCE_EXHAUSTED + elif http_status_code == 413: + return SPANSTATUS.FAILED_PRECONDITION + elif http_status_code == 401: + return SPANSTATUS.UNAUTHENTICATED + elif http_status_code == 409: + return SPANSTATUS.ALREADY_EXISTS + else: + return SPANSTATUS.INVALID_ARGUMENT + + elif 500 <= http_status_code < 600: + if http_status_code == 504: + return SPANSTATUS.DEADLINE_EXCEEDED + elif http_status_code == 501: + return SPANSTATUS.UNIMPLEMENTED + elif http_status_code == 503: + return SPANSTATUS.UNAVAILABLE + else: + return SPANSTATUS.INTERNAL_ERROR + + return SPANSTATUS.UNKNOWN_ERROR + + +class _SpanRecorder: + """Limits the number of spans recorded in a transaction.""" + + __slots__ = ("maxlen", "spans", "dropped_spans") + + def __init__(self, maxlen: int) -> None: + # FIXME: this is `maxlen - 1` only to preserve historical behavior + # enforced by tests. + # Either this should be changed to `maxlen` or the JS SDK implementation + # should be changed to match a consistent interpretation of what maxlen + # limits: either transaction+spans or only child spans. + self.maxlen = maxlen - 1 + self.spans: "List[Span]" = [] + self.dropped_spans: int = 0 + + def add(self, span: "Span") -> None: + if len(self.spans) > self.maxlen: + span._span_recorder = None + self.dropped_spans += 1 + else: + self.spans.append(span) + + +class Span: + """A span holds timing information of a block of code. + Spans can have multiple child spans thus forming a span tree. + + :param trace_id: The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + :param span_id: The span ID of this span. If omitted, a new span ID will be generated. + :param parent_span_id: The span ID of the parent span, if applicable. + :param same_process_as_parent: Whether this span is in the same process as the parent span. + :param sampled: Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + :param op: The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + :param description: A description of what operation is being performed within the span. + + .. deprecated:: 2.15.0 + Please use the `name` parameter, instead. + :param name: A string describing what operation is being performed within the span. + :param hub: The hub to use for this span. + + .. deprecated:: 2.0.0 + Please use the `scope` parameter, instead. + :param status: The span's status. Possible values are listed at + https://develop.sentry.dev/sdk/event-payloads/span/ + :param containing_transaction: The transaction that this span belongs to. + :param start_timestamp: The timestamp when the span started. If omitted, the current time + will be used. + :param scope: The scope to use for this span. If not provided, we use the current scope. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "same_process_as_parent", + "sampled", + "op", + "description", + "_measurements", + "start_timestamp", + "_start_timestamp_monotonic_ns", + "status", + "timestamp", + "_tags", + "_data", + "_span_recorder", + "hub", + "_context_manager_state", + "_containing_transaction", + "scope", + "origin", + "name", + "_flags", + "_flags_capacity", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + same_process_as_parent: bool = True, + sampled: "Optional[bool]" = None, + op: "Optional[str]" = None, + description: "Optional[str]" = None, + hub: "Optional[sentry_sdk.Hub]" = None, # deprecated + status: "Optional[str]" = None, + containing_transaction: "Optional[Transaction]" = None, + start_timestamp: "Optional[Union[datetime, float]]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + origin: str = "manual", + name: "Optional[str]" = None, + ) -> None: + self._trace_id = trace_id + self._span_id = span_id + self.parent_span_id = parent_span_id + self.same_process_as_parent = same_process_as_parent + self.sampled = sampled + self.op = op + self.description = name or description + self.status = status + self.hub = hub # backwards compatibility + self.scope = scope + self.origin = origin + self._measurements: "Dict[str, MeasurementValue]" = {} + self._tags: "MutableMapping[str, str]" = {} + self._data: "Dict[str, Any]" = {} + self._containing_transaction = containing_transaction + self._flags: "Dict[str, bool]" = {} + self._flags_capacity = 10 + + if hub is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use `scope` instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.scope = self.scope or hub.scope + + if start_timestamp is None: + start_timestamp = datetime.now(timezone.utc) + elif isinstance(start_timestamp, float): + start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) + self.start_timestamp = start_timestamp + try: + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + except AttributeError: + pass + + #: End timestamp of span + self.timestamp: "Optional[datetime]" = None + + self._span_recorder: "Optional[_SpanRecorder]" = None + + self.update_active_thread() + self.set_profiler_id(get_profiler_id()) + + # TODO this should really live on the Transaction class rather than the Span + # class + def init_span_recorder(self, maxlen: int) -> None: + if self._span_recorder is None: + self._span_recorder = _SpanRecorder(maxlen) + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + def __repr__(self) -> str: + return ( + "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.op, + self.description, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.origin, + ) + ) + + def __enter__(self) -> "Span": + scope = self.scope or sentry_sdk.get_current_scope() + old_span = scope.span + scope.span = self + self._context_manager_state = (scope, old_span) + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if value is not None and should_be_treated_as_error(ty, value): + self.set_status(SPANSTATUS.INTERNAL_ERROR) + + with capture_internal_exceptions(): + scope, old_span = self._context_manager_state + del self._context_manager_state + self.finish(scope) + scope.span = old_span + + @property + def containing_transaction(self) -> "Optional[Transaction]": + """The ``Transaction`` that this span belongs to. + The ``Transaction`` is the root of the span tree, + so one could also think of this ``Transaction`` as the "root span".""" + + # this is a getter rather than a regular attribute so that transactions + # can return `self` here instead (as a way to prevent them circularly + # referencing themselves) + return self._containing_transaction + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a sub-span from the current span or transaction. + + Takes the same arguments as the initializer of :py:class:`Span`. The + trace id, sampling decision, transaction pointer, and span recorder are + inherited from the current span/transaction. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + kwargs.setdefault("sampled", self.sampled) + + child = Span( + trace_id=self.trace_id, + parent_span_id=self.span_id, + containing_transaction=self.containing_transaction, + **kwargs, + ) + + span_recorder = ( + self.containing_transaction and self.containing_transaction._span_recorder + ) + if span_recorder: + span_recorder.add(child) + + return child + + @classmethod + def continue_from_environ( + cls, + environ: "Mapping[str, str]", + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a Transaction with the given params, then add in data pulled from + the ``sentry-trace`` and ``baggage`` headers from the environ (if any) + before returning the Transaction. + + This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` + in that it assumes header names in the form ``HTTP_HEADER_NAME`` - + such as you would get from a WSGI/ASGI environ - + rather than the form ``header-name``. + + :param environ: The ASGI/WSGI environ to pull information from. + """ + return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) + + @classmethod + def continue_from_headers( + cls, + headers: "Mapping[str, str]", + *, + _sample_rand: "Optional[str]" = None, + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a transaction with the given params (including any data pulled from + the ``sentry-trace`` and ``baggage`` headers). + + :param headers: The dictionary with the HTTP headers to pull information from. + :param _sample_rand: If provided, we override the sample_rand value from the + incoming headers with this value. (internal use only) + """ + logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") + + # TODO-neel move away from this kwargs stuff, it's confusing and opaque + # make more explicit + baggage = Baggage.from_incoming_header( + headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand + ) + kwargs.update({BAGGAGE_HEADER_NAME: baggage}) + + sentrytrace_kwargs = extract_sentrytrace_data( + headers.get(SENTRY_TRACE_HEADER_NAME) + ) + + if sentrytrace_kwargs is not None: + kwargs.update(sentrytrace_kwargs) + + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and immutable and won't be populated as head SDK. + baggage.freeze() + + transaction = Transaction(**kwargs) + transaction.same_process_as_parent = False + + return transaction + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. + If the span's containing transaction doesn't yet have a ``baggage`` value, + this will cause one to be generated and stored. + """ + if not self.containing_transaction: + # Do not propagate headers if there is no containing transaction. Otherwise, this + # span ends up being the root span of a new trace, and since it does not get sent + # to Sentry, the trace will be missing a root transaction. The dynamic sampling + # context will also be missing, breaking dynamic sampling & traces. + return + + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.containing_transaction.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + @classmethod + def from_traceparent( + cls, + traceparent: "Optional[str]", + **kwargs: "Any", + ) -> "Optional[Transaction]": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a ``Transaction`` with the given params, then add in data pulled from + the given ``sentry-trace`` header value before returning the ``Transaction``. + """ + if not traceparent: + return None + + return cls.continue_from_headers( + {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs + ) + + def to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def to_baggage(self) -> "Optional[Baggage]": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with this ``Span``, if any. (Taken from the root of the span tree.) + """ + if self.containing_transaction: + return self.containing_transaction.get_baggage() + return None + + def set_tag(self, key: str, value: "Any") -> None: + self._tags[key] = value + + def set_data(self, key: str, value: "Any") -> None: + self._data[key] = value + + def update_data(self, data: "Dict[str, Any]") -> None: + self._data.update(data) + + def set_flag(self, flag: str, result: bool) -> None: + if len(self._flags) < self._flags_capacity: + self._flags[flag] = result + + def set_status(self, value: str) -> None: + self.status = value + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_thread( + self, thread_id: "Optional[int]", thread_name: "Optional[str]" + ) -> None: + if thread_id is not None: + self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_data(SPANDATA.THREAD_NAME, thread_name) + + def set_profiler_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) + + def set_http_status(self, http_status: int) -> None: + self.set_tag( + "http.status_code", str(http_status) + ) # TODO-neel remove in major, we keep this for backwards compatibility + self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) + self.set_status(get_span_status_from_http_code(http_status)) + + def is_success(self) -> bool: + return self.status == "ok" + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + ) -> "Optional[str]": + """ + Sets the end timestamp of the span. + + Additionally it also creates a breadcrumb from the span, + if the span represents a database or HTTP request. + + :param scope: The scope to use for this transaction. + If not provided, the current scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + + :return: Always ``None``. The type is ``Optional[str]`` to match + the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. + """ + if self.timestamp is not None: + # This span is already finished, ignore. + return None + + try: + if end_timestamp: + if isinstance(end_timestamp, float): + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + self.timestamp = end_timestamp + else: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self.timestamp = self.start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + except AttributeError: + self.timestamp = datetime.now(timezone.utc) + + scope = scope or sentry_sdk.get_current_scope() + + # Copy conversation_id from scope to span data if this is an AI span + conversation_id = scope.get_conversation_id() + if conversation_id: + has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data + is_ai_span_op = self.op is not None and ( + self.op.startswith("ai.") or self.op.startswith("gen_ai.") + ) + if has_ai_op or is_ai_span_op: + self.set_data("gen_ai.conversation.id", conversation_id) + + maybe_create_breadcrumbs_from_span(scope, self) + + return None + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the span.""" + + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "same_process_as_parent": self.same_process_as_parent, + "op": self.op, + "description": self.description, + "start_timestamp": self.start_timestamp, + "timestamp": self.timestamp, + "origin": self.origin, + } + + if self.status: + rv["status"] = self.status + # TODO-neel remove redundant tag in major + self._tags["status"] = self.status + + if len(self._measurements) > 0: + rv["measurements"] = self._measurements + + tags = self._tags + if tags: + rv["tags"] = tags + + data = {} + data.update(self._flags) + data.update(self._data) + if data: + rv["data"] = data + + return rv + + def get_trace_context(self) -> "Any": + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "op": self.op, + "description": self.description, + "origin": self.origin, + } + if self.status: + rv["status"] = self.status + + if self.containing_transaction: + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) + + data = {} + + thread_id = self._data.get(SPANDATA.THREAD_ID) + if thread_id is not None: + data["thread.id"] = thread_id + + thread_name = self._data.get(SPANDATA.THREAD_NAME) + if thread_name is not None: + data["thread.name"] = thread_name + + if data: + rv["data"] = data + + return rv + + def get_profile_context(self) -> "Optional[ProfileContext]": + profiler_id = self._data.get(SPANDATA.PROFILER_ID) + if profiler_id is None: + return None + + return { + "profiler_id": profiler_id, + } + + def update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + self.set_thread(thread_id, thread_name) + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +class Transaction(Span): + """The Transaction is the root element that holds all the spans + for Sentry performance instrumentation. + + :param name: Identifier of the transaction. + Will show up in the Sentry UI. + :param parent_sampled: Whether the parent transaction was sampled. + If True this transaction will be kept, if False it will be discarded. + :param baggage: The W3C baggage header value. + (see https://www.w3.org/TR/baggage/) + :param source: A string describing the source of the transaction name. + This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations + for more information. Default "custom". + :param kwargs: Additional arguments to be passed to the Span constructor. + See :py:class:`sentry_sdk.tracing.Span` for available arguments. + """ + + __slots__ = ( + "name", + "source", + "parent_sampled", + # used to create baggage value for head SDKs in dynamic sampling + "sample_rate", + "_measurements", + "_contexts", + "_profile", + "_continuous_profile", + "_baggage", + "_sample_rand", + ) + + def __init__( # type: ignore[misc] + self, + name: str = "", + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + source: str = TransactionSource.CUSTOM, + **kwargs: "Unpack[SpanKwargs]", + ) -> None: + super().__init__(**kwargs) + + self.name = name + self.source = source + self.sample_rate: "Optional[float]" = None + self.parent_sampled = parent_sampled + self._measurements: "Dict[str, MeasurementValue]" = {} + self._contexts: "Dict[str, Any]" = {} + self._profile: "Optional[Profile]" = None + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._baggage = baggage + + baggage_sample_rand = ( + None if self._baggage is None else self._baggage._sample_rand() + ) + if baggage_sample_rand is not None: + self._sample_rand = baggage_sample_rand + else: + self._sample_rand = _generate_sample_rand(self.trace_id) + + def __repr__(self) -> str: + return ( + "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.name, + self.op, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.source, + self.origin, + ) + ) + + def _possibly_started(self) -> bool: + """Returns whether the transaction might have been started. + + If this returns False, we know that the transaction was not started + with sentry_sdk.start_transaction, and therefore the transaction will + be discarded. + """ + + # We must explicitly check self.sampled is False since self.sampled can be None + return self._span_recorder is not None or self.sampled is False + + def __enter__(self) -> "Transaction": + if not self._possibly_started(): + logger.debug( + "Transaction was entered without being started with sentry_sdk.start_transaction." + "The transaction will not be sent to Sentry. To fix, start the transaction by" + "passing it to sentry_sdk.start_transaction." + ) + + super().__enter__() + + if self._profile is not None: + self._profile.__enter__() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._profile is not None: + self._profile.__exit__(ty, value, tb) + + if self._continuous_profile is not None: + self._continuous_profile.stop() + + super().__exit__(ty, value, tb) + + @property + def containing_transaction(self) -> "Transaction": + """The root element of the span tree. + In the case of a transaction it is the transaction itself. + """ + + # Transactions (as spans) belong to themselves (as transactions). This + # is a getter rather than a regular attribute to avoid having a circular + # reference. + return self + + def _get_scope_from_finish_args( + self, + scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + ) -> "Optional[sentry_sdk.Scope]": + """ + Logic to get the scope from the arguments passed to finish. This + function exists for backwards compatibility with the old finish. + + TODO: Remove this function in the next major version. + """ + scope_or_hub = scope_arg + if hub_arg is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", + DeprecationWarning, + stacklevel=3, + ) + + scope_or_hub = hub_arg + + if isinstance(scope_or_hub, sentry_sdk.Hub): + warnings.warn( + "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", + DeprecationWarning, + stacklevel=3, + ) + + return scope_or_hub.scope + + return scope_or_hub + + def _get_log_representation(self) -> str: + return "{op}transaction <{name}>".format( + op=("<" + self.op + "> " if self.op else ""), name=self.name + ) + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """Finishes the transaction and sends it to Sentry. + All finished spans in the transaction will also be sent to Sentry. + + :param scope: The Scope to use for this transaction. + If not provided, the current Scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + :param hub: The hub to use for this transaction. + This argument is DEPRECATED. Please use the `scope` + parameter, instead. + + :return: The event ID if the transaction was sent to Sentry, + otherwise None. + """ + if self.timestamp is not None: + # This transaction is already finished, ignore. + return None + + # For backwards compatibility, we must handle the case where `scope` + # or `hub` could both either be a `Scope` or a `Hub`. + scope = self._get_scope_from_finish_args(scope, hub) + + scope = scope or self.scope or sentry_sdk.get_current_scope() + client = sentry_sdk.get_client() + + if not client.is_active(): + # We have no active client and therefore nowhere to send this transaction. + return None + + if self._span_recorder is None: + # Explicit check against False needed because self.sampled might be None + if self.sampled is False: + logger.debug("Discarding transaction because sampled = False") + else: + logger.debug( + "Discarding transaction because it was not started with sentry_sdk.start_transaction" + ) + + # This is not entirely accurate because discards here are not + # exclusively based on sample rate but also traces sampler, but + # we handle this the same here. + if client.transport and has_tracing_enabled(client.options): + if client.monitor and client.monitor.downsample_factor > 0: + reason = "backpressure" + else: + reason = "sample_rate" + + client.transport.record_lost_event(reason, data_category="transaction") + + # Only one span (the transaction itself) is discarded, since we did not record any spans here. + client.transport.record_lost_event(reason, data_category="span") + return None + + if not self.name: + logger.warning( + "Transaction has no name, falling back to ``." + ) + self.name = "" + + super().finish(scope, end_timestamp) + + status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) + if ( + status_code is not None + and status_code in client.options["trace_ignore_status_codes"] + ): + logger.debug( + "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( + transaction_description=self._get_log_representation(), + status_code=self._data[SPANDATA.HTTP_STATUS_CODE], + trace_ignore_status_codes=client.options[ + "trace_ignore_status_codes" + ], + ) + ) + if client.transport: + client.transport.record_lost_event( + "event_processor", data_category="transaction" + ) + + num_spans = len(self._span_recorder.spans) + 1 + client.transport.record_lost_event( + "event_processor", data_category="span", quantity=num_spans + ) + + self.sampled = False + + if not self.sampled: + # At this point a `sampled = None` should have already been resolved + # to a concrete decision. + if self.sampled is None: + logger.warning("Discarding transaction without sampling decision.") + + return None + + finished_spans = [] + has_gen_ai_span = False + if client.options.get("stream_gen_ai_spans", True): + for span in self._span_recorder.spans: + if span.timestamp is None: + continue + + if isinstance(span.op, str) and span.op.startswith("gen_ai."): + has_gen_ai_span = True + + finished_spans.append(span.to_json()) + else: + finished_spans = [ + span.to_json() + for span in self._span_recorder.spans + if span.timestamp is not None + ] + + len_diff = len(self._span_recorder.spans) - len(finished_spans) + dropped_spans = len_diff + self._span_recorder.dropped_spans + + # we do this to break the circular reference of transaction -> span + # recorder -> span -> containing transaction (which is where we started) + # before either the spans or the transaction goes out of scope and has + # to be garbage collected + self._span_recorder = None + + contexts = {} + contexts.update(self._contexts) + contexts.update({"trace": self.get_trace_context()}) + profile_context = self.get_profile_context() + if profile_context is not None: + contexts.update({"profile": profile_context}) + + event: "Event" = { + "type": "transaction", + "transaction": self.name, + "transaction_info": {"source": self.source}, + "contexts": contexts, + "tags": self._tags, + "timestamp": self.timestamp, + "start_timestamp": self.start_timestamp, + "spans": finished_spans, + } + + if dropped_spans > 0: + event["_dropped_spans"] = dropped_spans + + if has_gen_ai_span: + event["_has_gen_ai_span"] = True + + if self._profile is not None and self._profile.valid(): + event["profile"] = self._profile + self._profile = None + + event["measurements"] = self._measurements + + return scope.capture_event(event) + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + """Sets a context. Transactions can have multiple contexts + and they should follow the format described in the "Contexts Interface" + documentation. + + :param key: The name of the context. + :param value: The information about the context. + """ + self._contexts[key] = value + + def set_http_status(self, http_status: int) -> None: + """Sets the status of the Transaction according to the given HTTP status. + + :param http_status: The HTTP status code.""" + super().set_http_status(http_status) + self.set_context("response", {"status_code": http_status}) + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the transaction.""" + rv = super().to_json() + + rv["name"] = self.name + rv["source"] = self.source + rv["sampled"] = self.sampled + + return rv + + def get_trace_context(self) -> "Any": + trace_context = super().get_trace_context() + + if self._data: + trace_context["data"] = self._data + + return trace_context + + def get_baggage(self) -> "Baggage": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with the Transaction. + + The first time a new baggage with Sentry items is made, + it will be frozen.""" + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_transaction(self) + + return self._baggage + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the transaction's sampling decision, according to the following + precedence rules: + + 1. If a sampling decision is passed to `start_transaction` + (`start_transaction(name: "my transaction", sampled: True)`), that + decision will be used, regardless of anything else + + 2. If `traces_sampler` is defined, its decision will be used. It can + choose to keep or ignore any parent sampling decision, or use the + sampling context data to make its own decision or to choose a sample + rate for the transaction. + + 3. If `traces_sampler` is not defined, but there's a parent sampling + decision, the parent sampling decision will be used. + + 4. If `traces_sampler` is not defined and there's no parent sampling + decision, `traces_sample_rate` will be used. + """ + client = sentry_sdk.get_client() + + transaction_description = self._get_log_representation() + + # nothing to do if tracing is disabled + if not has_tracing_enabled(client.options): + self.sampled = False + return + + # if the user has forced a sampling decision by passing a `sampled` + # value when starting the transaction, go with that + if self.sampled is not None: + self.sample_rate = float(self.sampled) + return + + # we would have bailed already if neither `traces_sampler` nor + # `traces_sample_rate` were defined, so one of these should work; prefer + # the hook if so + sample_rate = ( + client.options["traces_sampler"](sampling_context) + if callable(client.options.get("traces_sampler")) + # default inheritance behavior + else ( + sampling_context["parent_sampled"] + if sampling_context["parent_sampled"] is not None + else client.options["traces_sample_rate"] + ) + ) + + # Since this is coming from the user (or from a function provided by the + # user), who knows what we might get. (The only valid values are + # booleans or numbers between 0 and 1.) + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning( + "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( + transaction_description=transaction_description, + ) + ) + self.sampled = False + return + + self.sample_rate = float(sample_rate) + + if client.monitor: + self.sample_rate /= 2**client.monitor.downsample_factor + + # if the function returned 0 (or false), or if `traces_sample_rate` is + # 0, it's a sign the transaction should be dropped + if not self.sample_rate: + logger.debug( + "[Tracing] Discarding {transaction_description} because {reason}".format( + transaction_description=transaction_description, + reason=( + "traces_sampler returned 0 or False" + if callable(client.options.get("traces_sampler")) + else "traces_sample_rate is set to 0" + ), + ) + ) + self.sampled = False + return + + # Now we roll the dice. + self.sampled = self._sample_rand < self.sample_rate + + if self.sampled: + logger.debug( + "[Tracing] Starting {transaction_description}".format( + transaction_description=transaction_description, + ) + ) + else: + logger.debug( + "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( + transaction_description=transaction_description, + sample_rate=self.sample_rate, + ) + ) + + # Private aliases matching StreamedSpan's private API + _get_baggage = get_baggage + _get_trace_context = get_trace_context + + +class NoOpSpan(Span): + def __repr__(self) -> str: + return "<%s>" % self.__class__.__name__ + + @property + def containing_transaction(self) -> "Optional[Transaction]": + return None + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "NoOpSpan": + return NoOpSpan() + + def to_traceparent(self) -> str: + return "" + + def to_baggage(self) -> "Optional[Baggage]": + return None + + def get_baggage(self) -> "Optional[Baggage]": + return None + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + return iter(()) + + def set_tag(self, key: str, value: "Any") -> None: + pass + + def set_data(self, key: str, value: "Any") -> None: + pass + + def update_data(self, data: "Dict[str, Any]") -> None: + pass + + def set_status(self, value: str) -> None: + pass + + def set_http_status(self, http_status: int) -> None: + pass + + def is_success(self) -> bool: + return True + + def to_json(self) -> "Dict[str, Any]": + return {} + + def get_trace_context(self) -> "Any": + return {} + + def get_profile_context(self) -> "Any": + return {} + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """ + The `hub` parameter is deprecated. Please use the `scope` parameter, instead. + """ + pass + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + pass + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + pass + + def init_span_recorder(self, maxlen: int) -> None: + pass + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + pass + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _get_baggage = get_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +if TYPE_CHECKING: + + @overload + def trace( + func: None = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": + # Handles: @trace() and @trace(op="custom") + pass + + @overload + def trace(func: "Callable[P, R]") -> "Callable[P, R]": + # Handles: @trace + pass + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a child span around a function call. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, a default operation will + be assigned based on the template. + :type op: str or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP, SPANTEMPLATE + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + op=OP.DB_QUERY, + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + + # With a custom template + @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) + def calculate_interest_rate(amount, rate, years): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import create_span_decorator + + decorator = create_span_decorator( + op=op, + name=name, + attributes=attributes, + template=template, + ) + + if func: + return decorator(func) + else: + return decorator + + +# Circular imports + +from sentry_sdk.tracing_utils import ( + Baggage, + EnvironHeaders, + _generate_sample_rand, + extract_sentrytrace_data, + has_tracing_enabled, + maybe_create_breadcrumbs_from_span, +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py new file mode 100644 index 0000000000..c2e34a795b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py @@ -0,0 +1,1694 @@ +import contextlib +import functools +import inspect +import os +import re +import sys +import uuid +import warnings +from collections.abc import Mapping, MutableMapping +from datetime import datetime, timedelta, timezone +from random import Random +from urllib.parse import quote, unquote + +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.utils import ( + _is_external_source, + _is_in_project_root, + _module_in_list, + capture_internal_exceptions, + filename_for_module, + is_sentry_url, + is_valid_sample_rate, + logger, + match_regex_list, + qualname_from_function, + safe_repr, + to_string, + try_convert, +) + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union + + from sentry_sdk._types import Attributes + + +SENTRY_TRACE_REGEX = re.compile( + "^[ \t]*" # whitespace + "([0-9a-f]{32})?" # trace_id + "-?([0-9a-f]{16})?" # span_id + "-?([01])?" # sampled + "[ \t]*$" # whitespace +) + + +# This is a normal base64 regex, modified to reflect that fact that we strip the +# trailing = or == off +base64_stripped = ( + # any of the characters in the base64 "alphabet", in multiples of 4 + "([a-zA-Z0-9+/]{4})*" + # either nothing or 2 or 3 base64-alphabet characters (see + # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for + # why there's never only 1 extra character) + "([a-zA-Z0-9+/]{2,3})?" +) + + +class EnvironHeaders(Mapping): # type: ignore + def __init__( + self, + environ: "Mapping[str, str]", + prefix: str = "HTTP_", + ) -> None: + self.environ = environ + self.prefix = prefix + + def __getitem__(self, key: str) -> "Optional[Any]": + return self.environ[self.prefix + key.replace("-", "_").upper()] + + def __len__(self) -> int: + return sum(1 for _ in iter(self)) + + def __iter__(self) -> "Generator[str, None, None]": + for k in self.environ: + if not isinstance(k, str): + continue + + k = k.replace("-", "_").upper() + if not k.startswith(self.prefix): + continue + + yield k[len(self.prefix) :] + + +def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: + """ + Returns True if either traces_sample_rate or traces_sampler is + defined and enable_tracing is set and not false. + """ + if options is None: + return False + + return bool( + options.get("enable_tracing") is not False + and ( + options.get("traces_sample_rate") is not None + or options.get("traces_sampler") is not None + ) + ) + + +def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return False + + return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" + + +def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return True + + return not options.get( + "stream_gen_ai_spans", True + ) and not has_span_streaming_enabled(options) + + +@contextlib.contextmanager +def record_sql_queries( + cursor: "Any", + query: "Any", + params_list: "Any", + paramstyle: "Optional[str]", + executemany: bool, + record_cursor_repr: bool = False, + span_origin: str = "manual", + span_op_override_value: "Optional[str]" = None, +) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": + # TODO: Bring back capturing of params by default + client = sentry_sdk.get_client() + if client.options["_experiments"].get("record_sql_params", False): + if not params_list or params_list == [None]: + params_list = None + + if paramstyle == "pyformat": + paramstyle = "format" + else: + params_list = None + paramstyle = None + + query = _format_sql(cursor, query) + + data = {} + if params_list is not None: + data["db.params"] = params_list + if paramstyle is not None: + data["db.paramstyle"] = paramstyle + if executemany: + data["db.executemany"] = True + if record_cursor_repr and cursor is not None: + data["db.cursor"] = cursor + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message=query, category="query", data=data) + + if has_span_streaming_enabled(client.options): + additional_attributes = {} + if query is not None: + additional_attributes["db.query.text"] = query + + with sentry_sdk.traces.start_span( + name="" if query is None else query, + attributes={ + "sentry.origin": span_origin, + "sentry.op": span_op_override_value + if span_op_override_value + else OP.DB, + **additional_attributes, + }, + ) as span: + yield span + else: + with sentry_sdk.start_span( + op=span_op_override_value if span_op_override_value is not None else OP.DB, + name=query, + origin=span_origin, + ) as span: + for k, v in data.items(): + span.set_data(k, v) + yield span + + +def maybe_create_breadcrumbs_from_span( + scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" +) -> None: + if span.op == OP.DB_REDIS: + scope.add_breadcrumb( + message=span.description, type="redis", category="redis", data=span._tags + ) + + elif span.op == OP.HTTP_CLIENT: + level = None + status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) + if status_code: + if 500 <= status_code <= 599: + level = "error" + elif 400 <= status_code <= 499: + level = "warning" + + if level: + scope.add_breadcrumb( + type="http", category="httplib", data=span._data, level=level + ) + else: + scope.add_breadcrumb(type="http", category="httplib", data=span._data) + + elif span.op == "subprocess": + scope.add_breadcrumb( + type="subprocess", + category="subprocess", + message=span.description, + data=span._data, + ) + + +def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": + try: + return frame.f_code.co_filename + except Exception: + return None + + +def _should_be_included( + is_sentry_sdk_frame: bool, + namespace: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", + abs_path: "Optional[str]", + project_root: "Optional[str]", +) -> bool: + # in_app_include takes precedence over in_app_exclude + should_be_included = _module_in_list(namespace, in_app_include) + should_be_excluded = _is_external_source(abs_path) or _module_in_list( + namespace, in_app_exclude + ) + return not is_sentry_sdk_frame and ( + should_be_included + or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) + ) + + +def add_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", + project_root: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", +) -> None: + """ + Adds OTel compatible source code information to the span + """ + # Find the correct frame + frame: "Union[FrameType, None]" = sys._getframe() + while frame is not None: + abs_path = _get_frame_module_abs_path(frame) + + try: + namespace: "Optional[str]" = frame.f_globals.get("__name__") + except Exception: + namespace = None + + is_sentry_sdk_frame = namespace is not None and namespace.startswith( + "sentry_sdk." + ) + + should_be_included = _should_be_included( + is_sentry_sdk_frame=is_sentry_sdk_frame, + namespace=namespace, + in_app_include=in_app_include, + in_app_exclude=in_app_exclude, + abs_path=abs_path, + project_root=project_root, + ) + if should_be_included: + break + + frame = frame.f_back + else: + frame = None + + # Set the data + if frame is not None: + try: + lineno = frame.f_lineno + except Exception: + lineno = None + if lineno is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_LINENO, lineno) + else: + span.set_attribute("code.line.number", lineno) + + try: + namespace = frame.f_globals.get("__name__") + except Exception: + namespace = None + if namespace is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_NAMESPACE, namespace) + else: + span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) + + filepath = _get_frame_module_abs_path(frame) + if filepath is not None: + if namespace is not None: + in_app_path = filename_for_module(namespace, filepath) + elif project_root is not None and filepath.startswith(project_root): + in_app_path = filepath.replace(project_root, "").lstrip(os.sep) + else: + in_app_path = filepath + + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) + else: + if in_app_path is not None: + span.set_attribute("code.file.path", in_app_path) + + try: + code_function = frame.f_code.co_name + except Exception: + code_function = None + + if code_function is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + else: + span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + + +def add_query_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a database query span + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_query_source = client.options.get("enable_db_query_source", True) + if not should_add_query_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("db_query_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def add_http_request_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a span for an outgoing HTTP request + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_request_source = client.options.get("enable_http_request_source", True) + if not should_add_request_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("http_request_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def extract_sentrytrace_data( + header: "Optional[str]", +) -> "Optional[Dict[str, Union[str, bool, None]]]": + """ + Given a `sentry-trace` header string, return a dictionary of data. + """ + if not header: + return None + + if "," in header: + # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. + parts = [part.strip() for part in header.split(",")] + header = next((part for part in parts if part), "") + + if not header: + return None + + if header.startswith("00-") and header.endswith("-00"): + header = header[3:-3] + + match = SENTRY_TRACE_REGEX.match(header) + if not match: + return None + + trace_id, parent_span_id, sampled_str = match.groups() + parent_sampled = None + + if trace_id: + trace_id = "{:032x}".format(int(trace_id, 16)) + if parent_span_id: + parent_span_id = "{:016x}".format(int(parent_span_id, 16)) + if sampled_str: + parent_sampled = sampled_str != "0" + + return { + "trace_id": trace_id, + "parent_span_id": parent_span_id, + "parent_sampled": parent_sampled, + } + + +def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": + real_sql = None + + # If we're using psycopg2, it could be that we're + # looking at a query that uses Composed objects. Use psycopg2's mogrify + # function to format the query. We lose per-parameter trimming but gain + # accuracy in formatting. + try: + if hasattr(cursor, "mogrify"): + real_sql = cursor.mogrify(sql) + if isinstance(real_sql, bytes): + real_sql = real_sql.decode(cursor.connection.encoding) + except Exception: + real_sql = None + + return real_sql or to_string(sql) + + +class PropagationContext: + """ + The PropagationContext represents the data of a trace in Sentry. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "parent_sampled", + "baggage", + "custom_sampling_context", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + dynamic_sampling_context: "Optional[Dict[str, str]]" = None, + baggage: "Optional[Baggage]" = None, + ) -> None: + self._trace_id = trace_id + """The trace id of the Sentry trace.""" + + self._span_id = span_id + """The span id of the currently executing span.""" + + self.parent_span_id = parent_span_id + """The id of the parent span that started this span. + The parent span could also be a span in an upstream service.""" + + self.parent_sampled = parent_sampled + """Boolean indicator if the parent span was sampled. + Important when the parent span originated in an upstream service, + because we want to sample the whole trace, or nothing from the trace.""" + + self.baggage = baggage + """Parsed baggage header that is used for dynamic sampling decisions.""" + + """DEPRECATED this only exists for backwards compat of constructor.""" + if baggage is None and dynamic_sampling_context is not None: + self.baggage = Baggage(dynamic_sampling_context) + + self.custom_sampling_context: "Optional[dict[str, Any]]" = None + + @classmethod + def from_incoming_data( + cls, incoming_data: "Dict[str, Any]" + ) -> "PropagationContext": + propagation_context = PropagationContext() + normalized_data = normalize_incoming_data(incoming_data) + + sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) + sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) + + # nothing to propagate if no sentry-trace + if sentrytrace_data is None: + return propagation_context + + baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) + baggage = ( + Baggage.from_incoming_header(baggage_header) if baggage_header else None + ) + + if not _should_continue_trace(baggage): + return propagation_context + + propagation_context.update(sentrytrace_data) + if baggage: + propagation_context.baggage = baggage + + propagation_context._fill_sample_rand() + + return propagation_context + + @property + def trace_id(self) -> str: + """The trace id of the Sentry trace.""" + if not self._trace_id: + # New trace, don't fill in sample_rand + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + """The span id of the currently executed span.""" + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + @property + def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": + return self.get_baggage().dynamic_sampling_context() + + def to_traceparent(self) -> str: + return f"{self.trace_id}-{self.span_id}" + + def get_baggage(self) -> "Baggage": + if self.baggage is None: + self.baggage = Baggage.populate_from_propagation_context(self) + return self.baggage + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. + """ + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def update(self, other_dict: "Dict[str, Any]") -> None: + """ + Updates the PropagationContext with data from the given dictionary. + """ + for key, value in other_dict.items(): + try: + setattr(self, key, value) + except AttributeError: + pass + + def _set_custom_sampling_context( + self, custom_sampling_context: "dict[str, Any]" + ) -> None: + self.custom_sampling_context = custom_sampling_context + + def __repr__(self) -> str: + return "".format( + self._trace_id, + self._span_id, + self.parent_span_id, + self.parent_sampled, + self.baggage, + ) + + def _fill_sample_rand(self) -> None: + """ + Ensure that there is a valid sample_rand value in the baggage. + + If there is a valid sample_rand value in the baggage, we keep it. + Otherwise, we generate a sample_rand value according to the following: + + - If we have a parent_sampled value and a sample_rate in the DSC, we compute + a sample_rand value randomly in the range: + - [0, sample_rate) if parent_sampled is True, + - or, in the range [sample_rate, 1) if parent_sampled is False. + + - If either parent_sampled or sample_rate is missing, we generate a random + value in the range [0, 1). + + The sample_rand is deterministically generated from the trace_id, if present. + + This function does nothing if there is no baggage. + """ + if self.baggage is None: + return + + sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) + if sample_rand is not None and 0 <= sample_rand < 1: + # sample_rand is present and valid, so don't overwrite it + return + + # Get the sample rate and compute the transformation that will map the random value + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). + sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) + + try: + sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) + except ValueError: + # ValueError is raised if the interval is invalid, i.e. lower >= upper. + # lower >= upper might happen if the incoming trace's sampled flag + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. + # We cannot generate a sensible sample_rand value in this case. + logger.debug( + f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " + f"and sample_rate={sample_rate}." + ) + return + + self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 + + def _sample_rand(self) -> "Optional[str]": + """Convenience method to get the sample_rand value from the baggage.""" + if self.baggage is None: + return None + + return self.baggage.sentry_items.get("sample_rand") + + +class Baggage: + """ + The W3C Baggage header information (see https://www.w3.org/TR/baggage/). + + Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. + Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but + it is the caller's responsibility to enforce this restriction. + """ + + __slots__ = ("sentry_items", "third_party_items", "mutable") + + SENTRY_PREFIX = "sentry-" + SENTRY_PREFIX_REGEX = re.compile("^sentry-") + + def __init__( + self, + sentry_items: "Dict[str, str]", + third_party_items: str = "", + mutable: bool = True, + ): + self.sentry_items = sentry_items + self.third_party_items = third_party_items + self.mutable = mutable + + @classmethod + def from_incoming_header( + cls, + header: "Optional[str]", + *, + _sample_rand: "Optional[str]" = None, + ) -> "Baggage": + """ + freeze if incoming header already has sentry baggage + """ + sentry_items = {} + third_party_items = "" + mutable = True + + if header: + for item in header.split(","): + if "=" not in item: + continue + + with capture_internal_exceptions(): + item = item.strip() + key, val = item.split("=", 1) + if Baggage.SENTRY_PREFIX_REGEX.match(key): + baggage_key = unquote(key.split("-")[1]) + sentry_items[baggage_key] = unquote(val) + mutable = False + else: + third_party_items += ("," if third_party_items else "") + item + + if _sample_rand is not None: + sentry_items["sample_rand"] = str(_sample_rand) + mutable = False + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": + """ + Deprecated: use populate_from_propagation_context + """ + if scope._propagation_context is None: + return Baggage({}) + + return Baggage.populate_from_propagation_context(scope._propagation_context) + + @classmethod + def populate_from_propagation_context( + cls, propagation_context: "PropagationContext" + ) -> "Baggage": + sentry_items: "Dict[str, str]" = {} + third_party_items = "" + mutable = False + + client = sentry_sdk.get_client() + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options + + sentry_items["trace_id"] = propagation_context.trace_id + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if options.get("traces_sample_rate"): + sentry_items["sample_rate"] = str(options["traces_sample_rate"]) + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def populate_from_transaction( + cls, transaction: "sentry_sdk.tracing.Transaction" + ) -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = transaction.trace_id + sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + transaction.name + and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES + ): + sentry_items["transaction"] = transaction.name + + if transaction.sample_rate is not None: + sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction.sampled is not None: + sentry_items["sampled"] = "true" if transaction.sampled else "false" + + # there's an existing baggage but it was mutable, + # which is why we are creating this new baggage. + # However, if by chance the user put some sentry items in there, give them precedence. + if transaction._baggage and transaction._baggage.sentry_items: + sentry_items.update(transaction._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + @classmethod + def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = segment.trace_id + sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + segment.get_attributes().get("sentry.span.source") + not in LOW_QUALITY_SEGMENT_SOURCES + ) and segment._name: + sentry_items["transaction"] = segment._name + + if segment._sample_rate is not None: + sentry_items["sample_rate"] = str(segment._sample_rate) + + if segment.sampled is not None: + sentry_items["sampled"] = "true" if segment.sampled else "false" + + # There's an existing baggage but it was mutable, which is why we are + # creating this new baggage. + # However, if by chance the user put some sentry items in there, give + # them precedence. + if segment._baggage and segment._baggage.sentry_items: + sentry_items.update(segment._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + def freeze(self) -> None: + self.mutable = False + + def dynamic_sampling_context(self) -> "Dict[str, str]": + header = {} + + for key, item in self.sentry_items.items(): + header[key] = item + + return header + + def serialize(self, include_third_party: bool = False) -> str: + items = [] + + for key, val in self.sentry_items.items(): + with capture_internal_exceptions(): + item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) + items.append(item) + + if include_third_party: + items.append(self.third_party_items) + + return ",".join(items) + + @staticmethod + def strip_sentry_baggage(header: str) -> str: + """Remove Sentry baggage from the given header. + + Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. + """ + return ",".join( + ( + item + for item in header.split(",") + if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) + ) + ) + + def _sample_rand(self) -> "Optional[float]": + """Convenience method to get the sample_rand value from the sentry_items. + + We validate the value and parse it as a float before returning it. The value is considered + valid if it is a float in the range [0, 1). + """ + sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) + + if sample_rand is not None and 0.0 <= sample_rand < 1.0: + return sample_rand + + return None + + def __repr__(self) -> str: + return f'' + + +def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: + """ + Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. + """ + trace_propagation_targets = client.options["trace_propagation_targets"] + + if is_sentry_url(client, url): + return False + + return match_regex_list(url, trace_propagation_targets, substring_matching=True) + + +def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": + """ + Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. + """ + data = {} + for key, value in incoming_data.items(): + if key.startswith("HTTP_"): + key = key[5:] + + key = key.replace("_", "-").lower() + data[key] = value + + return data + + +def create_span_decorator( + op: "Optional[Union[str, OP]]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Any": + """ + Create a span decorator that can wrap both sync and async functions. + + :param op: The operation type for the span. + :type op: str or :py:class:`sentry_sdk.consts.OP` or None + :param name: The name of the span. + :type name: str or None + :param attributes: Additional attributes to set on the span. + :type attributes: dict or None + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + """ + from sentry_sdk.scope import should_send_default_pii + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return await f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return await f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = await f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def create_streaming_span_decorator( + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Any": + """ + Create a span creating decorator that can wrap both sync and async functions. + """ + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + result = await f(*args, **kwargs) + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + return f(*args, **kwargs) + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.span + return current_span + + +def _generate_sample_rand( + trace_id: "Optional[str]", + *, + interval: "tuple[float, float]" = (0.0, 1.0), +) -> float: + """Generate a sample_rand value from a trace ID. + + The generated value will be pseudorandomly chosen from the provided + interval. Specifically, given (lower, upper) = interval, the generated + value will be in the range [lower, upper). The value has 6-digit precision, + so when printing with .6f, the value will never be rounded up. + + The pseudorandom number generator is seeded with the trace ID. + """ + lower, upper = interval + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly + raise ValueError("Invalid interval: lower must be less than upper") + + rng = Random(trace_id) + lower_scaled = int(lower * 1_000_000) + upper_scaled = int(upper * 1_000_000) + try: + sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) + except ValueError: + # In some corner cases it might happen that the range is too small + # In that case, just take the lower bound + sample_rand_scaled = lower_scaled + + return sample_rand_scaled / 1_000_000 + + +def _sample_rand_range( + parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" +) -> "tuple[float, float]": + """ + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values + that a generated sample_rand value must fall into, given the parent_sampled and + sample_rate values. + """ + if parent_sampled is None or sample_rate is None: + return 0.0, 1.0 + elif parent_sampled is True: + return 0.0, sample_rate + else: # parent_sampled is False + return sample_rate, 1.0 + + +def _get_value(source: "Any", key: str) -> "Optional[Any]": + """ + Gets a value from a source object. The source can be a dict or an object. + It is checked for dictionary keys and object attributes. + """ + value = None + if isinstance(source, dict): + value = source.get(key) + else: + if hasattr(source, key): + try: + value = getattr(source, key) + except Exception: + value = None + return value + + +def _get_span_name( + template: "Union[str, SPANTEMPLATE]", + name: str, + kwargs: "Optional[dict[str, Any]]" = None, +) -> str: + """ + Get the name of the span based on the template and the name. + """ + span_name = name + + if template == SPANTEMPLATE.AI_CHAT: + model = None + if kwargs: + for key in ("model", "model_name"): + if kwargs.get(key) and isinstance(kwargs[key], str): + model = kwargs[key] + break + + span_name = f"chat {model}" if model else "chat" + + elif template == SPANTEMPLATE.AI_AGENT: + span_name = f"invoke_agent {name}" + + elif template == SPANTEMPLATE.AI_TOOL: + span_name = f"execute_tool {name}" + + return span_name + + +def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: + """ + Get the operation of the span based on the template. + """ + mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { + SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, + SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, + SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, + } + op = mapping.get(template, OP.FUNCTION) + + return str(op) + + +def _get_input_attributes( + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Get input attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + mapping = { + "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), + "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), + "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), + "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), + "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), + "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), + "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), + "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), + } + + def _set_from_key(key: str, value: "Any") -> None: + if key in mapping: + (attribute, data_type) = mapping[key] + if value is not None and isinstance(value, data_type): + attributes[attribute] = value + + for key, value in list(kwargs.items()): + if key == "prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "user", "content": value} + ) + continue + + if key == "system_prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "system", "content": value} + ) + continue + + _set_from_key(key, value) + + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( + {"args": args, "kwargs": kwargs} + ) + + # Coerce to string + if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] + ) + + return attributes + + +def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": + """ + Get usage attributes. + """ + attributes = {} + + def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: + for key in keys: + value = _get_value(usage, key) + if value is not None and isinstance(value, int): + attributes[attribute] = value + + _set_from_keys( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + ("prompt_tokens", "input_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + ("completion_tokens", "output_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + ("total_tokens",), + ) + + return attributes + + +def _get_output_attributes( + template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> "dict[str, Any]": + """ + Get output attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + with capture_internal_exceptions(): + # Usage from result, result.usage, and result.metadata.usage + usage_candidates = [result] + + usage = _get_value(result, "usage") + usage_candidates.append(usage) + + meta = _get_value(result, "metadata") + usage = _get_value(meta, "usage") + usage_candidates.append(usage) + + for usage_candidate in usage_candidates: + if usage_candidate is not None: + attributes.update(_get_usage_attributes(usage_candidate)) + + # Response model + model_name = _get_value(result, "model") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + model_name = _get_value(result, "model_name") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + # Tool output + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) + + return attributes + + +def _set_input_attributes( + span: "Span", + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + name: str, + f: "Any", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> None: + """ + Set span input attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param f: The wrapped function. + :param args: The arguments to the wrapped function. + :param kwargs: The keyword arguments to the wrapped function. + """ + attributes: "dict[str, Any]" = {} + + if template == SPANTEMPLATE.AI_AGENT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_AGENT_NAME: name, + } + elif template == SPANTEMPLATE.AI_CHAT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + } + elif template == SPANTEMPLATE.AI_TOOL: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: name, + } + + docstring = f.__doc__ + if docstring is not None: + attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring + + attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) + span.update_data(attributes or {}) + + +def _set_output_attributes( + span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> None: + """ + Set span output attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param result: The result of the wrapped function. + """ + span.update_data(_get_output_attributes(template, send_pii, result) or {}) + + +def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: + """ + Check if we should continue the incoming trace according to the strict_trace_continuation spec. + https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation + """ + + client = sentry_sdk.get_client() + parsed_dsn = client.parsed_dsn + client_org_id = parsed_dsn.org_id if parsed_dsn else None + baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None + + if ( + client_org_id is not None + and baggage_org_id is not None + and client_org_id != baggage_org_id + ): + logger.debug( + f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + strict_trace_continuation: bool = client.options.get( + "strict_trace_continuation", False + ) + if strict_trace_continuation: + if (baggage_org_id is not None and client_org_id is None) or ( + baggage_org_id is None and client_org_id is not None + ): + logger.debug( + f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + return True + + +def add_sentry_baggage_to_headers( + headers: "MutableMapping[str, str]", sentry_baggage: str +) -> None: + """Add the Sentry baggage to the headers. + + This function directly mutates the provided headers. The provided sentry_baggage + is appended to the existing baggage. If the baggage already contains Sentry items, + they are stripped out first. + """ + existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") + stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) + + separator = "," if len(stripped_existing_baggage) > 0 else "" + + headers[BAGGAGE_HEADER_NAME] = ( + stripped_existing_baggage + separator + sentry_baggage + ) + + +def _make_sampling_decision( + name: str, + attributes: "Optional[Attributes]", + scope: "sentry_sdk.Scope", +) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": + """ + Decide whether a span should be sampled. + + Returns a tuple with: + 1. the sampling decision + 2. the effective sample rate + 3. the sample rand + 4. the reason for not sampling the span, if unsampled + """ + client = sentry_sdk.get_client() + + if not has_tracing_enabled(client.options): + return False, None, None, None + + propagation_context = scope.get_active_propagation_context() + + sample_rand = None + if propagation_context.baggage is not None: + sample_rand = propagation_context.baggage._sample_rand() + if sample_rand is None: + sample_rand = _generate_sample_rand(propagation_context.trace_id) + + # If there's a traces_sampler, use that; otherwise use traces_sample_rate + traces_sampler_defined = callable(client.options.get("traces_sampler")) + if traces_sampler_defined: + sampling_context = { + "span_context": { + "name": name, + "trace_id": propagation_context.trace_id, + "parent_span_id": propagation_context.parent_span_id, + "parent_sampled": propagation_context.parent_sampled, + "attributes": dict(attributes) if attributes else {}, + }, + } + + if propagation_context.custom_sampling_context: + sampling_context.update(propagation_context.custom_sampling_context) + + sample_rate = client.options["traces_sampler"](sampling_context) + else: + if propagation_context.parent_sampled is not None: + sample_rate = propagation_context.parent_sampled + else: + sample_rate = client.options["traces_sample_rate"] + + # Validate whether the sample_rate we got is actually valid. Since + # traces_sampler is user-provided, it could return anything. + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") + return False, None, None, "sample_rate" + + sample_rate = float(sample_rate) + if not sample_rate: + if traces_sampler_defined: + reason = "traces_sampler returned 0 or False" + else: + reason = "traces_sample_rate is set to 0" + + logger.debug(f"[Tracing] Discarding {name} because {reason}") + return False, 0.0, None, "sample_rate" + + # Adjust sample rate if we're under backpressure + sample_rate_before_backpressure = sample_rate + if client.monitor: + sample_rate /= 2**client.monitor.downsample_factor + + if not sample_rate: + logger.debug(f"[Tracing] Discarding {name} because backpressure") + return False, 0.0, None, "backpressure" + + # Make the actual decision + sampled = sample_rand < sample_rate + + if sampled: + logger.debug(f"[Tracing] Starting {name}") + outcome = None + + else: + # Determine why exactly the span will not be sampled. If we've lowered + # the effective sample_rate because of backpressure, check whether the + # span would've been sampled if backpressure wasn't active. If that's the + # case, backpressure is the actual reason, otherwise just pure sampling + # rate. + if ( + sample_rate_before_backpressure != sample_rate + and sample_rand < sample_rate_before_backpressure + ): + logger.debug(f"[Tracing] Discarding {name} because backpressure") + outcome = "backpressure" + + else: + logger.debug( + f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" + ) + outcome = "sample_rate" + + return sampled, sample_rate, sample_rand, outcome + + +def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: + """Determine if a span fits one of the rules in ignore_spans.""" + client = sentry_sdk.get_client() + ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") + + if not ignore_spans: + return False + + def _matches(rule: "Any", value: "Any") -> bool: + if isinstance(rule, Pattern): + if isinstance(value, str): + return bool(rule.fullmatch(value)) + else: + return False + + return rule == value + + for rule in ignore_spans: + if isinstance(rule, (str, Pattern)): + if _matches(rule, name): + return True + + elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): + name_matches = True + attributes_match = True + + if "name" in rule: + name_matches = _matches(rule["name"], name) + + if "attributes" in rule: + attributes = attributes or {} + + for attribute, value in rule["attributes"].items(): + if attribute not in attributes or not _matches( + value, attributes[attribute] + ): + attributes_match = False + break + + if name_matches and attributes_match: + return True + + return False + + +# Circular imports +from sentry_sdk.traces import ( + LOW_QUALITY_SEGMENT_SOURCES, + StreamedSpan, +) +from sentry_sdk.traces import ( + start_span as start_streaming_span, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + LOW_QUALITY_TRANSACTION_SOURCES, + SENTRY_TRACE_HEADER_NAME, + Span, +) + +if TYPE_CHECKING: + from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py new file mode 100644 index 0000000000..2b71fee429 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py @@ -0,0 +1,1255 @@ +import asyncio +import gzip +import io +import json +import logging +import os +import socket +import ssl +import time +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from urllib.request import getproxies + +try: + import brotli # type: ignore +except ImportError: + brotli = None + +try: + import httpcore +except ImportError: + httpcore = None # type: ignore[assignment,unused-ignore] + +try: + import h2 # noqa: F401 + + HTTP2_ENABLED = httpcore is not None +except ImportError: + HTTP2_ENABLED = False + +try: + import anyio # noqa: F401 + + ASYNC_TRANSPORT_AVAILABLE = httpcore is not None +except ImportError: + ASYNC_TRANSPORT_AVAILABLE = False + +from typing import TYPE_CHECKING, Dict, List, cast + +import certifi +import urllib3 + +import sentry_sdk +from sentry_sdk.consts import EndpointType +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + logger, + mark_sentry_task_internal, +) +from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + DefaultDict, + Iterable, + Mapping, + Optional, + Self, + Tuple, + Type, + Union, + ) + + from urllib3.poolmanager import PoolManager, ProxyManager + + from sentry_sdk._types import Event, EventDataCategory + +KEEP_ALIVE_SOCKET_OPTIONS = [] +for option in [ + (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 +]: + try: + KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) + except AttributeError: + # a specific option might not be available on specific systems, + # e.g. TCP_KEEPIDLE doesn't exist on macOS + pass + + +def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": + """Case-insensitive header lookup for httpcore-style responses.""" + header_lower = header.lower() + return next( + ( + val.decode("ascii") + for key, val in response.headers + if key.decode("ascii").lower() == header_lower + ), + None, + ) + + +class Transport(ABC): + """Baseclass for all transports. + + A transport is used to send an event to sentry. + """ + + parsed_dsn: "Optional[Dsn]" = None + + def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: + self.options = options + if options and options["dsn"]: + self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) + else: + self.parsed_dsn = None + + def capture_event(self: "Self", event: "Event") -> None: + """ + DEPRECATED: Please use capture_envelope instead. + + This gets invoked with the event dictionary when an event should + be sent to sentry. + """ + + warnings.warn( + "capture_event is deprecated, please use capture_envelope instead!", + DeprecationWarning, + stacklevel=2, + ) + + envelope = Envelope() + envelope.add_event(event) + self.capture_envelope(envelope) + + @abstractmethod + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + """ + Send an envelope to Sentry. + + Envelopes are a data container format that can hold any type of data + submitted to Sentry. We use it to send all event data (including errors, + transactions, crons check-ins, etc.) to Sentry. + """ + pass + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Any]" = None, + ) -> None: + """ + Wait `timeout` seconds for the current events to be sent out. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def kill(self: "Self") -> None: + """ + Forcefully kills the transport. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + """This increments a counter for event loss by reason and + data category by the given positive-int quantity (default 1). + + If an item is provided, the data category and quantity are + extracted from the item, and the values passed for + data_category and quantity are ignored. + + When recording a lost transaction via data_category="transaction", + the calling code should also record the lost spans via this method. + When recording lost spans, `quantity` should be set to the number + of contained spans, plus one for the transaction itself. When + passing an Item containing a transaction via the `item` parameter, + this method automatically records the lost spans. + """ + return None + + def is_healthy(self: "Self") -> bool: + return True + + +def _parse_rate_limits( + header: str, now: "Optional[datetime]" = None +) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": + if now is None: + now = datetime.now(timezone.utc) + + for limit in header.split(","): + try: + parameters = limit.strip().split(":") + retry_after_val, categories = parameters[:2] + + retry_after = now + timedelta(seconds=int(retry_after_val)) + for category in categories and categories.split(";") or (None,): + yield category, retry_after # type: ignore + except (LookupError, ValueError): + continue + + +class HttpTransportCore(Transport): + """Shared base class for sync and async transports.""" + + TIMEOUT = 30 # seconds + + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + from sentry_sdk.consts import VERSION + + Transport.__init__(self, options) + assert self.parsed_dsn is not None + self.options: "Dict[str, Any]" = options + self._worker = self._create_worker(options) + self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) + self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} + # We only use this Retry() class for the `get_retry_after` method it exposes + self._retry = urllib3.util.Retry() + self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( + defaultdict(int) + ) + self._last_client_report_sent = time.time() + + self._pool = self._make_pool() + + # Backwards compatibility for deprecated `self.hub_class` attribute + self._hub_cls = sentry_sdk.Hub + + experiments = options.get("_experiments", {}) + compression_level = experiments.get( + "transport_compression_level", + experiments.get("transport_zlib_compression_level"), + ) + compression_algo = experiments.get( + "transport_compression_algo", + ( + "gzip" + # if only compression level is set, assume gzip for backwards compatibility + # if we don't have brotli available, fallback to gzip + if compression_level is not None or brotli is None + else "br" + ), + ) + + if compression_algo == "br" and brotli is None: + logger.warning( + "You asked for brotli compression without the Brotli module, falling back to gzip -9" + ) + compression_algo = "gzip" + compression_level = None + + if compression_algo not in ("br", "gzip"): + logger.warning( + "Unknown compression algo %s, disabling compression", compression_algo + ) + self._compression_level = 0 + self._compression_algo = None + else: + self._compression_algo = compression_algo + + if compression_level is not None: + self._compression_level = compression_level + elif self._compression_algo == "gzip": + self._compression_level = 9 + elif self._compression_algo == "br": + self._compression_level = 4 + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + raise NotImplementedError() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + if not self.options["send_client_reports"]: + return + + if item is not None: + data_category = item.data_category + quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). + + if data_category == "transaction": + # Also record the lost spans + event = item.get_transaction_event() or {} + + # +1 for the transaction itself + span_count = ( + len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 + ) + self.record_lost_event(reason, "span", quantity=span_count) + + elif data_category == "log_item" and item: + # Also record size of lost logs in bytes + bytes_size = len(item.get_bytes()) + self.record_lost_event(reason, "log_byte", quantity=bytes_size) + + elif data_category == "attachment": + # quantity of 0 is actually 1 as we do not want to count + # empty attachments as actually empty. + quantity = len(item.get_bytes()) or 1 + + elif data_category is None: + raise TypeError("data category not provided") + + self._discarded_events[data_category, reason] += quantity + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return response.headers.get(header) + + def _update_rate_limits( + self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" + ) -> None: + # new sentries with more rate limit insights. We honor this header + # no matter of the status code to update our internal rate limits. + header = self._get_header_value(response, "x-sentry-rate-limits") + if header: + logger.warning("Rate-limited via x-sentry-rate-limits") + self._disabled_until.update(_parse_rate_limits(header)) + + # old sentries only communicate global rate limit hits via the + # retry-after header on 429. This header can also be emitted on new + # sentries if a proxy in front wants to globally slow things down. + elif response.status == 429: + logger.warning("Rate-limited via 429") + retry_after_value = self._get_header_value(response, "Retry-After") + retry_after = ( + self._retry.parse_retry_after(retry_after_value) + if retry_after_value is not None + else None + ) or 60 + self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( + seconds=retry_after + ) + + def _handle_request_error( + self: "Self", + envelope: "Optional[Envelope]", + loss_reason: str = "network", + record_reason: str = "network_error", + ) -> None: + def record_loss(reason: str) -> None: + if envelope is None: + self.record_lost_event(reason, data_category="error") + else: + for item in envelope.items: + self.record_lost_event(reason, item=item) + + self.on_dropped_event(loss_reason) + record_loss(record_reason) + + def _handle_response( + self: "Self", + response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", + envelope: "Optional[Envelope]", + ) -> None: + self._update_rate_limits(response) + + if response.status == 413: + size_exceeded_message = ( + "HTTP 413: Event dropped due to exceeded envelope size limit" + ) + response_message = getattr( + response, "data", getattr(response, "content", None) + ) + if response_message is not None: + size_exceeded_message += f" (body: {response_message})" + + logger.error(size_exceeded_message) + self._handle_request_error( + envelope=envelope, loss_reason="status_413", record_reason="send_error" + ) + + elif response.status == 429: + # if we hit a 429. Something was rate limited but we already + # acted on this in `self._update_rate_limits`. Note that we + # do not want to record event loss here as we will have recorded + # an outcome in relay already. + self.on_dropped_event("status_429") + pass + + elif response.status >= 300 or response.status < 200: + logger.error( + "Unexpected status code: %s (body: %s)", + response.status, + getattr(response, "data", getattr(response, "content", None)), + ) + self._handle_request_error( + envelope=envelope, loss_reason="status_{}".format(response.status) + ) + + def _update_headers( + self: "Self", + headers: "Dict[str, str]", + ) -> None: + headers.update( + { + "User-Agent": str(self._auth.client), + "X-Sentry-Auth": str(self._auth.to_header()), + } + ) + + def on_dropped_event(self: "Self", _reason: str) -> None: + return None + + def _fetch_pending_client_report( + self: "Self", force: bool = False, interval: int = 60 + ) -> "Optional[Item]": + if not self.options["send_client_reports"]: + return None + + if not (force or self._last_client_report_sent < time.time() - interval): + return None + + discarded_events = self._discarded_events + self._discarded_events = defaultdict(int) + self._last_client_report_sent = time.time() + + if not discarded_events: + return None + + return Item( + PayloadRef( + json={ + "timestamp": time.time(), + "discarded_events": [ + {"reason": reason, "category": category, "quantity": quantity} + for ( + (category, reason), + quantity, + ) in discarded_events.items() + ], + } + ), + type="client_report", + ) + + def _check_disabled(self, category: str) -> bool: + def _disabled(bucket: "Any") -> bool: + ts = self._disabled_until.get(bucket) + return ts is not None and ts > datetime.now(timezone.utc) + + return _disabled(category) or _disabled(None) + + def _is_rate_limited(self: "Self") -> bool: + return any( + ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() + ) + + def _is_worker_full(self: "Self") -> bool: + return self._worker.full() + + def is_healthy(self: "Self") -> bool: + return not (self._is_worker_full() or self._is_rate_limited()) + + def _prepare_envelope( + self: "Self", envelope: "Envelope" + ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": + # remove all items from the envelope which are over quota + new_items = [] + for item in envelope.items: + if self._check_disabled(item.data_category): + if item.data_category in ("transaction", "error", "default", "statsd"): + self.on_dropped_event("self_rate_limits") + self.record_lost_event("ratelimit_backoff", item=item) + else: + new_items.append(item) + + # Since we're modifying the envelope here make a copy so that others + # that hold references do not see their envelope modified. + envelope = Envelope(headers=envelope.headers, items=new_items) + + if not envelope.items: + return None + + # since we're already in the business of sending out an envelope here + # check if we have one pending for the stats session envelopes so we + # can attach it to this enveloped scheduled for sending. This will + # currently typically attach the client report to the most recent + # session update. + client_report_item = self._fetch_pending_client_report(interval=30) + if client_report_item is not None: + envelope.items.append(client_report_item) + + content_encoding, body = self._serialize_envelope(envelope) + + assert self.parsed_dsn is not None + logger.debug( + "Sending envelope [%s] project:%s host:%s", + envelope.description, + self.parsed_dsn.project_id, + self.parsed_dsn.host, + ) + + headers: "Dict[str, str]" = { + "Content-Type": "application/x-sentry-envelope", + } + if content_encoding: + headers["Content-Encoding"] = content_encoding + + return envelope, body, headers + + def _serialize_envelope( + self: "Self", envelope: "Envelope" + ) -> "tuple[Optional[str], io.BytesIO]": + content_encoding = None + body = io.BytesIO() + if self._compression_level == 0 or self._compression_algo is None: + envelope.serialize_into(body) + else: + content_encoding = self._compression_algo + if self._compression_algo == "br" and brotli is not None: + body.write( + brotli.compress( + envelope.serialize(), quality=self._compression_level + ) + ) + else: # assume gzip as we sanitize the algo value in init + with gzip.GzipFile( + fileobj=body, mode="w", compresslevel=self._compression_level + ) as f: + envelope.serialize_into(f) + + return content_encoding, body + + def _get_httpcore_pool_options( + self: "Self", http2: bool = False + ) -> "Dict[str, Any]": + """Shared pool options for httpcore-based transports (Http2 and Async).""" + options: "Dict[str, Any]" = { + "http2": http2, + "retries": 3, + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations( + self.options["ca_certs"] + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") + key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") + if cert_file is not None: + ssl_context.load_cert_chain(cert_file, key_file) + + options["ssl_context"] = ssl_context + return options + + def _resolve_proxy(self: "Self") -> "Optional[str]": + """Resolve proxy URL from options and environment. Returns proxy URL or None.""" + if self.parsed_dsn is None: + return None + + no_proxy = self._in_no_proxy(self.parsed_dsn) + proxy = None + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + return proxy or None + + @property + def _timeout_extensions(self: "Self") -> "Dict[str, Any]": + return { + "timeout": { + "pool": self.TIMEOUT, + "connect": self.TIMEOUT, + "write": self.TIMEOUT, + "read": self.TIMEOUT, + } + } + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + raise NotImplementedError() + + def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: + no_proxy = getproxies().get("no") + if not no_proxy: + return False + for host in no_proxy.split(","): + host = host.strip() + if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): + return True + return False + + def _make_pool( + self: "Self", + ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + raise NotImplementedError() + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": + raise NotImplementedError() + + def kill(self: "Self") -> None: + logger.debug("Killing HTTP transport") + self._worker.kill() + + +# Keep BaseHttpTransport as an alias for backwards compatibility +# and for the sync transport implementation +class BaseHttpTransport(HttpTransportCore): + """The base HTTP transport (synchronous).""" + + def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + response.close() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return BackgroundWorker(queue_size=options["transport_queue_size"]) + + def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def capture_envelope( + self, + envelope: "Envelope", + ) -> None: + def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + self._send_envelope(envelope) + self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + self._worker.flush(timeout, callback) + + @staticmethod + def _warn_hub_cls() -> None: + """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" + warnings.warn( + "The `hub_cls` attribute is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=3, + ) + + @property + def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + return self._hub_cls + + @hub_cls.setter + def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + self._hub_cls = value + + +class HttpTransport(BaseHttpTransport): + if TYPE_CHECKING: + _pool: "Union[PoolManager, ProxyManager]" + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + num_pools = self.options.get("_experiments", {}).get("transport_num_pools") + options = { + "num_pools": 2 if num_pools is None else int(num_pools), + "cert_reqs": "CERT_REQUIRED", + "timeout": urllib3.Timeout(total=self.TIMEOUT), + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if self.options["keep_alive"]: + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + options["ca_certs"] = ( + self.options["ca_certs"] # User-provided bundle from the SDK init + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + + options["cert_file"] = self.options["cert_file"] or os.environ.get( + "CLIENT_CERT_FILE" + ) + options["key_file"] = self.options["key_file"] or os.environ.get( + "CLIENT_KEY_FILE" + ) + + return options + + def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = None + no_proxy = self._in_no_proxy(self.parsed_dsn) + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + use_socks_proxy = True + try: + # Check if PySocks dependency is available + from urllib3.contrib.socks import SOCKSProxyManager + except ImportError: + use_socks_proxy = False + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", + proxy, + ) + + if use_socks_proxy: + return SOCKSProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + else: + return urllib3.ProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "urllib3.BaseHTTPResponse": + return self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + body=body, + headers=headers, + ) + + +class AsyncHttpTransport(HttpTransportCore): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + if not ASYNC_TRANSPORT_AVAILABLE: + raise RuntimeError( + "AsyncHttpTransport requires httpcore[asyncio]. " + "Install it with: pip install sentry-sdk[asyncio]" + ) + super().__init__(options) + # Requires event loop at init time + self.loop = asyncio.get_running_loop() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return AsyncWorker(queue_size=options["transport_queue_size"]) + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + async def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + await self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + async def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = await self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + await response.aclose() + + async def _request( # type: ignore[override] + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + return await self._pool.request( # type: ignore[misc,unused-ignore] + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + + async def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def _capture_envelope(self: "Self", envelope: "Envelope") -> None: + async def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + await self._send_envelope(envelope) + await self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + # Synchronous entry point + if self.loop and self.loop.is_running(): + self.loop.call_soon_threadsafe(self._capture_envelope, envelope) + else: + # The event loop is no longer running + logger.warning("Async Transport is not running in an event loop.") + self.on_dropped_event("internal_sdk_error") + for item in envelope.items: + self.record_lost_event("internal_sdk_error", item=item) + + def flush( # type: ignore[override] + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> "Optional[asyncio.Task[None]]": + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] + return None + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=HTTP2_ENABLED + and self.parsed_dsn is not None + and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + socks_opts = opts.copy() + if "socket_options" in socks_opts: + socket_options = socks_opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) + + return httpcore.AsyncConnectionPool(**opts) + + def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] + logger.debug("Killing HTTP transport") + self._worker.kill() + try: + # Return the pool cleanup task so caller can await it if needed + with mark_sentry_task_internal(): + return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] + except RuntimeError: + logger.warning("Event loop not running, aborting kill.") + return None + + +if not HTTP2_ENABLED: + # Sorry, no Http2Transport for you + class Http2Transport(HttpTransport): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + super().__init__(options) + logger.warning( + "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." + ) + +else: + + class Http2Transport(BaseHttpTransport): # type: ignore + """The HTTP2 transport based on httpcore.""" + + TIMEOUT = 15 + + if TYPE_CHECKING: + _pool: """Union[ + httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool + ]""" + + def _get_header_value( + self: "Self", response: "httpcore.Response", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + response = self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + return response + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + if "socket_options" in opts: + socket_options = opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.SOCKSProxy(proxy_url=proxy, **opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.HTTPProxy(proxy_url=proxy, **opts) + + return httpcore.ConnectionPool(**opts) + + +class _EnvelopePrinterTransport(Transport): + """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" + + def __init__(self, transport: "Transport") -> None: + Transport.__init__(self, options=transport.options) + self._inner = transport + self.parsed_dsn = transport.parsed_dsn + + self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") + self.envelope_logger.setLevel(logging.INFO) + self.envelope_logger.propagate = False + if not self.envelope_logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + self.envelope_logger.addHandler(handler) + + @property # type: ignore[misc] + def __class__(self) -> type: + return self._inner.__class__ + + def capture_envelope(self, envelope: "Envelope") -> None: + try: + self.envelope_logger.info("--- Sentry Envelope ---") + self.envelope_logger.info( + "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) + ) + for item in envelope.items: + self.envelope_logger.info(" Item type: %s", item.type) + self.envelope_logger.info( + " Item headers: %s", + json.dumps(item.headers, indent=2, default=str), + ) + try: + payload = json.loads(item.get_bytes()) + self.envelope_logger.info( + " Payload:\n%s", + json.dumps(payload, indent=2, default=str), + ) + except (ValueError, TypeError): + self.envelope_logger.info( + " Payload: ", + len(item.get_bytes()), + ) + self.envelope_logger.info("--- End Envelope ---") + except Exception: + pass + + self._inner.capture_envelope(envelope) + + def flush( + self, + timeout: float, + callback: "Optional[Any]" = None, + ) -> "Any": + return self._inner.flush(timeout, callback) + + def kill(self) -> "Any": + return self._inner.kill() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + self._inner.record_lost_event(reason, data_category, item, quantity=quantity) + + def is_healthy(self) -> bool: + return self._inner.is_healthy() + + def __getattr__(self, name: str) -> "Any": + return getattr(self._inner, name) + + +class _FunctionTransport(Transport): + """ + DEPRECATED: Users wishing to provide a custom transport should subclass + the Transport class, rather than providing a function. + """ + + def __init__( + self, + func: "Callable[[Event], None]", + ) -> None: + Transport.__init__(self) + self._func = func + + def capture_event( + self, + event: "Event", + ) -> None: + self._func(event) + return None + + def capture_envelope(self, envelope: "Envelope") -> None: + # Since function transports expect to be called with an event, we need + # to iterate over the envelope and call the function for each event, via + # the deprecated capture_event method. + event = envelope.get_event() + if event is not None: + self.capture_event(event) + + +def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": + ref_transport = options["transport"] + + use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) + use_async_transport = options.get("_experiments", {}).get("transport_async", False) + async_integration = any( + integration.__class__.__name__ == "AsyncioIntegration" + for integration in options.get("integrations") or [] + ) + + # By default, we use the http transport class + transport_cls: "Type[Transport]" = ( + Http2Transport if use_http2_transport else HttpTransport + ) + + if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: + try: + asyncio.get_running_loop() + if async_integration: + if use_http2_transport: + logger.warning( + "HTTP/2 transport is not supported with async transport. " + "Ignoring transport_http2 experiment." + ) + transport_cls = AsyncHttpTransport + else: + logger.warning( + "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." + ) + except RuntimeError: + # No event loop running, fall back to sync transport + logger.warning("No event loop running, falling back to sync transport.") + elif use_async_transport: + logger.warning( + "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." + ) + + transport: "Optional[Transport]" = None + + if isinstance(ref_transport, Transport): + transport = ref_transport + elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): + transport_cls = ref_transport + elif callable(ref_transport): + warnings.warn( + "Function transports are deprecated and will be removed in a future release." + "Please provide a Transport instance or subclass, instead.", + DeprecationWarning, + stacklevel=2, + ) + transport = _FunctionTransport(ref_transport) + + # if a transport class is given only instantiate it if the dsn is not + # empty or None + if transport is None and options["dsn"]: + transport = transport_cls(options) + + if transport is not None and os.environ.get( + "SENTRY_PRINT_ENVELOPES", "" + ).lower() in ("1", "true", "yes"): + transport = _EnvelopePrinterTransport(transport) + + return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py new file mode 100644 index 0000000000..dff91e3719 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py @@ -0,0 +1,52 @@ +""" +This module contains type definitions for the Sentry SDK's public API. +The types are re-exported from the internal module `sentry_sdk._types`. + +Disclaimer: Since types are a form of documentation, type definitions +may change in minor releases. Removing a type would be considered a +breaking change, and so we will only remove type definitions in major +releases. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Re-export types to make them available in the public API + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + EventDataCategory, + Hint, + Log, + Metric, + MonitorConfig, + SamplingContext, + ) +else: + from typing import Any + + # The lines below allow the types to be imported from outside `if TYPE_CHECKING` + # guards. The types in this module are only intended to be used for type hints. + Breadcrumb = Any + BreadcrumbHint = Any + Event = Any + EventDataCategory = Any + Hint = Any + Log = Any + MonitorConfig = Any + SamplingContext = Any + Metric = Any + + +__all__ = ( + "Breadcrumb", + "BreadcrumbHint", + "Event", + "EventDataCategory", + "Hint", + "Log", + "MonitorConfig", + "SamplingContext", + "Metric", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py new file mode 100644 index 0000000000..0963015351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py @@ -0,0 +1,2178 @@ +import base64 +import copy +import json +import linecache +import logging +import math +import os +import random +import re +import subprocess +import sys +import threading +import time +from collections import namedtuple +from contextlib import contextmanager +from datetime import datetime, timezone +from decimal import Decimal +from functools import partial, partialmethod, wraps +from numbers import Real +from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit + +try: + # Python 3.11 + from builtins import BaseExceptionGroup +except ImportError: + # Python 3.10 and below + BaseExceptionGroup = None # type: ignore + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._compat import PY37 +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue +from sentry_sdk.consts import ( + DEFAULT_ADD_FULL_STACK, + DEFAULT_MAX_STACK_FRAMES, + EndpointType, +) + +if TYPE_CHECKING: + from types import FrameType, TracebackType + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Iterator, + List, + NoReturn, + Optional, + ParamSpec, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from gevent.hub import Hub + + from sentry_sdk._types import ( + AttributeValue, + Event, + ExcInfo, + Hint, + Log, + Metric, + SerializedAttributeValue, + SpanJSON, + ) + + P = ParamSpec("P") + R = TypeVar("R") + + +epoch = datetime(1970, 1, 1) + +# The logger is created here but initialized in the debug support module +logger = logging.getLogger("sentry_sdk.errors") + +_installed_modules = None + +BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") + +FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) +TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) + +MAX_STACK_FRAMES = 2000 +"""Maximum number of stack frames to send to Sentry. + +If we have more than this number of stack frames, we will stop processing +the stacktrace to avoid getting stuck in a long-lasting loop. This value +exceeds the default sys.getrecursionlimit() of 1000, so users will only +be affected by this limit if they have a custom recursion limit. +""" + + +def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": + """Casts an ENV variable value to boolean using the constants defined above. + In strict mode, it may return None if the value doesn't match any of the predefined values. + """ + normalized = str(value).lower() if value is not None else None + + if normalized in FALSY_ENV_VALUES: + return False + + if normalized in TRUTHY_ENV_VALUES: + return True + + return None if strict else bool(value) + + +def json_dumps(data: "Any") -> bytes: + """Serialize data into a compact JSON representation encoded as UTF-8.""" + return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") + + +def get_git_revision() -> "Optional[str]": + try: + with open(os.path.devnull, "w+") as null: + # prevent command prompt windows from popping up on windows + startupinfo = None + if sys.platform == "win32" or sys.platform == "cygwin": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + revision = ( + subprocess.Popen( + ["git", "rev-parse", "HEAD"], + startupinfo=startupinfo, + stdout=subprocess.PIPE, + stderr=null, + stdin=null, + ) + .communicate()[0] + .strip() + .decode("utf-8") + ) + except (OSError, IOError, FileNotFoundError): + return None + + return revision + + +def get_default_release() -> "Optional[str]": + """Try to guess a default release.""" + release = os.environ.get("SENTRY_RELEASE") + if release: + return release + + release = get_git_revision() + if release: + return release + + for var in ( + "HEROKU_BUILD_COMMIT", + "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility + "SOURCE_VERSION", + "CODEBUILD_RESOLVED_SOURCE_VERSION", + "CIRCLE_SHA1", + "GAE_DEPLOYMENT_ID", + "K_REVISION", + ): + release = os.environ.get(var) + if release: + return release + return None + + +def get_sdk_name(installed_integrations: "List[str]") -> str: + """Return the SDK name including the name of the used web framework.""" + + # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier + # here because if django is not installed the integration is not accessible. + framework_integrations = [ + "django", + "flask", + "fastapi", + "bottle", + "falcon", + "quart", + "sanic", + "starlette", + "litestar", + "starlite", + "chalice", + "serverless", + "pyramid", + "tornado", + "aiohttp", + "aws_lambda", + "gcp", + "beam", + "asgi", + "wsgi", + ] + + for integration in framework_integrations: + if integration in installed_integrations: + return "sentry.python.{}".format(integration) + + return "sentry.python" + + +class CaptureInternalException: + __slots__ = () + + def __enter__(self) -> "ContextManager[Any]": + return self + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> bool: + if ty is not None and value is not None: + capture_internal_exception((ty, value, tb)) + + return True + + +_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() + + +def capture_internal_exceptions() -> "ContextManager[Any]": + return _CAPTURE_INTERNAL_EXCEPTION + + +def capture_internal_exception(exc_info: "ExcInfo") -> None: + """ + Capture an exception that is likely caused by a bug in the SDK + itself. + + These exceptions do not end up in Sentry and are just logged instead. + """ + if sentry_sdk.get_client().is_active(): + logger.error("Internal error in sentry_sdk", exc_info=exc_info) + + +def to_timestamp(value: "datetime") -> float: + return (value - epoch).total_seconds() + + +def format_timestamp(value: "datetime") -> str: + """Formats a timestamp in RFC 3339 format. + + Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. + """ + utctime = value.astimezone(timezone.utc) + + # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for + # several years now), and isoformat is slightly different. + return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +ISO_TZ_SEPARATORS = frozenset(("+", "-")) + + +def datetime_from_isoformat(value: str) -> "datetime": + try: + result = datetime.fromisoformat(value) + except (AttributeError, ValueError): + # py 3.6 + timestamp_format = ( + "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" + ) + if value.endswith("Z"): + value = value[:-1] + "+0000" + + if value[-6] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + value = value[:-3] + value[-2:] + elif value[-5] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + + result = datetime.strptime(value, timestamp_format) + return result.astimezone(timezone.utc) + + +def event_hint_with_exc_info( + exc_info: "Optional[ExcInfo]" = None, +) -> "Dict[str, Optional[ExcInfo]]": + """Creates a hint with the exc info filled in.""" + if exc_info is None: + exc_info = sys.exc_info() + else: + exc_info = exc_info_from_error(exc_info) + if exc_info[0] is None: + exc_info = None + return {"exc_info": exc_info} + + +class BadDsn(ValueError): + """Raised on invalid DSNs.""" + + +class Dsn: + """Represents a DSN.""" + + ORG_ID_REGEX = re.compile(r"^o(\d+)\.") + + def __init__( + self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None + ) -> None: + if isinstance(value, Dsn): + self.__dict__ = dict(value.__dict__) + return + parts = urlsplit(str(value)) + + if parts.scheme not in ("http", "https"): + raise BadDsn("Unsupported scheme %r" % parts.scheme) + self.scheme = parts.scheme + + if parts.hostname is None: + raise BadDsn("Missing hostname") + + self.host = parts.hostname + + if org_id is not None: + self.org_id: "Optional[str]" = org_id + else: + org_id_match = Dsn.ORG_ID_REGEX.match(self.host) + self.org_id = org_id_match.group(1) if org_id_match else None + + if parts.port is None: + self.port: int = self.scheme == "https" and 443 or 80 + else: + self.port = parts.port + + if not parts.username: + raise BadDsn("Missing public key") + + self.public_key = parts.username + self.secret_key = parts.password + + path = parts.path.rsplit("/", 1) + + try: + self.project_id = str(int(path.pop())) + except (ValueError, TypeError): + raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) + + self.path = "/".join(path) + "/" + + @property + def netloc(self) -> str: + """The netloc part of a DSN.""" + rv = self.host + if (self.scheme, self.port) not in (("http", 80), ("https", 443)): + rv = "%s:%s" % (rv, self.port) + return rv + + def to_auth(self, client: "Optional[Any]" = None) -> "Auth": + """Returns the auth info object for this dsn.""" + return Auth( + scheme=self.scheme, + host=self.netloc, + path=self.path, + project_id=self.project_id, + public_key=self.public_key, + secret_key=self.secret_key, + client=client, + ) + + def __str__(self) -> str: + return "%s://%s%s@%s%s%s" % ( + self.scheme, + self.public_key, + self.secret_key and "@" + self.secret_key or "", + self.netloc, + self.path, + self.project_id, + ) + + +class Auth: + """Helper object that represents the auth info.""" + + def __init__( + self, + scheme: str, + host: str, + project_id: str, + public_key: str, + secret_key: "Optional[str]" = None, + version: int = 7, + client: "Optional[Any]" = None, + path: str = "/", + ) -> None: + self.scheme = scheme + self.host = host + self.path = path + self.project_id = project_id + self.public_key = public_key + self.secret_key = secret_key + self.version = version + self.client = client + + def get_api_url( + self, + type: "EndpointType" = EndpointType.ENVELOPE, + ) -> str: + """Returns the API url for storing events.""" + return "%s://%s%sapi/%s/%s/" % ( + self.scheme, + self.host, + self.path, + self.project_id, + type.value, + ) + + def to_header(self) -> str: + """Returns the auth header a string.""" + rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] + if self.client is not None: + rv.append(("sentry_client", self.client)) + if self.secret_key is not None: + rv.append(("sentry_secret", self.secret_key)) + return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) + + +def get_type_name(cls: "Optional[type]") -> "Optional[str]": + return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) + + +def get_type_module(cls: "Optional[type]") -> "Optional[str]": + mod = getattr(cls, "__module__", None) + if mod not in (None, "builtins", "__builtins__"): + return mod + return None + + +def should_hide_frame(frame: "FrameType") -> bool: + try: + mod = frame.f_globals["__name__"] + if mod.startswith("sentry_sdk."): + return True + except (AttributeError, KeyError): + pass + + for flag_name in "__traceback_hide__", "__tracebackhide__": + try: + if frame.f_locals[flag_name]: + return True + except Exception: + pass + + return False + + +def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": + tb_: "Optional[TracebackType]" = tb + while tb_ is not None: + if not should_hide_frame(tb_.tb_frame): + yield tb_ + tb_ = tb_.tb_next + + +def get_lines_from_file( + filename: str, + lineno: int, + max_length: "Optional[int]" = None, + loader: "Optional[Any]" = None, + module: "Optional[str]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + context_lines = 5 + source = None + if loader is not None and hasattr(loader, "get_source"): + try: + source_str: "Optional[str]" = loader.get_source(module) + except (ImportError, IOError): + source_str = None + if source_str is not None: + source = source_str.splitlines() + + if source is None: + try: + source = linecache.getlines(filename) + except (OSError, IOError): + return [], None, [] + + if not source: + return [], None, [] + + lower_bound = max(0, lineno - context_lines) + upper_bound = min(lineno + 1 + context_lines, len(source)) + + try: + pre_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[lower_bound:lineno] + ] + context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) + post_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[(lineno + 1) : upper_bound] + ] + return pre_context, context_line, post_context + except IndexError: + # the file may have changed since it was loaded into memory + return [], None, [] + + +def get_source_context( + frame: "FrameType", + tb_lineno: "Optional[int]", + max_value_length: "Optional[int]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + try: + abs_path: "Optional[str]" = frame.f_code.co_filename + except Exception: + abs_path = None + try: + module = frame.f_globals["__name__"] + except Exception: + return [], None, [] + try: + loader = frame.f_globals["__loader__"] + except Exception: + loader = None + + if tb_lineno is not None and abs_path: + lineno = tb_lineno - 1 + return get_lines_from_file( + abs_path, lineno, max_value_length, loader=loader, module=module + ) + + return [], None, [] + + +def safe_str(value: "Any") -> str: + try: + return str(value) + except Exception: + return safe_repr(value) + + +def safe_repr(value: "Any") -> str: + try: + return repr(value) + except Exception: + return "" + + +def filename_for_module( + module: "Optional[str]", abs_path: "Optional[str]" +) -> "Optional[str]": + if not abs_path or not module: + return abs_path + + try: + if abs_path.endswith(".pyc"): + abs_path = abs_path[:-1] + + base_module = module.split(".", 1)[0] + if base_module == module: + return os.path.basename(abs_path) + + base_module_path = sys.modules[base_module].__file__ + if not base_module_path: + return abs_path + + return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( + os.sep + ) + except Exception: + return abs_path + + +def serialize_frame( + frame: "FrameType", + tb_lineno: "Optional[int]" = None, + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, +) -> "Dict[str, Any]": + f_code = getattr(frame, "f_code", None) + if not f_code: + abs_path = None + function = None + else: + abs_path = frame.f_code.co_filename + function = frame.f_code.co_name + try: + module = frame.f_globals["__name__"] + except Exception: + module = None + + if tb_lineno is None: + tb_lineno = frame.f_lineno + + try: + os_abs_path = os.path.abspath(abs_path) if abs_path else None + except Exception: + os_abs_path = None + + rv: "Dict[str, Any]" = { + "filename": filename_for_module(module, abs_path) or None, + "abs_path": os_abs_path, + "function": function or "", + "module": module, + "lineno": tb_lineno, + } + + if include_source_context: + rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( + frame, tb_lineno, max_value_length + ) + + if include_local_variables: + from sentry_sdk.serializer import serialize + + rv["vars"] = serialize( + dict(frame.f_locals), is_vars=True, custom_repr=custom_repr + ) + + return rv + + +def current_stacktrace( + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, +) -> "Dict[str, Any]": + __tracebackhide__ = True + frames = [] + + f: "Optional[FrameType]" = sys._getframe() + while f is not None: + if not should_hide_frame(f): + frames.append( + serialize_frame( + f, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + ) + ) + f = f.f_back + + frames.reverse() + + return {"frames": frames} + + +def get_errno(exc_value: BaseException) -> "Optional[Any]": + return getattr(exc_value, "errno", None) + + +def get_error_message(exc_value: "Optional[BaseException]") -> str: + message: str = safe_str( + getattr(exc_value, "message", "") + or getattr(exc_value, "detail", "") + or safe_str(exc_value) + ) + + # __notes__ should be a list of strings when notes are added + # via add_note, but can be anything else if __notes__ is set + # directly. We only support strings in __notes__, since that + # is the correct use. + notes: object = getattr(exc_value, "__notes__", None) + if isinstance(notes, list) and len(notes) > 0: + message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) + + return message + + +def single_exception_from_error_tuple( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: "Optional[int]" = None, + parent_id: "Optional[int]" = None, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "Dict[str, Any]": + """ + Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + """ + exception_value: "Dict[str, Any]" = {} + exception_value["mechanism"] = ( + mechanism.copy() if mechanism else {"type": "generic", "handled": True} + ) + if exception_id is not None: + exception_value["mechanism"]["exception_id"] = exception_id + + if exc_value is not None: + errno = get_errno(exc_value) + else: + errno = None + + if errno is not None: + exception_value["mechanism"].setdefault("meta", {}).setdefault( + "errno", {} + ).setdefault("number", errno) + + if source is not None: + exception_value["mechanism"]["source"] = source + + is_root_exception = exception_id == 0 + if not is_root_exception and parent_id is not None: + exception_value["mechanism"]["parent_id"] = parent_id + exception_value["mechanism"]["type"] = "chained" + + if is_root_exception and "type" not in exception_value["mechanism"]: + exception_value["mechanism"]["type"] = "generic" + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + if is_exception_group: + exception_value["mechanism"]["is_exception_group"] = True + + exception_value["module"] = get_type_module(exc_type) + exception_value["type"] = get_type_name(exc_type) + exception_value["value"] = get_error_message(exc_value) + + if client_options is None: + include_local_variables = True + include_source_context = True + max_value_length = None # fallback + custom_repr = None + else: + include_local_variables = client_options["include_local_variables"] + include_source_context = client_options["include_source_context"] + max_value_length = client_options["max_value_length"] + custom_repr = client_options.get("custom_repr") + + frames: "List[Dict[str, Any]]" = [ + serialize_frame( + tb.tb_frame, + tb_lineno=tb.tb_lineno, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + custom_repr=custom_repr, + ) + # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on + # processing a super-long stacktrace. + for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) + ] + + if len(frames) > MAX_STACK_FRAMES: + # If we have more frames than the limit, we remove the stacktrace completely. + # We don't trim the stacktrace here because we have not processed the whole + # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would + # intelligently trim by removing frames in the middle of the stacktrace, but + # since we don't have the whole stacktrace, we can't do that. Instead, we + # drop the entire stacktrace. + exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( + value=None + ) + + elif frames: + if not full_stack: + new_frames = frames + else: + new_frames = merge_stack_frames(frames, full_stack, client_options) + + exception_value["stacktrace"] = {"frames": new_frames} + + return exception_value + + +HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") + +if HAS_CHAINED_EXCEPTIONS: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + exc_type, exc_value, tb = exc_info + + seen_exceptions = [] + seen_exception_ids: "Set[int]" = set() + + while ( + exc_type is not None + and exc_value is not None + and id(exc_value) not in seen_exception_ids + ): + yield exc_type, exc_value, tb + + # Avoid hashing random types we don't know anything + # about. Use the list to keep a ref so that the `id` is + # not used for another object. + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + if exc_value.__suppress_context__: + cause = exc_value.__cause__ + else: + cause = exc_value.__context__ + if cause is None: + break + exc_type = type(cause) + exc_value = cause + tb = getattr(cause, "__traceback__", None) + +else: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + yield exc_info + + +def exceptions_from_error( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: int = 0, + parent_id: int = 0, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, + seen_exceptions: "Optional[list[BaseException]]" = None, + seen_exception_ids: "Optional[Set[int]]" = None, +) -> "Tuple[int, List[Dict[str, Any]]]": + """ + Creates the list of exceptions. + This can include chained exceptions and exceptions from an ExceptionGroup. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + + Args: + exception_id (int): + + Sequential counter for assigning ``mechanism.exception_id`` + to each processed exception. Is NOT the result of calling `id()` on the exception itself. + + parent_id (int): + + The ``mechanism.exception_id`` of the parent exception. + + Written into ``mechanism.parent_id`` in the event payload so Sentry can + reconstruct the exception tree. + + Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` + values for cycle detection. + """ + + if seen_exception_ids is None: + seen_exception_ids = set() + + if seen_exceptions is None: + seen_exceptions = [] + + if exc_value is not None and id(exc_value) in seen_exception_ids: + return (exception_id, []) + + if exc_value is not None: + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + parent = single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source=source, + full_stack=full_stack, + ) + exceptions = [parent] + + parent_id = exception_id + exception_id += 1 + + should_supress_context = ( + hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore + ) + if should_supress_context: + # Add direct cause. + # The field `__cause__` is set when raised with the exception (using the `from` keyword). + exception_has_cause = ( + exc_value + and hasattr(exc_value, "__cause__") + and exc_value.__cause__ is not None + ) + if exception_has_cause: + cause = exc_value.__cause__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(cause), + exc_value=cause, + tb=getattr(cause, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__cause__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + else: + # Add indirect cause. + # The field `__context__` is assigned if another exception occurs while handling the exception. + exception_has_content = ( + exc_value + and hasattr(exc_value, "__context__") + and exc_value.__context__ is not None + ) + if exception_has_content: + context = exc_value.__context__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(context), + exc_value=context, + tb=getattr(context, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__context__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + # Add exceptions from an ExceptionGroup. + is_exception_group = exc_value and hasattr(exc_value, "exceptions") + if is_exception_group: + for idx, e in enumerate(exc_value.exceptions): # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(e), + exc_value=e, + tb=getattr(e, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source="exceptions[%s]" % idx, + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + return (exception_id, exceptions) + + +def exceptions_from_error_tuple( + exc_info: "ExcInfo", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "List[Dict[str, Any]]": + exc_type, exc_value, tb = exc_info + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + + if is_exception_group: + (_, exceptions) = exceptions_from_error( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=0, + parent_id=0, + full_stack=full_stack, + ) + + else: + exceptions = [] + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + exceptions.append( + single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + full_stack=full_stack, + ) + ) + + exceptions.reverse() + + return exceptions + + +def to_string(value: str) -> str: + try: + return str(value) + except UnicodeDecodeError: + return repr(value)[1:-1] + + +def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": + if "stacktrace" in event: + yield event["stacktrace"] + if "threads" in event: + for thread in event["threads"].get("values") or (): + if "stacktrace" in thread: + yield thread["stacktrace"] + if "exception" in event: + for exception in event["exception"].get("values") or (): + if isinstance(exception, dict) and "stacktrace" in exception: + yield exception["stacktrace"] + + +def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + for frame in stacktrace.get("frames") or (): + yield frame + + +def handle_in_app( + event: "Event", + in_app_exclude: "Optional[List[str]]" = None, + in_app_include: "Optional[List[str]]" = None, + project_root: "Optional[str]" = None, +) -> "Event": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + set_in_app_in_frames( + stacktrace.get("frames"), + in_app_exclude=in_app_exclude, + in_app_include=in_app_include, + project_root=project_root, + ) + + return event + + +def set_in_app_in_frames( + frames: "Any", + in_app_exclude: "Optional[List[str]]", + in_app_include: "Optional[List[str]]", + project_root: "Optional[str]" = None, +) -> "Optional[Any]": + if not frames: + return None + + for frame in frames: + # if frame has already been marked as in_app, skip it + current_in_app = frame.get("in_app") + if current_in_app is not None: + continue + + module = frame.get("module") + + # check if module in frame is in the list of modules to include + if _module_in_list(module, in_app_include): + frame["in_app"] = True + continue + + # check if module in frame is in the list of modules to exclude + if _module_in_list(module, in_app_exclude): + frame["in_app"] = False + continue + + # if frame has no abs_path, skip further checks + abs_path = frame.get("abs_path") + if abs_path is None: + continue + + if _is_external_source(abs_path): + frame["in_app"] = False + continue + + if _is_in_project_root(abs_path, project_root): + frame["in_app"] = True + continue + + return frames + + +def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": + if isinstance(error, tuple) and len(error) == 3: + exc_type, exc_value, tb = error + elif isinstance(error, BaseException): + tb = getattr(error, "__traceback__", None) + if tb is not None: + exc_type = type(error) + exc_value = error + else: + exc_type, exc_value, tb = sys.exc_info() + if exc_value is not error: + tb = None + exc_value = error + exc_type = type(error) + + else: + raise ValueError("Expected Exception object to report, got %s!" % type(error)) + + exc_info = (exc_type, exc_value, tb) + + if TYPE_CHECKING: + # This cast is safe because exc_type and exc_value are either both + # None or both not None. + exc_info = cast(ExcInfo, exc_info) + + return exc_info + + +def merge_stack_frames( + frames: "List[Dict[str, Any]]", + full_stack: "List[Dict[str, Any]]", + client_options: "Optional[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Add the missing frames from full_stack to frames and return the merged list. + """ + frame_ids = { + ( + frame["abs_path"], + frame["context_line"], + frame["lineno"], + frame["function"], + ) + for frame in frames + } + + new_frames = [ + stackframe + for stackframe in full_stack + if ( + stackframe["abs_path"], + stackframe["context_line"], + stackframe["lineno"], + stackframe["function"], + ) + not in frame_ids + ] + new_frames.extend(frames) + + # Limit the number of frames + max_stack_frames = ( + client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) + if client_options + else None + ) + if max_stack_frames is not None: + new_frames = new_frames[len(new_frames) - max_stack_frames :] + + return new_frames + + +def event_from_exception( + exc_info: "Union[BaseException, ExcInfo]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, +) -> "Tuple[Event, Dict[str, Any]]": + exc_info = exc_info_from_error(exc_info) + hint = event_hint_with_exc_info(exc_info) + + if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): + full_stack = current_stacktrace( + include_local_variables=client_options["include_local_variables"], + max_value_length=client_options["max_value_length"], + )["frames"] + else: + full_stack = None + + return ( + { + "level": "error", + "exception": { + "values": exceptions_from_error_tuple( + exc_info, client_options, mechanism, full_stack + ) + }, + }, + hint, + ) + + +def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: + if name is None: + return False + + if not items: + return False + + for item in items: + if item == name or name.startswith(item + "."): + return True + + return False + + +def _is_external_source(abs_path: "Optional[str]") -> bool: + # check if frame is in 'site-packages' or 'dist-packages' + if abs_path is None: + return False + + external_source = ( + re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None + ) + return external_source + + +def _is_in_project_root( + abs_path: "Optional[str]", project_root: "Optional[str]" +) -> bool: + if abs_path is None or project_root is None: + return False + + # check if path is in the project root + if abs_path.startswith(project_root): + return True + + return False + + +def _truncate_by_bytes(string: str, max_bytes: int) -> str: + """ + Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. + """ + truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") + + return truncated + "..." + + +def _get_size_in_bytes(value: str) -> "Optional[int]": + try: + return len(value.encode("utf-8")) + except (UnicodeEncodeError, UnicodeDecodeError): + return None + + +def strip_string( + value: str, max_length: "Optional[int]" = None +) -> "Union[AnnotatedValue, str]": + if not value or max_length is None: + return value + + byte_size = _get_size_in_bytes(value) + text_size = len(value) + + if byte_size is not None and byte_size > max_length: + # truncate to max_length bytes, preserving code points + truncated_value = _truncate_by_bytes(value, max_length) + elif text_size is not None and text_size > max_length: + # fallback to truncating by string length + truncated_value = value[: max_length - 3] + "..." + else: + return value + + return AnnotatedValue( + value=truncated_value, + metadata={ + "len": byte_size or text_size, + "rem": [["!limit", "x", max_length - 3, max_length]], + }, + ) + + +def parse_version(version: str) -> "Optional[Tuple[int, ...]]": + """ + Parses a version string into a tuple of integers. + This uses the parsing loging from PEP 440: + https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + """ + VERSION_PATTERN = r""" # noqa: N806 + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+                [-_\.]?
+                (?P(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+            (?P                                         # post release
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?Ppost|rev|r)
+                    [-_\.]?
+                    (?P[0-9]+)?
+                )
+            )?
+            (?P                                          # dev release
+                [-_\.]?
+                (?Pdev)
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+        )
+        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+    """
+
+    pattern = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    try:
+        release = pattern.match(version).groupdict()["release"]  # type: ignore
+        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
+    except (TypeError, ValueError, AttributeError):
+        return None
+
+    return release_tuple
+
+
+def _is_contextvars_broken() -> bool:
+    """
+    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
+    """
+    try:
+        import gevent
+        from gevent.monkey import is_object_patched
+
+        # Get the MAJOR and MINOR version numbers of Gevent
+        version_tuple = tuple(
+            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
+        )
+        if is_object_patched("threading", "local"):
+            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
+            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
+            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
+            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
+            # for contextvars, is able to patch both thread locals and contextvars, in
+            # that case, check if contextvars are effectively patched.
+            if (
+                # Gevent 20.9.0+
+                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
+                # Gevent 20.5.0+ or Python < 3.7
+                or (is_object_patched("contextvars", "ContextVar"))
+            ):
+                return False
+
+            return True
+    except ImportError:
+        pass
+
+    try:
+        import greenlet
+        from eventlet.patcher import is_monkey_patched  # type: ignore
+
+        greenlet_version = parse_version(greenlet.__version__)
+
+        if greenlet_version is None:
+            logger.error(
+                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
+            )
+            return False
+
+        if is_monkey_patched("thread") and greenlet_version < (0, 5):
+            return True
+    except ImportError:
+        pass
+
+    return False
+
+
+def _make_threadlocal_contextvars(local: type) -> type:
+    class ContextVar:
+        # Super-limited impl of ContextVar
+
+        def __init__(self, name: str, default: "Any" = None) -> None:
+            self._name = name
+            self._default = default
+            self._local = local()
+            self._original_local = local()
+
+        def get(self, default: "Any" = None) -> "Any":
+            return getattr(self._local, "value", default or self._default)
+
+        def set(self, value: "Any") -> "Any":
+            token = str(random.getrandbits(64))
+            original_value = self.get()
+            setattr(self._original_local, token, original_value)
+            self._local.value = value
+            return token
+
+        def reset(self, token: "Any") -> None:
+            self._local.value = getattr(self._original_local, token)
+            # delete the original value (this way it works in Python 3.6+)
+            del self._original_local.__dict__[token]
+
+    return ContextVar
+
+
+def _get_contextvars() -> "Tuple[bool, type]":
+    """
+    Figure out the "right" contextvars installation to use. Returns a
+    `contextvars.ContextVar`-like class with a limited API.
+
+    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
+    """
+    if not _is_contextvars_broken():
+        # aiocontextvars is a PyPI package that ensures that the contextvars
+        # backport (also a PyPI package) works with asyncio under Python 3.6
+        #
+        # Import it if available.
+        if sys.version_info < (3, 7):
+            # `aiocontextvars` is absolutely required for functional
+            # contextvars on Python 3.6.
+            try:
+                from aiocontextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+        else:
+            # On Python 3.7 contextvars are functional.
+            try:
+                from contextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+
+    # Fall back to basic thread-local usage.
+
+    from threading import local
+
+    return False, _make_threadlocal_contextvars(local)
+
+
+HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
+
+CONTEXTVARS_ERROR_MESSAGE = """
+
+With asyncio/ASGI applications, the Sentry SDK requires a functional
+installation of `contextvars` to avoid leaking scope/context data across
+requests.
+
+Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
+"""
+
+_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
+
+# These exceptions won't set the span status to error if they occur. Use
+# register_control_flow_exception to add to this list
+_control_flow_exception_classes: "set[type]" = set()
+
+
+def is_internal_task() -> bool:
+    return _is_sentry_internal_task.get()
+
+
+@contextmanager
+def mark_sentry_task_internal() -> "Generator[None, None, None]":
+    """Context manager to mark a task as Sentry internal."""
+    token = _is_sentry_internal_task.set(True)
+    try:
+        yield
+    finally:
+        _is_sentry_internal_task.reset(token)
+
+
+def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
+    func_qualname: "Optional[str]" = None
+
+    prefix, suffix = "", ""
+
+    if isinstance(func, partial) and hasattr(func.func, "__name__"):
+        prefix, suffix = "partial()"
+        func = func.func
+    else:
+        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
+        # https://github.com/python/cpython/pull/16600
+        partial_method = getattr(func, "_partialmethod", None) or getattr(
+            func, "__partialmethod__", None
+        )
+        if isinstance(partial_method, partialmethod):
+            prefix, suffix = "partialmethod()"
+            func = partial_method.func
+
+    if hasattr(func, "__qualname__"):
+        func_qualname = func.__qualname__
+    elif hasattr(func, "__name__"):
+        func_qualname = func.__name__
+
+    if func_qualname is not None:
+        if hasattr(func, "__module__") and isinstance(func.__module__, str):
+            func_qualname = func.__module__ + "." + func_qualname
+        func_qualname = prefix + func_qualname + suffix
+
+    return func_qualname
+
+
+def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    return qualname_from_function(func)
+
+
+disable_capture_event = ContextVar("disable_capture_event")
+
+
+class ServerlessTimeoutWarning(Exception):  # noqa: N818
+    """Raised when a serverless method is about to reach its timeout."""
+
+    pass
+
+
+class TimeoutThread(threading.Thread):
+    """Creates a Thread which runs (sleeps) for a time duration equal to
+    waiting_time and raises a custom ServerlessTimeout exception.
+    """
+
+    def __init__(
+        self,
+        waiting_time: float,
+        configured_timeout: int,
+        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
+        current_scope: "Optional[sentry_sdk.Scope]" = None,
+    ) -> None:
+        threading.Thread.__init__(self)
+        self.waiting_time = waiting_time
+        self.configured_timeout = configured_timeout
+
+        self.isolation_scope = isolation_scope
+        self.current_scope = current_scope
+
+        self._stop_event = threading.Event()
+
+    def stop(self) -> None:
+        self._stop_event.set()
+
+    def _capture_exception(self) -> "ExcInfo":
+        exc_info = sys.exc_info()
+
+        client = sentry_sdk.get_client()
+        event, hint = event_from_exception(
+            exc_info,
+            client_options=client.options,
+            mechanism={"type": "threading", "handled": False},
+        )
+        sentry_sdk.capture_event(event, hint=hint)
+
+        return exc_info
+
+    def run(self) -> None:
+        self._stop_event.wait(self.waiting_time)
+
+        if self._stop_event.is_set():
+            return
+
+        integer_configured_timeout = int(self.configured_timeout)
+
+        # Setting up the exact integer value of configured time(in seconds)
+        if integer_configured_timeout < self.configured_timeout:
+            integer_configured_timeout = integer_configured_timeout + 1
+
+        # Raising Exception after timeout duration is reached
+        if self.isolation_scope is not None and self.current_scope is not None:
+            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
+                with sentry_sdk.scope.use_scope(self.current_scope):
+                    try:
+                        raise ServerlessTimeoutWarning(
+                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                                integer_configured_timeout
+                            )
+                        )
+                    except Exception:
+                        reraise(*self._capture_exception())
+
+        raise ServerlessTimeoutWarning(
+            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                integer_configured_timeout
+            )
+        )
+
+
+def to_base64(original: str) -> "Optional[str]":
+    """
+    Convert a string to base64, via UTF-8. Returns None on invalid input.
+    """
+    base64_string = None
+
+    try:
+        utf8_bytes = original.encode("UTF-8")
+        base64_bytes = base64.b64encode(utf8_bytes)
+        base64_string = base64_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
+
+    return base64_string
+
+
+def from_base64(base64_string: str) -> "Optional[str]":
+    """
+    Convert a string from base64, via UTF-8. Returns None on invalid input.
+    """
+    utf8_string = None
+
+    try:
+        only_valid_chars = BASE64_ALPHABET.match(base64_string)
+        assert only_valid_chars
+
+        base64_bytes = base64_string.encode("UTF-8")
+        utf8_bytes = base64.b64decode(base64_bytes)
+        utf8_string = utf8_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning(
+            "Unable to decode {b64} from base64:".format(b64=base64_string), err
+        )
+
+    return utf8_string
+
+
+Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
+
+
+def sanitize_url(
+    url: str,
+    remove_authority: bool = True,
+    remove_query_values: bool = True,
+    split: bool = False,
+) -> "Union[str, Components]":
+    """
+    Removes the authority and query parameter values from a given URL.
+    """
+    parsed_url = urlsplit(url)
+    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
+
+    # strip username:password (netloc can be usr:pwd@example.com)
+    if remove_authority:
+        netloc_parts = parsed_url.netloc.split("@")
+        if len(netloc_parts) > 1:
+            netloc = "%s:%s@%s" % (
+                SENSITIVE_DATA_SUBSTITUTE,
+                SENSITIVE_DATA_SUBSTITUTE,
+                netloc_parts[-1],
+            )
+        else:
+            netloc = parsed_url.netloc
+    else:
+        netloc = parsed_url.netloc
+
+    # strip values from query string
+    if remove_query_values:
+        query_string = unquote(
+            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
+        )
+    else:
+        query_string = parsed_url.query
+
+    components = Components(
+        scheme=parsed_url.scheme,
+        netloc=netloc,
+        query=query_string,
+        path=parsed_url.path,
+        fragment=parsed_url.fragment,
+    )
+
+    if split:
+        return components
+    else:
+        return urlunsplit(components)
+
+
+ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
+
+
+def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
+    """
+    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
+    parameters will be sanitized to remove sensitive data. The autority (username and password)
+    in the URL will always be removed.
+    """
+    parsed_url = sanitize_url(
+        url, remove_authority=True, remove_query_values=sanitize, split=True
+    )
+
+    base_url = urlunsplit(
+        Components(
+            scheme=parsed_url.scheme,  # type: ignore
+            netloc=parsed_url.netloc,  # type: ignore
+            query="",
+            path=parsed_url.path,  # type: ignore
+            fragment="",
+        )
+    )
+
+    return ParsedUrl(
+        url=base_url,
+        query=parsed_url.query,  # type: ignore
+        fragment=parsed_url.fragment,  # type: ignore
+    )
+
+
+def is_valid_sample_rate(rate: "Any", source: str) -> bool:
+    """
+    Checks the given sample rate to make sure it is valid type and value (a
+    boolean or a number between 0 and 1, inclusive).
+    """
+
+    # both booleans and NaN are instances of Real, so a) checking for Real
+    # checks for the possibility of a boolean also, and b) we have to check
+    # separately for NaN and Decimal does not derive from Real so need to check that too
+    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
+                source=source, rate=rate, type=type(rate)
+            )
+        )
+        return False
+
+    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
+    rate = float(rate)
+    if rate < 0 or rate > 1:
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
+                source=source, rate=rate
+            )
+        )
+        return False
+
+    return True
+
+
+def match_regex_list(
+    item: str,
+    regex_list: "Optional[List[str]]" = None,
+    substring_matching: bool = False,
+) -> bool:
+    if regex_list is None:
+        return False
+
+    for item_matcher in regex_list:
+        if not substring_matching and item_matcher[-1] != "$":
+            item_matcher += "$"
+
+        matched = re.search(item_matcher, item)
+        if matched:
+            return True
+
+    return False
+
+
+def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
+    """
+    Determines whether the given URL matches the Sentry DSN.
+    """
+    return (
+        client is not None
+        and client.transport is not None
+        and client.transport.parsed_dsn is not None
+        and client.transport.parsed_dsn.netloc in url
+    )
+
+
+def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
+    try:
+        from importlib import metadata
+
+        yielded = set()
+        for dist in metadata.distributions():
+            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
+            # `metadata` values may be `None`, see:
+            # https://github.com/python/cpython/issues/91216
+            # and
+            # https://github.com/python/importlib_metadata/issues/371
+            if name is not None:
+                normalized_name = _normalize_module_name(name)
+                if dist.version is not None and normalized_name not in yielded:
+                    yield normalized_name, dist.version
+                    yielded.add(normalized_name)
+
+    except ImportError:
+        # < py3.8
+        try:
+            import pkg_resources
+        except ImportError:
+            return
+
+        for info in pkg_resources.working_set:
+            yield _normalize_module_name(info.key), info.version
+
+
+def _normalize_module_name(name: str) -> str:
+    return name.lower()
+
+
+def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
+    # https://peps.python.org/pep-0503/#normalized-names
+    return re.sub(r"[-_.]+", "-", name)
+
+
+def _get_installed_modules() -> "Dict[str, str]":
+    global _installed_modules
+    if _installed_modules is None:
+        _installed_modules = dict(_generate_installed_modules())
+    return _installed_modules
+
+
+def package_version(package: str) -> "Optional[Tuple[int, ...]]":
+    normalized_package = _normalize_module_name(
+        _replace_hyphens_dots_and_underscores_with_dashes(package)
+    )
+
+    installed_packages = {
+        _replace_hyphens_dots_and_underscores_with_dashes(module): v
+        for module, v in _get_installed_modules().items()
+    }
+    version = installed_packages.get(normalized_package)
+    if version is None:
+        return None
+
+    return parse_version(version)
+
+
+def reraise(
+    tp: "Optional[Type[BaseException]]",
+    value: "Optional[BaseException]",
+    tb: "Optional[Any]" = None,
+) -> "NoReturn":
+    assert value is not None
+    if value.__traceback__ is not tb:
+        raise value.with_traceback(tb)
+    raise value
+
+
+def _no_op(*_a: "Any", **_k: "Any") -> None:
+    """No-op function for ensure_integration_enabled."""
+    pass
+
+
+if TYPE_CHECKING:
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+        original_function: "Callable[P, R]",
+    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
+
+
+def ensure_integration_enabled(
+    integration: "type[sentry_sdk.integrations.Integration]",
+    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
+) -> "Callable[[Callable[P, R]], Callable[P, R]]":
+    """
+    Ensures a given integration is enabled prior to calling a Sentry-patched function.
+
+    The function takes as its parameters the integration that must be enabled and the original
+    function that the SDK is patching. The function returns a function that takes the
+    decorated (Sentry-patched) function as its parameter, and returns a function that, when
+    called, checks whether the given integration is enabled. If the integration is enabled, the
+    function calls the decorated, Sentry-patched function. If the integration is not enabled,
+    the original function is called.
+
+    The function also takes care of preserving the original function's signature and docstring.
+
+    Example usage:
+
+    ```python
+    @ensure_integration_enabled(MyIntegration, my_function)
+    def patch_my_function():
+        with sentry_sdk.start_transaction(...):
+            return my_function()
+    ```
+    """
+    if TYPE_CHECKING:
+        # Type hint to ensure the default function has the right typing. The overloads
+        # ensure the default _no_op function is only used when R is None.
+        original_function = cast(Callable[P, R], original_function)
+
+    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
+        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
+            if sentry_sdk.get_client().get_integration(integration) is None:
+                return original_function(*args, **kwargs)
+
+            return sentry_patched_function(*args, **kwargs)
+
+        if original_function is _no_op:
+            return wraps(sentry_patched_function)(runner)
+
+        return wraps(original_function)(runner)
+
+    return patcher
+
+
+if PY37:
+
+    def nanosecond_time() -> int:
+        return time.perf_counter_ns()
+
+else:
+
+    def nanosecond_time() -> int:
+        return int(time.perf_counter() * 1e9)
+
+
+def now() -> float:
+    return time.perf_counter()
+
+
+try:
+    from gevent import get_hub as get_gevent_hub
+    from gevent.monkey import is_module_patched
+except ImportError:
+    # it's not great that the signatures are different, get_hub can't return None
+    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
+    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
+        return None
+
+    def is_module_patched(mod_name: str) -> bool:
+        # unable to import from gevent means no modules have been patched
+        return False
+
+
+def is_gevent() -> bool:
+    return is_module_patched("threading") or is_module_patched("_thread")
+
+
+def get_current_thread_meta(
+    thread: "Optional[threading.Thread]" = None,
+) -> "Tuple[Optional[int], Optional[str]]":
+    """
+    Try to get the id of the current thread, with various fall backs.
+    """
+
+    # if a thread is specified, that takes priority
+    if thread is not None:
+        try:
+            thread_id = thread.ident
+            thread_name = thread.name
+            if thread_id is not None:
+                return thread_id, thread_name
+        except AttributeError:
+            pass
+
+    # if the app is using gevent, we should look at the gevent hub first
+    # as the id there differs from what the threading module reports
+    if is_gevent():
+        gevent_hub = get_gevent_hub()
+        if gevent_hub is not None:
+            try:
+                # this is undocumented, so wrap it in try except to be safe
+                return gevent_hub.thread_ident, None
+            except AttributeError:
+                pass
+
+    # use the current thread's id if possible
+    try:
+        thread = threading.current_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # if we can't get the current thread id, fall back to the main thread id
+    try:
+        thread = threading.main_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # we've tried everything, time to give up
+    return None, None
+
+
+def _register_control_flow_exception(
+    exc_type: "Union[type, list[type], tuple[type], set[type]]",
+) -> None:
+    if isinstance(exc_type, (list, tuple, set)):
+        _control_flow_exception_classes.update(exc_type)
+    else:
+        _control_flow_exception_classes.add(exc_type)
+
+
+def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
+    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
+        # https://docs.python.org/3/library/exceptions.html#SystemExit
+        return False
+
+    if issubclass(ty, tuple(_control_flow_exception_classes)):
+        return False
+
+    return True
+
+
+if TYPE_CHECKING:
+    T = TypeVar("T")
+
+
+def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
+    """
+    Attempt to convert from an unknown type to a specific type, using the
+    given function. Return None if the conversion fails, i.e. if the function
+    raises an exception.
+    """
+    try:
+        if isinstance(value, convert_func):  # type: ignore
+            return value
+    except TypeError:
+        pass
+
+    try:
+        return convert_func(value)
+    except Exception:
+        return None
+
+
+def safe_serialize(data: "Any") -> str:
+    """Safely serialize to a readable string."""
+
+    def serialize_item(
+        item: "Any",
+    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
+        if callable(item):
+            try:
+                module = getattr(item, "__module__", None)
+                qualname = getattr(item, "__qualname__", None)
+                name = getattr(item, "__name__", "anonymous")
+
+                if module and qualname:
+                    full_path = f"{module}.{qualname}"
+                elif module and name:
+                    full_path = f"{module}.{name}"
+                else:
+                    full_path = name
+
+                return f""
+            except Exception:
+                return f""
+        elif isinstance(item, dict):
+            return {k: serialize_item(v) for k, v in item.items()}
+        elif isinstance(item, (list, tuple)):
+            return [serialize_item(x) for x in item]
+        elif hasattr(item, "__dict__"):
+            try:
+                attrs = {
+                    k: serialize_item(v)
+                    for k, v in vars(item).items()
+                    if not k.startswith("_")
+                }
+                return f"<{type(item).__name__} {attrs}>"
+            except Exception:
+                return repr(item)
+        else:
+            return item
+
+    try:
+        serialized = serialize_item(data)
+        return (
+            json.dumps(serialized, default=str)
+            if not isinstance(serialized, str)
+            else serialized
+        )
+    except Exception:
+        return str(data)
+
+
+def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(
+        options.get("enable_logs", False)
+        or options["_experiments"].get("enable_logs", False)
+    )
+
+
+def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return "data_collection" in options.get("_experiments", {})
+
+
+def get_before_send_log(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_log") or options["_experiments"].get(
+        "before_send_log"
+    )
+
+
+def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(options.get("enable_metrics", True))
+
+
+def get_before_send_metric(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_metric") or options["_experiments"].get(
+        "before_send_metric"
+    )
+
+
+def get_before_send_span(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
+    if options is None:
+        return None
+
+    return options["_experiments"].get("before_send_span")
+
+
+def format_attribute(val: "Any") -> "AttributeValue":
+    """
+    Turn unsupported attribute value types into an AttributeValue.
+
+    We do this as soon as a user-provided attribute is set, to prevent spans,
+    logs, metrics and similar from having live references to various objects.
+
+    Note: This is not the final attribute value format. Before they're sent,
+    they're serialized further into the actual format the protocol expects:
+    https://develop.sentry.dev/sdk/telemetry/attributes/
+    """
+    if isinstance(val, (bool, int, float, str)):
+        return val
+
+    if isinstance(val, (list, tuple)) and not val:
+        return []
+    elif isinstance(val, list):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return copy.deepcopy(val)
+    elif isinstance(val, tuple):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return list(val)
+
+    return safe_repr(val)
+
+
+def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
+    """Serialize attribute value to the transport format."""
+    if isinstance(val, bool):
+        return {"value": val, "type": "boolean"}
+    if isinstance(val, int):
+        return {"value": val, "type": "integer"}
+    if isinstance(val, float):
+        return {"value": val, "type": "double"}
+    if isinstance(val, str):
+        return {"value": val, "type": "string"}
+
+    if isinstance(val, list):
+        if not val:
+            return {"value": [], "type": "array"}
+
+        # Only lists of elements of a single type are supported
+        ty = type(val[0])
+        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
+            return {"value": val, "type": "array"}
+
+    # Coerce to string if we don't know what to do with the value. This should
+    # never happen as we pre-format early in format_attribute, but let's be safe.
+    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py
new file mode 100644
index 0000000000..5eb9b23130
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py
@@ -0,0 +1,308 @@
+import asyncio
+import os
+import threading
+from abc import ABC, abstractmethod
+from time import sleep, time
+from typing import TYPE_CHECKING
+
+from sentry_sdk._queue import FullError, Queue
+from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
+from sentry_sdk.utils import logger, mark_sentry_task_internal
+
+if TYPE_CHECKING:
+    from typing import Any, Callable, Optional
+
+
+_TERMINATOR = object()
+
+
+class Worker(ABC):
+    """Base class for all workers."""
+
+    @property
+    @abstractmethod
+    def is_alive(self) -> bool:
+        """Whether the worker is alive and running."""
+        pass
+
+    @abstractmethod
+    def kill(self) -> None:
+        """Kill the worker. It will not process any more events."""
+        pass
+
+    def flush(
+        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
+    ) -> None:
+        """Flush the worker, blocking until done or timeout is reached."""
+        return None
+
+    @abstractmethod
+    def full(self) -> bool:
+        """Whether the worker's queue is full."""
+        pass
+
+    @abstractmethod
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        """Schedule a callback. Returns True if queued, False if full."""
+        pass
+
+
+class BackgroundWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Queue" = Queue(queue_size)
+        self._lock = threading.Lock()
+        self._thread: "Optional[threading.Thread]" = None
+        self._thread_for_pid: "Optional[int]" = None
+
+    @property
+    def is_alive(self) -> bool:
+        if self._thread_for_pid != os.getpid():
+            return False
+        if not self._thread:
+            return False
+        return self._thread.is_alive()
+
+    def _ensure_thread(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    def _timed_queue_join(self, timeout: float) -> bool:
+        deadline = time() + timeout
+        queue = self._queue
+
+        queue.all_tasks_done.acquire()
+
+        try:
+            while queue.unfinished_tasks:
+                delay = deadline - time()
+                if delay <= 0:
+                    return False
+                queue.all_tasks_done.wait(timeout=delay)
+
+            return True
+        finally:
+            queue.all_tasks_done.release()
+
+    def start(self) -> None:
+        with self._lock:
+            if not self.is_alive:
+                self._thread = threading.Thread(
+                    target=self._target, name="sentry-sdk.BackgroundWorker"
+                )
+                self._thread.daemon = True
+                try:
+                    self._thread.start()
+                    self._thread_for_pid = os.getpid()
+                except RuntimeError:
+                    # At this point we can no longer start because the interpreter
+                    # is already shutting down.  Sadly at this point we can no longer
+                    # send out events.
+                    self._thread = None
+
+    def kill(self) -> None:
+        """
+        Kill worker thread. Returns immediately. Not useful for
+        waiting on shutdown for events, use `flush` for that.
+        """
+        logger.debug("background worker got kill request")
+        with self._lock:
+            if self._thread:
+                try:
+                    self._queue.put_nowait(_TERMINATOR)
+                except FullError:
+                    logger.debug("background worker queue full, kill failed")
+
+                self._thread = None
+                self._thread_for_pid = None
+
+    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
+        logger.debug("background worker got flush request")
+        with self._lock:
+            if self.is_alive and timeout > 0.0:
+                self._wait_flush(timeout, callback)
+        logger.debug("background worker flushed")
+
+    def full(self) -> bool:
+        return self._queue.full()
+
+    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
+        initial_timeout = min(0.1, timeout)
+        if not self._timed_queue_join(initial_timeout):
+            pending = self._queue.qsize() + 1
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            if not self._timed_queue_join(timeout - initial_timeout):
+                pending = self._queue.qsize() + 1
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_thread()
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except FullError:
+            return False
+
+    def _target(self) -> None:
+        while True:
+            callback = self._queue.get()
+            try:
+                if callback is _TERMINATOR:
+                    break
+                try:
+                    callback()
+                except Exception:
+                    logger.error("Failed processing job", exc_info=True)
+            finally:
+                self._queue.task_done()
+            sleep(0)
+
+
+class AsyncWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Optional[asyncio.Queue[Any]]" = None
+        self._queue_size = queue_size
+        self._task: "Optional[asyncio.Task[None]]" = None
+        # Event loop needs to remain in the same process
+        self._task_for_pid: "Optional[int]" = None
+        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
+        # Track active callback tasks so they have a strong reference and can be cancelled on kill
+        self._active_tasks: "set[asyncio.Task[None]]" = set()
+
+    @property
+    def is_alive(self) -> bool:
+        if self._task_for_pid != os.getpid():
+            return False
+        if not self._task or not self._loop:
+            return False
+        return self._loop.is_running() and not self._task.done()
+
+    def kill(self) -> None:
+        if self._task:
+            # Cancel the main consumer task to prevent duplicate consumers
+            self._task.cancel()
+            # Also cancel any active callback tasks
+            # Avoid modifying the set while cancelling tasks
+            tasks_to_cancel = set(self._active_tasks)
+            for task in tasks_to_cancel:
+                task.cancel()
+            self._active_tasks.clear()
+            self._loop = None
+            self._task = None
+            self._task_for_pid = None
+
+    def start(self) -> None:
+        if not self.is_alive:
+            try:
+                self._loop = asyncio.get_running_loop()
+                # Always create a fresh queue on start to avoid stale items
+                self._queue = asyncio.Queue(maxsize=self._queue_size)
+                with mark_sentry_task_internal():
+                    self._task = self._loop.create_task(self._target())
+                self._task_for_pid = os.getpid()
+            except RuntimeError:
+                # There is no event loop running
+                logger.warning("No event loop running, async worker not started")
+                self._loop = None
+                self._task = None
+                self._task_for_pid = None
+
+    def full(self) -> bool:
+        if self._queue is None:
+            return True
+        return self._queue.full()
+
+    def _ensure_task(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    async def _wait_flush(
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> None:
+        if not self._loop or not self._loop.is_running() or self._queue is None:
+            return
+
+        initial_timeout = min(0.1, timeout)
+
+        # Timeout on the join
+        try:
+            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
+        except asyncio.TimeoutError:
+            pending = self._queue.qsize() + len(self._active_tasks)
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            try:
+                remaining_timeout = timeout - initial_timeout
+                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
+            except asyncio.TimeoutError:
+                pending = self._queue.qsize() + len(self._active_tasks)
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def flush(  # type: ignore[override]
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> "Optional[asyncio.Task[None]]":
+        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
+            with mark_sentry_task_internal():
+                return self._loop.create_task(self._wait_flush(timeout, callback))
+        return None
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_task()
+        if self._queue is None:
+            return False
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except asyncio.QueueFull:
+            return False
+
+    async def _target(self) -> None:
+        if self._queue is None:
+            return
+        try:
+            while True:
+                callback = await self._queue.get()
+                if callback is _TERMINATOR:
+                    self._queue.task_done()
+                    break
+                # Firing tasks instead of awaiting them allows for concurrent requests
+                with mark_sentry_task_internal():
+                    task = asyncio.create_task(self._process_callback(callback))
+                # Create a strong reference to the task so it can be cancelled on kill
+                # and does not get garbage collected while running
+                self._active_tasks.add(task)
+                # Capture queue ref at dispatch time so done callbacks use the
+                # correct queue even if kill()/start() replace self._queue.
+                queue_ref = self._queue
+                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
+                # Yield to let the event loop run other tasks
+                await asyncio.sleep(0)
+        except asyncio.CancelledError:
+            pass  # Expected during kill()
+
+    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
+        # Callback is an async coroutine, need to await it
+        await callback()
+
+    def _on_task_complete(
+        self,
+        task: "asyncio.Task[None]",
+        queue: "Optional[asyncio.Queue[Any]]" = None,
+    ) -> None:
+        try:
+            task.result()
+        except asyncio.CancelledError:
+            pass  # Task was cancelled, expected during shutdown
+        except Exception:
+            logger.error("Failed processing job", exc_info=True)
+        finally:
+            # Mark the task as done and remove it from the active tasks set
+            # Use the queue reference captured at dispatch time, not self._queue,
+            # to avoid calling task_done() on a different queue after kill()/start().
+            if queue is not None:
+                queue.task_done()
+            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER
new file mode 100644
index 0000000000..5c69047b2e
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA
new file mode 100644
index 0000000000..9c7a4703f8
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA
@@ -0,0 +1,163 @@
+Metadata-Version: 2.4
+Name: urllib3
+Version: 2.7.0
+Summary: HTTP library with thread-safe connection pooling, file post, and more.
+Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
+Project-URL: Documentation, https://urllib3.readthedocs.io
+Project-URL: Code, https://github.com/urllib3/urllib3
+Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
+Author-email: Andrey Petrov 
+Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
+License-Expression: MIT
+License-File: LICENSE.txt
+Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Software Development :: Libraries
+Requires-Python: >=3.10
+Provides-Extra: brotli
+Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
+Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
+Provides-Extra: h2
+Requires-Dist: h2<5,>=4; extra == 'h2'
+Provides-Extra: socks
+Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
+Provides-Extra: zstd
+Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
+Description-Content-Type: text/markdown
+
+

+ +![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) + +

+ +

+ PyPI Version + Python Versions + Join our Discord + Coverage Status + Build Status on GitHub + Documentation Status
+ OpenSSF Scorecard + SLSA 3 + CII Best Practices +

+ +urllib3 is a powerful, *user-friendly* HTTP client for Python. +urllib3 brings many critical features that are missing from the Python +standard libraries: + +- Thread safety. +- Connection pooling. +- Client-side SSL/TLS verification. +- File uploads with multipart encoding. +- Helpers for retrying requests and dealing with HTTP redirects. +- Support for gzip, deflate, brotli, and zstd encoding. +- Proxy support for HTTP and SOCKS. +- 100% test coverage. + +... and many more features, but most importantly: Our maintainers have a 15+ +year track record of maintaining urllib3 with the highest code standards and +attention to security and safety. + +[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) +and you should too. + + +## Installing + +urllib3 can be installed with [pip](https://pip.pypa.io): + +```bash +$ python -m pip install urllib3 +``` + +Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): + +```bash +$ git clone https://github.com/urllib3/urllib3.git +$ cd urllib3 +$ pip install . +``` + +## Getting Started + +urllib3 is easy to use: + +```python3 +>>> import urllib3 +>>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") +>>> resp.status +200 +>>> resp.data +b"User-agent: *\nDisallow: /deny\n" +``` + +urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). + + +## Community + +urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and +collaborating with other contributors. Drop by and say hello 👋 + + +## Contributing + +urllib3 happily accepts contributions. Please see our +[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) +for some tips on getting started. + + +## Security Disclosures + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure with maintainers. + + +## Maintainers + +Meet our maintainers since 2008: + +- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) +- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) +- [@pquentin](https://github.com/pquentin) (Quentin Pradet) +- [@theacodes](https://github.com/theacodes) (Thea Flowers) +- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) +- [@lukasa](https://github.com/lukasa) (Cory Benfield) +- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) +- [@shazow](https://github.com/shazow) (Andrey Petrov) + +👋 + + +## Sponsorship + +If your company benefits from this library, please consider [sponsoring its +development](https://urllib3.readthedocs.io/en/latest/sponsors.html). + + +## For Enterprise + +Professional support for urllib3 is available as part of the [Tidelift +Subscription][1]. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD new file mode 100644 index 0000000000..8c4f9c0b4b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD @@ -0,0 +1,44 @@ +urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 +urllib3-2.7.0.dist-info/RECORD,, +urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 +urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 +urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 +urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 +urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 +urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 +urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 +urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 +urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 +urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 +urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 +urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 +urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 +urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 +urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 +urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 +urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 +urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 +urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 +urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 +urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 +urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 +urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 +urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 +urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 +urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 +urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 +urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 +urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 +urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 +urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 +urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 +urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 +urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 +urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 +urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 +urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 +urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 +urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL new file mode 100644 index 0000000000..b1b94fd58e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000..e6183d0276 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py new file mode 100644 index 0000000000..3fe782c8a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py @@ -0,0 +1,211 @@ +""" +Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more +""" + +from __future__ import annotations + +# Set default logging handler to avoid "No handler found" warnings. +import logging +import sys +import typing +import warnings +from logging import NullHandler + +from . import exceptions +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._version import __version__ +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import BaseHTTPResponse, HTTPResponse +from .util.request import make_headers +from .util.retry import Retry +from .util.timeout import Timeout + +# Ensure that Python is compiled with OpenSSL 1.1.1+ +# If the 'ssl' module isn't available at all that's +# fine, we only care if the module is available. +try: + import ssl +except ImportError: + pass +else: + if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: + warnings.warn( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/3020", + exceptions.NotOpenSSLWarning, + ) + elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: + raise ImportError( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/2168" + ) + +__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" +__license__ = "MIT" +__version__ = __version__ + +__all__ = ( + "HTTPConnectionPool", + "HTTPHeaderDict", + "HTTPSConnectionPool", + "PoolManager", + "ProxyManager", + "HTTPResponse", + "Retry", + "Timeout", + "add_stderr_logger", + "connection_from_url", + "disable_warnings", + "encode_multipart_formdata", + "make_headers", + "proxy_from_url", + "request", + "BaseHTTPResponse", +) + +logging.getLogger(__name__).addHandler(NullHandler()) + + +def add_stderr_logger( + level: int = logging.DEBUG, +) -> logging.StreamHandler[typing.TextIO]: + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug("Added a stderr logging handler to logger: %s", __name__) + return handler + + +# ... Clean up. +del NullHandler + + +# All warning filters *must* be appended unless you're really certain that they +# shouldn't be: otherwise, it's very hard for users to use most Python +# mechanisms to silence them. +# SecurityWarning's always go off by default. +warnings.simplefilter("always", exceptions.SecurityWarning, append=True) +# InsecurePlatformWarning's don't vary between requests, so we keep it default. +warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) + + +def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter("ignore", category) + + +_DEFAULT_POOL = PoolManager() + + +def request( + method: str, + url: str, + *, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + preload_content: bool | None = True, + decode_content: bool | None = True, + redirect: bool | None = True, + retries: Retry | bool | int | None = None, + timeout: Timeout | float | int | None = 3, + json: typing.Any | None = None, +) -> BaseHTTPResponse: + """ + A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. + Therefore, its side effects could be shared across dependencies relying on it. + To avoid side effects create a new ``PoolManager`` instance and use it instead. + The method does not accept low-level ``**urlopen_kw`` keyword arguments. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + + return _DEFAULT_POOL.request( + method, + url, + body=body, + fields=fields, + headers=headers, + preload_content=preload_content, + decode_content=decode_content, + redirect=redirect, + retries=retries, + timeout=timeout, + json=json, + ) + + +if sys.platform == "emscripten": + from .contrib.emscripten import inject_into_urllib3 # noqa: 401 + + inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py new file mode 100644 index 0000000000..992ec1657a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import typing + +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from .util.url import Url + +_TYPE_BODY = typing.Union[ + bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str +] + + +class ProxyConfig(typing.NamedTuple): + ssl_context: ssl.SSLContext | None + use_forwarding_for_https: bool + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + + +class _ResponseOptions(typing.NamedTuple): + # TODO: Remove this in favor of a better + # HTTP request/response lifecycle tracking. + request_method: str + request_url: str + preload_content: bool + decode_content: bool + enforce_content_length: bool + + +if typing.TYPE_CHECKING: + import ssl + from typing import Protocol + + from .response import BaseHTTPResponse + + class BaseHTTPConnection(Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + host: str + port: int + timeout: None | ( + float + ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool + proxy_is_verified: bool | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: ... + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: ... + + def connect(self) -> None: ... + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: ... + + def getresponse(self) -> BaseHTTPResponse: ... + + def close(self) -> None: ... + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + + class BaseHTTPSConnection(BaseHTTPConnection, Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + # Certificate verification methods + cert_reqs: int | str | None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + ssl_context: ssl.SSLContext | None + + # Trusted CAs + ca_certs: str | None + ca_cert_dir: str | None + ca_cert_data: None | str | bytes + + # TLS version + ssl_minimum_version: int | None + ssl_maximum_version: int | None + ssl_version: int | str | None # Deprecated + + # Client certificates + cert_file: str | None + key_file: str | None + key_password: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py new file mode 100644 index 0000000000..ee9ca662b6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py @@ -0,0 +1,486 @@ +from __future__ import annotations + +import typing +from collections import OrderedDict +from enum import Enum, auto +from threading import RLock + +if typing.TYPE_CHECKING: + # We can only import Protocol if TYPE_CHECKING because it's a development + # dependency, and is not available at runtime. + from typing import Protocol + + from typing_extensions import Self + + class HasGettableStringKeys(Protocol): + def keys(self) -> typing.Iterator[str]: ... + + def __getitem__(self, key: str) -> str: ... + + +__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] + + +# Key type +_KT = typing.TypeVar("_KT") +# Value type +_VT = typing.TypeVar("_VT") +# Default type +_DT = typing.TypeVar("_DT") + +ValidHTTPHeaderSource = typing.Union[ + "HTTPHeaderDict", + typing.Mapping[str, str], + typing.Iterable[tuple[str, str]], + "HasGettableStringKeys", +] + + +class _Sentinel(Enum): + not_passed = auto() + + +def ensure_can_construct_http_header_dict( + potential: object, +) -> ValidHTTPHeaderSource | None: + if isinstance(potential, HTTPHeaderDict): + return potential + elif isinstance(potential, typing.Mapping): + # Full runtime checking of the contents of a Mapping is expensive, so for the + # purposes of typechecking, we assume that any Mapping is the right shape. + return typing.cast(typing.Mapping[str, str], potential) + elif isinstance(potential, typing.Iterable): + # Similarly to Mapping, full runtime checking of the contents of an Iterable is + # expensive, so for the purposes of typechecking, we assume that any Iterable + # is the right shape. + return typing.cast(typing.Iterable[tuple[str, str]], potential) + elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): + return typing.cast("HasGettableStringKeys", potential) + else: + return None + + +class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + _container: typing.OrderedDict[_KT, _VT] + _maxsize: int + dispose_func: typing.Callable[[_VT], None] | None + lock: RLock + + def __init__( + self, + maxsize: int = 10, + dispose_func: typing.Callable[[_VT], None] | None = None, + ) -> None: + super().__init__() + self._maxsize = maxsize + self.dispose_func = dispose_func + self._container = OrderedDict() + self.lock = RLock() + + def __getitem__(self, key: _KT) -> _VT: + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key: _KT, value: _VT) -> None: + evicted_item = None + with self.lock: + # Possibly evict the existing value of 'key' + try: + # If the key exists, we'll overwrite it, which won't change the + # size of the pool. Because accessing a key should move it to + # the end of the eviction line, we pop it out first. + evicted_item = key, self._container.pop(key) + self._container[key] = value + except KeyError: + # When the key does not exist, we insert the value first so that + # evicting works in all cases, including when self._maxsize is 0 + self._container[key] = value + if len(self._container) > self._maxsize: + # If we didn't evict an existing value, and we've hit our maximum + # size, then we have to evict the least recently used item from + # the beginning of the container. + evicted_item = self._container.popitem(last=False) + + # After releasing the lock on the pool, dispose of any evicted value. + if evicted_item is not None and self.dispose_func: + _, evicted_value = evicted_item + self.dispose_func(evicted_value) + + def __delitem__(self, key: _KT) -> None: + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self) -> int: + with self.lock: + return len(self._container) + + def __iter__(self) -> typing.NoReturn: + raise NotImplementedError( + "Iteration over this class is unlikely to be threadsafe." + ) + + def clear(self) -> None: + with self.lock: + # Copy pointers to all values, then wipe the mapping + values = list(self._container.values()) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self) -> set[_KT]: # type: ignore[override] + with self.lock: + return set(self._container.keys()) + + +class HTTPHeaderDictItemView(set[tuple[str, str]]): + """ + HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of + address. + + If we directly try to get an item with a particular name, we will get a string + back that is the concatenated version of all the values: + + >>> d['X-Header-Name'] + 'Value1, Value2, Value3' + + However, if we iterate over an HTTPHeaderDict's items, we will optionally combine + these values based on whether combine=True was called when building up the dictionary + + >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) + >>> d.add("A", "2", combine=True) + >>> d.add("B", "bar") + >>> list(d.items()) + [ + ('A', '1, 2'), + ('B', 'foo'), + ('B', 'bar'), + ] + + This class conforms to the interface required by the MutableMapping ABC while + also giving us the nonstandard iteration behavior we want; items with duplicate + keys, ordered by time of first insertion. + """ + + _headers: HTTPHeaderDict + + def __init__(self, headers: HTTPHeaderDict) -> None: + self._headers = headers + + def __len__(self) -> int: + return len(list(self._headers.iteritems())) + + def __iter__(self) -> typing.Iterator[tuple[str, str]]: + return self._headers.iteritems() + + def __contains__(self, item: object) -> bool: + if isinstance(item, tuple) and len(item) == 2: + passed_key, passed_val = item + if isinstance(passed_key, str) and isinstance(passed_val, str): + return self._headers._has_value_for_header(passed_key, passed_val) + return False + + +class HTTPHeaderDict(typing.MutableMapping[str, str]): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 7230. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + """ + + _container: typing.MutableMapping[str, list[str]] + + def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): + super().__init__() + self._container = {} # 'dict' is insert-ordered + if headers is not None: + if isinstance(headers, HTTPHeaderDict): + self._copy_from(headers) + else: + self.extend(headers) + if kwargs: + self.extend(kwargs) + + def __setitem__(self, key: str, val: str) -> None: + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + self._container[key.lower()] = [key, val] + + def __getitem__(self, key: str) -> str: + if isinstance(key, bytes): + key = key.decode("latin-1") + val = self._container[key.lower()] + return ", ".join(val[1:]) + + def __delitem__(self, key: str) -> None: + if isinstance(key, bytes): + key = key.decode("latin-1") + del self._container[key.lower()] + + def __contains__(self, key: object) -> bool: + if isinstance(key, bytes): + key = key.decode("latin-1") + if isinstance(key, str): + return key.lower() in self._container + return False + + def setdefault(self, key: str, default: str = "") -> str: + return super().setdefault(key, default) + + def __eq__(self, other: object) -> bool: + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return False + else: + other_as_http_header_dict = type(self)(maybe_constructable) + + return {k.lower(): v for k, v in self.itermerged()} == { + k.lower(): v for k, v in other_as_http_header_dict.itermerged() + } + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __len__(self) -> int: + return len(self._container) + + def __iter__(self) -> typing.Iterator[str]: + # Only provide the originally cased names + for vals in self._container.values(): + yield vals[0] + + def discard(self, key: str) -> None: + try: + del self[key] + except KeyError: + pass + + def add(self, key: str, val: str, *, combine: bool = False) -> None: + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + If this is called with combine=True, instead of adding a new header value + as a distinct item during iteration, this will instead append the value to + any existing header value with a comma. If no existing header value exists + for the key, then the value will simply be added, ignoring the combine parameter. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + >>> list(headers.items()) + [('foo', 'bar'), ('foo', 'baz')] + >>> headers.add('foo', 'quz', combine=True) + >>> list(headers.items()) + [('foo', 'bar, baz, quz')] + """ + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + key_lower = key.lower() + new_vals = [key, val] + # Keep the common case aka no item present as fast as possible + vals = self._container.setdefault(key_lower, new_vals) + if new_vals is not vals: + # if there are values here, then there is at least the initial + # key/value pair + assert len(vals) >= 2 + if combine: + vals[-1] = vals[-1] + ", " + val + else: + vals.append(val) + + def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: + """Generic import function for any type of header-like object. + Adapted version of MutableMapping.update in order to insert items + with self.add instead of self.__setitem__ + """ + if len(args) > 1: + raise TypeError( + f"extend() takes at most 1 positional arguments ({len(args)} given)" + ) + other = args[0] if len(args) >= 1 else () + + if isinstance(other, HTTPHeaderDict): + for key, val in other.iteritems(): + self.add(key, val) + elif isinstance(other, typing.Mapping): + for key, val in other.items(): + self.add(key, val) + elif isinstance(other, typing.Iterable): + for key, value in other: + self.add(key, value) + elif hasattr(other, "keys") and hasattr(other, "__getitem__"): + # THIS IS NOT A TYPESAFE BRANCH + # In this branch, the object has a `keys` attr but is not a Mapping or any of + # the other types indicated in the method signature. We do some stuff with + # it as though it partially implements the Mapping interface, but we're not + # doing that stuff safely AT ALL. + for key in other.keys(): + self.add(key, other[key]) + + for key, value in kwargs.items(): + self.add(key, value) + + @typing.overload + def getlist(self, key: str) -> list[str]: ... + + @typing.overload + def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... + + def getlist( + self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed + ) -> list[str] | _DT: + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + if isinstance(key, bytes): + key = key.decode("latin-1") + try: + vals = self._container[key.lower()] + except KeyError: + if default is _Sentinel.not_passed: + # _DT is unbound; empty list is instance of List[str] + return [] + # _DT is bound; default is instance of _DT + return default + else: + # _DT may or may not be bound; vals[1:] is instance of List[str], which + # meets our external interface requirement of `Union[List[str], _DT]`. + return vals[1:] + + def _prepare_for_method_change(self) -> Self: + """ + Remove content-specific header fields before changing the request + method to GET or HEAD according to RFC 9110, Section 15.4. + """ + content_specific_headers = [ + "Content-Encoding", + "Content-Language", + "Content-Location", + "Content-Type", + "Content-Length", + "Digest", + "Last-Modified", + ] + for header in content_specific_headers: + self.discard(header) + return self + + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist + iget = getlist + + # Backwards compatibility for http.cookiejar + get_all = getlist + + def __repr__(self) -> str: + return f"{type(self).__name__}({dict(self.itermerged())})" + + def _copy_from(self, other: HTTPHeaderDict) -> None: + for key in other: + val = other.getlist(key) + self._container[key.lower()] = [key, *val] + + def copy(self) -> Self: + clone = type(self)() + clone._copy_from(self) + return clone + + def iteritems(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all header lines, including duplicate ones.""" + for key in self: + vals = self._container[key.lower()] + for val in vals[1:]: + yield vals[0], val + + def itermerged(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all headers, merging duplicate ones together.""" + for key in self: + val = self._container[key.lower()] + yield val[0], ", ".join(val[1:]) + + def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] + return HTTPHeaderDictItemView(self) + + def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: + if header_name in self: + return potential_value in self._container[header_name.lower()][1:] + return False + + def __ior__(self, other: object) -> HTTPHeaderDict: + # Supports extending a header dict in-place using operator |= + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + self.extend(maybe_constructable) + return self + + def __or__(self, other: object) -> Self: + # Supports merging header dicts using operator | + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = self.copy() + result.extend(maybe_constructable) + return result + + def __ror__(self, other: object) -> Self: + # Supports merging header dicts using operator | when other is on left side + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = type(self)(maybe_constructable) + result.extend(self) + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py new file mode 100644 index 0000000000..297c271bf4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import json as _json +import typing +from urllib.parse import urlencode + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .response import BaseHTTPResponse + +__all__ = ["RequestMethods"] + +_TYPE_ENCODE_URL_FIELDS = typing.Union[ + typing.Sequence[tuple[str, typing.Union[str, bytes]]], + typing.Mapping[str, typing.Union[str, bytes]], +] + + +class RequestMethods: + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`urllib3.HTTPConnectionPool` and + :class:`urllib3.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are + encoded in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} + + def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: + self.headers = headers or {} + + def urlopen( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **kw: typing.Any, + ) -> BaseHTTPResponse: # Abstract + raise NotImplementedError( + "Classes extending RequestMethods must implement " + "their own ``urlopen`` method." + ) + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + json: typing.Any | None = None, + **urlopen_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the + option to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the URL or request body, depending on ``method``. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + method = method.upper() + + if json is not None and body is not None: + raise TypeError( + "request got values for both 'body' and 'json' parameters which are mutually exclusive" + ) + + if json is not None: + if headers is None: + headers = self.headers + + if not ("content-type" in map(str.lower, headers.keys())): + headers = HTTPHeaderDict(headers) + headers["Content-Type"] = "application/json" + + body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + + if body is not None: + urlopen_kw["body"] = body + + if method in self._encode_url_methods: + return self.request_encode_url( + method, + url, + fields=fields, # type: ignore[arg-type] + headers=headers, + **urlopen_kw, + ) + else: + return self.request_encode_body( + method, url, fields=fields, headers=headers, **urlopen_kw + ) + + def request_encode_url( + self, + method: str, + url: str, + fields: _TYPE_ENCODE_URL_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the URL. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": headers} + extra_kw.update(urlopen_kw) + + if fields: + url += "?" + urlencode(fields) + + return self.urlopen(method, url, **extra_kw) + + def request_encode_body( + self, + method: str, + url: str, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :func:`urllib3.encode_multipart_formdata` is used to encode + the payload with the appropriate content type. Otherwise + :func:`urllib.parse.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request + signing, such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example:: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimic behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will + be overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param encode_multipart: + If True, encode the ``fields`` using the multipart/form-data MIME + format. + + :param multipart_boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} + body: bytes | str + + if fields: + if "body" in urlopen_kw: + raise TypeError( + "request got values for both 'fields' and 'body', can only specify one." + ) + + if encode_multipart: + body, content_type = encode_multipart_formdata( + fields, boundary=multipart_boundary + ) + else: + body, content_type = ( + urlencode(fields), # type: ignore[arg-type] + "application/x-www-form-urlencoded", + ) + + extra_kw["body"] = body + extra_kw["headers"].setdefault("Content-Type", content_type) + + extra_kw.update(urlopen_kw) + + return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py new file mode 100644 index 0000000000..bfed03985b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '2.7.0' +__version_tuple__ = version_tuple = (2, 7, 0) + +__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py new file mode 100644 index 0000000000..84e1dab945 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py @@ -0,0 +1,1099 @@ +from __future__ import annotations + +import datetime +import http.client +import logging +import os +import re +import socket +import sys +import threading +import typing +import warnings +from http.client import HTTPConnection as _HTTPConnection +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from .response import HTTPResponse + from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT + from .util.ssltransport import SSLTransport + +from ._collections import HTTPHeaderDict +from .http2 import probe as http2_probe +from .util.response import assert_header_parsing +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout +from .util.util import to_str +from .util.wait import wait_for_read + +try: # Compiled with SSL? + import ssl + + BaseSSLError = ssl.SSLError +except (ImportError, AttributeError): + ssl = None # type: ignore[assignment] + + class BaseSSLError(BaseException): # type: ignore[no-redef] + pass + + +from ._base_connection import _TYPE_BODY +from ._base_connection import ProxyConfig as ProxyConfig +from ._base_connection import _ResponseOptions as _ResponseOptions +from ._version import __version__ +from .exceptions import ( + ConnectTimeoutError, + HeaderParsingError, + NameResolutionError, + NewConnectionError, + ProxyError, + SystemTimeWarning, +) +from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ +from .util.request import body_to_chunks +from .util.ssl_ import assert_fingerprint as _assert_fingerprint +from .util.ssl_ import ( + create_urllib3_context, + is_ipaddress, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .util.ssl_match_hostname import CertificateError, match_hostname +from .util.url import Url + +# Not a no-op, we're adding this to the namespace so it can be imported. +ConnectionError = ConnectionError +BrokenPipeError = BrokenPipeError + + +log = logging.getLogger(__name__) + +port_by_scheme = {"http": 80, "https": 443} + +# When it comes time to update this value as a part of regular maintenance +# (ie test_recent_date is failing) update it to ~6 months before the current date. +RECENT_DATE = datetime.date(2025, 1, 1) + +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") + + +class HTTPConnection(_HTTPConnection): + """ + Based on :class:`http.client.HTTPConnection` but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + + Additional keyword parameters are used to configure attributes of the connection. + Accepted parameters include: + + - ``source_address``: Set the source address for the current connection. + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass: + + .. code-block:: python + + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + """ + + default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] + + #: Disable Nagle's algorithm by default. + #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` + default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + ] + + #: Whether this connection verifies the host's certificate. + is_verified: bool = False + + #: Whether this proxy connection verified the proxy host's certificate. + # If no proxy is currently connected to the value will be ``None``. + proxy_is_verified: bool | None = None + + blocksize: int + source_address: tuple[str, int] | None + socket_options: connection._TYPE_SOCKET_OPTIONS | None + + _has_connected_to_proxy: bool + _response_options: _ResponseOptions | None + _tunnel_host: str | None + _tunnel_port: int | None + _tunnel_scheme: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + super().__init__( + host=host, + port=port, + timeout=Timeout.resolve_default_timeout(timeout), + source_address=source_address, + blocksize=blocksize, + ) + self.socket_options = socket_options + self.proxy = proxy + self.proxy_config = proxy_config + + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host: str | None = None + self._tunnel_port: int | None = None + self._tunnel_scheme: str | None = None + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __repr__(self) -> str: + return f"<{self} at {id(self):#x}>" + + @property + def host(self) -> str: + """ + Getter method to remove any trailing dots that indicate the hostname is an FQDN. + + In general, SSL certificates don't include the trailing dot indicating a + fully-qualified domain name, and thus, they don't validate properly when + checked against a domain name that includes the dot. In addition, some + servers may not expect to receive the trailing dot when provided. + + However, the hostname with trailing dot is critical to DNS resolution; doing a + lookup with the trailing dot will properly only resolve the appropriate FQDN, + whereas a lookup without a trailing dot will search the system's search domain + list. Thus, it's important to keep the original host around for use only in + those cases where it's appropriate (i.e., when doing DNS lookup to establish the + actual TCP connection across which we're going to send HTTP requests). + """ + return self._dns_host.rstrip(".") + + @host.setter + def host(self, value: str) -> None: + """ + Setter for the `host` property. + + We assume that only urllib3 uses the _dns_host attribute; httplib itself + only uses `host`, and it seems reasonable that other libraries follow suit. + """ + self._dns_host = value + + def _new_conn(self) -> socket.socket: + """Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + try: + sock = connection.create_connection( + (self._dns_host, self.port), + self.timeout, + source_address=self.source_address, + socket_options=self.socket_options, + ) + except socket.gaierror as e: + raise NameResolutionError(self.host, self, e) from e + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except OSError as e: + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + sys.audit("http.client.connect", self, self.host, self.port) + + return sock + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + if scheme not in ("http", "https"): + raise ValueError( + f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" + ) + super().set_tunnel(host, port=port, headers=headers) + self._tunnel_scheme = scheme + + if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): + # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. + # When using connection_from_host, host will come without brackets. + def _wrap_ipv6(self, ip: bytes) -> bytes: + if b":" in ip and ip[0] != b"["[0]: + return b"[" + ip + b"]" + return ip + + if sys.version_info < (3, 11, 9): + # `_tunnel` copied from 3.11.13 backporting + # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 + # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 + def _tunnel(self) -> None: + _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] + connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + while True: + line = response.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise http.client.LineTooLong("header line") + if not line: + # for sites which EOF without sending a trailer + break + if line in (b"\r\n", b"\n", b""): + break + + if self.debuglevel > 0: + print("header:", line.decode()) + finally: + response.close() + + elif (3, 12) <= sys.version_info < (3, 12, 3): + # `_tunnel` copied from 3.12.11 backporting + # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 + def _tunnel(self) -> None: # noqa: F811 + connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] + + if self.debuglevel > 0: + for header in self._raw_proxy_headers: + print("header:", header.decode()) + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + + finally: + response.close() + + def connect(self) -> None: + self.sock = self._new_conn() + if self._tunnel_host: + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + if self._has_connected_to_proxy: + self.proxy_is_verified = False + + @property + def is_closed(self) -> bool: + return self.sock is None + + @property + def is_connected(self) -> bool: + if self.sock is None: + return False + return not wait_for_read(self.sock, timeout=0.0) + + @property + def has_connected_to_proxy(self) -> bool: + return self._has_connected_to_proxy + + @property + def proxy_is_forwarding(self) -> bool: + """ + Return True if a forwarding proxy is configured, else return False + """ + return bool(self.proxy) and self._tunnel_host is None + + @property + def proxy_is_tunneling(self) -> bool: + """ + Return True if a tunneling proxy is configured, else return False + """ + return self._tunnel_host is not None + + def close(self) -> None: + try: + super().close() + finally: + # Reset all stateful properties so connection + # can be re-used without leaking prior configs. + self.sock = None + self.is_verified = False + self.proxy_is_verified = None + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_scheme = None + + def putrequest( + self, + method: str, + url: str, + skip_host: bool = False, + skip_accept_encoding: bool = False, + ) -> None: + """""" + # Empty docstring because the indentation of CPython's implementation + # is broken but we don't want this method in our documentation. + match = _CONTAINS_CONTROL_CHAR_RE.search(method) + if match: + raise ValueError( + f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" + ) + + return super().putrequest( + method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding + ) + + def putheader(self, header: str, *values: str) -> None: # type: ignore[override] + """""" + if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): + super().putheader(header, *values) + elif to_str(header.lower()) not in SKIPPABLE_HEADERS: + skippable_headers = "', '".join( + [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] + ) + raise ValueError( + f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" + ) + + # `request` method's signature intentionally violates LSP. + # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + # Update the inner socket's timeout value to send the request. + # This only triggers if the connection is re-used. + if self.sock is not None: + self.sock.settimeout(self.timeout) + + # Store these values to be fed into the HTTPResponse + # object later. TODO: Remove this in favor of a real + # HTTP lifecycle mechanism. + + # We have to store these before we call .request() + # because sometimes we can still salvage a response + # off the wire even if we aren't able to completely + # send the request body. + self._response_options = _ResponseOptions( + request_method=method, + request_url=url, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + if headers is None: + headers = {} + header_keys = frozenset(to_str(k.lower()) for k in headers) + skip_accept_encoding = "accept-encoding" in header_keys + skip_host = "host" in header_keys + self.putrequest( + method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host + ) + + # Transform the body into an iterable of sendall()-able chunks + # and detect if an explicit Content-Length is doable. + chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) + chunks = chunks_and_cl.chunks + content_length = chunks_and_cl.content_length + + # When chunked is explicit set to 'True' we respect that. + if chunked: + if "transfer-encoding" not in header_keys: + self.putheader("Transfer-Encoding", "chunked") + else: + # Detect whether a framing mechanism is already in use. If so + # we respect that value, otherwise we pick chunked vs content-length + # depending on the type of 'body'. + if "content-length" in header_keys: + chunked = False + elif "transfer-encoding" in header_keys: + chunked = True + + # Otherwise we go off the recommendation of 'body_to_chunks()'. + else: + chunked = False + if content_length is None: + if chunks is not None: + chunked = True + self.putheader("Transfer-Encoding", "chunked") + else: + self.putheader("Content-Length", str(content_length)) + + # Now that framing headers are out of the way we send all the other headers. + if "user-agent" not in header_keys: + self.putheader("User-Agent", _get_default_user_agent()) + for header, value in headers.items(): + self.putheader(header, value) + self.endheaders() + + # If we're given a body we start sending that in chunks. + if chunks is not None: + for chunk in chunks: + # Sending empty chunks isn't allowed for TE: chunked + # as it indicates the end of the body. + if not chunk: + continue + if isinstance(chunk, str): + chunk = chunk.encode("utf-8") + if chunked: + self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) + else: + self.send(chunk) + + # Regardless of whether we have a body or not, if we're in + # chunked mode we want to send an explicit empty chunk. + if chunked: + self.send(b"0\r\n\r\n") + + def request_chunked( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + ) -> None: + """ + Alternative to the common request method, which sends the + body with chunked encoding and not as one block + """ + warnings.warn( + "HTTPConnection.request_chunked() is deprecated and will be removed " + "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", + category=FutureWarning, + stacklevel=2, + ) + self.request(method, url, body=body, headers=headers, chunked=True) + + def getresponse( # type: ignore[override] + self, + ) -> HTTPResponse: + """ + Get the response from the server. + + If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. + + If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. + """ + # Raise the same error as http.client.HTTPConnection + if self._response_options is None: + raise ResponseNotReady() + + # Reset this attribute for being used again. + resp_options = self._response_options + self._response_options = None + + # Since the connection's timeout value may have been updated + # we need to set the timeout on the socket. + self.sock.settimeout(self.timeout) + + # This is needed here to avoid circular import errors + from .response import HTTPResponse + + # Save a reference to the shutdown function before ownership is passed + # to httplib_response + # TODO should we implement it everywhere? + _shutdown = getattr(self.sock, "shutdown", None) + + # Get the response from http.client.HTTPConnection + httplib_response = super().getresponse() + + try: + assert_header_parsing(httplib_response.msg) + except (HeaderParsingError, TypeError) as hpe: + log.warning( + "Failed to parse headers (url=%s): %s", + _url_from_connection(self, resp_options.request_url), + hpe, + exc_info=True, + ) + + headers = HTTPHeaderDict(httplib_response.msg.items()) + + response = HTTPResponse( + body=httplib_response, + headers=headers, + status=httplib_response.status, + version=httplib_response.version, + version_string=getattr(self, "_http_vsn_str", "HTTP/?"), + reason=httplib_response.reason, + preload_content=resp_options.preload_content, + decode_content=resp_options.decode_content, + original_response=httplib_response, + enforce_content_length=resp_options.enforce_content_length, + request_method=resp_options.request_method, + request_url=resp_options.request_url, + sock_shutdown=_shutdown, + ) + return response + + +class HTTPSConnection(HTTPConnection): + """ + Many of the parameters to this constructor are passed to the underlying SSL + socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. + """ + + default_port = port_by_scheme["https"] # type: ignore[misc] + + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_fingerprint: str | None = None + _connect_callback: typing.Callable[..., None] | None = None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + # cert_reqs depends on ssl_context so calculate last. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + self.cert_reqs = cert_reqs + self._connect_callback = None + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + """ + This method should only be called once, before the connection is used. + """ + warnings.warn( + "HTTPSConnection.set_cert() is deprecated and will be removed " + "in urllib3 v3.0. Instead provide the parameters to the " + "HTTPSConnection constructor.", + category=FutureWarning, + stacklevel=2, + ) + + # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also + # have an SSLContext object in which case we'll use its verify_mode. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + def connect(self) -> None: + # Today we don't need to be doing this step before the /actual/ socket + # connection, however in the future we'll need to decide whether to + # create a new socket or re-use an existing "shared" socket as a part + # of the HTTP/2 handshake dance. + if self._tunnel_host is not None and self._tunnel_port is not None: + probe_http2_host = self._tunnel_host + probe_http2_port = self._tunnel_port + else: + probe_http2_host = self.host + probe_http2_port = self.port + + # Check if the target origin supports HTTP/2. + # If the value comes back as 'None' it means that the current thread + # is probing for HTTP/2 support. Otherwise, we're waiting for another + # probe to complete, or we get a value right away. + target_supports_http2: bool | None + if "h2" in ssl_.ALPN_PROTOCOLS: + target_supports_http2 = http2_probe.acquire_and_get( + host=probe_http2_host, port=probe_http2_port + ) + else: + # If HTTP/2 isn't going to be offered it doesn't matter if + # the target supports HTTP/2. Don't want to make a probe. + target_supports_http2 = False + + if self._connect_callback is not None: + self._connect_callback( + "before connect", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + try: + sock: socket.socket | ssl.SSLSocket + self.sock = sock = self._new_conn() + server_hostname: str = self.host + tls_in_tls = False + + # Do we need to establish a tunnel? + if self.proxy_is_tunneling: + # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. + if self._tunnel_scheme == "https": + # _connect_tls_proxy will verify and assign proxy_is_verified + self.sock = sock = self._connect_tls_proxy(self.host, sock) + tls_in_tls = True + elif self._tunnel_scheme == "http": + self.proxy_is_verified = False + + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + self._tunnel() + # Override the host with the one we're requesting data from. + server_hostname = typing.cast(str, self._tunnel_host) + + if self.server_hostname is not None: + server_hostname = self.server_hostname + + is_time_off = datetime.date.today() < RECENT_DATE + if is_time_off: + warnings.warn( + ( + f"System time is way off (before {RECENT_DATE}). This will probably " + "lead to SSL verification errors" + ), + SystemTimeWarning, + ) + + # Remove trailing '.' from fqdn hostnames to allow certificate validation + server_hostname_rm_dot = server_hostname.rstrip(".") + + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock=sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + server_hostname=server_hostname_rm_dot, + ssl_context=self.ssl_context, + tls_in_tls=tls_in_tls, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ) + self.sock = sock_and_verified.socket + + # If an error occurs during connection/handshake we may need to release + # our lock so another connection can probe the origin. + except BaseException: + if self._connect_callback is not None: + self._connect_callback( + "after connect failure", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + if target_supports_http2 is None: + http2_probe.set_and_release( + host=probe_http2_host, port=probe_http2_port, supports_http2=None + ) + raise + + # If this connection doesn't know if the origin supports HTTP/2 + # we report back to the HTTP/2 probe our result. + if target_supports_http2 is None: + supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" + http2_probe.set_and_release( + host=probe_http2_host, + port=probe_http2_port, + supports_http2=supports_http2, + ) + + # Forwarding proxies can never have a verified target since + # the proxy is the one doing the verification. Should instead + # use a CONNECT tunnel in order to verify the target. + # See: https://github.com/urllib3/urllib3/issues/3267. + if self.proxy_is_forwarding: + self.is_verified = False + else: + self.is_verified = sock_and_verified.is_verified + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + # Set `self.proxy_is_verified` unless it's already set while + # establishing a tunnel. + if self._has_connected_to_proxy and self.proxy_is_verified is None: + self.proxy_is_verified = sock_and_verified.is_verified + + def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: + """ + Establish a TLS connection to the proxy using the provided SSL context. + """ + # `_connect_tls_proxy` is called when self._tunnel_host is truthy. + proxy_config = typing.cast(ProxyConfig, self.proxy_config) + ssl_context = proxy_config.ssl_context + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + server_hostname=hostname, + ssl_context=ssl_context, + assert_hostname=proxy_config.assert_hostname, + assert_fingerprint=proxy_config.assert_fingerprint, + # Features that aren't implemented for proxies yet: + cert_file=None, + key_file=None, + key_password=None, + tls_in_tls=False, + ) + self.proxy_is_verified = sock_and_verified.is_verified + return sock_and_verified.socket # type: ignore[return-value] + + +class _WrappedAndVerifiedSocket(typing.NamedTuple): + """ + Wrapped socket and whether the connection is + verified after the TLS handshake + """ + + socket: ssl.SSLSocket | SSLTransport + is_verified: bool + + +def _ssl_wrap_socket_and_match_hostname( + sock: socket.socket, + *, + cert_reqs: None | str | int, + ssl_version: None | str | int, + ssl_minimum_version: int | None, + ssl_maximum_version: int | None, + cert_file: str | None, + key_file: str | None, + key_password: str | None, + ca_certs: str | None, + ca_cert_dir: str | None, + ca_cert_data: None | str | bytes, + assert_hostname: None | str | typing.Literal[False], + assert_fingerprint: str | None, + server_hostname: str | None, + ssl_context: ssl.SSLContext | None, + tls_in_tls: bool = False, +) -> _WrappedAndVerifiedSocket: + """Logic for constructing an SSLContext from all TLS parameters, passing + that down into ssl_wrap_socket, and then doing certificate verification + either via hostname or fingerprint. This function exists to guarantee + that both proxies and targets have the same behavior when connecting via TLS. + """ + default_ssl_context = False + if ssl_context is None: + default_ssl_context = True + context = create_urllib3_context( + ssl_version=resolve_ssl_version(ssl_version), + ssl_minimum_version=ssl_minimum_version, + ssl_maximum_version=ssl_maximum_version, + cert_reqs=resolve_cert_reqs(cert_reqs), + ) + else: + context = ssl_context + + context.verify_mode = resolve_cert_reqs(cert_reqs) + + # In some cases, we want to verify hostnames ourselves + if ( + # `ssl` can't verify fingerprints or alternate hostnames + assert_fingerprint + or assert_hostname + # assert_hostname can be set to False to disable hostname checking + or assert_hostname is False + # We still support OpenSSL 1.0.2, which prevents us from verifying + # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 + or ssl_.IS_PYOPENSSL + or not ssl_.HAS_NEVER_CHECK_COMMON_NAME + ): + context.check_hostname = False + + # Try to load OS default certs if none are given. We need to do the hasattr() check + # for custom pyOpenSSL SSLContext objects because they don't support + # load_default_certs(). + if ( + not ca_certs + and not ca_cert_dir + and not ca_cert_data + and default_ssl_context + and hasattr(context, "load_default_certs") + ): + context.load_default_certs() + + # Ensure that IPv6 addresses are in the proper format and don't have a + # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses + # and interprets them as DNS hostnames. + if server_hostname is not None: + normalized = server_hostname.strip("[]") + if "%" in normalized: + normalized = normalized[: normalized.rfind("%")] + if is_ipaddress(normalized): + server_hostname = normalized + + ssl_sock = ssl_wrap_socket( + sock=sock, + keyfile=key_file, + certfile=cert_file, + key_password=key_password, + ca_certs=ca_certs, + ca_cert_dir=ca_cert_dir, + ca_cert_data=ca_cert_data, + server_hostname=server_hostname, + ssl_context=context, + tls_in_tls=tls_in_tls, + ) + + try: + if assert_fingerprint: + _assert_fingerprint( + ssl_sock.getpeercert(binary_form=True), assert_fingerprint + ) + elif ( + context.verify_mode != ssl.CERT_NONE + and not context.check_hostname + and assert_hostname is not False + ): + cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] + + # Need to signal to our match_hostname whether to use 'commonName' or not. + # If we're using our own constructed SSLContext we explicitly set 'False' + # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. + if default_ssl_context: + hostname_checks_common_name = False + else: + hostname_checks_common_name = ( + getattr(context, "hostname_checks_common_name", False) or False + ) + + _match_hostname( + cert, + assert_hostname or server_hostname, # type: ignore[arg-type] + hostname_checks_common_name, + ) + + return _WrappedAndVerifiedSocket( + socket=ssl_sock, + is_verified=context.verify_mode == ssl.CERT_REQUIRED + or bool(assert_fingerprint), + ) + except BaseException: + ssl_sock.close() + raise + + +def _match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + asserted_hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + # Our upstream implementation of ssl.match_hostname() + # only applies this normalization to IP addresses so it doesn't + # match DNS SANs so we do the same thing! + stripped_hostname = asserted_hostname.strip("[]") + if is_ipaddress(stripped_hostname): + asserted_hostname = stripped_hostname + + try: + match_hostname(cert, asserted_hostname, hostname_checks_common_name) + except CertificateError as e: + log.warning( + "Certificate did not match expected hostname: %s. Certificate: %s", + asserted_hostname, + cert, + ) + # Add cert to exception and reraise so client code can inspect + # the cert when catching the exception, if they want to + e._peer_cert = cert # type: ignore[attr-defined] + raise + + +def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: + # Look for the phrase 'wrong version number', if found + # then we should warn the user that we're very sure that + # this proxy is HTTP-only and they have a configuration issue. + error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) + is_likely_http_proxy = ( + "wrong version number" in error_normalized + or "unknown protocol" in error_normalized + or "record layer failure" in error_normalized + ) + http_proxy_warning = ( + ". Your proxy appears to only use HTTP and not HTTPS, " + "try changing your proxy URL to be HTTP. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#https-proxy-error-http-proxy" + ) + new_err = ProxyError( + f"Unable to connect to proxy" + f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", + err, + ) + new_err.__cause__ = err + return new_err + + +def _get_default_user_agent() -> str: + return f"python-urllib3/{__version__}" + + +class DummyConnection: + """Used to detect a failed ConnectionCls import.""" + + +if not ssl: + HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 + + +VerifiedHTTPSConnection = HTTPSConnection + + +def _url_from_connection( + conn: HTTPConnection | HTTPSConnection, path: str | None = None +) -> str: + """Returns the URL from a given connection. This is mainly used for testing and logging.""" + + scheme = "https" if isinstance(conn, HTTPSConnection) else "http" + + return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py new file mode 100644 index 0000000000..70fbc5e725 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py @@ -0,0 +1,1191 @@ +from __future__ import annotations + +import errno +import logging +import queue +import sys +import typing +import warnings +import weakref +from socket import timeout as SocketTimeout +from types import TracebackType + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._request_methods import RequestMethods +from .connection import ( + BaseSSLError, + BrokenPipeError, + DummyConnection, + HTTPConnection, + HTTPException, + HTTPSConnection, + ProxyConfig, + _wrap_proxy_error, +) +from .connection import port_by_scheme as port_by_scheme +from .exceptions import ( + ClosedPoolError, + EmptyPoolError, + FullPoolError, + HostChangedError, + InsecureRequestWarning, + LocationValueError, + MaxRetryError, + NewConnectionError, + ProtocolError, + ProxyError, + ReadTimeoutError, + SSLError, + TimeoutError, +) +from .response import BaseHTTPResponse +from .util.connection import is_connection_dropped +from .util.proxy import connection_requires_http_tunnel +from .util.request import _TYPE_BODY_POSITION, set_file_position +from .util.retry import Retry +from .util.ssl_match_hostname import CertificateError +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout +from .util.url import Url, _encode_target +from .util.url import _normalize_host as normalize_host +from .util.url import parse_url +from .util.util import to_str + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + + from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + +_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] + + +# Pool objects +class ConnectionPool: + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + + .. note:: + ConnectionPool.urlopen() does not normalize or percent-encode target URIs + which is useful if your target server doesn't support percent-encoded + target URIs. + """ + + scheme: str | None = None + QueueCls = queue.LifoQueue + + def __init__(self, host: str, port: int | None = None) -> None: + if not host: + raise LocationValueError("No host specified.") + + self.host = _normalize_host(host, scheme=self.scheme) + self.port = port + + # This property uses 'normalize_host()' (not '_normalize_host()') + # to avoid removing square braces around IPv6 addresses. + # This value is sent to `HTTPConnection.set_tunnel()` if called + # because square braces are required for HTTP CONNECT tunneling. + self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.close() + # Return False to re-raise any potential exceptions + return False + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} + + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`http.client.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`http.client.HTTPConnection`. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to False, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param retries: + Retry configuration to use by default with requests in this pool. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.ProxyManager` + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.ProxyManager` + + :param \\**conn_kw: + Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, + :class:`urllib3.connection.HTTPSConnection` instances. + """ + + scheme = "http" + ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + _proxy_config: ProxyConfig | None = None, + **conn_kw: typing.Any, + ): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + if retries is None: + retries = Retry.DEFAULT + + self.timeout = timeout + self.retries = retries + + self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + self.proxy_config = _proxy_config + + # Fill the queue up so that doing get() on it will block properly + for _ in range(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + self.conn_kw = conn_kw + + if self.proxy: + # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. + # Defaulting `socket_options` to an empty list avoids it defaulting to + # ``HTTPConnection.default_socket_options``. + self.conn_kw.setdefault("socket_options", []) + + self.conn_kw["proxy"] = self.proxy + self.conn_kw["proxy_config"] = self.proxy_config + + # Do not pass 'self' as callback to 'finalize'. + # Then the 'finalize' would keep an endless living (leak) to self. + # By just passing a reference to the pool allows the garbage collector + # to free self if nobody else has a reference to it. + pool = self.pool + + # Close all the HTTPConnections in the pool before the + # HTTPConnectionPool object is garbage collected. + weakref.finalize(self, _close_pool_connections, pool) + + def _new_conn(self) -> BaseHTTPConnection: + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTP connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "80", + ) + + conn = self.ConnectionCls( + host=self.host, + port=self.port, + timeout=self.timeout.connect_timeout, + **self.conn_kw, + ) + return conn + + def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + + if self.pool is None: + raise ClosedPoolError(self, "Pool is closed.") + + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: + + except queue.Empty: + if self.block: + raise EmptyPoolError( + self, + "Pool is empty and a new connection can't be opened due to blocking mode.", + ) from None + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.debug("Resetting dropped connection: %s", self.host) + conn.close() + + return conn or self._new_conn() + + def _put_conn(self, conn: BaseHTTPConnection | None) -> None: + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + if self.pool is not None: + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except queue.Full: + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + if self.block: + # This should never happen if you got the conn from self._get_conn + raise FullPoolError( + self, + "Pool reached maximum size and no more connections are allowed.", + ) from None + + log.warning( + "Connection pool is full, discarding connection: %s. Connection pool size: %s", + self.host, + self.pool.qsize(), + ) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + + def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: + # Nothing to do for HTTP connections. + pass + + def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: + """Helper that always returns a :class:`urllib3.util.Timeout`""" + if timeout is _DEFAULT_TIMEOUT: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _raise_timeout( + self, + err: BaseSSLError | OSError | SocketTimeout, + url: str, + timeout_value: _TYPE_TIMEOUT | None, + ) -> None: + """Is the error actually a timeout? Will raise a ReadTimeout or pass""" + + if isinstance(err, SocketTimeout): + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + # See the above comment about EAGAIN in Python 3. + if hasattr(err, "errno") and err.errno in _blocking_errnos: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + def _make_request( + self, + conn: BaseHTTPConnection, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | None = None, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + chunked: bool = False, + response_conn: BaseHTTPConnection | None = None, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> BaseHTTPResponse: + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param response_conn: + Set this to ``None`` if you will handle releasing the connection or + set the connection to have the response release it. + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + timeout_obj.start_connect() + conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) + + try: + # Trigger any extra validation we need to do. + try: + self._validate_conn(conn) + except (SocketTimeout, BaseSSLError) as e: + self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) + raise + + # _validate_conn() starts the connection to an HTTPS proxy + # so we need to wrap errors with 'ProxyError' here too. + except ( + OSError, + NewConnectionError, + TimeoutError, + BaseSSLError, + CertificateError, + SSLError, + ) as e: + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + # If the connection didn't successfully connect to it's proxy + # then there + if isinstance( + new_e, (OSError, NewConnectionError, TimeoutError, SSLError) + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + raise new_e + + # conn.request() calls http.client.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + try: + conn.request( + method, + url, + body=body, + headers=headers, + chunked=chunked, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + # We are swallowing BrokenPipeError (errno.EPIPE) since the server is + # legitimately able to close the connection after sending a valid response. + # With this behaviour, the received response is still readable. + except BrokenPipeError: + pass + except OSError as e: + # MacOS/Linux + # EPROTOTYPE and ECONNRESET are needed on macOS + # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ + # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. + if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: + raise + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + if not conn.is_closed: + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={read_timeout})" + ) + conn.timeout = read_timeout + + # Receive the response from the server + try: + response = conn.getresponse() + except (BaseSSLError, OSError) as e: + self._raise_timeout(err=e, url=url, timeout_value=read_timeout) + raise + + # Set properties that are used by the pooling layer. + response.retries = retries + response._connection = response_conn # type: ignore[attr-defined] + response._pool = self # type: ignore[attr-defined] + + log.debug( + '%s://%s:%s "%s %s %s" %s %s', + self.scheme, + self.host, + self.port, + method, + url, + response.version_string, + response.status, + response.length_remaining, + ) + + return response + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + if self.pool is None: + return + # Disable access to the pool + old_pool, self.pool = self.pool, None + + # Close all the HTTPConnections in the pool. + _close_pool_connections(old_pool) + + def is_same_host(self, url: str) -> bool: + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith("/"): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + if host is not None: + host = _normalize_host(host, scheme=scheme) + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + redirect: bool = True, + assert_same_host: bool = True, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + pool_timeout: int | None = None, + release_conn: bool | None = None, + chunked: bool = False, + body_pos: _TYPE_BODY_POSITION | None = None, + preload_content: bool = True, + decode_content: bool = True, + **response_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method + such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When ``False``, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of ``preload_content`` + which defaults to ``True``. + + :param bool chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param int body_pos: + Position to seek to in file-like body in the event of a retry or + redirect. Typically this won't need to be set because urllib3 will + auto-populate the value when needed. + """ + # Ensure that the URL we're connecting to is properly encoded + if url.startswith("/"): + # URLs starting with / are inherently schemeless. + url = to_str(_encode_target(url)) + destination_scheme = None + else: + parsed_url = parse_url(url) + destination_scheme = parsed_url.scheme + url = to_str(parsed_url.url) + + if headers is None: + headers = self.headers + + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) + + if release_conn is None: + release_conn = preload_content + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries) + + conn = None + + # Track whether `conn` needs to be released before + # returning/raising/recursing. Update this variable if necessary, and + # leave `release_conn` constant throughout the function. That way, if + # the function recurses, the original value of `release_conn` will be + # passed down into the recursive call, and its value will be respected. + # + # See issue #651 [1] for details. + # + # [1] + release_this_conn = release_conn + + http_tunnel_required = connection_requires_http_tunnel( + self.proxy, self.proxy_config, destination_scheme + ) + + # Merge the proxy headers. Only done when not using HTTP CONNECT. We + # have to copy the headers dict so we can safely change it without those + # changes being reflected in anyone else's copy. + if not http_tunnel_required: + headers = headers.copy() # type: ignore[attr-defined] + headers.update(self.proxy_headers) # type: ignore[union-attr] + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + # Keep track of whether we cleanly exited the except block. This + # ensures we do proper cleanup in finally. + clean_exit = False + + # Rewind body position, if needed. Record current position + # for future rewinds in the event of a redirect/retry. + body_pos = set_file_position(body, body_pos) + + try: + # Request a connection from the queue. + timeout_obj = self._get_timeout(timeout) + conn = self._get_conn(timeout=pool_timeout) + + conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] + + # Is this a closed/new connection that requires CONNECT tunnelling? + if self.proxy is not None and http_tunnel_required and conn.is_closed: + try: + self._prepare_proxy(conn) + except (BaseSSLError, OSError, SocketTimeout) as e: + self._raise_timeout( + err=e, url=self.proxy.url, timeout_value=conn.timeout + ) + raise + + # If we're going to release the connection in ``finally:``, then + # the response doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = conn if not release_conn else None + + # Make the request on the HTTPConnection object + response = self._make_request( + conn, + method, + url, + timeout=timeout_obj, + body=body, + headers=headers, + chunked=chunked, + retries=retries, + response_conn=response_conn, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Everything went great! + clean_exit = True + + except EmptyPoolError: + # Didn't get a connection from the pool, no need to clean up + clean_exit = True + release_this_conn = False + raise + + except ( + TimeoutError, + HTTPException, + OSError, + ProtocolError, + BaseSSLError, + SSLError, + CertificateError, + ProxyError, + ) as e: + # Discard the connection for these exceptions. It will be + # replaced during the next _get_conn() call. + clean_exit = False + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + if isinstance( + new_e, + ( + OSError, + NewConnectionError, + TimeoutError, + SSLError, + HTTPException, + ), + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + elif isinstance(new_e, (OSError, HTTPException)): + new_e = ProtocolError("Connection aborted.", new_e) + + retries = retries.increment( + method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] + ) + retries.sleep() + + # Keep track of the error for the retry warning. + err = e + + finally: + if not clean_exit: + # We hit some kind of exception, handled or otherwise. We need + # to throw the connection away unless explicitly told not to. + # Close the connection, set the variable to None, and make sure + # we put the None back in the pool to avoid leaking it. + if conn: + conn.close() + conn = None + release_this_conn = True + + if release_this_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning( + "Retrying (%r) after connection broken by '%r': %s", retries, err, url + ) + return self.urlopen( + method, + url, + body, + headers, + retries, + redirect, + assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + body = None + headers = HTTPHeaderDict(headers)._prepare_for_method_change() + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # self.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not self.is_same_host( + redirect_location + ): + new_headers = headers.copy() # type: ignore[union-attr] + for header in headers: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + headers = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep_for_retry(response) + log.debug("Redirecting %s -> %s", url, redirect_location) + return self.urlopen( + method, + redirect_location, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Check if we should retry the HTTP response. + has_retry_after = bool(response.headers.get("Retry-After")) + if retries.is_retry(method, response.status, has_retry_after): + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_status: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep(response) + log.debug("Retry: %s", url) + return self.urlopen( + method, + url, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, + ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` + is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade + the connection socket into an SSL socket. + """ + + scheme = "https" + ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + ssl_version: int | str | None = None, + ssl_minimum_version: ssl.TLSVersion | None = None, + ssl_maximum_version: ssl.TLSVersion | None = None, + assert_hostname: str | typing.Literal[False] | None = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + **conn_kw: typing.Any, + ) -> None: + super().__init__( + host, + port, + timeout, + maxsize, + block, + headers, + retries, + _proxy, + _proxy_headers, + **conn_kw, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.ca_certs = ca_certs + self.ca_cert_dir = ca_cert_dir + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] + """Establishes a tunnel connection through HTTP CONNECT.""" + if self.proxy and self.proxy.scheme == "https": + tunnel_scheme = "https" + else: + tunnel_scheme = "http" + + conn.set_tunnel( + scheme=tunnel_scheme, + host=self._tunnel_host, + port=self.port, + headers=self.proxy_headers, + ) + conn.connect() + + def _new_conn(self) -> BaseHTTPSConnection: + """ + Return a fresh :class:`urllib3.connection.HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTPS connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "443", + ) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] + raise ImportError( + "Can't connect to HTTPS URL because the SSL module is not available." + ) + + actual_host: str = self.host + actual_port = self.port + if self.proxy is not None and self.proxy.host is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + return self.ConnectionCls( + host=actual_host, + port=actual_port, + timeout=self.timeout.connect_timeout, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + **self.conn_kw, + ) + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + super()._validate_conn(conn) + + # Force connect early to allow us to validate the connection. + if conn.is_closed: + conn.connect() + + # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 + if not conn.is_verified and not conn.proxy_is_verified: + warnings.warn( + ( + f"Unverified HTTPS request is being made to host '{conn.host}'. " + "Adding certificate verification is strongly advised. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#tls-warnings" + ), + InsecureRequestWarning, + ) + + +def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \\**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example:: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + port = port or port_by_scheme.get(scheme, 80) + if scheme == "https": + return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + else: + return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + """ + Normalize hosts for comparisons and use with sockets. + """ + + host = normalize_host(host, scheme) + + # httplib doesn't like it when we include brackets in IPv6 addresses + # Specifically, if we include brackets but also pass the port then + # httplib crazily doubles up the square brackets on the Host header. + # Instead, we need to make sure we never pass ``None`` as the port. + # However, for backward compatibility reasons we can't actually + # *assert* that. See http://bugs.python.org/issue28539 + if host and host.startswith("[") and host.endswith("]"): + host = host[1:-1] + return host + + +def _url_from_pool( + pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None +) -> str: + """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" + return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url + + +def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: + """Drains a queue of connections and closes each one.""" + try: + while True: + conn = pool.get(block=False) + if conn: + conn.close() + except queue.Empty: + pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py new file mode 100644 index 0000000000..e5b62b25e9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import urllib3.connection + +from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection + + +def inject_into_urllib3() -> None: + # override connection classes to use emscripten specific classes + # n.b. mypy complains about the overriding of classes below + # if it isn't ignored + HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection + HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection + urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] + urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] + urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py new file mode 100644 index 0000000000..63f79dd3be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import os +import typing + +# use http.client.HTTPException for consistency with non-emscripten +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady + +from ..._base_connection import _TYPE_BODY +from ...connection import HTTPConnection, ProxyConfig, port_by_scheme +from ...exceptions import TimeoutError +from ...response import BaseHTTPResponse +from ...util.connection import _TYPE_SOCKET_OPTIONS +from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from ...util.url import Url +from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request +from .request import EmscriptenRequest +from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + + +class EmscriptenHTTPConnection: + default_port: typing.ClassVar[int] = port_by_scheme["http"] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + timeout: None | (float) + + host: str + port: int + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool = False + proxy_is_verified: bool | None = None + + response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper + _response: EmscriptenResponse | None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + self.host = host + self.port = port + self.timeout = timeout if isinstance(timeout, float) else 0.0 + self.scheme = "http" + self._closed = True + self._response = None + # ignore these things because we don't + # have control over that stuff + self.proxy = None + self.proxy_config = None + self.blocksize = blocksize + self.source_address = None + self.socket_options = None + self.is_verified = False + + def set_tunnel( + self, + host: str, + port: int | None = 0, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + pass + + def connect(self) -> None: + pass + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + self._closed = False + if url.startswith("/"): + if self.port is not None: + port = f":{self.port}" + else: + port = "" + # no scheme / host / port included, make a full url + url = f"{self.scheme}://{self.host}{port}{url}" + request = EmscriptenRequest( + url=url, + method=method, + timeout=self.timeout if self.timeout else 0, + decode_content=decode_content, + ) + request.set_body(body) + if headers: + for k, v in headers.items(): + request.set_header(k, v) + self._response = None + try: + if not preload_content: + self._response = send_streaming_request(request) + if self._response is None: + self._response = send_request(request) + except _TimeoutError as e: + raise TimeoutError(e.message) from e + except _RequestError as e: + raise HTTPException(e.message) from e + + def getresponse(self) -> BaseHTTPResponse: + if self._response is not None: + return EmscriptenHttpResponseWrapper( + internal_response=self._response, + url=self._response.request.url, + connection=self, + ) + else: + raise ResponseNotReady() + + def close(self) -> None: + self._closed = True + self._response = None + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + return self._closed + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + return True + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + return False + + +class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): + default_port = port_by_scheme["https"] + # all this is basically ignored, as browser handles https + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + cert_file: str | None + key_file: str | None + key_password: str | None + ssl_context: typing.Any | None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None = None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: ( + None | _TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: typing.Any | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + self.scheme = "https" + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + self.cert_reqs = None + + # The browser will automatically verify all requests. + # We have no control over that setting. + self.is_verified = True + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + pass + + +# verify that this class implements BaseHTTP(s) connection correctly +if typing.TYPE_CHECKING: + _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) + _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js new file mode 100644 index 0000000000..faf141e1fa --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js @@ -0,0 +1,110 @@ +let Status = { + SUCCESS_HEADER: -1, + SUCCESS_EOF: -2, + ERROR_TIMEOUT: -3, + ERROR_EXCEPTION: -4, +}; + +let connections = new Map(); +let nextConnectionID = 1; +const encoder = new TextEncoder(); + +self.addEventListener("message", async function (event) { + if (event.data.close) { + let connectionID = event.data.close; + connections.delete(connectionID); + return; + } else if (event.data.getMore) { + let connectionID = event.data.getMore; + let { curOffset, value, reader, intBuffer, byteBuffer } = + connections.get(connectionID); + // if we still have some in buffer, then just send it back straight away + if (!value || curOffset >= value.length) { + // read another buffer if required + try { + let readResponse = await reader.read(); + + if (readResponse.done) { + // read everything - clear connection and return + connections.delete(connectionID); + Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); + Atomics.notify(intBuffer, 0); + // finished reading successfully + // return from event handler + return; + } + curOffset = 0; + connections.get(connectionID).value = readResponse.value; + value = readResponse.value; + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } + + // send as much buffer as we can + let curLen = value.length - curOffset; + if (curLen > byteBuffer.length) { + curLen = byteBuffer.length; + } + byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); + + Atomics.store(intBuffer, 0, curLen); // store current length in bytes + Atomics.notify(intBuffer, 0); + curOffset += curLen; + connections.get(connectionID).curOffset = curOffset; + + return; + } else { + // start fetch + let connectionID = nextConnectionID; + nextConnectionID += 1; + const intBuffer = new Int32Array(event.data.buffer); + const byteBuffer = new Uint8Array(event.data.buffer, 8); + try { + const response = await fetch(event.data.url, event.data.fetchParams); + // return the headers first via textencoder + var headers = []; + for (const pair of response.headers.entries()) { + headers.push([pair[0], pair[1]]); + } + let headerObj = { + headers: headers, + status: response.status, + connectionID, + }; + const headerText = JSON.stringify(headerObj); + let headerBytes = encoder.encode(headerText); + let written = headerBytes.length; + byteBuffer.set(headerBytes); + intBuffer[1] = written; + // make a connection + connections.set(connectionID, { + reader: response.body.getReader(), + intBuffer: intBuffer, + byteBuffer: byteBuffer, + value: undefined, + curOffset: 0, + }); + // set header ready + Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); + Atomics.notify(intBuffer, 0); + // all fetching after this goes through a new postmessage call with getMore + // this allows for parallel requests + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } +}); +self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py new file mode 100644 index 0000000000..612cfddc4c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py @@ -0,0 +1,726 @@ +""" +Support for streaming http requests in emscripten. + +A few caveats - + +If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled +https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md +*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the +JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case +timeouts and streaming should just work. + +Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. + +This approach has several caveats: + +Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. +Streaming only works if you're running pyodide in a web worker. + +Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch +operation, so it requires that you have crossOriginIsolation enabled, by serving over https +(or from localhost) with the two headers below set: + + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp + +You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in +JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole +request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. + +Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once +control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. + +NB: in this code, there are a lot of JavaScript objects. They are named js_* +to make it clear what type of object they are. +""" + +from __future__ import annotations + +import io +import json +from email.parser import Parser +from importlib.resources import files +from typing import TYPE_CHECKING, Any + +import js # type: ignore[import-not-found] +from pyodide.ffi import ( # type: ignore[import-not-found] + JsArray, + JsException, + JsProxy, + to_js, +) + +if TYPE_CHECKING: + from typing_extensions import Buffer + +from .request import EmscriptenRequest +from .response import EmscriptenResponse + +""" +There are some headers that trigger unintended CORS preflight requests. +See also https://github.com/koenvo/pyodide-http/issues/22 +""" +HEADERS_TO_IGNORE = ("user-agent",) + +SUCCESS_HEADER = -1 +SUCCESS_EOF = -2 +ERROR_TIMEOUT = -3 +ERROR_EXCEPTION = -4 + + +class _RequestError(Exception): + def __init__( + self, + message: str | None = None, + *, + request: EmscriptenRequest | None = None, + response: EmscriptenResponse | None = None, + ): + self.request = request + self.response = response + self.message = message + super().__init__(self.message) + + +class _StreamingError(_RequestError): + pass + + +class _TimeoutError(_RequestError): + pass + + +def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: + return to_js(dict_val, dict_converter=js.Object.fromEntries) + + +class _ReadStream(io.RawIOBase): + def __init__( + self, + int_buffer: JsArray, + byte_buffer: JsArray, + timeout: float, + worker: JsProxy, + connection_id: int, + request: EmscriptenRequest, + ): + self.int_buffer = int_buffer + self.byte_buffer = byte_buffer + self.read_pos = 0 + self.read_len = 0 + self.connection_id = connection_id + self.worker = worker + self.timeout = int(1000 * timeout) if timeout > 0 else None + self.is_live = True + self._is_closed = False + self.request: EmscriptenRequest | None = request + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.int_buffer = None + self.byte_buffer = None + self._is_closed = True + self.request = None + if self.is_live: + self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) + self.is_live = False + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def readinto(self, byte_obj: Buffer) -> int: + if not self.int_buffer: + raise _StreamingError( + "No buffer for stream in _ReadStream.readinto", + request=self.request, + response=None, + ) + if self.read_len == 0: + # wait for the worker to send something + js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) + self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) + if ( + js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) + == "timed-out" + ): + raise _TimeoutError + data_len = self.int_buffer[0] + if data_len > 0: + self.read_len = data_len + self.read_pos = 0 + elif data_len == ERROR_EXCEPTION: + string_len = self.int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", + request=self.request, + response=None, + ) + else: + # EOF, free the buffers and return zero + # and free the request + self.is_live = False + self.close() + return 0 + # copy from int32array to python bytes + ret_length = min(self.read_len, len(memoryview(byte_obj))) + subarray = self.byte_buffer.subarray( + self.read_pos, self.read_pos + ret_length + ).to_py() + memoryview(byte_obj)[0:ret_length] = subarray + self.read_len -= ret_length + self.read_pos += ret_length + return ret_length + + +class _StreamingFetcher: + def __init__(self) -> None: + # make web-worker and data buffer on startup + self.streaming_ready = False + streaming_worker_code = ( + files(__package__) + .joinpath("emscripten_fetch_worker.js") + .read_text(encoding="utf-8") + ) + js_data_blob = js.Blob.new( + to_js([streaming_worker_code], create_pyproxies=False), + _obj_from_dict({"type": "application/javascript"}), + ) + + def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: + def onMsg(e: JsProxy) -> None: + self.streaming_ready = True + js_resolve_fn(e) + + def onErr(e: JsProxy) -> None: + js_reject_fn(e) # Defensive: never happens in ci + + self.js_worker.onmessage = onMsg + self.js_worker.onerror = onErr + + js_data_url = js.URL.createObjectURL(js_data_blob) + self.js_worker = js.globalThis.Worker.new(js_data_url) + self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) + + def send(self, request: EmscriptenRequest) -> EmscriptenResponse: + headers = { + k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE + } + + body = request.body + fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} + # start the request off in the worker + timeout = int(1000 * request.timeout) if request.timeout > 0 else None + js_shared_buffer = js.SharedArrayBuffer.new(1048576) + js_int_buffer = js.Int32Array.new(js_shared_buffer) + js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) + + js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) + js.Atomics.notify(js_int_buffer, 0) + js_absolute_url = js.URL.new(request.url, js.location).href + self.js_worker.postMessage( + _obj_from_dict( + { + "buffer": js_shared_buffer, + "url": js_absolute_url, + "fetchParams": fetch_data, + } + ) + ) + # wait for the worker to send something + js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) + if js_int_buffer[0] == ERROR_TIMEOUT: + raise _TimeoutError( + "Timeout connecting to streaming request", + request=request, + response=None, + ) + elif js_int_buffer[0] == SUCCESS_HEADER: + # got response + # header length is in second int of intBuffer + string_len = js_int_buffer[1] + # decode the rest to a JSON string + js_decoder = js.TextDecoder.new() + # this does a copy (the slice) because decode can't work on shared array + # for some silly reason + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + # get it as an object + response_obj = json.loads(json_str) + return EmscriptenResponse( + request=request, + status_code=response_obj["status"], + headers=response_obj["headers"], + body=_ReadStream( + js_int_buffer, + js_byte_buffer, + request.timeout, + self.js_worker, + response_obj["connectionID"], + request, + ), + ) + elif js_int_buffer[0] == ERROR_EXCEPTION: + string_len = js_int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", request=request, response=None + ) + else: + raise _StreamingError( + f"Unknown status from worker in fetch: {js_int_buffer[0]}", + request=request, + response=None, + ) + + +class _JSPIReadStream(io.RawIOBase): + """ + A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch + response. This requires support for WebAssembly JavaScript Promise Integration + in the containing browser, and for pyodide to be launched via runPythonAsync. + + :param js_read_stream: + The JavaScript stream reader + + :param timeout: + Timeout in seconds + + :param request: + The request we're handling + + :param response: + The response this stream relates to + + :param js_abort_controller: + A JavaScript AbortController object, used for timeouts + """ + + def __init__( + self, + js_read_stream: Any, + timeout: float, + request: EmscriptenRequest, + response: EmscriptenResponse, + js_abort_controller: Any, # JavaScript AbortController for timeouts + ): + self.js_read_stream = js_read_stream + self.timeout = timeout + self._is_closed = False + self._is_done = False + self.request: EmscriptenRequest | None = request + self.response: EmscriptenResponse | None = response + self.current_buffer = None + self.current_buffer_pos = 0 + self.js_abort_controller = js_abort_controller + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.js_read_stream.cancel() + self.js_read_stream = None + self._is_closed = True + self._is_done = True + self.request = None + self.response = None + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def _get_next_buffer(self) -> bool: + result_js = _run_sync_with_timeout( + self.js_read_stream.read(), + self.timeout, + self.js_abort_controller, + request=self.request, + response=self.response, + ) + if result_js.done: + self._is_done = True + return False + else: + self.current_buffer = result_js.value.to_py() + self.current_buffer_pos = 0 + return True + + def readinto(self, byte_obj: Buffer) -> int: + if self.current_buffer is None: + if not self._get_next_buffer() or self.current_buffer is None: + self.close() + return 0 + ret_length = min( + len(byte_obj), len(self.current_buffer) - self.current_buffer_pos + ) + byte_obj[0:ret_length] = self.current_buffer[ + self.current_buffer_pos : self.current_buffer_pos + ret_length + ] + self.current_buffer_pos += ret_length + if self.current_buffer_pos == len(self.current_buffer): + self.current_buffer = None + return ret_length + + +# check if we are in a worker or not +def is_in_browser_main_thread() -> bool: + return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window + + +def is_cross_origin_isolated() -> bool: + return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated + + +def is_in_node() -> bool: + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + and hasattr(js.process.release, "name") + and js.process.release.name == "node" + ) + + +def is_worker_available() -> bool: + return hasattr(js, "Worker") and hasattr(js, "Blob") + + +_fetcher: _StreamingFetcher | None = None + +if is_worker_available() and ( + (is_cross_origin_isolated() and not is_in_browser_main_thread()) + and (not is_in_node()) +): + _fetcher = _StreamingFetcher() +else: + _fetcher = None + + +NODE_JSPI_ERROR = ( + "urllib3 only works in Node.js with pyodide.runPythonAsync" + " and requires the flag --experimental-wasm-stack-switching in " + " versions of node <24." +) + + +def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: + if has_jspi(): + return send_jspi_request(request, True) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + + if _fetcher and streaming_ready(): + return _fetcher.send(request) + else: + _show_streaming_warning() + return None + + +_SHOWN_TIMEOUT_WARNING = False + + +def _show_timeout_warning() -> None: + global _SHOWN_TIMEOUT_WARNING + if not _SHOWN_TIMEOUT_WARNING: + _SHOWN_TIMEOUT_WARNING = True + message = "Warning: Timeout is not available on main browser thread" + js.console.warn(message) + + +_SHOWN_STREAMING_WARNING = False + + +def _show_streaming_warning() -> None: + global _SHOWN_STREAMING_WARNING + if not _SHOWN_STREAMING_WARNING: + _SHOWN_STREAMING_WARNING = True + message = "Can't stream HTTP requests because: \n" + if not is_cross_origin_isolated(): + message += " Page is not cross-origin isolated\n" + if is_in_browser_main_thread(): + message += " Python is running in main browser thread\n" + if not is_worker_available(): + message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in + if streaming_ready() is False: + message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch +is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" + from js import console + + console.warn(message) + + +def send_request(request: EmscriptenRequest) -> EmscriptenResponse: + if has_jspi(): + return send_jspi_request(request, False) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + try: + js_xhr = js.XMLHttpRequest.new() + + if not is_in_browser_main_thread(): + js_xhr.responseType = "arraybuffer" + if request.timeout: + js_xhr.timeout = int(request.timeout * 1000) + else: + js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") + if request.timeout: + # timeout isn't available on the main thread - show a warning in console + # if it is set + _show_timeout_warning() + + js_xhr.open(request.method, request.url, False) + for name, value in request.headers.items(): + if name.lower() not in HEADERS_TO_IGNORE: + js_xhr.setRequestHeader(name, value) + + js_xhr.send(to_js(request.body)) + + headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) + + if not is_in_browser_main_thread(): + body = js_xhr.response.to_py().tobytes() + else: + body = js_xhr.response.encode("ISO-8859-15") + return EmscriptenResponse( + status_code=js_xhr.status, headers=headers, body=body, request=request + ) + except JsException as err: + if err.name == "TimeoutError": + raise _TimeoutError(err.message, request=request) + elif err.name == "NetworkError": + raise _RequestError(err.message, request=request) + else: + # general http error + raise _RequestError(err.message, request=request) + + +def send_jspi_request( + request: EmscriptenRequest, streaming: bool +) -> EmscriptenResponse: + """ + Send a request using WebAssembly JavaScript Promise Integration + to wrap the asynchronous JavaScript fetch api (experimental). + + :param request: + Request to send + + :param streaming: + Whether to stream the response + + :return: The response object + :rtype: EmscriptenResponse + """ + timeout = request.timeout + js_abort_controller = js.AbortController.new() + headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} + req_body = request.body + fetch_data = { + "headers": headers, + "body": to_js(req_body), + "method": request.method, + "signal": js_abort_controller.signal, + } + # Node.js returns the whole response (unlike opaqueredirect in browsers), + # so urllib3 can set `redirect: manual` to control redirects itself. + # https://stackoverflow.com/a/78524615 + if _is_node_js(): + fetch_data["redirect"] = "manual" + # Call JavaScript fetch (async api, returns a promise) + fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) + # Now suspend WebAssembly until we resolve that promise + # or time out. + response_js = _run_sync_with_timeout( + fetcher_promise_js, + timeout, + js_abort_controller, + request=request, + response=None, + ) + headers = {} + header_iter = response_js.headers.entries() + while True: + iter_value_js = header_iter.next() + if getattr(iter_value_js, "done", False): + break + else: + headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) + status_code = response_js.status + body: bytes | io.RawIOBase = b"" + + response = EmscriptenResponse( + status_code=status_code, headers=headers, body=b"", request=request + ) + if streaming: + # get via inputstream + if response_js.body is not None: + # get a reader from the fetch response + body_stream_js = response_js.body.getReader() + body = _JSPIReadStream( + body_stream_js, timeout, request, response, js_abort_controller + ) + else: + # get directly via arraybuffer + # n.b. this is another async JavaScript call. + body = _run_sync_with_timeout( + response_js.arrayBuffer(), + timeout, + js_abort_controller, + request=request, + response=response, + ).to_py() + response.body = body + return response + + +def _run_sync_with_timeout( + promise: Any, + timeout: float, + js_abort_controller: Any, + request: EmscriptenRequest | None, + response: EmscriptenResponse | None, +) -> Any: + """ + Await a JavaScript promise synchronously with a timeout which is implemented + via the AbortController + + :param promise: + Javascript promise to await + + :param timeout: + Timeout in seconds + + :param js_abort_controller: + A JavaScript AbortController object, used on timeout + + :param request: + The request being handled + + :param response: + The response being handled (if it exists yet) + + :raises _TimeoutError: If the request times out + :raises _RequestError: If the request raises a JavaScript exception + + :return: The result of awaiting the promise. + """ + timer_id = None + if timeout > 0: + timer_id = js.setTimeout( + js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) + ) + try: + from pyodide.ffi import run_sync + + # run_sync here uses WebAssembly JavaScript Promise Integration to + # suspend python until the JavaScript promise resolves. + return run_sync(promise) + except JsException as err: + if err.name == "AbortError": + raise _TimeoutError( + message="Request timed out", request=request, response=response + ) + else: + raise _RequestError(message=err.message, request=request, response=response) + finally: + if timer_id is not None: + js.clearTimeout(timer_id) + + +def has_jspi() -> bool: + """ + Return true if jspi can be used. + + This requires both browser support and also WebAssembly + to be in the correct state - i.e. that the javascript + call into python was async not sync. + + :return: True if jspi can be used. + :rtype: bool + """ + try: + from pyodide.ffi import can_run_sync, run_sync # noqa: F401 + + return bool(can_run_sync()) + except ImportError: + return False + + +def _is_node_js() -> bool: + """ + Check if we are in Node.js. + + :return: True if we are in Node.js. + :rtype: bool + """ + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + # According to the Node.js documentation, the release name is always "node". + and js.process.release.name == "node" + ) + + +def streaming_ready() -> bool | None: + if _fetcher: + return _fetcher.streaming_ready + else: + return None # no fetcher, return None to signify that + + +async def wait_for_streaming_ready() -> bool: + if _fetcher: + await _fetcher.js_worker_ready_promise + return True + else: + return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py new file mode 100644 index 0000000000..e692e692bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..._base_connection import _TYPE_BODY + + +@dataclass +class EmscriptenRequest: + method: str + url: str + params: dict[str, str] | None = None + body: _TYPE_BODY | None = None + headers: dict[str, str] = field(default_factory=dict) + timeout: float = 0 + decode_content: bool = True + + def set_header(self, name: str, value: str) -> None: + self.headers[name.capitalize()] = value + + def set_body(self, body: _TYPE_BODY | None) -> None: + self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py new file mode 100644 index 0000000000..ec1e1dbe83 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import json as _json +import logging +import typing +from contextlib import contextmanager +from dataclasses import dataclass +from http.client import HTTPException as HTTPException +from io import BytesIO, IOBase + +from ...exceptions import InvalidHeader, TimeoutError +from ...response import BaseHTTPResponse +from ...util.retry import Retry +from .request import EmscriptenRequest + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + + +@dataclass +class EmscriptenResponse: + status_code: int + headers: dict[str, str] + body: IOBase | bytes + request: EmscriptenRequest + + +class EmscriptenHttpResponseWrapper(BaseHTTPResponse): + def __init__( + self, + internal_response: EmscriptenResponse, + url: str | None = None, + connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, + ): + self._pool = None # set by pool class + self._body = None + self._uncached_read_occurred = False + self._response = internal_response + self._url = url + self._connection = connection + self._closed = False + super().__init__( + headers=internal_response.headers, + status=internal_response.status_code, + request_url=url, + version=0, + version_string="HTTP/?", + reason="", + decode_content=True, + ) + self.length_remaining = self._init_length(self._response.request.method) + self.length_is_certain = False + + @property + def url(self) -> str | None: + return self._url + + @url.setter + def url(self, url: str | None) -> None: + self._url = url + + @property + def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: + return self._connection + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + while True: + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + else: + break + + def _init_length(self, request_method: str | None) -> int | None: + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Check for responses that shouldn't include a body + if ( + self.status in (204, 304) + or 100 <= self.status < 200 + or request_method == "HEAD" + ): + length = 0 + + return length + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, # ignored because browser decodes always + cache_content: bool = False, + ) -> bytes: + if ( + self._closed + or self._response is None + or (isinstance(self._response.body, IOBase) and self._response.body.closed) + ): + return b"" + + with self._error_catcher(): + # body has been preloaded as a string by XmlHttpRequest + if not isinstance(self._response.body, IOBase): + self.length_remaining = len(self._response.body) + self.length_is_certain = True + # wrap body in IOStream + self._response.body = BytesIO(self._response.body) + if amt is not None and amt >= 0: + # don't cache partial content + cache_content = False + data = self._response.body.read(amt) + self._uncached_read_occurred = True + else: # read all we can (and cache it) + data = self._response.body.read() + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + self._uncached_read_occurred = True + if self.length_remaining is not None: + self.length_remaining = max(self.length_remaining - len(data), 0) + if len(data) == 0 or ( + self.length_is_certain and self.length_remaining == 0 + ): + # definitely finished reading, close response stream + self._response.body.close() + return typing.cast(bytes, data) + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Generator[bytes]: + # chunked is handled by browser + while True: + bytes = self.read(amt, decode_content) + if not bytes: + break + yield bytes + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + self.close() + + @property + def data(self) -> bytes: + if self._body: + return self._body + else: + return self.read(cache_content=True) + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + def close(self) -> None: + if not self._closed: + if isinstance(self._response.body, IOBase): + self._response.body.close() + if self._connection: + self._connection.close() + self._connection = None + self._closed = True + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch Emscripten specific exceptions thrown by fetch.py, + instead re-raising urllib3 variants, so that low-level exceptions + are not leaked in the high-level api. + + On exit, release the connection back to the pool. + """ + from .fetch import _RequestError, _TimeoutError # avoid circular import + + clean_exit = False + + try: + yield + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + except _TimeoutError as e: + raise TimeoutError(str(e)) + except _RequestError as e: + raise HTTPException(str(e)) + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now + if ( + isinstance(self._response.body, IOBase) + and not self._response.body.closed + ): + self._response.body.close() + # release the connection back to the pool + self.release_conn() + else: + # If we have read everything from the response stream, + # return the connection back to the pool. + if ( + isinstance(self._response.body, IOBase) + and self._response.body.closed + ): + self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py new file mode 100644 index 0000000000..f06b859992 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py @@ -0,0 +1,563 @@ +""" +Module for using pyOpenSSL as a TLS backend. This module was relevant before +the standard library ``ssl`` module supported SNI, but now that we've dropped +support for Python 2.7 all relevant Python versions support SNI so +**this module is no longer recommended**. + +This needs the following packages installed: + +* `pyOpenSSL`_ (tested with 19.0.0) +* `cryptography`_ (minimum 2.3, from pyopenssl) +* `idna`_ (minimum 2.1, from cryptography) + +However, pyOpenSSL depends on cryptography, so while we use all three directly here we +end up having relatively few packages required. + +You can install them with the following command: + +.. code-block:: bash + + $ python -m pip install pyopenssl cryptography idna + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this: + +.. code-block:: python + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +.. _pyopenssl: https://www.pyopenssl.org +.. _cryptography: https://cryptography.io +.. _idna: https://github.com/kjd/idna +""" + +from __future__ import annotations + +import OpenSSL.SSL # type: ignore[import-not-found] +from cryptography import x509 + +try: + from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] +except ImportError: + # UnsupportedExtension is gone in cryptography >= 2.1.0 + class UnsupportedExtension(Exception): # type: ignore[no-redef] + pass + + +import logging +import ssl +import typing +from io import BytesIO +from socket import socket as socket_cls + +from .. import util + +if typing.TYPE_CHECKING: + from OpenSSL.crypto import X509 # type: ignore[import-not-found] + + +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions: dict[int, int] = { + util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} +_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} + +# The SSLvX values are the most likely to be missing in the future +# but we check them all just to be sure. +_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( + OpenSSL.SSL, "OP_NO_SSLv3", 0 +) +_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) +_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) +_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) +_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) + +_openssl_to_ssl_minimum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, + ssl.TLSVersion.TLSv1_3: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), + ssl.TLSVersion.MAXIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), +} +_openssl_to_ssl_maximum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 + | _OP_NO_TLSv1 + | _OP_NO_TLSv1_1 + | _OP_NO_TLSv1_2 + | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, +} + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_SSLContext = util.ssl_.SSLContext + + +log = logging.getLogger(__name__) + + +def inject_into_urllib3() -> None: + "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." + + _validate_dependencies_met() + + util.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.IS_PYOPENSSL = True + util.ssl_.IS_PYOPENSSL = True + + +def extract_from_urllib3() -> None: + "Undo monkey-patching by :func:`inject_into_urllib3`." + + util.SSLContext = orig_util_SSLContext + util.ssl_.SSLContext = orig_util_SSLContext + util.IS_PYOPENSSL = False + util.ssl_.IS_PYOPENSSL = False + + +def _validate_dependencies_met() -> None: + """ + Verifies that PyOpenSSL's package-level dependencies have been met. + Throws `ImportError` if they are not met. + """ + # Method added in `cryptography==1.1`; not available in older versions + from cryptography.x509.extensions import Extensions + + if getattr(Extensions, "get_extension_for_class", None) is None: + raise ImportError( + "'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer." + ) + + # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 + # attribute is only present on those versions. + from OpenSSL.crypto import X509 + + x509 = X509() + if getattr(x509, "_x509", None) is None: + raise ImportError( + "'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer." + ) + + +def _dnsname_to_stdlib(name: str) -> str | None: + """ + Converts a dNSName SubjectAlternativeName field to the form used by the + standard library on the given Python version. + + Cryptography produces a dNSName as a unicode string that was idna-decoded + from ASCII bytes. We need to idna-encode that string to get it back, and + then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib + uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). + + If the name cannot be idna-encoded then we return None signalling that + the name given should be skipped. + """ + + def idna_encode(name: str) -> bytes | None: + """ + Borrowed wholesale from the Python Cryptography Project. It turns out + that we can't just safely call `idna.encode`: it can explode for + wildcard names. This avoids that problem. + """ + import idna + + try: + for prefix in ["*.", "."]: + if name.startswith(prefix): + name = name[len(prefix) :] + return prefix.encode("ascii") + idna.encode(name) + return idna.encode(name) + except idna.core.IDNAError: + return None + + # Don't send IPv6 addresses through the IDNA encoder. + if ":" in name: + return name + + encoded_name = idna_encode(name) + if encoded_name is None: + return None + return encoded_name.decode("utf-8") + + +def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: + """ + Given an PyOpenSSL certificate, provides all the subject alternative names. + """ + cert = peer_cert.to_cryptography() + + # We want to find the SAN extension. Ask Cryptography to locate it (it's + # faster than looping in Python) + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + except x509.ExtensionNotFound: + # No such extension, return the empty list. + return [] + except ( + x509.DuplicateExtension, + UnsupportedExtension, + x509.UnsupportedGeneralNameType, + UnicodeError, + ) as e: + # A problem has been found with the quality of the certificate. Assume + # no SAN field is present. + log.warning( + "A problem was encountered with the certificate that prevented " + "urllib3 from finding the SubjectAlternativeName field. This can " + "affect certificate validation. The error was %s", + e, + ) + return [] + + # We want to return dNSName and iPAddress fields. We need to cast the IPs + # back to strings because the match_hostname function wants them as + # strings. + # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 + # decoded. This is pretty frustrating, but that's what the standard library + # does with certificates, and so we need to attempt to do the same. + # We also want to skip over names which cannot be idna encoded. + names = [ + ("DNS", name) + for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) + if name is not None + ] + names.extend( + ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) + ) + + return names + + +class WrappedSocket: + """API-compatibility wrapper for Python OpenSSL's Connection-class.""" + + def __init__( + self, + connection: OpenSSL.SSL.Connection, + socket: socket_cls, + suppress_ragged_eofs: bool = True, + ) -> None: + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._io_refs = 0 + self._closed = False + + def fileno(self) -> int: + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self) -> None: + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return b"" + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b"" + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + else: + return data # type: ignore[no-any-return] + + def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: + try: + return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return 0 + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return 0 + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv_into(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + + def settimeout(self, timeout: float) -> None: + return self.socket.settimeout(timeout) + + def _send_until_done(self, data: bytes) -> int: + while True: + try: + return self.connection.send(data) # type: ignore[no-any-return] + except OpenSSL.SSL.WantWriteError as e: + if not util.wait_for_write(self.socket, self.socket.gettimeout()): + raise TimeoutError() from e + continue + except OpenSSL.SSL.SysCallError as e: + raise OSError(e.args[0], str(e)) from e + + def sendall(self, data: bytes) -> None: + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done( + data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] + ) + total_sent += sent + + def shutdown(self, how: int) -> None: + try: + self.connection.shutdown() + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"shutdown error: {e!r}") from e + + def close(self) -> None: + self._closed = True + if self._io_refs <= 0: + self._real_close() + + def _real_close(self) -> None: + try: + return self.connection.close() # type: ignore[no-any-return] + except OpenSSL.SSL.Error: + return + + def getpeercert( + self, binary_form: bool = False + ) -> dict[str, list[typing.Any]] | None: + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 # type: ignore[no-any-return] + + if binary_form: + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] + + return { + "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] + "subjectAltName": get_subj_alt_name(x509), + } + + def version(self) -> str: + return self.connection.get_protocol_version_name() # type: ignore[no-any-return] + + def selected_alpn_protocol(self) -> str | None: + alpn_proto = self.connection.get_alpn_proto_negotiated() + return alpn_proto.decode() if alpn_proto else None + + +WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] + + +class PyOpenSSLContext: + """ + I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible + for translating the interface of the standard library ``SSLContext`` object + to calls into PyOpenSSL. + """ + + def __init__(self, protocol: int) -> None: + self.protocol = _openssl_versions[protocol] + self._ctx = OpenSSL.SSL.Context(self.protocol) + self._options = 0 + self.check_hostname = False + self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED + self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED + self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST + + @property + def options(self) -> int: + return self._options + + @options.setter + def options(self, value: int) -> None: + self._options = value + self._set_ctx_options() + + @property + def verify_flags(self) -> int: + return self._verify_flags + + @verify_flags.setter + def verify_flags(self, value: int) -> None: + self._verify_flags = value + self._ctx.get_cert_store().set_flags(self._verify_flags) + + @property + def verify_mode(self) -> int: + return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] + + @verify_mode.setter + def verify_mode(self, value: ssl.VerifyMode) -> None: + self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) + + def set_default_verify_paths(self) -> None: + self._ctx.set_default_verify_paths() + + def set_ciphers(self, ciphers: bytes | str) -> None: + if isinstance(ciphers, str): + ciphers = ciphers.encode("utf-8") + self._ctx.set_cipher_list(ciphers) + + def load_verify_locations( + self, + cafile: str | None = None, + capath: str | None = None, + cadata: bytes | None = None, + ) -> None: + if cafile is not None: + cafile = cafile.encode("utf-8") # type: ignore[assignment] + if capath is not None: + capath = capath.encode("utf-8") # type: ignore[assignment] + try: + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e + + def load_cert_chain( + self, + certfile: str, + keyfile: str | None = None, + password: str | None = None, + ) -> None: + try: + self._ctx.use_certificate_chain_file(certfile) + if password is not None: + if not isinstance(password, bytes): + password = password.encode("utf-8") # type: ignore[assignment] + self._ctx.set_passwd_cb(lambda *_: password) + self._ctx.use_privatekey_file(keyfile or certfile) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e + + def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: + protocols = [util.util.to_bytes(p, "ascii") for p in protocols] + return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] + + def wrap_socket( + self, + sock: socket_cls, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bytes | str | None = None, + ) -> WrappedSocket: + cnx = OpenSSL.SSL.Connection(self._ctx, sock) + + # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 + if server_hostname and not util.ssl_.is_ipaddress(server_hostname): + if isinstance(server_hostname, str): + server_hostname = server_hostname.encode("utf-8") + cnx.set_tlsext_host_name(server_hostname) + + cnx.set_connect_state() + + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(sock, sock.gettimeout()): + raise TimeoutError("select timed out") from e + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"bad handshake: {e!r}") from e + break + + return WrappedSocket(cnx, sock) + + def _set_ctx_options(self) -> None: + self._ctx.set_options( + self._options + | _openssl_to_ssl_minimum_version[self._minimum_version] + | _openssl_to_ssl_maximum_version[self._maximum_version] + ) + + @property + def minimum_version(self) -> int: + return self._minimum_version + + @minimum_version.setter + def minimum_version(self, minimum_version: int) -> None: + self._minimum_version = minimum_version + self._set_ctx_options() + + @property + def maximum_version(self) -> int: + return self._maximum_version + + @maximum_version.setter + def maximum_version(self, maximum_version: int) -> None: + self._maximum_version = maximum_version + self._set_ctx_options() + + +def _verify_callback( + cnx: OpenSSL.SSL.Connection, + x509: X509, + err_no: int, + err_depth: int, + return_code: int, +) -> bool: + return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py new file mode 100644 index 0000000000..d37da8fc20 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py @@ -0,0 +1,228 @@ +""" +This module contains provisional support for SOCKS proxies from within +urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and +SOCKS5. To enable its functionality, either install PySocks or install this +module with the ``socks`` extra. + +The SOCKS implementation supports the full range of urllib3 features. It also +supports the following SOCKS features: + +- SOCKS4A (``proxy_url='socks4a://...``) +- SOCKS4 (``proxy_url='socks4://...``) +- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) +- SOCKS5 with local DNS (``proxy_url='socks5://...``) +- Usernames and passwords for the SOCKS proxy + +.. note:: + It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in + your ``proxy_url`` to ensure that DNS resolution is done from the remote + server instead of client-side when connecting to a domain name. + +SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 +supports IPv4, IPv6, and domain names. + +When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` +will be sent as the ``userid`` section of the SOCKS request: + +.. code-block:: python + + proxy_url="socks4a://@proxy-host" + +When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion +of the ``proxy_url`` will be sent as the username/password to authenticate +with the proxy: + +.. code-block:: python + + proxy_url="socks5h://:@proxy-host" + +""" + +from __future__ import annotations + +try: + import socks # type: ignore[import-untyped] +except ImportError: + import warnings + + from ..exceptions import DependencyWarning + + warnings.warn( + ( + "SOCKS support in urllib3 requires the installation of optional " + "dependencies: specifically, PySocks. For more information, see " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" + ), + DependencyWarning, + ) + raise + +import typing +from socket import timeout as SocketTimeout + +from ..connection import HTTPConnection, HTTPSConnection +from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from ..exceptions import ConnectTimeoutError, NewConnectionError +from ..poolmanager import PoolManager +from ..util.url import parse_url + +try: + import ssl +except ImportError: + ssl = None # type: ignore[assignment] + + +class _TYPE_SOCKS_OPTIONS(typing.TypedDict): + socks_version: int + proxy_host: str | None + proxy_port: str | None + username: str | None + password: str | None + rdns: bool + + +class SOCKSConnection(HTTPConnection): + """ + A plain-text HTTP connection that connects via a SOCKS proxy. + """ + + def __init__( + self, + _socks_options: _TYPE_SOCKS_OPTIONS, + *args: typing.Any, + **kwargs: typing.Any, + ) -> None: + self._socks_options = _socks_options + super().__init__(*args, **kwargs) + + def _new_conn(self) -> socks.socksocket: + """ + Establish a new connection via the SOCKS proxy. + """ + extra_kw: dict[str, typing.Any] = {} + if self.source_address: + extra_kw["source_address"] = self.source_address + + if self.socket_options: + extra_kw["socket_options"] = self.socket_options + + try: + conn = socks.create_connection( + (self.host, self.port), + proxy_type=self._socks_options["socks_version"], + proxy_addr=self._socks_options["proxy_host"], + proxy_port=self._socks_options["proxy_port"], + proxy_username=self._socks_options["username"], + proxy_password=self._socks_options["password"], + proxy_rdns=self._socks_options["rdns"], + timeout=self.timeout, + **extra_kw, + ) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except socks.ProxyError as e: + # This is fragile as hell, but it seems to be the only way to raise + # useful errors here. + if e.socket_err: + error = e.socket_err + if isinstance(error, SocketTimeout): + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + else: + # Adding `from e` messes with coverage somehow, so it's omitted. + # See #2386. + raise NewConnectionError( + self, f"Failed to establish a new connection: {error}" + ) + else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + except OSError as e: # Defensive: PySocks should catch all these. + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + return conn + + +# We don't need to duplicate the Verified/Unverified distinction from +# urllib3/connection.py here because the HTTPSConnection will already have been +# correctly set to either the Verified or Unverified form by that module. This +# means the SOCKSHTTPSConnection will automatically be the correct type. +class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): + pass + + +class SOCKSHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = SOCKSConnection + + +class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): + ConnectionCls = SOCKSHTTPSConnection + + +class SOCKSProxyManager(PoolManager): + """ + A version of the urllib3 ProxyManager that routes connections via the + defined SOCKS proxy. + """ + + pool_classes_by_scheme = { + "http": SOCKSHTTPConnectionPool, + "https": SOCKSHTTPSConnectionPool, + } + + def __init__( + self, + proxy_url: str, + username: str | None = None, + password: str | None = None, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ): + parsed = parse_url(proxy_url) + + if username is None and password is None and parsed.auth is not None: + split = parsed.auth.split(":") + if len(split) == 2: + username, password = split + if parsed.scheme == "socks5": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = False + elif parsed.scheme == "socks5h": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = True + elif parsed.scheme == "socks4": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = False + elif parsed.scheme == "socks4a": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = True + else: + raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") + + self.proxy_url = proxy_url + + socks_options = { + "socks_version": socks_version, + "proxy_host": parsed.host, + "proxy_port": parsed.port, + "username": username, + "password": password, + "rdns": rdns, + } + connection_pool_kw["_socks_options"] = socks_options + + super().__init__(num_pools, headers, **connection_pool_kw) + + self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py new file mode 100644 index 0000000000..3d7e9b93d3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import socket +import typing +import warnings +from email.errors import MessageDefect +from http.client import IncompleteRead as httplib_IncompleteRead + +if typing.TYPE_CHECKING: + from .connection import HTTPConnection + from .connectionpool import ConnectionPool + from .response import HTTPResponse + from .util.retry import Retry + +# Base Exceptions + + +class HTTPError(Exception): + """Base exception used by this module.""" + + +class HTTPWarning(Warning): + """Base warning used by this module.""" + + +_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] + + +class PoolError(HTTPError): + """Base exception for errors caused within a pool.""" + + def __init__(self, pool: ConnectionPool, message: str) -> None: + self.pool = pool + self._message = message + super().__init__(f"{pool}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + +class RequestError(PoolError): + """Base exception for PoolErrors that have associated URLs.""" + + def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: + self.url = url + super().__init__(pool, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self._message) + + +class SSLError(HTTPError): + """Raised when SSL certificate fails in an HTTPS connection.""" + + +class ProxyError(HTTPError): + """Raised when the connection to a proxy fails.""" + + # The original error is also available as __cause__. + original_error: Exception + + def __init__(self, message: str, error: Exception) -> None: + super().__init__(message, error) + self.original_error = error + + +class DecodeError(HTTPError): + """Raised when automatic decoding based on Content-Type fails.""" + + +class ProtocolError(HTTPError): + """Raised when something unexpected happens mid-request/response.""" + + +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + +# Leaf Exceptions + + +class MaxRetryError(RequestError): + """Raised when the maximum number of retries is exceeded. + + :param pool: The connection pool + :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` + :param str url: The requested Url + :param reason: The underlying error + :type reason: :class:`Exception` + + """ + + def __init__( + self, pool: ConnectionPool, url: str | None, reason: Exception | None = None + ) -> None: + self.reason = reason + + message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" + + super().__init__(pool, url, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self.reason) + + +class HostChangedError(RequestError): + """Raised when an existing pool gets a request for a foreign host.""" + + def __init__( + self, pool: ConnectionPool, url: str, retries: Retry | int = 3 + ) -> None: + message = f"Tried to open a foreign host with url: {url}" + super().__init__(pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """Raised when passing an invalid state to a timeout""" + + +class TimeoutError(HTTPError): + """Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + ` and :exc:`ConnectTimeoutErrors `. + """ + + +class ReadTimeoutError(TimeoutError, RequestError): + """Raised when a socket timeout occurs while receiving data from a server""" + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + """Raised when a socket timeout occurs while connecting to a server""" + + +class NewConnectionError(ConnectTimeoutError, HTTPError): + """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" + + def __init__(self, conn: HTTPConnection, message: str) -> None: + self.conn = conn + self._message = message + super().__init__(f"{conn}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + @property + def pool(self) -> HTTPConnection: + warnings.warn( + "The 'pool' property is deprecated and will be removed " + "in urllib3 v3.0. Use 'conn' instead.", + FutureWarning, + stacklevel=2, + ) + + return self.conn + + +class NameResolutionError(NewConnectionError): + """Raised when host name resolution fails.""" + + def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): + message = f"Failed to resolve '{host}' ({reason})" + self._host = host + self._reason = reason + super().__init__(conn, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (self._host, None, self._reason) + + +class EmptyPoolError(PoolError): + """Raised when a pool runs out of connections and no more are allowed.""" + + +class FullPoolError(PoolError): + """Raised when we try to add a connection to a full pool in blocking mode.""" + + +class ClosedPoolError(PoolError): + """Raised when a request enters a pool after the pool has been closed.""" + + +class LocationValueError(ValueError, HTTPError): + """Raised when there is something wrong with a given URL input.""" + + +class LocationParseError(LocationValueError): + """Raised when get_host or similar fails to parse the URL input.""" + + def __init__(self, location: str) -> None: + message = f"Failed to parse: {location}" + super().__init__(message) + + self.location = location + + +class URLSchemeUnknown(LocationValueError): + """Raised when a URL input has an unsupported scheme.""" + + def __init__(self, scheme: str): + message = f"Not supported URL scheme {scheme}" + super().__init__(message) + + self.scheme = scheme + + +class ResponseError(HTTPError): + """Used as a container for an error reason supplied in a MaxRetryError.""" + + GENERIC_ERROR = "too many error responses" + SPECIFIC_ERROR = "too many {status_code} error responses" + + +class SecurityWarning(HTTPWarning): + """Warned when performing security reducing actions""" + + +class InsecureRequestWarning(SecurityWarning): + """Warned when making an unverified HTTPS request.""" + + +class NotOpenSSLWarning(SecurityWarning): + """Warned when using unsupported SSL library""" + + +class SystemTimeWarning(SecurityWarning): + """Warned when system time is suspected to be wrong""" + + +class InsecurePlatformWarning(SecurityWarning): + """Warned when certain TLS/SSL configuration is not available on a platform.""" + + +class DependencyWarning(HTTPWarning): + """ + Warned when an attempt is made to import a module with missing optional + dependencies. + """ + + +class ResponseNotChunked(ProtocolError, ValueError): + """Response needs to be chunked in order to read it as chunks.""" + + +class BodyNotHttplibCompatible(HTTPError): + """ + Body should be :class:`http.client.HTTPResponse` like + (have an fp attribute which returns raw chunks) for read_chunked(). + """ + + +class IncompleteRead(HTTPError, httplib_IncompleteRead): + """ + Response length doesn't match expected Content-Length + + Subclass of :class:`http.client.IncompleteRead` to allow int value + for ``partial`` to avoid creating large objects on streamed reads. + """ + + partial: int # type: ignore[assignment] + expected: int + + def __init__(self, partial: int, expected: int) -> None: + self.partial = partial + self.expected = expected + + def __repr__(self) -> str: + return "IncompleteRead(%i bytes read, %i more expected)" % ( + self.partial, + self.expected, + ) + + +class InvalidChunkLength(HTTPError, httplib_IncompleteRead): + """Invalid chunk length in a chunked response.""" + + def __init__(self, response: HTTPResponse, length: bytes) -> None: + self.partial: int = response.tell() # type: ignore[assignment] + self.expected: int | None = response.length_remaining + self.response = response + self.length = length + + def __repr__(self) -> str: + return "InvalidChunkLength(got length %r, %i bytes read)" % ( + self.length, + self.partial, + ) + + +class InvalidHeader(HTTPError): + """The header provided was somehow invalid.""" + + +class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): + """ProxyManager does not support the supplied scheme""" + + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. + + def __init__(self, scheme: str | None) -> None: + # 'localhost' is here because our URL parser parses + # localhost:8080 -> scheme=localhost, remove if we fix this. + if scheme == "localhost": + scheme = None + if scheme is None: + message = "Proxy URL had no scheme, should start with http:// or https://" + else: + message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" + super().__init__(message) + + +class ProxySchemeUnsupported(ValueError): + """Fetching HTTPS resources through HTTPS proxies is unsupported""" + + +class HeaderParsingError(HTTPError): + """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" + + def __init__( + self, defects: list[MessageDefect], unparsed_data: bytes | str | None + ) -> None: + message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" + super().__init__(message) + + +class UnrewindableBodyError(HTTPError): + """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py new file mode 100644 index 0000000000..fe68e17732 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import email.utils +import mimetypes +import typing + +_TYPE_FIELD_VALUE = typing.Union[str, bytes] +_TYPE_FIELD_VALUE_TUPLE = typing.Union[ + _TYPE_FIELD_VALUE, + tuple[str, _TYPE_FIELD_VALUE], + tuple[str, _TYPE_FIELD_VALUE, str], +] + + +def guess_content_type( + filename: str | None, default: str = "application/octet-stream" +) -> str: + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetypes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Helper function to format and quote a single header parameter using the + strategy defined in RFC 2231. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows + `RFC 2388 Section 4.4 `_. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as ``bytes`` or `str``. + :returns: + An RFC-2231-formatted unicode string. + + .. deprecated:: 2.0.0 + Will be removed in urllib3 v3.0. This is not valid for + ``multipart/form-data`` header parameters. + """ + import warnings + + warnings.warn( + "'format_header_param_rfc2231' is insecure, deprecated and will be " + "removed in urllib3 v3.0. This is not valid for " + "multipart/form-data header parameters.", + FutureWarning, + stacklevel=2, + ) + + if isinstance(value, bytes): + value = value.decode("utf-8") + + if not any(ch in value for ch in '"\\\r\n'): + result = f'{name}="{value}"' + try: + result.encode("ascii") + except (UnicodeEncodeError, UnicodeDecodeError): + pass + else: + return result + + value = email.utils.encode_rfc2231(value, "utf-8") + value = f"{name}*={value}" + + return value + + +def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Format and quote a single multipart header parameter. + + This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching + the behavior of current browser and curl versions. Values are + assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are + percent encoded. + + .. _WHATWG HTML Standard: + https://html.spec.whatwg.org/multipage/ + form-control-infrastructure.html#multipart-form-data + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + :returns: + A string ``name="value"`` with the escaped value. + + .. versionchanged:: 2.0.0 + Matches the WHATWG HTML Standard as of 2021/06/10. Control + characters are no longer percent encoded. + + .. versionchanged:: 2.0.0 + Renamed from ``format_header_param_html5`` and + ``format_header_param``. The old names will be removed in + urllib3 v3.0. + """ + if isinstance(value, bytes): + value = value.decode("utf-8") + + # percent encode \n \r " + value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) + return f'{name}="{value}"' + + +def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param_html5' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +class RequestField: + """ + A data container for request body parameters. + + :param name: + The name of this request field. Must be unicode. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. Must be unicode. + :param headers: + An optional dict-like object of headers to initially use for the field. + + .. versionchanged:: 2.0.0 + The ``header_formatter`` parameter is deprecated and will + be removed in urllib3 v3.0. + """ + + def __init__( + self, + name: str, + data: _TYPE_FIELD_VALUE, + filename: str | None = None, + headers: typing.Mapping[str, str] | None = None, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ): + self._name = name + self._filename = filename + self.data = data + self.headers: dict[str, str | None] = {} + if headers: + self.headers = dict(headers) + + if header_formatter is not None: + import warnings + + warnings.warn( + "The 'header_formatter' parameter is deprecated and " + "will be removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + self.header_formatter = header_formatter + else: + self.header_formatter = format_multipart_header_param + + @classmethod + def from_tuples( + cls, + fieldname: str, + value: _TYPE_FIELD_VALUE_TUPLE, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ) -> RequestField: + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from + parameter of key/value strings AND key/filetuple. A filetuple is a + (filename, data, MIME type) tuple where the MIME type is optional. + For example:: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + filename: str | None + content_type: str | None + data: _TYPE_FIELD_VALUE + + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls( + fieldname, data, filename=filename, header_formatter=header_formatter + ) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Override this method to change how each multipart header + parameter is formatted. By default, this calls + :func:`format_multipart_header_param`. + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + + :meta public: + """ + return self.header_formatter(name, value) + + def _render_parts( + self, + header_parts: ( + dict[str, _TYPE_FIELD_VALUE | None] + | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] + ), + ) -> str: + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format + as `k1="v1"; k2="v2"; ...`. + """ + iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] + + parts = [] + if isinstance(header_parts, dict): + iterable = header_parts.items() + else: + iterable = header_parts + + for name, value in iterable: + if value is not None: + parts.append(self._render_part(name, value)) + + return "; ".join(parts) + + def render_headers(self) -> str: + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append(f"{sort_key}: {self.headers[sort_key]}") + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append(f"{header_name}: {header_value}") + + lines.append("\r\n") + return "\r\n".join(lines) + + def make_multipart( + self, + content_disposition: str | None = None, + content_type: str | None = None, + content_location: str | None = None, + ) -> None: + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_disposition: + The 'Content-Disposition' of the request body. Defaults to 'form-data' + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + content_disposition = (content_disposition or "form-data") + "; ".join( + [ + "", + self._render_parts( + (("name", self._name), ("filename", self._filename)) + ), + ] + ) + + self.headers["Content-Disposition"] = content_disposition + self.headers["Content-Type"] = content_type + self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py new file mode 100644 index 0000000000..14f70b05b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import binascii +import codecs +import os +import typing +from io import BytesIO + +from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField + +writer = codecs.lookup("utf-8")[3] + +_TYPE_FIELDS_SEQUENCE = typing.Sequence[ + typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] +] +_TYPE_FIELDS = typing.Union[ + _TYPE_FIELDS_SEQUENCE, + typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], +] + + +def choose_boundary() -> str: + """ + Our embarrassingly-simple replacement for mimetools.choose_boundary. + """ + return binascii.hexlify(os.urandom(16)).decode() + + +def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] + + if isinstance(fields, typing.Mapping): + iterable = fields.items() + else: + iterable = fields + + for field in iterable: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def encode_multipart_formdata( + fields: _TYPE_FIELDS, boundary: str | None = None +) -> tuple[bytes, str]: + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(f"--{boundary}\r\n".encode("latin-1")) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, str): + writer(body).write(data) + else: + body.write(data) + + body.write(b"\r\n") + + body.write(f"--{boundary}--\r\n".encode("latin-1")) + + content_type = f"multipart/form-data; boundary={boundary}" + + return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py new file mode 100644 index 0000000000..133e1d8f23 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from importlib.metadata import version + +__all__ = [ + "inject_into_urllib3", + "extract_from_urllib3", +] + +import typing + +orig_HTTPSConnection: typing.Any = None + + +def inject_into_urllib3() -> None: + # First check if h2 version is valid + h2_version = version("h2") + if not h2_version.startswith("4."): + raise ImportError( + "urllib3 v2 supports h2 version 4.x.x, currently " + f"the 'h2' module is compiled with {h2_version!r}. " + "See: https://github.com/urllib3/urllib3/issues/3290" + ) + + # Import here to avoid circular dependencies. + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + from .connection import HTTP2Connection + + global orig_HTTPSConnection + orig_HTTPSConnection = urllib3_connection.HTTPSConnection + + HTTPSConnectionPool.ConnectionCls = HTTP2Connection + urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] + + # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. + urllib3_util.ALPN_PROTOCOLS = ["h2"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] + + +def extract_from_urllib3() -> None: + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + + HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection + urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] + + urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py new file mode 100644 index 0000000000..0a026da0a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py @@ -0,0 +1,356 @@ +from __future__ import annotations + +import logging +import re +import threading +import types +import typing + +import h2.config +import h2.connection +import h2.events + +from .._base_connection import _TYPE_BODY +from .._collections import HTTPHeaderDict +from ..connection import HTTPSConnection, _get_default_user_agent +from ..exceptions import ConnectionError +from ..response import BaseHTTPResponse + +orig_HTTPSConnection = HTTPSConnection + +T = typing.TypeVar("T") + +log = logging.getLogger(__name__) + +RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") +RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") + + +def _is_legal_header_name(name: bytes) -> bool: + """ + "An implementation that validates fields according to the definitions in Sections + 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not + include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + + `http.client._is_legal_header_name` does not validate the field name according to the + HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. + + This does not allow for the `:` character in the header name, so should not + be used to validate pseudo-headers. + """ + return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) + + +def _is_illegal_header_value(value: bytes) -> bool: + """ + "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed + (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field + value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, + 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + """ + return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) + + +class _LockedObject(typing.Generic[T]): + """ + A wrapper class that hides a specific object behind a lock. + The goal here is to provide a simple way to protect access to an object + that cannot safely be simultaneously accessed from multiple threads. The + intended use of this class is simple: take hold of it with a context + manager, which returns the protected object. + """ + + __slots__ = ( + "lock", + "_obj", + ) + + def __init__(self, obj: T): + self.lock = threading.RLock() + self._obj = obj + + def __enter__(self) -> T: + self.lock.acquire() + return self._obj + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + self.lock.release() + + +class HTTP2Connection(HTTPSConnection): + def __init__( + self, host: str, port: int | None = None, **kwargs: typing.Any + ) -> None: + self._h2_conn = self._new_h2_conn() + self._h2_stream: int | None = None + self._headers: list[tuple[bytes, bytes]] = [] + + if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: + raise NotImplementedError("Proxies aren't supported with HTTP/2") + + super().__init__(host, port, **kwargs) + + if self._tunnel_host is not None: + raise NotImplementedError("Tunneling isn't supported with HTTP/2") + + def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: + config = h2.config.H2Configuration(client_side=True) + return _LockedObject(h2.connection.H2Connection(config=config)) + + def connect(self) -> None: + super().connect() + with self._h2_conn as conn: + conn.initiate_connection() + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + def putrequest( # type: ignore[override] + self, + method: str, + url: str, + **kwargs: typing.Any, + ) -> None: + """putrequest + This deviates from the HTTPConnection method signature since we never need to override + sending accept-encoding headers or the host header. + """ + if "skip_host" in kwargs: + raise NotImplementedError("`skip_host` isn't supported") + if "skip_accept_encoding" in kwargs: + raise NotImplementedError("`skip_accept_encoding` isn't supported") + + self._request_url = url or "/" + self._validate_path(url) # type: ignore[attr-defined] + + if ":" in self.host: + authority = f"[{self.host}]:{self.port or 443}" + else: + authority = f"{self.host}:{self.port or 443}" + + self._headers.append((b":scheme", b"https")) + self._headers.append((b":method", method.encode())) + self._headers.append((b":authority", authority.encode())) + self._headers.append((b":path", url.encode())) + + with self._h2_conn as conn: + self._h2_stream = conn.get_next_available_stream_id() + + def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] + # TODO SKIPPABLE_HEADERS from urllib3 are ignored. + header = header.encode() if isinstance(header, str) else header + header = header.lower() # A lot of upstream code uses capitalized headers. + if not _is_legal_header_name(header): + raise ValueError(f"Illegal header name {str(header)}") + + for value in values: + value = value.encode() if isinstance(value, str) else value + if _is_illegal_header_value(value): + raise ValueError(f"Illegal header value {str(value)}") + self._headers.append((header, value)) + + def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + conn.send_headers( + stream_id=self._h2_stream, + headers=self._headers, + end_stream=(message_body is None), + ) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + self._headers = [] # Reset headers for the next request. + + def send(self, data: typing.Any) -> None: + """Send data to the server. + `data` can be: `str`, `bytes`, an iterable, or file-like objects + that support a .read() method. + """ + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + if hasattr(data, "read"): # file-like objects + while True: + chunk = data.read(self.blocksize) + if not chunk: + break + if isinstance(chunk, str): + chunk = chunk.encode() + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + return + + if isinstance(data, str): # str -> bytes + data = data.encode() + + try: + if isinstance(data, bytes): + conn.send_data(self._h2_stream, data, end_stream=True) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + else: + for chunk in data: + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + except TypeError: + raise TypeError( + "`data` should be str, bytes, iterable, or file. got %r" + % type(data) + ) + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + raise NotImplementedError( + "HTTP/2 does not support setting up a tunnel through a proxy" + ) + + def getresponse( # type: ignore[override] + self, + ) -> HTTP2Response: + status = None + data = bytearray() + with self._h2_conn as conn: + end_stream = False + while not end_stream: + # TODO: Arbitrary read value. + if received_data := self.sock.recv(65535): + events = conn.receive_data(received_data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + headers = HTTPHeaderDict() + for header, value in event.headers: + if header == b":status": + status = int(value.decode()) + else: + headers.add( + header.decode("ascii"), value.decode("ascii") + ) + + elif isinstance(event, h2.events.DataReceived): + data += event.data + conn.acknowledge_received_data( + event.flow_controlled_length, event.stream_id + ) + + elif isinstance(event, h2.events.StreamEnded): + end_stream = True + + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + assert status is not None + return HTTP2Response( + status=status, + headers=headers, + request_url=self._request_url, + data=bytes(data), + ) + + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + **kwargs: typing.Any, + ) -> None: + """Send an HTTP/2 request""" + if "chunked" in kwargs: + # TODO this is often present from upstream. + # raise NotImplementedError("`chunked` isn't supported with HTTP/2") + pass + + if self.sock is not None: + self.sock.settimeout(self.timeout) + + self.putrequest(method, url) + + headers = headers or {} + for k, v in headers.items(): + if k.lower() == "transfer-encoding" and v == "chunked": + continue + else: + self.putheader(k, v) + + if b"user-agent" not in dict(self._headers): + self.putheader(b"user-agent", _get_default_user_agent()) + + if body: + self.endheaders(message_body=body) + self.send(body) + else: + self.endheaders() + + def close(self) -> None: + with self._h2_conn as conn: + try: + conn.close_connection() + if data := conn.data_to_send(): + self.sock.sendall(data) + except Exception: + pass + + # Reset all our HTTP/2 connection state. + self._h2_conn = self._new_h2_conn() + self._h2_stream = None + self._headers = [] + + super().close() + + +class HTTP2Response(BaseHTTPResponse): + # TODO: This is a woefully incomplete response object, but works for non-streaming. + def __init__( + self, + status: int, + headers: HTTPHeaderDict, + request_url: str, + data: bytes, + decode_content: bool = False, # TODO: support decoding + ) -> None: + super().__init__( + status=status, + headers=headers, + # Following CPython, we map HTTP versions to major * 10 + minor integers + version=20, + version_string="HTTP/2", + # No reason phrase in HTTP/2 + reason=None, + decode_content=decode_content, + request_url=request_url, + ) + self._data = data + self.length_remaining = 0 + + @property + def data(self) -> bytes: + return self._data + + def get_redirect_location(self) -> None: + return None + + def close(self) -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py new file mode 100644 index 0000000000..9ea900764f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import threading + + +class _HTTP2ProbeCache: + __slots__ = ( + "_lock", + "_cache_locks", + "_cache_values", + ) + + def __init__(self) -> None: + self._lock = threading.Lock() + self._cache_locks: dict[tuple[str, int], threading.RLock] = {} + self._cache_values: dict[tuple[str, int], bool | None] = {} + + def acquire_and_get(self, host: str, port: int) -> bool | None: + # By the end of this block we know that + # _cache_[values,locks] is available. + value = None + with self._lock: + key = (host, port) + try: + value = self._cache_values[key] + # If it's a known value we return right away. + if value is not None: + return value + except KeyError: + self._cache_locks[key] = threading.RLock() + self._cache_values[key] = None + + # If the value is unknown, we acquire the lock to signal + # to the requesting thread that the probe is in progress + # or that the current thread needs to return their findings. + key_lock = self._cache_locks[key] + key_lock.acquire() + try: + # If the by the time we get the lock the value has been + # updated we want to return the updated value. + value = self._cache_values[key] + + # In case an exception like KeyboardInterrupt is raised here. + except BaseException as e: # Defensive: + assert not isinstance(e, KeyError) # KeyError shouldn't be possible. + key_lock.release() + raise + + return value + + def set_and_release( + self, host: str, port: int, supports_http2: bool | None + ) -> None: + key = (host, port) + key_lock = self._cache_locks[key] + with key_lock: # Uses an RLock, so can be locked again from same thread. + if supports_http2 is None and self._cache_values[key] is not None: + raise ValueError( + "Cannot reset HTTP/2 support for origin after value has been set." + ) # Defensive: not expected in normal usage + + self._cache_values[key] = supports_http2 + key_lock.release() + + def _values(self) -> dict[tuple[str, int], bool | None]: + """This function is for testing purposes only. Gets the current state of the probe cache""" + with self._lock: + return {k: v for k, v in self._cache_values.items()} + + def _reset(self) -> None: + """This function is for testing purposes only. Reset the cache values""" + with self._lock: + self._cache_locks = {} + self._cache_values = {} + + +_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() + +set_and_release = _HTTP2_PROBE_CACHE.set_and_release +acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get +_values = _HTTP2_PROBE_CACHE._values +_reset = _HTTP2_PROBE_CACHE._reset + +__all__ = [ + "set_and_release", + "acquire_and_get", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py new file mode 100644 index 0000000000..8f2c56745c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py @@ -0,0 +1,653 @@ +from __future__ import annotations + +import functools +import logging +import typing +import warnings +from types import TracebackType +from urllib.parse import urljoin + +from ._collections import HTTPHeaderDict, RecentlyUsedContainer +from ._request_methods import RequestMethods +from .connection import ProxyConfig +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme +from .exceptions import ( + LocationValueError, + MaxRetryError, + ProxySchemeUnknown, + URLSchemeUnknown, +) +from .response import BaseHTTPResponse +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.proxy import connection_requires_http_tunnel +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import Url, parse_url + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + +__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] + + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ( + "key_file", + "cert_file", + "cert_reqs", + "ca_certs", + "ca_cert_data", + "ssl_version", + "ssl_minimum_version", + "ssl_maximum_version", + "ca_cert_dir", + "ssl_context", + "key_password", + "server_hostname", +) +# Default value for `blocksize` - a new parameter introduced to +# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 +_DEFAULT_BLOCKSIZE = 16384 + + +class PoolKey(typing.NamedTuple): + """ + All known keyword arguments that could be provided to the pool manager, its + pools, or the underlying connections. + + All custom key schemes should include the fields in this key at a minimum. + """ + + key_scheme: str + key_host: str + key_port: int | None + key_timeout: Timeout | float | int | None + key_retries: Retry | bool | int | None + key_block: bool | None + key_source_address: tuple[str, int] | None + key_key_file: str | None + key_key_password: str | None + key_cert_file: str | None + key_cert_reqs: str | None + key_ca_certs: str | None + key_ca_cert_data: str | bytes | None + key_ssl_version: int | str | None + key_ssl_minimum_version: ssl.TLSVersion | None + key_ssl_maximum_version: ssl.TLSVersion | None + key_ca_cert_dir: str | None + key_ssl_context: ssl.SSLContext | None + key_maxsize: int | None + key_headers: frozenset[tuple[str, str]] | None + key__proxy: Url | None + key__proxy_headers: frozenset[tuple[str, str]] | None + key__proxy_config: ProxyConfig | None + key_socket_options: _TYPE_SOCKET_OPTIONS | None + key__socks_options: frozenset[tuple[str, str]] | None + key_assert_hostname: bool | str | None + key_assert_fingerprint: str | None + key_server_hostname: str | None + key_blocksize: int | None + + +def _default_key_normalizer( + key_class: type[PoolKey], request_context: dict[str, typing.Any] +) -> PoolKey: + """ + Create a pool key out of a request context dictionary. + + According to RFC 3986, both the scheme and host are case-insensitive. + Therefore, this function normalizes both before constructing the pool + key for an HTTPS request. If you wish to change this behaviour, provide + alternate callables to ``key_fn_by_scheme``. + + :param key_class: + The class to use when constructing the key. This should be a namedtuple + with the ``scheme`` and ``host`` keys at a minimum. + :type key_class: namedtuple + :param request_context: + A dictionary-like object that contain the context for a request. + :type request_context: dict + + :return: A namedtuple that can be used as a connection pool key. + :rtype: PoolKey + """ + # Since we mutate the dictionary, make a copy first + context = request_context.copy() + context["scheme"] = context["scheme"].lower() + context["host"] = context["host"].lower() + + # These are both dictionaries and need to be transformed into frozensets + for key in ("headers", "_proxy_headers", "_socks_options"): + if key in context and context[key] is not None: + context[key] = frozenset(context[key].items()) + + # The socket_options key may be a list and needs to be transformed into a + # tuple. + socket_opts = context.get("socket_options") + if socket_opts is not None: + context["socket_options"] = tuple(socket_opts) + + # Map the kwargs to the names in the namedtuple - this is necessary since + # namedtuples can't have fields starting with '_'. + for key in list(context.keys()): + context["key_" + key] = context.pop(key) + + # Default to ``None`` for keys missing from the context + for field in key_class._fields: + if field not in context: + context[field] = None + + # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context + if context.get("key_blocksize") is None: + context["key_blocksize"] = _DEFAULT_BLOCKSIZE + + return key_class(**context) + + +#: A dictionary that maps a scheme to a callable that creates a pool key. +#: This can be used to alter the way pool keys are constructed, if desired. +#: Each PoolManager makes a copy of this dictionary so they can be configured +#: globally here, or individually on the instance. +key_fn_by_scheme = { + "http": functools.partial(_default_key_normalizer, PoolKey), + "https": functools.partial(_default_key_normalizer, PoolKey), +} + +pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \\**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example: + + .. code-block:: python + + import urllib3 + + http = urllib3.PoolManager(num_pools=2) + + resp1 = http.request("GET", "https://google.com/") + resp2 = http.request("GET", "https://google.com/mail") + resp3 = http.request("GET", "https://yahoo.com/") + + print(len(http.pools)) + # 2 + + """ + + proxy: Url | None = None + proxy_config: ProxyConfig | None = None + + def __init__( + self, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + super().__init__(headers) + # PoolManager handles redirects itself in PoolManager.urlopen(). + # It always passes redirect=False to the underlying connection pool to + # suppress per-pool redirect handling. If the user supplied a non-Retry + # value (int/bool/etc) for retries and we let the pool normalize it + # while redirect=False, the resulting Retry object would have redirect + # handling disabled, which can interfere with PoolManager's own + # redirect logic. Normalize here so redirects remain governed solely by + # PoolManager logic. + if "retries" in connection_pool_kw: + retries = connection_pool_kw["retries"] + if not isinstance(retries, Retry): + retries = Retry.from_int(retries) + connection_pool_kw = connection_pool_kw.copy() + connection_pool_kw["retries"] = retries + self.connection_pool_kw = connection_pool_kw + + self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] + self.pools = RecentlyUsedContainer(num_pools) + + # Locally set the pool classes and keys so other PoolManagers can + # override them. + self.pool_classes_by_scheme = pool_classes_by_scheme + self.key_fn_by_scheme = key_fn_by_scheme.copy() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.clear() + # Return False to re-raise any potential exceptions + return False + + def _new_pool( + self, + scheme: str, + host: str, + port: int, + request_context: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and + any additional pool keyword arguments. + + If ``request_context`` is provided, it is provided as keyword arguments + to the pool class used. This method is used to actually create the + connection pools handed out by :meth:`connection_from_url` and + companion methods. It is intended to be overridden for customization. + """ + pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] + if request_context is None: + request_context = self.connection_pool_kw.copy() + + # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly + # set to 'None' in the request_context. + if request_context.get("blocksize") is None: + request_context["blocksize"] = _DEFAULT_BLOCKSIZE + + # Although the context has everything necessary to create the pool, + # this function has historically only used the scheme, host, and port + # in the positional args. When an API change is acceptable these can + # be removed. + for key in ("scheme", "host", "port"): + request_context.pop(key, None) + + if scheme == "http": + for kw in SSL_KEYWORDS: + request_context.pop(kw, None) + + return pool_cls(host, port, **request_context) + + def clear(self) -> None: + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is + provided, it is merged with the instance's ``connection_pool_kw`` + variable and used to create the new connection pool, if one is + needed. + """ + + if not host: + raise LocationValueError("No host specified.") + + request_context = self._merge_pool_kwargs(pool_kwargs) + request_context["scheme"] = scheme or "http" + if not port: + port = port_by_scheme.get(request_context["scheme"].lower(), 80) + request_context["port"] = port + request_context["host"] = host + + return self.connection_from_context(request_context) + + def connection_from_context( + self, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. + + ``request_context`` must at least contain the ``scheme`` key and its + value must be a key in ``key_fn_by_scheme`` instance variable. + """ + if "strict" in request_context: + warnings.warn( + "The 'strict' parameter is no longer needed on Python 3+. " + "This will raise an error in urllib3 v3.0.", + FutureWarning, + ) + request_context.pop("strict") + + scheme = request_context["scheme"].lower() + pool_key_constructor = self.key_fn_by_scheme.get(scheme) + if not pool_key_constructor: + raise URLSchemeUnknown(scheme) + pool_key = pool_key_constructor(request_context) + + return self.connection_from_pool_key(pool_key, request_context=request_context) + + def connection_from_pool_key( + self, pool_key: PoolKey, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. + + ``pool_key`` should be a namedtuple that only contains immutable + objects. At a minimum it must have the ``scheme``, ``host``, and + ``port`` fields. + """ + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + scheme = request_context["scheme"] + host = request_context["host"] + port = request_context["port"] + pool = self._new_pool(scheme, host, port, request_context=request_context) + self.pools[pool_key] = pool + + return pool + + def connection_from_url( + self, url: str, pool_kwargs: dict[str, typing.Any] | None = None + ) -> HTTPConnectionPool: + """ + Similar to :func:`urllib3.connectionpool.connection_from_url`. + + If ``pool_kwargs`` is not provided and a new pool needs to be + constructed, ``self.connection_pool_kw`` is used to initialize + the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` + is provided, it is used instead. Note that if a new pool does not + need to be created for the request, the provided ``pool_kwargs`` are + not used. + """ + u = parse_url(url) + return self.connection_from_host( + u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs + ) + + def _merge_pool_kwargs( + self, override: dict[str, typing.Any] | None + ) -> dict[str, typing.Any]: + """ + Merge a dictionary of override values for self.connection_pool_kw. + + This does not modify self.connection_pool_kw and returns a new dict. + Any keys in the override dictionary with a value of ``None`` are + removed from the merged dictionary. + """ + base_pool_kwargs = self.connection_pool_kw.copy() + if override: + for key, value in override.items(): + if value is None: + try: + del base_pool_kwargs[key] + except KeyError: + pass + else: + base_pool_kwargs[key] = value + return base_pool_kwargs + + def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: + """ + Indicates if the proxy requires the complete destination URL in the + request. Normally this is only needed when not using an HTTP CONNECT + tunnel. + """ + if self.proxy is None: + return False + + return not connection_requires_http_tunnel( + self.proxy, self.proxy_config, parsed_url.scheme + ) + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + """ + Same as :meth:`urllib3.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + + if u.scheme is None: + warnings.warn( + "URLs without a scheme (ie 'https://') are deprecated and will raise an error " + "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " + "start with 'https://' or 'http://'. Read more in this issue: " + "https://github.com/urllib3/urllib3/issues/2920", + category=FutureWarning, + stacklevel=2, + ) + + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw["assert_same_host"] = False + kw["redirect"] = False + + if "headers" not in kw: + kw["headers"] = self.headers + + if self._proxy_requires_url_absolute_form(u): + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + kw["body"] = None + kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + + retries = kw.get("retries", response.retries) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # conn.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not conn.is_same_host( + redirect_location + ): + new_headers = kw["headers"].copy() + for header in kw["headers"]: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + kw["headers"] = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + kw["retries"] = retries + kw["redirect"] = redirect + + log.info("Redirecting %s -> %s", url, redirect_location) + + response.drain_conn() + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary containing headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + :param proxy_ssl_context: + The proxy SSL context is used to establish the TLS connection to the + proxy when using HTTPS proxies. + + :param use_forwarding_for_https: + (Defaults to False) If set to True will forward requests to the HTTPS + proxy to be made on behalf of the client instead of creating a TLS + tunnel via the CONNECT method. **Enabling this flag means that request + and response headers and content will be visible from the HTTPS proxy** + whereas tunneling keeps request and response headers and content + private. IP address, target hostname, SNI, and port are always visible + to an HTTPS proxy even when this flag is disabled. + + :param proxy_assert_hostname: + The hostname of the certificate to verify against. + + :param proxy_assert_fingerprint: + The fingerprint of the certificate to verify against. + + Example: + + .. code-block:: python + + import urllib3 + + proxy = urllib3.ProxyManager("https://localhost:3128/") + + resp1 = proxy.request("GET", "http://google.com/") + resp2 = proxy.request("GET", "http://httpbin.org/") + + # One pool was shared by both plain HTTP requests. + print(len(proxy.pools)) + # 1 + + resp3 = proxy.request("GET", "https://httpbin.org/") + resp4 = proxy.request("GET", "https://twitter.com/") + + # A separate pool was added for each HTTPS target. + print(len(proxy.pools)) + # 3 + + """ + + def __init__( + self, + proxy_url: str, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + proxy_headers: typing.Mapping[str, str] | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + use_forwarding_for_https: bool = False, + proxy_assert_hostname: None | str | typing.Literal[False] = None, + proxy_assert_fingerprint: str | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + if isinstance(proxy_url, HTTPConnectionPool): + str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" + else: + str_proxy_url = proxy_url + proxy = parse_url(str_proxy_url) + + if proxy.scheme not in ("http", "https"): + raise ProxySchemeUnknown(proxy.scheme) + + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + self.proxy_ssl_context = proxy_ssl_context + self.proxy_config = ProxyConfig( + proxy_ssl_context, + use_forwarding_for_https, + proxy_assert_hostname, + proxy_assert_fingerprint, + ) + + connection_pool_kw["_proxy"] = self.proxy + connection_pool_kw["_proxy_headers"] = self.proxy_headers + connection_pool_kw["_proxy_config"] = self.proxy_config + + super().__init__(num_pools, headers, **connection_pool_kw) + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + if scheme == "https": + return super().connection_from_host( + host, port, scheme, pool_kwargs=pool_kwargs + ) + + return super().connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] + ) + + def _set_proxy_headers( + self, url: str, headers: typing.Mapping[str, str] | None = None + ) -> typing.Mapping[str, str]: + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {"Accept": "*/*"} + + netloc = parse_url(url).netloc + if netloc: + headers_["Host"] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): + # For connections using HTTP CONNECT, httplib sets the necessary + # headers on the CONNECT to the proxy. If we're not using CONNECT, + # we'll definitely need to set 'Host' at the very least. + headers = kw.get("headers", self.headers) + kw["headers"] = self._set_proxy_headers(url, headers) + + return super().urlopen(method, url, redirect=redirect, **kw) + + +def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: + return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed new file mode 100644 index 0000000000..5f3ea3d919 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed @@ -0,0 +1,2 @@ +# Instruct type checkers to look for inline type annotations in this package. +# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py new file mode 100644 index 0000000000..e9246b75e3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py @@ -0,0 +1,1493 @@ +from __future__ import annotations + +import collections +import io +import json as _json +import logging +import socket +import sys +import typing +import warnings +import zlib +from contextlib import contextmanager +from http.client import HTTPMessage as _HttplibHTTPMessage +from http.client import HTTPResponse as _HttplibHTTPResponse +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from ._base_connection import BaseHTTPConnection + +try: + try: + import brotlicffi as brotli # type: ignore[import-not-found] + except ImportError: + import brotli # type: ignore[import-not-found] +except ImportError: + brotli = None + +from . import util +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .connection import BaseSSLError, HTTPConnection, HTTPException +from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, + DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, + InvalidHeader, + ProtocolError, + ReadTimeoutError, + ResponseNotChunked, + SSLError, +) +from .util.response import is_fp_closed, is_response_to_head +from .util.retry import Retry + +if typing.TYPE_CHECKING: + from .connectionpool import HTTPConnectionPool + +log = logging.getLogger(__name__) + + +class ContentDecoder: + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + raise NotImplementedError() + + @property + def has_unconsumed_tail(self) -> bool: + raise NotImplementedError() + + def flush(self) -> bytes: + raise NotImplementedError() + + +class DeflateDecoder(ContentDecoder): + def __init__(self) -> None: + self._first_try = True + self._first_try_data = b"" + self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + data = self._unfed_data + data + self._unfed_data = b"" + if not data and not self._obj.unconsumed_tail: + return data + original_max_length = max_length + if original_max_length < 0: + max_length = 0 + elif original_max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unfed_data = data + return b"" + + # Subsequent calls always reuse `self._obj`. zlib requires + # passing the unconsumed tail if decompression is to continue. + if not self._first_try: + return self._obj.decompress( + self._obj.unconsumed_tail + data, max_length=max_length + ) + + # First call tries with RFC 1950 ZLIB format. + self._first_try_data += data + try: + decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False + self._first_try_data = b"" + return decompressed + # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress( + self._first_try_data, max_length=original_max_length + ) + finally: + self._first_try_data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unfed_data) or ( + bool(self._obj.unconsumed_tail) and not self._first_try + ) + + def flush(self) -> bytes: + return self._obj.flush() + + +class GzipDecoderState: + FIRST_MEMBER = 0 + OTHER_MEMBERS = 1 + SWALLOW_DATA = 2 + + +class GzipDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER + self._unconsumed_tail = b"" + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() + if self._state == GzipDecoderState.SWALLOW_DATA: + return bytes(ret) + + if max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unconsumed_tail += data + return b"" + + # zlib requires passing the unconsumed tail to the subsequent + # call if decompression is to continue. + data = self._unconsumed_tail + data + if not data and self._obj.eof: + return bytes(ret) + + while True: + try: + ret += self._obj.decompress( + data, max_length=max(max_length - len(ret), 0) + ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA + self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise + + self._unconsumed_tail = data = ( + self._obj.unconsumed_tail or self._obj.unused_data + ) + if max_length > 0 and len(ret) >= max_length: + break + + if not data: + return bytes(ret) + # When the end of a gzip member is reached, a new decompressor + # must be created for unused (possibly future) data. + if self._obj.eof: + self._state = GzipDecoderState.OTHER_MEMBERS + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + + return bytes(ret) + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unconsumed_tail) + + def flush(self) -> bytes: + return self._obj.flush() + + +if brotli is not None: + + class BrotliDecoder(ContentDecoder): + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' + def __init__(self) -> None: + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): + setattr(self, "_decompress", self._obj.decompress) + else: + setattr(self, "_decompress", self._obj.process) + + # Requires Brotli >= 1.2.0 for `output_buffer_limit`. + def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: + raise NotImplementedError() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + try: + if max_length > 0: + return self._decompress(data, output_buffer_limit=max_length) + else: + return self._decompress(data) + except TypeError: + # Fallback for Brotli/brotlicffi/brotlipy versions without + # the `output_buffer_limit` parameter. + warnings.warn( + "Brotli >= 1.2.0 is required to prevent decompression bombs.", + DependencyWarning, + ) + return self._decompress(data) + + @property + def has_unconsumed_tail(self) -> bool: + try: + return not self._obj.can_accept_more_data() + except AttributeError: + return False + + def flush(self) -> bytes: + if hasattr(self._obj, "flush"): + return self._obj.flush() # type: ignore[no-any-return] + return b"" + + +try: + if sys.version_info >= (3, 14): + from compression import zstd + else: + from backports import zstd +except ImportError: + HAS_ZSTD = False +else: + HAS_ZSTD = True + + class ZstdDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zstd.ZstdDecompressor() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if not data and not self.has_unconsumed_tail: + return b"" + if self._obj.eof: + data = self._obj.unused_data + data + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress(data, max_length=max_length) + length = len(part) + data_parts = [part] + # Every loop iteration is supposed to read data from a separate frame. + # The loop breaks when: + # - enough data is read; + # - no more unused data is available; + # - end of the last read frame has not been reached (i.e., + # more data has to be fed). + while ( + self._obj.eof + and self._obj.unused_data + and (max_length < 0 or length < max_length) + ): + unused_data = self._obj.unused_data + if not self._obj.needs_input: + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress( + unused_data, + max_length=(max_length - length) if max_length > 0 else -1, + ) + if part_length := len(part): + data_parts.append(part) + length += part_length + elif self._obj.needs_input: + break + return b"".join(data_parts) + + @property + def has_unconsumed_tail(self) -> bool: + return not (self._obj.needs_input or self._obj.eof) or bool( + self._obj.unused_data + ) + + def flush(self) -> bytes: + if not self._obj.eof: + raise DecodeError("Zstandard data is incomplete") + return b"" + + +class MultiDecoder(ContentDecoder): + """ + From RFC7231: + If one or more encodings have been applied to a representation, the + sender that applied the encodings MUST generate a Content-Encoding + header field that lists the content codings in the order in which + they were applied. + """ + + # Maximum allowed number of chained HTTP encodings in the + # Content-Encoding header. + max_decode_links = 5 + + def __init__(self, modes: str) -> None: + encodings = [m.strip() for m in modes.split(",")] + if len(encodings) > self.max_decode_links: + raise DecodeError( + "Too many content encodings in the chain: " + f"{len(encodings)} > {self.max_decode_links}" + ) + self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self) -> bytes: + return self._decoders[0].flush() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if max_length <= 0: + for d in reversed(self._decoders): + data = d.decompress(data) + return data + + ret = bytearray() + # Every while loop iteration goes through all decoders once. + # It exits when enough data is read or no more data can be read. + # It is possible that the while loop iteration does not produce + # any data because we retrieve up to `max_length` from every + # decoder, and the amount of bytes may be insufficient for the + # next decoder to produce enough/any output. + while True: + any_data = False + for d in reversed(self._decoders): + data = d.decompress(data, max_length=max_length - len(ret)) + if data: + any_data = True + # We should not break when no data is returned because + # next decoders may produce data even with empty input. + ret += data + if not any_data or len(ret) >= max_length: + return bytes(ret) + data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return any(d.has_unconsumed_tail for d in self._decoders) + + +def _get_decoder(mode: str) -> ContentDecoder: + if "," in mode: + return MultiDecoder(mode) + + # According to RFC 9110 section 8.4.1.3, recipients should + # consider x-gzip equivalent to gzip + if mode in ("gzip", "x-gzip"): + return GzipDecoder() + + if brotli is not None and mode == "br": + return BrotliDecoder() + + if HAS_ZSTD and mode == "zstd": + return ZstdDecoder() + + return DeflateDecoder() + + +class BytesQueueBuffer: + """Memory-efficient bytes buffer + + To return decoded data in read() and still follow the BufferedIOBase API, we need a + buffer to always return the correct amount of bytes. + + This buffer should be filled using calls to put() + + Our maximum memory usage is determined by the sum of the size of: + + * self.buffer, which contains the full data + * the largest chunk that we will copy in get() + """ + + def __init__(self) -> None: + self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() + self._size: int = 0 + + def __len__(self) -> int: + return self._size + + def put(self, data: bytes) -> None: + self.buffer.append(data) + self._size += len(data) + + def get(self, n: int) -> bytes: + if n == 0: + return b"" + elif not self.buffer: + raise RuntimeError("buffer is empty") + elif n < 0: + raise ValueError("n should be > 0") + + if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): + self._size -= n + return self.buffer.popleft() + + fetched = 0 + ret = io.BytesIO() + while fetched < n: + remaining = n - fetched + chunk = self.buffer.popleft() + chunk_length = len(chunk) + if remaining < chunk_length: + chunk = memoryview(chunk) + left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] + ret.write(left_chunk) + self.buffer.appendleft(right_chunk) + self._size -= remaining + break + else: + ret.write(chunk) + self._size -= chunk_length + fetched += chunk_length + + if not self.buffer: + break + + return ret.getvalue() + + def get_all(self) -> bytes: + buffer = self.buffer + if not buffer: + assert self._size == 0 + return b"" + if len(buffer) == 1: + result = buffer.pop() + if isinstance(result, memoryview): + result = result.tobytes() + else: + ret = io.BytesIO() + ret.writelines(buffer.popleft() for _ in range(len(buffer))) + result = ret.getvalue() + self._size = 0 + return result + + +class BaseHTTPResponse(io.IOBase): + CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] + if brotli is not None: + CONTENT_DECODERS += ["br"] + if HAS_ZSTD: + CONTENT_DECODERS += ["zstd"] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + + if HAS_ZSTD: + DECODER_ERROR_CLASSES += (zstd.ZstdError,) + + def __init__( + self, + *, + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int, + version: int, + version_string: str, + reason: str | None, + decode_content: bool, + request_url: str | None, + retries: Retry | None = None, + ) -> None: + if isinstance(headers, HTTPHeaderDict): + self.headers = headers + else: + self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] + self.status = status + self.version = version + self.version_string = version_string + self.reason = reason + self.decode_content = decode_content + self._has_decoded_content = False + self._request_url: str | None = request_url + self.retries = retries + + self.chunked = False + tr_enc = self.headers.get("transfer-encoding", "").lower() + # Don't incur the penalty of creating a list and then discarding it + encodings = (enc.strip() for enc in tr_enc.split(",")) + if "chunked" in encodings: + self.chunked = True + + self._decoder: ContentDecoder | None = None + self.length_remaining: int | None + + def get_redirect_location(self) -> str | None | typing.Literal[False]: + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get("location") + return False + + @property + def data(self) -> bytes: + raise NotImplementedError() + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + @property + def url(self) -> str | None: + raise NotImplementedError() + + @url.setter + def url(self, url: str | None) -> None: + raise NotImplementedError() + + @property + def connection(self) -> BaseHTTPConnection | None: + raise NotImplementedError() + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + if retries is not None and retries.history: + self.url = retries.history[-1].redirect_location + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + raise NotImplementedError() + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + raise NotImplementedError() + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def release_conn(self) -> None: + raise NotImplementedError() + + def drain_conn(self) -> None: + raise NotImplementedError() + + def shutdown(self) -> None: + raise NotImplementedError() + + def close(self) -> None: + raise NotImplementedError() + + def _init_decoder(self) -> None: + """ + Set-up the _decoder attribute if necessary. + """ + # Note: content-encoding value should be case-insensitive, per RFC 7230 + # Section 3.2 + content_encoding = self.headers.get("content-encoding", "").lower() + if self._decoder is None: + if content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + elif "," in content_encoding: + encodings = [ + e.strip() + for e in content_encoding.split(",") + if e.strip() in self.CONTENT_DECODERS + ] + if encodings: + self._decoder = _get_decoder(content_encoding) + + def _decode( + self, + data: bytes, + decode_content: bool | None, + flush_decoder: bool, + max_length: int | None = None, + ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + if max_length is None or flush_decoder: + max_length = -1 + + try: + if self._decoder: + data = self._decoder.decompress(data, max_length=max_length) + self._has_decoded_content = True + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, + e, + ) from e + if flush_decoder: + data += self._flush_decoder() + + return data + + def _flush_decoder(self) -> bytes: + """ + Flushes the decoder. Should only be called if the decoder is actually + being used. + """ + if self._decoder: + return self._decoder.decompress(b"") + self._decoder.flush() + return b"" + + # Compatibility methods for `io` module + def readinto(self, b: bytearray | memoryview[int]) -> int: + temp = self.read(len(b)) + if len(temp) == 0: + return 0 + else: + b[: len(temp)] = temp + return len(temp) + + # Methods used by dependent libraries + def getheaders(self) -> HTTPHeaderDict: + return self.headers + + def getheader(self, name: str, default: str | None = None) -> str | None: + return self.headers.get(name, default) + + # Compatibility method for http.cookiejar + def info(self) -> HTTPHeaderDict: + return self.headers + + def geturl(self) -> str | None: + return self.url + + +class HTTPResponse(BaseHTTPResponse): + """ + HTTP Response container. + + Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. This + class is also compatible with the Python standard library's :mod:`io` + module, and can hence be treated as a readable object in the context of that + framework. + + Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param original_response: + When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + + :param retries: + The retries contains the last :class:`~urllib3.util.retry.Retry` that + was used during the request. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + + def __init__( + self, + body: _TYPE_BODY = "", + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int = 0, + version: int = 0, + version_string: str = "HTTP/?", + reason: str | None = None, + preload_content: bool = True, + decode_content: bool = True, + original_response: _HttplibHTTPResponse | None = None, + pool: HTTPConnectionPool | None = None, + connection: HTTPConnection | None = None, + msg: _HttplibHTTPMessage | None = None, + retries: Retry | None = None, + enforce_content_length: bool = True, + request_method: str | None = None, + request_url: str | None = None, + auto_close: bool = True, + sock_shutdown: typing.Callable[[int], None] | None = None, + ) -> None: + super().__init__( + headers=headers, + status=status, + version=version, + version_string=version_string, + reason=reason, + decode_content=decode_content, + request_url=request_url, + retries=retries, + ) + + self.enforce_content_length = enforce_content_length + self.auto_close = auto_close + + self._body = None + self._uncached_read_occurred = False + self._fp: _HttplibHTTPResponse | None = None + self._original_response = original_response + self._fp_bytes_read = 0 + self.msg = msg + + if body and isinstance(body, (str, bytes)): + self._body = body + + self._pool = pool + self._connection = connection + + if hasattr(body, "read"): + self._fp = body # type: ignore[assignment] + self._sock_shutdown = sock_shutdown + + # Are we using the chunked-style of transfer encoding? + self.chunk_left: int | None = None + + # Determine length of response + self.length_remaining = self._init_length(request_method) + + # Used to return the correct amount of bytes for partial read()s + self._decoded_buffer = BytesQueueBuffer() + + # If requested, preload the body. + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + """ + Read and discard any remaining HTTP response data in the response connection. + + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: + self._raw_read() + except (HTTPError, OSError, BaseSSLError, HTTPException): + pass + if self._has_decoded_content: + # `_raw_read` skips decompression, so we should clean up the + # decoder to avoid keeping unnecessary data in memory. + self._decoded_buffer = BytesQueueBuffer() + self._decoder = None + + @property + def data(self) -> bytes: + # For backwards-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body # type: ignore[return-value] + + if self._fp: + return self.read(cache_content=True) + + return None # type: ignore[return-value] + + @property + def connection(self) -> HTTPConnection | None: + return self._connection + + def isclosed(self) -> bool: + return is_fp_closed(self._fp) + + def tell(self) -> int: + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:`HTTPResponse.read` + if bytes are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def _init_length(self, request_method: str | None) -> int | None: + """ + Set initial length value for Response content if available. + """ + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + if self.chunked: + # This Response will fail with an IncompleteRead if it can't be + # received as chunked. This method falls back to attempt reading + # the response before raising an exception. + log.warning( + "Received response with both Content-Length and " + "Transfer-Encoding set. This is expressly forbidden " + "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " + "attempting to process response as Transfer-Encoding: " + "chunked." + ) + return None + + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Convert status to int for comparison + # In some cases, httplib returns a status of "_UNKNOWN" + try: + status = int(self.status) + except ValueError: + status = 0 + + # Check for responses that shouldn't include a body + if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": + length = 0 + + return length + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch low-level python exceptions, instead re-raising urllib3 + variants, so that low-level exceptions are not leaked in the + high-level api. + + On exit, release the connection back to the pool. + """ + clean_exit = False + + try: + try: + yield + + except SocketTimeout as e: + # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but + # there is yet no clean way to get at it from this context. + raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + + except BaseSSLError as e: + # SSL errors related to framing/MAC get wrapped and reraised here + raise SSLError(e) from e + + except IncompleteRead as e: + if ( + e.expected is not None + and e.partial is not None + and e.expected == -e.partial + ): + arg = "Response may not contain content." + else: + arg = f"Connection broken: {e!r}" + raise ProtocolError(arg, e) from e + + except (HTTPException, OSError) as e: + raise ProtocolError(f"Connection broken: {e!r}", e) from e + + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now to ensure that the connection is + # released back to the pool. + if self._original_response: + self._original_response.close() + + # Closing the response may not actually be sufficient to close + # everything, so if we have a hold of the connection close that + # too. + if self._connection: + self._connection.close() + + # If we hold the original response but it's closed now, we should + # return the connection back to the pool. + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def _fp_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Read a response with the thought that reading the number of bytes + larger than can fit in a 32-bit int at a time via SSL in some + known cases leads to an overflow error that has to be prevented + if `amt` or `self.length_remaining` indicate that a problem may + happen. + + This happens to urllib3 injected with pyOpenSSL-backed SSL-support. + """ + assert self._fp + c_int_max = 2**31 - 1 + if ( + (amt and amt > c_int_max) + or ( + amt is None + and self.length_remaining + and self.length_remaining > c_int_max + ) + ) and util.IS_PYOPENSSL: + if read1: + return self._fp.read1(c_int_max) + buffer = io.BytesIO() + # Besides `max_chunk_amt` being a maximum chunk size, it + # affects memory overhead of reading a response by this + # method in CPython. + # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum + # chunk size that does not lead to an overflow error, but + # 256 MiB is a compromise. + max_chunk_amt = 2**28 + while amt is None or amt != 0: + if amt is not None: + chunk_amt = min(amt, max_chunk_amt) + amt -= chunk_amt + else: + chunk_amt = max_chunk_amt + data = self._fp.read(chunk_amt) + if not data: + break + buffer.write(data) + del data # to reduce peak memory usage by `max_chunk_amt`. + return buffer.getvalue() + elif read1: + return self._fp.read1(amt) if amt is not None else self._fp.read1() + else: + # StringIO doesn't like amt=None + return self._fp.read(amt) if amt is not None else self._fp.read() + + def _raw_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Reads `amt` of bytes from the socket. + """ + if self._fp is None: + return None # type: ignore[return-value] + + fp_closed = getattr(self._fp, "closed", False) + + with self._error_catcher(): + data = self._fp_read(amt, read1=read1) if not fp_closed else b"" + if amt is not None and amt != 0 and not data: + # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do + # not properly close the connection in all cases. There is + # no harm in redundantly calling close. + self._fp.close() + if ( + self.enforce_content_length + and self.length_remaining is not None + and self.length_remaining != 0 + ): + # This is an edge case that httplib failed to cover due + # to concerns of backward compatibility. We're + # addressing it here to make sure IncompleteRead is + # raised during streaming, so all calls with incorrect + # Content-Length are caught. + raise IncompleteRead(self._fp_bytes_read, self.length_remaining) + elif read1 and ( + (amt != 0 and not data) or self.length_remaining == len(data) + ): + # All data has been read, but `self._fp.read1` in + # CPython 3.12 and older doesn't always close + # `http.client.HTTPResponse`, so we close it here. + # See https://github.com/python/cpython/issues/113199 + self._fp.close() + + if data: + self._fp_bytes_read += len(data) + if self.length_remaining is not None: + self.length_remaining -= len(data) + return data + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + """ + Similar to :meth:`http.client.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + self._init_decoder() + if decode_content is None: + decode_content = self.decode_content + + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + elif amt is not None: + cache_content = False + + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and len(self._decoded_buffer) < amt + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) >= amt: + return self._decoded_buffer.get(amt) + + data = self._raw_read(amt) + if not cache_content: + self._uncached_read_occurred = True + + flush_decoder = amt is None or (amt != 0 and not data) + + if ( + not data + and len(self._decoded_buffer) == 0 + and not (self._decoder and self._decoder.has_unconsumed_tail) + ): + return data + + if amt is None: + data = self._decode(data, decode_content, flush_decoder) + # It's possible that there is buffered decoded data after a + # partial read. + if decode_content and len(self._decoded_buffer) > 0: + self._decoded_buffer.put(data) + data = self._decoded_buffer.get_all() + + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + # do not waste memory on buffer when not decoding + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + + while len(self._decoded_buffer) < amt and data: + # TODO make sure to initially read enough data to get past the headers + # For example, the GZ file header takes 10 bytes, we don't want to read + # it one byte at a time + data = self._raw_read(amt) + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + data = self._decoded_buffer.get(amt) + + return data + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + """ + Similar to ``http.client.HTTPResponse.read1`` and documented + in :meth:`io.BufferedReader.read1`, but with an additional parameter: + ``decode_content``. + + :param amt: + How much of the content to read. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if decode_content is None: + decode_content = self.decode_content + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + # try and respond without going to the network + if self._has_decoded_content: + if not decode_content: + raise RuntimeError( + "Calling read1(decode_content=False) is not supported after " + "read1(decode_content=True) was called." + ) + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and (amt is None or len(self._decoded_buffer) < amt) + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=( + amt - len(self._decoded_buffer) if amt is not None else None + ), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) > 0: + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + if amt == 0: + return b"" + + # FIXME, this method's type doesn't say returning None is possible + data = self._raw_read(amt, read1=True) + self._uncached_read_occurred = True + if not decode_content or data is None: + return data + + self._init_decoder() + while True: + flush_decoder = not data + decoded_data = self._decode( + data, decode_content, flush_decoder, max_length=amt + ) + self._decoded_buffer.put(decoded_data) + if decoded_data or flush_decoder: + break + data = self._raw_read(8192, read1=True) + + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if amt == 0: + return + + if self.chunked and self.supports_chunked_reads(): + yield from self.read_chunked(amt, decode_content=decode_content) + else: + while ( + not is_fp_closed(self._fp) + or len(self._decoded_buffer) > 0 + or (self._decoder and self._decoder.has_unconsumed_tail) + ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + # Overrides from io.IOBase + def readable(self) -> bool: + return True + + def shutdown(self) -> None: + if not self._sock_shutdown: + raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") + if self._connection is None: + raise RuntimeError( + "Cannot shutdown as connection has already been released to the pool" + ) + self._sock_shutdown(socket.SHUT_RD) + + def close(self) -> None: + self._sock_shutdown = None + + if not self.closed and self._fp: + self._fp.close() + + if self._connection: + self._connection.close() + + if not self.auto_close: + io.IOBase.close(self) + + @property + def closed(self) -> bool: + if not self.auto_close: + return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] + elif self._fp is None: + return True + elif hasattr(self._fp, "isclosed"): + return self._fp.isclosed() + elif hasattr(self._fp, "closed"): + return self._fp.closed + else: + return True + + def fileno(self) -> int: + if self._fp is None: + raise OSError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise OSError( + "The file-like object this HTTPResponse is wrapped " + "around has no file descriptor" + ) + + def flush(self) -> None: + if ( + self._fp is not None + and hasattr(self._fp, "flush") + and not getattr(self._fp, "closed", False) + ): + return self._fp.flush() + + def supports_chunked_reads(self) -> bool: + """ + Checks if the underlying file-like object looks like a + :class:`http.client.HTTPResponse` object. We do this by testing for + the fp attribute. If it is present we assume it returns raw chunks as + processed by read_chunked(). + """ + return hasattr(self._fp, "fp") + + def _update_chunk_length(self) -> None: + # First, we'll figure out length of a chunk and then + # we'll try to read it from socket. + if self.chunk_left is not None: + return None + line = self._fp.fp.readline() # type: ignore[union-attr] + line = line.split(b";", 1)[0] + try: + self.chunk_left = int(line, 16) + except ValueError: + self.close() + if line: + # Invalid chunked protocol response, abort. + raise InvalidChunkLength(self, line) from None + else: + # Truncated at start of next chunk + raise ProtocolError("Response ended prematurely") from None + + def _handle_chunk(self, amt: int | None) -> bytes: + returned_chunk = None + if amt is None: + chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + returned_chunk = chunk + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + elif self.chunk_left is not None and amt < self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self.chunk_left = self.chunk_left - amt + returned_chunk = value + elif amt == self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + returned_chunk = value + else: # amt > self.chunk_left + returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + return returned_chunk # type: ignore[no-any-return] + + def read_chunked( + self, amt: int | None = None, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + Similar to :meth:`HTTPResponse.read`, but with an additional + parameter: ``decode_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + self._init_decoder() + # FIXME: Rewrite this method and make it a class with a better structured logic. + if not self.chunked: + raise ResponseNotChunked( + "Response is not chunked. " + "Header 'transfer-encoding: chunked' is missing." + ) + if not self.supports_chunked_reads(): + raise BodyNotHttplibCompatible( + "Body should be http.client.HTTPResponse like. " + "It should have have an fp attribute which returns raw chunks." + ) + + with self._error_catcher(): + # Don't bother reading the body of a HEAD request. + if self._original_response and is_response_to_head(self._original_response): + self._original_response.close() + return None + + # If a response is already read and closed + # then return immediately. + if self._fp.fp is None: # type: ignore[union-attr] + return None + + if amt == 0: + return + elif amt and amt < 0: + # Negative numbers and `None` should be treated the same, + # but httplib handles only `None` correctly. + amt = None + + while True: + # First, check if any data is left in the decoder's buffer. + if self._decoder and self._decoder.has_unconsumed_tail: + chunk = b"" + else: + self._update_chunk_length() + self._uncached_read_occurred = True + if self.chunk_left == 0: + break + chunk = self._handle_chunk(amt) + decoded = self._decode( + chunk, + decode_content=decode_content, + flush_decoder=False, + max_length=amt, + ) + if decoded: + yield decoded + + if decode_content: + # On CPython and PyPy, we should never need to flush the + # decoder. However, on Jython we *might* need to, so + # lets defensively do it anyway. + decoded = self._flush_decoder() + if decoded: # Platform-specific: Jython. + yield decoded + + # Chunk content ends with \r\n: discard it. + while self._fp is not None: + line = self._fp.fp.readline() + if not line: + # Some sites may not end with '\r\n'. + break + if line == b"\r\n": + break + + # We read everything; close the "file". + if self._original_response: + self._original_response.close() + + @property + def url(self) -> str | None: + """ + Returns the URL that was the source of this response. + If the request that generated this response redirected, this method + will return the final redirect location. + """ + return self._request_url + + @url.setter + def url(self, url: str | None) -> None: + self._request_url = url + + def __iter__(self) -> typing.Iterator[bytes]: + buffer: list[bytes] = [] + for chunk in self.stream(decode_content=True): + if b"\n" in chunk: + chunks = chunk.split(b"\n") + yield b"".join(buffer) + chunks[0] + b"\n" + for x in chunks[1:-1]: + yield x + b"\n" + if chunks[-1]: + buffer = [chunks[-1]] + else: + buffer = [] + else: + buffer.append(chunk) + if buffer: + yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py new file mode 100644 index 0000000000..534126033c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py @@ -0,0 +1,42 @@ +# For backwards compatibility, provide imports that used to be here. +from __future__ import annotations + +from .connection import is_connection_dropped +from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers +from .response import is_fp_closed +from .retry import Retry +from .ssl_ import ( + ALPN_PROTOCOLS, + IS_PYOPENSSL, + SSLContext, + assert_fingerprint, + create_urllib3_context, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import Timeout +from .url import Url, parse_url +from .wait import wait_for_read, wait_for_write + +__all__ = ( + "IS_PYOPENSSL", + "SSLContext", + "ALPN_PROTOCOLS", + "Retry", + "Timeout", + "Url", + "assert_fingerprint", + "create_urllib3_context", + "is_connection_dropped", + "is_fp_closed", + "parse_url", + "make_headers", + "resolve_cert_reqs", + "resolve_ssl_version", + "ssl_wrap_socket", + "wait_for_read", + "wait_for_write", + "SKIP_HEADER", + "SKIPPABLE_HEADERS", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py new file mode 100644 index 0000000000..f92519ee91 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import socket +import typing + +from ..exceptions import LocationParseError +from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT + +_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] + +if typing.TYPE_CHECKING: + from .._base_connection import BaseHTTPConnection + + +def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + :param conn: :class:`urllib3.connection.HTTPConnection` object. + """ + return not conn.is_connected + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +# One additional modification is that we avoid binding to IPv6 servers +# discovered in DNS if the system doesn't have IPv6 functionality. +def create_connection( + address: tuple[str, int], + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, +) -> socket.socket: + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`socket.getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith("["): + host = host.strip("[]") + err = None + + # Using the value from allowed_gai_family() in the context of getaddrinfo lets + # us select whether to work with IPv4 DNS records, IPv6 records, or both. + # The original create_connection function always returns all records. + family = allowed_gai_family() + + try: + host.encode("idna") + except UnicodeError: + raise LocationParseError(f"'{host}', label empty or too long") from None + + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + _set_socket_options(sock, socket_options) + + if timeout is not _DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + # Break explicitly a reference cycle + err = None + return sock + + except OSError as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + try: + raise err + finally: + # Break explicitly a reference cycle + err = None + else: + raise OSError("getaddrinfo returns an empty list") + + +def _set_socket_options( + sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None +) -> None: + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) + + +def allowed_gai_family() -> socket.AddressFamily: + """This function is designed to work in the context of + getaddrinfo, where family=socket.AF_UNSPEC is the default and + will perform a DNS search for both IPv6 and IPv4 records.""" + + family = socket.AF_INET + if HAS_IPV6: + family = socket.AF_UNSPEC + return family + + +def _has_ipv6(host: str) -> bool: + """Returns True if the system can bind an IPv6 address.""" + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/urllib3/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = True + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py new file mode 100644 index 0000000000..908fc6621d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .url import Url + +if typing.TYPE_CHECKING: + from ..connection import ProxyConfig + + +def connection_requires_http_tunnel( + proxy_url: Url | None = None, + proxy_config: ProxyConfig | None = None, + destination_scheme: str | None = None, +) -> bool: + """ + Returns True if the connection requires an HTTP CONNECT through the proxy. + + :param URL proxy_url: + URL of the proxy. + :param ProxyConfig proxy_config: + Proxy configuration from poolmanager.py + :param str destination_scheme: + The scheme of the destination. (i.e https, http, etc) + """ + # If we're not using a proxy, no way to use a tunnel. + if proxy_url is None: + return False + + # HTTP destinations never require tunneling, we always forward. + if destination_scheme == "http": + return False + + # Support for forwarding with HTTPS proxies and HTTPS destinations. + if ( + proxy_url.scheme == "https" + and proxy_config + and proxy_config.use_forwarding_for_https + ): + return False + + # Otherwise always use a tunnel. + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py new file mode 100644 index 0000000000..6c2372ba7e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +import io +import sys +import typing +from base64 import b64encode +from enum import Enum + +from ..exceptions import UnrewindableBodyError +from .util import to_bytes + +if typing.TYPE_CHECKING: + from typing import Final + +# Pass as a value within ``headers`` to skip +# emitting some HTTP headers that are added automatically. +# The only headers that are supported are ``Accept-Encoding``, +# ``Host``, and ``User-Agent``. +SKIP_HEADER = "@@@SKIP_HEADER@@@" +SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) + +ACCEPT_ENCODING = "gzip,deflate" +try: + try: + import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 + except ImportError: + import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",br" + +try: + if sys.version_info >= (3, 14): + from compression import zstd as _unused_module_zstd # noqa: F401 + else: + from backports import zstd as _unused_module_zstd # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",zstd" + + +class _TYPE_FAILEDTELL(Enum): + token = 0 + + +_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token + +_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] + +# When sending a request with these methods we aren't expecting +# a body so don't need to set an explicit 'Content-Length: 0' +# The reason we do this in the negative instead of tracking methods +# which 'should' have a body is because unknown methods should be +# treated as if they were 'POST' which *does* expect a body. +_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} + + +def make_headers( + keep_alive: bool | None = None, + accept_encoding: bool | list[str] | str | None = None, + user_agent: str | None = None, + basic_auth: str | None = None, + proxy_basic_auth: str | None = None, + disable_cache: bool | None = None, +) -> dict[str, str]: + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. If the dependencies for + Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or + Zstandard (the ``backports.zstd`` package for Python before 3.14) + algorithms are installed, then their encodings are + included in the string ('br' and 'zstd', respectively). + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example: + + .. code-block:: python + + import urllib3 + + print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) + # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + print(urllib3.util.make_headers(accept_encoding=True)) + # {'accept-encoding': 'gzip,deflate'} + """ + headers: dict[str, str] = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ",".join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers["accept-encoding"] = accept_encoding + + if user_agent: + headers["user-agent"] = user_agent + + if keep_alive: + headers["connection"] = "keep-alive" + + if basic_auth: + headers["authorization"] = ( + f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" + ) + + if proxy_basic_auth: + headers["proxy-authorization"] = ( + f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" + ) + + if disable_cache: + headers["cache-control"] = "no-cache" + + return headers + + +def set_file_position( + body: typing.Any, pos: _TYPE_BODY_POSITION | None +) -> _TYPE_BODY_POSITION | None: + """ + If a position is provided, move file to that point. + Otherwise, we'll attempt to record a position for future use. + """ + if pos is not None: + rewind_body(body, pos) + elif getattr(body, "tell", None) is not None: + try: + pos = body.tell() + except OSError: + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body. + pos = _FAILEDTELL + + return pos + + +def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: + """ + Attempt to rewind body to a certain position. + Primarily used for request redirects and retries. + + :param body: + File-like object that supports seek. + + :param int pos: + Position to seek to in file. + """ + body_seek = getattr(body, "seek", None) + if body_seek is not None and isinstance(body_pos, int): + try: + body_seek(body_pos) + except OSError as e: + raise UnrewindableBodyError( + "An error occurred when rewinding request body for redirect/retry." + ) from e + elif body_pos is _FAILEDTELL: + raise UnrewindableBodyError( + "Unable to record file position for rewinding " + "request body during a redirect/retry." + ) + else: + raise ValueError( + f"body_pos must be of type integer, instead it was {type(body_pos)}." + ) + + +class ChunksAndContentLength(typing.NamedTuple): + chunks: typing.Iterable[bytes] | None + content_length: int | None + + +def body_to_chunks( + body: typing.Any | None, method: str, blocksize: int +) -> ChunksAndContentLength: + """Takes the HTTP request method, body, and blocksize and + transforms them into an iterable of chunks to pass to + socket.sendall() and an optional 'Content-Length' header. + + A 'Content-Length' of 'None' indicates the length of the body + can't be determined so should use 'Transfer-Encoding: chunked' + for framing instead. + """ + + chunks: typing.Iterable[bytes] | None + content_length: int | None + + # No body, we need to make a recommendation on 'Content-Length' + # based on whether that request method is expected to have + # a body or not. + if body is None: + chunks = None + if method.upper() not in _METHODS_NOT_EXPECTING_BODY: + content_length = 0 + else: + content_length = None + + # Bytes or strings become bytes + elif isinstance(body, (str, bytes)): + chunks = (to_bytes(body),) + content_length = len(chunks[0]) + + # File-like object, TODO: use seek() and tell() for length? + elif hasattr(body, "read"): + + def chunk_readable() -> typing.Iterable[bytes]: + encode = isinstance(body, io.TextIOBase) + while True: + datablock = body.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("utf-8") + yield datablock + + chunks = chunk_readable() + content_length = None + + # Otherwise we need to start checking via duck-typing. + else: + try: + # Check if the body implements the buffer API. + mv = memoryview(body) + except TypeError: + try: + # Check if the body is an iterable + chunks = iter(body) + content_length = None + except TypeError: + raise TypeError( + f"'body' must be a bytes-like object, file-like " + f"object, or iterable. Instead was {body!r}" + ) from None + else: + # Since it implements the buffer API can be passed directly to socket.sendall() + chunks = (body,) + content_length = mv.nbytes + + return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py new file mode 100644 index 0000000000..0f4578696f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import http.client as httplib +from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj: object) -> bool: + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check `isclosed()` first, in case Python3 doesn't set `closed`. + # GH Issue #928 + return obj.isclosed() # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check via the official file-like-object way. + return obj.closed # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None # type: ignore[attr-defined] + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers: httplib.HTTPMessage) -> None: + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param http.client.HTTPMessage headers: Headers to verify. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError(f"expected httplib.Message, got {type(headers)}.") + + unparsed_data = None + + # get_payload is actually email.message.Message.get_payload; + # we're only interested in the result if it's not a multipart message + if not headers.is_multipart(): + payload = headers.get_payload() + + if isinstance(payload, (bytes, str)): + unparsed_data = payload + + # httplib is assuming a response body is available + # when parsing headers even when httplib only sends + # header data to parse_headers() This results in + # defects on multipart responses in particular. + # See: https://github.com/urllib3/urllib3/issues/800 + + # So we ignore the following defects: + # - StartBoundaryNotFoundDefect: + # The claimed start boundary was never found. + # - MultipartInvariantViolationDefect: + # A message claimed to be a multipart but no subparts were found. + defects = [ + defect + for defect in headers.defects + if not isinstance( + defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) + ) + ] + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response: httplib.HTTPResponse) -> bool: + """ + Checks whether the request of a response has been a HEAD-request. + + :param http.client.HTTPResponse response: + Response to check if the originating request + used 'HEAD' as a method. + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method_str = response._method # type: str # type: ignore[attr-defined] + return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py new file mode 100644 index 0000000000..7649898e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py @@ -0,0 +1,557 @@ +from __future__ import annotations + +import email +import logging +import random +import re +import time +import typing +from itertools import takewhile +from types import TracebackType + +from ..exceptions import ( + ConnectTimeoutError, + InvalidHeader, + MaxRetryError, + ProtocolError, + ProxyError, + ReadTimeoutError, + ResponseError, +) +from .util import reraise + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from ..connectionpool import ConnectionPool + from ..response import BaseHTTPResponse + +log = logging.getLogger(__name__) + + +# Data structure for representing the metadata of requests that result in a retry. +class RequestHistory(typing.NamedTuple): + method: str | None + url: str | None + error: Exception | None + status: int | None + redirect_location: str | None + + +class Retry: + """Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool: + + .. code-block:: python + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request("GET", "https://example.com/") + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=Retry(10)) + + Retries can be disabled by passing ``False``: + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int status: + How many times to retry on bad status codes. + + These are retries made on responses, where status code matches + ``status_forcelist``. + + Set to ``0`` to fail on the first retry of this type. + + :param int other: + How many times to retry on other errors. + + Other errors are errors that are not connect, read, redirect or status errors. + These errors might be raised after the request was sent to the server, so the + request might have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + If ``total`` is not set, it's a good idea to set this to 0 to account + for unexpected edge cases and avoid infinite retry loops. + + :param Collection allowed_methods: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + idempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. + + Set to a ``None`` value to retry on any verb. + + :param Collection status_forcelist: + A set of integer HTTP status codes that we should force a retry on. + A retry is initiated if the request method is in ``allowed_methods`` + and the response status code is in ``status_forcelist``. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts after the second try + (most errors are resolved immediately by a second try without a + delay). urllib3 will sleep for:: + + {backoff factor} * (2 ** ({number of previous retries})) + + seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: + + random.uniform(0, {backoff jitter}) + + seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will + sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever + be longer than `backoff_max`. + + By default, backoff is disabled (factor set to 0). + + :param float backoff_max: + The maximum backoff time (in seconds) between retry attempts. + This value caps the computed backoff from `backoff_factor`. + + :param float backoff_jitter: + Random jitter amount (in seconds) added to the computed backoff. + Jitter is sampled uniformly from `0` to `backoff_jitter`. + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have + been exhausted. + + :param tuple history: The history of the request encountered during + each call to :meth:`~Retry.increment`. The list is in the order + the requests occurred. Each list item is of class :class:`RequestHistory`. + + :param bool respect_retry_after_header: + Whether to respect Retry-After header on status codes defined as + :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. + + :param Collection remove_headers_on_redirect: + Sequence of headers to remove from the request when a response + indicating a redirect is returned before firing off the redirected + request. + + :param int retry_after_max: Number of seconds to allow as the maximum for + Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. + Any Retry-After headers larger than this value will be limited to this + value. + """ + + #: Default methods to be used for ``allowed_methods`` + DEFAULT_ALLOWED_METHODS = frozenset( + ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) + + #: Default status codes to be used for ``status_forcelist`` + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Default headers to be used for ``remove_headers_on_redirect`` + DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( + ["Cookie", "Authorization", "Proxy-Authorization"] + ) + + #: Default maximum backoff time. + DEFAULT_BACKOFF_MAX = 120 + + # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. + #: Default maximum allowed value for Retry-After headers in seconds + DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 + + # Backward compatibility; assigned outside of the class. + DEFAULT: typing.ClassVar[Retry] + + def __init__( + self, + total: bool | int | None = 10, + connect: int | None = None, + read: int | None = None, + redirect: bool | int | None = None, + status: int | None = None, + other: int | None = None, + allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, + status_forcelist: typing.Collection[int] | None = None, + backoff_factor: float = 0, + backoff_max: float = DEFAULT_BACKOFF_MAX, + raise_on_redirect: bool = True, + raise_on_status: bool = True, + history: tuple[RequestHistory, ...] | None = None, + respect_retry_after_header: bool = True, + remove_headers_on_redirect: typing.Collection[ + str + ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, + backoff_jitter: float = 0.0, + retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, + ) -> None: + self.total = total + self.connect = connect + self.read = read + self.status = status + self.other = other + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.allowed_methods = allowed_methods + self.backoff_factor = backoff_factor + self.backoff_max = backoff_max + self.retry_after_max = retry_after_max + self.raise_on_redirect = raise_on_redirect + self.raise_on_status = raise_on_status + self.history = history or () + self.respect_retry_after_header = respect_retry_after_header + self.remove_headers_on_redirect = frozenset( + h.lower() for h in remove_headers_on_redirect + ) + self.backoff_jitter = backoff_jitter + + def new(self, **kw: typing.Any) -> Self: + params = dict( + total=self.total, + connect=self.connect, + read=self.read, + redirect=self.redirect, + status=self.status, + other=self.other, + allowed_methods=self.allowed_methods, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + backoff_max=self.backoff_max, + retry_after_max=self.retry_after_max, + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, + remove_headers_on_redirect=self.remove_headers_on_redirect, + respect_retry_after_header=self.respect_retry_after_header, + backoff_jitter=self.backoff_jitter, + ) + + params.update(kw) + return type(self)(**params) # type: ignore[arg-type] + + @classmethod + def from_int( + cls, + retries: Retry | bool | int | None, + redirect: bool | int | None = True, + default: Retry | bool | int | None = None, + ) -> Retry: + """Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r", retries, new_retries) + return new_retries + + def get_backoff_time(self) -> float: + """Formula for computing the current backoff + + :rtype: float + """ + # We want to consider only the last consecutive errors sequence (Ignore redirects). + consecutive_errors_len = len( + list( + takewhile(lambda x: x.redirect_location is None, reversed(self.history)) + ) + ) + if consecutive_errors_len <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) + if self.backoff_jitter != 0.0: + backoff_value += random.random() * self.backoff_jitter + return float(max(0, min(self.backoff_max, backoff_value))) + + def parse_retry_after(self, retry_after: str) -> float: + seconds: float + # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = int(retry_after) + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + seconds = max(seconds, 0) + + # Check the seconds do not exceed the specified maximum + if seconds > self.retry_after_max: + seconds = self.retry_after_max + + return seconds + + def get_retry_after(self, response: BaseHTTPResponse) -> float | None: + """Get the value of Retry-After in seconds.""" + + retry_after = response.headers.get("Retry-After") + + if retry_after is None: + return None + + return self.parse_retry_after(retry_after) + + def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: + retry_after = self.get_retry_after(response) + if retry_after: + time.sleep(retry_after) + return True + + return False + + def _sleep_backoff(self) -> None: + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def sleep(self, response: BaseHTTPResponse | None = None) -> None: + """Sleep between retry attempts. + + This method will respect a server's ``Retry-After`` response header + and sleep the duration of the time requested. If that is not present, it + will use an exponential backoff. By default, the backoff factor is 0 and + this method will return immediately. + """ + + if self.respect_retry_after_header and response: + slept = self.sleep_for_retry(response) + if slept: + return + + self._sleep_backoff() + + def _is_connection_error(self, err: Exception) -> bool: + """Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + if isinstance(err, ProxyError): + err = err.original_error + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err: Exception) -> bool: + """Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def _is_method_retryable(self, method: str) -> bool: + """Checks if a given HTTP method should be retried upon, depending if + it is included in the allowed_methods + """ + if self.allowed_methods and method.upper() not in self.allowed_methods: + return False + return True + + def is_retry( + self, method: str, status_code: int, has_retry_after: bool = False + ) -> bool: + """Is this method/status code retryable? (Based on allowlists and control + variables such as the number of total retries to allow, whether to + respect the Retry-After header, whether this header is present, and + whether the returned status code is on the list of status codes to + be retried upon on the presence of the aforementioned header) + """ + if not self._is_method_retryable(method): + return False + + if self.status_forcelist and status_code in self.status_forcelist: + return True + + return bool( + self.total + and self.respect_retry_after_header + and has_retry_after + and (status_code in self.RETRY_AFTER_STATUS_CODES) + ) + + def is_exhausted(self) -> bool: + """Are we out of retries?""" + retry_counts = [ + x + for x in ( + self.total, + self.connect, + self.read, + self.redirect, + self.status, + self.other, + ) + if x + ] + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment( + self, + method: str | None = None, + url: str | None = None, + response: BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, + ) -> Self: + """Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.BaseHTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + connect = self.connect + read = self.read + redirect = self.redirect + status_count = self.status + other = self.other + cause = "unknown" + status = None + redirect_location = None + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False or method is None or not self._is_method_retryable(method): + raise reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + + elif error: + # Other retry? + if other is not None: + other -= 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = "too many redirects" + response_redirect_location = response.get_redirect_location() + if response_redirect_location: + redirect_location = response_redirect_location + status = response.status + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and the given method is in the allowed_methods + cause = ResponseError.GENERIC_ERROR + if response and response.status: + if status_count is not None: + status_count -= 1 + cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) + status = response.status + + history = self.history + ( + RequestHistory(method, url, error, status, redirect_location), + ) + + new_retry = self.new( + total=total, + connect=connect, + read=read, + redirect=redirect, + status=status_count, + other=other, + history=history, + ) + + if new_retry.is_exhausted(): + reason = error or ResponseError(cause) + raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] + + log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) + + return new_retry + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(total={self.total}, connect={self.connect}, " + f"read={self.read}, redirect={self.redirect}, status={self.status})" + ) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py new file mode 100644 index 0000000000..e66549a76c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +import hashlib +import hmac +import os +import socket +import sys +import typing +import warnings +from binascii import unhexlify + +from ..exceptions import ProxySchemeUnsupported, SSLError +from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE + +SSLContext = None +SSLTransport = None +HAS_NEVER_CHECK_COMMON_NAME = False +IS_PYOPENSSL = False +ALPN_PROTOCOLS = ["http/1.1"] + +_TYPE_VERSION_INFO = tuple[int, int, int, str, int] + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = { + length: getattr(hashlib, algorithm, None) + for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) +} + + +def _is_has_never_check_common_name_reliable( + openssl_version: str, +) -> bool: + # As of May 2023, all released versions of LibreSSL fail to reject certificates with + # only common names, see https://github.com/urllib3/urllib3/pull/3024 + is_openssl = openssl_version.startswith("OpenSSL ") + + return is_openssl + + +if typing.TYPE_CHECKING: + from ssl import VerifyMode + from typing import TypedDict + + from .ssltransport import SSLTransport as SSLTransportType + + class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): + subjectAltName: tuple[tuple[str, str], ...] + subject: tuple[tuple[tuple[str, str], ...], ...] + serialNumber: str + + +# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' +_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} + +try: # Do we have ssl at all? + import ssl + from ssl import ( # type: ignore[assignment] + CERT_REQUIRED, + HAS_NEVER_CHECK_COMMON_NAME, + OP_NO_COMPRESSION, + OP_NO_TICKET, + OPENSSL_VERSION, + PROTOCOL_TLS, + PROTOCOL_TLS_CLIENT, + VERIFY_X509_PARTIAL_CHAIN, + VERIFY_X509_STRICT, + OP_NO_SSLv2, + OP_NO_SSLv3, + SSLContext, + TLSVersion, + ) + + PROTOCOL_SSLv23 = PROTOCOL_TLS + + # Setting SSLContext.hostname_checks_common_name = False didn't work with + # LibreSSL, check details in the used function. + if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( + OPENSSL_VERSION, + ): # Defensive: + HAS_NEVER_CHECK_COMMON_NAME = False + + # Need to be careful here in case old TLS versions get + # removed in future 'ssl' module implementations. + for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): + try: + _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( + TLSVersion, attr + ) + except AttributeError: # Defensive: + continue + + from .ssltransport import SSLTransport # type: ignore[assignment] +except ImportError: + OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] + OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] + OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] + OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] + PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] + PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] + VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] + VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] + + +_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] + + +def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + if cert is None: + raise SSLError("No certificate for the peer.") + + fingerprint = fingerprint.replace(":", "").lower() + digest_length = len(fingerprint) + if digest_length not in HASHFUNC_MAP: + raise SSLError(f"Fingerprint of invalid length: {fingerprint}") + hashfunc = HASHFUNC_MAP.get(digest_length) + if hashfunc is None: + raise SSLError( + f"Hash function implementation unavailable for fingerprint length: {digest_length}" + ) + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not hmac.compare_digest(cert_digest, fingerprint_bytes): + raise SSLError( + f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' + ) + + +def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_REQUIRED`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbreviation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_REQUIRED + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "CERT_" + candidate) + return res # type: ignore[no-any-return] + + return candidate # type: ignore[return-value] + + +def resolve_ssl_version(candidate: None | int | str) -> int: + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_TLS + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "PROTOCOL_" + candidate) + return typing.cast(int, res) + + return candidate + + +def create_urllib3_context( + ssl_version: int | None = None, + cert_reqs: int | None = None, + options: int | None = None, + ciphers: str | None = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + verify_flags: int | None = None, +) -> ssl.SSLContext: + """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + + This parameter is deprecated instead use 'ssl_minimum_version'. + :param ssl_minimum_version: + The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + :param ssl_maximum_version: + The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the + default value. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. + :param ciphers: + Which cipher suites to allow the server to select. Defaults to either system configured + ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. + :param verify_flags: + The flags for certificate verification operations. These default to + ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + if SSLContext is None: + raise TypeError("Can't create an SSLContext object without an ssl module") + + # This means 'ssl_version' was specified as an exact value. + if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): + # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' + # to avoid conflicts. + if ssl_minimum_version is not None or ssl_maximum_version is not None: + raise ValueError( + "Can't specify both 'ssl_version' and either " + "'ssl_minimum_version' or 'ssl_maximum_version'" + ) + + # 'ssl_version' is deprecated and will be removed in the future. + else: + # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. + ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MINIMUM_SUPPORTED + ) + ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MAXIMUM_SUPPORTED + ) + + # This warning message is pushing users to use 'ssl_minimum_version' + # instead of both min/max. Best practice is to only set the minimum version and + # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' + warnings.warn( + "'ssl_version' option is deprecated and will be " + "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", + category=FutureWarning, + stacklevel=2, + ) + + context = SSLContext(PROTOCOL_TLS_CLIENT) + if ssl_minimum_version is not None: + context.minimum_version = ssl_minimum_version + else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here + context.minimum_version = TLSVersion.TLSv1_2 + + if ssl_maximum_version is not None: + context.maximum_version = ssl_maximum_version + + # Unless we're given ciphers defer to either system ciphers in + # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. + if ciphers: + context.set_ciphers(ciphers) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + # TLSv1.2 only. Unless set explicitly, do not request tickets. + # This may save some bandwidth on wire, and although the ticket is encrypted, + # there is a risk associated with it being on wire, + # if the server is not rotating its ticketing keys properly. + options |= OP_NO_TICKET + + context.options |= options + + if verify_flags is None: + verify_flags = 0 + # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN + # and VERIFY_X509_STRICT so we do the same + if sys.version_info >= (3, 13): + verify_flags |= VERIFY_X509_PARTIAL_CHAIN + verify_flags |= VERIFY_X509_STRICT + + context.verify_flags |= verify_flags + + # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is + # necessary for conditional client cert authentication with TLS 1.3. + # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using + # an SSLContext created by pyOpenSSL. + if getattr(context, "post_handshake_auth", None) is not None: + context.post_handshake_auth = True + + # The order of the below lines setting verify_mode and check_hostname + # matter due to safe-guards SSLContext has to prevent an SSLContext with + # check_hostname=True, verify_mode=NONE/OPTIONAL. + # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own + # 'ssl.match_hostname()' implementation. + if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: + context.verify_mode = cert_reqs + context.check_hostname = True + else: + context.check_hostname = False + context.verify_mode = cert_reqs + + context.hostname_checks_common_name = False + + if "SSLKEYLOGFILE" in os.environ: + sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) + else: + sslkeylogfile = None + if sslkeylogfile: + context.keylog_filename = sslkeylogfile + + return context + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: typing.Literal[False] = ..., +) -> ssl.SSLSocket: ... + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: bool = ..., +) -> ssl.SSLSocket | SSLTransportType: ... + + +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = None, + certfile: str | None = None, + cert_reqs: int | None = None, + ca_certs: str | None = None, + server_hostname: str | None = None, + ssl_version: int | None = None, + ciphers: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_cert_dir: str | None = None, + key_password: str | None = None, + ca_cert_data: None | str | bytes = None, + tls_in_tls: bool = False, +) -> ssl.SSLSocket | SSLTransportType: + """ + All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and + ca_cert_dir have the same meaning as they do when using + :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, + :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + :param key_password: + Optional password if the keyfile is encrypted. + :param ca_cert_data: + Optional string containing CA certificates in PEM format suitable for + passing as the cadata parameter to SSLContext.load_verify_locations() + :param tls_in_tls: + Use SSLTransport to wrap the existing socket. + """ + context = ssl_context + if context is None: + # Note: This branch of code and all the variables in it are only used in tests. + # We should consider deprecating and removing this code. + context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) + + if ca_certs or ca_cert_dir or ca_cert_data: + try: + context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) + except OSError as e: + raise SSLError(e) from e + + elif ssl_context is None and hasattr(context, "load_default_certs"): + # try to load OS default certs; works well on Windows. + context.load_default_certs() + + # Attempt to detect if we get the goofy behavior of the + # keyfile being encrypted and OpenSSL asking for the + # passphrase via the terminal and instead error out. + if keyfile and key_password is None and _is_key_file_encrypted(keyfile): + raise SSLError("Client private key is encrypted, password is required") + + if certfile: + if key_password is None: + context.load_cert_chain(certfile, keyfile) + else: + context.load_cert_chain(certfile, keyfile, key_password) + + context.set_alpn_protocols(ALPN_PROTOCOLS) + + ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) + return ssl_sock + + +def is_ipaddress(hostname: str | bytes) -> bool: + """Detects whether the hostname given is an IPv4 or IPv6 address. + Also detects IPv6 addresses with Zone IDs. + + :param str hostname: Hostname to examine. + :return: True if the hostname is an IP address, False otherwise. + """ + if isinstance(hostname, bytes): + # IDN A-label bytes are ASCII compatible. + hostname = hostname.decode("ascii") + return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) + + +def _is_key_file_encrypted(key_file: str) -> bool: + """Detects if a key file is encrypted or not.""" + with open(key_file) as f: + for line in f: + # Look for Proc-Type: 4,ENCRYPTED + if "ENCRYPTED" in line: + return True + + return False + + +def _ssl_wrap_socket_impl( + sock: socket.socket, + ssl_context: ssl.SSLContext, + tls_in_tls: bool, + server_hostname: str | None = None, +) -> ssl.SSLSocket | SSLTransportType: + if tls_in_tls: + if not SSLTransport: + # Import error, ssl is not available. + raise ProxySchemeUnsupported( + "TLS in TLS requires support for the 'ssl' module" + ) + + SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) + return SSLTransport(sock, ssl_context, server_hostname) + + return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py new file mode 100644 index 0000000000..94994f25ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py @@ -0,0 +1,153 @@ +"""The match_hostname() function from Python 3.5, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html +# It is modified to remove commonName support. + +from __future__ import annotations + +import ipaddress +import re +import typing +from ipaddress import IPv4Address, IPv6Address + +if typing.TYPE_CHECKING: + from .ssl_ import _TYPE_PEER_CERT_RET_DICT + +__version__ = "3.5.0.1" + + +class CertificateError(ValueError): + pass + + +def _dnsname_match( + dn: typing.Any, hostname: str, max_wildcards: int = 1 +) -> typing.Match[str] | None | bool: + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r".") + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count("*") + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn) + ) + + # speed up common case w/o wildcards + if not wildcards: + return bool(dn.lower() == hostname.lower()) + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == "*": + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append("[^.]+") + elif leftmost.startswith("xn--") or hostname.startswith("xn--"): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) + return pat.match(hostname) + + +def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: + """Exact matching of IP addresses. + + RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded + bytes of the IP address. An IP version 4 address is 4 octets, and an IP + version 6 address is 16 octets. [...] A reference identity of type IP-ID + matches if the address is identical to an iPAddress value of the + subjectAltName extension of the certificate." + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(ipname.rstrip()) + return bool(ip.packed == host_ip.packed) + + +def match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError( + "empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED" + ) + + try: + host_ip = ipaddress.ip_address(hostname) + except ValueError: + # Not an IP address (common case) + host_ip = None + dnsnames = [] + san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) + key: str + value: str + for key, value in san: + if key == "DNS": + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == "IP Address": + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + + # We only check 'commonName' if it's enabled and we're not verifying + # an IP address. IP addresses aren't valid within 'commonName'. + if hostname_checks_common_name and host_ip is None and not dnsnames: + for sub in cert.get("subject", ()): + for key, value in sub: + if key == "commonName": + if _dnsname_match(value, hostname): + return + dnsnames.append( + value + ) # Defensive: for older PyPy and OpenSSL versions + + if len(dnsnames) > 1: + raise CertificateError( + "hostname %r " + "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) + ) + elif len(dnsnames) == 1: + raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") + else: + raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py new file mode 100644 index 0000000000..6d59bc3bce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +import io +import socket +import ssl +import typing + +from ..exceptions import ProxySchemeUnsupported + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT + + +_WriteBuffer = typing.Union[bytearray, memoryview] +_ReturnValue = typing.TypeVar("_ReturnValue") + +SSL_BLOCKSIZE = 16384 + + +class SSLTransport: + """ + The SSLTransport wraps an existing socket and establishes an SSL connection. + + Contrary to Python's implementation of SSLSocket, it allows you to chain + multiple TLS connections together. It's particularly useful if you need to + implement TLS within TLS. + + The class supports most of the socket API operations. + """ + + @staticmethod + def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: + """ + Raises a ProxySchemeUnsupported if the provided ssl_context can't be used + for TLS in TLS. + + The only requirement is that the ssl_context provides the 'wrap_bio' + methods. + """ + + if not hasattr(ssl_context, "wrap_bio"): + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "available on non-native SSLContext" + ) + + def __init__( + self, + socket: socket.socket, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + suppress_ragged_eofs: bool = True, + ) -> None: + """ + Create an SSLTransport around socket using the provided ssl_context. + """ + self.incoming = ssl.MemoryBIO() + self.outgoing = ssl.MemoryBIO() + + self.suppress_ragged_eofs = suppress_ragged_eofs + self.socket = socket + + self.sslobj = ssl_context.wrap_bio( + self.incoming, self.outgoing, server_hostname=server_hostname + ) + + # Perform initial handshake. + self._ssl_io_loop(self.sslobj.do_handshake) + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: typing.Any) -> None: + self.close() + + def fileno(self) -> int: + return self.socket.fileno() + + def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: + return self._wrap_ssl_read(len, buffer) + + def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv") + return self._wrap_ssl_read(buflen) + + def recv_into( + self, + buffer: _WriteBuffer, + nbytes: int | None = None, + flags: int = 0, + ) -> None | int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv_into") + if nbytes is None: + nbytes = len(buffer) + return self.read(nbytes, buffer) + + def sendall(self, data: bytes, flags: int = 0) -> None: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall") + count = 0 + with memoryview(data) as view, view.cast("B") as byte_view: + amount = len(byte_view) + while count < amount: + v = self.send(byte_view[count:]) + count += v + + def send(self, data: bytes, flags: int = 0) -> int: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send") + return self._ssl_io_loop(self.sslobj.write, data) + + def makefile( + self, + mode: str, + buffering: int | None = None, + *, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: + """ + Python's httpclient uses makefile and buffered io when reading HTTP + messages and we need to support it. + + This is unfortunately a copy and paste of socket.py makefile with small + changes to point to the socket directly. + """ + if not set(mode) <= {"r", "w", "b"}: + raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") + + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] + self.socket._io_refs += 1 # type: ignore[attr-defined] + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + buffer: typing.BinaryIO + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode # type: ignore[misc] + return text + + def unwrap(self) -> None: + self._ssl_io_loop(self.sslobj.unwrap) + + def close(self) -> None: + self.socket.close() + + @typing.overload + def getpeercert( + self, binary_form: typing.Literal[False] = ... + ) -> _TYPE_PEER_CERT_RET_DICT | None: ... + + @typing.overload + def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... + + def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: + return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] + + def version(self) -> str | None: + return self.sslobj.version() + + def cipher(self) -> tuple[str, str, int] | None: + return self.sslobj.cipher() + + def selected_alpn_protocol(self) -> str | None: + return self.sslobj.selected_alpn_protocol() + + def shared_ciphers(self) -> list[tuple[str, str, int]] | None: + return self.sslobj.shared_ciphers() + + def compression(self) -> str | None: + return self.sslobj.compression() + + def settimeout(self, value: float | None) -> None: + self.socket.settimeout(value) + + def gettimeout(self) -> float | None: + return self.socket.gettimeout() + + def _decref_socketios(self) -> None: + self.socket._decref_socketios() # type: ignore[attr-defined] + + def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: + try: + return self._ssl_io_loop(self.sslobj.read, len, buffer) + except ssl.SSLError as e: + if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: + return 0 # eof, return 0. + else: + raise + + # func is sslobj.do_handshake or sslobj.unwrap + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... + + # func is sslobj.write, arg1 is data + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... + + # func is sslobj.read, arg1 is len, arg2 is buffer + @typing.overload + def _ssl_io_loop( + self, + func: typing.Callable[[int, bytearray | None], bytes], + arg1: int, + arg2: bytearray | None, + ) -> bytes: ... + + def _ssl_io_loop( + self, + func: typing.Callable[..., _ReturnValue], + arg1: None | bytes | int = None, + arg2: bytearray | None = None, + ) -> _ReturnValue: + """Performs an I/O loop between incoming/outgoing and the socket.""" + should_loop = True + ret = None + + while should_loop: + errno = None + try: + if arg1 is None and arg2 is None: + ret = func() + elif arg2 is None: + ret = func(arg1) + else: + ret = func(arg1, arg2) + except ssl.SSLError as e: + if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): + # WANT_READ, and WANT_WRITE are expected, others are not. + raise e + errno = e.errno + + buf = self.outgoing.read() + self.socket.sendall(buf) + + if errno is None: + should_loop = False + elif errno == ssl.SSL_ERROR_WANT_READ: + buf = self.socket.recv(SSL_BLOCKSIZE) + if buf: + self.incoming.write(buf) + else: + self.incoming.write_eof() + return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py new file mode 100644 index 0000000000..4bb1be11d9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import time +import typing +from enum import Enum +from socket import getdefaulttimeout + +from ..exceptions import TimeoutStateError + +if typing.TYPE_CHECKING: + from typing import Final + + +class _TYPE_DEFAULT(Enum): + # This value should never be passed to socket.settimeout() so for safety we use a -1. + # socket.settimout() raises a ValueError for negative values. + token = -1 + + +_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token + +_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] + + +class Timeout: + """Timeout configuration. + + Timeouts can be defined as a default for a pool: + + .. code-block:: python + + import urllib3 + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + + http = urllib3.PoolManager(timeout=timeout) + + resp = http.request("GET", "https://example.com/") + + print(resp.status) + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``: + + .. code-block:: python + + no_timeout = Timeout(connect=None, read=None) + response = http.request("GET", "https://example.com/", timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: int, float, or None + + :param connect: + The maximum amount of time (in seconds) to wait for a connection + attempt to a server to succeed. Omitting the parameter will default the + connect timeout to the system default, probably `the global default + timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: int, float, or None + + :param read: + The maximum amount of time (in seconds) to wait between consecutive + read operations for a response from the server. Omitting the parameter + will default the read timeout to the system default, probably `the + global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: int, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT + + def __init__( + self, + total: _TYPE_TIMEOUT = None, + connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + ) -> None: + self._connect = self._validate_timeout(connect, "connect") + self._read = self._validate_timeout(read, "read") + self.total = self._validate_timeout(total, "total") + self._start_connect: float | None = None + + def __repr__(self) -> str: + return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" + + # __str__ provided for backwards compatibility + __str__ = __repr__ + + @staticmethod + def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: + return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout + + @classmethod + def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: + """Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If it is a numeric value less than or equal to + zero, or the type is not an integer, float, or None. + """ + if value is None or value is _DEFAULT_TIMEOUT: + return value + + if isinstance(value, bool): + raise ValueError( + "Timeout cannot be a boolean value. It must " + "be an int, float or None." + ) + try: + float(value) + except (TypeError, ValueError): + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + try: + if value <= 0: + raise ValueError( + "Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value) + ) + except TypeError: + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + return value + + @classmethod + def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: + """Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self) -> Timeout: + """Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, total=self.total) + + def start_connect(self) -> float: + """Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = time.monotonic() + return self._start_connect + + def get_connect_duration(self) -> float: + """Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time in seconds. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError( + "Can't get connect duration for timer that has not started." + ) + return time.monotonic() - self._start_connect + + @property + def connect_timeout(self) -> _TYPE_TIMEOUT: + """Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is _DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) # type: ignore[type-var] + + @property + def read_timeout(self) -> float | None: + """Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if ( + self.total is not None + and self.total is not _DEFAULT_TIMEOUT + and self._read is not None + and self._read is not _DEFAULT_TIMEOUT + ): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), self._read)) + elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py new file mode 100644 index 0000000000..db057f17be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import re +import typing + +from ..exceptions import LocationParseError +from .util import to_str + +# We only want to normalize urls with an HTTP(S) scheme. +# urllib3 infers URLs without a scheme (None) to be http. +_NORMALIZABLE_SCHEMES = ("http", "https", None) + +# Almost all of these patterns were derived from the +# 'rfc3986' module: https://github.com/python-hyper/rfc3986 +_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") +_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") +_URI_RE = re.compile( + r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" + r"(?://([^\\/?#]*))?" + r"([^?#]*)" + r"(?:\?([^#]*))?" + r"(?:#(.*))?$", + re.UNICODE | re.DOTALL, +) + +_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" +_HEX_PAT = "[0-9A-Fa-f]{1,4}" +_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) +_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} +_variations = [ + # 6( h16 ":" ) ls32 + "(?:%(hex)s:){6}%(ls32)s", + # "::" 5( h16 ":" ) ls32 + "::(?:%(hex)s:){5}%(ls32)s", + # [ h16 ] "::" 4( h16 ":" ) ls32 + "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", + # [ *4( h16 ":" ) h16 ] "::" ls32 + "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", + # [ *5( h16 ":" ) h16 ] "::" h16 + "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", + # [ *6( h16 ":" ) h16 ] "::" + "(?:(?:%(hex)s:){0,6}%(hex)s)?::", +] + +_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" +_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" +_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" +_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" +_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" +_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") + +_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") +_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") +_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") +_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") +_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") + +_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( + _REG_NAME_PAT, + _IPV4_PAT, + _IPV6_ADDRZ_PAT, +) +_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) + +_UNRESERVED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" +) +_SUB_DELIM_CHARS = set("!$&'()*+,;=") +_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} +_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} +_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} + + +class Url( + typing.NamedTuple( + "Url", + [ + ("scheme", typing.Optional[str]), + ("auth", typing.Optional[str]), + ("host", typing.Optional[str]), + ("port", typing.Optional[int]), + ("path", typing.Optional[str]), + ("query", typing.Optional[str]), + ("fragment", typing.Optional[str]), + ], + ) +): + """ + Data structure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. Both the scheme and host are normalized as they are + both case-insensitive according to RFC 3986. + """ + + def __new__( # type: ignore[no-untyped-def] + cls, + scheme: str | None = None, + auth: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ): + if path and not path.startswith("/"): + path = "/" + path + if scheme is not None: + scheme = scheme.lower() + return super().__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self) -> str | None: + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self) -> str: + """Absolute path including the query string.""" + uri = self.path or "/" + + if self.query is not None: + uri += "?" + self.query + + return uri + + @property + def authority(self) -> str | None: + """ + Authority component as defined in RFC 3986 3.2. + This includes userinfo (auth), host and port. + + i.e. + userinfo@host:port + """ + userinfo = self.auth + netloc = self.netloc + if netloc is None or userinfo is None: + return netloc + else: + return f"{userinfo}@{netloc}" + + @property + def netloc(self) -> str | None: + """ + Network location including host and port. + + If you need the equivalent of urllib.parse's ``netloc``, + use the ``authority`` property instead. + """ + if self.host is None: + return None + if self.port: + return f"{self.host}:{self.port}" + return self.host + + @property + def url(self) -> str: + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: + + .. code-block:: python + + import urllib3 + + U = urllib3.util.parse_url("https://google.com/mail/") + + print(U.url) + # "https://google.com/mail/" + + print( urllib3.util.Url("https", "username:password", + "host.com", 80, "/path", "query", "fragment" + ).url + ) + # "https://username:password@host.com:80/path?query#fragment" + """ + scheme, auth, host, port, path, query, fragment = self + url = "" + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + "://" + if auth is not None: + url += auth + "@" + if host is not None: + url += host + if port is not None: + url += ":" + str(port) + if path is not None: + url += path + if query is not None: + url += "?" + query + if fragment is not None: + url += "#" + fragment + + return url + + def __str__(self) -> str: + return self.url + + +@typing.overload +def _encode_invalid_chars( + component: str, allowed_chars: typing.Container[str] +) -> str: # Abstract + ... + + +@typing.overload +def _encode_invalid_chars( + component: None, allowed_chars: typing.Container[str] +) -> None: # Abstract + ... + + +def _encode_invalid_chars( + component: str | None, allowed_chars: typing.Container[str] +) -> str | None: + """Percent-encodes a URI component without reapplying + onto an already percent-encoded component. + """ + if component is None: + return component + + component = to_str(component) + + # Normalize existing percent-encoded bytes. + # Try to see if the component we're encoding is already percent-encoded + # so we can skip all '%' characters but still encode all others. + component, percent_encodings = _PERCENT_RE.subn( + lambda match: match.group(0).upper(), component + ) + + uri_bytes = component.encode("utf-8", "surrogatepass") + is_percent_encoded = percent_encodings == uri_bytes.count(b"%") + encoded_component = bytearray() + + for i in range(0, len(uri_bytes)): + # Will return a single character bytestring + byte = uri_bytes[i : i + 1] + byte_ord = ord(byte) + if (is_percent_encoded and byte == b"%") or ( + byte_ord < 128 and byte.decode() in allowed_chars + ): + encoded_component += byte + continue + encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) + + return encoded_component.decode() + + +def _remove_path_dot_segments(path: str) -> str: + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = path.split("/") # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == ".": + continue + # Anything other than '..', should be appended to the output + if segment != "..": + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if path.startswith("/") and (not output or output[0]): + output.insert(0, "") + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if path.endswith(("/.", "/..")): + output.append("") + + return "/".join(output) + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + if host: + if scheme in _NORMALIZABLE_SCHEMES: + is_ipv6 = _IPV6_ADDRZ_RE.match(host) + if is_ipv6: + # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as + # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID + # separator as necessary to return a valid RFC 4007 scoped IP. + match = _ZONE_ID_RE.search(host) + if match: + start, end = match.span(1) + zone_id = host[start:end] + + if zone_id.startswith("%25") and zone_id != "%25": + zone_id = zone_id[3:] + else: + zone_id = zone_id[1:] + zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) + return f"{host[:start].lower()}%{zone_id}{host[end:]}" + else: + return host.lower() + elif not _IPV4_RE.match(host): + return to_str( + b".".join([_idna_encode(label) for label in host.split(".")]), + "ascii", + ) + return host + + +def _idna_encode(name: str) -> bytes: + if not name.isascii(): + try: + import idna + except ImportError: + raise LocationParseError( + "Unable to parse URL without the 'idna' module" + ) from None + + try: + return idna.encode(name.lower(), strict=True, std3_rules=True) + except idna.IDNAError: + raise LocationParseError( + f"Name '{name}' is not a valid IDNA label" + ) from None + + return name.lower().encode("ascii") + + +def _encode_target(target: str) -> str: + """Percent-encodes a request target so that there are no invalid characters + + Pre-condition for this function is that 'target' must start with '/'. + If that is the case then _TARGET_RE will always produce a match. + """ + match = _TARGET_RE.match(target) + if not match: # Defensive: + raise LocationParseError(f"{target!r} is not a valid request URI") + + path, query = match.groups() + encoded_target = _encode_invalid_chars(path, _PATH_CHARS) + if query is not None: + query = _encode_invalid_chars(query, _QUERY_CHARS) + encoded_target += "?" + query + return encoded_target + + +def parse_url(url: str) -> Url: + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + This parser is RFC 3986 and RFC 6874 compliant. + + The parser logic and helper functions are based heavily on + work done in the ``rfc3986`` module. + + :param str url: URL to parse into a :class:`.Url` namedtuple. + + Partly backwards-compatible with :mod:`urllib.parse`. + + Example: + + .. code-block:: python + + import urllib3 + + print( urllib3.util.parse_url('http://google.com/mail/')) + # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + + print( urllib3.util.parse_url('google.com:80')) + # Url(scheme=None, host='google.com', port=80, path=None, ...) + + print( urllib3.util.parse_url('/foo?bar')) + # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + if not url: + # Empty + return Url() + + source_url = url + if not _SCHEME_RE.search(url): + url = "//" + url + + scheme: str | None + authority: str | None + auth: str | None + host: str | None + port: str | None + port_int: int | None + path: str | None + query: str | None + fragment: str | None + + try: + scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] + normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES + + if scheme: + scheme = scheme.lower() + + if authority: + auth, _, host_port = authority.rpartition("@") + auth = auth or None + host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] + if auth and normalize_uri: + auth = _encode_invalid_chars(auth, _USERINFO_CHARS) + if port == "": + port = None + else: + auth, host, port = None, None, None + + if port is not None: + port_int = int(port) + if not (0 <= port_int <= 65535): + raise LocationParseError(url) + else: + port_int = None + + host = _normalize_host(host, scheme) + + if normalize_uri and path: + path = _remove_path_dot_segments(path) + path = _encode_invalid_chars(path, _PATH_CHARS) + if normalize_uri and query: + query = _encode_invalid_chars(query, _QUERY_CHARS) + if normalize_uri and fragment: + fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) + + except (ValueError, AttributeError) as e: + raise LocationParseError(source_url) from e + + # For the sake of backwards compatibility we put empty + # string values for path if there are any defined values + # beyond the path in the URL. + # TODO: Remove this when we break backwards compatibility. + if not path: + if query is not None or fragment is not None: + path = "" + else: + path = None + + return Url( + scheme=scheme, + auth=auth, + host=host, + port=port_int, + path=path, + query=query, + fragment=fragment, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py new file mode 100644 index 0000000000..35c77e4025 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import typing +from types import TracebackType + + +def to_bytes( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> bytes: + if isinstance(x, bytes): + return x + elif not isinstance(x, str): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.encode(encoding or "utf-8", errors=errors or "strict") + return x.encode() + + +def to_str( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> str: + if isinstance(x, str): + return x + elif not isinstance(x, bytes): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.decode(encoding or "utf-8", errors=errors or "strict") + return x.decode() + + +def reraise( + tp: type[BaseException] | None, + value: BaseException, + tb: TracebackType | None = None, +) -> typing.NoReturn: + try: + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None # type: ignore[assignment] + tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py new file mode 100644 index 0000000000..aeca0c7ad5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import select +import socket +from functools import partial + +__all__ = ["wait_for_read", "wait_for_write"] + + +# How should we wait on sockets? +# +# There are two types of APIs you can use for waiting on sockets: the fancy +# modern stateful APIs like epoll/kqueue, and the older stateless APIs like +# select/poll. The stateful APIs are more efficient when you have a lots of +# sockets to keep track of, because you can set them up once and then use them +# lots of times. But we only ever want to wait on a single socket at a time +# and don't want to keep track of state, so the stateless APIs are actually +# more efficient. So we want to use select() or poll(). +# +# Now, how do we choose between select() and poll()? On traditional Unixes, +# select() has a strange calling convention that makes it slow, or fail +# altogether, for high-numbered file descriptors. The point of poll() is to fix +# that, so on Unixes, we prefer poll(). +# +# On Windows, there is no poll() (or at least Python doesn't provide a wrapper +# for it), but that's OK, because on Windows, select() doesn't have this +# strange calling convention; plain select() works fine. +# +# So: on Windows we use select(), and everywhere else we use poll(). We also +# fall back to select() in case poll() is somehow broken or missing. + + +def select_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + rcheck = [] + wcheck = [] + if read: + rcheck.append(sock) + if write: + wcheck.append(sock) + # When doing a non-blocking connect, most systems signal success by + # marking the socket writable. Windows, though, signals success by marked + # it as "exceptional". We paper over the difference by checking the write + # sockets for both conditions. (The stdlib selectors module does the same + # thing.) + fn = partial(select.select, rcheck, wcheck, wcheck) + rready, wready, xready = fn(timeout) + return bool(rready or wready or xready) + + +def poll_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + mask = 0 + if read: + mask |= select.POLLIN + if write: + mask |= select.POLLOUT + poll_obj = select.poll() + poll_obj.register(sock, mask) + + # For some reason, poll() takes timeout in milliseconds + def do_poll(t: float | None) -> list[tuple[int, int]]: + if t is not None: + t *= 1000 + return poll_obj.poll(t) + + return bool(do_poll(timeout)) + + +def _have_working_poll() -> bool: + # Apparently some systems have a select.poll that fails as soon as you try + # to use it, either due to strange configuration or broken monkeypatching + # from libraries like eventlet/greenlet. + try: + poll_obj = select.poll() + poll_obj.poll(0) + except (AttributeError, OSError): + return False + else: + return True + + +def wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + # We delay choosing which implementation to use until the first time we're + # called. We could do it at import time, but then we might make the wrong + # decision if someone goes wild with monkeypatching select.poll after + # we're imported. + global wait_for_socket + if _have_working_poll(): + wait_for_socket = poll_wait_for_socket + elif hasattr(select, "select"): + wait_for_socket = select_wait_for_socket + return wait_for_socket(sock, read, write, timeout) + + +def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for reading to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, read=True, timeout=timeout) + + +def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for writing to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.lock b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER new file mode 100644 index 0000000000..5c69047b2e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA new file mode 100644 index 0000000000..0d4f101491 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.6.17 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/latest/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD new file mode 100644 index 0000000000..13b0ce7da4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD @@ -0,0 +1,12 @@ +certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 +certifi-2026.6.17.dist-info/RECORD,, +certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL new file mode 100644 index 0000000000..14a883f292 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE new file mode 100644 index 0000000000..62b076cdee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt new file mode 100644 index 0000000000..963eac530b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py new file mode 100644 index 0000000000..ed9a74b7a0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py new file mode 100644 index 0000000000..8945b5da85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem new file mode 100644 index 0000000000..1c2dbfeb68 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem @@ -0,0 +1,3863 @@ + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Label: "OISTE Server Root ECC G1" +# Serial: 47819833811561661340092227008453318557 +# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 +# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 +# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Label: "OISTE Server Root RSA G1" +# Serial: 113845518112613905024960613408179309848 +# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 +# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e +# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Label: "e-Szigno TLS Root CA 2023" +# Serial: 71934828665710877219916191754 +# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 +# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe +# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py new file mode 100644 index 0000000000..1c9661cc7c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/index.py new file mode 100644 index 0000000000..502a6ae3ee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/index.py @@ -0,0 +1,15 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[AwsLambdaIntegration()], +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py new file mode 100644 index 0000000000..8ce8d739c9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py @@ -0,0 +1,70 @@ +from sentry_sdk import metrics, profiler + +from sentry_sdk.scope import Scope # isort: skip +from sentry_sdk.client import Client # isort: skip +from sentry_sdk.consts import VERSION +from sentry_sdk.transport import HttpTransport, Transport + +from sentry_sdk.api import * # noqa # isort: skip + +__all__ = [ # noqa + "Hub", + "Scope", + "Client", + "Transport", + "HttpTransport", + "VERSION", + "integrations", + # From sentry_sdk.api + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "logger", + "metrics", + "profiler", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + +# Initialize the debug support after everything is loaded +from sentry_sdk.debug import init_debug_support + +init_debug_support() +del init_debug_support + +# circular imports +from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py new file mode 100644 index 0000000000..565fac2a2d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py @@ -0,0 +1,184 @@ +import os +import random +import threading +import weakref +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Generic, TypeVar + +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + +T = TypeVar("T") + + +class Batcher(Generic[T]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "" + CONTENT_TYPE = "" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + self._buffer: "list[T]" = [] + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._flush_event: "threading.Event" = threading.Event() + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._buffer = [] + self._running = True + self._lock = threading.Lock() + self._active = threading.local() + self._flush_event = threading.Event() + self._flusher = None + self._flusher_pid = None + + def _ensure_thread(self) -> bool: + """For forking processes we might need to restart this thread. + This ensures that our process actually has that thread running. + """ + if not self._running: + return False + + pid = os.getpid() + if self._flusher_pid == pid: + return True + + with self._lock: + # Recheck to make sure another thread didn't get here and start the + # the flusher in the meantime + if self._flusher_pid == pid: + return True + + self._flusher_pid = pid + + self._flusher = threading.Thread(target=self._flush_loop) + self._flusher.daemon = True + + try: + self._flusher.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return False + + return True + + def _flush_loop(self) -> None: + # Mark the flush-loop thread as active for its entire lifetime so + # that any re-entrant add() triggered by GC warnings during wait(), + # flush(), or Event operations is silently dropped instead of + # deadlocking on internal locks. + self._active.flag = True + while self._running: + self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) + self._flush_event.clear() + self._flush() + + def add(self, item: "T") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + if len(self._buffer) >= self.MAX_BEFORE_DROP: + self._record_lost(item) + return None + + self._buffer.append(item) + if len(self._buffer) >= self.MAX_BEFORE_FLUSH: + self._flush_event.set() + finally: + self._active.flag = False + + def kill(self) -> None: + if self._flusher is None: + return + + self._running = False + self._flush_event.set() + self._flusher = None + + def flush(self) -> None: + was_active = getattr(self._active, "flag", False) + self._active.flag = True + try: + self._flush() + finally: + self._active.flag = was_active + + def _add_to_envelope(self, envelope: "Envelope") -> None: + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": len(self._buffer), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(item) for item in self._buffer + ], + } + ), + ) + ) + + def _flush(self) -> "Optional[Envelope]": + envelope = Envelope( + headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} + ) + with self._lock: + if len(self._buffer) == 0: + return None + + self._add_to_envelope(envelope) + self._buffer.clear() + + self._capture_func(envelope) + return envelope + + def _record_lost(self, item: "T") -> None: + pass + + @staticmethod + def _to_transport_format(item: "T") -> "Any": + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py new file mode 100644 index 0000000000..f62175c09f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py @@ -0,0 +1,92 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, TypeVar + + T = TypeVar("T") + + +PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 +PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 +PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 +PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 + + +def with_metaclass(meta: "Any", *bases: "Any") -> "Any": + class MetaClass(type): + def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": + return meta(name, bases, d) + + return type.__new__(MetaClass, "temporary_class", (), {}) + + +def check_uwsgi_thread_support() -> bool: + # We check two things here: + # + # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if + # that's the case. + # + # 2. Additionally, if uWSGI is running in preforking mode (default), it needs + # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This + # is because any background threads spawned before the main process is + # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if + # --enable-threads is on. One has to explicitly provide + # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython + # after-fork hooks that take care of cleaning up stale thread data. + try: + from uwsgi import opt # type: ignore + except ImportError: + return True + + from sentry_sdk.consts import FALSE_VALUES + + def enabled(option: str) -> bool: + value = opt.get(option, False) + if isinstance(value, bool): + return value + + if isinstance(value, bytes): + try: + value = value.decode() + except Exception: + pass + + return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] + + # When `threads` is passed in as a uwsgi option, + # `enable-threads` is implied on. + threads_enabled = "threads" in opt or enabled("enable-threads") + fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") + lazy_mode = enabled("lazy-apps") or enabled("lazy") + + if lazy_mode and not threads_enabled: + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI without thread support. " + "This might lead to unexpected issues. " + 'Please run uWSGI with "--enable-threads" for full support.' + ) + ) + + return False + + elif not lazy_mode and (not threads_enabled or not fork_hooks_on): + from warnings import warn + + warn( + Warning( + "IMPORTANT: " + "We detected the use of uWSGI in preforking mode without " + "thread support. This might lead to crashing workers. " + 'Please run uWSGI with both "--enable-threads" and ' + '"--py-call-uwsgi-fork-hooks" for full support.' + ) + ) + + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py new file mode 100644 index 0000000000..923fcf6df8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py @@ -0,0 +1,78 @@ +import warnings +from typing import TYPE_CHECKING + +import sentry_sdk + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional + + import sentry_sdk.consts + + +class _InitGuard: + _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( + "Using the return value of sentry_sdk.init as a context manager " + "and manually calling the __enter__ and __exit__ methods on the " + "return value are deprecated. We are no longer maintaining this " + "functionality, and we will remove it in the next major release." + ) + + def __init__(self, client: "sentry_sdk.Client") -> None: + self._client = client + + def __enter__(self) -> "_InitGuard": + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + warnings.warn( + self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, + stacklevel=2, + category=DeprecationWarning, + ) + + c = self._client + if c is not None: + c.close() + + +def _check_python_deprecations() -> None: + # Since we're likely to deprecate Python versions in the future, I'm keeping + # this handy function around. Use this to detect the Python version used and + # to output logger.warning()s if it's deprecated. + pass + + +def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": + """Initializes the SDK and optionally integrations. + + This takes the same arguments as the client constructor. + """ + client = sentry_sdk.Client(*args, **kwargs) + sentry_sdk.get_global_scope().set_client(client) + _check_python_deprecations() + rv = _InitGuard(client) + return rv + + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `init` is a type to + # have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `ContextManager[Any]` to tell static analyzers about the return type. + + class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 + pass + +else: + # Alias `init` for actual usage. Go through the lambda indirection to throw + # PyCharm off of the weakly typed signature (it would otherwise discover + # both the weakly typed signature of `_init` and our faked `init` type). + + init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py new file mode 100644 index 0000000000..0719932ee9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Item, PayloadRef +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Log + + +class LogBatcher(Batcher["Log"]): + MAX_BEFORE_FLUSH = 100 + MAX_BEFORE_DROP = 1_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "log" + CONTENT_TYPE = "application/vnd.sentry.items.log+json" + + @staticmethod + def _to_transport_format(item: "Log") -> "Any": + if "sentry.severity_number" not in item["attributes"]: + item["attributes"]["sentry.severity_number"] = item["severity_number"] + if "sentry.severity_text" not in item["attributes"]: + item["attributes"]["sentry.severity_text"] = item["severity_text"] + + res = { + "timestamp": int(item["time_unix_nano"]) / 1.0e9, + "level": str(item["severity_text"]), + "body": str(item["body"]), + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + return res + + def _record_lost(self, item: "Log") -> None: + # Construct log envelope item without sending it to report lost bytes + log_item = Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": 1, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [self._to_transport_format(item)], + } + ), + ) + + self._record_lost_func( + reason="queue_overflow", + data_category="log_item", + item=log_item, + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py new file mode 100644 index 0000000000..16c238bcab --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py @@ -0,0 +1,43 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +_SENTINEL = object() + + +class LRUCache: + def __init__(self, max_size: int) -> None: + if max_size <= 0: + raise AssertionError(f"invalid max_size: {max_size}") + self.max_size = max_size + self._data: "dict[Any, Any]" = {} + self.hits = self.misses = 0 + self.full = False + + def set(self, key: "Any", value: "Any") -> None: + current = self._data.pop(key, _SENTINEL) + if current is not _SENTINEL: + self._data[key] = value + elif self.full: + self._data.pop(next(iter(self._data))) + self._data[key] = value + else: + self._data[key] = value + self.full = len(self._data) >= self.max_size + + def get(self, key: "Any", default: "Any" = None) -> "Any": + try: + ret = self._data.pop(key) + except KeyError: + self.misses += 1 + ret = default + else: + self.hits += 1 + self._data[key] = ret + + return ret + + def get_all(self) -> "list[tuple[Any, Any]]": + return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py new file mode 100644 index 0000000000..06bce1a282 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py @@ -0,0 +1,48 @@ +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.utils import serialize_attribute + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Metric + + +class MetricsBatcher(Batcher["Metric"]): + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 10_000 + FLUSH_WAIT_TIME = 5.0 + + TYPE = "trace_metric" + CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" + + @staticmethod + def _to_transport_format(item: "Metric") -> "Any": + res = { + "timestamp": item["timestamp"], + "name": item["name"], + "type": item["type"], + "value": item["value"], + "attributes": { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + }, + } + + if item.get("trace_id") is not None: + res["trace_id"] = item["trace_id"] + + if item.get("span_id") is not None: + res["span_id"] = item["span_id"] + + if item.get("unit") is not None: + res["unit"] = item["unit"] + + return res + + def _record_lost(self, item: "Metric") -> None: + self._record_lost_func( + reason="queue_overflow", + data_category="trace_metric", + quantity=1, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py new file mode 100644 index 0000000000..c28c8de9ac --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py @@ -0,0 +1,287 @@ +""" +A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') +with Lock swapped out for RLock to avoid a deadlock while garbage collecting. + +https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py + + +See also +https://codewithoutrules.com/2017/08/16/concurrency-python/ +https://bugs.python.org/issue14976 +https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 + +We also vendor the code to evade eventlet's broken monkeypatching, see +https://github.com/getsentry/sentry-python/pull/484 + + +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; + +All Rights Reserved + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + +""" + +import threading +from collections import deque +from time import time +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + +__all__ = ["EmptyError", "FullError", "Queue"] + + +class EmptyError(Exception): + "Exception raised by Queue.get(block=0)/get_nowait()." + + pass + + +class FullError(Exception): + "Exception raised by Queue.put(block=0)/put_nowait()." + + pass + + +class Queue: + """Create a queue object with a given maximum size. + + If maxsize is <= 0, the queue size is infinite. + """ + + def __init__(self, maxsize=0): + self.maxsize = maxsize + self._init(maxsize) + + # mutex must be held whenever the queue is mutating. All methods + # that acquire mutex must release it before returning. mutex + # is shared between the three conditions, so acquiring and + # releasing the conditions also acquires and releases mutex. + self.mutex = threading.RLock() + + # Notify not_empty whenever an item is added to the queue; a + # thread waiting to get is notified then. + self.not_empty = threading.Condition(self.mutex) + + # Notify not_full whenever an item is removed from the queue; + # a thread waiting to put is notified then. + self.not_full = threading.Condition(self.mutex) + + # Notify all_tasks_done whenever the number of unfinished tasks + # drops to zero; thread waiting to join() is notified to resume + self.all_tasks_done = threading.Condition(self.mutex) + self.unfinished_tasks = 0 + + def task_done(self): + """Indicate that a formerly enqueued task is complete. + + Used by Queue consumer threads. For each get() used to fetch a task, + a subsequent call to task_done() tells the queue that the processing + on the task is complete. + + If a join() is currently blocking, it will resume when all items + have been processed (meaning that a task_done() call was received + for every item that had been put() into the queue). + + Raises a ValueError if called more times than there were items + placed in the queue. + """ + with self.all_tasks_done: + unfinished = self.unfinished_tasks - 1 + if unfinished <= 0: + if unfinished < 0: + raise ValueError("task_done() called too many times") + self.all_tasks_done.notify_all() + self.unfinished_tasks = unfinished + + def join(self): + """Blocks until all items in the Queue have been gotten and processed. + + The count of unfinished tasks goes up whenever an item is added to the + queue. The count goes down whenever a consumer thread calls task_done() + to indicate the item was retrieved and all work on it is complete. + + When the count of unfinished tasks drops to zero, join() unblocks. + """ + with self.all_tasks_done: + while self.unfinished_tasks: + self.all_tasks_done.wait() + + def qsize(self): + """Return the approximate size of the queue (not reliable!).""" + with self.mutex: + return self._qsize() + + def empty(self): + """Return True if the queue is empty, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() == 0 + as a direct substitute, but be aware that either approach risks a race + condition where a queue can grow before the result of empty() or + qsize() can be used. + + To create code that needs to wait for all queued tasks to be + completed, the preferred technique is to use the join() method. + """ + with self.mutex: + return not self._qsize() + + def full(self): + """Return True if the queue is full, False otherwise (not reliable!). + + This method is likely to be removed at some point. Use qsize() >= n + as a direct substitute, but be aware that either approach risks a race + condition where a queue can shrink before the result of full() or + qsize() can be used. + """ + with self.mutex: + return 0 < self.maxsize <= self._qsize() + + def put(self, item, block=True, timeout=None): + """Put an item into the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until a free slot is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the FullError exception if no free slot was available within that time. + Otherwise ('block' is false), put an item on the queue if a free slot + is immediately available, else raise the FullError exception ('timeout' + is ignored in that case). + """ + with self.not_full: + if self.maxsize > 0: + if not block: + if self._qsize() >= self.maxsize: + raise FullError() + elif timeout is None: + while self._qsize() >= self.maxsize: + self.not_full.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while self._qsize() >= self.maxsize: + remaining = endtime - time() + if remaining <= 0.0: + raise FullError() + self.not_full.wait(remaining) + self._put(item) + self.unfinished_tasks += 1 + self.not_empty.notify() + + def get(self, block=True, timeout=None): + """Remove and return an item from the queue. + + If optional args 'block' is true and 'timeout' is None (the default), + block if necessary until an item is available. If 'timeout' is + a non-negative number, it blocks at most 'timeout' seconds and raises + the EmptyError exception if no item was available within that time. + Otherwise ('block' is false), return an item if one is immediately + available, else raise the EmptyError exception ('timeout' is ignored + in that case). + """ + with self.not_empty: + if not block: + if not self._qsize(): + raise EmptyError() + elif timeout is None: + while not self._qsize(): + self.not_empty.wait() + elif timeout < 0: + raise ValueError("'timeout' must be a non-negative number") + else: + endtime = time() + timeout + while not self._qsize(): + remaining = endtime - time() + if remaining <= 0.0: + raise EmptyError() + self.not_empty.wait(remaining) + item = self._get() + self.not_full.notify() + return item + + def put_nowait(self, item): + """Put an item into the queue without blocking. + + Only enqueue the item if a free slot is immediately available. + Otherwise raise the FullError exception. + """ + return self.put(item, block=False) + + def get_nowait(self): + """Remove and return an item from the queue without blocking. + + Only get an item if one is immediately available. Otherwise + raise the EmptyError exception. + """ + return self.get(block=False) + + # Override these methods to implement other queue organizations + # (e.g. stack or priority queue). + # These will only be called with appropriate locks held + + # Initialize the queue representation + def _init(self, maxsize): + self.queue: "Any" = deque() + + def _qsize(self): + return len(self.queue) + + # Put a new item in the queue + def _put(self, item): + self.queue.append(item) + + # Get an item from the queue + def _get(self): + return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py new file mode 100644 index 0000000000..79285c3386 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py @@ -0,0 +1,234 @@ +import os +import random +import threading +import time +import weakref +from collections import defaultdict +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._batcher import Batcher +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import format_timestamp, serialize_attribute + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + from sentry_sdk._types import SpanJSON + + +class SpanBatcher(Batcher["SpanJSON"]): + # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is + # a bit of a buffer for spans that appear between the trigger to flush + # and actually flushing the buffer. + # + # The max limits are all per trace (per bucket). + MAX_ENVELOPE_SIZE = 1000 # spans + MAX_BEFORE_FLUSH = 1000 + MAX_BEFORE_DROP = 2000 + MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB + + FLUSH_WAIT_TIME = 5.0 + + TYPE = "span" + CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" + + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + record_lost_func: "Callable[..., None]", + ) -> None: + # Spans from different traces cannot be emitted in the same envelope + # since the envelope contains a shared trace header. That's why we bucket + # by trace_id, so that we can then send the buckets each in its own + # envelope. + # trace_id -> span buffer + self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) + self._running_size: dict[str, int] = defaultdict(lambda: 0) + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + self._active: "threading.local" = threading.local() + + self._last_full_flush: float = time.monotonic() # drives time-based flushes + self._flush_event = threading.Event() + self._pending_flush: set[str] = set() # buckets to be flushed + + self._flusher: "Optional[threading.Thread]" = None + self._flusher_pid: "Optional[int]" = None + + # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._span_buffer = defaultdict(list) + self._running_size = defaultdict(lambda: 0) + self._running = True + + self._lock = threading.Lock() + self._active = threading.local() + + self._last_full_flush = time.monotonic() + self._flush_event = threading.Event() + self._pending_flush = set() + + self._flusher = None + self._flusher_pid = None + + def _flush_loop(self) -> None: + self._active.flag = True + while self._running: + jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 + self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) + self._flush_event.clear() + + self._flush(only_pending=True) + + if ( + time.monotonic() - self._last_full_flush + >= self.FLUSH_WAIT_TIME + jitter + ): + self._flush() + self._last_full_flush = time.monotonic() + + def add(self, span: "SpanJSON") -> None: + # Bail out if the current thread is already executing batcher code. + # This prevents deadlocks when code running inside the batcher (e.g. + # _add_to_envelope during flush, or _flush_event.wait/set) triggers + # a GC-emitted warning that routes back through the logging + # integration into add(). + if getattr(self._active, "flag", False): + return None + + self._active.flag = True + + try: + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + size = len(self._span_buffer[span["trace_id"]]) + if size >= self.MAX_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="span", + quantity=1, + ) + return None + + self._span_buffer[span["trace_id"]].append(span) + self._running_size[span["trace_id"]] += self._estimate_size(span) + + if ( + size + 1 >= self.MAX_BEFORE_FLUSH + or self._running_size[span["trace_id"]] + >= self.MAX_BYTES_BEFORE_FLUSH + ): + self._pending_flush.add(span["trace_id"]) + notify = True + else: + notify = False + + if notify: + self._flush_event.set() + finally: + self._active.flag = False + + @staticmethod + def _estimate_size(item: "SpanJSON") -> int: + # Rough estimate of serialized span size that's quick to compute. + # 210 is the rough size of the payload without attributes, and then we + # estimate the attributes separately. + estimate = 210 + for value in (item.get("attributes") or {}).values(): + estimate += 50 + + if isinstance(value, str): + estimate += len(value) + else: + estimate += len(str(value)) + + return estimate + + @staticmethod + def _to_transport_format(item: "SpanJSON") -> "Any": + res = {k: v for k, v in item.items() if k not in ("_segment_span",)} + + if item.get("attributes"): + res["attributes"] = { + k: serialize_attribute(v) for (k, v) in item["attributes"].items() + } + else: + del res["attributes"] + + return res + + def _flush(self, only_pending: bool = False) -> None: + with self._lock: + if only_pending: + buckets = list(self._pending_flush) + else: + # flush whole buffer, e.g. if the SDK is shutting down + buckets = list(self._span_buffer.keys()) + + self._pending_flush.clear() + + if not buckets: + return + + envelopes = [] + + for bucket_id in buckets: + spans = self._span_buffer.get(bucket_id) + if not spans: + continue + + dsc = spans[0]["_segment_span"]._dynamic_sampling_context() + + # Max per envelope is 1000, so if we happen to have more than + # 1000 spans in one bucket, we'll need to separate them. + for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): + end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) + + envelope = Envelope( + headers={ + "sent_at": format_timestamp(datetime.now(timezone.utc)), + "trace": dsc, + } + ) + + envelope.add_item( + Item( + type=self.TYPE, + content_type=self.CONTENT_TYPE, + headers={ + "item_count": end - start, + }, + payload=PayloadRef( + json={ + "version": 2, + "items": [ + self._to_transport_format(spans[j]) + for j in range(start, end) + ], + } + ), + ) + ) + + envelopes.append(envelope) + + del self._span_buffer[bucket_id] + del self._running_size[bucket_id] + + for envelope in envelopes: + self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py new file mode 100644 index 0000000000..fbd2578048 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py @@ -0,0 +1,481 @@ +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING, TypeVar, Union + +# Re-exported for compat, since code out there in the wild might use this variable. +MYPY = TYPE_CHECKING + + +SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" +BLOB_DATA_SUBSTITUTE = "[Blob substitute]" +OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" +UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" + + +class AnnotatedValue: + """ + Meta information for a data field in the event payload. + This is to tell Relay that we have tampered with the fields value. + See: + https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 + """ + + __slots__ = ("value", "metadata") + + def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: + self.value = value + self.metadata = metadata + + def __eq__(self, other: "Any") -> bool: + if not isinstance(other, AnnotatedValue): + return False + + return self.value == other.value and self.metadata == other.metadata + + def __str__(self: "AnnotatedValue") -> str: + return str({"value": str(self.value), "metadata": str(self.metadata)}) + + def __len__(self: "AnnotatedValue") -> int: + if self.value is not None: + return len(self.value) + else: + return 0 + + @classmethod + def removed_because_raw_data(cls) -> "AnnotatedValue": + """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" + # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition + # to span-first + return AnnotatedValue( + value="", + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_raw_data(cls) -> "AnnotatedValue": + """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" + return AnnotatedValue( + value=UNPARSABLE_RAW_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!raw", # Unparsable raw data + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": + """ + The actual value was removed because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition + # to span-first + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "x", # The fields original value was removed + ] + ] + }, + ) + + @classmethod + def substituted_because_over_size_limit( + cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE + ) -> "AnnotatedValue": + """ + The actual value was replaced because the size of the field exceeded the configured maximum size, + for example specified with the max_request_body_size sdk option. + """ + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of configured maximum size + "s", # The fields original value was substituted + ] + ] + }, + ) + + @classmethod + def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": + """The actual value was removed because it contained sensitive information.""" + return AnnotatedValue( + value=SENSITIVE_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) + "s", # The fields original value was substituted + ] + ] + }, + ) + + +T = TypeVar("T") +Annotated = Union[AnnotatedValue, T] + + +if TYPE_CHECKING: + from collections.abc import Container, MutableMapping, Sequence + from datetime import datetime + from types import TracebackType + from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + + class SDKInfo(TypedDict): + name: str + version: str + packages: "Sequence[Mapping[str, str]]" + + class KeyValueCollectionBehaviour(TypedDict): + mode: 'Literal["off", "denylist", "allowlist"]' + 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): + request: "KeyValueCollectionBehaviour" + + class HttpHeadersCollectionBehaviour(TypedDict): + request: "KeyValueCollectionBehaviour" + + class DataCollectionUserOptions(TypedDict, total=False): + user_info: bool + cookies: "KeyValueCollectionBehaviour" + http_headers: "HttpHeadersCollectionUserOptions" + http_bodies: "List[str]" + query_params: "KeyValueCollectionBehaviour" + graphql: "GraphQLCollectionUserOptions" + gen_ai: "GenAICollectionUserOptions" + database_query_data: bool + queues: bool + 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_query_data: bool + queues: bool + 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"] + + DurationUnit = Literal[ + "nanosecond", + "microsecond", + "millisecond", + "second", + "minute", + "hour", + "day", + "week", + ] + + InformationUnit = Literal[ + "bit", + "byte", + "kilobyte", + "kibibyte", + "megabyte", + "mebibyte", + "gigabyte", + "gibibyte", + "terabyte", + "tebibyte", + "petabyte", + "pebibyte", + "exabyte", + "exbibyte", + ] + + FractionUnit = Literal["ratio", "percent"] + MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] + + MeasurementValue = TypedDict( + "MeasurementValue", + { + "value": float, + "unit": NotRequired[Optional[MeasurementUnit]], + }, + ) + + Event = TypedDict( + "Event", + { + "breadcrumbs": Annotated[ + dict[Literal["values"], list[dict[str, Any]]] + ], # TODO: We can expand on this type + "check_in_id": str, + "contexts": dict[str, dict[str, object]], + "dist": str, + "duration": Optional[float], + "environment": Optional[str], + "errors": list[dict[str, Any]], # TODO: We can expand on this type + "event_id": str, + "exception": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "extra": MutableMapping[str, object], + "fingerprint": list[str], + "level": LogLevelStr, + "logentry": Mapping[str, object], + "logger": str, + "measurements": dict[str, MeasurementValue], + "message": str, + "modules": dict[str, str], + "monitor_config": Mapping[str, object], + "monitor_slug": Optional[str], + "platform": Literal["python"], + "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports + "release": Optional[str], + "request": dict[str, object], + "sdk": Mapping[str, object], + "server_name": str, + "spans": Annotated[list[dict[str, object]]], + "stacktrace": dict[ + str, object + ], # We access this key in the code, but I am unsure whether we ever set it + "start_timestamp": datetime, + "status": Optional[str], + "tags": MutableMapping[ + str, str + ], # Tags must be less than 200 characters each + "threads": dict[ + Literal["values"], list[dict[str, Any]] + ], # TODO: We can expand on this type + "timestamp": Optional[datetime], # Must be set before sending the event + "transaction": str, + "transaction_info": Mapping[str, Any], # TODO: We can expand on this type + "type": Literal["check_in", "transaction"], + "user": dict[str, object], + "_dropped_spans": int, + "_has_gen_ai_span": bool, + }, + total=False, + ) + + ExcInfo = Union[ + tuple[Type[BaseException], BaseException, Optional[TracebackType]], + tuple[None, None, None], + ] + + # TODO: Make a proper type definition for this (PRs welcome!) + Hint = Dict[str, Any] + + AttributeValue = ( + str + | bool + | float + | int + | list[str] + | list[bool] + | list[float] + | list[int] + | tuple[str, ...] + | tuple[bool, ...] + | tuple[float, ...] + | tuple[int, ...] + ) + Attributes = dict[str, AttributeValue] + + SerializedAttributeValue = TypedDict( + # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types + "SerializedAttributeValue", + { + "type": Literal[ + "string", + "boolean", + "double", + "integer", + "array", + ], + "value": AttributeValue, + }, + ) + + Log = TypedDict( + "Log", + { + "severity_text": str, + "severity_number": int, + "body": str, + "attributes": Attributes, + "time_unix_nano": int, + "trace_id": Optional[str], + "span_id": Optional[str], + }, + ) + + MetricType = Literal["counter", "gauge", "distribution"] + MetricUnit = Union[DurationUnit, InformationUnit, str] + + Metric = TypedDict( + "Metric", + { + "timestamp": float, + "trace_id": Optional[str], + "span_id": Optional[str], + "name": str, + "type": MetricType, + "value": float, + "unit": Optional[MetricUnit], + "attributes": Attributes, + }, + ) + + MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] + + SpanJSON = TypedDict( + "SpanJSON", + { + "trace_id": str, + "span_id": str, + "parent_span_id": NotRequired[str], + "name": str, + "status": str, + "is_segment": bool, + "start_timestamp": float, + "end_timestamp": NotRequired[float], + "attributes": NotRequired[Attributes], + "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], + }, + ) + + # TODO: Make a proper type definition for this (PRs welcome!) + Breadcrumb = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + BreadcrumbHint = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) + SamplingContext = Dict[str, Any] + + EventProcessor = Callable[[Event, Hint], Optional[Event]] + ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] + BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] + TransactionProcessor = Callable[[Event, Hint], Optional[Event]] + LogProcessor = Callable[[Log, Hint], Optional[Log]] + + TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] + + # https://github.com/python/mypy/issues/5710 + NotImplementedType = Any + + EventDataCategory = Literal[ + "default", + "error", + "crash", + "transaction", + "security", + "attachment", + "session", + "internal", + "profile", + "profile_chunk", + "monitor", + "span", + "log_item", + "log_byte", + "trace_metric", + ] + SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] + + ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] + ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] + + MonitorConfigScheduleType = Literal["crontab", "interval"] + MonitorConfigScheduleUnit = Literal[ + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", # not supported in Sentry and will result in a warning + ] + + MonitorConfigSchedule = TypedDict( + "MonitorConfigSchedule", + { + "type": MonitorConfigScheduleType, + "value": Union[int, str], + "unit": MonitorConfigScheduleUnit, + }, + total=False, + ) + + MonitorConfig = TypedDict( + "MonitorConfig", + { + "schedule": MonitorConfigSchedule, + "timezone": str, + "checkin_margin": int, + "max_runtime": int, + "failure_issue_threshold": int, + "recovery_threshold": int, + "owner": str, + }, + total=False, + ) + + HttpStatusCodeRange = Union[int, Container[int]] + + class TextPart(TypedDict): + type: Literal["text"] + content: str + + IgnoreSpansName = Union[str, Pattern[str]] + IgnoreSpansContext = TypedDict( + "IgnoreSpansContext", + {"name": IgnoreSpansName, "attributes": Attributes}, + total=False, + ) + IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py new file mode 100644 index 0000000000..98f932267f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py @@ -0,0 +1,100 @@ +""" +Copyright (c) 2007 by the Pallets team. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Dict, Iterator, Optional, Tuple + + +# +# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` +# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 +# +# We need this function because Django does not give us a "pure" http header +# dict. So we might as well use it for all WSGI integrations. +# +def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns only proper HTTP headers. + """ + for key, value in environ.items(): + key = str(key) + if key.startswith("HTTP_") and key not in ( + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + ): + yield key[5:].replace("_", "-").title(), value + elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): + yield key.replace("_", "-").title(), value + + +def _strip_default_port(host: str, scheme: "Optional[str]") -> str: + """Strip the port from the host if it's the default for the scheme.""" + if scheme == "http" and host.endswith(":80"): + return host[:-3] + if scheme == "https" and host.endswith(":443"): + return host[:-4] + return host + + +# `get_host` comes from `werkzeug.wsgi.get_host` +# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 + + +def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: + """ + Return the host for the given WSGI environment. + """ + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: + return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) + elif environ.get("HTTP_HOST"): + return _strip_default_port(environ["HTTP_HOST"], scheme) + elif environ.get("SERVER_NAME"): + # SERVER_NAME/SERVER_PORT describe the internal server, so use + # wsgi.url_scheme (not the forwarded scheme) for port decisions. + rv = environ["SERVER_NAME"] + if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( + ("https", "443"), + ("http", "80"), + ): + rv += ":" + environ["SERVER_PORT"] + return rv + else: + # In spite of the WSGI spec, SERVER_NAME might not be present. + return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py new file mode 100644 index 0000000000..404e57ff1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py @@ -0,0 +1,8 @@ +from .utils import ( + GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 + GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 + normalize_message_role, # noqa: F401 + normalize_message_roles, # noqa: F401 + set_conversation_id, # noqa: F401 + set_data_normalized, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py new file mode 100644 index 0000000000..b5eb8c55ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py @@ -0,0 +1,66 @@ +from collections.abc import Iterable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.chat import ( + ChatCompletionContentPartParam, + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ) + + from sentry_sdk._types import TextPart + + +def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: + return isinstance(message, dict) and message.get("role") == "system" + + +def _get_system_instructions( + messages: "Iterable[ChatCompletionMessageParam]", +) -> "list[ChatCompletionMessageParam]": + if not isinstance(messages, Iterable): + return [] + + return [message for message in messages if _is_system_instruction(message)] + + +def _get_text_items( + content: "Union[str, Iterable[ChatCompletionContentPartParam]]", +) -> "list[str]": + if isinstance(content, str): + return [content] + + if not isinstance(content, Iterable): + return [] + + text_items = [] + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + text = part.get("text", None) + if text is not None: + text_items.append(text) + + return text_items + + +def _transform_system_instructions( + system_instructions: "list[ChatCompletionSystemMessageParam]", +) -> "list[TextPart]": + instruction_text_parts: "list[TextPart]" = [] + + for instruction in system_instructions: + if not isinstance(instruction, dict): + continue + + content = instruction.get("content") + if content is None: + continue + + text_parts: "list[TextPart]" = [ + {"type": "text", "content": text} for text in _get_text_items(content) + ] + instruction_text_parts += text_parts + + return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py new file mode 100644 index 0000000000..8f751c3248 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Union + + from openai.types.responses import ResponseInputItemParam, ResponseInputParam + + +def _is_system_instruction(message: "ResponseInputItemParam") -> bool: + if not isinstance(message, dict) or not message.get("role") == "system": + return False + + return "type" not in message or message["type"] == "message" + + +def _get_system_instructions( + messages: "Union[str, ResponseInputParam]", +) -> "list[ResponseInputItemParam]": + if not isinstance(messages, list): + return [] + + return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py new file mode 100644 index 0000000000..35ee4bd788 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py @@ -0,0 +1,6 @@ +import re + +# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." +DATA_URL_BASE64_REGEX = re.compile( + r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py new file mode 100644 index 0000000000..d8840ad451 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py @@ -0,0 +1,147 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk.utils +from sentry_sdk import start_span +from sentry_sdk.ai.utils import _set_span_data_attribute +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, TypeVar, Union + + F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) + +_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) + + +def set_ai_pipeline_name(name: "Optional[str]") -> None: + _ai_pipeline_name.set(name) + + +def get_ai_pipeline_name() -> "Optional[str]": + return _ai_pipeline_name.get() + + +def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": + def decorator(f: "F") -> "F": + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + curr_pipeline = _ai_pipeline_name.get() + op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") + + with start_span(name=description, op=op, **span_kwargs) as span: + for k, v in kwargs.pop("sentry_tags", {}).items(): + span.set_tag(k, v) + for k, v in kwargs.pop("sentry_data", {}).items(): + span.set_data(k, v) + if curr_pipeline: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) + return await f(*args, **kwargs) + else: + _ai_pipeline_name.set(description) + try: + res = await f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + event, hint = sentry_sdk.utils.event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "ai_monitoring", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + reraise(*exc_info) + finally: + _ai_pipeline_name.set(None) + return res + + if inspect.iscoroutinefunction(f): + return wraps(f)(async_wrapped) # type: ignore + else: + return wraps(f)(sync_wrapped) # type: ignore + + return decorator + + +def record_token_usage( + span: "Union[Span, StreamedSpan]", + input_tokens: "Optional[int]" = None, + input_tokens_cached: "Optional[int]" = None, + input_tokens_cache_write: "Optional[int]" = None, + output_tokens: "Optional[int]" = None, + output_tokens_reasoning: "Optional[int]" = None, + total_tokens: "Optional[int]" = None, +) -> None: + # TODO: move pipeline name elsewhere + ai_pipeline_name = get_ai_pipeline_name() + if ai_pipeline_name: + _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) + + if input_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if input_tokens_cached is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + input_tokens_cached, + ) + + if input_tokens_cache_write is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + input_tokens_cache_write, + ) + + if output_tokens is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + ) + + if output_tokens_reasoning is not None: + _set_span_data_attribute( + span, + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + output_tokens_reasoning, + ) + + if total_tokens is None and input_tokens is not None and output_tokens is not None: + total_tokens = input_tokens + output_tokens + + if total_tokens is not None: + _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py new file mode 100644 index 0000000000..7b1ca9324b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py @@ -0,0 +1,782 @@ +import inspect +import json +from copy import deepcopy +from typing import TYPE_CHECKING + +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import logger + +MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB +# Maximum characters when only a single message is left after bytes truncation +MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 + + +class GEN_AI_ALLOWED_MESSAGE_ROLES: + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { + GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], + GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], + GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], + GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], +} + +GEN_AI_MESSAGE_ROLE_MAPPING = {} +for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): + for source_role in source_roles: + GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role + + +def parse_data_uri(url: str) -> "Tuple[str, str]": + """ + Parse a data URI and return (mime_type, content). + + Data URI format (RFC 2397): data:[][;base64], + + Examples: + data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") + data:text/plain,Hello → ("text/plain", "Hello") + data:;base64,SGVsbG8= → ("", "SGVsbG8=") + + Raises: + ValueError: If the URL is not a valid data URI (missing comma separator) + """ + if "," not in url: + raise ValueError("Invalid data URI: missing comma separator") + + header, content = url.split(",", 1) + + # Extract mime type from header + # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" + # Remove "data:" prefix, then take everything before the first semicolon + if header.startswith("data:"): + mime_part = header[5:] # Remove "data:" prefix + else: + mime_part = header + + mime_type = mime_part.split(";")[0] + + return mime_type, content + + +def get_modality_from_mime_type(mime_type: str) -> str: + """ + Infer the content modality from a MIME type string. + + Args: + mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") + + Returns: + One of: "image", "audio", "video", or "document" + Defaults to "image" for unknown or empty MIME types. + + Examples: + "image/jpeg" -> "image" + "audio/mp3" -> "audio" + "video/mp4" -> "video" + "application/pdf" -> "document" + "text/plain" -> "document" + """ + if not mime_type: + return "image" # Default fallback + + mime_lower = mime_type.lower() + if mime_lower.startswith("image/"): + return "image" + elif mime_lower.startswith("audio/"): + return "audio" + elif mime_lower.startswith("video/"): + return "video" + elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): + return "document" + else: + return "image" # Default fallback for unknown types + + +def transform_openai_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an OpenAI/LiteLLM content part to Sentry's standardized format. + + This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. + + Input format: + - {"type": "image_url", "image_url": {"url": "..."}} + - {"type": "image_url", "image_url": "..."} (string shorthand) + + Output format (one of): + - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from OpenAI/LiteLLM + + Returns: + A transformed dictionary in standardized format, or None if the format + is not OpenAI image_url format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type != "image_url": + return None + + image_url_data = content_part.get("image_url") + if isinstance(image_url_data, str): + url = image_url_data + elif isinstance(image_url_data, dict): + url = image_url_data.get("url", "") + else: + return None + + if not url: + return None + + # Check if it's a data URI (base64 encoded) + if url.startswith("data:"): + try: + mime_type, content = parse_data_uri(url) + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": content, + } + except ValueError: + # If parsing fails, return as URI + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + else: + # Regular URL + return { + "type": "uri", + "modality": "image", + "mime_type": "", + "uri": url, + } + + +def transform_anthropic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform an Anthropic content part to Sentry's standardized format. + + This handles the Anthropic image and document formats with source dictionaries. + + Input format: + - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} + - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} + - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} + - {"type": "document", "source": {...}} (same source formats) + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from Anthropic + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Anthropic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "document") or "source" not in content_part: + return None + + source = content_part.get("source") + if not isinstance(source, dict): + return None + + source_type = source.get("type") + media_type = source.get("media_type", "") + modality = ( + "document" + if block_type == "document" + else get_modality_from_mime_type(media_type) + ) + + if source_type == "base64": + return { + "type": "blob", + "modality": modality, + "mime_type": media_type, + "content": source.get("data", ""), + } + elif source_type == "url": + return { + "type": "uri", + "modality": modality, + "mime_type": media_type, + "uri": source.get("url", ""), + } + elif source_type == "file": + return { + "type": "file", + "modality": modality, + "mime_type": media_type, + "file_id": source.get("file_id", ""), + } + + return None + + +def transform_google_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a Google GenAI content part to Sentry's standardized format. + + This handles the Google GenAI inline_data and file_data formats. + + Input format: + - {"inline_data": {"mime_type": "...", "data": "..."}} + - {"file_data": {"mime_type": "...", "file_uri": "..."}} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + + Args: + content_part: A dictionary representing a content part from Google GenAI + + Returns: + A transformed dictionary in standardized format, or None if the format + is not Google format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Handle Google inline_data format + if "inline_data" in content_part: + inline_data = content_part.get("inline_data") + if isinstance(inline_data, dict): + mime_type = inline_data.get("mime_type", "") + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": inline_data.get("data", ""), + } + return None + + # Handle Google file_data format + if "file_data" in content_part: + file_data = content_part.get("file_data") + if isinstance(file_data, dict): + mime_type = file_data.get("mime_type", "") + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_data.get("file_uri", ""), + } + return None + + return None + + +def transform_generic_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a generic/LangChain-style content part to Sentry's standardized format. + + This handles generic formats where the type indicates the modality and + the data is provided via direct base64, url, or file_id fields. + + Input format: + - {"type": "image", "base64": "...", "mime_type": "..."} + - {"type": "audio", "url": "...", "mime_type": "..."} + - {"type": "video", "base64": "...", "mime_type": "..."} + - {"type": "file", "file_id": "...", "mime_type": "..."} + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part in generic format + + Returns: + A transformed dictionary in standardized format, or None if the format + is not generic format or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + block_type = content_part.get("type") + + if block_type not in ("image", "audio", "video", "file"): + return None + + # Ensure it's not Anthropic format (which also uses type: "image") + if "source" in content_part: + return None + + mime_type = content_part.get("mime_type", "") + modality = block_type if block_type != "file" else "document" + + # Check for base64 encoded content + if "base64" in content_part: + return { + "type": "blob", + "modality": modality, + "mime_type": mime_type, + "content": content_part.get("base64", ""), + } + # Check for URL reference + elif "url" in content_part: + return { + "type": "uri", + "modality": modality, + "mime_type": mime_type, + "uri": content_part.get("url", ""), + } + # Check for file_id reference + elif "file_id" in content_part: + return { + "type": "file", + "modality": modality, + "mime_type": mime_type, + "file_id": content_part.get("file_id", ""), + } + + return None + + +def transform_content_part( + content_part: "Dict[str, Any]", +) -> "Optional[Dict[str, Any]]": + """ + Transform a content part from various AI SDK formats to Sentry's standardized format. + + This is a heuristic dispatcher that detects the format and delegates to the + appropriate SDK-specific transformer. For direct SDK integration, prefer using + the specific transformers directly: + - transform_openai_content_part() for OpenAI/LiteLLM + - transform_anthropic_content_part() for Anthropic + - transform_google_content_part() for Google GenAI + - transform_generic_content_part() for LangChain and other generic formats + + Detection order: + 1. OpenAI: type == "image_url" + 2. Google: "inline_data" or "file_data" keys present + 3. Anthropic: type in ("image", "document") with "source" key + 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id + + Output format (one of): + - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} + - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} + - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} + + Args: + content_part: A dictionary representing a content part from an AI SDK + + Returns: + A transformed dictionary in standardized format, or None if the format + is unrecognized or transformation fails. + """ + if not isinstance(content_part, dict): + return None + + # Try OpenAI format first (most common, clear indicator) + result = transform_openai_content_part(content_part) + if result is not None: + return result + + # Try Google format (unique keys make it easy to detect) + result = transform_google_content_part(content_part) + if result is not None: + return result + + # Try Anthropic format (has "source" key) + result = transform_anthropic_content_part(content_part) + if result is not None: + return result + + # Try generic format as fallback + result = transform_generic_content_part(content_part) + if result is not None: + return result + + # Unrecognized format + return None + + +def transform_message_content(content: "Any") -> "Any": + """ + Transform message content, handling both string content and list of content blocks. + + For list content, each item is transformed using transform_content_part(). + Items that cannot be transformed (return None) are kept as-is. + + Args: + content: Message content - can be a string, list of content blocks, or other + + Returns: + - String content: returned as-is + - List content: list with each transformable item converted to standardized format + - Other: returned as-is + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + return transformed + + return content + + +def _normalize_data(data: "Any", unpack: bool = True) -> "Any": + # convert pydantic data (e.g. OpenAI v1+) to json compatible format + if hasattr(data, "model_dump"): + # Check if it's a class (type) rather than an instance + # Model classes can be passed as arguments (e.g., for schema definitions) + if inspect.isclass(data): + return f"" + + try: + return _normalize_data(data.model_dump(), unpack=unpack) + except Exception as e: + logger.warning("Could not convert pydantic data to JSON: %s", e) + return data if isinstance(data, (int, float, bool, str)) else str(data) + + if isinstance(data, list): + if unpack and len(data) == 1: + return _normalize_data(data[0], unpack=unpack) # remove empty dimensions + return list(_normalize_data(x, unpack=unpack) for x in data) + + if isinstance(data, dict): + return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} + + return data if isinstance(data, (int, float, bool, str)) else str(data) + + +def set_data_normalized( + span: "Union[Span, StreamedSpan]", + key: str, + value: "Any", + unpack: bool = True, +) -> None: + normalized = _normalize_data(value, unpack=unpack) + if isinstance(normalized, (int, float, bool, str)): + _set_span_data_attribute(span, key, normalized) + else: + _set_span_data_attribute(span, key, json.dumps(normalized)) + + +def _set_span_data_attribute( + span: "Union[Span, StreamedSpan]", key: str, value: "Any" +) -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(key, value) + else: + span.set_data(key, value) + + +def normalize_message_role(role: str) -> str: + """ + Normalize a message role to one of the 4 allowed gen_ai role values. + Maps "ai" -> "assistant" and keeps other standard roles unchanged. + """ + return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) + + +def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": + """ + Normalize roles in a list of messages to use standard gen_ai role values. + Creates a deep copy to avoid modifying the original messages. + """ + normalized_messages = [] + for message in messages: + if not isinstance(message, dict): + normalized_messages.append(message) + continue + normalized_message = message.copy() + if "role" in message: + normalized_message["role"] = normalize_message_role(message["role"]) + normalized_messages.append(normalized_message) + + return normalized_messages + + +def get_start_span_function() -> "Callable[..., Any]": + current_span = sentry_sdk.get_current_span() + + transaction_exists = ( + current_span is not None and current_span.containing_transaction is not None + ) + return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction + + +def _truncate_single_message_content_if_present( + message: "Dict[str, Any]", max_chars: int +) -> "Dict[str, Any]": + """ + Truncate a message's content to at most `max_chars` characters and append an + ellipsis if truncation occurs. + """ + if not isinstance(message, dict) or "content" not in message: + return message + content = message["content"] + + if isinstance(content, str): + if len(content) <= max_chars: + return message + message["content"] = content[:max_chars] + "..." + return message + + if isinstance(content, list): + remaining = max_chars + for item in content: + if isinstance(item, dict) and "text" in item: + text = item["text"] + if isinstance(text, str): + if len(text) > remaining: + item["text"] = text[:remaining] + "..." + remaining = 0 + else: + remaining -= len(text) + return message + + return message + + +def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: + """ + Find the index of the first message that would exceed the max bytes limit. + Compute the individual message sizes, and return the index of the first message from the back + of the list that would exceed the max bytes limit. + """ + running_sum = 0 + for idx in range(len(messages) - 1, -1, -1): + size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) + running_sum += size + if running_sum > max_bytes: + return idx + 1 + + return 0 + + +def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: + """ + Some content blocks contain an image_url property with base64 content as its value. + This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. + """ + if item.get("type") != "image_url": + return False + + image_url_val = item.get("image_url") + image_url = ( + image_url_val.get("url", "") + if isinstance(image_url_val, dict) + else (image_url_val or "") + ) + data_url_match = DATA_URL_BASE64_REGEX.match(image_url) + + return bool(data_url_match) + + +def redact_blob_message_parts( + messages: "List[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Redact blob message parts from the messages by replacing blob content with "[Filtered]". + + This function creates a deep copy of messages that contain blob content to avoid + mutating the original message dictionaries. Messages without blob content are + returned as-is to minimize copying overhead. + + e.g: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "data:image/jpeg;base64,..." + } + ] + } + becomes: + { + "role": "user", + "content": [ + { + "text": "How many ponies do you see in the image?", + "type": "text" + }, + { + "type": "blob", + "modality": "image", + "mime_type": "image/jpeg", + "content": "[Filtered]" + } + ] + } + """ + + # First pass: check if any message contains blob content + has_blobs = False + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and ( + item.get("type") == "blob" or _is_image_type_with_blob_content(item) + ): + has_blobs = True + break + if has_blobs: + break + + # If no blobs found, return original messages to avoid unnecessary copying + if not has_blobs: + return messages + + # Deep copy messages to avoid mutating the original + messages_copy = deepcopy(messages) + + # Second pass: redact blob content in the copy + for message in messages_copy: + if not isinstance(message, dict): + continue + + content = message.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + if item.get("type") == "blob": + item["content"] = BLOB_DATA_SUBSTITUTE + elif _is_image_type_with_blob_content(item): + if isinstance(item["image_url"], dict): + item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE + else: + item["image_url"] = BLOB_DATA_SUBSTITUTE + + return messages_copy + + +def truncate_messages_by_size( + messages: "List[Dict[str, Any]]", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Tuple[List[Dict[str, Any]], int]": + """ + Returns a truncated messages list, consisting of + - the last message, with its content truncated to `max_single_message_chars` characters, + if the last message's size exceeds `max_bytes` bytes; otherwise, + - the maximum number of messages, starting from the end of the `messages` list, whose total + serialized size does not exceed `max_bytes` bytes. + + In the single message case, the serialized message size may exceed `max_bytes`, because + truncation is based only on character count in that case. + """ + serialized_json = json.dumps(messages, separators=(",", ":")) + current_size = len(serialized_json.encode("utf-8")) + + if current_size <= max_bytes: + return messages, 0 + + truncation_index = _find_truncation_index(messages, max_bytes) + if truncation_index < len(messages): + truncated_messages = messages[truncation_index:] + else: + truncation_index = len(messages) - 1 + truncated_messages = messages[-1:] + + if len(truncated_messages) == 1: + truncated_messages[0] = _truncate_single_message_content_if_present( + deepcopy(truncated_messages[0]), max_chars=max_single_message_chars + ) + + return truncated_messages, truncation_index + + +def truncate_and_annotate_messages( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_message = _truncate_single_message_content_if_present( + deepcopy(messages[-1]), max_chars=max_single_message_chars + ) + if len(messages) > 1: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return [truncated_message] + + +def truncate_and_annotate_embedding_inputs( + messages: "Optional[List[Dict[str, Any]]]", + span: "Any", + scope: "Any", + max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, +) -> "Optional[List[Dict[str, Any]]]": + if not messages: + return None + + messages = redact_blob_message_parts(messages) + + truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) + if removed_count > 0: + scope._gen_ai_original_message_count[span.span_id] = len(messages) + + return truncated_messages + + +def set_conversation_id(conversation_id: str) -> None: + """ + Set the conversation_id in the scope. + """ + scope = sentry_sdk.get_current_scope() + scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py new file mode 100644 index 0000000000..5556b11ace --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py @@ -0,0 +1,573 @@ +import inspect +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import Client, tracing_utils +from sentry_sdk._init_implementation import init +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.crons import monitor +from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import get_current_span as _get_current_streamed_span +from sentry_sdk.tracing import NoOpSpan, Transaction, trace + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Optional, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + MeasurementUnit, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span, TransactionKwargs + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) +else: + + def overload(x: "T") -> "T": + return x + + +# When changing this, update __all__ in __init__.py too +__all__ = [ + "init", + "add_attachment", + "add_breadcrumb", + "capture_event", + "capture_exception", + "capture_message", + "configure_scope", + "continue_trace", + "flush", + "flush_async", + "get_baggage", + "get_client", + "get_global_scope", + "get_isolation_scope", + "get_current_scope", + "get_current_span", + "get_traceparent", + "is_initialized", + "isolation_scope", + "last_event_id", + "new_scope", + "push_scope", + "remove_attribute", + "set_attribute", + "set_context", + "set_extra", + "set_level", + "set_measurement", + "set_tag", + "set_tags", + "set_user", + "start_span", + "start_transaction", + "trace", + "monitor", + "start_session", + "end_session", + "set_transaction_name", + "update_current_span", +] + + +def scopemethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, + inspect.getdoc(getattr(Scope, f.__name__)), + ) + return f + + +def clientmethod(f: "F") -> "F": + f.__doc__ = "%s\n\n%s" % ( + "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, + inspect.getdoc(getattr(Client, f.__name__)), + ) + return f + + +@scopemethod +def get_client() -> "BaseClient": + return Scope.get_client() + + +def is_initialized() -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether Sentry has been initialized or not. + + If a client is available and the client is active + (meaning it is configured to send data) then + Sentry is initialized. + """ + return get_client().is_active() + + +@scopemethod +def get_global_scope() -> "Scope": + return Scope.get_global_scope() + + +@scopemethod +def get_isolation_scope() -> "Scope": + return Scope.get_isolation_scope() + + +@scopemethod +def get_current_scope() -> "Scope": + return Scope.get_current_scope() + + +@scopemethod +def last_event_id() -> "Optional[str]": + """ + See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding + this method's limitations. + """ + return Scope.last_event_id() + + +@scopemethod +def capture_event( + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) + + +@scopemethod +def capture_message( + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_message( + message, level, scope=scope, **scope_kwargs + ) + + +@scopemethod +def capture_exception( + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Any]" = None, + **scope_kwargs: "Any", +) -> "Optional[str]": + return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) + + +@scopemethod +def add_attachment( + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, +) -> None: + return get_isolation_scope().add_attachment( + bytes, filename, path, content_type, add_to_transactions + ) + + +@scopemethod +def add_breadcrumb( + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", +) -> None: + return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + +@overload +def configure_scope() -> "ContextManager[Scope]": + pass + + +@overload +def configure_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def configure_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + warnings.warn( + "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", + DeprecationWarning, + stacklevel=2, + ) + + scope = get_isolation_scope() + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + +@overload +def push_scope() -> "ContextManager[Scope]": + pass + + +@overload +def push_scope( # noqa: F811 + callback: "Callable[[Scope], None]", +) -> None: + pass + + +def push_scope( # noqa: F811 + callback: "Optional[Callable[[Scope], None]]" = None, +) -> "Optional[ContextManager[Scope]]": + """ + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + warnings.warn( + "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " + "Please consult our migration guide to learn how to migrate to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", + DeprecationWarning, + stacklevel=2, + ) + + if callback is not None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with push_scope() as scope: + callback(scope) + return None + + return _ScopeManager() + + +@scopemethod +def set_attribute(attribute: str, value: "Any") -> None: + """ + Set an attribute. + + Any attributes-based telemetry (logs, metrics) captured in this scope will + include this attribute. + """ + return get_isolation_scope().set_attribute(attribute, value) + + +@scopemethod +def remove_attribute(attribute: str) -> None: + """ + Remove an attribute. + + If the attribute doesn't exist, this function will not have any effect and + it will also not raise an exception. + """ + return get_isolation_scope().remove_attribute(attribute) + + +@scopemethod +def set_tag(key: str, value: "Any") -> None: + return get_isolation_scope().set_tag(key, value) + + +@scopemethod +def set_tags(tags: "Mapping[str, object]") -> None: + return get_isolation_scope().set_tags(tags) + + +@scopemethod +def set_context(key: str, value: "Dict[str, Any]") -> None: + return get_isolation_scope().set_context(key, value) + + +@scopemethod +def set_extra(key: str, value: "Any") -> None: + return get_isolation_scope().set_extra(key, value) + + +@scopemethod +def set_user(value: "Optional[Dict[str, Any]]") -> None: + return get_isolation_scope().set_user(value) + + +@scopemethod +def set_level(value: "LogLevelStr") -> None: + return get_isolation_scope().set_level(value) + + +@clientmethod +def flush( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return get_client().flush(timeout=timeout, callback=callback) + + +@clientmethod +async def flush_async( + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, +) -> None: + return await get_client().flush_async(timeout=timeout, callback=callback) + + +@scopemethod +def start_span( + **kwargs: "Any", +) -> "Span": + return get_current_scope().start_span(**kwargs) + + +@scopemethod +def start_transaction( + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", +) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction on the current scope. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + return get_current_scope().start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + +def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + transaction = get_current_scope().transaction + if transaction is not None: + transaction.set_measurement(name, value, unit) + + +def get_current_span( + scope: "Optional[Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + return tracing_utils.get_current_span(scope) + + +def get_traceparent() -> "Optional[str]": + """ + Returns the traceparent either from the active span or from the scope. + """ + return get_current_scope().get_traceparent() + + +def get_baggage() -> "Optional[str]": + """ + Returns Baggage either from the active span or from the scope. + """ + baggage = get_current_scope().get_baggage() + if baggage is not None: + return baggage.serialize() + + return None + + +def continue_trace( + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", +) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers, op, name, source, origin + ) + + +@scopemethod +def start_session( + session_mode: str = "application", +) -> None: + return get_isolation_scope().start_session(session_mode=session_mode) + + +@scopemethod +def end_session() -> None: + return get_isolation_scope().end_session() + + +@scopemethod +def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: + return get_current_scope().set_transaction_name(name, source) + + +def update_current_span( + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, + data: "Optional[dict[str, Any]]" = None, +) -> None: + """ + Update the current active span with the provided parameters. + + This function allows you to modify properties of the currently active span. + If no span is currently active, this function will do nothing. + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, the span's operation will + remain unchanged. + :type op: str or None + + :param name: The human-readable name/description for the span. This provides + more specific details about what the span represents (e.g., "GET /api/users", + "SELECT * FROM users"). If not provided, the span's name will remain unchanged. + :type name: str or None + + :param data: A dictionary of key-value pairs to add as data to the span. This + data will be merged with any existing span data. If not provided, + no data will be added. + + .. deprecated:: 2.35.0 + Use ``attributes`` instead. The ``data`` parameter will be removed + in a future version. + :type data: dict[str, Union[str, int, float, bool]] or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes will be merged with any existing span data. If not provided, + no attributes will be added. + :type attributes: dict[str, Union[str, int, float, bool]] or None + + :returns: None + + .. versionadded:: 2.35.0 + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP + + sentry_sdk.update_current_span( + op=OP.FUNCTION, + name="process_user_data", + attributes={"user_id": 123, "batch_size": 50} + ) + """ + if isinstance(_get_current_streamed_span(), StreamedSpan): + warnings.warn( + "The `update_current_span` API isn't available in streaming mode. " + "Retrieve the current span with get_current_span() and use its API " + "directly.", + DeprecationWarning, + stacklevel=2, + ) + return + + current_span = get_current_span() + + if current_span is None: + return + + if op is not None: + current_span.op = op + + if name is not None: + # internally it is still description + current_span.description = name + + if data is not None and attributes is not None: + raise ValueError( + "Cannot provide both `data` and `attributes`. Please use only `attributes`." + ) + + if data is not None: + warnings.warn( + "The `data` parameter is deprecated. Please use `attributes` instead.", + DeprecationWarning, + stacklevel=2, + ) + attributes = data + + if attributes is not None: + current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py new file mode 100644 index 0000000000..4d69d3acf2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py @@ -0,0 +1,71 @@ +import mimetypes +import os +from typing import TYPE_CHECKING + +from sentry_sdk.envelope import Item, PayloadRef + +if TYPE_CHECKING: + from typing import Callable, Optional, Union + + +class Attachment: + """Additional files/data to send along with an event. + + This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. + config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with + all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are + captured within the ``Scope``. + + To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for + ``add_attachment`` are the same as the parameters for this class's constructor. + + :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless + ``path`` is provided. + :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. + :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. + :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` + parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. + :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. + """ + + def __init__( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + if bytes is None and path is None: + raise TypeError("path or raw bytes required for attachment") + if filename is None and path is not None: + filename = os.path.basename(path) + if filename is None: + raise TypeError("filename is required for attachment") + if content_type is None: + content_type = mimetypes.guess_type(filename)[0] + self.bytes = bytes + self.filename = filename + self.path = path + self.content_type = content_type + self.add_to_transactions = add_to_transactions + + def to_envelope_item(self) -> "Item": + """Returns an envelope item for this attachment.""" + payload: "Union[None, PayloadRef, bytes]" = None + if self.bytes is not None: + if callable(self.bytes): + payload = self.bytes() + else: + payload = self.bytes + else: + payload = PayloadRef(path=self.path) + return Item( + payload=payload, + type="attachment", + content_type=self.content_type, + filename=self.filename, + ) + + def __repr__(self) -> str: + return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py new file mode 100644 index 0000000000..92cb42277e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py @@ -0,0 +1,1479 @@ +import json +import os +import platform +import random +import socket +import sys +import uuid +import warnings +from collections.abc import Iterable, Mapping +from datetime import datetime, timezone +from importlib import import_module +from typing import TYPE_CHECKING, Dict, List, cast, overload + +from sentry_sdk._compat import check_uwsgi_thread_support +from sentry_sdk._metrics_batcher import MetricsBatcher +from sentry_sdk._span_batcher import SpanBatcher +from sentry_sdk.consts import ( + DEFAULT_MAX_VALUE_LENGTH, + DEFAULT_OPTIONS, + INSTRUMENTER, + SPANDATA, + SPANSTATUS, + VERSION, + ClientConstructor, +) +from sentry_sdk.data_collection import ( + _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 +from sentry_sdk.monitor import Monitor +from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler +from sentry_sdk.profiler.transaction_profiler import ( + Profile, + has_profiling_enabled, + setup_profiler, +) +from sentry_sdk.scrubber import EventScrubber +from sentry_sdk.serializer import serialize +from sentry_sdk.sessions import SessionFlusher +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import trace +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import ( + AsyncHttpTransport, + HttpTransportCore, + make_transport, +) +from sentry_sdk.utils import ( + AnnotatedValue, + ContextVar, + capture_internal_exceptions, + current_stacktrace, + datetime_from_isoformat, + env_to_bool, + format_timestamp, + get_before_send_log, + get_before_send_metric, + get_before_send_span, + get_default_release, + get_sdk_name, + get_type_name, + handle_in_app, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union + + from sentry_sdk._log_batcher import LogBatcher + from sentry_sdk._metrics_batcher import MetricsBatcher + from sentry_sdk._types import ( + Event, + EventDataCategory, + Hint, + Log, + Metric, + SDKInfo, + SerializedAttributeValue, + ) + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.session import Session + from sentry_sdk.spotlight import SpotlightClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.transport import Item, Transport + from sentry_sdk.utils import Dsn + + I = TypeVar("I", bound=Integration) # noqa: E741 + +_client_init_debug = ContextVar("client_init_debug") + + +SDK_INFO: "SDKInfo" = { + "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() + "version": VERSION, + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], +} + + +def _serialized_v1_attribute_to_serialized_v2_attribute( + attribute_value: "Any", +) -> "Optional[SerializedAttributeValue]": + if isinstance(attribute_value, bool): + return { + "value": attribute_value, + "type": "boolean", + } + + if isinstance(attribute_value, int): + return { + "value": attribute_value, + "type": "integer", + } + + if isinstance(attribute_value, float): + return { + "value": attribute_value, + "type": "double", + } + + if isinstance(attribute_value, str): + return { + "value": attribute_value, + "type": "string", + } + + if isinstance(attribute_value, list): + if not attribute_value: + return {"value": [], "type": "array"} + + ty = type(attribute_value[0]) + if ty in (int, str, bool, float) and all( + type(v) is ty for v in attribute_value + ): + return { + "value": attribute_value, + "type": "array", + } + + # Types returned when the serializer for V1 span attributes recurses into some container types. + if isinstance(attribute_value, (dict, list)): + return { + "value": json.dumps(attribute_value), + "type": "string", + } + + return None + + +def _serialized_v1_span_to_serialized_v2_span( + span: "dict[str, Any]", event: "Event" +) -> "dict[str, Any]": + # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". + res: "dict[str, Any]" = { + "status": SpanStatus.OK.value, + "is_segment": False, + } + + if "trace_id" in span: + res["trace_id"] = span["trace_id"] + + if "span_id" in span: + res["span_id"] = span["span_id"] + + if "description" in span: + description = span["description"] + + if description is None and "op" in span: + description = span["op"] + + res["name"] = description + + if "start_timestamp" in span: + start_timestamp = None + try: + start_timestamp = datetime_from_isoformat(span["start_timestamp"]) + except Exception: + pass + + if start_timestamp is not None: + res["start_timestamp"] = start_timestamp.timestamp() + + if "timestamp" in span: + end_timestamp = None + try: + end_timestamp = datetime_from_isoformat(span["timestamp"]) + except Exception: + pass + + if end_timestamp is not None: + res["end_timestamp"] = end_timestamp.timestamp() + + if "parent_span_id" in span: + res["parent_span_id"] = span["parent_span_id"] + + if "status" in span and span["status"] != SPANSTATUS.OK: + res["status"] = "error" + + attributes: "Dict[str, Any]" = {} + + if "op" in span: + attributes["sentry.op"] = span["op"] + if "origin" in span: + attributes["sentry.origin"] = span["origin"] + + span_data = span.get("data") + if isinstance(span_data, dict): + attributes.update(span_data) + + span_tags = span.get("tags") + if isinstance(span_tags, dict): + attributes.update(span_tags) + + # See Scope._apply_user_attributes_to_telemetry() for user attributes. + user = event.get("user") + if isinstance(user, dict): + if "id" in user: + attributes["user.id"] = user["id"] + if "username" in user: + attributes["user.name"] = user["username"] + if "email" in user: + attributes["user.email"] = user["email"] + + # See Scope.set_global_attributes() for release, environment, and SDK metadata. + if "release" in event: + attributes["sentry.release"] = event["release"] + if "environment" in event: + attributes["sentry.environment"] = event["environment"] + if "server_name" in event: + attributes["server.address"] = event["server_name"] + if "transaction" in event: + attributes["sentry.segment.name"] = event["transaction"] + + trace_context = event.get("contexts", {}).get("trace", {}) + if "span_id" in trace_context: + attributes["sentry.segment.id"] = trace_context["span_id"] + + sdk_info = event.get("sdk") + if isinstance(sdk_info, dict): + if "name" in sdk_info: + attributes["sentry.sdk.name"] = sdk_info["name"] + if "version" in sdk_info: + attributes["sentry.sdk.version"] = sdk_info["version"] + + attributes["process.runtime.name"] = platform.python_implementation() + attributes["process.runtime.version"] = ( + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + ) + + if not attributes: + return res + + res["attributes"] = {} + for key, value in attributes.items(): + converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) + if converted_value is None: + continue + + res["attributes"][key] = converted_value + + # Remove redundant attribute, as status is stored in the status field. + if "status" in res["attributes"]: + del res["attributes"]["status"] + + return res + + +def _split_gen_ai_spans( + event_opt: "Event", +) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": + if "spans" not in event_opt: + return None + + spans: "Any" = event_opt["spans"] + if isinstance(spans, AnnotatedValue): + spans = spans.value + + if not isinstance(spans, Iterable): + return None + + non_gen_ai_spans = [] + gen_ai_spans = [] + for span in spans: + if not isinstance(span, dict): + non_gen_ai_spans.append(span) + continue + + span_op = span.get("op") + if isinstance(span_op, str) and span_op.startswith("gen_ai."): + gen_ai_spans.append(span) + else: + non_gen_ai_spans.append(span) + + return non_gen_ai_spans, gen_ai_spans + + +def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": + if args and (isinstance(args[0], (bytes, str)) or args[0] is None): + dsn: "Optional[str]" = args[0] + args = args[1:] + else: + dsn = None + + if len(args) > 1: + raise TypeError("Only single positional argument is expected") + + rv = dict(DEFAULT_OPTIONS) + options = dict(*args, **kwargs) + if dsn is not None and options.get("dsn") is None: + options["dsn"] = dsn + + for key, value in options.items(): + if key not in rv: + raise TypeError("Unknown option %r" % (key,)) + + rv[key] = value + + if rv["dsn"] is None: + rv["dsn"] = os.environ.get("SENTRY_DSN") + + if rv["release"] is None: + rv["release"] = get_default_release() + + if rv["environment"] is None: + rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" + + if rv["debug"] is None: + rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False + + if rv["server_name"] is None and hasattr(socket, "gethostname"): + rv["server_name"] = socket.gethostname() + + if rv["instrumenter"] is None: + rv["instrumenter"] = INSTRUMENTER.SENTRY + + if rv["project_root"] is None: + try: + project_root = os.getcwd() + except Exception: + project_root = None + + rv["project_root"] = project_root + + 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"] + ) + + if rv["socket_options"] and not isinstance(rv["socket_options"], list): + logger.warning( + "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." + ) + rv["socket_options"] = None + + if rv["keep_alive"] is None: + rv["keep_alive"] = ( + env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False + ) + + if rv["enable_tracing"] is not None: + warnings.warn( + "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", + DeprecationWarning, + stacklevel=2, + ) + + if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): + warnings.warn( + "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", + stacklevel=2, + ) + + return rv + + +try: + # Python 3.6+ + module_not_found_error = ModuleNotFoundError +except Exception: + # Older Python versions + module_not_found_error = ImportError # type: ignore + + +class BaseClient: + """ + .. versionadded:: 2.0.0 + + The basic definition of a client that is used for sending data to Sentry. + """ + + spotlight: "Optional[SpotlightClient]" = None + + def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: + self.options: "Dict[str, Any]" = ( + options if options is not None else DEFAULT_OPTIONS + ) + + self.transport: "Optional[Transport]" = None + self.monitor: "Optional[Monitor]" = None + self.log_batcher: "Optional[LogBatcher]" = None + self.metrics_batcher: "Optional[MetricsBatcher]" = None + self.span_batcher: "Optional[SpanBatcher]" = None + self.integrations: "dict[str, Integration]" = {} + + def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": + return {"options": {}} + + def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: + pass + + @property + def dsn(self) -> "Optional[str]": + return None + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + return None + + def should_send_default_pii(self) -> bool: + return False + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return False + + def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + return None + + def _capture_log(self, log: "Log", scope: "Scope") -> None: + pass + + def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: + pass + + def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: + pass + + def capture_session(self, *args: "Any", **kwargs: "Any") -> None: + return None + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, name_or_class: "Union[str, type[Integration]]" + ) -> "Optional[Integration]": + return None + + def close(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def flush(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def close_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: + return None + + def __enter__(self) -> "BaseClient": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + return None + + +class NonRecordingClient(BaseClient): + """ + .. versionadded:: 2.0.0 + + A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. + """ + + pass + + +class _Client(BaseClient): + """ + The client is internally responsible for capturing the events and + forwarding them to sentry through the configured transport. It takes + the client options as keyword arguments and optionally the DSN as first + argument. + + Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) + """ + + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + super(_Client, self).__init__(options=get_options(*args, **kwargs)) + self._init_impl() + + def __getstate__(self) -> "Any": + return {"options": self.options} + + def __setstate__(self, state: "Any") -> None: + self.options = state["options"] + self._init_impl() + + def _setup_instrumentation( + self, functions_to_trace: "Sequence[Dict[str, str]]" + ) -> None: + """ + Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. + """ + for function in functions_to_trace: + class_name = None + function_qualname = function["qualified_name"] + + if "." not in function_qualname: + logger.warning( + "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", + function_qualname, + ) + continue + + module_name, function_name = function_qualname.rsplit(".", 1) + + try: + # Try to import module and function + # ex: "mymodule.submodule.funcname" + + module_obj = import_module(module_name) + function_obj = getattr(module_obj, function_name) + setattr(module_obj, function_name, trace(function_obj)) + logger.debug("Enabled tracing for %s", function_qualname) + except module_not_found_error: + try: + # Try to import a class + # ex: "mymodule.submodule.MyClassName.member_function" + + module_name, class_name = module_name.rsplit(".", 1) + module_obj = import_module(module_name) + class_obj = getattr(module_obj, class_name) + function_obj = getattr(class_obj, function_name) + function_type = type(class_obj.__dict__[function_name]) + traced_function = trace(function_obj) + + if function_type in (staticmethod, classmethod): + traced_function = staticmethod(traced_function) + + setattr(class_obj, function_name, traced_function) + setattr(module_obj, class_name, class_obj) + logger.debug("Enabled tracing for %s", function_qualname) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + except Exception as e: + logger.warning( + "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", + function_qualname, + e, + ) + + def _init_impl(self) -> None: + old_debug = _client_init_debug.get(False) + + def _capture_envelope(envelope: "Envelope") -> None: + if self.spotlight is not None: + self.spotlight.capture_envelope(envelope) + if self.transport is not None: + self.transport.capture_envelope(envelope) + + def _record_lost_event( + reason: str, + data_category: "EventDataCategory", + item: "Optional[Item]" = None, + quantity: int = 1, + ) -> None: + if self.transport is not None: + self.transport.record_lost_event( + reason=reason, + data_category=data_category, + item=item, + quantity=quantity, + ) + + try: + _client_init_debug.set(self.options["debug"]) + self.transport = make_transport(self.options) + + self.monitor = None + if self.transport: + if self.options["enable_backpressure_handling"]: + self.monitor = Monitor(self.transport) + + # Setup Spotlight before creating batchers so _capture_envelope can use it. + # setup_spotlight handles all config/env var resolution per the SDK spec. + from sentry_sdk.spotlight import setup_spotlight + + self.spotlight = setup_spotlight(self.options) + if self.spotlight is not None and not self.options["dsn"]: + sample_all = lambda *_args, **_kwargs: 1.0 + self.options["send_default_pii"] = True + 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, + ) + + self.session_flusher = SessionFlusher(capture_func=_capture_envelope) + + self.log_batcher = None + + if has_logs_enabled(self.options): + from sentry_sdk._log_batcher import LogBatcher + + self.log_batcher = LogBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.metrics_batcher = None + if has_metrics_enabled(self.options): + self.metrics_batcher = MetricsBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + self.span_batcher = None + if has_span_streaming_enabled(self.options): + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + + max_request_body_size = ("always", "never", "small", "medium") + if self.options["max_request_body_size"] not in max_request_body_size: + raise ValueError( + "Invalid value for max_request_body_size. Must be one of {}".format( + max_request_body_size + ) + ) + + if self.options["_experiments"].get("otel_powered_performance", False): + logger.debug( + "[OTel] Enabling experimental OTel-powered performance monitoring." + ) + self.options["instrumenter"] = INSTRUMENTER.OTEL + if ( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" + not in _DEFAULT_INTEGRATIONS + ): + _DEFAULT_INTEGRATIONS.append( + "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", + ) + + self.integrations = setup_integrations( + self.options["integrations"], + with_defaults=self.options["default_integrations"], + with_auto_enabling_integrations=self.options[ + "auto_enabling_integrations" + ], + disabled_integrations=self.options["disabled_integrations"], + options=self.options, + ) + + sdk_name = get_sdk_name(list(self.integrations.keys())) + SDK_INFO["name"] = sdk_name + logger.debug("Setting SDK name to '%s'", sdk_name) + + if has_profiling_enabled(self.options): + try: + setup_profiler(self.options) + except Exception as e: + logger.debug("Can not set up profiler. (%s)", e) + else: + try: + setup_continuous_profiler( + self.options, + sdk_info=SDK_INFO, + capture_func=_capture_envelope, + ) + except Exception as e: + logger.debug("Can not set up continuous profiler. (%s)", e) + + finally: + _client_init_debug.set(old_debug) + + self._setup_instrumentation(self.options.get("functions_to_trace", [])) + + if ( + self.monitor + or self.log_batcher + or self.metrics_batcher + or self.span_batcher + or has_profiling_enabled(self.options) + or isinstance(self.transport, HttpTransportCore) + ): + # If we have anything on that could spawn a background thread, we + # need to check if it's safe to use them. + check_uwsgi_thread_support() + + def is_active(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client is active (able to send data to Sentry) + """ + return True + + def should_send_default_pii(self) -> bool: + """ + .. versionadded:: 2.0.0 + + Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. + """ + return self.options.get("send_default_pii") or False + + @property + def dsn(self) -> "Optional[str]": + """Returns the configured DSN as string.""" + return self.options["dsn"] + + @property + def parsed_dsn(self) -> "Optional[Dsn]": + """Returns the configured parsed DSN object.""" + return self.transport.parsed_dsn if self.transport else None + + def _prepare_event( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]", + ) -> "Optional[Event]": + previous_total_spans: "Optional[int]" = None + previous_total_breadcrumbs: "Optional[int]" = None + + if event.get("timestamp") is None: + event["timestamp"] = datetime.now(timezone.utc) + + is_transaction = event.get("type") == "transaction" + + if scope is not None: + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + event_ = scope.apply_to_event(event, hint, self.options) + + # one of the event/error processors returned None + if event_ is None: + if self.transport: + self.transport.record_lost_event( + "event_processor", + data_category=("transaction" if is_transaction else "error"), + ) + if is_transaction: + self.transport.record_lost_event( + "event_processor", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + return None + + event = event_ + spans_delta = spans_before - len( + cast(List[Dict[str, object]], event.get("spans", [])) + ) + span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) + + if is_transaction and self.transport is not None: + if spans_delta > 0: + self.transport.record_lost_event( + "event_processor", data_category="span", quantity=spans_delta + ) + if span_recorder_dropped_spans > 0: + self.transport.record_lost_event( + "buffer_overflow", + data_category="span", + quantity=span_recorder_dropped_spans, + ) + + dropped_spans: int = span_recorder_dropped_spans + spans_delta + if dropped_spans > 0: + previous_total_spans = spans_before + dropped_spans + if scope._n_breadcrumbs_truncated > 0: + breadcrumbs = event.get("breadcrumbs", {}) + values = ( + breadcrumbs.get("values", []) + if not isinstance(breadcrumbs, AnnotatedValue) + else [] + ) + previous_total_breadcrumbs = ( + len(values) + scope._n_breadcrumbs_truncated + ) + + if ( + not is_transaction + and self.options["attach_stacktrace"] + and "exception" not in event + and "stacktrace" not in event + and "threads" not in event + ): + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=self.options.get( + "include_local_variables", True + ), + max_value_length=self.options.get( + "max_value_length", DEFAULT_MAX_VALUE_LENGTH + ), + ), + "crashed": False, + "current": True, + } + ] + } + + for key in "release", "environment", "server_name", "dist": + if event.get(key) is None and self.options[key] is not None: + event[key] = str(self.options[key]).strip() + if event.get("sdk") is None: + sdk_info = dict(SDK_INFO) + sdk_info["integrations"] = sorted(self.integrations.keys()) + event["sdk"] = sdk_info + + if event.get("platform") is None: + event["platform"] = "python" + + event = handle_in_app( + event, + self.options["in_app_exclude"], + self.options["in_app_include"], + self.options["project_root"], + ) + + if event is not None: + event_scrubber = self.options["event_scrubber"] + if event_scrubber: + event_scrubber.scrub_event(event) + + if scope is not None and scope._gen_ai_original_message_count: + spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) + if isinstance(spans, list): + for span in spans: + span_id = span.get("span_id", None) + span_data = span.get("data", {}) + if ( + span_id + and span_id in scope._gen_ai_original_message_count + and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data + ): + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( + span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], + {"len": scope._gen_ai_original_message_count[span_id]}, + ) + if previous_total_spans is not None: + event["spans"] = AnnotatedValue( + event.get("spans", []), {"len": previous_total_spans} + ) + if previous_total_breadcrumbs is not None: + event["breadcrumbs"] = AnnotatedValue( + event.get("breadcrumbs", {"values": []}), + {"len": previous_total_breadcrumbs}, + ) + + # Postprocess the event here so that annotated types do + # generally not surface in before_send + if event is not None: + event = cast( + "Event", + serialize( + cast("Dict[str, Any]", event), + max_request_body_size=self.options.get("max_request_body_size"), + max_value_length=self.options.get("max_value_length"), + custom_repr=self.options.get("custom_repr"), + ), + ) + + before_send = self.options["before_send"] + if ( + before_send is not None + and event is not None + and event.get("type") != "transaction" + ): + new_event = None + with capture_internal_exceptions(): + new_event = before_send(event, hint or {}) + if new_event is None: + logger.info("before send dropped event") + if self.transport: + self.transport.record_lost_event( + "before_send", data_category="error" + ) + + # If this is an exception, reset the DedupeIntegration. It still + # remembers the dropped exception as the last exception, meaning + # that if the same exception happens again and is not dropped + # in before_send, it'd get dropped by DedupeIntegration. + if event.get("exception"): + DedupeIntegration.reset_last_seen() + + event = new_event + + before_send_transaction = self.options["before_send_transaction"] + if ( + before_send_transaction is not None + and event is not None + and event.get("type") == "transaction" + ): + new_event = None + spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) + with capture_internal_exceptions(): + new_event = before_send_transaction(event, hint or {}) + if new_event is None: + logger.info("before send transaction dropped event") + if self.transport: + self.transport.record_lost_event( + reason="before_send", data_category="transaction" + ) + self.transport.record_lost_event( + reason="before_send", + data_category="span", + quantity=spans_before + 1, # +1 for the transaction itself + ) + else: + spans_delta = spans_before - len(new_event.get("spans", [])) + if spans_delta > 0 and self.transport is not None: + self.transport.record_lost_event( + reason="before_send", data_category="span", quantity=spans_delta + ) + + event = new_event + + return event + + def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: + exc_info = hint.get("exc_info") + if exc_info is None: + return False + + error = exc_info[0] + error_type_name = get_type_name(exc_info[0]) + error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) + + for ignored_error in self.options["ignore_errors"]: + # String types are matched against the type name in the + # exception only + if isinstance(ignored_error, str): + if ignored_error == error_full_name or ignored_error == error_type_name: + return True + else: + if issubclass(error, ignored_error): + return True + + return False + + def _should_capture( + self, + event: "Event", + hint: "Hint", + scope: "Optional[Scope]" = None, + ) -> bool: + # Transactions are sampled independent of error events. + is_transaction = event.get("type") == "transaction" + if is_transaction: + return True + + ignoring_prevents_recursion = scope is not None and not scope._should_capture + if ignoring_prevents_recursion: + return False + + ignored_by_config_option = self._is_ignored_error(event, hint) + if ignored_by_config_option: + return False + + return True + + def _should_sample_error( + self, + event: "Event", + hint: "Hint", + ) -> bool: + error_sampler = self.options.get("error_sampler", None) + + if callable(error_sampler): + with capture_internal_exceptions(): + sample_rate = error_sampler(event, hint) + else: + sample_rate = self.options["sample_rate"] + + try: + not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate + except NameError: + logger.warning( + "The provided error_sampler raised an error. Defaulting to sampling the event." + ) + + # If the error_sampler raised an error, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + except TypeError: + parameter, verb = ( + ("error_sampler", "returned") + if callable(error_sampler) + else ("sample_rate", "contains") + ) + logger.warning( + "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." + % (parameter, verb, repr(sample_rate)) + ) + + # If the sample_rate has an invalid value, we should sample the event, since the default behavior + # (when no sample_rate or error_sampler is provided) is to sample all events. + not_in_sample_rate = False + + if not_in_sample_rate: + # because we will not sample this event, record a "lost event". + if self.transport: + self.transport.record_lost_event("sample_rate", data_category="error") + + return False + + return True + + def _update_session_from_event( + self, + session: "Session", + event: "Event", + ) -> None: + crashed = False + errored = False + user_agent = None + + exceptions = (event.get("exception") or {}).get("values") + if exceptions: + errored = True + for error in exceptions: + if isinstance(error, AnnotatedValue): + error = error.value or {} + mechanism = error.get("mechanism") + if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: + crashed = True + break + + user = event.get("user") + + if session.user_agent is None: + headers = (event.get("request") or {}).get("headers") + headers_dict = headers if isinstance(headers, dict) else {} + for k, v in headers_dict.items(): + if k.lower() == "user-agent": + user_agent = v + break + + session.update( + status="crashed" if crashed else None, + user=user, + user_agent=user_agent, + errors=session.errors + (errored or crashed), + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + ) -> "Optional[str]": + """Captures an event. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + + :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. + """ + hint = dict(hint or ()) + + if not self._should_capture(event, hint, scope): + return None + + profile = event.pop("profile", None) + + event_id = event.get("event_id") + if event_id is None: + event["event_id"] = event_id = uuid.uuid4().hex + + span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) + event_opt = self._prepare_event(event, hint, scope) + if event_opt is None: + return None + + # whenever we capture an event we also check if the session needs + # to be updated based on that information. + session = scope._session if scope else None + if session: + self._update_session_from_event(session, event) + + is_transaction = event_opt.get("type") == "transaction" + is_checkin = event_opt.get("type") == "check_in" + + if ( + not is_transaction + and not is_checkin + and not self._should_sample_error(event, hint) + ): + return None + + attachments = hint.get("attachments") + + trace_context = event_opt.get("contexts", {}).get("trace") or {} + dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) + + headers: "dict[str, object]" = { + "event_id": event_opt["event_id"], + "sent_at": format_timestamp(datetime.now(timezone.utc)), + } + + if dynamic_sampling_context: + headers["trace"] = dynamic_sampling_context + + envelope = Envelope(headers=headers) + + if is_transaction and isinstance(profile, Profile): + envelope.add_profile(profile.to_json(event_opt, self.options)) + + if is_transaction and not span_recorder_has_gen_ai_span: + envelope.add_transaction(event_opt) + elif is_transaction: + split_spans = _split_gen_ai_spans(event_opt) + if split_spans is None or not split_spans[1]: + envelope.add_transaction(event_opt) + else: + non_gen_ai_spans, gen_ai_spans = split_spans + + event_opt["spans"] = non_gen_ai_spans + envelope.add_transaction(event_opt) + + converted_gen_ai_spans = [ + _serialized_v1_span_to_serialized_v2_span(span, event_opt) + for span in gen_ai_spans + if isinstance(span, dict) + ] + + envelope.add_item( + Item( + type=SpanBatcher.TYPE, + content_type=SpanBatcher.CONTENT_TYPE, + headers={ + "item_count": len(converted_gen_ai_spans), + }, + payload=PayloadRef( + json={ + "version": 2, + "items": converted_gen_ai_spans, + }, + ), + ) + ) + + elif is_checkin: + envelope.add_checkin(event_opt) + else: + envelope.add_event(event_opt) + + for attachment in attachments or (): + envelope.add_item(attachment.to_envelope_item()) + + return_value = None + if self.spotlight: + self.spotlight.capture_envelope(envelope) + return_value = event_id + + if self.transport is not None: + self.transport.capture_envelope(envelope) + return_value = event_id + + return return_value + + def _capture_telemetry( + self, + telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", + ty: str, + scope: "Scope", + ) -> None: + """ + Capture attributes-based telemetry (logs, metrics, streamed spans). + + Apply any attributes set on the scope to it, and run the user's + before_send_{telemetry} on it, if applicable. + """ + if telemetry is None: + return + + scope.apply_to_telemetry(telemetry) + + before_send = None + + if ty == "log": + before_send = get_before_send_log(self.options) + serialized = telemetry + + elif ty == "metric": + before_send = get_before_send_metric(self.options) + serialized = telemetry + + elif ty == "span": + before_send = get_before_send_span(self.options) + serialized = telemetry._to_json() # type: ignore[union-attr] + + if before_send is not None: + serialized = before_send(serialized, {}) # type: ignore[arg-type] + + if ty in ("log", "metric"): + # Logs and metrics can be dropped in their respective + # before_send, so if we get None, don't queue them for sending. + if serialized is None: + return + + elif ty == "span" and isinstance(telemetry, StreamedSpan): + # Spans can't be dropped in before_send_span by design. They can + # be altered though (e.g. to sanitize). Only allow changes to + # name and attributes. + if isinstance(serialized, dict) and "name" in serialized: + telemetry.name = serialized["name"] # type: ignore[typeddict-item] + telemetry._attributes = {} + for k, v in (serialized.get("attributes") or {}).items(): + telemetry.set_attribute(k, v) + + else: + logger.debug( + "[Tracing] Invalid return value from before_send_span. Keeping original span." + ) + + serialized = telemetry._to_json() + + batcher = None + if ty == "log": + batcher = self.log_batcher + + elif ty == "metric": + batcher = self.metrics_batcher + + elif ty == "span": + # We need a reference to the segment span in the batcher to populate + # the dynamic sampling context (DSC) + serialized["_segment_span"] = telemetry._segment # type: ignore + batcher = self.span_batcher + + if batcher is not None: + batcher.add(serialized) # type: ignore + + def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: + self._capture_telemetry(log, "log", scope) + + def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: + self._capture_telemetry(metric, "metric", scope) + + def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: + self._capture_telemetry(span, "span", scope) + + def capture_session( + self, + session: "Session", + ) -> None: + if not session.release: + logger.info("Discarded session update because of missing release") + else: + self.session_flusher.add_session(session) + + if TYPE_CHECKING: + + @overload + def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... + + @overload + def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Optional[Integration]": + """Returns the integration for this client by name or class. + If the client does not have that integration then `None` is returned. + """ + if isinstance(name_or_class, str): + integration_name = name_or_class + elif name_or_class.identifier is not None: + integration_name = name_or_class.identifier + else: + raise ValueError("Integration has no name") + + return self.integrations.get(integration_name) + + def _has_async_transport(self) -> bool: + """Check if the current transport is async.""" + return isinstance(self.transport, AsyncHttpTransport) + + @property + def _batchers(self) -> "tuple[Any, ...]": + return tuple( + b + for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) + if b is not None + ) + + def _close_components(self) -> None: + """Kill all client components in the correct order.""" + self.session_flusher.kill() + for b in self._batchers: + b.kill() + if self.monitor: + self.monitor.kill() + + def _flush_components(self) -> None: + """Flush all client components.""" + self.session_flusher.flush() + for b in self._batchers: + b.flush() + + def close( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush`. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "close() used with AsyncHttpTransport. Use close_async() instead.", + stacklevel=2, + ) + self._flush_components() + else: + self.flush(timeout=timeout, callback=callback) + self._close_components() + self.transport.kill() + self.transport = None + + async def close_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously close the client and shut down the transport. Arguments have the same + semantics as :py:meth:`Client.flush_async`. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "close_async() used with non-async transport, aborting. Please use close() instead." + ) + return + await self.flush_async(timeout=timeout, callback=callback) + self._close_components() + kill_task = self.transport.kill() # type: ignore + if kill_task is not None: + await kill_task + self.transport = None + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if self._has_async_transport(): + warnings.warn( + "flush() used with AsyncHttpTransport. Use flush_async() instead.", + stacklevel=2, + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + + self.transport.flush(timeout=timeout, callback=callback) + + async def flush_async( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + Asynchronously wait for the current events to be sent. + + :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. + + :param callback: Is invoked with the number of pending events and the configured timeout. + """ + if self.transport is not None: + if not self._has_async_transport(): + logger.debug( + "flush_async() used with non-async transport, aborting. Please use flush() instead." + ) + return + if timeout is None: + timeout = self.options["shutdown_timeout"] + self._flush_components() + flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore + if flush_task is not None: + await flush_task + + def __enter__(self) -> "_Client": + return self + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + self.close() + + async def __aenter__(self) -> "_Client": + return self + + async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + await self.close_async() + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Make mypy, PyCharm and other static analyzers think `get_options` is a + # type to have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `Dict[str, Any]` to tell static analyzers about the return type. + + class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 + pass + + class Client(ClientConstructor, _Client): + pass + +else: + # Alias `get_options` for actual usage. Go through the lambda indirection + # to throw PyCharm off of the weakly typed signature (it would otherwise + # discover both the weakly typed signature of `_init` and our faked `init` + # type). + + get_options = (lambda: _get_options)() + Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py new file mode 100644 index 0000000000..759898f6ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py @@ -0,0 +1,1802 @@ +import itertools +from enum import Enum +from typing import TYPE_CHECKING + +DEFAULT_MAX_VALUE_LENGTH = None + +DEFAULT_MAX_STACK_FRAMES = 100 +DEFAULT_ADD_FULL_STACK = False + + +# Also needs to be at the top to prevent circular import +class EndpointType(Enum): + """ + The type of an endpoint. This is an enum, rather than a constant, for historical reasons + (the old /store endpoint). The enum also preserve future compatibility, in case we ever + have a new endpoint. + """ + + ENVELOPE = "envelope" + OTLP_TRACES = "integration/otlp/v1/traces" + + +class CompressionAlgo(Enum): + GZIP = "gzip" + BROTLI = "br" + + +if TYPE_CHECKING: + from typing import ( + AbstractSet, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + ) + + from typing_extensions import Literal, TypedDict + + import sentry_sdk + from sentry_sdk._types import ( + BreadcrumbProcessor, + ContinuousProfilerMode, + DataCollectionUserOptions, + Event, + EventProcessor, + Hint, + IgnoreSpansConfig, + Log, + Metric, + ProfilerMode, + SpanJSON, + TracesSampler, + TransactionProcessor, + ) + + # Experiments are feature flags to enable and disable certain unstable SDK + # functionality. Changing them from the defaults (`None`) in production + # code is highly discouraged. They are not subject to any stability + # guarantees such as the ones from semantic versioning. + Experiments = TypedDict( + "Experiments", + { + "max_spans": Optional[int], + "max_flags": Optional[int], + "record_sql_params": Optional[bool], + "continuous_profiling_auto_start": Optional[bool], + "continuous_profiling_mode": Optional[ContinuousProfilerMode], + "otel_powered_performance": Optional[bool], + "transport_zlib_compression_level": Optional[int], + "transport_compression_level": Optional[int], + "transport_compression_algo": Optional[CompressionAlgo], + "transport_num_pools": Optional[int], + "transport_http2": Optional[bool], + "transport_async": Optional[bool], + "enable_logs": Optional[bool], + "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], + "enable_metrics": Optional[bool], + "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], + "trace_lifecycle": Optional[Literal["static", "stream"]], + "ignore_spans": Optional[IgnoreSpansConfig], + "before_send_span": Optional[ + Callable[[SpanJSON, Hint], Optional[SpanJSON]] + ], + "suppress_asgi_chained_exceptions": Optional[bool], + "data_collection": Optional[DataCollectionUserOptions], + }, + total=False, + ) + +DEFAULT_QUEUE_SIZE = 100 +DEFAULT_MAX_BREADCRUMBS = 100 +MATCH_ALL = r".*" + +FALSE_VALUES = [ + "false", + "no", + "off", + "n", + "0", +] + + +class SPANTEMPLATE(str, Enum): + DEFAULT = "default" + AI_AGENT = "ai_agent" + AI_TOOL = "ai_tool" + AI_CHAT = "ai_chat" + + def __str__(self) -> str: + return self.value + + +class INSTRUMENTER: + SENTRY = "sentry" + OTEL = "otel" + + +class SPANNAME: + DB_COMMIT = "COMMIT" + DB_ROLLBACK = "ROLLBACK" + + +class SPANDATA: + """ + Additional information describing the type of the span. + See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ + """ + + AI_CITATIONS = "ai.citations" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + References or sources cited by the AI model in its response. + Example: ["Smith et al. 2020", "Jones 2019"] + """ + + AI_DOCUMENTS = "ai.documents" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Documents or content chunks used as context for the AI model. + Example: ["doc1.txt", "doc2.pdf"] + """ + + AI_FINISH_REASON = "ai.finish_reason" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. + + The reason why the model stopped generating. + Example: "length" + """ + + AI_FREQUENCY_PENALTY = "ai.frequency_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_FUNCTION_CALL = "ai.function_call" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_GENERATION_ID = "ai.generation_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. + + Unique identifier for the completion. + Example: "gen_123abc" + """ + + AI_INPUT_MESSAGES = "ai.input_messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. + + The input messages to an LLM call. + Example: [{"role": "user", "message": "hello"}] + """ + + AI_LOGIT_BIAS = "ai.logit_bias" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the logit bias + """ + + AI_METADATA = "ai.metadata" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Extra metadata passed to an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_MODEL_ID = "ai.model_id" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. + + The unique descriptor of the model being executed. + Example: gpt-4 + """ + + AI_PIPELINE_NAME = "ai.pipeline.name" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. + + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + AI_PREAMBLE = "ai.preamble" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the preamble parameter. + Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. + Example: "You are now a clown." + """ + + AI_PRESENCE_PENALTY = "ai.presence_penalty" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. + + Used to reduce repetitiveness of generated tokens. + Example: 0.5 + """ + + AI_RAW_PROMPTING = "ai.raw_prompting" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Minimize pre-processing done to the prompt sent to the LLM. + Example: true + """ + + AI_RESPONSE_FORMAT = "ai.response_format" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + For an AI model call, the format of the response + """ + + AI_RESPONSES = "ai.responses" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. + + The responses to an AI model call. Always as a list. + Example: ["hello", "world"] + """ + + AI_SEARCH_QUERIES = "ai.search_queries" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Queries used to search for relevant context or documents. + Example: ["climate change effects", "renewable energy"] + """ + + AI_SEARCH_REQUIRED = "ai.is_search_required" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Boolean indicating if the model needs to perform a search. + Example: true + """ + + AI_SEARCH_RESULTS = "ai.search_results" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Results returned from search queries for context. + Example: ["Result 1", "Result 2"] + """ + + AI_SEED = "ai.seed" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. + + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: 123.45 + """ + + AI_STREAMING = "ai.streaming" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. + + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + AI_TAGS = "ai.tags" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Tags that describe an AI pipeline step. + Example: {"executed_function": "add_integers"} + """ + + AI_TEMPERATURE = "ai.temperature" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. + + For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. + Example: 0.5 + """ + + AI_TEXTS = "ai.texts" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Raw text inputs provided to the model. + Example: ["What is machine learning?"] + """ + + AI_TOP_K = "ai.top_k" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. + + For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. + Example: 35 + """ + + AI_TOP_P = "ai.top_p" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. + + For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. + Example: 0.5 + """ + + AI_TOOL_CALLS = "ai.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls + """ + + AI_TOOLS = "ai.tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. + + For an AI model call, the functions that are available + """ + + AI_WARNINGS = "ai.warnings" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + + Warning messages generated during model execution. + Example: ["Token limit exceeded"] + """ + + CACHE_HIT = "cache.hit" + """ + A boolean indicating whether the requested data was found in the cache. + Example: true + """ + + CACHE_ITEM_SIZE = "cache.item_size" + """ + The size of the requested data in bytes. + Example: 58 + """ + + CACHE_KEY = "cache.key" + """ + The key of the requested data. + Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 + """ + + CLIENT_ADDRESS = "client.address" + """ + Client address of the network connection - IP address or Unix domain socket name. + Example: "10.1.2.80" + """ + + CODE_FILEPATH = "code.filepath" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FILE_PATH instead. + + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FILE_PATH = "code.file.path" + """ + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + Example: "/app/myapplication/http/handler/server.py" + """ + + CODE_FUNCTION = "code.function" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead. + + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_FUNCTION_NAME = "code.function.name" + """ + The method or function name, or equivalent (usually rightmost part of the code unit's name). + Example: "server_request" + """ + + CODE_LINENO = "code.lineno" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_LINE_NUMBER instead. + + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + Example: 42 + """ + + CODE_LINE_NUMBER = "code.line.number" + """ + The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. + Example: 42 + """ + + CODE_NAMESPACE = "code.namespace" + """ + .. deprecated:: + This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. + + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + Example: "http.handler" + """ + + DB_MONGODB_COLLECTION = "db.mongodb.collection" + """ + The MongoDB collection being accessed within the database. + See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes + Example: public.users; customers + """ + + DB_NAME = "db.name" + """ + .. deprecated:: + This attribute is deprecated. Use DB_NAMESPACE instead. + + The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + Example: myDatabase + """ + + DB_NAMESPACE = "db.namespace" + """ + The name of the database being accessed. + Example: "customers" + """ + + DB_DRIVER_NAME = "db.driver.name" + """ + The name of the database driver being used for the connection. + Example: "psycopg2" + """ + + DB_OPERATION = "db.operation" + """ + .. deprecated:: + This attribute is deprecated. Use DB_OPERATION_NAME instead. + + The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: findAndModify, HMSET, SELECT + """ + + DB_OPERATION_NAME = "db.operation.name" + """ + The name of the operation being executed. + Example: "SELECT" + """ + + DB_SYSTEM = "db.system" + """ + .. deprecated:: + This attribute is deprecated. Use DB_SYSTEM_NAME instead. + + An identifier for the database management system (DBMS) product being used. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: postgresql + """ + + DB_QUERY_TEXT = "db.query.text" + """ + The database query being executed. + Example: "SELECT * FROM users WHERE id = $1" + """ + + DB_SYSTEM_NAME = "db.system.name" + """ + An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. + Example: "postgresql" + """ + + DB_USER = "db.user" + """ + The name of the database user used for connecting to the database. + See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + Example: my_user + """ + + GEN_AI_AGENT_NAME = "gen_ai.agent.name" + """ + The name of the agent being used. + Example: "ResearchAssistant" + """ + + GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" + """ + The unique identifier for the conversation/thread with the AI model. + Example: "conv_abc123" + """ + + GEN_AI_CHOICE = "gen_ai.choice" + """ + The model's response message. + Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" + """ + + GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" + """ + The input to the embeddings operation. + Example: "Hello!" + """ + + GEN_AI_FUNCTION_ID = "gen_ai.function_id" + """ + Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. + Example: "my-awesome-function" + """ + + GEN_AI_OPERATION_NAME = "gen_ai.operation.name" + """ + The name of the operation being performed. + Example: "chat" + """ + + GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" + """ + Name of the AI pipeline or chain being executed. + Example: "qa-pipeline" + """ + + GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" + """ + The reason why the model stopped generating. + Example: "COMPLETE" + """ + + GEN_AI_RESPONSE_ID = "gen_ai.response.id" + """ + Unique identifier for the completion. + Example: "gen_123abc" + """ + + GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" + """ + Exact model identifier used to generate the response + Example: gpt-4o-mini-2024-07-18 + """ + + GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" + """ + Whether or not the AI model call's response was streamed back asynchronously + Example: true + """ + + GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The model's response text messages. + Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] + """ + + GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" + """ + The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. + Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] + """ + + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" + """ + The time it took to receive the first token from the model. + Example: 0.1 + """ + + GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. + + The tool calls in the model's response. + Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] + """ + + GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. + + The available tools for the model. + Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] + """ + + GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + """ + The list of source system tool definitions available to the GenAI agent or model. + Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] + """ + + GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" + """ + The frequency penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" + """ + The maximum number of tokens to generate in the response. + Example: 2048 + """ + + GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" + """ + The system instructions passed to the model. + Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] + """ + + GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. + + The messages passed to the model. The "content" can be a string or an array of objects. + Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] + """ + + GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" + """ + The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". + Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] + """ + + GEN_AI_REQUEST_MODEL = "gen_ai.request.model" + """ + The model identifier being used for the request. + Example: "gpt-4-turbo" + """ + + GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" + """ + The presence penalty parameter used to reduce repetitiveness of generated tokens. + Example: 0.1 + """ + + GEN_AI_REQUEST_SEED = "gen_ai.request.seed" + """ + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: "1234567890" + """ + + GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" + """ + The temperature parameter used to control randomness in the output. + Example: 0.7 + """ + + GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" + """ + Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). + Example: 35 + """ + + GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" + """ + The top_p parameter used to control diversity via nucleus sampling. + Example: 1.0 + """ + + GEN_AI_SYSTEM = "gen_ai.system" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. + + The name of the AI system being used. + Example: "openai" + """ + + GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" + """ + The Generative AI provider as identified by the client or server instrumentation. + Example: "openai" + """ + + GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" + """ + The description of the tool being used. + Example: "Searches the web for current information about a topic" + """ + + GEN_AI_TOOL_INPUT = "gen_ai.tool.input" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. + + The input of the tool being used. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" + """ + The arguments of the tool call. It has to be a stringified version of the arguments to the tool. + Example: {"location": "Paris"} + """ + + GEN_AI_TOOL_NAME = "gen_ai.tool.name" + """ + The name of the tool being used. + Example: "web_search" + """ + + GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" + """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. + + The output of the tool being used. + Example: "rainy, 57°F" + """ + + GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" + """ + The result of the tool call. It has to be a stringified version of the result of the tool. + Example: "rainy, 57°F" + """ + + GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + """ + The number of tokens in the input. + Example: 150 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" + """ + The number of cached tokens in the input. + Example: 50 + """ + + GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" + """ + The number of tokens written to the cache when processing the AI input (prompt). + Example: 100 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + """ + The number of tokens in the output. + Example: 250 + """ + + GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" + """ + The number of tokens used for reasoning in the output. + Example: 75 + """ + + GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" + """ + The total number of tokens used (input + output). + Example: 400 + """ + + GEN_AI_USER_MESSAGE = "gen_ai.user.message" + """ + The user message passed to the model. + Example: "What's the weather in Paris?" + """ + + HTTP_FRAGMENT = "http.fragment" + """ + The Fragments present in the URL. + Example: #foo=bar + """ + + HTTP_METHOD = "http.method" + """ + .. deprecated:: + This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. + + The HTTP method used. + Example: GET + """ + + HTTP_REQUEST_BODY_DATA = "http.request.body.data" + """ + HTTP request body data. Can be given as string or structural data of any format. + Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" + """ + + HTTP_REQUEST_HEADER = "http.request.header" + """ + Prefix for HTTP request header attributes. The header name (lowercased) is + appended to form the full attribute key. + Example: "http.request.header.content-type" + """ + + HTTP_REQUEST_METHOD = "http.request.method" + """ + The HTTP method used. + Example: GET + """ + + HTTP_QUERY = "http.query" + """ + The Query string present in the URL. + Example: ?foo=bar&bar=baz + """ + + HTTP_STATUS_CODE = "http.response.status_code" + """ + The HTTP status code as an integer. + Example: 418 + """ + + MESSAGING_DESTINATION_NAME = "messaging.destination.name" + """ + The destination name where the message is being consumed from, + e.g. the queue name or topic. + """ + + MESSAGING_MESSAGE_ID = "messaging.message.id" + """ + The message's identifier. + """ + + MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" + """ + The latency between when the task was enqueued and when it was started to be processed. + """ + + MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" + """ + Number of retries/attempts to process a message. + """ + + MESSAGING_SYSTEM = "messaging.system" + """ + The messaging system's name, e.g. `kafka`, `aws_sqs` + """ + + MIDDLEWARE_NAME = "middleware.name" + """ + The middleware's name, e.g. `AuthenticationMiddleware` + """ + + NETWORK_PROTOCOL_NAME = "network.protocol.name" + """ + The application layer protocol name used for the network connection. + Example: "http", "https" + """ + + NETWORK_PEER_ADDRESS = "network.peer.address" + """ + Peer address of the network connection - IP address or Unix domain socket name. + Example: 10.1.2.80, /tmp/my.sock, localhost + """ + + NETWORK_PEER_PORT = "network.peer.port" + """ + Peer port number of the network connection. + Example: 6379 + """ + + NETWORK_TRANSPORT = "network.transport" + """ + The transport protocol used for the network connection. + Example: "tcp", "udp", "unix" + """ + + PROCESS_PID = "process.pid" + """ + The process ID of the running process. + Example: 12345 + """ + + PROCESS_COMMAND_ARGS = "process.command_args" + """ + All the command arguments (including the command/executable itself) as received by the process. + Example: ["cmd/otecol","--config=config.yaml"] + """ + + PROFILER_ID = "profiler_id" + """ + Label identifying the profiler id that the span occurred in. This should be a string. + Example: "5249fbada8d5416482c2f6e47e337372" + """ + + RPC_METHOD = "rpc.method" + """ + The fully-qualified logical name of the method from the RPC interface perspective. + Example: "com.example.ExampleService/exampleMethod" + """ + + RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" + """ + Status code of the RPC returned by the RPC server or generated by the client. + Example: "DEADLINE_EXCEEDED" + """ + + SERVER_ADDRESS = "server.address" + """ + Name of the database host. + Example: example.com + """ + + SERVER_PORT = "server.port" + """ + Logical server port number + Example: 80; 8080; 443 + """ + + SERVER_SOCKET_ADDRESS = "server.socket.address" + """ + Physical server IP address or Unix socket address. + Example: 10.5.3.2 + """ + + SERVER_SOCKET_PORT = "server.socket.port" + """ + Physical server port. + Recommended: If different than server.port. + Example: 16456 + """ + + THREAD_ID = "thread.id" + """ + Identifier of a thread from where the span originated. This should be a string. + Example: "7972576320" + """ + + THREAD_NAME = "thread.name" + """ + Label identifying a thread from where the span originated. This should be a string. + Example: "MainThread" + """ + + USER_EMAIL = "user.email" + """ + User email address. + Example: "test@example.com" + """ + + USER_ID = "user.id" + """ + Unique identifier of the user. + Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" + """ + + USER_IP_ADDRESS = "user.ip_address" + """ + The IP address of the user that triggered the request. + Example: "10.1.2.80" + """ + + USER_NAME = "user.name" + """ + Short name or login/username of the user. + Example: "j.smith" + """ + + URL_FULL = "url.full" + """ + The URL of the resource that was fetched. + Example: "https://example.com/test?foo=bar#buzz" + """ + + URL_FRAGMENT = "url.fragment" + """ + The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. + Example: "details" + """ + + URL_PATH = "url.path" + """ + The URI path component. + Example: "/foo" + """ + + URL_QUERY = "url.query" + """ + The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. + Example: "foo=bar&bar=baz" + """ + + MCP_TOOL_NAME = "mcp.tool.name" + """ + The name of the MCP tool being called. + Example: "get_weather" + """ + + MCP_PROMPT_NAME = "mcp.prompt.name" + """ + The name of the MCP prompt being retrieved. + Example: "code_review" + """ + + MCP_RESOURCE_URI = "mcp.resource.uri" + """ + The URI of the MCP resource being accessed. + Example: "file:///path/to/resource" + """ + + MCP_METHOD_NAME = "mcp.method.name" + """ + The MCP protocol method name being called. + Example: "tools/call", "prompts/get", "resources/read" + """ + + MCP_REQUEST_ID = "mcp.request.id" + """ + The unique identifier for the MCP request. + Example: "req_123abc" + """ + + MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" + """ + The result/output content from an MCP tool execution. + Example: "The weather is sunny" + """ + + MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" + """ + The number of items/keys in the MCP tool result. + Example: 5 + """ + + MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" + """ + Whether the MCP tool execution resulted in an error. + Example: True + """ + + MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" + """ + The message content from an MCP prompt retrieval. + Example: "Review the following code..." + """ + + MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" + """ + The role of the message in an MCP prompt retrieval (only set for single-message prompts). + Example: "user", "assistant", "system" + """ + + MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" + """ + The number of messages in an MCP prompt result. + Example: 1, 3 + """ + + MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" + """ + The protocol/scheme of the MCP resource URI. + Example: "file", "http", "https" + """ + + MCP_TRANSPORT = "mcp.transport" + """ + The transport method used for MCP communication. + Example: "http", "sse", "stdio" + """ + + MCP_SESSION_ID = "mcp.session.id" + """ + The session identifier for the MCP connection. + Example: "a1b2c3d4e5f6" + """ + + SENTRY_DIST = "sentry.dist" + """ + The Sentry dist. + Example: "1.0" + """ + + SENTRY_ENVIRONMENT = "sentry.environment" + """ + The Sentry environment. + Example: "prod" + """ + + SENTRY_RELEASE = "sentry.release" + """ + The Sentry release. + Example: "1.2.3" + """ + + SENTRY_PLATFORM = "sentry.platform" + """ + The sdk platform that generated the event. + Example: "python" + """ + + SENTRY_SDK_NAME = "sentry.sdk.name" + """ + The name of the SDK. + Example: "python" + """ + + SENTRY_SDK_VERSION = "sentry.sdk.version" + """ + The SDK version. + Example: "1.2.3" + """ + + SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" + """ + A list of names identifying enabled integrations. + Example: ["AtexitIntegration", "StdlibIntegration"] + """ + + +class SPANSTATUS: + """ + The status of a Sentry span. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + + ABORTED = "aborted" + ALREADY_EXISTS = "already_exists" + CANCELLED = "cancelled" + DATA_LOSS = "data_loss" + DEADLINE_EXCEEDED = "deadline_exceeded" + FAILED_PRECONDITION = "failed_precondition" + INTERNAL_ERROR = "internal_error" + INVALID_ARGUMENT = "invalid_argument" + NOT_FOUND = "not_found" + OK = "ok" + OUT_OF_RANGE = "out_of_range" + PERMISSION_DENIED = "permission_denied" + RESOURCE_EXHAUSTED = "resource_exhausted" + UNAUTHENTICATED = "unauthenticated" + UNAVAILABLE = "unavailable" + UNIMPLEMENTED = "unimplemented" + UNKNOWN_ERROR = "unknown_error" + + +class OP: + ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" + CACHE_GET = "cache.get" + CACHE_PUT = "cache.put" + COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" + COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" + DB = "db" + DB_CURSOR_ITERATOR = "db.cursor.iter" + DB_CURSOR_FETCH = "db.cursor.fetch" + DB_REDIS = "db.redis" + EVENT_DJANGO = "event.django" + FUNCTION = "function" + FUNCTION_AWS = "function.aws" + FUNCTION_GCP = "function.gcp" + GEN_AI_CHAT = "gen_ai.chat" + GEN_AI_CREATE_AGENT = "gen_ai.create_agent" + GEN_AI_EMBEDDINGS = "gen_ai.embeddings" + GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" + GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" + GEN_AI_HANDOFF = "gen_ai.handoff" + GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" + GEN_AI_RESPONSES = "gen_ai.responses" + GRAPHQL_EXECUTE = "graphql.execute" + GRAPHQL_MUTATION = "graphql.mutation" + GRAPHQL_PARSE = "graphql.parse" + GRAPHQL_RESOLVE = "graphql.resolve" + GRAPHQL_SUBSCRIPTION = "graphql.subscription" + GRAPHQL_QUERY = "graphql.query" + GRAPHQL_VALIDATE = "graphql.validate" + GRPC_CLIENT = "grpc.client" + GRPC_SERVER = "grpc.server" + HTTP_CLIENT = "http.client" + HTTP_CLIENT_STREAM = "http.client.stream" + HTTP_SERVER = "http.server" + MIDDLEWARE_DJANGO = "middleware.django" + MIDDLEWARE_LITESTAR = "middleware.litestar" + MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" + MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" + MIDDLEWARE_STARLETTE = "middleware.starlette" + MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" + MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" + MIDDLEWARE_STARLITE = "middleware.starlite" + MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" + MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" + HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( + "ai.chat_completions.create.huggingface_hub" + ) + QUEUE_PROCESS = "queue.process" + QUEUE_PUBLISH = "queue.publish" + QUEUE_SUBMIT_ARQ = "queue.submit.arq" + QUEUE_TASK_ARQ = "queue.task.arq" + QUEUE_SUBMIT_CELERY = "queue.submit.celery" + QUEUE_TASK_CELERY = "queue.task.celery" + QUEUE_TASK_RQ = "queue.task.rq" + QUEUE_SUBMIT_HUEY = "queue.submit.huey" + QUEUE_TASK_HUEY = "queue.task.huey" + QUEUE_SUBMIT_RAY = "queue.submit.ray" + QUEUE_TASK_RAY = "queue.task.ray" + QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" + QUEUE_SUBMIT_DJANGO = "queue.submit.django" + SUBPROCESS = "subprocess" + SUBPROCESS_WAIT = "subprocess.wait" + SUBPROCESS_COMMUNICATE = "subprocess.communicate" + TEMPLATE_RENDER = "template.render" + VIEW_RENDER = "view.render" + VIEW_RESPONSE_RENDER = "view.response.render" + WEBSOCKET_SERVER = "websocket.server" + SOCKET_CONNECTION = "socket.connection" + SOCKET_DNS = "socket.dns" + MCP_SERVER = "mcp.server" + + +# This type exists to trick mypy and PyCharm into thinking `init` and `Client` +# take these arguments (even though they take opaque **kwargs) +class ClientConstructor: + def __init__( + self, + dsn: "Optional[str]" = None, + *, + max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + server_name: "Optional[str]" = None, + shutdown_timeout: float = 2, + integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 + in_app_include: "List[str]" = [], # noqa: B006 + in_app_exclude: "List[str]" = [], # noqa: B006 + default_integrations: bool = True, + dist: "Optional[str]" = None, + transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, + transport_queue_size: int = DEFAULT_QUEUE_SIZE, + sample_rate: float = 1.0, + send_default_pii: "Optional[bool]" = None, + http_proxy: "Optional[str]" = None, + https_proxy: "Optional[str]" = None, + ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 + max_request_body_size: str = "medium", + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, + keep_alive: "Optional[bool]" = None, + before_send: "Optional[EventProcessor]" = None, + before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, + debug: "Optional[bool]" = None, + attach_stacktrace: bool = False, + ca_certs: "Optional[str]" = None, + propagate_traces: bool = True, + traces_sample_rate: "Optional[float]" = None, + traces_sampler: "Optional[TracesSampler]" = None, + profiles_sample_rate: "Optional[float]" = None, + profiles_sampler: "Optional[TracesSampler]" = None, + profiler_mode: "Optional[ProfilerMode]" = None, + profile_lifecycle: 'Literal["manual", "trace"]' = "manual", + profile_session_sample_rate: "Optional[float]" = None, + auto_enabling_integrations: bool = True, + disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, + auto_session_tracking: bool = True, + send_client_reports: bool = True, + _experiments: "Experiments" = {}, # noqa: B006 + proxy_headers: "Optional[Dict[str, str]]" = None, + instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, + before_send_transaction: "Optional[TransactionProcessor]" = None, + project_root: "Optional[str]" = None, + enable_tracing: "Optional[bool]" = None, + include_local_variables: "Optional[bool]" = True, + include_source_context: "Optional[bool]" = True, + trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 + MATCH_ALL + ], + functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 + event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, + max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, + enable_backpressure_handling: bool = True, + error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, + enable_db_query_source: bool = True, + db_query_source_threshold_ms: int = 100, + enable_http_request_source: bool = True, + http_request_source_threshold_ms: int = 100, + spotlight: "Optional[Union[bool, str]]" = None, + cert_file: "Optional[str]" = None, + key_file: "Optional[str]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, + add_full_stack: bool = DEFAULT_ADD_FULL_STACK, + max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, + enable_logs: bool = False, + before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, + trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), + enable_metrics: bool = True, + before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, + org_id: "Optional[str]" = None, + strict_trace_continuation: bool = False, + stream_gen_ai_spans: bool = True, + ) -> None: + """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. + + :param dsn: The DSN tells the SDK where to send the events. + + If this option is not set, the SDK will just not send any data. + + The `dsn` config option takes precedence over the environment variable. + + Learn more about `DSN utilization `_. + + :param debug: Turns debug mode on or off. + + When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes + wrong with event sending. + + The default is always `False`. It's generally not recommended to turn it on in production because of the + increase in log output. + + The `debug` config option takes precedence over the environment variable. + + :param release: Sets the release. + + If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to + manually set it to guarantee that the release is in sync with your deploy integrations. + + Release names are strings, but some formats are detected by Sentry and might be rendered differently. + + See `the releases documentation `_ to learn how the SDK tries to + automatically configure a release. + + The `release` config option takes precedence over the environment variable. + + Learn more about how to send release data so Sentry can tell you about regressions between releases and + identify the potential source in `the product documentation `_. + + :param environment: Sets the environment. This string is freeform and set to `production` by default. + + A release can be associated with more than one environment to separate them in the UI (think `staging` vs + `production` or similar). + + The `environment` config option takes precedence over the environment variable. + + :param dist: The distribution of the application. + + Distributions are used to disambiguate build or deployment variants of the same release of an application. + + The dist can be for example a build number. + + :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. + + The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of + error events will be sent. + + Events are picked randomly. + + :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. + + This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and + which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number + between `0.0` and `1.0`, inclusive. + + The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to + send the event with the given probability. + + If this configuration option is specified, the `sample_rate` option is ignored. + + :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. + + Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're + sent to Sentry. + + By default, all errors are sent. + + :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. + + This defaults to `100`, but you can set this to any number. + + However, you should be aware that Sentry has a `maximum payload size `_ + and any events exceeding that payload size will be dropped. + + :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. + + Stack traces are always attached to exceptions; however, when this option is set, stack traces are also + sent with messages. + + This option means that stack traces appear next to all log messages. + + Grouping in Sentry is different for events with stack traces and without. As a result, you will get new + groups as you enable or disable this flag for certain events. + + :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) + `_ is added by active integrations. + + If you enable this option, be sure to manually remove what you don't want to send using our features for + managing `Sensitive Data `_. + + :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and + passwords from a `denylist`. + + It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. + + See how to `configure the scrubber here `_. + + :param include_source_context: When enabled, source context will be included in events sent to Sentry. + + This source context includes the five lines of code above and below the line of code where an error + happened. + + :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with + the event to help with debugging. + + :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the + moment an error occurs. + + But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will + be included in the stack trace sent to Sentry. + + :param max_stack_frames: This option limits the number of stack frames that will be captured when + `add_full_stack` is enabled. + + :param server_name: This option can be used to supply a server name. + + When provided, the name of the server is sent along and persisted in the event. + + For many integrations, the server name actually corresponds to the device hostname, even in situations + where the machine is not actually a server. + + :param project_root: The full path to the root directory of your application. + + The `project_root` is used to mark frames in a stack trace either as being in your application or outside + of the application. + + :param in_app_include: A list of string prefixes of module names that belong to the app. + + This option takes precedence over `in_app_exclude`. + + Sentry differentiates stack frames that are directly related to your application ("in application") from + stack frames that come from other packages such as the standard library, frameworks, or other dependencies. + + The application package is automatically marked as `inApp`. + + The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are + displayed by default. + + :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to + third-party packages. + + Modules considered not part of the app will be hidden from stack traces by default. + + This option can be overridden using `in_app_include`. + + :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. + It can be set to one of the following values: + + - `never`: Request bodies are never sent. + - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically + 4KB). + - `medium`: Medium and small requests will be captured (typically 10KB). + - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. + + Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ + expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of + how you configure this option. + + :param max_value_length: The number of characters after which the values containing text in the event payload + will be truncated. + + WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be + dropped by Sentry. + + :param ca_certs: A path to an alternative CA bundle file in PEM-format. + + :param send_client_reports: Set this boolean to `False` to disable sending of client reports. + + Client reports allow the client to send status reports about itself to Sentry, such as information about + events that were dropped before being sent. + + :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) + `_. + + This setting can be used to override the default config options for a specific auto-enabling integration + or to add an integration that is not auto-enabled. + + :param disabled_integrations: List of integrations that will be disabled. + + This setting can be used to explicitly turn off specific `auto-enabling integrations (list) + `_ or + `default `_ integrations. + + :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) + `_ should be enabled. + + When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding + framework/library is detected. + + :param default_integrations: Configures whether `default integrations + `_ should be enabled. + + Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling + integrations**, unless they are specifically added in the `integrations` option, described above. + + :param before_send: This function is called with an SDK-specific message or error event object, and can return + a modified event object, or `null` to skip reporting the event. + + This can be used, for instance, for manual PII stripping before sending. + + By the time `before_send` is executed, all scope data has already been applied to the event. Further + modification of the scope won't have any effect. + + :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can + return a modified transaction event object, or `null` to skip reporting the event. + + One way this might be used is for manual PII stripping before sending. + + :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb + is added to the scope. + + When nothing is returned from the function, the breadcrumb is dropped. + + To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. + + The callback typically gets a second argument (called a "hint") which contains the original object from + which the breadcrumb was created to further customize what the breadcrumb should look like. + + :param transport: Switches out the transport used to send events. + + How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to + send it through some more complex setup that requires proxy authentication. + + :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to + flush. + + :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. + + This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs + support a separate HTTPS proxy. + + SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix + systems, the `http_proxy` environment variable will be picked up. + + :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. + + This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for + HTTPS requests at all times. + + :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded + to `urllib3`'s `ProxyManager `_. + + :param shutdown_timeout: Controls how many seconds to wait before shutting down. + + Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending + events. The default is SDK specific but typically around two seconds. + + Setting this value too low may cause problems for sending events from command line applications. + + Setting the value too high will cause the application to block for a long time for users experiencing + network connectivity problems. + + :param keep_alive: Determines whether to keep the connection alive between requests. + + This can be useful in environments where you encounter frequent network issues such as connection resets. + + :param cert_file: Path to the client certificate to use. + + If set, supersedes the `CLIENT_CERT_FILE` environment variable. + + :param key_file: Path to the key file to use. + + If set, supersedes the `CLIENT_KEY_FILE` environment variable. + + :param socket_options: An optional list of socket options to use. + + These provide fine-grained, low-level control over the way the SDK connects to Sentry. + + If provided, the options will override the default `urllib3` `socket options + `_. + + :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction + will be sent to Sentry. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + Either this or `traces_sampler` must be defined to enable tracing. + + If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have + another service (for example a JS frontend) that makes requests to your service that include trace + information, those traces will be continued and thus transactions will be sent to Sentry. + + If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces + will be started and no incoming traces will be continued. + + :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be + sent to Sentry. + + It will automatically be passed information about the transaction and the context in which it's being + created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being + sent). + + Can also be used for filtering transactions, by returning `0` for those that are unwanted. + + Either this or `traces_sample_rate` must be defined to enable tracing. + + :param trace_propagation_targets: An optional property that controls which downstream services receive tracing + data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. + + The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. + + If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to + that request. + + String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ + a string provided through the option. + + If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the + instrumented client. + + :param functions_to_trace: An optional list of functions that should be set up for tracing. + + For each function in the list, a span will be created when the function is executed. + + Functions in the list are represented as strings containing the fully qualified name of the function. + + This is a convenient option, making it possible to have one central place for configuring what functions + to trace, instead of having custom instrumentation scattered all over your code base. + + To learn more, see the `Custom Instrumentation `_ documentation. + + :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health + checks on the SDK. + + If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second + intervals until recovery. + + This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. + + This option is enabled by default. + + :param enable_db_query_source: When enabled, the source location will be added to database queries. + + :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database + queries. + + The query location will be added to the query for queries slower than the specified threshold. + + :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. + + :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an + outgoing HTTP request. + + The request location will be added to the request for requests slower than the specified threshold. + + :param custom_repr: A custom `repr `_ function to run + while serializing an object. + + Use this to control how your custom objects and classes are visible in Sentry. + + Return a string for that repr value to be used or `None` to continue serializing how Sentry would have + done it anyway. + + :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled + transaction will be profiled. + + (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + + This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be + profiled. + + :param profiles_sampler: + + :param profiler_mode: + + :param profile_lifecycle: + + :param profile_session_sample_rate: + + :param enable_tracing: + + :param propagate_traces: + + :param auto_session_tracking: + + :param spotlight: + + :param instrumenter: + + :param enable_logs: Set `enable_logs` to True to enable the SDK to emit + Sentry logs. Defaults to False. + + :param before_send_log: An optional function to modify or filter out logs + before they're sent to Sentry. Any modifications to the log in this + function will be retained. If the function returns None, the log will + not be sent to Sentry. + + :param trace_ignore_status_codes: An optional property that disables tracing for + HTTP requests with certain status codes. + + Requests are not traced if the status code is contained in the provided set. + + If `trace_ignore_status_codes` is not provided, requests with any status code + may be traced. + + This option has no effect in span streaming mode (`trace_lifecycle="stream"`). + + :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the + `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. + + If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. + + The client's organization ID is extracted from the DSN or can be set with the `org_id` option. + If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. + This is useful to prevent traces of unknown third-party services from being continued in your application. + + :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases + but you can provide it explicitly for self-hosted and Relay setups. This value is used for + trace propagation and for features like `strict_trace_continuation`. + + :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to + reduce downstream data loss. + + :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. + + ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations + collect automatically, superseding `send_default_pii`. Passing a dict under + `_experiments={"data_collection": {...}}` opts into the feature; 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 + changes nothing. Restrict collection per category (user identity, cookies, HTTP + headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source + context). If `send_default_pii` is also set, `data_collection` takes precedence. + + Example:: + + sentry_sdk.init( + dsn="...", + _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, + ) + + See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. + """ + pass + + +def _get_default_options() -> "dict[str, Any]": + import inspect + + a = inspect.getfullargspec(ClientConstructor.__init__) + defaults = a.defaults or () + kwonlydefaults = a.kwonlydefaults or {} + + return dict( + itertools.chain( + zip(a.args[-len(defaults) :], defaults), + kwonlydefaults.items(), + ) + ) + + +DEFAULT_OPTIONS = _get_default_options() +del _get_default_options + + +VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py new file mode 100644 index 0000000000..b3287703b9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py @@ -0,0 +1,9 @@ +from sentry_sdk.crons.api import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.crons.decorator import monitor + +__all__ = [ + "capture_checkin", + "MonitorStatus", + "monitor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py new file mode 100644 index 0000000000..6ea3e36b6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py @@ -0,0 +1,60 @@ +import uuid +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, MonitorConfig + + +def _create_check_in_event( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration_s: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> "Event": + options = sentry_sdk.get_client().options + check_in_id = check_in_id or uuid.uuid4().hex + + check_in: "Event" = { + "type": "check_in", + "monitor_slug": monitor_slug, + "check_in_id": check_in_id, + "status": status, + "duration": duration_s, + "environment": options.get("environment", None), + "release": options.get("release", None), + } + + if monitor_config: + check_in["monitor_config"] = monitor_config + + return check_in + + +def capture_checkin( + monitor_slug: "Optional[str]" = None, + check_in_id: "Optional[str]" = None, + status: "Optional[str]" = None, + duration: "Optional[float]" = None, + monitor_config: "Optional[MonitorConfig]" = None, +) -> str: + check_in_event = _create_check_in_event( + monitor_slug=monitor_slug, + check_in_id=check_in_id, + status=status, + duration_s=duration, + monitor_config=monitor_config, + ) + + sentry_sdk.capture_event(check_in_event) + + logger.debug( + f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" + ) + + return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py new file mode 100644 index 0000000000..be686b4539 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py @@ -0,0 +1,4 @@ +class MonitorStatus: + IN_PROGRESS = "in_progress" + OK = "ok" + ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py new file mode 100644 index 0000000000..b13d350e15 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py @@ -0,0 +1,137 @@ +from functools import wraps +from inspect import iscoroutinefunction +from typing import TYPE_CHECKING + +from sentry_sdk.crons import capture_checkin +from sentry_sdk.crons.consts import MonitorStatus +from sentry_sdk.utils import now + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from types import TracebackType + from typing import ( + Any, + Optional, + ParamSpec, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from sentry_sdk._types import MonitorConfig + + P = ParamSpec("P") + R = TypeVar("R") + + +class monitor: # noqa: N801 + """ + Decorator/context manager to capture checkin events for a monitor. + + Usage (as decorator): + ``` + import sentry_sdk + + app = Celery() + + @app.task + @sentry_sdk.monitor(monitor_slug='my-fancy-slug') + def test(arg): + print(arg) + ``` + + This does not have to be used with Celery, but if you do use it with celery, + put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. + + Usage (as context manager): + ``` + import sentry_sdk + + def test(arg): + with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): + print(arg) + ``` + """ + + def __init__( + self, + monitor_slug: "Optional[str]" = None, + monitor_config: "Optional[MonitorConfig]" = None, + ) -> None: + self.monitor_slug = monitor_slug + self.monitor_config = monitor_config + + def __enter__(self) -> None: + self.start_timestamp = now() + self.check_in_id = capture_checkin( + monitor_slug=self.monitor_slug, + status=MonitorStatus.IN_PROGRESS, + monitor_config=self.monitor_config, + ) + + def __exit__( + self, + exc_type: "Optional[Type[BaseException]]", + exc_value: "Optional[BaseException]", + traceback: "Optional[TracebackType]", + ) -> None: + duration_s = now() - self.start_timestamp + + if exc_type is None and exc_value is None and traceback is None: + status = MonitorStatus.OK + else: + status = MonitorStatus.ERROR + + capture_checkin( + monitor_slug=self.monitor_slug, + check_in_id=self.check_in_id, + status=status, + duration=duration_s, + monitor_config=self.monitor_config, + ) + + if TYPE_CHECKING: + + @overload + def __call__( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + # Unfortunately, mypy does not give us any reliable way to type check the + # return value of an Awaitable (i.e. async function) for this overload, + # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. + ... + + @overload + def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... + + def __call__( + self, + fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", + ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": + if iscoroutinefunction(fn): + return self._async_wrapper(fn) + + else: + if TYPE_CHECKING: + fn = cast("Callable[P, R]", fn) + return self._sync_wrapper(fn) + + def _async_wrapper( + self, fn: "Callable[P, Awaitable[Any]]" + ) -> "Callable[P, Awaitable[Any]]": + @wraps(fn) + async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return await fn(*args, **kwargs) + + return inner + + def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": + @wraps(fn) + def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": + with self: + return fn(*args, **kwargs) + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py new file mode 100644 index 0000000000..bcdf767409 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py @@ -0,0 +1,319 @@ +""" +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, List, Mapping, Optional, cast + +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE + +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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: + """ + Return whether ``key`` matches the sensitive denylist using a partial, + case-insensitive substring match. + + :param extra_terms: additional deny terms (e.g. user-provided) to consider + alongside the built-in `_SENSITIVE_DENYLIST`. + """ + lowered = key.lower() + for term in _SENSITIVE_DENYLIST: + if term in lowered: + return True + if extra_terms: + for term in extra_terms: + if term and term.lower() in lowered: + return True + return False + + +def _apply_key_value_collection_filtering( + items: "Mapping[str, Any]", + behaviour: "KeyValueCollectionBehaviour", + substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, +) -> "Dict[str, Any]": + + if behaviour["mode"] == "off": + return {} + + result: "Dict[str, Any]" = {} + + if behaviour["mode"] == "allowlist": + for key, value in items.items(): + is_allowed = False + if isinstance(key, str): + lowered = key.lower() + is_allowed = any( + term and term.lower() in lowered + for term in behaviour.get("terms", []) + ) + if is_allowed and not _is_sensitive_key(key): + result[key] = value + else: + result[key] = substitute + return result + + # denylist behaviour + for key, value in items.items(): + if isinstance(key, str) and _is_sensitive_key( + key=key, extra_terms=behaviour.get("terms", []) + ): + result[key] = substitute + else: + result[key] = value + return result + + +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, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py new file mode 100644 index 0000000000..795882e9ef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py @@ -0,0 +1,37 @@ +import logging +import sys +import warnings +from logging import LogRecord + +from sentry_sdk import get_client +from sentry_sdk.client import _client_init_debug +from sentry_sdk.utils import logger + + +class _DebugFilter(logging.Filter): + def filter(self, record: "LogRecord") -> bool: + if _client_init_debug.get(False): + return True + + return get_client().options["debug"] + + +def init_debug_support() -> None: + if not logger.handlers: + configure_logger() + + +def configure_logger() -> None: + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.DEBUG) + logger.addFilter(_DebugFilter()) + + +def configure_debug_hub() -> None: + warnings.warn( + "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py new file mode 100644 index 0000000000..d2d4aae31a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py @@ -0,0 +1,332 @@ +import io +import json +import mimetypes +from typing import TYPE_CHECKING + +from sentry_sdk.session import Session +from sentry_sdk.utils import capture_internal_exceptions, json_dumps + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, List, Optional, Union + + from sentry_sdk._types import Event, EventDataCategory + + +def parse_json(data: "Union[bytes, str]") -> "Any": + # on some python 3 versions this needs to be bytes + if isinstance(data, bytes): + data = data.decode("utf-8", "replace") + return json.loads(data) + + +class Envelope: + """ + Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints + documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, + each envelope may have at most one Item with type "event" or "transaction" (but not both). + """ + + def __init__( + self, + headers: "Optional[Dict[str, Any]]" = None, + items: "Optional[List[Item]]" = None, + ) -> None: + if headers is not None: + headers = dict(headers) + self.headers = headers or {} + if items is None: + items = [] + else: + items = list(items) + self.items = items + + @property + def description(self) -> str: + return "envelope with %s items (%s)" % ( + len(self.items), + ", ".join(x.data_category for x in self.items), + ) + + def add_event( + self, + event: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=event), type="event")) + + def add_transaction( + self, + transaction: "Event", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) + + def add_profile( + self, + profile: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) + + def add_profile_chunk( + self, + profile_chunk: "Any", + ) -> None: + self.add_item( + Item( + payload=PayloadRef(json=profile_chunk), + type="profile_chunk", + headers={"platform": profile_chunk.get("platform", "python")}, + ) + ) + + def add_checkin( + self, + checkin: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) + + def add_session( + self, + session: "Union[Session, Any]", + ) -> None: + if isinstance(session, Session): + session = session.to_json() + self.add_item(Item(payload=PayloadRef(json=session), type="session")) + + def add_sessions( + self, + sessions: "Any", + ) -> None: + self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) + + def add_item( + self, + item: "Item", + ) -> None: + self.items.append(item) + + def get_event(self) -> "Optional[Event]": + for items in self.items: + event = items.get_event() + if event is not None: + return event + return None + + def get_transaction_event(self) -> "Optional[Event]": + for item in self.items: + event = item.get_transaction_event() + if event is not None: + return event + return None + + def __iter__(self) -> "Iterator[Item]": + return iter(self.items) + + def serialize_into( + self, + f: "Any", + ) -> None: + f.write(json_dumps(self.headers)) + f.write(b"\n") + for item in self.items: + item.serialize_into(f) + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Envelope": + headers = parse_json(f.readline()) + items = [] + while 1: + item = Item.deserialize_from(f) + if item is None: + break + items.append(item) + return cls(headers=headers, items=items) + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Envelope": + return cls.deserialize_from(io.BytesIO(bytes)) + + def __repr__(self) -> str: + return "" % (self.headers, self.items) + + +class PayloadRef: + def __init__( + self, + bytes: "Optional[bytes]" = None, + path: "Optional[Union[bytes, str]]" = None, + json: "Optional[Any]" = None, + ) -> None: + self.json = json + self.bytes = bytes + self.path = path + + def get_bytes(self) -> bytes: + if self.bytes is None: + if self.path is not None: + with capture_internal_exceptions(): + with open(self.path, "rb") as f: + self.bytes = f.read() + elif self.json is not None: + self.bytes = json_dumps(self.json) + return self.bytes or b"" + + @property + def inferred_content_type(self) -> str: + if self.json is not None: + return "application/json" + elif self.path is not None: + path = self.path + if isinstance(path, bytes): + path = path.decode("utf-8", "replace") + ty = mimetypes.guess_type(path)[0] + if ty: + return ty + return "application/octet-stream" + + def __repr__(self) -> str: + return "" % (self.inferred_content_type,) + + +class Item: + def __init__( + self, + payload: "Union[bytes, str, PayloadRef]", + headers: "Optional[Dict[str, Any]]" = None, + type: "Optional[str]" = None, + content_type: "Optional[str]" = None, + filename: "Optional[str]" = None, + ): + if headers is not None: + headers = dict(headers) + elif headers is None: + headers = {} + self.headers = headers + if isinstance(payload, bytes): + payload = PayloadRef(bytes=payload) + elif isinstance(payload, str): + payload = PayloadRef(bytes=payload.encode("utf-8")) + else: + payload = payload + + if filename is not None: + headers["filename"] = filename + if type is not None: + headers["type"] = type + if content_type is not None: + headers["content_type"] = content_type + elif "content_type" not in headers: + headers["content_type"] = payload.inferred_content_type + + self.payload = payload + + def __repr__(self) -> str: + return "" % ( + self.headers, + self.payload, + self.data_category, + ) + + @property + def type(self) -> "Optional[str]": + return self.headers.get("type") + + @property + def data_category(self) -> "EventDataCategory": + ty = self.headers.get("type") + if ty == "session" or ty == "sessions": + return "session" + elif ty == "attachment": + return "attachment" + elif ty == "transaction": + return "transaction" + elif ty == "span": + return "span" + elif ty == "event": + return "error" + elif ty == "log": + return "log_item" + elif ty == "trace_metric": + return "trace_metric" + elif ty == "client_report": + return "internal" + elif ty == "profile": + return "profile" + elif ty == "profile_chunk": + return "profile_chunk" + elif ty == "check_in": + return "monitor" + else: + return "default" + + def get_bytes(self) -> bytes: + return self.payload.get_bytes() + + def get_event(self) -> "Optional[Event]": + """ + Returns an error event if there is one. + """ + if self.type == "event" and self.payload.json is not None: + return self.payload.json + return None + + def get_transaction_event(self) -> "Optional[Event]": + if self.type == "transaction" and self.payload.json is not None: + return self.payload.json + return None + + def serialize_into( + self, + f: "Any", + ) -> None: + headers = dict(self.headers) + bytes = self.get_bytes() + headers["length"] = len(bytes) + f.write(json_dumps(headers)) + f.write(b"\n") + f.write(bytes) + f.write(b"\n") + + def serialize(self) -> bytes: + out = io.BytesIO() + self.serialize_into(out) + return out.getvalue() + + @classmethod + def deserialize_from( + cls, + f: "Any", + ) -> "Optional[Item]": + line = f.readline().rstrip() + if not line: + return None + headers = parse_json(line) + length = headers.get("length") + if length is not None: + payload = f.read(length) + f.readline() + else: + # if no length was specified we need to read up to the end of line + # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) + payload = f.readline().rstrip(b"\n") + if headers.get("type") in ("event", "transaction"): + rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) + else: + rv = cls(headers=headers, payload=payload) + return rv + + @classmethod + def deserialize( + cls, + bytes: bytes, + ) -> "Optional[Item]": + return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py new file mode 100644 index 0000000000..5eaa5e440b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py @@ -0,0 +1,75 @@ +import copy +from threading import Lock +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import TypedDict + + FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) + + +DEFAULT_FLAG_CAPACITY = 100 + + +class FlagBuffer: + def __init__(self, capacity: int) -> None: + self.capacity = capacity + self.lock = Lock() + + # Buffer is private. The name is mangled to discourage use. If you use this attribute + # directly you're on your own! + self.__buffer = LRUCache(capacity) + + def clear(self) -> None: + self.__buffer = LRUCache(self.capacity) + + def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": + with self.lock: + buffer = FlagBuffer(self.capacity) + buffer.__buffer = copy.deepcopy(self.__buffer, memo) + return buffer + + def get(self) -> "list[FlagData]": + with self.lock: + return [ + {"flag": key, "result": value} for key, value in self.__buffer.get_all() + ] + + def set(self, flag: str, result: bool) -> None: + if isinstance(result, FlagBuffer): + # If someone were to insert `self` into `self` this would create a circular dependency + # on the lock. This is of course a deadlock. However, this is far outside the expected + # usage of this class. We guard against it here for completeness and to document this + # expected failure mode. + raise ValueError( + "FlagBuffer instances can not be inserted into the dictionary." + ) + + with self.lock: + self.__buffer.set(flag, result) + + +def add_feature_flag(flag: str, result: bool) -> None: + """ + Records a flag and its value to be sent on subsequent error events. + We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. + """ + client = sentry_sdk.get_client() + + flags = sentry_sdk.get_isolation_scope().flags + flags.set(flag, result) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.get_current_span() + if span and isinstance(span, sentry_sdk.traces.StreamedSpan): + span.set_attribute(f"flag.evaluation.{flag}", result) + + else: + span = sentry_sdk.get_current_span() + if span and isinstance(span, Span): + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py new file mode 100644 index 0000000000..b17444d06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py @@ -0,0 +1,741 @@ +import warnings +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from sentry_sdk import ( + get_client, + get_current_scope, + get_global_scope, + get_isolation_scope, +) +from sentry_sdk._compat import with_metaclass +from sentry_sdk.client import Client +from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.scope import _ScopeManager +from sentry_sdk.tracing import ( + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.utils import ( + ContextVar, + logger, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + overload, + ) + + from typing_extensions import Unpack + + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + ExcInfo, + Hint, + LogLevelStr, + SamplingContext, + ) + from sentry_sdk.client import BaseClient + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.tracing import TransactionKwargs + + T = TypeVar("T") + +else: + + def overload(x: "T") -> "T": + return x + + +class SentryHubDeprecationWarning(DeprecationWarning): + """ + A custom deprecation warning to inform users that the Hub is deprecated. + """ + + _MESSAGE = ( + "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " + "Please consult our 1.x to 2.x migration guide for details on how to migrate " + "`Hub` usage to the new API: " + "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" + ) + + def __init__(self, *_: object) -> None: + super().__init__(self._MESSAGE) + + +@contextmanager +def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": + """Utility function to suppress deprecation warnings for the Hub.""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) + yield + + +_local = ContextVar("sentry_current_hub") + + +class HubMeta(type): + @property + def current(cls) -> "Hub": + """Returns the current instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + rv = _local.get(None) + if rv is None: + with _suppress_hub_deprecation_warning(): + # This will raise a deprecation warning; suppress it since we already warned above. + rv = Hub(GLOBAL_HUB) + _local.set(rv) + return rv + + @property + def main(cls) -> "Hub": + """Returns the main instance of the hub.""" + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + return GLOBAL_HUB + + +class Hub(with_metaclass(HubMeta)): # type: ignore + """ + .. deprecated:: 2.0.0 + The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. + + The hub wraps the concurrency management of the SDK. Each thread has + its own hub but the hub might transfer with the flow of execution if + context vars are available. + + If the hub is used with a with statement it's temporarily activated. + """ + + _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] + _scope: "Optional[Scope]" = None + + # Mypy doesn't pick up on the metaclass. + + if TYPE_CHECKING: + current: "Hub" = None # type: ignore[assignment] + main: "Optional[Hub]" = None + + def __init__( + self, + client_or_hub: "Optional[Union[Hub, Client]]" = None, + scope: "Optional[Any]" = None, + ) -> None: + warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) + + current_scope = None + + if isinstance(client_or_hub, Hub): + client = get_client() + if scope is None: + # hub cloning is going on, we use a fork of the current/isolation scope for context manager + scope = get_isolation_scope().fork() + current_scope = get_current_scope().fork() + else: + client = client_or_hub + get_global_scope().set_client(client) + + if scope is None: # so there is no Hub cloning going on + # just the current isolation scope is used for context manager + scope = get_isolation_scope() + current_scope = get_current_scope() + + if current_scope is None: + # just the current current scope is used for context manager + current_scope = get_current_scope() + + self._stack = [(client, scope)] # type: ignore + self._last_event_id: "Optional[str]" = None + self._old_hubs: "List[Hub]" = [] + + self._old_current_scopes: "List[Scope]" = [] + self._old_isolation_scopes: "List[Scope]" = [] + self._current_scope: "Scope" = current_scope + self._scope: "Scope" = scope + + def __enter__(self) -> "Hub": + self._old_hubs.append(Hub.current) + _local.set(self) + + current_scope = get_current_scope() + self._old_current_scopes.append(current_scope) + scope._current_scope.set(self._current_scope) + + isolation_scope = get_isolation_scope() + self._old_isolation_scopes.append(isolation_scope) + scope._isolation_scope.set(self._scope) + + return self + + def __exit__( + self, + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[Any]", + ) -> None: + old = self._old_hubs.pop() + _local.set(old) + + old_current_scope = self._old_current_scopes.pop() + scope._current_scope.set(old_current_scope) + + old_isolation_scope = self._old_isolation_scopes.pop() + scope._isolation_scope.set(old_isolation_scope) + + def run( + self, + callback: "Callable[[], T]", + ) -> "T": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Runs a callback in the context of the hub. Alternatively the + with statement can be used on the hub directly. + """ + with self: + return callback() + + def get_integration( + self, + name_or_class: "Union[str, Type[Integration]]", + ) -> "Any": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. + + Returns the integration for this hub by name or class. If there + is no client bound or the client does not have that integration + then `None` is returned. + + If the return value is not `None` the hub is guaranteed to have a + client attached. + """ + return get_client().get_integration(name_or_class) + + @property + def client(self) -> "Optional[BaseClient]": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Please use :py:func:`sentry_sdk.api.get_client` instead. + + Returns the current client on the hub. + """ + client = get_client() + + if not client.is_active(): + return None + + return client + + @property + def scope(self) -> "Scope": + """ + .. deprecated:: 2.0.0 + This property is deprecated and will be removed in a future release. + Returns the current scope on the hub. + """ + return get_isolation_scope() + + def last_event_id(self) -> "Optional[str]": + """ + Returns the last event ID. + + .. deprecated:: 1.40.5 + This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. + """ + logger.warning( + "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." + ) + return self._last_event_id + + def bind_client( + self, + new: "Optional[BaseClient]", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.set_client` instead. + + Binds a new client to the hub. + """ + get_global_scope().set_client(new) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. + + Captures an event. + + Alias of :py:meth:`sentry_sdk.Scope.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + """ + last_event_id = get_current_scope().capture_event( + event, hint, scope=scope, **scope_kwargs + ) + + is_transaction = event.get("type") == "transaction" + if last_event_id is not None and not is_transaction: + self._last_event_id = last_event_id + + return last_event_id + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. + + Captures a message. + + Alias of :py:meth:`sentry_sdk.Scope.capture_message`. + + :param message: The string to send as the message to Sentry. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_message( + message, level=level, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. + + Captures an exception. + + Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + last_event_id = get_current_scope().capture_exception( + error, scope=scope, **scope_kwargs + ) + + if last_event_id is not None: + self._last_event_id = last_event_id + + return last_event_id + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. + + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_span` instead. + + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + """ + scope = get_current_scope() + return scope.start_span(instrumenter=instrumenter, **kwargs) + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. + + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. + """ + scope = get_current_scope() + + # For backwards compatibility, we allow passing the scope as the hub. + # We need a major release to make this nice. (if someone searches the code: deprecated) + # Type checking disabled for this line because deprecated keys are not allowed in the type signature. + kwargs["hub"] = scope # type: ignore + + return scope.start_transaction( + transaction, instrumenter, custom_sampling_context, **kwargs + ) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + ) -> "Transaction": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. + + Sets the propagation context from environment or headers and returns a transaction. + """ + return get_isolation_scope().continue_trace( + environ_or_headers=environ_or_headers, op=op, name=name, source=source + ) + + @overload + def push_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def push_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def push_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pushes a new layer on the scope stack. + + :param callback: If provided, this method pushes a scope, calls + `callback`, and pops the scope again. + + :returns: If no `callback` is provided, a context manager that should + be used to pop the scope again. + """ + if callback is not None: + with self.push_scope() as scope: + callback(scope) + return None + + return _ScopeManager(self) + + def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Pops a scope layer from the stack. + + Try to use the context manager :py:meth:`push_scope` instead. + """ + rv = self._stack.pop() + assert self._stack, "stack must have at least one layer" + return rv + + @overload + def configure_scope( + self, + callback: "Optional[None]" = None, + ) -> "ContextManager[Scope]": + pass + + @overload + def configure_scope( # noqa: F811 + self, + callback: "Callable[[Scope], None]", + ) -> None: + pass + + def configure_scope( # noqa + self, + callback: "Optional[Callable[[Scope], None]]" = None, + continue_trace: bool = True, + ) -> "Optional[ContextManager[Scope]]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + + Reconfigures the scope. + + :param callback: If provided, call the callback with the current scope. + + :returns: If no callback is provided, returns a context manager that returns the scope. + """ + scope = get_isolation_scope() + + if continue_trace: + scope.generate_propagation_context() + + if callback is not None: + # TODO: used to return None when client is None. Check if this changes behavior. + callback(scope) + + return None + + @contextmanager + def inner() -> "Generator[Scope, None, None]": + yield scope + + return inner() + + def start_session( + self, + session_mode: str = "application", + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.start_session` instead. + + Starts a new session. + """ + get_isolation_scope().start_session( + session_mode=session_mode, + ) + + def end_session(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.end_session` instead. + + Ends the current session if there is one. + """ + get_isolation_scope().end_session() + + def stop_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. + + Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + get_isolation_scope().stop_auto_session_tracking() + + def resume_auto_session_tracking(self) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. + + Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + get_isolation_scope().resume_auto_session_tracking() + + def flush( + self, + timeout: "Optional[float]" = None, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.client._Client.flush` instead. + + Alias for :py:meth:`sentry_sdk.client._Client.flush` + """ + return get_client().flush(timeout=timeout, callback=callback) + + def get_traceparent(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. + + Returns the traceparent either from the active span or from the scope. + """ + current_scope = get_current_scope() + traceparent = current_scope.get_traceparent() + + if traceparent is None: + isolation_scope = get_isolation_scope() + traceparent = isolation_scope.get_traceparent() + + return traceparent + + def get_baggage(self) -> "Optional[str]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. + + Returns Baggage either from the active span or from the scope. + """ + current_scope = get_current_scope() + baggage = current_scope.get_baggage() + + if baggage is None: + isolation_scope = get_isolation_scope() + baggage = isolation_scope.get_baggage() + + if baggage is not None: + return baggage.serialize() + + return None + + def iter_trace_propagation_headers( + self, span: "Optional[Span]" = None + ) -> "Generator[Tuple[str, str], None, None]": + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. + + Return HTTP headers which allow propagation of trace data. Data taken + from the span representing the request, if available, or the current + span on the scope if not. + """ + return get_current_scope().iter_trace_propagation_headers( + span=span, + ) + + def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: + """ + .. deprecated:: 2.0.0 + This function is deprecated and will be removed in a future release. + Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. + + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + return get_current_scope().trace_propagation_meta( + span=span, + ) + + +with _suppress_hub_deprecation_warning(): + # Suppress deprecation warning for the Hub here, since we still always + # import this module. + GLOBAL_HUB = Hub() +_local.set(GLOBAL_HUB) + + +# Circular imports +from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py new file mode 100644 index 0000000000..677d34a81e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +from threading import Lock +from typing import TYPE_CHECKING + +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union + + +_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) + + +_installer_lock = Lock() + +# Set of all integration identifiers we have attempted to install +_processed_integrations: "Set[str]" = set() + +# Set of all integration identifiers we have actually installed +_installed_integrations: "Set[str]" = set() + + +def _generate_default_integrations_iterator( + integrations: "List[str]", + auto_enabling_integrations: "List[str]", +) -> "Callable[[bool], Iterator[Type[Integration]]]": + def iter_default_integrations( + with_auto_enabling_integrations: bool, + ) -> "Iterator[Type[Integration]]": + """Returns an iterator of the default integration classes:""" + from importlib import import_module + + if with_auto_enabling_integrations: + all_import_strings = integrations + auto_enabling_integrations + else: + all_import_strings = integrations + + for import_string in all_import_strings: + try: + module, cls = import_string.rsplit(".", 1) + yield getattr(import_module(module), cls) + except (DidNotEnable, SyntaxError) as e: + logger.debug( + "Did not import default integration %s: %s", import_string, e + ) + + if isinstance(iter_default_integrations.__doc__, str): + for import_string in integrations: + iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) + + return iter_default_integrations + + +_DEFAULT_INTEGRATIONS = [ + # stdlib/base runtime integrations + "sentry_sdk.integrations.argv.ArgvIntegration", + "sentry_sdk.integrations.atexit.AtexitIntegration", + "sentry_sdk.integrations.dedupe.DedupeIntegration", + "sentry_sdk.integrations.excepthook.ExcepthookIntegration", + "sentry_sdk.integrations.logging.LoggingIntegration", + "sentry_sdk.integrations.modules.ModulesIntegration", + "sentry_sdk.integrations.stdlib.StdlibIntegration", + "sentry_sdk.integrations.threading.ThreadingIntegration", +] + +_AUTO_ENABLING_INTEGRATIONS = [ + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.anthropic.AnthropicIntegration", + "sentry_sdk.integrations.ariadne.AriadneIntegration", + "sentry_sdk.integrations.arq.ArqIntegration", + "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", + "sentry_sdk.integrations.boto3.Boto3Integration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.chalice.ChaliceIntegration", + "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", + "sentry_sdk.integrations.cohere.CohereIntegration", + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.fastapi.FastApiIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.gql.GQLIntegration", + "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", + "sentry_sdk.integrations.graphene.GrapheneIntegration", + "sentry_sdk.integrations.httpx.HttpxIntegration", + "sentry_sdk.integrations.httpx2.Httpx2Integration", + "sentry_sdk.integrations.huey.HueyIntegration", + "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", + "sentry_sdk.integrations.langchain.LangchainIntegration", + "sentry_sdk.integrations.langgraph.LanggraphIntegration", + "sentry_sdk.integrations.litestar.LitestarIntegration", + "sentry_sdk.integrations.loguru.LoguruIntegration", + "sentry_sdk.integrations.mcp.MCPIntegration", + "sentry_sdk.integrations.openai.OpenAIIntegration", + "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", + "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", + "sentry_sdk.integrations.pymongo.PyMongoIntegration", + "sentry_sdk.integrations.pyramid.PyramidIntegration", + "sentry_sdk.integrations.quart.QuartIntegration", + "sentry_sdk.integrations.redis.RedisIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", + "sentry_sdk.integrations.starlette.StarletteIntegration", + "sentry_sdk.integrations.starlite.StarliteIntegration", + "sentry_sdk.integrations.strawberry.StrawberryIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", +] + +iter_default_integrations = _generate_default_integrations_iterator( + integrations=_DEFAULT_INTEGRATIONS, + auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, +) + +del _generate_default_integrations_iterator + + +_MIN_VERSIONS = { + "aiohttp": (3, 4), + "aiomysql": (0, 3, 0), + "anthropic": (0, 16), + "ariadne": (0, 20), + "arq": (0, 23), + "asyncpg": (0, 23), + "beam": (2, 12), + "boto3": (1, 16), # botocore + "bottle": (0, 12), + "celery": (4, 4, 7), + "chalice": (1, 16, 0), + "clickhouse_driver": (0, 2, 0), + "cohere": (5, 4, 0), + "django": (1, 8), + "dramatiq": (1, 9), + "falcon": (1, 4), + "fastapi": (0, 79, 0), + "flask": (1, 1, 4), + "gql": (3, 4, 1), + "graphene": (3, 3), + "google_genai": (1, 29, 0), # google-genai + "grpc": (1, 32, 0), # grpcio + "httpx": (0, 16, 0), + "httpx2": (2, 0, 0), + "huggingface_hub": (0, 24, 7), + "langchain": (0, 1, 0), + "langgraph": (0, 6, 6), + "launchdarkly": (9, 8, 0), + "litellm": (1, 77, 5), + "loguru": (0, 7, 0), + "mcp": (1, 15, 0), + "openai": (1, 0, 0), + "openai_agents": (0, 0, 19), + "openfeature": (0, 7, 1), + "pydantic_ai": (1, 0, 0), + "pymongo": (3, 5, 0), + "pyreqwest": (0, 11, 6), + "quart": (0, 16, 0), + "ray": (2, 7, 0), + "requests": (2, 0, 0), + "rq": (0, 6), + "sanic": (0, 8), + "sqlalchemy": (1, 2), + "starlette": (0, 16), + "starlite": (1, 48), + "statsig": (0, 55, 3), + "strawberry": (0, 209, 5), + "tornado": (6, 0), + "typer": (0, 15), + "unleash": (6, 0, 1), +} + + +_INTEGRATION_DEACTIVATES = { + "langchain": {"openai", "anthropic", "google_genai"}, + "openai_agents": {"openai"}, + "pydantic_ai": {"openai", "anthropic"}, +} + + +def setup_integrations( + integrations: "Sequence[Integration]", + with_defaults: bool = True, + with_auto_enabling_integrations: bool = False, + disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, + options: "Optional[Dict[str, Any]]" = None, +) -> "Dict[str, Integration]": + """ + Given a list of integration instances, this installs them all. + + When `with_defaults` is set to `True` all default integrations are added + unless they were already provided before. + + `disabled_integrations` takes precedence over `with_defaults` and + `with_auto_enabling_integrations`. + + Some integrations are designed to automatically deactivate other integrations + in order to avoid conflicts and prevent duplicate telemetry from being collected. + For example, enabling the `langchain` integration will auto-deactivate both the + `openai` and `anthropic` integrations. + + Users can override this behavior by: + - Explicitly providing an integration in the `integrations=[]` list, or + - Disabling the higher-level integration via the `disabled_integrations` option. + """ + integrations = dict( + (integration.identifier, integration) for integration in integrations or () + ) + + logger.debug("Setting up integrations (with default = %s)", with_defaults) + + user_provided_integrations = set(integrations.keys()) + + # Integrations that will not be enabled + disabled_integrations = [ + integration if isinstance(integration, type) else type(integration) + for integration in disabled_integrations or [] + ] + + # Integrations that are not explicitly set up by the user. + used_as_default_integration = set() + + if with_defaults: + for integration_cls in iter_default_integrations( + with_auto_enabling_integrations + ): + if integration_cls.identifier not in integrations: + instance = integration_cls() + integrations[instance.identifier] = instance + used_as_default_integration.add(instance.identifier) + + disabled_integration_identifiers = { + integration.identifier for integration in disabled_integrations + } + + for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): + if ( + integration in integrations + and integration not in disabled_integration_identifiers + ): + for target in targets_to_deactivate: + if target not in user_provided_integrations: + for cls in iter_default_integrations(True): + if cls.identifier == target: + if cls not in disabled_integrations: + disabled_integrations.append(cls) + logger.debug( + "Auto-deactivating %s integration because %s integration is active", + target, + integration, + ) + + for identifier, integration in integrations.items(): + with _installer_lock: + if identifier not in _processed_integrations: + if type(integration) in disabled_integrations: + logger.debug("Ignoring integration %s", identifier) + else: + logger.debug( + "Setting up previously not enabled integration %s", identifier + ) + try: + type(integration).setup_once() + integration.setup_once_with_options(options) + except DidNotEnable as e: + if identifier not in used_as_default_integration: + raise + + logger.debug( + "Did not enable default integration %s: %s", identifier, e + ) + else: + _installed_integrations.add(identifier) + + _processed_integrations.add(identifier) + + integrations = { + identifier: integration + for identifier, integration in integrations.items() + if identifier in _installed_integrations + } + + for identifier in integrations: + logger.debug("Enabling integration %s", identifier) + + return integrations + + +def _check_minimum_version( + integration: "type[Integration]", + version: "Optional[tuple[int, ...]]", + package: "Optional[str]" = None, +) -> None: + package = package or integration.identifier + + if version is None: + raise DidNotEnable(f"Unparsable {package} version.") + + min_version = _MIN_VERSIONS.get(integration.identifier) + if min_version is None: + return + + if version < min_version: + raise DidNotEnable( + f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." + ) + + +class DidNotEnable(Exception): # noqa: N818 + """ + The integration could not be enabled due to a trivial user error like + `flask` not being installed for the `FlaskIntegration`. + + This exception is silently swallowed for default integrations, but reraised + for explicitly enabled integrations. + """ + + +class Integration(ABC): + """Baseclass for all integrations. + + To accept options for an integration, implement your own constructor that + saves those options on `self`. + """ + + install = None + """Legacy method, do not implement.""" + + identifier: "str" = None # type: ignore[assignment] + """String unique ID of integration type""" + + @staticmethod + @abstractmethod + def setup_once() -> None: + """ + Initialize the integration. + + This function is only called once, ever. Configuration is not available + at this point, so the only thing to do here is to hook into exception + handlers, and perhaps do monkeypatches. + + Inside those hooks `Integration.current` can be used to access the + instance again. + """ + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + """ + Called after setup_once in rare cases on the instance and with options since we don't have those available above. + """ + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py new file mode 100644 index 0000000000..7ff4657013 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py @@ -0,0 +1,187 @@ +import urllib +from enum import Enum +from typing import TYPE_CHECKING + +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.scope import should_send_default_pii + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from typing_extensions import Literal + + from sentry_sdk.utils import AnnotatedValue + + +class _RootPathInPath(Enum): + EXCLUDED = "excluded" + EITHER = "either" + + +def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": + """ + Extract headers from the ASGI scope, in the format that the Sentry protocol expects. + """ + headers: "Dict[str, str]" = {} + for raw_key, raw_value in asgi_scope["headers"]: + key = raw_key.decode("latin-1") + value = raw_value.decode("latin-1") + if key in headers: + headers[key] = headers[key] + ", " + value + else: + headers[key] = value + + return headers + + +def _get_path( + asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" +) -> "str": + if root_path_in_path is _RootPathInPath.EXCLUDED: + return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") + + # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 + path = asgi_scope["path"] + root_path = asgi_scope.get("root_path", "") + + if not root_path or path == root_path or path.startswith(root_path + "/"): + return path + + return root_path + path + + +def _get_url( + asgi_scope: "Dict[str, Any]", + default_scheme: "Literal['ws', 'http']", + host: "Optional[Union[AnnotatedValue, str]]", + path: str, +) -> str: + """ + Extract URL from the ASGI scope, without also including the querystring. + """ + scheme = asgi_scope.get("scheme", default_scheme) + + server = asgi_scope.get("server", None) + + if host: + return "%s://%s%s" % (scheme, host, path) + + if server is not None: + host, port = server + default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) + if port != default_port: + return "%s://%s:%s%s" % (scheme, host, port, path) + return "%s://%s%s" % (scheme, host, path) + return path + + +def _get_query(asgi_scope: "Any") -> "Any": + """ + Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. + """ + qs = asgi_scope.get("query_string") + if not qs: + return None + return urllib.parse.unquote(qs.decode("latin-1")) + + +def _get_ip(asgi_scope: "Any") -> str: + """ + Extract IP Address from the ASGI scope based on request headers with fallback to scope client. + """ + headers = _get_headers(asgi_scope) + try: + return headers["x-forwarded-for"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return headers["x-real-ip"] + except KeyError: + pass + + return asgi_scope.get("client")[0] + + +def _get_request_data( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "Dict[str, Any]": + """ + Returns data related to the HTTP request from the ASGI scope. + """ + request_data: "Dict[str, Any]" = {} + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + request_data["method"] = asgi_scope.get("method") + + headers = _get_headers(asgi_scope) + + request_data["headers"] = _filter_headers( + headers, + use_annotated_value=False, + ) + + request_data["query_string"] = _get_query(asgi_scope) + + request_data["url"] = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} + + return request_data + + +def _get_request_attributes( + asgi_scope: "Any", + root_path_in_path: "_RootPathInPath", +) -> "dict[str, Any]": + """ + Return attributes related to the HTTP request from the ASGI scope. + """ + attributes: "dict[str, Any]" = {} + + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + if asgi_scope.get("method"): + attributes["http.request.method"] = asgi_scope["method"].upper() + + headers = _get_headers(asgi_scope) + + filtered_headers = _filter_headers(headers, use_annotated_value=False) + for header, value in filtered_headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + if should_send_default_pii(): + query = _get_query(asgi_scope) + if query: + attributes["http.query"] = query + + path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) + attributes["url.path"] = path + + url_without_query_string = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path=path, + ) + query_string = _get_query(asgi_scope) + attributes["url.full"] = ( + f"{url_without_query_string}?{query_string}" + if query_string is not None + else url_without_query_string + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + ip = _get_ip(asgi_scope) + attributes["client.address"] = ip + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py new file mode 100644 index 0000000000..ad0ab7c734 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py @@ -0,0 +1,282 @@ +import json +from contextlib import contextmanager +from copy import deepcopy + +import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.data_collection import _apply_key_value_collection_filtering +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger + +try: + from django.http.request import RawPostDataException + + _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) +except ImportError: + RawPostDataException = None + _RAW_DATA_EXCEPTIONS = (ValueError,) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union + + from sentry_sdk._types import Event, HttpStatusCodeRange + + +SENSITIVE_ENV_KEYS = ( + "REMOTE_ADDR", + "HTTP_X_FORWARDED_FOR", + "HTTP_SET_COOKIE", + "HTTP_COOKIE", + "HTTP_AUTHORIZATION", + "HTTP_PROXY_AUTHORIZATION", + "HTTP_X_API_KEY", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", +) + +SENSITIVE_HEADERS = tuple( + x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") +) + +DEFAULT_HTTP_METHODS_TO_CAPTURE = ( + "CONNECT", + "DELETE", + "GET", + # "HEAD", # do not capture HEAD requests by default + # "OPTIONS", # do not capture OPTIONS requests by default + "PATCH", + "POST", + "PUT", + "TRACE", +) + + +# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support +@contextmanager +def nullcontext() -> "Iterator[None]": + yield + + +def request_body_within_bounds( + client: "Optional[sentry_sdk.client.BaseClient]", content_length: int +) -> bool: + if client is None: + return False + + bodies = client.options["max_request_body_size"] + return not ( + bodies == "never" + or (bodies == "small" and content_length > 10**3) + or (bodies == "medium" and content_length > 10**4) + ) + + +class RequestExtractor: + """ + Base class for request extraction. + """ + + # It does not make sense to make this class an ABC because it is not used + # for typing, only so that child classes can inherit common methods from + # it. Only some child classes implement all methods that raise + # NotImplementedError in this class. + + def __init__(self, request: "Any") -> None: + self.request = request + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + + content_length = self.content_length() + request_info = event.get("request", {}) + + if should_send_default_pii(): + request_info["cookies"] = dict(self.cookies()) + + if not request_body_within_bounds(client, content_length): + data = AnnotatedValue.removed_because_over_size_limit() + else: + # First read the raw body data + # It is important to read this first because if it is Django + # it will cache the body and then we can read the cached version + # again in parsed_body() (or json() or wherever). + raw_data = None + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # If DjangoRestFramework is used it already read the body for us + # so reading it here will fail. We can ignore this. + pass + + parsed_body = self.parsed_body() + if parsed_body is not None: + data = parsed_body + elif raw_data: + data = AnnotatedValue.removed_because_raw_data() + else: + data = None + + if data is not None: + request_info["data"] = data + + event["request"] = deepcopy(request_info) + + def content_length(self) -> int: + try: + return int(self.env().get("CONTENT_LENGTH", 0)) + except ValueError: + return 0 + + def cookies(self) -> "MutableMapping[str, Any]": + raise NotImplementedError() + + def raw_data(self) -> "Optional[Union[str, bytes]]": + raise NotImplementedError() + + def form(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + form = self.form() + except Exception: + form = None + try: + files = self.files() + except Exception: + files = None + + if form or files: + data = {} + if form: + data = dict(form.items()) + if files: + for key in files.keys(): + data[key] = AnnotatedValue.removed_because_raw_data() + + return data + + return self.json() + + def is_json(self) -> bool: + return _is_json_content_type(self.env().get("CONTENT_TYPE")) + + def json(self) -> "Optional[Any]": + try: + if not self.is_json(): + return None + + try: + raw_data = self.raw_data() + except _RAW_DATA_EXCEPTIONS: + # The body might have already been read, in which case this will + # fail + raw_data = None + + if raw_data is None: + return None + + if isinstance(raw_data, str): + return json.loads(raw_data) + else: + return json.loads(raw_data.decode("utf-8")) + except ValueError: + pass + + return None + + def files(self) -> "Optional[Dict[str, Any]]": + raise NotImplementedError() + + def size_of_file(self, file: "Any") -> int: + raise NotImplementedError() + + def env(self) -> "Dict[str, Any]": + raise NotImplementedError() + + +def _is_json_content_type(ct: "Optional[str]") -> bool: + mt = (ct or "").split(";", 1)[0] + return ( + mt == "application/json" + or (mt.startswith("application/")) + and mt.endswith("+json") + ) + + +def _filter_headers( + headers: "Mapping[str, str]", + use_annotated_value: bool = True, +) -> "Mapping[str, Union[AnnotatedValue, str]]": + client_options = sentry_sdk.get_client().options + + if has_data_collection_enabled(client_options): + data_collection_configuration = client_options["data_collection"] + + filtered = _apply_key_value_collection_filtering( + items=headers, + behaviour=data_collection_configuration["http_headers"]["request"], + ) + + for key in filtered: + if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): + filtered[key] = SENSITIVE_DATA_SUBSTITUTE + + return filtered + else: + if should_send_default_pii(): + return headers + + substitute: "Union[AnnotatedValue, str]" = ( + SENSITIVE_DATA_SUBSTITUTE + if not use_annotated_value + else AnnotatedValue.removed_because_over_size_limit() + ) + + return { + k: ( + v + if k.upper().replace("-", "_") not in SENSITIVE_HEADERS + else substitute + ) + for k, v in headers.items() + } + + +def _in_http_status_code_range( + code: object, code_ranges: "list[HttpStatusCodeRange]" +) -> bool: + for target in code_ranges: + if isinstance(target, int): + if code == target: + return True + continue + + try: + if code in target: + return True + except TypeError: + logger.warning( + "failed_request_status_codes has to be a list of integers or containers" + ) + + return False + + +class HttpCodeRangeContainer: + """ + Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. + Used for backwards compatibility with the old `failed_request_status_codes` option. + """ + + def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: + self._code_ranges = code_ranges + + def __contains__(self, item: object) -> bool: + return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..59bae92a60 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,509 @@ +import sys +import weakref +from functools import wraps + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import ( + _filter_headers, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + NoOpStreamedSpan, + SegmentSource, + SpanStatus, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import ( + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + parse_url, + parse_version, + reraise, + transaction_from_function, +) + +try: + import asyncio + + from aiohttp import ClientSession, TraceConfig + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Set + from types import SimpleNamespace + from typing import Any, ContextManager, Optional, Tuple, Union + + from aiohttp import TraceRequestEndParams, TraceRequestStartParams + from aiohttp.web_request import Request + from aiohttp.web_urldispatcher import UrlMappingMatchInfo + + from sentry_sdk._types import Attributes, Event, EventProcessor + from sentry_sdk.tracing import Span + from sentry_sdk.utils import ExcInfo + + +TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + origin = f"auto.http.{identifier}" + + def __init__( + self, + transaction_style: str = "handler_name", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self._failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(AIOHTTP_VERSION) + _check_minimum_version(AioHttpIntegration, version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The aiohttp integration for Sentry requires Python 3.7+ " + " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + # In the aiohttp integration, all of their HTTP responses are Exceptions. + # Because they have to be raised and handled by the framework, we need to + # register the exceptions as control flow exceptions so that we don't + # accidentally overwrite a status of "ok" with "error". + _register_control_flow_exception(HTTPException) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(AioHttpIntegration) + if integration is None: + return await old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + # Scope data will not leak between requests because aiohttp + # create a task to wrap each request. + scope.generate_propagation_context() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + headers = dict(request.headers) + + span_ctx: "ContextManager[Union[Span, StreamedSpan]]" + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context({"aiohttp_request": request}) + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, + use_annotated_value=False, + ).items(): + header_attributes[ + f"http.request.header.{header.lower()}" + ] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + url_attributes = {} + if should_send_default_pii(): + url_attributes["url.full"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + url_attributes["url.path"] = request.path + + if request.query_string: + url_attributes["url.query"] = request.query_string + + client_address_attributes = {} + if should_send_default_pii() and request.remote: + client_address_attributes["client.address"] = request.remote + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) + + span_ctx = sentry_sdk.traces.start_span( + # If this name makes it to the UI, AIOHTTP's URL + # resolver did not find a route or died trying. + name="generic AIOHTTP request", + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": AioHttpIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE.value, + "http.request.method": request.method, + **url_attributes, + **client_address_attributes, + **header_attributes, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + name="generic AIOHTTP request", + source=TransactionSource.ROUTE, + origin=AioHttpIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"aiohttp_request": request}, + ) + + with span_ctx as span: + try: + response = await old_handle(self, request) + except HTTPException as e: + if isinstance(span, StreamedSpan) and not isinstance( + span, NoOpStreamedSpan + ): + span.set_attribute( + "http.response.status_code", e.status_code + ) + + if e.status_code >= 400: + span.status = SpanStatus.ERROR.value + else: + span.status = SpanStatus.OK.value + else: + # Since a NoOpStreamedSpan can end up here, we have to guard against it + # so this only gets set in the legacy transaction approach. + if not isinstance(span, NoOpStreamedSpan): + span.set_http_status(e.status_code) + + if ( + e.status_code + in integration._failed_request_status_codes + ): + _capture_exception() + raise + except (asyncio.CancelledError, ConnectionResetError): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR.value + else: + span.set_status(SPANSTATUS.CANCELLED) + raise + except Exception: + # This will probably map to a 500 but seems like we + # have no way to tell. Do not set span status. + reraise(*_capture_exception()) + + try: + # A valid response handler will return a valid response with a status. But, if the handler + # returns an invalid response (e.g. None), the line below will raise an AttributeError. + # Even though this is likely invalid, we need to handle this case to ensure we don't break + # the application. + response_status = response.status + except AttributeError: + pass + else: + if isinstance(span, StreamedSpan): + span.set_attribute( + "http.response.status_code", response_status + ) + span.status = ( + SpanStatus.ERROR.value + if response_status >= 400 + else SpanStatus.OK.value + ) + else: + span.set_http_status(response_status) + + return response + + Application._handle = sentry_app_handle + + old_urldispatcher_resolve = UrlDispatcher.resolve + + @wraps(old_urldispatcher_resolve) + async def sentry_urldispatcher_resolve( + self: "UrlDispatcher", request: "Request" + ) -> "UrlMappingMatchInfo": + rv = await old_urldispatcher_resolve(self, request) + + integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) + if integration is None: + return rv + + name = None + + try: + if integration.transaction_style == "handler_name": + name = transaction_from_function(rv.handler) + elif integration.transaction_style == "method_and_path_pattern": + route_info = rv.get_info() + pattern = route_info.get("path") or route_info.get("formatter") + name = "{} {}".format(request.method, pattern) + except Exception: + pass + + if name is not None: + current_span = sentry_sdk.get_current_span() + if isinstance(current_span, StreamedSpan) and not isinstance( + current_span, NoOpStreamedSpan + ): + current_span._segment.name = name + current_span._segment.set_attribute( + "sentry.span.source", + SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, + ) + else: + current_scope = sentry_sdk.get_current_scope() + current_scope.set_transaction_name( + name, + source=SOURCE_FOR_STYLE[integration.transaction_style], + ) + + return rv + + UrlDispatcher.resolve = sentry_urldispatcher_resolve + + old_client_session_init = ClientSession.__init__ + + @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) + def init(*args: "Any", **kwargs: "Any") -> None: + client_trace_configs = list(kwargs.get("trace_configs") or ()) + trace_config = create_trace_config() + client_trace_configs.append(trace_config) + + kwargs["trace_configs"] = client_trace_configs + return old_client_session_init(*args, **kwargs) + + ClientSession.__init__ = init + + +def create_trace_config() -> "TraceConfig": + async def on_request_start( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestStartParams", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(AioHttpIntegration) is None: + return + + method = params.method.upper() + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(params.url), sanitize=False) + + span_name = "%s %s" % ( + method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ) + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(client.options): + attributes: "Attributes" = { + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": AioHttpIntegration.origin, + "http.request.method": method, + } + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + attributes["url.path"] = params.url.path + + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) + else: + legacy_span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=span_name, + origin=AioHttpIntegration.origin, + ) + legacy_span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + legacy_span.set_data("url", parsed_url.url) + legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span = legacy_span + + if should_propagate_trace(client, str(params.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=params.url + ) + ) + if key == BAGGAGE_HEADER_NAME and params.headers.get( + BAGGAGE_HEADER_NAME + ): + # do not overwrite any existing baggage, just append to it + params.headers[key] += "," + value + else: + params.headers[key] = value + + trace_config_ctx.span = span + + async def on_request_end( + session: "ClientSession", + trace_config_ctx: "SimpleNamespace", + params: "TraceRequestEndParams", + ) -> None: + if trace_config_ctx.span is None: + return + + span = trace_config_ctx.span + status = int(params.response.status) + + if isinstance(span, StreamedSpan): + span.set_attribute("http.response.status_code", status) + span.status = ( + SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value + ) + + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.set_http_status(status) + span.set_data("reason", params.response.reason) + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + trace_config = TraceConfig() + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + + return trace_config + + +def _make_request_processor( + weak_request: "weakref.ReferenceType[Request]", +) -> "EventProcessor": + def aiohttp_processor( + event: "Event", + hint: "dict[str, Tuple[type, BaseException, Any]]", + ) -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + request_info = event.setdefault("request", {}) + + request_info["url"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote} + request_info["headers"] = _filter_headers(dict(request.headers)) + + # Just attach raw data here if it is within bounds, if available. + # Unfortunately there's no way to get structured data from aiohttp + # without awaiting on some coroutine. + request_info["data"] = get_aiohttp_request_data(request) + + return event + + return aiohttp_processor + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "aiohttp", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + return exc_info + + +BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" + + +def get_aiohttp_request_data( + request: "Request", +) -> "Union[Optional[str], AnnotatedValue]": + bytes_body = request._read_bytes + + if bytes_body is not None: + # we have body to show + if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): + return AnnotatedValue.substituted_because_over_size_limit() + + encoding = request.charset or "utf-8" + return bytes_body.decode(encoding, "replace") + + if request.can_read_body: + # body exists but we can't show it + return BODY_NOT_READ_MESSAGE + + # request has no body + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py new file mode 100644 index 0000000000..49459268e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Any, Awaitable, Callable, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import aiomysql # type: ignore[import-not-found] + from aiomysql.connection import Connection # type: ignore[import-not-found] + from aiomysql.cursors import Cursor # type: ignore[import-not-found] +except ImportError: + raise DidNotEnable("aiomysql not installed.") + + +class AioMySQLIntegration(Integration): + identifier = "aiomysql" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AioMySQLIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + aiomysql_version = parse_version(aiomysql.__version__) + _check_minimum_version(AioMySQLIntegration, aiomysql_version) + + Cursor.execute = _wrap_execute(Cursor.execute) + Cursor.executemany = _wrap_executemany(Cursor.executemany) + + # Patch Connection._connect — this catches ALL connections: + # - aiomysql.connect() + # - aiomysql.create_pool() (pool.py does `from .connection import connect` + # which ultimately calls Connection._connect) + # - Reconnects + Connection._connect = _wrap_connect(Connection._connect) + + +T = TypeVar("T") + + +def _normalize_query(query: str | bytes | bytearray) -> str: + if isinstance(query, (bytes, bytearray)): + query = query.decode("utf-8", errors="replace") + return " ".join(query.split()) + + +def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.execute to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + + # Skip if flagged by executemany (avoids double-recording). + # Do NOT reset the flag here — it must stay True for the entire + # duration of executemany, which may call execute multiple times + # in a loop (non-INSERT fallback). Only _wrap_executemany's + # finally block should clear it. + if getattr(cursor, "_sentry_skip_next_execute", False): + return await f(*args, **kwargs) + + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = params if integration and integration._record_params else None + param_style = "pyformat" if params_list else None + + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=False, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Cursor.executemany to capture SQL queries.""" + + async def _inner(*args: Any, **kwargs: Any) -> T: + if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + query = args[1] if len(args) > 1 else kwargs.get("query", "") + query_str = _normalize_query(query) + seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") + + conn = _get_connection(cursor) + + integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) + params_list = ( + seq_of_params if integration and integration._record_params else None + ) + param_style = "pyformat" if params_list else None + + # Prevent double-recording: _do_execute_many calls self.execute internally + cursor._sentry_skip_next_execute = True + try: + with record_sql_queries( + cursor=None, + query=query_str, + params_list=params_list, + paramstyle=param_style, + executemany=True, + span_origin=AioMySQLIntegration.origin, + ) as span: + if conn: + _set_db_data(span, conn) + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + finally: + cursor._sentry_skip_next_execute = False + + return _inner + + +def _get_connection(cursor: Any) -> Any: + """Get the underlying connection from a cursor.""" + return getattr(cursor, "connection", None) + + +def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: + """Wrap Connection._connect to capture connection spans.""" + + async def _inner(self: "Connection") -> T: + client = sentry_sdk.get_client() + if client.get_integration(AioMySQLIntegration) is None: + return await f(self) + + if has_span_streaming_enabled(client.options): + breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) + + span_attributes: dict[str, Any] = { + "sentry.op": OP.DB, + "sentry.origin": AioMySQLIntegration.origin, + } | breadcrumb_data + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=breadcrumb_data + ) + res = await f(self) + else: + connect_data = _get_connect_data(self) + + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AioMySQLIntegration.origin, + ) as span: + _set_db_data(span, self) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", + category="query", + data=connect_data, + ) + res = await f(self) + + return res + + return _inner + + +def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: + if use_streaming_keys: + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + data: dict[str, Any] = { + db_system: "mysql", + SPANDATA.DB_DRIVER_NAME: "aiomysql", + } + + host = getattr(conn, "host", None) + if host is not None: + data[SPANDATA.SERVER_ADDRESS] = host + + port = getattr(conn, "port", None) + if port is not None: + data[SPANDATA.SERVER_PORT] = port + + database = getattr(conn, "db", None) + if database is not None: + data[db_name] = database + + user = getattr(conn, "user", None) + if user is not None: + data[SPANDATA.DB_USER] = user + + return data + + +def _set_db_data(span: Any, conn: Any) -> None: + """Set database-related span data from connection object.""" + if isinstance(span, StreamedSpan): + set_value = span.set_attribute + db_system = SPANDATA.DB_SYSTEM_NAME + db_name = SPANDATA.DB_NAMESPACE + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + set_value = span.set_data + db_system = SPANDATA.DB_SYSTEM + db_name = SPANDATA.DB_NAME + + set_value(db_system, "mysql") + set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") + + host = getattr(conn, "host", None) + if host is not None: + set_value(SPANDATA.SERVER_ADDRESS, host) + + port = getattr(conn, "port", None) + if port is not None: + set_value(SPANDATA.SERVER_PORT, port) + + database = getattr(conn, "db", None) + if database is not None: + set_value(db_name, database) + + user = getattr(conn, "user", None) + if user is not None: + set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py new file mode 100644 index 0000000000..dfa4aef34c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py @@ -0,0 +1,1154 @@ +import json +import sys +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_anthropic_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + package_version, + reraise, + safe_serialize, +) + +try: + try: + from anthropic import NotGiven + except ImportError: + NotGiven = None + + try: + from anthropic import Omit + except ImportError: + Omit = None + + from anthropic import AsyncStream, Stream + from anthropic.lib.streaming import ( + AsyncMessageStream, + AsyncMessageStreamManager, + MessageStream, + MessageStreamManager, + ) + from anthropic.resources import AsyncMessages, Messages + from anthropic.types import ( + ContentBlockDeltaEvent, + ContentBlockStartEvent, + ContentBlockStopEvent, + MessageDeltaEvent, + MessageStartEvent, + MessageStopEvent, + ) + + if TYPE_CHECKING: + from anthropic.types import MessageStreamEvent, TextBlockParam +except ImportError: + raise DidNotEnable("Anthropic not installed") + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Iterator, + Optional, + Union, + ) + + from anthropic.types import ( + MessageParam, + ModelParam, + RawMessageStreamEvent, + TextBlockParam, + ToolUnionParam, + ) + + from sentry_sdk._types import TextPart + + +class _RecordedUsage: + output_tokens: int = 0 + input_tokens: int = 0 + cache_write_input_tokens: "Optional[int]" = 0 + cache_read_input_tokens: "Optional[int]" = 0 + + +class _StreamSpanContext: + """ + Sets accumulated data on the stream's span and finishes the span on exit. + Is a no-op if the stream has no span set, i.e., when the span has already been finished. + """ + + def __init__( + self, + stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", + # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). + guaranteed_streaming_state: bool = False, + ) -> None: + self._stream = stream + self._guaranteed_streaming_state = guaranteed_streaming_state + + def __enter__(self) -> "_StreamSpanContext": + return self + + def __exit__( + self, + exc_type: "Optional[type[BaseException]]", + exc_val: "Optional[BaseException]", + exc_tb: "Optional[Any]", + ) -> None: + with capture_internal_exceptions(): + if not hasattr(self._stream, "_span"): + return + + if not self._guaranteed_streaming_state and not hasattr( + self._stream, "_model" + ): + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + return + + _set_streaming_output_data( + span=self._stream._span, + integration=self._stream._integration, + model=self._stream._model, + usage=self._stream._usage, + content_blocks=self._stream._content_blocks, + response_id=self._stream._response_id, + finish_reason=self._stream._finish_reason, + ) + + self._stream._span.__exit__(exc_type, exc_val, exc_tb) + del self._stream._span + + +class AnthropicIntegration(Integration): + identifier = "anthropic" + origin = f"auto.ai.{identifier}" + + def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + version = package_version("anthropic") + _check_minimum_version(AnthropicIntegration, version) + + """ + client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. + Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. + The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, + information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. + + The span can be finished in two places: + - When the user exits the context manager or directly calls close(), the patched close() finishes the span. + - When iteration ends, the finally block in the _iterator wrapper finishes the span. + + Both paths may run. For example, the context manager exit can follow iterator exhaustion. + """ + Messages.create = _wrap_message_create(Messages.create) + Stream.close = _wrap_close(Stream.close) + + AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) + AsyncStream.close = _wrap_async_close(AsyncStream.close) + + """ + client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. + """ + Messages.stream = _wrap_message_stream(Messages.stream) + MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( + MessageStreamManager.__enter__ + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. + if not issubclass(MessageStream, Stream): + MessageStream.close = _wrap_close(MessageStream.close) + + AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) + AsyncMessageStreamManager.__aenter__ = ( + _wrap_async_message_stream_manager_aenter( + AsyncMessageStreamManager.__aenter__ + ) + ) + + # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a + # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. + if not issubclass(AsyncMessageStream, AsyncStream): + AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "anthropic", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": + """ + Get token usage from the Anthropic response. + Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) + """ + input_tokens = 0 + output_tokens = 0 + cache_read_input_tokens = 0 + cache_write_input_tokens = 0 + if hasattr(result, "usage"): + usage = result.usage + if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): + input_tokens = usage.input_tokens + if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): + output_tokens = usage.output_tokens + if hasattr(usage, "cache_read_input_tokens") and isinstance( + usage.cache_read_input_tokens, int + ): + cache_read_input_tokens = usage.cache_read_input_tokens + if hasattr(usage, "cache_creation_input_tokens") and isinstance( + usage.cache_creation_input_tokens, int + ): + cache_write_input_tokens = usage.cache_creation_input_tokens + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens so downstream cost calculations + # (input_tokens - cached) don't produce negative values. + input_tokens += cache_read_input_tokens + cache_write_input_tokens + + return ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) + + +def _collect_ai_data( + event: "MessageStreamEvent", + model: "str | None", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": + """ + Collect model information, token usage, and collect content blocks from the AI streaming response. + """ + with capture_internal_exceptions(): + if hasattr(event, "type"): + if event.type == "content_block_start": + pass + elif event.type == "content_block_delta": + if hasattr(event.delta, "text"): + content_blocks.append(event.delta.text) + elif hasattr(event.delta, "partial_json"): + content_blocks.append(event.delta.partial_json) + elif event.type == "content_block_stop": + pass + + # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. + # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 + if event.type == "message_start": + model = event.message.model or model + response_id = event.message.id + + incoming_usage = event.message.usage + usage.output_tokens = incoming_usage.output_tokens + usage.input_tokens = incoming_usage.input_tokens + + usage.cache_write_input_tokens = getattr( + incoming_usage, "cache_creation_input_tokens", None + ) + usage.cache_read_input_tokens = getattr( + incoming_usage, "cache_read_input_tokens", None + ) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + # Counterintuitive, but message_delta contains cumulative token counts :) + if event.type == "message_delta": + usage.output_tokens = event.usage.output_tokens + + # Update other usage fields if they exist in the event + input_tokens = getattr(event.usage, "input_tokens", None) + if input_tokens is not None: + usage.input_tokens = input_tokens + + cache_creation_input_tokens = getattr( + event.usage, "cache_creation_input_tokens", None + ) + if cache_creation_input_tokens is not None: + usage.cache_write_input_tokens = cache_creation_input_tokens + + cache_read_input_tokens = getattr( + event.usage, "cache_read_input_tokens", None + ) + if cache_read_input_tokens is not None: + usage.cache_read_input_tokens = cache_read_input_tokens + # TODO: Record event.usage.server_tool_use + + if event.delta.stop_reason is not None: + finish_reason = event.delta.stop_reason + + return (model, usage, content_blocks, response_id, finish_reason) + + return ( + model, + usage, + content_blocks, + response_id, + finish_reason, + ) + + +def _transform_anthropic_content_block( + content_block: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Transform an Anthropic content block using the Anthropic-specific transformer, + with special handling for Anthropic's text-type documents. + """ + # Handle Anthropic's text-type documents specially (not covered by shared function) + if content_block.get("type") == "document": + source = content_block.get("source") + if isinstance(source, dict) and source.get("type") == "text": + return { + "type": "text", + "text": source.get("data", ""), + } + + # Use Anthropic-specific transformation + result = transform_anthropic_content_part(content_block) + return result if result is not None else content_block + + +def _transform_system_instructions( + system_instructions: "Union[str, Iterable[TextBlockParam]]", +) -> "list[TextPart]": + if isinstance(system_instructions, str): + return [ + { + "type": "text", + "content": system_instructions, + } + ] + + return [ + { + "type": "text", + "content": instruction["text"], + } + for instruction in system_instructions + if isinstance(instruction, dict) and "text" in instruction + ] + + +def _set_common_input_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Optional[Union[str, Iterable[TextBlockParam]]]", + temperature: "Optional[float]", + top_k: "Optional[int]", + top_p: "Optional[float]", + tools: "Optional[Iterable[ToolUnionParam]]", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if ( + messages is not None + and len(messages) > 0 # type: ignore + and should_send_default_pii() + and integration.include_prompts + ): + if isinstance(system, str) or isinstance(system, Iterable): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system)), + ) + + normalized_messages = [] + for message in messages: + if ( + message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER + and "content" in message + and isinstance(message["content"], (list, tuple)) + ): + transformed_content = [] + for item in message["content"]: + # Skip tool_result items - they can contain images/documents + # with nested structures that are difficult to redact properly + if isinstance(item, dict) and item.get("type") == "tool_result": + continue + + # Transform content blocks (images, documents, etc.) + transformed_content.append( + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + ) + + # If there are non-tool-result items, add them as a message + if transformed_content: + normalized_messages.append( + { + "role": message.get("role"), + "content": transformed_content, + } + ) + else: + # Transform content for non-list messages or assistant messages + transformed_message = message.copy() + if "content" in transformed_message: + content = transformed_message["content"] + if isinstance(content, (list, tuple)): + transformed_message["content"] = [ + _transform_anthropic_content_block(item) + if isinstance(item, dict) + else item + for item in content + ] + normalized_messages.append(transformed_message) + + role_normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(role_normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else role_normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + if model is not None and _is_given(model): + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + if top_k is not None and _is_given(top_k): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore + set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) + + +def _set_create_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "AnthropicIntegration", +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=kwargs.get("max_tokens"), # type: ignore + messages=kwargs.get("messages"), # type: ignore + model=kwargs.get("model"), + system=kwargs.get("system"), + temperature=kwargs.get("temperature"), + top_k=kwargs.get("top_k"), + top_p=kwargs.get("top_p"), + tools=kwargs.get("tools"), + ) + + +def _wrap_synchronous_message_iterator( + stream: "Union[Stream, MessageStream]", + iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +async def _wrap_asynchronous_message_iterator( + stream: "Union[AsyncStream, AsyncMessageStream]", + iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", +) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. + """ + with _StreamSpanContext(stream, guaranteed_streaming_state=True): + async for event in iterator: + # Message and content types are aliases for corresponding Raw* types, introduced in + # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + if not isinstance( + event, + ( + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, + ), + ): + yield event + continue + + _accumulate_event_data(stream, event) + yield event + + +def _set_output_data( + span: "Union[Span, StreamedSpan]", + integration: "AnthropicIntegration", + model: "str | None", + input_tokens: "int | None", + output_tokens: "int | None", + cache_read_input_tokens: "int | None", + cache_write_input_tokens: "int | None", + content_blocks: "list[Any]", + response_id: "str | None" = None, + finish_reason: "str | None" = None, +) -> None: + """ + Set output data for the span based on the AI response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + if finish_reason is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in content_blocks: + if output["type"] == "text": + output_messages["response"].append(output["text"]) + elif output["type"] == "tool_use": + output_messages["tool"].append(output) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + record_token_usage( + span, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_tokens_cached=cache_read_input_tokens, + input_tokens_cache_write=cache_write_input_tokens, + ) + + +def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, Stream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_synchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +async def _sentry_patched_create_async( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Creates and manages an AI Client Span for both non-streaming and streaming calls. + """ + integration = kwargs.pop("integration") + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + return await f(*args, **kwargs) + + model = kwargs.get("model", "") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + _set_create_input_data(span, kwargs, integration) + + try: + result = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + if isinstance(result, AsyncStream): + result._span = span + result._integration = integration + + _initialize_data_accumulation_state(result) + result._iterator = _wrap_asynchronous_message_iterator( + result, + result._iterator, + ) + + return result + + with capture_internal_exceptions(): + if hasattr(result, "content"): + ( + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ) = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + + _set_output_data( + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_input_tokens=cache_read_input_tokens, + cache_write_input_tokens=cache_write_input_tokens, + content_blocks=content_blocks, + response_id=getattr(result, "id", None), + finish_reason=getattr(result, "stop_reason", None), + ) + elif isinstance(span, Span): + span.set_data("unknown_response", True) + + span.__exit__(None, None, None) + + return result + + +def _wrap_message_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return _sentry_patched_create_sync(f, *args, **kwargs) + + return _sentry_wrapped_create_sync + + +def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: + """ + Initialize fields for accumulating output on the Stream instance. + """ + if not hasattr(stream, "_model"): + stream._model = None + stream._usage = _RecordedUsage() + stream._content_blocks = [] + stream._response_id = None + stream._finish_reason = None + + +def _accumulate_event_data( + stream: "Union[Stream, MessageStream]", + event: "Union[RawMessageStreamEvent, MessageStreamEvent]", +) -> None: + """ + Update accumulated output from a single stream event. + """ + (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( + event, + stream._model, + stream._usage, + stream._content_blocks, + stream._response_id, + stream._finish_reason, + ) + + stream._model = model + stream._usage = usage + stream._content_blocks = content_blocks + stream._response_id = response_id + stream._finish_reason = finish_reason + + +def _set_streaming_output_data( + span: "Span", + integration: "AnthropicIntegration", + model: "Optional[str]", + usage: "_RecordedUsage", + content_blocks: "list[str]", + response_id: "Optional[str]", + finish_reason: "Optional[str]", +) -> None: + """ + Set output attributes on the AI Client Span. + """ + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + response_id=response_id, + finish_reason=finish_reason, + ) + + +def _wrap_close( + f: "Callable[..., None]", +) -> "Callable[..., None]": + """ + Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. + """ + + def close(self: "Union[Stream, MessageStream]") -> None: + with _StreamSpanContext(self): + return f(self) + + return close + + +def _wrap_message_create_async(f: "Any") -> "Any": + @wraps(f) + async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + kwargs["integration"] = integration + + return await _sentry_patched_create_async(f, *args, **kwargs) + + return _sentry_wrapped_create_async + + +def _wrap_async_close( + f: "Callable[..., Awaitable[None]]", +) -> "Callable[..., Awaitable[None]]": + """ + Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. + """ + + async def close(self: "AsyncStream") -> None: + with _StreamSpanContext(self): + return await f(self) + + return close + + +def _wrap_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_message_stream_manager_enter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": + if not hasattr(self, "_max_tokens"): + return f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return f(self) + + if self._messages is None: + return f(self) + + try: + iter(self._messages) + except TypeError: + return f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_synchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_enter + + +def _wrap_async_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on AI Client Spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream( + *args: "Any", **kwargs: "Any" + ) -> "AsyncMessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": + """ + Creates and manages AI Client Spans. + """ + + @wraps(f) + async def _sentry_patched_aenter( + self: "AsyncMessageStreamManager", + ) -> "AsyncMessageStream": + if not hasattr(self, "_max_tokens"): + return await f(self) + + client = sentry_sdk.get_client() + integration = client.get_integration(AnthropicIntegration) + + if integration is None: + return await f(self) + + if self._messages is None: + return await f(self) + + try: + iter(self._messages) + except TypeError: + return await f(self) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name="chat" if self._model is None else f"chat {self._model}".strip(), + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": AnthropicIntegration.origin, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name="chat" if self._model is None else f"chat {self._model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + _set_common_input_data( + span=span, + integration=integration, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + + try: + stream = await f(self) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + stream._span = span + stream._integration = integration + + _initialize_data_accumulation_state(stream) + stream._iterator = _wrap_asynchronous_message_iterator( + stream, + stream._iterator, + ) + + return stream + + return _sentry_patched_aenter + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different anthropic versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py new file mode 100644 index 0000000000..0215ffa093 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py @@ -0,0 +1,28 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class ArgvIntegration(Integration): + identifier = "argv" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: + extra = event.setdefault("extra", {}) + # If some event processor decided to set extra to e.g. an + # `int`, don't crash. Not here. + if isinstance(extra, dict): + extra["sys.argv"] = sys.argv + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py new file mode 100644 index 0000000000..1ce228d3a5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py @@ -0,0 +1,167 @@ +from importlib import import_module + +import sentry_sdk +from sentry_sdk import capture_event, get_client +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + # importing like this is necessary due to name shadowing in ariadne + # (ariadne.graphql is also a function) + ariadne_graphql = import_module("ariadne.graphql") +except ImportError: + raise DidNotEnable("ariadne is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, List, Optional + + from ariadne.types import ( # type: ignore + GraphQLError, + GraphQLResult, + GraphQLSchema, + QueryParser, + ) + from graphql.language.ast import DocumentNode + + from sentry_sdk._types import Event, EventProcessor + + +class AriadneIntegration(Integration): + identifier = "ariadne" + + @staticmethod + def setup_once() -> None: + version = package_version("ariadne") + _check_minimum_version(AriadneIntegration, version) + + ignore_logger("ariadne") + + _patch_graphql() + + +def _patch_graphql() -> None: + old_parse_query = ariadne_graphql.parse_query + old_handle_errors = ariadne_graphql.handle_graphql_errors + old_handle_query_result = ariadne_graphql.handle_query_result + + @ensure_integration_enabled(AriadneIntegration, old_parse_query) + def _sentry_patched_parse_query( + context_value: "Optional[Any]", + query_parser: "Optional[QueryParser]", + data: "Any", + ) -> "DocumentNode": + event_processor = _make_request_event_processor(data) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + result = old_parse_query(context_value, query_parser, data) + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_errors) + def _sentry_patched_handle_graphql_errors( + errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + result = old_handle_errors(errors, *args, **kwargs) + + event_processor = _make_response_event_processor(result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return result + + @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) + def _sentry_patched_handle_query_result( + result: "Any", *args: "Any", **kwargs: "Any" + ) -> "GraphQLResult": + query_result = old_handle_query_result(result, *args, **kwargs) + + event_processor = _make_response_event_processor(query_result[1]) + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + + client = get_client() + if client.is_active(): + with capture_internal_exceptions(): + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": AriadneIntegration.identifier, + "handled": False, + }, + ) + capture_event(event, hint=hint) + + return query_result + + ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore + ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore + ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore + + +def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": + """Add request data and api_target to events.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + if not isinstance(data, dict): + return event + + with capture_internal_exceptions(): + try: + content_length = int( + (data.get("headers") or {}).get("Content-Length", 0) + ) + except (TypeError, ValueError): + return event + + if should_send_default_pii() and request_body_within_bounds( + get_client(), content_length + ): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + request_info["data"] = data + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + return inner + + +def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": + """Add response data to the event's response context.""" + + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii() and response.get("errors"): + contexts = event.setdefault("contexts", {}) + contexts["response"] = { + "data": response, + } + + return event + + return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py new file mode 100644 index 0000000000..da03bafb8b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py @@ -0,0 +1,277 @@ +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Transaction, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +try: + import arq.worker + from arq.connections import ArqRedis + from arq.version import VERSION as ARQ_VERSION + from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker +except ImportError: + raise DidNotEnable("Arq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from arq.cron import CronJob + from arq.jobs import Job + from arq.typing import WorkerCoroutine + from arq.worker import Function + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) + + +class ArqIntegration(Integration): + identifier = "arq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + try: + if isinstance(ARQ_VERSION, str): + version = parse_version(ARQ_VERSION) + else: + version = ARQ_VERSION.version[:2] + + except (TypeError, ValueError): + version = None + + _check_minimum_version(ArqIntegration, version) + + patch_enqueue_job() + patch_run_job() + patch_create_worker() + + _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore + + ignore_logger("arq.worker") + + +def patch_enqueue_job() -> None: + old_enqueue_job = ArqRedis.enqueue_job + original_kwdefaults = old_enqueue_job.__kwdefaults__ + + async def _sentry_enqueue_job( + self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" + ) -> "Optional[Job]": + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_enqueue_job(self, function, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=function, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_ARQ, + "sentry.origin": ArqIntegration.origin, + }, + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin + ): + return await old_enqueue_job(self, function, *args, **kwargs) + + _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults + ArqRedis.enqueue_job = _sentry_enqueue_job + + +def patch_run_job() -> None: + old_run_job = Worker.run_job + + async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: + client = sentry_sdk.get_client() + if client.get_integration(ArqIntegration) is None: + return await old_run_job(self, job_id, score) + + with sentry_sdk.isolation_scope() as scope: + scope._name = "arq" + scope.clear_breadcrumbs() + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name="unknown arq task", + attributes={ + "sentry.op": OP.QUEUE_TASK_ARQ, + "sentry.origin": ArqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job_id, + }, + parent_span=None, + ): + return await old_run_job(self, job_id, score) + + transaction = Transaction( + name="unknown arq task", + status="ok", + op=OP.QUEUE_TASK_ARQ, + source=TransactionSource.TASK, + origin=ArqIntegration.origin, + ) + + with sentry_sdk.start_transaction(transaction): + return await old_run_job(self, job_id, score) + + Worker.run_job = _sentry_run_job + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + + if scope.transaction is not None: + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + scope.transaction.set_status(SPANSTATUS.ABORTED) + return + + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + + if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": ArqIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + if scope.transaction is not None: + scope.transaction.name = ctx["job_name"] + event["transaction"] = ctx["job_name"] + + tags = event.setdefault("tags", {}) + tags["arq_task_id"] = ctx["job_id"] + tags["arq_task_retry"] = ctx["job_try"] > 1 + extra = event.setdefault("extra", {}) + extra["arq-job"] = { + "task": ctx["job_name"], + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": ctx["job_try"], + } + + return event + + return event_processor + + +def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": + async def _sentry_coroutine( + ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(ArqIntegration) + if integration is None: + return await coroutine(ctx, *args, **kwargs) + + if has_span_streaming_enabled(client.options): + scope = sentry_sdk.get_current_scope() + span = scope.streamed_span + if span is not None: + span.name = name + + scope.set_transaction_name(name) + + sentry_sdk.get_isolation_scope().add_event_processor( + _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) + ) + + try: + result = await coroutine(ctx, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_coroutine + + +def patch_create_worker() -> None: + old_create_worker = arq.worker.create_worker + + @ensure_integration_enabled(ArqIntegration, old_create_worker) + def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": + settings_cls = args[0] if args else kwargs.get("settings_cls") + + if isinstance(settings_cls, dict): + if "functions" in settings_cls: + settings_cls["functions"] = [ + _get_arq_function(func) + for func in settings_cls.get("functions", []) + ] + if "cron_jobs" in settings_cls: + settings_cls["cron_jobs"] = [ + _get_arq_cron_job(cron_job) + for cron_job in settings_cls.get("cron_jobs", []) + ] + + if hasattr(settings_cls, "functions"): + settings_cls.functions = [ # type: ignore[union-attr] + _get_arq_function(func) + for func in settings_cls.functions # type: ignore[union-attr] + ] + if hasattr(settings_cls, "cron_jobs"): + settings_cls.cron_jobs = [ # type: ignore[union-attr] + _get_arq_cron_job(cron_job) + for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] + ] + + if "functions" in kwargs: + kwargs["functions"] = [ + _get_arq_function(func) for func in kwargs.get("functions", []) + ] + if "cron_jobs" in kwargs: + kwargs["cron_jobs"] = [ + _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) + ] + + return old_create_worker(*args, **kwargs) + + arq.worker.create_worker = _sentry_create_worker + + +def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": + arq_func = arq.worker.func(func) + arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) + + return arq_func + + +def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": + cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) + + return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py new file mode 100644 index 0000000000..8b1ff5e2a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py @@ -0,0 +1,543 @@ +""" +An ASGI middleware. + +Based on Tom Christie's `sentry-asgi `. +""" + +import inspect +import sys +from copy import deepcopy +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._asgi_common import ( + _get_headers, + _get_ip, + _get_path, + _get_request_attributes, + _get_request_data, + _get_url, + _RootPathInPath, +) +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import ( + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) +from sentry_sdk.traces import ( + SegmentSource, + StreamedSpan, +) +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + Transaction, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + ContextVar, + _get_installed_modules, + capture_internal_exceptions, + event_from_exception, + logger, + qualname_from_function, + reraise, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, ContextManager, Dict, Optional, Tuple, Union + + from sentry_sdk._types import Attributes, Event, Hint + from sentry_sdk.tracing import Span + + +_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") + +_DEFAULT_TRANSACTION_NAME = "generic ASGI request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 +if sys.version_info >= (3, 14): + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": mechanism_type, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _looks_like_asgi3(app: "Any") -> bool: + """ + Try to figure out if an application object supports ASGI3. + + This is how uvicorn figures out the application version as well. + """ + if inspect.isclass(app): + return hasattr(app, "__await__") + elif inspect.isfunction(app): + return iscoroutinefunction(app) + else: + call = getattr(app, "__call__", None) # noqa + return iscoroutinefunction(call) + + +class SentryAsgiMiddleware: + __slots__ = ( + "app", + "__call__", + "transaction_style", + "mechanism_type", + "span_origin", + "http_methods_to_capture", + "root_path_in_path", + ) + + def __init__( + self, + app: "Any", + unsafe_context_data: bool = False, + transaction_style: str = "endpoint", + mechanism_type: str = "asgi", + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + asgi_version: "Optional[int]" = None, + root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, + ) -> None: + """ + Instrument an ASGI application with Sentry. Provides HTTP/websocket + data to sent events and basic handling for exceptions bubbling up + through the middleware. + + :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. + """ + if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The ASGI middleware for Sentry requires Python 3.7+ " + "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + + asgi_middleware_while_using_starlette_or_fastapi = ( + mechanism_type == "asgi" and "starlette" in _get_installed_modules() + ) + if asgi_middleware_while_using_starlette_or_fastapi: + logger.warning( + "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " + "Please remove 'SentryAsgiMiddleware' from your project. " + "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." + ) + + self.transaction_style = transaction_style + self.mechanism_type = mechanism_type + self.span_origin = span_origin + self.app = app + self.http_methods_to_capture = http_methods_to_capture + self.root_path_in_path = root_path_in_path + + if asgi_version is None: + if _looks_like_asgi3(app): + asgi_version = 3 + else: + asgi_version = 2 + + if asgi_version == 3: + self.__call__ = self._run_asgi3 + elif asgi_version == 2: + self.__call__ = self._run_asgi2 # type: ignore + + def _capture_lifespan_exception(self, exc: Exception) -> None: + """Capture exceptions raise in application lifespan handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _capture_request_exception(self, exc: Exception) -> None: + """Capture exceptions raised in incoming request handlers. + + The separate function is needed to support overriding in derived integrations that use different catching mechanisms. + """ + return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) + + def _run_asgi2(self, scope: "Any") -> "Any": + async def inner(receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=2) + + return inner + + async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": + return await self._run_app(scope, receive, send, asgi_version=3) + + async def _run_app( + self, scope: "Any", receive: "Any", send: "Any", asgi_version: int + ) -> "Any": + is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) + is_lifespan = scope["type"] == "lifespan" + if is_recursive_asgi_middleware or is_lifespan: + try: + if asgi_version == 2: + return await self.app(scope)(receive, send) + else: + return await self.app(scope, receive, send) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_lifespan_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_lifespan_exception(exc) + reraise(*exc_info) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _asgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as sentry_scope: + with track_session(sentry_scope, session_mode="request"): + sentry_scope.clear_breadcrumbs() + sentry_scope._name = "asgi" + processor = partial(self.event_processor, asgi_scope=scope) + sentry_scope.add_event_processor(processor) + + ty = scope["type"] + ( + transaction_name, + transaction_source, + ) = self._get_transaction_name_and_source( + self.transaction_style, + scope, + ) + + method = scope.get("method", "").upper() + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + if span_streaming: + segment: "Optional[StreamedSpan]" = None + attributes: "Attributes" = { + "sentry.span.source": getattr( + transaction_source, "value", transaction_source + ), + "sentry.origin": self.span_origin, + "network.protocol.name": ty, + } + + if scope.get("client") and should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) + + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + sentry_sdk.traces.continue_trace(_get_headers(scope)) + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = f"{ty}.server" + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + else: + sentry_sdk.traces.new_trace() + + Scope.set_custom_sampling_context({"asgi_scope": scope}) + + attributes["sentry.op"] = OP.HTTP_SERVER + segment = sentry_sdk.traces.start_span( + name=transaction_name, + attributes=attributes, + parent_span=None, + ) + + span_ctx = segment or nullcontext() + + else: + transaction = None + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + transaction = continue_trace( + _get_headers(scope), + op="{}.server".format(ty), + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + else: + transaction = Transaction( + op=OP.HTTP_SERVER, + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + + if transaction: + transaction.set_tag("asgi.type", ty) + + span_ctx = ( + sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"asgi_scope": scope}, + ) + if transaction is not None + else nullcontext() + ) + + with span_ctx as span: + if isinstance(span, StreamedSpan): + for attribute, value in _get_request_attributes( + scope, + root_path_in_path=self.root_path_in_path, + ).items(): + span.set_attribute(attribute, value) + + try: + + async def _sentry_wrapped_send( + event: "Dict[str, Any]", + ) -> "Any": + if span is not None: + is_http_response = ( + event.get("type") == "http.response.start" + and "status" in event + ) + if is_http_response: + if isinstance(span, StreamedSpan): + span.status = ( + "error" + if event["status"] >= 400 + else "ok" + ) + span.set_attribute( + "http.response.status_code", + event["status"], + ) + else: + span.set_http_status(event["status"]) + + return await send(event) + + if asgi_version == 2: + return await self.app(scope)( + receive, _sentry_wrapped_send + ) + else: + return await self.app( + scope, receive, _sentry_wrapped_send + ) + + except Exception as exc: + suppress_chained_exceptions = ( + sentry_sdk.get_client() + .options.get("_experiments", {}) + .get("suppress_asgi_chained_exceptions", True) + ) + if suppress_chained_exceptions: + self._capture_request_exception(exc) + raise exc from None + + exc_info = sys.exc_info() + with capture_internal_exceptions(): + self._capture_request_exception(exc) + reraise(*exc_info) + + finally: + if isinstance(span, StreamedSpan): + already_set = ( + span is not None + and span.name != _DEFAULT_TRANSACTION_NAME + and span.get_attributes().get("sentry.span.source") + in [ + SegmentSource.COMPONENT.value, + SegmentSource.ROUTE.value, + SegmentSource.CUSTOM.value, + ] + ) + with capture_internal_exceptions(): + if not already_set: + name, source = ( + self._get_segment_name_and_source( + self.transaction_style, scope + ) + ) + span.name = name + span.set_attribute("sentry.span.source", source) + finally: + _asgi_middleware_applied.set(False) + + def event_processor( + self, event: "Event", hint: "Hint", asgi_scope: "Any" + ) -> "Optional[Event]": + request_data = event.get("request", {}) + request_data.update( + _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) + ) + event["request"] = deepcopy(request_data) + + # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) + transaction = event.get("transaction") + transaction_source = (event.get("transaction_info") or {}).get("source") + already_set = ( + transaction is not None + and transaction != _DEFAULT_TRANSACTION_NAME + and transaction_source + in [ + TransactionSource.COMPONENT, + TransactionSource.ROUTE, + TransactionSource.CUSTOM, + ] + ) + if not already_set: + name, source = self._get_transaction_name_and_source( + self.transaction_style, asgi_scope + ) + event["transaction"] = name + event["transaction_info"] = {"source": source} + + return event + + # Helper functions. + # + # Note: Those functions are not public API. If you want to mutate request + # data to your liking it's recommended to use the `before_send` callback + # for that. + + def _get_transaction_name_and_source( + self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SOURCE_FOR_STYLE[transaction_style] + ty = asgi_scope.get("type") + + if transaction_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = transaction_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + elif transaction_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = TransactionSource.URL + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + return name, source + + return name, source + + def _get_segment_name_and_source( + self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value + ty = asgi_scope.get("type") + + if segment_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = qualname_from_function(endpoint) or "" + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + elif segment_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path=_get_path( + asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path + ), + ) + source = SegmentSource.URL.value + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = SegmentSource.ROUTE.value + return name, source + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py new file mode 100644 index 0000000000..4b3f6d330e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py @@ -0,0 +1,280 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.transport import AsyncHttpTransport +from sentry_sdk.utils import ( + event_from_exception, + is_internal_task, + logger, + reraise, +) + +try: + import asyncio + from asyncio.tasks import Task +except ImportError: + raise DidNotEnable("asyncio not available") + +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from collections.abc import Coroutine + from typing import Any, Callable, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T", bound=Callable[..., Any]) + + +def get_name(coro: "Any") -> str: + return ( + getattr(coro, "__qualname__", None) + or getattr(coro, "__name__", None) + or "coroutine without __name__" + ) + + +def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": + # Only __name__ and __qualname__ are copied from function to coroutine in CPython + return functools.partial( + functools.update_wrapper, + wrapped=wrapped, # type: ignore + assigned=("__name__", "__qualname__"), + updated=(), + ) + + +def patch_loop_close() -> None: + """Patch loop.close to flush pending events before shutdown.""" + # Atexit shutdown hook happens after the event loop is closed. + # Therefore, it is necessary to patch the loop.close method to ensure + # that pending events are flushed before the interpreter shuts down. + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No running loop → cannot patch now + return + + if getattr(loop, "_sentry_flush_patched", False): + return + + async def _flush() -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + try: + if not isinstance(client.transport, AsyncHttpTransport): + return + + await client.close_async() + except Exception: + logger.warning("Sentry flush failed during loop shutdown", exc_info=True) + + orig_close = loop.close + + def _patched_close() -> None: + try: + loop.run_until_complete(_flush()) + except Exception: + logger.debug( + "Could not flush Sentry events during loop close", exc_info=True + ) + finally: + orig_close() + + loop.close = _patched_close # type: ignore + loop._sentry_flush_patched = True # type: ignore + + +def _create_task_with_factory( + orig_task_factory: "Any", + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", +) -> "asyncio.Task[Any]": + task = None + + # Trying to use user set task factory (if there is one) + if orig_task_factory: + task = orig_task_factory(loop, coro, **kwargs) + + if task is None: + # The default task factory in `asyncio` does not have its own function + # but is just a couple of lines in `asyncio.base_events.create_task()` + # Those lines are copied here. + + # WARNING: + # If the default behavior of the task creation in asyncio changes, + # this will break! + task = Task(coro, loop=loop, **kwargs) + if task._source_traceback: # type: ignore + del task._source_traceback[-1] # type: ignore + + return task + + +def patch_asyncio() -> None: + orig_task_factory = None + try: + loop = asyncio.get_running_loop() + orig_task_factory = loop.get_task_factory() + + # Check if already patched + if getattr(orig_task_factory, "_is_sentry_task_factory", False): + return + + def _sentry_task_factory( + loop: "asyncio.AbstractEventLoop", + coro: "Coroutine[Any, Any, Any]", + **kwargs: "Any", + ) -> "asyncio.Future[Any]": + # Check if this is an internal Sentry task + if is_internal_task(): + return _create_task_with_factory( + orig_task_factory, loop, coro, **kwargs + ) + + @_wrap_coroutine(coro) + async def _task_with_sentry_span_creation() -> "Any": + result = None + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncioIntegration) + task_spans = integration.task_spans if integration else False + + span_ctx: "Optional[Union[StreamedSpan, Span]]" = None + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope(): + if task_spans: + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=get_name(coro), + attributes={ + "sentry.op": OP.FUNCTION, + "sentry.origin": AsyncioIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.FUNCTION, + name=get_name(coro), + origin=AsyncioIntegration.origin, + ) + + with span_ctx if span_ctx else nullcontext(): + try: + result = await coro + except StopAsyncIteration as e: + raise e from None + except Exception: + reraise(*_capture_exception()) + + return result + + task = _create_task_with_factory( + orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs + ) + + # Set the task name to include the original coroutine's name + try: + task.set_name(f"{get_name(coro)} (Sentry-wrapped)") + except AttributeError: + # set_name might not be available in all Python versions + pass + + return task + + _sentry_task_factory._is_sentry_task_factory = True # type: ignore + loop.set_task_factory(_sentry_task_factory) # type: ignore + + except RuntimeError: + # When there is no running loop, we have nothing to patch. + logger.warning( + "There is no running asyncio loop so there is nothing Sentry can patch. " + "Please make sure you call sentry_sdk.init() within a running " + "asyncio loop for the AsyncioIntegration to work. " + "See https://docs.sentry.io/platforms/python/integrations/asyncio/" + ) + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + + integration = client.get_integration(AsyncioIntegration) + if integration is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "asyncio", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class AsyncioIntegration(Integration): + identifier = "asyncio" + origin = f"auto.function.{identifier}" + + def __init__(self, task_spans: bool = True) -> None: + self.task_spans = task_spans + + @staticmethod + def setup_once() -> None: + patch_asyncio() + patch_loop_close() + + +def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: + """ + Enable AsyncioIntegration with the provided options. + + This is useful in scenarios where Sentry needs to be initialized before + an event loop is set up, but you still want to instrument asyncio once there + is an event loop. In that case, you can sentry_sdk.init() early on without + the AsyncioIntegration and then, once the event loop has been set up, + execute: + + ```python + from sentry_sdk.integrations.asyncio import enable_asyncio_integration + + async def async_entrypoint(): + enable_asyncio_integration() + ``` + + Any arguments provided will be passed to AsyncioIntegration() as is. + + If AsyncioIntegration has already patched the current event loop, this + function won't have any effect. + + If AsyncioIntegration was provided in + sentry_sdk.init(disabled_integrations=[...]), this function will ignore that + and the integration will be enabled. + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # This function purposefully bypasses the integration machinery in + # integrations/__init__.py. _installed_integrations/_processed_integrations + # is used to prevent double patching the same module, but in the case of + # the AsyncioIntegration, we don't monkeypatch the standard library directly, + # we patch the currently running event loop, and we keep the record of doing + # that on the loop itself. + logger.debug("Setting up integration asyncio") + + integration = AsyncioIntegration(*args, **kwargs) + integration.setup_once() + + if "asyncio" not in client.integrations: + client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py new file mode 100644 index 0000000000..186176d268 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import contextlib +import re +from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_version, +) + +try: + import asyncpg # type: ignore + from asyncpg.cursor import ( # type: ignore + BaseCursor, + Cursor, + CursorIterator, + ) + +except ImportError: + raise DidNotEnable("asyncpg not installed.") + + +class AsyncPGIntegration(Integration): + identifier = "asyncpg" + origin = f"auto.db.{identifier}" + _record_params = False + + def __init__(self, *, record_params: bool = False): + AsyncPGIntegration._record_params = record_params + + @staticmethod + def setup_once() -> None: + # asyncpg.__version__ is a string containing the semantic version in the form of ".." + asyncpg_version = parse_version(asyncpg.__version__) + _check_minimum_version(AsyncPGIntegration, asyncpg_version) + + asyncpg.Connection.execute = _wrap_execute( + asyncpg.Connection.execute, + ) + + asyncpg.Connection._execute = _wrap_connection_method( + asyncpg.Connection._execute + ) + asyncpg.Connection._executemany = _wrap_connection_method( + asyncpg.Connection._executemany, executemany=True + ) + asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) + + BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) + BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) + + asyncpg.connect_utils._connect_addr = _wrap_connect_addr( + asyncpg.connect_utils._connect_addr + ) + + +T = TypeVar("T") + + +def _normalize_query(query: str) -> str: + return re.sub(r"\s+", " ", query).strip() + + +def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + # Avoid recording calls to _execute twice. + # Calls to Connection.execute with args also call + # Connection._execute, which is recorded separately + # args[0] = the connection object, args[1] is the query + if len(args) > 2: + return await f(*args, **kwargs) + + query = _normalize_query(args[1]) + with record_sql_queries( + cursor=None, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + span_origin=AsyncPGIntegration.origin, + ) as span: + res = await f(*args, **kwargs) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +SubCursor = TypeVar("SubCursor", bound=BaseCursor) + + +@contextlib.contextmanager +def _record( + cursor: "SubCursor | None", + query: str, + params_list: "tuple[Any, ...] | None", + *, + executemany: bool = False, +) -> "Iterator[Union[Span, StreamedSpan]]": + client = sentry_sdk.get_client() + integration = client.get_integration(AsyncPGIntegration) + if integration is not None and not integration._record_params: + params_list = None + + param_style = "pyformat" if params_list else None + + query = _normalize_query(query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=params_list, + paramstyle=param_style, + executemany=executemany, + record_cursor_repr=cursor is not None, + span_origin=AsyncPGIntegration.origin, + ) as span: + yield span + + +def _wrap_connection_method( + f: "Callable[..., Awaitable[T]]", *, executemany: bool = False +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + query = args[1] + params_list = args[2] if len(args) > 2 else None + with _record(None, query, params_list, executemany=executemany) as span: + _set_db_data(span, args[0]) + + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_cursor_method( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + cursor = args[0] + if type(cursor) is CursorIterator: + span_op_override_value = OP.DB_CURSOR_ITERATOR + elif type(cursor) is Cursor: + span_op_override_value = OP.DB_CURSOR_FETCH + else: + span_op_override_value = None + + query = _normalize_query(cursor._query) + with record_sql_queries( + cursor=cursor, + query=query, + params_list=None, + paramstyle=None, + executemany=False, + record_cursor_repr=True, + span_origin=AsyncPGIntegration.origin, + span_op_override_value=span_op_override_value, + ) as span: + _set_db_data(span, cursor._connection) + res = await f(*args, **kwargs) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return res + + return _inner + + +def _wrap_connect_addr( + f: "Callable[..., Awaitable[T]]", +) -> "Callable[..., Awaitable[T]]": + async def _inner(*args: "Any", **kwargs: "Any") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(AsyncPGIntegration) is None: + return await f(*args, **kwargs) + + user = kwargs["params"].user + database = kwargs["params"].database + addr = kwargs.get("addr") + + if has_span_streaming_enabled(client.options): + span_attributes = { + "sentry.op": OP.DB, + "sentry.origin": AsyncPGIntegration.origin, + SPANDATA.DB_SYSTEM_NAME: "postgresql", + SPANDATA.DB_USER: user, + SPANDATA.DB_NAMESPACE: database, + SPANDATA.DB_DRIVER_NAME: "asyncpg", + } + if addr: + try: + span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] + span_attributes[SPANDATA.SERVER_PORT] = addr[1] + except IndexError: + pass + + with sentry_sdk.traces.start_span( + name="connect", attributes=span_attributes + ) as span: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span_attributes + ) + res = await f(*args, **kwargs) + + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=AsyncPGIntegration.origin, + ) as span: + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + span.set_data(SPANDATA.DB_NAME, database) + span.set_data(SPANDATA.DB_USER, user) + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message="connect", category="query", data=span._data + ) + res = await f(*args, **kwargs) + + return res + + return _inner + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + addr = conn._addr + database = conn._params.database + user = conn._params.user + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") + if addr: + try: + span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_attribute(SPANDATA.DB_NAMESPACE, database) + + if user: + span.set_attribute(SPANDATA.DB_USER, user) + else: + # Remove this else block once we've completely migrated to streamed spans + # The use of deprecated attributes here is to ensure backwards compatibility + span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") + + if addr: + try: + span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) + span.set_data(SPANDATA.SERVER_PORT, addr[1]) + except IndexError: + pass + + if database: + span.set_data(SPANDATA.DB_NAME, database) + + if user: + span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py new file mode 100644 index 0000000000..b573e76f09 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py @@ -0,0 +1,51 @@ +import atexit +import os +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Optional + + +def default_callback(pending: int, timeout: int) -> None: + """This is the default shutdown callback that is set on the options. + It prints out a message to stderr that informs the user that some events + are still pending and the process is waiting for them to flush out. + """ + + def echo(msg: str) -> None: + sys.stderr.write(msg + "\n") + + echo("Sentry is attempting to send %i pending events" % pending) + echo("Waiting up to %s seconds" % timeout) + echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) + sys.stderr.flush() + + +class AtexitIntegration(Integration): + identifier = "atexit" + + def __init__(self, callback: "Optional[Any]" = None) -> None: + if callback is None: + callback = default_callback + self.callback = callback + + @staticmethod + def setup_once() -> None: + @atexit.register + def _shutdown() -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(AtexitIntegration) + + if integration is None: + return + + logger.debug("atexit: got shutdown signal") + logger.debug("atexit: shutting down client") + sentry_sdk.get_isolation_scope().end_session() + + client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py new file mode 100644 index 0000000000..c7fe77714a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py @@ -0,0 +1,542 @@ +import functools +import json +import re +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING +from urllib.parse import urlencode + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.cloud_resource_context import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + +# Constants +TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + + +def _wrap_init_error(init_error: "F") -> "F": + @ensure_integration_enabled(AwsLambdaIntegration, init_error) + def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + with capture_internal_exceptions(): + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + exc_info = sys.exc_info() + if exc_info and all(exc_info): + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + + else: + # Fall back to AWS lambdas JSON representation of the error + error_info = args[1] + if isinstance(error_info, str): + error_info = json.loads(error_info) + sentry_event = _event_from_error_json(error_info) + sentry_sdk.capture_event(sentry_event) + + return init_error(*args, **kwargs) + + return sentry_init_error # type: ignore + + +def _wrap_handler(handler: "F") -> "F": + @functools.wraps(handler) + def sentry_handler( + aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, + # `event` here is *likely* a dictionary, but also might be a number of + # other types (str, int, float, None). + # + # In some cases, it is a list (if the user is batch-invoking their + # function, for example), in which case we'll use the first entry as a + # representative from which to try pulling request data. (Presumably it + # will be the same for all events in the list, since they're all hitting + # the lambda in the same request.) + + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + + if integration is None: + return handler(aws_event, aws_context, *args, **kwargs) + + if isinstance(aws_event, list) and len(aws_event) >= 1: + request_data = aws_event[0] + batch_size = len(aws_event) + else: + request_data = aws_event + batch_size = 1 + + if not isinstance(request_data, dict): + # If we're not dealing with a dictionary, we won't be able to get + # headers, path, http method, etc in any case, so it's fine that + # this is empty + request_data = {} + + configured_time = aws_context.get_remaining_time_in_millis() + aws_region = aws_context.invoked_function_arn.split(":")[3] + + with sentry_sdk.isolation_scope() as scope: + timeout_thread = None + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + request_data, aws_context, configured_time + ) + ) + scope.set_tag("aws_region", aws_region) + if batch_size > 1: + scope.set_tag("batch_request", True) + scope.set_tag("batch_size", batch_size) + + # Starting the Timeout thread only if the configured time is greater than Timeout warning + # buffer and timeout_warning parameter is set True. + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = ( + configured_time - TIMEOUT_WARNING_BUFFER + ) / MILLIS_TO_SECONDS + + timeout_thread = TimeoutThread( + waiting_time, + configured_time / MILLIS_TO_SECONDS, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = request_data.get("headers", {}) + # Some AWS Services (ie. EventBridge) set headers as a list + # or None, so we must ensure it is a dict + if not isinstance(headers, dict): + headers = {} + + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + additional_attributes: "dict[str, Any]" = {} + if "httpMethod" in request_data: + additional_attributes["http.request.method"] = request_data[ + "httpMethod" + ] + + if should_send_default_pii() and "queryStringParameters" in request_data: + qs = request_data["queryStringParameters"] + if qs: + additional_attributes["url.query"] = urlencode(qs) + + sampling_context = { + "aws_event": aws_event, + "aws_context": aws_context, + } + + function_name = aws_context.function_name + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_AWS, + "sentry.origin": AwsLambdaIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.region": aws_region, + "cloud.resource_id": aws_context.invoked_function_arn, + "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, + "cloud.provider": CLOUD_PROVIDER.AWS, + "faas.name": function_name, + "faas.invocation_id": aws_context.aws_request_id, + "faas.version": aws_context.function_version, + "aws.lambda.invoked_arn": aws_context.invoked_function_arn, + "aws.log.group.names": [aws_context.log_group_name], + "aws.log.stream.names": [aws_context.log_stream_name], + "messaging.batch.message_count": batch_size, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_AWS, + name=function_name, + source=TransactionSource.COMPONENT, + origin=AwsLambdaIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return handler(aws_event, aws_context, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + + return sentry_handler # type: ignore + + +def _drain_queue() -> None: + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + if integration is not None: + # Flush out the event queue before AWS kills the + # process. + client.flush() + + +class AwsLambdaIntegration(Integration): + identifier = "aws_lambda" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + lambda_bootstrap = get_lambda_bootstrap() + if not lambda_bootstrap: + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find bootstrap module)" + ) + return + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find handle_event_request)" + ) + return + + pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 + + if pre_37: + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_event_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + old_handle_http_request = lambda_bootstrap.handle_http_request + + def sentry_handle_http_request( + request_handler: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request_handler = _wrap_handler(request_handler) + return old_handle_http_request(request_handler, *args, **kwargs) + + lambda_bootstrap.handle_http_request = sentry_handle_http_request + + # Patch to_json to drain the queue. This should work even when the + # SDK is initialized inside of the handler + + old_to_json = lambda_bootstrap.to_json + + def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return old_to_json(*args, **kwargs) + + lambda_bootstrap.to_json = sentry_to_json + else: + lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( + lambda_bootstrap.LambdaRuntimeClient.post_init_error + ) + + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f: "F") -> "F": + def inner(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore + + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + ) + + +def get_lambda_bootstrap() -> "Optional[Any]": + # Python 3.7: If the bootstrap module is *already imported*, it is the + # one we actually want to use (no idea what's in __main__) + # + # Python 3.8: bootstrap is also importable, but will be the same file + # as __main__ imported under a different name: + # + # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ + # sys.modules['__main__'] is not sys.modules['bootstrap'] + # + # Python 3.9: bootstrap is in __main__.awslambdaricmain + # + # On container builds using the `aws-lambda-python-runtime-interface-client` + # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap + # + # Such a setup would then make all monkeypatches useless. + if "bootstrap" in sys.modules: + return sys.modules["bootstrap"] + elif "__main__" in sys.modules: + module = sys.modules["__main__"] + # python3.9 runtime + if hasattr(module, "awslambdaricmain") and hasattr( + module.awslambdaricmain, "bootstrap" + ): + return module.awslambdaricmain.bootstrap + elif hasattr(module, "bootstrap"): + # awslambdaric python module in container builds + return module.bootstrap + + # python3.8 runtime + return module + else: + return None + + +def _make_request_event_processor( + aws_event: "Any", aws_context: "Any", configured_timeout: "Any" +) -> "EventProcessor": + start_time = datetime.now(timezone.utc) + + def event_processor( + sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time + ) -> "Optional[Event]": + remaining_time_in_milis = aws_context.get_remaining_time_in_millis() + exec_duration = configured_timeout - remaining_time_in_milis + + extra = sentry_event.setdefault("extra", {}) + extra["lambda"] = { + "function_name": aws_context.function_name, + "function_version": aws_context.function_version, + "invoked_function_arn": aws_context.invoked_function_arn, + "aws_request_id": aws_context.aws_request_id, + "execution_duration_in_millis": exec_duration, + "remaining_time_in_millis": remaining_time_in_milis, + } + + extra["cloudwatch logs"] = { + "url": _get_cloudwatch_logs_url(aws_context, start_time), + "log_group": aws_context.log_group_name, + "log_stream": aws_context.log_stream_name, + } + + request = sentry_event.get("request", {}) + + if "httpMethod" in aws_event: + request["method"] = aws_event["httpMethod"] + + request["url"] = _get_url(aws_event, aws_context) + + if "queryStringParameters" in aws_event: + request["query_string"] = aws_event["queryStringParameters"] + + if "headers" in aws_event: + request["headers"] = _filter_headers(aws_event["headers"]) + + if should_send_default_pii(): + user_info = sentry_event.setdefault("user", {}) + + identity = aws_event.get("identity") + if identity is None: + identity = {} + + id = identity.get("userArn") + if id is not None: + user_info.setdefault("id", id) + + ip = identity.get("sourceIp") + if ip is not None: + user_info.setdefault("ip_address", ip) + + if "body" in aws_event: + request["data"] = aws_event.get("body", "") + else: + if aws_event.get("body", None): + # Unfortunately couldn't find a way to get structured body from AWS + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + sentry_event["request"] = deepcopy(request) + + return sentry_event + + return event_processor + + +def _get_url(aws_event: "Any", aws_context: "Any") -> str: + path = aws_event.get("path", None) + + headers = aws_event.get("headers") + if headers is None: + headers = {} + + host = headers.get("Host", None) + proto = headers.get("X-Forwarded-Proto", None) + if proto and host and path: + return "{}://{}{}".format(proto, host, path) + return "awslambda:///{}".format(aws_context.function_name) + + +def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: + """ + Generates a CloudWatchLogs console URL based on the context object + + Arguments: + aws_context {Any} -- context from lambda handler + + Returns: + str -- AWS Console URL to logs. + """ + formatstring = "%Y-%m-%dT%H:%M:%SZ" + region = environ.get("AWS_REGION", "") + + url = ( + "https://console.{domain}/cloudwatch/home?region={region}" + "#logEventViewer:group={log_group};stream={log_stream}" + ";start={start_time};end={end_time}" + ).format( + domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", + region=region, + log_group=aws_context.log_group_name, + log_stream=aws_context.log_stream_name, + start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), + end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( + formatstring + ), + ) + + return url + + +def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": + frames = [] + for frame in formatted_tb: + match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) + if match: + file_name, line_number, func_name = match.groups() + line_number = int(line_number) + frames.append( + { + "filename": file_name, + "function": func_name, + "lineno": line_number, + "vars": None, + "pre_context": None, + "context_line": None, + "post_context": None, + } + ) + return frames + + +def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": + """ + Converts the error JSON from AWS Lambda into a Sentry error event. + This is not a full fletched event, but better than nothing. + + This is an example of where AWS creates the error JSON: + https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 + """ + event: "Event" = { + "level": "error", + "exception": { + "values": [ + { + "type": error_json.get("errorType"), + "value": error_json.get("errorMessage"), + "stacktrace": { + "frames": _parse_formatted_traceback( + error_json.get("stackTrace", []) + ), + }, + "mechanism": { + "type": "aws_lambda", + "handled": False, + }, + } + ], + }, + } + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py new file mode 100644 index 0000000000..31f45f73de --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py @@ -0,0 +1,164 @@ +import sys +import types +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, TypeVar + + from sentry_sdk._types import ExcInfo + + T = TypeVar("T") + F = TypeVar("F", bound=Callable[..., Any]) + + +WRAPPED_FUNC = "_wrapped_{}_" +INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py +USED_FUNC = "_sentry_used_" + + +class BeamIntegration(Integration): + identifier = "beam" + + @staticmethod + def setup_once() -> None: + from apache_beam.transforms.core import DoFn, ParDo # type: ignore + + ignore_logger("root") + ignore_logger("bundle_processor.create") + + function_patches = ["process", "start_bundle", "finish_bundle", "setup"] + for func_name in function_patches: + setattr( + DoFn, + INSPECT_FUNC.format(func_name), + _wrap_inspect_call(DoFn, func_name), + ) + + old_init = ParDo.__init__ + + def sentry_init_pardo( + self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Do not monkey patch init twice + if not getattr(self, "_sentry_is_patched", False): + for func_name in function_patches: + if not hasattr(fn, func_name): + continue + wrapped_func = WRAPPED_FUNC.format(func_name) + + # Check to see if inspect is set and process is not + # to avoid monkey patching process twice. + # Check to see if function is part of object for + # backwards compatibility. + process_func = getattr(fn, func_name) + inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) + if not getattr(inspect_func, USED_FUNC, False) and not getattr( + process_func, USED_FUNC, False + ): + setattr(fn, wrapped_func, process_func) + setattr(fn, func_name, _wrap_task_call(process_func)) + + self._sentry_is_patched = True + old_init(self, fn, *args, **kwargs) + + ParDo.__init__ = sentry_init_pardo + + +def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": + if not hasattr(cls, func_name): + return None + + def _inspect(self: "Any") -> "Any": + """ + Inspect function overrides the way Beam gets argspec. + """ + wrapped_func = WRAPPED_FUNC.format(func_name) + if hasattr(self, wrapped_func): + process_func = getattr(self, wrapped_func) + else: + process_func = getattr(self, func_name) + setattr(self, func_name, _wrap_task_call(process_func)) + setattr(self, wrapped_func, process_func) + + # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults + # (which uses Signatures internally) should be used instead. + try: + from apache_beam.transforms.core import get_function_args_defaults + + return get_function_args_defaults(process_func) + except ImportError: + from apache_beam.typehints.decorators import getfullargspec # type: ignore + + return getfullargspec(process_func) + + setattr(_inspect, USED_FUNC, True) + return _inspect + + +def _wrap_task_call(func: "F") -> "F": + """ + Wrap task call with a try catch to get exceptions. + """ + + @wraps(func) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + try: + gen = func(*args, **kwargs) + except Exception: + raise_exception() + + if not isinstance(gen, types.GeneratorType): + return gen + return _wrap_generator_call(gen) + + setattr(_inner, USED_FUNC, True) + return _inner # type: ignore + + +@ensure_integration_enabled(BeamIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + """ + Send Beam exception to Sentry. + """ + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "beam", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def raise_exception() -> None: + """ + Raise an exception. + """ + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) + reraise(*exc_info) + + +def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": + """ + Wrap the generator to handle any failures. + """ + while True: + try: + yield next(gen) + except StopIteration: + break + except Exception: + raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py new file mode 100644 index 0000000000..a7fdd99b21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py @@ -0,0 +1,187 @@ +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + parse_url, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Type, Union + + from botocore.model import ServiceId + +try: + from botocore import __version__ as BOTOCORE_VERSION + from botocore.awsrequest import AWSRequest + from botocore.client import BaseClient + from botocore.response import StreamingBody +except ImportError: + raise DidNotEnable("botocore is not installed") + + +class Boto3Integration(Integration): + identifier = "boto3" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTOCORE_VERSION) + _check_minimum_version(Boto3Integration, version, "botocore") + + orig_init = BaseClient.__init__ + + def sentry_patched_init( + self: "BaseClient", *args: "Any", **kwargs: "Any" + ) -> None: + orig_init(self, *args, **kwargs) + meta = self.meta + service_id = meta.service_model.service_id + meta.events.register( + "request-created", + partial(_sentry_request_created, service_id=service_id), + ) + meta.events.register("after-call", _sentry_after_call) + meta.events.register("after-call-error", _sentry_after_call_error) + + BaseClient.__init__ = sentry_patched_init # type: ignore + + +def _sentry_request_created( + service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" +) -> None: + description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) + + client = sentry_sdk.get_client() + if client.get_integration(Boto3Integration) is None: + return + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Boto3Integration.origin, + SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", + }, + ) + if request.url is not None and should_send_default_pii(): + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if request.method is not None: + span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name=description, + origin=Boto3Integration.origin, + ) + + if request.url is not None: + with capture_internal_exceptions(): + parsed_url = parse_url(request.url, sanitize=False) + span.set_data("aws.request.url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + span.set_tag("aws.service_id", service_id.hyphenize()) + span.set_tag("aws.operation_name", operation_name) + if request.method is not None: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + + # We do it in order for subsequent http calls/retries be + # attached to this span. + span.__enter__() + + # request.context is an open-ended data-structure + # where we can add anything useful in request life cycle. + request.context["_sentrysdk_span"] = span + + +def _sentry_after_call( + context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(None, None, None) + + body = parsed.get("Body") + if not isinstance(body, StreamingBody): + return + + streaming_span: "Union[Span, StreamedSpan]" + if isinstance(span, StreamedSpan): + streaming_span = sentry_sdk.traces.start_span( + name=span.name, + parent_span=span, + attributes={ + "sentry.op": OP.HTTP_CLIENT_STREAM, + "sentry.origin": Boto3Integration.origin, + }, + ) + else: + streaming_span = span.start_child( + op=OP.HTTP_CLIENT_STREAM, + name=span.description, + origin=Boto3Integration.origin, + ) + + orig_read = body.read + orig_close = body.close + + def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: + try: + ret = orig_read(*args, **kwargs) + if ret: + return ret + + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + return ret + except Exception: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + raise + + body.read = sentry_streaming_body_read # type: ignore + + def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: + if isinstance(streaming_span, StreamedSpan): + streaming_span.end() + else: + streaming_span.finish() + orig_close(*args, **kwargs) + + body.close = sentry_streaming_body_close # type: ignore + + +def _sentry_after_call_error( + context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" +) -> None: + span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + + # Span could be absent if the integration is disabled. + if span is None: + return + span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py new file mode 100644 index 0000000000..50f6ca2e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py @@ -0,0 +1,239 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, + _check_minimum_version, +) +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from collections.abc import Set + from typing import Any, Callable, Dict, Optional + + from bottle import FileUpload, FormsDict, LocalRequest # type: ignore + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + +try: + from bottle import ( + Bottle, + HTTPResponse, + Route, + ) + from bottle import ( + __version__ as BOTTLE_VERSION, + ) + from bottle import ( + request as bottle_request, + ) +except ImportError: + raise DidNotEnable("Bottle not installed") + + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class BottleIntegration(Integration): + identifier = "bottle" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + *, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + version = parse_version(BOTTLE_VERSION) + _check_minimum_version(BottleIntegration, version) + + old_app = Bottle.__call__ + + @ensure_integration_enabled(BottleIntegration, old_app) + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=BottleIntegration.origin, + ) + + return middleware(environ, start_response) + + Bottle.__call__ = sentry_patched_wsgi_app + + old_handle = Bottle._handle + + @functools.wraps(old_handle) + def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return old_handle(self, environ) + + scope = sentry_sdk.get_isolation_scope() + scope._name = "bottle" + scope.add_event_processor( + _make_request_event_processor(self, bottle_request, integration) + ) + res = old_handle(self, environ) + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + _set_segment_name_and_source( + transaction_style=integration.transaction_style + ) + + return res + + Bottle._handle = _patched_handle + + old_make_callback = Route._make_callback + + @functools.wraps(old_make_callback) + def patched_make_callback( + self: "Route", *args: object, **kwargs: object + ) -> "Any": + prepared_callback = old_make_callback(self, *args, **kwargs) + + integration = sentry_sdk.get_client().get_integration(BottleIntegration) + if integration is None: + return prepared_callback + + def wrapped_callback(*args: object, **kwargs: object) -> "Any": + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) + + return res + + return wrapped_callback + + Route._make_callback = patched_make_callback + + +class BottleRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[str, str]": + return self.request.cookies + + def raw_data(self) -> bytes: + return self.request.body.read() + + def form(self) -> "FormsDict": + if self.is_json(): + return None + return self.request.forms.decode() + + def files(self) -> "Optional[Dict[str, str]]": + if self.is_json(): + return None + + return self.request.files + + def size_of_file(self, file: "FileUpload") -> int: + return file.content_length + + +def _set_segment_name_and_source(transaction_style: str) -> None: + try: + if transaction_style == "url": + name = bottle_request.route.rule or "bottle request" + else: + name = ( + bottle_request.route.name + or transaction_from_function(bottle_request.route.callback) + or "bottle request" + ) + + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], + ) + except RuntimeError: + pass + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "url": + try: + name = request.route.rule or "" + except RuntimeError: + pass + + elif transaction_style == "endpoint": + try: + name = ( + request.route.name + or transaction_from_function(request.route.callback) + or "" + ) + except RuntimeError: + pass + + event["transaction"] = name + event["transaction_info"] = { + "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + } + + +def _make_request_event_processor( + app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, request) + + with capture_internal_exceptions(): + BottleRequestExtractor(request).extract_into_event(event) + + return event + + return event_processor + + +def _capture_exception(exception: BaseException, handled: bool) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "bottle", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py new file mode 100644 index 0000000000..532b13539b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py @@ -0,0 +1,612 @@ +import sys +from collections.abc import Mapping +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import isolation_scope +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.celery.beat import ( + _patch_beat_apply_entry, + _patch_redbeat_apply_async, + _setup_celery_beat_signals, +) +from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource +from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, List, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import VERSION as CELERY_VERSION # type: ignore + from celery.app.task import Task # type: ignore + from celery.app.trace import task_has_custom + from celery.exceptions import ( # type: ignore + Ignore, + Reject, + Retry, + SoftTimeLimitExceeded, + ) + from kombu import Producer # type: ignore +except ImportError: + raise DidNotEnable("Celery not installed") + + +CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) + + +class CeleryIntegration(Integration): + identifier = "celery" + origin = f"auto.queue.{identifier}" + + def __init__( + self, + propagate_traces: bool = True, + monitor_beat_tasks: bool = False, + exclude_beat_tasks: "Optional[List[str]]" = None, + ) -> None: + self.propagate_traces = propagate_traces + self.monitor_beat_tasks = monitor_beat_tasks + self.exclude_beat_tasks = exclude_beat_tasks + + _patch_beat_apply_entry() + _patch_redbeat_apply_async() + _setup_celery_beat_signals(monitor_beat_tasks) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(CeleryIntegration, CELERY_VERSION) + + _patch_build_tracer() + _patch_task_apply_async() + _patch_celery_send_task() + _patch_worker_exit() + _patch_producer_publish() + + # This logger logs every status of every task that ran on the worker. + # Meaning that every task's breadcrumbs are full of stuff like "Task + # raised unexpected ". + ignore_logger("celery.worker.job") + ignore_logger("celery.app.trace") + + # This is stdout/err redirected to a logger, can't deal with this + # (need event_level=logging.WARN to reproduce) + ignore_logger("celery.redirected") + + +def _set_status(status: str) -> None: + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + + if span_streaming and scope.streamed_span is not None: + scope.streamed_span.status = "ok" if status == "ok" else "error" + elif not span_streaming and scope.span is not None: + scope.span.set_status(status) + + +def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return + + if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): + # ??? Doesn't map to anything + _set_status("aborted") + return + + _set_status("internal_error") + + if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): + return + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "celery", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + task: "Any", + uuid: "Any", + args: "Any", + kwargs: "Any", + request: "Optional[Any]" = None, +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["celery_task_id"] = uuid + extra = event.setdefault("extra", {}) + extra["celery-job"] = { + "task_name": task.name, + "args": ( + args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE + ), + } + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): + event["fingerprint"] = [ + "celery", + "SoftTimeLimitExceeded", + getattr(task, "name", task), + ] + + return event + + return event_processor + + +def _update_celery_task_headers( + original_headers: "dict[str, Any]", + span: "Optional[Union[StreamedSpan, Span]]", + monitor_beat_tasks: bool, +) -> "dict[str, Any]": + """ + Updates the headers of the Celery task with the tracing information + and eventually Sentry Crons monitoring information for beat tasks. + """ + updated_headers = original_headers.copy() + with capture_internal_exceptions(): + # if span is None (when the task was started by Celery Beat) + # this will return the trace headers from the scope. + headers = dict( + sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) + ) + + if monitor_beat_tasks: + headers.update( + { + "sentry-monitor-start-timestamp-s": "%.9f" + % _now_seconds_since_epoch(), + } + ) + + # Add the time the task was enqueued to the headers + # This is used in the consumer to calculate the latency + updated_headers.update( + {"sentry-task-enqueued-time": _now_seconds_since_epoch()} + ) + + if headers: + existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) + sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) + + combined_baggage = sentry_baggage or existing_baggage + if sentry_baggage and existing_baggage: + # Merge incoming and sentry baggage, where the sentry trace information + # in the incoming baggage takes precedence and the third-party items + # are concatenated. + incoming = Baggage.from_incoming_header(existing_baggage) + combined = Baggage.from_incoming_header(sentry_baggage) + combined.sentry_items.update(incoming.sentry_items) + combined.third_party_items = ",".join( + [ + x + for x in [ + combined.third_party_items, + incoming.third_party_items, + ] + if x is not None and x != "" + ] + ) + combined_baggage = combined.serialize(include_third_party=True) + + updated_headers.update(headers) + if combined_baggage: + updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage + + # https://github.com/celery/celery/issues/4875 + # + # Need to setdefault the inner headers too since other + # tracing tools (dd-trace-py) also employ this exact + # workaround and we don't want to break them. + updated_headers.setdefault("headers", {}).update(headers) + if combined_baggage: + updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage + + # Add the Sentry options potentially added in `sentry_apply_entry` + # to the headers (done when auto-instrumenting Celery Beat tasks) + for key, value in updated_headers.items(): + if key.startswith("sentry-"): + updated_headers["headers"][key] = value + + # Preserve user-provided custom headers in the inner "headers" dict + # so they survive to task.request.headers on the worker (celery#4875). + for key, value in original_headers.items(): + if key != "headers" and key not in updated_headers["headers"]: + updated_headers["headers"][key] = value + + return updated_headers + + +class NoOpMgr: + def __enter__(self) -> None: + return None + + def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: + return None + + +def _wrap_task_run(f: "F") -> "F": + @wraps(f) + def apply_async(*args: "Any", **kwargs: "Any") -> "Any": + # Note: kwargs can contain headers=None, so no setdefault! + # Unsure which backend though. + client = sentry_sdk.get_client() + integration = client.get_integration(CeleryIntegration) + if integration is None: + return f(*args, **kwargs) + + kwarg_headers = kwargs.get("headers") or {} + propagate_traces = kwarg_headers.pop( + "sentry-propagate-traces", integration.propagate_traces + ) + + if not propagate_traces: + return f(*args, **kwargs) + + if isinstance(args[0], Task): + task_name: str = args[0].name + elif len(args) > 1 and isinstance(args[1], str): + task_name = args[1] + else: + task_name = "" + + span_streaming = has_span_streaming_enabled(client.options) + + task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" + + span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + if span_streaming: + if not task_started_from_beat and get_current_span() is not None: + span_mgr = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_CELERY, + "sentry.origin": CeleryIntegration.origin, + }, + ) + + else: + if not task_started_from_beat: + span_mgr = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_CELERY, + name=task_name, + origin=CeleryIntegration.origin, + ) + + with span_mgr as span: + kwargs["headers"] = _update_celery_task_headers( + kwarg_headers, span, integration.monitor_beat_tasks + ) + return f(*args, **kwargs) + + return apply_async # type: ignore + + +def _wrap_tracer(task: "Any", f: "F") -> "F": + # Need to wrap tracer for pushing the scope before prerun is sent, and + # popping it after postrun is sent. + # + # This is the reason we don't use signals for hooking in the first place. + # Also because in Celery 3, signal dispatch returns early if one handler + # crashes. + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + with isolation_scope() as scope: + scope._name = "celery" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) + + task_name = getattr(task, "name", "") + + custom_sampling_context = {} + with capture_internal_exceptions(): + custom_sampling_context = { + "celery_job": { + "task": task_name, + # for some reason, args[1] is a list if non-empty but a + # tuple if empty + "args": list(args[1]), + "kwargs": args[2], + } + } + + span: "Union[Span, StreamedSpan]" + span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() + + # Celery task objects are not a thing to be trusted. Even + # something such as attribute access can fail. + with capture_internal_exceptions(): + headers = args[3].get("headers") or {} + if span_streaming: + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(custom_sampling_context) + span = sentry_sdk.traces.start_span( + name=task_name, + parent_span=None, # make this a segment + attributes={ + "sentry.origin": CeleryIntegration.origin, + "sentry.span.source": TransactionSource.TASK.value, + "sentry.op": OP.QUEUE_TASK_CELERY, + }, + ) + + span_ctx = span + + else: + span = continue_trace( + headers, + op=OP.QUEUE_TASK_CELERY, + name=task_name, + source=TransactionSource.TASK, + origin=CeleryIntegration.origin, + ) + span.set_status(SPANSTATUS.OK) + + span_ctx = sentry_sdk.start_transaction( + span, + custom_sampling_context=custom_sampling_context, + ) + + with span_ctx: + return f(*args, **kwargs) + + return _inner # type: ignore + + +def _set_messaging_destination_name( + task: "Any", span: "Union[StreamedSpan, Span]" +) -> None: + """Set "messaging.destination.name" tag for span""" + with capture_internal_exceptions(): + delivery_info = task.request.delivery_info + if delivery_info: + routing_key = delivery_info.get("routing_key") + if delivery_info.get("exchange") == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning the tasks + # are sent to the queue with the same name as the routing key. + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + else: + span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + +def _wrap_task_call(task: "Any", f: "F") -> "F": + # Need to wrap task call because the exception is caught before we get to + # see it. Also celery's reported stacktrace is untrustworthy. + + @wraps(f) + def _inner(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return f(*args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + try: + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_PROCESS, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PROCESS, + name=task.name, + origin=CeleryIntegration.origin, + ) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + _set_messaging_destination_name(task, span) + + latency = None + with capture_internal_exceptions(): + if ( + task.request.headers is not None + and "sentry-task-enqueued-time" in task.request.headers + ): + latency = _now_seconds_since_epoch() - task.request.headers.pop( + "sentry-task-enqueued-time" + ) + + if latency is not None: + latency *= 1000 # milliseconds + set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) + + with capture_internal_exceptions(): + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries + ) + + with capture_internal_exceptions(): + with task.app.connection() as conn: + set_on_span( + SPANDATA.MESSAGING_SYSTEM, + conn.transport.driver_type, + ) + + return f(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(task, exc_info) + reraise(*exc_info) + + return _inner # type: ignore + + +def _patch_build_tracer() -> None: + import celery.app.trace as trace # type: ignore + + original_build_tracer = trace.build_tracer + + def sentry_build_tracer( + name: "Any", task: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not getattr(task, "_sentry_is_patched", False): + # determine whether Celery will use __call__ or run and patch + # accordingly + if task_has_custom(task, "__call__"): + type(task).__call__ = _wrap_task_call(task, type(task).__call__) + else: + task.run = _wrap_task_call(task, task.run) + + # `build_tracer` is apparently called for every task + # invocation. Can't wrap every celery task for every invocation + # or we will get infinitely nested wrapper functions. + task._sentry_is_patched = True + + return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) + + trace.build_tracer = sentry_build_tracer + + +def _patch_task_apply_async() -> None: + Task.apply_async = _wrap_task_run(Task.apply_async) + + +def _patch_celery_send_task() -> None: + from celery import Celery + + Celery.send_task = _wrap_task_run(Celery.send_task) + + +def _patch_worker_exit() -> None: + # Need to flush queue before worker shutdown because a crashing worker will + # call os._exit + from billiard.pool import Worker # type: ignore + + original_workloop = Worker.workloop + + def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": + try: + return original_workloop(*args, **kwargs) + finally: + with capture_internal_exceptions(): + if ( + sentry_sdk.get_client().get_integration(CeleryIntegration) + is not None + ): + sentry_sdk.flush() + + Worker.workloop = sentry_workloop + + +def _patch_producer_publish() -> None: + original_publish = Producer.publish + + def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(CeleryIntegration) is None: + return original_publish(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + kwargs_headers = kwargs.get("headers", {}) + if not isinstance(kwargs_headers, Mapping): + # Ensure kwargs_headers is a Mapping, so we can safely call get(). + # We don't expect this to happen, but it's better to be safe. Even + # if it does happen, only our instrumentation breaks. This line + # does not overwrite kwargs["headers"], so the original publish + # method will still work. + kwargs_headers = {} + + task_name = kwargs_headers.get("task") or "" + task_id = kwargs_headers.get("id") + retries = kwargs_headers.get("retries") + + routing_key = kwargs.get("routing_key") + exchange = kwargs.get("exchange") + + span: "Union[StreamedSpan, Span, None]" = None + if span_streaming: + if get_current_span() is not None: + span = sentry_sdk.traces.start_span( + name=task_name, + attributes={ + "sentry.op": OP.QUEUE_PUBLISH, + "sentry.origin": CeleryIntegration.origin, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.QUEUE_PUBLISH, + name=task_name, + origin=CeleryIntegration.origin, + ) + + if span is None: + return original_publish(self, *args, **kwargs) + + with span: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + if task_id is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) + + if exchange == "" and routing_key is not None: + # Empty exchange indicates the default exchange, meaning messages are + # routed to the queue with the same name as the routing key. + set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) + + if retries is not None: + set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) + + with capture_internal_exceptions(): + set_on_span( + SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type + ) + + return original_publish(self, *args, **kwargs) + + Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py new file mode 100644 index 0000000000..b5027d212a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py @@ -0,0 +1,291 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.crons import MonitorStatus, capture_checkin +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.celery.utils import ( + _get_humanized_interval, + _now_seconds_since_epoch, +) +from sentry_sdk.utils import ( + logger, + match_regex_list, +) + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, TypeVar, Union + + from sentry_sdk._types import ( + MonitorConfig, + MonitorConfigScheduleType, + MonitorConfigScheduleUnit, + ) + + F = TypeVar("F", bound=Callable[..., Any]) + + +try: + from celery import Celery, Task # type: ignore + from celery.beat import Scheduler # type: ignore + from celery.schedules import crontab, schedule # type: ignore + from celery.signals import ( # type: ignore + task_failure, + task_retry, + task_success, + ) +except ImportError: + raise DidNotEnable("Celery not installed") + +try: + from redbeat.schedulers import RedBeatScheduler # type: ignore +except ImportError: + RedBeatScheduler = None + + +def _get_headers(task: "Task") -> "dict[str, Any]": + headers = task.request.get("headers") or {} + + # flatten nested headers + if "headers" in headers: + headers.update(headers["headers"]) + del headers["headers"] + + headers.update(task.request.get("properties") or {}) + + return headers + + +def _get_monitor_config( + celery_schedule: "Any", app: "Celery", monitor_name: str +) -> "MonitorConfig": + monitor_config: "MonitorConfig" = {} + schedule_type: "Optional[MonitorConfigScheduleType]" = None + schedule_value: "Optional[Union[str, int]]" = None + schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None + + if isinstance(celery_schedule, crontab): + schedule_type = "crontab" + schedule_value = ( + "{0._orig_minute} " + "{0._orig_hour} " + "{0._orig_day_of_month} " + "{0._orig_month_of_year} " + "{0._orig_day_of_week}".format(celery_schedule) + ) + elif isinstance(celery_schedule, schedule): + schedule_type = "interval" + (schedule_value, schedule_unit) = _get_humanized_interval( + celery_schedule.seconds + ) + + if schedule_unit == "second": + logger.warning( + "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", + monitor_name, + schedule_value, + ) + return {} + + else: + logger.warning( + "Celery schedule type '%s' not supported by Sentry Crons.", + type(celery_schedule), + ) + return {} + + monitor_config["schedule"] = {} + monitor_config["schedule"]["type"] = schedule_type + monitor_config["schedule"]["value"] = schedule_value + + if schedule_unit is not None: + monitor_config["schedule"]["unit"] = schedule_unit + + monitor_config["timezone"] = ( + ( + hasattr(celery_schedule, "tz") + and celery_schedule.tz is not None + and str(celery_schedule.tz) + ) + or app.timezone + or "UTC" + ) + + return monitor_config + + +def _apply_crons_data_to_schedule_entry( + scheduler: "Any", + schedule_entry: "Any", + integration: "sentry_sdk.integrations.celery.CeleryIntegration", +) -> None: + """ + Add Sentry Crons information to the schedule_entry headers. + """ + if not integration.monitor_beat_tasks: + return + + monitor_name = schedule_entry.name + + task_should_be_excluded = match_regex_list( + monitor_name, integration.exclude_beat_tasks + ) + if task_should_be_excluded: + return + + celery_schedule = schedule_entry.schedule + app = scheduler.app + + monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) + + is_supported_schedule = bool(monitor_config) + if not is_supported_schedule: + return + + headers = schedule_entry.options.pop("headers", {}) + headers.update( + { + "sentry-monitor-slug": monitor_name, + "sentry-monitor-config": monitor_config, + } + ) + + check_in_id = capture_checkin( + monitor_slug=monitor_name, + monitor_config=monitor_config, + status=MonitorStatus.IN_PROGRESS, + ) + headers.update({"sentry-monitor-check-in-id": check_in_id}) + + # Set the Sentry configuration in the options of the ScheduleEntry. + # Those will be picked up in `apply_async` and added to the headers. + schedule_entry.options["headers"] = headers + + +def _wrap_beat_scheduler( + original_function: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Makes sure that: + - a new Sentry trace is started for each task started by Celery Beat and + it is propagated to the task. + - the Sentry Crons information is set in the Celery Beat task's + headers so that is monitored with Sentry Crons. + + After the patched function is called, + Celery Beat will call apply_async to put the task in the queue. + """ + # Patch only once + # Can't use __name__ here, because some of our tests mock original_apply_entry + already_patched = "sentry_patched_scheduler" in str(original_function) + if already_patched: + return original_function + + from sentry_sdk.integrations.celery import CeleryIntegration + + def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(CeleryIntegration) + if integration is None: + return original_function(*args, **kwargs) + + # Tasks started by Celery Beat start a new Trace + scope = sentry_sdk.get_isolation_scope() + scope.set_new_propagation_context() + scope._name = "celery-beat" + + scheduler, schedule_entry = args + _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) + + return original_function(*args, **kwargs) + + return sentry_patched_scheduler + + +def _patch_beat_apply_entry() -> None: + Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) + + +def _patch_redbeat_apply_async() -> None: + if RedBeatScheduler is None: + return + + RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) + + +def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: + if monitor_beat_tasks: + task_success.connect(crons_task_success) + task_failure.connect(crons_task_failure) + task_retry.connect(crons_task_retry) + + +def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_success %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.OK, + ) + + +def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_failure %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) + + +def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: + logger.debug("celery_task_retry %s", sender) + headers = _get_headers(sender) + + if "sentry-monitor-slug" not in headers: + return + + monitor_config = headers.get("sentry-monitor-config", {}) + + start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") + + capture_checkin( + monitor_slug=headers["sentry-monitor-slug"], + monitor_config=monitor_config, + check_in_id=headers["sentry-monitor-check-in-id"], + duration=( + _now_seconds_since_epoch() - float(start_timestamp_s) + if start_timestamp_s + else None + ), + status=MonitorStatus.ERROR, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py new file mode 100644 index 0000000000..8d181f1f24 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py @@ -0,0 +1,32 @@ +import time +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from typing import Tuple + + from sentry_sdk._types import MonitorConfigScheduleUnit + + +def _now_seconds_since_epoch() -> float: + # We cannot use `time.perf_counter()` when dealing with the duration + # of a Celery task, because the start of a Celery task and + # the end are recorded in different processes. + # Start happens in the Celery Beat process, + # the end in a Celery Worker process. + return time.time() + + +def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": + TIME_UNITS = ( # noqa: N806 + ("day", 60 * 60 * 24.0), + ("hour", 60 * 60.0), + ("minute", 60.0), + ) + + seconds = float(seconds) + for unit, divider in TIME_UNITS: + if seconds >= divider: + interval = int(seconds / divider) + return (interval, cast("MonitorConfigScheduleUnit", unit)) + + return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py new file mode 100644 index 0000000000..9baa0e5cdd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py @@ -0,0 +1,188 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.aws_lambda import _make_request_event_processor +from sentry_sdk.traces import ( + SpanStatus, + StreamedSpan, + get_current_span, +) +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + parse_version, + reraise, +) + +try: + import chalice # type: ignore + from chalice import Chalice, ChaliceViewError + from chalice import __version__ as CHALICE_VERSION + from chalice.app import ( # type: ignore + EventSourceHandler as ChaliceEventSourceHandler, + ) +except ImportError: + raise DidNotEnable("Chalice is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) + + +class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore + def __call__(self, event: "Any", context: "Any") -> "Any": + client = sentry_sdk.get_client() + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor(event, context, configured_time) + ) + try: + return ChaliceEventSourceHandler.__call__(self, event, context) + except Exception: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + client.flush() + reraise(*exc_info) + + +def _get_view_function_response( + app: "Any", view_function: "F", function_args: "Any" +) -> "F": + @wraps(view_function) + def wrapped_view_function(**function_args: "Any") -> "Any": + client = sentry_sdk.get_client() + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + configured_time = app.lambda_context.get_remaining_time_in_millis() + scope.add_event_processor( + _make_request_event_processor( + app.current_request.to_dict(), + app.lambda_context, + configured_time, + ) + ) + + if has_span_streaming_enabled(client.options): + current_span = get_current_span() + segment = None + if type(current_span) is StreamedSpan: + # A segment already exists (created by the AWS Lambda + # integration), so decorate it with Chalice attributes + # The AWS Lambda integration owns the span lifecycle + # (end + flush), but Chalice converts unhandled view exceptions + # into 500 responses, so the error must be captured here. + request_dict = app.current_request.to_dict() + headers = request_dict.get("headers", {}) + + header_attrs: "Dict[str, Any]" = {} + for header, value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attrs[f"http.request.header.{header.lower()}"] = value + + additional_attrs: "Dict[str, Any]" = {} + if "method" in request_dict: + additional_attrs["http.request.method"] = request_dict["method"] + + attributes = { + "sentry.origin": ChaliceIntegration.origin, + **header_attrs, + **additional_attrs, + } + + segment = current_span._segment + segment.set_attributes(attributes) + + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + if segment: + segment.status = SpanStatus.ERROR.value + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + if segment is None: + client.flush() + raise + else: + scope.set_transaction_name( + app.lambda_context.function_name, + source=TransactionSource.COMPONENT, + ) + try: + return view_function(**function_args) + except Exception as exc: + if isinstance(exc, ChaliceViewError): + raise + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "chalice", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + client.flush() + raise + + return wrapped_view_function # type: ignore + + +class ChaliceIntegration(Integration): + identifier = "chalice" + origin = f"auto.function.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(CHALICE_VERSION) + + if version is None: + raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) + + if version < (1, 20): + old_get_view_function_response = Chalice._get_view_function_response + else: + from chalice.app import RestAPIEventHandler + + old_get_view_function_response = ( + RestAPIEventHandler._get_view_function_response + ) + + def sentry_event_response( + app: "Any", view_function: "F", function_args: "Dict[str, Any]" + ) -> "Any": + wrapped_view_function = _get_view_function_response( + app, view_function, function_args + ) + + return old_get_view_function_response( + app, wrapped_view_function, function_args + ) + + if version < (1, 20): + Chalice._get_view_function_response = sentry_event_response + else: + RestAPIEventHandler._get_view_function_response = sentry_event_response + # for everything else (like events) + chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py new file mode 100644 index 0000000000..e6b3009548 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py @@ -0,0 +1,211 @@ +import functools +from typing import TYPE_CHECKING, TypeVar + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import Any, Callable, ParamSpec, Union +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + + +try: + from clickhouse_driver import VERSION # type: ignore[import-not-found] + from clickhouse_driver.client import Client # type: ignore[import-not-found] + from clickhouse_driver.connection import ( # type: ignore[import-not-found] + Connection, + ) + +except ImportError: + raise DidNotEnable("clickhouse-driver not installed.") + + +class ClickhouseDriverIntegration(Integration): + identifier = "clickhouse_driver" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(ClickhouseDriverIntegration, VERSION) + + # Every query is done using the Connection's `send_query` function + Connection.send_query = _wrap_start(Connection.send_query) + + # If the query contains parameters then the send_data function is used to send those parameters to clickhouse + _wrap_send_data() + + # Every query ends either with the Client's `receive_end_of_query` (no result expected) + # or its `receive_result` (result expected) + Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) + if hasattr(Client, "receive_end_of_insert_query"): + # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` + Client.receive_end_of_insert_query = _wrap_end( + Client.receive_end_of_insert_query + ) + Client.receive_result = _wrap_end(Client.receive_result) + + +P = ParamSpec("P") +T = TypeVar("T") + + +def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": + @functools.wraps(f) + def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": + client = sentry_sdk.get_client() + if client.get_integration(ClickhouseDriverIntegration) is None: + return f(*args, **kwargs) + + connection = args[0] + query = args[1] + query_id = args[2] if len(args) > 2 else kwargs.get("query_id") + params = args[3] if len(args) > 3 else kwargs.get("params") + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=query, # type: ignore + attributes={ + "sentry.op": OP.DB, + "sentry.origin": ClickhouseDriverIntegration.origin, + SPANDATA.DB_QUERY_TEXT: str(query), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=ClickhouseDriverIntegration.origin, + ) + + span.set_data("query", query) + + if query_id: + span.set_data("db.query_id", query_id) + + if params and should_send_default_pii(): + span.set_data("db.params", params) + + connection._sentry_span = span # type: ignore[attr-defined] + + _set_db_data(span, connection) + + # run the original code + ret = f(*args, **kwargs) + + return ret + + return _inner + + +def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": + def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": + res = f(*args, **kwargs) + instance = args[0] + span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] + + if span is None: + return res + + if isinstance(span, StreamedSpan): + span.end() + else: + if res is not None and should_send_default_pii(): + span.set_data("db.result", res) + + with capture_internal_exceptions(): + span.scope.add_breadcrumb( + message=span._data.pop("query"), category="query", data=span._data + ) + + span.finish() + + return res + + return _inner_end + + +def _wrap_send_data() -> None: + original_send_data = Client.send_data + + def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data + self, sample_block, data, types_check=False, columnar=False, *args, **kwargs + ): + span = getattr(self.connection, "_sentry_span", None) + + if isinstance(span, StreamedSpan): + _set_db_data(span, self.connection) + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + if span is not None: + _set_db_data(span, self.connection) + + if should_send_default_pii(): + db_params = span._data.get("db.params", []) + + if isinstance(data, (list, tuple)): + db_params.extend(data) + + else: # data is a generic iterator + orig_data = data + + # Wrap the generator to add items to db.params as they are yielded. + # This allows us to send the params to Sentry without needing to allocate + # memory for the entire generator at once. + def wrapped_generator() -> "Iterator[Any]": + for item in orig_data: + db_params.append(item) + yield item + + # Replace the original iterator with the wrapped one. + data = wrapped_generator() + + span.set_data("db.params", db_params) + + return original_send_data( + self, sample_block, data, types_check, columnar, *args, **kwargs + ) + + Client.send_data = _inner_send_data + + +def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") + span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) + + set_on_span = span.set_attribute + else: + span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") + span.set_data(SPANDATA.DB_NAME, connection.database) + + set_on_span = span.set_data + + set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") + set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) + set_on_span(SPANDATA.SERVER_PORT, connection.port) + set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py new file mode 100644 index 0000000000..f6285d0a9b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py @@ -0,0 +1,273 @@ +import json +from typing import TYPE_CHECKING + +import urllib3 + +from sentry_sdk.api import set_context +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Dict + + +CONTEXT_TYPE = "cloud_resource" + +HTTP_TIMEOUT = 2.0 + +AWS_METADATA_HOST = "169.254.169.254" +AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) +AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( + AWS_METADATA_HOST +) + +GCP_METADATA_HOST = "metadata.google.internal" +GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( + GCP_METADATA_HOST +) + + +class CLOUD_PROVIDER: # noqa: N801 + """ + Name of the cloud provider. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + ALIBABA = "alibaba_cloud" + AWS = "aws" + AZURE = "azure" + GCP = "gcp" + IBM = "ibm_cloud" + TENCENT = "tencent_cloud" + + +class CLOUD_PLATFORM: # noqa: N801 + """ + The cloud platform. + see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ + """ + + AWS_EC2 = "aws_ec2" + AWS_LAMBDA = "aws_lambda" + GCP_COMPUTE_ENGINE = "gcp_compute_engine" + + +class CloudResourceContextIntegration(Integration): + """ + Adds cloud resource context to the Senty scope + """ + + identifier = "cloudresourcecontext" + + cloud_provider = "" + + aws_token = "" + http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) + + gcp_metadata = None + + def __init__(self, cloud_provider: str = "") -> None: + CloudResourceContextIntegration.cloud_provider = cloud_provider + + @classmethod + def _is_aws(cls) -> bool: + try: + r = cls.http.request( + "PUT", + AWS_TOKEN_URL, + headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, + ) + + if r.status != 200: + return False + + cls.aws_token = r.data.decode() + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking AWS metadata service: %s", str(e)) + return False + + @classmethod + def _get_aws_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.AWS, + "cloud.platform": CLOUD_PLATFORM.AWS_EC2, + } + + try: + r = cls.http.request( + "GET", + AWS_METADATA_URL, + headers={"X-aws-ec2-metadata-token": cls.aws_token}, + ) + + if r.status != 200: + return ctx + + data = json.loads(r.data.decode("utf-8")) + + try: + ctx["cloud.account.id"] = data["accountId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = data["availabilityZone"] + except Exception: + pass + + try: + ctx["cloud.region"] = data["region"] + except Exception: + pass + + try: + ctx["host.id"] = data["instanceId"] + except Exception: + pass + + try: + ctx["host.type"] = data["instanceType"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching AWS metadata: %s", str(e)) + + return ctx + + @classmethod + def _is_gcp(cls) -> bool: + try: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return False + + cls.gcp_metadata = json.loads(r.data.decode("utf-8")) + return True + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + return False + except Exception as e: + logger.debug("Error checking GCP metadata service: %s", str(e)) + return False + + @classmethod + def _get_gcp_context(cls) -> "Dict[str, str]": + ctx = { + "cloud.provider": CLOUD_PROVIDER.GCP, + "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, + } + + gcp_metadata = cls.gcp_metadata + try: + if cls.gcp_metadata is None: + r = cls.http.request( + "GET", + GCP_METADATA_URL, + headers={"Metadata-Flavor": "Google"}, + ) + + if r.status != 200: + return ctx + + gcp_metadata = json.loads(r.data.decode("utf-8")) + cls.gcp_metadata = gcp_metadata + + try: + ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] + except Exception: + pass + + try: + ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( + "/" + )[-1] + except Exception: + pass + + try: + # only populated in google cloud run + ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] + except Exception: + pass + + try: + ctx["host.id"] = gcp_metadata["instance"]["id"] + except Exception: + pass + + except urllib3.exceptions.TimeoutError: + logger.debug( + "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT + ) + except Exception as e: + logger.debug("Error fetching GCP metadata: %s", str(e)) + + return ctx + + @classmethod + def _get_cloud_provider(cls) -> str: + if cls._is_aws(): + return CLOUD_PROVIDER.AWS + + if cls._is_gcp(): + return CLOUD_PROVIDER.GCP + + return "" + + @classmethod + def _get_cloud_resource_context(cls) -> "Dict[str, str]": + cloud_provider = ( + cls.cloud_provider + if cls.cloud_provider != "" + else CloudResourceContextIntegration._get_cloud_provider() + ) + if cloud_provider in context_getters.keys(): + return context_getters[cloud_provider]() + + return {} + + @staticmethod + def setup_once() -> None: + cloud_provider = CloudResourceContextIntegration.cloud_provider + unsupported_cloud_provider = ( + cloud_provider != "" and cloud_provider not in context_getters.keys() + ) + + if unsupported_cloud_provider: + logger.warning( + "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", + CloudResourceContextIntegration.cloud_provider, + list(context_getters.keys()), + ) + + context = CloudResourceContextIntegration._get_cloud_resource_context() + if context != {}: + set_context(CONTEXT_TYPE, context) + + +# Map with the currently supported cloud providers +# mapping to functions extracting the context +context_getters = { + CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, + CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, +} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py new file mode 100644 index 0000000000..7abf3f6808 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py @@ -0,0 +1,303 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterator, Union + + from sentry_sdk.tracing import Span + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise + +try: + from cohere import ( + ChatStreamEndEvent, + NonStreamedChatResponse, + ) + from cohere.base_client import BaseCohere + from cohere.client import Client + + if TYPE_CHECKING: + from cohere import StreamedChatResponse +except ImportError: + raise DidNotEnable("Cohere not installed") + +try: + # cohere 5.9.3+ + from cohere import StreamEndStreamedChatResponse +except ImportError: + from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse + + +COLLECTED_CHAT_PARAMS = { + "model": SPANDATA.AI_MODEL_ID, + "k": SPANDATA.AI_TOP_K, + "p": SPANDATA.AI_TOP_P, + "seed": SPANDATA.AI_SEED, + "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, + "raw_prompting": SPANDATA.AI_RAW_PROMPTING, +} + +COLLECTED_PII_CHAT_PARAMS = { + "tools": SPANDATA.AI_TOOLS, + "preamble": SPANDATA.AI_PREAMBLE, +} + +COLLECTED_CHAT_RESP_ATTRS = { + "generation_id": SPANDATA.AI_GENERATION_ID, + "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, + "finish_reason": SPANDATA.AI_FINISH_REASON, +} + +COLLECTED_PII_CHAT_RESP_ATTRS = { + "citations": SPANDATA.AI_CITATIONS, + "documents": SPANDATA.AI_DOCUMENTS, + "search_queries": SPANDATA.AI_SEARCH_QUERIES, + "search_results": SPANDATA.AI_SEARCH_RESULTS, + "tool_calls": SPANDATA.AI_TOOL_CALLS, +} + + +class CohereIntegration(Integration): + identifier = "cohere" + origin = f"auto.ai.{identifier}" + + def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) + Client.embed = _wrap_embed(Client.embed) + BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "cohere", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _end_span(span: "Any") -> None: + if isinstance(span, StreamedSpan): + span.end() + else: + span.__exit__(None, None, None) + + +def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": + def collect_chat_response_fields( + span: "Union[Span, StreamedSpan]", + res: "NonStreamedChatResponse", + include_pii: bool, + ) -> None: + if include_pii: + if hasattr(res, "text"): + set_data_normalized( + span, + SPANDATA.AI_RESPONSES, + [res.text], + ) + for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: + if hasattr(res, pii_attr): + set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) + + for attr in COLLECTED_CHAT_RESP_ATTRS: + if hasattr(res, attr): + set_data_normalized(span, "ai." + attr, getattr(res, attr)) + + if hasattr(res, "meta"): + if hasattr(res.meta, "billed_units"): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + output_tokens=res.meta.billed_units.output_tokens, + ) + elif hasattr(res.meta, "tokens"): + record_token_usage( + span, + input_tokens=res.meta.tokens.input_tokens, + output_tokens=res.meta.tokens.output_tokens, + ) + + if hasattr(res.meta, "warnings"): + set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) + + @wraps(f) + def new_chat(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if ( + integration is None + or "message" not in kwargs + or not isinstance(kwargs.get("message"), str) + ): + return f(*args, **kwargs) + + message = kwargs.get("message") + + if is_span_streaming_enabled: + span = sentry_sdk.traces.start_span( + name="cohere.client.Chat", + attributes={ + "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, + name="cohere.client.Chat", + origin=CohereIntegration.origin, + ) + span.__enter__() + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + + reraise(*exc_info) + + with capture_internal_exceptions(): + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, + SPANDATA.AI_INPUT_MESSAGES, + list( + map( + lambda x: { + "role": getattr(x, "role", "").lower(), + "content": getattr(x, "message", ""), + }, + kwargs.get("chat_history", []), + ) + ) + + [{"role": "user", "content": message}], + ) + for k, v in COLLECTED_PII_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + + for k, v in COLLECTED_CHAT_PARAMS.items(): + if k in kwargs: + set_data_normalized(span, v, kwargs[k]) + set_data_normalized(span, SPANDATA.AI_STREAMING, False) + + if streaming: + old_iterator = res + + def new_iterator() -> "Iterator[StreamedChatResponse]": + with capture_internal_exceptions(): + for x in old_iterator: + if isinstance(x, ChatStreamEndEvent) or isinstance( + x, StreamEndStreamedChatResponse + ): + collect_chat_response_fields( + span, + x.response, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + yield x + _end_span(span) + + return new_iterator() + elif isinstance(res, NonStreamedChatResponse): + collect_chat_response_fields( + span, + res, + include_pii=should_send_default_pii() + and integration.include_prompts, + ) + _end_span(span) + else: + set_data_normalized(span, "unknown_response", True) + _end_span(span) + return res + + return new_chat + + +def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(CohereIntegration) + if integration is None: + return f(*args, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name="Cohere Embedding Creation", + attributes={ + "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, + "sentry.origin": CohereIntegration.origin, + }, + ) + else: + span_ctx = get_start_span_function()( + op=consts.OP.COHERE_EMBEDDINGS_CREATE, + name="Cohere Embedding Creation", + origin=CohereIntegration.origin, + ) + + with span_ctx as span: + if "texts" in kwargs and ( + should_send_default_pii() and integration.include_prompts + ): + if isinstance(kwargs["texts"], str): + set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) + elif ( + isinstance(kwargs["texts"], list) + and len(kwargs["texts"]) > 0 + and isinstance(kwargs["texts"][0], str) + ): + set_data_normalized( + span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] + ) + + if "model" in kwargs: + set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + reraise(*exc_info) + if ( + hasattr(res, "meta") + and hasattr(res.meta, "billed_units") + and hasattr(res.meta.billed_units, "input_tokens") + ): + record_token_usage( + span, + input_tokens=res.meta.billed_units.input_tokens, + total_tokens=res.meta.billed_units.input_tokens, + ) + return res + + return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py new file mode 100644 index 0000000000..a0e9014666 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py @@ -0,0 +1,62 @@ +import weakref +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import ContextVar, logger + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + + +class DedupeIntegration(Integration): + identifier = "dedupe" + + def __init__(self) -> None: + self._last_seen = ContextVar("last-seen") + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + if hint is None: + return event + + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return event + + exc_info = hint.get("exc_info", None) + if exc_info is None: + return event + + last_seen = integration._last_seen.get(None) + if last_seen is not None: + # last_seen is either a weakref or the original instance + last_seen = ( + last_seen() if isinstance(last_seen, weakref.ref) else last_seen + ) + + exc = exc_info[1] + if last_seen is exc: + logger.info("DedupeIntegration dropped duplicated error event %s", exc) + return None + + # we can only weakref non builtin types + try: + integration._last_seen.set(weakref.ref(exc)) + except TypeError: + integration._last_seen.set(exc) + + return event + + @staticmethod + def reset_last_seen() -> None: + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return + + integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py new file mode 100644 index 0000000000..361b60079d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py @@ -0,0 +1,884 @@ +import inspect +import sys +import threading +import weakref +from importlib import import_module + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANNAME +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import ( + add_query_source, + has_span_streaming_enabled, + record_sql_queries, +) +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + SENSITIVE_DATA_SUBSTITUTE, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, + transaction_from_function, + walk_exception_chain, +) + +try: + from django import VERSION as DJANGO_VERSION + from django.conf import settings + from django.conf import settings as django_settings + from django.core import signals + from django.utils.functional import SimpleLazyObject + + try: + from django.urls import resolve + except ImportError: + from django.core.urlresolvers import resolve + + try: + from django.urls import Resolver404 + except ImportError: + from django.core.urlresolvers import Resolver404 + + # Only available in Django 3.0+ + try: + from django.core.handlers.asgi import ASGIRequest + except Exception: + ASGIRequest = None + +except ImportError: + raise DidNotEnable("Django not installed") + +from sentry_sdk.integrations.django.middleware import patch_django_middlewares +from sentry_sdk.integrations.django.signals_handlers import patch_signals +from sentry_sdk.integrations.django.tasks import patch_tasks +from sentry_sdk.integrations.django.templates import ( + get_template_frame_from_exception, + patch_templates, +) +from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER +from sentry_sdk.integrations.django.views import patch_views + +if DJANGO_VERSION[:2] > (1, 8): + from sentry_sdk.integrations.django.caching import patch_caching +else: + patch_caching = None # type: ignore + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from django.core.handlers.wsgi import WSGIRequest + from django.http.request import QueryDict + from django.http.response import HttpResponse + from django.utils.datastructures import MultiValueDict + + from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +if DJANGO_VERSION < (1, 10): + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated() + +else: + + def is_authenticated(request_user: "Any") -> bool: + return request_user.is_authenticated + + +TRANSACTION_STYLE_VALUES = ("function_name", "url") + + +class DjangoIntegration(Integration): + """ + Auto instrument a Django application. + + :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. + :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. + :param signals_spans: Whether to create spans for signals. Defaults to `True`. + :param signals_denylist: A list of signals to ignore when creating spans. + :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. + """ + + identifier = "django" + origin = f"auto.http.{identifier}" + origin_db = f"auto.db.{identifier}" + + transaction_style = "" + middleware_spans: "Optional[bool]" = None + signals_spans: "Optional[bool]" = None + cache_spans: "Optional[bool]" = None + signals_denylist: "list[signals.Signal]" = [] + + def __init__( + self, + transaction_style: str = "url", + middleware_spans: bool = False, + signals_spans: bool = True, + cache_spans: bool = False, + db_transaction_spans: bool = False, + signals_denylist: "Optional[list[signals.Signal]]" = None, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + + self.signals_spans = signals_spans + self.signals_denylist = signals_denylist or [] + + self.cache_spans = cache_spans + self.db_transaction_spans = db_transaction_spans + + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + _check_minimum_version(DjangoIntegration, DJANGO_VERSION) + + install_sql_hook() + # Patch in our custom middleware. + + # logs an error for every 500 + ignore_logger("django.server") + ignore_logger("django.request") + + from django.core.handlers.wsgi import WSGIHandler + + old_app = WSGIHandler.__call__ + + @ensure_integration_enabled(DjangoIntegration, old_app) + def sentry_patched_wsgi_handler( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + bound_old_app = old_app.__get__(self, WSGIHandler) + + from django.conf import settings + + use_x_forwarded_for = settings.USE_X_FORWARDED_HOST + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + middleware = SentryWsgiMiddleware( + bound_old_app, + use_x_forwarded_for, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + WSGIHandler.__call__ = sentry_patched_wsgi_handler + + _patch_get_response() + + _patch_django_asgi_handler() + + signals.got_request_exception.connect(_got_request_exception) + + @add_global_event_processor + def process_django_templates( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_, exc_value, _) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + frame = get_template_frame_from_exception(exc_value) + if frame is not None: + frames = exception.get("stacktrace", {}).get("frames", []) + + for i in reversed(range(len(frames))): + f = frames[i] + if ( + f.get("function") in ("Parser.parse", "parse", "render") + and f.get("module") == "django.template.base" + ): + i += 1 + break + else: + i = len(frames) + + frames.insert(i, frame) + + return event + + @add_global_repr_processor + def _django_queryset_repr( + value: "Any", hint: "Dict[str, Any]" + ) -> "Union[NotImplementedType, str]": + try: + # Django 1.6 can fail to import `QuerySet` when Django settings + # have not yet been initialized. + # + # If we fail to import, return `NotImplemented`. It's at least + # unlikely that we have a query set in `value` when importing + # `QuerySet` fails. + from django.db.models.query import QuerySet + except Exception: + return NotImplemented + + if not isinstance(value, QuerySet) or value._result_cache: + return NotImplemented + + return "<%s from %s at 0x%x>" % ( + value.__class__.__name__, + value.__module__, + id(value), + ) + + _patch_channels() + patch_django_middlewares() + patch_views() + patch_templates() + patch_signals() + patch_tasks() + add_template_context_repr_sequence() + + if patch_caching is not None: + patch_caching() + + +_DRF_PATCHED = False +_DRF_PATCH_LOCK = threading.Lock() + + +def _patch_drf() -> None: + """ + Patch Django Rest Framework for more/better request data. DRF's request + type is a wrapper around Django's request type. The attribute we're + interested in is `request.data`, which is a cached property containing a + parsed request body. Reading a request body from that property is more + reliable than reading from any of Django's own properties, as those don't + hold payloads in memory and therefore can only be accessed once. + + We patch the Django request object to include a weak backreference to the + DRF request object, such that we can later use either in + `DjangoRequestExtractor`. + + This function is not called directly on SDK setup, because importing almost + any part of Django Rest Framework will try to access Django settings (where + `sentry_sdk.init()` might be called from in the first place). Instead we + run this function on every request and do the patching on the first + request. + """ + + global _DRF_PATCHED + + if _DRF_PATCHED: + # Double-checked locking + return + + with _DRF_PATCH_LOCK: + if _DRF_PATCHED: + return + + # We set this regardless of whether the code below succeeds or fails. + # There is no point in trying to patch again on the next request. + _DRF_PATCHED = True + + with capture_internal_exceptions(): + try: + from rest_framework.views import APIView # type: ignore + except ImportError: + pass + else: + old_drf_initial = APIView.initial + + def sentry_patched_drf_initial( + self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + with capture_internal_exceptions(): + request._request._sentry_drf_request_backref = weakref.ref( + request + ) + pass + return old_drf_initial(self, request, *args, **kwargs) + + APIView.initial = sentry_patched_drf_initial + + +def _patch_channels() -> None: + try: + from channels.http import AsgiHandler # type: ignore + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because channels may not be used at all in + # the current process. That is the case when running traditional WSGI + # workers in gunicorn+gevent and the websocket stuff in a separate + # process. + logger.warning( + "We detected that you are using Django channels 2.0." + + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl + + patch_channels_asgi_handler_impl(AsgiHandler) + + +def _patch_django_asgi_handler() -> None: + try: + from django.core.handlers.asgi import ASGIHandler + except ImportError: + return + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + # + # We cannot hard-raise here because Django's ASGI stuff may not be used + # at all. + logger.warning( + "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE + ) + + from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl + + patch_django_asgi_handler_impl(ASGIHandler) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" +) -> None: + try: + transaction_name = None + if transaction_style == "function_name": + fn = resolve(request.path).func + transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) + + elif transaction_style == "url": + if hasattr(request, "urlconf"): + transaction_name = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + else: + transaction_name = LEGACY_RESOLVER.resolve(request.path_info) + + if transaction_name is None: + transaction_name = request.path_info + source = TransactionSource.URL + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name( + transaction_name, + source=source, + ) + except Resolver404: + urlconf = import_module(settings.ROOT_URLCONF) + # This exception only gets thrown when transaction_style is `function_name` + # So we don't check here what style is configured + if hasattr(urlconf, "handler404"): + handler = urlconf.handler404 + if isinstance(handler, str): + scope.transaction = handler + else: + scope.transaction = transaction_from_function( + getattr(handler, "view_class", handler) + ) + except Exception: + pass + + +def _before_get_response(request: "WSGIRequest") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return + + _patch_drf() + + scope = sentry_sdk.get_current_scope() + # Rely on WSGI middleware to start a trace + _set_transaction_name_and_source(scope, integration.transaction_style, request) + + scope.add_event_processor( + _make_wsgi_request_event_processor(weakref.ref(request), integration) + ) + + +def _attempt_resolve_again( + request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str +) -> None: + """ + Some django middlewares overwrite request.urlconf + so we need to respect that contract, + so we try to resolve the url again. + """ + if not hasattr(request, "urlconf"): + return + + _set_transaction_name_and_source(scope, transaction_style, request) + + +def _after_get_response(request: "WSGIRequest") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming and should_send_default_pii(): + user = getattr(request, "user", None) + + # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. + # Exit early if the user has not been materialized yet. + is_lazy = isinstance(user, SimpleLazyObject) + if is_lazy and hasattr(request, "_cached_user"): + user = request._cached_user + elif is_lazy: + return + + if user is None or not is_authenticated(user): + return + + user_info = {} + try: + user_info["id"] = str(user.pk) + except Exception: + pass + + try: + user_info["email"] = user.email + except Exception: + pass + + try: + user_info["username"] = user.get_username() + except Exception: + pass + + sentry_sdk.set_user(user_info) + + +def _patch_get_response() -> None: + """ + patch get_response, because at that point we have the Django request object + """ + from django.core.handlers.base import BaseHandler + + old_get_response = BaseHandler.get_response + + def sentry_patched_get_response( + self: "Any", request: "WSGIRequest" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + rv = old_get_response(self, request) + _after_get_response(request) + return rv + + BaseHandler.get_response = sentry_patched_get_response + + if hasattr(BaseHandler, "get_response_async"): + from sentry_sdk.integrations.django.asgi import patch_get_response_async + + patch_get_response_async(BaseHandler, _before_get_response) + + +def _make_wsgi_request_event_processor( + weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" +) -> "EventProcessor": + def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + request = weak_request() + if request is None: + return event + + django_3 = ASGIRequest is not None + if django_3 and type(request) == ASGIRequest: + # We have a `asgi_request_event_processor` for this. + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return wsgi_request_event_processor + + +def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + if integration is None: + return + + if request is not None and integration.transaction_style == "url": + scope = sentry_sdk.get_current_scope() + _attempt_resolve_again(request, scope, integration.transaction_style) + + event, hint = event_from_exception( + sys.exc_info(), + client_options=client.options, + mechanism={"type": "django", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class DjangoRequestExtractor(RequestExtractor): + def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: + try: + drf_request = request._sentry_drf_request_backref() + if drf_request is not None: + request = drf_request + except AttributeError: + pass + self.request = request + + def env(self) -> "Dict[str, str]": + return self.request.META + + def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": + privacy_cookies = [ + django_settings.CSRF_COOKIE_NAME, + django_settings.SESSION_COOKIE_NAME, + ] + + clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} + for key, val in self.request.COOKIES.items(): + if key in privacy_cookies: + clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE + else: + clean_cookies[key] = val + + return clean_cookies + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "QueryDict": + return self.request.POST + + def files(self) -> "MultiValueDict": + return self.request.FILES + + def size_of_file(self, file: "Any") -> int: + return file.size + + def parsed_body(self) -> "Optional[Dict[str, Any]]": + try: + return self.request.data + except Exception: + return RequestExtractor.parsed_body(self) + + +def _set_user_info(request: "WSGIRequest", event: "Event") -> None: + user_info = event.setdefault("user", {}) + + user = getattr(request, "user", None) + + if user is None or not is_authenticated(user): + return + + try: + user_info.setdefault("id", str(user.pk)) + except Exception: + pass + + try: + user_info.setdefault("email", user.email) + except Exception: + pass + + try: + user_info.setdefault("username", user.get_username()) + except Exception: + pass + + +def install_sql_hook() -> None: + """If installed this causes Django's queries to be captured.""" + try: + from django.db.backends.utils import CursorWrapper + except ImportError: + from django.db.backends.util import CursorWrapper + + try: + # django 1.6 and 1.7 compatability + from django.db.backends import BaseDatabaseWrapper + except ImportError: + # django 1.8 or later + from django.db.backends.base.base import BaseDatabaseWrapper + + try: + real_execute = CursorWrapper.execute + real_executemany = CursorWrapper.executemany + real_connect = BaseDatabaseWrapper.connect + real_commit = BaseDatabaseWrapper._commit + real_rollback = BaseDatabaseWrapper._rollback + except AttributeError: + # This won't work on Django versions < 1.6 + return + + @ensure_integration_enabled(DjangoIntegration, real_execute) + def execute( + self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=params, + paramstyle="format", + executemany=False, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + result = real_execute(self, sql, params) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_executemany) + def executemany( + self: "CursorWrapper", sql: "Any", param_list: "List[Any]" + ) -> "Any": + with record_sql_queries( + cursor=self.cursor, + query=sql, + params_list=param_list, + paramstyle="format", + executemany=True, + span_origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + + result = real_executemany(self, sql, param_list) + + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if not isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + return result + + @ensure_integration_enabled(DjangoIntegration, real_connect) + def connect(self: "BaseDatabaseWrapper") -> None: + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message="connect", category="query") + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="connect", + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self) + return real_connect(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + return real_connect(self) + + def _commit(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_commit(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_COMMIT, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_COMMIT, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + + def _rollback(self: "BaseDatabaseWrapper") -> None: + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.db_transaction_spans: + return real_rollback(self) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_ROLLBACK, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_ROLLBACK, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + + CursorWrapper.execute = execute + CursorWrapper.executemany = executemany + BaseDatabaseWrapper.connect = connect + BaseDatabaseWrapper._commit = _commit + BaseDatabaseWrapper._rollback = _rollback + ignore_logger("django.db.backends") + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", + cursor_or_db: "Any", + db_operation: "Optional[str]" = None, +) -> None: + db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db + vendor = db.vendor + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) + + if db_operation is not None: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) + else: + span.set_data(SPANDATA.DB_SYSTEM, vendor) + + if db_operation is not None: + span.set_data(SPANDATA.DB_OPERATION, db_operation) + + # Some custom backends override `__getattr__`, making it look like `cursor_or_db` + # actually has a `connection` and the `connection` has a `get_dsn_parameters` + # attribute, only to throw an error once you actually want to call it. + # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable + # function. + is_psycopg2 = ( + hasattr(cursor_or_db, "connection") + and hasattr(cursor_or_db.connection, "get_dsn_parameters") + and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) + ) + if is_psycopg2: + connection_params = cursor_or_db.connection.get_dsn_parameters() + else: + try: + # psycopg3, only extract needed params as get_parameters + # can be slow because of the additional logic to filter out default + # values + connection_params = { + "dbname": cursor_or_db.connection.info.dbname, + "port": cursor_or_db.connection.info.port, + } + # PGhost returns host or base dir of UNIX socket as an absolute path + # starting with /, use it only when it contains host + pg_host = cursor_or_db.connection.info.host + if pg_host and not pg_host.startswith("/"): + connection_params["host"] = pg_host + except Exception: + connection_params = db.get_connection_params() + + db_name = connection_params.get("dbname") or connection_params.get("database") + + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + + set_on_span = span.set_attribute + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + set_on_span = span.set_data + + server_address = connection_params.get("host") + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = connection_params.get("port") + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, str(server_port)) + + server_socket_address = connection_params.get("unix_socket") + if server_socket_address is not None: + set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) + + +def add_template_context_repr_sequence() -> None: + try: + from django.template.context import BaseContext + + add_repr_sequence_type(BaseContext) + except Exception: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py new file mode 100644 index 0000000000..43faffb5be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py @@ -0,0 +1,262 @@ +""" +Instrumentation for Django 3.0 + +Since this file contains `async def` it is conditionally imported in +`sentry_sdk.integrations.django` (depending on the existence of +`django.core.handlers.asgi`. +""" + +import asyncio +import functools +import inspect +from typing import TYPE_CHECKING + +from django.core.handlers.wsgi import WSGIRequest + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar, Union + + from django.core.handlers.asgi import ASGIRequest + from django.http.response import HttpResponse + + from sentry_sdk._types import Event, EventProcessor + + _F = TypeVar("_F", bound=Callable[..., Any]) + + +# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for +# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. +# The latter is replaced with the inspect.markcoroutinefunction decorator. +# Until 3.12 is the minimum supported Python version, provide a shim. +# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py +if hasattr(inspect, "markcoroutinefunction"): + iscoroutinefunction = inspect.iscoroutinefunction + markcoroutinefunction = inspect.markcoroutinefunction +else: + iscoroutinefunction = asyncio.iscoroutinefunction + + def markcoroutinefunction(func: "_F") -> "_F": + func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + return func + + +def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": + def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + from sentry_sdk.integrations.django import ( + DjangoRequestExtractor, + _set_user_info, + ) + + if request is None: + return event + + if type(request) == WSGIRequest: + return event + + with capture_internal_exceptions(): + DjangoRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _set_user_info(request, event) + + return event + + return asgi_request_event_processor + + +def patch_django_asgi_handler_impl(cls: "Any") -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + )._run_asgi3 + + return await middleware(scope, receive, send) + + cls.__call__ = sentry_patched_asgi_handler + + modern_django_asgi_support = hasattr(cls, "create_request") + if modern_django_asgi_support: + old_create_request = cls.create_request + + @ensure_integration_enabled(DjangoIntegration, old_create_request) + def sentry_patched_create_request( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + request, error_response = old_create_request(self, *args, **kwargs) + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_make_asgi_request_event_processor(request)) + + return request, error_response + + cls.create_request = sentry_patched_create_request + + +def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: + old_get_response_async = cls.get_response_async + + async def sentry_patched_get_response_async( + self: "Any", request: "Any" + ) -> "Union[HttpResponse, BaseException]": + _before_get_response(request) + return await old_get_response_async(self, request) + + cls.get_response_async = sentry_patched_get_response_async + + +def patch_channels_asgi_handler_impl(cls: "Any") -> None: + import channels # type: ignore + + from sentry_sdk.integrations.django import DjangoIntegration + + if channels.__version__ < "3.0.0": + old_app = cls.__call__ + + async def sentry_patched_asgi_handler( + self: "Any", receive: "Any", send: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return await old_app(self, receive, send) + + middleware = SentryAsgiMiddleware( + lambda _scope: old_app.__get__(self, cls), + unsafe_context_data=True, + span_origin=DjangoIntegration.origin, + http_methods_to_capture=integration.http_methods_to_capture, + ) + + return await middleware(self.scope)(receive, send) # type: ignore + + cls.__call__ = sentry_patched_asgi_handler + + else: + # The ASGI handler in Channels >= 3 has the same signature as + # the Django handler. + patch_django_asgi_handler_impl(cls) + + +def wrap_async_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + async def sentry_wrapped_callback( + request: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return await callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return await callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return await callback(request, *args, **kwargs) + + return sentry_wrapped_callback + + +def _asgi_middleware_mixin_factory( + _check_middleware_span: "Callable[..., Any]", +) -> "Any": + """ + Mixin class factory that generates a middleware mixin for handling requests + in async mode. + """ + + class SentryASGIMixin: + if TYPE_CHECKING: + _inner = None + + def __init__(self, get_response: "Callable[..., Any]") -> None: + self.get_response = get_response + self._acall_method = None + self._async_check() + + def _async_check(self) -> None: + """ + If get_response is a coroutine function, turns us into async mode so + a thread is not consumed during a whole request. + Taken from django.utils.deprecation::MiddlewareMixin._async_check + """ + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) + + def async_route_check(self) -> bool: + """ + Function that checks if we are in async mode, + and if we are forwards the handling of requests to __acall__ + """ + return iscoroutinefunction(self.get_response) + + async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": + f = self._acall_method + if f is None: + if hasattr(self._inner, "__acall__"): + self._acall_method = f = self._inner.__acall__ # type: ignore + else: + self._acall_method = f = self._inner + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return await f(*args, **kwargs) # type: ignore + + with middleware_span: + return await f(*args, **kwargs) # type: ignore + + return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py new file mode 100644 index 0000000000..faf1803c11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py @@ -0,0 +1,264 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.core.cache import CacheHandler +from urllib3.util import parse_url as urlparse + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional + + +METHODS_TO_INSTRUMENT = [ + "set", + "set_many", + "get", + "get_many", +] + + +def _get_span_description( + method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" +) -> str: + return _key_as_string(_get_safe_key(method_name, args, kwargs)) + + +def _patch_cache_method( + cache: "CacheHandler", + method_name: str, + address: "Optional[str]", + port: "Optional[int]", +) -> None: + from sentry_sdk.integrations.django import DjangoIntegration + + original_method = getattr(cache, method_name) + + @ensure_integration_enabled(DjangoIntegration, original_method) + def _instrument_call( + cache: "CacheHandler", + method_name: str, + original_method: "Callable[..., Any]", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + address: "Optional[str]", + port: "Optional[int]", + ) -> "Any": + is_set_operation = method_name.startswith("set") + is_get_method = method_name == "get" + is_get_many_method = method_name == "get_many" + + op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET + description = _get_span_description(method_name, args, kwargs) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": op, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_attribute(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + else: + with sentry_sdk.start_span( + op=op, + name=description, + origin=DjangoIntegration.origin, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_data(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_data(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + + @functools.wraps(original_method) + def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": + return _instrument_call( + cache, method_name, original_method, args, kwargs, address, port + ) + + setattr(cache, method_name, sentry_method) + + +def _patch_cache( + cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None +) -> None: + if not hasattr(cache, "_sentry_patched"): + for method_name in METHODS_TO_INSTRUMENT: + _patch_cache_method(cache, method_name, address, port) + cache._sentry_patched = True + + +def _get_address_port( + settings: "dict[str, Any]", +) -> "tuple[Optional[str], Optional[int]]": + location = settings.get("LOCATION") + + # TODO: location can also be an array of locations + # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis + # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 + if not isinstance(location, str): + return None, None + + if "://" in location: + parsed_url = urlparse(location) + # remove the username and password from URL to not leak sensitive data. + address = "{}://{}{}".format( + parsed_url.scheme or "", + parsed_url.hostname or "", + parsed_url.path or "", + ) + port = parsed_url.port + else: + address = location + port = None + + return address, int(port) if port is not None else None + + +def should_enable_cache_spans() -> bool: + from sentry_sdk.integrations.django import DjangoIntegration + + client = sentry_sdk.get_client() + integration = client.get_integration(DjangoIntegration) + from django.conf import settings + + return integration is not None and ( + (client.spotlight is not None and settings.DEBUG is True) + or integration.cache_spans is True + ) + + +def patch_caching() -> None: + if not hasattr(CacheHandler, "_sentry_patched"): + if DJANGO_VERSION < (3, 2): + original_get_item = CacheHandler.__getitem__ + + @functools.wraps(original_get_item) + def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": + cache = original_get_item(self, alias) + + if should_enable_cache_spans(): + from django.conf import settings + + address, port = _get_address_port( + settings.CACHES[alias or "default"] + ) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.__getitem__ = sentry_get_item + CacheHandler._sentry_patched = True + + else: + original_create_connection = CacheHandler.create_connection + + @functools.wraps(original_create_connection) + def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": + cache = original_create_connection(self, alias) + + if should_enable_cache_spans(): + address, port = _get_address_port(self.settings[alias or "default"]) + + _patch_cache(cache, address, port) + + return cache + + CacheHandler.create_connection = sentry_create_connection + CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py new file mode 100644 index 0000000000..a14ec96ff5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py @@ -0,0 +1,219 @@ +""" +Create spans from Django middleware invocations +""" + +from functools import wraps +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + F = TypeVar("F", bound=Callable[..., Any]) + +_import_string_should_wrap_middleware = ContextVar( + "import_string_should_wrap_middleware" +) + +DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) + +if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: + _asgi_middleware_mixin_factory = lambda _: object + iscoroutinefunction = lambda _: False +else: + from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction + + +def patch_django_middlewares() -> None: + from django.core.handlers import base + + old_import_string = base.import_string + + def sentry_patched_import_string(dotted_path: str) -> "Any": + rv = old_import_string(dotted_path) + + if _import_string_should_wrap_middleware.get(None): + rv = _wrap_middleware(rv, dotted_path) + + return rv + + base.import_string = sentry_patched_import_string + + old_load_middleware = base.BaseHandler.load_middleware + + def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": + _import_string_should_wrap_middleware.set(True) + try: + return old_load_middleware(*args, **kwargs) + finally: + _import_string_should_wrap_middleware.set(False) + + base.BaseHandler.load_middleware = sentry_patched_load_middleware + + +def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + def _check_middleware_span( + old_method: "Callable[..., Any]", + ) -> "Optional[Union[Span, StreamedSpan]]": + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None or not integration.middleware_spans: + return None + + function_name = transaction_from_function(old_method) + + description = middleware_name + function_basename = getattr(old_method, "__name__", None) + if function_basename: + description = "{}.{}".format(description, function_basename) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + middleware_span: "Union[Span, StreamedSpan]" + if span_streaming: + middleware_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.MIDDLEWARE_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + else: + middleware_span = sentry_sdk.start_span( + op=OP.MIDDLEWARE_DJANGO, + name=description, + origin=DjangoIntegration.origin, + ) + middleware_span.set_tag("django.function_name", function_name) + middleware_span.set_tag("django.middleware_name", middleware_name) + + return middleware_span + + def _get_wrapped_method(old_method: "F") -> "F": + with capture_internal_exceptions(): + # Middleware hooks (e.g. `process_view`, `process_exception`) may be + # `async def` when the middleware is async. A synchronous wrapper + # would hide the coroutine from Django's `iscoroutinefunction` check, + # causing Django to call the hook synchronously and never await the + # returned coroutine. Wrap async hooks with an async wrapper so the + # wrapped method continues to report as a coroutine function. + if iscoroutinefunction is not None and iscoroutinefunction(old_method): + + async def async_sentry_wrapped_method( + *args: "Any", **kwargs: "Any" + ) -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return await old_method(*args, **kwargs) + + with middleware_span: + return await old_method(*args, **kwargs) + + sentry_wrapped_method = async_sentry_wrapped_method + + else: + + def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": + middleware_span = _check_middleware_span(old_method) + + if middleware_span is None: + return old_method(*args, **kwargs) + + with middleware_span: + return old_method(*args, **kwargs) + + sentry_wrapped_method = sync_sentry_wrapped_method + + try: + # fails for __call__ of function on Python 2 (see py2.7-django-1.11) + sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) + + # Necessary for Django 3.1 + sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore + except Exception: + pass + + return sentry_wrapped_method # type: ignore + + return old_method + + class SentryWrappingMiddleware( + _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore + ): + sync_capable = getattr(middleware, "sync_capable", True) + async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( + middleware, "async_capable", False + ) + + def __init__( + self, + get_response: "Optional[Callable[..., Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> None: + if get_response: + self._inner = middleware(get_response, *args, **kwargs) + else: + self._inner = middleware(*args, **kwargs) + self.get_response = get_response + self._call_method = None + if self.async_capable: + super().__init__(get_response) + + # We need correct behavior for `hasattr()`, which we can only determine + # when we have an instance of the middleware we're wrapping. + def __getattr__(self, method_name: str) -> "Any": + if method_name not in ( + "process_request", + "process_view", + "process_template_response", + "process_response", + "process_exception", + ): + raise AttributeError() + + old_method = getattr(self._inner, method_name) + rv = _get_wrapped_method(old_method) + self.__dict__[method_name] = rv + return rv + + def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": + if hasattr(self, "async_route_check") and self.async_route_check(): + return self.__acall__(*args, **kwargs) + + f = self._call_method + if f is None: + self._call_method = f = self._inner.__call__ + + middleware_span = _check_middleware_span(old_method=f) + + if middleware_span is None: + return f(*args, **kwargs) + + with middleware_span: + return f(*args, **kwargs) + + for attr in ( + "__name__", + "__module__", + "__qualname__", + ): + if hasattr(middleware, attr): + setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) + + return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py new file mode 100644 index 0000000000..7140ead782 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py @@ -0,0 +1,105 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from django.dispatch import Signal + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.django import DJANGO_VERSION +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Union + + +def _get_receiver_name(receiver: "Callable[..., Any]") -> str: + name = "" + + if hasattr(receiver, "__qualname__"): + name = receiver.__qualname__ + elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ + name = receiver.__name__ + elif hasattr( + receiver, "func" + ): # certain functions (like partials) dont have a name + if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): + name = "partial()" + + if ( + name == "" + ): # In case nothing was found, return the string representation (this is the slowest case) + return str(receiver) + + if hasattr(receiver, "__module__"): # prepend with module, if there is one + name = receiver.__module__ + "." + name + + return name + + +def patch_signals() -> None: + """ + Patch django signal receivers to create a span. + + This only wraps sync receivers. Django>=5.0 introduced async receivers, but + since we don't create transactions for ASGI Django, we don't wrap them. + """ + from sentry_sdk.integrations.django import DjangoIntegration + + old_live_receivers = Signal._live_receivers + + def _sentry_live_receivers( + self: "Signal", sender: "Any" + ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": + if DJANGO_VERSION >= (5, 0): + sync_receivers, async_receivers = old_live_receivers(self, sender) + else: + sync_receivers = old_live_receivers(self, sender) + async_receivers = [] + + def sentry_sync_receiver_wrapper( + receiver: "Callable[..., Any]", + ) -> "Callable[..., Any]": + @wraps(receiver) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + signal_name = _get_receiver_name(receiver) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + with sentry_sdk.traces.start_span( + name=signal_name, + attributes={ + "sentry.op": OP.EVENT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + SPANDATA.CODE_FUNCTION_NAME: signal_name, + }, + ) as span: + return receiver(*args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.EVENT_DJANGO, + name=signal_name, + origin=DjangoIntegration.origin, + ) as span: + span.set_data("signal", signal_name) + return receiver(*args, **kwargs) + + return wrapper + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if ( + integration + and integration.signals_spans + and self not in integration.signals_denylist + ): + for idx, receiver in enumerate(sync_receivers): + sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) + + if DJANGO_VERSION >= (5, 0): + return sync_receivers, async_receivers + else: + return sync_receivers + + Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py new file mode 100644 index 0000000000..5e23c258fb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py @@ -0,0 +1,52 @@ +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import qualname_from_function + +try: + # django.tasks were added in Django 6.0 + from django.tasks.base import Task +except ImportError: + Task = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +def patch_tasks() -> None: + if Task is None: + return + + old_task_enqueue = Task.enqueue + + @wraps(old_task_enqueue) + def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is None: + return old_task_enqueue(self, *args, **kwargs) + + name = qualname_from_function(self.func) or "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_task_enqueue(self, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin + ): + return old_task_enqueue(self, *args, **kwargs) + + Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py new file mode 100644 index 0000000000..5ab89d4a74 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py @@ -0,0 +1,209 @@ +import functools +from typing import TYPE_CHECKING + +from django import VERSION as DJANGO_VERSION +from django.template import TemplateSyntaxError +from django.utils.safestring import mark_safe + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Dict, Iterator, Optional, Tuple + +try: + # support Django 1.9 + from django.template.base import Origin +except ImportError: + # backward compatibility + from django.template.loader import LoaderOrigin as Origin + + +def get_template_frame_from_exception( + exc_value: "Optional[BaseException]", +) -> "Optional[Dict[str, Any]]": + # As of Django 1.9 or so the new template debug thing showed up. + if hasattr(exc_value, "template_debug"): + return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore + + # As of r16833 (Django) all exceptions may contain a + # ``django_template_source`` attribute (rather than the legacy + # ``TemplateSyntaxError.source`` check) + if hasattr(exc_value, "django_template_source"): + return _get_template_frame_from_source( + exc_value.django_template_source # type: ignore + ) + + if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): + source = exc_value.source + if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): + return _get_template_frame_from_source(source) # type: ignore + + return None + + +def _get_template_name_description(template_name: str) -> str: + if isinstance(template_name, (list, tuple)): + if template_name: + return "[{}, ...]".format(template_name[0]) + else: + return template_name + + +def patch_templates() -> None: + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + real_rendered_content = SimpleTemplateResponse.rendered_content + + @property # type: ignore + @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) + def rendered_content(self: "SimpleTemplateResponse") -> str: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(self.template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_rendered_content.fget(self) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(self.template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", self.context_data) + return real_rendered_content.fget(self) + + SimpleTemplateResponse.rendered_content = rendered_content + + if DJANGO_VERSION < (1, 7): + return + import django.shortcuts + + real_render = django.shortcuts.render + + @functools.wraps(real_render) + @ensure_integration_enabled(DjangoIntegration, real_render) + def render( + request: "django.http.HttpRequest", + template_name: str, + context: "Optional[Dict[str, Any]]" = None, + *args: "Any", + **kwargs: "Any", + ) -> "django.http.HttpResponse": + # Inject trace meta tags into template context + context = context or {} + if "sentry_trace_meta" not in context: + context["sentry_trace_meta"] = mark_safe( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_render(request, template_name, context, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", context) + return real_render(request, template_name, context, *args, **kwargs) + + django.shortcuts.render = render + + +def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": + if debug is None: + return None + + lineno = debug["line"] + filename = debug["name"] + if filename is None: + filename = "" + + pre_context = [] + post_context = [] + context_line = None + + for i, line in debug["source_lines"]: + if i < lineno: + pre_context.append(line) + elif i > lineno: + post_context.append(line) + else: + context_line = line + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + "in_app": True, + } + + +def _linebreak_iter(template_source: str) -> "Iterator[int]": + yield 0 + p = template_source.find("\n") + while p >= 0: + yield p + 1 + p = template_source.find("\n", p + 1) + + +def _get_template_frame_from_source( + source: "Tuple[Origin, Tuple[int, int]]", +) -> "Optional[Dict[str, Any]]": + if not source: + return None + + origin, (start, end) = source + filename = getattr(origin, "loadname", None) + if filename is None: + filename = "" + template_source = origin.reload() + lineno = None + upto = 0 + pre_context = [] + post_context = [] + context_line = None + + for num, next in enumerate(_linebreak_iter(template_source)): + line = template_source[upto:next] + if start >= upto and end <= next: + lineno = num + context_line = line + elif lineno is None: + pre_context.append(line) + else: + post_context.append(line) + + upto = next + + if context_line is None or lineno is None: + return None + + return { + "filename": filename, + "lineno": lineno, + "pre_context": pre_context[-5:], + "post_context": post_context[:5], + "context_line": context_line, + } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py new file mode 100644 index 0000000000..192f0765e6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py @@ -0,0 +1,154 @@ +""" +Copied from raven-python. + +Despite being called "legacy" in some places this resolver is very much still +in use. +""" + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from re import Pattern + from typing import Dict, List, Optional, Tuple, Union + + from django.urls.resolvers import URLPattern, URLResolver + +from django import VERSION as DJANGO_VERSION + +if DJANGO_VERSION >= (2, 0): + from django.urls.resolvers import RoutePattern +else: + RoutePattern = None + +try: + from django.urls import get_resolver +except ImportError: + from django.core.urlresolvers import get_resolver + + +def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": + """Utility method for django's deprecated resolver.regex""" + try: + regex = resolver_or_pattern.regex + except AttributeError: + regex = resolver_or_pattern.pattern.regex + return regex + + +class RavenResolver: + _new_style_group_matcher = re.compile( + r"<(?:([^>:]+):)?([^>]+)>" + ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 + _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") + _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") + _non_named_group_matcher = re.compile(r"\([^\)]+\)") + # [foo|bar|baz] + _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") + _camel_re = re.compile(r"([A-Z]+)([a-z])") + + _cache: "Dict[URLPattern, str]" = {} + + def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: + r""" + Clean up urlpattern regexes into something readable by humans: + + From: + > "^(?P\w+)/athletes/(?P\w+)/$" + + To: + > "{sport_slug}/athletes/{athlete_slug}/" + """ + # "new-style" path patterns can be parsed directly without turning them + # into regexes first + if ( + RoutePattern is not None + and hasattr(pattern, "pattern") + and isinstance(pattern.pattern, RoutePattern) + ): + return self._new_style_group_matcher.sub( + lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) + ) + + result = get_regex(pattern).pattern + + # remove optional params + # TODO(dcramer): it'd be nice to change these into [%s] but it currently + # conflicts with the other rules because we're doing regexp matches + # rather than parsing tokens + result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) + + # handle named groups first + result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) + + # handle non-named groups + result = self._non_named_group_matcher.sub("{var}", result) + + # handle optional params + result = self._either_option_matcher.sub(lambda m: m.group(1), result) + + # clean up any outstanding regex-y characters. + result = ( + result.replace("^", "") + .replace("$", "") + .replace("?", "") + .replace("\\A", "") + .replace("\\Z", "") + .replace("//", "/") + .replace("\\", "") + ) + + return result + + def _resolve( + self, + resolver: "URLResolver", + path: str, + parents: "Optional[List[URLResolver]]" = None, + ) -> "Optional[str]": + match = get_regex(resolver).search(path) # Django < 2.0 + + if not match: + return None + + if parents is None: + parents = [resolver] + elif resolver not in parents: + parents = parents + [resolver] + + new_path = path[match.end() :] + for pattern in resolver.url_patterns: + # this is an include() + if not pattern.callback: + match_ = self._resolve(pattern, new_path, parents) + if match_: + return match_ + continue + elif not get_regex(pattern).search(new_path): + continue + + try: + return self._cache[pattern] + except KeyError: + pass + + prefix = "".join(self._simplify(p) for p in parents) + result = prefix + self._simplify(pattern) + if not result.startswith("/"): + result = "/" + result + self._cache[pattern] = result + return result + + return None + + def resolve( + self, + path: str, + urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, + ) -> "Optional[str]": + resolver = get_resolver(urlconf) + match = self._resolve(resolver, path) + return match + + +LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py new file mode 100644 index 0000000000..cf3012a75e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py @@ -0,0 +1,127 @@ +import functools +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +try: + from asyncio import iscoroutinefunction +except ImportError: + iscoroutinefunction = None # type: ignore + + +try: + from sentry_sdk.integrations.django.asgi import wrap_async_view +except (ImportError, SyntaxError): + wrap_async_view = None # type: ignore + + +def patch_views() -> None: + from django.core.handlers.base import BaseHandler + from django.template.response import SimpleTemplateResponse + + from sentry_sdk.integrations.django import DjangoIntegration + + old_make_view_atomic = BaseHandler.make_view_atomic + old_render = SimpleTemplateResponse.render + + def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="serialize response", + attributes={ + "sentry.op": OP.VIEW_RESPONSE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_render(self) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RESPONSE_RENDER, + name="serialize response", + origin=DjangoIntegration.origin, + ): + return old_render(self) + + @functools.wraps(old_make_view_atomic) + def sentry_patched_make_view_atomic( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + callback = old_make_view_atomic(self, *args, **kwargs) + + # XXX: The wrapper function is created for every request. Find more + # efficient way to wrap views (or build a cache?) + + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + if integration is not None: + is_async_view = ( + iscoroutinefunction is not None + and wrap_async_view is not None + and iscoroutinefunction(callback) + ) + if is_async_view: + sentry_wrapped_callback = wrap_async_view(callback) + else: + sentry_wrapped_callback = _wrap_sync_view(callback) + + else: + sentry_wrapped_callback = callback + + return sentry_wrapped_callback + + SimpleTemplateResponse.render = sentry_patched_render + BaseHandler.make_view_atomic = sentry_patched_make_view_atomic + + +def _wrap_sync_view(callback: "Any") -> "Any": + from sentry_sdk.integrations.django import DjangoIntegration + + @functools.wraps(callback) + def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + current_scope = sentry_sdk.get_current_scope() + if span_streaming: + current_span = current_scope.streamed_span + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + else: + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + # set the active thread id to the handler thread for sync views + # this isn't necessary for async views since that runs on main + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + integration = client.get_integration(DjangoIntegration) + if not integration or not integration.middleware_spans: + return callback(request, *args, **kwargs) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return callback(request, *args, **kwargs) + + return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py new file mode 100644 index 0000000000..310766ee3a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py @@ -0,0 +1,246 @@ +import json +from typing import TypeVar + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import request_body_within_bounds +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, +) + +R = TypeVar("R") + +try: + from dramatiq.broker import Broker + from dramatiq.errors import Retry + from dramatiq.message import Message + from dramatiq.middleware import Middleware, default_middleware +except ImportError: + raise DidNotEnable("Dramatiq is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Union + + from sentry_sdk._types import Event, Hint + + +class DramatiqIntegration(Integration): + """ + Dramatiq integration for Sentry + + Please make sure that you call `sentry_sdk.init` *before* initializing + your broker, as it monkey patches `Broker.__init__`. + + This integration was originally developed and maintained + by https://github.com/jacobsvante and later donated to the Sentry + project. + """ + + identifier = "dramatiq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_dramatiq_broker() + + +def _patch_dramatiq_broker() -> None: + original_broker__init__ = Broker.__init__ + + def sentry_patched_broker__init__( + self: "Broker", *args: "Any", **kw: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + + try: + middleware = kw.pop("middleware") + except KeyError: + # Unfortunately Broker and StubBroker allows middleware to be + # passed in as positional arguments, whilst RabbitmqBroker and + # RedisBroker does not. + if len(args) == 1: + middleware = args[0] + args = () + else: + middleware = None + + if middleware is None: + middleware = list(m() for m in default_middleware) + else: + middleware = list(middleware) + + if integration is not None: + middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] + middleware.insert(0, SentryMiddleware()) + + kw["middleware"] = middleware + original_broker__init__(self, *args, **kw) + + Broker.__init__ = sentry_patched_broker__init__ + + +class SentryMiddleware(Middleware): # type: ignore[misc] + """ + A Dramatiq middleware that automatically captures and sends + exceptions to Sentry. + + This is automatically added to every instantiated broker via the + DramatiqIntegration. + """ + + SENTRY_HEADERS_NAME = "_sentry_headers" + + def before_enqueue( + self, broker: "Broker", message: "Message[R]", delay: int + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + message.options[self.SENTRY_HEADERS_NAME] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + + def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(DramatiqIntegration) + if integration is None: + return + + message._scope_manager = sentry_sdk.isolation_scope() + scope = message._scope_manager.__enter__() + scope.clear_breadcrumbs() + scope.set_extra("dramatiq_message_id", message.message_id) + scope.add_event_processor(_make_message_event_processor(message, integration)) + + sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} + if "retries" in message.options: + # start new trace in case of retrying + sentry_headers = {} + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(sentry_headers) + span = sentry_sdk.traces.start_span( + name=message.actor_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_DRAMATIQ, + "sentry.origin": DramatiqIntegration.origin, + "sentry.span.source": SegmentSource.TASK.value, + }, + parent_span=None, + ) + message._sentry_span_ctx = span + else: + transaction = continue_trace( + sentry_headers, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + origin=DramatiqIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + sentry_sdk.start_transaction( + transaction, + name=message.actor_name, + op=OP.QUEUE_TASK_DRAMATIQ, + source=TransactionSource.TASK, + ) + transaction.__enter__() + message._sentry_span_ctx = transaction + + def after_process_message( + self, + broker: "Broker", + message: "Message[R]", + *, + result: "Optional[Any]" = None, + exception: "Optional[Exception]" = None, + ) -> None: + integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) + if integration is None: + return + + actor = broker.get_actor(message.actor_name) + throws = message.options.get("throws") or actor.options.get("throws") + + scope_manager = message._scope_manager + span_ctx = getattr(message, "_sentry_span_ctx", None) + if span_ctx is None: + return None + + is_event_capture_required = ( + exception is not None + and not (throws and isinstance(exception, throws)) + and not isinstance(exception, Retry) + ) + if not is_event_capture_required: + # normal transaction finish + span_ctx.__exit__(None, None, None) + scope_manager.__exit__(None, None, None) + return + + event, hint = event_from_exception( + exception, # type: ignore[arg-type] + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": DramatiqIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + # transaction error + span_ctx.__exit__(type(exception), exception, None) + scope_manager.__exit__(type(exception), exception, None) + + after_skip_message = after_process_message + + +def _make_message_event_processor( + message: "Message[R]", integration: "DramatiqIntegration" +) -> "Callable[[Event, Hint], Optional[Event]]": + def inner(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + DramatiqMessageExtractor(message).extract_into_event(event) + + return event + + return inner + + +class DramatiqMessageExtractor: + def __init__(self, message: "Message[R]") -> None: + self.message_data = dict(message.asdict()) + + def content_length(self) -> int: + return len(json.dumps(self.message_data)) + + def extract_into_event(self, event: "Event") -> None: + client = sentry_sdk.get_client() + if not client.is_active(): + return + + contexts = event.setdefault("contexts", {}) + request_info = contexts.setdefault("dramatiq", {}) + request_info["type"] = "dramatiq" + + data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None + if not request_body_within_bounds(client, self.content_length()): + data = AnnotatedValue.removed_because_over_size_limit() + else: + data = self.message_data + + request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py new file mode 100644 index 0000000000..6bbc61000d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py @@ -0,0 +1,76 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + + +class ExcepthookIntegration(Integration): + identifier = "excepthook" + + always_run = False + + def __init__(self, always_run: bool = False) -> None: + if not isinstance(always_run, bool): + raise ValueError( + "Invalid value for always_run: %s (must be type boolean)" + % (always_run,) + ) + self.always_run = always_run + + @staticmethod + def setup_once() -> None: + sys.excepthook = _make_excepthook(sys.excepthook) + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + if _should_send(integration.always_run): + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "excepthook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook + + +def _should_send(always_run: bool = False) -> bool: + if always_run: + return True + + if hasattr(sys, "ps1"): + # Disable the excepthook for interactive Python shells, otherwise + # every typo gets sent to Sentry. + return False + + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py new file mode 100644 index 0000000000..4473fcc435 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + + +class ExecutingIntegration(Integration): + identifier = "executing" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + frame = tb.tb_frame + source = Source.for_frame(frame) + sentry_frame["function"] = source.code_qualname(frame.f_code) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py new file mode 100644 index 0000000000..7a595bcf2a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + + from sentry_sdk._types import Event, EventProcessor + +# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` +# and `falcon.API` to `falcon.App` + +try: + import falcon # type: ignore + from falcon import __version__ as FALCON_VERSION +except ImportError: + raise DidNotEnable("Falcon not installed") + +try: + import falcon.app_helpers # type: ignore + + falcon_helpers = falcon.app_helpers + falcon_app_class = falcon.App + FALCON3 = True +except ImportError: + import falcon.api_helpers # type: ignore + + falcon_helpers = falcon.api_helpers + falcon_app_class = falcon.API + FALCON3 = False + + +_FALCON_UNSET: "Optional[object]" = None +if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ + with capture_internal_exceptions(): + from falcon.request import ( # type: ignore[import-not-found, no-redef] + _UNSET as _FALCON_UNSET, + ) + + +class FalconRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, Any]": + return self.request.env + + def cookies(self) -> "Dict[str, Any]": + return self.request.cookies + + def form(self) -> None: + return None # No such concept in Falcon + + def files(self) -> None: + return None # No such concept in Falcon + + def raw_data(self) -> "Optional[str]": + # As request data can only be read once we won't make this available + # to Sentry. Just send back a dummy string in case there was a + # content length. + # TODO(jmagnusson): Figure out if there's a way to support this + content_length = self.content_length() + if content_length > 0: + return "[REQUEST_CONTAINING_RAW_DATA]" + else: + return None + + def json(self) -> "Optional[Dict[str, Any]]": + # fallback to cached_media = None if self.request._media is not available + cached_media = None + with capture_internal_exceptions(): + # self.request._media is the cached self.request.media + # value. It is only available if self.request.media + # has already been accessed. Therefore, reading + # self.request._media will not exhaust the raw request + # stream (self.request.bounded_stream) because it has + # already been read if self.request._media is set. + cached_media = self.request._media + + if cached_media is not _FALCON_UNSET: + return cached_media + + return None + + +class SentryFalconMiddleware: + """Captures exceptions in Falcon requests and send to Sentry""" + + def process_request( + self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return + + scope = sentry_sdk.get_isolation_scope() + scope._name = "falcon" + scope.add_event_processor(_make_request_event_processor(req, integration)) + + def process_resource( + self, req: "Any", resp: "Any", resource: "Any", params: "Any" + ) -> None: + """ + Sets the segment name and source as the route is resolved when this runs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FalconIntegration) + if integration is None or not has_span_streaming_enabled(client.options): + return + + name_for_style = { + "uri_template": req.uri_template, + "path": req.path, + } + name = name_for_style[integration.transaction_style] + source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] + sentry_sdk.set_transaction_name(name, source) + + +TRANSACTION_STYLE_VALUES = ("uri_template", "path") + + +class FalconIntegration(Integration): + identifier = "falcon" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "uri_template") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + version = parse_version(FALCON_VERSION) + _check_minimum_version(FalconIntegration, version) + + _patch_wsgi_app() + _patch_handle_exception() + _patch_prepare_middleware() + + +def _patch_wsgi_app() -> None: + original_wsgi_app = falcon_app_class.__call__ + + def sentry_patched_wsgi_app( + self: "falcon.API", env: "Any", start_response: "Any" + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is None: + return original_wsgi_app(self, env, start_response) + + sentry_wrapped = SentryWsgiMiddleware( + lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), + span_origin=FalconIntegration.origin, + ) + + return sentry_wrapped(env, start_response) + + falcon_app_class.__call__ = sentry_patched_wsgi_app + + +def _patch_handle_exception() -> None: + original_handle_exception = falcon_app_class._handle_exception + + @ensure_integration_enabled(FalconIntegration, original_handle_exception) + def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": + # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception + # method signature from `(ex, req, resp, params)` to + # `(req, resp, ex, params)` + ex = response = None + with capture_internal_exceptions(): + ex = next(argument for argument in args if isinstance(argument, Exception)) + response = next( + argument for argument in args if isinstance(argument, falcon.Response) + ) + + was_handled = original_handle_exception(self, *args) + + if ex is None or response is None: + # Both ex and response should have a non-None value at this point; otherwise, + # there is an error with the SDK that will have been captured in the + # capture_internal_exceptions block above. + return was_handled + + if _exception_leads_to_http_5xx(ex, response): + event, hint = event_from_exception( + ex, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "falcon", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return was_handled + + falcon_app_class._handle_exception = sentry_patched_handle_exception + + +def _patch_prepare_middleware() -> None: + original_prepare_middleware = falcon_helpers.prepare_middleware + + def sentry_patched_prepare_middleware( + middleware: "Any" = None, + independent_middleware: "Any" = False, + asgi: bool = False, + ) -> "Any": + if asgi: + # We don't support ASGI Falcon apps, so we don't patch anything here + return original_prepare_middleware(middleware, independent_middleware, asgi) + + integration = sentry_sdk.get_client().get_integration(FalconIntegration) + if integration is not None: + middleware = [SentryFalconMiddleware()] + (middleware or []) + + # We intentionally omit the asgi argument here, since the default is False anyways, + # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. + return original_prepare_middleware(middleware, independent_middleware) + + falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware + + +def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: + is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( + "5" + ) + is_unhandled_error = not isinstance( + ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) + ) + + # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response + # at the stage where we capture it is listed as 200, even though we would expect to see a 500 + # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to + # only perform this check on Falcon 3+, despite the risk that some handled errors might be + # reported to Sentry as unhandled on Falcon 2. + return (is_server_error or is_unhandled_error) and ( + not FALCON3 or _has_http_5xx_status(response) + ) + + +def _has_http_5xx_status(response: "falcon.Response") -> bool: + return response.status.startswith("5") + + +def _set_transaction_name_and_source( + event: "Event", transaction_style: str, request: "falcon.Request" +) -> None: + name_for_style = { + "uri_template": request.uri_template, + "path": request.path, + } + event["transaction"] = name_for_style[transaction_style] + event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} + + +def _make_request_event_processor( + req: "falcon.Request", integration: "FalconIntegration" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + _set_transaction_name_and_source(event, integration.transaction_style, req) + + with capture_internal_exceptions(): + FalconRequestExtractor(req).extract_into_event(event) + + return event + + return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py new file mode 100644 index 0000000000..c7b97c88b1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py @@ -0,0 +1,201 @@ +import sys +from copy import deepcopy +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import transaction_from_function + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Dict + + from sentry_sdk._types import Event + +try: + from sentry_sdk.integrations.starlette import ( + StarletteIntegration, + StarletteRequestExtractor, + _get_cached_request_body_attribute, + ) +except DidNotEnable: + raise DidNotEnable("Starlette is not installed") + +try: + import fastapi # type: ignore +except ImportError: + raise DidNotEnable("FastAPI is not installed") + + +_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +class FastApiIntegration(StarletteIntegration): + identifier = "fastapi" + + @staticmethod + def setup_once() -> None: + patch_get_request_handler() + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = "" + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or "" + + elif transaction_style == "url": + route = request.scope.get("route") + + if route: + # FastAPI >= 0.137 stores the prefix-resolved path on an + # effective_route_context in scope["fastapi"], while + # scope["route"].path holds the unprefixed original. + # Prefer the effective context path when available. + effective_route_context = request.scope.get("fastapi", {}).get( + "effective_route_context" + ) + context_path = getattr(effective_route_context, "path", None) + + if context_path: + name = context_path + else: + path = getattr(route, "path", None) + if path is not None: + name = path + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + else: + source = SOURCE_FOR_STYLE[transaction_style] + + scope.set_transaction_name(name, source=source) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FastApiIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, Dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if info: + if "cookies" in info and should_send_default_pii(): + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = FastApiIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_get_request_handler() -> None: + old_get_request_handler = fastapi.routing.get_request_handler + + def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": + dependant = kwargs.get("dependant") + if ( + dependant + and dependant.call is not None + and not iscoroutinefunction(dependant.call) + # FastAPI >= 0.137 calls get_request_handler() on every request + # (router-tree traversal) rather than once at registration. Guard + # against accumulating _sentry_call wrappers on the shared + # dependant object, which would cause a RecursionError after ~987 + # requests as the call chain grows past Python's recursion limit. + and not getattr(dependant.call, "_sentry_is_patched", False) + ): + old_call = dependant.call + + @wraps(old_call) + def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": + current_scope = sentry_sdk.get_current_scope() + + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + segment = current_span._segment + segment._update_active_thread() + + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_call(*args, **kwargs) + + _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] + dependant.call = _sentry_call + + old_app = old_get_request_handler(*args, **kwargs) + + async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_app, *args, **kwargs) + + return _sentry_app + + fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py new file mode 100644 index 0000000000..1902091fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py @@ -0,0 +1,281 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + RequestExtractor, +) +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Union + + from werkzeug.datastructures import FileStorage, ImmutableMultiDict + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + + +try: + import flask_login # type: ignore +except ImportError: + flask_login = None + +try: + from flask import Flask, Request # type: ignore + from flask import request as flask_request + from flask.signals import ( + before_render_template, + got_request_exception, + request_started, + ) + from markupsafe import Markup +except ImportError: + raise DidNotEnable("Flask is not installed") + +try: + import blinker # noqa +except ImportError: + raise DidNotEnable("blinker is not installed") + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class FlaskIntegration(Integration): + identifier = "flask" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "endpoint", + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + @staticmethod + def setup_once() -> None: + try: + from quart import Quart # type: ignore + + if Flask == Quart: + # This is Quart masquerading as Flask, don't enable the Flask + # integration. See https://github.com/getsentry/sentry-python/issues/2709 + raise DidNotEnable( + "This is not a Flask app but rather Quart pretending to be Flask" + ) + except ImportError: + pass + + version = package_version("flask") + _check_minimum_version(FlaskIntegration, version) + + before_render_template.connect(_add_sentry_trace) + request_started.connect(_request_started) + got_request_exception.connect(_capture_exception) + + old_app = Flask.__call__ + + def sentry_patched_wsgi_app( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return old_app(self, environ, start_response) + + middleware = SentryWsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=FlaskIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + ) + return middleware(environ, start_response) + + Flask.__call__ = sentry_patched_wsgi_app + + +def _add_sentry_trace( + sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" +) -> None: + if "sentry_trace" in context: + return + + scope = sentry_sdk.get_current_scope() + trace_meta = Markup(scope.trace_propagation_meta()) + context["sentry_trace"] = trace_meta # for backwards compatibility + context["sentry_trace_meta"] = trace_meta + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + scope.set_transaction_name( + name_for_style[transaction_style], + source=SOURCE_FOR_STYLE[transaction_style], + ) + except Exception: + pass + + +def _request_started(app: "Flask", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(FlaskIntegration) + if integration is None: + return + + request = flask_request._get_current_object() + + # Set the transaction name and source here, + # but rely on WSGI middleware to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + scope.set_user(user_properties) + + evt_processor = _make_request_event_processor(app, request, integration) + scope.add_event_processor(evt_processor) + + +class FlaskRequestExtractor(RequestExtractor): + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "Dict[Any, Any]": + return { + k: v[0] if isinstance(v, list) and len(v) == 1 else v + for k, v in self.request.cookies.items() + } + + def raw_data(self) -> bytes: + return self.request.get_data() + + def form(self) -> "ImmutableMultiDict[str, Any]": + return self.request.form + + def files(self) -> "ImmutableMultiDict[str, Any]": + return self.request.files + + def is_json(self) -> bool: + return self.request.is_json + + def json(self) -> "Any": + return self.request.get_json(silent=True) + + def size_of_file(self, file: "FileStorage") -> int: + return file.content_length + + +def _make_request_event_processor( + app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + FlaskRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + _add_user_to_event(event) + + return event + + return inner + + +@ensure_integration_enabled(FlaskIntegration) +def _capture_exception( + sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "flask", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_flask_user_properties() -> "Dict[str, str]": + if flask_login is None: + return {} + + user = flask_login.current_user + if user is None: + return {} + + properties = {} + + try: + user_id = user.get_id() + if user_id is not None: + properties["id"] = user_id + except AttributeError: + # might happen if: + # - flask_login could not be imported + # - flask_login is not configured + # - no user is logged in + pass + + # The following attribute accesses are ineffective for the general + # Flask-Login case, because the User interface of Flask-Login does not + # care about anything but the ID. However, Flask-User (based on + # Flask-Login) documents a few optional extra attributes. + # + # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names + try: + if user.email is not None: + properties["email"] = user.email + except Exception: + pass + + try: + if user.username is not None: + properties["username"] = user.username + except Exception: + pass + + return properties + + +def _add_user_to_event(event: "Event") -> None: + with capture_internal_exceptions(): + user_properties = _get_flask_user_properties() + if user_properties: + user_info = event.setdefault("user", {}) + for key, value in user_properties.items(): + user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py new file mode 100644 index 0000000000..91a62b3a81 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py @@ -0,0 +1,286 @@ +import functools +import sys +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP +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 +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + TimeoutThread, + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +# Constants +TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event, EventProcessor, Hint + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_func(func: "F") -> "F": + @functools.wraps(func) + def sentry_func( + functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(GcpIntegration) + if integration is None: + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = environ.get("FUNCTION_TIMEOUT_SEC") + if not configured_time: + logger.debug( + "The configured timeout could not be fetched from Cloud Functions configuration." + ) + return func(functionhandler, gcp_event, *args, **kwargs) + + configured_time = int(configured_time) + + initial_time = datetime.now(timezone.utc) + + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + gcp_event, configured_time, initial_time + ) + ) + scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) + timeout_thread = None + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = configured_time - TIMEOUT_WARNING_BUFFER + + timeout_thread = TimeoutThread( + waiting_time, + configured_time, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = {} + header_attributes: "dict[str, Any]" = {} + if hasattr(gcp_event, "headers"): + headers = gcp_event.headers + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + # header_value will always be a string because we set `use_annotated_value` to false above + header_value + ) + + additional_attributes = {} + 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" + ) + + sampling_context = { + "gcp_env": { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + }, + "gcp_event": gcp_event, + } + + function_name = environ.get("FUNCTION_NAME", "") + + if environ.get("GCP_PROJECT"): + additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") + + if environ.get("FUNCTION_IDENTITY"): + additional_attributes["faas.identity"] = environ.get( + "FUNCTION_IDENTITY" + ) + + if environ.get("ENTRY_POINT"): + additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_GCP, + "sentry.origin": GcpIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.provider": CLOUD_PROVIDER.GCP, + "faas.name": function_name, + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_GCP, + name=environ.get("FUNCTION_NAME", ""), + source=TransactionSource.COMPONENT, + origin=GcpIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: + try: + return func(functionhandler, gcp_event, *args, **kwargs) + except Exception: + exc_info = sys.exc_info() + sentry_event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "gcp", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + # Flush out the event queue + client.flush() + + return sentry_func # type: ignore + + +class GcpIntegration(Integration): + identifier = "gcp" + origin = f"auto.function.{identifier}" + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + import __main__ as gcp_functions + + if not hasattr(gcp_functions, "worker_v1"): + logger.warning( + "GcpIntegration currently supports only Python 3.7 runtime environment." + ) + return + + worker1 = gcp_functions.worker_v1 + + worker1.FunctionHandler.invoke_user_function = _wrap_func( + worker1.FunctionHandler.invoke_user_function + ) + + +def _make_request_event_processor( + gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" +) -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + final_time = datetime.now(timezone.utc) + time_diff = final_time - initial_time + + execution_duration_in_millis = time_diff / timedelta(milliseconds=1) + + extra = event.setdefault("extra", {}) + extra["google cloud functions"] = { + "function_name": environ.get("FUNCTION_NAME"), + "function_entry_point": environ.get("ENTRY_POINT"), + "function_identity": environ.get("FUNCTION_IDENTITY"), + "function_region": environ.get("FUNCTION_REGION"), + "function_project": environ.get("GCP_PROJECT"), + "execution_duration_in_millis": execution_duration_in_millis, + "configured_timeout_in_seconds": configured_timeout, + } + + extra["google cloud logs"] = { + "url": _get_google_cloud_logs_url(final_time), + } + + request = event.get("request", {}) + + request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) + + if hasattr(gcp_event, "method"): + request["method"] = gcp_event.method + + if hasattr(gcp_event, "query_string"): + request["query_string"] = gcp_event.query_string.decode( + "utf-8", errors="replace" + ) + + if hasattr(gcp_event, "headers"): + request["headers"] = _filter_headers(gcp_event.headers) + + if should_send_default_pii(): + if hasattr(gcp_event, "data"): + request["data"] = gcp_event.data + else: + if hasattr(gcp_event, "data"): + # Unfortunately couldn't find a way to get structured body from GCP + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + event["request"] = deepcopy(request) + + return event + + return event_processor + + +def _get_google_cloud_logs_url(final_time: "datetime") -> str: + """ + Generates a Google Cloud Logs console URL based on the environment variables + Arguments: + final_time {datetime} -- Final time + Returns: + str -- Google Cloud Logs Console URL to logs. + """ + hour_ago = final_time - timedelta(hours=1) + formatstring = "%Y-%m-%dT%H:%M:%SZ" + + url = ( + "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" + "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" + "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" + "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" + "&interval=PT1H&scrollTimestamp={timestamp_end}" + ).format( + project=environ.get("GCP_PROJECT"), + function_name=environ.get("FUNCTION_NAME"), + region=environ.get("FUNCTION_REGION"), + timestamp_end=final_time.strftime(formatstring), + timestamp_start=hour_ago.strftime(formatstring), + ) + + return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py new file mode 100644 index 0000000000..4be0f479bc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py @@ -0,0 +1,96 @@ +import re +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + +# function is everything between index at @ +# and then we match on the @ plus the hex val +FUNCTION_RE = r"[^@]+?" +HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" + +_FRAME_RE_PATTERN = r""" +^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ +""".format( + FUNCTION_RE=FUNCTION_RE, + HEX_ADDRESS=HEX_ADDRESS, +) + +FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) + + +class GnuBacktraceIntegration(Integration): + identifier = "gnu_backtrace" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + return _process_gnu_backtrace(event, hint) + + +def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": + if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception in values: + frames = exception.get("stacktrace", {}).get("frames", []) + if not frames: + continue + + msg = exception.get("value", None) + if not msg: + continue + + additional_frames = [] + new_msg = [] + + for line in msg.splitlines(): + match = FRAME_RE.match(line) + if match: + additional_frames.append( + ( + int(match.group("index")), + { + "package": match.group("package") or None, + "function": match.group("function") or None, + "platform": "native", + }, + ) + ) + else: + # Put garbage lines back into message, not sure what to do with them. + new_msg.append(line) + + if additional_frames: + additional_frames.sort(key=lambda x: -x[0]) + for _, frame in additional_frames: + frames.append(frame) + + new_msg.append("") + exception["value"] = "\n".join(new_msg) + + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py new file mode 100644 index 0000000000..45652c3f71 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py @@ -0,0 +1,457 @@ +from functools import wraps +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + List, +) + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import SPANSTATUS +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.genai.models import AsyncModels, Models +except ImportError: + raise DidNotEnable("google-genai not installed") + + +from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN +from .streaming import ( + accumulate_streaming_response, + set_span_data_for_streaming_response, +) +from .utils import ( + _capture_exception, + prepare_embed_content_args, + prepare_generate_content_args, + set_span_data_for_embed_request, + set_span_data_for_embed_response, + set_span_data_for_request, + set_span_data_for_response, +) + + +class GoogleGenAIIntegration(Integration): + identifier = IDENTIFIER + origin = ORIGIN + + def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Patch sync methods + Models.generate_content = _wrap_generate_content(Models.generate_content) + Models.generate_content_stream = _wrap_generate_content_stream( + Models.generate_content_stream + ) + Models.embed_content = _wrap_embed_content(Models.embed_content) + + # Patch async methods + AsyncModels.generate_content = _wrap_async_generate_content( + AsyncModels.generate_content + ) + AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( + AsyncModels.generate_content_stream + ) + AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) + + +def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = f(self, *args, **kwargs) + + # Create wrapper iterator to accumulate responses + def new_iterator() -> "Iterator[Any]": + chunks: "List[Any]" = [] + try: + for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_generate_content_stream + + +def _wrap_async_generate_content_stream( + f: "Callable[..., Any]", +) -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content_stream( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + _model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + chat_span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + else: + chat_span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) + chat_span.__enter__() + + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) + + try: + stream = await f(self, *args, **kwargs) + + # Create wrapper async iterator to accumulate responses + async def new_async_iterator() -> "AsyncIterator[Any]": + chunks: "List[Any]" = [] + try: + async for chunk in stream: + chunks.append(chunk) + yield chunk + except Exception as exc: + _capture_exception(exc) + if isinstance(chat_span, StreamedSpan): + chat_span.status = SpanStatus.ERROR + else: + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + finally: + # Accumulate all chunks and set final response data on spans + if chunks: + accumulated_response = accumulate_streaming_response(chunks) + set_span_data_for_streaming_response( + chat_span, integration, accumulated_response + ) + chat_span.__exit__(None, None, None) + + return new_async_iterator() + + except Exception as exc: + _capture_exception(exc) + chat_span.__exit__(None, None, None) + raise + + return new_async_generate_content_stream + + +def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_generate_content + + +def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_generate_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model, contents, model_name = prepare_generate_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as chat_span: + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.status = SpanStatus.ERROR + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=ORIGIN, + ) as chat_span: + chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_request( + chat_span, integration, model_name, contents, kwargs + ) + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_response(chat_span, integration, response) + + return response + + return new_async_generate_content + + +def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_embed_content + + +def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_async_embed_content( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(GoogleGenAIIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name, contents = prepare_embed_content_args(args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, + SPANDATA.GEN_AI_REQUEST_MODEL: model_name, + }, + ) as span: + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.status = SpanStatus.ERROR + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + else: + with get_start_span_function()( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}", + origin=ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + set_span_data_for_embed_request(span, integration, contents, kwargs) + + try: + response = await f(self, *args, **kwargs) + except Exception as exc: + _capture_exception(exc) + span.set_status(SPANSTATUS.INTERNAL_ERROR) + raise + + set_span_data_for_embed_response(span, integration, response) + + return response + + return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py new file mode 100644 index 0000000000..5b53ebf0e2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py @@ -0,0 +1,16 @@ +GEN_AI_SYSTEM = "gcp.gemini" + +# Mapping of tool attributes to their descriptions +# These are all tools that are available in the Google GenAI API +TOOL_ATTRIBUTES_MAP = { + "google_search_retrieval": "Google Search retrieval tool", + "google_search": "Google Search tool", + "retrieval": "Retrieval tool", + "enterprise_web_search": "Enterprise web search tool", + "google_maps": "Google Maps tool", + "code_execution": "Code execution tool", + "computer_use": "Computer use tool", +} + +IDENTIFIER = "google_genai" +ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py new file mode 100644 index 0000000000..8414ea4f21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py @@ -0,0 +1,172 @@ +from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union + +from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import ( + safe_serialize, +) + +from .utils import ( + UsageData, + extract_contents_text, + extract_finish_reasons, + extract_tool_calls, + extract_usage_data, +) + +if TYPE_CHECKING: + from google.genai.types import GenerateContentResponse + + from sentry_sdk.tracing import Span + + +class AccumulatedResponse(TypedDict): + id: "Optional[str]" + model: "Optional[str]" + text: str + finish_reasons: "List[str]" + tool_calls: "List[dict[str, Any]]" + usage_metadata: "Optional[UsageData]" + + +def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": + return UsageData( + input_tokens=max(self["input_tokens"], other["input_tokens"]), + output_tokens=max(self["output_tokens"], other["output_tokens"]), + input_tokens_cached=max( + self["input_tokens_cached"], other["input_tokens_cached"] + ), + output_tokens_reasoning=max( + self["output_tokens_reasoning"], other["output_tokens_reasoning"] + ), + total_tokens=max(self["total_tokens"], other["total_tokens"]), + ) + + +def accumulate_streaming_response( + chunks: "List[GenerateContentResponse]", +) -> "AccumulatedResponse": + """Accumulate streaming chunks into a single response-like object.""" + accumulated_text = [] + finish_reasons = [] + tool_calls = [] + usage_data = None + response_id = None + model = None + + for chunk in chunks: + # Extract text and tool calls + if getattr(chunk, "candidates", None): + for candidate in getattr(chunk, "candidates", []): + if hasattr(candidate, "content") and getattr( + candidate.content, "parts", [] + ): + extracted_text = extract_contents_text(candidate.content) + if extracted_text: + accumulated_text.append(extracted_text) + + extracted_finish_reasons = extract_finish_reasons(chunk) + if extracted_finish_reasons: + finish_reasons.extend(extracted_finish_reasons) + + extracted_tool_calls = extract_tool_calls(chunk) + if extracted_tool_calls: + tool_calls.extend(extracted_tool_calls) + + # Use last possible chunk, in case of interruption, and + # gracefully handle missing intermediate tokens by taking maximum + # with previous token reporting. + chunk_usage_data = extract_usage_data(chunk) + usage_data = ( + chunk_usage_data + if usage_data is None + else element_wise_usage_max(usage_data, chunk_usage_data) + ) + + accumulated_response = AccumulatedResponse( + text="".join(accumulated_text), + finish_reasons=finish_reasons, + tool_calls=tool_calls, + usage_metadata=usage_data, + id=response_id, + model=model, + ) + + return accumulated_response + + +def set_span_data_for_streaming_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + accumulated_response: "AccumulatedResponse", +) -> None: + """Set span data for accumulated streaming response.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if ( + should_send_default_pii() + and integration.include_prompts + and accumulated_response.get("text") + ): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TEXT, + safe_serialize([accumulated_response["text"]]), + ) + + if accumulated_response.get("finish_reasons"): + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + accumulated_response["finish_reasons"], + ) + + if accumulated_response.get("tool_calls"): + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(accumulated_response["tool_calls"]), + ) + + response_id = accumulated_response.get("id") + if response_id is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) + + response_model = accumulated_response.get("model") + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if accumulated_response["usage_metadata"] is None: + return + + if accumulated_response["usage_metadata"]["input_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + accumulated_response["usage_metadata"]["input_tokens"], + ) + + if accumulated_response["usage_metadata"]["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + accumulated_response["usage_metadata"]["input_tokens_cached"], + ) + + if accumulated_response["usage_metadata"]["output_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + accumulated_response["usage_metadata"]["output_tokens"], + ) + + if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + accumulated_response["usage_metadata"]["output_tokens_reasoning"], + ) + + if accumulated_response["usage_metadata"]["total_tokens"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + accumulated_response["usage_metadata"]["total_tokens"], + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py new file mode 100644 index 0000000000..464a812680 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py @@ -0,0 +1,1118 @@ +import copy +import inspect +import json +from functools import wraps +from itertools import chain +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + TypedDict, + Union, +) + +from google.genai.types import Content, GenerateContentConfig, Part, PartDict + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.utils import ( + get_modality_from_mime_type, + normalize_message_roles, + set_data_normalized, + transform_google_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + safe_serialize, +) + +from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP + +if TYPE_CHECKING: + from google.genai.types import ( + ContentListUnion, + ContentUnion, + ContentUnionDict, + EmbedContentResponse, + GenerateContentResponse, + Model, + Tool, + ) + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +_is_PIL_available = False +try: + from PIL import Image as PILImage # type: ignore[import-not-found] + + _is_PIL_available = True +except ImportError: + pass + +# Keys to use when checking to see if a dict provided by the user +# is Part-like (as opposed to a Content or multi-turn conversation entry). +_PART_DICT_KEYS = PartDict.__optional_keys__ + + +class UsageData(TypedDict): + """Structure for token usage data.""" + + input_tokens: int + input_tokens_cached: int + output_tokens: int + output_tokens_reasoning: int + total_tokens: int + + +def extract_usage_data( + response: "Union[GenerateContentResponse, dict[str, Any]]", +) -> "UsageData": + """Extract usage data from response into a structured format. + + Args: + response: The GenerateContentResponse object or dictionary containing usage metadata + + Returns: + UsageData: Dictionary with input_tokens, input_tokens_cached, + output_tokens, and output_tokens_reasoning fields + """ + usage_data = UsageData( + input_tokens=0, + input_tokens_cached=0, + output_tokens=0, + output_tokens_reasoning=0, + total_tokens=0, + ) + + # Handle dictionary response (from streaming) + if isinstance(response, dict): + usage = response.get("usage_metadata", {}) + if not usage: + return usage_data + + prompt_tokens = usage.get("prompt_token_count", 0) or 0 + tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + cached_tokens = usage.get("cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + candidates_tokens = usage.get("candidates_token_count", 0) or 0 + # python-genai reports output and reasoning tokens separately + # reasoning should be sub-category of output tokens + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = usage.get("total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + if not hasattr(response, "usage_metadata"): + return usage_data + + usage = response.usage_metadata + + # Input tokens include both prompt and tool use prompt tokens + prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 + tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 + usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens + + # Cached input tokens + cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 + usage_data["input_tokens_cached"] = cached_tokens + + # Reasoning tokens + reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 + usage_data["output_tokens_reasoning"] = reasoning_tokens + + # output_tokens = candidates_tokens + reasoning_tokens + # google-genai reports output and reasoning tokens separately + candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 + usage_data["output_tokens"] = candidates_tokens + reasoning_tokens + + total_tokens = getattr(usage, "total_token_count", 0) or 0 + usage_data["total_tokens"] = total_tokens + + return usage_data + + +def _capture_exception(exc: "Any") -> None: + """Capture exception with Google GenAI mechanism.""" + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "google_genai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def get_model_name(model: "Union[str, Model]") -> str: + """Extract model name from model parameter.""" + if isinstance(model, str): + return model + # Handle case where model might be an object with a name attribute + if hasattr(model, "name"): + return str(model.name) + return str(model) + + +def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": + """Extract messages from contents parameter which can have various formats. + + Returns a list of message dictionaries in the format: + - System: {"role": "system", "content": "string"} + - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} + """ + if contents is None: + return [] + + messages = [] + + # Handle string case + if isinstance(contents, str): + return [{"role": "user", "content": contents}] + + # Handle list case + if isinstance(contents, list): + if contents and all(_is_part_like(item) for item in contents): + # All items are parts — merge into a single multi-part user message + content_parts = [] + for item in contents: + part = _extract_part_from_item(item) + if part is not None: + content_parts.append(part) + + return [{"role": "user", "content": content_parts}] + else: + # Multi-turn conversation or mixed content types + for item in contents: + item_messages = extract_contents_messages(item) + messages.extend(item_messages) + return messages + + # Handle dictionary case (ContentDict) + if isinstance(contents, dict): + role = contents.get("role", "user") + parts = contents.get("parts") + + if parts: + content_parts = [] + tool_messages = [] + + for part in parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + # Tool message - add separately + tool_messages.append(part_result) + else: + # Regular content part + content_parts.append(part_result) + + # Add main message if we have content parts + if content_parts: + # Normalize role: "model" -> "assistant" + normalized_role = "assistant" if role == "model" else role or "user" + messages.append({"role": normalized_role, "content": content_parts}) + + # Add tool messages + messages.extend(tool_messages) + elif "text" in contents: + messages.append( + { + "role": role, + "content": [{"text": contents["text"], "type": "text"}], + } + ) + elif "inline_data" in contents: + # The "data" will always be bytes (or bytes within a string), + # so if this is present, it's safe to automatically substitute with the placeholder + messages.append( + { + "inline_data": { + "mime_type": contents["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + ) + + return messages + + # Handle Content object + if hasattr(contents, "parts") and contents.parts: + role = getattr(contents, "role", None) or "user" + content_parts = [] + tool_messages = [] + + for part in contents.parts: + part_result = _extract_part_content(part) + if part_result is None: + continue + + if isinstance(part_result, dict) and part_result.get("role") == "tool": + tool_messages.append(part_result) + else: + content_parts.append(part_result) + + if content_parts: + normalized_role = "assistant" if role == "model" else role + messages.append({"role": normalized_role, "content": content_parts}) + + messages.extend(tool_messages) + return messages + + # Handle Part object directly + part_result = _extract_part_content(contents) + if part_result: + if isinstance(part_result, dict) and part_result.get("role") == "tool": + return [part_result] + else: + return [{"role": "user", "content": [part_result]}] + + # Handle PIL.Image.Image + if _is_PIL_available and isinstance(contents, PILImage.Image): + blob_part = _extract_pil_image(contents) + if blob_part: + return [{"role": "user", "content": [blob_part]}] + + # Handle File object + if hasattr(contents, "uri") and hasattr(contents, "mime_type"): + # File object + file_uri = getattr(contents, "uri", None) + mime_type = getattr(contents, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing + if file_uri is not None: + # Default to empty string if mime_type is None + if mime_type is None: + mime_type = "" + + blob_part = { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + return [{"role": "user", "content": [blob_part]}] + + # Handle direct text attribute + if hasattr(contents, "text") and contents.text: + return [ + {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} + ] + + return [] + + +def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": + """Extract content from a Part object or dict. + + Returns: + - dict for content part (text/blob) or tool message + - None if part should be skipped + """ + if part is None: + return None + + # Handle dict Part + if isinstance(part, dict): + # Check for function_response first (tool message) + if "function_response" in part: + return _extract_tool_message_from_part(part) + + if part.get("text"): + return {"text": part["text"], "type": "text"} + + # Try using Google-specific transform for dict formats (inline_data, file_data) + result = transform_google_content_part(part) + if result is not None: + # For inline_data with bytes data, substitute the content + if "inline_data" in part: + # inline_data.data will always be bytes, or a string containing base64-encoded bytes, + # so can automatically substitute without further checks + result["content"] = BLOB_DATA_SUBSTITUTE + return result + + return None + + # Handle Part object + # Check for function_response (tool message) + if hasattr(part, "function_response") and part.function_response: + return _extract_tool_message_from_part(part) + + # Handle text + if hasattr(part, "text") and part.text: + return {"text": part.text, "type": "text"} + + # Handle file_data + if hasattr(part, "file_data") and part.file_data: + file_data = part.file_data + file_uri = getattr(file_data, "file_uri", None) + mime_type = getattr(file_data, "mime_type", None) + # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) + if file_uri is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + # Handle inline_data + if hasattr(part, "inline_data") and part.inline_data: + inline_data = part.inline_data + data = getattr(inline_data, "data", None) + mime_type = getattr(inline_data, "mime_type", None) + # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) + if data is not None: + # Default to empty string if mime_type is None (consistent with transform_google_content_part) + if mime_type is None: + mime_type = "" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + return None + + +def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": + """Extract tool message from a Part with function_response. + + Returns: + {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} + or None if not a valid tool message + """ + function_response = None + + if isinstance(part, dict): + function_response = part.get("function_response") + elif hasattr(part, "function_response"): + function_response = part.function_response + + if not function_response: + return None + + # Extract fields from function_response + tool_call_id = None + tool_name = None + output = None + + if isinstance(function_response, dict): + tool_call_id = function_response.get("id") + tool_name = function_response.get("name") + response_dict = function_response.get("response", {}) + # Prefer "output" key if present, otherwise use entire response + output = response_dict.get("output", response_dict) + else: + # FunctionResponse object + tool_call_id = getattr(function_response, "id", None) + tool_name = getattr(function_response, "name", None) + response_obj = getattr(function_response, "response", None) + if response_obj is None: + response_obj = {} + if isinstance(response_obj, dict): + output = response_obj.get("output", response_obj) + else: + output = response_obj + + if not tool_name: + return None + + return { + "role": "tool", + "content": { + "toolCallId": str(tool_call_id) if tool_call_id else None, + "toolName": str(tool_name), + "output": safe_serialize(output) if output is not None else None, + }, + } + + +def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": + """Extract blob part from PIL.Image.Image.""" + if not _is_PIL_available or not isinstance(image, PILImage.Image): + return None + + # Get format, default to JPEG + format_str = image.format or "JPEG" + suffix = format_str.lower() + mime_type = f"image/{suffix}" + + return { + "type": "blob", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _is_part_like(item: "Any") -> bool: + """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" + if isinstance(item, (str, Part)): + return True + if isinstance(item, (list, Content)): + return False + if isinstance(item, dict): + if "role" in item or "parts" in item: + return False + # Part objects that came in as plain dicts + return bool(_PART_DICT_KEYS & item.keys()) + # File objects + if hasattr(item, "uri"): + return True + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return True + return False + + +def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": + """Convert a single part-like item to a content part dict.""" + if isinstance(item, str): + return {"text": item, "type": "text"} + + # Handle bare inline_data dicts directly to preserve the raw format + if isinstance(item, dict) and "inline_data" in item: + return { + "inline_data": { + "mime_type": item["inline_data"].get("mime_type", ""), + "data": BLOB_DATA_SUBSTITUTE, + } + } + + # For other dicts and Part objects, use existing _extract_part_content + result = _extract_part_content(item) + if result is not None: + return result + + # PIL.Image + if _is_PIL_available and isinstance(item, PILImage.Image): + return _extract_pil_image(item) + + # File objects + if hasattr(item, "uri") and hasattr(item, "mime_type"): + file_uri = getattr(item, "uri", None) + mime_type = getattr(item, "mime_type", None) or "" + if file_uri is not None: + return { + "type": "uri", + "modality": get_modality_from_mime_type(mime_type), + "mime_type": mime_type, + "uri": file_uri, + } + + return None + + +def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": + """Extract text from contents parameter which can have various formats. + + This is a compatibility function that extracts text from messages. + For new code, use extract_contents_messages instead. + """ + messages = extract_contents_messages(contents) + if not messages: + return None + + texts = [] + for message in messages: + content = message.get("content") + if isinstance(content, str): + texts.append(content) + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get("type") == "text": + texts.append(part.get("text", "")) + + return " ".join(texts) if texts else None + + +def _format_tools_for_span( + tools: "Iterable[Tool | Callable[..., Any]]", +) -> "Optional[List[dict[str, Any]]]": + """Format tools parameter for span data.""" + formatted_tools = [] + for tool in tools: + if callable(tool): + # Handle callable functions passed directly + formatted_tools.append( + { + "name": getattr(tool, "__name__", "unknown"), + "description": getattr(tool, "__doc__", None), + } + ) + elif ( + hasattr(tool, "function_declarations") + and tool.function_declarations is not None + ): + # Tool object with function declarations + for func_decl in tool.function_declarations: + formatted_tools.append( + { + "name": getattr(func_decl, "name", None), + "description": getattr(func_decl, "description", None), + } + ) + else: + # Check for predefined tool attributes - each of these tools + # is an attribute of the tool object, by default set to None + for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): + if getattr(tool, attr_name, None): + formatted_tools.append( + { + "name": attr_name, + "description": description, + } + ) + break + + return formatted_tools if formatted_tools else None + + +def extract_tool_calls( + response: "GenerateContentResponse", +) -> "Optional[List[dict[str, Any]]]": + """Extract tool/function calls from response candidates and automatic function calling history.""" + + tool_calls = [] + + # Extract from candidates, sometimes tool calls are nested under the content.parts object + if getattr(response, "candidates", []): + for candidate in response.candidates: + if not hasattr(candidate, "content") or not getattr( + candidate.content, "parts", [] + ): + continue + + for part in candidate.content.parts: + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if getattr(function_call, "args", None): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + # Extract from automatic_function_calling_history + # This is the history of tool calls made by the model + if getattr(response, "automatic_function_calling_history", None): + for content in response.automatic_function_calling_history: + if not getattr(content, "parts", None): + continue + + for part in getattr(content, "parts", []): + if getattr(part, "function_call", None): + function_call = part.function_call + tool_call = { + "name": getattr(function_call, "name", None), + "type": "function_call", + } + + # Extract arguments if available + if hasattr(function_call, "args"): + tool_call["arguments"] = safe_serialize(function_call.args) + + tool_calls.append(tool_call) + + return tool_calls if tool_calls else None + + +def _capture_tool_input( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" +) -> "dict[str, Any]": + """Capture tool input from args and kwargs.""" + tool_input = kwargs.copy() if kwargs else {} + + # If we have positional args, try to map them to the function signature + if args: + try: + sig = inspect.signature(tool) + param_names = list(sig.parameters.keys()) + for i, arg in enumerate(args): + if i < len(param_names): + tool_input[param_names[i]] = arg + except Exception: + # Fallback if we can't get the signature + tool_input["args"] = args + + return tool_input + + +def _create_tool_span( + tool_name: str, tool_doc: "Optional[str]" +) -> "Union[Span, StreamedSpan]": + """Create a span for tool execution.""" + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": ORIGIN, + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + if tool_doc: + span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=ORIGIN, + ) + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + if tool_doc: + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) + return span + + +def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": + """Wrap a tool to emit execute_tool spans when called.""" + if not callable(tool): + # Not a callable function, return as-is (predefined tools) + return tool + + tool_name = getattr(tool, "__name__", "unknown") + tool_doc = tool.__doc__ + + if inspect.iscoroutinefunction(tool): + # Async function + @wraps(tool) + async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = await tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return async_wrapped + else: + # Sync function + @wraps(tool) + def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": + with _create_tool_span(tool_name, tool_doc) as span: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + # Capture tool input + tool_input = _capture_tool_input(args, kwargs, tool) + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) + + try: + result = tool(*args, **kwargs) + + # Capture tool output + with capture_internal_exceptions(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + + return result + except Exception as exc: + _capture_exception(exc) + raise + + return sync_wrapped + + +def wrapped_config_with_tools( + config: "GenerateContentConfig", +) -> "GenerateContentConfig": + """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as + callable functions as a part of the config object.""" + + if not config or not getattr(config, "tools", None): + return config + + result = copy.copy(config) + result.tools = [wrapped_tool(tool) for tool in config.tools] + + return result + + +def _extract_response_text( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract text from response candidates.""" + + if not response or not getattr(response, "candidates", []): + return None + + texts = [] + for candidate in response.candidates: + if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): + continue + + if candidate.content is None or candidate.content.parts is None: + continue + + for part in candidate.content.parts: + if getattr(part, "text", None): + texts.append(part.text) + + return texts if texts else None + + +def extract_finish_reasons( + response: "GenerateContentResponse", +) -> "Optional[List[str]]": + """Extract finish reasons from response candidates.""" + if not response or not getattr(response, "candidates", []): + return None + + finish_reasons = [] + for candidate in response.candidates: + if getattr(candidate, "finish_reason", None): + # Convert enum value to string if necessary + reason = str(candidate.finish_reason) + # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") + if "." in reason: + reason = reason.split(".")[-1] + finish_reasons.append(reason) + + return finish_reasons if finish_reasons else None + + +def _transform_system_instruction_one_level( + system_instructions: "Union[ContentUnionDict, ContentUnion]", + can_be_content: bool, +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, str): + return [{"type": "text", "content": system_instructions}] + + if isinstance(system_instructions, Part) and system_instructions.text: + return [{"type": "text", "content": system_instructions.text}] + + if can_be_content and isinstance(system_instructions, Content): + if isinstance(system_instructions.parts, list): + for part in system_instructions.parts: + if isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + return text_parts + + if isinstance(system_instructions, dict) and system_instructions.get("text"): + return [{"type": "text", "content": system_instructions["text"]}] + + elif can_be_content and isinstance(system_instructions, dict): + parts = system_instructions.get("parts", []) + for part in parts: + if isinstance(part, Part) and isinstance(part.text, str): + text_parts.append({"type": "text", "content": part.text}) + elif isinstance(part, dict) and isinstance(part.get("text"), str): + text_parts.append({"type": "text", "content": part["text"]}) + return text_parts + + return text_parts + + +def _transform_system_instructions( + system_instructions: "Union[ContentUnionDict, ContentUnion]", +) -> "list[TextPart]": + text_parts: "list[TextPart]" = [] + + if isinstance(system_instructions, list): + text_parts = list( + chain.from_iterable( + _transform_system_instruction_one_level( + instructions, can_be_content=False + ) + for instructions in system_instructions + ) + ) + + return text_parts + + return _transform_system_instruction_one_level( + system_instructions, can_be_content=True + ) + + +def set_span_data_for_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + model: str, + contents: "ContentListUnion", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for the request.""" + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if kwargs.get("stream", False): + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + config: "Optional[GenerateContentConfig]" = kwargs.get("config") + + # Set input messages/prompts if PII is allowed + if should_send_default_pii() and integration.include_prompts: + messages = [] + + # Add system instruction if present + system_instructions = None + if config and hasattr(config, "system_instruction"): + system_instructions = config.system_instruction + elif isinstance(config, dict) and "system_instruction" in config: + system_instructions = config.get("system_instruction") + + if system_instructions is not None: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + # Extract messages from contents + contents_messages = extract_contents_messages(contents) + messages.extend(contents_messages) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Extract parameters directly from config (not nested under generation_config) + for param, span_key in [ + ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), + ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), + ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), + ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), + ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), + ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), + ("seed", SPANDATA.GEN_AI_REQUEST_SEED), + ]: + if hasattr(config, param): + value = getattr(config, param) + if value is not None: + set_on_span(span_key, value) + + # Set tools if available + if config is not None and hasattr(config, "tools"): + tools = config.tools + if tools: + formatted_tools = _format_tools_for_span(tools) + if formatted_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + formatted_tools, + unpack=False, + ) + + +def set_span_data_for_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "GenerateContentResponse", +) -> None: + """Set span data for the response.""" + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + if should_send_default_pii() and integration.include_prompts: + response_texts = _extract_response_text(response) + if response_texts: + # Format as JSON string array as per documentation + set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) + + tool_calls = extract_tool_calls(response) + if tool_calls: + # Tool calls should be JSON serialized + set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) + + finish_reasons = extract_finish_reasons(response) + if finish_reasons: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + + if getattr(response, "response_id", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) + + if getattr(response, "model_version", None): + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) + + usage_data = extract_usage_data(response) + + if usage_data["input_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) + + if usage_data["input_tokens_cached"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage_data["input_tokens_cached"], + ) + + if usage_data["output_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) + + if usage_data["output_tokens_reasoning"]: + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage_data["output_tokens_reasoning"], + ) + + if usage_data["total_tokens"]: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) + + +def prepare_generate_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[Any, Any, str]": + """Extract and prepare common arguments for generate_content methods.""" + model = args[0] if args else kwargs.get("model", "unknown") + contents = args[1] if len(args) > 1 else kwargs.get("contents") + model_name = get_model_name(model) + + config = kwargs.get("config") + wrapped_config = wrapped_config_with_tools(config) + if wrapped_config is not config: + kwargs["config"] = wrapped_config + + return model, contents, model_name + + +def prepare_embed_content_args( + args: "tuple[Any, ...]", kwargs: "dict[str, Any]" +) -> "tuple[str, Any]": + """Extract and prepare common arguments for embed_content methods. + + Returns: + tuple: (model_name, contents) + """ + model = kwargs.get("model", "unknown") + contents = kwargs.get("contents") + model_name = get_model_name(model) + + return model_name, contents + + +def set_span_data_for_embed_request( + span: "Union[Span, StreamedSpan]", + integration: "Any", + contents: "Any", + kwargs: "dict[str, Any]", +) -> None: + """Set span data for embedding request.""" + # Include input contents if PII is allowed + if should_send_default_pii() and integration.include_prompts: + if contents: + # For embeddings, contents is typically a list of strings/texts + input_texts = [] + + # Handle various content formats + if isinstance(contents, str): + input_texts = [contents] + elif isinstance(contents, list): + for item in contents: + text = extract_contents_text(item) + if text: + input_texts.append(text) + else: + text = extract_contents_text(contents) + if text: + input_texts = [text] + + if input_texts: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + input_texts, + unpack=False, + ) + + +def set_span_data_for_embed_response( + span: "Union[Span, StreamedSpan]", + integration: "Any", + response: "EmbedContentResponse", +) -> None: + """Set span data for embedding response.""" + if not response: + return + + # Extract token counts from embeddings statistics (Vertex AI only) + # Each embedding has its own statistics with token_count + if hasattr(response, "embeddings") and response.embeddings: + total_tokens = 0 + + for embedding in response.embeddings: + if hasattr(embedding, "statistics") and embedding.statistics: + token_count = getattr(embedding.statistics, "token_count", None) + if token_count is not None: + total_tokens += int(token_count) + + # Set token count if we found any + if total_tokens > 0: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) + else: + span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py new file mode 100644 index 0000000000..dcb60e9561 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py @@ -0,0 +1,173 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + parse_version, +) + +try: + import gql # type: ignore[import-not-found] + from gql.transport import ( # type: ignore[import-not-found] + AsyncTransport, + Transport, + ) + from gql.transport.exceptions import ( # type: ignore[import-not-found] + TransportQueryError, + ) + from graphql import ( + DocumentNode, + VariableDefinitionNode, + get_operation_ast, + print_ast, + ) + + try: + # gql 4.0+ + from gql import GraphQLRequest + except ImportError: + GraphQLRequest = None + +except ImportError: + raise DidNotEnable("gql is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Tuple, Union + + from sentry_sdk._types import Event, EventProcessor + + EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] + + +class GQLIntegration(Integration): + identifier = "gql" + + @staticmethod + def setup_once() -> None: + gql_version = parse_version(gql.__version__) + _check_minimum_version(GQLIntegration, gql_version) + + _patch_execute() + + +def _data_from_document(document: "DocumentNode") -> "EventDataType": + try: + operation_ast = get_operation_ast(document) + data: "EventDataType" = {"query": print_ast(document)} + + if operation_ast is not None: + data["variables"] = operation_ast.variable_definitions + if operation_ast.name is not None: + data["operationName"] = operation_ast.name.value + + return data + except (AttributeError, TypeError): + return dict() + + +def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: + """ + The RequestsHTTPTransport allows defining the HTTP method; all + other transports use POST. + """ + try: + return transport.method + except AttributeError: + return "POST" + + +def _request_info_from_transport( + transport: "Union[Transport, AsyncTransport, None]", +) -> "Dict[str, str]": + if transport is None: + return {} + + request_info = { + "method": _transport_method(transport), + } + + try: + request_info["url"] = transport.url + except AttributeError: + pass + + return request_info + + +def _patch_execute() -> None: + real_execute = gql.Client.execute + + # Maintain signature for backwards compatibility. + # gql.Client.execute() accepts a positional-only "request" + # parameter with version 4.0.0. + @ensure_integration_enabled(GQLIntegration, real_execute) + def sentry_patched_execute( + self: "gql.Client", + document: "DocumentNode", + *args: "Any", + **kwargs: "Any", + ) -> "Any": + scope = sentry_sdk.get_isolation_scope() + # document is a gql.GraphQLRequest with gql v4.0.0. + scope.add_event_processor(_make_gql_event_processor(self, document)) + + try: + # document is a gql.GraphQLRequest with gql v4.0.0. + return real_execute(self, document, *args, **kwargs) + except TransportQueryError as e: + event, hint = event_from_exception( + e, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "gql", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint) + raise e + + gql.Client.execute = sentry_patched_execute + + +def _make_gql_event_processor( + client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" +) -> "EventProcessor": + def processor(event: "Event", hint: "dict[str, Any]") -> "Event": + try: + errors = hint["exc_info"][1].errors + except (AttributeError, KeyError): + errors = None + + request = event.setdefault("request", {}) + request.update( + { + "api_target": "graphql", + **_request_info_from_transport(client.transport), + } + ) + + if should_send_default_pii(): + if GraphQLRequest is not None and isinstance( + document_or_request, GraphQLRequest + ): + # In v4.0.0, gql moved to using GraphQLRequest instead of + # DocumentNode in execute + # https://github.com/graphql-python/gql/pull/556 + document = document_or_request.document + else: + document = document_or_request + + request["data"] = _data_from_document(document) + contexts = event.setdefault("contexts", {}) + response = contexts.setdefault("response", {}) + response.update( + { + "data": {"errors": errors}, + "type": response, + } + ) + + return event + + return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py new file mode 100644 index 0000000000..4938a5c0f3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py @@ -0,0 +1,181 @@ +from contextlib import contextmanager + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from graphene.types import schema as graphene_schema # type: ignore +except ImportError: + raise DidNotEnable("graphene is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Any, Dict, Union + + from graphene.language.source import Source # type: ignore + from graphql.execution import ExecutionResult + from graphql.type import GraphQLSchema + + from sentry_sdk._types import Event + + +class GrapheneIntegration(Integration): + identifier = "graphene" + + @staticmethod + def setup_once() -> None: + version = package_version("graphene") + _check_minimum_version(GrapheneIntegration, version) + + _patch_graphql() + + +def _patch_graphql() -> None: + old_graphql_sync = graphene_schema.graphql_sync + old_graphql_async = graphene_schema.graphql + + @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) + def _sentry_patched_graphql_sync( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = old_graphql_sync(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + async def _sentry_patched_graphql_async( + schema: "GraphQLSchema", + source: "Union[str, Source]", + *args: "Any", + **kwargs: "Any", + ) -> "ExecutionResult": + integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) + if integration is None: + return await old_graphql_async(schema, source, *args, **kwargs) + + scope = sentry_sdk.get_isolation_scope() + scope.add_event_processor(_event_processor) + + with graphql_span(schema, source, kwargs): + result = await old_graphql_async(schema, source, *args, **kwargs) + + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + for error in result.errors or []: + event, hint = event_from_exception( + error, + client_options=client.options, + mechanism={ + "type": GrapheneIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + return result + + graphene_schema.graphql_sync = _sentry_patched_graphql_sync + graphene_schema.graphql = _sentry_patched_graphql_async + + +def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + if should_send_default_pii(): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + + elif event.get("request", {}).get("data"): + del event["request"]["data"] + + return event + + +@contextmanager +def graphql_span( + schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" +) -> "Generator[None, None, None]": + operation_name = kwargs.get("operation_name") or "" + + operation_type = "query" + op = OP.GRAPHQL_QUERY + if source.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif source.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + sentry_sdk.add_breadcrumb( + crumb={ + "data": { + "operation_name": operation_name, + "operation_type": operation_type, + }, + "category": "graphql.operation", + }, + ) + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + additional_attributes = {} + if should_send_default_pii(): + additional_attributes["graphql.document"] = source + + _graphql_span = sentry_sdk.traces.start_span( + name=operation_name, + attributes={ + "sentry.op": op, + "graphql.operation.name": operation_name, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) + + if should_send_default_pii(): + _graphql_span.set_data("graphql.document", source) + _graphql_span.set_data("graphql.operation.name", operation_name) + _graphql_span.set_data("graphql.operation.type", operation_type) + + _graphql_span.__enter__() + + try: + yield + finally: + if is_span_streaming_enabled: + _graphql_span.end() # type: ignore + else: + _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py new file mode 100644 index 0000000000..bf74ff1351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py @@ -0,0 +1,188 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any, Optional, Sequence + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .client import ClientInterceptor +from .server import ServerInterceptor + +try: + import grpc + from grpc import Channel, Server, intercept_channel + from grpc.aio import Channel as AsyncChannel + from grpc.aio import Server as AsyncServer + + from .aio.client import ( + SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, + ) + from .aio.client import ( + SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, + ) + from .aio.server import ServerInterceptor as AsyncServerInterceptor +except ImportError: + raise DidNotEnable("grpcio is not installed.") + +# Hack to get new Python features working in older versions +# without introducing a hard dependency on `typing_extensions` +# from: https://stackoverflow.com/a/71944042/300572 +if TYPE_CHECKING: + from typing import Callable, ParamSpec +else: + # Fake ParamSpec + class ParamSpec: + def __init__(self, _): + self.args = None + self.kwargs = None + + # Callable[anything] will return None + class _Callable: + def __getitem__(self, _): + return None + + # Make instances + Callable = _Callable() + +P = ParamSpec("P") + +GRPC_VERSION = parse_version(grpc.__version__) + + +def _is_channel_intercepted(channel: "Channel") -> bool: + interceptor = getattr(channel, "_interceptor", None) + while interceptor is not None: + if isinstance(interceptor, ClientInterceptor): + return True + + inner_channel = getattr(channel, "_channel", None) + if inner_channel is None: + return False + + channel = inner_channel + interceptor = getattr(channel, "_interceptor", None) + + return False + + +def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + "Wrapper for synchronous secure and insecure channel." + + @wraps(func) + def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": + channel = func(*args, **kwargs) + if not _is_channel_intercepted(channel): + return intercept_channel(channel, ClientInterceptor()) + else: + return channel + + return patched_channel + + +def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": + @wraps(func) + def patched_intercept_channel( + channel: "Channel", *interceptors: "grpc.ServerInterceptor" + ) -> "Channel": + if _is_channel_intercepted(channel): + interceptors = tuple( + [ + interceptor + for interceptor in interceptors + if not isinstance(interceptor, ClientInterceptor) + ] + ) + else: + interceptors = interceptors + return intercept_channel(channel, *interceptors) + + return patched_intercept_channel # type: ignore + + +def _wrap_channel_async( + func: "Callable[P, AsyncChannel]", +) -> "Callable[P, AsyncChannel]": + "Wrapper for asynchronous secure and insecure channel." + + @wraps(func) + def patched_channel( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Channel": + sentry_interceptors = [ + AsyncUnaryUnaryClientInterceptor(), + AsyncUnaryStreamClientIntercetor(), + ] + interceptors = [*sentry_interceptors, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_channel # type: ignore + + +def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": + """Wrapper for synchronous server.""" + + @wraps(func) + def patched_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + interceptors = [ + interceptor + for interceptor in interceptors or [] + if not isinstance(interceptor, ServerInterceptor) + ] + server_interceptor = ServerInterceptor() + interceptors = [server_interceptor, *(interceptors or [])] + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_server # type: ignore + + +def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": + """Wrapper for asynchronous server.""" + + @wraps(func) + def patched_aio_server( # type: ignore + *args: "P.args", + interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, + **kwargs: "P.kwargs", + ) -> "Server": + server_interceptor = AsyncServerInterceptor() + interceptors = [ + server_interceptor, + *(interceptors or []), + ] + + try: + # We prefer interceptors as a list because of compatibility with + # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 + # However, prior to grpc 1.42.0, only tuples were accepted, so we + # have no choice there. + if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): + interceptors = tuple(interceptors) + except Exception: + pass + + return func(*args, interceptors=interceptors, **kwargs) # type: ignore + + return patched_aio_server # type: ignore + + +class GRPCIntegration(Integration): + identifier = "grpc" + + @staticmethod + def setup_once() -> None: + import grpc + + grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) + grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) + grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) + + grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) + grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) + + grpc.server = _wrap_sync_server(grpc.server) + grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py new file mode 100644 index 0000000000..4d21815254 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py @@ -0,0 +1,7 @@ +from .client import ClientInterceptor +from .server import ServerInterceptor + +__all__ = [ + "ClientInterceptor", + "ServerInterceptor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py new file mode 100644 index 0000000000..d07b7f19be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py @@ -0,0 +1,146 @@ +from typing import Any, AsyncIterable, Callable, Union + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +try: + from google.protobuf.message import Message + from grpc.aio import ( + ClientCallDetails, + Metadata, + UnaryStreamCall, + UnaryStreamClientInterceptor, + UnaryUnaryCall, + UnaryUnaryClientInterceptor, + ) +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor: + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + if client_call_details.metadata is None: + client_call_details = client_call_details._replace(metadata=Metadata()) + elif not isinstance(client_call_details.metadata, Metadata): + # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 + # See https://github.com/grpc/grpc/issues/34298. + client_call_details = client_call_details._replace( + metadata=Metadata.from_tuple(client_call_details.metadata) + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + client_call_details.metadata.add(key, value) + return client_call_details + + +class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore + async def intercept_unary_unary( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[UnaryUnaryCall, Message]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + status_code = await response.code() + span.set_data("code", status_code.name) + + return response + + +class SentryUnaryStreamClientInterceptor( + ClientInterceptor, + UnaryStreamClientInterceptor, # type: ignore +): + async def intercept_unary_stream( + self, + continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method.decode(), + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method.decode(), + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method.decode(), + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = await continuation(client_call_details, request) + # status_code = await response.code() + # span.set_data("code", status_code) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py new file mode 100644 index 0000000000..010337e98c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py @@ -0,0 +1,134 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from typing import Any, Optional + + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler + from grpc.aio import AbortError, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Callable[[ServicerContext], str] | None" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + async def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", + handler_call_details: "HandlerCallDetails", + ) -> "Optional[Awaitable[RpcMethodHandler]]": + handler = await continuation(handler_call_details) + if handler is None: + return None + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + if not handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.unary_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + with sentry_sdk.isolation_scope(): + name = ( + custom_find_name(context) if custom_find_name else method_name + ) + if not name: + return await handler(request, context) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + # What if the headers are empty? + sentry_sdk.traces.continue_trace( + dict(context.invocation_metadata()) + ) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + else: + # What if the headers are empty? + transaction = sentry_sdk.continue_trace( + dict(context.invocation_metadata()), + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return await handler.unary_unary(request, context) + except AbortError: + raise + except Exception as exc: + event, hint = event_from_exception( + exc, + mechanism={"type": "grpc", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + raise + + elif not handler.request_streaming and handler.response_streaming: + handler_factory = grpc.unary_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.unary_stream(request, context): + yield r + + elif handler.request_streaming and not handler.response_streaming: + handler_factory = grpc.stream_unary_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": + response = handler.stream_unary(request, context) + return await response + + elif handler.request_streaming and handler.response_streaming: + handler_factory = grpc.stream_stream_rpc_method_handler + + async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore + async for r in handler.stream_stream(request, context): + yield r + + return handler_factory( + wrapped, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py new file mode 100644 index 0000000000..5384a0a78f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py @@ -0,0 +1,149 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Iterator, Union + +try: + import grpc + from google.protobuf.message import Message + from grpc import Call, ClientCallDetails + from grpc._interceptor import _UnaryOutcome + from grpc.aio._interceptor import UnaryStreamCall +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ClientInterceptor( + grpc.UnaryUnaryClientInterceptor, # type: ignore + grpc.UnaryStreamClientInterceptor, # type: ignore +): + def intercept_unary_unary( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "_UnaryOutcome": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary unary call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_attribute( + SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name + ) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary unary call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary unary") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + span.set_data("code", response.code().name) + + return response + + def intercept_unary_stream( + self: "ClientInterceptor", + continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", + client_call_details: "ClientCallDetails", + request: "Message", + ) -> "Union[Iterator[Message], Call]": + method = client_call_details.method + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + response: "UnaryStreamCall" + if span_streaming: + with sentry_sdk.traces.start_span( + name="unary stream call to %s" % method, + attributes={ + "sentry.op": OP.GRPC_CLIENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.RPC_METHOD: method, + }, + ) as span: + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + else: + with sentry_sdk.start_span( + op=OP.GRPC_CLIENT, + name="unary stream call to %s" % method, + origin=SPAN_ORIGIN, + ) as span: + span.set_data("type", "unary stream") + span.set_data("method", method) + + client_call_details = ( + self._update_client_call_details_metadata_from_scope( + client_call_details + ) + ) + + response = continuation(client_call_details, request) + # Setting code on unary-stream leads to execution getting stuck + # span.set_data("code", response.code().name) + + return response + + @staticmethod + def _update_client_call_details_metadata_from_scope( + client_call_details: "ClientCallDetails", + ) -> "ClientCallDetails": + metadata = ( + list(client_call_details.metadata) if client_call_details.metadata else [] + ) + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + metadata.append((key, value)) + + client_call_details = grpc._interceptor._ClientCallDetails( + method=client_call_details.method, + timeout=client_call_details.timeout, + metadata=metadata, + credentials=client_call_details.credentials, + wait_for_ready=client_call_details.wait_for_ready, + compression=client_call_details.compression, + ) + + return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py new file mode 100644 index 0000000000..9fdb975caf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py new file mode 100644 index 0000000000..1cba1d4b85 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py @@ -0,0 +1,91 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Callable, Optional + + from google.protobuf.message import Message + +try: + import grpc + from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext +except ImportError: + raise DidNotEnable("grpcio is not installed") + + +class ServerInterceptor(grpc.ServerInterceptor): # type: ignore + def __init__( + self: "ServerInterceptor", + find_name: "Optional[Callable[[ServicerContext], str]]" = None, + ) -> None: + self._custom_find_name = find_name + + super().__init__() + + def intercept_service( + self: "ServerInterceptor", + continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", + handler_call_details: "HandlerCallDetails", + ) -> "RpcMethodHandler": + handler = continuation(handler_call_details) + if not handler or not handler.unary_unary: + return handler + + method_name = handler_call_details.method + custom_find_name = self._custom_find_name + + def behavior(request: "Message", context: "ServicerContext") -> "Message": + with sentry_sdk.isolation_scope(): + name = custom_find_name(context) if custom_find_name else method_name + + if name: + metadata = dict(context.invocation_metadata()) + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(metadata) + + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.GRPC_SERVER, + "sentry.span.source": SegmentSource.CUSTOM.value, + "sentry.origin": SPAN_ORIGIN, + }, + parent_span=None, + ): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + transaction = sentry_sdk.continue_trace( + metadata, + op=OP.GRPC_SERVER, + name=name, + source=TransactionSource.CUSTOM, + origin=SPAN_ORIGIN, + ) + + with sentry_sdk.start_transaction(transaction=transaction): + try: + return handler.unary_unary(request, context) + except BaseException as e: + raise e + else: + return handler.unary_unary(request, context) + + return grpc.unary_unary_rpc_method_handler( + behavior, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py new file mode 100644 index 0000000000..a68f20b299 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx is not installed") + +__all__ = ["HttpxIntegration"] + + +class HttpxIntegration(Integration): + identifier = "httpx" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx_client() + _install_httpx_async_client() + + +def _install_httpx_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(HttpxIntegration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(HttpxIntegration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": HttpxIntegration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=HttpxIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py new file mode 100644 index 0000000000..25062aaa11 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py @@ -0,0 +1,263 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + logger, + parse_url, +) + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Attributes + + +try: + from httpx2 import AsyncClient, Client, Request, Response +except ImportError: + raise DidNotEnable("httpx2 is not installed") + +__all__ = ["Httpx2Integration"] + + +class Httpx2Integration(Integration): + identifier = "httpx2" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + httpx2 has its own transport layer and can be customized when needed, + so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. + """ + _install_httpx2_client() + _install_httpx2_async_client() + + +def _install_httpx2_client() -> None: + real_send = Client.send + + @ensure_integration_enabled(Httpx2Integration, real_send) + def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + Client.send = send # type: ignore + + +def _install_httpx2_async_client() -> None: + real_send = AsyncClient.send + + async def send( + self: "AsyncClient", request: "Request", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + if client.get_integration(Httpx2Integration) is None: + return await real_send(self, request, **kwargs) + + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) as streamed_span: + attributes: "Attributes" = {} + + if parsed_url is not None and should_send_default_pii(): + attributes["url.full"] = parsed_url.url + if parsed_url.query: + attributes["url.query"] = parsed_url.query + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + try: + rv = await real_send(self, request, **kwargs) + + streamed_span.status = "error" if rv.status_code >= 400 else "ok" + attributes["http.response.status_code"] = rv.status_code + finally: + streamed_span.set_attributes(attributes) + + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(streamed_span) + else: + with sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % ( + request.method, + parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, + ), + origin=Httpx2Integration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(client, str(request.url)): + for ( + key, + value, + ) in ( + sentry_sdk.get_current_scope().iter_trace_propagation_headers() + ): + logger.debug( + f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + rv = await real_send(self, request, **kwargs) + + span.set_http_status(rv.status_code) + span.set_data("reason", rv.reason_phrase) + + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv + + AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py new file mode 100644 index 0000000000..c3bbc8abcf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py @@ -0,0 +1,239 @@ +import sys +from datetime import datetime +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.api import continue_trace, get_baggage, get_traceparent +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + _register_control_flow_exception, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union + + from sentry_sdk._types import Event, EventProcessor, Hint + from sentry_sdk.utils import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + +try: + from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task + from huey.exceptions import CancelExecution, RetryTask, TaskLockedException +except ImportError: + raise DidNotEnable("Huey is not installed") + +try: + from huey.api import chord as HueyChord + from huey.api import group as HueyGroup +except ImportError: + HueyChord = None + HueyGroup = None + + +HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) + + +class HueyIntegration(Integration): + identifier = "huey" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_enqueue() + patch_execute() + _register_control_flow_exception( + [CancelExecution, RetryTask, TaskLockedException] + ) + + +def patch_enqueue() -> None: + old_enqueue = Huey.enqueue + + @ensure_integration_enabled(HueyIntegration, old_enqueue) + def _sentry_enqueue( + self: "Huey", item: "Any" + ) -> "Optional[Union[Result, ResultGroup]]": + if HueyChord is not None and isinstance(item, HueyChord): + span_name = "Huey Chord" + elif HueyGroup is not None and isinstance(item, HueyGroup): + span_name = "Huey Task Group" + else: + span_name = item.name + + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + span_ctx = None + if is_span_streaming_enabled: + span_ctx = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_HUEY, + "sentry.origin": HueyIntegration.origin, + }, + ) + else: + span_ctx = sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_HUEY, + name=span_name, + origin=HueyIntegration.origin, + ) + + no_headers_types = (PeriodicTask,) + tuple( + t for t in [HueyGroup, HueyChord] if t is not None + ) + with span_ctx: + if not isinstance(item, no_headers_types): + # Attach trace propagation data to task kwargs. We do + # not do this for periodic tasks, as these don't + # really have an originating transaction. + # Additionally, we do not do this for Huey groups or chords, as enqueue will + # recursively call this method for each task within the list, resulting + # in the trace propagation data being attached to each task individually + # (which we want) + item.kwargs["sentry_headers"] = { + BAGGAGE_HEADER_NAME: get_baggage(), + SENTRY_TRACE_HEADER_NAME: get_traceparent(), + } + return old_enqueue(self, item) + + Huey.enqueue = _sentry_enqueue + + +def _make_event_processor(task: "Any") -> "EventProcessor": + def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + tags = event.setdefault("tags", {}) + tags["huey_task_id"] = task.id + tags["huey_task_retry"] = task.default_retries > task.retries + extra = event.setdefault("extra", {}) + extra["huey-job"] = { + "task": task.name, + "args": ( + task.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + task.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "retry": (task.default_retries or 0) - task.retries, + } + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo") -> None: + scope = sentry_sdk.get_current_scope() + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.ABORTED) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.OK + return + + if not is_span_streaming_enabled: + scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + elif type(scope._span) is StreamedSpan: + scope._span._segment.status = SpanStatus.ERROR + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": HueyIntegration.identifier, "handled": False}, + ) + scope.capture_event(event, hint=hint) + + +def _wrap_task_execute(func: "F") -> "F": + @ensure_integration_enabled(HueyIntegration, func) + def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": + try: + result = func(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + return _sentry_execute # type: ignore + + +def patch_execute() -> None: + old_execute = Huey._execute + + @ensure_integration_enabled(HueyIntegration, old_execute) + def _sentry_execute( + self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None + ) -> "Any": + with sentry_sdk.isolation_scope() as scope: + with capture_internal_exceptions(): + scope._name = "huey" + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(task)) + + sentry_headers = task.kwargs.pop("sentry_headers", None) + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + + if is_span_streaming_enabled: + headers = sentry_headers or {} + sentry_sdk.traces.continue_trace(headers) + span_ctx = sentry_sdk.traces.start_span( + name=task.name, + attributes={ + "sentry.op": OP.QUEUE_TASK_HUEY, + "sentry.origin": HueyIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + "messaging.message.id": task.id, + "messaging.message.system": "huey", + "messaging.message.retry.count": (task.default_retries or 0) + - task.retries, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + sentry_headers or {}, + name=task.name, + op=OP.QUEUE_TASK_HUEY, + source=TransactionSource.TASK, + origin=HueyIntegration.origin, + ) + transaction.set_status(SPANSTATUS.OK) + span_ctx = sentry_sdk.start_transaction(transaction) + + if not getattr(task, "_sentry_is_patched", False): + task.execute = _wrap_task_execute(task.execute) + task._sentry_is_patched = True + + with span_ctx: + return old_execute(self, task, timestamp) + + Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py new file mode 100644 index 0000000000..835acc7279 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py @@ -0,0 +1,392 @@ +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + _set_span_data_attribute, + get_start_span_function, + set_data_normalized, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Iterable, Union + + from sentry_sdk.tracing import Span + +try: + import huggingface_hub.inference._client +except ImportError: + raise DidNotEnable("Huggingface not installed") + + +class HuggingfaceHubIntegration(Integration): + identifier = "huggingface_hub" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "HuggingfaceHubIntegration", include_prompts: bool = True + ) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks + huggingface_hub.inference._client.InferenceClient.text_generation = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.text_generation, + OP.GEN_AI_TEXT_COMPLETION, + ) + ) + huggingface_hub.inference._client.InferenceClient.chat_completion = ( + _wrap_huggingface_task( + huggingface_hub.inference._client.InferenceClient.chat_completion, + OP.GEN_AI_CHAT, + ) + ) + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "huggingface_hub", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": + @wraps(f) + def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) + if integration is None: + return f(*args, **kwargs) + + prompt = None + if "prompt" in kwargs: + prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] + elif len(args) >= 2: + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error + return f(*args, **kwargs) + + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] + + span: "Union[Span, StreamedSpan]" + if has_span_streaming_enabled(sentry_sdk.get_client().options): + span = sentry_sdk.traces.start_span( + name=f"{operation_name} {model}", + attributes={ + "sentry.op": op, + "sentry.origin": HuggingfaceHubIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=op, + name=f"{operation_name} {model}", + origin=HuggingfaceHubIntegration.origin, + ) + span.__enter__() + + _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) + + if model: + _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) + + # Input attributes + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + ) + + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + _set_span_data_attribute(span, span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution + try: + res = f(*args, **kwargs) + except Exception as e: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(e) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + tool_calls = None + usage = None + + with capture_internal_exceptions(): + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) + + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) + + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): + span.__exit__(None, None, None) + return res + + if kwargs.get("details", False): + # text-generation stream output + def new_details_iterator() -> "Iterable[Any]": + finish_reason = None + response_text_buffer: "list[str]" = [] + tokens_used = 0 + + with capture_internal_exceptions(): + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None + ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + span.__exit__(None, None, None) + + return new_details_iterator() + + else: + # chat-completion stream output + def new_iterator() -> "Iterable[str]": + finish_reason = None + response_model = None + response_text_buffer: "list[str]" = [] + tool_calls = None + usage = None + + with capture_internal_exceptions(): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if hasattr(chunk, "choices") and chunk.choices is not None: + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content + ) + + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason + + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls + + yield chunk + + if response_model is not None: + _set_span_data_attribute( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + + span.__exit__(None, None, None) + + return new_iterator() + + return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py new file mode 100644 index 0000000000..9dcbb189ce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py @@ -0,0 +1,1412 @@ +import itertools +import json +import sys +import warnings +from collections import OrderedDict +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + get_start_span_function, + normalize_message_roles, + set_data_normalized, + transform_content_part, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + _get_value, + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import capture_internal_exceptions, logger + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Dict, + Iterator, + List, + Optional, + Union, + ) + from uuid import UUID + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + + +try: + from langchain_core.agents import AgentFinish + from langchain_core.callbacks import ( + BaseCallbackHandler, + BaseCallbackManager, + Callbacks, + manager, + ) + from langchain_core.messages import BaseMessage + from langchain_core.outputs import LLMResult + +except ImportError: + raise DidNotEnable("langchain not installed") + + +try: + # >=v1 + from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] +except ImportError: + try: + # "Optional[str]": + ai_type = all_params.get("_type") + + if not ai_type or not isinstance(ai_type, str): + return None + + return ai_type + + +DATA_FIELDS = { + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, +} + + +def _transform_langchain_content_block( + content_block: "Dict[str, Any]", +) -> "Dict[str, Any]": + """ + Transform a LangChain content block using the shared transform_content_part function. + + Returns the original content block if transformation is not applicable + (e.g., for text blocks or unrecognized formats). + """ + result = transform_content_part(content_block) + return result if result is not None else content_block + + +def _transform_langchain_message_content(content: "Any") -> "Any": + """ + Transform LangChain message content, handling both string content and + list of content blocks. + """ + if isinstance(content, str): + return content + + if isinstance(content, (list, tuple)): + transformed = [] + for block in content: + if isinstance(block, dict): + transformed.append(_transform_langchain_content_block(block)) + else: + transformed.append(block) + return transformed + + return content + + +def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": + system_instructions = [] + + for list_ in messages: + for message in list_: + # type of content: str | list[str | dict] | None + if message.type == "system" and isinstance(message.content, str): + system_instructions.append(message.content) + + elif message.type == "system" and isinstance(message.content, list): + for item in message.content: + if isinstance(item, str): + system_instructions.append(item) + + elif isinstance(item, dict) and item.get("type") == "text": + instruction = item.get("text") + if isinstance(instruction, str): + system_instructions.append(instruction) + + return system_instructions + + +def _transform_system_instructions( + system_instructions: "List[str]", +) -> "List[TextPart]": + return [ + { + "type": "text", + "content": instruction, + } + for instruction in system_instructions + ] + + +class LangchainIntegration(Integration): + identifier = "langchain" + origin = f"auto.ai.{identifier}" + + _ignored_exceptions: "set[type[Exception]]" = set() + + def __init__( + self: "LangchainIntegration", + include_prompts: bool = True, + max_spans: "Optional[int]" = None, + ) -> None: + self.include_prompts = include_prompts + self.max_spans = max_spans + + if max_spans is not None: + warnings.warn( + "The `max_spans` parameter of `LangchainIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + manager._configure = _wrap_configure(manager._configure) + + if AgentExecutor is not None: + AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) + AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) + + # Patch embeddings providers + _patch_embeddings_provider(OpenAIEmbeddings) + _patch_embeddings_provider(AzureOpenAIEmbeddings) + _patch_embeddings_provider(VertexAIEmbeddings) + _patch_embeddings_provider(BedrockEmbeddings) + _patch_embeddings_provider(CohereEmbeddings) + _patch_embeddings_provider(MistralAIEmbeddings) + _patch_embeddings_provider(HuggingFaceEmbeddings) + _patch_embeddings_provider(OllamaEmbeddings) + + +class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] + """Callback handler that creates Sentry spans.""" + + def __init__( + self, max_span_map_size: "Optional[int]", include_prompts: bool + ) -> None: + self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() + self.max_span_map_size = max_span_map_size + self.include_prompts = include_prompts + + def gc_span_map(self) -> None: + if self.max_span_map_size is not None: + while len(self.span_map) > self.max_span_map_size: + run_id, span = self.span_map.popitem(last=False) + self._exit_span(span, run_id) + + def _handle_error(self, run_id: "UUID", error: "Any") -> None: + is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) + + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if is_ignored: + span.__exit__(None, None, None) + else: + sentry_sdk.capture_exception( + error, span._scope if isinstance(span, StreamedSpan) else span.scope + ) + span.__exit__(type(error), error, error.__traceback__) + + del self.span_map[run_id] + + def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": + # Transform content to handle multimodal data (images, audio, video, files) + transformed_content = _transform_langchain_message_content(message.content) + parsed = {"role": message.type, "content": transformed_content} + parsed.update(message.additional_kwargs) + return parsed + + def _create_span( + self: "SentryLangchainCallback", + run_id: "UUID", + parent_id: "Optional[Any]", + op: str, + name: str, + origin: str, + ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span = None + if parent_id: + parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( + self.span_map.get(parent_id) + ) + if parent_span: + span = ( + sentry_sdk.traces.start_span( + parent_span=parent_span, + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if isinstance(parent_span, StreamedSpan) + else parent_span.start_child(op=op, name=name, origin=origin) + ) + + if span is None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span = ( + sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": origin, + }, + ) + if span_streaming + else sentry_sdk.start_span(op=op, name=name, origin=origin) + ) + + span.__enter__() + self.span_map[run_id] = span + self.gc_span_map() + return span + + def _exit_span( + self: "SentryLangchainCallback", + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + run_id: "UUID", + ) -> None: + span.__exit__(None, None, None) + del self.span_map[run_id] + + def on_llm_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + prompts: "List[str]", + *, + run_id: "UUID", + tags: "Optional[List[str]]" = None, + parent_run_id: "Optional[UUID]" = None, + metadata: "Optional[Dict[str, Any]]" = None, + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + parent_run_id, + op=OP.GEN_AI_TEXT_COMPLETION, + name=f"text_completion {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") + + run_name = kwargs.get("name") + if run_name: + set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + if model: + set_on_span( + SPANDATA.GEN_AI_REQUEST_MODEL, + model, + ) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + normalized_messages = [ + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, + "content": {"type": "text", "text": prompt}, + } + for prompt in prompts + ] + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + messages: "List[List[BaseMessage]]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + all_params = kwargs.get("invocation_params", {}) + all_params.update(serialized.get("kwargs", {})) + + model = ( + all_params.get("model") + or all_params.get("model_name") + or all_params.get("model_id") + or "" + ) + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + if model: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + ai_system = _get_ai_system(all_params) + if ai_system: + set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + for key, attribute in DATA_FIELDS.items(): + if key in all_params and all_params[key] is not None: + set_data_normalized(span, attribute, all_params[key], unpack=False) + + _set_tools_on_span(span, all_params.get("tools")) + + if should_send_default_pii() and self.include_prompts: + system_instructions = _get_system_instructions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + normalized_messages = [] + for list_ in messages: + for message in list_: + if message.type == "system": + continue + + normalized_messages.append( + self._normalize_langchain_message(message) + ) + normalized_messages = normalize_message_roles(normalized_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + def on_chat_model_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_end( + self: "SentryLangchainCallback", + response: "LLMResult", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + try: + generation = response.generations[0][0] + except IndexError: + generation = None + + if generation is not None: + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + + try: + response_model = generation.message.response_metadata.get( + "model_name" + ) + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + except AttributeError: + pass + + try: + finish_reason = generation.generation_info.get("finish_reason") + if finish_reason is not None: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + [finish_reason], + ) + except AttributeError: + pass + + try: + if should_send_default_pii() and self.include_prompts: + tool_calls = getattr(generation.message, "tool_calls", None) + if tool_calls is not None and tool_calls != []: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, + ) + except AttributeError: + pass + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + [[x.text for x in list_] for list_ in response.generations], + ) + + _record_token_usage(span, response) + self._exit_span(span, run_id) + + def on_llm_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when LLM errors.""" + self._handle_error(run_id, error) + + def on_chat_model_error( + self: "SentryLangchainCallback", + error: "Union[Exception, KeyboardInterrupt]", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when Chat Model errors.""" + self._handle_error(run_id, error) + + def on_agent_finish( + self: "SentryLangchainCallback", + finish: "AgentFinish", + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() + ) + + self._exit_span(span, run_id) + + def on_tool_start( + self: "SentryLangchainCallback", + serialized: "Dict[str, Any]", + input_str: str, + *, + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool starts running.""" + with capture_internal_exceptions(): + if not run_id: + return + + tool_name = serialized.get("name") or kwargs.get("name") or "" + + span = self._create_span( + run_id, + kwargs.get("parent_run_id"), + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}".strip(), + origin=LangchainIntegration.origin, + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + tool_description = serialized.get("description") + if tool_description is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) + + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) + + run_name = kwargs.get("name") + if run_name: + set_on_span( + SPANDATA.GEN_AI_FUNCTION_ID, + run_name, + ) + + if should_send_default_pii() and self.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_TOOL_INPUT, + kwargs.get("inputs", [input_str]), + ) + + def on_tool_end( + self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" + ) -> "Any": + """Run when tool ends running.""" + with capture_internal_exceptions(): + if not run_id or run_id not in self.span_map: + return + + span = self.span_map[run_id] + + if should_send_default_pii() and self.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) + + self._exit_span(span, run_id) + + def on_tool_error( + self, + error: "SentryLangchainCallback", + *args: "Union[Exception, KeyboardInterrupt]", + run_id: "UUID", + **kwargs: "Any", + ) -> "Any": + """Run when tool errors.""" + self._handle_error(run_id, error) + + +def _extract_tokens( + token_usage: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + if not token_usage: + return None, None, None + + input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( + token_usage, "input_tokens" + ) + output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( + token_usage, "output_tokens" + ) + total_tokens = _get_value(token_usage, "total_tokens") + + return input_tokens, output_tokens, total_tokens + + +def _extract_tokens_from_generations( + generations: "Any", +) -> "tuple[Optional[int], Optional[int], Optional[int]]": + """Extract token usage from response.generations structure.""" + if not generations: + return None, None, None + + total_input = 0 + total_output = 0 + total_total = 0 + + for gen_list in generations: + for gen in gen_list: + token_usage = _get_token_usage(gen) + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + total_input += input_tokens if input_tokens is not None else 0 + total_output += output_tokens if output_tokens is not None else 0 + total_total += total_tokens if total_tokens is not None else 0 + + return ( + total_input if total_input > 0 else None, + total_output if total_output > 0 else None, + total_total if total_total > 0 else None, + ) + + +def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": + """ + Check multiple paths to extract token usage from different objects. + """ + possible_names = ("usage", "token_usage", "usage_metadata") + + message = _get_value(obj, "message") + if message is not None: + for name in possible_names: + usage = _get_value(message, name) + if usage is not None: + return usage + + llm_output = _get_value(obj, "llm_output") + if llm_output is not None: + for name in possible_names: + usage = _get_value(llm_output, name) + if usage is not None: + return usage + + for name in possible_names: + usage = _get_value(obj, name) + if usage is not None: + return usage + + return None + + +def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: + token_usage = _get_token_usage(response) + if token_usage: + input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) + else: + input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( + response.generations + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) + + +def _get_request_data( + obj: "Any", args: "Any", kwargs: "Any" +) -> "tuple[Optional[str], Optional[List[Any]]]": + """ + Get the agent name and available tools for the agent. + """ + agent = getattr(obj, "agent", None) + runnable = getattr(agent, "runnable", None) + runnable_config = getattr(runnable, "config", {}) + tools = ( + getattr(obj, "tools", None) + or getattr(agent, "tools", None) + or runnable_config.get("tools") + or runnable_config.get("available_tools") + ) + tools = tools if tools and len(tools) > 0 else None + + try: + agent_name = None + if len(args) > 1: + agent_name = args[1].get("run_name") + if agent_name is None: + agent_name = runnable_config.get("run_name") + except Exception: + pass + + return (agent_name, tools) + + +def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": + """Parse and simplify tools into a cleaner format.""" + if not tools: + return None + + if not isinstance(tools, (list, tuple)): + return None + + simplified_tools = [] + for tool in tools: + try: + if isinstance(tool, dict): + if "function" in tool and isinstance(tool["function"], dict): + func = tool["function"] + simplified_tool = { + "name": func.get("name"), + "description": func.get("description"), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif "name" in tool: + simplified_tool = { + "name": tool.get("name"), + "description": tool.get("description"), + } + simplified_tools.append(simplified_tool) + else: + name = ( + tool.get("name") + or tool.get("tool_name") + or tool.get("function_name") + ) + if name: + simplified_tools.append( + { + "name": name, + "description": tool.get("description") + or tool.get("desc"), + } + ) + elif hasattr(tool, "name"): + simplified_tool = { + "name": getattr(tool, "name", None), + "description": getattr(tool, "description", None) + or getattr(tool, "desc", None), + } + if simplified_tool["name"]: + simplified_tools.append(simplified_tool) + elif hasattr(tool, "__name__"): + simplified_tools.append( + { + "name": tool.__name__, + "description": getattr(tool, "__doc__", None), + } + ) + else: + tool_str = str(tool) + if tool_str and tool_str != "": + simplified_tools.append({"name": tool_str, "description": None}) + except Exception: + continue + + return simplified_tools if simplified_tools else None + + +def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: + """Set available tools data on a span if tools are provided.""" + if tools is not None: + simplified_tools = _simplify_langchain_tools(tools) + if simplified_tools: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + simplified_tools, + unpack=False, + ) + + +def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_configure( + callback_manager_cls: type, + inheritable_callbacks: "Callbacks" = None, + local_callbacks: "Callbacks" = None, + *args: "Any", + **kwargs: "Any", + ) -> "Any": + integration = sentry_sdk.get_client().get_integration(LangchainIntegration) + if integration is None: + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + local_callbacks = local_callbacks or [] + + # Handle each possible type of local_callbacks. For each type, we + # extract the list of callbacks to check for SentryLangchainCallback, + # and define a function that would add the SentryLangchainCallback + # to the existing callbacks list. + if isinstance(local_callbacks, BaseCallbackManager): + callbacks_list = local_callbacks.handlers + elif isinstance(local_callbacks, BaseCallbackHandler): + callbacks_list = [local_callbacks] + elif isinstance(local_callbacks, list): + callbacks_list = local_callbacks + else: + logger.debug("Unknown callback type: %s", local_callbacks) + # Just proceed with original function call + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + # Handle each possible type of inheritable_callbacks. + if isinstance(inheritable_callbacks, BaseCallbackManager): + inheritable_callbacks_list = inheritable_callbacks.handlers + elif isinstance(inheritable_callbacks, list): + inheritable_callbacks_list = inheritable_callbacks + else: + inheritable_callbacks_list = [] + + if not any( + isinstance(cb, SentryLangchainCallback) + for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) + ): + sentry_handler = SentryLangchainCallback( + integration.max_spans, + integration.include_prompts, + ) + if isinstance(local_callbacks, BaseCallbackManager): + local_callbacks = local_callbacks.copy() + local_callbacks.handlers = [ + *local_callbacks.handlers, + sentry_handler, + ] + elif isinstance(local_callbacks, BaseCallbackHandler): + local_callbacks = [local_callbacks, sentry_handler] + else: + local_callbacks = [*local_callbacks, sentry_handler] + + return f( + callback_manager_cls, + inheritable_callbacks, + local_callbacks, + *args, + **kwargs, + ) + + return new_configure + + +def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: False, + }, + ) as span: + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + else: + start_span_function = get_start_span_function() + + with start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) as span: + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) + + _set_tools_on_span(span, tools) + + # Run the agent + result = f(self, *args, **kwargs) + + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + return result + + return new_invoke + + +def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + run_name, tools = _get_request_data(self, args, kwargs) + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_RESPONSE_STREAMING: True, + }, + ) + + if run_name: + span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + else: + start_span_function = get_start_span_function() + + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", + origin=LangchainIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + if run_name: + span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) + + _set_tools_on_span(span, tools) + + input = args[0].get("input") if len(args) >= 1 else None + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Run the agent + result = f(self, *args, **kwargs) + + old_iterator = result + + def new_iterator() -> "Iterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + async def new_iterator_async() -> "AsyncIterator[Any]": + exc_info: "tuple[Any, Any, Any]" = (None, None, None) + try: + async for event in old_iterator: + yield event + + try: + output = event.get("output") + except Exception: + output = None + + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + + span.__exit__(None, None, None) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span.__exit__(*exc_info) + raise + + if str(type(result)) == "": + result = new_iterator_async() + else: + result = new_iterator() + + return result + + return new_stream + + +def _patch_embeddings_provider(provider_class: "Any") -> None: + """Patch an embeddings provider class with monitoring wrappers.""" + if provider_class is None: + return + + if hasattr(provider_class, "embed_documents"): + provider_class.embed_documents = _wrap_embedding_method( + provider_class.embed_documents + ) + if hasattr(provider_class, "embed_query"): + provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) + if hasattr(provider_class, "aembed_documents"): + provider_class.aembed_documents = _wrap_async_embedding_method( + provider_class.aembed_documents + ) + if hasattr(provider_class, "aembed_query"): + provider_class.aembed_query = _wrap_async_embedding_method( + provider_class.aembed_query + ) + + +def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap sync embedding methods (embed_documents and embed_query).""" + + @wraps(f) + def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = f(self, *args, **kwargs) + return result + + return new_embedding_method + + +def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": + """Wrap async embedding methods (aembed_documents and aembed_query).""" + + @wraps(f) + async def new_async_embedding_method( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LangchainIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + model_name = getattr(self, "model", None) or getattr(self, "model_name", None) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model_name}" if model_name else "embeddings", + attributes={ + "sentry.op": OP.GEN_AI_EMBEDDINGS, + "sentry.origin": LangchainIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", + }, + ) as span: + if model_name: + span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model_name}" if model_name else "embeddings", + origin=LangchainIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + if model_name: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Capture input if PII is allowed + if ( + should_send_default_pii() + and integration.include_prompts + and len(args) > 0 + ): + input_data = args[0] + # Normalize to list format + texts = input_data if isinstance(input_data, list) else [input_data] + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False + ) + + result = await f(self, *args, **kwargs) + return result + + return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py new file mode 100644 index 0000000000..3d3856a913 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py @@ -0,0 +1,515 @@ +from functools import wraps +from typing import Any, Callable, List, Optional + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration + +# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. +from sentry_sdk.integrations.langchain import LangchainIntegration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +try: + from langgraph.errors import GraphBubbleUp + from langgraph.graph import StateGraph + from langgraph.pregel import Pregel +except ImportError: + raise DidNotEnable("langgraph not installed") + + +class LanggraphIntegration(Integration): + identifier = "langgraph" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) + # LangGraph lets users create agents using a StateGraph or the Functional API. + # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and + # the functional API execute on a Pregel instance. Pregel is the runtime for the graph + # and the invocation happens on Pregel, so patching the invoke methods takes care of both. + # The streaming methods are not patched, because due to some internal reasons, LangGraph + # will automatically patch the streaming methods to run through invoke, and by doing this + # we prevent duplicate spans for invocations. + StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) + if hasattr(Pregel, "invoke"): + Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) + if hasattr(Pregel, "ainvoke"): + Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) + + +def _get_graph_name(graph_obj: "Any") -> "Optional[str]": + for attr in ["name", "graph_name", "__name__", "_name"]: + if hasattr(graph_obj, attr): + name = getattr(graph_obj, attr) + if name and isinstance(name, str): + return name + return None + + +def _normalize_langgraph_message(message: "Any") -> "Any": + if not hasattr(message, "content"): + return None + + parsed = {"role": getattr(message, "type", None), "content": message.content} + + for attr in [ + "name", + "tool_calls", + "function_call", + "tool_call_id", + "response_metadata", + ]: + if hasattr(message, attr): + value = getattr(message, attr) + if value is not None: + parsed[attr] = value + + return parsed + + +def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": + if not state: + return None + + messages = None + + if isinstance(state, dict): + messages = state.get("messages") + elif hasattr(state, "messages"): + messages = state.messages + elif hasattr(state, "get") and callable(state.get): + try: + messages = state.get("messages") + except Exception: + pass + + if not messages or not isinstance(messages, (list, tuple)): + return None + + normalized_messages = [] + for message in messages: + try: + normalized = _normalize_langgraph_message(message) + if normalized: + normalized_messages.append(normalized) + except Exception: + continue + + return normalized_messages if normalized_messages else None + + +def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None or has_span_streaming_enabled(client.options): + return f(self, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GEN_AI_CREATE_AGENT, + origin=LanggraphIntegration.origin, + ) as span: + compiled_graph = f(self, *args, **kwargs) + + compiled_graph_name = getattr(compiled_graph, "name", None) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) + + if compiled_graph_name: + span.description = f"create_agent {compiled_graph_name}" + else: + span.description = "create_agent" + + if kwargs.get("model", None) is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) + + tools = None + get_graph = getattr(compiled_graph, "get_graph", None) + if get_graph and callable(get_graph): + graph_obj = compiled_graph.get_graph() + nodes = getattr(graph_obj, "nodes", None) + if nodes and isinstance(nodes, dict): + tools_node = nodes.get("tools") + if tools_node: + data = getattr(tools_node, "data", None) + if data and hasattr(data, "tools_by_name"): + tools = list(data.tools_by_name.keys()) + + if tools is not None: + span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) + + return compiled_graph + + return new_compile + + +def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + else: + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + # Store input messages to later compare with output + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_invoke + + +def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(LanggraphIntegration) + if integration is None: + return await f(self, *args, **kwargs) + + graph_name = _get_graph_name(self) + span_name = ( + f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" + ) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": LanggraphIntegration.origin, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) as span: + if graph_name: + span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles( + input_messages + ) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + with get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=span_name, + origin=LanggraphIntegration.origin, + ) as span: + if graph_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + input_messages = None + if ( + len(args) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + input_messages = _parse_langgraph_messages(args[0]) + if input_messages: + normalized_input_messages = normalize_message_roles(input_messages) + + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages( + normalized_input_messages, span, scope + ) + if should_truncate_gen_ai_input(client.options) + else normalized_input_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + result = await f(self, *args, **kwargs) + + _set_response_attributes(span, input_messages, result, integration) + + return result + + return new_ainvoke + + +def _get_new_messages( + input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" +) -> "Optional[List[Any]]": + """Extract only the new messages added during this invocation.""" + if not output_messages: + return None + + if not input_messages: + return output_messages + + # only return the new messages, aka the output messages that are not in the input messages + input_count = len(input_messages) + new_messages = ( + output_messages[input_count:] if len(output_messages) > input_count else [] + ) + + return new_messages if new_messages else None + + +def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": + if not messages: + return None + + for message in reversed(messages): + if isinstance(message, dict): + role = message.get("role") + if role in ["assistant", "ai"]: + content = message.get("content") + if content and isinstance(content, str): + return content + + return None + + +def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": + if not messages: + return None + + tool_calls = [] + for message in messages: + if isinstance(message, dict): + msg_tool_calls = message.get("tool_calls") + if msg_tool_calls and isinstance(msg_tool_calls, list): + tool_calls.extend(msg_tool_calls) + + return tool_calls if tool_calls else None + + +def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + input_tokens = 0 + output_tokens = 0 + total_tokens = 0 + + for message in messages: + response_metadata = message.get("response_metadata") + if response_metadata is None: + continue + + token_usage = response_metadata.get("token_usage") + if not token_usage: + continue + + input_tokens += int(token_usage.get("prompt_tokens", 0)) + output_tokens += int(token_usage.get("completion_tokens", 0)) + total_tokens += int(token_usage.get("total_tokens", 0)) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if input_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) + + if output_tokens > 0: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) + + if total_tokens > 0: + set_on_span( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + total_tokens, + ) + + +def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: + if len(messages) == 0: + return + + last_message = messages[-1] + response_metadata = last_message.get("response_metadata") + if response_metadata is None: + return + + model_name = response_metadata.get("model_name") + if model_name is None: + return + + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) + + +def _set_response_attributes( + span: "Any", + input_messages: "Optional[List[Any]]", + result: "Any", + integration: "LanggraphIntegration", +) -> None: + parsed_response_messages = _parse_langgraph_messages(result) + new_messages = _get_new_messages(input_messages, parsed_response_messages) + + if new_messages is None: + return + + _set_usage_data(span, new_messages) + _set_response_model_name(span, new_messages) + + if not (should_send_default_pii() and integration.include_prompts): + return + + llm_response_text = _extract_llm_response_text(new_messages) + if llm_response_text: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) + elif new_messages: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) + else: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) + + tool_calls = _extract_tool_calls(new_messages) + if tool_calls: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(tool_calls), + unpack=False, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py new file mode 100644 index 0000000000..3c2c76450c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + import ldclient + from ldclient.hook import Hook, Metadata + + if TYPE_CHECKING: + from typing import Any + + from ldclient import LDClient + from ldclient.evaluation import EvaluationDetail + from ldclient.hook import EvaluationSeriesContext +except ImportError: + raise DidNotEnable("LaunchDarkly is not installed") + + +class LaunchDarklyIntegration(Integration): + identifier = "launchdarkly" + + def __init__(self, ld_client: "LDClient | None" = None) -> None: + """ + :param client: An initialized LDClient instance. If a client is not provided, this + integration will attempt to use the shared global instance. + """ + try: + client = ld_client or ldclient.get() + except Exception as exc: + raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) + + if not client.is_initialized(): + raise DidNotEnable("LaunchDarkly client is not initialized.") + + # Register the flag collection hook with the LD client. + client.add_hook(LaunchDarklyHook()) + + @staticmethod + def setup_once() -> None: + pass + + +class LaunchDarklyHook(Hook): + @property + def metadata(self) -> "Metadata": + return Metadata(name="sentry-flag-auditor") + + def after_evaluation( + self, + series_context: "EvaluationSeriesContext", + data: "dict[Any, Any]", + detail: "EvaluationDetail", + ) -> "dict[Any, Any]": + if isinstance(detail.value, bool): + add_feature_flag(series_context.key, detail.value) + + return data + + def before_evaluation( + self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" + ) -> "dict[Any, Any]": + return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py new file mode 100644 index 0000000000..49ead6b068 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py @@ -0,0 +1,376 @@ +import copy +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + set_data_normalized, + transform_openai_content_part, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import event_from_exception + +if TYPE_CHECKING: + from datetime import datetime + from typing import Any, Dict, List + +try: + import litellm # type: ignore[import-not-found] + from litellm import failure_callback, input_callback, success_callback +except ImportError: + raise DidNotEnable("LiteLLM not installed") + + +# Stash the span on a top-level key of the per-request kwargs dict litellm passes +# to every callback, so it lives and dies with the request. +_SPAN_KEY = "_sentry_span" + + +def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: + kwargs[_SPAN_KEY] = span + + +def _peek_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.get(_SPAN_KEY) + + +def _pop_span(kwargs: "Dict[str, Any]") -> "Any": + return kwargs.pop(_SPAN_KEY, None) + + +def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": + """ + Convert the message parts from OpenAI format to the `gen_ai.request.messages` format + using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). + + Deep copies messages to avoid mutating original kwargs. + """ + # Deep copy to avoid mutating original messages from kwargs + messages = copy.deepcopy(messages) + + for message in messages: + if not isinstance(message, dict): + continue + content = message.get("content") + if isinstance(content, (list, tuple)): + transformed = [] + for item in content: + if isinstance(item, dict): + result = transform_openai_content_part(item) + # If transformation succeeded, use the result; otherwise keep original + transformed.append(result if result is not None else item) + else: + transformed.append(item) + message["content"] = transformed + return messages + + +def _input_callback(kwargs: "Dict[str, Any]") -> None: + """Handle the start of a request.""" + client = sentry_sdk.get_client() + integration = client.get_integration(LiteLLMIntegration) + + if integration is None: + return + + # Get key parameters + full_model = kwargs.get("model", "") + try: + model, provider, _, _ = litellm.get_llm_provider(full_model) + except Exception: + model = full_model + provider = "unknown" + + call_type = kwargs.get("call_type", None) + if call_type == "embedding" or call_type == "aembedding": + operation = "embeddings" + else: + operation = "chat" + + # Start a new span/transaction + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"{operation} {model}", + attributes={ + "sentry.op": ( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + "sentry.origin": LiteLLMIntegration.origin, + }, + ) + else: + span = get_start_span_function()( + op=( + consts.OP.GEN_AI_CHAT + if operation == "chat" + else consts.OP.GEN_AI_EMBEDDINGS + ), + name=f"{operation} {model}", + origin=LiteLLMIntegration.origin, + ) + span.__enter__() + + _store_span(kwargs, span) + + # Set basic data + set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) + + # Record input/messages if allowed + if should_send_default_pii() and integration.include_prompts: + if operation == "embeddings": + # For embeddings, look for the 'input' parameter + embedding_input = kwargs.get("input") + if embedding_input: + scope = sentry_sdk.get_current_scope() + # Normalize to list format + input_list = ( + embedding_input + if isinstance(embedding_input, list) + else [embedding_input] + ) + client = sentry_sdk.get_client() + messages_data = ( + truncate_and_annotate_embedding_inputs(input_list, span, scope) + if should_truncate_gen_ai_input(client.options) + else input_list + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_EMBEDDINGS_INPUT, + messages_data, + unpack=False, + ) + else: + # For chat, look for the 'messages' parameter + messages = kwargs.get("messages", []) + if messages: + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages = _convert_message_parts(messages) + messages_data = ( + truncate_and_annotate_messages(messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + # Record other parameters + params = { + "model": SPANDATA.GEN_AI_REQUEST_MODEL, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + } + for key, attribute in params.items(): + value = kwargs.get(key) + if value is not None: + set_data_normalized(span, attribute, value) + + +async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: + return _input_callback(kwargs) + + +def _success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle successful completion.""" + + span = _peek_span(kwargs) + if span is None: + return + + integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) + if integration is None: + return + + try: + # Record model information + if hasattr(completion_response, "model"): + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model + ) + + # Record response content if allowed + if should_send_default_pii() and integration.include_prompts: + if hasattr(completion_response, "choices"): + response_messages = [] + for choice in completion_response.choices: + if hasattr(choice, "message"): + if hasattr(choice.message, "model_dump"): + response_messages.append(choice.message.model_dump()) + elif hasattr(choice.message, "dict"): + response_messages.append(choice.message.dict()) + else: + # Fallback for basic message objects + msg = {} + if hasattr(choice.message, "role"): + msg["role"] = choice.message.role + if hasattr(choice.message, "content"): + msg["content"] = choice.message.content + if hasattr(choice.message, "tool_calls"): + msg["tool_calls"] = choice.message.tool_calls + response_messages.append(msg) + + if response_messages: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages + ) + + # Record token usage + if hasattr(completion_response, "usage"): + usage = completion_response.usage + record_token_usage( + span, + input_tokens=getattr(usage, "prompt_tokens", None), + output_tokens=getattr(usage, "completion_tokens", None), + total_tokens=getattr(usage, "total_tokens", None), + ) + + finally: + is_streaming = kwargs.get("stream") + # Callback is fired multiple times when streaming a response. + # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 + if ( + is_streaming is not True + or "complete_streaming_response" in kwargs + or "async_complete_streaming_response" in kwargs + ): + span = _pop_span(kwargs) + if span is not None: + span.__exit__(None, None, None) + + +async def _async_success_callback( + kwargs: "Dict[str, Any]", + completion_response: "Any", + start_time: "datetime", + end_time: "datetime", +) -> None: + return _success_callback( + kwargs, + completion_response, + start_time, + end_time, + ) + + +def _failure_callback( + kwargs: "Dict[str, Any]", + exception: Exception, + start_time: "datetime", + end_time: "datetime", +) -> None: + """Handle request failure.""" + span = _pop_span(kwargs) + if span is None: + return + + try: + # Capture the exception + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "litellm", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + finally: + # Always finish the span and clean up + span.__exit__(type(exception), exception, None) + + +class LiteLLMIntegration(Integration): + """ + LiteLLM integration for Sentry. + + This integration automatically captures LiteLLM API calls and sends them to Sentry + for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM + supports, including OpenAI, Anthropic, Google, Cohere, and many others. + + Features: + - Automatic exception capture for all LiteLLM calls + - Token usage tracking across all providers + - Provider detection and attribution + - Input/output message capture (configurable) + - Streaming response support + - Cost tracking integration + + Usage: + + ```python + import litellm + import sentry_sdk + + # Initialize Sentry with the LiteLLM integration + sentry_sdk.init( + dsn="your-dsn", + send_default_pii=True + integrations=[ + sentry_sdk.integrations.LiteLLMIntegration( + include_prompts=True # Set to False to exclude message content + ) + ] + ) + + # All LiteLLM calls will now be monitored + response = litellm.completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Hello!"}] + ) + ``` + + Configuration: + - include_prompts (bool): Whether to include prompts and responses in spans. + Defaults to True. Set to False to exclude potentially sensitive data. + """ + + identifier = "litellm" + origin = f"auto.ai.{identifier}" + + def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """Set up LiteLLM callbacks for monitoring.""" + litellm.input_callback = input_callback or [] + if _input_callback not in litellm.input_callback: + litellm.input_callback.append(_input_callback) + if _async_input_callback not in litellm.input_callback: + litellm.input_callback.append(_async_input_callback) + + litellm.success_callback = success_callback or [] + if _success_callback not in litellm.success_callback: + litellm.success_callback.append(_success_callback) + if _async_success_callback not in litellm.success_callback: + litellm.success_callback.append(_async_success_callback) + + litellm.failure_callback = failure_callback or [] + if _failure_callback not in litellm.failure_callback: + litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py new file mode 100644 index 0000000000..f0c90a7921 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py @@ -0,0 +1,364 @@ +from collections.abc import Set +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from litestar import Litestar, Request # type: ignore + from litestar.data_extractors import ConnectionDataExtractor # type: ignore + from litestar.exceptions import HTTPException # type: ignore + from litestar.handlers.base import BaseRouteHandler # type: ignore + from litestar.middleware import DefineMiddleware # type: ignore + from litestar.routes.http import HTTPRoute # type: ignore +except ImportError: + raise DidNotEnable("Litestar is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from litestar.middleware import MiddlewareProtocol + from litestar.types import ( # type: ignore + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from litestar.types import ( + Scope as LitestarScope, + ) + from litestar.types.asgi_types import ASGIApp # type: ignore + + from sentry_sdk._types import Event, Hint + +_DEFAULT_TRANSACTION_NAME = "generic Litestar request" + + +class LitestarIntegration(Integration): + identifier = "litestar" + origin = f"auto.http.{identifier}" + + def __init__( + self, + failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + ) -> None: + self.failed_request_status_codes = failed_request_status_codes + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. + # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following + # (among other things): + # 1. Logs them, some at least (such as 500s) as errors + # 2. Calls after_exception hooks + # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event + # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will + # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. + ignore_logger("litestar") + + +class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + def _capture_request_exception(self, exc: Exception) -> None: + """Avoid catching exceptions from request handlers. + + Those exceptions are already handled in Litestar.after_exception handler. + We still catch exceptions from application lifespan handlers. + """ + pass + + +def patch_app_init() -> None: + """ + Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. + See: + - https://docs.litestar.dev/2/usage/applications.html#after-exception + - https://docs.litestar.dev/2/usage/middleware/using-middleware.html + """ + old__init__ = Litestar.__init__ + + @ensure_integration_enabled(LitestarIntegration, old__init__) + def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: + kwargs["after_exception"] = [ + exception_handler, + *(kwargs.get("after_exception") or []), + ] + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Litestar.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryLitestarASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "LitestarScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(LitestarIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=middleware_name, + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR, + "sentry.origin": LitestarIntegration.origin, + }, + ) as middleware_span: + middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.traces.start_span( + name=getattr(receive, "__qualname__", str(receive)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.traces.start_span( + name=getattr(send, "__qualname__", str(send)), + attributes={ + "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, + "sentry.origin": LitestarIntegration.origin, + }, + ) as span: + span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + else: + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR, + name=middleware_name, + origin=LitestarIntegration.origin, + ) as middleware_span: + middleware_span.set_tag("litestar.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if client.get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if client.get_integration(LitestarIntegration) is None: + return await send(message) + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_SEND, + name=getattr(send, "__qualname__", str(send)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type + elif hasattr(route_handler.fn, "value"): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = LitestarIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if isinstance(scope_user, dict): + return scope_user + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + return None + + +@ensure_integration_enabled(LitestarIntegration) +def exception_handler(exc: Exception, scope: "LitestarScope") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + if isinstance(exc, HTTPException): + integration = sentry_sdk.get_client().get_integration(LitestarIntegration) + if ( + integration is not None + and exc.status_code not in integration.failed_request_status_codes + ): + return + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": LitestarIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py new file mode 100644 index 0000000000..a310a0ced6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py @@ -0,0 +1,476 @@ +import logging +import sys +from datetime import datetime, timezone +from fnmatch import fnmatch +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.client import BaseClient +from sentry_sdk.integrations import Integration +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import ( + capture_internal_exceptions, + current_stacktrace, + event_from_exception, + has_logs_enabled, + safe_repr, + to_string, +) + +if TYPE_CHECKING: + from collections.abc import MutableMapping + from logging import LogRecord + from typing import Any, Dict, Optional + +DEFAULT_LEVEL = logging.INFO +DEFAULT_EVENT_LEVEL = logging.ERROR +LOGGING_TO_EVENT_LEVEL = { + logging.NOTSET: "notset", + logging.DEBUG: "debug", + logging.INFO: "info", + logging.WARN: "warning", # WARN is same a WARNING + logging.WARNING: "warning", + logging.ERROR: "error", + logging.FATAL: "fatal", + logging.CRITICAL: "fatal", # CRITICAL is same as FATAL +} + +# Map logging level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + logging.CRITICAL: 21, # fatal + logging.ERROR: 17, # error + logging.WARNING: 13, # warn + logging.INFO: 9, # info + logging.DEBUG: 5, # debug +} + + +# Capturing events from those loggers causes recursion errors. We cannot allow +# the user to unconditionally create events from those loggers under any +# circumstances. +# +# Note: Ignoring by logger name here is better than mucking with thread-locals. +# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. +# +# Events/breadcrumbs and Sentry Logs have separate ignore lists so that +# framework loggers silenced for events (e.g. django.server) can still be +# captured as Sentry Logs. +_IGNORED_LOGGERS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + +_IGNORED_LOGGERS_SENTRY_LOGS = set( + ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] +) + + +def ignore_logger( + name: str, +) -> None: + """This disables recording (both in breadcrumbs and as events) calls to + a logger of a specific name. Among other uses, many of our integrations + use this to prevent their actions being recorded as breadcrumbs. Exposed + to users as a way to quiet spammy loggers. + + This does **not** affect Sentry Logs — use + :py:func:`ignore_logger_for_sentry_logs` for that. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS.add(name) + + +def ignore_logger_for_sentry_logs( + name: str, +) -> None: + """This disables recording as Sentry Logs calls to a logger of a + specific name. + + :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). + """ + _IGNORED_LOGGERS_SENTRY_LOGS.add(name) + + +def unignore_logger( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger` call, re-enabling + recording of breadcrumbs and events for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS.discard(name) + + +def unignore_logger_for_sentry_logs( + name: str, +) -> None: + """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, + re-enabling recording of Sentry Logs for the named logger. + + :param name: The name of the logger to unignore. + """ + _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) + + +class LoggingIntegration(Integration): + identifier = "logging" + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + self._handler = None + self._breadcrumb_handler = None + self._sentry_logs_handler = None + + if level is not None: + self._breadcrumb_handler = BreadcrumbHandler(level=level) + + if sentry_logs_level is not None: + self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) + + if event_level is not None: + self._handler = EventHandler(level=event_level) + + def _handle_record(self, record: "LogRecord") -> None: + if self._handler is not None and record.levelno >= self._handler.level: + self._handler.handle(record) + + if ( + self._breadcrumb_handler is not None + and record.levelno >= self._breadcrumb_handler.level + ): + self._breadcrumb_handler.handle(record) + + def _handle_sentry_logs_record(self, record: "LogRecord") -> None: + if ( + self._sentry_logs_handler is not None + and record.levelno >= self._sentry_logs_handler.level + ): + self._sentry_logs_handler.handle(record) + + @staticmethod + def setup_once() -> None: + old_callhandlers = logging.Logger.callHandlers + + def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": + # keeping a local reference because the + # global might be discarded on shutdown + ignored_loggers = _IGNORED_LOGGERS + ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS + + try: + return old_callhandlers(self, record) + finally: + # This check is done twice, once also here before we even get + # the integration. Otherwise we have a high chance of getting + # into a recursion error when the integration is resolved + # (this also is slower). + name = record.name.strip() + + handle_events = ( + ignored_loggers is not None and name not in ignored_loggers + ) + handle_sentry_logs = ( + ignored_loggers_sentry_logs is not None + and name not in ignored_loggers_sentry_logs + ) + + if handle_events or handle_sentry_logs: + integration = sentry_sdk.get_client().get_integration( + LoggingIntegration + ) + if integration is not None: + if handle_events: + integration._handle_record(record) + if handle_sentry_logs: + integration._handle_sentry_logs_record(record) + + logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore + + +class _BaseHandler(logging.Handler): + COMMON_RECORD_ATTRS = frozenset( + ( + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "linenno", + "lineno", + "message", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack", + "tags", + "taskName", + "thread", + "threadName", + "stack_info", + ) + ) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + return LOGGING_TO_EVENT_LEVEL.get( + record.levelno, record.levelname.lower() if record.levelname else "" + ) + + def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": + return { + k: v + for k, v in vars(record).items() + if k not in self.COMMON_RECORD_ATTRS + and (not isinstance(k, str) or not k.startswith("_")) + } + + +class EventHandler(_BaseHandler): + """ + A logging handler that emits Sentry events for each log record + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + client_options = client.options + + # exc_info might be None or (None, None, None) + # + # exc_info may also be any falsy value due to Python stdlib being + # liberal with what it receives and Celery's billiard being "liberal" + # with what it sends. See + # https://github.com/getsentry/sentry-python/issues/904 + if record.exc_info and record.exc_info[0] is not None: + event, hint = event_from_exception( + record.exc_info, + client_options=client_options, + mechanism={"type": "logging", "handled": True}, + ) + elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: + event = {} + hint = {} + with capture_internal_exceptions(): + event["threads"] = { + "values": [ + { + "stacktrace": current_stacktrace( + include_local_variables=client_options[ + "include_local_variables" + ], + max_value_length=client_options["max_value_length"], + ), + "crashed": False, + "current": True, + } + ] + } + else: + event = {} + hint = {} + + hint["log_record"] = record + + level = self._logging_to_event_level(record) + if level in {"debug", "info", "warning", "error", "critical", "fatal"}: + event["level"] = level # type: ignore[typeddict-item] + event["logger"] = record.name + + if ( + sys.version_info < (3, 11) + and record.name == "py.warnings" + and record.msg == "%s" + ): + # warnings module on Python 3.10 and below sets record.msg to "%s" + # and record.args[0] to the actual warning message. + # This was fixed in https://github.com/python/cpython/pull/30975. + message = record.args[0] + params = () + else: + message = record.msg + params = record.args + + event["logentry"] = { + "message": to_string(message), + "formatted": record.getMessage(), + "params": params, + } + + event["extra"] = self._extra_from_record(record) + + sentry_sdk.capture_event(event, hint=hint) + + +# Legacy name +SentryHandler = EventHandler + + +class BreadcrumbHandler(_BaseHandler): + """ + A logging handler that records breadcrumbs for each log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + return self._emit(record) + + def _emit(self, record: "LogRecord") -> None: + if not self._can_record(record): + return + + sentry_sdk.add_breadcrumb( + self._breadcrumb_from_record(record), hint={"log_record": record} + ) + + def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": + return { + "type": "log", + "level": self._logging_to_event_level(record), + "category": record.name, + "message": record.message, + "timestamp": datetime.fromtimestamp(record.created, timezone.utc), + "data": self._extra_from_record(record), + } + + +class SentryLogsHandler(_BaseHandler): + """ + A logging handler that records Sentry logs for each Python log record. + + Note that you do not have to use this class if the logging integration is enabled, which it is by default. + """ + + def _can_record(self, record: "LogRecord") -> bool: + """Prevents ignored loggers from recording""" + for logger in _IGNORED_LOGGERS_SENTRY_LOGS: + if fnmatch(record.name.strip(), logger): + return False + return True + + def emit(self, record: "LogRecord") -> "Any": + with capture_internal_exceptions(): + self.format(record) + if not self._can_record(record): + return + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + self._capture_log_from_record(client, record) + + def _capture_log_from_record( + self, client: "BaseClient", record: "LogRecord" + ) -> None: + otel_severity_number, otel_severity_text = _log_level_to_otel( + record.levelno, SEVERITY_TO_OTEL_SEVERITY + ) + project_root = client.options["project_root"] + + attrs: "Any" = self._extra_from_record(record) + attrs["sentry.origin"] = "auto.log.stdlib" + + parameters_set = False + if record.args is not None: + if isinstance(record.args, tuple): + parameters_set = bool(record.args) + for i, arg in enumerate(record.args): + attrs[f"sentry.message.parameter.{i}"] = ( + arg + if isinstance(arg, (str, float, int, bool)) + else safe_repr(arg) + ) + elif isinstance(record.args, dict): + parameters_set = bool(record.args) + for key, value in record.args.items(): + attrs[f"sentry.message.parameter.{key}"] = ( + value + if isinstance(value, (str, float, int, bool)) + else safe_repr(value) + ) + + if parameters_set and isinstance(record.msg, str): + # only include template if there is at least one + # sentry.message.parameter.X set + attrs["sentry.message.template"] = record.msg + + if record.lineno: + attrs["code.line.number"] = record.lineno + + if record.pathname: + if project_root is not None and record.pathname.startswith(project_root): + attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record.pathname + + if record.funcName: + attrs["code.function.name"] = record.funcName + + if record.thread: + attrs["thread.id"] = record.thread + if record.threadName: + attrs["thread.name"] = record.threadName + + if record.process: + attrs["process.pid"] = record.process + if record.processName: + attrs["process.executable.name"] = record.processName + if record.name: + attrs["logger.name"] = record.name + + # noinspection PyProtectedMember + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record.message, + "attributes": attrs, + "time_unix_nano": int(record.created * 1e9), + "trace_id": None, + "span_id": None, + }, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py new file mode 100644 index 0000000000..dbb724d9a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py @@ -0,0 +1,208 @@ +import enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.logging import ( + BreadcrumbHandler, + EventHandler, + _BaseHandler, +) +from sentry_sdk.logger import _log_level_to_otel +from sentry_sdk.utils import has_logs_enabled, safe_repr + +if TYPE_CHECKING: + from logging import LogRecord + from typing import Any, Optional + +try: + import loguru + from loguru import logger + from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT + + if TYPE_CHECKING: + from loguru import Message +except ImportError: + raise DidNotEnable("LOGURU is not installed") + + +class LoggingLevels(enum.IntEnum): + TRACE = 5 + DEBUG = 10 + INFO = 20 + SUCCESS = 25 + WARNING = 30 + ERROR = 40 + CRITICAL = 50 + + +DEFAULT_LEVEL = LoggingLevels.INFO.value +DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value + + +SENTRY_LEVEL_FROM_LOGURU_LEVEL = { + "TRACE": "DEBUG", + "DEBUG": "DEBUG", + "INFO": "INFO", + "SUCCESS": "INFO", + "WARNING": "WARNING", + "ERROR": "ERROR", + "CRITICAL": "CRITICAL", +} + +# Map Loguru level numbers to corresponding OTel level numbers +SEVERITY_TO_OTEL_SEVERITY = { + LoggingLevels.CRITICAL: 21, # fatal + LoggingLevels.ERROR: 17, # error + LoggingLevels.WARNING: 13, # warn + LoggingLevels.SUCCESS: 11, # info + LoggingLevels.INFO: 9, # info + LoggingLevels.DEBUG: 5, # debug + LoggingLevels.TRACE: 1, # trace +} + + +class LoguruIntegration(Integration): + identifier = "loguru" + + level: "Optional[int]" = DEFAULT_LEVEL + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL + breadcrumb_format = DEFAULT_FORMAT + event_format = DEFAULT_FORMAT + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL + + def __init__( + self, + level: "Optional[int]" = DEFAULT_LEVEL, + event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, + breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, + sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, + ) -> None: + LoguruIntegration.level = level + LoguruIntegration.event_level = event_level + LoguruIntegration.breadcrumb_format = breadcrumb_format + LoguruIntegration.event_format = event_format + LoguruIntegration.sentry_logs_level = sentry_logs_level + + @staticmethod + def setup_once() -> None: + if LoguruIntegration.level is not None: + logger.add( + LoguruBreadcrumbHandler(level=LoguruIntegration.level), + level=LoguruIntegration.level, + format=LoguruIntegration.breadcrumb_format, + ) + + if LoguruIntegration.event_level is not None: + logger.add( + LoguruEventHandler(level=LoguruIntegration.event_level), + level=LoguruIntegration.event_level, + format=LoguruIntegration.event_format, + ) + + if LoguruIntegration.sentry_logs_level is not None: + logger.add( + loguru_sentry_logs_handler, + level=LoguruIntegration.sentry_logs_level, + ) + + +class _LoguruBaseHandler(_BaseHandler): + def __init__(self, *args: "Any", **kwargs: "Any") -> None: + if kwargs.get("level"): + kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( + kwargs.get("level", ""), DEFAULT_LEVEL + ) + + super().__init__(*args, **kwargs) + + def _logging_to_event_level(self, record: "LogRecord") -> str: + try: + return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ + LoggingLevels(record.levelno).name + ].lower() + except (ValueError, KeyError): + return record.levelname.lower() if record.levelname else "" + + +class LoguruEventHandler(_LoguruBaseHandler, EventHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" + + pass + + +class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): + """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" + + pass + + +def loguru_sentry_logs_handler(message: "Message") -> None: + # This is intentionally a callable sink instead of a standard logging handler + # since otherwise we wouldn't get direct access to message.record + client = sentry_sdk.get_client() + + if not client.is_active(): + return + + if not has_logs_enabled(client.options): + return + + record = message.record + + if ( + LoguruIntegration.sentry_logs_level is None + or record["level"].no < LoguruIntegration.sentry_logs_level + ): + return + + otel_severity_number, otel_severity_text = _log_level_to_otel( + record["level"].no, SEVERITY_TO_OTEL_SEVERITY + ) + + attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} + + project_root = client.options["project_root"] + if record.get("file"): + if project_root is not None and record["file"].path.startswith(project_root): + attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] + else: + attrs["code.file.path"] = record["file"].path + + if record.get("line") is not None: + attrs["code.line.number"] = record["line"] + + if record.get("function"): + attrs["code.function.name"] = record["function"] + + if record.get("thread"): + attrs["thread.name"] = record["thread"].name + attrs["thread.id"] = record["thread"].id + + if record.get("process"): + attrs["process.pid"] = record["process"].id + attrs["process.executable.name"] = record["process"].name + + if record.get("name"): + attrs["logger.name"] = record["name"] + + extra = record.get("extra") + if isinstance(extra, dict): + for key, value in extra.items(): + if isinstance(value, (str, int, float, bool)): + attrs[f"sentry.message.parameter.{key}"] = value + else: + attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": otel_severity_text, + "severity_number": otel_severity_number, + "body": record["message"], + "attributes": attrs, + "time_unix_nano": int(record["time"].timestamp() * 1e9), + "trace_id": None, + "span_id": None, + } + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py new file mode 100644 index 0000000000..79381fe06e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py @@ -0,0 +1,876 @@ +""" +Sentry integration for MCP (Model Context Protocol) servers. + +This integration instruments MCP servers to create spans for tool, prompt, +and resource handler execution, and captures errors that occur during execution. + +Supports the low-level `mcp.server.lowlevel.Server` API. +""" + +import inspect +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import nullcontext +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import package_version, safe_serialize + +MCP_PACKAGE_VERSION = package_version("mcp") + +try: + from mcp.server.lowlevel import Server + from mcp.server.streamable_http import ( + StreamableHTTPServerTransport, + ) + + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] + request_ctx, + ) +except ImportError: + raise DidNotEnable("MCP SDK not installed") + +try: + from fastmcp import FastMCP # type: ignore[import-not-found] +except ImportError: + FastMCP = None + +if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + try: + from mcp.server.context import ( + ServerRequestContext, + ) + except ImportError: + ServerRequestContext = None # type: ignore[assignment,misc] +else: + ServerRequestContext = None # type: ignore[assignment,misc] + + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Optional, Tuple, Union + + from starlette.types import Receive, Scope, Send + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +class MCPIntegration(Integration): + identifier = "mcp" + origin = "auto.ai.mcp" + + def __init__(self, include_prompts: bool = True) -> None: + """ + Initialize the MCP integration. + + Args: + include_prompts: Whether to include prompts (tool results and prompt content) + in span data. Requires send_default_pii=True. Default is True. + """ + self.include_prompts = include_prompts + + @staticmethod + def setup_once() -> None: + """ + Patches MCP server classes to instrument handler execution. + """ + _patch_lowlevel_server() + _patch_handle_request() + + if FastMCP is not None: + _patch_fastmcp() + + +def _get_active_http_scopes( + ctx: "Optional[Any]" = None, +) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return None + + if ( + ctx is None + or not hasattr(ctx, "request") + or ctx.request is None + or "state" not in ctx.request.scope + ): + return None + + return ( + ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), + ctx.request.scope["state"].get("sentry_sdk.current_scope"), + ) + + +def _get_request_context_data( + ctx: "Optional[Any]" = None, +) -> "tuple[Optional[str], Optional[str], str]": + """ + Extract request ID, session ID, and MCP transport type from the request context. + + Returns: + Tuple of (request_id, session_id, mcp_transport). + - request_id: May be None if not available + - session_id: May be None if not available + - mcp_transport: "http", "sse", "stdio" + """ + request_id: "Optional[str]" = None + session_id: "Optional[str]" = None + mcp_transport: str = "stdio" + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): + if ctx is None: + try: + ctx = request_ctx.get() + except LookupError: + return request_id, session_id, mcp_transport + + if ctx is not None: + request_id = ctx.request_id + if hasattr(ctx, "request") and ctx.request is not None: + request = ctx.request + # Detect transport type by checking request characteristics + if hasattr(request, "query_params") and request.query_params.get( + "session_id" + ): + # SSE transport uses query parameter + mcp_transport = "sse" + session_id = request.query_params.get("session_id") + elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): + # StreamableHTTP transport uses header + mcp_transport = "http" + session_id = request.headers.get("mcp-session-id") + + return request_id, session_id, mcp_transport + + +def _get_span_config( + handler_type: str, item_name: str +) -> "tuple[str, str, str, Optional[str]]": + """ + Get span configuration based on handler type. + + Returns: + Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) + Note: result_data_key is None for resources + """ + if handler_type == "tool": + span_data_key = SPANDATA.MCP_TOOL_NAME + mcp_method_name = "tools/call" + result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT + elif handler_type == "prompt": + span_data_key = SPANDATA.MCP_PROMPT_NAME + mcp_method_name = "prompts/get" + result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT + else: # resource + span_data_key = SPANDATA.MCP_RESOURCE_URI + mcp_method_name = "resources/read" + result_data_key = None # Resources don't capture result content + + span_name = f"{mcp_method_name} {item_name}" + return span_data_key, span_name, mcp_method_name, result_data_key + + +def _set_span_input_data( + span: "Union[StreamedSpan, Span]", + handler_name: str, + span_data_key: str, + mcp_method_name: str, + arguments: "dict[str, Any]", + request_id: "Optional[str]", + session_id: "Optional[str]", + mcp_transport: str, +) -> None: + """Set input span data for MCP handlers.""" + + # Set handler identifier + _set_span_data_attribute(span, span_data_key, handler_name) + _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) + + # Set transport/MCP transport type + _set_span_data_attribute( + span, + SPANDATA.NETWORK_TRANSPORT, + "pipe" if mcp_transport == "stdio" else "tcp", + ) + _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) + + # Set request_id if provided + if request_id: + _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) + + # Set session_id if provided + if session_id: + _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) + + # Set request arguments (excluding common request context objects) + for k, v in arguments.items(): + _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) + + +def _extract_tool_result_content(result: "Any") -> "Any": + """ + Extract meaningful content from MCP tool result. + + Tool handlers can return: + - CallToolResult (mcp v2+): Has .content list and optional .structured_content + - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) + - dict (StructuredContent): Return as-is + - list/Iterable (UnstructuredContent): Extract text from content blocks + """ + if result is None: + return None + + # Handle v2 CallToolResult-like objects (has .content list attribute) + if hasattr(result, "content") and isinstance( + getattr(result, "content", None), list + ): + # This is only present when a tool declares an output_schema + structured = getattr(result, "structured_content", None) + if structured is not None: + return structured + return _extract_text_from_content_blocks(result.content) + + # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) + if isinstance(result, tuple) and len(result) == 2: + # Return the structured content (2nd element) + return result[1] + + # Handle StructuredContent: dict + if isinstance(result, dict): + return result + + # Handle UnstructuredContent: iterable of ContentBlock objects + if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): + return _extract_text_from_content_blocks(result) + + return result + + +def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": + texts = [] + try: + for item in content_blocks: + if hasattr(item, "text"): + texts.append(item.text) + elif isinstance(item, dict) and "text" in item: + texts.append(item["text"]) + except Exception: + return content_blocks + return " ".join(texts) if texts else content_blocks + + +def _set_span_output_data( + span: "Union[StreamedSpan, Span]", + result: "Any", + result_data_key: "Optional[str]", + handler_type: str, +) -> None: + """Set output span data for MCP handlers.""" + if result is None: + return + + # Get integration to check PII settings + integration = sentry_sdk.get_client().get_integration(MCPIntegration) + if integration is None: + return + + # Check if we should include sensitive data + should_include_data = should_send_default_pii() and integration.include_prompts + + # For tools, extract the meaningful content + if handler_type == "tool": + extracted = _extract_tool_result_content(result) + if ( + extracted is not None + and should_include_data + and result_data_key is not None + ): + _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) + # Set content count if result is a dict + if isinstance(extracted, dict): + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) + ) + elif handler_type == "prompt": + # For prompts, count messages and set role/content only for single-message prompts + try: + messages: "Optional[list[str]]" = None + message_count = 0 + + # Check if result has messages attribute (GetPromptResult) + if hasattr(result, "messages") and result.messages: + messages = result.messages + message_count = len(messages) + # Also check if result is a dict with messages + elif isinstance(result, dict) and result.get("messages"): + messages = result["messages"] + message_count = len(messages) + + # Always set message count if we found messages + if message_count > 0: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count + ) + + # Only set role and content for single-message prompts if PII is allowed + if message_count == 1 and should_include_data and messages: + first_message = messages[0] + # Extract role + role = None + if hasattr(first_message, "role"): + role = first_message.role + elif isinstance(first_message, dict) and "role" in first_message: + role = first_message["role"] + + if role: + _set_span_data_attribute( + span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role + ) + + # Extract content text + content_text = None + if hasattr(first_message, "content"): + msg_content = first_message.content + # Content can be a TextContent object or similar + if hasattr(msg_content, "text"): + content_text = msg_content.text + elif isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + elif isinstance(first_message, dict) and "content" in first_message: + msg_content = first_message["content"] + if isinstance(msg_content, dict) and "text" in msg_content: + content_text = msg_content["text"] + elif isinstance(msg_content, str): + content_text = msg_content + + if content_text and result_data_key is not None: + _set_span_data_attribute(span, result_data_key, content_text) + except Exception: + # Silently ignore if we can't extract message info + pass + # Resources don't capture result content (result_data_key is None) + + +# Handler data preparation and wrapping + + +def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: + """Check if original_args contains a v2 ServerRequestContext as the first element.""" + return ( + ServerRequestContext is not None + and bool(original_args) + and isinstance(original_args[0], ServerRequestContext) + ) + + +def _extract_handler_data_from_params( + handler_type: str, + params: "Any", +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from a v2 typed params object. + + In MCP SDK v2, handlers receive (ctx, params) where params is a typed + Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). + """ + if handler_type == "tool": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + elif handler_type == "prompt": + handler_name = getattr(params, "name", "unknown") + arguments = getattr(params, "arguments", None) or {} + arguments = {"name": handler_name, **arguments} + else: # resource + handler_name = str(getattr(params, "uri", "unknown")) + arguments = {} + + return handler_name, arguments + + +def _extract_handler_data_from_args( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, +) -> "tuple[str, dict[str, Any]]": + """ + Extract handler name and arguments from v1 positional args. + + In MCP SDK v1, handlers receive positional args: + - Tool: (tool_name, arguments) + - Prompt: (name, arguments) + - Resource: (uri,) + """ + original_kwargs = original_kwargs or {} + + if handler_type == "tool": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + elif handler_type == "prompt": + if original_args: + handler_name = original_args[0] + elif original_kwargs.get("name"): + handler_name = original_kwargs["name"] + + arguments = {} + if len(original_args) > 1: + arguments = original_args[1] + elif original_kwargs.get("arguments"): + arguments = original_kwargs["arguments"] + + arguments = {"name": handler_name, **(arguments or {})} + + else: # resource + handler_name = "unknown" + if original_args: + handler_name = str(original_args[0]) + elif original_kwargs.get("uri"): + handler_name = str(original_kwargs["uri"]) + + arguments = {} + + return handler_name, arguments + + +def _prepare_handler_data( + handler_type: str, + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + params: "Optional[Any]" = None, +) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": + """ + Prepare common handler data for both v1 and v2 MCP SDK. + + Args: + handler_type: "tool", "prompt", or "resource" + original_args: Original positional args (v1 path) + original_kwargs: Original keyword args (v1 path) + params: Typed params object from v2 ServerRequestContext path + + Returns: + Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) + """ + if params is not None: + handler_name, arguments = _extract_handler_data_from_params( + handler_type, params + ) + elif _is_v2_context(original_args): + handler_name = "unknown" + arguments = {} + else: + handler_name, arguments = _extract_handler_data_from_args( + handler_type, original_args, original_kwargs + ) + + span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( + handler_type, handler_name + ) + + return ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) + + +async def _handler_wrapper( + handler_type: str, + func: "Callable[..., Any]", + original_args: "tuple[Any, ...]", + original_kwargs: "Optional[dict[str, Any]]" = None, + self: "Optional[Any]" = None, + force_await: bool = True, +) -> "Any": + """ + Wrapper for MCP handlers. + + Args: + handler_type: "tool", "prompt", or "resource" + func: The handler function to wrap + original_args: Original arguments passed to the handler + original_kwargs: Original keyword arguments passed to the handler + self: Optional instance for bound methods + """ + if original_kwargs is None: + original_kwargs = {} + + # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers + ctx: "Optional[Any]" = None + params: "Optional[Any]" = None + if ( + ServerRequestContext is not None + and original_args + and isinstance(original_args[0], ServerRequestContext) + ): + ctx = original_args[0] + params = original_args[1] if len(original_args) > 1 else None + + ( + handler_name, + arguments, + span_data_key, + span_name, + mcp_method_name, + result_data_key, + ) = _prepare_handler_data( + handler_type, original_args, original_kwargs, params=params + ) + + scopes = _get_active_http_scopes(ctx=ctx) + + isolation_scope_context: "ContextManager[Any]" + current_scope_context: "ContextManager[Any]" + + if scopes is None: + isolation_scope_context = nullcontext() + current_scope_context = nullcontext() + else: + isolation_scope, current_scope = scopes + + isolation_scope_context = ( + nullcontext() + if isolation_scope is None + else sentry_sdk.scope.use_isolation_scope(isolation_scope) + ) + current_scope_context = ( + nullcontext() + if current_scope is None + else sentry_sdk.scope.use_scope(current_scope) + ) + + # Get request ID, session ID, and transport from context + request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + + # Start span and execute + with isolation_scope_context: + with current_scope_context: + span_mgr: "Union[Span, StreamedSpan]" + if span_streaming: + span_mgr = sentry_sdk.traces.start_span( + name=span_name, + attributes={ + "sentry.op": OP.MCP_SERVER, + "sentry.origin": MCPIntegration.origin, + }, + ) + else: + span_mgr = get_start_span_function()( + op=OP.MCP_SERVER, + name=span_name, + origin=MCPIntegration.origin, + ) + + with span_mgr as span: + # Set input span data + _set_span_input_data( + span, + handler_name, + span_data_key, + mcp_method_name, + arguments, + request_id, + session_id, + mcp_transport, + ) + + # For resources, extract and set protocol + if handler_type == "resource": + uri = None + if params is not None: + uri = getattr(params, "uri", None) + + # v1 scenario + if ServerRequestContext is None: + if original_args: + uri = original_args[0] + else: + uri = original_kwargs.get("uri") + + protocol = None + if uri is not None and hasattr(uri, "scheme"): + protocol = uri.scheme + elif handler_name and "://" in handler_name: + protocol = handler_name.split("://")[0] + if protocol: + _set_span_data_attribute( + span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol + ) + + try: + # Execute the async handler + if self is not None: + original_args = (self, *original_args) + + result = func(*original_args, **original_kwargs) + if force_await or inspect.isawaitable(result): + result = await result + + except Exception as e: + # Set error flag for tools + if handler_type == "tool": + _set_span_data_attribute( + span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True + ) + sentry_sdk.capture_exception(e) + raise + + _set_span_output_data(span, result, result_data_key, handler_type) + + return result + + +def _create_instrumented_decorator( + original_decorator: "Callable[..., Any]", + handler_type: str, + *decorator_args: "Any", + **decorator_kwargs: "Any", +) -> "Callable[..., Any]": + """ + Create an instrumented version of an MCP decorator. + + This function intercepts MCP decorators (like @server.call_tool()) and injects + Sentry instrumentation into the handler registration flow. The returned decorator + will: + 1. Receive the user's handler function + 2. Pass the instrumented version to the original MCP decorator + + This ensures that when the handler is called at runtime, it's already wrapped + with Sentry spans and metrics collection. + + Args: + original_decorator: The original MCP decorator method (e.g., Server.call_tool) + handler_type: "tool", "prompt", or "resource" - determines span configuration + decorator_args: Positional arguments to pass to the original decorator (e.g., self) + decorator_kwargs: Keyword arguments to pass to the original decorator + + Returns: + A decorator function that instruments handlers before registering them + """ + + def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(func) + async def wrapper(*args: "Any") -> "Any": + return await _handler_wrapper(handler_type, func, args, force_await=False) + + # Then register it with the original MCP decorator + return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) + + return instrumented_decorator + + +_METHOD_TO_HANDLER_TYPE = { + "tools/call": "tool", + "prompts/get": "prompt", + "resources/read": "resource", +} + +# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via +# the Server(...) constructor kwargs rather than add_request_handler. The in-tree +# high-level MCPServer also wires its handlers through these kwargs. +_KWARG_TO_HANDLER_TYPE = { + "on_call_tool": "tool", + "on_get_prompt": "prompt", + "on_read_resource": "resource", +} + + +def _patch_lowlevel_server() -> None: + """ + Patches the mcp.server.lowlevel.Server class to instrument handler execution. + """ + if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): + _patch_lowlevel_server_v2() + else: + _patch_lowlevel_server_v1() + + +def _patch_lowlevel_server_v1() -> None: + """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" + # Patch call_tool decorator + original_call_tool = Server.call_tool # type: ignore[attr-defined] + + def patched_call_tool( + self: "Server", **kwargs: "Any" + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.call_tool that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_call_tool, "tool", self, **kwargs + )(func) + + Server.call_tool = patched_call_tool # type: ignore[attr-defined] + + # Patch get_prompt decorator + original_get_prompt = Server.get_prompt # type: ignore[attr-defined] + + def patched_get_prompt( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.get_prompt that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_get_prompt, "prompt", self + )(func) + + Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] + + # Patch read_resource decorator + original_read_resource = Server.read_resource # type: ignore[attr-defined] + + def patched_read_resource( + self: "Server", + ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": + """Patched version of Server.read_resource that adds Sentry instrumentation.""" + return lambda func: _create_instrumented_decorator( + original_read_resource, "resource", self + )(func) + + Server.read_resource = patched_read_resource # type: ignore[attr-defined] + + +def _wrap_v2_handler( + handler_type: str, handler: "Callable[..., Any]" +) -> "Callable[..., Any]": + """Wrap a v2 (ctx, params) handler with Sentry instrumentation. + + Idempotent: an already-wrapped handler is returned unchanged so handlers + registered through more than one path (e.g. MCPServer building a Server) + are not double-wrapped. + """ + if getattr(handler, "__sentry_mcp_wrapped__", False): + return handler + + @wraps(handler) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + return await _handler_wrapper( + handler_type, handler, args, kwargs, force_await=False + ) + + wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] + return wrapper + + +def _patch_lowlevel_server_v2() -> None: + """Patches the v2 Server to wrap tool/prompt/resource handlers. + + Handlers can be registered either via the Server(...) constructor kwargs + (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree + MCPServer and most lowlevel examples use — or via add_request_handler. + Both are patched. + """ + original_init = Server.__init__ + + @wraps(original_init) + def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: + for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): + handler = kwargs.get(kwarg) + if handler is not None: + kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) + original_init(self, *args, **kwargs) + + Server.__init__ = patched_init # type: ignore[method-assign] + + original_add_request_handler = Server.add_request_handler + + def patched_add_request_handler( + self: "Server", + method: str, + params_type: "Any", + handler: "Callable[..., Any]", + *args: "Any", + **kwargs: "Any", + ) -> None: + handler_type = _METHOD_TO_HANDLER_TYPE.get(method) + if handler_type is not None: + handler = _wrap_v2_handler(handler_type, handler) + + original_add_request_handler( + self, method, params_type, handler, *args, **kwargs + ) + + Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] + + +def _patch_handle_request() -> None: + original_handle_request = StreamableHTTPServerTransport.handle_request + + @wraps(original_handle_request) + async def patched_handle_request( + self: "StreamableHTTPServerTransport", + scope: "Scope", + receive: "Receive", + send: "Send", + ) -> None: + scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( + sentry_sdk.get_isolation_scope() + ) + scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() + await original_handle_request(self, scope, receive, send) + + StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] + + +def _patch_fastmcp() -> None: + """ + Patches the standalone fastmcp package's FastMCP class. + + The standalone fastmcp package (v2.14.0+) registers its own handlers for + prompts and resources directly, bypassing the Server decorators we patch. + This function patches the _get_prompt_mcp and _read_resource_mcp methods + to add instrumentation for those handlers. + """ + if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): + original_get_prompt_mcp = FastMCP._get_prompt_mcp + + @wraps(original_get_prompt_mcp) + async def patched_get_prompt_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "prompt", + original_get_prompt_mcp, + args, + kwargs, + self, + ) + + FastMCP._get_prompt_mcp = patched_get_prompt_mcp + + if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): + original_read_resource_mcp = FastMCP._read_resource_mcp + + @wraps(original_read_resource_mcp) + async def patched_read_resource_mcp( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + return await _handler_wrapper( + "resource", + original_read_resource_mcp, + args, + kwargs, + self, + ) + + FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py new file mode 100644 index 0000000000..b6111492bb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import _get_installed_modules + +if TYPE_CHECKING: + from typing import Any + + from sentry_sdk._types import Event + + +class ModulesIntegration(Integration): + identifier = "modules" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def processor(event: "Event", hint: "Any") -> "Event": + if event.get("type") == "transaction": + return event + + if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: + return event + + event["modules"] = _get_installed_modules() + return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py new file mode 100644 index 0000000000..186c665ed1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py @@ -0,0 +1,1530 @@ +import json +import sys +import time +from collections.abc import Iterable +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import consts +from sentry_sdk.ai._openai_completions_api import ( + _get_system_instructions as _get_system_instructions_completions, +) +from sentry_sdk.ai._openai_completions_api import ( + _get_text_items, + _transform_system_instructions, +) +from sentry_sdk.ai._openai_completions_api import ( + _is_system_instruction as _is_system_instruction_completions, +) +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions as _get_system_instructions_responses, +) +from sentry_sdk.ai._openai_responses_api import ( + _is_system_instruction as _is_system_instruction_responses, +) +from sentry_sdk.ai.monitoring import record_token_usage +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_embedding_inputs, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + reraise, + safe_serialize, +) + +if TYPE_CHECKING: + from typing import ( + Any, + AsyncIterator, + Callable, + Iterable, + Iterator, + List, + Optional, + Union, + ) + + from openai import Omit + from openai.types import CompletionUsage + from openai.types.responses import ( + ResponseInputParam, + ResponseStreamEvent, + SequenceNotStr, + ) + from openai.types.responses.response_usage import ResponseUsage + + from sentry_sdk._types import TextPart + from sentry_sdk.tracing import Span + +try: + try: + from openai import NotGiven + except ImportError: + NotGiven = None + + try: + from openai import Omit + except ImportError: + Omit = None + + from openai import AsyncStream, Stream + from openai.resources import AsyncEmbeddings, Embeddings + from openai.resources.chat.completions import AsyncCompletions, Completions + + if TYPE_CHECKING: + from openai.types.chat import ( + ChatCompletionChunk, + ChatCompletionMessageParam, + ) +except ImportError: + raise DidNotEnable("OpenAI not installed") + +RESPONSES_API_ENABLED = True +try: + # responses API support was introduced in v1.66.0 + from openai.resources.responses import AsyncResponses, Responses + from openai.types.responses.response_completed_event import ResponseCompletedEvent +except ImportError: + RESPONSES_API_ENABLED = False + + +class OpenAIIntegration(Integration): + identifier = "openai" + origin = f"auto.ai.{identifier}" + + def __init__( + self: "OpenAIIntegration", + include_prompts: bool = True, + tiktoken_encoding_name: "Optional[str]" = None, + ) -> None: + self.include_prompts = include_prompts + + self.tiktoken_encoding = None + if tiktoken_encoding_name is not None: + import tiktoken # type: ignore + + self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) + + @staticmethod + def setup_once() -> None: + Completions.create = _wrap_chat_completion_create(Completions.create) + AsyncCompletions.create = _wrap_async_chat_completion_create( + AsyncCompletions.create + ) + + Embeddings.create = _wrap_embeddings_create(Embeddings.create) + AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) + + if RESPONSES_API_ENABLED: + Responses.create = _wrap_responses_create(Responses.create) + AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) + + def count_tokens(self: "OpenAIIntegration", s: str) -> int: + if self.tiktoken_encoding is None: + return 0 + try: + return len(self.tiktoken_encoding.encode_ordinary(s)) + except Exception: + return 0 + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _has_attr_and_is_int( + token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str +) -> bool: + return hasattr(token_usage, attr_name) and isinstance( + getattr(token_usage, attr_name, None), int + ) + + +def _calculate_completions_token_usage( + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + streaming_message_total_token_usage: "Optional[CompletionUsage]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Chat Completions API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + usage = None + + if streaming_message_total_token_usage is not None: + usage = streaming_message_total_token_usage + elif hasattr(response, "usage"): + usage = response.usage + + if usage is not None: + if _has_attr_and_is_int(usage, "prompt_tokens"): + input_tokens = usage.prompt_tokens + if _has_attr_and_is_int(usage, "completion_tokens"): + output_tokens = usage.completion_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "prompt_tokens_details"): + cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "completion_tokens_details"): + reasoning = getattr( + usage.completion_tokens_details, "reasoning_tokens", None + ) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in messages or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "choices") and response.choices is not None: + for choice in response.choices: + if hasattr(choice, "message") and hasattr(choice.message, "content"): + output_tokens += count_tokens(choice.message.content) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _calculate_responses_token_usage( + input: "Any", + response: "Any", + span: "Union[Span, StreamedSpan]", + streaming_message_responses: "Optional[List[str]]", + count_tokens: "Callable[..., Any]", +) -> None: + """Extract and record token usage from a Responses API response.""" + input_tokens: "Optional[int]" = 0 + input_tokens_cached: "Optional[int]" = 0 + output_tokens: "Optional[int]" = 0 + output_tokens_reasoning: "Optional[int]" = 0 + total_tokens: "Optional[int]" = 0 + + if hasattr(response, "usage"): + usage = response.usage + + if _has_attr_and_is_int(usage, "input_tokens"): + input_tokens = usage.input_tokens + if _has_attr_and_is_int(usage, "output_tokens"): + output_tokens = usage.output_tokens + if _has_attr_and_is_int(usage, "total_tokens"): + total_tokens = usage.total_tokens + + if hasattr(usage, "input_tokens_details"): + cached = getattr(usage.input_tokens_details, "cached_tokens", None) + if isinstance(cached, int): + input_tokens_cached = cached + + if hasattr(usage, "output_tokens_details"): + reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) + if isinstance(reasoning, int): + output_tokens_reasoning = reasoning + + # Manually count input tokens + if input_tokens == 0: + for message in input or []: + if isinstance(message, str): + input_tokens += count_tokens(message) + continue + elif isinstance(message, dict): + message_content = message.get("content") + if message_content is None: + continue + # Deliberate use of Completions function for both Completions and Responses input format. + text_items = _get_text_items(message_content) + input_tokens += sum(count_tokens(text) for text in text_items) + continue + + # Manually count output tokens + if output_tokens == 0: + if streaming_message_responses is not None: + for message in streaming_message_responses: + output_tokens += count_tokens(message) + elif hasattr(response, "output"): + for output_item in response.output: + if hasattr(output_item, "content"): + for content_item in output_item.content: + if hasattr(content_item, "text"): + output_tokens += count_tokens(content_item.text) + + # Do not set token data if it is 0 + input_tokens = input_tokens or None + input_tokens_cached = input_tokens_cached or None + output_tokens = output_tokens or None + output_tokens_reasoning = output_tokens_reasoning or None + total_tokens = total_tokens or None + + record_token_usage( + span, + input_tokens=input_tokens, + input_tokens_cached=input_tokens_cached, + output_tokens=output_tokens, + output_tokens_reasoning=output_tokens_reasoning, + total_tokens=total_tokens, + ) + + +def _set_responses_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") + messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_output_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + conversation = kwargs.get("conversation") + if conversation is not None and _is_given(conversation): + conversation_id: "Optional[str]" = None + if isinstance(conversation, str): + conversation_id = conversation + elif isinstance(conversation, dict): + conversation_id = conversation.get("id") + if conversation_id is not None: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + if not should_send_default_pii() or not integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if ( + messages is None + and explicit_instructions is not None + and _is_given(explicit_instructions) + ): + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + [ + { + "type": "text", + "content": explicit_instructions, + } + ] + ), + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + if messages is None: + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + instructions_text_parts: "list[TextPart]" = [] + if explicit_instructions is not None and _is_given(explicit_instructions): + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions_responses(messages) + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + return + + non_system_messages = [ + message for message in messages if not _is_system_instruction_responses(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") + + +def _set_completions_api_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( + "messages" + ) + + tools = kwargs.get("tools") + if tools is not None and _is_given(tools) and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + max_tokens = kwargs.get("max_tokens") + if max_tokens is not None and _is_given(max_tokens): + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + + presence_penalty = kwargs.get("presence_penalty") + if presence_penalty is not None and _is_given(presence_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) + + frequency_penalty = kwargs.get("frequency_penalty") + if frequency_penalty is not None and _is_given(frequency_penalty): + set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) + + temperature = kwargs.get("temperature") + if temperature is not None and _is_given(temperature): + set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + + top_p = kwargs.get("top_p") + if top_p is not None and _is_given(top_p): + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + if isinstance(messages, str): + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + return + + messages = list(messages) + kwargs["messages"] = messages + + system_instructions = _get_system_instructions_completions(messages) + if len(system_instructions) > 0: + set_on_span( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(_transform_system_instructions(system_instructions)), + ) + + non_system_messages = [ + message + for message in messages + if not _is_system_instruction_completions(message) + ] + if len(non_system_messages) > 0: + normalized_messages = normalize_message_roles(non_system_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + +def _set_embeddings_input_data( + span: "Union[Span, StreamedSpan]", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", +) -> None: + messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( + "input" + ) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + model = kwargs.get("model") + if model is not None: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + if ( + not should_send_default_pii() + or not integration.include_prompts + or messages is None + ): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + return + + if isinstance(messages, str): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + normalized_messages = normalize_message_roles([messages]) # type: ignore + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + return + + # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 + if not isinstance(messages, Iterable) or isinstance(messages, dict): + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + return + + messages = list(messages) + kwargs["input"] = messages + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False + ) + + set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") + + +def _set_common_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + input: "Any", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + if hasattr(response, "model"): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) + + # Chat Completions API + if hasattr(response, "choices") and response.choices is not None: + if should_send_default_pii() and integration.include_prompts: + response_text = [ + choice.message.model_dump() + for choice in response.choices + if choice.message is not None + ] + if len(response_text) > 0: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) + + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + # Responses API + elif hasattr(response, "output"): + if should_send_default_pii() and integration.include_prompts: + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in response.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + output_messages["tool"], + unpack=False, + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) + + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + # Embeddings API (fallback for responses with neither choices nor output) + else: + _calculate_completions_token_usage( + messages=input, + response=response, + span=span, + streaming_message_responses=None, + streaming_message_total_token_usage=None, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_synchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + if "messages" not in kwargs: + # invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + try: + iter(kwargs["messages"]) + except TypeError: + # invalid call (in all versions), messages must be iterable + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"chat {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_CHAT, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_CHAT, + name=f"chat {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_completions_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + response._iterator = _wrap_asynchronous_completions_chunk_iterator( + span=span, + integration=integration, + start_time=start_time, + messages=messages, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_completions_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _set_completions_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + messages = kwargs.get("messages") + + if messages is not None and isinstance(messages, str): + messages = [messages] + + _set_common_output_data( + span, + response, + messages, + integration, + finish_span, + ) + + +def _wrap_synchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "Stream[ChatCompletionChunk]", + old_iterator: "Iterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "Iterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_completions_chunk_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + messages: "Optional[Iterable[ChatCompletionMessageParam]]", + response: "AsyncStream[ChatCompletionChunk]", + old_iterator: "AsyncIterator[ChatCompletionChunk]", + finish_span: "bool", +) -> "AsyncIterator[ChatCompletionChunk]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + streaming_message_total_token_usage = None + + async for x in old_iterator: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) + + with capture_internal_exceptions(): + if hasattr(x, "choices") and x.choices is not None: + choice_index = 0 + for choice in x.choices: + if hasattr(choice, "delta") and hasattr(choice.delta, "content"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + content = choice.delta.content + if len(data_buf) <= choice_index: + data_buf.append([]) + data_buf[choice_index].append(content or "") + choice_index += 1 + if hasattr(x, "usage"): + streaming_message_total_token_usage = x.usage + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + all_responses = None + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + _calculate_completions_token_usage( + messages=messages, + response=response, + span=span, + streaming_message_responses=all_responses, + streaming_message_total_token_usage=streaming_message_total_token_usage, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +def _wrap_synchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "Stream[ResponseStreamEvent]", + old_iterator: "Iterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "Iterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + + if finish_span: + span.__exit__(None, None, None) + + +async def _wrap_asynchronous_responses_event_iterator( + span: "Union[Span, StreamedSpan]", + integration: "OpenAIIntegration", + start_time: "Optional[float]", + input: "Optional[Union[str, ResponseInputParam]]", + response: "AsyncStream[ResponseStreamEvent]", + old_iterator: "AsyncIterator[ResponseStreamEvent]", + finish_span: "bool", +) -> "AsyncIterator[ResponseStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. + Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. + """ + ttft: "Optional[float]" = None + data_buf: "list[list[str]]" = [] # one for each choice + + count_tokens_manually = True + async for x in old_iterator: + with capture_internal_exceptions(): + if hasattr(x, "delta"): + if start_time is not None and ttft is None: + ttft = time.perf_counter() - start_time + if len(data_buf) == 0: + data_buf.append([]) + data_buf[0].append(x.delta or "") + + if isinstance(x, ResponseCompletedEvent): + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) + + _calculate_responses_token_usage( + input=input, + response=x.response, + span=span, + streaming_message_responses=None, + count_tokens=integration.count_tokens, + ) + count_tokens_manually = False + + yield x + + with capture_internal_exceptions(): + if ttft is not None: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + ) + if len(data_buf) > 0: + all_responses = ["".join(chunk) for chunk in data_buf] + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) + if count_tokens_manually: + _calculate_responses_token_usage( + input=input, + response=response, + span=span, + streaming_message_responses=all_responses, + count_tokens=integration.count_tokens, + ) + if finish_span: + span.__exit__(None, None, None) + + +def _set_responses_api_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _set_embeddings_output_data( + span: "Union[Span, StreamedSpan]", + response: "Any", + kwargs: "dict[str, Any]", + integration: "OpenAIIntegration", + finish_span: bool = True, +) -> None: + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + _set_common_output_data( + span, + response, + input, + integration, + finish_span, + ) + + +def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) + + return _new_sync_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) + + return await _new_async_chat_completion(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +async def _new_async_embeddings_create( + f: "Any", *args: "Any", **kwargs: "Any" +) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=f"embeddings {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + }, + ) as span: + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + else: + with get_start_span_function()( + op=consts.OP.GEN_AI_EMBEDDINGS, + name=f"embeddings {model}", + origin=OpenAIIntegration.origin, + ) as span: + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + _set_embeddings_input_data(span, kwargs, integration) + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + + _set_embeddings_output_data( + span, response, kwargs, integration, finish_span=False + ) + + return response + + +def _wrap_embeddings_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_embeddings_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_embeddings_create(f, *args, **kwargs) + + return _sentry_patched_create_async + + +def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, Stream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_synchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + model = kwargs.get("model") + + # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 + is_streaming_response = kwargs.get("stream", False) or False + + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces.start_span( + name=f"responses {model}", + attributes={ + "sentry.op": consts.OP.GEN_AI_RESPONSES, + "sentry.origin": OpenAIIntegration.origin, + SPANDATA.GEN_AI_SYSTEM: "openai", + SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, + }, + ) + else: + span = get_start_span_function()( + op=consts.OP.GEN_AI_RESPONSES, + name=f"responses {model}", + origin=OpenAIIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) + + _set_responses_api_input_data(span, kwargs, integration) + + start_time = time.perf_counter() + + try: + response = await f(*args, **kwargs) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + span.__exit__(*exc_info) + reraise(*exc_info) + + # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. + if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): + input = kwargs.get("input") + + if input is not None and isinstance(input, str): + input = [input] + + response._iterator = _wrap_asynchronous_responses_event_iterator( + span=span, + integration=integration, + start_time=start_time, + input=input, + response=response, + old_iterator=response._iterator, + finish_span=True, + ) + else: + _set_responses_api_output_data( + span, response, kwargs, integration, finish_span=True + ) + + return response + + +def _wrap_responses_create(f: "Any") -> "Any": + @wraps(f) + def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) + + return _new_sync_responses_create(f, *args, **kwargs) + + return _sentry_patched_create_sync + + +def _wrap_async_responses_create(f: "Any") -> "Any": + @wraps(f) + async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _new_async_responses_create(f, *args, **kwargs) + + return _sentry_patched_responses_async + + +def _is_given(obj: "Any") -> bool: + """ + Check for givenness safely across different openai versions. + """ + if NotGiven is not None and isinstance(obj, NotGiven): + return False + if Omit is not None and isinstance(obj, Omit): + return False + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py new file mode 100644 index 0000000000..5895f53ad3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py @@ -0,0 +1,250 @@ +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import parse_version + +from .patches import ( + _create_run_streamed_wrapper, + _create_run_wrapper, + _execute_final_output, + _execute_handoffs, + _get_all_tools, + _get_model, + _patch_error_tracing, + _run_single_turn, + _run_single_turn_streamed, +) + +try: + # "agents" is too generic. If someone has an agents.py file in their project + # or another package that's importable via "agents", no ImportError would + # be thrown and the integration would enable itself even if openai-agents is + # not installed. That's why we're adding the second, more specific import + # after it, even if we don't use it. + import agents + from agents.run import AgentRunner + from agents.version import __version__ as OPENAI_AGENTS_VERSION + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +try: + # AgentRunner methods moved in v0.8 + # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 + from agents.run_internal import run_loop, turn_preparation, turn_resolution +except ImportError: + run_loop = None + turn_preparation = None + turn_resolution = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + from agents.run_internal.run_steps import SingleStepResult + + +def _patch_runner() -> None: + # Create the root span for one full agent run (including eventual handoffs) + # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around + # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. + agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run + ) + + # Patch streaming runner + agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( + agents.run.DEFAULT_AGENT_RUNNER.run_streamed + ) + + +class OpenAIAgentsIntegration(Integration): + """ + NOTE: With version 0.8.0, the class methods below have been refactored to functions. + - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` + - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` + - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` + - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` + - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` + + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. + 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. + - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. + - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. + 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. + - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. + - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. + 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. + + Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. + Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. + An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. + Handoffs are a way to switch agent-wide configuration. + - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` + """ + + identifier = "openai_agents" + + @staticmethod + def setup_once() -> None: + _patch_error_tracing() + _patch_runner() + + library_version = parse_version(OPENAI_AGENTS_VERSION) + if library_version is not None and library_version >= ( + 0, + 8, + ): + if run_loop is not None: + + @wraps(run_loop.get_all_tools) + async def new_wrapped_get_all_tools( + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools( + run_loop.get_all_tools, agent, context_wrapper + ) + + agents.run.get_all_tools = new_wrapped_get_all_tools + + @wraps(run_loop.run_single_turn) + async def new_wrapped_run_single_turn( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn( + run_loop.run_single_turn, *args, **kwargs + ) + + agents.run.run_single_turn = new_wrapped_run_single_turn + + @wraps(run_loop.run_single_turn_streamed) + async def new_wrapped_run_single_turn_streamed( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + run_loop.run_single_turn_streamed, *args, **kwargs + ) + + agents.run.run_single_turn_streamed = ( + new_wrapped_run_single_turn_streamed + ) + + if turn_preparation is not None: + + @wraps(turn_preparation.get_model) + def new_wrapped_get_model( + agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(turn_preparation.get_model, agent, run_config) + + agents.run_internal.run_loop.get_model = new_wrapped_get_model + + if turn_resolution is not None: + original_execute_handoffs = turn_resolution.execute_handoffs + + @wraps(original_execute_handoffs) + async def new_wrapped_execute_handoffs( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs( + original_execute_handoffs, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_handoffs = ( + new_wrapped_execute_handoffs + ) + + original_execute_final_output = turn_resolution.execute_final_output + + @wraps(turn_resolution.execute_final_output) + async def new_wrapped_final_output( + *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents.run_internal.turn_resolution.execute_final_output = ( + new_wrapped_final_output + ) + + return + + original_get_all_tools = AgentRunner._get_all_tools + + @wraps(AgentRunner._get_all_tools.__func__) + async def old_wrapped_get_all_tools( + cls: "agents.Runner", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", + ) -> "list[agents.Tool]": + return await _get_all_tools(original_get_all_tools, agent, context_wrapper) + + agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) + + original_get_model = AgentRunner._get_model + + @wraps(AgentRunner._get_model.__func__) + def old_wrapped_get_model( + cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" + ) -> "agents.Model": + return _get_model(original_get_model, agent, run_config) + + agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) + + original_run_single_turn = AgentRunner._run_single_turn + + @wraps(AgentRunner._run_single_turn.__func__) + async def old_wrapped_run_single_turn( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn(original_run_single_turn, *args, **kwargs) + + agents.run.AgentRunner._run_single_turn = classmethod( + old_wrapped_run_single_turn + ) + + original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed + + @wraps(AgentRunner._run_single_turn_streamed.__func__) + async def old_wrapped_run_single_turn_streamed( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _run_single_turn_streamed( + original_run_single_turn_streamed, *args, **kwargs + ) + + agents.run.AgentRunner._run_single_turn_streamed = classmethod( + old_wrapped_run_single_turn_streamed + ) + + original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs + + @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) + async def old_wrapped_execute_handoffs( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) + + agents._run_impl.RunImpl.execute_handoffs = classmethod( + old_wrapped_execute_handoffs + ) + + original_execute_final_output = agents._run_impl.RunImpl.execute_final_output + + @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) + async def old_wrapped_final_output( + cls: "agents.Runner", *args: "Any", **kwargs: "Any" + ) -> "SingleStepResult": + return await _execute_final_output( + original_execute_final_output, *args, **kwargs + ) + + agents._run_impl.RunImpl.execute_final_output = classmethod( + old_wrapped_final_output + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py new file mode 100644 index 0000000000..f5de978be0 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py new file mode 100644 index 0000000000..85d48f2d41 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py @@ -0,0 +1,10 @@ +from .agent_run import ( + _execute_final_output, # noqa: F401 + _execute_handoffs, # noqa: F401 + _run_single_turn, # noqa: F401 + _run_single_turn_streamed, # noqa: F401 +) +from .error_tracing import _patch_error_tracing # noqa: F401 +from .models import _get_model # noqa: F401 +from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 +from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py new file mode 100644 index 0000000000..71883b2eef --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py @@ -0,0 +1,332 @@ +import sys +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import ( + handoff_span, + invoke_agent_span, + update_invoke_agent_span, +) + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable, Optional, Union + + from agents.run_internal.run_steps import SingleStepResult + + from sentry_sdk.tracing import Span + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: + """Check if there's an active agent span for this context""" + return getattr(context_wrapper, "_sentry_current_agent", None) is not None + + +def _get_current_agent( + context_wrapper: "agents.RunContextWrapper", +) -> "Optional[agents.Agent]": + """Get the current agent from context wrapper""" + return getattr(context_wrapper, "_sentry_current_agent", None) + + +def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: + """Close the workflow span for streaming executions if it exists.""" + if agent and hasattr(agent, "_sentry_workflow_span"): + workflow_span = agent._sentry_workflow_span + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + +def _maybe_start_agent_span( + context_wrapper: "agents.RunContextWrapper", + agent: "agents.Agent", + should_run_agent_start_hooks: bool, + span_kwargs: "dict[str, Any]", + is_streaming: bool = False, +) -> "Optional[Union[Span, StreamedSpan]]": + """ + Start an agent invocation span if conditions are met. + Handles ending any existing span for a different agent. + + Returns the new span if started, or the existing span if conditions aren't met. + """ + if not (should_run_agent_start_hooks and agent and context_wrapper): + return getattr(context_wrapper, "_sentry_agent_span", None) + + # End any existing span for a different agent + if _has_active_agent_span(context_wrapper): + current_agent = _get_current_agent(context_wrapper) + if current_agent and current_agent != agent: + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + # Store the agent on the context wrapper so we can access it later + context_wrapper._sentry_current_agent = agent + span = invoke_agent_span(context_wrapper, agent, span_kwargs) + context_wrapper._sentry_agent_span = span + agent._sentry_agent_span = span + + if not is_streaming: + return span + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + return span + + +async def _run_single_turn( + original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn that + - creates agent invocation spans if there is no already active agent invocation span. + - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. + """ + # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. + bindings = kwargs.get("bindings") + agent = ( + getattr(bindings, "public_agent", None) + if bindings is not None + else kwargs.get("agent") + ) + context_wrapper = kwargs.get("context_wrapper") + should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) + + span = _maybe_start_agent_span( + context_wrapper, agent, should_run_agent_start_hooks, kwargs + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn(*args, **kwargs) + + try: + result = await original_run_single_turn(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + return result + + +async def _run_single_turn_streamed( + original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched _run_single_turn_streamed that + - creates agent invocation spans for streaming if there is no already active agent invocation span. + - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. + + Note: Unlike _run_single_turn which uses keyword-only arguments (*,), + _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: + _run_single_turn_streamed( + streamed_result, # args[0] + bindings, # args[1] + hooks, # args[2] + context_wrapper, # args[3] + run_config, # args[4] + should_run_agent_start_hooks, # args[5] + tool_use_tracker, # args[6] + all_tools, # args[7] + server_conversation_tracker, # args[8] (optional) + ) + """ + streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") + # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. + agent_or_bindings = ( + args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) + ) + agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) + context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") + should_run_agent_start_hooks = bool( + args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) + ) + + span_kwargs: "dict[str, Any]" = {} + if streamed_result and hasattr(streamed_result, "input"): + span_kwargs["original_input"] = streamed_result.input + + span = _maybe_start_agent_span( + context_wrapper, + agent, + should_run_agent_start_hooks, + span_kwargs, + is_streaming=True, + ) + + if ( + span is None + or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) + or (not isinstance(span, StreamedSpan) and span.timestamp is not None) + ): + return await original_run_single_turn_streamed(*args, **kwargs) + + try: + result = await original_run_single_turn_streamed(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + return result + + +async def _execute_handoffs( + original_execute_handoffs: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_handoffs that + - creates and manages handoff spans. + - ends the agent invocation span. + - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. + """ + + context_wrapper = kwargs.get("context_wrapper") + run_handoffs = kwargs.get("run_handoffs") + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + + # Create Sentry handoff span for the first handoff (agents library only processes the first one) + if run_handoffs: + first_handoff = run_handoffs[0] + handoff_agent_name = first_handoff.handoff.agent_name + handoff_span(context_wrapper, agent, handoff_agent_name) + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + # Call original method with all parameters + try: + return await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + reraise(*exc_info) + + # Call original method with all parameters + try: + result = await original_execute_handoffs(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result + + +async def _execute_final_output( + original_execute_final_output: "Callable[..., SingleStepResult]", + *args: "Any", + **kwargs: "Any", +) -> "SingleStepResult": + """ + Patched execute_final_output that + - ends the agent invocation span. + - ends the workflow span if the response is streamed. + """ + + # openai-agents >= 0.14 renamed `agent` to `public_agent`. + agent = kwargs.get("public_agent", kwargs.get("agent")) + context_wrapper = kwargs.get("context_wrapper") + final_output = kwargs.get("final_output") + + if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): + try: + return await original_execute_final_output(*args, **kwargs) + finally: + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + + try: + result = await original_execute_final_output(*args, **kwargs) + except Exception: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) + _close_streaming_workflow_span(agent) + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + + span = getattr(context_wrapper, "_sentry_agent_span", None) + if span: + update_invoke_agent_span( + span=span, context=context_wrapper, agent=agent, output=final_output + ) + span.__exit__(None, None, None) + delattr(context_wrapper, "_sentry_agent_span") + + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py new file mode 100644 index 0000000000..68dadb3101 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py @@ -0,0 +1,74 @@ +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANSTATUS +from sentry_sdk.traces import SpanStatus +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if TYPE_CHECKING: + from typing import Any + + +def _patch_error_tracing() -> None: + """ + Patches agents error tracing function to inject our span error logic + when a tool execution fails. + + In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span + In older versions, it was at: agents._utils.attach_error_to_current_span + + This works even when the module or function doesn't exist. + """ + error_tracing_module = None + + # Try newer location first (agents.util._error_tracing) + try: + from agents.util import _error_tracing + + error_tracing_module = _error_tracing + except (ImportError, AttributeError): + pass + + # Try older location (agents._utils) + if error_tracing_module is None: + try: + import agents._utils + + error_tracing_module = agents._utils + except (ImportError, AttributeError): + # Module doesn't exist in either location, nothing to patch + return + + # Check if the function exists + if not hasattr(error_tracing_module, "attach_error_to_current_span"): + return + + original_attach_error = error_tracing_module.attach_error_to_current_span + + @wraps(original_attach_error) + def sentry_attach_error_to_current_span( + error: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Wraps agents' error attachment to also set Sentry span status to error. + This allows us to properly track tool execution errors even though + the agents library swallows exceptions. + """ + # Set the current Sentry span to errored + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + current_span = sentry_sdk.get_current_scope().streamed_span + if current_span is not None: + current_span.status = SpanStatus.ERROR + else: + current_span = sentry_sdk.get_current_span() + if current_span is not None: + current_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # Call the original function + return original_attach_error(error, *args, **kwargs) + + error_tracing_module.attach_error_to_current_span = ( + sentry_attach_error_to_current_span + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py new file mode 100644 index 0000000000..634c9fdca1 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py @@ -0,0 +1,197 @@ +import copy +import time +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_sentry_baggage_to_headers, + should_propagate_trace, +) +from sentry_sdk.utils import logger + +from ..spans import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + from sentry_sdk.tracing import Span + +try: + import agents + from agents.tool import HostedMCPTool +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _set_response_model_on_agent_span( + agent: "agents.Agent", response_model: "Optional[str]" +) -> None: + """Set the response model on the agent's invoke_agent span if available.""" + if response_model: + agent_span = getattr(agent, "_sentry_agent_span", None) + if agent_span: + if isinstance(agent_span, StreamedSpan): + agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + else: + agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + +def _inject_trace_propagation_headers( + hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" +) -> None: + headers = hosted_tool.tool_config.get("headers") + if headers is None: + headers = {} + hosted_tool.tool_config["headers"] = headers + + mcp_url = hosted_tool.tool_config.get("server_url") + if not mcp_url: + return + + if should_propagate_trace(sentry_sdk.get_client(), mcp_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( + key=key, value=value, mcp_url=mcp_url + ) + ) + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(headers, value) + else: + headers[key] = value + + +def _get_model( + original_get_model: "Callable[..., agents.Model]", + agent: "agents.Agent", + run_config: "agents.RunConfig", +) -> "agents.Model": + """ + Responsible for + - creating and managing AI client spans. + - adding trace propagation headers to tools with type HostedMCPTool. + - setting the response model on agent invocation spans. + """ + # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) + # because we only patch its direct methods, all underlying data can remain unchanged. + model = copy.copy(original_get_model(agent, run_config)) + + # Capture the request model name for spans (agent.model can be None when using defaults) + request_model_name = model.model if hasattr(model, "model") else str(model) + agent._sentry_request_model = request_model_name + + # Wrap _fetch_response if it exists (for OpenAI models) to capture response model + if hasattr(model, "_fetch_response"): + original_fetch_response = model._fetch_response + + @wraps(original_fetch_response) + async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": + response = await original_fetch_response(*args, **kwargs) + if hasattr(response, "model") and response.model: + agent._sentry_response_model = str(response.model) + return response + + model._fetch_response = wrapped_fetch_response + + original_get_response = model.get_response + + @wraps(original_get_response) + async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": + mcp_tools = kwargs.get("tools") + hosted_tools = [] + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + result = await original_get_response(*args, **kwargs) + + # Get response model captured from _fetch_response and clean up + response_model = getattr(agent, "_sentry_response_model", None) + if response_model: + delattr(agent, "_sentry_response_model") + + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span(span, result, response_model, agent) + + return result + + model.get_response = wrapped_get_response + + # Also wrap stream_response for streaming support + if hasattr(model, "stream_response"): + original_stream_response = model.stream_response + + @wraps(original_stream_response) + async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": + span_kwargs = dict(kwargs) + if len(args) > 0: + span_kwargs["system_instructions"] = args[0] + if len(args) > 1: + span_kwargs["input"] = args[1] + + hosted_tools = [] + if len(args) > 3: + mcp_tools = args[3] + + if mcp_tools is not None: + hosted_tools = [ + tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) + ] + + with ai_client_span(agent, span_kwargs) as span: + for hosted_tool in hosted_tools: + _inject_trace_propagation_headers(hosted_tool, span=span) + + set_on_span = ( + span.set_attribute + if isinstance(span, StreamedSpan) + else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + + streaming_response = None + ttft_recorded = False + # Capture start time locally to avoid race conditions with concurrent requests + start_time = time.perf_counter() + + async for event in original_stream_response(*args, **kwargs): + # Detect first content token (text delta event) + if not ttft_recorded and hasattr(event, "delta"): + ttft = time.perf_counter() - start_time + set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) + ttft_recorded = True + + # Capture the full response from ResponseCompletedEvent + if hasattr(event, "response"): + streaming_response = event.response + yield event + + # Update span with response data (usage, output, model) + if streaming_response: + response_model = ( + str(streaming_response.model) + if hasattr(streaming_response, "model") + and streaming_response.model + else None + ) + _set_response_model_on_agent_span(agent, response_model) + update_ai_client_span( + span, streaming_response, response_model, agent + ) + + model.stream_response = wrapped_stream_response + + return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py new file mode 100644 index 0000000000..5f9996595f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py @@ -0,0 +1,221 @@ +import sys +from functools import wraps + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import agent_workflow_span, update_invoke_agent_span +from ..utils import _capture_exception + +try: + from agents.exceptions import AgentsException +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, AsyncIterator, Callable + + +def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run methods to + - create and manage a root span for the agent workflow runs. + - end the agent invocation span if an `AgentsException` is raised in `run()`. + + Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), + so it does not need to be wrapped separately. + """ + + @wraps(original_func) + async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + with agent_workflow_span(agent) as workflow_span: + # Set conversation ID on workflow span early so it's captured even on errors + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + run_result = await original_func(*args, **kwargs) + except AgentsException as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + + context_wrapper = getattr(exc.run_data, "context_wrapper", None) + if context_wrapper is not None: + invoke_agent_span = getattr( + context_wrapper, "_sentry_agent_span", None + ) + + if invoke_agent_span is not None and ( + ( + isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.end_timestamp is None + ) + or ( + not isinstance(invoke_agent_span, StreamedSpan) + and invoke_agent_span.timestamp is None + ) + ): + update_invoke_agent_span( + span=invoke_agent_span, + context=context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(*exc_info) + delattr(context_wrapper, "_sentry_agent_span") + reraise(*exc_info) + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Invoke agent span is not finished in this case. + # This is much less likely to occur than other cases because + # AgentRunner.run() is "just" a while loop around _run_single_turn. + _capture_exception(exc) + reraise(*exc_info) + + invoke_agent_span = getattr( + run_result.context_wrapper, "_sentry_agent_span", None + ) + if not invoke_agent_span: + return run_result + + update_invoke_agent_span( + span=invoke_agent_span, + context=run_result.context_wrapper, + agent=agent, + ) + + invoke_agent_span.__exit__(None, None, None) + delattr(run_result.context_wrapper, "_sentry_agent_span") + return run_result + + return wrapper + + +def _create_run_streamed_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps the agents.Runner.run_streamed method to + - create a root span for streaming agent workflow runs. + - end the workflow span if and only if the response stream is consumed or cancelled. + + Unlike run(), run_streamed() returns immediately with a RunResultStreaming object + while execution continues in a background task. The workflow span must stay open + throughout the streaming operation and close when streaming completes or is abandoned. + + Note: We don't use isolation_scope() here because it uses context variables that + cannot span async boundaries (the __enter__ and __exit__ would be called from + different async contexts, causing ValueError). + """ + + @wraps(original_func) + def wrapper(*args: "Any", **kwargs: "Any") -> "Any": + # Clone agent because agent invocation spans are attached per run. + if "starting_agent" in kwargs: + agent = kwargs["starting_agent"].clone() + else: + agent = args[0].clone() + + # Capture conversation_id from kwargs if provided + conversation_id = kwargs.get("conversation_id") + if conversation_id: + agent._sentry_conversation_id = conversation_id + + # Start workflow span immediately (before run_streamed returns) + workflow_span = agent_workflow_span(agent) + workflow_span.__enter__() + + # Set conversation ID on workflow span early so it's captured even on errors + if conversation_id: + if isinstance(workflow_span, StreamedSpan): + workflow_span.set_attribute( + SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id + ) + else: + workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) + + # Store span on agent for cleanup + agent._sentry_workflow_span = workflow_span + + if "starting_agent" in kwargs: + kwargs["starting_agent"] = agent + else: + args = (agent, *args[1:]) + + try: + # Call original function to get RunResultStreaming + run_result = original_func(*args, **kwargs) + except Exception as exc: + # If run_streamed itself fails (not the background task), clean up immediately + workflow_span.__exit__(*sys.exc_info()) + _capture_exception(exc) + raise + + def _close_workflow_span() -> None: + if hasattr(agent, "_sentry_workflow_span"): + workflow_span.__exit__(*sys.exc_info()) + delattr(agent, "_sentry_workflow_span") + + if hasattr(run_result, "stream_events"): + original_stream_events = run_result.stream_events + + @wraps(original_stream_events) + async def wrapped_stream_events( + *stream_args: "Any", **stream_kwargs: "Any" + ) -> "AsyncIterator[Any]": + try: + async for event in original_stream_events( + *stream_args, **stream_kwargs + ): + yield event + finally: + _close_workflow_span() + + run_result.stream_events = wrapped_stream_events + + if hasattr(run_result, "cancel"): + original_cancel = run_result.cancel + + @wraps(original_cancel) + def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": + try: + return original_cancel(*cancel_args, **cancel_kwargs) + finally: + _close_workflow_span() + + run_result.cancel = wrapped_cancel + + return run_result + + return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py new file mode 100644 index 0000000000..bd13b9d61a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py @@ -0,0 +1,70 @@ +from functools import wraps +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import execute_tool_span, update_execute_tool_span + +if TYPE_CHECKING: + from typing import Any, Awaitable, Callable + +try: + import agents +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +async def _get_all_tools( + original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", + agent: "agents.Agent", + context_wrapper: "agents.RunContextWrapper", +) -> "list[agents.Tool]": + """ + Responsible for creating and managing `gen_ai.execute_tool` spans. + """ + # Get the original tools + tools = await original_get_all_tools(agent, context_wrapper) + + wrapped_tools = [] + for tool in tools: + # Wrap only the function tools (for now) + if tool.__class__.__name__ != "FunctionTool": + wrapped_tools.append(tool) + continue + + # Create a new FunctionTool with our wrapped invoke method + original_on_invoke = tool.on_invoke_tool + + def create_wrapped_invoke( + current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" + ) -> "Callable[..., Any]": + @wraps(current_on_invoke) + async def sentry_wrapped_on_invoke_tool( + *args: "Any", **kwargs: "Any" + ) -> "Any": + with execute_tool_span(current_tool, *args, **kwargs) as span: + # We can not capture exceptions in tool execution here because + # `_on_invoke_tool` is swallowing the exception here: + # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 + # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter + # I was unable to monkey patch it because those are evaluated at module import time + # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` + # because it is nested inside this import time code. As if they made it hard to patch on purpose... + result = await current_on_invoke(*args, **kwargs) + update_execute_tool_span(span, agent, current_tool, result) + + return result + + return sentry_wrapped_on_invoke_tool + + wrapped_tool = agents.FunctionTool( + name=tool.name, + description=tool.description, + params_json_schema=tool.params_json_schema, + on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), + strict_json_schema=tool.strict_json_schema, + is_enabled=tool.is_enabled, + ) + wrapped_tools.append(wrapped_tool) + + return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py new file mode 100644 index 0000000000..08802a87a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py @@ -0,0 +1,8 @@ +from .agent_workflow import agent_workflow_span # noqa: F401 +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .handoff import handoff_span # noqa: F401 +from .invoke_agent import ( + invoke_agent_span, # noqa: F401 + update_invoke_agent_span, # noqa: F401 +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py new file mode 100644 index 0000000000..758f06db8d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + from typing import Union + + import agents + + +def agent_workflow_span( + agent: "agents.Agent", +) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": + # Create a transaction or a span if an transaction is already active + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} + ) + + return span + + span = get_start_span_function()( + name=f"{agent.name} workflow", + origin=SPAN_ORIGIN, + ) + + return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py new file mode 100644 index 0000000000..f4f02cb674 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_input_data, + _set_output_data, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from agents import Agent + + +def ai_client_span( + agent: "Agent", get_response_kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + # TODO-anton: implement other types of operations. Now "chat" is hardcoded. + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + + _set_agent_data(span, agent) + _set_input_data(span, get_response_kwargs) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + response: "Any", + response_model: "Optional[str]" = None, + agent: "Optional[Agent]" = None, +) -> None: + """Update AI client span with response data (works for streaming and non-streaming).""" + if hasattr(response, "usage") and response.usage: + _set_usage_data(span, response.usage) + + if hasattr(response, "output") and response.output: + _set_output_data(span, response) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if response_model is not None: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + elif hasattr(response, "model") and response.model: + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) + + # Set conversation ID from agent if available + if agent: + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py new file mode 100644 index 0000000000..fd3a430951 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -0,0 +1,82 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def execute_tool_span( + tool: "agents.Tool", *args: "Any", **kwargs: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool.name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool.name, + SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool.name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) + + set_on_span = span.set_data + + if should_send_default_pii(): + input = args[1] + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + agent: "agents.Agent", + tool: "agents.Tool", + result: "Any", +) -> None: + _set_agent_data(span, agent) + + if isinstance(result, str) and result.startswith( + "An error occurred while running the tool" + ): + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if should_send_default_pii(): + set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py new file mode 100644 index 0000000000..ea91464afb --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +from ..consts import SPAN_ORIGIN + +if TYPE_CHECKING: + import agents + + +def handoff_span( + context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str +) -> None: + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"handoff from {from_agent.name} to {to_agent_name}", + attributes={ + "sentry.op": OP.GEN_AI_HANDOFF, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "handoff", + }, + ) as span: + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + with sentry_sdk.start_span( + op=OP.GEN_AI_HANDOFF, + name=f"handoff from {from_agent.name} to {to_agent_name}", + origin=SPAN_ORIGIN, + ) as span: + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") + + # Add conversation ID from agent + conv_id = getattr(from_agent, "_sentry_conversation_id", None) + if conv_id: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py new file mode 100644 index 0000000000..c21145ac4a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -0,0 +1,122 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _set_usage_data + +if TYPE_CHECKING: + from typing import Any, Union + + import agents + + +def invoke_agent_span( + context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {agent.name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + start_span_function = get_start_span_function() + span = start_span_function( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {agent.name}", + origin=SPAN_ORIGIN, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + if should_send_default_pii(): + messages = [] + if agent.instructions: + message = ( + agent.instructions + if isinstance(agent.instructions, str) + else safe_serialize(agent.instructions) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "system", + } + ) + + original_input = kwargs.get("original_input") + if original_input is not None: + message = ( + original_input + if isinstance(original_input, str) + else safe_serialize(original_input) + ) + messages.append( + { + "content": [{"text": message, "type": "text"}], + "role": "user", + } + ) + + if len(messages) > 0: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + _set_agent_data(span, agent) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + context: "agents.RunContextWrapper", + agent: "agents.Agent", + output: "Any" = None, +) -> None: + # Add aggregated usage data from context_wrapper + if hasattr(context, "usage"): + _set_usage_data(span, context.usage) + + if should_send_default_pii(): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) + + # Add conversation ID from agent + conv_id = getattr(agent, "_sentry_conversation_id", None) + if conv_id: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) + else: + span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py new file mode 100644 index 0000000000..224a5f66ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py @@ -0,0 +1,244 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai._openai_completions_api import _transform_system_instructions +from sentry_sdk.ai._openai_responses_api import ( + _get_system_instructions, + _is_system_instruction, +) +from sentry_sdk.ai.utils import ( + GEN_AI_ALLOWED_MESSAGE_ROLES, + normalize_message_role, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import should_truncate_gen_ai_input +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Union + + from agents import TResponseInputItem, Usage + + from sentry_sdk._types import TextPart + +try: + import agents + +except ImportError: + raise DidNotEnable("OpenAI Agents not installed") + + +def _capture_exception(exc: "Any") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "openai_agents", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + set_on_span( + SPANDATA.GEN_AI_SYSTEM, "openai" + ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. + + set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) + + if agent.model_settings.max_tokens: + set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) + + # Get model name from agent.model or fall back to request model (for when agent.model is None/default) + model_name = None + if agent.model: + model_name = agent.model.model if hasattr(agent.model, "model") else agent.model + elif hasattr(agent, "_sentry_request_model"): + model_name = agent._sentry_request_model + + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + if agent.model_settings.presence_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + agent.model_settings.presence_penalty, + ) + + if agent.model_settings.temperature: + set_on_span( + SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature + ) + + if agent.model_settings.top_p: + set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) + + if agent.model_settings.frequency_penalty: + set_on_span( + SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + agent.model_settings.frequency_penalty, + ) + + if len(agent.tools) > 0: + set_on_span( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + safe_serialize([vars(tool) for tool in agent.tools]), + ) + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" +) -> None: + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, + usage.input_tokens_details.cached_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + set_on_span( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, + usage.output_tokens_details.reasoning_tokens, + ) + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) + + +def _set_input_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + get_response_kwargs: "dict[str, Any]", +) -> None: + if not should_send_default_pii(): + return + request_messages = [] + + messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) + + instructions_text_parts: "list[TextPart]" = [] + explicit_instructions = get_response_kwargs.get("system_instructions") + if explicit_instructions is not None: + instructions_text_parts.append( + { + "type": "text", + "content": explicit_instructions, + } + ) + + system_instructions = _get_system_instructions(messages) + + # Deliberate use of function accepting completions API type because + # of shared structure FOR THIS PURPOSE ONLY. + instructions_text_parts += _transform_system_instructions(system_instructions) + + if len(instructions_text_parts) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps(instructions_text_parts), + ) + + non_system_messages = [ + message for message in messages if not _is_system_instruction(message) + ] + for message in non_system_messages: + if "role" in message: + normalized_role = normalize_message_role(message.get("role")) # type: ignore + content = message.get("content") # type: ignore + request_messages.append( + { + "role": normalized_role, + "content": ( + [{"type": "text", "text": content}] + if isinstance(content, str) + else content + ), + } + ) + else: + if message.get("type") == "function_call": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, + "content": [message], + } + ) + elif message.get("type") == "function_call_output": # type: ignore + request_messages.append( + { + "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, + "content": [message], + } + ) + + normalized_messages = normalize_message_roles(request_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, + ) + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + if not should_send_default_pii(): + return + + output_messages: "dict[str, list[Any]]" = { + "response": [], + "tool": [], + } + + for output in result.output: + if output.type == "function_call": + output_messages["tool"].append(output.dict()) + elif output.type == "message": + for output_message in output.content: + try: + output_messages["response"].append(output_message.text) + except AttributeError: + # Unknown output message type, just return the json + output_messages["response"].append(output_message.dict()) + + if len(output_messages["tool"]) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + safe_serialize(output_messages["tool"]), + ) + + if len(output_messages["response"]) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py new file mode 100644 index 0000000000..281604fe38 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from openfeature import api + from openfeature.hook import Hook + + if TYPE_CHECKING: + from openfeature.hook import HookContext, HookHints +except ImportError: + raise DidNotEnable("OpenFeature is not installed") + + +class OpenFeatureIntegration(Integration): + identifier = "openfeature" + + @staticmethod + def setup_once() -> None: + # Register the hook within the global openfeature hooks list. + api.add_hooks(hooks=[OpenFeatureHook()]) + + +class OpenFeatureHook(Hook): + def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: + if isinstance(details.value, bool): + add_feature_flag(details.flag_key, details.value) + + def error( + self, hook_context: "HookContext", exception: Exception, hints: "HookHints" + ) -> None: + if isinstance(hook_context.default_value, bool): + add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py new file mode 100644 index 0000000000..d76b8058ee --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py @@ -0,0 +1,7 @@ +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor + +__all__ = [ + "SentryPropagator", + "SentrySpanProcessor", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py new file mode 100644 index 0000000000..d6733036ea --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py @@ -0,0 +1,9 @@ +from sentry_sdk.integrations import DidNotEnable + +try: + from opentelemetry.context import create_key +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +SENTRY_TRACE_KEY = create_key("sentry-trace") +SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py new file mode 100644 index 0000000000..472e8181f9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py @@ -0,0 +1,81 @@ +""" +IMPORTANT: The contents of this file are part of a proof of concept and as such +are experimental and not suitable for production use. They may be changed or +removed at any time without prior notice. +""" + +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Dict, Optional + +try: + from opentelemetry import trace + from opentelemetry.propagate import set_global_textmap + from opentelemetry.sdk.trace import TracerProvider +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +try: + from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] + DjangoInstrumentor, + ) +except ImportError: + DjangoInstrumentor = None + + +CONFIGURABLE_INSTRUMENTATIONS = { + DjangoInstrumentor: {"is_sql_commentor_enabled": True}, +} + + +class OpenTelemetryIntegration(Integration): + identifier = "opentelemetry" + + @staticmethod + def setup_once() -> None: + pass + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if has_span_streaming_enabled(options): + logger.warning( + "[OTel] OpenTelemetryIntegration is not compatible with span streaming " + "(trace_lifecycle='stream') and will be disabled." + ) + return + + warnings.warn( + "OpenTelemetryIntegration is deprecated. " + "Please use OTLPIntegration instead: " + "https://docs.sentry.io/platforms/python/integrations/otlp/", + DeprecationWarning, + stacklevel=2, + ) + + _setup_sentry_tracing() + # _setup_instrumentors() + + logger.debug("[OTel] Finished setting up OpenTelemetry integration") + + +def _setup_sentry_tracing() -> None: + provider = TracerProvider() + provider.add_span_processor(SentrySpanProcessor()) + trace.set_tracer_provider(provider) + set_global_textmap(SentryPropagator()) + + +def _setup_instrumentors() -> None: + for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): + if instrumentor is None: + continue + instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py new file mode 100644 index 0000000000..5b5f13861d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py @@ -0,0 +1,128 @@ +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.integrations.opentelemetry.span_processor import ( + SentrySpanProcessor, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data + +try: + from opentelemetry import trace + from opentelemetry.context import ( + Context, + get_current, + set_value, + ) + from opentelemetry.propagators.textmap import ( + CarrierT, + Getter, + Setter, + TextMapPropagator, + default_getter, + default_setter, + ) + from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional, Set + + +class SentryPropagator(TextMapPropagator): + """ + Propagates tracing headers for Sentry's tracing system in a way OTel understands. + """ + + def extract( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + getter: "Getter[CarrierT]" = default_getter, + ) -> "Context": + if context is None: + context = get_current() + + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + if not sentry_trace: + return context + + sentrytrace = extract_sentrytrace_data(sentry_trace[0]) + if not sentrytrace: + return context + + context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) + + trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] + + span_context = SpanContext( + trace_id=int(trace_id, 16), # type: ignore + span_id=int(span_id, 16), # type: ignore + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags=TraceFlags(TraceFlags.SAMPLED), + is_remote=True, + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + if baggage_header: + baggage = Baggage.from_incoming_header(baggage_header[0]) + else: + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + baggage = Baggage(sentry_items={}) + + baggage.freeze() + context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) + + span = NonRecordingSpan(span_context) + modified_context = trace.set_span_in_context(span, context) + return modified_context + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + if context is None: + context = get_current() + + current_span = trace.get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + span_id = trace.format_span_id(current_span_context.span_id) + + span_map = SentrySpanProcessor.otel_span_map + sentry_span = span_map.get(span_id, None) + if not sentry_span: + return + + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) + + if sentry_span.containing_transaction: + baggage = sentry_span.containing_transaction.get_baggage() + if baggage: + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + @property + def fields(self) -> "Set[str]": + return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py new file mode 100644 index 0000000000..1efd2ac455 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -0,0 +1,407 @@ +from datetime import datetime, timezone +from time import time +from typing import TYPE_CHECKING, cast + +from urllib3.util import parse_url as urlparse + +from sentry_sdk import get_client, start_transaction +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.integrations.opentelemetry.consts import ( + SENTRY_BAGGAGE_KEY, + SENTRY_TRACE_KEY, +) +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing import Transaction + +try: + from opentelemetry.context import get_value + from opentelemetry.sdk.trace import ReadableSpan as OTelSpan + from opentelemetry.sdk.trace import SpanProcessor + from opentelemetry.semconv.trace import SpanAttributes + from opentelemetry.trace import ( + SpanKind, + format_span_id, + format_trace_id, + get_current_span, + ) + from opentelemetry.trace.span import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + ) +except ImportError: + raise DidNotEnable("opentelemetry not installed") + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from opentelemetry import context as context_api + from opentelemetry.trace import SpanContext + + from sentry_sdk._types import Event, Hint + +OPEN_TELEMETRY_CONTEXT = "otel" +SPAN_MAX_TIME_OPEN_MINUTES = 10 +SPAN_ORIGIN = "auto.otel" + + +def link_trace_context_to_error_event( + event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" +) -> "Event": + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return event + + if hasattr(event, "type") and event["type"] == "transaction": + return event + + otel_span = get_current_span() + if not otel_span: + return event + + ctx = otel_span.get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return event + + sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) + if not sentry_span: + return event + + contexts = event.setdefault("contexts", {}) + contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) + + return event + + +class SentrySpanProcessor(SpanProcessor): + """ + Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. + """ + + # The mapping from otel span ids to sentry spans + otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} + + # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES + open_spans: "dict[int, set[str]]" = {} + + initialized: "bool" = False + + def __new__(cls) -> "SentrySpanProcessor": + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + + # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). + return cls.instance # type: ignore[misc] + + def __init__(self) -> None: + if self.initialized: + return + + @add_global_event_processor + def global_event_processor(event: "Event", hint: "Hint") -> "Event": + return link_trace_context_to_error_event(event, self.otel_span_map) + + self.initialized = True + + def _prune_old_spans(self: "SentrySpanProcessor") -> None: + """ + Prune spans that have been open for too long. + """ + current_time_minutes = int(time() / 60) + for span_start_minutes in list( + self.open_spans.keys() + ): # making a list because we change the dict + # prune empty open spans buckets + if self.open_spans[span_start_minutes] == set(): + self.open_spans.pop(span_start_minutes) + + # prune old buckets + elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: + for span_id in self.open_spans.pop(span_start_minutes): + self.otel_span_map.pop(span_id, None) + + def on_start( + self, + otel_span: "OTelSpan", + parent_context: "Optional[context_api.Context]" = None, + ) -> None: + client = get_client() + + if not client.parsed_dsn: + return + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + if self._is_sentry_span(otel_span): + return + + trace_data = self._get_trace_data( + span_context, otel_span.parent, parent_context + ) + + parent_span_id = trace_data["parent_span_id"] + sentry_parent_span = ( + self.otel_span_map.get(parent_span_id) if parent_span_id else None + ) + + start_timestamp = None + if otel_span.start_time is not None: + start_timestamp = datetime.fromtimestamp( + otel_span.start_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span = None + if sentry_parent_span: + sentry_span = sentry_parent_span.start_child( + span_id=trace_data["span_id"], + name=otel_span.name, + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + else: + sentry_span = start_transaction( + name=otel_span.name, + span_id=trace_data["span_id"], + parent_span_id=parent_span_id, + trace_id=trace_data["trace_id"], + baggage=trace_data["baggage"], + start_timestamp=start_timestamp, + instrumenter=INSTRUMENTER.OTEL, + origin=SPAN_ORIGIN, + ) + + self.otel_span_map[trace_data["span_id"]] = sentry_span + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).add( + trace_data["span_id"] + ) + + self._prune_old_spans() + + def on_end(self, otel_span: "OTelSpan") -> None: + client = get_client() + + if client.options["instrumenter"] != INSTRUMENTER.OTEL: + return + + span_context = otel_span.get_span_context() + if span_context is None or not span_context.is_valid: + return + + span_id = format_span_id(span_context.span_id) + sentry_span = self.otel_span_map.pop(span_id, None) + if not sentry_span: + return + + sentry_span.op = otel_span.name + + self._update_span_with_otel_status(sentry_span, otel_span) + + if isinstance(sentry_span, Transaction): + sentry_span.name = otel_span.name + sentry_span.set_context( + OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) + ) + self._update_transaction_with_otel_data(sentry_span, otel_span) + + else: + self._update_span_with_otel_data(sentry_span, otel_span) + + end_timestamp = None + if otel_span.end_time is not None: + end_timestamp = datetime.fromtimestamp( + otel_span.end_time / 1e9, timezone.utc + ) # OTel spans have nanosecond precision + + sentry_span.finish(end_timestamp=end_timestamp) + + if otel_span.start_time is not None: + span_start_in_minutes = int( + otel_span.start_time / 1e9 / 60 + ) # OTel spans have nanosecond precision + self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) + + self._prune_old_spans() + + def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: + """ + Break infinite loop: + HTTP requests to Sentry are caught by OTel and send again to Sentry. + """ + otel_span_url = None + if otel_span.attributes is not None: + otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) + otel_span_url = cast("Optional[str]", otel_span_url) + + parsed_dsn = get_client().parsed_dsn + dsn_url = parsed_dsn.netloc if parsed_dsn else None + + if otel_span_url and dsn_url and dsn_url in otel_span_url: + return True + + return False + + def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": + """ + Returns the OTel context for Sentry. + See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context + """ + ctx = {} + + if otel_span.attributes: + ctx["attributes"] = dict(otel_span.attributes) + + if otel_span.resource.attributes: + ctx["resource"] = dict(otel_span.resource.attributes) + + return ctx + + def _get_trace_data( + self, + span_context: "SpanContext", + parent_span_context: "Optional[SpanContext]", + parent_context: "Optional[context_api.Context]", + ) -> "dict[str, Any]": + """ + Extracts tracing information from one OTel span's context and its parent OTel context. + """ + trace_data: "dict[str, Any]" = {} + + span_id = format_span_id(span_context.span_id) + trace_data["span_id"] = span_id + + trace_id = format_trace_id(span_context.trace_id) + trace_data["trace_id"] = trace_id + + parent_span_id = ( + format_span_id(parent_span_context.span_id) if parent_span_context else None + ) + trace_data["parent_span_id"] = parent_span_id + + sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) + sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) + trace_data["parent_sampled"] = ( + sentry_trace_data["parent_sampled"] if sentry_trace_data else None + ) + + baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) + trace_data["baggage"] = baggage + + return trace_data + + def _update_span_with_otel_status( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Set the Sentry span status from the OTel span + """ + if otel_span.status.is_unset: + return + + if otel_span.status.is_ok: + sentry_span.set_status(SPANSTATUS.OK) + return + + sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) + + def _update_span_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + """ + Convert OTel span data and update the Sentry span with it. + This should eventually happen on the server when ingesting the spans. + """ + sentry_span.set_data("otel.kind", otel_span.kind) + + op = otel_span.name + description = otel_span.name + + if otel_span.attributes is not None: + for key, val in otel_span.attributes.items(): + sentry_span.set_data(key, val) + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + http_method = cast("Optional[str]", http_method) + + db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) + + if http_method: + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + description = http_method + + peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) + if peer_name: + description += " {}".format(peer_name) + + target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) + if target: + description += " {}".format(target) + + if not peer_name and not target: + url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) + url = cast("Optional[str]", url) + if url: + parsed_url = urlparse(url) + url = "{}://{}{}".format( + parsed_url.scheme, parsed_url.netloc, parsed_url.path + ) + description += " {}".format(url) + + status_code = otel_span.attributes.get( + SpanAttributes.HTTP_STATUS_CODE, None + ) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + elif db_query: + op = "db" + statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) + statement = cast("Optional[str]", statement) + if statement: + description = statement + + sentry_span.op = op + sentry_span.description = description + + def _update_transaction_with_otel_data( + self, sentry_span: "SentrySpan", otel_span: "OTelSpan" + ) -> None: + if otel_span.attributes is None: + return + + http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) + + if http_method: + status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + status_code = cast("Optional[int]", status_code) + if status_code: + sentry_span.set_http_status(status_code) + + op = "http" + + if otel_span.kind == SpanKind.SERVER: + op += ".server" + elif otel_span.kind == SpanKind.CLIENT: + op += ".client" + + sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py new file mode 100644 index 0000000000..b91f2cfd21 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py @@ -0,0 +1,223 @@ +from sentry_sdk import capture_event, get_client +from sentry_sdk.consts import VERSION, EndpointType +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import register_external_propagation_context +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + event_from_exception, + logger, +) + +try: + from opentelemetry.context import ( + Context, + get_current, + get_value, + ) + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.propagate import set_global_textmap + from opentelemetry.propagators.textmap import ( + CarrierT, + Setter, + default_setter, + ) + from opentelemetry.sdk.trace import Span, TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + SpanContext, + format_span_id, + format_trace_id, + get_current_span, + get_tracer_provider, + set_tracer_provider, + ) + + from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY + from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator +except ImportError: + raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Tuple + + +def otel_propagation_context() -> "Optional[Tuple[str, str]]": + """ + Get the (trace_id, span_id) from opentelemetry if exists. + """ + ctx = get_current_span().get_span_context() + + if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: + return None + + return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) + + +def setup_otlp_traces_exporter( + dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None +) -> None: + tracer_provider = get_tracer_provider() + + if not isinstance(tracer_provider, TracerProvider): + logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") + tracer_provider = TracerProvider() + set_tracer_provider(tracer_provider) + + endpoint = None + headers = None + if collector_url: + endpoint = collector_url + logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") + elif dsn: + auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") + endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) + headers = {"X-Sentry-Auth": auth.to_header()} + logger.debug(f"[OTLP] Sending traces to {endpoint}") + + otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + + +_sentry_patched_exception = False + + +def setup_capture_exceptions() -> None: + """ + Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. + """ + global _sentry_patched_exception + _original_record_exception = Span.record_exception + + if _sentry_patched_exception: + return + + def _sentry_patched_record_exception( + self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration and otlp_integration.capture_exceptions: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=get_client().options, + mechanism={"type": OTLPIntegration.identifier, "handled": False}, + ) + capture_event(event, hint=hint) + + _original_record_exception(self, exception, *args, **kwargs) + + Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] + _sentry_patched_exception = True + + +class SentryOTLPPropagator(SentryPropagator): + """ + We need to override the inject of the older propagator since that + is SpanProcessor based. + + !!! Note regarding baggage: + We cannot meaningfully populate a new baggage as a head SDK + when we are using OTLP since we don't have any sort of transaction semantic to + track state across a group of spans. + + For incoming baggage, we just pass it on as is so that case is correctly handled. + """ + + def inject( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + setter: "Setter[CarrierT]" = default_setter, + ) -> None: + otlp_integration = get_client().get_integration(OTLPIntegration) + if otlp_integration is None: + return + + if context is None: + context = get_current() + + current_span = get_current_span(context) + current_span_context = current_span.get_span_context() + + if not current_span_context.is_valid: + return + + sentry_trace = _to_traceparent(current_span_context) + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) + + baggage = get_value(SENTRY_BAGGAGE_KEY, context) + if baggage is not None and isinstance(baggage, Baggage): + baggage_data = baggage.serialize() + if baggage_data: + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + + +def _to_traceparent(span_context: "SpanContext") -> str: + """ + Helper method to generate the sentry-trace header. + """ + span_id = format_span_id(span_context.span_id) + trace_id = format_trace_id(span_context.trace_id) + sampled = span_context.trace_flags.sampled + + return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" + + +class OTLPIntegration(Integration): + """ + Automatically setup OTLP ingestion from the DSN. + + :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. + Set to False to setup the TracerProvider manually. + :param collector_url: URL of your own OpenTelemetry collector, defaults to None. + When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. + :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. + Set to False to configure propagators manually or to disable propagation. + :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. + Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. + """ + + identifier = "otlp" + + def __init__( + self, + setup_otlp_traces_exporter: bool = True, + collector_url: "Optional[str]" = None, + setup_propagator: bool = True, + capture_exceptions: bool = False, + ) -> None: + self.setup_otlp_traces_exporter = setup_otlp_traces_exporter + self.collector_url = collector_url + self.setup_propagator = setup_propagator + self.capture_exceptions = capture_exceptions + + @staticmethod + def setup_once() -> None: + logger.debug("[OTLP] Setting up trace linking for all events") + register_external_propagation_context(otel_propagation_context) + + def setup_once_with_options( + self, options: "Optional[Dict[str, Any]]" = None + ) -> None: + if self.setup_otlp_traces_exporter: + logger.debug("[OTLP] Setting up OTLP exporter") + dsn: "Optional[str]" = options.get("dsn") if options else None + setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) + + if self.setup_propagator: + logger.debug("[OTLP] Setting up propagator for distributed tracing") + # TODO-neel better propagator support, chain with existing ones if possible instead of replacing + set_global_textmap(SentryOTLPPropagator()) + + setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py new file mode 100644 index 0000000000..569e3e1fa5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py @@ -0,0 +1,134 @@ +import ast +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk import serializer +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import iter_stacks, walk_exception_chain + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, List, Optional, Tuple + + from sentry_sdk._types import Event, Hint + +try: + from executing import Source +except ImportError: + raise DidNotEnable("executing is not installed") + +try: + from pure_eval import Evaluator +except ImportError: + raise DidNotEnable("pure_eval is not installed") + +try: + # Used implicitly, just testing it's available + import asttokens # noqa +except ImportError: + raise DidNotEnable("asttokens is not installed") + + +class PureEvalIntegration(Integration): + identifier = "pure_eval" + + @staticmethod + def setup_once() -> None: + @add_global_event_processor + def add_executing_info( + event: "Event", hint: "Optional[Hint]" + ) -> "Optional[Event]": + if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + sentry_frame["vars"] = ( + pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] + ) + return event + + +def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": + source = Source.for_frame(frame) + if not source.tree: + return {} + + statements = source.statements_at_line(frame.f_lineno) + if not statements: + return {} + + scope = stmt = list(statements)[0] + while True: + # Get the parent first in case the original statement is already + # a function definition, e.g. if we're calling a decorator + # In that case we still want the surrounding scope, not that function + scope = scope.parent + if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): + break + + evaluator = Evaluator.from_frame(frame) + expressions = evaluator.interesting_expressions_grouped(scope) + + def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": + # Prioritise expressions with a node closer to the statement executed + # without being after that statement + # A higher return value is better - the expression will appear + # earlier in the list of values and is less likely to be trimmed + nodes, _value = expression + + def start(n: "ast.expr") -> "Tuple[int, int]": + return (n.lineno, n.col_offset) + + nodes_before_stmt = [ + node for node in nodes if start(node) < stmt.last_token.end + ] + if nodes_before_stmt: + # The position of the last node before or in the statement + return max(start(node) for node in nodes_before_stmt) + else: + # The position of the first node after the statement + # Negative means it's always lower priority than nodes that come before + # Less negative means closer to the statement and higher priority + lineno, col_offset = min(start(node) for node in nodes) + return (-lineno, -col_offset) + + # This adds the first_token and last_token attributes to nodes + atok = source.asttokens() + + expressions.sort(key=closeness, reverse=True) + vars = { + atok.get_text(nodes[0]): value + for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] + } + return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py new file mode 100644 index 0000000000..81e7cf8090 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -0,0 +1,187 @@ +import functools + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import capture_internal_exceptions, parse_version + +try: + import pydantic_ai # type: ignore # noqa: F401 + from pydantic_ai import Agent +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +from importlib.metadata import PackageNotFoundError, version +from typing import TYPE_CHECKING + +from .patches import ( + _patch_agent_run, + _patch_graph_nodes, + _patch_tool_execution, +) +from .spans.ai_client import ai_client_span, update_ai_client_span + +if TYPE_CHECKING: + from typing import Any + + from pydantic_ai import ModelRequestContext, RunContext + from pydantic_ai.capabilities import Hooks # type: ignore + from pydantic_ai.messages import ModelResponse # type: ignore + + +def register_hooks(hooks: "Hooks") -> None: + """ + Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. + """ + + @hooks.on.before_model_request # type: ignore + async def on_request( + ctx: "RunContext[None]", request_context: "ModelRequestContext" + ) -> "ModelRequestContext": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return request_context + + span = ai_client_span( + messages=request_context.messages, + agent=None, + model=request_context.model, + model_settings=request_context.model_settings, + ) + + run_context_metadata["_sentry_span"] = span + span.__enter__() + + return request_context + + @hooks.on.after_model_request # type: ignore + async def on_response( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + response: "ModelResponse", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + if not isinstance(run_context_metadata, dict): + return response + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + return response + + update_ai_client_span(span, response) + span.__exit__(None, None, None) + + return response + + @hooks.on.model_request_error # type: ignore + async def on_error( + ctx: "RunContext[None]", + *, + request_context: "ModelRequestContext", + error: "Exception", + ) -> "ModelResponse": + run_context_metadata = ctx.metadata + + if not isinstance(run_context_metadata, dict): + raise error + + span = run_context_metadata.pop("_sentry_span", None) + if span is None: + raise error + + with capture_internal_exceptions(): + span.__exit__(type(error), error, error.__traceback__) + + raise error + + original_init = Agent.__init__ + + @functools.wraps(original_init) + def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: + caps = list(kwargs.get("capabilities") or []) + caps.append(hooks) + kwargs["capabilities"] = caps + + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {} # Used as shared reference between hooks + + return original_init(self, *args, **kwargs) + + Agent.__init__ = patched_init + + +class PydanticAIIntegration(Integration): + """ + Typical interaction with the library: + 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. + 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. + - Each run invocation has `RunContext` objects that are passed to the library hooks. + 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. + + Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. + Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). + The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. + + The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that + instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. + """ + + identifier = "pydantic_ai" + origin = f"auto.ai.{identifier}" + using_request_hooks = False + + def __init__( + self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True + ) -> None: + """ + Initialize the Pydantic AI integration. + + Args: + include_prompts: Whether to include prompts and messages in span data. + Requires send_default_pii=True. Defaults to True. + handled_tool_exceptions: Capture tool call exceptions that Pydantic AI + internally prevents from bubbling up. + """ + self.include_prompts = include_prompts + self.handled_tool_call_exceptions = handled_tool_call_exceptions + + @staticmethod + def setup_once() -> None: + """ + Set up the pydantic-ai integration. + + This patches the key methods in pydantic-ai to create Sentry spans for: + - Agent invocations (Agent.run methods) + - Model requests (AI client calls) + - Tool executions + """ + _patch_agent_run() + _patch_tool_execution() + + PydanticAIIntegration.using_request_hooks = False + try: + PYDANTIC_AI_VERSION = version("pydantic-ai-slim") + except PackageNotFoundError: + return + + PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) + if PYDANTIC_AI_VERSION is None: + return + + # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c + if PYDANTIC_AI_VERSION < ( + 1, + 73, + ): + _patch_graph_nodes() + return + + try: + from pydantic_ai.capabilities import Hooks + except ImportError: + return + + PydanticAIIntegration.using_request_hooks = True + hooks = Hooks() + register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py new file mode 100644 index 0000000000..afa66dc47d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py @@ -0,0 +1 @@ +SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py new file mode 100644 index 0000000000..d0ea6242b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py @@ -0,0 +1,3 @@ +from .agent_run import _patch_agent_run # noqa: F401 +from .graph_nodes import _patch_graph_nodes # noqa: F401 +from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py new file mode 100644 index 0000000000..3039e40698 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py @@ -0,0 +1,198 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import invoke_agent_span, update_invoke_agent_span +from ..utils import _capture_exception, pop_agent, push_agent + +try: + from pydantic_ai.agent import Agent # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Union + + +class _StreamingContextManagerWrapper: + """Wrapper for streaming methods that return async context managers.""" + + def __init__( + self, + agent: "Any", + original_ctx_manager: "Any", + user_prompt: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = True, + ) -> None: + self.agent = agent + self.original_ctx_manager = original_ctx_manager + self.user_prompt = user_prompt + self.model = model + self.model_settings = model_settings + self.is_streaming = is_streaming + self._isolation_scope: "Any" = None + self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None + self._result: "Any" = None + + async def __aenter__(self) -> "Any": + # Set up isolation scope and invoke_agent span + self._isolation_scope = sentry_sdk.isolation_scope() + self._isolation_scope.__enter__() + + # Create invoke_agent span (will be closed in __aexit__) + self._span = invoke_agent_span( + self.user_prompt, + self.agent, + self.model, + self.model_settings, + self.is_streaming, + ) + self._span.__enter__() + + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur + push_agent(self.agent, self.is_streaming) + + # Enter the original context manager + result = await self.original_ctx_manager.__aenter__() + self._result = result + return result + + async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: + try: + # Exit the original context manager first + await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) + + # Update span with result if successful + if exc_type is None and self._result and self._span is not None: + update_invoke_agent_span(self._span, self._result) + finally: + # Pop agent from contextvar stack + pop_agent() + + # Clean up invoke span + if self._span: + self._span.__exit__(exc_type, exc_val, exc_tb) + + # Clean up isolation scope + if self._isolation_scope: + self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) + + +def _create_run_wrapper( + original_func: "Callable[..., Any]", is_streaming: bool = False +) -> "Callable[..., Any]": + """ + Wraps the Agent.run method to create an invoke_agent span. + + Args: + original_func: The original run method + is_streaming: Whether this is a streaming method (for future use) + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Isolate each workflow so that when agents are run in asyncio tasks they + # don't touch each other's scopes + with sentry_sdk.isolation_scope(): + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Create invoke_agent span + with invoke_agent_span( + user_prompt, self, model, model_settings, is_streaming + ) as span: + # Push agent to contextvar stack after span is successfully created and entered + # This ensures proper pairing with pop_agent() in finally even if exceptions occur + push_agent(self, is_streaming) + + try: + result = await original_func(self, *args, **kwargs) + + # Update span with result + update_invoke_agent_span(span, result) + + return result + except Exception as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc) + reraise(*exc_info) + finally: + # Pop agent from contextvar stack + pop_agent() + + return wrapper + + +def _create_streaming_wrapper( + original_func: "Callable[..., Any]", +) -> "Callable[..., Any]": + """ + Wraps run_stream method that returns an async context manager. + """ + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) # Required to avoid circular import + + @wraps(original_func) + def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + # Extract parameters for the span + user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) + model = kwargs.get("model") + model_settings = kwargs.get("model_settings") + + if PydanticAIIntegration.using_request_hooks: + metadata = kwargs.get("metadata") + if metadata is None: + kwargs["metadata"] = {"_sentry_span": None} + + # Call original function to get the context manager + original_ctx_manager = original_func(self, *args, **kwargs) + + # Wrap it with our instrumentation + return _StreamingContextManagerWrapper( + agent=self, + original_ctx_manager=original_ctx_manager, + user_prompt=user_prompt, + model=model, + model_settings=model_settings, + is_streaming=True, + ) + + return wrapper + + +def _patch_agent_run() -> None: + """ + Patches the Agent run methods to create spans for agent execution. + + This patches both non-streaming (run, run_sync) and streaming + (run_stream, run_stream_events) methods. + """ + + # Store original methods + original_run = Agent.run + original_run_stream = Agent.run_stream + + # Wrap and apply patches for non-streaming methods + Agent.run = _create_run_wrapper(original_run, is_streaming=False) + + # Wrap and apply patches for streaming methods + Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py new file mode 100644 index 0000000000..afb10395f4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py @@ -0,0 +1,106 @@ +from contextlib import asynccontextmanager +from functools import wraps + +from sentry_sdk.integrations import DidNotEnable + +from ..spans import ( + ai_client_span, + update_ai_client_span, +) + +try: + from pydantic_ai._agent_graph import ModelRequestNode # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + +def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": + """Extract common data needed for creating chat spans. + + Returns: + Tuple of (messages, model, model_settings) + """ + # Extract model and settings from context + model = None + model_settings = None + if hasattr(ctx, "deps"): + model = getattr(ctx.deps, "model", None) + model_settings = getattr(ctx.deps, "model_settings", None) + + # Build full message list: history + current request + messages = [] + if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): + messages.extend(ctx.state.message_history) + + current_request = getattr(node, "request", None) + if current_request: + messages.append(current_request) + + return messages, model, model_settings + + +def _patch_graph_nodes() -> None: + """ + Patches the graph node execution to create appropriate spans. + + ModelRequestNode -> Creates ai_client span for model requests + CallToolsNode -> Handles tool calls (spans created in tool patching) + """ + + # Patch ModelRequestNode to create ai_client spans + original_model_request_run = ModelRequestNode.run + + @wraps(original_model_request_run) + async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + with ai_client_span(messages, None, model, model_settings) as span: + result = await original_model_request_run(self, ctx) + + # Extract response from result if available + model_response = None + if hasattr(result, "model_response"): + model_response = result.model_response + + update_ai_client_span(span, model_response) + return result + + ModelRequestNode.run = wrapped_model_request_run + + # Patch ModelRequestNode.stream for streaming requests + original_model_request_stream = ModelRequestNode.stream + + def create_wrapped_stream( + original_stream_method: "Callable[..., Any]", + ) -> "Callable[..., Any]": + """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" + + @asynccontextmanager + @wraps(original_stream_method) + async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": + messages, model, model_settings = _extract_span_data(self, ctx) + + # Create chat span for streaming request + with ai_client_span(messages, None, model, model_settings) as span: + # Call the original stream method + async with original_stream_method(self, ctx) as stream: + yield stream + + # After streaming completes, update span with response data + # The ModelRequestNode stores the final response in _result + model_response = None + if hasattr(self, "_result") and self._result is not None: + # _result is a NextNode containing the model_response + if hasattr(self._result, "model_response"): + model_response = self._result.model_response + + update_ai_client_span(span, model_response) + + return wrapped_model_request_stream + + ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py new file mode 100644 index 0000000000..5646b5d47c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py @@ -0,0 +1,177 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable +from sentry_sdk.utils import capture_internal_exceptions, reraise + +from ..spans import execute_tool_span, update_execute_tool_span +from ..utils import _capture_exception, get_current_agent + +if TYPE_CHECKING: + from typing import Any + +try: + try: + from pydantic_ai.tool_manager import ToolManager # type: ignore + except ImportError: + from pydantic_ai._tool_manager import ToolManager # type: ignore + + from pydantic_ai.exceptions import ToolRetryError # type: ignore +except ImportError: + raise DidNotEnable("pydantic-ai not installed") + + +def _patch_tool_execution() -> None: + if hasattr(ToolManager, "execute_tool_call"): + _patch_execute_tool_call() + + elif hasattr(ToolManager, "_call_tool"): + # older versions + _patch_call_tool() + + +def _patch_execute_tool_call() -> None: + original_execute_tool_call = ToolManager.execute_tool_call + + @wraps(original_execute_tool_call) + async def wrapped_execute_tool_call( + self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + if not validated or not hasattr(validated, "call"): + return await original_execute_tool_call(self, validated, *args, **kwargs) + + # Extract tool info before calling original + call = validated.call + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_execute_tool_call( + self, + validated, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + return await original_execute_tool_call(self, validated, *args, **kwargs) + + ToolManager.execute_tool_call = wrapped_execute_tool_call + + +def _patch_call_tool() -> None: + """ + Patch ToolManager._call_tool to create execute_tool spans. + + This is the single point where ALL tool calls flow through in pydantic_ai, + regardless of toolset type (function, MCP, combined, wrapper, etc.). + + By patching here, we avoid: + - Patching multiple toolset classes + - Dealing with signature mismatches from instrumented MCP servers + - Complex nested toolset handling + """ + original_call_tool = ToolManager._call_tool + + @wraps(original_call_tool) + async def wrapped_call_tool( + self: "Any", call: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Extract tool info before calling original + name = call.tool_name + tool = self.tools.get(name) if self.tools else None + selected_tool_definition = getattr(tool, "tool_def", None) + + # Get agent from contextvar + agent = get_current_agent() + + if agent and tool: + try: + args_dict = call.args_as_dict() + except Exception: + args_dict = call.args if isinstance(call.args, dict) else {} + + # Create execute_tool span + # Nesting is handled by isolation_scope() to ensure proper parent-child relationships + with sentry_sdk.isolation_scope(): + with execute_tool_span( + name, + args_dict, + agent, + tool_definition=selected_tool_definition, + ) as span: + try: + result = await original_call_tool( + self, + call, + *args, + **kwargs, + ) + update_execute_tool_span(span, result) + return result + except ToolRetryError as exc: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + # Avoid circular import due to multi-file integration structure + from sentry_sdk.integrations.pydantic_ai import ( + PydanticAIIntegration, + ) + + integration = sentry_sdk.get_client().get_integration( + PydanticAIIntegration + ) + if ( + integration is not None + and integration.handled_tool_call_exceptions + ): + _capture_exception(exc, handled=True) + reraise(*exc_info) + + # No span context - just call original + return await original_call_tool( + self, + call, + *args, + **kwargs, + ) + + ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py new file mode 100644 index 0000000000..574046d645 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py @@ -0,0 +1,3 @@ +from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 +from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 +from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py new file mode 100644 index 0000000000..27deb0c55c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -0,0 +1,331 @@ +import json +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _get_model_name, + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, + get_current_agent, + get_is_streaming, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, + _set_usage_data, +) + +if TYPE_CHECKING: + from typing import Any, Dict, List, Union + + from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore + + from sentry_sdk._types import TextPart as SentryTextPart + +try: + from pydantic_ai.messages import ( + BaseToolCallPart, + BaseToolReturnPart, + BinaryContent, + ImageUrl, + SystemPromptPart, + TextPart, + ThinkingPart, + UserPromptPart, + ) +except ImportError: + # Fallback if these classes are not available + BaseToolCallPart = None + BaseToolReturnPart = None + SystemPromptPart = None + UserPromptPart = None + TextPart = None + ThinkingPart = None + BinaryContent = None + ImageUrl = None + + +def _transform_system_instructions( + permanent_instructions: "list[SystemPromptPart]", + current_instructions: "list[str]", +) -> "list[SentryTextPart]": + text_parts: "list[SentryTextPart]" = [ + { + "type": "text", + "content": instruction.content, + } + for instruction in permanent_instructions + ] + + text_parts.extend( + { + "type": "text", + "content": instruction, + } + for instruction in current_instructions + ) + + return text_parts + + +def _get_system_instructions( + messages: "list[ModelMessage]", +) -> "tuple[list[SystemPromptPart], list[str]]": + permanent_instructions = [] + current_instructions = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + if SystemPromptPart and isinstance(part, SystemPromptPart): + permanent_instructions.append(part) + + if hasattr(msg, "instructions") and msg.instructions is not None: + current_instructions.append(msg.instructions) + + return permanent_instructions, current_instructions + + +def _set_input_messages( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" +) -> None: + """Set input messages data on a span.""" + if not _should_send_prompts(): + return + + if not messages: + return + + permanent_instructions, current_instructions = _get_system_instructions(messages) + if len(permanent_instructions) > 0 or len(current_instructions) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + else: + span.set_data( + SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, + json.dumps( + _transform_system_instructions( + permanent_instructions, current_instructions + ) + ), + ) + + try: + formatted_messages = [] + + for msg in messages: + if hasattr(msg, "parts"): + for part in msg.parts: + role = "user" + # Use isinstance checks with proper base classes + if SystemPromptPart and isinstance(part, SystemPromptPart): + continue + elif ( + (TextPart and isinstance(part, TextPart)) + or (ThinkingPart and isinstance(part, ThinkingPart)) + or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) + ): + role = "assistant" + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + role = "tool" + + content: "List[Dict[str, Any] | str]" = [] + tool_calls = None + tool_call_id = None + + # Handle ToolCallPart (assistant requesting tool use) + if BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = {} + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + if tool_call_data: + tool_calls = [tool_call_data] + # Handle ToolReturnPart (tool result) + elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): + if hasattr(part, "tool_name"): + tool_call_id = part.tool_name + if hasattr(part, "content"): + content.append({"type": "text", "text": str(part.content)}) + # Handle regular content + elif hasattr(part, "content"): + if isinstance(part.content, str): + content.append({"type": "text", "text": part.content}) + elif isinstance(part.content, list): + for item in part.content: + if isinstance(item, str): + content.append({"type": "text", "text": item}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + else: + content.append(safe_serialize(item)) + else: + content.append({"type": "text", "text": str(part.content)}) + # Add message if we have content or tool calls + if content or tool_calls: + message: "Dict[str, Any]" = {"role": role} + if content: + message["content"] = content + if tool_calls: + message["tool_calls"] = tool_calls + if tool_call_id: + message["tool_call_id"] = tool_call_id + formatted_messages.append(message) + + if formatted_messages: + normalized_messages = normalize_message_roles(formatted_messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + except Exception: + # If we fail to format messages, just skip it + pass + + +def _set_output_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" +) -> None: + """Set output data on a span.""" + if not _should_send_prompts(): + return + + if not response: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + + try: + # Extract text from ModelResponse + if hasattr(response, "parts"): + texts = [] + tool_calls = [] + + for part in response.parts: + if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): + texts.append(part.content) + elif BaseToolCallPart and isinstance(part, BaseToolCallPart): + tool_call_data = { + "type": "function", + } + if hasattr(part, "tool_name"): + tool_call_data["name"] = part.tool_name + if hasattr(part, "args"): + tool_call_data["arguments"] = safe_serialize(part.args) + tool_calls.append(tool_call_data) + + if texts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) + + if tool_calls: + set_on_span( + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) + ) + + except Exception: + # If we fail to format output, just skip it + pass + + +def ai_client_span( + messages: "Any", agent: "Any", model: "Any", model_settings: "Any" +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for an AI client call (model request). + + Args: + messages: Full conversation history (list of messages) + agent: Agent object + model: Model object + model_settings: Model settings + """ + # Determine model name for span name + model_obj = model + if agent and hasattr(agent, "model"): + model_obj = agent.model + + model_name = _get_model_name(model_obj) or "unknown" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"chat {model_name}", + attributes={ + "sentry.op": OP.GEN_AI_CHAT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_CHAT, + name=f"chat {model_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") + # Set streaming flag from contextvar + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + + # Add available tools if agent is available + agent_obj = agent or get_current_agent() + _set_available_tools(span, agent_obj) + + # Set input messages (full conversation history) + if messages: + _set_input_messages(span, messages) + + return span + + +def update_ai_client_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" +) -> None: + """Update the AI client span with response data.""" + if not span: + return + + # Set usage data if available + if model_response and hasattr(model_response, "usage"): + _set_usage_data(span, model_response.usage) + + # Set output data + _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py new file mode 100644 index 0000000000..7648c1418a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import safe_serialize + +from ..consts import SPAN_ORIGIN +from ..utils import _set_agent_data, _should_send_prompts + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from pydantic_ai._tool_manager import ToolDefinition # type: ignore + + +def execute_tool_span( + tool_name: str, + tool_args: "Any", + agent: "Any", + tool_definition: "Optional[ToolDefinition]" = None, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for tool execution. + + Args: + tool_name: The name of the tool being executed + tool_args: The arguments passed to the tool + agent: The agent executing the tool + tool_definition: The definition of the tool, if available + """ + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"execute_tool {tool_name}", + attributes={ + "sentry.op": OP.GEN_AI_EXECUTE_TOOL, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: tool_name, + }, + ) + + set_on_span = span.set_attribute + else: + span = sentry_sdk.start_span( + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {tool_name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") + span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) + + set_on_span = span.set_data + + if tool_definition is not None and hasattr(tool_definition, "description"): + set_on_span( + SPANDATA.GEN_AI_TOOL_DESCRIPTION, + tool_definition.description, + ) + + _set_agent_data(span, agent) + + if _should_send_prompts() and tool_args is not None: + set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) + + return span + + +def update_execute_tool_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" +) -> None: + """Update the execute tool span with the result.""" + if not span: + return + + if not _should_send_prompts() or result is None: + return + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) + else: + span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py new file mode 100644 index 0000000000..f0c68e85ba --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -0,0 +1,184 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.ai.utils import ( + get_start_span_function, + normalize_message_roles, + set_data_normalized, + truncate_and_annotate_messages, +) +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + has_span_streaming_enabled, + should_truncate_gen_ai_input, +) + +from ..consts import SPAN_ORIGIN +from ..utils import ( + _set_agent_data, + _set_available_tools, + _set_model_data, + _should_send_prompts, +) +from .utils import ( + _serialize_binary_content_item, + _serialize_image_url_item, +) + +if TYPE_CHECKING: + from typing import Any, Union + +try: + from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore +except ImportError: + BinaryContent = None + ImageUrl = None + + +def invoke_agent_span( + user_prompt: "Any", + agent: "Any", + model: "Any", + model_settings: "Any", + is_streaming: bool = False, +) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": + """Create a span for invoking the agent.""" + # Determine agent name for span + name = "agent" + if agent and getattr(agent, "name", None): + name = agent.name + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + span = sentry_sdk.traces.start_span( + name=f"invoke_agent {name}", + attributes={ + "sentry.op": OP.GEN_AI_INVOKE_AGENT, + "sentry.origin": SPAN_ORIGIN, + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + }, + ) + else: + span = get_start_span_function()( + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {name}", + origin=SPAN_ORIGIN, + ) + + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + + _set_agent_data(span, agent) + _set_model_data(span, model, model_settings) + _set_available_tools(span, agent) + + # Add user prompt and system prompts if available and prompts are enabled + if _should_send_prompts(): + messages = [] + + # Add system prompts (both instructions and system_prompt) + system_texts = [] + + if agent: + # Check for system_prompt + system_prompts = getattr(agent, "_system_prompts", None) or [] + for prompt in system_prompts: + if isinstance(prompt, str): + system_texts.append(prompt) + + # Check for instructions (stored in _instructions) + instructions = getattr(agent, "_instructions", None) + if instructions: + if isinstance(instructions, str): + system_texts.append(instructions) + elif isinstance(instructions, (list, tuple)): + for instr in instructions: + if isinstance(instr, str): + system_texts.append(instr) + elif callable(instr): + # Skip dynamic/callable instructions + pass + + # Add all system texts as system messages + for system_text in system_texts: + messages.append( + { + "content": [{"text": system_text, "type": "text"}], + "role": "system", + } + ) + + # Add user prompt + if user_prompt: + if isinstance(user_prompt, str): + messages.append( + { + "content": [{"text": user_prompt, "type": "text"}], + "role": "user", + } + ) + elif isinstance(user_prompt, list): + # Handle list of user content + content = [] + for item in user_prompt: + if isinstance(item, str): + content.append({"text": item, "type": "text"}) + elif ImageUrl and isinstance(item, ImageUrl): + content.append(_serialize_image_url_item(item)) + elif BinaryContent and isinstance(item, BinaryContent): + content.append(_serialize_binary_content_item(item)) + if content: + messages.append( + { + "content": content, + "role": "user", + } + ) + + if messages: + normalized_messages = normalize_message_roles(messages) + client = sentry_sdk.get_client() + scope = sentry_sdk.get_current_scope() + messages_data = ( + truncate_and_annotate_messages(normalized_messages, span, scope) + if should_truncate_gen_ai_input(client.options) + else normalized_messages + ) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False + ) + + return span + + +def update_invoke_agent_span( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + result: "Any", +) -> None: + """Update and close the invoke agent span.""" + if not span or not result: + return + + # Extract output from result + output = getattr(result, "output", None) + + # Set response text if prompts are enabled + if _should_send_prompts() and output: + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False + ) + + # Set model name from response if available + if hasattr(result, "response"): + try: + response = result.response + if hasattr(response, "model_name") and response.model_name: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name + ) + else: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) + except Exception: + # If response access fails, continue without setting model name + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py new file mode 100644 index 0000000000..330496c6b2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py @@ -0,0 +1,87 @@ +"""Utility functions for PydanticAI span instrumentation.""" + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import BLOB_DATA_SUBSTITUTE +from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX +from sentry_sdk.ai.utils import get_modality_from_mime_type +from sentry_sdk.consts import SPANDATA +from sentry_sdk.traces import StreamedSpan + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore + + +def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": + """Serialize an ImageUrl content item for span data. + + For data URLs containing base64-encoded images, the content is redacted. + For regular HTTP URLs, the URL string is preserved. + """ + url = str(item.url) + data_url_match = DATA_URL_BASE64_REGEX.match(url) + + if data_url_match: + return { + "type": "image", + "content": BLOB_DATA_SUBSTITUTE, + } + + return { + "type": "image", + "content": url, + } + + +def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": + """Serialize a BinaryContent item for span data, redacting the blob data.""" + return { + "type": "blob", + "modality": get_modality_from_mime_type(item.media_type), + "mime_type": item.media_type, + "content": BLOB_DATA_SUBSTITUTE, + } + + +def _set_usage_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + usage: "Union[RequestUsage, RunUsage]", +) -> None: + """Set token usage data on a span. + + This function works with both RequestUsage (single request) and + RunUsage (agent run) objects from pydantic_ai. + + Args: + span: The Sentry span to set data on. + usage: RequestUsage or RunUsage object containing token usage information. + """ + if usage is None: + return + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if hasattr(usage, "input_tokens") and usage.input_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) + + # Pydantic AI uses cache_read_tokens (not input_tokens_cached) + if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) + + # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) + if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: + set_on_span( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, + usage.cache_write_tokens, + ) + + if hasattr(usage, "output_tokens") and usage.output_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) + + if hasattr(usage, "total_tokens") and usage.total_tokens is not None: + set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py new file mode 100644 index 0000000000..340dcf8953 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py @@ -0,0 +1,233 @@ +from contextvars import ContextVar +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import event_from_exception, safe_serialize + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + +# Store the current agent context in a contextvar for re-entrant safety +# Using a list as a stack to support nested agent calls +_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( + "pydantic_ai_agent_context_stack", default=[] +) + + +def push_agent(agent: "Any", is_streaming: bool = False) -> None: + """Push an agent context onto the stack along with its streaming flag.""" + stack = _agent_context_stack.get().copy() + stack.append({"agent": agent, "is_streaming": is_streaming}) + _agent_context_stack.set(stack) + + +def pop_agent() -> None: + """Pop an agent context from the stack.""" + stack = _agent_context_stack.get().copy() + if stack: + stack.pop() + _agent_context_stack.set(stack) + + +def get_current_agent() -> "Any": + """Get the current agent from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1]["agent"] + return None + + +def get_is_streaming() -> bool: + """Get the streaming flag from the contextvar stack.""" + stack = _agent_context_stack.get() + if stack: + return stack[-1].get("is_streaming", False) + return False + + +def _should_send_prompts() -> bool: + """ + Check if prompts should be sent to Sentry. + + This checks both send_default_pii and the include_prompts integration setting. + """ + if not should_send_default_pii(): + return False + + from . import PydanticAIIntegration + + # Get the integration instance from the client + integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) + + if integration is None: + return False + + return getattr(integration, "include_prompts", False) + + +def _set_agent_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set agent-related data on a span. + + Args: + span: The span to set data on + agent: Agent object (can be None, will try to get from contextvar if not provided) + """ + # Extract agent name from agent object or contextvar + agent_obj = agent + if not agent_obj: + # Try to get from contextvar + agent_obj = get_current_agent() + + if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + else: + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) + + +def _get_model_name(model_obj: "Any") -> "Optional[str]": + """Extract model name from a model object. + + Args: + model_obj: Model object to extract name from + + Returns: + Model name string or None if not found + """ + if not model_obj: + return None + + if hasattr(model_obj, "model_name"): + return model_obj.model_name + elif hasattr(model_obj, "name"): + try: + return model_obj.name() + except Exception: + return str(model_obj) + elif isinstance(model_obj, str): + return model_obj + else: + return str(model_obj) + + +def _set_model_data( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", + model: "Any", + model_settings: "Any", +) -> None: + """Set model-related data on a span. + + Args: + span: The span to set data on + model: Model object (can be None, will try to get from agent if not provided) + model_settings: Model settings (can be None, will try to get from agent if not provided) + """ + # Try to get agent from contextvar if we need it + agent_obj = get_current_agent() + + # Extract model information + model_obj = model + if not model_obj and agent_obj and hasattr(agent_obj, "model"): + model_obj = agent_obj.model + + set_on_span = ( + span.set_attribute if isinstance(span, StreamedSpan) else span.set_data + ) + + if model_obj: + # Set system from model + if hasattr(model_obj, "system"): + set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) + + # Set model name + model_name = _get_model_name(model_obj) + if model_name: + set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) + + # Extract model settings + settings = model_settings + if not settings and agent_obj and hasattr(agent_obj, "model_settings"): + settings = agent_obj.model_settings + + if settings: + settings_map = { + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + } + + # ModelSettings is a TypedDict (dict at runtime), so use dict access + if isinstance(settings, dict): + for setting_name, spandata_key in settings_map.items(): + value = settings.get(setting_name) + if value is not None: + set_on_span(spandata_key, value) + else: + # Fallback for object-style settings + for setting_name, spandata_key in settings_map.items(): + if hasattr(settings, setting_name): + value = getattr(settings, setting_name) + if value is not None: + set_on_span(spandata_key, value) + + +def _set_available_tools( + span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" +) -> None: + """Set available tools data on a span from an agent's function toolset. + + Args: + span: The span to set data on + agent: Agent object with _function_toolset attribute + """ + if not agent or not hasattr(agent, "_function_toolset"): + return + + try: + tools = [] + # Get tools from the function toolset + if hasattr(agent._function_toolset, "tools"): + for tool_name, tool in agent._function_toolset.tools.items(): + tool_info = {"name": tool_name} + + # Add description from function_schema if available + if hasattr(tool, "function_schema"): + schema = tool.function_schema + if getattr(schema, "description", None): + tool_info["description"] = schema.description + + # Add parameters from json_schema + if getattr(schema, "json_schema", None): + tool_info["parameters"] = schema.json_schema + + tools.append(tool_info) + + if tools: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + else: + span.set_data( + SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) + ) + except Exception: + # If we can't extract tools, just skip it + pass + + +def _capture_exception(exc: "Any", handled: bool = False) -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pydantic_ai", "handled": handled}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py new file mode 100644 index 0000000000..2616f4d5a3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py @@ -0,0 +1,262 @@ +import copy +import json + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +try: + from pymongo import monitoring +except ImportError: + raise DidNotEnable("Pymongo not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Dict, Union + + from pymongo.monitoring import ( + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + ) + + +SAFE_COMMAND_ATTRIBUTES = [ + "insert", + "ordered", + "find", + "limit", + "singleBatch", + "aggregate", + "createIndexes", + "indexes", + "delete", + "findAndModify", + "renameCollection", + "to", + "drop", +] + + +def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": + for key in command: + is_safe_field = key in SAFE_COMMAND_ATTRIBUTES + if is_safe_field: + # Skip if safe key + continue + + update_db_command = key == "update" and "findAndModify" not in command + if update_db_command: + # Also skip "update" db command because it is save. + # There is also an "update" key in the "findAndModify" command, which is NOT safe! + continue + + # Special stripping for documents + is_document = key == "documents" + if is_document: + for doc in command[key]: + for doc_key in doc: + doc[doc_key] = "%s" + continue + + # Special stripping for dict style fields + is_dict_field = key in ["filter", "query", "update"] + if is_dict_field: + for item_key in command[key]: + command[key][item_key] = "%s" + continue + + # For pipeline fields strip the `$match` dict + is_pipeline_field = key == "pipeline" + if is_pipeline_field: + for pipeline in command[key]: + for match_key in pipeline["$match"] if "$match" in pipeline else []: + pipeline["$match"][match_key] = "%s" + continue + + # Default stripping + command[key] = "%s" + + return command + + +def _get_db_data(event: "Any") -> "Dict[str, Any]": + data = {} + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + data[SPANDATA.DB_DRIVER_NAME] = "pymongo" + db_name = event.database_name + + server_address = event.connection_id[0] + if server_address is not None: + data[SPANDATA.SERVER_ADDRESS] = server_address + + server_port = event.connection_id[1] + if server_port is not None: + data[SPANDATA.SERVER_PORT] = server_port + + if is_span_streaming_enabled: + data["db.system.name"] = "mongodb" + + if db_name is not None: + data["db.namespace"] = db_name + else: + data[SPANDATA.DB_SYSTEM] = "mongodb" + + if db_name is not None: + data[SPANDATA.DB_NAME] = db_name + + return data + + +class CommandTracer(monitoring.CommandListener): + def __init__(self) -> None: + self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} + + def _operation_key( + self, + event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", + ) -> int: + return event.request_id + + def started(self, event: "CommandStartedEvent") -> None: + client = sentry_sdk.get_client() + if client.get_integration(PyMongoIntegration) is None: + return + + with capture_internal_exceptions(): + command = dict(copy.deepcopy(event.command)) + + command.pop("$db", None) + command.pop("$clusterTime", None) + command.pop("$signature", None) + + db_data = _get_db_data(event) + + collection_name = command.get(event.command_name) + operation_name = event.command_name + db_name = event.database_name + + lsid = command.pop("lsid", None) + if not should_send_default_pii(): + command = _strip_pii(command) + + query = json.dumps(command, default=str) + + if has_span_streaming_enabled(client.options): + span_first_data = { + "db.operation.name": operation_name, + "db.collection.name": collection_name, + SPANDATA.DB_QUERY_TEXT: query, + "sentry.op": OP.DB, + "sentry.origin": PyMongoIntegration.origin, + **db_data, + } + + span = sentry_sdk.traces.start_span( + name=query, attributes=span_first_data + ) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, + category="query", + type=OP.DB, + data=span_first_data, + ) + + else: + tags = { + "db.name": db_name, + SPANDATA.DB_SYSTEM: "mongodb", + SPANDATA.DB_DRIVER_NAME: "pymongo", + SPANDATA.DB_OPERATION: operation_name, + # The below is a deprecated field, but leaving for legacy reasons. + # The v2 spans will use `db.collection.name` instead. + SPANDATA.DB_MONGODB_COLLECTION: collection_name, + } + + try: + tags["net.peer.name"] = event.connection_id[0] + tags["net.peer.port"] = str(event.connection_id[1]) + except TypeError: + pass + + data: "Dict[str, Any]" = {"operation_ids": {}} + data["operation_ids"]["operation"] = event.operation_id + data["operation_ids"]["request"] = event.request_id + + data.update(db_data) + + try: + if lsid: + lsid_id = lsid["id"] + data["operation_ids"]["session"] = str(lsid_id) + except KeyError: + pass + + span = sentry_sdk.start_span( + op=OP.DB, + name=query, + origin=PyMongoIntegration.origin, + ) + + for tag, value in tags.items(): + # set the tag for backwards-compatibility. + # TODO: remove the set_tag call in the next major release! + span.set_tag(tag, value) + span.set_data(tag, value) + + for key, value in data.items(): + span.set_data(key, value) + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb( + message=query, category="query", type=OP.DB, data=tags + ) + + self._ongoing_operations[self._operation_key(event)] = span.__enter__() + + def failed(self, event: "CommandFailedEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + # Ignoring NoOpStreamedSpan as it will always have a status of "ok" + if type(span) is StreamedSpan: + span.status = SpanStatus.ERROR + elif type(span) is Span: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + span.__exit__(None, None, None) + except KeyError: + return + + def succeeded(self, event: "CommandSucceededEvent") -> None: + if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: + return + + try: + span = self._ongoing_operations.pop(self._operation_key(event)) + if type(span) is Span: + span.set_status(SPANSTATUS.OK) + span.__exit__(None, None, None) + except KeyError: + pass + + +class PyMongoIntegration(Integration): + identifier = "pymongo" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py new file mode 100644 index 0000000000..6837d8345c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py @@ -0,0 +1,239 @@ +import functools +import os +import sys +import weakref + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import RequestExtractor +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + reraise, +) + +try: + from pyramid.httpexceptions import HTTPException + from pyramid.request import Request +except ImportError: + raise DidNotEnable("Pyramid not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional + + from pyramid.response import Response + from webob.cookies import RequestCookies + from webob.request import _FieldStorageWithFile + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.integrations.wsgi import _ScopedResponse + from sentry_sdk.utils import ExcInfo + + +if getattr(Request, "authenticated_userid", None): + + def authenticated_userid(request: "Request") -> "Optional[Any]": + return request.authenticated_userid + +else: + # bw-compat for pyramid < 1.5 + from pyramid.security import authenticated_userid # type: ignore + + +TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") + + +class PyramidIntegration(Integration): + identifier = "pyramid" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "route_name") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + from pyramid import router + + old_call_view = router._call_view + + @functools.wraps(old_call_view) + def sentry_patched_call_view( + registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" + ) -> "Response": + client = sentry_sdk.get_client() + integration = client.get_integration(PyramidIntegration) + if integration is None: + return old_call_view(registry, request, *args, **kwargs) + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + + scope = sentry_sdk.get_isolation_scope() + + if should_send_default_pii() and has_span_streaming_enabled(client.options): + user_id = authenticated_userid(request) + if user_id: + scope.set_user({"id": user_id}) + + scope.add_event_processor( + _make_event_processor(weakref.ref(request), integration) + ) + + return old_call_view(registry, request, *args, **kwargs) + + router._call_view = sentry_patched_call_view + + if hasattr(Request, "invoke_exception_view"): + old_invoke_exception_view = Request.invoke_exception_view + + def sentry_patched_invoke_exception_view( + self: "Request", *args: "Any", **kwargs: "Any" + ) -> "Any": + rv = old_invoke_exception_view(self, *args, **kwargs) + + if ( + self.exc_info + and all(self.exc_info) + and rv.status_int == 500 + and sentry_sdk.get_client().get_integration(PyramidIntegration) + is not None + ): + _capture_exception(self.exc_info) + + return rv + + Request.invoke_exception_view = sentry_patched_invoke_exception_view + + old_wsgi_call = router.Router.__call__ + + @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) + def sentry_patched_wsgi_call( + self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "_ScopedResponse": + def sentry_patched_inner_wsgi_call( + environ: "Dict[str, Any]", start_response: "Callable[..., Any]" + ) -> "Any": + try: + return old_wsgi_call(self, environ, start_response) + except Exception: + einfo = sys.exc_info() + _capture_exception(einfo) + reraise(*einfo) + + middleware = SentryWsgiMiddleware( + sentry_patched_inner_wsgi_call, + span_origin=PyramidIntegration.origin, + ) + return middleware(environ, start_response) + + router.Router.__call__ = sentry_patched_wsgi_call + + +@ensure_integration_enabled(PyramidIntegration) +def _capture_exception(exc_info: "ExcInfo") -> None: + if exc_info[0] is None or issubclass(exc_info[0], HTTPException): + return + + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "pyramid", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "route_name": request.matched_route.name, + "route_pattern": request.matched_route.pattern, + } + is_span_streaming_enabled = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if is_span_streaming_enabled + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + scope.set_transaction_name( + name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +class PyramidRequestExtractor(RequestExtractor): + def url(self) -> str: + return self.request.path_url + + def env(self) -> "Dict[str, str]": + return self.request.environ + + def cookies(self) -> "RequestCookies": + return self.request.cookies + + def raw_data(self) -> str: + return self.request.text + + def form(self) -> "Dict[str, str]": + return { + key: value + for key, value in self.request.POST.items() + if not getattr(value, "filename", None) + } + + def files(self) -> "Dict[str, _FieldStorageWithFile]": + return { + key: value + for key, value in self.request.POST.items() + if getattr(value, "filename", None) + } + + def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: + file = postdata.file + try: + return os.fstat(file.fileno()).st_size + except Exception: + return 0 + + +def _make_event_processor( + weak_request: "Callable[[], Request]", integration: "PyramidIntegration" +) -> "EventProcessor": + def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + PyramidRequestExtractor(request).extract_into_event(event) + + if should_send_default_pii(): + with capture_internal_exceptions(): + user_info = event.setdefault("user", {}) + user_info.setdefault("id", authenticated_userid(request)) + + return event + + return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py new file mode 100644 index 0000000000..aae68c4c10 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py @@ -0,0 +1,197 @@ +from contextlib import contextmanager +from typing import Any, Generator + +import sentry_sdk +from sentry_sdk import start_span +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import BAGGAGE_HEADER_NAME +from sentry_sdk.tracing_utils import ( + add_http_request_source, + add_sentry_baggage_to_headers, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + logger, + parse_url, +) + +try: + from pyreqwest.client import ( # type: ignore[import-not-found] + ClientBuilder, + SyncClientBuilder, + ) + from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] + from pyreqwest.request import ( # type: ignore[import-not-found] + OneOffRequestBuilder, + Request, + SyncOneOffRequestBuilder, + ) + from pyreqwest.response import ( # type: ignore[import-not-found] + Response, + SyncResponse, + ) +except ImportError: + raise DidNotEnable("pyreqwest not installed or incompatible version installed") + + +class PyreqwestIntegration(Integration): + identifier = "pyreqwest" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _patch_pyreqwest() + + +def _patch_pyreqwest() -> None: + # Patch Client Builders + _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) + _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) + + # Patch Request Builders + _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) + _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) + + +def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: + if not hasattr(cls, method_name): + return + + original_method = getattr(cls, method_name) + + def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + if not getattr(self, "_sentry_instrumented", False): + integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) + if integration is not None: + self.with_middleware(middleware) + try: + self._sentry_instrumented = True + except (TypeError, AttributeError): + # In case the instance itself is immutable or doesn't allow extra attributes + pass + return original_method(self, *args, **kwargs) + + setattr(cls, method_name, sentry_patched_method) + + +@contextmanager +def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(str(request.url), sanitize=False) + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": PyreqwestIntegration.origin, + SPANDATA.HTTP_REQUEST_METHOD: request.method, + }, + ) as span: + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + return + + with start_span( + op=OP.HTTP_CLIENT, + name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", + origin=PyreqwestIntegration.origin, + ) as span: + span.set_data(SPANDATA.HTTP_METHOD, request.method) + if parsed_url is not None: + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + + if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( + key=key, value=value, url=request.url + ) + ) + + if key == BAGGAGE_HEADER_NAME: + add_sentry_baggage_to_headers(request.headers, value) + else: + request.headers[key] = value + + yield span + + with capture_internal_exceptions(): + add_http_request_source(span) + + +async def sentry_async_middleware( + request: "Request", next_handler: "Next" +) -> "Response": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return await next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = await next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response + + +def sentry_sync_middleware( + request: "Request", next_handler: "SyncNext" +) -> "SyncResponse": + if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: + return next_handler.run(request) + + with _sentry_pyreqwest_span(request) as span: + response = next_handler.run(request) + if isinstance(span, StreamedSpan): + span.status = "error" if response.status >= 400 else "ok" + span.set_attribute( + SPANDATA.HTTP_STATUS_CODE, + response.status, + ) + else: + span.set_http_status(response.status) + + return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py new file mode 100644 index 0000000000..6a5603d825 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py @@ -0,0 +1,292 @@ +import asyncio +import inspect +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Union + + from sentry_sdk._types import Event, EventProcessor + +try: + import quart_auth # type: ignore +except ImportError: + quart_auth = None + +try: + from quart import ( # type: ignore + Quart, + Request, + has_request_context, + has_websocket_context, + request, + websocket, + ) + from quart.signals import ( # type: ignore + got_background_exception, + got_request_exception, + got_websocket_exception, + request_started, + websocket_started, + ) +except ImportError: + raise DidNotEnable("Quart is not installed") +else: + # Quart 0.19 is based on Flask and hence no longer has a Scaffold + try: + from quart.scaffold import Scaffold # type: ignore + except ImportError: + from flask.sansio.scaffold import Scaffold # type: ignore + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class QuartIntegration(Integration): + identifier = "quart" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__(self, transaction_style: str = "endpoint") -> None: + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + + @staticmethod + def setup_once() -> None: + request_started.connect(_request_websocket_started) + websocket_started.connect(_request_websocket_started) + got_background_exception.connect(_capture_exception) + got_request_exception.connect(_capture_exception) + got_websocket_exception.connect(_capture_exception) + + patch_asgi_app() + patch_scaffold_route() + + +def patch_asgi_app() -> None: + old_app = Quart.__call__ + + async def sentry_patched_asgi_app( + self: "Any", scope: "Any", receive: "Any", send: "Any" + ) -> "Any": + if sentry_sdk.get_client().get_integration(QuartIntegration) is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + span_origin=QuartIntegration.origin, + asgi_version=3, + ) + return await middleware(scope, receive, send) + + Quart.__call__ = sentry_patched_asgi_app + + +def patch_scaffold_route() -> None: + # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 + if sys.version_info >= (3, 12): + iscoroutinefunction = inspect.iscoroutinefunction + else: + iscoroutinefunction = asyncio.iscoroutinefunction + + old_route = Scaffold.route + + def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": + old_decorator = old_route(*args, **kwargs) + + def decorator(old_func: "Any") -> "Any": + if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): + + @wraps(old_func) + @ensure_integration_enabled(QuartIntegration, old_func) + def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + span = get_current_span() + if span is not None and hasattr(span, "_segment"): + span._segment._update_active_thread() + else: + current_scope = sentry_sdk.get_current_scope() + if current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + return old_func(*args, **kwargs) + + return old_decorator(_sentry_func) + + return old_decorator(old_func) + + return decorator + + Scaffold.route = _sentry_route + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" +) -> None: + try: + name_for_style = { + "url": request.url_rule.rule, + "endpoint": request.url_rule.endpoint, + } + + source = ( + SEGMENT_SOURCE_FOR_STYLE[transaction_style] + if has_span_streaming_enabled(sentry_sdk.get_client().options) + else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + ) + + scope.set_transaction_name( + name=name_for_style[transaction_style], + source=source, + ) + except Exception: + pass + + +async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + if has_request_context(): + request_websocket = request._get_current_object() + if has_websocket_context(): + request_websocket = websocket._get_current_object() + + # Set the transaction name here, but rely on ASGI middleware + # to actually start the transaction + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket + ) + + scope = sentry_sdk.get_isolation_scope() + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + current_span = get_current_span() + if type(current_span) is StreamedSpan: + segment = current_span._segment + + segment.set_attribute("http.request.method", request_websocket.method) + header_attributes: "dict[str, Any]" = {} + + for header, header_value in _filter_headers( + dict(request_websocket.headers), use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + segment.set_attributes(header_attributes) + + if should_send_default_pii(): + segment.set_attribute("url.full", request_websocket.url) + segment.set_attribute( + "url.query", + request_websocket.query_string.decode("utf-8", errors="replace"), + ) + + user_properties = {} + if len(request_websocket.access_route) >= 1: + segment.set_attribute( + "client.address", request_websocket.access_route[0] + ) + user_properties["ip_address"] = request_websocket.access_route[0] + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_properties["id"] = current_user_id + + if user_properties: + existing_user_properties = scope._user or {} + scope.set_user({**existing_user_properties, **user_properties}) + + evt_processor = _make_request_event_processor(app, request_websocket, integration) + scope.add_event_processor(evt_processor) + + +def _make_request_event_processor( + app: "Quart", request: "Request", integration: "QuartIntegration" +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + # if the request is gone we are fine not logging the data from + # it. This might happen if the processor is pushed away to + # another thread. + if request is None: + return event + + with capture_internal_exceptions(): + # TODO: Figure out what to do with request body. Methods on request + # are async, but event processors are not. + + request_info = event.setdefault("request", {}) + request_info["url"] = request.url + request_info["query_string"] = request.query_string + request_info["method"] = request.method + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + if len(request.access_route) >= 1: + request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} + + current_user_id = _get_current_user_id_from_quart() + if current_user_id: + user_info = event.setdefault("user", {}) + user_info["id"] = current_user_id + + return event + + return inner + + +async def _capture_exception( + sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" +) -> None: + integration = sentry_sdk.get_client().get_integration(QuartIntegration) + if integration is None: + return + + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "quart", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_current_user_id_from_quart() -> "str | None": + if quart_auth is None: + return None + + if quart_auth.current_user is None: + return None + + try: + return quart_auth.current_user._auth_id + except Exception: + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py new file mode 100644 index 0000000000..f723a96f3c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py @@ -0,0 +1,240 @@ +import functools +import inspect +import sys + +import sentry_sdk +from sentry_sdk.consts import OP, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + event_from_exception, + logger, + package_version, + qualname_from_function, + reraise, +) + +try: + import ray # type: ignore[import-not-found] + from ray import remote +except ImportError: + raise DidNotEnable("Ray not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional + + from sentry_sdk.utils import ExcInfo + + +def _check_sentry_initialized() -> None: + if sentry_sdk.get_client().is_active(): + return + + logger.debug( + "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." + ) + + +def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: + # Patching new_func signature to add the _sentry_tracing parameter to it + # Ray later inspects the signature and finds the unexpected parameter otherwise + signature = inspect.signature(func) + params = list(signature.parameters.values()) + sentry_tracing_param = inspect.Parameter( + "_sentry_tracing", + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + ) + + # Keyword only arguments are penultimate if function has variadic keyword arguments + if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: + params.insert(-1, sentry_tracing_param) + else: + params.append(sentry_tracing_param) + + func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] + + +def _patch_ray_remote() -> None: + old_remote = remote + + @functools.wraps(old_remote) + def new_remote( + f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" + ) -> "Callable[..., Any]": + if inspect.isclass(f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(f, *args, **kwargs) + + def wrapper(user_f: "Callable[..., Any]") -> "Any": + if inspect.isclass(user_f): + # Ray Actors + # (https://docs.ray.io/en/latest/ray-core/actors.html) + # are not supported + # (Only Ray Tasks are supported) + return old_remote(*args, **kwargs)(user_f) + + @functools.wraps(user_f) + def new_func( + *f_args: "Any", + _sentry_tracing: "Optional[dict[str, Any]]" = None, + **f_kwargs: "Any", + ) -> "Any": + _check_sentry_initialized() + + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + sentry_sdk.traces.continue_trace(_sentry_tracing or {}) + + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RAY, + "sentry.origin": RayIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + }, + parent_span=None, + ): + try: + result = user_f(*f_args, **f_kwargs) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + transaction = sentry_sdk.continue_trace( + _sentry_tracing or {}, + op=OP.QUEUE_TASK_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + source=TransactionSource.TASK, + ) + + with sentry_sdk.start_transaction(transaction) as transaction: + try: + result = user_f(*f_args, **f_kwargs) + transaction.set_status(SPANSTATUS.OK) + except Exception: + transaction.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + _insert_sentry_tracing_in_signature(new_func) + + if f: + rv = old_remote(new_func) + else: + rv = old_remote(*args, **kwargs)(new_func) + old_remote_method = rv.remote + + def _remote_method_with_header_propagation( + *args: "Any", **kwargs: "Any" + ) -> "Any": + """ + Ray Client + """ + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) + if span_streaming: + function_name = qualname_from_function(user_f) + with sentry_sdk.traces.start_span( + name="unknown Ray task" + if function_name is None + else function_name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_RAY, + "sentry.origin": RayIntegration.origin, + }, + ): + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + except Exception: + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_RAY, + name=qualname_from_function(user_f), + origin=RayIntegration.origin, + ) as span: + tracing = { + k: v + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() + } + try: + result = old_remote_method( + *args, **kwargs, _sentry_tracing=tracing + ) + span.set_status(SPANSTATUS.OK) + except Exception: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + + return result + + rv.remote = _remote_method_with_header_propagation + + return rv + + if f is not None: + return wrapper(f) + else: + return wrapper + + ray.remote = new_remote + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={ + "handled": False, + "type": RayIntegration.identifier, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + +class RayIntegration(Integration): + identifier = "ray" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = package_version("ray") + _check_minimum_version(RayIntegration, version) + + _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py new file mode 100644 index 0000000000..7095721ed2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py @@ -0,0 +1,49 @@ +import warnings +from typing import TYPE_CHECKING + +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE +from sentry_sdk.integrations.redis.rb import _patch_rb +from sentry_sdk.integrations.redis.redis import _patch_redis +from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster +from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +class RedisIntegration(Integration): + identifier = "redis" + + def __init__( + self, + max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, + cache_prefixes: "Optional[list[str]]" = None, + ) -> None: + self.max_data_size = max_data_size + self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] + + if max_data_size is not None: + warnings.warn( + "The `max_data_size` parameter of `RedisIntegration` is " + "deprecated and will be removed in version 3.0 of sentry-sdk.", + DeprecationWarning, + stacklevel=2, + ) + + @staticmethod + def setup_once() -> None: + try: + from redis import StrictRedis, client + except ImportError: + raise DidNotEnable("Redis client not installed") + + _patch_redis(StrictRedis, client) + _patch_redis_cluster() + _patch_rb() + + try: + _patch_rediscluster() + except Exception: + logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py new file mode 100644 index 0000000000..8fc3d0c3a9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py @@ -0,0 +1,177 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from redis.asyncio.client import Pipeline, StrictRedis + from redis.asyncio.cluster import ClusterPipeline, RedisCluster + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_async_pipeline( + pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return await old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + try: + command_seq = self._execution_strategy._command_queue + except AttributeError: + if is_cluster: + command_seq = self._command_stack + else: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.is_transaction, + command_seq, + ) + + return await old_execute(self, *args, **kwargs) + + pipeline_cls.execute = _sentry_execute # type: ignore + + +def patch_redis_async_client( + cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + async def _sentry_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return await old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = await old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py new file mode 100644 index 0000000000..58d686b099 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN +from sentry_sdk.integrations.redis.modules.caches import ( + _compile_cache_span_properties, + _set_cache_data, +) +from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties +from sentry_sdk.integrations.redis.utils import ( + _get_safe_command, + _set_client_data, + _set_pipeline_data, +) +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional, Union + + from sentry_sdk.traces import StreamedSpan + + +def patch_redis_pipeline( + pipeline_cls: "Any", + is_cluster: bool, + get_command_args_fn: "Any", + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + old_execute = pipeline_cls.execute + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RedisIntegration) is None: + return old_execute(self, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="redis.pipeline.execute", + attributes={ + "sentry.origin": SPAN_ORIGIN, + "sentry.op": OP.DB_REDIS, + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.DB_REDIS, + name="redis.pipeline.execute", + origin=SPAN_ORIGIN, + ) + + with span: + with capture_internal_exceptions(): + command_seq = None + try: + command_seq = self._execution_strategy.command_queue + except AttributeError: + command_seq = self.command_stack + + set_db_data_fn(span, self) + _set_pipeline_data( + span, + is_cluster, + get_command_args_fn, + False if is_cluster else self.transaction, + command_seq, + ) + + return old_execute(self, *args, **kwargs) + + pipeline_cls.execute = sentry_patched_execute + + +def patch_redis_client( + cls: "Any", + is_cluster: bool, + set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", +) -> None: + """ + This function can be used to instrument custom redis client classes or + subclasses. + """ + old_execute_command = cls.execute_command + + from sentry_sdk.integrations.redis import RedisIntegration + + def sentry_patched_execute_command( + self: "Any", name: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + integration = client.get_integration(RedisIntegration) + if integration is None: + return old_execute_command(self, name, *args, **kwargs) + + span_streaming = has_span_streaming_enabled(client.options) + + cache_properties = _compile_cache_span_properties( + name, + args, + kwargs, + integration, + ) + + additional_cache_span_attributes = {} + with capture_internal_exceptions(): + additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( + _get_safe_command(name, args) + ) + + cache_span: "Optional[Union[Span, StreamedSpan]]" = None + if cache_properties["is_cache_key"] and cache_properties["op"] is not None: + if span_streaming: + cache_span = sentry_sdk.traces.start_span( + name=cache_properties["description"], + attributes={ + "sentry.op": cache_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_cache_span_attributes, + }, + ) + else: + cache_span = sentry_sdk.start_span( + op=cache_properties["op"], + name=cache_properties["description"], + origin=SPAN_ORIGIN, + ) + cache_span.__enter__() + + db_properties = _compile_db_span_properties(integration, name, args) + + additional_db_span_attributes = {} + with capture_internal_exceptions(): + additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( + name, args + ) + + db_span: "Union[Span, StreamedSpan]" + if span_streaming: + db_span = sentry_sdk.traces.start_span( + name=db_properties["description"], + attributes={ + "sentry.op": db_properties["op"], + "sentry.origin": SPAN_ORIGIN, + **additional_db_span_attributes, + }, + ) + else: + db_span = sentry_sdk.start_span( + op=db_properties["op"], + name=db_properties["description"], + origin=SPAN_ORIGIN, + ) + db_span.__enter__() + + set_db_data_fn(db_span, self) + _set_client_data(db_span, is_cluster, name, *args) + + value = old_execute_command(self, name, *args, **kwargs) + + db_span.__exit__(None, None, None) + + if cache_span: + _set_cache_data(cache_span, self, cache_properties, value) + cache_span.__exit__(None, None, None) + + return value + + cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py new file mode 100644 index 0000000000..0822c2c930 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py @@ -0,0 +1,19 @@ +SPAN_ORIGIN = "auto.db.redis" + +_SINGLE_KEY_COMMANDS = frozenset( + ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], +) +_MULTI_KEY_COMMANDS = frozenset( + [ + "del", + "touch", + "unlink", + "mget", + ], +) +_COMMANDS_INCLUDING_SENSITIVE_DATA = [ + "auth", +] +_MAX_NUM_ARGS = 10 # Trim argument lists to this many values +_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values +_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py new file mode 100644 index 0000000000..35f20fddd9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py @@ -0,0 +1,136 @@ +""" +Code used for the Caches module in Sentry +""" + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +GET_COMMANDS = ("get", "mget") +SET_COMMANDS = ("set", "setex") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _get_op(name: str) -> "Optional[str]": + op = None + if name.lower() in GET_COMMANDS: + op = OP.CACHE_GET + elif name.lower() in SET_COMMANDS: + op = OP.CACHE_PUT + + return op + + +def _compile_cache_span_properties( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> "dict[str, Any]": + key = _get_safe_key(redis_command, args, kwargs) + key_as_string = _key_as_string(key) + keys_as_string = key_as_string.split(", ") + + is_cache_key = False + for prefix in integration.cache_prefixes: + for kee in keys_as_string: + if kee.startswith(prefix): + is_cache_key = True + break + if is_cache_key: + break + + value = None + if redis_command.lower() in SET_COMMANDS: + value = args[-1] + + properties = { + "op": _get_op(redis_command), + "description": _get_cache_span_description( + redis_command, args, kwargs, integration + ), + "key": key, + "key_as_string": key_as_string, + "redis_command": redis_command.lower(), + "is_cache_key": is_cache_key, + "value": value, + } + + return properties + + +def _get_cache_span_description( + redis_command: str, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", + integration: "RedisIntegration", +) -> str: + description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_cache_data( + span: "Union[Span, StreamedSpan]", + redis_client: "Any", + properties: "dict[str, Any]", + return_value: "Optional[Any]", +) -> None: + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + with capture_internal_exceptions(): + set_on_span(SPANDATA.CACHE_KEY, properties["key"]) + + if properties["redis_command"] in GET_COMMANDS: + if return_value is not None: + set_on_span(SPANDATA.CACHE_HIT, True) + size = ( + len(str(return_value).encode("utf-8")) + if not isinstance(return_value, bytes) + else len(return_value) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + else: + set_on_span(SPANDATA.CACHE_HIT, False) + + elif properties["redis_command"] in SET_COMMANDS: + if properties["value"] is not None: + size = ( + len(properties["value"].encode("utf-8")) + if not isinstance(properties["value"], bytes) + else len(properties["value"]) + ) + set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) + + try: + connection_params = redis_client.connection_pool.connection_kwargs + except AttributeError: + # If it is a cluster, there is no connection_pool attribute so we + # need to get the default node from the cluster instance + default_node = redis_client.get_default_node() + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + + host = connection_params.get("host") + if host is not None: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) + + port = connection_params.get("port") + if port is not None: + set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py new file mode 100644 index 0000000000..69207bf6f6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py @@ -0,0 +1,88 @@ +""" +Code used for the Queries module in Sentry +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.redis.utils import _get_safe_command +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import Redis + + from sentry_sdk.integrations.redis import RedisIntegration + from sentry_sdk.tracing import Span + + +def _compile_db_span_properties( + integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" +) -> "dict[str, Any]": + description = _get_db_span_description(integration, redis_command, args) + + properties = { + "op": OP.DB_REDIS, + "description": description, + } + + return properties + + +def _get_db_span_description( + integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" +) -> str: + description = command_name + + with capture_internal_exceptions(): + description = _get_safe_command(command_name, args) + + if integration.max_data_size and len(description) > integration.max_data_size: + description = description[: integration.max_data_size - len("...")] + "..." + + return description + + +def _set_db_data_on_span( + span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" +) -> None: + db = connection_params.get("db") + host = connection_params.get("host") + port = connection_params.get("port") + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") + span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) + + if host is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_attribute(SPANDATA.SERVER_PORT, port) + + else: + span.set_data(SPANDATA.DB_SYSTEM, "redis") + span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") + + if db is not None: + span.set_data(SPANDATA.DB_NAME, str(db)) + + if host is not None: + span.set_data(SPANDATA.SERVER_ADDRESS, host) + + if port is not None: + span.set_data(SPANDATA.SERVER_PORT, port) + + +def _set_db_data( + span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" +) -> None: + try: + _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) + except AttributeError: + pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py new file mode 100644 index 0000000000..e2ce863fe8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py @@ -0,0 +1,31 @@ +""" +Instrumentation for Redis Blaster (rb) + +https://github.com/getsentry/rb +""" + +from sentry_sdk.integrations.redis._sync_common import patch_redis_client +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + + +def _patch_rb() -> None: + try: + import rb.clients # type: ignore + except ImportError: + pass + else: + patch_redis_client( + rb.clients.FanoutClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.MappingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_client( + rb.clients.RoutingClient, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py new file mode 100644 index 0000000000..e704c9bc6a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py @@ -0,0 +1,67 @@ +""" +Instrumentation for Redis + +https://github.com/redis/redis-py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data + +if TYPE_CHECKING: + from typing import Any, Sequence + + +def _get_redis_command_args(command: "Any") -> "Sequence[Any]": + return command[0] + + +def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 + patch_redis_client( + StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_pipeline( + client.Pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + try: + strict_pipeline = client.StrictPipeline + except AttributeError: + pass + else: + patch_redis_pipeline( + strict_pipeline, + is_cluster=False, + get_command_args_fn=_get_redis_command_args, + set_db_data_fn=_set_db_data, + ) + + try: + import redis.asyncio + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + redis.asyncio.client.StrictRedis, + is_cluster=False, + set_db_data_fn=_set_db_data, + ) + patch_redis_async_pipeline( + redis.asyncio.client.Pipeline, + False, + _get_redis_command_args, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py new file mode 100644 index 0000000000..b6c95e6abd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py @@ -0,0 +1,115 @@ +""" +Instrumentation for RedisCluster +This is part of the main redis-py client. + +https://github.com/redis/redis-py/blob/master/redis/cluster.py +""" + +from typing import TYPE_CHECKING + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command +from sentry_sdk.utils import capture_internal_exceptions + +if TYPE_CHECKING: + from typing import Any, Union + + from redis import RedisCluster + from redis.asyncio.cluster import ( + ClusterPipeline as AsyncClusterPipeline, + ) + from redis.asyncio.cluster import ( + RedisCluster as AsyncRedisCluster, + ) + + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.tracing import Span + + +def _set_async_cluster_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_instance: "AsyncRedisCluster[Any]", +) -> None: + default_node = async_redis_cluster_instance.get_default_node() + if default_node is not None and default_node.connection_kwargs is not None: + _set_db_data_on_span(span, default_node.connection_kwargs) + + +def _set_async_cluster_pipeline_db_data( + span: "Union[Span, StreamedSpan]", + async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", +) -> None: + with capture_internal_exceptions(): + client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) + if client is None: + # In older redis-py versions, the AsyncClusterPipeline had a `_client` + # attr but it is private so potentially problematic and mypy does not + # recognize it - see + # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 + client = ( + async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] + ) + + _set_async_cluster_db_data( + span, + client, + ) + + +def _set_cluster_db_data( + span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" +) -> None: + default_node = redis_cluster_instance.get_default_node() + + if default_node is not None: + connection_params = { + "host": default_node.host, + "port": default_node.port, + } + _set_db_data_on_span(span, connection_params) + + +def _patch_redis_cluster() -> None: + """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" + try: + from redis import RedisCluster, cluster + except ImportError: + pass + else: + patch_redis_client( + RedisCluster, + is_cluster=True, + set_db_data_fn=_set_cluster_db_data, + ) + patch_redis_pipeline( + cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_cluster_db_data, + ) + + try: + from redis.asyncio import cluster as async_cluster + except ImportError: + pass + else: + from sentry_sdk.integrations.redis._async_common import ( + patch_redis_async_client, + patch_redis_async_pipeline, + ) + + patch_redis_async_client( + async_cluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_async_cluster_db_data, + ) + patch_redis_async_pipeline( + async_cluster.ClusterPipeline, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_async_cluster_pipeline_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py new file mode 100644 index 0000000000..3437aa1f2f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py @@ -0,0 +1,49 @@ +""" +Instrumentation for redis-py-cluster +The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). + +https://github.com/grokzen/redis-py-cluster +""" + +from sentry_sdk.integrations.redis._sync_common import ( + patch_redis_client, + patch_redis_pipeline, +) +from sentry_sdk.integrations.redis.modules.queries import _set_db_data +from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command + + +def _patch_rediscluster() -> None: + try: + import rediscluster # type: ignore + except ImportError: + return + + patch_redis_client( + rediscluster.RedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + + # up to v1.3.6, __version__ attribute is a tuple + # from v2.0.0, __version__ is a string and VERSION a tuple + version = getattr(rediscluster, "VERSION", rediscluster.__version__) + + # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 + # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst + if (0, 2, 0) < version < (2, 0, 0): + pipeline_cls = rediscluster.pipeline.StrictClusterPipeline + patch_redis_client( + rediscluster.StrictRedisCluster, + is_cluster=True, + set_db_data_fn=_set_db_data, + ) + else: + pipeline_cls = rediscluster.pipeline.ClusterPipeline + + patch_redis_pipeline( + pipeline_cls, + is_cluster=True, + get_command_args_fn=_parse_rediscluster_command, + set_db_data_fn=_set_db_data, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py new file mode 100644 index 0000000000..7e04df9c69 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py @@ -0,0 +1,159 @@ +from typing import TYPE_CHECKING + +from sentry_sdk.consts import SPANDATA +from sentry_sdk.integrations.redis.consts import ( + _COMMANDS_INCLUDING_SENSITIVE_DATA, + _MAX_NUM_ARGS, + _MAX_NUM_COMMANDS, + _MULTI_KEY_COMMANDS, + _SINGLE_KEY_COMMANDS, +) +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + +if TYPE_CHECKING: + from typing import Any, Optional, Sequence, Union + + +def _get_safe_command(name: str, args: "Sequence[Any]") -> str: + command_parts = [name] + + name_low = name.lower() + send_default_pii = should_send_default_pii() + + for i, arg in enumerate(args): + if i > _MAX_NUM_ARGS: + break + + if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + continue + + arg_is_the_key = i == 0 + if arg_is_the_key: + command_parts.append(repr(arg)) + else: + if send_default_pii: + command_parts.append(repr(arg)) + else: + command_parts.append(SENSITIVE_DATA_SUBSTITUTE) + + command = " ".join(command_parts) + return command + + +def _safe_decode(key: "Any") -> str: + if isinstance(key, bytes): + try: + return key.decode() + except UnicodeDecodeError: + return "" + + return str(key) + + +def _key_as_string(key: "Any") -> str: + if isinstance(key, (dict, list, tuple)): + key = ", ".join(_safe_decode(x) for x in key) + elif isinstance(key, bytes): + key = _safe_decode(key) + elif key is None: + key = "" + else: + key = str(key) + + return key + + +def _get_safe_key( + method_name: str, + args: "Optional[tuple[Any, ...]]", + kwargs: "Optional[dict[str, Any]]", +) -> "Optional[tuple[str, ...]]": + """ + Gets the key (or keys) from the given method_name. + The method_name could be a redis command or a django caching command + """ + key = None + + if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: + # for example redis "mget" + key = tuple(args) + + elif args is not None and len(args) >= 1: + # for example django "set_many/get_many" or redis "get" + if isinstance(args[0], (dict, list, tuple)): + key = tuple(args[0]) + else: + key = (args[0],) + + elif kwargs is not None and "key" in kwargs: + # this is a legacy case for older versions of Django + if isinstance(kwargs["key"], (list, tuple)): + if len(kwargs["key"]) > 0: + key = tuple(kwargs["key"]) + else: + if kwargs["key"] is not None: + key = (kwargs["key"],) + + return key + + +def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": + return command.args + + +def _set_pipeline_data( + span: "Union[Span, StreamedSpan]", + is_cluster: bool, + get_command_args_fn: "Any", + is_transaction: bool, + commands_seq: "Sequence[Any]", +) -> None: + # TODO: Remove this whole function when removing transaction based tracing + if isinstance(span, StreamedSpan): + return + + span.set_tag("redis.is_cluster", is_cluster) + span.set_tag("redis.transaction", is_transaction) + + commands = [] + for i, arg in enumerate(commands_seq): + if i >= _MAX_NUM_COMMANDS: + break + + command = get_command_args_fn(arg) + commands.append(_get_safe_command(command[0], command[1:])) + + span.set_data( + "redis.commands", + { + "count": len(commands_seq), + "first_ten": commands, + }, + ) + + +def _set_client_data( + span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" +) -> None: + if isinstance(span, StreamedSpan): + if name: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) + else: + span.set_tag("redis.is_cluster", is_cluster) + if name: + span.set_tag("redis.command", name) + span.set_tag(SPANDATA.DB_OPERATION, name) + + if name and args: + name_low = name.lower() + if (name_low in _SINGLE_KEY_COMMANDS) or ( + name_low in _MULTI_KEY_COMMANDS and len(args) == 1 + ): + if isinstance(span, StreamedSpan): + span.set_attribute("db.redis.key", args[0]) + else: + span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py new file mode 100644 index 0000000000..edd48a9f67 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py @@ -0,0 +1,225 @@ +import functools +import weakref + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + event_from_exception, + format_timestamp, + parse_version, +) + +try: + from rq.job import JobStatus + from rq.queue import Queue + from rq.timeouts import JobTimeoutException + from rq.version import VERSION as RQ_VERSION + from rq.worker import Worker +except ImportError: + raise DidNotEnable("RQ not installed") + +try: + from rq.worker import BaseWorker + + if not hasattr(BaseWorker, "perform_job"): + BaseWorker = None +except ImportError: + BaseWorker = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + + from rq.job import Job + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + +class RqIntegration(Integration): + identifier = "rq" + origin = f"auto.queue.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(RQ_VERSION) + _check_minimum_version(RqIntegration, version) + + # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly + # instead of Worker, so we need to patch BaseWorker to cover both. + # For older versions where BaseWorker doesn't exist or doesn't have + # perform_job, we patch Worker. + worker_cls = BaseWorker if BaseWorker is not None else Worker + + old_perform_job = worker_cls.perform_job + + @functools.wraps(old_perform_job) + def sentry_patched_perform_job( + self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" + ) -> bool: + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_perform_job(self, job, *args, **kwargs) + + with sentry_sdk.new_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_event_processor(weakref.ref(job))) + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace( + job.meta.get("_sentry_trace_headers") or {} + ) + + Scope.set_custom_sampling_context({"rq_job": job}) + + func_name = None + with capture_internal_exceptions(): + func_name = job.func_name + + with sentry_sdk.traces.start_span( + name="unknown RQ task" if func_name is None else func_name, + attributes={ + "sentry.op": OP.QUEUE_TASK_RQ, + "sentry.origin": RqIntegration.origin, + "sentry.span.source": SegmentSource.TASK, + SPANDATA.MESSAGING_MESSAGE_ID: job.id, + }, + parent_span=None, + ) as span: + if func_name is not None: + span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) + + rv = old_perform_job(self, job, *args, **kwargs) + else: + transaction = continue_trace( + job.meta.get("_sentry_trace_headers") or {}, + op=OP.QUEUE_TASK_RQ, + name="unknown RQ task", + source=TransactionSource.TASK, + origin=RqIntegration.origin, + ) + + with capture_internal_exceptions(): + transaction.name = job.func_name + + with sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"rq_job": job}, + ): + rv = old_perform_job(self, job, *args, **kwargs) + + if self.is_horse: + # We're inside of a forked process and RQ is + # about to call `os._exit`. Make sure that our + # events get sent out. + sentry_sdk.get_client().flush() + + return rv + + worker_cls.perform_job = sentry_patched_perform_job + + old_handle_exception = worker_cls.handle_exception + + def sentry_patched_handle_exception( + self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" + ) -> "Any": + retry = ( + hasattr(job, "retries_left") + and job.retries_left + and job.retries_left > 0 + ) + failed = job._status == JobStatus.FAILED or job.is_failed + if failed and not retry: + _capture_exception(exc_info) + + return old_handle_exception(self, job, *exc_info, **kwargs) + + worker_cls.handle_exception = sentry_patched_handle_exception + + old_enqueue_job = Queue.enqueue_job + + @functools.wraps(old_enqueue_job) + def sentry_patched_enqueue_job( + self: "Queue", job: "Any", **kwargs: "Any" + ) -> "Any": + client = sentry_sdk.get_client() + if client.get_integration(RqIntegration) is None: + return old_enqueue_job(self, job, **kwargs) + + scope = sentry_sdk.get_current_scope() + span = ( + scope.streamed_span + if has_span_streaming_enabled(client.options) + else scope.span + ) + if span is not None: + job.meta["_sentry_trace_headers"] = dict( + scope.iter_trace_propagation_headers() + ) + + return old_enqueue_job(self, job, **kwargs) + + Queue.enqueue_job = sentry_patched_enqueue_job + + ignore_logger("rq.worker") + + +def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": + def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + job = weak_job() + if job is not None: + with capture_internal_exceptions(): + extra = event.setdefault("extra", {}) + rq_job = { + "job_id": job.id, + "func": job.func_name, + "args": ( + job.args + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "kwargs": ( + job.kwargs + if should_send_default_pii() + else SENSITIVE_DATA_SUBSTITUTE + ), + "description": job.description, + } + + if job.enqueued_at: + rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) + if job.started_at: + rq_job["started_at"] = format_timestamp(job.started_at) + + extra["rq-job"] = rq_job + + if "exc_info" in hint: + with capture_internal_exceptions(): + if issubclass(hint["exc_info"][0], JobTimeoutException): + event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] + + return event + + return event_processor + + +def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: + client = sentry_sdk.get_client() + + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "rq", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py new file mode 100644 index 0000000000..622e3c17af --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py @@ -0,0 +1,300 @@ +""" +This integration ingests tracing data from native extensions written in Rust. + +Using it requires additional setup on the Rust side to accept a +`RustTracingLayer` Python object and register it with the `tracing-subscriber` +using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: +```rust +#[pyfunction] +pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { + tracing_subscriber::registry() + .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) + .init(); +} +``` + +Usage in Python would then look like: +``` +sentry_sdk.init( + dsn=sentry_dsn, + integrations=[ + RustTracingIntegration( + "demo_rust_extension", + demo_rust_extension.initialize_tracing, + event_type_mapping=event_type_mapping, + ) + ], +) +``` + +Each native extension requires its own integration. +""" + +import json +from enum import Enum, auto +from typing import Any, Callable, Dict, Optional, Union + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span as SentrySpan +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE + + +class RustTracingLevel(Enum): + Trace = "TRACE" + Debug = "DEBUG" + Info = "INFO" + Warn = "WARN" + Error = "ERROR" + + +class EventTypeMapping(Enum): + Ignore = auto() + Exc = auto() + Breadcrumb = auto() + Event = auto() + + +def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": + level = RustTracingLevel(level) + if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): + return "debug" + elif level == RustTracingLevel.Info: + return "info" + elif level == RustTracingLevel.Warn: + return "warning" + elif level == RustTracingLevel.Error: + return "error" + else: + # Better this than crashing + return "info" + + +def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": + metadata = event.get("metadata", {}) + contexts = {} + + location = {} + for field in ["module_path", "file", "line"]: + if field in metadata: + location[field] = metadata[field] + if len(location) > 0: + contexts["rust_tracing_location"] = location + + fields = {} + for field in metadata.get("fields", []): + fields[field] = event.get(field) + if len(fields) > 0: + contexts["rust_tracing_fields"] = fields + + return contexts + + +def process_event(event: "Dict[str, Any]") -> None: + metadata = event.get("metadata", {}) + + logger = metadata.get("target") + level = tracing_level_to_sentry_level(metadata.get("level")) + message: "sentry_sdk._types.Any" = event.get("message") + contexts = extract_contexts(event) + + sentry_event: "sentry_sdk._types.Event" = { + "logger": logger, + "level": level, + "message": message, + "contexts": contexts, + } + + sentry_sdk.capture_event(sentry_event) + + +def process_exception(event: "Dict[str, Any]") -> None: + process_event(event) + + +def process_breadcrumb(event: "Dict[str, Any]") -> None: + level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) + message = event.get("message") + + sentry_sdk.add_breadcrumb(level=level, message=message) + + +def default_span_filter(metadata: "Dict[str, Any]") -> bool: + return RustTracingLevel(metadata.get("level")) in ( + RustTracingLevel.Error, + RustTracingLevel.Warn, + RustTracingLevel.Info, + ) + + +def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": + level = RustTracingLevel(metadata.get("level")) + if level == RustTracingLevel.Error: + return EventTypeMapping.Exc + elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): + return EventTypeMapping.Breadcrumb + elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): + return EventTypeMapping.Ignore + else: + return EventTypeMapping.Ignore + + +class RustTracingLayer: + def __init__( + self, + origin: str, + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.origin = origin + self.event_type_mapping = event_type_mapping + self.span_filter = span_filter + self.include_tracing_fields = include_tracing_fields + + def _include_tracing_fields(self) -> bool: + """ + By default, the values of tracing fields are not included in case they + contain PII. A user may override that by passing `True` for the + `include_tracing_fields` keyword argument of this integration or by + setting `send_default_pii` to `True` in their Sentry client options. + """ + return ( + should_send_default_pii() + if self.include_tracing_fields is None + else self.include_tracing_fields + ) + + def on_event(self, event: str, sentry_span: "SentrySpan") -> None: + deserialized_event = json.loads(event) + metadata = deserialized_event.get("metadata", {}) + + event_type = self.event_type_mapping(metadata) + if event_type == EventTypeMapping.Ignore: + return + elif event_type == EventTypeMapping.Exc: + process_exception(deserialized_event) + elif event_type == EventTypeMapping.Breadcrumb: + process_breadcrumb(deserialized_event) + elif event_type == EventTypeMapping.Event: + process_event(deserialized_event) + + def on_new_span( + self, attrs: str, span_id: str + ) -> "Optional[Union[SentrySpan, StreamedSpan]]": + attrs = json.loads(attrs) + metadata = attrs.get("metadata", {}) + + if not self.span_filter(metadata): + return None + + module_path = metadata.get("module_path") + name = metadata.get("name") + message = attrs.get("message") + + if message is not None: + sentry_span_name = message + elif module_path is not None and name is not None: + sentry_span_name = f"{module_path}::{name}" # noqa: E231 + elif name is not None: + sentry_span_name = name + else: + sentry_span_name = "" + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + sentry_span = sentry_sdk.traces.start_span( + name=sentry_span_name, + attributes={ + "sentry.op": "function", + "sentry.origin": self.origin, + }, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_attribute(field, attrs.get(field)) + else: + sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) + + return sentry_span + + sentry_span = sentry_sdk.start_span( + op="function", + name=sentry_span_name, + origin=self.origin, + ) + fields = metadata.get("fields", []) + for field in fields: + if self._include_tracing_fields(): + sentry_span.set_data(field, attrs.get(field)) + else: + sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) + + sentry_span.__enter__() + return sentry_span + + def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: + if sentry_span is None: + return + + sentry_span.__exit__(None, None, None) + + def on_record( + self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" + ) -> None: + if sentry_span is None: + return + + set_on_span = ( + sentry_span.set_attribute + if isinstance(sentry_span, StreamedSpan) + else sentry_span.set_data + ) + + deserialized_values = json.loads(values) + for key, value in deserialized_values.items(): + if self._include_tracing_fields(): + set_on_span(key, value) + else: + set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) + + +class RustTracingIntegration(Integration): + """ + Ingests tracing data from a Rust native extension's `tracing` instrumentation. + + If a project uses more than one Rust native extension, each one will need + its own instance of `RustTracingIntegration` with an initializer function + specific to that extension. + + Since all of the setup for this integration requires instance-specific state + which is not available in `setup_once()`, setup instead happens in `__init__()`. + """ + + def __init__( + self, + identifier: str, + initializer: "Callable[[RustTracingLayer], None]", + event_type_mapping: """Callable[ + [Dict[str, Any]], EventTypeMapping + ]""" = default_event_type_mapping, + span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, + include_tracing_fields: "Optional[bool]" = None, + ): + self.identifier = identifier + origin = f"auto.function.rust_tracing.{identifier}" + self.tracing_layer = RustTracingLayer( + origin, event_type_mapping, span_filter, include_tracing_fields + ) + + initializer(self.tracing_layer) + + @staticmethod + def setup_once() -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py new file mode 100644 index 0000000000..908fceb0cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py @@ -0,0 +1,431 @@ +import sys +import warnings +import weakref +from inspect import isawaitable +from typing import TYPE_CHECKING +from urllib.parse import urlsplit + +import sentry_sdk +from sentry_sdk import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + reraise, +) + +if TYPE_CHECKING: + from collections.abc import Container + from typing import Any, Callable, Dict, Optional, Union + + from sanic.request import Request, RequestParameters + from sanic.response import BaseHTTPResponse + from sanic.router import Route + + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint + +try: + from sanic import Sanic + from sanic import __version__ as SANIC_VERSION + from sanic.exceptions import SanicException + from sanic.handlers import ErrorHandler + from sanic.router import Router +except ImportError: + raise DidNotEnable("Sanic not installed") + +old_error_handler_lookup = ErrorHandler.lookup +old_handle_request = Sanic.handle_request +old_router_get = Router.get + +try: + # This method was introduced in Sanic v21.9 + old_startup = Sanic._startup +except AttributeError: + pass + + +class SanicIntegration(Integration): + identifier = "sanic" + origin = f"auto.http.{identifier}" + version: "Optional[tuple[int, ...]]" = None + + def __init__( + self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) + ) -> None: + """ + The unsampled_statuses parameter can be used to specify for which HTTP statuses the + transactions should not be sent to Sentry. By default, transactions are sent for all + HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all + HTTP statuses, including 404. + """ + self._unsampled_statuses = unsampled_statuses or set() + + @staticmethod + def setup_once() -> None: + SanicIntegration.version = parse_version(SANIC_VERSION) + _check_minimum_version(SanicIntegration, SanicIntegration.version) + + if not HAS_REAL_CONTEXTVARS: + # We better have contextvars or we're going to leak state between + # requests. + raise DidNotEnable( + "The sanic integration for Sentry requires Python 3.7+ " + " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE + ) + + if SANIC_VERSION.startswith("0.8."): + # Sanic 0.8 and older creates a logger named "root" and puts a + # stringified version of every exception in there (without exc_info), + # which our error deduplication can't detect. + # + # We explicitly check the version here because it is a very + # invasive step to ignore this logger and not necessary in newer + # versions at all. + # + # https://github.com/huge-success/sanic/issues/1332 + ignore_logger("root") + + if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): + _setup_legacy_sanic() + return + + _setup_sanic() + + +class SanicRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return dict(self.request.cookies) + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "RequestParameters": + return self.request.form + + def is_json(self) -> bool: + raise NotImplementedError() + + def json(self) -> "Optional[Any]": + return self.request.json + + def files(self) -> "RequestParameters": + return self.request.files + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) + + +def _setup_sanic() -> None: + Sanic._startup = _startup + ErrorHandler.lookup = _sentry_error_handler_lookup + + +def _setup_legacy_sanic() -> None: + Sanic.handle_request = _legacy_handle_request + Router.get = _legacy_router_get + ErrorHandler.lookup = _sentry_error_handler_lookup + + +async def _startup(self: "Sanic") -> None: + # This happens about as early in the lifecycle as possible, just after the + # Request object is created. The body has not yet been consumed. + self.signal("http.lifecycle.request")(_context_enter) + + # This happens after the handler is complete. In v21.9 this signal is not + # dispatched when there is an exception. Therefore we need to close out + # and call _context_exit from the custom exception handler as well. + # See https://github.com/sanic-org/sanic/issues/2297 + self.signal("http.lifecycle.response")(_context_exit) + + # This happens inside of request handling immediately after the route + # has been identified by the router. + self.signal("http.routing.after")(_set_transaction) + + # The above signals need to be declared before this can be called. + await old_startup(self) + + +async def _context_enter(request: "Request") -> None: + request.ctx._sentry_do_integration = ( + sentry_sdk.get_client().get_integration(SanicIntegration) is not None + ) + + if not request.ctx._sentry_do_integration: + return + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + weak_request = weakref.ref(request) + request.ctx._sentry_scope = sentry_sdk.isolation_scope() + scope = request.ctx._sentry_scope.__enter__() + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + if is_span_streaming_enabled: + integration = client.get_integration(SanicIntegration) + if ( + isinstance(integration, SanicIntegration) + and integration._unsampled_statuses + ): + warnings.warn( + "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", + stacklevel=2, + ) + + sentry_sdk.traces.continue_trace(dict(request.headers)) + scope.set_custom_sampling_context({"sanic_request": request}) + + if should_send_default_pii() and request.remote_addr: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + + span = sentry_sdk.traces.start_span( + # Unless the request results in a 404 error, the name and source + # will get overwritten in _set_transaction + name=request.path, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": SanicIntegration.origin, + "sentry.span.source": SegmentSource.URL.value, + }, + parent_span=None, + ) + request.ctx._sentry_root_span = span + else: + transaction = continue_trace( + dict(request.headers), + op=OP.HTTP_SERVER, + # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction + name=request.path, + source=TransactionSource.URL, + origin=SanicIntegration.origin, + ) + request.ctx._sentry_root_span = sentry_sdk.start_transaction( + transaction + ).__enter__() + + +async def _context_exit( + request: "Request", response: "Optional[BaseHTTPResponse]" = None +) -> None: + with capture_internal_exceptions(): + if not request.ctx._sentry_do_integration: + return + + integration = sentry_sdk.get_client().get_integration(SanicIntegration) + + response_status = None if response is None else response.status + + # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception + # happens while trying to end the transaction, we still attempt to exit the hub. + with capture_internal_exceptions(): + span = request.ctx._sentry_root_span + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes(request).items(): + span.set_attribute(attr, value) + if response_status is not None: + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) + span.status = "error" if response_status >= 400 else "ok" + + span.end() + + else: + span.set_http_status(response_status) + span.sampled &= ( + isinstance(integration, SanicIntegration) + and response_status not in integration._unsampled_statuses + ) + + span.__exit__(None, None, None) + + request.ctx._sentry_scope.__exit__(None, None, None) + + +async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: + if request.ctx._sentry_do_integration: + with capture_internal_exceptions(): + scope = sentry_sdk.get_current_scope() + route_name = route.name.replace(request.app.name, "").strip(".") + scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) + + +def _sentry_error_handler_lookup( + self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" +) -> "Optional[object]": + _capture_exception(exception) + old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) + + if old_error_handler is None: + return None + + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return old_error_handler + + async def sentry_wrapped_error_handler( + request: "Request", exception: Exception + ) -> "Any": + try: + response = old_error_handler(request, exception) + if isawaitable(response): + response = await response + return response + except Exception: + # Report errors that occur in Sanic error handler. These + # exceptions will not even show up in Sanic's + # `sanic.exceptions` logger. + exc_info = sys.exc_info() + _capture_exception(exc_info) + reraise(*exc_info) + finally: + # As mentioned in previous comment in _startup, this can be removed + # after https://github.com/sanic-org/sanic/issues/2297 is resolved + if SanicIntegration.version and SanicIntegration.version == (21, 9): + await _context_exit(request) + + return sentry_wrapped_error_handler + + +async def _legacy_handle_request( + self: "Any", request: "Request", *args: "Any", **kwargs: "Any" +) -> "Any": + if sentry_sdk.get_client().get_integration(SanicIntegration) is None: + return await old_handle_request(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + scope.add_event_processor(_make_request_processor(weak_request)) + + response = old_handle_request(self, request, *args, **kwargs) + if isawaitable(response): + response = await response + + return response + + +def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": + rv = old_router_get(self, *args) + if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: + with capture_internal_exceptions(): + scope = sentry_sdk.get_isolation_scope() + if SanicIntegration.version and SanicIntegration.version >= (21, 3): + # Sanic versions above and including 21.3 append the app name to the + # route name, and so we need to remove it from Route name so the + # transaction name is consistent across all versions + sanic_app_name = self.ctx.app.name + sanic_route = rv[0].name + + if sanic_route.startswith("%s." % sanic_app_name): + # We add a 1 to the len of the sanic_app_name because there is a dot + # that joins app name and the route name + # Format: app_name.route_name + sanic_route = sanic_route[len(sanic_app_name) + 1 :] + + scope.set_transaction_name( + sanic_route, source=TransactionSource.COMPONENT + ) + else: + scope.set_transaction_name( + rv[0].__name__, source=TransactionSource.COMPONENT + ) + + return rv + + +@ensure_integration_enabled(SanicIntegration) +def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: + with capture_internal_exceptions(): + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "sanic", "handled": False}, + ) + + if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: + return + + sentry_sdk.capture_event(event, hint=hint) + + +def _get_request_attributes(request: "Request") -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from a Sanic request. + """ + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + urlparts = urlsplit(request.url) + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.url + attributes["url.path"] = urlparts.path + + if urlparts.query: + attributes[SPANDATA.HTTP_QUERY] = urlparts.query + + if urlparts.scheme: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme + + if should_send_default_pii() and request.remote_addr: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr + + return attributes + + +def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": + def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": + try: + if hint and issubclass(hint["exc_info"][0], SanicException): + return None + except KeyError: + pass + + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + extractor = SanicRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + urlparts = urlsplit(request.url) + + request_info["url"] = "%s://%s%s" % ( + urlparts.scheme, + urlparts.netloc, + urlparts.path, + ) + + request_info["query_string"] = urlparts.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_addr} + request_info["headers"] = _filter_headers(dict(request.headers)) + + return event + + return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py new file mode 100644 index 0000000000..16f91b28ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py @@ -0,0 +1,65 @@ +import sys +from functools import wraps +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import event_from_exception, reraise + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar, Union, overload + + F = TypeVar("F", bound=Callable[..., Any]) + +else: + + def overload(x: "F") -> "F": + return x + + +@overload +def serverless_function(f: "F", flush: bool = True) -> "F": + pass + + +@overload +def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 + pass + + +def serverless_function( # noqa + f: "Optional[F]" = None, flush: bool = True +) -> "Union[F, Callable[[F], F]]": + def wrapper(f: "F") -> "F": + @wraps(f) + def inner(*args: "Any", **kwargs: "Any") -> "Any": + with sentry_sdk.isolation_scope() as scope: + scope.clear_breadcrumbs() + + try: + return f(*args, **kwargs) + except Exception: + _capture_and_reraise() + finally: + if flush: + sentry_sdk.flush() + + return inner # type: ignore + + if f is None: + return wrapper + else: + return wrapper(f) + + +def _capture_and_reraise() -> None: + exc_info = sys.exc_info() + client = sentry_sdk.get_client() + if client.is_active(): + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "serverless", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py new file mode 100644 index 0000000000..775170fb9f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py @@ -0,0 +1,142 @@ +import socket + +import sentry_sdk +from sentry_sdk._types import MYPY +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.tracing_utils import has_span_streaming_enabled + +if MYPY: + from socket import AddressFamily, SocketKind + from typing import List, Optional, Tuple, Union + +__all__ = ["SocketIntegration"] + + +class SocketIntegration(Integration): + identifier = "socket" + origin = f"auto.socket.{identifier}" + + @staticmethod + def setup_once() -> None: + """ + patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) + """ + _patch_create_connection() + _patch_getaddrinfo() + + +def _get_span_description( + host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" +) -> str: + try: + host = host.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + try: + port = port.decode() # type: ignore + except (UnicodeDecodeError, AttributeError): + pass + + description = "%s:%s" % (host, port) # type: ignore + return description + + +def _patch_create_connection() -> None: + real_create_connection = socket.create_connection + + def create_connection( + address: "Tuple[Optional[str], int]", + timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore + source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, + ) -> "socket.socket": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_create_connection(address, timeout, source_address) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(address[0], address[1]), + attributes={ + "sentry.op": OP.SOCKET_CONNECTION, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if address[0] is not None: + span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) + span.set_attribute(SPANDATA.SERVER_PORT, address[1]) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_CONNECTION, + name=_get_span_description(address[0], address[1]), + origin=SocketIntegration.origin, + ) as span: + span.set_data("address", address) + span.set_data("timeout", timeout) + span.set_data("source_address", source_address) + + return real_create_connection( + address=address, timeout=timeout, source_address=source_address + ) + + socket.create_connection = create_connection # type: ignore + + +def _patch_getaddrinfo() -> None: + real_getaddrinfo = socket.getaddrinfo + + def getaddrinfo( + host: "Union[bytes, str, None]", + port: "Union[bytes, str, int, None]", + family: int = 0, + type: int = 0, + proto: int = 0, + flags: int = 0, + ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": + client = sentry_sdk.get_client() + integration = client.get_integration(SocketIntegration) + if integration is None: + return real_getaddrinfo(host, port, family, type, proto, flags) + + if has_span_streaming_enabled(client.options): + with sentry_sdk.traces.start_span( + name=_get_span_description(host, port), + attributes={ + "sentry.op": OP.SOCKET_DNS, + "sentry.origin": SocketIntegration.origin, + }, + ) as span: + if isinstance(host, str): + span.set_attribute(SPANDATA.SERVER_ADDRESS, host) + elif isinstance(host, bytes): + span.set_attribute( + SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") + ) + + if isinstance(port, int): + span.set_attribute(SPANDATA.SERVER_PORT, port) + elif port is not None: + try: + span.set_attribute(SPANDATA.SERVER_PORT, int(port)) + except (ValueError, TypeError): + pass + + return real_getaddrinfo(host, port, family, type, proto, flags) + else: + with sentry_sdk.start_span( + op=OP.SOCKET_DNS, + name=_get_span_description(host, port), + origin=SocketIntegration.origin, + ) as span: + span.set_data("host", host) + span.set_data("port", port) + + return real_getaddrinfo(host, port, family, type, proto, flags) + + socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py new file mode 100644 index 0000000000..10d94163c5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py @@ -0,0 +1,4 @@ +from sentry_sdk.integrations.spark.spark_driver import SparkIntegration +from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration + +__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py new file mode 100644 index 0000000000..a83532b6a6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py @@ -0,0 +1,278 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled + +if TYPE_CHECKING: + from typing import Any, Optional + + from pyspark import SparkContext + + from sentry_sdk._types import Event, Hint + + +class SparkIntegration(Integration): + identifier = "spark" + + @staticmethod + def setup_once() -> None: + _setup_sentry_tracing() + + +def _set_app_properties() -> None: + """ + Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. + This allows worker integration to have access to app_name and application_id. + """ + from pyspark import SparkContext + + spark_context = SparkContext._active_spark_context + if spark_context: + spark_context.setLocalProperty( + "sentry_app_name", + spark_context.appName, + ) + spark_context.setLocalProperty( + "sentry_application_id", + spark_context.applicationId, + ) + + +def _start_sentry_listener(sc: "SparkContext") -> None: + """ + Start java gateway server to add custom `SparkListener` + """ + from pyspark.java_gateway import ensure_callback_server_started + + gw = sc._gateway + ensure_callback_server_started(gw) + listener = SentryListener() + sc._jsc.sc().addSparkListener(listener) + + +def _add_event_processor(sc: "SparkContext") -> None: + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + if sentry_sdk.get_client().get_integration(SparkIntegration) is None: + return event + + if sc._active_spark_context is None: + return event + + event.setdefault("user", {}).setdefault("id", sc.sparkUser()) + + event.setdefault("tags", {}).setdefault( + "executor.id", sc._conf.get("spark.executor.id") + ) + event["tags"].setdefault( + "spark-submit.deployMode", + sc._conf.get("spark.submit.deployMode"), + ) + event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) + event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) + event["tags"].setdefault("spark_version", sc.version) + event["tags"].setdefault("app_name", sc.appName) + event["tags"].setdefault("application_id", sc.applicationId) + event["tags"].setdefault("master", sc.master) + event["tags"].setdefault("spark_home", sc.sparkHome) + + event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) + + return event + + +def _activate_integration(sc: "SparkContext") -> None: + _start_sentry_listener(sc) + _set_app_properties() + _add_event_processor(sc) + + +def _patch_spark_context_init() -> None: + from pyspark import SparkContext + + spark_context_init = SparkContext._do_init + + @ensure_integration_enabled(SparkIntegration, spark_context_init) + def _sentry_patched_spark_context_init( + self: "SparkContext", *args: "Any", **kwargs: "Any" + ) -> "Optional[Any]": + rv = spark_context_init(self, *args, **kwargs) + _activate_integration(self) + return rv + + SparkContext._do_init = _sentry_patched_spark_context_init + + +def _setup_sentry_tracing() -> None: + from pyspark import SparkContext + + if SparkContext._active_spark_context is not None: + _activate_integration(SparkContext._active_spark_context) + return + _patch_spark_context_init() + + +class SparkListener: + def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 + pass + + def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorBlacklistedForStage( # noqa: N802 + self, + executorBlacklistedForStage: "Any", # noqa: N803 + ) -> None: + pass + + def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 + pass + + def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 + pass + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 + pass + + def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 + pass + + def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 + pass + + def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 + pass + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + pass + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 + pass + + def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 + pass + + def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 + pass + + class Java: + implements = ["org.apache.spark.scheduler.SparkListenerInterface"] + + +class SentryListener(SparkListener): + def _add_breadcrumb( + self, + level: str, + message: str, + data: "Optional[dict[str, Any]]" = None, + ) -> None: + sentry_sdk.get_isolation_scope().add_breadcrumb( + level=level, message=message, data=data + ) + + def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + message = "Job {} Started".format(jobStart.jobId()) + self._add_breadcrumb(level="info", message=message) + _set_app_properties() + + def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 + level = "" + message = "" + data = {"result": jobEnd.jobResult().toString()} + + if jobEnd.jobResult().toString() == "JobSucceeded": + level = "info" + message = "Job {} Ended".format(jobEnd.jobId()) + else: + level = "warning" + message = "Job {} Failed".format(jobEnd.jobId()) + + self._add_breadcrumb(level=level, message=message, data=data) + + def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 + stage_info = stageSubmitted.stageInfo() + message = "Stage {} Submitted".format(stage_info.stageId()) + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + self._add_breadcrumb(level="info", message=message, data=data) + _set_app_properties() + + def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 + from py4j.protocol import Py4JJavaError # type: ignore + + stage_info = stageCompleted.stageInfo() + message = "" + level = "" + + data = {"name": stage_info.name()} + attempt_id = _get_attempt_id(stage_info) + if attempt_id is not None: + data["attemptId"] = attempt_id + + # Have to Try Except because stageInfo.failureReason() is typed with Scala Option + try: + data["reason"] = stage_info.failureReason().get() + message = "Stage {} Failed".format(stage_info.stageId()) + level = "warning" + except Py4JJavaError: + message = "Stage {} Completed".format(stage_info.stageId()) + level = "info" + + self._add_breadcrumb(level=level, message=message, data=data) + + +def _get_attempt_id(stage_info: "Any") -> "Optional[int]": + try: + return stage_info.attemptId() + except Exception: + pass + + try: + return stage_info.attemptNumber() + except Exception: + pass + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py new file mode 100644 index 0000000000..5906472748 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py @@ -0,0 +1,109 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_hint_with_exc_info, + exc_info_from_error, + single_exception_from_error_tuple, + walk_exception_chain, +) + +if TYPE_CHECKING: + from typing import Any, Optional + + from sentry_sdk._types import Event, ExcInfo, Hint + + +class SparkWorkerIntegration(Integration): + identifier = "spark_worker" + + @staticmethod + def setup_once() -> None: + import pyspark.daemon as original_daemon + + original_daemon.worker_main = _sentry_worker_main + + +def _capture_exception(exc_info: "ExcInfo") -> None: + client = sentry_sdk.get_client() + + mechanism = {"type": "spark", "handled": False} + + exc_info = exc_info_from_error(exc_info) + + exc_type, exc_value, tb = exc_info + rv = [] + + # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + if exc_type not in (SystemExit, EOFError, ConnectionResetError): + rv.append( + single_exception_from_error_tuple( + exc_type, exc_value, tb, client.options, mechanism + ) + ) + + if rv: + rv.reverse() + hint = event_hint_with_exc_info(exc_info) + event: "Event" = {"level": "error", "exception": {"values": rv}} + + _tag_task_context() + + sentry_sdk.capture_event(event, hint=hint) + + +def _tag_task_context() -> None: + from pyspark.taskcontext import TaskContext + + scope = sentry_sdk.get_isolation_scope() + + @scope.add_event_processor + def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": + with capture_internal_exceptions(): + integration = sentry_sdk.get_client().get_integration( + SparkWorkerIntegration + ) + task_context = TaskContext.get() + + if integration is None or task_context is None: + return event + + event.setdefault("tags", {}).setdefault( + "stageId", str(task_context.stageId()) + ) + event["tags"].setdefault("partitionId", str(task_context.partitionId())) + event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) + event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) + + if task_context._localProperties: + if "sentry_app_name" in task_context._localProperties: + event["tags"].setdefault( + "app_name", task_context._localProperties["sentry_app_name"] + ) + event["tags"].setdefault( + "application_id", + task_context._localProperties["sentry_application_id"], + ) + + if "callSite.short" in task_context._localProperties: + event.setdefault("extra", {}).setdefault( + "callSite", task_context._localProperties["callSite.short"] + ) + + return event + + +def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: + import pyspark.worker as original_worker + + try: + original_worker.main(*args, **kwargs) + except SystemExit: + if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: + exc_info = sys.exc_info() + with capture_internal_exceptions(): + _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py new file mode 100644 index 0000000000..962fe4ee53 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py @@ -0,0 +1,185 @@ +from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.traces import SpanStatus, StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + add_query_source, + record_sql_queries, +) +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + parse_version, +) + +try: + from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore + from sqlalchemy.engine import Engine # type: ignore + from sqlalchemy.event import listen # type: ignore +except ImportError: + raise DidNotEnable("SQLAlchemy not installed.") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, ContextManager, Optional, Union + + +class SqlalchemyIntegration(Integration): + identifier = "sqlalchemy" + origin = f"auto.db.{identifier}" + + @staticmethod + def setup_once() -> None: + version = parse_version(SQLALCHEMY_VERSION) + _check_minimum_version(SqlalchemyIntegration, version) + + listen(Engine, "before_cursor_execute", _before_cursor_execute) + listen(Engine, "after_cursor_execute", _after_cursor_execute) + listen(Engine, "handle_error", _handle_error) + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _before_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + executemany: bool, + *args: "Any", +) -> None: + ctx_mgr = record_sql_queries( + cursor, + statement, + parameters, + paramstyle=context and context.dialect and context.dialect.paramstyle or None, + executemany=executemany, + span_origin=SqlalchemyIntegration.origin, + ) + context._sentry_sql_span_manager = ctx_mgr + + span = ctx_mgr.__enter__() + + if span is not None: + _set_db_data(span, conn) + context._sentry_sql_span = span + + +@ensure_integration_enabled(SqlalchemyIntegration) +def _after_cursor_execute( + conn: "Any", + cursor: "Any", + statement: "Any", + parameters: "Any", + context: "Any", + *args: "Any", +) -> None: + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + context, "_sentry_sql_span_manager", None + ) + + # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. + span: "Optional[Union[Span, StreamedSpan]]" = getattr( + context, "_sentry_sql_span", None + ) + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_query_source(span) + + if ctx_mgr is not None: + context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + if isinstance(span, Span): + with capture_internal_exceptions(): + add_query_source(span) + + +def _handle_error(context: "Any", *args: "Any") -> None: + execution_context = context.execution_context + if execution_context is None: + return + + span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) + + if span is not None: + if isinstance(span, StreamedSpan): + span.status = SpanStatus.ERROR + else: + span.set_status(SPANSTATUS.INTERNAL_ERROR) + + # _after_cursor_execute does not get called for crashing SQL stmts. Judging + # from SQLAlchemy codebase it does seem like any error coming into this + # handler is going to be fatal. + ctx_mgr: "Optional[ContextManager[Any]]" = getattr( + execution_context, "_sentry_sql_span_manager", None + ) + + if ctx_mgr is not None: + execution_context._sentry_sql_span_manager = None + ctx_mgr.__exit__(None, None, None) + + +# See: https://docs.sqlalchemy.org/en/20/dialects/index.html +def _get_db_system(name: str) -> "Optional[str]": + name = str(name) + + if "sqlite" in name: + return "sqlite" + + if "postgres" in name: + return "postgresql" + + if "mariadb" in name: + return "mariadb" + + if "mysql" in name: + return "mysql" + + if "oracle" in name: + return "oracle" + + return None + + +def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: + db_system = _get_db_system(conn.engine.name) + + if isinstance(span, StreamedSpan): + if db_system is not None: + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) + else: + if db_system is not None: + span.set_data(SPANDATA.DB_SYSTEM, db_system) + + if isinstance(span, StreamedSpan): + set_on_span = span.set_attribute + else: + set_on_span = span.set_data + + try: + driver = conn.dialect.driver + if driver: + set_on_span(SPANDATA.DB_DRIVER_NAME, driver) + except Exception: + pass + + if conn.engine.url is None: + return + + db_name = conn.engine.url.database + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + server_address = conn.engine.url.host + if server_address is not None: + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) + + server_port = conn.engine.url.port + if server_port is not None: + set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py new file mode 100644 index 0000000000..3f9fbf4d8e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py @@ -0,0 +1,858 @@ +import functools +import json +import sys +import warnings +from collections.abc import Set +from copy import deepcopy +from json import JSONDecodeError +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, + DidNotEnable, + Integration, +) +from sentry_sdk.integrations._asgi_common import _RootPathInPath +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + HttpCodeRangeContainer, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import StreamedSpan, get_current_span +from sentry_sdk.tracing import ( + SOURCE_FOR_STYLE, + TransactionSource, +) +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + parse_version, + transaction_from_function, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Awaitable, + Callable, + Container, + Dict, + Optional, + Tuple, + Union, + ) + + from sentry_sdk._types import Event, HttpStatusCodeRange +try: + import starlette + from starlette import __version__ as STARLETTE_VERSION + from starlette.applications import Starlette + from starlette.datastructures import ( + UploadFile, + ) + from starlette.middleware import Middleware + from starlette.middleware.authentication import ( + AuthenticationMiddleware, + ) + from starlette.requests import Request + from starlette.routing import Match + from starlette.types import ASGIApp, Receive, Send + from starlette.types import Scope as StarletteScope +except ImportError: + raise DidNotEnable("Starlette is not installed") + +try: + # Starlette 0.20 + from starlette.middleware.exceptions import ExceptionMiddleware +except ImportError: + # Startlette 0.19.1 + from starlette.exceptions import ExceptionMiddleware # type: ignore + +try: + # Optional dependency of Starlette to parse form data. + try: + # python-multipart 0.0.13 and later + import python_multipart as multipart + except ImportError: + # python-multipart 0.0.12 and earlier + import multipart # type: ignore +except ImportError: + multipart = None # type: ignore[assignment] + + +# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 +if sys.version_info >= (3, 13): # pragma: no cover + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction + + +_DEFAULT_TRANSACTION_NAME = "generic Starlette request" + +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + + +class StarletteIntegration(Integration): + identifier = "starlette" + origin = f"auto.http.{identifier}" + + transaction_style = "" + + def __init__( + self, + transaction_style: str = "url", + failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, + middleware_spans: bool = False, + http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ): + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style + self.middleware_spans = middleware_spans + self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) + + if isinstance(failed_request_status_codes, Set): + self.failed_request_status_codes: "Container[int]" = ( + failed_request_status_codes + ) + else: + warnings.warn( + "Passing a list or None for failed_request_status_codes is deprecated. " + "Please pass a set of int instead.", + DeprecationWarning, + stacklevel=2, + ) + + if failed_request_status_codes is None: + self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES + else: + self.failed_request_status_codes = HttpCodeRangeContainer( + failed_request_status_codes + ) + + @staticmethod + def setup_once() -> None: + version = parse_version(STARLETTE_VERSION) + + if version is None: + raise DidNotEnable( + "Unparsable Starlette version: {}".format(STARLETTE_VERSION) + ) + + patch_middlewares() + # Starlette tolerates both starting with: + # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. + root_path_in_path = ( + _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED + ) + patch_asgi_app(root_path_in_path=root_path_in_path) + patch_request_response() + + if version >= (0, 24): + patch_templates() + + +def _enable_span_for_middleware( + middleware_class: "Any", +) -> "Any": + old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ + + async def _create_span_call( + app: "Any", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + **kwargs: "Any", + ) -> None: + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await old_call(app, scope, receive, send, **kwargs) + + # Update transaction name with middleware name + name, source = _get_transaction_from_middleware(app, scope, integration) + + if name is not None: + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=source, + ) + + if not integration.middleware_spans: + return await old_call(app, scope, receive, send, **kwargs) + + middleware_name = app.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarletteIntegration.origin, + "middleware.name": middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarletteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlette.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLETTE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlette.middleware_name", middleware_name) + return await send(*args, **kwargs) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(app, scope, new_receive, new_send, **kwargs) + + not_yet_patched = old_call.__name__ not in [ + "_create_span_call", + "_sentry_authenticationmiddleware_call", + "_sentry_exceptionmiddleware_call", + ] + + if not_yet_patched: + middleware_class.__call__ = _create_span_call + + return middleware_class + + +def _serialize_request_body_data(data: "Any") -> str: + # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values + def _default(value: "Any") -> "Any": + if isinstance(value, AnnotatedValue): + return value.value + return str(value) + + return json.dumps(data, default=_default) + + +@ensure_integration_enabled(StarletteIntegration) +def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: + event, hint = event_from_exception( + exception, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarletteIntegration.identifier, "handled": handled}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def patch_exception_middleware(middleware_class: "Any") -> None: + """ + Capture all exceptions in Starlette app and + also extract user information. + """ + old_middleware_init = middleware_class.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + + if not_yet_patched: + + def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: + old_middleware_init(self, *args, **kwargs) + + # Patch existing exception handlers + old_handlers = self._exception_handlers.copy() + + async def _sentry_patched_exception_handler( + self: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration( + StarletteIntegration + ) + + exp = args[0] + + if integration is not None: + is_http_server_error = ( + hasattr(exp, "status_code") + and isinstance(exp.status_code, int) + and exp.status_code in integration.failed_request_status_codes + ) + if is_http_server_error: + _capture_exception(exp, handled=True) + + # Find a matching handler + old_handler = None + for cls in type(exp).__mro__: + if cls in old_handlers: + old_handler = old_handlers[cls] + break + + if old_handler is None: + return + + if _is_async_callable(old_handler): + return await old_handler(self, *args, **kwargs) + else: + return old_handler(self, *args, **kwargs) + + for key in self._exception_handlers.keys(): + self._exception_handlers[key] = _sentry_patched_exception_handler + + middleware_class.__init__ = _sentry_middleware_init + + old_call = middleware_class.__call__ + + async def _sentry_exceptionmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + # Also add the user (that was eventually set by be Authentication middle + # that was called before this middleware). This is done because the authentication + # middleware sets the user in the scope and then (in the same function) + # calls this exception middelware. In case there is no exception (or no handler + # for the type of exception occuring) then the exception bubbles up and setting the + # user information into the sentry scope is done in auth middleware and the + # ASGI middleware will then send everything to Sentry and this is fine. + # But if there is an exception happening that the exception middleware here + # has a handler for, it will send the exception directly to Sentry, so we need + # the user information right now. + # This is why we do it here. + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_exceptionmiddleware_call + + +@ensure_integration_enabled(StarletteIntegration) +def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: + """ + Extracts user information from the ASGI scope and + adds it to Sentry's scope. + """ + if "user" not in scope: + return + + if not should_send_default_pii(): + return + + user_info: "Dict[str, Any]" = {} + starlette_user = scope["user"] + + username = getattr(starlette_user, "username", None) + if username: + user_info.setdefault("username", starlette_user.username) + + user_id = getattr(starlette_user, "id", None) + if user_id: + user_info.setdefault("id", starlette_user.id) + + email = getattr(starlette_user, "email", None) + if email: + user_info.setdefault("email", starlette_user.email) + + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + +def patch_authentication_middleware(middleware_class: "Any") -> None: + """ + Add user information to Sentry scope. + """ + old_call = middleware_class.__call__ + + not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) + + if not_yet_patched: + + async def _sentry_authenticationmiddleware_call( + self: "Dict[str, Any]", + scope: "Dict[str, Any]", + receive: "Callable[[], Awaitable[Dict[str, Any]]]", + send: "Callable[[Dict[str, Any]], Awaitable[None]]", + ) -> None: + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_authenticationmiddleware_call + + +def patch_middlewares() -> None: + """ + Patches Starlettes `Middleware` class to record + spans for every middleware invoked. + """ + old_middleware_init = Middleware.__init__ + + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) + if not_yet_patched: + + def _sentry_middleware_init( + self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" + ) -> None: + if cls == SentryAsgiMiddleware: + return old_middleware_init(self, cls, *args, **kwargs) + + span_enabled_cls = _enable_span_for_middleware(cls) + old_middleware_init(self, span_enabled_cls, *args, **kwargs) + + if cls == AuthenticationMiddleware: + patch_authentication_middleware(cls) + + if cls == ExceptionMiddleware: + patch_exception_middleware(cls) + + Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] + + +def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: + """ + Instrument Starlette ASGI app using the SentryAsgiMiddleware. + """ + old_app = Starlette.__call__ + + async def _sentry_patched_asgi_app( + self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StarletteIntegration) + if integration is None: + return await old_app(self, scope, receive, send) + + middleware = SentryAsgiMiddleware( + lambda *a, **kw: old_app(self, *a, **kw), + mechanism_type=StarletteIntegration.identifier, + transaction_style=integration.transaction_style, + span_origin=StarletteIntegration.origin, + http_methods_to_capture=( + integration.http_methods_to_capture + if integration + else DEFAULT_HTTP_METHODS_TO_CAPTURE + ), + asgi_version=3, + root_path_in_path=root_path_in_path, + ) + + return await middleware(scope, receive, send) + + Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] + + +# This was vendored in from Starlette to support Starlette 0.19.1 because +# this function was only introduced in 0.20.x +def _is_async_callable(obj: "Any") -> bool: + while isinstance(obj, functools.partial): + obj = obj.func + + return iscoroutinefunction(obj) or ( + callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] + ) + + +def _get_cached_request_body_attribute( + client: "sentry_sdk.client.BaseClient", request: "Request" +) -> "Optional[str]": + """ + Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. + """ + if "content-length" not in request.headers: + return None + + try: + content_length = int(request.headers["content-length"]) + except ValueError: + return None + + if content_length and not request_body_within_bounds(client, content_length): + return OVER_SIZE_LIMIT_SUBSTITUTE + + if hasattr(request, "_json"): + return json.dumps(request._json) + + formdata_body = getattr(request, "_form", None) + if formdata_body is None: + return None + + form_data = {} + for key, val in formdata_body.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = val if not is_file else "[Unparsable]" + + return json.dumps(form_data) + + +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(StarletteIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), + integration.transaction_style, + request, + ) + + sentry_scope = sentry_sdk.get_isolation_scope() + extractor = StarletteRequestExtractor(request) + + info = await extractor.extract_request_info() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + # Add info from request to event + request_info = event.get("request", {}) + if info: + if "cookies" in info: + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = get_current_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + +def patch_request_response() -> None: + old_request_response = starlette.routing.request_response + + def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": + old_func = func + + is_coroutine = _is_async_callable(old_func) + if is_coroutine: + + async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": + return await _wrap_async_handler(old_func, *args, **kwargs) + + func = _sentry_async_func + + else: + + @functools.wraps(old_func) + def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + + integration = client.get_integration(StarletteIntegration) + if integration is None: + return old_func(*args, **kwargs) + + current_scope = sentry_sdk.get_current_scope() + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming: + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + current_span._segment._update_active_thread() + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + request = args[0] + + _set_transaction_name_and_source( + sentry_scope, integration.transaction_style, request + ) + + extractor = StarletteRequestExtractor(request) + cookies = extractor.extract_cookies_from_request() + + def _make_request_event_processor( + req: "Any", integration: "Any" + ) -> "Callable[[Event, dict[str, Any]], Event]": + def event_processor( + event: "Event", hint: "dict[str, Any]" + ) -> "Event": + # Extract information from request + request_info = event.get("request", {}) + if cookies: + request_info["cookies"] = cookies + + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope._name = StarletteIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + return old_func(*args, **kwargs) + + func = _sentry_sync_func + + return old_request_response(func) + + starlette.routing.request_response = _sentry_request_response + + +def patch_templates() -> None: + # If markupsafe is not installed, then Jinja2 is not installed + # (markupsafe is a dependency of Jinja2) + # In this case we do not need to patch the Jinja2Templates class + try: + from markupsafe import Markup + except ImportError: + return # Nothing to do + + # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a + try: + from starlette.templating import Jinja2Templates + except ImportError: + return + + old_jinja2templates_init = Jinja2Templates.__init__ + + not_yet_patched = "_sentry_jinja2templates_init" not in str( + old_jinja2templates_init + ) + + if not_yet_patched: + + def _sentry_jinja2templates_init( + self: "Jinja2Templates", *args: "Any", **kwargs: "Any" + ) -> None: + def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": + trace_meta = Markup( + sentry_sdk.get_current_scope().trace_propagation_meta() + ) + return { + "sentry_trace_meta": trace_meta, + } + + kwargs.setdefault("context_processors", []) + + if add_sentry_trace_meta not in kwargs["context_processors"]: + kwargs["context_processors"].append(add_sentry_trace_meta) + + return old_jinja2templates_init(self, *args, **kwargs) + + Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] + + +class StarletteRequestExtractor: + """ + Extracts useful information from the Starlette request + (like form data or cookies) and adds it to the Sentry event. + """ + + def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: + self.request = request + + def extract_cookies_from_request( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + cookies: "Optional[Dict[str, Any]]" = None + if should_send_default_pii(): + cookies = self.cookies() + + return cookies + + async def extract_request_info( + self: "StarletteRequestExtractor", + ) -> "Optional[Dict[str, Any]]": + client = sentry_sdk.get_client() + + request_info: "Dict[str, Any]" = {} + + with capture_internal_exceptions(): + # Add cookies + if should_send_default_pii(): + request_info["cookies"] = self.cookies() + + # If there is no body, just return the cookies + content_length = await self.content_length() + if not content_length: + return request_info + + # Add annotation if body is too big + if content_length and not request_body_within_bounds( + client, content_length + ): + request_info["data"] = AnnotatedValue.removed_because_over_size_limit() + return request_info + + # Add JSON body, if it is a JSON request + json = await self.json() + if json: + request_info["data"] = json + return request_info + + # Add form as key/value pairs, if request has form data + form = await self.form() + if form: + form_data = {} + for key, val in form.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = ( + val + if not is_file + else AnnotatedValue.removed_because_raw_data() + ) + + request_info["data"] = form_data + return request_info + + # Raw data, do not add body just an annotation + request_info["data"] = AnnotatedValue.removed_because_raw_data() + return request_info + + async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": + if "content-length" in self.request.headers: + return int(self.request.headers["content-length"]) + + return None + + def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": + return self.request.cookies + + async def form(self: "StarletteRequestExtractor") -> "Any": + if multipart is None: + return None + + # Parse the body first to get it cached, as Starlette does not cache form() as it + # does with body() and json() https://github.com/encode/starlette/discussions/1933 + # Calling `.form()` without calling `.body()` first will + # potentially break the users project. + await self.request.body() + + return await self.request.form() + + def is_json(self: "StarletteRequestExtractor") -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": + if not self.is_json(): + return None + try: + return await self.request.json() + except JSONDecodeError: + return None + + +def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": + router = scope.get("router") + if not router: + return None + + for route in router.routes: + match = route.matches(scope) + if match[0] == Match.FULL: + try: + return route.path + except AttributeError: + # routes added via app.host() won't have a path attribute + return scope.get("path") + + return None + + +def _set_transaction_name_and_source( + scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" +) -> None: + name = None + source = SOURCE_FOR_STYLE[transaction_style] + + if transaction_style == "endpoint": + endpoint = request.scope.get("endpoint") + if endpoint: + name = transaction_from_function(endpoint) or None + + elif transaction_style == "url": + name = _transaction_name_from_router(request.scope) + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + scope.set_transaction_name(name, source=source) + + +def _get_transaction_from_middleware( + app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" +) -> "Tuple[Optional[str], Optional[str]]": + name = None + source = None + + if integration.transaction_style == "endpoint": + name = transaction_from_function(app.__class__) + source = TransactionSource.COMPONENT + elif integration.transaction_style == "url": + name = _transaction_name_from_router(asgi_scope) + source = TransactionSource.ROUTE + + return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py new file mode 100644 index 0000000000..1eebd37e84 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py @@ -0,0 +1,314 @@ +from copy import deepcopy + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from pydantic import BaseModel + from starlite import Request, Starlite, State # type: ignore + from starlite.handlers.base import BaseRouteHandler # type: ignore + from starlite.middleware import DefineMiddleware # type: ignore + from starlite.plugins.base import get_plugin_for_value # type: ignore + from starlite.routes.http import HTTPRoute # type: ignore + from starlite.utils import ( # type: ignore + ConnectionDataExtractor, + Ref, + is_async_callable, + ) +except ImportError: + raise DidNotEnable("Starlite is not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Optional, Union + + from starlite import MiddlewareProtocol + from starlite.types import ( # type: ignore + ASGIApp, + Hint, + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Send, + WebSocketReceiveMessage, + ) + from starlite.types import ( + Scope as StarliteScope, + ) + + from sentry_sdk._types import Event + + +_DEFAULT_TRANSACTION_NAME = "generic Starlite request" + + +class StarliteIntegration(Integration): + identifier = "starlite" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + +class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): + def __init__( + self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin + ) -> None: + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + asgi_version=3, + ) + + +def patch_app_init() -> None: + """ + Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. + See: + - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception + - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ + """ + old__init__ = Starlite.__init__ + + @ensure_integration_enabled(StarliteIntegration, old__init__) + def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: + after_exception = kwargs.pop("after_exception", []) + kwargs.update( + after_exception=[ + exception_handler, + *( + after_exception + if isinstance(after_exception, list) + else [after_exception] + ), + ] + ) + + middleware = kwargs.get("middleware") or [] + kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Starlite.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) + def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old_resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryStarliteASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "StarliteScope", + receive: "Receive", + send: "Send", + ) -> None: + client = sentry_sdk.get_client() + if client.get_integration(StarliteIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarliteIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarliteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE, name=middleware_name + ) as middleware_span: + if not is_span_streaming_enabled: + middleware_span.set_tag("starlite.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await receive(*args, **kwargs) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_RECEIVE, + name=getattr(receive, "__qualname__", str(receive)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await send(message) + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE_SEND, + name=getattr(send, "__qualname__", str(send)), + ) as span: + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = sentry_sdk.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + route_handler = scope.get("route_handler") + + func = None + if route_handler.name is not None: + name = route_handler.name + elif isinstance(route_handler.fn, Ref): + func = route_handler.fn.value + else: + func = route_handler.fn + if func is not None: + name = transaction_from_function(func) + + source = SOURCE_FOR_STYLE["endpoint"] + + if not name: + name = _DEFAULT_TRANSACTION_NAME + source = TransactionSource.ROUTE + + sentry_sdk.set_transaction_name(name, source) + sentry_scope.set_transaction_name(name, source) + + def event_processor(event: "Event", _: "Hint") -> "Event": + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + event["request"] = deepcopy(request_info) + return event + + sentry_scope._name = StarliteIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": + scope_user = scope.get("user") + if not scope_user: + return None + if isinstance(scope_user, dict): + return scope_user + if isinstance(scope_user, BaseModel): + return scope_user.dict() + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + plugin = get_plugin_for_value(scope_user) + if plugin and not is_async_callable(plugin.to_dict): + return plugin.to_dict(scope_user) + + return None + + +@ensure_integration_enabled(StarliteIntegration) +def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: + user_info: "Optional[dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope.set_user(user_info) + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": StarliteIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py new file mode 100644 index 0000000000..746e09b36f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py @@ -0,0 +1,37 @@ +from functools import wraps +from typing import TYPE_CHECKING, Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.utils import parse_version + +try: + from statsig import statsig as statsig_module + from statsig.version import __version__ as STATSIG_VERSION +except ImportError: + raise DidNotEnable("statsig is not installed") + +if TYPE_CHECKING: + from statsig.statsig_user import StatsigUser + + +class StatsigIntegration(Integration): + identifier = "statsig" + + @staticmethod + def setup_once() -> None: + version = parse_version(STATSIG_VERSION) + _check_minimum_version(StatsigIntegration, version, "statsig") + + # Wrap and patch evaluation method(s) in the statsig module + old_check_gate = statsig_module.check_gate + + @wraps(old_check_gate) + def sentry_check_gate( + user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_check_gate(user, gate, *args, **kwargs) + add_feature_flag(gate, enabled) + return enabled + + statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py new file mode 100644 index 0000000000..82f30f2dda --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py @@ -0,0 +1,404 @@ +import os +import platform +import subprocess +import sys +from http.client import HTTPConnection, HTTPResponse +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor, should_send_default_pii +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import ( + EnvironHeaders, + add_http_request_source, + has_span_streaming_enabled, + should_propagate_trace, +) +from sentry_sdk.utils import ( + SENSITIVE_DATA_SUBSTITUTE, + capture_internal_exceptions, + ensure_integration_enabled, + is_sentry_url, + logger, + parse_url, + safe_repr, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Union + + from sentry_sdk._types import Event, Hint + + +_RUNTIME_CONTEXT: "dict[str, object]" = { + "name": platform.python_implementation(), + "version": "%s.%s.%s" % (sys.version_info[:3]), + "build": sys.version, +} + + +class StdlibIntegration(Integration): + identifier = "stdlib" + + @staticmethod + def setup_once() -> None: + _install_httplib() + _install_subprocess() + + @add_global_event_processor + def add_python_runtime_context( + event: "Event", hint: "Hint" + ) -> "Optional[Event]": + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is not None: + contexts = event.setdefault("contexts", {}) + if isinstance(contexts, dict) and "runtime" not in contexts: + contexts["runtime"] = _RUNTIME_CONTEXT + + return event + + +def _complete_span(span: "Union[Span, StreamedSpan]") -> None: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + add_http_request_source(span) + span.end() + else: + span.finish() + with capture_internal_exceptions(): + add_http_request_source(span) + + +def _install_httplib() -> None: + real_putrequest = HTTPConnection.putrequest + real_getresponse = HTTPConnection.getresponse + real_read = HTTPResponse.read + real_close = HTTPResponse.close + + def putrequest( + self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + default_port = self.default_port + + # proxies go through set_tunnel + tunnel_host = getattr(self, "_tunnel_host", None) + if tunnel_host: + host = tunnel_host + port = getattr(self, "_tunnel_port", default_port) + else: + host = self.host + port = self.port + + client = sentry_sdk.get_client() + if client.get_integration(StdlibIntegration) is None or is_sentry_url( + client, host + ): + return real_putrequest(self, method, url, *args, **kwargs) + + real_url = url + if real_url is None or not real_url.startswith(("http://", "https://")): + real_url = "%s://%s%s%s" % ( + default_port == 443 and "https" or "http", + host, + port != default_port and ":%s" % port or "", + url, + ) + + parsed_url = None + with capture_internal_exceptions(): + parsed_url = parse_url(real_url, sanitize=False) + + span_streaming = has_span_streaming_enabled(client.options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + attributes={ + "sentry.origin": "auto.http.stdlib.httplib", + "sentry.op": OP.HTTP_CLIENT, + SPANDATA.HTTP_REQUEST_METHOD: method, + }, + ) + + if parsed_url is not None and should_send_default_pii(): + span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) + span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) + span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) + + set_on_span = span.set_attribute + + else: + span = sentry_sdk.start_span( + op=OP.HTTP_CLIENT, + name="%s %s" + % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), + origin="auto.http.stdlib.httplib", + ) + + span.set_data(SPANDATA.HTTP_METHOD, method) + if parsed_url is not None: + span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + span.set_data("url", parsed_url.url) + span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) + + set_on_span = span.set_data + + # for proxies, these point to the proxy host/port + if tunnel_host: + set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) + set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) + + rv = real_putrequest(self, method, url, *args, **kwargs) + + if should_propagate_trace(client, real_url): + for ( + key, + value, + ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + logger.debug( + "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( + key=key, value=value, real_url=real_url + ) + ) + self.putheader(key, value) + + self._sentrysdk_span = span # type: ignore[attr-defined] + + return rv + + def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": + span = getattr(self, "_sentrysdk_span", None) + + if span is None: + return real_getresponse(self, *args, **kwargs) + + try: + rv = real_getresponse(self, *args, **kwargs) + except BaseException: + _complete_span(span) + raise + + if isinstance(span, StreamedSpan): + status_code = int(rv.status) + span.status = "error" if status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", status_code) + else: + span.set_http_status(int(rv.status)) + span.set_data("reason", rv.reason) + + # getresponse doesn't include actually reading the response body. This + # is done in read(). So if the metadata/headers suggest there's a body to + # read, don't finish the span just yet, but save it for ending it later. + has_body = rv.chunked or (rv.length is not None and rv.length > 0) + if has_body: + rv._sentrysdk_span = span # type: ignore[attr-defined] + else: + _complete_span(span) + + return rv + + def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": + try: + return real_read(self, *args, **kwargs) + finally: + span = getattr(self, "_sentrysdk_span", None) + # read() might be called multiple times to consume a single body, + # so we can't just end the span when read() is done. Instead, + # try to figure out whether the response body has been fully read. + if span and (self.fp is None or self.closed): + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + def close(self: "HTTPResponse") -> None: + # We patch close() as a best effort fallback in case the span is not + # ended yet in getresponse() or read(). + + try: + real_close(self) + finally: + span = getattr(self, "_sentrysdk_span", None) + if span is not None: + self._sentrysdk_span = None # type: ignore[attr-defined] + _complete_span(span) + + HTTPConnection.putrequest = putrequest # type: ignore[method-assign] + HTTPConnection.getresponse = getresponse # type: ignore[method-assign] + HTTPResponse.read = read # type: ignore[method-assign] + HTTPResponse.close = close # type: ignore[assignment,method-assign] + + +def _init_argument( + args: "List[Any]", + kwargs: "Dict[Any, Any]", + name: str, + position: int, + setdefault_callback: "Optional[Callable[[Any], Any]]" = None, +) -> "Any": + """ + given (*args, **kwargs) of a function call, retrieve (and optionally set a + default for) an argument by either name or position. + + This is useful for wrapping functions with complex type signatures and + extracting a few arguments without needing to redefine that function's + entire type signature. + """ + + if name in kwargs: + rv = kwargs[name] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + kwargs[name] = rv + elif position < len(args): + rv = args[position] + if setdefault_callback is not None: + rv = setdefault_callback(rv) + if rv is not None: + args[position] = rv + else: + rv = setdefault_callback and setdefault_callback(None) + if rv is not None: + kwargs[name] = rv + + return rv + + +def _install_subprocess() -> None: + old_popen_init = subprocess.Popen.__init__ + + @ensure_integration_enabled(StdlibIntegration, old_popen_init) + def sentry_patched_popen_init( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> None: + # Convert from tuple to list to be able to set values. + a = list(a) + + args = _init_argument(a, kw, "args", 0) or [] + cwd = _init_argument(a, kw, "cwd", 9) + + # if args is not a list or tuple (and e.g. some iterator instead), + # let's not use it at all. There are too many things that can go wrong + # when trying to collect an iterator into a list and setting that list + # into `a` again. + # + # Also invocations where `args` is not a sequence are not actually + # legal. They just happen to work under CPython. + description = None + + if isinstance(args, (list, tuple)) and len(args) < 100: + with capture_internal_exceptions(): + description = " ".join(map(str, args)) + + if description is None: + description = safe_repr(args) + + env = None + + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + span: "Union[Span, StreamedSpan]" + if span_streaming: + span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.SUBPROCESS, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) + else: + span = sentry_sdk.start_span( + op=OP.SUBPROCESS, + name=description, + origin="auto.subprocess.stdlib.subprocess", + ) + + with span: + for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( + span=span + ): + if env is None: + env = _init_argument( + a, + kw, + "env", + 10, + lambda x: dict(x if x is not None else os.environ), + ) + env["SUBPROCESS_" + k.upper().replace("-", "_")] = v + + if cwd and isinstance(span, Span): + span.set_data("subprocess.cwd", cwd) + + rv = old_popen_init(self, *a, **kw) + + if isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + else: + span.set_tag("subprocess.pid", self.pid) + + return rv + + subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore + + old_popen_wait = subprocess.Popen.wait + + @ensure_integration_enabled(StdlibIntegration, old_popen_wait) + def sentry_patched_popen_wait( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_WAIT, + attributes={ + "sentry.op": OP.SUBPROCESS_WAIT, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_wait(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_WAIT, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_wait(self, *a, **kw) + + subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore + + old_popen_communicate = subprocess.Popen.communicate + + @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) + def sentry_patched_popen_communicate( + self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" + ) -> "Any": + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) + if span_streaming: + with sentry_sdk.traces.start_span( + name=OP.SUBPROCESS_COMMUNICATE, + attributes={ + "sentry.op": OP.SUBPROCESS_COMMUNICATE, + "sentry.origin": "auto.subprocess.stdlib.subprocess", + }, + ) as span: + span.set_attribute(SPANDATA.PROCESS_PID, self.pid) + return old_popen_communicate(self, *a, **kw) + else: + with sentry_sdk.start_span( + op=OP.SUBPROCESS_COMMUNICATE, + origin="auto.subprocess.stdlib.subprocess", + ) as span: + span.set_tag("subprocess.pid", self.pid) + return old_popen_communicate(self, *a, **kw) + + subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore + + +def get_subprocess_traceparent_headers() -> "EnvironHeaders": + return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py new file mode 100644 index 0000000000..5f00e8bf6d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py @@ -0,0 +1,493 @@ +import functools +import hashlib +import warnings +from inspect import isawaitable + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + package_version, +) + +try: + from functools import cached_property +except ImportError: + # The strawberry integration requires Python 3.8+. functools.cached_property + # was added in 3.8, so this check is technically not needed, but since this + # is an auto-enabling integration, we might get to executing this import in + # lower Python versions, so we need to deal with it. + raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") + +try: + from strawberry import Schema + from strawberry.extensions import SchemaExtension + from strawberry.extensions.tracing.utils import ( + should_skip_tracing as strawberry_should_skip_tracing, + ) + from strawberry.http import async_base_view, sync_base_view +except ImportError: + raise DidNotEnable("strawberry-graphql is not installed") + +try: + from strawberry.extensions.tracing import ( + SentryTracingExtension as StrawberrySentryAsyncExtension, + ) + from strawberry.extensions.tracing import ( + SentryTracingExtensionSync as StrawberrySentrySyncExtension, + ) +except ImportError: + StrawberrySentryAsyncExtension = None + StrawberrySentrySyncExtension = None + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, Generator, List, Optional + + from graphql import GraphQLError, GraphQLResolveInfo + from strawberry.http import GraphQLHTTPResponse + from strawberry.types import ExecutionContext + + from sentry_sdk._types import Event, EventProcessor + + +ignore_logger("strawberry.execution") + + +class StrawberryIntegration(Integration): + identifier = "strawberry" + origin = f"auto.graphql.{identifier}" + + def __init__(self, async_execution: "Optional[bool]" = None) -> None: + if async_execution not in (None, False, True): + raise ValueError( + 'Invalid value for async_execution: "{}" (must be bool)'.format( + async_execution + ) + ) + self.async_execution = async_execution + + @staticmethod + def setup_once() -> None: + version = package_version("strawberry-graphql") + _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") + + _patch_schema_init() + _patch_views() + + +def _patch_schema_init() -> None: + old_schema_init = Schema.__init__ + + @functools.wraps(old_schema_init) + def _sentry_patched_schema_init( + self: "Schema", *args: "Any", **kwargs: "Any" + ) -> None: + integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) + if integration is None: + return old_schema_init(self, *args, **kwargs) + + extensions = kwargs.get("extensions") or [] + + should_use_async_extension: "Optional[bool]" = None + if integration.async_execution is not None: + should_use_async_extension = integration.async_execution + else: + # try to figure it out ourselves + should_use_async_extension = _guess_if_using_async(extensions) + + if should_use_async_extension is None: + warnings.warn( + "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", + stacklevel=2, + ) + should_use_async_extension = False + + # remove the built in strawberry sentry extension, if present + extensions = [ + extension + for extension in extensions + if extension + not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) + ] + + # add our extension + extensions = [ + SentryAsyncExtension if should_use_async_extension else SentrySyncExtension + ] + extensions + + kwargs["extensions"] = extensions + + return old_schema_init(self, *args, **kwargs) + + Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] + + +class SentryAsyncExtension(SchemaExtension): + def __init__( + self: "Any", + *, + execution_context: "Optional[ExecutionContext]" = None, + ) -> None: + if execution_context: + self.execution_context = execution_context + + @cached_property + def _resource_name(self) -> str: + query_hash = self.hash_query(self.execution_context.query) # type: ignore + + if self.execution_context.operation_name: + return "{}:{}".format(self.execution_context.operation_name, query_hash) + + return query_hash + + def hash_query(self, query: str) -> str: + return hashlib.md5(query.encode("utf-8")).hexdigest() + + def on_operation(self) -> "Generator[None, None, None]": + operation_name = self.execution_context.operation_name + + operation_type = "query" + op = OP.GRAPHQL_QUERY + + if self.execution_context.query is None: + self.execution_context.query = "" + + if self.execution_context.query.strip().startswith("mutation"): + operation_type = "mutation" + op = OP.GRAPHQL_MUTATION + elif self.execution_context.query.strip().startswith("subscription"): + operation_type = "subscription" + op = OP.GRAPHQL_SUBSCRIPTION + + description = operation_type + if operation_name: + description += " {}".format(operation_name) + + sentry_sdk.add_breadcrumb( + category="graphql.operation", + data={ + "operation_name": operation_name, + "operation_type": operation_type, + }, + ) + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_request_event_processor(self.execution_context) + scope.add_event_processor(event_processor) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + additional_attributes: "dict[str, Any]" = {} + + if should_send_default_pii(): + additional_attributes["graphql.document"] = self.execution_context.query + + if operation_name: + additional_attributes["graphql.operation.name"] = operation_name + + graphql_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": op, + "graphql.operation.type": operation_type, + **additional_attributes, + }, + ) + else: + graphql_span = sentry_sdk.start_span( + op=op, + name=description, + origin=StrawberryIntegration.origin, + ) + graphql_span.__enter__() + + if type(graphql_span) is Span: + if should_send_default_pii(): + graphql_span.set_data("graphql.document", self.execution_context.query) + + graphql_span.set_data("graphql.operation.type", operation_type) + graphql_span.set_data("graphql.operation.name", operation_name) + # This attribute is being removed in streamed spans + graphql_span.set_data("graphql.resource_name", self._resource_name) + + yield + + if type(graphql_span) is StreamedSpan: + if self.execution_context.operation_name: + segment = graphql_span._segment + segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) + segment.set_attribute("sentry.op", op) + segment.name = self.execution_context.operation_name + elif isinstance(graphql_span, Span): + transaction = graphql_span.containing_transaction + if transaction and self.execution_context.operation_name: + transaction.name = self.execution_context.operation_name + transaction.source = TransactionSource.COMPONENT + transaction.op = op + + graphql_span.__exit__(None, None, None) + + def on_validate(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + validation_span = sentry_sdk.traces.start_span( + name="validation", + attributes={ + "sentry.op": OP.GRAPHQL_VALIDATE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + validation_span = sentry_sdk.start_span( + op=OP.GRAPHQL_VALIDATE, + name="validation", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during validation, we still need to close the span + try: + yield + finally: + if isinstance(validation_span, StreamedSpan): + validation_span.end() + else: + validation_span.finish() + + def on_parse(self) -> "Generator[None, None, None]": + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + if is_span_streaming_enabled: + parsing_span = sentry_sdk.traces.start_span( + name="parsing", + attributes={ + "sentry.op": OP.GRAPHQL_PARSE, + "sentry.origin": StrawberryIntegration.origin, + }, + ) + else: + parsing_span = sentry_sdk.start_span( + op=OP.GRAPHQL_PARSE, + name="parsing", + origin=StrawberryIntegration.origin, + ) + + # If an exception is raised during parsing, we still need to close the span + try: + yield + finally: + if isinstance(parsing_span, StreamedSpan): + parsing_span.end() + else: + parsing_span.finish() + + def should_skip_tracing( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + info: "GraphQLResolveInfo", + ) -> bool: + return strawberry_should_skip_tracing(_next, info) + + async def _resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + result = _next(root, info, *args, **kwargs) + + if isawaitable(result): + result = await result + + return result + + async def resolve( + self, + _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return await self._resolve(_next, root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return await self._resolve(_next, root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return await self._resolve(_next, root, info, *args, **kwargs) + + +class SentrySyncExtension(SentryAsyncExtension): + def resolve( + self, + _next: "Callable[[Any, Any, Any, Any], Any]", + root: "Any", + info: "GraphQLResolveInfo", + *args: str, + **kwargs: "Any", + ) -> "Any": + if self.should_skip_tracing(_next, info): + return _next(root, info, *args, **kwargs) + + field_path = "{}.{}".format(info.parent_type, info.field_name) + + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + if is_span_streaming_enabled: + with sentry_sdk.traces.start_span( + name=f"resolving {field_path}", + attributes={ + "sentry.origin": StrawberryIntegration.origin, + "sentry.op": OP.GRAPHQL_RESOLVE, + }, + ): + return _next(root, info, *args, **kwargs) + + with sentry_sdk.start_span( + op=OP.GRAPHQL_RESOLVE, + name="resolving {}".format(field_path), + origin=StrawberryIntegration.origin, + ) as span: + span.set_data("graphql.field_name", info.field_name) + span.set_data("graphql.parent_type", info.parent_type.name) + span.set_data("graphql.field_path", field_path) + span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) + + return _next(root, info, *args, **kwargs) + + +def _patch_views() -> None: + old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors + old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors + + def _sentry_patched_async_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_async_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + def _sentry_patched_sync_view_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + old_sync_view_handle_errors(self, errors, response_data) + _sentry_patched_handle_errors(self, errors, response_data) + + @ensure_integration_enabled(StrawberryIntegration) + def _sentry_patched_handle_errors( + self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" + ) -> None: + if not errors: + return + + scope = sentry_sdk.get_isolation_scope() + event_processor = _make_response_event_processor(response_data) + scope.add_event_processor(event_processor) + + with capture_internal_exceptions(): + for error in errors: + event, hint = event_from_exception( + error, + client_options=sentry_sdk.get_client().options, + mechanism={ + "type": StrawberryIntegration.identifier, + "handled": False, + }, + ) + sentry_sdk.capture_event(event, hint=hint) + + async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_async_view_handle_errors + ) + sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] + _sentry_patched_sync_view_handle_errors + ) + + +def _make_request_event_processor( + execution_context: "ExecutionContext", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + request_data = event.setdefault("request", {}) + request_data["api_target"] = "graphql" + + if not request_data.get("data"): + data: "dict[str, Any]" = {"query": execution_context.query} + if execution_context.variables: + data["variables"] = execution_context.variables + if execution_context.operation_name: + data["operationName"] = execution_context.operation_name + + request_data["data"] = data + + else: + try: + del event["request"]["data"] + except (KeyError, TypeError): + pass + + return event + + return inner + + +def _make_response_event_processor( + response_data: "GraphQLHTTPResponse", +) -> "EventProcessor": + def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + if should_send_default_pii(): + contexts = event.setdefault("contexts", {}) + contexts["response"] = {"data": response_data} + + return event + + return inner + + +def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": + if StrawberrySentryAsyncExtension in extensions: + return True + elif StrawberrySentrySyncExtension in extensions: + return False + + return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py new file mode 100644 index 0000000000..4927c0e885 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py @@ -0,0 +1,65 @@ +import functools +import sys + +import sentry_sdk +from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import NoReturn, Union + + +class SysExitIntegration(Integration): + """Captures sys.exit calls and sends them as events to Sentry. + + By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit + exceptions generated by sys.exit calls and send them to Sentry. + + This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and + non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. + Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. + """ + + identifier = "sys_exit" + + def __init__(self, *, capture_successful_exits: bool = False) -> None: + self._capture_successful_exits = capture_successful_exits + + @staticmethod + def setup_once() -> None: + SysExitIntegration._patch_sys_exit() + + @staticmethod + def _patch_sys_exit() -> None: + old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit + + @functools.wraps(old_exit) + def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": + # @ensure_integration_enabled ensures that this is non-None + integration = sentry_sdk.get_client().get_integration(SysExitIntegration) + if integration is None: + old_exit(__status) + + try: + old_exit(__status) + except SystemExit as e: + with capture_internal_exceptions(): + if integration._capture_successful_exits or __status not in ( + 0, + None, + ): + _capture_exception(e) + raise e + + sys.exit = sentry_patched_exit + + +def _capture_exception(exc: "SystemExit") -> None: + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": SysExitIntegration.identifier, "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py new file mode 100644 index 0000000000..f3ba046332 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py @@ -0,0 +1,196 @@ +import sys +import warnings +from concurrent.futures import Future, ThreadPoolExecutor +from functools import wraps +from threading import Thread, current_thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import use_isolation_scope, use_scope +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, + logger, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import ExcInfo + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T", bound=Any) + + +class ThreadingIntegration(Integration): + identifier = "threading" + + def __init__( + self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True + ) -> None: + if propagate_hub is not None: + logger.warning( + "Deprecated: propagate_hub is deprecated. This will be removed in the future." + ) + + # Note: propagate_hub did not have any effect on propagation of scope data + # scope data was always propagated no matter what the value of propagate_hub was + # This is why the default for propagate_scope is True + + self.propagate_scope = propagate_scope + + if propagate_hub is not None: + self.propagate_scope = propagate_hub + + @staticmethod + def setup_once() -> None: + old_start = Thread.start + + try: + from django import VERSION as django_version # noqa: N811 + except ImportError: + django_version = None + + try: + import channels # type: ignore[import-untyped] + + channels_version = channels.__version__ + except (ImportError, AttributeError): + channels_version = None + + is_async_emulated_with_threads = ( + sys.version_info < (3, 9) + and channels_version is not None + and channels_version < "4.0.0" + and django_version is not None + and django_version >= (3, 0) + and django_version < (4, 0) + ) + + @wraps(old_start) + def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return old_start(self, *a, **kw) + + if integration.propagate_scope: + if is_async_emulated_with_threads: + warnings.warn( + "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " + "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " + "Please either upgrade to Django channels 4.0+, use Django's async features " + "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", + stacklevel=2, + ) + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + + else: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + # Patching instance methods in `start()` creates a reference cycle if + # done in a naive way. See + # https://github.com/getsentry/sentry-python/pull/434 + # + # In threading module, using current_thread API will access current thread instance + # without holding it to avoid a reference cycle in an easier way. + with capture_internal_exceptions(): + new_run = _wrap_run( + isolation_scope, + current_scope, + getattr(self.run, "__func__", self.run), + ) + self.run = new_run # type: ignore + + return old_start(self, *a, **kw) + + Thread.start = sentry_start # type: ignore + ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore + ThreadPoolExecutor.submit, is_async_emulated_with_threads + ) + + +def _wrap_run( + isolation_scope_to_use: "Optional[sentry_sdk.Scope]", + current_scope_to_use: "Optional[sentry_sdk.Scope]", + old_run_func: "F", +) -> "F": + @wraps(old_run_func) + def run(*a: "Any", **kw: "Any") -> "Any": + def _run_old_run_func() -> "Any": + try: + self = current_thread() + return old_run_func(self, *a[1:], **kw) + except Exception: + reraise(*_capture_exception()) + + if isolation_scope_to_use is not None and current_scope_to_use is not None: + with use_isolation_scope(isolation_scope_to_use): + with use_scope(current_scope_to_use): + return _run_old_run_func() + else: + return _run_old_run_func() + + return run # type: ignore + + +def _wrap_threadpool_executor_submit( + func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool +) -> "Callable[..., Future[T]]": + """ + Wrap submit call to propagate scopes on task submission. + """ + + @wraps(func) + def sentry_submit( + self: "ThreadPoolExecutor", + fn: "Callable[..., T]", + *args: "Any", + **kwargs: "Any", + ) -> "Future[T]": + integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) + if integration is None: + return func(self, fn, *args, **kwargs) + + if integration.propagate_scope and is_async_emulated_with_threads: + isolation_scope = sentry_sdk.get_isolation_scope() + current_scope = sentry_sdk.get_current_scope() + elif integration.propagate_scope: + isolation_scope = sentry_sdk.get_isolation_scope().fork() + current_scope = sentry_sdk.get_current_scope().fork() + else: + isolation_scope = None + current_scope = None + + def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": + if isolation_scope is not None and current_scope is not None: + with use_isolation_scope(isolation_scope): + with use_scope(current_scope): + return fn(*args, **kwargs) + + return fn(*args, **kwargs) + + return func(self, wrapped_fn, *args, **kwargs) + + return sentry_submit + + +def _capture_exception() -> "ExcInfo": + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + if client.get_integration(ThreadingIntegration) is not None: + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "threading", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py new file mode 100644 index 0000000000..859b0d0870 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py @@ -0,0 +1,320 @@ +import contextlib +import weakref +from inspect import iscoroutinefunction + +import sentry_sdk +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version +from sentry_sdk.integrations._wsgi_common import ( + RequestExtractor, + _filter_headers, + _is_json_content_type, + request_body_within_bounds, +) +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + CONTEXTVARS_ERROR_MESSAGE, + HAS_REAL_CONTEXTVARS, + AnnotatedValue, + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from tornado import version_info as TORNADO_VERSION + from tornado.gen import coroutine + from tornado.web import HTTPError, RequestHandler +except ImportError: + raise DidNotEnable("Tornado not installed") + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.tracing import Span + + +class TornadoIntegration(Integration): + identifier = "tornado" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + _check_minimum_version(TornadoIntegration, TORNADO_VERSION) + + if not HAS_REAL_CONTEXTVARS: + # Tornado is async. We better have contextvars or we're going to leak + # state between requests. + raise DidNotEnable( + "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" + + CONTEXTVARS_ERROR_MESSAGE + ) + + ignore_logger("tornado.access") + + old_execute = RequestHandler._execute + + awaitable = iscoroutinefunction(old_execute) + + if awaitable: + # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) + # In that case our method should be a coroutine function too + async def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + return await old_execute(self, *args, **kwargs) + + else: + + @coroutine # type: ignore + def sentry_execute_request_handler( + self: "RequestHandler", *args: "Any", **kwargs: "Any" + ) -> "Any": + with _handle_request_impl(self): + result = yield from old_execute(self, *args, **kwargs) + return result + + RequestHandler._execute = sentry_execute_request_handler + + old_log_exception = RequestHandler.log_exception + + def sentry_log_exception( + self: "Any", + ty: type, + value: BaseException, + tb: "Any", + *args: "Any", + **kwargs: "Any", + ) -> "Optional[Any]": + _capture_exception(ty, value, tb) + return old_log_exception(self, ty, value, tb, *args, **kwargs) + + RequestHandler.log_exception = sentry_log_exception + + +_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" + + +@contextlib.contextmanager +def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": + integration = sentry_sdk.get_client().get_integration(TornadoIntegration) + + if integration is None: + yield + return + + weak_handler = weakref.ref(self) + client = sentry_sdk.get_client() + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + with sentry_sdk.isolation_scope() as scope: + headers = self.request.headers + + scope.clear_breadcrumbs() + processor = _make_event_processor(weak_handler) + scope.add_event_processor(processor) + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + + if is_span_streaming_enabled: + sentry_sdk.traces.continue_trace(dict(headers)) + scope.set_custom_sampling_context({"tornado_request": self.request}) + + if should_send_default_pii() and self.request.remote_ip: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_ROOT_SPAN_NAME, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": TornadoIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # Like with all other integrations, this is our + # fallback transaction in case there is no route. + # sentry_urldispatcher_resolve is responsible for + # setting a transaction name later. + name=_DEFAULT_ROOT_SPAN_NAME, + source=TransactionSource.ROUTE, + origin=TornadoIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"tornado_request": self.request}, + ) + + with span_ctx as span: + try: + yield + finally: + if type(span) is StreamedSpan: + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + self.request + ).items(): + span.set_attribute(attr, value) + + with capture_internal_exceptions(): + method = getattr(self, self.request.method.lower(), None) + if method is not None: + span_name = transaction_from_function(method) + if span_name: + span.name = span_name + span.set_attribute( + "sentry.span.source", + SegmentSource.COMPONENT, + ) + + with capture_internal_exceptions(): + status_int = self.get_status() + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) + span.status = "error" if status_int >= 400 else "ok" + + +def _get_request_attributes(request: "Any") -> "Dict[str, Any]": + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + + if should_send_default_pii(): + attributes[SPANDATA.URL_FULL] = request.full_url() + attributes["url.path"] = request.path + + if request.query: + attributes[SPANDATA.URL_QUERY] = request.query + + if request.protocol: + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol + + if should_send_default_pii() and request.remote_ip: + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + + with capture_internal_exceptions(): + raw_data = _get_tornado_request_data(request) + body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data + if body_data is not None: + attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data + + return attributes + + +def _get_tornado_request_data( + request: "Any", +) -> "Union[Optional[str], AnnotatedValue]": + body = request.body + if not body: + return None + + if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): + return AnnotatedValue.substituted_because_over_size_limit() + + return body.decode("utf-8", "replace") + + +@ensure_integration_enabled(TornadoIntegration) +def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: + if isinstance(value, HTTPError): + return + + event, hint = event_from_exception( + (ty, value, tb), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "tornado", "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) + + +def _make_event_processor( + weak_handler: "Callable[[], RequestHandler]", +) -> "EventProcessor": + def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": + handler = weak_handler() + if handler is None: + return event + + request = handler.request + + with capture_internal_exceptions(): + method = getattr(handler, handler.request.method.lower()) + event["transaction"] = transaction_from_function(method) or "" + event["transaction_info"] = {"source": TransactionSource.COMPONENT} + + with capture_internal_exceptions(): + extractor = TornadoRequestExtractor(request) + extractor.extract_into_event(event) + + request_info = event["request"] + + request_info["url"] = "%s://%s%s" % ( + request.protocol, + request.host, + request.path, + ) + + request_info["query_string"] = request.query + request_info["method"] = request.method + request_info["env"] = {"REMOTE_ADDR": request.remote_ip} + request_info["headers"] = _filter_headers(dict(request.headers)) + + if should_send_default_pii(): + try: + current_user = handler.current_user + except Exception: + current_user = None + + if current_user: + event.setdefault("user", {}).setdefault("is_authenticated", True) + + return event + + return tornado_processor + + +class TornadoRequestExtractor(RequestExtractor): + def content_length(self) -> int: + if self.request.body is None: + return 0 + return len(self.request.body) + + def cookies(self) -> "Dict[str, str]": + return {k: v.value for k, v in self.request.cookies.items()} + + def raw_data(self) -> bytes: + return self.request.body + + def form(self) -> "Dict[str, Any]": + return { + k: [v.decode("latin1", "replace") for v in vs] + for k, vs in self.request.body_arguments.items() + } + + def is_json(self) -> bool: + return _is_json_content_type(self.request.headers.get("content-type")) + + def files(self) -> "Dict[str, Any]": + return {k: v[0] for k, v in self.request.files.items() if v} + + def size_of_file(self, file: "Any") -> int: + return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py new file mode 100644 index 0000000000..0449a8f10c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py @@ -0,0 +1,52 @@ +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.utils import ensure_integration_enabled, event_from_exception + +try: + from trytond.exceptions import TrytonException # type: ignore + from trytond.wsgi import app # type: ignore +except ImportError: + raise DidNotEnable("Trytond is not installed.") + +# TODO: trytond-worker, trytond-cron and trytond-admin intergations + + +class TrytondWSGIIntegration(Integration): + identifier = "trytond_wsgi" + origin = f"auto.http.{identifier}" + + def __init__(self) -> None: + pass + + @staticmethod + def setup_once() -> None: + app.wsgi_app = SentryWsgiMiddleware( + app.wsgi_app, + span_origin=TrytondWSGIIntegration.origin, + ) + + @ensure_integration_enabled(TrytondWSGIIntegration) + def error_handler(e: Exception) -> None: + if isinstance(e, TrytonException): + return + else: + client = sentry_sdk.get_client() + event, hint = event_from_exception( + e, + client_options=client.options, + mechanism={"type": "trytond", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + # Expected error handlers signature was changed + # when the error_handler decorator was introduced + # in Tryton-5.4 + if hasattr(app, "error_handler"): + + @app.error_handler + def _(app, request, e): # type: ignore + error_handler(e) + + else: + app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py new file mode 100644 index 0000000000..497f0539ec --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, Optional, Type + + Excepthook = Callable[ + [Type[BaseException], BaseException, Optional[TracebackType]], + Any, + ] + +try: + import typer + from typer.main import except_hook +except ImportError: + raise DidNotEnable("Typer not installed") + + +class TyperIntegration(Integration): + identifier = "typer" + + @staticmethod + def setup_once() -> None: + typer.main.except_hook = _make_excepthook(except_hook) # type: ignore + + +def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": + def sentry_sdk_excepthook( + type_: "Type[BaseException]", + value: BaseException, + traceback: "Optional[TracebackType]", + ) -> None: + integration = sentry_sdk.get_client().get_integration(TyperIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_excepthook(type_, value, traceback) + + with capture_internal_exceptions(): + event, hint = event_from_exception( + (type_, value, traceback), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "typer", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_excepthook(type_, value, traceback) + + return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py new file mode 100644 index 0000000000..0316f6b88a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py @@ -0,0 +1,33 @@ +from functools import wraps +from typing import Any + +from sentry_sdk.feature_flags import add_feature_flag +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + from UnleashClient import UnleashClient +except ImportError: + raise DidNotEnable("UnleashClient is not installed") + + +class UnleashIntegration(Integration): + identifier = "unleash" + + @staticmethod + def setup_once() -> None: + # Wrap and patch evaluation methods (class methods) + old_is_enabled = UnleashClient.is_enabled + + @wraps(old_is_enabled) + def sentry_is_enabled( + self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" + ) -> "Any": + enabled = old_is_enabled(self, feature, *args, **kwargs) + + # We have no way of knowing what type of unleash feature this is, so we have to treat + # it as a boolean / toggle feature. + add_feature_flag(feature, enabled) + + return enabled + + UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py new file mode 100644 index 0000000000..2c7280a1f2 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py @@ -0,0 +1,50 @@ +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.utils import ( + capture_internal_exceptions, + event_from_exception, +) + +if TYPE_CHECKING: + from typing import Any, Callable + + +class UnraisablehookIntegration(Integration): + identifier = "unraisablehook" + + @staticmethod + def setup_once() -> None: + sys.unraisablehook = _make_unraisable(sys.unraisablehook) + + +def _make_unraisable( + old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", +) -> "Callable[[sys.UnraisableHookArgs], Any]": + def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: + integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) + + # Note: If we replace this with ensure_integration_enabled then + # we break the exceptiongroup backport; + # See: https://github.com/getsentry/sentry-python/issues/3097 + if integration is None: + return old_unraisablehook(unraisable) + + if unraisable.exc_value and unraisable.exc_traceback: + with capture_internal_exceptions(): + event, hint = event_from_exception( + ( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ), + client_options=sentry_sdk.get_client().options, + mechanism={"type": "unraisablehook", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return old_unraisablehook(unraisable) + + return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py new file mode 100644 index 0000000000..e776ed915a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py @@ -0,0 +1,427 @@ +import sys +from functools import partial +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._werkzeug import _get_headers, get_host +from sentry_sdk.api import continue_trace +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations._wsgi_common import ( + DEFAULT_HTTP_METHODS_TO_CAPTURE, + _filter_headers, + nullcontext, +) +from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope +from sentry_sdk.sessions import track_session +from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.tracing import Span, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Iterator, + Optional, + Protocol, + Tuple, + TypeVar, + Union, + ) + + from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.utils import ExcInfo + + WsgiResponseIter = TypeVar("WsgiResponseIter") + WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") + WsgiExcInfo = TypeVar("WsgiExcInfo") + + class StartResponse(Protocol): + def __call__( + self, + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, + ) -> "WsgiResponseIter": # type: ignore + pass + + +_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_DEFAULT_TRANSACTION_NAME = "generic WSGI request" + + +def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: + return s.encode("latin1").decode(charset, errors) + + +def get_request_url( + environ: "Dict[str, str]", use_x_forwarded_for: bool = False +) -> str: + """Return the absolute URL without query string for the given WSGI + environment.""" + script_name = environ.get("SCRIPT_NAME", "").rstrip("/") + path_info = environ.get("PATH_INFO", "").lstrip("/") + path = f"{script_name}/{path_info}" + + scheme = environ.get("wsgi.url_scheme") + if use_x_forwarded_for: + scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) + + return "%s://%s/%s" % ( + scheme, + get_host(environ, use_x_forwarded_for), + wsgi_decoding_dance(path).lstrip("/"), + ) + + +class SentryWsgiMiddleware: + __slots__ = ( + "app", + "use_x_forwarded_for", + "span_origin", + "http_methods_to_capture", + ) + + def __init__( + self, + app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", + use_x_forwarded_for: bool = False, + span_origin: str = "manual", + http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, + ) -> None: + self.app = app + self.use_x_forwarded_for = use_x_forwarded_for + self.span_origin = span_origin + self.http_methods_to_capture = http_methods_to_capture + + def __call__( + self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" + ) -> "Any": + if _wsgi_middleware_applied.get(False): + return self.app(environ, start_response) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + _wsgi_middleware_applied.set(True) + try: + with sentry_sdk.isolation_scope() as scope: + with track_session(scope, session_mode="request"): + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + scope._name = "wsgi" + scope.add_event_processor( + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for + ) + ) + + method = environ.get("REQUEST_METHOD", "").upper() + + span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None + if method in self.http_methods_to_capture: + if span_streaming: + sentry_sdk.traces.continue_trace( + dict(_get_headers(environ)) + ) + Scope.set_custom_sampling_context({"wsgi_environ": environ}) + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, client_ip + ) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_TRANSACTION_NAME, + attributes={ + "sentry.span.source": SegmentSource.ROUTE, + "sentry.origin": self.span_origin, + "sentry.op": OP.HTTP_SERVER, + }, + parent_span=None, + ) + else: + transaction = continue_trace( + environ, + op=OP.HTTP_SERVER, + name=_DEFAULT_TRANSACTION_NAME, + source=TransactionSource.ROUTE, + origin=self.span_origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"wsgi_environ": environ}, + ) + + span_ctx = span_ctx or nullcontext() + + with span_ctx as span: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + environ, self.use_x_forwarded_for + ).items(): + span.set_attribute(attr, value) + + try: + response = self.app( + environ, + partial(_sentry_start_response, start_response, span), + ) + except BaseException: + reraise(*_capture_exception()) + finally: + _wsgi_middleware_applied.set(False) + + # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses + # is determined by a pointer equality check on the response object + # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). + # + # If we were to return a _ScopedResponse, this would cause the check to always fail + # since it's checking the files are exactly the same. + # + # To avoid this and ensure that the offloading mechanism works as expected when it's + # enabled, we check if the response is a file-like object (determined by the presence + # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, + # it would've been used in handling the file in the response). + # + # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, + # so we want to make sure we don't interfere with those either. + # + # If all conditions are met, we return the original response object directly, + # allowing uWSGI to handle it as intended. + if ( + environ.get("wsgi.file_wrapper") + and getattr(response, "fileno", None) is not None + ): + return response + + return _ScopedResponse(scope, response) + + +def _sentry_start_response( + old_start_response: "StartResponse", + span: "Optional[Union[Span, StreamedSpan]]", + status: str, + response_headers: "WsgiResponseHeaders", + exc_info: "Optional[WsgiExcInfo]" = None, +) -> "WsgiResponseIter": # type: ignore[type-var] + with capture_internal_exceptions(): + status_int = int(status.split(" ", 1)[0]) + if span is not None: + if isinstance(span, StreamedSpan): + span.status = "error" if status_int >= 400 else "ok" + span.set_attribute("http.response.status_code", status_int) + else: + span.set_http_status(status_int) + + if exc_info is None: + # The Django Rest Framework WSGI test client, and likely other + # (incorrect) implementations, cannot deal with the exc_info argument + # if one is present. Avoid providing a third argument if not necessary. + return old_start_response(status, response_headers) + else: + return old_start_response(status, response_headers, exc_info) + + +def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": + """ + Returns our explicitly included environment variables we want to + capture (server name, port and remote addr if pii is enabled). + """ + keys = ["SERVER_NAME", "SERVER_PORT"] + if should_send_default_pii(): + # make debugging of proxy setup easier. Proxy headers are + # in headers. + keys += ["REMOTE_ADDR"] + + for key in keys: + if key in environ: + yield key, environ[key] + + +def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": + """ + Infer the user IP address from various headers. This cannot be used in + security sensitive situations since the value may be forged from a client, + but it's good enough for the event payload. + """ + try: + return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() + except (KeyError, IndexError): + pass + + try: + return environ["HTTP_X_REAL_IP"] + except KeyError: + pass + + return environ.get("REMOTE_ADDR") + + +def _capture_exception() -> "ExcInfo": + """ + Captures the current exception and sends it to Sentry. + Returns the ExcInfo tuple to it can be reraised afterwards. + """ + exc_info = sys.exc_info() + e = exc_info[1] + + # SystemExit(0) is the only uncaught exception that is expected behavior + should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) + if not should_skip_capture: + event, hint = event_from_exception( + exc_info, + client_options=sentry_sdk.get_client().options, + mechanism={"type": "wsgi", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + + +class _ScopedResponse: + """ + Users a separate scope for each response chunk. + + This will make WSGI apps more tolerant against: + - WSGI servers streaming responses from a different thread/from + different threads than the one that called start_response + - close() not being called + - WSGI servers streaming responses interleaved from the same thread + """ + + __slots__ = ("_response", "_scope") + + def __init__( + self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" + ) -> None: + self._scope = scope + self._response = response + + def __iter__(self) -> "Iterator[bytes]": + iterator = iter(self._response) + + while True: + with use_isolation_scope(self._scope): + try: + chunk = next(iterator) + except StopIteration: + break + except BaseException: + reraise(*_capture_exception()) + + yield chunk + + def close(self) -> None: + with use_isolation_scope(self._scope): + try: + self._response.close() # type: ignore + except AttributeError: + pass + except BaseException: + reraise(*_capture_exception()) + + +def _make_wsgi_event_processor( + environ: "Dict[str, str]", use_x_forwarded_for: bool +) -> "EventProcessor": + # It's a bit unfortunate that we have to extract and parse the request data + # from the environ so eagerly, but there are a few good reasons for this. + # + # We might be in a situation where the scope never gets torn down + # properly. In that case we will have an unnecessary strong reference to + # all objects in the environ (some of which may take a lot of memory) when + # we're really just interested in a few of them. + # + # Keeping the environment around for longer than the request lifecycle is + # also not necessarily something uWSGI can deal with: + # https://github.com/unbit/uwsgi/issues/1950 + + client_ip = get_client_ip(environ) + request_url = get_request_url(environ, use_x_forwarded_for) + query_string = environ.get("QUERY_STRING") + method = environ.get("REQUEST_METHOD") + env = dict(_get_environ(environ)) + headers = _filter_headers(dict(_get_headers(environ))) + + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": + with capture_internal_exceptions(): + # if the code below fails halfway through we at least have some data + request_info = event.setdefault("request", {}) + + if should_send_default_pii(): + user_info = event.setdefault("user", {}) + if client_ip: + user_info.setdefault("ip_address", client_ip) + + request_info["url"] = request_url + request_info["query_string"] = query_string + request_info["method"] = method + request_info["env"] = env + request_info["headers"] = headers + + return event + + return event_processor + + +def _get_request_attributes( + environ: "Dict[str, str]", + use_x_forwarded_for: bool = False, +) -> "Dict[str, Any]": + """ + Return span attributes related to the HTTP request from the WSGI environ. + """ + attributes: "dict[str, Any]" = {} + + method = environ.get("REQUEST_METHOD") + if method: + attributes["http.request.method"] = method.upper() + + headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + url_scheme = environ.get("wsgi.url_scheme") + if url_scheme: + attributes["network.protocol.name"] = url_scheme + + server_name = environ.get("SERVER_NAME") + if server_name: + attributes["server.address"] = server_name + + server_port = environ.get("SERVER_PORT") + if server_port: + try: + attributes["server.port"] = int(server_port) + except ValueError: + pass + + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + attributes["client.address"] = client_ip + + query_string = environ.get("QUERY_STRING") + if query_string: + attributes["http.query"] = query_string + + path = environ.get("PATH_INFO", "") + if path: + attributes["url.path"] = path + + attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) + + return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py new file mode 100644 index 0000000000..d7f4425dfd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py @@ -0,0 +1,88 @@ +# NOTE: this is the logger sentry exposes to users, not some generic logger. +import functools +import time +from typing import TYPE_CHECKING, Any + +import sentry_sdk +from sentry_sdk.utils import capture_internal_exceptions, format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes + + +OTEL_RANGES = [ + # ((severity level range), severity text) + # https://opentelemetry.io/docs/specs/otel/logs/data-model + ((1, 4), "trace"), + ((5, 8), "debug"), + ((9, 12), "info"), + ((13, 16), "warn"), + ((17, 20), "error"), + ((21, 24), "fatal"), +] + + +class _dict_default_key(dict): # type: ignore[type-arg] + """dict that returns the key if missing.""" + + def __missing__(self, key: str) -> str: + return "{" + key + "}" + + +def _capture_log( + severity_text: str, severity_number: int, template: str, **kwargs: "Any" +) -> None: + body = template + + attributes: "Attributes" = {} + + if "attributes" in kwargs: + provided_attributes = kwargs.pop("attributes") or {} + for attribute, value in provided_attributes.items(): + attributes[attribute] = format_attribute(value) + + for k, v in kwargs.items(): + attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) + + if kwargs: + # only attach template if there are parameters + attributes["sentry.message.template"] = format_attribute(template) + + with capture_internal_exceptions(): + body = template.format_map(_dict_default_key(kwargs)) + + sentry_sdk.get_current_scope()._capture_log( + { + "severity_text": severity_text, + "severity_number": severity_number, + "attributes": attributes, + "body": body, + "time_unix_nano": time.time_ns(), + "trace_id": None, + "span_id": None, + } + ) + + +trace = functools.partial(_capture_log, "trace", 1) +debug = functools.partial(_capture_log, "debug", 5) +info = functools.partial(_capture_log, "info", 9) +warning = functools.partial(_capture_log, "warn", 13) +error = functools.partial(_capture_log, "error", 17) +fatal = functools.partial(_capture_log, "fatal", 21) + + +def _otel_severity_text(otel_severity_number: int) -> str: + for (lower, upper), severity in OTEL_RANGES: + if lower <= otel_severity_number <= upper: + return severity + + return "default" + + +def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": + for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): + if level >= py_level: + return otel_severity_number, _otel_severity_text(otel_severity_number) + + return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py new file mode 100644 index 0000000000..27b7468c0d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py @@ -0,0 +1,62 @@ +import time +from typing import TYPE_CHECKING, Any, Optional + +import sentry_sdk +from sentry_sdk.utils import format_attribute + +if TYPE_CHECKING: + from sentry_sdk._types import Attributes, Metric, MetricType + + +def _capture_metric( + name: str, + metric_type: "MetricType", + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[Attributes]" = None, +) -> None: + attrs: "Attributes" = {} + + if attributes: + for k, v in attributes.items(): + attrs[k] = format_attribute(v) + + metric: "Metric" = { + "timestamp": time.time(), + "trace_id": None, + "span_id": None, + "name": name, + "type": metric_type, + "value": float(value), + "unit": unit, + "attributes": attrs, + } + + sentry_sdk.get_current_scope()._capture_metric(metric) + + +def count( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "counter", value, unit, attributes) + + +def gauge( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "gauge", value, unit, attributes) + + +def distribution( + name: str, + value: float, + unit: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, +) -> None: + _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py new file mode 100644 index 0000000000..d2ba298c35 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py @@ -0,0 +1,138 @@ +import os +import time +import weakref +from threading import Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Optional + + +MAX_DOWNSAMPLE_FACTOR = 10 + + +class Monitor: + """ + Performs health checks in a separate thread once every interval seconds + and updates the internal state. Other parts of the SDK only read this state + and act accordingly. + """ + + name = "sentry.monitor" + + _thread: "Optional[Thread]" + _thread_for_pid: "Optional[int]" + + def __init__( + self, transport: "sentry_sdk.transport.Transport", interval: float = 10 + ) -> None: + self.transport: "sentry_sdk.transport.Transport" = transport + self.interval: float = interval + + self._healthy = True + self._downsample_factor: int = 0 + self._running = True + self._reset_thread_state() + + # See https://github.com/getsentry/sentry-python/issues/6148. + # If os.fork() runs while another thread holds self._thread_lock, + # the child inherits the lock locked but the holding thread does + # not exist in the child, so the lock can never be released and + # _ensure_running deadlocks forever. Reinitialise the lock and + # cached thread/pid in the child so it starts clean regardless + # of inherited state. We bind via a WeakMethod so the + # permanently-registered fork handler does not pin this Monitor + # (and its Transport): register_at_fork has no unregister API. + # POSIX-only; Windows uses spawn. + if hasattr(os, "register_at_fork"): + weak_reset = weakref.WeakMethod(self._reset_thread_state) + + def _reset_in_child() -> None: + method = weak_reset() + if method is not None: + method() + + os.register_at_fork(after_in_child=_reset_in_child) + + def _reset_thread_state(self) -> None: + self._thread = None + self._thread_lock = Lock() + self._thread_for_pid = None + + def _ensure_running(self) -> None: + """ + Check that the monitor has an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + while self._running: + time.sleep(self.interval) + if self._running: + self.run() + + thread = Thread(name=self.name, target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def run(self) -> None: + self.check_health() + self.set_downsample_factor() + + def set_downsample_factor(self) -> None: + if self._healthy: + if self._downsample_factor > 0: + logger.debug( + "[Monitor] health check positive, reverting to normal sampling" + ) + self._downsample_factor = 0 + else: + if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: + self._downsample_factor += 1 + logger.debug( + "[Monitor] health check negative, downsampling with a factor of %d", + self._downsample_factor, + ) + + def check_health(self) -> None: + """ + Perform the actual health checks, + currently only checks if the transport is rate-limited. + TODO: augment in the future with more checks. + """ + self._healthy = self.transport.is_healthy() + + def is_healthy(self) -> bool: + self._ensure_running() + return self._healthy + + @property + def downsample_factor(self) -> int: + self._ensure_running() + return self._downsample_factor + + def kill(self) -> None: + self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py new file mode 100644 index 0000000000..d562405295 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py @@ -0,0 +1,49 @@ +from sentry_sdk.profiler.continuous_profiler import ( + start_profile_session, + start_profiler, + stop_profile_session, + stop_profiler, +) +from sentry_sdk.profiler.transaction_profiler import ( + MAX_PROFILE_DURATION_NS, + PROFILE_MINIMUM_SAMPLES, + GeventScheduler, + Profile, + Scheduler, + ThreadScheduler, + has_profiling_enabled, + setup_profiler, + teardown_profiler, +) +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + MAX_STACK_DEPTH, + extract_frame, + extract_stack, + frame_id, + get_frame_name, +) + +__all__ = [ + "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` + "start_profiler", + "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` + "stop_profiler", + # DEPRECATED: The following was re-exported for backwards compatibility. It + # will be removed from sentry_sdk.profiler in a future release. + "MAX_PROFILE_DURATION_NS", + "PROFILE_MINIMUM_SAMPLES", + "Profile", + "Scheduler", + "ThreadScheduler", + "GeventScheduler", + "has_profiling_enabled", + "setup_profiler", + "teardown_profiler", + "DEFAULT_SAMPLING_FREQUENCY", + "MAX_STACK_DEPTH", + "get_frame_name", + "extract_frame", + "extract_stack", + "frame_id", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py new file mode 100644 index 0000000000..ed525f52bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py @@ -0,0 +1,703 @@ +import atexit +import os +import random +import sys +import threading +import time +import uuid +import warnings +from collections import deque +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.consts import VERSION +from sentry_sdk.envelope import Envelope +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + is_gevent, + logger, + now, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union + + from typing_extensions import TypedDict + + from sentry_sdk._types import ContinuousProfilerMode, SDKInfo + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "timestamp": float, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + ThreadPool = None + + +_scheduler: "Optional[ContinuousScheduler]" = None + + +def setup_continuous_profiler( + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", +) -> bool: + global _scheduler + + already_initialized = _scheduler is not None + + if already_initialized: + logger.debug("[Profiling] Continuous Profiler is already setup") + teardown_continuous_profiler() + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventContinuousScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventContinuousScheduler.mode + else: + default_profiler_mode = ThreadContinuousScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + # TODO: deprecate this and just use the existing `profiler_mode` + experiments = options.get("_experiments", {}) + + profiler_mode = ( + experiments.get("continuous_profiling_mode") or default_profiler_mode + ) + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if profiler_mode == ThreadContinuousScheduler.mode: + _scheduler = ThreadContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + elif profiler_mode == GeventContinuousScheduler.mode: + _scheduler = GeventContinuousScheduler( + frequency, options, sdk_info, capture_func + ) + else: + raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up continuous profiler in {mode} mode".format( + mode=_scheduler.mode + ) + ) + + if not already_initialized: + atexit.register(teardown_continuous_profiler) + + return True + + +def is_profile_session_sampled() -> bool: + if _scheduler is None: + return False + return _scheduler.sampled + + +def try_autostart_continuous_profiler() -> None: + # TODO: deprecate this as it'll be replaced by the auto lifecycle option + + if _scheduler is None: + return + + if not _scheduler.is_auto_start_enabled(): + return + + _scheduler.manual_start() + + +def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": + if _scheduler is None: + return None + + return _scheduler.auto_start() + + +def start_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_start() + + +def start_profile_session() -> None: + warnings.warn( + "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + start_profiler() + + +def stop_profiler() -> None: + if _scheduler is None: + return + + _scheduler.manual_stop() + + +def stop_profile_session() -> None: + warnings.warn( + "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) + stop_profiler() + + +def teardown_continuous_profiler() -> None: + stop_profiler() + + global _scheduler + _scheduler = None + + +def get_profiler_id() -> "Union[str, None]": + if _scheduler is None: + return None + return _scheduler.profiler_id + + +def determine_profile_session_sampling_decision( + sample_rate: "Union[float, None]", +) -> bool: + # `None` is treated as `0.0` + if not sample_rate: + return False + + return random.random() < float(sample_rate) + + +class ContinuousProfile: + active: bool = True + + def stop(self) -> None: + self.active = False + + +class ContinuousScheduler: + mode: "ContinuousProfilerMode" = "unknown" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.interval = 1.0 / frequency + self.options = options + self.sdk_info = sdk_info + self.capture_func = capture_func + + self.lifecycle = self.options.get("profile_lifecycle") + profile_session_sample_rate = self.options.get("profile_session_sample_rate") + self.sampled = determine_profile_session_sampling_decision( + profile_session_sample_rate + ) + + self.sampler = self.make_sampler() + self.buffer: "Optional[ProfileBuffer]" = None + self.pid: "Optional[int]" = None + + self.running = False + self.soft_shutdown = False + + self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) + self.active_profiles: "Set[ContinuousProfile]" = set() + + def is_auto_start_enabled(self) -> bool: + # Ensure that the scheduler only autostarts once per process. + # This is necessary because many web servers use forks to spawn + # additional processes. And the profiler is only spawned on the + # master process, then it often only profiles the main process + # and not the ones where the requests are being handled. + if self.pid == os.getpid(): + return False + + experiments = self.options.get("_experiments") + if not experiments: + return False + + return experiments.get("continuous_profiling_auto_start") + + def auto_start(self) -> "Union[ContinuousProfile, None]": + if not self.sampled: + return None + + if self.lifecycle != "trace": + return None + + logger.debug("[Profiling] Auto starting profiler") + + profile = ContinuousProfile() + + self.new_profiles.append(profile) + self.ensure_running() + + return profile + + def manual_start(self) -> None: + if not self.sampled: + return + + if self.lifecycle != "manual": + return + + self.ensure_running() + + def manual_stop(self) -> None: + if self.lifecycle != "manual": + return + + self.teardown() + + def ensure_running(self) -> None: + raise NotImplementedError + + def teardown(self) -> None: + raise NotImplementedError + + def pause(self) -> None: + raise NotImplementedError + + def reset_buffer(self) -> None: + self.buffer = ProfileBuffer( + self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func + ) + + @property + def profiler_id(self) -> "Union[str, None]": + if not self.running or self.buffer is None: + return None + return self.buffer.profiler_id + + def make_sampler(self) -> "Callable[..., bool]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + if self.lifecycle == "trace": + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + return True + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + inactive_profiles = [] + + for profile in self.active_profiles: + if not profile.active: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + else: + + def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + + ts = now() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return False + + if self.buffer is not None: + self.buffer.write(ts, sample) + + return False + + return _sample_stack + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.soft_shutdown = self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # the soft shutdown happens here to give it a chance + # for the profiler to be reused + if self.soft_shutdown: + self.running = False + + # make sure to explicitly exit the profiler here or there might + # be multiple profilers at once + break + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + buffer = self.buffer + if buffer is not None: + buffer.flush() + + +class ThreadContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ContinuousProfilerMode" = "thread" + name = "sentry.profiler.ThreadContinuousScheduler" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[threading.Thread]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +class GeventContinuousScheduler(ContinuousScheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ContinuousProfilerMode" = "gevent" + + def __init__( + self, + frequency: int, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + capture_func: "Callable[[Envelope], None]", + ) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency, options, sdk_info, capture_func) + + self.thread: "Optional[_ThreadPool]" = None + self.lock = threading.Lock() + + def ensure_running(self) -> None: + self.soft_shutdown = False + + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # if the profiler thread is changing, + # we should create a new buffer along with it + self.reset_buffer() + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + + def teardown(self) -> None: + if self.running: + self.running = False + + if self.thread is not None: + self.thread.join() + self.thread = None + + +PROFILE_BUFFER_SECONDS = 60 + + +class ProfileBuffer: + def __init__( + self, + options: "Dict[str, Any]", + sdk_info: "SDKInfo", + buffer_size: int, + capture_func: "Callable[[Envelope], None]", + ) -> None: + self.options = options + self.sdk_info = sdk_info + self.buffer_size = buffer_size + self.capture_func = capture_func + + self.profiler_id = uuid.uuid4().hex + self.chunk = ProfileChunk() + + # Make sure to use the same clock to compute a sample's monotonic timestamp + # to ensure the timestamps are correctly aligned. + self.start_monotonic_time = now() + + # Make sure the start timestamp is defined only once per profiler id. + # This prevents issues with clock drift within a single profiler session. + # + # Subtracting the start_monotonic_time here to find a fixed starting position + # for relative monotonic timestamps for each sample. + self.start_timestamp = ( + datetime.now(timezone.utc).timestamp() - self.start_monotonic_time + ) + + def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: + if self.should_flush(monotonic_time): + self.flush() + self.chunk = ProfileChunk() + self.start_monotonic_time = now() + + self.chunk.write(self.start_timestamp + monotonic_time, sample) + + def should_flush(self, monotonic_time: float) -> bool: + # If the delta between the new monotonic time and the start monotonic time + # exceeds the buffer size, it means we should flush the chunk + return monotonic_time - self.start_monotonic_time >= self.buffer_size + + def flush(self) -> None: + chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) + envelope = Envelope() + envelope.add_profile_chunk(chunk) + self.capture_func(envelope) + + +class ProfileChunk: + def __init__(self) -> None: + self.chunk_id = uuid.uuid4().hex + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + def write(self, ts: float, sample: "ExtractedSample") -> None: + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "timestamp": ts, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def to_json( + self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" + ) -> "Dict[str, Any]": + profile = { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + }, + } + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + payload = { + "chunk_id": self.chunk_id, + "client_sdk": { + "name": sdk_info["name"], + "version": VERSION, + }, + "platform": "python", + "profile": profile, + "profiler_id": profiler_id, + "version": "2", + } + + for key in "release", "environment", "dist": + if options[key] is not None: + payload[key] = str(options[key]).strip() + + return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py new file mode 100644 index 0000000000..fe65774c0b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py @@ -0,0 +1,802 @@ +""" +This file is originally based on code from https://github.com/nylas/nylas-perftools, +which is published under the following license: + +The MIT License (MIT) + +Copyright (c) 2014 Nylas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import atexit +import os +import platform +import random +import sys +import threading +import time +import uuid +import warnings +from abc import ABC, abstractmethod +from collections import deque +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) +from sentry_sdk.utils import ( + capture_internal_exception, + capture_internal_exceptions, + get_current_thread_meta, + is_gevent, + is_valid_sample_rate, + logger, + nanosecond_time, + set_in_app_in_frames, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type + + from typing_extensions import TypedDict + + from sentry_sdk._types import Event, ProfilerMode, SamplingContext + from sentry_sdk.profiler.utils import ( + ExtractedSample, + FrameId, + ProcessedFrame, + ProcessedStack, + ProcessedThreadMetadata, + StackId, + ThreadId, + ) + + ProcessedSample = TypedDict( + "ProcessedSample", + { + "elapsed_since_start_ns": str, + "thread_id": ThreadId, + "stack_id": int, + }, + ) + + ProcessedProfile = TypedDict( + "ProcessedProfile", + { + "frames": List[ProcessedFrame], + "stacks": List[ProcessedStack], + "samples": List[ProcessedSample], + "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], + }, + ) + + +try: + from gevent.monkey import get_original + from gevent.threadpool import ThreadPool as _ThreadPool + + ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool + thread_sleep = get_original("time", "sleep") +except ImportError: + thread_sleep = time.sleep + + ThreadPool = None + + +_scheduler: "Optional[Scheduler]" = None + + +# The minimum number of unique samples that must exist in a profile to be +# considered valid. +PROFILE_MINIMUM_SAMPLES = 2 + + +def has_profiling_enabled(options: "Dict[str, Any]") -> bool: + profiles_sampler = options["profiles_sampler"] + if profiles_sampler is not None: + return True + + profiles_sample_rate = options["profiles_sample_rate"] + if profiles_sample_rate is not None and profiles_sample_rate > 0: + return True + + profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") + if profiles_sample_rate is not None: + logger.warning( + "_experiments['profiles_sample_rate'] is deprecated. " + "Please use the non-experimental profiles_sample_rate option " + "directly." + ) + if profiles_sample_rate > 0: + return True + + return False + + +def setup_profiler(options: "Dict[str, Any]") -> bool: + global _scheduler + + if _scheduler is not None: + logger.debug("[Profiling] Profiler is already setup") + return False + + frequency = DEFAULT_SAMPLING_FREQUENCY + + if is_gevent(): + # If gevent has patched the threading modules then we cannot rely on + # them to spawn a native thread for sampling. + # Instead we default to the GeventScheduler which is capable of + # spawning native threads within gevent. + default_profiler_mode = GeventScheduler.mode + else: + default_profiler_mode = ThreadScheduler.mode + + if options.get("profiler_mode") is not None: + profiler_mode = options["profiler_mode"] + else: + profiler_mode = options.get("_experiments", {}).get("profiler_mode") + if profiler_mode is not None: + logger.warning( + "_experiments['profiler_mode'] is deprecated. Please use the " + "non-experimental profiler_mode option directly." + ) + profiler_mode = profiler_mode or default_profiler_mode + + if ( + profiler_mode == ThreadScheduler.mode + # for legacy reasons, we'll keep supporting sleep mode for this scheduler + or profiler_mode == "sleep" + ): + _scheduler = ThreadScheduler(frequency=frequency) + elif profiler_mode == GeventScheduler.mode: + _scheduler = GeventScheduler(frequency=frequency) + else: + raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) + + logger.debug( + "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) + ) + _scheduler.setup() + + atexit.register(teardown_profiler) + + return True + + +def teardown_profiler() -> None: + global _scheduler + + if _scheduler is not None: + _scheduler.teardown() + + _scheduler = None + + +MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds + + +class Profile: + def __init__( + self, + sampled: "Optional[bool]", + start_ns: int, + hub: "Optional[sentry_sdk.Hub]" = None, + scheduler: "Optional[Scheduler]" = None, + ) -> None: + self.scheduler = _scheduler if scheduler is None else scheduler + + self.event_id: str = uuid.uuid4().hex + + self.sampled: "Optional[bool]" = sampled + + # Various framework integrations are capable of overwriting the active thread id. + # If it is set to `None` at the end of the profile, we fall back to the default. + self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 + self.active_thread_id: "Optional[int]" = None + + try: + self.start_ns: int = start_ns + except AttributeError: + self.start_ns = 0 + + self.stop_ns: int = 0 + self.active: bool = False + + self.indexed_frames: "Dict[FrameId, int]" = {} + self.indexed_stacks: "Dict[StackId, int]" = {} + self.frames: "List[ProcessedFrame]" = [] + self.stacks: "List[ProcessedStack]" = [] + self.samples: "List[ProcessedSample]" = [] + + self.unique_samples = 0 + + # Backwards compatibility with the old hub property + self._hub: "Optional[sentry_sdk.Hub]" = None + if hub is not None: + self._hub = hub + warnings.warn( + "The `hub` parameter is deprecated. Please do not use it.", + DeprecationWarning, + stacklevel=2, + ) + + def update_active_thread_id(self) -> None: + self.active_thread_id = get_current_thread_meta()[0] + logger.debug( + "[Profiling] updating active thread id to {tid}".format( + tid=self.active_thread_id + ) + ) + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the profile's sampling decision according to the following + precedence rules: + + 1. If the transaction to be profiled is not sampled, that decision + will be used, regardless of anything else. + + 2. Use `profiles_sample_rate` to decide. + """ + + # The corresponding transaction was not sampled, + # so don't generate a profile for it. + if not self.sampled: + logger.debug( + "[Profiling] Discarding profile because transaction is discarded." + ) + self.sampled = False + return + + # The profiler hasn't been properly initialized. + if self.scheduler is None: + logger.debug( + "[Profiling] Discarding profile because profiler was not started." + ) + self.sampled = False + return + + client = sentry_sdk.get_client() + if not client.is_active(): + self.sampled = False + return + + options = client.options + + if callable(options.get("profiles_sampler")): + sample_rate = options["profiles_sampler"](sampling_context) + elif options["profiles_sample_rate"] is not None: + sample_rate = options["profiles_sample_rate"] + else: + sample_rate = options["_experiments"].get("profiles_sample_rate") + + # The profiles_sample_rate option was not set, so profiling + # was never enabled. + if sample_rate is None: + logger.debug( + "[Profiling] Discarding profile because profiling was not enabled." + ) + self.sampled = False + return + + if not is_valid_sample_rate(sample_rate, source="Profiling"): + logger.warning( + "[Profiling] Discarding profile because of invalid sample rate." + ) + self.sampled = False + return + + # Now we roll the dice. random.random is inclusive of 0, but not of 1, + # so strict < is safe here. In case sample_rate is a boolean, cast it + # to a float (True becomes 1.0 and False becomes 0.0) + self.sampled = random.random() < float(sample_rate) + + if self.sampled: + logger.debug("[Profiling] Initializing profile") + else: + logger.debug( + "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( + sample_rate=float(sample_rate) + ) + ) + + def start(self) -> None: + if not self.sampled or self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Starting profile") + self.active = True + if not self.start_ns: + self.start_ns = nanosecond_time() + self.scheduler.start_profiling(self) + + def stop(self) -> None: + if not self.sampled or not self.active: + return + + assert self.scheduler, "No scheduler specified" + logger.debug("[Profiling] Stopping profile") + self.active = False + self.stop_ns = nanosecond_time() + + def __enter__(self) -> "Profile": + scope = sentry_sdk.get_isolation_scope() + old_profile = scope.profile + scope.profile = self + + self._context_manager_state = (scope, old_profile) + + self.start() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + with capture_internal_exceptions(): + self.stop() + + scope, old_profile = self._context_manager_state + del self._context_manager_state + + scope.profile = old_profile + + def write(self, ts: int, sample: "ExtractedSample") -> None: + if not self.active: + return + + if ts < self.start_ns: + return + + offset = ts - self.start_ns + if offset > MAX_PROFILE_DURATION_NS: + self.stop() + return + + self.unique_samples += 1 + + elapsed_since_start_ns = str(offset) + + for tid, (stack_id, frame_ids, frames) in sample: + try: + # Check if the stack is indexed first, this lets us skip + # indexing frames if it's not necessary + if stack_id not in self.indexed_stacks: + for i, frame_id in enumerate(frame_ids): + if frame_id not in self.indexed_frames: + self.indexed_frames[frame_id] = len(self.indexed_frames) + self.frames.append(frames[i]) + + self.indexed_stacks[stack_id] = len(self.indexed_stacks) + self.stacks.append( + [self.indexed_frames[frame_id] for frame_id in frame_ids] + ) + + self.samples.append( + { + "elapsed_since_start_ns": elapsed_since_start_ns, + "thread_id": tid, + "stack_id": self.indexed_stacks[stack_id], + } + ) + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + + def process(self) -> "ProcessedProfile": + # This collects the thread metadata at the end of a profile. Doing it + # this way means that any threads that terminate before the profile ends + # will not have any metadata associated with it. + thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { + str(thread.ident): { + "name": str(thread.name), + } + for thread in threading.enumerate() + } + + return { + "frames": self.frames, + "stacks": self.stacks, + "samples": self.samples, + "thread_metadata": thread_metadata, + } + + def to_json( + self, event_opt: "Event", options: "Dict[str, Any]" + ) -> "Dict[str, Any]": + profile = self.process() + + set_in_app_in_frames( + profile["frames"], + options["in_app_exclude"], + options["in_app_include"], + options["project_root"], + ) + + return { + "environment": event_opt.get("environment"), + "event_id": self.event_id, + "platform": "python", + "profile": profile, + "release": event_opt.get("release", ""), + "timestamp": event_opt["start_timestamp"], + "version": "1", + "device": { + "architecture": platform.machine(), + }, + "os": { + "name": platform.system(), + "version": platform.release(), + }, + "runtime": { + "name": platform.python_implementation(), + "version": platform.python_version(), + }, + "transactions": [ + { + "id": event_opt["event_id"], + "name": event_opt["transaction"], + # we start the transaction before the profile and this is + # the transaction start time relative to the profile, so we + # hardcode it to 0 until we can start the profile before + "relative_start_ns": "0", + # use the duration of the profile instead of the transaction + # because we end the transaction after the profile + "relative_end_ns": str(self.stop_ns - self.start_ns), + "trace_id": event_opt["contexts"]["trace"]["trace_id"], + "active_thread_id": str( + self._default_active_thread_id + if self.active_thread_id is None + else self.active_thread_id + ), + } + ], + } + + def valid(self) -> bool: + client = sentry_sdk.get_client() + if not client.is_active(): + return False + + if not has_profiling_enabled(client.options): + return False + + if self.sampled is None or not self.sampled: + if client.transport: + client.transport.record_lost_event( + "sample_rate", data_category="profile" + ) + return False + + if self.unique_samples < PROFILE_MINIMUM_SAMPLES: + if client.transport: + client.transport.record_lost_event( + "insufficient_data", data_category="profile" + ) + logger.debug("[Profiling] Discarding profile because insufficient samples.") + return False + + return True + + @property + def hub(self) -> "Optional[sentry_sdk.Hub]": + warnings.warn( + "The `hub` attribute is deprecated. Please do not access it.", + DeprecationWarning, + stacklevel=2, + ) + return self._hub + + @hub.setter + def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: + warnings.warn( + "The `hub` attribute is deprecated. Please do not set it.", + DeprecationWarning, + stacklevel=2, + ) + self._hub = value + + +class Scheduler(ABC): + mode: "ProfilerMode" = "unknown" + + def __init__(self, frequency: int) -> None: + self.interval = 1.0 / frequency + + self.sampler = self.make_sampler() + + # cap the number of new profiles at any time so it does not grow infinitely + self.new_profiles: "Deque[Profile]" = deque(maxlen=128) + self.active_profiles: "Set[Profile]" = set() + + def __enter__(self) -> "Scheduler": + self.setup() + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self.teardown() + + @abstractmethod + def setup(self) -> None: + pass + + @abstractmethod + def teardown(self) -> None: + pass + + def ensure_running(self) -> None: + """ + Ensure the scheduler is running. By default, this method is a no-op. + The method should be overridden by any implementation for which it is + relevant. + """ + return None + + def start_profiling(self, profile: "Profile") -> None: + self.ensure_running() + self.new_profiles.append(profile) + + def make_sampler(self) -> "Callable[..., None]": + cwd = os.getcwd() + + cache = LRUCache(max_size=256) + + def _sample_stack(*args: "Any", **kwargs: "Any") -> None: + """ + Take a sample of the stack on all the threads in the process. + This should be called at a regular interval to collect samples. + """ + # no profiles taking place, so we can stop early + if not self.new_profiles and not self.active_profiles: + # make sure to clear the cache if we're not profiling so we dont + # keep a reference to the last stack of frames around + return + + # This is the number of profiles we want to pop off. + # It's possible another thread adds a new profile to + # the list and we spend longer than we want inside + # the loop below. + # + # Also make sure to set this value before extracting + # frames so we do not write to any new profiles that + # were started after this point. + new_profiles = len(self.new_profiles) + + now = nanosecond_time() + + try: + sample = [ + (str(tid), extract_stack(frame, cache, cwd)) + for tid, frame in sys._current_frames().items() + ] + except AttributeError: + # For some reason, the frame we get doesn't have certain attributes. + # When this happens, we abandon the current sample as it's bad. + capture_internal_exception(sys.exc_info()) + return + + # Move the new profiles into the active_profiles set. + # + # We cannot directly add the to active_profiles set + # in `start_profiling` because it is called from other + # threads which can cause a RuntimeError when it the + # set sizes changes during iteration without a lock. + # + # We also want to avoid using a lock here so threads + # that are starting profiles are not blocked until it + # can acquire the lock. + for _ in range(new_profiles): + self.active_profiles.add(self.new_profiles.popleft()) + + inactive_profiles = [] + + for profile in self.active_profiles: + if profile.active: + profile.write(now, sample) + else: + # If a profile is marked inactive, we buffer it + # to `inactive_profiles` so it can be removed. + # We cannot remove it here as it would result + # in a RuntimeError. + inactive_profiles.append(profile) + + for profile in inactive_profiles: + self.active_profiles.remove(profile) + + return _sample_stack + + +class ThreadScheduler(Scheduler): + """ + This scheduler is based on running a daemon thread that will call + the sampler at a regular interval. + """ + + mode: "ProfilerMode" = "thread" + name = "sentry.profiler.ThreadScheduler" + + def __init__(self, frequency: int) -> None: + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[threading.Thread]" = None + self.pid: "Optional[int]" = None + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + """ + Check that the profiler has an active thread to run in, and start one if + that's not the case. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self.running + will be False after running this function. + """ + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + # make sure the thread is a daemon here otherwise this + # can keep the application running after other threads + # have exited + self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) + try: + self.thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() + + +class GeventScheduler(Scheduler): + """ + This scheduler is based on the thread scheduler but adapted to work with + gevent. When using gevent, it may monkey patch the threading modules + (`threading` and `_thread`). This results in the use of greenlets instead + of native threads. + + This is an issue because the sampler CANNOT run in a greenlet because + 1. Other greenlets doing sync work will prevent the sampler from running + 2. The greenlet runs in the same thread as other greenlets so when taking + a sample, other greenlets will have been evicted from the thread. This + results in a sample containing only the sampler's code. + """ + + mode: "ProfilerMode" = "gevent" + name = "sentry.profiler.GeventScheduler" + + def __init__(self, frequency: int) -> None: + if ThreadPool is None: + raise ValueError("Profiler mode: {} is not available".format(self.mode)) + + super().__init__(frequency=frequency) + + # used to signal to the thread that it should stop + self.running = False + self.thread: "Optional[_ThreadPool]" = None + self.pid: "Optional[int]" = None + + # This intentionally uses the gevent patched threading.Lock. + # The lock will be required when first trying to start profiles + # as we need to spawn the profiler thread from the greenlets. + self.lock = threading.Lock() + + def setup(self) -> None: + pass + + def teardown(self) -> None: + if self.running: + self.running = False + if self.thread is not None: + self.thread.join() + + def ensure_running(self) -> None: + pid = os.getpid() + + # is running on the right process + if self.running and self.pid == pid: + return + + with self.lock: + # another thread may have tried to acquire the lock + # at the same time so it may start another thread + # make sure to check again before proceeding + if self.running and self.pid == pid: + return + + self.pid = pid + self.running = True + + self.thread = ThreadPool(1) # type: ignore[misc] + try: + self.thread.spawn(self.run) + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.running = False + self.thread = None + return + + def run(self) -> None: + last = time.perf_counter() + + while self.running: + self.sampler() + + # some time may have elapsed since the last time + # we sampled, so we need to account for that and + # not sleep for too long + elapsed = time.perf_counter() - last + if elapsed < self.interval: + thread_sleep(self.interval - elapsed) + + # after sleeping, make sure to take the current + # timestamp so we can use it next iteration + last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py new file mode 100644 index 0000000000..e9f0fec07f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py @@ -0,0 +1,186 @@ +import os +from collections import deque +from typing import TYPE_CHECKING + +from sentry_sdk._compat import PY311 +from sentry_sdk.utils import filename_for_module + +if TYPE_CHECKING: + from types import FrameType + from typing import Deque, List, Optional, Sequence, Tuple + + from typing_extensions import TypedDict + + from sentry_sdk._lru_cache import LRUCache + + ThreadId = str + + ProcessedStack = List[int] + + ProcessedFrame = TypedDict( + "ProcessedFrame", + { + "abs_path": str, + "filename": Optional[str], + "function": str, + "lineno": int, + "module": Optional[str], + }, + ) + + ProcessedThreadMetadata = TypedDict( + "ProcessedThreadMetadata", + {"name": str}, + ) + + FrameId = Tuple[ + str, # abs_path + int, # lineno + str, # function + ] + FrameIds = Tuple[FrameId, ...] + + # The exact value of this id is not very meaningful. The purpose + # of this id is to give us a compact and unique identifier for a + # raw stack that can be used as a key to a dictionary so that it + # can be used during the sampled format generation. + StackId = Tuple[int, int] + + ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] + ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] + +# The default sampling frequency to use. This is set at 101 in order to +# mitigate the effects of lockstep sampling. +DEFAULT_SAMPLING_FREQUENCY = 101 + + +# We want to impose a stack depth limit so that samples aren't too large. +MAX_STACK_DEPTH = 128 + + +if PY311: + + def get_frame_name(frame: "FrameType") -> str: + return frame.f_code.co_qualname + +else: + + def get_frame_name(frame: "FrameType") -> str: + f_code = frame.f_code + co_varnames = f_code.co_varnames + + # co_name only contains the frame name. If the frame was a method, + # the class name will NOT be included. + name = f_code.co_name + + # if it was a method, we can get the class name by inspecting + # the f_locals for the `self` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `self` if its an instance method + co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals + ): + for cls in type(frame.f_locals["self"]).__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # if it was a class method, (decorated with `@classmethod`) + # we can get the class name by inspecting the f_locals for the `cls` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `cls` if its a class method + co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals + ): + for cls in frame.f_locals["cls"].__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except (AttributeError, ValueError): + pass + + # nothing we can do if it is a staticmethod (decorated with @staticmethod) + + # we've done all we can, time to give up and return what we have + return name + + +def frame_id(raw_frame: "FrameType") -> "FrameId": + return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) + + +def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": + abs_path = raw_frame.f_code.co_filename + + try: + module = raw_frame.f_globals["__name__"] + except Exception: + module = None + + # namedtuples can be many times slower when initialing + # and accessing attribute so we opt to use a tuple here instead + return { + # This originally was `os.path.abspath(abs_path)` but that had + # a large performance overhead. + # + # According to docs, this is equivalent to + # `os.path.normpath(os.path.join(os.getcwd(), path))`. + # The `os.getcwd()` call is slow here, so we precompute it. + # + # Additionally, since we are using normalized path already, + # we skip calling `os.path.normpath` entirely. + "abs_path": os.path.join(cwd, abs_path), + "module": module, + "filename": filename_for_module(module, abs_path) or None, + "function": fid[2], + "lineno": raw_frame.f_lineno, + } + + +def extract_stack( + raw_frame: "Optional[FrameType]", + cache: "LRUCache", + cwd: str, + max_stack_depth: int = MAX_STACK_DEPTH, +) -> "ExtractedStack": + """ + Extracts the stack starting the specified frame. The extracted stack + assumes the specified frame is the top of the stack, and works back + to the bottom of the stack. + + In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, + only the first `MAX_STACK_DEPTH` frames will be returned. + """ + + raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) + + while raw_frame is not None: + f_back = raw_frame.f_back + raw_frames.append(raw_frame) + raw_frame = f_back + + frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) + frames = [] + for i, fid in enumerate(frame_ids): + frame = cache.get(fid) + if frame is None: + frame = extract_frame(fid, raw_frames[i], cwd) + cache.set(fid, frame) + frames.append(frame) + + # Instead of mapping the stack into frame ids and hashing + # that as a tuple, we can directly hash the stack. + # This saves us from having to generate yet another list. + # Additionally, using the stack as the key directly is + # costly because the stack can be large, so we pre-hash + # the stack, and use the hash as the key as this will be + # needed a few times to improve performance. + # + # To Reduce the likelihood of hash collisions, we include + # the stack depth. This means that only stacks of the same + # depth can suffer from hash collisions. + stack_id = len(raw_frames), hash(frame_ids) + + return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py new file mode 100644 index 0000000000..4fd22714cf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py @@ -0,0 +1,2185 @@ +import os +import platform +import sys +import warnings +from collections import deque +from contextlib import contextmanager +from copy import copy, deepcopy +from datetime import datetime, timezone +from enum import Enum +from functools import wraps +from itertools import chain +from typing import TYPE_CHECKING, cast + +import sentry_sdk +from sentry_sdk._types import AnnotatedValue +from sentry_sdk.attachments import Attachment +from sentry_sdk.consts import ( + DEFAULT_MAX_BREADCRUMBS, + FALSE_VALUES, + INSTRUMENTER, + SPANDATA, +) +from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.profiler.transaction_profiler import Profile +from sentry_sdk.session import Session +from sentry_sdk.traces import ( + _DEFAULT_PARENT_SPAN, + NoOpStreamedSpan, + StreamedSpan, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + SENTRY_TRACE_HEADER_NAME, + NoOpSpan, + Span, + Transaction, +) +from sentry_sdk.tracing_utils import ( + Baggage, + PropagationContext, + _make_sampling_decision, + has_span_streaming_enabled, + has_tracing_enabled, + is_ignored_span, +) +from sentry_sdk.utils import ( + ContextVar, + capture_internal_exception, + capture_internal_exceptions, + datetime_from_isoformat, + disable_capture_event, + event_from_exception, + exc_info_from_error, + format_attribute, + has_logs_enabled, + has_metrics_enabled, + logger, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ( + Any, + Callable, + Deque, + Dict, + Generator, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + ) + + from typing_extensions import Unpack + + import sentry_sdk + from sentry_sdk._types import ( + Attributes, + AttributeValue, + Breadcrumb, + BreadcrumbHint, + ErrorProcessor, + Event, + EventProcessor, + ExcInfo, + Hint, + Log, + LogLevelStr, + Metric, + SamplingContext, + Type, + ) + from sentry_sdk.tracing import TransactionKwargs + + P = ParamSpec("P") + R = TypeVar("R") + + F = TypeVar("F", bound=Callable[..., Any]) + T = TypeVar("T") + + +# Holds data that will be added to **all** events sent by this process. +# In case this is a http server (think web framework) with multiple users +# the data will be added to events of all users. +# Typically this is used for process wide data such as the release. +_global_scope: "Optional[Scope]" = None + +# Holds data for the active request. +# This is used to isolate data for different requests or users. +# The isolation scope is usually created by integrations, but may also +# be created manually +_isolation_scope = ContextVar("isolation_scope", default=None) + +# Holds data for the active span. +# This can be used to manually add additional data to a span. +_current_scope = ContextVar("current_scope", default=None) + +global_event_processors: "List[EventProcessor]" = [] + +# A function returning a (trace_id, span_id) tuple +# from an external tracing source (such as otel) +_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None + + +class ScopeType(Enum): + CURRENT = "current" + ISOLATION = "isolation" + GLOBAL = "global" + MERGED = "merged" + + +class _ScopeManager: + def __init__(self, hub: "Optional[Any]" = None) -> None: + self._old_scopes: "List[Scope]" = [] + + def __enter__(self) -> "Scope": + isolation_scope = Scope.get_isolation_scope() + + self._old_scopes.append(isolation_scope) + + forked_scope = isolation_scope.fork() + _isolation_scope.set(forked_scope) + + return forked_scope + + def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: + old_scope = self._old_scopes.pop() + _isolation_scope.set(old_scope) + + +def add_global_event_processor(processor: "EventProcessor") -> None: + global_event_processors.append(processor) + + +def register_external_propagation_context( + fn: "Callable[[], Optional[Tuple[str, str]]]", +) -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = fn + + +def remove_external_propagation_context() -> None: + global _external_propagation_context_fn + _external_propagation_context_fn = None + + +def get_external_propagation_context() -> "Optional[Tuple[str, str]]": + return ( + _external_propagation_context_fn() if _external_propagation_context_fn else None + ) + + +def has_external_propagation_context() -> bool: + return _external_propagation_context_fn is not None + + +def _attr_setter(fn: "Any") -> "Any": + return property(fset=fn, doc=fn.__doc__) + + +def _disable_capture(fn: "F") -> "F": + @wraps(fn) + def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": + if not self._should_capture: + return + try: + self._should_capture = False + return fn(self, *args, **kwargs) + finally: + self._should_capture = True + + return wrapper # type: ignore + + +class Scope: + """The scope holds extra information that should be sent with all + events that belong to it. + """ + + # NOTE: Even though it should not happen, the scope needs to not crash when + # accessed by multiple threads. It's fine if it's full of races, but those + # races should never make the user application crash. + # + # The same needs to hold for any accesses of the scope the SDK makes. + + __slots__ = ( + "_level", + "_name", + "_fingerprint", + # note that for legacy reasons, _transaction is the transaction *name*, + # not a Transaction object (the object is stored in _span) + "_transaction", + "_transaction_info", + "_user", + "_tags", + "_contexts", + "_extras", + "_breadcrumbs", + "_n_breadcrumbs_truncated", + "_gen_ai_original_message_count", + "_gen_ai_conversation_id", + "_event_processors", + "_error_processors", + "_should_capture", + "_span", + "_session", + "_attachments", + "_force_auto_session_tracking", + "_profile", + "_propagation_context", + "client", + "_type", + "_last_event_id", + "_flags", + "_attributes", + ) + + def __init__( + self, + ty: "Optional[ScopeType]" = None, + client: "Optional[sentry_sdk.Client]" = None, + ) -> None: + self._type = ty + + self._event_processors: "List[EventProcessor]" = [] + self._error_processors: "List[ErrorProcessor]" = [] + + self._name: "Optional[str]" = None + self._propagation_context: "Optional[PropagationContext]" = None + self._n_breadcrumbs_truncated: int = 0 + self._gen_ai_original_message_count: "Dict[str, int]" = {} + + self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() + + if client is not None: + self.set_client(client) + + self.clear() + + incoming_trace_information = self._load_trace_data_from_env() + self.generate_propagation_context(incoming_data=incoming_trace_information) + + def __copy__(self) -> "Scope": + """ + Returns a copy of this scope. + This also creates a copy of all referenced data structures. + """ + rv: "Scope" = object.__new__(self.__class__) + + rv._type = self._type + rv.client = self.client + rv._level = self._level + rv._name = self._name + rv._fingerprint = self._fingerprint + rv._transaction = self._transaction + rv._transaction_info = self._transaction_info.copy() + rv._user = self._user + + rv._tags = self._tags.copy() + rv._contexts = self._contexts.copy() + rv._extras = self._extras.copy() + + rv._breadcrumbs = copy(self._breadcrumbs) + rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated + rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() + rv._event_processors = self._event_processors.copy() + rv._error_processors = self._error_processors.copy() + rv._propagation_context = self._propagation_context + + rv._should_capture = self._should_capture + rv._span = self._span + rv._session = self._session + rv._force_auto_session_tracking = self._force_auto_session_tracking + rv._attachments = self._attachments.copy() + + rv._profile = self._profile + + rv._last_event_id = self._last_event_id + + rv._flags = deepcopy(self._flags) + + rv._attributes = self._attributes.copy() + + rv._gen_ai_conversation_id = self._gen_ai_conversation_id + + return rv + + @classmethod + def get_current_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the current scope. + """ + current_scope = _current_scope.get() + if current_scope is None: + current_scope = Scope(ty=ScopeType.CURRENT) + _current_scope.set(current_scope) + + return current_scope + + @classmethod + def set_current_scope(cls, new_current_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new current scope overwriting the existing current scope. + :param new_current_scope: The scope to set as the new current scope. + """ + _current_scope.set(new_current_scope) + + @classmethod + def get_isolation_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the isolation scope. + """ + isolation_scope = _isolation_scope.get() + if isolation_scope is None: + isolation_scope = Scope(ty=ScopeType.ISOLATION) + _isolation_scope.set(isolation_scope) + + return isolation_scope + + @classmethod + def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: + """ + .. versionadded:: 2.0.0 + + Sets the given scope as the new isolation scope overwriting the existing isolation scope. + :param new_isolation_scope: The scope to set as the new isolation scope. + """ + _isolation_scope.set(new_isolation_scope) + + @classmethod + def get_global_scope(cls) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns the global scope. + """ + global _global_scope + if _global_scope is None: + _global_scope = Scope(ty=ScopeType.GLOBAL) + + return _global_scope + + def set_global_attributes(self) -> None: + from sentry_sdk.client import SDK_INFO + + self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) + self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) + + self.set_attribute( + "process.runtime.name", + platform.python_implementation(), + ) + self.set_attribute( + "process.runtime.version", + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + ) + + options = sentry_sdk.get_client().options + + server_name = options.get("server_name") + if server_name: + self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) + + environment = options.get("environment") + if environment: + self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) + + release = options.get("release") + if release: + self.set_attribute(SPANDATA.SENTRY_RELEASE, release) + + @classmethod + def last_event_id(cls) -> "Optional[str]": + """ + .. versionadded:: 2.2.0 + + Returns event ID of the event most recently captured by the isolation scope, or None if no event + has been captured. We do not consider events that are dropped, e.g. by a before_send hook. + Transactions also are not considered events in this context. + + The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; + whether the event is sent depends on the transport. The event could be sent later or not at all. + Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or + various other reasons. + """ + return cls.get_isolation_scope()._last_event_id + + def _merge_scopes( + self, + additional_scope: "Optional[Scope]" = None, + additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, + ) -> "Scope": + """ + Merges global, isolation and current scope into a new scope and + adds the given additional scope or additional scope kwargs to it. + """ + if additional_scope and additional_scope_kwargs: + raise TypeError("cannot provide scope and kwargs") + + final_scope = copy(_global_scope) if _global_scope is not None else Scope() + final_scope._type = ScopeType.MERGED + + isolation_scope = _isolation_scope.get() + if isolation_scope is not None: + final_scope.update_from_scope(isolation_scope) + + current_scope = _current_scope.get() + if current_scope is not None: + final_scope.update_from_scope(current_scope) + + if self != current_scope and self != isolation_scope: + final_scope.update_from_scope(self) + + if additional_scope is not None: + if callable(additional_scope): + additional_scope(final_scope) + else: + final_scope.update_from_scope(additional_scope) + + elif additional_scope_kwargs: + final_scope.update_from_kwargs(**additional_scope_kwargs) + + return final_scope + + @classmethod + def get_client(cls) -> "sentry_sdk.client.BaseClient": + """ + .. versionadded:: 2.0.0 + + Returns the currently used :py:class:`sentry_sdk.Client`. + This checks the current scope, the isolation scope and the global scope for a client. + If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. + """ + current_scope = _current_scope.get() + try: + client = current_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + isolation_scope = _isolation_scope.get() + try: + client = isolation_scope.client + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + try: + client = _global_scope.client # type: ignore + except AttributeError: + client = None + + if client is not None and client.is_active(): + return client + + return NonRecordingClient() + + def set_client( + self, client: "Optional[sentry_sdk.client.BaseClient]" = None + ) -> None: + """ + .. versionadded:: 2.0.0 + + Sets the client for this scope. + + :param client: The client to use in this scope. + If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. + + """ + if client is not None: + self.client = client + # We need a client to set the initial global attributes on the global + # scope since they mostly come from client options, so populate them + # as soon as a client is set + sentry_sdk.get_global_scope().set_global_attributes() + else: + self.client = NonRecordingClient() + + def fork(self) -> "Scope": + """ + .. versionadded:: 2.0.0 + + Returns a fork of this scope. + """ + forked_scope = copy(self) + return forked_scope + + def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": + """ + Load Sentry trace id and baggage from environment variables. + Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". + """ + incoming_trace_information: "Optional[Dict[str, str]]" = None + + sentry_use_environment = ( + os.environ.get("SENTRY_USE_ENVIRONMENT") or "" + ).lower() + use_environment = sentry_use_environment not in FALSE_VALUES + if use_environment: + incoming_trace_information = {} + + if os.environ.get("SENTRY_TRACE"): + incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( + os.environ.get("SENTRY_TRACE") or "" + ) + + if os.environ.get("SENTRY_BAGGAGE"): + incoming_trace_information[BAGGAGE_HEADER_NAME] = ( + os.environ.get("SENTRY_BAGGAGE") or "" + ) + + return incoming_trace_information or None + + def set_new_propagation_context(self) -> None: + """ + Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. + """ + self._propagation_context = PropagationContext() + + def generate_propagation_context( + self, incoming_data: "Optional[Dict[str, str]]" = None + ) -> None: + """ + Makes sure the propagation context is set on the scope. + If there is `incoming_data` overwrite existing propagation context. + If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. + """ + if incoming_data is not None: + self._propagation_context = PropagationContext.from_incoming_data( + incoming_data + ) + + # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring + if self._type != ScopeType.CURRENT: + if self._propagation_context is None: + self.set_new_propagation_context() + + def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": + """ + Returns the Dynamic Sampling Context from the Propagation Context. + If not existing, creates a new one. + + Deprecated: Logic moved to PropagationContext, don't use directly. + """ + if self._propagation_context is None: + return None + + return self._propagation_context.dynamic_sampling_context + + def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": + """ + Returns the Sentry "sentry-trace" header (aka the traceparent) from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().to_traceparent() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return traceparent from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_traceparent() + elif not span_streaming and self.span is not None: + return self.span._to_traceparent() + + # else return traceparent from the propagation context + return self.get_active_propagation_context().to_traceparent() + + def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": + """ + Returns the Sentry "baggage" header containing trace information from the + currently active span or the scopes Propagation Context. + """ + client = self.get_client() + + if not has_tracing_enabled(client.options): + return self.get_active_propagation_context().get_baggage() + + span_streaming = has_span_streaming_enabled(client.options) + # If we have an active span, return baggage from there + if span_streaming and type(self.streamed_span) is StreamedSpan: + return self.streamed_span._to_baggage() + elif not span_streaming and self.span is not None: + return self.span._to_baggage() + + # else return baggage from the propagation context + return self.get_active_propagation_context().get_baggage() + + def get_trace_context(self) -> "Dict[str, Any]": + """ + Returns the Sentry "trace" context from the Propagation Context. + """ + if ( + has_tracing_enabled(self.get_client().options) + and self._span is not None + and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) + ): + return self._span._get_trace_context() + + # if we are tracing externally (otel), those values take precedence + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + trace_id, span_id = external_propagation_context + return {"trace_id": trace_id, "span_id": span_id} + + propagation_context = self.get_active_propagation_context() + + return { + "trace_id": propagation_context.trace_id, + "span_id": propagation_context.span_id, + "parent_span_id": propagation_context.parent_span_id, + "dynamic_sampling_context": propagation_context.dynamic_sampling_context, + } + + def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: + """ + Return meta tags which should be injected into HTML templates + to allow propagation of trace information. + """ + span = kwargs.pop("span", None) + if span is not None: + logger.warning( + "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." + ) + + meta = "" + + for name, content in self.iter_trace_propagation_headers(): + meta += f'' + + return meta + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. + Deprecated: use PropagationContext.iter_headers instead. + """ + if self._propagation_context is not None: + yield from self._propagation_context.iter_headers() + + def iter_trace_propagation_headers( + self, *args: "Any", **kwargs: "Any" + ) -> "Generator[Tuple[str, str], None, None]": + """ + Return HTTP headers which allow propagation of trace data. + + If a span is given, the trace data will taken from the span. + If no span is given, the trace data is taken from the scope. + """ + client = self.get_client() + if not client.options.get("propagate_traces"): + warnings.warn( + "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", + DeprecationWarning, + stacklevel=2, + ) + return + + span = kwargs.pop("span", None) + if not span: + span_streaming = has_span_streaming_enabled(client.options) + span = self.streamed_span if span_streaming else self.span + + if ( + has_tracing_enabled(client.options) + and span is not None + and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) + ): + for header in span._iter_headers(): + yield header + elif has_external_propagation_context(): + # when we have an external_propagation_context (otlp) + # we leave outgoing propagation to the propagator + return + else: + for header in self.get_active_propagation_context().iter_headers(): + yield header + + def get_active_propagation_context(self) -> "PropagationContext": + if self._propagation_context is not None: + return self._propagation_context + + current_scope = self.get_current_scope() + if current_scope._propagation_context is not None: + return current_scope._propagation_context + + isolation_scope = self.get_isolation_scope() + # should actually never happen, but just in case someone calls scope.clear + if isolation_scope._propagation_context is None: + isolation_scope._propagation_context = PropagationContext() + return isolation_scope._propagation_context + + @classmethod + def set_custom_sampling_context( + cls, custom_sampling_context: "dict[str, Any]" + ) -> None: + cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( + custom_sampling_context + ) + + def clear(self) -> None: + """Clears the entire scope.""" + self._level: "Optional[LogLevelStr]" = None + self._fingerprint: "Optional[List[str]]" = None + self._transaction: "Optional[str]" = None + self._transaction_info: "dict[str, str]" = {} + self._user: "Optional[Dict[str, Any]]" = None + + self._tags: "Dict[str, Any]" = {} + self._contexts: "Dict[str, Dict[str, Any]]" = {} + self._extras: "dict[str, Any]" = {} + self._attachments: "List[Attachment]" = [] + + self.clear_breadcrumbs() + self._should_capture: bool = True + + self._span: "Optional[Union[Span, StreamedSpan]]" = None + self._session: "Optional[Session]" = None + self._force_auto_session_tracking: "Optional[bool]" = None + + self._profile: "Optional[Profile]" = None + + self._propagation_context = None + + # self._last_event_id is only applicable to isolation scopes + self._last_event_id: "Optional[str]" = None + self._flags: "Optional[FlagBuffer]" = None + + self._attributes: "Attributes" = {} + + self._gen_ai_conversation_id: "Optional[str]" = None + + @_attr_setter + def level(self, value: "LogLevelStr") -> None: + """ + When set this overrides the level. + + .. deprecated:: 1.0.0 + Use :func:`set_level` instead. + + :param value: The level to set. + """ + logger.warning( + "Deprecated: use .set_level() instead. This will be removed in the future." + ) + + self._level = value + + def set_level(self, value: "LogLevelStr") -> None: + """ + Sets the level for the scope. + + :param value: The level to set. + """ + self._level = value + + @_attr_setter + def fingerprint(self, value: "Optional[List[str]]") -> None: + """When set this overrides the default fingerprint.""" + self._fingerprint = value + + @property + def transaction(self) -> "Any": + # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 + """Return the transaction (root span) in the scope, if any.""" + + # there is no span/transaction on the scope + if self._span is None: + return None + + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + # there is an orphan span on the scope + if self._span.containing_transaction is None: + return None + + # there is either a transaction (which is its own containing + # transaction) or a non-orphan span on the scope + return self._span.containing_transaction + + @transaction.setter + def transaction(self, value: "Any") -> None: + # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 + """When set this forces a specific transaction name to be set. + + Deprecated: use set_transaction_name instead.""" + + # XXX: the docstring above is misleading. The implementation of + # apply_to_event prefers an existing value of event.transaction over + # anything set in the scope. + # XXX: note that with the introduction of the Scope.transaction getter, + # there is a semantic and type mismatch between getter and setter. The + # getter returns a Transaction, the setter sets a transaction name. + # Without breaking version compatibility, we could make the setter set a + # transaction name or transaction (self._span) depending on the type of + # the value argument. + + logger.warning( + "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." + ) + self._transaction = value + if self._span: + if isinstance(self._span, StreamedSpan): + warnings.warn( + "Scope.transaction is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return None + + if self._span.containing_transaction: + self._span.containing_transaction.name = value + + def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: + """Set the transaction name and optionally the transaction source.""" + self._transaction = name + if self._span: + if isinstance(self._span, NoOpStreamedSpan): + return + + elif isinstance(self._span, StreamedSpan): + self._span._segment.name = name + if source: + self._span._segment.set_attribute( + "sentry.span.source", getattr(source, "value", source) + ) + + elif self._span.containing_transaction: + self._span.containing_transaction.name = name + if source: + self._span.containing_transaction.source = source + + if source: + self._transaction_info["source"] = source + + @_attr_setter + def user(self, value: "Optional[Dict[str, Any]]") -> None: + """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" + warnings.warn( + "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", + DeprecationWarning, + stacklevel=2, + ) + self.set_user(value) + + def set_user(self, value: "Optional[Dict[str, Any]]") -> None: + """Sets a user for the scope.""" + self._user = value + + session = self.get_isolation_scope()._session + if session is not None: + session.update(user=value) + + @property + def span(self) -> "Optional[Span]": + """Get/set current tracing span or transaction.""" + return self._span if isinstance(self._span, Span) else None + + @span.setter + def span(self, span: "Optional[Span]") -> None: + self._span = span + # XXX: this differs from the implementation in JS, there Scope.setSpan + # does not set Scope._transactionName. + if isinstance(span, Transaction): + transaction = span + if transaction.name: + self._transaction = transaction.name + if transaction.source: + self._transaction_info["source"] = transaction.source + + @property + def streamed_span(self) -> "Optional[StreamedSpan]": + """Get/set current tracing span.""" + return self._span if isinstance(self._span, StreamedSpan) else None + + @streamed_span.setter + def streamed_span(self, span: "Optional[StreamedSpan]") -> None: + self._span = span + + # Also set _transaction and _transaction_info in streaming mode as this + # is used for populating events and linking them to segments + if type(span) is StreamedSpan and span._is_segment(): + self._transaction = span.name + if span._attributes.get("sentry.span.source"): + self._transaction_info["source"] = str( + span._attributes["sentry.span.source"] + ) + + @property + def profile(self) -> "Optional[Profile]": + return self._profile + + @profile.setter + def profile(self, profile: "Optional[Profile]") -> None: + self._profile = profile + + def set_tag(self, key: str, value: "Any") -> None: + """ + Sets a tag for a key to a specific value. + + :param key: Key of the tag to set. + + :param value: Value of the tag to set. + """ + self._tags[key] = value + + def set_tags(self, tags: "Mapping[str, object]") -> None: + """Sets multiple tags at once. + + This method updates multiple tags at once. The tags are passed as a dictionary + or other mapping type. + + Calling this method is equivalent to calling `set_tag` on each key-value pair + in the mapping. If a tag key already exists in the scope, its value will be + updated. If the tag key does not exist in the scope, the key-value pair will + be added to the scope. + + This method only modifies tag keys in the `tags` mapping passed to the method. + `scope.set_tags({})` is, therefore, a no-op. + + :param tags: A mapping of tag keys to tag values to set. + """ + self._tags.update(tags) + + def remove_tag(self, key: str) -> None: + """ + Removes a specific tag. + + :param key: Key of the tag to remove. + """ + self._tags.pop(key, None) + + def set_context( + self, + key: str, + value: "Dict[str, Any]", + ) -> None: + """ + Binds a context at a certain key to a specific value. + """ + self._contexts[key] = value + + def remove_context( + self, + key: str, + ) -> None: + """Removes a context.""" + self._contexts.pop(key, None) + + def set_extra( + self, + key: str, + value: "Any", + ) -> None: + """Sets an extra key to a specific value.""" + self._extras[key] = value + + def remove_extra( + self, + key: str, + ) -> None: + """Removes a specific extra key.""" + self._extras.pop(key, None) + + def set_conversation_id(self, conversation_id: str) -> None: + """ + Sets the conversation ID for gen_ai spans. + + :param conversation_id: The conversation ID to set. + """ + self._gen_ai_conversation_id = conversation_id + + def get_conversation_id(self) -> "Optional[str]": + """ + Gets the conversation ID for gen_ai spans. + + :returns: The conversation ID, or None if not set. + """ + return self._gen_ai_conversation_id + + def remove_conversation_id(self) -> None: + """Removes the conversation ID.""" + self._gen_ai_conversation_id = None + + def clear_breadcrumbs(self) -> None: + """Clears breadcrumb buffer.""" + self._breadcrumbs: "Deque[Breadcrumb]" = deque() + self._n_breadcrumbs_truncated = 0 + + def add_attachment( + self, + bytes: "Union[None, bytes, Callable[[], bytes]]" = None, + filename: "Optional[str]" = None, + path: "Optional[str]" = None, + content_type: "Optional[str]" = None, + add_to_transactions: bool = False, + ) -> None: + """Adds an attachment to future events sent from this scope. + + The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. + """ + self._attachments.append( + Attachment( + bytes=bytes, + path=path, + filename=filename, + content_type=content_type, + add_to_transactions=add_to_transactions, + ) + ) + + def add_breadcrumb( + self, + crumb: "Optional[Breadcrumb]" = None, + hint: "Optional[BreadcrumbHint]" = None, + **kwargs: "Any", + ) -> None: + """ + Adds a breadcrumb. + + :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. + + :param hint: An optional value that can be used by `before_breadcrumb` + to customize the breadcrumbs that are emitted. + """ + client = self.get_client() + + if not client.is_active(): + logger.info("Dropped breadcrumb because no client bound") + return + + before_breadcrumb = client.options.get("before_breadcrumb") + max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) + + crumb = dict(crumb or ()) + crumb.update(kwargs) + if not crumb: + return + + hint = dict(hint or ()) + + if crumb.get("timestamp") is None: + crumb["timestamp"] = datetime.now(timezone.utc) + if crumb.get("type") is None: + crumb["type"] = "default" + + if before_breadcrumb is not None: + new_crumb = before_breadcrumb(crumb, hint) + else: + new_crumb = crumb + + if new_crumb is not None: + self._breadcrumbs.append(new_crumb) + else: + logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) + + while len(self._breadcrumbs) > max_breadcrumbs: + self._breadcrumbs.popleft() + self._n_breadcrumbs_truncated += 1 + + def start_transaction( + self, + transaction: "Optional[Transaction]" = None, + instrumenter: str = INSTRUMENTER.SENTRY, + custom_sampling_context: "Optional[SamplingContext]" = None, + **kwargs: "Unpack[TransactionKwargs]", + ) -> "Union[Transaction, NoOpSpan]": + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + + This is the entry point to manual tracing instrumentation. + + A tree structure can be built by adding child spans to the transaction, + and child spans to other spans. To start a new child span within the + transaction or any span, call the respective `.start_child()` method. + + Every child span must be finished before the transaction is finished, + otherwise the unfinished spans are discarded. + + When used as context managers, spans and transactions are automatically + finished at the end of the `with` block. If not using context managers, + call the `.finish()` method. + + When the transaction is finished, it will be sent to Sentry with all its + finished child spans. + + :param transaction: The transaction to start. If omitted, we create and + start a new transaction. + :param instrumenter: This parameter is meant for internal use only. It + will be removed in the next major version. + :param custom_sampling_context: The transaction's custom sampling context. + :param kwargs: Optional keyword arguments to be passed to the Transaction + constructor. See :py:class:`sentry_sdk.tracing.Transaction` for + available arguments. + """ + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + try_autostart_continuous_profiler() + + custom_sampling_context = custom_sampling_context or {} + + # kwargs at this point has type TransactionKwargs, since we have removed + # the client and custom_sampling_context from it. + transaction_kwargs: "TransactionKwargs" = kwargs + + # if we haven't been given a transaction, make one + if transaction is None: + transaction = Transaction(**transaction_kwargs) + + # use traces_sample_rate, traces_sampler, and/or inheritance to make a + # sampling decision + sampling_context = { + "transaction_context": transaction.to_json(), + "parent_sampled": transaction.parent_sampled, + } + sampling_context.update(custom_sampling_context) + transaction._set_initial_sampling_decision(sampling_context=sampling_context) + + # update the sample rate in the dsc + if transaction.sample_rate is not None: + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction._baggage: + transaction._baggage.sentry_items["sample_rate"] = str( + transaction.sample_rate + ) + + if transaction.sampled: + profile = Profile( + transaction.sampled, transaction._start_timestamp_monotonic_ns + ) + profile._set_initial_sampling_decision(sampling_context=sampling_context) + + transaction._profile = profile + + transaction._continuous_profile = try_profile_lifecycle_trace_start() + + # Typically, the profiler is set when the transaction is created. But when + # using the auto lifecycle, the profiler isn't running when the first + # transaction is started. So make sure we update the profiler id on it. + if transaction._continuous_profile is not None: + transaction.set_profiler_id(get_profiler_id()) + + # we don't bother to keep spans if we already know we're not going to + # send the transaction + max_spans = (client.options["_experiments"].get("max_spans")) or 1000 + transaction.init_span_recorder(maxlen=max_spans) + + return transaction + + def start_span( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a span whose parent is the currently active span or transaction, if any. + + The return value is a :py:class:`sentry_sdk.tracing.Span` instance, + typically used as a context manager to start and stop timing in a `with` + block. + + Only spans contained in a transaction are sent to Sentry. Most + integrations start a transaction at the appropriate time, for example + for every incoming HTTP request. Use + :py:meth:`sentry_sdk.start_transaction` to start a new transaction when + one is not already in progress. + + For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + client = sentry_sdk.get_client() + if has_span_streaming_enabled(client.options): + warnings.warn( + "Scope.start_span is not available in streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return NoOpSpan() + + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + with new_scope(): + kwargs.setdefault("scope", self) + + client = self.get_client() + + configuration_instrumenter = client.options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + # get current span or transaction + span = self.span or self.get_isolation_scope().span + if isinstance(span, StreamedSpan): + # make mypy happy + return NoOpSpan() + + if span is None: + # New spans get the `trace_id` from the scope + if "trace_id" not in kwargs: + propagation_context = self.get_active_propagation_context() + kwargs["trace_id"] = propagation_context.trace_id + + span = Span(**kwargs) + else: + # Children take `trace_id`` from the parent span. + span = span.start_child(**kwargs) + + return span + + def start_streamed_span( + self, + name: str, + attributes: "Optional[Attributes]", + parent_span: "Optional[StreamedSpan]", + active: bool, + ) -> "StreamedSpan": + # TODO: rename to start_span once we drop the old API + if isinstance(parent_span, NoOpStreamedSpan): + # parent_span is only set if the user explicitly set it + logger.debug( + "Ignored parent span provided. Span will be parented to the " + "currently active span instead." + ) + + if parent_span is _DEFAULT_PARENT_SPAN or isinstance( + parent_span, NoOpStreamedSpan + ): + parent_span = self.streamed_span + + # If no eligible parent_span was provided and there is no currently + # active span, this is a segment + if parent_span is None: + propagation_context = self.get_active_propagation_context() + + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + scope=self, + unsampled_reason="ignored", + ) + + sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( + name, + attributes, + self, + ) + + if sample_rate is not None: + self._update_sample_rate(sample_rate) + + if sampled is False: + return NoOpStreamedSpan( + scope=self, + unsampled_reason=outcome, + ) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=None, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + parent_sampled=propagation_context.parent_sampled, + baggage=propagation_context.baggage, + sample_rand=sample_rand, + sample_rate=sample_rate, + ) + + # This is a child span; take propagation context from the parent span + with new_scope(): + if is_ignored_span(name, attributes): + return NoOpStreamedSpan( + unsampled_reason="ignored", + ) + + if isinstance(parent_span, NoOpStreamedSpan): + return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) + + return StreamedSpan( + name=name, + attributes=attributes, + active=active, + scope=self, + segment=parent_span._segment, + trace_id=parent_span.trace_id, + parent_span_id=parent_span.span_id, + parent_sampled=parent_span.sampled, + ) + + def _update_sample_rate(self, sample_rate: float) -> None: + # If we had to adjust the sample rate when setting the sampling decision + # for a span, it needs to be updated in the propagation context too + propagation_context = self.get_active_propagation_context() + baggage = propagation_context.baggage + + if baggage is not None: + baggage.sentry_items["sample_rate"] = str(sample_rate) + + def continue_trace( + self, + environ_or_headers: "Dict[str, Any]", + op: "Optional[str]" = None, + name: "Optional[str]" = None, + source: "Optional[str]" = None, + origin: str = "manual", + ) -> "Transaction": + """ + Sets the propagation context from environment or headers and returns a transaction. + """ + self.generate_propagation_context(environ_or_headers) + + # generate_propagation_context ensures that the propagation_context is not None. + propagation_context = cast(PropagationContext, self._propagation_context) + + optional_kwargs = {} + if name: + optional_kwargs["name"] = name + if source: + optional_kwargs["source"] = source + + return Transaction( + op=op, + origin=origin, + baggage=propagation_context.baggage, + parent_sampled=propagation_context.parent_sampled, + trace_id=propagation_context.trace_id, + parent_span_id=propagation_context.parent_span_id, + same_process_as_parent=False, + **optional_kwargs, + ) + + def capture_event( + self, + event: "Event", + hint: "Optional[Hint]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures an event. + + Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. + + :param event: A ready-made event that can be directly sent to Sentry. + + :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + scope = self._merge_scopes(scope, scope_kwargs) + + event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) + + if event_id is not None and event.get("type") != "transaction": + self.get_isolation_scope()._last_event_id = event_id + + return event_id + + def _capture_log(self, log: "Optional[Log]") -> None: + if log is None: + return + + client = self.get_client() + if not has_logs_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" + ) + + client._capture_log(log, scope=merged_scope) + + def _capture_metric(self, metric: "Optional[Metric]") -> None: + if metric is None: + return + + client = self.get_client() + if not has_metrics_enabled(client.options): + return + + merged_scope = self._merge_scopes() + + debug = client.options.get("debug", False) + if debug: + logger.debug( + f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" + ) + + client._capture_metric(metric, scope=merged_scope) + + def _capture_span(self, span: "Optional[StreamedSpan]") -> None: + if span is None: + return + + client = self.get_client() + if not has_span_streaming_enabled(client.options): + return + + merged_scope = self._merge_scopes() + client._capture_span(span, scope=merged_scope) + + def capture_message( + self, + message: str, + level: "Optional[LogLevelStr]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """ + Captures a message. + + :param message: The string to send as the message. + + :param level: If no level is provided, the default level is `info`. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if level is None: + level = "info" + + event: "Event" = { + "message": message, + "level": level, + } + + return self.capture_event(event, scope=scope, **scope_kwargs) + + def capture_exception( + self, + error: "Optional[Union[BaseException, ExcInfo]]" = None, + scope: "Optional[Scope]" = None, + **scope_kwargs: "Any", + ) -> "Optional[str]": + """Captures an exception. + + :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. + + :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :param scope_kwargs: Optional data to apply to event. + For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. + The `scope` and `scope_kwargs` parameters are mutually exclusive. + + :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). + """ + if disable_capture_event.get(False): + return None + + if error is not None: + exc_info = exc_info_from_error(error) + else: + exc_info = sys.exc_info() + + event, hint = event_from_exception( + exc_info, client_options=self.get_client().options + ) + + try: + return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) + except Exception: + capture_internal_exception(sys.exc_info()) + + return None + + def start_session(self, *args: "Any", **kwargs: "Any") -> None: + """Starts a new session.""" + session_mode = kwargs.pop("session_mode", "application") + + self.end_session() + + client = self.get_client() + self._session = Session( + release=client.options.get("release"), + environment=client.options.get("environment"), + user=self._user, + session_mode=session_mode, + ) + + def end_session(self, *args: "Any", **kwargs: "Any") -> None: + """Ends the current session if there is one.""" + session = self._session + self._session = None + + if session is not None: + session.close() + self.get_client().capture_session(session) + + def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: + """Stops automatic session tracking. + + This temporarily session tracking for the current scope when called. + To resume session tracking call `resume_auto_session_tracking`. + """ + self.end_session() + self._force_auto_session_tracking = False + + def resume_auto_session_tracking(self) -> None: + """Resumes automatic session tracking for the current scope if + disabled earlier. This requires that generally automatic session + tracking is enabled. + """ + self._force_auto_session_tracking = None + + def add_event_processor( + self, + func: "EventProcessor", + ) -> None: + """Register a scope local event processor on the scope. + + :param func: This function behaves like `before_send.` + """ + if len(self._event_processors) > 20: + logger.warning( + "Too many event processors on scope! Clearing list to free up some memory: %r", + self._event_processors, + ) + del self._event_processors[:] + + self._event_processors.append(func) + + def add_error_processor( + self, + func: "ErrorProcessor", + cls: "Optional[Type[BaseException]]" = None, + ) -> None: + """Register a scope local error processor on the scope. + + :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. + + :param cls: Optionally, only process exceptions of this type. + """ + if cls is not None: + cls_ = cls # For mypy. + real_func = func + + def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": + try: + is_inst = isinstance(exc_info[1], cls_) + except Exception: + is_inst = False + if is_inst: + return real_func(event, exc_info) + return event + + self._error_processors.append(func) + + def _apply_level_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._level is not None: + event["level"] = self._level + + def _apply_breadcrumbs_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + event.setdefault("breadcrumbs", {}) + + # This check is just for mypy - + if not isinstance(event["breadcrumbs"], AnnotatedValue): + event["breadcrumbs"].setdefault("values", []) + event["breadcrumbs"]["values"].extend(self._breadcrumbs) + + # Attempt to sort timestamps + try: + if not isinstance(event["breadcrumbs"], AnnotatedValue): + for crumb in event["breadcrumbs"]["values"]: + if isinstance(crumb["timestamp"], str): + crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) + + event["breadcrumbs"]["values"].sort( + key=lambda crumb: crumb["timestamp"] + ) + except Exception as err: + logger.debug("Error when sorting breadcrumbs", exc_info=err) + pass + + def _apply_user_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("user") is None and self._user is not None: + event["user"] = self._user + + def _apply_transaction_name_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction") is None and self._transaction is not None: + event["transaction"] = self._transaction + + def _apply_transaction_info_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("transaction_info") is None and self._transaction_info is not None: + event["transaction_info"] = self._transaction_info + + def _apply_fingerprint_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if event.get("fingerprint") is None and self._fingerprint is not None: + event["fingerprint"] = self._fingerprint + + def _apply_extra_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._extras: + event.setdefault("extra", {}).update(self._extras) + + def _apply_tags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._tags: + event.setdefault("tags", {}).update(self._tags) + + def _apply_contexts_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + if self._contexts: + event.setdefault("contexts", {}).update(self._contexts) + + contexts = event.setdefault("contexts", {}) + + # Add "trace" context + if contexts.get("trace") is None: + contexts["trace"] = self.get_trace_context() + + def _apply_flags_to_event( + self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" + ) -> None: + flags = self.flags.get() + if len(flags) > 0: + event.setdefault("contexts", {}).setdefault("flags", {}).update( + {"values": flags} + ) + + def _apply_scope_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + # TODO: turn Logs, Metrics into actual classes + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + for attribute, value in self._attributes.items(): + if attribute not in attributes: + attributes[attribute] = value + + def _apply_user_attributes_to_telemetry( + self, telemetry: "Union[Log, Metric, StreamedSpan]" + ) -> None: + if isinstance(telemetry, dict): + attributes = telemetry["attributes"] + else: + attributes = telemetry._attributes + + if not should_send_default_pii() or self._user is None: + return + + for attribute_name, user_attribute in ( + ("user.id", "id"), + ("user.name", "username"), + ("user.email", "email"), + ("user.ip_address", "ip_address"), + ): + if ( + user_attribute in self._user + and attribute_name not in attributes + and self._user[user_attribute] is not None + ): + attributes[attribute_name] = self._user[user_attribute] + + def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": + logger.info("%s (%s) dropped event", ty, cause) + return None + + def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the error processors on the event and returns the modified event. + """ + exc_info = hint.get("exc_info") + if exc_info is not None: + error_processors = chain( + self.get_global_scope()._error_processors, + self.get_isolation_scope()._error_processors, + self.get_current_scope()._error_processors, + ) + + for error_processor in error_processors: + new_event = error_processor(event, exc_info) + if new_event is None: + return self._drop(error_processor, "error processor") + + event = new_event + + return event + + def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": + """ + Runs the event processors on the event and returns the modified event. + """ + ty = event.get("type") + is_check_in = ty == "check_in" + + if not is_check_in: + # Get scopes without creating them to prevent infinite recursion + isolation_scope = _isolation_scope.get() + current_scope = _current_scope.get() + + event_processors = chain( + global_event_processors, + _global_scope and _global_scope._event_processors or [], + isolation_scope and isolation_scope._event_processors or [], + current_scope and current_scope._event_processors or [], + ) + + for event_processor in event_processors: + new_event = event + with capture_internal_exceptions(): + new_event = event_processor(event, hint) + if new_event is None: + return self._drop(event_processor, "event processor") + event = new_event + + return event + + @_disable_capture + def apply_to_event( + self, + event: "Event", + hint: "Hint", + options: "Optional[Dict[str, Any]]" = None, + ) -> "Optional[Event]": + """Applies the information contained on the scope to the given event.""" + ty = event.get("type") + is_transaction = ty == "transaction" + is_check_in = ty == "check_in" + + # put all attachments into the hint. This lets callbacks play around + # with attachments. We also later pull this out of the hint when we + # create the envelope. + attachments_to_send = hint.get("attachments") or [] + for attachment in self._attachments: + if not is_transaction or attachment.add_to_transactions: + attachments_to_send.append(attachment) + hint["attachments"] = attachments_to_send + + self._apply_contexts_to_event(event, hint, options) + + if is_check_in: + # Check-ins only support the trace context, strip all others + event["contexts"] = { + "trace": event.setdefault("contexts", {}).get("trace", {}) + } + + if not is_check_in: + self._apply_level_to_event(event, hint, options) + self._apply_fingerprint_to_event(event, hint, options) + self._apply_user_to_event(event, hint, options) + self._apply_transaction_name_to_event(event, hint, options) + self._apply_transaction_info_to_event(event, hint, options) + self._apply_tags_to_event(event, hint, options) + self._apply_extra_to_event(event, hint, options) + + if not is_transaction and not is_check_in: + self._apply_breadcrumbs_to_event(event, hint, options) + self._apply_flags_to_event(event, hint, options) + + event = self.run_error_processors(event, hint) + if event is None: + return None + + event = self.run_event_processors(event, hint) + if event is None: + return None + + return event + + @_disable_capture + def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: + # Attributes-based events and telemetry go through here (logs, metrics, + # spansV2) + if not isinstance(telemetry, StreamedSpan): + trace_context = self.get_trace_context() + trace_id = trace_context.get("trace_id") + if telemetry.get("trace_id") is None and trace_id is not None: + telemetry["trace_id"] = trace_id + + # span_id should only be populated if there's an active span. We can't + # use the trace_context here because it synthesizes a span_id if there + # isn't one + if telemetry.get("span_id") is None: + if self._span is not None and not isinstance( + self._span, (NoOpStreamedSpan, NoOpSpan) + ): + telemetry["span_id"] = self._span.span_id + else: + external_propagation_context = get_external_propagation_context() + if external_propagation_context: + _, span_id = external_propagation_context + if span_id is not None: + telemetry["span_id"] = span_id + + self._apply_scope_attributes_to_telemetry(telemetry) + self._apply_user_attributes_to_telemetry(telemetry) + + def update_from_scope(self, scope: "Scope") -> None: + """Update the scope with another scope's data.""" + if scope._level is not None: + self._level = scope._level + if scope._fingerprint is not None: + self._fingerprint = scope._fingerprint + if scope._transaction is not None: + self._transaction = scope._transaction + if scope._transaction_info is not None: + self._transaction_info.update(scope._transaction_info) + if scope._user is not None: + self._user = scope._user + if scope._tags: + self._tags.update(scope._tags) + if scope._contexts: + self._contexts.update(scope._contexts) + if scope._extras: + self._extras.update(scope._extras) + if scope._breadcrumbs: + self._breadcrumbs.extend(scope._breadcrumbs) + if scope._n_breadcrumbs_truncated: + self._n_breadcrumbs_truncated = ( + self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated + ) + if scope._gen_ai_original_message_count: + self._gen_ai_original_message_count.update( + scope._gen_ai_original_message_count + ) + if scope._gen_ai_conversation_id: + self._gen_ai_conversation_id = scope._gen_ai_conversation_id + if scope._span: + self._span = scope._span + if scope._attachments: + self._attachments.extend(scope._attachments) + if scope._profile: + self._profile = scope._profile + if scope._propagation_context: + self._propagation_context = scope._propagation_context + if scope._session: + self._session = scope._session + if scope._flags: + if not self._flags: + self._flags = deepcopy(scope._flags) + else: + for flag in scope._flags.get(): + self._flags.set(flag["flag"], flag["result"]) + if scope._attributes: + self._attributes.update(scope._attributes) + + def update_from_kwargs( + self, + user: "Optional[Any]" = None, + level: "Optional[LogLevelStr]" = None, + extras: "Optional[Dict[str, Any]]" = None, + contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, + tags: "Optional[Dict[str, str]]" = None, + fingerprint: "Optional[List[str]]" = None, + attributes: "Optional[Attributes]" = None, + ) -> None: + """Update the scope's attributes.""" + if level is not None: + self._level = level + if user is not None: + self._user = user + if extras is not None: + self._extras.update(extras) + if contexts is not None: + self._contexts.update(contexts) + if tags is not None: + self._tags.update(tags) + if fingerprint is not None: + self._fingerprint = fingerprint + if attributes is not None: + self._attributes.update(attributes) + + def __repr__(self) -> str: + return "<%s id=%s name=%s type=%s>" % ( + self.__class__.__name__, + hex(id(self)), + self._name, + self._type, + ) + + @property + def flags(self) -> "FlagBuffer": + if self._flags is None: + max_flags = ( + self.get_client().options["_experiments"].get("max_flags") + or DEFAULT_FLAG_CAPACITY + ) + self._flags = FlagBuffer(capacity=max_flags) + return self._flags + + def set_attribute(self, attribute: str, value: "AttributeValue") -> None: + """ + Set an attribute on the scope. + + Any attributes-based telemetry (logs, metrics) captured while this scope + is active will inherit attributes set on the scope. + """ + self._attributes[attribute] = format_attribute(value) + + def remove_attribute(self, attribute: str) -> None: + """Remove an attribute if set on the scope. No-op if there is no such attribute.""" + try: + del self._attributes[attribute] + except KeyError: + pass + + +@contextmanager +def new_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current scope and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.new_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + new_scope = current_scope.fork() + token = _current_scope.set(new_scope) + + try: + yield new_scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `scope` and runs the wrapped code in it. + After the wrapped code is executed, the original scope is restored. + + Example Usage: + Suppose the variable `scope` contains a `Scope` object, which is not currently + the active scope. + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.use_scope(scope): + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # set given scope as current scope + token = _current_scope.set(scope) + + try: + yield scope + + finally: + try: + # restore original scope + _current_scope.reset(token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def isolation_scope() -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that forks the current isolation scope and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # fork isolation scope + isolation_scope = Scope.get_isolation_scope() + new_isolation_scope = isolation_scope.fork() + isolation_token = _isolation_scope.set(new_isolation_scope) + + try: + yield new_isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +@contextmanager +def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": + """ + .. versionadded:: 2.0.0 + + Context manager that uses the given `isolation_scope` and runs the wrapped code in it. + The current scope is also forked to not bleed data into the existing current scope. + After the wrapped code is executed, the original scopes are restored. + + Example Usage: + + .. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("color", "green") + sentry_sdk.capture_message("hello") # will include `color` tag. + + sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. + + """ + # fork current scope + current_scope = Scope.get_current_scope() + forked_current_scope = current_scope.fork() + current_token = _current_scope.set(forked_current_scope) + + # set given scope as isolation scope + isolation_token = _isolation_scope.set(isolation_scope) + + try: + yield isolation_scope + + finally: + # restore original scopes + try: + _current_scope.reset(current_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + try: + _isolation_scope.reset(isolation_token) + except (LookupError, ValueError): + capture_internal_exception(sys.exc_info()) + + +def should_send_default_pii() -> bool: + """Shortcut for `Scope.get_client().should_send_default_pii()`.""" + return Scope.get_client().should_send_default_pii() + + +# Circular imports +from sentry_sdk.client import NonRecordingClient + +if TYPE_CHECKING: + import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py new file mode 100644 index 0000000000..6794491325 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py @@ -0,0 +1,176 @@ +from typing import TYPE_CHECKING, Dict, List, cast + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + iter_event_frames, +) + +if TYPE_CHECKING: + from typing import Optional + + from sentry_sdk._types import Event + + +DEFAULT_DENYLIST = [ + # stolen from relay + "password", + "passwd", + "secret", + "api_key", + "apikey", + "auth", + "credentials", + "mysql_pwd", + "privatekey", + "private_key", + "token", + "session", + # django + "csrftoken", + "sessionid", + # wsgi + "x_csrftoken", + "x_forwarded_for", + "set_cookie", + "cookie", + "authorization", + "proxy-authorization", + "x_api_key", + # other common names used in the wild + "aiohttp_session", # aiohttp + "connect.sid", # Express + "csrf_token", # Pyramid + "csrf", # (this is a cookie name used in accepted answers on stack overflow) + "_csrf", # Express + "_csrf_token", # Bottle + "PHPSESSID", # PHP + "_session", # Sanic + "symfony", # Symfony + "user_session", # Vue + "_xsrf", # Tornado + "XSRF-TOKEN", # Angular, Laravel +] + +DEFAULT_PII_DENYLIST = [ + "x_forwarded_for", + "x_real_ip", + "ip_address", + "remote_addr", +] + + +class EventScrubber: + def __init__( + self, + denylist: "Optional[List[str]]" = None, + recursive: bool = False, + send_default_pii: bool = False, + pii_denylist: "Optional[List[str]]" = None, + ) -> None: + """ + A scrubber that goes through the event payload and removes sensitive data configured through denylists. + + :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. + :param recursive: Whether to scrub the event payload recursively, default False. + :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. + :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. + """ + self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist + + if not send_default_pii: + pii_denylist = ( + DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist + ) + self.denylist += pii_denylist + + self.denylist = [x.lower() for x in self.denylist] + self.recursive = recursive + + def scrub_list(self, lst: object) -> None: + """ + If a list is passed to this method, the method recursively searches the list and any + nested lists for any dictionaries. The method calls scrub_dict on all dictionaries + it finds. + If the parameter passed to this method is not a list, the method does nothing. + """ + if not isinstance(lst, list): + return + + for v in lst: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_dict(self, d: object) -> None: + """ + If a dictionary is passed to this method, the method scrubs the dictionary of any + sensitive data. The method calls itself recursively on any nested dictionaries ( + including dictionaries nested in lists) if self.recursive is True. + This method does nothing if the parameter passed to it is not a dictionary. + """ + if not isinstance(d, dict): + return + + for k, v in d.items(): + # The cast is needed because mypy is not smart enough to figure out that k must be a + # string after the isinstance check. + if isinstance(k, str) and k.lower() in self.denylist: + d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() + elif self.recursive: + self.scrub_dict(v) # no-op unless v is a dict + self.scrub_list(v) # no-op unless v is a list + + def scrub_request(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "request" in event: + if "headers" in event["request"]: + self.scrub_dict(event["request"]["headers"]) + if "cookies" in event["request"]: + self.scrub_dict(event["request"]["cookies"]) + if "data" in event["request"]: + self.scrub_dict(event["request"]["data"]) + + def scrub_extra(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "extra" in event: + self.scrub_dict(event["extra"]) + + def scrub_user(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "user" in event: + user = event["user"] + if "ip_address" in self.denylist and isinstance(user, dict): + user.pop("ip_address", None) + self.scrub_dict(user) + + def scrub_breadcrumbs(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "breadcrumbs" in event: + if ( + not isinstance(event["breadcrumbs"], AnnotatedValue) + and "values" in event["breadcrumbs"] + ): + for value in event["breadcrumbs"]["values"]: + if "data" in value: + self.scrub_dict(value["data"]) + + def scrub_frames(self, event: "Event") -> None: + with capture_internal_exceptions(): + for frame in iter_event_frames(event): + if "vars" in frame: + self.scrub_dict(frame["vars"]) + + def scrub_spans(self, event: "Event") -> None: + with capture_internal_exceptions(): + if "spans" in event: + for span in cast(List[Dict[str, object]], event["spans"]): + if "data" in span: + self.scrub_dict(span["data"]) + + def scrub_event(self, event: "Event") -> None: + self.scrub_request(event) + self.scrub_extra(event) + self.scrub_user(event) + self.scrub_breadcrumbs(event) + self.scrub_frames(event) + self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py new file mode 100644 index 0000000000..6bf6f6e70b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py @@ -0,0 +1,418 @@ +import math +import sys +from array import array +from collections.abc import Mapping +from datetime import datetime +from typing import TYPE_CHECKING + +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exception, + disable_capture_event, + format_timestamp, + safe_repr, + strip_string, +) + +if TYPE_CHECKING: + from types import TracebackType + from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union + + from sentry_sdk._types import NotImplementedType + + Span = Dict[str, Any] + + ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] + Segment = Union[str, int] + + +# Bytes are technically not strings in Python 3, but we can serialize them +serializable_str_types = (str, bytes, bytearray, memoryview) + + +# Maximum length of JSON-serialized event payloads that can be safely sent +# before the server may reject the event due to its size. This is not intended +# to reflect actual values defined server-side, but rather only be an upper +# bound for events sent by the SDK. +# +# Can be overwritten if wanting to send more bytes, e.g. with a custom server. +# When changing this, keep in mind that events may be a little bit larger than +# this value due to attached metadata, so keep the number conservative. +MAX_EVENT_BYTES = 10**6 + +# Maximum depth and breadth of databags. Excess data will be trimmed. If +# max_request_body_size is "always", request bodies won't be trimmed. +MAX_DATABAG_DEPTH = 5 +MAX_DATABAG_BREADTH = 10 +CYCLE_MARKER = "" + + +global_repr_processors: "List[ReprProcessor]" = [] + + +def add_global_repr_processor(processor: "ReprProcessor") -> None: + global_repr_processors.append(processor) + + +sequence_types: "List[type]" = [tuple, list, set, frozenset, array] + + +def add_repr_sequence_type(ty: type) -> None: + sequence_types.append(ty) + + +class Memo: + __slots__ = ("_ids", "_objs") + + def __init__(self) -> None: + self._ids: "Dict[int, Any]" = {} + self._objs: "List[Any]" = [] + + def memoize(self, obj: "Any") -> "ContextManager[bool]": + self._objs.append(obj) + return self + + def __enter__(self) -> bool: + obj = self._objs[-1] + if id(obj) in self._ids: + return True + else: + self._ids[id(obj)] = obj + return False + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> None: + self._ids.pop(id(self._objs.pop()), None) + + +class _Serializer: + """Holds the state of a single serialize() call.""" + + __slots__ = ( + "memo", + "path", + "meta_stack", + "keep_request_bodies", + "max_value_length", + "is_vars", + "custom_repr", + ) + + def __init__( + self, + keep_request_bodies: bool, + max_value_length: "Optional[int]", + is_vars: bool, + custom_repr: "Optional[Callable[..., Optional[str]]]", + ) -> None: + self.memo = Memo() + self.path: "List[Segment]" = [] + self.meta_stack: "List[Dict[str, Any]]" = [] + self.keep_request_bodies = keep_request_bodies + self.max_value_length = max_value_length + self.is_vars = is_vars + self.custom_repr = custom_repr + + def _safe_repr_wrapper(self, value: "Any") -> str: + try: + repr_value = None + if self.custom_repr is not None: + repr_value = self.custom_repr(value) + return repr_value or safe_repr(value) + except Exception: + return safe_repr(value) + + def _annotate(self, **meta: "Any") -> None: + while len(self.meta_stack) <= len(self.path): + try: + segment = self.path[len(self.meta_stack) - 1] + node = self.meta_stack[-1].setdefault(str(segment), {}) + except IndexError: + node = {} + + self.meta_stack.append(node) + + self.meta_stack[-1].setdefault("", {}).update(meta) + + def _is_databag(self) -> "Optional[bool]": + """ + A databag is any value that we need to trim. + True for stuff like vars, request bodies, breadcrumbs and extra. + + :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". + """ + try: + if self.is_vars: + return True + + is_request_body = self._is_request_body() + if is_request_body in (True, None): + return is_request_body + + p0 = self.path[0] + if p0 == "breadcrumbs" and self.path[1] == "values": + self.path[2] + return True + + if p0 == "extra": + return True + + except IndexError: + return None + + return False + + def _is_span_attribute(self) -> "Optional[bool]": + try: + if self.path[0] == "spans" and self.path[2] == "data": + return True + except IndexError: + return None + + return False + + def _is_request_body(self) -> "Optional[bool]": + try: + if self.path[0] == "request" and self.path[1] == "data": + return True + except IndexError: + return None + + return False + + def _serialize_node( + self, + obj: "Any", + is_databag: "Optional[bool]" = None, + is_request_body: "Optional[bool]" = None, + should_repr_strings: "Optional[bool]" = None, + segment: "Optional[Segment]" = None, + remaining_breadth: "Optional[Union[int, float]]" = None, + remaining_depth: "Optional[Union[int, float]]" = None, + ) -> "Any": + if segment is not None: + self.path.append(segment) + + try: + with self.memo.memoize(obj) as result: + if result: + return CYCLE_MARKER + + return self._serialize_node_impl( + obj, + is_databag=is_databag, + is_request_body=is_request_body, + should_repr_strings=should_repr_strings, + remaining_depth=remaining_depth, + remaining_breadth=remaining_breadth, + ) + except BaseException: + capture_internal_exception(sys.exc_info()) + + if is_databag: + return "" + + return None + finally: + if segment is not None: + self.path.pop() + del self.meta_stack[len(self.path) + 1 :] + + def _flatten_annotated(self, obj: "Any") -> "Any": + if isinstance(obj, AnnotatedValue): + self._annotate(**obj.metadata) + obj = obj.value + return obj + + def _serialize_node_impl( + self, + obj: "Any", + is_databag: "Optional[bool]", + is_request_body: "Optional[bool]", + should_repr_strings: "Optional[bool]", + remaining_depth: "Optional[Union[float, int]]", + remaining_breadth: "Optional[Union[float, int]]", + ) -> "Any": + if isinstance(obj, AnnotatedValue): + should_repr_strings = False + if should_repr_strings is None: + should_repr_strings = self.is_vars + + if is_databag is None: + is_databag = self._is_databag() + + if is_request_body is None: + is_request_body = self._is_request_body() + + if is_databag: + if is_request_body and self.keep_request_bodies: + remaining_depth = float("inf") + remaining_breadth = float("inf") + else: + if remaining_depth is None: + remaining_depth = MAX_DATABAG_DEPTH + if remaining_breadth is None: + remaining_breadth = MAX_DATABAG_BREADTH + + obj = self._flatten_annotated(obj) + + if remaining_depth is not None and remaining_depth <= 0: + self._annotate(rem=[["!limit", "x"]]) + if is_databag: + return self._flatten_annotated( + strip_string( + self._safe_repr_wrapper(obj), max_length=self.max_value_length + ) + ) + return None + + is_span_attribute = self._is_span_attribute() + if (is_databag or is_span_attribute) and global_repr_processors: + hints = {"memo": self.memo, "remaining_depth": remaining_depth} + for processor in global_repr_processors: + result = processor(obj, hints) + if result is not NotImplemented: + return self._flatten_annotated(result) + + sentry_repr = getattr(type(obj), "__sentry_repr__", None) + + if obj is None or isinstance(obj, (bool, int, float)): + if should_repr_strings or ( + isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) + ): + return self._safe_repr_wrapper(obj) + else: + return obj + + elif callable(sentry_repr): + return sentry_repr(obj) + + elif isinstance(obj, datetime): + return ( + str(format_timestamp(obj)) + if not should_repr_strings + else self._safe_repr_wrapper(obj) + ) + + elif isinstance(obj, Mapping): + # Create temporary copy here to avoid calling too much code that + # might mutate our dictionary while we're still iterating over it. + obj = dict(obj.items()) + + rv_dict: "Dict[str, Any]" = {} + i = 0 + + for k, v in obj.items(): + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) + break + + str_k = str(k) + v = self._serialize_node( + v, + segment=str_k, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + rv_dict[str_k] = v + i += 1 + + return rv_dict + + elif not isinstance(obj, serializable_str_types) and isinstance( + obj, tuple(sequence_types) + ): + rv_list = [] + + for i, v in enumerate(obj): # type: ignore[arg-type] + if remaining_breadth is not None and i >= remaining_breadth: + self._annotate(len=len(obj)) # type: ignore[arg-type] + break + + rv_list.append( + self._serialize_node( + v, + segment=i, + should_repr_strings=should_repr_strings, + is_databag=is_databag, + is_request_body=is_request_body, + remaining_depth=( + remaining_depth - 1 if remaining_depth is not None else None + ), + remaining_breadth=remaining_breadth, + ) + ) + + return rv_list + + if should_repr_strings: + obj = self._safe_repr_wrapper(obj) + else: + if isinstance(obj, bytes) or isinstance(obj, bytearray): + obj = obj.decode("utf-8", "replace") + + if not isinstance(obj, str): + obj = self._safe_repr_wrapper(obj) + + is_span_description = ( + len(self.path) == 3 + and self.path[0] == "spans" + and self.path[-1] == "description" + ) + if is_span_description: + return obj + + return self._flatten_annotated( + strip_string(obj, max_length=self.max_value_length) + ) + + +def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": + """ + A very smart serializer that takes a dict and emits a json-friendly dict. + Currently used for serializing the final Event and also prematurely while fetching the stack + local variables for each frame in a stacktrace. + + It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. + The algorithm itself is a recursive graph walk down the data structures it encounters. + + It has the following responsibilities: + * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. + * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. + * Annotating the payload with the _meta field whenever trimming happens. + + :param max_request_body_size: If set to "always", will never trim request bodies. + :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. + :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. + :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. + + """ + serializer = _Serializer( + keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", + max_value_length=kwargs.pop("max_value_length", None), + is_vars=kwargs.pop("is_vars", False), + custom_repr=kwargs.pop("custom_repr", None), + ) + + disable_capture_event.set(True) + try: + serialized_event = serializer._serialize_node(event, **kwargs) + if ( + not serializer.is_vars + and serializer.meta_stack + and isinstance(serialized_event, dict) + ): + serialized_event["_meta"] = serializer.meta_stack[0] + + return serialized_event + finally: + disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py new file mode 100644 index 0000000000..3ffd071bbc --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py @@ -0,0 +1,165 @@ +import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Union + + from sentry_sdk._types import SessionStatus + + +def _minute_trunc(ts: "datetime") -> "datetime": + return ts.replace(second=0, microsecond=0) + + +def _make_uuid( + val: "Union[str, uuid.UUID]", +) -> "uuid.UUID": + if isinstance(val, uuid.UUID): + return val + return uuid.UUID(val) + + +class Session: + def __init__( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + session_mode: str = "application", + ) -> None: + if sid is None: + sid = uuid.uuid4() + if started is None: + started = datetime.now(timezone.utc) + if status is None: + status = "ok" + self.status = status + self.did: "Optional[str]" = None + self.started = started + self.release: "Optional[str]" = None + self.environment: "Optional[str]" = None + self.duration: "Optional[float]" = None + self.user_agent: "Optional[str]" = None + self.ip_address: "Optional[str]" = None + self.session_mode: str = session_mode + self.errors = 0 + + self.update( + sid=sid, + did=did, + timestamp=timestamp, + duration=duration, + release=release, + environment=environment, + user_agent=user_agent, + ip_address=ip_address, + errors=errors, + user=user, + ) + + @property + def truncated_started(self) -> "datetime": + return _minute_trunc(self.started) + + def update( + self, + sid: "Optional[Union[str, uuid.UUID]]" = None, + did: "Optional[str]" = None, + timestamp: "Optional[datetime]" = None, + started: "Optional[datetime]" = None, + duration: "Optional[float]" = None, + status: "Optional[SessionStatus]" = None, + release: "Optional[str]" = None, + environment: "Optional[str]" = None, + user_agent: "Optional[str]" = None, + ip_address: "Optional[str]" = None, + errors: "Optional[int]" = None, + user: "Optional[Any]" = None, + ) -> None: + # If a user is supplied we pull some data form it + if user: + if ip_address is None: + ip_address = user.get("ip_address") + if did is None: + did = user.get("id") or user.get("email") or user.get("username") + + if sid is not None: + self.sid = _make_uuid(sid) + if did is not None: + self.did = str(did) + if timestamp is None: + timestamp = datetime.now(timezone.utc) + self.timestamp = timestamp + if started is not None: + self.started = started + if duration is not None: + self.duration = duration + if release is not None: + self.release = release + if environment is not None: + self.environment = environment + if ip_address is not None: + self.ip_address = ip_address + if user_agent is not None: + self.user_agent = user_agent + if errors is not None: + self.errors = errors + + if status is not None: + self.status = status + + def close( + self, + status: "Optional[SessionStatus]" = None, + ) -> "Any": + if status is None and self.status == "ok": + status = "exited" + if status is not None: + self.update(status=status) + + def get_json_attrs( + self, + with_user_info: "Optional[bool]" = True, + ) -> "Any": + attrs = {} + if self.release is not None: + attrs["release"] = self.release + if self.environment is not None: + attrs["environment"] = self.environment + if with_user_info: + if self.ip_address is not None: + attrs["ip_address"] = self.ip_address + if self.user_agent is not None: + attrs["user_agent"] = self.user_agent + return attrs + + def to_json(self) -> "Any": + rv: "Dict[str, Any]" = { + "sid": str(self.sid), + "init": True, + "started": format_timestamp(self.started), + "timestamp": format_timestamp(self.timestamp), + "status": self.status, + } + if self.errors: + rv["errors"] = self.errors + if self.did is not None: + rv["did"] = self.did + if self.duration is not None: + rv["duration"] = self.duration + attrs = self.get_json_attrs() + if attrs: + rv["attrs"] = attrs + return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py new file mode 100644 index 0000000000..aabf874fcd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py @@ -0,0 +1,262 @@ +import os +import warnings +from contextlib import contextmanager +from threading import Event, Lock, Thread +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.envelope import Envelope +from sentry_sdk.session import Session +from sentry_sdk.utils import format_timestamp + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Generator, List, Optional, Union + + +def is_auto_session_tracking_enabled( + hub: "Optional[sentry_sdk.Hub]" = None, +) -> "Union[Any, bool, None]": + """DEPRECATED: Utility function to find out if session tracking is enabled.""" + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + + should_track = hub.scope._force_auto_session_tracking + + if should_track is None: + client_options = hub.client.options if hub.client else {} + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking( + hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: Use track_session instead + Starts and stops a session automatically around a block. + """ + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "Use track_session instead.", + DeprecationWarning, + stacklevel=2, + ) + + if hub is None: + hub = sentry_sdk.Hub.current + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + should_track = is_auto_session_tracking_enabled(hub) + if should_track: + hub.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + hub.end_session() + + +def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: + """ + DEPRECATED: Utility function to find out if session tracking is enabled. + """ + + warnings.warn( + "This function is deprecated and will be removed in the next major release. " + "There is no public API replacement.", + DeprecationWarning, + stacklevel=2, + ) + + # Internal callers should use private _is_auto_session_tracking_enabled, instead. + return _is_auto_session_tracking_enabled(scope) + + +def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: + """ + Utility function to find out if session tracking is enabled. + """ + + should_track = scope._force_auto_session_tracking + if should_track is None: + client_options = sentry_sdk.get_client().options + should_track = client_options.get("auto_session_tracking", False) + + return should_track + + +@contextmanager +def auto_session_tracking_scope( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """DEPRECATED: This function is a deprecated alias for track_session. + Starts and stops a session automatically around a block. + """ + + warnings.warn( + "This function is a deprecated alias for track_session and will be removed in the next major release.", + DeprecationWarning, + stacklevel=2, + ) + + with track_session(scope, session_mode=session_mode): + yield + + +@contextmanager +def track_session( + scope: "sentry_sdk.Scope", session_mode: str = "application" +) -> "Generator[None, None, None]": + """ + Start a new session in the provided scope, assuming session tracking is enabled. + This is a no-op context manager if session tracking is not enabled. + """ + + should_track = _is_auto_session_tracking_enabled(scope) + if should_track: + scope.start_session(session_mode=session_mode) + try: + yield + finally: + if should_track: + scope.end_session() + + +TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") +MAX_ENVELOPE_ITEMS = 100 + + +def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": + return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} + + +class SessionFlusher: + def __init__( + self, + capture_func: "Callable[[Envelope], None]", + flush_interval: int = 60, + ) -> None: + self.capture_func = capture_func + self.flush_interval = flush_interval + self.pending_sessions: "List[Any]" = [] + self.pending_aggregates: "Dict[Any, Any]" = {} + self._thread: "Optional[Thread]" = None + self._thread_lock = Lock() + self._aggregate_lock = Lock() + self._thread_for_pid: "Optional[int]" = None + self.__shutdown_requested = Event() + + def flush(self) -> None: + pending_sessions = self.pending_sessions + self.pending_sessions = [] + + with self._aggregate_lock: + pending_aggregates = self.pending_aggregates + self.pending_aggregates = {} + + envelope = Envelope() + for session in pending_sessions: + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_session(session) + + for attrs, states in pending_aggregates.items(): + if len(envelope.items) == MAX_ENVELOPE_ITEMS: + self.capture_func(envelope) + envelope = Envelope() + + envelope.add_sessions(make_aggregate_envelope(states, attrs)) + + if len(envelope.items) > 0: + self.capture_func(envelope) + + def _ensure_running(self) -> None: + """ + Check that we have an active thread to run in, or create one if not. + + Note that this might fail (e.g. in Python 3.12 it's not possible to + spawn new threads at interpreter shutdown). In that case self._running + will be False after running this function. + """ + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + with self._thread_lock: + if self._thread_for_pid == os.getpid() and self._thread is not None: + return None + + def _thread() -> None: + running = True + while running: + running = not self.__shutdown_requested.wait(self.flush_interval) + self.flush() + + thread = Thread(target=_thread) + thread.daemon = True + try: + thread.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self.__shutdown_requested.set() + return None + + self._thread = thread + self._thread_for_pid = os.getpid() + + return None + + def add_aggregate_session( + self, + session: "Session", + ) -> None: + # NOTE on `session.did`: + # the protocol can deal with buckets that have a distinct-id, however + # in practice we expect the python SDK to have an extremely high cardinality + # here, effectively making aggregation useless, therefore we do not + # aggregate per-did. + + # For this part we can get away with using the global interpreter lock + with self._aggregate_lock: + attrs = session.get_json_attrs(with_user_info=False) + primary_key = tuple(sorted(attrs.items())) + secondary_key = session.truncated_started # (, session.did) + states = self.pending_aggregates.setdefault(primary_key, {}) + state = states.setdefault(secondary_key, {}) + + if "started" not in state: + state["started"] = format_timestamp(session.truncated_started) + # if session.did is not None: + # state["did"] = session.did + if session.status == "crashed": + state["crashed"] = state.get("crashed", 0) + 1 + elif session.status == "abnormal": + state["abnormal"] = state.get("abnormal", 0) + 1 + elif session.errors > 0: + state["errored"] = state.get("errored", 0) + 1 + else: + state["exited"] = state.get("exited", 0) + 1 + + def add_session( + self, + session: "Session", + ) -> None: + if session.session_mode == "request": + self.add_aggregate_session(session) + else: + self.pending_sessions.append(session.to_json()) + self._ensure_running() + + def kill(self) -> None: + self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py new file mode 100644 index 0000000000..2dcc86bc47 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py @@ -0,0 +1,327 @@ +import io +import logging +import os +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from itertools import chain, product +from typing import TYPE_CHECKING + +import urllib3 + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Self + +from sentry_sdk.envelope import Envelope +from sentry_sdk.utils import ( + capture_internal_exceptions, + env_to_bool, +) +from sentry_sdk.utils import ( + logger as sentry_logger, +) + +logger = logging.getLogger("spotlight") + + +DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" +DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" + + +class SpotlightClient: + """ + A client for sending envelopes to Sentry Spotlight. + + Implements exponential backoff retry logic per the SDK spec: + - Logs error at least once when server is unreachable + - Does not log for every failed envelope + - Uses exponential backoff to avoid hammering an unavailable server + - Never blocks normal Sentry operation + """ + + # Exponential backoff settings + INITIAL_RETRY_DELAY = 1.0 # Start with 1 second + MAX_RETRY_DELAY = 60.0 # Max 60 seconds + + def __init__(self, url: str) -> None: + self.url = url + self.http = urllib3.PoolManager() + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time: float = 0.0 + + def capture_envelope(self, envelope: "Envelope") -> None: + # Check if we're in backoff period - skip sending to avoid blocking + if self._last_error_time > 0: + time_since_error = time.time() - self._last_error_time + if time_since_error < self._retry_delay: + # Still in backoff period, skip this envelope + return + + body = io.BytesIO() + envelope.serialize_into(body) + try: + req = self.http.request( + url=self.url, + body=body.getvalue(), + method="POST", + headers={ + "Content-Type": "application/x-sentry-envelope", + }, + ) + req.close() + # Success - reset backoff state + self._retry_delay = self.INITIAL_RETRY_DELAY + self._last_error_time = 0.0 + except Exception as e: + self._last_error_time = time.time() + + # Increase backoff delay exponentially first, so logged value matches actual wait + self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) + + # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) + sentry_logger.warning( + "Failed to send envelope to Spotlight at %s: %s. " + "Will retry after %.1f seconds.", + self.url, + e, + self._retry_delay, + ) + + +try: + from django.conf import settings + from django.http import HttpRequest, HttpResponse, HttpResponseServerError + from django.utils.deprecation import MiddlewareMixin + + SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" + SPOTLIGHT_JS_SNIPPET_PATTERN = ( + "\n" + '\n' + ) + SPOTLIGHT_ERROR_PAGE_SNIPPET = ( + '\n' + '\n' + ) + CHARSET_PREFIX = "charset=" + BODY_TAG_NAME = "body" + BODY_CLOSE_TAG_POSSIBILITIES = tuple( + "".format("".join(chars)) + for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) + ) + + class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] + _spotlight_script: "Optional[str]" = None + _spotlight_url: "Optional[str]" = None + + def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: + super().__init__(get_response) + + import sentry_sdk.api + + self.sentry_sdk = sentry_sdk.api + + spotlight_client = self.sentry_sdk.get_client().spotlight + if spotlight_client is None: + sentry_logger.warning( + "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." + ) + return None + # Spotlight URL has a trailing `/stream` part at the end so split it off + self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") + + @property + def spotlight_script(self: "Self") -> "Optional[str]": + if self._spotlight_url is not None and self._spotlight_script is None: + try: + spotlight_js_url = urllib.parse.urljoin( + self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH + ) + req = urllib.request.Request( + spotlight_js_url, + method="HEAD", + ) + urllib.request.urlopen(req) + self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( + spotlight_url=self._spotlight_url, + spotlight_js_url=spotlight_js_url, + ) + except urllib.error.URLError as err: + sentry_logger.debug( + "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", + spotlight_js_url, + exc_info=err, + ) + + return self._spotlight_script + + def process_response( + self: "Self", _request: "HttpRequest", response: "HttpResponse" + ) -> "Optional[HttpResponse]": + content_type_header = tuple( + p.strip() + for p in response.headers.get("Content-Type", "").lower().split(";") + ) + content_type = content_type_header[0] + if len(content_type_header) > 1 and content_type_header[1].startswith( + CHARSET_PREFIX + ): + encoding = content_type_header[1][len(CHARSET_PREFIX) :] + else: + encoding = "utf-8" + + if ( + self.spotlight_script is not None + and not response.streaming + and content_type == "text/html" + ): + content_length = len(response.content) + injection = self.spotlight_script.encode(encoding) + injection_site = next( + ( + idx + for idx in ( + response.content.rfind(body_variant.encode(encoding)) + for body_variant in BODY_CLOSE_TAG_POSSIBILITIES + ) + if idx > -1 + ), + content_length, + ) + + # This approach works even when we don't have a `` tag + response.content = ( + response.content[:injection_site] + + injection + + response.content[injection_site:] + ) + + if response.has_header("Content-Length"): + response.headers["Content-Length"] = content_length + len(injection) + + return response + + def process_exception( + self: "Self", _request: "HttpRequest", exception: Exception + ) -> "Optional[HttpResponseServerError]": + if not settings.DEBUG or not self._spotlight_url: + return None + + try: + spotlight = ( + urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") + ) + except urllib.error.URLError: + return None + else: + event_id = self.sentry_sdk.capture_exception(exception) + return HttpResponseServerError( + spotlight.replace( + "", + SPOTLIGHT_ERROR_PAGE_SNIPPET.format( + spotlight_url=self._spotlight_url, event_id=event_id + ), + ) + ) + +except ImportError: + settings = None + + +def _resolve_spotlight_url( + spotlight_config: "Any", sentry_logger: "Any" +) -> "Optional[str]": + """ + Resolve the Spotlight URL based on config and environment variable. + + Implements precedence rules per the SDK spec: + https://develop.sentry.dev/sdk/expected-features/spotlight/ + + Returns the resolved URL string, or None if Spotlight should be disabled. + """ + spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") + + # Parse env var to determine if it's a boolean or URL + spotlight_from_env: "Optional[bool]" = None + spotlight_env_url: "Optional[str]" = None + if spotlight_env_value: + parsed = env_to_bool(spotlight_env_value, strict=True) + if parsed is None: + # It's a URL string + spotlight_from_env = True + spotlight_env_url = spotlight_env_value + else: + spotlight_from_env = parsed + + # Apply precedence rules per spec: + # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules + if spotlight_config is False: + # Config explicitly disables spotlight - warn if env var was set + if spotlight_from_env: + sentry_logger.warning( + "Spotlight is disabled via spotlight=False config option, " + "ignoring SENTRY_SPOTLIGHT environment variable." + ) + return None + elif spotlight_config is True: + # Config enables spotlight with boolean true + # If env var has URL, use env var URL per spec + if spotlight_env_url: + return spotlight_env_url + else: + return DEFAULT_SPOTLIGHT_URL + elif isinstance(spotlight_config, str): + # Config has URL string - use config URL, warn if env var differs + if spotlight_env_value and spotlight_env_value != spotlight_config: + sentry_logger.warning( + "Spotlight URL from config (%s) takes precedence over " + "SENTRY_SPOTLIGHT environment variable (%s).", + spotlight_config, + spotlight_env_value, + ) + return spotlight_config + elif spotlight_config is None: + # No config - use env var + if spotlight_env_url: + return spotlight_env_url + elif spotlight_from_env: + return DEFAULT_SPOTLIGHT_URL + # else: stays None (disabled) + + return None + + +def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": + url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) + + if url is None: + return None + + # Only set up logging handler when spotlight is actually enabled + _handler = logging.StreamHandler(sys.stderr) + _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) + logger.addHandler(_handler) + logger.setLevel(logging.INFO) + + # Update options with resolved URL for consistency + options["spotlight"] = url + + with capture_internal_exceptions(): + if ( + settings is not None + and settings.DEBUG + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) + and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) + ): + middleware = settings.MIDDLEWARE + if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: + settings.MIDDLEWARE = type(middleware)( + chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) + ) + logger.info("Enabled Spotlight integration for Django") + + client = SpotlightClient(url) + logger.info("Enabled Spotlight using sidecar at %s", url) + + return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py new file mode 100644 index 0000000000..5ee7e8460b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py @@ -0,0 +1,843 @@ +""" +EXPERIMENTAL. Do not use in production. + +The API in this file is only meant to be used in span streaming mode. + +You can enable span streaming mode via +sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). +""" + +import sys +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import SPANDATA +from sentry_sdk.profiler.continuous_profiler import ( + get_profiler_id, + try_autostart_continuous_profiler, + try_profile_lifecycle_trace_start, +) +from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.utils import ( + capture_internal_exceptions, + format_attribute, + get_current_thread_meta, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + Iterator, + Optional, + ParamSpec, + TypeVar, + Union, + overload, + ) + + from sentry_sdk._types import Attributes, AttributeValue, SpanJSON + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + + P = ParamSpec("P") + R = TypeVar("R") + + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +class SpanStatus(str, Enum): + OK = "ok" + ERROR = "error" + + def __str__(self) -> str: + return self.value + + +_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) + + +# Segment source, see +# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource +class SegmentSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_SEGMENT_SOURCES = [ + SegmentSource.URL, +] + + +SOURCE_FOR_STYLE = { + "endpoint": SegmentSource.COMPONENT, + "function_name": SegmentSource.COMPONENT, + "handler_name": SegmentSource.COMPONENT, + "method_and_path_pattern": SegmentSource.ROUTE, + "path": SegmentSource.URL, + "route_name": SegmentSource.COMPONENT, + "route_pattern": SegmentSource.ROUTE, + "uri_template": SegmentSource.ROUTE, + "url": SegmentSource.ROUTE, +} + + +# Sentinel value for an unset parent_span to be able to distinguish it from +# a None set by the user +_DEFAULT_PARENT_SPAN = object() + + +def start_span( + name: str, + attributes: "Optional[Attributes]" = None, + parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] + active: bool = True, +) -> "StreamedSpan": + """ + Start a span. + + EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() + instead. + + The span's parent, unless provided explicitly via the `parent_span` argument, + will be the current active span, if any. If there is none, this span will + become the root of a new span tree. If you explicitly want this span to be + top-level without a parent, set `parent_span=None`. + + `start_span()` can either be used as context manager or you can use the span + object it returns and explicitly end it via `span.end()`. The following is + equivalent: + + ```python + import sentry_sdk + + with sentry_sdk.traces.start_span(name="My Span"): + # do something + + # The span automatically finishes once the `with` block is exited + ``` + + ```python + import sentry_sdk + + span = sentry_sdk.traces.start_span(name="My Span") + # do something + span.end() + ``` + + To continue a trace from another service, call + `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. + + :param name: The name to identify this span by. + :type name: str + + :param attributes: Key-value attributes to set on the span from the start. + These will also be accessible in the traces sampler. + :type attributes: "Optional[Attributes]" + + :param parent_span: A span instance that the new span should consider its + parent. If not provided, the parent will be set to the currently active + span, if any. If set to `None`, this span will become a new root-level + span. + :type parent_span: "Optional[StreamedSpan]" + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to `False`. + :type active: bool + + :return: The span that has been started. + :rtype: StreamedSpan + """ + from sentry_sdk.tracing_utils import has_span_streaming_enabled + + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "sentry_sdk.start_transaction() and sentry_sdk.start_span() " + "instead.", + stacklevel=2, + ) + return NoOpStreamedSpan() + + return sentry_sdk.get_current_scope().start_streamed_span( + name, attributes, parent_span, active + ) + + +def continue_trace(incoming: "dict[str, Any]") -> None: + """ + Continue a trace from headers or environment variables. + + EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. + + This function sets the propagation context on the scope. Any span started + in the updated scope will belong under the trace extracted from the + provided propagation headers or environment variables. + + continue_trace() doesn't start any spans on its own. Use the start_span() + API for that. + """ + # This is set both on the isolation and the current scope for compatibility + # reasons. Conceptually, it belongs on the isolation scope, and it also + # used to be set there in non-span-first mode. But in span first mode, we + # start spans on the current scope, regardless of type, like JS does, so we + # need to set the propagation context there. + sentry_sdk.get_isolation_scope().generate_propagation_context( + incoming, + ) + sentry_sdk.get_current_scope().generate_propagation_context( + incoming, + ) + + +def new_trace() -> None: + """ + Resets the propagation context, forcing a new trace. + + EXPERIMENTAL. + + This function sets the propagation context on the scope. Any span started + in the updated scope will start its own trace. + + new_trace() doesn't start any spans on its own. Use the start_span() API + for that. + """ + sentry_sdk.get_isolation_scope().set_new_propagation_context() + sentry_sdk.get_current_scope().set_new_propagation_context() + + +class StreamedSpan: + """ + A span holds timing information of a block of code. + + Spans can have multiple child spans, thus forming a span tree. + + This is the Span First span implementation that streams spans. The original + transaction-based span implementation lives in tracing.Span. + """ + + __slots__ = ( + "_name", + "_attributes", + "_active", + "_span_id", + "_trace_id", + "_parent_span_id", + "_segment", + "_parent_sampled", + "_start_timestamp", + "_start_timestamp_monotonic_ns", + "_end_timestamp", + "_status", + "_scope", + "_previous_span_on_scope", + "_baggage", + "_sample_rand", + "_sample_rate", + "_continuous_profile", + ) + + def __init__( + self, + *, + name: str, + attributes: "Optional[Attributes]" = None, + active: bool = True, + scope: "sentry_sdk.Scope", + segment: "Optional[StreamedSpan]" = None, + trace_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + sample_rate: "Optional[float]" = None, + sample_rand: "Optional[float]" = None, + ): + self._name: str = name + self._active: bool = active + self._attributes: "Attributes" = { + "sentry.origin": "manual", + "sentry.trace_lifecycle": "stream", + } + + if attributes: + for attribute, value in attributes.items(): + self.set_attribute(attribute, value) + + self._scope = scope + + self._segment = segment or self + + self._trace_id: "Optional[str]" = trace_id + self._parent_span_id = parent_span_id + self._parent_sampled = parent_sampled + self._baggage = baggage + self._sample_rand = sample_rand + self._sample_rate = sample_rate + + self._start_timestamp = datetime.now(timezone.utc) + self._end_timestamp: "Optional[datetime]" = None + + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + + self._span_id: "Optional[str]" = None + + self._status = SpanStatus.OK.value + + self._update_active_thread() + + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._start_profile() + self._set_profile_id(get_profiler_id()) + + self._set_segment_attributes() + + self._start() + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__}(" + f"name={self._name}, " + f"trace_id={self.trace_id}, " + f"span_id={self.span_id}, " + f"parent_span_id={self._parent_span_id}, " + f"active={self._active})>" + ) + + def __enter__(self) -> "StreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore + return + + if value is not None and should_be_treated_as_error(ty, value): + self.status = SpanStatus.ERROR.value + + self._end() + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + """ + Finish this span and queue it for sending. + + :param end_timestamp: End timestamp to use instead of current time. + :type end_timestamp: "Optional[Union[float, datetime]]" + """ + self._end(end_timestamp) + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self.end(end_timestamp) + + def _start(self) -> None: + if self._active: + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._end_timestamp is not None: + # This span is already finished, ignore. + return + + # Stop the profiler + if self._is_segment() and self._continuous_profile is not None: + with capture_internal_exceptions(): + self._continuous_profile.stop() + + # Detach from scope + if self._active: + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + # Set attributes from the segment. These are set on span end on purpose + # so that we have the best chance to capture the segment's final name + # (since it might change during its lifetime) + self.set_attribute("sentry.segment.id", self._segment.span_id) + self.set_attribute("sentry.segment.name", self._segment.name) + + # Set the end timestamp + if end_timestamp is not None: + if isinstance(end_timestamp, (float, int)): + try: + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + except Exception: + pass + + if isinstance(end_timestamp, datetime): + self._end_timestamp = end_timestamp + else: + logger.debug( + "[Tracing] Failed to set end_timestamp. Using current time instead." + ) + + if self._end_timestamp is None: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self._end_timestamp = self._start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + + client = sentry_sdk.get_client() + if not client.is_active(): + return + + # Finally, queue the span for sending to Sentry + self._scope._capture_span(self) + + def get_attributes(self) -> "Attributes": + return self._attributes + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + self._attributes[key] = format_attribute(value) + + def set_attributes(self, attributes: "Attributes") -> None: + for key, value in attributes.items(): + self.set_attribute(key, value) + + def remove_attribute(self, key: str) -> None: + try: + del self._attributes[key] + except KeyError: + pass + + @property + def status(self) -> "str": + return self._status + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + if isinstance(status, Enum): + status = status.value + + if status not in _VALID_SPAN_STATUSES: + logger.debug( + f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' + ) + return + + self._status = status + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def active(self) -> bool: + return self._active + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @property + def sampled(self) -> "Optional[bool]": + return True + + @property + def start_timestamp(self) -> "Optional[datetime]": + return self._start_timestamp + + @property + def end_timestamp(self) -> "Optional[datetime]": + return self._end_timestamp + + def _is_segment(self) -> bool: + return self._segment is self + + def _update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + + if thread_id is not None: + self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_attribute(SPANDATA.THREAD_NAME, thread_name) + + def _dynamic_sampling_context(self) -> "dict[str, str]": + return self._segment._get_baggage().dynamic_sampling_context() + + def _to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def _to_baggage(self) -> "Optional[Baggage]": + if self._segment: + return self._segment._get_baggage() + return None + + def _get_baggage(self) -> "Baggage": + """ + Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with + the segment. + + The first time a new baggage with Sentry items is made, it will be frozen. + """ + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_segment(self) + + return self._baggage + + def _iter_headers(self) -> "Iterator[tuple[str, str]]": + if not self._segment: + return + + yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() + + baggage = self._segment._get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def _get_trace_context(self) -> "dict[str, Any]": + # Even if spans themselves are not event-based anymore, we need this + # to populate trace context on events + context: "dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self._parent_span_id, + "dynamic_sampling_context": self._dynamic_sampling_context(), + } + + if "sentry.op" in self._attributes: + context["op"] = self._attributes["sentry.op"] + if "sentry.origin" in self._attributes: + context["origin"] = self._attributes["sentry.origin"] + + return context + + def _set_profile_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_attribute("sentry.profiler_id", profiler_id) + + def _start_profile(self) -> None: + if not self._is_segment(): + return + + try_autostart_continuous_profiler() + + self._continuous_profile = try_profile_lifecycle_trace_start() + + def _set_segment_attributes(self) -> None: + if not self._is_segment(): + return + + client = sentry_sdk.get_client() + + self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") + self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) + self.set_attribute( + SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) + ) + + if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: + self.set_attribute( + SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() + ) + + def _to_json(self) -> "SpanJSON": + res: "SpanJSON" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "name": self._name if self._name is not None else "", + "status": self._status, + "is_segment": self._is_segment(), + "start_timestamp": self._start_timestamp.timestamp(), + } + + if self._end_timestamp: + res["end_timestamp"] = self._end_timestamp.timestamp() + + if self._parent_span_id: + res["parent_span_id"] = self._parent_span_id + + res["attributes"] = {k: v for k, v in self._attributes.items()} + + return res + + +class NoOpStreamedSpan(StreamedSpan): + __slots__ = ( + "_finished", + "_unsampled_reason", + ) + + def __init__( + self, + unsampled_reason: "Optional[str]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + ) -> None: + self._scope = scope # type: ignore[assignment] + self._unsampled_reason = unsampled_reason + + self._finished = False + + self._start() + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(sampled={self.sampled})>" + + def __enter__(self) -> "NoOpStreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + self._end() + + def _start(self) -> None: + if self._scope is None: + return + + old_span = self._scope.streamed_span + self._scope.streamed_span = self + self._previous_span_on_scope = old_span + + def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + if self._finished: + return + + if self._unsampled_reason is not None: + client = sentry_sdk.get_client() + if client.is_active() and client.transport: + logger.debug( + f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" + ) + client.transport.record_lost_event( + reason=self._unsampled_reason, + data_category="span", + quantity=1, + ) + + if self._scope and hasattr(self, "_previous_span_on_scope"): + with capture_internal_exceptions(): + old_span = self._previous_span_on_scope + del self._previous_span_on_scope + self._scope.streamed_span = old_span + + self._finished = True + + def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + self._end() + + def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: + warnings.warn( + "span.finish() is deprecated. Use span.end() instead.", + stacklevel=2, + category=DeprecationWarning, + ) + + self._end() + + def get_attributes(self) -> "Attributes": + return {} + + def set_attribute(self, key: str, value: "AttributeValue") -> None: + pass + + def set_attributes(self, attributes: "Attributes") -> None: + pass + + def remove_attribute(self, key: str) -> None: + pass + + def _is_segment(self) -> bool: + return self._scope is not None + + @property + def status(self) -> "str": + return SpanStatus.OK.value + + @status.setter + def status(self, status: "Union[SpanStatus, str]") -> None: + pass + + @property + def name(self) -> str: + return "" + + @name.setter + def name(self, value: str) -> None: + pass + + @property + def active(self) -> bool: + return True + + @property + def span_id(self) -> str: + return "0000000000000000" + + @property + def trace_id(self) -> str: + return "00000000000000000000000000000000" + + @property + def sampled(self) -> "Optional[bool]": + return False + + @property + def start_timestamp(self) -> "Optional[datetime]": + return None + + @property + def end_timestamp(self) -> "Optional[datetime]": + return None + + +if TYPE_CHECKING: + + @overload + def trace( + func: "Callable[P, R]", + ) -> "Callable[P, R]": ... + + @overload + def trace( + func: None = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a span around a function call. + + EXPERIMENTAL. Use @sentry_sdk.trace instead. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to False. + :type active: bool + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import ( + create_streaming_span_decorator, + ) + + decorator = create_streaming_span_decorator( + name=name, + attributes=attributes, + active=active, + ) + + if func: + return decorator(func) + else: + return decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[StreamedSpan]": + """ + Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. + + This function will only return a non-`None` value when the streaming trace lifecycle is enabled. + To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.streamed_span + return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py new file mode 100644 index 0000000000..1790e13fbf --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py @@ -0,0 +1,1475 @@ +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.profiler.continuous_profiler import get_profiler_id +from sentry_sdk.utils import ( + capture_internal_exceptions, + get_current_thread_meta, + is_valid_sample_rate, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, MutableMapping + from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + overload, + ) + + from typing_extensions import TypedDict, Unpack + + P = ParamSpec("P") + R = TypeVar("R") + + from sentry_sdk._types import ( + Event, + MeasurementUnit, + MeasurementValue, + SamplingContext, + ) + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + from sentry_sdk.profiler.transaction_profiler import Profile + + class SpanKwargs(TypedDict, total=False): + trace_id: str + """ + The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + """ + + span_id: str + """The span ID of this span. If omitted, a new span ID will be generated.""" + + parent_span_id: str + """The span ID of the parent span, if applicable.""" + + same_process_as_parent: bool + """Whether this span is in the same process as the parent span.""" + + sampled: bool + """ + Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + """ + + op: str + """ + The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + """ + + description: str + """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" + + hub: "Optional[sentry_sdk.Hub]" + """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" + + status: str + """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" + + containing_transaction: "Optional[Transaction]" + """The transaction that this span belongs to.""" + + start_timestamp: "Optional[Union[datetime, float]]" + """ + The timestamp when the span started. If omitted, the current time + will be used. + """ + + scope: "sentry_sdk.Scope" + """The scope to use for this span. If not provided, we use the current scope.""" + + origin: str + """ + The origin of the span. + See https://develop.sentry.dev/sdk/performance/trace-origin/ + Default "manual". + """ + + name: str + """A string describing what operation is being performed within the span/transaction.""" + + class TransactionKwargs(SpanKwargs, total=False): + source: str + """ + A string describing the source of the transaction name. This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. + Default "custom". + """ + + parent_sampled: bool + """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" + + baggage: "Baggage" + """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" + + ProfileContext = TypedDict( + "ProfileContext", + { + "profiler_id": str, + }, + ) + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +# Transaction source +# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations +class TransactionSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_TRANSACTION_SOURCES = [ + TransactionSource.URL, +] + +SOURCE_FOR_STYLE = { + "endpoint": TransactionSource.COMPONENT, + "function_name": TransactionSource.COMPONENT, + "handler_name": TransactionSource.COMPONENT, + "method_and_path_pattern": TransactionSource.ROUTE, + "path": TransactionSource.URL, + "route_name": TransactionSource.COMPONENT, + "route_pattern": TransactionSource.ROUTE, + "uri_template": TransactionSource.ROUTE, + "url": TransactionSource.ROUTE, +} + + +def get_span_status_from_http_code(http_status_code: int) -> str: + """ + Returns the Sentry status corresponding to the given HTTP status code. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + if http_status_code < 400: + return SPANSTATUS.OK + + elif 400 <= http_status_code < 500: + if http_status_code == 403: + return SPANSTATUS.PERMISSION_DENIED + elif http_status_code == 404: + return SPANSTATUS.NOT_FOUND + elif http_status_code == 429: + return SPANSTATUS.RESOURCE_EXHAUSTED + elif http_status_code == 413: + return SPANSTATUS.FAILED_PRECONDITION + elif http_status_code == 401: + return SPANSTATUS.UNAUTHENTICATED + elif http_status_code == 409: + return SPANSTATUS.ALREADY_EXISTS + else: + return SPANSTATUS.INVALID_ARGUMENT + + elif 500 <= http_status_code < 600: + if http_status_code == 504: + return SPANSTATUS.DEADLINE_EXCEEDED + elif http_status_code == 501: + return SPANSTATUS.UNIMPLEMENTED + elif http_status_code == 503: + return SPANSTATUS.UNAVAILABLE + else: + return SPANSTATUS.INTERNAL_ERROR + + return SPANSTATUS.UNKNOWN_ERROR + + +class _SpanRecorder: + """Limits the number of spans recorded in a transaction.""" + + __slots__ = ("maxlen", "spans", "dropped_spans") + + def __init__(self, maxlen: int) -> None: + # FIXME: this is `maxlen - 1` only to preserve historical behavior + # enforced by tests. + # Either this should be changed to `maxlen` or the JS SDK implementation + # should be changed to match a consistent interpretation of what maxlen + # limits: either transaction+spans or only child spans. + self.maxlen = maxlen - 1 + self.spans: "List[Span]" = [] + self.dropped_spans: int = 0 + + def add(self, span: "Span") -> None: + if len(self.spans) > self.maxlen: + span._span_recorder = None + self.dropped_spans += 1 + else: + self.spans.append(span) + + +class Span: + """A span holds timing information of a block of code. + Spans can have multiple child spans thus forming a span tree. + + :param trace_id: The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + :param span_id: The span ID of this span. If omitted, a new span ID will be generated. + :param parent_span_id: The span ID of the parent span, if applicable. + :param same_process_as_parent: Whether this span is in the same process as the parent span. + :param sampled: Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + :param op: The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + :param description: A description of what operation is being performed within the span. + + .. deprecated:: 2.15.0 + Please use the `name` parameter, instead. + :param name: A string describing what operation is being performed within the span. + :param hub: The hub to use for this span. + + .. deprecated:: 2.0.0 + Please use the `scope` parameter, instead. + :param status: The span's status. Possible values are listed at + https://develop.sentry.dev/sdk/event-payloads/span/ + :param containing_transaction: The transaction that this span belongs to. + :param start_timestamp: The timestamp when the span started. If omitted, the current time + will be used. + :param scope: The scope to use for this span. If not provided, we use the current scope. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "same_process_as_parent", + "sampled", + "op", + "description", + "_measurements", + "start_timestamp", + "_start_timestamp_monotonic_ns", + "status", + "timestamp", + "_tags", + "_data", + "_span_recorder", + "hub", + "_context_manager_state", + "_containing_transaction", + "scope", + "origin", + "name", + "_flags", + "_flags_capacity", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + same_process_as_parent: bool = True, + sampled: "Optional[bool]" = None, + op: "Optional[str]" = None, + description: "Optional[str]" = None, + hub: "Optional[sentry_sdk.Hub]" = None, # deprecated + status: "Optional[str]" = None, + containing_transaction: "Optional[Transaction]" = None, + start_timestamp: "Optional[Union[datetime, float]]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, + origin: str = "manual", + name: "Optional[str]" = None, + ) -> None: + self._trace_id = trace_id + self._span_id = span_id + self.parent_span_id = parent_span_id + self.same_process_as_parent = same_process_as_parent + self.sampled = sampled + self.op = op + self.description = name or description + self.status = status + self.hub = hub # backwards compatibility + self.scope = scope + self.origin = origin + self._measurements: "Dict[str, MeasurementValue]" = {} + self._tags: "MutableMapping[str, str]" = {} + self._data: "Dict[str, Any]" = {} + self._containing_transaction = containing_transaction + self._flags: "Dict[str, bool]" = {} + self._flags_capacity = 10 + + if hub is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use `scope` instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.scope = self.scope or hub.scope + + if start_timestamp is None: + start_timestamp = datetime.now(timezone.utc) + elif isinstance(start_timestamp, float): + start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) + self.start_timestamp = start_timestamp + try: + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + except AttributeError: + pass + + #: End timestamp of span + self.timestamp: "Optional[datetime]" = None + + self._span_recorder: "Optional[_SpanRecorder]" = None + + self.update_active_thread() + self.set_profiler_id(get_profiler_id()) + + # TODO this should really live on the Transaction class rather than the Span + # class + def init_span_recorder(self, maxlen: int) -> None: + if self._span_recorder is None: + self._span_recorder = _SpanRecorder(maxlen) + + @property + def trace_id(self) -> str: + if not self._trace_id: + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + def __repr__(self) -> str: + return ( + "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.op, + self.description, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.origin, + ) + ) + + def __enter__(self) -> "Span": + scope = self.scope or sentry_sdk.get_current_scope() + old_span = scope.span + scope.span = self + self._context_manager_state = (scope, old_span) + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if value is not None and should_be_treated_as_error(ty, value): + self.set_status(SPANSTATUS.INTERNAL_ERROR) + + with capture_internal_exceptions(): + scope, old_span = self._context_manager_state + del self._context_manager_state + self.finish(scope) + scope.span = old_span + + @property + def containing_transaction(self) -> "Optional[Transaction]": + """The ``Transaction`` that this span belongs to. + The ``Transaction`` is the root of the span tree, + so one could also think of this ``Transaction`` as the "root span".""" + + # this is a getter rather than a regular attribute so that transactions + # can return `self` here instead (as a way to prevent them circularly + # referencing themselves) + return self._containing_transaction + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "Span": + """ + Start a sub-span from the current span or transaction. + + Takes the same arguments as the initializer of :py:class:`Span`. The + trace id, sampling decision, transaction pointer, and span recorder are + inherited from the current span/transaction. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + kwargs.setdefault("sampled", self.sampled) + + child = Span( + trace_id=self.trace_id, + parent_span_id=self.span_id, + containing_transaction=self.containing_transaction, + **kwargs, + ) + + span_recorder = ( + self.containing_transaction and self.containing_transaction._span_recorder + ) + if span_recorder: + span_recorder.add(child) + + return child + + @classmethod + def continue_from_environ( + cls, + environ: "Mapping[str, str]", + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a Transaction with the given params, then add in data pulled from + the ``sentry-trace`` and ``baggage`` headers from the environ (if any) + before returning the Transaction. + + This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` + in that it assumes header names in the form ``HTTP_HEADER_NAME`` - + such as you would get from a WSGI/ASGI environ - + rather than the form ``header-name``. + + :param environ: The ASGI/WSGI environ to pull information from. + """ + return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) + + @classmethod + def continue_from_headers( + cls, + headers: "Mapping[str, str]", + *, + _sample_rand: "Optional[str]" = None, + **kwargs: "Any", + ) -> "Transaction": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a transaction with the given params (including any data pulled from + the ``sentry-trace`` and ``baggage`` headers). + + :param headers: The dictionary with the HTTP headers to pull information from. + :param _sample_rand: If provided, we override the sample_rand value from the + incoming headers with this value. (internal use only) + """ + logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") + + # TODO-neel move away from this kwargs stuff, it's confusing and opaque + # make more explicit + baggage = Baggage.from_incoming_header( + headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand + ) + kwargs.update({BAGGAGE_HEADER_NAME: baggage}) + + sentrytrace_kwargs = extract_sentrytrace_data( + headers.get(SENTRY_TRACE_HEADER_NAME) + ) + + if sentrytrace_kwargs is not None: + kwargs.update(sentrytrace_kwargs) + + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and immutable and won't be populated as head SDK. + baggage.freeze() + + transaction = Transaction(**kwargs) + transaction.same_process_as_parent = False + + return transaction + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. + If the span's containing transaction doesn't yet have a ``baggage`` value, + this will cause one to be generated and stored. + """ + if not self.containing_transaction: + # Do not propagate headers if there is no containing transaction. Otherwise, this + # span ends up being the root span of a new trace, and since it does not get sent + # to Sentry, the trace will be missing a root transaction. The dynamic sampling + # context will also be missing, breaking dynamic sampling & traces. + return + + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.containing_transaction.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + @classmethod + def from_traceparent( + cls, + traceparent: "Optional[str]", + **kwargs: "Any", + ) -> "Optional[Transaction]": + """ + DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. + + Create a ``Transaction`` with the given params, then add in data pulled from + the given ``sentry-trace`` header value before returning the ``Transaction``. + """ + if not traceparent: + return None + + return cls.continue_from_headers( + {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs + ) + + def to_traceparent(self) -> str: + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def to_baggage(self) -> "Optional[Baggage]": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with this ``Span``, if any. (Taken from the root of the span tree.) + """ + if self.containing_transaction: + return self.containing_transaction.get_baggage() + return None + + def set_tag(self, key: str, value: "Any") -> None: + self._tags[key] = value + + def set_data(self, key: str, value: "Any") -> None: + self._data[key] = value + + def update_data(self, data: "Dict[str, Any]") -> None: + self._data.update(data) + + def set_flag(self, flag: str, result: bool) -> None: + if len(self._flags) < self._flags_capacity: + self._flags[flag] = result + + def set_status(self, value: str) -> None: + self.status = value + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_thread( + self, thread_id: "Optional[int]", thread_name: "Optional[str]" + ) -> None: + if thread_id is not None: + self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_data(SPANDATA.THREAD_NAME, thread_name) + + def set_profiler_id(self, profiler_id: "Optional[str]") -> None: + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) + + def set_http_status(self, http_status: int) -> None: + self.set_tag( + "http.status_code", str(http_status) + ) # TODO-neel remove in major, we keep this for backwards compatibility + self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) + self.set_status(get_span_status_from_http_code(http_status)) + + def is_success(self) -> bool: + return self.status == "ok" + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + ) -> "Optional[str]": + """ + Sets the end timestamp of the span. + + Additionally it also creates a breadcrumb from the span, + if the span represents a database or HTTP request. + + :param scope: The scope to use for this transaction. + If not provided, the current scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + + :return: Always ``None``. The type is ``Optional[str]`` to match + the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. + """ + if self.timestamp is not None: + # This span is already finished, ignore. + return None + + try: + if end_timestamp: + if isinstance(end_timestamp, float): + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + self.timestamp = end_timestamp + else: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self.timestamp = self.start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + except AttributeError: + self.timestamp = datetime.now(timezone.utc) + + scope = scope or sentry_sdk.get_current_scope() + + # Copy conversation_id from scope to span data if this is an AI span + conversation_id = scope.get_conversation_id() + if conversation_id: + has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data + is_ai_span_op = self.op is not None and ( + self.op.startswith("ai.") or self.op.startswith("gen_ai.") + ) + if has_ai_op or is_ai_span_op: + self.set_data("gen_ai.conversation.id", conversation_id) + + maybe_create_breadcrumbs_from_span(scope, self) + + return None + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the span.""" + + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "same_process_as_parent": self.same_process_as_parent, + "op": self.op, + "description": self.description, + "start_timestamp": self.start_timestamp, + "timestamp": self.timestamp, + "origin": self.origin, + } + + if self.status: + rv["status"] = self.status + # TODO-neel remove redundant tag in major + self._tags["status"] = self.status + + if len(self._measurements) > 0: + rv["measurements"] = self._measurements + + tags = self._tags + if tags: + rv["tags"] = tags + + data = {} + data.update(self._flags) + data.update(self._data) + if data: + rv["data"] = data + + return rv + + def get_trace_context(self) -> "Any": + rv: "Dict[str, Any]" = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "op": self.op, + "description": self.description, + "origin": self.origin, + } + if self.status: + rv["status"] = self.status + + if self.containing_transaction: + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) + + data = {} + + thread_id = self._data.get(SPANDATA.THREAD_ID) + if thread_id is not None: + data["thread.id"] = thread_id + + thread_name = self._data.get(SPANDATA.THREAD_NAME) + if thread_name is not None: + data["thread.name"] = thread_name + + if data: + rv["data"] = data + + return rv + + def get_profile_context(self) -> "Optional[ProfileContext]": + profiler_id = self._data.get(SPANDATA.PROFILER_ID) + if profiler_id is None: + return None + + return { + "profiler_id": profiler_id, + } + + def update_active_thread(self) -> None: + thread_id, thread_name = get_current_thread_meta() + self.set_thread(thread_id, thread_name) + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +class Transaction(Span): + """The Transaction is the root element that holds all the spans + for Sentry performance instrumentation. + + :param name: Identifier of the transaction. + Will show up in the Sentry UI. + :param parent_sampled: Whether the parent transaction was sampled. + If True this transaction will be kept, if False it will be discarded. + :param baggage: The W3C baggage header value. + (see https://www.w3.org/TR/baggage/) + :param source: A string describing the source of the transaction name. + This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations + for more information. Default "custom". + :param kwargs: Additional arguments to be passed to the Span constructor. + See :py:class:`sentry_sdk.tracing.Span` for available arguments. + """ + + __slots__ = ( + "name", + "source", + "parent_sampled", + # used to create baggage value for head SDKs in dynamic sampling + "sample_rate", + "_measurements", + "_contexts", + "_profile", + "_continuous_profile", + "_baggage", + "_sample_rand", + ) + + def __init__( # type: ignore[misc] + self, + name: str = "", + parent_sampled: "Optional[bool]" = None, + baggage: "Optional[Baggage]" = None, + source: str = TransactionSource.CUSTOM, + **kwargs: "Unpack[SpanKwargs]", + ) -> None: + super().__init__(**kwargs) + + self.name = name + self.source = source + self.sample_rate: "Optional[float]" = None + self.parent_sampled = parent_sampled + self._measurements: "Dict[str, MeasurementValue]" = {} + self._contexts: "Dict[str, Any]" = {} + self._profile: "Optional[Profile]" = None + self._continuous_profile: "Optional[ContinuousProfile]" = None + self._baggage = baggage + + baggage_sample_rand = ( + None if self._baggage is None else self._baggage._sample_rand() + ) + if baggage_sample_rand is not None: + self._sample_rand = baggage_sample_rand + else: + self._sample_rand = _generate_sample_rand(self.trace_id) + + def __repr__(self) -> str: + return ( + "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.name, + self.op, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.source, + self.origin, + ) + ) + + def _possibly_started(self) -> bool: + """Returns whether the transaction might have been started. + + If this returns False, we know that the transaction was not started + with sentry_sdk.start_transaction, and therefore the transaction will + be discarded. + """ + + # We must explicitly check self.sampled is False since self.sampled can be None + return self._span_recorder is not None or self.sampled is False + + def __enter__(self) -> "Transaction": + if not self._possibly_started(): + logger.debug( + "Transaction was entered without being started with sentry_sdk.start_transaction." + "The transaction will not be sent to Sentry. To fix, start the transaction by" + "passing it to sentry_sdk.start_transaction." + ) + + super().__enter__() + + if self._profile is not None: + self._profile.__enter__() + + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + if self._profile is not None: + self._profile.__exit__(ty, value, tb) + + if self._continuous_profile is not None: + self._continuous_profile.stop() + + super().__exit__(ty, value, tb) + + @property + def containing_transaction(self) -> "Transaction": + """The root element of the span tree. + In the case of a transaction it is the transaction itself. + """ + + # Transactions (as spans) belong to themselves (as transactions). This + # is a getter rather than a regular attribute to avoid having a circular + # reference. + return self + + def _get_scope_from_finish_args( + self, + scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", + ) -> "Optional[sentry_sdk.Scope]": + """ + Logic to get the scope from the arguments passed to finish. This + function exists for backwards compatibility with the old finish. + + TODO: Remove this function in the next major version. + """ + scope_or_hub = scope_arg + if hub_arg is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", + DeprecationWarning, + stacklevel=3, + ) + + scope_or_hub = hub_arg + + if isinstance(scope_or_hub, sentry_sdk.Hub): + warnings.warn( + "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", + DeprecationWarning, + stacklevel=3, + ) + + return scope_or_hub.scope + + return scope_or_hub + + def _get_log_representation(self) -> str: + return "{op}transaction <{name}>".format( + op=("<" + self.op + "> " if self.op else ""), name=self.name + ) + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """Finishes the transaction and sends it to Sentry. + All finished spans in the transaction will also be sent to Sentry. + + :param scope: The Scope to use for this transaction. + If not provided, the current Scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + :param hub: The hub to use for this transaction. + This argument is DEPRECATED. Please use the `scope` + parameter, instead. + + :return: The event ID if the transaction was sent to Sentry, + otherwise None. + """ + if self.timestamp is not None: + # This transaction is already finished, ignore. + return None + + # For backwards compatibility, we must handle the case where `scope` + # or `hub` could both either be a `Scope` or a `Hub`. + scope = self._get_scope_from_finish_args(scope, hub) + + scope = scope or self.scope or sentry_sdk.get_current_scope() + client = sentry_sdk.get_client() + + if not client.is_active(): + # We have no active client and therefore nowhere to send this transaction. + return None + + if self._span_recorder is None: + # Explicit check against False needed because self.sampled might be None + if self.sampled is False: + logger.debug("Discarding transaction because sampled = False") + else: + logger.debug( + "Discarding transaction because it was not started with sentry_sdk.start_transaction" + ) + + # This is not entirely accurate because discards here are not + # exclusively based on sample rate but also traces sampler, but + # we handle this the same here. + if client.transport and has_tracing_enabled(client.options): + if client.monitor and client.monitor.downsample_factor > 0: + reason = "backpressure" + else: + reason = "sample_rate" + + client.transport.record_lost_event(reason, data_category="transaction") + + # Only one span (the transaction itself) is discarded, since we did not record any spans here. + client.transport.record_lost_event(reason, data_category="span") + return None + + if not self.name: + logger.warning( + "Transaction has no name, falling back to ``." + ) + self.name = "" + + super().finish(scope, end_timestamp) + + status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) + if ( + status_code is not None + and status_code in client.options["trace_ignore_status_codes"] + ): + logger.debug( + "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( + transaction_description=self._get_log_representation(), + status_code=self._data[SPANDATA.HTTP_STATUS_CODE], + trace_ignore_status_codes=client.options[ + "trace_ignore_status_codes" + ], + ) + ) + if client.transport: + client.transport.record_lost_event( + "event_processor", data_category="transaction" + ) + + num_spans = len(self._span_recorder.spans) + 1 + client.transport.record_lost_event( + "event_processor", data_category="span", quantity=num_spans + ) + + self.sampled = False + + if not self.sampled: + # At this point a `sampled = None` should have already been resolved + # to a concrete decision. + if self.sampled is None: + logger.warning("Discarding transaction without sampling decision.") + + return None + + finished_spans = [] + has_gen_ai_span = False + if client.options.get("stream_gen_ai_spans", True): + for span in self._span_recorder.spans: + if span.timestamp is None: + continue + + if isinstance(span.op, str) and span.op.startswith("gen_ai."): + has_gen_ai_span = True + + finished_spans.append(span.to_json()) + else: + finished_spans = [ + span.to_json() + for span in self._span_recorder.spans + if span.timestamp is not None + ] + + len_diff = len(self._span_recorder.spans) - len(finished_spans) + dropped_spans = len_diff + self._span_recorder.dropped_spans + + # we do this to break the circular reference of transaction -> span + # recorder -> span -> containing transaction (which is where we started) + # before either the spans or the transaction goes out of scope and has + # to be garbage collected + self._span_recorder = None + + contexts = {} + contexts.update(self._contexts) + contexts.update({"trace": self.get_trace_context()}) + profile_context = self.get_profile_context() + if profile_context is not None: + contexts.update({"profile": profile_context}) + + event: "Event" = { + "type": "transaction", + "transaction": self.name, + "transaction_info": {"source": self.source}, + "contexts": contexts, + "tags": self._tags, + "timestamp": self.timestamp, + "start_timestamp": self.start_timestamp, + "spans": finished_spans, + } + + if dropped_spans > 0: + event["_dropped_spans"] = dropped_spans + + if has_gen_ai_span: + event["_has_gen_ai_span"] = True + + if self._profile is not None and self._profile.valid(): + event["profile"] = self._profile + self._profile = None + + event["measurements"] = self._measurements + + return scope.capture_event(event) + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + """ + .. deprecated:: 2.28.0 + This function is deprecated and will be removed in the next major release. + """ + + warnings.warn( + "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", + DeprecationWarning, + stacklevel=2, + ) + self._measurements[name] = {"value": value, "unit": unit} + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + """Sets a context. Transactions can have multiple contexts + and they should follow the format described in the "Contexts Interface" + documentation. + + :param key: The name of the context. + :param value: The information about the context. + """ + self._contexts[key] = value + + def set_http_status(self, http_status: int) -> None: + """Sets the status of the Transaction according to the given HTTP status. + + :param http_status: The HTTP status code.""" + super().set_http_status(http_status) + self.set_context("response", {"status_code": http_status}) + + def to_json(self) -> "Dict[str, Any]": + """Returns a JSON-compatible representation of the transaction.""" + rv = super().to_json() + + rv["name"] = self.name + rv["source"] = self.source + rv["sampled"] = self.sampled + + return rv + + def get_trace_context(self) -> "Any": + trace_context = super().get_trace_context() + + if self._data: + trace_context["data"] = self._data + + return trace_context + + def get_baggage(self) -> "Baggage": + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with the Transaction. + + The first time a new baggage with Sentry items is made, + it will be frozen.""" + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_transaction(self) + + return self._baggage + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + """ + Sets the transaction's sampling decision, according to the following + precedence rules: + + 1. If a sampling decision is passed to `start_transaction` + (`start_transaction(name: "my transaction", sampled: True)`), that + decision will be used, regardless of anything else + + 2. If `traces_sampler` is defined, its decision will be used. It can + choose to keep or ignore any parent sampling decision, or use the + sampling context data to make its own decision or to choose a sample + rate for the transaction. + + 3. If `traces_sampler` is not defined, but there's a parent sampling + decision, the parent sampling decision will be used. + + 4. If `traces_sampler` is not defined and there's no parent sampling + decision, `traces_sample_rate` will be used. + """ + client = sentry_sdk.get_client() + + transaction_description = self._get_log_representation() + + # nothing to do if tracing is disabled + if not has_tracing_enabled(client.options): + self.sampled = False + return + + # if the user has forced a sampling decision by passing a `sampled` + # value when starting the transaction, go with that + if self.sampled is not None: + self.sample_rate = float(self.sampled) + return + + # we would have bailed already if neither `traces_sampler` nor + # `traces_sample_rate` were defined, so one of these should work; prefer + # the hook if so + sample_rate = ( + client.options["traces_sampler"](sampling_context) + if callable(client.options.get("traces_sampler")) + # default inheritance behavior + else ( + sampling_context["parent_sampled"] + if sampling_context["parent_sampled"] is not None + else client.options["traces_sample_rate"] + ) + ) + + # Since this is coming from the user (or from a function provided by the + # user), who knows what we might get. (The only valid values are + # booleans or numbers between 0 and 1.) + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning( + "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( + transaction_description=transaction_description, + ) + ) + self.sampled = False + return + + self.sample_rate = float(sample_rate) + + if client.monitor: + self.sample_rate /= 2**client.monitor.downsample_factor + + # if the function returned 0 (or false), or if `traces_sample_rate` is + # 0, it's a sign the transaction should be dropped + if not self.sample_rate: + logger.debug( + "[Tracing] Discarding {transaction_description} because {reason}".format( + transaction_description=transaction_description, + reason=( + "traces_sampler returned 0 or False" + if callable(client.options.get("traces_sampler")) + else "traces_sample_rate is set to 0" + ), + ) + ) + self.sampled = False + return + + # Now we roll the dice. + self.sampled = self._sample_rand < self.sample_rate + + if self.sampled: + logger.debug( + "[Tracing] Starting {transaction_description}".format( + transaction_description=transaction_description, + ) + ) + else: + logger.debug( + "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( + transaction_description=transaction_description, + sample_rate=self.sample_rate, + ) + ) + + # Private aliases matching StreamedSpan's private API + _get_baggage = get_baggage + _get_trace_context = get_trace_context + + +class NoOpSpan(Span): + def __repr__(self) -> str: + return "<%s>" % self.__class__.__name__ + + @property + def containing_transaction(self) -> "Optional[Transaction]": + return None + + def start_child( + self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" + ) -> "NoOpSpan": + return NoOpSpan() + + def to_traceparent(self) -> str: + return "" + + def to_baggage(self) -> "Optional[Baggage]": + return None + + def get_baggage(self) -> "Optional[Baggage]": + return None + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + return iter(()) + + def set_tag(self, key: str, value: "Any") -> None: + pass + + def set_data(self, key: str, value: "Any") -> None: + pass + + def update_data(self, data: "Dict[str, Any]") -> None: + pass + + def set_status(self, value: str) -> None: + pass + + def set_http_status(self, http_status: int) -> None: + pass + + def is_success(self) -> bool: + return True + + def to_json(self) -> "Dict[str, Any]": + return {} + + def get_trace_context(self) -> "Any": + return {} + + def get_profile_context(self) -> "Any": + return {} + + def finish( + self, + scope: "Optional[sentry_sdk.Scope]" = None, + end_timestamp: "Optional[Union[float, datetime]]" = None, + *, + hub: "Optional[sentry_sdk.Hub]" = None, + ) -> "Optional[str]": + """ + The `hub` parameter is deprecated. Please use the `scope` parameter, instead. + """ + pass + + def set_measurement( + self, name: str, value: float, unit: "MeasurementUnit" = "" + ) -> None: + pass + + def set_context(self, key: str, value: "dict[str, Any]") -> None: + pass + + def init_span_recorder(self, maxlen: int) -> None: + pass + + def _set_initial_sampling_decision( + self, sampling_context: "SamplingContext" + ) -> None: + pass + + # Private aliases matching StreamedSpan's private API + _to_traceparent = to_traceparent + _to_baggage = to_baggage + _get_baggage = get_baggage + _iter_headers = iter_headers + _get_trace_context = get_trace_context + + +if TYPE_CHECKING: + + @overload + def trace( + func: None = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, + ) -> "Callable[[Callable[P, R]], Callable[P, R]]": + # Handles: @trace() and @trace(op="custom") + pass + + @overload + def trace(func: "Callable[P, R]") -> "Callable[P, R]": + # Handles: @trace + pass + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + op: "Optional[str]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a child span around a function call. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, a default operation will + be assigned based on the template. + :type op: str or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP, SPANTEMPLATE + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + op=OP.DB_QUERY, + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + + # With a custom template + @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) + def calculate_interest_rate(amount, rate, years): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import create_span_decorator + + decorator = create_span_decorator( + op=op, + name=name, + attributes=attributes, + template=template, + ) + + if func: + return decorator(func) + else: + return decorator + + +# Circular imports + +from sentry_sdk.tracing_utils import ( + Baggage, + EnvironHeaders, + _generate_sample_rand, + extract_sentrytrace_data, + has_tracing_enabled, + maybe_create_breadcrumbs_from_span, +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py new file mode 100644 index 0000000000..c2e34a795b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py @@ -0,0 +1,1694 @@ +import contextlib +import functools +import inspect +import os +import re +import sys +import uuid +import warnings +from collections.abc import Mapping, MutableMapping +from datetime import datetime, timedelta, timezone +from random import Random +from urllib.parse import quote, unquote + +try: + from re import Pattern +except ImportError: + # 3.6 + from typing import Pattern + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.utils import ( + _is_external_source, + _is_in_project_root, + _module_in_list, + capture_internal_exceptions, + filename_for_module, + is_sentry_url, + is_valid_sample_rate, + logger, + match_regex_list, + qualname_from_function, + safe_repr, + to_string, + try_convert, +) + +if TYPE_CHECKING: + from types import FrameType + from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union + + from sentry_sdk._types import Attributes + + +SENTRY_TRACE_REGEX = re.compile( + "^[ \t]*" # whitespace + "([0-9a-f]{32})?" # trace_id + "-?([0-9a-f]{16})?" # span_id + "-?([01])?" # sampled + "[ \t]*$" # whitespace +) + + +# This is a normal base64 regex, modified to reflect that fact that we strip the +# trailing = or == off +base64_stripped = ( + # any of the characters in the base64 "alphabet", in multiples of 4 + "([a-zA-Z0-9+/]{4})*" + # either nothing or 2 or 3 base64-alphabet characters (see + # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for + # why there's never only 1 extra character) + "([a-zA-Z0-9+/]{2,3})?" +) + + +class EnvironHeaders(Mapping): # type: ignore + def __init__( + self, + environ: "Mapping[str, str]", + prefix: str = "HTTP_", + ) -> None: + self.environ = environ + self.prefix = prefix + + def __getitem__(self, key: str) -> "Optional[Any]": + return self.environ[self.prefix + key.replace("-", "_").upper()] + + def __len__(self) -> int: + return sum(1 for _ in iter(self)) + + def __iter__(self) -> "Generator[str, None, None]": + for k in self.environ: + if not isinstance(k, str): + continue + + k = k.replace("-", "_").upper() + if not k.startswith(self.prefix): + continue + + yield k[len(self.prefix) :] + + +def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: + """ + Returns True if either traces_sample_rate or traces_sampler is + defined and enable_tracing is set and not false. + """ + if options is None: + return False + + return bool( + options.get("enable_tracing") is not False + and ( + options.get("traces_sample_rate") is not None + or options.get("traces_sampler") is not None + ) + ) + + +def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return False + + return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" + + +def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: + if options is None: + return True + + return not options.get( + "stream_gen_ai_spans", True + ) and not has_span_streaming_enabled(options) + + +@contextlib.contextmanager +def record_sql_queries( + cursor: "Any", + query: "Any", + params_list: "Any", + paramstyle: "Optional[str]", + executemany: bool, + record_cursor_repr: bool = False, + span_origin: str = "manual", + span_op_override_value: "Optional[str]" = None, +) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": + # TODO: Bring back capturing of params by default + client = sentry_sdk.get_client() + if client.options["_experiments"].get("record_sql_params", False): + if not params_list or params_list == [None]: + params_list = None + + if paramstyle == "pyformat": + paramstyle = "format" + else: + params_list = None + paramstyle = None + + query = _format_sql(cursor, query) + + data = {} + if params_list is not None: + data["db.params"] = params_list + if paramstyle is not None: + data["db.paramstyle"] = paramstyle + if executemany: + data["db.executemany"] = True + if record_cursor_repr and cursor is not None: + data["db.cursor"] = cursor + + with capture_internal_exceptions(): + sentry_sdk.add_breadcrumb(message=query, category="query", data=data) + + if has_span_streaming_enabled(client.options): + additional_attributes = {} + if query is not None: + additional_attributes["db.query.text"] = query + + with sentry_sdk.traces.start_span( + name="" if query is None else query, + attributes={ + "sentry.origin": span_origin, + "sentry.op": span_op_override_value + if span_op_override_value + else OP.DB, + **additional_attributes, + }, + ) as span: + yield span + else: + with sentry_sdk.start_span( + op=span_op_override_value if span_op_override_value is not None else OP.DB, + name=query, + origin=span_origin, + ) as span: + for k, v in data.items(): + span.set_data(k, v) + yield span + + +def maybe_create_breadcrumbs_from_span( + scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" +) -> None: + if span.op == OP.DB_REDIS: + scope.add_breadcrumb( + message=span.description, type="redis", category="redis", data=span._tags + ) + + elif span.op == OP.HTTP_CLIENT: + level = None + status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) + if status_code: + if 500 <= status_code <= 599: + level = "error" + elif 400 <= status_code <= 499: + level = "warning" + + if level: + scope.add_breadcrumb( + type="http", category="httplib", data=span._data, level=level + ) + else: + scope.add_breadcrumb(type="http", category="httplib", data=span._data) + + elif span.op == "subprocess": + scope.add_breadcrumb( + type="subprocess", + category="subprocess", + message=span.description, + data=span._data, + ) + + +def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": + try: + return frame.f_code.co_filename + except Exception: + return None + + +def _should_be_included( + is_sentry_sdk_frame: bool, + namespace: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", + abs_path: "Optional[str]", + project_root: "Optional[str]", +) -> bool: + # in_app_include takes precedence over in_app_exclude + should_be_included = _module_in_list(namespace, in_app_include) + should_be_excluded = _is_external_source(abs_path) or _module_in_list( + namespace, in_app_exclude + ) + return not is_sentry_sdk_frame and ( + should_be_included + or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) + ) + + +def add_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", + project_root: "Optional[str]", + in_app_include: "Optional[list[str]]", + in_app_exclude: "Optional[list[str]]", +) -> None: + """ + Adds OTel compatible source code information to the span + """ + # Find the correct frame + frame: "Union[FrameType, None]" = sys._getframe() + while frame is not None: + abs_path = _get_frame_module_abs_path(frame) + + try: + namespace: "Optional[str]" = frame.f_globals.get("__name__") + except Exception: + namespace = None + + is_sentry_sdk_frame = namespace is not None and namespace.startswith( + "sentry_sdk." + ) + + should_be_included = _should_be_included( + is_sentry_sdk_frame=is_sentry_sdk_frame, + namespace=namespace, + in_app_include=in_app_include, + in_app_exclude=in_app_exclude, + abs_path=abs_path, + project_root=project_root, + ) + if should_be_included: + break + + frame = frame.f_back + else: + frame = None + + # Set the data + if frame is not None: + try: + lineno = frame.f_lineno + except Exception: + lineno = None + if lineno is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_LINENO, lineno) + else: + span.set_attribute("code.line.number", lineno) + + try: + namespace = frame.f_globals.get("__name__") + except Exception: + namespace = None + if namespace is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_NAMESPACE, namespace) + else: + span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) + + filepath = _get_frame_module_abs_path(frame) + if filepath is not None: + if namespace is not None: + in_app_path = filename_for_module(namespace, filepath) + elif project_root is not None and filepath.startswith(project_root): + in_app_path = filepath.replace(project_root, "").lstrip(os.sep) + else: + in_app_path = filepath + + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) + else: + if in_app_path is not None: + span.set_attribute("code.file.path", in_app_path) + + try: + code_function = frame.f_code.co_name + except Exception: + code_function = None + + if code_function is not None: + if isinstance(span, Span): + span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + else: + span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) + + +def add_query_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a database query span + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_query_source = client.options.get("enable_db_query_source", True) + if not should_add_query_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("db_query_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def add_http_request_source( + span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", +) -> None: + """ + Adds OTel compatible source code information to a span for an outgoing HTTP request + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + if isinstance(span, Span): + # In the StreamedSpan case, we need to add the extra span information before + # the span finishes, so it's expected that this will be None. In the Span case, + # it should already be finished. + if span.timestamp is None: + return + + if span.start_timestamp is None: + return + + should_add_request_source = client.options.get("enable_http_request_source", True) + if not should_add_request_source: + return + + if isinstance(span, StreamedSpan): + end_timestamp = span.end_timestamp + else: + end_timestamp = span.timestamp + + end_timestamp = end_timestamp or datetime.now(timezone.utc) + + duration = end_timestamp - span.start_timestamp + threshold = client.options.get("http_request_source_threshold_ms", 0) + slow_query = duration / timedelta(milliseconds=1) > threshold + + if not slow_query: + return + + add_source( + span=span, + project_root=client.options["project_root"], + in_app_include=client.options.get("in_app_include"), + in_app_exclude=client.options.get("in_app_exclude"), + ) + + +def extract_sentrytrace_data( + header: "Optional[str]", +) -> "Optional[Dict[str, Union[str, bool, None]]]": + """ + Given a `sentry-trace` header string, return a dictionary of data. + """ + if not header: + return None + + if "," in header: + # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. + parts = [part.strip() for part in header.split(",")] + header = next((part for part in parts if part), "") + + if not header: + return None + + if header.startswith("00-") and header.endswith("-00"): + header = header[3:-3] + + match = SENTRY_TRACE_REGEX.match(header) + if not match: + return None + + trace_id, parent_span_id, sampled_str = match.groups() + parent_sampled = None + + if trace_id: + trace_id = "{:032x}".format(int(trace_id, 16)) + if parent_span_id: + parent_span_id = "{:016x}".format(int(parent_span_id, 16)) + if sampled_str: + parent_sampled = sampled_str != "0" + + return { + "trace_id": trace_id, + "parent_span_id": parent_span_id, + "parent_sampled": parent_sampled, + } + + +def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": + real_sql = None + + # If we're using psycopg2, it could be that we're + # looking at a query that uses Composed objects. Use psycopg2's mogrify + # function to format the query. We lose per-parameter trimming but gain + # accuracy in formatting. + try: + if hasattr(cursor, "mogrify"): + real_sql = cursor.mogrify(sql) + if isinstance(real_sql, bytes): + real_sql = real_sql.decode(cursor.connection.encoding) + except Exception: + real_sql = None + + return real_sql or to_string(sql) + + +class PropagationContext: + """ + The PropagationContext represents the data of a trace in Sentry. + """ + + __slots__ = ( + "_trace_id", + "_span_id", + "parent_span_id", + "parent_sampled", + "baggage", + "custom_sampling_context", + ) + + def __init__( + self, + trace_id: "Optional[str]" = None, + span_id: "Optional[str]" = None, + parent_span_id: "Optional[str]" = None, + parent_sampled: "Optional[bool]" = None, + dynamic_sampling_context: "Optional[Dict[str, str]]" = None, + baggage: "Optional[Baggage]" = None, + ) -> None: + self._trace_id = trace_id + """The trace id of the Sentry trace.""" + + self._span_id = span_id + """The span id of the currently executing span.""" + + self.parent_span_id = parent_span_id + """The id of the parent span that started this span. + The parent span could also be a span in an upstream service.""" + + self.parent_sampled = parent_sampled + """Boolean indicator if the parent span was sampled. + Important when the parent span originated in an upstream service, + because we want to sample the whole trace, or nothing from the trace.""" + + self.baggage = baggage + """Parsed baggage header that is used for dynamic sampling decisions.""" + + """DEPRECATED this only exists for backwards compat of constructor.""" + if baggage is None and dynamic_sampling_context is not None: + self.baggage = Baggage(dynamic_sampling_context) + + self.custom_sampling_context: "Optional[dict[str, Any]]" = None + + @classmethod + def from_incoming_data( + cls, incoming_data: "Dict[str, Any]" + ) -> "PropagationContext": + propagation_context = PropagationContext() + normalized_data = normalize_incoming_data(incoming_data) + + sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) + sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) + + # nothing to propagate if no sentry-trace + if sentrytrace_data is None: + return propagation_context + + baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) + baggage = ( + Baggage.from_incoming_header(baggage_header) if baggage_header else None + ) + + if not _should_continue_trace(baggage): + return propagation_context + + propagation_context.update(sentrytrace_data) + if baggage: + propagation_context.baggage = baggage + + propagation_context._fill_sample_rand() + + return propagation_context + + @property + def trace_id(self) -> str: + """The trace id of the Sentry trace.""" + if not self._trace_id: + # New trace, don't fill in sample_rand + self._trace_id = uuid.uuid4().hex + + return self._trace_id + + @trace_id.setter + def trace_id(self, value: str) -> None: + self._trace_id = value + + @property + def span_id(self) -> str: + """The span id of the currently executed span.""" + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + + @span_id.setter + def span_id(self, value: str) -> None: + self._span_id = value + + @property + def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": + return self.get_baggage().dynamic_sampling_context() + + def to_traceparent(self) -> str: + return f"{self.trace_id}-{self.span_id}" + + def get_baggage(self) -> "Baggage": + if self.baggage is None: + self.baggage = Baggage.populate_from_propagation_context(self) + return self.baggage + + def iter_headers(self) -> "Iterator[Tuple[str, str]]": + """ + Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. + """ + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + def update(self, other_dict: "Dict[str, Any]") -> None: + """ + Updates the PropagationContext with data from the given dictionary. + """ + for key, value in other_dict.items(): + try: + setattr(self, key, value) + except AttributeError: + pass + + def _set_custom_sampling_context( + self, custom_sampling_context: "dict[str, Any]" + ) -> None: + self.custom_sampling_context = custom_sampling_context + + def __repr__(self) -> str: + return "".format( + self._trace_id, + self._span_id, + self.parent_span_id, + self.parent_sampled, + self.baggage, + ) + + def _fill_sample_rand(self) -> None: + """ + Ensure that there is a valid sample_rand value in the baggage. + + If there is a valid sample_rand value in the baggage, we keep it. + Otherwise, we generate a sample_rand value according to the following: + + - If we have a parent_sampled value and a sample_rate in the DSC, we compute + a sample_rand value randomly in the range: + - [0, sample_rate) if parent_sampled is True, + - or, in the range [sample_rate, 1) if parent_sampled is False. + + - If either parent_sampled or sample_rate is missing, we generate a random + value in the range [0, 1). + + The sample_rand is deterministically generated from the trace_id, if present. + + This function does nothing if there is no baggage. + """ + if self.baggage is None: + return + + sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) + if sample_rand is not None and 0 <= sample_rand < 1: + # sample_rand is present and valid, so don't overwrite it + return + + # Get the sample rate and compute the transformation that will map the random value + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). + sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) + + try: + sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) + except ValueError: + # ValueError is raised if the interval is invalid, i.e. lower >= upper. + # lower >= upper might happen if the incoming trace's sampled flag + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. + # We cannot generate a sensible sample_rand value in this case. + logger.debug( + f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " + f"and sample_rate={sample_rate}." + ) + return + + self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 + + def _sample_rand(self) -> "Optional[str]": + """Convenience method to get the sample_rand value from the baggage.""" + if self.baggage is None: + return None + + return self.baggage.sentry_items.get("sample_rand") + + +class Baggage: + """ + The W3C Baggage header information (see https://www.w3.org/TR/baggage/). + + Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. + Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but + it is the caller's responsibility to enforce this restriction. + """ + + __slots__ = ("sentry_items", "third_party_items", "mutable") + + SENTRY_PREFIX = "sentry-" + SENTRY_PREFIX_REGEX = re.compile("^sentry-") + + def __init__( + self, + sentry_items: "Dict[str, str]", + third_party_items: str = "", + mutable: bool = True, + ): + self.sentry_items = sentry_items + self.third_party_items = third_party_items + self.mutable = mutable + + @classmethod + def from_incoming_header( + cls, + header: "Optional[str]", + *, + _sample_rand: "Optional[str]" = None, + ) -> "Baggage": + """ + freeze if incoming header already has sentry baggage + """ + sentry_items = {} + third_party_items = "" + mutable = True + + if header: + for item in header.split(","): + if "=" not in item: + continue + + with capture_internal_exceptions(): + item = item.strip() + key, val = item.split("=", 1) + if Baggage.SENTRY_PREFIX_REGEX.match(key): + baggage_key = unquote(key.split("-")[1]) + sentry_items[baggage_key] = unquote(val) + mutable = False + else: + third_party_items += ("," if third_party_items else "") + item + + if _sample_rand is not None: + sentry_items["sample_rand"] = str(_sample_rand) + mutable = False + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": + """ + Deprecated: use populate_from_propagation_context + """ + if scope._propagation_context is None: + return Baggage({}) + + return Baggage.populate_from_propagation_context(scope._propagation_context) + + @classmethod + def populate_from_propagation_context( + cls, propagation_context: "PropagationContext" + ) -> "Baggage": + sentry_items: "Dict[str, str]" = {} + third_party_items = "" + mutable = False + + client = sentry_sdk.get_client() + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options + + sentry_items["trace_id"] = propagation_context.trace_id + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if options.get("traces_sample_rate"): + sentry_items["sample_rate"] = str(options["traces_sample_rate"]) + + return Baggage(sentry_items, third_party_items, mutable) + + @classmethod + def populate_from_transaction( + cls, transaction: "sentry_sdk.tracing.Transaction" + ) -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = transaction.trace_id + sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + transaction.name + and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES + ): + sentry_items["transaction"] = transaction.name + + if transaction.sample_rate is not None: + sentry_items["sample_rate"] = str(transaction.sample_rate) + + if transaction.sampled is not None: + sentry_items["sampled"] = "true" if transaction.sampled else "false" + + # there's an existing baggage but it was mutable, + # which is why we are creating this new baggage. + # However, if by chance the user put some sentry items in there, give them precedence. + if transaction._baggage and transaction._baggage.sentry_items: + sentry_items.update(transaction._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + @classmethod + def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": + """ + Populate fresh baggage entry with sentry_items and make it immutable + if this is the head SDK which originates traces. + """ + client = sentry_sdk.get_client() + sentry_items: "Dict[str, str]" = {} + + if not client.is_active(): + return Baggage(sentry_items) + + options = client.options or {} + + sentry_items["trace_id"] = segment.trace_id + sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 + + if options.get("environment"): + sentry_items["environment"] = options["environment"] + + if options.get("release"): + sentry_items["release"] = options["release"] + + if client.parsed_dsn: + sentry_items["public_key"] = client.parsed_dsn.public_key + if client.parsed_dsn.org_id: + sentry_items["org_id"] = client.parsed_dsn.org_id + + if ( + segment.get_attributes().get("sentry.span.source") + not in LOW_QUALITY_SEGMENT_SOURCES + ) and segment._name: + sentry_items["transaction"] = segment._name + + if segment._sample_rate is not None: + sentry_items["sample_rate"] = str(segment._sample_rate) + + if segment.sampled is not None: + sentry_items["sampled"] = "true" if segment.sampled else "false" + + # There's an existing baggage but it was mutable, which is why we are + # creating this new baggage. + # However, if by chance the user put some sentry items in there, give + # them precedence. + if segment._baggage and segment._baggage.sentry_items: + sentry_items.update(segment._baggage.sentry_items) + + return Baggage(sentry_items, mutable=False) + + def freeze(self) -> None: + self.mutable = False + + def dynamic_sampling_context(self) -> "Dict[str, str]": + header = {} + + for key, item in self.sentry_items.items(): + header[key] = item + + return header + + def serialize(self, include_third_party: bool = False) -> str: + items = [] + + for key, val in self.sentry_items.items(): + with capture_internal_exceptions(): + item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) + items.append(item) + + if include_third_party: + items.append(self.third_party_items) + + return ",".join(items) + + @staticmethod + def strip_sentry_baggage(header: str) -> str: + """Remove Sentry baggage from the given header. + + Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. + """ + return ",".join( + ( + item + for item in header.split(",") + if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) + ) + ) + + def _sample_rand(self) -> "Optional[float]": + """Convenience method to get the sample_rand value from the sentry_items. + + We validate the value and parse it as a float before returning it. The value is considered + valid if it is a float in the range [0, 1). + """ + sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) + + if sample_rand is not None and 0.0 <= sample_rand < 1.0: + return sample_rand + + return None + + def __repr__(self) -> str: + return f'' + + +def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: + """ + Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. + """ + trace_propagation_targets = client.options["trace_propagation_targets"] + + if is_sentry_url(client, url): + return False + + return match_regex_list(url, trace_propagation_targets, substring_matching=True) + + +def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": + """ + Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. + """ + data = {} + for key, value in incoming_data.items(): + if key.startswith("HTTP_"): + key = key[5:] + + key = key.replace("_", "-").lower() + data[key] = value + + return data + + +def create_span_decorator( + op: "Optional[Union[str, OP]]" = None, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, +) -> "Any": + """ + Create a span decorator that can wrap both sync and async functions. + + :param op: The operation type for the span. + :type op: str or :py:class:`sentry_sdk.consts.OP` or None + :param name: The name of the span. + :type name: str or None + :param attributes: Additional attributes to set on the span. + :type attributes: dict or None + :param template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + """ + from sentry_sdk.scope import should_send_default_pii + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return await f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return await f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = await f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + current_span = get_current_span() + + if current_span is None: + logger.debug( + "Cannot create a child span for %s. " + "Please start a Sentry transaction before calling this function.", + qualname_from_function(f), + ) + return f(*args, **kwargs) + + if isinstance(current_span, StreamedSpan): + warnings.warn( + "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", + DeprecationWarning, + stacklevel=2, + ) + return f(*args, **kwargs) + + span_op = op or _get_span_op(template) + function_name = name or qualname_from_function(f) or "" + span_name = _get_span_name(template, function_name, kwargs) + send_pii = should_send_default_pii() + + with current_span.start_child( + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) + + result = f(*args, **kwargs) + + _set_output_attributes(span, template, send_pii, result) + + return result + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def create_streaming_span_decorator( + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Any": + """ + Create a span creating decorator that can wrap both sync and async functions. + """ + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + result = await f(*args, **kwargs) + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + client = sentry_sdk.get_client() + if client.is_active() and not has_span_streaming_enabled(client.options): + warnings.warn( + "Using span streaming API in non-span-streaming mode. Use " + "@sentry_sdk.trace instead.", + stacklevel=2, + ) + + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + return f(*args, **kwargs) + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + +def get_current_span( + scope: "Optional[sentry_sdk.Scope]" = None, +) -> "Optional[Span]": + """ + Returns the currently active span if there is one running, otherwise `None` + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.span + return current_span + + +def _generate_sample_rand( + trace_id: "Optional[str]", + *, + interval: "tuple[float, float]" = (0.0, 1.0), +) -> float: + """Generate a sample_rand value from a trace ID. + + The generated value will be pseudorandomly chosen from the provided + interval. Specifically, given (lower, upper) = interval, the generated + value will be in the range [lower, upper). The value has 6-digit precision, + so when printing with .6f, the value will never be rounded up. + + The pseudorandom number generator is seeded with the trace ID. + """ + lower, upper = interval + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly + raise ValueError("Invalid interval: lower must be less than upper") + + rng = Random(trace_id) + lower_scaled = int(lower * 1_000_000) + upper_scaled = int(upper * 1_000_000) + try: + sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) + except ValueError: + # In some corner cases it might happen that the range is too small + # In that case, just take the lower bound + sample_rand_scaled = lower_scaled + + return sample_rand_scaled / 1_000_000 + + +def _sample_rand_range( + parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" +) -> "tuple[float, float]": + """ + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values + that a generated sample_rand value must fall into, given the parent_sampled and + sample_rate values. + """ + if parent_sampled is None or sample_rate is None: + return 0.0, 1.0 + elif parent_sampled is True: + return 0.0, sample_rate + else: # parent_sampled is False + return sample_rate, 1.0 + + +def _get_value(source: "Any", key: str) -> "Optional[Any]": + """ + Gets a value from a source object. The source can be a dict or an object. + It is checked for dictionary keys and object attributes. + """ + value = None + if isinstance(source, dict): + value = source.get(key) + else: + if hasattr(source, key): + try: + value = getattr(source, key) + except Exception: + value = None + return value + + +def _get_span_name( + template: "Union[str, SPANTEMPLATE]", + name: str, + kwargs: "Optional[dict[str, Any]]" = None, +) -> str: + """ + Get the name of the span based on the template and the name. + """ + span_name = name + + if template == SPANTEMPLATE.AI_CHAT: + model = None + if kwargs: + for key in ("model", "model_name"): + if kwargs.get(key) and isinstance(kwargs[key], str): + model = kwargs[key] + break + + span_name = f"chat {model}" if model else "chat" + + elif template == SPANTEMPLATE.AI_AGENT: + span_name = f"invoke_agent {name}" + + elif template == SPANTEMPLATE.AI_TOOL: + span_name = f"execute_tool {name}" + + return span_name + + +def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: + """ + Get the operation of the span based on the template. + """ + mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { + SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, + SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, + SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, + } + op = mapping.get(template, OP.FUNCTION) + + return str(op) + + +def _get_input_attributes( + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> "dict[str, Any]": + """ + Get input attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + mapping = { + "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), + "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), + "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), + "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), + "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), + "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), + "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), + "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), + "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), + } + + def _set_from_key(key: str, value: "Any") -> None: + if key in mapping: + (attribute, data_type) = mapping[key] + if value is not None and isinstance(value, data_type): + attributes[attribute] = value + + for key, value in list(kwargs.items()): + if key == "prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "user", "content": value} + ) + continue + + if key == "system_prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "system", "content": value} + ) + continue + + _set_from_key(key, value) + + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( + {"args": args, "kwargs": kwargs} + ) + + # Coerce to string + if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] + ) + + return attributes + + +def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": + """ + Get usage attributes. + """ + attributes = {} + + def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: + for key in keys: + value = _get_value(usage, key) + if value is not None and isinstance(value, int): + attributes[attribute] = value + + _set_from_keys( + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, + ("prompt_tokens", "input_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, + ("completion_tokens", "output_tokens"), + ) + _set_from_keys( + SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, + ("total_tokens",), + ) + + return attributes + + +def _get_output_attributes( + template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> "dict[str, Any]": + """ + Get output attributes for the given span template. + """ + attributes: "dict[str, Any]" = {} + + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: + with capture_internal_exceptions(): + # Usage from result, result.usage, and result.metadata.usage + usage_candidates = [result] + + usage = _get_value(result, "usage") + usage_candidates.append(usage) + + meta = _get_value(result, "metadata") + usage = _get_value(meta, "usage") + usage_candidates.append(usage) + + for usage_candidate in usage_candidates: + if usage_candidate is not None: + attributes.update(_get_usage_attributes(usage_candidate)) + + # Response model + model_name = _get_value(result, "model") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + model_name = _get_value(result, "model_name") + if model_name is not None and isinstance(model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name + + # Tool output + if template == SPANTEMPLATE.AI_TOOL and send_pii: + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) + + return attributes + + +def _set_input_attributes( + span: "Span", + template: "Union[str, SPANTEMPLATE]", + send_pii: bool, + name: str, + f: "Any", + args: "tuple[Any, ...]", + kwargs: "dict[str, Any]", +) -> None: + """ + Set span input attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param f: The wrapped function. + :param args: The arguments to the wrapped function. + :param kwargs: The keyword arguments to the wrapped function. + """ + attributes: "dict[str, Any]" = {} + + if template == SPANTEMPLATE.AI_AGENT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_AGENT_NAME: name, + } + elif template == SPANTEMPLATE.AI_CHAT: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + } + elif template == SPANTEMPLATE.AI_TOOL: + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: name, + } + + docstring = f.__doc__ + if docstring is not None: + attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring + + attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) + span.update_data(attributes or {}) + + +def _set_output_attributes( + span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" +) -> None: + """ + Set span output attributes based on the given span template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. + :param result: The result of the wrapped function. + """ + span.update_data(_get_output_attributes(template, send_pii, result) or {}) + + +def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: + """ + Check if we should continue the incoming trace according to the strict_trace_continuation spec. + https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation + """ + + client = sentry_sdk.get_client() + parsed_dsn = client.parsed_dsn + client_org_id = parsed_dsn.org_id if parsed_dsn else None + baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None + + if ( + client_org_id is not None + and baggage_org_id is not None + and client_org_id != baggage_org_id + ): + logger.debug( + f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + strict_trace_continuation: bool = client.options.get( + "strict_trace_continuation", False + ) + if strict_trace_continuation: + if (baggage_org_id is not None and client_org_id is None) or ( + baggage_org_id is None and client_org_id is not None + ): + logger.debug( + f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" + ) + return False + + return True + + +def add_sentry_baggage_to_headers( + headers: "MutableMapping[str, str]", sentry_baggage: str +) -> None: + """Add the Sentry baggage to the headers. + + This function directly mutates the provided headers. The provided sentry_baggage + is appended to the existing baggage. If the baggage already contains Sentry items, + they are stripped out first. + """ + existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") + stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) + + separator = "," if len(stripped_existing_baggage) > 0 else "" + + headers[BAGGAGE_HEADER_NAME] = ( + stripped_existing_baggage + separator + sentry_baggage + ) + + +def _make_sampling_decision( + name: str, + attributes: "Optional[Attributes]", + scope: "sentry_sdk.Scope", +) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": + """ + Decide whether a span should be sampled. + + Returns a tuple with: + 1. the sampling decision + 2. the effective sample rate + 3. the sample rand + 4. the reason for not sampling the span, if unsampled + """ + client = sentry_sdk.get_client() + + if not has_tracing_enabled(client.options): + return False, None, None, None + + propagation_context = scope.get_active_propagation_context() + + sample_rand = None + if propagation_context.baggage is not None: + sample_rand = propagation_context.baggage._sample_rand() + if sample_rand is None: + sample_rand = _generate_sample_rand(propagation_context.trace_id) + + # If there's a traces_sampler, use that; otherwise use traces_sample_rate + traces_sampler_defined = callable(client.options.get("traces_sampler")) + if traces_sampler_defined: + sampling_context = { + "span_context": { + "name": name, + "trace_id": propagation_context.trace_id, + "parent_span_id": propagation_context.parent_span_id, + "parent_sampled": propagation_context.parent_sampled, + "attributes": dict(attributes) if attributes else {}, + }, + } + + if propagation_context.custom_sampling_context: + sampling_context.update(propagation_context.custom_sampling_context) + + sample_rate = client.options["traces_sampler"](sampling_context) + else: + if propagation_context.parent_sampled is not None: + sample_rate = propagation_context.parent_sampled + else: + sample_rate = client.options["traces_sample_rate"] + + # Validate whether the sample_rate we got is actually valid. Since + # traces_sampler is user-provided, it could return anything. + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") + return False, None, None, "sample_rate" + + sample_rate = float(sample_rate) + if not sample_rate: + if traces_sampler_defined: + reason = "traces_sampler returned 0 or False" + else: + reason = "traces_sample_rate is set to 0" + + logger.debug(f"[Tracing] Discarding {name} because {reason}") + return False, 0.0, None, "sample_rate" + + # Adjust sample rate if we're under backpressure + sample_rate_before_backpressure = sample_rate + if client.monitor: + sample_rate /= 2**client.monitor.downsample_factor + + if not sample_rate: + logger.debug(f"[Tracing] Discarding {name} because backpressure") + return False, 0.0, None, "backpressure" + + # Make the actual decision + sampled = sample_rand < sample_rate + + if sampled: + logger.debug(f"[Tracing] Starting {name}") + outcome = None + + else: + # Determine why exactly the span will not be sampled. If we've lowered + # the effective sample_rate because of backpressure, check whether the + # span would've been sampled if backpressure wasn't active. If that's the + # case, backpressure is the actual reason, otherwise just pure sampling + # rate. + if ( + sample_rate_before_backpressure != sample_rate + and sample_rand < sample_rate_before_backpressure + ): + logger.debug(f"[Tracing] Discarding {name} because backpressure") + outcome = "backpressure" + + else: + logger.debug( + f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" + ) + outcome = "sample_rate" + + return sampled, sample_rate, sample_rand, outcome + + +def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: + """Determine if a span fits one of the rules in ignore_spans.""" + client = sentry_sdk.get_client() + ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") + + if not ignore_spans: + return False + + def _matches(rule: "Any", value: "Any") -> bool: + if isinstance(rule, Pattern): + if isinstance(value, str): + return bool(rule.fullmatch(value)) + else: + return False + + return rule == value + + for rule in ignore_spans: + if isinstance(rule, (str, Pattern)): + if _matches(rule, name): + return True + + elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): + name_matches = True + attributes_match = True + + if "name" in rule: + name_matches = _matches(rule["name"], name) + + if "attributes" in rule: + attributes = attributes or {} + + for attribute, value in rule["attributes"].items(): + if attribute not in attributes or not _matches( + value, attributes[attribute] + ): + attributes_match = False + break + + if name_matches and attributes_match: + return True + + return False + + +# Circular imports +from sentry_sdk.traces import ( + LOW_QUALITY_SEGMENT_SOURCES, + StreamedSpan, +) +from sentry_sdk.traces import ( + start_span as start_streaming_span, +) +from sentry_sdk.tracing import ( + BAGGAGE_HEADER_NAME, + LOW_QUALITY_TRANSACTION_SOURCES, + SENTRY_TRACE_HEADER_NAME, + Span, +) + +if TYPE_CHECKING: + from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py new file mode 100644 index 0000000000..2b71fee429 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py @@ -0,0 +1,1255 @@ +import asyncio +import gzip +import io +import json +import logging +import os +import socket +import ssl +import time +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from urllib.request import getproxies + +try: + import brotli # type: ignore +except ImportError: + brotli = None + +try: + import httpcore +except ImportError: + httpcore = None # type: ignore[assignment,unused-ignore] + +try: + import h2 # noqa: F401 + + HTTP2_ENABLED = httpcore is not None +except ImportError: + HTTP2_ENABLED = False + +try: + import anyio # noqa: F401 + + ASYNC_TRANSPORT_AVAILABLE = httpcore is not None +except ImportError: + ASYNC_TRANSPORT_AVAILABLE = False + +from typing import TYPE_CHECKING, Dict, List, cast + +import certifi +import urllib3 + +import sentry_sdk +from sentry_sdk.consts import EndpointType +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.utils import ( + Dsn, + capture_internal_exceptions, + logger, + mark_sentry_task_internal, +) +from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + DefaultDict, + Iterable, + Mapping, + Optional, + Self, + Tuple, + Type, + Union, + ) + + from urllib3.poolmanager import PoolManager, ProxyManager + + from sentry_sdk._types import Event, EventDataCategory + +KEEP_ALIVE_SOCKET_OPTIONS = [] +for option in [ + (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 + (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 +]: + try: + KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) + except AttributeError: + # a specific option might not be available on specific systems, + # e.g. TCP_KEEPIDLE doesn't exist on macOS + pass + + +def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": + """Case-insensitive header lookup for httpcore-style responses.""" + header_lower = header.lower() + return next( + ( + val.decode("ascii") + for key, val in response.headers + if key.decode("ascii").lower() == header_lower + ), + None, + ) + + +class Transport(ABC): + """Baseclass for all transports. + + A transport is used to send an event to sentry. + """ + + parsed_dsn: "Optional[Dsn]" = None + + def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: + self.options = options + if options and options["dsn"]: + self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) + else: + self.parsed_dsn = None + + def capture_event(self: "Self", event: "Event") -> None: + """ + DEPRECATED: Please use capture_envelope instead. + + This gets invoked with the event dictionary when an event should + be sent to sentry. + """ + + warnings.warn( + "capture_event is deprecated, please use capture_envelope instead!", + DeprecationWarning, + stacklevel=2, + ) + + envelope = Envelope() + envelope.add_event(event) + self.capture_envelope(envelope) + + @abstractmethod + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + """ + Send an envelope to Sentry. + + Envelopes are a data container format that can hold any type of data + submitted to Sentry. We use it to send all event data (including errors, + transactions, crons check-ins, etc.) to Sentry. + """ + pass + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Any]" = None, + ) -> None: + """ + Wait `timeout` seconds for the current events to be sent out. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def kill(self: "Self") -> None: + """ + Forcefully kills the transport. + + The default implementation is a no-op, since this method may only be relevant to some transports. + Subclasses should override this method if necessary. + """ + return None + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + """This increments a counter for event loss by reason and + data category by the given positive-int quantity (default 1). + + If an item is provided, the data category and quantity are + extracted from the item, and the values passed for + data_category and quantity are ignored. + + When recording a lost transaction via data_category="transaction", + the calling code should also record the lost spans via this method. + When recording lost spans, `quantity` should be set to the number + of contained spans, plus one for the transaction itself. When + passing an Item containing a transaction via the `item` parameter, + this method automatically records the lost spans. + """ + return None + + def is_healthy(self: "Self") -> bool: + return True + + +def _parse_rate_limits( + header: str, now: "Optional[datetime]" = None +) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": + if now is None: + now = datetime.now(timezone.utc) + + for limit in header.split(","): + try: + parameters = limit.strip().split(":") + retry_after_val, categories = parameters[:2] + + retry_after = now + timedelta(seconds=int(retry_after_val)) + for category in categories and categories.split(";") or (None,): + yield category, retry_after # type: ignore + except (LookupError, ValueError): + continue + + +class HttpTransportCore(Transport): + """Shared base class for sync and async transports.""" + + TIMEOUT = 30 # seconds + + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + from sentry_sdk.consts import VERSION + + Transport.__init__(self, options) + assert self.parsed_dsn is not None + self.options: "Dict[str, Any]" = options + self._worker = self._create_worker(options) + self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) + self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} + # We only use this Retry() class for the `get_retry_after` method it exposes + self._retry = urllib3.util.Retry() + self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( + defaultdict(int) + ) + self._last_client_report_sent = time.time() + + self._pool = self._make_pool() + + # Backwards compatibility for deprecated `self.hub_class` attribute + self._hub_cls = sentry_sdk.Hub + + experiments = options.get("_experiments", {}) + compression_level = experiments.get( + "transport_compression_level", + experiments.get("transport_zlib_compression_level"), + ) + compression_algo = experiments.get( + "transport_compression_algo", + ( + "gzip" + # if only compression level is set, assume gzip for backwards compatibility + # if we don't have brotli available, fallback to gzip + if compression_level is not None or brotli is None + else "br" + ), + ) + + if compression_algo == "br" and brotli is None: + logger.warning( + "You asked for brotli compression without the Brotli module, falling back to gzip -9" + ) + compression_algo = "gzip" + compression_level = None + + if compression_algo not in ("br", "gzip"): + logger.warning( + "Unknown compression algo %s, disabling compression", compression_algo + ) + self._compression_level = 0 + self._compression_algo = None + else: + self._compression_algo = compression_algo + + if compression_level is not None: + self._compression_level = compression_level + elif self._compression_algo == "gzip": + self._compression_level = 9 + elif self._compression_algo == "br": + self._compression_level = 4 + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + raise NotImplementedError() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + if not self.options["send_client_reports"]: + return + + if item is not None: + data_category = item.data_category + quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). + + if data_category == "transaction": + # Also record the lost spans + event = item.get_transaction_event() or {} + + # +1 for the transaction itself + span_count = ( + len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 + ) + self.record_lost_event(reason, "span", quantity=span_count) + + elif data_category == "log_item" and item: + # Also record size of lost logs in bytes + bytes_size = len(item.get_bytes()) + self.record_lost_event(reason, "log_byte", quantity=bytes_size) + + elif data_category == "attachment": + # quantity of 0 is actually 1 as we do not want to count + # empty attachments as actually empty. + quantity = len(item.get_bytes()) or 1 + + elif data_category is None: + raise TypeError("data category not provided") + + self._discarded_events[data_category, reason] += quantity + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return response.headers.get(header) + + def _update_rate_limits( + self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" + ) -> None: + # new sentries with more rate limit insights. We honor this header + # no matter of the status code to update our internal rate limits. + header = self._get_header_value(response, "x-sentry-rate-limits") + if header: + logger.warning("Rate-limited via x-sentry-rate-limits") + self._disabled_until.update(_parse_rate_limits(header)) + + # old sentries only communicate global rate limit hits via the + # retry-after header on 429. This header can also be emitted on new + # sentries if a proxy in front wants to globally slow things down. + elif response.status == 429: + logger.warning("Rate-limited via 429") + retry_after_value = self._get_header_value(response, "Retry-After") + retry_after = ( + self._retry.parse_retry_after(retry_after_value) + if retry_after_value is not None + else None + ) or 60 + self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( + seconds=retry_after + ) + + def _handle_request_error( + self: "Self", + envelope: "Optional[Envelope]", + loss_reason: str = "network", + record_reason: str = "network_error", + ) -> None: + def record_loss(reason: str) -> None: + if envelope is None: + self.record_lost_event(reason, data_category="error") + else: + for item in envelope.items: + self.record_lost_event(reason, item=item) + + self.on_dropped_event(loss_reason) + record_loss(record_reason) + + def _handle_response( + self: "Self", + response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", + envelope: "Optional[Envelope]", + ) -> None: + self._update_rate_limits(response) + + if response.status == 413: + size_exceeded_message = ( + "HTTP 413: Event dropped due to exceeded envelope size limit" + ) + response_message = getattr( + response, "data", getattr(response, "content", None) + ) + if response_message is not None: + size_exceeded_message += f" (body: {response_message})" + + logger.error(size_exceeded_message) + self._handle_request_error( + envelope=envelope, loss_reason="status_413", record_reason="send_error" + ) + + elif response.status == 429: + # if we hit a 429. Something was rate limited but we already + # acted on this in `self._update_rate_limits`. Note that we + # do not want to record event loss here as we will have recorded + # an outcome in relay already. + self.on_dropped_event("status_429") + pass + + elif response.status >= 300 or response.status < 200: + logger.error( + "Unexpected status code: %s (body: %s)", + response.status, + getattr(response, "data", getattr(response, "content", None)), + ) + self._handle_request_error( + envelope=envelope, loss_reason="status_{}".format(response.status) + ) + + def _update_headers( + self: "Self", + headers: "Dict[str, str]", + ) -> None: + headers.update( + { + "User-Agent": str(self._auth.client), + "X-Sentry-Auth": str(self._auth.to_header()), + } + ) + + def on_dropped_event(self: "Self", _reason: str) -> None: + return None + + def _fetch_pending_client_report( + self: "Self", force: bool = False, interval: int = 60 + ) -> "Optional[Item]": + if not self.options["send_client_reports"]: + return None + + if not (force or self._last_client_report_sent < time.time() - interval): + return None + + discarded_events = self._discarded_events + self._discarded_events = defaultdict(int) + self._last_client_report_sent = time.time() + + if not discarded_events: + return None + + return Item( + PayloadRef( + json={ + "timestamp": time.time(), + "discarded_events": [ + {"reason": reason, "category": category, "quantity": quantity} + for ( + (category, reason), + quantity, + ) in discarded_events.items() + ], + } + ), + type="client_report", + ) + + def _check_disabled(self, category: str) -> bool: + def _disabled(bucket: "Any") -> bool: + ts = self._disabled_until.get(bucket) + return ts is not None and ts > datetime.now(timezone.utc) + + return _disabled(category) or _disabled(None) + + def _is_rate_limited(self: "Self") -> bool: + return any( + ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() + ) + + def _is_worker_full(self: "Self") -> bool: + return self._worker.full() + + def is_healthy(self: "Self") -> bool: + return not (self._is_worker_full() or self._is_rate_limited()) + + def _prepare_envelope( + self: "Self", envelope: "Envelope" + ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": + # remove all items from the envelope which are over quota + new_items = [] + for item in envelope.items: + if self._check_disabled(item.data_category): + if item.data_category in ("transaction", "error", "default", "statsd"): + self.on_dropped_event("self_rate_limits") + self.record_lost_event("ratelimit_backoff", item=item) + else: + new_items.append(item) + + # Since we're modifying the envelope here make a copy so that others + # that hold references do not see their envelope modified. + envelope = Envelope(headers=envelope.headers, items=new_items) + + if not envelope.items: + return None + + # since we're already in the business of sending out an envelope here + # check if we have one pending for the stats session envelopes so we + # can attach it to this enveloped scheduled for sending. This will + # currently typically attach the client report to the most recent + # session update. + client_report_item = self._fetch_pending_client_report(interval=30) + if client_report_item is not None: + envelope.items.append(client_report_item) + + content_encoding, body = self._serialize_envelope(envelope) + + assert self.parsed_dsn is not None + logger.debug( + "Sending envelope [%s] project:%s host:%s", + envelope.description, + self.parsed_dsn.project_id, + self.parsed_dsn.host, + ) + + headers: "Dict[str, str]" = { + "Content-Type": "application/x-sentry-envelope", + } + if content_encoding: + headers["Content-Encoding"] = content_encoding + + return envelope, body, headers + + def _serialize_envelope( + self: "Self", envelope: "Envelope" + ) -> "tuple[Optional[str], io.BytesIO]": + content_encoding = None + body = io.BytesIO() + if self._compression_level == 0 or self._compression_algo is None: + envelope.serialize_into(body) + else: + content_encoding = self._compression_algo + if self._compression_algo == "br" and brotli is not None: + body.write( + brotli.compress( + envelope.serialize(), quality=self._compression_level + ) + ) + else: # assume gzip as we sanitize the algo value in init + with gzip.GzipFile( + fileobj=body, mode="w", compresslevel=self._compression_level + ) as f: + envelope.serialize_into(f) + + return content_encoding, body + + def _get_httpcore_pool_options( + self: "Self", http2: bool = False + ) -> "Dict[str, Any]": + """Shared pool options for httpcore-based transports (Http2 and Async).""" + options: "Dict[str, Any]" = { + "http2": http2, + "retries": 3, + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations( + self.options["ca_certs"] + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") + key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") + if cert_file is not None: + ssl_context.load_cert_chain(cert_file, key_file) + + options["ssl_context"] = ssl_context + return options + + def _resolve_proxy(self: "Self") -> "Optional[str]": + """Resolve proxy URL from options and environment. Returns proxy URL or None.""" + if self.parsed_dsn is None: + return None + + no_proxy = self._in_no_proxy(self.parsed_dsn) + proxy = None + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + return proxy or None + + @property + def _timeout_extensions(self: "Self") -> "Dict[str, Any]": + return { + "timeout": { + "pool": self.TIMEOUT, + "connect": self.TIMEOUT, + "write": self.TIMEOUT, + "read": self.TIMEOUT, + } + } + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + raise NotImplementedError() + + def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: + no_proxy = getproxies().get("no") + if not no_proxy: + return False + for host in no_proxy.split(","): + host = host.strip() + if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): + return True + return False + + def _make_pool( + self: "Self", + ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + raise NotImplementedError() + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": + raise NotImplementedError() + + def kill(self: "Self") -> None: + logger.debug("Killing HTTP transport") + self._worker.kill() + + +# Keep BaseHttpTransport as an alias for backwards compatibility +# and for the sync transport implementation +class BaseHttpTransport(HttpTransportCore): + """The base HTTP transport (synchronous).""" + + def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + response.close() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return BackgroundWorker(queue_size=options["transport_queue_size"]) + + def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def capture_envelope( + self, + envelope: "Envelope", + ) -> None: + def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + self._send_envelope(envelope) + self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def flush( + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> None: + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + self._worker.flush(timeout, callback) + + @staticmethod + def _warn_hub_cls() -> None: + """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" + warnings.warn( + "The `hub_cls` attribute is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=3, + ) + + @property + def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + return self._hub_cls + + @hub_cls.setter + def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: + """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" + HttpTransport._warn_hub_cls() + self._hub_cls = value + + +class HttpTransport(BaseHttpTransport): + if TYPE_CHECKING: + _pool: "Union[PoolManager, ProxyManager]" + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + num_pools = self.options.get("_experiments", {}).get("transport_num_pools") + options = { + "num_pools": 2 if num_pools is None else int(num_pools), + "cert_reqs": "CERT_REQUIRED", + "timeout": urllib3.Timeout(total=self.TIMEOUT), + } + + socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None + + if self.options["socket_options"] is not None: + socket_options = self.options["socket_options"] + + if self.options["keep_alive"]: + if socket_options is None: + socket_options = [] + + used_options = {(o[0], o[1]) for o in socket_options} + for default_option in KEEP_ALIVE_SOCKET_OPTIONS: + if (default_option[0], default_option[1]) not in used_options: + socket_options.append(default_option) + + if socket_options is not None: + options["socket_options"] = socket_options + + options["ca_certs"] = ( + self.options["ca_certs"] # User-provided bundle from the SDK init + or os.environ.get("SSL_CERT_FILE") + or os.environ.get("REQUESTS_CA_BUNDLE") + or certifi.where() + ) + + options["cert_file"] = self.options["cert_file"] or os.environ.get( + "CLIENT_CERT_FILE" + ) + options["key_file"] = self.options["key_file"] or os.environ.get( + "CLIENT_KEY_FILE" + ) + + return options + + def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = None + no_proxy = self._in_no_proxy(self.parsed_dsn) + + # try HTTPS first + https_proxy = self.options["https_proxy"] + if self.parsed_dsn.scheme == "https" and (https_proxy != ""): + proxy = https_proxy or (not no_proxy and getproxies().get("https")) + + # maybe fallback to HTTP proxy + http_proxy = self.options["http_proxy"] + if not proxy and (http_proxy != ""): + proxy = http_proxy or (not no_proxy and getproxies().get("http")) + + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + use_socks_proxy = True + try: + # Check if PySocks dependency is available + from urllib3.contrib.socks import SOCKSProxyManager + except ImportError: + use_socks_proxy = False + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", + proxy, + ) + + if use_socks_proxy: + return SOCKSProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + else: + return urllib3.ProxyManager(proxy, **opts) + else: + return urllib3.PoolManager(**opts) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "urllib3.BaseHTTPResponse": + return self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + body=body, + headers=headers, + ) + + +class AsyncHttpTransport(HttpTransportCore): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + if not ASYNC_TRANSPORT_AVAILABLE: + raise RuntimeError( + "AsyncHttpTransport requires httpcore[asyncio]. " + "Install it with: pip install sentry-sdk[asyncio]" + ) + super().__init__(options) + # Requires event loop at init time + self.loop = asyncio.get_running_loop() + + def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": + return AsyncWorker(queue_size=options["transport_queue_size"]) + + def _get_header_value( + self: "Self", response: "Any", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + async def _send_envelope(self: "Self", envelope: "Envelope") -> None: + _prepared_envelope = self._prepare_envelope(envelope) + if _prepared_envelope is not None: + envelope, body, headers = _prepared_envelope + await self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) + return None + + async def _send_request( + self: "Self", + body: bytes, + headers: "Dict[str, str]", + endpoint_type: "EndpointType", + envelope: "Optional[Envelope]" = None, + ) -> None: + self._update_headers(headers) + try: + response = await self._request( + "POST", + endpoint_type, + body, + headers, + ) + except Exception: + self._handle_request_error(envelope=envelope, loss_reason="network") + raise + try: + self._handle_response(response=response, envelope=envelope) + finally: + await response.aclose() + + async def _request( # type: ignore[override] + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + return await self._pool.request( # type: ignore[misc,unused-ignore] + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + + async def _flush_client_reports(self: "Self", force: bool = False) -> None: + client_report = self._fetch_pending_client_report(force=force, interval=60) + if client_report is not None: + self.capture_envelope(Envelope(items=[client_report])) + + def _capture_envelope(self: "Self", envelope: "Envelope") -> None: + async def send_envelope_wrapper() -> None: + with capture_internal_exceptions(): + await self._send_envelope(envelope) + await self._flush_client_reports() + + if not self._worker.submit(send_envelope_wrapper): + self.on_dropped_event("full_queue") + for item in envelope.items: + self.record_lost_event("queue_overflow", item=item) + + def capture_envelope(self: "Self", envelope: "Envelope") -> None: + # Synchronous entry point + if self.loop and self.loop.is_running(): + self.loop.call_soon_threadsafe(self._capture_envelope, envelope) + else: + # The event loop is no longer running + logger.warning("Async Transport is not running in an event loop.") + self.on_dropped_event("internal_sdk_error") + for item in envelope.items: + self.record_lost_event("internal_sdk_error", item=item) + + def flush( # type: ignore[override] + self: "Self", + timeout: float, + callback: "Optional[Callable[[int, float], None]]" = None, + ) -> "Optional[asyncio.Task[None]]": + logger.debug("Flushing HTTP transport") + + if timeout > 0: + self._worker.submit(lambda: self._flush_client_reports(force=True)) + return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] + return None + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=HTTP2_ENABLED + and self.parsed_dsn is not None + and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + socks_opts = opts.copy() + if "socket_options" in socks_opts: + socket_options = socks_opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) + + return httpcore.AsyncConnectionPool(**opts) + + def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] + logger.debug("Killing HTTP transport") + self._worker.kill() + try: + # Return the pool cleanup task so caller can await it if needed + with mark_sentry_task_internal(): + return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] + except RuntimeError: + logger.warning("Event loop not running, aborting kill.") + return None + + +if not HTTP2_ENABLED: + # Sorry, no Http2Transport for you + class Http2Transport(HttpTransport): + def __init__(self: "Self", options: "Dict[str, Any]") -> None: + super().__init__(options) + logger.warning( + "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." + ) + +else: + + class Http2Transport(BaseHttpTransport): # type: ignore + """The HTTP2 transport based on httpcore.""" + + TIMEOUT = 15 + + if TYPE_CHECKING: + _pool: """Union[ + httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool + ]""" + + def _get_header_value( + self: "Self", response: "httpcore.Response", header: str + ) -> "Optional[str]": + return _get_httpcore_header_value(response, header) + + def _request( + self: "Self", + method: str, + endpoint_type: "EndpointType", + body: "Any", + headers: "Mapping[str, str]", + ) -> "httpcore.Response": + response = self._pool.request( + method, + self._auth.get_api_url(endpoint_type), + content=body, + headers=headers, # type: ignore[arg-type,unused-ignore] + extensions=self._timeout_extensions, + ) + return response + + def _get_pool_options(self: "Self") -> "Dict[str, Any]": + return self._get_httpcore_pool_options( + http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" + ) + + def _make_pool( + self: "Self", + ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": + if self.parsed_dsn is None: + raise ValueError("Cannot create HTTP-based transport without valid DSN") + + proxy = self._resolve_proxy() + opts = self._get_pool_options() + + if proxy: + proxy_headers = self.options["proxy_headers"] + if proxy_headers: + opts["proxy_headers"] = proxy_headers + + if proxy.startswith("socks"): + try: + if "socket_options" in opts: + socket_options = opts.pop("socket_options") + if socket_options: + logger.warning( + "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." + ) + return httpcore.SOCKSProxy(proxy_url=proxy, **opts) + except RuntimeError: + logger.warning( + "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", + proxy, + ) + else: + return httpcore.HTTPProxy(proxy_url=proxy, **opts) + + return httpcore.ConnectionPool(**opts) + + +class _EnvelopePrinterTransport(Transport): + """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" + + def __init__(self, transport: "Transport") -> None: + Transport.__init__(self, options=transport.options) + self._inner = transport + self.parsed_dsn = transport.parsed_dsn + + self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") + self.envelope_logger.setLevel(logging.INFO) + self.envelope_logger.propagate = False + if not self.envelope_logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter("%(message)s")) + self.envelope_logger.addHandler(handler) + + @property # type: ignore[misc] + def __class__(self) -> type: + return self._inner.__class__ + + def capture_envelope(self, envelope: "Envelope") -> None: + try: + self.envelope_logger.info("--- Sentry Envelope ---") + self.envelope_logger.info( + "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) + ) + for item in envelope.items: + self.envelope_logger.info(" Item type: %s", item.type) + self.envelope_logger.info( + " Item headers: %s", + json.dumps(item.headers, indent=2, default=str), + ) + try: + payload = json.loads(item.get_bytes()) + self.envelope_logger.info( + " Payload:\n%s", + json.dumps(payload, indent=2, default=str), + ) + except (ValueError, TypeError): + self.envelope_logger.info( + " Payload: ", + len(item.get_bytes()), + ) + self.envelope_logger.info("--- End Envelope ---") + except Exception: + pass + + self._inner.capture_envelope(envelope) + + def flush( + self, + timeout: float, + callback: "Optional[Any]" = None, + ) -> "Any": + return self._inner.flush(timeout, callback) + + def kill(self) -> "Any": + return self._inner.kill() + + def record_lost_event( + self, + reason: str, + data_category: "Optional[EventDataCategory]" = None, + item: "Optional[Item]" = None, + *, + quantity: int = 1, + ) -> None: + self._inner.record_lost_event(reason, data_category, item, quantity=quantity) + + def is_healthy(self) -> bool: + return self._inner.is_healthy() + + def __getattr__(self, name: str) -> "Any": + return getattr(self._inner, name) + + +class _FunctionTransport(Transport): + """ + DEPRECATED: Users wishing to provide a custom transport should subclass + the Transport class, rather than providing a function. + """ + + def __init__( + self, + func: "Callable[[Event], None]", + ) -> None: + Transport.__init__(self) + self._func = func + + def capture_event( + self, + event: "Event", + ) -> None: + self._func(event) + return None + + def capture_envelope(self, envelope: "Envelope") -> None: + # Since function transports expect to be called with an event, we need + # to iterate over the envelope and call the function for each event, via + # the deprecated capture_event method. + event = envelope.get_event() + if event is not None: + self.capture_event(event) + + +def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": + ref_transport = options["transport"] + + use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) + use_async_transport = options.get("_experiments", {}).get("transport_async", False) + async_integration = any( + integration.__class__.__name__ == "AsyncioIntegration" + for integration in options.get("integrations") or [] + ) + + # By default, we use the http transport class + transport_cls: "Type[Transport]" = ( + Http2Transport if use_http2_transport else HttpTransport + ) + + if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: + try: + asyncio.get_running_loop() + if async_integration: + if use_http2_transport: + logger.warning( + "HTTP/2 transport is not supported with async transport. " + "Ignoring transport_http2 experiment." + ) + transport_cls = AsyncHttpTransport + else: + logger.warning( + "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." + ) + except RuntimeError: + # No event loop running, fall back to sync transport + logger.warning("No event loop running, falling back to sync transport.") + elif use_async_transport: + logger.warning( + "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." + ) + + transport: "Optional[Transport]" = None + + if isinstance(ref_transport, Transport): + transport = ref_transport + elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): + transport_cls = ref_transport + elif callable(ref_transport): + warnings.warn( + "Function transports are deprecated and will be removed in a future release." + "Please provide a Transport instance or subclass, instead.", + DeprecationWarning, + stacklevel=2, + ) + transport = _FunctionTransport(ref_transport) + + # if a transport class is given only instantiate it if the dsn is not + # empty or None + if transport is None and options["dsn"]: + transport = transport_cls(options) + + if transport is not None and os.environ.get( + "SENTRY_PRINT_ENVELOPES", "" + ).lower() in ("1", "true", "yes"): + transport = _EnvelopePrinterTransport(transport) + + return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py new file mode 100644 index 0000000000..dff91e3719 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py @@ -0,0 +1,52 @@ +""" +This module contains type definitions for the Sentry SDK's public API. +The types are re-exported from the internal module `sentry_sdk._types`. + +Disclaimer: Since types are a form of documentation, type definitions +may change in minor releases. Removing a type would be considered a +breaking change, and so we will only remove type definitions in major +releases. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Re-export types to make them available in the public API + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + EventDataCategory, + Hint, + Log, + Metric, + MonitorConfig, + SamplingContext, + ) +else: + from typing import Any + + # The lines below allow the types to be imported from outside `if TYPE_CHECKING` + # guards. The types in this module are only intended to be used for type hints. + Breadcrumb = Any + BreadcrumbHint = Any + Event = Any + EventDataCategory = Any + Hint = Any + Log = Any + MonitorConfig = Any + SamplingContext = Any + Metric = Any + + +__all__ = ( + "Breadcrumb", + "BreadcrumbHint", + "Event", + "EventDataCategory", + "Hint", + "Log", + "MonitorConfig", + "SamplingContext", + "Metric", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py new file mode 100644 index 0000000000..0963015351 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py @@ -0,0 +1,2178 @@ +import base64 +import copy +import json +import linecache +import logging +import math +import os +import random +import re +import subprocess +import sys +import threading +import time +from collections import namedtuple +from contextlib import contextmanager +from datetime import datetime, timezone +from decimal import Decimal +from functools import partial, partialmethod, wraps +from numbers import Real +from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit + +try: + # Python 3.11 + from builtins import BaseExceptionGroup +except ImportError: + # Python 3.10 and below + BaseExceptionGroup = None # type: ignore + +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk._compat import PY37 +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue +from sentry_sdk.consts import ( + DEFAULT_ADD_FULL_STACK, + DEFAULT_MAX_STACK_FRAMES, + EndpointType, +) + +if TYPE_CHECKING: + from types import FrameType, TracebackType + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Iterator, + List, + NoReturn, + Optional, + ParamSpec, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, + ) + + from gevent.hub import Hub + + from sentry_sdk._types import ( + AttributeValue, + Event, + ExcInfo, + Hint, + Log, + Metric, + SerializedAttributeValue, + SpanJSON, + ) + + P = ParamSpec("P") + R = TypeVar("R") + + +epoch = datetime(1970, 1, 1) + +# The logger is created here but initialized in the debug support module +logger = logging.getLogger("sentry_sdk.errors") + +_installed_modules = None + +BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") + +FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) +TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) + +MAX_STACK_FRAMES = 2000 +"""Maximum number of stack frames to send to Sentry. + +If we have more than this number of stack frames, we will stop processing +the stacktrace to avoid getting stuck in a long-lasting loop. This value +exceeds the default sys.getrecursionlimit() of 1000, so users will only +be affected by this limit if they have a custom recursion limit. +""" + + +def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": + """Casts an ENV variable value to boolean using the constants defined above. + In strict mode, it may return None if the value doesn't match any of the predefined values. + """ + normalized = str(value).lower() if value is not None else None + + if normalized in FALSY_ENV_VALUES: + return False + + if normalized in TRUTHY_ENV_VALUES: + return True + + return None if strict else bool(value) + + +def json_dumps(data: "Any") -> bytes: + """Serialize data into a compact JSON representation encoded as UTF-8.""" + return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") + + +def get_git_revision() -> "Optional[str]": + try: + with open(os.path.devnull, "w+") as null: + # prevent command prompt windows from popping up on windows + startupinfo = None + if sys.platform == "win32" or sys.platform == "cygwin": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + revision = ( + subprocess.Popen( + ["git", "rev-parse", "HEAD"], + startupinfo=startupinfo, + stdout=subprocess.PIPE, + stderr=null, + stdin=null, + ) + .communicate()[0] + .strip() + .decode("utf-8") + ) + except (OSError, IOError, FileNotFoundError): + return None + + return revision + + +def get_default_release() -> "Optional[str]": + """Try to guess a default release.""" + release = os.environ.get("SENTRY_RELEASE") + if release: + return release + + release = get_git_revision() + if release: + return release + + for var in ( + "HEROKU_BUILD_COMMIT", + "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility + "SOURCE_VERSION", + "CODEBUILD_RESOLVED_SOURCE_VERSION", + "CIRCLE_SHA1", + "GAE_DEPLOYMENT_ID", + "K_REVISION", + ): + release = os.environ.get(var) + if release: + return release + return None + + +def get_sdk_name(installed_integrations: "List[str]") -> str: + """Return the SDK name including the name of the used web framework.""" + + # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier + # here because if django is not installed the integration is not accessible. + framework_integrations = [ + "django", + "flask", + "fastapi", + "bottle", + "falcon", + "quart", + "sanic", + "starlette", + "litestar", + "starlite", + "chalice", + "serverless", + "pyramid", + "tornado", + "aiohttp", + "aws_lambda", + "gcp", + "beam", + "asgi", + "wsgi", + ] + + for integration in framework_integrations: + if integration in installed_integrations: + return "sentry.python.{}".format(integration) + + return "sentry.python" + + +class CaptureInternalException: + __slots__ = () + + def __enter__(self) -> "ContextManager[Any]": + return self + + def __exit__( + self, + ty: "Optional[Type[BaseException]]", + value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + ) -> bool: + if ty is not None and value is not None: + capture_internal_exception((ty, value, tb)) + + return True + + +_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() + + +def capture_internal_exceptions() -> "ContextManager[Any]": + return _CAPTURE_INTERNAL_EXCEPTION + + +def capture_internal_exception(exc_info: "ExcInfo") -> None: + """ + Capture an exception that is likely caused by a bug in the SDK + itself. + + These exceptions do not end up in Sentry and are just logged instead. + """ + if sentry_sdk.get_client().is_active(): + logger.error("Internal error in sentry_sdk", exc_info=exc_info) + + +def to_timestamp(value: "datetime") -> float: + return (value - epoch).total_seconds() + + +def format_timestamp(value: "datetime") -> str: + """Formats a timestamp in RFC 3339 format. + + Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. + """ + utctime = value.astimezone(timezone.utc) + + # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for + # several years now), and isoformat is slightly different. + return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +ISO_TZ_SEPARATORS = frozenset(("+", "-")) + + +def datetime_from_isoformat(value: str) -> "datetime": + try: + result = datetime.fromisoformat(value) + except (AttributeError, ValueError): + # py 3.6 + timestamp_format = ( + "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" + ) + if value.endswith("Z"): + value = value[:-1] + "+0000" + + if value[-6] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + value = value[:-3] + value[-2:] + elif value[-5] in ISO_TZ_SEPARATORS: + timestamp_format += "%z" + + result = datetime.strptime(value, timestamp_format) + return result.astimezone(timezone.utc) + + +def event_hint_with_exc_info( + exc_info: "Optional[ExcInfo]" = None, +) -> "Dict[str, Optional[ExcInfo]]": + """Creates a hint with the exc info filled in.""" + if exc_info is None: + exc_info = sys.exc_info() + else: + exc_info = exc_info_from_error(exc_info) + if exc_info[0] is None: + exc_info = None + return {"exc_info": exc_info} + + +class BadDsn(ValueError): + """Raised on invalid DSNs.""" + + +class Dsn: + """Represents a DSN.""" + + ORG_ID_REGEX = re.compile(r"^o(\d+)\.") + + def __init__( + self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None + ) -> None: + if isinstance(value, Dsn): + self.__dict__ = dict(value.__dict__) + return + parts = urlsplit(str(value)) + + if parts.scheme not in ("http", "https"): + raise BadDsn("Unsupported scheme %r" % parts.scheme) + self.scheme = parts.scheme + + if parts.hostname is None: + raise BadDsn("Missing hostname") + + self.host = parts.hostname + + if org_id is not None: + self.org_id: "Optional[str]" = org_id + else: + org_id_match = Dsn.ORG_ID_REGEX.match(self.host) + self.org_id = org_id_match.group(1) if org_id_match else None + + if parts.port is None: + self.port: int = self.scheme == "https" and 443 or 80 + else: + self.port = parts.port + + if not parts.username: + raise BadDsn("Missing public key") + + self.public_key = parts.username + self.secret_key = parts.password + + path = parts.path.rsplit("/", 1) + + try: + self.project_id = str(int(path.pop())) + except (ValueError, TypeError): + raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) + + self.path = "/".join(path) + "/" + + @property + def netloc(self) -> str: + """The netloc part of a DSN.""" + rv = self.host + if (self.scheme, self.port) not in (("http", 80), ("https", 443)): + rv = "%s:%s" % (rv, self.port) + return rv + + def to_auth(self, client: "Optional[Any]" = None) -> "Auth": + """Returns the auth info object for this dsn.""" + return Auth( + scheme=self.scheme, + host=self.netloc, + path=self.path, + project_id=self.project_id, + public_key=self.public_key, + secret_key=self.secret_key, + client=client, + ) + + def __str__(self) -> str: + return "%s://%s%s@%s%s%s" % ( + self.scheme, + self.public_key, + self.secret_key and "@" + self.secret_key or "", + self.netloc, + self.path, + self.project_id, + ) + + +class Auth: + """Helper object that represents the auth info.""" + + def __init__( + self, + scheme: str, + host: str, + project_id: str, + public_key: str, + secret_key: "Optional[str]" = None, + version: int = 7, + client: "Optional[Any]" = None, + path: str = "/", + ) -> None: + self.scheme = scheme + self.host = host + self.path = path + self.project_id = project_id + self.public_key = public_key + self.secret_key = secret_key + self.version = version + self.client = client + + def get_api_url( + self, + type: "EndpointType" = EndpointType.ENVELOPE, + ) -> str: + """Returns the API url for storing events.""" + return "%s://%s%sapi/%s/%s/" % ( + self.scheme, + self.host, + self.path, + self.project_id, + type.value, + ) + + def to_header(self) -> str: + """Returns the auth header a string.""" + rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] + if self.client is not None: + rv.append(("sentry_client", self.client)) + if self.secret_key is not None: + rv.append(("sentry_secret", self.secret_key)) + return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) + + +def get_type_name(cls: "Optional[type]") -> "Optional[str]": + return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) + + +def get_type_module(cls: "Optional[type]") -> "Optional[str]": + mod = getattr(cls, "__module__", None) + if mod not in (None, "builtins", "__builtins__"): + return mod + return None + + +def should_hide_frame(frame: "FrameType") -> bool: + try: + mod = frame.f_globals["__name__"] + if mod.startswith("sentry_sdk."): + return True + except (AttributeError, KeyError): + pass + + for flag_name in "__traceback_hide__", "__tracebackhide__": + try: + if frame.f_locals[flag_name]: + return True + except Exception: + pass + + return False + + +def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": + tb_: "Optional[TracebackType]" = tb + while tb_ is not None: + if not should_hide_frame(tb_.tb_frame): + yield tb_ + tb_ = tb_.tb_next + + +def get_lines_from_file( + filename: str, + lineno: int, + max_length: "Optional[int]" = None, + loader: "Optional[Any]" = None, + module: "Optional[str]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + context_lines = 5 + source = None + if loader is not None and hasattr(loader, "get_source"): + try: + source_str: "Optional[str]" = loader.get_source(module) + except (ImportError, IOError): + source_str = None + if source_str is not None: + source = source_str.splitlines() + + if source is None: + try: + source = linecache.getlines(filename) + except (OSError, IOError): + return [], None, [] + + if not source: + return [], None, [] + + lower_bound = max(0, lineno - context_lines) + upper_bound = min(lineno + 1 + context_lines, len(source)) + + try: + pre_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[lower_bound:lineno] + ] + context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) + post_context = [ + strip_string(line.strip("\r\n"), max_length=max_length) + for line in source[(lineno + 1) : upper_bound] + ] + return pre_context, context_line, post_context + except IndexError: + # the file may have changed since it was loaded into memory + return [], None, [] + + +def get_source_context( + frame: "FrameType", + tb_lineno: "Optional[int]", + max_value_length: "Optional[int]" = None, +) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": + try: + abs_path: "Optional[str]" = frame.f_code.co_filename + except Exception: + abs_path = None + try: + module = frame.f_globals["__name__"] + except Exception: + return [], None, [] + try: + loader = frame.f_globals["__loader__"] + except Exception: + loader = None + + if tb_lineno is not None and abs_path: + lineno = tb_lineno - 1 + return get_lines_from_file( + abs_path, lineno, max_value_length, loader=loader, module=module + ) + + return [], None, [] + + +def safe_str(value: "Any") -> str: + try: + return str(value) + except Exception: + return safe_repr(value) + + +def safe_repr(value: "Any") -> str: + try: + return repr(value) + except Exception: + return "" + + +def filename_for_module( + module: "Optional[str]", abs_path: "Optional[str]" +) -> "Optional[str]": + if not abs_path or not module: + return abs_path + + try: + if abs_path.endswith(".pyc"): + abs_path = abs_path[:-1] + + base_module = module.split(".", 1)[0] + if base_module == module: + return os.path.basename(abs_path) + + base_module_path = sys.modules[base_module].__file__ + if not base_module_path: + return abs_path + + return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( + os.sep + ) + except Exception: + return abs_path + + +def serialize_frame( + frame: "FrameType", + tb_lineno: "Optional[int]" = None, + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, + custom_repr: "Optional[Callable[..., Optional[str]]]" = None, +) -> "Dict[str, Any]": + f_code = getattr(frame, "f_code", None) + if not f_code: + abs_path = None + function = None + else: + abs_path = frame.f_code.co_filename + function = frame.f_code.co_name + try: + module = frame.f_globals["__name__"] + except Exception: + module = None + + if tb_lineno is None: + tb_lineno = frame.f_lineno + + try: + os_abs_path = os.path.abspath(abs_path) if abs_path else None + except Exception: + os_abs_path = None + + rv: "Dict[str, Any]" = { + "filename": filename_for_module(module, abs_path) or None, + "abs_path": os_abs_path, + "function": function or "", + "module": module, + "lineno": tb_lineno, + } + + if include_source_context: + rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( + frame, tb_lineno, max_value_length + ) + + if include_local_variables: + from sentry_sdk.serializer import serialize + + rv["vars"] = serialize( + dict(frame.f_locals), is_vars=True, custom_repr=custom_repr + ) + + return rv + + +def current_stacktrace( + include_local_variables: bool = True, + include_source_context: bool = True, + max_value_length: "Optional[int]" = None, +) -> "Dict[str, Any]": + __tracebackhide__ = True + frames = [] + + f: "Optional[FrameType]" = sys._getframe() + while f is not None: + if not should_hide_frame(f): + frames.append( + serialize_frame( + f, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + ) + ) + f = f.f_back + + frames.reverse() + + return {"frames": frames} + + +def get_errno(exc_value: BaseException) -> "Optional[Any]": + return getattr(exc_value, "errno", None) + + +def get_error_message(exc_value: "Optional[BaseException]") -> str: + message: str = safe_str( + getattr(exc_value, "message", "") + or getattr(exc_value, "detail", "") + or safe_str(exc_value) + ) + + # __notes__ should be a list of strings when notes are added + # via add_note, but can be anything else if __notes__ is set + # directly. We only support strings in __notes__, since that + # is the correct use. + notes: object = getattr(exc_value, "__notes__", None) + if isinstance(notes, list) and len(notes) > 0: + message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) + + return message + + +def single_exception_from_error_tuple( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: "Optional[int]" = None, + parent_id: "Optional[int]" = None, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "Dict[str, Any]": + """ + Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + """ + exception_value: "Dict[str, Any]" = {} + exception_value["mechanism"] = ( + mechanism.copy() if mechanism else {"type": "generic", "handled": True} + ) + if exception_id is not None: + exception_value["mechanism"]["exception_id"] = exception_id + + if exc_value is not None: + errno = get_errno(exc_value) + else: + errno = None + + if errno is not None: + exception_value["mechanism"].setdefault("meta", {}).setdefault( + "errno", {} + ).setdefault("number", errno) + + if source is not None: + exception_value["mechanism"]["source"] = source + + is_root_exception = exception_id == 0 + if not is_root_exception and parent_id is not None: + exception_value["mechanism"]["parent_id"] = parent_id + exception_value["mechanism"]["type"] = "chained" + + if is_root_exception and "type" not in exception_value["mechanism"]: + exception_value["mechanism"]["type"] = "generic" + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + if is_exception_group: + exception_value["mechanism"]["is_exception_group"] = True + + exception_value["module"] = get_type_module(exc_type) + exception_value["type"] = get_type_name(exc_type) + exception_value["value"] = get_error_message(exc_value) + + if client_options is None: + include_local_variables = True + include_source_context = True + max_value_length = None # fallback + custom_repr = None + else: + include_local_variables = client_options["include_local_variables"] + include_source_context = client_options["include_source_context"] + max_value_length = client_options["max_value_length"] + custom_repr = client_options.get("custom_repr") + + frames: "List[Dict[str, Any]]" = [ + serialize_frame( + tb.tb_frame, + tb_lineno=tb.tb_lineno, + include_local_variables=include_local_variables, + include_source_context=include_source_context, + max_value_length=max_value_length, + custom_repr=custom_repr, + ) + # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on + # processing a super-long stacktrace. + for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) + ] + + if len(frames) > MAX_STACK_FRAMES: + # If we have more frames than the limit, we remove the stacktrace completely. + # We don't trim the stacktrace here because we have not processed the whole + # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would + # intelligently trim by removing frames in the middle of the stacktrace, but + # since we don't have the whole stacktrace, we can't do that. Instead, we + # drop the entire stacktrace. + exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( + value=None + ) + + elif frames: + if not full_stack: + new_frames = frames + else: + new_frames = merge_stack_frames(frames, full_stack, client_options) + + exception_value["stacktrace"] = {"frames": new_frames} + + return exception_value + + +HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") + +if HAS_CHAINED_EXCEPTIONS: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + exc_type, exc_value, tb = exc_info + + seen_exceptions = [] + seen_exception_ids: "Set[int]" = set() + + while ( + exc_type is not None + and exc_value is not None + and id(exc_value) not in seen_exception_ids + ): + yield exc_type, exc_value, tb + + # Avoid hashing random types we don't know anything + # about. Use the list to keep a ref so that the `id` is + # not used for another object. + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + if exc_value.__suppress_context__: + cause = exc_value.__cause__ + else: + cause = exc_value.__context__ + if cause is None: + break + exc_type = type(cause) + exc_value = cause + tb = getattr(cause, "__traceback__", None) + +else: + + def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": + yield exc_info + + +def exceptions_from_error( + exc_type: "Optional[type]", + exc_value: "Optional[BaseException]", + tb: "Optional[TracebackType]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + exception_id: int = 0, + parent_id: int = 0, + source: "Optional[str]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, + seen_exceptions: "Optional[list[BaseException]]" = None, + seen_exception_ids: "Optional[Set[int]]" = None, +) -> "Tuple[int, List[Dict[str, Any]]]": + """ + Creates the list of exceptions. + This can include chained exceptions and exceptions from an ExceptionGroup. + + See the Exception Interface documentation for more details: + https://develop.sentry.dev/sdk/event-payloads/exception/ + + Args: + exception_id (int): + + Sequential counter for assigning ``mechanism.exception_id`` + to each processed exception. Is NOT the result of calling `id()` on the exception itself. + + parent_id (int): + + The ``mechanism.exception_id`` of the parent exception. + + Written into ``mechanism.parent_id`` in the event payload so Sentry can + reconstruct the exception tree. + + Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` + values for cycle detection. + """ + + if seen_exception_ids is None: + seen_exception_ids = set() + + if seen_exceptions is None: + seen_exceptions = [] + + if exc_value is not None and id(exc_value) in seen_exception_ids: + return (exception_id, []) + + if exc_value is not None: + seen_exceptions.append(exc_value) + seen_exception_ids.add(id(exc_value)) + + parent = single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source=source, + full_stack=full_stack, + ) + exceptions = [parent] + + parent_id = exception_id + exception_id += 1 + + should_supress_context = ( + hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore + ) + if should_supress_context: + # Add direct cause. + # The field `__cause__` is set when raised with the exception (using the `from` keyword). + exception_has_cause = ( + exc_value + and hasattr(exc_value, "__cause__") + and exc_value.__cause__ is not None + ) + if exception_has_cause: + cause = exc_value.__cause__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(cause), + exc_value=cause, + tb=getattr(cause, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__cause__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + else: + # Add indirect cause. + # The field `__context__` is assigned if another exception occurs while handling the exception. + exception_has_content = ( + exc_value + and hasattr(exc_value, "__context__") + and exc_value.__context__ is not None + ) + if exception_has_content: + context = exc_value.__context__ # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(context), + exc_value=context, + tb=getattr(context, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + source="__context__", + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + # Add exceptions from an ExceptionGroup. + is_exception_group = exc_value and hasattr(exc_value, "exceptions") + if is_exception_group: + for idx, e in enumerate(exc_value.exceptions): # type: ignore + (exception_id, child_exceptions) = exceptions_from_error( + exc_type=type(e), + exc_value=e, + tb=getattr(e, "__traceback__", None), + client_options=client_options, + mechanism=mechanism, + exception_id=exception_id, + parent_id=parent_id, + source="exceptions[%s]" % idx, + full_stack=full_stack, + seen_exceptions=seen_exceptions, + seen_exception_ids=seen_exception_ids, + ) + exceptions.extend(child_exceptions) + + return (exception_id, exceptions) + + +def exceptions_from_error_tuple( + exc_info: "ExcInfo", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, + full_stack: "Optional[list[dict[str, Any]]]" = None, +) -> "List[Dict[str, Any]]": + exc_type, exc_value, tb = exc_info + + is_exception_group = BaseExceptionGroup is not None and isinstance( + exc_value, BaseExceptionGroup + ) + + if is_exception_group: + (_, exceptions) = exceptions_from_error( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + exception_id=0, + parent_id=0, + full_stack=full_stack, + ) + + else: + exceptions = [] + for exc_type, exc_value, tb in walk_exception_chain(exc_info): + exceptions.append( + single_exception_from_error_tuple( + exc_type=exc_type, + exc_value=exc_value, + tb=tb, + client_options=client_options, + mechanism=mechanism, + full_stack=full_stack, + ) + ) + + exceptions.reverse() + + return exceptions + + +def to_string(value: str) -> str: + try: + return str(value) + except UnicodeDecodeError: + return repr(value)[1:-1] + + +def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": + if "stacktrace" in event: + yield event["stacktrace"] + if "threads" in event: + for thread in event["threads"].get("values") or (): + if "stacktrace" in thread: + yield thread["stacktrace"] + if "exception" in event: + for exception in event["exception"].get("values") or (): + if isinstance(exception, dict) and "stacktrace" in exception: + yield exception["stacktrace"] + + +def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + for frame in stacktrace.get("frames") or (): + yield frame + + +def handle_in_app( + event: "Event", + in_app_exclude: "Optional[List[str]]" = None, + in_app_include: "Optional[List[str]]" = None, + project_root: "Optional[str]" = None, +) -> "Event": + for stacktrace in iter_event_stacktraces(event): + if isinstance(stacktrace, AnnotatedValue): + stacktrace = stacktrace.value or {} + + set_in_app_in_frames( + stacktrace.get("frames"), + in_app_exclude=in_app_exclude, + in_app_include=in_app_include, + project_root=project_root, + ) + + return event + + +def set_in_app_in_frames( + frames: "Any", + in_app_exclude: "Optional[List[str]]", + in_app_include: "Optional[List[str]]", + project_root: "Optional[str]" = None, +) -> "Optional[Any]": + if not frames: + return None + + for frame in frames: + # if frame has already been marked as in_app, skip it + current_in_app = frame.get("in_app") + if current_in_app is not None: + continue + + module = frame.get("module") + + # check if module in frame is in the list of modules to include + if _module_in_list(module, in_app_include): + frame["in_app"] = True + continue + + # check if module in frame is in the list of modules to exclude + if _module_in_list(module, in_app_exclude): + frame["in_app"] = False + continue + + # if frame has no abs_path, skip further checks + abs_path = frame.get("abs_path") + if abs_path is None: + continue + + if _is_external_source(abs_path): + frame["in_app"] = False + continue + + if _is_in_project_root(abs_path, project_root): + frame["in_app"] = True + continue + + return frames + + +def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": + if isinstance(error, tuple) and len(error) == 3: + exc_type, exc_value, tb = error + elif isinstance(error, BaseException): + tb = getattr(error, "__traceback__", None) + if tb is not None: + exc_type = type(error) + exc_value = error + else: + exc_type, exc_value, tb = sys.exc_info() + if exc_value is not error: + tb = None + exc_value = error + exc_type = type(error) + + else: + raise ValueError("Expected Exception object to report, got %s!" % type(error)) + + exc_info = (exc_type, exc_value, tb) + + if TYPE_CHECKING: + # This cast is safe because exc_type and exc_value are either both + # None or both not None. + exc_info = cast(ExcInfo, exc_info) + + return exc_info + + +def merge_stack_frames( + frames: "List[Dict[str, Any]]", + full_stack: "List[Dict[str, Any]]", + client_options: "Optional[Dict[str, Any]]", +) -> "List[Dict[str, Any]]": + """ + Add the missing frames from full_stack to frames and return the merged list. + """ + frame_ids = { + ( + frame["abs_path"], + frame["context_line"], + frame["lineno"], + frame["function"], + ) + for frame in frames + } + + new_frames = [ + stackframe + for stackframe in full_stack + if ( + stackframe["abs_path"], + stackframe["context_line"], + stackframe["lineno"], + stackframe["function"], + ) + not in frame_ids + ] + new_frames.extend(frames) + + # Limit the number of frames + max_stack_frames = ( + client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) + if client_options + else None + ) + if max_stack_frames is not None: + new_frames = new_frames[len(new_frames) - max_stack_frames :] + + return new_frames + + +def event_from_exception( + exc_info: "Union[BaseException, ExcInfo]", + client_options: "Optional[Dict[str, Any]]" = None, + mechanism: "Optional[Dict[str, Any]]" = None, +) -> "Tuple[Event, Dict[str, Any]]": + exc_info = exc_info_from_error(exc_info) + hint = event_hint_with_exc_info(exc_info) + + if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): + full_stack = current_stacktrace( + include_local_variables=client_options["include_local_variables"], + max_value_length=client_options["max_value_length"], + )["frames"] + else: + full_stack = None + + return ( + { + "level": "error", + "exception": { + "values": exceptions_from_error_tuple( + exc_info, client_options, mechanism, full_stack + ) + }, + }, + hint, + ) + + +def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: + if name is None: + return False + + if not items: + return False + + for item in items: + if item == name or name.startswith(item + "."): + return True + + return False + + +def _is_external_source(abs_path: "Optional[str]") -> bool: + # check if frame is in 'site-packages' or 'dist-packages' + if abs_path is None: + return False + + external_source = ( + re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None + ) + return external_source + + +def _is_in_project_root( + abs_path: "Optional[str]", project_root: "Optional[str]" +) -> bool: + if abs_path is None or project_root is None: + return False + + # check if path is in the project root + if abs_path.startswith(project_root): + return True + + return False + + +def _truncate_by_bytes(string: str, max_bytes: int) -> str: + """ + Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. + """ + truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") + + return truncated + "..." + + +def _get_size_in_bytes(value: str) -> "Optional[int]": + try: + return len(value.encode("utf-8")) + except (UnicodeEncodeError, UnicodeDecodeError): + return None + + +def strip_string( + value: str, max_length: "Optional[int]" = None +) -> "Union[AnnotatedValue, str]": + if not value or max_length is None: + return value + + byte_size = _get_size_in_bytes(value) + text_size = len(value) + + if byte_size is not None and byte_size > max_length: + # truncate to max_length bytes, preserving code points + truncated_value = _truncate_by_bytes(value, max_length) + elif text_size is not None and text_size > max_length: + # fallback to truncating by string length + truncated_value = value[: max_length - 3] + "..." + else: + return value + + return AnnotatedValue( + value=truncated_value, + metadata={ + "len": byte_size or text_size, + "rem": [["!limit", "x", max_length - 3, max_length]], + }, + ) + + +def parse_version(version: str) -> "Optional[Tuple[int, ...]]": + """ + Parses a version string into a tuple of integers. + This uses the parsing loging from PEP 440: + https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + """ + VERSION_PATTERN = r""" # noqa: N806 + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+                [-_\.]?
+                (?P(a|b|c|rc|alpha|beta|pre|preview))
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+            (?P                                         # post release
+                (?:-(?P[0-9]+))
+                |
+                (?:
+                    [-_\.]?
+                    (?Ppost|rev|r)
+                    [-_\.]?
+                    (?P[0-9]+)?
+                )
+            )?
+            (?P                                          # dev release
+                [-_\.]?
+                (?Pdev)
+                [-_\.]?
+                (?P[0-9]+)?
+            )?
+        )
+        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+    """
+
+    pattern = re.compile(
+        r"^\s*" + VERSION_PATTERN + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
+
+    try:
+        release = pattern.match(version).groupdict()["release"]  # type: ignore
+        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
+    except (TypeError, ValueError, AttributeError):
+        return None
+
+    return release_tuple
+
+
+def _is_contextvars_broken() -> bool:
+    """
+    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
+    """
+    try:
+        import gevent
+        from gevent.monkey import is_object_patched
+
+        # Get the MAJOR and MINOR version numbers of Gevent
+        version_tuple = tuple(
+            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
+        )
+        if is_object_patched("threading", "local"):
+            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
+            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
+            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
+            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
+            # for contextvars, is able to patch both thread locals and contextvars, in
+            # that case, check if contextvars are effectively patched.
+            if (
+                # Gevent 20.9.0+
+                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
+                # Gevent 20.5.0+ or Python < 3.7
+                or (is_object_patched("contextvars", "ContextVar"))
+            ):
+                return False
+
+            return True
+    except ImportError:
+        pass
+
+    try:
+        import greenlet
+        from eventlet.patcher import is_monkey_patched  # type: ignore
+
+        greenlet_version = parse_version(greenlet.__version__)
+
+        if greenlet_version is None:
+            logger.error(
+                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
+            )
+            return False
+
+        if is_monkey_patched("thread") and greenlet_version < (0, 5):
+            return True
+    except ImportError:
+        pass
+
+    return False
+
+
+def _make_threadlocal_contextvars(local: type) -> type:
+    class ContextVar:
+        # Super-limited impl of ContextVar
+
+        def __init__(self, name: str, default: "Any" = None) -> None:
+            self._name = name
+            self._default = default
+            self._local = local()
+            self._original_local = local()
+
+        def get(self, default: "Any" = None) -> "Any":
+            return getattr(self._local, "value", default or self._default)
+
+        def set(self, value: "Any") -> "Any":
+            token = str(random.getrandbits(64))
+            original_value = self.get()
+            setattr(self._original_local, token, original_value)
+            self._local.value = value
+            return token
+
+        def reset(self, token: "Any") -> None:
+            self._local.value = getattr(self._original_local, token)
+            # delete the original value (this way it works in Python 3.6+)
+            del self._original_local.__dict__[token]
+
+    return ContextVar
+
+
+def _get_contextvars() -> "Tuple[bool, type]":
+    """
+    Figure out the "right" contextvars installation to use. Returns a
+    `contextvars.ContextVar`-like class with a limited API.
+
+    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
+    """
+    if not _is_contextvars_broken():
+        # aiocontextvars is a PyPI package that ensures that the contextvars
+        # backport (also a PyPI package) works with asyncio under Python 3.6
+        #
+        # Import it if available.
+        if sys.version_info < (3, 7):
+            # `aiocontextvars` is absolutely required for functional
+            # contextvars on Python 3.6.
+            try:
+                from aiocontextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+        else:
+            # On Python 3.7 contextvars are functional.
+            try:
+                from contextvars import ContextVar
+
+                return True, ContextVar
+            except ImportError:
+                pass
+
+    # Fall back to basic thread-local usage.
+
+    from threading import local
+
+    return False, _make_threadlocal_contextvars(local)
+
+
+HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
+
+CONTEXTVARS_ERROR_MESSAGE = """
+
+With asyncio/ASGI applications, the Sentry SDK requires a functional
+installation of `contextvars` to avoid leaking scope/context data across
+requests.
+
+Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
+"""
+
+_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
+
+# These exceptions won't set the span status to error if they occur. Use
+# register_control_flow_exception to add to this list
+_control_flow_exception_classes: "set[type]" = set()
+
+
+def is_internal_task() -> bool:
+    return _is_sentry_internal_task.get()
+
+
+@contextmanager
+def mark_sentry_task_internal() -> "Generator[None, None, None]":
+    """Context manager to mark a task as Sentry internal."""
+    token = _is_sentry_internal_task.set(True)
+    try:
+        yield
+    finally:
+        _is_sentry_internal_task.reset(token)
+
+
+def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
+    func_qualname: "Optional[str]" = None
+
+    prefix, suffix = "", ""
+
+    if isinstance(func, partial) and hasattr(func.func, "__name__"):
+        prefix, suffix = "partial()"
+        func = func.func
+    else:
+        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
+        # https://github.com/python/cpython/pull/16600
+        partial_method = getattr(func, "_partialmethod", None) or getattr(
+            func, "__partialmethod__", None
+        )
+        if isinstance(partial_method, partialmethod):
+            prefix, suffix = "partialmethod()"
+            func = partial_method.func
+
+    if hasattr(func, "__qualname__"):
+        func_qualname = func.__qualname__
+    elif hasattr(func, "__name__"):
+        func_qualname = func.__name__
+
+    if func_qualname is not None:
+        if hasattr(func, "__module__") and isinstance(func.__module__, str):
+            func_qualname = func.__module__ + "." + func_qualname
+        func_qualname = prefix + func_qualname + suffix
+
+    return func_qualname
+
+
+def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
+    return qualname_from_function(func)
+
+
+disable_capture_event = ContextVar("disable_capture_event")
+
+
+class ServerlessTimeoutWarning(Exception):  # noqa: N818
+    """Raised when a serverless method is about to reach its timeout."""
+
+    pass
+
+
+class TimeoutThread(threading.Thread):
+    """Creates a Thread which runs (sleeps) for a time duration equal to
+    waiting_time and raises a custom ServerlessTimeout exception.
+    """
+
+    def __init__(
+        self,
+        waiting_time: float,
+        configured_timeout: int,
+        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
+        current_scope: "Optional[sentry_sdk.Scope]" = None,
+    ) -> None:
+        threading.Thread.__init__(self)
+        self.waiting_time = waiting_time
+        self.configured_timeout = configured_timeout
+
+        self.isolation_scope = isolation_scope
+        self.current_scope = current_scope
+
+        self._stop_event = threading.Event()
+
+    def stop(self) -> None:
+        self._stop_event.set()
+
+    def _capture_exception(self) -> "ExcInfo":
+        exc_info = sys.exc_info()
+
+        client = sentry_sdk.get_client()
+        event, hint = event_from_exception(
+            exc_info,
+            client_options=client.options,
+            mechanism={"type": "threading", "handled": False},
+        )
+        sentry_sdk.capture_event(event, hint=hint)
+
+        return exc_info
+
+    def run(self) -> None:
+        self._stop_event.wait(self.waiting_time)
+
+        if self._stop_event.is_set():
+            return
+
+        integer_configured_timeout = int(self.configured_timeout)
+
+        # Setting up the exact integer value of configured time(in seconds)
+        if integer_configured_timeout < self.configured_timeout:
+            integer_configured_timeout = integer_configured_timeout + 1
+
+        # Raising Exception after timeout duration is reached
+        if self.isolation_scope is not None and self.current_scope is not None:
+            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
+                with sentry_sdk.scope.use_scope(self.current_scope):
+                    try:
+                        raise ServerlessTimeoutWarning(
+                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                                integer_configured_timeout
+                            )
+                        )
+                    except Exception:
+                        reraise(*self._capture_exception())
+
+        raise ServerlessTimeoutWarning(
+            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
+                integer_configured_timeout
+            )
+        )
+
+
+def to_base64(original: str) -> "Optional[str]":
+    """
+    Convert a string to base64, via UTF-8. Returns None on invalid input.
+    """
+    base64_string = None
+
+    try:
+        utf8_bytes = original.encode("UTF-8")
+        base64_bytes = base64.b64encode(utf8_bytes)
+        base64_string = base64_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
+
+    return base64_string
+
+
+def from_base64(base64_string: str) -> "Optional[str]":
+    """
+    Convert a string from base64, via UTF-8. Returns None on invalid input.
+    """
+    utf8_string = None
+
+    try:
+        only_valid_chars = BASE64_ALPHABET.match(base64_string)
+        assert only_valid_chars
+
+        base64_bytes = base64_string.encode("UTF-8")
+        utf8_bytes = base64.b64decode(base64_bytes)
+        utf8_string = utf8_bytes.decode("UTF-8")
+    except Exception as err:
+        logger.warning(
+            "Unable to decode {b64} from base64:".format(b64=base64_string), err
+        )
+
+    return utf8_string
+
+
+Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
+
+
+def sanitize_url(
+    url: str,
+    remove_authority: bool = True,
+    remove_query_values: bool = True,
+    split: bool = False,
+) -> "Union[str, Components]":
+    """
+    Removes the authority and query parameter values from a given URL.
+    """
+    parsed_url = urlsplit(url)
+    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
+
+    # strip username:password (netloc can be usr:pwd@example.com)
+    if remove_authority:
+        netloc_parts = parsed_url.netloc.split("@")
+        if len(netloc_parts) > 1:
+            netloc = "%s:%s@%s" % (
+                SENSITIVE_DATA_SUBSTITUTE,
+                SENSITIVE_DATA_SUBSTITUTE,
+                netloc_parts[-1],
+            )
+        else:
+            netloc = parsed_url.netloc
+    else:
+        netloc = parsed_url.netloc
+
+    # strip values from query string
+    if remove_query_values:
+        query_string = unquote(
+            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
+        )
+    else:
+        query_string = parsed_url.query
+
+    components = Components(
+        scheme=parsed_url.scheme,
+        netloc=netloc,
+        query=query_string,
+        path=parsed_url.path,
+        fragment=parsed_url.fragment,
+    )
+
+    if split:
+        return components
+    else:
+        return urlunsplit(components)
+
+
+ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
+
+
+def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
+    """
+    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
+    parameters will be sanitized to remove sensitive data. The autority (username and password)
+    in the URL will always be removed.
+    """
+    parsed_url = sanitize_url(
+        url, remove_authority=True, remove_query_values=sanitize, split=True
+    )
+
+    base_url = urlunsplit(
+        Components(
+            scheme=parsed_url.scheme,  # type: ignore
+            netloc=parsed_url.netloc,  # type: ignore
+            query="",
+            path=parsed_url.path,  # type: ignore
+            fragment="",
+        )
+    )
+
+    return ParsedUrl(
+        url=base_url,
+        query=parsed_url.query,  # type: ignore
+        fragment=parsed_url.fragment,  # type: ignore
+    )
+
+
+def is_valid_sample_rate(rate: "Any", source: str) -> bool:
+    """
+    Checks the given sample rate to make sure it is valid type and value (a
+    boolean or a number between 0 and 1, inclusive).
+    """
+
+    # both booleans and NaN are instances of Real, so a) checking for Real
+    # checks for the possibility of a boolean also, and b) we have to check
+    # separately for NaN and Decimal does not derive from Real so need to check that too
+    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
+                source=source, rate=rate, type=type(rate)
+            )
+        )
+        return False
+
+    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
+    rate = float(rate)
+    if rate < 0 or rate > 1:
+        logger.warning(
+            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
+                source=source, rate=rate
+            )
+        )
+        return False
+
+    return True
+
+
+def match_regex_list(
+    item: str,
+    regex_list: "Optional[List[str]]" = None,
+    substring_matching: bool = False,
+) -> bool:
+    if regex_list is None:
+        return False
+
+    for item_matcher in regex_list:
+        if not substring_matching and item_matcher[-1] != "$":
+            item_matcher += "$"
+
+        matched = re.search(item_matcher, item)
+        if matched:
+            return True
+
+    return False
+
+
+def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
+    """
+    Determines whether the given URL matches the Sentry DSN.
+    """
+    return (
+        client is not None
+        and client.transport is not None
+        and client.transport.parsed_dsn is not None
+        and client.transport.parsed_dsn.netloc in url
+    )
+
+
+def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
+    try:
+        from importlib import metadata
+
+        yielded = set()
+        for dist in metadata.distributions():
+            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
+            # `metadata` values may be `None`, see:
+            # https://github.com/python/cpython/issues/91216
+            # and
+            # https://github.com/python/importlib_metadata/issues/371
+            if name is not None:
+                normalized_name = _normalize_module_name(name)
+                if dist.version is not None and normalized_name not in yielded:
+                    yield normalized_name, dist.version
+                    yielded.add(normalized_name)
+
+    except ImportError:
+        # < py3.8
+        try:
+            import pkg_resources
+        except ImportError:
+            return
+
+        for info in pkg_resources.working_set:
+            yield _normalize_module_name(info.key), info.version
+
+
+def _normalize_module_name(name: str) -> str:
+    return name.lower()
+
+
+def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
+    # https://peps.python.org/pep-0503/#normalized-names
+    return re.sub(r"[-_.]+", "-", name)
+
+
+def _get_installed_modules() -> "Dict[str, str]":
+    global _installed_modules
+    if _installed_modules is None:
+        _installed_modules = dict(_generate_installed_modules())
+    return _installed_modules
+
+
+def package_version(package: str) -> "Optional[Tuple[int, ...]]":
+    normalized_package = _normalize_module_name(
+        _replace_hyphens_dots_and_underscores_with_dashes(package)
+    )
+
+    installed_packages = {
+        _replace_hyphens_dots_and_underscores_with_dashes(module): v
+        for module, v in _get_installed_modules().items()
+    }
+    version = installed_packages.get(normalized_package)
+    if version is None:
+        return None
+
+    return parse_version(version)
+
+
+def reraise(
+    tp: "Optional[Type[BaseException]]",
+    value: "Optional[BaseException]",
+    tb: "Optional[Any]" = None,
+) -> "NoReturn":
+    assert value is not None
+    if value.__traceback__ is not tb:
+        raise value.with_traceback(tb)
+    raise value
+
+
+def _no_op(*_a: "Any", **_k: "Any") -> None:
+    """No-op function for ensure_integration_enabled."""
+    pass
+
+
+if TYPE_CHECKING:
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+        original_function: "Callable[P, R]",
+    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
+
+    @overload
+    def ensure_integration_enabled(
+        integration: "type[sentry_sdk.integrations.Integration]",
+    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
+
+
+def ensure_integration_enabled(
+    integration: "type[sentry_sdk.integrations.Integration]",
+    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
+) -> "Callable[[Callable[P, R]], Callable[P, R]]":
+    """
+    Ensures a given integration is enabled prior to calling a Sentry-patched function.
+
+    The function takes as its parameters the integration that must be enabled and the original
+    function that the SDK is patching. The function returns a function that takes the
+    decorated (Sentry-patched) function as its parameter, and returns a function that, when
+    called, checks whether the given integration is enabled. If the integration is enabled, the
+    function calls the decorated, Sentry-patched function. If the integration is not enabled,
+    the original function is called.
+
+    The function also takes care of preserving the original function's signature and docstring.
+
+    Example usage:
+
+    ```python
+    @ensure_integration_enabled(MyIntegration, my_function)
+    def patch_my_function():
+        with sentry_sdk.start_transaction(...):
+            return my_function()
+    ```
+    """
+    if TYPE_CHECKING:
+        # Type hint to ensure the default function has the right typing. The overloads
+        # ensure the default _no_op function is only used when R is None.
+        original_function = cast(Callable[P, R], original_function)
+
+    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
+        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
+            if sentry_sdk.get_client().get_integration(integration) is None:
+                return original_function(*args, **kwargs)
+
+            return sentry_patched_function(*args, **kwargs)
+
+        if original_function is _no_op:
+            return wraps(sentry_patched_function)(runner)
+
+        return wraps(original_function)(runner)
+
+    return patcher
+
+
+if PY37:
+
+    def nanosecond_time() -> int:
+        return time.perf_counter_ns()
+
+else:
+
+    def nanosecond_time() -> int:
+        return int(time.perf_counter() * 1e9)
+
+
+def now() -> float:
+    return time.perf_counter()
+
+
+try:
+    from gevent import get_hub as get_gevent_hub
+    from gevent.monkey import is_module_patched
+except ImportError:
+    # it's not great that the signatures are different, get_hub can't return None
+    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
+    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
+        return None
+
+    def is_module_patched(mod_name: str) -> bool:
+        # unable to import from gevent means no modules have been patched
+        return False
+
+
+def is_gevent() -> bool:
+    return is_module_patched("threading") or is_module_patched("_thread")
+
+
+def get_current_thread_meta(
+    thread: "Optional[threading.Thread]" = None,
+) -> "Tuple[Optional[int], Optional[str]]":
+    """
+    Try to get the id of the current thread, with various fall backs.
+    """
+
+    # if a thread is specified, that takes priority
+    if thread is not None:
+        try:
+            thread_id = thread.ident
+            thread_name = thread.name
+            if thread_id is not None:
+                return thread_id, thread_name
+        except AttributeError:
+            pass
+
+    # if the app is using gevent, we should look at the gevent hub first
+    # as the id there differs from what the threading module reports
+    if is_gevent():
+        gevent_hub = get_gevent_hub()
+        if gevent_hub is not None:
+            try:
+                # this is undocumented, so wrap it in try except to be safe
+                return gevent_hub.thread_ident, None
+            except AttributeError:
+                pass
+
+    # use the current thread's id if possible
+    try:
+        thread = threading.current_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # if we can't get the current thread id, fall back to the main thread id
+    try:
+        thread = threading.main_thread()
+        thread_id = thread.ident
+        thread_name = thread.name
+        if thread_id is not None:
+            return thread_id, thread_name
+    except AttributeError:
+        pass
+
+    # we've tried everything, time to give up
+    return None, None
+
+
+def _register_control_flow_exception(
+    exc_type: "Union[type, list[type], tuple[type], set[type]]",
+) -> None:
+    if isinstance(exc_type, (list, tuple, set)):
+        _control_flow_exception_classes.update(exc_type)
+    else:
+        _control_flow_exception_classes.add(exc_type)
+
+
+def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
+    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
+        # https://docs.python.org/3/library/exceptions.html#SystemExit
+        return False
+
+    if issubclass(ty, tuple(_control_flow_exception_classes)):
+        return False
+
+    return True
+
+
+if TYPE_CHECKING:
+    T = TypeVar("T")
+
+
+def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
+    """
+    Attempt to convert from an unknown type to a specific type, using the
+    given function. Return None if the conversion fails, i.e. if the function
+    raises an exception.
+    """
+    try:
+        if isinstance(value, convert_func):  # type: ignore
+            return value
+    except TypeError:
+        pass
+
+    try:
+        return convert_func(value)
+    except Exception:
+        return None
+
+
+def safe_serialize(data: "Any") -> str:
+    """Safely serialize to a readable string."""
+
+    def serialize_item(
+        item: "Any",
+    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
+        if callable(item):
+            try:
+                module = getattr(item, "__module__", None)
+                qualname = getattr(item, "__qualname__", None)
+                name = getattr(item, "__name__", "anonymous")
+
+                if module and qualname:
+                    full_path = f"{module}.{qualname}"
+                elif module and name:
+                    full_path = f"{module}.{name}"
+                else:
+                    full_path = name
+
+                return f""
+            except Exception:
+                return f""
+        elif isinstance(item, dict):
+            return {k: serialize_item(v) for k, v in item.items()}
+        elif isinstance(item, (list, tuple)):
+            return [serialize_item(x) for x in item]
+        elif hasattr(item, "__dict__"):
+            try:
+                attrs = {
+                    k: serialize_item(v)
+                    for k, v in vars(item).items()
+                    if not k.startswith("_")
+                }
+                return f"<{type(item).__name__} {attrs}>"
+            except Exception:
+                return repr(item)
+        else:
+            return item
+
+    try:
+        serialized = serialize_item(data)
+        return (
+            json.dumps(serialized, default=str)
+            if not isinstance(serialized, str)
+            else serialized
+        )
+    except Exception:
+        return str(data)
+
+
+def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(
+        options.get("enable_logs", False)
+        or options["_experiments"].get("enable_logs", False)
+    )
+
+
+def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return "data_collection" in options.get("_experiments", {})
+
+
+def get_before_send_log(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_log") or options["_experiments"].get(
+        "before_send_log"
+    )
+
+
+def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
+    if options is None:
+        return False
+
+    return bool(options.get("enable_metrics", True))
+
+
+def get_before_send_metric(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
+    if options is None:
+        return None
+
+    return options.get("before_send_metric") or options["_experiments"].get(
+        "before_send_metric"
+    )
+
+
+def get_before_send_span(
+    options: "Optional[dict[str, Any]]",
+) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
+    if options is None:
+        return None
+
+    return options["_experiments"].get("before_send_span")
+
+
+def format_attribute(val: "Any") -> "AttributeValue":
+    """
+    Turn unsupported attribute value types into an AttributeValue.
+
+    We do this as soon as a user-provided attribute is set, to prevent spans,
+    logs, metrics and similar from having live references to various objects.
+
+    Note: This is not the final attribute value format. Before they're sent,
+    they're serialized further into the actual format the protocol expects:
+    https://develop.sentry.dev/sdk/telemetry/attributes/
+    """
+    if isinstance(val, (bool, int, float, str)):
+        return val
+
+    if isinstance(val, (list, tuple)) and not val:
+        return []
+    elif isinstance(val, list):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return copy.deepcopy(val)
+    elif isinstance(val, tuple):
+        ty = type(val[0])
+        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
+            return list(val)
+
+    return safe_repr(val)
+
+
+def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
+    """Serialize attribute value to the transport format."""
+    if isinstance(val, bool):
+        return {"value": val, "type": "boolean"}
+    if isinstance(val, int):
+        return {"value": val, "type": "integer"}
+    if isinstance(val, float):
+        return {"value": val, "type": "double"}
+    if isinstance(val, str):
+        return {"value": val, "type": "string"}
+
+    if isinstance(val, list):
+        if not val:
+            return {"value": [], "type": "array"}
+
+        # Only lists of elements of a single type are supported
+        ty = type(val[0])
+        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
+            return {"value": val, "type": "array"}
+
+    # Coerce to string if we don't know what to do with the value. This should
+    # never happen as we pre-format early in format_attribute, but let's be safe.
+    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py
new file mode 100644
index 0000000000..5eb9b23130
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py
@@ -0,0 +1,308 @@
+import asyncio
+import os
+import threading
+from abc import ABC, abstractmethod
+from time import sleep, time
+from typing import TYPE_CHECKING
+
+from sentry_sdk._queue import FullError, Queue
+from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
+from sentry_sdk.utils import logger, mark_sentry_task_internal
+
+if TYPE_CHECKING:
+    from typing import Any, Callable, Optional
+
+
+_TERMINATOR = object()
+
+
+class Worker(ABC):
+    """Base class for all workers."""
+
+    @property
+    @abstractmethod
+    def is_alive(self) -> bool:
+        """Whether the worker is alive and running."""
+        pass
+
+    @abstractmethod
+    def kill(self) -> None:
+        """Kill the worker. It will not process any more events."""
+        pass
+
+    def flush(
+        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
+    ) -> None:
+        """Flush the worker, blocking until done or timeout is reached."""
+        return None
+
+    @abstractmethod
+    def full(self) -> bool:
+        """Whether the worker's queue is full."""
+        pass
+
+    @abstractmethod
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        """Schedule a callback. Returns True if queued, False if full."""
+        pass
+
+
+class BackgroundWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Queue" = Queue(queue_size)
+        self._lock = threading.Lock()
+        self._thread: "Optional[threading.Thread]" = None
+        self._thread_for_pid: "Optional[int]" = None
+
+    @property
+    def is_alive(self) -> bool:
+        if self._thread_for_pid != os.getpid():
+            return False
+        if not self._thread:
+            return False
+        return self._thread.is_alive()
+
+    def _ensure_thread(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    def _timed_queue_join(self, timeout: float) -> bool:
+        deadline = time() + timeout
+        queue = self._queue
+
+        queue.all_tasks_done.acquire()
+
+        try:
+            while queue.unfinished_tasks:
+                delay = deadline - time()
+                if delay <= 0:
+                    return False
+                queue.all_tasks_done.wait(timeout=delay)
+
+            return True
+        finally:
+            queue.all_tasks_done.release()
+
+    def start(self) -> None:
+        with self._lock:
+            if not self.is_alive:
+                self._thread = threading.Thread(
+                    target=self._target, name="sentry-sdk.BackgroundWorker"
+                )
+                self._thread.daemon = True
+                try:
+                    self._thread.start()
+                    self._thread_for_pid = os.getpid()
+                except RuntimeError:
+                    # At this point we can no longer start because the interpreter
+                    # is already shutting down.  Sadly at this point we can no longer
+                    # send out events.
+                    self._thread = None
+
+    def kill(self) -> None:
+        """
+        Kill worker thread. Returns immediately. Not useful for
+        waiting on shutdown for events, use `flush` for that.
+        """
+        logger.debug("background worker got kill request")
+        with self._lock:
+            if self._thread:
+                try:
+                    self._queue.put_nowait(_TERMINATOR)
+                except FullError:
+                    logger.debug("background worker queue full, kill failed")
+
+                self._thread = None
+                self._thread_for_pid = None
+
+    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
+        logger.debug("background worker got flush request")
+        with self._lock:
+            if self.is_alive and timeout > 0.0:
+                self._wait_flush(timeout, callback)
+        logger.debug("background worker flushed")
+
+    def full(self) -> bool:
+        return self._queue.full()
+
+    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
+        initial_timeout = min(0.1, timeout)
+        if not self._timed_queue_join(initial_timeout):
+            pending = self._queue.qsize() + 1
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            if not self._timed_queue_join(timeout - initial_timeout):
+                pending = self._queue.qsize() + 1
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_thread()
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except FullError:
+            return False
+
+    def _target(self) -> None:
+        while True:
+            callback = self._queue.get()
+            try:
+                if callback is _TERMINATOR:
+                    break
+                try:
+                    callback()
+                except Exception:
+                    logger.error("Failed processing job", exc_info=True)
+            finally:
+                self._queue.task_done()
+            sleep(0)
+
+
+class AsyncWorker(Worker):
+    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
+        self._queue: "Optional[asyncio.Queue[Any]]" = None
+        self._queue_size = queue_size
+        self._task: "Optional[asyncio.Task[None]]" = None
+        # Event loop needs to remain in the same process
+        self._task_for_pid: "Optional[int]" = None
+        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
+        # Track active callback tasks so they have a strong reference and can be cancelled on kill
+        self._active_tasks: "set[asyncio.Task[None]]" = set()
+
+    @property
+    def is_alive(self) -> bool:
+        if self._task_for_pid != os.getpid():
+            return False
+        if not self._task or not self._loop:
+            return False
+        return self._loop.is_running() and not self._task.done()
+
+    def kill(self) -> None:
+        if self._task:
+            # Cancel the main consumer task to prevent duplicate consumers
+            self._task.cancel()
+            # Also cancel any active callback tasks
+            # Avoid modifying the set while cancelling tasks
+            tasks_to_cancel = set(self._active_tasks)
+            for task in tasks_to_cancel:
+                task.cancel()
+            self._active_tasks.clear()
+            self._loop = None
+            self._task = None
+            self._task_for_pid = None
+
+    def start(self) -> None:
+        if not self.is_alive:
+            try:
+                self._loop = asyncio.get_running_loop()
+                # Always create a fresh queue on start to avoid stale items
+                self._queue = asyncio.Queue(maxsize=self._queue_size)
+                with mark_sentry_task_internal():
+                    self._task = self._loop.create_task(self._target())
+                self._task_for_pid = os.getpid()
+            except RuntimeError:
+                # There is no event loop running
+                logger.warning("No event loop running, async worker not started")
+                self._loop = None
+                self._task = None
+                self._task_for_pid = None
+
+    def full(self) -> bool:
+        if self._queue is None:
+            return True
+        return self._queue.full()
+
+    def _ensure_task(self) -> None:
+        if not self.is_alive:
+            self.start()
+
+    async def _wait_flush(
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> None:
+        if not self._loop or not self._loop.is_running() or self._queue is None:
+            return
+
+        initial_timeout = min(0.1, timeout)
+
+        # Timeout on the join
+        try:
+            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
+        except asyncio.TimeoutError:
+            pending = self._queue.qsize() + len(self._active_tasks)
+            logger.debug("%d event(s) pending on flush", pending)
+            if callback is not None:
+                callback(pending, timeout)
+
+            try:
+                remaining_timeout = timeout - initial_timeout
+                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
+            except asyncio.TimeoutError:
+                pending = self._queue.qsize() + len(self._active_tasks)
+                logger.error("flush timed out, dropped %s events", pending)
+
+    def flush(  # type: ignore[override]
+        self, timeout: float, callback: "Optional[Any]" = None
+    ) -> "Optional[asyncio.Task[None]]":
+        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
+            with mark_sentry_task_internal():
+                return self._loop.create_task(self._wait_flush(timeout, callback))
+        return None
+
+    def submit(self, callback: "Callable[[], Any]") -> bool:
+        self._ensure_task()
+        if self._queue is None:
+            return False
+        try:
+            self._queue.put_nowait(callback)
+            return True
+        except asyncio.QueueFull:
+            return False
+
+    async def _target(self) -> None:
+        if self._queue is None:
+            return
+        try:
+            while True:
+                callback = await self._queue.get()
+                if callback is _TERMINATOR:
+                    self._queue.task_done()
+                    break
+                # Firing tasks instead of awaiting them allows for concurrent requests
+                with mark_sentry_task_internal():
+                    task = asyncio.create_task(self._process_callback(callback))
+                # Create a strong reference to the task so it can be cancelled on kill
+                # and does not get garbage collected while running
+                self._active_tasks.add(task)
+                # Capture queue ref at dispatch time so done callbacks use the
+                # correct queue even if kill()/start() replace self._queue.
+                queue_ref = self._queue
+                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
+                # Yield to let the event loop run other tasks
+                await asyncio.sleep(0)
+        except asyncio.CancelledError:
+            pass  # Expected during kill()
+
+    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
+        # Callback is an async coroutine, need to await it
+        await callback()
+
+    def _on_task_complete(
+        self,
+        task: "asyncio.Task[None]",
+        queue: "Optional[asyncio.Queue[Any]]" = None,
+    ) -> None:
+        try:
+            task.result()
+        except asyncio.CancelledError:
+            pass  # Task was cancelled, expected during shutdown
+        except Exception:
+            logger.error("Failed processing job", exc_info=True)
+        finally:
+            # Mark the task as done and remove it from the active tasks set
+            # Use the queue reference captured at dispatch time, not self._queue,
+            # to avoid calling task_done() on a different queue after kill()/start().
+            if queue is not None:
+                queue.task_done()
+            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER
new file mode 100644
index 0000000000..5c69047b2e
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA
new file mode 100644
index 0000000000..9c7a4703f8
--- /dev/null
+++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA
@@ -0,0 +1,163 @@
+Metadata-Version: 2.4
+Name: urllib3
+Version: 2.7.0
+Summary: HTTP library with thread-safe connection pooling, file post, and more.
+Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
+Project-URL: Documentation, https://urllib3.readthedocs.io
+Project-URL: Code, https://github.com/urllib3/urllib3
+Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
+Author-email: Andrey Petrov 
+Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
+License-Expression: MIT
+License-File: LICENSE.txt
+Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Software Development :: Libraries
+Requires-Python: >=3.10
+Provides-Extra: brotli
+Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
+Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
+Provides-Extra: h2
+Requires-Dist: h2<5,>=4; extra == 'h2'
+Provides-Extra: socks
+Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
+Provides-Extra: zstd
+Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
+Description-Content-Type: text/markdown
+
+

+ +![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) + +

+ +

+ PyPI Version + Python Versions + Join our Discord + Coverage Status + Build Status on GitHub + Documentation Status
+ OpenSSF Scorecard + SLSA 3 + CII Best Practices +

+ +urllib3 is a powerful, *user-friendly* HTTP client for Python. +urllib3 brings many critical features that are missing from the Python +standard libraries: + +- Thread safety. +- Connection pooling. +- Client-side SSL/TLS verification. +- File uploads with multipart encoding. +- Helpers for retrying requests and dealing with HTTP redirects. +- Support for gzip, deflate, brotli, and zstd encoding. +- Proxy support for HTTP and SOCKS. +- 100% test coverage. + +... and many more features, but most importantly: Our maintainers have a 15+ +year track record of maintaining urllib3 with the highest code standards and +attention to security and safety. + +[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) +and you should too. + + +## Installing + +urllib3 can be installed with [pip](https://pip.pypa.io): + +```bash +$ python -m pip install urllib3 +``` + +Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): + +```bash +$ git clone https://github.com/urllib3/urllib3.git +$ cd urllib3 +$ pip install . +``` + +## Getting Started + +urllib3 is easy to use: + +```python3 +>>> import urllib3 +>>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") +>>> resp.status +200 +>>> resp.data +b"User-agent: *\nDisallow: /deny\n" +``` + +urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). + + +## Community + +urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and +collaborating with other contributors. Drop by and say hello 👋 + + +## Contributing + +urllib3 happily accepts contributions. Please see our +[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) +for some tips on getting started. + + +## Security Disclosures + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure with maintainers. + + +## Maintainers + +Meet our maintainers since 2008: + +- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) +- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) +- [@pquentin](https://github.com/pquentin) (Quentin Pradet) +- [@theacodes](https://github.com/theacodes) (Thea Flowers) +- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) +- [@lukasa](https://github.com/lukasa) (Cory Benfield) +- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) +- [@shazow](https://github.com/shazow) (Andrey Petrov) + +👋 + + +## Sponsorship + +If your company benefits from this library, please consider [sponsoring its +development](https://urllib3.readthedocs.io/en/latest/sponsors.html). + + +## For Enterprise + +Professional support for urllib3 is available as part of the [Tidelift +Subscription][1]. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD new file mode 100644 index 0000000000..8c4f9c0b4b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD @@ -0,0 +1,44 @@ +urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 +urllib3-2.7.0.dist-info/RECORD,, +urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 +urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 +urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 +urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 +urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 +urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 +urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 +urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 +urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 +urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 +urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 +urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 +urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 +urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 +urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 +urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 +urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 +urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 +urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 +urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 +urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 +urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 +urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 +urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 +urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 +urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 +urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 +urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 +urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 +urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 +urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 +urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 +urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 +urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 +urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 +urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 +urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 +urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 +urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/REQUESTED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL new file mode 100644 index 0000000000..b1b94fd58e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000..e6183d0276 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py new file mode 100644 index 0000000000..3fe782c8a4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py @@ -0,0 +1,211 @@ +""" +Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more +""" + +from __future__ import annotations + +# Set default logging handler to avoid "No handler found" warnings. +import logging +import sys +import typing +import warnings +from logging import NullHandler + +from . import exceptions +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._version import __version__ +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import BaseHTTPResponse, HTTPResponse +from .util.request import make_headers +from .util.retry import Retry +from .util.timeout import Timeout + +# Ensure that Python is compiled with OpenSSL 1.1.1+ +# If the 'ssl' module isn't available at all that's +# fine, we only care if the module is available. +try: + import ssl +except ImportError: + pass +else: + if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: + warnings.warn( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/3020", + exceptions.NotOpenSSLWarning, + ) + elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: + raise ImportError( + "urllib3 v2 only supports OpenSSL 1.1.1+, currently " + f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " + "See: https://github.com/urllib3/urllib3/issues/2168" + ) + +__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" +__license__ = "MIT" +__version__ = __version__ + +__all__ = ( + "HTTPConnectionPool", + "HTTPHeaderDict", + "HTTPSConnectionPool", + "PoolManager", + "ProxyManager", + "HTTPResponse", + "Retry", + "Timeout", + "add_stderr_logger", + "connection_from_url", + "disable_warnings", + "encode_multipart_formdata", + "make_headers", + "proxy_from_url", + "request", + "BaseHTTPResponse", +) + +logging.getLogger(__name__).addHandler(NullHandler()) + + +def add_stderr_logger( + level: int = logging.DEBUG, +) -> logging.StreamHandler[typing.TextIO]: + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug("Added a stderr logging handler to logger: %s", __name__) + return handler + + +# ... Clean up. +del NullHandler + + +# All warning filters *must* be appended unless you're really certain that they +# shouldn't be: otherwise, it's very hard for users to use most Python +# mechanisms to silence them. +# SecurityWarning's always go off by default. +warnings.simplefilter("always", exceptions.SecurityWarning, append=True) +# InsecurePlatformWarning's don't vary between requests, so we keep it default. +warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) + + +def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: + """ + Helper for quickly disabling all urllib3 warnings. + """ + warnings.simplefilter("ignore", category) + + +_DEFAULT_POOL = PoolManager() + + +def request( + method: str, + url: str, + *, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + preload_content: bool | None = True, + decode_content: bool | None = True, + redirect: bool | None = True, + retries: Retry | bool | int | None = None, + timeout: Timeout | float | int | None = 3, + json: typing.Any | None = None, +) -> BaseHTTPResponse: + """ + A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. + Therefore, its side effects could be shared across dependencies relying on it. + To avoid side effects create a new ``PoolManager`` instance and use it instead. + The method does not accept low-level ``**urlopen_kw`` keyword arguments. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + + return _DEFAULT_POOL.request( + method, + url, + body=body, + fields=fields, + headers=headers, + preload_content=preload_content, + decode_content=decode_content, + redirect=redirect, + retries=retries, + timeout=timeout, + json=json, + ) + + +if sys.platform == "emscripten": + from .contrib.emscripten import inject_into_urllib3 # noqa: 401 + + inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py new file mode 100644 index 0000000000..992ec1657a --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import typing + +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from .util.url import Url + +_TYPE_BODY = typing.Union[ + bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str +] + + +class ProxyConfig(typing.NamedTuple): + ssl_context: ssl.SSLContext | None + use_forwarding_for_https: bool + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + + +class _ResponseOptions(typing.NamedTuple): + # TODO: Remove this in favor of a better + # HTTP request/response lifecycle tracking. + request_method: str + request_url: str + preload_content: bool + decode_content: bool + enforce_content_length: bool + + +if typing.TYPE_CHECKING: + import ssl + from typing import Protocol + + from .response import BaseHTTPResponse + + class BaseHTTPConnection(Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + host: str + port: int + timeout: None | ( + float + ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool + proxy_is_verified: bool | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: ... + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: ... + + def connect(self) -> None: ... + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: ... + + def getresponse(self) -> BaseHTTPResponse: ... + + def close(self) -> None: ... + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + + class BaseHTTPSConnection(BaseHTTPConnection, Protocol): + default_port: typing.ClassVar[int] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + # Certificate verification methods + cert_reqs: int | str | None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None + ssl_context: ssl.SSLContext | None + + # Trusted CAs + ca_certs: str | None + ca_cert_dir: str | None + ca_cert_data: None | str | bytes + + # TLS version + ssl_minimum_version: int | None + ssl_maximum_version: int | None + ssl_version: int | str | None # Deprecated + + # Client certificates + cert_file: str | None + key_file: str | None + key_password: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: _TYPE_SOCKET_OPTIONS | None = ..., + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py new file mode 100644 index 0000000000..ee9ca662b6 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py @@ -0,0 +1,486 @@ +from __future__ import annotations + +import typing +from collections import OrderedDict +from enum import Enum, auto +from threading import RLock + +if typing.TYPE_CHECKING: + # We can only import Protocol if TYPE_CHECKING because it's a development + # dependency, and is not available at runtime. + from typing import Protocol + + from typing_extensions import Self + + class HasGettableStringKeys(Protocol): + def keys(self) -> typing.Iterator[str]: ... + + def __getitem__(self, key: str) -> str: ... + + +__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] + + +# Key type +_KT = typing.TypeVar("_KT") +# Value type +_VT = typing.TypeVar("_VT") +# Default type +_DT = typing.TypeVar("_DT") + +ValidHTTPHeaderSource = typing.Union[ + "HTTPHeaderDict", + typing.Mapping[str, str], + typing.Iterable[tuple[str, str]], + "HasGettableStringKeys", +] + + +class _Sentinel(Enum): + not_passed = auto() + + +def ensure_can_construct_http_header_dict( + potential: object, +) -> ValidHTTPHeaderSource | None: + if isinstance(potential, HTTPHeaderDict): + return potential + elif isinstance(potential, typing.Mapping): + # Full runtime checking of the contents of a Mapping is expensive, so for the + # purposes of typechecking, we assume that any Mapping is the right shape. + return typing.cast(typing.Mapping[str, str], potential) + elif isinstance(potential, typing.Iterable): + # Similarly to Mapping, full runtime checking of the contents of an Iterable is + # expensive, so for the purposes of typechecking, we assume that any Iterable + # is the right shape. + return typing.cast(typing.Iterable[tuple[str, str]], potential) + elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): + return typing.cast("HasGettableStringKeys", potential) + else: + return None + + +class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + _container: typing.OrderedDict[_KT, _VT] + _maxsize: int + dispose_func: typing.Callable[[_VT], None] | None + lock: RLock + + def __init__( + self, + maxsize: int = 10, + dispose_func: typing.Callable[[_VT], None] | None = None, + ) -> None: + super().__init__() + self._maxsize = maxsize + self.dispose_func = dispose_func + self._container = OrderedDict() + self.lock = RLock() + + def __getitem__(self, key: _KT) -> _VT: + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key: _KT, value: _VT) -> None: + evicted_item = None + with self.lock: + # Possibly evict the existing value of 'key' + try: + # If the key exists, we'll overwrite it, which won't change the + # size of the pool. Because accessing a key should move it to + # the end of the eviction line, we pop it out first. + evicted_item = key, self._container.pop(key) + self._container[key] = value + except KeyError: + # When the key does not exist, we insert the value first so that + # evicting works in all cases, including when self._maxsize is 0 + self._container[key] = value + if len(self._container) > self._maxsize: + # If we didn't evict an existing value, and we've hit our maximum + # size, then we have to evict the least recently used item from + # the beginning of the container. + evicted_item = self._container.popitem(last=False) + + # After releasing the lock on the pool, dispose of any evicted value. + if evicted_item is not None and self.dispose_func: + _, evicted_value = evicted_item + self.dispose_func(evicted_value) + + def __delitem__(self, key: _KT) -> None: + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self) -> int: + with self.lock: + return len(self._container) + + def __iter__(self) -> typing.NoReturn: + raise NotImplementedError( + "Iteration over this class is unlikely to be threadsafe." + ) + + def clear(self) -> None: + with self.lock: + # Copy pointers to all values, then wipe the mapping + values = list(self._container.values()) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self) -> set[_KT]: # type: ignore[override] + with self.lock: + return set(self._container.keys()) + + +class HTTPHeaderDictItemView(set[tuple[str, str]]): + """ + HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of + address. + + If we directly try to get an item with a particular name, we will get a string + back that is the concatenated version of all the values: + + >>> d['X-Header-Name'] + 'Value1, Value2, Value3' + + However, if we iterate over an HTTPHeaderDict's items, we will optionally combine + these values based on whether combine=True was called when building up the dictionary + + >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) + >>> d.add("A", "2", combine=True) + >>> d.add("B", "bar") + >>> list(d.items()) + [ + ('A', '1, 2'), + ('B', 'foo'), + ('B', 'bar'), + ] + + This class conforms to the interface required by the MutableMapping ABC while + also giving us the nonstandard iteration behavior we want; items with duplicate + keys, ordered by time of first insertion. + """ + + _headers: HTTPHeaderDict + + def __init__(self, headers: HTTPHeaderDict) -> None: + self._headers = headers + + def __len__(self) -> int: + return len(list(self._headers.iteritems())) + + def __iter__(self) -> typing.Iterator[tuple[str, str]]: + return self._headers.iteritems() + + def __contains__(self, item: object) -> bool: + if isinstance(item, tuple) and len(item) == 2: + passed_key, passed_val = item + if isinstance(passed_key, str) and isinstance(passed_val, str): + return self._headers._has_value_for_header(passed_key, passed_val) + return False + + +class HTTPHeaderDict(typing.MutableMapping[str, str]): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 7230. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + """ + + _container: typing.MutableMapping[str, list[str]] + + def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): + super().__init__() + self._container = {} # 'dict' is insert-ordered + if headers is not None: + if isinstance(headers, HTTPHeaderDict): + self._copy_from(headers) + else: + self.extend(headers) + if kwargs: + self.extend(kwargs) + + def __setitem__(self, key: str, val: str) -> None: + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + self._container[key.lower()] = [key, val] + + def __getitem__(self, key: str) -> str: + if isinstance(key, bytes): + key = key.decode("latin-1") + val = self._container[key.lower()] + return ", ".join(val[1:]) + + def __delitem__(self, key: str) -> None: + if isinstance(key, bytes): + key = key.decode("latin-1") + del self._container[key.lower()] + + def __contains__(self, key: object) -> bool: + if isinstance(key, bytes): + key = key.decode("latin-1") + if isinstance(key, str): + return key.lower() in self._container + return False + + def setdefault(self, key: str, default: str = "") -> str: + return super().setdefault(key, default) + + def __eq__(self, other: object) -> bool: + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return False + else: + other_as_http_header_dict = type(self)(maybe_constructable) + + return {k.lower(): v for k, v in self.itermerged()} == { + k.lower(): v for k, v in other_as_http_header_dict.itermerged() + } + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __len__(self) -> int: + return len(self._container) + + def __iter__(self) -> typing.Iterator[str]: + # Only provide the originally cased names + for vals in self._container.values(): + yield vals[0] + + def discard(self, key: str) -> None: + try: + del self[key] + except KeyError: + pass + + def add(self, key: str, val: str, *, combine: bool = False) -> None: + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + If this is called with combine=True, instead of adding a new header value + as a distinct item during iteration, this will instead append the value to + any existing header value with a comma. If no existing header value exists + for the key, then the value will simply be added, ignoring the combine parameter. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + >>> list(headers.items()) + [('foo', 'bar'), ('foo', 'baz')] + >>> headers.add('foo', 'quz', combine=True) + >>> list(headers.items()) + [('foo', 'bar, baz, quz')] + """ + # avoid a bytes/str comparison by decoding before httplib + if isinstance(key, bytes): + key = key.decode("latin-1") + key_lower = key.lower() + new_vals = [key, val] + # Keep the common case aka no item present as fast as possible + vals = self._container.setdefault(key_lower, new_vals) + if new_vals is not vals: + # if there are values here, then there is at least the initial + # key/value pair + assert len(vals) >= 2 + if combine: + vals[-1] = vals[-1] + ", " + val + else: + vals.append(val) + + def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: + """Generic import function for any type of header-like object. + Adapted version of MutableMapping.update in order to insert items + with self.add instead of self.__setitem__ + """ + if len(args) > 1: + raise TypeError( + f"extend() takes at most 1 positional arguments ({len(args)} given)" + ) + other = args[0] if len(args) >= 1 else () + + if isinstance(other, HTTPHeaderDict): + for key, val in other.iteritems(): + self.add(key, val) + elif isinstance(other, typing.Mapping): + for key, val in other.items(): + self.add(key, val) + elif isinstance(other, typing.Iterable): + for key, value in other: + self.add(key, value) + elif hasattr(other, "keys") and hasattr(other, "__getitem__"): + # THIS IS NOT A TYPESAFE BRANCH + # In this branch, the object has a `keys` attr but is not a Mapping or any of + # the other types indicated in the method signature. We do some stuff with + # it as though it partially implements the Mapping interface, but we're not + # doing that stuff safely AT ALL. + for key in other.keys(): + self.add(key, other[key]) + + for key, value in kwargs.items(): + self.add(key, value) + + @typing.overload + def getlist(self, key: str) -> list[str]: ... + + @typing.overload + def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... + + def getlist( + self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed + ) -> list[str] | _DT: + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + if isinstance(key, bytes): + key = key.decode("latin-1") + try: + vals = self._container[key.lower()] + except KeyError: + if default is _Sentinel.not_passed: + # _DT is unbound; empty list is instance of List[str] + return [] + # _DT is bound; default is instance of _DT + return default + else: + # _DT may or may not be bound; vals[1:] is instance of List[str], which + # meets our external interface requirement of `Union[List[str], _DT]`. + return vals[1:] + + def _prepare_for_method_change(self) -> Self: + """ + Remove content-specific header fields before changing the request + method to GET or HEAD according to RFC 9110, Section 15.4. + """ + content_specific_headers = [ + "Content-Encoding", + "Content-Language", + "Content-Location", + "Content-Type", + "Content-Length", + "Digest", + "Last-Modified", + ] + for header in content_specific_headers: + self.discard(header) + return self + + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist + iget = getlist + + # Backwards compatibility for http.cookiejar + get_all = getlist + + def __repr__(self) -> str: + return f"{type(self).__name__}({dict(self.itermerged())})" + + def _copy_from(self, other: HTTPHeaderDict) -> None: + for key in other: + val = other.getlist(key) + self._container[key.lower()] = [key, *val] + + def copy(self) -> Self: + clone = type(self)() + clone._copy_from(self) + return clone + + def iteritems(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all header lines, including duplicate ones.""" + for key in self: + vals = self._container[key.lower()] + for val in vals[1:]: + yield vals[0], val + + def itermerged(self) -> typing.Iterator[tuple[str, str]]: + """Iterate over all headers, merging duplicate ones together.""" + for key in self: + val = self._container[key.lower()] + yield val[0], ", ".join(val[1:]) + + def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] + return HTTPHeaderDictItemView(self) + + def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: + if header_name in self: + return potential_value in self._container[header_name.lower()][1:] + return False + + def __ior__(self, other: object) -> HTTPHeaderDict: + # Supports extending a header dict in-place using operator |= + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + self.extend(maybe_constructable) + return self + + def __or__(self, other: object) -> Self: + # Supports merging header dicts using operator | + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = self.copy() + result.extend(maybe_constructable) + return result + + def __ror__(self, other: object) -> Self: + # Supports merging header dicts using operator | when other is on left side + # combining items with add instead of __setitem__ + maybe_constructable = ensure_can_construct_http_header_dict(other) + if maybe_constructable is None: + return NotImplemented + result = type(self)(maybe_constructable) + result.extend(self) + return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py new file mode 100644 index 0000000000..297c271bf4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import json as _json +import typing +from urllib.parse import urlencode + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .filepost import _TYPE_FIELDS, encode_multipart_formdata +from .response import BaseHTTPResponse + +__all__ = ["RequestMethods"] + +_TYPE_ENCODE_URL_FIELDS = typing.Union[ + typing.Sequence[tuple[str, typing.Union[str, bytes]]], + typing.Mapping[str, typing.Union[str, bytes]], +] + + +class RequestMethods: + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`urllib3.HTTPConnectionPool` and + :class:`urllib3.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are + encoded in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} + + def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: + self.headers = headers or {} + + def urlopen( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **kw: typing.Any, + ) -> BaseHTTPResponse: # Abstract + raise NotImplementedError( + "Classes extending RequestMethods must implement " + "their own ``urlopen`` method." + ) + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + json: typing.Any | None = None, + **urlopen_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the + option to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param fields: + Data to encode and send in the URL or request body, depending on ``method``. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param json: + Data to encode and send as JSON with UTF-encoded in the request body. + The ``"Content-Type"`` header will be set to ``"application/json"`` + unless specified otherwise. + """ + method = method.upper() + + if json is not None and body is not None: + raise TypeError( + "request got values for both 'body' and 'json' parameters which are mutually exclusive" + ) + + if json is not None: + if headers is None: + headers = self.headers + + if not ("content-type" in map(str.lower, headers.keys())): + headers = HTTPHeaderDict(headers) + headers["Content-Type"] = "application/json" + + body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + + if body is not None: + urlopen_kw["body"] = body + + if method in self._encode_url_methods: + return self.request_encode_url( + method, + url, + fields=fields, # type: ignore[arg-type] + headers=headers, + **urlopen_kw, + ) + else: + return self.request_encode_body( + method, url, fields=fields, headers=headers, **urlopen_kw + ) + + def request_encode_url( + self, + method: str, + url: str, + fields: _TYPE_ENCODE_URL_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the URL. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": headers} + extra_kw.update(urlopen_kw) + + if fields: + url += "?" + urlencode(fields) + + return self.urlopen(method, url, **extra_kw) + + def request_encode_body( + self, + method: str, + url: str, + fields: _TYPE_FIELDS | None = None, + headers: typing.Mapping[str, str] | None = None, + encode_multipart: bool = True, + multipart_boundary: str | None = None, + **urlopen_kw: str, + ) -> BaseHTTPResponse: + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :func:`urllib3.encode_multipart_formdata` is used to encode + the payload with the appropriate content type. Otherwise + :func:`urllib.parse.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request + signing, such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example:: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimic behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will + be overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param fields: + Data to encode and send in the request body. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param encode_multipart: + If True, encode the ``fields`` using the multipart/form-data MIME + format. + + :param multipart_boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + if headers is None: + headers = self.headers + + extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} + body: bytes | str + + if fields: + if "body" in urlopen_kw: + raise TypeError( + "request got values for both 'fields' and 'body', can only specify one." + ) + + if encode_multipart: + body, content_type = encode_multipart_formdata( + fields, boundary=multipart_boundary + ) + else: + body, content_type = ( + urlencode(fields), # type: ignore[arg-type] + "application/x-www-form-urlencoded", + ) + + extra_kw["body"] = body + extra_kw["headers"].setdefault("Content-Type", content_type) + + extra_kw.update(urlopen_kw) + + return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py new file mode 100644 index 0000000000..bfed03985b --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '2.7.0' +__version_tuple__ = version_tuple = (2, 7, 0) + +__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py new file mode 100644 index 0000000000..84e1dab945 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py @@ -0,0 +1,1099 @@ +from __future__ import annotations + +import datetime +import http.client +import logging +import os +import re +import socket +import sys +import threading +import typing +import warnings +from http.client import HTTPConnection as _HTTPConnection +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from .response import HTTPResponse + from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT + from .util.ssltransport import SSLTransport + +from ._collections import HTTPHeaderDict +from .http2 import probe as http2_probe +from .util.response import assert_header_parsing +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout +from .util.util import to_str +from .util.wait import wait_for_read + +try: # Compiled with SSL? + import ssl + + BaseSSLError = ssl.SSLError +except (ImportError, AttributeError): + ssl = None # type: ignore[assignment] + + class BaseSSLError(BaseException): # type: ignore[no-redef] + pass + + +from ._base_connection import _TYPE_BODY +from ._base_connection import ProxyConfig as ProxyConfig +from ._base_connection import _ResponseOptions as _ResponseOptions +from ._version import __version__ +from .exceptions import ( + ConnectTimeoutError, + HeaderParsingError, + NameResolutionError, + NewConnectionError, + ProxyError, + SystemTimeWarning, +) +from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ +from .util.request import body_to_chunks +from .util.ssl_ import assert_fingerprint as _assert_fingerprint +from .util.ssl_ import ( + create_urllib3_context, + is_ipaddress, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .util.ssl_match_hostname import CertificateError, match_hostname +from .util.url import Url + +# Not a no-op, we're adding this to the namespace so it can be imported. +ConnectionError = ConnectionError +BrokenPipeError = BrokenPipeError + + +log = logging.getLogger(__name__) + +port_by_scheme = {"http": 80, "https": 443} + +# When it comes time to update this value as a part of regular maintenance +# (ie test_recent_date is failing) update it to ~6 months before the current date. +RECENT_DATE = datetime.date(2025, 1, 1) + +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") + + +class HTTPConnection(_HTTPConnection): + """ + Based on :class:`http.client.HTTPConnection` but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + + Additional keyword parameters are used to configure attributes of the connection. + Accepted parameters include: + + - ``source_address``: Set the source address for the current connection. + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass: + + .. code-block:: python + + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + """ + + default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] + + #: Disable Nagle's algorithm by default. + #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` + default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + ] + + #: Whether this connection verifies the host's certificate. + is_verified: bool = False + + #: Whether this proxy connection verified the proxy host's certificate. + # If no proxy is currently connected to the value will be ``None``. + proxy_is_verified: bool | None = None + + blocksize: int + source_address: tuple[str, int] | None + socket_options: connection._TYPE_SOCKET_OPTIONS | None + + _has_connected_to_proxy: bool + _response_options: _ResponseOptions | None + _tunnel_host: str | None + _tunnel_port: int | None + _tunnel_scheme: str | None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + super().__init__( + host=host, + port=port, + timeout=Timeout.resolve_default_timeout(timeout), + source_address=source_address, + blocksize=blocksize, + ) + self.socket_options = socket_options + self.proxy = proxy + self.proxy_config = proxy_config + + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host: str | None = None + self._tunnel_port: int | None = None + self._tunnel_scheme: str | None = None + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __repr__(self) -> str: + return f"<{self} at {id(self):#x}>" + + @property + def host(self) -> str: + """ + Getter method to remove any trailing dots that indicate the hostname is an FQDN. + + In general, SSL certificates don't include the trailing dot indicating a + fully-qualified domain name, and thus, they don't validate properly when + checked against a domain name that includes the dot. In addition, some + servers may not expect to receive the trailing dot when provided. + + However, the hostname with trailing dot is critical to DNS resolution; doing a + lookup with the trailing dot will properly only resolve the appropriate FQDN, + whereas a lookup without a trailing dot will search the system's search domain + list. Thus, it's important to keep the original host around for use only in + those cases where it's appropriate (i.e., when doing DNS lookup to establish the + actual TCP connection across which we're going to send HTTP requests). + """ + return self._dns_host.rstrip(".") + + @host.setter + def host(self, value: str) -> None: + """ + Setter for the `host` property. + + We assume that only urllib3 uses the _dns_host attribute; httplib itself + only uses `host`, and it seems reasonable that other libraries follow suit. + """ + self._dns_host = value + + def _new_conn(self) -> socket.socket: + """Establish a socket connection and set nodelay settings on it. + + :return: New socket connection. + """ + try: + sock = connection.create_connection( + (self._dns_host, self.port), + self.timeout, + source_address=self.source_address, + socket_options=self.socket_options, + ) + except socket.gaierror as e: + raise NameResolutionError(self.host, self, e) from e + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except OSError as e: + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + sys.audit("http.client.connect", self, self.host, self.port) + + return sock + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + if scheme not in ("http", "https"): + raise ValueError( + f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" + ) + super().set_tunnel(host, port=port, headers=headers) + self._tunnel_scheme = scheme + + if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): + # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. + # When using connection_from_host, host will come without brackets. + def _wrap_ipv6(self, ip: bytes) -> bytes: + if b":" in ip and ip[0] != b"["[0]: + return b"[" + ip + b"]" + return ip + + if sys.version_info < (3, 11, 9): + # `_tunnel` copied from 3.11.13 backporting + # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 + # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 + def _tunnel(self) -> None: + _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] + connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + while True: + line = response.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise http.client.LineTooLong("header line") + if not line: + # for sites which EOF without sending a trailer + break + if line in (b"\r\n", b"\n", b""): + break + + if self.debuglevel > 0: + print("header:", line.decode()) + finally: + response.close() + + elif (3, 12) <= sys.version_info < (3, 12, 3): + # `_tunnel` copied from 3.12.11 backporting + # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 + def _tunnel(self) -> None: # noqa: F811 + connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] + self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] + self._tunnel_port, + ) + headers = [connect] + for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] + headers.append(f"{header}: {value}\r\n".encode("latin-1")) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of + # potentially emitting a series of small packets. + self.send(b"".join(headers)) + del headers + + response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] + try: + (version, code, message) = response._read_status() # type: ignore[attr-defined] + + self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] + + if self.debuglevel > 0: + for header in self._raw_proxy_headers: + print("header:", header.decode()) + + if code != http.HTTPStatus.OK: + self.close() + raise OSError( + f"Tunnel connection failed: {code} {message.strip()}" + ) + + finally: + response.close() + + def connect(self) -> None: + self.sock = self._new_conn() + if self._tunnel_host: + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + if self._has_connected_to_proxy: + self.proxy_is_verified = False + + @property + def is_closed(self) -> bool: + return self.sock is None + + @property + def is_connected(self) -> bool: + if self.sock is None: + return False + return not wait_for_read(self.sock, timeout=0.0) + + @property + def has_connected_to_proxy(self) -> bool: + return self._has_connected_to_proxy + + @property + def proxy_is_forwarding(self) -> bool: + """ + Return True if a forwarding proxy is configured, else return False + """ + return bool(self.proxy) and self._tunnel_host is None + + @property + def proxy_is_tunneling(self) -> bool: + """ + Return True if a tunneling proxy is configured, else return False + """ + return self._tunnel_host is not None + + def close(self) -> None: + try: + super().close() + finally: + # Reset all stateful properties so connection + # can be re-used without leaking prior configs. + self.sock = None + self.is_verified = False + self.proxy_is_verified = None + self._has_connected_to_proxy = False + self._response_options = None + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_scheme = None + + def putrequest( + self, + method: str, + url: str, + skip_host: bool = False, + skip_accept_encoding: bool = False, + ) -> None: + """""" + # Empty docstring because the indentation of CPython's implementation + # is broken but we don't want this method in our documentation. + match = _CONTAINS_CONTROL_CHAR_RE.search(method) + if match: + raise ValueError( + f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" + ) + + return super().putrequest( + method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding + ) + + def putheader(self, header: str, *values: str) -> None: # type: ignore[override] + """""" + if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): + super().putheader(header, *values) + elif to_str(header.lower()) not in SKIPPABLE_HEADERS: + skippable_headers = "', '".join( + [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] + ) + raise ValueError( + f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" + ) + + # `request` method's signature intentionally violates LSP. + # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + # Update the inner socket's timeout value to send the request. + # This only triggers if the connection is re-used. + if self.sock is not None: + self.sock.settimeout(self.timeout) + + # Store these values to be fed into the HTTPResponse + # object later. TODO: Remove this in favor of a real + # HTTP lifecycle mechanism. + + # We have to store these before we call .request() + # because sometimes we can still salvage a response + # off the wire even if we aren't able to completely + # send the request body. + self._response_options = _ResponseOptions( + request_method=method, + request_url=url, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + if headers is None: + headers = {} + header_keys = frozenset(to_str(k.lower()) for k in headers) + skip_accept_encoding = "accept-encoding" in header_keys + skip_host = "host" in header_keys + self.putrequest( + method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host + ) + + # Transform the body into an iterable of sendall()-able chunks + # and detect if an explicit Content-Length is doable. + chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) + chunks = chunks_and_cl.chunks + content_length = chunks_and_cl.content_length + + # When chunked is explicit set to 'True' we respect that. + if chunked: + if "transfer-encoding" not in header_keys: + self.putheader("Transfer-Encoding", "chunked") + else: + # Detect whether a framing mechanism is already in use. If so + # we respect that value, otherwise we pick chunked vs content-length + # depending on the type of 'body'. + if "content-length" in header_keys: + chunked = False + elif "transfer-encoding" in header_keys: + chunked = True + + # Otherwise we go off the recommendation of 'body_to_chunks()'. + else: + chunked = False + if content_length is None: + if chunks is not None: + chunked = True + self.putheader("Transfer-Encoding", "chunked") + else: + self.putheader("Content-Length", str(content_length)) + + # Now that framing headers are out of the way we send all the other headers. + if "user-agent" not in header_keys: + self.putheader("User-Agent", _get_default_user_agent()) + for header, value in headers.items(): + self.putheader(header, value) + self.endheaders() + + # If we're given a body we start sending that in chunks. + if chunks is not None: + for chunk in chunks: + # Sending empty chunks isn't allowed for TE: chunked + # as it indicates the end of the body. + if not chunk: + continue + if isinstance(chunk, str): + chunk = chunk.encode("utf-8") + if chunked: + self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) + else: + self.send(chunk) + + # Regardless of whether we have a body or not, if we're in + # chunked mode we want to send an explicit empty chunk. + if chunked: + self.send(b"0\r\n\r\n") + + def request_chunked( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + ) -> None: + """ + Alternative to the common request method, which sends the + body with chunked encoding and not as one block + """ + warnings.warn( + "HTTPConnection.request_chunked() is deprecated and will be removed " + "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", + category=FutureWarning, + stacklevel=2, + ) + self.request(method, url, body=body, headers=headers, chunked=True) + + def getresponse( # type: ignore[override] + self, + ) -> HTTPResponse: + """ + Get the response from the server. + + If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. + + If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. + """ + # Raise the same error as http.client.HTTPConnection + if self._response_options is None: + raise ResponseNotReady() + + # Reset this attribute for being used again. + resp_options = self._response_options + self._response_options = None + + # Since the connection's timeout value may have been updated + # we need to set the timeout on the socket. + self.sock.settimeout(self.timeout) + + # This is needed here to avoid circular import errors + from .response import HTTPResponse + + # Save a reference to the shutdown function before ownership is passed + # to httplib_response + # TODO should we implement it everywhere? + _shutdown = getattr(self.sock, "shutdown", None) + + # Get the response from http.client.HTTPConnection + httplib_response = super().getresponse() + + try: + assert_header_parsing(httplib_response.msg) + except (HeaderParsingError, TypeError) as hpe: + log.warning( + "Failed to parse headers (url=%s): %s", + _url_from_connection(self, resp_options.request_url), + hpe, + exc_info=True, + ) + + headers = HTTPHeaderDict(httplib_response.msg.items()) + + response = HTTPResponse( + body=httplib_response, + headers=headers, + status=httplib_response.status, + version=httplib_response.version, + version_string=getattr(self, "_http_vsn_str", "HTTP/?"), + reason=httplib_response.reason, + preload_content=resp_options.preload_content, + decode_content=resp_options.decode_content, + original_response=httplib_response, + enforce_content_length=resp_options.enforce_content_length, + request_method=resp_options.request_method, + request_url=resp_options.request_url, + sock_shutdown=_shutdown, + ) + return response + + +class HTTPSConnection(HTTPConnection): + """ + Many of the parameters to this constructor are passed to the underlying SSL + socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. + """ + + default_port = port_by_scheme["https"] # type: ignore[misc] + + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_fingerprint: str | None = None + _connect_callback: typing.Callable[..., None] | None = None + + def __init__( + self, + host: str, + port: int | None = None, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: None | ( + connection._TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + # cert_reqs depends on ssl_context so calculate last. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + self.cert_reqs = cert_reqs + self._connect_callback = None + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + """ + This method should only be called once, before the connection is used. + """ + warnings.warn( + "HTTPSConnection.set_cert() is deprecated and will be removed " + "in urllib3 v3.0. Instead provide the parameters to the " + "HTTPSConnection constructor.", + category=FutureWarning, + stacklevel=2, + ) + + # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also + # have an SSLContext object in which case we'll use its verify_mode. + if cert_reqs is None: + if self.ssl_context is not None: + cert_reqs = self.ssl_context.verify_mode + else: + cert_reqs = resolve_cert_reqs(None) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + def connect(self) -> None: + # Today we don't need to be doing this step before the /actual/ socket + # connection, however in the future we'll need to decide whether to + # create a new socket or re-use an existing "shared" socket as a part + # of the HTTP/2 handshake dance. + if self._tunnel_host is not None and self._tunnel_port is not None: + probe_http2_host = self._tunnel_host + probe_http2_port = self._tunnel_port + else: + probe_http2_host = self.host + probe_http2_port = self.port + + # Check if the target origin supports HTTP/2. + # If the value comes back as 'None' it means that the current thread + # is probing for HTTP/2 support. Otherwise, we're waiting for another + # probe to complete, or we get a value right away. + target_supports_http2: bool | None + if "h2" in ssl_.ALPN_PROTOCOLS: + target_supports_http2 = http2_probe.acquire_and_get( + host=probe_http2_host, port=probe_http2_port + ) + else: + # If HTTP/2 isn't going to be offered it doesn't matter if + # the target supports HTTP/2. Don't want to make a probe. + target_supports_http2 = False + + if self._connect_callback is not None: + self._connect_callback( + "before connect", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + try: + sock: socket.socket | ssl.SSLSocket + self.sock = sock = self._new_conn() + server_hostname: str = self.host + tls_in_tls = False + + # Do we need to establish a tunnel? + if self.proxy_is_tunneling: + # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. + if self._tunnel_scheme == "https": + # _connect_tls_proxy will verify and assign proxy_is_verified + self.sock = sock = self._connect_tls_proxy(self.host, sock) + tls_in_tls = True + elif self._tunnel_scheme == "http": + self.proxy_is_verified = False + + # If we're tunneling it means we're connected to our proxy. + self._has_connected_to_proxy = True + + self._tunnel() + # Override the host with the one we're requesting data from. + server_hostname = typing.cast(str, self._tunnel_host) + + if self.server_hostname is not None: + server_hostname = self.server_hostname + + is_time_off = datetime.date.today() < RECENT_DATE + if is_time_off: + warnings.warn( + ( + f"System time is way off (before {RECENT_DATE}). This will probably " + "lead to SSL verification errors" + ), + SystemTimeWarning, + ) + + # Remove trailing '.' from fqdn hostnames to allow certificate validation + server_hostname_rm_dot = server_hostname.rstrip(".") + + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock=sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + server_hostname=server_hostname_rm_dot, + ssl_context=self.ssl_context, + tls_in_tls=tls_in_tls, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ) + self.sock = sock_and_verified.socket + + # If an error occurs during connection/handshake we may need to release + # our lock so another connection can probe the origin. + except BaseException: + if self._connect_callback is not None: + self._connect_callback( + "after connect failure", + thread_id=threading.get_ident(), + target_supports_http2=target_supports_http2, + ) + + if target_supports_http2 is None: + http2_probe.set_and_release( + host=probe_http2_host, port=probe_http2_port, supports_http2=None + ) + raise + + # If this connection doesn't know if the origin supports HTTP/2 + # we report back to the HTTP/2 probe our result. + if target_supports_http2 is None: + supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" + http2_probe.set_and_release( + host=probe_http2_host, + port=probe_http2_port, + supports_http2=supports_http2, + ) + + # Forwarding proxies can never have a verified target since + # the proxy is the one doing the verification. Should instead + # use a CONNECT tunnel in order to verify the target. + # See: https://github.com/urllib3/urllib3/issues/3267. + if self.proxy_is_forwarding: + self.is_verified = False + else: + self.is_verified = sock_and_verified.is_verified + + # If there's a proxy to be connected to we are fully connected. + # This is set twice (once above and here) due to forwarding proxies + # not using tunnelling. + self._has_connected_to_proxy = bool(self.proxy) + + # Set `self.proxy_is_verified` unless it's already set while + # establishing a tunnel. + if self._has_connected_to_proxy and self.proxy_is_verified is None: + self.proxy_is_verified = sock_and_verified.is_verified + + def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: + """ + Establish a TLS connection to the proxy using the provided SSL context. + """ + # `_connect_tls_proxy` is called when self._tunnel_host is truthy. + proxy_config = typing.cast(ProxyConfig, self.proxy_config) + ssl_context = proxy_config.ssl_context + sock_and_verified = _ssl_wrap_socket_and_match_hostname( + sock, + cert_reqs=self.cert_reqs, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + server_hostname=hostname, + ssl_context=ssl_context, + assert_hostname=proxy_config.assert_hostname, + assert_fingerprint=proxy_config.assert_fingerprint, + # Features that aren't implemented for proxies yet: + cert_file=None, + key_file=None, + key_password=None, + tls_in_tls=False, + ) + self.proxy_is_verified = sock_and_verified.is_verified + return sock_and_verified.socket # type: ignore[return-value] + + +class _WrappedAndVerifiedSocket(typing.NamedTuple): + """ + Wrapped socket and whether the connection is + verified after the TLS handshake + """ + + socket: ssl.SSLSocket | SSLTransport + is_verified: bool + + +def _ssl_wrap_socket_and_match_hostname( + sock: socket.socket, + *, + cert_reqs: None | str | int, + ssl_version: None | str | int, + ssl_minimum_version: int | None, + ssl_maximum_version: int | None, + cert_file: str | None, + key_file: str | None, + key_password: str | None, + ca_certs: str | None, + ca_cert_dir: str | None, + ca_cert_data: None | str | bytes, + assert_hostname: None | str | typing.Literal[False], + assert_fingerprint: str | None, + server_hostname: str | None, + ssl_context: ssl.SSLContext | None, + tls_in_tls: bool = False, +) -> _WrappedAndVerifiedSocket: + """Logic for constructing an SSLContext from all TLS parameters, passing + that down into ssl_wrap_socket, and then doing certificate verification + either via hostname or fingerprint. This function exists to guarantee + that both proxies and targets have the same behavior when connecting via TLS. + """ + default_ssl_context = False + if ssl_context is None: + default_ssl_context = True + context = create_urllib3_context( + ssl_version=resolve_ssl_version(ssl_version), + ssl_minimum_version=ssl_minimum_version, + ssl_maximum_version=ssl_maximum_version, + cert_reqs=resolve_cert_reqs(cert_reqs), + ) + else: + context = ssl_context + + context.verify_mode = resolve_cert_reqs(cert_reqs) + + # In some cases, we want to verify hostnames ourselves + if ( + # `ssl` can't verify fingerprints or alternate hostnames + assert_fingerprint + or assert_hostname + # assert_hostname can be set to False to disable hostname checking + or assert_hostname is False + # We still support OpenSSL 1.0.2, which prevents us from verifying + # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 + or ssl_.IS_PYOPENSSL + or not ssl_.HAS_NEVER_CHECK_COMMON_NAME + ): + context.check_hostname = False + + # Try to load OS default certs if none are given. We need to do the hasattr() check + # for custom pyOpenSSL SSLContext objects because they don't support + # load_default_certs(). + if ( + not ca_certs + and not ca_cert_dir + and not ca_cert_data + and default_ssl_context + and hasattr(context, "load_default_certs") + ): + context.load_default_certs() + + # Ensure that IPv6 addresses are in the proper format and don't have a + # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses + # and interprets them as DNS hostnames. + if server_hostname is not None: + normalized = server_hostname.strip("[]") + if "%" in normalized: + normalized = normalized[: normalized.rfind("%")] + if is_ipaddress(normalized): + server_hostname = normalized + + ssl_sock = ssl_wrap_socket( + sock=sock, + keyfile=key_file, + certfile=cert_file, + key_password=key_password, + ca_certs=ca_certs, + ca_cert_dir=ca_cert_dir, + ca_cert_data=ca_cert_data, + server_hostname=server_hostname, + ssl_context=context, + tls_in_tls=tls_in_tls, + ) + + try: + if assert_fingerprint: + _assert_fingerprint( + ssl_sock.getpeercert(binary_form=True), assert_fingerprint + ) + elif ( + context.verify_mode != ssl.CERT_NONE + and not context.check_hostname + and assert_hostname is not False + ): + cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] + + # Need to signal to our match_hostname whether to use 'commonName' or not. + # If we're using our own constructed SSLContext we explicitly set 'False' + # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. + if default_ssl_context: + hostname_checks_common_name = False + else: + hostname_checks_common_name = ( + getattr(context, "hostname_checks_common_name", False) or False + ) + + _match_hostname( + cert, + assert_hostname or server_hostname, # type: ignore[arg-type] + hostname_checks_common_name, + ) + + return _WrappedAndVerifiedSocket( + socket=ssl_sock, + is_verified=context.verify_mode == ssl.CERT_REQUIRED + or bool(assert_fingerprint), + ) + except BaseException: + ssl_sock.close() + raise + + +def _match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + asserted_hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + # Our upstream implementation of ssl.match_hostname() + # only applies this normalization to IP addresses so it doesn't + # match DNS SANs so we do the same thing! + stripped_hostname = asserted_hostname.strip("[]") + if is_ipaddress(stripped_hostname): + asserted_hostname = stripped_hostname + + try: + match_hostname(cert, asserted_hostname, hostname_checks_common_name) + except CertificateError as e: + log.warning( + "Certificate did not match expected hostname: %s. Certificate: %s", + asserted_hostname, + cert, + ) + # Add cert to exception and reraise so client code can inspect + # the cert when catching the exception, if they want to + e._peer_cert = cert # type: ignore[attr-defined] + raise + + +def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: + # Look for the phrase 'wrong version number', if found + # then we should warn the user that we're very sure that + # this proxy is HTTP-only and they have a configuration issue. + error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) + is_likely_http_proxy = ( + "wrong version number" in error_normalized + or "unknown protocol" in error_normalized + or "record layer failure" in error_normalized + ) + http_proxy_warning = ( + ". Your proxy appears to only use HTTP and not HTTPS, " + "try changing your proxy URL to be HTTP. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#https-proxy-error-http-proxy" + ) + new_err = ProxyError( + f"Unable to connect to proxy" + f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", + err, + ) + new_err.__cause__ = err + return new_err + + +def _get_default_user_agent() -> str: + return f"python-urllib3/{__version__}" + + +class DummyConnection: + """Used to detect a failed ConnectionCls import.""" + + +if not ssl: + HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 + + +VerifiedHTTPSConnection = HTTPSConnection + + +def _url_from_connection( + conn: HTTPConnection | HTTPSConnection, path: str | None = None +) -> str: + """Returns the URL from a given connection. This is mainly used for testing and logging.""" + + scheme = "https" if isinstance(conn, HTTPSConnection) else "http" + + return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py new file mode 100644 index 0000000000..70fbc5e725 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py @@ -0,0 +1,1191 @@ +from __future__ import annotations + +import errno +import logging +import queue +import sys +import typing +import warnings +import weakref +from socket import timeout as SocketTimeout +from types import TracebackType + +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from ._request_methods import RequestMethods +from .connection import ( + BaseSSLError, + BrokenPipeError, + DummyConnection, + HTTPConnection, + HTTPException, + HTTPSConnection, + ProxyConfig, + _wrap_proxy_error, +) +from .connection import port_by_scheme as port_by_scheme +from .exceptions import ( + ClosedPoolError, + EmptyPoolError, + FullPoolError, + HostChangedError, + InsecureRequestWarning, + LocationValueError, + MaxRetryError, + NewConnectionError, + ProtocolError, + ProxyError, + ReadTimeoutError, + SSLError, + TimeoutError, +) +from .response import BaseHTTPResponse +from .util.connection import is_connection_dropped +from .util.proxy import connection_requires_http_tunnel +from .util.request import _TYPE_BODY_POSITION, set_file_position +from .util.retry import Retry +from .util.ssl_match_hostname import CertificateError +from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout +from .util.url import Url, _encode_target +from .util.url import _normalize_host as normalize_host +from .util.url import parse_url +from .util.util import to_str + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + + from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + +_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] + + +# Pool objects +class ConnectionPool: + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + + .. note:: + ConnectionPool.urlopen() does not normalize or percent-encode target URIs + which is useful if your target server doesn't support percent-encoded + target URIs. + """ + + scheme: str | None = None + QueueCls = queue.LifoQueue + + def __init__(self, host: str, port: int | None = None) -> None: + if not host: + raise LocationValueError("No host specified.") + + self.host = _normalize_host(host, scheme=self.scheme) + self.port = port + + # This property uses 'normalize_host()' (not '_normalize_host()') + # to avoid removing square braces around IPv6 addresses. + # This value is sent to `HTTPConnection.set_tunnel()` if called + # because square braces are required for HTTP CONNECT tunneling. + self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() + + def __str__(self) -> str: + return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.close() + # Return False to re-raise any potential exceptions + return False + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} + + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`http.client.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`http.client.HTTPConnection`. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to False, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param retries: + Retry configuration to use by default with requests in this pool. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.ProxyManager` + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.ProxyManager` + + :param \\**conn_kw: + Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, + :class:`urllib3.connection.HTTPSConnection` instances. + """ + + scheme = "http" + ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + _proxy_config: ProxyConfig | None = None, + **conn_kw: typing.Any, + ): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + if retries is None: + retries = Retry.DEFAULT + + self.timeout = timeout + self.retries = retries + + self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + self.proxy_config = _proxy_config + + # Fill the queue up so that doing get() on it will block properly + for _ in range(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + self.conn_kw = conn_kw + + if self.proxy: + # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. + # Defaulting `socket_options` to an empty list avoids it defaulting to + # ``HTTPConnection.default_socket_options``. + self.conn_kw.setdefault("socket_options", []) + + self.conn_kw["proxy"] = self.proxy + self.conn_kw["proxy_config"] = self.proxy_config + + # Do not pass 'self' as callback to 'finalize'. + # Then the 'finalize' would keep an endless living (leak) to self. + # By just passing a reference to the pool allows the garbage collector + # to free self if nobody else has a reference to it. + pool = self.pool + + # Close all the HTTPConnections in the pool before the + # HTTPConnectionPool object is garbage collected. + weakref.finalize(self, _close_pool_connections, pool) + + def _new_conn(self) -> BaseHTTPConnection: + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTP connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "80", + ) + + conn = self.ConnectionCls( + host=self.host, + port=self.port, + timeout=self.timeout.connect_timeout, + **self.conn_kw, + ) + return conn + + def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + + if self.pool is None: + raise ClosedPoolError(self, "Pool is closed.") + + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: + + except queue.Empty: + if self.block: + raise EmptyPoolError( + self, + "Pool is empty and a new connection can't be opened due to blocking mode.", + ) from None + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.debug("Resetting dropped connection: %s", self.host) + conn.close() + + return conn or self._new_conn() + + def _put_conn(self, conn: BaseHTTPConnection | None) -> None: + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + if self.pool is not None: + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except queue.Full: + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + if self.block: + # This should never happen if you got the conn from self._get_conn + raise FullPoolError( + self, + "Pool reached maximum size and no more connections are allowed.", + ) from None + + log.warning( + "Connection pool is full, discarding connection: %s. Connection pool size: %s", + self.host, + self.pool.qsize(), + ) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + + def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: + # Nothing to do for HTTP connections. + pass + + def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: + """Helper that always returns a :class:`urllib3.util.Timeout`""" + if timeout is _DEFAULT_TIMEOUT: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _raise_timeout( + self, + err: BaseSSLError | OSError | SocketTimeout, + url: str, + timeout_value: _TYPE_TIMEOUT | None, + ) -> None: + """Is the error actually a timeout? Will raise a ReadTimeout or pass""" + + if isinstance(err, SocketTimeout): + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + # See the above comment about EAGAIN in Python 3. + if hasattr(err, "errno") and err.errno in _blocking_errnos: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={timeout_value})" + ) from err + + def _make_request( + self, + conn: BaseHTTPConnection, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | None = None, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + chunked: bool = False, + response_conn: BaseHTTPConnection | None = None, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> BaseHTTPResponse: + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + Pass ``None`` to retry until you receive a response. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param response_conn: + Set this to ``None`` if you will handle releasing the connection or + set the connection to have the response release it. + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + timeout_obj.start_connect() + conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) + + try: + # Trigger any extra validation we need to do. + try: + self._validate_conn(conn) + except (SocketTimeout, BaseSSLError) as e: + self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) + raise + + # _validate_conn() starts the connection to an HTTPS proxy + # so we need to wrap errors with 'ProxyError' here too. + except ( + OSError, + NewConnectionError, + TimeoutError, + BaseSSLError, + CertificateError, + SSLError, + ) as e: + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + # If the connection didn't successfully connect to it's proxy + # then there + if isinstance( + new_e, (OSError, NewConnectionError, TimeoutError, SSLError) + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + raise new_e + + # conn.request() calls http.client.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + try: + conn.request( + method, + url, + body=body, + headers=headers, + chunked=chunked, + preload_content=preload_content, + decode_content=decode_content, + enforce_content_length=enforce_content_length, + ) + + # We are swallowing BrokenPipeError (errno.EPIPE) since the server is + # legitimately able to close the connection after sending a valid response. + # With this behaviour, the received response is still readable. + except BrokenPipeError: + pass + except OSError as e: + # MacOS/Linux + # EPROTOTYPE and ECONNRESET are needed on macOS + # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ + # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. + if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: + raise + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + if not conn.is_closed: + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, f"Read timed out. (read timeout={read_timeout})" + ) + conn.timeout = read_timeout + + # Receive the response from the server + try: + response = conn.getresponse() + except (BaseSSLError, OSError) as e: + self._raise_timeout(err=e, url=url, timeout_value=read_timeout) + raise + + # Set properties that are used by the pooling layer. + response.retries = retries + response._connection = response_conn # type: ignore[attr-defined] + response._pool = self # type: ignore[attr-defined] + + log.debug( + '%s://%s:%s "%s %s %s" %s %s', + self.scheme, + self.host, + self.port, + method, + url, + response.version_string, + response.status, + response.length_remaining, + ) + + return response + + def close(self) -> None: + """ + Close all pooled connections and disable the pool. + """ + if self.pool is None: + return + # Disable access to the pool + old_pool, self.pool = self.pool, None + + # Close all the HTTPConnections in the pool. + _close_pool_connections(old_pool) + + def is_same_host(self, url: str) -> bool: + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith("/"): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + if host is not None: + host = _normalize_host(host, scheme=scheme) + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + redirect: bool = True, + assert_same_host: bool = True, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + pool_timeout: int | None = None, + release_conn: bool | None = None, + chunked: bool = False, + body_pos: _TYPE_BODY_POSITION | None = None, + preload_content: bool = True, + decode_content: bool = True, + **response_kw: typing.Any, + ) -> BaseHTTPResponse: + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method + such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param url: + The URL to perform the request on. + + :param body: + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Configure the number of retries to allow before raising a + :class:`~urllib3.exceptions.MaxRetryError` exception. + + If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a + :class:`~urllib3.util.retry.Retry` object for fine-grained control + over different types of retries. + Pass an integer number to retry connection errors that many times, + but no other types of errors. Pass zero to never retry. + + If ``False``, then retries are disabled and any exception is raised + immediately. Also, instead of raising a MaxRetryError on redirects, + the redirect response will be returned. + + :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When ``False``, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param bool preload_content: + If True, the response's body will be preloaded into memory. + + :param bool decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of ``preload_content`` + which defaults to ``True``. + + :param bool chunked: + If True, urllib3 will send the body using chunked transfer + encoding. Otherwise, urllib3 will send the body using the standard + content-length form. Defaults to False. + + :param int body_pos: + Position to seek to in file-like body in the event of a retry or + redirect. Typically this won't need to be set because urllib3 will + auto-populate the value when needed. + """ + # Ensure that the URL we're connecting to is properly encoded + if url.startswith("/"): + # URLs starting with / are inherently schemeless. + url = to_str(_encode_target(url)) + destination_scheme = None + else: + parsed_url = parse_url(url) + destination_scheme = parsed_url.scheme + url = to_str(parsed_url.url) + + if headers is None: + headers = self.headers + + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) + + if release_conn is None: + release_conn = preload_content + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries) + + conn = None + + # Track whether `conn` needs to be released before + # returning/raising/recursing. Update this variable if necessary, and + # leave `release_conn` constant throughout the function. That way, if + # the function recurses, the original value of `release_conn` will be + # passed down into the recursive call, and its value will be respected. + # + # See issue #651 [1] for details. + # + # [1] + release_this_conn = release_conn + + http_tunnel_required = connection_requires_http_tunnel( + self.proxy, self.proxy_config, destination_scheme + ) + + # Merge the proxy headers. Only done when not using HTTP CONNECT. We + # have to copy the headers dict so we can safely change it without those + # changes being reflected in anyone else's copy. + if not http_tunnel_required: + headers = headers.copy() # type: ignore[attr-defined] + headers.update(self.proxy_headers) # type: ignore[union-attr] + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + # Keep track of whether we cleanly exited the except block. This + # ensures we do proper cleanup in finally. + clean_exit = False + + # Rewind body position, if needed. Record current position + # for future rewinds in the event of a redirect/retry. + body_pos = set_file_position(body, body_pos) + + try: + # Request a connection from the queue. + timeout_obj = self._get_timeout(timeout) + conn = self._get_conn(timeout=pool_timeout) + + conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] + + # Is this a closed/new connection that requires CONNECT tunnelling? + if self.proxy is not None and http_tunnel_required and conn.is_closed: + try: + self._prepare_proxy(conn) + except (BaseSSLError, OSError, SocketTimeout) as e: + self._raise_timeout( + err=e, url=self.proxy.url, timeout_value=conn.timeout + ) + raise + + # If we're going to release the connection in ``finally:``, then + # the response doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = conn if not release_conn else None + + # Make the request on the HTTPConnection object + response = self._make_request( + conn, + method, + url, + timeout=timeout_obj, + body=body, + headers=headers, + chunked=chunked, + retries=retries, + response_conn=response_conn, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Everything went great! + clean_exit = True + + except EmptyPoolError: + # Didn't get a connection from the pool, no need to clean up + clean_exit = True + release_this_conn = False + raise + + except ( + TimeoutError, + HTTPException, + OSError, + ProtocolError, + BaseSSLError, + SSLError, + CertificateError, + ProxyError, + ) as e: + # Discard the connection for these exceptions. It will be + # replaced during the next _get_conn() call. + clean_exit = False + new_e: Exception = e + if isinstance(e, (BaseSSLError, CertificateError)): + new_e = SSLError(e) + if isinstance( + new_e, + ( + OSError, + NewConnectionError, + TimeoutError, + SSLError, + HTTPException, + ), + ) and (conn and conn.proxy and not conn.has_connected_to_proxy): + new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) + elif isinstance(new_e, (OSError, HTTPException)): + new_e = ProtocolError("Connection aborted.", new_e) + + retries = retries.increment( + method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] + ) + retries.sleep() + + # Keep track of the error for the retry warning. + err = e + + finally: + if not clean_exit: + # We hit some kind of exception, handled or otherwise. We need + # to throw the connection away unless explicitly told not to. + # Close the connection, set the variable to None, and make sure + # we put the None back in the pool to avoid leaking it. + if conn: + conn.close() + conn = None + release_this_conn = True + + if release_this_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning( + "Retrying (%r) after connection broken by '%r': %s", retries, err, url + ) + return self.urlopen( + method, + url, + body, + headers, + retries, + redirect, + assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + body = None + headers = HTTPHeaderDict(headers)._prepare_for_method_change() + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # self.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not self.is_same_host( + redirect_location + ): + new_headers = headers.copy() # type: ignore[union-attr] + for header in headers: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + headers = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep_for_retry(response) + log.debug("Redirecting %s -> %s", url, redirect_location) + return self.urlopen( + method, + redirect_location, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + # Check if we should retry the HTTP response. + has_retry_after = bool(response.headers.get("Retry-After")) + if retries.is_retry(method, response.status, has_retry_after): + try: + retries = retries.increment(method, url, response=response, _pool=self) + except MaxRetryError: + if retries.raise_on_status: + response.drain_conn() + raise + return response + + response.drain_conn() + retries.sleep(response) + log.debug("Retry: %s", url) + return self.urlopen( + method, + url, + body, + headers, + retries=retries, + redirect=redirect, + assert_same_host=assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + chunked=chunked, + body_pos=body_pos, + preload_content=preload_content, + decode_content=decode_content, + **response_kw, + ) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, + ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` + is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade + the connection socket into an SSL socket. + """ + + scheme = "https" + ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection + + def __init__( + self, + host: str, + port: int | None = None, + timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, + maxsize: int = 1, + block: bool = False, + headers: typing.Mapping[str, str] | None = None, + retries: Retry | bool | int | None = None, + _proxy: Url | None = None, + _proxy_headers: typing.Mapping[str, str] | None = None, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + ssl_version: int | str | None = None, + ssl_minimum_version: ssl.TLSVersion | None = None, + ssl_maximum_version: ssl.TLSVersion | None = None, + assert_hostname: str | typing.Literal[False] | None = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + **conn_kw: typing.Any, + ) -> None: + super().__init__( + host, + port, + timeout, + maxsize, + block, + headers, + retries, + _proxy, + _proxy_headers, + **conn_kw, + ) + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.key_password = key_password + self.ca_certs = ca_certs + self.ca_cert_dir = ca_cert_dir + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] + """Establishes a tunnel connection through HTTP CONNECT.""" + if self.proxy and self.proxy.scheme == "https": + tunnel_scheme = "https" + else: + tunnel_scheme = "http" + + conn.set_tunnel( + scheme=tunnel_scheme, + host=self._tunnel_host, + port=self.port, + headers=self.proxy_headers, + ) + conn.connect() + + def _new_conn(self) -> BaseHTTPSConnection: + """ + Return a fresh :class:`urllib3.connection.HTTPConnection`. + """ + self.num_connections += 1 + log.debug( + "Starting new HTTPS connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "443", + ) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] + raise ImportError( + "Can't connect to HTTPS URL because the SSL module is not available." + ) + + actual_host: str = self.host + actual_port = self.port + if self.proxy is not None and self.proxy.host is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + return self.ConnectionCls( + host=actual_host, + port=actual_port, + timeout=self.timeout.connect_timeout, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ssl_version=self.ssl_version, + ssl_minimum_version=self.ssl_minimum_version, + ssl_maximum_version=self.ssl_maximum_version, + **self.conn_kw, + ) + + def _validate_conn(self, conn: BaseHTTPConnection) -> None: + """ + Called right before a request is made, after the socket is created. + """ + super()._validate_conn(conn) + + # Force connect early to allow us to validate the connection. + if conn.is_closed: + conn.connect() + + # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 + if not conn.is_verified and not conn.proxy_is_verified: + warnings.warn( + ( + f"Unverified HTTPS request is being made to host '{conn.host}'. " + "Adding certificate verification is strongly advised. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#tls-warnings" + ), + InsecureRequestWarning, + ) + + +def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \\**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example:: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, _, host, port, *_ = parse_url(url) + scheme = scheme or "http" + port = port or port_by_scheme.get(scheme, 80) + if scheme == "https": + return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + else: + return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + """ + Normalize hosts for comparisons and use with sockets. + """ + + host = normalize_host(host, scheme) + + # httplib doesn't like it when we include brackets in IPv6 addresses + # Specifically, if we include brackets but also pass the port then + # httplib crazily doubles up the square brackets on the Host header. + # Instead, we need to make sure we never pass ``None`` as the port. + # However, for backward compatibility reasons we can't actually + # *assert* that. See http://bugs.python.org/issue28539 + if host and host.startswith("[") and host.endswith("]"): + host = host[1:-1] + return host + + +def _url_from_pool( + pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None +) -> str: + """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" + return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url + + +def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: + """Drains a queue of connections and closes each one.""" + try: + while True: + conn = pool.get(block=False) + if conn: + conn.close() + except queue.Empty: + pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py new file mode 100644 index 0000000000..e5b62b25e9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import urllib3.connection + +from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection + + +def inject_into_urllib3() -> None: + # override connection classes to use emscripten specific classes + # n.b. mypy complains about the overriding of classes below + # if it isn't ignored + HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection + HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection + urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] + urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] + urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py new file mode 100644 index 0000000000..63f79dd3be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import os +import typing + +# use http.client.HTTPException for consistency with non-emscripten +from http.client import HTTPException as HTTPException # noqa: F401 +from http.client import ResponseNotReady + +from ..._base_connection import _TYPE_BODY +from ...connection import HTTPConnection, ProxyConfig, port_by_scheme +from ...exceptions import TimeoutError +from ...response import BaseHTTPResponse +from ...util.connection import _TYPE_SOCKET_OPTIONS +from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT +from ...util.url import Url +from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request +from .request import EmscriptenRequest +from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + + +class EmscriptenHTTPConnection: + default_port: typing.ClassVar[int] = port_by_scheme["http"] + default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] + + timeout: None | (float) + + host: str + port: int + blocksize: int + source_address: tuple[str, int] | None + socket_options: _TYPE_SOCKET_OPTIONS | None + + proxy: Url | None + proxy_config: ProxyConfig | None + + is_verified: bool = False + proxy_is_verified: bool | None = None + + response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper + _response: EmscriptenResponse | None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 8192, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + ) -> None: + self.host = host + self.port = port + self.timeout = timeout if isinstance(timeout, float) else 0.0 + self.scheme = "http" + self._closed = True + self._response = None + # ignore these things because we don't + # have control over that stuff + self.proxy = None + self.proxy_config = None + self.blocksize = blocksize + self.source_address = None + self.socket_options = None + self.is_verified = False + + def set_tunnel( + self, + host: str, + port: int | None = 0, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + pass + + def connect(self) -> None: + pass + + def request( + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + # We know *at least* botocore is depending on the order of the + # first 3 parameters so to be safe we only mark the later ones + # as keyword-only to ensure we have space to extend. + *, + chunked: bool = False, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + ) -> None: + self._closed = False + if url.startswith("/"): + if self.port is not None: + port = f":{self.port}" + else: + port = "" + # no scheme / host / port included, make a full url + url = f"{self.scheme}://{self.host}{port}{url}" + request = EmscriptenRequest( + url=url, + method=method, + timeout=self.timeout if self.timeout else 0, + decode_content=decode_content, + ) + request.set_body(body) + if headers: + for k, v in headers.items(): + request.set_header(k, v) + self._response = None + try: + if not preload_content: + self._response = send_streaming_request(request) + if self._response is None: + self._response = send_request(request) + except _TimeoutError as e: + raise TimeoutError(e.message) from e + except _RequestError as e: + raise HTTPException(e.message) from e + + def getresponse(self) -> BaseHTTPResponse: + if self._response is not None: + return EmscriptenHttpResponseWrapper( + internal_response=self._response, + url=self._response.request.url, + connection=self, + ) + else: + raise ResponseNotReady() + + def close(self) -> None: + self._closed = True + self._response = None + + @property + def is_closed(self) -> bool: + """Whether the connection either is brand new or has been previously closed. + If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` + properties must be False. + """ + return self._closed + + @property + def is_connected(self) -> bool: + """Whether the connection is actively connected to any origin (proxy or target)""" + return True + + @property + def has_connected_to_proxy(self) -> bool: + """Whether the connection has successfully connected to its proxy. + This returns False if no proxy is in use. Used to determine whether + errors are coming from the proxy layer or from tunnelling to the target origin. + """ + return False + + +class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): + default_port = port_by_scheme["https"] + # all this is basically ignored, as browser handles https + cert_reqs: int | str | None = None + ca_certs: str | None = None + ca_cert_dir: str | None = None + ca_cert_data: None | str | bytes = None + cert_file: str | None + key_file: str | None + key_password: str | None + ssl_context: typing.Any | None + ssl_version: int | str | None = None + ssl_minimum_version: int | None = None + ssl_maximum_version: int | None = None + assert_hostname: None | str | typing.Literal[False] + assert_fingerprint: str | None = None + + def __init__( + self, + host: str, + port: int = 0, + *, + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + blocksize: int = 16384, + socket_options: ( + None | _TYPE_SOCKET_OPTIONS + ) = HTTPConnection.default_socket_options, + proxy: Url | None = None, + proxy_config: ProxyConfig | None = None, + cert_reqs: int | str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + server_hostname: str | None = None, + ssl_context: typing.Any | None = None, + ca_certs: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + ssl_version: int | str | None = None, # Deprecated + cert_file: str | None = None, + key_file: str | None = None, + key_password: str | None = None, + ) -> None: + super().__init__( + host, + port=port, + timeout=timeout, + source_address=source_address, + blocksize=blocksize, + socket_options=socket_options, + proxy=proxy, + proxy_config=proxy_config, + ) + self.scheme = "https" + + self.key_file = key_file + self.cert_file = cert_file + self.key_password = key_password + self.ssl_context = ssl_context + self.server_hostname = server_hostname + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + self.ssl_version = ssl_version + self.ssl_minimum_version = ssl_minimum_version + self.ssl_maximum_version = ssl_maximum_version + self.ca_certs = ca_certs and os.path.expanduser(ca_certs) + self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data + + self.cert_reqs = None + + # The browser will automatically verify all requests. + # We have no control over that setting. + self.is_verified = True + + def set_cert( + self, + key_file: str | None = None, + cert_file: str | None = None, + cert_reqs: int | str | None = None, + key_password: str | None = None, + ca_certs: str | None = None, + assert_hostname: None | str | typing.Literal[False] = None, + assert_fingerprint: str | None = None, + ca_cert_dir: str | None = None, + ca_cert_data: None | str | bytes = None, + ) -> None: + pass + + +# verify that this class implements BaseHTTP(s) connection correctly +if typing.TYPE_CHECKING: + _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) + _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js new file mode 100644 index 0000000000..faf141e1fa --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js @@ -0,0 +1,110 @@ +let Status = { + SUCCESS_HEADER: -1, + SUCCESS_EOF: -2, + ERROR_TIMEOUT: -3, + ERROR_EXCEPTION: -4, +}; + +let connections = new Map(); +let nextConnectionID = 1; +const encoder = new TextEncoder(); + +self.addEventListener("message", async function (event) { + if (event.data.close) { + let connectionID = event.data.close; + connections.delete(connectionID); + return; + } else if (event.data.getMore) { + let connectionID = event.data.getMore; + let { curOffset, value, reader, intBuffer, byteBuffer } = + connections.get(connectionID); + // if we still have some in buffer, then just send it back straight away + if (!value || curOffset >= value.length) { + // read another buffer if required + try { + let readResponse = await reader.read(); + + if (readResponse.done) { + // read everything - clear connection and return + connections.delete(connectionID); + Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); + Atomics.notify(intBuffer, 0); + // finished reading successfully + // return from event handler + return; + } + curOffset = 0; + connections.get(connectionID).value = readResponse.value; + value = readResponse.value; + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } + + // send as much buffer as we can + let curLen = value.length - curOffset; + if (curLen > byteBuffer.length) { + curLen = byteBuffer.length; + } + byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); + + Atomics.store(intBuffer, 0, curLen); // store current length in bytes + Atomics.notify(intBuffer, 0); + curOffset += curLen; + connections.get(connectionID).curOffset = curOffset; + + return; + } else { + // start fetch + let connectionID = nextConnectionID; + nextConnectionID += 1; + const intBuffer = new Int32Array(event.data.buffer); + const byteBuffer = new Uint8Array(event.data.buffer, 8); + try { + const response = await fetch(event.data.url, event.data.fetchParams); + // return the headers first via textencoder + var headers = []; + for (const pair of response.headers.entries()) { + headers.push([pair[0], pair[1]]); + } + let headerObj = { + headers: headers, + status: response.status, + connectionID, + }; + const headerText = JSON.stringify(headerObj); + let headerBytes = encoder.encode(headerText); + let written = headerBytes.length; + byteBuffer.set(headerBytes); + intBuffer[1] = written; + // make a connection + connections.set(connectionID, { + reader: response.body.getReader(), + intBuffer: intBuffer, + byteBuffer: byteBuffer, + value: undefined, + curOffset: 0, + }); + // set header ready + Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); + Atomics.notify(intBuffer, 0); + // all fetching after this goes through a new postmessage call with getMore + // this allows for parallel requests + } catch (error) { + console.log("Request exception:", error); + let errorBytes = encoder.encode(error.message); + let written = errorBytes.length; + byteBuffer.set(errorBytes); + intBuffer[1] = written; + Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); + Atomics.notify(intBuffer, 0); + } + } +}); +self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py new file mode 100644 index 0000000000..612cfddc4c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py @@ -0,0 +1,726 @@ +""" +Support for streaming http requests in emscripten. + +A few caveats - + +If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled +https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md +*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the +JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case +timeouts and streaming should just work. + +Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. + +This approach has several caveats: + +Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. +Streaming only works if you're running pyodide in a web worker. + +Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch +operation, so it requires that you have crossOriginIsolation enabled, by serving over https +(or from localhost) with the two headers below set: + + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp + +You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in +JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole +request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. + +Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once +control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. + +NB: in this code, there are a lot of JavaScript objects. They are named js_* +to make it clear what type of object they are. +""" + +from __future__ import annotations + +import io +import json +from email.parser import Parser +from importlib.resources import files +from typing import TYPE_CHECKING, Any + +import js # type: ignore[import-not-found] +from pyodide.ffi import ( # type: ignore[import-not-found] + JsArray, + JsException, + JsProxy, + to_js, +) + +if TYPE_CHECKING: + from typing_extensions import Buffer + +from .request import EmscriptenRequest +from .response import EmscriptenResponse + +""" +There are some headers that trigger unintended CORS preflight requests. +See also https://github.com/koenvo/pyodide-http/issues/22 +""" +HEADERS_TO_IGNORE = ("user-agent",) + +SUCCESS_HEADER = -1 +SUCCESS_EOF = -2 +ERROR_TIMEOUT = -3 +ERROR_EXCEPTION = -4 + + +class _RequestError(Exception): + def __init__( + self, + message: str | None = None, + *, + request: EmscriptenRequest | None = None, + response: EmscriptenResponse | None = None, + ): + self.request = request + self.response = response + self.message = message + super().__init__(self.message) + + +class _StreamingError(_RequestError): + pass + + +class _TimeoutError(_RequestError): + pass + + +def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: + return to_js(dict_val, dict_converter=js.Object.fromEntries) + + +class _ReadStream(io.RawIOBase): + def __init__( + self, + int_buffer: JsArray, + byte_buffer: JsArray, + timeout: float, + worker: JsProxy, + connection_id: int, + request: EmscriptenRequest, + ): + self.int_buffer = int_buffer + self.byte_buffer = byte_buffer + self.read_pos = 0 + self.read_len = 0 + self.connection_id = connection_id + self.worker = worker + self.timeout = int(1000 * timeout) if timeout > 0 else None + self.is_live = True + self._is_closed = False + self.request: EmscriptenRequest | None = request + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.int_buffer = None + self.byte_buffer = None + self._is_closed = True + self.request = None + if self.is_live: + self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) + self.is_live = False + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def readinto(self, byte_obj: Buffer) -> int: + if not self.int_buffer: + raise _StreamingError( + "No buffer for stream in _ReadStream.readinto", + request=self.request, + response=None, + ) + if self.read_len == 0: + # wait for the worker to send something + js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) + self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) + if ( + js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) + == "timed-out" + ): + raise _TimeoutError + data_len = self.int_buffer[0] + if data_len > 0: + self.read_len = data_len + self.read_pos = 0 + elif data_len == ERROR_EXCEPTION: + string_len = self.int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", + request=self.request, + response=None, + ) + else: + # EOF, free the buffers and return zero + # and free the request + self.is_live = False + self.close() + return 0 + # copy from int32array to python bytes + ret_length = min(self.read_len, len(memoryview(byte_obj))) + subarray = self.byte_buffer.subarray( + self.read_pos, self.read_pos + ret_length + ).to_py() + memoryview(byte_obj)[0:ret_length] = subarray + self.read_len -= ret_length + self.read_pos += ret_length + return ret_length + + +class _StreamingFetcher: + def __init__(self) -> None: + # make web-worker and data buffer on startup + self.streaming_ready = False + streaming_worker_code = ( + files(__package__) + .joinpath("emscripten_fetch_worker.js") + .read_text(encoding="utf-8") + ) + js_data_blob = js.Blob.new( + to_js([streaming_worker_code], create_pyproxies=False), + _obj_from_dict({"type": "application/javascript"}), + ) + + def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: + def onMsg(e: JsProxy) -> None: + self.streaming_ready = True + js_resolve_fn(e) + + def onErr(e: JsProxy) -> None: + js_reject_fn(e) # Defensive: never happens in ci + + self.js_worker.onmessage = onMsg + self.js_worker.onerror = onErr + + js_data_url = js.URL.createObjectURL(js_data_blob) + self.js_worker = js.globalThis.Worker.new(js_data_url) + self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) + + def send(self, request: EmscriptenRequest) -> EmscriptenResponse: + headers = { + k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE + } + + body = request.body + fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} + # start the request off in the worker + timeout = int(1000 * request.timeout) if request.timeout > 0 else None + js_shared_buffer = js.SharedArrayBuffer.new(1048576) + js_int_buffer = js.Int32Array.new(js_shared_buffer) + js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) + + js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) + js.Atomics.notify(js_int_buffer, 0) + js_absolute_url = js.URL.new(request.url, js.location).href + self.js_worker.postMessage( + _obj_from_dict( + { + "buffer": js_shared_buffer, + "url": js_absolute_url, + "fetchParams": fetch_data, + } + ) + ) + # wait for the worker to send something + js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) + if js_int_buffer[0] == ERROR_TIMEOUT: + raise _TimeoutError( + "Timeout connecting to streaming request", + request=request, + response=None, + ) + elif js_int_buffer[0] == SUCCESS_HEADER: + # got response + # header length is in second int of intBuffer + string_len = js_int_buffer[1] + # decode the rest to a JSON string + js_decoder = js.TextDecoder.new() + # this does a copy (the slice) because decode can't work on shared array + # for some silly reason + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + # get it as an object + response_obj = json.loads(json_str) + return EmscriptenResponse( + request=request, + status_code=response_obj["status"], + headers=response_obj["headers"], + body=_ReadStream( + js_int_buffer, + js_byte_buffer, + request.timeout, + self.js_worker, + response_obj["connectionID"], + request, + ), + ) + elif js_int_buffer[0] == ERROR_EXCEPTION: + string_len = js_int_buffer[1] + # decode the error string + js_decoder = js.TextDecoder.new() + json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) + raise _StreamingError( + f"Exception thrown in fetch: {json_str}", request=request, response=None + ) + else: + raise _StreamingError( + f"Unknown status from worker in fetch: {js_int_buffer[0]}", + request=request, + response=None, + ) + + +class _JSPIReadStream(io.RawIOBase): + """ + A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch + response. This requires support for WebAssembly JavaScript Promise Integration + in the containing browser, and for pyodide to be launched via runPythonAsync. + + :param js_read_stream: + The JavaScript stream reader + + :param timeout: + Timeout in seconds + + :param request: + The request we're handling + + :param response: + The response this stream relates to + + :param js_abort_controller: + A JavaScript AbortController object, used for timeouts + """ + + def __init__( + self, + js_read_stream: Any, + timeout: float, + request: EmscriptenRequest, + response: EmscriptenResponse, + js_abort_controller: Any, # JavaScript AbortController for timeouts + ): + self.js_read_stream = js_read_stream + self.timeout = timeout + self._is_closed = False + self._is_done = False + self.request: EmscriptenRequest | None = request + self.response: EmscriptenResponse | None = response + self.current_buffer = None + self.current_buffer_pos = 0 + self.js_abort_controller = js_abort_controller + + def __del__(self) -> None: + self.close() + + # this is compatible with _base_connection + def is_closed(self) -> bool: + return self._is_closed + + # for compatibility with RawIOBase + @property + def closed(self) -> bool: + return self.is_closed() + + def close(self) -> None: + if self.is_closed(): + return + self.read_len = 0 + self.read_pos = 0 + self.js_read_stream.cancel() + self.js_read_stream = None + self._is_closed = True + self._is_done = True + self.request = None + self.response = None + super().close() + + def readable(self) -> bool: + return True + + def writable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def _get_next_buffer(self) -> bool: + result_js = _run_sync_with_timeout( + self.js_read_stream.read(), + self.timeout, + self.js_abort_controller, + request=self.request, + response=self.response, + ) + if result_js.done: + self._is_done = True + return False + else: + self.current_buffer = result_js.value.to_py() + self.current_buffer_pos = 0 + return True + + def readinto(self, byte_obj: Buffer) -> int: + if self.current_buffer is None: + if not self._get_next_buffer() or self.current_buffer is None: + self.close() + return 0 + ret_length = min( + len(byte_obj), len(self.current_buffer) - self.current_buffer_pos + ) + byte_obj[0:ret_length] = self.current_buffer[ + self.current_buffer_pos : self.current_buffer_pos + ret_length + ] + self.current_buffer_pos += ret_length + if self.current_buffer_pos == len(self.current_buffer): + self.current_buffer = None + return ret_length + + +# check if we are in a worker or not +def is_in_browser_main_thread() -> bool: + return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window + + +def is_cross_origin_isolated() -> bool: + return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated + + +def is_in_node() -> bool: + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + and hasattr(js.process.release, "name") + and js.process.release.name == "node" + ) + + +def is_worker_available() -> bool: + return hasattr(js, "Worker") and hasattr(js, "Blob") + + +_fetcher: _StreamingFetcher | None = None + +if is_worker_available() and ( + (is_cross_origin_isolated() and not is_in_browser_main_thread()) + and (not is_in_node()) +): + _fetcher = _StreamingFetcher() +else: + _fetcher = None + + +NODE_JSPI_ERROR = ( + "urllib3 only works in Node.js with pyodide.runPythonAsync" + " and requires the flag --experimental-wasm-stack-switching in " + " versions of node <24." +) + + +def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: + if has_jspi(): + return send_jspi_request(request, True) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + + if _fetcher and streaming_ready(): + return _fetcher.send(request) + else: + _show_streaming_warning() + return None + + +_SHOWN_TIMEOUT_WARNING = False + + +def _show_timeout_warning() -> None: + global _SHOWN_TIMEOUT_WARNING + if not _SHOWN_TIMEOUT_WARNING: + _SHOWN_TIMEOUT_WARNING = True + message = "Warning: Timeout is not available on main browser thread" + js.console.warn(message) + + +_SHOWN_STREAMING_WARNING = False + + +def _show_streaming_warning() -> None: + global _SHOWN_STREAMING_WARNING + if not _SHOWN_STREAMING_WARNING: + _SHOWN_STREAMING_WARNING = True + message = "Can't stream HTTP requests because: \n" + if not is_cross_origin_isolated(): + message += " Page is not cross-origin isolated\n" + if is_in_browser_main_thread(): + message += " Python is running in main browser thread\n" + if not is_worker_available(): + message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in + if streaming_ready() is False: + message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch +is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" + from js import console + + console.warn(message) + + +def send_request(request: EmscriptenRequest) -> EmscriptenResponse: + if has_jspi(): + return send_jspi_request(request, False) + elif is_in_node(): + raise _RequestError( + message=NODE_JSPI_ERROR, + request=request, + response=None, + ) + try: + js_xhr = js.XMLHttpRequest.new() + + if not is_in_browser_main_thread(): + js_xhr.responseType = "arraybuffer" + if request.timeout: + js_xhr.timeout = int(request.timeout * 1000) + else: + js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") + if request.timeout: + # timeout isn't available on the main thread - show a warning in console + # if it is set + _show_timeout_warning() + + js_xhr.open(request.method, request.url, False) + for name, value in request.headers.items(): + if name.lower() not in HEADERS_TO_IGNORE: + js_xhr.setRequestHeader(name, value) + + js_xhr.send(to_js(request.body)) + + headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) + + if not is_in_browser_main_thread(): + body = js_xhr.response.to_py().tobytes() + else: + body = js_xhr.response.encode("ISO-8859-15") + return EmscriptenResponse( + status_code=js_xhr.status, headers=headers, body=body, request=request + ) + except JsException as err: + if err.name == "TimeoutError": + raise _TimeoutError(err.message, request=request) + elif err.name == "NetworkError": + raise _RequestError(err.message, request=request) + else: + # general http error + raise _RequestError(err.message, request=request) + + +def send_jspi_request( + request: EmscriptenRequest, streaming: bool +) -> EmscriptenResponse: + """ + Send a request using WebAssembly JavaScript Promise Integration + to wrap the asynchronous JavaScript fetch api (experimental). + + :param request: + Request to send + + :param streaming: + Whether to stream the response + + :return: The response object + :rtype: EmscriptenResponse + """ + timeout = request.timeout + js_abort_controller = js.AbortController.new() + headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} + req_body = request.body + fetch_data = { + "headers": headers, + "body": to_js(req_body), + "method": request.method, + "signal": js_abort_controller.signal, + } + # Node.js returns the whole response (unlike opaqueredirect in browsers), + # so urllib3 can set `redirect: manual` to control redirects itself. + # https://stackoverflow.com/a/78524615 + if _is_node_js(): + fetch_data["redirect"] = "manual" + # Call JavaScript fetch (async api, returns a promise) + fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) + # Now suspend WebAssembly until we resolve that promise + # or time out. + response_js = _run_sync_with_timeout( + fetcher_promise_js, + timeout, + js_abort_controller, + request=request, + response=None, + ) + headers = {} + header_iter = response_js.headers.entries() + while True: + iter_value_js = header_iter.next() + if getattr(iter_value_js, "done", False): + break + else: + headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) + status_code = response_js.status + body: bytes | io.RawIOBase = b"" + + response = EmscriptenResponse( + status_code=status_code, headers=headers, body=b"", request=request + ) + if streaming: + # get via inputstream + if response_js.body is not None: + # get a reader from the fetch response + body_stream_js = response_js.body.getReader() + body = _JSPIReadStream( + body_stream_js, timeout, request, response, js_abort_controller + ) + else: + # get directly via arraybuffer + # n.b. this is another async JavaScript call. + body = _run_sync_with_timeout( + response_js.arrayBuffer(), + timeout, + js_abort_controller, + request=request, + response=response, + ).to_py() + response.body = body + return response + + +def _run_sync_with_timeout( + promise: Any, + timeout: float, + js_abort_controller: Any, + request: EmscriptenRequest | None, + response: EmscriptenResponse | None, +) -> Any: + """ + Await a JavaScript promise synchronously with a timeout which is implemented + via the AbortController + + :param promise: + Javascript promise to await + + :param timeout: + Timeout in seconds + + :param js_abort_controller: + A JavaScript AbortController object, used on timeout + + :param request: + The request being handled + + :param response: + The response being handled (if it exists yet) + + :raises _TimeoutError: If the request times out + :raises _RequestError: If the request raises a JavaScript exception + + :return: The result of awaiting the promise. + """ + timer_id = None + if timeout > 0: + timer_id = js.setTimeout( + js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) + ) + try: + from pyodide.ffi import run_sync + + # run_sync here uses WebAssembly JavaScript Promise Integration to + # suspend python until the JavaScript promise resolves. + return run_sync(promise) + except JsException as err: + if err.name == "AbortError": + raise _TimeoutError( + message="Request timed out", request=request, response=response + ) + else: + raise _RequestError(message=err.message, request=request, response=response) + finally: + if timer_id is not None: + js.clearTimeout(timer_id) + + +def has_jspi() -> bool: + """ + Return true if jspi can be used. + + This requires both browser support and also WebAssembly + to be in the correct state - i.e. that the javascript + call into python was async not sync. + + :return: True if jspi can be used. + :rtype: bool + """ + try: + from pyodide.ffi import can_run_sync, run_sync # noqa: F401 + + return bool(can_run_sync()) + except ImportError: + return False + + +def _is_node_js() -> bool: + """ + Check if we are in Node.js. + + :return: True if we are in Node.js. + :rtype: bool + """ + return ( + hasattr(js, "process") + and hasattr(js.process, "release") + # According to the Node.js documentation, the release name is always "node". + and js.process.release.name == "node" + ) + + +def streaming_ready() -> bool | None: + if _fetcher: + return _fetcher.streaming_ready + else: + return None # no fetcher, return None to signify that + + +async def wait_for_streaming_ready() -> bool: + if _fetcher: + await _fetcher.js_worker_ready_promise + return True + else: + return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py new file mode 100644 index 0000000000..e692e692bd --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..._base_connection import _TYPE_BODY + + +@dataclass +class EmscriptenRequest: + method: str + url: str + params: dict[str, str] | None = None + body: _TYPE_BODY | None = None + headers: dict[str, str] = field(default_factory=dict) + timeout: float = 0 + decode_content: bool = True + + def set_header(self, name: str, value: str) -> None: + self.headers[name.capitalize()] = value + + def set_body(self, body: _TYPE_BODY | None) -> None: + self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py new file mode 100644 index 0000000000..ec1e1dbe83 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import json as _json +import logging +import typing +from contextlib import contextmanager +from dataclasses import dataclass +from http.client import HTTPException as HTTPException +from io import BytesIO, IOBase + +from ...exceptions import InvalidHeader, TimeoutError +from ...response import BaseHTTPResponse +from ...util.retry import Retry +from .request import EmscriptenRequest + +if typing.TYPE_CHECKING: + from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection + +log = logging.getLogger(__name__) + + +@dataclass +class EmscriptenResponse: + status_code: int + headers: dict[str, str] + body: IOBase | bytes + request: EmscriptenRequest + + +class EmscriptenHttpResponseWrapper(BaseHTTPResponse): + def __init__( + self, + internal_response: EmscriptenResponse, + url: str | None = None, + connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, + ): + self._pool = None # set by pool class + self._body = None + self._uncached_read_occurred = False + self._response = internal_response + self._url = url + self._connection = connection + self._closed = False + super().__init__( + headers=internal_response.headers, + status=internal_response.status_code, + request_url=url, + version=0, + version_string="HTTP/?", + reason="", + decode_content=True, + ) + self.length_remaining = self._init_length(self._response.request.method) + self.length_is_certain = False + + @property + def url(self) -> str | None: + return self._url + + @url.setter + def url(self, url: str | None) -> None: + self._url = url + + @property + def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: + return self._connection + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + while True: + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + else: + break + + def _init_length(self, request_method: str | None) -> int | None: + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Check for responses that shouldn't include a body + if ( + self.status in (204, 304) + or 100 <= self.status < 200 + or request_method == "HEAD" + ): + length = 0 + + return length + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, # ignored because browser decodes always + cache_content: bool = False, + ) -> bytes: + if ( + self._closed + or self._response is None + or (isinstance(self._response.body, IOBase) and self._response.body.closed) + ): + return b"" + + with self._error_catcher(): + # body has been preloaded as a string by XmlHttpRequest + if not isinstance(self._response.body, IOBase): + self.length_remaining = len(self._response.body) + self.length_is_certain = True + # wrap body in IOStream + self._response.body = BytesIO(self._response.body) + if amt is not None and amt >= 0: + # don't cache partial content + cache_content = False + data = self._response.body.read(amt) + self._uncached_read_occurred = True + else: # read all we can (and cache it) + data = self._response.body.read() + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + self._uncached_read_occurred = True + if self.length_remaining is not None: + self.length_remaining = max(self.length_remaining - len(data), 0) + if len(data) == 0 or ( + self.length_is_certain and self.length_remaining == 0 + ): + # definitely finished reading, close response stream + self._response.body.close() + return typing.cast(bytes, data) + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Generator[bytes]: + # chunked is handled by browser + while True: + bytes = self.read(amt, decode_content) + if not bytes: + break + yield bytes + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + self.close() + + @property + def data(self) -> bytes: + if self._body: + return self._body + else: + return self.read(cache_content=True) + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + def close(self) -> None: + if not self._closed: + if isinstance(self._response.body, IOBase): + self._response.body.close() + if self._connection: + self._connection.close() + self._connection = None + self._closed = True + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch Emscripten specific exceptions thrown by fetch.py, + instead re-raising urllib3 variants, so that low-level exceptions + are not leaked in the high-level api. + + On exit, release the connection back to the pool. + """ + from .fetch import _RequestError, _TimeoutError # avoid circular import + + clean_exit = False + + try: + yield + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + except _TimeoutError as e: + raise TimeoutError(str(e)) + except _RequestError as e: + raise HTTPException(str(e)) + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now + if ( + isinstance(self._response.body, IOBase) + and not self._response.body.closed + ): + self._response.body.close() + # release the connection back to the pool + self.release_conn() + else: + # If we have read everything from the response stream, + # return the connection back to the pool. + if ( + isinstance(self._response.body, IOBase) + and self._response.body.closed + ): + self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py new file mode 100644 index 0000000000..f06b859992 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py @@ -0,0 +1,563 @@ +""" +Module for using pyOpenSSL as a TLS backend. This module was relevant before +the standard library ``ssl`` module supported SNI, but now that we've dropped +support for Python 2.7 all relevant Python versions support SNI so +**this module is no longer recommended**. + +This needs the following packages installed: + +* `pyOpenSSL`_ (tested with 19.0.0) +* `cryptography`_ (minimum 2.3, from pyopenssl) +* `idna`_ (minimum 2.1, from cryptography) + +However, pyOpenSSL depends on cryptography, so while we use all three directly here we +end up having relatively few packages required. + +You can install them with the following command: + +.. code-block:: bash + + $ python -m pip install pyopenssl cryptography idna + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this: + +.. code-block:: python + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +.. _pyopenssl: https://www.pyopenssl.org +.. _cryptography: https://cryptography.io +.. _idna: https://github.com/kjd/idna +""" + +from __future__ import annotations + +import OpenSSL.SSL # type: ignore[import-not-found] +from cryptography import x509 + +try: + from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] +except ImportError: + # UnsupportedExtension is gone in cryptography >= 2.1.0 + class UnsupportedExtension(Exception): # type: ignore[no-redef] + pass + + +import logging +import ssl +import typing +from io import BytesIO +from socket import socket as socket_cls + +from .. import util + +if typing.TYPE_CHECKING: + from OpenSSL.crypto import X509 # type: ignore[import-not-found] + + +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions: dict[int, int] = { + util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} +_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} + +# The SSLvX values are the most likely to be missing in the future +# but we check them all just to be sure. +_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( + OpenSSL.SSL, "OP_NO_SSLv3", 0 +) +_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) +_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) +_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) +_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) + +_openssl_to_ssl_minimum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, + ssl.TLSVersion.TLSv1_3: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), + ssl.TLSVersion.MAXIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), +} +_openssl_to_ssl_maximum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 + | _OP_NO_TLSv1 + | _OP_NO_TLSv1_1 + | _OP_NO_TLSv1_2 + | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, +} + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_SSLContext = util.ssl_.SSLContext + + +log = logging.getLogger(__name__) + + +def inject_into_urllib3() -> None: + "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." + + _validate_dependencies_met() + + util.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.IS_PYOPENSSL = True + util.ssl_.IS_PYOPENSSL = True + + +def extract_from_urllib3() -> None: + "Undo monkey-patching by :func:`inject_into_urllib3`." + + util.SSLContext = orig_util_SSLContext + util.ssl_.SSLContext = orig_util_SSLContext + util.IS_PYOPENSSL = False + util.ssl_.IS_PYOPENSSL = False + + +def _validate_dependencies_met() -> None: + """ + Verifies that PyOpenSSL's package-level dependencies have been met. + Throws `ImportError` if they are not met. + """ + # Method added in `cryptography==1.1`; not available in older versions + from cryptography.x509.extensions import Extensions + + if getattr(Extensions, "get_extension_for_class", None) is None: + raise ImportError( + "'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer." + ) + + # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 + # attribute is only present on those versions. + from OpenSSL.crypto import X509 + + x509 = X509() + if getattr(x509, "_x509", None) is None: + raise ImportError( + "'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer." + ) + + +def _dnsname_to_stdlib(name: str) -> str | None: + """ + Converts a dNSName SubjectAlternativeName field to the form used by the + standard library on the given Python version. + + Cryptography produces a dNSName as a unicode string that was idna-decoded + from ASCII bytes. We need to idna-encode that string to get it back, and + then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib + uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). + + If the name cannot be idna-encoded then we return None signalling that + the name given should be skipped. + """ + + def idna_encode(name: str) -> bytes | None: + """ + Borrowed wholesale from the Python Cryptography Project. It turns out + that we can't just safely call `idna.encode`: it can explode for + wildcard names. This avoids that problem. + """ + import idna + + try: + for prefix in ["*.", "."]: + if name.startswith(prefix): + name = name[len(prefix) :] + return prefix.encode("ascii") + idna.encode(name) + return idna.encode(name) + except idna.core.IDNAError: + return None + + # Don't send IPv6 addresses through the IDNA encoder. + if ":" in name: + return name + + encoded_name = idna_encode(name) + if encoded_name is None: + return None + return encoded_name.decode("utf-8") + + +def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: + """ + Given an PyOpenSSL certificate, provides all the subject alternative names. + """ + cert = peer_cert.to_cryptography() + + # We want to find the SAN extension. Ask Cryptography to locate it (it's + # faster than looping in Python) + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + except x509.ExtensionNotFound: + # No such extension, return the empty list. + return [] + except ( + x509.DuplicateExtension, + UnsupportedExtension, + x509.UnsupportedGeneralNameType, + UnicodeError, + ) as e: + # A problem has been found with the quality of the certificate. Assume + # no SAN field is present. + log.warning( + "A problem was encountered with the certificate that prevented " + "urllib3 from finding the SubjectAlternativeName field. This can " + "affect certificate validation. The error was %s", + e, + ) + return [] + + # We want to return dNSName and iPAddress fields. We need to cast the IPs + # back to strings because the match_hostname function wants them as + # strings. + # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 + # decoded. This is pretty frustrating, but that's what the standard library + # does with certificates, and so we need to attempt to do the same. + # We also want to skip over names which cannot be idna encoded. + names = [ + ("DNS", name) + for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) + if name is not None + ] + names.extend( + ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) + ) + + return names + + +class WrappedSocket: + """API-compatibility wrapper for Python OpenSSL's Connection-class.""" + + def __init__( + self, + connection: OpenSSL.SSL.Connection, + socket: socket_cls, + suppress_ragged_eofs: bool = True, + ) -> None: + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._io_refs = 0 + self._closed = False + + def fileno(self) -> int: + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self) -> None: + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return b"" + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b"" + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + else: + return data # type: ignore[no-any-return] + + def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: + try: + return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return 0 + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return 0 + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise TimeoutError("The read operation timed out") from e + else: + return self.recv_into(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + + def settimeout(self, timeout: float) -> None: + return self.socket.settimeout(timeout) + + def _send_until_done(self, data: bytes) -> int: + while True: + try: + return self.connection.send(data) # type: ignore[no-any-return] + except OpenSSL.SSL.WantWriteError as e: + if not util.wait_for_write(self.socket, self.socket.gettimeout()): + raise TimeoutError() from e + continue + except OpenSSL.SSL.SysCallError as e: + raise OSError(e.args[0], str(e)) from e + + def sendall(self, data: bytes) -> None: + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done( + data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] + ) + total_sent += sent + + def shutdown(self, how: int) -> None: + try: + self.connection.shutdown() + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"shutdown error: {e!r}") from e + + def close(self) -> None: + self._closed = True + if self._io_refs <= 0: + self._real_close() + + def _real_close(self) -> None: + try: + return self.connection.close() # type: ignore[no-any-return] + except OpenSSL.SSL.Error: + return + + def getpeercert( + self, binary_form: bool = False + ) -> dict[str, list[typing.Any]] | None: + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 # type: ignore[no-any-return] + + if binary_form: + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] + + return { + "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] + "subjectAltName": get_subj_alt_name(x509), + } + + def version(self) -> str: + return self.connection.get_protocol_version_name() # type: ignore[no-any-return] + + def selected_alpn_protocol(self) -> str | None: + alpn_proto = self.connection.get_alpn_proto_negotiated() + return alpn_proto.decode() if alpn_proto else None + + +WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] + + +class PyOpenSSLContext: + """ + I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible + for translating the interface of the standard library ``SSLContext`` object + to calls into PyOpenSSL. + """ + + def __init__(self, protocol: int) -> None: + self.protocol = _openssl_versions[protocol] + self._ctx = OpenSSL.SSL.Context(self.protocol) + self._options = 0 + self.check_hostname = False + self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED + self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED + self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST + + @property + def options(self) -> int: + return self._options + + @options.setter + def options(self, value: int) -> None: + self._options = value + self._set_ctx_options() + + @property + def verify_flags(self) -> int: + return self._verify_flags + + @verify_flags.setter + def verify_flags(self, value: int) -> None: + self._verify_flags = value + self._ctx.get_cert_store().set_flags(self._verify_flags) + + @property + def verify_mode(self) -> int: + return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] + + @verify_mode.setter + def verify_mode(self, value: ssl.VerifyMode) -> None: + self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) + + def set_default_verify_paths(self) -> None: + self._ctx.set_default_verify_paths() + + def set_ciphers(self, ciphers: bytes | str) -> None: + if isinstance(ciphers, str): + ciphers = ciphers.encode("utf-8") + self._ctx.set_cipher_list(ciphers) + + def load_verify_locations( + self, + cafile: str | None = None, + capath: str | None = None, + cadata: bytes | None = None, + ) -> None: + if cafile is not None: + cafile = cafile.encode("utf-8") # type: ignore[assignment] + if capath is not None: + capath = capath.encode("utf-8") # type: ignore[assignment] + try: + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e + + def load_cert_chain( + self, + certfile: str, + keyfile: str | None = None, + password: str | None = None, + ) -> None: + try: + self._ctx.use_certificate_chain_file(certfile) + if password is not None: + if not isinstance(password, bytes): + password = password.encode("utf-8") # type: ignore[assignment] + self._ctx.set_passwd_cb(lambda *_: password) + self._ctx.use_privatekey_file(keyfile or certfile) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e + + def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: + protocols = [util.util.to_bytes(p, "ascii") for p in protocols] + return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] + + def wrap_socket( + self, + sock: socket_cls, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bytes | str | None = None, + ) -> WrappedSocket: + cnx = OpenSSL.SSL.Connection(self._ctx, sock) + + # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 + if server_hostname and not util.ssl_.is_ipaddress(server_hostname): + if isinstance(server_hostname, str): + server_hostname = server_hostname.encode("utf-8") + cnx.set_tlsext_host_name(server_hostname) + + cnx.set_connect_state() + + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(sock, sock.gettimeout()): + raise TimeoutError("select timed out") from e + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"bad handshake: {e!r}") from e + break + + return WrappedSocket(cnx, sock) + + def _set_ctx_options(self) -> None: + self._ctx.set_options( + self._options + | _openssl_to_ssl_minimum_version[self._minimum_version] + | _openssl_to_ssl_maximum_version[self._maximum_version] + ) + + @property + def minimum_version(self) -> int: + return self._minimum_version + + @minimum_version.setter + def minimum_version(self, minimum_version: int) -> None: + self._minimum_version = minimum_version + self._set_ctx_options() + + @property + def maximum_version(self) -> int: + return self._maximum_version + + @maximum_version.setter + def maximum_version(self, maximum_version: int) -> None: + self._maximum_version = maximum_version + self._set_ctx_options() + + +def _verify_callback( + cnx: OpenSSL.SSL.Connection, + x509: X509, + err_no: int, + err_depth: int, + return_code: int, +) -> bool: + return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py new file mode 100644 index 0000000000..d37da8fc20 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py @@ -0,0 +1,228 @@ +""" +This module contains provisional support for SOCKS proxies from within +urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and +SOCKS5. To enable its functionality, either install PySocks or install this +module with the ``socks`` extra. + +The SOCKS implementation supports the full range of urllib3 features. It also +supports the following SOCKS features: + +- SOCKS4A (``proxy_url='socks4a://...``) +- SOCKS4 (``proxy_url='socks4://...``) +- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) +- SOCKS5 with local DNS (``proxy_url='socks5://...``) +- Usernames and passwords for the SOCKS proxy + +.. note:: + It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in + your ``proxy_url`` to ensure that DNS resolution is done from the remote + server instead of client-side when connecting to a domain name. + +SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 +supports IPv4, IPv6, and domain names. + +When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` +will be sent as the ``userid`` section of the SOCKS request: + +.. code-block:: python + + proxy_url="socks4a://@proxy-host" + +When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion +of the ``proxy_url`` will be sent as the username/password to authenticate +with the proxy: + +.. code-block:: python + + proxy_url="socks5h://:@proxy-host" + +""" + +from __future__ import annotations + +try: + import socks # type: ignore[import-untyped] +except ImportError: + import warnings + + from ..exceptions import DependencyWarning + + warnings.warn( + ( + "SOCKS support in urllib3 requires the installation of optional " + "dependencies: specifically, PySocks. For more information, see " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" + ), + DependencyWarning, + ) + raise + +import typing +from socket import timeout as SocketTimeout + +from ..connection import HTTPConnection, HTTPSConnection +from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from ..exceptions import ConnectTimeoutError, NewConnectionError +from ..poolmanager import PoolManager +from ..util.url import parse_url + +try: + import ssl +except ImportError: + ssl = None # type: ignore[assignment] + + +class _TYPE_SOCKS_OPTIONS(typing.TypedDict): + socks_version: int + proxy_host: str | None + proxy_port: str | None + username: str | None + password: str | None + rdns: bool + + +class SOCKSConnection(HTTPConnection): + """ + A plain-text HTTP connection that connects via a SOCKS proxy. + """ + + def __init__( + self, + _socks_options: _TYPE_SOCKS_OPTIONS, + *args: typing.Any, + **kwargs: typing.Any, + ) -> None: + self._socks_options = _socks_options + super().__init__(*args, **kwargs) + + def _new_conn(self) -> socks.socksocket: + """ + Establish a new connection via the SOCKS proxy. + """ + extra_kw: dict[str, typing.Any] = {} + if self.source_address: + extra_kw["source_address"] = self.source_address + + if self.socket_options: + extra_kw["socket_options"] = self.socket_options + + try: + conn = socks.create_connection( + (self.host, self.port), + proxy_type=self._socks_options["socks_version"], + proxy_addr=self._socks_options["proxy_host"], + proxy_port=self._socks_options["proxy_port"], + proxy_username=self._socks_options["username"], + proxy_password=self._socks_options["password"], + proxy_rdns=self._socks_options["rdns"], + timeout=self.timeout, + **extra_kw, + ) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except socks.ProxyError as e: + # This is fragile as hell, but it seems to be the only way to raise + # useful errors here. + if e.socket_err: + error = e.socket_err + if isinstance(error, SocketTimeout): + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + else: + # Adding `from e` messes with coverage somehow, so it's omitted. + # See #2386. + raise NewConnectionError( + self, f"Failed to establish a new connection: {error}" + ) + else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + except OSError as e: # Defensive: PySocks should catch all these. + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + return conn + + +# We don't need to duplicate the Verified/Unverified distinction from +# urllib3/connection.py here because the HTTPSConnection will already have been +# correctly set to either the Verified or Unverified form by that module. This +# means the SOCKSHTTPSConnection will automatically be the correct type. +class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): + pass + + +class SOCKSHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = SOCKSConnection + + +class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): + ConnectionCls = SOCKSHTTPSConnection + + +class SOCKSProxyManager(PoolManager): + """ + A version of the urllib3 ProxyManager that routes connections via the + defined SOCKS proxy. + """ + + pool_classes_by_scheme = { + "http": SOCKSHTTPConnectionPool, + "https": SOCKSHTTPSConnectionPool, + } + + def __init__( + self, + proxy_url: str, + username: str | None = None, + password: str | None = None, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ): + parsed = parse_url(proxy_url) + + if username is None and password is None and parsed.auth is not None: + split = parsed.auth.split(":") + if len(split) == 2: + username, password = split + if parsed.scheme == "socks5": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = False + elif parsed.scheme == "socks5h": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = True + elif parsed.scheme == "socks4": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = False + elif parsed.scheme == "socks4a": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = True + else: + raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") + + self.proxy_url = proxy_url + + socks_options = { + "socks_version": socks_version, + "proxy_host": parsed.host, + "proxy_port": parsed.port, + "username": username, + "password": password, + "rdns": rdns, + } + connection_pool_kw["_socks_options"] = socks_options + + super().__init__(num_pools, headers, **connection_pool_kw) + + self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py new file mode 100644 index 0000000000..3d7e9b93d3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +import socket +import typing +import warnings +from email.errors import MessageDefect +from http.client import IncompleteRead as httplib_IncompleteRead + +if typing.TYPE_CHECKING: + from .connection import HTTPConnection + from .connectionpool import ConnectionPool + from .response import HTTPResponse + from .util.retry import Retry + +# Base Exceptions + + +class HTTPError(Exception): + """Base exception used by this module.""" + + +class HTTPWarning(Warning): + """Base warning used by this module.""" + + +_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] + + +class PoolError(HTTPError): + """Base exception for errors caused within a pool.""" + + def __init__(self, pool: ConnectionPool, message: str) -> None: + self.pool = pool + self._message = message + super().__init__(f"{pool}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + +class RequestError(PoolError): + """Base exception for PoolErrors that have associated URLs.""" + + def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: + self.url = url + super().__init__(pool, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self._message) + + +class SSLError(HTTPError): + """Raised when SSL certificate fails in an HTTPS connection.""" + + +class ProxyError(HTTPError): + """Raised when the connection to a proxy fails.""" + + # The original error is also available as __cause__. + original_error: Exception + + def __init__(self, message: str, error: Exception) -> None: + super().__init__(message, error) + self.original_error = error + + +class DecodeError(HTTPError): + """Raised when automatic decoding based on Content-Type fails.""" + + +class ProtocolError(HTTPError): + """Raised when something unexpected happens mid-request/response.""" + + +#: Renamed to ProtocolError but aliased for backwards compatibility. +ConnectionError = ProtocolError + + +# Leaf Exceptions + + +class MaxRetryError(RequestError): + """Raised when the maximum number of retries is exceeded. + + :param pool: The connection pool + :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` + :param str url: The requested Url + :param reason: The underlying error + :type reason: :class:`Exception` + + """ + + def __init__( + self, pool: ConnectionPool, url: str | None, reason: Exception | None = None + ) -> None: + self.reason = reason + + message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" + + super().__init__(pool, url, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self.url, self.reason) + + +class HostChangedError(RequestError): + """Raised when an existing pool gets a request for a foreign host.""" + + def __init__( + self, pool: ConnectionPool, url: str, retries: Retry | int = 3 + ) -> None: + message = f"Tried to open a foreign host with url: {url}" + super().__init__(pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """Raised when passing an invalid state to a timeout""" + + +class TimeoutError(HTTPError): + """Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + ` and :exc:`ConnectTimeoutErrors `. + """ + + +class ReadTimeoutError(TimeoutError, RequestError): + """Raised when a socket timeout occurs while receiving data from a server""" + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + """Raised when a socket timeout occurs while connecting to a server""" + + +class NewConnectionError(ConnectTimeoutError, HTTPError): + """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" + + def __init__(self, conn: HTTPConnection, message: str) -> None: + self.conn = conn + self._message = message + super().__init__(f"{conn}: {message}") + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (None, self._message) + + @property + def pool(self) -> HTTPConnection: + warnings.warn( + "The 'pool' property is deprecated and will be removed " + "in urllib3 v3.0. Use 'conn' instead.", + FutureWarning, + stacklevel=2, + ) + + return self.conn + + +class NameResolutionError(NewConnectionError): + """Raised when host name resolution fails.""" + + def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): + message = f"Failed to resolve '{host}' ({reason})" + self._host = host + self._reason = reason + super().__init__(conn, message) + + def __reduce__(self) -> _TYPE_REDUCE_RESULT: + # For pickling purposes. + return self.__class__, (self._host, None, self._reason) + + +class EmptyPoolError(PoolError): + """Raised when a pool runs out of connections and no more are allowed.""" + + +class FullPoolError(PoolError): + """Raised when we try to add a connection to a full pool in blocking mode.""" + + +class ClosedPoolError(PoolError): + """Raised when a request enters a pool after the pool has been closed.""" + + +class LocationValueError(ValueError, HTTPError): + """Raised when there is something wrong with a given URL input.""" + + +class LocationParseError(LocationValueError): + """Raised when get_host or similar fails to parse the URL input.""" + + def __init__(self, location: str) -> None: + message = f"Failed to parse: {location}" + super().__init__(message) + + self.location = location + + +class URLSchemeUnknown(LocationValueError): + """Raised when a URL input has an unsupported scheme.""" + + def __init__(self, scheme: str): + message = f"Not supported URL scheme {scheme}" + super().__init__(message) + + self.scheme = scheme + + +class ResponseError(HTTPError): + """Used as a container for an error reason supplied in a MaxRetryError.""" + + GENERIC_ERROR = "too many error responses" + SPECIFIC_ERROR = "too many {status_code} error responses" + + +class SecurityWarning(HTTPWarning): + """Warned when performing security reducing actions""" + + +class InsecureRequestWarning(SecurityWarning): + """Warned when making an unverified HTTPS request.""" + + +class NotOpenSSLWarning(SecurityWarning): + """Warned when using unsupported SSL library""" + + +class SystemTimeWarning(SecurityWarning): + """Warned when system time is suspected to be wrong""" + + +class InsecurePlatformWarning(SecurityWarning): + """Warned when certain TLS/SSL configuration is not available on a platform.""" + + +class DependencyWarning(HTTPWarning): + """ + Warned when an attempt is made to import a module with missing optional + dependencies. + """ + + +class ResponseNotChunked(ProtocolError, ValueError): + """Response needs to be chunked in order to read it as chunks.""" + + +class BodyNotHttplibCompatible(HTTPError): + """ + Body should be :class:`http.client.HTTPResponse` like + (have an fp attribute which returns raw chunks) for read_chunked(). + """ + + +class IncompleteRead(HTTPError, httplib_IncompleteRead): + """ + Response length doesn't match expected Content-Length + + Subclass of :class:`http.client.IncompleteRead` to allow int value + for ``partial`` to avoid creating large objects on streamed reads. + """ + + partial: int # type: ignore[assignment] + expected: int + + def __init__(self, partial: int, expected: int) -> None: + self.partial = partial + self.expected = expected + + def __repr__(self) -> str: + return "IncompleteRead(%i bytes read, %i more expected)" % ( + self.partial, + self.expected, + ) + + +class InvalidChunkLength(HTTPError, httplib_IncompleteRead): + """Invalid chunk length in a chunked response.""" + + def __init__(self, response: HTTPResponse, length: bytes) -> None: + self.partial: int = response.tell() # type: ignore[assignment] + self.expected: int | None = response.length_remaining + self.response = response + self.length = length + + def __repr__(self) -> str: + return "InvalidChunkLength(got length %r, %i bytes read)" % ( + self.length, + self.partial, + ) + + +class InvalidHeader(HTTPError): + """The header provided was somehow invalid.""" + + +class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): + """ProxyManager does not support the supplied scheme""" + + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. + + def __init__(self, scheme: str | None) -> None: + # 'localhost' is here because our URL parser parses + # localhost:8080 -> scheme=localhost, remove if we fix this. + if scheme == "localhost": + scheme = None + if scheme is None: + message = "Proxy URL had no scheme, should start with http:// or https://" + else: + message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" + super().__init__(message) + + +class ProxySchemeUnsupported(ValueError): + """Fetching HTTPS resources through HTTPS proxies is unsupported""" + + +class HeaderParsingError(HTTPError): + """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" + + def __init__( + self, defects: list[MessageDefect], unparsed_data: bytes | str | None + ) -> None: + message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" + super().__init__(message) + + +class UnrewindableBodyError(HTTPError): + """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py new file mode 100644 index 0000000000..fe68e17732 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import email.utils +import mimetypes +import typing + +_TYPE_FIELD_VALUE = typing.Union[str, bytes] +_TYPE_FIELD_VALUE_TUPLE = typing.Union[ + _TYPE_FIELD_VALUE, + tuple[str, _TYPE_FIELD_VALUE], + tuple[str, _TYPE_FIELD_VALUE, str], +] + + +def guess_content_type( + filename: str | None, default: str = "application/octet-stream" +) -> str: + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetypes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Helper function to format and quote a single header parameter using the + strategy defined in RFC 2231. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows + `RFC 2388 Section 4.4 `_. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as ``bytes`` or `str``. + :returns: + An RFC-2231-formatted unicode string. + + .. deprecated:: 2.0.0 + Will be removed in urllib3 v3.0. This is not valid for + ``multipart/form-data`` header parameters. + """ + import warnings + + warnings.warn( + "'format_header_param_rfc2231' is insecure, deprecated and will be " + "removed in urllib3 v3.0. This is not valid for " + "multipart/form-data header parameters.", + FutureWarning, + stacklevel=2, + ) + + if isinstance(value, bytes): + value = value.decode("utf-8") + + if not any(ch in value for ch in '"\\\r\n'): + result = f'{name}="{value}"' + try: + result.encode("ascii") + except (UnicodeEncodeError, UnicodeDecodeError): + pass + else: + return result + + value = email.utils.encode_rfc2231(value, "utf-8") + value = f"{name}*={value}" + + return value + + +def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Format and quote a single multipart header parameter. + + This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching + the behavior of current browser and curl versions. Values are + assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are + percent encoded. + + .. _WHATWG HTML Standard: + https://html.spec.whatwg.org/multipage/ + form-control-infrastructure.html#multipart-form-data + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + :returns: + A string ``name="value"`` with the escaped value. + + .. versionchanged:: 2.0.0 + Matches the WHATWG HTML Standard as of 2021/06/10. Control + characters are no longer percent encoded. + + .. versionchanged:: 2.0.0 + Renamed from ``format_header_param_html5`` and + ``format_header_param``. The old names will be removed in + urllib3 v3.0. + """ + if isinstance(value, bytes): + value = value.decode("utf-8") + + # percent encode \n \r " + value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) + return f'{name}="{value}"' + + +def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param_html5' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + .. deprecated:: 2.0.0 + Renamed to :func:`format_multipart_header_param`. Will be + removed in urllib3 v3.0. + """ + import warnings + + warnings.warn( + "'format_header_param' has been renamed to " + "'format_multipart_header_param'. The old name will be " + "removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + return format_multipart_header_param(name, value) + + +class RequestField: + """ + A data container for request body parameters. + + :param name: + The name of this request field. Must be unicode. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. Must be unicode. + :param headers: + An optional dict-like object of headers to initially use for the field. + + .. versionchanged:: 2.0.0 + The ``header_formatter`` parameter is deprecated and will + be removed in urllib3 v3.0. + """ + + def __init__( + self, + name: str, + data: _TYPE_FIELD_VALUE, + filename: str | None = None, + headers: typing.Mapping[str, str] | None = None, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ): + self._name = name + self._filename = filename + self.data = data + self.headers: dict[str, str | None] = {} + if headers: + self.headers = dict(headers) + + if header_formatter is not None: + import warnings + + warnings.warn( + "The 'header_formatter' parameter is deprecated and " + "will be removed in urllib3 v3.0.", + FutureWarning, + stacklevel=2, + ) + self.header_formatter = header_formatter + else: + self.header_formatter = format_multipart_header_param + + @classmethod + def from_tuples( + cls, + fieldname: str, + value: _TYPE_FIELD_VALUE_TUPLE, + header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, + ) -> RequestField: + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from + parameter of key/value strings AND key/filetuple. A filetuple is a + (filename, data, MIME type) tuple where the MIME type is optional. + For example:: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + filename: str | None + content_type: str | None + data: _TYPE_FIELD_VALUE + + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls( + fieldname, data, filename=filename, header_formatter=header_formatter + ) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: + """ + Override this method to change how each multipart header + parameter is formatted. By default, this calls + :func:`format_multipart_header_param`. + + :param name: + The name of the parameter, an ASCII-only ``str``. + :param value: + The value of the parameter, a ``str`` or UTF-8 encoded + ``bytes``. + + :meta public: + """ + return self.header_formatter(name, value) + + def _render_parts( + self, + header_parts: ( + dict[str, _TYPE_FIELD_VALUE | None] + | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] + ), + ) -> str: + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format + as `k1="v1"; k2="v2"; ...`. + """ + iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] + + parts = [] + if isinstance(header_parts, dict): + iterable = header_parts.items() + else: + iterable = header_parts + + for name, value in iterable: + if value is not None: + parts.append(self._render_part(name, value)) + + return "; ".join(parts) + + def render_headers(self) -> str: + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append(f"{sort_key}: {self.headers[sort_key]}") + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append(f"{header_name}: {header_value}") + + lines.append("\r\n") + return "\r\n".join(lines) + + def make_multipart( + self, + content_disposition: str | None = None, + content_type: str | None = None, + content_location: str | None = None, + ) -> None: + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_disposition: + The 'Content-Disposition' of the request body. Defaults to 'form-data' + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + content_disposition = (content_disposition or "form-data") + "; ".join( + [ + "", + self._render_parts( + (("name", self._name), ("filename", self._filename)) + ), + ] + ) + + self.headers["Content-Disposition"] = content_disposition + self.headers["Content-Type"] = content_type + self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py new file mode 100644 index 0000000000..14f70b05b4 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import binascii +import codecs +import os +import typing +from io import BytesIO + +from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField + +writer = codecs.lookup("utf-8")[3] + +_TYPE_FIELDS_SEQUENCE = typing.Sequence[ + typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] +] +_TYPE_FIELDS = typing.Union[ + _TYPE_FIELDS_SEQUENCE, + typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], +] + + +def choose_boundary() -> str: + """ + Our embarrassingly-simple replacement for mimetools.choose_boundary. + """ + return binascii.hexlify(os.urandom(16)).decode() + + +def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] + + if isinstance(fields, typing.Mapping): + iterable = fields.items() + else: + iterable = fields + + for field in iterable: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def encode_multipart_formdata( + fields: _TYPE_FIELDS, boundary: str | None = None +) -> tuple[bytes, str]: + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`urllib3.filepost.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(f"--{boundary}\r\n".encode("latin-1")) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, str): + writer(body).write(data) + else: + body.write(data) + + body.write(b"\r\n") + + body.write(f"--{boundary}--\r\n".encode("latin-1")) + + content_type = f"multipart/form-data; boundary={boundary}" + + return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py new file mode 100644 index 0000000000..133e1d8f23 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from importlib.metadata import version + +__all__ = [ + "inject_into_urllib3", + "extract_from_urllib3", +] + +import typing + +orig_HTTPSConnection: typing.Any = None + + +def inject_into_urllib3() -> None: + # First check if h2 version is valid + h2_version = version("h2") + if not h2_version.startswith("4."): + raise ImportError( + "urllib3 v2 supports h2 version 4.x.x, currently " + f"the 'h2' module is compiled with {h2_version!r}. " + "See: https://github.com/urllib3/urllib3/issues/3290" + ) + + # Import here to avoid circular dependencies. + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + from .connection import HTTP2Connection + + global orig_HTTPSConnection + orig_HTTPSConnection = urllib3_connection.HTTPSConnection + + HTTPSConnectionPool.ConnectionCls = HTTP2Connection + urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] + + # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. + urllib3_util.ALPN_PROTOCOLS = ["h2"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] + + +def extract_from_urllib3() -> None: + from .. import connection as urllib3_connection + from .. import util as urllib3_util + from ..connectionpool import HTTPSConnectionPool + from ..util import ssl_ as urllib3_util_ssl + + HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection + urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] + + urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] + urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py new file mode 100644 index 0000000000..0a026da0a8 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py @@ -0,0 +1,356 @@ +from __future__ import annotations + +import logging +import re +import threading +import types +import typing + +import h2.config +import h2.connection +import h2.events + +from .._base_connection import _TYPE_BODY +from .._collections import HTTPHeaderDict +from ..connection import HTTPSConnection, _get_default_user_agent +from ..exceptions import ConnectionError +from ..response import BaseHTTPResponse + +orig_HTTPSConnection = HTTPSConnection + +T = typing.TypeVar("T") + +log = logging.getLogger(__name__) + +RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") +RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") + + +def _is_legal_header_name(name: bytes) -> bool: + """ + "An implementation that validates fields according to the definitions in Sections + 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not + include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + + `http.client._is_legal_header_name` does not validate the field name according to the + HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. + + This does not allow for the `:` character in the header name, so should not + be used to validate pseudo-headers. + """ + return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) + + +def _is_illegal_header_value(value: bytes) -> bool: + """ + "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed + (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field + value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, + 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) + """ + return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) + + +class _LockedObject(typing.Generic[T]): + """ + A wrapper class that hides a specific object behind a lock. + The goal here is to provide a simple way to protect access to an object + that cannot safely be simultaneously accessed from multiple threads. The + intended use of this class is simple: take hold of it with a context + manager, which returns the protected object. + """ + + __slots__ = ( + "lock", + "_obj", + ) + + def __init__(self, obj: T): + self.lock = threading.RLock() + self._obj = obj + + def __enter__(self) -> T: + self.lock.acquire() + return self._obj + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + self.lock.release() + + +class HTTP2Connection(HTTPSConnection): + def __init__( + self, host: str, port: int | None = None, **kwargs: typing.Any + ) -> None: + self._h2_conn = self._new_h2_conn() + self._h2_stream: int | None = None + self._headers: list[tuple[bytes, bytes]] = [] + + if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: + raise NotImplementedError("Proxies aren't supported with HTTP/2") + + super().__init__(host, port, **kwargs) + + if self._tunnel_host is not None: + raise NotImplementedError("Tunneling isn't supported with HTTP/2") + + def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: + config = h2.config.H2Configuration(client_side=True) + return _LockedObject(h2.connection.H2Connection(config=config)) + + def connect(self) -> None: + super().connect() + with self._h2_conn as conn: + conn.initiate_connection() + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + def putrequest( # type: ignore[override] + self, + method: str, + url: str, + **kwargs: typing.Any, + ) -> None: + """putrequest + This deviates from the HTTPConnection method signature since we never need to override + sending accept-encoding headers or the host header. + """ + if "skip_host" in kwargs: + raise NotImplementedError("`skip_host` isn't supported") + if "skip_accept_encoding" in kwargs: + raise NotImplementedError("`skip_accept_encoding` isn't supported") + + self._request_url = url or "/" + self._validate_path(url) # type: ignore[attr-defined] + + if ":" in self.host: + authority = f"[{self.host}]:{self.port or 443}" + else: + authority = f"{self.host}:{self.port or 443}" + + self._headers.append((b":scheme", b"https")) + self._headers.append((b":method", method.encode())) + self._headers.append((b":authority", authority.encode())) + self._headers.append((b":path", url.encode())) + + with self._h2_conn as conn: + self._h2_stream = conn.get_next_available_stream_id() + + def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] + # TODO SKIPPABLE_HEADERS from urllib3 are ignored. + header = header.encode() if isinstance(header, str) else header + header = header.lower() # A lot of upstream code uses capitalized headers. + if not _is_legal_header_name(header): + raise ValueError(f"Illegal header name {str(header)}") + + for value in values: + value = value.encode() if isinstance(value, str) else value + if _is_illegal_header_value(value): + raise ValueError(f"Illegal header value {str(value)}") + self._headers.append((header, value)) + + def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + conn.send_headers( + stream_id=self._h2_stream, + headers=self._headers, + end_stream=(message_body is None), + ) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + self._headers = [] # Reset headers for the next request. + + def send(self, data: typing.Any) -> None: + """Send data to the server. + `data` can be: `str`, `bytes`, an iterable, or file-like objects + that support a .read() method. + """ + if self._h2_stream is None: + raise ConnectionError("Must call `putrequest` first.") + + with self._h2_conn as conn: + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + if hasattr(data, "read"): # file-like objects + while True: + chunk = data.read(self.blocksize) + if not chunk: + break + if isinstance(chunk, str): + chunk = chunk.encode() + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + return + + if isinstance(data, str): # str -> bytes + data = data.encode() + + try: + if isinstance(data, bytes): + conn.send_data(self._h2_stream, data, end_stream=True) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + else: + for chunk in data: + conn.send_data(self._h2_stream, chunk, end_stream=False) + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + conn.end_stream(self._h2_stream) + except TypeError: + raise TypeError( + "`data` should be str, bytes, iterable, or file. got %r" + % type(data) + ) + + def set_tunnel( + self, + host: str, + port: int | None = None, + headers: typing.Mapping[str, str] | None = None, + scheme: str = "http", + ) -> None: + raise NotImplementedError( + "HTTP/2 does not support setting up a tunnel through a proxy" + ) + + def getresponse( # type: ignore[override] + self, + ) -> HTTP2Response: + status = None + data = bytearray() + with self._h2_conn as conn: + end_stream = False + while not end_stream: + # TODO: Arbitrary read value. + if received_data := self.sock.recv(65535): + events = conn.receive_data(received_data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + headers = HTTPHeaderDict() + for header, value in event.headers: + if header == b":status": + status = int(value.decode()) + else: + headers.add( + header.decode("ascii"), value.decode("ascii") + ) + + elif isinstance(event, h2.events.DataReceived): + data += event.data + conn.acknowledge_received_data( + event.flow_controlled_length, event.stream_id + ) + + elif isinstance(event, h2.events.StreamEnded): + end_stream = True + + if data_to_send := conn.data_to_send(): + self.sock.sendall(data_to_send) + + assert status is not None + return HTTP2Response( + status=status, + headers=headers, + request_url=self._request_url, + data=bytes(data), + ) + + def request( # type: ignore[override] + self, + method: str, + url: str, + body: _TYPE_BODY | None = None, + headers: typing.Mapping[str, str] | None = None, + *, + preload_content: bool = True, + decode_content: bool = True, + enforce_content_length: bool = True, + **kwargs: typing.Any, + ) -> None: + """Send an HTTP/2 request""" + if "chunked" in kwargs: + # TODO this is often present from upstream. + # raise NotImplementedError("`chunked` isn't supported with HTTP/2") + pass + + if self.sock is not None: + self.sock.settimeout(self.timeout) + + self.putrequest(method, url) + + headers = headers or {} + for k, v in headers.items(): + if k.lower() == "transfer-encoding" and v == "chunked": + continue + else: + self.putheader(k, v) + + if b"user-agent" not in dict(self._headers): + self.putheader(b"user-agent", _get_default_user_agent()) + + if body: + self.endheaders(message_body=body) + self.send(body) + else: + self.endheaders() + + def close(self) -> None: + with self._h2_conn as conn: + try: + conn.close_connection() + if data := conn.data_to_send(): + self.sock.sendall(data) + except Exception: + pass + + # Reset all our HTTP/2 connection state. + self._h2_conn = self._new_h2_conn() + self._h2_stream = None + self._headers = [] + + super().close() + + +class HTTP2Response(BaseHTTPResponse): + # TODO: This is a woefully incomplete response object, but works for non-streaming. + def __init__( + self, + status: int, + headers: HTTPHeaderDict, + request_url: str, + data: bytes, + decode_content: bool = False, # TODO: support decoding + ) -> None: + super().__init__( + status=status, + headers=headers, + # Following CPython, we map HTTP versions to major * 10 + minor integers + version=20, + version_string="HTTP/2", + # No reason phrase in HTTP/2 + reason=None, + decode_content=decode_content, + request_url=request_url, + ) + self._data = data + self.length_remaining = 0 + + @property + def data(self) -> bytes: + return self._data + + def get_redirect_location(self) -> None: + return None + + def close(self) -> None: + pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py new file mode 100644 index 0000000000..9ea900764f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import threading + + +class _HTTP2ProbeCache: + __slots__ = ( + "_lock", + "_cache_locks", + "_cache_values", + ) + + def __init__(self) -> None: + self._lock = threading.Lock() + self._cache_locks: dict[tuple[str, int], threading.RLock] = {} + self._cache_values: dict[tuple[str, int], bool | None] = {} + + def acquire_and_get(self, host: str, port: int) -> bool | None: + # By the end of this block we know that + # _cache_[values,locks] is available. + value = None + with self._lock: + key = (host, port) + try: + value = self._cache_values[key] + # If it's a known value we return right away. + if value is not None: + return value + except KeyError: + self._cache_locks[key] = threading.RLock() + self._cache_values[key] = None + + # If the value is unknown, we acquire the lock to signal + # to the requesting thread that the probe is in progress + # or that the current thread needs to return their findings. + key_lock = self._cache_locks[key] + key_lock.acquire() + try: + # If the by the time we get the lock the value has been + # updated we want to return the updated value. + value = self._cache_values[key] + + # In case an exception like KeyboardInterrupt is raised here. + except BaseException as e: # Defensive: + assert not isinstance(e, KeyError) # KeyError shouldn't be possible. + key_lock.release() + raise + + return value + + def set_and_release( + self, host: str, port: int, supports_http2: bool | None + ) -> None: + key = (host, port) + key_lock = self._cache_locks[key] + with key_lock: # Uses an RLock, so can be locked again from same thread. + if supports_http2 is None and self._cache_values[key] is not None: + raise ValueError( + "Cannot reset HTTP/2 support for origin after value has been set." + ) # Defensive: not expected in normal usage + + self._cache_values[key] = supports_http2 + key_lock.release() + + def _values(self) -> dict[tuple[str, int], bool | None]: + """This function is for testing purposes only. Gets the current state of the probe cache""" + with self._lock: + return {k: v for k, v in self._cache_values.items()} + + def _reset(self) -> None: + """This function is for testing purposes only. Reset the cache values""" + with self._lock: + self._cache_locks = {} + self._cache_values = {} + + +_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() + +set_and_release = _HTTP2_PROBE_CACHE.set_and_release +acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get +_values = _HTTP2_PROBE_CACHE._values +_reset = _HTTP2_PROBE_CACHE._reset + +__all__ = [ + "set_and_release", + "acquire_and_get", +] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py new file mode 100644 index 0000000000..8f2c56745c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py @@ -0,0 +1,653 @@ +from __future__ import annotations + +import functools +import logging +import typing +import warnings +from types import TracebackType +from urllib.parse import urljoin + +from ._collections import HTTPHeaderDict, RecentlyUsedContainer +from ._request_methods import RequestMethods +from .connection import ProxyConfig +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme +from .exceptions import ( + LocationValueError, + MaxRetryError, + ProxySchemeUnknown, + URLSchemeUnknown, +) +from .response import BaseHTTPResponse +from .util.connection import _TYPE_SOCKET_OPTIONS +from .util.proxy import connection_requires_http_tunnel +from .util.retry import Retry +from .util.timeout import Timeout +from .util.url import Url, parse_url + +if typing.TYPE_CHECKING: + import ssl + + from typing_extensions import Self + +__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] + + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ( + "key_file", + "cert_file", + "cert_reqs", + "ca_certs", + "ca_cert_data", + "ssl_version", + "ssl_minimum_version", + "ssl_maximum_version", + "ca_cert_dir", + "ssl_context", + "key_password", + "server_hostname", +) +# Default value for `blocksize` - a new parameter introduced to +# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 +_DEFAULT_BLOCKSIZE = 16384 + + +class PoolKey(typing.NamedTuple): + """ + All known keyword arguments that could be provided to the pool manager, its + pools, or the underlying connections. + + All custom key schemes should include the fields in this key at a minimum. + """ + + key_scheme: str + key_host: str + key_port: int | None + key_timeout: Timeout | float | int | None + key_retries: Retry | bool | int | None + key_block: bool | None + key_source_address: tuple[str, int] | None + key_key_file: str | None + key_key_password: str | None + key_cert_file: str | None + key_cert_reqs: str | None + key_ca_certs: str | None + key_ca_cert_data: str | bytes | None + key_ssl_version: int | str | None + key_ssl_minimum_version: ssl.TLSVersion | None + key_ssl_maximum_version: ssl.TLSVersion | None + key_ca_cert_dir: str | None + key_ssl_context: ssl.SSLContext | None + key_maxsize: int | None + key_headers: frozenset[tuple[str, str]] | None + key__proxy: Url | None + key__proxy_headers: frozenset[tuple[str, str]] | None + key__proxy_config: ProxyConfig | None + key_socket_options: _TYPE_SOCKET_OPTIONS | None + key__socks_options: frozenset[tuple[str, str]] | None + key_assert_hostname: bool | str | None + key_assert_fingerprint: str | None + key_server_hostname: str | None + key_blocksize: int | None + + +def _default_key_normalizer( + key_class: type[PoolKey], request_context: dict[str, typing.Any] +) -> PoolKey: + """ + Create a pool key out of a request context dictionary. + + According to RFC 3986, both the scheme and host are case-insensitive. + Therefore, this function normalizes both before constructing the pool + key for an HTTPS request. If you wish to change this behaviour, provide + alternate callables to ``key_fn_by_scheme``. + + :param key_class: + The class to use when constructing the key. This should be a namedtuple + with the ``scheme`` and ``host`` keys at a minimum. + :type key_class: namedtuple + :param request_context: + A dictionary-like object that contain the context for a request. + :type request_context: dict + + :return: A namedtuple that can be used as a connection pool key. + :rtype: PoolKey + """ + # Since we mutate the dictionary, make a copy first + context = request_context.copy() + context["scheme"] = context["scheme"].lower() + context["host"] = context["host"].lower() + + # These are both dictionaries and need to be transformed into frozensets + for key in ("headers", "_proxy_headers", "_socks_options"): + if key in context and context[key] is not None: + context[key] = frozenset(context[key].items()) + + # The socket_options key may be a list and needs to be transformed into a + # tuple. + socket_opts = context.get("socket_options") + if socket_opts is not None: + context["socket_options"] = tuple(socket_opts) + + # Map the kwargs to the names in the namedtuple - this is necessary since + # namedtuples can't have fields starting with '_'. + for key in list(context.keys()): + context["key_" + key] = context.pop(key) + + # Default to ``None`` for keys missing from the context + for field in key_class._fields: + if field not in context: + context[field] = None + + # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context + if context.get("key_blocksize") is None: + context["key_blocksize"] = _DEFAULT_BLOCKSIZE + + return key_class(**context) + + +#: A dictionary that maps a scheme to a callable that creates a pool key. +#: This can be used to alter the way pool keys are constructed, if desired. +#: Each PoolManager makes a copy of this dictionary so they can be configured +#: globally here, or individually on the instance. +key_fn_by_scheme = { + "http": functools.partial(_default_key_normalizer, PoolKey), + "https": functools.partial(_default_key_normalizer, PoolKey), +} + +pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \\**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example: + + .. code-block:: python + + import urllib3 + + http = urllib3.PoolManager(num_pools=2) + + resp1 = http.request("GET", "https://google.com/") + resp2 = http.request("GET", "https://google.com/mail") + resp3 = http.request("GET", "https://yahoo.com/") + + print(len(http.pools)) + # 2 + + """ + + proxy: Url | None = None + proxy_config: ProxyConfig | None = None + + def __init__( + self, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + super().__init__(headers) + # PoolManager handles redirects itself in PoolManager.urlopen(). + # It always passes redirect=False to the underlying connection pool to + # suppress per-pool redirect handling. If the user supplied a non-Retry + # value (int/bool/etc) for retries and we let the pool normalize it + # while redirect=False, the resulting Retry object would have redirect + # handling disabled, which can interfere with PoolManager's own + # redirect logic. Normalize here so redirects remain governed solely by + # PoolManager logic. + if "retries" in connection_pool_kw: + retries = connection_pool_kw["retries"] + if not isinstance(retries, Retry): + retries = Retry.from_int(retries) + connection_pool_kw = connection_pool_kw.copy() + connection_pool_kw["retries"] = retries + self.connection_pool_kw = connection_pool_kw + + self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] + self.pools = RecentlyUsedContainer(num_pools) + + # Locally set the pool classes and keys so other PoolManagers can + # override them. + self.pool_classes_by_scheme = pool_classes_by_scheme + self.key_fn_by_scheme = key_fn_by_scheme.copy() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> typing.Literal[False]: + self.clear() + # Return False to re-raise any potential exceptions + return False + + def _new_pool( + self, + scheme: str, + host: str, + port: int, + request_context: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and + any additional pool keyword arguments. + + If ``request_context`` is provided, it is provided as keyword arguments + to the pool class used. This method is used to actually create the + connection pools handed out by :meth:`connection_from_url` and + companion methods. It is intended to be overridden for customization. + """ + pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] + if request_context is None: + request_context = self.connection_pool_kw.copy() + + # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly + # set to 'None' in the request_context. + if request_context.get("blocksize") is None: + request_context["blocksize"] = _DEFAULT_BLOCKSIZE + + # Although the context has everything necessary to create the pool, + # this function has historically only used the scheme, host, and port + # in the positional args. When an API change is acceptable these can + # be removed. + for key in ("scheme", "host", "port"): + request_context.pop(key, None) + + if scheme == "http": + for kw in SSL_KEYWORDS: + request_context.pop(kw, None) + + return pool_cls(host, port, **request_context) + + def clear(self) -> None: + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is + provided, it is merged with the instance's ``connection_pool_kw`` + variable and used to create the new connection pool, if one is + needed. + """ + + if not host: + raise LocationValueError("No host specified.") + + request_context = self._merge_pool_kwargs(pool_kwargs) + request_context["scheme"] = scheme or "http" + if not port: + port = port_by_scheme.get(request_context["scheme"].lower(), 80) + request_context["port"] = port + request_context["host"] = host + + return self.connection_from_context(request_context) + + def connection_from_context( + self, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. + + ``request_context`` must at least contain the ``scheme`` key and its + value must be a key in ``key_fn_by_scheme`` instance variable. + """ + if "strict" in request_context: + warnings.warn( + "The 'strict' parameter is no longer needed on Python 3+. " + "This will raise an error in urllib3 v3.0.", + FutureWarning, + ) + request_context.pop("strict") + + scheme = request_context["scheme"].lower() + pool_key_constructor = self.key_fn_by_scheme.get(scheme) + if not pool_key_constructor: + raise URLSchemeUnknown(scheme) + pool_key = pool_key_constructor(request_context) + + return self.connection_from_pool_key(pool_key, request_context=request_context) + + def connection_from_pool_key( + self, pool_key: PoolKey, request_context: dict[str, typing.Any] + ) -> HTTPConnectionPool: + """ + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. + + ``pool_key`` should be a namedtuple that only contains immutable + objects. At a minimum it must have the ``scheme``, ``host``, and + ``port`` fields. + """ + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + scheme = request_context["scheme"] + host = request_context["host"] + port = request_context["port"] + pool = self._new_pool(scheme, host, port, request_context=request_context) + self.pools[pool_key] = pool + + return pool + + def connection_from_url( + self, url: str, pool_kwargs: dict[str, typing.Any] | None = None + ) -> HTTPConnectionPool: + """ + Similar to :func:`urllib3.connectionpool.connection_from_url`. + + If ``pool_kwargs`` is not provided and a new pool needs to be + constructed, ``self.connection_pool_kw`` is used to initialize + the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` + is provided, it is used instead. Note that if a new pool does not + need to be created for the request, the provided ``pool_kwargs`` are + not used. + """ + u = parse_url(url) + return self.connection_from_host( + u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs + ) + + def _merge_pool_kwargs( + self, override: dict[str, typing.Any] | None + ) -> dict[str, typing.Any]: + """ + Merge a dictionary of override values for self.connection_pool_kw. + + This does not modify self.connection_pool_kw and returns a new dict. + Any keys in the override dictionary with a value of ``None`` are + removed from the merged dictionary. + """ + base_pool_kwargs = self.connection_pool_kw.copy() + if override: + for key, value in override.items(): + if value is None: + try: + del base_pool_kwargs[key] + except KeyError: + pass + else: + base_pool_kwargs[key] = value + return base_pool_kwargs + + def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: + """ + Indicates if the proxy requires the complete destination URL in the + request. Normally this is only needed when not using an HTTP CONNECT + tunnel. + """ + if self.proxy is None: + return False + + return not connection_requires_http_tunnel( + self.proxy, self.proxy_config, parsed_url.scheme + ) + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + """ + Same as :meth:`urllib3.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + + if u.scheme is None: + warnings.warn( + "URLs without a scheme (ie 'https://') are deprecated and will raise an error " + "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " + "start with 'https://' or 'http://'. Read more in this issue: " + "https://github.com/urllib3/urllib3/issues/2920", + category=FutureWarning, + stacklevel=2, + ) + + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw["assert_same_host"] = False + kw["redirect"] = False + + if "headers" not in kw: + kw["headers"] = self.headers + + if self._proxy_requires_url_absolute_form(u): + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + if response.status == 303: + # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" + # And lose the body not to transfer anything sensitive. + kw["body"] = None + kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + + retries = kw.get("retries", response.retries) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + + # Strip headers marked as unsafe to forward to the redirected location. + # Check remove_headers_on_redirect to avoid a potential network call within + # conn.is_same_host() which may use socket.gethostbyname() in the future. + if retries.remove_headers_on_redirect and not conn.is_same_host( + redirect_location + ): + new_headers = kw["headers"].copy() + for header in kw["headers"]: + if header.lower() in retries.remove_headers_on_redirect: + new_headers.pop(header, None) + kw["headers"] = new_headers + + try: + retries = retries.increment(method, url, response=response, _pool=conn) + except MaxRetryError: + if retries.raise_on_redirect: + response.drain_conn() + raise + return response + + kw["retries"] = retries + kw["redirect"] = redirect + + log.info("Redirecting %s -> %s", url, redirect_location) + + response.drain_conn() + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary containing headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + :param proxy_ssl_context: + The proxy SSL context is used to establish the TLS connection to the + proxy when using HTTPS proxies. + + :param use_forwarding_for_https: + (Defaults to False) If set to True will forward requests to the HTTPS + proxy to be made on behalf of the client instead of creating a TLS + tunnel via the CONNECT method. **Enabling this flag means that request + and response headers and content will be visible from the HTTPS proxy** + whereas tunneling keeps request and response headers and content + private. IP address, target hostname, SNI, and port are always visible + to an HTTPS proxy even when this flag is disabled. + + :param proxy_assert_hostname: + The hostname of the certificate to verify against. + + :param proxy_assert_fingerprint: + The fingerprint of the certificate to verify against. + + Example: + + .. code-block:: python + + import urllib3 + + proxy = urllib3.ProxyManager("https://localhost:3128/") + + resp1 = proxy.request("GET", "http://google.com/") + resp2 = proxy.request("GET", "http://httpbin.org/") + + # One pool was shared by both plain HTTP requests. + print(len(proxy.pools)) + # 1 + + resp3 = proxy.request("GET", "https://httpbin.org/") + resp4 = proxy.request("GET", "https://twitter.com/") + + # A separate pool was added for each HTTPS target. + print(len(proxy.pools)) + # 3 + + """ + + def __init__( + self, + proxy_url: str, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + proxy_headers: typing.Mapping[str, str] | None = None, + proxy_ssl_context: ssl.SSLContext | None = None, + use_forwarding_for_https: bool = False, + proxy_assert_hostname: None | str | typing.Literal[False] = None, + proxy_assert_fingerprint: str | None = None, + **connection_pool_kw: typing.Any, + ) -> None: + if isinstance(proxy_url, HTTPConnectionPool): + str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" + else: + str_proxy_url = proxy_url + proxy = parse_url(str_proxy_url) + + if proxy.scheme not in ("http", "https"): + raise ProxySchemeUnknown(proxy.scheme) + + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + self.proxy_ssl_context = proxy_ssl_context + self.proxy_config = ProxyConfig( + proxy_ssl_context, + use_forwarding_for_https, + proxy_assert_hostname, + proxy_assert_fingerprint, + ) + + connection_pool_kw["_proxy"] = self.proxy + connection_pool_kw["_proxy_headers"] = self.proxy_headers + connection_pool_kw["_proxy_config"] = self.proxy_config + + super().__init__(num_pools, headers, **connection_pool_kw) + + def connection_from_host( + self, + host: str | None, + port: int | None = None, + scheme: str | None = "http", + pool_kwargs: dict[str, typing.Any] | None = None, + ) -> HTTPConnectionPool: + if scheme == "https": + return super().connection_from_host( + host, port, scheme, pool_kwargs=pool_kwargs + ) + + return super().connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] + ) + + def _set_proxy_headers( + self, url: str, headers: typing.Mapping[str, str] | None = None + ) -> typing.Mapping[str, str]: + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {"Accept": "*/*"} + + netloc = parse_url(url).netloc + if netloc: + headers_["Host"] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen( # type: ignore[override] + self, method: str, url: str, redirect: bool = True, **kw: typing.Any + ) -> BaseHTTPResponse: + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): + # For connections using HTTP CONNECT, httplib sets the necessary + # headers on the CONNECT to the proxy. If we're not using CONNECT, + # we'll definitely need to set 'Host' at the very least. + headers = kw.get("headers", self.headers) + kw["headers"] = self._set_proxy_headers(url, headers) + + return super().urlopen(method, url, redirect=redirect, **kw) + + +def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: + return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed new file mode 100644 index 0000000000..5f3ea3d919 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed @@ -0,0 +1,2 @@ +# Instruct type checkers to look for inline type annotations in this package. +# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py new file mode 100644 index 0000000000..e9246b75e3 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py @@ -0,0 +1,1493 @@ +from __future__ import annotations + +import collections +import io +import json as _json +import logging +import socket +import sys +import typing +import warnings +import zlib +from contextlib import contextmanager +from http.client import HTTPMessage as _HttplibHTTPMessage +from http.client import HTTPResponse as _HttplibHTTPResponse +from socket import timeout as SocketTimeout + +if typing.TYPE_CHECKING: + from ._base_connection import BaseHTTPConnection + +try: + try: + import brotlicffi as brotli # type: ignore[import-not-found] + except ImportError: + import brotli # type: ignore[import-not-found] +except ImportError: + brotli = None + +from . import util +from ._base_connection import _TYPE_BODY +from ._collections import HTTPHeaderDict +from .connection import BaseSSLError, HTTPConnection, HTTPException +from .exceptions import ( + BodyNotHttplibCompatible, + DecodeError, + DependencyWarning, + HTTPError, + IncompleteRead, + InvalidChunkLength, + InvalidHeader, + ProtocolError, + ReadTimeoutError, + ResponseNotChunked, + SSLError, +) +from .util.response import is_fp_closed, is_response_to_head +from .util.retry import Retry + +if typing.TYPE_CHECKING: + from .connectionpool import HTTPConnectionPool + +log = logging.getLogger(__name__) + + +class ContentDecoder: + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + raise NotImplementedError() + + @property + def has_unconsumed_tail(self) -> bool: + raise NotImplementedError() + + def flush(self) -> bytes: + raise NotImplementedError() + + +class DeflateDecoder(ContentDecoder): + def __init__(self) -> None: + self._first_try = True + self._first_try_data = b"" + self._unfed_data = b"" + self._obj = zlib.decompressobj() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + data = self._unfed_data + data + self._unfed_data = b"" + if not data and not self._obj.unconsumed_tail: + return data + original_max_length = max_length + if original_max_length < 0: + max_length = 0 + elif original_max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unfed_data = data + return b"" + + # Subsequent calls always reuse `self._obj`. zlib requires + # passing the unconsumed tail if decompression is to continue. + if not self._first_try: + return self._obj.decompress( + self._obj.unconsumed_tail + data, max_length=max_length + ) + + # First call tries with RFC 1950 ZLIB format. + self._first_try_data += data + try: + decompressed = self._obj.decompress(data, max_length=max_length) + if decompressed: + self._first_try = False + self._first_try_data = b"" + return decompressed + # On failure, it falls back to RFC 1951 DEFLATE format. + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress( + self._first_try_data, max_length=original_max_length + ) + finally: + self._first_try_data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unfed_data) or ( + bool(self._obj.unconsumed_tail) and not self._first_try + ) + + def flush(self) -> bytes: + return self._obj.flush() + + +class GzipDecoderState: + FIRST_MEMBER = 0 + OTHER_MEMBERS = 1 + SWALLOW_DATA = 2 + + +class GzipDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + self._state = GzipDecoderState.FIRST_MEMBER + self._unconsumed_tail = b"" + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + ret = bytearray() + if self._state == GzipDecoderState.SWALLOW_DATA: + return bytes(ret) + + if max_length == 0: + # We should not pass 0 to the zlib decompressor because 0 is + # the default value that will make zlib decompress without a + # length limit. + # Data should be stored for subsequent calls. + self._unconsumed_tail += data + return b"" + + # zlib requires passing the unconsumed tail to the subsequent + # call if decompression is to continue. + data = self._unconsumed_tail + data + if not data and self._obj.eof: + return bytes(ret) + + while True: + try: + ret += self._obj.decompress( + data, max_length=max(max_length - len(ret), 0) + ) + except zlib.error: + previous_state = self._state + # Ignore data after the first error + self._state = GzipDecoderState.SWALLOW_DATA + self._unconsumed_tail = b"" + if previous_state == GzipDecoderState.OTHER_MEMBERS: + # Allow trailing garbage acceptable in other gzip clients + return bytes(ret) + raise + + self._unconsumed_tail = data = ( + self._obj.unconsumed_tail or self._obj.unused_data + ) + if max_length > 0 and len(ret) >= max_length: + break + + if not data: + return bytes(ret) + # When the end of a gzip member is reached, a new decompressor + # must be created for unused (possibly future) data. + if self._obj.eof: + self._state = GzipDecoderState.OTHER_MEMBERS + self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) + + return bytes(ret) + + @property + def has_unconsumed_tail(self) -> bool: + return bool(self._unconsumed_tail) + + def flush(self) -> bytes: + return self._obj.flush() + + +if brotli is not None: + + class BrotliDecoder(ContentDecoder): + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' + def __init__(self) -> None: + self._obj = brotli.Decompressor() + if hasattr(self._obj, "decompress"): + setattr(self, "_decompress", self._obj.decompress) + else: + setattr(self, "_decompress", self._obj.process) + + # Requires Brotli >= 1.2.0 for `output_buffer_limit`. + def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: + raise NotImplementedError() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + try: + if max_length > 0: + return self._decompress(data, output_buffer_limit=max_length) + else: + return self._decompress(data) + except TypeError: + # Fallback for Brotli/brotlicffi/brotlipy versions without + # the `output_buffer_limit` parameter. + warnings.warn( + "Brotli >= 1.2.0 is required to prevent decompression bombs.", + DependencyWarning, + ) + return self._decompress(data) + + @property + def has_unconsumed_tail(self) -> bool: + try: + return not self._obj.can_accept_more_data() + except AttributeError: + return False + + def flush(self) -> bytes: + if hasattr(self._obj, "flush"): + return self._obj.flush() # type: ignore[no-any-return] + return b"" + + +try: + if sys.version_info >= (3, 14): + from compression import zstd + else: + from backports import zstd +except ImportError: + HAS_ZSTD = False +else: + HAS_ZSTD = True + + class ZstdDecoder(ContentDecoder): + def __init__(self) -> None: + self._obj = zstd.ZstdDecompressor() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if not data and not self.has_unconsumed_tail: + return b"" + if self._obj.eof: + data = self._obj.unused_data + data + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress(data, max_length=max_length) + length = len(part) + data_parts = [part] + # Every loop iteration is supposed to read data from a separate frame. + # The loop breaks when: + # - enough data is read; + # - no more unused data is available; + # - end of the last read frame has not been reached (i.e., + # more data has to be fed). + while ( + self._obj.eof + and self._obj.unused_data + and (max_length < 0 or length < max_length) + ): + unused_data = self._obj.unused_data + if not self._obj.needs_input: + self._obj = zstd.ZstdDecompressor() + part = self._obj.decompress( + unused_data, + max_length=(max_length - length) if max_length > 0 else -1, + ) + if part_length := len(part): + data_parts.append(part) + length += part_length + elif self._obj.needs_input: + break + return b"".join(data_parts) + + @property + def has_unconsumed_tail(self) -> bool: + return not (self._obj.needs_input or self._obj.eof) or bool( + self._obj.unused_data + ) + + def flush(self) -> bytes: + if not self._obj.eof: + raise DecodeError("Zstandard data is incomplete") + return b"" + + +class MultiDecoder(ContentDecoder): + """ + From RFC7231: + If one or more encodings have been applied to a representation, the + sender that applied the encodings MUST generate a Content-Encoding + header field that lists the content codings in the order in which + they were applied. + """ + + # Maximum allowed number of chained HTTP encodings in the + # Content-Encoding header. + max_decode_links = 5 + + def __init__(self, modes: str) -> None: + encodings = [m.strip() for m in modes.split(",")] + if len(encodings) > self.max_decode_links: + raise DecodeError( + "Too many content encodings in the chain: " + f"{len(encodings)} > {self.max_decode_links}" + ) + self._decoders = [_get_decoder(e) for e in encodings] + + def flush(self) -> bytes: + return self._decoders[0].flush() + + def decompress(self, data: bytes, max_length: int = -1) -> bytes: + if max_length <= 0: + for d in reversed(self._decoders): + data = d.decompress(data) + return data + + ret = bytearray() + # Every while loop iteration goes through all decoders once. + # It exits when enough data is read or no more data can be read. + # It is possible that the while loop iteration does not produce + # any data because we retrieve up to `max_length` from every + # decoder, and the amount of bytes may be insufficient for the + # next decoder to produce enough/any output. + while True: + any_data = False + for d in reversed(self._decoders): + data = d.decompress(data, max_length=max_length - len(ret)) + if data: + any_data = True + # We should not break when no data is returned because + # next decoders may produce data even with empty input. + ret += data + if not any_data or len(ret) >= max_length: + return bytes(ret) + data = b"" + + @property + def has_unconsumed_tail(self) -> bool: + return any(d.has_unconsumed_tail for d in self._decoders) + + +def _get_decoder(mode: str) -> ContentDecoder: + if "," in mode: + return MultiDecoder(mode) + + # According to RFC 9110 section 8.4.1.3, recipients should + # consider x-gzip equivalent to gzip + if mode in ("gzip", "x-gzip"): + return GzipDecoder() + + if brotli is not None and mode == "br": + return BrotliDecoder() + + if HAS_ZSTD and mode == "zstd": + return ZstdDecoder() + + return DeflateDecoder() + + +class BytesQueueBuffer: + """Memory-efficient bytes buffer + + To return decoded data in read() and still follow the BufferedIOBase API, we need a + buffer to always return the correct amount of bytes. + + This buffer should be filled using calls to put() + + Our maximum memory usage is determined by the sum of the size of: + + * self.buffer, which contains the full data + * the largest chunk that we will copy in get() + """ + + def __init__(self) -> None: + self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() + self._size: int = 0 + + def __len__(self) -> int: + return self._size + + def put(self, data: bytes) -> None: + self.buffer.append(data) + self._size += len(data) + + def get(self, n: int) -> bytes: + if n == 0: + return b"" + elif not self.buffer: + raise RuntimeError("buffer is empty") + elif n < 0: + raise ValueError("n should be > 0") + + if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): + self._size -= n + return self.buffer.popleft() + + fetched = 0 + ret = io.BytesIO() + while fetched < n: + remaining = n - fetched + chunk = self.buffer.popleft() + chunk_length = len(chunk) + if remaining < chunk_length: + chunk = memoryview(chunk) + left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] + ret.write(left_chunk) + self.buffer.appendleft(right_chunk) + self._size -= remaining + break + else: + ret.write(chunk) + self._size -= chunk_length + fetched += chunk_length + + if not self.buffer: + break + + return ret.getvalue() + + def get_all(self) -> bytes: + buffer = self.buffer + if not buffer: + assert self._size == 0 + return b"" + if len(buffer) == 1: + result = buffer.pop() + if isinstance(result, memoryview): + result = result.tobytes() + else: + ret = io.BytesIO() + ret.writelines(buffer.popleft() for _ in range(len(buffer))) + result = ret.getvalue() + self._size = 0 + return result + + +class BaseHTTPResponse(io.IOBase): + CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] + if brotli is not None: + CONTENT_DECODERS += ["br"] + if HAS_ZSTD: + CONTENT_DECODERS += ["zstd"] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) + if brotli is not None: + DECODER_ERROR_CLASSES += (brotli.error,) + + if HAS_ZSTD: + DECODER_ERROR_CLASSES += (zstd.ZstdError,) + + def __init__( + self, + *, + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int, + version: int, + version_string: str, + reason: str | None, + decode_content: bool, + request_url: str | None, + retries: Retry | None = None, + ) -> None: + if isinstance(headers, HTTPHeaderDict): + self.headers = headers + else: + self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] + self.status = status + self.version = version + self.version_string = version_string + self.reason = reason + self.decode_content = decode_content + self._has_decoded_content = False + self._request_url: str | None = request_url + self.retries = retries + + self.chunked = False + tr_enc = self.headers.get("transfer-encoding", "").lower() + # Don't incur the penalty of creating a list and then discarding it + encodings = (enc.strip() for enc in tr_enc.split(",")) + if "chunked" in encodings: + self.chunked = True + + self._decoder: ContentDecoder | None = None + self.length_remaining: int | None + + def get_redirect_location(self) -> str | None | typing.Literal[False]: + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get("location") + return False + + @property + def data(self) -> bytes: + raise NotImplementedError() + + def json(self) -> typing.Any: + """ + Deserializes the body of the HTTP response as a Python object. + + The body of the HTTP response must be encoded using UTF-8, as per + `RFC 8529 Section 8.1 `_. + + To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to + your custom decoder instead. + + If the body of the HTTP response is not decodable to UTF-8, a + `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a + valid JSON document, a `json.JSONDecodeError` will be raised. + + Read more :ref:`here `. + + :returns: The body of the HTTP response as a Python object. + """ + data = self.data.decode("utf-8") + return _json.loads(data) + + @property + def url(self) -> str | None: + raise NotImplementedError() + + @url.setter + def url(self, url: str | None) -> None: + raise NotImplementedError() + + @property + def connection(self) -> BaseHTTPConnection | None: + raise NotImplementedError() + + @property + def retries(self) -> Retry | None: + return self._retries + + @retries.setter + def retries(self, retries: Retry | None) -> None: + # Override the request_url if retries has a redirect location. + if retries is not None and retries.history: + self.url = retries.history[-1].redirect_location + self._retries = retries + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + raise NotImplementedError() + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + raise NotImplementedError() + + def read_chunked( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> typing.Iterator[bytes]: + raise NotImplementedError() + + def release_conn(self) -> None: + raise NotImplementedError() + + def drain_conn(self) -> None: + raise NotImplementedError() + + def shutdown(self) -> None: + raise NotImplementedError() + + def close(self) -> None: + raise NotImplementedError() + + def _init_decoder(self) -> None: + """ + Set-up the _decoder attribute if necessary. + """ + # Note: content-encoding value should be case-insensitive, per RFC 7230 + # Section 3.2 + content_encoding = self.headers.get("content-encoding", "").lower() + if self._decoder is None: + if content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + elif "," in content_encoding: + encodings = [ + e.strip() + for e in content_encoding.split(",") + if e.strip() in self.CONTENT_DECODERS + ] + if encodings: + self._decoder = _get_decoder(content_encoding) + + def _decode( + self, + data: bytes, + decode_content: bool | None, + flush_decoder: bool, + max_length: int | None = None, + ) -> bytes: + """ + Decode the data passed in and potentially flush the decoder. + """ + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + if max_length is None or flush_decoder: + max_length = -1 + + try: + if self._decoder: + data = self._decoder.decompress(data, max_length=max_length) + self._has_decoded_content = True + except self.DECODER_ERROR_CLASSES as e: + content_encoding = self.headers.get("content-encoding", "").lower() + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, + e, + ) from e + if flush_decoder: + data += self._flush_decoder() + + return data + + def _flush_decoder(self) -> bytes: + """ + Flushes the decoder. Should only be called if the decoder is actually + being used. + """ + if self._decoder: + return self._decoder.decompress(b"") + self._decoder.flush() + return b"" + + # Compatibility methods for `io` module + def readinto(self, b: bytearray | memoryview[int]) -> int: + temp = self.read(len(b)) + if len(temp) == 0: + return 0 + else: + b[: len(temp)] = temp + return len(temp) + + # Methods used by dependent libraries + def getheaders(self) -> HTTPHeaderDict: + return self.headers + + def getheader(self, name: str, default: str | None = None) -> str | None: + return self.headers.get(name, default) + + # Compatibility method for http.cookiejar + def info(self) -> HTTPHeaderDict: + return self.headers + + def geturl(self) -> str | None: + return self.url + + +class HTTPResponse(BaseHTTPResponse): + """ + HTTP Response container. + + Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. This + class is also compatible with the Python standard library's :mod:`io` + module, and can hence be treated as a readable object in the context of that + framework. + + Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param original_response: + When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + + :param retries: + The retries contains the last :class:`~urllib3.util.retry.Retry` that + was used during the request. + + :param enforce_content_length: + Enforce content length checking. Body returned by server must match + value of Content-Length header, if present. Otherwise, raise error. + """ + + def __init__( + self, + body: _TYPE_BODY = "", + headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, + status: int = 0, + version: int = 0, + version_string: str = "HTTP/?", + reason: str | None = None, + preload_content: bool = True, + decode_content: bool = True, + original_response: _HttplibHTTPResponse | None = None, + pool: HTTPConnectionPool | None = None, + connection: HTTPConnection | None = None, + msg: _HttplibHTTPMessage | None = None, + retries: Retry | None = None, + enforce_content_length: bool = True, + request_method: str | None = None, + request_url: str | None = None, + auto_close: bool = True, + sock_shutdown: typing.Callable[[int], None] | None = None, + ) -> None: + super().__init__( + headers=headers, + status=status, + version=version, + version_string=version_string, + reason=reason, + decode_content=decode_content, + request_url=request_url, + retries=retries, + ) + + self.enforce_content_length = enforce_content_length + self.auto_close = auto_close + + self._body = None + self._uncached_read_occurred = False + self._fp: _HttplibHTTPResponse | None = None + self._original_response = original_response + self._fp_bytes_read = 0 + self.msg = msg + + if body and isinstance(body, (str, bytes)): + self._body = body + + self._pool = pool + self._connection = connection + + if hasattr(body, "read"): + self._fp = body # type: ignore[assignment] + self._sock_shutdown = sock_shutdown + + # Are we using the chunked-style of transfer encoding? + self.chunk_left: int | None = None + + # Determine length of response + self.length_remaining = self._init_length(request_method) + + # Used to return the correct amount of bytes for partial read()s + self._decoded_buffer = BytesQueueBuffer() + + # If requested, preload the body. + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def release_conn(self) -> None: + if not self._pool or not self._connection: + return None + + self._pool._put_conn(self._connection) + self._connection = None + + def drain_conn(self) -> None: + """ + Read and discard any remaining HTTP response data in the response connection. + + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: + self._raw_read() + except (HTTPError, OSError, BaseSSLError, HTTPException): + pass + if self._has_decoded_content: + # `_raw_read` skips decompression, so we should clean up the + # decoder to avoid keeping unnecessary data in memory. + self._decoded_buffer = BytesQueueBuffer() + self._decoder = None + + @property + def data(self) -> bytes: + # For backwards-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body # type: ignore[return-value] + + if self._fp: + return self.read(cache_content=True) + + return None # type: ignore[return-value] + + @property + def connection(self) -> HTTPConnection | None: + return self._connection + + def isclosed(self) -> bool: + return is_fp_closed(self._fp) + + def tell(self) -> int: + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:`HTTPResponse.read` + if bytes are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def _init_length(self, request_method: str | None) -> int | None: + """ + Set initial length value for Response content if available. + """ + length: int | None + content_length: str | None = self.headers.get("content-length") + + if content_length is not None: + if self.chunked: + # This Response will fail with an IncompleteRead if it can't be + # received as chunked. This method falls back to attempt reading + # the response before raising an exception. + log.warning( + "Received response with both Content-Length and " + "Transfer-Encoding set. This is expressly forbidden " + "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " + "attempting to process response as Transfer-Encoding: " + "chunked." + ) + return None + + try: + # RFC 7230 section 3.3.2 specifies multiple content lengths can + # be sent in a single Content-Length header + # (e.g. Content-Length: 42, 42). This line ensures the values + # are all valid ints and that as long as the `set` length is 1, + # all values are the same. Otherwise, the header is invalid. + lengths = {int(val) for val in content_length.split(",")} + if len(lengths) > 1: + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % content_length + ) + length = lengths.pop() + except ValueError: + length = None + else: + if length < 0: + length = None + + else: # if content_length is None + length = None + + # Convert status to int for comparison + # In some cases, httplib returns a status of "_UNKNOWN" + try: + status = int(self.status) + except ValueError: + status = 0 + + # Check for responses that shouldn't include a body + if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": + length = 0 + + return length + + @contextmanager + def _error_catcher(self) -> typing.Generator[None]: + """ + Catch low-level python exceptions, instead re-raising urllib3 + variants, so that low-level exceptions are not leaked in the + high-level api. + + On exit, release the connection back to the pool. + """ + clean_exit = False + + try: + try: + yield + + except SocketTimeout as e: + # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but + # there is yet no clean way to get at it from this context. + raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + + except BaseSSLError as e: + # SSL errors related to framing/MAC get wrapped and reraised here + raise SSLError(e) from e + + except IncompleteRead as e: + if ( + e.expected is not None + and e.partial is not None + and e.expected == -e.partial + ): + arg = "Response may not contain content." + else: + arg = f"Connection broken: {e!r}" + raise ProtocolError(arg, e) from e + + except (HTTPException, OSError) as e: + raise ProtocolError(f"Connection broken: {e!r}", e) from e + + # If no exception is thrown, we should avoid cleaning up + # unnecessarily. + clean_exit = True + finally: + # If we didn't terminate cleanly, we need to throw away our + # connection. + if not clean_exit: + # The response may not be closed but we're not going to use it + # anymore so close it now to ensure that the connection is + # released back to the pool. + if self._original_response: + self._original_response.close() + + # Closing the response may not actually be sufficient to close + # everything, so if we have a hold of the connection close that + # too. + if self._connection: + self._connection.close() + + # If we hold the original response but it's closed now, we should + # return the connection back to the pool. + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def _fp_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Read a response with the thought that reading the number of bytes + larger than can fit in a 32-bit int at a time via SSL in some + known cases leads to an overflow error that has to be prevented + if `amt` or `self.length_remaining` indicate that a problem may + happen. + + This happens to urllib3 injected with pyOpenSSL-backed SSL-support. + """ + assert self._fp + c_int_max = 2**31 - 1 + if ( + (amt and amt > c_int_max) + or ( + amt is None + and self.length_remaining + and self.length_remaining > c_int_max + ) + ) and util.IS_PYOPENSSL: + if read1: + return self._fp.read1(c_int_max) + buffer = io.BytesIO() + # Besides `max_chunk_amt` being a maximum chunk size, it + # affects memory overhead of reading a response by this + # method in CPython. + # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum + # chunk size that does not lead to an overflow error, but + # 256 MiB is a compromise. + max_chunk_amt = 2**28 + while amt is None or amt != 0: + if amt is not None: + chunk_amt = min(amt, max_chunk_amt) + amt -= chunk_amt + else: + chunk_amt = max_chunk_amt + data = self._fp.read(chunk_amt) + if not data: + break + buffer.write(data) + del data # to reduce peak memory usage by `max_chunk_amt`. + return buffer.getvalue() + elif read1: + return self._fp.read1(amt) if amt is not None else self._fp.read1() + else: + # StringIO doesn't like amt=None + return self._fp.read(amt) if amt is not None else self._fp.read() + + def _raw_read( + self, + amt: int | None = None, + *, + read1: bool = False, + ) -> bytes: + """ + Reads `amt` of bytes from the socket. + """ + if self._fp is None: + return None # type: ignore[return-value] + + fp_closed = getattr(self._fp, "closed", False) + + with self._error_catcher(): + data = self._fp_read(amt, read1=read1) if not fp_closed else b"" + if amt is not None and amt != 0 and not data: + # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do + # not properly close the connection in all cases. There is + # no harm in redundantly calling close. + self._fp.close() + if ( + self.enforce_content_length + and self.length_remaining is not None + and self.length_remaining != 0 + ): + # This is an edge case that httplib failed to cover due + # to concerns of backward compatibility. We're + # addressing it here to make sure IncompleteRead is + # raised during streaming, so all calls with incorrect + # Content-Length are caught. + raise IncompleteRead(self._fp_bytes_read, self.length_remaining) + elif read1 and ( + (amt != 0 and not data) or self.length_remaining == len(data) + ): + # All data has been read, but `self._fp.read1` in + # CPython 3.12 and older doesn't always close + # `http.client.HTTPResponse`, so we close it here. + # See https://github.com/python/cpython/issues/113199 + self._fp.close() + + if data: + self._fp_bytes_read += len(data) + if self.length_remaining is not None: + self.length_remaining -= len(data) + return data + + def read( + self, + amt: int | None = None, + decode_content: bool | None = None, + cache_content: bool = False, + ) -> bytes: + """ + Similar to :meth:`http.client.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + self._init_decoder() + if decode_content is None: + decode_content = self.decode_content + + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + elif amt is not None: + cache_content = False + + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and len(self._decoded_buffer) < amt + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) >= amt: + return self._decoded_buffer.get(amt) + + data = self._raw_read(amt) + if not cache_content: + self._uncached_read_occurred = True + + flush_decoder = amt is None or (amt != 0 and not data) + + if ( + not data + and len(self._decoded_buffer) == 0 + and not (self._decoder and self._decoder.has_unconsumed_tail) + ): + return data + + if amt is None: + data = self._decode(data, decode_content, flush_decoder) + # It's possible that there is buffered decoded data after a + # partial read. + if decode_content and len(self._decoded_buffer) > 0: + self._decoded_buffer.put(data) + data = self._decoded_buffer.get_all() + + if cache_content and not self._uncached_read_occurred: + self._body = data + else: + # do not waste memory on buffer when not decoding + if not decode_content: + if self._has_decoded_content: + raise RuntimeError( + "Calling read(decode_content=False) is not supported after " + "read(decode_content=True) was called." + ) + return data + + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + + while len(self._decoded_buffer) < amt and data: + # TODO make sure to initially read enough data to get past the headers + # For example, the GZ file header takes 10 bytes, we don't want to read + # it one byte at a time + data = self._raw_read(amt) + decoded_data = self._decode( + data, + decode_content, + flush_decoder, + max_length=amt - len(self._decoded_buffer), + ) + self._decoded_buffer.put(decoded_data) + data = self._decoded_buffer.get(amt) + + return data + + def read1( + self, + amt: int | None = None, + decode_content: bool | None = None, + ) -> bytes: + """ + Similar to ``http.client.HTTPResponse.read1`` and documented + in :meth:`io.BufferedReader.read1`, but with an additional parameter: + ``decode_content``. + + :param amt: + How much of the content to read. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if decode_content is None: + decode_content = self.decode_content + if amt and amt < 0: + # Negative numbers and `None` should be treated the same. + amt = None + # try and respond without going to the network + if self._has_decoded_content: + if not decode_content: + raise RuntimeError( + "Calling read1(decode_content=False) is not supported after " + "read1(decode_content=True) was called." + ) + if ( + self._decoder + and self._decoder.has_unconsumed_tail + and (amt is None or len(self._decoded_buffer) < amt) + ): + decoded_data = self._decode( + b"", + decode_content, + flush_decoder=False, + max_length=( + amt - len(self._decoded_buffer) if amt is not None else None + ), + ) + self._decoded_buffer.put(decoded_data) + if len(self._decoded_buffer) > 0: + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + if amt == 0: + return b"" + + # FIXME, this method's type doesn't say returning None is possible + data = self._raw_read(amt, read1=True) + self._uncached_read_occurred = True + if not decode_content or data is None: + return data + + self._init_decoder() + while True: + flush_decoder = not data + decoded_data = self._decode( + data, decode_content, flush_decoder, max_length=amt + ) + self._decoded_buffer.put(decoded_data) + if decoded_data or flush_decoder: + break + data = self._raw_read(8192, read1=True) + + if amt is None: + return self._decoded_buffer.get_all() + return self._decoded_buffer.get(amt) + + def stream( + self, amt: int | None = 2**16, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + if amt == 0: + return + + if self.chunked and self.supports_chunked_reads(): + yield from self.read_chunked(amt, decode_content=decode_content) + else: + while ( + not is_fp_closed(self._fp) + or len(self._decoded_buffer) > 0 + or (self._decoder and self._decoder.has_unconsumed_tail) + ): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + # Overrides from io.IOBase + def readable(self) -> bool: + return True + + def shutdown(self) -> None: + if not self._sock_shutdown: + raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") + if self._connection is None: + raise RuntimeError( + "Cannot shutdown as connection has already been released to the pool" + ) + self._sock_shutdown(socket.SHUT_RD) + + def close(self) -> None: + self._sock_shutdown = None + + if not self.closed and self._fp: + self._fp.close() + + if self._connection: + self._connection.close() + + if not self.auto_close: + io.IOBase.close(self) + + @property + def closed(self) -> bool: + if not self.auto_close: + return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] + elif self._fp is None: + return True + elif hasattr(self._fp, "isclosed"): + return self._fp.isclosed() + elif hasattr(self._fp, "closed"): + return self._fp.closed + else: + return True + + def fileno(self) -> int: + if self._fp is None: + raise OSError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise OSError( + "The file-like object this HTTPResponse is wrapped " + "around has no file descriptor" + ) + + def flush(self) -> None: + if ( + self._fp is not None + and hasattr(self._fp, "flush") + and not getattr(self._fp, "closed", False) + ): + return self._fp.flush() + + def supports_chunked_reads(self) -> bool: + """ + Checks if the underlying file-like object looks like a + :class:`http.client.HTTPResponse` object. We do this by testing for + the fp attribute. If it is present we assume it returns raw chunks as + processed by read_chunked(). + """ + return hasattr(self._fp, "fp") + + def _update_chunk_length(self) -> None: + # First, we'll figure out length of a chunk and then + # we'll try to read it from socket. + if self.chunk_left is not None: + return None + line = self._fp.fp.readline() # type: ignore[union-attr] + line = line.split(b";", 1)[0] + try: + self.chunk_left = int(line, 16) + except ValueError: + self.close() + if line: + # Invalid chunked protocol response, abort. + raise InvalidChunkLength(self, line) from None + else: + # Truncated at start of next chunk + raise ProtocolError("Response ended prematurely") from None + + def _handle_chunk(self, amt: int | None) -> bytes: + returned_chunk = None + if amt is None: + chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + returned_chunk = chunk + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + elif self.chunk_left is not None and amt < self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self.chunk_left = self.chunk_left - amt + returned_chunk = value + elif amt == self.chunk_left: + value = self._fp._safe_read(amt) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + returned_chunk = value + else: # amt > self.chunk_left + returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] + self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. + self.chunk_left = None + return returned_chunk # type: ignore[no-any-return] + + def read_chunked( + self, amt: int | None = None, decode_content: bool | None = None + ) -> typing.Generator[bytes]: + """ + Similar to :meth:`HTTPResponse.read`, but with an additional + parameter: ``decode_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + self._init_decoder() + # FIXME: Rewrite this method and make it a class with a better structured logic. + if not self.chunked: + raise ResponseNotChunked( + "Response is not chunked. " + "Header 'transfer-encoding: chunked' is missing." + ) + if not self.supports_chunked_reads(): + raise BodyNotHttplibCompatible( + "Body should be http.client.HTTPResponse like. " + "It should have have an fp attribute which returns raw chunks." + ) + + with self._error_catcher(): + # Don't bother reading the body of a HEAD request. + if self._original_response and is_response_to_head(self._original_response): + self._original_response.close() + return None + + # If a response is already read and closed + # then return immediately. + if self._fp.fp is None: # type: ignore[union-attr] + return None + + if amt == 0: + return + elif amt and amt < 0: + # Negative numbers and `None` should be treated the same, + # but httplib handles only `None` correctly. + amt = None + + while True: + # First, check if any data is left in the decoder's buffer. + if self._decoder and self._decoder.has_unconsumed_tail: + chunk = b"" + else: + self._update_chunk_length() + self._uncached_read_occurred = True + if self.chunk_left == 0: + break + chunk = self._handle_chunk(amt) + decoded = self._decode( + chunk, + decode_content=decode_content, + flush_decoder=False, + max_length=amt, + ) + if decoded: + yield decoded + + if decode_content: + # On CPython and PyPy, we should never need to flush the + # decoder. However, on Jython we *might* need to, so + # lets defensively do it anyway. + decoded = self._flush_decoder() + if decoded: # Platform-specific: Jython. + yield decoded + + # Chunk content ends with \r\n: discard it. + while self._fp is not None: + line = self._fp.fp.readline() + if not line: + # Some sites may not end with '\r\n'. + break + if line == b"\r\n": + break + + # We read everything; close the "file". + if self._original_response: + self._original_response.close() + + @property + def url(self) -> str | None: + """ + Returns the URL that was the source of this response. + If the request that generated this response redirected, this method + will return the final redirect location. + """ + return self._request_url + + @url.setter + def url(self, url: str | None) -> None: + self._request_url = url + + def __iter__(self) -> typing.Iterator[bytes]: + buffer: list[bytes] = [] + for chunk in self.stream(decode_content=True): + if b"\n" in chunk: + chunks = chunk.split(b"\n") + yield b"".join(buffer) + chunks[0] + b"\n" + for x in chunks[1:-1]: + yield x + b"\n" + if chunks[-1]: + buffer = [chunks[-1]] + else: + buffer = [] + else: + buffer.append(chunk) + if buffer: + yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py new file mode 100644 index 0000000000..534126033c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py @@ -0,0 +1,42 @@ +# For backwards compatibility, provide imports that used to be here. +from __future__ import annotations + +from .connection import is_connection_dropped +from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers +from .response import is_fp_closed +from .retry import Retry +from .ssl_ import ( + ALPN_PROTOCOLS, + IS_PYOPENSSL, + SSLContext, + assert_fingerprint, + create_urllib3_context, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import Timeout +from .url import Url, parse_url +from .wait import wait_for_read, wait_for_write + +__all__ = ( + "IS_PYOPENSSL", + "SSLContext", + "ALPN_PROTOCOLS", + "Retry", + "Timeout", + "Url", + "assert_fingerprint", + "create_urllib3_context", + "is_connection_dropped", + "is_fp_closed", + "parse_url", + "make_headers", + "resolve_cert_reqs", + "resolve_ssl_version", + "ssl_wrap_socket", + "wait_for_read", + "wait_for_write", + "SKIP_HEADER", + "SKIPPABLE_HEADERS", +) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py new file mode 100644 index 0000000000..f92519ee91 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import socket +import typing + +from ..exceptions import LocationParseError +from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT + +_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] + +if typing.TYPE_CHECKING: + from .._base_connection import BaseHTTPConnection + + +def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + :param conn: :class:`urllib3.connection.HTTPConnection` object. + """ + return not conn.is_connected + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +# One additional modification is that we avoid binding to IPv6 servers +# discovered in DNS if the system doesn't have IPv6 functionality. +def create_connection( + address: tuple[str, int], + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, +) -> socket.socket: + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`socket.getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith("["): + host = host.strip("[]") + err = None + + # Using the value from allowed_gai_family() in the context of getaddrinfo lets + # us select whether to work with IPv4 DNS records, IPv6 records, or both. + # The original create_connection function always returns all records. + family = allowed_gai_family() + + try: + host.encode("idna") + except UnicodeError: + raise LocationParseError(f"'{host}', label empty or too long") from None + + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + _set_socket_options(sock, socket_options) + + if timeout is not _DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + # Break explicitly a reference cycle + err = None + return sock + + except OSError as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + try: + raise err + finally: + # Break explicitly a reference cycle + err = None + else: + raise OSError("getaddrinfo returns an empty list") + + +def _set_socket_options( + sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None +) -> None: + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) + + +def allowed_gai_family() -> socket.AddressFamily: + """This function is designed to work in the context of + getaddrinfo, where family=socket.AF_UNSPEC is the default and + will perform a DNS search for both IPv6 and IPv4 records.""" + + family = socket.AF_INET + if HAS_IPV6: + family = socket.AF_UNSPEC + return family + + +def _has_ipv6(host: str) -> bool: + """Returns True if the system can bind an IPv6 address.""" + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/urllib3/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = True + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py new file mode 100644 index 0000000000..908fc6621d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .url import Url + +if typing.TYPE_CHECKING: + from ..connection import ProxyConfig + + +def connection_requires_http_tunnel( + proxy_url: Url | None = None, + proxy_config: ProxyConfig | None = None, + destination_scheme: str | None = None, +) -> bool: + """ + Returns True if the connection requires an HTTP CONNECT through the proxy. + + :param URL proxy_url: + URL of the proxy. + :param ProxyConfig proxy_config: + Proxy configuration from poolmanager.py + :param str destination_scheme: + The scheme of the destination. (i.e https, http, etc) + """ + # If we're not using a proxy, no way to use a tunnel. + if proxy_url is None: + return False + + # HTTP destinations never require tunneling, we always forward. + if destination_scheme == "http": + return False + + # Support for forwarding with HTTPS proxies and HTTPS destinations. + if ( + proxy_url.scheme == "https" + and proxy_config + and proxy_config.use_forwarding_for_https + ): + return False + + # Otherwise always use a tunnel. + return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py new file mode 100644 index 0000000000..6c2372ba7e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +import io +import sys +import typing +from base64 import b64encode +from enum import Enum + +from ..exceptions import UnrewindableBodyError +from .util import to_bytes + +if typing.TYPE_CHECKING: + from typing import Final + +# Pass as a value within ``headers`` to skip +# emitting some HTTP headers that are added automatically. +# The only headers that are supported are ``Accept-Encoding``, +# ``Host``, and ``User-Agent``. +SKIP_HEADER = "@@@SKIP_HEADER@@@" +SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) + +ACCEPT_ENCODING = "gzip,deflate" +try: + try: + import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 + except ImportError: + import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",br" + +try: + if sys.version_info >= (3, 14): + from compression import zstd as _unused_module_zstd # noqa: F401 + else: + from backports import zstd as _unused_module_zstd # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",zstd" + + +class _TYPE_FAILEDTELL(Enum): + token = 0 + + +_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token + +_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] + +# When sending a request with these methods we aren't expecting +# a body so don't need to set an explicit 'Content-Length: 0' +# The reason we do this in the negative instead of tracking methods +# which 'should' have a body is because unknown methods should be +# treated as if they were 'POST' which *does* expect a body. +_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} + + +def make_headers( + keep_alive: bool | None = None, + accept_encoding: bool | list[str] | str | None = None, + user_agent: str | None = None, + basic_auth: str | None = None, + proxy_basic_auth: str | None = None, + disable_cache: bool | None = None, +) -> dict[str, str]: + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. If the dependencies for + Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or + Zstandard (the ``backports.zstd`` package for Python before 3.14) + algorithms are installed, then their encodings are + included in the string ('br' and 'zstd', respectively). + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example: + + .. code-block:: python + + import urllib3 + + print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) + # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + print(urllib3.util.make_headers(accept_encoding=True)) + # {'accept-encoding': 'gzip,deflate'} + """ + headers: dict[str, str] = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ",".join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers["accept-encoding"] = accept_encoding + + if user_agent: + headers["user-agent"] = user_agent + + if keep_alive: + headers["connection"] = "keep-alive" + + if basic_auth: + headers["authorization"] = ( + f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" + ) + + if proxy_basic_auth: + headers["proxy-authorization"] = ( + f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" + ) + + if disable_cache: + headers["cache-control"] = "no-cache" + + return headers + + +def set_file_position( + body: typing.Any, pos: _TYPE_BODY_POSITION | None +) -> _TYPE_BODY_POSITION | None: + """ + If a position is provided, move file to that point. + Otherwise, we'll attempt to record a position for future use. + """ + if pos is not None: + rewind_body(body, pos) + elif getattr(body, "tell", None) is not None: + try: + pos = body.tell() + except OSError: + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body. + pos = _FAILEDTELL + + return pos + + +def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: + """ + Attempt to rewind body to a certain position. + Primarily used for request redirects and retries. + + :param body: + File-like object that supports seek. + + :param int pos: + Position to seek to in file. + """ + body_seek = getattr(body, "seek", None) + if body_seek is not None and isinstance(body_pos, int): + try: + body_seek(body_pos) + except OSError as e: + raise UnrewindableBodyError( + "An error occurred when rewinding request body for redirect/retry." + ) from e + elif body_pos is _FAILEDTELL: + raise UnrewindableBodyError( + "Unable to record file position for rewinding " + "request body during a redirect/retry." + ) + else: + raise ValueError( + f"body_pos must be of type integer, instead it was {type(body_pos)}." + ) + + +class ChunksAndContentLength(typing.NamedTuple): + chunks: typing.Iterable[bytes] | None + content_length: int | None + + +def body_to_chunks( + body: typing.Any | None, method: str, blocksize: int +) -> ChunksAndContentLength: + """Takes the HTTP request method, body, and blocksize and + transforms them into an iterable of chunks to pass to + socket.sendall() and an optional 'Content-Length' header. + + A 'Content-Length' of 'None' indicates the length of the body + can't be determined so should use 'Transfer-Encoding: chunked' + for framing instead. + """ + + chunks: typing.Iterable[bytes] | None + content_length: int | None + + # No body, we need to make a recommendation on 'Content-Length' + # based on whether that request method is expected to have + # a body or not. + if body is None: + chunks = None + if method.upper() not in _METHODS_NOT_EXPECTING_BODY: + content_length = 0 + else: + content_length = None + + # Bytes or strings become bytes + elif isinstance(body, (str, bytes)): + chunks = (to_bytes(body),) + content_length = len(chunks[0]) + + # File-like object, TODO: use seek() and tell() for length? + elif hasattr(body, "read"): + + def chunk_readable() -> typing.Iterable[bytes]: + encode = isinstance(body, io.TextIOBase) + while True: + datablock = body.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("utf-8") + yield datablock + + chunks = chunk_readable() + content_length = None + + # Otherwise we need to start checking via duck-typing. + else: + try: + # Check if the body implements the buffer API. + mv = memoryview(body) + except TypeError: + try: + # Check if the body is an iterable + chunks = iter(body) + content_length = None + except TypeError: + raise TypeError( + f"'body' must be a bytes-like object, file-like " + f"object, or iterable. Instead was {body!r}" + ) from None + else: + # Since it implements the buffer API can be passed directly to socket.sendall() + chunks = (body,) + content_length = mv.nbytes + + return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py new file mode 100644 index 0000000000..0f4578696f --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import http.client as httplib +from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj: object) -> bool: + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check `isclosed()` first, in case Python3 doesn't set `closed`. + # GH Issue #928 + return obj.isclosed() # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check via the official file-like-object way. + return obj.closed # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None # type: ignore[attr-defined] + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers: httplib.HTTPMessage) -> None: + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param http.client.HTTPMessage headers: Headers to verify. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError(f"expected httplib.Message, got {type(headers)}.") + + unparsed_data = None + + # get_payload is actually email.message.Message.get_payload; + # we're only interested in the result if it's not a multipart message + if not headers.is_multipart(): + payload = headers.get_payload() + + if isinstance(payload, (bytes, str)): + unparsed_data = payload + + # httplib is assuming a response body is available + # when parsing headers even when httplib only sends + # header data to parse_headers() This results in + # defects on multipart responses in particular. + # See: https://github.com/urllib3/urllib3/issues/800 + + # So we ignore the following defects: + # - StartBoundaryNotFoundDefect: + # The claimed start boundary was never found. + # - MultipartInvariantViolationDefect: + # A message claimed to be a multipart but no subparts were found. + defects = [ + defect + for defect in headers.defects + if not isinstance( + defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) + ) + ] + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response: httplib.HTTPResponse) -> bool: + """ + Checks whether the request of a response has been a HEAD-request. + + :param http.client.HTTPResponse response: + Response to check if the originating request + used 'HEAD' as a method. + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method_str = response._method # type: str # type: ignore[attr-defined] + return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py new file mode 100644 index 0000000000..7649898e1d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py @@ -0,0 +1,557 @@ +from __future__ import annotations + +import email +import logging +import random +import re +import time +import typing +from itertools import takewhile +from types import TracebackType + +from ..exceptions import ( + ConnectTimeoutError, + InvalidHeader, + MaxRetryError, + ProtocolError, + ProxyError, + ReadTimeoutError, + ResponseError, +) +from .util import reraise + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from ..connectionpool import ConnectionPool + from ..response import BaseHTTPResponse + +log = logging.getLogger(__name__) + + +# Data structure for representing the metadata of requests that result in a retry. +class RequestHistory(typing.NamedTuple): + method: str | None + url: str | None + error: Exception | None + status: int | None + redirect_location: str | None + + +class Retry: + """Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool: + + .. code-block:: python + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request("GET", "https://example.com/") + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=Retry(10)) + + Retries can be disabled by passing ``False``: + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int status: + How many times to retry on bad status codes. + + These are retries made on responses, where status code matches + ``status_forcelist``. + + Set to ``0`` to fail on the first retry of this type. + + :param int other: + How many times to retry on other errors. + + Other errors are errors that are not connect, read, redirect or status errors. + These errors might be raised after the request was sent to the server, so the + request might have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + If ``total`` is not set, it's a good idea to set this to 0 to account + for unexpected edge cases and avoid infinite retry loops. + + :param Collection allowed_methods: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + idempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. + + Set to a ``None`` value to retry on any verb. + + :param Collection status_forcelist: + A set of integer HTTP status codes that we should force a retry on. + A retry is initiated if the request method is in ``allowed_methods`` + and the response status code is in ``status_forcelist``. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts after the second try + (most errors are resolved immediately by a second try without a + delay). urllib3 will sleep for:: + + {backoff factor} * (2 ** ({number of previous retries})) + + seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: + + random.uniform(0, {backoff jitter}) + + seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will + sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever + be longer than `backoff_max`. + + By default, backoff is disabled (factor set to 0). + + :param float backoff_max: + The maximum backoff time (in seconds) between retry attempts. + This value caps the computed backoff from `backoff_factor`. + + :param float backoff_jitter: + Random jitter amount (in seconds) added to the computed backoff. + Jitter is sampled uniformly from `0` to `backoff_jitter`. + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have + been exhausted. + + :param tuple history: The history of the request encountered during + each call to :meth:`~Retry.increment`. The list is in the order + the requests occurred. Each list item is of class :class:`RequestHistory`. + + :param bool respect_retry_after_header: + Whether to respect Retry-After header on status codes defined as + :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. + + :param Collection remove_headers_on_redirect: + Sequence of headers to remove from the request when a response + indicating a redirect is returned before firing off the redirected + request. + + :param int retry_after_max: Number of seconds to allow as the maximum for + Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. + Any Retry-After headers larger than this value will be limited to this + value. + """ + + #: Default methods to be used for ``allowed_methods`` + DEFAULT_ALLOWED_METHODS = frozenset( + ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) + + #: Default status codes to be used for ``status_forcelist`` + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Default headers to be used for ``remove_headers_on_redirect`` + DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( + ["Cookie", "Authorization", "Proxy-Authorization"] + ) + + #: Default maximum backoff time. + DEFAULT_BACKOFF_MAX = 120 + + # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. + #: Default maximum allowed value for Retry-After headers in seconds + DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 + + # Backward compatibility; assigned outside of the class. + DEFAULT: typing.ClassVar[Retry] + + def __init__( + self, + total: bool | int | None = 10, + connect: int | None = None, + read: int | None = None, + redirect: bool | int | None = None, + status: int | None = None, + other: int | None = None, + allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, + status_forcelist: typing.Collection[int] | None = None, + backoff_factor: float = 0, + backoff_max: float = DEFAULT_BACKOFF_MAX, + raise_on_redirect: bool = True, + raise_on_status: bool = True, + history: tuple[RequestHistory, ...] | None = None, + respect_retry_after_header: bool = True, + remove_headers_on_redirect: typing.Collection[ + str + ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, + backoff_jitter: float = 0.0, + retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, + ) -> None: + self.total = total + self.connect = connect + self.read = read + self.status = status + self.other = other + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.allowed_methods = allowed_methods + self.backoff_factor = backoff_factor + self.backoff_max = backoff_max + self.retry_after_max = retry_after_max + self.raise_on_redirect = raise_on_redirect + self.raise_on_status = raise_on_status + self.history = history or () + self.respect_retry_after_header = respect_retry_after_header + self.remove_headers_on_redirect = frozenset( + h.lower() for h in remove_headers_on_redirect + ) + self.backoff_jitter = backoff_jitter + + def new(self, **kw: typing.Any) -> Self: + params = dict( + total=self.total, + connect=self.connect, + read=self.read, + redirect=self.redirect, + status=self.status, + other=self.other, + allowed_methods=self.allowed_methods, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + backoff_max=self.backoff_max, + retry_after_max=self.retry_after_max, + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, + remove_headers_on_redirect=self.remove_headers_on_redirect, + respect_retry_after_header=self.respect_retry_after_header, + backoff_jitter=self.backoff_jitter, + ) + + params.update(kw) + return type(self)(**params) # type: ignore[arg-type] + + @classmethod + def from_int( + cls, + retries: Retry | bool | int | None, + redirect: bool | int | None = True, + default: Retry | bool | int | None = None, + ) -> Retry: + """Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r", retries, new_retries) + return new_retries + + def get_backoff_time(self) -> float: + """Formula for computing the current backoff + + :rtype: float + """ + # We want to consider only the last consecutive errors sequence (Ignore redirects). + consecutive_errors_len = len( + list( + takewhile(lambda x: x.redirect_location is None, reversed(self.history)) + ) + ) + if consecutive_errors_len <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) + if self.backoff_jitter != 0.0: + backoff_value += random.random() * self.backoff_jitter + return float(max(0, min(self.backoff_max, backoff_value))) + + def parse_retry_after(self, retry_after: str) -> float: + seconds: float + # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = int(retry_after) + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + seconds = max(seconds, 0) + + # Check the seconds do not exceed the specified maximum + if seconds > self.retry_after_max: + seconds = self.retry_after_max + + return seconds + + def get_retry_after(self, response: BaseHTTPResponse) -> float | None: + """Get the value of Retry-After in seconds.""" + + retry_after = response.headers.get("Retry-After") + + if retry_after is None: + return None + + return self.parse_retry_after(retry_after) + + def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: + retry_after = self.get_retry_after(response) + if retry_after: + time.sleep(retry_after) + return True + + return False + + def _sleep_backoff(self) -> None: + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def sleep(self, response: BaseHTTPResponse | None = None) -> None: + """Sleep between retry attempts. + + This method will respect a server's ``Retry-After`` response header + and sleep the duration of the time requested. If that is not present, it + will use an exponential backoff. By default, the backoff factor is 0 and + this method will return immediately. + """ + + if self.respect_retry_after_header and response: + slept = self.sleep_for_retry(response) + if slept: + return + + self._sleep_backoff() + + def _is_connection_error(self, err: Exception) -> bool: + """Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + if isinstance(err, ProxyError): + err = err.original_error + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err: Exception) -> bool: + """Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def _is_method_retryable(self, method: str) -> bool: + """Checks if a given HTTP method should be retried upon, depending if + it is included in the allowed_methods + """ + if self.allowed_methods and method.upper() not in self.allowed_methods: + return False + return True + + def is_retry( + self, method: str, status_code: int, has_retry_after: bool = False + ) -> bool: + """Is this method/status code retryable? (Based on allowlists and control + variables such as the number of total retries to allow, whether to + respect the Retry-After header, whether this header is present, and + whether the returned status code is on the list of status codes to + be retried upon on the presence of the aforementioned header) + """ + if not self._is_method_retryable(method): + return False + + if self.status_forcelist and status_code in self.status_forcelist: + return True + + return bool( + self.total + and self.respect_retry_after_header + and has_retry_after + and (status_code in self.RETRY_AFTER_STATUS_CODES) + ) + + def is_exhausted(self) -> bool: + """Are we out of retries?""" + retry_counts = [ + x + for x in ( + self.total, + self.connect, + self.read, + self.redirect, + self.status, + self.other, + ) + if x + ] + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment( + self, + method: str | None = None, + url: str | None = None, + response: BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, + ) -> Self: + """Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.BaseHTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + connect = self.connect + read = self.read + redirect = self.redirect + status_count = self.status + other = self.other + cause = "unknown" + status = None + redirect_location = None + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False or method is None or not self._is_method_retryable(method): + raise reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + + elif error: + # Other retry? + if other is not None: + other -= 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = "too many redirects" + response_redirect_location = response.get_redirect_location() + if response_redirect_location: + redirect_location = response_redirect_location + status = response.status + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and the given method is in the allowed_methods + cause = ResponseError.GENERIC_ERROR + if response and response.status: + if status_count is not None: + status_count -= 1 + cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) + status = response.status + + history = self.history + ( + RequestHistory(method, url, error, status, redirect_location), + ) + + new_retry = self.new( + total=total, + connect=connect, + read=read, + redirect=redirect, + status=status_count, + other=other, + history=history, + ) + + if new_retry.is_exhausted(): + reason = error or ResponseError(cause) + raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] + + log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) + + return new_retry + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(total={self.total}, connect={self.connect}, " + f"read={self.read}, redirect={self.redirect}, status={self.status})" + ) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py new file mode 100644 index 0000000000..e66549a76c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +import hashlib +import hmac +import os +import socket +import sys +import typing +import warnings +from binascii import unhexlify + +from ..exceptions import ProxySchemeUnsupported, SSLError +from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE + +SSLContext = None +SSLTransport = None +HAS_NEVER_CHECK_COMMON_NAME = False +IS_PYOPENSSL = False +ALPN_PROTOCOLS = ["http/1.1"] + +_TYPE_VERSION_INFO = tuple[int, int, int, str, int] + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = { + length: getattr(hashlib, algorithm, None) + for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) +} + + +def _is_has_never_check_common_name_reliable( + openssl_version: str, +) -> bool: + # As of May 2023, all released versions of LibreSSL fail to reject certificates with + # only common names, see https://github.com/urllib3/urllib3/pull/3024 + is_openssl = openssl_version.startswith("OpenSSL ") + + return is_openssl + + +if typing.TYPE_CHECKING: + from ssl import VerifyMode + from typing import TypedDict + + from .ssltransport import SSLTransport as SSLTransportType + + class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): + subjectAltName: tuple[tuple[str, str], ...] + subject: tuple[tuple[tuple[str, str], ...], ...] + serialNumber: str + + +# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' +_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} + +try: # Do we have ssl at all? + import ssl + from ssl import ( # type: ignore[assignment] + CERT_REQUIRED, + HAS_NEVER_CHECK_COMMON_NAME, + OP_NO_COMPRESSION, + OP_NO_TICKET, + OPENSSL_VERSION, + PROTOCOL_TLS, + PROTOCOL_TLS_CLIENT, + VERIFY_X509_PARTIAL_CHAIN, + VERIFY_X509_STRICT, + OP_NO_SSLv2, + OP_NO_SSLv3, + SSLContext, + TLSVersion, + ) + + PROTOCOL_SSLv23 = PROTOCOL_TLS + + # Setting SSLContext.hostname_checks_common_name = False didn't work with + # LibreSSL, check details in the used function. + if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( + OPENSSL_VERSION, + ): # Defensive: + HAS_NEVER_CHECK_COMMON_NAME = False + + # Need to be careful here in case old TLS versions get + # removed in future 'ssl' module implementations. + for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): + try: + _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( + TLSVersion, attr + ) + except AttributeError: # Defensive: + continue + + from .ssltransport import SSLTransport # type: ignore[assignment] +except ImportError: + OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] + OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] + OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] + OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] + PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] + PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] + VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] + VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] + + +_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] + + +def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + if cert is None: + raise SSLError("No certificate for the peer.") + + fingerprint = fingerprint.replace(":", "").lower() + digest_length = len(fingerprint) + if digest_length not in HASHFUNC_MAP: + raise SSLError(f"Fingerprint of invalid length: {fingerprint}") + hashfunc = HASHFUNC_MAP.get(digest_length) + if hashfunc is None: + raise SSLError( + f"Hash function implementation unavailable for fingerprint length: {digest_length}" + ) + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not hmac.compare_digest(cert_digest, fingerprint_bytes): + raise SSLError( + f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' + ) + + +def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_REQUIRED`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbreviation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_REQUIRED + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "CERT_" + candidate) + return res # type: ignore[no-any-return] + + return candidate # type: ignore[return-value] + + +def resolve_ssl_version(candidate: None | int | str) -> int: + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_TLS + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "PROTOCOL_" + candidate) + return typing.cast(int, res) + + return candidate + + +def create_urllib3_context( + ssl_version: int | None = None, + cert_reqs: int | None = None, + options: int | None = None, + ciphers: str | None = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, + verify_flags: int | None = None, +) -> ssl.SSLContext: + """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + + This parameter is deprecated instead use 'ssl_minimum_version'. + :param ssl_minimum_version: + The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + :param ssl_maximum_version: + The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the + default value. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. + :param ciphers: + Which cipher suites to allow the server to select. Defaults to either system configured + ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. + :param verify_flags: + The flags for certificate verification operations. These default to + ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + if SSLContext is None: + raise TypeError("Can't create an SSLContext object without an ssl module") + + # This means 'ssl_version' was specified as an exact value. + if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): + # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' + # to avoid conflicts. + if ssl_minimum_version is not None or ssl_maximum_version is not None: + raise ValueError( + "Can't specify both 'ssl_version' and either " + "'ssl_minimum_version' or 'ssl_maximum_version'" + ) + + # 'ssl_version' is deprecated and will be removed in the future. + else: + # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. + ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MINIMUM_SUPPORTED + ) + ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MAXIMUM_SUPPORTED + ) + + # This warning message is pushing users to use 'ssl_minimum_version' + # instead of both min/max. Best practice is to only set the minimum version and + # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' + warnings.warn( + "'ssl_version' option is deprecated and will be " + "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", + category=FutureWarning, + stacklevel=2, + ) + + context = SSLContext(PROTOCOL_TLS_CLIENT) + if ssl_minimum_version is not None: + context.minimum_version = ssl_minimum_version + else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here + context.minimum_version = TLSVersion.TLSv1_2 + + if ssl_maximum_version is not None: + context.maximum_version = ssl_maximum_version + + # Unless we're given ciphers defer to either system ciphers in + # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. + if ciphers: + context.set_ciphers(ciphers) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + # TLSv1.2 only. Unless set explicitly, do not request tickets. + # This may save some bandwidth on wire, and although the ticket is encrypted, + # there is a risk associated with it being on wire, + # if the server is not rotating its ticketing keys properly. + options |= OP_NO_TICKET + + context.options |= options + + if verify_flags is None: + verify_flags = 0 + # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN + # and VERIFY_X509_STRICT so we do the same + if sys.version_info >= (3, 13): + verify_flags |= VERIFY_X509_PARTIAL_CHAIN + verify_flags |= VERIFY_X509_STRICT + + context.verify_flags |= verify_flags + + # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is + # necessary for conditional client cert authentication with TLS 1.3. + # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using + # an SSLContext created by pyOpenSSL. + if getattr(context, "post_handshake_auth", None) is not None: + context.post_handshake_auth = True + + # The order of the below lines setting verify_mode and check_hostname + # matter due to safe-guards SSLContext has to prevent an SSLContext with + # check_hostname=True, verify_mode=NONE/OPTIONAL. + # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own + # 'ssl.match_hostname()' implementation. + if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: + context.verify_mode = cert_reqs + context.check_hostname = True + else: + context.check_hostname = False + context.verify_mode = cert_reqs + + context.hostname_checks_common_name = False + + if "SSLKEYLOGFILE" in os.environ: + sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) + else: + sslkeylogfile = None + if sslkeylogfile: + context.keylog_filename = sslkeylogfile + + return context + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: typing.Literal[False] = ..., +) -> ssl.SSLSocket: ... + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: bool = ..., +) -> ssl.SSLSocket | SSLTransportType: ... + + +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = None, + certfile: str | None = None, + cert_reqs: int | None = None, + ca_certs: str | None = None, + server_hostname: str | None = None, + ssl_version: int | None = None, + ciphers: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_cert_dir: str | None = None, + key_password: str | None = None, + ca_cert_data: None | str | bytes = None, + tls_in_tls: bool = False, +) -> ssl.SSLSocket | SSLTransportType: + """ + All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and + ca_cert_dir have the same meaning as they do when using + :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, + :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + :param key_password: + Optional password if the keyfile is encrypted. + :param ca_cert_data: + Optional string containing CA certificates in PEM format suitable for + passing as the cadata parameter to SSLContext.load_verify_locations() + :param tls_in_tls: + Use SSLTransport to wrap the existing socket. + """ + context = ssl_context + if context is None: + # Note: This branch of code and all the variables in it are only used in tests. + # We should consider deprecating and removing this code. + context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) + + if ca_certs or ca_cert_dir or ca_cert_data: + try: + context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) + except OSError as e: + raise SSLError(e) from e + + elif ssl_context is None and hasattr(context, "load_default_certs"): + # try to load OS default certs; works well on Windows. + context.load_default_certs() + + # Attempt to detect if we get the goofy behavior of the + # keyfile being encrypted and OpenSSL asking for the + # passphrase via the terminal and instead error out. + if keyfile and key_password is None and _is_key_file_encrypted(keyfile): + raise SSLError("Client private key is encrypted, password is required") + + if certfile: + if key_password is None: + context.load_cert_chain(certfile, keyfile) + else: + context.load_cert_chain(certfile, keyfile, key_password) + + context.set_alpn_protocols(ALPN_PROTOCOLS) + + ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) + return ssl_sock + + +def is_ipaddress(hostname: str | bytes) -> bool: + """Detects whether the hostname given is an IPv4 or IPv6 address. + Also detects IPv6 addresses with Zone IDs. + + :param str hostname: Hostname to examine. + :return: True if the hostname is an IP address, False otherwise. + """ + if isinstance(hostname, bytes): + # IDN A-label bytes are ASCII compatible. + hostname = hostname.decode("ascii") + return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) + + +def _is_key_file_encrypted(key_file: str) -> bool: + """Detects if a key file is encrypted or not.""" + with open(key_file) as f: + for line in f: + # Look for Proc-Type: 4,ENCRYPTED + if "ENCRYPTED" in line: + return True + + return False + + +def _ssl_wrap_socket_impl( + sock: socket.socket, + ssl_context: ssl.SSLContext, + tls_in_tls: bool, + server_hostname: str | None = None, +) -> ssl.SSLSocket | SSLTransportType: + if tls_in_tls: + if not SSLTransport: + # Import error, ssl is not available. + raise ProxySchemeUnsupported( + "TLS in TLS requires support for the 'ssl' module" + ) + + SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) + return SSLTransport(sock, ssl_context, server_hostname) + + return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py new file mode 100644 index 0000000000..94994f25ae --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py @@ -0,0 +1,153 @@ +"""The match_hostname() function from Python 3.5, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html +# It is modified to remove commonName support. + +from __future__ import annotations + +import ipaddress +import re +import typing +from ipaddress import IPv4Address, IPv6Address + +if typing.TYPE_CHECKING: + from .ssl_ import _TYPE_PEER_CERT_RET_DICT + +__version__ = "3.5.0.1" + + +class CertificateError(ValueError): + pass + + +def _dnsname_match( + dn: typing.Any, hostname: str, max_wildcards: int = 1 +) -> typing.Match[str] | None | bool: + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r".") + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count("*") + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn) + ) + + # speed up common case w/o wildcards + if not wildcards: + return bool(dn.lower() == hostname.lower()) + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == "*": + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append("[^.]+") + elif leftmost.startswith("xn--") or hostname.startswith("xn--"): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) + return pat.match(hostname) + + +def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: + """Exact matching of IP addresses. + + RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded + bytes of the IP address. An IP version 4 address is 4 octets, and an IP + version 6 address is 16 octets. [...] A reference identity of type IP-ID + matches if the address is identical to an iPAddress value of the + subjectAltName extension of the certificate." + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(ipname.rstrip()) + return bool(ip.packed == host_ip.packed) + + +def match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError( + "empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED" + ) + + try: + host_ip = ipaddress.ip_address(hostname) + except ValueError: + # Not an IP address (common case) + host_ip = None + dnsnames = [] + san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) + key: str + value: str + for key, value in san: + if key == "DNS": + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == "IP Address": + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + + # We only check 'commonName' if it's enabled and we're not verifying + # an IP address. IP addresses aren't valid within 'commonName'. + if hostname_checks_common_name and host_ip is None and not dnsnames: + for sub in cert.get("subject", ()): + for key, value in sub: + if key == "commonName": + if _dnsname_match(value, hostname): + return + dnsnames.append( + value + ) # Defensive: for older PyPy and OpenSSL versions + + if len(dnsnames) > 1: + raise CertificateError( + "hostname %r " + "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) + ) + elif len(dnsnames) == 1: + raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") + else: + raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py new file mode 100644 index 0000000000..6d59bc3bce --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +import io +import socket +import ssl +import typing + +from ..exceptions import ProxySchemeUnsupported + +if typing.TYPE_CHECKING: + from typing_extensions import Self + + from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT + + +_WriteBuffer = typing.Union[bytearray, memoryview] +_ReturnValue = typing.TypeVar("_ReturnValue") + +SSL_BLOCKSIZE = 16384 + + +class SSLTransport: + """ + The SSLTransport wraps an existing socket and establishes an SSL connection. + + Contrary to Python's implementation of SSLSocket, it allows you to chain + multiple TLS connections together. It's particularly useful if you need to + implement TLS within TLS. + + The class supports most of the socket API operations. + """ + + @staticmethod + def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: + """ + Raises a ProxySchemeUnsupported if the provided ssl_context can't be used + for TLS in TLS. + + The only requirement is that the ssl_context provides the 'wrap_bio' + methods. + """ + + if not hasattr(ssl_context, "wrap_bio"): + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "available on non-native SSLContext" + ) + + def __init__( + self, + socket: socket.socket, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + suppress_ragged_eofs: bool = True, + ) -> None: + """ + Create an SSLTransport around socket using the provided ssl_context. + """ + self.incoming = ssl.MemoryBIO() + self.outgoing = ssl.MemoryBIO() + + self.suppress_ragged_eofs = suppress_ragged_eofs + self.socket = socket + + self.sslobj = ssl_context.wrap_bio( + self.incoming, self.outgoing, server_hostname=server_hostname + ) + + # Perform initial handshake. + self._ssl_io_loop(self.sslobj.do_handshake) + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: typing.Any) -> None: + self.close() + + def fileno(self) -> int: + return self.socket.fileno() + + def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: + return self._wrap_ssl_read(len, buffer) + + def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv") + return self._wrap_ssl_read(buflen) + + def recv_into( + self, + buffer: _WriteBuffer, + nbytes: int | None = None, + flags: int = 0, + ) -> None | int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv_into") + if nbytes is None: + nbytes = len(buffer) + return self.read(nbytes, buffer) + + def sendall(self, data: bytes, flags: int = 0) -> None: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall") + count = 0 + with memoryview(data) as view, view.cast("B") as byte_view: + amount = len(byte_view) + while count < amount: + v = self.send(byte_view[count:]) + count += v + + def send(self, data: bytes, flags: int = 0) -> int: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send") + return self._ssl_io_loop(self.sslobj.write, data) + + def makefile( + self, + mode: str, + buffering: int | None = None, + *, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: + """ + Python's httpclient uses makefile and buffered io when reading HTTP + messages and we need to support it. + + This is unfortunately a copy and paste of socket.py makefile with small + changes to point to the socket directly. + """ + if not set(mode) <= {"r", "w", "b"}: + raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") + + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] + self.socket._io_refs += 1 # type: ignore[attr-defined] + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + buffer: typing.BinaryIO + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode # type: ignore[misc] + return text + + def unwrap(self) -> None: + self._ssl_io_loop(self.sslobj.unwrap) + + def close(self) -> None: + self.socket.close() + + @typing.overload + def getpeercert( + self, binary_form: typing.Literal[False] = ... + ) -> _TYPE_PEER_CERT_RET_DICT | None: ... + + @typing.overload + def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... + + def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: + return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] + + def version(self) -> str | None: + return self.sslobj.version() + + def cipher(self) -> tuple[str, str, int] | None: + return self.sslobj.cipher() + + def selected_alpn_protocol(self) -> str | None: + return self.sslobj.selected_alpn_protocol() + + def shared_ciphers(self) -> list[tuple[str, str, int]] | None: + return self.sslobj.shared_ciphers() + + def compression(self) -> str | None: + return self.sslobj.compression() + + def settimeout(self, value: float | None) -> None: + self.socket.settimeout(value) + + def gettimeout(self) -> float | None: + return self.socket.gettimeout() + + def _decref_socketios(self) -> None: + self.socket._decref_socketios() # type: ignore[attr-defined] + + def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: + try: + return self._ssl_io_loop(self.sslobj.read, len, buffer) + except ssl.SSLError as e: + if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: + return 0 # eof, return 0. + else: + raise + + # func is sslobj.do_handshake or sslobj.unwrap + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... + + # func is sslobj.write, arg1 is data + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... + + # func is sslobj.read, arg1 is len, arg2 is buffer + @typing.overload + def _ssl_io_loop( + self, + func: typing.Callable[[int, bytearray | None], bytes], + arg1: int, + arg2: bytearray | None, + ) -> bytes: ... + + def _ssl_io_loop( + self, + func: typing.Callable[..., _ReturnValue], + arg1: None | bytes | int = None, + arg2: bytearray | None = None, + ) -> _ReturnValue: + """Performs an I/O loop between incoming/outgoing and the socket.""" + should_loop = True + ret = None + + while should_loop: + errno = None + try: + if arg1 is None and arg2 is None: + ret = func() + elif arg2 is None: + ret = func(arg1) + else: + ret = func(arg1, arg2) + except ssl.SSLError as e: + if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): + # WANT_READ, and WANT_WRITE are expected, others are not. + raise e + errno = e.errno + + buf = self.outgoing.read() + self.socket.sendall(buf) + + if errno is None: + should_loop = False + elif errno == ssl.SSL_ERROR_WANT_READ: + buf = self.socket.recv(SSL_BLOCKSIZE) + if buf: + self.incoming.write(buf) + else: + self.incoming.write_eof() + return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py new file mode 100644 index 0000000000..4bb1be11d9 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +import time +import typing +from enum import Enum +from socket import getdefaulttimeout + +from ..exceptions import TimeoutStateError + +if typing.TYPE_CHECKING: + from typing import Final + + +class _TYPE_DEFAULT(Enum): + # This value should never be passed to socket.settimeout() so for safety we use a -1. + # socket.settimout() raises a ValueError for negative values. + token = -1 + + +_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token + +_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] + + +class Timeout: + """Timeout configuration. + + Timeouts can be defined as a default for a pool: + + .. code-block:: python + + import urllib3 + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + + http = urllib3.PoolManager(timeout=timeout) + + resp = http.request("GET", "https://example.com/") + + print(resp.status) + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``: + + .. code-block:: python + + no_timeout = Timeout(connect=None, read=None) + response = http.request("GET", "https://example.com/", timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: int, float, or None + + :param connect: + The maximum amount of time (in seconds) to wait for a connection + attempt to a server to succeed. Omitting the parameter will default the + connect timeout to the system default, probably `the global default + timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: int, float, or None + + :param read: + The maximum amount of time (in seconds) to wait between consecutive + read operations for a response from the server. Omitting the parameter + will default the read timeout to the system default, probably `the + global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: int, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT + + def __init__( + self, + total: _TYPE_TIMEOUT = None, + connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + ) -> None: + self._connect = self._validate_timeout(connect, "connect") + self._read = self._validate_timeout(read, "read") + self.total = self._validate_timeout(total, "total") + self._start_connect: float | None = None + + def __repr__(self) -> str: + return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" + + # __str__ provided for backwards compatibility + __str__ = __repr__ + + @staticmethod + def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: + return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout + + @classmethod + def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: + """Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If it is a numeric value less than or equal to + zero, or the type is not an integer, float, or None. + """ + if value is None or value is _DEFAULT_TIMEOUT: + return value + + if isinstance(value, bool): + raise ValueError( + "Timeout cannot be a boolean value. It must " + "be an int, float or None." + ) + try: + float(value) + except (TypeError, ValueError): + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + try: + if value <= 0: + raise ValueError( + "Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value) + ) + except TypeError: + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + return value + + @classmethod + def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: + """Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self) -> Timeout: + """Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, total=self.total) + + def start_connect(self) -> float: + """Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = time.monotonic() + return self._start_connect + + def get_connect_duration(self) -> float: + """Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time in seconds. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError( + "Can't get connect duration for timer that has not started." + ) + return time.monotonic() - self._start_connect + + @property + def connect_timeout(self) -> _TYPE_TIMEOUT: + """Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is _DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) # type: ignore[type-var] + + @property + def read_timeout(self) -> float | None: + """Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if ( + self.total is not None + and self.total is not _DEFAULT_TIMEOUT + and self._read is not None + and self._read is not _DEFAULT_TIMEOUT + ): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), self._read)) + elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py new file mode 100644 index 0000000000..db057f17be --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import re +import typing + +from ..exceptions import LocationParseError +from .util import to_str + +# We only want to normalize urls with an HTTP(S) scheme. +# urllib3 infers URLs without a scheme (None) to be http. +_NORMALIZABLE_SCHEMES = ("http", "https", None) + +# Almost all of these patterns were derived from the +# 'rfc3986' module: https://github.com/python-hyper/rfc3986 +_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") +_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") +_URI_RE = re.compile( + r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" + r"(?://([^\\/?#]*))?" + r"([^?#]*)" + r"(?:\?([^#]*))?" + r"(?:#(.*))?$", + re.UNICODE | re.DOTALL, +) + +_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" +_HEX_PAT = "[0-9A-Fa-f]{1,4}" +_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) +_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} +_variations = [ + # 6( h16 ":" ) ls32 + "(?:%(hex)s:){6}%(ls32)s", + # "::" 5( h16 ":" ) ls32 + "::(?:%(hex)s:){5}%(ls32)s", + # [ h16 ] "::" 4( h16 ":" ) ls32 + "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", + # [ *4( h16 ":" ) h16 ] "::" ls32 + "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", + # [ *5( h16 ":" ) h16 ] "::" h16 + "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", + # [ *6( h16 ":" ) h16 ] "::" + "(?:(?:%(hex)s:){0,6}%(hex)s)?::", +] + +_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" +_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" +_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" +_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" +_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" +_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") + +_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") +_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") +_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") +_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") +_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") + +_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( + _REG_NAME_PAT, + _IPV4_PAT, + _IPV6_ADDRZ_PAT, +) +_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) + +_UNRESERVED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" +) +_SUB_DELIM_CHARS = set("!$&'()*+,;=") +_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} +_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} +_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} + + +class Url( + typing.NamedTuple( + "Url", + [ + ("scheme", typing.Optional[str]), + ("auth", typing.Optional[str]), + ("host", typing.Optional[str]), + ("port", typing.Optional[int]), + ("path", typing.Optional[str]), + ("query", typing.Optional[str]), + ("fragment", typing.Optional[str]), + ], + ) +): + """ + Data structure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. Both the scheme and host are normalized as they are + both case-insensitive according to RFC 3986. + """ + + def __new__( # type: ignore[no-untyped-def] + cls, + scheme: str | None = None, + auth: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ): + if path and not path.startswith("/"): + path = "/" + path + if scheme is not None: + scheme = scheme.lower() + return super().__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self) -> str | None: + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self) -> str: + """Absolute path including the query string.""" + uri = self.path or "/" + + if self.query is not None: + uri += "?" + self.query + + return uri + + @property + def authority(self) -> str | None: + """ + Authority component as defined in RFC 3986 3.2. + This includes userinfo (auth), host and port. + + i.e. + userinfo@host:port + """ + userinfo = self.auth + netloc = self.netloc + if netloc is None or userinfo is None: + return netloc + else: + return f"{userinfo}@{netloc}" + + @property + def netloc(self) -> str | None: + """ + Network location including host and port. + + If you need the equivalent of urllib.parse's ``netloc``, + use the ``authority`` property instead. + """ + if self.host is None: + return None + if self.port: + return f"{self.host}:{self.port}" + return self.host + + @property + def url(self) -> str: + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: + + .. code-block:: python + + import urllib3 + + U = urllib3.util.parse_url("https://google.com/mail/") + + print(U.url) + # "https://google.com/mail/" + + print( urllib3.util.Url("https", "username:password", + "host.com", 80, "/path", "query", "fragment" + ).url + ) + # "https://username:password@host.com:80/path?query#fragment" + """ + scheme, auth, host, port, path, query, fragment = self + url = "" + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + "://" + if auth is not None: + url += auth + "@" + if host is not None: + url += host + if port is not None: + url += ":" + str(port) + if path is not None: + url += path + if query is not None: + url += "?" + query + if fragment is not None: + url += "#" + fragment + + return url + + def __str__(self) -> str: + return self.url + + +@typing.overload +def _encode_invalid_chars( + component: str, allowed_chars: typing.Container[str] +) -> str: # Abstract + ... + + +@typing.overload +def _encode_invalid_chars( + component: None, allowed_chars: typing.Container[str] +) -> None: # Abstract + ... + + +def _encode_invalid_chars( + component: str | None, allowed_chars: typing.Container[str] +) -> str | None: + """Percent-encodes a URI component without reapplying + onto an already percent-encoded component. + """ + if component is None: + return component + + component = to_str(component) + + # Normalize existing percent-encoded bytes. + # Try to see if the component we're encoding is already percent-encoded + # so we can skip all '%' characters but still encode all others. + component, percent_encodings = _PERCENT_RE.subn( + lambda match: match.group(0).upper(), component + ) + + uri_bytes = component.encode("utf-8", "surrogatepass") + is_percent_encoded = percent_encodings == uri_bytes.count(b"%") + encoded_component = bytearray() + + for i in range(0, len(uri_bytes)): + # Will return a single character bytestring + byte = uri_bytes[i : i + 1] + byte_ord = ord(byte) + if (is_percent_encoded and byte == b"%") or ( + byte_ord < 128 and byte.decode() in allowed_chars + ): + encoded_component += byte + continue + encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) + + return encoded_component.decode() + + +def _remove_path_dot_segments(path: str) -> str: + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = path.split("/") # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == ".": + continue + # Anything other than '..', should be appended to the output + if segment != "..": + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if path.startswith("/") and (not output or output[0]): + output.insert(0, "") + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if path.endswith(("/.", "/..")): + output.append("") + + return "/".join(output) + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + if host: + if scheme in _NORMALIZABLE_SCHEMES: + is_ipv6 = _IPV6_ADDRZ_RE.match(host) + if is_ipv6: + # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as + # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID + # separator as necessary to return a valid RFC 4007 scoped IP. + match = _ZONE_ID_RE.search(host) + if match: + start, end = match.span(1) + zone_id = host[start:end] + + if zone_id.startswith("%25") and zone_id != "%25": + zone_id = zone_id[3:] + else: + zone_id = zone_id[1:] + zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) + return f"{host[:start].lower()}%{zone_id}{host[end:]}" + else: + return host.lower() + elif not _IPV4_RE.match(host): + return to_str( + b".".join([_idna_encode(label) for label in host.split(".")]), + "ascii", + ) + return host + + +def _idna_encode(name: str) -> bytes: + if not name.isascii(): + try: + import idna + except ImportError: + raise LocationParseError( + "Unable to parse URL without the 'idna' module" + ) from None + + try: + return idna.encode(name.lower(), strict=True, std3_rules=True) + except idna.IDNAError: + raise LocationParseError( + f"Name '{name}' is not a valid IDNA label" + ) from None + + return name.lower().encode("ascii") + + +def _encode_target(target: str) -> str: + """Percent-encodes a request target so that there are no invalid characters + + Pre-condition for this function is that 'target' must start with '/'. + If that is the case then _TARGET_RE will always produce a match. + """ + match = _TARGET_RE.match(target) + if not match: # Defensive: + raise LocationParseError(f"{target!r} is not a valid request URI") + + path, query = match.groups() + encoded_target = _encode_invalid_chars(path, _PATH_CHARS) + if query is not None: + query = _encode_invalid_chars(query, _QUERY_CHARS) + encoded_target += "?" + query + return encoded_target + + +def parse_url(url: str) -> Url: + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + This parser is RFC 3986 and RFC 6874 compliant. + + The parser logic and helper functions are based heavily on + work done in the ``rfc3986`` module. + + :param str url: URL to parse into a :class:`.Url` namedtuple. + + Partly backwards-compatible with :mod:`urllib.parse`. + + Example: + + .. code-block:: python + + import urllib3 + + print( urllib3.util.parse_url('http://google.com/mail/')) + # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + + print( urllib3.util.parse_url('google.com:80')) + # Url(scheme=None, host='google.com', port=80, path=None, ...) + + print( urllib3.util.parse_url('/foo?bar')) + # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + if not url: + # Empty + return Url() + + source_url = url + if not _SCHEME_RE.search(url): + url = "//" + url + + scheme: str | None + authority: str | None + auth: str | None + host: str | None + port: str | None + port_int: int | None + path: str | None + query: str | None + fragment: str | None + + try: + scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] + normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES + + if scheme: + scheme = scheme.lower() + + if authority: + auth, _, host_port = authority.rpartition("@") + auth = auth or None + host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] + if auth and normalize_uri: + auth = _encode_invalid_chars(auth, _USERINFO_CHARS) + if port == "": + port = None + else: + auth, host, port = None, None, None + + if port is not None: + port_int = int(port) + if not (0 <= port_int <= 65535): + raise LocationParseError(url) + else: + port_int = None + + host = _normalize_host(host, scheme) + + if normalize_uri and path: + path = _remove_path_dot_segments(path) + path = _encode_invalid_chars(path, _PATH_CHARS) + if normalize_uri and query: + query = _encode_invalid_chars(query, _QUERY_CHARS) + if normalize_uri and fragment: + fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) + + except (ValueError, AttributeError) as e: + raise LocationParseError(source_url) from e + + # For the sake of backwards compatibility we put empty + # string values for path if there are any defined values + # beyond the path in the URL. + # TODO: Remove this when we break backwards compatibility. + if not path: + if query is not None or fragment is not None: + path = "" + else: + path = None + + return Url( + scheme=scheme, + auth=auth, + host=host, + port=port_int, + path=path, + query=query, + fragment=fragment, + ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py new file mode 100644 index 0000000000..35c77e4025 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import typing +from types import TracebackType + + +def to_bytes( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> bytes: + if isinstance(x, bytes): + return x + elif not isinstance(x, str): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.encode(encoding or "utf-8", errors=errors or "strict") + return x.encode() + + +def to_str( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> str: + if isinstance(x, str): + return x + elif not isinstance(x, bytes): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.decode(encoding or "utf-8", errors=errors or "strict") + return x.decode() + + +def reraise( + tp: type[BaseException] | None, + value: BaseException, + tb: TracebackType | None = None, +) -> typing.NoReturn: + try: + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None # type: ignore[assignment] + tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py new file mode 100644 index 0000000000..aeca0c7ad5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import select +import socket +from functools import partial + +__all__ = ["wait_for_read", "wait_for_write"] + + +# How should we wait on sockets? +# +# There are two types of APIs you can use for waiting on sockets: the fancy +# modern stateful APIs like epoll/kqueue, and the older stateless APIs like +# select/poll. The stateful APIs are more efficient when you have a lots of +# sockets to keep track of, because you can set them up once and then use them +# lots of times. But we only ever want to wait on a single socket at a time +# and don't want to keep track of state, so the stateless APIs are actually +# more efficient. So we want to use select() or poll(). +# +# Now, how do we choose between select() and poll()? On traditional Unixes, +# select() has a strange calling convention that makes it slow, or fail +# altogether, for high-numbered file descriptors. The point of poll() is to fix +# that, so on Unixes, we prefer poll(). +# +# On Windows, there is no poll() (or at least Python doesn't provide a wrapper +# for it), but that's OK, because on Windows, select() doesn't have this +# strange calling convention; plain select() works fine. +# +# So: on Windows we use select(), and everywhere else we use poll(). We also +# fall back to select() in case poll() is somehow broken or missing. + + +def select_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + rcheck = [] + wcheck = [] + if read: + rcheck.append(sock) + if write: + wcheck.append(sock) + # When doing a non-blocking connect, most systems signal success by + # marking the socket writable. Windows, though, signals success by marked + # it as "exceptional". We paper over the difference by checking the write + # sockets for both conditions. (The stdlib selectors module does the same + # thing.) + fn = partial(select.select, rcheck, wcheck, wcheck) + rready, wready, xready = fn(timeout) + return bool(rready or wready or xready) + + +def poll_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + mask = 0 + if read: + mask |= select.POLLIN + if write: + mask |= select.POLLOUT + poll_obj = select.poll() + poll_obj.register(sock, mask) + + # For some reason, poll() takes timeout in milliseconds + def do_poll(t: float | None) -> list[tuple[int, int]]: + if t is not None: + t *= 1000 + return poll_obj.poll(t) + + return bool(do_poll(timeout)) + + +def _have_working_poll() -> bool: + # Apparently some systems have a select.poll that fails as soon as you try + # to use it, either due to strange configuration or broken monkeypatching + # from libraries like eventlet/greenlet. + try: + poll_obj = select.poll() + poll_obj.poll(0) + except (AttributeError, OSError): + return False + else: + return True + + +def wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + # We delay choosing which implementation to use until the first time we're + # called. We could do it at import time, but then we might make the wrong + # decision if someone goes wild with monkeypatching select.poll after + # we're imported. + global wait_for_socket + if _have_working_poll(): + wait_for_socket = poll_wait_for_socket + elif hasattr(select, "select"): + wait_for_socket = select_wait_for_socket + return wait_for_socket(sock, read, write, timeout) + + +def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for reading to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, read=True, timeout=timeout) + + +def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for writing to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index ec01284e58..e01001fb6b 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -354,7 +354,7 @@ def test_non_dict_event( assert transaction_event["tags"]["batch_request"] is True -def test_request_data(lambda_client, test_environment): +def test_request_data_with_send_default_pii_false(lambda_client, test_environment): payload = b""" { "resource": "/asd", @@ -363,7 +363,9 @@ def test_request_data(lambda_client, test_environment): "headers": { "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", "User-Agent": "custom", - "X-Forwarded-Proto": "https" + "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret" }, "queryStringParameters": { "bonkers": "true" @@ -391,9 +393,186 @@ def test_request_data(lambda_client, test_environment): assert transaction_event["request"] == { "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + # X-Forwarded-Proto is not sensitive and passes through. + "X-Forwarded-Proto": "https", + # With send_default_pii=False, _filter_headers substitutes the + # SENSITIVE_HEADERS (Authorization, Cookie); the EventScrubber + # also scrubs them. Both end up as "[Filtered]". + "Authorization": "[Filtered]", + "Cookie": "[Filtered]", + }, + "method": "GET", + "query_string": {"bonkers": "true"}, + "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", + } + + +def test_request_data_with_send_default_pii_true(lambda_client, test_environment): + payload = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret" + }, + "queryStringParameters": { + "bonkers": "true" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + } + }, + "body": null, + "isBase64Encoded": false + } + """ + + lambda_client.invoke( + FunctionName="BasicOkSendDefaultPii", + Payload=payload, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + # With send_default_pii=True (and no data_collection config), + # _filter_headers passes headers through untouched. Authorization + # and Cookie are still scrubbed to "[Filtered]" by the always-on + # EventScrubber (DEFAULT_DENYLIST), independent of PII settings. + "Authorization": "[Filtered]", + "Cookie": "[Filtered]", + }, + "method": "GET", + "query_string": {"bonkers": "true"}, + "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", + "data": None, + } + + +def test_request_data_with_data_collection_allowlist(lambda_client, test_environment): + payload = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret", + "X-Allow-Me": "yes" + }, + "queryStringParameters": { + "bonkers": "true" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + } + }, + "body": null, + "isBase64Encoded": false + } + """ + + lambda_client.invoke( + FunctionName="BasicOkDataCollectionAllowlist", + Payload=payload, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + "headers": { + # Allowlisted, non-sensitive headers pass through. + "User-Agent": "custom", + "X-Allow-Me": "yes", + # Not allowlisted -> substituted. + "Host": "[Filtered]", + "X-Forwarded-Proto": "[Filtered]", + # Allowlisted but sensitive -> still filtered; an allowlist entry + # cannot override the built-in sensitive denylist. + "Authorization": "[Filtered]", + # Not allowlisted, and cookies are always substituted. + "Cookie": "[Filtered]", + }, + "method": "GET", + "query_string": {"bonkers": "true"}, + "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", + } + + +def test_request_data_with_data_collection_denylist(lambda_client, test_environment): + payload = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", "User-Agent": "custom", "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret", + "X-Custom": "keep-me" + }, + "queryStringParameters": { + "bonkers": "true" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + } + }, + "body": null, + "isBase64Encoded": false + } + """ + + lambda_client.invoke( + FunctionName="BasicOkDataCollectionDenylist", + Payload=payload, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + "headers": { + # Not denied by any term -> pass through. + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "X-Custom": "keep-me", + # Denied by custom terms. + "User-Agent": "[Filtered]", + "X-Forwarded-Proto": "[Filtered]", + # Denied by the built-in sensitive denylist. + "Authorization": "[Filtered]", + # Cookies are always substituted. + "Cookie": "[Filtered]", }, "method": "GET", "query_string": {"bonkers": "true"}, @@ -401,6 +580,52 @@ def test_request_data(lambda_client, test_environment): } +def test_request_data_with_data_collection_off(lambda_client, test_environment): + payload = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret" + }, + "queryStringParameters": { + "bonkers": "true" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + } + }, + "body": null, + "isBase64Encoded": false + } + """ + + lambda_client.invoke( + FunctionName="BasicOkDataCollectionOff", + Payload=payload, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + # With request headers collection turned off, no headers are collected. + "headers": {}, + "method": "GET", + "query_string": {"bonkers": "true"}, + "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", + } + + def test_trace_continuation(lambda_client, test_environment): trace_id = "471a43a4192642f0b136d5159a501701" parent_span_id = "6e8f22c393e68f19" From 61d16186a9670aed99cd3332622dd14f8325d3b0 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 16:07:48 -0400 Subject: [PATCH 7/8] fix(aws-lambda): Remove vendored deps from new embedded-sdk test fixtures The new lambda_functions_with_embedded_sdk fixture directories were missing the .gitignore that the other fixtures use to keep everything except index.py untracked. As a result, certifi and urllib3 packages installed by the test setup got committed, and ruff failed CI linting against them since they're unmodified third-party code. Add the missing .gitignore to each new fixture directory and remove the committed vendored packages; they are regenerated automatically at test time via `uv pip install --target`. --- .../BasicOkDataCollectionAllowlist/.gitignore | 11 + .../certifi-2026.6.17.dist-info/INSTALLER | 1 - .../certifi-2026.6.17.dist-info/METADATA | 78 - .../certifi-2026.6.17.dist-info/RECORD | 12 - .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 - .../licenses/LICENSE | 20 - .../certifi-2026.6.17.dist-info/top_level.txt | 1 - .../certifi/__init__.py | 4 - .../certifi/__main__.py | 12 - .../certifi/cacert.pem | 3863 ----------------- .../certifi/core.py | 83 - .../certifi/py.typed | 0 .../sentry_sdk/__init__.py | 70 - .../sentry_sdk/_batcher.py | 184 - .../sentry_sdk/_compat.py | 92 - .../sentry_sdk/_init_implementation.py | 78 - .../sentry_sdk/_log_batcher.py | 66 - .../sentry_sdk/_lru_cache.py | 43 - .../sentry_sdk/_metrics_batcher.py | 48 - .../sentry_sdk/_queue.py | 287 -- .../sentry_sdk/_span_batcher.py | 234 - .../sentry_sdk/_types.py | 481 -- .../sentry_sdk/_werkzeug.py | 100 - .../sentry_sdk/ai/__init__.py | 8 - .../sentry_sdk/ai/_openai_completions_api.py | 66 - .../sentry_sdk/ai/_openai_responses_api.py | 22 - .../sentry_sdk/ai/consts.py | 6 - .../sentry_sdk/ai/monitoring.py | 147 - .../sentry_sdk/ai/utils.py | 782 ---- .../sentry_sdk/api.py | 573 --- .../sentry_sdk/attachments.py | 71 - .../sentry_sdk/client.py | 1479 ------- .../sentry_sdk/consts.py | 1802 -------- .../sentry_sdk/crons/__init__.py | 9 - .../sentry_sdk/crons/api.py | 60 - .../sentry_sdk/crons/consts.py | 4 - .../sentry_sdk/crons/decorator.py | 137 - .../sentry_sdk/data_collection.py | 319 -- .../sentry_sdk/debug.py | 37 - .../sentry_sdk/envelope.py | 332 -- .../sentry_sdk/feature_flags.py | 75 - .../sentry_sdk/hub.py | 741 ---- .../sentry_sdk/integrations/__init__.py | 352 -- .../sentry_sdk/integrations/_asgi_common.py | 187 - .../sentry_sdk/integrations/_wsgi_common.py | 282 -- .../sentry_sdk/integrations/aiohttp.py | 509 --- .../sentry_sdk/integrations/aiomysql.py | 275 -- .../sentry_sdk/integrations/anthropic.py | 1154 ----- .../sentry_sdk/integrations/argv.py | 28 - .../sentry_sdk/integrations/ariadne.py | 167 - .../sentry_sdk/integrations/arq.py | 277 -- .../sentry_sdk/integrations/asgi.py | 543 --- .../sentry_sdk/integrations/asyncio.py | 280 -- .../sentry_sdk/integrations/asyncpg.py | 312 -- .../sentry_sdk/integrations/atexit.py | 51 - .../sentry_sdk/integrations/aws_lambda.py | 542 --- .../sentry_sdk/integrations/beam.py | 164 - .../sentry_sdk/integrations/boto3.py | 187 - .../sentry_sdk/integrations/bottle.py | 239 - .../integrations/celery/__init__.py | 612 --- .../sentry_sdk/integrations/celery/beat.py | 291 -- .../sentry_sdk/integrations/celery/utils.py | 32 - .../sentry_sdk/integrations/chalice.py | 188 - .../integrations/clickhouse_driver.py | 211 - .../integrations/cloud_resource_context.py | 273 -- .../sentry_sdk/integrations/cohere.py | 303 -- .../sentry_sdk/integrations/dedupe.py | 62 - .../integrations/django/__init__.py | 884 ---- .../sentry_sdk/integrations/django/asgi.py | 262 -- .../sentry_sdk/integrations/django/caching.py | 264 -- .../integrations/django/middleware.py | 219 - .../integrations/django/signals_handlers.py | 105 - .../sentry_sdk/integrations/django/tasks.py | 52 - .../integrations/django/templates.py | 209 - .../integrations/django/transactions.py | 154 - .../sentry_sdk/integrations/django/views.py | 127 - .../sentry_sdk/integrations/dramatiq.py | 246 -- .../sentry_sdk/integrations/excepthook.py | 76 - .../sentry_sdk/integrations/executing.py | 66 - .../sentry_sdk/integrations/falcon.py | 278 -- .../sentry_sdk/integrations/fastapi.py | 201 - .../sentry_sdk/integrations/flask.py | 281 -- .../sentry_sdk/integrations/gcp.py | 286 -- .../sentry_sdk/integrations/gnu_backtrace.py | 96 - .../integrations/google_genai/__init__.py | 457 -- .../integrations/google_genai/consts.py | 16 - .../integrations/google_genai/streaming.py | 172 - .../integrations/google_genai/utils.py | 1118 ----- .../sentry_sdk/integrations/gql.py | 173 - .../sentry_sdk/integrations/graphene.py | 181 - .../sentry_sdk/integrations/grpc/__init__.py | 188 - .../integrations/grpc/aio/__init__.py | 7 - .../integrations/grpc/aio/client.py | 146 - .../integrations/grpc/aio/server.py | 134 - .../sentry_sdk/integrations/grpc/client.py | 149 - .../sentry_sdk/integrations/grpc/consts.py | 1 - .../sentry_sdk/integrations/grpc/server.py | 91 - .../sentry_sdk/integrations/httpx.py | 263 -- .../sentry_sdk/integrations/httpx2.py | 263 -- .../sentry_sdk/integrations/huey.py | 239 - .../integrations/huggingface_hub.py | 392 -- .../sentry_sdk/integrations/langchain.py | 1412 ------ .../sentry_sdk/integrations/langgraph.py | 515 --- .../sentry_sdk/integrations/launchdarkly.py | 63 - .../sentry_sdk/integrations/litellm.py | 376 -- .../sentry_sdk/integrations/litestar.py | 364 -- .../sentry_sdk/integrations/logging.py | 476 -- .../sentry_sdk/integrations/loguru.py | 208 - .../sentry_sdk/integrations/mcp.py | 876 ---- .../sentry_sdk/integrations/modules.py | 28 - .../sentry_sdk/integrations/openai.py | 1530 ------- .../integrations/openai_agents/__init__.py | 250 -- .../integrations/openai_agents/consts.py | 1 - .../openai_agents/patches/__init__.py | 10 - .../openai_agents/patches/agent_run.py | 332 -- .../openai_agents/patches/error_tracing.py | 74 - .../openai_agents/patches/models.py | 197 - .../openai_agents/patches/runner.py | 221 - .../openai_agents/patches/tools.py | 70 - .../openai_agents/spans/__init__.py | 8 - .../openai_agents/spans/agent_workflow.py | 32 - .../openai_agents/spans/ai_client.py | 84 - .../openai_agents/spans/execute_tool.py | 82 - .../openai_agents/spans/handoff.py | 41 - .../openai_agents/spans/invoke_agent.py | 122 - .../integrations/openai_agents/utils.py | 244 -- .../sentry_sdk/integrations/openfeature.py | 34 - .../integrations/opentelemetry/__init__.py | 7 - .../integrations/opentelemetry/consts.py | 9 - .../integrations/opentelemetry/integration.py | 81 - .../integrations/opentelemetry/propagator.py | 128 - .../opentelemetry/span_processor.py | 407 -- .../sentry_sdk/integrations/otlp.py | 223 - .../sentry_sdk/integrations/pure_eval.py | 134 - .../integrations/pydantic_ai/__init__.py | 187 - .../integrations/pydantic_ai/consts.py | 1 - .../pydantic_ai/patches/__init__.py | 3 - .../pydantic_ai/patches/agent_run.py | 198 - .../pydantic_ai/patches/graph_nodes.py | 106 - .../integrations/pydantic_ai/patches/tools.py | 177 - .../pydantic_ai/spans/__init__.py | 3 - .../pydantic_ai/spans/ai_client.py | 331 -- .../pydantic_ai/spans/execute_tool.py | 84 - .../pydantic_ai/spans/invoke_agent.py | 184 - .../integrations/pydantic_ai/spans/utils.py | 87 - .../integrations/pydantic_ai/utils.py | 233 - .../sentry_sdk/integrations/pymongo.py | 262 -- .../sentry_sdk/integrations/pyramid.py | 239 - .../sentry_sdk/integrations/pyreqwest.py | 197 - .../sentry_sdk/integrations/quart.py | 292 -- .../sentry_sdk/integrations/ray.py | 240 - .../sentry_sdk/integrations/redis/__init__.py | 49 - .../integrations/redis/_async_common.py | 177 - .../integrations/redis/_sync_common.py | 176 - .../sentry_sdk/integrations/redis/consts.py | 19 - .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 - .../integrations/redis/modules/queries.py | 88 - .../sentry_sdk/integrations/redis/rb.py | 31 - .../sentry_sdk/integrations/redis/redis.py | 67 - .../integrations/redis/redis_cluster.py | 115 - .../redis/redis_py_cluster_legacy.py | 49 - .../sentry_sdk/integrations/redis/utils.py | 159 - .../sentry_sdk/integrations/rq.py | 225 - .../sentry_sdk/integrations/rust_tracing.py | 300 -- .../sentry_sdk/integrations/sanic.py | 431 -- .../sentry_sdk/integrations/serverless.py | 65 - .../sentry_sdk/integrations/socket.py | 142 - .../sentry_sdk/integrations/spark/__init__.py | 4 - .../integrations/spark/spark_driver.py | 278 -- .../integrations/spark/spark_worker.py | 109 - .../sentry_sdk/integrations/sqlalchemy.py | 185 - .../sentry_sdk/integrations/starlette.py | 858 ---- .../sentry_sdk/integrations/starlite.py | 314 -- .../sentry_sdk/integrations/statsig.py | 37 - .../sentry_sdk/integrations/stdlib.py | 404 -- .../sentry_sdk/integrations/strawberry.py | 493 --- .../sentry_sdk/integrations/sys_exit.py | 65 - .../sentry_sdk/integrations/threading.py | 196 - .../sentry_sdk/integrations/tornado.py | 320 -- .../sentry_sdk/integrations/trytond.py | 52 - .../sentry_sdk/integrations/typer.py | 58 - .../sentry_sdk/integrations/unleash.py | 33 - .../sentry_sdk/integrations/unraisablehook.py | 50 - .../sentry_sdk/integrations/wsgi.py | 427 -- .../sentry_sdk/logger.py | 88 - .../sentry_sdk/metrics.py | 62 - .../sentry_sdk/monitor.py | 138 - .../sentry_sdk/profiler/__init__.py | 49 - .../profiler/continuous_profiler.py | 703 --- .../profiler/transaction_profiler.py | 802 ---- .../sentry_sdk/profiler/utils.py | 186 - .../sentry_sdk/py.typed | 0 .../sentry_sdk/scope.py | 2185 ---------- .../sentry_sdk/scrubber.py | 176 - .../sentry_sdk/serializer.py | 418 -- .../sentry_sdk/session.py | 165 - .../sentry_sdk/sessions.py | 262 -- .../sentry_sdk/spotlight.py | 327 -- .../sentry_sdk/traces.py | 843 ---- .../sentry_sdk/tracing.py | 1475 ------- .../sentry_sdk/tracing_utils.py | 1694 -------- .../sentry_sdk/transport.py | 1255 ------ .../sentry_sdk/types.py | 52 - .../sentry_sdk/utils.py | 2178 ---------- .../sentry_sdk/worker.py | 308 -- .../urllib3-2.7.0.dist-info/INSTALLER | 1 - .../urllib3-2.7.0.dist-info/METADATA | 163 - .../urllib3-2.7.0.dist-info/RECORD | 44 - .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 - .../licenses/LICENSE.txt | 21 - .../urllib3/__init__.py | 211 - .../urllib3/_base_connection.py | 167 - .../urllib3/_collections.py | 486 --- .../urllib3/_request_methods.py | 278 -- .../urllib3/_version.py | 24 - .../urllib3/connection.py | 1099 ----- .../urllib3/connectionpool.py | 1191 ----- .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 - .../urllib3/contrib/emscripten/connection.py | 260 -- .../emscripten/emscripten_fetch_worker.js | 110 - .../urllib3/contrib/emscripten/fetch.py | 726 ---- .../urllib3/contrib/emscripten/request.py | 22 - .../urllib3/contrib/emscripten/response.py | 281 -- .../urllib3/contrib/pyopenssl.py | 563 --- .../urllib3/contrib/socks.py | 228 - .../urllib3/exceptions.py | 335 -- .../urllib3/fields.py | 341 -- .../urllib3/filepost.py | 89 - .../urllib3/http2/__init__.py | 53 - .../urllib3/http2/connection.py | 356 -- .../urllib3/http2/probe.py | 87 - .../urllib3/poolmanager.py | 653 --- .../urllib3/py.typed | 2 - .../urllib3/response.py | 1493 ------- .../urllib3/util/__init__.py | 42 - .../urllib3/util/connection.py | 137 - .../urllib3/util/proxy.py | 43 - .../urllib3/util/request.py | 263 -- .../urllib3/util/response.py | 101 - .../urllib3/util/retry.py | 557 --- .../urllib3/util/ssl_.py | 477 -- .../urllib3/util/ssl_match_hostname.py | 153 - .../urllib3/util/ssltransport.py | 271 -- .../urllib3/util/timeout.py | 275 -- .../urllib3/util/url.py | 469 -- .../urllib3/util/util.py | 42 - .../urllib3/util/wait.py | 124 - .../BasicOkDataCollectionDenylist/.gitignore | 11 + .../certifi-2026.6.17.dist-info/INSTALLER | 1 - .../certifi-2026.6.17.dist-info/METADATA | 78 - .../certifi-2026.6.17.dist-info/RECORD | 12 - .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 - .../licenses/LICENSE | 20 - .../certifi-2026.6.17.dist-info/top_level.txt | 1 - .../certifi/__init__.py | 4 - .../certifi/__main__.py | 12 - .../certifi/cacert.pem | 3863 ----------------- .../certifi/core.py | 83 - .../certifi/py.typed | 0 .../sentry_sdk/__init__.py | 70 - .../sentry_sdk/_batcher.py | 184 - .../sentry_sdk/_compat.py | 92 - .../sentry_sdk/_init_implementation.py | 78 - .../sentry_sdk/_log_batcher.py | 66 - .../sentry_sdk/_lru_cache.py | 43 - .../sentry_sdk/_metrics_batcher.py | 48 - .../sentry_sdk/_queue.py | 287 -- .../sentry_sdk/_span_batcher.py | 234 - .../sentry_sdk/_types.py | 481 -- .../sentry_sdk/_werkzeug.py | 100 - .../sentry_sdk/ai/__init__.py | 8 - .../sentry_sdk/ai/_openai_completions_api.py | 66 - .../sentry_sdk/ai/_openai_responses_api.py | 22 - .../sentry_sdk/ai/consts.py | 6 - .../sentry_sdk/ai/monitoring.py | 147 - .../sentry_sdk/ai/utils.py | 782 ---- .../sentry_sdk/api.py | 573 --- .../sentry_sdk/attachments.py | 71 - .../sentry_sdk/client.py | 1479 ------- .../sentry_sdk/consts.py | 1802 -------- .../sentry_sdk/crons/__init__.py | 9 - .../sentry_sdk/crons/api.py | 60 - .../sentry_sdk/crons/consts.py | 4 - .../sentry_sdk/crons/decorator.py | 137 - .../sentry_sdk/data_collection.py | 319 -- .../sentry_sdk/debug.py | 37 - .../sentry_sdk/envelope.py | 332 -- .../sentry_sdk/feature_flags.py | 75 - .../sentry_sdk/hub.py | 741 ---- .../sentry_sdk/integrations/__init__.py | 352 -- .../sentry_sdk/integrations/_asgi_common.py | 187 - .../sentry_sdk/integrations/_wsgi_common.py | 282 -- .../sentry_sdk/integrations/aiohttp.py | 509 --- .../sentry_sdk/integrations/aiomysql.py | 275 -- .../sentry_sdk/integrations/anthropic.py | 1154 ----- .../sentry_sdk/integrations/argv.py | 28 - .../sentry_sdk/integrations/ariadne.py | 167 - .../sentry_sdk/integrations/arq.py | 277 -- .../sentry_sdk/integrations/asgi.py | 543 --- .../sentry_sdk/integrations/asyncio.py | 280 -- .../sentry_sdk/integrations/asyncpg.py | 312 -- .../sentry_sdk/integrations/atexit.py | 51 - .../sentry_sdk/integrations/aws_lambda.py | 542 --- .../sentry_sdk/integrations/beam.py | 164 - .../sentry_sdk/integrations/boto3.py | 187 - .../sentry_sdk/integrations/bottle.py | 239 - .../integrations/celery/__init__.py | 612 --- .../sentry_sdk/integrations/celery/beat.py | 291 -- .../sentry_sdk/integrations/celery/utils.py | 32 - .../sentry_sdk/integrations/chalice.py | 188 - .../integrations/clickhouse_driver.py | 211 - .../integrations/cloud_resource_context.py | 273 -- .../sentry_sdk/integrations/cohere.py | 303 -- .../sentry_sdk/integrations/dedupe.py | 62 - .../integrations/django/__init__.py | 884 ---- .../sentry_sdk/integrations/django/asgi.py | 262 -- .../sentry_sdk/integrations/django/caching.py | 264 -- .../integrations/django/middleware.py | 219 - .../integrations/django/signals_handlers.py | 105 - .../sentry_sdk/integrations/django/tasks.py | 52 - .../integrations/django/templates.py | 209 - .../integrations/django/transactions.py | 154 - .../sentry_sdk/integrations/django/views.py | 127 - .../sentry_sdk/integrations/dramatiq.py | 246 -- .../sentry_sdk/integrations/excepthook.py | 76 - .../sentry_sdk/integrations/executing.py | 66 - .../sentry_sdk/integrations/falcon.py | 278 -- .../sentry_sdk/integrations/fastapi.py | 201 - .../sentry_sdk/integrations/flask.py | 281 -- .../sentry_sdk/integrations/gcp.py | 286 -- .../sentry_sdk/integrations/gnu_backtrace.py | 96 - .../integrations/google_genai/__init__.py | 457 -- .../integrations/google_genai/consts.py | 16 - .../integrations/google_genai/streaming.py | 172 - .../integrations/google_genai/utils.py | 1118 ----- .../sentry_sdk/integrations/gql.py | 173 - .../sentry_sdk/integrations/graphene.py | 181 - .../sentry_sdk/integrations/grpc/__init__.py | 188 - .../integrations/grpc/aio/__init__.py | 7 - .../integrations/grpc/aio/client.py | 146 - .../integrations/grpc/aio/server.py | 134 - .../sentry_sdk/integrations/grpc/client.py | 149 - .../sentry_sdk/integrations/grpc/consts.py | 1 - .../sentry_sdk/integrations/grpc/server.py | 91 - .../sentry_sdk/integrations/httpx.py | 263 -- .../sentry_sdk/integrations/httpx2.py | 263 -- .../sentry_sdk/integrations/huey.py | 239 - .../integrations/huggingface_hub.py | 392 -- .../sentry_sdk/integrations/langchain.py | 1412 ------ .../sentry_sdk/integrations/langgraph.py | 515 --- .../sentry_sdk/integrations/launchdarkly.py | 63 - .../sentry_sdk/integrations/litellm.py | 376 -- .../sentry_sdk/integrations/litestar.py | 364 -- .../sentry_sdk/integrations/logging.py | 476 -- .../sentry_sdk/integrations/loguru.py | 208 - .../sentry_sdk/integrations/mcp.py | 876 ---- .../sentry_sdk/integrations/modules.py | 28 - .../sentry_sdk/integrations/openai.py | 1530 ------- .../integrations/openai_agents/__init__.py | 250 -- .../integrations/openai_agents/consts.py | 1 - .../openai_agents/patches/__init__.py | 10 - .../openai_agents/patches/agent_run.py | 332 -- .../openai_agents/patches/error_tracing.py | 74 - .../openai_agents/patches/models.py | 197 - .../openai_agents/patches/runner.py | 221 - .../openai_agents/patches/tools.py | 70 - .../openai_agents/spans/__init__.py | 8 - .../openai_agents/spans/agent_workflow.py | 32 - .../openai_agents/spans/ai_client.py | 84 - .../openai_agents/spans/execute_tool.py | 82 - .../openai_agents/spans/handoff.py | 41 - .../openai_agents/spans/invoke_agent.py | 122 - .../integrations/openai_agents/utils.py | 244 -- .../sentry_sdk/integrations/openfeature.py | 34 - .../integrations/opentelemetry/__init__.py | 7 - .../integrations/opentelemetry/consts.py | 9 - .../integrations/opentelemetry/integration.py | 81 - .../integrations/opentelemetry/propagator.py | 128 - .../opentelemetry/span_processor.py | 407 -- .../sentry_sdk/integrations/otlp.py | 223 - .../sentry_sdk/integrations/pure_eval.py | 134 - .../integrations/pydantic_ai/__init__.py | 187 - .../integrations/pydantic_ai/consts.py | 1 - .../pydantic_ai/patches/__init__.py | 3 - .../pydantic_ai/patches/agent_run.py | 198 - .../pydantic_ai/patches/graph_nodes.py | 106 - .../integrations/pydantic_ai/patches/tools.py | 177 - .../pydantic_ai/spans/__init__.py | 3 - .../pydantic_ai/spans/ai_client.py | 331 -- .../pydantic_ai/spans/execute_tool.py | 84 - .../pydantic_ai/spans/invoke_agent.py | 184 - .../integrations/pydantic_ai/spans/utils.py | 87 - .../integrations/pydantic_ai/utils.py | 233 - .../sentry_sdk/integrations/pymongo.py | 262 -- .../sentry_sdk/integrations/pyramid.py | 239 - .../sentry_sdk/integrations/pyreqwest.py | 197 - .../sentry_sdk/integrations/quart.py | 292 -- .../sentry_sdk/integrations/ray.py | 240 - .../sentry_sdk/integrations/redis/__init__.py | 49 - .../integrations/redis/_async_common.py | 177 - .../integrations/redis/_sync_common.py | 176 - .../sentry_sdk/integrations/redis/consts.py | 19 - .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 - .../integrations/redis/modules/queries.py | 88 - .../sentry_sdk/integrations/redis/rb.py | 31 - .../sentry_sdk/integrations/redis/redis.py | 67 - .../integrations/redis/redis_cluster.py | 115 - .../redis/redis_py_cluster_legacy.py | 49 - .../sentry_sdk/integrations/redis/utils.py | 159 - .../sentry_sdk/integrations/rq.py | 225 - .../sentry_sdk/integrations/rust_tracing.py | 300 -- .../sentry_sdk/integrations/sanic.py | 431 -- .../sentry_sdk/integrations/serverless.py | 65 - .../sentry_sdk/integrations/socket.py | 142 - .../sentry_sdk/integrations/spark/__init__.py | 4 - .../integrations/spark/spark_driver.py | 278 -- .../integrations/spark/spark_worker.py | 109 - .../sentry_sdk/integrations/sqlalchemy.py | 185 - .../sentry_sdk/integrations/starlette.py | 858 ---- .../sentry_sdk/integrations/starlite.py | 314 -- .../sentry_sdk/integrations/statsig.py | 37 - .../sentry_sdk/integrations/stdlib.py | 404 -- .../sentry_sdk/integrations/strawberry.py | 493 --- .../sentry_sdk/integrations/sys_exit.py | 65 - .../sentry_sdk/integrations/threading.py | 196 - .../sentry_sdk/integrations/tornado.py | 320 -- .../sentry_sdk/integrations/trytond.py | 52 - .../sentry_sdk/integrations/typer.py | 58 - .../sentry_sdk/integrations/unleash.py | 33 - .../sentry_sdk/integrations/unraisablehook.py | 50 - .../sentry_sdk/integrations/wsgi.py | 427 -- .../sentry_sdk/logger.py | 88 - .../sentry_sdk/metrics.py | 62 - .../sentry_sdk/monitor.py | 138 - .../sentry_sdk/profiler/__init__.py | 49 - .../profiler/continuous_profiler.py | 703 --- .../profiler/transaction_profiler.py | 802 ---- .../sentry_sdk/profiler/utils.py | 186 - .../sentry_sdk/py.typed | 0 .../sentry_sdk/scope.py | 2185 ---------- .../sentry_sdk/scrubber.py | 176 - .../sentry_sdk/serializer.py | 418 -- .../sentry_sdk/session.py | 165 - .../sentry_sdk/sessions.py | 262 -- .../sentry_sdk/spotlight.py | 327 -- .../sentry_sdk/traces.py | 843 ---- .../sentry_sdk/tracing.py | 1475 ------- .../sentry_sdk/tracing_utils.py | 1694 -------- .../sentry_sdk/transport.py | 1255 ------ .../sentry_sdk/types.py | 52 - .../sentry_sdk/utils.py | 2178 ---------- .../sentry_sdk/worker.py | 308 -- .../urllib3-2.7.0.dist-info/INSTALLER | 1 - .../urllib3-2.7.0.dist-info/METADATA | 163 - .../urllib3-2.7.0.dist-info/RECORD | 44 - .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 - .../licenses/LICENSE.txt | 21 - .../urllib3/__init__.py | 211 - .../urllib3/_base_connection.py | 167 - .../urllib3/_collections.py | 486 --- .../urllib3/_request_methods.py | 278 -- .../urllib3/_version.py | 24 - .../urllib3/connection.py | 1099 ----- .../urllib3/connectionpool.py | 1191 ----- .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 - .../urllib3/contrib/emscripten/connection.py | 260 -- .../emscripten/emscripten_fetch_worker.js | 110 - .../urllib3/contrib/emscripten/fetch.py | 726 ---- .../urllib3/contrib/emscripten/request.py | 22 - .../urllib3/contrib/emscripten/response.py | 281 -- .../urllib3/contrib/pyopenssl.py | 563 --- .../urllib3/contrib/socks.py | 228 - .../urllib3/exceptions.py | 335 -- .../urllib3/fields.py | 341 -- .../urllib3/filepost.py | 89 - .../urllib3/http2/__init__.py | 53 - .../urllib3/http2/connection.py | 356 -- .../urllib3/http2/probe.py | 87 - .../urllib3/poolmanager.py | 653 --- .../urllib3/py.typed | 2 - .../urllib3/response.py | 1493 ------- .../urllib3/util/__init__.py | 42 - .../urllib3/util/connection.py | 137 - .../urllib3/util/proxy.py | 43 - .../urllib3/util/request.py | 263 -- .../urllib3/util/response.py | 101 - .../urllib3/util/retry.py | 557 --- .../urllib3/util/ssl_.py | 477 -- .../urllib3/util/ssl_match_hostname.py | 153 - .../urllib3/util/ssltransport.py | 271 -- .../urllib3/util/timeout.py | 275 -- .../urllib3/util/url.py | 469 -- .../urllib3/util/util.py | 42 - .../urllib3/util/wait.py | 124 - .../BasicOkDataCollectionOff/.gitignore | 11 + .../certifi-2026.6.17.dist-info/INSTALLER | 1 - .../certifi-2026.6.17.dist-info/METADATA | 78 - .../certifi-2026.6.17.dist-info/RECORD | 12 - .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 - .../licenses/LICENSE | 20 - .../certifi-2026.6.17.dist-info/top_level.txt | 1 - .../certifi/__init__.py | 4 - .../certifi/__main__.py | 12 - .../certifi/cacert.pem | 3863 ----------------- .../BasicOkDataCollectionOff/certifi/core.py | 83 - .../BasicOkDataCollectionOff/certifi/py.typed | 0 .../sentry_sdk/__init__.py | 70 - .../sentry_sdk/_batcher.py | 184 - .../sentry_sdk/_compat.py | 92 - .../sentry_sdk/_init_implementation.py | 78 - .../sentry_sdk/_log_batcher.py | 66 - .../sentry_sdk/_lru_cache.py | 43 - .../sentry_sdk/_metrics_batcher.py | 48 - .../sentry_sdk/_queue.py | 287 -- .../sentry_sdk/_span_batcher.py | 234 - .../sentry_sdk/_types.py | 481 -- .../sentry_sdk/_werkzeug.py | 100 - .../sentry_sdk/ai/__init__.py | 8 - .../sentry_sdk/ai/_openai_completions_api.py | 66 - .../sentry_sdk/ai/_openai_responses_api.py | 22 - .../sentry_sdk/ai/consts.py | 6 - .../sentry_sdk/ai/monitoring.py | 147 - .../sentry_sdk/ai/utils.py | 782 ---- .../sentry_sdk/api.py | 573 --- .../sentry_sdk/attachments.py | 71 - .../sentry_sdk/client.py | 1479 ------- .../sentry_sdk/consts.py | 1802 -------- .../sentry_sdk/crons/__init__.py | 9 - .../sentry_sdk/crons/api.py | 60 - .../sentry_sdk/crons/consts.py | 4 - .../sentry_sdk/crons/decorator.py | 137 - .../sentry_sdk/data_collection.py | 319 -- .../sentry_sdk/debug.py | 37 - .../sentry_sdk/envelope.py | 332 -- .../sentry_sdk/feature_flags.py | 75 - .../sentry_sdk/hub.py | 741 ---- .../sentry_sdk/integrations/__init__.py | 352 -- .../sentry_sdk/integrations/_asgi_common.py | 187 - .../sentry_sdk/integrations/_wsgi_common.py | 282 -- .../sentry_sdk/integrations/aiohttp.py | 509 --- .../sentry_sdk/integrations/aiomysql.py | 275 -- .../sentry_sdk/integrations/anthropic.py | 1154 ----- .../sentry_sdk/integrations/argv.py | 28 - .../sentry_sdk/integrations/ariadne.py | 167 - .../sentry_sdk/integrations/arq.py | 277 -- .../sentry_sdk/integrations/asgi.py | 543 --- .../sentry_sdk/integrations/asyncio.py | 280 -- .../sentry_sdk/integrations/asyncpg.py | 312 -- .../sentry_sdk/integrations/atexit.py | 51 - .../sentry_sdk/integrations/aws_lambda.py | 542 --- .../sentry_sdk/integrations/beam.py | 164 - .../sentry_sdk/integrations/boto3.py | 187 - .../sentry_sdk/integrations/bottle.py | 239 - .../integrations/celery/__init__.py | 612 --- .../sentry_sdk/integrations/celery/beat.py | 291 -- .../sentry_sdk/integrations/celery/utils.py | 32 - .../sentry_sdk/integrations/chalice.py | 188 - .../integrations/clickhouse_driver.py | 211 - .../integrations/cloud_resource_context.py | 273 -- .../sentry_sdk/integrations/cohere.py | 303 -- .../sentry_sdk/integrations/dedupe.py | 62 - .../integrations/django/__init__.py | 884 ---- .../sentry_sdk/integrations/django/asgi.py | 262 -- .../sentry_sdk/integrations/django/caching.py | 264 -- .../integrations/django/middleware.py | 219 - .../integrations/django/signals_handlers.py | 105 - .../sentry_sdk/integrations/django/tasks.py | 52 - .../integrations/django/templates.py | 209 - .../integrations/django/transactions.py | 154 - .../sentry_sdk/integrations/django/views.py | 127 - .../sentry_sdk/integrations/dramatiq.py | 246 -- .../sentry_sdk/integrations/excepthook.py | 76 - .../sentry_sdk/integrations/executing.py | 66 - .../sentry_sdk/integrations/falcon.py | 278 -- .../sentry_sdk/integrations/fastapi.py | 201 - .../sentry_sdk/integrations/flask.py | 281 -- .../sentry_sdk/integrations/gcp.py | 286 -- .../sentry_sdk/integrations/gnu_backtrace.py | 96 - .../integrations/google_genai/__init__.py | 457 -- .../integrations/google_genai/consts.py | 16 - .../integrations/google_genai/streaming.py | 172 - .../integrations/google_genai/utils.py | 1118 ----- .../sentry_sdk/integrations/gql.py | 173 - .../sentry_sdk/integrations/graphene.py | 181 - .../sentry_sdk/integrations/grpc/__init__.py | 188 - .../integrations/grpc/aio/__init__.py | 7 - .../integrations/grpc/aio/client.py | 146 - .../integrations/grpc/aio/server.py | 134 - .../sentry_sdk/integrations/grpc/client.py | 149 - .../sentry_sdk/integrations/grpc/consts.py | 1 - .../sentry_sdk/integrations/grpc/server.py | 91 - .../sentry_sdk/integrations/httpx.py | 263 -- .../sentry_sdk/integrations/httpx2.py | 263 -- .../sentry_sdk/integrations/huey.py | 239 - .../integrations/huggingface_hub.py | 392 -- .../sentry_sdk/integrations/langchain.py | 1412 ------ .../sentry_sdk/integrations/langgraph.py | 515 --- .../sentry_sdk/integrations/launchdarkly.py | 63 - .../sentry_sdk/integrations/litellm.py | 376 -- .../sentry_sdk/integrations/litestar.py | 364 -- .../sentry_sdk/integrations/logging.py | 476 -- .../sentry_sdk/integrations/loguru.py | 208 - .../sentry_sdk/integrations/mcp.py | 876 ---- .../sentry_sdk/integrations/modules.py | 28 - .../sentry_sdk/integrations/openai.py | 1530 ------- .../integrations/openai_agents/__init__.py | 250 -- .../integrations/openai_agents/consts.py | 1 - .../openai_agents/patches/__init__.py | 10 - .../openai_agents/patches/agent_run.py | 332 -- .../openai_agents/patches/error_tracing.py | 74 - .../openai_agents/patches/models.py | 197 - .../openai_agents/patches/runner.py | 221 - .../openai_agents/patches/tools.py | 70 - .../openai_agents/spans/__init__.py | 8 - .../openai_agents/spans/agent_workflow.py | 32 - .../openai_agents/spans/ai_client.py | 84 - .../openai_agents/spans/execute_tool.py | 82 - .../openai_agents/spans/handoff.py | 41 - .../openai_agents/spans/invoke_agent.py | 122 - .../integrations/openai_agents/utils.py | 244 -- .../sentry_sdk/integrations/openfeature.py | 34 - .../integrations/opentelemetry/__init__.py | 7 - .../integrations/opentelemetry/consts.py | 9 - .../integrations/opentelemetry/integration.py | 81 - .../integrations/opentelemetry/propagator.py | 128 - .../opentelemetry/span_processor.py | 407 -- .../sentry_sdk/integrations/otlp.py | 223 - .../sentry_sdk/integrations/pure_eval.py | 134 - .../integrations/pydantic_ai/__init__.py | 187 - .../integrations/pydantic_ai/consts.py | 1 - .../pydantic_ai/patches/__init__.py | 3 - .../pydantic_ai/patches/agent_run.py | 198 - .../pydantic_ai/patches/graph_nodes.py | 106 - .../integrations/pydantic_ai/patches/tools.py | 177 - .../pydantic_ai/spans/__init__.py | 3 - .../pydantic_ai/spans/ai_client.py | 331 -- .../pydantic_ai/spans/execute_tool.py | 84 - .../pydantic_ai/spans/invoke_agent.py | 184 - .../integrations/pydantic_ai/spans/utils.py | 87 - .../integrations/pydantic_ai/utils.py | 233 - .../sentry_sdk/integrations/pymongo.py | 262 -- .../sentry_sdk/integrations/pyramid.py | 239 - .../sentry_sdk/integrations/pyreqwest.py | 197 - .../sentry_sdk/integrations/quart.py | 292 -- .../sentry_sdk/integrations/ray.py | 240 - .../sentry_sdk/integrations/redis/__init__.py | 49 - .../integrations/redis/_async_common.py | 177 - .../integrations/redis/_sync_common.py | 176 - .../sentry_sdk/integrations/redis/consts.py | 19 - .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 - .../integrations/redis/modules/queries.py | 88 - .../sentry_sdk/integrations/redis/rb.py | 31 - .../sentry_sdk/integrations/redis/redis.py | 67 - .../integrations/redis/redis_cluster.py | 115 - .../redis/redis_py_cluster_legacy.py | 49 - .../sentry_sdk/integrations/redis/utils.py | 159 - .../sentry_sdk/integrations/rq.py | 225 - .../sentry_sdk/integrations/rust_tracing.py | 300 -- .../sentry_sdk/integrations/sanic.py | 431 -- .../sentry_sdk/integrations/serverless.py | 65 - .../sentry_sdk/integrations/socket.py | 142 - .../sentry_sdk/integrations/spark/__init__.py | 4 - .../integrations/spark/spark_driver.py | 278 -- .../integrations/spark/spark_worker.py | 109 - .../sentry_sdk/integrations/sqlalchemy.py | 185 - .../sentry_sdk/integrations/starlette.py | 858 ---- .../sentry_sdk/integrations/starlite.py | 314 -- .../sentry_sdk/integrations/statsig.py | 37 - .../sentry_sdk/integrations/stdlib.py | 404 -- .../sentry_sdk/integrations/strawberry.py | 493 --- .../sentry_sdk/integrations/sys_exit.py | 65 - .../sentry_sdk/integrations/threading.py | 196 - .../sentry_sdk/integrations/tornado.py | 320 -- .../sentry_sdk/integrations/trytond.py | 52 - .../sentry_sdk/integrations/typer.py | 58 - .../sentry_sdk/integrations/unleash.py | 33 - .../sentry_sdk/integrations/unraisablehook.py | 50 - .../sentry_sdk/integrations/wsgi.py | 427 -- .../sentry_sdk/logger.py | 88 - .../sentry_sdk/metrics.py | 62 - .../sentry_sdk/monitor.py | 138 - .../sentry_sdk/profiler/__init__.py | 49 - .../profiler/continuous_profiler.py | 703 --- .../profiler/transaction_profiler.py | 802 ---- .../sentry_sdk/profiler/utils.py | 186 - .../sentry_sdk/py.typed | 0 .../sentry_sdk/scope.py | 2185 ---------- .../sentry_sdk/scrubber.py | 176 - .../sentry_sdk/serializer.py | 418 -- .../sentry_sdk/session.py | 165 - .../sentry_sdk/sessions.py | 262 -- .../sentry_sdk/spotlight.py | 327 -- .../sentry_sdk/traces.py | 843 ---- .../sentry_sdk/tracing.py | 1475 ------- .../sentry_sdk/tracing_utils.py | 1694 -------- .../sentry_sdk/transport.py | 1255 ------ .../sentry_sdk/types.py | 52 - .../sentry_sdk/utils.py | 2178 ---------- .../sentry_sdk/worker.py | 308 -- .../urllib3-2.7.0.dist-info/INSTALLER | 1 - .../urllib3-2.7.0.dist-info/METADATA | 163 - .../urllib3-2.7.0.dist-info/RECORD | 44 - .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 - .../licenses/LICENSE.txt | 21 - .../urllib3/__init__.py | 211 - .../urllib3/_base_connection.py | 167 - .../urllib3/_collections.py | 486 --- .../urllib3/_request_methods.py | 278 -- .../urllib3/_version.py | 24 - .../urllib3/connection.py | 1099 ----- .../urllib3/connectionpool.py | 1191 ----- .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 - .../urllib3/contrib/emscripten/connection.py | 260 -- .../emscripten/emscripten_fetch_worker.js | 110 - .../urllib3/contrib/emscripten/fetch.py | 726 ---- .../urllib3/contrib/emscripten/request.py | 22 - .../urllib3/contrib/emscripten/response.py | 281 -- .../urllib3/contrib/pyopenssl.py | 563 --- .../urllib3/contrib/socks.py | 228 - .../urllib3/exceptions.py | 335 -- .../urllib3/fields.py | 341 -- .../urllib3/filepost.py | 89 - .../urllib3/http2/__init__.py | 53 - .../urllib3/http2/connection.py | 356 -- .../urllib3/http2/probe.py | 87 - .../urllib3/poolmanager.py | 653 --- .../BasicOkDataCollectionOff/urllib3/py.typed | 2 - .../urllib3/response.py | 1493 ------- .../urllib3/util/__init__.py | 42 - .../urllib3/util/connection.py | 137 - .../urllib3/util/proxy.py | 43 - .../urllib3/util/request.py | 263 -- .../urllib3/util/response.py | 101 - .../urllib3/util/retry.py | 557 --- .../urllib3/util/ssl_.py | 477 -- .../urllib3/util/ssl_match_hostname.py | 153 - .../urllib3/util/ssltransport.py | 271 -- .../urllib3/util/timeout.py | 275 -- .../urllib3/util/url.py | 469 -- .../urllib3/util/util.py | 42 - .../urllib3/util/wait.py | 124 - .../BasicOkSendDefaultPii/.gitignore | 11 + .../certifi-2026.6.17.dist-info/INSTALLER | 1 - .../certifi-2026.6.17.dist-info/METADATA | 78 - .../certifi-2026.6.17.dist-info/RECORD | 12 - .../certifi-2026.6.17.dist-info/REQUESTED | 0 .../certifi-2026.6.17.dist-info/WHEEL | 5 - .../licenses/LICENSE | 20 - .../certifi-2026.6.17.dist-info/top_level.txt | 1 - .../BasicOkSendDefaultPii/certifi/__init__.py | 4 - .../BasicOkSendDefaultPii/certifi/__main__.py | 12 - .../BasicOkSendDefaultPii/certifi/cacert.pem | 3863 ----------------- .../BasicOkSendDefaultPii/certifi/core.py | 83 - .../BasicOkSendDefaultPii/certifi/py.typed | 0 .../sentry_sdk/__init__.py | 70 - .../sentry_sdk/_batcher.py | 184 - .../sentry_sdk/_compat.py | 92 - .../sentry_sdk/_init_implementation.py | 78 - .../sentry_sdk/_log_batcher.py | 66 - .../sentry_sdk/_lru_cache.py | 43 - .../sentry_sdk/_metrics_batcher.py | 48 - .../sentry_sdk/_queue.py | 287 -- .../sentry_sdk/_span_batcher.py | 234 - .../sentry_sdk/_types.py | 481 -- .../sentry_sdk/_werkzeug.py | 100 - .../sentry_sdk/ai/__init__.py | 8 - .../sentry_sdk/ai/_openai_completions_api.py | 66 - .../sentry_sdk/ai/_openai_responses_api.py | 22 - .../sentry_sdk/ai/consts.py | 6 - .../sentry_sdk/ai/monitoring.py | 147 - .../sentry_sdk/ai/utils.py | 782 ---- .../BasicOkSendDefaultPii/sentry_sdk/api.py | 573 --- .../sentry_sdk/attachments.py | 71 - .../sentry_sdk/client.py | 1479 ------- .../sentry_sdk/consts.py | 1802 -------- .../sentry_sdk/crons/__init__.py | 9 - .../sentry_sdk/crons/api.py | 60 - .../sentry_sdk/crons/consts.py | 4 - .../sentry_sdk/crons/decorator.py | 137 - .../sentry_sdk/data_collection.py | 319 -- .../BasicOkSendDefaultPii/sentry_sdk/debug.py | 37 - .../sentry_sdk/envelope.py | 332 -- .../sentry_sdk/feature_flags.py | 75 - .../BasicOkSendDefaultPii/sentry_sdk/hub.py | 741 ---- .../sentry_sdk/integrations/__init__.py | 352 -- .../sentry_sdk/integrations/_asgi_common.py | 187 - .../sentry_sdk/integrations/_wsgi_common.py | 282 -- .../sentry_sdk/integrations/aiohttp.py | 509 --- .../sentry_sdk/integrations/aiomysql.py | 275 -- .../sentry_sdk/integrations/anthropic.py | 1154 ----- .../sentry_sdk/integrations/argv.py | 28 - .../sentry_sdk/integrations/ariadne.py | 167 - .../sentry_sdk/integrations/arq.py | 277 -- .../sentry_sdk/integrations/asgi.py | 543 --- .../sentry_sdk/integrations/asyncio.py | 280 -- .../sentry_sdk/integrations/asyncpg.py | 312 -- .../sentry_sdk/integrations/atexit.py | 51 - .../sentry_sdk/integrations/aws_lambda.py | 542 --- .../sentry_sdk/integrations/beam.py | 164 - .../sentry_sdk/integrations/boto3.py | 187 - .../sentry_sdk/integrations/bottle.py | 239 - .../integrations/celery/__init__.py | 612 --- .../sentry_sdk/integrations/celery/beat.py | 291 -- .../sentry_sdk/integrations/celery/utils.py | 32 - .../sentry_sdk/integrations/chalice.py | 188 - .../integrations/clickhouse_driver.py | 211 - .../integrations/cloud_resource_context.py | 273 -- .../sentry_sdk/integrations/cohere.py | 303 -- .../sentry_sdk/integrations/dedupe.py | 62 - .../integrations/django/__init__.py | 884 ---- .../sentry_sdk/integrations/django/asgi.py | 262 -- .../sentry_sdk/integrations/django/caching.py | 264 -- .../integrations/django/middleware.py | 219 - .../integrations/django/signals_handlers.py | 105 - .../sentry_sdk/integrations/django/tasks.py | 52 - .../integrations/django/templates.py | 209 - .../integrations/django/transactions.py | 154 - .../sentry_sdk/integrations/django/views.py | 127 - .../sentry_sdk/integrations/dramatiq.py | 246 -- .../sentry_sdk/integrations/excepthook.py | 76 - .../sentry_sdk/integrations/executing.py | 66 - .../sentry_sdk/integrations/falcon.py | 278 -- .../sentry_sdk/integrations/fastapi.py | 201 - .../sentry_sdk/integrations/flask.py | 281 -- .../sentry_sdk/integrations/gcp.py | 286 -- .../sentry_sdk/integrations/gnu_backtrace.py | 96 - .../integrations/google_genai/__init__.py | 457 -- .../integrations/google_genai/consts.py | 16 - .../integrations/google_genai/streaming.py | 172 - .../integrations/google_genai/utils.py | 1118 ----- .../sentry_sdk/integrations/gql.py | 173 - .../sentry_sdk/integrations/graphene.py | 181 - .../sentry_sdk/integrations/grpc/__init__.py | 188 - .../integrations/grpc/aio/__init__.py | 7 - .../integrations/grpc/aio/client.py | 146 - .../integrations/grpc/aio/server.py | 134 - .../sentry_sdk/integrations/grpc/client.py | 149 - .../sentry_sdk/integrations/grpc/consts.py | 1 - .../sentry_sdk/integrations/grpc/server.py | 91 - .../sentry_sdk/integrations/httpx.py | 263 -- .../sentry_sdk/integrations/httpx2.py | 263 -- .../sentry_sdk/integrations/huey.py | 239 - .../integrations/huggingface_hub.py | 392 -- .../sentry_sdk/integrations/langchain.py | 1412 ------ .../sentry_sdk/integrations/langgraph.py | 515 --- .../sentry_sdk/integrations/launchdarkly.py | 63 - .../sentry_sdk/integrations/litellm.py | 376 -- .../sentry_sdk/integrations/litestar.py | 364 -- .../sentry_sdk/integrations/logging.py | 476 -- .../sentry_sdk/integrations/loguru.py | 208 - .../sentry_sdk/integrations/mcp.py | 876 ---- .../sentry_sdk/integrations/modules.py | 28 - .../sentry_sdk/integrations/openai.py | 1530 ------- .../integrations/openai_agents/__init__.py | 250 -- .../integrations/openai_agents/consts.py | 1 - .../openai_agents/patches/__init__.py | 10 - .../openai_agents/patches/agent_run.py | 332 -- .../openai_agents/patches/error_tracing.py | 74 - .../openai_agents/patches/models.py | 197 - .../openai_agents/patches/runner.py | 221 - .../openai_agents/patches/tools.py | 70 - .../openai_agents/spans/__init__.py | 8 - .../openai_agents/spans/agent_workflow.py | 32 - .../openai_agents/spans/ai_client.py | 84 - .../openai_agents/spans/execute_tool.py | 82 - .../openai_agents/spans/handoff.py | 41 - .../openai_agents/spans/invoke_agent.py | 122 - .../integrations/openai_agents/utils.py | 244 -- .../sentry_sdk/integrations/openfeature.py | 34 - .../integrations/opentelemetry/__init__.py | 7 - .../integrations/opentelemetry/consts.py | 9 - .../integrations/opentelemetry/integration.py | 81 - .../integrations/opentelemetry/propagator.py | 128 - .../opentelemetry/span_processor.py | 407 -- .../sentry_sdk/integrations/otlp.py | 223 - .../sentry_sdk/integrations/pure_eval.py | 134 - .../integrations/pydantic_ai/__init__.py | 187 - .../integrations/pydantic_ai/consts.py | 1 - .../pydantic_ai/patches/__init__.py | 3 - .../pydantic_ai/patches/agent_run.py | 198 - .../pydantic_ai/patches/graph_nodes.py | 106 - .../integrations/pydantic_ai/patches/tools.py | 177 - .../pydantic_ai/spans/__init__.py | 3 - .../pydantic_ai/spans/ai_client.py | 331 -- .../pydantic_ai/spans/execute_tool.py | 84 - .../pydantic_ai/spans/invoke_agent.py | 184 - .../integrations/pydantic_ai/spans/utils.py | 87 - .../integrations/pydantic_ai/utils.py | 233 - .../sentry_sdk/integrations/pymongo.py | 262 -- .../sentry_sdk/integrations/pyramid.py | 239 - .../sentry_sdk/integrations/pyreqwest.py | 197 - .../sentry_sdk/integrations/quart.py | 292 -- .../sentry_sdk/integrations/ray.py | 240 - .../sentry_sdk/integrations/redis/__init__.py | 49 - .../integrations/redis/_async_common.py | 177 - .../integrations/redis/_sync_common.py | 176 - .../sentry_sdk/integrations/redis/consts.py | 19 - .../integrations/redis/modules/__init__.py | 0 .../integrations/redis/modules/caches.py | 136 - .../integrations/redis/modules/queries.py | 88 - .../sentry_sdk/integrations/redis/rb.py | 31 - .../sentry_sdk/integrations/redis/redis.py | 67 - .../integrations/redis/redis_cluster.py | 115 - .../redis/redis_py_cluster_legacy.py | 49 - .../sentry_sdk/integrations/redis/utils.py | 159 - .../sentry_sdk/integrations/rq.py | 225 - .../sentry_sdk/integrations/rust_tracing.py | 300 -- .../sentry_sdk/integrations/sanic.py | 431 -- .../sentry_sdk/integrations/serverless.py | 65 - .../sentry_sdk/integrations/socket.py | 142 - .../sentry_sdk/integrations/spark/__init__.py | 4 - .../integrations/spark/spark_driver.py | 278 -- .../integrations/spark/spark_worker.py | 109 - .../sentry_sdk/integrations/sqlalchemy.py | 185 - .../sentry_sdk/integrations/starlette.py | 858 ---- .../sentry_sdk/integrations/starlite.py | 314 -- .../sentry_sdk/integrations/statsig.py | 37 - .../sentry_sdk/integrations/stdlib.py | 404 -- .../sentry_sdk/integrations/strawberry.py | 493 --- .../sentry_sdk/integrations/sys_exit.py | 65 - .../sentry_sdk/integrations/threading.py | 196 - .../sentry_sdk/integrations/tornado.py | 320 -- .../sentry_sdk/integrations/trytond.py | 52 - .../sentry_sdk/integrations/typer.py | 58 - .../sentry_sdk/integrations/unleash.py | 33 - .../sentry_sdk/integrations/unraisablehook.py | 50 - .../sentry_sdk/integrations/wsgi.py | 427 -- .../sentry_sdk/logger.py | 88 - .../sentry_sdk/metrics.py | 62 - .../sentry_sdk/monitor.py | 138 - .../sentry_sdk/profiler/__init__.py | 49 - .../profiler/continuous_profiler.py | 703 --- .../profiler/transaction_profiler.py | 802 ---- .../sentry_sdk/profiler/utils.py | 186 - .../BasicOkSendDefaultPii/sentry_sdk/py.typed | 0 .../BasicOkSendDefaultPii/sentry_sdk/scope.py | 2185 ---------- .../sentry_sdk/scrubber.py | 176 - .../sentry_sdk/serializer.py | 418 -- .../sentry_sdk/session.py | 165 - .../sentry_sdk/sessions.py | 262 -- .../sentry_sdk/spotlight.py | 327 -- .../sentry_sdk/traces.py | 843 ---- .../sentry_sdk/tracing.py | 1475 ------- .../sentry_sdk/tracing_utils.py | 1694 -------- .../sentry_sdk/transport.py | 1255 ------ .../BasicOkSendDefaultPii/sentry_sdk/types.py | 52 - .../BasicOkSendDefaultPii/sentry_sdk/utils.py | 2178 ---------- .../sentry_sdk/worker.py | 308 -- .../urllib3-2.7.0.dist-info/INSTALLER | 1 - .../urllib3-2.7.0.dist-info/METADATA | 163 - .../urllib3-2.7.0.dist-info/RECORD | 44 - .../urllib3-2.7.0.dist-info/REQUESTED | 0 .../urllib3-2.7.0.dist-info/WHEEL | 4 - .../licenses/LICENSE.txt | 21 - .../BasicOkSendDefaultPii/urllib3/__init__.py | 211 - .../urllib3/_base_connection.py | 167 - .../urllib3/_collections.py | 486 --- .../urllib3/_request_methods.py | 278 -- .../BasicOkSendDefaultPii/urllib3/_version.py | 24 - .../urllib3/connection.py | 1099 ----- .../urllib3/connectionpool.py | 1191 ----- .../urllib3/contrib/__init__.py | 0 .../urllib3/contrib/emscripten/__init__.py | 17 - .../urllib3/contrib/emscripten/connection.py | 260 -- .../emscripten/emscripten_fetch_worker.js | 110 - .../urllib3/contrib/emscripten/fetch.py | 726 ---- .../urllib3/contrib/emscripten/request.py | 22 - .../urllib3/contrib/emscripten/response.py | 281 -- .../urllib3/contrib/pyopenssl.py | 563 --- .../urllib3/contrib/socks.py | 228 - .../urllib3/exceptions.py | 335 -- .../BasicOkSendDefaultPii/urllib3/fields.py | 341 -- .../BasicOkSendDefaultPii/urllib3/filepost.py | 89 - .../urllib3/http2/__init__.py | 53 - .../urllib3/http2/connection.py | 356 -- .../urllib3/http2/probe.py | 87 - .../urllib3/poolmanager.py | 653 --- .../BasicOkSendDefaultPii/urllib3/py.typed | 2 - .../BasicOkSendDefaultPii/urllib3/response.py | 1493 ------- .../urllib3/util/__init__.py | 42 - .../urllib3/util/connection.py | 137 - .../urllib3/util/proxy.py | 43 - .../urllib3/util/request.py | 263 -- .../urllib3/util/response.py | 101 - .../urllib3/util/retry.py | 557 --- .../urllib3/util/ssl_.py | 477 -- .../urllib3/util/ssl_match_hostname.py | 153 - .../urllib3/util/ssltransport.py | 271 -- .../urllib3/util/timeout.py | 275 -- .../BasicOkSendDefaultPii/urllib3/util/url.py | 469 -- .../urllib3/util/util.py | 42 - .../urllib3/util/wait.py | 124 - 1004 files changed, 44 insertions(+), 283796 deletions(-) create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.gitignore delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.gitignore delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.gitignore delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.gitignore delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/REQUESTED delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py delete mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA deleted file mode 100644 index 0d4f101491..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/METADATA +++ /dev/null @@ -1,78 +0,0 @@ -Metadata-Version: 2.4 -Name: certifi -Version: 2026.6.17 -Summary: Python package for providing Mozilla's CA Bundle. -Home-page: https://github.com/certifi/python-certifi -Author: Kenneth Reitz -Author-email: me@kennethreitz.com -License: MPL-2.0 -Project-URL: Source, https://github.com/certifi/python-certifi -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) -Classifier: Natural Language :: English -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Requires-Python: >=3.7 -License-File: LICENSE -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: home-page -Dynamic: license -Dynamic: license-file -Dynamic: project-url -Dynamic: requires-python -Dynamic: summary - -Certifi: Python SSL Certificates -================================ - -Certifi provides Mozilla's carefully curated collection of Root Certificates for -validating the trustworthiness of SSL certificates while verifying the identity -of TLS hosts. It has been extracted from the `Requests`_ project. - -Installation ------------- - -``certifi`` is available on PyPI. Simply install it with ``pip``:: - - $ pip install certifi - -Usage ------ - -To reference the installed certificate authority (CA) bundle, you can use the -built-in function:: - - >>> import certifi - - >>> certifi.where() - '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' - -Or from the command line:: - - $ python -m certifi - /usr/local/lib/python3.7/site-packages/certifi/cacert.pem - -Enjoy! - -.. _`Requests`: https://requests.readthedocs.io/en/latest/ - -Addition/Removal of Certificates --------------------------------- - -Certifi does not support any addition/removal or other modification of the -CA trust store content. This project is intended to provide a reliable and -highly portable root of trust to python deployments. Look to upstream projects -for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD deleted file mode 100644 index 13b0ce7da4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/RECORD +++ /dev/null @@ -1,12 +0,0 @@ -certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 -certifi-2026.6.17.dist-info/RECORD,, -certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 -certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 -certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 -certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 -certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 -certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 -certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 -certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL deleted file mode 100644 index 14a883f292..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (82.0.1) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE deleted file mode 100644 index 62b076cdee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/licenses/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -This package contains a modified version of ca-bundle.crt: - -ca-bundle.crt -- Bundle of CA Root Certificates - -This is a bundle of X.509 certificates of public Certificate Authorities -(CA). These were automatically extracted from Mozilla's root certificates -file (certdata.txt). This file can be found in the mozilla source tree: -https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt -It contains the certificates in PEM format and therefore -can be directly used with curl / libcurl / php_curl, or with -an Apache+mod_ssl webserver for SSL client authentication. -Just configure this file as the SSLCACertificateFile.# - -***** BEGIN LICENSE BLOCK ***** -This Source Code Form is subject to the terms of the Mozilla Public License, -v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain -one at http://mozilla.org/MPL/2.0/. - -***** END LICENSE BLOCK ***** -@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt deleted file mode 100644 index 963eac530b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi-2026.6.17.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py deleted file mode 100644 index ed9a74b7a0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .core import contents, where - -__all__ = ["contents", "where"] -__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py deleted file mode 100644 index 8945b5da85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/__main__.py +++ /dev/null @@ -1,12 +0,0 @@ -import argparse - -from certifi import contents, where - -parser = argparse.ArgumentParser() -parser.add_argument("-c", "--contents", action="store_true") -args = parser.parse_args() - -if args.contents: - print(contents()) -else: - print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem deleted file mode 100644 index 1c2dbfeb68..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/cacert.pem +++ /dev/null @@ -1,3863 +0,0 @@ - -# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Label: "COMODO ECC Certification Authority" -# Serial: 41578283867086692638256921589707938090 -# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 -# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 -# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT -IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw -MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N -T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR -FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J -cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW -BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm -fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv -GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" -# Serial: 80544274841616 -# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 -# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 -# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG -EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 -MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl -cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR -dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB -pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM -b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm -aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz -IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT -lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz -AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 -VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG -ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 -BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG -AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M -U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh -bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C -+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F -uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 -XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Label: "Microsec e-Szigno Root CA 2009" -# Serial: 14014712776195784473 -# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 -# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e -# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G -CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y -OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx -FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp -Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP -kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc -cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U -fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 -N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC -xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 -+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM -Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG -SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h -mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk -ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c -2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t -HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Label: "GlobalSign Root CA - R3" -# Serial: 4835703278459759426209954 -# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 -# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad -# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 -MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 -RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT -gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm -KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd -QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ -XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o -LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU -RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp -jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK -6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX -mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs -Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH -WD9f ------END CERTIFICATE----- - -# Issuer: CN=Izenpe.com O=IZENPE S.A. -# Subject: CN=Izenpe.com O=IZENPE S.A. -# Label: "Izenpe.com" -# Serial: 917563065490389241595536686991402621 -# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 -# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 -# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f ------BEGIN CERTIFICATE----- -MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 -MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 -ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD -VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j -b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq -scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO -xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H -LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX -uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD -yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ -JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q -rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN -BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L -hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB -QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ -HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu -Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg -QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB -BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx -MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA -A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb -laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 -awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo -JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw -LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT -VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk -LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb -UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ -QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ -naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls -QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE----- - -# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Label: "Go Daddy Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 -# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b -# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT -EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp -ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz -NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH -EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE -AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD -E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH -/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy -DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh -GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR -tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA -AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX -WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu -9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr -gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo -2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI -4uJEvlz36hz1 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 -# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e -# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Services Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 -# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f -# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs -ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD -VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy -ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy -dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p -OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 -8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K -Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe -hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk -6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q -AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI -bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB -ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z -qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn -0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN -sSi6 ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA" -# Serial: 279744 -# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 -# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e -# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM -MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D -ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU -cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 -WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg -Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw -IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH -UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM -TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU -BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM -kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x -AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV -HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y -sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL -I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 -J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY -VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Label: "TWCA Root Certification Authority" -# Serial: 1 -# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 -# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 -# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES -MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU -V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz -WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO -LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE -AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH -K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX -RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z -rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx -3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq -hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC -MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls -XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D -lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn -aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ -YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Label: "Security Communication RootCA2" -# Serial: 0 -# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 -# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 -# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl -MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe -U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX -DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy -dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj -YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV -OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr -zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM -VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ -hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO -ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw -awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs -OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF -coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc -okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 -t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy -1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ -SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Label: "Actalis Authentication Root CA" -# Serial: 6271844772424770508 -# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 -# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac -# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE -BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w -MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC -SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 -ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv -UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX -4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 -KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ -gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb -rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ -51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F -be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe -KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F -v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn -fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 -jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz -ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL -e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 -jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz -WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V -SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j -pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX -X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok -fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R -K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU -ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU -LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT -LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 2 Root CA" -# Serial: 2 -# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 -# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 -# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr -6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV -L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 -1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx -MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ -QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB -arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr -Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi -FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS -P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN -9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz -uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h -9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t -OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo -+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 -KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 -DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us -H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ -I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 -5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h -3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz -Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 3 Root CA" -# Serial: 2 -# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec -# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 -# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y -ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E -N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 -tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX -0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c -/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X -KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY -zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS -O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D -34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP -K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv -Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj -QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS -IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 -HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa -O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv -033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u -dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE -kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 -3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD -u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq -4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 3" -# Serial: 1 -# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef -# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 -# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN -8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ -RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 -hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 -ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM -EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 -A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy -WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ -1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 -6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT -91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p -TpPDpFQUWw== ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 2009" -# Serial: 623603 -# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f -# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 -# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 ------BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha -ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM -HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 -UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 -tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R -ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM -lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp -/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G -A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy -MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl -cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js -L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL -BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni -acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K -zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 -PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y -Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 EV 2009" -# Serial: 623604 -# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 -# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 -# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw -NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV -BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn -ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 -3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z -qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR -p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 -HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw -ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea -HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw -Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh -c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E -RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt -dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku -Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp -3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF -CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na -xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX -KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE----- - -# Issuer: CN=CA Disig Root R2 O=Disig a.s. -# Subject: CN=CA Disig Root R2 O=Disig a.s. -# Label: "CA Disig Root R2" -# Serial: 10572350602393338211 -# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 -# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 -# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV -BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu -MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy -MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx -EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe -NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH -PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I -x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe -QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR -yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO -QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 -H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ -QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD -i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs -nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 -rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI -hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM -tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf -GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb -lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka -+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal -TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i -nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 -gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr -G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os -zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x -L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE----- - -# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Label: "ACCVRAIZ1" -# Serial: 6828503384748696800 -# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 -# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 -# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 ------BEGIN CERTIFICATE----- -MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE -AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw -CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ -BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND -VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb -qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY -HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo -G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA -lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr -IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ -0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH -k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 -4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO -m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa -cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl -uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI -KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls -ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG -AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 -VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT -VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG -CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA -cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA -QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA -7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA -cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA -QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA -czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu -aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt -aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud -DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF -BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp -D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU -JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m -AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD -vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms -tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH -7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h -I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA -h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF -d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H -pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE----- - -# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA Global Root CA" -# Serial: 3262 -# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 -# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 -# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx -EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT -VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 -NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT -B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF -10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz -0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh -MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH -zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc -46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 -yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi -laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP -oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA -BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE -qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm -4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL -1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF -H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo -RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ -nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh -15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW -6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW -nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j -wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz -aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy -KwbQBM0= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 2" -# Serial: 1 -# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a -# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 -# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd -AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC -FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi -1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq -jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ -wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ -WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy -NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC -uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw -IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 -g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP -BSeOE6Fuwg== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot 2011 O=Atos -# Subject: CN=Atos TrustedRoot 2011 O=Atos -# Label: "Atos TrustedRoot 2011" -# Serial: 6643877497813316402 -# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 -# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 -# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE -AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG -EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM -FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC -REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp -Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM -VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ -SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ -4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L -cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi -eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG -A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 -DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j -vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP -DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc -maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D -lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv -KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 1 G3" -# Serial: 687049649626669250736271037606554624078720034195 -# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab -# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 -# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 -MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV -wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe -rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 -68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh -4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp -UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o -abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc -3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G -KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt -hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO -Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt -zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD -ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC -MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 -cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN -qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 -YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv -b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 -8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k -NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj -ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp -q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt -nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 2 G3" -# Serial: 390156079458959257446133169266079962026824725800 -# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 -# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 -# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 -MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf -qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW -n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym -c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ -O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 -o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j -IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq -IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz -8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh -vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l -7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG -cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD -ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC -roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga -W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n -lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE -+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV -csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd -dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg -KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM -HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 -WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 3 G3" -# Serial: 268090761170461462463995952157327242137089239581 -# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 -# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d -# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 -MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR -/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu -FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR -U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c -ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR -FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k -A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw -eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl -sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp -VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q -A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ -ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD -ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI -FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv -oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg -u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP -0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf -3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl -8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ -DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN -PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ -ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G2" -# Serial: 15385348160840213938643033620894905419 -# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d -# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f -# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 ------BEGIN CERTIFICATE----- -MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA -n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc -biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp -EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA -bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu -YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB -AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW -BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI -QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I -0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni -lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 -B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv -ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo -IhNzbM8m9Yop5w== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G3" -# Serial: 15459312981008553731928384953135426796 -# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb -# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 -# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg -RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf -Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q -RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD -AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY -JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv -6pZjamVFkpUBtA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G2" -# Serial: 4293743540046975378534879503202253541 -# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 -# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 -# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f ------BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH -MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI -2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx -1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ -q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz -tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ -vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV -5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY -1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 -NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG -Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 -8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe -pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G3" -# Serial: 7089244469030293291760083333884364146 -# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca -# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e -# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 ------BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe -Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw -EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x -IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG -fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO -Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx -AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ -oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 -sycX ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Trusted Root G4" -# Serial: 7451500558977370777930084869016614236 -# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 -# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 -# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 ------BEGIN CERTIFICATE----- -MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg -RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y -ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If -xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV -ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO -DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ -jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ -CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi -EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM -fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY -uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK -chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t -9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD -ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 -SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd -+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc -fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa -sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N -cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N -0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie -4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI -r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 -/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm -gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE----- - -# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Label: "COMODO RSA Certification Authority" -# Serial: 101909084537582093308941363524873193117 -# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 -# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 -# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 ------BEGIN CERTIFICATE----- -MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB -hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV -BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT -EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR -6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X -pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC -9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV -/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf -Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z -+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w -qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah -SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC -u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf -Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq -crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E -FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl -wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM -4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV -2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna -FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ -CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK -boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke -jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL -S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb -QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl -0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB -NVOFBkpdn627G190 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Label: "USERTrust RSA Certification Authority" -# Serial: 2645093764781058787591871645665788717 -# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 -# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e -# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 ------BEGIN CERTIFICATE----- -MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB -iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl -cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV -BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw -MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV -BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B -3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY -tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ -Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 -VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT -79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 -c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT -Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l -c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee -UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE -Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd -BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G -A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF -Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO -VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 -ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs -8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR -iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze -Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ -XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ -qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB -VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB -L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG -jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Label: "USERTrust ECC Certification Authority" -# Serial: 123013823720199481456569720443997572134 -# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 -# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 -# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a ------BEGIN CERTIFICATE----- -MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL -MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl -eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT -JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT -Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg -VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo -I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng -o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G -A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB -zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW -RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Label: "GlobalSign ECC Root CA - R5" -# Serial: 32785792099990507226680698011560947931244 -# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 -# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa -# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 ------BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc -8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke -hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI -KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg -515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO -xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Label: "IdenTrust Commercial Root CA 1" -# Serial: 13298821034946342390520003877796839426 -# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 -# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 -# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu -VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw -MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw -JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT -3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU -+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp -S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 -bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi -T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL -vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK -Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK -dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT -c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv -l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N -iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD -ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH -6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt -LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 -nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 -+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK -W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT -AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq -l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG -4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ -mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A -7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Label: "IdenTrust Public Sector Root CA 1" -# Serial: 13298821034946342390521976156843933698 -# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba -# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd -# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu -VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN -MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 -MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 -ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy -RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS -bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF -/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R -3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw -EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy -9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V -GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ -2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV -WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD -W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN -AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj -t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV -DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 -TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G -lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW -mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df -WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 -+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ -tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA -GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv -8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE----- - -# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority -# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority -# Label: "CFCA EV ROOT" -# Serial: 407555286 -# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 -# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 -# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD -TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx -MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j -aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP -T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 -sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL -TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 -/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp -7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz -EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt -hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP -a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot -aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg -TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV -PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv -cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL -tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd -BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT -ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL -jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS -ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy -P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 -xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d -Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN -5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe -/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z -AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ -5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GB CA" -# Serial: 157768595616588414422159278966750757568 -# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d -# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed -# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt -MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg -Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i -YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x -CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG -b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh -bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 -HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx -WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX -1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk -u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P -99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r -M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB -BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh -cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 -gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO -ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf -aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic -Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE----- - -# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Label: "SZAFIR ROOT CA2" -# Serial: 357043034767186914217277344587386743377558296292 -# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 -# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de -# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 -ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw -NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L -cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg -Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN -QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT -3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw -3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 -3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 -BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN -XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF -AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw -8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG -nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP -oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy -d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg -LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA 2" -# Serial: 44979900017204383099463764357512596969 -# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 -# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 -# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 ------BEGIN CERTIFICATE----- -MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB -gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu -QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG -A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz -OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ -VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 -b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA -DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn -0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB -OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE -fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E -Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m -o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i -sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW -OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez -Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS -adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n -3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ -F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf -CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 -XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm -djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ -WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb -AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq -P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko -b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj -XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P -5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi -DrW5viSP ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce -# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 -# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 ------BEGIN CERTIFICATE----- -MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix -DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k -IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT -N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v -dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG -A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh -ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx -QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA -4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 -AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 -4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C -ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV -9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD -gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 -Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq -NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko -LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc -Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd -ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I -XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI -M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot -9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V -Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea -j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh -X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ -l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf -bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 -pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK -e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 -vm9qp/UsQu0yrbYhnr68 ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef -# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 -# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 ------BEGIN CERTIFICATE----- -MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN -BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl -bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv -b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ -BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj -YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 -MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 -dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg -QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa -jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi -C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep -lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof -TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X1 O=Internet Security Research Group -# Subject: CN=ISRG Root X1 O=Internet Security Research Group -# Label: "ISRG Root X1" -# Serial: 172886928669790476064670243504169061120 -# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e -# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 -# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- - -# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Label: "AC RAIZ FNMT-RCM" -# Serial: 485876308206448804701554682760554759 -# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d -# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 -# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx -CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ -WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ -BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG -Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ -yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf -BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz -WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF -tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z -374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC -IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL -mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 -wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS -MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 -ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet -UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H -YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 -LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD -nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 -RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM -LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf -77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N -JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm -fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp -6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp -1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B -9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok -RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv -uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 1 O=Amazon -# Subject: CN=Amazon Root CA 1 O=Amazon -# Label: "Amazon Root CA 1" -# Serial: 143266978916655856878034712317230054538369994 -# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 -# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 -# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 2 O=Amazon -# Subject: CN=Amazon Root CA 2 O=Amazon -# Label: "Amazon Root CA 2" -# Serial: 143266982885963551818349160658925006970653239 -# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 -# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a -# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 3 O=Amazon -# Subject: CN=Amazon Root CA 3 O=Amazon -# Label: "Amazon Root CA 3" -# Serial: 143266986699090766294700635381230934788665930 -# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 -# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e -# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl -ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr -ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr -BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM -YyRIHN8wfdVoOw== ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 4 O=Amazon -# Subject: CN=Amazon Root CA 4 O=Amazon -# Label: "Amazon Root CA 4" -# Serial: 143266989758080763974105200630763877849284878 -# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd -# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be -# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi -9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk -M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB -MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw -CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW -1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- - -# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" -# Serial: 1 -# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 -# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca -# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 ------BEGIN CERTIFICATE----- -MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx -GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp -bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w -KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 -BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy -dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG -EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll -IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU -QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT -TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg -LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 -a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr -LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr -N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X -YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ -iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f -AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH -V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh -AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf -IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 -lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c -8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf -lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE----- - -# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Label: "GDCA TrustAUTH R5 ROOT" -# Serial: 9009899650740120186 -# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 -# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 -# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 ------BEGIN CERTIFICATE----- -MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE -BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ -IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 -MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV -BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w -HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj -Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj -TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u -KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj -qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm -MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 -ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP -zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk -L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC -jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA -HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC -AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg -p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm -DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 -COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry -L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf -JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg -IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io -2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV -09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ -XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq -T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe -MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Label: "SSL.com Root Certification Authority RSA" -# Serial: 8875640296558310041 -# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 -# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb -# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 ------BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE -BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK -DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz -OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R -xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX -qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC -C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 -6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh -/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF -YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E -JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc -US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 -ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm -+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi -M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G -A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV -cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc -Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs -PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ -q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 -cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr -a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I -H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y -K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu -nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf -oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY -Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com Root Certification Authority ECC" -# Serial: 8495723813297216424 -# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e -# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a -# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 ------BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz -WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 -b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS -b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI -7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg -CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud -EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD -VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T -kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ -gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority RSA R2" -# Serial: 6248227494352943350 -# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 -# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a -# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c ------BEGIN CERTIFICATE----- -MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV -BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE -CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy -MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G -A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD -DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq -M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf -OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa -4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 -HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR -aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA -b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ -Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV -PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO -pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu -UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY -MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV -HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 -9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW -s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 -Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg -cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM -79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz -/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt -ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm -Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK -QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ -w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi -S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 -mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority ECC" -# Serial: 3182246526754555285 -# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 -# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d -# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 ------BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx -NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv -bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA -VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku -WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX -5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ -ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg -h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Label: "GlobalSign Root CA - R6" -# Serial: 1417766617973444989252670301619537 -# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae -# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 -# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg -MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx -MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET -MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI -xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k -ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD -aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw -LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw -1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX -k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 -SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h -bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n -WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY -rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce -MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu -bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN -nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt -Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 -55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj -vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf -cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz -oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp -nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs -pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v -JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R -8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 -5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GC CA" -# Serial: 44084345621038548146064804565436152554 -# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 -# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 -# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d ------BEGIN CERTIFICATE----- -MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw -CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 -bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg -Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ -BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu -ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS -b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni -eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W -p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T -rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV -57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg -Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE----- - -# Issuer: CN=UCA Global G2 Root O=UniTrust -# Subject: CN=UCA Global G2 Root O=UniTrust -# Label: "UCA Global G2 Root" -# Serial: 124779693093741543919145257850076631279 -# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 -# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a -# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH -bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x -CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds -b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr -b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 -kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm -VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R -VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc -C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj -tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY -D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv -j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl -NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 -iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP -O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV -ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj -L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 -1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl -1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU -b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV -PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj -y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb -EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg -DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI -+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy -YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX -UB+K+wb1whnw0A== ------END CERTIFICATE----- - -# Issuer: CN=UCA Extended Validation Root O=UniTrust -# Subject: CN=UCA Extended Validation Root O=UniTrust -# Label: "UCA Extended Validation Root" -# Serial: 106100277556486529736699587978573607008 -# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 -# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a -# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF -eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx -MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV -BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog -D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS -sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop -O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk -sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi -c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj -VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz -KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ -TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G -sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs -1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD -fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN -l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR -ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ -VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 -c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp -4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s -t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj -2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO -vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C -xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx -cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM -fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax ------END CERTIFICATE----- - -# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Label: "Certigna Root CA" -# Serial: 269714418870597844693661054334862075617 -# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 -# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 -# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 ------BEGIN CERTIFICATE----- -MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw -WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw -MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x -MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD -VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX -BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO -ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M -CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu -I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm -TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh -C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf -ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz -IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT -Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k -JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 -hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB -GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of -1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov -L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo -dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr -aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq -hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L -6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG -HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 -0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB -lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi -o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 -gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v -faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 -Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh -jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw -3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign Root CA - G1" -# Serial: 235931866688319308814040 -# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac -# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c -# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 ------BEGIN CERTIFICATE----- -MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD -VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU -ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH -MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO -MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv -Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz -f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO -8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq -d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM -tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt -Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB -o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x -PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM -wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d -GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH -6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby -RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx -iN66zB+Afko= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign ECC Root CA - G3" -# Serial: 287880440101571086945156 -# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 -# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 -# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b ------BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG -EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo -bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g -RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ -TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s -b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw -djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 -WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS -fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB -zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB -CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD -+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Label: "emSign Root CA - C1" -# Serial: 825510296613316004955058 -# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 -# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 -# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f ------BEGIN CERTIFICATE----- -MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG -A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg -SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v -dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ -BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ -HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH -3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH -GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c -xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 -aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq -TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 -/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 -kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG -YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT -+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo -WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Label: "emSign ECC Root CA - C3" -# Serial: 582948710642506000014504 -# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 -# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 -# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 ------BEGIN CERTIFICATE----- -MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG -EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx -IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND -IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci -MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti -sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O -BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB -Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c -3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J -0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== ------END CERTIFICATE----- - -# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Label: "Hongkong Post Root CA 3" -# Serial: 46170865288971385588281144162979347873371282084 -# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 -# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 -# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 ------BEGIN CERTIFICATE----- -MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL -BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ -SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n -a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 -NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT -CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u -Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO -dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI -VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV -9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY -2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY -vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt -bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb -x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ -l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK -TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj -Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e -i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw -DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG -7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk -MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr -gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk -GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS -3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm -Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ -l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c -JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP -L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa -LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG -mpv0 ------END CERTIFICATE----- - -# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft ECC Root Certificate Authority 2017" -# Serial: 136839042543790627607696632466672567020 -# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 -# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 -# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 ------BEGIN CERTIFICATE----- -MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD -VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw -MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV -UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy -b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR -ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb -hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 -FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV -L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB -iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE----- - -# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft RSA Root Certificate Authority 2017" -# Serial: 40975477897264996090493496164228220339 -# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 -# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 -# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 ------BEGIN CERTIFICATE----- -MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl -MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw -NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG -EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N -aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ -Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 -ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 -HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm -gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ -jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc -aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG -YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 -W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K -UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH -+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q -W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC -LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC -gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 -tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh -SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 -TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 -pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR -xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp -GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 -dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN -AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB -RA+GsCyRxj3qrg+E ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Label: "e-Szigno Root CA 2017" -# Serial: 411379200276854331539784714 -# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 -# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 -# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 ------BEGIN CERTIFICATE----- -MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV -BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk -LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv -b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ -BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg -THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v -IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv -xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H -Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB -eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo -jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ -+efcMQ== ------END CERTIFICATE----- - -# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Label: "certSIGN Root CA G2" -# Serial: 313609486401300475190 -# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 -# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 -# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV -BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g -Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ -BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ -R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF -dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw -vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ -uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp -n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs -cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW -xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P -rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF -DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx -DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy -LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C -eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ -d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq -kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC -b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl -qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 -OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c -NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk -ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO -pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj -03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk -PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE -1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX -QRBdJ3NghVdJIgc= ------END CERTIFICATE----- - -# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Label: "NAVER Global Root Certification Authority" -# Serial: 9013692873798656336226253319739695165984492813 -# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b -# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 -# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 ------BEGIN CERTIFICATE----- -MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM -BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG -T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx -CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD -b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA -iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH -38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE -HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz -kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP -szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq -vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf -nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG -YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo -0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a -CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K -AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I -36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB -Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN -qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj -cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm -+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL -hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe -lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 -p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 -piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR -LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX -5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO -dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul -9XXeifdy ------END CERTIFICATE----- - -# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" -# Serial: 131542671362353147877283741781055151509 -# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb -# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a -# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb ------BEGIN CERTIFICATE----- -MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw -CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw -FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S -Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 -MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL -DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS -QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH -sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK -Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu -SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC -MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy -v+c= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Label: "GlobalSign Root R46" -# Serial: 1552617688466950547958867513931858518042577 -# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef -# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 -# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA -MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD -VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy -MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt -c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ -OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG -vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud -316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo -0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE -y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF -zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE -+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN -I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs -x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa -ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC -4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 -7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg -JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti -2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk -pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF -FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt -rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk -ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 -u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP -4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 -N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 -vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Label: "GlobalSign Root E46" -# Serial: 1552617690338932563915843282459653771421763 -# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f -# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 -# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 ------BEGIN CERTIFICATE----- -MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx -CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD -ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw -MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex -HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq -R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd -yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ -7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 -+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE----- - -# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Label: "ANF Secure Server Root CA" -# Serial: 996390341000653745 -# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 -# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 -# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV -BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk -YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV -BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN -MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF -UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD -VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v -dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj -cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q -yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH -2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX -H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL -zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR -p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz -W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ -SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn -LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 -n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B -u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj -o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC -AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L -9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej -rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK -pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 -vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq -OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ -/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 -2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI -+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 -MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo -tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE----- - -# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum EC-384 CA" -# Serial: 160250656287871593594747141429395092468 -# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 -# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed -# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 ------BEGIN CERTIFICATE----- -MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw -CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw -JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT -EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 -WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT -LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX -BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE -KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm -Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 -EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J -UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn -nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Root CA" -# Serial: 40870380103424195783807378461123655149 -# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 -# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 -# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd ------BEGIN CERTIFICATE----- -MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 -MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu -MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV -BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw -MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg -U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo -b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ -n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q -p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq -NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF -8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 -HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa -mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi -7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF -ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P -qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ -v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 -Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 -vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD -ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 -WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo -zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR -5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ -GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf -5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq -0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D -P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM -qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP -0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf -E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE----- - -# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Label: "TunTrust Root CA" -# Serial: 108534058042236574382096126452369648152337120275 -# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 -# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb -# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL -BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg -Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv -b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG -EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u -IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ -n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd -2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF -VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ -GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF -li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU -r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 -eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb -MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg -jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB -7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW -5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE -ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 -90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z -xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu -QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 -FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH -22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP -xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn -dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 -Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b -nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ -CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH -u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj -d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS RSA Root CA 2021" -# Serial: 76817823531813593706434026085292783742 -# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 -# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d -# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs -MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg -Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL -MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl -YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv -b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l -mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE -4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv -a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M -pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw -Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b -LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY -AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB -AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq -E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr -W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ -CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU -X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 -f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja -H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP -JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P -zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt -jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 -/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT -BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 -aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW -xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU -63ZTGI0RmLo= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS ECC Root CA 2021" -# Serial: 137515985548005187474074462014555733966 -# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 -# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 -# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 ------BEGIN CERTIFICATE----- -MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw -CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh -cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v -dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG -A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj -aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg -Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 -KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y -STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD -AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw -SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN -nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE----- - -# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" -# Serial: 1977337328857672817 -# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 -# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe -# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE -BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h -cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 -MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg -Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 -thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM -cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG -L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i -NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h -X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b -m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy -Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja -EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T -KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF -6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh -OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc -tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd -IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j -b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC -AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw -ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m -iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF -Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ -hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P -Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE -EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV -1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t -CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR -5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw -f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 -ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK -GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE----- - -# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus ECC Root CA" -# Serial: 630369271402956006249506845124680065938238527194 -# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 -# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 -# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 ------BEGIN CERTIFICATE----- -MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw -RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY -BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz -MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u -LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 -v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd -e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw -V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA -AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG -GJTO ------END CERTIFICATE----- - -# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus Root CA" -# Serial: 387574501246983434957692974888460947164905180485 -# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc -# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 -# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 ------BEGIN CERTIFICATE----- -MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL -BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x -FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx -MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s -THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc -IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU -AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ -GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 -8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH -flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt -J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim -0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN -pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ -UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW -OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB -AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet -8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd -nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j -bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM -Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv -TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS -S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr -I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 -b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB -UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P -Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven -sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X2 O=Internet Security Research Group -# Subject: CN=ISRG Root X2 O=Internet Security Research Group -# Label: "ISRG Root X2" -# Serial: 87493402998870891108772069816698636114 -# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 -# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af -# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 ------BEGIN CERTIFICATE----- -MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw -CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg -R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 -MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT -ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw -EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW -+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 -ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI -zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW -tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 -/q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE----- - -# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Label: "HiPKI Root CA - G1" -# Serial: 60966262342023497858655262305426234976 -# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 -# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 -# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc ------BEGIN CERTIFICATE----- -MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP -MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 -ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa -Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 -YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw -qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv -Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 -lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz -Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ -KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK -FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj -HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr -y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ -/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM -a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 -fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG -SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi -7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc -SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza -ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc -XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg -iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho -L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF -Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr -kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ -vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU -YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Label: "GlobalSign ECC Root CA - R4" -# Serial: 159662223612894884239637590694 -# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc -# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 -# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 ------BEGIN CERTIFICATE----- -MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD -VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw -MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g -UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT -BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx -uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV -HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ -+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 -bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R1 O=Google Trust Services LLC -# Subject: CN=GTS Root R1 O=Google Trust Services LLC -# Label: "GTS Root R1" -# Serial: 159662320309726417404178440727 -# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 -# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a -# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw -MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp -Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo -27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w -Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw -TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl -qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH -szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 -Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk -MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 -wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p -aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN -VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID -AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb -C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe -QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy -h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 -7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J -ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef -MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ -Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT -6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ -0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm -2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb -bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R3 O=Google Trust Services LLC -# Subject: CN=GTS Root R3 O=Google Trust Services LLC -# Label: "GTS Root R3" -# Serial: 159662495401136852707857743206 -# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 -# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 -# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G -jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 -4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 -VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm -ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R4 O=Google Trust Services LLC -# Subject: CN=GTS Root R4 O=Google Trust Services LLC -# Label: "GTS Root R4" -# Serial: 159662532700760215368942768210 -# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 -# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 -# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi -QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR -HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D -9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 -p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD ------END CERTIFICATE----- - -# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj -# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj -# Label: "Telia Root CA v2" -# Serial: 7288924052977061235122729490515358 -# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 -# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd -# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx -CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE -AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 -NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ -MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq -AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 -vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 -lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD -n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT -7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o -6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC -TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 -WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R -DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI -pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj -YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy -rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ -8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi -0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM -A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS -SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K -TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF -6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er -3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt -Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT -VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW -ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA -rBPuUBQemMc= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 1 2020" -# Serial: 165870826978392376648679885835942448534 -# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed -# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 -# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 -NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS -zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 -QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ -VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW -wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV -dWNbFJWcHwHP2NVypw87 ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 1 2020" -# Serial: 126288379621884218666039612629459926992 -# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e -# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 -# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 -NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC -/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD -wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 -OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA -y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb -gfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS ECC P384 Root G5" -# Serial: 13129116028163249804115411775095713523 -# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed -# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee -# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 ------BEGIN CERTIFICATE----- -MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp -Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 -MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ -bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS -7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp -0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS -B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 -BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ -LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 -DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS RSA4096 Root G5" -# Serial: 11930366277458970227240571539258396554 -# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 -# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 -# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN -MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT -HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN -NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs -IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ -ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 -2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp -wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM -pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD -nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po -sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx -Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd -Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX -KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe -XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL -tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv -TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN -AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw -GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H -PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF -O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ -REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik -AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv -/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ -p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw -MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF -qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK -ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root R1 O=Certainly -# Subject: CN=Certainly Root R1 O=Certainly -# Label: "Certainly Root R1" -# Serial: 188833316161142517227353805653483829216 -# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 -# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af -# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw -PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy -dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 -YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 -1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT -vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed -aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 -1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 -r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 -cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ -wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ -6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA -2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH -Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR -eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB -/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u -d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr -PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d -8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi -1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd -rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di -taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 -lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj -yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn -Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy -yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n -wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 -OV+KmalBWQewLK8= ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root E1 O=Certainly -# Subject: CN=Certainly Root E1 O=Certainly -# Label: "Certainly Root E1" -# Serial: 8168531406727139161245376702891150584 -# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 -# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b -# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 ------BEGIN CERTIFICATE----- -MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw -CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu -bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ -BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s -eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK -+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 -QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 -hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm -ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG -BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE----- - -# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Label: "Security Communication ECC RootCA1" -# Serial: 15446673492073852651 -# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 -# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 -# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 ------BEGIN CERTIFICATE----- -MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT -AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD -VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx -NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT -HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 -IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl -dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK -ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu -9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O -be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA1" -# Serial: 113562791157148395269083148143378328608 -# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 -# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a -# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU -MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI -T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz -MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF -SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh -bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z -xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ -spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 -58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR -at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll -5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq -nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK -V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ -pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO -z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn -jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ -WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF -7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 -YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli -awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u -+2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 -X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN -SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo -P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI -+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz -znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 -eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 -YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy -r/6zcCwupvI= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA2" -# Serial: 58605626836079930195615843123109055211 -# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c -# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 -# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 ------BEGIN CERTIFICATE----- -MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw -CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ -VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy -MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ -TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS -b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B -IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ -+kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK -sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA -94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B -43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root E46" -# Serial: 88989738453351742415770396670917916916 -# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 -# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a -# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 ------BEGIN CERTIFICATE----- -MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw -CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T -ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN -MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG -A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT -ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC -WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ -6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B -Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa -qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q -4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root R46" -# Serial: 156256931880233212765902055439220583700 -# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 -# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 -# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 ------BEGIN CERTIFICATE----- -MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf -MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD -Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw -HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY -MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp -YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa -ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz -SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf -iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X -ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 -IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS -VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE -SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu -+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt -8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L -HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt -zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P -AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c -mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ -YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 -gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA -Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB -JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX -DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui -TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 -dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 -LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp -0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY -QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS RSA Root CA 2022" -# Serial: 148535279242832292258835760425842727825 -# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da -# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca -# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed ------BEGIN CERTIFICATE----- -MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO -MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD -DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX -DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw -b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP -L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY -t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins -S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 -PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO -L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 -R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w -dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS -+YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS -d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG -AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f -gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z -NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt -hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM -QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf -R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ -DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW -P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy -lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq -bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w -AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q -r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji -Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU -98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS ECC Root CA 2022" -# Serial: 26605119622390491762507526719404364228 -# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 -# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 -# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 ------BEGIN CERTIFICATE----- -MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT -U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 -MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh -dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm -acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN -SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME -GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW -uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp -15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN -b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA ECC TLS 2021" -# Serial: 81873346711060652204712539181482831616 -# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 -# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd -# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 ------BEGIN CERTIFICATE----- -MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w -LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w -CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 -MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF -Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI -zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X -tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 -AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 -KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD -aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu -CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo -9H1/IISpQuQo ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA RSA TLS 2021" -# Serial: 111436099570196163832749341232207667876 -# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 -# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 -# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f ------BEGIN CERTIFICATE----- -MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM -MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx -MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 -MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD -QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z -4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv -Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ -kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs -GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln -nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh -3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD -0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy -geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 -ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB -c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI -pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU -dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB -DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS -4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs -o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ -qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw -xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM -rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 -AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR -0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY -o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 -dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE -oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G3" -# Serial: 576386314500428537169965010905813481816650257167 -# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 -# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 -# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 ------BEGIN CERTIFICATE----- -MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM -BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe -Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw -IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU -cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS -T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK -AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 -nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep -qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA -yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs -hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX -zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv -kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT -f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA -uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB -o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih -MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E -BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 -wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 -XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 -JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j -ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV -VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx -xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on -AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d -7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj -gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV -+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo -FGWsJwt0ivKH ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G4" -# Serial: 451799571007117016466790293371524403291602933463 -# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb -# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a -# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c ------BEGIN CERTIFICATE----- -MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw -WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y -MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD -VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz -dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx -s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw -LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij -YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD -pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE -AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR -UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj -/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS ECC Root 2020" -# Serial: 72082518505882327255703894282316633856 -# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd -# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec -# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 ------BEGIN CERTIFICATE----- -MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw -CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH -bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw -MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx -JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE -AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O -tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP -f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA -MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di -z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn -27iQ7t0l ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS RSA Root 2023" -# Serial: 44676229530606711399881795178081572759 -# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 -# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 -# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj -MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 -eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy -MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC -REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG -A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 -cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV -cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA -U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 -Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug -BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy -8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J -co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg -8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 -rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 -mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg -+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX -gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 -p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ -pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm -9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw -M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd -GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ -CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t -xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ -w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK -L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj -X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q -ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm -dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= ------END CERTIFICATE----- - -# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA CYBER Root CA" -# Serial: 85076849864375384482682434040119489222 -# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 -# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 -# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ -MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 -IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 -WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO -LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg -Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P -40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF -avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ -34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i -JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu -j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf -Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP -2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA -S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA -oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC -kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW -5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd -BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB -AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t -tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn -68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn -TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t -RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx -f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI -Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz -8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 -NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX -xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 -t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA14" -# Serial: 575790784512929437950770173562378038616896959179 -# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 -# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f -# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM -BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u -LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw -NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD -eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS -b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ -FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg -vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy -6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo -/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J -kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ -0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib -y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac -18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs -0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB -SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL -ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk -86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E -rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib -ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT -zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS -DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 -2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo -FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy -K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 -dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl -Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB -365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c -JRNItX+S ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA15" -# Serial: 126083514594751269499665114766174399806381178503 -# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 -# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d -# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a ------BEGIN CERTIFICATE----- -MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw -UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM -dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy -NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl -cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 -IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 -wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR -ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT -9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp -4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 -bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 2 2023" -# Serial: 153168538924886464690566649552453098598 -# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 -# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 -# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw -OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr -i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE -gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 -k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT -Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl -2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U -cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP -/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS -uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ -0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N -DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ -XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 -GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI -FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n -riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR -VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc -LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn -4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD -hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG -koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 -ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS -Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 -knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ -hJ65bvspmZDogNOfJA== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS ECC Root CA" -# Serial: 310892014698942880364840003424242768478804666567 -# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c -# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 -# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 ------BEGIN CERTIFICATE----- -MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw -WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw -NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE -ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB -c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ -AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp -guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw -DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 -L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR -OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS RSA Root CA" -# Serial: 160405846464868906657516898462547310235378010780 -# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 -# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa -# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 ------BEGIN CERTIFICATE----- -MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM -BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN -MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG -A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 -c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC -AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ -NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ -Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 -HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 -ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb -xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX -i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ -UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j -TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT -bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 -S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT -MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 -Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 -iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt -7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp -2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ -g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj -pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M -pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP -XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe -SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 -ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy -323imttUQ/hHWKNddBWcwauwxzQ= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 2 2023" -# Serial: 139766439402180512324132425437959641711 -# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 -# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b -# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw -OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK -F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE -7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe -EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 -lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb -RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV -jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc -jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx -TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ -ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk -hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF -NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH -kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 -QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 -pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q -3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU -t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX -cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 -ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT -2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs -7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP -gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst -Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh -XBxvWHZks/wCuPWdCg== ------END CERTIFICATE----- - -# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Label: "SwissSign RSA TLS Root CA 2022 - 1" -# Serial: 388078645722908516278762308316089881486363258315 -# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 -# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce -# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 ------BEGIN CERTIFICATE----- -MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE -AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx -MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT -d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg -MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX -vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 -LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX -5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE -EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt -/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x -0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 -KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM -0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd -OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta -clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK -wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 -DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL -BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 -10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz -Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ -iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc -gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM -ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF -LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp -zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td -Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 -rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO -gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Label: "OISTE Server Root ECC G1" -# Serial: 47819833811561661340092227008453318557 -# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 -# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 -# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 ------BEGIN CERTIFICATE----- -MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw -CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY -T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy -NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp -b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy -cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N -2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 -TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C -tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR -QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD -YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Label: "OISTE Server Root RSA G1" -# Serial: 113845518112613905024960613408179309848 -# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 -# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e -# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL -MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE -AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 -MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k -YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM -vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b -rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk -ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z -O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R -tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS -jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh -sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho -mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu -+zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR -i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT -kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU -8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 -zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 -I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG -5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 -qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP -AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk -gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs -YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 -9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome -/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 -J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 -wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy -BiElxky8j3C7DOReIoMt0r7+hVu05L0= ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Label: "e-Szigno TLS Root CA 2023" -# Serial: 71934828665710877219916191754 -# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 -# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe -# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 ------BEGIN CERTIFICATE----- -MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU -TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow -dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy -b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T -emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE -AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS -AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v -SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K -ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI -zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt -y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl -C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 -uWWL ------END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py deleted file mode 100644 index 1c9661cc7c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/core.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -certifi.py -~~~~~~~~~~ - -This module returns the installation location of cacert.pem or its contents. -""" -import sys -import atexit - -def exit_cacert_ctx() -> None: - _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] - - -if sys.version_info >= (3, 11): - - from importlib.resources import as_file, files - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the file - # in cases where we're inside of a zipimport situation until someone - # actually calls where(), but we don't want to re-extract the file - # on every call of where(), so we'll do it once then store it in a - # global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you to - # manage the cleanup of this file, so it doesn't actually return a - # path, it returns a context manager that will give you the path - # when you enter it and will do any cleanup when you leave it. In - # the common case of not needing a temporary file, it will just - # return the file system location and the __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") - -else: - - from importlib.resources import path as get_path, read_text - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the - # file in cases where we're inside of a zipimport situation until - # someone actually calls where(), but we don't want to re-extract - # the file on every call of where(), so we'll do it once then store - # it in a global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you - # to manage the cleanup of this file, so it doesn't actually - # return a path, it returns a context manager that will give - # you the path when you enter it and will do any cleanup when - # you leave it. In the common case of not needing a temporary - # file, it will just return the file system location and the - # __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = get_path("certifi", "cacert.pem") - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/certifi/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py deleted file mode 100644 index 8ce8d739c9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from sentry_sdk import metrics, profiler - -from sentry_sdk.scope import Scope # isort: skip -from sentry_sdk.client import Client # isort: skip -from sentry_sdk.consts import VERSION -from sentry_sdk.transport import HttpTransport, Transport - -from sentry_sdk.api import * # noqa # isort: skip - -__all__ = [ # noqa - "Hub", - "Scope", - "Client", - "Transport", - "HttpTransport", - "VERSION", - "integrations", - # From sentry_sdk.api - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "logger", - "metrics", - "profiler", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - -# Initialize the debug support after everything is loaded -from sentry_sdk.debug import init_debug_support - -init_debug_support() -del init_debug_support - -# circular imports -from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py deleted file mode 100644 index 565fac2a2d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_batcher.py +++ /dev/null @@ -1,184 +0,0 @@ -import os -import random -import threading -import weakref -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Generic, TypeVar - -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - -T = TypeVar("T") - - -class Batcher(Generic[T]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "" - CONTENT_TYPE = "" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - self._buffer: "list[T]" = [] - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._flush_event: "threading.Event" = threading.Event() - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._buffer = [] - self._running = True - self._lock = threading.Lock() - self._active = threading.local() - self._flush_event = threading.Event() - self._flusher = None - self._flusher_pid = None - - def _ensure_thread(self) -> bool: - """For forking processes we might need to restart this thread. - This ensures that our process actually has that thread running. - """ - if not self._running: - return False - - pid = os.getpid() - if self._flusher_pid == pid: - return True - - with self._lock: - # Recheck to make sure another thread didn't get here and start the - # the flusher in the meantime - if self._flusher_pid == pid: - return True - - self._flusher_pid = pid - - self._flusher = threading.Thread(target=self._flush_loop) - self._flusher.daemon = True - - try: - self._flusher.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return False - - return True - - def _flush_loop(self) -> None: - # Mark the flush-loop thread as active for its entire lifetime so - # that any re-entrant add() triggered by GC warnings during wait(), - # flush(), or Event operations is silently dropped instead of - # deadlocking on internal locks. - self._active.flag = True - while self._running: - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) - self._flush_event.clear() - self._flush() - - def add(self, item: "T") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - if len(self._buffer) >= self.MAX_BEFORE_DROP: - self._record_lost(item) - return None - - self._buffer.append(item) - if len(self._buffer) >= self.MAX_BEFORE_FLUSH: - self._flush_event.set() - finally: - self._active.flag = False - - def kill(self) -> None: - if self._flusher is None: - return - - self._running = False - self._flush_event.set() - self._flusher = None - - def flush(self) -> None: - was_active = getattr(self._active, "flag", False) - self._active.flag = True - try: - self._flush() - finally: - self._active.flag = was_active - - def _add_to_envelope(self, envelope: "Envelope") -> None: - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": len(self._buffer), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(item) for item in self._buffer - ], - } - ), - ) - ) - - def _flush(self) -> "Optional[Envelope]": - envelope = Envelope( - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} - ) - with self._lock: - if len(self._buffer) == 0: - return None - - self._add_to_envelope(envelope) - self._buffer.clear() - - self._capture_func(envelope) - return envelope - - def _record_lost(self, item: "T") -> None: - pass - - @staticmethod - def _to_transport_format(item: "T") -> "Any": - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py deleted file mode 100644 index f62175c09f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_compat.py +++ /dev/null @@ -1,92 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, TypeVar - - T = TypeVar("T") - - -PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 -PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 -PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 -PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 - - -def with_metaclass(meta: "Any", *bases: "Any") -> "Any": - class MetaClass(type): - def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": - return meta(name, bases, d) - - return type.__new__(MetaClass, "temporary_class", (), {}) - - -def check_uwsgi_thread_support() -> bool: - # We check two things here: - # - # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if - # that's the case. - # - # 2. Additionally, if uWSGI is running in preforking mode (default), it needs - # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This - # is because any background threads spawned before the main process is - # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if - # --enable-threads is on. One has to explicitly provide - # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython - # after-fork hooks that take care of cleaning up stale thread data. - try: - from uwsgi import opt # type: ignore - except ImportError: - return True - - from sentry_sdk.consts import FALSE_VALUES - - def enabled(option: str) -> bool: - value = opt.get(option, False) - if isinstance(value, bool): - return value - - if isinstance(value, bytes): - try: - value = value.decode() - except Exception: - pass - - return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] - - # When `threads` is passed in as a uwsgi option, - # `enable-threads` is implied on. - threads_enabled = "threads" in opt or enabled("enable-threads") - fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") - lazy_mode = enabled("lazy-apps") or enabled("lazy") - - if lazy_mode and not threads_enabled: - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI without thread support. " - "This might lead to unexpected issues. " - 'Please run uWSGI with "--enable-threads" for full support.' - ) - ) - - return False - - elif not lazy_mode and (not threads_enabled or not fork_hooks_on): - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI in preforking mode without " - "thread support. This might lead to crashing workers. " - 'Please run uWSGI with both "--enable-threads" and ' - '"--py-call-uwsgi-fork-hooks" for full support.' - ) - ) - - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py deleted file mode 100644 index 923fcf6df8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_init_implementation.py +++ /dev/null @@ -1,78 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -import sentry_sdk - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional - - import sentry_sdk.consts - - -class _InitGuard: - _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( - "Using the return value of sentry_sdk.init as a context manager " - "and manually calling the __enter__ and __exit__ methods on the " - "return value are deprecated. We are no longer maintaining this " - "functionality, and we will remove it in the next major release." - ) - - def __init__(self, client: "sentry_sdk.Client") -> None: - self._client = client - - def __enter__(self) -> "_InitGuard": - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - c = self._client - if c is not None: - c.close() - - -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass - - -def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": - """Initializes the SDK and optionally integrations. - - This takes the same arguments as the client constructor. - """ - client = sentry_sdk.Client(*args, **kwargs) - sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() - rv = _InitGuard(client) - return rv - - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `init` is a type to - # have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `ContextManager[Any]` to tell static analyzers about the return type. - - class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 - pass - -else: - # Alias `init` for actual usage. Go through the lambda indirection to throw - # PyCharm off of the weakly typed signature (it would otherwise discover - # both the weakly typed signature of `_init` and our faked `init` type). - - init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py deleted file mode 100644 index 0719932ee9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_log_batcher.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Item, PayloadRef -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Log - - -class LogBatcher(Batcher["Log"]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "log" - CONTENT_TYPE = "application/vnd.sentry.items.log+json" - - @staticmethod - def _to_transport_format(item: "Log") -> "Any": - if "sentry.severity_number" not in item["attributes"]: - item["attributes"]["sentry.severity_number"] = item["severity_number"] - if "sentry.severity_text" not in item["attributes"]: - item["attributes"]["sentry.severity_text"] = item["severity_text"] - - res = { - "timestamp": int(item["time_unix_nano"]) / 1.0e9, - "level": str(item["severity_text"]), - "body": str(item["body"]), - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - return res - - def _record_lost(self, item: "Log") -> None: - # Construct log envelope item without sending it to report lost bytes - log_item = Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": 1, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [self._to_transport_format(item)], - } - ), - ) - - self._record_lost_func( - reason="queue_overflow", - data_category="log_item", - item=log_item, - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py deleted file mode 100644 index 16c238bcab..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_lru_cache.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -_SENTINEL = object() - - -class LRUCache: - def __init__(self, max_size: int) -> None: - if max_size <= 0: - raise AssertionError(f"invalid max_size: {max_size}") - self.max_size = max_size - self._data: "dict[Any, Any]" = {} - self.hits = self.misses = 0 - self.full = False - - def set(self, key: "Any", value: "Any") -> None: - current = self._data.pop(key, _SENTINEL) - if current is not _SENTINEL: - self._data[key] = value - elif self.full: - self._data.pop(next(iter(self._data))) - self._data[key] = value - else: - self._data[key] = value - self.full = len(self._data) >= self.max_size - - def get(self, key: "Any", default: "Any" = None) -> "Any": - try: - ret = self._data.pop(key) - except KeyError: - self.misses += 1 - ret = default - else: - self.hits += 1 - self._data[key] = ret - - return ret - - def get_all(self) -> "list[tuple[Any, Any]]": - return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py deleted file mode 100644 index 06bce1a282..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_metrics_batcher.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Metric - - -class MetricsBatcher(Batcher["Metric"]): - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 10_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "trace_metric" - CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" - - @staticmethod - def _to_transport_format(item: "Metric") -> "Any": - res = { - "timestamp": item["timestamp"], - "name": item["name"], - "type": item["type"], - "value": item["value"], - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - if item.get("unit") is not None: - res["unit"] = item["unit"] - - return res - - def _record_lost(self, item: "Metric") -> None: - self._record_lost_func( - reason="queue_overflow", - data_category="trace_metric", - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py deleted file mode 100644 index c28c8de9ac..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_queue.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') -with Lock swapped out for RLock to avoid a deadlock while garbage collecting. - -https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py - - -See also -https://codewithoutrules.com/2017/08/16/concurrency-python/ -https://bugs.python.org/issue14976 -https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 - -We also vendor the code to evade eventlet's broken monkeypatching, see -https://github.com/getsentry/sentry-python/pull/484 - - -Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; - -All Rights Reserved - - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - -""" - -import threading -from collections import deque -from time import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - -__all__ = ["EmptyError", "FullError", "Queue"] - - -class EmptyError(Exception): - "Exception raised by Queue.get(block=0)/get_nowait()." - - pass - - -class FullError(Exception): - "Exception raised by Queue.put(block=0)/put_nowait()." - - pass - - -class Queue: - """Create a queue object with a given maximum size. - - If maxsize is <= 0, the queue size is infinite. - """ - - def __init__(self, maxsize=0): - self.maxsize = maxsize - self._init(maxsize) - - # mutex must be held whenever the queue is mutating. All methods - # that acquire mutex must release it before returning. mutex - # is shared between the three conditions, so acquiring and - # releasing the conditions also acquires and releases mutex. - self.mutex = threading.RLock() - - # Notify not_empty whenever an item is added to the queue; a - # thread waiting to get is notified then. - self.not_empty = threading.Condition(self.mutex) - - # Notify not_full whenever an item is removed from the queue; - # a thread waiting to put is notified then. - self.not_full = threading.Condition(self.mutex) - - # Notify all_tasks_done whenever the number of unfinished tasks - # drops to zero; thread waiting to join() is notified to resume - self.all_tasks_done = threading.Condition(self.mutex) - self.unfinished_tasks = 0 - - def task_done(self): - """Indicate that a formerly enqueued task is complete. - - Used by Queue consumer threads. For each get() used to fetch a task, - a subsequent call to task_done() tells the queue that the processing - on the task is complete. - - If a join() is currently blocking, it will resume when all items - have been processed (meaning that a task_done() call was received - for every item that had been put() into the queue). - - Raises a ValueError if called more times than there were items - placed in the queue. - """ - with self.all_tasks_done: - unfinished = self.unfinished_tasks - 1 - if unfinished <= 0: - if unfinished < 0: - raise ValueError("task_done() called too many times") - self.all_tasks_done.notify_all() - self.unfinished_tasks = unfinished - - def join(self): - """Blocks until all items in the Queue have been gotten and processed. - - The count of unfinished tasks goes up whenever an item is added to the - queue. The count goes down whenever a consumer thread calls task_done() - to indicate the item was retrieved and all work on it is complete. - - When the count of unfinished tasks drops to zero, join() unblocks. - """ - with self.all_tasks_done: - while self.unfinished_tasks: - self.all_tasks_done.wait() - - def qsize(self): - """Return the approximate size of the queue (not reliable!).""" - with self.mutex: - return self._qsize() - - def empty(self): - """Return True if the queue is empty, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() == 0 - as a direct substitute, but be aware that either approach risks a race - condition where a queue can grow before the result of empty() or - qsize() can be used. - - To create code that needs to wait for all queued tasks to be - completed, the preferred technique is to use the join() method. - """ - with self.mutex: - return not self._qsize() - - def full(self): - """Return True if the queue is full, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() >= n - as a direct substitute, but be aware that either approach risks a race - condition where a queue can shrink before the result of full() or - qsize() can be used. - """ - with self.mutex: - return 0 < self.maxsize <= self._qsize() - - def put(self, item, block=True, timeout=None): - """Put an item into the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until a free slot is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the FullError exception if no free slot was available within that time. - Otherwise ('block' is false), put an item on the queue if a free slot - is immediately available, else raise the FullError exception ('timeout' - is ignored in that case). - """ - with self.not_full: - if self.maxsize > 0: - if not block: - if self._qsize() >= self.maxsize: - raise FullError() - elif timeout is None: - while self._qsize() >= self.maxsize: - self.not_full.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while self._qsize() >= self.maxsize: - remaining = endtime - time() - if remaining <= 0.0: - raise FullError() - self.not_full.wait(remaining) - self._put(item) - self.unfinished_tasks += 1 - self.not_empty.notify() - - def get(self, block=True, timeout=None): - """Remove and return an item from the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until an item is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the EmptyError exception if no item was available within that time. - Otherwise ('block' is false), return an item if one is immediately - available, else raise the EmptyError exception ('timeout' is ignored - in that case). - """ - with self.not_empty: - if not block: - if not self._qsize(): - raise EmptyError() - elif timeout is None: - while not self._qsize(): - self.not_empty.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while not self._qsize(): - remaining = endtime - time() - if remaining <= 0.0: - raise EmptyError() - self.not_empty.wait(remaining) - item = self._get() - self.not_full.notify() - return item - - def put_nowait(self, item): - """Put an item into the queue without blocking. - - Only enqueue the item if a free slot is immediately available. - Otherwise raise the FullError exception. - """ - return self.put(item, block=False) - - def get_nowait(self): - """Remove and return an item from the queue without blocking. - - Only get an item if one is immediately available. Otherwise - raise the EmptyError exception. - """ - return self.get(block=False) - - # Override these methods to implement other queue organizations - # (e.g. stack or priority queue). - # These will only be called with appropriate locks held - - # Initialize the queue representation - def _init(self, maxsize): - self.queue: "Any" = deque() - - def _qsize(self): - return len(self.queue) - - # Put a new item in the queue - def _put(self, item): - self.queue.append(item) - - # Get an item from the queue - def _get(self): - return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py deleted file mode 100644 index 79285c3386..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_span_batcher.py +++ /dev/null @@ -1,234 +0,0 @@ -import os -import random -import threading -import time -import weakref -from collections import defaultdict -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp, serialize_attribute - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - from sentry_sdk._types import SpanJSON - - -class SpanBatcher(Batcher["SpanJSON"]): - # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is - # a bit of a buffer for spans that appear between the trigger to flush - # and actually flushing the buffer. - # - # The max limits are all per trace (per bucket). - MAX_ENVELOPE_SIZE = 1000 # spans - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 2000 - MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB - - FLUSH_WAIT_TIME = 5.0 - - TYPE = "span" - CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - # Spans from different traces cannot be emitted in the same envelope - # since the envelope contains a shared trace header. That's why we bucket - # by trace_id, so that we can then send the buckets each in its own - # envelope. - # trace_id -> span buffer - self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) - self._running_size: dict[str, int] = defaultdict(lambda: 0) - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._last_full_flush: float = time.monotonic() # drives time-based flushes - self._flush_event = threading.Event() - self._pending_flush: set[str] = set() # buckets to be flushed - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._span_buffer = defaultdict(list) - self._running_size = defaultdict(lambda: 0) - self._running = True - - self._lock = threading.Lock() - self._active = threading.local() - - self._last_full_flush = time.monotonic() - self._flush_event = threading.Event() - self._pending_flush = set() - - self._flusher = None - self._flusher_pid = None - - def _flush_loop(self) -> None: - self._active.flag = True - while self._running: - jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 - self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) - self._flush_event.clear() - - self._flush(only_pending=True) - - if ( - time.monotonic() - self._last_full_flush - >= self.FLUSH_WAIT_TIME + jitter - ): - self._flush() - self._last_full_flush = time.monotonic() - - def add(self, span: "SpanJSON") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - size = len(self._span_buffer[span["trace_id"]]) - if size >= self.MAX_BEFORE_DROP: - self._record_lost_func( - reason="queue_overflow", - data_category="span", - quantity=1, - ) - return None - - self._span_buffer[span["trace_id"]].append(span) - self._running_size[span["trace_id"]] += self._estimate_size(span) - - if ( - size + 1 >= self.MAX_BEFORE_FLUSH - or self._running_size[span["trace_id"]] - >= self.MAX_BYTES_BEFORE_FLUSH - ): - self._pending_flush.add(span["trace_id"]) - notify = True - else: - notify = False - - if notify: - self._flush_event.set() - finally: - self._active.flag = False - - @staticmethod - def _estimate_size(item: "SpanJSON") -> int: - # Rough estimate of serialized span size that's quick to compute. - # 210 is the rough size of the payload without attributes, and then we - # estimate the attributes separately. - estimate = 210 - for value in (item.get("attributes") or {}).values(): - estimate += 50 - - if isinstance(value, str): - estimate += len(value) - else: - estimate += len(str(value)) - - return estimate - - @staticmethod - def _to_transport_format(item: "SpanJSON") -> "Any": - res = {k: v for k, v in item.items() if k not in ("_segment_span",)} - - if item.get("attributes"): - res["attributes"] = { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - } - else: - del res["attributes"] - - return res - - def _flush(self, only_pending: bool = False) -> None: - with self._lock: - if only_pending: - buckets = list(self._pending_flush) - else: - # flush whole buffer, e.g. if the SDK is shutting down - buckets = list(self._span_buffer.keys()) - - self._pending_flush.clear() - - if not buckets: - return - - envelopes = [] - - for bucket_id in buckets: - spans = self._span_buffer.get(bucket_id) - if not spans: - continue - - dsc = spans[0]["_segment_span"]._dynamic_sampling_context() - - # Max per envelope is 1000, so if we happen to have more than - # 1000 spans in one bucket, we'll need to separate them. - for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): - end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) - - envelope = Envelope( - headers={ - "sent_at": format_timestamp(datetime.now(timezone.utc)), - "trace": dsc, - } - ) - - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": end - start, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(spans[j]) - for j in range(start, end) - ], - } - ), - ) - ) - - envelopes.append(envelope) - - del self._span_buffer[bucket_id] - del self._running_size[bucket_id] - - for envelope in envelopes: - self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py deleted file mode 100644 index fbd2578048..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_types.py +++ /dev/null @@ -1,481 +0,0 @@ -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING, TypeVar, Union - -# Re-exported for compat, since code out there in the wild might use this variable. -MYPY = TYPE_CHECKING - - -SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" -BLOB_DATA_SUBSTITUTE = "[Blob substitute]" -OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" -UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" - - -class AnnotatedValue: - """ - Meta information for a data field in the event payload. - This is to tell Relay that we have tampered with the fields value. - See: - https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 - """ - - __slots__ = ("value", "metadata") - - def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: - self.value = value - self.metadata = metadata - - def __eq__(self, other: "Any") -> bool: - if not isinstance(other, AnnotatedValue): - return False - - return self.value == other.value and self.metadata == other.metadata - - def __str__(self: "AnnotatedValue") -> str: - return str({"value": str(self.value), "metadata": str(self.metadata)}) - - def __len__(self: "AnnotatedValue") -> int: - if self.value is not None: - return len(self.value) - else: - return 0 - - @classmethod - def removed_because_raw_data(cls) -> "AnnotatedValue": - """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" - # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition - # to span-first - return AnnotatedValue( - value="", - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_raw_data(cls) -> "AnnotatedValue": - """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" - return AnnotatedValue( - value=UNPARSABLE_RAW_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": - """ - The actual value was removed because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition - # to span-first - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_over_size_limit( - cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE - ) -> "AnnotatedValue": - """ - The actual value was replaced because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": - """The actual value was removed because it contained sensitive information.""" - return AnnotatedValue( - value=SENSITIVE_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) - "s", # The fields original value was substituted - ] - ] - }, - ) - - -T = TypeVar("T") -Annotated = Union[AnnotatedValue, T] - - -if TYPE_CHECKING: - from collections.abc import Container, MutableMapping, Sequence - from datetime import datetime - from types import TracebackType - from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - - class SDKInfo(TypedDict): - name: str - version: str - packages: "Sequence[Mapping[str, str]]" - - class KeyValueCollectionBehaviour(TypedDict): - mode: 'Literal["off", "denylist", "allowlist"]' - 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): - request: "KeyValueCollectionBehaviour" - - class HttpHeadersCollectionBehaviour(TypedDict): - request: "KeyValueCollectionBehaviour" - - class DataCollectionUserOptions(TypedDict, total=False): - user_info: bool - cookies: "KeyValueCollectionBehaviour" - http_headers: "HttpHeadersCollectionUserOptions" - http_bodies: "List[str]" - query_params: "KeyValueCollectionBehaviour" - graphql: "GraphQLCollectionUserOptions" - gen_ai: "GenAICollectionUserOptions" - database_query_data: bool - queues: bool - 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_query_data: bool - queues: bool - 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"] - - DurationUnit = Literal[ - "nanosecond", - "microsecond", - "millisecond", - "second", - "minute", - "hour", - "day", - "week", - ] - - InformationUnit = Literal[ - "bit", - "byte", - "kilobyte", - "kibibyte", - "megabyte", - "mebibyte", - "gigabyte", - "gibibyte", - "terabyte", - "tebibyte", - "petabyte", - "pebibyte", - "exabyte", - "exbibyte", - ] - - FractionUnit = Literal["ratio", "percent"] - MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] - - MeasurementValue = TypedDict( - "MeasurementValue", - { - "value": float, - "unit": NotRequired[Optional[MeasurementUnit]], - }, - ) - - Event = TypedDict( - "Event", - { - "breadcrumbs": Annotated[ - dict[Literal["values"], list[dict[str, Any]]] - ], # TODO: We can expand on this type - "check_in_id": str, - "contexts": dict[str, dict[str, object]], - "dist": str, - "duration": Optional[float], - "environment": Optional[str], - "errors": list[dict[str, Any]], # TODO: We can expand on this type - "event_id": str, - "exception": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "extra": MutableMapping[str, object], - "fingerprint": list[str], - "level": LogLevelStr, - "logentry": Mapping[str, object], - "logger": str, - "measurements": dict[str, MeasurementValue], - "message": str, - "modules": dict[str, str], - "monitor_config": Mapping[str, object], - "monitor_slug": Optional[str], - "platform": Literal["python"], - "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports - "release": Optional[str], - "request": dict[str, object], - "sdk": Mapping[str, object], - "server_name": str, - "spans": Annotated[list[dict[str, object]]], - "stacktrace": dict[ - str, object - ], # We access this key in the code, but I am unsure whether we ever set it - "start_timestamp": datetime, - "status": Optional[str], - "tags": MutableMapping[ - str, str - ], # Tags must be less than 200 characters each - "threads": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "timestamp": Optional[datetime], # Must be set before sending the event - "transaction": str, - "transaction_info": Mapping[str, Any], # TODO: We can expand on this type - "type": Literal["check_in", "transaction"], - "user": dict[str, object], - "_dropped_spans": int, - "_has_gen_ai_span": bool, - }, - total=False, - ) - - ExcInfo = Union[ - tuple[Type[BaseException], BaseException, Optional[TracebackType]], - tuple[None, None, None], - ] - - # TODO: Make a proper type definition for this (PRs welcome!) - Hint = Dict[str, Any] - - AttributeValue = ( - str - | bool - | float - | int - | list[str] - | list[bool] - | list[float] - | list[int] - | tuple[str, ...] - | tuple[bool, ...] - | tuple[float, ...] - | tuple[int, ...] - ) - Attributes = dict[str, AttributeValue] - - SerializedAttributeValue = TypedDict( - # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types - "SerializedAttributeValue", - { - "type": Literal[ - "string", - "boolean", - "double", - "integer", - "array", - ], - "value": AttributeValue, - }, - ) - - Log = TypedDict( - "Log", - { - "severity_text": str, - "severity_number": int, - "body": str, - "attributes": Attributes, - "time_unix_nano": int, - "trace_id": Optional[str], - "span_id": Optional[str], - }, - ) - - MetricType = Literal["counter", "gauge", "distribution"] - MetricUnit = Union[DurationUnit, InformationUnit, str] - - Metric = TypedDict( - "Metric", - { - "timestamp": float, - "trace_id": Optional[str], - "span_id": Optional[str], - "name": str, - "type": MetricType, - "value": float, - "unit": Optional[MetricUnit], - "attributes": Attributes, - }, - ) - - MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] - - SpanJSON = TypedDict( - "SpanJSON", - { - "trace_id": str, - "span_id": str, - "parent_span_id": NotRequired[str], - "name": str, - "status": str, - "is_segment": bool, - "start_timestamp": float, - "end_timestamp": NotRequired[float], - "attributes": NotRequired[Attributes], - "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], - }, - ) - - # TODO: Make a proper type definition for this (PRs welcome!) - Breadcrumb = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - BreadcrumbHint = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - SamplingContext = Dict[str, Any] - - EventProcessor = Callable[[Event, Hint], Optional[Event]] - ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] - BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] - TransactionProcessor = Callable[[Event, Hint], Optional[Event]] - LogProcessor = Callable[[Log, Hint], Optional[Log]] - - TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] - - # https://github.com/python/mypy/issues/5710 - NotImplementedType = Any - - EventDataCategory = Literal[ - "default", - "error", - "crash", - "transaction", - "security", - "attachment", - "session", - "internal", - "profile", - "profile_chunk", - "monitor", - "span", - "log_item", - "log_byte", - "trace_metric", - ] - SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] - - ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] - ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] - - MonitorConfigScheduleType = Literal["crontab", "interval"] - MonitorConfigScheduleUnit = Literal[ - "year", - "month", - "week", - "day", - "hour", - "minute", - "second", # not supported in Sentry and will result in a warning - ] - - MonitorConfigSchedule = TypedDict( - "MonitorConfigSchedule", - { - "type": MonitorConfigScheduleType, - "value": Union[int, str], - "unit": MonitorConfigScheduleUnit, - }, - total=False, - ) - - MonitorConfig = TypedDict( - "MonitorConfig", - { - "schedule": MonitorConfigSchedule, - "timezone": str, - "checkin_margin": int, - "max_runtime": int, - "failure_issue_threshold": int, - "recovery_threshold": int, - "owner": str, - }, - total=False, - ) - - HttpStatusCodeRange = Union[int, Container[int]] - - class TextPart(TypedDict): - type: Literal["text"] - content: str - - IgnoreSpansName = Union[str, Pattern[str]] - IgnoreSpansContext = TypedDict( - "IgnoreSpansContext", - {"name": IgnoreSpansName, "attributes": Attributes}, - total=False, - ) - IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py deleted file mode 100644 index 98f932267f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/_werkzeug.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Copyright (c) 2007 by the Pallets team. - -Some rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, Iterator, Optional, Tuple - - -# -# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` -# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 -# -# We need this function because Django does not give us a "pure" http header -# dict. So we might as well use it for all WSGI integrations. -# -def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns only proper HTTP headers. - """ - for key, value in environ.items(): - key = str(key) - if key.startswith("HTTP_") and key not in ( - "HTTP_CONTENT_TYPE", - "HTTP_CONTENT_LENGTH", - ): - yield key[5:].replace("_", "-").title(), value - elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): - yield key.replace("_", "-").title(), value - - -def _strip_default_port(host: str, scheme: "Optional[str]") -> str: - """Strip the port from the host if it's the default for the scheme.""" - if scheme == "http" and host.endswith(":80"): - return host[:-3] - if scheme == "https" and host.endswith(":443"): - return host[:-4] - return host - - -# `get_host` comes from `werkzeug.wsgi.get_host` -# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 - - -def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: - """ - Return the host for the given WSGI environment. - """ - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: - return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) - elif environ.get("HTTP_HOST"): - return _strip_default_port(environ["HTTP_HOST"], scheme) - elif environ.get("SERVER_NAME"): - # SERVER_NAME/SERVER_PORT describe the internal server, so use - # wsgi.url_scheme (not the forwarded scheme) for port decisions. - rv = environ["SERVER_NAME"] - if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( - ("https", "443"), - ("http", "80"), - ): - rv += ":" + environ["SERVER_PORT"] - return rv - else: - # In spite of the WSGI spec, SERVER_NAME might not be present. - return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py deleted file mode 100644 index 404e57ff1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .utils import ( - GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 - GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 - normalize_message_role, # noqa: F401 - normalize_message_roles, # noqa: F401 - set_conversation_id, # noqa: F401 - set_data_normalized, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py deleted file mode 100644 index b5eb8c55ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_completions_api.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections.abc import Iterable -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.chat import ( - ChatCompletionContentPartParam, - ChatCompletionMessageParam, - ChatCompletionSystemMessageParam, - ) - - from sentry_sdk._types import TextPart - - -def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: - return isinstance(message, dict) and message.get("role") == "system" - - -def _get_system_instructions( - messages: "Iterable[ChatCompletionMessageParam]", -) -> "list[ChatCompletionMessageParam]": - if not isinstance(messages, Iterable): - return [] - - return [message for message in messages if _is_system_instruction(message)] - - -def _get_text_items( - content: "Union[str, Iterable[ChatCompletionContentPartParam]]", -) -> "list[str]": - if isinstance(content, str): - return [content] - - if not isinstance(content, Iterable): - return [] - - text_items = [] - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - text = part.get("text", None) - if text is not None: - text_items.append(text) - - return text_items - - -def _transform_system_instructions( - system_instructions: "list[ChatCompletionSystemMessageParam]", -) -> "list[TextPart]": - instruction_text_parts: "list[TextPart]" = [] - - for instruction in system_instructions: - if not isinstance(instruction, dict): - continue - - content = instruction.get("content") - if content is None: - continue - - text_parts: "list[TextPart]" = [ - {"type": "text", "content": text} for text in _get_text_items(content) - ] - instruction_text_parts += text_parts - - return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py deleted file mode 100644 index 8f751c3248..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/_openai_responses_api.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.responses import ResponseInputItemParam, ResponseInputParam - - -def _is_system_instruction(message: "ResponseInputItemParam") -> bool: - if not isinstance(message, dict) or not message.get("role") == "system": - return False - - return "type" not in message or message["type"] == "message" - - -def _get_system_instructions( - messages: "Union[str, ResponseInputParam]", -) -> "list[ResponseInputItemParam]": - if not isinstance(messages, list): - return [] - - return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py deleted file mode 100644 index 35ee4bd788..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/consts.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - -# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." -DATA_URL_BASE64_REGEX = re.compile( - r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py deleted file mode 100644 index d8840ad451..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/monitoring.py +++ /dev/null @@ -1,147 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk.utils -from sentry_sdk import start_span -from sentry_sdk.ai.utils import _set_span_data_attribute -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, TypeVar, Union - - F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) - -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) - - -def set_ai_pipeline_name(name: "Optional[str]") -> None: - _ai_pipeline_name.set(name) - - -def get_ai_pipeline_name() -> "Optional[str]": - return _ai_pipeline_name.get() - - -def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": - def decorator(f: "F") -> "F": - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return await f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = await f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - if inspect.iscoroutinefunction(f): - return wraps(f)(async_wrapped) # type: ignore - else: - return wraps(f)(sync_wrapped) # type: ignore - - return decorator - - -def record_token_usage( - span: "Union[Span, StreamedSpan]", - input_tokens: "Optional[int]" = None, - input_tokens_cached: "Optional[int]" = None, - input_tokens_cache_write: "Optional[int]" = None, - output_tokens: "Optional[int]" = None, - output_tokens_reasoning: "Optional[int]" = None, - total_tokens: "Optional[int]" = None, -) -> None: - # TODO: move pipeline name elsewhere - ai_pipeline_name = get_ai_pipeline_name() - if ai_pipeline_name: - _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) - - if input_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if input_tokens_cached is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - input_tokens_cached, - ) - - if input_tokens_cache_write is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - input_tokens_cache_write, - ) - - if output_tokens is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens - ) - - if output_tokens_reasoning is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - output_tokens_reasoning, - ) - - if total_tokens is None and input_tokens is not None and output_tokens is not None: - total_tokens = input_tokens + output_tokens - - if total_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py deleted file mode 100644 index 7b1ca9324b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/ai/utils.py +++ /dev/null @@ -1,782 +0,0 @@ -import inspect -import json -from copy import deepcopy -from typing import TYPE_CHECKING - -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Tuple, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import logger - -MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB -# Maximum characters when only a single message is left after bytes truncation -MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 - - -class GEN_AI_ALLOWED_MESSAGE_ROLES: - SYSTEM = "system" - USER = "user" - ASSISTANT = "assistant" - TOOL = "tool" - - -GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { - GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], - GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], - GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], - GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], -} - -GEN_AI_MESSAGE_ROLE_MAPPING = {} -for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): - for source_role in source_roles: - GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role - - -def parse_data_uri(url: str) -> "Tuple[str, str]": - """ - Parse a data URI and return (mime_type, content). - - Data URI format (RFC 2397): data:[][;base64], - - Examples: - data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") - data:text/plain,Hello → ("text/plain", "Hello") - data:;base64,SGVsbG8= → ("", "SGVsbG8=") - - Raises: - ValueError: If the URL is not a valid data URI (missing comma separator) - """ - if "," not in url: - raise ValueError("Invalid data URI: missing comma separator") - - header, content = url.split(",", 1) - - # Extract mime type from header - # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" - # Remove "data:" prefix, then take everything before the first semicolon - if header.startswith("data:"): - mime_part = header[5:] # Remove "data:" prefix - else: - mime_part = header - - mime_type = mime_part.split(";")[0] - - return mime_type, content - - -def get_modality_from_mime_type(mime_type: str) -> str: - """ - Infer the content modality from a MIME type string. - - Args: - mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") - - Returns: - One of: "image", "audio", "video", or "document" - Defaults to "image" for unknown or empty MIME types. - - Examples: - "image/jpeg" -> "image" - "audio/mp3" -> "audio" - "video/mp4" -> "video" - "application/pdf" -> "document" - "text/plain" -> "document" - """ - if not mime_type: - return "image" # Default fallback - - mime_lower = mime_type.lower() - if mime_lower.startswith("image/"): - return "image" - elif mime_lower.startswith("audio/"): - return "audio" - elif mime_lower.startswith("video/"): - return "video" - elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): - return "document" - else: - return "image" # Default fallback for unknown types - - -def transform_openai_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an OpenAI/LiteLLM content part to Sentry's standardized format. - - This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. - - Input format: - - {"type": "image_url", "image_url": {"url": "..."}} - - {"type": "image_url", "image_url": "..."} (string shorthand) - - Output format (one of): - - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from OpenAI/LiteLLM - - Returns: - A transformed dictionary in standardized format, or None if the format - is not OpenAI image_url format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type != "image_url": - return None - - image_url_data = content_part.get("image_url") - if isinstance(image_url_data, str): - url = image_url_data - elif isinstance(image_url_data, dict): - url = image_url_data.get("url", "") - else: - return None - - if not url: - return None - - # Check if it's a data URI (base64 encoded) - if url.startswith("data:"): - try: - mime_type, content = parse_data_uri(url) - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": content, - } - except ValueError: - # If parsing fails, return as URI - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - else: - # Regular URL - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - - -def transform_anthropic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an Anthropic content part to Sentry's standardized format. - - This handles the Anthropic image and document formats with source dictionaries. - - Input format: - - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} - - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} - - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} - - {"type": "document", "source": {...}} (same source formats) - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from Anthropic - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Anthropic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "document") or "source" not in content_part: - return None - - source = content_part.get("source") - if not isinstance(source, dict): - return None - - source_type = source.get("type") - media_type = source.get("media_type", "") - modality = ( - "document" - if block_type == "document" - else get_modality_from_mime_type(media_type) - ) - - if source_type == "base64": - return { - "type": "blob", - "modality": modality, - "mime_type": media_type, - "content": source.get("data", ""), - } - elif source_type == "url": - return { - "type": "uri", - "modality": modality, - "mime_type": media_type, - "uri": source.get("url", ""), - } - elif source_type == "file": - return { - "type": "file", - "modality": modality, - "mime_type": media_type, - "file_id": source.get("file_id", ""), - } - - return None - - -def transform_google_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a Google GenAI content part to Sentry's standardized format. - - This handles the Google GenAI inline_data and file_data formats. - - Input format: - - {"inline_data": {"mime_type": "...", "data": "..."}} - - {"file_data": {"mime_type": "...", "file_uri": "..."}} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from Google GenAI - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Google format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Handle Google inline_data format - if "inline_data" in content_part: - inline_data = content_part.get("inline_data") - if isinstance(inline_data, dict): - mime_type = inline_data.get("mime_type", "") - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": inline_data.get("data", ""), - } - return None - - # Handle Google file_data format - if "file_data" in content_part: - file_data = content_part.get("file_data") - if isinstance(file_data, dict): - mime_type = file_data.get("mime_type", "") - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_data.get("file_uri", ""), - } - return None - - return None - - -def transform_generic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a generic/LangChain-style content part to Sentry's standardized format. - - This handles generic formats where the type indicates the modality and - the data is provided via direct base64, url, or file_id fields. - - Input format: - - {"type": "image", "base64": "...", "mime_type": "..."} - - {"type": "audio", "url": "...", "mime_type": "..."} - - {"type": "video", "base64": "...", "mime_type": "..."} - - {"type": "file", "file_id": "...", "mime_type": "..."} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part in generic format - - Returns: - A transformed dictionary in standardized format, or None if the format - is not generic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "audio", "video", "file"): - return None - - # Ensure it's not Anthropic format (which also uses type: "image") - if "source" in content_part: - return None - - mime_type = content_part.get("mime_type", "") - modality = block_type if block_type != "file" else "document" - - # Check for base64 encoded content - if "base64" in content_part: - return { - "type": "blob", - "modality": modality, - "mime_type": mime_type, - "content": content_part.get("base64", ""), - } - # Check for URL reference - elif "url" in content_part: - return { - "type": "uri", - "modality": modality, - "mime_type": mime_type, - "uri": content_part.get("url", ""), - } - # Check for file_id reference - elif "file_id" in content_part: - return { - "type": "file", - "modality": modality, - "mime_type": mime_type, - "file_id": content_part.get("file_id", ""), - } - - return None - - -def transform_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a content part from various AI SDK formats to Sentry's standardized format. - - This is a heuristic dispatcher that detects the format and delegates to the - appropriate SDK-specific transformer. For direct SDK integration, prefer using - the specific transformers directly: - - transform_openai_content_part() for OpenAI/LiteLLM - - transform_anthropic_content_part() for Anthropic - - transform_google_content_part() for Google GenAI - - transform_generic_content_part() for LangChain and other generic formats - - Detection order: - 1. OpenAI: type == "image_url" - 2. Google: "inline_data" or "file_data" keys present - 3. Anthropic: type in ("image", "document") with "source" key - 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from an AI SDK - - Returns: - A transformed dictionary in standardized format, or None if the format - is unrecognized or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Try OpenAI format first (most common, clear indicator) - result = transform_openai_content_part(content_part) - if result is not None: - return result - - # Try Google format (unique keys make it easy to detect) - result = transform_google_content_part(content_part) - if result is not None: - return result - - # Try Anthropic format (has "source" key) - result = transform_anthropic_content_part(content_part) - if result is not None: - return result - - # Try generic format as fallback - result = transform_generic_content_part(content_part) - if result is not None: - return result - - # Unrecognized format - return None - - -def transform_message_content(content: "Any") -> "Any": - """ - Transform message content, handling both string content and list of content blocks. - - For list content, each item is transformed using transform_content_part(). - Items that cannot be transformed (return None) are kept as-is. - - Args: - content: Message content - can be a string, list of content blocks, or other - - Returns: - - String content: returned as-is - - List content: list with each transformable item converted to standardized format - - Other: returned as-is - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - return transformed - - return content - - -def _normalize_data(data: "Any", unpack: bool = True) -> "Any": - # convert pydantic data (e.g. OpenAI v1+) to json compatible format - if hasattr(data, "model_dump"): - # Check if it's a class (type) rather than an instance - # Model classes can be passed as arguments (e.g., for schema definitions) - if inspect.isclass(data): - return f"" - - try: - return _normalize_data(data.model_dump(), unpack=unpack) - except Exception as e: - logger.warning("Could not convert pydantic data to JSON: %s", e) - return data if isinstance(data, (int, float, bool, str)) else str(data) - - if isinstance(data, list): - if unpack and len(data) == 1: - return _normalize_data(data[0], unpack=unpack) # remove empty dimensions - return list(_normalize_data(x, unpack=unpack) for x in data) - - if isinstance(data, dict): - return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} - - return data if isinstance(data, (int, float, bool, str)) else str(data) - - -def set_data_normalized( - span: "Union[Span, StreamedSpan]", - key: str, - value: "Any", - unpack: bool = True, -) -> None: - normalized = _normalize_data(value, unpack=unpack) - if isinstance(normalized, (int, float, bool, str)): - _set_span_data_attribute(span, key, normalized) - else: - _set_span_data_attribute(span, key, json.dumps(normalized)) - - -def _set_span_data_attribute( - span: "Union[Span, StreamedSpan]", key: str, value: "Any" -) -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(key, value) - else: - span.set_data(key, value) - - -def normalize_message_role(role: str) -> str: - """ - Normalize a message role to one of the 4 allowed gen_ai role values. - Maps "ai" -> "assistant" and keeps other standard roles unchanged. - """ - return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) - - -def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": - """ - Normalize roles in a list of messages to use standard gen_ai role values. - Creates a deep copy to avoid modifying the original messages. - """ - normalized_messages = [] - for message in messages: - if not isinstance(message, dict): - normalized_messages.append(message) - continue - normalized_message = message.copy() - if "role" in message: - normalized_message["role"] = normalize_message_role(message["role"]) - normalized_messages.append(normalized_message) - - return normalized_messages - - -def get_start_span_function() -> "Callable[..., Any]": - current_span = sentry_sdk.get_current_span() - - transaction_exists = ( - current_span is not None and current_span.containing_transaction is not None - ) - return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction - - -def _truncate_single_message_content_if_present( - message: "Dict[str, Any]", max_chars: int -) -> "Dict[str, Any]": - """ - Truncate a message's content to at most `max_chars` characters and append an - ellipsis if truncation occurs. - """ - if not isinstance(message, dict) or "content" not in message: - return message - content = message["content"] - - if isinstance(content, str): - if len(content) <= max_chars: - return message - message["content"] = content[:max_chars] + "..." - return message - - if isinstance(content, list): - remaining = max_chars - for item in content: - if isinstance(item, dict) and "text" in item: - text = item["text"] - if isinstance(text, str): - if len(text) > remaining: - item["text"] = text[:remaining] + "..." - remaining = 0 - else: - remaining -= len(text) - return message - - return message - - -def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: - """ - Find the index of the first message that would exceed the max bytes limit. - Compute the individual message sizes, and return the index of the first message from the back - of the list that would exceed the max bytes limit. - """ - running_sum = 0 - for idx in range(len(messages) - 1, -1, -1): - size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) - running_sum += size - if running_sum > max_bytes: - return idx + 1 - - return 0 - - -def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: - """ - Some content blocks contain an image_url property with base64 content as its value. - This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. - """ - if item.get("type") != "image_url": - return False - - image_url_val = item.get("image_url") - image_url = ( - image_url_val.get("url", "") - if isinstance(image_url_val, dict) - else (image_url_val or "") - ) - data_url_match = DATA_URL_BASE64_REGEX.match(image_url) - - return bool(data_url_match) - - -def redact_blob_message_parts( - messages: "List[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Redact blob message parts from the messages by replacing blob content with "[Filtered]". - - This function creates a deep copy of messages that contain blob content to avoid - mutating the original message dictionaries. Messages without blob content are - returned as-is to minimize copying overhead. - - e.g: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "data:image/jpeg;base64,..." - } - ] - } - becomes: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "[Filtered]" - } - ] - } - """ - - # First pass: check if any message contains blob content - has_blobs = False - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and ( - item.get("type") == "blob" or _is_image_type_with_blob_content(item) - ): - has_blobs = True - break - if has_blobs: - break - - # If no blobs found, return original messages to avoid unnecessary copying - if not has_blobs: - return messages - - # Deep copy messages to avoid mutating the original - messages_copy = deepcopy(messages) - - # Second pass: redact blob content in the copy - for message in messages_copy: - if not isinstance(message, dict): - continue - - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict): - if item.get("type") == "blob": - item["content"] = BLOB_DATA_SUBSTITUTE - elif _is_image_type_with_blob_content(item): - if isinstance(item["image_url"], dict): - item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE - else: - item["image_url"] = BLOB_DATA_SUBSTITUTE - - return messages_copy - - -def truncate_messages_by_size( - messages: "List[Dict[str, Any]]", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Tuple[List[Dict[str, Any]], int]": - """ - Returns a truncated messages list, consisting of - - the last message, with its content truncated to `max_single_message_chars` characters, - if the last message's size exceeds `max_bytes` bytes; otherwise, - - the maximum number of messages, starting from the end of the `messages` list, whose total - serialized size does not exceed `max_bytes` bytes. - - In the single message case, the serialized message size may exceed `max_bytes`, because - truncation is based only on character count in that case. - """ - serialized_json = json.dumps(messages, separators=(",", ":")) - current_size = len(serialized_json.encode("utf-8")) - - if current_size <= max_bytes: - return messages, 0 - - truncation_index = _find_truncation_index(messages, max_bytes) - if truncation_index < len(messages): - truncated_messages = messages[truncation_index:] - else: - truncation_index = len(messages) - 1 - truncated_messages = messages[-1:] - - if len(truncated_messages) == 1: - truncated_messages[0] = _truncate_single_message_content_if_present( - deepcopy(truncated_messages[0]), max_chars=max_single_message_chars - ) - - return truncated_messages, truncation_index - - -def truncate_and_annotate_messages( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_message = _truncate_single_message_content_if_present( - deepcopy(messages[-1]), max_chars=max_single_message_chars - ) - if len(messages) > 1: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return [truncated_message] - - -def truncate_and_annotate_embedding_inputs( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) - if removed_count > 0: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return truncated_messages - - -def set_conversation_id(conversation_id: str) -> None: - """ - Set the conversation_id in the scope. - """ - scope = sentry_sdk.get_current_scope() - scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py deleted file mode 100644 index 5556b11ace..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/api.py +++ /dev/null @@ -1,573 +0,0 @@ -import inspect -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import Client, tracing_utils -from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.crons import monitor -from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.traces import get_current_span as _get_current_streamed_span -from sentry_sdk.tracing import NoOpSpan, Transaction, trace - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Optional, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - MeasurementUnit, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span, TransactionKwargs - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) -else: - - def overload(x: "T") -> "T": - return x - - -# When changing this, update __all__ in __init__.py too -__all__ = [ - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - - -def scopemethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, - inspect.getdoc(getattr(Scope, f.__name__)), - ) - return f - - -def clientmethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, - inspect.getdoc(getattr(Client, f.__name__)), - ) - return f - - -@scopemethod -def get_client() -> "BaseClient": - return Scope.get_client() - - -def is_initialized() -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether Sentry has been initialized or not. - - If a client is available and the client is active - (meaning it is configured to send data) then - Sentry is initialized. - """ - return get_client().is_active() - - -@scopemethod -def get_global_scope() -> "Scope": - return Scope.get_global_scope() - - -@scopemethod -def get_isolation_scope() -> "Scope": - return Scope.get_isolation_scope() - - -@scopemethod -def get_current_scope() -> "Scope": - return Scope.get_current_scope() - - -@scopemethod -def last_event_id() -> "Optional[str]": - """ - See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding - this method's limitations. - """ - return Scope.last_event_id() - - -@scopemethod -def capture_event( - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) - - -@scopemethod -def capture_message( - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_message( - message, level, scope=scope, **scope_kwargs - ) - - -@scopemethod -def capture_exception( - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) - - -@scopemethod -def add_attachment( - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, -) -> None: - return get_isolation_scope().add_attachment( - bytes, filename, path, content_type, add_to_transactions - ) - - -@scopemethod -def add_breadcrumb( - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", -) -> None: - return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - -@overload -def configure_scope() -> "ContextManager[Scope]": - pass - - -@overload -def configure_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def configure_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - warnings.warn( - "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", - DeprecationWarning, - stacklevel=2, - ) - - scope = get_isolation_scope() - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - -@overload -def push_scope() -> "ContextManager[Scope]": - pass - - -@overload -def push_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def push_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - warnings.warn( - "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", - DeprecationWarning, - stacklevel=2, - ) - - if callback is not None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with push_scope() as scope: - callback(scope) - return None - - return _ScopeManager() - - -@scopemethod -def set_attribute(attribute: str, value: "Any") -> None: - """ - Set an attribute. - - Any attributes-based telemetry (logs, metrics) captured in this scope will - include this attribute. - """ - return get_isolation_scope().set_attribute(attribute, value) - - -@scopemethod -def remove_attribute(attribute: str) -> None: - """ - Remove an attribute. - - If the attribute doesn't exist, this function will not have any effect and - it will also not raise an exception. - """ - return get_isolation_scope().remove_attribute(attribute) - - -@scopemethod -def set_tag(key: str, value: "Any") -> None: - return get_isolation_scope().set_tag(key, value) - - -@scopemethod -def set_tags(tags: "Mapping[str, object]") -> None: - return get_isolation_scope().set_tags(tags) - - -@scopemethod -def set_context(key: str, value: "Dict[str, Any]") -> None: - return get_isolation_scope().set_context(key, value) - - -@scopemethod -def set_extra(key: str, value: "Any") -> None: - return get_isolation_scope().set_extra(key, value) - - -@scopemethod -def set_user(value: "Optional[Dict[str, Any]]") -> None: - return get_isolation_scope().set_user(value) - - -@scopemethod -def set_level(value: "LogLevelStr") -> None: - return get_isolation_scope().set_level(value) - - -@clientmethod -def flush( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return get_client().flush(timeout=timeout, callback=callback) - - -@clientmethod -async def flush_async( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return await get_client().flush_async(timeout=timeout, callback=callback) - - -@scopemethod -def start_span( - **kwargs: "Any", -) -> "Span": - return get_current_scope().start_span(**kwargs) - - -@scopemethod -def start_transaction( - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", -) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction on the current scope. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - return get_current_scope().start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - -def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - transaction = get_current_scope().transaction - if transaction is not None: - transaction.set_measurement(name, value, unit) - - -def get_current_span( - scope: "Optional[Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - return tracing_utils.get_current_span(scope) - - -def get_traceparent() -> "Optional[str]": - """ - Returns the traceparent either from the active span or from the scope. - """ - return get_current_scope().get_traceparent() - - -def get_baggage() -> "Optional[str]": - """ - Returns Baggage either from the active span or from the scope. - """ - baggage = get_current_scope().get_baggage() - if baggage is not None: - return baggage.serialize() - - return None - - -def continue_trace( - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", -) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers, op, name, source, origin - ) - - -@scopemethod -def start_session( - session_mode: str = "application", -) -> None: - return get_isolation_scope().start_session(session_mode=session_mode) - - -@scopemethod -def end_session() -> None: - return get_isolation_scope().end_session() - - -@scopemethod -def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: - return get_current_scope().set_transaction_name(name, source) - - -def update_current_span( - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, - data: "Optional[dict[str, Any]]" = None, -) -> None: - """ - Update the current active span with the provided parameters. - - This function allows you to modify properties of the currently active span. - If no span is currently active, this function will do nothing. - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, the span's operation will - remain unchanged. - :type op: str or None - - :param name: The human-readable name/description for the span. This provides - more specific details about what the span represents (e.g., "GET /api/users", - "SELECT * FROM users"). If not provided, the span's name will remain unchanged. - :type name: str or None - - :param data: A dictionary of key-value pairs to add as data to the span. This - data will be merged with any existing span data. If not provided, - no data will be added. - - .. deprecated:: 2.35.0 - Use ``attributes`` instead. The ``data`` parameter will be removed - in a future version. - :type data: dict[str, Union[str, int, float, bool]] or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes will be merged with any existing span data. If not provided, - no attributes will be added. - :type attributes: dict[str, Union[str, int, float, bool]] or None - - :returns: None - - .. versionadded:: 2.35.0 - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP - - sentry_sdk.update_current_span( - op=OP.FUNCTION, - name="process_user_data", - attributes={"user_id": 123, "batch_size": 50} - ) - """ - if isinstance(_get_current_streamed_span(), StreamedSpan): - warnings.warn( - "The `update_current_span` API isn't available in streaming mode. " - "Retrieve the current span with get_current_span() and use its API " - "directly.", - DeprecationWarning, - stacklevel=2, - ) - return - - current_span = get_current_span() - - if current_span is None: - return - - if op is not None: - current_span.op = op - - if name is not None: - # internally it is still description - current_span.description = name - - if data is not None and attributes is not None: - raise ValueError( - "Cannot provide both `data` and `attributes`. Please use only `attributes`." - ) - - if data is not None: - warnings.warn( - "The `data` parameter is deprecated. Please use `attributes` instead.", - DeprecationWarning, - stacklevel=2, - ) - attributes = data - - if attributes is not None: - current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py deleted file mode 100644 index 4d69d3acf2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/attachments.py +++ /dev/null @@ -1,71 +0,0 @@ -import mimetypes -import os -from typing import TYPE_CHECKING - -from sentry_sdk.envelope import Item, PayloadRef - -if TYPE_CHECKING: - from typing import Callable, Optional, Union - - -class Attachment: - """Additional files/data to send along with an event. - - This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. - config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with - all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are - captured within the ``Scope``. - - To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for - ``add_attachment`` are the same as the parameters for this class's constructor. - - :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless - ``path`` is provided. - :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. - :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. - :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` - parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. - :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. - """ - - def __init__( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - if bytes is None and path is None: - raise TypeError("path or raw bytes required for attachment") - if filename is None and path is not None: - filename = os.path.basename(path) - if filename is None: - raise TypeError("filename is required for attachment") - if content_type is None: - content_type = mimetypes.guess_type(filename)[0] - self.bytes = bytes - self.filename = filename - self.path = path - self.content_type = content_type - self.add_to_transactions = add_to_transactions - - def to_envelope_item(self) -> "Item": - """Returns an envelope item for this attachment.""" - payload: "Union[None, PayloadRef, bytes]" = None - if self.bytes is not None: - if callable(self.bytes): - payload = self.bytes() - else: - payload = self.bytes - else: - payload = PayloadRef(path=self.path) - return Item( - payload=payload, - type="attachment", - content_type=self.content_type, - filename=self.filename, - ) - - def __repr__(self) -> str: - return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py deleted file mode 100644 index 92cb42277e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/client.py +++ /dev/null @@ -1,1479 +0,0 @@ -import json -import os -import platform -import random -import socket -import sys -import uuid -import warnings -from collections.abc import Iterable, Mapping -from datetime import datetime, timezone -from importlib import import_module -from typing import TYPE_CHECKING, Dict, List, cast, overload - -from sentry_sdk._compat import check_uwsgi_thread_support -from sentry_sdk._metrics_batcher import MetricsBatcher -from sentry_sdk._span_batcher import SpanBatcher -from sentry_sdk.consts import ( - DEFAULT_MAX_VALUE_LENGTH, - DEFAULT_OPTIONS, - INSTRUMENTER, - SPANDATA, - SPANSTATUS, - VERSION, - ClientConstructor, -) -from sentry_sdk.data_collection import ( - _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 -from sentry_sdk.monitor import Monitor -from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler -from sentry_sdk.profiler.transaction_profiler import ( - Profile, - has_profiling_enabled, - setup_profiler, -) -from sentry_sdk.scrubber import EventScrubber -from sentry_sdk.serializer import serialize -from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import trace -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import ( - AsyncHttpTransport, - HttpTransportCore, - make_transport, -) -from sentry_sdk.utils import ( - AnnotatedValue, - ContextVar, - capture_internal_exceptions, - current_stacktrace, - datetime_from_isoformat, - env_to_bool, - format_timestamp, - get_before_send_log, - get_before_send_metric, - get_before_send_span, - get_default_release, - get_sdk_name, - get_type_name, - handle_in_app, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union - - from sentry_sdk._log_batcher import LogBatcher - from sentry_sdk._metrics_batcher import MetricsBatcher - from sentry_sdk._types import ( - Event, - EventDataCategory, - Hint, - Log, - Metric, - SDKInfo, - SerializedAttributeValue, - ) - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.session import Session - from sentry_sdk.spotlight import SpotlightClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Item, Transport - from sentry_sdk.utils import Dsn - - I = TypeVar("I", bound=Integration) # noqa: E741 - -_client_init_debug = ContextVar("client_init_debug") - - -SDK_INFO: "SDKInfo" = { - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() - "version": VERSION, - "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], -} - - -def _serialized_v1_attribute_to_serialized_v2_attribute( - attribute_value: "Any", -) -> "Optional[SerializedAttributeValue]": - if isinstance(attribute_value, bool): - return { - "value": attribute_value, - "type": "boolean", - } - - if isinstance(attribute_value, int): - return { - "value": attribute_value, - "type": "integer", - } - - if isinstance(attribute_value, float): - return { - "value": attribute_value, - "type": "double", - } - - if isinstance(attribute_value, str): - return { - "value": attribute_value, - "type": "string", - } - - if isinstance(attribute_value, list): - if not attribute_value: - return {"value": [], "type": "array"} - - ty = type(attribute_value[0]) - if ty in (int, str, bool, float) and all( - type(v) is ty for v in attribute_value - ): - return { - "value": attribute_value, - "type": "array", - } - - # Types returned when the serializer for V1 span attributes recurses into some container types. - if isinstance(attribute_value, (dict, list)): - return { - "value": json.dumps(attribute_value), - "type": "string", - } - - return None - - -def _serialized_v1_span_to_serialized_v2_span( - span: "dict[str, Any]", event: "Event" -) -> "dict[str, Any]": - # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". - res: "dict[str, Any]" = { - "status": SpanStatus.OK.value, - "is_segment": False, - } - - if "trace_id" in span: - res["trace_id"] = span["trace_id"] - - if "span_id" in span: - res["span_id"] = span["span_id"] - - if "description" in span: - description = span["description"] - - if description is None and "op" in span: - description = span["op"] - - res["name"] = description - - if "start_timestamp" in span: - start_timestamp = None - try: - start_timestamp = datetime_from_isoformat(span["start_timestamp"]) - except Exception: - pass - - if start_timestamp is not None: - res["start_timestamp"] = start_timestamp.timestamp() - - if "timestamp" in span: - end_timestamp = None - try: - end_timestamp = datetime_from_isoformat(span["timestamp"]) - except Exception: - pass - - if end_timestamp is not None: - res["end_timestamp"] = end_timestamp.timestamp() - - if "parent_span_id" in span: - res["parent_span_id"] = span["parent_span_id"] - - if "status" in span and span["status"] != SPANSTATUS.OK: - res["status"] = "error" - - attributes: "Dict[str, Any]" = {} - - if "op" in span: - attributes["sentry.op"] = span["op"] - if "origin" in span: - attributes["sentry.origin"] = span["origin"] - - span_data = span.get("data") - if isinstance(span_data, dict): - attributes.update(span_data) - - span_tags = span.get("tags") - if isinstance(span_tags, dict): - attributes.update(span_tags) - - # See Scope._apply_user_attributes_to_telemetry() for user attributes. - user = event.get("user") - if isinstance(user, dict): - if "id" in user: - attributes["user.id"] = user["id"] - if "username" in user: - attributes["user.name"] = user["username"] - if "email" in user: - attributes["user.email"] = user["email"] - - # See Scope.set_global_attributes() for release, environment, and SDK metadata. - if "release" in event: - attributes["sentry.release"] = event["release"] - if "environment" in event: - attributes["sentry.environment"] = event["environment"] - if "server_name" in event: - attributes["server.address"] = event["server_name"] - if "transaction" in event: - attributes["sentry.segment.name"] = event["transaction"] - - trace_context = event.get("contexts", {}).get("trace", {}) - if "span_id" in trace_context: - attributes["sentry.segment.id"] = trace_context["span_id"] - - sdk_info = event.get("sdk") - if isinstance(sdk_info, dict): - if "name" in sdk_info: - attributes["sentry.sdk.name"] = sdk_info["name"] - if "version" in sdk_info: - attributes["sentry.sdk.version"] = sdk_info["version"] - - attributes["process.runtime.name"] = platform.python_implementation() - attributes["process.runtime.version"] = ( - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ) - - if not attributes: - return res - - res["attributes"] = {} - for key, value in attributes.items(): - converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) - if converted_value is None: - continue - - res["attributes"][key] = converted_value - - # Remove redundant attribute, as status is stored in the status field. - if "status" in res["attributes"]: - del res["attributes"]["status"] - - return res - - -def _split_gen_ai_spans( - event_opt: "Event", -) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": - if "spans" not in event_opt: - return None - - spans: "Any" = event_opt["spans"] - if isinstance(spans, AnnotatedValue): - spans = spans.value - - if not isinstance(spans, Iterable): - return None - - non_gen_ai_spans = [] - gen_ai_spans = [] - for span in spans: - if not isinstance(span, dict): - non_gen_ai_spans.append(span) - continue - - span_op = span.get("op") - if isinstance(span_op, str) and span_op.startswith("gen_ai."): - gen_ai_spans.append(span) - else: - non_gen_ai_spans.append(span) - - return non_gen_ai_spans, gen_ai_spans - - -def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": - if args and (isinstance(args[0], (bytes, str)) or args[0] is None): - dsn: "Optional[str]" = args[0] - args = args[1:] - else: - dsn = None - - if len(args) > 1: - raise TypeError("Only single positional argument is expected") - - rv = dict(DEFAULT_OPTIONS) - options = dict(*args, **kwargs) - if dsn is not None and options.get("dsn") is None: - options["dsn"] = dsn - - for key, value in options.items(): - if key not in rv: - raise TypeError("Unknown option %r" % (key,)) - - rv[key] = value - - if rv["dsn"] is None: - rv["dsn"] = os.environ.get("SENTRY_DSN") - - if rv["release"] is None: - rv["release"] = get_default_release() - - if rv["environment"] is None: - rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" - - if rv["debug"] is None: - rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False - - if rv["server_name"] is None and hasattr(socket, "gethostname"): - rv["server_name"] = socket.gethostname() - - if rv["instrumenter"] is None: - rv["instrumenter"] = INSTRUMENTER.SENTRY - - if rv["project_root"] is None: - try: - project_root = os.getcwd() - except Exception: - project_root = None - - rv["project_root"] = project_root - - 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"] - ) - - if rv["socket_options"] and not isinstance(rv["socket_options"], list): - logger.warning( - "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." - ) - rv["socket_options"] = None - - if rv["keep_alive"] is None: - rv["keep_alive"] = ( - env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False - ) - - if rv["enable_tracing"] is not None: - warnings.warn( - "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", - DeprecationWarning, - stacklevel=2, - ) - - if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): - warnings.warn( - "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", - stacklevel=2, - ) - - return rv - - -try: - # Python 3.6+ - module_not_found_error = ModuleNotFoundError -except Exception: - # Older Python versions - module_not_found_error = ImportError # type: ignore - - -class BaseClient: - """ - .. versionadded:: 2.0.0 - - The basic definition of a client that is used for sending data to Sentry. - """ - - spotlight: "Optional[SpotlightClient]" = None - - def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: - self.options: "Dict[str, Any]" = ( - options if options is not None else DEFAULT_OPTIONS - ) - - self.transport: "Optional[Transport]" = None - self.monitor: "Optional[Monitor]" = None - self.log_batcher: "Optional[LogBatcher]" = None - self.metrics_batcher: "Optional[MetricsBatcher]" = None - self.span_batcher: "Optional[SpanBatcher]" = None - self.integrations: "dict[str, Integration]" = {} - - def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": - return {"options": {}} - - def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: - pass - - @property - def dsn(self) -> "Optional[str]": - return None - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - return None - - def should_send_default_pii(self) -> bool: - return False - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return False - - def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - return None - - def _capture_log(self, log: "Log", scope: "Scope") -> None: - pass - - def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: - pass - - def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: - pass - - def capture_session(self, *args: "Any", **kwargs: "Any") -> None: - return None - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, name_or_class: "Union[str, type[Integration]]" - ) -> "Optional[Integration]": - return None - - def close(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def flush(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def close_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def __enter__(self) -> "BaseClient": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - return None - - -class NonRecordingClient(BaseClient): - """ - .. versionadded:: 2.0.0 - - A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. - """ - - pass - - -class _Client(BaseClient): - """ - The client is internally responsible for capturing the events and - forwarding them to sentry through the configured transport. It takes - the client options as keyword arguments and optionally the DSN as first - argument. - - Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) - """ - - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - super(_Client, self).__init__(options=get_options(*args, **kwargs)) - self._init_impl() - - def __getstate__(self) -> "Any": - return {"options": self.options} - - def __setstate__(self, state: "Any") -> None: - self.options = state["options"] - self._init_impl() - - def _setup_instrumentation( - self, functions_to_trace: "Sequence[Dict[str, str]]" - ) -> None: - """ - Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. - """ - for function in functions_to_trace: - class_name = None - function_qualname = function["qualified_name"] - - if "." not in function_qualname: - logger.warning( - "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", - function_qualname, - ) - continue - - module_name, function_name = function_qualname.rsplit(".", 1) - - try: - # Try to import module and function - # ex: "mymodule.submodule.funcname" - - module_obj = import_module(module_name) - function_obj = getattr(module_obj, function_name) - setattr(module_obj, function_name, trace(function_obj)) - logger.debug("Enabled tracing for %s", function_qualname) - except module_not_found_error: - try: - # Try to import a class - # ex: "mymodule.submodule.MyClassName.member_function" - - module_name, class_name = module_name.rsplit(".", 1) - module_obj = import_module(module_name) - class_obj = getattr(module_obj, class_name) - function_obj = getattr(class_obj, function_name) - function_type = type(class_obj.__dict__[function_name]) - traced_function = trace(function_obj) - - if function_type in (staticmethod, classmethod): - traced_function = staticmethod(traced_function) - - setattr(class_obj, function_name, traced_function) - setattr(module_obj, class_name, class_obj) - logger.debug("Enabled tracing for %s", function_qualname) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - def _init_impl(self) -> None: - old_debug = _client_init_debug.get(False) - - def _capture_envelope(envelope: "Envelope") -> None: - if self.spotlight is not None: - self.spotlight.capture_envelope(envelope) - if self.transport is not None: - self.transport.capture_envelope(envelope) - - def _record_lost_event( - reason: str, - data_category: "EventDataCategory", - item: "Optional[Item]" = None, - quantity: int = 1, - ) -> None: - if self.transport is not None: - self.transport.record_lost_event( - reason=reason, - data_category=data_category, - item=item, - quantity=quantity, - ) - - try: - _client_init_debug.set(self.options["debug"]) - self.transport = make_transport(self.options) - - self.monitor = None - if self.transport: - if self.options["enable_backpressure_handling"]: - self.monitor = Monitor(self.transport) - - # Setup Spotlight before creating batchers so _capture_envelope can use it. - # setup_spotlight handles all config/env var resolution per the SDK spec. - from sentry_sdk.spotlight import setup_spotlight - - self.spotlight = setup_spotlight(self.options) - if self.spotlight is not None and not self.options["dsn"]: - sample_all = lambda *_args, **_kwargs: 1.0 - self.options["send_default_pii"] = True - 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, - ) - - self.session_flusher = SessionFlusher(capture_func=_capture_envelope) - - self.log_batcher = None - - if has_logs_enabled(self.options): - from sentry_sdk._log_batcher import LogBatcher - - self.log_batcher = LogBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.metrics_batcher = None - if has_metrics_enabled(self.options): - self.metrics_batcher = MetricsBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.span_batcher = None - if has_span_streaming_enabled(self.options): - self.span_batcher = SpanBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - max_request_body_size = ("always", "never", "small", "medium") - if self.options["max_request_body_size"] not in max_request_body_size: - raise ValueError( - "Invalid value for max_request_body_size. Must be one of {}".format( - max_request_body_size - ) - ) - - if self.options["_experiments"].get("otel_powered_performance", False): - logger.debug( - "[OTel] Enabling experimental OTel-powered performance monitoring." - ) - self.options["instrumenter"] = INSTRUMENTER.OTEL - if ( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - not in _DEFAULT_INTEGRATIONS - ): - _DEFAULT_INTEGRATIONS.append( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", - ) - - self.integrations = setup_integrations( - self.options["integrations"], - with_defaults=self.options["default_integrations"], - with_auto_enabling_integrations=self.options[ - "auto_enabling_integrations" - ], - disabled_integrations=self.options["disabled_integrations"], - options=self.options, - ) - - sdk_name = get_sdk_name(list(self.integrations.keys())) - SDK_INFO["name"] = sdk_name - logger.debug("Setting SDK name to '%s'", sdk_name) - - if has_profiling_enabled(self.options): - try: - setup_profiler(self.options) - except Exception as e: - logger.debug("Can not set up profiler. (%s)", e) - else: - try: - setup_continuous_profiler( - self.options, - sdk_info=SDK_INFO, - capture_func=_capture_envelope, - ) - except Exception as e: - logger.debug("Can not set up continuous profiler. (%s)", e) - - finally: - _client_init_debug.set(old_debug) - - self._setup_instrumentation(self.options.get("functions_to_trace", [])) - - if ( - self.monitor - or self.log_batcher - or self.metrics_batcher - or self.span_batcher - or has_profiling_enabled(self.options) - or isinstance(self.transport, HttpTransportCore) - ): - # If we have anything on that could spawn a background thread, we - # need to check if it's safe to use them. - check_uwsgi_thread_support() - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return True - - def should_send_default_pii(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. - """ - return self.options.get("send_default_pii") or False - - @property - def dsn(self) -> "Optional[str]": - """Returns the configured DSN as string.""" - return self.options["dsn"] - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - """Returns the configured parsed DSN object.""" - return self.transport.parsed_dsn if self.transport else None - - def _prepare_event( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]", - ) -> "Optional[Event]": - previous_total_spans: "Optional[int]" = None - previous_total_breadcrumbs: "Optional[int]" = None - - if event.get("timestamp") is None: - event["timestamp"] = datetime.now(timezone.utc) - - is_transaction = event.get("type") == "transaction" - - if scope is not None: - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - event_ = scope.apply_to_event(event, hint, self.options) - - # one of the event/error processors returned None - if event_ is None: - if self.transport: - self.transport.record_lost_event( - "event_processor", - data_category=("transaction" if is_transaction else "error"), - ) - if is_transaction: - self.transport.record_lost_event( - "event_processor", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - return None - - event = event_ - spans_delta = spans_before - len( - cast(List[Dict[str, object]], event.get("spans", [])) - ) - span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - - if is_transaction and self.transport is not None: - if spans_delta > 0: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) - if span_recorder_dropped_spans > 0: - self.transport.record_lost_event( - "buffer_overflow", - data_category="span", - quantity=span_recorder_dropped_spans, - ) - - dropped_spans: int = span_recorder_dropped_spans + spans_delta - if dropped_spans > 0: - previous_total_spans = spans_before + dropped_spans - if scope._n_breadcrumbs_truncated > 0: - breadcrumbs = event.get("breadcrumbs", {}) - values = ( - breadcrumbs.get("values", []) - if not isinstance(breadcrumbs, AnnotatedValue) - else [] - ) - previous_total_breadcrumbs = ( - len(values) + scope._n_breadcrumbs_truncated - ) - - if ( - not is_transaction - and self.options["attach_stacktrace"] - and "exception" not in event - and "stacktrace" not in event - and "threads" not in event - ): - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=self.options.get( - "include_local_variables", True - ), - max_value_length=self.options.get( - "max_value_length", DEFAULT_MAX_VALUE_LENGTH - ), - ), - "crashed": False, - "current": True, - } - ] - } - - for key in "release", "environment", "server_name", "dist": - if event.get(key) is None and self.options[key] is not None: - event[key] = str(self.options[key]).strip() - if event.get("sdk") is None: - sdk_info = dict(SDK_INFO) - sdk_info["integrations"] = sorted(self.integrations.keys()) - event["sdk"] = sdk_info - - if event.get("platform") is None: - event["platform"] = "python" - - event = handle_in_app( - event, - self.options["in_app_exclude"], - self.options["in_app_include"], - self.options["project_root"], - ) - - if event is not None: - event_scrubber = self.options["event_scrubber"] - if event_scrubber: - event_scrubber.scrub_event(event) - - if scope is not None and scope._gen_ai_original_message_count: - spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) - if isinstance(spans, list): - for span in spans: - span_id = span.get("span_id", None) - span_data = span.get("data", {}) - if ( - span_id - and span_id in scope._gen_ai_original_message_count - and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data - ): - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], - {"len": scope._gen_ai_original_message_count[span_id]}, - ) - if previous_total_spans is not None: - event["spans"] = AnnotatedValue( - event.get("spans", []), {"len": previous_total_spans} - ) - if previous_total_breadcrumbs is not None: - event["breadcrumbs"] = AnnotatedValue( - event.get("breadcrumbs", {"values": []}), - {"len": previous_total_breadcrumbs}, - ) - - # Postprocess the event here so that annotated types do - # generally not surface in before_send - if event is not None: - event = cast( - "Event", - serialize( - cast("Dict[str, Any]", event), - max_request_body_size=self.options.get("max_request_body_size"), - max_value_length=self.options.get("max_value_length"), - custom_repr=self.options.get("custom_repr"), - ), - ) - - before_send = self.options["before_send"] - if ( - before_send is not None - and event is not None - and event.get("type") != "transaction" - ): - new_event = None - with capture_internal_exceptions(): - new_event = before_send(event, hint or {}) - if new_event is None: - logger.info("before send dropped event") - if self.transport: - self.transport.record_lost_event( - "before_send", data_category="error" - ) - - # If this is an exception, reset the DedupeIntegration. It still - # remembers the dropped exception as the last exception, meaning - # that if the same exception happens again and is not dropped - # in before_send, it'd get dropped by DedupeIntegration. - if event.get("exception"): - DedupeIntegration.reset_last_seen() - - event = new_event - - before_send_transaction = self.options["before_send_transaction"] - if ( - before_send_transaction is not None - and event is not None - and event.get("type") == "transaction" - ): - new_event = None - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - with capture_internal_exceptions(): - new_event = before_send_transaction(event, hint or {}) - if new_event is None: - logger.info("before send transaction dropped event") - if self.transport: - self.transport.record_lost_event( - reason="before_send", data_category="transaction" - ) - self.transport.record_lost_event( - reason="before_send", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - else: - spans_delta = spans_before - len(new_event.get("spans", [])) - if spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - reason="before_send", data_category="span", quantity=spans_delta - ) - - event = new_event - - return event - - def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: - exc_info = hint.get("exc_info") - if exc_info is None: - return False - - error = exc_info[0] - error_type_name = get_type_name(exc_info[0]) - error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) - - for ignored_error in self.options["ignore_errors"]: - # String types are matched against the type name in the - # exception only - if isinstance(ignored_error, str): - if ignored_error == error_full_name or ignored_error == error_type_name: - return True - else: - if issubclass(error, ignored_error): - return True - - return False - - def _should_capture( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]" = None, - ) -> bool: - # Transactions are sampled independent of error events. - is_transaction = event.get("type") == "transaction" - if is_transaction: - return True - - ignoring_prevents_recursion = scope is not None and not scope._should_capture - if ignoring_prevents_recursion: - return False - - ignored_by_config_option = self._is_ignored_error(event, hint) - if ignored_by_config_option: - return False - - return True - - def _should_sample_error( - self, - event: "Event", - hint: "Hint", - ) -> bool: - error_sampler = self.options.get("error_sampler", None) - - if callable(error_sampler): - with capture_internal_exceptions(): - sample_rate = error_sampler(event, hint) - else: - sample_rate = self.options["sample_rate"] - - try: - not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate - except NameError: - logger.warning( - "The provided error_sampler raised an error. Defaulting to sampling the event." - ) - - # If the error_sampler raised an error, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - except TypeError: - parameter, verb = ( - ("error_sampler", "returned") - if callable(error_sampler) - else ("sample_rate", "contains") - ) - logger.warning( - "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." - % (parameter, verb, repr(sample_rate)) - ) - - # If the sample_rate has an invalid value, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - - if not_in_sample_rate: - # because we will not sample this event, record a "lost event". - if self.transport: - self.transport.record_lost_event("sample_rate", data_category="error") - - return False - - return True - - def _update_session_from_event( - self, - session: "Session", - event: "Event", - ) -> None: - crashed = False - errored = False - user_agent = None - - exceptions = (event.get("exception") or {}).get("values") - if exceptions: - errored = True - for error in exceptions: - if isinstance(error, AnnotatedValue): - error = error.value or {} - mechanism = error.get("mechanism") - if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: - crashed = True - break - - user = event.get("user") - - if session.user_agent is None: - headers = (event.get("request") or {}).get("headers") - headers_dict = headers if isinstance(headers, dict) else {} - for k, v in headers_dict.items(): - if k.lower() == "user-agent": - user_agent = v - break - - session.update( - status="crashed" if crashed else None, - user=user, - user_agent=user_agent, - errors=session.errors + (errored or crashed), - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - ) -> "Optional[str]": - """Captures an event. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - - :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. - """ - hint = dict(hint or ()) - - if not self._should_capture(event, hint, scope): - return None - - profile = event.pop("profile", None) - - event_id = event.get("event_id") - if event_id is None: - event["event_id"] = event_id = uuid.uuid4().hex - - span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) - event_opt = self._prepare_event(event, hint, scope) - if event_opt is None: - return None - - # whenever we capture an event we also check if the session needs - # to be updated based on that information. - session = scope._session if scope else None - if session: - self._update_session_from_event(session, event) - - is_transaction = event_opt.get("type") == "transaction" - is_checkin = event_opt.get("type") == "check_in" - - if ( - not is_transaction - and not is_checkin - and not self._should_sample_error(event, hint) - ): - return None - - attachments = hint.get("attachments") - - trace_context = event_opt.get("contexts", {}).get("trace") or {} - dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) - - headers: "dict[str, object]" = { - "event_id": event_opt["event_id"], - "sent_at": format_timestamp(datetime.now(timezone.utc)), - } - - if dynamic_sampling_context: - headers["trace"] = dynamic_sampling_context - - envelope = Envelope(headers=headers) - - if is_transaction and isinstance(profile, Profile): - envelope.add_profile(profile.to_json(event_opt, self.options)) - - if is_transaction and not span_recorder_has_gen_ai_span: - envelope.add_transaction(event_opt) - elif is_transaction: - split_spans = _split_gen_ai_spans(event_opt) - if split_spans is None or not split_spans[1]: - envelope.add_transaction(event_opt) - else: - non_gen_ai_spans, gen_ai_spans = split_spans - - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) - - converted_gen_ai_spans = [ - _serialized_v1_span_to_serialized_v2_span(span, event_opt) - for span in gen_ai_spans - if isinstance(span, dict) - ] - - envelope.add_item( - Item( - type=SpanBatcher.TYPE, - content_type=SpanBatcher.CONTENT_TYPE, - headers={ - "item_count": len(converted_gen_ai_spans), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": converted_gen_ai_spans, - }, - ), - ) - ) - - elif is_checkin: - envelope.add_checkin(event_opt) - else: - envelope.add_event(event_opt) - - for attachment in attachments or (): - envelope.add_item(attachment.to_envelope_item()) - - return_value = None - if self.spotlight: - self.spotlight.capture_envelope(envelope) - return_value = event_id - - if self.transport is not None: - self.transport.capture_envelope(envelope) - return_value = event_id - - return return_value - - def _capture_telemetry( - self, - telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", - ty: str, - scope: "Scope", - ) -> None: - """ - Capture attributes-based telemetry (logs, metrics, streamed spans). - - Apply any attributes set on the scope to it, and run the user's - before_send_{telemetry} on it, if applicable. - """ - if telemetry is None: - return - - scope.apply_to_telemetry(telemetry) - - before_send = None - - if ty == "log": - before_send = get_before_send_log(self.options) - serialized = telemetry - - elif ty == "metric": - before_send = get_before_send_metric(self.options) - serialized = telemetry - - elif ty == "span": - before_send = get_before_send_span(self.options) - serialized = telemetry._to_json() # type: ignore[union-attr] - - if before_send is not None: - serialized = before_send(serialized, {}) # type: ignore[arg-type] - - if ty in ("log", "metric"): - # Logs and metrics can be dropped in their respective - # before_send, so if we get None, don't queue them for sending. - if serialized is None: - return - - elif ty == "span" and isinstance(telemetry, StreamedSpan): - # Spans can't be dropped in before_send_span by design. They can - # be altered though (e.g. to sanitize). Only allow changes to - # name and attributes. - if isinstance(serialized, dict) and "name" in serialized: - telemetry.name = serialized["name"] # type: ignore[typeddict-item] - telemetry._attributes = {} - for k, v in (serialized.get("attributes") or {}).items(): - telemetry.set_attribute(k, v) - - else: - logger.debug( - "[Tracing] Invalid return value from before_send_span. Keeping original span." - ) - - serialized = telemetry._to_json() - - batcher = None - if ty == "log": - batcher = self.log_batcher - - elif ty == "metric": - batcher = self.metrics_batcher - - elif ty == "span": - # We need a reference to the segment span in the batcher to populate - # the dynamic sampling context (DSC) - serialized["_segment_span"] = telemetry._segment # type: ignore - batcher = self.span_batcher - - if batcher is not None: - batcher.add(serialized) # type: ignore - - def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: - self._capture_telemetry(log, "log", scope) - - def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: - self._capture_telemetry(metric, "metric", scope) - - def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: - self._capture_telemetry(span, "span", scope) - - def capture_session( - self, - session: "Session", - ) -> None: - if not session.release: - logger.info("Discarded session update because of missing release") - else: - self.session_flusher.add_session(session) - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Optional[Integration]": - """Returns the integration for this client by name or class. - If the client does not have that integration then `None` is returned. - """ - if isinstance(name_or_class, str): - integration_name = name_or_class - elif name_or_class.identifier is not None: - integration_name = name_or_class.identifier - else: - raise ValueError("Integration has no name") - - return self.integrations.get(integration_name) - - def _has_async_transport(self) -> bool: - """Check if the current transport is async.""" - return isinstance(self.transport, AsyncHttpTransport) - - @property - def _batchers(self) -> "tuple[Any, ...]": - return tuple( - b - for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) - if b is not None - ) - - def _close_components(self) -> None: - """Kill all client components in the correct order.""" - self.session_flusher.kill() - for b in self._batchers: - b.kill() - if self.monitor: - self.monitor.kill() - - def _flush_components(self) -> None: - """Flush all client components.""" - self.session_flusher.flush() - for b in self._batchers: - b.flush() - - def close( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush`. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "close() used with AsyncHttpTransport. Use close_async() instead.", - stacklevel=2, - ) - self._flush_components() - else: - self.flush(timeout=timeout, callback=callback) - self._close_components() - self.transport.kill() - self.transport = None - - async def close_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush_async`. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "close_async() used with non-async transport, aborting. Please use close() instead." - ) - return - await self.flush_async(timeout=timeout, callback=callback) - self._close_components() - kill_task = self.transport.kill() # type: ignore - if kill_task is not None: - await kill_task - self.transport = None - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "flush() used with AsyncHttpTransport. Use flush_async() instead.", - stacklevel=2, - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - - self.transport.flush(timeout=timeout, callback=callback) - - async def flush_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "flush_async() used with non-async transport, aborting. Please use flush() instead." - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore - if flush_task is not None: - await flush_task - - def __enter__(self) -> "_Client": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - self.close() - - async def __aenter__(self) -> "_Client": - return self - - async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - await self.close_async() - - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `get_options` is a - # type to have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `Dict[str, Any]` to tell static analyzers about the return type. - - class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 - pass - - class Client(ClientConstructor, _Client): - pass - -else: - # Alias `get_options` for actual usage. Go through the lambda indirection - # to throw PyCharm off of the weakly typed signature (it would otherwise - # discover both the weakly typed signature of `_init` and our faked `init` - # type). - - get_options = (lambda: _get_options)() - Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py deleted file mode 100644 index 759898f6ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/consts.py +++ /dev/null @@ -1,1802 +0,0 @@ -import itertools -from enum import Enum -from typing import TYPE_CHECKING - -DEFAULT_MAX_VALUE_LENGTH = None - -DEFAULT_MAX_STACK_FRAMES = 100 -DEFAULT_ADD_FULL_STACK = False - - -# Also needs to be at the top to prevent circular import -class EndpointType(Enum): - """ - The type of an endpoint. This is an enum, rather than a constant, for historical reasons - (the old /store endpoint). The enum also preserve future compatibility, in case we ever - have a new endpoint. - """ - - ENVELOPE = "envelope" - OTLP_TRACES = "integration/otlp/v1/traces" - - -class CompressionAlgo(Enum): - GZIP = "gzip" - BROTLI = "br" - - -if TYPE_CHECKING: - from typing import ( - AbstractSet, - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - ) - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - from sentry_sdk._types import ( - BreadcrumbProcessor, - ContinuousProfilerMode, - DataCollectionUserOptions, - Event, - EventProcessor, - Hint, - IgnoreSpansConfig, - Log, - Metric, - ProfilerMode, - SpanJSON, - TracesSampler, - TransactionProcessor, - ) - - # Experiments are feature flags to enable and disable certain unstable SDK - # functionality. Changing them from the defaults (`None`) in production - # code is highly discouraged. They are not subject to any stability - # guarantees such as the ones from semantic versioning. - Experiments = TypedDict( - "Experiments", - { - "max_spans": Optional[int], - "max_flags": Optional[int], - "record_sql_params": Optional[bool], - "continuous_profiling_auto_start": Optional[bool], - "continuous_profiling_mode": Optional[ContinuousProfilerMode], - "otel_powered_performance": Optional[bool], - "transport_zlib_compression_level": Optional[int], - "transport_compression_level": Optional[int], - "transport_compression_algo": Optional[CompressionAlgo], - "transport_num_pools": Optional[int], - "transport_http2": Optional[bool], - "transport_async": Optional[bool], - "enable_logs": Optional[bool], - "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], - "enable_metrics": Optional[bool], - "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], - "trace_lifecycle": Optional[Literal["static", "stream"]], - "ignore_spans": Optional[IgnoreSpansConfig], - "before_send_span": Optional[ - Callable[[SpanJSON, Hint], Optional[SpanJSON]] - ], - "suppress_asgi_chained_exceptions": Optional[bool], - "data_collection": Optional[DataCollectionUserOptions], - }, - total=False, - ) - -DEFAULT_QUEUE_SIZE = 100 -DEFAULT_MAX_BREADCRUMBS = 100 -MATCH_ALL = r".*" - -FALSE_VALUES = [ - "false", - "no", - "off", - "n", - "0", -] - - -class SPANTEMPLATE(str, Enum): - DEFAULT = "default" - AI_AGENT = "ai_agent" - AI_TOOL = "ai_tool" - AI_CHAT = "ai_chat" - - def __str__(self) -> str: - return self.value - - -class INSTRUMENTER: - SENTRY = "sentry" - OTEL = "otel" - - -class SPANNAME: - DB_COMMIT = "COMMIT" - DB_ROLLBACK = "ROLLBACK" - - -class SPANDATA: - """ - Additional information describing the type of the span. - See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ - """ - - AI_CITATIONS = "ai.citations" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - References or sources cited by the AI model in its response. - Example: ["Smith et al. 2020", "Jones 2019"] - """ - - AI_DOCUMENTS = "ai.documents" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Documents or content chunks used as context for the AI model. - Example: ["doc1.txt", "doc2.pdf"] - """ - - AI_FINISH_REASON = "ai.finish_reason" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. - - The reason why the model stopped generating. - Example: "length" - """ - - AI_FREQUENCY_PENALTY = "ai.frequency_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_FUNCTION_CALL = "ai.function_call" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_GENERATION_ID = "ai.generation_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. - - Unique identifier for the completion. - Example: "gen_123abc" - """ - - AI_INPUT_MESSAGES = "ai.input_messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. - - The input messages to an LLM call. - Example: [{"role": "user", "message": "hello"}] - """ - - AI_LOGIT_BIAS = "ai.logit_bias" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the logit bias - """ - - AI_METADATA = "ai.metadata" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Extra metadata passed to an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_MODEL_ID = "ai.model_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. - - The unique descriptor of the model being executed. - Example: gpt-4 - """ - - AI_PIPELINE_NAME = "ai.pipeline.name" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. - - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - AI_PREAMBLE = "ai.preamble" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the preamble parameter. - Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. - Example: "You are now a clown." - """ - - AI_PRESENCE_PENALTY = "ai.presence_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_RAW_PROMPTING = "ai.raw_prompting" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Minimize pre-processing done to the prompt sent to the LLM. - Example: true - """ - - AI_RESPONSE_FORMAT = "ai.response_format" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the format of the response - """ - - AI_RESPONSES = "ai.responses" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. - - The responses to an AI model call. Always as a list. - Example: ["hello", "world"] - """ - - AI_SEARCH_QUERIES = "ai.search_queries" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Queries used to search for relevant context or documents. - Example: ["climate change effects", "renewable energy"] - """ - - AI_SEARCH_REQUIRED = "ai.is_search_required" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Boolean indicating if the model needs to perform a search. - Example: true - """ - - AI_SEARCH_RESULTS = "ai.search_results" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Results returned from search queries for context. - Example: ["Result 1", "Result 2"] - """ - - AI_SEED = "ai.seed" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. - - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: 123.45 - """ - - AI_STREAMING = "ai.streaming" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. - - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - AI_TAGS = "ai.tags" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Tags that describe an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_TEMPERATURE = "ai.temperature" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. - - For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. - Example: 0.5 - """ - - AI_TEXTS = "ai.texts" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Raw text inputs provided to the model. - Example: ["What is machine learning?"] - """ - - AI_TOP_K = "ai.top_k" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. - - For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. - Example: 35 - """ - - AI_TOP_P = "ai.top_p" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. - - For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. - Example: 0.5 - """ - - AI_TOOL_CALLS = "ai.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_TOOLS = "ai.tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. - - For an AI model call, the functions that are available - """ - - AI_WARNINGS = "ai.warnings" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Warning messages generated during model execution. - Example: ["Token limit exceeded"] - """ - - CACHE_HIT = "cache.hit" - """ - A boolean indicating whether the requested data was found in the cache. - Example: true - """ - - CACHE_ITEM_SIZE = "cache.item_size" - """ - The size of the requested data in bytes. - Example: 58 - """ - - CACHE_KEY = "cache.key" - """ - The key of the requested data. - Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 - """ - - CLIENT_ADDRESS = "client.address" - """ - Client address of the network connection - IP address or Unix domain socket name. - Example: "10.1.2.80" - """ - - CODE_FILEPATH = "code.filepath" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FILE_PATH instead. - - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FILE_PATH = "code.file.path" - """ - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FUNCTION = "code.function" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead. - - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_FUNCTION_NAME = "code.function.name" - """ - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_LINENO = "code.lineno" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_LINE_NUMBER instead. - - The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. - Example: 42 - """ - - CODE_LINE_NUMBER = "code.line.number" - """ - The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. - Example: 42 - """ - - CODE_NAMESPACE = "code.namespace" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. - - The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. - Example: "http.handler" - """ - - DB_MONGODB_COLLECTION = "db.mongodb.collection" - """ - The MongoDB collection being accessed within the database. - See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes - Example: public.users; customers - """ - - DB_NAME = "db.name" - """ - .. deprecated:: - This attribute is deprecated. Use DB_NAMESPACE instead. - - The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - Example: myDatabase - """ - - DB_NAMESPACE = "db.namespace" - """ - The name of the database being accessed. - Example: "customers" - """ - - DB_DRIVER_NAME = "db.driver.name" - """ - The name of the database driver being used for the connection. - Example: "psycopg2" - """ - - DB_OPERATION = "db.operation" - """ - .. deprecated:: - This attribute is deprecated. Use DB_OPERATION_NAME instead. - - The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: findAndModify, HMSET, SELECT - """ - - DB_OPERATION_NAME = "db.operation.name" - """ - The name of the operation being executed. - Example: "SELECT" - """ - - DB_SYSTEM = "db.system" - """ - .. deprecated:: - This attribute is deprecated. Use DB_SYSTEM_NAME instead. - - An identifier for the database management system (DBMS) product being used. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: postgresql - """ - - DB_QUERY_TEXT = "db.query.text" - """ - The database query being executed. - Example: "SELECT * FROM users WHERE id = $1" - """ - - DB_SYSTEM_NAME = "db.system.name" - """ - An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. - Example: "postgresql" - """ - - DB_USER = "db.user" - """ - The name of the database user used for connecting to the database. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: my_user - """ - - GEN_AI_AGENT_NAME = "gen_ai.agent.name" - """ - The name of the agent being used. - Example: "ResearchAssistant" - """ - - GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" - """ - The unique identifier for the conversation/thread with the AI model. - Example: "conv_abc123" - """ - - GEN_AI_CHOICE = "gen_ai.choice" - """ - The model's response message. - Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" - """ - - GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" - """ - The input to the embeddings operation. - Example: "Hello!" - """ - - GEN_AI_FUNCTION_ID = "gen_ai.function_id" - """ - Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. - Example: "my-awesome-function" - """ - - GEN_AI_OPERATION_NAME = "gen_ai.operation.name" - """ - The name of the operation being performed. - Example: "chat" - """ - - GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" - """ - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" - """ - The reason why the model stopped generating. - Example: "COMPLETE" - """ - - GEN_AI_RESPONSE_ID = "gen_ai.response.id" - """ - Unique identifier for the completion. - Example: "gen_123abc" - """ - - GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" - """ - Exact model identifier used to generate the response - Example: gpt-4o-mini-2024-07-18 - """ - - GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" - """ - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The model's response text messages. - Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] - """ - - GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" - """ - The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. - Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] - """ - - GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" - """ - The time it took to receive the first token from the model. - Example: 0.1 - """ - - GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The tool calls in the model's response. - Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] - """ - - GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. - - The available tools for the model. - Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] - """ - - GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" - """ - The list of source system tool definitions available to the GenAI agent or model. - Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] - """ - - GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" - """ - The frequency penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" - """ - The maximum number of tokens to generate in the response. - Example: 2048 - """ - - GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" - """ - The system instructions passed to the model. - Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] - """ - - GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. - - The messages passed to the model. The "content" can be a string or an array of objects. - Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] - """ - - GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" - """ - The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". - Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] - """ - - GEN_AI_REQUEST_MODEL = "gen_ai.request.model" - """ - The model identifier being used for the request. - Example: "gpt-4-turbo" - """ - - GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" - """ - The presence penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_SEED = "gen_ai.request.seed" - """ - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: "1234567890" - """ - - GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" - """ - The temperature parameter used to control randomness in the output. - Example: 0.7 - """ - - GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" - """ - Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). - Example: 35 - """ - - GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" - """ - The top_p parameter used to control diversity via nucleus sampling. - Example: 1.0 - """ - - GEN_AI_SYSTEM = "gen_ai.system" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. - - The name of the AI system being used. - Example: "openai" - """ - - GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" - """ - The Generative AI provider as identified by the client or server instrumentation. - Example: "openai" - """ - - GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" - """ - The description of the tool being used. - Example: "Searches the web for current information about a topic" - """ - - GEN_AI_TOOL_INPUT = "gen_ai.tool.input" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. - - The input of the tool being used. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" - """ - The arguments of the tool call. It has to be a stringified version of the arguments to the tool. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_NAME = "gen_ai.tool.name" - """ - The name of the tool being used. - Example: "web_search" - """ - - GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. - - The output of the tool being used. - Example: "rainy, 57°F" - """ - - GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" - """ - The result of the tool call. It has to be a stringified version of the result of the tool. - Example: "rainy, 57°F" - """ - - GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" - """ - The number of tokens in the input. - Example: 150 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" - """ - The number of cached tokens in the input. - Example: 50 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" - """ - The number of tokens written to the cache when processing the AI input (prompt). - Example: 100 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" - """ - The number of tokens in the output. - Example: 250 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" - """ - The number of tokens used for reasoning in the output. - Example: 75 - """ - - GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" - """ - The total number of tokens used (input + output). - Example: 400 - """ - - GEN_AI_USER_MESSAGE = "gen_ai.user.message" - """ - The user message passed to the model. - Example: "What's the weather in Paris?" - """ - - HTTP_FRAGMENT = "http.fragment" - """ - The Fragments present in the URL. - Example: #foo=bar - """ - - HTTP_METHOD = "http.method" - """ - .. deprecated:: - This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. - - The HTTP method used. - Example: GET - """ - - HTTP_REQUEST_BODY_DATA = "http.request.body.data" - """ - HTTP request body data. Can be given as string or structural data of any format. - Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" - """ - - HTTP_REQUEST_HEADER = "http.request.header" - """ - Prefix for HTTP request header attributes. The header name (lowercased) is - appended to form the full attribute key. - Example: "http.request.header.content-type" - """ - - HTTP_REQUEST_METHOD = "http.request.method" - """ - The HTTP method used. - Example: GET - """ - - HTTP_QUERY = "http.query" - """ - The Query string present in the URL. - Example: ?foo=bar&bar=baz - """ - - HTTP_STATUS_CODE = "http.response.status_code" - """ - The HTTP status code as an integer. - Example: 418 - """ - - MESSAGING_DESTINATION_NAME = "messaging.destination.name" - """ - The destination name where the message is being consumed from, - e.g. the queue name or topic. - """ - - MESSAGING_MESSAGE_ID = "messaging.message.id" - """ - The message's identifier. - """ - - MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" - """ - The latency between when the task was enqueued and when it was started to be processed. - """ - - MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" - """ - Number of retries/attempts to process a message. - """ - - MESSAGING_SYSTEM = "messaging.system" - """ - The messaging system's name, e.g. `kafka`, `aws_sqs` - """ - - MIDDLEWARE_NAME = "middleware.name" - """ - The middleware's name, e.g. `AuthenticationMiddleware` - """ - - NETWORK_PROTOCOL_NAME = "network.protocol.name" - """ - The application layer protocol name used for the network connection. - Example: "http", "https" - """ - - NETWORK_PEER_ADDRESS = "network.peer.address" - """ - Peer address of the network connection - IP address or Unix domain socket name. - Example: 10.1.2.80, /tmp/my.sock, localhost - """ - - NETWORK_PEER_PORT = "network.peer.port" - """ - Peer port number of the network connection. - Example: 6379 - """ - - NETWORK_TRANSPORT = "network.transport" - """ - The transport protocol used for the network connection. - Example: "tcp", "udp", "unix" - """ - - PROCESS_PID = "process.pid" - """ - The process ID of the running process. - Example: 12345 - """ - - PROCESS_COMMAND_ARGS = "process.command_args" - """ - All the command arguments (including the command/executable itself) as received by the process. - Example: ["cmd/otecol","--config=config.yaml"] - """ - - PROFILER_ID = "profiler_id" - """ - Label identifying the profiler id that the span occurred in. This should be a string. - Example: "5249fbada8d5416482c2f6e47e337372" - """ - - RPC_METHOD = "rpc.method" - """ - The fully-qualified logical name of the method from the RPC interface perspective. - Example: "com.example.ExampleService/exampleMethod" - """ - - RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" - """ - Status code of the RPC returned by the RPC server or generated by the client. - Example: "DEADLINE_EXCEEDED" - """ - - SERVER_ADDRESS = "server.address" - """ - Name of the database host. - Example: example.com - """ - - SERVER_PORT = "server.port" - """ - Logical server port number - Example: 80; 8080; 443 - """ - - SERVER_SOCKET_ADDRESS = "server.socket.address" - """ - Physical server IP address or Unix socket address. - Example: 10.5.3.2 - """ - - SERVER_SOCKET_PORT = "server.socket.port" - """ - Physical server port. - Recommended: If different than server.port. - Example: 16456 - """ - - THREAD_ID = "thread.id" - """ - Identifier of a thread from where the span originated. This should be a string. - Example: "7972576320" - """ - - THREAD_NAME = "thread.name" - """ - Label identifying a thread from where the span originated. This should be a string. - Example: "MainThread" - """ - - USER_EMAIL = "user.email" - """ - User email address. - Example: "test@example.com" - """ - - USER_ID = "user.id" - """ - Unique identifier of the user. - Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" - """ - - USER_IP_ADDRESS = "user.ip_address" - """ - The IP address of the user that triggered the request. - Example: "10.1.2.80" - """ - - USER_NAME = "user.name" - """ - Short name or login/username of the user. - Example: "j.smith" - """ - - URL_FULL = "url.full" - """ - The URL of the resource that was fetched. - Example: "https://example.com/test?foo=bar#buzz" - """ - - URL_FRAGMENT = "url.fragment" - """ - The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. - Example: "details" - """ - - URL_PATH = "url.path" - """ - The URI path component. - Example: "/foo" - """ - - URL_QUERY = "url.query" - """ - The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. - Example: "foo=bar&bar=baz" - """ - - MCP_TOOL_NAME = "mcp.tool.name" - """ - The name of the MCP tool being called. - Example: "get_weather" - """ - - MCP_PROMPT_NAME = "mcp.prompt.name" - """ - The name of the MCP prompt being retrieved. - Example: "code_review" - """ - - MCP_RESOURCE_URI = "mcp.resource.uri" - """ - The URI of the MCP resource being accessed. - Example: "file:///path/to/resource" - """ - - MCP_METHOD_NAME = "mcp.method.name" - """ - The MCP protocol method name being called. - Example: "tools/call", "prompts/get", "resources/read" - """ - - MCP_REQUEST_ID = "mcp.request.id" - """ - The unique identifier for the MCP request. - Example: "req_123abc" - """ - - MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" - """ - The result/output content from an MCP tool execution. - Example: "The weather is sunny" - """ - - MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" - """ - The number of items/keys in the MCP tool result. - Example: 5 - """ - - MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" - """ - Whether the MCP tool execution resulted in an error. - Example: True - """ - - MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" - """ - The message content from an MCP prompt retrieval. - Example: "Review the following code..." - """ - - MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" - """ - The role of the message in an MCP prompt retrieval (only set for single-message prompts). - Example: "user", "assistant", "system" - """ - - MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" - """ - The number of messages in an MCP prompt result. - Example: 1, 3 - """ - - MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" - """ - The protocol/scheme of the MCP resource URI. - Example: "file", "http", "https" - """ - - MCP_TRANSPORT = "mcp.transport" - """ - The transport method used for MCP communication. - Example: "http", "sse", "stdio" - """ - - MCP_SESSION_ID = "mcp.session.id" - """ - The session identifier for the MCP connection. - Example: "a1b2c3d4e5f6" - """ - - SENTRY_DIST = "sentry.dist" - """ - The Sentry dist. - Example: "1.0" - """ - - SENTRY_ENVIRONMENT = "sentry.environment" - """ - The Sentry environment. - Example: "prod" - """ - - SENTRY_RELEASE = "sentry.release" - """ - The Sentry release. - Example: "1.2.3" - """ - - SENTRY_PLATFORM = "sentry.platform" - """ - The sdk platform that generated the event. - Example: "python" - """ - - SENTRY_SDK_NAME = "sentry.sdk.name" - """ - The name of the SDK. - Example: "python" - """ - - SENTRY_SDK_VERSION = "sentry.sdk.version" - """ - The SDK version. - Example: "1.2.3" - """ - - SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" - """ - A list of names identifying enabled integrations. - Example: ["AtexitIntegration", "StdlibIntegration"] - """ - - -class SPANSTATUS: - """ - The status of a Sentry span. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - - ABORTED = "aborted" - ALREADY_EXISTS = "already_exists" - CANCELLED = "cancelled" - DATA_LOSS = "data_loss" - DEADLINE_EXCEEDED = "deadline_exceeded" - FAILED_PRECONDITION = "failed_precondition" - INTERNAL_ERROR = "internal_error" - INVALID_ARGUMENT = "invalid_argument" - NOT_FOUND = "not_found" - OK = "ok" - OUT_OF_RANGE = "out_of_range" - PERMISSION_DENIED = "permission_denied" - RESOURCE_EXHAUSTED = "resource_exhausted" - UNAUTHENTICATED = "unauthenticated" - UNAVAILABLE = "unavailable" - UNIMPLEMENTED = "unimplemented" - UNKNOWN_ERROR = "unknown_error" - - -class OP: - ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" - CACHE_GET = "cache.get" - CACHE_PUT = "cache.put" - COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" - COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" - DB = "db" - DB_CURSOR_ITERATOR = "db.cursor.iter" - DB_CURSOR_FETCH = "db.cursor.fetch" - DB_REDIS = "db.redis" - EVENT_DJANGO = "event.django" - FUNCTION = "function" - FUNCTION_AWS = "function.aws" - FUNCTION_GCP = "function.gcp" - GEN_AI_CHAT = "gen_ai.chat" - GEN_AI_CREATE_AGENT = "gen_ai.create_agent" - GEN_AI_EMBEDDINGS = "gen_ai.embeddings" - GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" - GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" - GEN_AI_HANDOFF = "gen_ai.handoff" - GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" - GEN_AI_RESPONSES = "gen_ai.responses" - GRAPHQL_EXECUTE = "graphql.execute" - GRAPHQL_MUTATION = "graphql.mutation" - GRAPHQL_PARSE = "graphql.parse" - GRAPHQL_RESOLVE = "graphql.resolve" - GRAPHQL_SUBSCRIPTION = "graphql.subscription" - GRAPHQL_QUERY = "graphql.query" - GRAPHQL_VALIDATE = "graphql.validate" - GRPC_CLIENT = "grpc.client" - GRPC_SERVER = "grpc.server" - HTTP_CLIENT = "http.client" - HTTP_CLIENT_STREAM = "http.client.stream" - HTTP_SERVER = "http.server" - MIDDLEWARE_DJANGO = "middleware.django" - MIDDLEWARE_LITESTAR = "middleware.litestar" - MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" - MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" - MIDDLEWARE_STARLETTE = "middleware.starlette" - MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" - MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" - MIDDLEWARE_STARLITE = "middleware.starlite" - MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" - MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" - HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( - "ai.chat_completions.create.huggingface_hub" - ) - QUEUE_PROCESS = "queue.process" - QUEUE_PUBLISH = "queue.publish" - QUEUE_SUBMIT_ARQ = "queue.submit.arq" - QUEUE_TASK_ARQ = "queue.task.arq" - QUEUE_SUBMIT_CELERY = "queue.submit.celery" - QUEUE_TASK_CELERY = "queue.task.celery" - QUEUE_TASK_RQ = "queue.task.rq" - QUEUE_SUBMIT_HUEY = "queue.submit.huey" - QUEUE_TASK_HUEY = "queue.task.huey" - QUEUE_SUBMIT_RAY = "queue.submit.ray" - QUEUE_TASK_RAY = "queue.task.ray" - QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" - QUEUE_SUBMIT_DJANGO = "queue.submit.django" - SUBPROCESS = "subprocess" - SUBPROCESS_WAIT = "subprocess.wait" - SUBPROCESS_COMMUNICATE = "subprocess.communicate" - TEMPLATE_RENDER = "template.render" - VIEW_RENDER = "view.render" - VIEW_RESPONSE_RENDER = "view.response.render" - WEBSOCKET_SERVER = "websocket.server" - SOCKET_CONNECTION = "socket.connection" - SOCKET_DNS = "socket.dns" - MCP_SERVER = "mcp.server" - - -# This type exists to trick mypy and PyCharm into thinking `init` and `Client` -# take these arguments (even though they take opaque **kwargs) -class ClientConstructor: - def __init__( - self, - dsn: "Optional[str]" = None, - *, - max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - server_name: "Optional[str]" = None, - shutdown_timeout: float = 2, - integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 - in_app_include: "List[str]" = [], # noqa: B006 - in_app_exclude: "List[str]" = [], # noqa: B006 - default_integrations: bool = True, - dist: "Optional[str]" = None, - transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, - transport_queue_size: int = DEFAULT_QUEUE_SIZE, - sample_rate: float = 1.0, - send_default_pii: "Optional[bool]" = None, - http_proxy: "Optional[str]" = None, - https_proxy: "Optional[str]" = None, - ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 - max_request_body_size: str = "medium", - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, - keep_alive: "Optional[bool]" = None, - before_send: "Optional[EventProcessor]" = None, - before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, - debug: "Optional[bool]" = None, - attach_stacktrace: bool = False, - ca_certs: "Optional[str]" = None, - propagate_traces: bool = True, - traces_sample_rate: "Optional[float]" = None, - traces_sampler: "Optional[TracesSampler]" = None, - profiles_sample_rate: "Optional[float]" = None, - profiles_sampler: "Optional[TracesSampler]" = None, - profiler_mode: "Optional[ProfilerMode]" = None, - profile_lifecycle: 'Literal["manual", "trace"]' = "manual", - profile_session_sample_rate: "Optional[float]" = None, - auto_enabling_integrations: bool = True, - disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, - auto_session_tracking: bool = True, - send_client_reports: bool = True, - _experiments: "Experiments" = {}, # noqa: B006 - proxy_headers: "Optional[Dict[str, str]]" = None, - instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, - before_send_transaction: "Optional[TransactionProcessor]" = None, - project_root: "Optional[str]" = None, - enable_tracing: "Optional[bool]" = None, - include_local_variables: "Optional[bool]" = True, - include_source_context: "Optional[bool]" = True, - trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 - MATCH_ALL - ], - functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 - event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, - max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, - enable_backpressure_handling: bool = True, - error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, - enable_db_query_source: bool = True, - db_query_source_threshold_ms: int = 100, - enable_http_request_source: bool = True, - http_request_source_threshold_ms: int = 100, - spotlight: "Optional[Union[bool, str]]" = None, - cert_file: "Optional[str]" = None, - key_file: "Optional[str]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, - add_full_stack: bool = DEFAULT_ADD_FULL_STACK, - max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, - enable_logs: bool = False, - before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, - trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), - enable_metrics: bool = True, - before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, - org_id: "Optional[str]" = None, - strict_trace_continuation: bool = False, - stream_gen_ai_spans: bool = True, - ) -> None: - """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. - - :param dsn: The DSN tells the SDK where to send the events. - - If this option is not set, the SDK will just not send any data. - - The `dsn` config option takes precedence over the environment variable. - - Learn more about `DSN utilization `_. - - :param debug: Turns debug mode on or off. - - When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes - wrong with event sending. - - The default is always `False`. It's generally not recommended to turn it on in production because of the - increase in log output. - - The `debug` config option takes precedence over the environment variable. - - :param release: Sets the release. - - If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to - manually set it to guarantee that the release is in sync with your deploy integrations. - - Release names are strings, but some formats are detected by Sentry and might be rendered differently. - - See `the releases documentation `_ to learn how the SDK tries to - automatically configure a release. - - The `release` config option takes precedence over the environment variable. - - Learn more about how to send release data so Sentry can tell you about regressions between releases and - identify the potential source in `the product documentation `_. - - :param environment: Sets the environment. This string is freeform and set to `production` by default. - - A release can be associated with more than one environment to separate them in the UI (think `staging` vs - `production` or similar). - - The `environment` config option takes precedence over the environment variable. - - :param dist: The distribution of the application. - - Distributions are used to disambiguate build or deployment variants of the same release of an application. - - The dist can be for example a build number. - - :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. - - The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of - error events will be sent. - - Events are picked randomly. - - :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. - - This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and - which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number - between `0.0` and `1.0`, inclusive. - - The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to - send the event with the given probability. - - If this configuration option is specified, the `sample_rate` option is ignored. - - :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. - - Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're - sent to Sentry. - - By default, all errors are sent. - - :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. - - This defaults to `100`, but you can set this to any number. - - However, you should be aware that Sentry has a `maximum payload size `_ - and any events exceeding that payload size will be dropped. - - :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. - - Stack traces are always attached to exceptions; however, when this option is set, stack traces are also - sent with messages. - - This option means that stack traces appear next to all log messages. - - Grouping in Sentry is different for events with stack traces and without. As a result, you will get new - groups as you enable or disable this flag for certain events. - - :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) - `_ is added by active integrations. - - If you enable this option, be sure to manually remove what you don't want to send using our features for - managing `Sensitive Data `_. - - :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and - passwords from a `denylist`. - - It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. - - See how to `configure the scrubber here `_. - - :param include_source_context: When enabled, source context will be included in events sent to Sentry. - - This source context includes the five lines of code above and below the line of code where an error - happened. - - :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with - the event to help with debugging. - - :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the - moment an error occurs. - - But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will - be included in the stack trace sent to Sentry. - - :param max_stack_frames: This option limits the number of stack frames that will be captured when - `add_full_stack` is enabled. - - :param server_name: This option can be used to supply a server name. - - When provided, the name of the server is sent along and persisted in the event. - - For many integrations, the server name actually corresponds to the device hostname, even in situations - where the machine is not actually a server. - - :param project_root: The full path to the root directory of your application. - - The `project_root` is used to mark frames in a stack trace either as being in your application or outside - of the application. - - :param in_app_include: A list of string prefixes of module names that belong to the app. - - This option takes precedence over `in_app_exclude`. - - Sentry differentiates stack frames that are directly related to your application ("in application") from - stack frames that come from other packages such as the standard library, frameworks, or other dependencies. - - The application package is automatically marked as `inApp`. - - The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are - displayed by default. - - :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to - third-party packages. - - Modules considered not part of the app will be hidden from stack traces by default. - - This option can be overridden using `in_app_include`. - - :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. - It can be set to one of the following values: - - - `never`: Request bodies are never sent. - - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically - 4KB). - - `medium`: Medium and small requests will be captured (typically 10KB). - - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. - - Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ - expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of - how you configure this option. - - :param max_value_length: The number of characters after which the values containing text in the event payload - will be truncated. - - WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be - dropped by Sentry. - - :param ca_certs: A path to an alternative CA bundle file in PEM-format. - - :param send_client_reports: Set this boolean to `False` to disable sending of client reports. - - Client reports allow the client to send status reports about itself to Sentry, such as information about - events that were dropped before being sent. - - :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) - `_. - - This setting can be used to override the default config options for a specific auto-enabling integration - or to add an integration that is not auto-enabled. - - :param disabled_integrations: List of integrations that will be disabled. - - This setting can be used to explicitly turn off specific `auto-enabling integrations (list) - `_ or - `default `_ integrations. - - :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) - `_ should be enabled. - - When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding - framework/library is detected. - - :param default_integrations: Configures whether `default integrations - `_ should be enabled. - - Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling - integrations**, unless they are specifically added in the `integrations` option, described above. - - :param before_send: This function is called with an SDK-specific message or error event object, and can return - a modified event object, or `null` to skip reporting the event. - - This can be used, for instance, for manual PII stripping before sending. - - By the time `before_send` is executed, all scope data has already been applied to the event. Further - modification of the scope won't have any effect. - - :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can - return a modified transaction event object, or `null` to skip reporting the event. - - One way this might be used is for manual PII stripping before sending. - - :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb - is added to the scope. - - When nothing is returned from the function, the breadcrumb is dropped. - - To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. - - The callback typically gets a second argument (called a "hint") which contains the original object from - which the breadcrumb was created to further customize what the breadcrumb should look like. - - :param transport: Switches out the transport used to send events. - - How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to - send it through some more complex setup that requires proxy authentication. - - :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to - flush. - - :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. - - This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs - support a separate HTTPS proxy. - - SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix - systems, the `http_proxy` environment variable will be picked up. - - :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. - - This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for - HTTPS requests at all times. - - :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded - to `urllib3`'s `ProxyManager `_. - - :param shutdown_timeout: Controls how many seconds to wait before shutting down. - - Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending - events. The default is SDK specific but typically around two seconds. - - Setting this value too low may cause problems for sending events from command line applications. - - Setting the value too high will cause the application to block for a long time for users experiencing - network connectivity problems. - - :param keep_alive: Determines whether to keep the connection alive between requests. - - This can be useful in environments where you encounter frequent network issues such as connection resets. - - :param cert_file: Path to the client certificate to use. - - If set, supersedes the `CLIENT_CERT_FILE` environment variable. - - :param key_file: Path to the key file to use. - - If set, supersedes the `CLIENT_KEY_FILE` environment variable. - - :param socket_options: An optional list of socket options to use. - - These provide fine-grained, low-level control over the way the SDK connects to Sentry. - - If provided, the options will override the default `urllib3` `socket options - `_. - - :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction - will be sent to Sentry. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - Either this or `traces_sampler` must be defined to enable tracing. - - If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have - another service (for example a JS frontend) that makes requests to your service that include trace - information, those traces will be continued and thus transactions will be sent to Sentry. - - If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces - will be started and no incoming traces will be continued. - - :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be - sent to Sentry. - - It will automatically be passed information about the transaction and the context in which it's being - created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being - sent). - - Can also be used for filtering transactions, by returning `0` for those that are unwanted. - - Either this or `traces_sample_rate` must be defined to enable tracing. - - :param trace_propagation_targets: An optional property that controls which downstream services receive tracing - data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. - - The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. - - If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to - that request. - - String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ - a string provided through the option. - - If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the - instrumented client. - - :param functions_to_trace: An optional list of functions that should be set up for tracing. - - For each function in the list, a span will be created when the function is executed. - - Functions in the list are represented as strings containing the fully qualified name of the function. - - This is a convenient option, making it possible to have one central place for configuring what functions - to trace, instead of having custom instrumentation scattered all over your code base. - - To learn more, see the `Custom Instrumentation `_ documentation. - - :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health - checks on the SDK. - - If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second - intervals until recovery. - - This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. - - This option is enabled by default. - - :param enable_db_query_source: When enabled, the source location will be added to database queries. - - :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database - queries. - - The query location will be added to the query for queries slower than the specified threshold. - - :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. - - :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an - outgoing HTTP request. - - The request location will be added to the request for requests slower than the specified threshold. - - :param custom_repr: A custom `repr `_ function to run - while serializing an object. - - Use this to control how your custom objects and classes are visible in Sentry. - - Return a string for that repr value to be used or `None` to continue serializing how Sentry would have - done it anyway. - - :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled - transaction will be profiled. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be - profiled. - - :param profiles_sampler: - - :param profiler_mode: - - :param profile_lifecycle: - - :param profile_session_sample_rate: - - :param enable_tracing: - - :param propagate_traces: - - :param auto_session_tracking: - - :param spotlight: - - :param instrumenter: - - :param enable_logs: Set `enable_logs` to True to enable the SDK to emit - Sentry logs. Defaults to False. - - :param before_send_log: An optional function to modify or filter out logs - before they're sent to Sentry. Any modifications to the log in this - function will be retained. If the function returns None, the log will - not be sent to Sentry. - - :param trace_ignore_status_codes: An optional property that disables tracing for - HTTP requests with certain status codes. - - Requests are not traced if the status code is contained in the provided set. - - If `trace_ignore_status_codes` is not provided, requests with any status code - may be traced. - - This option has no effect in span streaming mode (`trace_lifecycle="stream"`). - - :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the - `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. - - If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. - - The client's organization ID is extracted from the DSN or can be set with the `org_id` option. - If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. - This is useful to prevent traces of unknown third-party services from being continued in your application. - - :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases - but you can provide it explicitly for self-hosted and Relay setups. This value is used for - trace propagation and for features like `strict_trace_continuation`. - - :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to - reduce downstream data loss. - - :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. - - ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations - collect automatically, superseding `send_default_pii`. Passing a dict under - `_experiments={"data_collection": {...}}` opts into the feature; 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 - changes nothing. Restrict collection per category (user identity, cookies, HTTP - headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source - context). If `send_default_pii` is also set, `data_collection` takes precedence. - - Example:: - - sentry_sdk.init( - dsn="...", - _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, - ) - - See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. - """ - pass - - -def _get_default_options() -> "dict[str, Any]": - import inspect - - a = inspect.getfullargspec(ClientConstructor.__init__) - defaults = a.defaults or () - kwonlydefaults = a.kwonlydefaults or {} - - return dict( - itertools.chain( - zip(a.args[-len(defaults) :], defaults), - kwonlydefaults.items(), - ) - ) - - -DEFAULT_OPTIONS = _get_default_options() -del _get_default_options - - -VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py deleted file mode 100644 index b3287703b9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.crons.api import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.crons.decorator import monitor - -__all__ = [ - "capture_checkin", - "MonitorStatus", - "monitor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py deleted file mode 100644 index 6ea3e36b6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/api.py +++ /dev/null @@ -1,60 +0,0 @@ -import uuid -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, MonitorConfig - - -def _create_check_in_event( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration_s: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> "Event": - options = sentry_sdk.get_client().options - check_in_id = check_in_id or uuid.uuid4().hex - - check_in: "Event" = { - "type": "check_in", - "monitor_slug": monitor_slug, - "check_in_id": check_in_id, - "status": status, - "duration": duration_s, - "environment": options.get("environment", None), - "release": options.get("release", None), - } - - if monitor_config: - check_in["monitor_config"] = monitor_config - - return check_in - - -def capture_checkin( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> str: - check_in_event = _create_check_in_event( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=status, - duration_s=duration, - monitor_config=monitor_config, - ) - - sentry_sdk.capture_event(check_in_event) - - logger.debug( - f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" - ) - - return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py deleted file mode 100644 index be686b4539..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/consts.py +++ /dev/null @@ -1,4 +0,0 @@ -class MonitorStatus: - IN_PROGRESS = "in_progress" - OK = "ok" - ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py deleted file mode 100644 index b13d350e15..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/crons/decorator.py +++ /dev/null @@ -1,137 +0,0 @@ -from functools import wraps -from inspect import iscoroutinefunction -from typing import TYPE_CHECKING - -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from types import TracebackType - from typing import ( - Any, - Optional, - ParamSpec, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from sentry_sdk._types import MonitorConfig - - P = ParamSpec("P") - R = TypeVar("R") - - -class monitor: # noqa: N801 - """ - Decorator/context manager to capture checkin events for a monitor. - - Usage (as decorator): - ``` - import sentry_sdk - - app = Celery() - - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` - - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. - - Usage (as context manager): - ``` - import sentry_sdk - - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` - """ - - def __init__( - self, - monitor_slug: "Optional[str]" = None, - monitor_config: "Optional[MonitorConfig]" = None, - ) -> None: - self.monitor_slug = monitor_slug - self.monitor_config = monitor_config - - def __enter__(self) -> None: - self.start_timestamp = now() - self.check_in_id = capture_checkin( - monitor_slug=self.monitor_slug, - status=MonitorStatus.IN_PROGRESS, - monitor_config=self.monitor_config, - ) - - def __exit__( - self, - exc_type: "Optional[Type[BaseException]]", - exc_value: "Optional[BaseException]", - traceback: "Optional[TracebackType]", - ) -> None: - duration_s = now() - self.start_timestamp - - if exc_type is None and exc_value is None and traceback is None: - status = MonitorStatus.OK - else: - status = MonitorStatus.ERROR - - capture_checkin( - monitor_slug=self.monitor_slug, - check_in_id=self.check_in_id, - status=status, - duration=duration_s, - monitor_config=self.monitor_config, - ) - - if TYPE_CHECKING: - - @overload - def __call__( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - # Unfortunately, mypy does not give us any reliable way to type check the - # return value of an Awaitable (i.e. async function) for this overload, - # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. - ... - - @overload - def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... - - def __call__( - self, - fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", - ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": - if iscoroutinefunction(fn): - return self._async_wrapper(fn) - - else: - if TYPE_CHECKING: - fn = cast("Callable[P, R]", fn) - return self._sync_wrapper(fn) - - def _async_wrapper( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - @wraps(fn) - async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return await fn(*args, **kwargs) - - return inner - - def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": - @wraps(fn) - def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return fn(*args, **kwargs) - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py deleted file mode 100644 index bcdf767409..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/data_collection.py +++ /dev/null @@ -1,319 +0,0 @@ -""" -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, List, Mapping, Optional, cast - -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE - -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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: - """ - Return whether ``key`` matches the sensitive denylist using a partial, - case-insensitive substring match. - - :param extra_terms: additional deny terms (e.g. user-provided) to consider - alongside the built-in `_SENSITIVE_DENYLIST`. - """ - lowered = key.lower() - for term in _SENSITIVE_DENYLIST: - if term in lowered: - return True - if extra_terms: - for term in extra_terms: - if term and term.lower() in lowered: - return True - return False - - -def _apply_key_value_collection_filtering( - items: "Mapping[str, Any]", - behaviour: "KeyValueCollectionBehaviour", - substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, -) -> "Dict[str, Any]": - - if behaviour["mode"] == "off": - return {} - - result: "Dict[str, Any]" = {} - - if behaviour["mode"] == "allowlist": - for key, value in items.items(): - is_allowed = False - if isinstance(key, str): - lowered = key.lower() - is_allowed = any( - term and term.lower() in lowered - for term in behaviour.get("terms", []) - ) - if is_allowed and not _is_sensitive_key(key): - result[key] = value - else: - result[key] = substitute - return result - - # denylist behaviour - for key, value in items.items(): - if isinstance(key, str) and _is_sensitive_key( - key=key, extra_terms=behaviour.get("terms", []) - ): - result[key] = substitute - else: - result[key] = value - return result - - -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, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py deleted file mode 100644 index 795882e9ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/debug.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -import sys -import warnings -from logging import LogRecord - -from sentry_sdk import get_client -from sentry_sdk.client import _client_init_debug -from sentry_sdk.utils import logger - - -class _DebugFilter(logging.Filter): - def filter(self, record: "LogRecord") -> bool: - if _client_init_debug.get(False): - return True - - return get_client().options["debug"] - - -def init_debug_support() -> None: - if not logger.handlers: - configure_logger() - - -def configure_logger() -> None: - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.DEBUG) - logger.addFilter(_DebugFilter()) - - -def configure_debug_hub() -> None: - warnings.warn( - "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py deleted file mode 100644 index d2d4aae31a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/envelope.py +++ /dev/null @@ -1,332 +0,0 @@ -import io -import json -import mimetypes -from typing import TYPE_CHECKING - -from sentry_sdk.session import Session -from sentry_sdk.utils import capture_internal_exceptions, json_dumps - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, List, Optional, Union - - from sentry_sdk._types import Event, EventDataCategory - - -def parse_json(data: "Union[bytes, str]") -> "Any": - # on some python 3 versions this needs to be bytes - if isinstance(data, bytes): - data = data.decode("utf-8", "replace") - return json.loads(data) - - -class Envelope: - """ - Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints - documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, - each envelope may have at most one Item with type "event" or "transaction" (but not both). - """ - - def __init__( - self, - headers: "Optional[Dict[str, Any]]" = None, - items: "Optional[List[Item]]" = None, - ) -> None: - if headers is not None: - headers = dict(headers) - self.headers = headers or {} - if items is None: - items = [] - else: - items = list(items) - self.items = items - - @property - def description(self) -> str: - return "envelope with %s items (%s)" % ( - len(self.items), - ", ".join(x.data_category for x in self.items), - ) - - def add_event( - self, - event: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=event), type="event")) - - def add_transaction( - self, - transaction: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - - def add_profile( - self, - profile: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) - - def add_profile_chunk( - self, - profile_chunk: "Any", - ) -> None: - self.add_item( - Item( - payload=PayloadRef(json=profile_chunk), - type="profile_chunk", - headers={"platform": profile_chunk.get("platform", "python")}, - ) - ) - - def add_checkin( - self, - checkin: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) - - def add_session( - self, - session: "Union[Session, Any]", - ) -> None: - if isinstance(session, Session): - session = session.to_json() - self.add_item(Item(payload=PayloadRef(json=session), type="session")) - - def add_sessions( - self, - sessions: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) - - def add_item( - self, - item: "Item", - ) -> None: - self.items.append(item) - - def get_event(self) -> "Optional[Event]": - for items in self.items: - event = items.get_event() - if event is not None: - return event - return None - - def get_transaction_event(self) -> "Optional[Event]": - for item in self.items: - event = item.get_transaction_event() - if event is not None: - return event - return None - - def __iter__(self) -> "Iterator[Item]": - return iter(self.items) - - def serialize_into( - self, - f: "Any", - ) -> None: - f.write(json_dumps(self.headers)) - f.write(b"\n") - for item in self.items: - item.serialize_into(f) - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Envelope": - headers = parse_json(f.readline()) - items = [] - while 1: - item = Item.deserialize_from(f) - if item is None: - break - items.append(item) - return cls(headers=headers, items=items) - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Envelope": - return cls.deserialize_from(io.BytesIO(bytes)) - - def __repr__(self) -> str: - return "" % (self.headers, self.items) - - -class PayloadRef: - def __init__( - self, - bytes: "Optional[bytes]" = None, - path: "Optional[Union[bytes, str]]" = None, - json: "Optional[Any]" = None, - ) -> None: - self.json = json - self.bytes = bytes - self.path = path - - def get_bytes(self) -> bytes: - if self.bytes is None: - if self.path is not None: - with capture_internal_exceptions(): - with open(self.path, "rb") as f: - self.bytes = f.read() - elif self.json is not None: - self.bytes = json_dumps(self.json) - return self.bytes or b"" - - @property - def inferred_content_type(self) -> str: - if self.json is not None: - return "application/json" - elif self.path is not None: - path = self.path - if isinstance(path, bytes): - path = path.decode("utf-8", "replace") - ty = mimetypes.guess_type(path)[0] - if ty: - return ty - return "application/octet-stream" - - def __repr__(self) -> str: - return "" % (self.inferred_content_type,) - - -class Item: - def __init__( - self, - payload: "Union[bytes, str, PayloadRef]", - headers: "Optional[Dict[str, Any]]" = None, - type: "Optional[str]" = None, - content_type: "Optional[str]" = None, - filename: "Optional[str]" = None, - ): - if headers is not None: - headers = dict(headers) - elif headers is None: - headers = {} - self.headers = headers - if isinstance(payload, bytes): - payload = PayloadRef(bytes=payload) - elif isinstance(payload, str): - payload = PayloadRef(bytes=payload.encode("utf-8")) - else: - payload = payload - - if filename is not None: - headers["filename"] = filename - if type is not None: - headers["type"] = type - if content_type is not None: - headers["content_type"] = content_type - elif "content_type" not in headers: - headers["content_type"] = payload.inferred_content_type - - self.payload = payload - - def __repr__(self) -> str: - return "" % ( - self.headers, - self.payload, - self.data_category, - ) - - @property - def type(self) -> "Optional[str]": - return self.headers.get("type") - - @property - def data_category(self) -> "EventDataCategory": - ty = self.headers.get("type") - if ty == "session" or ty == "sessions": - return "session" - elif ty == "attachment": - return "attachment" - elif ty == "transaction": - return "transaction" - elif ty == "span": - return "span" - elif ty == "event": - return "error" - elif ty == "log": - return "log_item" - elif ty == "trace_metric": - return "trace_metric" - elif ty == "client_report": - return "internal" - elif ty == "profile": - return "profile" - elif ty == "profile_chunk": - return "profile_chunk" - elif ty == "check_in": - return "monitor" - else: - return "default" - - def get_bytes(self) -> bytes: - return self.payload.get_bytes() - - def get_event(self) -> "Optional[Event]": - """ - Returns an error event if there is one. - """ - if self.type == "event" and self.payload.json is not None: - return self.payload.json - return None - - def get_transaction_event(self) -> "Optional[Event]": - if self.type == "transaction" and self.payload.json is not None: - return self.payload.json - return None - - def serialize_into( - self, - f: "Any", - ) -> None: - headers = dict(self.headers) - bytes = self.get_bytes() - headers["length"] = len(bytes) - f.write(json_dumps(headers)) - f.write(b"\n") - f.write(bytes) - f.write(b"\n") - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Optional[Item]": - line = f.readline().rstrip() - if not line: - return None - headers = parse_json(line) - length = headers.get("length") - if length is not None: - payload = f.read(length) - f.readline() - else: - # if no length was specified we need to read up to the end of line - # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) - payload = f.readline().rstrip(b"\n") - if headers.get("type") in ("event", "transaction"): - rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) - else: - rv = cls(headers=headers, payload=payload) - return rv - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Optional[Item]": - return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py deleted file mode 100644 index 5eaa5e440b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/feature_flags.py +++ /dev/null @@ -1,75 +0,0 @@ -import copy -from threading import Lock -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import TypedDict - - FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) - - -DEFAULT_FLAG_CAPACITY = 100 - - -class FlagBuffer: - def __init__(self, capacity: int) -> None: - self.capacity = capacity - self.lock = Lock() - - # Buffer is private. The name is mangled to discourage use. If you use this attribute - # directly you're on your own! - self.__buffer = LRUCache(capacity) - - def clear(self) -> None: - self.__buffer = LRUCache(self.capacity) - - def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": - with self.lock: - buffer = FlagBuffer(self.capacity) - buffer.__buffer = copy.deepcopy(self.__buffer, memo) - return buffer - - def get(self) -> "list[FlagData]": - with self.lock: - return [ - {"flag": key, "result": value} for key, value in self.__buffer.get_all() - ] - - def set(self, flag: str, result: bool) -> None: - if isinstance(result, FlagBuffer): - # If someone were to insert `self` into `self` this would create a circular dependency - # on the lock. This is of course a deadlock. However, this is far outside the expected - # usage of this class. We guard against it here for completeness and to document this - # expected failure mode. - raise ValueError( - "FlagBuffer instances can not be inserted into the dictionary." - ) - - with self.lock: - self.__buffer.set(flag, result) - - -def add_feature_flag(flag: str, result: bool) -> None: - """ - Records a flag and its value to be sent on subsequent error events. - We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. - """ - client = sentry_sdk.get_client() - - flags = sentry_sdk.get_isolation_scope().flags - flags.set(flag, result) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.get_current_span() - if span and isinstance(span, sentry_sdk.traces.StreamedSpan): - span.set_attribute(f"flag.evaluation.{flag}", result) - - else: - span = sentry_sdk.get_current_span() - if span and isinstance(span, Span): - span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py deleted file mode 100644 index b17444d06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/hub.py +++ /dev/null @@ -1,741 +0,0 @@ -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import ( - get_client, - get_current_scope, - get_global_scope, - get_isolation_scope, -) -from sentry_sdk._compat import with_metaclass -from sentry_sdk.client import Client -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.scope import _ScopeManager -from sentry_sdk.tracing import ( - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.utils import ( - ContextVar, - logger, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.tracing import TransactionKwargs - - T = TypeVar("T") - -else: - - def overload(x: "T") -> "T": - return x - - -class SentryHubDeprecationWarning(DeprecationWarning): - """ - A custom deprecation warning to inform users that the Hub is deprecated. - """ - - _MESSAGE = ( - "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " - "Please consult our 1.x to 2.x migration guide for details on how to migrate " - "`Hub` usage to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" - ) - - def __init__(self, *_: object) -> None: - super().__init__(self._MESSAGE) - - -@contextmanager -def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": - """Utility function to suppress deprecation warnings for the Hub.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) - yield - - -_local = ContextVar("sentry_current_hub") - - -class HubMeta(type): - @property - def current(cls) -> "Hub": - """Returns the current instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - rv = _local.get(None) - if rv is None: - with _suppress_hub_deprecation_warning(): - # This will raise a deprecation warning; suppress it since we already warned above. - rv = Hub(GLOBAL_HUB) - _local.set(rv) - return rv - - @property - def main(cls) -> "Hub": - """Returns the main instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - return GLOBAL_HUB - - -class Hub(with_metaclass(HubMeta)): # type: ignore - """ - .. deprecated:: 2.0.0 - The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. - - The hub wraps the concurrency management of the SDK. Each thread has - its own hub but the hub might transfer with the flow of execution if - context vars are available. - - If the hub is used with a with statement it's temporarily activated. - """ - - _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] - _scope: "Optional[Scope]" = None - - # Mypy doesn't pick up on the metaclass. - - if TYPE_CHECKING: - current: "Hub" = None # type: ignore[assignment] - main: "Optional[Hub]" = None - - def __init__( - self, - client_or_hub: "Optional[Union[Hub, Client]]" = None, - scope: "Optional[Any]" = None, - ) -> None: - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - - current_scope = None - - if isinstance(client_or_hub, Hub): - client = get_client() - if scope is None: - # hub cloning is going on, we use a fork of the current/isolation scope for context manager - scope = get_isolation_scope().fork() - current_scope = get_current_scope().fork() - else: - client = client_or_hub - get_global_scope().set_client(client) - - if scope is None: # so there is no Hub cloning going on - # just the current isolation scope is used for context manager - scope = get_isolation_scope() - current_scope = get_current_scope() - - if current_scope is None: - # just the current current scope is used for context manager - current_scope = get_current_scope() - - self._stack = [(client, scope)] # type: ignore - self._last_event_id: "Optional[str]" = None - self._old_hubs: "List[Hub]" = [] - - self._old_current_scopes: "List[Scope]" = [] - self._old_isolation_scopes: "List[Scope]" = [] - self._current_scope: "Scope" = current_scope - self._scope: "Scope" = scope - - def __enter__(self) -> "Hub": - self._old_hubs.append(Hub.current) - _local.set(self) - - current_scope = get_current_scope() - self._old_current_scopes.append(current_scope) - scope._current_scope.set(self._current_scope) - - isolation_scope = get_isolation_scope() - self._old_isolation_scopes.append(isolation_scope) - scope._isolation_scope.set(self._scope) - - return self - - def __exit__( - self, - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[Any]", - ) -> None: - old = self._old_hubs.pop() - _local.set(old) - - old_current_scope = self._old_current_scopes.pop() - scope._current_scope.set(old_current_scope) - - old_isolation_scope = self._old_isolation_scopes.pop() - scope._isolation_scope.set(old_isolation_scope) - - def run( - self, - callback: "Callable[[], T]", - ) -> "T": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Runs a callback in the context of the hub. Alternatively the - with statement can be used on the hub directly. - """ - with self: - return callback() - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Any": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. - - Returns the integration for this hub by name or class. If there - is no client bound or the client does not have that integration - then `None` is returned. - - If the return value is not `None` the hub is guaranteed to have a - client attached. - """ - return get_client().get_integration(name_or_class) - - @property - def client(self) -> "Optional[BaseClient]": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Please use :py:func:`sentry_sdk.api.get_client` instead. - - Returns the current client on the hub. - """ - client = get_client() - - if not client.is_active(): - return None - - return client - - @property - def scope(self) -> "Scope": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Returns the current scope on the hub. - """ - return get_isolation_scope() - - def last_event_id(self) -> "Optional[str]": - """ - Returns the last event ID. - - .. deprecated:: 1.40.5 - This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. - """ - logger.warning( - "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." - ) - return self._last_event_id - - def bind_client( - self, - new: "Optional[BaseClient]", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.set_client` instead. - - Binds a new client to the hub. - """ - get_global_scope().set_client(new) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. - - Captures an event. - - Alias of :py:meth:`sentry_sdk.Scope.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - """ - last_event_id = get_current_scope().capture_event( - event, hint, scope=scope, **scope_kwargs - ) - - is_transaction = event.get("type") == "transaction" - if last_event_id is not None and not is_transaction: - self._last_event_id = last_event_id - - return last_event_id - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. - - Captures a message. - - Alias of :py:meth:`sentry_sdk.Scope.capture_message`. - - :param message: The string to send as the message to Sentry. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_message( - message, level=level, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. - - Captures an exception. - - Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_exception( - error, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. - - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_span` instead. - - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - """ - scope = get_current_scope() - return scope.start_span(instrumenter=instrumenter, **kwargs) - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. - - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. - """ - scope = get_current_scope() - - # For backwards compatibility, we allow passing the scope as the hub. - # We need a major release to make this nice. (if someone searches the code: deprecated) - # Type checking disabled for this line because deprecated keys are not allowed in the type signature. - kwargs["hub"] = scope # type: ignore - - return scope.start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - ) -> "Transaction": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. - - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers=environ_or_headers, op=op, name=name, source=source - ) - - @overload - def push_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def push_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def push_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - if callback is not None: - with self.push_scope() as scope: - callback(scope) - return None - - return _ScopeManager(self) - - def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pops a scope layer from the stack. - - Try to use the context manager :py:meth:`push_scope` instead. - """ - rv = self._stack.pop() - assert self._stack, "stack must have at least one layer" - return rv - - @overload - def configure_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def configure_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def configure_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - scope = get_isolation_scope() - - if continue_trace: - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - def start_session( - self, - session_mode: str = "application", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_session` instead. - - Starts a new session. - """ - get_isolation_scope().start_session( - session_mode=session_mode, - ) - - def end_session(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.end_session` instead. - - Ends the current session if there is one. - """ - get_isolation_scope().end_session() - - def stop_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. - - Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - get_isolation_scope().stop_auto_session_tracking() - - def resume_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. - - Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - get_isolation_scope().resume_auto_session_tracking() - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.flush` instead. - - Alias for :py:meth:`sentry_sdk.client._Client.flush` - """ - return get_client().flush(timeout=timeout, callback=callback) - - def get_traceparent(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. - - Returns the traceparent either from the active span or from the scope. - """ - current_scope = get_current_scope() - traceparent = current_scope.get_traceparent() - - if traceparent is None: - isolation_scope = get_isolation_scope() - traceparent = isolation_scope.get_traceparent() - - return traceparent - - def get_baggage(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. - - Returns Baggage either from the active span or from the scope. - """ - current_scope = get_current_scope() - baggage = current_scope.get_baggage() - - if baggage is None: - isolation_scope = get_isolation_scope() - baggage = isolation_scope.get_baggage() - - if baggage is not None: - return baggage.serialize() - - return None - - def iter_trace_propagation_headers( - self, span: "Optional[Span]" = None - ) -> "Generator[Tuple[str, str], None, None]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. - - Return HTTP headers which allow propagation of trace data. Data taken - from the span representing the request, if available, or the current - span on the scope if not. - """ - return get_current_scope().iter_trace_propagation_headers( - span=span, - ) - - def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. - - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - return get_current_scope().trace_propagation_meta( - span=span, - ) - - -with _suppress_hub_deprecation_warning(): - # Suppress deprecation warning for the Hub here, since we still always - # import this module. - GLOBAL_HUB = Hub() -_local.set(GLOBAL_HUB) - - -# Circular imports -from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py deleted file mode 100644 index 677d34a81e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/__init__.py +++ /dev/null @@ -1,352 +0,0 @@ -from abc import ABC, abstractmethod -from threading import Lock -from typing import TYPE_CHECKING - -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union - - -_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) - - -_installer_lock = Lock() - -# Set of all integration identifiers we have attempted to install -_processed_integrations: "Set[str]" = set() - -# Set of all integration identifiers we have actually installed -_installed_integrations: "Set[str]" = set() - - -def _generate_default_integrations_iterator( - integrations: "List[str]", - auto_enabling_integrations: "List[str]", -) -> "Callable[[bool], Iterator[Type[Integration]]]": - def iter_default_integrations( - with_auto_enabling_integrations: bool, - ) -> "Iterator[Type[Integration]]": - """Returns an iterator of the default integration classes:""" - from importlib import import_module - - if with_auto_enabling_integrations: - all_import_strings = integrations + auto_enabling_integrations - else: - all_import_strings = integrations - - for import_string in all_import_strings: - try: - module, cls = import_string.rsplit(".", 1) - yield getattr(import_module(module), cls) - except (DidNotEnable, SyntaxError) as e: - logger.debug( - "Did not import default integration %s: %s", import_string, e - ) - - if isinstance(iter_default_integrations.__doc__, str): - for import_string in integrations: - iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) - - return iter_default_integrations - - -_DEFAULT_INTEGRATIONS = [ - # stdlib/base runtime integrations - "sentry_sdk.integrations.argv.ArgvIntegration", - "sentry_sdk.integrations.atexit.AtexitIntegration", - "sentry_sdk.integrations.dedupe.DedupeIntegration", - "sentry_sdk.integrations.excepthook.ExcepthookIntegration", - "sentry_sdk.integrations.logging.LoggingIntegration", - "sentry_sdk.integrations.modules.ModulesIntegration", - "sentry_sdk.integrations.stdlib.StdlibIntegration", - "sentry_sdk.integrations.threading.ThreadingIntegration", -] - -_AUTO_ENABLING_INTEGRATIONS = [ - "sentry_sdk.integrations.aiohttp.AioHttpIntegration", - "sentry_sdk.integrations.anthropic.AnthropicIntegration", - "sentry_sdk.integrations.ariadne.AriadneIntegration", - "sentry_sdk.integrations.arq.ArqIntegration", - "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", - "sentry_sdk.integrations.boto3.Boto3Integration", - "sentry_sdk.integrations.bottle.BottleIntegration", - "sentry_sdk.integrations.celery.CeleryIntegration", - "sentry_sdk.integrations.chalice.ChaliceIntegration", - "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", - "sentry_sdk.integrations.cohere.CohereIntegration", - "sentry_sdk.integrations.django.DjangoIntegration", - "sentry_sdk.integrations.falcon.FalconIntegration", - "sentry_sdk.integrations.fastapi.FastApiIntegration", - "sentry_sdk.integrations.flask.FlaskIntegration", - "sentry_sdk.integrations.gql.GQLIntegration", - "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", - "sentry_sdk.integrations.graphene.GrapheneIntegration", - "sentry_sdk.integrations.httpx.HttpxIntegration", - "sentry_sdk.integrations.httpx2.Httpx2Integration", - "sentry_sdk.integrations.huey.HueyIntegration", - "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", - "sentry_sdk.integrations.langchain.LangchainIntegration", - "sentry_sdk.integrations.langgraph.LanggraphIntegration", - "sentry_sdk.integrations.litestar.LitestarIntegration", - "sentry_sdk.integrations.loguru.LoguruIntegration", - "sentry_sdk.integrations.mcp.MCPIntegration", - "sentry_sdk.integrations.openai.OpenAIIntegration", - "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", - "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", - "sentry_sdk.integrations.pymongo.PyMongoIntegration", - "sentry_sdk.integrations.pyramid.PyramidIntegration", - "sentry_sdk.integrations.quart.QuartIntegration", - "sentry_sdk.integrations.redis.RedisIntegration", - "sentry_sdk.integrations.rq.RqIntegration", - "sentry_sdk.integrations.sanic.SanicIntegration", - "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", - "sentry_sdk.integrations.starlette.StarletteIntegration", - "sentry_sdk.integrations.starlite.StarliteIntegration", - "sentry_sdk.integrations.strawberry.StrawberryIntegration", - "sentry_sdk.integrations.tornado.TornadoIntegration", -] - -iter_default_integrations = _generate_default_integrations_iterator( - integrations=_DEFAULT_INTEGRATIONS, - auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, -) - -del _generate_default_integrations_iterator - - -_MIN_VERSIONS = { - "aiohttp": (3, 4), - "aiomysql": (0, 3, 0), - "anthropic": (0, 16), - "ariadne": (0, 20), - "arq": (0, 23), - "asyncpg": (0, 23), - "beam": (2, 12), - "boto3": (1, 16), # botocore - "bottle": (0, 12), - "celery": (4, 4, 7), - "chalice": (1, 16, 0), - "clickhouse_driver": (0, 2, 0), - "cohere": (5, 4, 0), - "django": (1, 8), - "dramatiq": (1, 9), - "falcon": (1, 4), - "fastapi": (0, 79, 0), - "flask": (1, 1, 4), - "gql": (3, 4, 1), - "graphene": (3, 3), - "google_genai": (1, 29, 0), # google-genai - "grpc": (1, 32, 0), # grpcio - "httpx": (0, 16, 0), - "httpx2": (2, 0, 0), - "huggingface_hub": (0, 24, 7), - "langchain": (0, 1, 0), - "langgraph": (0, 6, 6), - "launchdarkly": (9, 8, 0), - "litellm": (1, 77, 5), - "loguru": (0, 7, 0), - "mcp": (1, 15, 0), - "openai": (1, 0, 0), - "openai_agents": (0, 0, 19), - "openfeature": (0, 7, 1), - "pydantic_ai": (1, 0, 0), - "pymongo": (3, 5, 0), - "pyreqwest": (0, 11, 6), - "quart": (0, 16, 0), - "ray": (2, 7, 0), - "requests": (2, 0, 0), - "rq": (0, 6), - "sanic": (0, 8), - "sqlalchemy": (1, 2), - "starlette": (0, 16), - "starlite": (1, 48), - "statsig": (0, 55, 3), - "strawberry": (0, 209, 5), - "tornado": (6, 0), - "typer": (0, 15), - "unleash": (6, 0, 1), -} - - -_INTEGRATION_DEACTIVATES = { - "langchain": {"openai", "anthropic", "google_genai"}, - "openai_agents": {"openai"}, - "pydantic_ai": {"openai", "anthropic"}, -} - - -def setup_integrations( - integrations: "Sequence[Integration]", - with_defaults: bool = True, - with_auto_enabling_integrations: bool = False, - disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, - options: "Optional[Dict[str, Any]]" = None, -) -> "Dict[str, Integration]": - """ - Given a list of integration instances, this installs them all. - - When `with_defaults` is set to `True` all default integrations are added - unless they were already provided before. - - `disabled_integrations` takes precedence over `with_defaults` and - `with_auto_enabling_integrations`. - - Some integrations are designed to automatically deactivate other integrations - in order to avoid conflicts and prevent duplicate telemetry from being collected. - For example, enabling the `langchain` integration will auto-deactivate both the - `openai` and `anthropic` integrations. - - Users can override this behavior by: - - Explicitly providing an integration in the `integrations=[]` list, or - - Disabling the higher-level integration via the `disabled_integrations` option. - """ - integrations = dict( - (integration.identifier, integration) for integration in integrations or () - ) - - logger.debug("Setting up integrations (with default = %s)", with_defaults) - - user_provided_integrations = set(integrations.keys()) - - # Integrations that will not be enabled - disabled_integrations = [ - integration if isinstance(integration, type) else type(integration) - for integration in disabled_integrations or [] - ] - - # Integrations that are not explicitly set up by the user. - used_as_default_integration = set() - - if with_defaults: - for integration_cls in iter_default_integrations( - with_auto_enabling_integrations - ): - if integration_cls.identifier not in integrations: - instance = integration_cls() - integrations[instance.identifier] = instance - used_as_default_integration.add(instance.identifier) - - disabled_integration_identifiers = { - integration.identifier for integration in disabled_integrations - } - - for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): - if ( - integration in integrations - and integration not in disabled_integration_identifiers - ): - for target in targets_to_deactivate: - if target not in user_provided_integrations: - for cls in iter_default_integrations(True): - if cls.identifier == target: - if cls not in disabled_integrations: - disabled_integrations.append(cls) - logger.debug( - "Auto-deactivating %s integration because %s integration is active", - target, - integration, - ) - - for identifier, integration in integrations.items(): - with _installer_lock: - if identifier not in _processed_integrations: - if type(integration) in disabled_integrations: - logger.debug("Ignoring integration %s", identifier) - else: - logger.debug( - "Setting up previously not enabled integration %s", identifier - ) - try: - type(integration).setup_once() - integration.setup_once_with_options(options) - except DidNotEnable as e: - if identifier not in used_as_default_integration: - raise - - logger.debug( - "Did not enable default integration %s: %s", identifier, e - ) - else: - _installed_integrations.add(identifier) - - _processed_integrations.add(identifier) - - integrations = { - identifier: integration - for identifier, integration in integrations.items() - if identifier in _installed_integrations - } - - for identifier in integrations: - logger.debug("Enabling integration %s", identifier) - - return integrations - - -def _check_minimum_version( - integration: "type[Integration]", - version: "Optional[tuple[int, ...]]", - package: "Optional[str]" = None, -) -> None: - package = package or integration.identifier - - if version is None: - raise DidNotEnable(f"Unparsable {package} version.") - - min_version = _MIN_VERSIONS.get(integration.identifier) - if min_version is None: - return - - if version < min_version: - raise DidNotEnable( - f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." - ) - - -class DidNotEnable(Exception): # noqa: N818 - """ - The integration could not be enabled due to a trivial user error like - `flask` not being installed for the `FlaskIntegration`. - - This exception is silently swallowed for default integrations, but reraised - for explicitly enabled integrations. - """ - - -class Integration(ABC): - """Baseclass for all integrations. - - To accept options for an integration, implement your own constructor that - saves those options on `self`. - """ - - install = None - """Legacy method, do not implement.""" - - identifier: "str" = None # type: ignore[assignment] - """String unique ID of integration type""" - - @staticmethod - @abstractmethod - def setup_once() -> None: - """ - Initialize the integration. - - This function is only called once, ever. Configuration is not available - at this point, so the only thing to do here is to hook into exception - handlers, and perhaps do monkeypatches. - - Inside those hooks `Integration.current` can be used to access the - instance again. - """ - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - """ - Called after setup_once in rare cases on the instance and with options since we don't have those available above. - """ - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py deleted file mode 100644 index 7ff4657013..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_asgi_common.py +++ /dev/null @@ -1,187 +0,0 @@ -import urllib -from enum import Enum -from typing import TYPE_CHECKING - -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.scope import should_send_default_pii - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from typing_extensions import Literal - - from sentry_sdk.utils import AnnotatedValue - - -class _RootPathInPath(Enum): - EXCLUDED = "excluded" - EITHER = "either" - - -def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": - """ - Extract headers from the ASGI scope, in the format that the Sentry protocol expects. - """ - headers: "Dict[str, str]" = {} - for raw_key, raw_value in asgi_scope["headers"]: - key = raw_key.decode("latin-1") - value = raw_value.decode("latin-1") - if key in headers: - headers[key] = headers[key] + ", " + value - else: - headers[key] = value - - return headers - - -def _get_path( - asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" -) -> "str": - if root_path_in_path is _RootPathInPath.EXCLUDED: - return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") - - # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 - path = asgi_scope["path"] - root_path = asgi_scope.get("root_path", "") - - if not root_path or path == root_path or path.startswith(root_path + "/"): - return path - - return root_path + path - - -def _get_url( - asgi_scope: "Dict[str, Any]", - default_scheme: "Literal['ws', 'http']", - host: "Optional[Union[AnnotatedValue, str]]", - path: str, -) -> str: - """ - Extract URL from the ASGI scope, without also including the querystring. - """ - scheme = asgi_scope.get("scheme", default_scheme) - - server = asgi_scope.get("server", None) - - if host: - return "%s://%s%s" % (scheme, host, path) - - if server is not None: - host, port = server - default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) - if port != default_port: - return "%s://%s:%s%s" % (scheme, host, port, path) - return "%s://%s%s" % (scheme, host, path) - return path - - -def _get_query(asgi_scope: "Any") -> "Any": - """ - Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. - """ - qs = asgi_scope.get("query_string") - if not qs: - return None - return urllib.parse.unquote(qs.decode("latin-1")) - - -def _get_ip(asgi_scope: "Any") -> str: - """ - Extract IP Address from the ASGI scope based on request headers with fallback to scope client. - """ - headers = _get_headers(asgi_scope) - try: - return headers["x-forwarded-for"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return headers["x-real-ip"] - except KeyError: - pass - - return asgi_scope.get("client")[0] - - -def _get_request_data( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "Dict[str, Any]": - """ - Returns data related to the HTTP request from the ASGI scope. - """ - request_data: "Dict[str, Any]" = {} - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - request_data["method"] = asgi_scope.get("method") - - headers = _get_headers(asgi_scope) - - request_data["headers"] = _filter_headers( - headers, - use_annotated_value=False, - ) - - request_data["query_string"] = _get_query(asgi_scope) - - request_data["url"] = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} - - return request_data - - -def _get_request_attributes( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "dict[str, Any]": - """ - Return attributes related to the HTTP request from the ASGI scope. - """ - attributes: "dict[str, Any]" = {} - - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - if asgi_scope.get("method"): - attributes["http.request.method"] = asgi_scope["method"].upper() - - headers = _get_headers(asgi_scope) - - filtered_headers = _filter_headers(headers, use_annotated_value=False) - for header, value in filtered_headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - if should_send_default_pii(): - query = _get_query(asgi_scope) - if query: - attributes["http.query"] = query - - path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) - attributes["url.path"] = path - - url_without_query_string = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=path, - ) - query_string = _get_query(asgi_scope) - attributes["url.full"] = ( - f"{url_without_query_string}?{query_string}" - if query_string is not None - else url_without_query_string - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - ip = _get_ip(asgi_scope) - attributes["client.address"] = ip - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py deleted file mode 100644 index ad0ab7c734..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/_wsgi_common.py +++ /dev/null @@ -1,282 +0,0 @@ -import json -from contextlib import contextmanager -from copy import deepcopy - -import sentry_sdk -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE -from sentry_sdk.data_collection import _apply_key_value_collection_filtering -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger - -try: - from django.http.request import RawPostDataException - - _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) -except ImportError: - RawPostDataException = None - _RAW_DATA_EXCEPTIONS = (ValueError,) - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union - - from sentry_sdk._types import Event, HttpStatusCodeRange - - -SENSITIVE_ENV_KEYS = ( - "REMOTE_ADDR", - "HTTP_X_FORWARDED_FOR", - "HTTP_SET_COOKIE", - "HTTP_COOKIE", - "HTTP_AUTHORIZATION", - "HTTP_PROXY_AUTHORIZATION", - "HTTP_X_API_KEY", - "HTTP_X_FORWARDED_FOR", - "HTTP_X_REAL_IP", -) - -SENSITIVE_HEADERS = tuple( - x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") -) - -DEFAULT_HTTP_METHODS_TO_CAPTURE = ( - "CONNECT", - "DELETE", - "GET", - # "HEAD", # do not capture HEAD requests by default - # "OPTIONS", # do not capture OPTIONS requests by default - "PATCH", - "POST", - "PUT", - "TRACE", -) - - -# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support -@contextmanager -def nullcontext() -> "Iterator[None]": - yield - - -def request_body_within_bounds( - client: "Optional[sentry_sdk.client.BaseClient]", content_length: int -) -> bool: - if client is None: - return False - - bodies = client.options["max_request_body_size"] - return not ( - bodies == "never" - or (bodies == "small" and content_length > 10**3) - or (bodies == "medium" and content_length > 10**4) - ) - - -class RequestExtractor: - """ - Base class for request extraction. - """ - - # It does not make sense to make this class an ABC because it is not used - # for typing, only so that child classes can inherit common methods from - # it. Only some child classes implement all methods that raise - # NotImplementedError in this class. - - def __init__(self, request: "Any") -> None: - self.request = request - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - - content_length = self.content_length() - request_info = event.get("request", {}) - - if should_send_default_pii(): - request_info["cookies"] = dict(self.cookies()) - - if not request_body_within_bounds(client, content_length): - data = AnnotatedValue.removed_because_over_size_limit() - else: - # First read the raw body data - # It is important to read this first because if it is Django - # it will cache the body and then we can read the cached version - # again in parsed_body() (or json() or wherever). - raw_data = None - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # If DjangoRestFramework is used it already read the body for us - # so reading it here will fail. We can ignore this. - pass - - parsed_body = self.parsed_body() - if parsed_body is not None: - data = parsed_body - elif raw_data: - data = AnnotatedValue.removed_because_raw_data() - else: - data = None - - if data is not None: - request_info["data"] = data - - event["request"] = deepcopy(request_info) - - def content_length(self) -> int: - try: - return int(self.env().get("CONTENT_LENGTH", 0)) - except ValueError: - return 0 - - def cookies(self) -> "MutableMapping[str, Any]": - raise NotImplementedError() - - def raw_data(self) -> "Optional[Union[str, bytes]]": - raise NotImplementedError() - - def form(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - form = self.form() - except Exception: - form = None - try: - files = self.files() - except Exception: - files = None - - if form or files: - data = {} - if form: - data = dict(form.items()) - if files: - for key in files.keys(): - data[key] = AnnotatedValue.removed_because_raw_data() - - return data - - return self.json() - - def is_json(self) -> bool: - return _is_json_content_type(self.env().get("CONTENT_TYPE")) - - def json(self) -> "Optional[Any]": - try: - if not self.is_json(): - return None - - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # The body might have already been read, in which case this will - # fail - raw_data = None - - if raw_data is None: - return None - - if isinstance(raw_data, str): - return json.loads(raw_data) - else: - return json.loads(raw_data.decode("utf-8")) - except ValueError: - pass - - return None - - def files(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def size_of_file(self, file: "Any") -> int: - raise NotImplementedError() - - def env(self) -> "Dict[str, Any]": - raise NotImplementedError() - - -def _is_json_content_type(ct: "Optional[str]") -> bool: - mt = (ct or "").split(";", 1)[0] - return ( - mt == "application/json" - or (mt.startswith("application/")) - and mt.endswith("+json") - ) - - -def _filter_headers( - headers: "Mapping[str, str]", - use_annotated_value: bool = True, -) -> "Mapping[str, Union[AnnotatedValue, str]]": - client_options = sentry_sdk.get_client().options - - if has_data_collection_enabled(client_options): - data_collection_configuration = client_options["data_collection"] - - filtered = _apply_key_value_collection_filtering( - items=headers, - behaviour=data_collection_configuration["http_headers"]["request"], - ) - - for key in filtered: - if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): - filtered[key] = SENSITIVE_DATA_SUBSTITUTE - - return filtered - else: - if should_send_default_pii(): - return headers - - substitute: "Union[AnnotatedValue, str]" = ( - SENSITIVE_DATA_SUBSTITUTE - if not use_annotated_value - else AnnotatedValue.removed_because_over_size_limit() - ) - - return { - k: ( - v - if k.upper().replace("-", "_") not in SENSITIVE_HEADERS - else substitute - ) - for k, v in headers.items() - } - - -def _in_http_status_code_range( - code: object, code_ranges: "list[HttpStatusCodeRange]" -) -> bool: - for target in code_ranges: - if isinstance(target, int): - if code == target: - return True - continue - - try: - if code in target: - return True - except TypeError: - logger.warning( - "failed_request_status_codes has to be a list of integers or containers" - ) - - return False - - -class HttpCodeRangeContainer: - """ - Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. - Used for backwards compatibility with the old `failed_request_status_codes` option. - """ - - def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: - self._code_ranges = code_ranges - - def __contains__(self, item: object) -> bool: - return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py deleted file mode 100644 index 59bae92a60..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiohttp.py +++ /dev/null @@ -1,509 +0,0 @@ -import sys -import weakref -from functools import wraps - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import ( - _filter_headers, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - NoOpStreamedSpan, - SegmentSource, - SpanStatus, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import ( - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - parse_url, - parse_version, - reraise, - transaction_from_function, -) - -try: - import asyncio - - from aiohttp import ClientSession, TraceConfig - from aiohttp import __version__ as AIOHTTP_VERSION - from aiohttp.web import Application, HTTPException, UrlDispatcher -except ImportError: - raise DidNotEnable("AIOHTTP not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Set - from types import SimpleNamespace - from typing import Any, ContextManager, Optional, Tuple, Union - - from aiohttp import TraceRequestEndParams, TraceRequestStartParams - from aiohttp.web_request import Request - from aiohttp.web_urldispatcher import UrlMappingMatchInfo - - from sentry_sdk._types import Attributes, Event, EventProcessor - from sentry_sdk.tracing import Span - from sentry_sdk.utils import ExcInfo - - -TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") - - -class AioHttpIntegration(Integration): - identifier = "aiohttp" - origin = f"auto.http.{identifier}" - - def __init__( - self, - transaction_style: str = "handler_name", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self._failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(AIOHTTP_VERSION) - _check_minimum_version(AioHttpIntegration, version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - # In the aiohttp integration, all of their HTTP responses are Exceptions. - # Because they have to be raised and handled by the framework, we need to - # register the exceptions as control flow exceptions so that we don't - # accidentally overwrite a status of "ok" with "error". - _register_control_flow_exception(HTTPException) - - ignore_logger("aiohttp.server") - - old_handle = Application._handle - - async def sentry_app_handle( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(AioHttpIntegration) - if integration is None: - return await old_handle(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - # Scope data will not leak between requests because aiohttp - # create a task to wrap each request. - scope.generate_propagation_context() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - headers = dict(request.headers) - - span_ctx: "ContextManager[Union[Span, StreamedSpan]]" - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context({"aiohttp_request": request}) - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, - use_annotated_value=False, - ).items(): - header_attributes[ - f"http.request.header.{header.lower()}" - ] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - url_attributes = {} - if should_send_default_pii(): - url_attributes["url.full"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - url_attributes["url.path"] = request.path - - if request.query_string: - url_attributes["url.query"] = request.query_string - - client_address_attributes = {} - if should_send_default_pii() and request.remote: - client_address_attributes["client.address"] = request.remote - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, request.remote - ) - - span_ctx = sentry_sdk.traces.start_span( - # If this name makes it to the UI, AIOHTTP's URL - # resolver did not find a route or died trying. - name="generic AIOHTTP request", - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": AioHttpIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE.value, - "http.request.method": request.method, - **url_attributes, - **client_address_attributes, - **header_attributes, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # If this transaction name makes it to the UI, AIOHTTP's - # URL resolver did not find a route or died trying. - name="generic AIOHTTP request", - source=TransactionSource.ROUTE, - origin=AioHttpIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"aiohttp_request": request}, - ) - - with span_ctx as span: - try: - response = await old_handle(self, request) - except HTTPException as e: - if isinstance(span, StreamedSpan) and not isinstance( - span, NoOpStreamedSpan - ): - span.set_attribute( - "http.response.status_code", e.status_code - ) - - if e.status_code >= 400: - span.status = SpanStatus.ERROR.value - else: - span.status = SpanStatus.OK.value - else: - # Since a NoOpStreamedSpan can end up here, we have to guard against it - # so this only gets set in the legacy transaction approach. - if not isinstance(span, NoOpStreamedSpan): - span.set_http_status(e.status_code) - - if ( - e.status_code - in integration._failed_request_status_codes - ): - _capture_exception() - raise - except (asyncio.CancelledError, ConnectionResetError): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR.value - else: - span.set_status(SPANSTATUS.CANCELLED) - raise - except Exception: - # This will probably map to a 500 but seems like we - # have no way to tell. Do not set span status. - reraise(*_capture_exception()) - - try: - # A valid response handler will return a valid response with a status. But, if the handler - # returns an invalid response (e.g. None), the line below will raise an AttributeError. - # Even though this is likely invalid, we need to handle this case to ensure we don't break - # the application. - response_status = response.status - except AttributeError: - pass - else: - if isinstance(span, StreamedSpan): - span.set_attribute( - "http.response.status_code", response_status - ) - span.status = ( - SpanStatus.ERROR.value - if response_status >= 400 - else SpanStatus.OK.value - ) - else: - span.set_http_status(response_status) - - return response - - Application._handle = sentry_app_handle - - old_urldispatcher_resolve = UrlDispatcher.resolve - - @wraps(old_urldispatcher_resolve) - async def sentry_urldispatcher_resolve( - self: "UrlDispatcher", request: "Request" - ) -> "UrlMappingMatchInfo": - rv = await old_urldispatcher_resolve(self, request) - - integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) - if integration is None: - return rv - - name = None - - try: - if integration.transaction_style == "handler_name": - name = transaction_from_function(rv.handler) - elif integration.transaction_style == "method_and_path_pattern": - route_info = rv.get_info() - pattern = route_info.get("path") or route_info.get("formatter") - name = "{} {}".format(request.method, pattern) - except Exception: - pass - - if name is not None: - current_span = sentry_sdk.get_current_span() - if isinstance(current_span, StreamedSpan) and not isinstance( - current_span, NoOpStreamedSpan - ): - current_span._segment.name = name - current_span._segment.set_attribute( - "sentry.span.source", - SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, - ) - else: - current_scope = sentry_sdk.get_current_scope() - current_scope.set_transaction_name( - name, - source=SOURCE_FOR_STYLE[integration.transaction_style], - ) - - return rv - - UrlDispatcher.resolve = sentry_urldispatcher_resolve - - old_client_session_init = ClientSession.__init__ - - @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) - def init(*args: "Any", **kwargs: "Any") -> None: - client_trace_configs = list(kwargs.get("trace_configs") or ()) - trace_config = create_trace_config() - client_trace_configs.append(trace_config) - - kwargs["trace_configs"] = client_trace_configs - return old_client_session_init(*args, **kwargs) - - ClientSession.__init__ = init - - -def create_trace_config() -> "TraceConfig": - async def on_request_start( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestStartParams", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(AioHttpIntegration) is None: - return - - method = params.method.upper() - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(params.url), sanitize=False) - - span_name = "%s %s" % ( - method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ) - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(client.options): - attributes: "Attributes" = { - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": AioHttpIntegration.origin, - "http.request.method": method, - } - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - attributes["url.path"] = params.url.path - - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) - else: - legacy_span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=span_name, - origin=AioHttpIntegration.origin, - ) - legacy_span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - legacy_span.set_data("url", parsed_url.url) - legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span = legacy_span - - if should_propagate_trace(client, str(params.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=params.url - ) - ) - if key == BAGGAGE_HEADER_NAME and params.headers.get( - BAGGAGE_HEADER_NAME - ): - # do not overwrite any existing baggage, just append to it - params.headers[key] += "," + value - else: - params.headers[key] = value - - trace_config_ctx.span = span - - async def on_request_end( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestEndParams", - ) -> None: - if trace_config_ctx.span is None: - return - - span = trace_config_ctx.span - status = int(params.response.status) - - if isinstance(span, StreamedSpan): - span.set_attribute("http.response.status_code", status) - span.status = ( - SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value - ) - - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.set_http_status(status) - span.set_data("reason", params.response.reason) - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - trace_config = TraceConfig() - - trace_config.on_request_start.append(on_request_start) - trace_config.on_request_end.append(on_request_end) - - return trace_config - - -def _make_request_processor( - weak_request: "weakref.ReferenceType[Request]", -) -> "EventProcessor": - def aiohttp_processor( - event: "Event", - hint: "dict[str, Tuple[type, BaseException, Any]]", - ) -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - request_info = event.setdefault("request", {}) - - request_info["url"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote} - request_info["headers"] = _filter_headers(dict(request.headers)) - - # Just attach raw data here if it is within bounds, if available. - # Unfortunately there's no way to get structured data from aiohttp - # without awaiting on some coroutine. - request_info["data"] = get_aiohttp_request_data(request) - - return event - - return aiohttp_processor - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "aiohttp", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - return exc_info - - -BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" - - -def get_aiohttp_request_data( - request: "Request", -) -> "Union[Optional[str], AnnotatedValue]": - bytes_body = request._read_bytes - - if bytes_body is not None: - # we have body to show - if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): - return AnnotatedValue.substituted_because_over_size_limit() - - encoding = request.charset or "utf-8" - return bytes_body.decode(encoding, "replace") - - if request.can_read_body: - # body exists but we can't show it - return BODY_NOT_READ_MESSAGE - - # request has no body - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py deleted file mode 100644 index 49459268e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aiomysql.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import annotations - -from typing import Any, Awaitable, Callable, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import aiomysql # type: ignore[import-not-found] - from aiomysql.connection import Connection # type: ignore[import-not-found] - from aiomysql.cursors import Cursor # type: ignore[import-not-found] -except ImportError: - raise DidNotEnable("aiomysql not installed.") - - -class AioMySQLIntegration(Integration): - identifier = "aiomysql" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AioMySQLIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - aiomysql_version = parse_version(aiomysql.__version__) - _check_minimum_version(AioMySQLIntegration, aiomysql_version) - - Cursor.execute = _wrap_execute(Cursor.execute) - Cursor.executemany = _wrap_executemany(Cursor.executemany) - - # Patch Connection._connect — this catches ALL connections: - # - aiomysql.connect() - # - aiomysql.create_pool() (pool.py does `from .connection import connect` - # which ultimately calls Connection._connect) - # - Reconnects - Connection._connect = _wrap_connect(Connection._connect) - - -T = TypeVar("T") - - -def _normalize_query(query: str | bytes | bytearray) -> str: - if isinstance(query, (bytes, bytearray)): - query = query.decode("utf-8", errors="replace") - return " ".join(query.split()) - - -def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.execute to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - - # Skip if flagged by executemany (avoids double-recording). - # Do NOT reset the flag here — it must stay True for the entire - # duration of executemany, which may call execute multiple times - # in a loop (non-INSERT fallback). Only _wrap_executemany's - # finally block should clear it. - if getattr(cursor, "_sentry_skip_next_execute", False): - return await f(*args, **kwargs) - - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = params if integration and integration._record_params else None - param_style = "pyformat" if params_list else None - - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=False, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.executemany to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = ( - seq_of_params if integration and integration._record_params else None - ) - param_style = "pyformat" if params_list else None - - # Prevent double-recording: _do_execute_many calls self.execute internally - cursor._sentry_skip_next_execute = True - try: - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=True, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - finally: - cursor._sentry_skip_next_execute = False - - return _inner - - -def _get_connection(cursor: Any) -> Any: - """Get the underlying connection from a cursor.""" - return getattr(cursor, "connection", None) - - -def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Connection._connect to capture connection spans.""" - - async def _inner(self: "Connection") -> T: - client = sentry_sdk.get_client() - if client.get_integration(AioMySQLIntegration) is None: - return await f(self) - - if has_span_streaming_enabled(client.options): - breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) - - span_attributes: dict[str, Any] = { - "sentry.op": OP.DB, - "sentry.origin": AioMySQLIntegration.origin, - } | breadcrumb_data - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=breadcrumb_data - ) - res = await f(self) - else: - connect_data = _get_connect_data(self) - - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AioMySQLIntegration.origin, - ) as span: - _set_db_data(span, self) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", - category="query", - data=connect_data, - ) - res = await f(self) - - return res - - return _inner - - -def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: - if use_streaming_keys: - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - data: dict[str, Any] = { - db_system: "mysql", - SPANDATA.DB_DRIVER_NAME: "aiomysql", - } - - host = getattr(conn, "host", None) - if host is not None: - data[SPANDATA.SERVER_ADDRESS] = host - - port = getattr(conn, "port", None) - if port is not None: - data[SPANDATA.SERVER_PORT] = port - - database = getattr(conn, "db", None) - if database is not None: - data[db_name] = database - - user = getattr(conn, "user", None) - if user is not None: - data[SPANDATA.DB_USER] = user - - return data - - -def _set_db_data(span: Any, conn: Any) -> None: - """Set database-related span data from connection object.""" - if isinstance(span, StreamedSpan): - set_value = span.set_attribute - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - set_value = span.set_data - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - set_value(db_system, "mysql") - set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") - - host = getattr(conn, "host", None) - if host is not None: - set_value(SPANDATA.SERVER_ADDRESS, host) - - port = getattr(conn, "port", None) - if port is not None: - set_value(SPANDATA.SERVER_PORT, port) - - database = getattr(conn, "db", None) - if database is not None: - set_value(db_name, database) - - user = getattr(conn, "user", None) - if user is not None: - set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py deleted file mode 100644 index dfa4aef34c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/anthropic.py +++ /dev/null @@ -1,1154 +0,0 @@ -import json -import sys -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_anthropic_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - package_version, - reraise, - safe_serialize, -) - -try: - try: - from anthropic import NotGiven - except ImportError: - NotGiven = None - - try: - from anthropic import Omit - except ImportError: - Omit = None - - from anthropic import AsyncStream, Stream - from anthropic.lib.streaming import ( - AsyncMessageStream, - AsyncMessageStreamManager, - MessageStream, - MessageStreamManager, - ) - from anthropic.resources import AsyncMessages, Messages - from anthropic.types import ( - ContentBlockDeltaEvent, - ContentBlockStartEvent, - ContentBlockStopEvent, - MessageDeltaEvent, - MessageStartEvent, - MessageStopEvent, - ) - - if TYPE_CHECKING: - from anthropic.types import MessageStreamEvent, TextBlockParam -except ImportError: - raise DidNotEnable("Anthropic not installed") - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Iterator, - Optional, - Union, - ) - - from anthropic.types import ( - MessageParam, - ModelParam, - RawMessageStreamEvent, - TextBlockParam, - ToolUnionParam, - ) - - from sentry_sdk._types import TextPart - - -class _RecordedUsage: - output_tokens: int = 0 - input_tokens: int = 0 - cache_write_input_tokens: "Optional[int]" = 0 - cache_read_input_tokens: "Optional[int]" = 0 - - -class _StreamSpanContext: - """ - Sets accumulated data on the stream's span and finishes the span on exit. - Is a no-op if the stream has no span set, i.e., when the span has already been finished. - """ - - def __init__( - self, - stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", - # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). - guaranteed_streaming_state: bool = False, - ) -> None: - self._stream = stream - self._guaranteed_streaming_state = guaranteed_streaming_state - - def __enter__(self) -> "_StreamSpanContext": - return self - - def __exit__( - self, - exc_type: "Optional[type[BaseException]]", - exc_val: "Optional[BaseException]", - exc_tb: "Optional[Any]", - ) -> None: - with capture_internal_exceptions(): - if not hasattr(self._stream, "_span"): - return - - if not self._guaranteed_streaming_state and not hasattr( - self._stream, "_model" - ): - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - return - - _set_streaming_output_data( - span=self._stream._span, - integration=self._stream._integration, - model=self._stream._model, - usage=self._stream._usage, - content_blocks=self._stream._content_blocks, - response_id=self._stream._response_id, - finish_reason=self._stream._finish_reason, - ) - - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - - -class AnthropicIntegration(Integration): - identifier = "anthropic" - origin = f"auto.ai.{identifier}" - - def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - version = package_version("anthropic") - _check_minimum_version(AnthropicIntegration, version) - - """ - client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. - Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. - The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, - information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. - - The span can be finished in two places: - - When the user exits the context manager or directly calls close(), the patched close() finishes the span. - - When iteration ends, the finally block in the _iterator wrapper finishes the span. - - Both paths may run. For example, the context manager exit can follow iterator exhaustion. - """ - Messages.create = _wrap_message_create(Messages.create) - Stream.close = _wrap_close(Stream.close) - - AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) - AsyncStream.close = _wrap_async_close(AsyncStream.close) - - """ - client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. - """ - Messages.stream = _wrap_message_stream(Messages.stream) - MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( - MessageStreamManager.__enter__ - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. - if not issubclass(MessageStream, Stream): - MessageStream.close = _wrap_close(MessageStream.close) - - AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) - AsyncMessageStreamManager.__aenter__ = ( - _wrap_async_message_stream_manager_aenter( - AsyncMessageStreamManager.__aenter__ - ) - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. - if not issubclass(AsyncMessageStream, AsyncStream): - AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "anthropic", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": - """ - Get token usage from the Anthropic response. - Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) - """ - input_tokens = 0 - output_tokens = 0 - cache_read_input_tokens = 0 - cache_write_input_tokens = 0 - if hasattr(result, "usage"): - usage = result.usage - if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): - input_tokens = usage.input_tokens - if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): - output_tokens = usage.output_tokens - if hasattr(usage, "cache_read_input_tokens") and isinstance( - usage.cache_read_input_tokens, int - ): - cache_read_input_tokens = usage.cache_read_input_tokens - if hasattr(usage, "cache_creation_input_tokens") and isinstance( - usage.cache_creation_input_tokens, int - ): - cache_write_input_tokens = usage.cache_creation_input_tokens - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens so downstream cost calculations - # (input_tokens - cached) don't produce negative values. - input_tokens += cache_read_input_tokens + cache_write_input_tokens - - return ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) - - -def _collect_ai_data( - event: "MessageStreamEvent", - model: "str | None", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": - """ - Collect model information, token usage, and collect content blocks from the AI streaming response. - """ - with capture_internal_exceptions(): - if hasattr(event, "type"): - if event.type == "content_block_start": - pass - elif event.type == "content_block_delta": - if hasattr(event.delta, "text"): - content_blocks.append(event.delta.text) - elif hasattr(event.delta, "partial_json"): - content_blocks.append(event.delta.partial_json) - elif event.type == "content_block_stop": - pass - - # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. - # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 - if event.type == "message_start": - model = event.message.model or model - response_id = event.message.id - - incoming_usage = event.message.usage - usage.output_tokens = incoming_usage.output_tokens - usage.input_tokens = incoming_usage.input_tokens - - usage.cache_write_input_tokens = getattr( - incoming_usage, "cache_creation_input_tokens", None - ) - usage.cache_read_input_tokens = getattr( - incoming_usage, "cache_read_input_tokens", None - ) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - # Counterintuitive, but message_delta contains cumulative token counts :) - if event.type == "message_delta": - usage.output_tokens = event.usage.output_tokens - - # Update other usage fields if they exist in the event - input_tokens = getattr(event.usage, "input_tokens", None) - if input_tokens is not None: - usage.input_tokens = input_tokens - - cache_creation_input_tokens = getattr( - event.usage, "cache_creation_input_tokens", None - ) - if cache_creation_input_tokens is not None: - usage.cache_write_input_tokens = cache_creation_input_tokens - - cache_read_input_tokens = getattr( - event.usage, "cache_read_input_tokens", None - ) - if cache_read_input_tokens is not None: - usage.cache_read_input_tokens = cache_read_input_tokens - # TODO: Record event.usage.server_tool_use - - if event.delta.stop_reason is not None: - finish_reason = event.delta.stop_reason - - return (model, usage, content_blocks, response_id, finish_reason) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - -def _transform_anthropic_content_block( - content_block: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Transform an Anthropic content block using the Anthropic-specific transformer, - with special handling for Anthropic's text-type documents. - """ - # Handle Anthropic's text-type documents specially (not covered by shared function) - if content_block.get("type") == "document": - source = content_block.get("source") - if isinstance(source, dict) and source.get("type") == "text": - return { - "type": "text", - "text": source.get("data", ""), - } - - # Use Anthropic-specific transformation - result = transform_anthropic_content_part(content_block) - return result if result is not None else content_block - - -def _transform_system_instructions( - system_instructions: "Union[str, Iterable[TextBlockParam]]", -) -> "list[TextPart]": - if isinstance(system_instructions, str): - return [ - { - "type": "text", - "content": system_instructions, - } - ] - - return [ - { - "type": "text", - "content": instruction["text"], - } - for instruction in system_instructions - if isinstance(instruction, dict) and "text" in instruction - ] - - -def _set_common_input_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - max_tokens: "int", - messages: "Iterable[MessageParam]", - model: "ModelParam", - system: "Optional[Union[str, Iterable[TextBlockParam]]]", - temperature: "Optional[float]", - top_k: "Optional[int]", - top_p: "Optional[float]", - tools: "Optional[Iterable[ToolUnionParam]]", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if ( - messages is not None - and len(messages) > 0 # type: ignore - and should_send_default_pii() - and integration.include_prompts - ): - if isinstance(system, str) or isinstance(system, Iterable): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system)), - ) - - normalized_messages = [] - for message in messages: - if ( - message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER - and "content" in message - and isinstance(message["content"], (list, tuple)) - ): - transformed_content = [] - for item in message["content"]: - # Skip tool_result items - they can contain images/documents - # with nested structures that are difficult to redact properly - if isinstance(item, dict) and item.get("type") == "tool_result": - continue - - # Transform content blocks (images, documents, etc.) - transformed_content.append( - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - ) - - # If there are non-tool-result items, add them as a message - if transformed_content: - normalized_messages.append( - { - "role": message.get("role"), - "content": transformed_content, - } - ) - else: - # Transform content for non-list messages or assistant messages - transformed_message = message.copy() - if "content" in transformed_message: - content = transformed_message["content"] - if isinstance(content, (list, tuple)): - transformed_message["content"] = [ - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - for item in content - ] - normalized_messages.append(transformed_message) - - role_normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(role_normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else role_normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - if model is not None and _is_given(model): - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - if top_k is not None and _is_given(top_k): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore - set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) - - -def _set_create_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "AnthropicIntegration", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=kwargs.get("max_tokens"), # type: ignore - messages=kwargs.get("messages"), # type: ignore - model=kwargs.get("model"), - system=kwargs.get("system"), - temperature=kwargs.get("temperature"), - top_k=kwargs.get("top_k"), - top_p=kwargs.get("top_p"), - tools=kwargs.get("tools"), - ) - - -def _wrap_synchronous_message_iterator( - stream: "Union[Stream, MessageStream]", - iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -async def _wrap_asynchronous_message_iterator( - stream: "Union[AsyncStream, AsyncMessageStream]", - iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - async for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -def _set_output_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - model: "str | None", - input_tokens: "int | None", - output_tokens: "int | None", - cache_read_input_tokens: "int | None", - cache_write_input_tokens: "int | None", - content_blocks: "list[Any]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> None: - """ - Set output data for the span based on the AI response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - if finish_reason is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in content_blocks: - if output["type"] == "text": - output_messages["response"].append(output["text"]) - elif output["type"] == "tool_use": - output_messages["tool"].append(output) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - record_token_usage( - span, - input_tokens=input_tokens, - output_tokens=output_tokens, - input_tokens_cached=cache_read_input_tokens, - input_tokens_cache_write=cache_write_input_tokens, - ) - - -def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, Stream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_synchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -async def _sentry_patched_create_async( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return await f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, AsyncStream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_asynchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -def _wrap_message_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return _sentry_patched_create_sync(f, *args, **kwargs) - - return _sentry_wrapped_create_sync - - -def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: - """ - Initialize fields for accumulating output on the Stream instance. - """ - if not hasattr(stream, "_model"): - stream._model = None - stream._usage = _RecordedUsage() - stream._content_blocks = [] - stream._response_id = None - stream._finish_reason = None - - -def _accumulate_event_data( - stream: "Union[Stream, MessageStream]", - event: "Union[RawMessageStreamEvent, MessageStreamEvent]", -) -> None: - """ - Update accumulated output from a single stream event. - """ - (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( - event, - stream._model, - stream._usage, - stream._content_blocks, - stream._response_id, - stream._finish_reason, - ) - - stream._model = model - stream._usage = usage - stream._content_blocks = content_blocks - stream._response_id = response_id - stream._finish_reason = finish_reason - - -def _set_streaming_output_data( - span: "Span", - integration: "AnthropicIntegration", - model: "Optional[str]", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "Optional[str]", - finish_reason: "Optional[str]", -) -> None: - """ - Set output attributes on the AI Client Span. - """ - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - response_id=response_id, - finish_reason=finish_reason, - ) - - -def _wrap_close( - f: "Callable[..., None]", -) -> "Callable[..., None]": - """ - Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. - """ - - def close(self: "Union[Stream, MessageStream]") -> None: - with _StreamSpanContext(self): - return f(self) - - return close - - -def _wrap_message_create_async(f: "Any") -> "Any": - @wraps(f) - async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return await _sentry_patched_create_async(f, *args, **kwargs) - - return _sentry_wrapped_create_async - - -def _wrap_async_close( - f: "Callable[..., Awaitable[None]]", -) -> "Callable[..., Awaitable[None]]": - """ - Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. - """ - - async def close(self: "AsyncStream") -> None: - with _StreamSpanContext(self): - return await f(self) - - return close - - -def _wrap_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_message_stream_manager_enter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": - if not hasattr(self, "_max_tokens"): - return f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return f(self) - - if self._messages is None: - return f(self) - - try: - iter(self._messages) - except TypeError: - return f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_synchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_enter - - -def _wrap_async_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream( - *args: "Any", **kwargs: "Any" - ) -> "AsyncMessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - async def _sentry_patched_aenter( - self: "AsyncMessageStreamManager", - ) -> "AsyncMessageStream": - if not hasattr(self, "_max_tokens"): - return await f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return await f(self) - - if self._messages is None: - return await f(self) - - try: - iter(self._messages) - except TypeError: - return await f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = await f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_asynchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_aenter - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different anthropic versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py deleted file mode 100644 index 0215ffa093..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/argv.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class ArgvIntegration(Integration): - identifier = "argv" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: - extra = event.setdefault("extra", {}) - # If some event processor decided to set extra to e.g. an - # `int`, don't crash. Not here. - if isinstance(extra, dict): - extra["sys.argv"] = sys.argv - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py deleted file mode 100644 index 1ce228d3a5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ariadne.py +++ /dev/null @@ -1,167 +0,0 @@ -from importlib import import_module - -import sentry_sdk -from sentry_sdk import capture_event, get_client -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - # importing like this is necessary due to name shadowing in ariadne - # (ariadne.graphql is also a function) - ariadne_graphql = import_module("ariadne.graphql") -except ImportError: - raise DidNotEnable("ariadne is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional - - from ariadne.types import ( # type: ignore - GraphQLError, - GraphQLResult, - GraphQLSchema, - QueryParser, - ) - from graphql.language.ast import DocumentNode - - from sentry_sdk._types import Event, EventProcessor - - -class AriadneIntegration(Integration): - identifier = "ariadne" - - @staticmethod - def setup_once() -> None: - version = package_version("ariadne") - _check_minimum_version(AriadneIntegration, version) - - ignore_logger("ariadne") - - _patch_graphql() - - -def _patch_graphql() -> None: - old_parse_query = ariadne_graphql.parse_query - old_handle_errors = ariadne_graphql.handle_graphql_errors - old_handle_query_result = ariadne_graphql.handle_query_result - - @ensure_integration_enabled(AriadneIntegration, old_parse_query) - def _sentry_patched_parse_query( - context_value: "Optional[Any]", - query_parser: "Optional[QueryParser]", - data: "Any", - ) -> "DocumentNode": - event_processor = _make_request_event_processor(data) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - result = old_parse_query(context_value, query_parser, data) - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_errors) - def _sentry_patched_handle_graphql_errors( - errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - result = old_handle_errors(errors, *args, **kwargs) - - event_processor = _make_response_event_processor(result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) - def _sentry_patched_handle_query_result( - result: "Any", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - query_result = old_handle_query_result(result, *args, **kwargs) - - event_processor = _make_response_event_processor(query_result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return query_result - - ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore - ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore - ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore - - -def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": - """Add request data and api_target to events.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - if not isinstance(data, dict): - return event - - with capture_internal_exceptions(): - try: - content_length = int( - (data.get("headers") or {}).get("Content-Length", 0) - ) - except (TypeError, ValueError): - return event - - if should_send_default_pii() and request_body_within_bounds( - get_client(), content_length - ): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - request_info["data"] = data - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - return inner - - -def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": - """Add response data to the event's response context.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii() and response.get("errors"): - contexts = event.setdefault("contexts", {}) - contexts["response"] = { - "data": response, - } - - return event - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py deleted file mode 100644 index da03bafb8b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/arq.py +++ /dev/null @@ -1,277 +0,0 @@ -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Transaction, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -try: - import arq.worker - from arq.connections import ArqRedis - from arq.version import VERSION as ARQ_VERSION - from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker -except ImportError: - raise DidNotEnable("Arq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from arq.cron import CronJob - from arq.jobs import Job - from arq.typing import WorkerCoroutine - from arq.worker import Function - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) - - -class ArqIntegration(Integration): - identifier = "arq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - try: - if isinstance(ARQ_VERSION, str): - version = parse_version(ARQ_VERSION) - else: - version = ARQ_VERSION.version[:2] - - except (TypeError, ValueError): - version = None - - _check_minimum_version(ArqIntegration, version) - - patch_enqueue_job() - patch_run_job() - patch_create_worker() - - _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore - - ignore_logger("arq.worker") - - -def patch_enqueue_job() -> None: - old_enqueue_job = ArqRedis.enqueue_job - original_kwdefaults = old_enqueue_job.__kwdefaults__ - - async def _sentry_enqueue_job( - self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" - ) -> "Optional[Job]": - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_enqueue_job(self, function, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=function, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_ARQ, - "sentry.origin": ArqIntegration.origin, - }, - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults - ArqRedis.enqueue_job = _sentry_enqueue_job - - -def patch_run_job() -> None: - old_run_job = Worker.run_job - - async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_run_job(self, job_id, score) - - with sentry_sdk.isolation_scope() as scope: - scope._name = "arq" - scope.clear_breadcrumbs() - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name="unknown arq task", - attributes={ - "sentry.op": OP.QUEUE_TASK_ARQ, - "sentry.origin": ArqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job_id, - }, - parent_span=None, - ): - return await old_run_job(self, job_id, score) - - transaction = Transaction( - name="unknown arq task", - status="ok", - op=OP.QUEUE_TASK_ARQ, - source=TransactionSource.TASK, - origin=ArqIntegration.origin, - ) - - with sentry_sdk.start_transaction(transaction): - return await old_run_job(self, job_id, score) - - Worker.run_job = _sentry_run_job - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - - if scope.transaction is not None: - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - scope.transaction.set_status(SPANSTATUS.ABORTED) - return - - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": ArqIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - if scope.transaction is not None: - scope.transaction.name = ctx["job_name"] - event["transaction"] = ctx["job_name"] - - tags = event.setdefault("tags", {}) - tags["arq_task_id"] = ctx["job_id"] - tags["arq_task_retry"] = ctx["job_try"] > 1 - extra = event.setdefault("extra", {}) - extra["arq-job"] = { - "task": ctx["job_name"], - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": ctx["job_try"], - } - - return event - - return event_processor - - -def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": - async def _sentry_coroutine( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(ArqIntegration) - if integration is None: - return await coroutine(ctx, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - scope = sentry_sdk.get_current_scope() - span = scope.streamed_span - if span is not None: - span.name = name - - scope.set_transaction_name(name) - - sentry_sdk.get_isolation_scope().add_event_processor( - _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) - ) - - try: - result = await coroutine(ctx, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_coroutine - - -def patch_create_worker() -> None: - old_create_worker = arq.worker.create_worker - - @ensure_integration_enabled(ArqIntegration, old_create_worker) - def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": - settings_cls = args[0] if args else kwargs.get("settings_cls") - - if isinstance(settings_cls, dict): - if "functions" in settings_cls: - settings_cls["functions"] = [ - _get_arq_function(func) - for func in settings_cls.get("functions", []) - ] - if "cron_jobs" in settings_cls: - settings_cls["cron_jobs"] = [ - _get_arq_cron_job(cron_job) - for cron_job in settings_cls.get("cron_jobs", []) - ] - - if hasattr(settings_cls, "functions"): - settings_cls.functions = [ # type: ignore[union-attr] - _get_arq_function(func) - for func in settings_cls.functions # type: ignore[union-attr] - ] - if hasattr(settings_cls, "cron_jobs"): - settings_cls.cron_jobs = [ # type: ignore[union-attr] - _get_arq_cron_job(cron_job) - for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] - ] - - if "functions" in kwargs: - kwargs["functions"] = [ - _get_arq_function(func) for func in kwargs.get("functions", []) - ] - if "cron_jobs" in kwargs: - kwargs["cron_jobs"] = [ - _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) - ] - - return old_create_worker(*args, **kwargs) - - arq.worker.create_worker = _sentry_create_worker - - -def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": - arq_func = arq.worker.func(func) - arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) - - return arq_func - - -def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": - cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) - - return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py deleted file mode 100644 index 8b1ff5e2a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asgi.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -An ASGI middleware. - -Based on Tom Christie's `sentry-asgi `. -""" - -import inspect -import sys -from copy import deepcopy -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._asgi_common import ( - _get_headers, - _get_ip, - _get_path, - _get_request_attributes, - _get_request_data, - _get_url, - _RootPathInPath, -) -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - SegmentSource, - StreamedSpan, -) -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - Transaction, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, - _get_installed_modules, - capture_internal_exceptions, - event_from_exception, - logger, - qualname_from_function, - reraise, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, ContextManager, Dict, Optional, Tuple, Union - - from sentry_sdk._types import Attributes, Event, Hint - from sentry_sdk.tracing import Span - - -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") - -_DEFAULT_TRANSACTION_NAME = "generic ASGI request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 -if sys.version_info >= (3, 14): - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": mechanism_type, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _looks_like_asgi3(app: "Any") -> bool: - """ - Try to figure out if an application object supports ASGI3. - - This is how uvicorn figures out the application version as well. - """ - if inspect.isclass(app): - return hasattr(app, "__await__") - elif inspect.isfunction(app): - return iscoroutinefunction(app) - else: - call = getattr(app, "__call__", None) # noqa - return iscoroutinefunction(call) - - -class SentryAsgiMiddleware: - __slots__ = ( - "app", - "__call__", - "transaction_style", - "mechanism_type", - "span_origin", - "http_methods_to_capture", - "root_path_in_path", - ) - - def __init__( - self, - app: "Any", - unsafe_context_data: bool = False, - transaction_style: str = "endpoint", - mechanism_type: str = "asgi", - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - asgi_version: "Optional[int]" = None, - root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, - ) -> None: - """ - Instrument an ASGI application with Sentry. Provides HTTP/websocket - data to sent events and basic handling for exceptions bubbling up - through the middleware. - - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. - """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - - asgi_middleware_while_using_starlette_or_fastapi = ( - mechanism_type == "asgi" and "starlette" in _get_installed_modules() - ) - if asgi_middleware_while_using_starlette_or_fastapi: - logger.warning( - "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " - "Please remove 'SentryAsgiMiddleware' from your project. " - "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." - ) - - self.transaction_style = transaction_style - self.mechanism_type = mechanism_type - self.span_origin = span_origin - self.app = app - self.http_methods_to_capture = http_methods_to_capture - self.root_path_in_path = root_path_in_path - - if asgi_version is None: - if _looks_like_asgi3(app): - asgi_version = 3 - else: - asgi_version = 2 - - if asgi_version == 3: - self.__call__ = self._run_asgi3 - elif asgi_version == 2: - self.__call__ = self._run_asgi2 # type: ignore - - def _capture_lifespan_exception(self, exc: Exception) -> None: - """Capture exceptions raise in application lifespan handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _capture_request_exception(self, exc: Exception) -> None: - """Capture exceptions raised in incoming request handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _run_asgi2(self, scope: "Any") -> "Any": - async def inner(receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=2) - - return inner - - async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=3) - - async def _run_app( - self, scope: "Any", receive: "Any", send: "Any", asgi_version: int - ) -> "Any": - is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) - is_lifespan = scope["type"] == "lifespan" - if is_recursive_asgi_middleware or is_lifespan: - try: - if asgi_version == 2: - return await self.app(scope)(receive, send) - else: - return await self.app(scope, receive, send) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_lifespan_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_lifespan_exception(exc) - reraise(*exc_info) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _asgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as sentry_scope: - with track_session(sentry_scope, session_mode="request"): - sentry_scope.clear_breadcrumbs() - sentry_scope._name = "asgi" - processor = partial(self.event_processor, asgi_scope=scope) - sentry_scope.add_event_processor(processor) - - ty = scope["type"] - ( - transaction_name, - transaction_source, - ) = self._get_transaction_name_and_source( - self.transaction_style, - scope, - ) - - method = scope.get("method", "").upper() - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - if span_streaming: - segment: "Optional[StreamedSpan]" = None - attributes: "Attributes" = { - "sentry.span.source": getattr( - transaction_source, "value", transaction_source - ), - "sentry.origin": self.span_origin, - "network.protocol.name": ty, - } - - if scope.get("client") and should_send_default_pii(): - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) - - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - sentry_sdk.traces.continue_trace(_get_headers(scope)) - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = f"{ty}.server" - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - else: - sentry_sdk.traces.new_trace() - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = OP.HTTP_SERVER - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - - span_ctx = segment or nullcontext() - - else: - transaction = None - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - transaction = continue_trace( - _get_headers(scope), - op="{}.server".format(ty), - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - - if transaction: - transaction.set_tag("asgi.type", ty) - - span_ctx = ( - sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"asgi_scope": scope}, - ) - if transaction is not None - else nullcontext() - ) - - with span_ctx as span: - if isinstance(span, StreamedSpan): - for attribute, value in _get_request_attributes( - scope, - root_path_in_path=self.root_path_in_path, - ).items(): - span.set_attribute(attribute, value) - - try: - - async def _sentry_wrapped_send( - event: "Dict[str, Any]", - ) -> "Any": - if span is not None: - is_http_response = ( - event.get("type") == "http.response.start" - and "status" in event - ) - if is_http_response: - if isinstance(span, StreamedSpan): - span.status = ( - "error" - if event["status"] >= 400 - else "ok" - ) - span.set_attribute( - "http.response.status_code", - event["status"], - ) - else: - span.set_http_status(event["status"]) - - return await send(event) - - if asgi_version == 2: - return await self.app(scope)( - receive, _sentry_wrapped_send - ) - else: - return await self.app( - scope, receive, _sentry_wrapped_send - ) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_request_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_request_exception(exc) - reraise(*exc_info) - - finally: - if isinstance(span, StreamedSpan): - already_set = ( - span is not None - and span.name != _DEFAULT_TRANSACTION_NAME - and span.get_attributes().get("sentry.span.source") - in [ - SegmentSource.COMPONENT.value, - SegmentSource.ROUTE.value, - SegmentSource.CUSTOM.value, - ] - ) - with capture_internal_exceptions(): - if not already_set: - name, source = ( - self._get_segment_name_and_source( - self.transaction_style, scope - ) - ) - span.name = name - span.set_attribute("sentry.span.source", source) - finally: - _asgi_middleware_applied.set(False) - - def event_processor( - self, event: "Event", hint: "Hint", asgi_scope: "Any" - ) -> "Optional[Event]": - request_data = event.get("request", {}) - request_data.update( - _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) - ) - event["request"] = deepcopy(request_data) - - # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) - transaction = event.get("transaction") - transaction_source = (event.get("transaction_info") or {}).get("source") - already_set = ( - transaction is not None - and transaction != _DEFAULT_TRANSACTION_NAME - and transaction_source - in [ - TransactionSource.COMPONENT, - TransactionSource.ROUTE, - TransactionSource.CUSTOM, - ] - ) - if not already_set: - name, source = self._get_transaction_name_and_source( - self.transaction_style, asgi_scope - ) - event["transaction"] = name - event["transaction_info"] = {"source": source} - - return event - - # Helper functions. - # - # Note: Those functions are not public API. If you want to mutate request - # data to your liking it's recommended to use the `before_send` callback - # for that. - - def _get_transaction_name_and_source( - self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SOURCE_FOR_STYLE[transaction_style] - ty = asgi_scope.get("type") - - if transaction_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = transaction_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - elif transaction_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - return name, source - - return name, source - - def _get_segment_name_and_source( - self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value - ty = asgi_scope.get("type") - - if segment_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = qualname_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - elif segment_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = SegmentSource.ROUTE.value - return name, source - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py deleted file mode 100644 index 4b3f6d330e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncio.py +++ /dev/null @@ -1,280 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import AsyncHttpTransport -from sentry_sdk.utils import ( - event_from_exception, - is_internal_task, - logger, - reraise, -) - -try: - import asyncio - from asyncio.tasks import Task -except ImportError: - raise DidNotEnable("asyncio not available") - -from typing import TYPE_CHECKING, Optional, Union - -if TYPE_CHECKING: - from collections.abc import Coroutine - from typing import Any, Callable, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T", bound=Callable[..., Any]) - - -def get_name(coro: "Any") -> str: - return ( - getattr(coro, "__qualname__", None) - or getattr(coro, "__name__", None) - or "coroutine without __name__" - ) - - -def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": - # Only __name__ and __qualname__ are copied from function to coroutine in CPython - return functools.partial( - functools.update_wrapper, - wrapped=wrapped, # type: ignore - assigned=("__name__", "__qualname__"), - updated=(), - ) - - -def patch_loop_close() -> None: - """Patch loop.close to flush pending events before shutdown.""" - # Atexit shutdown hook happens after the event loop is closed. - # Therefore, it is necessary to patch the loop.close method to ensure - # that pending events are flushed before the interpreter shuts down. - try: - loop = asyncio.get_running_loop() - except RuntimeError: - # No running loop → cannot patch now - return - - if getattr(loop, "_sentry_flush_patched", False): - return - - async def _flush() -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - try: - if not isinstance(client.transport, AsyncHttpTransport): - return - - await client.close_async() - except Exception: - logger.warning("Sentry flush failed during loop shutdown", exc_info=True) - - orig_close = loop.close - - def _patched_close() -> None: - try: - loop.run_until_complete(_flush()) - except Exception: - logger.debug( - "Could not flush Sentry events during loop close", exc_info=True - ) - finally: - orig_close() - - loop.close = _patched_close # type: ignore - loop._sentry_flush_patched = True # type: ignore - - -def _create_task_with_factory( - orig_task_factory: "Any", - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", -) -> "asyncio.Task[Any]": - task = None - - # Trying to use user set task factory (if there is one) - if orig_task_factory: - task = orig_task_factory(loop, coro, **kwargs) - - if task is None: - # The default task factory in `asyncio` does not have its own function - # but is just a couple of lines in `asyncio.base_events.create_task()` - # Those lines are copied here. - - # WARNING: - # If the default behavior of the task creation in asyncio changes, - # this will break! - task = Task(coro, loop=loop, **kwargs) - if task._source_traceback: # type: ignore - del task._source_traceback[-1] # type: ignore - - return task - - -def patch_asyncio() -> None: - orig_task_factory = None - try: - loop = asyncio.get_running_loop() - orig_task_factory = loop.get_task_factory() - - # Check if already patched - if getattr(orig_task_factory, "_is_sentry_task_factory", False): - return - - def _sentry_task_factory( - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", - ) -> "asyncio.Future[Any]": - # Check if this is an internal Sentry task - if is_internal_task(): - return _create_task_with_factory( - orig_task_factory, loop, coro, **kwargs - ) - - @_wrap_coroutine(coro) - async def _task_with_sentry_span_creation() -> "Any": - result = None - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncioIntegration) - task_spans = integration.task_spans if integration else False - - span_ctx: "Optional[Union[StreamedSpan, Span]]" = None - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope(): - if task_spans: - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=get_name(coro), - attributes={ - "sentry.op": OP.FUNCTION, - "sentry.origin": AsyncioIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.FUNCTION, - name=get_name(coro), - origin=AsyncioIntegration.origin, - ) - - with span_ctx if span_ctx else nullcontext(): - try: - result = await coro - except StopAsyncIteration as e: - raise e from None - except Exception: - reraise(*_capture_exception()) - - return result - - task = _create_task_with_factory( - orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs - ) - - # Set the task name to include the original coroutine's name - try: - task.set_name(f"{get_name(coro)} (Sentry-wrapped)") - except AttributeError: - # set_name might not be available in all Python versions - pass - - return task - - _sentry_task_factory._is_sentry_task_factory = True # type: ignore - loop.set_task_factory(_sentry_task_factory) # type: ignore - - except RuntimeError: - # When there is no running loop, we have nothing to patch. - logger.warning( - "There is no running asyncio loop so there is nothing Sentry can patch. " - "Please make sure you call sentry_sdk.init() within a running " - "asyncio loop for the AsyncioIntegration to work. " - "See https://docs.sentry.io/platforms/python/integrations/asyncio/" - ) - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - - integration = client.get_integration(AsyncioIntegration) - if integration is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "asyncio", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class AsyncioIntegration(Integration): - identifier = "asyncio" - origin = f"auto.function.{identifier}" - - def __init__(self, task_spans: bool = True) -> None: - self.task_spans = task_spans - - @staticmethod - def setup_once() -> None: - patch_asyncio() - patch_loop_close() - - -def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: - """ - Enable AsyncioIntegration with the provided options. - - This is useful in scenarios where Sentry needs to be initialized before - an event loop is set up, but you still want to instrument asyncio once there - is an event loop. In that case, you can sentry_sdk.init() early on without - the AsyncioIntegration and then, once the event loop has been set up, - execute: - - ```python - from sentry_sdk.integrations.asyncio import enable_asyncio_integration - - async def async_entrypoint(): - enable_asyncio_integration() - ``` - - Any arguments provided will be passed to AsyncioIntegration() as is. - - If AsyncioIntegration has already patched the current event loop, this - function won't have any effect. - - If AsyncioIntegration was provided in - sentry_sdk.init(disabled_integrations=[...]), this function will ignore that - and the integration will be enabled. - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # This function purposefully bypasses the integration machinery in - # integrations/__init__.py. _installed_integrations/_processed_integrations - # is used to prevent double patching the same module, but in the case of - # the AsyncioIntegration, we don't monkeypatch the standard library directly, - # we patch the currently running event loop, and we keep the record of doing - # that on the loop itself. - logger.debug("Setting up integration asyncio") - - integration = AsyncioIntegration(*args, **kwargs) - integration.setup_once() - - if "asyncio" not in client.integrations: - client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py deleted file mode 100644 index 186176d268..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/asyncpg.py +++ /dev/null @@ -1,312 +0,0 @@ -from __future__ import annotations - -import contextlib -import re -from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import asyncpg # type: ignore - from asyncpg.cursor import ( # type: ignore - BaseCursor, - Cursor, - CursorIterator, - ) - -except ImportError: - raise DidNotEnable("asyncpg not installed.") - - -class AsyncPGIntegration(Integration): - identifier = "asyncpg" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AsyncPGIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - # asyncpg.__version__ is a string containing the semantic version in the form of ".." - asyncpg_version = parse_version(asyncpg.__version__) - _check_minimum_version(AsyncPGIntegration, asyncpg_version) - - asyncpg.Connection.execute = _wrap_execute( - asyncpg.Connection.execute, - ) - - asyncpg.Connection._execute = _wrap_connection_method( - asyncpg.Connection._execute - ) - asyncpg.Connection._executemany = _wrap_connection_method( - asyncpg.Connection._executemany, executemany=True - ) - asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) - - BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) - BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) - - asyncpg.connect_utils._connect_addr = _wrap_connect_addr( - asyncpg.connect_utils._connect_addr - ) - - -T = TypeVar("T") - - -def _normalize_query(query: str) -> str: - return re.sub(r"\s+", " ", query).strip() - - -def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - # Avoid recording calls to _execute twice. - # Calls to Connection.execute with args also call - # Connection._execute, which is recorded separately - # args[0] = the connection object, args[1] is the query - if len(args) > 2: - return await f(*args, **kwargs) - - query = _normalize_query(args[1]) - with record_sql_queries( - cursor=None, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - span_origin=AsyncPGIntegration.origin, - ) as span: - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -SubCursor = TypeVar("SubCursor", bound=BaseCursor) - - -@contextlib.contextmanager -def _record( - cursor: "SubCursor | None", - query: str, - params_list: "tuple[Any, ...] | None", - *, - executemany: bool = False, -) -> "Iterator[Union[Span, StreamedSpan]]": - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncPGIntegration) - if integration is not None and not integration._record_params: - params_list = None - - param_style = "pyformat" if params_list else None - - query = _normalize_query(query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=params_list, - paramstyle=param_style, - executemany=executemany, - record_cursor_repr=cursor is not None, - span_origin=AsyncPGIntegration.origin, - ) as span: - yield span - - -def _wrap_connection_method( - f: "Callable[..., Awaitable[T]]", *, executemany: bool = False -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - query = args[1] - params_list = args[2] if len(args) > 2 else None - with _record(None, query, params_list, executemany=executemany) as span: - _set_db_data(span, args[0]) - - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_cursor_method( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - if type(cursor) is CursorIterator: - span_op_override_value = OP.DB_CURSOR_ITERATOR - elif type(cursor) is Cursor: - span_op_override_value = OP.DB_CURSOR_FETCH - else: - span_op_override_value = None - - query = _normalize_query(cursor._query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - record_cursor_repr=True, - span_origin=AsyncPGIntegration.origin, - span_op_override_value=span_op_override_value, - ) as span: - _set_db_data(span, cursor._connection) - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_connect_addr( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - user = kwargs["params"].user - database = kwargs["params"].database - addr = kwargs.get("addr") - - if has_span_streaming_enabled(client.options): - span_attributes = { - "sentry.op": OP.DB, - "sentry.origin": AsyncPGIntegration.origin, - SPANDATA.DB_SYSTEM_NAME: "postgresql", - SPANDATA.DB_USER: user, - SPANDATA.DB_NAMESPACE: database, - SPANDATA.DB_DRIVER_NAME: "asyncpg", - } - if addr: - try: - span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] - span_attributes[SPANDATA.SERVER_PORT] = addr[1] - except IndexError: - pass - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span_attributes - ) - res = await f(*args, **kwargs) - - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AsyncPGIntegration.origin, - ) as span: - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - span.set_data(SPANDATA.DB_NAME, database) - span.set_data(SPANDATA.DB_USER, user) - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span._data - ) - res = await f(*args, **kwargs) - - return res - - return _inner - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - addr = conn._addr - database = conn._params.database - user = conn._params.user - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") - if addr: - try: - span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_attribute(SPANDATA.DB_NAMESPACE, database) - - if user: - span.set_attribute(SPANDATA.DB_USER, user) - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_data(SPANDATA.DB_NAME, database) - - if user: - span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py deleted file mode 100644 index b573e76f09..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/atexit.py +++ /dev/null @@ -1,51 +0,0 @@ -import atexit -import os -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Optional - - -def default_callback(pending: int, timeout: int) -> None: - """This is the default shutdown callback that is set on the options. - It prints out a message to stderr that informs the user that some events - are still pending and the process is waiting for them to flush out. - """ - - def echo(msg: str) -> None: - sys.stderr.write(msg + "\n") - - echo("Sentry is attempting to send %i pending events" % pending) - echo("Waiting up to %s seconds" % timeout) - echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) - sys.stderr.flush() - - -class AtexitIntegration(Integration): - identifier = "atexit" - - def __init__(self, callback: "Optional[Any]" = None) -> None: - if callback is None: - callback = default_callback - self.callback = callback - - @staticmethod - def setup_once() -> None: - @atexit.register - def _shutdown() -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(AtexitIntegration) - - if integration is None: - return - - logger.debug("atexit: got shutdown signal") - logger.debug("atexit: shutting down client") - sentry_sdk.get_isolation_scope().end_session() - - client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py deleted file mode 100644 index c7fe77714a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/aws_lambda.py +++ /dev/null @@ -1,542 +0,0 @@ -import functools -import json -import re -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING -from urllib.parse import urlencode - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.cloud_resource_context import ( - CLOUD_PLATFORM, - CLOUD_PROVIDER, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - -# Constants -TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - - -def _wrap_init_error(init_error: "F") -> "F": - @ensure_integration_enabled(AwsLambdaIntegration, init_error) - def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - with capture_internal_exceptions(): - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - exc_info = sys.exc_info() - if exc_info and all(exc_info): - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - - else: - # Fall back to AWS lambdas JSON representation of the error - error_info = args[1] - if isinstance(error_info, str): - error_info = json.loads(error_info) - sentry_event = _event_from_error_json(error_info) - sentry_sdk.capture_event(sentry_event) - - return init_error(*args, **kwargs) - - return sentry_init_error # type: ignore - - -def _wrap_handler(handler: "F") -> "F": - @functools.wraps(handler) - def sentry_handler( - aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, - # `event` here is *likely* a dictionary, but also might be a number of - # other types (str, int, float, None). - # - # In some cases, it is a list (if the user is batch-invoking their - # function, for example), in which case we'll use the first entry as a - # representative from which to try pulling request data. (Presumably it - # will be the same for all events in the list, since they're all hitting - # the lambda in the same request.) - - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - - if integration is None: - return handler(aws_event, aws_context, *args, **kwargs) - - if isinstance(aws_event, list) and len(aws_event) >= 1: - request_data = aws_event[0] - batch_size = len(aws_event) - else: - request_data = aws_event - batch_size = 1 - - if not isinstance(request_data, dict): - # If we're not dealing with a dictionary, we won't be able to get - # headers, path, http method, etc in any case, so it's fine that - # this is empty - request_data = {} - - configured_time = aws_context.get_remaining_time_in_millis() - aws_region = aws_context.invoked_function_arn.split(":")[3] - - with sentry_sdk.isolation_scope() as scope: - timeout_thread = None - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - request_data, aws_context, configured_time - ) - ) - scope.set_tag("aws_region", aws_region) - if batch_size > 1: - scope.set_tag("batch_request", True) - scope.set_tag("batch_size", batch_size) - - # Starting the Timeout thread only if the configured time is greater than Timeout warning - # buffer and timeout_warning parameter is set True. - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = ( - configured_time - TIMEOUT_WARNING_BUFFER - ) / MILLIS_TO_SECONDS - - timeout_thread = TimeoutThread( - waiting_time, - configured_time / MILLIS_TO_SECONDS, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = request_data.get("headers", {}) - # Some AWS Services (ie. EventBridge) set headers as a list - # or None, so we must ensure it is a dict - if not isinstance(headers, dict): - headers = {} - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - additional_attributes: "dict[str, Any]" = {} - if "httpMethod" in request_data: - additional_attributes["http.request.method"] = request_data[ - "httpMethod" - ] - - if should_send_default_pii() and "queryStringParameters" in request_data: - qs = request_data["queryStringParameters"] - if qs: - additional_attributes["url.query"] = urlencode(qs) - - sampling_context = { - "aws_event": aws_event, - "aws_context": aws_context, - } - - function_name = aws_context.function_name - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_AWS, - "sentry.origin": AwsLambdaIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.region": aws_region, - "cloud.resource_id": aws_context.invoked_function_arn, - "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, - "cloud.provider": CLOUD_PROVIDER.AWS, - "faas.name": function_name, - "faas.invocation_id": aws_context.aws_request_id, - "faas.version": aws_context.function_version, - "aws.lambda.invoked_arn": aws_context.invoked_function_arn, - "aws.log.group.names": [aws_context.log_group_name], - "aws.log.stream.names": [aws_context.log_stream_name], - "messaging.batch.message_count": batch_size, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_AWS, - name=function_name, - source=TransactionSource.COMPONENT, - origin=AwsLambdaIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return handler(aws_event, aws_context, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - - return sentry_handler # type: ignore - - -def _drain_queue() -> None: - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - if integration is not None: - # Flush out the event queue before AWS kills the - # process. - client.flush() - - -class AwsLambdaIntegration(Integration): - identifier = "aws_lambda" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - lambda_bootstrap = get_lambda_bootstrap() - if not lambda_bootstrap: - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find bootstrap module)" - ) - return - - if not hasattr(lambda_bootstrap, "handle_event_request"): - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find handle_event_request)" - ) - return - - pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 - - if pre_37: - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_event_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - old_handle_http_request = lambda_bootstrap.handle_http_request - - def sentry_handle_http_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_http_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_http_request = sentry_handle_http_request - - # Patch to_json to drain the queue. This should work even when the - # SDK is initialized inside of the handler - - old_to_json = lambda_bootstrap.to_json - - def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return old_to_json(*args, **kwargs) - - lambda_bootstrap.to_json = sentry_to_json - else: - lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( - lambda_bootstrap.LambdaRuntimeClient.post_init_error - ) - - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( # type: ignore - lambda_runtime_client, request_handler, *args, **kwargs - ): - request_handler = _wrap_handler(request_handler) - return old_handle_event_request( - lambda_runtime_client, request_handler, *args, **kwargs - ) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - # Patch the runtime client to drain the queue. This should work - # even when the SDK is initialized inside of the handler - - def _wrap_post_function(f: "F") -> "F": - def inner(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return f(*args, **kwargs) - - return inner # type: ignore - - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result - ) - ) - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error - ) - ) - - -def get_lambda_bootstrap() -> "Optional[Any]": - # Python 3.7: If the bootstrap module is *already imported*, it is the - # one we actually want to use (no idea what's in __main__) - # - # Python 3.8: bootstrap is also importable, but will be the same file - # as __main__ imported under a different name: - # - # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ - # sys.modules['__main__'] is not sys.modules['bootstrap'] - # - # Python 3.9: bootstrap is in __main__.awslambdaricmain - # - # On container builds using the `aws-lambda-python-runtime-interface-client` - # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap - # - # Such a setup would then make all monkeypatches useless. - if "bootstrap" in sys.modules: - return sys.modules["bootstrap"] - elif "__main__" in sys.modules: - module = sys.modules["__main__"] - # python3.9 runtime - if hasattr(module, "awslambdaricmain") and hasattr( - module.awslambdaricmain, "bootstrap" - ): - return module.awslambdaricmain.bootstrap - elif hasattr(module, "bootstrap"): - # awslambdaric python module in container builds - return module.bootstrap - - # python3.8 runtime - return module - else: - return None - - -def _make_request_event_processor( - aws_event: "Any", aws_context: "Any", configured_timeout: "Any" -) -> "EventProcessor": - start_time = datetime.now(timezone.utc) - - def event_processor( - sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time - ) -> "Optional[Event]": - remaining_time_in_milis = aws_context.get_remaining_time_in_millis() - exec_duration = configured_timeout - remaining_time_in_milis - - extra = sentry_event.setdefault("extra", {}) - extra["lambda"] = { - "function_name": aws_context.function_name, - "function_version": aws_context.function_version, - "invoked_function_arn": aws_context.invoked_function_arn, - "aws_request_id": aws_context.aws_request_id, - "execution_duration_in_millis": exec_duration, - "remaining_time_in_millis": remaining_time_in_milis, - } - - extra["cloudwatch logs"] = { - "url": _get_cloudwatch_logs_url(aws_context, start_time), - "log_group": aws_context.log_group_name, - "log_stream": aws_context.log_stream_name, - } - - request = sentry_event.get("request", {}) - - if "httpMethod" in aws_event: - request["method"] = aws_event["httpMethod"] - - request["url"] = _get_url(aws_event, aws_context) - - if "queryStringParameters" in aws_event: - request["query_string"] = aws_event["queryStringParameters"] - - if "headers" in aws_event: - request["headers"] = _filter_headers(aws_event["headers"]) - - if should_send_default_pii(): - user_info = sentry_event.setdefault("user", {}) - - identity = aws_event.get("identity") - if identity is None: - identity = {} - - id = identity.get("userArn") - if id is not None: - user_info.setdefault("id", id) - - ip = identity.get("sourceIp") - if ip is not None: - user_info.setdefault("ip_address", ip) - - if "body" in aws_event: - request["data"] = aws_event.get("body", "") - else: - if aws_event.get("body", None): - # Unfortunately couldn't find a way to get structured body from AWS - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - sentry_event["request"] = deepcopy(request) - - return sentry_event - - return event_processor - - -def _get_url(aws_event: "Any", aws_context: "Any") -> str: - path = aws_event.get("path", None) - - headers = aws_event.get("headers") - if headers is None: - headers = {} - - host = headers.get("Host", None) - proto = headers.get("X-Forwarded-Proto", None) - if proto and host and path: - return "{}://{}{}".format(proto, host, path) - return "awslambda:///{}".format(aws_context.function_name) - - -def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: - """ - Generates a CloudWatchLogs console URL based on the context object - - Arguments: - aws_context {Any} -- context from lambda handler - - Returns: - str -- AWS Console URL to logs. - """ - formatstring = "%Y-%m-%dT%H:%M:%SZ" - region = environ.get("AWS_REGION", "") - - url = ( - "https://console.{domain}/cloudwatch/home?region={region}" - "#logEventViewer:group={log_group};stream={log_stream}" - ";start={start_time};end={end_time}" - ).format( - domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", - region=region, - log_group=aws_context.log_group_name, - log_stream=aws_context.log_stream_name, - start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), - end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( - formatstring - ), - ) - - return url - - -def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": - frames = [] - for frame in formatted_tb: - match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) - if match: - file_name, line_number, func_name = match.groups() - line_number = int(line_number) - frames.append( - { - "filename": file_name, - "function": func_name, - "lineno": line_number, - "vars": None, - "pre_context": None, - "context_line": None, - "post_context": None, - } - ) - return frames - - -def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": - """ - Converts the error JSON from AWS Lambda into a Sentry error event. - This is not a full fletched event, but better than nothing. - - This is an example of where AWS creates the error JSON: - https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 - """ - event: "Event" = { - "level": "error", - "exception": { - "values": [ - { - "type": error_json.get("errorType"), - "value": error_json.get("errorMessage"), - "stacktrace": { - "frames": _parse_formatted_traceback( - error_json.get("stackTrace", []) - ), - }, - "mechanism": { - "type": "aws_lambda", - "handled": False, - }, - } - ], - }, - } - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py deleted file mode 100644 index 31f45f73de..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/beam.py +++ /dev/null @@ -1,164 +0,0 @@ -import sys -import types -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) - - -WRAPPED_FUNC = "_wrapped_{}_" -INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py -USED_FUNC = "_sentry_used_" - - -class BeamIntegration(Integration): - identifier = "beam" - - @staticmethod - def setup_once() -> None: - from apache_beam.transforms.core import DoFn, ParDo # type: ignore - - ignore_logger("root") - ignore_logger("bundle_processor.create") - - function_patches = ["process", "start_bundle", "finish_bundle", "setup"] - for func_name in function_patches: - setattr( - DoFn, - INSPECT_FUNC.format(func_name), - _wrap_inspect_call(DoFn, func_name), - ) - - old_init = ParDo.__init__ - - def sentry_init_pardo( - self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Do not monkey patch init twice - if not getattr(self, "_sentry_is_patched", False): - for func_name in function_patches: - if not hasattr(fn, func_name): - continue - wrapped_func = WRAPPED_FUNC.format(func_name) - - # Check to see if inspect is set and process is not - # to avoid monkey patching process twice. - # Check to see if function is part of object for - # backwards compatibility. - process_func = getattr(fn, func_name) - inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) - if not getattr(inspect_func, USED_FUNC, False) and not getattr( - process_func, USED_FUNC, False - ): - setattr(fn, wrapped_func, process_func) - setattr(fn, func_name, _wrap_task_call(process_func)) - - self._sentry_is_patched = True - old_init(self, fn, *args, **kwargs) - - ParDo.__init__ = sentry_init_pardo - - -def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": - if not hasattr(cls, func_name): - return None - - def _inspect(self: "Any") -> "Any": - """ - Inspect function overrides the way Beam gets argspec. - """ - wrapped_func = WRAPPED_FUNC.format(func_name) - if hasattr(self, wrapped_func): - process_func = getattr(self, wrapped_func) - else: - process_func = getattr(self, func_name) - setattr(self, func_name, _wrap_task_call(process_func)) - setattr(self, wrapped_func, process_func) - - # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults - # (which uses Signatures internally) should be used instead. - try: - from apache_beam.transforms.core import get_function_args_defaults - - return get_function_args_defaults(process_func) - except ImportError: - from apache_beam.typehints.decorators import getfullargspec # type: ignore - - return getfullargspec(process_func) - - setattr(_inspect, USED_FUNC, True) - return _inspect - - -def _wrap_task_call(func: "F") -> "F": - """ - Wrap task call with a try catch to get exceptions. - """ - - @wraps(func) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - try: - gen = func(*args, **kwargs) - except Exception: - raise_exception() - - if not isinstance(gen, types.GeneratorType): - return gen - return _wrap_generator_call(gen) - - setattr(_inner, USED_FUNC, True) - return _inner # type: ignore - - -@ensure_integration_enabled(BeamIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - """ - Send Beam exception to Sentry. - """ - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "beam", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def raise_exception() -> None: - """ - Raise an exception. - """ - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) - reraise(*exc_info) - - -def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": - """ - Wrap the generator to handle any failures. - """ - while True: - try: - yield next(gen) - except StopIteration: - break - except Exception: - raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py deleted file mode 100644 index a7fdd99b21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/boto3.py +++ /dev/null @@ -1,187 +0,0 @@ -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_url, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Type, Union - - from botocore.model import ServiceId - -try: - from botocore import __version__ as BOTOCORE_VERSION - from botocore.awsrequest import AWSRequest - from botocore.client import BaseClient - from botocore.response import StreamingBody -except ImportError: - raise DidNotEnable("botocore is not installed") - - -class Boto3Integration(Integration): - identifier = "boto3" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTOCORE_VERSION) - _check_minimum_version(Boto3Integration, version, "botocore") - - orig_init = BaseClient.__init__ - - def sentry_patched_init( - self: "BaseClient", *args: "Any", **kwargs: "Any" - ) -> None: - orig_init(self, *args, **kwargs) - meta = self.meta - service_id = meta.service_model.service_id - meta.events.register( - "request-created", - partial(_sentry_request_created, service_id=service_id), - ) - meta.events.register("after-call", _sentry_after_call) - meta.events.register("after-call-error", _sentry_after_call_error) - - BaseClient.__init__ = sentry_patched_init # type: ignore - - -def _sentry_request_created( - service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" -) -> None: - description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) - - client = sentry_sdk.get_client() - if client.get_integration(Boto3Integration) is None: - return - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Boto3Integration.origin, - SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", - }, - ) - if request.url is not None and should_send_default_pii(): - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if request.method is not None: - span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=description, - origin=Boto3Integration.origin, - ) - - if request.url is not None: - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_data("aws.request.url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - span.set_tag("aws.service_id", service_id.hyphenize()) - span.set_tag("aws.operation_name", operation_name) - if request.method is not None: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - - # We do it in order for subsequent http calls/retries be - # attached to this span. - span.__enter__() - - # request.context is an open-ended data-structure - # where we can add anything useful in request life cycle. - request.context["_sentrysdk_span"] = span - - -def _sentry_after_call( - context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(None, None, None) - - body = parsed.get("Body") - if not isinstance(body, StreamingBody): - return - - streaming_span: "Union[Span, StreamedSpan]" - if isinstance(span, StreamedSpan): - streaming_span = sentry_sdk.traces.start_span( - name=span.name, - parent_span=span, - attributes={ - "sentry.op": OP.HTTP_CLIENT_STREAM, - "sentry.origin": Boto3Integration.origin, - }, - ) - else: - streaming_span = span.start_child( - op=OP.HTTP_CLIENT_STREAM, - name=span.description, - origin=Boto3Integration.origin, - ) - - orig_read = body.read - orig_close = body.close - - def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: - try: - ret = orig_read(*args, **kwargs) - if ret: - return ret - - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - return ret - except Exception: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - raise - - body.read = sentry_streaming_body_read # type: ignore - - def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - orig_close(*args, **kwargs) - - body.close = sentry_streaming_body_close # type: ignore - - -def _sentry_after_call_error( - context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py deleted file mode 100644 index 50f6ca2e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/bottle.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from collections.abc import Set - from typing import Any, Callable, Dict, Optional - - from bottle import FileUpload, FormsDict, LocalRequest # type: ignore - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - -try: - from bottle import ( - Bottle, - HTTPResponse, - Route, - ) - from bottle import ( - __version__ as BOTTLE_VERSION, - ) - from bottle import ( - request as bottle_request, - ) -except ImportError: - raise DidNotEnable("Bottle not installed") - - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class BottleIntegration(Integration): - identifier = "bottle" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTTLE_VERSION) - _check_minimum_version(BottleIntegration, version) - - old_app = Bottle.__call__ - - @ensure_integration_enabled(BottleIntegration, old_app) - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=BottleIntegration.origin, - ) - - return middleware(environ, start_response) - - Bottle.__call__ = sentry_patched_wsgi_app - - old_handle = Bottle._handle - - @functools.wraps(old_handle) - def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return old_handle(self, environ) - - scope = sentry_sdk.get_isolation_scope() - scope._name = "bottle" - scope.add_event_processor( - _make_request_event_processor(self, bottle_request, integration) - ) - res = old_handle(self, environ) - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - _set_segment_name_and_source( - transaction_style=integration.transaction_style - ) - - return res - - Bottle._handle = _patched_handle - - old_make_callback = Route._make_callback - - @functools.wraps(old_make_callback) - def patched_make_callback( - self: "Route", *args: object, **kwargs: object - ) -> "Any": - prepared_callback = old_make_callback(self, *args, **kwargs) - - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return prepared_callback - - def wrapped_callback(*args: object, **kwargs: object) -> "Any": - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) - - return res - - return wrapped_callback - - Route._make_callback = patched_make_callback - - -class BottleRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[str, str]": - return self.request.cookies - - def raw_data(self) -> bytes: - return self.request.body.read() - - def form(self) -> "FormsDict": - if self.is_json(): - return None - return self.request.forms.decode() - - def files(self) -> "Optional[Dict[str, str]]": - if self.is_json(): - return None - - return self.request.files - - def size_of_file(self, file: "FileUpload") -> int: - return file.content_length - - -def _set_segment_name_and_source(transaction_style: str) -> None: - try: - if transaction_style == "url": - name = bottle_request.route.rule or "bottle request" - else: - name = ( - bottle_request.route.name - or transaction_from_function(bottle_request.route.callback) - or "bottle request" - ) - - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], - ) - except RuntimeError: - pass - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "url": - try: - name = request.route.rule or "" - except RuntimeError: - pass - - elif transaction_style == "endpoint": - try: - name = ( - request.route.name - or transaction_from_function(request.route.callback) - or "" - ) - except RuntimeError: - pass - - event["transaction"] = name - event["transaction_info"] = { - "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - } - - -def _make_request_event_processor( - app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, request) - - with capture_internal_exceptions(): - BottleRequestExtractor(request).extract_into_event(event) - - return event - - return event_processor - - -def _capture_exception(exception: BaseException, handled: bool) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "bottle", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py deleted file mode 100644 index 532b13539b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/__init__.py +++ /dev/null @@ -1,612 +0,0 @@ -import sys -from collections.abc import Mapping -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import isolation_scope -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.celery.beat import ( - _patch_beat_apply_entry, - _patch_redbeat_apply_async, - _setup_celery_beat_signals, -) -from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource -from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, List, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import VERSION as CELERY_VERSION # type: ignore - from celery.app.task import Task # type: ignore - from celery.app.trace import task_has_custom - from celery.exceptions import ( # type: ignore - Ignore, - Reject, - Retry, - SoftTimeLimitExceeded, - ) - from kombu import Producer # type: ignore -except ImportError: - raise DidNotEnable("Celery not installed") - - -CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) - - -class CeleryIntegration(Integration): - identifier = "celery" - origin = f"auto.queue.{identifier}" - - def __init__( - self, - propagate_traces: bool = True, - monitor_beat_tasks: bool = False, - exclude_beat_tasks: "Optional[List[str]]" = None, - ) -> None: - self.propagate_traces = propagate_traces - self.monitor_beat_tasks = monitor_beat_tasks - self.exclude_beat_tasks = exclude_beat_tasks - - _patch_beat_apply_entry() - _patch_redbeat_apply_async() - _setup_celery_beat_signals(monitor_beat_tasks) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(CeleryIntegration, CELERY_VERSION) - - _patch_build_tracer() - _patch_task_apply_async() - _patch_celery_send_task() - _patch_worker_exit() - _patch_producer_publish() - - # This logger logs every status of every task that ran on the worker. - # Meaning that every task's breadcrumbs are full of stuff like "Task - # raised unexpected ". - ignore_logger("celery.worker.job") - ignore_logger("celery.app.trace") - - # This is stdout/err redirected to a logger, can't deal with this - # (need event_level=logging.WARN to reproduce) - ignore_logger("celery.redirected") - - -def _set_status(status: str) -> None: - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - - if span_streaming and scope.streamed_span is not None: - scope.streamed_span.status = "ok" if status == "ok" else "error" - elif not span_streaming and scope.span is not None: - scope.span.set_status(status) - - -def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return - - if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): - # ??? Doesn't map to anything - _set_status("aborted") - return - - _set_status("internal_error") - - if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): - return - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "celery", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - task: "Any", - uuid: "Any", - args: "Any", - kwargs: "Any", - request: "Optional[Any]" = None, -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["celery_task_id"] = uuid - extra = event.setdefault("extra", {}) - extra["celery-job"] = { - "task_name": task.name, - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - } - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): - event["fingerprint"] = [ - "celery", - "SoftTimeLimitExceeded", - getattr(task, "name", task), - ] - - return event - - return event_processor - - -def _update_celery_task_headers( - original_headers: "dict[str, Any]", - span: "Optional[Union[StreamedSpan, Span]]", - monitor_beat_tasks: bool, -) -> "dict[str, Any]": - """ - Updates the headers of the Celery task with the tracing information - and eventually Sentry Crons monitoring information for beat tasks. - """ - updated_headers = original_headers.copy() - with capture_internal_exceptions(): - # if span is None (when the task was started by Celery Beat) - # this will return the trace headers from the scope. - headers = dict( - sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) - ) - - if monitor_beat_tasks: - headers.update( - { - "sentry-monitor-start-timestamp-s": "%.9f" - % _now_seconds_since_epoch(), - } - ) - - # Add the time the task was enqueued to the headers - # This is used in the consumer to calculate the latency - updated_headers.update( - {"sentry-task-enqueued-time": _now_seconds_since_epoch()} - ) - - if headers: - existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) - sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) - - combined_baggage = sentry_baggage or existing_baggage - if sentry_baggage and existing_baggage: - # Merge incoming and sentry baggage, where the sentry trace information - # in the incoming baggage takes precedence and the third-party items - # are concatenated. - incoming = Baggage.from_incoming_header(existing_baggage) - combined = Baggage.from_incoming_header(sentry_baggage) - combined.sentry_items.update(incoming.sentry_items) - combined.third_party_items = ",".join( - [ - x - for x in [ - combined.third_party_items, - incoming.third_party_items, - ] - if x is not None and x != "" - ] - ) - combined_baggage = combined.serialize(include_third_party=True) - - updated_headers.update(headers) - if combined_baggage: - updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage - - # https://github.com/celery/celery/issues/4875 - # - # Need to setdefault the inner headers too since other - # tracing tools (dd-trace-py) also employ this exact - # workaround and we don't want to break them. - updated_headers.setdefault("headers", {}).update(headers) - if combined_baggage: - updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage - - # Add the Sentry options potentially added in `sentry_apply_entry` - # to the headers (done when auto-instrumenting Celery Beat tasks) - for key, value in updated_headers.items(): - if key.startswith("sentry-"): - updated_headers["headers"][key] = value - - # Preserve user-provided custom headers in the inner "headers" dict - # so they survive to task.request.headers on the worker (celery#4875). - for key, value in original_headers.items(): - if key != "headers" and key not in updated_headers["headers"]: - updated_headers["headers"][key] = value - - return updated_headers - - -class NoOpMgr: - def __enter__(self) -> None: - return None - - def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: - return None - - -def _wrap_task_run(f: "F") -> "F": - @wraps(f) - def apply_async(*args: "Any", **kwargs: "Any") -> "Any": - # Note: kwargs can contain headers=None, so no setdefault! - # Unsure which backend though. - client = sentry_sdk.get_client() - integration = client.get_integration(CeleryIntegration) - if integration is None: - return f(*args, **kwargs) - - kwarg_headers = kwargs.get("headers") or {} - propagate_traces = kwarg_headers.pop( - "sentry-propagate-traces", integration.propagate_traces - ) - - if not propagate_traces: - return f(*args, **kwargs) - - if isinstance(args[0], Task): - task_name: str = args[0].name - elif len(args) > 1 and isinstance(args[1], str): - task_name = args[1] - else: - task_name = "" - - span_streaming = has_span_streaming_enabled(client.options) - - task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" - - span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - if span_streaming: - if not task_started_from_beat and get_current_span() is not None: - span_mgr = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_CELERY, - "sentry.origin": CeleryIntegration.origin, - }, - ) - - else: - if not task_started_from_beat: - span_mgr = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_CELERY, - name=task_name, - origin=CeleryIntegration.origin, - ) - - with span_mgr as span: - kwargs["headers"] = _update_celery_task_headers( - kwarg_headers, span, integration.monitor_beat_tasks - ) - return f(*args, **kwargs) - - return apply_async # type: ignore - - -def _wrap_tracer(task: "Any", f: "F") -> "F": - # Need to wrap tracer for pushing the scope before prerun is sent, and - # popping it after postrun is sent. - # - # This is the reason we don't use signals for hooking in the first place. - # Also because in Celery 3, signal dispatch returns early if one handler - # crashes. - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - with isolation_scope() as scope: - scope._name = "celery" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - - task_name = getattr(task, "name", "") - - custom_sampling_context = {} - with capture_internal_exceptions(): - custom_sampling_context = { - "celery_job": { - "task": task_name, - # for some reason, args[1] is a list if non-empty but a - # tuple if empty - "args": list(args[1]), - "kwargs": args[2], - } - } - - span: "Union[Span, StreamedSpan]" - span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - - # Celery task objects are not a thing to be trusted. Even - # something such as attribute access can fail. - with capture_internal_exceptions(): - headers = args[3].get("headers") or {} - if span_streaming: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(custom_sampling_context) - span = sentry_sdk.traces.start_span( - name=task_name, - parent_span=None, # make this a segment - attributes={ - "sentry.origin": CeleryIntegration.origin, - "sentry.span.source": TransactionSource.TASK.value, - "sentry.op": OP.QUEUE_TASK_CELERY, - }, - ) - - span_ctx = span - - else: - span = continue_trace( - headers, - op=OP.QUEUE_TASK_CELERY, - name=task_name, - source=TransactionSource.TASK, - origin=CeleryIntegration.origin, - ) - span.set_status(SPANSTATUS.OK) - - span_ctx = sentry_sdk.start_transaction( - span, - custom_sampling_context=custom_sampling_context, - ) - - with span_ctx: - return f(*args, **kwargs) - - return _inner # type: ignore - - -def _set_messaging_destination_name( - task: "Any", span: "Union[StreamedSpan, Span]" -) -> None: - """Set "messaging.destination.name" tag for span""" - with capture_internal_exceptions(): - delivery_info = task.request.delivery_info - if delivery_info: - routing_key = delivery_info.get("routing_key") - if delivery_info.get("exchange") == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning the tasks - # are sent to the queue with the same name as the routing key. - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - else: - span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - -def _wrap_task_call(task: "Any", f: "F") -> "F": - # Need to wrap task call because the exception is caught before we get to - # see it. Also celery's reported stacktrace is untrustworthy. - - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - try: - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_PROCESS, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PROCESS, - name=task.name, - origin=CeleryIntegration.origin, - ) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - _set_messaging_destination_name(task, span) - - latency = None - with capture_internal_exceptions(): - if ( - task.request.headers is not None - and "sentry-task-enqueued-time" in task.request.headers - ): - latency = _now_seconds_since_epoch() - task.request.headers.pop( - "sentry-task-enqueued-time" - ) - - if latency is not None: - latency *= 1000 # milliseconds - set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) - - with capture_internal_exceptions(): - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries - ) - - with capture_internal_exceptions(): - with task.app.connection() as conn: - set_on_span( - SPANDATA.MESSAGING_SYSTEM, - conn.transport.driver_type, - ) - - return f(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(task, exc_info) - reraise(*exc_info) - - return _inner # type: ignore - - -def _patch_build_tracer() -> None: - import celery.app.trace as trace # type: ignore - - original_build_tracer = trace.build_tracer - - def sentry_build_tracer( - name: "Any", task: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not getattr(task, "_sentry_is_patched", False): - # determine whether Celery will use __call__ or run and patch - # accordingly - if task_has_custom(task, "__call__"): - type(task).__call__ = _wrap_task_call(task, type(task).__call__) - else: - task.run = _wrap_task_call(task, task.run) - - # `build_tracer` is apparently called for every task - # invocation. Can't wrap every celery task for every invocation - # or we will get infinitely nested wrapper functions. - task._sentry_is_patched = True - - return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) - - trace.build_tracer = sentry_build_tracer - - -def _patch_task_apply_async() -> None: - Task.apply_async = _wrap_task_run(Task.apply_async) - - -def _patch_celery_send_task() -> None: - from celery import Celery - - Celery.send_task = _wrap_task_run(Celery.send_task) - - -def _patch_worker_exit() -> None: - # Need to flush queue before worker shutdown because a crashing worker will - # call os._exit - from billiard.pool import Worker # type: ignore - - original_workloop = Worker.workloop - - def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": - try: - return original_workloop(*args, **kwargs) - finally: - with capture_internal_exceptions(): - if ( - sentry_sdk.get_client().get_integration(CeleryIntegration) - is not None - ): - sentry_sdk.flush() - - Worker.workloop = sentry_workloop - - -def _patch_producer_publish() -> None: - original_publish = Producer.publish - - def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return original_publish(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - kwargs_headers = kwargs.get("headers", {}) - if not isinstance(kwargs_headers, Mapping): - # Ensure kwargs_headers is a Mapping, so we can safely call get(). - # We don't expect this to happen, but it's better to be safe. Even - # if it does happen, only our instrumentation breaks. This line - # does not overwrite kwargs["headers"], so the original publish - # method will still work. - kwargs_headers = {} - - task_name = kwargs_headers.get("task") or "" - task_id = kwargs_headers.get("id") - retries = kwargs_headers.get("retries") - - routing_key = kwargs.get("routing_key") - exchange = kwargs.get("exchange") - - span: "Union[StreamedSpan, Span, None]" = None - if span_streaming: - if get_current_span() is not None: - span = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_PUBLISH, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PUBLISH, - name=task_name, - origin=CeleryIntegration.origin, - ) - - if span is None: - return original_publish(self, *args, **kwargs) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - if task_id is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) - - if exchange == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning messages are - # routed to the queue with the same name as the routing key. - set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - if retries is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type - ) - - return original_publish(self, *args, **kwargs) - - Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py deleted file mode 100644 index b5027d212a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/beat.py +++ /dev/null @@ -1,291 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.crons import MonitorStatus, capture_checkin -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.celery.utils import ( - _get_humanized_interval, - _now_seconds_since_epoch, -) -from sentry_sdk.utils import ( - logger, - match_regex_list, -) - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, TypeVar, Union - - from sentry_sdk._types import ( - MonitorConfig, - MonitorConfigScheduleType, - MonitorConfigScheduleUnit, - ) - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import Celery, Task # type: ignore - from celery.beat import Scheduler # type: ignore - from celery.schedules import crontab, schedule # type: ignore - from celery.signals import ( # type: ignore - task_failure, - task_retry, - task_success, - ) -except ImportError: - raise DidNotEnable("Celery not installed") - -try: - from redbeat.schedulers import RedBeatScheduler # type: ignore -except ImportError: - RedBeatScheduler = None - - -def _get_headers(task: "Task") -> "dict[str, Any]": - headers = task.request.get("headers") or {} - - # flatten nested headers - if "headers" in headers: - headers.update(headers["headers"]) - del headers["headers"] - - headers.update(task.request.get("properties") or {}) - - return headers - - -def _get_monitor_config( - celery_schedule: "Any", app: "Celery", monitor_name: str -) -> "MonitorConfig": - monitor_config: "MonitorConfig" = {} - schedule_type: "Optional[MonitorConfigScheduleType]" = None - schedule_value: "Optional[Union[str, int]]" = None - schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None - - if isinstance(celery_schedule, crontab): - schedule_type = "crontab" - schedule_value = ( - "{0._orig_minute} " - "{0._orig_hour} " - "{0._orig_day_of_month} " - "{0._orig_month_of_year} " - "{0._orig_day_of_week}".format(celery_schedule) - ) - elif isinstance(celery_schedule, schedule): - schedule_type = "interval" - (schedule_value, schedule_unit) = _get_humanized_interval( - celery_schedule.seconds - ) - - if schedule_unit == "second": - logger.warning( - "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", - monitor_name, - schedule_value, - ) - return {} - - else: - logger.warning( - "Celery schedule type '%s' not supported by Sentry Crons.", - type(celery_schedule), - ) - return {} - - monitor_config["schedule"] = {} - monitor_config["schedule"]["type"] = schedule_type - monitor_config["schedule"]["value"] = schedule_value - - if schedule_unit is not None: - monitor_config["schedule"]["unit"] = schedule_unit - - monitor_config["timezone"] = ( - ( - hasattr(celery_schedule, "tz") - and celery_schedule.tz is not None - and str(celery_schedule.tz) - ) - or app.timezone - or "UTC" - ) - - return monitor_config - - -def _apply_crons_data_to_schedule_entry( - scheduler: "Any", - schedule_entry: "Any", - integration: "sentry_sdk.integrations.celery.CeleryIntegration", -) -> None: - """ - Add Sentry Crons information to the schedule_entry headers. - """ - if not integration.monitor_beat_tasks: - return - - monitor_name = schedule_entry.name - - task_should_be_excluded = match_regex_list( - monitor_name, integration.exclude_beat_tasks - ) - if task_should_be_excluded: - return - - celery_schedule = schedule_entry.schedule - app = scheduler.app - - monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) - - is_supported_schedule = bool(monitor_config) - if not is_supported_schedule: - return - - headers = schedule_entry.options.pop("headers", {}) - headers.update( - { - "sentry-monitor-slug": monitor_name, - "sentry-monitor-config": monitor_config, - } - ) - - check_in_id = capture_checkin( - monitor_slug=monitor_name, - monitor_config=monitor_config, - status=MonitorStatus.IN_PROGRESS, - ) - headers.update({"sentry-monitor-check-in-id": check_in_id}) - - # Set the Sentry configuration in the options of the ScheduleEntry. - # Those will be picked up in `apply_async` and added to the headers. - schedule_entry.options["headers"] = headers - - -def _wrap_beat_scheduler( - original_function: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Makes sure that: - - a new Sentry trace is started for each task started by Celery Beat and - it is propagated to the task. - - the Sentry Crons information is set in the Celery Beat task's - headers so that is monitored with Sentry Crons. - - After the patched function is called, - Celery Beat will call apply_async to put the task in the queue. - """ - # Patch only once - # Can't use __name__ here, because some of our tests mock original_apply_entry - already_patched = "sentry_patched_scheduler" in str(original_function) - if already_patched: - return original_function - - from sentry_sdk.integrations.celery import CeleryIntegration - - def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(CeleryIntegration) - if integration is None: - return original_function(*args, **kwargs) - - # Tasks started by Celery Beat start a new Trace - scope = sentry_sdk.get_isolation_scope() - scope.set_new_propagation_context() - scope._name = "celery-beat" - - scheduler, schedule_entry = args - _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) - - return original_function(*args, **kwargs) - - return sentry_patched_scheduler - - -def _patch_beat_apply_entry() -> None: - Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) - - -def _patch_redbeat_apply_async() -> None: - if RedBeatScheduler is None: - return - - RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) - - -def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: - if monitor_beat_tasks: - task_success.connect(crons_task_success) - task_failure.connect(crons_task_failure) - task_retry.connect(crons_task_retry) - - -def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_success %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.OK, - ) - - -def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_failure %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) - - -def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_retry %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py deleted file mode 100644 index 8d181f1f24..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/celery/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import time -from typing import TYPE_CHECKING, cast - -if TYPE_CHECKING: - from typing import Tuple - - from sentry_sdk._types import MonitorConfigScheduleUnit - - -def _now_seconds_since_epoch() -> float: - # We cannot use `time.perf_counter()` when dealing with the duration - # of a Celery task, because the start of a Celery task and - # the end are recorded in different processes. - # Start happens in the Celery Beat process, - # the end in a Celery Worker process. - return time.time() - - -def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": - TIME_UNITS = ( # noqa: N806 - ("day", 60 * 60 * 24.0), - ("hour", 60 * 60.0), - ("minute", 60.0), - ) - - seconds = float(seconds) - for unit, divider in TIME_UNITS: - if seconds >= divider: - interval = int(seconds / divider) - return (interval, cast("MonitorConfigScheduleUnit", unit)) - - return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py deleted file mode 100644 index 9baa0e5cdd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/chalice.py +++ /dev/null @@ -1,188 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.aws_lambda import _make_request_event_processor -from sentry_sdk.traces import ( - SpanStatus, - StreamedSpan, - get_current_span, -) -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - parse_version, - reraise, -) - -try: - import chalice # type: ignore - from chalice import Chalice, ChaliceViewError - from chalice import __version__ as CHALICE_VERSION - from chalice.app import ( # type: ignore - EventSourceHandler as ChaliceEventSourceHandler, - ) -except ImportError: - raise DidNotEnable("Chalice is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, TypeVar - - F = TypeVar("F", bound=Callable[..., Any]) - - -class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore - def __call__(self, event: "Any", context: "Any") -> "Any": - client = sentry_sdk.get_client() - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor(event, context, configured_time) - ) - try: - return ChaliceEventSourceHandler.__call__(self, event, context) - except Exception: - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - client.flush() - reraise(*exc_info) - - -def _get_view_function_response( - app: "Any", view_function: "F", function_args: "Any" -) -> "F": - @wraps(view_function) - def wrapped_view_function(**function_args: "Any") -> "Any": - client = sentry_sdk.get_client() - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = app.lambda_context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor( - app.current_request.to_dict(), - app.lambda_context, - configured_time, - ) - ) - - if has_span_streaming_enabled(client.options): - current_span = get_current_span() - segment = None - if type(current_span) is StreamedSpan: - # A segment already exists (created by the AWS Lambda - # integration), so decorate it with Chalice attributes - # The AWS Lambda integration owns the span lifecycle - # (end + flush), but Chalice converts unhandled view exceptions - # into 500 responses, so the error must be captured here. - request_dict = app.current_request.to_dict() - headers = request_dict.get("headers", {}) - - header_attrs: "Dict[str, Any]" = {} - for header, value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attrs[f"http.request.header.{header.lower()}"] = value - - additional_attrs: "Dict[str, Any]" = {} - if "method" in request_dict: - additional_attrs["http.request.method"] = request_dict["method"] - - attributes = { - "sentry.origin": ChaliceIntegration.origin, - **header_attrs, - **additional_attrs, - } - - segment = current_span._segment - segment.set_attributes(attributes) - - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - if segment: - segment.status = SpanStatus.ERROR.value - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - if segment is None: - client.flush() - raise - else: - scope.set_transaction_name( - app.lambda_context.function_name, - source=TransactionSource.COMPONENT, - ) - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - client.flush() - raise - - return wrapped_view_function # type: ignore - - -class ChaliceIntegration(Integration): - identifier = "chalice" - origin = f"auto.function.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(CHALICE_VERSION) - - if version is None: - raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) - - if version < (1, 20): - old_get_view_function_response = Chalice._get_view_function_response - else: - from chalice.app import RestAPIEventHandler - - old_get_view_function_response = ( - RestAPIEventHandler._get_view_function_response - ) - - def sentry_event_response( - app: "Any", view_function: "F", function_args: "Dict[str, Any]" - ) -> "Any": - wrapped_view_function = _get_view_function_response( - app, view_function, function_args - ) - - return old_get_view_function_response( - app, wrapped_view_function, function_args - ) - - if version < (1, 20): - Chalice._get_view_function_response = sentry_event_response - else: - RestAPIEventHandler._get_view_function_response = sentry_event_response - # for everything else (like events) - chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py deleted file mode 100644 index e6b3009548..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/clickhouse_driver.py +++ /dev/null @@ -1,211 +0,0 @@ -import functools -from typing import TYPE_CHECKING, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Any, Callable, ParamSpec, Union -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - - -try: - from clickhouse_driver import VERSION # type: ignore[import-not-found] - from clickhouse_driver.client import Client # type: ignore[import-not-found] - from clickhouse_driver.connection import ( # type: ignore[import-not-found] - Connection, - ) - -except ImportError: - raise DidNotEnable("clickhouse-driver not installed.") - - -class ClickhouseDriverIntegration(Integration): - identifier = "clickhouse_driver" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(ClickhouseDriverIntegration, VERSION) - - # Every query is done using the Connection's `send_query` function - Connection.send_query = _wrap_start(Connection.send_query) - - # If the query contains parameters then the send_data function is used to send those parameters to clickhouse - _wrap_send_data() - - # Every query ends either with the Client's `receive_end_of_query` (no result expected) - # or its `receive_result` (result expected) - Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) - if hasattr(Client, "receive_end_of_insert_query"): - # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` - Client.receive_end_of_insert_query = _wrap_end( - Client.receive_end_of_insert_query - ) - Client.receive_result = _wrap_end(Client.receive_result) - - -P = ParamSpec("P") -T = TypeVar("T") - - -def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": - @functools.wraps(f) - def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(ClickhouseDriverIntegration) is None: - return f(*args, **kwargs) - - connection = args[0] - query = args[1] - query_id = args[2] if len(args) > 2 else kwargs.get("query_id") - params = args[3] if len(args) > 3 else kwargs.get("params") - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=query, # type: ignore - attributes={ - "sentry.op": OP.DB, - "sentry.origin": ClickhouseDriverIntegration.origin, - SPANDATA.DB_QUERY_TEXT: str(query), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=ClickhouseDriverIntegration.origin, - ) - - span.set_data("query", query) - - if query_id: - span.set_data("db.query_id", query_id) - - if params and should_send_default_pii(): - span.set_data("db.params", params) - - connection._sentry_span = span # type: ignore[attr-defined] - - _set_db_data(span, connection) - - # run the original code - ret = f(*args, **kwargs) - - return ret - - return _inner - - -def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": - def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": - res = f(*args, **kwargs) - instance = args[0] - span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] - - if span is None: - return res - - if isinstance(span, StreamedSpan): - span.end() - else: - if res is not None and should_send_default_pii(): - span.set_data("db.result", res) - - with capture_internal_exceptions(): - span.scope.add_breadcrumb( - message=span._data.pop("query"), category="query", data=span._data - ) - - span.finish() - - return res - - return _inner_end - - -def _wrap_send_data() -> None: - original_send_data = Client.send_data - - def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data - self, sample_block, data, types_check=False, columnar=False, *args, **kwargs - ): - span = getattr(self.connection, "_sentry_span", None) - - if isinstance(span, StreamedSpan): - _set_db_data(span, self.connection) - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - if span is not None: - _set_db_data(span, self.connection) - - if should_send_default_pii(): - db_params = span._data.get("db.params", []) - - if isinstance(data, (list, tuple)): - db_params.extend(data) - - else: # data is a generic iterator - orig_data = data - - # Wrap the generator to add items to db.params as they are yielded. - # This allows us to send the params to Sentry without needing to allocate - # memory for the entire generator at once. - def wrapped_generator() -> "Iterator[Any]": - for item in orig_data: - db_params.append(item) - yield item - - # Replace the original iterator with the wrapped one. - data = wrapped_generator() - - span.set_data("db.params", db_params) - - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - Client.send_data = _inner_send_data - - -def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") - span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) - - set_on_span = span.set_attribute - else: - span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") - span.set_data(SPANDATA.DB_NAME, connection.database) - - set_on_span = span.set_data - - set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") - set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) - set_on_span(SPANDATA.SERVER_PORT, connection.port) - set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py deleted file mode 100644 index f6285d0a9b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cloud_resource_context.py +++ /dev/null @@ -1,273 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import urllib3 - -from sentry_sdk.api import set_context -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Dict - - -CONTEXT_TYPE = "cloud_resource" - -HTTP_TIMEOUT = 2.0 - -AWS_METADATA_HOST = "169.254.169.254" -AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) -AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( - AWS_METADATA_HOST -) - -GCP_METADATA_HOST = "metadata.google.internal" -GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( - GCP_METADATA_HOST -) - - -class CLOUD_PROVIDER: # noqa: N801 - """ - Name of the cloud provider. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - ALIBABA = "alibaba_cloud" - AWS = "aws" - AZURE = "azure" - GCP = "gcp" - IBM = "ibm_cloud" - TENCENT = "tencent_cloud" - - -class CLOUD_PLATFORM: # noqa: N801 - """ - The cloud platform. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - AWS_EC2 = "aws_ec2" - AWS_LAMBDA = "aws_lambda" - GCP_COMPUTE_ENGINE = "gcp_compute_engine" - - -class CloudResourceContextIntegration(Integration): - """ - Adds cloud resource context to the Senty scope - """ - - identifier = "cloudresourcecontext" - - cloud_provider = "" - - aws_token = "" - http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) - - gcp_metadata = None - - def __init__(self, cloud_provider: str = "") -> None: - CloudResourceContextIntegration.cloud_provider = cloud_provider - - @classmethod - def _is_aws(cls) -> bool: - try: - r = cls.http.request( - "PUT", - AWS_TOKEN_URL, - headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, - ) - - if r.status != 200: - return False - - cls.aws_token = r.data.decode() - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking AWS metadata service: %s", str(e)) - return False - - @classmethod - def _get_aws_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.AWS, - "cloud.platform": CLOUD_PLATFORM.AWS_EC2, - } - - try: - r = cls.http.request( - "GET", - AWS_METADATA_URL, - headers={"X-aws-ec2-metadata-token": cls.aws_token}, - ) - - if r.status != 200: - return ctx - - data = json.loads(r.data.decode("utf-8")) - - try: - ctx["cloud.account.id"] = data["accountId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = data["availabilityZone"] - except Exception: - pass - - try: - ctx["cloud.region"] = data["region"] - except Exception: - pass - - try: - ctx["host.id"] = data["instanceId"] - except Exception: - pass - - try: - ctx["host.type"] = data["instanceType"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching AWS metadata: %s", str(e)) - - return ctx - - @classmethod - def _is_gcp(cls) -> bool: - try: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return False - - cls.gcp_metadata = json.loads(r.data.decode("utf-8")) - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking GCP metadata service: %s", str(e)) - return False - - @classmethod - def _get_gcp_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.GCP, - "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, - } - - gcp_metadata = cls.gcp_metadata - try: - if cls.gcp_metadata is None: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return ctx - - gcp_metadata = json.loads(r.data.decode("utf-8")) - cls.gcp_metadata = gcp_metadata - - try: - ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( - "/" - )[-1] - except Exception: - pass - - try: - # only populated in google cloud run - ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] - except Exception: - pass - - try: - ctx["host.id"] = gcp_metadata["instance"]["id"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching GCP metadata: %s", str(e)) - - return ctx - - @classmethod - def _get_cloud_provider(cls) -> str: - if cls._is_aws(): - return CLOUD_PROVIDER.AWS - - if cls._is_gcp(): - return CLOUD_PROVIDER.GCP - - return "" - - @classmethod - def _get_cloud_resource_context(cls) -> "Dict[str, str]": - cloud_provider = ( - cls.cloud_provider - if cls.cloud_provider != "" - else CloudResourceContextIntegration._get_cloud_provider() - ) - if cloud_provider in context_getters.keys(): - return context_getters[cloud_provider]() - - return {} - - @staticmethod - def setup_once() -> None: - cloud_provider = CloudResourceContextIntegration.cloud_provider - unsupported_cloud_provider = ( - cloud_provider != "" and cloud_provider not in context_getters.keys() - ) - - if unsupported_cloud_provider: - logger.warning( - "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", - CloudResourceContextIntegration.cloud_provider, - list(context_getters.keys()), - ) - - context = CloudResourceContextIntegration._get_cloud_resource_context() - if context != {}: - set_context(CONTEXT_TYPE, context) - - -# Map with the currently supported cloud providers -# mapping to functions extracting the context -context_getters = { - CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, - CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, -} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py deleted file mode 100644 index 7abf3f6808..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/cohere.py +++ /dev/null @@ -1,303 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise - -try: - from cohere import ( - ChatStreamEndEvent, - NonStreamedChatResponse, - ) - from cohere.base_client import BaseCohere - from cohere.client import Client - - if TYPE_CHECKING: - from cohere import StreamedChatResponse -except ImportError: - raise DidNotEnable("Cohere not installed") - -try: - # cohere 5.9.3+ - from cohere import StreamEndStreamedChatResponse -except ImportError: - from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse - - -COLLECTED_CHAT_PARAMS = { - "model": SPANDATA.AI_MODEL_ID, - "k": SPANDATA.AI_TOP_K, - "p": SPANDATA.AI_TOP_P, - "seed": SPANDATA.AI_SEED, - "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, - "raw_prompting": SPANDATA.AI_RAW_PROMPTING, -} - -COLLECTED_PII_CHAT_PARAMS = { - "tools": SPANDATA.AI_TOOLS, - "preamble": SPANDATA.AI_PREAMBLE, -} - -COLLECTED_CHAT_RESP_ATTRS = { - "generation_id": SPANDATA.AI_GENERATION_ID, - "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, - "finish_reason": SPANDATA.AI_FINISH_REASON, -} - -COLLECTED_PII_CHAT_RESP_ATTRS = { - "citations": SPANDATA.AI_CITATIONS, - "documents": SPANDATA.AI_DOCUMENTS, - "search_queries": SPANDATA.AI_SEARCH_QUERIES, - "search_results": SPANDATA.AI_SEARCH_RESULTS, - "tool_calls": SPANDATA.AI_TOOL_CALLS, -} - - -class CohereIntegration(Integration): - identifier = "cohere" - origin = f"auto.ai.{identifier}" - - def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) - Client.embed = _wrap_embed(Client.embed) - BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "cohere", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _end_span(span: "Any") -> None: - if isinstance(span, StreamedSpan): - span.end() - else: - span.__exit__(None, None, None) - - -def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": - def collect_chat_response_fields( - span: "Union[Span, StreamedSpan]", - res: "NonStreamedChatResponse", - include_pii: bool, - ) -> None: - if include_pii: - if hasattr(res, "text"): - set_data_normalized( - span, - SPANDATA.AI_RESPONSES, - [res.text], - ) - for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: - if hasattr(res, pii_attr): - set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) - - for attr in COLLECTED_CHAT_RESP_ATTRS: - if hasattr(res, attr): - set_data_normalized(span, "ai." + attr, getattr(res, attr)) - - if hasattr(res, "meta"): - if hasattr(res.meta, "billed_units"): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - output_tokens=res.meta.billed_units.output_tokens, - ) - elif hasattr(res.meta, "tokens"): - record_token_usage( - span, - input_tokens=res.meta.tokens.input_tokens, - output_tokens=res.meta.tokens.output_tokens, - ) - - if hasattr(res.meta, "warnings"): - set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) - - @wraps(f) - def new_chat(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if ( - integration is None - or "message" not in kwargs - or not isinstance(kwargs.get("message"), str) - ): - return f(*args, **kwargs) - - message = kwargs.get("message") - - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name="cohere.client.Chat", - attributes={ - "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - name="cohere.client.Chat", - origin=CohereIntegration.origin, - ) - span.__enter__() - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - - reraise(*exc_info) - - with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, - SPANDATA.AI_INPUT_MESSAGES, - list( - map( - lambda x: { - "role": getattr(x, "role", "").lower(), - "content": getattr(x, "message", ""), - }, - kwargs.get("chat_history", []), - ) - ) - + [{"role": "user", "content": message}], - ) - for k, v in COLLECTED_PII_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - - for k, v in COLLECTED_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - set_data_normalized(span, SPANDATA.AI_STREAMING, False) - - if streaming: - old_iterator = res - - def new_iterator() -> "Iterator[StreamedChatResponse]": - with capture_internal_exceptions(): - for x in old_iterator: - if isinstance(x, ChatStreamEndEvent) or isinstance( - x, StreamEndStreamedChatResponse - ): - collect_chat_response_fields( - span, - x.response, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - yield x - _end_span(span) - - return new_iterator() - elif isinstance(res, NonStreamedChatResponse): - collect_chat_response_fields( - span, - res, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - _end_span(span) - else: - set_data_normalized(span, "unknown_response", True) - _end_span(span) - return res - - return new_chat - - -def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - if integration is None: - return f(*args, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name="Cohere Embedding Creation", - attributes={ - "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span_ctx = get_start_span_function()( - op=consts.OP.COHERE_EMBEDDINGS_CREATE, - name="Cohere Embedding Creation", - origin=CohereIntegration.origin, - ) - - with span_ctx as span: - if "texts" in kwargs and ( - should_send_default_pii() and integration.include_prompts - ): - if isinstance(kwargs["texts"], str): - set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) - elif ( - isinstance(kwargs["texts"], list) - and len(kwargs["texts"]) > 0 - and isinstance(kwargs["texts"][0], str) - ): - set_data_normalized( - span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] - ) - - if "model" in kwargs: - set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - reraise(*exc_info) - if ( - hasattr(res, "meta") - and hasattr(res.meta, "billed_units") - and hasattr(res.meta.billed_units, "input_tokens") - ): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - total_tokens=res.meta.billed_units.input_tokens, - ) - return res - - return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py deleted file mode 100644 index a0e9014666..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dedupe.py +++ /dev/null @@ -1,62 +0,0 @@ -import weakref -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class DedupeIntegration(Integration): - identifier = "dedupe" - - def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if hint is None: - return event - - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return event - - exc_info = hint.get("exc_info", None) - if exc_info is None: - return event - - last_seen = integration._last_seen.get(None) - if last_seen is not None: - # last_seen is either a weakref or the original instance - last_seen = ( - last_seen() if isinstance(last_seen, weakref.ref) else last_seen - ) - - exc = exc_info[1] - if last_seen is exc: - logger.info("DedupeIntegration dropped duplicated error event %s", exc) - return None - - # we can only weakref non builtin types - try: - integration._last_seen.set(weakref.ref(exc)) - except TypeError: - integration._last_seen.set(exc) - - return event - - @staticmethod - def reset_last_seen() -> None: - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return - - integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py deleted file mode 100644 index 361b60079d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/__init__.py +++ /dev/null @@ -1,884 +0,0 @@ -import inspect -import sys -import threading -import weakref -from importlib import import_module - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANNAME -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - transaction_from_function, - walk_exception_chain, -) - -try: - from django import VERSION as DJANGO_VERSION - from django.conf import settings - from django.conf import settings as django_settings - from django.core import signals - from django.utils.functional import SimpleLazyObject - - try: - from django.urls import resolve - except ImportError: - from django.core.urlresolvers import resolve - - try: - from django.urls import Resolver404 - except ImportError: - from django.core.urlresolvers import Resolver404 - - # Only available in Django 3.0+ - try: - from django.core.handlers.asgi import ASGIRequest - except Exception: - ASGIRequest = None - -except ImportError: - raise DidNotEnable("Django not installed") - -from sentry_sdk.integrations.django.middleware import patch_django_middlewares -from sentry_sdk.integrations.django.signals_handlers import patch_signals -from sentry_sdk.integrations.django.tasks import patch_tasks -from sentry_sdk.integrations.django.templates import ( - get_template_frame_from_exception, - patch_templates, -) -from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER -from sentry_sdk.integrations.django.views import patch_views - -if DJANGO_VERSION[:2] > (1, 8): - from sentry_sdk.integrations.django.caching import patch_caching -else: - patch_caching = None # type: ignore - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from django.core.handlers.wsgi import WSGIRequest - from django.http.request import QueryDict - from django.http.response import HttpResponse - from django.utils.datastructures import MultiValueDict - - from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -if DJANGO_VERSION < (1, 10): - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated() - -else: - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated - - -TRANSACTION_STYLE_VALUES = ("function_name", "url") - - -class DjangoIntegration(Integration): - """ - Auto instrument a Django application. - - :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. - :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. - :param signals_spans: Whether to create spans for signals. Defaults to `True`. - :param signals_denylist: A list of signals to ignore when creating spans. - :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. - """ - - identifier = "django" - origin = f"auto.http.{identifier}" - origin_db = f"auto.db.{identifier}" - - transaction_style = "" - middleware_spans: "Optional[bool]" = None - signals_spans: "Optional[bool]" = None - cache_spans: "Optional[bool]" = None - signals_denylist: "list[signals.Signal]" = [] - - def __init__( - self, - transaction_style: str = "url", - middleware_spans: bool = False, - signals_spans: bool = True, - cache_spans: bool = False, - db_transaction_spans: bool = False, - signals_denylist: "Optional[list[signals.Signal]]" = None, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - - self.signals_spans = signals_spans - self.signals_denylist = signals_denylist or [] - - self.cache_spans = cache_spans - self.db_transaction_spans = db_transaction_spans - - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(DjangoIntegration, DJANGO_VERSION) - - install_sql_hook() - # Patch in our custom middleware. - - # logs an error for every 500 - ignore_logger("django.server") - ignore_logger("django.request") - - from django.core.handlers.wsgi import WSGIHandler - - old_app = WSGIHandler.__call__ - - @ensure_integration_enabled(DjangoIntegration, old_app) - def sentry_patched_wsgi_handler( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - bound_old_app = old_app.__get__(self, WSGIHandler) - - from django.conf import settings - - use_x_forwarded_for = settings.USE_X_FORWARDED_HOST - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - middleware = SentryWsgiMiddleware( - bound_old_app, - use_x_forwarded_for, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - WSGIHandler.__call__ = sentry_patched_wsgi_handler - - _patch_get_response() - - _patch_django_asgi_handler() - - signals.got_request_exception.connect(_got_request_exception) - - @add_global_event_processor - def process_django_templates( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_, exc_value, _) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - frame = get_template_frame_from_exception(exc_value) - if frame is not None: - frames = exception.get("stacktrace", {}).get("frames", []) - - for i in reversed(range(len(frames))): - f = frames[i] - if ( - f.get("function") in ("Parser.parse", "parse", "render") - and f.get("module") == "django.template.base" - ): - i += 1 - break - else: - i = len(frames) - - frames.insert(i, frame) - - return event - - @add_global_repr_processor - def _django_queryset_repr( - value: "Any", hint: "Dict[str, Any]" - ) -> "Union[NotImplementedType, str]": - try: - # Django 1.6 can fail to import `QuerySet` when Django settings - # have not yet been initialized. - # - # If we fail to import, return `NotImplemented`. It's at least - # unlikely that we have a query set in `value` when importing - # `QuerySet` fails. - from django.db.models.query import QuerySet - except Exception: - return NotImplemented - - if not isinstance(value, QuerySet) or value._result_cache: - return NotImplemented - - return "<%s from %s at 0x%x>" % ( - value.__class__.__name__, - value.__module__, - id(value), - ) - - _patch_channels() - patch_django_middlewares() - patch_views() - patch_templates() - patch_signals() - patch_tasks() - add_template_context_repr_sequence() - - if patch_caching is not None: - patch_caching() - - -_DRF_PATCHED = False -_DRF_PATCH_LOCK = threading.Lock() - - -def _patch_drf() -> None: - """ - Patch Django Rest Framework for more/better request data. DRF's request - type is a wrapper around Django's request type. The attribute we're - interested in is `request.data`, which is a cached property containing a - parsed request body. Reading a request body from that property is more - reliable than reading from any of Django's own properties, as those don't - hold payloads in memory and therefore can only be accessed once. - - We patch the Django request object to include a weak backreference to the - DRF request object, such that we can later use either in - `DjangoRequestExtractor`. - - This function is not called directly on SDK setup, because importing almost - any part of Django Rest Framework will try to access Django settings (where - `sentry_sdk.init()` might be called from in the first place). Instead we - run this function on every request and do the patching on the first - request. - """ - - global _DRF_PATCHED - - if _DRF_PATCHED: - # Double-checked locking - return - - with _DRF_PATCH_LOCK: - if _DRF_PATCHED: - return - - # We set this regardless of whether the code below succeeds or fails. - # There is no point in trying to patch again on the next request. - _DRF_PATCHED = True - - with capture_internal_exceptions(): - try: - from rest_framework.views import APIView # type: ignore - except ImportError: - pass - else: - old_drf_initial = APIView.initial - - def sentry_patched_drf_initial( - self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - with capture_internal_exceptions(): - request._request._sentry_drf_request_backref = weakref.ref( - request - ) - pass - return old_drf_initial(self, request, *args, **kwargs) - - APIView.initial = sentry_patched_drf_initial - - -def _patch_channels() -> None: - try: - from channels.http import AsgiHandler # type: ignore - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl - - patch_channels_asgi_handler_impl(AsgiHandler) - - -def _patch_django_asgi_handler() -> None: - try: - from django.core.handlers.asgi import ASGIHandler - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl - - patch_django_asgi_handler_impl(ASGIHandler) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" -) -> None: - try: - transaction_name = None - if transaction_style == "function_name": - fn = resolve(request.path).func - transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) - - elif transaction_style == "url": - if hasattr(request, "urlconf"): - transaction_name = LEGACY_RESOLVER.resolve( - request.path_info, urlconf=request.urlconf - ) - else: - transaction_name = LEGACY_RESOLVER.resolve(request.path_info) - - if transaction_name is None: - transaction_name = request.path_info - source = TransactionSource.URL - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name( - transaction_name, - source=source, - ) - except Resolver404: - urlconf = import_module(settings.ROOT_URLCONF) - # This exception only gets thrown when transaction_style is `function_name` - # So we don't check here what style is configured - if hasattr(urlconf, "handler404"): - handler = urlconf.handler404 - if isinstance(handler, str): - scope.transaction = handler - else: - scope.transaction = transaction_from_function( - getattr(handler, "view_class", handler) - ) - except Exception: - pass - - -def _before_get_response(request: "WSGIRequest") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return - - _patch_drf() - - scope = sentry_sdk.get_current_scope() - # Rely on WSGI middleware to start a trace - _set_transaction_name_and_source(scope, integration.transaction_style, request) - - scope.add_event_processor( - _make_wsgi_request_event_processor(weakref.ref(request), integration) - ) - - -def _attempt_resolve_again( - request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str -) -> None: - """ - Some django middlewares overwrite request.urlconf - so we need to respect that contract, - so we try to resolve the url again. - """ - if not hasattr(request, "urlconf"): - return - - _set_transaction_name_and_source(scope, transaction_style, request) - - -def _after_get_response(request: "WSGIRequest") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming and should_send_default_pii(): - user = getattr(request, "user", None) - - # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. - # Exit early if the user has not been materialized yet. - is_lazy = isinstance(user, SimpleLazyObject) - if is_lazy and hasattr(request, "_cached_user"): - user = request._cached_user - elif is_lazy: - return - - if user is None or not is_authenticated(user): - return - - user_info = {} - try: - user_info["id"] = str(user.pk) - except Exception: - pass - - try: - user_info["email"] = user.email - except Exception: - pass - - try: - user_info["username"] = user.get_username() - except Exception: - pass - - sentry_sdk.set_user(user_info) - - -def _patch_get_response() -> None: - """ - patch get_response, because at that point we have the Django request object - """ - from django.core.handlers.base import BaseHandler - - old_get_response = BaseHandler.get_response - - def sentry_patched_get_response( - self: "Any", request: "WSGIRequest" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - rv = old_get_response(self, request) - _after_get_response(request) - return rv - - BaseHandler.get_response = sentry_patched_get_response - - if hasattr(BaseHandler, "get_response_async"): - from sentry_sdk.integrations.django.asgi import patch_get_response_async - - patch_get_response_async(BaseHandler, _before_get_response) - - -def _make_wsgi_request_event_processor( - weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" -) -> "EventProcessor": - def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - request = weak_request() - if request is None: - return event - - django_3 = ASGIRequest is not None - if django_3 and type(request) == ASGIRequest: - # We have a `asgi_request_event_processor` for this. - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return wsgi_request_event_processor - - -def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if request is not None and integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - event, hint = event_from_exception( - sys.exc_info(), - client_options=client.options, - mechanism={"type": "django", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class DjangoRequestExtractor(RequestExtractor): - def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: - try: - drf_request = request._sentry_drf_request_backref() - if drf_request is not None: - request = drf_request - except AttributeError: - pass - self.request = request - - def env(self) -> "Dict[str, str]": - return self.request.META - - def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": - privacy_cookies = [ - django_settings.CSRF_COOKIE_NAME, - django_settings.SESSION_COOKIE_NAME, - ] - - clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} - for key, val in self.request.COOKIES.items(): - if key in privacy_cookies: - clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE - else: - clean_cookies[key] = val - - return clean_cookies - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "QueryDict": - return self.request.POST - - def files(self) -> "MultiValueDict": - return self.request.FILES - - def size_of_file(self, file: "Any") -> int: - return file.size - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - return self.request.data - except Exception: - return RequestExtractor.parsed_body(self) - - -def _set_user_info(request: "WSGIRequest", event: "Event") -> None: - user_info = event.setdefault("user", {}) - - user = getattr(request, "user", None) - - if user is None or not is_authenticated(user): - return - - try: - user_info.setdefault("id", str(user.pk)) - except Exception: - pass - - try: - user_info.setdefault("email", user.email) - except Exception: - pass - - try: - user_info.setdefault("username", user.get_username()) - except Exception: - pass - - -def install_sql_hook() -> None: - """If installed this causes Django's queries to be captured.""" - try: - from django.db.backends.utils import CursorWrapper - except ImportError: - from django.db.backends.util import CursorWrapper - - try: - # django 1.6 and 1.7 compatability - from django.db.backends import BaseDatabaseWrapper - except ImportError: - # django 1.8 or later - from django.db.backends.base.base import BaseDatabaseWrapper - - try: - real_execute = CursorWrapper.execute - real_executemany = CursorWrapper.executemany - real_connect = BaseDatabaseWrapper.connect - real_commit = BaseDatabaseWrapper._commit - real_rollback = BaseDatabaseWrapper._rollback - except AttributeError: - # This won't work on Django versions < 1.6 - return - - @ensure_integration_enabled(DjangoIntegration, real_execute) - def execute( - self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=params, - paramstyle="format", - executemany=False, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - result = real_execute(self, sql, params) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_executemany) - def executemany( - self: "CursorWrapper", sql: "Any", param_list: "List[Any]" - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=param_list, - paramstyle="format", - executemany=True, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - - result = real_executemany(self, sql, param_list) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_connect) - def connect(self: "BaseDatabaseWrapper") -> None: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message="connect", category="query") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="connect", - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self) - return real_connect(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - return real_connect(self) - - def _commit(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_commit(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_COMMIT, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_COMMIT, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - - def _rollback(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_rollback(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_ROLLBACK, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_ROLLBACK, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - - CursorWrapper.execute = execute - CursorWrapper.executemany = executemany - BaseDatabaseWrapper.connect = connect - BaseDatabaseWrapper._commit = _commit - BaseDatabaseWrapper._rollback = _rollback - ignore_logger("django.db.backends") - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", - cursor_or_db: "Any", - db_operation: "Optional[str]" = None, -) -> None: - db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db - vendor = db.vendor - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) - - if db_operation is not None: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) - else: - span.set_data(SPANDATA.DB_SYSTEM, vendor) - - if db_operation is not None: - span.set_data(SPANDATA.DB_OPERATION, db_operation) - - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` - # actually has a `connection` and the `connection` has a `get_dsn_parameters` - # attribute, only to throw an error once you actually want to call it. - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable - # function. - is_psycopg2 = ( - hasattr(cursor_or_db, "connection") - and hasattr(cursor_or_db.connection, "get_dsn_parameters") - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) - ) - if is_psycopg2: - connection_params = cursor_or_db.connection.get_dsn_parameters() - else: - try: - # psycopg3, only extract needed params as get_parameters - # can be slow because of the additional logic to filter out default - # values - connection_params = { - "dbname": cursor_or_db.connection.info.dbname, - "port": cursor_or_db.connection.info.port, - } - # PGhost returns host or base dir of UNIX socket as an absolute path - # starting with /, use it only when it contains host - pg_host = cursor_or_db.connection.info.host - if pg_host and not pg_host.startswith("/"): - connection_params["host"] = pg_host - except Exception: - connection_params = db.get_connection_params() - - db_name = connection_params.get("dbname") or connection_params.get("database") - - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - - set_on_span = span.set_attribute - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - set_on_span = span.set_data - - server_address = connection_params.get("host") - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = connection_params.get("port") - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, str(server_port)) - - server_socket_address = connection_params.get("unix_socket") - if server_socket_address is not None: - set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) - - -def add_template_context_repr_sequence() -> None: - try: - from django.template.context import BaseContext - - add_repr_sequence_type(BaseContext) - except Exception: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py deleted file mode 100644 index 43faffb5be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/asgi.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Instrumentation for Django 3.0 - -Since this file contains `async def` it is conditionally imported in -`sentry_sdk.integrations.django` (depending on the existence of -`django.core.handlers.asgi`. -""" - -import asyncio -import functools -import inspect -from typing import TYPE_CHECKING - -from django.core.handlers.wsgi import WSGIRequest - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, TypeVar, Union - - from django.core.handlers.asgi import ASGIRequest - from django.http.response import HttpResponse - - from sentry_sdk._types import Event, EventProcessor - - _F = TypeVar("_F", bound=Callable[..., Any]) - - -# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for -# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. -# The latter is replaced with the inspect.markcoroutinefunction decorator. -# Until 3.12 is the minimum supported Python version, provide a shim. -# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py -if hasattr(inspect, "markcoroutinefunction"): - iscoroutinefunction = inspect.iscoroutinefunction - markcoroutinefunction = inspect.markcoroutinefunction -else: - iscoroutinefunction = asyncio.iscoroutinefunction - - def markcoroutinefunction(func: "_F") -> "_F": - func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore - return func - - -def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": - def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - from sentry_sdk.integrations.django import ( - DjangoRequestExtractor, - _set_user_info, - ) - - if request is None: - return event - - if type(request) == WSGIRequest: - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return asgi_request_event_processor - - -def patch_django_asgi_handler_impl(cls: "Any") -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - )._run_asgi3 - - return await middleware(scope, receive, send) - - cls.__call__ = sentry_patched_asgi_handler - - modern_django_asgi_support = hasattr(cls, "create_request") - if modern_django_asgi_support: - old_create_request = cls.create_request - - @ensure_integration_enabled(DjangoIntegration, old_create_request) - def sentry_patched_create_request( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request, error_response = old_create_request(self, *args, **kwargs) - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_make_asgi_request_event_processor(request)) - - return request, error_response - - cls.create_request = sentry_patched_create_request - - -def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: - old_get_response_async = cls.get_response_async - - async def sentry_patched_get_response_async( - self: "Any", request: "Any" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - return await old_get_response_async(self, request) - - cls.get_response_async = sentry_patched_get_response_async - - -def patch_channels_asgi_handler_impl(cls: "Any") -> None: - import channels # type: ignore - - from sentry_sdk.integrations.django import DjangoIntegration - - if channels.__version__ < "3.0.0": - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, receive, send) - - middleware = SentryAsgiMiddleware( - lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - ) - - return await middleware(self.scope)(receive, send) # type: ignore - - cls.__call__ = sentry_patched_asgi_handler - - else: - # The ASGI handler in Channels >= 3 has the same signature as - # the Django handler. - patch_django_asgi_handler_impl(cls) - - -def wrap_async_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - async def sentry_wrapped_callback( - request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return await callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return await callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return await callback(request, *args, **kwargs) - - return sentry_wrapped_callback - - -def _asgi_middleware_mixin_factory( - _check_middleware_span: "Callable[..., Any]", -) -> "Any": - """ - Mixin class factory that generates a middleware mixin for handling requests - in async mode. - """ - - class SentryASGIMixin: - if TYPE_CHECKING: - _inner = None - - def __init__(self, get_response: "Callable[..., Any]") -> None: - self.get_response = get_response - self._acall_method = None - self._async_check() - - def _async_check(self) -> None: - """ - If get_response is a coroutine function, turns us into async mode so - a thread is not consumed during a whole request. - Taken from django.utils.deprecation::MiddlewareMixin._async_check - """ - if iscoroutinefunction(self.get_response): - markcoroutinefunction(self) - - def async_route_check(self) -> bool: - """ - Function that checks if we are in async mode, - and if we are forwards the handling of requests to __acall__ - """ - return iscoroutinefunction(self.get_response) - - async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": - f = self._acall_method - if f is None: - if hasattr(self._inner, "__acall__"): - self._acall_method = f = self._inner.__acall__ # type: ignore - else: - self._acall_method = f = self._inner - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return await f(*args, **kwargs) # type: ignore - - with middleware_span: - return await f(*args, **kwargs) # type: ignore - - return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py deleted file mode 100644 index faf1803c11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/caching.py +++ /dev/null @@ -1,264 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.core.cache import CacheHandler -from urllib3.util import parse_url as urlparse - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - -METHODS_TO_INSTRUMENT = [ - "set", - "set_many", - "get", - "get_many", -] - - -def _get_span_description( - method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" -) -> str: - return _key_as_string(_get_safe_key(method_name, args, kwargs)) - - -def _patch_cache_method( - cache: "CacheHandler", - method_name: str, - address: "Optional[str]", - port: "Optional[int]", -) -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - original_method = getattr(cache, method_name) - - @ensure_integration_enabled(DjangoIntegration, original_method) - def _instrument_call( - cache: "CacheHandler", - method_name: str, - original_method: "Callable[..., Any]", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - address: "Optional[str]", - port: "Optional[int]", - ) -> "Any": - is_set_operation = method_name.startswith("set") - is_get_method = method_name == "get" - is_get_many_method = method_name == "get_many" - - op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET - description = _get_span_description(method_name, args, kwargs) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": op, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_attribute(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - else: - with sentry_sdk.start_span( - op=op, - name=description, - origin=DjangoIntegration.origin, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_data(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_data(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - - @functools.wraps(original_method) - def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": - return _instrument_call( - cache, method_name, original_method, args, kwargs, address, port - ) - - setattr(cache, method_name, sentry_method) - - -def _patch_cache( - cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None -) -> None: - if not hasattr(cache, "_sentry_patched"): - for method_name in METHODS_TO_INSTRUMENT: - _patch_cache_method(cache, method_name, address, port) - cache._sentry_patched = True - - -def _get_address_port( - settings: "dict[str, Any]", -) -> "tuple[Optional[str], Optional[int]]": - location = settings.get("LOCATION") - - # TODO: location can also be an array of locations - # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis - # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 - if not isinstance(location, str): - return None, None - - if "://" in location: - parsed_url = urlparse(location) - # remove the username and password from URL to not leak sensitive data. - address = "{}://{}{}".format( - parsed_url.scheme or "", - parsed_url.hostname or "", - parsed_url.path or "", - ) - port = parsed_url.port - else: - address = location - port = None - - return address, int(port) if port is not None else None - - -def should_enable_cache_spans() -> bool: - from sentry_sdk.integrations.django import DjangoIntegration - - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - from django.conf import settings - - return integration is not None and ( - (client.spotlight is not None and settings.DEBUG is True) - or integration.cache_spans is True - ) - - -def patch_caching() -> None: - if not hasattr(CacheHandler, "_sentry_patched"): - if DJANGO_VERSION < (3, 2): - original_get_item = CacheHandler.__getitem__ - - @functools.wraps(original_get_item) - def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": - cache = original_get_item(self, alias) - - if should_enable_cache_spans(): - from django.conf import settings - - address, port = _get_address_port( - settings.CACHES[alias or "default"] - ) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.__getitem__ = sentry_get_item - CacheHandler._sentry_patched = True - - else: - original_create_connection = CacheHandler.create_connection - - @functools.wraps(original_create_connection) - def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": - cache = original_create_connection(self, alias) - - if should_enable_cache_spans(): - address, port = _get_address_port(self.settings[alias or "default"]) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.create_connection = sentry_create_connection - CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py deleted file mode 100644 index a14ec96ff5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/middleware.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Create spans from Django middleware invocations -""" - -from functools import wraps -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - F = TypeVar("F", bound=Callable[..., Any]) - -_import_string_should_wrap_middleware = ContextVar( - "import_string_should_wrap_middleware" -) - -DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) - -if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: - _asgi_middleware_mixin_factory = lambda _: object - iscoroutinefunction = lambda _: False -else: - from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction - - -def patch_django_middlewares() -> None: - from django.core.handlers import base - - old_import_string = base.import_string - - def sentry_patched_import_string(dotted_path: str) -> "Any": - rv = old_import_string(dotted_path) - - if _import_string_should_wrap_middleware.get(None): - rv = _wrap_middleware(rv, dotted_path) - - return rv - - base.import_string = sentry_patched_import_string - - old_load_middleware = base.BaseHandler.load_middleware - - def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": - _import_string_should_wrap_middleware.set(True) - try: - return old_load_middleware(*args, **kwargs) - finally: - _import_string_should_wrap_middleware.set(False) - - base.BaseHandler.load_middleware = sentry_patched_load_middleware - - -def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - def _check_middleware_span( - old_method: "Callable[..., Any]", - ) -> "Optional[Union[Span, StreamedSpan]]": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None or not integration.middleware_spans: - return None - - function_name = transaction_from_function(old_method) - - description = middleware_name - function_basename = getattr(old_method, "__name__", None) - if function_basename: - description = "{}.{}".format(description, function_basename) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - middleware_span: "Union[Span, StreamedSpan]" - if span_streaming: - middleware_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.MIDDLEWARE_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - else: - middleware_span = sentry_sdk.start_span( - op=OP.MIDDLEWARE_DJANGO, - name=description, - origin=DjangoIntegration.origin, - ) - middleware_span.set_tag("django.function_name", function_name) - middleware_span.set_tag("django.middleware_name", middleware_name) - - return middleware_span - - def _get_wrapped_method(old_method: "F") -> "F": - with capture_internal_exceptions(): - # Middleware hooks (e.g. `process_view`, `process_exception`) may be - # `async def` when the middleware is async. A synchronous wrapper - # would hide the coroutine from Django's `iscoroutinefunction` check, - # causing Django to call the hook synchronously and never await the - # returned coroutine. Wrap async hooks with an async wrapper so the - # wrapped method continues to report as a coroutine function. - if iscoroutinefunction is not None and iscoroutinefunction(old_method): - - async def async_sentry_wrapped_method( - *args: "Any", **kwargs: "Any" - ) -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return await old_method(*args, **kwargs) - - with middleware_span: - return await old_method(*args, **kwargs) - - sentry_wrapped_method = async_sentry_wrapped_method - - else: - - def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return old_method(*args, **kwargs) - - with middleware_span: - return old_method(*args, **kwargs) - - sentry_wrapped_method = sync_sentry_wrapped_method - - try: - # fails for __call__ of function on Python 2 (see py2.7-django-1.11) - sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) - - # Necessary for Django 3.1 - sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore - except Exception: - pass - - return sentry_wrapped_method # type: ignore - - return old_method - - class SentryWrappingMiddleware( - _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore - ): - sync_capable = getattr(middleware, "sync_capable", True) - async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( - middleware, "async_capable", False - ) - - def __init__( - self, - get_response: "Optional[Callable[..., Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> None: - if get_response: - self._inner = middleware(get_response, *args, **kwargs) - else: - self._inner = middleware(*args, **kwargs) - self.get_response = get_response - self._call_method = None - if self.async_capable: - super().__init__(get_response) - - # We need correct behavior for `hasattr()`, which we can only determine - # when we have an instance of the middleware we're wrapping. - def __getattr__(self, method_name: str) -> "Any": - if method_name not in ( - "process_request", - "process_view", - "process_template_response", - "process_response", - "process_exception", - ): - raise AttributeError() - - old_method = getattr(self._inner, method_name) - rv = _get_wrapped_method(old_method) - self.__dict__[method_name] = rv - return rv - - def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": - if hasattr(self, "async_route_check") and self.async_route_check(): - return self.__acall__(*args, **kwargs) - - f = self._call_method - if f is None: - self._call_method = f = self._inner.__call__ - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return f(*args, **kwargs) - - with middleware_span: - return f(*args, **kwargs) - - for attr in ( - "__name__", - "__module__", - "__qualname__", - ): - if hasattr(middleware, attr): - setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) - - return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py deleted file mode 100644 index 7140ead782..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/signals_handlers.py +++ /dev/null @@ -1,105 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from django.dispatch import Signal - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.django import DJANGO_VERSION -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Union - - -def _get_receiver_name(receiver: "Callable[..., Any]") -> str: - name = "" - - if hasattr(receiver, "__qualname__"): - name = receiver.__qualname__ - elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ - name = receiver.__name__ - elif hasattr( - receiver, "func" - ): # certain functions (like partials) dont have a name - if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): - name = "partial()" - - if ( - name == "" - ): # In case nothing was found, return the string representation (this is the slowest case) - return str(receiver) - - if hasattr(receiver, "__module__"): # prepend with module, if there is one - name = receiver.__module__ + "." + name - - return name - - -def patch_signals() -> None: - """ - Patch django signal receivers to create a span. - - This only wraps sync receivers. Django>=5.0 introduced async receivers, but - since we don't create transactions for ASGI Django, we don't wrap them. - """ - from sentry_sdk.integrations.django import DjangoIntegration - - old_live_receivers = Signal._live_receivers - - def _sentry_live_receivers( - self: "Signal", sender: "Any" - ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": - if DJANGO_VERSION >= (5, 0): - sync_receivers, async_receivers = old_live_receivers(self, sender) - else: - sync_receivers = old_live_receivers(self, sender) - async_receivers = [] - - def sentry_sync_receiver_wrapper( - receiver: "Callable[..., Any]", - ) -> "Callable[..., Any]": - @wraps(receiver) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - signal_name = _get_receiver_name(receiver) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - with sentry_sdk.traces.start_span( - name=signal_name, - attributes={ - "sentry.op": OP.EVENT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.CODE_FUNCTION_NAME: signal_name, - }, - ) as span: - return receiver(*args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.EVENT_DJANGO, - name=signal_name, - origin=DjangoIntegration.origin, - ) as span: - span.set_data("signal", signal_name) - return receiver(*args, **kwargs) - - return wrapper - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if ( - integration - and integration.signals_spans - and self not in integration.signals_denylist - ): - for idx, receiver in enumerate(sync_receivers): - sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) - - if DJANGO_VERSION >= (5, 0): - return sync_receivers, async_receivers - else: - return sync_receivers - - Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py deleted file mode 100644 index 5e23c258fb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/tasks.py +++ /dev/null @@ -1,52 +0,0 @@ -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import qualname_from_function - -try: - # django.tasks were added in Django 6.0 - from django.tasks.base import Task -except ImportError: - Task = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -def patch_tasks() -> None: - if Task is None: - return - - old_task_enqueue = Task.enqueue - - @wraps(old_task_enqueue) - def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return old_task_enqueue(self, *args, **kwargs) - - name = qualname_from_function(self.func) or "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_task_enqueue(self, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin - ): - return old_task_enqueue(self, *args, **kwargs) - - Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py deleted file mode 100644 index 5ab89d4a74..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/templates.py +++ /dev/null @@ -1,209 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.template import TemplateSyntaxError -from django.utils.safestring import mark_safe - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Optional, Tuple - -try: - # support Django 1.9 - from django.template.base import Origin -except ImportError: - # backward compatibility - from django.template.loader import LoaderOrigin as Origin - - -def get_template_frame_from_exception( - exc_value: "Optional[BaseException]", -) -> "Optional[Dict[str, Any]]": - # As of Django 1.9 or so the new template debug thing showed up. - if hasattr(exc_value, "template_debug"): - return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore - - # As of r16833 (Django) all exceptions may contain a - # ``django_template_source`` attribute (rather than the legacy - # ``TemplateSyntaxError.source`` check) - if hasattr(exc_value, "django_template_source"): - return _get_template_frame_from_source( - exc_value.django_template_source # type: ignore - ) - - if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): - source = exc_value.source - if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): - return _get_template_frame_from_source(source) # type: ignore - - return None - - -def _get_template_name_description(template_name: str) -> str: - if isinstance(template_name, (list, tuple)): - if template_name: - return "[{}, ...]".format(template_name[0]) - else: - return template_name - - -def patch_templates() -> None: - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - real_rendered_content = SimpleTemplateResponse.rendered_content - - @property # type: ignore - @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) - def rendered_content(self: "SimpleTemplateResponse") -> str: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(self.template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_rendered_content.fget(self) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(self.template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", self.context_data) - return real_rendered_content.fget(self) - - SimpleTemplateResponse.rendered_content = rendered_content - - if DJANGO_VERSION < (1, 7): - return - import django.shortcuts - - real_render = django.shortcuts.render - - @functools.wraps(real_render) - @ensure_integration_enabled(DjangoIntegration, real_render) - def render( - request: "django.http.HttpRequest", - template_name: str, - context: "Optional[Dict[str, Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> "django.http.HttpResponse": - # Inject trace meta tags into template context - context = context or {} - if "sentry_trace_meta" not in context: - context["sentry_trace_meta"] = mark_safe( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_render(request, template_name, context, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", context) - return real_render(request, template_name, context, *args, **kwargs) - - django.shortcuts.render = render - - -def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": - if debug is None: - return None - - lineno = debug["line"] - filename = debug["name"] - if filename is None: - filename = "" - - pre_context = [] - post_context = [] - context_line = None - - for i, line in debug["source_lines"]: - if i < lineno: - pre_context.append(line) - elif i > lineno: - post_context.append(line) - else: - context_line = line - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - "in_app": True, - } - - -def _linebreak_iter(template_source: str) -> "Iterator[int]": - yield 0 - p = template_source.find("\n") - while p >= 0: - yield p + 1 - p = template_source.find("\n", p + 1) - - -def _get_template_frame_from_source( - source: "Tuple[Origin, Tuple[int, int]]", -) -> "Optional[Dict[str, Any]]": - if not source: - return None - - origin, (start, end) = source - filename = getattr(origin, "loadname", None) - if filename is None: - filename = "" - template_source = origin.reload() - lineno = None - upto = 0 - pre_context = [] - post_context = [] - context_line = None - - for num, next in enumerate(_linebreak_iter(template_source)): - line = template_source[upto:next] - if start >= upto and end <= next: - lineno = num - context_line = line - elif lineno is None: - pre_context.append(line) - else: - post_context.append(line) - - upto = next - - if context_line is None or lineno is None: - return None - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py deleted file mode 100644 index 192f0765e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/transactions.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Copied from raven-python. - -Despite being called "legacy" in some places this resolver is very much still -in use. -""" - -import re -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from re import Pattern - from typing import Dict, List, Optional, Tuple, Union - - from django.urls.resolvers import URLPattern, URLResolver - -from django import VERSION as DJANGO_VERSION - -if DJANGO_VERSION >= (2, 0): - from django.urls.resolvers import RoutePattern -else: - RoutePattern = None - -try: - from django.urls import get_resolver -except ImportError: - from django.core.urlresolvers import get_resolver - - -def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": - """Utility method for django's deprecated resolver.regex""" - try: - regex = resolver_or_pattern.regex - except AttributeError: - regex = resolver_or_pattern.pattern.regex - return regex - - -class RavenResolver: - _new_style_group_matcher = re.compile( - r"<(?:([^>:]+):)?([^>]+)>" - ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 - _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") - _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") - _non_named_group_matcher = re.compile(r"\([^\)]+\)") - # [foo|bar|baz] - _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") - _camel_re = re.compile(r"([A-Z]+)([a-z])") - - _cache: "Dict[URLPattern, str]" = {} - - def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: - r""" - Clean up urlpattern regexes into something readable by humans: - - From: - > "^(?P\w+)/athletes/(?P\w+)/$" - - To: - > "{sport_slug}/athletes/{athlete_slug}/" - """ - # "new-style" path patterns can be parsed directly without turning them - # into regexes first - if ( - RoutePattern is not None - and hasattr(pattern, "pattern") - and isinstance(pattern.pattern, RoutePattern) - ): - return self._new_style_group_matcher.sub( - lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) - ) - - result = get_regex(pattern).pattern - - # remove optional params - # TODO(dcramer): it'd be nice to change these into [%s] but it currently - # conflicts with the other rules because we're doing regexp matches - # rather than parsing tokens - result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) - - # handle named groups first - result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) - - # handle non-named groups - result = self._non_named_group_matcher.sub("{var}", result) - - # handle optional params - result = self._either_option_matcher.sub(lambda m: m.group(1), result) - - # clean up any outstanding regex-y characters. - result = ( - result.replace("^", "") - .replace("$", "") - .replace("?", "") - .replace("\\A", "") - .replace("\\Z", "") - .replace("//", "/") - .replace("\\", "") - ) - - return result - - def _resolve( - self, - resolver: "URLResolver", - path: str, - parents: "Optional[List[URLResolver]]" = None, - ) -> "Optional[str]": - match = get_regex(resolver).search(path) # Django < 2.0 - - if not match: - return None - - if parents is None: - parents = [resolver] - elif resolver not in parents: - parents = parents + [resolver] - - new_path = path[match.end() :] - for pattern in resolver.url_patterns: - # this is an include() - if not pattern.callback: - match_ = self._resolve(pattern, new_path, parents) - if match_: - return match_ - continue - elif not get_regex(pattern).search(new_path): - continue - - try: - return self._cache[pattern] - except KeyError: - pass - - prefix = "".join(self._simplify(p) for p in parents) - result = prefix + self._simplify(pattern) - if not result.startswith("/"): - result = "/" + result - self._cache[pattern] = result - return result - - return None - - def resolve( - self, - path: str, - urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, - ) -> "Optional[str]": - resolver = get_resolver(urlconf) - match = self._resolve(resolver, path) - return match - - -LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py deleted file mode 100644 index cf3012a75e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/django/views.py +++ /dev/null @@ -1,127 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -try: - from asyncio import iscoroutinefunction -except ImportError: - iscoroutinefunction = None # type: ignore - - -try: - from sentry_sdk.integrations.django.asgi import wrap_async_view -except (ImportError, SyntaxError): - wrap_async_view = None # type: ignore - - -def patch_views() -> None: - from django.core.handlers.base import BaseHandler - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - old_make_view_atomic = BaseHandler.make_view_atomic - old_render = SimpleTemplateResponse.render - - def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="serialize response", - attributes={ - "sentry.op": OP.VIEW_RESPONSE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_render(self) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RESPONSE_RENDER, - name="serialize response", - origin=DjangoIntegration.origin, - ): - return old_render(self) - - @functools.wraps(old_make_view_atomic) - def sentry_patched_make_view_atomic( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - callback = old_make_view_atomic(self, *args, **kwargs) - - # XXX: The wrapper function is created for every request. Find more - # efficient way to wrap views (or build a cache?) - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is not None: - is_async_view = ( - iscoroutinefunction is not None - and wrap_async_view is not None - and iscoroutinefunction(callback) - ) - if is_async_view: - sentry_wrapped_callback = wrap_async_view(callback) - else: - sentry_wrapped_callback = _wrap_sync_view(callback) - - else: - sentry_wrapped_callback = callback - - return sentry_wrapped_callback - - SimpleTemplateResponse.render = sentry_patched_render - BaseHandler.make_view_atomic = sentry_patched_make_view_atomic - - -def _wrap_sync_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - # set the active thread id to the handler thread for sync views - # this isn't necessary for async views since that runs on main - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return callback(request, *args, **kwargs) - - return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py deleted file mode 100644 index 310766ee3a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/dramatiq.py +++ /dev/null @@ -1,246 +0,0 @@ -import json -from typing import TypeVar - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - event_from_exception, -) - -R = TypeVar("R") - -try: - from dramatiq.broker import Broker - from dramatiq.errors import Retry - from dramatiq.message import Message - from dramatiq.middleware import Middleware, default_middleware -except ImportError: - raise DidNotEnable("Dramatiq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Union - - from sentry_sdk._types import Event, Hint - - -class DramatiqIntegration(Integration): - """ - Dramatiq integration for Sentry - - Please make sure that you call `sentry_sdk.init` *before* initializing - your broker, as it monkey patches `Broker.__init__`. - - This integration was originally developed and maintained - by https://github.com/jacobsvante and later donated to the Sentry - project. - """ - - identifier = "dramatiq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_dramatiq_broker() - - -def _patch_dramatiq_broker() -> None: - original_broker__init__ = Broker.__init__ - - def sentry_patched_broker__init__( - self: "Broker", *args: "Any", **kw: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - - try: - middleware = kw.pop("middleware") - except KeyError: - # Unfortunately Broker and StubBroker allows middleware to be - # passed in as positional arguments, whilst RabbitmqBroker and - # RedisBroker does not. - if len(args) == 1: - middleware = args[0] - args = () - else: - middleware = None - - if middleware is None: - middleware = list(m() for m in default_middleware) - else: - middleware = list(middleware) - - if integration is not None: - middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] - middleware.insert(0, SentryMiddleware()) - - kw["middleware"] = middleware - original_broker__init__(self, *args, **kw) - - Broker.__init__ = sentry_patched_broker__init__ - - -class SentryMiddleware(Middleware): # type: ignore[misc] - """ - A Dramatiq middleware that automatically captures and sends - exceptions to Sentry. - - This is automatically added to every instantiated broker via the - DramatiqIntegration. - """ - - SENTRY_HEADERS_NAME = "_sentry_headers" - - def before_enqueue( - self, broker: "Broker", message: "Message[R]", delay: int - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - message.options[self.SENTRY_HEADERS_NAME] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - - def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DramatiqIntegration) - if integration is None: - return - - message._scope_manager = sentry_sdk.isolation_scope() - scope = message._scope_manager.__enter__() - scope.clear_breadcrumbs() - scope.set_extra("dramatiq_message_id", message.message_id) - scope.add_event_processor(_make_message_event_processor(message, integration)) - - sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} - if "retries" in message.options: - # start new trace in case of retrying - sentry_headers = {} - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(sentry_headers) - span = sentry_sdk.traces.start_span( - name=message.actor_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_DRAMATIQ, - "sentry.origin": DramatiqIntegration.origin, - "sentry.span.source": SegmentSource.TASK.value, - }, - parent_span=None, - ) - message._sentry_span_ctx = span - else: - transaction = continue_trace( - sentry_headers, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - origin=DramatiqIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - sentry_sdk.start_transaction( - transaction, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - ) - transaction.__enter__() - message._sentry_span_ctx = transaction - - def after_process_message( - self, - broker: "Broker", - message: "Message[R]", - *, - result: "Optional[Any]" = None, - exception: "Optional[Exception]" = None, - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - actor = broker.get_actor(message.actor_name) - throws = message.options.get("throws") or actor.options.get("throws") - - scope_manager = message._scope_manager - span_ctx = getattr(message, "_sentry_span_ctx", None) - if span_ctx is None: - return None - - is_event_capture_required = ( - exception is not None - and not (throws and isinstance(exception, throws)) - and not isinstance(exception, Retry) - ) - if not is_event_capture_required: - # normal transaction finish - span_ctx.__exit__(None, None, None) - scope_manager.__exit__(None, None, None) - return - - event, hint = event_from_exception( - exception, # type: ignore[arg-type] - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": DramatiqIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - # transaction error - span_ctx.__exit__(type(exception), exception, None) - scope_manager.__exit__(type(exception), exception, None) - - after_skip_message = after_process_message - - -def _make_message_event_processor( - message: "Message[R]", integration: "DramatiqIntegration" -) -> "Callable[[Event, Hint], Optional[Event]]": - def inner(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - DramatiqMessageExtractor(message).extract_into_event(event) - - return event - - return inner - - -class DramatiqMessageExtractor: - def __init__(self, message: "Message[R]") -> None: - self.message_data = dict(message.asdict()) - - def content_length(self) -> int: - return len(json.dumps(self.message_data)) - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - contexts = event.setdefault("contexts", {}) - request_info = contexts.setdefault("dramatiq", {}) - request_info["type"] = "dramatiq" - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - if not request_body_within_bounds(client, self.content_length()): - data = AnnotatedValue.removed_because_over_size_limit() - else: - data = self.message_data - - request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py deleted file mode 100644 index 6bbc61000d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/excepthook.py +++ /dev/null @@ -1,76 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - - -class ExcepthookIntegration(Integration): - identifier = "excepthook" - - always_run = False - - def __init__(self, always_run: bool = False) -> None: - if not isinstance(always_run, bool): - raise ValueError( - "Invalid value for always_run: %s (must be type boolean)" - % (always_run,) - ) - self.always_run = always_run - - @staticmethod - def setup_once() -> None: - sys.excepthook = _make_excepthook(sys.excepthook) - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - if _should_send(integration.always_run): - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "excepthook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook - - -def _should_send(always_run: bool = False) -> bool: - if always_run: - return True - - if hasattr(sys, "ps1"): - # Disable the excepthook for interactive Python shells, otherwise - # every typo gets sent to Sentry. - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py deleted file mode 100644 index 4473fcc435..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/executing.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - - -class ExecutingIntegration(Integration): - identifier = "executing" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - frame = tb.tb_frame - source = Source.for_frame(frame) - sentry_frame["function"] = source.code_qualname(frame.f_code) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py deleted file mode 100644 index 7a595bcf2a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/falcon.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - - from sentry_sdk._types import Event, EventProcessor - -# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` -# and `falcon.API` to `falcon.App` - -try: - import falcon # type: ignore - from falcon import __version__ as FALCON_VERSION -except ImportError: - raise DidNotEnable("Falcon not installed") - -try: - import falcon.app_helpers # type: ignore - - falcon_helpers = falcon.app_helpers - falcon_app_class = falcon.App - FALCON3 = True -except ImportError: - import falcon.api_helpers # type: ignore - - falcon_helpers = falcon.api_helpers - falcon_app_class = falcon.API - FALCON3 = False - - -_FALCON_UNSET: "Optional[object]" = None -if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ - with capture_internal_exceptions(): - from falcon.request import ( # type: ignore[import-not-found, no-redef] - _UNSET as _FALCON_UNSET, - ) - - -class FalconRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, Any]": - return self.request.env - - def cookies(self) -> "Dict[str, Any]": - return self.request.cookies - - def form(self) -> None: - return None # No such concept in Falcon - - def files(self) -> None: - return None # No such concept in Falcon - - def raw_data(self) -> "Optional[str]": - # As request data can only be read once we won't make this available - # to Sentry. Just send back a dummy string in case there was a - # content length. - # TODO(jmagnusson): Figure out if there's a way to support this - content_length = self.content_length() - if content_length > 0: - return "[REQUEST_CONTAINING_RAW_DATA]" - else: - return None - - def json(self) -> "Optional[Dict[str, Any]]": - # fallback to cached_media = None if self.request._media is not available - cached_media = None - with capture_internal_exceptions(): - # self.request._media is the cached self.request.media - # value. It is only available if self.request.media - # has already been accessed. Therefore, reading - # self.request._media will not exhaust the raw request - # stream (self.request.bounded_stream) because it has - # already been read if self.request._media is set. - cached_media = self.request._media - - if cached_media is not _FALCON_UNSET: - return cached_media - - return None - - -class SentryFalconMiddleware: - """Captures exceptions in Falcon requests and send to Sentry""" - - def process_request( - self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return - - scope = sentry_sdk.get_isolation_scope() - scope._name = "falcon" - scope.add_event_processor(_make_request_event_processor(req, integration)) - - def process_resource( - self, req: "Any", resp: "Any", resource: "Any", params: "Any" - ) -> None: - """ - Sets the segment name and source as the route is resolved when this runs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FalconIntegration) - if integration is None or not has_span_streaming_enabled(client.options): - return - - name_for_style = { - "uri_template": req.uri_template, - "path": req.path, - } - name = name_for_style[integration.transaction_style] - source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] - sentry_sdk.set_transaction_name(name, source) - - -TRANSACTION_STYLE_VALUES = ("uri_template", "path") - - -class FalconIntegration(Integration): - identifier = "falcon" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "uri_template") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - version = parse_version(FALCON_VERSION) - _check_minimum_version(FalconIntegration, version) - - _patch_wsgi_app() - _patch_handle_exception() - _patch_prepare_middleware() - - -def _patch_wsgi_app() -> None: - original_wsgi_app = falcon_app_class.__call__ - - def sentry_patched_wsgi_app( - self: "falcon.API", env: "Any", start_response: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return original_wsgi_app(self, env, start_response) - - sentry_wrapped = SentryWsgiMiddleware( - lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), - span_origin=FalconIntegration.origin, - ) - - return sentry_wrapped(env, start_response) - - falcon_app_class.__call__ = sentry_patched_wsgi_app - - -def _patch_handle_exception() -> None: - original_handle_exception = falcon_app_class._handle_exception - - @ensure_integration_enabled(FalconIntegration, original_handle_exception) - def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": - # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception - # method signature from `(ex, req, resp, params)` to - # `(req, resp, ex, params)` - ex = response = None - with capture_internal_exceptions(): - ex = next(argument for argument in args if isinstance(argument, Exception)) - response = next( - argument for argument in args if isinstance(argument, falcon.Response) - ) - - was_handled = original_handle_exception(self, *args) - - if ex is None or response is None: - # Both ex and response should have a non-None value at this point; otherwise, - # there is an error with the SDK that will have been captured in the - # capture_internal_exceptions block above. - return was_handled - - if _exception_leads_to_http_5xx(ex, response): - event, hint = event_from_exception( - ex, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "falcon", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return was_handled - - falcon_app_class._handle_exception = sentry_patched_handle_exception - - -def _patch_prepare_middleware() -> None: - original_prepare_middleware = falcon_helpers.prepare_middleware - - def sentry_patched_prepare_middleware( - middleware: "Any" = None, - independent_middleware: "Any" = False, - asgi: bool = False, - ) -> "Any": - if asgi: - # We don't support ASGI Falcon apps, so we don't patch anything here - return original_prepare_middleware(middleware, independent_middleware, asgi) - - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is not None: - middleware = [SentryFalconMiddleware()] + (middleware or []) - - # We intentionally omit the asgi argument here, since the default is False anyways, - # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. - return original_prepare_middleware(middleware, independent_middleware) - - falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware - - -def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: - is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( - "5" - ) - is_unhandled_error = not isinstance( - ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) - ) - - # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response - # at the stage where we capture it is listed as 200, even though we would expect to see a 500 - # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to - # only perform this check on Falcon 3+, despite the risk that some handled errors might be - # reported to Sentry as unhandled on Falcon 2. - return (is_server_error or is_unhandled_error) and ( - not FALCON3 or _has_http_5xx_status(response) - ) - - -def _has_http_5xx_status(response: "falcon.Response") -> bool: - return response.status.startswith("5") - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "falcon.Request" -) -> None: - name_for_style = { - "uri_template": request.uri_template, - "path": request.path, - } - event["transaction"] = name_for_style[transaction_style] - event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} - - -def _make_request_event_processor( - req: "falcon.Request", integration: "FalconIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, req) - - with capture_internal_exceptions(): - FalconRequestExtractor(req).extract_into_event(event) - - return event - - return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py deleted file mode 100644 index c7b97c88b1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/fastapi.py +++ /dev/null @@ -1,201 +0,0 @@ -import sys -from copy import deepcopy -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import transaction_from_function - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Dict - - from sentry_sdk._types import Event - -try: - from sentry_sdk.integrations.starlette import ( - StarletteIntegration, - StarletteRequestExtractor, - _get_cached_request_body_attribute, - ) -except DidNotEnable: - raise DidNotEnable("Starlette is not installed") - -try: - import fastapi # type: ignore -except ImportError: - raise DidNotEnable("FastAPI is not installed") - - -_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -class FastApiIntegration(StarletteIntegration): - identifier = "fastapi" - - @staticmethod - def setup_once() -> None: - patch_get_request_handler() - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or "" - - elif transaction_style == "url": - route = request.scope.get("route") - - if route: - # FastAPI >= 0.137 stores the prefix-resolved path on an - # effective_route_context in scope["fastapi"], while - # scope["route"].path holds the unprefixed original. - # Prefer the effective context path when available. - effective_route_context = request.scope.get("fastapi", {}).get( - "effective_route_context" - ) - context_path = getattr(effective_route_context, "path", None) - - if context_path: - name = context_path - else: - path = getattr(route, "path", None) - if path is not None: - name = path - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name(name, source=source) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FastApiIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, Dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = FastApiIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_get_request_handler() -> None: - old_get_request_handler = fastapi.routing.get_request_handler - - def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": - dependant = kwargs.get("dependant") - if ( - dependant - and dependant.call is not None - and not iscoroutinefunction(dependant.call) - # FastAPI >= 0.137 calls get_request_handler() on every request - # (router-tree traversal) rather than once at registration. Guard - # against accumulating _sentry_call wrappers on the shared - # dependant object, which would cause a RecursionError after ~987 - # requests as the call chain grows past Python's recursion limit. - and not getattr(dependant.call, "_sentry_is_patched", False) - ): - old_call = dependant.call - - @wraps(old_call) - def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": - current_scope = sentry_sdk.get_current_scope() - - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_call(*args, **kwargs) - - _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] - dependant.call = _sentry_call - - old_app = old_get_request_handler(*args, **kwargs) - - async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_app, *args, **kwargs) - - return _sentry_app - - fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py deleted file mode 100644 index 1902091fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/flask.py +++ /dev/null @@ -1,281 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Union - - from werkzeug.datastructures import FileStorage, ImmutableMultiDict - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - - -try: - import flask_login # type: ignore -except ImportError: - flask_login = None - -try: - from flask import Flask, Request # type: ignore - from flask import request as flask_request - from flask.signals import ( - before_render_template, - got_request_exception, - request_started, - ) - from markupsafe import Markup -except ImportError: - raise DidNotEnable("Flask is not installed") - -try: - import blinker # noqa -except ImportError: - raise DidNotEnable("blinker is not installed") - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class FlaskIntegration(Integration): - identifier = "flask" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - try: - from quart import Quart # type: ignore - - if Flask == Quart: - # This is Quart masquerading as Flask, don't enable the Flask - # integration. See https://github.com/getsentry/sentry-python/issues/2709 - raise DidNotEnable( - "This is not a Flask app but rather Quart pretending to be Flask" - ) - except ImportError: - pass - - version = package_version("flask") - _check_minimum_version(FlaskIntegration, version) - - before_render_template.connect(_add_sentry_trace) - request_started.connect(_request_started) - got_request_exception.connect(_capture_exception) - - old_app = Flask.__call__ - - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return old_app(self, environ, start_response) - - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=FlaskIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - Flask.__call__ = sentry_patched_wsgi_app - - -def _add_sentry_trace( - sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" -) -> None: - if "sentry_trace" in context: - return - - scope = sentry_sdk.get_current_scope() - trace_meta = Markup(scope.trace_propagation_meta()) - context["sentry_trace"] = trace_meta # for backwards compatibility - context["sentry_trace_meta"] = trace_meta - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - scope.set_transaction_name( - name_for_style[transaction_style], - source=SOURCE_FOR_STYLE[transaction_style], - ) - except Exception: - pass - - -def _request_started(app: "Flask", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return - - request = flask_request._get_current_object() - - # Set the transaction name and source here, - # but rely on WSGI middleware to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - scope.set_user(user_properties) - - evt_processor = _make_request_event_processor(app, request, integration) - scope.add_event_processor(evt_processor) - - -class FlaskRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[Any, Any]": - return { - k: v[0] if isinstance(v, list) and len(v) == 1 else v - for k, v in self.request.cookies.items() - } - - def raw_data(self) -> bytes: - return self.request.get_data() - - def form(self) -> "ImmutableMultiDict[str, Any]": - return self.request.form - - def files(self) -> "ImmutableMultiDict[str, Any]": - return self.request.files - - def is_json(self) -> bool: - return self.request.is_json - - def json(self) -> "Any": - return self.request.get_json(silent=True) - - def size_of_file(self, file: "FileStorage") -> int: - return file.content_length - - -def _make_request_event_processor( - app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - FlaskRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _add_user_to_event(event) - - return event - - return inner - - -@ensure_integration_enabled(FlaskIntegration) -def _capture_exception( - sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "flask", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_flask_user_properties() -> "Dict[str, str]": - if flask_login is None: - return {} - - user = flask_login.current_user - if user is None: - return {} - - properties = {} - - try: - user_id = user.get_id() - if user_id is not None: - properties["id"] = user_id - except AttributeError: - # might happen if: - # - flask_login could not be imported - # - flask_login is not configured - # - no user is logged in - pass - - # The following attribute accesses are ineffective for the general - # Flask-Login case, because the User interface of Flask-Login does not - # care about anything but the ID. However, Flask-User (based on - # Flask-Login) documents a few optional extra attributes. - # - # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names - try: - if user.email is not None: - properties["email"] = user.email - except Exception: - pass - - try: - if user.username is not None: - properties["username"] = user.username - except Exception: - pass - - return properties - - -def _add_user_to_event(event: "Event") -> None: - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - user_info = event.setdefault("user", {}) - for key, value in user_properties.items(): - user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py deleted file mode 100644 index 91a62b3a81..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gcp.py +++ /dev/null @@ -1,286 +0,0 @@ -import functools -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -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 -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -# Constants -TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -def _wrap_func(func: "F") -> "F": - @functools.wraps(func) - def sentry_func( - functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(GcpIntegration) - if integration is None: - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = environ.get("FUNCTION_TIMEOUT_SEC") - if not configured_time: - logger.debug( - "The configured timeout could not be fetched from Cloud Functions configuration." - ) - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = int(configured_time) - - initial_time = datetime.now(timezone.utc) - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - gcp_event, configured_time, initial_time - ) - ) - scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) - timeout_thread = None - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = configured_time - TIMEOUT_WARNING_BUFFER - - timeout_thread = TimeoutThread( - waiting_time, - configured_time, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = {} - header_attributes: "dict[str, Any]" = {} - if hasattr(gcp_event, "headers"): - headers = gcp_event.headers - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - additional_attributes = {} - 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" - ) - - sampling_context = { - "gcp_env": { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - }, - "gcp_event": gcp_event, - } - - function_name = environ.get("FUNCTION_NAME", "") - - if environ.get("GCP_PROJECT"): - additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") - - if environ.get("FUNCTION_IDENTITY"): - additional_attributes["faas.identity"] = environ.get( - "FUNCTION_IDENTITY" - ) - - if environ.get("ENTRY_POINT"): - additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_GCP, - "sentry.origin": GcpIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.provider": CLOUD_PROVIDER.GCP, - "faas.name": function_name, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_GCP, - name=environ.get("FUNCTION_NAME", ""), - source=TransactionSource.COMPONENT, - origin=GcpIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return func(functionhandler, gcp_event, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "gcp", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - # Flush out the event queue - client.flush() - - return sentry_func # type: ignore - - -class GcpIntegration(Integration): - identifier = "gcp" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - import __main__ as gcp_functions - - if not hasattr(gcp_functions, "worker_v1"): - logger.warning( - "GcpIntegration currently supports only Python 3.7 runtime environment." - ) - return - - worker1 = gcp_functions.worker_v1 - - worker1.FunctionHandler.invoke_user_function = _wrap_func( - worker1.FunctionHandler.invoke_user_function - ) - - -def _make_request_event_processor( - gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - final_time = datetime.now(timezone.utc) - time_diff = final_time - initial_time - - execution_duration_in_millis = time_diff / timedelta(milliseconds=1) - - extra = event.setdefault("extra", {}) - extra["google cloud functions"] = { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - "execution_duration_in_millis": execution_duration_in_millis, - "configured_timeout_in_seconds": configured_timeout, - } - - extra["google cloud logs"] = { - "url": _get_google_cloud_logs_url(final_time), - } - - request = event.get("request", {}) - - request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) - - if hasattr(gcp_event, "method"): - request["method"] = gcp_event.method - - if hasattr(gcp_event, "query_string"): - request["query_string"] = gcp_event.query_string.decode( - "utf-8", errors="replace" - ) - - if hasattr(gcp_event, "headers"): - request["headers"] = _filter_headers(gcp_event.headers) - - if should_send_default_pii(): - if hasattr(gcp_event, "data"): - request["data"] = gcp_event.data - else: - if hasattr(gcp_event, "data"): - # Unfortunately couldn't find a way to get structured body from GCP - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - event["request"] = deepcopy(request) - - return event - - return event_processor - - -def _get_google_cloud_logs_url(final_time: "datetime") -> str: - """ - Generates a Google Cloud Logs console URL based on the environment variables - Arguments: - final_time {datetime} -- Final time - Returns: - str -- Google Cloud Logs Console URL to logs. - """ - hour_ago = final_time - timedelta(hours=1) - formatstring = "%Y-%m-%dT%H:%M:%SZ" - - url = ( - "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" - "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" - "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" - "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" - "&interval=PT1H&scrollTimestamp={timestamp_end}" - ).format( - project=environ.get("GCP_PROJECT"), - function_name=environ.get("FUNCTION_NAME"), - region=environ.get("FUNCTION_REGION"), - timestamp_end=final_time.strftime(formatstring), - timestamp_start=hour_ago.strftime(formatstring), - ) - - return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py deleted file mode 100644 index 4be0f479bc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gnu_backtrace.py +++ /dev/null @@ -1,96 +0,0 @@ -import re -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - -# function is everything between index at @ -# and then we match on the @ plus the hex val -FUNCTION_RE = r"[^@]+?" -HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" - -_FRAME_RE_PATTERN = r""" -^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ -""".format( - FUNCTION_RE=FUNCTION_RE, - HEX_ADDRESS=HEX_ADDRESS, -) - -FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) - - -class GnuBacktraceIntegration(Integration): - identifier = "gnu_backtrace" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - return _process_gnu_backtrace(event, hint) - - -def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception in values: - frames = exception.get("stacktrace", {}).get("frames", []) - if not frames: - continue - - msg = exception.get("value", None) - if not msg: - continue - - additional_frames = [] - new_msg = [] - - for line in msg.splitlines(): - match = FRAME_RE.match(line) - if match: - additional_frames.append( - ( - int(match.group("index")), - { - "package": match.group("package") or None, - "function": match.group("function") or None, - "platform": "native", - }, - ) - ) - else: - # Put garbage lines back into message, not sure what to do with them. - new_msg.append(line) - - if additional_frames: - additional_frames.sort(key=lambda x: -x[0]) - for _, frame in additional_frames: - frames.append(frame) - - new_msg.append("") - exception["value"] = "\n".join(new_msg) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py deleted file mode 100644 index 45652c3f71..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/__init__.py +++ /dev/null @@ -1,457 +0,0 @@ -from functools import wraps -from typing import ( - Any, - AsyncIterator, - Callable, - Iterator, - List, -) - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import SPANSTATUS -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.genai.models import AsyncModels, Models -except ImportError: - raise DidNotEnable("google-genai not installed") - - -from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN -from .streaming import ( - accumulate_streaming_response, - set_span_data_for_streaming_response, -) -from .utils import ( - _capture_exception, - prepare_embed_content_args, - prepare_generate_content_args, - set_span_data_for_embed_request, - set_span_data_for_embed_response, - set_span_data_for_request, - set_span_data_for_response, -) - - -class GoogleGenAIIntegration(Integration): - identifier = IDENTIFIER - origin = ORIGIN - - def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Patch sync methods - Models.generate_content = _wrap_generate_content(Models.generate_content) - Models.generate_content_stream = _wrap_generate_content_stream( - Models.generate_content_stream - ) - Models.embed_content = _wrap_embed_content(Models.embed_content) - - # Patch async methods - AsyncModels.generate_content = _wrap_async_generate_content( - AsyncModels.generate_content - ) - AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( - AsyncModels.generate_content_stream - ) - AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) - - -def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = f(self, *args, **kwargs) - - # Create wrapper iterator to accumulate responses - def new_iterator() -> "Iterator[Any]": - chunks: "List[Any]" = [] - try: - for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_generate_content_stream - - -def _wrap_async_generate_content_stream( - f: "Callable[..., Any]", -) -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = await f(self, *args, **kwargs) - - # Create wrapper async iterator to accumulate responses - async def new_async_iterator() -> "AsyncIterator[Any]": - chunks: "List[Any]" = [] - try: - async for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_async_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_async_generate_content_stream - - -def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_generate_content - - -def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_async_generate_content - - -def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_embed_content - - -def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_embed_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py deleted file mode 100644 index 5b53ebf0e2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/consts.py +++ /dev/null @@ -1,16 +0,0 @@ -GEN_AI_SYSTEM = "gcp.gemini" - -# Mapping of tool attributes to their descriptions -# These are all tools that are available in the Google GenAI API -TOOL_ATTRIBUTES_MAP = { - "google_search_retrieval": "Google Search retrieval tool", - "google_search": "Google Search tool", - "retrieval": "Retrieval tool", - "enterprise_web_search": "Enterprise web search tool", - "google_maps": "Google Maps tool", - "code_execution": "Code execution tool", - "computer_use": "Computer use tool", -} - -IDENTIFIER = "google_genai" -ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py deleted file mode 100644 index 8414ea4f21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/streaming.py +++ /dev/null @@ -1,172 +0,0 @@ -from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union - -from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import ( - safe_serialize, -) - -from .utils import ( - UsageData, - extract_contents_text, - extract_finish_reasons, - extract_tool_calls, - extract_usage_data, -) - -if TYPE_CHECKING: - from google.genai.types import GenerateContentResponse - - from sentry_sdk.tracing import Span - - -class AccumulatedResponse(TypedDict): - id: "Optional[str]" - model: "Optional[str]" - text: str - finish_reasons: "List[str]" - tool_calls: "List[dict[str, Any]]" - usage_metadata: "Optional[UsageData]" - - -def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": - return UsageData( - input_tokens=max(self["input_tokens"], other["input_tokens"]), - output_tokens=max(self["output_tokens"], other["output_tokens"]), - input_tokens_cached=max( - self["input_tokens_cached"], other["input_tokens_cached"] - ), - output_tokens_reasoning=max( - self["output_tokens_reasoning"], other["output_tokens_reasoning"] - ), - total_tokens=max(self["total_tokens"], other["total_tokens"]), - ) - - -def accumulate_streaming_response( - chunks: "List[GenerateContentResponse]", -) -> "AccumulatedResponse": - """Accumulate streaming chunks into a single response-like object.""" - accumulated_text = [] - finish_reasons = [] - tool_calls = [] - usage_data = None - response_id = None - model = None - - for chunk in chunks: - # Extract text and tool calls - if getattr(chunk, "candidates", None): - for candidate in getattr(chunk, "candidates", []): - if hasattr(candidate, "content") and getattr( - candidate.content, "parts", [] - ): - extracted_text = extract_contents_text(candidate.content) - if extracted_text: - accumulated_text.append(extracted_text) - - extracted_finish_reasons = extract_finish_reasons(chunk) - if extracted_finish_reasons: - finish_reasons.extend(extracted_finish_reasons) - - extracted_tool_calls = extract_tool_calls(chunk) - if extracted_tool_calls: - tool_calls.extend(extracted_tool_calls) - - # Use last possible chunk, in case of interruption, and - # gracefully handle missing intermediate tokens by taking maximum - # with previous token reporting. - chunk_usage_data = extract_usage_data(chunk) - usage_data = ( - chunk_usage_data - if usage_data is None - else element_wise_usage_max(usage_data, chunk_usage_data) - ) - - accumulated_response = AccumulatedResponse( - text="".join(accumulated_text), - finish_reasons=finish_reasons, - tool_calls=tool_calls, - usage_metadata=usage_data, - id=response_id, - model=model, - ) - - return accumulated_response - - -def set_span_data_for_streaming_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - accumulated_response: "AccumulatedResponse", -) -> None: - """Set span data for accumulated streaming response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if ( - should_send_default_pii() - and integration.include_prompts - and accumulated_response.get("text") - ): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TEXT, - safe_serialize([accumulated_response["text"]]), - ) - - if accumulated_response.get("finish_reasons"): - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - accumulated_response["finish_reasons"], - ) - - if accumulated_response.get("tool_calls"): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(accumulated_response["tool_calls"]), - ) - - response_id = accumulated_response.get("id") - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - - response_model = accumulated_response.get("model") - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - if accumulated_response["usage_metadata"] is None: - return - - if accumulated_response["usage_metadata"]["input_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - accumulated_response["usage_metadata"]["input_tokens"], - ) - - if accumulated_response["usage_metadata"]["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - accumulated_response["usage_metadata"]["input_tokens_cached"], - ) - - if accumulated_response["usage_metadata"]["output_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - accumulated_response["usage_metadata"]["output_tokens"], - ) - - if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - accumulated_response["usage_metadata"]["output_tokens_reasoning"], - ) - - if accumulated_response["usage_metadata"]["total_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - accumulated_response["usage_metadata"]["total_tokens"], - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py deleted file mode 100644 index 464a812680..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/google_genai/utils.py +++ /dev/null @@ -1,1118 +0,0 @@ -import copy -import inspect -import json -from functools import wraps -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - TypedDict, - Union, -) - -from google.genai.types import Content, GenerateContentConfig, Part, PartDict - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.utils import ( - get_modality_from_mime_type, - normalize_message_roles, - set_data_normalized, - transform_google_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - safe_serialize, -) - -from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP - -if TYPE_CHECKING: - from google.genai.types import ( - ContentListUnion, - ContentUnion, - ContentUnionDict, - EmbedContentResponse, - GenerateContentResponse, - Model, - Tool, - ) - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -_is_PIL_available = False -try: - from PIL import Image as PILImage # type: ignore[import-not-found] - - _is_PIL_available = True -except ImportError: - pass - -# Keys to use when checking to see if a dict provided by the user -# is Part-like (as opposed to a Content or multi-turn conversation entry). -_PART_DICT_KEYS = PartDict.__optional_keys__ - - -class UsageData(TypedDict): - """Structure for token usage data.""" - - input_tokens: int - input_tokens_cached: int - output_tokens: int - output_tokens_reasoning: int - total_tokens: int - - -def extract_usage_data( - response: "Union[GenerateContentResponse, dict[str, Any]]", -) -> "UsageData": - """Extract usage data from response into a structured format. - - Args: - response: The GenerateContentResponse object or dictionary containing usage metadata - - Returns: - UsageData: Dictionary with input_tokens, input_tokens_cached, - output_tokens, and output_tokens_reasoning fields - """ - usage_data = UsageData( - input_tokens=0, - input_tokens_cached=0, - output_tokens=0, - output_tokens_reasoning=0, - total_tokens=0, - ) - - # Handle dictionary response (from streaming) - if isinstance(response, dict): - usage = response.get("usage_metadata", {}) - if not usage: - return usage_data - - prompt_tokens = usage.get("prompt_token_count", 0) or 0 - tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - cached_tokens = usage.get("cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - candidates_tokens = usage.get("candidates_token_count", 0) or 0 - # python-genai reports output and reasoning tokens separately - # reasoning should be sub-category of output tokens - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = usage.get("total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - if not hasattr(response, "usage_metadata"): - return usage_data - - usage = response.usage_metadata - - # Input tokens include both prompt and tool use prompt tokens - prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 - tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - # Cached input tokens - cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - # Reasoning tokens - reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - # output_tokens = candidates_tokens + reasoning_tokens - # google-genai reports output and reasoning tokens separately - candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = getattr(usage, "total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - -def _capture_exception(exc: "Any") -> None: - """Capture exception with Google GenAI mechanism.""" - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "google_genai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def get_model_name(model: "Union[str, Model]") -> str: - """Extract model name from model parameter.""" - if isinstance(model, str): - return model - # Handle case where model might be an object with a name attribute - if hasattr(model, "name"): - return str(model.name) - return str(model) - - -def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": - """Extract messages from contents parameter which can have various formats. - - Returns a list of message dictionaries in the format: - - System: {"role": "system", "content": "string"} - - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} - """ - if contents is None: - return [] - - messages = [] - - # Handle string case - if isinstance(contents, str): - return [{"role": "user", "content": contents}] - - # Handle list case - if isinstance(contents, list): - if contents and all(_is_part_like(item) for item in contents): - # All items are parts — merge into a single multi-part user message - content_parts = [] - for item in contents: - part = _extract_part_from_item(item) - if part is not None: - content_parts.append(part) - - return [{"role": "user", "content": content_parts}] - else: - # Multi-turn conversation or mixed content types - for item in contents: - item_messages = extract_contents_messages(item) - messages.extend(item_messages) - return messages - - # Handle dictionary case (ContentDict) - if isinstance(contents, dict): - role = contents.get("role", "user") - parts = contents.get("parts") - - if parts: - content_parts = [] - tool_messages = [] - - for part in parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - # Tool message - add separately - tool_messages.append(part_result) - else: - # Regular content part - content_parts.append(part_result) - - # Add main message if we have content parts - if content_parts: - # Normalize role: "model" -> "assistant" - normalized_role = "assistant" if role == "model" else role or "user" - messages.append({"role": normalized_role, "content": content_parts}) - - # Add tool messages - messages.extend(tool_messages) - elif "text" in contents: - messages.append( - { - "role": role, - "content": [{"text": contents["text"], "type": "text"}], - } - ) - elif "inline_data" in contents: - # The "data" will always be bytes (or bytes within a string), - # so if this is present, it's safe to automatically substitute with the placeholder - messages.append( - { - "inline_data": { - "mime_type": contents["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - ) - - return messages - - # Handle Content object - if hasattr(contents, "parts") and contents.parts: - role = getattr(contents, "role", None) or "user" - content_parts = [] - tool_messages = [] - - for part in contents.parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - tool_messages.append(part_result) - else: - content_parts.append(part_result) - - if content_parts: - normalized_role = "assistant" if role == "model" else role - messages.append({"role": normalized_role, "content": content_parts}) - - messages.extend(tool_messages) - return messages - - # Handle Part object directly - part_result = _extract_part_content(contents) - if part_result: - if isinstance(part_result, dict) and part_result.get("role") == "tool": - return [part_result] - else: - return [{"role": "user", "content": [part_result]}] - - # Handle PIL.Image.Image - if _is_PIL_available and isinstance(contents, PILImage.Image): - blob_part = _extract_pil_image(contents) - if blob_part: - return [{"role": "user", "content": [blob_part]}] - - # Handle File object - if hasattr(contents, "uri") and hasattr(contents, "mime_type"): - # File object - file_uri = getattr(contents, "uri", None) - mime_type = getattr(contents, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing - if file_uri is not None: - # Default to empty string if mime_type is None - if mime_type is None: - mime_type = "" - - blob_part = { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - return [{"role": "user", "content": [blob_part]}] - - # Handle direct text attribute - if hasattr(contents, "text") and contents.text: - return [ - {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} - ] - - return [] - - -def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": - """Extract content from a Part object or dict. - - Returns: - - dict for content part (text/blob) or tool message - - None if part should be skipped - """ - if part is None: - return None - - # Handle dict Part - if isinstance(part, dict): - # Check for function_response first (tool message) - if "function_response" in part: - return _extract_tool_message_from_part(part) - - if part.get("text"): - return {"text": part["text"], "type": "text"} - - # Try using Google-specific transform for dict formats (inline_data, file_data) - result = transform_google_content_part(part) - if result is not None: - # For inline_data with bytes data, substitute the content - if "inline_data" in part: - # inline_data.data will always be bytes, or a string containing base64-encoded bytes, - # so can automatically substitute without further checks - result["content"] = BLOB_DATA_SUBSTITUTE - return result - - return None - - # Handle Part object - # Check for function_response (tool message) - if hasattr(part, "function_response") and part.function_response: - return _extract_tool_message_from_part(part) - - # Handle text - if hasattr(part, "text") and part.text: - return {"text": part.text, "type": "text"} - - # Handle file_data - if hasattr(part, "file_data") and part.file_data: - file_data = part.file_data - file_uri = getattr(file_data, "file_uri", None) - mime_type = getattr(file_data, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) - if file_uri is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - # Handle inline_data - if hasattr(part, "inline_data") and part.inline_data: - inline_data = part.inline_data - data = getattr(inline_data, "data", None) - mime_type = getattr(inline_data, "mime_type", None) - # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) - if data is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - return None - - -def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": - """Extract tool message from a Part with function_response. - - Returns: - {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} - or None if not a valid tool message - """ - function_response = None - - if isinstance(part, dict): - function_response = part.get("function_response") - elif hasattr(part, "function_response"): - function_response = part.function_response - - if not function_response: - return None - - # Extract fields from function_response - tool_call_id = None - tool_name = None - output = None - - if isinstance(function_response, dict): - tool_call_id = function_response.get("id") - tool_name = function_response.get("name") - response_dict = function_response.get("response", {}) - # Prefer "output" key if present, otherwise use entire response - output = response_dict.get("output", response_dict) - else: - # FunctionResponse object - tool_call_id = getattr(function_response, "id", None) - tool_name = getattr(function_response, "name", None) - response_obj = getattr(function_response, "response", None) - if response_obj is None: - response_obj = {} - if isinstance(response_obj, dict): - output = response_obj.get("output", response_obj) - else: - output = response_obj - - if not tool_name: - return None - - return { - "role": "tool", - "content": { - "toolCallId": str(tool_call_id) if tool_call_id else None, - "toolName": str(tool_name), - "output": safe_serialize(output) if output is not None else None, - }, - } - - -def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": - """Extract blob part from PIL.Image.Image.""" - if not _is_PIL_available or not isinstance(image, PILImage.Image): - return None - - # Get format, default to JPEG - format_str = image.format or "JPEG" - suffix = format_str.lower() - mime_type = f"image/{suffix}" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _is_part_like(item: "Any") -> bool: - """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" - if isinstance(item, (str, Part)): - return True - if isinstance(item, (list, Content)): - return False - if isinstance(item, dict): - if "role" in item or "parts" in item: - return False - # Part objects that came in as plain dicts - return bool(_PART_DICT_KEYS & item.keys()) - # File objects - if hasattr(item, "uri"): - return True - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return True - return False - - -def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": - """Convert a single part-like item to a content part dict.""" - if isinstance(item, str): - return {"text": item, "type": "text"} - - # Handle bare inline_data dicts directly to preserve the raw format - if isinstance(item, dict) and "inline_data" in item: - return { - "inline_data": { - "mime_type": item["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - - # For other dicts and Part objects, use existing _extract_part_content - result = _extract_part_content(item) - if result is not None: - return result - - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return _extract_pil_image(item) - - # File objects - if hasattr(item, "uri") and hasattr(item, "mime_type"): - file_uri = getattr(item, "uri", None) - mime_type = getattr(item, "mime_type", None) or "" - if file_uri is not None: - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - return None - - -def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": - """Extract text from contents parameter which can have various formats. - - This is a compatibility function that extracts text from messages. - For new code, use extract_contents_messages instead. - """ - messages = extract_contents_messages(contents) - if not messages: - return None - - texts = [] - for message in messages: - content = message.get("content") - if isinstance(content, str): - texts.append(content) - elif isinstance(content, list): - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - texts.append(part.get("text", "")) - - return " ".join(texts) if texts else None - - -def _format_tools_for_span( - tools: "Iterable[Tool | Callable[..., Any]]", -) -> "Optional[List[dict[str, Any]]]": - """Format tools parameter for span data.""" - formatted_tools = [] - for tool in tools: - if callable(tool): - # Handle callable functions passed directly - formatted_tools.append( - { - "name": getattr(tool, "__name__", "unknown"), - "description": getattr(tool, "__doc__", None), - } - ) - elif ( - hasattr(tool, "function_declarations") - and tool.function_declarations is not None - ): - # Tool object with function declarations - for func_decl in tool.function_declarations: - formatted_tools.append( - { - "name": getattr(func_decl, "name", None), - "description": getattr(func_decl, "description", None), - } - ) - else: - # Check for predefined tool attributes - each of these tools - # is an attribute of the tool object, by default set to None - for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): - if getattr(tool, attr_name, None): - formatted_tools.append( - { - "name": attr_name, - "description": description, - } - ) - break - - return formatted_tools if formatted_tools else None - - -def extract_tool_calls( - response: "GenerateContentResponse", -) -> "Optional[List[dict[str, Any]]]": - """Extract tool/function calls from response candidates and automatic function calling history.""" - - tool_calls = [] - - # Extract from candidates, sometimes tool calls are nested under the content.parts object - if getattr(response, "candidates", []): - for candidate in response.candidates: - if not hasattr(candidate, "content") or not getattr( - candidate.content, "parts", [] - ): - continue - - for part in candidate.content.parts: - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if getattr(function_call, "args", None): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - # Extract from automatic_function_calling_history - # This is the history of tool calls made by the model - if getattr(response, "automatic_function_calling_history", None): - for content in response.automatic_function_calling_history: - if not getattr(content, "parts", None): - continue - - for part in getattr(content, "parts", []): - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if hasattr(function_call, "args"): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - return tool_calls if tool_calls else None - - -def _capture_tool_input( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" -) -> "dict[str, Any]": - """Capture tool input from args and kwargs.""" - tool_input = kwargs.copy() if kwargs else {} - - # If we have positional args, try to map them to the function signature - if args: - try: - sig = inspect.signature(tool) - param_names = list(sig.parameters.keys()) - for i, arg in enumerate(args): - if i < len(param_names): - tool_input[param_names[i]] = arg - except Exception: - # Fallback if we can't get the signature - tool_input["args"] = args - - return tool_input - - -def _create_tool_span( - tool_name: str, tool_doc: "Optional[str]" -) -> "Union[Span, StreamedSpan]": - """Create a span for tool execution.""" - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - if tool_doc: - span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=ORIGIN, - ) - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - if tool_doc: - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - -def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": - """Wrap a tool to emit execute_tool spans when called.""" - if not callable(tool): - # Not a callable function, return as-is (predefined tools) - return tool - - tool_name = getattr(tool, "__name__", "unknown") - tool_doc = tool.__doc__ - - if inspect.iscoroutinefunction(tool): - # Async function - @wraps(tool) - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = await tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return async_wrapped - else: - # Sync function - @wraps(tool) - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return sync_wrapped - - -def wrapped_config_with_tools( - config: "GenerateContentConfig", -) -> "GenerateContentConfig": - """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as - callable functions as a part of the config object.""" - - if not config or not getattr(config, "tools", None): - return config - - result = copy.copy(config) - result.tools = [wrapped_tool(tool) for tool in config.tools] - - return result - - -def _extract_response_text( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract text from response candidates.""" - - if not response or not getattr(response, "candidates", []): - return None - - texts = [] - for candidate in response.candidates: - if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): - continue - - if candidate.content is None or candidate.content.parts is None: - continue - - for part in candidate.content.parts: - if getattr(part, "text", None): - texts.append(part.text) - - return texts if texts else None - - -def extract_finish_reasons( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract finish reasons from response candidates.""" - if not response or not getattr(response, "candidates", []): - return None - - finish_reasons = [] - for candidate in response.candidates: - if getattr(candidate, "finish_reason", None): - # Convert enum value to string if necessary - reason = str(candidate.finish_reason) - # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") - if "." in reason: - reason = reason.split(".")[-1] - finish_reasons.append(reason) - - return finish_reasons if finish_reasons else None - - -def _transform_system_instruction_one_level( - system_instructions: "Union[ContentUnionDict, ContentUnion]", - can_be_content: bool, -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, str): - return [{"type": "text", "content": system_instructions}] - - if isinstance(system_instructions, Part) and system_instructions.text: - return [{"type": "text", "content": system_instructions.text}] - - if can_be_content and isinstance(system_instructions, Content): - if isinstance(system_instructions.parts, list): - for part in system_instructions.parts: - if isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - return text_parts - - if isinstance(system_instructions, dict) and system_instructions.get("text"): - return [{"type": "text", "content": system_instructions["text"]}] - - elif can_be_content and isinstance(system_instructions, dict): - parts = system_instructions.get("parts", []) - for part in parts: - if isinstance(part, Part) and isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - elif isinstance(part, dict) and isinstance(part.get("text"), str): - text_parts.append({"type": "text", "content": part["text"]}) - return text_parts - - return text_parts - - -def _transform_system_instructions( - system_instructions: "Union[ContentUnionDict, ContentUnion]", -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, list): - text_parts = list( - chain.from_iterable( - _transform_system_instruction_one_level( - instructions, can_be_content=False - ) - for instructions in system_instructions - ) - ) - - return text_parts - - return _transform_system_instruction_one_level( - system_instructions, can_be_content=True - ) - - -def set_span_data_for_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - model: str, - contents: "ContentListUnion", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for the request.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if kwargs.get("stream", False): - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - config: "Optional[GenerateContentConfig]" = kwargs.get("config") - - # Set input messages/prompts if PII is allowed - if should_send_default_pii() and integration.include_prompts: - messages = [] - - # Add system instruction if present - system_instructions = None - if config and hasattr(config, "system_instruction"): - system_instructions = config.system_instruction - elif isinstance(config, dict) and "system_instruction" in config: - system_instructions = config.get("system_instruction") - - if system_instructions is not None: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - # Extract messages from contents - contents_messages = extract_contents_messages(contents) - messages.extend(contents_messages) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Extract parameters directly from config (not nested under generation_config) - for param, span_key in [ - ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), - ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), - ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), - ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), - ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), - ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), - ("seed", SPANDATA.GEN_AI_REQUEST_SEED), - ]: - if hasattr(config, param): - value = getattr(config, param) - if value is not None: - set_on_span(span_key, value) - - # Set tools if available - if config is not None and hasattr(config, "tools"): - tools = config.tools - if tools: - formatted_tools = _format_tools_for_span(tools) - if formatted_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - formatted_tools, - unpack=False, - ) - - -def set_span_data_for_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "GenerateContentResponse", -) -> None: - """Set span data for the response.""" - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if should_send_default_pii() and integration.include_prompts: - response_texts = _extract_response_text(response) - if response_texts: - # Format as JSON string array as per documentation - set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) - - tool_calls = extract_tool_calls(response) - if tool_calls: - # Tool calls should be JSON serialized - set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) - - finish_reasons = extract_finish_reasons(response) - if finish_reasons: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons - ) - - if getattr(response, "response_id", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) - - if getattr(response, "model_version", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) - - usage_data = extract_usage_data(response) - - if usage_data["input_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) - - if usage_data["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage_data["input_tokens_cached"], - ) - - if usage_data["output_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) - - if usage_data["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage_data["output_tokens_reasoning"], - ) - - if usage_data["total_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) - - -def prepare_generate_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[Any, Any, str]": - """Extract and prepare common arguments for generate_content methods.""" - model = args[0] if args else kwargs.get("model", "unknown") - contents = args[1] if len(args) > 1 else kwargs.get("contents") - model_name = get_model_name(model) - - config = kwargs.get("config") - wrapped_config = wrapped_config_with_tools(config) - if wrapped_config is not config: - kwargs["config"] = wrapped_config - - return model, contents, model_name - - -def prepare_embed_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[str, Any]": - """Extract and prepare common arguments for embed_content methods. - - Returns: - tuple: (model_name, contents) - """ - model = kwargs.get("model", "unknown") - contents = kwargs.get("contents") - model_name = get_model_name(model) - - return model_name, contents - - -def set_span_data_for_embed_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - contents: "Any", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for embedding request.""" - # Include input contents if PII is allowed - if should_send_default_pii() and integration.include_prompts: - if contents: - # For embeddings, contents is typically a list of strings/texts - input_texts = [] - - # Handle various content formats - if isinstance(contents, str): - input_texts = [contents] - elif isinstance(contents, list): - for item in contents: - text = extract_contents_text(item) - if text: - input_texts.append(text) - else: - text = extract_contents_text(contents) - if text: - input_texts = [text] - - if input_texts: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - input_texts, - unpack=False, - ) - - -def set_span_data_for_embed_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "EmbedContentResponse", -) -> None: - """Set span data for embedding response.""" - if not response: - return - - # Extract token counts from embeddings statistics (Vertex AI only) - # Each embedding has its own statistics with token_count - if hasattr(response, "embeddings") and response.embeddings: - total_tokens = 0 - - for embedding in response.embeddings: - if hasattr(embedding, "statistics") and embedding.statistics: - token_count = getattr(embedding.statistics, "token_count", None) - if token_count is not None: - total_tokens += int(token_count) - - # Set token count if we found any - if total_tokens > 0: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) - else: - span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py deleted file mode 100644 index dcb60e9561..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/gql.py +++ /dev/null @@ -1,173 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -try: - import gql # type: ignore[import-not-found] - from gql.transport import ( # type: ignore[import-not-found] - AsyncTransport, - Transport, - ) - from gql.transport.exceptions import ( # type: ignore[import-not-found] - TransportQueryError, - ) - from graphql import ( - DocumentNode, - VariableDefinitionNode, - get_operation_ast, - print_ast, - ) - - try: - # gql 4.0+ - from gql import GraphQLRequest - except ImportError: - GraphQLRequest = None - -except ImportError: - raise DidNotEnable("gql is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Tuple, Union - - from sentry_sdk._types import Event, EventProcessor - - EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] - - -class GQLIntegration(Integration): - identifier = "gql" - - @staticmethod - def setup_once() -> None: - gql_version = parse_version(gql.__version__) - _check_minimum_version(GQLIntegration, gql_version) - - _patch_execute() - - -def _data_from_document(document: "DocumentNode") -> "EventDataType": - try: - operation_ast = get_operation_ast(document) - data: "EventDataType" = {"query": print_ast(document)} - - if operation_ast is not None: - data["variables"] = operation_ast.variable_definitions - if operation_ast.name is not None: - data["operationName"] = operation_ast.name.value - - return data - except (AttributeError, TypeError): - return dict() - - -def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: - """ - The RequestsHTTPTransport allows defining the HTTP method; all - other transports use POST. - """ - try: - return transport.method - except AttributeError: - return "POST" - - -def _request_info_from_transport( - transport: "Union[Transport, AsyncTransport, None]", -) -> "Dict[str, str]": - if transport is None: - return {} - - request_info = { - "method": _transport_method(transport), - } - - try: - request_info["url"] = transport.url - except AttributeError: - pass - - return request_info - - -def _patch_execute() -> None: - real_execute = gql.Client.execute - - # Maintain signature for backwards compatibility. - # gql.Client.execute() accepts a positional-only "request" - # parameter with version 4.0.0. - @ensure_integration_enabled(GQLIntegration, real_execute) - def sentry_patched_execute( - self: "gql.Client", - document: "DocumentNode", - *args: "Any", - **kwargs: "Any", - ) -> "Any": - scope = sentry_sdk.get_isolation_scope() - # document is a gql.GraphQLRequest with gql v4.0.0. - scope.add_event_processor(_make_gql_event_processor(self, document)) - - try: - # document is a gql.GraphQLRequest with gql v4.0.0. - return real_execute(self, document, *args, **kwargs) - except TransportQueryError as e: - event, hint = event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "gql", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint) - raise e - - gql.Client.execute = sentry_patched_execute - - -def _make_gql_event_processor( - client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" -) -> "EventProcessor": - def processor(event: "Event", hint: "dict[str, Any]") -> "Event": - try: - errors = hint["exc_info"][1].errors - except (AttributeError, KeyError): - errors = None - - request = event.setdefault("request", {}) - request.update( - { - "api_target": "graphql", - **_request_info_from_transport(client.transport), - } - ) - - if should_send_default_pii(): - if GraphQLRequest is not None and isinstance( - document_or_request, GraphQLRequest - ): - # In v4.0.0, gql moved to using GraphQLRequest instead of - # DocumentNode in execute - # https://github.com/graphql-python/gql/pull/556 - document = document_or_request.document - else: - document = document_or_request - - request["data"] = _data_from_document(document) - contexts = event.setdefault("contexts", {}) - response = contexts.setdefault("response", {}) - response.update( - { - "data": {"errors": errors}, - "type": response, - } - ) - - return event - - return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py deleted file mode 100644 index 4938a5c0f3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/graphene.py +++ /dev/null @@ -1,181 +0,0 @@ -from contextlib import contextmanager - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from graphene.types import schema as graphene_schema # type: ignore -except ImportError: - raise DidNotEnable("graphene is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Generator - from typing import Any, Dict, Union - - from graphene.language.source import Source # type: ignore - from graphql.execution import ExecutionResult - from graphql.type import GraphQLSchema - - from sentry_sdk._types import Event - - -class GrapheneIntegration(Integration): - identifier = "graphene" - - @staticmethod - def setup_once() -> None: - version = package_version("graphene") - _check_minimum_version(GrapheneIntegration, version) - - _patch_graphql() - - -def _patch_graphql() -> None: - old_graphql_sync = graphene_schema.graphql_sync - old_graphql_async = graphene_schema.graphql - - @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) - def _sentry_patched_graphql_sync( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = old_graphql_sync(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - async def _sentry_patched_graphql_async( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) - if integration is None: - return await old_graphql_async(schema, source, *args, **kwargs) - - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = await old_graphql_async(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - graphene_schema.graphql_sync = _sentry_patched_graphql_sync - graphene_schema.graphql = _sentry_patched_graphql_async - - -def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - if should_send_default_pii(): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - -@contextmanager -def graphql_span( - schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" -) -> "Generator[None, None, None]": - operation_name = kwargs.get("operation_name") or "" - - operation_type = "query" - op = OP.GRAPHQL_QUERY - if source.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif source.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - sentry_sdk.add_breadcrumb( - crumb={ - "data": { - "operation_name": operation_name, - "operation_type": operation_type, - }, - "category": "graphql.operation", - }, - ) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - additional_attributes = {} - if should_send_default_pii(): - additional_attributes["graphql.document"] = source - - _graphql_span = sentry_sdk.traces.start_span( - name=operation_name, - attributes={ - "sentry.op": op, - "graphql.operation.name": operation_name, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) - - if should_send_default_pii(): - _graphql_span.set_data("graphql.document", source) - _graphql_span.set_data("graphql.operation.name", operation_name) - _graphql_span.set_data("graphql.operation.type", operation_type) - - _graphql_span.__enter__() - - try: - yield - finally: - if is_span_streaming_enabled: - _graphql_span.end() # type: ignore - else: - _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py deleted file mode 100644 index bf74ff1351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/__init__.py +++ /dev/null @@ -1,188 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any, Optional, Sequence - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .client import ClientInterceptor -from .server import ServerInterceptor - -try: - import grpc - from grpc import Channel, Server, intercept_channel - from grpc.aio import Channel as AsyncChannel - from grpc.aio import Server as AsyncServer - - from .aio.client import ( - SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, - ) - from .aio.client import ( - SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, - ) - from .aio.server import ServerInterceptor as AsyncServerInterceptor -except ImportError: - raise DidNotEnable("grpcio is not installed.") - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from typing import Callable, ParamSpec -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - -P = ParamSpec("P") - -GRPC_VERSION = parse_version(grpc.__version__) - - -def _is_channel_intercepted(channel: "Channel") -> bool: - interceptor = getattr(channel, "_interceptor", None) - while interceptor is not None: - if isinstance(interceptor, ClientInterceptor): - return True - - inner_channel = getattr(channel, "_channel", None) - if inner_channel is None: - return False - - channel = inner_channel - interceptor = getattr(channel, "_interceptor", None) - - return False - - -def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - "Wrapper for synchronous secure and insecure channel." - - @wraps(func) - def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": - channel = func(*args, **kwargs) - if not _is_channel_intercepted(channel): - return intercept_channel(channel, ClientInterceptor()) - else: - return channel - - return patched_channel - - -def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - @wraps(func) - def patched_intercept_channel( - channel: "Channel", *interceptors: "grpc.ServerInterceptor" - ) -> "Channel": - if _is_channel_intercepted(channel): - interceptors = tuple( - [ - interceptor - for interceptor in interceptors - if not isinstance(interceptor, ClientInterceptor) - ] - ) - else: - interceptors = interceptors - return intercept_channel(channel, *interceptors) - - return patched_intercept_channel # type: ignore - - -def _wrap_channel_async( - func: "Callable[P, AsyncChannel]", -) -> "Callable[P, AsyncChannel]": - "Wrapper for asynchronous secure and insecure channel." - - @wraps(func) - def patched_channel( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Channel": - sentry_interceptors = [ - AsyncUnaryUnaryClientInterceptor(), - AsyncUnaryStreamClientIntercetor(), - ] - interceptors = [*sentry_interceptors, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_channel # type: ignore - - -def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": - """Wrapper for synchronous server.""" - - @wraps(func) - def patched_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - interceptors = [ - interceptor - for interceptor in interceptors or [] - if not isinstance(interceptor, ServerInterceptor) - ] - server_interceptor = ServerInterceptor() - interceptors = [server_interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_server # type: ignore - - -def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": - """Wrapper for asynchronous server.""" - - @wraps(func) - def patched_aio_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - server_interceptor = AsyncServerInterceptor() - interceptors = [ - server_interceptor, - *(interceptors or []), - ] - - try: - # We prefer interceptors as a list because of compatibility with - # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 - # However, prior to grpc 1.42.0, only tuples were accepted, so we - # have no choice there. - if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): - interceptors = tuple(interceptors) - except Exception: - pass - - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_aio_server # type: ignore - - -class GRPCIntegration(Integration): - identifier = "grpc" - - @staticmethod - def setup_once() -> None: - import grpc - - grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) - grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) - grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) - - grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) - grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) - - grpc.server = _wrap_sync_server(grpc.server) - grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py deleted file mode 100644 index 4d21815254..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .client import ClientInterceptor -from .server import ServerInterceptor - -__all__ = [ - "ClientInterceptor", - "ServerInterceptor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py deleted file mode 100644 index d07b7f19be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/client.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import Any, AsyncIterable, Callable, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.protobuf.message import Message - from grpc.aio import ( - ClientCallDetails, - Metadata, - UnaryStreamCall, - UnaryStreamClientInterceptor, - UnaryUnaryCall, - UnaryUnaryClientInterceptor, - ) -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor: - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - if client_call_details.metadata is None: - client_call_details = client_call_details._replace(metadata=Metadata()) - elif not isinstance(client_call_details.metadata, Metadata): - # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 - # See https://github.com/grpc/grpc/issues/34298. - client_call_details = client_call_details._replace( - metadata=Metadata.from_tuple(client_call_details.metadata) - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - client_call_details.metadata.add(key, value) - return client_call_details - - -class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore - async def intercept_unary_unary( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[UnaryUnaryCall, Message]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_data("code", status_code.name) - - return response - - -class SentryUnaryStreamClientInterceptor( - ClientInterceptor, - UnaryStreamClientInterceptor, # type: ignore -): - async def intercept_unary_stream( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - # status_code = await response.code() - # span.set_data("code", status_code) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py deleted file mode 100644 index 010337e98c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/aio/server.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from typing import Any, Optional - - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler - from grpc.aio import AbortError, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Callable[[ServicerContext], str] | None" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - async def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", - handler_call_details: "HandlerCallDetails", - ) -> "Optional[Awaitable[RpcMethodHandler]]": - handler = await continuation(handler_call_details) - if handler is None: - return None - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - if not handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.unary_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - with sentry_sdk.isolation_scope(): - name = ( - custom_find_name(context) if custom_find_name else method_name - ) - if not name: - return await handler(request, context) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - # What if the headers are empty? - sentry_sdk.traces.continue_trace( - dict(context.invocation_metadata()) - ) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - else: - # What if the headers are empty? - transaction = sentry_sdk.continue_trace( - dict(context.invocation_metadata()), - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - - elif not handler.request_streaming and handler.response_streaming: - handler_factory = grpc.unary_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.unary_stream(request, context): - yield r - - elif handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.stream_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - response = handler.stream_unary(request, context) - return await response - - elif handler.request_streaming and handler.response_streaming: - handler_factory = grpc.stream_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.stream_stream(request, context): - yield r - - return handler_factory( - wrapped, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py deleted file mode 100644 index 5384a0a78f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/client.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Iterator, Union - -try: - import grpc - from google.protobuf.message import Message - from grpc import Call, ClientCallDetails - from grpc._interceptor import _UnaryOutcome - from grpc.aio._interceptor import UnaryStreamCall -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor( - grpc.UnaryUnaryClientInterceptor, # type: ignore - grpc.UnaryStreamClientInterceptor, # type: ignore -): - def intercept_unary_unary( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "_UnaryOutcome": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_attribute( - SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name - ) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_data("code", response.code().name) - - return response - - def intercept_unary_stream( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[Iterator[Message], Call]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - response: "UnaryStreamCall" - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - metadata = ( - list(client_call_details.metadata) if client_call_details.metadata else [] - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - metadata.append((key, value)) - - client_call_details = grpc._interceptor._ClientCallDetails( - method=client_call_details.method, - timeout=client_call_details.timeout, - metadata=metadata, - credentials=client_call_details.credentials, - wait_for_ready=client_call_details.wait_for_ready, - compression=client_call_details.compression, - ) - - return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py deleted file mode 100644 index 9fdb975caf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py deleted file mode 100644 index 1cba1d4b85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/grpc/server.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Callable, Optional - - from google.protobuf.message import Message - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Optional[Callable[[ServicerContext], str]]" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", - handler_call_details: "HandlerCallDetails", - ) -> "RpcMethodHandler": - handler = continuation(handler_call_details) - if not handler or not handler.unary_unary: - return handler - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - def behavior(request: "Message", context: "ServicerContext") -> "Message": - with sentry_sdk.isolation_scope(): - name = custom_find_name(context) if custom_find_name else method_name - - if name: - metadata = dict(context.invocation_metadata()) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(metadata) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - transaction = sentry_sdk.continue_trace( - metadata, - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - return handler.unary_unary(request, context) - - return grpc.unary_unary_rpc_method_handler( - behavior, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py deleted file mode 100644 index a68f20b299..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx is not installed") - -__all__ = ["HttpxIntegration"] - - -class HttpxIntegration(Integration): - identifier = "httpx" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx_client() - _install_httpx_async_client() - - -def _install_httpx_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(HttpxIntegration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(HttpxIntegration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py deleted file mode 100644 index 25062aaa11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/httpx2.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx2 import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx2 is not installed") - -__all__ = ["Httpx2Integration"] - - -class Httpx2Integration(Integration): - identifier = "httpx2" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx2 has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx2_client() - _install_httpx2_async_client() - - -def _install_httpx2_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(Httpx2Integration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx2_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(Httpx2Integration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py deleted file mode 100644 index c3bbc8abcf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huey.py +++ /dev/null @@ -1,239 +0,0 @@ -import sys -from datetime import datetime -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, Hint - from sentry_sdk.utils import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - -try: - from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task - from huey.exceptions import CancelExecution, RetryTask, TaskLockedException -except ImportError: - raise DidNotEnable("Huey is not installed") - -try: - from huey.api import chord as HueyChord - from huey.api import group as HueyGroup -except ImportError: - HueyChord = None - HueyGroup = None - - -HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) - - -class HueyIntegration(Integration): - identifier = "huey" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_enqueue() - patch_execute() - _register_control_flow_exception( - [CancelExecution, RetryTask, TaskLockedException] - ) - - -def patch_enqueue() -> None: - old_enqueue = Huey.enqueue - - @ensure_integration_enabled(HueyIntegration, old_enqueue) - def _sentry_enqueue( - self: "Huey", item: "Any" - ) -> "Optional[Union[Result, ResultGroup]]": - if HueyChord is not None and isinstance(item, HueyChord): - span_name = "Huey Chord" - elif HueyGroup is not None and isinstance(item, HueyGroup): - span_name = "Huey Task Group" - else: - span_name = item.name - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - span_ctx = None - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_HUEY, - "sentry.origin": HueyIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_HUEY, - name=span_name, - origin=HueyIntegration.origin, - ) - - no_headers_types = (PeriodicTask,) + tuple( - t for t in [HueyGroup, HueyChord] if t is not None - ) - with span_ctx: - if not isinstance(item, no_headers_types): - # Attach trace propagation data to task kwargs. We do - # not do this for periodic tasks, as these don't - # really have an originating transaction. - # Additionally, we do not do this for Huey groups or chords, as enqueue will - # recursively call this method for each task within the list, resulting - # in the trace propagation data being attached to each task individually - # (which we want) - item.kwargs["sentry_headers"] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - return old_enqueue(self, item) - - Huey.enqueue = _sentry_enqueue - - -def _make_event_processor(task: "Any") -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["huey_task_id"] = task.id - tags["huey_task_retry"] = task.default_retries > task.retries - extra = event.setdefault("extra", {}) - extra["huey-job"] = { - "task": task.name, - "args": ( - task.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - task.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": (task.default_retries or 0) - task.retries, - } - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.ABORTED) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.OK - return - - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.ERROR - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": HueyIntegration.identifier, "handled": False}, - ) - scope.capture_event(event, hint=hint) - - -def _wrap_task_execute(func: "F") -> "F": - @ensure_integration_enabled(HueyIntegration, func) - def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": - try: - result = func(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_execute # type: ignore - - -def patch_execute() -> None: - old_execute = Huey._execute - - @ensure_integration_enabled(HueyIntegration, old_execute) - def _sentry_execute( - self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None - ) -> "Any": - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope._name = "huey" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task)) - - sentry_headers = task.kwargs.pop("sentry_headers", None) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - headers = sentry_headers or {} - sentry_sdk.traces.continue_trace(headers) - span_ctx = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_TASK_HUEY, - "sentry.origin": HueyIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - "messaging.message.id": task.id, - "messaging.message.system": "huey", - "messaging.message.retry.count": (task.default_retries or 0) - - task.retries, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - sentry_headers or {}, - name=task.name, - op=OP.QUEUE_TASK_HUEY, - source=TransactionSource.TASK, - origin=HueyIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - span_ctx = sentry_sdk.start_transaction(transaction) - - if not getattr(task, "_sentry_is_patched", False): - task.execute = _wrap_task_execute(task.execute) - task._sentry_is_patched = True - - with span_ctx: - return old_execute(self, task, timestamp) - - Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py deleted file mode 100644 index 835acc7279..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/huggingface_hub.py +++ /dev/null @@ -1,392 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - _set_span_data_attribute, - get_start_span_function, - set_data_normalized, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Union - - from sentry_sdk.tracing import Span - -try: - import huggingface_hub.inference._client -except ImportError: - raise DidNotEnable("Huggingface not installed") - - -class HuggingfaceHubIntegration(Integration): - identifier = "huggingface_hub" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "HuggingfaceHubIntegration", include_prompts: bool = True - ) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks - huggingface_hub.inference._client.InferenceClient.text_generation = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.text_generation, - OP.GEN_AI_TEXT_COMPLETION, - ) - ) - huggingface_hub.inference._client.InferenceClient.chat_completion = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.chat_completion, - OP.GEN_AI_CHAT, - ) - ) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "huggingface_hub", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": - @wraps(f) - def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) - if integration is None: - return f(*args, **kwargs) - - prompt = None - if "prompt" in kwargs: - prompt = kwargs["prompt"] - elif "messages" in kwargs: - prompt = kwargs["messages"] - elif len(args) >= 2: - if isinstance(args[1], str) or isinstance(args[1], list): - prompt = args[1] - - if prompt is None: - # invalid call, dont instrument, let it return error - return f(*args, **kwargs) - - client = args[0] - model = client.model or kwargs.get("model") or "" - operation_name = op.split(".")[-1] - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(sentry_sdk.get_client().options): - span = sentry_sdk.traces.start_span( - name=f"{operation_name} {model}", - attributes={ - "sentry.op": op, - "sentry.origin": HuggingfaceHubIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=op, - name=f"{operation_name} {model}", - origin=HuggingfaceHubIntegration.origin, - ) - span.__enter__() - - _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - - if model: - _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) - - # Input attributes - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False - ) - - attribute_mapping = { - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - } - - for attribute, span_attribute in attribute_mapping.items(): - value = kwargs.get(attribute, None) - if value is not None: - if isinstance(value, (int, float, bool, str)): - _set_span_data_attribute(span, span_attribute, value) - else: - set_data_normalized(span, span_attribute, value, unpack=False) - - # LLM Execution - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Output attributes - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - tool_calls = None - usage = None - - with capture_internal_exceptions(): - if isinstance(res, str) and res is not None: - response_text_buffer.append(res) - - if hasattr(res, "generated_text") and res.generated_text is not None: - response_text_buffer.append(res.generated_text) - - if hasattr(res, "model") and res.model is not None: - response_model = res.model - - if hasattr(res, "details") and hasattr(res.details, "finish_reason"): - finish_reason = res.details.finish_reason - - if ( - hasattr(res, "details") - and hasattr(res.details, "generated_tokens") - and res.details.generated_tokens is not None - ): - tokens_used = res.details.generated_tokens - - if hasattr(res, "usage") and res.usage is not None: - usage = res.usage - - if hasattr(res, "choices") and res.choices is not None: - for choice in res.choices: - if hasattr(choice, "finish_reason"): - finish_reason = choice.finish_reason - if hasattr(choice, "message") and hasattr( - choice.message, "tool_calls" - ): - tool_calls = choice.message.tool_calls - if ( - hasattr(choice, "message") - and hasattr(choice.message, "content") - and choice.message.content is not None - ): - response_text_buffer.append(choice.message.content) - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - elif tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - # If the response is not a generator (meaning a streaming response) - # we are done and can return the response - if not inspect.isgenerator(res): - span.__exit__(None, None, None) - return res - - if kwargs.get("details", False): - # text-generation stream output - def new_details_iterator() -> "Iterable[Any]": - finish_reason = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - - with capture_internal_exceptions(): - for chunk in res: - if ( - hasattr(chunk, "token") - and hasattr(chunk.token, "text") - and chunk.token.text is not None - ): - response_text_buffer.append(chunk.token.text) - - if hasattr(chunk, "details") and hasattr( - chunk.details, "finish_reason" - ): - finish_reason = chunk.details.finish_reason - - if ( - hasattr(chunk, "details") - and hasattr(chunk.details, "generated_tokens") - and chunk.details.generated_tokens is not None - ): - tokens_used = chunk.details.generated_tokens - - yield chunk - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - span.__exit__(None, None, None) - - return new_details_iterator() - - else: - # chat-completion stream output - def new_iterator() -> "Iterable[str]": - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tool_calls = None - usage = None - - with capture_internal_exceptions(): - for chunk in res: - if hasattr(chunk, "model") and chunk.model is not None: - response_model = chunk.model - - if hasattr(chunk, "usage") and chunk.usage is not None: - usage = chunk.usage - - if isinstance(chunk, str): - if chunk is not None: - response_text_buffer.append(chunk) - - if hasattr(chunk, "choices") and chunk.choices is not None: - for choice in chunk.choices: - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "content") - and choice.delta.content is not None - ): - response_text_buffer.append( - choice.delta.content - ) - - if ( - hasattr(choice, "finish_reason") - and choice.finish_reason is not None - ): - finish_reason = choice.finish_reason - - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "tool_calls") - and choice.delta.tool_calls is not None - ): - tool_calls = choice.delta.tool_calls - - yield chunk - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - - span.__exit__(None, None, None) - - return new_iterator() - - return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py deleted file mode 100644 index 9dcbb189ce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langchain.py +++ /dev/null @@ -1,1412 +0,0 @@ -import itertools -import json -import sys -import warnings -from collections import OrderedDict -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - _get_value, - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import capture_internal_exceptions, logger - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Dict, - Iterator, - List, - Optional, - Union, - ) - from uuid import UUID - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - - -try: - from langchain_core.agents import AgentFinish - from langchain_core.callbacks import ( - BaseCallbackHandler, - BaseCallbackManager, - Callbacks, - manager, - ) - from langchain_core.messages import BaseMessage - from langchain_core.outputs import LLMResult - -except ImportError: - raise DidNotEnable("langchain not installed") - - -try: - # >=v1 - from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] -except ImportError: - try: - # "Optional[str]": - ai_type = all_params.get("_type") - - if not ai_type or not isinstance(ai_type, str): - return None - - return ai_type - - -DATA_FIELDS = { - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, -} - - -def _transform_langchain_content_block( - content_block: "Dict[str, Any]", -) -> "Dict[str, Any]": - """ - Transform a LangChain content block using the shared transform_content_part function. - - Returns the original content block if transformation is not applicable - (e.g., for text blocks or unrecognized formats). - """ - result = transform_content_part(content_block) - return result if result is not None else content_block - - -def _transform_langchain_message_content(content: "Any") -> "Any": - """ - Transform LangChain message content, handling both string content and - list of content blocks. - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for block in content: - if isinstance(block, dict): - transformed.append(_transform_langchain_content_block(block)) - else: - transformed.append(block) - return transformed - - return content - - -def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": - system_instructions = [] - - for list_ in messages: - for message in list_: - # type of content: str | list[str | dict] | None - if message.type == "system" and isinstance(message.content, str): - system_instructions.append(message.content) - - elif message.type == "system" and isinstance(message.content, list): - for item in message.content: - if isinstance(item, str): - system_instructions.append(item) - - elif isinstance(item, dict) and item.get("type") == "text": - instruction = item.get("text") - if isinstance(instruction, str): - system_instructions.append(instruction) - - return system_instructions - - -def _transform_system_instructions( - system_instructions: "List[str]", -) -> "List[TextPart]": - return [ - { - "type": "text", - "content": instruction, - } - for instruction in system_instructions - ] - - -class LangchainIntegration(Integration): - identifier = "langchain" - origin = f"auto.ai.{identifier}" - - _ignored_exceptions: "set[type[Exception]]" = set() - - def __init__( - self: "LangchainIntegration", - include_prompts: bool = True, - max_spans: "Optional[int]" = None, - ) -> None: - self.include_prompts = include_prompts - self.max_spans = max_spans - - if max_spans is not None: - warnings.warn( - "The `max_spans` parameter of `LangchainIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - manager._configure = _wrap_configure(manager._configure) - - if AgentExecutor is not None: - AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) - AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) - - # Patch embeddings providers - _patch_embeddings_provider(OpenAIEmbeddings) - _patch_embeddings_provider(AzureOpenAIEmbeddings) - _patch_embeddings_provider(VertexAIEmbeddings) - _patch_embeddings_provider(BedrockEmbeddings) - _patch_embeddings_provider(CohereEmbeddings) - _patch_embeddings_provider(MistralAIEmbeddings) - _patch_embeddings_provider(HuggingFaceEmbeddings) - _patch_embeddings_provider(OllamaEmbeddings) - - -class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] - """Callback handler that creates Sentry spans.""" - - def __init__( - self, max_span_map_size: "Optional[int]", include_prompts: bool - ) -> None: - self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() - self.max_span_map_size = max_span_map_size - self.include_prompts = include_prompts - - def gc_span_map(self) -> None: - if self.max_span_map_size is not None: - while len(self.span_map) > self.max_span_map_size: - run_id, span = self.span_map.popitem(last=False) - self._exit_span(span, run_id) - - def _handle_error(self, run_id: "UUID", error: "Any") -> None: - is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) - - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if is_ignored: - span.__exit__(None, None, None) - else: - sentry_sdk.capture_exception( - error, span._scope if isinstance(span, StreamedSpan) else span.scope - ) - span.__exit__(type(error), error, error.__traceback__) - - del self.span_map[run_id] - - def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": - # Transform content to handle multimodal data (images, audio, video, files) - transformed_content = _transform_langchain_message_content(message.content) - parsed = {"role": message.type, "content": transformed_content} - parsed.update(message.additional_kwargs) - return parsed - - def _create_span( - self: "SentryLangchainCallback", - run_id: "UUID", - parent_id: "Optional[Any]", - op: str, - name: str, - origin: str, - ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span = None - if parent_id: - parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( - self.span_map.get(parent_id) - ) - if parent_span: - span = ( - sentry_sdk.traces.start_span( - parent_span=parent_span, - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if isinstance(parent_span, StreamedSpan) - else parent_span.start_child(op=op, name=name, origin=origin) - ) - - if span is None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span = ( - sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if span_streaming - else sentry_sdk.start_span(op=op, name=name, origin=origin) - ) - - span.__enter__() - self.span_map[run_id] = span - self.gc_span_map() - return span - - def _exit_span( - self: "SentryLangchainCallback", - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - run_id: "UUID", - ) -> None: - span.__exit__(None, None, None) - del self.span_map[run_id] - - def on_llm_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - prompts: "List[str]", - *, - run_id: "UUID", - tags: "Optional[List[str]]" = None, - parent_run_id: "Optional[UUID]" = None, - metadata: "Optional[Dict[str, Any]]" = None, - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - parent_run_id, - op=OP.GEN_AI_TEXT_COMPLETION, - name=f"text_completion {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") - - run_name = kwargs.get("name") - if run_name: - set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - if model: - set_on_span( - SPANDATA.GEN_AI_REQUEST_MODEL, - model, - ) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - normalized_messages = [ - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, - "content": {"type": "text", "text": prompt}, - } - for prompt in prompts - ] - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - messages: "List[List[BaseMessage]]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if model: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - system_instructions = _get_system_instructions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - normalized_messages = [] - for list_ in messages: - for message in list_: - if message.type == "system": - continue - - normalized_messages.append( - self._normalize_langchain_message(message) - ) - normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - try: - generation = response.generations[0][0] - except IndexError: - generation = None - - if generation is not None: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - - try: - response_model = generation.message.response_metadata.get( - "model_name" - ) - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - except AttributeError: - pass - - try: - finish_reason = generation.generation_info.get("finish_reason") - if finish_reason is not None: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - [finish_reason], - ) - except AttributeError: - pass - - try: - if should_send_default_pii() and self.include_prompts: - tool_calls = getattr(generation.message, "tool_calls", None) - if tool_calls is not None and tool_calls != []: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - except AttributeError: - pass - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM errors.""" - self._handle_error(run_id, error) - - def on_chat_model_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model errors.""" - self._handle_error(run_id, error) - - def on_agent_finish( - self: "SentryLangchainCallback", - finish: "AgentFinish", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() - ) - - self._exit_span(span, run_id) - - def on_tool_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - input_str: str, - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - tool_name = serialized.get("name") or kwargs.get("name") or "" - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - tool_description = serialized.get("description") - if tool_description is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_TOOL_INPUT, - kwargs.get("inputs", [input_str]), - ) - - def on_tool_end( - self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" - ) -> "Any": - """Run when tool ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) - - self._exit_span(span, run_id) - - def on_tool_error( - self, - error: "SentryLangchainCallback", - *args: "Union[Exception, KeyboardInterrupt]", - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool errors.""" - self._handle_error(run_id, error) - - -def _extract_tokens( - token_usage: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - if not token_usage: - return None, None, None - - input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( - token_usage, "input_tokens" - ) - output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( - token_usage, "output_tokens" - ) - total_tokens = _get_value(token_usage, "total_tokens") - - return input_tokens, output_tokens, total_tokens - - -def _extract_tokens_from_generations( - generations: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - """Extract token usage from response.generations structure.""" - if not generations: - return None, None, None - - total_input = 0 - total_output = 0 - total_total = 0 - - for gen_list in generations: - for gen in gen_list: - token_usage = _get_token_usage(gen) - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - total_input += input_tokens if input_tokens is not None else 0 - total_output += output_tokens if output_tokens is not None else 0 - total_total += total_tokens if total_tokens is not None else 0 - - return ( - total_input if total_input > 0 else None, - total_output if total_output > 0 else None, - total_total if total_total > 0 else None, - ) - - -def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": - """ - Check multiple paths to extract token usage from different objects. - """ - possible_names = ("usage", "token_usage", "usage_metadata") - - message = _get_value(obj, "message") - if message is not None: - for name in possible_names: - usage = _get_value(message, name) - if usage is not None: - return usage - - llm_output = _get_value(obj, "llm_output") - if llm_output is not None: - for name in possible_names: - usage = _get_value(llm_output, name) - if usage is not None: - return usage - - for name in possible_names: - usage = _get_value(obj, name) - if usage is not None: - return usage - - return None - - -def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: - token_usage = _get_token_usage(response) - if token_usage: - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - else: - input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( - response.generations - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) - - -def _get_request_data( - obj: "Any", args: "Any", kwargs: "Any" -) -> "tuple[Optional[str], Optional[List[Any]]]": - """ - Get the agent name and available tools for the agent. - """ - agent = getattr(obj, "agent", None) - runnable = getattr(agent, "runnable", None) - runnable_config = getattr(runnable, "config", {}) - tools = ( - getattr(obj, "tools", None) - or getattr(agent, "tools", None) - or runnable_config.get("tools") - or runnable_config.get("available_tools") - ) - tools = tools if tools and len(tools) > 0 else None - - try: - agent_name = None - if len(args) > 1: - agent_name = args[1].get("run_name") - if agent_name is None: - agent_name = runnable_config.get("run_name") - except Exception: - pass - - return (agent_name, tools) - - -def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": - """Parse and simplify tools into a cleaner format.""" - if not tools: - return None - - if not isinstance(tools, (list, tuple)): - return None - - simplified_tools = [] - for tool in tools: - try: - if isinstance(tool, dict): - if "function" in tool and isinstance(tool["function"], dict): - func = tool["function"] - simplified_tool = { - "name": func.get("name"), - "description": func.get("description"), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif "name" in tool: - simplified_tool = { - "name": tool.get("name"), - "description": tool.get("description"), - } - simplified_tools.append(simplified_tool) - else: - name = ( - tool.get("name") - or tool.get("tool_name") - or tool.get("function_name") - ) - if name: - simplified_tools.append( - { - "name": name, - "description": tool.get("description") - or tool.get("desc"), - } - ) - elif hasattr(tool, "name"): - simplified_tool = { - "name": getattr(tool, "name", None), - "description": getattr(tool, "description", None) - or getattr(tool, "desc", None), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif hasattr(tool, "__name__"): - simplified_tools.append( - { - "name": tool.__name__, - "description": getattr(tool, "__doc__", None), - } - ) - else: - tool_str = str(tool) - if tool_str and tool_str != "": - simplified_tools.append({"name": tool_str, "description": None}) - except Exception: - continue - - return simplified_tools if simplified_tools else None - - -def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: - """Set available tools data on a span if tools are provided.""" - if tools is not None: - simplified_tools = _simplify_langchain_tools(tools) - if simplified_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - simplified_tools, - unpack=False, - ) - - -def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_configure( - callback_manager_cls: type, - inheritable_callbacks: "Callbacks" = None, - local_callbacks: "Callbacks" = None, - *args: "Any", - **kwargs: "Any", - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(LangchainIntegration) - if integration is None: - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - local_callbacks = local_callbacks or [] - - # Handle each possible type of local_callbacks. For each type, we - # extract the list of callbacks to check for SentryLangchainCallback, - # and define a function that would add the SentryLangchainCallback - # to the existing callbacks list. - if isinstance(local_callbacks, BaseCallbackManager): - callbacks_list = local_callbacks.handlers - elif isinstance(local_callbacks, BaseCallbackHandler): - callbacks_list = [local_callbacks] - elif isinstance(local_callbacks, list): - callbacks_list = local_callbacks - else: - logger.debug("Unknown callback type: %s", local_callbacks) - # Just proceed with original function call - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - # Handle each possible type of inheritable_callbacks. - if isinstance(inheritable_callbacks, BaseCallbackManager): - inheritable_callbacks_list = inheritable_callbacks.handlers - elif isinstance(inheritable_callbacks, list): - inheritable_callbacks_list = inheritable_callbacks - else: - inheritable_callbacks_list = [] - - if not any( - isinstance(cb, SentryLangchainCallback) - for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) - ): - sentry_handler = SentryLangchainCallback( - integration.max_spans, - integration.include_prompts, - ) - if isinstance(local_callbacks, BaseCallbackManager): - local_callbacks = local_callbacks.copy() - local_callbacks.handlers = [ - *local_callbacks.handlers, - sentry_handler, - ] - elif isinstance(local_callbacks, BaseCallbackHandler): - local_callbacks = [local_callbacks, sentry_handler] - else: - local_callbacks = [*local_callbacks, sentry_handler] - - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - return new_configure - - -def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: False, - }, - ) as span: - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - else: - start_span_function = get_start_span_function() - - with start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) as span: - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - - return new_invoke - - -def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - else: - start_span_function = get_start_span_function() - - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - input = args[0].get("input") if len(args) >= 1 else None - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Run the agent - result = f(self, *args, **kwargs) - - old_iterator = result - - def new_iterator() -> "Iterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - async def new_iterator_async() -> "AsyncIterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - async for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - if str(type(result)) == "": - result = new_iterator_async() - else: - result = new_iterator() - - return result - - return new_stream - - -def _patch_embeddings_provider(provider_class: "Any") -> None: - """Patch an embeddings provider class with monitoring wrappers.""" - if provider_class is None: - return - - if hasattr(provider_class, "embed_documents"): - provider_class.embed_documents = _wrap_embedding_method( - provider_class.embed_documents - ) - if hasattr(provider_class, "embed_query"): - provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) - if hasattr(provider_class, "aembed_documents"): - provider_class.aembed_documents = _wrap_async_embedding_method( - provider_class.aembed_documents - ) - if hasattr(provider_class, "aembed_query"): - provider_class.aembed_query = _wrap_async_embedding_method( - provider_class.aembed_query - ) - - -def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap sync embedding methods (embed_documents and embed_query).""" - - @wraps(f) - def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - - return new_embedding_method - - -def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap async embedding methods (aembed_documents and aembed_query).""" - - @wraps(f) - async def new_async_embedding_method( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - - return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py deleted file mode 100644 index 3d3856a913..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/langgraph.py +++ /dev/null @@ -1,515 +0,0 @@ -from functools import wraps -from typing import Any, Callable, List, Optional - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration - -# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. -from sentry_sdk.integrations.langchain import LangchainIntegration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -try: - from langgraph.errors import GraphBubbleUp - from langgraph.graph import StateGraph - from langgraph.pregel import Pregel -except ImportError: - raise DidNotEnable("langgraph not installed") - - -class LanggraphIntegration(Integration): - identifier = "langgraph" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) - # LangGraph lets users create agents using a StateGraph or the Functional API. - # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and - # the functional API execute on a Pregel instance. Pregel is the runtime for the graph - # and the invocation happens on Pregel, so patching the invoke methods takes care of both. - # The streaming methods are not patched, because due to some internal reasons, LangGraph - # will automatically patch the streaming methods to run through invoke, and by doing this - # we prevent duplicate spans for invocations. - StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) - if hasattr(Pregel, "invoke"): - Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) - if hasattr(Pregel, "ainvoke"): - Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) - - -def _get_graph_name(graph_obj: "Any") -> "Optional[str]": - for attr in ["name", "graph_name", "__name__", "_name"]: - if hasattr(graph_obj, attr): - name = getattr(graph_obj, attr) - if name and isinstance(name, str): - return name - return None - - -def _normalize_langgraph_message(message: "Any") -> "Any": - if not hasattr(message, "content"): - return None - - parsed = {"role": getattr(message, "type", None), "content": message.content} - - for attr in [ - "name", - "tool_calls", - "function_call", - "tool_call_id", - "response_metadata", - ]: - if hasattr(message, attr): - value = getattr(message, attr) - if value is not None: - parsed[attr] = value - - return parsed - - -def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": - if not state: - return None - - messages = None - - if isinstance(state, dict): - messages = state.get("messages") - elif hasattr(state, "messages"): - messages = state.messages - elif hasattr(state, "get") and callable(state.get): - try: - messages = state.get("messages") - except Exception: - pass - - if not messages or not isinstance(messages, (list, tuple)): - return None - - normalized_messages = [] - for message in messages: - try: - normalized = _normalize_langgraph_message(message) - if normalized: - normalized_messages.append(normalized) - except Exception: - continue - - return normalized_messages if normalized_messages else None - - -def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None or has_span_streaming_enabled(client.options): - return f(self, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GEN_AI_CREATE_AGENT, - origin=LanggraphIntegration.origin, - ) as span: - compiled_graph = f(self, *args, **kwargs) - - compiled_graph_name = getattr(compiled_graph, "name", None) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) - - if compiled_graph_name: - span.description = f"create_agent {compiled_graph_name}" - else: - span.description = "create_agent" - - if kwargs.get("model", None) is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) - - tools = None - get_graph = getattr(compiled_graph, "get_graph", None) - if get_graph and callable(get_graph): - graph_obj = compiled_graph.get_graph() - nodes = getattr(graph_obj, "nodes", None) - if nodes and isinstance(nodes, dict): - tools_node = nodes.get("tools") - if tools_node: - data = getattr(tools_node, "data", None) - if data and hasattr(data, "tools_by_name"): - tools = list(data.tools_by_name.keys()) - - if tools is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) - - return compiled_graph - - return new_compile - - -def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - else: - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_invoke - - -def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles(input_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_ainvoke - - -def _get_new_messages( - input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" -) -> "Optional[List[Any]]": - """Extract only the new messages added during this invocation.""" - if not output_messages: - return None - - if not input_messages: - return output_messages - - # only return the new messages, aka the output messages that are not in the input messages - input_count = len(input_messages) - new_messages = ( - output_messages[input_count:] if len(output_messages) > input_count else [] - ) - - return new_messages if new_messages else None - - -def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": - if not messages: - return None - - for message in reversed(messages): - if isinstance(message, dict): - role = message.get("role") - if role in ["assistant", "ai"]: - content = message.get("content") - if content and isinstance(content, str): - return content - - return None - - -def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": - if not messages: - return None - - tool_calls = [] - for message in messages: - if isinstance(message, dict): - msg_tool_calls = message.get("tool_calls") - if msg_tool_calls and isinstance(msg_tool_calls, list): - tool_calls.extend(msg_tool_calls) - - return tool_calls if tool_calls else None - - -def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - input_tokens = 0 - output_tokens = 0 - total_tokens = 0 - - for message in messages: - response_metadata = message.get("response_metadata") - if response_metadata is None: - continue - - token_usage = response_metadata.get("token_usage") - if not token_usage: - continue - - input_tokens += int(token_usage.get("prompt_tokens", 0)) - output_tokens += int(token_usage.get("completion_tokens", 0)) - total_tokens += int(token_usage.get("total_tokens", 0)) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens > 0: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - total_tokens, - ) - - -def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - if len(messages) == 0: - return - - last_message = messages[-1] - response_metadata = last_message.get("response_metadata") - if response_metadata is None: - return - - model_name = response_metadata.get("model_name") - if model_name is None: - return - - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) - - -def _set_response_attributes( - span: "Any", - input_messages: "Optional[List[Any]]", - result: "Any", - integration: "LanggraphIntegration", -) -> None: - parsed_response_messages = _parse_langgraph_messages(result) - new_messages = _get_new_messages(input_messages, parsed_response_messages) - - if new_messages is None: - return - - _set_usage_data(span, new_messages) - _set_response_model_name(span, new_messages) - - if not (should_send_default_pii() and integration.include_prompts): - return - - llm_response_text = _extract_llm_response_text(new_messages) - if llm_response_text: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) - elif new_messages: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) - else: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) - - tool_calls = _extract_tool_calls(new_messages) - if tool_calls: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(tool_calls), - unpack=False, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py deleted file mode 100644 index 3c2c76450c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/launchdarkly.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - import ldclient - from ldclient.hook import Hook, Metadata - - if TYPE_CHECKING: - from typing import Any - - from ldclient import LDClient - from ldclient.evaluation import EvaluationDetail - from ldclient.hook import EvaluationSeriesContext -except ImportError: - raise DidNotEnable("LaunchDarkly is not installed") - - -class LaunchDarklyIntegration(Integration): - identifier = "launchdarkly" - - def __init__(self, ld_client: "LDClient | None" = None) -> None: - """ - :param client: An initialized LDClient instance. If a client is not provided, this - integration will attempt to use the shared global instance. - """ - try: - client = ld_client or ldclient.get() - except Exception as exc: - raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) - - if not client.is_initialized(): - raise DidNotEnable("LaunchDarkly client is not initialized.") - - # Register the flag collection hook with the LD client. - client.add_hook(LaunchDarklyHook()) - - @staticmethod - def setup_once() -> None: - pass - - -class LaunchDarklyHook(Hook): - @property - def metadata(self) -> "Metadata": - return Metadata(name="sentry-flag-auditor") - - def after_evaluation( - self, - series_context: "EvaluationSeriesContext", - data: "dict[Any, Any]", - detail: "EvaluationDetail", - ) -> "dict[Any, Any]": - if isinstance(detail.value, bool): - add_feature_flag(series_context.key, detail.value) - - return data - - def before_evaluation( - self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" - ) -> "dict[Any, Any]": - return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py deleted file mode 100644 index 49ead6b068..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litellm.py +++ /dev/null @@ -1,376 +0,0 @@ -import copy -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - set_data_normalized, - transform_openai_content_part, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from datetime import datetime - from typing import Any, Dict, List - -try: - import litellm # type: ignore[import-not-found] - from litellm import failure_callback, input_callback, success_callback -except ImportError: - raise DidNotEnable("LiteLLM not installed") - - -# Stash the span on a top-level key of the per-request kwargs dict litellm passes -# to every callback, so it lives and dies with the request. -_SPAN_KEY = "_sentry_span" - - -def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: - kwargs[_SPAN_KEY] = span - - -def _peek_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.get(_SPAN_KEY) - - -def _pop_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.pop(_SPAN_KEY, None) - - -def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": - """ - Convert the message parts from OpenAI format to the `gen_ai.request.messages` format - using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). - - Deep copies messages to avoid mutating original kwargs. - """ - # Deep copy to avoid mutating original messages from kwargs - messages = copy.deepcopy(messages) - - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_openai_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - message["content"] = transformed - return messages - - -def _input_callback(kwargs: "Dict[str, Any]") -> None: - """Handle the start of a request.""" - client = sentry_sdk.get_client() - integration = client.get_integration(LiteLLMIntegration) - - if integration is None: - return - - # Get key parameters - full_model = kwargs.get("model", "") - try: - model, provider, _, _ = litellm.get_llm_provider(full_model) - except Exception: - model = full_model - provider = "unknown" - - call_type = kwargs.get("call_type", None) - if call_type == "embedding" or call_type == "aembedding": - operation = "embeddings" - else: - operation = "chat" - - # Start a new span/transaction - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"{operation} {model}", - attributes={ - "sentry.op": ( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - "sentry.origin": LiteLLMIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - name=f"{operation} {model}", - origin=LiteLLMIntegration.origin, - ) - span.__enter__() - - _store_span(kwargs, span) - - # Set basic data - set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) - - # Record input/messages if allowed - if should_send_default_pii() and integration.include_prompts: - if operation == "embeddings": - # For embeddings, look for the 'input' parameter - embedding_input = kwargs.get("input") - if embedding_input: - scope = sentry_sdk.get_current_scope() - # Normalize to list format - input_list = ( - embedding_input - if isinstance(embedding_input, list) - else [embedding_input] - ) - client = sentry_sdk.get_client() - messages_data = ( - truncate_and_annotate_embedding_inputs(input_list, span, scope) - if should_truncate_gen_ai_input(client.options) - else input_list - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - messages_data, - unpack=False, - ) - else: - # For chat, look for the 'messages' parameter - messages = kwargs.get("messages", []) - if messages: - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages = _convert_message_parts(messages) - messages_data = ( - truncate_and_annotate_messages(messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Record other parameters - params = { - "model": SPANDATA.GEN_AI_REQUEST_MODEL, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - } - for key, attribute in params.items(): - value = kwargs.get(key) - if value is not None: - set_data_normalized(span, attribute, value) - - -async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: - return _input_callback(kwargs) - - -def _success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle successful completion.""" - - span = _peek_span(kwargs) - if span is None: - return - - integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) - if integration is None: - return - - try: - # Record model information - if hasattr(completion_response, "model"): - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model - ) - - # Record response content if allowed - if should_send_default_pii() and integration.include_prompts: - if hasattr(completion_response, "choices"): - response_messages = [] - for choice in completion_response.choices: - if hasattr(choice, "message"): - if hasattr(choice.message, "model_dump"): - response_messages.append(choice.message.model_dump()) - elif hasattr(choice.message, "dict"): - response_messages.append(choice.message.dict()) - else: - # Fallback for basic message objects - msg = {} - if hasattr(choice.message, "role"): - msg["role"] = choice.message.role - if hasattr(choice.message, "content"): - msg["content"] = choice.message.content - if hasattr(choice.message, "tool_calls"): - msg["tool_calls"] = choice.message.tool_calls - response_messages.append(msg) - - if response_messages: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages - ) - - # Record token usage - if hasattr(completion_response, "usage"): - usage = completion_response.usage - record_token_usage( - span, - input_tokens=getattr(usage, "prompt_tokens", None), - output_tokens=getattr(usage, "completion_tokens", None), - total_tokens=getattr(usage, "total_tokens", None), - ) - - finally: - is_streaming = kwargs.get("stream") - # Callback is fired multiple times when streaming a response. - # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 - if ( - is_streaming is not True - or "complete_streaming_response" in kwargs - or "async_complete_streaming_response" in kwargs - ): - span = _pop_span(kwargs) - if span is not None: - span.__exit__(None, None, None) - - -async def _async_success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - return _success_callback( - kwargs, - completion_response, - start_time, - end_time, - ) - - -def _failure_callback( - kwargs: "Dict[str, Any]", - exception: Exception, - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle request failure.""" - span = _pop_span(kwargs) - if span is None: - return - - try: - # Capture the exception - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "litellm", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - finally: - # Always finish the span and clean up - span.__exit__(type(exception), exception, None) - - -class LiteLLMIntegration(Integration): - """ - LiteLLM integration for Sentry. - - This integration automatically captures LiteLLM API calls and sends them to Sentry - for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM - supports, including OpenAI, Anthropic, Google, Cohere, and many others. - - Features: - - Automatic exception capture for all LiteLLM calls - - Token usage tracking across all providers - - Provider detection and attribution - - Input/output message capture (configurable) - - Streaming response support - - Cost tracking integration - - Usage: - - ```python - import litellm - import sentry_sdk - - # Initialize Sentry with the LiteLLM integration - sentry_sdk.init( - dsn="your-dsn", - send_default_pii=True - integrations=[ - sentry_sdk.integrations.LiteLLMIntegration( - include_prompts=True # Set to False to exclude message content - ) - ] - ) - - # All LiteLLM calls will now be monitored - response = litellm.completion( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": "Hello!"}] - ) - ``` - - Configuration: - - include_prompts (bool): Whether to include prompts and responses in spans. - Defaults to True. Set to False to exclude potentially sensitive data. - """ - - identifier = "litellm" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """Set up LiteLLM callbacks for monitoring.""" - litellm.input_callback = input_callback or [] - if _input_callback not in litellm.input_callback: - litellm.input_callback.append(_input_callback) - if _async_input_callback not in litellm.input_callback: - litellm.input_callback.append(_async_input_callback) - - litellm.success_callback = success_callback or [] - if _success_callback not in litellm.success_callback: - litellm.success_callback.append(_success_callback) - if _async_success_callback not in litellm.success_callback: - litellm.success_callback.append(_async_success_callback) - - litellm.failure_callback = failure_callback or [] - if _failure_callback not in litellm.failure_callback: - litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py deleted file mode 100644 index f0c90a7921..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/litestar.py +++ /dev/null @@ -1,364 +0,0 @@ -from collections.abc import Set -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from litestar import Litestar, Request # type: ignore - from litestar.data_extractors import ConnectionDataExtractor # type: ignore - from litestar.exceptions import HTTPException # type: ignore - from litestar.handlers.base import BaseRouteHandler # type: ignore - from litestar.middleware import DefineMiddleware # type: ignore - from litestar.routes.http import HTTPRoute # type: ignore -except ImportError: - raise DidNotEnable("Litestar is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from litestar.middleware import MiddlewareProtocol - from litestar.types import ( # type: ignore - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from litestar.types import ( - Scope as LitestarScope, - ) - from litestar.types.asgi_types import ASGIApp # type: ignore - - from sentry_sdk._types import Event, Hint - -_DEFAULT_TRANSACTION_NAME = "generic Litestar request" - - -class LitestarIntegration(Integration): - identifier = "litestar" - origin = f"auto.http.{identifier}" - - def __init__( - self, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. - # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following - # (among other things): - # 1. Logs them, some at least (such as 500s) as errors - # 2. Calls after_exception hooks - # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event - # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will - # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. - ignore_logger("litestar") - - -class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - def _capture_request_exception(self, exc: Exception) -> None: - """Avoid catching exceptions from request handlers. - - Those exceptions are already handled in Litestar.after_exception handler. - We still catch exceptions from application lifespan handlers. - """ - pass - - -def patch_app_init() -> None: - """ - Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. - See: - - https://docs.litestar.dev/2/usage/applications.html#after-exception - - https://docs.litestar.dev/2/usage/middleware/using-middleware.html - """ - old__init__ = Litestar.__init__ - - @ensure_integration_enabled(LitestarIntegration, old__init__) - def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: - kwargs["after_exception"] = [ - exception_handler, - *(kwargs.get("after_exception") or []), - ] - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Litestar.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryLitestarASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "LitestarScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(LitestarIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=middleware_name, - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR, - "sentry.origin": LitestarIntegration.origin, - }, - ) as middleware_span: - middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.traces.start_span( - name=getattr(receive, "__qualname__", str(receive)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.traces.start_span( - name=getattr(send, "__qualname__", str(send)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - else: - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR, - name=middleware_name, - origin=LitestarIntegration.origin, - ) as middleware_span: - middleware_span.set_tag("litestar.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_SEND, - name=getattr(send, "__qualname__", str(send)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type - elif hasattr(route_handler.fn, "value"): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = LitestarIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if isinstance(scope_user, dict): - return scope_user - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - return None - - -@ensure_integration_enabled(LitestarIntegration) -def exception_handler(exc: Exception, scope: "LitestarScope") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - if isinstance(exc, HTTPException): - integration = sentry_sdk.get_client().get_integration(LitestarIntegration) - if ( - integration is not None - and exc.status_code not in integration.failed_request_status_codes - ): - return - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": LitestarIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py deleted file mode 100644 index a310a0ced6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/logging.py +++ /dev/null @@ -1,476 +0,0 @@ -import logging -import sys -from datetime import datetime, timezone -from fnmatch import fnmatch -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.client import BaseClient -from sentry_sdk.integrations import Integration -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import ( - capture_internal_exceptions, - current_stacktrace, - event_from_exception, - has_logs_enabled, - safe_repr, - to_string, -) - -if TYPE_CHECKING: - from collections.abc import MutableMapping - from logging import LogRecord - from typing import Any, Dict, Optional - -DEFAULT_LEVEL = logging.INFO -DEFAULT_EVENT_LEVEL = logging.ERROR -LOGGING_TO_EVENT_LEVEL = { - logging.NOTSET: "notset", - logging.DEBUG: "debug", - logging.INFO: "info", - logging.WARN: "warning", # WARN is same a WARNING - logging.WARNING: "warning", - logging.ERROR: "error", - logging.FATAL: "fatal", - logging.CRITICAL: "fatal", # CRITICAL is same as FATAL -} - -# Map logging level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - logging.CRITICAL: 21, # fatal - logging.ERROR: 17, # error - logging.WARNING: 13, # warn - logging.INFO: 9, # info - logging.DEBUG: 5, # debug -} - - -# Capturing events from those loggers causes recursion errors. We cannot allow -# the user to unconditionally create events from those loggers under any -# circumstances. -# -# Note: Ignoring by logger name here is better than mucking with thread-locals. -# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. -# -# Events/breadcrumbs and Sentry Logs have separate ignore lists so that -# framework loggers silenced for events (e.g. django.server) can still be -# captured as Sentry Logs. -_IGNORED_LOGGERS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - -_IGNORED_LOGGERS_SENTRY_LOGS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - - -def ignore_logger( - name: str, -) -> None: - """This disables recording (both in breadcrumbs and as events) calls to - a logger of a specific name. Among other uses, many of our integrations - use this to prevent their actions being recorded as breadcrumbs. Exposed - to users as a way to quiet spammy loggers. - - This does **not** affect Sentry Logs — use - :py:func:`ignore_logger_for_sentry_logs` for that. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS.add(name) - - -def ignore_logger_for_sentry_logs( - name: str, -) -> None: - """This disables recording as Sentry Logs calls to a logger of a - specific name. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS_SENTRY_LOGS.add(name) - - -def unignore_logger( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger` call, re-enabling - recording of breadcrumbs and events for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS.discard(name) - - -def unignore_logger_for_sentry_logs( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, - re-enabling recording of Sentry Logs for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) - - -class LoggingIntegration(Integration): - identifier = "logging" - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - self._handler = None - self._breadcrumb_handler = None - self._sentry_logs_handler = None - - if level is not None: - self._breadcrumb_handler = BreadcrumbHandler(level=level) - - if sentry_logs_level is not None: - self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) - - if event_level is not None: - self._handler = EventHandler(level=event_level) - - def _handle_record(self, record: "LogRecord") -> None: - if self._handler is not None and record.levelno >= self._handler.level: - self._handler.handle(record) - - if ( - self._breadcrumb_handler is not None - and record.levelno >= self._breadcrumb_handler.level - ): - self._breadcrumb_handler.handle(record) - - def _handle_sentry_logs_record(self, record: "LogRecord") -> None: - if ( - self._sentry_logs_handler is not None - and record.levelno >= self._sentry_logs_handler.level - ): - self._sentry_logs_handler.handle(record) - - @staticmethod - def setup_once() -> None: - old_callhandlers = logging.Logger.callHandlers - - def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": - # keeping a local reference because the - # global might be discarded on shutdown - ignored_loggers = _IGNORED_LOGGERS - ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS - - try: - return old_callhandlers(self, record) - finally: - # This check is done twice, once also here before we even get - # the integration. Otherwise we have a high chance of getting - # into a recursion error when the integration is resolved - # (this also is slower). - name = record.name.strip() - - handle_events = ( - ignored_loggers is not None and name not in ignored_loggers - ) - handle_sentry_logs = ( - ignored_loggers_sentry_logs is not None - and name not in ignored_loggers_sentry_logs - ) - - if handle_events or handle_sentry_logs: - integration = sentry_sdk.get_client().get_integration( - LoggingIntegration - ) - if integration is not None: - if handle_events: - integration._handle_record(record) - if handle_sentry_logs: - integration._handle_sentry_logs_record(record) - - logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore - - -class _BaseHandler(logging.Handler): - COMMON_RECORD_ATTRS = frozenset( - ( - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "linenno", - "lineno", - "message", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack", - "tags", - "taskName", - "thread", - "threadName", - "stack_info", - ) - ) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - return LOGGING_TO_EVENT_LEVEL.get( - record.levelno, record.levelname.lower() if record.levelname else "" - ) - - def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": - return { - k: v - for k, v in vars(record).items() - if k not in self.COMMON_RECORD_ATTRS - and (not isinstance(k, str) or not k.startswith("_")) - } - - -class EventHandler(_BaseHandler): - """ - A logging handler that emits Sentry events for each log record - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - client_options = client.options - - # exc_info might be None or (None, None, None) - # - # exc_info may also be any falsy value due to Python stdlib being - # liberal with what it receives and Celery's billiard being "liberal" - # with what it sends. See - # https://github.com/getsentry/sentry-python/issues/904 - if record.exc_info and record.exc_info[0] is not None: - event, hint = event_from_exception( - record.exc_info, - client_options=client_options, - mechanism={"type": "logging", "handled": True}, - ) - elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: - event = {} - hint = {} - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=client_options[ - "include_local_variables" - ], - max_value_length=client_options["max_value_length"], - ), - "crashed": False, - "current": True, - } - ] - } - else: - event = {} - hint = {} - - hint["log_record"] = record - - level = self._logging_to_event_level(record) - if level in {"debug", "info", "warning", "error", "critical", "fatal"}: - event["level"] = level # type: ignore[typeddict-item] - event["logger"] = record.name - - if ( - sys.version_info < (3, 11) - and record.name == "py.warnings" - and record.msg == "%s" - ): - # warnings module on Python 3.10 and below sets record.msg to "%s" - # and record.args[0] to the actual warning message. - # This was fixed in https://github.com/python/cpython/pull/30975. - message = record.args[0] - params = () - else: - message = record.msg - params = record.args - - event["logentry"] = { - "message": to_string(message), - "formatted": record.getMessage(), - "params": params, - } - - event["extra"] = self._extra_from_record(record) - - sentry_sdk.capture_event(event, hint=hint) - - -# Legacy name -SentryHandler = EventHandler - - -class BreadcrumbHandler(_BaseHandler): - """ - A logging handler that records breadcrumbs for each log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - sentry_sdk.add_breadcrumb( - self._breadcrumb_from_record(record), hint={"log_record": record} - ) - - def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": - return { - "type": "log", - "level": self._logging_to_event_level(record), - "category": record.name, - "message": record.message, - "timestamp": datetime.fromtimestamp(record.created, timezone.utc), - "data": self._extra_from_record(record), - } - - -class SentryLogsHandler(_BaseHandler): - """ - A logging handler that records Sentry logs for each Python log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS_SENTRY_LOGS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - self._capture_log_from_record(client, record) - - def _capture_log_from_record( - self, client: "BaseClient", record: "LogRecord" - ) -> None: - otel_severity_number, otel_severity_text = _log_level_to_otel( - record.levelno, SEVERITY_TO_OTEL_SEVERITY - ) - project_root = client.options["project_root"] - - attrs: "Any" = self._extra_from_record(record) - attrs["sentry.origin"] = "auto.log.stdlib" - - parameters_set = False - if record.args is not None: - if isinstance(record.args, tuple): - parameters_set = bool(record.args) - for i, arg in enumerate(record.args): - attrs[f"sentry.message.parameter.{i}"] = ( - arg - if isinstance(arg, (str, float, int, bool)) - else safe_repr(arg) - ) - elif isinstance(record.args, dict): - parameters_set = bool(record.args) - for key, value in record.args.items(): - attrs[f"sentry.message.parameter.{key}"] = ( - value - if isinstance(value, (str, float, int, bool)) - else safe_repr(value) - ) - - if parameters_set and isinstance(record.msg, str): - # only include template if there is at least one - # sentry.message.parameter.X set - attrs["sentry.message.template"] = record.msg - - if record.lineno: - attrs["code.line.number"] = record.lineno - - if record.pathname: - if project_root is not None and record.pathname.startswith(project_root): - attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record.pathname - - if record.funcName: - attrs["code.function.name"] = record.funcName - - if record.thread: - attrs["thread.id"] = record.thread - if record.threadName: - attrs["thread.name"] = record.threadName - - if record.process: - attrs["process.pid"] = record.process - if record.processName: - attrs["process.executable.name"] = record.processName - if record.name: - attrs["logger.name"] = record.name - - # noinspection PyProtectedMember - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record.message, - "attributes": attrs, - "time_unix_nano": int(record.created * 1e9), - "trace_id": None, - "span_id": None, - }, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py deleted file mode 100644 index dbb724d9a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/loguru.py +++ /dev/null @@ -1,208 +0,0 @@ -import enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.logging import ( - BreadcrumbHandler, - EventHandler, - _BaseHandler, -) -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import has_logs_enabled, safe_repr - -if TYPE_CHECKING: - from logging import LogRecord - from typing import Any, Optional - -try: - import loguru - from loguru import logger - from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT - - if TYPE_CHECKING: - from loguru import Message -except ImportError: - raise DidNotEnable("LOGURU is not installed") - - -class LoggingLevels(enum.IntEnum): - TRACE = 5 - DEBUG = 10 - INFO = 20 - SUCCESS = 25 - WARNING = 30 - ERROR = 40 - CRITICAL = 50 - - -DEFAULT_LEVEL = LoggingLevels.INFO.value -DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value - - -SENTRY_LEVEL_FROM_LOGURU_LEVEL = { - "TRACE": "DEBUG", - "DEBUG": "DEBUG", - "INFO": "INFO", - "SUCCESS": "INFO", - "WARNING": "WARNING", - "ERROR": "ERROR", - "CRITICAL": "CRITICAL", -} - -# Map Loguru level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - LoggingLevels.CRITICAL: 21, # fatal - LoggingLevels.ERROR: 17, # error - LoggingLevels.WARNING: 13, # warn - LoggingLevels.SUCCESS: 11, # info - LoggingLevels.INFO: 9, # info - LoggingLevels.DEBUG: 5, # debug - LoggingLevels.TRACE: 1, # trace -} - - -class LoguruIntegration(Integration): - identifier = "loguru" - - level: "Optional[int]" = DEFAULT_LEVEL - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL - breadcrumb_format = DEFAULT_FORMAT - event_format = DEFAULT_FORMAT - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - LoguruIntegration.level = level - LoguruIntegration.event_level = event_level - LoguruIntegration.breadcrumb_format = breadcrumb_format - LoguruIntegration.event_format = event_format - LoguruIntegration.sentry_logs_level = sentry_logs_level - - @staticmethod - def setup_once() -> None: - if LoguruIntegration.level is not None: - logger.add( - LoguruBreadcrumbHandler(level=LoguruIntegration.level), - level=LoguruIntegration.level, - format=LoguruIntegration.breadcrumb_format, - ) - - if LoguruIntegration.event_level is not None: - logger.add( - LoguruEventHandler(level=LoguruIntegration.event_level), - level=LoguruIntegration.event_level, - format=LoguruIntegration.event_format, - ) - - if LoguruIntegration.sentry_logs_level is not None: - logger.add( - loguru_sentry_logs_handler, - level=LoguruIntegration.sentry_logs_level, - ) - - -class _LoguruBaseHandler(_BaseHandler): - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - if kwargs.get("level"): - kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( - kwargs.get("level", ""), DEFAULT_LEVEL - ) - - super().__init__(*args, **kwargs) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - try: - return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ - LoggingLevels(record.levelno).name - ].lower() - except (ValueError, KeyError): - return record.levelname.lower() if record.levelname else "" - - -class LoguruEventHandler(_LoguruBaseHandler, EventHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" - - pass - - -class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" - - pass - - -def loguru_sentry_logs_handler(message: "Message") -> None: - # This is intentionally a callable sink instead of a standard logging handler - # since otherwise we wouldn't get direct access to message.record - client = sentry_sdk.get_client() - - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - record = message.record - - if ( - LoguruIntegration.sentry_logs_level is None - or record["level"].no < LoguruIntegration.sentry_logs_level - ): - return - - otel_severity_number, otel_severity_text = _log_level_to_otel( - record["level"].no, SEVERITY_TO_OTEL_SEVERITY - ) - - attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} - - project_root = client.options["project_root"] - if record.get("file"): - if project_root is not None and record["file"].path.startswith(project_root): - attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record["file"].path - - if record.get("line") is not None: - attrs["code.line.number"] = record["line"] - - if record.get("function"): - attrs["code.function.name"] = record["function"] - - if record.get("thread"): - attrs["thread.name"] = record["thread"].name - attrs["thread.id"] = record["thread"].id - - if record.get("process"): - attrs["process.pid"] = record["process"].id - attrs["process.executable.name"] = record["process"].name - - if record.get("name"): - attrs["logger.name"] = record["name"] - - extra = record.get("extra") - if isinstance(extra, dict): - for key, value in extra.items(): - if isinstance(value, (str, int, float, bool)): - attrs[f"sentry.message.parameter.{key}"] = value - else: - attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record["message"], - "attributes": attrs, - "time_unix_nano": int(record["time"].timestamp() * 1e9), - "trace_id": None, - "span_id": None, - } - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py deleted file mode 100644 index 79381fe06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/mcp.py +++ /dev/null @@ -1,876 +0,0 @@ -""" -Sentry integration for MCP (Model Context Protocol) servers. - -This integration instruments MCP servers to create spans for tool, prompt, -and resource handler execution, and captures errors that occur during execution. - -Supports the low-level `mcp.server.lowlevel.Server` API. -""" - -import inspect -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import package_version, safe_serialize - -MCP_PACKAGE_VERSION = package_version("mcp") - -try: - from mcp.server.lowlevel import Server - from mcp.server.streamable_http import ( - StreamableHTTPServerTransport, - ) - - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] - request_ctx, - ) -except ImportError: - raise DidNotEnable("MCP SDK not installed") - -try: - from fastmcp import FastMCP # type: ignore[import-not-found] -except ImportError: - FastMCP = None - -if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - try: - from mcp.server.context import ( - ServerRequestContext, - ) - except ImportError: - ServerRequestContext = None # type: ignore[assignment,misc] -else: - ServerRequestContext = None # type: ignore[assignment,misc] - - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Optional, Tuple, Union - - from starlette.types import Receive, Scope, Send - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -class MCPIntegration(Integration): - identifier = "mcp" - origin = "auto.ai.mcp" - - def __init__(self, include_prompts: bool = True) -> None: - """ - Initialize the MCP integration. - - Args: - include_prompts: Whether to include prompts (tool results and prompt content) - in span data. Requires send_default_pii=True. Default is True. - """ - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """ - Patches MCP server classes to instrument handler execution. - """ - _patch_lowlevel_server() - _patch_handle_request() - - if FastMCP is not None: - _patch_fastmcp() - - -def _get_active_http_scopes( - ctx: "Optional[Any]" = None, -) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return None - - if ( - ctx is None - or not hasattr(ctx, "request") - or ctx.request is None - or "state" not in ctx.request.scope - ): - return None - - return ( - ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), - ctx.request.scope["state"].get("sentry_sdk.current_scope"), - ) - - -def _get_request_context_data( - ctx: "Optional[Any]" = None, -) -> "tuple[Optional[str], Optional[str], str]": - """ - Extract request ID, session ID, and MCP transport type from the request context. - - Returns: - Tuple of (request_id, session_id, mcp_transport). - - request_id: May be None if not available - - session_id: May be None if not available - - mcp_transport: "http", "sse", "stdio" - """ - request_id: "Optional[str]" = None - session_id: "Optional[str]" = None - mcp_transport: str = "stdio" - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return request_id, session_id, mcp_transport - - if ctx is not None: - request_id = ctx.request_id - if hasattr(ctx, "request") and ctx.request is not None: - request = ctx.request - # Detect transport type by checking request characteristics - if hasattr(request, "query_params") and request.query_params.get( - "session_id" - ): - # SSE transport uses query parameter - mcp_transport = "sse" - session_id = request.query_params.get("session_id") - elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): - # StreamableHTTP transport uses header - mcp_transport = "http" - session_id = request.headers.get("mcp-session-id") - - return request_id, session_id, mcp_transport - - -def _get_span_config( - handler_type: str, item_name: str -) -> "tuple[str, str, str, Optional[str]]": - """ - Get span configuration based on handler type. - - Returns: - Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) - Note: result_data_key is None for resources - """ - if handler_type == "tool": - span_data_key = SPANDATA.MCP_TOOL_NAME - mcp_method_name = "tools/call" - result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT - elif handler_type == "prompt": - span_data_key = SPANDATA.MCP_PROMPT_NAME - mcp_method_name = "prompts/get" - result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT - else: # resource - span_data_key = SPANDATA.MCP_RESOURCE_URI - mcp_method_name = "resources/read" - result_data_key = None # Resources don't capture result content - - span_name = f"{mcp_method_name} {item_name}" - return span_data_key, span_name, mcp_method_name, result_data_key - - -def _set_span_input_data( - span: "Union[StreamedSpan, Span]", - handler_name: str, - span_data_key: str, - mcp_method_name: str, - arguments: "dict[str, Any]", - request_id: "Optional[str]", - session_id: "Optional[str]", - mcp_transport: str, -) -> None: - """Set input span data for MCP handlers.""" - - # Set handler identifier - _set_span_data_attribute(span, span_data_key, handler_name) - _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) - - # Set transport/MCP transport type - _set_span_data_attribute( - span, - SPANDATA.NETWORK_TRANSPORT, - "pipe" if mcp_transport == "stdio" else "tcp", - ) - _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) - - # Set request_id if provided - if request_id: - _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) - - # Set session_id if provided - if session_id: - _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) - - # Set request arguments (excluding common request context objects) - for k, v in arguments.items(): - _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) - - -def _extract_tool_result_content(result: "Any") -> "Any": - """ - Extract meaningful content from MCP tool result. - - Tool handlers can return: - - CallToolResult (mcp v2+): Has .content list and optional .structured_content - - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) - - dict (StructuredContent): Return as-is - - list/Iterable (UnstructuredContent): Extract text from content blocks - """ - if result is None: - return None - - # Handle v2 CallToolResult-like objects (has .content list attribute) - if hasattr(result, "content") and isinstance( - getattr(result, "content", None), list - ): - # This is only present when a tool declares an output_schema - structured = getattr(result, "structured_content", None) - if structured is not None: - return structured - return _extract_text_from_content_blocks(result.content) - - # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) - if isinstance(result, tuple) and len(result) == 2: - # Return the structured content (2nd element) - return result[1] - - # Handle StructuredContent: dict - if isinstance(result, dict): - return result - - # Handle UnstructuredContent: iterable of ContentBlock objects - if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): - return _extract_text_from_content_blocks(result) - - return result - - -def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": - texts = [] - try: - for item in content_blocks: - if hasattr(item, "text"): - texts.append(item.text) - elif isinstance(item, dict) and "text" in item: - texts.append(item["text"]) - except Exception: - return content_blocks - return " ".join(texts) if texts else content_blocks - - -def _set_span_output_data( - span: "Union[StreamedSpan, Span]", - result: "Any", - result_data_key: "Optional[str]", - handler_type: str, -) -> None: - """Set output span data for MCP handlers.""" - if result is None: - return - - # Get integration to check PII settings - integration = sentry_sdk.get_client().get_integration(MCPIntegration) - if integration is None: - return - - # Check if we should include sensitive data - should_include_data = should_send_default_pii() and integration.include_prompts - - # For tools, extract the meaningful content - if handler_type == "tool": - extracted = _extract_tool_result_content(result) - if ( - extracted is not None - and should_include_data - and result_data_key is not None - ): - _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) - # Set content count if result is a dict - if isinstance(extracted, dict): - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) - ) - elif handler_type == "prompt": - # For prompts, count messages and set role/content only for single-message prompts - try: - messages: "Optional[list[str]]" = None - message_count = 0 - - # Check if result has messages attribute (GetPromptResult) - if hasattr(result, "messages") and result.messages: - messages = result.messages - message_count = len(messages) - # Also check if result is a dict with messages - elif isinstance(result, dict) and result.get("messages"): - messages = result["messages"] - message_count = len(messages) - - # Always set message count if we found messages - if message_count > 0: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count - ) - - # Only set role and content for single-message prompts if PII is allowed - if message_count == 1 and should_include_data and messages: - first_message = messages[0] - # Extract role - role = None - if hasattr(first_message, "role"): - role = first_message.role - elif isinstance(first_message, dict) and "role" in first_message: - role = first_message["role"] - - if role: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role - ) - - # Extract content text - content_text = None - if hasattr(first_message, "content"): - msg_content = first_message.content - # Content can be a TextContent object or similar - if hasattr(msg_content, "text"): - content_text = msg_content.text - elif isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - elif isinstance(first_message, dict) and "content" in first_message: - msg_content = first_message["content"] - if isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - - if content_text and result_data_key is not None: - _set_span_data_attribute(span, result_data_key, content_text) - except Exception: - # Silently ignore if we can't extract message info - pass - # Resources don't capture result content (result_data_key is None) - - -# Handler data preparation and wrapping - - -def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: - """Check if original_args contains a v2 ServerRequestContext as the first element.""" - return ( - ServerRequestContext is not None - and bool(original_args) - and isinstance(original_args[0], ServerRequestContext) - ) - - -def _extract_handler_data_from_params( - handler_type: str, - params: "Any", -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from a v2 typed params object. - - In MCP SDK v2, handlers receive (ctx, params) where params is a typed - Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). - """ - if handler_type == "tool": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - elif handler_type == "prompt": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - arguments = {"name": handler_name, **arguments} - else: # resource - handler_name = str(getattr(params, "uri", "unknown")) - arguments = {} - - return handler_name, arguments - - -def _extract_handler_data_from_args( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from v1 positional args. - - In MCP SDK v1, handlers receive positional args: - - Tool: (tool_name, arguments) - - Prompt: (name, arguments) - - Resource: (uri,) - """ - original_kwargs = original_kwargs or {} - - if handler_type == "tool": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - elif handler_type == "prompt": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - arguments = {"name": handler_name, **(arguments or {})} - - else: # resource - handler_name = "unknown" - if original_args: - handler_name = str(original_args[0]) - elif original_kwargs.get("uri"): - handler_name = str(original_kwargs["uri"]) - - arguments = {} - - return handler_name, arguments - - -def _prepare_handler_data( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - params: "Optional[Any]" = None, -) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": - """ - Prepare common handler data for both v1 and v2 MCP SDK. - - Args: - handler_type: "tool", "prompt", or "resource" - original_args: Original positional args (v1 path) - original_kwargs: Original keyword args (v1 path) - params: Typed params object from v2 ServerRequestContext path - - Returns: - Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) - """ - if params is not None: - handler_name, arguments = _extract_handler_data_from_params( - handler_type, params - ) - elif _is_v2_context(original_args): - handler_name = "unknown" - arguments = {} - else: - handler_name, arguments = _extract_handler_data_from_args( - handler_type, original_args, original_kwargs - ) - - span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( - handler_type, handler_name - ) - - return ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) - - -async def _handler_wrapper( - handler_type: str, - func: "Callable[..., Any]", - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - self: "Optional[Any]" = None, - force_await: bool = True, -) -> "Any": - """ - Wrapper for MCP handlers. - - Args: - handler_type: "tool", "prompt", or "resource" - func: The handler function to wrap - original_args: Original arguments passed to the handler - original_kwargs: Original keyword arguments passed to the handler - self: Optional instance for bound methods - """ - if original_kwargs is None: - original_kwargs = {} - - # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers - ctx: "Optional[Any]" = None - params: "Optional[Any]" = None - if ( - ServerRequestContext is not None - and original_args - and isinstance(original_args[0], ServerRequestContext) - ): - ctx = original_args[0] - params = original_args[1] if len(original_args) > 1 else None - - ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) = _prepare_handler_data( - handler_type, original_args, original_kwargs, params=params - ) - - scopes = _get_active_http_scopes(ctx=ctx) - - isolation_scope_context: "ContextManager[Any]" - current_scope_context: "ContextManager[Any]" - - if scopes is None: - isolation_scope_context = nullcontext() - current_scope_context = nullcontext() - else: - isolation_scope, current_scope = scopes - - isolation_scope_context = ( - nullcontext() - if isolation_scope is None - else sentry_sdk.scope.use_isolation_scope(isolation_scope) - ) - current_scope_context = ( - nullcontext() - if current_scope is None - else sentry_sdk.scope.use_scope(current_scope) - ) - - # Get request ID, session ID, and transport from context - request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - - # Start span and execute - with isolation_scope_context: - with current_scope_context: - span_mgr: "Union[Span, StreamedSpan]" - if span_streaming: - span_mgr = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.MCP_SERVER, - "sentry.origin": MCPIntegration.origin, - }, - ) - else: - span_mgr = get_start_span_function()( - op=OP.MCP_SERVER, - name=span_name, - origin=MCPIntegration.origin, - ) - - with span_mgr as span: - # Set input span data - _set_span_input_data( - span, - handler_name, - span_data_key, - mcp_method_name, - arguments, - request_id, - session_id, - mcp_transport, - ) - - # For resources, extract and set protocol - if handler_type == "resource": - uri = None - if params is not None: - uri = getattr(params, "uri", None) - - # v1 scenario - if ServerRequestContext is None: - if original_args: - uri = original_args[0] - else: - uri = original_kwargs.get("uri") - - protocol = None - if uri is not None and hasattr(uri, "scheme"): - protocol = uri.scheme - elif handler_name and "://" in handler_name: - protocol = handler_name.split("://")[0] - if protocol: - _set_span_data_attribute( - span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol - ) - - try: - # Execute the async handler - if self is not None: - original_args = (self, *original_args) - - result = func(*original_args, **original_kwargs) - if force_await or inspect.isawaitable(result): - result = await result - - except Exception as e: - # Set error flag for tools - if handler_type == "tool": - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True - ) - sentry_sdk.capture_exception(e) - raise - - _set_span_output_data(span, result, result_data_key, handler_type) - - return result - - -def _create_instrumented_decorator( - original_decorator: "Callable[..., Any]", - handler_type: str, - *decorator_args: "Any", - **decorator_kwargs: "Any", -) -> "Callable[..., Any]": - """ - Create an instrumented version of an MCP decorator. - - This function intercepts MCP decorators (like @server.call_tool()) and injects - Sentry instrumentation into the handler registration flow. The returned decorator - will: - 1. Receive the user's handler function - 2. Pass the instrumented version to the original MCP decorator - - This ensures that when the handler is called at runtime, it's already wrapped - with Sentry spans and metrics collection. - - Args: - original_decorator: The original MCP decorator method (e.g., Server.call_tool) - handler_type: "tool", "prompt", or "resource" - determines span configuration - decorator_args: Positional arguments to pass to the original decorator (e.g., self) - decorator_kwargs: Keyword arguments to pass to the original decorator - - Returns: - A decorator function that instruments handlers before registering them - """ - - def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(func) - async def wrapper(*args: "Any") -> "Any": - return await _handler_wrapper(handler_type, func, args, force_await=False) - - # Then register it with the original MCP decorator - return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) - - return instrumented_decorator - - -_METHOD_TO_HANDLER_TYPE = { - "tools/call": "tool", - "prompts/get": "prompt", - "resources/read": "resource", -} - -# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via -# the Server(...) constructor kwargs rather than add_request_handler. The in-tree -# high-level MCPServer also wires its handlers through these kwargs. -_KWARG_TO_HANDLER_TYPE = { - "on_call_tool": "tool", - "on_get_prompt": "prompt", - "on_read_resource": "resource", -} - - -def _patch_lowlevel_server() -> None: - """ - Patches the mcp.server.lowlevel.Server class to instrument handler execution. - """ - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - _patch_lowlevel_server_v2() - else: - _patch_lowlevel_server_v1() - - -def _patch_lowlevel_server_v1() -> None: - """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" - # Patch call_tool decorator - original_call_tool = Server.call_tool # type: ignore[attr-defined] - - def patched_call_tool( - self: "Server", **kwargs: "Any" - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.call_tool that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_call_tool, "tool", self, **kwargs - )(func) - - Server.call_tool = patched_call_tool # type: ignore[attr-defined] - - # Patch get_prompt decorator - original_get_prompt = Server.get_prompt # type: ignore[attr-defined] - - def patched_get_prompt( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.get_prompt that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_get_prompt, "prompt", self - )(func) - - Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] - - # Patch read_resource decorator - original_read_resource = Server.read_resource # type: ignore[attr-defined] - - def patched_read_resource( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.read_resource that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_read_resource, "resource", self - )(func) - - Server.read_resource = patched_read_resource # type: ignore[attr-defined] - - -def _wrap_v2_handler( - handler_type: str, handler: "Callable[..., Any]" -) -> "Callable[..., Any]": - """Wrap a v2 (ctx, params) handler with Sentry instrumentation. - - Idempotent: an already-wrapped handler is returned unchanged so handlers - registered through more than one path (e.g. MCPServer building a Server) - are not double-wrapped. - """ - if getattr(handler, "__sentry_mcp_wrapped__", False): - return handler - - @wraps(handler) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - return await _handler_wrapper( - handler_type, handler, args, kwargs, force_await=False - ) - - wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] - return wrapper - - -def _patch_lowlevel_server_v2() -> None: - """Patches the v2 Server to wrap tool/prompt/resource handlers. - - Handlers can be registered either via the Server(...) constructor kwargs - (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree - MCPServer and most lowlevel examples use — or via add_request_handler. - Both are patched. - """ - original_init = Server.__init__ - - @wraps(original_init) - def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: - for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): - handler = kwargs.get(kwarg) - if handler is not None: - kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) - original_init(self, *args, **kwargs) - - Server.__init__ = patched_init # type: ignore[method-assign] - - original_add_request_handler = Server.add_request_handler - - def patched_add_request_handler( - self: "Server", - method: str, - params_type: "Any", - handler: "Callable[..., Any]", - *args: "Any", - **kwargs: "Any", - ) -> None: - handler_type = _METHOD_TO_HANDLER_TYPE.get(method) - if handler_type is not None: - handler = _wrap_v2_handler(handler_type, handler) - - original_add_request_handler( - self, method, params_type, handler, *args, **kwargs - ) - - Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] - - -def _patch_handle_request() -> None: - original_handle_request = StreamableHTTPServerTransport.handle_request - - @wraps(original_handle_request) - async def patched_handle_request( - self: "StreamableHTTPServerTransport", - scope: "Scope", - receive: "Receive", - send: "Send", - ) -> None: - scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( - sentry_sdk.get_isolation_scope() - ) - scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() - await original_handle_request(self, scope, receive, send) - - StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] - - -def _patch_fastmcp() -> None: - """ - Patches the standalone fastmcp package's FastMCP class. - - The standalone fastmcp package (v2.14.0+) registers its own handlers for - prompts and resources directly, bypassing the Server decorators we patch. - This function patches the _get_prompt_mcp and _read_resource_mcp methods - to add instrumentation for those handlers. - """ - if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): - original_get_prompt_mcp = FastMCP._get_prompt_mcp - - @wraps(original_get_prompt_mcp) - async def patched_get_prompt_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "prompt", - original_get_prompt_mcp, - args, - kwargs, - self, - ) - - FastMCP._get_prompt_mcp = patched_get_prompt_mcp - - if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): - original_read_resource_mcp = FastMCP._read_resource_mcp - - @wraps(original_read_resource_mcp) - async def patched_read_resource_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "resource", - original_read_resource_mcp, - args, - kwargs, - self, - ) - - FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py deleted file mode 100644 index b6111492bb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/modules.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import _get_installed_modules - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - - -class ModulesIntegration(Integration): - identifier = "modules" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Any") -> "Event": - if event.get("type") == "transaction": - return event - - if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: - return event - - event["modules"] = _get_installed_modules() - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py deleted file mode 100644 index 186c665ed1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai.py +++ /dev/null @@ -1,1530 +0,0 @@ -import json -import sys -import time -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai._openai_completions_api import ( - _get_system_instructions as _get_system_instructions_completions, -) -from sentry_sdk.ai._openai_completions_api import ( - _get_text_items, - _transform_system_instructions, -) -from sentry_sdk.ai._openai_completions_api import ( - _is_system_instruction as _is_system_instruction_completions, -) -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions as _get_system_instructions_responses, -) -from sentry_sdk.ai._openai_responses_api import ( - _is_system_instruction as _is_system_instruction_responses, -) -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, - safe_serialize, -) - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Iterable, - Iterator, - List, - Optional, - Union, - ) - - from openai import Omit - from openai.types import CompletionUsage - from openai.types.responses import ( - ResponseInputParam, - ResponseStreamEvent, - SequenceNotStr, - ) - from openai.types.responses.response_usage import ResponseUsage - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -try: - try: - from openai import NotGiven - except ImportError: - NotGiven = None - - try: - from openai import Omit - except ImportError: - Omit = None - - from openai import AsyncStream, Stream - from openai.resources import AsyncEmbeddings, Embeddings - from openai.resources.chat.completions import AsyncCompletions, Completions - - if TYPE_CHECKING: - from openai.types.chat import ( - ChatCompletionChunk, - ChatCompletionMessageParam, - ) -except ImportError: - raise DidNotEnable("OpenAI not installed") - -RESPONSES_API_ENABLED = True -try: - # responses API support was introduced in v1.66.0 - from openai.resources.responses import AsyncResponses, Responses - from openai.types.responses.response_completed_event import ResponseCompletedEvent -except ImportError: - RESPONSES_API_ENABLED = False - - -class OpenAIIntegration(Integration): - identifier = "openai" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "OpenAIIntegration", - include_prompts: bool = True, - tiktoken_encoding_name: "Optional[str]" = None, - ) -> None: - self.include_prompts = include_prompts - - self.tiktoken_encoding = None - if tiktoken_encoding_name is not None: - import tiktoken # type: ignore - - self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) - - @staticmethod - def setup_once() -> None: - Completions.create = _wrap_chat_completion_create(Completions.create) - AsyncCompletions.create = _wrap_async_chat_completion_create( - AsyncCompletions.create - ) - - Embeddings.create = _wrap_embeddings_create(Embeddings.create) - AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) - - if RESPONSES_API_ENABLED: - Responses.create = _wrap_responses_create(Responses.create) - AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) - - def count_tokens(self: "OpenAIIntegration", s: str) -> int: - if self.tiktoken_encoding is None: - return 0 - try: - return len(self.tiktoken_encoding.encode_ordinary(s)) - except Exception: - return 0 - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _has_attr_and_is_int( - token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str -) -> bool: - return hasattr(token_usage, attr_name) and isinstance( - getattr(token_usage, attr_name, None), int - ) - - -def _calculate_completions_token_usage( - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - streaming_message_total_token_usage: "Optional[CompletionUsage]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Chat Completions API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - usage = None - - if streaming_message_total_token_usage is not None: - usage = streaming_message_total_token_usage - elif hasattr(response, "usage"): - usage = response.usage - - if usage is not None: - if _has_attr_and_is_int(usage, "prompt_tokens"): - input_tokens = usage.prompt_tokens - if _has_attr_and_is_int(usage, "completion_tokens"): - output_tokens = usage.completion_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "prompt_tokens_details"): - cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "completion_tokens_details"): - reasoning = getattr( - usage.completion_tokens_details, "reasoning_tokens", None - ) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in messages or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "choices") and response.choices is not None: - for choice in response.choices: - if hasattr(choice, "message") and hasattr(choice.message, "content"): - output_tokens += count_tokens(choice.message.content) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _calculate_responses_token_usage( - input: "Any", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Responses API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - - if hasattr(response, "usage"): - usage = response.usage - - if _has_attr_and_is_int(usage, "input_tokens"): - input_tokens = usage.input_tokens - if _has_attr_and_is_int(usage, "output_tokens"): - output_tokens = usage.output_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "input_tokens_details"): - cached = getattr(usage.input_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "output_tokens_details"): - reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in input or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - # Deliberate use of Completions function for both Completions and Responses input format. - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "output"): - for output_item in response.output: - if hasattr(output_item, "content"): - for content_item in output_item.content: - if hasattr(content_item, "text"): - output_tokens += count_tokens(content_item.text) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _set_responses_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") - messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_output_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - conversation = kwargs.get("conversation") - if conversation is not None and _is_given(conversation): - conversation_id: "Optional[str]" = None - if isinstance(conversation, str): - conversation_id = conversation - elif isinstance(conversation, dict): - conversation_id = conversation.get("id") - if conversation_id is not None: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - if not should_send_default_pii() or not integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if ( - messages is None - and explicit_instructions is not None - and _is_given(explicit_instructions) - ): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - [ - { - "type": "text", - "content": explicit_instructions, - } - ] - ), - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if messages is None: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - instructions_text_parts: "list[TextPart]" = [] - if explicit_instructions is not None and _is_given(explicit_instructions): - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions_responses(messages) - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - non_system_messages = [ - message for message in messages if not _is_system_instruction_responses(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - - -def _set_completions_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( - "messages" - ) - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - presence_penalty = kwargs.get("presence_penalty") - if presence_penalty is not None and _is_given(presence_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) - - frequency_penalty = kwargs.get("frequency_penalty") - if frequency_penalty is not None and _is_given(frequency_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - messages = list(messages) - kwargs["messages"] = messages - - system_instructions = _get_system_instructions_completions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - non_system_messages = [ - message - for message in messages - if not _is_system_instruction_completions(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - -def _set_embeddings_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( - "input" - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - return - - if isinstance(messages, str): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - return - - messages = list(messages) - kwargs["input"] = messages - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - -def _set_common_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - input: "Any", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - if hasattr(response, "model"): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) - - # Chat Completions API - if hasattr(response, "choices") and response.choices is not None: - if should_send_default_pii() and integration.include_prompts: - response_text = [ - choice.message.model_dump() - for choice in response.choices - if choice.message is not None - ] - if len(response_text) > 0: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) - - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - # Responses API - elif hasattr(response, "output"): - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in response.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - # Embeddings API (fallback for responses with neither choices nor output) - else: - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_synchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_asynchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _set_completions_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - _set_common_output_data( - span, - response, - messages, - integration, - finish_span, - ) - - -def _wrap_synchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Stream[ChatCompletionChunk]", - old_iterator: "Iterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "Iterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "AsyncStream[ChatCompletionChunk]", - old_iterator: "AsyncIterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "AsyncIterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - async for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -def _wrap_synchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "Stream[ResponseStreamEvent]", - old_iterator: "Iterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "Iterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "AsyncStream[ResponseStreamEvent]", - old_iterator: "AsyncIterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "AsyncIterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft: "Optional[float]" = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - async for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _set_responses_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _set_embeddings_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - return _new_sync_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - return await _new_async_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -async def _new_async_embeddings_create( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -def _wrap_embeddings_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_embeddings_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_synchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_asynchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _wrap_responses_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_responses_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_responses_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_responses_create(f, *args, **kwargs) - - return _sentry_patched_responses_async - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different openai versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py deleted file mode 100644 index 5895f53ad3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/__init__.py +++ /dev/null @@ -1,250 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .patches import ( - _create_run_streamed_wrapper, - _create_run_wrapper, - _execute_final_output, - _execute_handoffs, - _get_all_tools, - _get_model, - _patch_error_tracing, - _run_single_turn, - _run_single_turn_streamed, -) - -try: - # "agents" is too generic. If someone has an agents.py file in their project - # or another package that's importable via "agents", no ImportError would - # be thrown and the integration would enable itself even if openai-agents is - # not installed. That's why we're adding the second, more specific import - # after it, even if we don't use it. - import agents - from agents.run import AgentRunner - from agents.version import __version__ as OPENAI_AGENTS_VERSION - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -try: - # AgentRunner methods moved in v0.8 - # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop, turn_preparation, turn_resolution -except ImportError: - run_loop = None - turn_preparation = None - turn_resolution = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - from agents.run_internal.run_steps import SingleStepResult - - -def _patch_runner() -> None: - # Create the root span for one full agent run (including eventual handoffs) - # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around - # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. - agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run - ) - - # Patch streaming runner - agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run_streamed - ) - - -class OpenAIAgentsIntegration(Integration): - """ - NOTE: With version 0.8.0, the class methods below have been refactored to functions. - - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` - - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` - - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` - - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` - - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` - - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. - 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. - - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. - - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. - 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. - - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. - - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. - 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. - - Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. - Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. - An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. - Handoffs are a way to switch agent-wide configuration. - - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` - """ - - identifier = "openai_agents" - - @staticmethod - def setup_once() -> None: - _patch_error_tracing() - _patch_runner() - - library_version = parse_version(OPENAI_AGENTS_VERSION) - if library_version is not None and library_version >= ( - 0, - 8, - ): - if run_loop is not None: - - @wraps(run_loop.get_all_tools) - async def new_wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools( - run_loop.get_all_tools, agent, context_wrapper - ) - - agents.run.get_all_tools = new_wrapped_get_all_tools - - @wraps(run_loop.run_single_turn) - async def new_wrapped_run_single_turn( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn( - run_loop.run_single_turn, *args, **kwargs - ) - - agents.run.run_single_turn = new_wrapped_run_single_turn - - @wraps(run_loop.run_single_turn_streamed) - async def new_wrapped_run_single_turn_streamed( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - run_loop.run_single_turn_streamed, *args, **kwargs - ) - - agents.run.run_single_turn_streamed = ( - new_wrapped_run_single_turn_streamed - ) - - if turn_preparation is not None: - - @wraps(turn_preparation.get_model) - def new_wrapped_get_model( - agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(turn_preparation.get_model, agent, run_config) - - agents.run_internal.run_loop.get_model = new_wrapped_get_model - - if turn_resolution is not None: - original_execute_handoffs = turn_resolution.execute_handoffs - - @wraps(original_execute_handoffs) - async def new_wrapped_execute_handoffs( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs( - original_execute_handoffs, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_handoffs = ( - new_wrapped_execute_handoffs - ) - - original_execute_final_output = turn_resolution.execute_final_output - - @wraps(turn_resolution.execute_final_output) - async def new_wrapped_final_output( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_final_output = ( - new_wrapped_final_output - ) - - return - - original_get_all_tools = AgentRunner._get_all_tools - - @wraps(AgentRunner._get_all_tools.__func__) - async def old_wrapped_get_all_tools( - cls: "agents.Runner", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) - - agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) - - original_get_model = AgentRunner._get_model - - @wraps(AgentRunner._get_model.__func__) - def old_wrapped_get_model( - cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(original_get_model, agent, run_config) - - agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) - - original_run_single_turn = AgentRunner._run_single_turn - - @wraps(AgentRunner._run_single_turn.__func__) - async def old_wrapped_run_single_turn( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn(original_run_single_turn, *args, **kwargs) - - agents.run.AgentRunner._run_single_turn = classmethod( - old_wrapped_run_single_turn - ) - - original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed - - @wraps(AgentRunner._run_single_turn_streamed.__func__) - async def old_wrapped_run_single_turn_streamed( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - original_run_single_turn_streamed, *args, **kwargs - ) - - agents.run.AgentRunner._run_single_turn_streamed = classmethod( - old_wrapped_run_single_turn_streamed - ) - - original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs - - @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) - async def old_wrapped_execute_handoffs( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) - - agents._run_impl.RunImpl.execute_handoffs = classmethod( - old_wrapped_execute_handoffs - ) - - original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - - @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) - async def old_wrapped_final_output( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents._run_impl.RunImpl.execute_final_output = classmethod( - old_wrapped_final_output - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py deleted file mode 100644 index f5de978be0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py deleted file mode 100644 index 85d48f2d41..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .agent_run import ( - _execute_final_output, # noqa: F401 - _execute_handoffs, # noqa: F401 - _run_single_turn, # noqa: F401 - _run_single_turn_streamed, # noqa: F401 -) -from .error_tracing import _patch_error_tracing # noqa: F401 -from .models import _get_model # noqa: F401 -from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 -from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py deleted file mode 100644 index 71883b2eef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ /dev/null @@ -1,332 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import ( - handoff_span, - invoke_agent_span, - update_invoke_agent_span, -) - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, Union - - from agents.run_internal.run_steps import SingleStepResult - - from sentry_sdk.tracing import Span - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: - """Check if there's an active agent span for this context""" - return getattr(context_wrapper, "_sentry_current_agent", None) is not None - - -def _get_current_agent( - context_wrapper: "agents.RunContextWrapper", -) -> "Optional[agents.Agent]": - """Get the current agent from context wrapper""" - return getattr(context_wrapper, "_sentry_current_agent", None) - - -def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: - """Close the workflow span for streaming executions if it exists.""" - if agent and hasattr(agent, "_sentry_workflow_span"): - workflow_span = agent._sentry_workflow_span - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - -def _maybe_start_agent_span( - context_wrapper: "agents.RunContextWrapper", - agent: "agents.Agent", - should_run_agent_start_hooks: bool, - span_kwargs: "dict[str, Any]", - is_streaming: bool = False, -) -> "Optional[Union[Span, StreamedSpan]]": - """ - Start an agent invocation span if conditions are met. - Handles ending any existing span for a different agent. - - Returns the new span if started, or the existing span if conditions aren't met. - """ - if not (should_run_agent_start_hooks and agent and context_wrapper): - return getattr(context_wrapper, "_sentry_agent_span", None) - - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - # Store the agent on the context wrapper so we can access it later - context_wrapper._sentry_current_agent = agent - span = invoke_agent_span(context_wrapper, agent, span_kwargs) - context_wrapper._sentry_agent_span = span - agent._sentry_agent_span = span - - if not is_streaming: - return span - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - return span - - -async def _run_single_turn( - original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn that - - creates agent invocation spans if there is no already active agent invocation span. - - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. - """ - # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. - bindings = kwargs.get("bindings") - agent = ( - getattr(bindings, "public_agent", None) - if bindings is not None - else kwargs.get("agent") - ) - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) - - span = _maybe_start_agent_span( - context_wrapper, agent, should_run_agent_start_hooks, kwargs - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn(*args, **kwargs) - - try: - result = await original_run_single_turn(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - return result - - -async def _run_single_turn_streamed( - original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn_streamed that - - creates agent invocation spans for streaming if there is no already active agent invocation span. - - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. - - Note: Unlike _run_single_turn which uses keyword-only arguments (*,), - _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: - _run_single_turn_streamed( - streamed_result, # args[0] - bindings, # args[1] - hooks, # args[2] - context_wrapper, # args[3] - run_config, # args[4] - should_run_agent_start_hooks, # args[5] - tool_use_tracker, # args[6] - all_tools, # args[7] - server_conversation_tracker, # args[8] (optional) - ) - """ - streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") - # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. - agent_or_bindings = ( - args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) - ) - agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) - context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") - should_run_agent_start_hooks = bool( - args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) - ) - - span_kwargs: "dict[str, Any]" = {} - if streamed_result and hasattr(streamed_result, "input"): - span_kwargs["original_input"] = streamed_result.input - - span = _maybe_start_agent_span( - context_wrapper, - agent, - should_run_agent_start_hooks, - span_kwargs, - is_streaming=True, - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn_streamed(*args, **kwargs) - - try: - result = await original_run_single_turn_streamed(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - return result - - -async def _execute_handoffs( - original_execute_handoffs: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_handoffs that - - creates and manages handoff spans. - - ends the agent invocation span. - - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. - """ - - context_wrapper = kwargs.get("context_wrapper") - run_handoffs = kwargs.get("run_handoffs") - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - - # Create Sentry handoff span for the first handoff (agents library only processes the first one) - if run_handoffs: - first_handoff = run_handoffs[0] - handoff_agent_name = first_handoff.handoff.agent_name - handoff_span(context_wrapper, agent, handoff_agent_name) - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - # Call original method with all parameters - try: - return await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - # Call original method with all parameters - try: - result = await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result - - -async def _execute_final_output( - original_execute_final_output: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_final_output that - - ends the agent invocation span. - - ends the workflow span if the response is streamed. - """ - - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - context_wrapper = kwargs.get("context_wrapper") - final_output = kwargs.get("final_output") - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - try: - return await original_execute_final_output(*args, **kwargs) - finally: - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - - try: - result = await original_execute_final_output(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py deleted file mode 100644 index 68dadb3101..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py +++ /dev/null @@ -1,74 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANSTATUS -from sentry_sdk.traces import SpanStatus -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -def _patch_error_tracing() -> None: - """ - Patches agents error tracing function to inject our span error logic - when a tool execution fails. - - In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span - In older versions, it was at: agents._utils.attach_error_to_current_span - - This works even when the module or function doesn't exist. - """ - error_tracing_module = None - - # Try newer location first (agents.util._error_tracing) - try: - from agents.util import _error_tracing - - error_tracing_module = _error_tracing - except (ImportError, AttributeError): - pass - - # Try older location (agents._utils) - if error_tracing_module is None: - try: - import agents._utils - - error_tracing_module = agents._utils - except (ImportError, AttributeError): - # Module doesn't exist in either location, nothing to patch - return - - # Check if the function exists - if not hasattr(error_tracing_module, "attach_error_to_current_span"): - return - - original_attach_error = error_tracing_module.attach_error_to_current_span - - @wraps(original_attach_error) - def sentry_attach_error_to_current_span( - error: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Wraps agents' error attachment to also set Sentry span status to error. - This allows us to properly track tool execution errors even though - the agents library swallows exceptions. - """ - # Set the current Sentry span to errored - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - current_span = sentry_sdk.get_current_scope().streamed_span - if current_span is not None: - current_span.status = SpanStatus.ERROR - else: - current_span = sentry_sdk.get_current_span() - if current_span is not None: - current_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # Call the original function - return original_attach_error(error, *args, **kwargs) - - error_tracing_module.attach_error_to_current_span = ( - sentry_attach_error_to_current_span - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py deleted file mode 100644 index 634c9fdca1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/models.py +++ /dev/null @@ -1,197 +0,0 @@ -import copy -import time -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_sentry_baggage_to_headers, - should_propagate_trace, -) -from sentry_sdk.utils import logger - -from ..spans import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - from sentry_sdk.tracing import Span - -try: - import agents - from agents.tool import HostedMCPTool -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _set_response_model_on_agent_span( - agent: "agents.Agent", response_model: "Optional[str]" -) -> None: - """Set the response model on the agent's invoke_agent span if available.""" - if response_model: - agent_span = getattr(agent, "_sentry_agent_span", None) - if agent_span: - if isinstance(agent_span, StreamedSpan): - agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - else: - agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - -def _inject_trace_propagation_headers( - hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" -) -> None: - headers = hosted_tool.tool_config.get("headers") - if headers is None: - headers = {} - hosted_tool.tool_config["headers"] = headers - - mcp_url = hosted_tool.tool_config.get("server_url") - if not mcp_url: - return - - if should_propagate_trace(sentry_sdk.get_client(), mcp_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( - key=key, value=value, mcp_url=mcp_url - ) - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(headers, value) - else: - headers[key] = value - - -def _get_model( - original_get_model: "Callable[..., agents.Model]", - agent: "agents.Agent", - run_config: "agents.RunConfig", -) -> "agents.Model": - """ - Responsible for - - creating and managing AI client spans. - - adding trace propagation headers to tools with type HostedMCPTool. - - setting the response model on agent invocation spans. - """ - # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) - # because we only patch its direct methods, all underlying data can remain unchanged. - model = copy.copy(original_get_model(agent, run_config)) - - # Capture the request model name for spans (agent.model can be None when using defaults) - request_model_name = model.model if hasattr(model, "model") else str(model) - agent._sentry_request_model = request_model_name - - # Wrap _fetch_response if it exists (for OpenAI models) to capture response model - if hasattr(model, "_fetch_response"): - original_fetch_response = model._fetch_response - - @wraps(original_fetch_response) - async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": - response = await original_fetch_response(*args, **kwargs) - if hasattr(response, "model") and response.model: - agent._sentry_response_model = str(response.model) - return response - - model._fetch_response = wrapped_fetch_response - - original_get_response = model.get_response - - @wraps(original_get_response) - async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": - mcp_tools = kwargs.get("tools") - hosted_tools = [] - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - result = await original_get_response(*args, **kwargs) - - # Get response model captured from _fetch_response and clean up - response_model = getattr(agent, "_sentry_response_model", None) - if response_model: - delattr(agent, "_sentry_response_model") - - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span(span, result, response_model, agent) - - return result - - model.get_response = wrapped_get_response - - # Also wrap stream_response for streaming support - if hasattr(model, "stream_response"): - original_stream_response = model.stream_response - - @wraps(original_stream_response) - async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": - span_kwargs = dict(kwargs) - if len(args) > 0: - span_kwargs["system_instructions"] = args[0] - if len(args) > 1: - span_kwargs["input"] = args[1] - - hosted_tools = [] - if len(args) > 3: - mcp_tools = args[3] - - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, span_kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - streaming_response = None - ttft_recorded = False - # Capture start time locally to avoid race conditions with concurrent requests - start_time = time.perf_counter() - - async for event in original_stream_response(*args, **kwargs): - # Detect first content token (text delta event) - if not ttft_recorded and hasattr(event, "delta"): - ttft = time.perf_counter() - start_time - set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) - ttft_recorded = True - - # Capture the full response from ResponseCompletedEvent - if hasattr(event, "response"): - streaming_response = event.response - yield event - - # Update span with response data (usage, output, model) - if streaming_response: - response_model = ( - str(streaming_response.model) - if hasattr(streaming_response, "model") - and streaming_response.model - else None - ) - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span( - span, streaming_response, response_model, agent - ) - - model.stream_response = wrapped_stream_response - - return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py deleted file mode 100644 index 5f9996595f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/runner.py +++ /dev/null @@ -1,221 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import agent_workflow_span, update_invoke_agent_span -from ..utils import _capture_exception - -try: - from agents.exceptions import AgentsException -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, AsyncIterator, Callable - - -def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run methods to - - create and manage a root span for the agent workflow runs. - - end the agent invocation span if an `AgentsException` is raised in `run()`. - - Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), - so it does not need to be wrapped separately. - """ - - @wraps(original_func) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - with agent_workflow_span(agent) as workflow_span: - # Set conversation ID on workflow span early so it's captured even on errors - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - run_result = await original_func(*args, **kwargs) - except AgentsException as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - - context_wrapper = getattr(exc.run_data, "context_wrapper", None) - if context_wrapper is not None: - invoke_agent_span = getattr( - context_wrapper, "_sentry_agent_span", None - ) - - if invoke_agent_span is not None and ( - ( - isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.end_timestamp is None - ) - or ( - not isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.timestamp is None - ) - ): - update_invoke_agent_span( - span=invoke_agent_span, - context=context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Invoke agent span is not finished in this case. - # This is much less likely to occur than other cases because - # AgentRunner.run() is "just" a while loop around _run_single_turn. - _capture_exception(exc) - reraise(*exc_info) - - invoke_agent_span = getattr( - run_result.context_wrapper, "_sentry_agent_span", None - ) - if not invoke_agent_span: - return run_result - - update_invoke_agent_span( - span=invoke_agent_span, - context=run_result.context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(None, None, None) - delattr(run_result.context_wrapper, "_sentry_agent_span") - return run_result - - return wrapper - - -def _create_run_streamed_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run_streamed method to - - create a root span for streaming agent workflow runs. - - end the workflow span if and only if the response stream is consumed or cancelled. - - Unlike run(), run_streamed() returns immediately with a RunResultStreaming object - while execution continues in a background task. The workflow span must stay open - throughout the streaming operation and close when streaming completes or is abandoned. - - Note: We don't use isolation_scope() here because it uses context variables that - cannot span async boundaries (the __enter__ and __exit__ would be called from - different async contexts, causing ValueError). - """ - - @wraps(original_func) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - # Capture conversation_id from kwargs if provided - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - # Start workflow span immediately (before run_streamed returns) - workflow_span = agent_workflow_span(agent) - workflow_span.__enter__() - - # Set conversation ID on workflow span early so it's captured even on errors - if conversation_id: - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - # Store span on agent for cleanup - agent._sentry_workflow_span = workflow_span - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - # Call original function to get RunResultStreaming - run_result = original_func(*args, **kwargs) - except Exception as exc: - # If run_streamed itself fails (not the background task), clean up immediately - workflow_span.__exit__(*sys.exc_info()) - _capture_exception(exc) - raise - - def _close_workflow_span() -> None: - if hasattr(agent, "_sentry_workflow_span"): - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - if hasattr(run_result, "stream_events"): - original_stream_events = run_result.stream_events - - @wraps(original_stream_events) - async def wrapped_stream_events( - *stream_args: "Any", **stream_kwargs: "Any" - ) -> "AsyncIterator[Any]": - try: - async for event in original_stream_events( - *stream_args, **stream_kwargs - ): - yield event - finally: - _close_workflow_span() - - run_result.stream_events = wrapped_stream_events - - if hasattr(run_result, "cancel"): - original_cancel = run_result.cancel - - @wraps(original_cancel) - def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": - try: - return original_cancel(*cancel_args, **cancel_kwargs) - finally: - _close_workflow_span() - - run_result.cancel = wrapped_cancel - - return run_result - - return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py deleted file mode 100644 index bd13b9d61a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/patches/tools.py +++ /dev/null @@ -1,70 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import execute_tool_span, update_execute_tool_span - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -async def _get_all_tools( - original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", -) -> "list[agents.Tool]": - """ - Responsible for creating and managing `gen_ai.execute_tool` spans. - """ - # Get the original tools - tools = await original_get_all_tools(agent, context_wrapper) - - wrapped_tools = [] - for tool in tools: - # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": - wrapped_tools.append(tool) - continue - - # Create a new FunctionTool with our wrapped invoke method - original_on_invoke = tool.on_invoke_tool - - def create_wrapped_invoke( - current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" - ) -> "Callable[..., Any]": - @wraps(current_on_invoke) - async def sentry_wrapped_on_invoke_tool( - *args: "Any", **kwargs: "Any" - ) -> "Any": - with execute_tool_span(current_tool, *args, **kwargs) as span: - # We can not capture exceptions in tool execution here because - # `_on_invoke_tool` is swallowing the exception here: - # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 - # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter - # I was unable to monkey patch it because those are evaluated at module import time - # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` - # because it is nested inside this import time code. As if they made it hard to patch on purpose... - result = await current_on_invoke(*args, **kwargs) - update_execute_tool_span(span, agent, current_tool, result) - - return result - - return sentry_wrapped_on_invoke_tool - - wrapped_tool = agents.FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), - strict_json_schema=tool.strict_json_schema, - is_enabled=tool.is_enabled, - ) - wrapped_tools.append(wrapped_tool) - - return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py deleted file mode 100644 index 08802a87a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .agent_workflow import agent_workflow_span # noqa: F401 -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .handoff import handoff_span # noqa: F401 -from .invoke_agent import ( - invoke_agent_span, # noqa: F401 - update_invoke_agent_span, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py deleted file mode 100644 index 758f06db8d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - from typing import Union - - import agents - - -def agent_workflow_span( - agent: "agents.Agent", -) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": - # Create a transaction or a span if an transaction is already active - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} - ) - - return span - - span = get_start_span_function()( - name=f"{agent.name} workflow", - origin=SPAN_ORIGIN, - ) - - return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py deleted file mode 100644 index f4f02cb674..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_input_data, - _set_output_data, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from agents import Agent - - -def ai_client_span( - agent: "Agent", get_response_kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - # TODO-anton: implement other types of operations. Now "chat" is hardcoded. - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - _set_agent_data(span, agent) - _set_input_data(span, get_response_kwargs) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - response: "Any", - response_model: "Optional[str]" = None, - agent: "Optional[Agent]" = None, -) -> None: - """Update AI client span with response data (works for streaming and non-streaming).""" - if hasattr(response, "usage") and response.usage: - _set_usage_data(span, response.usage) - - if hasattr(response, "output") and response.output: - _set_output_data(span, response) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - elif hasattr(response, "model") and response.model: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) - - # Set conversation ID from agent if available - if agent: - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py deleted file mode 100644 index fd3a430951..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def execute_tool_span( - tool: "agents.Tool", *args: "Any", **kwargs: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool.name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool.name, - SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool.name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - - set_on_span = span.set_data - - if should_send_default_pii(): - input = args[1] - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - agent: "agents.Agent", - tool: "agents.Tool", - result: "Any", -) -> None: - _set_agent_data(span, agent) - - if isinstance(result, str) and result.startswith( - "An error occurred while running the tool" - ): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if should_send_default_pii(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py deleted file mode 100644 index ea91464afb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - import agents - - -def handoff_span( - context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str -) -> None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"handoff from {from_agent.name} to {to_agent_name}", - attributes={ - "sentry.op": OP.GEN_AI_HANDOFF, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "handoff", - }, - ) as span: - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_HANDOFF, - name=f"handoff from {from_agent.name} to {to_agent_name}", - origin=SPAN_ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") - - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py deleted file mode 100644 index c21145ac4a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _set_usage_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def invoke_agent_span( - context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {agent.name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - start_span_function = get_start_span_function() - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent.name}", - origin=SPAN_ORIGIN, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - if should_send_default_pii(): - messages = [] - if agent.instructions: - message = ( - agent.instructions - if isinstance(agent.instructions, str) - else safe_serialize(agent.instructions) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "system", - } - ) - - original_input = kwargs.get("original_input") - if original_input is not None: - message = ( - original_input - if isinstance(original_input, str) - else safe_serialize(original_input) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "user", - } - ) - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - _set_agent_data(span, agent) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - context: "agents.RunContextWrapper", - agent: "agents.Agent", - output: "Any" = None, -) -> None: - # Add aggregated usage data from context_wrapper - if hasattr(context, "usage"): - _set_usage_data(span, context.usage) - - if should_send_default_pii(): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py deleted file mode 100644 index 224a5f66ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openai_agents/utils.py +++ /dev/null @@ -1,244 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai._openai_completions_api import _transform_system_instructions -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions, - _is_system_instruction, -) -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - normalize_message_role, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import should_truncate_gen_ai_input -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Union - - from agents import TResponseInputItem, Usage - - from sentry_sdk._types import TextPart - -try: - import agents - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai_agents", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span( - SPANDATA.GEN_AI_SYSTEM, "openai" - ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. - - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) - - if agent.model_settings.max_tokens: - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) - - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - if agent.model_settings.presence_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - agent.model_settings.presence_penalty, - ) - - if agent.model_settings.temperature: - set_on_span( - SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature - ) - - if agent.model_settings.top_p: - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) - - if agent.model_settings.frequency_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - agent.model_settings.frequency_penalty, - ) - - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage.input_tokens_details.cached_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage.output_tokens_details.reasoning_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) - - -def _set_input_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - get_response_kwargs: "dict[str, Any]", -) -> None: - if not should_send_default_pii(): - return - request_messages = [] - - messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) - - instructions_text_parts: "list[TextPart]" = [] - explicit_instructions = get_response_kwargs.get("system_instructions") - if explicit_instructions is not None: - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions(messages) - - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - non_system_messages = [ - message for message in messages if not _is_system_instruction(message) - ] - for message in non_system_messages: - if "role" in message: - normalized_role = normalize_message_role(message.get("role")) # type: ignore - content = message.get("content") # type: ignore - request_messages.append( - { - "role": normalized_role, - "content": ( - [{"type": "text", "text": content}] - if isinstance(content, str) - else content - ), - } - ) - else: - if message.get("type") == "function_call": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, - "content": [message], - } - ) - elif message.get("type") == "function_call_output": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, - "content": [message], - } - ) - - normalized_messages = normalize_message_roles(request_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - if not should_send_default_pii(): - return - - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in result.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - else: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py deleted file mode 100644 index 281604fe38..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/openfeature.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from openfeature import api - from openfeature.hook import Hook - - if TYPE_CHECKING: - from openfeature.hook import HookContext, HookHints -except ImportError: - raise DidNotEnable("OpenFeature is not installed") - - -class OpenFeatureIntegration(Integration): - identifier = "openfeature" - - @staticmethod - def setup_once() -> None: - # Register the hook within the global openfeature hooks list. - api.add_hooks(hooks=[OpenFeatureHook()]) - - -class OpenFeatureHook(Hook): - def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: - if isinstance(details.value, bool): - add_feature_flag(details.flag_key, details.value) - - def error( - self, hook_context: "HookContext", exception: Exception, hints: "HookHints" - ) -> None: - if isinstance(hook_context.default_value, bool): - add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py deleted file mode 100644 index d76b8058ee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor - -__all__ = [ - "SentryPropagator", - "SentrySpanProcessor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py deleted file mode 100644 index d6733036ea..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable - -try: - from opentelemetry.context import create_key -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -SENTRY_TRACE_KEY = create_key("sentry-trace") -SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py deleted file mode 100644 index 472e8181f9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/integration.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -IMPORTANT: The contents of this file are part of a proof of concept and as such -are experimental and not suitable for production use. They may be changed or -removed at any time without prior notice. -""" - -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - -try: - from opentelemetry import trace - from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -try: - from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] - DjangoInstrumentor, - ) -except ImportError: - DjangoInstrumentor = None - - -CONFIGURABLE_INSTRUMENTATIONS = { - DjangoInstrumentor: {"is_sql_commentor_enabled": True}, -} - - -class OpenTelemetryIntegration(Integration): - identifier = "opentelemetry" - - @staticmethod - def setup_once() -> None: - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if has_span_streaming_enabled(options): - logger.warning( - "[OTel] OpenTelemetryIntegration is not compatible with span streaming " - "(trace_lifecycle='stream') and will be disabled." - ) - return - - warnings.warn( - "OpenTelemetryIntegration is deprecated. " - "Please use OTLPIntegration instead: " - "https://docs.sentry.io/platforms/python/integrations/otlp/", - DeprecationWarning, - stacklevel=2, - ) - - _setup_sentry_tracing() - # _setup_instrumentors() - - logger.debug("[OTel] Finished setting up OpenTelemetry integration") - - -def _setup_sentry_tracing() -> None: - provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) - trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) - - -def _setup_instrumentors() -> None: - for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): - if instrumentor is None: - continue - instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py deleted file mode 100644 index 5b5f13861d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/propagator.py +++ /dev/null @@ -1,128 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data - -try: - from opentelemetry import trace - from opentelemetry.context import ( - Context, - get_current, - set_value, - ) - from opentelemetry.propagators.textmap import ( - CarrierT, - Getter, - Setter, - TextMapPropagator, - default_getter, - default_setter, - ) - from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Set - - -class SentryPropagator(TextMapPropagator): - """ - Propagates tracing headers for Sentry's tracing system in a way OTel understands. - """ - - def extract( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - getter: "Getter[CarrierT]" = default_getter, - ) -> "Context": - if context is None: - context = get_current() - - sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) - if not sentry_trace: - return context - - sentrytrace = extract_sentrytrace_data(sentry_trace[0]) - if not sentrytrace: - return context - - context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) - - trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] - - span_context = SpanContext( - trace_id=int(trace_id, 16), # type: ignore - span_id=int(span_id, 16), # type: ignore - # we simulate a sampled trace on the otel side and leave the sampling to sentry - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - - baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) - - if baggage_header: - baggage = Baggage.from_incoming_header(baggage_header[0]) - else: - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and frozen and won't be populated as head SDK. - baggage = Baggage(sentry_items={}) - - baggage.freeze() - context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) - - span = NonRecordingSpan(span_context) - modified_context = trace.set_span_in_context(span, context) - return modified_context - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - if context is None: - context = get_current() - - current_span = trace.get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - span_id = trace.format_span_id(current_span_context.span_id) - - span_map = SentrySpanProcessor.otel_span_map - sentry_span = span_map.get(span_id, None) - if not sentry_span: - return - - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) - - if sentry_span.containing_transaction: - baggage = sentry_span.containing_transaction.get_baggage() - if baggage: - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - @property - def fields(self) -> "Set[str]": - return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py deleted file mode 100644 index 1efd2ac455..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/opentelemetry/span_processor.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from time import time -from typing import TYPE_CHECKING, cast - -from urllib3.util import parse_url as urlparse - -from sentry_sdk import get_client, start_transaction -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing import Transaction - -try: - from opentelemetry.context import get_value - from opentelemetry.sdk.trace import ReadableSpan as OTelSpan - from opentelemetry.sdk.trace import SpanProcessor - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import ( - SpanKind, - format_span_id, - format_trace_id, - get_current_span, - ) - from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from opentelemetry import context as context_api - from opentelemetry.trace import SpanContext - - from sentry_sdk._types import Event, Hint - -OPEN_TELEMETRY_CONTEXT = "otel" -SPAN_MAX_TIME_OPEN_MINUTES = 10 -SPAN_ORIGIN = "auto.otel" - - -def link_trace_context_to_error_event( - event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" -) -> "Event": - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return event - - if hasattr(event, "type") and event["type"] == "transaction": - return event - - otel_span = get_current_span() - if not otel_span: - return event - - ctx = otel_span.get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return event - - sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) - if not sentry_span: - return event - - contexts = event.setdefault("contexts", {}) - contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) - - return event - - -class SentrySpanProcessor(SpanProcessor): - """ - Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. - """ - - # The mapping from otel span ids to sentry spans - otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} - - # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES - open_spans: "dict[int, set[str]]" = {} - - initialized: "bool" = False - - def __new__(cls) -> "SentrySpanProcessor": - if not hasattr(cls, "instance"): - cls.instance = super().__new__(cls) - - # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). - return cls.instance # type: ignore[misc] - - def __init__(self) -> None: - if self.initialized: - return - - @add_global_event_processor - def global_event_processor(event: "Event", hint: "Hint") -> "Event": - return link_trace_context_to_error_event(event, self.otel_span_map) - - self.initialized = True - - def _prune_old_spans(self: "SentrySpanProcessor") -> None: - """ - Prune spans that have been open for too long. - """ - current_time_minutes = int(time() / 60) - for span_start_minutes in list( - self.open_spans.keys() - ): # making a list because we change the dict - # prune empty open spans buckets - if self.open_spans[span_start_minutes] == set(): - self.open_spans.pop(span_start_minutes) - - # prune old buckets - elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: - for span_id in self.open_spans.pop(span_start_minutes): - self.otel_span_map.pop(span_id, None) - - def on_start( - self, - otel_span: "OTelSpan", - parent_context: "Optional[context_api.Context]" = None, - ) -> None: - client = get_client() - - if not client.parsed_dsn: - return - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - if self._is_sentry_span(otel_span): - return - - trace_data = self._get_trace_data( - span_context, otel_span.parent, parent_context - ) - - parent_span_id = trace_data["parent_span_id"] - sentry_parent_span = ( - self.otel_span_map.get(parent_span_id) if parent_span_id else None - ) - - start_timestamp = None - if otel_span.start_time is not None: - start_timestamp = datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span = None - if sentry_parent_span: - sentry_span = sentry_parent_span.start_child( - span_id=trace_data["span_id"], - name=otel_span.name, - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - else: - sentry_span = start_transaction( - name=otel_span.name, - span_id=trace_data["span_id"], - parent_span_id=parent_span_id, - trace_id=trace_data["trace_id"], - baggage=trace_data["baggage"], - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - - self.otel_span_map[trace_data["span_id"]] = sentry_span - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).add( - trace_data["span_id"] - ) - - self._prune_old_spans() - - def on_end(self, otel_span: "OTelSpan") -> None: - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - span_id = format_span_id(span_context.span_id) - sentry_span = self.otel_span_map.pop(span_id, None) - if not sentry_span: - return - - sentry_span.op = otel_span.name - - self._update_span_with_otel_status(sentry_span, otel_span) - - if isinstance(sentry_span, Transaction): - sentry_span.name = otel_span.name - sentry_span.set_context( - OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) - ) - self._update_transaction_with_otel_data(sentry_span, otel_span) - - else: - self._update_span_with_otel_data(sentry_span, otel_span) - - end_timestamp = None - if otel_span.end_time is not None: - end_timestamp = datetime.fromtimestamp( - otel_span.end_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span.finish(end_timestamp=end_timestamp) - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) - - self._prune_old_spans() - - def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: - """ - Break infinite loop: - HTTP requests to Sentry are caught by OTel and send again to Sentry. - """ - otel_span_url = None - if otel_span.attributes is not None: - otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) - otel_span_url = cast("Optional[str]", otel_span_url) - - parsed_dsn = get_client().parsed_dsn - dsn_url = parsed_dsn.netloc if parsed_dsn else None - - if otel_span_url and dsn_url and dsn_url in otel_span_url: - return True - - return False - - def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": - """ - Returns the OTel context for Sentry. - See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context - """ - ctx = {} - - if otel_span.attributes: - ctx["attributes"] = dict(otel_span.attributes) - - if otel_span.resource.attributes: - ctx["resource"] = dict(otel_span.resource.attributes) - - return ctx - - def _get_trace_data( - self, - span_context: "SpanContext", - parent_span_context: "Optional[SpanContext]", - parent_context: "Optional[context_api.Context]", - ) -> "dict[str, Any]": - """ - Extracts tracing information from one OTel span's context and its parent OTel context. - """ - trace_data: "dict[str, Any]" = {} - - span_id = format_span_id(span_context.span_id) - trace_data["span_id"] = span_id - - trace_id = format_trace_id(span_context.trace_id) - trace_data["trace_id"] = trace_id - - parent_span_id = ( - format_span_id(parent_span_context.span_id) if parent_span_context else None - ) - trace_data["parent_span_id"] = parent_span_id - - sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) - sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) - trace_data["parent_sampled"] = ( - sentry_trace_data["parent_sampled"] if sentry_trace_data else None - ) - - baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) - trace_data["baggage"] = baggage - - return trace_data - - def _update_span_with_otel_status( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Set the Sentry span status from the OTel span - """ - if otel_span.status.is_unset: - return - - if otel_span.status.is_ok: - sentry_span.set_status(SPANSTATUS.OK) - return - - sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - def _update_span_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Convert OTel span data and update the Sentry span with it. - This should eventually happen on the server when ingesting the spans. - """ - sentry_span.set_data("otel.kind", otel_span.kind) - - op = otel_span.name - description = otel_span.name - - if otel_span.attributes is not None: - for key, val in otel_span.attributes.items(): - sentry_span.set_data(key, val) - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - http_method = cast("Optional[str]", http_method) - - db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) - - if http_method: - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - description = http_method - - peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) - if peer_name: - description += " {}".format(peer_name) - - target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) - if target: - description += " {}".format(target) - - if not peer_name and not target: - url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) - url = cast("Optional[str]", url) - if url: - parsed_url = urlparse(url) - url = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path - ) - description += " {}".format(url) - - status_code = otel_span.attributes.get( - SpanAttributes.HTTP_STATUS_CODE, None - ) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - elif db_query: - op = "db" - statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) - statement = cast("Optional[str]", statement) - if statement: - description = statement - - sentry_span.op = op - sentry_span.description = description - - def _update_transaction_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - if otel_span.attributes is None: - return - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - - if http_method: - status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py deleted file mode 100644 index b91f2cfd21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/otlp.py +++ /dev/null @@ -1,223 +0,0 @@ -from sentry_sdk import capture_event, get_client -from sentry_sdk.consts import VERSION, EndpointType -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import register_external_propagation_context -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - event_from_exception, - logger, -) - -try: - from opentelemetry.context import ( - Context, - get_current, - get_value, - ) - from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter - from opentelemetry.propagate import set_global_textmap - from opentelemetry.propagators.textmap import ( - CarrierT, - Setter, - default_setter, - ) - from opentelemetry.sdk.trace import Span, TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - from opentelemetry.trace import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - SpanContext, - format_span_id, - format_trace_id, - get_current_span, - get_tracer_provider, - set_tracer_provider, - ) - - from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY - from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -except ImportError: - raise DidNotEnable("opentelemetry-distro[otlp] is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple - - -def otel_propagation_context() -> "Optional[Tuple[str, str]]": - """ - Get the (trace_id, span_id) from opentelemetry if exists. - """ - ctx = get_current_span().get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return None - - return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) - - -def setup_otlp_traces_exporter( - dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None -) -> None: - tracer_provider = get_tracer_provider() - - if not isinstance(tracer_provider, TracerProvider): - logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") - tracer_provider = TracerProvider() - set_tracer_provider(tracer_provider) - - endpoint = None - headers = None - if collector_url: - endpoint = collector_url - logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") - elif dsn: - auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") - endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) - headers = {"X-Sentry-Auth": auth.to_header()} - logger.debug(f"[OTLP] Sending traces to {endpoint}") - - otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) - span_processor = BatchSpanProcessor(otlp_exporter) - tracer_provider.add_span_processor(span_processor) - - -_sentry_patched_exception = False - - -def setup_capture_exceptions() -> None: - """ - Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. - """ - global _sentry_patched_exception - _original_record_exception = Span.record_exception - - if _sentry_patched_exception: - return - - def _sentry_patched_record_exception( - self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration and otlp_integration.capture_exceptions: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=get_client().options, - mechanism={"type": OTLPIntegration.identifier, "handled": False}, - ) - capture_event(event, hint=hint) - - _original_record_exception(self, exception, *args, **kwargs) - - Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] - _sentry_patched_exception = True - - -class SentryOTLPPropagator(SentryPropagator): - """ - We need to override the inject of the older propagator since that - is SpanProcessor based. - - !!! Note regarding baggage: - We cannot meaningfully populate a new baggage as a head SDK - when we are using OTLP since we don't have any sort of transaction semantic to - track state across a group of spans. - - For incoming baggage, we just pass it on as is so that case is correctly handled. - """ - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration is None: - return - - if context is None: - context = get_current() - - current_span = get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - sentry_trace = _to_traceparent(current_span_context) - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) - - baggage = get_value(SENTRY_BAGGAGE_KEY, context) - if baggage is not None and isinstance(baggage, Baggage): - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - -def _to_traceparent(span_context: "SpanContext") -> str: - """ - Helper method to generate the sentry-trace header. - """ - span_id = format_span_id(span_context.span_id) - trace_id = format_trace_id(span_context.trace_id) - sampled = span_context.trace_flags.sampled - - return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" - - -class OTLPIntegration(Integration): - """ - Automatically setup OTLP ingestion from the DSN. - - :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. - Set to False to setup the TracerProvider manually. - :param collector_url: URL of your own OpenTelemetry collector, defaults to None. - When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. - :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. - Set to False to configure propagators manually or to disable propagation. - :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. - Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. - """ - - identifier = "otlp" - - def __init__( - self, - setup_otlp_traces_exporter: bool = True, - collector_url: "Optional[str]" = None, - setup_propagator: bool = True, - capture_exceptions: bool = False, - ) -> None: - self.setup_otlp_traces_exporter = setup_otlp_traces_exporter - self.collector_url = collector_url - self.setup_propagator = setup_propagator - self.capture_exceptions = capture_exceptions - - @staticmethod - def setup_once() -> None: - logger.debug("[OTLP] Setting up trace linking for all events") - register_external_propagation_context(otel_propagation_context) - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if self.setup_otlp_traces_exporter: - logger.debug("[OTLP] Setting up OTLP exporter") - dsn: "Optional[str]" = options.get("dsn") if options else None - setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) - - if self.setup_propagator: - logger.debug("[OTLP] Setting up propagator for distributed tracing") - # TODO-neel better propagator support, chain with existing ones if possible instead of replacing - set_global_textmap(SentryOTLPPropagator()) - - setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py deleted file mode 100644 index 569e3e1fa5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pure_eval.py +++ /dev/null @@ -1,134 +0,0 @@ -import ast -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import serializer -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, List, Optional, Tuple - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - -try: - from pure_eval import Evaluator -except ImportError: - raise DidNotEnable("pure_eval is not installed") - -try: - # Used implicitly, just testing it's available - import asttokens # noqa -except ImportError: - raise DidNotEnable("asttokens is not installed") - - -class PureEvalIntegration(Integration): - identifier = "pure_eval" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - sentry_frame["vars"] = ( - pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] - ) - return event - - -def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": - source = Source.for_frame(frame) - if not source.tree: - return {} - - statements = source.statements_at_line(frame.f_lineno) - if not statements: - return {} - - scope = stmt = list(statements)[0] - while True: - # Get the parent first in case the original statement is already - # a function definition, e.g. if we're calling a decorator - # In that case we still want the surrounding scope, not that function - scope = scope.parent - if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): - break - - evaluator = Evaluator.from_frame(frame) - expressions = evaluator.interesting_expressions_grouped(scope) - - def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": - # Prioritise expressions with a node closer to the statement executed - # without being after that statement - # A higher return value is better - the expression will appear - # earlier in the list of values and is less likely to be trimmed - nodes, _value = expression - - def start(n: "ast.expr") -> "Tuple[int, int]": - return (n.lineno, n.col_offset) - - nodes_before_stmt = [ - node for node in nodes if start(node) < stmt.last_token.end - ] - if nodes_before_stmt: - # The position of the last node before or in the statement - return max(start(node) for node in nodes_before_stmt) - else: - # The position of the first node after the statement - # Negative means it's always lower priority than nodes that come before - # Less negative means closer to the statement and higher priority - lineno, col_offset = min(start(node) for node in nodes) - return (-lineno, -col_offset) - - # This adds the first_token and last_token attributes to nodes - atok = source.asttokens() - - expressions.sort(key=closeness, reverse=True) - vars = { - atok.get_text(nodes[0]): value - for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] - } - return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py deleted file mode 100644 index 81e7cf8090..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/__init__.py +++ /dev/null @@ -1,187 +0,0 @@ -import functools - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import capture_internal_exceptions, parse_version - -try: - import pydantic_ai # type: ignore # noqa: F401 - from pydantic_ai import Agent -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -from importlib.metadata import PackageNotFoundError, version -from typing import TYPE_CHECKING - -from .patches import ( - _patch_agent_run, - _patch_graph_nodes, - _patch_tool_execution, -) -from .spans.ai_client import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any - - from pydantic_ai import ModelRequestContext, RunContext - from pydantic_ai.capabilities import Hooks # type: ignore - from pydantic_ai.messages import ModelResponse # type: ignore - - -def register_hooks(hooks: "Hooks") -> None: - """ - Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. - """ - - @hooks.on.before_model_request # type: ignore - async def on_request( - ctx: "RunContext[None]", request_context: "ModelRequestContext" - ) -> "ModelRequestContext": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return request_context - - span = ai_client_span( - messages=request_context.messages, - agent=None, - model=request_context.model, - model_settings=request_context.model_settings, - ) - - run_context_metadata["_sentry_span"] = span - span.__enter__() - - return request_context - - @hooks.on.after_model_request # type: ignore - async def on_response( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - response: "ModelResponse", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return response - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - return response - - update_ai_client_span(span, response) - span.__exit__(None, None, None) - - return response - - @hooks.on.model_request_error # type: ignore - async def on_error( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - error: "Exception", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - - if not isinstance(run_context_metadata, dict): - raise error - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - raise error - - with capture_internal_exceptions(): - span.__exit__(type(error), error, error.__traceback__) - - raise error - - original_init = Agent.__init__ - - @functools.wraps(original_init) - def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: - caps = list(kwargs.get("capabilities") or []) - caps.append(hooks) - kwargs["capabilities"] = caps - - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {} # Used as shared reference between hooks - - return original_init(self, *args, **kwargs) - - Agent.__init__ = patched_init - - -class PydanticAIIntegration(Integration): - """ - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. - 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. - - Each run invocation has `RunContext` objects that are passed to the library hooks. - 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. - Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). - The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. - - The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that - instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. - """ - - identifier = "pydantic_ai" - origin = f"auto.ai.{identifier}" - using_request_hooks = False - - def __init__( - self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True - ) -> None: - """ - Initialize the Pydantic AI integration. - - Args: - include_prompts: Whether to include prompts and messages in span data. - Requires send_default_pii=True. Defaults to True. - handled_tool_exceptions: Capture tool call exceptions that Pydantic AI - internally prevents from bubbling up. - """ - self.include_prompts = include_prompts - self.handled_tool_call_exceptions = handled_tool_call_exceptions - - @staticmethod - def setup_once() -> None: - """ - Set up the pydantic-ai integration. - - This patches the key methods in pydantic-ai to create Sentry spans for: - - Agent invocations (Agent.run methods) - - Model requests (AI client calls) - - Tool executions - """ - _patch_agent_run() - _patch_tool_execution() - - PydanticAIIntegration.using_request_hooks = False - try: - PYDANTIC_AI_VERSION = version("pydantic-ai-slim") - except PackageNotFoundError: - return - - PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) - if PYDANTIC_AI_VERSION is None: - return - - # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c - if PYDANTIC_AI_VERSION < ( - 1, - 73, - ): - _patch_graph_nodes() - return - - try: - from pydantic_ai.capabilities import Hooks - except ImportError: - return - - PydanticAIIntegration.using_request_hooks = True - hooks = Hooks() - register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py deleted file mode 100644 index afa66dc47d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py deleted file mode 100644 index d0ea6242b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .agent_run import _patch_agent_run # noqa: F401 -from .graph_nodes import _patch_graph_nodes # noqa: F401 -from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py deleted file mode 100644 index 3039e40698..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ /dev/null @@ -1,198 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import invoke_agent_span, update_invoke_agent_span -from ..utils import _capture_exception, pop_agent, push_agent - -try: - from pydantic_ai.agent import Agent # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - -class _StreamingContextManagerWrapper: - """Wrapper for streaming methods that return async context managers.""" - - def __init__( - self, - agent: "Any", - original_ctx_manager: "Any", - user_prompt: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = True, - ) -> None: - self.agent = agent - self.original_ctx_manager = original_ctx_manager - self.user_prompt = user_prompt - self.model = model - self.model_settings = model_settings - self.is_streaming = is_streaming - self._isolation_scope: "Any" = None - self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None - self._result: "Any" = None - - async def __aenter__(self) -> "Any": - # Set up isolation scope and invoke_agent span - self._isolation_scope = sentry_sdk.isolation_scope() - self._isolation_scope.__enter__() - - # Create invoke_agent span (will be closed in __aexit__) - self._span = invoke_agent_span( - self.user_prompt, - self.agent, - self.model, - self.model_settings, - self.is_streaming, - ) - self._span.__enter__() - - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur - push_agent(self.agent, self.is_streaming) - - # Enter the original context manager - result = await self.original_ctx_manager.__aenter__() - self._result = result - return result - - async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: - try: - # Exit the original context manager first - await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) - - # Update span with result if successful - if exc_type is None and self._result and self._span is not None: - update_invoke_agent_span(self._span, self._result) - finally: - # Pop agent from contextvar stack - pop_agent() - - # Clean up invoke span - if self._span: - self._span.__exit__(exc_type, exc_val, exc_tb) - - # Clean up isolation scope - if self._isolation_scope: - self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) - - -def _create_run_wrapper( - original_func: "Callable[..., Any]", is_streaming: bool = False -) -> "Callable[..., Any]": - """ - Wraps the Agent.run method to create an invoke_agent span. - - Args: - original_func: The original run method - is_streaming: Whether this is a streaming method (for future use) - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Create invoke_agent span - with invoke_agent_span( - user_prompt, self, model, model_settings, is_streaming - ) as span: - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in finally even if exceptions occur - push_agent(self, is_streaming) - - try: - result = await original_func(self, *args, **kwargs) - - # Update span with result - update_invoke_agent_span(span, result) - - return result - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - finally: - # Pop agent from contextvar stack - pop_agent() - - return wrapper - - -def _create_streaming_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps run_stream method that returns an async context manager. - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Call original function to get the context manager - original_ctx_manager = original_func(self, *args, **kwargs) - - # Wrap it with our instrumentation - return _StreamingContextManagerWrapper( - agent=self, - original_ctx_manager=original_ctx_manager, - user_prompt=user_prompt, - model=model, - model_settings=model_settings, - is_streaming=True, - ) - - return wrapper - - -def _patch_agent_run() -> None: - """ - Patches the Agent run methods to create spans for agent execution. - - This patches both non-streaming (run, run_sync) and streaming - (run_stream, run_stream_events) methods. - """ - - # Store original methods - original_run = Agent.run - original_run_stream = Agent.run_stream - - # Wrap and apply patches for non-streaming methods - Agent.run = _create_run_wrapper(original_run, is_streaming=False) - - # Wrap and apply patches for streaming methods - Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py deleted file mode 100644 index afb10395f4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +++ /dev/null @@ -1,106 +0,0 @@ -from contextlib import asynccontextmanager -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import ( - ai_client_span, - update_ai_client_span, -) - -try: - from pydantic_ai._agent_graph import ModelRequestNode # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - -def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": - """Extract common data needed for creating chat spans. - - Returns: - Tuple of (messages, model, model_settings) - """ - # Extract model and settings from context - model = None - model_settings = None - if hasattr(ctx, "deps"): - model = getattr(ctx.deps, "model", None) - model_settings = getattr(ctx.deps, "model_settings", None) - - # Build full message list: history + current request - messages = [] - if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): - messages.extend(ctx.state.message_history) - - current_request = getattr(node, "request", None) - if current_request: - messages.append(current_request) - - return messages, model, model_settings - - -def _patch_graph_nodes() -> None: - """ - Patches the graph node execution to create appropriate spans. - - ModelRequestNode -> Creates ai_client span for model requests - CallToolsNode -> Handles tool calls (spans created in tool patching) - """ - - # Patch ModelRequestNode to create ai_client spans - original_model_request_run = ModelRequestNode.run - - @wraps(original_model_request_run) - async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - with ai_client_span(messages, None, model, model_settings) as span: - result = await original_model_request_run(self, ctx) - - # Extract response from result if available - model_response = None - if hasattr(result, "model_response"): - model_response = result.model_response - - update_ai_client_span(span, model_response) - return result - - ModelRequestNode.run = wrapped_model_request_run - - # Patch ModelRequestNode.stream for streaming requests - original_model_request_stream = ModelRequestNode.stream - - def create_wrapped_stream( - original_stream_method: "Callable[..., Any]", - ) -> "Callable[..., Any]": - """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" - - @asynccontextmanager - @wraps(original_stream_method) - async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - # Create chat span for streaming request - with ai_client_span(messages, None, model, model_settings) as span: - # Call the original stream method - async with original_stream_method(self, ctx) as stream: - yield stream - - # After streaming completes, update span with response data - # The ModelRequestNode stores the final response in _result - model_response = None - if hasattr(self, "_result") and self._result is not None: - # _result is a NextNode containing the model_response - if hasattr(self._result, "model_response"): - model_response = self._result.model_response - - update_ai_client_span(span, model_response) - - return wrapped_model_request_stream - - ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py deleted file mode 100644 index 5646b5d47c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ /dev/null @@ -1,177 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import execute_tool_span, update_execute_tool_span -from ..utils import _capture_exception, get_current_agent - -if TYPE_CHECKING: - from typing import Any - -try: - try: - from pydantic_ai.tool_manager import ToolManager # type: ignore - except ImportError: - from pydantic_ai._tool_manager import ToolManager # type: ignore - - from pydantic_ai.exceptions import ToolRetryError # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -def _patch_tool_execution() -> None: - if hasattr(ToolManager, "execute_tool_call"): - _patch_execute_tool_call() - - elif hasattr(ToolManager, "_call_tool"): - # older versions - _patch_call_tool() - - -def _patch_execute_tool_call() -> None: - original_execute_tool_call = ToolManager.execute_tool_call - - @wraps(original_execute_tool_call) - async def wrapped_execute_tool_call( - self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not validated or not hasattr(validated, "call"): - return await original_execute_tool_call(self, validated, *args, **kwargs) - - # Extract tool info before calling original - call = validated.call - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_execute_tool_call( - self, - validated, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - return await original_execute_tool_call(self, validated, *args, **kwargs) - - ToolManager.execute_tool_call = wrapped_execute_tool_call - - -def _patch_call_tool() -> None: - """ - Patch ToolManager._call_tool to create execute_tool spans. - - This is the single point where ALL tool calls flow through in pydantic_ai, - regardless of toolset type (function, MCP, combined, wrapper, etc.). - - By patching here, we avoid: - - Patching multiple toolset classes - - Dealing with signature mismatches from instrumented MCP servers - - Complex nested toolset handling - """ - original_call_tool = ToolManager._call_tool - - @wraps(original_call_tool) - async def wrapped_call_tool( - self: "Any", call: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Extract tool info before calling original - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_call_tool( - self, - call, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - # No span context - just call original - return await original_call_tool( - self, - call, - *args, - **kwargs, - ) - - ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py deleted file mode 100644 index 574046d645..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py deleted file mode 100644 index 27deb0c55c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ /dev/null @@ -1,331 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _get_model_name, - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, - get_current_agent, - get_is_streaming, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Dict, List, Union - - from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore - - from sentry_sdk._types import TextPart as SentryTextPart - -try: - from pydantic_ai.messages import ( - BaseToolCallPart, - BaseToolReturnPart, - BinaryContent, - ImageUrl, - SystemPromptPart, - TextPart, - ThinkingPart, - UserPromptPart, - ) -except ImportError: - # Fallback if these classes are not available - BaseToolCallPart = None - BaseToolReturnPart = None - SystemPromptPart = None - UserPromptPart = None - TextPart = None - ThinkingPart = None - BinaryContent = None - ImageUrl = None - - -def _transform_system_instructions( - permanent_instructions: "list[SystemPromptPart]", - current_instructions: "list[str]", -) -> "list[SentryTextPart]": - text_parts: "list[SentryTextPart]" = [ - { - "type": "text", - "content": instruction.content, - } - for instruction in permanent_instructions - ] - - text_parts.extend( - { - "type": "text", - "content": instruction, - } - for instruction in current_instructions - ) - - return text_parts - - -def _get_system_instructions( - messages: "list[ModelMessage]", -) -> "tuple[list[SystemPromptPart], list[str]]": - permanent_instructions = [] - current_instructions = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - if SystemPromptPart and isinstance(part, SystemPromptPart): - permanent_instructions.append(part) - - if hasattr(msg, "instructions") and msg.instructions is not None: - current_instructions.append(msg.instructions) - - return permanent_instructions, current_instructions - - -def _set_input_messages( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" -) -> None: - """Set input messages data on a span.""" - if not _should_send_prompts(): - return - - if not messages: - return - - permanent_instructions, current_instructions = _get_system_instructions(messages) - if len(permanent_instructions) > 0 or len(current_instructions) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - - try: - formatted_messages = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - role = "user" - # Use isinstance checks with proper base classes - if SystemPromptPart and isinstance(part, SystemPromptPart): - continue - elif ( - (TextPart and isinstance(part, TextPart)) - or (ThinkingPart and isinstance(part, ThinkingPart)) - or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) - ): - role = "assistant" - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - role = "tool" - - content: "List[Dict[str, Any] | str]" = [] - tool_calls = None - tool_call_id = None - - # Handle ToolCallPart (assistant requesting tool use) - if BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = {} - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - if tool_call_data: - tool_calls = [tool_call_data] - # Handle ToolReturnPart (tool result) - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - if hasattr(part, "tool_name"): - tool_call_id = part.tool_name - if hasattr(part, "content"): - content.append({"type": "text", "text": str(part.content)}) - # Handle regular content - elif hasattr(part, "content"): - if isinstance(part.content, str): - content.append({"type": "text", "text": part.content}) - elif isinstance(part.content, list): - for item in part.content: - if isinstance(item, str): - content.append({"type": "text", "text": item}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - else: - content.append(safe_serialize(item)) - else: - content.append({"type": "text", "text": str(part.content)}) - # Add message if we have content or tool calls - if content or tool_calls: - message: "Dict[str, Any]" = {"role": role} - if content: - message["content"] = content - if tool_calls: - message["tool_calls"] = tool_calls - if tool_call_id: - message["tool_call_id"] = tool_call_id - formatted_messages.append(message) - - if formatted_messages: - normalized_messages = normalize_message_roles(formatted_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - except Exception: - # If we fail to format messages, just skip it - pass - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" -) -> None: - """Set output data on a span.""" - if not _should_send_prompts(): - return - - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - - try: - # Extract text from ModelResponse - if hasattr(response, "parts"): - texts = [] - tool_calls = [] - - for part in response.parts: - if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): - texts.append(part.content) - elif BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = { - "type": "function", - } - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - tool_calls.append(tool_call_data) - - if texts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) - - if tool_calls: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) - ) - - except Exception: - # If we fail to format output, just skip it - pass - - -def ai_client_span( - messages: "Any", agent: "Any", model: "Any", model_settings: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for an AI client call (model request). - - Args: - messages: Full conversation history (list of messages) - agent: Agent object - model: Model object - model_settings: Model settings - """ - # Determine model name for span name - model_obj = model - if agent and hasattr(agent, "model"): - model_obj = agent.model - - model_name = _get_model_name(model_obj) or "unknown" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - # Set streaming flag from contextvar - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - - # Add available tools if agent is available - agent_obj = agent or get_current_agent() - _set_available_tools(span, agent_obj) - - # Set input messages (full conversation history) - if messages: - _set_input_messages(span, messages) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" -) -> None: - """Update the AI client span with response data.""" - if not span: - return - - # Set usage data if available - if model_response and hasattr(model_response, "usage"): - _set_usage_data(span, model_response.usage) - - # Set output data - _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py deleted file mode 100644 index 7648c1418a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _should_send_prompts - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from pydantic_ai._tool_manager import ToolDefinition # type: ignore - - -def execute_tool_span( - tool_name: str, - tool_args: "Any", - agent: "Any", - tool_definition: "Optional[ToolDefinition]" = None, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for tool execution. - - Args: - tool_name: The name of the tool being executed - tool_args: The arguments passed to the tool - agent: The agent executing the tool - tool_definition: The definition of the tool, if available - """ - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - set_on_span = span.set_data - - if tool_definition is not None and hasattr(tool_definition, "description"): - set_on_span( - SPANDATA.GEN_AI_TOOL_DESCRIPTION, - tool_definition.description, - ) - - _set_agent_data(span, agent) - - if _should_send_prompts() and tool_args is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - """Update the execute tool span with the result.""" - if not span: - return - - if not _should_send_prompts() or result is None: - return - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - else: - span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py deleted file mode 100644 index f0c68e85ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ /dev/null @@ -1,184 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, -) - -if TYPE_CHECKING: - from typing import Any, Union - -try: - from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore -except ImportError: - BinaryContent = None - ImageUrl = None - - -def invoke_agent_span( - user_prompt: "Any", - agent: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = False, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for invoking the agent.""" - # Determine agent name for span - name = "agent" - if agent and getattr(agent, "name", None): - name = agent.name - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - _set_available_tools(span, agent) - - # Add user prompt and system prompts if available and prompts are enabled - if _should_send_prompts(): - messages = [] - - # Add system prompts (both instructions and system_prompt) - system_texts = [] - - if agent: - # Check for system_prompt - system_prompts = getattr(agent, "_system_prompts", None) or [] - for prompt in system_prompts: - if isinstance(prompt, str): - system_texts.append(prompt) - - # Check for instructions (stored in _instructions) - instructions = getattr(agent, "_instructions", None) - if instructions: - if isinstance(instructions, str): - system_texts.append(instructions) - elif isinstance(instructions, (list, tuple)): - for instr in instructions: - if isinstance(instr, str): - system_texts.append(instr) - elif callable(instr): - # Skip dynamic/callable instructions - pass - - # Add all system texts as system messages - for system_text in system_texts: - messages.append( - { - "content": [{"text": system_text, "type": "text"}], - "role": "system", - } - ) - - # Add user prompt - if user_prompt: - if isinstance(user_prompt, str): - messages.append( - { - "content": [{"text": user_prompt, "type": "text"}], - "role": "user", - } - ) - elif isinstance(user_prompt, list): - # Handle list of user content - content = [] - for item in user_prompt: - if isinstance(item, str): - content.append({"text": item, "type": "text"}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - if content: - messages.append( - { - "content": content, - "role": "user", - } - ) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - result: "Any", -) -> None: - """Update and close the invoke agent span.""" - if not span or not result: - return - - # Extract output from result - output = getattr(result, "output", None) - - # Set response text if prompts are enabled - if _should_send_prompts() and output: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False - ) - - # Set model name from response if available - if hasattr(result, "response"): - try: - response = result.response - if hasattr(response, "model_name") and response.model_name: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - except Exception: - # If response access fails, continue without setting model name - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py deleted file mode 100644 index 330496c6b2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/spans/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Utility functions for PydanticAI span instrumentation.""" - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX -from sentry_sdk.ai.utils import get_modality_from_mime_type -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore - - -def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": - """Serialize an ImageUrl content item for span data. - - For data URLs containing base64-encoded images, the content is redacted. - For regular HTTP URLs, the URL string is preserved. - """ - url = str(item.url) - data_url_match = DATA_URL_BASE64_REGEX.match(url) - - if data_url_match: - return { - "type": "image", - "content": BLOB_DATA_SUBSTITUTE, - } - - return { - "type": "image", - "content": url, - } - - -def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": - """Serialize a BinaryContent item for span data, redacting the blob data.""" - return { - "type": "blob", - "modality": get_modality_from_mime_type(item.media_type), - "mime_type": item.media_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - usage: "Union[RequestUsage, RunUsage]", -) -> None: - """Set token usage data on a span. - - This function works with both RequestUsage (single request) and - RunUsage (agent run) objects from pydantic_ai. - - Args: - span: The Sentry span to set data on. - usage: RequestUsage or RunUsage object containing token usage information. - """ - if usage is None: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if hasattr(usage, "input_tokens") and usage.input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - - # Pydantic AI uses cache_read_tokens (not input_tokens_cached) - if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) - - # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) - if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - usage.cache_write_tokens, - ) - - if hasattr(usage, "output_tokens") and usage.output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - - if hasattr(usage, "total_tokens") and usage.total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py deleted file mode 100644 index 340dcf8953..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pydantic_ai/utils.py +++ /dev/null @@ -1,233 +0,0 @@ -from contextvars import ContextVar -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - -# Store the current agent context in a contextvar for re-entrant safety -# Using a list as a stack to support nested agent calls -_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( - "pydantic_ai_agent_context_stack", default=[] -) - - -def push_agent(agent: "Any", is_streaming: bool = False) -> None: - """Push an agent context onto the stack along with its streaming flag.""" - stack = _agent_context_stack.get().copy() - stack.append({"agent": agent, "is_streaming": is_streaming}) - _agent_context_stack.set(stack) - - -def pop_agent() -> None: - """Pop an agent context from the stack.""" - stack = _agent_context_stack.get().copy() - if stack: - stack.pop() - _agent_context_stack.set(stack) - - -def get_current_agent() -> "Any": - """Get the current agent from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1]["agent"] - return None - - -def get_is_streaming() -> bool: - """Get the streaming flag from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1].get("is_streaming", False) - return False - - -def _should_send_prompts() -> bool: - """ - Check if prompts should be sent to Sentry. - - This checks both send_default_pii and the include_prompts integration setting. - """ - if not should_send_default_pii(): - return False - - from . import PydanticAIIntegration - - # Get the integration instance from the client - integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) - - if integration is None: - return False - - return getattr(integration, "include_prompts", False) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set agent-related data on a span. - - Args: - span: The span to set data on - agent: Agent object (can be None, will try to get from contextvar if not provided) - """ - # Extract agent name from agent object or contextvar - agent_obj = agent - if not agent_obj: - # Try to get from contextvar - agent_obj = get_current_agent() - - if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - else: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - - -def _get_model_name(model_obj: "Any") -> "Optional[str]": - """Extract model name from a model object. - - Args: - model_obj: Model object to extract name from - - Returns: - Model name string or None if not found - """ - if not model_obj: - return None - - if hasattr(model_obj, "model_name"): - return model_obj.model_name - elif hasattr(model_obj, "name"): - try: - return model_obj.name() - except Exception: - return str(model_obj) - elif isinstance(model_obj, str): - return model_obj - else: - return str(model_obj) - - -def _set_model_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - model: "Any", - model_settings: "Any", -) -> None: - """Set model-related data on a span. - - Args: - span: The span to set data on - model: Model object (can be None, will try to get from agent if not provided) - model_settings: Model settings (can be None, will try to get from agent if not provided) - """ - # Try to get agent from contextvar if we need it - agent_obj = get_current_agent() - - # Extract model information - model_obj = model - if not model_obj and agent_obj and hasattr(agent_obj, "model"): - model_obj = agent_obj.model - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if model_obj: - # Set system from model - if hasattr(model_obj, "system"): - set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) - - # Set model name - model_name = _get_model_name(model_obj) - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Extract model settings - settings = model_settings - if not settings and agent_obj and hasattr(agent_obj, "model_settings"): - settings = agent_obj.model_settings - - if settings: - settings_map = { - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - } - - # ModelSettings is a TypedDict (dict at runtime), so use dict access - if isinstance(settings, dict): - for setting_name, spandata_key in settings_map.items(): - value = settings.get(setting_name) - if value is not None: - set_on_span(spandata_key, value) - else: - # Fallback for object-style settings - for setting_name, spandata_key in settings_map.items(): - if hasattr(settings, setting_name): - value = getattr(settings, setting_name) - if value is not None: - set_on_span(spandata_key, value) - - -def _set_available_tools( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set available tools data on a span from an agent's function toolset. - - Args: - span: The span to set data on - agent: Agent object with _function_toolset attribute - """ - if not agent or not hasattr(agent, "_function_toolset"): - return - - try: - tools = [] - # Get tools from the function toolset - if hasattr(agent._function_toolset, "tools"): - for tool_name, tool in agent._function_toolset.tools.items(): - tool_info = {"name": tool_name} - - # Add description from function_schema if available - if hasattr(tool, "function_schema"): - schema = tool.function_schema - if getattr(schema, "description", None): - tool_info["description"] = schema.description - - # Add parameters from json_schema - if getattr(schema, "json_schema", None): - tool_info["parameters"] = schema.json_schema - - tools.append(tool_info) - - if tools: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - else: - span.set_data( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - except Exception: - # If we can't extract tools, just skip it - pass - - -def _capture_exception(exc: "Any", handled: bool = False) -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pydantic_ai", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py deleted file mode 100644 index 2616f4d5a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pymongo.py +++ /dev/null @@ -1,262 +0,0 @@ -import copy -import json - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -try: - from pymongo import monitoring -except ImportError: - raise DidNotEnable("Pymongo not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pymongo.monitoring import ( - CommandFailedEvent, - CommandStartedEvent, - CommandSucceededEvent, - ) - - -SAFE_COMMAND_ATTRIBUTES = [ - "insert", - "ordered", - "find", - "limit", - "singleBatch", - "aggregate", - "createIndexes", - "indexes", - "delete", - "findAndModify", - "renameCollection", - "to", - "drop", -] - - -def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": - for key in command: - is_safe_field = key in SAFE_COMMAND_ATTRIBUTES - if is_safe_field: - # Skip if safe key - continue - - update_db_command = key == "update" and "findAndModify" not in command - if update_db_command: - # Also skip "update" db command because it is save. - # There is also an "update" key in the "findAndModify" command, which is NOT safe! - continue - - # Special stripping for documents - is_document = key == "documents" - if is_document: - for doc in command[key]: - for doc_key in doc: - doc[doc_key] = "%s" - continue - - # Special stripping for dict style fields - is_dict_field = key in ["filter", "query", "update"] - if is_dict_field: - for item_key in command[key]: - command[key][item_key] = "%s" - continue - - # For pipeline fields strip the `$match` dict - is_pipeline_field = key == "pipeline" - if is_pipeline_field: - for pipeline in command[key]: - for match_key in pipeline["$match"] if "$match" in pipeline else []: - pipeline["$match"][match_key] = "%s" - continue - - # Default stripping - command[key] = "%s" - - return command - - -def _get_db_data(event: "Any") -> "Dict[str, Any]": - data = {} - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - data[SPANDATA.DB_DRIVER_NAME] = "pymongo" - db_name = event.database_name - - server_address = event.connection_id[0] - if server_address is not None: - data[SPANDATA.SERVER_ADDRESS] = server_address - - server_port = event.connection_id[1] - if server_port is not None: - data[SPANDATA.SERVER_PORT] = server_port - - if is_span_streaming_enabled: - data["db.system.name"] = "mongodb" - - if db_name is not None: - data["db.namespace"] = db_name - else: - data[SPANDATA.DB_SYSTEM] = "mongodb" - - if db_name is not None: - data[SPANDATA.DB_NAME] = db_name - - return data - - -class CommandTracer(monitoring.CommandListener): - def __init__(self) -> None: - self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} - - def _operation_key( - self, - event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", - ) -> int: - return event.request_id - - def started(self, event: "CommandStartedEvent") -> None: - client = sentry_sdk.get_client() - if client.get_integration(PyMongoIntegration) is None: - return - - with capture_internal_exceptions(): - command = dict(copy.deepcopy(event.command)) - - command.pop("$db", None) - command.pop("$clusterTime", None) - command.pop("$signature", None) - - db_data = _get_db_data(event) - - collection_name = command.get(event.command_name) - operation_name = event.command_name - db_name = event.database_name - - lsid = command.pop("lsid", None) - if not should_send_default_pii(): - command = _strip_pii(command) - - query = json.dumps(command, default=str) - - if has_span_streaming_enabled(client.options): - span_first_data = { - "db.operation.name": operation_name, - "db.collection.name": collection_name, - SPANDATA.DB_QUERY_TEXT: query, - "sentry.op": OP.DB, - "sentry.origin": PyMongoIntegration.origin, - **db_data, - } - - span = sentry_sdk.traces.start_span( - name=query, attributes=span_first_data - ) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, - category="query", - type=OP.DB, - data=span_first_data, - ) - - else: - tags = { - "db.name": db_name, - SPANDATA.DB_SYSTEM: "mongodb", - SPANDATA.DB_DRIVER_NAME: "pymongo", - SPANDATA.DB_OPERATION: operation_name, - # The below is a deprecated field, but leaving for legacy reasons. - # The v2 spans will use `db.collection.name` instead. - SPANDATA.DB_MONGODB_COLLECTION: collection_name, - } - - try: - tags["net.peer.name"] = event.connection_id[0] - tags["net.peer.port"] = str(event.connection_id[1]) - except TypeError: - pass - - data: "Dict[str, Any]" = {"operation_ids": {}} - data["operation_ids"]["operation"] = event.operation_id - data["operation_ids"]["request"] = event.request_id - - data.update(db_data) - - try: - if lsid: - lsid_id = lsid["id"] - data["operation_ids"]["session"] = str(lsid_id) - except KeyError: - pass - - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=PyMongoIntegration.origin, - ) - - for tag, value in tags.items(): - # set the tag for backwards-compatibility. - # TODO: remove the set_tag call in the next major release! - span.set_tag(tag, value) - span.set_data(tag, value) - - for key, value in data.items(): - span.set_data(key, value) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, category="query", type=OP.DB, data=tags - ) - - self._ongoing_operations[self._operation_key(event)] = span.__enter__() - - def failed(self, event: "CommandFailedEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - # Ignoring NoOpStreamedSpan as it will always have a status of "ok" - if type(span) is StreamedSpan: - span.status = SpanStatus.ERROR - elif type(span) is Span: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - span.__exit__(None, None, None) - except KeyError: - return - - def succeeded(self, event: "CommandSucceededEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - if type(span) is Span: - span.set_status(SPANSTATUS.OK) - span.__exit__(None, None, None) - except KeyError: - pass - - -class PyMongoIntegration(Integration): - identifier = "pymongo" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py deleted file mode 100644 index 6837d8345c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyramid.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -import os -import sys -import weakref - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -try: - from pyramid.httpexceptions import HTTPException - from pyramid.request import Request -except ImportError: - raise DidNotEnable("Pyramid not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional - - from pyramid.response import Response - from webob.cookies import RequestCookies - from webob.request import _FieldStorageWithFile - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.utils import ExcInfo - - -if getattr(Request, "authenticated_userid", None): - - def authenticated_userid(request: "Request") -> "Optional[Any]": - return request.authenticated_userid - -else: - # bw-compat for pyramid < 1.5 - from pyramid.security import authenticated_userid # type: ignore - - -TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") - - -class PyramidIntegration(Integration): - identifier = "pyramid" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "route_name") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - from pyramid import router - - old_call_view = router._call_view - - @functools.wraps(old_call_view) - def sentry_patched_call_view( - registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - integration = client.get_integration(PyramidIntegration) - if integration is None: - return old_call_view(registry, request, *args, **kwargs) - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii() and has_span_streaming_enabled(client.options): - user_id = authenticated_userid(request) - if user_id: - scope.set_user({"id": user_id}) - - scope.add_event_processor( - _make_event_processor(weakref.ref(request), integration) - ) - - return old_call_view(registry, request, *args, **kwargs) - - router._call_view = sentry_patched_call_view - - if hasattr(Request, "invoke_exception_view"): - old_invoke_exception_view = Request.invoke_exception_view - - def sentry_patched_invoke_exception_view( - self: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - rv = old_invoke_exception_view(self, *args, **kwargs) - - if ( - self.exc_info - and all(self.exc_info) - and rv.status_int == 500 - and sentry_sdk.get_client().get_integration(PyramidIntegration) - is not None - ): - _capture_exception(self.exc_info) - - return rv - - Request.invoke_exception_view = sentry_patched_invoke_exception_view - - old_wsgi_call = router.Router.__call__ - - @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) - def sentry_patched_wsgi_call( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - def sentry_patched_inner_wsgi_call( - environ: "Dict[str, Any]", start_response: "Callable[..., Any]" - ) -> "Any": - try: - return old_wsgi_call(self, environ, start_response) - except Exception: - einfo = sys.exc_info() - _capture_exception(einfo) - reraise(*einfo) - - middleware = SentryWsgiMiddleware( - sentry_patched_inner_wsgi_call, - span_origin=PyramidIntegration.origin, - ) - return middleware(environ, start_response) - - router.Router.__call__ = sentry_patched_wsgi_call - - -@ensure_integration_enabled(PyramidIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - if exc_info[0] is None or issubclass(exc_info[0], HTTPException): - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pyramid", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "route_name": request.matched_route.name, - "route_pattern": request.matched_route.pattern, - } - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if is_span_streaming_enabled - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - scope.set_transaction_name( - name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -class PyramidRequestExtractor(RequestExtractor): - def url(self) -> str: - return self.request.path_url - - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "RequestCookies": - return self.request.cookies - - def raw_data(self) -> str: - return self.request.text - - def form(self) -> "Dict[str, str]": - return { - key: value - for key, value in self.request.POST.items() - if not getattr(value, "filename", None) - } - - def files(self) -> "Dict[str, _FieldStorageWithFile]": - return { - key: value - for key, value in self.request.POST.items() - if getattr(value, "filename", None) - } - - def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: - file = postdata.file - try: - return os.fstat(file.fileno()).st_size - except Exception: - return 0 - - -def _make_event_processor( - weak_request: "Callable[[], Request]", integration: "PyramidIntegration" -) -> "EventProcessor": - def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - PyramidRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_info = event.setdefault("user", {}) - user_info.setdefault("id", authenticated_userid(request)) - - return event - - return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py deleted file mode 100644 index aae68c4c10..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/pyreqwest.py +++ /dev/null @@ -1,197 +0,0 @@ -from contextlib import contextmanager -from typing import Any, Generator - -import sentry_sdk -from sentry_sdk import start_span -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - logger, - parse_url, -) - -try: - from pyreqwest.client import ( # type: ignore[import-not-found] - ClientBuilder, - SyncClientBuilder, - ) - from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] - from pyreqwest.request import ( # type: ignore[import-not-found] - OneOffRequestBuilder, - Request, - SyncOneOffRequestBuilder, - ) - from pyreqwest.response import ( # type: ignore[import-not-found] - Response, - SyncResponse, - ) -except ImportError: - raise DidNotEnable("pyreqwest not installed or incompatible version installed") - - -class PyreqwestIntegration(Integration): - identifier = "pyreqwest" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_pyreqwest() - - -def _patch_pyreqwest() -> None: - # Patch Client Builders - _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) - _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) - - # Patch Request Builders - _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) - _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) - - -def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: - if not hasattr(cls, method_name): - return - - original_method = getattr(cls, method_name) - - def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - if not getattr(self, "_sentry_instrumented", False): - integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) - if integration is not None: - self.with_middleware(middleware) - try: - self._sentry_instrumented = True - except (TypeError, AttributeError): - # In case the instance itself is immutable or doesn't allow extra attributes - pass - return original_method(self, *args, **kwargs) - - setattr(cls, method_name, sentry_patched_method) - - -@contextmanager -def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": PyreqwestIntegration.origin, - SPANDATA.HTTP_REQUEST_METHOD: request.method, - }, - ) as span: - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - return - - with start_span( - op=OP.HTTP_CLIENT, - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - origin=PyreqwestIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - -async def sentry_async_middleware( - request: "Request", next_handler: "Next" -) -> "Response": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return await next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = await next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response - - -def sentry_sync_middleware( - request: "Request", next_handler: "SyncNext" -) -> "SyncResponse": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py deleted file mode 100644 index 6a5603d825..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/quart.py +++ /dev/null @@ -1,292 +0,0 @@ -import asyncio -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Union - - from sentry_sdk._types import Event, EventProcessor - -try: - import quart_auth # type: ignore -except ImportError: - quart_auth = None - -try: - from quart import ( # type: ignore - Quart, - Request, - has_request_context, - has_websocket_context, - request, - websocket, - ) - from quart.signals import ( # type: ignore - got_background_exception, - got_request_exception, - got_websocket_exception, - request_started, - websocket_started, - ) -except ImportError: - raise DidNotEnable("Quart is not installed") -else: - # Quart 0.19 is based on Flask and hence no longer has a Scaffold - try: - from quart.scaffold import Scaffold # type: ignore - except ImportError: - from flask.sansio.scaffold import Scaffold # type: ignore - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class QuartIntegration(Integration): - identifier = "quart" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "endpoint") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - request_started.connect(_request_websocket_started) - websocket_started.connect(_request_websocket_started) - got_background_exception.connect(_capture_exception) - got_request_exception.connect(_capture_exception) - got_websocket_exception.connect(_capture_exception) - - patch_asgi_app() - patch_scaffold_route() - - -def patch_asgi_app() -> None: - old_app = Quart.__call__ - - async def sentry_patched_asgi_app( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - if sentry_sdk.get_client().get_integration(QuartIntegration) is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=QuartIntegration.origin, - asgi_version=3, - ) - return await middleware(scope, receive, send) - - Quart.__call__ = sentry_patched_asgi_app - - -def patch_scaffold_route() -> None: - # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 - if sys.version_info >= (3, 12): - iscoroutinefunction = inspect.iscoroutinefunction - else: - iscoroutinefunction = asyncio.iscoroutinefunction - - old_route = Scaffold.route - - def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": - old_decorator = old_route(*args, **kwargs) - - def decorator(old_func: "Any") -> "Any": - if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): - - @wraps(old_func) - @ensure_integration_enabled(QuartIntegration, old_func) - def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - span = get_current_span() - if span is not None and hasattr(span, "_segment"): - span._segment._update_active_thread() - else: - current_scope = sentry_sdk.get_current_scope() - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_func(*args, **kwargs) - - return old_decorator(_sentry_func) - - return old_decorator(old_func) - - return decorator - - Scaffold.route = _sentry_route - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if has_span_streaming_enabled(sentry_sdk.get_client().options) - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - - scope.set_transaction_name( - name=name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - if has_request_context(): - request_websocket = request._get_current_object() - if has_websocket_context(): - request_websocket = websocket._get_current_object() - - # Set the transaction name here, but rely on ASGI middleware - # to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket - ) - - scope = sentry_sdk.get_isolation_scope() - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - current_span = get_current_span() - if type(current_span) is StreamedSpan: - segment = current_span._segment - - segment.set_attribute("http.request.method", request_websocket.method) - header_attributes: "dict[str, Any]" = {} - - for header, header_value in _filter_headers( - dict(request_websocket.headers), use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - segment.set_attributes(header_attributes) - - if should_send_default_pii(): - segment.set_attribute("url.full", request_websocket.url) - segment.set_attribute( - "url.query", - request_websocket.query_string.decode("utf-8", errors="replace"), - ) - - user_properties = {} - if len(request_websocket.access_route) >= 1: - segment.set_attribute( - "client.address", request_websocket.access_route[0] - ) - user_properties["ip_address"] = request_websocket.access_route[0] - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_properties["id"] = current_user_id - - if user_properties: - existing_user_properties = scope._user or {} - scope.set_user({**existing_user_properties, **user_properties}) - - evt_processor = _make_request_event_processor(app, request_websocket, integration) - scope.add_event_processor(evt_processor) - - -def _make_request_event_processor( - app: "Quart", request: "Request", integration: "QuartIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - # TODO: Figure out what to do with request body. Methods on request - # are async, but event processors are not. - - request_info = event.setdefault("request", {}) - request_info["url"] = request.url - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - if len(request.access_route) >= 1: - request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_info = event.setdefault("user", {}) - user_info["id"] = current_user_id - - return event - - return inner - - -async def _capture_exception( - sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "quart", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_current_user_id_from_quart() -> "str | None": - if quart_auth is None: - return None - - if quart_auth.current_user is None: - return None - - try: - return quart_auth.current_user._auth_id - except Exception: - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py deleted file mode 100644 index f723a96f3c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/ray.py +++ /dev/null @@ -1,240 +0,0 @@ -import functools -import inspect -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - event_from_exception, - logger, - package_version, - qualname_from_function, - reraise, -) - -try: - import ray # type: ignore[import-not-found] - from ray import remote -except ImportError: - raise DidNotEnable("Ray not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional - - from sentry_sdk.utils import ExcInfo - - -def _check_sentry_initialized() -> None: - if sentry_sdk.get_client().is_active(): - return - - logger.debug( - "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." - ) - - -def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: - # Patching new_func signature to add the _sentry_tracing parameter to it - # Ray later inspects the signature and finds the unexpected parameter otherwise - signature = inspect.signature(func) - params = list(signature.parameters.values()) - sentry_tracing_param = inspect.Parameter( - "_sentry_tracing", - kind=inspect.Parameter.KEYWORD_ONLY, - default=None, - ) - - # Keyword only arguments are penultimate if function has variadic keyword arguments - if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: - params.insert(-1, sentry_tracing_param) - else: - params.append(sentry_tracing_param) - - func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] - - -def _patch_ray_remote() -> None: - old_remote = remote - - @functools.wraps(old_remote) - def new_remote( - f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" - ) -> "Callable[..., Any]": - if inspect.isclass(f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(f, *args, **kwargs) - - def wrapper(user_f: "Callable[..., Any]") -> "Any": - if inspect.isclass(user_f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(*args, **kwargs)(user_f) - - @functools.wraps(user_f) - def new_func( - *f_args: "Any", - _sentry_tracing: "Optional[dict[str, Any]]" = None, - **f_kwargs: "Any", - ) -> "Any": - _check_sentry_initialized() - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(_sentry_tracing or {}) - - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RAY, - "sentry.origin": RayIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - }, - parent_span=None, - ): - try: - result = user_f(*f_args, **f_kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - transaction = sentry_sdk.continue_trace( - _sentry_tracing or {}, - op=OP.QUEUE_TASK_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - source=TransactionSource.TASK, - ) - - with sentry_sdk.start_transaction(transaction) as transaction: - try: - result = user_f(*f_args, **f_kwargs) - transaction.set_status(SPANSTATUS.OK) - except Exception: - transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - _insert_sentry_tracing_in_signature(new_func) - - if f: - rv = old_remote(new_func) - else: - rv = old_remote(*args, **kwargs)(new_func) - old_remote_method = rv.remote - - def _remote_method_with_header_propagation( - *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Ray Client - """ - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_RAY, - "sentry.origin": RayIntegration.origin, - }, - ): - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - ) as span: - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - span.set_status(SPANSTATUS.OK) - except Exception: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - rv.remote = _remote_method_with_header_propagation - - return rv - - if f is not None: - return wrapper(f) - else: - return wrapper - - ray.remote = new_remote - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={ - "handled": False, - "type": RayIntegration.identifier, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class RayIntegration(Integration): - identifier = "ray" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = package_version("ray") - _check_minimum_version(RayIntegration, version) - - _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py deleted file mode 100644 index 7095721ed2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE -from sentry_sdk.integrations.redis.rb import _patch_rb -from sentry_sdk.integrations.redis.redis import _patch_redis -from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster -from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -class RedisIntegration(Integration): - identifier = "redis" - - def __init__( - self, - max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, - cache_prefixes: "Optional[list[str]]" = None, - ) -> None: - self.max_data_size = max_data_size - self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] - - if max_data_size is not None: - warnings.warn( - "The `max_data_size` parameter of `RedisIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - try: - from redis import StrictRedis, client - except ImportError: - raise DidNotEnable("Redis client not installed") - - _patch_redis(StrictRedis, client) - _patch_redis_cluster() - _patch_rb() - - try: - _patch_rediscluster() - except Exception: - logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py deleted file mode 100644 index 8fc3d0c3a9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_async_common.py +++ /dev/null @@ -1,177 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from redis.asyncio.client import Pipeline, StrictRedis - from redis.asyncio.cluster import ClusterPipeline, RedisCluster - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_async_pipeline( - pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return await old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - try: - command_seq = self._execution_strategy._command_queue - except AttributeError: - if is_cluster: - command_seq = self._command_stack - else: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.is_transaction, - command_seq, - ) - - return await old_execute(self, *args, **kwargs) - - pipeline_cls.execute = _sentry_execute # type: ignore - - -def patch_redis_async_client( - cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return await old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = await old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py deleted file mode 100644 index 58d686b099..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/_sync_common.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_pipeline( - pipeline_cls: "Any", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - command_seq = None - try: - command_seq = self._execution_strategy.command_queue - except AttributeError: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.transaction, - command_seq, - ) - - return old_execute(self, *args, **kwargs) - - pipeline_cls.execute = sentry_patched_execute - - -def patch_redis_client( - cls: "Any", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - """ - This function can be used to instrument custom redis client classes or - subclasses. - """ - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py deleted file mode 100644 index 0822c2c930..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/consts.py +++ /dev/null @@ -1,19 +0,0 @@ -SPAN_ORIGIN = "auto.db.redis" - -_SINGLE_KEY_COMMANDS = frozenset( - ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], -) -_MULTI_KEY_COMMANDS = frozenset( - [ - "del", - "touch", - "unlink", - "mget", - ], -) -_COMMANDS_INCLUDING_SENSITIVE_DATA = [ - "auth", -] -_MAX_NUM_ARGS = 10 # Trim argument lists to this many values -_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values -_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py deleted file mode 100644 index 35f20fddd9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/caches.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Code used for the Caches module in Sentry -""" - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -GET_COMMANDS = ("get", "mget") -SET_COMMANDS = ("set", "setex") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _get_op(name: str) -> "Optional[str]": - op = None - if name.lower() in GET_COMMANDS: - op = OP.CACHE_GET - elif name.lower() in SET_COMMANDS: - op = OP.CACHE_PUT - - return op - - -def _compile_cache_span_properties( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> "dict[str, Any]": - key = _get_safe_key(redis_command, args, kwargs) - key_as_string = _key_as_string(key) - keys_as_string = key_as_string.split(", ") - - is_cache_key = False - for prefix in integration.cache_prefixes: - for kee in keys_as_string: - if kee.startswith(prefix): - is_cache_key = True - break - if is_cache_key: - break - - value = None - if redis_command.lower() in SET_COMMANDS: - value = args[-1] - - properties = { - "op": _get_op(redis_command), - "description": _get_cache_span_description( - redis_command, args, kwargs, integration - ), - "key": key, - "key_as_string": key_as_string, - "redis_command": redis_command.lower(), - "is_cache_key": is_cache_key, - "value": value, - } - - return properties - - -def _get_cache_span_description( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> str: - description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_cache_data( - span: "Union[Span, StreamedSpan]", - redis_client: "Any", - properties: "dict[str, Any]", - return_value: "Optional[Any]", -) -> None: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - with capture_internal_exceptions(): - set_on_span(SPANDATA.CACHE_KEY, properties["key"]) - - if properties["redis_command"] in GET_COMMANDS: - if return_value is not None: - set_on_span(SPANDATA.CACHE_HIT, True) - size = ( - len(str(return_value).encode("utf-8")) - if not isinstance(return_value, bytes) - else len(return_value) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - else: - set_on_span(SPANDATA.CACHE_HIT, False) - - elif properties["redis_command"] in SET_COMMANDS: - if properties["value"] is not None: - size = ( - len(properties["value"].encode("utf-8")) - if not isinstance(properties["value"], bytes) - else len(properties["value"]) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - - try: - connection_params = redis_client.connection_pool.connection_kwargs - except AttributeError: - # If it is a cluster, there is no connection_pool attribute so we - # need to get the default node from the cluster instance - default_node = redis_client.get_default_node() - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - - host = connection_params.get("host") - if host is not None: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) - - port = connection_params.get("port") - if port is not None: - set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py deleted file mode 100644 index 69207bf6f6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/modules/queries.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Code used for the Queries module in Sentry -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_command -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import Redis - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _compile_db_span_properties( - integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" -) -> "dict[str, Any]": - description = _get_db_span_description(integration, redis_command, args) - - properties = { - "op": OP.DB_REDIS, - "description": description, - } - - return properties - - -def _get_db_span_description( - integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" -) -> str: - description = command_name - - with capture_internal_exceptions(): - description = _get_safe_command(command_name, args) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_db_data_on_span( - span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" -) -> None: - db = connection_params.get("db") - host = connection_params.get("host") - port = connection_params.get("port") - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) - - if host is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_attribute(SPANDATA.SERVER_PORT, port) - - else: - span.set_data(SPANDATA.DB_SYSTEM, "redis") - span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_data(SPANDATA.DB_NAME, str(db)) - - if host is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_data(SPANDATA.SERVER_PORT, port) - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" -) -> None: - try: - _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) - except AttributeError: - pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py deleted file mode 100644 index e2ce863fe8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/rb.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Instrumentation for Redis Blaster (rb) - -https://github.com/getsentry/rb -""" - -from sentry_sdk.integrations.redis._sync_common import patch_redis_client -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - - -def _patch_rb() -> None: - try: - import rb.clients # type: ignore - except ImportError: - pass - else: - patch_redis_client( - rb.clients.FanoutClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.MappingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.RoutingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py deleted file mode 100644 index e704c9bc6a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Instrumentation for Redis - -https://github.com/redis/redis-py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - -if TYPE_CHECKING: - from typing import Any, Sequence - - -def _get_redis_command_args(command: "Any") -> "Sequence[Any]": - return command[0] - - -def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 - patch_redis_client( - StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_pipeline( - client.Pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - try: - strict_pipeline = client.StrictPipeline - except AttributeError: - pass - else: - patch_redis_pipeline( - strict_pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - - try: - import redis.asyncio - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - redis.asyncio.client.StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_async_pipeline( - redis.asyncio.client.Pipeline, - False, - _get_redis_command_args, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py deleted file mode 100644 index b6c95e6abd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_cluster.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Instrumentation for RedisCluster -This is part of the main redis-py client. - -https://github.com/redis/redis-py/blob/master/redis/cluster.py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import RedisCluster - from redis.asyncio.cluster import ( - ClusterPipeline as AsyncClusterPipeline, - ) - from redis.asyncio.cluster import ( - RedisCluster as AsyncRedisCluster, - ) - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -def _set_async_cluster_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_instance: "AsyncRedisCluster[Any]", -) -> None: - default_node = async_redis_cluster_instance.get_default_node() - if default_node is not None and default_node.connection_kwargs is not None: - _set_db_data_on_span(span, default_node.connection_kwargs) - - -def _set_async_cluster_pipeline_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", -) -> None: - with capture_internal_exceptions(): - client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) - if client is None: - # In older redis-py versions, the AsyncClusterPipeline had a `_client` - # attr but it is private so potentially problematic and mypy does not - # recognize it - see - # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 - client = ( - async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] - ) - - _set_async_cluster_db_data( - span, - client, - ) - - -def _set_cluster_db_data( - span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" -) -> None: - default_node = redis_cluster_instance.get_default_node() - - if default_node is not None: - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - _set_db_data_on_span(span, connection_params) - - -def _patch_redis_cluster() -> None: - """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" - try: - from redis import RedisCluster, cluster - except ImportError: - pass - else: - patch_redis_client( - RedisCluster, - is_cluster=True, - set_db_data_fn=_set_cluster_db_data, - ) - patch_redis_pipeline( - cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_cluster_db_data, - ) - - try: - from redis.asyncio import cluster as async_cluster - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - async_cluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_async_cluster_db_data, - ) - patch_redis_async_pipeline( - async_cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_async_cluster_pipeline_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py deleted file mode 100644 index 3437aa1f2f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Instrumentation for redis-py-cluster -The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). - -https://github.com/grokzen/redis-py-cluster -""" - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command - - -def _patch_rediscluster() -> None: - try: - import rediscluster # type: ignore - except ImportError: - return - - patch_redis_client( - rediscluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - - # up to v1.3.6, __version__ attribute is a tuple - # from v2.0.0, __version__ is a string and VERSION a tuple - version = getattr(rediscluster, "VERSION", rediscluster.__version__) - - # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 - # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst - if (0, 2, 0) < version < (2, 0, 0): - pipeline_cls = rediscluster.pipeline.StrictClusterPipeline - patch_redis_client( - rediscluster.StrictRedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - else: - pipeline_cls = rediscluster.pipeline.ClusterPipeline - - patch_redis_pipeline( - pipeline_cls, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py deleted file mode 100644 index 7e04df9c69..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/redis/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations.redis.consts import ( - _COMMANDS_INCLUDING_SENSITIVE_DATA, - _MAX_NUM_ARGS, - _MAX_NUM_COMMANDS, - _MULTI_KEY_COMMANDS, - _SINGLE_KEY_COMMANDS, -) -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - -if TYPE_CHECKING: - from typing import Any, Optional, Sequence, Union - - -def _get_safe_command(name: str, args: "Sequence[Any]") -> str: - command_parts = [name] - - name_low = name.lower() - send_default_pii = should_send_default_pii() - - for i, arg in enumerate(args): - if i > _MAX_NUM_ARGS: - break - - if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - continue - - arg_is_the_key = i == 0 - if arg_is_the_key: - command_parts.append(repr(arg)) - else: - if send_default_pii: - command_parts.append(repr(arg)) - else: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - - command = " ".join(command_parts) - return command - - -def _safe_decode(key: "Any") -> str: - if isinstance(key, bytes): - try: - return key.decode() - except UnicodeDecodeError: - return "" - - return str(key) - - -def _key_as_string(key: "Any") -> str: - if isinstance(key, (dict, list, tuple)): - key = ", ".join(_safe_decode(x) for x in key) - elif isinstance(key, bytes): - key = _safe_decode(key) - elif key is None: - key = "" - else: - key = str(key) - - return key - - -def _get_safe_key( - method_name: str, - args: "Optional[tuple[Any, ...]]", - kwargs: "Optional[dict[str, Any]]", -) -> "Optional[tuple[str, ...]]": - """ - Gets the key (or keys) from the given method_name. - The method_name could be a redis command or a django caching command - """ - key = None - - if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: - # for example redis "mget" - key = tuple(args) - - elif args is not None and len(args) >= 1: - # for example django "set_many/get_many" or redis "get" - if isinstance(args[0], (dict, list, tuple)): - key = tuple(args[0]) - else: - key = (args[0],) - - elif kwargs is not None and "key" in kwargs: - # this is a legacy case for older versions of Django - if isinstance(kwargs["key"], (list, tuple)): - if len(kwargs["key"]) > 0: - key = tuple(kwargs["key"]) - else: - if kwargs["key"] is not None: - key = (kwargs["key"],) - - return key - - -def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": - return command.args - - -def _set_pipeline_data( - span: "Union[Span, StreamedSpan]", - is_cluster: bool, - get_command_args_fn: "Any", - is_transaction: bool, - commands_seq: "Sequence[Any]", -) -> None: - # TODO: Remove this whole function when removing transaction based tracing - if isinstance(span, StreamedSpan): - return - - span.set_tag("redis.is_cluster", is_cluster) - span.set_tag("redis.transaction", is_transaction) - - commands = [] - for i, arg in enumerate(commands_seq): - if i >= _MAX_NUM_COMMANDS: - break - - command = get_command_args_fn(arg) - commands.append(_get_safe_command(command[0], command[1:])) - - span.set_data( - "redis.commands", - { - "count": len(commands_seq), - "first_ten": commands, - }, - ) - - -def _set_client_data( - span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" -) -> None: - if isinstance(span, StreamedSpan): - if name: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) - else: - span.set_tag("redis.is_cluster", is_cluster) - if name: - span.set_tag("redis.command", name) - span.set_tag(SPANDATA.DB_OPERATION, name) - - if name and args: - name_low = name.lower() - if (name_low in _SINGLE_KEY_COMMANDS) or ( - name_low in _MULTI_KEY_COMMANDS and len(args) == 1 - ): - if isinstance(span, StreamedSpan): - span.set_attribute("db.redis.key", args[0]) - else: - span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py deleted file mode 100644 index edd48a9f67..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rq.py +++ /dev/null @@ -1,225 +0,0 @@ -import functools -import weakref - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - format_timestamp, - parse_version, -) - -try: - from rq.job import JobStatus - from rq.queue import Queue - from rq.timeouts import JobTimeoutException - from rq.version import VERSION as RQ_VERSION - from rq.worker import Worker -except ImportError: - raise DidNotEnable("RQ not installed") - -try: - from rq.worker import BaseWorker - - if not hasattr(BaseWorker, "perform_job"): - BaseWorker = None -except ImportError: - BaseWorker = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - from rq.job import Job - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - -class RqIntegration(Integration): - identifier = "rq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(RQ_VERSION) - _check_minimum_version(RqIntegration, version) - - # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly - # instead of Worker, so we need to patch BaseWorker to cover both. - # For older versions where BaseWorker doesn't exist or doesn't have - # perform_job, we patch Worker. - worker_cls = BaseWorker if BaseWorker is not None else Worker - - old_perform_job = worker_cls.perform_job - - @functools.wraps(old_perform_job) - def sentry_patched_perform_job( - self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" - ) -> bool: - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_perform_job(self, job, *args, **kwargs) - - with sentry_sdk.new_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(weakref.ref(job))) - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace( - job.meta.get("_sentry_trace_headers") or {} - ) - - Scope.set_custom_sampling_context({"rq_job": job}) - - func_name = None - with capture_internal_exceptions(): - func_name = job.func_name - - with sentry_sdk.traces.start_span( - name="unknown RQ task" if func_name is None else func_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RQ, - "sentry.origin": RqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job.id, - }, - parent_span=None, - ) as span: - if func_name is not None: - span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) - - rv = old_perform_job(self, job, *args, **kwargs) - else: - transaction = continue_trace( - job.meta.get("_sentry_trace_headers") or {}, - op=OP.QUEUE_TASK_RQ, - name="unknown RQ task", - source=TransactionSource.TASK, - origin=RqIntegration.origin, - ) - - with capture_internal_exceptions(): - transaction.name = job.func_name - - with sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"rq_job": job}, - ): - rv = old_perform_job(self, job, *args, **kwargs) - - if self.is_horse: - # We're inside of a forked process and RQ is - # about to call `os._exit`. Make sure that our - # events get sent out. - sentry_sdk.get_client().flush() - - return rv - - worker_cls.perform_job = sentry_patched_perform_job - - old_handle_exception = worker_cls.handle_exception - - def sentry_patched_handle_exception( - self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" - ) -> "Any": - retry = ( - hasattr(job, "retries_left") - and job.retries_left - and job.retries_left > 0 - ) - failed = job._status == JobStatus.FAILED or job.is_failed - if failed and not retry: - _capture_exception(exc_info) - - return old_handle_exception(self, job, *exc_info, **kwargs) - - worker_cls.handle_exception = sentry_patched_handle_exception - - old_enqueue_job = Queue.enqueue_job - - @functools.wraps(old_enqueue_job) - def sentry_patched_enqueue_job( - self: "Queue", job: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_enqueue_job(self, job, **kwargs) - - scope = sentry_sdk.get_current_scope() - span = ( - scope.streamed_span - if has_span_streaming_enabled(client.options) - else scope.span - ) - if span is not None: - job.meta["_sentry_trace_headers"] = dict( - scope.iter_trace_propagation_headers() - ) - - return old_enqueue_job(self, job, **kwargs) - - Queue.enqueue_job = sentry_patched_enqueue_job - - ignore_logger("rq.worker") - - -def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - job = weak_job() - if job is not None: - with capture_internal_exceptions(): - extra = event.setdefault("extra", {}) - rq_job = { - "job_id": job.id, - "func": job.func_name, - "args": ( - job.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - job.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "description": job.description, - } - - if job.enqueued_at: - rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) - if job.started_at: - rq_job["started_at"] = format_timestamp(job.started_at) - - extra["rq-job"] = rq_job - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], JobTimeoutException): - event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "rq", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py deleted file mode 100644 index 622e3c17af..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/rust_tracing.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -This integration ingests tracing data from native extensions written in Rust. - -Using it requires additional setup on the Rust side to accept a -`RustTracingLayer` Python object and register it with the `tracing-subscriber` -using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: -```rust -#[pyfunction] -pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { - tracing_subscriber::registry() - .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) - .init(); -} -``` - -Usage in Python would then look like: -``` -sentry_sdk.init( - dsn=sentry_dsn, - integrations=[ - RustTracingIntegration( - "demo_rust_extension", - demo_rust_extension.initialize_tracing, - event_type_mapping=event_type_mapping, - ) - ], -) -``` - -Each native extension requires its own integration. -""" - -import json -from enum import Enum, auto -from typing import Any, Callable, Dict, Optional, Union - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - - -class RustTracingLevel(Enum): - Trace = "TRACE" - Debug = "DEBUG" - Info = "INFO" - Warn = "WARN" - Error = "ERROR" - - -class EventTypeMapping(Enum): - Ignore = auto() - Exc = auto() - Breadcrumb = auto() - Event = auto() - - -def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": - level = RustTracingLevel(level) - if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): - return "debug" - elif level == RustTracingLevel.Info: - return "info" - elif level == RustTracingLevel.Warn: - return "warning" - elif level == RustTracingLevel.Error: - return "error" - else: - # Better this than crashing - return "info" - - -def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": - metadata = event.get("metadata", {}) - contexts = {} - - location = {} - for field in ["module_path", "file", "line"]: - if field in metadata: - location[field] = metadata[field] - if len(location) > 0: - contexts["rust_tracing_location"] = location - - fields = {} - for field in metadata.get("fields", []): - fields[field] = event.get(field) - if len(fields) > 0: - contexts["rust_tracing_fields"] = fields - - return contexts - - -def process_event(event: "Dict[str, Any]") -> None: - metadata = event.get("metadata", {}) - - logger = metadata.get("target") - level = tracing_level_to_sentry_level(metadata.get("level")) - message: "sentry_sdk._types.Any" = event.get("message") - contexts = extract_contexts(event) - - sentry_event: "sentry_sdk._types.Event" = { - "logger": logger, - "level": level, - "message": message, - "contexts": contexts, - } - - sentry_sdk.capture_event(sentry_event) - - -def process_exception(event: "Dict[str, Any]") -> None: - process_event(event) - - -def process_breadcrumb(event: "Dict[str, Any]") -> None: - level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) - message = event.get("message") - - sentry_sdk.add_breadcrumb(level=level, message=message) - - -def default_span_filter(metadata: "Dict[str, Any]") -> bool: - return RustTracingLevel(metadata.get("level")) in ( - RustTracingLevel.Error, - RustTracingLevel.Warn, - RustTracingLevel.Info, - ) - - -def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": - level = RustTracingLevel(metadata.get("level")) - if level == RustTracingLevel.Error: - return EventTypeMapping.Exc - elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): - return EventTypeMapping.Breadcrumb - elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): - return EventTypeMapping.Ignore - else: - return EventTypeMapping.Ignore - - -class RustTracingLayer: - def __init__( - self, - origin: str, - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.origin = origin - self.event_type_mapping = event_type_mapping - self.span_filter = span_filter - self.include_tracing_fields = include_tracing_fields - - def _include_tracing_fields(self) -> bool: - """ - By default, the values of tracing fields are not included in case they - contain PII. A user may override that by passing `True` for the - `include_tracing_fields` keyword argument of this integration or by - setting `send_default_pii` to `True` in their Sentry client options. - """ - return ( - should_send_default_pii() - if self.include_tracing_fields is None - else self.include_tracing_fields - ) - - def on_event(self, event: str, sentry_span: "SentrySpan") -> None: - deserialized_event = json.loads(event) - metadata = deserialized_event.get("metadata", {}) - - event_type = self.event_type_mapping(metadata) - if event_type == EventTypeMapping.Ignore: - return - elif event_type == EventTypeMapping.Exc: - process_exception(deserialized_event) - elif event_type == EventTypeMapping.Breadcrumb: - process_breadcrumb(deserialized_event) - elif event_type == EventTypeMapping.Event: - process_event(deserialized_event) - - def on_new_span( - self, attrs: str, span_id: str - ) -> "Optional[Union[SentrySpan, StreamedSpan]]": - attrs = json.loads(attrs) - metadata = attrs.get("metadata", {}) - - if not self.span_filter(metadata): - return None - - module_path = metadata.get("module_path") - name = metadata.get("name") - message = attrs.get("message") - - if message is not None: - sentry_span_name = message - elif module_path is not None and name is not None: - sentry_span_name = f"{module_path}::{name}" # noqa: E231 - elif name is not None: - sentry_span_name = name - else: - sentry_span_name = "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - sentry_span = sentry_sdk.traces.start_span( - name=sentry_span_name, - attributes={ - "sentry.op": "function", - "sentry.origin": self.origin, - }, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_attribute(field, attrs.get(field)) - else: - sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) - - return sentry_span - - sentry_span = sentry_sdk.start_span( - op="function", - name=sentry_span_name, - origin=self.origin, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_data(field, attrs.get(field)) - else: - sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) - - sentry_span.__enter__() - return sentry_span - - def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: - if sentry_span is None: - return - - sentry_span.__exit__(None, None, None) - - def on_record( - self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" - ) -> None: - if sentry_span is None: - return - - set_on_span = ( - sentry_span.set_attribute - if isinstance(sentry_span, StreamedSpan) - else sentry_span.set_data - ) - - deserialized_values = json.loads(values) - for key, value in deserialized_values.items(): - if self._include_tracing_fields(): - set_on_span(key, value) - else: - set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) - - -class RustTracingIntegration(Integration): - """ - Ingests tracing data from a Rust native extension's `tracing` instrumentation. - - If a project uses more than one Rust native extension, each one will need - its own instance of `RustTracingIntegration` with an initializer function - specific to that extension. - - Since all of the setup for this integration requires instance-specific state - which is not available in `setup_once()`, setup instead happens in `__init__()`. - """ - - def __init__( - self, - identifier: str, - initializer: "Callable[[RustTracingLayer], None]", - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.identifier = identifier - origin = f"auto.function.rust_tracing.{identifier}" - self.tracing_layer = RustTracingLayer( - origin, event_type_mapping, span_filter, include_tracing_fields - ) - - initializer(self.tracing_layer) - - @staticmethod - def setup_once() -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py deleted file mode 100644 index 908fceb0cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sanic.py +++ /dev/null @@ -1,431 +0,0 @@ -import sys -import warnings -import weakref -from inspect import isawaitable -from typing import TYPE_CHECKING -from urllib.parse import urlsplit - -import sentry_sdk -from sentry_sdk import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -if TYPE_CHECKING: - from collections.abc import Container - from typing import Any, Callable, Dict, Optional, Union - - from sanic.request import Request, RequestParameters - from sanic.response import BaseHTTPResponse - from sanic.router import Route - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -try: - from sanic import Sanic - from sanic import __version__ as SANIC_VERSION - from sanic.exceptions import SanicException - from sanic.handlers import ErrorHandler - from sanic.router import Router -except ImportError: - raise DidNotEnable("Sanic not installed") - -old_error_handler_lookup = ErrorHandler.lookup -old_handle_request = Sanic.handle_request -old_router_get = Router.get - -try: - # This method was introduced in Sanic v21.9 - old_startup = Sanic._startup -except AttributeError: - pass - - -class SanicIntegration(Integration): - identifier = "sanic" - origin = f"auto.http.{identifier}" - version: "Optional[tuple[int, ...]]" = None - - def __init__( - self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) - ) -> None: - """ - The unsampled_statuses parameter can be used to specify for which HTTP statuses the - transactions should not be sent to Sentry. By default, transactions are sent for all - HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all - HTTP statuses, including 404. - """ - self._unsampled_statuses = unsampled_statuses or set() - - @staticmethod - def setup_once() -> None: - SanicIntegration.version = parse_version(SANIC_VERSION) - _check_minimum_version(SanicIntegration, SanicIntegration.version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - if SANIC_VERSION.startswith("0.8."): - # Sanic 0.8 and older creates a logger named "root" and puts a - # stringified version of every exception in there (without exc_info), - # which our error deduplication can't detect. - # - # We explicitly check the version here because it is a very - # invasive step to ignore this logger and not necessary in newer - # versions at all. - # - # https://github.com/huge-success/sanic/issues/1332 - ignore_logger("root") - - if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): - _setup_legacy_sanic() - return - - _setup_sanic() - - -class SanicRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return dict(self.request.cookies) - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "RequestParameters": - return self.request.form - - def is_json(self) -> bool: - raise NotImplementedError() - - def json(self) -> "Optional[Any]": - return self.request.json - - def files(self) -> "RequestParameters": - return self.request.files - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) - - -def _setup_sanic() -> None: - Sanic._startup = _startup - ErrorHandler.lookup = _sentry_error_handler_lookup - - -def _setup_legacy_sanic() -> None: - Sanic.handle_request = _legacy_handle_request - Router.get = _legacy_router_get - ErrorHandler.lookup = _sentry_error_handler_lookup - - -async def _startup(self: "Sanic") -> None: - # This happens about as early in the lifecycle as possible, just after the - # Request object is created. The body has not yet been consumed. - self.signal("http.lifecycle.request")(_context_enter) - - # This happens after the handler is complete. In v21.9 this signal is not - # dispatched when there is an exception. Therefore we need to close out - # and call _context_exit from the custom exception handler as well. - # See https://github.com/sanic-org/sanic/issues/2297 - self.signal("http.lifecycle.response")(_context_exit) - - # This happens inside of request handling immediately after the route - # has been identified by the router. - self.signal("http.routing.after")(_set_transaction) - - # The above signals need to be declared before this can be called. - await old_startup(self) - - -async def _context_enter(request: "Request") -> None: - request.ctx._sentry_do_integration = ( - sentry_sdk.get_client().get_integration(SanicIntegration) is not None - ) - - if not request.ctx._sentry_do_integration: - return - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - weak_request = weakref.ref(request) - request.ctx._sentry_scope = sentry_sdk.isolation_scope() - scope = request.ctx._sentry_scope.__enter__() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - if is_span_streaming_enabled: - integration = client.get_integration(SanicIntegration) - if ( - isinstance(integration, SanicIntegration) - and integration._unsampled_statuses - ): - warnings.warn( - "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", - stacklevel=2, - ) - - sentry_sdk.traces.continue_trace(dict(request.headers)) - scope.set_custom_sampling_context({"sanic_request": request}) - - if should_send_default_pii() and request.remote_addr: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) - - span = sentry_sdk.traces.start_span( - # Unless the request results in a 404 error, the name and source - # will get overwritten in _set_transaction - name=request.path, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": SanicIntegration.origin, - "sentry.span.source": SegmentSource.URL.value, - }, - parent_span=None, - ) - request.ctx._sentry_root_span = span - else: - transaction = continue_trace( - dict(request.headers), - op=OP.HTTP_SERVER, - # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction - name=request.path, - source=TransactionSource.URL, - origin=SanicIntegration.origin, - ) - request.ctx._sentry_root_span = sentry_sdk.start_transaction( - transaction - ).__enter__() - - -async def _context_exit( - request: "Request", response: "Optional[BaseHTTPResponse]" = None -) -> None: - with capture_internal_exceptions(): - if not request.ctx._sentry_do_integration: - return - - integration = sentry_sdk.get_client().get_integration(SanicIntegration) - - response_status = None if response is None else response.status - - # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception - # happens while trying to end the transaction, we still attempt to exit the hub. - with capture_internal_exceptions(): - span = request.ctx._sentry_root_span - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes(request).items(): - span.set_attribute(attr, value) - if response_status is not None: - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) - span.status = "error" if response_status >= 400 else "ok" - - span.end() - - else: - span.set_http_status(response_status) - span.sampled &= ( - isinstance(integration, SanicIntegration) - and response_status not in integration._unsampled_statuses - ) - - span.__exit__(None, None, None) - - request.ctx._sentry_scope.__exit__(None, None, None) - - -async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: - if request.ctx._sentry_do_integration: - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - route_name = route.name.replace(request.app.name, "").strip(".") - scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) - - -def _sentry_error_handler_lookup( - self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" -) -> "Optional[object]": - _capture_exception(exception) - old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) - - if old_error_handler is None: - return None - - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return old_error_handler - - async def sentry_wrapped_error_handler( - request: "Request", exception: Exception - ) -> "Any": - try: - response = old_error_handler(request, exception) - if isawaitable(response): - response = await response - return response - except Exception: - # Report errors that occur in Sanic error handler. These - # exceptions will not even show up in Sanic's - # `sanic.exceptions` logger. - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - finally: - # As mentioned in previous comment in _startup, this can be removed - # after https://github.com/sanic-org/sanic/issues/2297 is resolved - if SanicIntegration.version and SanicIntegration.version == (21, 9): - await _context_exit(request) - - return sentry_wrapped_error_handler - - -async def _legacy_handle_request( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" -) -> "Any": - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return await old_handle_request(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - response = old_handle_request(self, request, *args, **kwargs) - if isawaitable(response): - response = await response - - return response - - -def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": - rv = old_router_get(self, *args) - if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: - with capture_internal_exceptions(): - scope = sentry_sdk.get_isolation_scope() - if SanicIntegration.version and SanicIntegration.version >= (21, 3): - # Sanic versions above and including 21.3 append the app name to the - # route name, and so we need to remove it from Route name so the - # transaction name is consistent across all versions - sanic_app_name = self.ctx.app.name - sanic_route = rv[0].name - - if sanic_route.startswith("%s." % sanic_app_name): - # We add a 1 to the len of the sanic_app_name because there is a dot - # that joins app name and the route name - # Format: app_name.route_name - sanic_route = sanic_route[len(sanic_app_name) + 1 :] - - scope.set_transaction_name( - sanic_route, source=TransactionSource.COMPONENT - ) - else: - scope.set_transaction_name( - rv[0].__name__, source=TransactionSource.COMPONENT - ) - - return rv - - -@ensure_integration_enabled(SanicIntegration) -def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "sanic", "handled": False}, - ) - - if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: - return - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_request_attributes(request: "Request") -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from a Sanic request. - """ - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - urlparts = urlsplit(request.url) - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.url - attributes["url.path"] = urlparts.path - - if urlparts.query: - attributes[SPANDATA.HTTP_QUERY] = urlparts.query - - if urlparts.scheme: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme - - if should_send_default_pii() and request.remote_addr: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr - - return attributes - - -def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": - def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - try: - if hint and issubclass(hint["exc_info"][0], SanicException): - return None - except KeyError: - pass - - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - extractor = SanicRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - urlparts = urlsplit(request.url) - - request_info["url"] = "%s://%s%s" % ( - urlparts.scheme, - urlparts.netloc, - urlparts.path, - ) - - request_info["query_string"] = urlparts.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_addr} - request_info["headers"] = _filter_headers(dict(request.headers)) - - return event - - return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py deleted file mode 100644 index 16f91b28ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/serverless.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import event_from_exception, reraise - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union, overload - - F = TypeVar("F", bound=Callable[..., Any]) - -else: - - def overload(x: "F") -> "F": - return x - - -@overload -def serverless_function(f: "F", flush: bool = True) -> "F": - pass - - -@overload -def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 - pass - - -def serverless_function( # noqa - f: "Optional[F]" = None, flush: bool = True -) -> "Union[F, Callable[[F], F]]": - def wrapper(f: "F") -> "F": - @wraps(f) - def inner(*args: "Any", **kwargs: "Any") -> "Any": - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - - try: - return f(*args, **kwargs) - except Exception: - _capture_and_reraise() - finally: - if flush: - sentry_sdk.flush() - - return inner # type: ignore - - if f is None: - return wrapper - else: - return wrapper(f) - - -def _capture_and_reraise() -> None: - exc_info = sys.exc_info() - client = sentry_sdk.get_client() - if client.is_active(): - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "serverless", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py deleted file mode 100644 index 775170fb9f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/socket.py +++ /dev/null @@ -1,142 +0,0 @@ -import socket - -import sentry_sdk -from sentry_sdk._types import MYPY -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if MYPY: - from socket import AddressFamily, SocketKind - from typing import List, Optional, Tuple, Union - -__all__ = ["SocketIntegration"] - - -class SocketIntegration(Integration): - identifier = "socket" - origin = f"auto.socket.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) - """ - _patch_create_connection() - _patch_getaddrinfo() - - -def _get_span_description( - host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" -) -> str: - try: - host = host.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - try: - port = port.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - description = "%s:%s" % (host, port) # type: ignore - return description - - -def _patch_create_connection() -> None: - real_create_connection = socket.create_connection - - def create_connection( - address: "Tuple[Optional[str], int]", - timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore - source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, - ) -> "socket.socket": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_create_connection(address, timeout, source_address) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(address[0], address[1]), - attributes={ - "sentry.op": OP.SOCKET_CONNECTION, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if address[0] is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) - span.set_attribute(SPANDATA.SERVER_PORT, address[1]) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_CONNECTION, - name=_get_span_description(address[0], address[1]), - origin=SocketIntegration.origin, - ) as span: - span.set_data("address", address) - span.set_data("timeout", timeout) - span.set_data("source_address", source_address) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - - socket.create_connection = create_connection # type: ignore - - -def _patch_getaddrinfo() -> None: - real_getaddrinfo = socket.getaddrinfo - - def getaddrinfo( - host: "Union[bytes, str, None]", - port: "Union[bytes, str, int, None]", - family: int = 0, - type: int = 0, - proto: int = 0, - flags: int = 0, - ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_getaddrinfo(host, port, family, type, proto, flags) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(host, port), - attributes={ - "sentry.op": OP.SOCKET_DNS, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if isinstance(host, str): - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - elif isinstance(host, bytes): - span.set_attribute( - SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") - ) - - if isinstance(port, int): - span.set_attribute(SPANDATA.SERVER_PORT, port) - elif port is not None: - try: - span.set_attribute(SPANDATA.SERVER_PORT, int(port)) - except (ValueError, TypeError): - pass - - return real_getaddrinfo(host, port, family, type, proto, flags) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_DNS, - name=_get_span_description(host, port), - origin=SocketIntegration.origin, - ) as span: - span.set_data("host", host) - span.set_data("port", port) - - return real_getaddrinfo(host, port, family, type, proto, flags) - - socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py deleted file mode 100644 index 10d94163c5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from sentry_sdk.integrations.spark.spark_driver import SparkIntegration -from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration - -__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py deleted file mode 100644 index a83532b6a6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_driver.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Optional - - from pyspark import SparkContext - - from sentry_sdk._types import Event, Hint - - -class SparkIntegration(Integration): - identifier = "spark" - - @staticmethod - def setup_once() -> None: - _setup_sentry_tracing() - - -def _set_app_properties() -> None: - """ - Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. - This allows worker integration to have access to app_name and application_id. - """ - from pyspark import SparkContext - - spark_context = SparkContext._active_spark_context - if spark_context: - spark_context.setLocalProperty( - "sentry_app_name", - spark_context.appName, - ) - spark_context.setLocalProperty( - "sentry_application_id", - spark_context.applicationId, - ) - - -def _start_sentry_listener(sc: "SparkContext") -> None: - """ - Start java gateway server to add custom `SparkListener` - """ - from pyspark.java_gateway import ensure_callback_server_started - - gw = sc._gateway - ensure_callback_server_started(gw) - listener = SentryListener() - sc._jsc.sc().addSparkListener(listener) - - -def _add_event_processor(sc: "SparkContext") -> None: - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - if sentry_sdk.get_client().get_integration(SparkIntegration) is None: - return event - - if sc._active_spark_context is None: - return event - - event.setdefault("user", {}).setdefault("id", sc.sparkUser()) - - event.setdefault("tags", {}).setdefault( - "executor.id", sc._conf.get("spark.executor.id") - ) - event["tags"].setdefault( - "spark-submit.deployMode", - sc._conf.get("spark.submit.deployMode"), - ) - event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) - event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) - event["tags"].setdefault("spark_version", sc.version) - event["tags"].setdefault("app_name", sc.appName) - event["tags"].setdefault("application_id", sc.applicationId) - event["tags"].setdefault("master", sc.master) - event["tags"].setdefault("spark_home", sc.sparkHome) - - event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) - - return event - - -def _activate_integration(sc: "SparkContext") -> None: - _start_sentry_listener(sc) - _set_app_properties() - _add_event_processor(sc) - - -def _patch_spark_context_init() -> None: - from pyspark import SparkContext - - spark_context_init = SparkContext._do_init - - @ensure_integration_enabled(SparkIntegration, spark_context_init) - def _sentry_patched_spark_context_init( - self: "SparkContext", *args: "Any", **kwargs: "Any" - ) -> "Optional[Any]": - rv = spark_context_init(self, *args, **kwargs) - _activate_integration(self) - return rv - - SparkContext._do_init = _sentry_patched_spark_context_init - - -def _setup_sentry_tracing() -> None: - from pyspark import SparkContext - - if SparkContext._active_spark_context is not None: - _activate_integration(SparkContext._active_spark_context) - return - _patch_spark_context_init() - - -class SparkListener: - def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 - pass - - def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklistedForStage( # noqa: N802 - self, - executorBlacklistedForStage: "Any", # noqa: N803 - ) -> None: - pass - - def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 - pass - - def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 - pass - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - pass - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 - pass - - def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 - pass - - class Java: - implements = ["org.apache.spark.scheduler.SparkListenerInterface"] - - -class SentryListener(SparkListener): - def _add_breadcrumb( - self, - level: str, - message: str, - data: "Optional[dict[str, Any]]" = None, - ) -> None: - sentry_sdk.get_isolation_scope().add_breadcrumb( - level=level, message=message, data=data - ) - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - message = "Job {} Started".format(jobStart.jobId()) - self._add_breadcrumb(level="info", message=message) - _set_app_properties() - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - level = "" - message = "" - data = {"result": jobEnd.jobResult().toString()} - - if jobEnd.jobResult().toString() == "JobSucceeded": - level = "info" - message = "Job {} Ended".format(jobEnd.jobId()) - else: - level = "warning" - message = "Job {} Failed".format(jobEnd.jobId()) - - self._add_breadcrumb(level=level, message=message, data=data) - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - stage_info = stageSubmitted.stageInfo() - message = "Stage {} Submitted".format(stage_info.stageId()) - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - self._add_breadcrumb(level="info", message=message, data=data) - _set_app_properties() - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - from py4j.protocol import Py4JJavaError # type: ignore - - stage_info = stageCompleted.stageInfo() - message = "" - level = "" - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - # Have to Try Except because stageInfo.failureReason() is typed with Scala Option - try: - data["reason"] = stage_info.failureReason().get() - message = "Stage {} Failed".format(stage_info.stageId()) - level = "warning" - except Py4JJavaError: - message = "Stage {} Completed".format(stage_info.stageId()) - level = "info" - - self._add_breadcrumb(level=level, message=message, data=data) - - -def _get_attempt_id(stage_info: "Any") -> "Optional[int]": - try: - return stage_info.attemptId() - except Exception: - pass - - try: - return stage_info.attemptNumber() - except Exception: - pass - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py deleted file mode 100644 index 5906472748..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/spark/spark_worker.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_hint_with_exc_info, - exc_info_from_error, - single_exception_from_error_tuple, - walk_exception_chain, -) - -if TYPE_CHECKING: - from typing import Any, Optional - - from sentry_sdk._types import Event, ExcInfo, Hint - - -class SparkWorkerIntegration(Integration): - identifier = "spark_worker" - - @staticmethod - def setup_once() -> None: - import pyspark.daemon as original_daemon - - original_daemon.worker_main = _sentry_worker_main - - -def _capture_exception(exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - - mechanism = {"type": "spark", "handled": False} - - exc_info = exc_info_from_error(exc_info) - - exc_type, exc_value, tb = exc_info - rv = [] - - # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - if exc_type not in (SystemExit, EOFError, ConnectionResetError): - rv.append( - single_exception_from_error_tuple( - exc_type, exc_value, tb, client.options, mechanism - ) - ) - - if rv: - rv.reverse() - hint = event_hint_with_exc_info(exc_info) - event: "Event" = {"level": "error", "exception": {"values": rv}} - - _tag_task_context() - - sentry_sdk.capture_event(event, hint=hint) - - -def _tag_task_context() -> None: - from pyspark.taskcontext import TaskContext - - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - integration = sentry_sdk.get_client().get_integration( - SparkWorkerIntegration - ) - task_context = TaskContext.get() - - if integration is None or task_context is None: - return event - - event.setdefault("tags", {}).setdefault( - "stageId", str(task_context.stageId()) - ) - event["tags"].setdefault("partitionId", str(task_context.partitionId())) - event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) - event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) - - if task_context._localProperties: - if "sentry_app_name" in task_context._localProperties: - event["tags"].setdefault( - "app_name", task_context._localProperties["sentry_app_name"] - ) - event["tags"].setdefault( - "application_id", - task_context._localProperties["sentry_application_id"], - ) - - if "callSite.short" in task_context._localProperties: - event.setdefault("extra", {}).setdefault( - "callSite", task_context._localProperties["callSite.short"] - ) - - return event - - -def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: - import pyspark.worker as original_worker - - try: - original_worker.main(*args, **kwargs) - except SystemExit: - if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py deleted file mode 100644 index 962fe4ee53..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sqlalchemy.py +++ /dev/null @@ -1,185 +0,0 @@ -from sentry_sdk.consts import SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - parse_version, -) - -try: - from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore - from sqlalchemy.engine import Engine # type: ignore - from sqlalchemy.event import listen # type: ignore -except ImportError: - raise DidNotEnable("SQLAlchemy not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional, Union - - -class SqlalchemyIntegration(Integration): - identifier = "sqlalchemy" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(SQLALCHEMY_VERSION) - _check_minimum_version(SqlalchemyIntegration, version) - - listen(Engine, "before_cursor_execute", _before_cursor_execute) - listen(Engine, "after_cursor_execute", _after_cursor_execute) - listen(Engine, "handle_error", _handle_error) - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _before_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - executemany: bool, - *args: "Any", -) -> None: - ctx_mgr = record_sql_queries( - cursor, - statement, - parameters, - paramstyle=context and context.dialect and context.dialect.paramstyle or None, - executemany=executemany, - span_origin=SqlalchemyIntegration.origin, - ) - context._sentry_sql_span_manager = ctx_mgr - - span = ctx_mgr.__enter__() - - if span is not None: - _set_db_data(span, conn) - context._sentry_sql_span = span - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _after_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - *args: "Any", -) -> None: - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - context, "_sentry_sql_span_manager", None - ) - - # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. - span: "Optional[Union[Span, StreamedSpan]]" = getattr( - context, "_sentry_sql_span", None - ) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if ctx_mgr is not None: - context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - if isinstance(span, Span): - with capture_internal_exceptions(): - add_query_source(span) - - -def _handle_error(context: "Any", *args: "Any") -> None: - execution_context = context.execution_context - if execution_context is None: - return - - span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) - - if span is not None: - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # _after_cursor_execute does not get called for crashing SQL stmts. Judging - # from SQLAlchemy codebase it does seem like any error coming into this - # handler is going to be fatal. - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - execution_context, "_sentry_sql_span_manager", None - ) - - if ctx_mgr is not None: - execution_context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - -# See: https://docs.sqlalchemy.org/en/20/dialects/index.html -def _get_db_system(name: str) -> "Optional[str]": - name = str(name) - - if "sqlite" in name: - return "sqlite" - - if "postgres" in name: - return "postgresql" - - if "mariadb" in name: - return "mariadb" - - if "mysql" in name: - return "mysql" - - if "oracle" in name: - return "oracle" - - return None - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - db_system = _get_db_system(conn.engine.name) - - if isinstance(span, StreamedSpan): - if db_system is not None: - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) - else: - if db_system is not None: - span.set_data(SPANDATA.DB_SYSTEM, db_system) - - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - try: - driver = conn.dialect.driver - if driver: - set_on_span(SPANDATA.DB_DRIVER_NAME, driver) - except Exception: - pass - - if conn.engine.url is None: - return - - db_name = conn.engine.url.database - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - server_address = conn.engine.url.host - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = conn.engine.url.port - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py deleted file mode 100644 index 3f9fbf4d8e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlette.py +++ /dev/null @@ -1,858 +0,0 @@ -import functools -import json -import sys -import warnings -from collections.abc import Set -from copy import deepcopy -from json import JSONDecodeError -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations._asgi_common import _RootPathInPath -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - HttpCodeRangeContainer, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Awaitable, - Callable, - Container, - Dict, - Optional, - Tuple, - Union, - ) - - from sentry_sdk._types import Event, HttpStatusCodeRange -try: - import starlette - from starlette import __version__ as STARLETTE_VERSION - from starlette.applications import Starlette - from starlette.datastructures import ( - UploadFile, - ) - from starlette.middleware import Middleware - from starlette.middleware.authentication import ( - AuthenticationMiddleware, - ) - from starlette.requests import Request - from starlette.routing import Match - from starlette.types import ASGIApp, Receive, Send - from starlette.types import Scope as StarletteScope -except ImportError: - raise DidNotEnable("Starlette is not installed") - -try: - # Starlette 0.20 - from starlette.middleware.exceptions import ExceptionMiddleware -except ImportError: - # Startlette 0.19.1 - from starlette.exceptions import ExceptionMiddleware # type: ignore - -try: - # Optional dependency of Starlette to parse form data. - try: - # python-multipart 0.0.13 and later - import python_multipart as multipart - except ImportError: - # python-multipart 0.0.12 and earlier - import multipart # type: ignore -except ImportError: - multipart = None # type: ignore[assignment] - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -_DEFAULT_TRANSACTION_NAME = "generic Starlette request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class StarletteIntegration(Integration): - identifier = "starlette" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "url", - failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - middleware_spans: bool = False, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ): - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - if isinstance(failed_request_status_codes, Set): - self.failed_request_status_codes: "Container[int]" = ( - failed_request_status_codes - ) - else: - warnings.warn( - "Passing a list or None for failed_request_status_codes is deprecated. " - "Please pass a set of int instead.", - DeprecationWarning, - stacklevel=2, - ) - - if failed_request_status_codes is None: - self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES - else: - self.failed_request_status_codes = HttpCodeRangeContainer( - failed_request_status_codes - ) - - @staticmethod - def setup_once() -> None: - version = parse_version(STARLETTE_VERSION) - - if version is None: - raise DidNotEnable( - "Unparsable Starlette version: {}".format(STARLETTE_VERSION) - ) - - patch_middlewares() - # Starlette tolerates both starting with: - # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. - root_path_in_path = ( - _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED - ) - patch_asgi_app(root_path_in_path=root_path_in_path) - patch_request_response() - - if version >= (0, 24): - patch_templates() - - -def _enable_span_for_middleware( - middleware_class: "Any", -) -> "Any": - old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ - - async def _create_span_call( - app: "Any", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - **kwargs: "Any", - ) -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await old_call(app, scope, receive, send, **kwargs) - - # Update transaction name with middleware name - name, source = _get_transaction_from_middleware(app, scope, integration) - - if name is not None: - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=source, - ) - - if not integration.middleware_spans: - return await old_call(app, scope, receive, send, **kwargs) - - middleware_name = app.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarletteIntegration.origin, - "middleware.name": middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarletteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlette.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await send(*args, **kwargs) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(app, scope, new_receive, new_send, **kwargs) - - not_yet_patched = old_call.__name__ not in [ - "_create_span_call", - "_sentry_authenticationmiddleware_call", - "_sentry_exceptionmiddleware_call", - ] - - if not_yet_patched: - middleware_class.__call__ = _create_span_call - - return middleware_class - - -def _serialize_request_body_data(data: "Any") -> str: - # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values - def _default(value: "Any") -> "Any": - if isinstance(value, AnnotatedValue): - return value.value - return str(value) - - return json.dumps(data, default=_default) - - -@ensure_integration_enabled(StarletteIntegration) -def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarletteIntegration.identifier, "handled": handled}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def patch_exception_middleware(middleware_class: "Any") -> None: - """ - Capture all exceptions in Starlette app and - also extract user information. - """ - old_middleware_init = middleware_class.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - - if not_yet_patched: - - def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: - old_middleware_init(self, *args, **kwargs) - - # Patch existing exception handlers - old_handlers = self._exception_handlers.copy() - - async def _sentry_patched_exception_handler( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration( - StarletteIntegration - ) - - exp = args[0] - - if integration is not None: - is_http_server_error = ( - hasattr(exp, "status_code") - and isinstance(exp.status_code, int) - and exp.status_code in integration.failed_request_status_codes - ) - if is_http_server_error: - _capture_exception(exp, handled=True) - - # Find a matching handler - old_handler = None - for cls in type(exp).__mro__: - if cls in old_handlers: - old_handler = old_handlers[cls] - break - - if old_handler is None: - return - - if _is_async_callable(old_handler): - return await old_handler(self, *args, **kwargs) - else: - return old_handler(self, *args, **kwargs) - - for key in self._exception_handlers.keys(): - self._exception_handlers[key] = _sentry_patched_exception_handler - - middleware_class.__init__ = _sentry_middleware_init - - old_call = middleware_class.__call__ - - async def _sentry_exceptionmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - # Also add the user (that was eventually set by be Authentication middle - # that was called before this middleware). This is done because the authentication - # middleware sets the user in the scope and then (in the same function) - # calls this exception middelware. In case there is no exception (or no handler - # for the type of exception occuring) then the exception bubbles up and setting the - # user information into the sentry scope is done in auth middleware and the - # ASGI middleware will then send everything to Sentry and this is fine. - # But if there is an exception happening that the exception middleware here - # has a handler for, it will send the exception directly to Sentry, so we need - # the user information right now. - # This is why we do it here. - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_exceptionmiddleware_call - - -@ensure_integration_enabled(StarletteIntegration) -def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: - """ - Extracts user information from the ASGI scope and - adds it to Sentry's scope. - """ - if "user" not in scope: - return - - if not should_send_default_pii(): - return - - user_info: "Dict[str, Any]" = {} - starlette_user = scope["user"] - - username = getattr(starlette_user, "username", None) - if username: - user_info.setdefault("username", starlette_user.username) - - user_id = getattr(starlette_user, "id", None) - if user_id: - user_info.setdefault("id", starlette_user.id) - - email = getattr(starlette_user, "email", None) - if email: - user_info.setdefault("email", starlette_user.email) - - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - -def patch_authentication_middleware(middleware_class: "Any") -> None: - """ - Add user information to Sentry scope. - """ - old_call = middleware_class.__call__ - - not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) - - if not_yet_patched: - - async def _sentry_authenticationmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_authenticationmiddleware_call - - -def patch_middlewares() -> None: - """ - Patches Starlettes `Middleware` class to record - spans for every middleware invoked. - """ - old_middleware_init = Middleware.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - if not_yet_patched: - - def _sentry_middleware_init( - self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - if cls == SentryAsgiMiddleware: - return old_middleware_init(self, cls, *args, **kwargs) - - span_enabled_cls = _enable_span_for_middleware(cls) - old_middleware_init(self, span_enabled_cls, *args, **kwargs) - - if cls == AuthenticationMiddleware: - patch_authentication_middleware(cls) - - if cls == ExceptionMiddleware: - patch_exception_middleware(cls) - - Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] - - -def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: - """ - Instrument Starlette ASGI app using the SentryAsgiMiddleware. - """ - old_app = Starlette.__call__ - - async def _sentry_patched_asgi_app( - self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StarletteIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - mechanism_type=StarletteIntegration.identifier, - transaction_style=integration.transaction_style, - span_origin=StarletteIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - asgi_version=3, - root_path_in_path=root_path_in_path, - ) - - return await middleware(scope, receive, send) - - Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] - - -# This was vendored in from Starlette to support Starlette 0.19.1 because -# this function was only introduced in 0.20.x -def _is_async_callable(obj: "Any") -> bool: - while isinstance(obj, functools.partial): - obj = obj.func - - return iscoroutinefunction(obj) or ( - callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] - ) - - -def _get_cached_request_body_attribute( - client: "sentry_sdk.client.BaseClient", request: "Request" -) -> "Optional[str]": - """ - Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. - """ - if "content-length" not in request.headers: - return None - - try: - content_length = int(request.headers["content-length"]) - except ValueError: - return None - - if content_length and not request_body_within_bounds(client, content_length): - return OVER_SIZE_LIMIT_SUBSTITUTE - - if hasattr(request, "_json"): - return json.dumps(request._json) - - formdata_body = getattr(request, "_form", None) - if formdata_body is None: - return None - - form_data = {} - for key, val in formdata_body.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = val if not is_file else "[Unparsable]" - - return json.dumps(form_data) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), - integration.transaction_style, - request, - ) - - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Add info from request to event - request_info = event.get("request", {}) - if info: - if "cookies" in info: - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_request_response() -> None: - old_request_response = starlette.routing.request_response - - def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": - old_func = func - - is_coroutine = _is_async_callable(old_func) - if is_coroutine: - - async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_func, *args, **kwargs) - - func = _sentry_async_func - - else: - - @functools.wraps(old_func) - def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(StarletteIntegration) - if integration is None: - return old_func(*args, **kwargs) - - current_scope = sentry_sdk.get_current_scope() - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming: - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - current_span._segment._update_active_thread() - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - request = args[0] - - _set_transaction_name_and_source( - sentry_scope, integration.transaction_style, request - ) - - extractor = StarletteRequestExtractor(request) - cookies = extractor.extract_cookies_from_request() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor( - event: "Event", hint: "dict[str, Any]" - ) -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if cookies: - request_info["cookies"] = cookies - - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - return old_func(*args, **kwargs) - - func = _sentry_sync_func - - return old_request_response(func) - - starlette.routing.request_response = _sentry_request_response - - -def patch_templates() -> None: - # If markupsafe is not installed, then Jinja2 is not installed - # (markupsafe is a dependency of Jinja2) - # In this case we do not need to patch the Jinja2Templates class - try: - from markupsafe import Markup - except ImportError: - return # Nothing to do - - # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a - try: - from starlette.templating import Jinja2Templates - except ImportError: - return - - old_jinja2templates_init = Jinja2Templates.__init__ - - not_yet_patched = "_sentry_jinja2templates_init" not in str( - old_jinja2templates_init - ) - - if not_yet_patched: - - def _sentry_jinja2templates_init( - self: "Jinja2Templates", *args: "Any", **kwargs: "Any" - ) -> None: - def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": - trace_meta = Markup( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - return { - "sentry_trace_meta": trace_meta, - } - - kwargs.setdefault("context_processors", []) - - if add_sentry_trace_meta not in kwargs["context_processors"]: - kwargs["context_processors"].append(add_sentry_trace_meta) - - return old_jinja2templates_init(self, *args, **kwargs) - - Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] - - -class StarletteRequestExtractor: - """ - Extracts useful information from the Starlette request - (like form data or cookies) and adds it to the Sentry event. - """ - - def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: - self.request = request - - def extract_cookies_from_request( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - cookies: "Optional[Dict[str, Any]]" = None - if should_send_default_pii(): - cookies = self.cookies() - - return cookies - - async def extract_request_info( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - client = sentry_sdk.get_client() - - request_info: "Dict[str, Any]" = {} - - with capture_internal_exceptions(): - # Add cookies - if should_send_default_pii(): - request_info["cookies"] = self.cookies() - - # If there is no body, just return the cookies - content_length = await self.content_length() - if not content_length: - return request_info - - # Add annotation if body is too big - if content_length and not request_body_within_bounds( - client, content_length - ): - request_info["data"] = AnnotatedValue.removed_because_over_size_limit() - return request_info - - # Add JSON body, if it is a JSON request - json = await self.json() - if json: - request_info["data"] = json - return request_info - - # Add form as key/value pairs, if request has form data - form = await self.form() - if form: - form_data = {} - for key, val in form.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = ( - val - if not is_file - else AnnotatedValue.removed_because_raw_data() - ) - - request_info["data"] = form_data - return request_info - - # Raw data, do not add body just an annotation - request_info["data"] = AnnotatedValue.removed_because_raw_data() - return request_info - - async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": - if "content-length" in self.request.headers: - return int(self.request.headers["content-length"]) - - return None - - def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": - return self.request.cookies - - async def form(self: "StarletteRequestExtractor") -> "Any": - if multipart is None: - return None - - # Parse the body first to get it cached, as Starlette does not cache form() as it - # does with body() and json() https://github.com/encode/starlette/discussions/1933 - # Calling `.form()` without calling `.body()` first will - # potentially break the users project. - await self.request.body() - - return await self.request.form() - - def is_json(self: "StarletteRequestExtractor") -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": - if not self.is_json(): - return None - try: - return await self.request.json() - except JSONDecodeError: - return None - - -def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": - router = scope.get("router") - if not router: - return None - - for route in router.routes: - match = route.matches(scope) - if match[0] == Match.FULL: - try: - return route.path - except AttributeError: - # routes added via app.host() won't have a path attribute - return scope.get("path") - - return None - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = None - source = SOURCE_FOR_STYLE[transaction_style] - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or None - - elif transaction_style == "url": - name = _transaction_name_from_router(request.scope) - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - scope.set_transaction_name(name, source=source) - - -def _get_transaction_from_middleware( - app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" -) -> "Tuple[Optional[str], Optional[str]]": - name = None - source = None - - if integration.transaction_style == "endpoint": - name = transaction_from_function(app.__class__) - source = TransactionSource.COMPONENT - elif integration.transaction_style == "url": - name = _transaction_name_from_router(asgi_scope) - source = TransactionSource.ROUTE - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py deleted file mode 100644 index 1eebd37e84..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/starlite.py +++ /dev/null @@ -1,314 +0,0 @@ -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from pydantic import BaseModel - from starlite import Request, Starlite, State # type: ignore - from starlite.handlers.base import BaseRouteHandler # type: ignore - from starlite.middleware import DefineMiddleware # type: ignore - from starlite.plugins.base import get_plugin_for_value # type: ignore - from starlite.routes.http import HTTPRoute # type: ignore - from starlite.utils import ( # type: ignore - ConnectionDataExtractor, - Ref, - is_async_callable, - ) -except ImportError: - raise DidNotEnable("Starlite is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from starlite import MiddlewareProtocol - from starlite.types import ( # type: ignore - ASGIApp, - Hint, - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from starlite.types import ( - Scope as StarliteScope, - ) - - from sentry_sdk._types import Event - - -_DEFAULT_TRANSACTION_NAME = "generic Starlite request" - - -class StarliteIntegration(Integration): - identifier = "starlite" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - -class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - -def patch_app_init() -> None: - """ - Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. - See: - - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception - - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ - """ - old__init__ = Starlite.__init__ - - @ensure_integration_enabled(StarliteIntegration, old__init__) - def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: - after_exception = kwargs.pop("after_exception", []) - kwargs.update( - after_exception=[ - exception_handler, - *( - after_exception - if isinstance(after_exception, list) - else [after_exception] - ), - ] - ) - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Starlite.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryStarliteASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "StarliteScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(StarliteIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarliteIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarliteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlite.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await receive(*args, **kwargs) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await send(message) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - elif isinstance(route_handler.fn, Ref): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = StarliteIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if not scope_user: - return None - if isinstance(scope_user, dict): - return scope_user - if isinstance(scope_user, BaseModel): - return scope_user.dict() - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - plugin = get_plugin_for_value(scope_user) - if plugin and not is_async_callable(plugin.to_dict): - return plugin.to_dict(scope_user) - - return None - - -@ensure_integration_enabled(StarliteIntegration) -def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarliteIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py deleted file mode 100644 index 746e09b36f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/statsig.py +++ /dev/null @@ -1,37 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.utils import parse_version - -try: - from statsig import statsig as statsig_module - from statsig.version import __version__ as STATSIG_VERSION -except ImportError: - raise DidNotEnable("statsig is not installed") - -if TYPE_CHECKING: - from statsig.statsig_user import StatsigUser - - -class StatsigIntegration(Integration): - identifier = "statsig" - - @staticmethod - def setup_once() -> None: - version = parse_version(STATSIG_VERSION) - _check_minimum_version(StatsigIntegration, version, "statsig") - - # Wrap and patch evaluation method(s) in the statsig module - old_check_gate = statsig_module.check_gate - - @wraps(old_check_gate) - def sentry_check_gate( - user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_check_gate(user, gate, *args, **kwargs) - add_feature_flag(gate, enabled) - return enabled - - statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py deleted file mode 100644 index 82f30f2dda..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/stdlib.py +++ /dev/null @@ -1,404 +0,0 @@ -import os -import platform -import subprocess -import sys -from http.client import HTTPConnection, HTTPResponse -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - EnvironHeaders, - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - is_sentry_url, - logger, - parse_url, - safe_repr, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from sentry_sdk._types import Event, Hint - - -_RUNTIME_CONTEXT: "dict[str, object]" = { - "name": platform.python_implementation(), - "version": "%s.%s.%s" % (sys.version_info[:3]), - "build": sys.version, -} - - -class StdlibIntegration(Integration): - identifier = "stdlib" - - @staticmethod - def setup_once() -> None: - _install_httplib() - _install_subprocess() - - @add_global_event_processor - def add_python_runtime_context( - event: "Event", hint: "Hint" - ) -> "Optional[Event]": - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is not None: - contexts = event.setdefault("contexts", {}) - if isinstance(contexts, dict) and "runtime" not in contexts: - contexts["runtime"] = _RUNTIME_CONTEXT - - return event - - -def _complete_span(span: "Union[Span, StreamedSpan]") -> None: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - -def _install_httplib() -> None: - real_putrequest = HTTPConnection.putrequest - real_getresponse = HTTPConnection.getresponse - real_read = HTTPResponse.read - real_close = HTTPResponse.close - - def putrequest( - self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - default_port = self.default_port - - # proxies go through set_tunnel - tunnel_host = getattr(self, "_tunnel_host", None) - if tunnel_host: - host = tunnel_host - port = getattr(self, "_tunnel_port", default_port) - else: - host = self.host - port = self.port - - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is None or is_sentry_url( - client, host - ): - return real_putrequest(self, method, url, *args, **kwargs) - - real_url = url - if real_url is None or not real_url.startswith(("http://", "https://")): - real_url = "%s://%s%s%s" % ( - default_port == 443 and "https" or "http", - host, - port != default_port and ":%s" % port or "", - url, - ) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(real_url, sanitize=False) - - span_streaming = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - attributes={ - "sentry.origin": "auto.http.stdlib.httplib", - "sentry.op": OP.HTTP_CLIENT, - SPANDATA.HTTP_REQUEST_METHOD: method, - }, - ) - - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - - set_on_span = span.set_attribute - - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - origin="auto.http.stdlib.httplib", - ) - - span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - - set_on_span = span.set_data - - # for proxies, these point to the proxy host/port - if tunnel_host: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) - set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) - - rv = real_putrequest(self, method, url, *args, **kwargs) - - if should_propagate_trace(client, real_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=key, value=value, real_url=real_url - ) - ) - self.putheader(key, value) - - self._sentrysdk_span = span # type: ignore[attr-defined] - - return rv - - def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": - span = getattr(self, "_sentrysdk_span", None) - - if span is None: - return real_getresponse(self, *args, **kwargs) - - try: - rv = real_getresponse(self, *args, **kwargs) - except BaseException: - _complete_span(span) - raise - - if isinstance(span, StreamedSpan): - status_code = int(rv.status) - span.status = "error" if status_code >= 400 else "ok" - span.set_attribute("http.response.status_code", status_code) - else: - span.set_http_status(int(rv.status)) - span.set_data("reason", rv.reason) - - # getresponse doesn't include actually reading the response body. This - # is done in read(). So if the metadata/headers suggest there's a body to - # read, don't finish the span just yet, but save it for ending it later. - has_body = rv.chunked or (rv.length is not None and rv.length > 0) - if has_body: - rv._sentrysdk_span = span # type: ignore[attr-defined] - else: - _complete_span(span) - - return rv - - def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": - try: - return real_read(self, *args, **kwargs) - finally: - span = getattr(self, "_sentrysdk_span", None) - # read() might be called multiple times to consume a single body, - # so we can't just end the span when read() is done. Instead, - # try to figure out whether the response body has been fully read. - if span and (self.fp is None or self.closed): - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - def close(self: "HTTPResponse") -> None: - # We patch close() as a best effort fallback in case the span is not - # ended yet in getresponse() or read(). - - try: - real_close(self) - finally: - span = getattr(self, "_sentrysdk_span", None) - if span is not None: - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - HTTPConnection.putrequest = putrequest # type: ignore[method-assign] - HTTPConnection.getresponse = getresponse # type: ignore[method-assign] - HTTPResponse.read = read # type: ignore[method-assign] - HTTPResponse.close = close # type: ignore[assignment,method-assign] - - -def _init_argument( - args: "List[Any]", - kwargs: "Dict[Any, Any]", - name: str, - position: int, - setdefault_callback: "Optional[Callable[[Any], Any]]" = None, -) -> "Any": - """ - given (*args, **kwargs) of a function call, retrieve (and optionally set a - default for) an argument by either name or position. - - This is useful for wrapping functions with complex type signatures and - extracting a few arguments without needing to redefine that function's - entire type signature. - """ - - if name in kwargs: - rv = kwargs[name] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - kwargs[name] = rv - elif position < len(args): - rv = args[position] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - args[position] = rv - else: - rv = setdefault_callback and setdefault_callback(None) - if rv is not None: - kwargs[name] = rv - - return rv - - -def _install_subprocess() -> None: - old_popen_init = subprocess.Popen.__init__ - - @ensure_integration_enabled(StdlibIntegration, old_popen_init) - def sentry_patched_popen_init( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> None: - # Convert from tuple to list to be able to set values. - a = list(a) - - args = _init_argument(a, kw, "args", 0) or [] - cwd = _init_argument(a, kw, "cwd", 9) - - # if args is not a list or tuple (and e.g. some iterator instead), - # let's not use it at all. There are too many things that can go wrong - # when trying to collect an iterator into a list and setting that list - # into `a` again. - # - # Also invocations where `args` is not a sequence are not actually - # legal. They just happen to work under CPython. - description = None - - if isinstance(args, (list, tuple)) and len(args) < 100: - with capture_internal_exceptions(): - description = " ".join(map(str, args)) - - if description is None: - description = safe_repr(args) - - env = None - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.SUBPROCESS, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.SUBPROCESS, - name=description, - origin="auto.subprocess.stdlib.subprocess", - ) - - with span: - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - if env is None: - env = _init_argument( - a, - kw, - "env", - 10, - lambda x: dict(x if x is not None else os.environ), - ) - env["SUBPROCESS_" + k.upper().replace("-", "_")] = v - - if cwd and isinstance(span, Span): - span.set_data("subprocess.cwd", cwd) - - rv = old_popen_init(self, *a, **kw) - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - else: - span.set_tag("subprocess.pid", self.pid) - - return rv - - subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore - - old_popen_wait = subprocess.Popen.wait - - @ensure_integration_enabled(StdlibIntegration, old_popen_wait) - def sentry_patched_popen_wait( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_WAIT, - attributes={ - "sentry.op": OP.SUBPROCESS_WAIT, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_wait(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_WAIT, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_wait(self, *a, **kw) - - subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore - - old_popen_communicate = subprocess.Popen.communicate - - @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) - def sentry_patched_popen_communicate( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_COMMUNICATE, - attributes={ - "sentry.op": OP.SUBPROCESS_COMMUNICATE, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_communicate(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_COMMUNICATE, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_communicate(self, *a, **kw) - - subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore - - -def get_subprocess_traceparent_headers() -> "EnvironHeaders": - return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py deleted file mode 100644 index 5f00e8bf6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/strawberry.py +++ /dev/null @@ -1,493 +0,0 @@ -import functools -import hashlib -import warnings -from inspect import isawaitable - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from functools import cached_property -except ImportError: - # The strawberry integration requires Python 3.8+. functools.cached_property - # was added in 3.8, so this check is technically not needed, but since this - # is an auto-enabling integration, we might get to executing this import in - # lower Python versions, so we need to deal with it. - raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") - -try: - from strawberry import Schema - from strawberry.extensions import SchemaExtension - from strawberry.extensions.tracing.utils import ( - should_skip_tracing as strawberry_should_skip_tracing, - ) - from strawberry.http import async_base_view, sync_base_view -except ImportError: - raise DidNotEnable("strawberry-graphql is not installed") - -try: - from strawberry.extensions.tracing import ( - SentryTracingExtension as StrawberrySentryAsyncExtension, - ) - from strawberry.extensions.tracing import ( - SentryTracingExtensionSync as StrawberrySentrySyncExtension, - ) -except ImportError: - StrawberrySentryAsyncExtension = None - StrawberrySentrySyncExtension = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Generator, List, Optional - - from graphql import GraphQLError, GraphQLResolveInfo - from strawberry.http import GraphQLHTTPResponse - from strawberry.types import ExecutionContext - - from sentry_sdk._types import Event, EventProcessor - - -ignore_logger("strawberry.execution") - - -class StrawberryIntegration(Integration): - identifier = "strawberry" - origin = f"auto.graphql.{identifier}" - - def __init__(self, async_execution: "Optional[bool]" = None) -> None: - if async_execution not in (None, False, True): - raise ValueError( - 'Invalid value for async_execution: "{}" (must be bool)'.format( - async_execution - ) - ) - self.async_execution = async_execution - - @staticmethod - def setup_once() -> None: - version = package_version("strawberry-graphql") - _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") - - _patch_schema_init() - _patch_views() - - -def _patch_schema_init() -> None: - old_schema_init = Schema.__init__ - - @functools.wraps(old_schema_init) - def _sentry_patched_schema_init( - self: "Schema", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) - if integration is None: - return old_schema_init(self, *args, **kwargs) - - extensions = kwargs.get("extensions") or [] - - should_use_async_extension: "Optional[bool]" = None - if integration.async_execution is not None: - should_use_async_extension = integration.async_execution - else: - # try to figure it out ourselves - should_use_async_extension = _guess_if_using_async(extensions) - - if should_use_async_extension is None: - warnings.warn( - "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", - stacklevel=2, - ) - should_use_async_extension = False - - # remove the built in strawberry sentry extension, if present - extensions = [ - extension - for extension in extensions - if extension - not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) - ] - - # add our extension - extensions = [ - SentryAsyncExtension if should_use_async_extension else SentrySyncExtension - ] + extensions - - kwargs["extensions"] = extensions - - return old_schema_init(self, *args, **kwargs) - - Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] - - -class SentryAsyncExtension(SchemaExtension): - def __init__( - self: "Any", - *, - execution_context: "Optional[ExecutionContext]" = None, - ) -> None: - if execution_context: - self.execution_context = execution_context - - @cached_property - def _resource_name(self) -> str: - query_hash = self.hash_query(self.execution_context.query) # type: ignore - - if self.execution_context.operation_name: - return "{}:{}".format(self.execution_context.operation_name, query_hash) - - return query_hash - - def hash_query(self, query: str) -> str: - return hashlib.md5(query.encode("utf-8")).hexdigest() - - def on_operation(self) -> "Generator[None, None, None]": - operation_name = self.execution_context.operation_name - - operation_type = "query" - op = OP.GRAPHQL_QUERY - - if self.execution_context.query is None: - self.execution_context.query = "" - - if self.execution_context.query.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif self.execution_context.query.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - description = operation_type - if operation_name: - description += " {}".format(operation_name) - - sentry_sdk.add_breadcrumb( - category="graphql.operation", - data={ - "operation_name": operation_name, - "operation_type": operation_type, - }, - ) - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_request_event_processor(self.execution_context) - scope.add_event_processor(event_processor) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - additional_attributes: "dict[str, Any]" = {} - - if should_send_default_pii(): - additional_attributes["graphql.document"] = self.execution_context.query - - if operation_name: - additional_attributes["graphql.operation.name"] = operation_name - - graphql_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": op, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - graphql_span = sentry_sdk.start_span( - op=op, - name=description, - origin=StrawberryIntegration.origin, - ) - graphql_span.__enter__() - - if type(graphql_span) is Span: - if should_send_default_pii(): - graphql_span.set_data("graphql.document", self.execution_context.query) - - graphql_span.set_data("graphql.operation.type", operation_type) - graphql_span.set_data("graphql.operation.name", operation_name) - # This attribute is being removed in streamed spans - graphql_span.set_data("graphql.resource_name", self._resource_name) - - yield - - if type(graphql_span) is StreamedSpan: - if self.execution_context.operation_name: - segment = graphql_span._segment - segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) - segment.set_attribute("sentry.op", op) - segment.name = self.execution_context.operation_name - elif isinstance(graphql_span, Span): - transaction = graphql_span.containing_transaction - if transaction and self.execution_context.operation_name: - transaction.name = self.execution_context.operation_name - transaction.source = TransactionSource.COMPONENT - transaction.op = op - - graphql_span.__exit__(None, None, None) - - def on_validate(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - validation_span = sentry_sdk.traces.start_span( - name="validation", - attributes={ - "sentry.op": OP.GRAPHQL_VALIDATE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - validation_span = sentry_sdk.start_span( - op=OP.GRAPHQL_VALIDATE, - name="validation", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during validation, we still need to close the span - try: - yield - finally: - if isinstance(validation_span, StreamedSpan): - validation_span.end() - else: - validation_span.finish() - - def on_parse(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - parsing_span = sentry_sdk.traces.start_span( - name="parsing", - attributes={ - "sentry.op": OP.GRAPHQL_PARSE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - parsing_span = sentry_sdk.start_span( - op=OP.GRAPHQL_PARSE, - name="parsing", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during parsing, we still need to close the span - try: - yield - finally: - if isinstance(parsing_span, StreamedSpan): - parsing_span.end() - else: - parsing_span.finish() - - def should_skip_tracing( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - info: "GraphQLResolveInfo", - ) -> bool: - return strawberry_should_skip_tracing(_next, info) - - async def _resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result - - async def resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return await self._resolve(_next, root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return await self._resolve(_next, root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return await self._resolve(_next, root, info, *args, **kwargs) - - -class SentrySyncExtension(SentryAsyncExtension): - def resolve( - self, - _next: "Callable[[Any, Any, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return _next(root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return _next(root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return _next(root, info, *args, **kwargs) - - -def _patch_views() -> None: - old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors - old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors - - def _sentry_patched_async_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_async_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - def _sentry_patched_sync_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_sync_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - @ensure_integration_enabled(StrawberryIntegration) - def _sentry_patched_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - if not errors: - return - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_response_event_processor(response_data) - scope.add_event_processor(event_processor) - - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": StrawberryIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_async_view_handle_errors - ) - sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_sync_view_handle_errors - ) - - -def _make_request_event_processor( - execution_context: "ExecutionContext", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - request_data = event.setdefault("request", {}) - request_data["api_target"] = "graphql" - - if not request_data.get("data"): - data: "dict[str, Any]" = {"query": execution_context.query} - if execution_context.variables: - data["variables"] = execution_context.variables - if execution_context.operation_name: - data["operationName"] = execution_context.operation_name - - request_data["data"] = data - - else: - try: - del event["request"]["data"] - except (KeyError, TypeError): - pass - - return event - - return inner - - -def _make_response_event_processor( - response_data: "GraphQLHTTPResponse", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - contexts = event.setdefault("contexts", {}) - contexts["response"] = {"data": response_data} - - return event - - return inner - - -def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": - if StrawberrySentryAsyncExtension in extensions: - return True - elif StrawberrySentrySyncExtension in extensions: - return False - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py deleted file mode 100644 index 4927c0e885..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/sys_exit.py +++ /dev/null @@ -1,65 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import NoReturn, Union - - -class SysExitIntegration(Integration): - """Captures sys.exit calls and sends them as events to Sentry. - - By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit - exceptions generated by sys.exit calls and send them to Sentry. - - This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and - non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. - Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. - """ - - identifier = "sys_exit" - - def __init__(self, *, capture_successful_exits: bool = False) -> None: - self._capture_successful_exits = capture_successful_exits - - @staticmethod - def setup_once() -> None: - SysExitIntegration._patch_sys_exit() - - @staticmethod - def _patch_sys_exit() -> None: - old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit - - @functools.wraps(old_exit) - def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": - # @ensure_integration_enabled ensures that this is non-None - integration = sentry_sdk.get_client().get_integration(SysExitIntegration) - if integration is None: - old_exit(__status) - - try: - old_exit(__status) - except SystemExit as e: - with capture_internal_exceptions(): - if integration._capture_successful_exits or __status not in ( - 0, - None, - ): - _capture_exception(e) - raise e - - sys.exit = sentry_patched_exit - - -def _capture_exception(exc: "SystemExit") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": SysExitIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py deleted file mode 100644 index f3ba046332..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/threading.py +++ /dev/null @@ -1,196 +0,0 @@ -import sys -import warnings -from concurrent.futures import Future, ThreadPoolExecutor -from functools import wraps -from threading import Thread, current_thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import use_isolation_scope, use_scope -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T", bound=Any) - - -class ThreadingIntegration(Integration): - identifier = "threading" - - def __init__( - self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True - ) -> None: - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - - self.propagate_scope = propagate_scope - - if propagate_hub is not None: - self.propagate_scope = propagate_hub - - @staticmethod - def setup_once() -> None: - old_start = Thread.start - - try: - from django import VERSION as django_version # noqa: N811 - except ImportError: - django_version = None - - try: - import channels # type: ignore[import-untyped] - - channels_version = channels.__version__ - except (ImportError, AttributeError): - channels_version = None - - is_async_emulated_with_threads = ( - sys.version_info < (3, 9) - and channels_version is not None - and channels_version < "4.0.0" - and django_version is not None - and django_version >= (3, 0) - and django_version < (4, 0) - ) - - @wraps(old_start) - def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return old_start(self, *a, **kw) - - if integration.propagate_scope: - if is_async_emulated_with_threads: - warnings.warn( - "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " - "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " - "Please either upgrade to Django channels 4.0+, use Django's async features " - "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", - stacklevel=2, - ) - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - - else: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - # Patching instance methods in `start()` creates a reference cycle if - # done in a naive way. See - # https://github.com/getsentry/sentry-python/pull/434 - # - # In threading module, using current_thread API will access current thread instance - # without holding it to avoid a reference cycle in an easier way. - with capture_internal_exceptions(): - new_run = _wrap_run( - isolation_scope, - current_scope, - getattr(self.run, "__func__", self.run), - ) - self.run = new_run # type: ignore - - return old_start(self, *a, **kw) - - Thread.start = sentry_start # type: ignore - ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore - ThreadPoolExecutor.submit, is_async_emulated_with_threads - ) - - -def _wrap_run( - isolation_scope_to_use: "Optional[sentry_sdk.Scope]", - current_scope_to_use: "Optional[sentry_sdk.Scope]", - old_run_func: "F", -) -> "F": - @wraps(old_run_func) - def run(*a: "Any", **kw: "Any") -> "Any": - def _run_old_run_func() -> "Any": - try: - self = current_thread() - return old_run_func(self, *a[1:], **kw) - except Exception: - reraise(*_capture_exception()) - - if isolation_scope_to_use is not None and current_scope_to_use is not None: - with use_isolation_scope(isolation_scope_to_use): - with use_scope(current_scope_to_use): - return _run_old_run_func() - else: - return _run_old_run_func() - - return run # type: ignore - - -def _wrap_threadpool_executor_submit( - func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool -) -> "Callable[..., Future[T]]": - """ - Wrap submit call to propagate scopes on task submission. - """ - - @wraps(func) - def sentry_submit( - self: "ThreadPoolExecutor", - fn: "Callable[..., T]", - *args: "Any", - **kwargs: "Any", - ) -> "Future[T]": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return func(self, fn, *args, **kwargs) - - if integration.propagate_scope and is_async_emulated_with_threads: - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - elif integration.propagate_scope: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": - if isolation_scope is not None and current_scope is not None: - with use_isolation_scope(isolation_scope): - with use_scope(current_scope): - return fn(*args, **kwargs) - - return fn(*args, **kwargs) - - return func(self, wrapped_fn, *args, **kwargs) - - return sentry_submit - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - if client.get_integration(ThreadingIntegration) is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "threading", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py deleted file mode 100644 index 859b0d0870..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/tornado.py +++ /dev/null @@ -1,320 +0,0 @@ -import contextlib -import weakref -from inspect import iscoroutinefunction - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - RequestExtractor, - _filter_headers, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from tornado import version_info as TORNADO_VERSION - from tornado.gen import coroutine - from tornado.web import HTTPError, RequestHandler -except ImportError: - raise DidNotEnable("Tornado not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.tracing import Span - - -class TornadoIntegration(Integration): - identifier = "tornado" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - - ignore_logger("tornado.access") - - old_execute = RequestHandler._execute - - awaitable = iscoroutinefunction(old_execute) - - if awaitable: - # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) - # In that case our method should be a coroutine function too - async def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - return await old_execute(self, *args, **kwargs) - - else: - - @coroutine # type: ignore - def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - result = yield from old_execute(self, *args, **kwargs) - return result - - RequestHandler._execute = sentry_execute_request_handler - - old_log_exception = RequestHandler.log_exception - - def sentry_log_exception( - self: "Any", - ty: type, - value: BaseException, - tb: "Any", - *args: "Any", - **kwargs: "Any", - ) -> "Optional[Any]": - _capture_exception(ty, value, tb) - return old_log_exception(self, ty, value, tb, *args, **kwargs) - - RequestHandler.log_exception = sentry_log_exception - - -_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" - - -@contextlib.contextmanager -def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": - integration = sentry_sdk.get_client().get_integration(TornadoIntegration) - - if integration is None: - yield - return - - weak_handler = weakref.ref(self) - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - headers = self.request.headers - - scope.clear_breadcrumbs() - processor = _make_event_processor(weak_handler) - scope.add_event_processor(processor) - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(dict(headers)) - scope.set_custom_sampling_context({"tornado_request": self.request}) - - if should_send_default_pii() and self.request.remote_ip: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_ROOT_SPAN_NAME, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": TornadoIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # Like with all other integrations, this is our - # fallback transaction in case there is no route. - # sentry_urldispatcher_resolve is responsible for - # setting a transaction name later. - name=_DEFAULT_ROOT_SPAN_NAME, - source=TransactionSource.ROUTE, - origin=TornadoIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"tornado_request": self.request}, - ) - - with span_ctx as span: - try: - yield - finally: - if type(span) is StreamedSpan: - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - self.request - ).items(): - span.set_attribute(attr, value) - - with capture_internal_exceptions(): - method = getattr(self, self.request.method.lower(), None) - if method is not None: - span_name = transaction_from_function(method) - if span_name: - span.name = span_name - span.set_attribute( - "sentry.span.source", - SegmentSource.COMPONENT, - ) - - with capture_internal_exceptions(): - status_int = self.get_status() - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) - span.status = "error" if status_int >= 400 else "ok" - - -def _get_request_attributes(request: "Any") -> "Dict[str, Any]": - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.full_url() - attributes["url.path"] = request.path - - if request.query: - attributes[SPANDATA.URL_QUERY] = request.query - - if request.protocol: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol - - if should_send_default_pii() and request.remote_ip: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip - - with capture_internal_exceptions(): - raw_data = _get_tornado_request_data(request) - body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data - if body_data is not None: - attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data - - return attributes - - -def _get_tornado_request_data( - request: "Any", -) -> "Union[Optional[str], AnnotatedValue]": - body = request.body - if not body: - return None - - if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): - return AnnotatedValue.substituted_because_over_size_limit() - - return body.decode("utf-8", "replace") - - -@ensure_integration_enabled(TornadoIntegration) -def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: - if isinstance(value, HTTPError): - return - - event, hint = event_from_exception( - (ty, value, tb), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "tornado", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - weak_handler: "Callable[[], RequestHandler]", -) -> "EventProcessor": - def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - handler = weak_handler() - if handler is None: - return event - - request = handler.request - - with capture_internal_exceptions(): - method = getattr(handler, handler.request.method.lower()) - event["transaction"] = transaction_from_function(method) or "" - event["transaction_info"] = {"source": TransactionSource.COMPONENT} - - with capture_internal_exceptions(): - extractor = TornadoRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - - request_info["url"] = "%s://%s%s" % ( - request.protocol, - request.host, - request.path, - ) - - request_info["query_string"] = request.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_ip} - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - try: - current_user = handler.current_user - except Exception: - current_user = None - - if current_user: - event.setdefault("user", {}).setdefault("is_authenticated", True) - - return event - - return tornado_processor - - -class TornadoRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return {k: v.value for k, v in self.request.cookies.items()} - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "Dict[str, Any]": - return { - k: [v.decode("latin1", "replace") for v in vs] - for k, vs in self.request.body_arguments.items() - } - - def is_json(self) -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - def files(self) -> "Dict[str, Any]": - return {k: v[0] for k, v in self.request.files.items() if v} - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py deleted file mode 100644 index 0449a8f10c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/trytond.py +++ /dev/null @@ -1,52 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.utils import ensure_integration_enabled, event_from_exception - -try: - from trytond.exceptions import TrytonException # type: ignore - from trytond.wsgi import app # type: ignore -except ImportError: - raise DidNotEnable("Trytond is not installed.") - -# TODO: trytond-worker, trytond-cron and trytond-admin intergations - - -class TrytondWSGIIntegration(Integration): - identifier = "trytond_wsgi" - origin = f"auto.http.{identifier}" - - def __init__(self) -> None: - pass - - @staticmethod - def setup_once() -> None: - app.wsgi_app = SentryWsgiMiddleware( - app.wsgi_app, - span_origin=TrytondWSGIIntegration.origin, - ) - - @ensure_integration_enabled(TrytondWSGIIntegration) - def error_handler(e: Exception) -> None: - if isinstance(e, TrytonException): - return - else: - client = sentry_sdk.get_client() - event, hint = event_from_exception( - e, - client_options=client.options, - mechanism={"type": "trytond", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - # Expected error handlers signature was changed - # when the error_handler decorator was introduced - # in Tryton-5.4 - if hasattr(app, "error_handler"): - - @app.error_handler - def _(app, request, e): # type: ignore - error_handler(e) - - else: - app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py deleted file mode 100644 index 497f0539ec..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/typer.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - -try: - import typer - from typer.main import except_hook -except ImportError: - raise DidNotEnable("Typer not installed") - - -class TyperIntegration(Integration): - identifier = "typer" - - @staticmethod - def setup_once() -> None: - typer.main.except_hook = _make_excepthook(except_hook) # type: ignore - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(TyperIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "typer", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py deleted file mode 100644 index 0316f6b88a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unleash.py +++ /dev/null @@ -1,33 +0,0 @@ -from functools import wraps -from typing import Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from UnleashClient import UnleashClient -except ImportError: - raise DidNotEnable("UnleashClient is not installed") - - -class UnleashIntegration(Integration): - identifier = "unleash" - - @staticmethod - def setup_once() -> None: - # Wrap and patch evaluation methods (class methods) - old_is_enabled = UnleashClient.is_enabled - - @wraps(old_is_enabled) - def sentry_is_enabled( - self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_is_enabled(self, feature, *args, **kwargs) - - # We have no way of knowing what type of unleash feature this is, so we have to treat - # it as a boolean / toggle feature. - add_feature_flag(feature, enabled) - - return enabled - - UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py deleted file mode 100644 index 2c7280a1f2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/unraisablehook.py +++ /dev/null @@ -1,50 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Callable - - -class UnraisablehookIntegration(Integration): - identifier = "unraisablehook" - - @staticmethod - def setup_once() -> None: - sys.unraisablehook = _make_unraisable(sys.unraisablehook) - - -def _make_unraisable( - old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", -) -> "Callable[[sys.UnraisableHookArgs], Any]": - def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: - integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_unraisablehook(unraisable) - - if unraisable.exc_value and unraisable.exc_traceback: - with capture_internal_exceptions(): - event, hint = event_from_exception( - ( - unraisable.exc_type, - unraisable.exc_value, - unraisable.exc_traceback, - ), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "unraisablehook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_unraisablehook(unraisable) - - return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py deleted file mode 100644 index e776ed915a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/integrations/wsgi.py +++ /dev/null @@ -1,427 +0,0 @@ -import sys -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._werkzeug import _get_headers, get_host -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - _filter_headers, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Iterator, - Optional, - Protocol, - Tuple, - TypeVar, - Union, - ) - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - WsgiResponseIter = TypeVar("WsgiResponseIter") - WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") - WsgiExcInfo = TypeVar("WsgiExcInfo") - - class StartResponse(Protocol): - def __call__( - self, - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, - ) -> "WsgiResponseIter": # type: ignore - pass - - -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") -_DEFAULT_TRANSACTION_NAME = "generic WSGI request" - - -def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: - return s.encode("latin1").decode(charset, errors) - - -def get_request_url( - environ: "Dict[str, str]", use_x_forwarded_for: bool = False -) -> str: - """Return the absolute URL without query string for the given WSGI - environment.""" - script_name = environ.get("SCRIPT_NAME", "").rstrip("/") - path_info = environ.get("PATH_INFO", "").lstrip("/") - path = f"{script_name}/{path_info}" - - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - return "%s://%s/%s" % ( - scheme, - get_host(environ, use_x_forwarded_for), - wsgi_decoding_dance(path).lstrip("/"), - ) - - -class SentryWsgiMiddleware: - __slots__ = ( - "app", - "use_x_forwarded_for", - "span_origin", - "http_methods_to_capture", - ) - - def __init__( - self, - app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", - use_x_forwarded_for: bool = False, - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - self.app = app - self.use_x_forwarded_for = use_x_forwarded_for - self.span_origin = span_origin - self.http_methods_to_capture = http_methods_to_capture - - def __call__( - self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "Any": - if _wsgi_middleware_applied.get(False): - return self.app(environ, start_response) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _wsgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope._name = "wsgi" - scope.add_event_processor( - _make_wsgi_event_processor( - environ, self.use_x_forwarded_for - ) - ) - - method = environ.get("REQUEST_METHOD", "").upper() - - span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None - if method in self.http_methods_to_capture: - if span_streaming: - sentry_sdk.traces.continue_trace( - dict(_get_headers(environ)) - ) - Scope.set_custom_sampling_context({"wsgi_environ": environ}) - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, client_ip - ) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_TRANSACTION_NAME, - attributes={ - "sentry.span.source": SegmentSource.ROUTE, - "sentry.origin": self.span_origin, - "sentry.op": OP.HTTP_SERVER, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - environ, - op=OP.HTTP_SERVER, - name=_DEFAULT_TRANSACTION_NAME, - source=TransactionSource.ROUTE, - origin=self.span_origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"wsgi_environ": environ}, - ) - - span_ctx = span_ctx or nullcontext() - - with span_ctx as span: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - environ, self.use_x_forwarded_for - ).items(): - span.set_attribute(attr, value) - - try: - response = self.app( - environ, - partial(_sentry_start_response, start_response, span), - ) - except BaseException: - reraise(*_capture_exception()) - finally: - _wsgi_middleware_applied.set(False) - - # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses - # is determined by a pointer equality check on the response object - # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). - # - # If we were to return a _ScopedResponse, this would cause the check to always fail - # since it's checking the files are exactly the same. - # - # To avoid this and ensure that the offloading mechanism works as expected when it's - # enabled, we check if the response is a file-like object (determined by the presence - # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, - # it would've been used in handling the file in the response). - # - # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, - # so we want to make sure we don't interfere with those either. - # - # If all conditions are met, we return the original response object directly, - # allowing uWSGI to handle it as intended. - if ( - environ.get("wsgi.file_wrapper") - and getattr(response, "fileno", None) is not None - ): - return response - - return _ScopedResponse(scope, response) - - -def _sentry_start_response( - old_start_response: "StartResponse", - span: "Optional[Union[Span, StreamedSpan]]", - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, -) -> "WsgiResponseIter": # type: ignore[type-var] - with capture_internal_exceptions(): - status_int = int(status.split(" ", 1)[0]) - if span is not None: - if isinstance(span, StreamedSpan): - span.status = "error" if status_int >= 400 else "ok" - span.set_attribute("http.response.status_code", status_int) - else: - span.set_http_status(status_int) - - if exc_info is None: - # The Django Rest Framework WSGI test client, and likely other - # (incorrect) implementations, cannot deal with the exc_info argument - # if one is present. Avoid providing a third argument if not necessary. - return old_start_response(status, response_headers) - else: - return old_start_response(status, response_headers, exc_info) - - -def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns our explicitly included environment variables we want to - capture (server name, port and remote addr if pii is enabled). - """ - keys = ["SERVER_NAME", "SERVER_PORT"] - if should_send_default_pii(): - # make debugging of proxy setup easier. Proxy headers are - # in headers. - keys += ["REMOTE_ADDR"] - - for key in keys: - if key in environ: - yield key, environ[key] - - -def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": - """ - Infer the user IP address from various headers. This cannot be used in - security sensitive situations since the value may be forged from a client, - but it's good enough for the event payload. - """ - try: - return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return environ["HTTP_X_REAL_IP"] - except KeyError: - pass - - return environ.get("REMOTE_ADDR") - - -def _capture_exception() -> "ExcInfo": - """ - Captures the current exception and sends it to Sentry. - Returns the ExcInfo tuple to it can be reraised afterwards. - """ - exc_info = sys.exc_info() - e = exc_info[1] - - # SystemExit(0) is the only uncaught exception that is expected behavior - should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) - if not should_skip_capture: - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "wsgi", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class _ScopedResponse: - """ - Users a separate scope for each response chunk. - - This will make WSGI apps more tolerant against: - - WSGI servers streaming responses from a different thread/from - different threads than the one that called start_response - - close() not being called - - WSGI servers streaming responses interleaved from the same thread - """ - - __slots__ = ("_response", "_scope") - - def __init__( - self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" - ) -> None: - self._scope = scope - self._response = response - - def __iter__(self) -> "Iterator[bytes]": - iterator = iter(self._response) - - while True: - with use_isolation_scope(self._scope): - try: - chunk = next(iterator) - except StopIteration: - break - except BaseException: - reraise(*_capture_exception()) - - yield chunk - - def close(self) -> None: - with use_isolation_scope(self._scope): - try: - self._response.close() # type: ignore - except AttributeError: - pass - except BaseException: - reraise(*_capture_exception()) - - -def _make_wsgi_event_processor( - environ: "Dict[str, str]", use_x_forwarded_for: bool -) -> "EventProcessor": - # It's a bit unfortunate that we have to extract and parse the request data - # from the environ so eagerly, but there are a few good reasons for this. - # - # We might be in a situation where the scope never gets torn down - # properly. In that case we will have an unnecessary strong reference to - # all objects in the environ (some of which may take a lot of memory) when - # we're really just interested in a few of them. - # - # Keeping the environment around for longer than the request lifecycle is - # also not necessarily something uWSGI can deal with: - # https://github.com/unbit/uwsgi/issues/1950 - - client_ip = get_client_ip(environ) - request_url = get_request_url(environ, use_x_forwarded_for) - query_string = environ.get("QUERY_STRING") - method = environ.get("REQUEST_METHOD") - env = dict(_get_environ(environ)) - headers = _filter_headers(dict(_get_headers(environ))) - - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - # if the code below fails halfway through we at least have some data - request_info = event.setdefault("request", {}) - - if should_send_default_pii(): - user_info = event.setdefault("user", {}) - if client_ip: - user_info.setdefault("ip_address", client_ip) - - request_info["url"] = request_url - request_info["query_string"] = query_string - request_info["method"] = method - request_info["env"] = env - request_info["headers"] = headers - - return event - - return event_processor - - -def _get_request_attributes( - environ: "Dict[str, str]", - use_x_forwarded_for: bool = False, -) -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from the WSGI environ. - """ - attributes: "dict[str, Any]" = {} - - method = environ.get("REQUEST_METHOD") - if method: - attributes["http.request.method"] = method.upper() - - headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - url_scheme = environ.get("wsgi.url_scheme") - if url_scheme: - attributes["network.protocol.name"] = url_scheme - - server_name = environ.get("SERVER_NAME") - if server_name: - attributes["server.address"] = server_name - - server_port = environ.get("SERVER_PORT") - if server_port: - try: - attributes["server.port"] = int(server_port) - except ValueError: - pass - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - attributes["client.address"] = client_ip - - query_string = environ.get("QUERY_STRING") - if query_string: - attributes["http.query"] = query_string - - path = environ.get("PATH_INFO", "") - if path: - attributes["url.path"] = path - - attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py deleted file mode 100644 index d7f4425dfd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/logger.py +++ /dev/null @@ -1,88 +0,0 @@ -# NOTE: this is the logger sentry exposes to users, not some generic logger. -import functools -import time -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk.utils import capture_internal_exceptions, format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes - - -OTEL_RANGES = [ - # ((severity level range), severity text) - # https://opentelemetry.io/docs/specs/otel/logs/data-model - ((1, 4), "trace"), - ((5, 8), "debug"), - ((9, 12), "info"), - ((13, 16), "warn"), - ((17, 20), "error"), - ((21, 24), "fatal"), -] - - -class _dict_default_key(dict): # type: ignore[type-arg] - """dict that returns the key if missing.""" - - def __missing__(self, key: str) -> str: - return "{" + key + "}" - - -def _capture_log( - severity_text: str, severity_number: int, template: str, **kwargs: "Any" -) -> None: - body = template - - attributes: "Attributes" = {} - - if "attributes" in kwargs: - provided_attributes = kwargs.pop("attributes") or {} - for attribute, value in provided_attributes.items(): - attributes[attribute] = format_attribute(value) - - for k, v in kwargs.items(): - attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) - - if kwargs: - # only attach template if there are parameters - attributes["sentry.message.template"] = format_attribute(template) - - with capture_internal_exceptions(): - body = template.format_map(_dict_default_key(kwargs)) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": severity_text, - "severity_number": severity_number, - "attributes": attributes, - "body": body, - "time_unix_nano": time.time_ns(), - "trace_id": None, - "span_id": None, - } - ) - - -trace = functools.partial(_capture_log, "trace", 1) -debug = functools.partial(_capture_log, "debug", 5) -info = functools.partial(_capture_log, "info", 9) -warning = functools.partial(_capture_log, "warn", 13) -error = functools.partial(_capture_log, "error", 17) -fatal = functools.partial(_capture_log, "fatal", 21) - - -def _otel_severity_text(otel_severity_number: int) -> str: - for (lower, upper), severity in OTEL_RANGES: - if lower <= otel_severity_number <= upper: - return severity - - return "default" - - -def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": - for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): - if level >= py_level: - return otel_severity_number, _otel_severity_text(otel_severity_number) - - return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py deleted file mode 100644 index 27b7468c0d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/metrics.py +++ /dev/null @@ -1,62 +0,0 @@ -import time -from typing import TYPE_CHECKING, Any, Optional - -import sentry_sdk -from sentry_sdk.utils import format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes, Metric, MetricType - - -def _capture_metric( - name: str, - metric_type: "MetricType", - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[Attributes]" = None, -) -> None: - attrs: "Attributes" = {} - - if attributes: - for k, v in attributes.items(): - attrs[k] = format_attribute(v) - - metric: "Metric" = { - "timestamp": time.time(), - "trace_id": None, - "span_id": None, - "name": name, - "type": metric_type, - "value": float(value), - "unit": unit, - "attributes": attrs, - } - - sentry_sdk.get_current_scope()._capture_metric(metric) - - -def count( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "counter", value, unit, attributes) - - -def gauge( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "gauge", value, unit, attributes) - - -def distribution( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py deleted file mode 100644 index d2ba298c35..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/monitor.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import time -import weakref -from threading import Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -MAX_DOWNSAMPLE_FACTOR = 10 - - -class Monitor: - """ - Performs health checks in a separate thread once every interval seconds - and updates the internal state. Other parts of the SDK only read this state - and act accordingly. - """ - - name = "sentry.monitor" - - _thread: "Optional[Thread]" - _thread_for_pid: "Optional[int]" - - def __init__( - self, transport: "sentry_sdk.transport.Transport", interval: float = 10 - ) -> None: - self.transport: "sentry_sdk.transport.Transport" = transport - self.interval: float = interval - - self._healthy = True - self._downsample_factor: int = 0 - self._running = True - self._reset_thread_state() - - # See https://github.com/getsentry/sentry-python/issues/6148. - # If os.fork() runs while another thread holds self._thread_lock, - # the child inherits the lock locked but the holding thread does - # not exist in the child, so the lock can never be released and - # _ensure_running deadlocks forever. Reinitialise the lock and - # cached thread/pid in the child so it starts clean regardless - # of inherited state. We bind via a WeakMethod so the - # permanently-registered fork handler does not pin this Monitor - # (and its Transport): register_at_fork has no unregister API. - # POSIX-only; Windows uses spawn. - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._thread = None - self._thread_lock = Lock() - self._thread_for_pid = None - - def _ensure_running(self) -> None: - """ - Check that the monitor has an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - while self._running: - time.sleep(self.interval) - if self._running: - self.run() - - thread = Thread(name=self.name, target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def run(self) -> None: - self.check_health() - self.set_downsample_factor() - - def set_downsample_factor(self) -> None: - if self._healthy: - if self._downsample_factor > 0: - logger.debug( - "[Monitor] health check positive, reverting to normal sampling" - ) - self._downsample_factor = 0 - else: - if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: - self._downsample_factor += 1 - logger.debug( - "[Monitor] health check negative, downsampling with a factor of %d", - self._downsample_factor, - ) - - def check_health(self) -> None: - """ - Perform the actual health checks, - currently only checks if the transport is rate-limited. - TODO: augment in the future with more checks. - """ - self._healthy = self.transport.is_healthy() - - def is_healthy(self) -> bool: - self._ensure_running() - return self._healthy - - @property - def downsample_factor(self) -> int: - self._ensure_running() - return self._downsample_factor - - def kill(self) -> None: - self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py deleted file mode 100644 index d562405295..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -from sentry_sdk.profiler.continuous_profiler import ( - start_profile_session, - start_profiler, - stop_profile_session, - stop_profiler, -) -from sentry_sdk.profiler.transaction_profiler import ( - MAX_PROFILE_DURATION_NS, - PROFILE_MINIMUM_SAMPLES, - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - has_profiling_enabled, - setup_profiler, - teardown_profiler, -) -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - MAX_STACK_DEPTH, - extract_frame, - extract_stack, - frame_id, - get_frame_name, -) - -__all__ = [ - "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` - "start_profiler", - "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` - "stop_profiler", - # DEPRECATED: The following was re-exported for backwards compatibility. It - # will be removed from sentry_sdk.profiler in a future release. - "MAX_PROFILE_DURATION_NS", - "PROFILE_MINIMUM_SAMPLES", - "Profile", - "Scheduler", - "ThreadScheduler", - "GeventScheduler", - "has_profiling_enabled", - "setup_profiler", - "teardown_profiler", - "DEFAULT_SAMPLING_FREQUENCY", - "MAX_STACK_DEPTH", - "get_frame_name", - "extract_frame", - "extract_stack", - "frame_id", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py deleted file mode 100644 index ed525f52bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/continuous_profiler.py +++ /dev/null @@ -1,703 +0,0 @@ -import atexit -import os -import random -import sys -import threading -import time -import uuid -import warnings -from collections import deque -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.consts import VERSION -from sentry_sdk.envelope import Envelope -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - is_gevent, - logger, - now, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union - - from typing_extensions import TypedDict - - from sentry_sdk._types import ContinuousProfilerMode, SDKInfo - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "timestamp": float, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - ThreadPool = None - - -_scheduler: "Optional[ContinuousScheduler]" = None - - -def setup_continuous_profiler( - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", -) -> bool: - global _scheduler - - already_initialized = _scheduler is not None - - if already_initialized: - logger.debug("[Profiling] Continuous Profiler is already setup") - teardown_continuous_profiler() - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventContinuousScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventContinuousScheduler.mode - else: - default_profiler_mode = ThreadContinuousScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - # TODO: deprecate this and just use the existing `profiler_mode` - experiments = options.get("_experiments", {}) - - profiler_mode = ( - experiments.get("continuous_profiling_mode") or default_profiler_mode - ) - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if profiler_mode == ThreadContinuousScheduler.mode: - _scheduler = ThreadContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - elif profiler_mode == GeventContinuousScheduler.mode: - _scheduler = GeventContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - else: - raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up continuous profiler in {mode} mode".format( - mode=_scheduler.mode - ) - ) - - if not already_initialized: - atexit.register(teardown_continuous_profiler) - - return True - - -def is_profile_session_sampled() -> bool: - if _scheduler is None: - return False - return _scheduler.sampled - - -def try_autostart_continuous_profiler() -> None: - # TODO: deprecate this as it'll be replaced by the auto lifecycle option - - if _scheduler is None: - return - - if not _scheduler.is_auto_start_enabled(): - return - - _scheduler.manual_start() - - -def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": - if _scheduler is None: - return None - - return _scheduler.auto_start() - - -def start_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_start() - - -def start_profile_session() -> None: - warnings.warn( - "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - start_profiler() - - -def stop_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_stop() - - -def stop_profile_session() -> None: - warnings.warn( - "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - stop_profiler() - - -def teardown_continuous_profiler() -> None: - stop_profiler() - - global _scheduler - _scheduler = None - - -def get_profiler_id() -> "Union[str, None]": - if _scheduler is None: - return None - return _scheduler.profiler_id - - -def determine_profile_session_sampling_decision( - sample_rate: "Union[float, None]", -) -> bool: - # `None` is treated as `0.0` - if not sample_rate: - return False - - return random.random() < float(sample_rate) - - -class ContinuousProfile: - active: bool = True - - def stop(self) -> None: - self.active = False - - -class ContinuousScheduler: - mode: "ContinuousProfilerMode" = "unknown" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.interval = 1.0 / frequency - self.options = options - self.sdk_info = sdk_info - self.capture_func = capture_func - - self.lifecycle = self.options.get("profile_lifecycle") - profile_session_sample_rate = self.options.get("profile_session_sample_rate") - self.sampled = determine_profile_session_sampling_decision( - profile_session_sample_rate - ) - - self.sampler = self.make_sampler() - self.buffer: "Optional[ProfileBuffer]" = None - self.pid: "Optional[int]" = None - - self.running = False - self.soft_shutdown = False - - self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) - self.active_profiles: "Set[ContinuousProfile]" = set() - - def is_auto_start_enabled(self) -> bool: - # Ensure that the scheduler only autostarts once per process. - # This is necessary because many web servers use forks to spawn - # additional processes. And the profiler is only spawned on the - # master process, then it often only profiles the main process - # and not the ones where the requests are being handled. - if self.pid == os.getpid(): - return False - - experiments = self.options.get("_experiments") - if not experiments: - return False - - return experiments.get("continuous_profiling_auto_start") - - def auto_start(self) -> "Union[ContinuousProfile, None]": - if not self.sampled: - return None - - if self.lifecycle != "trace": - return None - - logger.debug("[Profiling] Auto starting profiler") - - profile = ContinuousProfile() - - self.new_profiles.append(profile) - self.ensure_running() - - return profile - - def manual_start(self) -> None: - if not self.sampled: - return - - if self.lifecycle != "manual": - return - - self.ensure_running() - - def manual_stop(self) -> None: - if self.lifecycle != "manual": - return - - self.teardown() - - def ensure_running(self) -> None: - raise NotImplementedError - - def teardown(self) -> None: - raise NotImplementedError - - def pause(self) -> None: - raise NotImplementedError - - def reset_buffer(self) -> None: - self.buffer = ProfileBuffer( - self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func - ) - - @property - def profiler_id(self) -> "Union[str, None]": - if not self.running or self.buffer is None: - return None - return self.buffer.profiler_id - - def make_sampler(self) -> "Callable[..., bool]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - if self.lifecycle == "trace": - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - return True - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - inactive_profiles = [] - - for profile in self.active_profiles: - if not profile.active: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - else: - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - return _sample_stack - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.soft_shutdown = self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # the soft shutdown happens here to give it a chance - # for the profiler to be reused - if self.soft_shutdown: - self.running = False - - # make sure to explicitly exit the profiler here or there might - # be multiple profilers at once - break - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - buffer = self.buffer - if buffer is not None: - buffer.flush() - - -class ThreadContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ContinuousProfilerMode" = "thread" - name = "sentry.profiler.ThreadContinuousScheduler" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[threading.Thread]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -class GeventContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ContinuousProfilerMode" = "gevent" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[_ThreadPool]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -PROFILE_BUFFER_SECONDS = 60 - - -class ProfileBuffer: - def __init__( - self, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - buffer_size: int, - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.options = options - self.sdk_info = sdk_info - self.buffer_size = buffer_size - self.capture_func = capture_func - - self.profiler_id = uuid.uuid4().hex - self.chunk = ProfileChunk() - - # Make sure to use the same clock to compute a sample's monotonic timestamp - # to ensure the timestamps are correctly aligned. - self.start_monotonic_time = now() - - # Make sure the start timestamp is defined only once per profiler id. - # This prevents issues with clock drift within a single profiler session. - # - # Subtracting the start_monotonic_time here to find a fixed starting position - # for relative monotonic timestamps for each sample. - self.start_timestamp = ( - datetime.now(timezone.utc).timestamp() - self.start_monotonic_time - ) - - def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: - if self.should_flush(monotonic_time): - self.flush() - self.chunk = ProfileChunk() - self.start_monotonic_time = now() - - self.chunk.write(self.start_timestamp + monotonic_time, sample) - - def should_flush(self, monotonic_time: float) -> bool: - # If the delta between the new monotonic time and the start monotonic time - # exceeds the buffer size, it means we should flush the chunk - return monotonic_time - self.start_monotonic_time >= self.buffer_size - - def flush(self) -> None: - chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) - envelope = Envelope() - envelope.add_profile_chunk(chunk) - self.capture_func(envelope) - - -class ProfileChunk: - def __init__(self) -> None: - self.chunk_id = uuid.uuid4().hex - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - def write(self, ts: float, sample: "ExtractedSample") -> None: - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "timestamp": ts, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def to_json( - self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" - ) -> "Dict[str, Any]": - profile = { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - }, - } - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - payload = { - "chunk_id": self.chunk_id, - "client_sdk": { - "name": sdk_info["name"], - "version": VERSION, - }, - "platform": "python", - "profile": profile, - "profiler_id": profiler_id, - "version": "2", - } - - for key in "release", "environment", "dist": - if options[key] is not None: - payload[key] = str(options[key]).strip() - - return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py deleted file mode 100644 index fe65774c0b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/transaction_profiler.py +++ /dev/null @@ -1,802 +0,0 @@ -""" -This file is originally based on code from https://github.com/nylas/nylas-perftools, -which is published under the following license: - -The MIT License (MIT) - -Copyright (c) 2014 Nylas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import atexit -import os -import platform -import random -import sys -import threading -import time -import uuid -import warnings -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - capture_internal_exceptions, - get_current_thread_meta, - is_gevent, - is_valid_sample_rate, - logger, - nanosecond_time, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type - - from typing_extensions import TypedDict - - from sentry_sdk._types import Event, ProfilerMode, SamplingContext - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - ProcessedThreadMetadata, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "elapsed_since_start_ns": str, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - ProcessedProfile = TypedDict( - "ProcessedProfile", - { - "frames": List[ProcessedFrame], - "stacks": List[ProcessedStack], - "samples": List[ProcessedSample], - "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - - ThreadPool = None - - -_scheduler: "Optional[Scheduler]" = None - - -# The minimum number of unique samples that must exist in a profile to be -# considered valid. -PROFILE_MINIMUM_SAMPLES = 2 - - -def has_profiling_enabled(options: "Dict[str, Any]") -> bool: - profiles_sampler = options["profiles_sampler"] - if profiles_sampler is not None: - return True - - profiles_sample_rate = options["profiles_sample_rate"] - if profiles_sample_rate is not None and profiles_sample_rate > 0: - return True - - profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") - if profiles_sample_rate is not None: - logger.warning( - "_experiments['profiles_sample_rate'] is deprecated. " - "Please use the non-experimental profiles_sample_rate option " - "directly." - ) - if profiles_sample_rate > 0: - return True - - return False - - -def setup_profiler(options: "Dict[str, Any]") -> bool: - global _scheduler - - if _scheduler is not None: - logger.debug("[Profiling] Profiler is already setup") - return False - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventScheduler.mode - else: - default_profiler_mode = ThreadScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - profiler_mode = options.get("_experiments", {}).get("profiler_mode") - if profiler_mode is not None: - logger.warning( - "_experiments['profiler_mode'] is deprecated. Please use the " - "non-experimental profiler_mode option directly." - ) - profiler_mode = profiler_mode or default_profiler_mode - - if ( - profiler_mode == ThreadScheduler.mode - # for legacy reasons, we'll keep supporting sleep mode for this scheduler - or profiler_mode == "sleep" - ): - _scheduler = ThreadScheduler(frequency=frequency) - elif profiler_mode == GeventScheduler.mode: - _scheduler = GeventScheduler(frequency=frequency) - else: - raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) - ) - _scheduler.setup() - - atexit.register(teardown_profiler) - - return True - - -def teardown_profiler() -> None: - global _scheduler - - if _scheduler is not None: - _scheduler.teardown() - - _scheduler = None - - -MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds - - -class Profile: - def __init__( - self, - sampled: "Optional[bool]", - start_ns: int, - hub: "Optional[sentry_sdk.Hub]" = None, - scheduler: "Optional[Scheduler]" = None, - ) -> None: - self.scheduler = _scheduler if scheduler is None else scheduler - - self.event_id: str = uuid.uuid4().hex - - self.sampled: "Optional[bool]" = sampled - - # Various framework integrations are capable of overwriting the active thread id. - # If it is set to `None` at the end of the profile, we fall back to the default. - self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 - self.active_thread_id: "Optional[int]" = None - - try: - self.start_ns: int = start_ns - except AttributeError: - self.start_ns = 0 - - self.stop_ns: int = 0 - self.active: bool = False - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - self.unique_samples = 0 - - # Backwards compatibility with the old hub property - self._hub: "Optional[sentry_sdk.Hub]" = None - if hub is not None: - self._hub = hub - warnings.warn( - "The `hub` parameter is deprecated. Please do not use it.", - DeprecationWarning, - stacklevel=2, - ) - - def update_active_thread_id(self) -> None: - self.active_thread_id = get_current_thread_meta()[0] - logger.debug( - "[Profiling] updating active thread id to {tid}".format( - tid=self.active_thread_id - ) - ) - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the profile's sampling decision according to the following - precedence rules: - - 1. If the transaction to be profiled is not sampled, that decision - will be used, regardless of anything else. - - 2. Use `profiles_sample_rate` to decide. - """ - - # The corresponding transaction was not sampled, - # so don't generate a profile for it. - if not self.sampled: - logger.debug( - "[Profiling] Discarding profile because transaction is discarded." - ) - self.sampled = False - return - - # The profiler hasn't been properly initialized. - if self.scheduler is None: - logger.debug( - "[Profiling] Discarding profile because profiler was not started." - ) - self.sampled = False - return - - client = sentry_sdk.get_client() - if not client.is_active(): - self.sampled = False - return - - options = client.options - - if callable(options.get("profiles_sampler")): - sample_rate = options["profiles_sampler"](sampling_context) - elif options["profiles_sample_rate"] is not None: - sample_rate = options["profiles_sample_rate"] - else: - sample_rate = options["_experiments"].get("profiles_sample_rate") - - # The profiles_sample_rate option was not set, so profiling - # was never enabled. - if sample_rate is None: - logger.debug( - "[Profiling] Discarding profile because profiling was not enabled." - ) - self.sampled = False - return - - if not is_valid_sample_rate(sample_rate, source="Profiling"): - logger.warning( - "[Profiling] Discarding profile because of invalid sample rate." - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < float(sample_rate) - - if self.sampled: - logger.debug("[Profiling] Initializing profile") - else: - logger.debug( - "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( - sample_rate=float(sample_rate) - ) - ) - - def start(self) -> None: - if not self.sampled or self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Starting profile") - self.active = True - if not self.start_ns: - self.start_ns = nanosecond_time() - self.scheduler.start_profiling(self) - - def stop(self) -> None: - if not self.sampled or not self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Stopping profile") - self.active = False - self.stop_ns = nanosecond_time() - - def __enter__(self) -> "Profile": - scope = sentry_sdk.get_isolation_scope() - old_profile = scope.profile - scope.profile = self - - self._context_manager_state = (scope, old_profile) - - self.start() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - with capture_internal_exceptions(): - self.stop() - - scope, old_profile = self._context_manager_state - del self._context_manager_state - - scope.profile = old_profile - - def write(self, ts: int, sample: "ExtractedSample") -> None: - if not self.active: - return - - if ts < self.start_ns: - return - - offset = ts - self.start_ns - if offset > MAX_PROFILE_DURATION_NS: - self.stop() - return - - self.unique_samples += 1 - - elapsed_since_start_ns = str(offset) - - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "elapsed_since_start_ns": elapsed_since_start_ns, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def process(self) -> "ProcessedProfile": - # This collects the thread metadata at the end of a profile. Doing it - # this way means that any threads that terminate before the profile ends - # will not have any metadata associated with it. - thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - } - - return { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": thread_metadata, - } - - def to_json( - self, event_opt: "Event", options: "Dict[str, Any]" - ) -> "Dict[str, Any]": - profile = self.process() - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - return { - "environment": event_opt.get("environment"), - "event_id": self.event_id, - "platform": "python", - "profile": profile, - "release": event_opt.get("release", ""), - "timestamp": event_opt["start_timestamp"], - "version": "1", - "device": { - "architecture": platform.machine(), - }, - "os": { - "name": platform.system(), - "version": platform.release(), - }, - "runtime": { - "name": platform.python_implementation(), - "version": platform.python_version(), - }, - "transactions": [ - { - "id": event_opt["event_id"], - "name": event_opt["transaction"], - # we start the transaction before the profile and this is - # the transaction start time relative to the profile, so we - # hardcode it to 0 until we can start the profile before - "relative_start_ns": "0", - # use the duration of the profile instead of the transaction - # because we end the transaction after the profile - "relative_end_ns": str(self.stop_ns - self.start_ns), - "trace_id": event_opt["contexts"]["trace"]["trace_id"], - "active_thread_id": str( - self._default_active_thread_id - if self.active_thread_id is None - else self.active_thread_id - ), - } - ], - } - - def valid(self) -> bool: - client = sentry_sdk.get_client() - if not client.is_active(): - return False - - if not has_profiling_enabled(client.options): - return False - - if self.sampled is None or not self.sampled: - if client.transport: - client.transport.record_lost_event( - "sample_rate", data_category="profile" - ) - return False - - if self.unique_samples < PROFILE_MINIMUM_SAMPLES: - if client.transport: - client.transport.record_lost_event( - "insufficient_data", data_category="profile" - ) - logger.debug("[Profiling] Discarding profile because insufficient samples.") - return False - - return True - - @property - def hub(self) -> "Optional[sentry_sdk.Hub]": - warnings.warn( - "The `hub` attribute is deprecated. Please do not access it.", - DeprecationWarning, - stacklevel=2, - ) - return self._hub - - @hub.setter - def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: - warnings.warn( - "The `hub` attribute is deprecated. Please do not set it.", - DeprecationWarning, - stacklevel=2, - ) - self._hub = value - - -class Scheduler(ABC): - mode: "ProfilerMode" = "unknown" - - def __init__(self, frequency: int) -> None: - self.interval = 1.0 / frequency - - self.sampler = self.make_sampler() - - # cap the number of new profiles at any time so it does not grow infinitely - self.new_profiles: "Deque[Profile]" = deque(maxlen=128) - self.active_profiles: "Set[Profile]" = set() - - def __enter__(self) -> "Scheduler": - self.setup() - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self.teardown() - - @abstractmethod - def setup(self) -> None: - pass - - @abstractmethod - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - """ - Ensure the scheduler is running. By default, this method is a no-op. - The method should be overridden by any implementation for which it is - relevant. - """ - return None - - def start_profiling(self, profile: "Profile") -> None: - self.ensure_running() - self.new_profiles.append(profile) - - def make_sampler(self) -> "Callable[..., None]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - def _sample_stack(*args: "Any", **kwargs: "Any") -> None: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - # make sure to clear the cache if we're not profiling so we dont - # keep a reference to the last stack of frames around - return - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - now = nanosecond_time() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - - inactive_profiles = [] - - for profile in self.active_profiles: - if profile.active: - profile.write(now, sample) - else: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - return _sample_stack - - -class ThreadScheduler(Scheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ProfilerMode" = "thread" - name = "sentry.profiler.ThreadScheduler" - - def __init__(self, frequency: int) -> None: - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[threading.Thread]" = None - self.pid: "Optional[int]" = None - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - """ - Check that the profiler has an active thread to run in, and start one if - that's not the case. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self.running - will be False after running this function. - """ - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - -class GeventScheduler(Scheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ProfilerMode" = "gevent" - name = "sentry.profiler.GeventScheduler" - - def __init__(self, frequency: int) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[_ThreadPool]" = None - self.pid: "Optional[int]" = None - - # This intentionally uses the gevent patched threading.Lock. - # The lock will be required when first trying to start profiles - # as we need to spawn the profiler thread from the greenlets. - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py deleted file mode 100644 index e9f0fec07f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/profiler/utils.py +++ /dev/null @@ -1,186 +0,0 @@ -import os -from collections import deque -from typing import TYPE_CHECKING - -from sentry_sdk._compat import PY311 -from sentry_sdk.utils import filename_for_module - -if TYPE_CHECKING: - from types import FrameType - from typing import Deque, List, Optional, Sequence, Tuple - - from typing_extensions import TypedDict - - from sentry_sdk._lru_cache import LRUCache - - ThreadId = str - - ProcessedStack = List[int] - - ProcessedFrame = TypedDict( - "ProcessedFrame", - { - "abs_path": str, - "filename": Optional[str], - "function": str, - "lineno": int, - "module": Optional[str], - }, - ) - - ProcessedThreadMetadata = TypedDict( - "ProcessedThreadMetadata", - {"name": str}, - ) - - FrameId = Tuple[ - str, # abs_path - int, # lineno - str, # function - ] - FrameIds = Tuple[FrameId, ...] - - # The exact value of this id is not very meaningful. The purpose - # of this id is to give us a compact and unique identifier for a - # raw stack that can be used as a key to a dictionary so that it - # can be used during the sampled format generation. - StackId = Tuple[int, int] - - ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] - ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] - -# The default sampling frequency to use. This is set at 101 in order to -# mitigate the effects of lockstep sampling. -DEFAULT_SAMPLING_FREQUENCY = 101 - - -# We want to impose a stack depth limit so that samples aren't too large. -MAX_STACK_DEPTH = 128 - - -if PY311: - - def get_frame_name(frame: "FrameType") -> str: - return frame.f_code.co_qualname - -else: - - def get_frame_name(frame: "FrameType") -> str: - f_code = frame.f_code - co_varnames = f_code.co_varnames - - # co_name only contains the frame name. If the frame was a method, - # the class name will NOT be included. - name = f_code.co_name - - # if it was a method, we can get the class name by inspecting - # the f_locals for the `self` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `self` if its an instance method - co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals - ): - for cls in type(frame.f_locals["self"]).__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # if it was a class method, (decorated with `@classmethod`) - # we can get the class name by inspecting the f_locals for the `cls` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `cls` if its a class method - co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals - ): - for cls in frame.f_locals["cls"].__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # nothing we can do if it is a staticmethod (decorated with @staticmethod) - - # we've done all we can, time to give up and return what we have - return name - - -def frame_id(raw_frame: "FrameType") -> "FrameId": - return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) - - -def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": - abs_path = raw_frame.f_code.co_filename - - try: - module = raw_frame.f_globals["__name__"] - except Exception: - module = None - - # namedtuples can be many times slower when initialing - # and accessing attribute so we opt to use a tuple here instead - return { - # This originally was `os.path.abspath(abs_path)` but that had - # a large performance overhead. - # - # According to docs, this is equivalent to - # `os.path.normpath(os.path.join(os.getcwd(), path))`. - # The `os.getcwd()` call is slow here, so we precompute it. - # - # Additionally, since we are using normalized path already, - # we skip calling `os.path.normpath` entirely. - "abs_path": os.path.join(cwd, abs_path), - "module": module, - "filename": filename_for_module(module, abs_path) or None, - "function": fid[2], - "lineno": raw_frame.f_lineno, - } - - -def extract_stack( - raw_frame: "Optional[FrameType]", - cache: "LRUCache", - cwd: str, - max_stack_depth: int = MAX_STACK_DEPTH, -) -> "ExtractedStack": - """ - Extracts the stack starting the specified frame. The extracted stack - assumes the specified frame is the top of the stack, and works back - to the bottom of the stack. - - In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, - only the first `MAX_STACK_DEPTH` frames will be returned. - """ - - raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) - - while raw_frame is not None: - f_back = raw_frame.f_back - raw_frames.append(raw_frame) - raw_frame = f_back - - frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) - frames = [] - for i, fid in enumerate(frame_ids): - frame = cache.get(fid) - if frame is None: - frame = extract_frame(fid, raw_frames[i], cwd) - cache.set(fid, frame) - frames.append(frame) - - # Instead of mapping the stack into frame ids and hashing - # that as a tuple, we can directly hash the stack. - # This saves us from having to generate yet another list. - # Additionally, using the stack as the key directly is - # costly because the stack can be large, so we pre-hash - # the stack, and use the hash as the key as this will be - # needed a few times to improve performance. - # - # To Reduce the likelihood of hash collisions, we include - # the stack depth. This means that only stacks of the same - # depth can suffer from hash collisions. - stack_id = len(raw_frames), hash(frame_ids) - - return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py deleted file mode 100644 index 4fd22714cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scope.py +++ /dev/null @@ -1,2185 +0,0 @@ -import os -import platform -import sys -import warnings -from collections import deque -from contextlib import contextmanager -from copy import copy, deepcopy -from datetime import datetime, timezone -from enum import Enum -from functools import wraps -from itertools import chain -from typing import TYPE_CHECKING, cast - -import sentry_sdk -from sentry_sdk._types import AnnotatedValue -from sentry_sdk.attachments import Attachment -from sentry_sdk.consts import ( - DEFAULT_MAX_BREADCRUMBS, - FALSE_VALUES, - INSTRUMENTER, - SPANDATA, -) -from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.profiler.transaction_profiler import Profile -from sentry_sdk.session import Session -from sentry_sdk.traces import ( - _DEFAULT_PARENT_SPAN, - NoOpStreamedSpan, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.tracing_utils import ( - Baggage, - PropagationContext, - _make_sampling_decision, - has_span_streaming_enabled, - has_tracing_enabled, - is_ignored_span, -) -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exception, - capture_internal_exceptions, - datetime_from_isoformat, - disable_capture_event, - event_from_exception, - exc_info_from_error, - format_attribute, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - Deque, - Dict, - Generator, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - ) - - from typing_extensions import Unpack - - import sentry_sdk - from sentry_sdk._types import ( - Attributes, - AttributeValue, - Breadcrumb, - BreadcrumbHint, - ErrorProcessor, - Event, - EventProcessor, - ExcInfo, - Hint, - Log, - LogLevelStr, - Metric, - SamplingContext, - Type, - ) - from sentry_sdk.tracing import TransactionKwargs - - P = ParamSpec("P") - R = TypeVar("R") - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T") - - -# Holds data that will be added to **all** events sent by this process. -# In case this is a http server (think web framework) with multiple users -# the data will be added to events of all users. -# Typically this is used for process wide data such as the release. -_global_scope: "Optional[Scope]" = None - -# Holds data for the active request. -# This is used to isolate data for different requests or users. -# The isolation scope is usually created by integrations, but may also -# be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) - -# Holds data for the active span. -# This can be used to manually add additional data to a span. -_current_scope = ContextVar("current_scope", default=None) - -global_event_processors: "List[EventProcessor]" = [] - -# A function returning a (trace_id, span_id) tuple -# from an external tracing source (such as otel) -_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None - - -class ScopeType(Enum): - CURRENT = "current" - ISOLATION = "isolation" - GLOBAL = "global" - MERGED = "merged" - - -class _ScopeManager: - def __init__(self, hub: "Optional[Any]" = None) -> None: - self._old_scopes: "List[Scope]" = [] - - def __enter__(self) -> "Scope": - isolation_scope = Scope.get_isolation_scope() - - self._old_scopes.append(isolation_scope) - - forked_scope = isolation_scope.fork() - _isolation_scope.set(forked_scope) - - return forked_scope - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - old_scope = self._old_scopes.pop() - _isolation_scope.set(old_scope) - - -def add_global_event_processor(processor: "EventProcessor") -> None: - global_event_processors.append(processor) - - -def register_external_propagation_context( - fn: "Callable[[], Optional[Tuple[str, str]]]", -) -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = fn - - -def remove_external_propagation_context() -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = None - - -def get_external_propagation_context() -> "Optional[Tuple[str, str]]": - return ( - _external_propagation_context_fn() if _external_propagation_context_fn else None - ) - - -def has_external_propagation_context() -> bool: - return _external_propagation_context_fn is not None - - -def _attr_setter(fn: "Any") -> "Any": - return property(fset=fn, doc=fn.__doc__) - - -def _disable_capture(fn: "F") -> "F": - @wraps(fn) - def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": - if not self._should_capture: - return - try: - self._should_capture = False - return fn(self, *args, **kwargs) - finally: - self._should_capture = True - - return wrapper # type: ignore - - -class Scope: - """The scope holds extra information that should be sent with all - events that belong to it. - """ - - # NOTE: Even though it should not happen, the scope needs to not crash when - # accessed by multiple threads. It's fine if it's full of races, but those - # races should never make the user application crash. - # - # The same needs to hold for any accesses of the scope the SDK makes. - - __slots__ = ( - "_level", - "_name", - "_fingerprint", - # note that for legacy reasons, _transaction is the transaction *name*, - # not a Transaction object (the object is stored in _span) - "_transaction", - "_transaction_info", - "_user", - "_tags", - "_contexts", - "_extras", - "_breadcrumbs", - "_n_breadcrumbs_truncated", - "_gen_ai_original_message_count", - "_gen_ai_conversation_id", - "_event_processors", - "_error_processors", - "_should_capture", - "_span", - "_session", - "_attachments", - "_force_auto_session_tracking", - "_profile", - "_propagation_context", - "client", - "_type", - "_last_event_id", - "_flags", - "_attributes", - ) - - def __init__( - self, - ty: "Optional[ScopeType]" = None, - client: "Optional[sentry_sdk.Client]" = None, - ) -> None: - self._type = ty - - self._event_processors: "List[EventProcessor]" = [] - self._error_processors: "List[ErrorProcessor]" = [] - - self._name: "Optional[str]" = None - self._propagation_context: "Optional[PropagationContext]" = None - self._n_breadcrumbs_truncated: int = 0 - self._gen_ai_original_message_count: "Dict[str, int]" = {} - - self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() - - if client is not None: - self.set_client(client) - - self.clear() - - incoming_trace_information = self._load_trace_data_from_env() - self.generate_propagation_context(incoming_data=incoming_trace_information) - - def __copy__(self) -> "Scope": - """ - Returns a copy of this scope. - This also creates a copy of all referenced data structures. - """ - rv: "Scope" = object.__new__(self.__class__) - - rv._type = self._type - rv.client = self.client - rv._level = self._level - rv._name = self._name - rv._fingerprint = self._fingerprint - rv._transaction = self._transaction - rv._transaction_info = self._transaction_info.copy() - rv._user = self._user - - rv._tags = self._tags.copy() - rv._contexts = self._contexts.copy() - rv._extras = self._extras.copy() - - rv._breadcrumbs = copy(self._breadcrumbs) - rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated - rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() - rv._event_processors = self._event_processors.copy() - rv._error_processors = self._error_processors.copy() - rv._propagation_context = self._propagation_context - - rv._should_capture = self._should_capture - rv._span = self._span - rv._session = self._session - rv._force_auto_session_tracking = self._force_auto_session_tracking - rv._attachments = self._attachments.copy() - - rv._profile = self._profile - - rv._last_event_id = self._last_event_id - - rv._flags = deepcopy(self._flags) - - rv._attributes = self._attributes.copy() - - rv._gen_ai_conversation_id = self._gen_ai_conversation_id - - return rv - - @classmethod - def get_current_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the current scope. - """ - current_scope = _current_scope.get() - if current_scope is None: - current_scope = Scope(ty=ScopeType.CURRENT) - _current_scope.set(current_scope) - - return current_scope - - @classmethod - def set_current_scope(cls, new_current_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new current scope overwriting the existing current scope. - :param new_current_scope: The scope to set as the new current scope. - """ - _current_scope.set(new_current_scope) - - @classmethod - def get_isolation_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the isolation scope. - """ - isolation_scope = _isolation_scope.get() - if isolation_scope is None: - isolation_scope = Scope(ty=ScopeType.ISOLATION) - _isolation_scope.set(isolation_scope) - - return isolation_scope - - @classmethod - def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new isolation scope overwriting the existing isolation scope. - :param new_isolation_scope: The scope to set as the new isolation scope. - """ - _isolation_scope.set(new_isolation_scope) - - @classmethod - def get_global_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the global scope. - """ - global _global_scope - if _global_scope is None: - _global_scope = Scope(ty=ScopeType.GLOBAL) - - return _global_scope - - def set_global_attributes(self) -> None: - from sentry_sdk.client import SDK_INFO - - self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) - self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) - - self.set_attribute( - "process.runtime.name", - platform.python_implementation(), - ) - self.set_attribute( - "process.runtime.version", - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", - ) - - options = sentry_sdk.get_client().options - - server_name = options.get("server_name") - if server_name: - self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) - - environment = options.get("environment") - if environment: - self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) - - release = options.get("release") - if release: - self.set_attribute(SPANDATA.SENTRY_RELEASE, release) - - @classmethod - def last_event_id(cls) -> "Optional[str]": - """ - .. versionadded:: 2.2.0 - - Returns event ID of the event most recently captured by the isolation scope, or None if no event - has been captured. We do not consider events that are dropped, e.g. by a before_send hook. - Transactions also are not considered events in this context. - - The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; - whether the event is sent depends on the transport. The event could be sent later or not at all. - Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or - various other reasons. - """ - return cls.get_isolation_scope()._last_event_id - - def _merge_scopes( - self, - additional_scope: "Optional[Scope]" = None, - additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, - ) -> "Scope": - """ - Merges global, isolation and current scope into a new scope and - adds the given additional scope or additional scope kwargs to it. - """ - if additional_scope and additional_scope_kwargs: - raise TypeError("cannot provide scope and kwargs") - - final_scope = copy(_global_scope) if _global_scope is not None else Scope() - final_scope._type = ScopeType.MERGED - - isolation_scope = _isolation_scope.get() - if isolation_scope is not None: - final_scope.update_from_scope(isolation_scope) - - current_scope = _current_scope.get() - if current_scope is not None: - final_scope.update_from_scope(current_scope) - - if self != current_scope and self != isolation_scope: - final_scope.update_from_scope(self) - - if additional_scope is not None: - if callable(additional_scope): - additional_scope(final_scope) - else: - final_scope.update_from_scope(additional_scope) - - elif additional_scope_kwargs: - final_scope.update_from_kwargs(**additional_scope_kwargs) - - return final_scope - - @classmethod - def get_client(cls) -> "sentry_sdk.client.BaseClient": - """ - .. versionadded:: 2.0.0 - - Returns the currently used :py:class:`sentry_sdk.Client`. - This checks the current scope, the isolation scope and the global scope for a client. - If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. - """ - current_scope = _current_scope.get() - try: - client = current_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - isolation_scope = _isolation_scope.get() - try: - client = isolation_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - try: - client = _global_scope.client # type: ignore - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - return NonRecordingClient() - - def set_client( - self, client: "Optional[sentry_sdk.client.BaseClient]" = None - ) -> None: - """ - .. versionadded:: 2.0.0 - - Sets the client for this scope. - - :param client: The client to use in this scope. - If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. - - """ - if client is not None: - self.client = client - # We need a client to set the initial global attributes on the global - # scope since they mostly come from client options, so populate them - # as soon as a client is set - sentry_sdk.get_global_scope().set_global_attributes() - else: - self.client = NonRecordingClient() - - def fork(self) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns a fork of this scope. - """ - forked_scope = copy(self) - return forked_scope - - def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": - """ - Load Sentry trace id and baggage from environment variables. - Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". - """ - incoming_trace_information: "Optional[Dict[str, str]]" = None - - sentry_use_environment = ( - os.environ.get("SENTRY_USE_ENVIRONMENT") or "" - ).lower() - use_environment = sentry_use_environment not in FALSE_VALUES - if use_environment: - incoming_trace_information = {} - - if os.environ.get("SENTRY_TRACE"): - incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( - os.environ.get("SENTRY_TRACE") or "" - ) - - if os.environ.get("SENTRY_BAGGAGE"): - incoming_trace_information[BAGGAGE_HEADER_NAME] = ( - os.environ.get("SENTRY_BAGGAGE") or "" - ) - - return incoming_trace_information or None - - def set_new_propagation_context(self) -> None: - """ - Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. - """ - self._propagation_context = PropagationContext() - - def generate_propagation_context( - self, incoming_data: "Optional[Dict[str, str]]" = None - ) -> None: - """ - Makes sure the propagation context is set on the scope. - If there is `incoming_data` overwrite existing propagation context. - If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. - """ - if incoming_data is not None: - self._propagation_context = PropagationContext.from_incoming_data( - incoming_data - ) - - # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring - if self._type != ScopeType.CURRENT: - if self._propagation_context is None: - self.set_new_propagation_context() - - def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": - """ - Returns the Dynamic Sampling Context from the Propagation Context. - If not existing, creates a new one. - - Deprecated: Logic moved to PropagationContext, don't use directly. - """ - if self._propagation_context is None: - return None - - return self._propagation_context.dynamic_sampling_context - - def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - """ - Returns the Sentry "sentry-trace" header (aka the traceparent) from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().to_traceparent() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return traceparent from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_traceparent() - elif not span_streaming and self.span is not None: - return self.span._to_traceparent() - - # else return traceparent from the propagation context - return self.get_active_propagation_context().to_traceparent() - - def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": - """ - Returns the Sentry "baggage" header containing trace information from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().get_baggage() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return baggage from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_baggage() - elif not span_streaming and self.span is not None: - return self.span._to_baggage() - - # else return baggage from the propagation context - return self.get_active_propagation_context().get_baggage() - - def get_trace_context(self) -> "Dict[str, Any]": - """ - Returns the Sentry "trace" context from the Propagation Context. - """ - if ( - has_tracing_enabled(self.get_client().options) - and self._span is not None - and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) - ): - return self._span._get_trace_context() - - # if we are tracing externally (otel), those values take precedence - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - trace_id, span_id = external_propagation_context - return {"trace_id": trace_id, "span_id": span_id} - - propagation_context = self.get_active_propagation_context() - - return { - "trace_id": propagation_context.trace_id, - "span_id": propagation_context.span_id, - "parent_span_id": propagation_context.parent_span_id, - "dynamic_sampling_context": propagation_context.dynamic_sampling_context, - } - - def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: - """ - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - span = kwargs.pop("span", None) - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - meta = "" - - for name, content in self.iter_trace_propagation_headers(): - meta += f'' - - return meta - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. - Deprecated: use PropagationContext.iter_headers instead. - """ - if self._propagation_context is not None: - yield from self._propagation_context.iter_headers() - - def iter_trace_propagation_headers( - self, *args: "Any", **kwargs: "Any" - ) -> "Generator[Tuple[str, str], None, None]": - """ - Return HTTP headers which allow propagation of trace data. - - If a span is given, the trace data will taken from the span. - If no span is given, the trace data is taken from the scope. - """ - client = self.get_client() - if not client.options.get("propagate_traces"): - warnings.warn( - "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", - DeprecationWarning, - stacklevel=2, - ) - return - - span = kwargs.pop("span", None) - if not span: - span_streaming = has_span_streaming_enabled(client.options) - span = self.streamed_span if span_streaming else self.span - - if ( - has_tracing_enabled(client.options) - and span is not None - and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) - ): - for header in span._iter_headers(): - yield header - elif has_external_propagation_context(): - # when we have an external_propagation_context (otlp) - # we leave outgoing propagation to the propagator - return - else: - for header in self.get_active_propagation_context().iter_headers(): - yield header - - def get_active_propagation_context(self) -> "PropagationContext": - if self._propagation_context is not None: - return self._propagation_context - - current_scope = self.get_current_scope() - if current_scope._propagation_context is not None: - return current_scope._propagation_context - - isolation_scope = self.get_isolation_scope() - # should actually never happen, but just in case someone calls scope.clear - if isolation_scope._propagation_context is None: - isolation_scope._propagation_context = PropagationContext() - return isolation_scope._propagation_context - - @classmethod - def set_custom_sampling_context( - cls, custom_sampling_context: "dict[str, Any]" - ) -> None: - cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( - custom_sampling_context - ) - - def clear(self) -> None: - """Clears the entire scope.""" - self._level: "Optional[LogLevelStr]" = None - self._fingerprint: "Optional[List[str]]" = None - self._transaction: "Optional[str]" = None - self._transaction_info: "dict[str, str]" = {} - self._user: "Optional[Dict[str, Any]]" = None - - self._tags: "Dict[str, Any]" = {} - self._contexts: "Dict[str, Dict[str, Any]]" = {} - self._extras: "dict[str, Any]" = {} - self._attachments: "List[Attachment]" = [] - - self.clear_breadcrumbs() - self._should_capture: bool = True - - self._span: "Optional[Union[Span, StreamedSpan]]" = None - self._session: "Optional[Session]" = None - self._force_auto_session_tracking: "Optional[bool]" = None - - self._profile: "Optional[Profile]" = None - - self._propagation_context = None - - # self._last_event_id is only applicable to isolation scopes - self._last_event_id: "Optional[str]" = None - self._flags: "Optional[FlagBuffer]" = None - - self._attributes: "Attributes" = {} - - self._gen_ai_conversation_id: "Optional[str]" = None - - @_attr_setter - def level(self, value: "LogLevelStr") -> None: - """ - When set this overrides the level. - - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. - - :param value: The level to set. - """ - logger.warning( - "Deprecated: use .set_level() instead. This will be removed in the future." - ) - - self._level = value - - def set_level(self, value: "LogLevelStr") -> None: - """ - Sets the level for the scope. - - :param value: The level to set. - """ - self._level = value - - @_attr_setter - def fingerprint(self, value: "Optional[List[str]]") -> None: - """When set this overrides the default fingerprint.""" - self._fingerprint = value - - @property - def transaction(self) -> "Any": - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" - - # there is no span/transaction on the scope - if self._span is None: - return None - - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - # there is an orphan span on the scope - if self._span.containing_transaction is None: - return None - - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value: "Any") -> None: - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span: - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - if self._span.containing_transaction: - self._span.containing_transaction.name = value - - def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: - """Set the transaction name and optionally the transaction source.""" - self._transaction = name - if self._span: - if isinstance(self._span, NoOpStreamedSpan): - return - - elif isinstance(self._span, StreamedSpan): - self._span._segment.name = name - if source: - self._span._segment.set_attribute( - "sentry.span.source", getattr(source, "value", source) - ) - - elif self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source - - if source: - self._transaction_info["source"] = source - - @_attr_setter - def user(self, value: "Optional[Dict[str, Any]]") -> None: - """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" - warnings.warn( - "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", - DeprecationWarning, - stacklevel=2, - ) - self.set_user(value) - - def set_user(self, value: "Optional[Dict[str, Any]]") -> None: - """Sets a user for the scope.""" - self._user = value - - session = self.get_isolation_scope()._session - if session is not None: - session.update(user=value) - - @property - def span(self) -> "Optional[Span]": - """Get/set current tracing span or transaction.""" - return self._span if isinstance(self._span, Span) else None - - @span.setter - def span(self, span: "Optional[Span]") -> None: - self._span = span - # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source - - @property - def streamed_span(self) -> "Optional[StreamedSpan]": - """Get/set current tracing span.""" - return self._span if isinstance(self._span, StreamedSpan) else None - - @streamed_span.setter - def streamed_span(self, span: "Optional[StreamedSpan]") -> None: - self._span = span - - # Also set _transaction and _transaction_info in streaming mode as this - # is used for populating events and linking them to segments - if type(span) is StreamedSpan and span._is_segment(): - self._transaction = span.name - if span._attributes.get("sentry.span.source"): - self._transaction_info["source"] = str( - span._attributes["sentry.span.source"] - ) - - @property - def profile(self) -> "Optional[Profile]": - return self._profile - - @profile.setter - def profile(self, profile: "Optional[Profile]") -> None: - self._profile = profile - - def set_tag(self, key: str, value: "Any") -> None: - """ - Sets a tag for a key to a specific value. - - :param key: Key of the tag to set. - - :param value: Value of the tag to set. - """ - self._tags[key] = value - - def set_tags(self, tags: "Mapping[str, object]") -> None: - """Sets multiple tags at once. - - This method updates multiple tags at once. The tags are passed as a dictionary - or other mapping type. - - Calling this method is equivalent to calling `set_tag` on each key-value pair - in the mapping. If a tag key already exists in the scope, its value will be - updated. If the tag key does not exist in the scope, the key-value pair will - be added to the scope. - - This method only modifies tag keys in the `tags` mapping passed to the method. - `scope.set_tags({})` is, therefore, a no-op. - - :param tags: A mapping of tag keys to tag values to set. - """ - self._tags.update(tags) - - def remove_tag(self, key: str) -> None: - """ - Removes a specific tag. - - :param key: Key of the tag to remove. - """ - self._tags.pop(key, None) - - def set_context( - self, - key: str, - value: "Dict[str, Any]", - ) -> None: - """ - Binds a context at a certain key to a specific value. - """ - self._contexts[key] = value - - def remove_context( - self, - key: str, - ) -> None: - """Removes a context.""" - self._contexts.pop(key, None) - - def set_extra( - self, - key: str, - value: "Any", - ) -> None: - """Sets an extra key to a specific value.""" - self._extras[key] = value - - def remove_extra( - self, - key: str, - ) -> None: - """Removes a specific extra key.""" - self._extras.pop(key, None) - - def set_conversation_id(self, conversation_id: str) -> None: - """ - Sets the conversation ID for gen_ai spans. - - :param conversation_id: The conversation ID to set. - """ - self._gen_ai_conversation_id = conversation_id - - def get_conversation_id(self) -> "Optional[str]": - """ - Gets the conversation ID for gen_ai spans. - - :returns: The conversation ID, or None if not set. - """ - return self._gen_ai_conversation_id - - def remove_conversation_id(self) -> None: - """Removes the conversation ID.""" - self._gen_ai_conversation_id = None - - def clear_breadcrumbs(self) -> None: - """Clears breadcrumb buffer.""" - self._breadcrumbs: "Deque[Breadcrumb]" = deque() - self._n_breadcrumbs_truncated = 0 - - def add_attachment( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - """Adds an attachment to future events sent from this scope. - - The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. - """ - self._attachments.append( - Attachment( - bytes=bytes, - path=path, - filename=filename, - content_type=content_type, - add_to_transactions=add_to_transactions, - ) - ) - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - client = self.get_client() - - if not client.is_active(): - logger.info("Dropped breadcrumb because no client bound") - return - - before_breadcrumb = client.options.get("before_breadcrumb") - max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) - - crumb = dict(crumb or ()) - crumb.update(kwargs) - if not crumb: - return - - hint = dict(hint or ()) - - if crumb.get("timestamp") is None: - crumb["timestamp"] = datetime.now(timezone.utc) - if crumb.get("type") is None: - crumb["type"] = "default" - - if before_breadcrumb is not None: - new_crumb = before_breadcrumb(crumb, hint) - else: - new_crumb = crumb - - if new_crumb is not None: - self._breadcrumbs.append(new_crumb) - else: - logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) - - while len(self._breadcrumbs) > max_breadcrumbs: - self._breadcrumbs.popleft() - self._n_breadcrumbs_truncated += 1 - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - try_autostart_continuous_profiler() - - custom_sampling_context = custom_sampling_context or {} - - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs: "TransactionKwargs" = kwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - # update the sample rate in the dsc - if transaction.sample_rate is not None: - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction._baggage: - transaction._baggage.sentry_items["sample_rate"] = str( - transaction.sample_rate - ) - - if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - - transaction._continuous_profile = try_profile_lifecycle_trace_start() - - # Typically, the profiler is set when the transaction is created. But when - # using the auto lifecycle, the profiler isn't running when the first - # transaction is started. So make sure we update the profiler id on it. - if transaction._continuous_profile is not None: - transaction.set_profiler_id(get_profiler_id()) - - # we don't bother to keep spans if we already know we're not going to - # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) - - return transaction - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - warnings.warn( - "Scope.start_span is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return NoOpSpan() - - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - # get current span or transaction - span = self.span or self.get_isolation_scope().span - if isinstance(span, StreamedSpan): - # make mypy happy - return NoOpSpan() - - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - kwargs["trace_id"] = propagation_context.trace_id - - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) - - return span - - def start_streamed_span( - self, - name: str, - attributes: "Optional[Attributes]", - parent_span: "Optional[StreamedSpan]", - active: bool, - ) -> "StreamedSpan": - # TODO: rename to start_span once we drop the old API - if isinstance(parent_span, NoOpStreamedSpan): - # parent_span is only set if the user explicitly set it - logger.debug( - "Ignored parent span provided. Span will be parented to the " - "currently active span instead." - ) - - if parent_span is _DEFAULT_PARENT_SPAN or isinstance( - parent_span, NoOpStreamedSpan - ): - parent_span = self.streamed_span - - # If no eligible parent_span was provided and there is no currently - # active span, this is a segment - if parent_span is None: - propagation_context = self.get_active_propagation_context() - - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - scope=self, - unsampled_reason="ignored", - ) - - sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( - name, - attributes, - self, - ) - - if sample_rate is not None: - self._update_sample_rate(sample_rate) - - if sampled is False: - return NoOpStreamedSpan( - scope=self, - unsampled_reason=outcome, - ) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=None, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - parent_sampled=propagation_context.parent_sampled, - baggage=propagation_context.baggage, - sample_rand=sample_rand, - sample_rate=sample_rate, - ) - - # This is a child span; take propagation context from the parent span - with new_scope(): - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - unsampled_reason="ignored", - ) - - if isinstance(parent_span, NoOpStreamedSpan): - return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=parent_span._segment, - trace_id=parent_span.trace_id, - parent_span_id=parent_span.span_id, - parent_sampled=parent_span.sampled, - ) - - def _update_sample_rate(self, sample_rate: float) -> None: - # If we had to adjust the sample rate when setting the sampling decision - # for a span, it needs to be updated in the propagation context too - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(sample_rate) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", - ) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - self.generate_propagation_context(environ_or_headers) - - # generate_propagation_context ensures that the propagation_context is not None. - propagation_context = cast(PropagationContext, self._propagation_context) - - optional_kwargs = {} - if name: - optional_kwargs["name"] = name - if source: - optional_kwargs["source"] = source - - return Transaction( - op=op, - origin=origin, - baggage=propagation_context.baggage, - parent_sampled=propagation_context.parent_sampled, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - same_process_as_parent=False, - **optional_kwargs, - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures an event. - - Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - scope = self._merge_scopes(scope, scope_kwargs) - - event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) - - if event_id is not None and event.get("type") != "transaction": - self.get_isolation_scope()._last_event_id = event_id - - return event_id - - def _capture_log(self, log: "Optional[Log]") -> None: - if log is None: - return - - client = self.get_client() - if not has_logs_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" - ) - - client._capture_log(log, scope=merged_scope) - - def _capture_metric(self, metric: "Optional[Metric]") -> None: - if metric is None: - return - - client = self.get_client() - if not has_metrics_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" - ) - - client._capture_metric(metric, scope=merged_scope) - - def _capture_span(self, span: "Optional[StreamedSpan]") -> None: - if span is None: - return - - client = self.get_client() - if not has_span_streaming_enabled(client.options): - return - - merged_scope = self._merge_scopes() - client._capture_span(span, scope=merged_scope) - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures a message. - - :param message: The string to send as the message. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if level is None: - level = "info" - - event: "Event" = { - "message": message, - "level": level, - } - - return self.capture_event(event, scope=scope, **scope_kwargs) - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """Captures an exception. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if error is not None: - exc_info = exc_info_from_error(error) - else: - exc_info = sys.exc_info() - - event, hint = event_from_exception( - exc_info, client_options=self.get_client().options - ) - - try: - return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) - except Exception: - capture_internal_exception(sys.exc_info()) - - return None - - def start_session(self, *args: "Any", **kwargs: "Any") -> None: - """Starts a new session.""" - session_mode = kwargs.pop("session_mode", "application") - - self.end_session() - - client = self.get_client() - self._session = Session( - release=client.options.get("release"), - environment=client.options.get("environment"), - user=self._user, - session_mode=session_mode, - ) - - def end_session(self, *args: "Any", **kwargs: "Any") -> None: - """Ends the current session if there is one.""" - session = self._session - self._session = None - - if session is not None: - session.close() - self.get_client().capture_session(session) - - def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: - """Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - self.end_session() - self._force_auto_session_tracking = False - - def resume_auto_session_tracking(self) -> None: - """Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - self._force_auto_session_tracking = None - - def add_event_processor( - self, - func: "EventProcessor", - ) -> None: - """Register a scope local event processor on the scope. - - :param func: This function behaves like `before_send.` - """ - if len(self._event_processors) > 20: - logger.warning( - "Too many event processors on scope! Clearing list to free up some memory: %r", - self._event_processors, - ) - del self._event_processors[:] - - self._event_processors.append(func) - - def add_error_processor( - self, - func: "ErrorProcessor", - cls: "Optional[Type[BaseException]]" = None, - ) -> None: - """Register a scope local error processor on the scope. - - :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. - - :param cls: Optionally, only process exceptions of this type. - """ - if cls is not None: - cls_ = cls # For mypy. - real_func = func - - def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": - try: - is_inst = isinstance(exc_info[1], cls_) - except Exception: - is_inst = False - if is_inst: - return real_func(event, exc_info) - return event - - self._error_processors.append(func) - - def _apply_level_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._level is not None: - event["level"] = self._level - - def _apply_breadcrumbs_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - event.setdefault("breadcrumbs", {}) - - # This check is just for mypy - - if not isinstance(event["breadcrumbs"], AnnotatedValue): - event["breadcrumbs"].setdefault("values", []) - event["breadcrumbs"]["values"].extend(self._breadcrumbs) - - # Attempt to sort timestamps - try: - if not isinstance(event["breadcrumbs"], AnnotatedValue): - for crumb in event["breadcrumbs"]["values"]: - if isinstance(crumb["timestamp"], str): - crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) - - event["breadcrumbs"]["values"].sort( - key=lambda crumb: crumb["timestamp"] - ) - except Exception as err: - logger.debug("Error when sorting breadcrumbs", exc_info=err) - pass - - def _apply_user_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("user") is None and self._user is not None: - event["user"] = self._user - - def _apply_transaction_name_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction") is None and self._transaction is not None: - event["transaction"] = self._transaction - - def _apply_transaction_info_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction_info") is None and self._transaction_info is not None: - event["transaction_info"] = self._transaction_info - - def _apply_fingerprint_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("fingerprint") is None and self._fingerprint is not None: - event["fingerprint"] = self._fingerprint - - def _apply_extra_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._extras: - event.setdefault("extra", {}).update(self._extras) - - def _apply_tags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._tags: - event.setdefault("tags", {}).update(self._tags) - - def _apply_contexts_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._contexts: - event.setdefault("contexts", {}).update(self._contexts) - - contexts = event.setdefault("contexts", {}) - - # Add "trace" context - if contexts.get("trace") is None: - contexts["trace"] = self.get_trace_context() - - def _apply_flags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - flags = self.flags.get() - if len(flags) > 0: - event.setdefault("contexts", {}).setdefault("flags", {}).update( - {"values": flags} - ) - - def _apply_scope_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - # TODO: turn Logs, Metrics into actual classes - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - for attribute, value in self._attributes.items(): - if attribute not in attributes: - attributes[attribute] = value - - def _apply_user_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - if not should_send_default_pii() or self._user is None: - return - - for attribute_name, user_attribute in ( - ("user.id", "id"), - ("user.name", "username"), - ("user.email", "email"), - ("user.ip_address", "ip_address"), - ): - if ( - user_attribute in self._user - and attribute_name not in attributes - and self._user[user_attribute] is not None - ): - attributes[attribute_name] = self._user[user_attribute] - - def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": - logger.info("%s (%s) dropped event", ty, cause) - return None - - def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the error processors on the event and returns the modified event. - """ - exc_info = hint.get("exc_info") - if exc_info is not None: - error_processors = chain( - self.get_global_scope()._error_processors, - self.get_isolation_scope()._error_processors, - self.get_current_scope()._error_processors, - ) - - for error_processor in error_processors: - new_event = error_processor(event, exc_info) - if new_event is None: - return self._drop(error_processor, "error processor") - - event = new_event - - return event - - def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the event processors on the event and returns the modified event. - """ - ty = event.get("type") - is_check_in = ty == "check_in" - - if not is_check_in: - # Get scopes without creating them to prevent infinite recursion - isolation_scope = _isolation_scope.get() - current_scope = _current_scope.get() - - event_processors = chain( - global_event_processors, - _global_scope and _global_scope._event_processors or [], - isolation_scope and isolation_scope._event_processors or [], - current_scope and current_scope._event_processors or [], - ) - - for event_processor in event_processors: - new_event = event - with capture_internal_exceptions(): - new_event = event_processor(event, hint) - if new_event is None: - return self._drop(event_processor, "event processor") - event = new_event - - return event - - @_disable_capture - def apply_to_event( - self, - event: "Event", - hint: "Hint", - options: "Optional[Dict[str, Any]]" = None, - ) -> "Optional[Event]": - """Applies the information contained on the scope to the given event.""" - ty = event.get("type") - is_transaction = ty == "transaction" - is_check_in = ty == "check_in" - - # put all attachments into the hint. This lets callbacks play around - # with attachments. We also later pull this out of the hint when we - # create the envelope. - attachments_to_send = hint.get("attachments") or [] - for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: - attachments_to_send.append(attachment) - hint["attachments"] = attachments_to_send - - self._apply_contexts_to_event(event, hint, options) - - if is_check_in: - # Check-ins only support the trace context, strip all others - event["contexts"] = { - "trace": event.setdefault("contexts", {}).get("trace", {}) - } - - if not is_check_in: - self._apply_level_to_event(event, hint, options) - self._apply_fingerprint_to_event(event, hint, options) - self._apply_user_to_event(event, hint, options) - self._apply_transaction_name_to_event(event, hint, options) - self._apply_transaction_info_to_event(event, hint, options) - self._apply_tags_to_event(event, hint, options) - self._apply_extra_to_event(event, hint, options) - - if not is_transaction and not is_check_in: - self._apply_breadcrumbs_to_event(event, hint, options) - self._apply_flags_to_event(event, hint, options) - - event = self.run_error_processors(event, hint) - if event is None: - return None - - event = self.run_event_processors(event, hint) - if event is None: - return None - - return event - - @_disable_capture - def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: - # Attributes-based events and telemetry go through here (logs, metrics, - # spansV2) - if not isinstance(telemetry, StreamedSpan): - trace_context = self.get_trace_context() - trace_id = trace_context.get("trace_id") - if telemetry.get("trace_id") is None and trace_id is not None: - telemetry["trace_id"] = trace_id - - # span_id should only be populated if there's an active span. We can't - # use the trace_context here because it synthesizes a span_id if there - # isn't one - if telemetry.get("span_id") is None: - if self._span is not None and not isinstance( - self._span, (NoOpStreamedSpan, NoOpSpan) - ): - telemetry["span_id"] = self._span.span_id - else: - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - _, span_id = external_propagation_context - if span_id is not None: - telemetry["span_id"] = span_id - - self._apply_scope_attributes_to_telemetry(telemetry) - self._apply_user_attributes_to_telemetry(telemetry) - - def update_from_scope(self, scope: "Scope") -> None: - """Update the scope with another scope's data.""" - if scope._level is not None: - self._level = scope._level - if scope._fingerprint is not None: - self._fingerprint = scope._fingerprint - if scope._transaction is not None: - self._transaction = scope._transaction - if scope._transaction_info is not None: - self._transaction_info.update(scope._transaction_info) - if scope._user is not None: - self._user = scope._user - if scope._tags: - self._tags.update(scope._tags) - if scope._contexts: - self._contexts.update(scope._contexts) - if scope._extras: - self._extras.update(scope._extras) - if scope._breadcrumbs: - self._breadcrumbs.extend(scope._breadcrumbs) - if scope._n_breadcrumbs_truncated: - self._n_breadcrumbs_truncated = ( - self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated - ) - if scope._gen_ai_original_message_count: - self._gen_ai_original_message_count.update( - scope._gen_ai_original_message_count - ) - if scope._gen_ai_conversation_id: - self._gen_ai_conversation_id = scope._gen_ai_conversation_id - if scope._span: - self._span = scope._span - if scope._attachments: - self._attachments.extend(scope._attachments) - if scope._profile: - self._profile = scope._profile - if scope._propagation_context: - self._propagation_context = scope._propagation_context - if scope._session: - self._session = scope._session - if scope._flags: - if not self._flags: - self._flags = deepcopy(scope._flags) - else: - for flag in scope._flags.get(): - self._flags.set(flag["flag"], flag["result"]) - if scope._attributes: - self._attributes.update(scope._attributes) - - def update_from_kwargs( - self, - user: "Optional[Any]" = None, - level: "Optional[LogLevelStr]" = None, - extras: "Optional[Dict[str, Any]]" = None, - contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, - tags: "Optional[Dict[str, str]]" = None, - fingerprint: "Optional[List[str]]" = None, - attributes: "Optional[Attributes]" = None, - ) -> None: - """Update the scope's attributes.""" - if level is not None: - self._level = level - if user is not None: - self._user = user - if extras is not None: - self._extras.update(extras) - if contexts is not None: - self._contexts.update(contexts) - if tags is not None: - self._tags.update(tags) - if fingerprint is not None: - self._fingerprint = fingerprint - if attributes is not None: - self._attributes.update(attributes) - - def __repr__(self) -> str: - return "<%s id=%s name=%s type=%s>" % ( - self.__class__.__name__, - hex(id(self)), - self._name, - self._type, - ) - - @property - def flags(self) -> "FlagBuffer": - if self._flags is None: - max_flags = ( - self.get_client().options["_experiments"].get("max_flags") - or DEFAULT_FLAG_CAPACITY - ) - self._flags = FlagBuffer(capacity=max_flags) - return self._flags - - def set_attribute(self, attribute: str, value: "AttributeValue") -> None: - """ - Set an attribute on the scope. - - Any attributes-based telemetry (logs, metrics) captured while this scope - is active will inherit attributes set on the scope. - """ - self._attributes[attribute] = format_attribute(value) - - def remove_attribute(self, attribute: str) -> None: - """Remove an attribute if set on the scope. No-op if there is no such attribute.""" - try: - del self._attributes[attribute] - except KeyError: - pass - - -@contextmanager -def new_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current scope and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.new_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - new_scope = current_scope.fork() - token = _current_scope.set(new_scope) - - try: - yield new_scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `scope` and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - Suppose the variable `scope` contains a `Scope` object, which is not currently - the active scope. - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.use_scope(scope): - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # set given scope as current scope - token = _current_scope.set(scope) - - try: - yield scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def isolation_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current isolation scope and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # fork isolation scope - isolation_scope = Scope.get_isolation_scope() - new_isolation_scope = isolation_scope.fork() - isolation_token = _isolation_scope.set(new_isolation_scope) - - try: - yield new_isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `isolation_scope` and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # set given scope as isolation scope - isolation_token = _isolation_scope.set(isolation_scope) - - try: - yield isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -def should_send_default_pii() -> bool: - """Shortcut for `Scope.get_client().should_send_default_pii()`.""" - return Scope.get_client().should_send_default_pii() - - -# Circular imports -from sentry_sdk.client import NonRecordingClient - -if TYPE_CHECKING: - import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py deleted file mode 100644 index 6794491325..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/scrubber.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING, Dict, List, cast - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - iter_event_frames, -) - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event - - -DEFAULT_DENYLIST = [ - # stolen from relay - "password", - "passwd", - "secret", - "api_key", - "apikey", - "auth", - "credentials", - "mysql_pwd", - "privatekey", - "private_key", - "token", - "session", - # django - "csrftoken", - "sessionid", - # wsgi - "x_csrftoken", - "x_forwarded_for", - "set_cookie", - "cookie", - "authorization", - "proxy-authorization", - "x_api_key", - # other common names used in the wild - "aiohttp_session", # aiohttp - "connect.sid", # Express - "csrf_token", # Pyramid - "csrf", # (this is a cookie name used in accepted answers on stack overflow) - "_csrf", # Express - "_csrf_token", # Bottle - "PHPSESSID", # PHP - "_session", # Sanic - "symfony", # Symfony - "user_session", # Vue - "_xsrf", # Tornado - "XSRF-TOKEN", # Angular, Laravel -] - -DEFAULT_PII_DENYLIST = [ - "x_forwarded_for", - "x_real_ip", - "ip_address", - "remote_addr", -] - - -class EventScrubber: - def __init__( - self, - denylist: "Optional[List[str]]" = None, - recursive: bool = False, - send_default_pii: bool = False, - pii_denylist: "Optional[List[str]]" = None, - ) -> None: - """ - A scrubber that goes through the event payload and removes sensitive data configured through denylists. - - :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. - :param recursive: Whether to scrub the event payload recursively, default False. - :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. - :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. - """ - self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist - - if not send_default_pii: - pii_denylist = ( - DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist - ) - self.denylist += pii_denylist - - self.denylist = [x.lower() for x in self.denylist] - self.recursive = recursive - - def scrub_list(self, lst: object) -> None: - """ - If a list is passed to this method, the method recursively searches the list and any - nested lists for any dictionaries. The method calls scrub_dict on all dictionaries - it finds. - If the parameter passed to this method is not a list, the method does nothing. - """ - if not isinstance(lst, list): - return - - for v in lst: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_dict(self, d: object) -> None: - """ - If a dictionary is passed to this method, the method scrubs the dictionary of any - sensitive data. The method calls itself recursively on any nested dictionaries ( - including dictionaries nested in lists) if self.recursive is True. - This method does nothing if the parameter passed to it is not a dictionary. - """ - if not isinstance(d, dict): - return - - for k, v in d.items(): - # The cast is needed because mypy is not smart enough to figure out that k must be a - # string after the isinstance check. - if isinstance(k, str) and k.lower() in self.denylist: - d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() - elif self.recursive: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_request(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "request" in event: - if "headers" in event["request"]: - self.scrub_dict(event["request"]["headers"]) - if "cookies" in event["request"]: - self.scrub_dict(event["request"]["cookies"]) - if "data" in event["request"]: - self.scrub_dict(event["request"]["data"]) - - def scrub_extra(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "extra" in event: - self.scrub_dict(event["extra"]) - - def scrub_user(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "user" in event: - user = event["user"] - if "ip_address" in self.denylist and isinstance(user, dict): - user.pop("ip_address", None) - self.scrub_dict(user) - - def scrub_breadcrumbs(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "breadcrumbs" in event: - if ( - not isinstance(event["breadcrumbs"], AnnotatedValue) - and "values" in event["breadcrumbs"] - ): - for value in event["breadcrumbs"]["values"]: - if "data" in value: - self.scrub_dict(value["data"]) - - def scrub_frames(self, event: "Event") -> None: - with capture_internal_exceptions(): - for frame in iter_event_frames(event): - if "vars" in frame: - self.scrub_dict(frame["vars"]) - - def scrub_spans(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "spans" in event: - for span in cast(List[Dict[str, object]], event["spans"]): - if "data" in span: - self.scrub_dict(span["data"]) - - def scrub_event(self, event: "Event") -> None: - self.scrub_request(event) - self.scrub_extra(event) - self.scrub_user(event) - self.scrub_breadcrumbs(event) - self.scrub_frames(event) - self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py deleted file mode 100644 index 6bf6f6e70b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/serializer.py +++ /dev/null @@ -1,418 +0,0 @@ -import math -import sys -from array import array -from collections.abc import Mapping -from datetime import datetime -from typing import TYPE_CHECKING - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exception, - disable_capture_event, - format_timestamp, - safe_repr, - strip_string, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union - - from sentry_sdk._types import NotImplementedType - - Span = Dict[str, Any] - - ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] - Segment = Union[str, int] - - -# Bytes are technically not strings in Python 3, but we can serialize them -serializable_str_types = (str, bytes, bytearray, memoryview) - - -# Maximum length of JSON-serialized event payloads that can be safely sent -# before the server may reject the event due to its size. This is not intended -# to reflect actual values defined server-side, but rather only be an upper -# bound for events sent by the SDK. -# -# Can be overwritten if wanting to send more bytes, e.g. with a custom server. -# When changing this, keep in mind that events may be a little bit larger than -# this value due to attached metadata, so keep the number conservative. -MAX_EVENT_BYTES = 10**6 - -# Maximum depth and breadth of databags. Excess data will be trimmed. If -# max_request_body_size is "always", request bodies won't be trimmed. -MAX_DATABAG_DEPTH = 5 -MAX_DATABAG_BREADTH = 10 -CYCLE_MARKER = "" - - -global_repr_processors: "List[ReprProcessor]" = [] - - -def add_global_repr_processor(processor: "ReprProcessor") -> None: - global_repr_processors.append(processor) - - -sequence_types: "List[type]" = [tuple, list, set, frozenset, array] - - -def add_repr_sequence_type(ty: type) -> None: - sequence_types.append(ty) - - -class Memo: - __slots__ = ("_ids", "_objs") - - def __init__(self) -> None: - self._ids: "Dict[int, Any]" = {} - self._objs: "List[Any]" = [] - - def memoize(self, obj: "Any") -> "ContextManager[bool]": - self._objs.append(obj) - return self - - def __enter__(self) -> bool: - obj = self._objs[-1] - if id(obj) in self._ids: - return True - else: - self._ids[id(obj)] = obj - return False - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> None: - self._ids.pop(id(self._objs.pop()), None) - - -class _Serializer: - """Holds the state of a single serialize() call.""" - - __slots__ = ( - "memo", - "path", - "meta_stack", - "keep_request_bodies", - "max_value_length", - "is_vars", - "custom_repr", - ) - - def __init__( - self, - keep_request_bodies: bool, - max_value_length: "Optional[int]", - is_vars: bool, - custom_repr: "Optional[Callable[..., Optional[str]]]", - ) -> None: - self.memo = Memo() - self.path: "List[Segment]" = [] - self.meta_stack: "List[Dict[str, Any]]" = [] - self.keep_request_bodies = keep_request_bodies - self.max_value_length = max_value_length - self.is_vars = is_vars - self.custom_repr = custom_repr - - def _safe_repr_wrapper(self, value: "Any") -> str: - try: - repr_value = None - if self.custom_repr is not None: - repr_value = self.custom_repr(value) - return repr_value or safe_repr(value) - except Exception: - return safe_repr(value) - - def _annotate(self, **meta: "Any") -> None: - while len(self.meta_stack) <= len(self.path): - try: - segment = self.path[len(self.meta_stack) - 1] - node = self.meta_stack[-1].setdefault(str(segment), {}) - except IndexError: - node = {} - - self.meta_stack.append(node) - - self.meta_stack[-1].setdefault("", {}).update(meta) - - def _is_databag(self) -> "Optional[bool]": - """ - A databag is any value that we need to trim. - True for stuff like vars, request bodies, breadcrumbs and extra. - - :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". - """ - try: - if self.is_vars: - return True - - is_request_body = self._is_request_body() - if is_request_body in (True, None): - return is_request_body - - p0 = self.path[0] - if p0 == "breadcrumbs" and self.path[1] == "values": - self.path[2] - return True - - if p0 == "extra": - return True - - except IndexError: - return None - - return False - - def _is_span_attribute(self) -> "Optional[bool]": - try: - if self.path[0] == "spans" and self.path[2] == "data": - return True - except IndexError: - return None - - return False - - def _is_request_body(self) -> "Optional[bool]": - try: - if self.path[0] == "request" and self.path[1] == "data": - return True - except IndexError: - return None - - return False - - def _serialize_node( - self, - obj: "Any", - is_databag: "Optional[bool]" = None, - is_request_body: "Optional[bool]" = None, - should_repr_strings: "Optional[bool]" = None, - segment: "Optional[Segment]" = None, - remaining_breadth: "Optional[Union[int, float]]" = None, - remaining_depth: "Optional[Union[int, float]]" = None, - ) -> "Any": - if segment is not None: - self.path.append(segment) - - try: - with self.memo.memoize(obj) as result: - if result: - return CYCLE_MARKER - - return self._serialize_node_impl( - obj, - is_databag=is_databag, - is_request_body=is_request_body, - should_repr_strings=should_repr_strings, - remaining_depth=remaining_depth, - remaining_breadth=remaining_breadth, - ) - except BaseException: - capture_internal_exception(sys.exc_info()) - - if is_databag: - return "" - - return None - finally: - if segment is not None: - self.path.pop() - del self.meta_stack[len(self.path) + 1 :] - - def _flatten_annotated(self, obj: "Any") -> "Any": - if isinstance(obj, AnnotatedValue): - self._annotate(**obj.metadata) - obj = obj.value - return obj - - def _serialize_node_impl( - self, - obj: "Any", - is_databag: "Optional[bool]", - is_request_body: "Optional[bool]", - should_repr_strings: "Optional[bool]", - remaining_depth: "Optional[Union[float, int]]", - remaining_breadth: "Optional[Union[float, int]]", - ) -> "Any": - if isinstance(obj, AnnotatedValue): - should_repr_strings = False - if should_repr_strings is None: - should_repr_strings = self.is_vars - - if is_databag is None: - is_databag = self._is_databag() - - if is_request_body is None: - is_request_body = self._is_request_body() - - if is_databag: - if is_request_body and self.keep_request_bodies: - remaining_depth = float("inf") - remaining_breadth = float("inf") - else: - if remaining_depth is None: - remaining_depth = MAX_DATABAG_DEPTH - if remaining_breadth is None: - remaining_breadth = MAX_DATABAG_BREADTH - - obj = self._flatten_annotated(obj) - - if remaining_depth is not None and remaining_depth <= 0: - self._annotate(rem=[["!limit", "x"]]) - if is_databag: - return self._flatten_annotated( - strip_string( - self._safe_repr_wrapper(obj), max_length=self.max_value_length - ) - ) - return None - - is_span_attribute = self._is_span_attribute() - if (is_databag or is_span_attribute) and global_repr_processors: - hints = {"memo": self.memo, "remaining_depth": remaining_depth} - for processor in global_repr_processors: - result = processor(obj, hints) - if result is not NotImplemented: - return self._flatten_annotated(result) - - sentry_repr = getattr(type(obj), "__sentry_repr__", None) - - if obj is None or isinstance(obj, (bool, int, float)): - if should_repr_strings or ( - isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) - ): - return self._safe_repr_wrapper(obj) - else: - return obj - - elif callable(sentry_repr): - return sentry_repr(obj) - - elif isinstance(obj, datetime): - return ( - str(format_timestamp(obj)) - if not should_repr_strings - else self._safe_repr_wrapper(obj) - ) - - elif isinstance(obj, Mapping): - # Create temporary copy here to avoid calling too much code that - # might mutate our dictionary while we're still iterating over it. - obj = dict(obj.items()) - - rv_dict: "Dict[str, Any]" = {} - i = 0 - - for k, v in obj.items(): - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) - break - - str_k = str(k) - v = self._serialize_node( - v, - segment=str_k, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - rv_dict[str_k] = v - i += 1 - - return rv_dict - - elif not isinstance(obj, serializable_str_types) and isinstance( - obj, tuple(sequence_types) - ): - rv_list = [] - - for i, v in enumerate(obj): # type: ignore[arg-type] - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) # type: ignore[arg-type] - break - - rv_list.append( - self._serialize_node( - v, - segment=i, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - ) - - return rv_list - - if should_repr_strings: - obj = self._safe_repr_wrapper(obj) - else: - if isinstance(obj, bytes) or isinstance(obj, bytearray): - obj = obj.decode("utf-8", "replace") - - if not isinstance(obj, str): - obj = self._safe_repr_wrapper(obj) - - is_span_description = ( - len(self.path) == 3 - and self.path[0] == "spans" - and self.path[-1] == "description" - ) - if is_span_description: - return obj - - return self._flatten_annotated( - strip_string(obj, max_length=self.max_value_length) - ) - - -def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": - """ - A very smart serializer that takes a dict and emits a json-friendly dict. - Currently used for serializing the final Event and also prematurely while fetching the stack - local variables for each frame in a stacktrace. - - It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. - The algorithm itself is a recursive graph walk down the data structures it encounters. - - It has the following responsibilities: - * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. - * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. - * Annotating the payload with the _meta field whenever trimming happens. - - :param max_request_body_size: If set to "always", will never trim request bodies. - :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. - :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. - :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. - - """ - serializer = _Serializer( - keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", - max_value_length=kwargs.pop("max_value_length", None), - is_vars=kwargs.pop("is_vars", False), - custom_repr=kwargs.pop("custom_repr", None), - ) - - disable_capture_event.set(True) - try: - serialized_event = serializer._serialize_node(event, **kwargs) - if ( - not serializer.is_vars - and serializer.meta_stack - and isinstance(serialized_event, dict) - ): - serialized_event["_meta"] = serializer.meta_stack[0] - - return serialized_event - finally: - disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py deleted file mode 100644 index 3ffd071bbc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/session.py +++ /dev/null @@ -1,165 +0,0 @@ -import uuid -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from sentry_sdk._types import SessionStatus - - -def _minute_trunc(ts: "datetime") -> "datetime": - return ts.replace(second=0, microsecond=0) - - -def _make_uuid( - val: "Union[str, uuid.UUID]", -) -> "uuid.UUID": - if isinstance(val, uuid.UUID): - return val - return uuid.UUID(val) - - -class Session: - def __init__( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - session_mode: str = "application", - ) -> None: - if sid is None: - sid = uuid.uuid4() - if started is None: - started = datetime.now(timezone.utc) - if status is None: - status = "ok" - self.status = status - self.did: "Optional[str]" = None - self.started = started - self.release: "Optional[str]" = None - self.environment: "Optional[str]" = None - self.duration: "Optional[float]" = None - self.user_agent: "Optional[str]" = None - self.ip_address: "Optional[str]" = None - self.session_mode: str = session_mode - self.errors = 0 - - self.update( - sid=sid, - did=did, - timestamp=timestamp, - duration=duration, - release=release, - environment=environment, - user_agent=user_agent, - ip_address=ip_address, - errors=errors, - user=user, - ) - - @property - def truncated_started(self) -> "datetime": - return _minute_trunc(self.started) - - def update( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - ) -> None: - # If a user is supplied we pull some data form it - if user: - if ip_address is None: - ip_address = user.get("ip_address") - if did is None: - did = user.get("id") or user.get("email") or user.get("username") - - if sid is not None: - self.sid = _make_uuid(sid) - if did is not None: - self.did = str(did) - if timestamp is None: - timestamp = datetime.now(timezone.utc) - self.timestamp = timestamp - if started is not None: - self.started = started - if duration is not None: - self.duration = duration - if release is not None: - self.release = release - if environment is not None: - self.environment = environment - if ip_address is not None: - self.ip_address = ip_address - if user_agent is not None: - self.user_agent = user_agent - if errors is not None: - self.errors = errors - - if status is not None: - self.status = status - - def close( - self, - status: "Optional[SessionStatus]" = None, - ) -> "Any": - if status is None and self.status == "ok": - status = "exited" - if status is not None: - self.update(status=status) - - def get_json_attrs( - self, - with_user_info: "Optional[bool]" = True, - ) -> "Any": - attrs = {} - if self.release is not None: - attrs["release"] = self.release - if self.environment is not None: - attrs["environment"] = self.environment - if with_user_info: - if self.ip_address is not None: - attrs["ip_address"] = self.ip_address - if self.user_agent is not None: - attrs["user_agent"] = self.user_agent - return attrs - - def to_json(self) -> "Any": - rv: "Dict[str, Any]" = { - "sid": str(self.sid), - "init": True, - "started": format_timestamp(self.started), - "timestamp": format_timestamp(self.timestamp), - "status": self.status, - } - if self.errors: - rv["errors"] = self.errors - if self.did is not None: - rv["did"] = self.did - if self.duration is not None: - rv["duration"] = self.duration - attrs = self.get_json_attrs() - if attrs: - rv["attrs"] = attrs - return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py deleted file mode 100644 index aabf874fcd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/sessions.py +++ /dev/null @@ -1,262 +0,0 @@ -import os -import warnings -from contextlib import contextmanager -from threading import Event, Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.envelope import Envelope -from sentry_sdk.session import Session -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, List, Optional, Union - - -def is_auto_session_tracking_enabled( - hub: "Optional[sentry_sdk.Hub]" = None, -) -> "Union[Any, bool, None]": - """DEPRECATED: Utility function to find out if session tracking is enabled.""" - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - - should_track = hub.scope._force_auto_session_tracking - - if should_track is None: - client_options = hub.client.options if hub.client else {} - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking( - hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: Use track_session instead - Starts and stops a session automatically around a block. - """ - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "Use track_session instead.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - should_track = is_auto_session_tracking_enabled(hub) - if should_track: - hub.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - hub.end_session() - - -def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: - """ - DEPRECATED: Utility function to find out if session tracking is enabled. - """ - - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - return _is_auto_session_tracking_enabled(scope) - - -def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: - """ - Utility function to find out if session tracking is enabled. - """ - - should_track = scope._force_auto_session_tracking - if should_track is None: - client_options = sentry_sdk.get_client().options - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking_scope( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: This function is a deprecated alias for track_session. - Starts and stops a session automatically around a block. - """ - - warnings.warn( - "This function is a deprecated alias for track_session and will be removed in the next major release.", - DeprecationWarning, - stacklevel=2, - ) - - with track_session(scope, session_mode=session_mode): - yield - - -@contextmanager -def track_session( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """ - Start a new session in the provided scope, assuming session tracking is enabled. - This is a no-op context manager if session tracking is not enabled. - """ - - should_track = _is_auto_session_tracking_enabled(scope) - if should_track: - scope.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - scope.end_session() - - -TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") -MAX_ENVELOPE_ITEMS = 100 - - -def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": - return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} - - -class SessionFlusher: - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - flush_interval: int = 60, - ) -> None: - self.capture_func = capture_func - self.flush_interval = flush_interval - self.pending_sessions: "List[Any]" = [] - self.pending_aggregates: "Dict[Any, Any]" = {} - self._thread: "Optional[Thread]" = None - self._thread_lock = Lock() - self._aggregate_lock = Lock() - self._thread_for_pid: "Optional[int]" = None - self.__shutdown_requested = Event() - - def flush(self) -> None: - pending_sessions = self.pending_sessions - self.pending_sessions = [] - - with self._aggregate_lock: - pending_aggregates = self.pending_aggregates - self.pending_aggregates = {} - - envelope = Envelope() - for session in pending_sessions: - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_session(session) - - for attrs, states in pending_aggregates.items(): - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_sessions(make_aggregate_envelope(states, attrs)) - - if len(envelope.items) > 0: - self.capture_func(envelope) - - def _ensure_running(self) -> None: - """ - Check that we have an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - running = True - while running: - running = not self.__shutdown_requested.wait(self.flush_interval) - self.flush() - - thread = Thread(target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.__shutdown_requested.set() - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def add_aggregate_session( - self, - session: "Session", - ) -> None: - # NOTE on `session.did`: - # the protocol can deal with buckets that have a distinct-id, however - # in practice we expect the python SDK to have an extremely high cardinality - # here, effectively making aggregation useless, therefore we do not - # aggregate per-did. - - # For this part we can get away with using the global interpreter lock - with self._aggregate_lock: - attrs = session.get_json_attrs(with_user_info=False) - primary_key = tuple(sorted(attrs.items())) - secondary_key = session.truncated_started # (, session.did) - states = self.pending_aggregates.setdefault(primary_key, {}) - state = states.setdefault(secondary_key, {}) - - if "started" not in state: - state["started"] = format_timestamp(session.truncated_started) - # if session.did is not None: - # state["did"] = session.did - if session.status == "crashed": - state["crashed"] = state.get("crashed", 0) + 1 - elif session.status == "abnormal": - state["abnormal"] = state.get("abnormal", 0) + 1 - elif session.errors > 0: - state["errored"] = state.get("errored", 0) + 1 - else: - state["exited"] = state.get("exited", 0) + 1 - - def add_session( - self, - session: "Session", - ) -> None: - if session.session_mode == "request": - self.add_aggregate_session(session) - else: - self.pending_sessions.append(session.to_json()) - self._ensure_running() - - def kill(self) -> None: - self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py deleted file mode 100644 index 2dcc86bc47..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/spotlight.py +++ /dev/null @@ -1,327 +0,0 @@ -import io -import logging -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from itertools import chain, product -from typing import TYPE_CHECKING - -import urllib3 - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Self - -from sentry_sdk.envelope import Envelope -from sentry_sdk.utils import ( - capture_internal_exceptions, - env_to_bool, -) -from sentry_sdk.utils import ( - logger as sentry_logger, -) - -logger = logging.getLogger("spotlight") - - -DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" -DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" - - -class SpotlightClient: - """ - A client for sending envelopes to Sentry Spotlight. - - Implements exponential backoff retry logic per the SDK spec: - - Logs error at least once when server is unreachable - - Does not log for every failed envelope - - Uses exponential backoff to avoid hammering an unavailable server - - Never blocks normal Sentry operation - """ - - # Exponential backoff settings - INITIAL_RETRY_DELAY = 1.0 # Start with 1 second - MAX_RETRY_DELAY = 60.0 # Max 60 seconds - - def __init__(self, url: str) -> None: - self.url = url - self.http = urllib3.PoolManager() - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time: float = 0.0 - - def capture_envelope(self, envelope: "Envelope") -> None: - # Check if we're in backoff period - skip sending to avoid blocking - if self._last_error_time > 0: - time_since_error = time.time() - self._last_error_time - if time_since_error < self._retry_delay: - # Still in backoff period, skip this envelope - return - - body = io.BytesIO() - envelope.serialize_into(body) - try: - req = self.http.request( - url=self.url, - body=body.getvalue(), - method="POST", - headers={ - "Content-Type": "application/x-sentry-envelope", - }, - ) - req.close() - # Success - reset backoff state - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time = 0.0 - except Exception as e: - self._last_error_time = time.time() - - # Increase backoff delay exponentially first, so logged value matches actual wait - self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) - - # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) - sentry_logger.warning( - "Failed to send envelope to Spotlight at %s: %s. " - "Will retry after %.1f seconds.", - self.url, - e, - self._retry_delay, - ) - - -try: - from django.conf import settings - from django.http import HttpRequest, HttpResponse, HttpResponseServerError - from django.utils.deprecation import MiddlewareMixin - - SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" - SPOTLIGHT_JS_SNIPPET_PATTERN = ( - "\n" - '\n' - ) - SPOTLIGHT_ERROR_PAGE_SNIPPET = ( - '\n' - '\n' - ) - CHARSET_PREFIX = "charset=" - BODY_TAG_NAME = "body" - BODY_CLOSE_TAG_POSSIBILITIES = tuple( - "".format("".join(chars)) - for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) - ) - - class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] - _spotlight_script: "Optional[str]" = None - _spotlight_url: "Optional[str]" = None - - def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: - super().__init__(get_response) - - import sentry_sdk.api - - self.sentry_sdk = sentry_sdk.api - - spotlight_client = self.sentry_sdk.get_client().spotlight - if spotlight_client is None: - sentry_logger.warning( - "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." - ) - return None - # Spotlight URL has a trailing `/stream` part at the end so split it off - self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") - - @property - def spotlight_script(self: "Self") -> "Optional[str]": - if self._spotlight_url is not None and self._spotlight_script is None: - try: - spotlight_js_url = urllib.parse.urljoin( - self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH - ) - req = urllib.request.Request( - spotlight_js_url, - method="HEAD", - ) - urllib.request.urlopen(req) - self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( - spotlight_url=self._spotlight_url, - spotlight_js_url=spotlight_js_url, - ) - except urllib.error.URLError as err: - sentry_logger.debug( - "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", - spotlight_js_url, - exc_info=err, - ) - - return self._spotlight_script - - def process_response( - self: "Self", _request: "HttpRequest", response: "HttpResponse" - ) -> "Optional[HttpResponse]": - content_type_header = tuple( - p.strip() - for p in response.headers.get("Content-Type", "").lower().split(";") - ) - content_type = content_type_header[0] - if len(content_type_header) > 1 and content_type_header[1].startswith( - CHARSET_PREFIX - ): - encoding = content_type_header[1][len(CHARSET_PREFIX) :] - else: - encoding = "utf-8" - - if ( - self.spotlight_script is not None - and not response.streaming - and content_type == "text/html" - ): - content_length = len(response.content) - injection = self.spotlight_script.encode(encoding) - injection_site = next( - ( - idx - for idx in ( - response.content.rfind(body_variant.encode(encoding)) - for body_variant in BODY_CLOSE_TAG_POSSIBILITIES - ) - if idx > -1 - ), - content_length, - ) - - # This approach works even when we don't have a `` tag - response.content = ( - response.content[:injection_site] - + injection - + response.content[injection_site:] - ) - - if response.has_header("Content-Length"): - response.headers["Content-Length"] = content_length + len(injection) - - return response - - def process_exception( - self: "Self", _request: "HttpRequest", exception: Exception - ) -> "Optional[HttpResponseServerError]": - if not settings.DEBUG or not self._spotlight_url: - return None - - try: - spotlight = ( - urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") - ) - except urllib.error.URLError: - return None - else: - event_id = self.sentry_sdk.capture_exception(exception) - return HttpResponseServerError( - spotlight.replace( - "", - SPOTLIGHT_ERROR_PAGE_SNIPPET.format( - spotlight_url=self._spotlight_url, event_id=event_id - ), - ) - ) - -except ImportError: - settings = None - - -def _resolve_spotlight_url( - spotlight_config: "Any", sentry_logger: "Any" -) -> "Optional[str]": - """ - Resolve the Spotlight URL based on config and environment variable. - - Implements precedence rules per the SDK spec: - https://develop.sentry.dev/sdk/expected-features/spotlight/ - - Returns the resolved URL string, or None if Spotlight should be disabled. - """ - spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") - - # Parse env var to determine if it's a boolean or URL - spotlight_from_env: "Optional[bool]" = None - spotlight_env_url: "Optional[str]" = None - if spotlight_env_value: - parsed = env_to_bool(spotlight_env_value, strict=True) - if parsed is None: - # It's a URL string - spotlight_from_env = True - spotlight_env_url = spotlight_env_value - else: - spotlight_from_env = parsed - - # Apply precedence rules per spec: - # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules - if spotlight_config is False: - # Config explicitly disables spotlight - warn if env var was set - if spotlight_from_env: - sentry_logger.warning( - "Spotlight is disabled via spotlight=False config option, " - "ignoring SENTRY_SPOTLIGHT environment variable." - ) - return None - elif spotlight_config is True: - # Config enables spotlight with boolean true - # If env var has URL, use env var URL per spec - if spotlight_env_url: - return spotlight_env_url - else: - return DEFAULT_SPOTLIGHT_URL - elif isinstance(spotlight_config, str): - # Config has URL string - use config URL, warn if env var differs - if spotlight_env_value and spotlight_env_value != spotlight_config: - sentry_logger.warning( - "Spotlight URL from config (%s) takes precedence over " - "SENTRY_SPOTLIGHT environment variable (%s).", - spotlight_config, - spotlight_env_value, - ) - return spotlight_config - elif spotlight_config is None: - # No config - use env var - if spotlight_env_url: - return spotlight_env_url - elif spotlight_from_env: - return DEFAULT_SPOTLIGHT_URL - # else: stays None (disabled) - - return None - - -def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": - url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) - - if url is None: - return None - - # Only set up logging handler when spotlight is actually enabled - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.INFO) - - # Update options with resolved URL for consistency - options["spotlight"] = url - - with capture_internal_exceptions(): - if ( - settings is not None - and settings.DEBUG - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) - ): - middleware = settings.MIDDLEWARE - if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: - settings.MIDDLEWARE = type(middleware)( - chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) - ) - logger.info("Enabled Spotlight integration for Django") - - client = SpotlightClient(url) - logger.info("Enabled Spotlight using sidecar at %s", url) - - return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py deleted file mode 100644 index 5ee7e8460b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/traces.py +++ /dev/null @@ -1,843 +0,0 @@ -""" -EXPERIMENTAL. Do not use in production. - -The API in this file is only meant to be used in span streaming mode. - -You can enable span streaming mode via -sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). -""" - -import sys -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - capture_internal_exceptions, - format_attribute, - get_current_thread_meta, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Iterator, - Optional, - ParamSpec, - TypeVar, - Union, - overload, - ) - - from sentry_sdk._types import Attributes, AttributeValue, SpanJSON - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - - P = ParamSpec("P") - R = TypeVar("R") - - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -class SpanStatus(str, Enum): - OK = "ok" - ERROR = "error" - - def __str__(self) -> str: - return self.value - - -_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) - - -# Segment source, see -# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource -class SegmentSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_SEGMENT_SOURCES = [ - SegmentSource.URL, -] - - -SOURCE_FOR_STYLE = { - "endpoint": SegmentSource.COMPONENT, - "function_name": SegmentSource.COMPONENT, - "handler_name": SegmentSource.COMPONENT, - "method_and_path_pattern": SegmentSource.ROUTE, - "path": SegmentSource.URL, - "route_name": SegmentSource.COMPONENT, - "route_pattern": SegmentSource.ROUTE, - "uri_template": SegmentSource.ROUTE, - "url": SegmentSource.ROUTE, -} - - -# Sentinel value for an unset parent_span to be able to distinguish it from -# a None set by the user -_DEFAULT_PARENT_SPAN = object() - - -def start_span( - name: str, - attributes: "Optional[Attributes]" = None, - parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] - active: bool = True, -) -> "StreamedSpan": - """ - Start a span. - - EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() - instead. - - The span's parent, unless provided explicitly via the `parent_span` argument, - will be the current active span, if any. If there is none, this span will - become the root of a new span tree. If you explicitly want this span to be - top-level without a parent, set `parent_span=None`. - - `start_span()` can either be used as context manager or you can use the span - object it returns and explicitly end it via `span.end()`. The following is - equivalent: - - ```python - import sentry_sdk - - with sentry_sdk.traces.start_span(name="My Span"): - # do something - - # The span automatically finishes once the `with` block is exited - ``` - - ```python - import sentry_sdk - - span = sentry_sdk.traces.start_span(name="My Span") - # do something - span.end() - ``` - - To continue a trace from another service, call - `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. - - :param name: The name to identify this span by. - :type name: str - - :param attributes: Key-value attributes to set on the span from the start. - These will also be accessible in the traces sampler. - :type attributes: "Optional[Attributes]" - - :param parent_span: A span instance that the new span should consider its - parent. If not provided, the parent will be set to the currently active - span, if any. If set to `None`, this span will become a new root-level - span. - :type parent_span: "Optional[StreamedSpan]" - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to `False`. - :type active: bool - - :return: The span that has been started. - :rtype: StreamedSpan - """ - from sentry_sdk.tracing_utils import has_span_streaming_enabled - - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "sentry_sdk.start_transaction() and sentry_sdk.start_span() " - "instead.", - stacklevel=2, - ) - return NoOpStreamedSpan() - - return sentry_sdk.get_current_scope().start_streamed_span( - name, attributes, parent_span, active - ) - - -def continue_trace(incoming: "dict[str, Any]") -> None: - """ - Continue a trace from headers or environment variables. - - EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. - - This function sets the propagation context on the scope. Any span started - in the updated scope will belong under the trace extracted from the - provided propagation headers or environment variables. - - continue_trace() doesn't start any spans on its own. Use the start_span() - API for that. - """ - # This is set both on the isolation and the current scope for compatibility - # reasons. Conceptually, it belongs on the isolation scope, and it also - # used to be set there in non-span-first mode. But in span first mode, we - # start spans on the current scope, regardless of type, like JS does, so we - # need to set the propagation context there. - sentry_sdk.get_isolation_scope().generate_propagation_context( - incoming, - ) - sentry_sdk.get_current_scope().generate_propagation_context( - incoming, - ) - - -def new_trace() -> None: - """ - Resets the propagation context, forcing a new trace. - - EXPERIMENTAL. - - This function sets the propagation context on the scope. Any span started - in the updated scope will start its own trace. - - new_trace() doesn't start any spans on its own. Use the start_span() API - for that. - """ - sentry_sdk.get_isolation_scope().set_new_propagation_context() - sentry_sdk.get_current_scope().set_new_propagation_context() - - -class StreamedSpan: - """ - A span holds timing information of a block of code. - - Spans can have multiple child spans, thus forming a span tree. - - This is the Span First span implementation that streams spans. The original - transaction-based span implementation lives in tracing.Span. - """ - - __slots__ = ( - "_name", - "_attributes", - "_active", - "_span_id", - "_trace_id", - "_parent_span_id", - "_segment", - "_parent_sampled", - "_start_timestamp", - "_start_timestamp_monotonic_ns", - "_end_timestamp", - "_status", - "_scope", - "_previous_span_on_scope", - "_baggage", - "_sample_rand", - "_sample_rate", - "_continuous_profile", - ) - - def __init__( - self, - *, - name: str, - attributes: "Optional[Attributes]" = None, - active: bool = True, - scope: "sentry_sdk.Scope", - segment: "Optional[StreamedSpan]" = None, - trace_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - sample_rate: "Optional[float]" = None, - sample_rand: "Optional[float]" = None, - ): - self._name: str = name - self._active: bool = active - self._attributes: "Attributes" = { - "sentry.origin": "manual", - "sentry.trace_lifecycle": "stream", - } - - if attributes: - for attribute, value in attributes.items(): - self.set_attribute(attribute, value) - - self._scope = scope - - self._segment = segment or self - - self._trace_id: "Optional[str]" = trace_id - self._parent_span_id = parent_span_id - self._parent_sampled = parent_sampled - self._baggage = baggage - self._sample_rand = sample_rand - self._sample_rate = sample_rate - - self._start_timestamp = datetime.now(timezone.utc) - self._end_timestamp: "Optional[datetime]" = None - - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - - self._span_id: "Optional[str]" = None - - self._status = SpanStatus.OK.value - - self._update_active_thread() - - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._start_profile() - self._set_profile_id(get_profiler_id()) - - self._set_segment_attributes() - - self._start() - - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__}(" - f"name={self._name}, " - f"trace_id={self.trace_id}, " - f"span_id={self.span_id}, " - f"parent_span_id={self._parent_span_id}, " - f"active={self._active})>" - ) - - def __enter__(self) -> "StreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore - return - - if value is not None and should_be_treated_as_error(ty, value): - self.status = SpanStatus.ERROR.value - - self._end() - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - """ - Finish this span and queue it for sending. - - :param end_timestamp: End timestamp to use instead of current time. - :type end_timestamp: "Optional[Union[float, datetime]]" - """ - self._end(end_timestamp) - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self.end(end_timestamp) - - def _start(self) -> None: - if self._active: - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore. - return - - # Stop the profiler - if self._is_segment() and self._continuous_profile is not None: - with capture_internal_exceptions(): - self._continuous_profile.stop() - - # Detach from scope - if self._active: - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - # Set attributes from the segment. These are set on span end on purpose - # so that we have the best chance to capture the segment's final name - # (since it might change during its lifetime) - self.set_attribute("sentry.segment.id", self._segment.span_id) - self.set_attribute("sentry.segment.name", self._segment.name) - - # Set the end timestamp - if end_timestamp is not None: - if isinstance(end_timestamp, (float, int)): - try: - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - except Exception: - pass - - if isinstance(end_timestamp, datetime): - self._end_timestamp = end_timestamp - else: - logger.debug( - "[Tracing] Failed to set end_timestamp. Using current time instead." - ) - - if self._end_timestamp is None: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self._end_timestamp = self._start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # Finally, queue the span for sending to Sentry - self._scope._capture_span(self) - - def get_attributes(self) -> "Attributes": - return self._attributes - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - self._attributes[key] = format_attribute(value) - - def set_attributes(self, attributes: "Attributes") -> None: - for key, value in attributes.items(): - self.set_attribute(key, value) - - def remove_attribute(self, key: str) -> None: - try: - del self._attributes[key] - except KeyError: - pass - - @property - def status(self) -> "str": - return self._status - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - if isinstance(status, Enum): - status = status.value - - if status not in _VALID_SPAN_STATUSES: - logger.debug( - f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' - ) - return - - self._status = status - - @property - def name(self) -> str: - return self._name - - @name.setter - def name(self, name: str) -> None: - self._name = name - - @property - def active(self) -> bool: - return self._active - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @property - def sampled(self) -> "Optional[bool]": - return True - - @property - def start_timestamp(self) -> "Optional[datetime]": - return self._start_timestamp - - @property - def end_timestamp(self) -> "Optional[datetime]": - return self._end_timestamp - - def _is_segment(self) -> bool: - return self._segment is self - - def _update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - - if thread_id is not None: - self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_attribute(SPANDATA.THREAD_NAME, thread_name) - - def _dynamic_sampling_context(self) -> "dict[str, str]": - return self._segment._get_baggage().dynamic_sampling_context() - - def _to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def _to_baggage(self) -> "Optional[Baggage]": - if self._segment: - return self._segment._get_baggage() - return None - - def _get_baggage(self) -> "Baggage": - """ - Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with - the segment. - - The first time a new baggage with Sentry items is made, it will be frozen. - """ - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_segment(self) - - return self._baggage - - def _iter_headers(self) -> "Iterator[tuple[str, str]]": - if not self._segment: - return - - yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() - - baggage = self._segment._get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def _get_trace_context(self) -> "dict[str, Any]": - # Even if spans themselves are not event-based anymore, we need this - # to populate trace context on events - context: "dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self._parent_span_id, - "dynamic_sampling_context": self._dynamic_sampling_context(), - } - - if "sentry.op" in self._attributes: - context["op"] = self._attributes["sentry.op"] - if "sentry.origin" in self._attributes: - context["origin"] = self._attributes["sentry.origin"] - - return context - - def _set_profile_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_attribute("sentry.profiler_id", profiler_id) - - def _start_profile(self) -> None: - if not self._is_segment(): - return - - try_autostart_continuous_profiler() - - self._continuous_profile = try_profile_lifecycle_trace_start() - - def _set_segment_attributes(self) -> None: - if not self._is_segment(): - return - - client = sentry_sdk.get_client() - - self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") - self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) - self.set_attribute( - SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) - ) - - if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: - self.set_attribute( - SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() - ) - - def _to_json(self) -> "SpanJSON": - res: "SpanJSON" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "name": self._name if self._name is not None else "", - "status": self._status, - "is_segment": self._is_segment(), - "start_timestamp": self._start_timestamp.timestamp(), - } - - if self._end_timestamp: - res["end_timestamp"] = self._end_timestamp.timestamp() - - if self._parent_span_id: - res["parent_span_id"] = self._parent_span_id - - res["attributes"] = {k: v for k, v in self._attributes.items()} - - return res - - -class NoOpStreamedSpan(StreamedSpan): - __slots__ = ( - "_finished", - "_unsampled_reason", - ) - - def __init__( - self, - unsampled_reason: "Optional[str]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - ) -> None: - self._scope = scope # type: ignore[assignment] - self._unsampled_reason = unsampled_reason - - self._finished = False - - self._start() - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}(sampled={self.sampled})>" - - def __enter__(self) -> "NoOpStreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self._end() - - def _start(self) -> None: - if self._scope is None: - return - - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._finished: - return - - if self._unsampled_reason is not None: - client = sentry_sdk.get_client() - if client.is_active() and client.transport: - logger.debug( - f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" - ) - client.transport.record_lost_event( - reason=self._unsampled_reason, - data_category="span", - quantity=1, - ) - - if self._scope and hasattr(self, "_previous_span_on_scope"): - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - self._finished = True - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - self._end() - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self._end() - - def get_attributes(self) -> "Attributes": - return {} - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - pass - - def set_attributes(self, attributes: "Attributes") -> None: - pass - - def remove_attribute(self, key: str) -> None: - pass - - def _is_segment(self) -> bool: - return self._scope is not None - - @property - def status(self) -> "str": - return SpanStatus.OK.value - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - pass - - @property - def name(self) -> str: - return "" - - @name.setter - def name(self, value: str) -> None: - pass - - @property - def active(self) -> bool: - return True - - @property - def span_id(self) -> str: - return "0000000000000000" - - @property - def trace_id(self) -> str: - return "00000000000000000000000000000000" - - @property - def sampled(self) -> "Optional[bool]": - return False - - @property - def start_timestamp(self) -> "Optional[datetime]": - return None - - @property - def end_timestamp(self) -> "Optional[datetime]": - return None - - -if TYPE_CHECKING: - - @overload - def trace( - func: "Callable[P, R]", - ) -> "Callable[P, R]": ... - - @overload - def trace( - func: None = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a span around a function call. - - EXPERIMENTAL. Use @sentry_sdk.trace instead. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to False. - :type active: bool - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import ( - create_streaming_span_decorator, - ) - - decorator = create_streaming_span_decorator( - name=name, - attributes=attributes, - active=active, - ) - - if func: - return decorator(func) - else: - return decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[StreamedSpan]": - """ - Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. - - This function will only return a non-`None` value when the streaming trace lifecycle is enabled. - To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.streamed_span - return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py deleted file mode 100644 index 1790e13fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing.py +++ /dev/null @@ -1,1475 +0,0 @@ -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE -from sentry_sdk.profiler.continuous_profiler import get_profiler_id -from sentry_sdk.utils import ( - capture_internal_exceptions, - get_current_thread_meta, - is_valid_sample_rate, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping - from typing import ( - Any, - Dict, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - overload, - ) - - from typing_extensions import TypedDict, Unpack - - P = ParamSpec("P") - R = TypeVar("R") - - from sentry_sdk._types import ( - Event, - MeasurementUnit, - MeasurementValue, - SamplingContext, - ) - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - from sentry_sdk.profiler.transaction_profiler import Profile - - class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - - op: str - """ - The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - """ - - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - - hub: "Optional[sentry_sdk.Hub]" - """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" - - status: str - """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - - containing_transaction: "Optional[Transaction]" - """The transaction that this span belongs to.""" - - start_timestamp: "Optional[Union[datetime, float]]" - """ - The timestamp when the span started. If omitted, the current time - will be used. - """ - - scope: "sentry_sdk.Scope" - """The scope to use for this span. If not provided, we use the current scope.""" - - origin: str - """ - The origin of the span. - See https://develop.sentry.dev/sdk/performance/trace-origin/ - Default "manual". - """ - - name: str - """A string describing what operation is being performed within the span/transaction.""" - - class TransactionKwargs(SpanKwargs, total=False): - source: str - """ - A string describing the source of the transaction name. This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. - Default "custom". - """ - - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - - ProfileContext = TypedDict( - "ProfileContext", - { - "profiler_id": str, - }, - ) - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -# Transaction source -# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations -class TransactionSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_TRANSACTION_SOURCES = [ - TransactionSource.URL, -] - -SOURCE_FOR_STYLE = { - "endpoint": TransactionSource.COMPONENT, - "function_name": TransactionSource.COMPONENT, - "handler_name": TransactionSource.COMPONENT, - "method_and_path_pattern": TransactionSource.ROUTE, - "path": TransactionSource.URL, - "route_name": TransactionSource.COMPONENT, - "route_pattern": TransactionSource.ROUTE, - "uri_template": TransactionSource.ROUTE, - "url": TransactionSource.ROUTE, -} - - -def get_span_status_from_http_code(http_status_code: int) -> str: - """ - Returns the Sentry status corresponding to the given HTTP status code. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - if http_status_code < 400: - return SPANSTATUS.OK - - elif 400 <= http_status_code < 500: - if http_status_code == 403: - return SPANSTATUS.PERMISSION_DENIED - elif http_status_code == 404: - return SPANSTATUS.NOT_FOUND - elif http_status_code == 429: - return SPANSTATUS.RESOURCE_EXHAUSTED - elif http_status_code == 413: - return SPANSTATUS.FAILED_PRECONDITION - elif http_status_code == 401: - return SPANSTATUS.UNAUTHENTICATED - elif http_status_code == 409: - return SPANSTATUS.ALREADY_EXISTS - else: - return SPANSTATUS.INVALID_ARGUMENT - - elif 500 <= http_status_code < 600: - if http_status_code == 504: - return SPANSTATUS.DEADLINE_EXCEEDED - elif http_status_code == 501: - return SPANSTATUS.UNIMPLEMENTED - elif http_status_code == 503: - return SPANSTATUS.UNAVAILABLE - else: - return SPANSTATUS.INTERNAL_ERROR - - return SPANSTATUS.UNKNOWN_ERROR - - -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans", "dropped_spans") - - def __init__(self, maxlen: int) -> None: - # FIXME: this is `maxlen - 1` only to preserve historical behavior - # enforced by tests. - # Either this should be changed to `maxlen` or the JS SDK implementation - # should be changed to match a consistent interpretation of what maxlen - # limits: either transaction+spans or only child spans. - self.maxlen = maxlen - 1 - self.spans: "List[Span]" = [] - self.dropped_spans: int = 0 - - def add(self, span: "Span") -> None: - if len(self.spans) > self.maxlen: - span._span_recorder = None - self.dropped_spans += 1 - else: - self.spans.append(span) - - -class Span: - """A span holds timing information of a block of code. - Spans can have multiple child spans thus forming a span tree. - - :param trace_id: The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - :param span_id: The span ID of this span. If omitted, a new span ID will be generated. - :param parent_span_id: The span ID of the parent span, if applicable. - :param same_process_as_parent: Whether this span is in the same process as the parent span. - :param sampled: Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - :param op: The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - :param description: A description of what operation is being performed within the span. - - .. deprecated:: 2.15.0 - Please use the `name` parameter, instead. - :param name: A string describing what operation is being performed within the span. - :param hub: The hub to use for this span. - - .. deprecated:: 2.0.0 - Please use the `scope` parameter, instead. - :param status: The span's status. Possible values are listed at - https://develop.sentry.dev/sdk/event-payloads/span/ - :param containing_transaction: The transaction that this span belongs to. - :param start_timestamp: The timestamp when the span started. If omitted, the current time - will be used. - :param scope: The scope to use for this span. If not provided, we use the current scope. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "same_process_as_parent", - "sampled", - "op", - "description", - "_measurements", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "hub", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - "_flags", - "_flags_capacity", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - same_process_as_parent: bool = True, - sampled: "Optional[bool]" = None, - op: "Optional[str]" = None, - description: "Optional[str]" = None, - hub: "Optional[sentry_sdk.Hub]" = None, # deprecated - status: "Optional[str]" = None, - containing_transaction: "Optional[Transaction]" = None, - start_timestamp: "Optional[Union[datetime, float]]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - origin: str = "manual", - name: "Optional[str]" = None, - ) -> None: - self._trace_id = trace_id - self._span_id = span_id - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = name or description - self.status = status - self.hub = hub # backwards compatibility - self.scope = scope - self.origin = origin - self._measurements: "Dict[str, MeasurementValue]" = {} - self._tags: "MutableMapping[str, str]" = {} - self._data: "Dict[str, Any]" = {} - self._containing_transaction = containing_transaction - self._flags: "Dict[str, bool]" = {} - self._flags_capacity = 10 - - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - - if start_timestamp is None: - start_timestamp = datetime.now(timezone.utc) - elif isinstance(start_timestamp, float): - start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) - self.start_timestamp = start_timestamp - try: - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp: "Optional[datetime]" = None - - self._span_recorder: "Optional[_SpanRecorder]" = None - - self.update_active_thread() - self.set_profiler_id(get_profiler_id()) - - # TODO this should really live on the Transaction class rather than the Span - # class - def init_span_recorder(self, maxlen: int) -> None: - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - def __repr__(self) -> str: - return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.op, - self.description, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.origin, - ) - ) - - def __enter__(self) -> "Span": - scope = self.scope or sentry_sdk.get_current_scope() - old_span = scope.span - scope.span = self - self._context_manager_state = (scope, old_span) - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - with capture_internal_exceptions(): - scope, old_span = self._context_manager_state - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self) -> "Optional[Transaction]": - """The ``Transaction`` that this span belongs to. - The ``Transaction`` is the root of the span tree, - so one could also think of this ``Transaction`` as the "root span".""" - - # this is a getter rather than a regular attribute so that transactions - # can return `self` here instead (as a way to prevent them circularly - # referencing themselves) - return self._containing_transaction - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a sub-span from the current span or transaction. - - Takes the same arguments as the initializer of :py:class:`Span`. The - trace id, sampling decision, transaction pointer, and span recorder are - inherited from the current span/transaction. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - kwargs.setdefault("sampled", self.sampled) - - child = Span( - trace_id=self.trace_id, - parent_span_id=self.span_id, - containing_transaction=self.containing_transaction, - **kwargs, - ) - - span_recorder = ( - self.containing_transaction and self.containing_transaction._span_recorder - ) - if span_recorder: - span_recorder.add(child) - - return child - - @classmethod - def continue_from_environ( - cls, - environ: "Mapping[str, str]", - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a Transaction with the given params, then add in data pulled from - the ``sentry-trace`` and ``baggage`` headers from the environ (if any) - before returning the Transaction. - - This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` - in that it assumes header names in the form ``HTTP_HEADER_NAME`` - - such as you would get from a WSGI/ASGI environ - - rather than the form ``header-name``. - - :param environ: The ASGI/WSGI environ to pull information from. - """ - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers: "Mapping[str, str]", - *, - _sample_rand: "Optional[str]" = None, - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a transaction with the given params (including any data pulled from - the ``sentry-trace`` and ``baggage`` headers). - - :param headers: The dictionary with the HTTP headers to pull information from. - :param _sample_rand: If provided, we override the sample_rand value from the - incoming headers with this value. (internal use only) - """ - logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header( - headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand - ) - kwargs.update({BAGGAGE_HEADER_NAME: baggage}) - - sentrytrace_kwargs = extract_sentrytrace_data( - headers.get(SENTRY_TRACE_HEADER_NAME) - ) - - if sentrytrace_kwargs is not None: - kwargs.update(sentrytrace_kwargs) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. - If the span's containing transaction doesn't yet have a ``baggage`` value, - this will cause one to be generated and stored. - """ - if not self.containing_transaction: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - return - - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.containing_transaction.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - @classmethod - def from_traceparent( - cls, - traceparent: "Optional[str]", - **kwargs: "Any", - ) -> "Optional[Transaction]": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def to_baggage(self) -> "Optional[Baggage]": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with this ``Span``, if any. (Taken from the root of the span tree.) - """ - if self.containing_transaction: - return self.containing_transaction.get_baggage() - return None - - def set_tag(self, key: str, value: "Any") -> None: - self._tags[key] = value - - def set_data(self, key: str, value: "Any") -> None: - self._data[key] = value - - def update_data(self, data: "Dict[str, Any]") -> None: - self._data.update(data) - - def set_flag(self, flag: str, result: bool) -> None: - if len(self._flags) < self._flags_capacity: - self._flags[flag] = result - - def set_status(self, value: str) -> None: - self.status = value - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_thread( - self, thread_id: "Optional[int]", thread_name: "Optional[str]" - ) -> None: - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) - - def set_profiler_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status: int) -> None: - self.set_tag( - "http.status_code", str(http_status) - ) # TODO-neel remove in major, we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self) -> bool: - return self.status == "ok" - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ - if self.timestamp is not None: - # This span is already finished, ignore. - return None - - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp - else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - except AttributeError: - self.timestamp = datetime.now(timezone.utc) - - scope = scope or sentry_sdk.get_current_scope() - - # Copy conversation_id from scope to span data if this is an AI span - conversation_id = scope.get_conversation_id() - if conversation_id: - has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data - is_ai_span_op = self.op is not None and ( - self.op.startswith("ai.") or self.op.startswith("gen_ai.") - ) - if has_ai_op or is_ai_span_op: - self.set_data("gen_ai.conversation.id", conversation_id) - - maybe_create_breadcrumbs_from_span(scope, self) - - return None - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the span.""" - - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "same_process_as_parent": self.same_process_as_parent, - "op": self.op, - "description": self.description, - "start_timestamp": self.start_timestamp, - "timestamp": self.timestamp, - "origin": self.origin, - } - - if self.status: - rv["status"] = self.status - # TODO-neel remove redundant tag in major - self._tags["status"] = self.status - - if len(self._measurements) > 0: - rv["measurements"] = self._measurements - - tags = self._tags - if tags: - rv["tags"] = tags - - data = {} - data.update(self._flags) - data.update(self._data) - if data: - rv["data"] = data - - return rv - - def get_trace_context(self) -> "Any": - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "op": self.op, - "description": self.description, - "origin": self.origin, - } - if self.status: - rv["status"] = self.status - - if self.containing_transaction: - rv["dynamic_sampling_context"] = ( - self.containing_transaction.get_baggage().dynamic_sampling_context() - ) - - data = {} - - thread_id = self._data.get(SPANDATA.THREAD_ID) - if thread_id is not None: - data["thread.id"] = thread_id - - thread_name = self._data.get(SPANDATA.THREAD_NAME) - if thread_name is not None: - data["thread.name"] = thread_name - - if data: - rv["data"] = data - - return rv - - def get_profile_context(self) -> "Optional[ProfileContext]": - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - def update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -class Transaction(Span): - """The Transaction is the root element that holds all the spans - for Sentry performance instrumentation. - - :param name: Identifier of the transaction. - Will show up in the Sentry UI. - :param parent_sampled: Whether the parent transaction was sampled. - If True this transaction will be kept, if False it will be discarded. - :param baggage: The W3C baggage header value. - (see https://www.w3.org/TR/baggage/) - :param source: A string describing the source of the transaction name. - This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations - for more information. Default "custom". - :param kwargs: Additional arguments to be passed to the Span constructor. - See :py:class:`sentry_sdk.tracing.Span` for available arguments. - """ - - __slots__ = ( - "name", - "source", - "parent_sampled", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_measurements", - "_contexts", - "_profile", - "_continuous_profile", - "_baggage", - "_sample_rand", - ) - - def __init__( # type: ignore[misc] - self, - name: str = "", - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - source: str = TransactionSource.CUSTOM, - **kwargs: "Unpack[SpanKwargs]", - ) -> None: - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate: "Optional[float]" = None - self.parent_sampled = parent_sampled - self._measurements: "Dict[str, MeasurementValue]" = {} - self._contexts: "Dict[str, Any]" = {} - self._profile: "Optional[Profile]" = None - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._baggage = baggage - - baggage_sample_rand = ( - None if self._baggage is None else self._baggage._sample_rand() - ) - if baggage_sample_rand is not None: - self._sample_rand = baggage_sample_rand - else: - self._sample_rand = _generate_sample_rand(self.trace_id) - - def __repr__(self) -> str: - return ( - "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.name, - self.op, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.source, - self.origin, - ) - ) - - def _possibly_started(self) -> bool: - """Returns whether the transaction might have been started. - - If this returns False, we know that the transaction was not started - with sentry_sdk.start_transaction, and therefore the transaction will - be discarded. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self) -> "Transaction": - if not self._possibly_started(): - logger.debug( - "Transaction was entered without being started with sentry_sdk.start_transaction." - "The transaction will not be sent to Sentry. To fix, start the transaction by" - "passing it to sentry_sdk.start_transaction." - ) - - super().__enter__() - - if self._profile is not None: - self._profile.__enter__() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - - if self._continuous_profile is not None: - self._continuous_profile.stop() - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self) -> "Transaction": - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def _get_scope_from_finish_args( - self, - scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - ) -> "Optional[sentry_sdk.Scope]": - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - - def _get_log_representation(self) -> str: - return "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. - - :param scope: The Scope to use for this transaction. - If not provided, the current Scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args(scope, hub) - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be None - if self.sampled is False: - logger.debug("Discarding transaction because sampled = False") - else: - logger.debug( - "Discarding transaction because it was not started with sentry_sdk.start_transaction" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - if client.transport and has_tracing_enabled(client.options): - if client.monitor and client.monitor.downsample_factor > 0: - reason = "backpressure" - else: - reason = "sample_rate" - - client.transport.record_lost_event(reason, data_category="transaction") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) - if ( - status_code is not None - and status_code in client.options["trace_ignore_status_codes"] - ): - logger.debug( - "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( - transaction_description=self._get_log_representation(), - status_code=self._data[SPANDATA.HTTP_STATUS_CODE], - trace_ignore_status_codes=client.options[ - "trace_ignore_status_codes" - ], - ) - ) - if client.transport: - client.transport.record_lost_event( - "event_processor", data_category="transaction" - ) - - num_spans = len(self._span_recorder.spans) + 1 - client.transport.record_lost_event( - "event_processor", data_category="span", quantity=num_spans - ) - - self.sampled = False - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - if self.sampled is None: - logger.warning("Discarding transaction without sampling decision.") - - return None - - finished_spans = [] - has_gen_ai_span = False - if client.options.get("stream_gen_ai_spans", True): - for span in self._span_recorder.spans: - if span.timestamp is None: - continue - - if isinstance(span.op, str) and span.op.startswith("gen_ai."): - has_gen_ai_span = True - - finished_spans.append(span.to_json()) - else: - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - len_diff = len(self._span_recorder.spans) - len(finished_spans) - dropped_spans = len_diff + self._span_recorder.dropped_spans - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - self._span_recorder = None - - contexts = {} - contexts.update(self._contexts) - contexts.update({"trace": self.get_trace_context()}) - profile_context = self.get_profile_context() - if profile_context is not None: - contexts.update({"profile": profile_context}) - - event: "Event" = { - "type": "transaction", - "transaction": self.name, - "transaction_info": {"source": self.source}, - "contexts": contexts, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": finished_spans, - } - - if dropped_spans > 0: - event["_dropped_spans"] = dropped_spans - - if has_gen_ai_span: - event["_has_gen_ai_span"] = True - - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - - event["measurements"] = self._measurements - - return scope.capture_event(event) - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - """Sets a context. Transactions can have multiple contexts - and they should follow the format described in the "Contexts Interface" - documentation. - - :param key: The name of the context. - :param value: The information about the context. - """ - self._contexts[key] = value - - def set_http_status(self, http_status: int) -> None: - """Sets the status of the Transaction according to the given HTTP status. - - :param http_status: The HTTP status code.""" - super().set_http_status(http_status) - self.set_context("response", {"status_code": http_status}) - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the transaction.""" - rv = super().to_json() - - rv["name"] = self.name - rv["source"] = self.source - rv["sampled"] = self.sampled - - return rv - - def get_trace_context(self) -> "Any": - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self) -> "Baggage": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with the Transaction. - - The first time a new baggage with Sentry items is made, - it will be frozen.""" - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_transaction(self) - - return self._baggage - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the transaction's sampling decision, according to the following - precedence rules: - - 1. If a sampling decision is passed to `start_transaction` - (`start_transaction(name: "my transaction", sampled: True)`), that - decision will be used, regardless of anything else - - 2. If `traces_sampler` is defined, its decision will be used. It can - choose to keep or ignore any parent sampling decision, or use the - sampling context data to make its own decision or to choose a sample - rate for the transaction. - - 3. If `traces_sampler` is not defined, but there's a parent sampling - decision, the parent sampling decision will be used. - - 4. If `traces_sampler` is not defined and there's no parent sampling - decision, `traces_sample_rate` will be used. - """ - client = sentry_sdk.get_client() - - transaction_description = self._get_log_representation() - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) - # default inheritance behavior - else ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning( - "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( - transaction_description=transaction_description, - ) - ) - self.sampled = False - return - - self.sample_rate = float(sample_rate) - - if client.monitor: - self.sample_rate /= 2**client.monitor.downsample_factor - - # if the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - if not self.sample_rate: - logger.debug( - "[Tracing] Discarding {transaction_description} because {reason}".format( - transaction_description=transaction_description, - reason=( - "traces_sampler returned 0 or False" - if callable(client.options.get("traces_sampler")) - else "traces_sample_rate is set to 0" - ), - ) - ) - self.sampled = False - return - - # Now we roll the dice. - self.sampled = self._sample_rand < self.sample_rate - - if self.sampled: - logger.debug( - "[Tracing] Starting {transaction_description}".format( - transaction_description=transaction_description, - ) - ) - else: - logger.debug( - "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( - transaction_description=transaction_description, - sample_rate=self.sample_rate, - ) - ) - - # Private aliases matching StreamedSpan's private API - _get_baggage = get_baggage - _get_trace_context = get_trace_context - - -class NoOpSpan(Span): - def __repr__(self) -> str: - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self) -> "Optional[Transaction]": - return None - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "NoOpSpan": - return NoOpSpan() - - def to_traceparent(self) -> str: - return "" - - def to_baggage(self) -> "Optional[Baggage]": - return None - - def get_baggage(self) -> "Optional[Baggage]": - return None - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - return iter(()) - - def set_tag(self, key: str, value: "Any") -> None: - pass - - def set_data(self, key: str, value: "Any") -> None: - pass - - def update_data(self, data: "Dict[str, Any]") -> None: - pass - - def set_status(self, value: str) -> None: - pass - - def set_http_status(self, http_status: int) -> None: - pass - - def is_success(self) -> bool: - return True - - def to_json(self) -> "Dict[str, Any]": - return {} - - def get_trace_context(self) -> "Any": - return {} - - def get_profile_context(self) -> "Any": - return {} - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ - pass - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - pass - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - pass - - def init_span_recorder(self, maxlen: int) -> None: - pass - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - pass - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _get_baggage = get_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -if TYPE_CHECKING: - - @overload - def trace( - func: None = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": - # Handles: @trace() and @trace(op="custom") - pass - - @overload - def trace(func: "Callable[P, R]") -> "Callable[P, R]": - # Handles: @trace - pass - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a child span around a function call. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, a default operation will - be assigned based on the template. - :type op: str or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP, SPANTEMPLATE - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - op=OP.DB_QUERY, - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - - # With a custom template - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def calculate_interest_rate(amount, rate, years): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import create_span_decorator - - decorator = create_span_decorator( - op=op, - name=name, - attributes=attributes, - template=template, - ) - - if func: - return decorator(func) - else: - return decorator - - -# Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - _generate_sample_rand, - extract_sentrytrace_data, - has_tracing_enabled, - maybe_create_breadcrumbs_from_span, -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py deleted file mode 100644 index c2e34a795b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/tracing_utils.py +++ /dev/null @@ -1,1694 +0,0 @@ -import contextlib -import functools -import inspect -import os -import re -import sys -import uuid -import warnings -from collections.abc import Mapping, MutableMapping -from datetime import datetime, timedelta, timezone -from random import Random -from urllib.parse import quote, unquote - -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE -from sentry_sdk.utils import ( - _is_external_source, - _is_in_project_root, - _module_in_list, - capture_internal_exceptions, - filename_for_module, - is_sentry_url, - is_valid_sample_rate, - logger, - match_regex_list, - qualname_from_function, - safe_repr, - to_string, - try_convert, -) - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union - - from sentry_sdk._types import Attributes - - -SENTRY_TRACE_REGEX = re.compile( - "^[ \t]*" # whitespace - "([0-9a-f]{32})?" # trace_id - "-?([0-9a-f]{16})?" # span_id - "-?([01])?" # sampled - "[ \t]*$" # whitespace -) - - -# This is a normal base64 regex, modified to reflect that fact that we strip the -# trailing = or == off -base64_stripped = ( - # any of the characters in the base64 "alphabet", in multiples of 4 - "([a-zA-Z0-9+/]{4})*" - # either nothing or 2 or 3 base64-alphabet characters (see - # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for - # why there's never only 1 extra character) - "([a-zA-Z0-9+/]{2,3})?" -) - - -class EnvironHeaders(Mapping): # type: ignore - def __init__( - self, - environ: "Mapping[str, str]", - prefix: str = "HTTP_", - ) -> None: - self.environ = environ - self.prefix = prefix - - def __getitem__(self, key: str) -> "Optional[Any]": - return self.environ[self.prefix + key.replace("-", "_").upper()] - - def __len__(self) -> int: - return sum(1 for _ in iter(self)) - - def __iter__(self) -> "Generator[str, None, None]": - for k in self.environ: - if not isinstance(k, str): - continue - - k = k.replace("-", "_").upper() - if not k.startswith(self.prefix): - continue - - yield k[len(self.prefix) :] - - -def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: - """ - Returns True if either traces_sample_rate or traces_sampler is - defined and enable_tracing is set and not false. - """ - if options is None: - return False - - return bool( - options.get("enable_tracing") is not False - and ( - options.get("traces_sample_rate") is not None - or options.get("traces_sampler") is not None - ) - ) - - -def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return False - - return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" - - -def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return True - - return not options.get( - "stream_gen_ai_spans", True - ) and not has_span_streaming_enabled(options) - - -@contextlib.contextmanager -def record_sql_queries( - cursor: "Any", - query: "Any", - params_list: "Any", - paramstyle: "Optional[str]", - executemany: bool, - record_cursor_repr: bool = False, - span_origin: str = "manual", - span_op_override_value: "Optional[str]" = None, -) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": - # TODO: Bring back capturing of params by default - client = sentry_sdk.get_client() - if client.options["_experiments"].get("record_sql_params", False): - if not params_list or params_list == [None]: - params_list = None - - if paramstyle == "pyformat": - paramstyle = "format" - else: - params_list = None - paramstyle = None - - query = _format_sql(cursor, query) - - data = {} - if params_list is not None: - data["db.params"] = params_list - if paramstyle is not None: - data["db.paramstyle"] = paramstyle - if executemany: - data["db.executemany"] = True - if record_cursor_repr and cursor is not None: - data["db.cursor"] = cursor - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message=query, category="query", data=data) - - if has_span_streaming_enabled(client.options): - additional_attributes = {} - if query is not None: - additional_attributes["db.query.text"] = query - - with sentry_sdk.traces.start_span( - name="" if query is None else query, - attributes={ - "sentry.origin": span_origin, - "sentry.op": span_op_override_value - if span_op_override_value - else OP.DB, - **additional_attributes, - }, - ) as span: - yield span - else: - with sentry_sdk.start_span( - op=span_op_override_value if span_op_override_value is not None else OP.DB, - name=query, - origin=span_origin, - ) as span: - for k, v in data.items(): - span.set_data(k, v) - yield span - - -def maybe_create_breadcrumbs_from_span( - scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" -) -> None: - if span.op == OP.DB_REDIS: - scope.add_breadcrumb( - message=span.description, type="redis", category="redis", data=span._tags - ) - - elif span.op == OP.HTTP_CLIENT: - level = None - status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) - if status_code: - if 500 <= status_code <= 599: - level = "error" - elif 400 <= status_code <= 499: - level = "warning" - - if level: - scope.add_breadcrumb( - type="http", category="httplib", data=span._data, level=level - ) - else: - scope.add_breadcrumb(type="http", category="httplib", data=span._data) - - elif span.op == "subprocess": - scope.add_breadcrumb( - type="subprocess", - category="subprocess", - message=span.description, - data=span._data, - ) - - -def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": - try: - return frame.f_code.co_filename - except Exception: - return None - - -def _should_be_included( - is_sentry_sdk_frame: bool, - namespace: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", - abs_path: "Optional[str]", - project_root: "Optional[str]", -) -> bool: - # in_app_include takes precedence over in_app_exclude - should_be_included = _module_in_list(namespace, in_app_include) - should_be_excluded = _is_external_source(abs_path) or _module_in_list( - namespace, in_app_exclude - ) - return not is_sentry_sdk_frame and ( - should_be_included - or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) - ) - - -def add_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", - project_root: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", -) -> None: - """ - Adds OTel compatible source code information to the span - """ - # Find the correct frame - frame: "Union[FrameType, None]" = sys._getframe() - while frame is not None: - abs_path = _get_frame_module_abs_path(frame) - - try: - namespace: "Optional[str]" = frame.f_globals.get("__name__") - except Exception: - namespace = None - - is_sentry_sdk_frame = namespace is not None and namespace.startswith( - "sentry_sdk." - ) - - should_be_included = _should_be_included( - is_sentry_sdk_frame=is_sentry_sdk_frame, - namespace=namespace, - in_app_include=in_app_include, - in_app_exclude=in_app_exclude, - abs_path=abs_path, - project_root=project_root, - ) - if should_be_included: - break - - frame = frame.f_back - else: - frame = None - - # Set the data - if frame is not None: - try: - lineno = frame.f_lineno - except Exception: - lineno = None - if lineno is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_LINENO, lineno) - else: - span.set_attribute("code.line.number", lineno) - - try: - namespace = frame.f_globals.get("__name__") - except Exception: - namespace = None - if namespace is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_NAMESPACE, namespace) - else: - span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) - - filepath = _get_frame_module_abs_path(frame) - if filepath is not None: - if namespace is not None: - in_app_path = filename_for_module(namespace, filepath) - elif project_root is not None and filepath.startswith(project_root): - in_app_path = filepath.replace(project_root, "").lstrip(os.sep) - else: - in_app_path = filepath - - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) - else: - if in_app_path is not None: - span.set_attribute("code.file.path", in_app_path) - - try: - code_function = frame.f_code.co_name - except Exception: - code_function = None - - if code_function is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - else: - span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - - -def add_query_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a database query span - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_query_source = client.options.get("enable_db_query_source", True) - if not should_add_query_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("db_query_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def add_http_request_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a span for an outgoing HTTP request - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_request_source = client.options.get("enable_http_request_source", True) - if not should_add_request_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("http_request_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def extract_sentrytrace_data( - header: "Optional[str]", -) -> "Optional[Dict[str, Union[str, bool, None]]]": - """ - Given a `sentry-trace` header string, return a dictionary of data. - """ - if not header: - return None - - if "," in header: - # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. - parts = [part.strip() for part in header.split(",")] - header = next((part for part in parts if part), "") - - if not header: - return None - - if header.startswith("00-") and header.endswith("-00"): - header = header[3:-3] - - match = SENTRY_TRACE_REGEX.match(header) - if not match: - return None - - trace_id, parent_span_id, sampled_str = match.groups() - parent_sampled = None - - if trace_id: - trace_id = "{:032x}".format(int(trace_id, 16)) - if parent_span_id: - parent_span_id = "{:016x}".format(int(parent_span_id, 16)) - if sampled_str: - parent_sampled = sampled_str != "0" - - return { - "trace_id": trace_id, - "parent_span_id": parent_span_id, - "parent_sampled": parent_sampled, - } - - -def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": - real_sql = None - - # If we're using psycopg2, it could be that we're - # looking at a query that uses Composed objects. Use psycopg2's mogrify - # function to format the query. We lose per-parameter trimming but gain - # accuracy in formatting. - try: - if hasattr(cursor, "mogrify"): - real_sql = cursor.mogrify(sql) - if isinstance(real_sql, bytes): - real_sql = real_sql.decode(cursor.connection.encoding) - except Exception: - real_sql = None - - return real_sql or to_string(sql) - - -class PropagationContext: - """ - The PropagationContext represents the data of a trace in Sentry. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "parent_sampled", - "baggage", - "custom_sampling_context", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - dynamic_sampling_context: "Optional[Dict[str, str]]" = None, - baggage: "Optional[Baggage]" = None, - ) -> None: - self._trace_id = trace_id - """The trace id of the Sentry trace.""" - - self._span_id = span_id - """The span id of the currently executing span.""" - - self.parent_span_id = parent_span_id - """The id of the parent span that started this span. - The parent span could also be a span in an upstream service.""" - - self.parent_sampled = parent_sampled - """Boolean indicator if the parent span was sampled. - Important when the parent span originated in an upstream service, - because we want to sample the whole trace, or nothing from the trace.""" - - self.baggage = baggage - """Parsed baggage header that is used for dynamic sampling decisions.""" - - """DEPRECATED this only exists for backwards compat of constructor.""" - if baggage is None and dynamic_sampling_context is not None: - self.baggage = Baggage(dynamic_sampling_context) - - self.custom_sampling_context: "Optional[dict[str, Any]]" = None - - @classmethod - def from_incoming_data( - cls, incoming_data: "Dict[str, Any]" - ) -> "PropagationContext": - propagation_context = PropagationContext() - normalized_data = normalize_incoming_data(incoming_data) - - sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) - sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) - - # nothing to propagate if no sentry-trace - if sentrytrace_data is None: - return propagation_context - - baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) - baggage = ( - Baggage.from_incoming_header(baggage_header) if baggage_header else None - ) - - if not _should_continue_trace(baggage): - return propagation_context - - propagation_context.update(sentrytrace_data) - if baggage: - propagation_context.baggage = baggage - - propagation_context._fill_sample_rand() - - return propagation_context - - @property - def trace_id(self) -> str: - """The trace id of the Sentry trace.""" - if not self._trace_id: - # New trace, don't fill in sample_rand - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - """The span id of the currently executed span.""" - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - @property - def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": - return self.get_baggage().dynamic_sampling_context() - - def to_traceparent(self) -> str: - return f"{self.trace_id}-{self.span_id}" - - def get_baggage(self) -> "Baggage": - if self.baggage is None: - self.baggage = Baggage.populate_from_propagation_context(self) - return self.baggage - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. - """ - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def update(self, other_dict: "Dict[str, Any]") -> None: - """ - Updates the PropagationContext with data from the given dictionary. - """ - for key, value in other_dict.items(): - try: - setattr(self, key, value) - except AttributeError: - pass - - def _set_custom_sampling_context( - self, custom_sampling_context: "dict[str, Any]" - ) -> None: - self.custom_sampling_context = custom_sampling_context - - def __repr__(self) -> str: - return "".format( - self._trace_id, - self._span_id, - self.parent_span_id, - self.parent_sampled, - self.baggage, - ) - - def _fill_sample_rand(self) -> None: - """ - Ensure that there is a valid sample_rand value in the baggage. - - If there is a valid sample_rand value in the baggage, we keep it. - Otherwise, we generate a sample_rand value according to the following: - - - If we have a parent_sampled value and a sample_rate in the DSC, we compute - a sample_rand value randomly in the range: - - [0, sample_rate) if parent_sampled is True, - - or, in the range [sample_rate, 1) if parent_sampled is False. - - - If either parent_sampled or sample_rate is missing, we generate a random - value in the range [0, 1). - - The sample_rand is deterministically generated from the trace_id, if present. - - This function does nothing if there is no baggage. - """ - if self.baggage is None: - return - - sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) - if sample_rand is not None and 0 <= sample_rand < 1: - # sample_rand is present and valid, so don't overwrite it - return - - # Get the sample rate and compute the transformation that will map the random value - # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). - sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) - lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) - - try: - sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) - except ValueError: - # ValueError is raised if the interval is invalid, i.e. lower >= upper. - # lower >= upper might happen if the incoming trace's sampled flag - # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. - # We cannot generate a sensible sample_rand value in this case. - logger.debug( - f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " - f"and sample_rate={sample_rate}." - ) - return - - self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 - - def _sample_rand(self) -> "Optional[str]": - """Convenience method to get the sample_rand value from the baggage.""" - if self.baggage is None: - return None - - return self.baggage.sentry_items.get("sample_rand") - - -class Baggage: - """ - The W3C Baggage header information (see https://www.w3.org/TR/baggage/). - - Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. - Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but - it is the caller's responsibility to enforce this restriction. - """ - - __slots__ = ("sentry_items", "third_party_items", "mutable") - - SENTRY_PREFIX = "sentry-" - SENTRY_PREFIX_REGEX = re.compile("^sentry-") - - def __init__( - self, - sentry_items: "Dict[str, str]", - third_party_items: str = "", - mutable: bool = True, - ): - self.sentry_items = sentry_items - self.third_party_items = third_party_items - self.mutable = mutable - - @classmethod - def from_incoming_header( - cls, - header: "Optional[str]", - *, - _sample_rand: "Optional[str]" = None, - ) -> "Baggage": - """ - freeze if incoming header already has sentry baggage - """ - sentry_items = {} - third_party_items = "" - mutable = True - - if header: - for item in header.split(","): - if "=" not in item: - continue - - with capture_internal_exceptions(): - item = item.strip() - key, val = item.split("=", 1) - if Baggage.SENTRY_PREFIX_REGEX.match(key): - baggage_key = unquote(key.split("-")[1]) - sentry_items[baggage_key] = unquote(val) - mutable = False - else: - third_party_items += ("," if third_party_items else "") + item - - if _sample_rand is not None: - sentry_items["sample_rand"] = str(_sample_rand) - mutable = False - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": - """ - Deprecated: use populate_from_propagation_context - """ - if scope._propagation_context is None: - return Baggage({}) - - return Baggage.populate_from_propagation_context(scope._propagation_context) - - @classmethod - def populate_from_propagation_context( - cls, propagation_context: "PropagationContext" - ) -> "Baggage": - sentry_items: "Dict[str, str]" = {} - third_party_items = "" - mutable = False - - client = sentry_sdk.get_client() - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options - - sentry_items["trace_id"] = propagation_context.trace_id - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if options.get("traces_sample_rate"): - sentry_items["sample_rate"] = str(options["traces_sample_rate"]) - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def populate_from_transaction( - cls, transaction: "sentry_sdk.tracing.Transaction" - ) -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - transaction.name - and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES - ): - sentry_items["transaction"] = transaction.name - - if transaction.sample_rate is not None: - sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction.sampled is not None: - sentry_items["sampled"] = "true" if transaction.sampled else "false" - - # there's an existing baggage but it was mutable, - # which is why we are creating this new baggage. - # However, if by chance the user put some sentry items in there, give them precedence. - if transaction._baggage and transaction._baggage.sentry_items: - sentry_items.update(transaction._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - @classmethod - def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = segment.trace_id - sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - segment.get_attributes().get("sentry.span.source") - not in LOW_QUALITY_SEGMENT_SOURCES - ) and segment._name: - sentry_items["transaction"] = segment._name - - if segment._sample_rate is not None: - sentry_items["sample_rate"] = str(segment._sample_rate) - - if segment.sampled is not None: - sentry_items["sampled"] = "true" if segment.sampled else "false" - - # There's an existing baggage but it was mutable, which is why we are - # creating this new baggage. - # However, if by chance the user put some sentry items in there, give - # them precedence. - if segment._baggage and segment._baggage.sentry_items: - sentry_items.update(segment._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - def freeze(self) -> None: - self.mutable = False - - def dynamic_sampling_context(self) -> "Dict[str, str]": - header = {} - - for key, item in self.sentry_items.items(): - header[key] = item - - return header - - def serialize(self, include_third_party: bool = False) -> str: - items = [] - - for key, val in self.sentry_items.items(): - with capture_internal_exceptions(): - item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) - items.append(item) - - if include_third_party: - items.append(self.third_party_items) - - return ",".join(items) - - @staticmethod - def strip_sentry_baggage(header: str) -> str: - """Remove Sentry baggage from the given header. - - Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. - """ - return ",".join( - ( - item - for item in header.split(",") - if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) - ) - ) - - def _sample_rand(self) -> "Optional[float]": - """Convenience method to get the sample_rand value from the sentry_items. - - We validate the value and parse it as a float before returning it. The value is considered - valid if it is a float in the range [0, 1). - """ - sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) - - if sample_rand is not None and 0.0 <= sample_rand < 1.0: - return sample_rand - - return None - - def __repr__(self) -> str: - return f'' - - -def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: - """ - Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. - """ - trace_propagation_targets = client.options["trace_propagation_targets"] - - if is_sentry_url(client, url): - return False - - return match_regex_list(url, trace_propagation_targets, substring_matching=True) - - -def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": - """ - Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. - """ - data = {} - for key, value in incoming_data.items(): - if key.startswith("HTTP_"): - key = key[5:] - - key = key.replace("_", "-").lower() - data[key] = value - - return data - - -def create_span_decorator( - op: "Optional[Union[str, OP]]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Any": - """ - Create a span decorator that can wrap both sync and async functions. - - :param op: The operation type for the span. - :type op: str or :py:class:`sentry_sdk.consts.OP` or None - :param name: The name of the span. - :type name: str or None - :param attributes: Additional attributes to set on the span. - :type attributes: dict or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - """ - from sentry_sdk.scope import should_send_default_pii - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return await f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return await f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = await f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def create_streaming_span_decorator( - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Any": - """ - Create a span creating decorator that can wrap both sync and async functions. - """ - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - result = await f(*args, **kwargs) - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - return f(*args, **kwargs) - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - -def _generate_sample_rand( - trace_id: "Optional[str]", - *, - interval: "tuple[float, float]" = (0.0, 1.0), -) -> float: - """Generate a sample_rand value from a trace ID. - - The generated value will be pseudorandomly chosen from the provided - interval. Specifically, given (lower, upper) = interval, the generated - value will be in the range [lower, upper). The value has 6-digit precision, - so when printing with .6f, the value will never be rounded up. - - The pseudorandom number generator is seeded with the trace ID. - """ - lower, upper = interval - if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly - raise ValueError("Invalid interval: lower must be less than upper") - - rng = Random(trace_id) - lower_scaled = int(lower * 1_000_000) - upper_scaled = int(upper * 1_000_000) - try: - sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) - except ValueError: - # In some corner cases it might happen that the range is too small - # In that case, just take the lower bound - sample_rand_scaled = lower_scaled - - return sample_rand_scaled / 1_000_000 - - -def _sample_rand_range( - parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" -) -> "tuple[float, float]": - """ - Compute the lower (inclusive) and upper (exclusive) bounds of the range of values - that a generated sample_rand value must fall into, given the parent_sampled and - sample_rate values. - """ - if parent_sampled is None or sample_rate is None: - return 0.0, 1.0 - elif parent_sampled is True: - return 0.0, sample_rate - else: # parent_sampled is False - return sample_rate, 1.0 - - -def _get_value(source: "Any", key: str) -> "Optional[Any]": - """ - Gets a value from a source object. The source can be a dict or an object. - It is checked for dictionary keys and object attributes. - """ - value = None - if isinstance(source, dict): - value = source.get(key) - else: - if hasattr(source, key): - try: - value = getattr(source, key) - except Exception: - value = None - return value - - -def _get_span_name( - template: "Union[str, SPANTEMPLATE]", - name: str, - kwargs: "Optional[dict[str, Any]]" = None, -) -> str: - """ - Get the name of the span based on the template and the name. - """ - span_name = name - - if template == SPANTEMPLATE.AI_CHAT: - model = None - if kwargs: - for key in ("model", "model_name"): - if kwargs.get(key) and isinstance(kwargs[key], str): - model = kwargs[key] - break - - span_name = f"chat {model}" if model else "chat" - - elif template == SPANTEMPLATE.AI_AGENT: - span_name = f"invoke_agent {name}" - - elif template == SPANTEMPLATE.AI_TOOL: - span_name = f"execute_tool {name}" - - return span_name - - -def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: - """ - Get the operation of the span based on the template. - """ - mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { - SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, - SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, - SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, - } - op = mapping.get(template, OP.FUNCTION) - - return str(op) - - -def _get_input_attributes( - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Get input attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - mapping = { - "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), - "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), - "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), - "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), - "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), - "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), - "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), - "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), - } - - def _set_from_key(key: str, value: "Any") -> None: - if key in mapping: - (attribute, data_type) = mapping[key] - if value is not None and isinstance(value, data_type): - attributes[attribute] = value - - for key, value in list(kwargs.items()): - if key == "prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "user", "content": value} - ) - continue - - if key == "system_prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "system", "content": value} - ) - continue - - _set_from_key(key, value) - - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) - - # Coerce to string - if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) - - return attributes - - -def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": - """ - Get usage attributes. - """ - attributes = {} - - def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: - for key in keys: - value = _get_value(usage, key) - if value is not None and isinstance(value, int): - attributes[attribute] = value - - _set_from_keys( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - ("prompt_tokens", "input_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - ("completion_tokens", "output_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - ("total_tokens",), - ) - - return attributes - - -def _get_output_attributes( - template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> "dict[str, Any]": - """ - Get output attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - with capture_internal_exceptions(): - # Usage from result, result.usage, and result.metadata.usage - usage_candidates = [result] - - usage = _get_value(result, "usage") - usage_candidates.append(usage) - - meta = _get_value(result, "metadata") - usage = _get_value(meta, "usage") - usage_candidates.append(usage) - - for usage_candidate in usage_candidates: - if usage_candidate is not None: - attributes.update(_get_usage_attributes(usage_candidate)) - - # Response model - model_name = _get_value(result, "model") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - model_name = _get_value(result, "model_name") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - # Tool output - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) - - return attributes - - -def _set_input_attributes( - span: "Span", - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - name: str, - f: "Any", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> None: - """ - Set span input attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param f: The wrapped function. - :param args: The arguments to the wrapped function. - :param kwargs: The keyword arguments to the wrapped function. - """ - attributes: "dict[str, Any]" = {} - - if template == SPANTEMPLATE.AI_AGENT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: name, - } - elif template == SPANTEMPLATE.AI_CHAT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - } - elif template == SPANTEMPLATE.AI_TOOL: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: name, - } - - docstring = f.__doc__ - if docstring is not None: - attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - - attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.update_data(attributes or {}) - - -def _set_output_attributes( - span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> None: - """ - Set span output attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param result: The result of the wrapped function. - """ - span.update_data(_get_output_attributes(template, send_pii, result) or {}) - - -def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: - """ - Check if we should continue the incoming trace according to the strict_trace_continuation spec. - https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation - """ - - client = sentry_sdk.get_client() - parsed_dsn = client.parsed_dsn - client_org_id = parsed_dsn.org_id if parsed_dsn else None - baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None - - if ( - client_org_id is not None - and baggage_org_id is not None - and client_org_id != baggage_org_id - ): - logger.debug( - f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - strict_trace_continuation: bool = client.options.get( - "strict_trace_continuation", False - ) - if strict_trace_continuation: - if (baggage_org_id is not None and client_org_id is None) or ( - baggage_org_id is None and client_org_id is not None - ): - logger.debug( - f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - return True - - -def add_sentry_baggage_to_headers( - headers: "MutableMapping[str, str]", sentry_baggage: str -) -> None: - """Add the Sentry baggage to the headers. - - This function directly mutates the provided headers. The provided sentry_baggage - is appended to the existing baggage. If the baggage already contains Sentry items, - they are stripped out first. - """ - existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") - stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) - - separator = "," if len(stripped_existing_baggage) > 0 else "" - - headers[BAGGAGE_HEADER_NAME] = ( - stripped_existing_baggage + separator + sentry_baggage - ) - - -def _make_sampling_decision( - name: str, - attributes: "Optional[Attributes]", - scope: "sentry_sdk.Scope", -) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": - """ - Decide whether a span should be sampled. - - Returns a tuple with: - 1. the sampling decision - 2. the effective sample rate - 3. the sample rand - 4. the reason for not sampling the span, if unsampled - """ - client = sentry_sdk.get_client() - - if not has_tracing_enabled(client.options): - return False, None, None, None - - propagation_context = scope.get_active_propagation_context() - - sample_rand = None - if propagation_context.baggage is not None: - sample_rand = propagation_context.baggage._sample_rand() - if sample_rand is None: - sample_rand = _generate_sample_rand(propagation_context.trace_id) - - # If there's a traces_sampler, use that; otherwise use traces_sample_rate - traces_sampler_defined = callable(client.options.get("traces_sampler")) - if traces_sampler_defined: - sampling_context = { - "span_context": { - "name": name, - "trace_id": propagation_context.trace_id, - "parent_span_id": propagation_context.parent_span_id, - "parent_sampled": propagation_context.parent_sampled, - "attributes": dict(attributes) if attributes else {}, - }, - } - - if propagation_context.custom_sampling_context: - sampling_context.update(propagation_context.custom_sampling_context) - - sample_rate = client.options["traces_sampler"](sampling_context) - else: - if propagation_context.parent_sampled is not None: - sample_rate = propagation_context.parent_sampled - else: - sample_rate = client.options["traces_sample_rate"] - - # Validate whether the sample_rate we got is actually valid. Since - # traces_sampler is user-provided, it could return anything. - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") - return False, None, None, "sample_rate" - - sample_rate = float(sample_rate) - if not sample_rate: - if traces_sampler_defined: - reason = "traces_sampler returned 0 or False" - else: - reason = "traces_sample_rate is set to 0" - - logger.debug(f"[Tracing] Discarding {name} because {reason}") - return False, 0.0, None, "sample_rate" - - # Adjust sample rate if we're under backpressure - sample_rate_before_backpressure = sample_rate - if client.monitor: - sample_rate /= 2**client.monitor.downsample_factor - - if not sample_rate: - logger.debug(f"[Tracing] Discarding {name} because backpressure") - return False, 0.0, None, "backpressure" - - # Make the actual decision - sampled = sample_rand < sample_rate - - if sampled: - logger.debug(f"[Tracing] Starting {name}") - outcome = None - - else: - # Determine why exactly the span will not be sampled. If we've lowered - # the effective sample_rate because of backpressure, check whether the - # span would've been sampled if backpressure wasn't active. If that's the - # case, backpressure is the actual reason, otherwise just pure sampling - # rate. - if ( - sample_rate_before_backpressure != sample_rate - and sample_rand < sample_rate_before_backpressure - ): - logger.debug(f"[Tracing] Discarding {name} because backpressure") - outcome = "backpressure" - - else: - logger.debug( - f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" - ) - outcome = "sample_rate" - - return sampled, sample_rate, sample_rand, outcome - - -def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: - """Determine if a span fits one of the rules in ignore_spans.""" - client = sentry_sdk.get_client() - ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") - - if not ignore_spans: - return False - - def _matches(rule: "Any", value: "Any") -> bool: - if isinstance(rule, Pattern): - if isinstance(value, str): - return bool(rule.fullmatch(value)) - else: - return False - - return rule == value - - for rule in ignore_spans: - if isinstance(rule, (str, Pattern)): - if _matches(rule, name): - return True - - elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): - name_matches = True - attributes_match = True - - if "name" in rule: - name_matches = _matches(rule["name"], name) - - if "attributes" in rule: - attributes = attributes or {} - - for attribute, value in rule["attributes"].items(): - if attribute not in attributes or not _matches( - value, attributes[attribute] - ): - attributes_match = False - break - - if name_matches and attributes_match: - return True - - return False - - -# Circular imports -from sentry_sdk.traces import ( - LOW_QUALITY_SEGMENT_SOURCES, - StreamedSpan, -) -from sentry_sdk.traces import ( - start_span as start_streaming_span, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - LOW_QUALITY_TRANSACTION_SOURCES, - SENTRY_TRACE_HEADER_NAME, - Span, -) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py deleted file mode 100644 index 2b71fee429..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/transport.py +++ /dev/null @@ -1,1255 +0,0 @@ -import asyncio -import gzip -import io -import json -import logging -import os -import socket -import ssl -import time -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from urllib.request import getproxies - -try: - import brotli # type: ignore -except ImportError: - brotli = None - -try: - import httpcore -except ImportError: - httpcore = None # type: ignore[assignment,unused-ignore] - -try: - import h2 # noqa: F401 - - HTTP2_ENABLED = httpcore is not None -except ImportError: - HTTP2_ENABLED = False - -try: - import anyio # noqa: F401 - - ASYNC_TRANSPORT_AVAILABLE = httpcore is not None -except ImportError: - ASYNC_TRANSPORT_AVAILABLE = False - -from typing import TYPE_CHECKING, Dict, List, cast - -import certifi -import urllib3 - -import sentry_sdk -from sentry_sdk.consts import EndpointType -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - logger, - mark_sentry_task_internal, -) -from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - DefaultDict, - Iterable, - Mapping, - Optional, - Self, - Tuple, - Type, - Union, - ) - - from urllib3.poolmanager import PoolManager, ProxyManager - - from sentry_sdk._types import Event, EventDataCategory - -KEEP_ALIVE_SOCKET_OPTIONS = [] -for option in [ - (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 -]: - try: - KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) - except AttributeError: - # a specific option might not be available on specific systems, - # e.g. TCP_KEEPIDLE doesn't exist on macOS - pass - - -def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": - """Case-insensitive header lookup for httpcore-style responses.""" - header_lower = header.lower() - return next( - ( - val.decode("ascii") - for key, val in response.headers - if key.decode("ascii").lower() == header_lower - ), - None, - ) - - -class Transport(ABC): - """Baseclass for all transports. - - A transport is used to send an event to sentry. - """ - - parsed_dsn: "Optional[Dsn]" = None - - def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: - self.options = options - if options and options["dsn"]: - self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) - else: - self.parsed_dsn = None - - def capture_event(self: "Self", event: "Event") -> None: - """ - DEPRECATED: Please use capture_envelope instead. - - This gets invoked with the event dictionary when an event should - be sent to sentry. - """ - - warnings.warn( - "capture_event is deprecated, please use capture_envelope instead!", - DeprecationWarning, - stacklevel=2, - ) - - envelope = Envelope() - envelope.add_event(event) - self.capture_envelope(envelope) - - @abstractmethod - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - """ - Send an envelope to Sentry. - - Envelopes are a data container format that can hold any type of data - submitted to Sentry. We use it to send all event data (including errors, - transactions, crons check-ins, etc.) to Sentry. - """ - pass - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Any]" = None, - ) -> None: - """ - Wait `timeout` seconds for the current events to be sent out. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def kill(self: "Self") -> None: - """ - Forcefully kills the transport. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - """This increments a counter for event loss by reason and - data category by the given positive-int quantity (default 1). - - If an item is provided, the data category and quantity are - extracted from the item, and the values passed for - data_category and quantity are ignored. - - When recording a lost transaction via data_category="transaction", - the calling code should also record the lost spans via this method. - When recording lost spans, `quantity` should be set to the number - of contained spans, plus one for the transaction itself. When - passing an Item containing a transaction via the `item` parameter, - this method automatically records the lost spans. - """ - return None - - def is_healthy(self: "Self") -> bool: - return True - - -def _parse_rate_limits( - header: str, now: "Optional[datetime]" = None -) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": - if now is None: - now = datetime.now(timezone.utc) - - for limit in header.split(","): - try: - parameters = limit.strip().split(":") - retry_after_val, categories = parameters[:2] - - retry_after = now + timedelta(seconds=int(retry_after_val)) - for category in categories and categories.split(";") or (None,): - yield category, retry_after # type: ignore - except (LookupError, ValueError): - continue - - -class HttpTransportCore(Transport): - """Shared base class for sync and async transports.""" - - TIMEOUT = 30 # seconds - - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - from sentry_sdk.consts import VERSION - - Transport.__init__(self, options) - assert self.parsed_dsn is not None - self.options: "Dict[str, Any]" = options - self._worker = self._create_worker(options) - self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) - self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} - # We only use this Retry() class for the `get_retry_after` method it exposes - self._retry = urllib3.util.Retry() - self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( - defaultdict(int) - ) - self._last_client_report_sent = time.time() - - self._pool = self._make_pool() - - # Backwards compatibility for deprecated `self.hub_class` attribute - self._hub_cls = sentry_sdk.Hub - - experiments = options.get("_experiments", {}) - compression_level = experiments.get( - "transport_compression_level", - experiments.get("transport_zlib_compression_level"), - ) - compression_algo = experiments.get( - "transport_compression_algo", - ( - "gzip" - # if only compression level is set, assume gzip for backwards compatibility - # if we don't have brotli available, fallback to gzip - if compression_level is not None or brotli is None - else "br" - ), - ) - - if compression_algo == "br" and brotli is None: - logger.warning( - "You asked for brotli compression without the Brotli module, falling back to gzip -9" - ) - compression_algo = "gzip" - compression_level = None - - if compression_algo not in ("br", "gzip"): - logger.warning( - "Unknown compression algo %s, disabling compression", compression_algo - ) - self._compression_level = 0 - self._compression_algo = None - else: - self._compression_algo = compression_algo - - if compression_level is not None: - self._compression_level = compression_level - elif self._compression_algo == "gzip": - self._compression_level = 9 - elif self._compression_algo == "br": - self._compression_level = 4 - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - raise NotImplementedError() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - if not self.options["send_client_reports"]: - return - - if item is not None: - data_category = item.data_category - quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). - - if data_category == "transaction": - # Also record the lost spans - event = item.get_transaction_event() or {} - - # +1 for the transaction itself - span_count = ( - len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 - ) - self.record_lost_event(reason, "span", quantity=span_count) - - elif data_category == "log_item" and item: - # Also record size of lost logs in bytes - bytes_size = len(item.get_bytes()) - self.record_lost_event(reason, "log_byte", quantity=bytes_size) - - elif data_category == "attachment": - # quantity of 0 is actually 1 as we do not want to count - # empty attachments as actually empty. - quantity = len(item.get_bytes()) or 1 - - elif data_category is None: - raise TypeError("data category not provided") - - self._discarded_events[data_category, reason] += quantity - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return response.headers.get(header) - - def _update_rate_limits( - self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" - ) -> None: - # new sentries with more rate limit insights. We honor this header - # no matter of the status code to update our internal rate limits. - header = self._get_header_value(response, "x-sentry-rate-limits") - if header: - logger.warning("Rate-limited via x-sentry-rate-limits") - self._disabled_until.update(_parse_rate_limits(header)) - - # old sentries only communicate global rate limit hits via the - # retry-after header on 429. This header can also be emitted on new - # sentries if a proxy in front wants to globally slow things down. - elif response.status == 429: - logger.warning("Rate-limited via 429") - retry_after_value = self._get_header_value(response, "Retry-After") - retry_after = ( - self._retry.parse_retry_after(retry_after_value) - if retry_after_value is not None - else None - ) or 60 - self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( - seconds=retry_after - ) - - def _handle_request_error( - self: "Self", - envelope: "Optional[Envelope]", - loss_reason: str = "network", - record_reason: str = "network_error", - ) -> None: - def record_loss(reason: str) -> None: - if envelope is None: - self.record_lost_event(reason, data_category="error") - else: - for item in envelope.items: - self.record_lost_event(reason, item=item) - - self.on_dropped_event(loss_reason) - record_loss(record_reason) - - def _handle_response( - self: "Self", - response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", - envelope: "Optional[Envelope]", - ) -> None: - self._update_rate_limits(response) - - if response.status == 413: - size_exceeded_message = ( - "HTTP 413: Event dropped due to exceeded envelope size limit" - ) - response_message = getattr( - response, "data", getattr(response, "content", None) - ) - if response_message is not None: - size_exceeded_message += f" (body: {response_message})" - - logger.error(size_exceeded_message) - self._handle_request_error( - envelope=envelope, loss_reason="status_413", record_reason="send_error" - ) - - elif response.status == 429: - # if we hit a 429. Something was rate limited but we already - # acted on this in `self._update_rate_limits`. Note that we - # do not want to record event loss here as we will have recorded - # an outcome in relay already. - self.on_dropped_event("status_429") - pass - - elif response.status >= 300 or response.status < 200: - logger.error( - "Unexpected status code: %s (body: %s)", - response.status, - getattr(response, "data", getattr(response, "content", None)), - ) - self._handle_request_error( - envelope=envelope, loss_reason="status_{}".format(response.status) - ) - - def _update_headers( - self: "Self", - headers: "Dict[str, str]", - ) -> None: - headers.update( - { - "User-Agent": str(self._auth.client), - "X-Sentry-Auth": str(self._auth.to_header()), - } - ) - - def on_dropped_event(self: "Self", _reason: str) -> None: - return None - - def _fetch_pending_client_report( - self: "Self", force: bool = False, interval: int = 60 - ) -> "Optional[Item]": - if not self.options["send_client_reports"]: - return None - - if not (force or self._last_client_report_sent < time.time() - interval): - return None - - discarded_events = self._discarded_events - self._discarded_events = defaultdict(int) - self._last_client_report_sent = time.time() - - if not discarded_events: - return None - - return Item( - PayloadRef( - json={ - "timestamp": time.time(), - "discarded_events": [ - {"reason": reason, "category": category, "quantity": quantity} - for ( - (category, reason), - quantity, - ) in discarded_events.items() - ], - } - ), - type="client_report", - ) - - def _check_disabled(self, category: str) -> bool: - def _disabled(bucket: "Any") -> bool: - ts = self._disabled_until.get(bucket) - return ts is not None and ts > datetime.now(timezone.utc) - - return _disabled(category) or _disabled(None) - - def _is_rate_limited(self: "Self") -> bool: - return any( - ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() - ) - - def _is_worker_full(self: "Self") -> bool: - return self._worker.full() - - def is_healthy(self: "Self") -> bool: - return not (self._is_worker_full() or self._is_rate_limited()) - - def _prepare_envelope( - self: "Self", envelope: "Envelope" - ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": - # remove all items from the envelope which are over quota - new_items = [] - for item in envelope.items: - if self._check_disabled(item.data_category): - if item.data_category in ("transaction", "error", "default", "statsd"): - self.on_dropped_event("self_rate_limits") - self.record_lost_event("ratelimit_backoff", item=item) - else: - new_items.append(item) - - # Since we're modifying the envelope here make a copy so that others - # that hold references do not see their envelope modified. - envelope = Envelope(headers=envelope.headers, items=new_items) - - if not envelope.items: - return None - - # since we're already in the business of sending out an envelope here - # check if we have one pending for the stats session envelopes so we - # can attach it to this enveloped scheduled for sending. This will - # currently typically attach the client report to the most recent - # session update. - client_report_item = self._fetch_pending_client_report(interval=30) - if client_report_item is not None: - envelope.items.append(client_report_item) - - content_encoding, body = self._serialize_envelope(envelope) - - assert self.parsed_dsn is not None - logger.debug( - "Sending envelope [%s] project:%s host:%s", - envelope.description, - self.parsed_dsn.project_id, - self.parsed_dsn.host, - ) - - headers: "Dict[str, str]" = { - "Content-Type": "application/x-sentry-envelope", - } - if content_encoding: - headers["Content-Encoding"] = content_encoding - - return envelope, body, headers - - def _serialize_envelope( - self: "Self", envelope: "Envelope" - ) -> "tuple[Optional[str], io.BytesIO]": - content_encoding = None - body = io.BytesIO() - if self._compression_level == 0 or self._compression_algo is None: - envelope.serialize_into(body) - else: - content_encoding = self._compression_algo - if self._compression_algo == "br" and brotli is not None: - body.write( - brotli.compress( - envelope.serialize(), quality=self._compression_level - ) - ) - else: # assume gzip as we sanitize the algo value in init - with gzip.GzipFile( - fileobj=body, mode="w", compresslevel=self._compression_level - ) as f: - envelope.serialize_into(f) - - return content_encoding, body - - def _get_httpcore_pool_options( - self: "Self", http2: bool = False - ) -> "Dict[str, Any]": - """Shared pool options for httpcore-based transports (Http2 and Async).""" - options: "Dict[str, Any]" = { - "http2": http2, - "retries": 3, - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - ssl_context = ssl.create_default_context() - ssl_context.load_verify_locations( - self.options["ca_certs"] - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") - key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") - if cert_file is not None: - ssl_context.load_cert_chain(cert_file, key_file) - - options["ssl_context"] = ssl_context - return options - - def _resolve_proxy(self: "Self") -> "Optional[str]": - """Resolve proxy URL from options and environment. Returns proxy URL or None.""" - if self.parsed_dsn is None: - return None - - no_proxy = self._in_no_proxy(self.parsed_dsn) - proxy = None - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - return proxy or None - - @property - def _timeout_extensions(self: "Self") -> "Dict[str, Any]": - return { - "timeout": { - "pool": self.TIMEOUT, - "connect": self.TIMEOUT, - "write": self.TIMEOUT, - "read": self.TIMEOUT, - } - } - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - raise NotImplementedError() - - def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: - no_proxy = getproxies().get("no") - if not no_proxy: - return False - for host in no_proxy.split(","): - host = host.strip() - if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): - return True - return False - - def _make_pool( - self: "Self", - ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - raise NotImplementedError() - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": - raise NotImplementedError() - - def kill(self: "Self") -> None: - logger.debug("Killing HTTP transport") - self._worker.kill() - - -# Keep BaseHttpTransport as an alias for backwards compatibility -# and for the sync transport implementation -class BaseHttpTransport(HttpTransportCore): - """The base HTTP transport (synchronous).""" - - def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - response.close() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return BackgroundWorker(queue_size=options["transport_queue_size"]) - - def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def capture_envelope( - self, - envelope: "Envelope", - ) -> None: - def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - self._send_envelope(envelope) - self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - self._worker.flush(timeout, callback) - - @staticmethod - def _warn_hub_cls() -> None: - """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" - warnings.warn( - "The `hub_cls` attribute is deprecated and will be removed in a future release.", - DeprecationWarning, - stacklevel=3, - ) - - @property - def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - return self._hub_cls - - @hub_cls.setter - def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - self._hub_cls = value - - -class HttpTransport(BaseHttpTransport): - if TYPE_CHECKING: - _pool: "Union[PoolManager, ProxyManager]" - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - num_pools = self.options.get("_experiments", {}).get("transport_num_pools") - options = { - "num_pools": 2 if num_pools is None else int(num_pools), - "cert_reqs": "CERT_REQUIRED", - "timeout": urllib3.Timeout(total=self.TIMEOUT), - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if self.options["keep_alive"]: - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - options["ca_certs"] = ( - self.options["ca_certs"] # User-provided bundle from the SDK init - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - - options["cert_file"] = self.options["cert_file"] or os.environ.get( - "CLIENT_CERT_FILE" - ) - options["key_file"] = self.options["key_file"] or os.environ.get( - "CLIENT_KEY_FILE" - ) - - return options - - def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = None - no_proxy = self._in_no_proxy(self.parsed_dsn) - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - use_socks_proxy = True - try: - # Check if PySocks dependency is available - from urllib3.contrib.socks import SOCKSProxyManager - except ImportError: - use_socks_proxy = False - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", - proxy, - ) - - if use_socks_proxy: - return SOCKSProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - else: - return urllib3.ProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "urllib3.BaseHTTPResponse": - return self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - body=body, - headers=headers, - ) - - -class AsyncHttpTransport(HttpTransportCore): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - if not ASYNC_TRANSPORT_AVAILABLE: - raise RuntimeError( - "AsyncHttpTransport requires httpcore[asyncio]. " - "Install it with: pip install sentry-sdk[asyncio]" - ) - super().__init__(options) - # Requires event loop at init time - self.loop = asyncio.get_running_loop() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return AsyncWorker(queue_size=options["transport_queue_size"]) - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - async def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - await self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - async def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = await self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - await response.aclose() - - async def _request( # type: ignore[override] - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - return await self._pool.request( # type: ignore[misc,unused-ignore] - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - - async def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def _capture_envelope(self: "Self", envelope: "Envelope") -> None: - async def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - await self._send_envelope(envelope) - await self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - # Synchronous entry point - if self.loop and self.loop.is_running(): - self.loop.call_soon_threadsafe(self._capture_envelope, envelope) - else: - # The event loop is no longer running - logger.warning("Async Transport is not running in an event loop.") - self.on_dropped_event("internal_sdk_error") - for item in envelope.items: - self.record_lost_event("internal_sdk_error", item=item) - - def flush( # type: ignore[override] - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> "Optional[asyncio.Task[None]]": - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] - return None - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=HTTP2_ENABLED - and self.parsed_dsn is not None - and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - socks_opts = opts.copy() - if "socket_options" in socks_opts: - socket_options = socks_opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) - - return httpcore.AsyncConnectionPool(**opts) - - def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] - logger.debug("Killing HTTP transport") - self._worker.kill() - try: - # Return the pool cleanup task so caller can await it if needed - with mark_sentry_task_internal(): - return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] - except RuntimeError: - logger.warning("Event loop not running, aborting kill.") - return None - - -if not HTTP2_ENABLED: - # Sorry, no Http2Transport for you - class Http2Transport(HttpTransport): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - super().__init__(options) - logger.warning( - "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." - ) - -else: - - class Http2Transport(BaseHttpTransport): # type: ignore - """The HTTP2 transport based on httpcore.""" - - TIMEOUT = 15 - - if TYPE_CHECKING: - _pool: """Union[ - httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool - ]""" - - def _get_header_value( - self: "Self", response: "httpcore.Response", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - response = self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - return response - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - if "socket_options" in opts: - socket_options = opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.SOCKSProxy(proxy_url=proxy, **opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.HTTPProxy(proxy_url=proxy, **opts) - - return httpcore.ConnectionPool(**opts) - - -class _EnvelopePrinterTransport(Transport): - """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" - - def __init__(self, transport: "Transport") -> None: - Transport.__init__(self, options=transport.options) - self._inner = transport - self.parsed_dsn = transport.parsed_dsn - - self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") - self.envelope_logger.setLevel(logging.INFO) - self.envelope_logger.propagate = False - if not self.envelope_logger.handlers: - handler = logging.StreamHandler() - handler.setLevel(logging.INFO) - handler.setFormatter(logging.Formatter("%(message)s")) - self.envelope_logger.addHandler(handler) - - @property # type: ignore[misc] - def __class__(self) -> type: - return self._inner.__class__ - - def capture_envelope(self, envelope: "Envelope") -> None: - try: - self.envelope_logger.info("--- Sentry Envelope ---") - self.envelope_logger.info( - "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) - ) - for item in envelope.items: - self.envelope_logger.info(" Item type: %s", item.type) - self.envelope_logger.info( - " Item headers: %s", - json.dumps(item.headers, indent=2, default=str), - ) - try: - payload = json.loads(item.get_bytes()) - self.envelope_logger.info( - " Payload:\n%s", - json.dumps(payload, indent=2, default=str), - ) - except (ValueError, TypeError): - self.envelope_logger.info( - " Payload: ", - len(item.get_bytes()), - ) - self.envelope_logger.info("--- End Envelope ---") - except Exception: - pass - - self._inner.capture_envelope(envelope) - - def flush( - self, - timeout: float, - callback: "Optional[Any]" = None, - ) -> "Any": - return self._inner.flush(timeout, callback) - - def kill(self) -> "Any": - return self._inner.kill() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - self._inner.record_lost_event(reason, data_category, item, quantity=quantity) - - def is_healthy(self) -> bool: - return self._inner.is_healthy() - - def __getattr__(self, name: str) -> "Any": - return getattr(self._inner, name) - - -class _FunctionTransport(Transport): - """ - DEPRECATED: Users wishing to provide a custom transport should subclass - the Transport class, rather than providing a function. - """ - - def __init__( - self, - func: "Callable[[Event], None]", - ) -> None: - Transport.__init__(self) - self._func = func - - def capture_event( - self, - event: "Event", - ) -> None: - self._func(event) - return None - - def capture_envelope(self, envelope: "Envelope") -> None: - # Since function transports expect to be called with an event, we need - # to iterate over the envelope and call the function for each event, via - # the deprecated capture_event method. - event = envelope.get_event() - if event is not None: - self.capture_event(event) - - -def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": - ref_transport = options["transport"] - - use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) - use_async_transport = options.get("_experiments", {}).get("transport_async", False) - async_integration = any( - integration.__class__.__name__ == "AsyncioIntegration" - for integration in options.get("integrations") or [] - ) - - # By default, we use the http transport class - transport_cls: "Type[Transport]" = ( - Http2Transport if use_http2_transport else HttpTransport - ) - - if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: - try: - asyncio.get_running_loop() - if async_integration: - if use_http2_transport: - logger.warning( - "HTTP/2 transport is not supported with async transport. " - "Ignoring transport_http2 experiment." - ) - transport_cls = AsyncHttpTransport - else: - logger.warning( - "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." - ) - except RuntimeError: - # No event loop running, fall back to sync transport - logger.warning("No event loop running, falling back to sync transport.") - elif use_async_transport: - logger.warning( - "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." - ) - - transport: "Optional[Transport]" = None - - if isinstance(ref_transport, Transport): - transport = ref_transport - elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): - transport_cls = ref_transport - elif callable(ref_transport): - warnings.warn( - "Function transports are deprecated and will be removed in a future release." - "Please provide a Transport instance or subclass, instead.", - DeprecationWarning, - stacklevel=2, - ) - transport = _FunctionTransport(ref_transport) - - # if a transport class is given only instantiate it if the dsn is not - # empty or None - if transport is None and options["dsn"]: - transport = transport_cls(options) - - if transport is not None and os.environ.get( - "SENTRY_PRINT_ENVELOPES", "" - ).lower() in ("1", "true", "yes"): - transport = _EnvelopePrinterTransport(transport) - - return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py deleted file mode 100644 index dff91e3719..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/types.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This module contains type definitions for the Sentry SDK's public API. -The types are re-exported from the internal module `sentry_sdk._types`. - -Disclaimer: Since types are a form of documentation, type definitions -may change in minor releases. Removing a type would be considered a -breaking change, and so we will only remove type definitions in major -releases. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Re-export types to make them available in the public API - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - EventDataCategory, - Hint, - Log, - Metric, - MonitorConfig, - SamplingContext, - ) -else: - from typing import Any - - # The lines below allow the types to be imported from outside `if TYPE_CHECKING` - # guards. The types in this module are only intended to be used for type hints. - Breadcrumb = Any - BreadcrumbHint = Any - Event = Any - EventDataCategory = Any - Hint = Any - Log = Any - MonitorConfig = Any - SamplingContext = Any - Metric = Any - - -__all__ = ( - "Breadcrumb", - "BreadcrumbHint", - "Event", - "EventDataCategory", - "Hint", - "Log", - "MonitorConfig", - "SamplingContext", - "Metric", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py deleted file mode 100644 index 0963015351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/utils.py +++ /dev/null @@ -1,2178 +0,0 @@ -import base64 -import copy -import json -import linecache -import logging -import math -import os -import random -import re -import subprocess -import sys -import threading -import time -from collections import namedtuple -from contextlib import contextmanager -from datetime import datetime, timezone -from decimal import Decimal -from functools import partial, partialmethod, wraps -from numbers import Real -from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit - -try: - # Python 3.11 - from builtins import BaseExceptionGroup -except ImportError: - # Python 3.10 and below - BaseExceptionGroup = None # type: ignore - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._compat import PY37 -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue -from sentry_sdk.consts import ( - DEFAULT_ADD_FULL_STACK, - DEFAULT_MAX_STACK_FRAMES, - EndpointType, -) - -if TYPE_CHECKING: - from types import FrameType, TracebackType - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Iterator, - List, - NoReturn, - Optional, - ParamSpec, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from gevent.hub import Hub - - from sentry_sdk._types import ( - AttributeValue, - Event, - ExcInfo, - Hint, - Log, - Metric, - SerializedAttributeValue, - SpanJSON, - ) - - P = ParamSpec("P") - R = TypeVar("R") - - -epoch = datetime(1970, 1, 1) - -# The logger is created here but initialized in the debug support module -logger = logging.getLogger("sentry_sdk.errors") - -_installed_modules = None - -BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") - -FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) -TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) - -MAX_STACK_FRAMES = 2000 -"""Maximum number of stack frames to send to Sentry. - -If we have more than this number of stack frames, we will stop processing -the stacktrace to avoid getting stuck in a long-lasting loop. This value -exceeds the default sys.getrecursionlimit() of 1000, so users will only -be affected by this limit if they have a custom recursion limit. -""" - - -def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": - """Casts an ENV variable value to boolean using the constants defined above. - In strict mode, it may return None if the value doesn't match any of the predefined values. - """ - normalized = str(value).lower() if value is not None else None - - if normalized in FALSY_ENV_VALUES: - return False - - if normalized in TRUTHY_ENV_VALUES: - return True - - return None if strict else bool(value) - - -def json_dumps(data: "Any") -> bytes: - """Serialize data into a compact JSON representation encoded as UTF-8.""" - return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") - - -def get_git_revision() -> "Optional[str]": - try: - with open(os.path.devnull, "w+") as null: - # prevent command prompt windows from popping up on windows - startupinfo = None - if sys.platform == "win32" or sys.platform == "cygwin": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - revision = ( - subprocess.Popen( - ["git", "rev-parse", "HEAD"], - startupinfo=startupinfo, - stdout=subprocess.PIPE, - stderr=null, - stdin=null, - ) - .communicate()[0] - .strip() - .decode("utf-8") - ) - except (OSError, IOError, FileNotFoundError): - return None - - return revision - - -def get_default_release() -> "Optional[str]": - """Try to guess a default release.""" - release = os.environ.get("SENTRY_RELEASE") - if release: - return release - - release = get_git_revision() - if release: - return release - - for var in ( - "HEROKU_BUILD_COMMIT", - "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility - "SOURCE_VERSION", - "CODEBUILD_RESOLVED_SOURCE_VERSION", - "CIRCLE_SHA1", - "GAE_DEPLOYMENT_ID", - "K_REVISION", - ): - release = os.environ.get(var) - if release: - return release - return None - - -def get_sdk_name(installed_integrations: "List[str]") -> str: - """Return the SDK name including the name of the used web framework.""" - - # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier - # here because if django is not installed the integration is not accessible. - framework_integrations = [ - "django", - "flask", - "fastapi", - "bottle", - "falcon", - "quart", - "sanic", - "starlette", - "litestar", - "starlite", - "chalice", - "serverless", - "pyramid", - "tornado", - "aiohttp", - "aws_lambda", - "gcp", - "beam", - "asgi", - "wsgi", - ] - - for integration in framework_integrations: - if integration in installed_integrations: - return "sentry.python.{}".format(integration) - - return "sentry.python" - - -class CaptureInternalException: - __slots__ = () - - def __enter__(self) -> "ContextManager[Any]": - return self - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> bool: - if ty is not None and value is not None: - capture_internal_exception((ty, value, tb)) - - return True - - -_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() - - -def capture_internal_exceptions() -> "ContextManager[Any]": - return _CAPTURE_INTERNAL_EXCEPTION - - -def capture_internal_exception(exc_info: "ExcInfo") -> None: - """ - Capture an exception that is likely caused by a bug in the SDK - itself. - - These exceptions do not end up in Sentry and are just logged instead. - """ - if sentry_sdk.get_client().is_active(): - logger.error("Internal error in sentry_sdk", exc_info=exc_info) - - -def to_timestamp(value: "datetime") -> float: - return (value - epoch).total_seconds() - - -def format_timestamp(value: "datetime") -> str: - """Formats a timestamp in RFC 3339 format. - - Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. - """ - utctime = value.astimezone(timezone.utc) - - # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for - # several years now), and isoformat is slightly different. - return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - -ISO_TZ_SEPARATORS = frozenset(("+", "-")) - - -def datetime_from_isoformat(value: str) -> "datetime": - try: - result = datetime.fromisoformat(value) - except (AttributeError, ValueError): - # py 3.6 - timestamp_format = ( - "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" - ) - if value.endswith("Z"): - value = value[:-1] + "+0000" - - if value[-6] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - value = value[:-3] + value[-2:] - elif value[-5] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - - result = datetime.strptime(value, timestamp_format) - return result.astimezone(timezone.utc) - - -def event_hint_with_exc_info( - exc_info: "Optional[ExcInfo]" = None, -) -> "Dict[str, Optional[ExcInfo]]": - """Creates a hint with the exc info filled in.""" - if exc_info is None: - exc_info = sys.exc_info() - else: - exc_info = exc_info_from_error(exc_info) - if exc_info[0] is None: - exc_info = None - return {"exc_info": exc_info} - - -class BadDsn(ValueError): - """Raised on invalid DSNs.""" - - -class Dsn: - """Represents a DSN.""" - - ORG_ID_REGEX = re.compile(r"^o(\d+)\.") - - def __init__( - self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None - ) -> None: - if isinstance(value, Dsn): - self.__dict__ = dict(value.__dict__) - return - parts = urlsplit(str(value)) - - if parts.scheme not in ("http", "https"): - raise BadDsn("Unsupported scheme %r" % parts.scheme) - self.scheme = parts.scheme - - if parts.hostname is None: - raise BadDsn("Missing hostname") - - self.host = parts.hostname - - if org_id is not None: - self.org_id: "Optional[str]" = org_id - else: - org_id_match = Dsn.ORG_ID_REGEX.match(self.host) - self.org_id = org_id_match.group(1) if org_id_match else None - - if parts.port is None: - self.port: int = self.scheme == "https" and 443 or 80 - else: - self.port = parts.port - - if not parts.username: - raise BadDsn("Missing public key") - - self.public_key = parts.username - self.secret_key = parts.password - - path = parts.path.rsplit("/", 1) - - try: - self.project_id = str(int(path.pop())) - except (ValueError, TypeError): - raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) - - self.path = "/".join(path) + "/" - - @property - def netloc(self) -> str: - """The netloc part of a DSN.""" - rv = self.host - if (self.scheme, self.port) not in (("http", 80), ("https", 443)): - rv = "%s:%s" % (rv, self.port) - return rv - - def to_auth(self, client: "Optional[Any]" = None) -> "Auth": - """Returns the auth info object for this dsn.""" - return Auth( - scheme=self.scheme, - host=self.netloc, - path=self.path, - project_id=self.project_id, - public_key=self.public_key, - secret_key=self.secret_key, - client=client, - ) - - def __str__(self) -> str: - return "%s://%s%s@%s%s%s" % ( - self.scheme, - self.public_key, - self.secret_key and "@" + self.secret_key or "", - self.netloc, - self.path, - self.project_id, - ) - - -class Auth: - """Helper object that represents the auth info.""" - - def __init__( - self, - scheme: str, - host: str, - project_id: str, - public_key: str, - secret_key: "Optional[str]" = None, - version: int = 7, - client: "Optional[Any]" = None, - path: str = "/", - ) -> None: - self.scheme = scheme - self.host = host - self.path = path - self.project_id = project_id - self.public_key = public_key - self.secret_key = secret_key - self.version = version - self.client = client - - def get_api_url( - self, - type: "EndpointType" = EndpointType.ENVELOPE, - ) -> str: - """Returns the API url for storing events.""" - return "%s://%s%sapi/%s/%s/" % ( - self.scheme, - self.host, - self.path, - self.project_id, - type.value, - ) - - def to_header(self) -> str: - """Returns the auth header a string.""" - rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] - if self.client is not None: - rv.append(("sentry_client", self.client)) - if self.secret_key is not None: - rv.append(("sentry_secret", self.secret_key)) - return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) - - -def get_type_name(cls: "Optional[type]") -> "Optional[str]": - return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) - - -def get_type_module(cls: "Optional[type]") -> "Optional[str]": - mod = getattr(cls, "__module__", None) - if mod not in (None, "builtins", "__builtins__"): - return mod - return None - - -def should_hide_frame(frame: "FrameType") -> bool: - try: - mod = frame.f_globals["__name__"] - if mod.startswith("sentry_sdk."): - return True - except (AttributeError, KeyError): - pass - - for flag_name in "__traceback_hide__", "__tracebackhide__": - try: - if frame.f_locals[flag_name]: - return True - except Exception: - pass - - return False - - -def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": - tb_: "Optional[TracebackType]" = tb - while tb_ is not None: - if not should_hide_frame(tb_.tb_frame): - yield tb_ - tb_ = tb_.tb_next - - -def get_lines_from_file( - filename: str, - lineno: int, - max_length: "Optional[int]" = None, - loader: "Optional[Any]" = None, - module: "Optional[str]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - context_lines = 5 - source = None - if loader is not None and hasattr(loader, "get_source"): - try: - source_str: "Optional[str]" = loader.get_source(module) - except (ImportError, IOError): - source_str = None - if source_str is not None: - source = source_str.splitlines() - - if source is None: - try: - source = linecache.getlines(filename) - except (OSError, IOError): - return [], None, [] - - if not source: - return [], None, [] - - lower_bound = max(0, lineno - context_lines) - upper_bound = min(lineno + 1 + context_lines, len(source)) - - try: - pre_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[lower_bound:lineno] - ] - context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) - post_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[(lineno + 1) : upper_bound] - ] - return pre_context, context_line, post_context - except IndexError: - # the file may have changed since it was loaded into memory - return [], None, [] - - -def get_source_context( - frame: "FrameType", - tb_lineno: "Optional[int]", - max_value_length: "Optional[int]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - try: - abs_path: "Optional[str]" = frame.f_code.co_filename - except Exception: - abs_path = None - try: - module = frame.f_globals["__name__"] - except Exception: - return [], None, [] - try: - loader = frame.f_globals["__loader__"] - except Exception: - loader = None - - if tb_lineno is not None and abs_path: - lineno = tb_lineno - 1 - return get_lines_from_file( - abs_path, lineno, max_value_length, loader=loader, module=module - ) - - return [], None, [] - - -def safe_str(value: "Any") -> str: - try: - return str(value) - except Exception: - return safe_repr(value) - - -def safe_repr(value: "Any") -> str: - try: - return repr(value) - except Exception: - return "" - - -def filename_for_module( - module: "Optional[str]", abs_path: "Optional[str]" -) -> "Optional[str]": - if not abs_path or not module: - return abs_path - - try: - if abs_path.endswith(".pyc"): - abs_path = abs_path[:-1] - - base_module = module.split(".", 1)[0] - if base_module == module: - return os.path.basename(abs_path) - - base_module_path = sys.modules[base_module].__file__ - if not base_module_path: - return abs_path - - return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( - os.sep - ) - except Exception: - return abs_path - - -def serialize_frame( - frame: "FrameType", - tb_lineno: "Optional[int]" = None, - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, -) -> "Dict[str, Any]": - f_code = getattr(frame, "f_code", None) - if not f_code: - abs_path = None - function = None - else: - abs_path = frame.f_code.co_filename - function = frame.f_code.co_name - try: - module = frame.f_globals["__name__"] - except Exception: - module = None - - if tb_lineno is None: - tb_lineno = frame.f_lineno - - try: - os_abs_path = os.path.abspath(abs_path) if abs_path else None - except Exception: - os_abs_path = None - - rv: "Dict[str, Any]" = { - "filename": filename_for_module(module, abs_path) or None, - "abs_path": os_abs_path, - "function": function or "", - "module": module, - "lineno": tb_lineno, - } - - if include_source_context: - rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( - frame, tb_lineno, max_value_length - ) - - if include_local_variables: - from sentry_sdk.serializer import serialize - - rv["vars"] = serialize( - dict(frame.f_locals), is_vars=True, custom_repr=custom_repr - ) - - return rv - - -def current_stacktrace( - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, -) -> "Dict[str, Any]": - __tracebackhide__ = True - frames = [] - - f: "Optional[FrameType]" = sys._getframe() - while f is not None: - if not should_hide_frame(f): - frames.append( - serialize_frame( - f, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - ) - ) - f = f.f_back - - frames.reverse() - - return {"frames": frames} - - -def get_errno(exc_value: BaseException) -> "Optional[Any]": - return getattr(exc_value, "errno", None) - - -def get_error_message(exc_value: "Optional[BaseException]") -> str: - message: str = safe_str( - getattr(exc_value, "message", "") - or getattr(exc_value, "detail", "") - or safe_str(exc_value) - ) - - # __notes__ should be a list of strings when notes are added - # via add_note, but can be anything else if __notes__ is set - # directly. We only support strings in __notes__, since that - # is the correct use. - notes: object = getattr(exc_value, "__notes__", None) - if isinstance(notes, list) and len(notes) > 0: - message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) - - return message - - -def single_exception_from_error_tuple( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: "Optional[int]" = None, - parent_id: "Optional[int]" = None, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "Dict[str, Any]": - """ - Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - """ - exception_value: "Dict[str, Any]" = {} - exception_value["mechanism"] = ( - mechanism.copy() if mechanism else {"type": "generic", "handled": True} - ) - if exception_id is not None: - exception_value["mechanism"]["exception_id"] = exception_id - - if exc_value is not None: - errno = get_errno(exc_value) - else: - errno = None - - if errno is not None: - exception_value["mechanism"].setdefault("meta", {}).setdefault( - "errno", {} - ).setdefault("number", errno) - - if source is not None: - exception_value["mechanism"]["source"] = source - - is_root_exception = exception_id == 0 - if not is_root_exception and parent_id is not None: - exception_value["mechanism"]["parent_id"] = parent_id - exception_value["mechanism"]["type"] = "chained" - - if is_root_exception and "type" not in exception_value["mechanism"]: - exception_value["mechanism"]["type"] = "generic" - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - if is_exception_group: - exception_value["mechanism"]["is_exception_group"] = True - - exception_value["module"] = get_type_module(exc_type) - exception_value["type"] = get_type_name(exc_type) - exception_value["value"] = get_error_message(exc_value) - - if client_options is None: - include_local_variables = True - include_source_context = True - max_value_length = None # fallback - custom_repr = None - else: - include_local_variables = client_options["include_local_variables"] - include_source_context = client_options["include_source_context"] - max_value_length = client_options["max_value_length"] - custom_repr = client_options.get("custom_repr") - - frames: "List[Dict[str, Any]]" = [ - serialize_frame( - tb.tb_frame, - tb_lineno=tb.tb_lineno, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - custom_repr=custom_repr, - ) - # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on - # processing a super-long stacktrace. - for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) - ] - - if len(frames) > MAX_STACK_FRAMES: - # If we have more frames than the limit, we remove the stacktrace completely. - # We don't trim the stacktrace here because we have not processed the whole - # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would - # intelligently trim by removing frames in the middle of the stacktrace, but - # since we don't have the whole stacktrace, we can't do that. Instead, we - # drop the entire stacktrace. - exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( - value=None - ) - - elif frames: - if not full_stack: - new_frames = frames - else: - new_frames = merge_stack_frames(frames, full_stack, client_options) - - exception_value["stacktrace"] = {"frames": new_frames} - - return exception_value - - -HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") - -if HAS_CHAINED_EXCEPTIONS: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - exc_type, exc_value, tb = exc_info - - seen_exceptions = [] - seen_exception_ids: "Set[int]" = set() - - while ( - exc_type is not None - and exc_value is not None - and id(exc_value) not in seen_exception_ids - ): - yield exc_type, exc_value, tb - - # Avoid hashing random types we don't know anything - # about. Use the list to keep a ref so that the `id` is - # not used for another object. - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - if exc_value.__suppress_context__: - cause = exc_value.__cause__ - else: - cause = exc_value.__context__ - if cause is None: - break - exc_type = type(cause) - exc_value = cause - tb = getattr(cause, "__traceback__", None) - -else: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - yield exc_info - - -def exceptions_from_error( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: int = 0, - parent_id: int = 0, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, - seen_exceptions: "Optional[list[BaseException]]" = None, - seen_exception_ids: "Optional[Set[int]]" = None, -) -> "Tuple[int, List[Dict[str, Any]]]": - """ - Creates the list of exceptions. - This can include chained exceptions and exceptions from an ExceptionGroup. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - - Args: - exception_id (int): - - Sequential counter for assigning ``mechanism.exception_id`` - to each processed exception. Is NOT the result of calling `id()` on the exception itself. - - parent_id (int): - - The ``mechanism.exception_id`` of the parent exception. - - Written into ``mechanism.parent_id`` in the event payload so Sentry can - reconstruct the exception tree. - - Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` - values for cycle detection. - """ - - if seen_exception_ids is None: - seen_exception_ids = set() - - if seen_exceptions is None: - seen_exceptions = [] - - if exc_value is not None and id(exc_value) in seen_exception_ids: - return (exception_id, []) - - if exc_value is not None: - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - parent = single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source=source, - full_stack=full_stack, - ) - exceptions = [parent] - - parent_id = exception_id - exception_id += 1 - - should_supress_context = ( - hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore - ) - if should_supress_context: - # Add direct cause. - # The field `__cause__` is set when raised with the exception (using the `from` keyword). - exception_has_cause = ( - exc_value - and hasattr(exc_value, "__cause__") - and exc_value.__cause__ is not None - ) - if exception_has_cause: - cause = exc_value.__cause__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(cause), - exc_value=cause, - tb=getattr(cause, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__cause__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - else: - # Add indirect cause. - # The field `__context__` is assigned if another exception occurs while handling the exception. - exception_has_content = ( - exc_value - and hasattr(exc_value, "__context__") - and exc_value.__context__ is not None - ) - if exception_has_content: - context = exc_value.__context__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(context), - exc_value=context, - tb=getattr(context, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__context__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - # Add exceptions from an ExceptionGroup. - is_exception_group = exc_value and hasattr(exc_value, "exceptions") - if is_exception_group: - for idx, e in enumerate(exc_value.exceptions): # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(e), - exc_value=e, - tb=getattr(e, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source="exceptions[%s]" % idx, - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - return (exception_id, exceptions) - - -def exceptions_from_error_tuple( - exc_info: "ExcInfo", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "List[Dict[str, Any]]": - exc_type, exc_value, tb = exc_info - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - - if is_exception_group: - (_, exceptions) = exceptions_from_error( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=0, - parent_id=0, - full_stack=full_stack, - ) - - else: - exceptions = [] - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - exceptions.append( - single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - full_stack=full_stack, - ) - ) - - exceptions.reverse() - - return exceptions - - -def to_string(value: str) -> str: - try: - return str(value) - except UnicodeDecodeError: - return repr(value)[1:-1] - - -def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": - if "stacktrace" in event: - yield event["stacktrace"] - if "threads" in event: - for thread in event["threads"].get("values") or (): - if "stacktrace" in thread: - yield thread["stacktrace"] - if "exception" in event: - for exception in event["exception"].get("values") or (): - if isinstance(exception, dict) and "stacktrace" in exception: - yield exception["stacktrace"] - - -def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - for frame in stacktrace.get("frames") or (): - yield frame - - -def handle_in_app( - event: "Event", - in_app_exclude: "Optional[List[str]]" = None, - in_app_include: "Optional[List[str]]" = None, - project_root: "Optional[str]" = None, -) -> "Event": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - set_in_app_in_frames( - stacktrace.get("frames"), - in_app_exclude=in_app_exclude, - in_app_include=in_app_include, - project_root=project_root, - ) - - return event - - -def set_in_app_in_frames( - frames: "Any", - in_app_exclude: "Optional[List[str]]", - in_app_include: "Optional[List[str]]", - project_root: "Optional[str]" = None, -) -> "Optional[Any]": - if not frames: - return None - - for frame in frames: - # if frame has already been marked as in_app, skip it - current_in_app = frame.get("in_app") - if current_in_app is not None: - continue - - module = frame.get("module") - - # check if module in frame is in the list of modules to include - if _module_in_list(module, in_app_include): - frame["in_app"] = True - continue - - # check if module in frame is in the list of modules to exclude - if _module_in_list(module, in_app_exclude): - frame["in_app"] = False - continue - - # if frame has no abs_path, skip further checks - abs_path = frame.get("abs_path") - if abs_path is None: - continue - - if _is_external_source(abs_path): - frame["in_app"] = False - continue - - if _is_in_project_root(abs_path, project_root): - frame["in_app"] = True - continue - - return frames - - -def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": - if isinstance(error, tuple) and len(error) == 3: - exc_type, exc_value, tb = error - elif isinstance(error, BaseException): - tb = getattr(error, "__traceback__", None) - if tb is not None: - exc_type = type(error) - exc_value = error - else: - exc_type, exc_value, tb = sys.exc_info() - if exc_value is not error: - tb = None - exc_value = error - exc_type = type(error) - - else: - raise ValueError("Expected Exception object to report, got %s!" % type(error)) - - exc_info = (exc_type, exc_value, tb) - - if TYPE_CHECKING: - # This cast is safe because exc_type and exc_value are either both - # None or both not None. - exc_info = cast(ExcInfo, exc_info) - - return exc_info - - -def merge_stack_frames( - frames: "List[Dict[str, Any]]", - full_stack: "List[Dict[str, Any]]", - client_options: "Optional[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Add the missing frames from full_stack to frames and return the merged list. - """ - frame_ids = { - ( - frame["abs_path"], - frame["context_line"], - frame["lineno"], - frame["function"], - ) - for frame in frames - } - - new_frames = [ - stackframe - for stackframe in full_stack - if ( - stackframe["abs_path"], - stackframe["context_line"], - stackframe["lineno"], - stackframe["function"], - ) - not in frame_ids - ] - new_frames.extend(frames) - - # Limit the number of frames - max_stack_frames = ( - client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) - if client_options - else None - ) - if max_stack_frames is not None: - new_frames = new_frames[len(new_frames) - max_stack_frames :] - - return new_frames - - -def event_from_exception( - exc_info: "Union[BaseException, ExcInfo]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, -) -> "Tuple[Event, Dict[str, Any]]": - exc_info = exc_info_from_error(exc_info) - hint = event_hint_with_exc_info(exc_info) - - if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): - full_stack = current_stacktrace( - include_local_variables=client_options["include_local_variables"], - max_value_length=client_options["max_value_length"], - )["frames"] - else: - full_stack = None - - return ( - { - "level": "error", - "exception": { - "values": exceptions_from_error_tuple( - exc_info, client_options, mechanism, full_stack - ) - }, - }, - hint, - ) - - -def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: - if name is None: - return False - - if not items: - return False - - for item in items: - if item == name or name.startswith(item + "."): - return True - - return False - - -def _is_external_source(abs_path: "Optional[str]") -> bool: - # check if frame is in 'site-packages' or 'dist-packages' - if abs_path is None: - return False - - external_source = ( - re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None - ) - return external_source - - -def _is_in_project_root( - abs_path: "Optional[str]", project_root: "Optional[str]" -) -> bool: - if abs_path is None or project_root is None: - return False - - # check if path is in the project root - if abs_path.startswith(project_root): - return True - - return False - - -def _truncate_by_bytes(string: str, max_bytes: int) -> str: - """ - Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. - """ - truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") - - return truncated + "..." - - -def _get_size_in_bytes(value: str) -> "Optional[int]": - try: - return len(value.encode("utf-8")) - except (UnicodeEncodeError, UnicodeDecodeError): - return None - - -def strip_string( - value: str, max_length: "Optional[int]" = None -) -> "Union[AnnotatedValue, str]": - if not value or max_length is None: - return value - - byte_size = _get_size_in_bytes(value) - text_size = len(value) - - if byte_size is not None and byte_size > max_length: - # truncate to max_length bytes, preserving code points - truncated_value = _truncate_by_bytes(value, max_length) - elif text_size is not None and text_size > max_length: - # fallback to truncating by string length - truncated_value = value[: max_length - 3] + "..." - else: - return value - - return AnnotatedValue( - value=truncated_value, - metadata={ - "len": byte_size or text_size, - "rem": [["!limit", "x", max_length - 3, max_length]], - }, - ) - - -def parse_version(version: str) -> "Optional[Tuple[int, ...]]": - """ - Parses a version string into a tuple of integers. - This uses the parsing loging from PEP 440: - https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions - """ - VERSION_PATTERN = r""" # noqa: N806 - v? - (?: - (?:(?P[0-9]+)!)? # epoch - (?P[0-9]+(?:\.[0-9]+)*) # release segment - (?P
                                          # pre-release
-                [-_\.]?
-                (?P(a|b|c|rc|alpha|beta|pre|preview))
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-            (?P                                         # post release
-                (?:-(?P[0-9]+))
-                |
-                (?:
-                    [-_\.]?
-                    (?Ppost|rev|r)
-                    [-_\.]?
-                    (?P[0-9]+)?
-                )
-            )?
-            (?P                                          # dev release
-                [-_\.]?
-                (?Pdev)
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-        )
-        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
-    """
-
-    pattern = re.compile(
-        r"^\s*" + VERSION_PATTERN + r"\s*$",
-        re.VERBOSE | re.IGNORECASE,
-    )
-
-    try:
-        release = pattern.match(version).groupdict()["release"]  # type: ignore
-        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
-    except (TypeError, ValueError, AttributeError):
-        return None
-
-    return release_tuple
-
-
-def _is_contextvars_broken() -> bool:
-    """
-    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
-    """
-    try:
-        import gevent
-        from gevent.monkey import is_object_patched
-
-        # Get the MAJOR and MINOR version numbers of Gevent
-        version_tuple = tuple(
-            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
-        )
-        if is_object_patched("threading", "local"):
-            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
-            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
-            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
-            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
-            # for contextvars, is able to patch both thread locals and contextvars, in
-            # that case, check if contextvars are effectively patched.
-            if (
-                # Gevent 20.9.0+
-                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
-                # Gevent 20.5.0+ or Python < 3.7
-                or (is_object_patched("contextvars", "ContextVar"))
-            ):
-                return False
-
-            return True
-    except ImportError:
-        pass
-
-    try:
-        import greenlet
-        from eventlet.patcher import is_monkey_patched  # type: ignore
-
-        greenlet_version = parse_version(greenlet.__version__)
-
-        if greenlet_version is None:
-            logger.error(
-                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
-            )
-            return False
-
-        if is_monkey_patched("thread") and greenlet_version < (0, 5):
-            return True
-    except ImportError:
-        pass
-
-    return False
-
-
-def _make_threadlocal_contextvars(local: type) -> type:
-    class ContextVar:
-        # Super-limited impl of ContextVar
-
-        def __init__(self, name: str, default: "Any" = None) -> None:
-            self._name = name
-            self._default = default
-            self._local = local()
-            self._original_local = local()
-
-        def get(self, default: "Any" = None) -> "Any":
-            return getattr(self._local, "value", default or self._default)
-
-        def set(self, value: "Any") -> "Any":
-            token = str(random.getrandbits(64))
-            original_value = self.get()
-            setattr(self._original_local, token, original_value)
-            self._local.value = value
-            return token
-
-        def reset(self, token: "Any") -> None:
-            self._local.value = getattr(self._original_local, token)
-            # delete the original value (this way it works in Python 3.6+)
-            del self._original_local.__dict__[token]
-
-    return ContextVar
-
-
-def _get_contextvars() -> "Tuple[bool, type]":
-    """
-    Figure out the "right" contextvars installation to use. Returns a
-    `contextvars.ContextVar`-like class with a limited API.
-
-    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
-    """
-    if not _is_contextvars_broken():
-        # aiocontextvars is a PyPI package that ensures that the contextvars
-        # backport (also a PyPI package) works with asyncio under Python 3.6
-        #
-        # Import it if available.
-        if sys.version_info < (3, 7):
-            # `aiocontextvars` is absolutely required for functional
-            # contextvars on Python 3.6.
-            try:
-                from aiocontextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-        else:
-            # On Python 3.7 contextvars are functional.
-            try:
-                from contextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-
-    # Fall back to basic thread-local usage.
-
-    from threading import local
-
-    return False, _make_threadlocal_contextvars(local)
-
-
-HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
-
-CONTEXTVARS_ERROR_MESSAGE = """
-
-With asyncio/ASGI applications, the Sentry SDK requires a functional
-installation of `contextvars` to avoid leaking scope/context data across
-requests.
-
-Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
-"""
-
-_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
-
-# These exceptions won't set the span status to error if they occur. Use
-# register_control_flow_exception to add to this list
-_control_flow_exception_classes: "set[type]" = set()
-
-
-def is_internal_task() -> bool:
-    return _is_sentry_internal_task.get()
-
-
-@contextmanager
-def mark_sentry_task_internal() -> "Generator[None, None, None]":
-    """Context manager to mark a task as Sentry internal."""
-    token = _is_sentry_internal_task.set(True)
-    try:
-        yield
-    finally:
-        _is_sentry_internal_task.reset(token)
-
-
-def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
-    func_qualname: "Optional[str]" = None
-
-    prefix, suffix = "", ""
-
-    if isinstance(func, partial) and hasattr(func.func, "__name__"):
-        prefix, suffix = "partial()"
-        func = func.func
-    else:
-        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
-        # https://github.com/python/cpython/pull/16600
-        partial_method = getattr(func, "_partialmethod", None) or getattr(
-            func, "__partialmethod__", None
-        )
-        if isinstance(partial_method, partialmethod):
-            prefix, suffix = "partialmethod()"
-            func = partial_method.func
-
-    if hasattr(func, "__qualname__"):
-        func_qualname = func.__qualname__
-    elif hasattr(func, "__name__"):
-        func_qualname = func.__name__
-
-    if func_qualname is not None:
-        if hasattr(func, "__module__") and isinstance(func.__module__, str):
-            func_qualname = func.__module__ + "." + func_qualname
-        func_qualname = prefix + func_qualname + suffix
-
-    return func_qualname
-
-
-def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    return qualname_from_function(func)
-
-
-disable_capture_event = ContextVar("disable_capture_event")
-
-
-class ServerlessTimeoutWarning(Exception):  # noqa: N818
-    """Raised when a serverless method is about to reach its timeout."""
-
-    pass
-
-
-class TimeoutThread(threading.Thread):
-    """Creates a Thread which runs (sleeps) for a time duration equal to
-    waiting_time and raises a custom ServerlessTimeout exception.
-    """
-
-    def __init__(
-        self,
-        waiting_time: float,
-        configured_timeout: int,
-        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
-        current_scope: "Optional[sentry_sdk.Scope]" = None,
-    ) -> None:
-        threading.Thread.__init__(self)
-        self.waiting_time = waiting_time
-        self.configured_timeout = configured_timeout
-
-        self.isolation_scope = isolation_scope
-        self.current_scope = current_scope
-
-        self._stop_event = threading.Event()
-
-    def stop(self) -> None:
-        self._stop_event.set()
-
-    def _capture_exception(self) -> "ExcInfo":
-        exc_info = sys.exc_info()
-
-        client = sentry_sdk.get_client()
-        event, hint = event_from_exception(
-            exc_info,
-            client_options=client.options,
-            mechanism={"type": "threading", "handled": False},
-        )
-        sentry_sdk.capture_event(event, hint=hint)
-
-        return exc_info
-
-    def run(self) -> None:
-        self._stop_event.wait(self.waiting_time)
-
-        if self._stop_event.is_set():
-            return
-
-        integer_configured_timeout = int(self.configured_timeout)
-
-        # Setting up the exact integer value of configured time(in seconds)
-        if integer_configured_timeout < self.configured_timeout:
-            integer_configured_timeout = integer_configured_timeout + 1
-
-        # Raising Exception after timeout duration is reached
-        if self.isolation_scope is not None and self.current_scope is not None:
-            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
-                with sentry_sdk.scope.use_scope(self.current_scope):
-                    try:
-                        raise ServerlessTimeoutWarning(
-                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                                integer_configured_timeout
-                            )
-                        )
-                    except Exception:
-                        reraise(*self._capture_exception())
-
-        raise ServerlessTimeoutWarning(
-            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                integer_configured_timeout
-            )
-        )
-
-
-def to_base64(original: str) -> "Optional[str]":
-    """
-    Convert a string to base64, via UTF-8. Returns None on invalid input.
-    """
-    base64_string = None
-
-    try:
-        utf8_bytes = original.encode("UTF-8")
-        base64_bytes = base64.b64encode(utf8_bytes)
-        base64_string = base64_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
-
-    return base64_string
-
-
-def from_base64(base64_string: str) -> "Optional[str]":
-    """
-    Convert a string from base64, via UTF-8. Returns None on invalid input.
-    """
-    utf8_string = None
-
-    try:
-        only_valid_chars = BASE64_ALPHABET.match(base64_string)
-        assert only_valid_chars
-
-        base64_bytes = base64_string.encode("UTF-8")
-        utf8_bytes = base64.b64decode(base64_bytes)
-        utf8_string = utf8_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning(
-            "Unable to decode {b64} from base64:".format(b64=base64_string), err
-        )
-
-    return utf8_string
-
-
-Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
-
-
-def sanitize_url(
-    url: str,
-    remove_authority: bool = True,
-    remove_query_values: bool = True,
-    split: bool = False,
-) -> "Union[str, Components]":
-    """
-    Removes the authority and query parameter values from a given URL.
-    """
-    parsed_url = urlsplit(url)
-    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
-
-    # strip username:password (netloc can be usr:pwd@example.com)
-    if remove_authority:
-        netloc_parts = parsed_url.netloc.split("@")
-        if len(netloc_parts) > 1:
-            netloc = "%s:%s@%s" % (
-                SENSITIVE_DATA_SUBSTITUTE,
-                SENSITIVE_DATA_SUBSTITUTE,
-                netloc_parts[-1],
-            )
-        else:
-            netloc = parsed_url.netloc
-    else:
-        netloc = parsed_url.netloc
-
-    # strip values from query string
-    if remove_query_values:
-        query_string = unquote(
-            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
-        )
-    else:
-        query_string = parsed_url.query
-
-    components = Components(
-        scheme=parsed_url.scheme,
-        netloc=netloc,
-        query=query_string,
-        path=parsed_url.path,
-        fragment=parsed_url.fragment,
-    )
-
-    if split:
-        return components
-    else:
-        return urlunsplit(components)
-
-
-ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
-
-
-def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
-    """
-    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
-    parameters will be sanitized to remove sensitive data. The autority (username and password)
-    in the URL will always be removed.
-    """
-    parsed_url = sanitize_url(
-        url, remove_authority=True, remove_query_values=sanitize, split=True
-    )
-
-    base_url = urlunsplit(
-        Components(
-            scheme=parsed_url.scheme,  # type: ignore
-            netloc=parsed_url.netloc,  # type: ignore
-            query="",
-            path=parsed_url.path,  # type: ignore
-            fragment="",
-        )
-    )
-
-    return ParsedUrl(
-        url=base_url,
-        query=parsed_url.query,  # type: ignore
-        fragment=parsed_url.fragment,  # type: ignore
-    )
-
-
-def is_valid_sample_rate(rate: "Any", source: str) -> bool:
-    """
-    Checks the given sample rate to make sure it is valid type and value (a
-    boolean or a number between 0 and 1, inclusive).
-    """
-
-    # both booleans and NaN are instances of Real, so a) checking for Real
-    # checks for the possibility of a boolean also, and b) we have to check
-    # separately for NaN and Decimal does not derive from Real so need to check that too
-    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
-                source=source, rate=rate, type=type(rate)
-            )
-        )
-        return False
-
-    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
-    rate = float(rate)
-    if rate < 0 or rate > 1:
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
-                source=source, rate=rate
-            )
-        )
-        return False
-
-    return True
-
-
-def match_regex_list(
-    item: str,
-    regex_list: "Optional[List[str]]" = None,
-    substring_matching: bool = False,
-) -> bool:
-    if regex_list is None:
-        return False
-
-    for item_matcher in regex_list:
-        if not substring_matching and item_matcher[-1] != "$":
-            item_matcher += "$"
-
-        matched = re.search(item_matcher, item)
-        if matched:
-            return True
-
-    return False
-
-
-def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
-    """
-    Determines whether the given URL matches the Sentry DSN.
-    """
-    return (
-        client is not None
-        and client.transport is not None
-        and client.transport.parsed_dsn is not None
-        and client.transport.parsed_dsn.netloc in url
-    )
-
-
-def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
-    try:
-        from importlib import metadata
-
-        yielded = set()
-        for dist in metadata.distributions():
-            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
-            # `metadata` values may be `None`, see:
-            # https://github.com/python/cpython/issues/91216
-            # and
-            # https://github.com/python/importlib_metadata/issues/371
-            if name is not None:
-                normalized_name = _normalize_module_name(name)
-                if dist.version is not None and normalized_name not in yielded:
-                    yield normalized_name, dist.version
-                    yielded.add(normalized_name)
-
-    except ImportError:
-        # < py3.8
-        try:
-            import pkg_resources
-        except ImportError:
-            return
-
-        for info in pkg_resources.working_set:
-            yield _normalize_module_name(info.key), info.version
-
-
-def _normalize_module_name(name: str) -> str:
-    return name.lower()
-
-
-def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
-    # https://peps.python.org/pep-0503/#normalized-names
-    return re.sub(r"[-_.]+", "-", name)
-
-
-def _get_installed_modules() -> "Dict[str, str]":
-    global _installed_modules
-    if _installed_modules is None:
-        _installed_modules = dict(_generate_installed_modules())
-    return _installed_modules
-
-
-def package_version(package: str) -> "Optional[Tuple[int, ...]]":
-    normalized_package = _normalize_module_name(
-        _replace_hyphens_dots_and_underscores_with_dashes(package)
-    )
-
-    installed_packages = {
-        _replace_hyphens_dots_and_underscores_with_dashes(module): v
-        for module, v in _get_installed_modules().items()
-    }
-    version = installed_packages.get(normalized_package)
-    if version is None:
-        return None
-
-    return parse_version(version)
-
-
-def reraise(
-    tp: "Optional[Type[BaseException]]",
-    value: "Optional[BaseException]",
-    tb: "Optional[Any]" = None,
-) -> "NoReturn":
-    assert value is not None
-    if value.__traceback__ is not tb:
-        raise value.with_traceback(tb)
-    raise value
-
-
-def _no_op(*_a: "Any", **_k: "Any") -> None:
-    """No-op function for ensure_integration_enabled."""
-    pass
-
-
-if TYPE_CHECKING:
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-        original_function: "Callable[P, R]",
-    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
-
-
-def ensure_integration_enabled(
-    integration: "type[sentry_sdk.integrations.Integration]",
-    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
-) -> "Callable[[Callable[P, R]], Callable[P, R]]":
-    """
-    Ensures a given integration is enabled prior to calling a Sentry-patched function.
-
-    The function takes as its parameters the integration that must be enabled and the original
-    function that the SDK is patching. The function returns a function that takes the
-    decorated (Sentry-patched) function as its parameter, and returns a function that, when
-    called, checks whether the given integration is enabled. If the integration is enabled, the
-    function calls the decorated, Sentry-patched function. If the integration is not enabled,
-    the original function is called.
-
-    The function also takes care of preserving the original function's signature and docstring.
-
-    Example usage:
-
-    ```python
-    @ensure_integration_enabled(MyIntegration, my_function)
-    def patch_my_function():
-        with sentry_sdk.start_transaction(...):
-            return my_function()
-    ```
-    """
-    if TYPE_CHECKING:
-        # Type hint to ensure the default function has the right typing. The overloads
-        # ensure the default _no_op function is only used when R is None.
-        original_function = cast(Callable[P, R], original_function)
-
-    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
-        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
-            if sentry_sdk.get_client().get_integration(integration) is None:
-                return original_function(*args, **kwargs)
-
-            return sentry_patched_function(*args, **kwargs)
-
-        if original_function is _no_op:
-            return wraps(sentry_patched_function)(runner)
-
-        return wraps(original_function)(runner)
-
-    return patcher
-
-
-if PY37:
-
-    def nanosecond_time() -> int:
-        return time.perf_counter_ns()
-
-else:
-
-    def nanosecond_time() -> int:
-        return int(time.perf_counter() * 1e9)
-
-
-def now() -> float:
-    return time.perf_counter()
-
-
-try:
-    from gevent import get_hub as get_gevent_hub
-    from gevent.monkey import is_module_patched
-except ImportError:
-    # it's not great that the signatures are different, get_hub can't return None
-    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
-    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
-        return None
-
-    def is_module_patched(mod_name: str) -> bool:
-        # unable to import from gevent means no modules have been patched
-        return False
-
-
-def is_gevent() -> bool:
-    return is_module_patched("threading") or is_module_patched("_thread")
-
-
-def get_current_thread_meta(
-    thread: "Optional[threading.Thread]" = None,
-) -> "Tuple[Optional[int], Optional[str]]":
-    """
-    Try to get the id of the current thread, with various fall backs.
-    """
-
-    # if a thread is specified, that takes priority
-    if thread is not None:
-        try:
-            thread_id = thread.ident
-            thread_name = thread.name
-            if thread_id is not None:
-                return thread_id, thread_name
-        except AttributeError:
-            pass
-
-    # if the app is using gevent, we should look at the gevent hub first
-    # as the id there differs from what the threading module reports
-    if is_gevent():
-        gevent_hub = get_gevent_hub()
-        if gevent_hub is not None:
-            try:
-                # this is undocumented, so wrap it in try except to be safe
-                return gevent_hub.thread_ident, None
-            except AttributeError:
-                pass
-
-    # use the current thread's id if possible
-    try:
-        thread = threading.current_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # if we can't get the current thread id, fall back to the main thread id
-    try:
-        thread = threading.main_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # we've tried everything, time to give up
-    return None, None
-
-
-def _register_control_flow_exception(
-    exc_type: "Union[type, list[type], tuple[type], set[type]]",
-) -> None:
-    if isinstance(exc_type, (list, tuple, set)):
-        _control_flow_exception_classes.update(exc_type)
-    else:
-        _control_flow_exception_classes.add(exc_type)
-
-
-def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
-    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
-        # https://docs.python.org/3/library/exceptions.html#SystemExit
-        return False
-
-    if issubclass(ty, tuple(_control_flow_exception_classes)):
-        return False
-
-    return True
-
-
-if TYPE_CHECKING:
-    T = TypeVar("T")
-
-
-def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
-    """
-    Attempt to convert from an unknown type to a specific type, using the
-    given function. Return None if the conversion fails, i.e. if the function
-    raises an exception.
-    """
-    try:
-        if isinstance(value, convert_func):  # type: ignore
-            return value
-    except TypeError:
-        pass
-
-    try:
-        return convert_func(value)
-    except Exception:
-        return None
-
-
-def safe_serialize(data: "Any") -> str:
-    """Safely serialize to a readable string."""
-
-    def serialize_item(
-        item: "Any",
-    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
-        if callable(item):
-            try:
-                module = getattr(item, "__module__", None)
-                qualname = getattr(item, "__qualname__", None)
-                name = getattr(item, "__name__", "anonymous")
-
-                if module and qualname:
-                    full_path = f"{module}.{qualname}"
-                elif module and name:
-                    full_path = f"{module}.{name}"
-                else:
-                    full_path = name
-
-                return f""
-            except Exception:
-                return f""
-        elif isinstance(item, dict):
-            return {k: serialize_item(v) for k, v in item.items()}
-        elif isinstance(item, (list, tuple)):
-            return [serialize_item(x) for x in item]
-        elif hasattr(item, "__dict__"):
-            try:
-                attrs = {
-                    k: serialize_item(v)
-                    for k, v in vars(item).items()
-                    if not k.startswith("_")
-                }
-                return f"<{type(item).__name__} {attrs}>"
-            except Exception:
-                return repr(item)
-        else:
-            return item
-
-    try:
-        serialized = serialize_item(data)
-        return (
-            json.dumps(serialized, default=str)
-            if not isinstance(serialized, str)
-            else serialized
-        )
-    except Exception:
-        return str(data)
-
-
-def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(
-        options.get("enable_logs", False)
-        or options["_experiments"].get("enable_logs", False)
-    )
-
-
-def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return "data_collection" in options.get("_experiments", {})
-
-
-def get_before_send_log(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_log") or options["_experiments"].get(
-        "before_send_log"
-    )
-
-
-def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(options.get("enable_metrics", True))
-
-
-def get_before_send_metric(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_metric") or options["_experiments"].get(
-        "before_send_metric"
-    )
-
-
-def get_before_send_span(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
-    if options is None:
-        return None
-
-    return options["_experiments"].get("before_send_span")
-
-
-def format_attribute(val: "Any") -> "AttributeValue":
-    """
-    Turn unsupported attribute value types into an AttributeValue.
-
-    We do this as soon as a user-provided attribute is set, to prevent spans,
-    logs, metrics and similar from having live references to various objects.
-
-    Note: This is not the final attribute value format. Before they're sent,
-    they're serialized further into the actual format the protocol expects:
-    https://develop.sentry.dev/sdk/telemetry/attributes/
-    """
-    if isinstance(val, (bool, int, float, str)):
-        return val
-
-    if isinstance(val, (list, tuple)) and not val:
-        return []
-    elif isinstance(val, list):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return copy.deepcopy(val)
-    elif isinstance(val, tuple):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return list(val)
-
-    return safe_repr(val)
-
-
-def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
-    """Serialize attribute value to the transport format."""
-    if isinstance(val, bool):
-        return {"value": val, "type": "boolean"}
-    if isinstance(val, int):
-        return {"value": val, "type": "integer"}
-    if isinstance(val, float):
-        return {"value": val, "type": "double"}
-    if isinstance(val, str):
-        return {"value": val, "type": "string"}
-
-    if isinstance(val, list):
-        if not val:
-            return {"value": [], "type": "array"}
-
-        # Only lists of elements of a single type are supported
-        ty = type(val[0])
-        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
-            return {"value": val, "type": "array"}
-
-    # Coerce to string if we don't know what to do with the value. This should
-    # never happen as we pre-format early in format_attribute, but let's be safe.
-    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py
deleted file mode 100644
index 5eb9b23130..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/sentry_sdk/worker.py
+++ /dev/null
@@ -1,308 +0,0 @@
-import asyncio
-import os
-import threading
-from abc import ABC, abstractmethod
-from time import sleep, time
-from typing import TYPE_CHECKING
-
-from sentry_sdk._queue import FullError, Queue
-from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
-from sentry_sdk.utils import logger, mark_sentry_task_internal
-
-if TYPE_CHECKING:
-    from typing import Any, Callable, Optional
-
-
-_TERMINATOR = object()
-
-
-class Worker(ABC):
-    """Base class for all workers."""
-
-    @property
-    @abstractmethod
-    def is_alive(self) -> bool:
-        """Whether the worker is alive and running."""
-        pass
-
-    @abstractmethod
-    def kill(self) -> None:
-        """Kill the worker. It will not process any more events."""
-        pass
-
-    def flush(
-        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
-    ) -> None:
-        """Flush the worker, blocking until done or timeout is reached."""
-        return None
-
-    @abstractmethod
-    def full(self) -> bool:
-        """Whether the worker's queue is full."""
-        pass
-
-    @abstractmethod
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        """Schedule a callback. Returns True if queued, False if full."""
-        pass
-
-
-class BackgroundWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Queue" = Queue(queue_size)
-        self._lock = threading.Lock()
-        self._thread: "Optional[threading.Thread]" = None
-        self._thread_for_pid: "Optional[int]" = None
-
-    @property
-    def is_alive(self) -> bool:
-        if self._thread_for_pid != os.getpid():
-            return False
-        if not self._thread:
-            return False
-        return self._thread.is_alive()
-
-    def _ensure_thread(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    def _timed_queue_join(self, timeout: float) -> bool:
-        deadline = time() + timeout
-        queue = self._queue
-
-        queue.all_tasks_done.acquire()
-
-        try:
-            while queue.unfinished_tasks:
-                delay = deadline - time()
-                if delay <= 0:
-                    return False
-                queue.all_tasks_done.wait(timeout=delay)
-
-            return True
-        finally:
-            queue.all_tasks_done.release()
-
-    def start(self) -> None:
-        with self._lock:
-            if not self.is_alive:
-                self._thread = threading.Thread(
-                    target=self._target, name="sentry-sdk.BackgroundWorker"
-                )
-                self._thread.daemon = True
-                try:
-                    self._thread.start()
-                    self._thread_for_pid = os.getpid()
-                except RuntimeError:
-                    # At this point we can no longer start because the interpreter
-                    # is already shutting down.  Sadly at this point we can no longer
-                    # send out events.
-                    self._thread = None
-
-    def kill(self) -> None:
-        """
-        Kill worker thread. Returns immediately. Not useful for
-        waiting on shutdown for events, use `flush` for that.
-        """
-        logger.debug("background worker got kill request")
-        with self._lock:
-            if self._thread:
-                try:
-                    self._queue.put_nowait(_TERMINATOR)
-                except FullError:
-                    logger.debug("background worker queue full, kill failed")
-
-                self._thread = None
-                self._thread_for_pid = None
-
-    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
-        logger.debug("background worker got flush request")
-        with self._lock:
-            if self.is_alive and timeout > 0.0:
-                self._wait_flush(timeout, callback)
-        logger.debug("background worker flushed")
-
-    def full(self) -> bool:
-        return self._queue.full()
-
-    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
-        initial_timeout = min(0.1, timeout)
-        if not self._timed_queue_join(initial_timeout):
-            pending = self._queue.qsize() + 1
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            if not self._timed_queue_join(timeout - initial_timeout):
-                pending = self._queue.qsize() + 1
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_thread()
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except FullError:
-            return False
-
-    def _target(self) -> None:
-        while True:
-            callback = self._queue.get()
-            try:
-                if callback is _TERMINATOR:
-                    break
-                try:
-                    callback()
-                except Exception:
-                    logger.error("Failed processing job", exc_info=True)
-            finally:
-                self._queue.task_done()
-            sleep(0)
-
-
-class AsyncWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Optional[asyncio.Queue[Any]]" = None
-        self._queue_size = queue_size
-        self._task: "Optional[asyncio.Task[None]]" = None
-        # Event loop needs to remain in the same process
-        self._task_for_pid: "Optional[int]" = None
-        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
-        # Track active callback tasks so they have a strong reference and can be cancelled on kill
-        self._active_tasks: "set[asyncio.Task[None]]" = set()
-
-    @property
-    def is_alive(self) -> bool:
-        if self._task_for_pid != os.getpid():
-            return False
-        if not self._task or not self._loop:
-            return False
-        return self._loop.is_running() and not self._task.done()
-
-    def kill(self) -> None:
-        if self._task:
-            # Cancel the main consumer task to prevent duplicate consumers
-            self._task.cancel()
-            # Also cancel any active callback tasks
-            # Avoid modifying the set while cancelling tasks
-            tasks_to_cancel = set(self._active_tasks)
-            for task in tasks_to_cancel:
-                task.cancel()
-            self._active_tasks.clear()
-            self._loop = None
-            self._task = None
-            self._task_for_pid = None
-
-    def start(self) -> None:
-        if not self.is_alive:
-            try:
-                self._loop = asyncio.get_running_loop()
-                # Always create a fresh queue on start to avoid stale items
-                self._queue = asyncio.Queue(maxsize=self._queue_size)
-                with mark_sentry_task_internal():
-                    self._task = self._loop.create_task(self._target())
-                self._task_for_pid = os.getpid()
-            except RuntimeError:
-                # There is no event loop running
-                logger.warning("No event loop running, async worker not started")
-                self._loop = None
-                self._task = None
-                self._task_for_pid = None
-
-    def full(self) -> bool:
-        if self._queue is None:
-            return True
-        return self._queue.full()
-
-    def _ensure_task(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    async def _wait_flush(
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> None:
-        if not self._loop or not self._loop.is_running() or self._queue is None:
-            return
-
-        initial_timeout = min(0.1, timeout)
-
-        # Timeout on the join
-        try:
-            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
-        except asyncio.TimeoutError:
-            pending = self._queue.qsize() + len(self._active_tasks)
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            try:
-                remaining_timeout = timeout - initial_timeout
-                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
-            except asyncio.TimeoutError:
-                pending = self._queue.qsize() + len(self._active_tasks)
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def flush(  # type: ignore[override]
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> "Optional[asyncio.Task[None]]":
-        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
-            with mark_sentry_task_internal():
-                return self._loop.create_task(self._wait_flush(timeout, callback))
-        return None
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_task()
-        if self._queue is None:
-            return False
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except asyncio.QueueFull:
-            return False
-
-    async def _target(self) -> None:
-        if self._queue is None:
-            return
-        try:
-            while True:
-                callback = await self._queue.get()
-                if callback is _TERMINATOR:
-                    self._queue.task_done()
-                    break
-                # Firing tasks instead of awaiting them allows for concurrent requests
-                with mark_sentry_task_internal():
-                    task = asyncio.create_task(self._process_callback(callback))
-                # Create a strong reference to the task so it can be cancelled on kill
-                # and does not get garbage collected while running
-                self._active_tasks.add(task)
-                # Capture queue ref at dispatch time so done callbacks use the
-                # correct queue even if kill()/start() replace self._queue.
-                queue_ref = self._queue
-                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
-                # Yield to let the event loop run other tasks
-                await asyncio.sleep(0)
-        except asyncio.CancelledError:
-            pass  # Expected during kill()
-
-    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
-        # Callback is an async coroutine, need to await it
-        await callback()
-
-    def _on_task_complete(
-        self,
-        task: "asyncio.Task[None]",
-        queue: "Optional[asyncio.Queue[Any]]" = None,
-    ) -> None:
-        try:
-            task.result()
-        except asyncio.CancelledError:
-            pass  # Task was cancelled, expected during shutdown
-        except Exception:
-            logger.error("Failed processing job", exc_info=True)
-        finally:
-            # Mark the task as done and remove it from the active tasks set
-            # Use the queue reference captured at dispatch time, not self._queue,
-            # to avoid calling task_done() on a different queue after kill()/start().
-            if queue is not None:
-                queue.task_done()
-            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER
deleted file mode 100644
index 5c69047b2e..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/INSTALLER
+++ /dev/null
@@ -1 +0,0 @@
-uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA
deleted file mode 100644
index 9c7a4703f8..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/METADATA
+++ /dev/null
@@ -1,163 +0,0 @@
-Metadata-Version: 2.4
-Name: urllib3
-Version: 2.7.0
-Summary: HTTP library with thread-safe connection pooling, file post, and more.
-Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
-Project-URL: Documentation, https://urllib3.readthedocs.io
-Project-URL: Code, https://github.com/urllib3/urllib3
-Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
-Author-email: Andrey Petrov 
-Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
-License-Expression: MIT
-License-File: LICENSE.txt
-Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: 3.14
-Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.10
-Provides-Extra: brotli
-Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
-Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
-Provides-Extra: h2
-Requires-Dist: h2<5,>=4; extra == 'h2'
-Provides-Extra: socks
-Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
-Provides-Extra: zstd
-Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
-Description-Content-Type: text/markdown
-
-

- -![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) - -

- -

- PyPI Version - Python Versions - Join our Discord - Coverage Status - Build Status on GitHub - Documentation Status
- OpenSSF Scorecard - SLSA 3 - CII Best Practices -

- -urllib3 is a powerful, *user-friendly* HTTP client for Python. -urllib3 brings many critical features that are missing from the Python -standard libraries: - -- Thread safety. -- Connection pooling. -- Client-side SSL/TLS verification. -- File uploads with multipart encoding. -- Helpers for retrying requests and dealing with HTTP redirects. -- Support for gzip, deflate, brotli, and zstd encoding. -- Proxy support for HTTP and SOCKS. -- 100% test coverage. - -... and many more features, but most importantly: Our maintainers have a 15+ -year track record of maintaining urllib3 with the highest code standards and -attention to security and safety. - -[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) -and you should too. - - -## Installing - -urllib3 can be installed with [pip](https://pip.pypa.io): - -```bash -$ python -m pip install urllib3 -``` - -Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): - -```bash -$ git clone https://github.com/urllib3/urllib3.git -$ cd urllib3 -$ pip install . -``` - -## Getting Started - -urllib3 is easy to use: - -```python3 ->>> import urllib3 ->>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") ->>> resp.status -200 ->>> resp.data -b"User-agent: *\nDisallow: /deny\n" -``` - -urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). - - -## Community - -urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and -collaborating with other contributors. Drop by and say hello 👋 - - -## Contributing - -urllib3 happily accepts contributions. Please see our -[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) -for some tips on getting started. - - -## Security Disclosures - -To report a security vulnerability, please use the -[Tidelift security contact](https://tidelift.com/security). -Tidelift will coordinate the fix and disclosure with maintainers. - - -## Maintainers - -Meet our maintainers since 2008: - -- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) -- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) -- [@pquentin](https://github.com/pquentin) (Quentin Pradet) -- [@theacodes](https://github.com/theacodes) (Thea Flowers) -- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) -- [@lukasa](https://github.com/lukasa) (Cory Benfield) -- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) -- [@shazow](https://github.com/shazow) (Andrey Petrov) - -👋 - - -## Sponsorship - -If your company benefits from this library, please consider [sponsoring its -development](https://urllib3.readthedocs.io/en/latest/sponsors.html). - - -## For Enterprise - -Professional support for urllib3 is available as part of the [Tidelift -Subscription][1]. Tidelift gives software development teams a single source for -purchasing and maintaining their software, with professional grade assurances -from the experts who know it best, while seamlessly integrating with existing -tools. - -[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD deleted file mode 100644 index 8c4f9c0b4b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/RECORD +++ /dev/null @@ -1,44 +0,0 @@ -urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 -urllib3-2.7.0.dist-info/RECORD,, -urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 -urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 -urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 -urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 -urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 -urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 -urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 -urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 -urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 -urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 -urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 -urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 -urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 -urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 -urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 -urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 -urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 -urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 -urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 -urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 -urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 -urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 -urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 -urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 -urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 -urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 -urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 -urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 -urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 -urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 -urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 -urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 -urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 -urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 -urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 -urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 -urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 -urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 -urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL deleted file mode 100644 index b1b94fd58e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.29.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt deleted file mode 100644 index e6183d0276..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2008-2020 Andrey Petrov and contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py deleted file mode 100644 index 3fe782c8a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/__init__.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more -""" - -from __future__ import annotations - -# Set default logging handler to avoid "No handler found" warnings. -import logging -import sys -import typing -import warnings -from logging import NullHandler - -from . import exceptions -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._version import __version__ -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .poolmanager import PoolManager, ProxyManager, proxy_from_url -from .response import BaseHTTPResponse, HTTPResponse -from .util.request import make_headers -from .util.retry import Retry -from .util.timeout import Timeout - -# Ensure that Python is compiled with OpenSSL 1.1.1+ -# If the 'ssl' module isn't available at all that's -# fine, we only care if the module is available. -try: - import ssl -except ImportError: - pass -else: - if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: - warnings.warn( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/3020", - exceptions.NotOpenSSLWarning, - ) - elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: - raise ImportError( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/2168" - ) - -__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" -__license__ = "MIT" -__version__ = __version__ - -__all__ = ( - "HTTPConnectionPool", - "HTTPHeaderDict", - "HTTPSConnectionPool", - "PoolManager", - "ProxyManager", - "HTTPResponse", - "Retry", - "Timeout", - "add_stderr_logger", - "connection_from_url", - "disable_warnings", - "encode_multipart_formdata", - "make_headers", - "proxy_from_url", - "request", - "BaseHTTPResponse", -) - -logging.getLogger(__name__).addHandler(NullHandler()) - - -def add_stderr_logger( - level: int = logging.DEBUG, -) -> logging.StreamHandler[typing.TextIO]: - """ - Helper for quickly adding a StreamHandler to the logger. Useful for - debugging. - - Returns the handler after adding it. - """ - # This method needs to be in this __init__.py to get the __name__ correct - # even if urllib3 is vendored within another package. - logger = logging.getLogger(__name__) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) - logger.addHandler(handler) - logger.setLevel(level) - logger.debug("Added a stderr logging handler to logger: %s", __name__) - return handler - - -# ... Clean up. -del NullHandler - - -# All warning filters *must* be appended unless you're really certain that they -# shouldn't be: otherwise, it's very hard for users to use most Python -# mechanisms to silence them. -# SecurityWarning's always go off by default. -warnings.simplefilter("always", exceptions.SecurityWarning, append=True) -# InsecurePlatformWarning's don't vary between requests, so we keep it default. -warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) - - -def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: - """ - Helper for quickly disabling all urllib3 warnings. - """ - warnings.simplefilter("ignore", category) - - -_DEFAULT_POOL = PoolManager() - - -def request( - method: str, - url: str, - *, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - preload_content: bool | None = True, - decode_content: bool | None = True, - redirect: bool | None = True, - retries: Retry | bool | int | None = None, - timeout: Timeout | float | int | None = 3, - json: typing.Any | None = None, -) -> BaseHTTPResponse: - """ - A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. - Therefore, its side effects could be shared across dependencies relying on it. - To avoid side effects create a new ``PoolManager`` instance and use it instead. - The method does not accept low-level ``**urlopen_kw`` keyword arguments. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - - return _DEFAULT_POOL.request( - method, - url, - body=body, - fields=fields, - headers=headers, - preload_content=preload_content, - decode_content=decode_content, - redirect=redirect, - retries=retries, - timeout=timeout, - json=json, - ) - - -if sys.platform == "emscripten": - from .contrib.emscripten import inject_into_urllib3 # noqa: 401 - - inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py deleted file mode 100644 index 992ec1657a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_base_connection.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -import typing - -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from .util.url import Url - -_TYPE_BODY = typing.Union[ - bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str -] - - -class ProxyConfig(typing.NamedTuple): - ssl_context: ssl.SSLContext | None - use_forwarding_for_https: bool - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - - -class _ResponseOptions(typing.NamedTuple): - # TODO: Remove this in favor of a better - # HTTP request/response lifecycle tracking. - request_method: str - request_url: str - preload_content: bool - decode_content: bool - enforce_content_length: bool - - -if typing.TYPE_CHECKING: - import ssl - from typing import Protocol - - from .response import BaseHTTPResponse - - class BaseHTTPConnection(Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - host: str - port: int - timeout: None | ( - float - ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool - proxy_is_verified: bool | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: ... - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: ... - - def connect(self) -> None: ... - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: ... - - def getresponse(self) -> BaseHTTPResponse: ... - - def close(self) -> None: ... - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - - class BaseHTTPSConnection(BaseHTTPConnection, Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - # Certificate verification methods - cert_reqs: int | str | None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - ssl_context: ssl.SSLContext | None - - # Trusted CAs - ca_certs: str | None - ca_cert_dir: str | None - ca_cert_data: None | str | bytes - - # TLS version - ssl_minimum_version: int | None - ssl_maximum_version: int | None - ssl_version: int | str | None # Deprecated - - # Client certificates - cert_file: str | None - key_file: str | None - key_password: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py deleted file mode 100644 index ee9ca662b6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_collections.py +++ /dev/null @@ -1,486 +0,0 @@ -from __future__ import annotations - -import typing -from collections import OrderedDict -from enum import Enum, auto -from threading import RLock - -if typing.TYPE_CHECKING: - # We can only import Protocol if TYPE_CHECKING because it's a development - # dependency, and is not available at runtime. - from typing import Protocol - - from typing_extensions import Self - - class HasGettableStringKeys(Protocol): - def keys(self) -> typing.Iterator[str]: ... - - def __getitem__(self, key: str) -> str: ... - - -__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] - - -# Key type -_KT = typing.TypeVar("_KT") -# Value type -_VT = typing.TypeVar("_VT") -# Default type -_DT = typing.TypeVar("_DT") - -ValidHTTPHeaderSource = typing.Union[ - "HTTPHeaderDict", - typing.Mapping[str, str], - typing.Iterable[tuple[str, str]], - "HasGettableStringKeys", -] - - -class _Sentinel(Enum): - not_passed = auto() - - -def ensure_can_construct_http_header_dict( - potential: object, -) -> ValidHTTPHeaderSource | None: - if isinstance(potential, HTTPHeaderDict): - return potential - elif isinstance(potential, typing.Mapping): - # Full runtime checking of the contents of a Mapping is expensive, so for the - # purposes of typechecking, we assume that any Mapping is the right shape. - return typing.cast(typing.Mapping[str, str], potential) - elif isinstance(potential, typing.Iterable): - # Similarly to Mapping, full runtime checking of the contents of an Iterable is - # expensive, so for the purposes of typechecking, we assume that any Iterable - # is the right shape. - return typing.cast(typing.Iterable[tuple[str, str]], potential) - elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): - return typing.cast("HasGettableStringKeys", potential) - else: - return None - - -class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): - """ - Provides a thread-safe dict-like container which maintains up to - ``maxsize`` keys while throwing away the least-recently-used keys beyond - ``maxsize``. - - :param maxsize: - Maximum number of recent elements to retain. - - :param dispose_func: - Every time an item is evicted from the container, - ``dispose_func(value)`` is called. Callback which will get called - """ - - _container: typing.OrderedDict[_KT, _VT] - _maxsize: int - dispose_func: typing.Callable[[_VT], None] | None - lock: RLock - - def __init__( - self, - maxsize: int = 10, - dispose_func: typing.Callable[[_VT], None] | None = None, - ) -> None: - super().__init__() - self._maxsize = maxsize - self.dispose_func = dispose_func - self._container = OrderedDict() - self.lock = RLock() - - def __getitem__(self, key: _KT) -> _VT: - # Re-insert the item, moving it to the end of the eviction line. - with self.lock: - item = self._container.pop(key) - self._container[key] = item - return item - - def __setitem__(self, key: _KT, value: _VT) -> None: - evicted_item = None - with self.lock: - # Possibly evict the existing value of 'key' - try: - # If the key exists, we'll overwrite it, which won't change the - # size of the pool. Because accessing a key should move it to - # the end of the eviction line, we pop it out first. - evicted_item = key, self._container.pop(key) - self._container[key] = value - except KeyError: - # When the key does not exist, we insert the value first so that - # evicting works in all cases, including when self._maxsize is 0 - self._container[key] = value - if len(self._container) > self._maxsize: - # If we didn't evict an existing value, and we've hit our maximum - # size, then we have to evict the least recently used item from - # the beginning of the container. - evicted_item = self._container.popitem(last=False) - - # After releasing the lock on the pool, dispose of any evicted value. - if evicted_item is not None and self.dispose_func: - _, evicted_value = evicted_item - self.dispose_func(evicted_value) - - def __delitem__(self, key: _KT) -> None: - with self.lock: - value = self._container.pop(key) - - if self.dispose_func: - self.dispose_func(value) - - def __len__(self) -> int: - with self.lock: - return len(self._container) - - def __iter__(self) -> typing.NoReturn: - raise NotImplementedError( - "Iteration over this class is unlikely to be threadsafe." - ) - - def clear(self) -> None: - with self.lock: - # Copy pointers to all values, then wipe the mapping - values = list(self._container.values()) - self._container.clear() - - if self.dispose_func: - for value in values: - self.dispose_func(value) - - def keys(self) -> set[_KT]: # type: ignore[override] - with self.lock: - return set(self._container.keys()) - - -class HTTPHeaderDictItemView(set[tuple[str, str]]): - """ - HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of - address. - - If we directly try to get an item with a particular name, we will get a string - back that is the concatenated version of all the values: - - >>> d['X-Header-Name'] - 'Value1, Value2, Value3' - - However, if we iterate over an HTTPHeaderDict's items, we will optionally combine - these values based on whether combine=True was called when building up the dictionary - - >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) - >>> d.add("A", "2", combine=True) - >>> d.add("B", "bar") - >>> list(d.items()) - [ - ('A', '1, 2'), - ('B', 'foo'), - ('B', 'bar'), - ] - - This class conforms to the interface required by the MutableMapping ABC while - also giving us the nonstandard iteration behavior we want; items with duplicate - keys, ordered by time of first insertion. - """ - - _headers: HTTPHeaderDict - - def __init__(self, headers: HTTPHeaderDict) -> None: - self._headers = headers - - def __len__(self) -> int: - return len(list(self._headers.iteritems())) - - def __iter__(self) -> typing.Iterator[tuple[str, str]]: - return self._headers.iteritems() - - def __contains__(self, item: object) -> bool: - if isinstance(item, tuple) and len(item) == 2: - passed_key, passed_val = item - if isinstance(passed_key, str) and isinstance(passed_val, str): - return self._headers._has_value_for_header(passed_key, passed_val) - return False - - -class HTTPHeaderDict(typing.MutableMapping[str, str]): - """ - :param headers: - An iterable of field-value pairs. Must not contain multiple field names - when compared case-insensitively. - - :param kwargs: - Additional field-value pairs to pass in to ``dict.update``. - - A ``dict`` like container for storing HTTP Headers. - - Field names are stored and compared case-insensitively in compliance with - RFC 7230. Iteration provides the first case-sensitive key seen for each - case-insensitive pair. - - Using ``__setitem__`` syntax overwrites fields that compare equal - case-insensitively in order to maintain ``dict``'s api. For fields that - compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` - in a loop. - - If multiple fields that are equal case-insensitively are passed to the - constructor or ``.update``, the behavior is undefined and some will be - lost. - - >>> headers = HTTPHeaderDict() - >>> headers.add('Set-Cookie', 'foo=bar') - >>> headers.add('set-cookie', 'baz=quxx') - >>> headers['content-length'] = '7' - >>> headers['SET-cookie'] - 'foo=bar, baz=quxx' - >>> headers['Content-Length'] - '7' - """ - - _container: typing.MutableMapping[str, list[str]] - - def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): - super().__init__() - self._container = {} # 'dict' is insert-ordered - if headers is not None: - if isinstance(headers, HTTPHeaderDict): - self._copy_from(headers) - else: - self.extend(headers) - if kwargs: - self.extend(kwargs) - - def __setitem__(self, key: str, val: str) -> None: - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - self._container[key.lower()] = [key, val] - - def __getitem__(self, key: str) -> str: - if isinstance(key, bytes): - key = key.decode("latin-1") - val = self._container[key.lower()] - return ", ".join(val[1:]) - - def __delitem__(self, key: str) -> None: - if isinstance(key, bytes): - key = key.decode("latin-1") - del self._container[key.lower()] - - def __contains__(self, key: object) -> bool: - if isinstance(key, bytes): - key = key.decode("latin-1") - if isinstance(key, str): - return key.lower() in self._container - return False - - def setdefault(self, key: str, default: str = "") -> str: - return super().setdefault(key, default) - - def __eq__(self, other: object) -> bool: - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return False - else: - other_as_http_header_dict = type(self)(maybe_constructable) - - return {k.lower(): v for k, v in self.itermerged()} == { - k.lower(): v for k, v in other_as_http_header_dict.itermerged() - } - - def __ne__(self, other: object) -> bool: - return not self.__eq__(other) - - def __len__(self) -> int: - return len(self._container) - - def __iter__(self) -> typing.Iterator[str]: - # Only provide the originally cased names - for vals in self._container.values(): - yield vals[0] - - def discard(self, key: str) -> None: - try: - del self[key] - except KeyError: - pass - - def add(self, key: str, val: str, *, combine: bool = False) -> None: - """Adds a (name, value) pair, doesn't overwrite the value if it already - exists. - - If this is called with combine=True, instead of adding a new header value - as a distinct item during iteration, this will instead append the value to - any existing header value with a comma. If no existing header value exists - for the key, then the value will simply be added, ignoring the combine parameter. - - >>> headers = HTTPHeaderDict(foo='bar') - >>> headers.add('Foo', 'baz') - >>> headers['foo'] - 'bar, baz' - >>> list(headers.items()) - [('foo', 'bar'), ('foo', 'baz')] - >>> headers.add('foo', 'quz', combine=True) - >>> list(headers.items()) - [('foo', 'bar, baz, quz')] - """ - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - key_lower = key.lower() - new_vals = [key, val] - # Keep the common case aka no item present as fast as possible - vals = self._container.setdefault(key_lower, new_vals) - if new_vals is not vals: - # if there are values here, then there is at least the initial - # key/value pair - assert len(vals) >= 2 - if combine: - vals[-1] = vals[-1] + ", " + val - else: - vals.append(val) - - def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: - """Generic import function for any type of header-like object. - Adapted version of MutableMapping.update in order to insert items - with self.add instead of self.__setitem__ - """ - if len(args) > 1: - raise TypeError( - f"extend() takes at most 1 positional arguments ({len(args)} given)" - ) - other = args[0] if len(args) >= 1 else () - - if isinstance(other, HTTPHeaderDict): - for key, val in other.iteritems(): - self.add(key, val) - elif isinstance(other, typing.Mapping): - for key, val in other.items(): - self.add(key, val) - elif isinstance(other, typing.Iterable): - for key, value in other: - self.add(key, value) - elif hasattr(other, "keys") and hasattr(other, "__getitem__"): - # THIS IS NOT A TYPESAFE BRANCH - # In this branch, the object has a `keys` attr but is not a Mapping or any of - # the other types indicated in the method signature. We do some stuff with - # it as though it partially implements the Mapping interface, but we're not - # doing that stuff safely AT ALL. - for key in other.keys(): - self.add(key, other[key]) - - for key, value in kwargs.items(): - self.add(key, value) - - @typing.overload - def getlist(self, key: str) -> list[str]: ... - - @typing.overload - def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... - - def getlist( - self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed - ) -> list[str] | _DT: - """Returns a list of all the values for the named field. Returns an - empty list if the key doesn't exist.""" - if isinstance(key, bytes): - key = key.decode("latin-1") - try: - vals = self._container[key.lower()] - except KeyError: - if default is _Sentinel.not_passed: - # _DT is unbound; empty list is instance of List[str] - return [] - # _DT is bound; default is instance of _DT - return default - else: - # _DT may or may not be bound; vals[1:] is instance of List[str], which - # meets our external interface requirement of `Union[List[str], _DT]`. - return vals[1:] - - def _prepare_for_method_change(self) -> Self: - """ - Remove content-specific header fields before changing the request - method to GET or HEAD according to RFC 9110, Section 15.4. - """ - content_specific_headers = [ - "Content-Encoding", - "Content-Language", - "Content-Location", - "Content-Type", - "Content-Length", - "Digest", - "Last-Modified", - ] - for header in content_specific_headers: - self.discard(header) - return self - - # Backwards compatibility for httplib - getheaders = getlist - getallmatchingheaders = getlist - iget = getlist - - # Backwards compatibility for http.cookiejar - get_all = getlist - - def __repr__(self) -> str: - return f"{type(self).__name__}({dict(self.itermerged())})" - - def _copy_from(self, other: HTTPHeaderDict) -> None: - for key in other: - val = other.getlist(key) - self._container[key.lower()] = [key, *val] - - def copy(self) -> Self: - clone = type(self)() - clone._copy_from(self) - return clone - - def iteritems(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all header lines, including duplicate ones.""" - for key in self: - vals = self._container[key.lower()] - for val in vals[1:]: - yield vals[0], val - - def itermerged(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all headers, merging duplicate ones together.""" - for key in self: - val = self._container[key.lower()] - yield val[0], ", ".join(val[1:]) - - def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] - return HTTPHeaderDictItemView(self) - - def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: - if header_name in self: - return potential_value in self._container[header_name.lower()][1:] - return False - - def __ior__(self, other: object) -> HTTPHeaderDict: - # Supports extending a header dict in-place using operator |= - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - self.extend(maybe_constructable) - return self - - def __or__(self, other: object) -> Self: - # Supports merging header dicts using operator | - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = self.copy() - result.extend(maybe_constructable) - return result - - def __ror__(self, other: object) -> Self: - # Supports merging header dicts using operator | when other is on left side - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = type(self)(maybe_constructable) - result.extend(self) - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py deleted file mode 100644 index 297c271bf4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_request_methods.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -import json as _json -import typing -from urllib.parse import urlencode - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .response import BaseHTTPResponse - -__all__ = ["RequestMethods"] - -_TYPE_ENCODE_URL_FIELDS = typing.Union[ - typing.Sequence[tuple[str, typing.Union[str, bytes]]], - typing.Mapping[str, typing.Union[str, bytes]], -] - - -class RequestMethods: - """ - Convenience mixin for classes who implement a :meth:`urlopen` method, such - as :class:`urllib3.HTTPConnectionPool` and - :class:`urllib3.PoolManager`. - - Provides behavior for making common types of HTTP request methods and - decides which type of request field encoding to use. - - Specifically, - - :meth:`.request_encode_url` is for sending requests whose fields are - encoded in the URL (such as GET, HEAD, DELETE). - - :meth:`.request_encode_body` is for sending requests whose fields are - encoded in the *body* of the request using multipart or www-form-urlencoded - (such as for POST, PUT, PATCH). - - :meth:`.request` is for making any kind of request, it will look up the - appropriate encoding format and use one of the above two methods to make - the request. - - Initializer parameters: - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - """ - - _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} - - def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: - self.headers = headers or {} - - def urlopen( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **kw: typing.Any, - ) -> BaseHTTPResponse: # Abstract - raise NotImplementedError( - "Classes extending RequestMethods must implement " - "their own ``urlopen`` method." - ) - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - json: typing.Any | None = None, - **urlopen_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the appropriate encoding of - ``fields`` based on the ``method`` used. - - This is a convenience method that requires the least amount of manual - effort. It can be used in most situations, while still having the - option to drop down to more specific methods when necessary, such as - :meth:`request_encode_url`, :meth:`request_encode_body`, - or even the lowest level :meth:`urlopen`. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the URL or request body, depending on ``method``. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - method = method.upper() - - if json is not None and body is not None: - raise TypeError( - "request got values for both 'body' and 'json' parameters which are mutually exclusive" - ) - - if json is not None: - if headers is None: - headers = self.headers - - if not ("content-type" in map(str.lower, headers.keys())): - headers = HTTPHeaderDict(headers) - headers["Content-Type"] = "application/json" - - body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( - "utf-8" - ) - - if body is not None: - urlopen_kw["body"] = body - - if method in self._encode_url_methods: - return self.request_encode_url( - method, - url, - fields=fields, # type: ignore[arg-type] - headers=headers, - **urlopen_kw, - ) - else: - return self.request_encode_body( - method, url, fields=fields, headers=headers, **urlopen_kw - ) - - def request_encode_url( - self, - method: str, - url: str, - fields: _TYPE_ENCODE_URL_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the url. This is useful for request methods like GET, HEAD, DELETE, etc. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the URL. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": headers} - extra_kw.update(urlopen_kw) - - if fields: - url += "?" + urlencode(fields) - - return self.urlopen(method, url, **extra_kw) - - def request_encode_body( - self, - method: str, - url: str, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the body. This is useful for request methods like POST, PUT, PATCH, etc. - - When ``encode_multipart=True`` (default), then - :func:`urllib3.encode_multipart_formdata` is used to encode - the payload with the appropriate content type. Otherwise - :func:`urllib.parse.urlencode` is used with the - 'application/x-www-form-urlencoded' content type. - - Multipart encoding must be used when posting files, and it's reasonably - safe to use it in other times too. However, it may break request - signing, such as with OAuth. - - Supports an optional ``fields`` parameter of key/value strings AND - key/filetuple. A filetuple is a (filename, data, MIME type) tuple where - the MIME type is optional. For example:: - - fields = { - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), - 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - } - - When uploading a file, providing a filename (the first parameter of the - tuple) is optional but recommended to best mimic behavior of browsers. - - Note that if ``headers`` are supplied, the 'Content-Type' header will - be overwritten because it depends on the dynamic random boundary string - which is used to compose the body of the request. The random boundary - string can be explicitly set with the ``multipart_boundary`` parameter. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param encode_multipart: - If True, encode the ``fields`` using the multipart/form-data MIME - format. - - :param multipart_boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} - body: bytes | str - - if fields: - if "body" in urlopen_kw: - raise TypeError( - "request got values for both 'fields' and 'body', can only specify one." - ) - - if encode_multipart: - body, content_type = encode_multipart_formdata( - fields, boundary=multipart_boundary - ) - else: - body, content_type = ( - urlencode(fields), # type: ignore[arg-type] - "application/x-www-form-urlencoded", - ) - - extra_kw["body"] = body - extra_kw["headers"].setdefault("Content-Type", content_type) - - extra_kw.update(urlopen_kw) - - return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py deleted file mode 100644 index bfed03985b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/_version.py +++ /dev/null @@ -1,24 +0,0 @@ -# file generated by vcs-versioning -# don't change, don't track in version control -from __future__ import annotations - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -version: str -__version__: str -__version_tuple__: tuple[int | str, ...] -version_tuple: tuple[int | str, ...] -commit_id: str | None -__commit_id__: str | None - -__version__ = version = '2.7.0' -__version_tuple__ = version_tuple = (2, 7, 0) - -__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py deleted file mode 100644 index 84e1dab945..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connection.py +++ /dev/null @@ -1,1099 +0,0 @@ -from __future__ import annotations - -import datetime -import http.client -import logging -import os -import re -import socket -import sys -import threading -import typing -import warnings -from http.client import HTTPConnection as _HTTPConnection -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from .response import HTTPResponse - from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT - from .util.ssltransport import SSLTransport - -from ._collections import HTTPHeaderDict -from .http2 import probe as http2_probe -from .util.response import assert_header_parsing -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout -from .util.util import to_str -from .util.wait import wait_for_read - -try: # Compiled with SSL? - import ssl - - BaseSSLError = ssl.SSLError -except (ImportError, AttributeError): - ssl = None # type: ignore[assignment] - - class BaseSSLError(BaseException): # type: ignore[no-redef] - pass - - -from ._base_connection import _TYPE_BODY -from ._base_connection import ProxyConfig as ProxyConfig -from ._base_connection import _ResponseOptions as _ResponseOptions -from ._version import __version__ -from .exceptions import ( - ConnectTimeoutError, - HeaderParsingError, - NameResolutionError, - NewConnectionError, - ProxyError, - SystemTimeWarning, -) -from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ -from .util.request import body_to_chunks -from .util.ssl_ import assert_fingerprint as _assert_fingerprint -from .util.ssl_ import ( - create_urllib3_context, - is_ipaddress, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .util.ssl_match_hostname import CertificateError, match_hostname -from .util.url import Url - -# Not a no-op, we're adding this to the namespace so it can be imported. -ConnectionError = ConnectionError -BrokenPipeError = BrokenPipeError - - -log = logging.getLogger(__name__) - -port_by_scheme = {"http": 80, "https": 443} - -# When it comes time to update this value as a part of regular maintenance -# (ie test_recent_date is failing) update it to ~6 months before the current date. -RECENT_DATE = datetime.date(2025, 1, 1) - -_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") - - -class HTTPConnection(_HTTPConnection): - """ - Based on :class:`http.client.HTTPConnection` but provides an extra constructor - backwards-compatibility layer between older and newer Pythons. - - Additional keyword parameters are used to configure attributes of the connection. - Accepted parameters include: - - - ``source_address``: Set the source address for the current connection. - - ``socket_options``: Set specific options on the underlying socket. If not specified, then - defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling - Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. - - For example, if you wish to enable TCP Keep Alive in addition to the defaults, - you might pass: - - .. code-block:: python - - HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ] - - Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). - """ - - default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] - - #: Disable Nagle's algorithm by default. - #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` - default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - ] - - #: Whether this connection verifies the host's certificate. - is_verified: bool = False - - #: Whether this proxy connection verified the proxy host's certificate. - # If no proxy is currently connected to the value will be ``None``. - proxy_is_verified: bool | None = None - - blocksize: int - source_address: tuple[str, int] | None - socket_options: connection._TYPE_SOCKET_OPTIONS | None - - _has_connected_to_proxy: bool - _response_options: _ResponseOptions | None - _tunnel_host: str | None - _tunnel_port: int | None - _tunnel_scheme: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - super().__init__( - host=host, - port=port, - timeout=Timeout.resolve_default_timeout(timeout), - source_address=source_address, - blocksize=blocksize, - ) - self.socket_options = socket_options - self.proxy = proxy - self.proxy_config = proxy_config - - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host: str | None = None - self._tunnel_port: int | None = None - self._tunnel_scheme: str | None = None - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __repr__(self) -> str: - return f"<{self} at {id(self):#x}>" - - @property - def host(self) -> str: - """ - Getter method to remove any trailing dots that indicate the hostname is an FQDN. - - In general, SSL certificates don't include the trailing dot indicating a - fully-qualified domain name, and thus, they don't validate properly when - checked against a domain name that includes the dot. In addition, some - servers may not expect to receive the trailing dot when provided. - - However, the hostname with trailing dot is critical to DNS resolution; doing a - lookup with the trailing dot will properly only resolve the appropriate FQDN, - whereas a lookup without a trailing dot will search the system's search domain - list. Thus, it's important to keep the original host around for use only in - those cases where it's appropriate (i.e., when doing DNS lookup to establish the - actual TCP connection across which we're going to send HTTP requests). - """ - return self._dns_host.rstrip(".") - - @host.setter - def host(self, value: str) -> None: - """ - Setter for the `host` property. - - We assume that only urllib3 uses the _dns_host attribute; httplib itself - only uses `host`, and it seems reasonable that other libraries follow suit. - """ - self._dns_host = value - - def _new_conn(self) -> socket.socket: - """Establish a socket connection and set nodelay settings on it. - - :return: New socket connection. - """ - try: - sock = connection.create_connection( - (self._dns_host, self.port), - self.timeout, - source_address=self.source_address, - socket_options=self.socket_options, - ) - except socket.gaierror as e: - raise NameResolutionError(self.host, self, e) from e - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except OSError as e: - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - sys.audit("http.client.connect", self, self.host, self.port) - - return sock - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - if scheme not in ("http", "https"): - raise ValueError( - f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" - ) - super().set_tunnel(host, port=port, headers=headers) - self._tunnel_scheme = scheme - - if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): - # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. - # When using connection_from_host, host will come without brackets. - def _wrap_ipv6(self, ip: bytes) -> bytes: - if b":" in ip and ip[0] != b"["[0]: - return b"[" + ip + b"]" - return ip - - if sys.version_info < (3, 11, 9): - # `_tunnel` copied from 3.11.13 backporting - # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 - # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 - def _tunnel(self) -> None: - _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] - connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - while True: - line = response.fp.readline(_MAXLINE + 1) - if len(line) > _MAXLINE: - raise http.client.LineTooLong("header line") - if not line: - # for sites which EOF without sending a trailer - break - if line in (b"\r\n", b"\n", b""): - break - - if self.debuglevel > 0: - print("header:", line.decode()) - finally: - response.close() - - elif (3, 12) <= sys.version_info < (3, 12, 3): - # `_tunnel` copied from 3.12.11 backporting - # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 - def _tunnel(self) -> None: # noqa: F811 - connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] - - if self.debuglevel > 0: - for header in self._raw_proxy_headers: - print("header:", header.decode()) - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - - finally: - response.close() - - def connect(self) -> None: - self.sock = self._new_conn() - if self._tunnel_host: - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - # TODO: Fix tunnel so it doesn't depend on self.sock state. - self._tunnel() - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - if self._has_connected_to_proxy: - self.proxy_is_verified = False - - @property - def is_closed(self) -> bool: - return self.sock is None - - @property - def is_connected(self) -> bool: - if self.sock is None: - return False - return not wait_for_read(self.sock, timeout=0.0) - - @property - def has_connected_to_proxy(self) -> bool: - return self._has_connected_to_proxy - - @property - def proxy_is_forwarding(self) -> bool: - """ - Return True if a forwarding proxy is configured, else return False - """ - return bool(self.proxy) and self._tunnel_host is None - - @property - def proxy_is_tunneling(self) -> bool: - """ - Return True if a tunneling proxy is configured, else return False - """ - return self._tunnel_host is not None - - def close(self) -> None: - try: - super().close() - finally: - # Reset all stateful properties so connection - # can be re-used without leaking prior configs. - self.sock = None - self.is_verified = False - self.proxy_is_verified = None - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host = None - self._tunnel_port = None - self._tunnel_scheme = None - - def putrequest( - self, - method: str, - url: str, - skip_host: bool = False, - skip_accept_encoding: bool = False, - ) -> None: - """""" - # Empty docstring because the indentation of CPython's implementation - # is broken but we don't want this method in our documentation. - match = _CONTAINS_CONTROL_CHAR_RE.search(method) - if match: - raise ValueError( - f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" - ) - - return super().putrequest( - method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding - ) - - def putheader(self, header: str, *values: str) -> None: # type: ignore[override] - """""" - if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): - super().putheader(header, *values) - elif to_str(header.lower()) not in SKIPPABLE_HEADERS: - skippable_headers = "', '".join( - [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] - ) - raise ValueError( - f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" - ) - - # `request` method's signature intentionally violates LSP. - # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - # Update the inner socket's timeout value to send the request. - # This only triggers if the connection is re-used. - if self.sock is not None: - self.sock.settimeout(self.timeout) - - # Store these values to be fed into the HTTPResponse - # object later. TODO: Remove this in favor of a real - # HTTP lifecycle mechanism. - - # We have to store these before we call .request() - # because sometimes we can still salvage a response - # off the wire even if we aren't able to completely - # send the request body. - self._response_options = _ResponseOptions( - request_method=method, - request_url=url, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - if headers is None: - headers = {} - header_keys = frozenset(to_str(k.lower()) for k in headers) - skip_accept_encoding = "accept-encoding" in header_keys - skip_host = "host" in header_keys - self.putrequest( - method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host - ) - - # Transform the body into an iterable of sendall()-able chunks - # and detect if an explicit Content-Length is doable. - chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) - chunks = chunks_and_cl.chunks - content_length = chunks_and_cl.content_length - - # When chunked is explicit set to 'True' we respect that. - if chunked: - if "transfer-encoding" not in header_keys: - self.putheader("Transfer-Encoding", "chunked") - else: - # Detect whether a framing mechanism is already in use. If so - # we respect that value, otherwise we pick chunked vs content-length - # depending on the type of 'body'. - if "content-length" in header_keys: - chunked = False - elif "transfer-encoding" in header_keys: - chunked = True - - # Otherwise we go off the recommendation of 'body_to_chunks()'. - else: - chunked = False - if content_length is None: - if chunks is not None: - chunked = True - self.putheader("Transfer-Encoding", "chunked") - else: - self.putheader("Content-Length", str(content_length)) - - # Now that framing headers are out of the way we send all the other headers. - if "user-agent" not in header_keys: - self.putheader("User-Agent", _get_default_user_agent()) - for header, value in headers.items(): - self.putheader(header, value) - self.endheaders() - - # If we're given a body we start sending that in chunks. - if chunks is not None: - for chunk in chunks: - # Sending empty chunks isn't allowed for TE: chunked - # as it indicates the end of the body. - if not chunk: - continue - if isinstance(chunk, str): - chunk = chunk.encode("utf-8") - if chunked: - self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) - else: - self.send(chunk) - - # Regardless of whether we have a body or not, if we're in - # chunked mode we want to send an explicit empty chunk. - if chunked: - self.send(b"0\r\n\r\n") - - def request_chunked( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - ) -> None: - """ - Alternative to the common request method, which sends the - body with chunked encoding and not as one block - """ - warnings.warn( - "HTTPConnection.request_chunked() is deprecated and will be removed " - "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", - category=FutureWarning, - stacklevel=2, - ) - self.request(method, url, body=body, headers=headers, chunked=True) - - def getresponse( # type: ignore[override] - self, - ) -> HTTPResponse: - """ - Get the response from the server. - - If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. - - If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. - """ - # Raise the same error as http.client.HTTPConnection - if self._response_options is None: - raise ResponseNotReady() - - # Reset this attribute for being used again. - resp_options = self._response_options - self._response_options = None - - # Since the connection's timeout value may have been updated - # we need to set the timeout on the socket. - self.sock.settimeout(self.timeout) - - # This is needed here to avoid circular import errors - from .response import HTTPResponse - - # Save a reference to the shutdown function before ownership is passed - # to httplib_response - # TODO should we implement it everywhere? - _shutdown = getattr(self.sock, "shutdown", None) - - # Get the response from http.client.HTTPConnection - httplib_response = super().getresponse() - - try: - assert_header_parsing(httplib_response.msg) - except (HeaderParsingError, TypeError) as hpe: - log.warning( - "Failed to parse headers (url=%s): %s", - _url_from_connection(self, resp_options.request_url), - hpe, - exc_info=True, - ) - - headers = HTTPHeaderDict(httplib_response.msg.items()) - - response = HTTPResponse( - body=httplib_response, - headers=headers, - status=httplib_response.status, - version=httplib_response.version, - version_string=getattr(self, "_http_vsn_str", "HTTP/?"), - reason=httplib_response.reason, - preload_content=resp_options.preload_content, - decode_content=resp_options.decode_content, - original_response=httplib_response, - enforce_content_length=resp_options.enforce_content_length, - request_method=resp_options.request_method, - request_url=resp_options.request_url, - sock_shutdown=_shutdown, - ) - return response - - -class HTTPSConnection(HTTPConnection): - """ - Many of the parameters to this constructor are passed to the underlying SSL - socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. - """ - - default_port = port_by_scheme["https"] # type: ignore[misc] - - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_fingerprint: str | None = None - _connect_callback: typing.Callable[..., None] | None = None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - # cert_reqs depends on ssl_context so calculate last. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - self.cert_reqs = cert_reqs - self._connect_callback = None - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - """ - This method should only be called once, before the connection is used. - """ - warnings.warn( - "HTTPSConnection.set_cert() is deprecated and will be removed " - "in urllib3 v3.0. Instead provide the parameters to the " - "HTTPSConnection constructor.", - category=FutureWarning, - stacklevel=2, - ) - - # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also - # have an SSLContext object in which case we'll use its verify_mode. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - def connect(self) -> None: - # Today we don't need to be doing this step before the /actual/ socket - # connection, however in the future we'll need to decide whether to - # create a new socket or re-use an existing "shared" socket as a part - # of the HTTP/2 handshake dance. - if self._tunnel_host is not None and self._tunnel_port is not None: - probe_http2_host = self._tunnel_host - probe_http2_port = self._tunnel_port - else: - probe_http2_host = self.host - probe_http2_port = self.port - - # Check if the target origin supports HTTP/2. - # If the value comes back as 'None' it means that the current thread - # is probing for HTTP/2 support. Otherwise, we're waiting for another - # probe to complete, or we get a value right away. - target_supports_http2: bool | None - if "h2" in ssl_.ALPN_PROTOCOLS: - target_supports_http2 = http2_probe.acquire_and_get( - host=probe_http2_host, port=probe_http2_port - ) - else: - # If HTTP/2 isn't going to be offered it doesn't matter if - # the target supports HTTP/2. Don't want to make a probe. - target_supports_http2 = False - - if self._connect_callback is not None: - self._connect_callback( - "before connect", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - try: - sock: socket.socket | ssl.SSLSocket - self.sock = sock = self._new_conn() - server_hostname: str = self.host - tls_in_tls = False - - # Do we need to establish a tunnel? - if self.proxy_is_tunneling: - # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. - if self._tunnel_scheme == "https": - # _connect_tls_proxy will verify and assign proxy_is_verified - self.sock = sock = self._connect_tls_proxy(self.host, sock) - tls_in_tls = True - elif self._tunnel_scheme == "http": - self.proxy_is_verified = False - - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - self._tunnel() - # Override the host with the one we're requesting data from. - server_hostname = typing.cast(str, self._tunnel_host) - - if self.server_hostname is not None: - server_hostname = self.server_hostname - - is_time_off = datetime.date.today() < RECENT_DATE - if is_time_off: - warnings.warn( - ( - f"System time is way off (before {RECENT_DATE}). This will probably " - "lead to SSL verification errors" - ), - SystemTimeWarning, - ) - - # Remove trailing '.' from fqdn hostnames to allow certificate validation - server_hostname_rm_dot = server_hostname.rstrip(".") - - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock=sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - server_hostname=server_hostname_rm_dot, - ssl_context=self.ssl_context, - tls_in_tls=tls_in_tls, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ) - self.sock = sock_and_verified.socket - - # If an error occurs during connection/handshake we may need to release - # our lock so another connection can probe the origin. - except BaseException: - if self._connect_callback is not None: - self._connect_callback( - "after connect failure", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - if target_supports_http2 is None: - http2_probe.set_and_release( - host=probe_http2_host, port=probe_http2_port, supports_http2=None - ) - raise - - # If this connection doesn't know if the origin supports HTTP/2 - # we report back to the HTTP/2 probe our result. - if target_supports_http2 is None: - supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" - http2_probe.set_and_release( - host=probe_http2_host, - port=probe_http2_port, - supports_http2=supports_http2, - ) - - # Forwarding proxies can never have a verified target since - # the proxy is the one doing the verification. Should instead - # use a CONNECT tunnel in order to verify the target. - # See: https://github.com/urllib3/urllib3/issues/3267. - if self.proxy_is_forwarding: - self.is_verified = False - else: - self.is_verified = sock_and_verified.is_verified - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - # Set `self.proxy_is_verified` unless it's already set while - # establishing a tunnel. - if self._has_connected_to_proxy and self.proxy_is_verified is None: - self.proxy_is_verified = sock_and_verified.is_verified - - def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: - """ - Establish a TLS connection to the proxy using the provided SSL context. - """ - # `_connect_tls_proxy` is called when self._tunnel_host is truthy. - proxy_config = typing.cast(ProxyConfig, self.proxy_config) - ssl_context = proxy_config.ssl_context - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - server_hostname=hostname, - ssl_context=ssl_context, - assert_hostname=proxy_config.assert_hostname, - assert_fingerprint=proxy_config.assert_fingerprint, - # Features that aren't implemented for proxies yet: - cert_file=None, - key_file=None, - key_password=None, - tls_in_tls=False, - ) - self.proxy_is_verified = sock_and_verified.is_verified - return sock_and_verified.socket # type: ignore[return-value] - - -class _WrappedAndVerifiedSocket(typing.NamedTuple): - """ - Wrapped socket and whether the connection is - verified after the TLS handshake - """ - - socket: ssl.SSLSocket | SSLTransport - is_verified: bool - - -def _ssl_wrap_socket_and_match_hostname( - sock: socket.socket, - *, - cert_reqs: None | str | int, - ssl_version: None | str | int, - ssl_minimum_version: int | None, - ssl_maximum_version: int | None, - cert_file: str | None, - key_file: str | None, - key_password: str | None, - ca_certs: str | None, - ca_cert_dir: str | None, - ca_cert_data: None | str | bytes, - assert_hostname: None | str | typing.Literal[False], - assert_fingerprint: str | None, - server_hostname: str | None, - ssl_context: ssl.SSLContext | None, - tls_in_tls: bool = False, -) -> _WrappedAndVerifiedSocket: - """Logic for constructing an SSLContext from all TLS parameters, passing - that down into ssl_wrap_socket, and then doing certificate verification - either via hostname or fingerprint. This function exists to guarantee - that both proxies and targets have the same behavior when connecting via TLS. - """ - default_ssl_context = False - if ssl_context is None: - default_ssl_context = True - context = create_urllib3_context( - ssl_version=resolve_ssl_version(ssl_version), - ssl_minimum_version=ssl_minimum_version, - ssl_maximum_version=ssl_maximum_version, - cert_reqs=resolve_cert_reqs(cert_reqs), - ) - else: - context = ssl_context - - context.verify_mode = resolve_cert_reqs(cert_reqs) - - # In some cases, we want to verify hostnames ourselves - if ( - # `ssl` can't verify fingerprints or alternate hostnames - assert_fingerprint - or assert_hostname - # assert_hostname can be set to False to disable hostname checking - or assert_hostname is False - # We still support OpenSSL 1.0.2, which prevents us from verifying - # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 - or ssl_.IS_PYOPENSSL - or not ssl_.HAS_NEVER_CHECK_COMMON_NAME - ): - context.check_hostname = False - - # Try to load OS default certs if none are given. We need to do the hasattr() check - # for custom pyOpenSSL SSLContext objects because they don't support - # load_default_certs(). - if ( - not ca_certs - and not ca_cert_dir - and not ca_cert_data - and default_ssl_context - and hasattr(context, "load_default_certs") - ): - context.load_default_certs() - - # Ensure that IPv6 addresses are in the proper format and don't have a - # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses - # and interprets them as DNS hostnames. - if server_hostname is not None: - normalized = server_hostname.strip("[]") - if "%" in normalized: - normalized = normalized[: normalized.rfind("%")] - if is_ipaddress(normalized): - server_hostname = normalized - - ssl_sock = ssl_wrap_socket( - sock=sock, - keyfile=key_file, - certfile=cert_file, - key_password=key_password, - ca_certs=ca_certs, - ca_cert_dir=ca_cert_dir, - ca_cert_data=ca_cert_data, - server_hostname=server_hostname, - ssl_context=context, - tls_in_tls=tls_in_tls, - ) - - try: - if assert_fingerprint: - _assert_fingerprint( - ssl_sock.getpeercert(binary_form=True), assert_fingerprint - ) - elif ( - context.verify_mode != ssl.CERT_NONE - and not context.check_hostname - and assert_hostname is not False - ): - cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] - - # Need to signal to our match_hostname whether to use 'commonName' or not. - # If we're using our own constructed SSLContext we explicitly set 'False' - # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. - if default_ssl_context: - hostname_checks_common_name = False - else: - hostname_checks_common_name = ( - getattr(context, "hostname_checks_common_name", False) or False - ) - - _match_hostname( - cert, - assert_hostname or server_hostname, # type: ignore[arg-type] - hostname_checks_common_name, - ) - - return _WrappedAndVerifiedSocket( - socket=ssl_sock, - is_verified=context.verify_mode == ssl.CERT_REQUIRED - or bool(assert_fingerprint), - ) - except BaseException: - ssl_sock.close() - raise - - -def _match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - asserted_hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - # Our upstream implementation of ssl.match_hostname() - # only applies this normalization to IP addresses so it doesn't - # match DNS SANs so we do the same thing! - stripped_hostname = asserted_hostname.strip("[]") - if is_ipaddress(stripped_hostname): - asserted_hostname = stripped_hostname - - try: - match_hostname(cert, asserted_hostname, hostname_checks_common_name) - except CertificateError as e: - log.warning( - "Certificate did not match expected hostname: %s. Certificate: %s", - asserted_hostname, - cert, - ) - # Add cert to exception and reraise so client code can inspect - # the cert when catching the exception, if they want to - e._peer_cert = cert # type: ignore[attr-defined] - raise - - -def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: - # Look for the phrase 'wrong version number', if found - # then we should warn the user that we're very sure that - # this proxy is HTTP-only and they have a configuration issue. - error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) - is_likely_http_proxy = ( - "wrong version number" in error_normalized - or "unknown protocol" in error_normalized - or "record layer failure" in error_normalized - ) - http_proxy_warning = ( - ". Your proxy appears to only use HTTP and not HTTPS, " - "try changing your proxy URL to be HTTP. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#https-proxy-error-http-proxy" - ) - new_err = ProxyError( - f"Unable to connect to proxy" - f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", - err, - ) - new_err.__cause__ = err - return new_err - - -def _get_default_user_agent() -> str: - return f"python-urllib3/{__version__}" - - -class DummyConnection: - """Used to detect a failed ConnectionCls import.""" - - -if not ssl: - HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 - - -VerifiedHTTPSConnection = HTTPSConnection - - -def _url_from_connection( - conn: HTTPConnection | HTTPSConnection, path: str | None = None -) -> str: - """Returns the URL from a given connection. This is mainly used for testing and logging.""" - - scheme = "https" if isinstance(conn, HTTPSConnection) else "http" - - return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py deleted file mode 100644 index 70fbc5e725..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/connectionpool.py +++ /dev/null @@ -1,1191 +0,0 @@ -from __future__ import annotations - -import errno -import logging -import queue -import sys -import typing -import warnings -import weakref -from socket import timeout as SocketTimeout -from types import TracebackType - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._request_methods import RequestMethods -from .connection import ( - BaseSSLError, - BrokenPipeError, - DummyConnection, - HTTPConnection, - HTTPException, - HTTPSConnection, - ProxyConfig, - _wrap_proxy_error, -) -from .connection import port_by_scheme as port_by_scheme -from .exceptions import ( - ClosedPoolError, - EmptyPoolError, - FullPoolError, - HostChangedError, - InsecureRequestWarning, - LocationValueError, - MaxRetryError, - NewConnectionError, - ProtocolError, - ProxyError, - ReadTimeoutError, - SSLError, - TimeoutError, -) -from .response import BaseHTTPResponse -from .util.connection import is_connection_dropped -from .util.proxy import connection_requires_http_tunnel -from .util.request import _TYPE_BODY_POSITION, set_file_position -from .util.retry import Retry -from .util.ssl_match_hostname import CertificateError -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout -from .util.url import Url, _encode_target -from .util.url import _normalize_host as normalize_host -from .util.url import parse_url -from .util.util import to_str - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - - from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - -_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] - - -# Pool objects -class ConnectionPool: - """ - Base class for all connection pools, such as - :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. - - .. note:: - ConnectionPool.urlopen() does not normalize or percent-encode target URIs - which is useful if your target server doesn't support percent-encoded - target URIs. - """ - - scheme: str | None = None - QueueCls = queue.LifoQueue - - def __init__(self, host: str, port: int | None = None) -> None: - if not host: - raise LocationValueError("No host specified.") - - self.host = _normalize_host(host, scheme=self.scheme) - self.port = port - - # This property uses 'normalize_host()' (not '_normalize_host()') - # to avoid removing square braces around IPv6 addresses. - # This value is sent to `HTTPConnection.set_tunnel()` if called - # because square braces are required for HTTP CONNECT tunneling. - self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.close() - # Return False to re-raise any potential exceptions - return False - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - - -# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 -_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} - - -class HTTPConnectionPool(ConnectionPool, RequestMethods): - """ - Thread-safe connection pool for one host. - - :param host: - Host used for this HTTP Connection (e.g. "localhost"), passed into - :class:`http.client.HTTPConnection`. - - :param port: - Port used for this HTTP Connection (None is equivalent to 80), passed - into :class:`http.client.HTTPConnection`. - - :param timeout: - Socket timeout in seconds for each individual connection. This can - be a float or integer, which sets the timeout for the HTTP request, - or an instance of :class:`urllib3.util.Timeout` which gives you more - fine-grained control over request timeouts. After the constructor has - been parsed, this is always a `urllib3.util.Timeout` object. - - :param maxsize: - Number of connections to save that can be reused. More than 1 is useful - in multithreaded situations. If ``block`` is set to False, more - connections will be created but they will not be saved once they've - been used. - - :param block: - If set to True, no more than ``maxsize`` connections will be used at - a time. When no free connections are available, the call will block - until a connection has been released. This is a useful side effect for - particular multithreaded situations where one does not want to use more - than maxsize connections per host to prevent flooding. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param retries: - Retry configuration to use by default with requests in this pool. - - :param _proxy: - Parsed proxy URL, should not be used directly, instead, see - :class:`urllib3.ProxyManager` - - :param _proxy_headers: - A dictionary with proxy headers, should not be used directly, - instead, see :class:`urllib3.ProxyManager` - - :param \\**conn_kw: - Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, - :class:`urllib3.connection.HTTPSConnection` instances. - """ - - scheme = "http" - ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - _proxy_config: ProxyConfig | None = None, - **conn_kw: typing.Any, - ): - ConnectionPool.__init__(self, host, port) - RequestMethods.__init__(self, headers) - - if not isinstance(timeout, Timeout): - timeout = Timeout.from_float(timeout) - - if retries is None: - retries = Retry.DEFAULT - - self.timeout = timeout - self.retries = retries - - self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) - self.block = block - - self.proxy = _proxy - self.proxy_headers = _proxy_headers or {} - self.proxy_config = _proxy_config - - # Fill the queue up so that doing get() on it will block properly - for _ in range(maxsize): - self.pool.put(None) - - # These are mostly for testing and debugging purposes. - self.num_connections = 0 - self.num_requests = 0 - self.conn_kw = conn_kw - - if self.proxy: - # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. - # Defaulting `socket_options` to an empty list avoids it defaulting to - # ``HTTPConnection.default_socket_options``. - self.conn_kw.setdefault("socket_options", []) - - self.conn_kw["proxy"] = self.proxy - self.conn_kw["proxy_config"] = self.proxy_config - - # Do not pass 'self' as callback to 'finalize'. - # Then the 'finalize' would keep an endless living (leak) to self. - # By just passing a reference to the pool allows the garbage collector - # to free self if nobody else has a reference to it. - pool = self.pool - - # Close all the HTTPConnections in the pool before the - # HTTPConnectionPool object is garbage collected. - weakref.finalize(self, _close_pool_connections, pool) - - def _new_conn(self) -> BaseHTTPConnection: - """ - Return a fresh :class:`HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTP connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "80", - ) - - conn = self.ConnectionCls( - host=self.host, - port=self.port, - timeout=self.timeout.connect_timeout, - **self.conn_kw, - ) - return conn - - def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: - """ - Get a connection. Will return a pooled connection if one is available. - - If no connections are available and :prop:`.block` is ``False``, then a - fresh connection is returned. - - :param timeout: - Seconds to wait before giving up and raising - :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and - :prop:`.block` is ``True``. - """ - conn = None - - if self.pool is None: - raise ClosedPoolError(self, "Pool is closed.") - - try: - conn = self.pool.get(block=self.block, timeout=timeout) - - except AttributeError: # self.pool is None - raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: - - except queue.Empty: - if self.block: - raise EmptyPoolError( - self, - "Pool is empty and a new connection can't be opened due to blocking mode.", - ) from None - pass # Oh well, we'll create a new connection then - - # If this is a persistent connection, check if it got disconnected - if conn and is_connection_dropped(conn): - log.debug("Resetting dropped connection: %s", self.host) - conn.close() - - return conn or self._new_conn() - - def _put_conn(self, conn: BaseHTTPConnection | None) -> None: - """ - Put a connection back into the pool. - - :param conn: - Connection object for the current host and port as returned by - :meth:`._new_conn` or :meth:`._get_conn`. - - If the pool is already full, the connection is closed and discarded - because we exceeded maxsize. If connections are discarded frequently, - then maxsize should be increased. - - If the pool is closed, then the connection will be closed and discarded. - """ - if self.pool is not None: - try: - self.pool.put(conn, block=False) - return # Everything is dandy, done. - except AttributeError: - # self.pool is None. - pass - except queue.Full: - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - if self.block: - # This should never happen if you got the conn from self._get_conn - raise FullPoolError( - self, - "Pool reached maximum size and no more connections are allowed.", - ) from None - - log.warning( - "Connection pool is full, discarding connection: %s. Connection pool size: %s", - self.host, - self.pool.qsize(), - ) - - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - - def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: - # Nothing to do for HTTP connections. - pass - - def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: - """Helper that always returns a :class:`urllib3.util.Timeout`""" - if timeout is _DEFAULT_TIMEOUT: - return self.timeout.clone() - - if isinstance(timeout, Timeout): - return timeout.clone() - else: - # User passed us an int/float. This is for backwards compatibility, - # can be removed later - return Timeout.from_float(timeout) - - def _raise_timeout( - self, - err: BaseSSLError | OSError | SocketTimeout, - url: str, - timeout_value: _TYPE_TIMEOUT | None, - ) -> None: - """Is the error actually a timeout? Will raise a ReadTimeout or pass""" - - if isinstance(err, SocketTimeout): - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - # See the above comment about EAGAIN in Python 3. - if hasattr(err, "errno") and err.errno in _blocking_errnos: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - def _make_request( - self, - conn: BaseHTTPConnection, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | None = None, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - chunked: bool = False, - response_conn: BaseHTTPConnection | None = None, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> BaseHTTPResponse: - """ - Perform a request on a given urllib connection object taken from our - pool. - - :param conn: - a connection from one of our connection pools - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - Pass ``None`` to retry until you receive a response. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param response_conn: - Set this to ``None`` if you will handle releasing the connection or - set the connection to have the response release it. - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - self.num_requests += 1 - - timeout_obj = self._get_timeout(timeout) - timeout_obj.start_connect() - conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) - - try: - # Trigger any extra validation we need to do. - try: - self._validate_conn(conn) - except (SocketTimeout, BaseSSLError) as e: - self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) - raise - - # _validate_conn() starts the connection to an HTTPS proxy - # so we need to wrap errors with 'ProxyError' here too. - except ( - OSError, - NewConnectionError, - TimeoutError, - BaseSSLError, - CertificateError, - SSLError, - ) as e: - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - # If the connection didn't successfully connect to it's proxy - # then there - if isinstance( - new_e, (OSError, NewConnectionError, TimeoutError, SSLError) - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - raise new_e - - # conn.request() calls http.client.*.request, not the method in - # urllib3.request. It also calls makefile (recv) on the socket. - try: - conn.request( - method, - url, - body=body, - headers=headers, - chunked=chunked, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - # We are swallowing BrokenPipeError (errno.EPIPE) since the server is - # legitimately able to close the connection after sending a valid response. - # With this behaviour, the received response is still readable. - except BrokenPipeError: - pass - except OSError as e: - # MacOS/Linux - # EPROTOTYPE and ECONNRESET are needed on macOS - # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ - # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. - if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: - raise - - # Reset the timeout for the recv() on the socket - read_timeout = timeout_obj.read_timeout - - if not conn.is_closed: - # In Python 3 socket.py will catch EAGAIN and return None when you - # try and read into the file pointer created by http.client, which - # instead raises a BadStatusLine exception. Instead of catching - # the exception and assuming all BadStatusLine exceptions are read - # timeouts, check for a zero timeout before making the request. - if read_timeout == 0: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={read_timeout})" - ) - conn.timeout = read_timeout - - # Receive the response from the server - try: - response = conn.getresponse() - except (BaseSSLError, OSError) as e: - self._raise_timeout(err=e, url=url, timeout_value=read_timeout) - raise - - # Set properties that are used by the pooling layer. - response.retries = retries - response._connection = response_conn # type: ignore[attr-defined] - response._pool = self # type: ignore[attr-defined] - - log.debug( - '%s://%s:%s "%s %s %s" %s %s', - self.scheme, - self.host, - self.port, - method, - url, - response.version_string, - response.status, - response.length_remaining, - ) - - return response - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - if self.pool is None: - return - # Disable access to the pool - old_pool, self.pool = self.pool, None - - # Close all the HTTPConnections in the pool. - _close_pool_connections(old_pool) - - def is_same_host(self, url: str) -> bool: - """ - Check if the given ``url`` is a member of the same host as this - connection pool. - """ - if url.startswith("/"): - return True - - # TODO: Add optional support for socket.gethostbyname checking. - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - if host is not None: - host = _normalize_host(host, scheme=scheme) - - # Use explicit default port for comparison when none is given - if self.port and not port: - port = port_by_scheme.get(scheme) - elif not self.port and port == port_by_scheme.get(scheme): - port = None - - return (scheme, host, port) == (self.scheme, self.host, self.port) - - def urlopen( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - redirect: bool = True, - assert_same_host: bool = True, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - pool_timeout: int | None = None, - release_conn: bool | None = None, - chunked: bool = False, - body_pos: _TYPE_BODY_POSITION | None = None, - preload_content: bool = True, - decode_content: bool = True, - **response_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Get a connection from the pool and perform an HTTP request. This is the - lowest level call for making a request, so you'll need to specify all - the raw details. - - .. note:: - - More commonly, it's appropriate to use a convenience method - such as :meth:`request`. - - .. note:: - - `release_conn` will only behave as expected if - `preload_content=False` because we want to make - `preload_content=False` the default behaviour someday soon without - breaking backwards compatibility. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param assert_same_host: - If ``True``, will make sure that the host of the pool requests is - consistent else will raise HostChangedError. When ``False``, you can - use the pool on an HTTP proxy and request foreign hosts. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param pool_timeout: - If set and the pool is set to block=True, then this method will - block for ``pool_timeout`` seconds and raise EmptyPoolError if no - connection is available within the time period. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param release_conn: - If False, then the urlopen call will not release the connection - back into the pool once a response is received (but will release if - you read the entire contents of the response such as when - `preload_content=True`). This is useful if you're not preloading - the response's content immediately. You will need to call - ``r.release_conn()`` on the response ``r`` to return the connection - back into the pool. If None, it takes the value of ``preload_content`` - which defaults to ``True``. - - :param bool chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param int body_pos: - Position to seek to in file-like body in the event of a retry or - redirect. Typically this won't need to be set because urllib3 will - auto-populate the value when needed. - """ - # Ensure that the URL we're connecting to is properly encoded - if url.startswith("/"): - # URLs starting with / are inherently schemeless. - url = to_str(_encode_target(url)) - destination_scheme = None - else: - parsed_url = parse_url(url) - destination_scheme = parsed_url.scheme - url = to_str(parsed_url.url) - - if headers is None: - headers = self.headers - - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect, default=self.retries) - - if release_conn is None: - release_conn = preload_content - - # Check host - if assert_same_host and not self.is_same_host(url): - raise HostChangedError(self, url, retries) - - conn = None - - # Track whether `conn` needs to be released before - # returning/raising/recursing. Update this variable if necessary, and - # leave `release_conn` constant throughout the function. That way, if - # the function recurses, the original value of `release_conn` will be - # passed down into the recursive call, and its value will be respected. - # - # See issue #651 [1] for details. - # - # [1] - release_this_conn = release_conn - - http_tunnel_required = connection_requires_http_tunnel( - self.proxy, self.proxy_config, destination_scheme - ) - - # Merge the proxy headers. Only done when not using HTTP CONNECT. We - # have to copy the headers dict so we can safely change it without those - # changes being reflected in anyone else's copy. - if not http_tunnel_required: - headers = headers.copy() # type: ignore[attr-defined] - headers.update(self.proxy_headers) # type: ignore[union-attr] - - # Must keep the exception bound to a separate variable or else Python 3 - # complains about UnboundLocalError. - err = None - - # Keep track of whether we cleanly exited the except block. This - # ensures we do proper cleanup in finally. - clean_exit = False - - # Rewind body position, if needed. Record current position - # for future rewinds in the event of a redirect/retry. - body_pos = set_file_position(body, body_pos) - - try: - # Request a connection from the queue. - timeout_obj = self._get_timeout(timeout) - conn = self._get_conn(timeout=pool_timeout) - - conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] - - # Is this a closed/new connection that requires CONNECT tunnelling? - if self.proxy is not None and http_tunnel_required and conn.is_closed: - try: - self._prepare_proxy(conn) - except (BaseSSLError, OSError, SocketTimeout) as e: - self._raise_timeout( - err=e, url=self.proxy.url, timeout_value=conn.timeout - ) - raise - - # If we're going to release the connection in ``finally:``, then - # the response doesn't need to know about the connection. Otherwise - # it will also try to release it and we'll have a double-release - # mess. - response_conn = conn if not release_conn else None - - # Make the request on the HTTPConnection object - response = self._make_request( - conn, - method, - url, - timeout=timeout_obj, - body=body, - headers=headers, - chunked=chunked, - retries=retries, - response_conn=response_conn, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Everything went great! - clean_exit = True - - except EmptyPoolError: - # Didn't get a connection from the pool, no need to clean up - clean_exit = True - release_this_conn = False - raise - - except ( - TimeoutError, - HTTPException, - OSError, - ProtocolError, - BaseSSLError, - SSLError, - CertificateError, - ProxyError, - ) as e: - # Discard the connection for these exceptions. It will be - # replaced during the next _get_conn() call. - clean_exit = False - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - if isinstance( - new_e, - ( - OSError, - NewConnectionError, - TimeoutError, - SSLError, - HTTPException, - ), - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - elif isinstance(new_e, (OSError, HTTPException)): - new_e = ProtocolError("Connection aborted.", new_e) - - retries = retries.increment( - method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] - ) - retries.sleep() - - # Keep track of the error for the retry warning. - err = e - - finally: - if not clean_exit: - # We hit some kind of exception, handled or otherwise. We need - # to throw the connection away unless explicitly told not to. - # Close the connection, set the variable to None, and make sure - # we put the None back in the pool to avoid leaking it. - if conn: - conn.close() - conn = None - release_this_conn = True - - if release_this_conn: - # Put the connection back to be reused. If the connection is - # expired then it will be None, which will get replaced with a - # fresh connection during _get_conn. - self._put_conn(conn) - - if not conn: - # Try again - log.warning( - "Retrying (%r) after connection broken by '%r': %s", retries, err, url - ) - return self.urlopen( - method, - url, - body, - headers, - retries, - redirect, - assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Handle redirect? - redirect_location = redirect and response.get_redirect_location() - if redirect_location: - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - body = None - headers = HTTPHeaderDict(headers)._prepare_for_method_change() - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # self.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not self.is_same_host( - redirect_location - ): - new_headers = headers.copy() # type: ignore[union-attr] - for header in headers: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - headers = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep_for_retry(response) - log.debug("Redirecting %s -> %s", url, redirect_location) - return self.urlopen( - method, - redirect_location, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Check if we should retry the HTTP response. - has_retry_after = bool(response.headers.get("Retry-After")) - if retries.is_retry(method, response.status, has_retry_after): - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_status: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep(response) - log.debug("Retry: %s", url) - return self.urlopen( - method, - url, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - return response - - -class HTTPSConnectionPool(HTTPConnectionPool): - """ - Same as :class:`.HTTPConnectionPool`, but HTTPS. - - :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, - ``assert_hostname`` and ``host`` in this order to verify connections. - If ``assert_hostname`` is False, no verification is done. - - The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, - ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` - is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade - the connection socket into an SSL socket. - """ - - scheme = "https" - ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - ssl_version: int | str | None = None, - ssl_minimum_version: ssl.TLSVersion | None = None, - ssl_maximum_version: ssl.TLSVersion | None = None, - assert_hostname: str | typing.Literal[False] | None = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - **conn_kw: typing.Any, - ) -> None: - super().__init__( - host, - port, - timeout, - maxsize, - block, - headers, - retries, - _proxy, - _proxy_headers, - **conn_kw, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.ca_certs = ca_certs - self.ca_cert_dir = ca_cert_dir - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - - def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] - """Establishes a tunnel connection through HTTP CONNECT.""" - if self.proxy and self.proxy.scheme == "https": - tunnel_scheme = "https" - else: - tunnel_scheme = "http" - - conn.set_tunnel( - scheme=tunnel_scheme, - host=self._tunnel_host, - port=self.port, - headers=self.proxy_headers, - ) - conn.connect() - - def _new_conn(self) -> BaseHTTPSConnection: - """ - Return a fresh :class:`urllib3.connection.HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTPS connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "443", - ) - - if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] - raise ImportError( - "Can't connect to HTTPS URL because the SSL module is not available." - ) - - actual_host: str = self.host - actual_port = self.port - if self.proxy is not None and self.proxy.host is not None: - actual_host = self.proxy.host - actual_port = self.proxy.port - - return self.ConnectionCls( - host=actual_host, - port=actual_port, - timeout=self.timeout.connect_timeout, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - **self.conn_kw, - ) - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - super()._validate_conn(conn) - - # Force connect early to allow us to validate the connection. - if conn.is_closed: - conn.connect() - - # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 - if not conn.is_verified and not conn.proxy_is_verified: - warnings.warn( - ( - f"Unverified HTTPS request is being made to host '{conn.host}'. " - "Adding certificate verification is strongly advised. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#tls-warnings" - ), - InsecureRequestWarning, - ) - - -def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: - """ - Given a url, return an :class:`.ConnectionPool` instance of its host. - - This is a shortcut for not having to parse out the scheme, host, and port - of the url before creating an :class:`.ConnectionPool` instance. - - :param url: - Absolute URL string that must include the scheme. Port is optional. - - :param \\**kw: - Passes additional parameters to the constructor of the appropriate - :class:`.ConnectionPool`. Useful for specifying things like - timeout, maxsize, headers, etc. - - Example:: - - >>> conn = connection_from_url('http://google.com/') - >>> r = conn.request('GET', '/') - """ - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - port = port or port_by_scheme.get(scheme, 80) - if scheme == "https": - return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - else: - return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - """ - Normalize hosts for comparisons and use with sockets. - """ - - host = normalize_host(host, scheme) - - # httplib doesn't like it when we include brackets in IPv6 addresses - # Specifically, if we include brackets but also pass the port then - # httplib crazily doubles up the square brackets on the Host header. - # Instead, we need to make sure we never pass ``None`` as the port. - # However, for backward compatibility reasons we can't actually - # *assert* that. See http://bugs.python.org/issue28539 - if host and host.startswith("[") and host.endswith("]"): - host = host[1:-1] - return host - - -def _url_from_pool( - pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None -) -> str: - """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" - return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url - - -def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: - """Drains a queue of connections and closes each one.""" - try: - while True: - conn = pool.get(block=False) - if conn: - conn.close() - except queue.Empty: - pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py deleted file mode 100644 index e5b62b25e9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import urllib3.connection - -from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection - - -def inject_into_urllib3() -> None: - # override connection classes to use emscripten specific classes - # n.b. mypy complains about the overriding of classes below - # if it isn't ignored - HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection - HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection - urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] - urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] - urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py deleted file mode 100644 index 63f79dd3be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/connection.py +++ /dev/null @@ -1,260 +0,0 @@ -from __future__ import annotations - -import os -import typing - -# use http.client.HTTPException for consistency with non-emscripten -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady - -from ..._base_connection import _TYPE_BODY -from ...connection import HTTPConnection, ProxyConfig, port_by_scheme -from ...exceptions import TimeoutError -from ...response import BaseHTTPResponse -from ...util.connection import _TYPE_SOCKET_OPTIONS -from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from ...util.url import Url -from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request -from .request import EmscriptenRequest -from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - - -class EmscriptenHTTPConnection: - default_port: typing.ClassVar[int] = port_by_scheme["http"] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - timeout: None | (float) - - host: str - port: int - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool = False - proxy_is_verified: bool | None = None - - response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper - _response: EmscriptenResponse | None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - self.host = host - self.port = port - self.timeout = timeout if isinstance(timeout, float) else 0.0 - self.scheme = "http" - self._closed = True - self._response = None - # ignore these things because we don't - # have control over that stuff - self.proxy = None - self.proxy_config = None - self.blocksize = blocksize - self.source_address = None - self.socket_options = None - self.is_verified = False - - def set_tunnel( - self, - host: str, - port: int | None = 0, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - pass - - def connect(self) -> None: - pass - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - self._closed = False - if url.startswith("/"): - if self.port is not None: - port = f":{self.port}" - else: - port = "" - # no scheme / host / port included, make a full url - url = f"{self.scheme}://{self.host}{port}{url}" - request = EmscriptenRequest( - url=url, - method=method, - timeout=self.timeout if self.timeout else 0, - decode_content=decode_content, - ) - request.set_body(body) - if headers: - for k, v in headers.items(): - request.set_header(k, v) - self._response = None - try: - if not preload_content: - self._response = send_streaming_request(request) - if self._response is None: - self._response = send_request(request) - except _TimeoutError as e: - raise TimeoutError(e.message) from e - except _RequestError as e: - raise HTTPException(e.message) from e - - def getresponse(self) -> BaseHTTPResponse: - if self._response is not None: - return EmscriptenHttpResponseWrapper( - internal_response=self._response, - url=self._response.request.url, - connection=self, - ) - else: - raise ResponseNotReady() - - def close(self) -> None: - self._closed = True - self._response = None - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - return self._closed - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - return True - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - return False - - -class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): - default_port = port_by_scheme["https"] - # all this is basically ignored, as browser handles https - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - cert_file: str | None - key_file: str | None - key_password: str | None - ssl_context: typing.Any | None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None = None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: ( - None | _TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: typing.Any | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - self.scheme = "https" - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - self.cert_reqs = None - - # The browser will automatically verify all requests. - # We have no control over that setting. - self.is_verified = True - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - pass - - -# verify that this class implements BaseHTTP(s) connection correctly -if typing.TYPE_CHECKING: - _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) - _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js deleted file mode 100644 index faf141e1fa..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/emscripten_fetch_worker.js +++ /dev/null @@ -1,110 +0,0 @@ -let Status = { - SUCCESS_HEADER: -1, - SUCCESS_EOF: -2, - ERROR_TIMEOUT: -3, - ERROR_EXCEPTION: -4, -}; - -let connections = new Map(); -let nextConnectionID = 1; -const encoder = new TextEncoder(); - -self.addEventListener("message", async function (event) { - if (event.data.close) { - let connectionID = event.data.close; - connections.delete(connectionID); - return; - } else if (event.data.getMore) { - let connectionID = event.data.getMore; - let { curOffset, value, reader, intBuffer, byteBuffer } = - connections.get(connectionID); - // if we still have some in buffer, then just send it back straight away - if (!value || curOffset >= value.length) { - // read another buffer if required - try { - let readResponse = await reader.read(); - - if (readResponse.done) { - // read everything - clear connection and return - connections.delete(connectionID); - Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); - Atomics.notify(intBuffer, 0); - // finished reading successfully - // return from event handler - return; - } - curOffset = 0; - connections.get(connectionID).value = readResponse.value; - value = readResponse.value; - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } - - // send as much buffer as we can - let curLen = value.length - curOffset; - if (curLen > byteBuffer.length) { - curLen = byteBuffer.length; - } - byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); - - Atomics.store(intBuffer, 0, curLen); // store current length in bytes - Atomics.notify(intBuffer, 0); - curOffset += curLen; - connections.get(connectionID).curOffset = curOffset; - - return; - } else { - // start fetch - let connectionID = nextConnectionID; - nextConnectionID += 1; - const intBuffer = new Int32Array(event.data.buffer); - const byteBuffer = new Uint8Array(event.data.buffer, 8); - try { - const response = await fetch(event.data.url, event.data.fetchParams); - // return the headers first via textencoder - var headers = []; - for (const pair of response.headers.entries()) { - headers.push([pair[0], pair[1]]); - } - let headerObj = { - headers: headers, - status: response.status, - connectionID, - }; - const headerText = JSON.stringify(headerObj); - let headerBytes = encoder.encode(headerText); - let written = headerBytes.length; - byteBuffer.set(headerBytes); - intBuffer[1] = written; - // make a connection - connections.set(connectionID, { - reader: response.body.getReader(), - intBuffer: intBuffer, - byteBuffer: byteBuffer, - value: undefined, - curOffset: 0, - }); - // set header ready - Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); - Atomics.notify(intBuffer, 0); - // all fetching after this goes through a new postmessage call with getMore - // this allows for parallel requests - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } -}); -self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py deleted file mode 100644 index 612cfddc4c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/fetch.py +++ /dev/null @@ -1,726 +0,0 @@ -""" -Support for streaming http requests in emscripten. - -A few caveats - - -If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled -https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md -*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the -JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case -timeouts and streaming should just work. - -Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. - -This approach has several caveats: - -Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. -Streaming only works if you're running pyodide in a web worker. - -Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch -operation, so it requires that you have crossOriginIsolation enabled, by serving over https -(or from localhost) with the two headers below set: - - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp - -You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in -JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole -request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. - -Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once -control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. - -NB: in this code, there are a lot of JavaScript objects. They are named js_* -to make it clear what type of object they are. -""" - -from __future__ import annotations - -import io -import json -from email.parser import Parser -from importlib.resources import files -from typing import TYPE_CHECKING, Any - -import js # type: ignore[import-not-found] -from pyodide.ffi import ( # type: ignore[import-not-found] - JsArray, - JsException, - JsProxy, - to_js, -) - -if TYPE_CHECKING: - from typing_extensions import Buffer - -from .request import EmscriptenRequest -from .response import EmscriptenResponse - -""" -There are some headers that trigger unintended CORS preflight requests. -See also https://github.com/koenvo/pyodide-http/issues/22 -""" -HEADERS_TO_IGNORE = ("user-agent",) - -SUCCESS_HEADER = -1 -SUCCESS_EOF = -2 -ERROR_TIMEOUT = -3 -ERROR_EXCEPTION = -4 - - -class _RequestError(Exception): - def __init__( - self, - message: str | None = None, - *, - request: EmscriptenRequest | None = None, - response: EmscriptenResponse | None = None, - ): - self.request = request - self.response = response - self.message = message - super().__init__(self.message) - - -class _StreamingError(_RequestError): - pass - - -class _TimeoutError(_RequestError): - pass - - -def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: - return to_js(dict_val, dict_converter=js.Object.fromEntries) - - -class _ReadStream(io.RawIOBase): - def __init__( - self, - int_buffer: JsArray, - byte_buffer: JsArray, - timeout: float, - worker: JsProxy, - connection_id: int, - request: EmscriptenRequest, - ): - self.int_buffer = int_buffer - self.byte_buffer = byte_buffer - self.read_pos = 0 - self.read_len = 0 - self.connection_id = connection_id - self.worker = worker - self.timeout = int(1000 * timeout) if timeout > 0 else None - self.is_live = True - self._is_closed = False - self.request: EmscriptenRequest | None = request - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.int_buffer = None - self.byte_buffer = None - self._is_closed = True - self.request = None - if self.is_live: - self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) - self.is_live = False - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def readinto(self, byte_obj: Buffer) -> int: - if not self.int_buffer: - raise _StreamingError( - "No buffer for stream in _ReadStream.readinto", - request=self.request, - response=None, - ) - if self.read_len == 0: - # wait for the worker to send something - js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) - self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) - if ( - js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) - == "timed-out" - ): - raise _TimeoutError - data_len = self.int_buffer[0] - if data_len > 0: - self.read_len = data_len - self.read_pos = 0 - elif data_len == ERROR_EXCEPTION: - string_len = self.int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", - request=self.request, - response=None, - ) - else: - # EOF, free the buffers and return zero - # and free the request - self.is_live = False - self.close() - return 0 - # copy from int32array to python bytes - ret_length = min(self.read_len, len(memoryview(byte_obj))) - subarray = self.byte_buffer.subarray( - self.read_pos, self.read_pos + ret_length - ).to_py() - memoryview(byte_obj)[0:ret_length] = subarray - self.read_len -= ret_length - self.read_pos += ret_length - return ret_length - - -class _StreamingFetcher: - def __init__(self) -> None: - # make web-worker and data buffer on startup - self.streaming_ready = False - streaming_worker_code = ( - files(__package__) - .joinpath("emscripten_fetch_worker.js") - .read_text(encoding="utf-8") - ) - js_data_blob = js.Blob.new( - to_js([streaming_worker_code], create_pyproxies=False), - _obj_from_dict({"type": "application/javascript"}), - ) - - def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: - def onMsg(e: JsProxy) -> None: - self.streaming_ready = True - js_resolve_fn(e) - - def onErr(e: JsProxy) -> None: - js_reject_fn(e) # Defensive: never happens in ci - - self.js_worker.onmessage = onMsg - self.js_worker.onerror = onErr - - js_data_url = js.URL.createObjectURL(js_data_blob) - self.js_worker = js.globalThis.Worker.new(js_data_url) - self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) - - def send(self, request: EmscriptenRequest) -> EmscriptenResponse: - headers = { - k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE - } - - body = request.body - fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} - # start the request off in the worker - timeout = int(1000 * request.timeout) if request.timeout > 0 else None - js_shared_buffer = js.SharedArrayBuffer.new(1048576) - js_int_buffer = js.Int32Array.new(js_shared_buffer) - js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) - - js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) - js.Atomics.notify(js_int_buffer, 0) - js_absolute_url = js.URL.new(request.url, js.location).href - self.js_worker.postMessage( - _obj_from_dict( - { - "buffer": js_shared_buffer, - "url": js_absolute_url, - "fetchParams": fetch_data, - } - ) - ) - # wait for the worker to send something - js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) - if js_int_buffer[0] == ERROR_TIMEOUT: - raise _TimeoutError( - "Timeout connecting to streaming request", - request=request, - response=None, - ) - elif js_int_buffer[0] == SUCCESS_HEADER: - # got response - # header length is in second int of intBuffer - string_len = js_int_buffer[1] - # decode the rest to a JSON string - js_decoder = js.TextDecoder.new() - # this does a copy (the slice) because decode can't work on shared array - # for some silly reason - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - # get it as an object - response_obj = json.loads(json_str) - return EmscriptenResponse( - request=request, - status_code=response_obj["status"], - headers=response_obj["headers"], - body=_ReadStream( - js_int_buffer, - js_byte_buffer, - request.timeout, - self.js_worker, - response_obj["connectionID"], - request, - ), - ) - elif js_int_buffer[0] == ERROR_EXCEPTION: - string_len = js_int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", request=request, response=None - ) - else: - raise _StreamingError( - f"Unknown status from worker in fetch: {js_int_buffer[0]}", - request=request, - response=None, - ) - - -class _JSPIReadStream(io.RawIOBase): - """ - A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch - response. This requires support for WebAssembly JavaScript Promise Integration - in the containing browser, and for pyodide to be launched via runPythonAsync. - - :param js_read_stream: - The JavaScript stream reader - - :param timeout: - Timeout in seconds - - :param request: - The request we're handling - - :param response: - The response this stream relates to - - :param js_abort_controller: - A JavaScript AbortController object, used for timeouts - """ - - def __init__( - self, - js_read_stream: Any, - timeout: float, - request: EmscriptenRequest, - response: EmscriptenResponse, - js_abort_controller: Any, # JavaScript AbortController for timeouts - ): - self.js_read_stream = js_read_stream - self.timeout = timeout - self._is_closed = False - self._is_done = False - self.request: EmscriptenRequest | None = request - self.response: EmscriptenResponse | None = response - self.current_buffer = None - self.current_buffer_pos = 0 - self.js_abort_controller = js_abort_controller - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.js_read_stream.cancel() - self.js_read_stream = None - self._is_closed = True - self._is_done = True - self.request = None - self.response = None - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def _get_next_buffer(self) -> bool: - result_js = _run_sync_with_timeout( - self.js_read_stream.read(), - self.timeout, - self.js_abort_controller, - request=self.request, - response=self.response, - ) - if result_js.done: - self._is_done = True - return False - else: - self.current_buffer = result_js.value.to_py() - self.current_buffer_pos = 0 - return True - - def readinto(self, byte_obj: Buffer) -> int: - if self.current_buffer is None: - if not self._get_next_buffer() or self.current_buffer is None: - self.close() - return 0 - ret_length = min( - len(byte_obj), len(self.current_buffer) - self.current_buffer_pos - ) - byte_obj[0:ret_length] = self.current_buffer[ - self.current_buffer_pos : self.current_buffer_pos + ret_length - ] - self.current_buffer_pos += ret_length - if self.current_buffer_pos == len(self.current_buffer): - self.current_buffer = None - return ret_length - - -# check if we are in a worker or not -def is_in_browser_main_thread() -> bool: - return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window - - -def is_cross_origin_isolated() -> bool: - return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated - - -def is_in_node() -> bool: - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - and hasattr(js.process.release, "name") - and js.process.release.name == "node" - ) - - -def is_worker_available() -> bool: - return hasattr(js, "Worker") and hasattr(js, "Blob") - - -_fetcher: _StreamingFetcher | None = None - -if is_worker_available() and ( - (is_cross_origin_isolated() and not is_in_browser_main_thread()) - and (not is_in_node()) -): - _fetcher = _StreamingFetcher() -else: - _fetcher = None - - -NODE_JSPI_ERROR = ( - "urllib3 only works in Node.js with pyodide.runPythonAsync" - " and requires the flag --experimental-wasm-stack-switching in " - " versions of node <24." -) - - -def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: - if has_jspi(): - return send_jspi_request(request, True) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - - if _fetcher and streaming_ready(): - return _fetcher.send(request) - else: - _show_streaming_warning() - return None - - -_SHOWN_TIMEOUT_WARNING = False - - -def _show_timeout_warning() -> None: - global _SHOWN_TIMEOUT_WARNING - if not _SHOWN_TIMEOUT_WARNING: - _SHOWN_TIMEOUT_WARNING = True - message = "Warning: Timeout is not available on main browser thread" - js.console.warn(message) - - -_SHOWN_STREAMING_WARNING = False - - -def _show_streaming_warning() -> None: - global _SHOWN_STREAMING_WARNING - if not _SHOWN_STREAMING_WARNING: - _SHOWN_STREAMING_WARNING = True - message = "Can't stream HTTP requests because: \n" - if not is_cross_origin_isolated(): - message += " Page is not cross-origin isolated\n" - if is_in_browser_main_thread(): - message += " Python is running in main browser thread\n" - if not is_worker_available(): - message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in - if streaming_ready() is False: - message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch -is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" - from js import console - - console.warn(message) - - -def send_request(request: EmscriptenRequest) -> EmscriptenResponse: - if has_jspi(): - return send_jspi_request(request, False) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - try: - js_xhr = js.XMLHttpRequest.new() - - if not is_in_browser_main_thread(): - js_xhr.responseType = "arraybuffer" - if request.timeout: - js_xhr.timeout = int(request.timeout * 1000) - else: - js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") - if request.timeout: - # timeout isn't available on the main thread - show a warning in console - # if it is set - _show_timeout_warning() - - js_xhr.open(request.method, request.url, False) - for name, value in request.headers.items(): - if name.lower() not in HEADERS_TO_IGNORE: - js_xhr.setRequestHeader(name, value) - - js_xhr.send(to_js(request.body)) - - headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) - - if not is_in_browser_main_thread(): - body = js_xhr.response.to_py().tobytes() - else: - body = js_xhr.response.encode("ISO-8859-15") - return EmscriptenResponse( - status_code=js_xhr.status, headers=headers, body=body, request=request - ) - except JsException as err: - if err.name == "TimeoutError": - raise _TimeoutError(err.message, request=request) - elif err.name == "NetworkError": - raise _RequestError(err.message, request=request) - else: - # general http error - raise _RequestError(err.message, request=request) - - -def send_jspi_request( - request: EmscriptenRequest, streaming: bool -) -> EmscriptenResponse: - """ - Send a request using WebAssembly JavaScript Promise Integration - to wrap the asynchronous JavaScript fetch api (experimental). - - :param request: - Request to send - - :param streaming: - Whether to stream the response - - :return: The response object - :rtype: EmscriptenResponse - """ - timeout = request.timeout - js_abort_controller = js.AbortController.new() - headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} - req_body = request.body - fetch_data = { - "headers": headers, - "body": to_js(req_body), - "method": request.method, - "signal": js_abort_controller.signal, - } - # Node.js returns the whole response (unlike opaqueredirect in browsers), - # so urllib3 can set `redirect: manual` to control redirects itself. - # https://stackoverflow.com/a/78524615 - if _is_node_js(): - fetch_data["redirect"] = "manual" - # Call JavaScript fetch (async api, returns a promise) - fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) - # Now suspend WebAssembly until we resolve that promise - # or time out. - response_js = _run_sync_with_timeout( - fetcher_promise_js, - timeout, - js_abort_controller, - request=request, - response=None, - ) - headers = {} - header_iter = response_js.headers.entries() - while True: - iter_value_js = header_iter.next() - if getattr(iter_value_js, "done", False): - break - else: - headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) - status_code = response_js.status - body: bytes | io.RawIOBase = b"" - - response = EmscriptenResponse( - status_code=status_code, headers=headers, body=b"", request=request - ) - if streaming: - # get via inputstream - if response_js.body is not None: - # get a reader from the fetch response - body_stream_js = response_js.body.getReader() - body = _JSPIReadStream( - body_stream_js, timeout, request, response, js_abort_controller - ) - else: - # get directly via arraybuffer - # n.b. this is another async JavaScript call. - body = _run_sync_with_timeout( - response_js.arrayBuffer(), - timeout, - js_abort_controller, - request=request, - response=response, - ).to_py() - response.body = body - return response - - -def _run_sync_with_timeout( - promise: Any, - timeout: float, - js_abort_controller: Any, - request: EmscriptenRequest | None, - response: EmscriptenResponse | None, -) -> Any: - """ - Await a JavaScript promise synchronously with a timeout which is implemented - via the AbortController - - :param promise: - Javascript promise to await - - :param timeout: - Timeout in seconds - - :param js_abort_controller: - A JavaScript AbortController object, used on timeout - - :param request: - The request being handled - - :param response: - The response being handled (if it exists yet) - - :raises _TimeoutError: If the request times out - :raises _RequestError: If the request raises a JavaScript exception - - :return: The result of awaiting the promise. - """ - timer_id = None - if timeout > 0: - timer_id = js.setTimeout( - js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) - ) - try: - from pyodide.ffi import run_sync - - # run_sync here uses WebAssembly JavaScript Promise Integration to - # suspend python until the JavaScript promise resolves. - return run_sync(promise) - except JsException as err: - if err.name == "AbortError": - raise _TimeoutError( - message="Request timed out", request=request, response=response - ) - else: - raise _RequestError(message=err.message, request=request, response=response) - finally: - if timer_id is not None: - js.clearTimeout(timer_id) - - -def has_jspi() -> bool: - """ - Return true if jspi can be used. - - This requires both browser support and also WebAssembly - to be in the correct state - i.e. that the javascript - call into python was async not sync. - - :return: True if jspi can be used. - :rtype: bool - """ - try: - from pyodide.ffi import can_run_sync, run_sync # noqa: F401 - - return bool(can_run_sync()) - except ImportError: - return False - - -def _is_node_js() -> bool: - """ - Check if we are in Node.js. - - :return: True if we are in Node.js. - :rtype: bool - """ - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - # According to the Node.js documentation, the release name is always "node". - and js.process.release.name == "node" - ) - - -def streaming_ready() -> bool | None: - if _fetcher: - return _fetcher.streaming_ready - else: - return None # no fetcher, return None to signify that - - -async def wait_for_streaming_ready() -> bool: - if _fetcher: - await _fetcher.js_worker_ready_promise - return True - else: - return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py deleted file mode 100644 index e692e692bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/request.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from ..._base_connection import _TYPE_BODY - - -@dataclass -class EmscriptenRequest: - method: str - url: str - params: dict[str, str] | None = None - body: _TYPE_BODY | None = None - headers: dict[str, str] = field(default_factory=dict) - timeout: float = 0 - decode_content: bool = True - - def set_header(self, name: str, value: str) -> None: - self.headers[name.capitalize()] = value - - def set_body(self, body: _TYPE_BODY | None) -> None: - self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py deleted file mode 100644 index ec1e1dbe83..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/emscripten/response.py +++ /dev/null @@ -1,281 +0,0 @@ -from __future__ import annotations - -import json as _json -import logging -import typing -from contextlib import contextmanager -from dataclasses import dataclass -from http.client import HTTPException as HTTPException -from io import BytesIO, IOBase - -from ...exceptions import InvalidHeader, TimeoutError -from ...response import BaseHTTPResponse -from ...util.retry import Retry -from .request import EmscriptenRequest - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - - -@dataclass -class EmscriptenResponse: - status_code: int - headers: dict[str, str] - body: IOBase | bytes - request: EmscriptenRequest - - -class EmscriptenHttpResponseWrapper(BaseHTTPResponse): - def __init__( - self, - internal_response: EmscriptenResponse, - url: str | None = None, - connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, - ): - self._pool = None # set by pool class - self._body = None - self._uncached_read_occurred = False - self._response = internal_response - self._url = url - self._connection = connection - self._closed = False - super().__init__( - headers=internal_response.headers, - status=internal_response.status_code, - request_url=url, - version=0, - version_string="HTTP/?", - reason="", - decode_content=True, - ) - self.length_remaining = self._init_length(self._response.request.method) - self.length_is_certain = False - - @property - def url(self) -> str | None: - return self._url - - @url.setter - def url(self, url: str | None) -> None: - self._url = url - - @property - def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: - return self._connection - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - while True: - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - else: - break - - def _init_length(self, request_method: str | None) -> int | None: - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Check for responses that shouldn't include a body - if ( - self.status in (204, 304) - or 100 <= self.status < 200 - or request_method == "HEAD" - ): - length = 0 - - return length - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, # ignored because browser decodes always - cache_content: bool = False, - ) -> bytes: - if ( - self._closed - or self._response is None - or (isinstance(self._response.body, IOBase) and self._response.body.closed) - ): - return b"" - - with self._error_catcher(): - # body has been preloaded as a string by XmlHttpRequest - if not isinstance(self._response.body, IOBase): - self.length_remaining = len(self._response.body) - self.length_is_certain = True - # wrap body in IOStream - self._response.body = BytesIO(self._response.body) - if amt is not None and amt >= 0: - # don't cache partial content - cache_content = False - data = self._response.body.read(amt) - self._uncached_read_occurred = True - else: # read all we can (and cache it) - data = self._response.body.read() - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - self._uncached_read_occurred = True - if self.length_remaining is not None: - self.length_remaining = max(self.length_remaining - len(data), 0) - if len(data) == 0 or ( - self.length_is_certain and self.length_remaining == 0 - ): - # definitely finished reading, close response stream - self._response.body.close() - return typing.cast(bytes, data) - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Generator[bytes]: - # chunked is handled by browser - while True: - bytes = self.read(amt, decode_content) - if not bytes: - break - yield bytes - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - self.close() - - @property - def data(self) -> bytes: - if self._body: - return self._body - else: - return self.read(cache_content=True) - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - def close(self) -> None: - if not self._closed: - if isinstance(self._response.body, IOBase): - self._response.body.close() - if self._connection: - self._connection.close() - self._connection = None - self._closed = True - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch Emscripten specific exceptions thrown by fetch.py, - instead re-raising urllib3 variants, so that low-level exceptions - are not leaked in the high-level api. - - On exit, release the connection back to the pool. - """ - from .fetch import _RequestError, _TimeoutError # avoid circular import - - clean_exit = False - - try: - yield - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - except _TimeoutError as e: - raise TimeoutError(str(e)) - except _RequestError as e: - raise HTTPException(str(e)) - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now - if ( - isinstance(self._response.body, IOBase) - and not self._response.body.closed - ): - self._response.body.close() - # release the connection back to the pool - self.release_conn() - else: - # If we have read everything from the response stream, - # return the connection back to the pool. - if ( - isinstance(self._response.body, IOBase) - and self._response.body.closed - ): - self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py deleted file mode 100644 index f06b859992..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/pyopenssl.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Module for using pyOpenSSL as a TLS backend. This module was relevant before -the standard library ``ssl`` module supported SNI, but now that we've dropped -support for Python 2.7 all relevant Python versions support SNI so -**this module is no longer recommended**. - -This needs the following packages installed: - -* `pyOpenSSL`_ (tested with 19.0.0) -* `cryptography`_ (minimum 2.3, from pyopenssl) -* `idna`_ (minimum 2.1, from cryptography) - -However, pyOpenSSL depends on cryptography, so while we use all three directly here we -end up having relatively few packages required. - -You can install them with the following command: - -.. code-block:: bash - - $ python -m pip install pyopenssl cryptography idna - -To activate certificate checking, call -:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code -before you begin making HTTP requests. This can be done in a ``sitecustomize`` -module, or at any other time before your application begins using ``urllib3``, -like this: - -.. code-block:: python - - try: - import urllib3.contrib.pyopenssl - urllib3.contrib.pyopenssl.inject_into_urllib3() - except ImportError: - pass - -.. _pyopenssl: https://www.pyopenssl.org -.. _cryptography: https://cryptography.io -.. _idna: https://github.com/kjd/idna -""" - -from __future__ import annotations - -import OpenSSL.SSL # type: ignore[import-not-found] -from cryptography import x509 - -try: - from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] -except ImportError: - # UnsupportedExtension is gone in cryptography >= 2.1.0 - class UnsupportedExtension(Exception): # type: ignore[no-redef] - pass - - -import logging -import ssl -import typing -from io import BytesIO -from socket import socket as socket_cls - -from .. import util - -if typing.TYPE_CHECKING: - from OpenSSL.crypto import X509 # type: ignore[import-not-found] - - -__all__ = ["inject_into_urllib3", "extract_from_urllib3"] - -# Map from urllib3 to PyOpenSSL compatible parameter-values. -_openssl_versions: dict[int, int] = { - util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, -} - -if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD - -if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD - - -_stdlib_to_openssl_verify = { - ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, - ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, - ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER - + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, -} -_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} - -# The SSLvX values are the most likely to be missing in the future -# but we check them all just to be sure. -_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( - OpenSSL.SSL, "OP_NO_SSLv3", 0 -) -_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) -_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) -_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) -_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) - -_openssl_to_ssl_minimum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, - ssl.TLSVersion.TLSv1_3: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), - ssl.TLSVersion.MAXIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), -} -_openssl_to_ssl_maximum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 - | _OP_NO_TLSv1 - | _OP_NO_TLSv1_1 - | _OP_NO_TLSv1_2 - | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, -} - -# OpenSSL will only write 16K at a time -SSL_WRITE_BLOCKSIZE = 16384 - -orig_util_SSLContext = util.ssl_.SSLContext - - -log = logging.getLogger(__name__) - - -def inject_into_urllib3() -> None: - "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." - - _validate_dependencies_met() - - util.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.IS_PYOPENSSL = True - util.ssl_.IS_PYOPENSSL = True - - -def extract_from_urllib3() -> None: - "Undo monkey-patching by :func:`inject_into_urllib3`." - - util.SSLContext = orig_util_SSLContext - util.ssl_.SSLContext = orig_util_SSLContext - util.IS_PYOPENSSL = False - util.ssl_.IS_PYOPENSSL = False - - -def _validate_dependencies_met() -> None: - """ - Verifies that PyOpenSSL's package-level dependencies have been met. - Throws `ImportError` if they are not met. - """ - # Method added in `cryptography==1.1`; not available in older versions - from cryptography.x509.extensions import Extensions - - if getattr(Extensions, "get_extension_for_class", None) is None: - raise ImportError( - "'cryptography' module missing required functionality. " - "Try upgrading to v1.3.4 or newer." - ) - - # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 - # attribute is only present on those versions. - from OpenSSL.crypto import X509 - - x509 = X509() - if getattr(x509, "_x509", None) is None: - raise ImportError( - "'pyOpenSSL' module missing required functionality. " - "Try upgrading to v0.14 or newer." - ) - - -def _dnsname_to_stdlib(name: str) -> str | None: - """ - Converts a dNSName SubjectAlternativeName field to the form used by the - standard library on the given Python version. - - Cryptography produces a dNSName as a unicode string that was idna-decoded - from ASCII bytes. We need to idna-encode that string to get it back, and - then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib - uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). - - If the name cannot be idna-encoded then we return None signalling that - the name given should be skipped. - """ - - def idna_encode(name: str) -> bytes | None: - """ - Borrowed wholesale from the Python Cryptography Project. It turns out - that we can't just safely call `idna.encode`: it can explode for - wildcard names. This avoids that problem. - """ - import idna - - try: - for prefix in ["*.", "."]: - if name.startswith(prefix): - name = name[len(prefix) :] - return prefix.encode("ascii") + idna.encode(name) - return idna.encode(name) - except idna.core.IDNAError: - return None - - # Don't send IPv6 addresses through the IDNA encoder. - if ":" in name: - return name - - encoded_name = idna_encode(name) - if encoded_name is None: - return None - return encoded_name.decode("utf-8") - - -def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: - """ - Given an PyOpenSSL certificate, provides all the subject alternative names. - """ - cert = peer_cert.to_cryptography() - - # We want to find the SAN extension. Ask Cryptography to locate it (it's - # faster than looping in Python) - try: - ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value - except x509.ExtensionNotFound: - # No such extension, return the empty list. - return [] - except ( - x509.DuplicateExtension, - UnsupportedExtension, - x509.UnsupportedGeneralNameType, - UnicodeError, - ) as e: - # A problem has been found with the quality of the certificate. Assume - # no SAN field is present. - log.warning( - "A problem was encountered with the certificate that prevented " - "urllib3 from finding the SubjectAlternativeName field. This can " - "affect certificate validation. The error was %s", - e, - ) - return [] - - # We want to return dNSName and iPAddress fields. We need to cast the IPs - # back to strings because the match_hostname function wants them as - # strings. - # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 - # decoded. This is pretty frustrating, but that's what the standard library - # does with certificates, and so we need to attempt to do the same. - # We also want to skip over names which cannot be idna encoded. - names = [ - ("DNS", name) - for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) - if name is not None - ] - names.extend( - ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) - ) - - return names - - -class WrappedSocket: - """API-compatibility wrapper for Python OpenSSL's Connection-class.""" - - def __init__( - self, - connection: OpenSSL.SSL.Connection, - socket: socket_cls, - suppress_ragged_eofs: bool = True, - ) -> None: - self.connection = connection - self.socket = socket - self.suppress_ragged_eofs = suppress_ragged_eofs - self._io_refs = 0 - self._closed = False - - def fileno(self) -> int: - return self.socket.fileno() - - # Copy-pasted from Python 3.5 source code - def _decref_socketios(self) -> None: - if self._io_refs > 0: - self._io_refs -= 1 - if self._closed: - self.close() - - def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: - try: - data = self.connection.recv(*args, **kwargs) - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return b"" - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return b"" - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - else: - return data # type: ignore[no-any-return] - - def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: - try: - return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return 0 - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return 0 - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv_into(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - - def settimeout(self, timeout: float) -> None: - return self.socket.settimeout(timeout) - - def _send_until_done(self, data: bytes) -> int: - while True: - try: - return self.connection.send(data) # type: ignore[no-any-return] - except OpenSSL.SSL.WantWriteError as e: - if not util.wait_for_write(self.socket, self.socket.gettimeout()): - raise TimeoutError() from e - continue - except OpenSSL.SSL.SysCallError as e: - raise OSError(e.args[0], str(e)) from e - - def sendall(self, data: bytes) -> None: - total_sent = 0 - while total_sent < len(data): - sent = self._send_until_done( - data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] - ) - total_sent += sent - - def shutdown(self, how: int) -> None: - try: - self.connection.shutdown() - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"shutdown error: {e!r}") from e - - def close(self) -> None: - self._closed = True - if self._io_refs <= 0: - self._real_close() - - def _real_close(self) -> None: - try: - return self.connection.close() # type: ignore[no-any-return] - except OpenSSL.SSL.Error: - return - - def getpeercert( - self, binary_form: bool = False - ) -> dict[str, list[typing.Any]] | None: - x509 = self.connection.get_peer_certificate() - - if not x509: - return x509 # type: ignore[no-any-return] - - if binary_form: - return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] - - return { - "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] - "subjectAltName": get_subj_alt_name(x509), - } - - def version(self) -> str: - return self.connection.get_protocol_version_name() # type: ignore[no-any-return] - - def selected_alpn_protocol(self) -> str | None: - alpn_proto = self.connection.get_alpn_proto_negotiated() - return alpn_proto.decode() if alpn_proto else None - - -WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] - - -class PyOpenSSLContext: - """ - I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible - for translating the interface of the standard library ``SSLContext`` object - to calls into PyOpenSSL. - """ - - def __init__(self, protocol: int) -> None: - self.protocol = _openssl_versions[protocol] - self._ctx = OpenSSL.SSL.Context(self.protocol) - self._options = 0 - self.check_hostname = False - self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED - self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED - self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST - - @property - def options(self) -> int: - return self._options - - @options.setter - def options(self, value: int) -> None: - self._options = value - self._set_ctx_options() - - @property - def verify_flags(self) -> int: - return self._verify_flags - - @verify_flags.setter - def verify_flags(self, value: int) -> None: - self._verify_flags = value - self._ctx.get_cert_store().set_flags(self._verify_flags) - - @property - def verify_mode(self) -> int: - return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] - - @verify_mode.setter - def verify_mode(self, value: ssl.VerifyMode) -> None: - self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) - - def set_default_verify_paths(self) -> None: - self._ctx.set_default_verify_paths() - - def set_ciphers(self, ciphers: bytes | str) -> None: - if isinstance(ciphers, str): - ciphers = ciphers.encode("utf-8") - self._ctx.set_cipher_list(ciphers) - - def load_verify_locations( - self, - cafile: str | None = None, - capath: str | None = None, - cadata: bytes | None = None, - ) -> None: - if cafile is not None: - cafile = cafile.encode("utf-8") # type: ignore[assignment] - if capath is not None: - capath = capath.encode("utf-8") # type: ignore[assignment] - try: - self._ctx.load_verify_locations(cafile, capath) - if cadata is not None: - self._ctx.load_verify_locations(BytesIO(cadata)) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e - - def load_cert_chain( - self, - certfile: str, - keyfile: str | None = None, - password: str | None = None, - ) -> None: - try: - self._ctx.use_certificate_chain_file(certfile) - if password is not None: - if not isinstance(password, bytes): - password = password.encode("utf-8") # type: ignore[assignment] - self._ctx.set_passwd_cb(lambda *_: password) - self._ctx.use_privatekey_file(keyfile or certfile) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e - - def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: - protocols = [util.util.to_bytes(p, "ascii") for p in protocols] - return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] - - def wrap_socket( - self, - sock: socket_cls, - server_side: bool = False, - do_handshake_on_connect: bool = True, - suppress_ragged_eofs: bool = True, - server_hostname: bytes | str | None = None, - ) -> WrappedSocket: - cnx = OpenSSL.SSL.Connection(self._ctx, sock) - - # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 - if server_hostname and not util.ssl_.is_ipaddress(server_hostname): - if isinstance(server_hostname, str): - server_hostname = server_hostname.encode("utf-8") - cnx.set_tlsext_host_name(server_hostname) - - cnx.set_connect_state() - - while True: - try: - cnx.do_handshake() - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(sock, sock.gettimeout()): - raise TimeoutError("select timed out") from e - continue - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"bad handshake: {e!r}") from e - break - - return WrappedSocket(cnx, sock) - - def _set_ctx_options(self) -> None: - self._ctx.set_options( - self._options - | _openssl_to_ssl_minimum_version[self._minimum_version] - | _openssl_to_ssl_maximum_version[self._maximum_version] - ) - - @property - def minimum_version(self) -> int: - return self._minimum_version - - @minimum_version.setter - def minimum_version(self, minimum_version: int) -> None: - self._minimum_version = minimum_version - self._set_ctx_options() - - @property - def maximum_version(self) -> int: - return self._maximum_version - - @maximum_version.setter - def maximum_version(self, maximum_version: int) -> None: - self._maximum_version = maximum_version - self._set_ctx_options() - - -def _verify_callback( - cnx: OpenSSL.SSL.Connection, - x509: X509, - err_no: int, - err_depth: int, - return_code: int, -) -> bool: - return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py deleted file mode 100644 index d37da8fc20..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/contrib/socks.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -This module contains provisional support for SOCKS proxies from within -urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and -SOCKS5. To enable its functionality, either install PySocks or install this -module with the ``socks`` extra. - -The SOCKS implementation supports the full range of urllib3 features. It also -supports the following SOCKS features: - -- SOCKS4A (``proxy_url='socks4a://...``) -- SOCKS4 (``proxy_url='socks4://...``) -- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) -- SOCKS5 with local DNS (``proxy_url='socks5://...``) -- Usernames and passwords for the SOCKS proxy - -.. note:: - It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in - your ``proxy_url`` to ensure that DNS resolution is done from the remote - server instead of client-side when connecting to a domain name. - -SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 -supports IPv4, IPv6, and domain names. - -When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` -will be sent as the ``userid`` section of the SOCKS request: - -.. code-block:: python - - proxy_url="socks4a://@proxy-host" - -When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion -of the ``proxy_url`` will be sent as the username/password to authenticate -with the proxy: - -.. code-block:: python - - proxy_url="socks5h://:@proxy-host" - -""" - -from __future__ import annotations - -try: - import socks # type: ignore[import-untyped] -except ImportError: - import warnings - - from ..exceptions import DependencyWarning - - warnings.warn( - ( - "SOCKS support in urllib3 requires the installation of optional " - "dependencies: specifically, PySocks. For more information, see " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" - ), - DependencyWarning, - ) - raise - -import typing -from socket import timeout as SocketTimeout - -from ..connection import HTTPConnection, HTTPSConnection -from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from ..exceptions import ConnectTimeoutError, NewConnectionError -from ..poolmanager import PoolManager -from ..util.url import parse_url - -try: - import ssl -except ImportError: - ssl = None # type: ignore[assignment] - - -class _TYPE_SOCKS_OPTIONS(typing.TypedDict): - socks_version: int - proxy_host: str | None - proxy_port: str | None - username: str | None - password: str | None - rdns: bool - - -class SOCKSConnection(HTTPConnection): - """ - A plain-text HTTP connection that connects via a SOCKS proxy. - """ - - def __init__( - self, - _socks_options: _TYPE_SOCKS_OPTIONS, - *args: typing.Any, - **kwargs: typing.Any, - ) -> None: - self._socks_options = _socks_options - super().__init__(*args, **kwargs) - - def _new_conn(self) -> socks.socksocket: - """ - Establish a new connection via the SOCKS proxy. - """ - extra_kw: dict[str, typing.Any] = {} - if self.source_address: - extra_kw["source_address"] = self.source_address - - if self.socket_options: - extra_kw["socket_options"] = self.socket_options - - try: - conn = socks.create_connection( - (self.host, self.port), - proxy_type=self._socks_options["socks_version"], - proxy_addr=self._socks_options["proxy_host"], - proxy_port=self._socks_options["proxy_port"], - proxy_username=self._socks_options["username"], - proxy_password=self._socks_options["password"], - proxy_rdns=self._socks_options["rdns"], - timeout=self.timeout, - **extra_kw, - ) - - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except socks.ProxyError as e: - # This is fragile as hell, but it seems to be the only way to raise - # useful errors here. - if e.socket_err: - error = e.socket_err - if isinstance(error, SocketTimeout): - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - else: - # Adding `from e` messes with coverage somehow, so it's omitted. - # See #2386. - raise NewConnectionError( - self, f"Failed to establish a new connection: {error}" - ) - else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - except OSError as e: # Defensive: PySocks should catch all these. - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - return conn - - -# We don't need to duplicate the Verified/Unverified distinction from -# urllib3/connection.py here because the HTTPSConnection will already have been -# correctly set to either the Verified or Unverified form by that module. This -# means the SOCKSHTTPSConnection will automatically be the correct type. -class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): - pass - - -class SOCKSHTTPConnectionPool(HTTPConnectionPool): - ConnectionCls = SOCKSConnection - - -class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): - ConnectionCls = SOCKSHTTPSConnection - - -class SOCKSProxyManager(PoolManager): - """ - A version of the urllib3 ProxyManager that routes connections via the - defined SOCKS proxy. - """ - - pool_classes_by_scheme = { - "http": SOCKSHTTPConnectionPool, - "https": SOCKSHTTPSConnectionPool, - } - - def __init__( - self, - proxy_url: str, - username: str | None = None, - password: str | None = None, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ): - parsed = parse_url(proxy_url) - - if username is None and password is None and parsed.auth is not None: - split = parsed.auth.split(":") - if len(split) == 2: - username, password = split - if parsed.scheme == "socks5": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = False - elif parsed.scheme == "socks5h": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = True - elif parsed.scheme == "socks4": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = False - elif parsed.scheme == "socks4a": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = True - else: - raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") - - self.proxy_url = proxy_url - - socks_options = { - "socks_version": socks_version, - "proxy_host": parsed.host, - "proxy_port": parsed.port, - "username": username, - "password": password, - "rdns": rdns, - } - connection_pool_kw["_socks_options"] = socks_options - - super().__init__(num_pools, headers, **connection_pool_kw) - - self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py deleted file mode 100644 index 3d7e9b93d3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/exceptions.py +++ /dev/null @@ -1,335 +0,0 @@ -from __future__ import annotations - -import socket -import typing -import warnings -from email.errors import MessageDefect -from http.client import IncompleteRead as httplib_IncompleteRead - -if typing.TYPE_CHECKING: - from .connection import HTTPConnection - from .connectionpool import ConnectionPool - from .response import HTTPResponse - from .util.retry import Retry - -# Base Exceptions - - -class HTTPError(Exception): - """Base exception used by this module.""" - - -class HTTPWarning(Warning): - """Base warning used by this module.""" - - -_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] - - -class PoolError(HTTPError): - """Base exception for errors caused within a pool.""" - - def __init__(self, pool: ConnectionPool, message: str) -> None: - self.pool = pool - self._message = message - super().__init__(f"{pool}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - -class RequestError(PoolError): - """Base exception for PoolErrors that have associated URLs.""" - - def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: - self.url = url - super().__init__(pool, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self._message) - - -class SSLError(HTTPError): - """Raised when SSL certificate fails in an HTTPS connection.""" - - -class ProxyError(HTTPError): - """Raised when the connection to a proxy fails.""" - - # The original error is also available as __cause__. - original_error: Exception - - def __init__(self, message: str, error: Exception) -> None: - super().__init__(message, error) - self.original_error = error - - -class DecodeError(HTTPError): - """Raised when automatic decoding based on Content-Type fails.""" - - -class ProtocolError(HTTPError): - """Raised when something unexpected happens mid-request/response.""" - - -#: Renamed to ProtocolError but aliased for backwards compatibility. -ConnectionError = ProtocolError - - -# Leaf Exceptions - - -class MaxRetryError(RequestError): - """Raised when the maximum number of retries is exceeded. - - :param pool: The connection pool - :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` - :param str url: The requested Url - :param reason: The underlying error - :type reason: :class:`Exception` - - """ - - def __init__( - self, pool: ConnectionPool, url: str | None, reason: Exception | None = None - ) -> None: - self.reason = reason - - message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" - - super().__init__(pool, url, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self.reason) - - -class HostChangedError(RequestError): - """Raised when an existing pool gets a request for a foreign host.""" - - def __init__( - self, pool: ConnectionPool, url: str, retries: Retry | int = 3 - ) -> None: - message = f"Tried to open a foreign host with url: {url}" - super().__init__(pool, url, message) - self.retries = retries - - -class TimeoutStateError(HTTPError): - """Raised when passing an invalid state to a timeout""" - - -class TimeoutError(HTTPError): - """Raised when a socket timeout error occurs. - - Catching this error will catch both :exc:`ReadTimeoutErrors - ` and :exc:`ConnectTimeoutErrors `. - """ - - -class ReadTimeoutError(TimeoutError, RequestError): - """Raised when a socket timeout occurs while receiving data from a server""" - - -# This timeout error does not have a URL attached and needs to inherit from the -# base HTTPError -class ConnectTimeoutError(TimeoutError): - """Raised when a socket timeout occurs while connecting to a server""" - - -class NewConnectionError(ConnectTimeoutError, HTTPError): - """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" - - def __init__(self, conn: HTTPConnection, message: str) -> None: - self.conn = conn - self._message = message - super().__init__(f"{conn}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - @property - def pool(self) -> HTTPConnection: - warnings.warn( - "The 'pool' property is deprecated and will be removed " - "in urllib3 v3.0. Use 'conn' instead.", - FutureWarning, - stacklevel=2, - ) - - return self.conn - - -class NameResolutionError(NewConnectionError): - """Raised when host name resolution fails.""" - - def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): - message = f"Failed to resolve '{host}' ({reason})" - self._host = host - self._reason = reason - super().__init__(conn, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (self._host, None, self._reason) - - -class EmptyPoolError(PoolError): - """Raised when a pool runs out of connections and no more are allowed.""" - - -class FullPoolError(PoolError): - """Raised when we try to add a connection to a full pool in blocking mode.""" - - -class ClosedPoolError(PoolError): - """Raised when a request enters a pool after the pool has been closed.""" - - -class LocationValueError(ValueError, HTTPError): - """Raised when there is something wrong with a given URL input.""" - - -class LocationParseError(LocationValueError): - """Raised when get_host or similar fails to parse the URL input.""" - - def __init__(self, location: str) -> None: - message = f"Failed to parse: {location}" - super().__init__(message) - - self.location = location - - -class URLSchemeUnknown(LocationValueError): - """Raised when a URL input has an unsupported scheme.""" - - def __init__(self, scheme: str): - message = f"Not supported URL scheme {scheme}" - super().__init__(message) - - self.scheme = scheme - - -class ResponseError(HTTPError): - """Used as a container for an error reason supplied in a MaxRetryError.""" - - GENERIC_ERROR = "too many error responses" - SPECIFIC_ERROR = "too many {status_code} error responses" - - -class SecurityWarning(HTTPWarning): - """Warned when performing security reducing actions""" - - -class InsecureRequestWarning(SecurityWarning): - """Warned when making an unverified HTTPS request.""" - - -class NotOpenSSLWarning(SecurityWarning): - """Warned when using unsupported SSL library""" - - -class SystemTimeWarning(SecurityWarning): - """Warned when system time is suspected to be wrong""" - - -class InsecurePlatformWarning(SecurityWarning): - """Warned when certain TLS/SSL configuration is not available on a platform.""" - - -class DependencyWarning(HTTPWarning): - """ - Warned when an attempt is made to import a module with missing optional - dependencies. - """ - - -class ResponseNotChunked(ProtocolError, ValueError): - """Response needs to be chunked in order to read it as chunks.""" - - -class BodyNotHttplibCompatible(HTTPError): - """ - Body should be :class:`http.client.HTTPResponse` like - (have an fp attribute which returns raw chunks) for read_chunked(). - """ - - -class IncompleteRead(HTTPError, httplib_IncompleteRead): - """ - Response length doesn't match expected Content-Length - - Subclass of :class:`http.client.IncompleteRead` to allow int value - for ``partial`` to avoid creating large objects on streamed reads. - """ - - partial: int # type: ignore[assignment] - expected: int - - def __init__(self, partial: int, expected: int) -> None: - self.partial = partial - self.expected = expected - - def __repr__(self) -> str: - return "IncompleteRead(%i bytes read, %i more expected)" % ( - self.partial, - self.expected, - ) - - -class InvalidChunkLength(HTTPError, httplib_IncompleteRead): - """Invalid chunk length in a chunked response.""" - - def __init__(self, response: HTTPResponse, length: bytes) -> None: - self.partial: int = response.tell() # type: ignore[assignment] - self.expected: int | None = response.length_remaining - self.response = response - self.length = length - - def __repr__(self) -> str: - return "InvalidChunkLength(got length %r, %i bytes read)" % ( - self.length, - self.partial, - ) - - -class InvalidHeader(HTTPError): - """The header provided was somehow invalid.""" - - -class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): - """ProxyManager does not support the supplied scheme""" - - # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. - - def __init__(self, scheme: str | None) -> None: - # 'localhost' is here because our URL parser parses - # localhost:8080 -> scheme=localhost, remove if we fix this. - if scheme == "localhost": - scheme = None - if scheme is None: - message = "Proxy URL had no scheme, should start with http:// or https://" - else: - message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" - super().__init__(message) - - -class ProxySchemeUnsupported(ValueError): - """Fetching HTTPS resources through HTTPS proxies is unsupported""" - - -class HeaderParsingError(HTTPError): - """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" - - def __init__( - self, defects: list[MessageDefect], unparsed_data: bytes | str | None - ) -> None: - message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" - super().__init__(message) - - -class UnrewindableBodyError(HTTPError): - """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py deleted file mode 100644 index fe68e17732..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/fields.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import email.utils -import mimetypes -import typing - -_TYPE_FIELD_VALUE = typing.Union[str, bytes] -_TYPE_FIELD_VALUE_TUPLE = typing.Union[ - _TYPE_FIELD_VALUE, - tuple[str, _TYPE_FIELD_VALUE], - tuple[str, _TYPE_FIELD_VALUE, str], -] - - -def guess_content_type( - filename: str | None, default: str = "application/octet-stream" -) -> str: - """ - Guess the "Content-Type" of a file. - - :param filename: - The filename to guess the "Content-Type" of using :mod:`mimetypes`. - :param default: - If no "Content-Type" can be guessed, default to `default`. - """ - if filename: - return mimetypes.guess_type(filename)[0] or default - return default - - -def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Helper function to format and quote a single header parameter using the - strategy defined in RFC 2231. - - Particularly useful for header parameters which might contain - non-ASCII values, like file names. This follows - `RFC 2388 Section 4.4 `_. - - :param name: - The name of the parameter, a string expected to be ASCII only. - :param value: - The value of the parameter, provided as ``bytes`` or `str``. - :returns: - An RFC-2231-formatted unicode string. - - .. deprecated:: 2.0.0 - Will be removed in urllib3 v3.0. This is not valid for - ``multipart/form-data`` header parameters. - """ - import warnings - - warnings.warn( - "'format_header_param_rfc2231' is insecure, deprecated and will be " - "removed in urllib3 v3.0. This is not valid for " - "multipart/form-data header parameters.", - FutureWarning, - stacklevel=2, - ) - - if isinstance(value, bytes): - value = value.decode("utf-8") - - if not any(ch in value for ch in '"\\\r\n'): - result = f'{name}="{value}"' - try: - result.encode("ascii") - except (UnicodeEncodeError, UnicodeDecodeError): - pass - else: - return result - - value = email.utils.encode_rfc2231(value, "utf-8") - value = f"{name}*={value}" - - return value - - -def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Format and quote a single multipart header parameter. - - This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching - the behavior of current browser and curl versions. Values are - assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are - percent encoded. - - .. _WHATWG HTML Standard: - https://html.spec.whatwg.org/multipage/ - form-control-infrastructure.html#multipart-form-data - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - :returns: - A string ``name="value"`` with the escaped value. - - .. versionchanged:: 2.0.0 - Matches the WHATWG HTML Standard as of 2021/06/10. Control - characters are no longer percent encoded. - - .. versionchanged:: 2.0.0 - Renamed from ``format_header_param_html5`` and - ``format_header_param``. The old names will be removed in - urllib3 v3.0. - """ - if isinstance(value, bytes): - value = value.decode("utf-8") - - # percent encode \n \r " - value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) - return f'{name}="{value}"' - - -def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param_html5' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -class RequestField: - """ - A data container for request body parameters. - - :param name: - The name of this request field. Must be unicode. - :param data: - The data/value body. - :param filename: - An optional filename of the request field. Must be unicode. - :param headers: - An optional dict-like object of headers to initially use for the field. - - .. versionchanged:: 2.0.0 - The ``header_formatter`` parameter is deprecated and will - be removed in urllib3 v3.0. - """ - - def __init__( - self, - name: str, - data: _TYPE_FIELD_VALUE, - filename: str | None = None, - headers: typing.Mapping[str, str] | None = None, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ): - self._name = name - self._filename = filename - self.data = data - self.headers: dict[str, str | None] = {} - if headers: - self.headers = dict(headers) - - if header_formatter is not None: - import warnings - - warnings.warn( - "The 'header_formatter' parameter is deprecated and " - "will be removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - self.header_formatter = header_formatter - else: - self.header_formatter = format_multipart_header_param - - @classmethod - def from_tuples( - cls, - fieldname: str, - value: _TYPE_FIELD_VALUE_TUPLE, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ) -> RequestField: - """ - A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. - - Supports constructing :class:`~urllib3.fields.RequestField` from - parameter of key/value strings AND key/filetuple. A filetuple is a - (filename, data, MIME type) tuple where the MIME type is optional. - For example:: - - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - - Field names and filenames must be unicode. - """ - filename: str | None - content_type: str | None - data: _TYPE_FIELD_VALUE - - if isinstance(value, tuple): - if len(value) == 3: - filename, data, content_type = value - else: - filename, data = value - content_type = guess_content_type(filename) - else: - filename = None - content_type = None - data = value - - request_param = cls( - fieldname, data, filename=filename, header_formatter=header_formatter - ) - request_param.make_multipart(content_type=content_type) - - return request_param - - def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Override this method to change how each multipart header - parameter is formatted. By default, this calls - :func:`format_multipart_header_param`. - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - - :meta public: - """ - return self.header_formatter(name, value) - - def _render_parts( - self, - header_parts: ( - dict[str, _TYPE_FIELD_VALUE | None] - | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] - ), - ) -> str: - """ - Helper function to format and quote a single header. - - Useful for single headers that are composed of multiple items. E.g., - 'Content-Disposition' fields. - - :param header_parts: - A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format - as `k1="v1"; k2="v2"; ...`. - """ - iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] - - parts = [] - if isinstance(header_parts, dict): - iterable = header_parts.items() - else: - iterable = header_parts - - for name, value in iterable: - if value is not None: - parts.append(self._render_part(name, value)) - - return "; ".join(parts) - - def render_headers(self) -> str: - """ - Renders the headers for this request field. - """ - lines = [] - - sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] - for sort_key in sort_keys: - if self.headers.get(sort_key, False): - lines.append(f"{sort_key}: {self.headers[sort_key]}") - - for header_name, header_value in self.headers.items(): - if header_name not in sort_keys: - if header_value: - lines.append(f"{header_name}: {header_value}") - - lines.append("\r\n") - return "\r\n".join(lines) - - def make_multipart( - self, - content_disposition: str | None = None, - content_type: str | None = None, - content_location: str | None = None, - ) -> None: - """ - Makes this request field into a multipart request field. - - This method overrides "Content-Disposition", "Content-Type" and - "Content-Location" headers to the request parameter. - - :param content_disposition: - The 'Content-Disposition' of the request body. Defaults to 'form-data' - :param content_type: - The 'Content-Type' of the request body. - :param content_location: - The 'Content-Location' of the request body. - - """ - content_disposition = (content_disposition or "form-data") + "; ".join( - [ - "", - self._render_parts( - (("name", self._name), ("filename", self._filename)) - ), - ] - ) - - self.headers["Content-Disposition"] = content_disposition - self.headers["Content-Type"] = content_type - self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py deleted file mode 100644 index 14f70b05b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/filepost.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -import binascii -import codecs -import os -import typing -from io import BytesIO - -from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField - -writer = codecs.lookup("utf-8")[3] - -_TYPE_FIELDS_SEQUENCE = typing.Sequence[ - typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] -] -_TYPE_FIELDS = typing.Union[ - _TYPE_FIELDS_SEQUENCE, - typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], -] - - -def choose_boundary() -> str: - """ - Our embarrassingly-simple replacement for mimetools.choose_boundary. - """ - return binascii.hexlify(os.urandom(16)).decode() - - -def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: - """ - Iterate over fields. - - Supports list of (k, v) tuples and dicts, and lists of - :class:`~urllib3.fields.RequestField`. - - """ - iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] - - if isinstance(fields, typing.Mapping): - iterable = fields.items() - else: - iterable = fields - - for field in iterable: - if isinstance(field, RequestField): - yield field - else: - yield RequestField.from_tuples(*field) - - -def encode_multipart_formdata( - fields: _TYPE_FIELDS, boundary: str | None = None -) -> tuple[bytes, str]: - """ - Encode a dictionary of ``fields`` using the multipart/form-data MIME format. - - :param fields: - Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). - Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. - - :param boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - body = BytesIO() - if boundary is None: - boundary = choose_boundary() - - for field in iter_field_objects(fields): - body.write(f"--{boundary}\r\n".encode("latin-1")) - - writer(body).write(field.render_headers()) - data = field.data - - if isinstance(data, int): - data = str(data) # Backwards compatibility - - if isinstance(data, str): - writer(body).write(data) - else: - body.write(data) - - body.write(b"\r\n") - - body.write(f"--{boundary}--\r\n".encode("latin-1")) - - content_type = f"multipart/form-data; boundary={boundary}" - - return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py deleted file mode 100644 index 133e1d8f23..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from importlib.metadata import version - -__all__ = [ - "inject_into_urllib3", - "extract_from_urllib3", -] - -import typing - -orig_HTTPSConnection: typing.Any = None - - -def inject_into_urllib3() -> None: - # First check if h2 version is valid - h2_version = version("h2") - if not h2_version.startswith("4."): - raise ImportError( - "urllib3 v2 supports h2 version 4.x.x, currently " - f"the 'h2' module is compiled with {h2_version!r}. " - "See: https://github.com/urllib3/urllib3/issues/3290" - ) - - # Import here to avoid circular dependencies. - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - from .connection import HTTP2Connection - - global orig_HTTPSConnection - orig_HTTPSConnection = urllib3_connection.HTTPSConnection - - HTTPSConnectionPool.ConnectionCls = HTTP2Connection - urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] - - # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. - urllib3_util.ALPN_PROTOCOLS = ["h2"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] - - -def extract_from_urllib3() -> None: - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - - HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection - urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] - - urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py deleted file mode 100644 index 0a026da0a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/connection.py +++ /dev/null @@ -1,356 +0,0 @@ -from __future__ import annotations - -import logging -import re -import threading -import types -import typing - -import h2.config -import h2.connection -import h2.events - -from .._base_connection import _TYPE_BODY -from .._collections import HTTPHeaderDict -from ..connection import HTTPSConnection, _get_default_user_agent -from ..exceptions import ConnectionError -from ..response import BaseHTTPResponse - -orig_HTTPSConnection = HTTPSConnection - -T = typing.TypeVar("T") - -log = logging.getLogger(__name__) - -RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") -RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") - - -def _is_legal_header_name(name: bytes) -> bool: - """ - "An implementation that validates fields according to the definitions in Sections - 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not - include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - - `http.client._is_legal_header_name` does not validate the field name according to the - HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. - - This does not allow for the `:` character in the header name, so should not - be used to validate pseudo-headers. - """ - return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) - - -def _is_illegal_header_value(value: bytes) -> bool: - """ - "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed - (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field - value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, - 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - """ - return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) - - -class _LockedObject(typing.Generic[T]): - """ - A wrapper class that hides a specific object behind a lock. - The goal here is to provide a simple way to protect access to an object - that cannot safely be simultaneously accessed from multiple threads. The - intended use of this class is simple: take hold of it with a context - manager, which returns the protected object. - """ - - __slots__ = ( - "lock", - "_obj", - ) - - def __init__(self, obj: T): - self.lock = threading.RLock() - self._obj = obj - - def __enter__(self) -> T: - self.lock.acquire() - return self._obj - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> None: - self.lock.release() - - -class HTTP2Connection(HTTPSConnection): - def __init__( - self, host: str, port: int | None = None, **kwargs: typing.Any - ) -> None: - self._h2_conn = self._new_h2_conn() - self._h2_stream: int | None = None - self._headers: list[tuple[bytes, bytes]] = [] - - if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: - raise NotImplementedError("Proxies aren't supported with HTTP/2") - - super().__init__(host, port, **kwargs) - - if self._tunnel_host is not None: - raise NotImplementedError("Tunneling isn't supported with HTTP/2") - - def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: - config = h2.config.H2Configuration(client_side=True) - return _LockedObject(h2.connection.H2Connection(config=config)) - - def connect(self) -> None: - super().connect() - with self._h2_conn as conn: - conn.initiate_connection() - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - def putrequest( # type: ignore[override] - self, - method: str, - url: str, - **kwargs: typing.Any, - ) -> None: - """putrequest - This deviates from the HTTPConnection method signature since we never need to override - sending accept-encoding headers or the host header. - """ - if "skip_host" in kwargs: - raise NotImplementedError("`skip_host` isn't supported") - if "skip_accept_encoding" in kwargs: - raise NotImplementedError("`skip_accept_encoding` isn't supported") - - self._request_url = url or "/" - self._validate_path(url) # type: ignore[attr-defined] - - if ":" in self.host: - authority = f"[{self.host}]:{self.port or 443}" - else: - authority = f"{self.host}:{self.port or 443}" - - self._headers.append((b":scheme", b"https")) - self._headers.append((b":method", method.encode())) - self._headers.append((b":authority", authority.encode())) - self._headers.append((b":path", url.encode())) - - with self._h2_conn as conn: - self._h2_stream = conn.get_next_available_stream_id() - - def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] - # TODO SKIPPABLE_HEADERS from urllib3 are ignored. - header = header.encode() if isinstance(header, str) else header - header = header.lower() # A lot of upstream code uses capitalized headers. - if not _is_legal_header_name(header): - raise ValueError(f"Illegal header name {str(header)}") - - for value in values: - value = value.encode() if isinstance(value, str) else value - if _is_illegal_header_value(value): - raise ValueError(f"Illegal header value {str(value)}") - self._headers.append((header, value)) - - def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - conn.send_headers( - stream_id=self._h2_stream, - headers=self._headers, - end_stream=(message_body is None), - ) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - self._headers = [] # Reset headers for the next request. - - def send(self, data: typing.Any) -> None: - """Send data to the server. - `data` can be: `str`, `bytes`, an iterable, or file-like objects - that support a .read() method. - """ - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - if hasattr(data, "read"): # file-like objects - while True: - chunk = data.read(self.blocksize) - if not chunk: - break - if isinstance(chunk, str): - chunk = chunk.encode() - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - return - - if isinstance(data, str): # str -> bytes - data = data.encode() - - try: - if isinstance(data, bytes): - conn.send_data(self._h2_stream, data, end_stream=True) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - else: - for chunk in data: - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - except TypeError: - raise TypeError( - "`data` should be str, bytes, iterable, or file. got %r" - % type(data) - ) - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - raise NotImplementedError( - "HTTP/2 does not support setting up a tunnel through a proxy" - ) - - def getresponse( # type: ignore[override] - self, - ) -> HTTP2Response: - status = None - data = bytearray() - with self._h2_conn as conn: - end_stream = False - while not end_stream: - # TODO: Arbitrary read value. - if received_data := self.sock.recv(65535): - events = conn.receive_data(received_data) - for event in events: - if isinstance(event, h2.events.ResponseReceived): - headers = HTTPHeaderDict() - for header, value in event.headers: - if header == b":status": - status = int(value.decode()) - else: - headers.add( - header.decode("ascii"), value.decode("ascii") - ) - - elif isinstance(event, h2.events.DataReceived): - data += event.data - conn.acknowledge_received_data( - event.flow_controlled_length, event.stream_id - ) - - elif isinstance(event, h2.events.StreamEnded): - end_stream = True - - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - assert status is not None - return HTTP2Response( - status=status, - headers=headers, - request_url=self._request_url, - data=bytes(data), - ) - - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - **kwargs: typing.Any, - ) -> None: - """Send an HTTP/2 request""" - if "chunked" in kwargs: - # TODO this is often present from upstream. - # raise NotImplementedError("`chunked` isn't supported with HTTP/2") - pass - - if self.sock is not None: - self.sock.settimeout(self.timeout) - - self.putrequest(method, url) - - headers = headers or {} - for k, v in headers.items(): - if k.lower() == "transfer-encoding" and v == "chunked": - continue - else: - self.putheader(k, v) - - if b"user-agent" not in dict(self._headers): - self.putheader(b"user-agent", _get_default_user_agent()) - - if body: - self.endheaders(message_body=body) - self.send(body) - else: - self.endheaders() - - def close(self) -> None: - with self._h2_conn as conn: - try: - conn.close_connection() - if data := conn.data_to_send(): - self.sock.sendall(data) - except Exception: - pass - - # Reset all our HTTP/2 connection state. - self._h2_conn = self._new_h2_conn() - self._h2_stream = None - self._headers = [] - - super().close() - - -class HTTP2Response(BaseHTTPResponse): - # TODO: This is a woefully incomplete response object, but works for non-streaming. - def __init__( - self, - status: int, - headers: HTTPHeaderDict, - request_url: str, - data: bytes, - decode_content: bool = False, # TODO: support decoding - ) -> None: - super().__init__( - status=status, - headers=headers, - # Following CPython, we map HTTP versions to major * 10 + minor integers - version=20, - version_string="HTTP/2", - # No reason phrase in HTTP/2 - reason=None, - decode_content=decode_content, - request_url=request_url, - ) - self._data = data - self.length_remaining = 0 - - @property - def data(self) -> bytes: - return self._data - - def get_redirect_location(self) -> None: - return None - - def close(self) -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py deleted file mode 100644 index 9ea900764f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/http2/probe.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import threading - - -class _HTTP2ProbeCache: - __slots__ = ( - "_lock", - "_cache_locks", - "_cache_values", - ) - - def __init__(self) -> None: - self._lock = threading.Lock() - self._cache_locks: dict[tuple[str, int], threading.RLock] = {} - self._cache_values: dict[tuple[str, int], bool | None] = {} - - def acquire_and_get(self, host: str, port: int) -> bool | None: - # By the end of this block we know that - # _cache_[values,locks] is available. - value = None - with self._lock: - key = (host, port) - try: - value = self._cache_values[key] - # If it's a known value we return right away. - if value is not None: - return value - except KeyError: - self._cache_locks[key] = threading.RLock() - self._cache_values[key] = None - - # If the value is unknown, we acquire the lock to signal - # to the requesting thread that the probe is in progress - # or that the current thread needs to return their findings. - key_lock = self._cache_locks[key] - key_lock.acquire() - try: - # If the by the time we get the lock the value has been - # updated we want to return the updated value. - value = self._cache_values[key] - - # In case an exception like KeyboardInterrupt is raised here. - except BaseException as e: # Defensive: - assert not isinstance(e, KeyError) # KeyError shouldn't be possible. - key_lock.release() - raise - - return value - - def set_and_release( - self, host: str, port: int, supports_http2: bool | None - ) -> None: - key = (host, port) - key_lock = self._cache_locks[key] - with key_lock: # Uses an RLock, so can be locked again from same thread. - if supports_http2 is None and self._cache_values[key] is not None: - raise ValueError( - "Cannot reset HTTP/2 support for origin after value has been set." - ) # Defensive: not expected in normal usage - - self._cache_values[key] = supports_http2 - key_lock.release() - - def _values(self) -> dict[tuple[str, int], bool | None]: - """This function is for testing purposes only. Gets the current state of the probe cache""" - with self._lock: - return {k: v for k, v in self._cache_values.items()} - - def _reset(self) -> None: - """This function is for testing purposes only. Reset the cache values""" - with self._lock: - self._cache_locks = {} - self._cache_values = {} - - -_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() - -set_and_release = _HTTP2_PROBE_CACHE.set_and_release -acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get -_values = _HTTP2_PROBE_CACHE._values -_reset = _HTTP2_PROBE_CACHE._reset - -__all__ = [ - "set_and_release", - "acquire_and_get", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py deleted file mode 100644 index 8f2c56745c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/poolmanager.py +++ /dev/null @@ -1,653 +0,0 @@ -from __future__ import annotations - -import functools -import logging -import typing -import warnings -from types import TracebackType -from urllib.parse import urljoin - -from ._collections import HTTPHeaderDict, RecentlyUsedContainer -from ._request_methods import RequestMethods -from .connection import ProxyConfig -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme -from .exceptions import ( - LocationValueError, - MaxRetryError, - ProxySchemeUnknown, - URLSchemeUnknown, -) -from .response import BaseHTTPResponse -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.proxy import connection_requires_http_tunnel -from .util.retry import Retry -from .util.timeout import Timeout -from .util.url import Url, parse_url - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - -__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] - - -log = logging.getLogger(__name__) - -SSL_KEYWORDS = ( - "key_file", - "cert_file", - "cert_reqs", - "ca_certs", - "ca_cert_data", - "ssl_version", - "ssl_minimum_version", - "ssl_maximum_version", - "ca_cert_dir", - "ssl_context", - "key_password", - "server_hostname", -) -# Default value for `blocksize` - a new parameter introduced to -# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 -_DEFAULT_BLOCKSIZE = 16384 - - -class PoolKey(typing.NamedTuple): - """ - All known keyword arguments that could be provided to the pool manager, its - pools, or the underlying connections. - - All custom key schemes should include the fields in this key at a minimum. - """ - - key_scheme: str - key_host: str - key_port: int | None - key_timeout: Timeout | float | int | None - key_retries: Retry | bool | int | None - key_block: bool | None - key_source_address: tuple[str, int] | None - key_key_file: str | None - key_key_password: str | None - key_cert_file: str | None - key_cert_reqs: str | None - key_ca_certs: str | None - key_ca_cert_data: str | bytes | None - key_ssl_version: int | str | None - key_ssl_minimum_version: ssl.TLSVersion | None - key_ssl_maximum_version: ssl.TLSVersion | None - key_ca_cert_dir: str | None - key_ssl_context: ssl.SSLContext | None - key_maxsize: int | None - key_headers: frozenset[tuple[str, str]] | None - key__proxy: Url | None - key__proxy_headers: frozenset[tuple[str, str]] | None - key__proxy_config: ProxyConfig | None - key_socket_options: _TYPE_SOCKET_OPTIONS | None - key__socks_options: frozenset[tuple[str, str]] | None - key_assert_hostname: bool | str | None - key_assert_fingerprint: str | None - key_server_hostname: str | None - key_blocksize: int | None - - -def _default_key_normalizer( - key_class: type[PoolKey], request_context: dict[str, typing.Any] -) -> PoolKey: - """ - Create a pool key out of a request context dictionary. - - According to RFC 3986, both the scheme and host are case-insensitive. - Therefore, this function normalizes both before constructing the pool - key for an HTTPS request. If you wish to change this behaviour, provide - alternate callables to ``key_fn_by_scheme``. - - :param key_class: - The class to use when constructing the key. This should be a namedtuple - with the ``scheme`` and ``host`` keys at a minimum. - :type key_class: namedtuple - :param request_context: - A dictionary-like object that contain the context for a request. - :type request_context: dict - - :return: A namedtuple that can be used as a connection pool key. - :rtype: PoolKey - """ - # Since we mutate the dictionary, make a copy first - context = request_context.copy() - context["scheme"] = context["scheme"].lower() - context["host"] = context["host"].lower() - - # These are both dictionaries and need to be transformed into frozensets - for key in ("headers", "_proxy_headers", "_socks_options"): - if key in context and context[key] is not None: - context[key] = frozenset(context[key].items()) - - # The socket_options key may be a list and needs to be transformed into a - # tuple. - socket_opts = context.get("socket_options") - if socket_opts is not None: - context["socket_options"] = tuple(socket_opts) - - # Map the kwargs to the names in the namedtuple - this is necessary since - # namedtuples can't have fields starting with '_'. - for key in list(context.keys()): - context["key_" + key] = context.pop(key) - - # Default to ``None`` for keys missing from the context - for field in key_class._fields: - if field not in context: - context[field] = None - - # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context - if context.get("key_blocksize") is None: - context["key_blocksize"] = _DEFAULT_BLOCKSIZE - - return key_class(**context) - - -#: A dictionary that maps a scheme to a callable that creates a pool key. -#: This can be used to alter the way pool keys are constructed, if desired. -#: Each PoolManager makes a copy of this dictionary so they can be configured -#: globally here, or individually on the instance. -key_fn_by_scheme = { - "http": functools.partial(_default_key_normalizer, PoolKey), - "https": functools.partial(_default_key_normalizer, PoolKey), -} - -pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} - - -class PoolManager(RequestMethods): - """ - Allows for arbitrary requests while transparently keeping track of - necessary connection pools for you. - - :param num_pools: - Number of connection pools to cache before discarding the least - recently used pool. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param \\**connection_pool_kw: - Additional parameters are used to create fresh - :class:`urllib3.connectionpool.ConnectionPool` instances. - - Example: - - .. code-block:: python - - import urllib3 - - http = urllib3.PoolManager(num_pools=2) - - resp1 = http.request("GET", "https://google.com/") - resp2 = http.request("GET", "https://google.com/mail") - resp3 = http.request("GET", "https://yahoo.com/") - - print(len(http.pools)) - # 2 - - """ - - proxy: Url | None = None - proxy_config: ProxyConfig | None = None - - def __init__( - self, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - super().__init__(headers) - # PoolManager handles redirects itself in PoolManager.urlopen(). - # It always passes redirect=False to the underlying connection pool to - # suppress per-pool redirect handling. If the user supplied a non-Retry - # value (int/bool/etc) for retries and we let the pool normalize it - # while redirect=False, the resulting Retry object would have redirect - # handling disabled, which can interfere with PoolManager's own - # redirect logic. Normalize here so redirects remain governed solely by - # PoolManager logic. - if "retries" in connection_pool_kw: - retries = connection_pool_kw["retries"] - if not isinstance(retries, Retry): - retries = Retry.from_int(retries) - connection_pool_kw = connection_pool_kw.copy() - connection_pool_kw["retries"] = retries - self.connection_pool_kw = connection_pool_kw - - self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] - self.pools = RecentlyUsedContainer(num_pools) - - # Locally set the pool classes and keys so other PoolManagers can - # override them. - self.pool_classes_by_scheme = pool_classes_by_scheme - self.key_fn_by_scheme = key_fn_by_scheme.copy() - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.clear() - # Return False to re-raise any potential exceptions - return False - - def _new_pool( - self, - scheme: str, - host: str, - port: int, - request_context: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and - any additional pool keyword arguments. - - If ``request_context`` is provided, it is provided as keyword arguments - to the pool class used. This method is used to actually create the - connection pools handed out by :meth:`connection_from_url` and - companion methods. It is intended to be overridden for customization. - """ - pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] - if request_context is None: - request_context = self.connection_pool_kw.copy() - - # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly - # set to 'None' in the request_context. - if request_context.get("blocksize") is None: - request_context["blocksize"] = _DEFAULT_BLOCKSIZE - - # Although the context has everything necessary to create the pool, - # this function has historically only used the scheme, host, and port - # in the positional args. When an API change is acceptable these can - # be removed. - for key in ("scheme", "host", "port"): - request_context.pop(key, None) - - if scheme == "http": - for kw in SSL_KEYWORDS: - request_context.pop(kw, None) - - return pool_cls(host, port, **request_context) - - def clear(self) -> None: - """ - Empty our store of pools and direct them all to close. - - This will not affect in-flight connections, but they will not be - re-used after completion. - """ - self.pools.clear() - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. - - If ``port`` isn't given, it will be derived from the ``scheme`` using - ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is - provided, it is merged with the instance's ``connection_pool_kw`` - variable and used to create the new connection pool, if one is - needed. - """ - - if not host: - raise LocationValueError("No host specified.") - - request_context = self._merge_pool_kwargs(pool_kwargs) - request_context["scheme"] = scheme or "http" - if not port: - port = port_by_scheme.get(request_context["scheme"].lower(), 80) - request_context["port"] = port - request_context["host"] = host - - return self.connection_from_context(request_context) - - def connection_from_context( - self, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. - - ``request_context`` must at least contain the ``scheme`` key and its - value must be a key in ``key_fn_by_scheme`` instance variable. - """ - if "strict" in request_context: - warnings.warn( - "The 'strict' parameter is no longer needed on Python 3+. " - "This will raise an error in urllib3 v3.0.", - FutureWarning, - ) - request_context.pop("strict") - - scheme = request_context["scheme"].lower() - pool_key_constructor = self.key_fn_by_scheme.get(scheme) - if not pool_key_constructor: - raise URLSchemeUnknown(scheme) - pool_key = pool_key_constructor(request_context) - - return self.connection_from_pool_key(pool_key, request_context=request_context) - - def connection_from_pool_key( - self, pool_key: PoolKey, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. - - ``pool_key`` should be a namedtuple that only contains immutable - objects. At a minimum it must have the ``scheme``, ``host``, and - ``port`` fields. - """ - with self.pools.lock: - # If the scheme, host, or port doesn't match existing open - # connections, open a new ConnectionPool. - pool = self.pools.get(pool_key) - if pool: - return pool - - # Make a fresh ConnectionPool of the desired type - scheme = request_context["scheme"] - host = request_context["host"] - port = request_context["port"] - pool = self._new_pool(scheme, host, port, request_context=request_context) - self.pools[pool_key] = pool - - return pool - - def connection_from_url( - self, url: str, pool_kwargs: dict[str, typing.Any] | None = None - ) -> HTTPConnectionPool: - """ - Similar to :func:`urllib3.connectionpool.connection_from_url`. - - If ``pool_kwargs`` is not provided and a new pool needs to be - constructed, ``self.connection_pool_kw`` is used to initialize - the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` - is provided, it is used instead. Note that if a new pool does not - need to be created for the request, the provided ``pool_kwargs`` are - not used. - """ - u = parse_url(url) - return self.connection_from_host( - u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs - ) - - def _merge_pool_kwargs( - self, override: dict[str, typing.Any] | None - ) -> dict[str, typing.Any]: - """ - Merge a dictionary of override values for self.connection_pool_kw. - - This does not modify self.connection_pool_kw and returns a new dict. - Any keys in the override dictionary with a value of ``None`` are - removed from the merged dictionary. - """ - base_pool_kwargs = self.connection_pool_kw.copy() - if override: - for key, value in override.items(): - if value is None: - try: - del base_pool_kwargs[key] - except KeyError: - pass - else: - base_pool_kwargs[key] = value - return base_pool_kwargs - - def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: - """ - Indicates if the proxy requires the complete destination URL in the - request. Normally this is only needed when not using an HTTP CONNECT - tunnel. - """ - if self.proxy is None: - return False - - return not connection_requires_http_tunnel( - self.proxy, self.proxy_config, parsed_url.scheme - ) - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - """ - Same as :meth:`urllib3.HTTPConnectionPool.urlopen` - with custom cross-host redirect logic and only sends the request-uri - portion of the ``url``. - - The given ``url`` parameter must be absolute, such that an appropriate - :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. - """ - u = parse_url(url) - - if u.scheme is None: - warnings.warn( - "URLs without a scheme (ie 'https://') are deprecated and will raise an error " - "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " - "start with 'https://' or 'http://'. Read more in this issue: " - "https://github.com/urllib3/urllib3/issues/2920", - category=FutureWarning, - stacklevel=2, - ) - - conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - - kw["assert_same_host"] = False - kw["redirect"] = False - - if "headers" not in kw: - kw["headers"] = self.headers - - if self._proxy_requires_url_absolute_form(u): - response = conn.urlopen(method, url, **kw) - else: - response = conn.urlopen(method, u.request_uri, **kw) - - redirect_location = redirect and response.get_redirect_location() - if not redirect_location: - return response - - # Support relative URLs for redirecting. - redirect_location = urljoin(url, redirect_location) - - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - kw["body"] = None - kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() - - retries = kw.get("retries", response.retries) - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect) - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # conn.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not conn.is_same_host( - redirect_location - ): - new_headers = kw["headers"].copy() - for header in kw["headers"]: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - kw["headers"] = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=conn) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - kw["retries"] = retries - kw["redirect"] = redirect - - log.info("Redirecting %s -> %s", url, redirect_location) - - response.drain_conn() - return self.urlopen(method, redirect_location, **kw) - - -class ProxyManager(PoolManager): - """ - Behaves just like :class:`PoolManager`, but sends all requests through - the defined proxy, using the CONNECT method for HTTPS URLs. - - :param proxy_url: - The URL of the proxy to be used. - - :param proxy_headers: - A dictionary containing headers that will be sent to the proxy. In case - of HTTP they are being sent with each request, while in the - HTTPS/CONNECT case they are sent only once. Could be used for proxy - authentication. - - :param proxy_ssl_context: - The proxy SSL context is used to establish the TLS connection to the - proxy when using HTTPS proxies. - - :param use_forwarding_for_https: - (Defaults to False) If set to True will forward requests to the HTTPS - proxy to be made on behalf of the client instead of creating a TLS - tunnel via the CONNECT method. **Enabling this flag means that request - and response headers and content will be visible from the HTTPS proxy** - whereas tunneling keeps request and response headers and content - private. IP address, target hostname, SNI, and port are always visible - to an HTTPS proxy even when this flag is disabled. - - :param proxy_assert_hostname: - The hostname of the certificate to verify against. - - :param proxy_assert_fingerprint: - The fingerprint of the certificate to verify against. - - Example: - - .. code-block:: python - - import urllib3 - - proxy = urllib3.ProxyManager("https://localhost:3128/") - - resp1 = proxy.request("GET", "http://google.com/") - resp2 = proxy.request("GET", "http://httpbin.org/") - - # One pool was shared by both plain HTTP requests. - print(len(proxy.pools)) - # 1 - - resp3 = proxy.request("GET", "https://httpbin.org/") - resp4 = proxy.request("GET", "https://twitter.com/") - - # A separate pool was added for each HTTPS target. - print(len(proxy.pools)) - # 3 - - """ - - def __init__( - self, - proxy_url: str, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - proxy_headers: typing.Mapping[str, str] | None = None, - proxy_ssl_context: ssl.SSLContext | None = None, - use_forwarding_for_https: bool = False, - proxy_assert_hostname: None | str | typing.Literal[False] = None, - proxy_assert_fingerprint: str | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - if isinstance(proxy_url, HTTPConnectionPool): - str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" - else: - str_proxy_url = proxy_url - proxy = parse_url(str_proxy_url) - - if proxy.scheme not in ("http", "https"): - raise ProxySchemeUnknown(proxy.scheme) - - if not proxy.port: - port = port_by_scheme.get(proxy.scheme, 80) - proxy = proxy._replace(port=port) - - self.proxy = proxy - self.proxy_headers = proxy_headers or {} - self.proxy_ssl_context = proxy_ssl_context - self.proxy_config = ProxyConfig( - proxy_ssl_context, - use_forwarding_for_https, - proxy_assert_hostname, - proxy_assert_fingerprint, - ) - - connection_pool_kw["_proxy"] = self.proxy - connection_pool_kw["_proxy_headers"] = self.proxy_headers - connection_pool_kw["_proxy_config"] = self.proxy_config - - super().__init__(num_pools, headers, **connection_pool_kw) - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - if scheme == "https": - return super().connection_from_host( - host, port, scheme, pool_kwargs=pool_kwargs - ) - - return super().connection_from_host( - self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] - ) - - def _set_proxy_headers( - self, url: str, headers: typing.Mapping[str, str] | None = None - ) -> typing.Mapping[str, str]: - """ - Sets headers needed by proxies: specifically, the Accept and Host - headers. Only sets headers not provided by the user. - """ - headers_ = {"Accept": "*/*"} - - netloc = parse_url(url).netloc - if netloc: - headers_["Host"] = netloc - - if headers: - headers_.update(headers) - return headers_ - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." - u = parse_url(url) - if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): - # For connections using HTTP CONNECT, httplib sets the necessary - # headers on the CONNECT to the proxy. If we're not using CONNECT, - # we'll definitely need to set 'Host' at the very least. - headers = kw.get("headers", self.headers) - kw["headers"] = self._set_proxy_headers(url, headers) - - return super().urlopen(method, url, redirect=redirect, **kw) - - -def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: - return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed deleted file mode 100644 index 5f3ea3d919..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/py.typed +++ /dev/null @@ -1,2 +0,0 @@ -# Instruct type checkers to look for inline type annotations in this package. -# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py deleted file mode 100644 index e9246b75e3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/response.py +++ /dev/null @@ -1,1493 +0,0 @@ -from __future__ import annotations - -import collections -import io -import json as _json -import logging -import socket -import sys -import typing -import warnings -import zlib -from contextlib import contextmanager -from http.client import HTTPMessage as _HttplibHTTPMessage -from http.client import HTTPResponse as _HttplibHTTPResponse -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from ._base_connection import BaseHTTPConnection - -try: - try: - import brotlicffi as brotli # type: ignore[import-not-found] - except ImportError: - import brotli # type: ignore[import-not-found] -except ImportError: - brotli = None - -from . import util -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .connection import BaseSSLError, HTTPConnection, HTTPException -from .exceptions import ( - BodyNotHttplibCompatible, - DecodeError, - DependencyWarning, - HTTPError, - IncompleteRead, - InvalidChunkLength, - InvalidHeader, - ProtocolError, - ReadTimeoutError, - ResponseNotChunked, - SSLError, -) -from .util.response import is_fp_closed, is_response_to_head -from .util.retry import Retry - -if typing.TYPE_CHECKING: - from .connectionpool import HTTPConnectionPool - -log = logging.getLogger(__name__) - - -class ContentDecoder: - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - raise NotImplementedError() - - @property - def has_unconsumed_tail(self) -> bool: - raise NotImplementedError() - - def flush(self) -> bytes: - raise NotImplementedError() - - -class DeflateDecoder(ContentDecoder): - def __init__(self) -> None: - self._first_try = True - self._first_try_data = b"" - self._unfed_data = b"" - self._obj = zlib.decompressobj() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - data = self._unfed_data + data - self._unfed_data = b"" - if not data and not self._obj.unconsumed_tail: - return data - original_max_length = max_length - if original_max_length < 0: - max_length = 0 - elif original_max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unfed_data = data - return b"" - - # Subsequent calls always reuse `self._obj`. zlib requires - # passing the unconsumed tail if decompression is to continue. - if not self._first_try: - return self._obj.decompress( - self._obj.unconsumed_tail + data, max_length=max_length - ) - - # First call tries with RFC 1950 ZLIB format. - self._first_try_data += data - try: - decompressed = self._obj.decompress(data, max_length=max_length) - if decompressed: - self._first_try = False - self._first_try_data = b"" - return decompressed - # On failure, it falls back to RFC 1951 DEFLATE format. - except zlib.error: - self._first_try = False - self._obj = zlib.decompressobj(-zlib.MAX_WBITS) - try: - return self.decompress( - self._first_try_data, max_length=original_max_length - ) - finally: - self._first_try_data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unfed_data) or ( - bool(self._obj.unconsumed_tail) and not self._first_try - ) - - def flush(self) -> bytes: - return self._obj.flush() - - -class GzipDecoderState: - FIRST_MEMBER = 0 - OTHER_MEMBERS = 1 - SWALLOW_DATA = 2 - - -class GzipDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - self._state = GzipDecoderState.FIRST_MEMBER - self._unconsumed_tail = b"" - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - ret = bytearray() - if self._state == GzipDecoderState.SWALLOW_DATA: - return bytes(ret) - - if max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unconsumed_tail += data - return b"" - - # zlib requires passing the unconsumed tail to the subsequent - # call if decompression is to continue. - data = self._unconsumed_tail + data - if not data and self._obj.eof: - return bytes(ret) - - while True: - try: - ret += self._obj.decompress( - data, max_length=max(max_length - len(ret), 0) - ) - except zlib.error: - previous_state = self._state - # Ignore data after the first error - self._state = GzipDecoderState.SWALLOW_DATA - self._unconsumed_tail = b"" - if previous_state == GzipDecoderState.OTHER_MEMBERS: - # Allow trailing garbage acceptable in other gzip clients - return bytes(ret) - raise - - self._unconsumed_tail = data = ( - self._obj.unconsumed_tail or self._obj.unused_data - ) - if max_length > 0 and len(ret) >= max_length: - break - - if not data: - return bytes(ret) - # When the end of a gzip member is reached, a new decompressor - # must be created for unused (possibly future) data. - if self._obj.eof: - self._state = GzipDecoderState.OTHER_MEMBERS - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - - return bytes(ret) - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unconsumed_tail) - - def flush(self) -> bytes: - return self._obj.flush() - - -if brotli is not None: - - class BrotliDecoder(ContentDecoder): - # Supports both 'brotlipy' and 'Brotli' packages - # since they share an import name. The top branches - # are for 'brotlipy' and bottom branches for 'Brotli' - def __init__(self) -> None: - self._obj = brotli.Decompressor() - if hasattr(self._obj, "decompress"): - setattr(self, "_decompress", self._obj.decompress) - else: - setattr(self, "_decompress", self._obj.process) - - # Requires Brotli >= 1.2.0 for `output_buffer_limit`. - def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: - raise NotImplementedError() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - try: - if max_length > 0: - return self._decompress(data, output_buffer_limit=max_length) - else: - return self._decompress(data) - except TypeError: - # Fallback for Brotli/brotlicffi/brotlipy versions without - # the `output_buffer_limit` parameter. - warnings.warn( - "Brotli >= 1.2.0 is required to prevent decompression bombs.", - DependencyWarning, - ) - return self._decompress(data) - - @property - def has_unconsumed_tail(self) -> bool: - try: - return not self._obj.can_accept_more_data() - except AttributeError: - return False - - def flush(self) -> bytes: - if hasattr(self._obj, "flush"): - return self._obj.flush() # type: ignore[no-any-return] - return b"" - - -try: - if sys.version_info >= (3, 14): - from compression import zstd - else: - from backports import zstd -except ImportError: - HAS_ZSTD = False -else: - HAS_ZSTD = True - - class ZstdDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zstd.ZstdDecompressor() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if not data and not self.has_unconsumed_tail: - return b"" - if self._obj.eof: - data = self._obj.unused_data + data - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress(data, max_length=max_length) - length = len(part) - data_parts = [part] - # Every loop iteration is supposed to read data from a separate frame. - # The loop breaks when: - # - enough data is read; - # - no more unused data is available; - # - end of the last read frame has not been reached (i.e., - # more data has to be fed). - while ( - self._obj.eof - and self._obj.unused_data - and (max_length < 0 or length < max_length) - ): - unused_data = self._obj.unused_data - if not self._obj.needs_input: - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress( - unused_data, - max_length=(max_length - length) if max_length > 0 else -1, - ) - if part_length := len(part): - data_parts.append(part) - length += part_length - elif self._obj.needs_input: - break - return b"".join(data_parts) - - @property - def has_unconsumed_tail(self) -> bool: - return not (self._obj.needs_input or self._obj.eof) or bool( - self._obj.unused_data - ) - - def flush(self) -> bytes: - if not self._obj.eof: - raise DecodeError("Zstandard data is incomplete") - return b"" - - -class MultiDecoder(ContentDecoder): - """ - From RFC7231: - If one or more encodings have been applied to a representation, the - sender that applied the encodings MUST generate a Content-Encoding - header field that lists the content codings in the order in which - they were applied. - """ - - # Maximum allowed number of chained HTTP encodings in the - # Content-Encoding header. - max_decode_links = 5 - - def __init__(self, modes: str) -> None: - encodings = [m.strip() for m in modes.split(",")] - if len(encodings) > self.max_decode_links: - raise DecodeError( - "Too many content encodings in the chain: " - f"{len(encodings)} > {self.max_decode_links}" - ) - self._decoders = [_get_decoder(e) for e in encodings] - - def flush(self) -> bytes: - return self._decoders[0].flush() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if max_length <= 0: - for d in reversed(self._decoders): - data = d.decompress(data) - return data - - ret = bytearray() - # Every while loop iteration goes through all decoders once. - # It exits when enough data is read or no more data can be read. - # It is possible that the while loop iteration does not produce - # any data because we retrieve up to `max_length` from every - # decoder, and the amount of bytes may be insufficient for the - # next decoder to produce enough/any output. - while True: - any_data = False - for d in reversed(self._decoders): - data = d.decompress(data, max_length=max_length - len(ret)) - if data: - any_data = True - # We should not break when no data is returned because - # next decoders may produce data even with empty input. - ret += data - if not any_data or len(ret) >= max_length: - return bytes(ret) - data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return any(d.has_unconsumed_tail for d in self._decoders) - - -def _get_decoder(mode: str) -> ContentDecoder: - if "," in mode: - return MultiDecoder(mode) - - # According to RFC 9110 section 8.4.1.3, recipients should - # consider x-gzip equivalent to gzip - if mode in ("gzip", "x-gzip"): - return GzipDecoder() - - if brotli is not None and mode == "br": - return BrotliDecoder() - - if HAS_ZSTD and mode == "zstd": - return ZstdDecoder() - - return DeflateDecoder() - - -class BytesQueueBuffer: - """Memory-efficient bytes buffer - - To return decoded data in read() and still follow the BufferedIOBase API, we need a - buffer to always return the correct amount of bytes. - - This buffer should be filled using calls to put() - - Our maximum memory usage is determined by the sum of the size of: - - * self.buffer, which contains the full data - * the largest chunk that we will copy in get() - """ - - def __init__(self) -> None: - self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() - self._size: int = 0 - - def __len__(self) -> int: - return self._size - - def put(self, data: bytes) -> None: - self.buffer.append(data) - self._size += len(data) - - def get(self, n: int) -> bytes: - if n == 0: - return b"" - elif not self.buffer: - raise RuntimeError("buffer is empty") - elif n < 0: - raise ValueError("n should be > 0") - - if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): - self._size -= n - return self.buffer.popleft() - - fetched = 0 - ret = io.BytesIO() - while fetched < n: - remaining = n - fetched - chunk = self.buffer.popleft() - chunk_length = len(chunk) - if remaining < chunk_length: - chunk = memoryview(chunk) - left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] - ret.write(left_chunk) - self.buffer.appendleft(right_chunk) - self._size -= remaining - break - else: - ret.write(chunk) - self._size -= chunk_length - fetched += chunk_length - - if not self.buffer: - break - - return ret.getvalue() - - def get_all(self) -> bytes: - buffer = self.buffer - if not buffer: - assert self._size == 0 - return b"" - if len(buffer) == 1: - result = buffer.pop() - if isinstance(result, memoryview): - result = result.tobytes() - else: - ret = io.BytesIO() - ret.writelines(buffer.popleft() for _ in range(len(buffer))) - result = ret.getvalue() - self._size = 0 - return result - - -class BaseHTTPResponse(io.IOBase): - CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] - if brotli is not None: - CONTENT_DECODERS += ["br"] - if HAS_ZSTD: - CONTENT_DECODERS += ["zstd"] - REDIRECT_STATUSES = [301, 302, 303, 307, 308] - - DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) - if brotli is not None: - DECODER_ERROR_CLASSES += (brotli.error,) - - if HAS_ZSTD: - DECODER_ERROR_CLASSES += (zstd.ZstdError,) - - def __init__( - self, - *, - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int, - version: int, - version_string: str, - reason: str | None, - decode_content: bool, - request_url: str | None, - retries: Retry | None = None, - ) -> None: - if isinstance(headers, HTTPHeaderDict): - self.headers = headers - else: - self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] - self.status = status - self.version = version - self.version_string = version_string - self.reason = reason - self.decode_content = decode_content - self._has_decoded_content = False - self._request_url: str | None = request_url - self.retries = retries - - self.chunked = False - tr_enc = self.headers.get("transfer-encoding", "").lower() - # Don't incur the penalty of creating a list and then discarding it - encodings = (enc.strip() for enc in tr_enc.split(",")) - if "chunked" in encodings: - self.chunked = True - - self._decoder: ContentDecoder | None = None - self.length_remaining: int | None - - def get_redirect_location(self) -> str | None | typing.Literal[False]: - """ - Should we redirect and where to? - - :returns: Truthy redirect location string if we got a redirect status - code and valid location. ``None`` if redirect status and no - location. ``False`` if not a redirect status code. - """ - if self.status in self.REDIRECT_STATUSES: - return self.headers.get("location") - return False - - @property - def data(self) -> bytes: - raise NotImplementedError() - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - @property - def url(self) -> str | None: - raise NotImplementedError() - - @url.setter - def url(self, url: str | None) -> None: - raise NotImplementedError() - - @property - def connection(self) -> BaseHTTPConnection | None: - raise NotImplementedError() - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - if retries is not None and retries.history: - self.url = retries.history[-1].redirect_location - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - raise NotImplementedError() - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - raise NotImplementedError() - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def release_conn(self) -> None: - raise NotImplementedError() - - def drain_conn(self) -> None: - raise NotImplementedError() - - def shutdown(self) -> None: - raise NotImplementedError() - - def close(self) -> None: - raise NotImplementedError() - - def _init_decoder(self) -> None: - """ - Set-up the _decoder attribute if necessary. - """ - # Note: content-encoding value should be case-insensitive, per RFC 7230 - # Section 3.2 - content_encoding = self.headers.get("content-encoding", "").lower() - if self._decoder is None: - if content_encoding in self.CONTENT_DECODERS: - self._decoder = _get_decoder(content_encoding) - elif "," in content_encoding: - encodings = [ - e.strip() - for e in content_encoding.split(",") - if e.strip() in self.CONTENT_DECODERS - ] - if encodings: - self._decoder = _get_decoder(content_encoding) - - def _decode( - self, - data: bytes, - decode_content: bool | None, - flush_decoder: bool, - max_length: int | None = None, - ) -> bytes: - """ - Decode the data passed in and potentially flush the decoder. - """ - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - if max_length is None or flush_decoder: - max_length = -1 - - try: - if self._decoder: - data = self._decoder.decompress(data, max_length=max_length) - self._has_decoded_content = True - except self.DECODER_ERROR_CLASSES as e: - content_encoding = self.headers.get("content-encoding", "").lower() - raise DecodeError( - "Received response with content-encoding: %s, but " - "failed to decode it." % content_encoding, - e, - ) from e - if flush_decoder: - data += self._flush_decoder() - - return data - - def _flush_decoder(self) -> bytes: - """ - Flushes the decoder. Should only be called if the decoder is actually - being used. - """ - if self._decoder: - return self._decoder.decompress(b"") + self._decoder.flush() - return b"" - - # Compatibility methods for `io` module - def readinto(self, b: bytearray | memoryview[int]) -> int: - temp = self.read(len(b)) - if len(temp) == 0: - return 0 - else: - b[: len(temp)] = temp - return len(temp) - - # Methods used by dependent libraries - def getheaders(self) -> HTTPHeaderDict: - return self.headers - - def getheader(self, name: str, default: str | None = None) -> str | None: - return self.headers.get(name, default) - - # Compatibility method for http.cookiejar - def info(self) -> HTTPHeaderDict: - return self.headers - - def geturl(self) -> str | None: - return self.url - - -class HTTPResponse(BaseHTTPResponse): - """ - HTTP Response container. - - Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is - loaded and decoded on-demand when the ``data`` property is accessed. This - class is also compatible with the Python standard library's :mod:`io` - module, and can hence be treated as a readable object in the context of that - framework. - - Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param original_response: - When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` - object, it's convenient to include the original for debug purposes. It's - otherwise unused. - - :param retries: - The retries contains the last :class:`~urllib3.util.retry.Retry` that - was used during the request. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - - def __init__( - self, - body: _TYPE_BODY = "", - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int = 0, - version: int = 0, - version_string: str = "HTTP/?", - reason: str | None = None, - preload_content: bool = True, - decode_content: bool = True, - original_response: _HttplibHTTPResponse | None = None, - pool: HTTPConnectionPool | None = None, - connection: HTTPConnection | None = None, - msg: _HttplibHTTPMessage | None = None, - retries: Retry | None = None, - enforce_content_length: bool = True, - request_method: str | None = None, - request_url: str | None = None, - auto_close: bool = True, - sock_shutdown: typing.Callable[[int], None] | None = None, - ) -> None: - super().__init__( - headers=headers, - status=status, - version=version, - version_string=version_string, - reason=reason, - decode_content=decode_content, - request_url=request_url, - retries=retries, - ) - - self.enforce_content_length = enforce_content_length - self.auto_close = auto_close - - self._body = None - self._uncached_read_occurred = False - self._fp: _HttplibHTTPResponse | None = None - self._original_response = original_response - self._fp_bytes_read = 0 - self.msg = msg - - if body and isinstance(body, (str, bytes)): - self._body = body - - self._pool = pool - self._connection = connection - - if hasattr(body, "read"): - self._fp = body # type: ignore[assignment] - self._sock_shutdown = sock_shutdown - - # Are we using the chunked-style of transfer encoding? - self.chunk_left: int | None = None - - # Determine length of response - self.length_remaining = self._init_length(request_method) - - # Used to return the correct amount of bytes for partial read()s - self._decoded_buffer = BytesQueueBuffer() - - # If requested, preload the body. - if preload_content and not self._body: - self._body = self.read(decode_content=decode_content) - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - """ - Read and discard any remaining HTTP response data in the response connection. - - Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. - """ - try: - self._raw_read() - except (HTTPError, OSError, BaseSSLError, HTTPException): - pass - if self._has_decoded_content: - # `_raw_read` skips decompression, so we should clean up the - # decoder to avoid keeping unnecessary data in memory. - self._decoded_buffer = BytesQueueBuffer() - self._decoder = None - - @property - def data(self) -> bytes: - # For backwards-compat with earlier urllib3 0.4 and earlier. - if self._body: - return self._body # type: ignore[return-value] - - if self._fp: - return self.read(cache_content=True) - - return None # type: ignore[return-value] - - @property - def connection(self) -> HTTPConnection | None: - return self._connection - - def isclosed(self) -> bool: - return is_fp_closed(self._fp) - - def tell(self) -> int: - """ - Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:`HTTPResponse.read` - if bytes are encoded on the wire (e.g, compressed). - """ - return self._fp_bytes_read - - def _init_length(self, request_method: str | None) -> int | None: - """ - Set initial length value for Response content if available. - """ - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - if self.chunked: - # This Response will fail with an IncompleteRead if it can't be - # received as chunked. This method falls back to attempt reading - # the response before raising an exception. - log.warning( - "Received response with both Content-Length and " - "Transfer-Encoding set. This is expressly forbidden " - "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " - "attempting to process response as Transfer-Encoding: " - "chunked." - ) - return None - - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Convert status to int for comparison - # In some cases, httplib returns a status of "_UNKNOWN" - try: - status = int(self.status) - except ValueError: - status = 0 - - # Check for responses that shouldn't include a body - if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": - length = 0 - - return length - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch low-level python exceptions, instead re-raising urllib3 - variants, so that low-level exceptions are not leaked in the - high-level api. - - On exit, release the connection back to the pool. - """ - clean_exit = False - - try: - try: - yield - - except SocketTimeout as e: - # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but - # there is yet no clean way to get at it from this context. - raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] - - except BaseSSLError as e: - # SSL errors related to framing/MAC get wrapped and reraised here - raise SSLError(e) from e - - except IncompleteRead as e: - if ( - e.expected is not None - and e.partial is not None - and e.expected == -e.partial - ): - arg = "Response may not contain content." - else: - arg = f"Connection broken: {e!r}" - raise ProtocolError(arg, e) from e - - except (HTTPException, OSError) as e: - raise ProtocolError(f"Connection broken: {e!r}", e) from e - - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now to ensure that the connection is - # released back to the pool. - if self._original_response: - self._original_response.close() - - # Closing the response may not actually be sufficient to close - # everything, so if we have a hold of the connection close that - # too. - if self._connection: - self._connection.close() - - # If we hold the original response but it's closed now, we should - # return the connection back to the pool. - if self._original_response and self._original_response.isclosed(): - self.release_conn() - - def _fp_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Read a response with the thought that reading the number of bytes - larger than can fit in a 32-bit int at a time via SSL in some - known cases leads to an overflow error that has to be prevented - if `amt` or `self.length_remaining` indicate that a problem may - happen. - - This happens to urllib3 injected with pyOpenSSL-backed SSL-support. - """ - assert self._fp - c_int_max = 2**31 - 1 - if ( - (amt and amt > c_int_max) - or ( - amt is None - and self.length_remaining - and self.length_remaining > c_int_max - ) - ) and util.IS_PYOPENSSL: - if read1: - return self._fp.read1(c_int_max) - buffer = io.BytesIO() - # Besides `max_chunk_amt` being a maximum chunk size, it - # affects memory overhead of reading a response by this - # method in CPython. - # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum - # chunk size that does not lead to an overflow error, but - # 256 MiB is a compromise. - max_chunk_amt = 2**28 - while amt is None or amt != 0: - if amt is not None: - chunk_amt = min(amt, max_chunk_amt) - amt -= chunk_amt - else: - chunk_amt = max_chunk_amt - data = self._fp.read(chunk_amt) - if not data: - break - buffer.write(data) - del data # to reduce peak memory usage by `max_chunk_amt`. - return buffer.getvalue() - elif read1: - return self._fp.read1(amt) if amt is not None else self._fp.read1() - else: - # StringIO doesn't like amt=None - return self._fp.read(amt) if amt is not None else self._fp.read() - - def _raw_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Reads `amt` of bytes from the socket. - """ - if self._fp is None: - return None # type: ignore[return-value] - - fp_closed = getattr(self._fp, "closed", False) - - with self._error_catcher(): - data = self._fp_read(amt, read1=read1) if not fp_closed else b"" - if amt is not None and amt != 0 and not data: - # Platform-specific: Buggy versions of Python. - # Close the connection when no data is returned - # - # This is redundant to what httplib/http.client _should_ - # already do. However, versions of python released before - # December 15, 2012 (http://bugs.python.org/issue16298) do - # not properly close the connection in all cases. There is - # no harm in redundantly calling close. - self._fp.close() - if ( - self.enforce_content_length - and self.length_remaining is not None - and self.length_remaining != 0 - ): - # This is an edge case that httplib failed to cover due - # to concerns of backward compatibility. We're - # addressing it here to make sure IncompleteRead is - # raised during streaming, so all calls with incorrect - # Content-Length are caught. - raise IncompleteRead(self._fp_bytes_read, self.length_remaining) - elif read1 and ( - (amt != 0 and not data) or self.length_remaining == len(data) - ): - # All data has been read, but `self._fp.read1` in - # CPython 3.12 and older doesn't always close - # `http.client.HTTPResponse`, so we close it here. - # See https://github.com/python/cpython/issues/113199 - self._fp.close() - - if data: - self._fp_bytes_read += len(data) - if self.length_remaining is not None: - self.length_remaining -= len(data) - return data - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - """ - Similar to :meth:`http.client.HTTPResponse.read`, but with two additional - parameters: ``decode_content`` and ``cache_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param cache_content: - If True, will save the returned data such that the same result is - returned despite of the state of the underlying file object. This - is useful if you want the ``.data`` property to continue working - after having ``.read()`` the file object. (Overridden if ``amt`` is - set.) - """ - self._init_decoder() - if decode_content is None: - decode_content = self.decode_content - - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - elif amt is not None: - cache_content = False - - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and len(self._decoded_buffer) < amt - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) >= amt: - return self._decoded_buffer.get(amt) - - data = self._raw_read(amt) - if not cache_content: - self._uncached_read_occurred = True - - flush_decoder = amt is None or (amt != 0 and not data) - - if ( - not data - and len(self._decoded_buffer) == 0 - and not (self._decoder and self._decoder.has_unconsumed_tail) - ): - return data - - if amt is None: - data = self._decode(data, decode_content, flush_decoder) - # It's possible that there is buffered decoded data after a - # partial read. - if decode_content and len(self._decoded_buffer) > 0: - self._decoded_buffer.put(data) - data = self._decoded_buffer.get_all() - - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - # do not waste memory on buffer when not decoding - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - - while len(self._decoded_buffer) < amt and data: - # TODO make sure to initially read enough data to get past the headers - # For example, the GZ file header takes 10 bytes, we don't want to read - # it one byte at a time - data = self._raw_read(amt) - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - data = self._decoded_buffer.get(amt) - - return data - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - """ - Similar to ``http.client.HTTPResponse.read1`` and documented - in :meth:`io.BufferedReader.read1`, but with an additional parameter: - ``decode_content``. - - :param amt: - How much of the content to read. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if decode_content is None: - decode_content = self.decode_content - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - # try and respond without going to the network - if self._has_decoded_content: - if not decode_content: - raise RuntimeError( - "Calling read1(decode_content=False) is not supported after " - "read1(decode_content=True) was called." - ) - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and (amt is None or len(self._decoded_buffer) < amt) - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=( - amt - len(self._decoded_buffer) if amt is not None else None - ), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) > 0: - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - if amt == 0: - return b"" - - # FIXME, this method's type doesn't say returning None is possible - data = self._raw_read(amt, read1=True) - self._uncached_read_occurred = True - if not decode_content or data is None: - return data - - self._init_decoder() - while True: - flush_decoder = not data - decoded_data = self._decode( - data, decode_content, flush_decoder, max_length=amt - ) - self._decoded_buffer.put(decoded_data) - if decoded_data or flush_decoder: - break - data = self._raw_read(8192, read1=True) - - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if amt == 0: - return - - if self.chunked and self.supports_chunked_reads(): - yield from self.read_chunked(amt, decode_content=decode_content) - else: - while ( - not is_fp_closed(self._fp) - or len(self._decoded_buffer) > 0 - or (self._decoder and self._decoder.has_unconsumed_tail) - ): - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - - # Overrides from io.IOBase - def readable(self) -> bool: - return True - - def shutdown(self) -> None: - if not self._sock_shutdown: - raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") - if self._connection is None: - raise RuntimeError( - "Cannot shutdown as connection has already been released to the pool" - ) - self._sock_shutdown(socket.SHUT_RD) - - def close(self) -> None: - self._sock_shutdown = None - - if not self.closed and self._fp: - self._fp.close() - - if self._connection: - self._connection.close() - - if not self.auto_close: - io.IOBase.close(self) - - @property - def closed(self) -> bool: - if not self.auto_close: - return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] - elif self._fp is None: - return True - elif hasattr(self._fp, "isclosed"): - return self._fp.isclosed() - elif hasattr(self._fp, "closed"): - return self._fp.closed - else: - return True - - def fileno(self) -> int: - if self._fp is None: - raise OSError("HTTPResponse has no file to get a fileno from") - elif hasattr(self._fp, "fileno"): - return self._fp.fileno() - else: - raise OSError( - "The file-like object this HTTPResponse is wrapped " - "around has no file descriptor" - ) - - def flush(self) -> None: - if ( - self._fp is not None - and hasattr(self._fp, "flush") - and not getattr(self._fp, "closed", False) - ): - return self._fp.flush() - - def supports_chunked_reads(self) -> bool: - """ - Checks if the underlying file-like object looks like a - :class:`http.client.HTTPResponse` object. We do this by testing for - the fp attribute. If it is present we assume it returns raw chunks as - processed by read_chunked(). - """ - return hasattr(self._fp, "fp") - - def _update_chunk_length(self) -> None: - # First, we'll figure out length of a chunk and then - # we'll try to read it from socket. - if self.chunk_left is not None: - return None - line = self._fp.fp.readline() # type: ignore[union-attr] - line = line.split(b";", 1)[0] - try: - self.chunk_left = int(line, 16) - except ValueError: - self.close() - if line: - # Invalid chunked protocol response, abort. - raise InvalidChunkLength(self, line) from None - else: - # Truncated at start of next chunk - raise ProtocolError("Response ended prematurely") from None - - def _handle_chunk(self, amt: int | None) -> bytes: - returned_chunk = None - if amt is None: - chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - returned_chunk = chunk - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - elif self.chunk_left is not None and amt < self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self.chunk_left = self.chunk_left - amt - returned_chunk = value - elif amt == self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - returned_chunk = value - else: # amt > self.chunk_left - returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - return returned_chunk # type: ignore[no-any-return] - - def read_chunked( - self, amt: int | None = None, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - Similar to :meth:`HTTPResponse.read`, but with an additional - parameter: ``decode_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - self._init_decoder() - # FIXME: Rewrite this method and make it a class with a better structured logic. - if not self.chunked: - raise ResponseNotChunked( - "Response is not chunked. " - "Header 'transfer-encoding: chunked' is missing." - ) - if not self.supports_chunked_reads(): - raise BodyNotHttplibCompatible( - "Body should be http.client.HTTPResponse like. " - "It should have have an fp attribute which returns raw chunks." - ) - - with self._error_catcher(): - # Don't bother reading the body of a HEAD request. - if self._original_response and is_response_to_head(self._original_response): - self._original_response.close() - return None - - # If a response is already read and closed - # then return immediately. - if self._fp.fp is None: # type: ignore[union-attr] - return None - - if amt == 0: - return - elif amt and amt < 0: - # Negative numbers and `None` should be treated the same, - # but httplib handles only `None` correctly. - amt = None - - while True: - # First, check if any data is left in the decoder's buffer. - if self._decoder and self._decoder.has_unconsumed_tail: - chunk = b"" - else: - self._update_chunk_length() - self._uncached_read_occurred = True - if self.chunk_left == 0: - break - chunk = self._handle_chunk(amt) - decoded = self._decode( - chunk, - decode_content=decode_content, - flush_decoder=False, - max_length=amt, - ) - if decoded: - yield decoded - - if decode_content: - # On CPython and PyPy, we should never need to flush the - # decoder. However, on Jython we *might* need to, so - # lets defensively do it anyway. - decoded = self._flush_decoder() - if decoded: # Platform-specific: Jython. - yield decoded - - # Chunk content ends with \r\n: discard it. - while self._fp is not None: - line = self._fp.fp.readline() - if not line: - # Some sites may not end with '\r\n'. - break - if line == b"\r\n": - break - - # We read everything; close the "file". - if self._original_response: - self._original_response.close() - - @property - def url(self) -> str | None: - """ - Returns the URL that was the source of this response. - If the request that generated this response redirected, this method - will return the final redirect location. - """ - return self._request_url - - @url.setter - def url(self, url: str | None) -> None: - self._request_url = url - - def __iter__(self) -> typing.Iterator[bytes]: - buffer: list[bytes] = [] - for chunk in self.stream(decode_content=True): - if b"\n" in chunk: - chunks = chunk.split(b"\n") - yield b"".join(buffer) + chunks[0] + b"\n" - for x in chunks[1:-1]: - yield x + b"\n" - if chunks[-1]: - buffer = [chunks[-1]] - else: - buffer = [] - else: - buffer.append(chunk) - if buffer: - yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py deleted file mode 100644 index 534126033c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# For backwards compatibility, provide imports that used to be here. -from __future__ import annotations - -from .connection import is_connection_dropped -from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers -from .response import is_fp_closed -from .retry import Retry -from .ssl_ import ( - ALPN_PROTOCOLS, - IS_PYOPENSSL, - SSLContext, - assert_fingerprint, - create_urllib3_context, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .timeout import Timeout -from .url import Url, parse_url -from .wait import wait_for_read, wait_for_write - -__all__ = ( - "IS_PYOPENSSL", - "SSLContext", - "ALPN_PROTOCOLS", - "Retry", - "Timeout", - "Url", - "assert_fingerprint", - "create_urllib3_context", - "is_connection_dropped", - "is_fp_closed", - "parse_url", - "make_headers", - "resolve_cert_reqs", - "resolve_ssl_version", - "ssl_wrap_socket", - "wait_for_read", - "wait_for_write", - "SKIP_HEADER", - "SKIPPABLE_HEADERS", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py deleted file mode 100644 index f92519ee91..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/connection.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import annotations - -import socket -import typing - -from ..exceptions import LocationParseError -from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT - -_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] - -if typing.TYPE_CHECKING: - from .._base_connection import BaseHTTPConnection - - -def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific - """ - Returns True if the connection is dropped and should be closed. - :param conn: :class:`urllib3.connection.HTTPConnection` object. - """ - return not conn.is_connected - - -# This function is copied from socket.py in the Python 2.7 standard -# library test suite. Added to its signature is only `socket_options`. -# One additional modification is that we avoid binding to IPv6 servers -# discovered in DNS if the system doesn't have IPv6 functionality. -def create_connection( - address: tuple[str, int], - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, -) -> socket.socket: - """Connect to *address* and return the socket object. - - Convenience function. Connect to *address* (a 2-tuple ``(host, - port)``) and return the socket object. Passing the optional - *timeout* parameter will set the timeout on the socket instance - before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`socket.getdefaulttimeout` - is used. If *source_address* is set it must be a tuple of (host, port) - for the socket to bind as a source address before making the connection. - An host of '' or port 0 tells the OS to use the default. - """ - - host, port = address - if host.startswith("["): - host = host.strip("[]") - err = None - - # Using the value from allowed_gai_family() in the context of getaddrinfo lets - # us select whether to work with IPv4 DNS records, IPv6 records, or both. - # The original create_connection function always returns all records. - family = allowed_gai_family() - - try: - host.encode("idna") - except UnicodeError: - raise LocationParseError(f"'{host}', label empty or too long") from None - - for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = None - try: - sock = socket.socket(af, socktype, proto) - - # If provided, set socket level options before connecting. - _set_socket_options(sock, socket_options) - - if timeout is not _DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(sa) - # Break explicitly a reference cycle - err = None - return sock - - except OSError as _: - err = _ - if sock is not None: - sock.close() - - if err is not None: - try: - raise err - finally: - # Break explicitly a reference cycle - err = None - else: - raise OSError("getaddrinfo returns an empty list") - - -def _set_socket_options( - sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None -) -> None: - if options is None: - return - - for opt in options: - sock.setsockopt(*opt) - - -def allowed_gai_family() -> socket.AddressFamily: - """This function is designed to work in the context of - getaddrinfo, where family=socket.AF_UNSPEC is the default and - will perform a DNS search for both IPv6 and IPv4 records.""" - - family = socket.AF_INET - if HAS_IPV6: - family = socket.AF_UNSPEC - return family - - -def _has_ipv6(host: str) -> bool: - """Returns True if the system can bind an IPv6 address.""" - sock = None - has_ipv6 = False - - if socket.has_ipv6: - # has_ipv6 returns true if cPython was compiled with IPv6 support. - # It does not tell us if the system has IPv6 support enabled. To - # determine that we must bind to an IPv6 address. - # https://github.com/urllib3/urllib3/pull/611 - # https://bugs.python.org/issue658327 - try: - sock = socket.socket(socket.AF_INET6) - sock.bind((host, 0)) - has_ipv6 = True - except Exception: - pass - - if sock: - sock.close() - return has_ipv6 - - -HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py deleted file mode 100644 index 908fc6621d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/proxy.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import typing - -from .url import Url - -if typing.TYPE_CHECKING: - from ..connection import ProxyConfig - - -def connection_requires_http_tunnel( - proxy_url: Url | None = None, - proxy_config: ProxyConfig | None = None, - destination_scheme: str | None = None, -) -> bool: - """ - Returns True if the connection requires an HTTP CONNECT through the proxy. - - :param URL proxy_url: - URL of the proxy. - :param ProxyConfig proxy_config: - Proxy configuration from poolmanager.py - :param str destination_scheme: - The scheme of the destination. (i.e https, http, etc) - """ - # If we're not using a proxy, no way to use a tunnel. - if proxy_url is None: - return False - - # HTTP destinations never require tunneling, we always forward. - if destination_scheme == "http": - return False - - # Support for forwarding with HTTPS proxies and HTTPS destinations. - if ( - proxy_url.scheme == "https" - and proxy_config - and proxy_config.use_forwarding_for_https - ): - return False - - # Otherwise always use a tunnel. - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py deleted file mode 100644 index 6c2372ba7e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/request.py +++ /dev/null @@ -1,263 +0,0 @@ -from __future__ import annotations - -import io -import sys -import typing -from base64 import b64encode -from enum import Enum - -from ..exceptions import UnrewindableBodyError -from .util import to_bytes - -if typing.TYPE_CHECKING: - from typing import Final - -# Pass as a value within ``headers`` to skip -# emitting some HTTP headers that are added automatically. -# The only headers that are supported are ``Accept-Encoding``, -# ``Host``, and ``User-Agent``. -SKIP_HEADER = "@@@SKIP_HEADER@@@" -SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) - -ACCEPT_ENCODING = "gzip,deflate" -try: - try: - import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 - except ImportError: - import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",br" - -try: - if sys.version_info >= (3, 14): - from compression import zstd as _unused_module_zstd # noqa: F401 - else: - from backports import zstd as _unused_module_zstd # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",zstd" - - -class _TYPE_FAILEDTELL(Enum): - token = 0 - - -_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token - -_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] - -# When sending a request with these methods we aren't expecting -# a body so don't need to set an explicit 'Content-Length: 0' -# The reason we do this in the negative instead of tracking methods -# which 'should' have a body is because unknown methods should be -# treated as if they were 'POST' which *does* expect a body. -_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} - - -def make_headers( - keep_alive: bool | None = None, - accept_encoding: bool | list[str] | str | None = None, - user_agent: str | None = None, - basic_auth: str | None = None, - proxy_basic_auth: str | None = None, - disable_cache: bool | None = None, -) -> dict[str, str]: - """ - Shortcuts for generating request headers. - - :param keep_alive: - If ``True``, adds 'connection: keep-alive' header. - - :param accept_encoding: - Can be a boolean, list, or string. - ``True`` translates to 'gzip,deflate'. If the dependencies for - Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or - Zstandard (the ``backports.zstd`` package for Python before 3.14) - algorithms are installed, then their encodings are - included in the string ('br' and 'zstd', respectively). - List will get joined by comma. - String will be used as provided. - - :param user_agent: - String representing the user-agent you want, such as - "python-urllib3/0.6" - - :param basic_auth: - Colon-separated username:password string for 'authorization: basic ...' - auth header. - - :param proxy_basic_auth: - Colon-separated username:password string for 'proxy-authorization: basic ...' - auth header. - - :param disable_cache: - If ``True``, adds 'cache-control: no-cache' header. - - Example: - - .. code-block:: python - - import urllib3 - - print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) - # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} - print(urllib3.util.make_headers(accept_encoding=True)) - # {'accept-encoding': 'gzip,deflate'} - """ - headers: dict[str, str] = {} - if accept_encoding: - if isinstance(accept_encoding, str): - pass - elif isinstance(accept_encoding, list): - accept_encoding = ",".join(accept_encoding) - else: - accept_encoding = ACCEPT_ENCODING - headers["accept-encoding"] = accept_encoding - - if user_agent: - headers["user-agent"] = user_agent - - if keep_alive: - headers["connection"] = "keep-alive" - - if basic_auth: - headers["authorization"] = ( - f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" - ) - - if proxy_basic_auth: - headers["proxy-authorization"] = ( - f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" - ) - - if disable_cache: - headers["cache-control"] = "no-cache" - - return headers - - -def set_file_position( - body: typing.Any, pos: _TYPE_BODY_POSITION | None -) -> _TYPE_BODY_POSITION | None: - """ - If a position is provided, move file to that point. - Otherwise, we'll attempt to record a position for future use. - """ - if pos is not None: - rewind_body(body, pos) - elif getattr(body, "tell", None) is not None: - try: - pos = body.tell() - except OSError: - # This differentiates from None, allowing us to catch - # a failed `tell()` later when trying to rewind the body. - pos = _FAILEDTELL - - return pos - - -def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: - """ - Attempt to rewind body to a certain position. - Primarily used for request redirects and retries. - - :param body: - File-like object that supports seek. - - :param int pos: - Position to seek to in file. - """ - body_seek = getattr(body, "seek", None) - if body_seek is not None and isinstance(body_pos, int): - try: - body_seek(body_pos) - except OSError as e: - raise UnrewindableBodyError( - "An error occurred when rewinding request body for redirect/retry." - ) from e - elif body_pos is _FAILEDTELL: - raise UnrewindableBodyError( - "Unable to record file position for rewinding " - "request body during a redirect/retry." - ) - else: - raise ValueError( - f"body_pos must be of type integer, instead it was {type(body_pos)}." - ) - - -class ChunksAndContentLength(typing.NamedTuple): - chunks: typing.Iterable[bytes] | None - content_length: int | None - - -def body_to_chunks( - body: typing.Any | None, method: str, blocksize: int -) -> ChunksAndContentLength: - """Takes the HTTP request method, body, and blocksize and - transforms them into an iterable of chunks to pass to - socket.sendall() and an optional 'Content-Length' header. - - A 'Content-Length' of 'None' indicates the length of the body - can't be determined so should use 'Transfer-Encoding: chunked' - for framing instead. - """ - - chunks: typing.Iterable[bytes] | None - content_length: int | None - - # No body, we need to make a recommendation on 'Content-Length' - # based on whether that request method is expected to have - # a body or not. - if body is None: - chunks = None - if method.upper() not in _METHODS_NOT_EXPECTING_BODY: - content_length = 0 - else: - content_length = None - - # Bytes or strings become bytes - elif isinstance(body, (str, bytes)): - chunks = (to_bytes(body),) - content_length = len(chunks[0]) - - # File-like object, TODO: use seek() and tell() for length? - elif hasattr(body, "read"): - - def chunk_readable() -> typing.Iterable[bytes]: - encode = isinstance(body, io.TextIOBase) - while True: - datablock = body.read(blocksize) - if not datablock: - break - if encode: - datablock = datablock.encode("utf-8") - yield datablock - - chunks = chunk_readable() - content_length = None - - # Otherwise we need to start checking via duck-typing. - else: - try: - # Check if the body implements the buffer API. - mv = memoryview(body) - except TypeError: - try: - # Check if the body is an iterable - chunks = iter(body) - content_length = None - except TypeError: - raise TypeError( - f"'body' must be a bytes-like object, file-like " - f"object, or iterable. Instead was {body!r}" - ) from None - else: - # Since it implements the buffer API can be passed directly to socket.sendall() - chunks = (body,) - content_length = mv.nbytes - - return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py deleted file mode 100644 index 0f4578696f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/response.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import http.client as httplib -from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect - -from ..exceptions import HeaderParsingError - - -def is_fp_closed(obj: object) -> bool: - """ - Checks whether a given file-like object is closed. - - :param obj: - The file-like object to check. - """ - - try: - # Check `isclosed()` first, in case Python3 doesn't set `closed`. - # GH Issue #928 - return obj.isclosed() # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check via the official file-like-object way. - return obj.closed # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check if the object is a container for another file-like object that - # gets released on exhaustion (e.g. HTTPResponse). - return obj.fp is None # type: ignore[attr-defined] - except AttributeError: - pass - - raise ValueError("Unable to determine whether fp is closed.") - - -def assert_header_parsing(headers: httplib.HTTPMessage) -> None: - """ - Asserts whether all headers have been successfully parsed. - Extracts encountered errors from the result of parsing headers. - - Only works on Python 3. - - :param http.client.HTTPMessage headers: Headers to verify. - - :raises urllib3.exceptions.HeaderParsingError: - If parsing errors are found. - """ - - # This will fail silently if we pass in the wrong kind of parameter. - # To make debugging easier add an explicit check. - if not isinstance(headers, httplib.HTTPMessage): - raise TypeError(f"expected httplib.Message, got {type(headers)}.") - - unparsed_data = None - - # get_payload is actually email.message.Message.get_payload; - # we're only interested in the result if it's not a multipart message - if not headers.is_multipart(): - payload = headers.get_payload() - - if isinstance(payload, (bytes, str)): - unparsed_data = payload - - # httplib is assuming a response body is available - # when parsing headers even when httplib only sends - # header data to parse_headers() This results in - # defects on multipart responses in particular. - # See: https://github.com/urllib3/urllib3/issues/800 - - # So we ignore the following defects: - # - StartBoundaryNotFoundDefect: - # The claimed start boundary was never found. - # - MultipartInvariantViolationDefect: - # A message claimed to be a multipart but no subparts were found. - defects = [ - defect - for defect in headers.defects - if not isinstance( - defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) - ) - ] - - if defects or unparsed_data: - raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) - - -def is_response_to_head(response: httplib.HTTPResponse) -> bool: - """ - Checks whether the request of a response has been a HEAD-request. - - :param http.client.HTTPResponse response: - Response to check if the originating request - used 'HEAD' as a method. - """ - # FIXME: Can we do this somehow without accessing private httplib _method? - method_str = response._method # type: str # type: ignore[attr-defined] - return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py deleted file mode 100644 index 7649898e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/retry.py +++ /dev/null @@ -1,557 +0,0 @@ -from __future__ import annotations - -import email -import logging -import random -import re -import time -import typing -from itertools import takewhile -from types import TracebackType - -from ..exceptions import ( - ConnectTimeoutError, - InvalidHeader, - MaxRetryError, - ProtocolError, - ProxyError, - ReadTimeoutError, - ResponseError, -) -from .util import reraise - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from ..connectionpool import ConnectionPool - from ..response import BaseHTTPResponse - -log = logging.getLogger(__name__) - - -# Data structure for representing the metadata of requests that result in a retry. -class RequestHistory(typing.NamedTuple): - method: str | None - url: str | None - error: Exception | None - status: int | None - redirect_location: str | None - - -class Retry: - """Retry configuration. - - Each retry attempt will create a new Retry object with updated values, so - they can be safely reused. - - Retries can be defined as a default for a pool: - - .. code-block:: python - - retries = Retry(connect=5, read=2, redirect=5) - http = PoolManager(retries=retries) - response = http.request("GET", "https://example.com/") - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=Retry(10)) - - Retries can be disabled by passing ``False``: - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=False) - - Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless - retries are disabled, in which case the causing exception will be raised. - - :param int total: - Total number of retries to allow. Takes precedence over other counts. - - Set to ``None`` to remove this constraint and fall back on other - counts. - - Set to ``0`` to fail on the first retry. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int connect: - How many connection-related errors to retry on. - - These are errors raised before the request is sent to the remote server, - which we assume has not triggered the server to process the request. - - Set to ``0`` to fail on the first retry of this type. - - :param int read: - How many times to retry on read errors. - - These errors are raised after the request was sent to the server, so the - request may have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - :param int redirect: - How many redirects to perform. Limit this to avoid infinite redirect - loops. - - A redirect is a HTTP response with a status code 301, 302, 303, 307 or - 308. - - Set to ``0`` to fail on the first retry of this type. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int status: - How many times to retry on bad status codes. - - These are retries made on responses, where status code matches - ``status_forcelist``. - - Set to ``0`` to fail on the first retry of this type. - - :param int other: - How many times to retry on other errors. - - Other errors are errors that are not connect, read, redirect or status errors. - These errors might be raised after the request was sent to the server, so the - request might have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - If ``total`` is not set, it's a good idea to set this to 0 to account - for unexpected edge cases and avoid infinite retry loops. - - :param Collection allowed_methods: - Set of uppercased HTTP method verbs that we should retry on. - - By default, we only retry on methods which are considered to be - idempotent (multiple requests with the same parameters end with the - same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. - - Set to a ``None`` value to retry on any verb. - - :param Collection status_forcelist: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``allowed_methods`` - and the response status code is in ``status_forcelist``. - - By default, this is disabled with ``None``. - - :param float backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). urllib3 will sleep for:: - - {backoff factor} * (2 ** ({number of previous retries})) - - seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: - - random.uniform(0, {backoff jitter}) - - seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will - sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever - be longer than `backoff_max`. - - By default, backoff is disabled (factor set to 0). - - :param float backoff_max: - The maximum backoff time (in seconds) between retry attempts. - This value caps the computed backoff from `backoff_factor`. - - :param float backoff_jitter: - Random jitter amount (in seconds) added to the computed backoff. - Jitter is sampled uniformly from `0` to `backoff_jitter`. - - :param bool raise_on_redirect: Whether, if the number of redirects is - exhausted, to raise a MaxRetryError, or to return a response with a - response code in the 3xx range. - - :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: - whether we should raise an exception, or return a response, - if status falls in ``status_forcelist`` range and retries have - been exhausted. - - :param tuple history: The history of the request encountered during - each call to :meth:`~Retry.increment`. The list is in the order - the requests occurred. Each list item is of class :class:`RequestHistory`. - - :param bool respect_retry_after_header: - Whether to respect Retry-After header on status codes defined as - :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. - - :param Collection remove_headers_on_redirect: - Sequence of headers to remove from the request when a response - indicating a redirect is returned before firing off the redirected - request. - - :param int retry_after_max: Number of seconds to allow as the maximum for - Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. - Any Retry-After headers larger than this value will be limited to this - value. - """ - - #: Default methods to be used for ``allowed_methods`` - DEFAULT_ALLOWED_METHODS = frozenset( - ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] - ) - - #: Default status codes to be used for ``status_forcelist`` - RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - - #: Default headers to be used for ``remove_headers_on_redirect`` - DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( - ["Cookie", "Authorization", "Proxy-Authorization"] - ) - - #: Default maximum backoff time. - DEFAULT_BACKOFF_MAX = 120 - - # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. - #: Default maximum allowed value for Retry-After headers in seconds - DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 - - # Backward compatibility; assigned outside of the class. - DEFAULT: typing.ClassVar[Retry] - - def __init__( - self, - total: bool | int | None = 10, - connect: int | None = None, - read: int | None = None, - redirect: bool | int | None = None, - status: int | None = None, - other: int | None = None, - allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, - status_forcelist: typing.Collection[int] | None = None, - backoff_factor: float = 0, - backoff_max: float = DEFAULT_BACKOFF_MAX, - raise_on_redirect: bool = True, - raise_on_status: bool = True, - history: tuple[RequestHistory, ...] | None = None, - respect_retry_after_header: bool = True, - remove_headers_on_redirect: typing.Collection[ - str - ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, - backoff_jitter: float = 0.0, - retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, - ) -> None: - self.total = total - self.connect = connect - self.read = read - self.status = status - self.other = other - - if redirect is False or total is False: - redirect = 0 - raise_on_redirect = False - - self.redirect = redirect - self.status_forcelist = status_forcelist or set() - self.allowed_methods = allowed_methods - self.backoff_factor = backoff_factor - self.backoff_max = backoff_max - self.retry_after_max = retry_after_max - self.raise_on_redirect = raise_on_redirect - self.raise_on_status = raise_on_status - self.history = history or () - self.respect_retry_after_header = respect_retry_after_header - self.remove_headers_on_redirect = frozenset( - h.lower() for h in remove_headers_on_redirect - ) - self.backoff_jitter = backoff_jitter - - def new(self, **kw: typing.Any) -> Self: - params = dict( - total=self.total, - connect=self.connect, - read=self.read, - redirect=self.redirect, - status=self.status, - other=self.other, - allowed_methods=self.allowed_methods, - status_forcelist=self.status_forcelist, - backoff_factor=self.backoff_factor, - backoff_max=self.backoff_max, - retry_after_max=self.retry_after_max, - raise_on_redirect=self.raise_on_redirect, - raise_on_status=self.raise_on_status, - history=self.history, - remove_headers_on_redirect=self.remove_headers_on_redirect, - respect_retry_after_header=self.respect_retry_after_header, - backoff_jitter=self.backoff_jitter, - ) - - params.update(kw) - return type(self)(**params) # type: ignore[arg-type] - - @classmethod - def from_int( - cls, - retries: Retry | bool | int | None, - redirect: bool | int | None = True, - default: Retry | bool | int | None = None, - ) -> Retry: - """Backwards-compatibility for the old retries format.""" - if retries is None: - retries = default if default is not None else cls.DEFAULT - - if isinstance(retries, Retry): - return retries - - redirect = bool(redirect) and None - new_retries = cls(retries, redirect=redirect) - log.debug("Converted retries value: %r -> %r", retries, new_retries) - return new_retries - - def get_backoff_time(self) -> float: - """Formula for computing the current backoff - - :rtype: float - """ - # We want to consider only the last consecutive errors sequence (Ignore redirects). - consecutive_errors_len = len( - list( - takewhile(lambda x: x.redirect_location is None, reversed(self.history)) - ) - ) - if consecutive_errors_len <= 1: - return 0 - - backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) - if self.backoff_jitter != 0.0: - backoff_value += random.random() * self.backoff_jitter - return float(max(0, min(self.backoff_max, backoff_value))) - - def parse_retry_after(self, retry_after: str) -> float: - seconds: float - # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 - if re.match(r"^\s*[0-9]+\s*$", retry_after): - seconds = int(retry_after) - else: - retry_date_tuple = email.utils.parsedate_tz(retry_after) - if retry_date_tuple is None: - raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") - - retry_date = email.utils.mktime_tz(retry_date_tuple) - seconds = retry_date - time.time() - - seconds = max(seconds, 0) - - # Check the seconds do not exceed the specified maximum - if seconds > self.retry_after_max: - seconds = self.retry_after_max - - return seconds - - def get_retry_after(self, response: BaseHTTPResponse) -> float | None: - """Get the value of Retry-After in seconds.""" - - retry_after = response.headers.get("Retry-After") - - if retry_after is None: - return None - - return self.parse_retry_after(retry_after) - - def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: - retry_after = self.get_retry_after(response) - if retry_after: - time.sleep(retry_after) - return True - - return False - - def _sleep_backoff(self) -> None: - backoff = self.get_backoff_time() - if backoff <= 0: - return - time.sleep(backoff) - - def sleep(self, response: BaseHTTPResponse | None = None) -> None: - """Sleep between retry attempts. - - This method will respect a server's ``Retry-After`` response header - and sleep the duration of the time requested. If that is not present, it - will use an exponential backoff. By default, the backoff factor is 0 and - this method will return immediately. - """ - - if self.respect_retry_after_header and response: - slept = self.sleep_for_retry(response) - if slept: - return - - self._sleep_backoff() - - def _is_connection_error(self, err: Exception) -> bool: - """Errors when we're fairly sure that the server did not receive the - request, so it should be safe to retry. - """ - if isinstance(err, ProxyError): - err = err.original_error - return isinstance(err, ConnectTimeoutError) - - def _is_read_error(self, err: Exception) -> bool: - """Errors that occur after the request has been started, so we should - assume that the server began processing it. - """ - return isinstance(err, (ReadTimeoutError, ProtocolError)) - - def _is_method_retryable(self, method: str) -> bool: - """Checks if a given HTTP method should be retried upon, depending if - it is included in the allowed_methods - """ - if self.allowed_methods and method.upper() not in self.allowed_methods: - return False - return True - - def is_retry( - self, method: str, status_code: int, has_retry_after: bool = False - ) -> bool: - """Is this method/status code retryable? (Based on allowlists and control - variables such as the number of total retries to allow, whether to - respect the Retry-After header, whether this header is present, and - whether the returned status code is on the list of status codes to - be retried upon on the presence of the aforementioned header) - """ - if not self._is_method_retryable(method): - return False - - if self.status_forcelist and status_code in self.status_forcelist: - return True - - return bool( - self.total - and self.respect_retry_after_header - and has_retry_after - and (status_code in self.RETRY_AFTER_STATUS_CODES) - ) - - def is_exhausted(self) -> bool: - """Are we out of retries?""" - retry_counts = [ - x - for x in ( - self.total, - self.connect, - self.read, - self.redirect, - self.status, - self.other, - ) - if x - ] - if not retry_counts: - return False - - return min(retry_counts) < 0 - - def increment( - self, - method: str | None = None, - url: str | None = None, - response: BaseHTTPResponse | None = None, - error: Exception | None = None, - _pool: ConnectionPool | None = None, - _stacktrace: TracebackType | None = None, - ) -> Self: - """Return a new Retry object with incremented retry counters. - - :param response: A response object, or None, if the server did not - return a response. - :type response: :class:`~urllib3.response.BaseHTTPResponse` - :param Exception error: An error encountered during the request, or - None if the response was received successfully. - - :return: A new ``Retry`` object. - """ - if self.total is False and error: - # Disabled, indicate to re-raise the error. - raise reraise(type(error), error, _stacktrace) - - total = self.total - if total is not None: - total -= 1 - - connect = self.connect - read = self.read - redirect = self.redirect - status_count = self.status - other = self.other - cause = "unknown" - status = None - redirect_location = None - - if error and self._is_connection_error(error): - # Connect retry? - if connect is False: - raise reraise(type(error), error, _stacktrace) - elif connect is not None: - connect -= 1 - - elif error and self._is_read_error(error): - # Read retry? - if read is False or method is None or not self._is_method_retryable(method): - raise reraise(type(error), error, _stacktrace) - elif read is not None: - read -= 1 - - elif error: - # Other retry? - if other is not None: - other -= 1 - - elif response and response.get_redirect_location(): - # Redirect retry? - if redirect is not None: - redirect -= 1 - cause = "too many redirects" - response_redirect_location = response.get_redirect_location() - if response_redirect_location: - redirect_location = response_redirect_location - status = response.status - - else: - # Incrementing because of a server error like a 500 in - # status_forcelist and the given method is in the allowed_methods - cause = ResponseError.GENERIC_ERROR - if response and response.status: - if status_count is not None: - status_count -= 1 - cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) - status = response.status - - history = self.history + ( - RequestHistory(method, url, error, status, redirect_location), - ) - - new_retry = self.new( - total=total, - connect=connect, - read=read, - redirect=redirect, - status=status_count, - other=other, - history=history, - ) - - if new_retry.is_exhausted(): - reason = error or ResponseError(cause) - raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] - - log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) - - return new_retry - - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(total={self.total}, connect={self.connect}, " - f"read={self.read}, redirect={self.redirect}, status={self.status})" - ) - - -# For backwards compatibility (equivalent to pre-v1.9): -Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py deleted file mode 100644 index e66549a76c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_.py +++ /dev/null @@ -1,477 +0,0 @@ -from __future__ import annotations - -import hashlib -import hmac -import os -import socket -import sys -import typing -import warnings -from binascii import unhexlify - -from ..exceptions import ProxySchemeUnsupported, SSLError -from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE - -SSLContext = None -SSLTransport = None -HAS_NEVER_CHECK_COMMON_NAME = False -IS_PYOPENSSL = False -ALPN_PROTOCOLS = ["http/1.1"] - -_TYPE_VERSION_INFO = tuple[int, int, int, str, int] - -# Maps the length of a digest to a possible hash function producing this digest -HASHFUNC_MAP = { - length: getattr(hashlib, algorithm, None) - for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) -} - - -def _is_has_never_check_common_name_reliable( - openssl_version: str, -) -> bool: - # As of May 2023, all released versions of LibreSSL fail to reject certificates with - # only common names, see https://github.com/urllib3/urllib3/pull/3024 - is_openssl = openssl_version.startswith("OpenSSL ") - - return is_openssl - - -if typing.TYPE_CHECKING: - from ssl import VerifyMode - from typing import TypedDict - - from .ssltransport import SSLTransport as SSLTransportType - - class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): - subjectAltName: tuple[tuple[str, str], ...] - subject: tuple[tuple[tuple[str, str], ...], ...] - serialNumber: str - - -# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' -_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} - -try: # Do we have ssl at all? - import ssl - from ssl import ( # type: ignore[assignment] - CERT_REQUIRED, - HAS_NEVER_CHECK_COMMON_NAME, - OP_NO_COMPRESSION, - OP_NO_TICKET, - OPENSSL_VERSION, - PROTOCOL_TLS, - PROTOCOL_TLS_CLIENT, - VERIFY_X509_PARTIAL_CHAIN, - VERIFY_X509_STRICT, - OP_NO_SSLv2, - OP_NO_SSLv3, - SSLContext, - TLSVersion, - ) - - PROTOCOL_SSLv23 = PROTOCOL_TLS - - # Setting SSLContext.hostname_checks_common_name = False didn't work with - # LibreSSL, check details in the used function. - if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( - OPENSSL_VERSION, - ): # Defensive: - HAS_NEVER_CHECK_COMMON_NAME = False - - # Need to be careful here in case old TLS versions get - # removed in future 'ssl' module implementations. - for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): - try: - _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( - TLSVersion, attr - ) - except AttributeError: # Defensive: - continue - - from .ssltransport import SSLTransport # type: ignore[assignment] -except ImportError: - OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] - OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] - OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] - OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] - PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] - PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] - VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] - VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] - - -_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] - - -def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: - """ - Checks if given fingerprint matches the supplied certificate. - - :param cert: - Certificate as bytes object. - :param fingerprint: - Fingerprint as string of hexdigits, can be interspersed by colons. - """ - - if cert is None: - raise SSLError("No certificate for the peer.") - - fingerprint = fingerprint.replace(":", "").lower() - digest_length = len(fingerprint) - if digest_length not in HASHFUNC_MAP: - raise SSLError(f"Fingerprint of invalid length: {fingerprint}") - hashfunc = HASHFUNC_MAP.get(digest_length) - if hashfunc is None: - raise SSLError( - f"Hash function implementation unavailable for fingerprint length: {digest_length}" - ) - - # We need encode() here for py32; works on py2 and p33. - fingerprint_bytes = unhexlify(fingerprint.encode()) - - cert_digest = hashfunc(cert).digest() - - if not hmac.compare_digest(cert_digest, fingerprint_bytes): - raise SSLError( - f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' - ) - - -def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: - """ - Resolves the argument to a numeric constant, which can be passed to - the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_REQUIRED`. - If given a string it is assumed to be the name of the constant in the - :mod:`ssl` module or its abbreviation. - (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. - If it's neither `None` nor a string we assume it is already the numeric - constant which can directly be passed to wrap_socket. - """ - if candidate is None: - return CERT_REQUIRED - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "CERT_" + candidate) - return res # type: ignore[no-any-return] - - return candidate # type: ignore[return-value] - - -def resolve_ssl_version(candidate: None | int | str) -> int: - """ - like resolve_cert_reqs - """ - if candidate is None: - return PROTOCOL_TLS - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "PROTOCOL_" + candidate) - return typing.cast(int, res) - - return candidate - - -def create_urllib3_context( - ssl_version: int | None = None, - cert_reqs: int | None = None, - options: int | None = None, - ciphers: str | None = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - verify_flags: int | None = None, -) -> ssl.SSLContext: - """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. - - :param ssl_version: - The desired protocol version to use. This will default to - PROTOCOL_SSLv23 which will negotiate the highest protocol that both - the server and your installation of OpenSSL support. - - This parameter is deprecated instead use 'ssl_minimum_version'. - :param ssl_minimum_version: - The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - :param ssl_maximum_version: - The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the - default value. - :param cert_reqs: - Whether to require the certificate verification. This defaults to - ``ssl.CERT_REQUIRED``. - :param options: - Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, - ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. - :param ciphers: - Which cipher suites to allow the server to select. Defaults to either system configured - ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. - :param verify_flags: - The flags for certificate verification operations. These default to - ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. - :returns: - Constructed SSLContext object with specified options - :rtype: SSLContext - """ - if SSLContext is None: - raise TypeError("Can't create an SSLContext object without an ssl module") - - # This means 'ssl_version' was specified as an exact value. - if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): - # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' - # to avoid conflicts. - if ssl_minimum_version is not None or ssl_maximum_version is not None: - raise ValueError( - "Can't specify both 'ssl_version' and either " - "'ssl_minimum_version' or 'ssl_maximum_version'" - ) - - # 'ssl_version' is deprecated and will be removed in the future. - else: - # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. - ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MINIMUM_SUPPORTED - ) - ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MAXIMUM_SUPPORTED - ) - - # This warning message is pushing users to use 'ssl_minimum_version' - # instead of both min/max. Best practice is to only set the minimum version and - # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' - warnings.warn( - "'ssl_version' option is deprecated and will be " - "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", - category=FutureWarning, - stacklevel=2, - ) - - context = SSLContext(PROTOCOL_TLS_CLIENT) - if ssl_minimum_version is not None: - context.minimum_version = ssl_minimum_version - else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here - context.minimum_version = TLSVersion.TLSv1_2 - - if ssl_maximum_version is not None: - context.maximum_version = ssl_maximum_version - - # Unless we're given ciphers defer to either system ciphers in - # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. - if ciphers: - context.set_ciphers(ciphers) - - # Setting the default here, as we may have no ssl module on import - cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if options is None: - options = 0 - # SSLv2 is easily broken and is considered harmful and dangerous - options |= OP_NO_SSLv2 - # SSLv3 has several problems and is now dangerous - options |= OP_NO_SSLv3 - # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ - # (issue #309) - options |= OP_NO_COMPRESSION - # TLSv1.2 only. Unless set explicitly, do not request tickets. - # This may save some bandwidth on wire, and although the ticket is encrypted, - # there is a risk associated with it being on wire, - # if the server is not rotating its ticketing keys properly. - options |= OP_NO_TICKET - - context.options |= options - - if verify_flags is None: - verify_flags = 0 - # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN - # and VERIFY_X509_STRICT so we do the same - if sys.version_info >= (3, 13): - verify_flags |= VERIFY_X509_PARTIAL_CHAIN - verify_flags |= VERIFY_X509_STRICT - - context.verify_flags |= verify_flags - - # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is - # necessary for conditional client cert authentication with TLS 1.3. - # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using - # an SSLContext created by pyOpenSSL. - if getattr(context, "post_handshake_auth", None) is not None: - context.post_handshake_auth = True - - # The order of the below lines setting verify_mode and check_hostname - # matter due to safe-guards SSLContext has to prevent an SSLContext with - # check_hostname=True, verify_mode=NONE/OPTIONAL. - # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own - # 'ssl.match_hostname()' implementation. - if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: - context.verify_mode = cert_reqs - context.check_hostname = True - else: - context.check_hostname = False - context.verify_mode = cert_reqs - - context.hostname_checks_common_name = False - - if "SSLKEYLOGFILE" in os.environ: - sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) - else: - sslkeylogfile = None - if sslkeylogfile: - context.keylog_filename = sslkeylogfile - - return context - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: typing.Literal[False] = ..., -) -> ssl.SSLSocket: ... - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: bool = ..., -) -> ssl.SSLSocket | SSLTransportType: ... - - -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = None, - certfile: str | None = None, - cert_reqs: int | None = None, - ca_certs: str | None = None, - server_hostname: str | None = None, - ssl_version: int | None = None, - ciphers: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_cert_dir: str | None = None, - key_password: str | None = None, - ca_cert_data: None | str | bytes = None, - tls_in_tls: bool = False, -) -> ssl.SSLSocket | SSLTransportType: - """ - All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and - ca_cert_dir have the same meaning as they do when using - :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, - :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. - - :param server_hostname: - When SNI is supported, the expected hostname of the certificate - :param ssl_context: - A pre-made :class:`SSLContext` object. If none is provided, one will - be created using :func:`create_urllib3_context`. - :param ciphers: - A string of ciphers we wish the client to support. - :param ca_cert_dir: - A directory containing CA certificates in multiple separate files, as - supported by OpenSSL's -CApath flag or the capath argument to - SSLContext.load_verify_locations(). - :param key_password: - Optional password if the keyfile is encrypted. - :param ca_cert_data: - Optional string containing CA certificates in PEM format suitable for - passing as the cadata parameter to SSLContext.load_verify_locations() - :param tls_in_tls: - Use SSLTransport to wrap the existing socket. - """ - context = ssl_context - if context is None: - # Note: This branch of code and all the variables in it are only used in tests. - # We should consider deprecating and removing this code. - context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) - - if ca_certs or ca_cert_dir or ca_cert_data: - try: - context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) - except OSError as e: - raise SSLError(e) from e - - elif ssl_context is None and hasattr(context, "load_default_certs"): - # try to load OS default certs; works well on Windows. - context.load_default_certs() - - # Attempt to detect if we get the goofy behavior of the - # keyfile being encrypted and OpenSSL asking for the - # passphrase via the terminal and instead error out. - if keyfile and key_password is None and _is_key_file_encrypted(keyfile): - raise SSLError("Client private key is encrypted, password is required") - - if certfile: - if key_password is None: - context.load_cert_chain(certfile, keyfile) - else: - context.load_cert_chain(certfile, keyfile, key_password) - - context.set_alpn_protocols(ALPN_PROTOCOLS) - - ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) - return ssl_sock - - -def is_ipaddress(hostname: str | bytes) -> bool: - """Detects whether the hostname given is an IPv4 or IPv6 address. - Also detects IPv6 addresses with Zone IDs. - - :param str hostname: Hostname to examine. - :return: True if the hostname is an IP address, False otherwise. - """ - if isinstance(hostname, bytes): - # IDN A-label bytes are ASCII compatible. - hostname = hostname.decode("ascii") - return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) - - -def _is_key_file_encrypted(key_file: str) -> bool: - """Detects if a key file is encrypted or not.""" - with open(key_file) as f: - for line in f: - # Look for Proc-Type: 4,ENCRYPTED - if "ENCRYPTED" in line: - return True - - return False - - -def _ssl_wrap_socket_impl( - sock: socket.socket, - ssl_context: ssl.SSLContext, - tls_in_tls: bool, - server_hostname: str | None = None, -) -> ssl.SSLSocket | SSLTransportType: - if tls_in_tls: - if not SSLTransport: - # Import error, ssl is not available. - raise ProxySchemeUnsupported( - "TLS in TLS requires support for the 'ssl' module" - ) - - SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) - return SSLTransport(sock, ssl_context, server_hostname) - - return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py deleted file mode 100644 index 94994f25ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssl_match_hostname.py +++ /dev/null @@ -1,153 +0,0 @@ -"""The match_hostname() function from Python 3.5, essential when using SSL.""" - -# Note: This file is under the PSF license as the code comes from the python -# stdlib. http://docs.python.org/3/license.html -# It is modified to remove commonName support. - -from __future__ import annotations - -import ipaddress -import re -import typing -from ipaddress import IPv4Address, IPv6Address - -if typing.TYPE_CHECKING: - from .ssl_ import _TYPE_PEER_CERT_RET_DICT - -__version__ = "3.5.0.1" - - -class CertificateError(ValueError): - pass - - -def _dnsname_match( - dn: typing.Any, hostname: str, max_wildcards: int = 1 -) -> typing.Match[str] | None | bool: - """Matching according to RFC 6125, section 6.4.3 - - http://tools.ietf.org/html/rfc6125#section-6.4.3 - """ - pats = [] - if not dn: - return False - - # Ported from python3-syntax: - # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r".") - leftmost = parts[0] - remainder = parts[1:] - - wildcards = leftmost.count("*") - if wildcards > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survey of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn) - ) - - # speed up common case w/o wildcards - if not wildcards: - return bool(dn.lower() == hostname.lower()) - - # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. - if leftmost == "*": - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append("[^.]+") - elif leftmost.startswith("xn--") or hostname.startswith("xn--"): - # RFC 6125, section 6.4.3, subitem 3. - # The client SHOULD NOT attempt to match a presented identifier - # where the wildcard character is embedded within an A-label or - # U-label of an internationalized domain name. - pats.append(re.escape(leftmost)) - else: - # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) - - # add the remaining fragments, ignore any wildcards - for frag in remainder: - pats.append(re.escape(frag)) - - pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) - return pat.match(hostname) - - -def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: - """Exact matching of IP addresses. - - RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded - bytes of the IP address. An IP version 4 address is 4 octets, and an IP - version 6 address is 16 octets. [...] A reference identity of type IP-ID - matches if the address is identical to an iPAddress value of the - subjectAltName extension of the certificate." - """ - # OpenSSL may add a trailing newline to a subjectAltName's IP address - # Divergence from upstream: ipaddress can't handle byte str - ip = ipaddress.ip_address(ipname.rstrip()) - return bool(ip.packed == host_ip.packed) - - -def match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError( - "empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED" - ) - - try: - host_ip = ipaddress.ip_address(hostname) - except ValueError: - # Not an IP address (common case) - host_ip = None - dnsnames = [] - san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) - key: str - value: str - for key, value in san: - if key == "DNS": - if host_ip is None and _dnsname_match(value, hostname): - return - dnsnames.append(value) - elif key == "IP Address": - if host_ip is not None and _ipaddress_match(value, host_ip): - return - dnsnames.append(value) - - # We only check 'commonName' if it's enabled and we're not verifying - # an IP address. IP addresses aren't valid within 'commonName'. - if hostname_checks_common_name and host_ip is None and not dnsnames: - for sub in cert.get("subject", ()): - for key, value in sub: - if key == "commonName": - if _dnsname_match(value, hostname): - return - dnsnames.append( - value - ) # Defensive: for older PyPy and OpenSSL versions - - if len(dnsnames) > 1: - raise CertificateError( - "hostname %r " - "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) - ) - elif len(dnsnames) == 1: - raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") - else: - raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py deleted file mode 100644 index 6d59bc3bce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/ssltransport.py +++ /dev/null @@ -1,271 +0,0 @@ -from __future__ import annotations - -import io -import socket -import ssl -import typing - -from ..exceptions import ProxySchemeUnsupported - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT - - -_WriteBuffer = typing.Union[bytearray, memoryview] -_ReturnValue = typing.TypeVar("_ReturnValue") - -SSL_BLOCKSIZE = 16384 - - -class SSLTransport: - """ - The SSLTransport wraps an existing socket and establishes an SSL connection. - - Contrary to Python's implementation of SSLSocket, it allows you to chain - multiple TLS connections together. It's particularly useful if you need to - implement TLS within TLS. - - The class supports most of the socket API operations. - """ - - @staticmethod - def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: - """ - Raises a ProxySchemeUnsupported if the provided ssl_context can't be used - for TLS in TLS. - - The only requirement is that the ssl_context provides the 'wrap_bio' - methods. - """ - - if not hasattr(ssl_context, "wrap_bio"): - raise ProxySchemeUnsupported( - "TLS in TLS requires SSLContext.wrap_bio() which isn't " - "available on non-native SSLContext" - ) - - def __init__( - self, - socket: socket.socket, - ssl_context: ssl.SSLContext, - server_hostname: str | None = None, - suppress_ragged_eofs: bool = True, - ) -> None: - """ - Create an SSLTransport around socket using the provided ssl_context. - """ - self.incoming = ssl.MemoryBIO() - self.outgoing = ssl.MemoryBIO() - - self.suppress_ragged_eofs = suppress_ragged_eofs - self.socket = socket - - self.sslobj = ssl_context.wrap_bio( - self.incoming, self.outgoing, server_hostname=server_hostname - ) - - # Perform initial handshake. - self._ssl_io_loop(self.sslobj.do_handshake) - - def __enter__(self) -> Self: - return self - - def __exit__(self, *_: typing.Any) -> None: - self.close() - - def fileno(self) -> int: - return self.socket.fileno() - - def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: - return self._wrap_ssl_read(len, buffer) - - def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv") - return self._wrap_ssl_read(buflen) - - def recv_into( - self, - buffer: _WriteBuffer, - nbytes: int | None = None, - flags: int = 0, - ) -> None | int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv_into") - if nbytes is None: - nbytes = len(buffer) - return self.read(nbytes, buffer) - - def sendall(self, data: bytes, flags: int = 0) -> None: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to sendall") - count = 0 - with memoryview(data) as view, view.cast("B") as byte_view: - amount = len(byte_view) - while count < amount: - v = self.send(byte_view[count:]) - count += v - - def send(self, data: bytes, flags: int = 0) -> int: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to send") - return self._ssl_io_loop(self.sslobj.write, data) - - def makefile( - self, - mode: str, - buffering: int | None = None, - *, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: - """ - Python's httpclient uses makefile and buffered io when reading HTTP - messages and we need to support it. - - This is unfortunately a copy and paste of socket.py makefile with small - changes to point to the socket directly. - """ - if not set(mode) <= {"r", "w", "b"}: - raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") - - writing = "w" in mode - reading = "r" in mode or not writing - assert reading or writing - binary = "b" in mode - rawmode = "" - if reading: - rawmode += "r" - if writing: - rawmode += "w" - raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] - self.socket._io_refs += 1 # type: ignore[attr-defined] - if buffering is None: - buffering = -1 - if buffering < 0: - buffering = io.DEFAULT_BUFFER_SIZE - if buffering == 0: - if not binary: - raise ValueError("unbuffered streams must be binary") - return raw - buffer: typing.BinaryIO - if reading and writing: - buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] - elif reading: - buffer = io.BufferedReader(raw, buffering) - else: - assert writing - buffer = io.BufferedWriter(raw, buffering) - if binary: - return buffer - text = io.TextIOWrapper(buffer, encoding, errors, newline) - text.mode = mode # type: ignore[misc] - return text - - def unwrap(self) -> None: - self._ssl_io_loop(self.sslobj.unwrap) - - def close(self) -> None: - self.socket.close() - - @typing.overload - def getpeercert( - self, binary_form: typing.Literal[False] = ... - ) -> _TYPE_PEER_CERT_RET_DICT | None: ... - - @typing.overload - def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... - - def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: - return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] - - def version(self) -> str | None: - return self.sslobj.version() - - def cipher(self) -> tuple[str, str, int] | None: - return self.sslobj.cipher() - - def selected_alpn_protocol(self) -> str | None: - return self.sslobj.selected_alpn_protocol() - - def shared_ciphers(self) -> list[tuple[str, str, int]] | None: - return self.sslobj.shared_ciphers() - - def compression(self) -> str | None: - return self.sslobj.compression() - - def settimeout(self, value: float | None) -> None: - self.socket.settimeout(value) - - def gettimeout(self) -> float | None: - return self.socket.gettimeout() - - def _decref_socketios(self) -> None: - self.socket._decref_socketios() # type: ignore[attr-defined] - - def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: - try: - return self._ssl_io_loop(self.sslobj.read, len, buffer) - except ssl.SSLError as e: - if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: - return 0 # eof, return 0. - else: - raise - - # func is sslobj.do_handshake or sslobj.unwrap - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... - - # func is sslobj.write, arg1 is data - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... - - # func is sslobj.read, arg1 is len, arg2 is buffer - @typing.overload - def _ssl_io_loop( - self, - func: typing.Callable[[int, bytearray | None], bytes], - arg1: int, - arg2: bytearray | None, - ) -> bytes: ... - - def _ssl_io_loop( - self, - func: typing.Callable[..., _ReturnValue], - arg1: None | bytes | int = None, - arg2: bytearray | None = None, - ) -> _ReturnValue: - """Performs an I/O loop between incoming/outgoing and the socket.""" - should_loop = True - ret = None - - while should_loop: - errno = None - try: - if arg1 is None and arg2 is None: - ret = func() - elif arg2 is None: - ret = func(arg1) - else: - ret = func(arg1, arg2) - except ssl.SSLError as e: - if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): - # WANT_READ, and WANT_WRITE are expected, others are not. - raise e - errno = e.errno - - buf = self.outgoing.read() - self.socket.sendall(buf) - - if errno is None: - should_loop = False - elif errno == ssl.SSL_ERROR_WANT_READ: - buf = self.socket.recv(SSL_BLOCKSIZE) - if buf: - self.incoming.write(buf) - else: - self.incoming.write_eof() - return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py deleted file mode 100644 index 4bb1be11d9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/timeout.py +++ /dev/null @@ -1,275 +0,0 @@ -from __future__ import annotations - -import time -import typing -from enum import Enum -from socket import getdefaulttimeout - -from ..exceptions import TimeoutStateError - -if typing.TYPE_CHECKING: - from typing import Final - - -class _TYPE_DEFAULT(Enum): - # This value should never be passed to socket.settimeout() so for safety we use a -1. - # socket.settimout() raises a ValueError for negative values. - token = -1 - - -_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token - -_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] - - -class Timeout: - """Timeout configuration. - - Timeouts can be defined as a default for a pool: - - .. code-block:: python - - import urllib3 - - timeout = urllib3.util.Timeout(connect=2.0, read=7.0) - - http = urllib3.PoolManager(timeout=timeout) - - resp = http.request("GET", "https://example.com/") - - print(resp.status) - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", timeout=Timeout(10)) - - Timeouts can be disabled by setting all the parameters to ``None``: - - .. code-block:: python - - no_timeout = Timeout(connect=None, read=None) - response = http.request("GET", "https://example.com/", timeout=no_timeout) - - - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: int, float, or None - - :param connect: - The maximum amount of time (in seconds) to wait for a connection - attempt to a server to succeed. Omitting the parameter will default the - connect timeout to the system default, probably `the global default - timeout in socket.py - `_. - None will set an infinite timeout for connection attempts. - - :type connect: int, float, or None - - :param read: - The maximum amount of time (in seconds) to wait between consecutive - read operations for a response from the server. Omitting the parameter - will default the read timeout to the system default, probably `the - global default timeout in socket.py - `_. - None will set an infinite timeout. - - :type read: int, float, or None - - .. note:: - - Many factors can affect the total amount of time for urllib3 to return - an HTTP response. - - For example, Python's DNS resolver does not obey the timeout specified - on the socket. Other factors that can affect total request time include - high CPU load, high swap, the program running at a low priority level, - or other behaviors. - - In addition, the read and total timeouts only measure the time between - read operations on the socket connecting the client and the server, - not the total amount of time for the request to return a complete - response. For most requests, the timeout is raised because the server - has not sent the first byte in the specified time. This is not always - the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not trigger, even though the request will take - several minutes to complete. - """ - - #: A sentinel object representing the default timeout value - DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT - - def __init__( - self, - total: _TYPE_TIMEOUT = None, - connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - ) -> None: - self._connect = self._validate_timeout(connect, "connect") - self._read = self._validate_timeout(read, "read") - self.total = self._validate_timeout(total, "total") - self._start_connect: float | None = None - - def __repr__(self) -> str: - return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" - - # __str__ provided for backwards compatibility - __str__ = __repr__ - - @staticmethod - def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: - return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout - - @classmethod - def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: - """Check that a timeout attribute is valid. - - :param value: The timeout value to validate - :param name: The name of the timeout attribute to validate. This is - used to specify in error messages. - :return: The validated and casted version of the given value. - :raises ValueError: If it is a numeric value less than or equal to - zero, or the type is not an integer, float, or None. - """ - if value is None or value is _DEFAULT_TIMEOUT: - return value - - if isinstance(value, bool): - raise ValueError( - "Timeout cannot be a boolean value. It must " - "be an int, float or None." - ) - try: - float(value) - except (TypeError, ValueError): - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - try: - if value <= 0: - raise ValueError( - "Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than or equal to 0." % (name, value) - ) - except TypeError: - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - return value - - @classmethod - def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: - """Create a new Timeout from a legacy timeout value. - - The timeout value used by httplib.py sets the same timeout on the - connect(), and recv() socket requests. This creates a :class:`Timeout` - object that sets the individual timeouts to the ``timeout`` value - passed to this function. - - :param timeout: The legacy timeout value. - :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None - :return: Timeout object - :rtype: :class:`Timeout` - """ - return Timeout(read=timeout, connect=timeout) - - def clone(self) -> Timeout: - """Create a copy of the timeout object - - Timeout properties are stored per-pool but each request needs a fresh - Timeout object to ensure each one has its own start/stop configured. - - :return: a copy of the timeout object - :rtype: :class:`Timeout` - """ - # We can't use copy.deepcopy because that will also create a new object - # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to - # detect the user default. - return Timeout(connect=self._connect, read=self._read, total=self.total) - - def start_connect(self) -> float: - """Start the timeout clock, used during a connect() attempt - - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to start a timer that has been started already. - """ - if self._start_connect is not None: - raise TimeoutStateError("Timeout timer has already been started.") - self._start_connect = time.monotonic() - return self._start_connect - - def get_connect_duration(self) -> float: - """Gets the time elapsed since the call to :meth:`start_connect`. - - :return: Elapsed time in seconds. - :rtype: float - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to get duration for a timer that hasn't been started. - """ - if self._start_connect is None: - raise TimeoutStateError( - "Can't get connect duration for timer that has not started." - ) - return time.monotonic() - self._start_connect - - @property - def connect_timeout(self) -> _TYPE_TIMEOUT: - """Get the value to use when setting a connection timeout. - - This will be a positive float or integer, the value None - (never timeout), or the default system timeout. - - :return: Connect timeout. - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - """ - if self.total is None: - return self._connect - - if self._connect is None or self._connect is _DEFAULT_TIMEOUT: - return self.total - - return min(self._connect, self.total) # type: ignore[type-var] - - @property - def read_timeout(self) -> float | None: - """Get the value for the read timeout. - - This assumes some time has elapsed in the connection timeout and - computes the read timeout appropriately. - - If self.total is set, the read timeout is dependent on the amount of - time taken by the connect timeout. If the connection time has not been - established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be - raised. - - :return: Value to use for the read timeout. - :rtype: int, float or None - :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` - has not yet been called on this object. - """ - if ( - self.total is not None - and self.total is not _DEFAULT_TIMEOUT - and self._read is not None - and self._read is not _DEFAULT_TIMEOUT - ): - # In case the connect timeout has not yet been established. - if self._start_connect is None: - return self._read - return max(0, min(self.total - self.get_connect_duration(), self._read)) - elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: - return max(0, self.total - self.get_connect_duration()) - else: - return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py deleted file mode 100644 index db057f17be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/url.py +++ /dev/null @@ -1,469 +0,0 @@ -from __future__ import annotations - -import re -import typing - -from ..exceptions import LocationParseError -from .util import to_str - -# We only want to normalize urls with an HTTP(S) scheme. -# urllib3 infers URLs without a scheme (None) to be http. -_NORMALIZABLE_SCHEMES = ("http", "https", None) - -# Almost all of these patterns were derived from the -# 'rfc3986' module: https://github.com/python-hyper/rfc3986 -_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") -_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") -_URI_RE = re.compile( - r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" - r"(?://([^\\/?#]*))?" - r"([^?#]*)" - r"(?:\?([^#]*))?" - r"(?:#(.*))?$", - re.UNICODE | re.DOTALL, -) - -_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" -_HEX_PAT = "[0-9A-Fa-f]{1,4}" -_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) -_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} -_variations = [ - # 6( h16 ":" ) ls32 - "(?:%(hex)s:){6}%(ls32)s", - # "::" 5( h16 ":" ) ls32 - "::(?:%(hex)s:){5}%(ls32)s", - # [ h16 ] "::" 4( h16 ":" ) ls32 - "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", - # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 - "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", - # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 - "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", - # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 - "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", - # [ *4( h16 ":" ) h16 ] "::" ls32 - "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", - # [ *5( h16 ":" ) h16 ] "::" h16 - "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", - # [ *6( h16 ":" ) h16 ] "::" - "(?:(?:%(hex)s:){0,6}%(hex)s)?::", -] - -_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" -_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" -_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" -_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" -_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" -_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") - -_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") -_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") -_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") -_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") -_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") - -_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( - _REG_NAME_PAT, - _IPV4_PAT, - _IPV6_ADDRZ_PAT, -) -_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) - -_UNRESERVED_CHARS = set( - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" -) -_SUB_DELIM_CHARS = set("!$&'()*+,;=") -_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} -_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} -_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} - - -class Url( - typing.NamedTuple( - "Url", - [ - ("scheme", typing.Optional[str]), - ("auth", typing.Optional[str]), - ("host", typing.Optional[str]), - ("port", typing.Optional[int]), - ("path", typing.Optional[str]), - ("query", typing.Optional[str]), - ("fragment", typing.Optional[str]), - ], - ) -): - """ - Data structure for representing an HTTP URL. Used as a return value for - :func:`parse_url`. Both the scheme and host are normalized as they are - both case-insensitive according to RFC 3986. - """ - - def __new__( # type: ignore[no-untyped-def] - cls, - scheme: str | None = None, - auth: str | None = None, - host: str | None = None, - port: int | None = None, - path: str | None = None, - query: str | None = None, - fragment: str | None = None, - ): - if path and not path.startswith("/"): - path = "/" + path - if scheme is not None: - scheme = scheme.lower() - return super().__new__(cls, scheme, auth, host, port, path, query, fragment) - - @property - def hostname(self) -> str | None: - """For backwards-compatibility with urlparse. We're nice like that.""" - return self.host - - @property - def request_uri(self) -> str: - """Absolute path including the query string.""" - uri = self.path or "/" - - if self.query is not None: - uri += "?" + self.query - - return uri - - @property - def authority(self) -> str | None: - """ - Authority component as defined in RFC 3986 3.2. - This includes userinfo (auth), host and port. - - i.e. - userinfo@host:port - """ - userinfo = self.auth - netloc = self.netloc - if netloc is None or userinfo is None: - return netloc - else: - return f"{userinfo}@{netloc}" - - @property - def netloc(self) -> str | None: - """ - Network location including host and port. - - If you need the equivalent of urllib.parse's ``netloc``, - use the ``authority`` property instead. - """ - if self.host is None: - return None - if self.port: - return f"{self.host}:{self.port}" - return self.host - - @property - def url(self) -> str: - """ - Convert self into a url - - This function should more or less round-trip with :func:`.parse_url`. The - returned url may not be exactly the same as the url inputted to - :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls - with a blank port will have : removed). - - Example: - - .. code-block:: python - - import urllib3 - - U = urllib3.util.parse_url("https://google.com/mail/") - - print(U.url) - # "https://google.com/mail/" - - print( urllib3.util.Url("https", "username:password", - "host.com", 80, "/path", "query", "fragment" - ).url - ) - # "https://username:password@host.com:80/path?query#fragment" - """ - scheme, auth, host, port, path, query, fragment = self - url = "" - - # We use "is not None" we want things to happen with empty strings (or 0 port) - if scheme is not None: - url += scheme + "://" - if auth is not None: - url += auth + "@" - if host is not None: - url += host - if port is not None: - url += ":" + str(port) - if path is not None: - url += path - if query is not None: - url += "?" + query - if fragment is not None: - url += "#" + fragment - - return url - - def __str__(self) -> str: - return self.url - - -@typing.overload -def _encode_invalid_chars( - component: str, allowed_chars: typing.Container[str] -) -> str: # Abstract - ... - - -@typing.overload -def _encode_invalid_chars( - component: None, allowed_chars: typing.Container[str] -) -> None: # Abstract - ... - - -def _encode_invalid_chars( - component: str | None, allowed_chars: typing.Container[str] -) -> str | None: - """Percent-encodes a URI component without reapplying - onto an already percent-encoded component. - """ - if component is None: - return component - - component = to_str(component) - - # Normalize existing percent-encoded bytes. - # Try to see if the component we're encoding is already percent-encoded - # so we can skip all '%' characters but still encode all others. - component, percent_encodings = _PERCENT_RE.subn( - lambda match: match.group(0).upper(), component - ) - - uri_bytes = component.encode("utf-8", "surrogatepass") - is_percent_encoded = percent_encodings == uri_bytes.count(b"%") - encoded_component = bytearray() - - for i in range(0, len(uri_bytes)): - # Will return a single character bytestring - byte = uri_bytes[i : i + 1] - byte_ord = ord(byte) - if (is_percent_encoded and byte == b"%") or ( - byte_ord < 128 and byte.decode() in allowed_chars - ): - encoded_component += byte - continue - encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) - - return encoded_component.decode() - - -def _remove_path_dot_segments(path: str) -> str: - # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code - segments = path.split("/") # Turn the path into a list of segments - output = [] # Initialize the variable to use to store output - - for segment in segments: - # '.' is the current directory, so ignore it, it is superfluous - if segment == ".": - continue - # Anything other than '..', should be appended to the output - if segment != "..": - output.append(segment) - # In this case segment == '..', if we can, we should pop the last - # element - elif output: - output.pop() - - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if path.startswith("/") and (not output or output[0]): - output.insert(0, "") - - # If the path starts with '/.' or '/..' ensure we add one more empty - # string to add a trailing '/' - if path.endswith(("/.", "/..")): - output.append("") - - return "/".join(output) - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - if host: - if scheme in _NORMALIZABLE_SCHEMES: - is_ipv6 = _IPV6_ADDRZ_RE.match(host) - if is_ipv6: - # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as - # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID - # separator as necessary to return a valid RFC 4007 scoped IP. - match = _ZONE_ID_RE.search(host) - if match: - start, end = match.span(1) - zone_id = host[start:end] - - if zone_id.startswith("%25") and zone_id != "%25": - zone_id = zone_id[3:] - else: - zone_id = zone_id[1:] - zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) - return f"{host[:start].lower()}%{zone_id}{host[end:]}" - else: - return host.lower() - elif not _IPV4_RE.match(host): - return to_str( - b".".join([_idna_encode(label) for label in host.split(".")]), - "ascii", - ) - return host - - -def _idna_encode(name: str) -> bytes: - if not name.isascii(): - try: - import idna - except ImportError: - raise LocationParseError( - "Unable to parse URL without the 'idna' module" - ) from None - - try: - return idna.encode(name.lower(), strict=True, std3_rules=True) - except idna.IDNAError: - raise LocationParseError( - f"Name '{name}' is not a valid IDNA label" - ) from None - - return name.lower().encode("ascii") - - -def _encode_target(target: str) -> str: - """Percent-encodes a request target so that there are no invalid characters - - Pre-condition for this function is that 'target' must start with '/'. - If that is the case then _TARGET_RE will always produce a match. - """ - match = _TARGET_RE.match(target) - if not match: # Defensive: - raise LocationParseError(f"{target!r} is not a valid request URI") - - path, query = match.groups() - encoded_target = _encode_invalid_chars(path, _PATH_CHARS) - if query is not None: - query = _encode_invalid_chars(query, _QUERY_CHARS) - encoded_target += "?" + query - return encoded_target - - -def parse_url(url: str) -> Url: - """ - Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is - performed to parse incomplete urls. Fields not provided will be None. - This parser is RFC 3986 and RFC 6874 compliant. - - The parser logic and helper functions are based heavily on - work done in the ``rfc3986`` module. - - :param str url: URL to parse into a :class:`.Url` namedtuple. - - Partly backwards-compatible with :mod:`urllib.parse`. - - Example: - - .. code-block:: python - - import urllib3 - - print( urllib3.util.parse_url('http://google.com/mail/')) - # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) - - print( urllib3.util.parse_url('google.com:80')) - # Url(scheme=None, host='google.com', port=80, path=None, ...) - - print( urllib3.util.parse_url('/foo?bar')) - # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) - """ - if not url: - # Empty - return Url() - - source_url = url - if not _SCHEME_RE.search(url): - url = "//" + url - - scheme: str | None - authority: str | None - auth: str | None - host: str | None - port: str | None - port_int: int | None - path: str | None - query: str | None - fragment: str | None - - try: - scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] - normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES - - if scheme: - scheme = scheme.lower() - - if authority: - auth, _, host_port = authority.rpartition("@") - auth = auth or None - host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] - if auth and normalize_uri: - auth = _encode_invalid_chars(auth, _USERINFO_CHARS) - if port == "": - port = None - else: - auth, host, port = None, None, None - - if port is not None: - port_int = int(port) - if not (0 <= port_int <= 65535): - raise LocationParseError(url) - else: - port_int = None - - host = _normalize_host(host, scheme) - - if normalize_uri and path: - path = _remove_path_dot_segments(path) - path = _encode_invalid_chars(path, _PATH_CHARS) - if normalize_uri and query: - query = _encode_invalid_chars(query, _QUERY_CHARS) - if normalize_uri and fragment: - fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) - - except (ValueError, AttributeError) as e: - raise LocationParseError(source_url) from e - - # For the sake of backwards compatibility we put empty - # string values for path if there are any defined values - # beyond the path in the URL. - # TODO: Remove this when we break backwards compatibility. - if not path: - if query is not None or fragment is not None: - path = "" - else: - path = None - - return Url( - scheme=scheme, - auth=auth, - host=host, - port=port_int, - path=path, - query=query, - fragment=fragment, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py deleted file mode 100644 index 35c77e4025..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/util.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import typing -from types import TracebackType - - -def to_bytes( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> bytes: - if isinstance(x, bytes): - return x - elif not isinstance(x, str): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.encode(encoding or "utf-8", errors=errors or "strict") - return x.encode() - - -def to_str( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> str: - if isinstance(x, str): - return x - elif not isinstance(x, bytes): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.decode(encoding or "utf-8", errors=errors or "strict") - return x.decode() - - -def reraise( - tp: type[BaseException] | None, - value: BaseException, - tb: TracebackType | None = None, -) -> typing.NoReturn: - try: - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - finally: - value = None # type: ignore[assignment] - tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py deleted file mode 100644 index aeca0c7ad5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionAllowlist/urllib3/util/wait.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import select -import socket -from functools import partial - -__all__ = ["wait_for_read", "wait_for_write"] - - -# How should we wait on sockets? -# -# There are two types of APIs you can use for waiting on sockets: the fancy -# modern stateful APIs like epoll/kqueue, and the older stateless APIs like -# select/poll. The stateful APIs are more efficient when you have a lots of -# sockets to keep track of, because you can set them up once and then use them -# lots of times. But we only ever want to wait on a single socket at a time -# and don't want to keep track of state, so the stateless APIs are actually -# more efficient. So we want to use select() or poll(). -# -# Now, how do we choose between select() and poll()? On traditional Unixes, -# select() has a strange calling convention that makes it slow, or fail -# altogether, for high-numbered file descriptors. The point of poll() is to fix -# that, so on Unixes, we prefer poll(). -# -# On Windows, there is no poll() (or at least Python doesn't provide a wrapper -# for it), but that's OK, because on Windows, select() doesn't have this -# strange calling convention; plain select() works fine. -# -# So: on Windows we use select(), and everywhere else we use poll(). We also -# fall back to select() in case poll() is somehow broken or missing. - - -def select_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - rcheck = [] - wcheck = [] - if read: - rcheck.append(sock) - if write: - wcheck.append(sock) - # When doing a non-blocking connect, most systems signal success by - # marking the socket writable. Windows, though, signals success by marked - # it as "exceptional". We paper over the difference by checking the write - # sockets for both conditions. (The stdlib selectors module does the same - # thing.) - fn = partial(select.select, rcheck, wcheck, wcheck) - rready, wready, xready = fn(timeout) - return bool(rready or wready or xready) - - -def poll_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - mask = 0 - if read: - mask |= select.POLLIN - if write: - mask |= select.POLLOUT - poll_obj = select.poll() - poll_obj.register(sock, mask) - - # For some reason, poll() takes timeout in milliseconds - def do_poll(t: float | None) -> list[tuple[int, int]]: - if t is not None: - t *= 1000 - return poll_obj.poll(t) - - return bool(do_poll(timeout)) - - -def _have_working_poll() -> bool: - # Apparently some systems have a select.poll that fails as soon as you try - # to use it, either due to strange configuration or broken monkeypatching - # from libraries like eventlet/greenlet. - try: - poll_obj = select.poll() - poll_obj.poll(0) - except (AttributeError, OSError): - return False - else: - return True - - -def wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - # We delay choosing which implementation to use until the first time we're - # called. We could do it at import time, but then we might make the wrong - # decision if someone goes wild with monkeypatching select.poll after - # we're imported. - global wait_for_socket - if _have_working_poll(): - wait_for_socket = poll_wait_for_socket - elif hasattr(select, "select"): - wait_for_socket = select_wait_for_socket - return wait_for_socket(sock, read, write, timeout) - - -def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for reading to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, read=True, timeout=timeout) - - -def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for writing to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA deleted file mode 100644 index 0d4f101491..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/METADATA +++ /dev/null @@ -1,78 +0,0 @@ -Metadata-Version: 2.4 -Name: certifi -Version: 2026.6.17 -Summary: Python package for providing Mozilla's CA Bundle. -Home-page: https://github.com/certifi/python-certifi -Author: Kenneth Reitz -Author-email: me@kennethreitz.com -License: MPL-2.0 -Project-URL: Source, https://github.com/certifi/python-certifi -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) -Classifier: Natural Language :: English -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Requires-Python: >=3.7 -License-File: LICENSE -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: home-page -Dynamic: license -Dynamic: license-file -Dynamic: project-url -Dynamic: requires-python -Dynamic: summary - -Certifi: Python SSL Certificates -================================ - -Certifi provides Mozilla's carefully curated collection of Root Certificates for -validating the trustworthiness of SSL certificates while verifying the identity -of TLS hosts. It has been extracted from the `Requests`_ project. - -Installation ------------- - -``certifi`` is available on PyPI. Simply install it with ``pip``:: - - $ pip install certifi - -Usage ------ - -To reference the installed certificate authority (CA) bundle, you can use the -built-in function:: - - >>> import certifi - - >>> certifi.where() - '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' - -Or from the command line:: - - $ python -m certifi - /usr/local/lib/python3.7/site-packages/certifi/cacert.pem - -Enjoy! - -.. _`Requests`: https://requests.readthedocs.io/en/latest/ - -Addition/Removal of Certificates --------------------------------- - -Certifi does not support any addition/removal or other modification of the -CA trust store content. This project is intended to provide a reliable and -highly portable root of trust to python deployments. Look to upstream projects -for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD deleted file mode 100644 index 13b0ce7da4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/RECORD +++ /dev/null @@ -1,12 +0,0 @@ -certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 -certifi-2026.6.17.dist-info/RECORD,, -certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 -certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 -certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 -certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 -certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 -certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 -certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 -certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL deleted file mode 100644 index 14a883f292..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (82.0.1) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE deleted file mode 100644 index 62b076cdee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/licenses/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -This package contains a modified version of ca-bundle.crt: - -ca-bundle.crt -- Bundle of CA Root Certificates - -This is a bundle of X.509 certificates of public Certificate Authorities -(CA). These were automatically extracted from Mozilla's root certificates -file (certdata.txt). This file can be found in the mozilla source tree: -https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt -It contains the certificates in PEM format and therefore -can be directly used with curl / libcurl / php_curl, or with -an Apache+mod_ssl webserver for SSL client authentication. -Just configure this file as the SSLCACertificateFile.# - -***** BEGIN LICENSE BLOCK ***** -This Source Code Form is subject to the terms of the Mozilla Public License, -v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain -one at http://mozilla.org/MPL/2.0/. - -***** END LICENSE BLOCK ***** -@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt deleted file mode 100644 index 963eac530b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi-2026.6.17.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py deleted file mode 100644 index ed9a74b7a0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .core import contents, where - -__all__ = ["contents", "where"] -__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py deleted file mode 100644 index 8945b5da85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/__main__.py +++ /dev/null @@ -1,12 +0,0 @@ -import argparse - -from certifi import contents, where - -parser = argparse.ArgumentParser() -parser.add_argument("-c", "--contents", action="store_true") -args = parser.parse_args() - -if args.contents: - print(contents()) -else: - print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem deleted file mode 100644 index 1c2dbfeb68..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/cacert.pem +++ /dev/null @@ -1,3863 +0,0 @@ - -# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Label: "COMODO ECC Certification Authority" -# Serial: 41578283867086692638256921589707938090 -# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 -# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 -# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT -IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw -MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N -T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR -FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J -cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW -BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm -fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv -GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" -# Serial: 80544274841616 -# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 -# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 -# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG -EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 -MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl -cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR -dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB -pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM -b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm -aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz -IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT -lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz -AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 -VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG -ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 -BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG -AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M -U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh -bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C -+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F -uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 -XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Label: "Microsec e-Szigno Root CA 2009" -# Serial: 14014712776195784473 -# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 -# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e -# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G -CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y -OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx -FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp -Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP -kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc -cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U -fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 -N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC -xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 -+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM -Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG -SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h -mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk -ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c -2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t -HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Label: "GlobalSign Root CA - R3" -# Serial: 4835703278459759426209954 -# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 -# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad -# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 -MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 -RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT -gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm -KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd -QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ -XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o -LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU -RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp -jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK -6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX -mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs -Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH -WD9f ------END CERTIFICATE----- - -# Issuer: CN=Izenpe.com O=IZENPE S.A. -# Subject: CN=Izenpe.com O=IZENPE S.A. -# Label: "Izenpe.com" -# Serial: 917563065490389241595536686991402621 -# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 -# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 -# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f ------BEGIN CERTIFICATE----- -MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 -MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 -ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD -VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j -b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq -scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO -xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H -LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX -uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD -yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ -JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q -rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN -BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L -hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB -QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ -HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu -Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg -QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB -BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx -MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA -A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb -laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 -awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo -JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw -LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT -VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk -LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb -UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ -QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ -naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls -QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE----- - -# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Label: "Go Daddy Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 -# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b -# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT -EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp -ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz -NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH -EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE -AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD -E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH -/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy -DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh -GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR -tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA -AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX -WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu -9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr -gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo -2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI -4uJEvlz36hz1 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 -# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e -# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Services Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 -# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f -# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs -ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD -VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy -ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy -dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p -OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 -8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K -Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe -hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk -6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q -AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI -bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB -ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z -qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn -0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN -sSi6 ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA" -# Serial: 279744 -# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 -# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e -# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM -MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D -ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU -cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 -WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg -Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw -IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH -UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM -TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU -BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM -kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x -AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV -HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y -sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL -I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 -J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY -VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Label: "TWCA Root Certification Authority" -# Serial: 1 -# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 -# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 -# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES -MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU -V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz -WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO -LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE -AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH -K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX -RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z -rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx -3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq -hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC -MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls -XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D -lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn -aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ -YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Label: "Security Communication RootCA2" -# Serial: 0 -# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 -# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 -# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl -MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe -U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX -DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy -dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj -YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV -OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr -zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM -VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ -hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO -ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw -awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs -OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF -coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc -okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 -t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy -1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ -SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Label: "Actalis Authentication Root CA" -# Serial: 6271844772424770508 -# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 -# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac -# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE -BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w -MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC -SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 -ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv -UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX -4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 -KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ -gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb -rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ -51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F -be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe -KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F -v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn -fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 -jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz -ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL -e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 -jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz -WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V -SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j -pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX -X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok -fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R -K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU -ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU -LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT -LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 2 Root CA" -# Serial: 2 -# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 -# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 -# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr -6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV -L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 -1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx -MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ -QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB -arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr -Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi -FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS -P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN -9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz -uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h -9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t -OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo -+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 -KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 -DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us -H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ -I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 -5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h -3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz -Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 3 Root CA" -# Serial: 2 -# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec -# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 -# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y -ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E -N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 -tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX -0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c -/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X -KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY -zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS -O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D -34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP -K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv -Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj -QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS -IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 -HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa -O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv -033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u -dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE -kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 -3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD -u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq -4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 3" -# Serial: 1 -# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef -# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 -# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN -8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ -RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 -hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 -ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM -EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 -A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy -WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ -1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 -6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT -91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p -TpPDpFQUWw== ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 2009" -# Serial: 623603 -# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f -# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 -# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 ------BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha -ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM -HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 -UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 -tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R -ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM -lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp -/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G -A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy -MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl -cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js -L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL -BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni -acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K -zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 -PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y -Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 EV 2009" -# Serial: 623604 -# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 -# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 -# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw -NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV -BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn -ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 -3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z -qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR -p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 -HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw -ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea -HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw -Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh -c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E -RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt -dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku -Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp -3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF -CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na -xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX -KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE----- - -# Issuer: CN=CA Disig Root R2 O=Disig a.s. -# Subject: CN=CA Disig Root R2 O=Disig a.s. -# Label: "CA Disig Root R2" -# Serial: 10572350602393338211 -# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 -# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 -# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV -BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu -MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy -MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx -EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe -NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH -PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I -x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe -QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR -yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO -QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 -H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ -QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD -i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs -nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 -rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI -hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM -tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf -GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb -lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka -+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal -TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i -nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 -gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr -G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os -zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x -L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE----- - -# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Label: "ACCVRAIZ1" -# Serial: 6828503384748696800 -# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 -# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 -# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 ------BEGIN CERTIFICATE----- -MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE -AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw -CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ -BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND -VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb -qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY -HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo -G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA -lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr -IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ -0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH -k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 -4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO -m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa -cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl -uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI -KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls -ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG -AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 -VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT -VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG -CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA -cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA -QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA -7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA -cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA -QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA -czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu -aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt -aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud -DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF -BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp -D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU -JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m -AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD -vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms -tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH -7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h -I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA -h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF -d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H -pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE----- - -# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA Global Root CA" -# Serial: 3262 -# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 -# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 -# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx -EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT -VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 -NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT -B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF -10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz -0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh -MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH -zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc -46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 -yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi -laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP -oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA -BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE -qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm -4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL -1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF -H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo -RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ -nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh -15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW -6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW -nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j -wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz -aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy -KwbQBM0= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 2" -# Serial: 1 -# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a -# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 -# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd -AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC -FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi -1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq -jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ -wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ -WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy -NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC -uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw -IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 -g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP -BSeOE6Fuwg== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot 2011 O=Atos -# Subject: CN=Atos TrustedRoot 2011 O=Atos -# Label: "Atos TrustedRoot 2011" -# Serial: 6643877497813316402 -# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 -# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 -# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE -AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG -EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM -FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC -REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp -Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM -VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ -SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ -4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L -cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi -eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG -A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 -DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j -vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP -DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc -maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D -lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv -KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 1 G3" -# Serial: 687049649626669250736271037606554624078720034195 -# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab -# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 -# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 -MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV -wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe -rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 -68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh -4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp -UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o -abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc -3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G -KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt -hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO -Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt -zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD -ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC -MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 -cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN -qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 -YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv -b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 -8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k -NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj -ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp -q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt -nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 2 G3" -# Serial: 390156079458959257446133169266079962026824725800 -# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 -# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 -# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 -MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf -qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW -n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym -c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ -O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 -o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j -IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq -IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz -8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh -vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l -7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG -cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD -ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC -roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga -W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n -lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE -+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV -csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd -dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg -KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM -HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 -WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 3 G3" -# Serial: 268090761170461462463995952157327242137089239581 -# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 -# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d -# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 -MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR -/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu -FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR -U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c -ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR -FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k -A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw -eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl -sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp -VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q -A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ -ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD -ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI -FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv -oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg -u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP -0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf -3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl -8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ -DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN -PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ -ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G2" -# Serial: 15385348160840213938643033620894905419 -# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d -# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f -# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 ------BEGIN CERTIFICATE----- -MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA -n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc -biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp -EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA -bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu -YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB -AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW -BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI -QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I -0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni -lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 -B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv -ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo -IhNzbM8m9Yop5w== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G3" -# Serial: 15459312981008553731928384953135426796 -# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb -# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 -# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg -RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf -Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q -RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD -AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY -JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv -6pZjamVFkpUBtA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G2" -# Serial: 4293743540046975378534879503202253541 -# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 -# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 -# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f ------BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH -MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI -2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx -1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ -q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz -tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ -vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV -5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY -1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 -NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG -Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 -8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe -pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G3" -# Serial: 7089244469030293291760083333884364146 -# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca -# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e -# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 ------BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe -Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw -EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x -IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG -fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO -Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx -AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ -oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 -sycX ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Trusted Root G4" -# Serial: 7451500558977370777930084869016614236 -# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 -# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 -# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 ------BEGIN CERTIFICATE----- -MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg -RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y -ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If -xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV -ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO -DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ -jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ -CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi -EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM -fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY -uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK -chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t -9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD -ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 -SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd -+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc -fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa -sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N -cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N -0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie -4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI -r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 -/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm -gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE----- - -# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Label: "COMODO RSA Certification Authority" -# Serial: 101909084537582093308941363524873193117 -# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 -# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 -# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 ------BEGIN CERTIFICATE----- -MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB -hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV -BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT -EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR -6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X -pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC -9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV -/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf -Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z -+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w -qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah -SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC -u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf -Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq -crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E -FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl -wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM -4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV -2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna -FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ -CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK -boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke -jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL -S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb -QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl -0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB -NVOFBkpdn627G190 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Label: "USERTrust RSA Certification Authority" -# Serial: 2645093764781058787591871645665788717 -# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 -# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e -# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 ------BEGIN CERTIFICATE----- -MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB -iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl -cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV -BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw -MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV -BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B -3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY -tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ -Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 -VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT -79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 -c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT -Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l -c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee -UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE -Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd -BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G -A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF -Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO -VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 -ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs -8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR -iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze -Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ -XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ -qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB -VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB -L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG -jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Label: "USERTrust ECC Certification Authority" -# Serial: 123013823720199481456569720443997572134 -# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 -# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 -# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a ------BEGIN CERTIFICATE----- -MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL -MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl -eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT -JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT -Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg -VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo -I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng -o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G -A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB -zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW -RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Label: "GlobalSign ECC Root CA - R5" -# Serial: 32785792099990507226680698011560947931244 -# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 -# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa -# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 ------BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc -8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke -hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI -KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg -515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO -xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Label: "IdenTrust Commercial Root CA 1" -# Serial: 13298821034946342390520003877796839426 -# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 -# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 -# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu -VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw -MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw -JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT -3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU -+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp -S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 -bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi -T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL -vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK -Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK -dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT -c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv -l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N -iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD -ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH -6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt -LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 -nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 -+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK -W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT -AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq -l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG -4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ -mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A -7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Label: "IdenTrust Public Sector Root CA 1" -# Serial: 13298821034946342390521976156843933698 -# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba -# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd -# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu -VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN -MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 -MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 -ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy -RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS -bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF -/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R -3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw -EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy -9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V -GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ -2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV -WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD -W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN -AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj -t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV -DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 -TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G -lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW -mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df -WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 -+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ -tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA -GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv -8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE----- - -# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority -# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority -# Label: "CFCA EV ROOT" -# Serial: 407555286 -# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 -# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 -# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD -TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx -MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j -aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP -T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 -sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL -TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 -/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp -7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz -EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt -hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP -a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot -aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg -TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV -PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv -cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL -tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd -BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT -ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL -jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS -ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy -P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 -xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d -Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN -5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe -/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z -AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ -5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GB CA" -# Serial: 157768595616588414422159278966750757568 -# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d -# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed -# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt -MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg -Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i -YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x -CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG -b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh -bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 -HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx -WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX -1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk -u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P -99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r -M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB -BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh -cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 -gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO -ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf -aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic -Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE----- - -# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Label: "SZAFIR ROOT CA2" -# Serial: 357043034767186914217277344587386743377558296292 -# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 -# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de -# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 -ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw -NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L -cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg -Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN -QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT -3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw -3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 -3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 -BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN -XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF -AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw -8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG -nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP -oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy -d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg -LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA 2" -# Serial: 44979900017204383099463764357512596969 -# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 -# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 -# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 ------BEGIN CERTIFICATE----- -MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB -gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu -QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG -A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz -OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ -VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 -b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA -DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn -0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB -OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE -fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E -Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m -o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i -sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW -OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez -Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS -adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n -3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ -F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf -CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 -XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm -djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ -WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb -AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq -P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko -b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj -XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P -5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi -DrW5viSP ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce -# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 -# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 ------BEGIN CERTIFICATE----- -MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix -DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k -IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT -N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v -dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG -A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh -ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx -QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA -4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 -AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 -4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C -ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV -9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD -gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 -Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq -NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko -LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc -Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd -ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I -XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI -M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot -9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V -Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea -j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh -X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ -l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf -bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 -pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK -e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 -vm9qp/UsQu0yrbYhnr68 ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef -# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 -# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 ------BEGIN CERTIFICATE----- -MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN -BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl -bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv -b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ -BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj -YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 -MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 -dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg -QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa -jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi -C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep -lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof -TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X1 O=Internet Security Research Group -# Subject: CN=ISRG Root X1 O=Internet Security Research Group -# Label: "ISRG Root X1" -# Serial: 172886928669790476064670243504169061120 -# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e -# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 -# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- - -# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Label: "AC RAIZ FNMT-RCM" -# Serial: 485876308206448804701554682760554759 -# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d -# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 -# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx -CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ -WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ -BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG -Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ -yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf -BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz -WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF -tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z -374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC -IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL -mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 -wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS -MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 -ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet -UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H -YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 -LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD -nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 -RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM -LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf -77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N -JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm -fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp -6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp -1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B -9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok -RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv -uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 1 O=Amazon -# Subject: CN=Amazon Root CA 1 O=Amazon -# Label: "Amazon Root CA 1" -# Serial: 143266978916655856878034712317230054538369994 -# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 -# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 -# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 2 O=Amazon -# Subject: CN=Amazon Root CA 2 O=Amazon -# Label: "Amazon Root CA 2" -# Serial: 143266982885963551818349160658925006970653239 -# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 -# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a -# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 3 O=Amazon -# Subject: CN=Amazon Root CA 3 O=Amazon -# Label: "Amazon Root CA 3" -# Serial: 143266986699090766294700635381230934788665930 -# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 -# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e -# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl -ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr -ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr -BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM -YyRIHN8wfdVoOw== ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 4 O=Amazon -# Subject: CN=Amazon Root CA 4 O=Amazon -# Label: "Amazon Root CA 4" -# Serial: 143266989758080763974105200630763877849284878 -# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd -# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be -# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi -9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk -M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB -MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw -CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW -1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- - -# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" -# Serial: 1 -# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 -# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca -# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 ------BEGIN CERTIFICATE----- -MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx -GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp -bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w -KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 -BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy -dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG -EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll -IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU -QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT -TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg -LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 -a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr -LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr -N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X -YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ -iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f -AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH -V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh -AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf -IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 -lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c -8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf -lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE----- - -# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Label: "GDCA TrustAUTH R5 ROOT" -# Serial: 9009899650740120186 -# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 -# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 -# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 ------BEGIN CERTIFICATE----- -MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE -BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ -IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 -MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV -BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w -HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj -Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj -TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u -KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj -qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm -MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 -ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP -zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk -L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC -jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA -HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC -AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg -p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm -DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 -COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry -L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf -JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg -IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io -2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV -09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ -XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq -T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe -MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Label: "SSL.com Root Certification Authority RSA" -# Serial: 8875640296558310041 -# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 -# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb -# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 ------BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE -BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK -DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz -OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R -xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX -qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC -C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 -6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh -/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF -YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E -JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc -US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 -ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm -+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi -M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G -A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV -cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc -Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs -PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ -q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 -cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr -a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I -H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y -K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu -nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf -oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY -Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com Root Certification Authority ECC" -# Serial: 8495723813297216424 -# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e -# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a -# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 ------BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz -WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 -b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS -b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI -7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg -CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud -EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD -VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T -kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ -gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority RSA R2" -# Serial: 6248227494352943350 -# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 -# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a -# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c ------BEGIN CERTIFICATE----- -MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV -BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE -CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy -MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G -A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD -DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq -M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf -OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa -4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 -HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR -aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA -b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ -Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV -PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO -pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu -UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY -MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV -HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 -9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW -s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 -Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg -cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM -79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz -/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt -ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm -Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK -QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ -w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi -S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 -mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority ECC" -# Serial: 3182246526754555285 -# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 -# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d -# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 ------BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx -NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv -bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA -VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku -WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX -5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ -ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg -h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Label: "GlobalSign Root CA - R6" -# Serial: 1417766617973444989252670301619537 -# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae -# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 -# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg -MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx -MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET -MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI -xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k -ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD -aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw -LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw -1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX -k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 -SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h -bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n -WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY -rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce -MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu -bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN -nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt -Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 -55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj -vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf -cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz -oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp -nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs -pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v -JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R -8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 -5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GC CA" -# Serial: 44084345621038548146064804565436152554 -# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 -# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 -# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d ------BEGIN CERTIFICATE----- -MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw -CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 -bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg -Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ -BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu -ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS -b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni -eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W -p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T -rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV -57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg -Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE----- - -# Issuer: CN=UCA Global G2 Root O=UniTrust -# Subject: CN=UCA Global G2 Root O=UniTrust -# Label: "UCA Global G2 Root" -# Serial: 124779693093741543919145257850076631279 -# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 -# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a -# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH -bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x -CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds -b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr -b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 -kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm -VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R -VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc -C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj -tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY -D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv -j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl -NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 -iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP -O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV -ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj -L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 -1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl -1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU -b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV -PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj -y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb -EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg -DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI -+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy -YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX -UB+K+wb1whnw0A== ------END CERTIFICATE----- - -# Issuer: CN=UCA Extended Validation Root O=UniTrust -# Subject: CN=UCA Extended Validation Root O=UniTrust -# Label: "UCA Extended Validation Root" -# Serial: 106100277556486529736699587978573607008 -# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 -# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a -# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF -eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx -MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV -BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog -D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS -sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop -O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk -sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi -c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj -VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz -KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ -TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G -sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs -1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD -fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN -l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR -ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ -VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 -c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp -4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s -t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj -2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO -vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C -xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx -cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM -fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax ------END CERTIFICATE----- - -# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Label: "Certigna Root CA" -# Serial: 269714418870597844693661054334862075617 -# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 -# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 -# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 ------BEGIN CERTIFICATE----- -MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw -WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw -MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x -MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD -VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX -BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO -ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M -CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu -I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm -TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh -C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf -ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz -IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT -Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k -JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 -hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB -GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of -1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov -L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo -dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr -aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq -hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L -6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG -HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 -0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB -lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi -o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 -gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v -faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 -Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh -jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw -3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign Root CA - G1" -# Serial: 235931866688319308814040 -# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac -# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c -# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 ------BEGIN CERTIFICATE----- -MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD -VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU -ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH -MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO -MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv -Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz -f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO -8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq -d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM -tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt -Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB -o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x -PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM -wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d -GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH -6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby -RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx -iN66zB+Afko= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign ECC Root CA - G3" -# Serial: 287880440101571086945156 -# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 -# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 -# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b ------BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG -EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo -bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g -RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ -TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s -b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw -djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 -WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS -fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB -zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB -CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD -+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Label: "emSign Root CA - C1" -# Serial: 825510296613316004955058 -# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 -# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 -# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f ------BEGIN CERTIFICATE----- -MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG -A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg -SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v -dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ -BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ -HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH -3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH -GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c -xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 -aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq -TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 -/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 -kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG -YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT -+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo -WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Label: "emSign ECC Root CA - C3" -# Serial: 582948710642506000014504 -# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 -# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 -# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 ------BEGIN CERTIFICATE----- -MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG -EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx -IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND -IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci -MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti -sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O -BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB -Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c -3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J -0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== ------END CERTIFICATE----- - -# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Label: "Hongkong Post Root CA 3" -# Serial: 46170865288971385588281144162979347873371282084 -# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 -# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 -# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 ------BEGIN CERTIFICATE----- -MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL -BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ -SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n -a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 -NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT -CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u -Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO -dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI -VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV -9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY -2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY -vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt -bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb -x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ -l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK -TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj -Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e -i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw -DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG -7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk -MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr -gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk -GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS -3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm -Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ -l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c -JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP -L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa -LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG -mpv0 ------END CERTIFICATE----- - -# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft ECC Root Certificate Authority 2017" -# Serial: 136839042543790627607696632466672567020 -# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 -# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 -# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 ------BEGIN CERTIFICATE----- -MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD -VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw -MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV -UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy -b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR -ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb -hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 -FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV -L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB -iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE----- - -# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft RSA Root Certificate Authority 2017" -# Serial: 40975477897264996090493496164228220339 -# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 -# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 -# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 ------BEGIN CERTIFICATE----- -MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl -MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw -NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG -EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N -aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ -Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 -ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 -HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm -gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ -jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc -aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG -YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 -W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K -UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH -+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q -W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC -LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC -gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 -tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh -SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 -TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 -pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR -xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp -GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 -dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN -AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB -RA+GsCyRxj3qrg+E ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Label: "e-Szigno Root CA 2017" -# Serial: 411379200276854331539784714 -# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 -# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 -# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 ------BEGIN CERTIFICATE----- -MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV -BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk -LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv -b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ -BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg -THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v -IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv -xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H -Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB -eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo -jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ -+efcMQ== ------END CERTIFICATE----- - -# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Label: "certSIGN Root CA G2" -# Serial: 313609486401300475190 -# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 -# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 -# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV -BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g -Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ -BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ -R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF -dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw -vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ -uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp -n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs -cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW -xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P -rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF -DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx -DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy -LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C -eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ -d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq -kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC -b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl -qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 -OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c -NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk -ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO -pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj -03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk -PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE -1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX -QRBdJ3NghVdJIgc= ------END CERTIFICATE----- - -# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Label: "NAVER Global Root Certification Authority" -# Serial: 9013692873798656336226253319739695165984492813 -# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b -# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 -# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 ------BEGIN CERTIFICATE----- -MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM -BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG -T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx -CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD -b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA -iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH -38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE -HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz -kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP -szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq -vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf -nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG -YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo -0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a -CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K -AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I -36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB -Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN -qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj -cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm -+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL -hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe -lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 -p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 -piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR -LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX -5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO -dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul -9XXeifdy ------END CERTIFICATE----- - -# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" -# Serial: 131542671362353147877283741781055151509 -# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb -# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a -# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb ------BEGIN CERTIFICATE----- -MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw -CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw -FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S -Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 -MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL -DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS -QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH -sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK -Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu -SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC -MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy -v+c= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Label: "GlobalSign Root R46" -# Serial: 1552617688466950547958867513931858518042577 -# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef -# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 -# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA -MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD -VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy -MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt -c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ -OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG -vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud -316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo -0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE -y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF -zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE -+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN -I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs -x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa -ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC -4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 -7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg -JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti -2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk -pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF -FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt -rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk -ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 -u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP -4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 -N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 -vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Label: "GlobalSign Root E46" -# Serial: 1552617690338932563915843282459653771421763 -# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f -# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 -# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 ------BEGIN CERTIFICATE----- -MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx -CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD -ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw -MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex -HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq -R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd -yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ -7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 -+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE----- - -# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Label: "ANF Secure Server Root CA" -# Serial: 996390341000653745 -# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 -# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 -# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV -BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk -YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV -BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN -MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF -UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD -VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v -dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj -cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q -yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH -2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX -H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL -zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR -p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz -W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ -SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn -LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 -n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B -u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj -o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC -AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L -9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej -rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK -pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 -vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq -OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ -/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 -2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI -+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 -MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo -tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE----- - -# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum EC-384 CA" -# Serial: 160250656287871593594747141429395092468 -# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 -# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed -# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 ------BEGIN CERTIFICATE----- -MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw -CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw -JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT -EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 -WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT -LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX -BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE -KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm -Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 -EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J -UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn -nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Root CA" -# Serial: 40870380103424195783807378461123655149 -# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 -# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 -# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd ------BEGIN CERTIFICATE----- -MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 -MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu -MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV -BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw -MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg -U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo -b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ -n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q -p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq -NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF -8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 -HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa -mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi -7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF -ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P -qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ -v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 -Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 -vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD -ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 -WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo -zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR -5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ -GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf -5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq -0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D -P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM -qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP -0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf -E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE----- - -# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Label: "TunTrust Root CA" -# Serial: 108534058042236574382096126452369648152337120275 -# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 -# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb -# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL -BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg -Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv -b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG -EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u -IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ -n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd -2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF -VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ -GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF -li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU -r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 -eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb -MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg -jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB -7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW -5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE -ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 -90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z -xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu -QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 -FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH -22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP -xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn -dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 -Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b -nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ -CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH -u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj -d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS RSA Root CA 2021" -# Serial: 76817823531813593706434026085292783742 -# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 -# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d -# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs -MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg -Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL -MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl -YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv -b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l -mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE -4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv -a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M -pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw -Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b -LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY -AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB -AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq -E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr -W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ -CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU -X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 -f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja -H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP -JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P -zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt -jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 -/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT -BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 -aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW -xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU -63ZTGI0RmLo= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS ECC Root CA 2021" -# Serial: 137515985548005187474074462014555733966 -# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 -# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 -# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 ------BEGIN CERTIFICATE----- -MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw -CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh -cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v -dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG -A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj -aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg -Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 -KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y -STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD -AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw -SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN -nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE----- - -# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" -# Serial: 1977337328857672817 -# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 -# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe -# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE -BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h -cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 -MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg -Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 -thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM -cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG -L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i -NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h -X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b -m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy -Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja -EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T -KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF -6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh -OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc -tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd -IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j -b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC -AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw -ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m -iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF -Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ -hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P -Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE -EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV -1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t -CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR -5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw -f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 -ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK -GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE----- - -# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus ECC Root CA" -# Serial: 630369271402956006249506845124680065938238527194 -# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 -# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 -# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 ------BEGIN CERTIFICATE----- -MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw -RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY -BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz -MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u -LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 -v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd -e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw -V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA -AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG -GJTO ------END CERTIFICATE----- - -# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus Root CA" -# Serial: 387574501246983434957692974888460947164905180485 -# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc -# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 -# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 ------BEGIN CERTIFICATE----- -MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL -BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x -FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx -MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s -THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc -IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU -AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ -GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 -8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH -flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt -J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim -0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN -pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ -UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW -OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB -AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet -8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd -nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j -bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM -Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv -TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS -S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr -I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 -b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB -UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P -Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven -sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X2 O=Internet Security Research Group -# Subject: CN=ISRG Root X2 O=Internet Security Research Group -# Label: "ISRG Root X2" -# Serial: 87493402998870891108772069816698636114 -# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 -# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af -# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 ------BEGIN CERTIFICATE----- -MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw -CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg -R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 -MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT -ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw -EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW -+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 -ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI -zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW -tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 -/q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE----- - -# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Label: "HiPKI Root CA - G1" -# Serial: 60966262342023497858655262305426234976 -# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 -# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 -# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc ------BEGIN CERTIFICATE----- -MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP -MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 -ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa -Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 -YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw -qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv -Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 -lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz -Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ -KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK -FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj -HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr -y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ -/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM -a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 -fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG -SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi -7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc -SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza -ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc -XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg -iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho -L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF -Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr -kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ -vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU -YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Label: "GlobalSign ECC Root CA - R4" -# Serial: 159662223612894884239637590694 -# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc -# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 -# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 ------BEGIN CERTIFICATE----- -MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD -VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw -MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g -UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT -BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx -uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV -HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ -+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 -bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R1 O=Google Trust Services LLC -# Subject: CN=GTS Root R1 O=Google Trust Services LLC -# Label: "GTS Root R1" -# Serial: 159662320309726417404178440727 -# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 -# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a -# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw -MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp -Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo -27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w -Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw -TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl -qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH -szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 -Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk -MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 -wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p -aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN -VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID -AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb -C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe -QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy -h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 -7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J -ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef -MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ -Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT -6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ -0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm -2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb -bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R3 O=Google Trust Services LLC -# Subject: CN=GTS Root R3 O=Google Trust Services LLC -# Label: "GTS Root R3" -# Serial: 159662495401136852707857743206 -# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 -# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 -# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G -jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 -4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 -VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm -ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R4 O=Google Trust Services LLC -# Subject: CN=GTS Root R4 O=Google Trust Services LLC -# Label: "GTS Root R4" -# Serial: 159662532700760215368942768210 -# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 -# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 -# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi -QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR -HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D -9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 -p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD ------END CERTIFICATE----- - -# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj -# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj -# Label: "Telia Root CA v2" -# Serial: 7288924052977061235122729490515358 -# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 -# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd -# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx -CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE -AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 -NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ -MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq -AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 -vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 -lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD -n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT -7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o -6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC -TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 -WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R -DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI -pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj -YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy -rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ -8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi -0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM -A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS -SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K -TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF -6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er -3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt -Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT -VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW -ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA -rBPuUBQemMc= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 1 2020" -# Serial: 165870826978392376648679885835942448534 -# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed -# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 -# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 -NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS -zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 -QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ -VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW -wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV -dWNbFJWcHwHP2NVypw87 ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 1 2020" -# Serial: 126288379621884218666039612629459926992 -# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e -# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 -# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 -NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC -/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD -wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 -OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA -y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb -gfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS ECC P384 Root G5" -# Serial: 13129116028163249804115411775095713523 -# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed -# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee -# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 ------BEGIN CERTIFICATE----- -MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp -Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 -MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ -bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS -7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp -0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS -B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 -BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ -LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 -DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS RSA4096 Root G5" -# Serial: 11930366277458970227240571539258396554 -# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 -# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 -# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN -MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT -HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN -NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs -IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ -ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 -2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp -wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM -pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD -nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po -sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx -Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd -Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX -KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe -XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL -tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv -TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN -AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw -GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H -PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF -O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ -REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik -AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv -/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ -p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw -MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF -qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK -ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root R1 O=Certainly -# Subject: CN=Certainly Root R1 O=Certainly -# Label: "Certainly Root R1" -# Serial: 188833316161142517227353805653483829216 -# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 -# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af -# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw -PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy -dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 -YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 -1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT -vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed -aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 -1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 -r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 -cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ -wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ -6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA -2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH -Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR -eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB -/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u -d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr -PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d -8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi -1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd -rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di -taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 -lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj -yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn -Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy -yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n -wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 -OV+KmalBWQewLK8= ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root E1 O=Certainly -# Subject: CN=Certainly Root E1 O=Certainly -# Label: "Certainly Root E1" -# Serial: 8168531406727139161245376702891150584 -# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 -# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b -# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 ------BEGIN CERTIFICATE----- -MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw -CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu -bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ -BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s -eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK -+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 -QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 -hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm -ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG -BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE----- - -# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Label: "Security Communication ECC RootCA1" -# Serial: 15446673492073852651 -# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 -# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 -# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 ------BEGIN CERTIFICATE----- -MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT -AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD -VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx -NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT -HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 -IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl -dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK -ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu -9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O -be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA1" -# Serial: 113562791157148395269083148143378328608 -# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 -# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a -# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU -MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI -T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz -MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF -SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh -bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z -xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ -spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 -58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR -at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll -5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq -nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK -V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ -pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO -z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn -jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ -WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF -7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 -YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli -awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u -+2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 -X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN -SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo -P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI -+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz -znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 -eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 -YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy -r/6zcCwupvI= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA2" -# Serial: 58605626836079930195615843123109055211 -# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c -# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 -# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 ------BEGIN CERTIFICATE----- -MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw -CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ -VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy -MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ -TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS -b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B -IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ -+kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK -sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA -94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B -43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root E46" -# Serial: 88989738453351742415770396670917916916 -# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 -# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a -# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 ------BEGIN CERTIFICATE----- -MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw -CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T -ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN -MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG -A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT -ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC -WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ -6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B -Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa -qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q -4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root R46" -# Serial: 156256931880233212765902055439220583700 -# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 -# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 -# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 ------BEGIN CERTIFICATE----- -MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf -MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD -Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw -HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY -MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp -YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa -ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz -SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf -iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X -ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 -IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS -VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE -SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu -+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt -8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L -HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt -zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P -AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c -mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ -YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 -gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA -Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB -JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX -DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui -TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 -dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 -LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp -0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY -QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS RSA Root CA 2022" -# Serial: 148535279242832292258835760425842727825 -# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da -# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca -# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed ------BEGIN CERTIFICATE----- -MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO -MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD -DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX -DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw -b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP -L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY -t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins -S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 -PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO -L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 -R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w -dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS -+YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS -d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG -AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f -gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z -NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt -hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM -QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf -R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ -DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW -P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy -lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq -bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w -AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q -r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji -Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU -98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS ECC Root CA 2022" -# Serial: 26605119622390491762507526719404364228 -# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 -# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 -# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 ------BEGIN CERTIFICATE----- -MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT -U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 -MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh -dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm -acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN -SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME -GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW -uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp -15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN -b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA ECC TLS 2021" -# Serial: 81873346711060652204712539181482831616 -# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 -# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd -# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 ------BEGIN CERTIFICATE----- -MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w -LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w -CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 -MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF -Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI -zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X -tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 -AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 -KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD -aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu -CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo -9H1/IISpQuQo ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA RSA TLS 2021" -# Serial: 111436099570196163832749341232207667876 -# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 -# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 -# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f ------BEGIN CERTIFICATE----- -MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM -MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx -MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 -MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD -QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z -4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv -Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ -kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs -GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln -nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh -3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD -0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy -geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 -ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB -c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI -pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU -dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB -DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS -4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs -o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ -qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw -xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM -rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 -AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR -0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY -o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 -dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE -oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G3" -# Serial: 576386314500428537169965010905813481816650257167 -# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 -# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 -# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 ------BEGIN CERTIFICATE----- -MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM -BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe -Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw -IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU -cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS -T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK -AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 -nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep -qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA -yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs -hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX -zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv -kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT -f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA -uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB -o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih -MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E -BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 -wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 -XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 -JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j -ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV -VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx -xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on -AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d -7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj -gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV -+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo -FGWsJwt0ivKH ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G4" -# Serial: 451799571007117016466790293371524403291602933463 -# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb -# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a -# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c ------BEGIN CERTIFICATE----- -MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw -WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y -MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD -VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz -dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx -s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw -LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij -YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD -pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE -AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR -UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj -/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS ECC Root 2020" -# Serial: 72082518505882327255703894282316633856 -# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd -# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec -# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 ------BEGIN CERTIFICATE----- -MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw -CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH -bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw -MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx -JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE -AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O -tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP -f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA -MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di -z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn -27iQ7t0l ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS RSA Root 2023" -# Serial: 44676229530606711399881795178081572759 -# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 -# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 -# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj -MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 -eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy -MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC -REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG -A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 -cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV -cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA -U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 -Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug -BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy -8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J -co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg -8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 -rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 -mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg -+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX -gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 -p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ -pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm -9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw -M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd -GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ -CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t -xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ -w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK -L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj -X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q -ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm -dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= ------END CERTIFICATE----- - -# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA CYBER Root CA" -# Serial: 85076849864375384482682434040119489222 -# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 -# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 -# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ -MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 -IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 -WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO -LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg -Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P -40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF -avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ -34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i -JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu -j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf -Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP -2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA -S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA -oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC -kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW -5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd -BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB -AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t -tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn -68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn -TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t -RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx -f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI -Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz -8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 -NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX -xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 -t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA14" -# Serial: 575790784512929437950770173562378038616896959179 -# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 -# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f -# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM -BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u -LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw -NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD -eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS -b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ -FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg -vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy -6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo -/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J -kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ -0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib -y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac -18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs -0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB -SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL -ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk -86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E -rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib -ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT -zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS -DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 -2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo -FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy -K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 -dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl -Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB -365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c -JRNItX+S ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA15" -# Serial: 126083514594751269499665114766174399806381178503 -# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 -# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d -# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a ------BEGIN CERTIFICATE----- -MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw -UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM -dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy -NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl -cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 -IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 -wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR -ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT -9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp -4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 -bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 2 2023" -# Serial: 153168538924886464690566649552453098598 -# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 -# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 -# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw -OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr -i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE -gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 -k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT -Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl -2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U -cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP -/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS -uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ -0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N -DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ -XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 -GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI -FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n -riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR -VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc -LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn -4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD -hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG -koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 -ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS -Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 -knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ -hJ65bvspmZDogNOfJA== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS ECC Root CA" -# Serial: 310892014698942880364840003424242768478804666567 -# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c -# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 -# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 ------BEGIN CERTIFICATE----- -MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw -WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw -NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE -ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB -c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ -AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp -guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw -DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 -L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR -OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS RSA Root CA" -# Serial: 160405846464868906657516898462547310235378010780 -# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 -# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa -# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 ------BEGIN CERTIFICATE----- -MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM -BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN -MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG -A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 -c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC -AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ -NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ -Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 -HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 -ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb -xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX -i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ -UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j -TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT -bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 -S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT -MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 -Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 -iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt -7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp -2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ -g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj -pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M -pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP -XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe -SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 -ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy -323imttUQ/hHWKNddBWcwauwxzQ= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 2 2023" -# Serial: 139766439402180512324132425437959641711 -# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 -# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b -# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw -OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK -F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE -7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe -EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 -lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb -RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV -jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc -jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx -TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ -ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk -hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF -NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH -kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 -QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 -pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q -3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU -t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX -cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 -ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT -2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs -7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP -gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst -Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh -XBxvWHZks/wCuPWdCg== ------END CERTIFICATE----- - -# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Label: "SwissSign RSA TLS Root CA 2022 - 1" -# Serial: 388078645722908516278762308316089881486363258315 -# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 -# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce -# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 ------BEGIN CERTIFICATE----- -MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE -AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx -MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT -d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg -MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX -vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 -LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX -5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE -EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt -/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x -0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 -KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM -0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd -OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta -clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK -wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 -DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL -BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 -10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz -Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ -iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc -gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM -ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF -LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp -zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td -Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 -rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO -gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Label: "OISTE Server Root ECC G1" -# Serial: 47819833811561661340092227008453318557 -# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 -# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 -# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 ------BEGIN CERTIFICATE----- -MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw -CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY -T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy -NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp -b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy -cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N -2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 -TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C -tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR -QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD -YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Label: "OISTE Server Root RSA G1" -# Serial: 113845518112613905024960613408179309848 -# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 -# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e -# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL -MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE -AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 -MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k -YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM -vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b -rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk -ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z -O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R -tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS -jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh -sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho -mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu -+zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR -i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT -kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU -8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 -zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 -I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG -5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 -qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP -AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk -gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs -YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 -9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome -/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 -J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 -wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy -BiElxky8j3C7DOReIoMt0r7+hVu05L0= ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Label: "e-Szigno TLS Root CA 2023" -# Serial: 71934828665710877219916191754 -# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 -# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe -# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 ------BEGIN CERTIFICATE----- -MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU -TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow -dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy -b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T -emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE -AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS -AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v -SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K -ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI -zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt -y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl -C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 -uWWL ------END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py deleted file mode 100644 index 1c9661cc7c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/core.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -certifi.py -~~~~~~~~~~ - -This module returns the installation location of cacert.pem or its contents. -""" -import sys -import atexit - -def exit_cacert_ctx() -> None: - _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] - - -if sys.version_info >= (3, 11): - - from importlib.resources import as_file, files - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the file - # in cases where we're inside of a zipimport situation until someone - # actually calls where(), but we don't want to re-extract the file - # on every call of where(), so we'll do it once then store it in a - # global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you to - # manage the cleanup of this file, so it doesn't actually return a - # path, it returns a context manager that will give you the path - # when you enter it and will do any cleanup when you leave it. In - # the common case of not needing a temporary file, it will just - # return the file system location and the __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") - -else: - - from importlib.resources import path as get_path, read_text - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the - # file in cases where we're inside of a zipimport situation until - # someone actually calls where(), but we don't want to re-extract - # the file on every call of where(), so we'll do it once then store - # it in a global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you - # to manage the cleanup of this file, so it doesn't actually - # return a path, it returns a context manager that will give - # you the path when you enter it and will do any cleanup when - # you leave it. In the common case of not needing a temporary - # file, it will just return the file system location and the - # __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = get_path("certifi", "cacert.pem") - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/certifi/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py deleted file mode 100644 index 8ce8d739c9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from sentry_sdk import metrics, profiler - -from sentry_sdk.scope import Scope # isort: skip -from sentry_sdk.client import Client # isort: skip -from sentry_sdk.consts import VERSION -from sentry_sdk.transport import HttpTransport, Transport - -from sentry_sdk.api import * # noqa # isort: skip - -__all__ = [ # noqa - "Hub", - "Scope", - "Client", - "Transport", - "HttpTransport", - "VERSION", - "integrations", - # From sentry_sdk.api - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "logger", - "metrics", - "profiler", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - -# Initialize the debug support after everything is loaded -from sentry_sdk.debug import init_debug_support - -init_debug_support() -del init_debug_support - -# circular imports -from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py deleted file mode 100644 index 565fac2a2d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_batcher.py +++ /dev/null @@ -1,184 +0,0 @@ -import os -import random -import threading -import weakref -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Generic, TypeVar - -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - -T = TypeVar("T") - - -class Batcher(Generic[T]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "" - CONTENT_TYPE = "" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - self._buffer: "list[T]" = [] - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._flush_event: "threading.Event" = threading.Event() - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._buffer = [] - self._running = True - self._lock = threading.Lock() - self._active = threading.local() - self._flush_event = threading.Event() - self._flusher = None - self._flusher_pid = None - - def _ensure_thread(self) -> bool: - """For forking processes we might need to restart this thread. - This ensures that our process actually has that thread running. - """ - if not self._running: - return False - - pid = os.getpid() - if self._flusher_pid == pid: - return True - - with self._lock: - # Recheck to make sure another thread didn't get here and start the - # the flusher in the meantime - if self._flusher_pid == pid: - return True - - self._flusher_pid = pid - - self._flusher = threading.Thread(target=self._flush_loop) - self._flusher.daemon = True - - try: - self._flusher.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return False - - return True - - def _flush_loop(self) -> None: - # Mark the flush-loop thread as active for its entire lifetime so - # that any re-entrant add() triggered by GC warnings during wait(), - # flush(), or Event operations is silently dropped instead of - # deadlocking on internal locks. - self._active.flag = True - while self._running: - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) - self._flush_event.clear() - self._flush() - - def add(self, item: "T") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - if len(self._buffer) >= self.MAX_BEFORE_DROP: - self._record_lost(item) - return None - - self._buffer.append(item) - if len(self._buffer) >= self.MAX_BEFORE_FLUSH: - self._flush_event.set() - finally: - self._active.flag = False - - def kill(self) -> None: - if self._flusher is None: - return - - self._running = False - self._flush_event.set() - self._flusher = None - - def flush(self) -> None: - was_active = getattr(self._active, "flag", False) - self._active.flag = True - try: - self._flush() - finally: - self._active.flag = was_active - - def _add_to_envelope(self, envelope: "Envelope") -> None: - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": len(self._buffer), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(item) for item in self._buffer - ], - } - ), - ) - ) - - def _flush(self) -> "Optional[Envelope]": - envelope = Envelope( - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} - ) - with self._lock: - if len(self._buffer) == 0: - return None - - self._add_to_envelope(envelope) - self._buffer.clear() - - self._capture_func(envelope) - return envelope - - def _record_lost(self, item: "T") -> None: - pass - - @staticmethod - def _to_transport_format(item: "T") -> "Any": - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py deleted file mode 100644 index f62175c09f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_compat.py +++ /dev/null @@ -1,92 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, TypeVar - - T = TypeVar("T") - - -PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 -PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 -PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 -PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 - - -def with_metaclass(meta: "Any", *bases: "Any") -> "Any": - class MetaClass(type): - def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": - return meta(name, bases, d) - - return type.__new__(MetaClass, "temporary_class", (), {}) - - -def check_uwsgi_thread_support() -> bool: - # We check two things here: - # - # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if - # that's the case. - # - # 2. Additionally, if uWSGI is running in preforking mode (default), it needs - # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This - # is because any background threads spawned before the main process is - # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if - # --enable-threads is on. One has to explicitly provide - # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython - # after-fork hooks that take care of cleaning up stale thread data. - try: - from uwsgi import opt # type: ignore - except ImportError: - return True - - from sentry_sdk.consts import FALSE_VALUES - - def enabled(option: str) -> bool: - value = opt.get(option, False) - if isinstance(value, bool): - return value - - if isinstance(value, bytes): - try: - value = value.decode() - except Exception: - pass - - return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] - - # When `threads` is passed in as a uwsgi option, - # `enable-threads` is implied on. - threads_enabled = "threads" in opt or enabled("enable-threads") - fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") - lazy_mode = enabled("lazy-apps") or enabled("lazy") - - if lazy_mode and not threads_enabled: - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI without thread support. " - "This might lead to unexpected issues. " - 'Please run uWSGI with "--enable-threads" for full support.' - ) - ) - - return False - - elif not lazy_mode and (not threads_enabled or not fork_hooks_on): - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI in preforking mode without " - "thread support. This might lead to crashing workers. " - 'Please run uWSGI with both "--enable-threads" and ' - '"--py-call-uwsgi-fork-hooks" for full support.' - ) - ) - - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py deleted file mode 100644 index 923fcf6df8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_init_implementation.py +++ /dev/null @@ -1,78 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -import sentry_sdk - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional - - import sentry_sdk.consts - - -class _InitGuard: - _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( - "Using the return value of sentry_sdk.init as a context manager " - "and manually calling the __enter__ and __exit__ methods on the " - "return value are deprecated. We are no longer maintaining this " - "functionality, and we will remove it in the next major release." - ) - - def __init__(self, client: "sentry_sdk.Client") -> None: - self._client = client - - def __enter__(self) -> "_InitGuard": - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - c = self._client - if c is not None: - c.close() - - -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass - - -def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": - """Initializes the SDK and optionally integrations. - - This takes the same arguments as the client constructor. - """ - client = sentry_sdk.Client(*args, **kwargs) - sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() - rv = _InitGuard(client) - return rv - - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `init` is a type to - # have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `ContextManager[Any]` to tell static analyzers about the return type. - - class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 - pass - -else: - # Alias `init` for actual usage. Go through the lambda indirection to throw - # PyCharm off of the weakly typed signature (it would otherwise discover - # both the weakly typed signature of `_init` and our faked `init` type). - - init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py deleted file mode 100644 index 0719932ee9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_log_batcher.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Item, PayloadRef -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Log - - -class LogBatcher(Batcher["Log"]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "log" - CONTENT_TYPE = "application/vnd.sentry.items.log+json" - - @staticmethod - def _to_transport_format(item: "Log") -> "Any": - if "sentry.severity_number" not in item["attributes"]: - item["attributes"]["sentry.severity_number"] = item["severity_number"] - if "sentry.severity_text" not in item["attributes"]: - item["attributes"]["sentry.severity_text"] = item["severity_text"] - - res = { - "timestamp": int(item["time_unix_nano"]) / 1.0e9, - "level": str(item["severity_text"]), - "body": str(item["body"]), - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - return res - - def _record_lost(self, item: "Log") -> None: - # Construct log envelope item without sending it to report lost bytes - log_item = Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": 1, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [self._to_transport_format(item)], - } - ), - ) - - self._record_lost_func( - reason="queue_overflow", - data_category="log_item", - item=log_item, - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py deleted file mode 100644 index 16c238bcab..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_lru_cache.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -_SENTINEL = object() - - -class LRUCache: - def __init__(self, max_size: int) -> None: - if max_size <= 0: - raise AssertionError(f"invalid max_size: {max_size}") - self.max_size = max_size - self._data: "dict[Any, Any]" = {} - self.hits = self.misses = 0 - self.full = False - - def set(self, key: "Any", value: "Any") -> None: - current = self._data.pop(key, _SENTINEL) - if current is not _SENTINEL: - self._data[key] = value - elif self.full: - self._data.pop(next(iter(self._data))) - self._data[key] = value - else: - self._data[key] = value - self.full = len(self._data) >= self.max_size - - def get(self, key: "Any", default: "Any" = None) -> "Any": - try: - ret = self._data.pop(key) - except KeyError: - self.misses += 1 - ret = default - else: - self.hits += 1 - self._data[key] = ret - - return ret - - def get_all(self) -> "list[tuple[Any, Any]]": - return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py deleted file mode 100644 index 06bce1a282..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_metrics_batcher.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Metric - - -class MetricsBatcher(Batcher["Metric"]): - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 10_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "trace_metric" - CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" - - @staticmethod - def _to_transport_format(item: "Metric") -> "Any": - res = { - "timestamp": item["timestamp"], - "name": item["name"], - "type": item["type"], - "value": item["value"], - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - if item.get("unit") is not None: - res["unit"] = item["unit"] - - return res - - def _record_lost(self, item: "Metric") -> None: - self._record_lost_func( - reason="queue_overflow", - data_category="trace_metric", - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py deleted file mode 100644 index c28c8de9ac..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_queue.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') -with Lock swapped out for RLock to avoid a deadlock while garbage collecting. - -https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py - - -See also -https://codewithoutrules.com/2017/08/16/concurrency-python/ -https://bugs.python.org/issue14976 -https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 - -We also vendor the code to evade eventlet's broken monkeypatching, see -https://github.com/getsentry/sentry-python/pull/484 - - -Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; - -All Rights Reserved - - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - -""" - -import threading -from collections import deque -from time import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - -__all__ = ["EmptyError", "FullError", "Queue"] - - -class EmptyError(Exception): - "Exception raised by Queue.get(block=0)/get_nowait()." - - pass - - -class FullError(Exception): - "Exception raised by Queue.put(block=0)/put_nowait()." - - pass - - -class Queue: - """Create a queue object with a given maximum size. - - If maxsize is <= 0, the queue size is infinite. - """ - - def __init__(self, maxsize=0): - self.maxsize = maxsize - self._init(maxsize) - - # mutex must be held whenever the queue is mutating. All methods - # that acquire mutex must release it before returning. mutex - # is shared between the three conditions, so acquiring and - # releasing the conditions also acquires and releases mutex. - self.mutex = threading.RLock() - - # Notify not_empty whenever an item is added to the queue; a - # thread waiting to get is notified then. - self.not_empty = threading.Condition(self.mutex) - - # Notify not_full whenever an item is removed from the queue; - # a thread waiting to put is notified then. - self.not_full = threading.Condition(self.mutex) - - # Notify all_tasks_done whenever the number of unfinished tasks - # drops to zero; thread waiting to join() is notified to resume - self.all_tasks_done = threading.Condition(self.mutex) - self.unfinished_tasks = 0 - - def task_done(self): - """Indicate that a formerly enqueued task is complete. - - Used by Queue consumer threads. For each get() used to fetch a task, - a subsequent call to task_done() tells the queue that the processing - on the task is complete. - - If a join() is currently blocking, it will resume when all items - have been processed (meaning that a task_done() call was received - for every item that had been put() into the queue). - - Raises a ValueError if called more times than there were items - placed in the queue. - """ - with self.all_tasks_done: - unfinished = self.unfinished_tasks - 1 - if unfinished <= 0: - if unfinished < 0: - raise ValueError("task_done() called too many times") - self.all_tasks_done.notify_all() - self.unfinished_tasks = unfinished - - def join(self): - """Blocks until all items in the Queue have been gotten and processed. - - The count of unfinished tasks goes up whenever an item is added to the - queue. The count goes down whenever a consumer thread calls task_done() - to indicate the item was retrieved and all work on it is complete. - - When the count of unfinished tasks drops to zero, join() unblocks. - """ - with self.all_tasks_done: - while self.unfinished_tasks: - self.all_tasks_done.wait() - - def qsize(self): - """Return the approximate size of the queue (not reliable!).""" - with self.mutex: - return self._qsize() - - def empty(self): - """Return True if the queue is empty, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() == 0 - as a direct substitute, but be aware that either approach risks a race - condition where a queue can grow before the result of empty() or - qsize() can be used. - - To create code that needs to wait for all queued tasks to be - completed, the preferred technique is to use the join() method. - """ - with self.mutex: - return not self._qsize() - - def full(self): - """Return True if the queue is full, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() >= n - as a direct substitute, but be aware that either approach risks a race - condition where a queue can shrink before the result of full() or - qsize() can be used. - """ - with self.mutex: - return 0 < self.maxsize <= self._qsize() - - def put(self, item, block=True, timeout=None): - """Put an item into the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until a free slot is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the FullError exception if no free slot was available within that time. - Otherwise ('block' is false), put an item on the queue if a free slot - is immediately available, else raise the FullError exception ('timeout' - is ignored in that case). - """ - with self.not_full: - if self.maxsize > 0: - if not block: - if self._qsize() >= self.maxsize: - raise FullError() - elif timeout is None: - while self._qsize() >= self.maxsize: - self.not_full.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while self._qsize() >= self.maxsize: - remaining = endtime - time() - if remaining <= 0.0: - raise FullError() - self.not_full.wait(remaining) - self._put(item) - self.unfinished_tasks += 1 - self.not_empty.notify() - - def get(self, block=True, timeout=None): - """Remove and return an item from the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until an item is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the EmptyError exception if no item was available within that time. - Otherwise ('block' is false), return an item if one is immediately - available, else raise the EmptyError exception ('timeout' is ignored - in that case). - """ - with self.not_empty: - if not block: - if not self._qsize(): - raise EmptyError() - elif timeout is None: - while not self._qsize(): - self.not_empty.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while not self._qsize(): - remaining = endtime - time() - if remaining <= 0.0: - raise EmptyError() - self.not_empty.wait(remaining) - item = self._get() - self.not_full.notify() - return item - - def put_nowait(self, item): - """Put an item into the queue without blocking. - - Only enqueue the item if a free slot is immediately available. - Otherwise raise the FullError exception. - """ - return self.put(item, block=False) - - def get_nowait(self): - """Remove and return an item from the queue without blocking. - - Only get an item if one is immediately available. Otherwise - raise the EmptyError exception. - """ - return self.get(block=False) - - # Override these methods to implement other queue organizations - # (e.g. stack or priority queue). - # These will only be called with appropriate locks held - - # Initialize the queue representation - def _init(self, maxsize): - self.queue: "Any" = deque() - - def _qsize(self): - return len(self.queue) - - # Put a new item in the queue - def _put(self, item): - self.queue.append(item) - - # Get an item from the queue - def _get(self): - return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py deleted file mode 100644 index 79285c3386..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_span_batcher.py +++ /dev/null @@ -1,234 +0,0 @@ -import os -import random -import threading -import time -import weakref -from collections import defaultdict -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp, serialize_attribute - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - from sentry_sdk._types import SpanJSON - - -class SpanBatcher(Batcher["SpanJSON"]): - # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is - # a bit of a buffer for spans that appear between the trigger to flush - # and actually flushing the buffer. - # - # The max limits are all per trace (per bucket). - MAX_ENVELOPE_SIZE = 1000 # spans - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 2000 - MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB - - FLUSH_WAIT_TIME = 5.0 - - TYPE = "span" - CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - # Spans from different traces cannot be emitted in the same envelope - # since the envelope contains a shared trace header. That's why we bucket - # by trace_id, so that we can then send the buckets each in its own - # envelope. - # trace_id -> span buffer - self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) - self._running_size: dict[str, int] = defaultdict(lambda: 0) - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._last_full_flush: float = time.monotonic() # drives time-based flushes - self._flush_event = threading.Event() - self._pending_flush: set[str] = set() # buckets to be flushed - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._span_buffer = defaultdict(list) - self._running_size = defaultdict(lambda: 0) - self._running = True - - self._lock = threading.Lock() - self._active = threading.local() - - self._last_full_flush = time.monotonic() - self._flush_event = threading.Event() - self._pending_flush = set() - - self._flusher = None - self._flusher_pid = None - - def _flush_loop(self) -> None: - self._active.flag = True - while self._running: - jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 - self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) - self._flush_event.clear() - - self._flush(only_pending=True) - - if ( - time.monotonic() - self._last_full_flush - >= self.FLUSH_WAIT_TIME + jitter - ): - self._flush() - self._last_full_flush = time.monotonic() - - def add(self, span: "SpanJSON") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - size = len(self._span_buffer[span["trace_id"]]) - if size >= self.MAX_BEFORE_DROP: - self._record_lost_func( - reason="queue_overflow", - data_category="span", - quantity=1, - ) - return None - - self._span_buffer[span["trace_id"]].append(span) - self._running_size[span["trace_id"]] += self._estimate_size(span) - - if ( - size + 1 >= self.MAX_BEFORE_FLUSH - or self._running_size[span["trace_id"]] - >= self.MAX_BYTES_BEFORE_FLUSH - ): - self._pending_flush.add(span["trace_id"]) - notify = True - else: - notify = False - - if notify: - self._flush_event.set() - finally: - self._active.flag = False - - @staticmethod - def _estimate_size(item: "SpanJSON") -> int: - # Rough estimate of serialized span size that's quick to compute. - # 210 is the rough size of the payload without attributes, and then we - # estimate the attributes separately. - estimate = 210 - for value in (item.get("attributes") or {}).values(): - estimate += 50 - - if isinstance(value, str): - estimate += len(value) - else: - estimate += len(str(value)) - - return estimate - - @staticmethod - def _to_transport_format(item: "SpanJSON") -> "Any": - res = {k: v for k, v in item.items() if k not in ("_segment_span",)} - - if item.get("attributes"): - res["attributes"] = { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - } - else: - del res["attributes"] - - return res - - def _flush(self, only_pending: bool = False) -> None: - with self._lock: - if only_pending: - buckets = list(self._pending_flush) - else: - # flush whole buffer, e.g. if the SDK is shutting down - buckets = list(self._span_buffer.keys()) - - self._pending_flush.clear() - - if not buckets: - return - - envelopes = [] - - for bucket_id in buckets: - spans = self._span_buffer.get(bucket_id) - if not spans: - continue - - dsc = spans[0]["_segment_span"]._dynamic_sampling_context() - - # Max per envelope is 1000, so if we happen to have more than - # 1000 spans in one bucket, we'll need to separate them. - for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): - end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) - - envelope = Envelope( - headers={ - "sent_at": format_timestamp(datetime.now(timezone.utc)), - "trace": dsc, - } - ) - - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": end - start, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(spans[j]) - for j in range(start, end) - ], - } - ), - ) - ) - - envelopes.append(envelope) - - del self._span_buffer[bucket_id] - del self._running_size[bucket_id] - - for envelope in envelopes: - self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py deleted file mode 100644 index fbd2578048..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_types.py +++ /dev/null @@ -1,481 +0,0 @@ -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING, TypeVar, Union - -# Re-exported for compat, since code out there in the wild might use this variable. -MYPY = TYPE_CHECKING - - -SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" -BLOB_DATA_SUBSTITUTE = "[Blob substitute]" -OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" -UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" - - -class AnnotatedValue: - """ - Meta information for a data field in the event payload. - This is to tell Relay that we have tampered with the fields value. - See: - https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 - """ - - __slots__ = ("value", "metadata") - - def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: - self.value = value - self.metadata = metadata - - def __eq__(self, other: "Any") -> bool: - if not isinstance(other, AnnotatedValue): - return False - - return self.value == other.value and self.metadata == other.metadata - - def __str__(self: "AnnotatedValue") -> str: - return str({"value": str(self.value), "metadata": str(self.metadata)}) - - def __len__(self: "AnnotatedValue") -> int: - if self.value is not None: - return len(self.value) - else: - return 0 - - @classmethod - def removed_because_raw_data(cls) -> "AnnotatedValue": - """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" - # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition - # to span-first - return AnnotatedValue( - value="", - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_raw_data(cls) -> "AnnotatedValue": - """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" - return AnnotatedValue( - value=UNPARSABLE_RAW_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": - """ - The actual value was removed because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition - # to span-first - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_over_size_limit( - cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE - ) -> "AnnotatedValue": - """ - The actual value was replaced because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": - """The actual value was removed because it contained sensitive information.""" - return AnnotatedValue( - value=SENSITIVE_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) - "s", # The fields original value was substituted - ] - ] - }, - ) - - -T = TypeVar("T") -Annotated = Union[AnnotatedValue, T] - - -if TYPE_CHECKING: - from collections.abc import Container, MutableMapping, Sequence - from datetime import datetime - from types import TracebackType - from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - - class SDKInfo(TypedDict): - name: str - version: str - packages: "Sequence[Mapping[str, str]]" - - class KeyValueCollectionBehaviour(TypedDict): - mode: 'Literal["off", "denylist", "allowlist"]' - 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): - request: "KeyValueCollectionBehaviour" - - class HttpHeadersCollectionBehaviour(TypedDict): - request: "KeyValueCollectionBehaviour" - - class DataCollectionUserOptions(TypedDict, total=False): - user_info: bool - cookies: "KeyValueCollectionBehaviour" - http_headers: "HttpHeadersCollectionUserOptions" - http_bodies: "List[str]" - query_params: "KeyValueCollectionBehaviour" - graphql: "GraphQLCollectionUserOptions" - gen_ai: "GenAICollectionUserOptions" - database_query_data: bool - queues: bool - 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_query_data: bool - queues: bool - 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"] - - DurationUnit = Literal[ - "nanosecond", - "microsecond", - "millisecond", - "second", - "minute", - "hour", - "day", - "week", - ] - - InformationUnit = Literal[ - "bit", - "byte", - "kilobyte", - "kibibyte", - "megabyte", - "mebibyte", - "gigabyte", - "gibibyte", - "terabyte", - "tebibyte", - "petabyte", - "pebibyte", - "exabyte", - "exbibyte", - ] - - FractionUnit = Literal["ratio", "percent"] - MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] - - MeasurementValue = TypedDict( - "MeasurementValue", - { - "value": float, - "unit": NotRequired[Optional[MeasurementUnit]], - }, - ) - - Event = TypedDict( - "Event", - { - "breadcrumbs": Annotated[ - dict[Literal["values"], list[dict[str, Any]]] - ], # TODO: We can expand on this type - "check_in_id": str, - "contexts": dict[str, dict[str, object]], - "dist": str, - "duration": Optional[float], - "environment": Optional[str], - "errors": list[dict[str, Any]], # TODO: We can expand on this type - "event_id": str, - "exception": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "extra": MutableMapping[str, object], - "fingerprint": list[str], - "level": LogLevelStr, - "logentry": Mapping[str, object], - "logger": str, - "measurements": dict[str, MeasurementValue], - "message": str, - "modules": dict[str, str], - "monitor_config": Mapping[str, object], - "monitor_slug": Optional[str], - "platform": Literal["python"], - "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports - "release": Optional[str], - "request": dict[str, object], - "sdk": Mapping[str, object], - "server_name": str, - "spans": Annotated[list[dict[str, object]]], - "stacktrace": dict[ - str, object - ], # We access this key in the code, but I am unsure whether we ever set it - "start_timestamp": datetime, - "status": Optional[str], - "tags": MutableMapping[ - str, str - ], # Tags must be less than 200 characters each - "threads": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "timestamp": Optional[datetime], # Must be set before sending the event - "transaction": str, - "transaction_info": Mapping[str, Any], # TODO: We can expand on this type - "type": Literal["check_in", "transaction"], - "user": dict[str, object], - "_dropped_spans": int, - "_has_gen_ai_span": bool, - }, - total=False, - ) - - ExcInfo = Union[ - tuple[Type[BaseException], BaseException, Optional[TracebackType]], - tuple[None, None, None], - ] - - # TODO: Make a proper type definition for this (PRs welcome!) - Hint = Dict[str, Any] - - AttributeValue = ( - str - | bool - | float - | int - | list[str] - | list[bool] - | list[float] - | list[int] - | tuple[str, ...] - | tuple[bool, ...] - | tuple[float, ...] - | tuple[int, ...] - ) - Attributes = dict[str, AttributeValue] - - SerializedAttributeValue = TypedDict( - # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types - "SerializedAttributeValue", - { - "type": Literal[ - "string", - "boolean", - "double", - "integer", - "array", - ], - "value": AttributeValue, - }, - ) - - Log = TypedDict( - "Log", - { - "severity_text": str, - "severity_number": int, - "body": str, - "attributes": Attributes, - "time_unix_nano": int, - "trace_id": Optional[str], - "span_id": Optional[str], - }, - ) - - MetricType = Literal["counter", "gauge", "distribution"] - MetricUnit = Union[DurationUnit, InformationUnit, str] - - Metric = TypedDict( - "Metric", - { - "timestamp": float, - "trace_id": Optional[str], - "span_id": Optional[str], - "name": str, - "type": MetricType, - "value": float, - "unit": Optional[MetricUnit], - "attributes": Attributes, - }, - ) - - MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] - - SpanJSON = TypedDict( - "SpanJSON", - { - "trace_id": str, - "span_id": str, - "parent_span_id": NotRequired[str], - "name": str, - "status": str, - "is_segment": bool, - "start_timestamp": float, - "end_timestamp": NotRequired[float], - "attributes": NotRequired[Attributes], - "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], - }, - ) - - # TODO: Make a proper type definition for this (PRs welcome!) - Breadcrumb = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - BreadcrumbHint = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - SamplingContext = Dict[str, Any] - - EventProcessor = Callable[[Event, Hint], Optional[Event]] - ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] - BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] - TransactionProcessor = Callable[[Event, Hint], Optional[Event]] - LogProcessor = Callable[[Log, Hint], Optional[Log]] - - TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] - - # https://github.com/python/mypy/issues/5710 - NotImplementedType = Any - - EventDataCategory = Literal[ - "default", - "error", - "crash", - "transaction", - "security", - "attachment", - "session", - "internal", - "profile", - "profile_chunk", - "monitor", - "span", - "log_item", - "log_byte", - "trace_metric", - ] - SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] - - ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] - ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] - - MonitorConfigScheduleType = Literal["crontab", "interval"] - MonitorConfigScheduleUnit = Literal[ - "year", - "month", - "week", - "day", - "hour", - "minute", - "second", # not supported in Sentry and will result in a warning - ] - - MonitorConfigSchedule = TypedDict( - "MonitorConfigSchedule", - { - "type": MonitorConfigScheduleType, - "value": Union[int, str], - "unit": MonitorConfigScheduleUnit, - }, - total=False, - ) - - MonitorConfig = TypedDict( - "MonitorConfig", - { - "schedule": MonitorConfigSchedule, - "timezone": str, - "checkin_margin": int, - "max_runtime": int, - "failure_issue_threshold": int, - "recovery_threshold": int, - "owner": str, - }, - total=False, - ) - - HttpStatusCodeRange = Union[int, Container[int]] - - class TextPart(TypedDict): - type: Literal["text"] - content: str - - IgnoreSpansName = Union[str, Pattern[str]] - IgnoreSpansContext = TypedDict( - "IgnoreSpansContext", - {"name": IgnoreSpansName, "attributes": Attributes}, - total=False, - ) - IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py deleted file mode 100644 index 98f932267f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/_werkzeug.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Copyright (c) 2007 by the Pallets team. - -Some rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, Iterator, Optional, Tuple - - -# -# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` -# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 -# -# We need this function because Django does not give us a "pure" http header -# dict. So we might as well use it for all WSGI integrations. -# -def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns only proper HTTP headers. - """ - for key, value in environ.items(): - key = str(key) - if key.startswith("HTTP_") and key not in ( - "HTTP_CONTENT_TYPE", - "HTTP_CONTENT_LENGTH", - ): - yield key[5:].replace("_", "-").title(), value - elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): - yield key.replace("_", "-").title(), value - - -def _strip_default_port(host: str, scheme: "Optional[str]") -> str: - """Strip the port from the host if it's the default for the scheme.""" - if scheme == "http" and host.endswith(":80"): - return host[:-3] - if scheme == "https" and host.endswith(":443"): - return host[:-4] - return host - - -# `get_host` comes from `werkzeug.wsgi.get_host` -# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 - - -def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: - """ - Return the host for the given WSGI environment. - """ - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: - return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) - elif environ.get("HTTP_HOST"): - return _strip_default_port(environ["HTTP_HOST"], scheme) - elif environ.get("SERVER_NAME"): - # SERVER_NAME/SERVER_PORT describe the internal server, so use - # wsgi.url_scheme (not the forwarded scheme) for port decisions. - rv = environ["SERVER_NAME"] - if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( - ("https", "443"), - ("http", "80"), - ): - rv += ":" + environ["SERVER_PORT"] - return rv - else: - # In spite of the WSGI spec, SERVER_NAME might not be present. - return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py deleted file mode 100644 index 404e57ff1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .utils import ( - GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 - GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 - normalize_message_role, # noqa: F401 - normalize_message_roles, # noqa: F401 - set_conversation_id, # noqa: F401 - set_data_normalized, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py deleted file mode 100644 index b5eb8c55ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_completions_api.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections.abc import Iterable -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.chat import ( - ChatCompletionContentPartParam, - ChatCompletionMessageParam, - ChatCompletionSystemMessageParam, - ) - - from sentry_sdk._types import TextPart - - -def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: - return isinstance(message, dict) and message.get("role") == "system" - - -def _get_system_instructions( - messages: "Iterable[ChatCompletionMessageParam]", -) -> "list[ChatCompletionMessageParam]": - if not isinstance(messages, Iterable): - return [] - - return [message for message in messages if _is_system_instruction(message)] - - -def _get_text_items( - content: "Union[str, Iterable[ChatCompletionContentPartParam]]", -) -> "list[str]": - if isinstance(content, str): - return [content] - - if not isinstance(content, Iterable): - return [] - - text_items = [] - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - text = part.get("text", None) - if text is not None: - text_items.append(text) - - return text_items - - -def _transform_system_instructions( - system_instructions: "list[ChatCompletionSystemMessageParam]", -) -> "list[TextPart]": - instruction_text_parts: "list[TextPart]" = [] - - for instruction in system_instructions: - if not isinstance(instruction, dict): - continue - - content = instruction.get("content") - if content is None: - continue - - text_parts: "list[TextPart]" = [ - {"type": "text", "content": text} for text in _get_text_items(content) - ] - instruction_text_parts += text_parts - - return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py deleted file mode 100644 index 8f751c3248..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/_openai_responses_api.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.responses import ResponseInputItemParam, ResponseInputParam - - -def _is_system_instruction(message: "ResponseInputItemParam") -> bool: - if not isinstance(message, dict) or not message.get("role") == "system": - return False - - return "type" not in message or message["type"] == "message" - - -def _get_system_instructions( - messages: "Union[str, ResponseInputParam]", -) -> "list[ResponseInputItemParam]": - if not isinstance(messages, list): - return [] - - return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py deleted file mode 100644 index 35ee4bd788..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/consts.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - -# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." -DATA_URL_BASE64_REGEX = re.compile( - r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py deleted file mode 100644 index d8840ad451..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/monitoring.py +++ /dev/null @@ -1,147 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk.utils -from sentry_sdk import start_span -from sentry_sdk.ai.utils import _set_span_data_attribute -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, TypeVar, Union - - F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) - -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) - - -def set_ai_pipeline_name(name: "Optional[str]") -> None: - _ai_pipeline_name.set(name) - - -def get_ai_pipeline_name() -> "Optional[str]": - return _ai_pipeline_name.get() - - -def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": - def decorator(f: "F") -> "F": - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return await f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = await f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - if inspect.iscoroutinefunction(f): - return wraps(f)(async_wrapped) # type: ignore - else: - return wraps(f)(sync_wrapped) # type: ignore - - return decorator - - -def record_token_usage( - span: "Union[Span, StreamedSpan]", - input_tokens: "Optional[int]" = None, - input_tokens_cached: "Optional[int]" = None, - input_tokens_cache_write: "Optional[int]" = None, - output_tokens: "Optional[int]" = None, - output_tokens_reasoning: "Optional[int]" = None, - total_tokens: "Optional[int]" = None, -) -> None: - # TODO: move pipeline name elsewhere - ai_pipeline_name = get_ai_pipeline_name() - if ai_pipeline_name: - _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) - - if input_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if input_tokens_cached is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - input_tokens_cached, - ) - - if input_tokens_cache_write is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - input_tokens_cache_write, - ) - - if output_tokens is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens - ) - - if output_tokens_reasoning is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - output_tokens_reasoning, - ) - - if total_tokens is None and input_tokens is not None and output_tokens is not None: - total_tokens = input_tokens + output_tokens - - if total_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py deleted file mode 100644 index 7b1ca9324b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/ai/utils.py +++ /dev/null @@ -1,782 +0,0 @@ -import inspect -import json -from copy import deepcopy -from typing import TYPE_CHECKING - -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Tuple, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import logger - -MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB -# Maximum characters when only a single message is left after bytes truncation -MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 - - -class GEN_AI_ALLOWED_MESSAGE_ROLES: - SYSTEM = "system" - USER = "user" - ASSISTANT = "assistant" - TOOL = "tool" - - -GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { - GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], - GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], - GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], - GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], -} - -GEN_AI_MESSAGE_ROLE_MAPPING = {} -for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): - for source_role in source_roles: - GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role - - -def parse_data_uri(url: str) -> "Tuple[str, str]": - """ - Parse a data URI and return (mime_type, content). - - Data URI format (RFC 2397): data:[][;base64], - - Examples: - data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") - data:text/plain,Hello → ("text/plain", "Hello") - data:;base64,SGVsbG8= → ("", "SGVsbG8=") - - Raises: - ValueError: If the URL is not a valid data URI (missing comma separator) - """ - if "," not in url: - raise ValueError("Invalid data URI: missing comma separator") - - header, content = url.split(",", 1) - - # Extract mime type from header - # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" - # Remove "data:" prefix, then take everything before the first semicolon - if header.startswith("data:"): - mime_part = header[5:] # Remove "data:" prefix - else: - mime_part = header - - mime_type = mime_part.split(";")[0] - - return mime_type, content - - -def get_modality_from_mime_type(mime_type: str) -> str: - """ - Infer the content modality from a MIME type string. - - Args: - mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") - - Returns: - One of: "image", "audio", "video", or "document" - Defaults to "image" for unknown or empty MIME types. - - Examples: - "image/jpeg" -> "image" - "audio/mp3" -> "audio" - "video/mp4" -> "video" - "application/pdf" -> "document" - "text/plain" -> "document" - """ - if not mime_type: - return "image" # Default fallback - - mime_lower = mime_type.lower() - if mime_lower.startswith("image/"): - return "image" - elif mime_lower.startswith("audio/"): - return "audio" - elif mime_lower.startswith("video/"): - return "video" - elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): - return "document" - else: - return "image" # Default fallback for unknown types - - -def transform_openai_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an OpenAI/LiteLLM content part to Sentry's standardized format. - - This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. - - Input format: - - {"type": "image_url", "image_url": {"url": "..."}} - - {"type": "image_url", "image_url": "..."} (string shorthand) - - Output format (one of): - - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from OpenAI/LiteLLM - - Returns: - A transformed dictionary in standardized format, or None if the format - is not OpenAI image_url format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type != "image_url": - return None - - image_url_data = content_part.get("image_url") - if isinstance(image_url_data, str): - url = image_url_data - elif isinstance(image_url_data, dict): - url = image_url_data.get("url", "") - else: - return None - - if not url: - return None - - # Check if it's a data URI (base64 encoded) - if url.startswith("data:"): - try: - mime_type, content = parse_data_uri(url) - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": content, - } - except ValueError: - # If parsing fails, return as URI - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - else: - # Regular URL - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - - -def transform_anthropic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an Anthropic content part to Sentry's standardized format. - - This handles the Anthropic image and document formats with source dictionaries. - - Input format: - - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} - - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} - - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} - - {"type": "document", "source": {...}} (same source formats) - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from Anthropic - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Anthropic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "document") or "source" not in content_part: - return None - - source = content_part.get("source") - if not isinstance(source, dict): - return None - - source_type = source.get("type") - media_type = source.get("media_type", "") - modality = ( - "document" - if block_type == "document" - else get_modality_from_mime_type(media_type) - ) - - if source_type == "base64": - return { - "type": "blob", - "modality": modality, - "mime_type": media_type, - "content": source.get("data", ""), - } - elif source_type == "url": - return { - "type": "uri", - "modality": modality, - "mime_type": media_type, - "uri": source.get("url", ""), - } - elif source_type == "file": - return { - "type": "file", - "modality": modality, - "mime_type": media_type, - "file_id": source.get("file_id", ""), - } - - return None - - -def transform_google_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a Google GenAI content part to Sentry's standardized format. - - This handles the Google GenAI inline_data and file_data formats. - - Input format: - - {"inline_data": {"mime_type": "...", "data": "..."}} - - {"file_data": {"mime_type": "...", "file_uri": "..."}} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from Google GenAI - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Google format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Handle Google inline_data format - if "inline_data" in content_part: - inline_data = content_part.get("inline_data") - if isinstance(inline_data, dict): - mime_type = inline_data.get("mime_type", "") - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": inline_data.get("data", ""), - } - return None - - # Handle Google file_data format - if "file_data" in content_part: - file_data = content_part.get("file_data") - if isinstance(file_data, dict): - mime_type = file_data.get("mime_type", "") - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_data.get("file_uri", ""), - } - return None - - return None - - -def transform_generic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a generic/LangChain-style content part to Sentry's standardized format. - - This handles generic formats where the type indicates the modality and - the data is provided via direct base64, url, or file_id fields. - - Input format: - - {"type": "image", "base64": "...", "mime_type": "..."} - - {"type": "audio", "url": "...", "mime_type": "..."} - - {"type": "video", "base64": "...", "mime_type": "..."} - - {"type": "file", "file_id": "...", "mime_type": "..."} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part in generic format - - Returns: - A transformed dictionary in standardized format, or None if the format - is not generic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "audio", "video", "file"): - return None - - # Ensure it's not Anthropic format (which also uses type: "image") - if "source" in content_part: - return None - - mime_type = content_part.get("mime_type", "") - modality = block_type if block_type != "file" else "document" - - # Check for base64 encoded content - if "base64" in content_part: - return { - "type": "blob", - "modality": modality, - "mime_type": mime_type, - "content": content_part.get("base64", ""), - } - # Check for URL reference - elif "url" in content_part: - return { - "type": "uri", - "modality": modality, - "mime_type": mime_type, - "uri": content_part.get("url", ""), - } - # Check for file_id reference - elif "file_id" in content_part: - return { - "type": "file", - "modality": modality, - "mime_type": mime_type, - "file_id": content_part.get("file_id", ""), - } - - return None - - -def transform_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a content part from various AI SDK formats to Sentry's standardized format. - - This is a heuristic dispatcher that detects the format and delegates to the - appropriate SDK-specific transformer. For direct SDK integration, prefer using - the specific transformers directly: - - transform_openai_content_part() for OpenAI/LiteLLM - - transform_anthropic_content_part() for Anthropic - - transform_google_content_part() for Google GenAI - - transform_generic_content_part() for LangChain and other generic formats - - Detection order: - 1. OpenAI: type == "image_url" - 2. Google: "inline_data" or "file_data" keys present - 3. Anthropic: type in ("image", "document") with "source" key - 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from an AI SDK - - Returns: - A transformed dictionary in standardized format, or None if the format - is unrecognized or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Try OpenAI format first (most common, clear indicator) - result = transform_openai_content_part(content_part) - if result is not None: - return result - - # Try Google format (unique keys make it easy to detect) - result = transform_google_content_part(content_part) - if result is not None: - return result - - # Try Anthropic format (has "source" key) - result = transform_anthropic_content_part(content_part) - if result is not None: - return result - - # Try generic format as fallback - result = transform_generic_content_part(content_part) - if result is not None: - return result - - # Unrecognized format - return None - - -def transform_message_content(content: "Any") -> "Any": - """ - Transform message content, handling both string content and list of content blocks. - - For list content, each item is transformed using transform_content_part(). - Items that cannot be transformed (return None) are kept as-is. - - Args: - content: Message content - can be a string, list of content blocks, or other - - Returns: - - String content: returned as-is - - List content: list with each transformable item converted to standardized format - - Other: returned as-is - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - return transformed - - return content - - -def _normalize_data(data: "Any", unpack: bool = True) -> "Any": - # convert pydantic data (e.g. OpenAI v1+) to json compatible format - if hasattr(data, "model_dump"): - # Check if it's a class (type) rather than an instance - # Model classes can be passed as arguments (e.g., for schema definitions) - if inspect.isclass(data): - return f"" - - try: - return _normalize_data(data.model_dump(), unpack=unpack) - except Exception as e: - logger.warning("Could not convert pydantic data to JSON: %s", e) - return data if isinstance(data, (int, float, bool, str)) else str(data) - - if isinstance(data, list): - if unpack and len(data) == 1: - return _normalize_data(data[0], unpack=unpack) # remove empty dimensions - return list(_normalize_data(x, unpack=unpack) for x in data) - - if isinstance(data, dict): - return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} - - return data if isinstance(data, (int, float, bool, str)) else str(data) - - -def set_data_normalized( - span: "Union[Span, StreamedSpan]", - key: str, - value: "Any", - unpack: bool = True, -) -> None: - normalized = _normalize_data(value, unpack=unpack) - if isinstance(normalized, (int, float, bool, str)): - _set_span_data_attribute(span, key, normalized) - else: - _set_span_data_attribute(span, key, json.dumps(normalized)) - - -def _set_span_data_attribute( - span: "Union[Span, StreamedSpan]", key: str, value: "Any" -) -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(key, value) - else: - span.set_data(key, value) - - -def normalize_message_role(role: str) -> str: - """ - Normalize a message role to one of the 4 allowed gen_ai role values. - Maps "ai" -> "assistant" and keeps other standard roles unchanged. - """ - return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) - - -def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": - """ - Normalize roles in a list of messages to use standard gen_ai role values. - Creates a deep copy to avoid modifying the original messages. - """ - normalized_messages = [] - for message in messages: - if not isinstance(message, dict): - normalized_messages.append(message) - continue - normalized_message = message.copy() - if "role" in message: - normalized_message["role"] = normalize_message_role(message["role"]) - normalized_messages.append(normalized_message) - - return normalized_messages - - -def get_start_span_function() -> "Callable[..., Any]": - current_span = sentry_sdk.get_current_span() - - transaction_exists = ( - current_span is not None and current_span.containing_transaction is not None - ) - return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction - - -def _truncate_single_message_content_if_present( - message: "Dict[str, Any]", max_chars: int -) -> "Dict[str, Any]": - """ - Truncate a message's content to at most `max_chars` characters and append an - ellipsis if truncation occurs. - """ - if not isinstance(message, dict) or "content" not in message: - return message - content = message["content"] - - if isinstance(content, str): - if len(content) <= max_chars: - return message - message["content"] = content[:max_chars] + "..." - return message - - if isinstance(content, list): - remaining = max_chars - for item in content: - if isinstance(item, dict) and "text" in item: - text = item["text"] - if isinstance(text, str): - if len(text) > remaining: - item["text"] = text[:remaining] + "..." - remaining = 0 - else: - remaining -= len(text) - return message - - return message - - -def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: - """ - Find the index of the first message that would exceed the max bytes limit. - Compute the individual message sizes, and return the index of the first message from the back - of the list that would exceed the max bytes limit. - """ - running_sum = 0 - for idx in range(len(messages) - 1, -1, -1): - size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) - running_sum += size - if running_sum > max_bytes: - return idx + 1 - - return 0 - - -def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: - """ - Some content blocks contain an image_url property with base64 content as its value. - This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. - """ - if item.get("type") != "image_url": - return False - - image_url_val = item.get("image_url") - image_url = ( - image_url_val.get("url", "") - if isinstance(image_url_val, dict) - else (image_url_val or "") - ) - data_url_match = DATA_URL_BASE64_REGEX.match(image_url) - - return bool(data_url_match) - - -def redact_blob_message_parts( - messages: "List[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Redact blob message parts from the messages by replacing blob content with "[Filtered]". - - This function creates a deep copy of messages that contain blob content to avoid - mutating the original message dictionaries. Messages without blob content are - returned as-is to minimize copying overhead. - - e.g: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "data:image/jpeg;base64,..." - } - ] - } - becomes: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "[Filtered]" - } - ] - } - """ - - # First pass: check if any message contains blob content - has_blobs = False - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and ( - item.get("type") == "blob" or _is_image_type_with_blob_content(item) - ): - has_blobs = True - break - if has_blobs: - break - - # If no blobs found, return original messages to avoid unnecessary copying - if not has_blobs: - return messages - - # Deep copy messages to avoid mutating the original - messages_copy = deepcopy(messages) - - # Second pass: redact blob content in the copy - for message in messages_copy: - if not isinstance(message, dict): - continue - - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict): - if item.get("type") == "blob": - item["content"] = BLOB_DATA_SUBSTITUTE - elif _is_image_type_with_blob_content(item): - if isinstance(item["image_url"], dict): - item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE - else: - item["image_url"] = BLOB_DATA_SUBSTITUTE - - return messages_copy - - -def truncate_messages_by_size( - messages: "List[Dict[str, Any]]", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Tuple[List[Dict[str, Any]], int]": - """ - Returns a truncated messages list, consisting of - - the last message, with its content truncated to `max_single_message_chars` characters, - if the last message's size exceeds `max_bytes` bytes; otherwise, - - the maximum number of messages, starting from the end of the `messages` list, whose total - serialized size does not exceed `max_bytes` bytes. - - In the single message case, the serialized message size may exceed `max_bytes`, because - truncation is based only on character count in that case. - """ - serialized_json = json.dumps(messages, separators=(",", ":")) - current_size = len(serialized_json.encode("utf-8")) - - if current_size <= max_bytes: - return messages, 0 - - truncation_index = _find_truncation_index(messages, max_bytes) - if truncation_index < len(messages): - truncated_messages = messages[truncation_index:] - else: - truncation_index = len(messages) - 1 - truncated_messages = messages[-1:] - - if len(truncated_messages) == 1: - truncated_messages[0] = _truncate_single_message_content_if_present( - deepcopy(truncated_messages[0]), max_chars=max_single_message_chars - ) - - return truncated_messages, truncation_index - - -def truncate_and_annotate_messages( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_message = _truncate_single_message_content_if_present( - deepcopy(messages[-1]), max_chars=max_single_message_chars - ) - if len(messages) > 1: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return [truncated_message] - - -def truncate_and_annotate_embedding_inputs( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) - if removed_count > 0: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return truncated_messages - - -def set_conversation_id(conversation_id: str) -> None: - """ - Set the conversation_id in the scope. - """ - scope = sentry_sdk.get_current_scope() - scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py deleted file mode 100644 index 5556b11ace..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/api.py +++ /dev/null @@ -1,573 +0,0 @@ -import inspect -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import Client, tracing_utils -from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.crons import monitor -from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.traces import get_current_span as _get_current_streamed_span -from sentry_sdk.tracing import NoOpSpan, Transaction, trace - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Optional, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - MeasurementUnit, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span, TransactionKwargs - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) -else: - - def overload(x: "T") -> "T": - return x - - -# When changing this, update __all__ in __init__.py too -__all__ = [ - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - - -def scopemethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, - inspect.getdoc(getattr(Scope, f.__name__)), - ) - return f - - -def clientmethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, - inspect.getdoc(getattr(Client, f.__name__)), - ) - return f - - -@scopemethod -def get_client() -> "BaseClient": - return Scope.get_client() - - -def is_initialized() -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether Sentry has been initialized or not. - - If a client is available and the client is active - (meaning it is configured to send data) then - Sentry is initialized. - """ - return get_client().is_active() - - -@scopemethod -def get_global_scope() -> "Scope": - return Scope.get_global_scope() - - -@scopemethod -def get_isolation_scope() -> "Scope": - return Scope.get_isolation_scope() - - -@scopemethod -def get_current_scope() -> "Scope": - return Scope.get_current_scope() - - -@scopemethod -def last_event_id() -> "Optional[str]": - """ - See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding - this method's limitations. - """ - return Scope.last_event_id() - - -@scopemethod -def capture_event( - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) - - -@scopemethod -def capture_message( - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_message( - message, level, scope=scope, **scope_kwargs - ) - - -@scopemethod -def capture_exception( - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) - - -@scopemethod -def add_attachment( - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, -) -> None: - return get_isolation_scope().add_attachment( - bytes, filename, path, content_type, add_to_transactions - ) - - -@scopemethod -def add_breadcrumb( - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", -) -> None: - return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - -@overload -def configure_scope() -> "ContextManager[Scope]": - pass - - -@overload -def configure_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def configure_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - warnings.warn( - "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", - DeprecationWarning, - stacklevel=2, - ) - - scope = get_isolation_scope() - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - -@overload -def push_scope() -> "ContextManager[Scope]": - pass - - -@overload -def push_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def push_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - warnings.warn( - "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", - DeprecationWarning, - stacklevel=2, - ) - - if callback is not None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with push_scope() as scope: - callback(scope) - return None - - return _ScopeManager() - - -@scopemethod -def set_attribute(attribute: str, value: "Any") -> None: - """ - Set an attribute. - - Any attributes-based telemetry (logs, metrics) captured in this scope will - include this attribute. - """ - return get_isolation_scope().set_attribute(attribute, value) - - -@scopemethod -def remove_attribute(attribute: str) -> None: - """ - Remove an attribute. - - If the attribute doesn't exist, this function will not have any effect and - it will also not raise an exception. - """ - return get_isolation_scope().remove_attribute(attribute) - - -@scopemethod -def set_tag(key: str, value: "Any") -> None: - return get_isolation_scope().set_tag(key, value) - - -@scopemethod -def set_tags(tags: "Mapping[str, object]") -> None: - return get_isolation_scope().set_tags(tags) - - -@scopemethod -def set_context(key: str, value: "Dict[str, Any]") -> None: - return get_isolation_scope().set_context(key, value) - - -@scopemethod -def set_extra(key: str, value: "Any") -> None: - return get_isolation_scope().set_extra(key, value) - - -@scopemethod -def set_user(value: "Optional[Dict[str, Any]]") -> None: - return get_isolation_scope().set_user(value) - - -@scopemethod -def set_level(value: "LogLevelStr") -> None: - return get_isolation_scope().set_level(value) - - -@clientmethod -def flush( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return get_client().flush(timeout=timeout, callback=callback) - - -@clientmethod -async def flush_async( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return await get_client().flush_async(timeout=timeout, callback=callback) - - -@scopemethod -def start_span( - **kwargs: "Any", -) -> "Span": - return get_current_scope().start_span(**kwargs) - - -@scopemethod -def start_transaction( - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", -) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction on the current scope. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - return get_current_scope().start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - -def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - transaction = get_current_scope().transaction - if transaction is not None: - transaction.set_measurement(name, value, unit) - - -def get_current_span( - scope: "Optional[Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - return tracing_utils.get_current_span(scope) - - -def get_traceparent() -> "Optional[str]": - """ - Returns the traceparent either from the active span or from the scope. - """ - return get_current_scope().get_traceparent() - - -def get_baggage() -> "Optional[str]": - """ - Returns Baggage either from the active span or from the scope. - """ - baggage = get_current_scope().get_baggage() - if baggage is not None: - return baggage.serialize() - - return None - - -def continue_trace( - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", -) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers, op, name, source, origin - ) - - -@scopemethod -def start_session( - session_mode: str = "application", -) -> None: - return get_isolation_scope().start_session(session_mode=session_mode) - - -@scopemethod -def end_session() -> None: - return get_isolation_scope().end_session() - - -@scopemethod -def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: - return get_current_scope().set_transaction_name(name, source) - - -def update_current_span( - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, - data: "Optional[dict[str, Any]]" = None, -) -> None: - """ - Update the current active span with the provided parameters. - - This function allows you to modify properties of the currently active span. - If no span is currently active, this function will do nothing. - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, the span's operation will - remain unchanged. - :type op: str or None - - :param name: The human-readable name/description for the span. This provides - more specific details about what the span represents (e.g., "GET /api/users", - "SELECT * FROM users"). If not provided, the span's name will remain unchanged. - :type name: str or None - - :param data: A dictionary of key-value pairs to add as data to the span. This - data will be merged with any existing span data. If not provided, - no data will be added. - - .. deprecated:: 2.35.0 - Use ``attributes`` instead. The ``data`` parameter will be removed - in a future version. - :type data: dict[str, Union[str, int, float, bool]] or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes will be merged with any existing span data. If not provided, - no attributes will be added. - :type attributes: dict[str, Union[str, int, float, bool]] or None - - :returns: None - - .. versionadded:: 2.35.0 - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP - - sentry_sdk.update_current_span( - op=OP.FUNCTION, - name="process_user_data", - attributes={"user_id": 123, "batch_size": 50} - ) - """ - if isinstance(_get_current_streamed_span(), StreamedSpan): - warnings.warn( - "The `update_current_span` API isn't available in streaming mode. " - "Retrieve the current span with get_current_span() and use its API " - "directly.", - DeprecationWarning, - stacklevel=2, - ) - return - - current_span = get_current_span() - - if current_span is None: - return - - if op is not None: - current_span.op = op - - if name is not None: - # internally it is still description - current_span.description = name - - if data is not None and attributes is not None: - raise ValueError( - "Cannot provide both `data` and `attributes`. Please use only `attributes`." - ) - - if data is not None: - warnings.warn( - "The `data` parameter is deprecated. Please use `attributes` instead.", - DeprecationWarning, - stacklevel=2, - ) - attributes = data - - if attributes is not None: - current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py deleted file mode 100644 index 4d69d3acf2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/attachments.py +++ /dev/null @@ -1,71 +0,0 @@ -import mimetypes -import os -from typing import TYPE_CHECKING - -from sentry_sdk.envelope import Item, PayloadRef - -if TYPE_CHECKING: - from typing import Callable, Optional, Union - - -class Attachment: - """Additional files/data to send along with an event. - - This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. - config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with - all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are - captured within the ``Scope``. - - To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for - ``add_attachment`` are the same as the parameters for this class's constructor. - - :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless - ``path`` is provided. - :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. - :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. - :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` - parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. - :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. - """ - - def __init__( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - if bytes is None and path is None: - raise TypeError("path or raw bytes required for attachment") - if filename is None and path is not None: - filename = os.path.basename(path) - if filename is None: - raise TypeError("filename is required for attachment") - if content_type is None: - content_type = mimetypes.guess_type(filename)[0] - self.bytes = bytes - self.filename = filename - self.path = path - self.content_type = content_type - self.add_to_transactions = add_to_transactions - - def to_envelope_item(self) -> "Item": - """Returns an envelope item for this attachment.""" - payload: "Union[None, PayloadRef, bytes]" = None - if self.bytes is not None: - if callable(self.bytes): - payload = self.bytes() - else: - payload = self.bytes - else: - payload = PayloadRef(path=self.path) - return Item( - payload=payload, - type="attachment", - content_type=self.content_type, - filename=self.filename, - ) - - def __repr__(self) -> str: - return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py deleted file mode 100644 index 92cb42277e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/client.py +++ /dev/null @@ -1,1479 +0,0 @@ -import json -import os -import platform -import random -import socket -import sys -import uuid -import warnings -from collections.abc import Iterable, Mapping -from datetime import datetime, timezone -from importlib import import_module -from typing import TYPE_CHECKING, Dict, List, cast, overload - -from sentry_sdk._compat import check_uwsgi_thread_support -from sentry_sdk._metrics_batcher import MetricsBatcher -from sentry_sdk._span_batcher import SpanBatcher -from sentry_sdk.consts import ( - DEFAULT_MAX_VALUE_LENGTH, - DEFAULT_OPTIONS, - INSTRUMENTER, - SPANDATA, - SPANSTATUS, - VERSION, - ClientConstructor, -) -from sentry_sdk.data_collection import ( - _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 -from sentry_sdk.monitor import Monitor -from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler -from sentry_sdk.profiler.transaction_profiler import ( - Profile, - has_profiling_enabled, - setup_profiler, -) -from sentry_sdk.scrubber import EventScrubber -from sentry_sdk.serializer import serialize -from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import trace -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import ( - AsyncHttpTransport, - HttpTransportCore, - make_transport, -) -from sentry_sdk.utils import ( - AnnotatedValue, - ContextVar, - capture_internal_exceptions, - current_stacktrace, - datetime_from_isoformat, - env_to_bool, - format_timestamp, - get_before_send_log, - get_before_send_metric, - get_before_send_span, - get_default_release, - get_sdk_name, - get_type_name, - handle_in_app, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union - - from sentry_sdk._log_batcher import LogBatcher - from sentry_sdk._metrics_batcher import MetricsBatcher - from sentry_sdk._types import ( - Event, - EventDataCategory, - Hint, - Log, - Metric, - SDKInfo, - SerializedAttributeValue, - ) - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.session import Session - from sentry_sdk.spotlight import SpotlightClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Item, Transport - from sentry_sdk.utils import Dsn - - I = TypeVar("I", bound=Integration) # noqa: E741 - -_client_init_debug = ContextVar("client_init_debug") - - -SDK_INFO: "SDKInfo" = { - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() - "version": VERSION, - "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], -} - - -def _serialized_v1_attribute_to_serialized_v2_attribute( - attribute_value: "Any", -) -> "Optional[SerializedAttributeValue]": - if isinstance(attribute_value, bool): - return { - "value": attribute_value, - "type": "boolean", - } - - if isinstance(attribute_value, int): - return { - "value": attribute_value, - "type": "integer", - } - - if isinstance(attribute_value, float): - return { - "value": attribute_value, - "type": "double", - } - - if isinstance(attribute_value, str): - return { - "value": attribute_value, - "type": "string", - } - - if isinstance(attribute_value, list): - if not attribute_value: - return {"value": [], "type": "array"} - - ty = type(attribute_value[0]) - if ty in (int, str, bool, float) and all( - type(v) is ty for v in attribute_value - ): - return { - "value": attribute_value, - "type": "array", - } - - # Types returned when the serializer for V1 span attributes recurses into some container types. - if isinstance(attribute_value, (dict, list)): - return { - "value": json.dumps(attribute_value), - "type": "string", - } - - return None - - -def _serialized_v1_span_to_serialized_v2_span( - span: "dict[str, Any]", event: "Event" -) -> "dict[str, Any]": - # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". - res: "dict[str, Any]" = { - "status": SpanStatus.OK.value, - "is_segment": False, - } - - if "trace_id" in span: - res["trace_id"] = span["trace_id"] - - if "span_id" in span: - res["span_id"] = span["span_id"] - - if "description" in span: - description = span["description"] - - if description is None and "op" in span: - description = span["op"] - - res["name"] = description - - if "start_timestamp" in span: - start_timestamp = None - try: - start_timestamp = datetime_from_isoformat(span["start_timestamp"]) - except Exception: - pass - - if start_timestamp is not None: - res["start_timestamp"] = start_timestamp.timestamp() - - if "timestamp" in span: - end_timestamp = None - try: - end_timestamp = datetime_from_isoformat(span["timestamp"]) - except Exception: - pass - - if end_timestamp is not None: - res["end_timestamp"] = end_timestamp.timestamp() - - if "parent_span_id" in span: - res["parent_span_id"] = span["parent_span_id"] - - if "status" in span and span["status"] != SPANSTATUS.OK: - res["status"] = "error" - - attributes: "Dict[str, Any]" = {} - - if "op" in span: - attributes["sentry.op"] = span["op"] - if "origin" in span: - attributes["sentry.origin"] = span["origin"] - - span_data = span.get("data") - if isinstance(span_data, dict): - attributes.update(span_data) - - span_tags = span.get("tags") - if isinstance(span_tags, dict): - attributes.update(span_tags) - - # See Scope._apply_user_attributes_to_telemetry() for user attributes. - user = event.get("user") - if isinstance(user, dict): - if "id" in user: - attributes["user.id"] = user["id"] - if "username" in user: - attributes["user.name"] = user["username"] - if "email" in user: - attributes["user.email"] = user["email"] - - # See Scope.set_global_attributes() for release, environment, and SDK metadata. - if "release" in event: - attributes["sentry.release"] = event["release"] - if "environment" in event: - attributes["sentry.environment"] = event["environment"] - if "server_name" in event: - attributes["server.address"] = event["server_name"] - if "transaction" in event: - attributes["sentry.segment.name"] = event["transaction"] - - trace_context = event.get("contexts", {}).get("trace", {}) - if "span_id" in trace_context: - attributes["sentry.segment.id"] = trace_context["span_id"] - - sdk_info = event.get("sdk") - if isinstance(sdk_info, dict): - if "name" in sdk_info: - attributes["sentry.sdk.name"] = sdk_info["name"] - if "version" in sdk_info: - attributes["sentry.sdk.version"] = sdk_info["version"] - - attributes["process.runtime.name"] = platform.python_implementation() - attributes["process.runtime.version"] = ( - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ) - - if not attributes: - return res - - res["attributes"] = {} - for key, value in attributes.items(): - converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) - if converted_value is None: - continue - - res["attributes"][key] = converted_value - - # Remove redundant attribute, as status is stored in the status field. - if "status" in res["attributes"]: - del res["attributes"]["status"] - - return res - - -def _split_gen_ai_spans( - event_opt: "Event", -) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": - if "spans" not in event_opt: - return None - - spans: "Any" = event_opt["spans"] - if isinstance(spans, AnnotatedValue): - spans = spans.value - - if not isinstance(spans, Iterable): - return None - - non_gen_ai_spans = [] - gen_ai_spans = [] - for span in spans: - if not isinstance(span, dict): - non_gen_ai_spans.append(span) - continue - - span_op = span.get("op") - if isinstance(span_op, str) and span_op.startswith("gen_ai."): - gen_ai_spans.append(span) - else: - non_gen_ai_spans.append(span) - - return non_gen_ai_spans, gen_ai_spans - - -def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": - if args and (isinstance(args[0], (bytes, str)) or args[0] is None): - dsn: "Optional[str]" = args[0] - args = args[1:] - else: - dsn = None - - if len(args) > 1: - raise TypeError("Only single positional argument is expected") - - rv = dict(DEFAULT_OPTIONS) - options = dict(*args, **kwargs) - if dsn is not None and options.get("dsn") is None: - options["dsn"] = dsn - - for key, value in options.items(): - if key not in rv: - raise TypeError("Unknown option %r" % (key,)) - - rv[key] = value - - if rv["dsn"] is None: - rv["dsn"] = os.environ.get("SENTRY_DSN") - - if rv["release"] is None: - rv["release"] = get_default_release() - - if rv["environment"] is None: - rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" - - if rv["debug"] is None: - rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False - - if rv["server_name"] is None and hasattr(socket, "gethostname"): - rv["server_name"] = socket.gethostname() - - if rv["instrumenter"] is None: - rv["instrumenter"] = INSTRUMENTER.SENTRY - - if rv["project_root"] is None: - try: - project_root = os.getcwd() - except Exception: - project_root = None - - rv["project_root"] = project_root - - 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"] - ) - - if rv["socket_options"] and not isinstance(rv["socket_options"], list): - logger.warning( - "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." - ) - rv["socket_options"] = None - - if rv["keep_alive"] is None: - rv["keep_alive"] = ( - env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False - ) - - if rv["enable_tracing"] is not None: - warnings.warn( - "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", - DeprecationWarning, - stacklevel=2, - ) - - if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): - warnings.warn( - "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", - stacklevel=2, - ) - - return rv - - -try: - # Python 3.6+ - module_not_found_error = ModuleNotFoundError -except Exception: - # Older Python versions - module_not_found_error = ImportError # type: ignore - - -class BaseClient: - """ - .. versionadded:: 2.0.0 - - The basic definition of a client that is used for sending data to Sentry. - """ - - spotlight: "Optional[SpotlightClient]" = None - - def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: - self.options: "Dict[str, Any]" = ( - options if options is not None else DEFAULT_OPTIONS - ) - - self.transport: "Optional[Transport]" = None - self.monitor: "Optional[Monitor]" = None - self.log_batcher: "Optional[LogBatcher]" = None - self.metrics_batcher: "Optional[MetricsBatcher]" = None - self.span_batcher: "Optional[SpanBatcher]" = None - self.integrations: "dict[str, Integration]" = {} - - def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": - return {"options": {}} - - def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: - pass - - @property - def dsn(self) -> "Optional[str]": - return None - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - return None - - def should_send_default_pii(self) -> bool: - return False - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return False - - def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - return None - - def _capture_log(self, log: "Log", scope: "Scope") -> None: - pass - - def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: - pass - - def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: - pass - - def capture_session(self, *args: "Any", **kwargs: "Any") -> None: - return None - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, name_or_class: "Union[str, type[Integration]]" - ) -> "Optional[Integration]": - return None - - def close(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def flush(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def close_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def __enter__(self) -> "BaseClient": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - return None - - -class NonRecordingClient(BaseClient): - """ - .. versionadded:: 2.0.0 - - A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. - """ - - pass - - -class _Client(BaseClient): - """ - The client is internally responsible for capturing the events and - forwarding them to sentry through the configured transport. It takes - the client options as keyword arguments and optionally the DSN as first - argument. - - Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) - """ - - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - super(_Client, self).__init__(options=get_options(*args, **kwargs)) - self._init_impl() - - def __getstate__(self) -> "Any": - return {"options": self.options} - - def __setstate__(self, state: "Any") -> None: - self.options = state["options"] - self._init_impl() - - def _setup_instrumentation( - self, functions_to_trace: "Sequence[Dict[str, str]]" - ) -> None: - """ - Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. - """ - for function in functions_to_trace: - class_name = None - function_qualname = function["qualified_name"] - - if "." not in function_qualname: - logger.warning( - "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", - function_qualname, - ) - continue - - module_name, function_name = function_qualname.rsplit(".", 1) - - try: - # Try to import module and function - # ex: "mymodule.submodule.funcname" - - module_obj = import_module(module_name) - function_obj = getattr(module_obj, function_name) - setattr(module_obj, function_name, trace(function_obj)) - logger.debug("Enabled tracing for %s", function_qualname) - except module_not_found_error: - try: - # Try to import a class - # ex: "mymodule.submodule.MyClassName.member_function" - - module_name, class_name = module_name.rsplit(".", 1) - module_obj = import_module(module_name) - class_obj = getattr(module_obj, class_name) - function_obj = getattr(class_obj, function_name) - function_type = type(class_obj.__dict__[function_name]) - traced_function = trace(function_obj) - - if function_type in (staticmethod, classmethod): - traced_function = staticmethod(traced_function) - - setattr(class_obj, function_name, traced_function) - setattr(module_obj, class_name, class_obj) - logger.debug("Enabled tracing for %s", function_qualname) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - def _init_impl(self) -> None: - old_debug = _client_init_debug.get(False) - - def _capture_envelope(envelope: "Envelope") -> None: - if self.spotlight is not None: - self.spotlight.capture_envelope(envelope) - if self.transport is not None: - self.transport.capture_envelope(envelope) - - def _record_lost_event( - reason: str, - data_category: "EventDataCategory", - item: "Optional[Item]" = None, - quantity: int = 1, - ) -> None: - if self.transport is not None: - self.transport.record_lost_event( - reason=reason, - data_category=data_category, - item=item, - quantity=quantity, - ) - - try: - _client_init_debug.set(self.options["debug"]) - self.transport = make_transport(self.options) - - self.monitor = None - if self.transport: - if self.options["enable_backpressure_handling"]: - self.monitor = Monitor(self.transport) - - # Setup Spotlight before creating batchers so _capture_envelope can use it. - # setup_spotlight handles all config/env var resolution per the SDK spec. - from sentry_sdk.spotlight import setup_spotlight - - self.spotlight = setup_spotlight(self.options) - if self.spotlight is not None and not self.options["dsn"]: - sample_all = lambda *_args, **_kwargs: 1.0 - self.options["send_default_pii"] = True - 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, - ) - - self.session_flusher = SessionFlusher(capture_func=_capture_envelope) - - self.log_batcher = None - - if has_logs_enabled(self.options): - from sentry_sdk._log_batcher import LogBatcher - - self.log_batcher = LogBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.metrics_batcher = None - if has_metrics_enabled(self.options): - self.metrics_batcher = MetricsBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.span_batcher = None - if has_span_streaming_enabled(self.options): - self.span_batcher = SpanBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - max_request_body_size = ("always", "never", "small", "medium") - if self.options["max_request_body_size"] not in max_request_body_size: - raise ValueError( - "Invalid value for max_request_body_size. Must be one of {}".format( - max_request_body_size - ) - ) - - if self.options["_experiments"].get("otel_powered_performance", False): - logger.debug( - "[OTel] Enabling experimental OTel-powered performance monitoring." - ) - self.options["instrumenter"] = INSTRUMENTER.OTEL - if ( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - not in _DEFAULT_INTEGRATIONS - ): - _DEFAULT_INTEGRATIONS.append( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", - ) - - self.integrations = setup_integrations( - self.options["integrations"], - with_defaults=self.options["default_integrations"], - with_auto_enabling_integrations=self.options[ - "auto_enabling_integrations" - ], - disabled_integrations=self.options["disabled_integrations"], - options=self.options, - ) - - sdk_name = get_sdk_name(list(self.integrations.keys())) - SDK_INFO["name"] = sdk_name - logger.debug("Setting SDK name to '%s'", sdk_name) - - if has_profiling_enabled(self.options): - try: - setup_profiler(self.options) - except Exception as e: - logger.debug("Can not set up profiler. (%s)", e) - else: - try: - setup_continuous_profiler( - self.options, - sdk_info=SDK_INFO, - capture_func=_capture_envelope, - ) - except Exception as e: - logger.debug("Can not set up continuous profiler. (%s)", e) - - finally: - _client_init_debug.set(old_debug) - - self._setup_instrumentation(self.options.get("functions_to_trace", [])) - - if ( - self.monitor - or self.log_batcher - or self.metrics_batcher - or self.span_batcher - or has_profiling_enabled(self.options) - or isinstance(self.transport, HttpTransportCore) - ): - # If we have anything on that could spawn a background thread, we - # need to check if it's safe to use them. - check_uwsgi_thread_support() - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return True - - def should_send_default_pii(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. - """ - return self.options.get("send_default_pii") or False - - @property - def dsn(self) -> "Optional[str]": - """Returns the configured DSN as string.""" - return self.options["dsn"] - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - """Returns the configured parsed DSN object.""" - return self.transport.parsed_dsn if self.transport else None - - def _prepare_event( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]", - ) -> "Optional[Event]": - previous_total_spans: "Optional[int]" = None - previous_total_breadcrumbs: "Optional[int]" = None - - if event.get("timestamp") is None: - event["timestamp"] = datetime.now(timezone.utc) - - is_transaction = event.get("type") == "transaction" - - if scope is not None: - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - event_ = scope.apply_to_event(event, hint, self.options) - - # one of the event/error processors returned None - if event_ is None: - if self.transport: - self.transport.record_lost_event( - "event_processor", - data_category=("transaction" if is_transaction else "error"), - ) - if is_transaction: - self.transport.record_lost_event( - "event_processor", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - return None - - event = event_ - spans_delta = spans_before - len( - cast(List[Dict[str, object]], event.get("spans", [])) - ) - span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - - if is_transaction and self.transport is not None: - if spans_delta > 0: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) - if span_recorder_dropped_spans > 0: - self.transport.record_lost_event( - "buffer_overflow", - data_category="span", - quantity=span_recorder_dropped_spans, - ) - - dropped_spans: int = span_recorder_dropped_spans + spans_delta - if dropped_spans > 0: - previous_total_spans = spans_before + dropped_spans - if scope._n_breadcrumbs_truncated > 0: - breadcrumbs = event.get("breadcrumbs", {}) - values = ( - breadcrumbs.get("values", []) - if not isinstance(breadcrumbs, AnnotatedValue) - else [] - ) - previous_total_breadcrumbs = ( - len(values) + scope._n_breadcrumbs_truncated - ) - - if ( - not is_transaction - and self.options["attach_stacktrace"] - and "exception" not in event - and "stacktrace" not in event - and "threads" not in event - ): - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=self.options.get( - "include_local_variables", True - ), - max_value_length=self.options.get( - "max_value_length", DEFAULT_MAX_VALUE_LENGTH - ), - ), - "crashed": False, - "current": True, - } - ] - } - - for key in "release", "environment", "server_name", "dist": - if event.get(key) is None and self.options[key] is not None: - event[key] = str(self.options[key]).strip() - if event.get("sdk") is None: - sdk_info = dict(SDK_INFO) - sdk_info["integrations"] = sorted(self.integrations.keys()) - event["sdk"] = sdk_info - - if event.get("platform") is None: - event["platform"] = "python" - - event = handle_in_app( - event, - self.options["in_app_exclude"], - self.options["in_app_include"], - self.options["project_root"], - ) - - if event is not None: - event_scrubber = self.options["event_scrubber"] - if event_scrubber: - event_scrubber.scrub_event(event) - - if scope is not None and scope._gen_ai_original_message_count: - spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) - if isinstance(spans, list): - for span in spans: - span_id = span.get("span_id", None) - span_data = span.get("data", {}) - if ( - span_id - and span_id in scope._gen_ai_original_message_count - and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data - ): - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], - {"len": scope._gen_ai_original_message_count[span_id]}, - ) - if previous_total_spans is not None: - event["spans"] = AnnotatedValue( - event.get("spans", []), {"len": previous_total_spans} - ) - if previous_total_breadcrumbs is not None: - event["breadcrumbs"] = AnnotatedValue( - event.get("breadcrumbs", {"values": []}), - {"len": previous_total_breadcrumbs}, - ) - - # Postprocess the event here so that annotated types do - # generally not surface in before_send - if event is not None: - event = cast( - "Event", - serialize( - cast("Dict[str, Any]", event), - max_request_body_size=self.options.get("max_request_body_size"), - max_value_length=self.options.get("max_value_length"), - custom_repr=self.options.get("custom_repr"), - ), - ) - - before_send = self.options["before_send"] - if ( - before_send is not None - and event is not None - and event.get("type") != "transaction" - ): - new_event = None - with capture_internal_exceptions(): - new_event = before_send(event, hint or {}) - if new_event is None: - logger.info("before send dropped event") - if self.transport: - self.transport.record_lost_event( - "before_send", data_category="error" - ) - - # If this is an exception, reset the DedupeIntegration. It still - # remembers the dropped exception as the last exception, meaning - # that if the same exception happens again and is not dropped - # in before_send, it'd get dropped by DedupeIntegration. - if event.get("exception"): - DedupeIntegration.reset_last_seen() - - event = new_event - - before_send_transaction = self.options["before_send_transaction"] - if ( - before_send_transaction is not None - and event is not None - and event.get("type") == "transaction" - ): - new_event = None - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - with capture_internal_exceptions(): - new_event = before_send_transaction(event, hint or {}) - if new_event is None: - logger.info("before send transaction dropped event") - if self.transport: - self.transport.record_lost_event( - reason="before_send", data_category="transaction" - ) - self.transport.record_lost_event( - reason="before_send", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - else: - spans_delta = spans_before - len(new_event.get("spans", [])) - if spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - reason="before_send", data_category="span", quantity=spans_delta - ) - - event = new_event - - return event - - def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: - exc_info = hint.get("exc_info") - if exc_info is None: - return False - - error = exc_info[0] - error_type_name = get_type_name(exc_info[0]) - error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) - - for ignored_error in self.options["ignore_errors"]: - # String types are matched against the type name in the - # exception only - if isinstance(ignored_error, str): - if ignored_error == error_full_name or ignored_error == error_type_name: - return True - else: - if issubclass(error, ignored_error): - return True - - return False - - def _should_capture( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]" = None, - ) -> bool: - # Transactions are sampled independent of error events. - is_transaction = event.get("type") == "transaction" - if is_transaction: - return True - - ignoring_prevents_recursion = scope is not None and not scope._should_capture - if ignoring_prevents_recursion: - return False - - ignored_by_config_option = self._is_ignored_error(event, hint) - if ignored_by_config_option: - return False - - return True - - def _should_sample_error( - self, - event: "Event", - hint: "Hint", - ) -> bool: - error_sampler = self.options.get("error_sampler", None) - - if callable(error_sampler): - with capture_internal_exceptions(): - sample_rate = error_sampler(event, hint) - else: - sample_rate = self.options["sample_rate"] - - try: - not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate - except NameError: - logger.warning( - "The provided error_sampler raised an error. Defaulting to sampling the event." - ) - - # If the error_sampler raised an error, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - except TypeError: - parameter, verb = ( - ("error_sampler", "returned") - if callable(error_sampler) - else ("sample_rate", "contains") - ) - logger.warning( - "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." - % (parameter, verb, repr(sample_rate)) - ) - - # If the sample_rate has an invalid value, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - - if not_in_sample_rate: - # because we will not sample this event, record a "lost event". - if self.transport: - self.transport.record_lost_event("sample_rate", data_category="error") - - return False - - return True - - def _update_session_from_event( - self, - session: "Session", - event: "Event", - ) -> None: - crashed = False - errored = False - user_agent = None - - exceptions = (event.get("exception") or {}).get("values") - if exceptions: - errored = True - for error in exceptions: - if isinstance(error, AnnotatedValue): - error = error.value or {} - mechanism = error.get("mechanism") - if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: - crashed = True - break - - user = event.get("user") - - if session.user_agent is None: - headers = (event.get("request") or {}).get("headers") - headers_dict = headers if isinstance(headers, dict) else {} - for k, v in headers_dict.items(): - if k.lower() == "user-agent": - user_agent = v - break - - session.update( - status="crashed" if crashed else None, - user=user, - user_agent=user_agent, - errors=session.errors + (errored or crashed), - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - ) -> "Optional[str]": - """Captures an event. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - - :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. - """ - hint = dict(hint or ()) - - if not self._should_capture(event, hint, scope): - return None - - profile = event.pop("profile", None) - - event_id = event.get("event_id") - if event_id is None: - event["event_id"] = event_id = uuid.uuid4().hex - - span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) - event_opt = self._prepare_event(event, hint, scope) - if event_opt is None: - return None - - # whenever we capture an event we also check if the session needs - # to be updated based on that information. - session = scope._session if scope else None - if session: - self._update_session_from_event(session, event) - - is_transaction = event_opt.get("type") == "transaction" - is_checkin = event_opt.get("type") == "check_in" - - if ( - not is_transaction - and not is_checkin - and not self._should_sample_error(event, hint) - ): - return None - - attachments = hint.get("attachments") - - trace_context = event_opt.get("contexts", {}).get("trace") or {} - dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) - - headers: "dict[str, object]" = { - "event_id": event_opt["event_id"], - "sent_at": format_timestamp(datetime.now(timezone.utc)), - } - - if dynamic_sampling_context: - headers["trace"] = dynamic_sampling_context - - envelope = Envelope(headers=headers) - - if is_transaction and isinstance(profile, Profile): - envelope.add_profile(profile.to_json(event_opt, self.options)) - - if is_transaction and not span_recorder_has_gen_ai_span: - envelope.add_transaction(event_opt) - elif is_transaction: - split_spans = _split_gen_ai_spans(event_opt) - if split_spans is None or not split_spans[1]: - envelope.add_transaction(event_opt) - else: - non_gen_ai_spans, gen_ai_spans = split_spans - - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) - - converted_gen_ai_spans = [ - _serialized_v1_span_to_serialized_v2_span(span, event_opt) - for span in gen_ai_spans - if isinstance(span, dict) - ] - - envelope.add_item( - Item( - type=SpanBatcher.TYPE, - content_type=SpanBatcher.CONTENT_TYPE, - headers={ - "item_count": len(converted_gen_ai_spans), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": converted_gen_ai_spans, - }, - ), - ) - ) - - elif is_checkin: - envelope.add_checkin(event_opt) - else: - envelope.add_event(event_opt) - - for attachment in attachments or (): - envelope.add_item(attachment.to_envelope_item()) - - return_value = None - if self.spotlight: - self.spotlight.capture_envelope(envelope) - return_value = event_id - - if self.transport is not None: - self.transport.capture_envelope(envelope) - return_value = event_id - - return return_value - - def _capture_telemetry( - self, - telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", - ty: str, - scope: "Scope", - ) -> None: - """ - Capture attributes-based telemetry (logs, metrics, streamed spans). - - Apply any attributes set on the scope to it, and run the user's - before_send_{telemetry} on it, if applicable. - """ - if telemetry is None: - return - - scope.apply_to_telemetry(telemetry) - - before_send = None - - if ty == "log": - before_send = get_before_send_log(self.options) - serialized = telemetry - - elif ty == "metric": - before_send = get_before_send_metric(self.options) - serialized = telemetry - - elif ty == "span": - before_send = get_before_send_span(self.options) - serialized = telemetry._to_json() # type: ignore[union-attr] - - if before_send is not None: - serialized = before_send(serialized, {}) # type: ignore[arg-type] - - if ty in ("log", "metric"): - # Logs and metrics can be dropped in their respective - # before_send, so if we get None, don't queue them for sending. - if serialized is None: - return - - elif ty == "span" and isinstance(telemetry, StreamedSpan): - # Spans can't be dropped in before_send_span by design. They can - # be altered though (e.g. to sanitize). Only allow changes to - # name and attributes. - if isinstance(serialized, dict) and "name" in serialized: - telemetry.name = serialized["name"] # type: ignore[typeddict-item] - telemetry._attributes = {} - for k, v in (serialized.get("attributes") or {}).items(): - telemetry.set_attribute(k, v) - - else: - logger.debug( - "[Tracing] Invalid return value from before_send_span. Keeping original span." - ) - - serialized = telemetry._to_json() - - batcher = None - if ty == "log": - batcher = self.log_batcher - - elif ty == "metric": - batcher = self.metrics_batcher - - elif ty == "span": - # We need a reference to the segment span in the batcher to populate - # the dynamic sampling context (DSC) - serialized["_segment_span"] = telemetry._segment # type: ignore - batcher = self.span_batcher - - if batcher is not None: - batcher.add(serialized) # type: ignore - - def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: - self._capture_telemetry(log, "log", scope) - - def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: - self._capture_telemetry(metric, "metric", scope) - - def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: - self._capture_telemetry(span, "span", scope) - - def capture_session( - self, - session: "Session", - ) -> None: - if not session.release: - logger.info("Discarded session update because of missing release") - else: - self.session_flusher.add_session(session) - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Optional[Integration]": - """Returns the integration for this client by name or class. - If the client does not have that integration then `None` is returned. - """ - if isinstance(name_or_class, str): - integration_name = name_or_class - elif name_or_class.identifier is not None: - integration_name = name_or_class.identifier - else: - raise ValueError("Integration has no name") - - return self.integrations.get(integration_name) - - def _has_async_transport(self) -> bool: - """Check if the current transport is async.""" - return isinstance(self.transport, AsyncHttpTransport) - - @property - def _batchers(self) -> "tuple[Any, ...]": - return tuple( - b - for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) - if b is not None - ) - - def _close_components(self) -> None: - """Kill all client components in the correct order.""" - self.session_flusher.kill() - for b in self._batchers: - b.kill() - if self.monitor: - self.monitor.kill() - - def _flush_components(self) -> None: - """Flush all client components.""" - self.session_flusher.flush() - for b in self._batchers: - b.flush() - - def close( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush`. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "close() used with AsyncHttpTransport. Use close_async() instead.", - stacklevel=2, - ) - self._flush_components() - else: - self.flush(timeout=timeout, callback=callback) - self._close_components() - self.transport.kill() - self.transport = None - - async def close_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush_async`. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "close_async() used with non-async transport, aborting. Please use close() instead." - ) - return - await self.flush_async(timeout=timeout, callback=callback) - self._close_components() - kill_task = self.transport.kill() # type: ignore - if kill_task is not None: - await kill_task - self.transport = None - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "flush() used with AsyncHttpTransport. Use flush_async() instead.", - stacklevel=2, - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - - self.transport.flush(timeout=timeout, callback=callback) - - async def flush_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "flush_async() used with non-async transport, aborting. Please use flush() instead." - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore - if flush_task is not None: - await flush_task - - def __enter__(self) -> "_Client": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - self.close() - - async def __aenter__(self) -> "_Client": - return self - - async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - await self.close_async() - - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `get_options` is a - # type to have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `Dict[str, Any]` to tell static analyzers about the return type. - - class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 - pass - - class Client(ClientConstructor, _Client): - pass - -else: - # Alias `get_options` for actual usage. Go through the lambda indirection - # to throw PyCharm off of the weakly typed signature (it would otherwise - # discover both the weakly typed signature of `_init` and our faked `init` - # type). - - get_options = (lambda: _get_options)() - Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py deleted file mode 100644 index 759898f6ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/consts.py +++ /dev/null @@ -1,1802 +0,0 @@ -import itertools -from enum import Enum -from typing import TYPE_CHECKING - -DEFAULT_MAX_VALUE_LENGTH = None - -DEFAULT_MAX_STACK_FRAMES = 100 -DEFAULT_ADD_FULL_STACK = False - - -# Also needs to be at the top to prevent circular import -class EndpointType(Enum): - """ - The type of an endpoint. This is an enum, rather than a constant, for historical reasons - (the old /store endpoint). The enum also preserve future compatibility, in case we ever - have a new endpoint. - """ - - ENVELOPE = "envelope" - OTLP_TRACES = "integration/otlp/v1/traces" - - -class CompressionAlgo(Enum): - GZIP = "gzip" - BROTLI = "br" - - -if TYPE_CHECKING: - from typing import ( - AbstractSet, - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - ) - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - from sentry_sdk._types import ( - BreadcrumbProcessor, - ContinuousProfilerMode, - DataCollectionUserOptions, - Event, - EventProcessor, - Hint, - IgnoreSpansConfig, - Log, - Metric, - ProfilerMode, - SpanJSON, - TracesSampler, - TransactionProcessor, - ) - - # Experiments are feature flags to enable and disable certain unstable SDK - # functionality. Changing them from the defaults (`None`) in production - # code is highly discouraged. They are not subject to any stability - # guarantees such as the ones from semantic versioning. - Experiments = TypedDict( - "Experiments", - { - "max_spans": Optional[int], - "max_flags": Optional[int], - "record_sql_params": Optional[bool], - "continuous_profiling_auto_start": Optional[bool], - "continuous_profiling_mode": Optional[ContinuousProfilerMode], - "otel_powered_performance": Optional[bool], - "transport_zlib_compression_level": Optional[int], - "transport_compression_level": Optional[int], - "transport_compression_algo": Optional[CompressionAlgo], - "transport_num_pools": Optional[int], - "transport_http2": Optional[bool], - "transport_async": Optional[bool], - "enable_logs": Optional[bool], - "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], - "enable_metrics": Optional[bool], - "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], - "trace_lifecycle": Optional[Literal["static", "stream"]], - "ignore_spans": Optional[IgnoreSpansConfig], - "before_send_span": Optional[ - Callable[[SpanJSON, Hint], Optional[SpanJSON]] - ], - "suppress_asgi_chained_exceptions": Optional[bool], - "data_collection": Optional[DataCollectionUserOptions], - }, - total=False, - ) - -DEFAULT_QUEUE_SIZE = 100 -DEFAULT_MAX_BREADCRUMBS = 100 -MATCH_ALL = r".*" - -FALSE_VALUES = [ - "false", - "no", - "off", - "n", - "0", -] - - -class SPANTEMPLATE(str, Enum): - DEFAULT = "default" - AI_AGENT = "ai_agent" - AI_TOOL = "ai_tool" - AI_CHAT = "ai_chat" - - def __str__(self) -> str: - return self.value - - -class INSTRUMENTER: - SENTRY = "sentry" - OTEL = "otel" - - -class SPANNAME: - DB_COMMIT = "COMMIT" - DB_ROLLBACK = "ROLLBACK" - - -class SPANDATA: - """ - Additional information describing the type of the span. - See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ - """ - - AI_CITATIONS = "ai.citations" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - References or sources cited by the AI model in its response. - Example: ["Smith et al. 2020", "Jones 2019"] - """ - - AI_DOCUMENTS = "ai.documents" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Documents or content chunks used as context for the AI model. - Example: ["doc1.txt", "doc2.pdf"] - """ - - AI_FINISH_REASON = "ai.finish_reason" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. - - The reason why the model stopped generating. - Example: "length" - """ - - AI_FREQUENCY_PENALTY = "ai.frequency_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_FUNCTION_CALL = "ai.function_call" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_GENERATION_ID = "ai.generation_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. - - Unique identifier for the completion. - Example: "gen_123abc" - """ - - AI_INPUT_MESSAGES = "ai.input_messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. - - The input messages to an LLM call. - Example: [{"role": "user", "message": "hello"}] - """ - - AI_LOGIT_BIAS = "ai.logit_bias" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the logit bias - """ - - AI_METADATA = "ai.metadata" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Extra metadata passed to an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_MODEL_ID = "ai.model_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. - - The unique descriptor of the model being executed. - Example: gpt-4 - """ - - AI_PIPELINE_NAME = "ai.pipeline.name" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. - - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - AI_PREAMBLE = "ai.preamble" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the preamble parameter. - Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. - Example: "You are now a clown." - """ - - AI_PRESENCE_PENALTY = "ai.presence_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_RAW_PROMPTING = "ai.raw_prompting" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Minimize pre-processing done to the prompt sent to the LLM. - Example: true - """ - - AI_RESPONSE_FORMAT = "ai.response_format" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the format of the response - """ - - AI_RESPONSES = "ai.responses" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. - - The responses to an AI model call. Always as a list. - Example: ["hello", "world"] - """ - - AI_SEARCH_QUERIES = "ai.search_queries" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Queries used to search for relevant context or documents. - Example: ["climate change effects", "renewable energy"] - """ - - AI_SEARCH_REQUIRED = "ai.is_search_required" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Boolean indicating if the model needs to perform a search. - Example: true - """ - - AI_SEARCH_RESULTS = "ai.search_results" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Results returned from search queries for context. - Example: ["Result 1", "Result 2"] - """ - - AI_SEED = "ai.seed" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. - - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: 123.45 - """ - - AI_STREAMING = "ai.streaming" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. - - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - AI_TAGS = "ai.tags" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Tags that describe an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_TEMPERATURE = "ai.temperature" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. - - For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. - Example: 0.5 - """ - - AI_TEXTS = "ai.texts" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Raw text inputs provided to the model. - Example: ["What is machine learning?"] - """ - - AI_TOP_K = "ai.top_k" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. - - For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. - Example: 35 - """ - - AI_TOP_P = "ai.top_p" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. - - For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. - Example: 0.5 - """ - - AI_TOOL_CALLS = "ai.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_TOOLS = "ai.tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. - - For an AI model call, the functions that are available - """ - - AI_WARNINGS = "ai.warnings" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Warning messages generated during model execution. - Example: ["Token limit exceeded"] - """ - - CACHE_HIT = "cache.hit" - """ - A boolean indicating whether the requested data was found in the cache. - Example: true - """ - - CACHE_ITEM_SIZE = "cache.item_size" - """ - The size of the requested data in bytes. - Example: 58 - """ - - CACHE_KEY = "cache.key" - """ - The key of the requested data. - Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 - """ - - CLIENT_ADDRESS = "client.address" - """ - Client address of the network connection - IP address or Unix domain socket name. - Example: "10.1.2.80" - """ - - CODE_FILEPATH = "code.filepath" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FILE_PATH instead. - - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FILE_PATH = "code.file.path" - """ - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FUNCTION = "code.function" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead. - - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_FUNCTION_NAME = "code.function.name" - """ - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_LINENO = "code.lineno" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_LINE_NUMBER instead. - - The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. - Example: 42 - """ - - CODE_LINE_NUMBER = "code.line.number" - """ - The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. - Example: 42 - """ - - CODE_NAMESPACE = "code.namespace" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. - - The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. - Example: "http.handler" - """ - - DB_MONGODB_COLLECTION = "db.mongodb.collection" - """ - The MongoDB collection being accessed within the database. - See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes - Example: public.users; customers - """ - - DB_NAME = "db.name" - """ - .. deprecated:: - This attribute is deprecated. Use DB_NAMESPACE instead. - - The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - Example: myDatabase - """ - - DB_NAMESPACE = "db.namespace" - """ - The name of the database being accessed. - Example: "customers" - """ - - DB_DRIVER_NAME = "db.driver.name" - """ - The name of the database driver being used for the connection. - Example: "psycopg2" - """ - - DB_OPERATION = "db.operation" - """ - .. deprecated:: - This attribute is deprecated. Use DB_OPERATION_NAME instead. - - The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: findAndModify, HMSET, SELECT - """ - - DB_OPERATION_NAME = "db.operation.name" - """ - The name of the operation being executed. - Example: "SELECT" - """ - - DB_SYSTEM = "db.system" - """ - .. deprecated:: - This attribute is deprecated. Use DB_SYSTEM_NAME instead. - - An identifier for the database management system (DBMS) product being used. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: postgresql - """ - - DB_QUERY_TEXT = "db.query.text" - """ - The database query being executed. - Example: "SELECT * FROM users WHERE id = $1" - """ - - DB_SYSTEM_NAME = "db.system.name" - """ - An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. - Example: "postgresql" - """ - - DB_USER = "db.user" - """ - The name of the database user used for connecting to the database. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: my_user - """ - - GEN_AI_AGENT_NAME = "gen_ai.agent.name" - """ - The name of the agent being used. - Example: "ResearchAssistant" - """ - - GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" - """ - The unique identifier for the conversation/thread with the AI model. - Example: "conv_abc123" - """ - - GEN_AI_CHOICE = "gen_ai.choice" - """ - The model's response message. - Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" - """ - - GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" - """ - The input to the embeddings operation. - Example: "Hello!" - """ - - GEN_AI_FUNCTION_ID = "gen_ai.function_id" - """ - Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. - Example: "my-awesome-function" - """ - - GEN_AI_OPERATION_NAME = "gen_ai.operation.name" - """ - The name of the operation being performed. - Example: "chat" - """ - - GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" - """ - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" - """ - The reason why the model stopped generating. - Example: "COMPLETE" - """ - - GEN_AI_RESPONSE_ID = "gen_ai.response.id" - """ - Unique identifier for the completion. - Example: "gen_123abc" - """ - - GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" - """ - Exact model identifier used to generate the response - Example: gpt-4o-mini-2024-07-18 - """ - - GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" - """ - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The model's response text messages. - Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] - """ - - GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" - """ - The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. - Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] - """ - - GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" - """ - The time it took to receive the first token from the model. - Example: 0.1 - """ - - GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The tool calls in the model's response. - Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] - """ - - GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. - - The available tools for the model. - Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] - """ - - GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" - """ - The list of source system tool definitions available to the GenAI agent or model. - Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] - """ - - GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" - """ - The frequency penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" - """ - The maximum number of tokens to generate in the response. - Example: 2048 - """ - - GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" - """ - The system instructions passed to the model. - Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] - """ - - GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. - - The messages passed to the model. The "content" can be a string or an array of objects. - Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] - """ - - GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" - """ - The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". - Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] - """ - - GEN_AI_REQUEST_MODEL = "gen_ai.request.model" - """ - The model identifier being used for the request. - Example: "gpt-4-turbo" - """ - - GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" - """ - The presence penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_SEED = "gen_ai.request.seed" - """ - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: "1234567890" - """ - - GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" - """ - The temperature parameter used to control randomness in the output. - Example: 0.7 - """ - - GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" - """ - Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). - Example: 35 - """ - - GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" - """ - The top_p parameter used to control diversity via nucleus sampling. - Example: 1.0 - """ - - GEN_AI_SYSTEM = "gen_ai.system" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. - - The name of the AI system being used. - Example: "openai" - """ - - GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" - """ - The Generative AI provider as identified by the client or server instrumentation. - Example: "openai" - """ - - GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" - """ - The description of the tool being used. - Example: "Searches the web for current information about a topic" - """ - - GEN_AI_TOOL_INPUT = "gen_ai.tool.input" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. - - The input of the tool being used. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" - """ - The arguments of the tool call. It has to be a stringified version of the arguments to the tool. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_NAME = "gen_ai.tool.name" - """ - The name of the tool being used. - Example: "web_search" - """ - - GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. - - The output of the tool being used. - Example: "rainy, 57°F" - """ - - GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" - """ - The result of the tool call. It has to be a stringified version of the result of the tool. - Example: "rainy, 57°F" - """ - - GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" - """ - The number of tokens in the input. - Example: 150 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" - """ - The number of cached tokens in the input. - Example: 50 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" - """ - The number of tokens written to the cache when processing the AI input (prompt). - Example: 100 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" - """ - The number of tokens in the output. - Example: 250 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" - """ - The number of tokens used for reasoning in the output. - Example: 75 - """ - - GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" - """ - The total number of tokens used (input + output). - Example: 400 - """ - - GEN_AI_USER_MESSAGE = "gen_ai.user.message" - """ - The user message passed to the model. - Example: "What's the weather in Paris?" - """ - - HTTP_FRAGMENT = "http.fragment" - """ - The Fragments present in the URL. - Example: #foo=bar - """ - - HTTP_METHOD = "http.method" - """ - .. deprecated:: - This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. - - The HTTP method used. - Example: GET - """ - - HTTP_REQUEST_BODY_DATA = "http.request.body.data" - """ - HTTP request body data. Can be given as string or structural data of any format. - Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" - """ - - HTTP_REQUEST_HEADER = "http.request.header" - """ - Prefix for HTTP request header attributes. The header name (lowercased) is - appended to form the full attribute key. - Example: "http.request.header.content-type" - """ - - HTTP_REQUEST_METHOD = "http.request.method" - """ - The HTTP method used. - Example: GET - """ - - HTTP_QUERY = "http.query" - """ - The Query string present in the URL. - Example: ?foo=bar&bar=baz - """ - - HTTP_STATUS_CODE = "http.response.status_code" - """ - The HTTP status code as an integer. - Example: 418 - """ - - MESSAGING_DESTINATION_NAME = "messaging.destination.name" - """ - The destination name where the message is being consumed from, - e.g. the queue name or topic. - """ - - MESSAGING_MESSAGE_ID = "messaging.message.id" - """ - The message's identifier. - """ - - MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" - """ - The latency between when the task was enqueued and when it was started to be processed. - """ - - MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" - """ - Number of retries/attempts to process a message. - """ - - MESSAGING_SYSTEM = "messaging.system" - """ - The messaging system's name, e.g. `kafka`, `aws_sqs` - """ - - MIDDLEWARE_NAME = "middleware.name" - """ - The middleware's name, e.g. `AuthenticationMiddleware` - """ - - NETWORK_PROTOCOL_NAME = "network.protocol.name" - """ - The application layer protocol name used for the network connection. - Example: "http", "https" - """ - - NETWORK_PEER_ADDRESS = "network.peer.address" - """ - Peer address of the network connection - IP address or Unix domain socket name. - Example: 10.1.2.80, /tmp/my.sock, localhost - """ - - NETWORK_PEER_PORT = "network.peer.port" - """ - Peer port number of the network connection. - Example: 6379 - """ - - NETWORK_TRANSPORT = "network.transport" - """ - The transport protocol used for the network connection. - Example: "tcp", "udp", "unix" - """ - - PROCESS_PID = "process.pid" - """ - The process ID of the running process. - Example: 12345 - """ - - PROCESS_COMMAND_ARGS = "process.command_args" - """ - All the command arguments (including the command/executable itself) as received by the process. - Example: ["cmd/otecol","--config=config.yaml"] - """ - - PROFILER_ID = "profiler_id" - """ - Label identifying the profiler id that the span occurred in. This should be a string. - Example: "5249fbada8d5416482c2f6e47e337372" - """ - - RPC_METHOD = "rpc.method" - """ - The fully-qualified logical name of the method from the RPC interface perspective. - Example: "com.example.ExampleService/exampleMethod" - """ - - RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" - """ - Status code of the RPC returned by the RPC server or generated by the client. - Example: "DEADLINE_EXCEEDED" - """ - - SERVER_ADDRESS = "server.address" - """ - Name of the database host. - Example: example.com - """ - - SERVER_PORT = "server.port" - """ - Logical server port number - Example: 80; 8080; 443 - """ - - SERVER_SOCKET_ADDRESS = "server.socket.address" - """ - Physical server IP address or Unix socket address. - Example: 10.5.3.2 - """ - - SERVER_SOCKET_PORT = "server.socket.port" - """ - Physical server port. - Recommended: If different than server.port. - Example: 16456 - """ - - THREAD_ID = "thread.id" - """ - Identifier of a thread from where the span originated. This should be a string. - Example: "7972576320" - """ - - THREAD_NAME = "thread.name" - """ - Label identifying a thread from where the span originated. This should be a string. - Example: "MainThread" - """ - - USER_EMAIL = "user.email" - """ - User email address. - Example: "test@example.com" - """ - - USER_ID = "user.id" - """ - Unique identifier of the user. - Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" - """ - - USER_IP_ADDRESS = "user.ip_address" - """ - The IP address of the user that triggered the request. - Example: "10.1.2.80" - """ - - USER_NAME = "user.name" - """ - Short name or login/username of the user. - Example: "j.smith" - """ - - URL_FULL = "url.full" - """ - The URL of the resource that was fetched. - Example: "https://example.com/test?foo=bar#buzz" - """ - - URL_FRAGMENT = "url.fragment" - """ - The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. - Example: "details" - """ - - URL_PATH = "url.path" - """ - The URI path component. - Example: "/foo" - """ - - URL_QUERY = "url.query" - """ - The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. - Example: "foo=bar&bar=baz" - """ - - MCP_TOOL_NAME = "mcp.tool.name" - """ - The name of the MCP tool being called. - Example: "get_weather" - """ - - MCP_PROMPT_NAME = "mcp.prompt.name" - """ - The name of the MCP prompt being retrieved. - Example: "code_review" - """ - - MCP_RESOURCE_URI = "mcp.resource.uri" - """ - The URI of the MCP resource being accessed. - Example: "file:///path/to/resource" - """ - - MCP_METHOD_NAME = "mcp.method.name" - """ - The MCP protocol method name being called. - Example: "tools/call", "prompts/get", "resources/read" - """ - - MCP_REQUEST_ID = "mcp.request.id" - """ - The unique identifier for the MCP request. - Example: "req_123abc" - """ - - MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" - """ - The result/output content from an MCP tool execution. - Example: "The weather is sunny" - """ - - MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" - """ - The number of items/keys in the MCP tool result. - Example: 5 - """ - - MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" - """ - Whether the MCP tool execution resulted in an error. - Example: True - """ - - MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" - """ - The message content from an MCP prompt retrieval. - Example: "Review the following code..." - """ - - MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" - """ - The role of the message in an MCP prompt retrieval (only set for single-message prompts). - Example: "user", "assistant", "system" - """ - - MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" - """ - The number of messages in an MCP prompt result. - Example: 1, 3 - """ - - MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" - """ - The protocol/scheme of the MCP resource URI. - Example: "file", "http", "https" - """ - - MCP_TRANSPORT = "mcp.transport" - """ - The transport method used for MCP communication. - Example: "http", "sse", "stdio" - """ - - MCP_SESSION_ID = "mcp.session.id" - """ - The session identifier for the MCP connection. - Example: "a1b2c3d4e5f6" - """ - - SENTRY_DIST = "sentry.dist" - """ - The Sentry dist. - Example: "1.0" - """ - - SENTRY_ENVIRONMENT = "sentry.environment" - """ - The Sentry environment. - Example: "prod" - """ - - SENTRY_RELEASE = "sentry.release" - """ - The Sentry release. - Example: "1.2.3" - """ - - SENTRY_PLATFORM = "sentry.platform" - """ - The sdk platform that generated the event. - Example: "python" - """ - - SENTRY_SDK_NAME = "sentry.sdk.name" - """ - The name of the SDK. - Example: "python" - """ - - SENTRY_SDK_VERSION = "sentry.sdk.version" - """ - The SDK version. - Example: "1.2.3" - """ - - SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" - """ - A list of names identifying enabled integrations. - Example: ["AtexitIntegration", "StdlibIntegration"] - """ - - -class SPANSTATUS: - """ - The status of a Sentry span. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - - ABORTED = "aborted" - ALREADY_EXISTS = "already_exists" - CANCELLED = "cancelled" - DATA_LOSS = "data_loss" - DEADLINE_EXCEEDED = "deadline_exceeded" - FAILED_PRECONDITION = "failed_precondition" - INTERNAL_ERROR = "internal_error" - INVALID_ARGUMENT = "invalid_argument" - NOT_FOUND = "not_found" - OK = "ok" - OUT_OF_RANGE = "out_of_range" - PERMISSION_DENIED = "permission_denied" - RESOURCE_EXHAUSTED = "resource_exhausted" - UNAUTHENTICATED = "unauthenticated" - UNAVAILABLE = "unavailable" - UNIMPLEMENTED = "unimplemented" - UNKNOWN_ERROR = "unknown_error" - - -class OP: - ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" - CACHE_GET = "cache.get" - CACHE_PUT = "cache.put" - COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" - COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" - DB = "db" - DB_CURSOR_ITERATOR = "db.cursor.iter" - DB_CURSOR_FETCH = "db.cursor.fetch" - DB_REDIS = "db.redis" - EVENT_DJANGO = "event.django" - FUNCTION = "function" - FUNCTION_AWS = "function.aws" - FUNCTION_GCP = "function.gcp" - GEN_AI_CHAT = "gen_ai.chat" - GEN_AI_CREATE_AGENT = "gen_ai.create_agent" - GEN_AI_EMBEDDINGS = "gen_ai.embeddings" - GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" - GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" - GEN_AI_HANDOFF = "gen_ai.handoff" - GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" - GEN_AI_RESPONSES = "gen_ai.responses" - GRAPHQL_EXECUTE = "graphql.execute" - GRAPHQL_MUTATION = "graphql.mutation" - GRAPHQL_PARSE = "graphql.parse" - GRAPHQL_RESOLVE = "graphql.resolve" - GRAPHQL_SUBSCRIPTION = "graphql.subscription" - GRAPHQL_QUERY = "graphql.query" - GRAPHQL_VALIDATE = "graphql.validate" - GRPC_CLIENT = "grpc.client" - GRPC_SERVER = "grpc.server" - HTTP_CLIENT = "http.client" - HTTP_CLIENT_STREAM = "http.client.stream" - HTTP_SERVER = "http.server" - MIDDLEWARE_DJANGO = "middleware.django" - MIDDLEWARE_LITESTAR = "middleware.litestar" - MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" - MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" - MIDDLEWARE_STARLETTE = "middleware.starlette" - MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" - MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" - MIDDLEWARE_STARLITE = "middleware.starlite" - MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" - MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" - HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( - "ai.chat_completions.create.huggingface_hub" - ) - QUEUE_PROCESS = "queue.process" - QUEUE_PUBLISH = "queue.publish" - QUEUE_SUBMIT_ARQ = "queue.submit.arq" - QUEUE_TASK_ARQ = "queue.task.arq" - QUEUE_SUBMIT_CELERY = "queue.submit.celery" - QUEUE_TASK_CELERY = "queue.task.celery" - QUEUE_TASK_RQ = "queue.task.rq" - QUEUE_SUBMIT_HUEY = "queue.submit.huey" - QUEUE_TASK_HUEY = "queue.task.huey" - QUEUE_SUBMIT_RAY = "queue.submit.ray" - QUEUE_TASK_RAY = "queue.task.ray" - QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" - QUEUE_SUBMIT_DJANGO = "queue.submit.django" - SUBPROCESS = "subprocess" - SUBPROCESS_WAIT = "subprocess.wait" - SUBPROCESS_COMMUNICATE = "subprocess.communicate" - TEMPLATE_RENDER = "template.render" - VIEW_RENDER = "view.render" - VIEW_RESPONSE_RENDER = "view.response.render" - WEBSOCKET_SERVER = "websocket.server" - SOCKET_CONNECTION = "socket.connection" - SOCKET_DNS = "socket.dns" - MCP_SERVER = "mcp.server" - - -# This type exists to trick mypy and PyCharm into thinking `init` and `Client` -# take these arguments (even though they take opaque **kwargs) -class ClientConstructor: - def __init__( - self, - dsn: "Optional[str]" = None, - *, - max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - server_name: "Optional[str]" = None, - shutdown_timeout: float = 2, - integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 - in_app_include: "List[str]" = [], # noqa: B006 - in_app_exclude: "List[str]" = [], # noqa: B006 - default_integrations: bool = True, - dist: "Optional[str]" = None, - transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, - transport_queue_size: int = DEFAULT_QUEUE_SIZE, - sample_rate: float = 1.0, - send_default_pii: "Optional[bool]" = None, - http_proxy: "Optional[str]" = None, - https_proxy: "Optional[str]" = None, - ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 - max_request_body_size: str = "medium", - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, - keep_alive: "Optional[bool]" = None, - before_send: "Optional[EventProcessor]" = None, - before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, - debug: "Optional[bool]" = None, - attach_stacktrace: bool = False, - ca_certs: "Optional[str]" = None, - propagate_traces: bool = True, - traces_sample_rate: "Optional[float]" = None, - traces_sampler: "Optional[TracesSampler]" = None, - profiles_sample_rate: "Optional[float]" = None, - profiles_sampler: "Optional[TracesSampler]" = None, - profiler_mode: "Optional[ProfilerMode]" = None, - profile_lifecycle: 'Literal["manual", "trace"]' = "manual", - profile_session_sample_rate: "Optional[float]" = None, - auto_enabling_integrations: bool = True, - disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, - auto_session_tracking: bool = True, - send_client_reports: bool = True, - _experiments: "Experiments" = {}, # noqa: B006 - proxy_headers: "Optional[Dict[str, str]]" = None, - instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, - before_send_transaction: "Optional[TransactionProcessor]" = None, - project_root: "Optional[str]" = None, - enable_tracing: "Optional[bool]" = None, - include_local_variables: "Optional[bool]" = True, - include_source_context: "Optional[bool]" = True, - trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 - MATCH_ALL - ], - functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 - event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, - max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, - enable_backpressure_handling: bool = True, - error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, - enable_db_query_source: bool = True, - db_query_source_threshold_ms: int = 100, - enable_http_request_source: bool = True, - http_request_source_threshold_ms: int = 100, - spotlight: "Optional[Union[bool, str]]" = None, - cert_file: "Optional[str]" = None, - key_file: "Optional[str]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, - add_full_stack: bool = DEFAULT_ADD_FULL_STACK, - max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, - enable_logs: bool = False, - before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, - trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), - enable_metrics: bool = True, - before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, - org_id: "Optional[str]" = None, - strict_trace_continuation: bool = False, - stream_gen_ai_spans: bool = True, - ) -> None: - """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. - - :param dsn: The DSN tells the SDK where to send the events. - - If this option is not set, the SDK will just not send any data. - - The `dsn` config option takes precedence over the environment variable. - - Learn more about `DSN utilization `_. - - :param debug: Turns debug mode on or off. - - When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes - wrong with event sending. - - The default is always `False`. It's generally not recommended to turn it on in production because of the - increase in log output. - - The `debug` config option takes precedence over the environment variable. - - :param release: Sets the release. - - If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to - manually set it to guarantee that the release is in sync with your deploy integrations. - - Release names are strings, but some formats are detected by Sentry and might be rendered differently. - - See `the releases documentation `_ to learn how the SDK tries to - automatically configure a release. - - The `release` config option takes precedence over the environment variable. - - Learn more about how to send release data so Sentry can tell you about regressions between releases and - identify the potential source in `the product documentation `_. - - :param environment: Sets the environment. This string is freeform and set to `production` by default. - - A release can be associated with more than one environment to separate them in the UI (think `staging` vs - `production` or similar). - - The `environment` config option takes precedence over the environment variable. - - :param dist: The distribution of the application. - - Distributions are used to disambiguate build or deployment variants of the same release of an application. - - The dist can be for example a build number. - - :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. - - The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of - error events will be sent. - - Events are picked randomly. - - :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. - - This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and - which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number - between `0.0` and `1.0`, inclusive. - - The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to - send the event with the given probability. - - If this configuration option is specified, the `sample_rate` option is ignored. - - :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. - - Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're - sent to Sentry. - - By default, all errors are sent. - - :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. - - This defaults to `100`, but you can set this to any number. - - However, you should be aware that Sentry has a `maximum payload size `_ - and any events exceeding that payload size will be dropped. - - :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. - - Stack traces are always attached to exceptions; however, when this option is set, stack traces are also - sent with messages. - - This option means that stack traces appear next to all log messages. - - Grouping in Sentry is different for events with stack traces and without. As a result, you will get new - groups as you enable or disable this flag for certain events. - - :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) - `_ is added by active integrations. - - If you enable this option, be sure to manually remove what you don't want to send using our features for - managing `Sensitive Data `_. - - :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and - passwords from a `denylist`. - - It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. - - See how to `configure the scrubber here `_. - - :param include_source_context: When enabled, source context will be included in events sent to Sentry. - - This source context includes the five lines of code above and below the line of code where an error - happened. - - :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with - the event to help with debugging. - - :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the - moment an error occurs. - - But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will - be included in the stack trace sent to Sentry. - - :param max_stack_frames: This option limits the number of stack frames that will be captured when - `add_full_stack` is enabled. - - :param server_name: This option can be used to supply a server name. - - When provided, the name of the server is sent along and persisted in the event. - - For many integrations, the server name actually corresponds to the device hostname, even in situations - where the machine is not actually a server. - - :param project_root: The full path to the root directory of your application. - - The `project_root` is used to mark frames in a stack trace either as being in your application or outside - of the application. - - :param in_app_include: A list of string prefixes of module names that belong to the app. - - This option takes precedence over `in_app_exclude`. - - Sentry differentiates stack frames that are directly related to your application ("in application") from - stack frames that come from other packages such as the standard library, frameworks, or other dependencies. - - The application package is automatically marked as `inApp`. - - The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are - displayed by default. - - :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to - third-party packages. - - Modules considered not part of the app will be hidden from stack traces by default. - - This option can be overridden using `in_app_include`. - - :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. - It can be set to one of the following values: - - - `never`: Request bodies are never sent. - - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically - 4KB). - - `medium`: Medium and small requests will be captured (typically 10KB). - - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. - - Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ - expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of - how you configure this option. - - :param max_value_length: The number of characters after which the values containing text in the event payload - will be truncated. - - WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be - dropped by Sentry. - - :param ca_certs: A path to an alternative CA bundle file in PEM-format. - - :param send_client_reports: Set this boolean to `False` to disable sending of client reports. - - Client reports allow the client to send status reports about itself to Sentry, such as information about - events that were dropped before being sent. - - :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) - `_. - - This setting can be used to override the default config options for a specific auto-enabling integration - or to add an integration that is not auto-enabled. - - :param disabled_integrations: List of integrations that will be disabled. - - This setting can be used to explicitly turn off specific `auto-enabling integrations (list) - `_ or - `default `_ integrations. - - :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) - `_ should be enabled. - - When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding - framework/library is detected. - - :param default_integrations: Configures whether `default integrations - `_ should be enabled. - - Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling - integrations**, unless they are specifically added in the `integrations` option, described above. - - :param before_send: This function is called with an SDK-specific message or error event object, and can return - a modified event object, or `null` to skip reporting the event. - - This can be used, for instance, for manual PII stripping before sending. - - By the time `before_send` is executed, all scope data has already been applied to the event. Further - modification of the scope won't have any effect. - - :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can - return a modified transaction event object, or `null` to skip reporting the event. - - One way this might be used is for manual PII stripping before sending. - - :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb - is added to the scope. - - When nothing is returned from the function, the breadcrumb is dropped. - - To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. - - The callback typically gets a second argument (called a "hint") which contains the original object from - which the breadcrumb was created to further customize what the breadcrumb should look like. - - :param transport: Switches out the transport used to send events. - - How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to - send it through some more complex setup that requires proxy authentication. - - :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to - flush. - - :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. - - This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs - support a separate HTTPS proxy. - - SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix - systems, the `http_proxy` environment variable will be picked up. - - :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. - - This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for - HTTPS requests at all times. - - :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded - to `urllib3`'s `ProxyManager `_. - - :param shutdown_timeout: Controls how many seconds to wait before shutting down. - - Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending - events. The default is SDK specific but typically around two seconds. - - Setting this value too low may cause problems for sending events from command line applications. - - Setting the value too high will cause the application to block for a long time for users experiencing - network connectivity problems. - - :param keep_alive: Determines whether to keep the connection alive between requests. - - This can be useful in environments where you encounter frequent network issues such as connection resets. - - :param cert_file: Path to the client certificate to use. - - If set, supersedes the `CLIENT_CERT_FILE` environment variable. - - :param key_file: Path to the key file to use. - - If set, supersedes the `CLIENT_KEY_FILE` environment variable. - - :param socket_options: An optional list of socket options to use. - - These provide fine-grained, low-level control over the way the SDK connects to Sentry. - - If provided, the options will override the default `urllib3` `socket options - `_. - - :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction - will be sent to Sentry. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - Either this or `traces_sampler` must be defined to enable tracing. - - If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have - another service (for example a JS frontend) that makes requests to your service that include trace - information, those traces will be continued and thus transactions will be sent to Sentry. - - If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces - will be started and no incoming traces will be continued. - - :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be - sent to Sentry. - - It will automatically be passed information about the transaction and the context in which it's being - created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being - sent). - - Can also be used for filtering transactions, by returning `0` for those that are unwanted. - - Either this or `traces_sample_rate` must be defined to enable tracing. - - :param trace_propagation_targets: An optional property that controls which downstream services receive tracing - data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. - - The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. - - If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to - that request. - - String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ - a string provided through the option. - - If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the - instrumented client. - - :param functions_to_trace: An optional list of functions that should be set up for tracing. - - For each function in the list, a span will be created when the function is executed. - - Functions in the list are represented as strings containing the fully qualified name of the function. - - This is a convenient option, making it possible to have one central place for configuring what functions - to trace, instead of having custom instrumentation scattered all over your code base. - - To learn more, see the `Custom Instrumentation `_ documentation. - - :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health - checks on the SDK. - - If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second - intervals until recovery. - - This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. - - This option is enabled by default. - - :param enable_db_query_source: When enabled, the source location will be added to database queries. - - :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database - queries. - - The query location will be added to the query for queries slower than the specified threshold. - - :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. - - :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an - outgoing HTTP request. - - The request location will be added to the request for requests slower than the specified threshold. - - :param custom_repr: A custom `repr `_ function to run - while serializing an object. - - Use this to control how your custom objects and classes are visible in Sentry. - - Return a string for that repr value to be used or `None` to continue serializing how Sentry would have - done it anyway. - - :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled - transaction will be profiled. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be - profiled. - - :param profiles_sampler: - - :param profiler_mode: - - :param profile_lifecycle: - - :param profile_session_sample_rate: - - :param enable_tracing: - - :param propagate_traces: - - :param auto_session_tracking: - - :param spotlight: - - :param instrumenter: - - :param enable_logs: Set `enable_logs` to True to enable the SDK to emit - Sentry logs. Defaults to False. - - :param before_send_log: An optional function to modify or filter out logs - before they're sent to Sentry. Any modifications to the log in this - function will be retained. If the function returns None, the log will - not be sent to Sentry. - - :param trace_ignore_status_codes: An optional property that disables tracing for - HTTP requests with certain status codes. - - Requests are not traced if the status code is contained in the provided set. - - If `trace_ignore_status_codes` is not provided, requests with any status code - may be traced. - - This option has no effect in span streaming mode (`trace_lifecycle="stream"`). - - :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the - `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. - - If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. - - The client's organization ID is extracted from the DSN or can be set with the `org_id` option. - If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. - This is useful to prevent traces of unknown third-party services from being continued in your application. - - :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases - but you can provide it explicitly for self-hosted and Relay setups. This value is used for - trace propagation and for features like `strict_trace_continuation`. - - :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to - reduce downstream data loss. - - :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. - - ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations - collect automatically, superseding `send_default_pii`. Passing a dict under - `_experiments={"data_collection": {...}}` opts into the feature; 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 - changes nothing. Restrict collection per category (user identity, cookies, HTTP - headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source - context). If `send_default_pii` is also set, `data_collection` takes precedence. - - Example:: - - sentry_sdk.init( - dsn="...", - _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, - ) - - See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. - """ - pass - - -def _get_default_options() -> "dict[str, Any]": - import inspect - - a = inspect.getfullargspec(ClientConstructor.__init__) - defaults = a.defaults or () - kwonlydefaults = a.kwonlydefaults or {} - - return dict( - itertools.chain( - zip(a.args[-len(defaults) :], defaults), - kwonlydefaults.items(), - ) - ) - - -DEFAULT_OPTIONS = _get_default_options() -del _get_default_options - - -VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py deleted file mode 100644 index b3287703b9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.crons.api import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.crons.decorator import monitor - -__all__ = [ - "capture_checkin", - "MonitorStatus", - "monitor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py deleted file mode 100644 index 6ea3e36b6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/api.py +++ /dev/null @@ -1,60 +0,0 @@ -import uuid -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, MonitorConfig - - -def _create_check_in_event( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration_s: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> "Event": - options = sentry_sdk.get_client().options - check_in_id = check_in_id or uuid.uuid4().hex - - check_in: "Event" = { - "type": "check_in", - "monitor_slug": monitor_slug, - "check_in_id": check_in_id, - "status": status, - "duration": duration_s, - "environment": options.get("environment", None), - "release": options.get("release", None), - } - - if monitor_config: - check_in["monitor_config"] = monitor_config - - return check_in - - -def capture_checkin( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> str: - check_in_event = _create_check_in_event( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=status, - duration_s=duration, - monitor_config=monitor_config, - ) - - sentry_sdk.capture_event(check_in_event) - - logger.debug( - f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" - ) - - return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py deleted file mode 100644 index be686b4539..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/consts.py +++ /dev/null @@ -1,4 +0,0 @@ -class MonitorStatus: - IN_PROGRESS = "in_progress" - OK = "ok" - ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py deleted file mode 100644 index b13d350e15..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/crons/decorator.py +++ /dev/null @@ -1,137 +0,0 @@ -from functools import wraps -from inspect import iscoroutinefunction -from typing import TYPE_CHECKING - -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from types import TracebackType - from typing import ( - Any, - Optional, - ParamSpec, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from sentry_sdk._types import MonitorConfig - - P = ParamSpec("P") - R = TypeVar("R") - - -class monitor: # noqa: N801 - """ - Decorator/context manager to capture checkin events for a monitor. - - Usage (as decorator): - ``` - import sentry_sdk - - app = Celery() - - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` - - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. - - Usage (as context manager): - ``` - import sentry_sdk - - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` - """ - - def __init__( - self, - monitor_slug: "Optional[str]" = None, - monitor_config: "Optional[MonitorConfig]" = None, - ) -> None: - self.monitor_slug = monitor_slug - self.monitor_config = monitor_config - - def __enter__(self) -> None: - self.start_timestamp = now() - self.check_in_id = capture_checkin( - monitor_slug=self.monitor_slug, - status=MonitorStatus.IN_PROGRESS, - monitor_config=self.monitor_config, - ) - - def __exit__( - self, - exc_type: "Optional[Type[BaseException]]", - exc_value: "Optional[BaseException]", - traceback: "Optional[TracebackType]", - ) -> None: - duration_s = now() - self.start_timestamp - - if exc_type is None and exc_value is None and traceback is None: - status = MonitorStatus.OK - else: - status = MonitorStatus.ERROR - - capture_checkin( - monitor_slug=self.monitor_slug, - check_in_id=self.check_in_id, - status=status, - duration=duration_s, - monitor_config=self.monitor_config, - ) - - if TYPE_CHECKING: - - @overload - def __call__( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - # Unfortunately, mypy does not give us any reliable way to type check the - # return value of an Awaitable (i.e. async function) for this overload, - # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. - ... - - @overload - def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... - - def __call__( - self, - fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", - ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": - if iscoroutinefunction(fn): - return self._async_wrapper(fn) - - else: - if TYPE_CHECKING: - fn = cast("Callable[P, R]", fn) - return self._sync_wrapper(fn) - - def _async_wrapper( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - @wraps(fn) - async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return await fn(*args, **kwargs) - - return inner - - def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": - @wraps(fn) - def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return fn(*args, **kwargs) - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py deleted file mode 100644 index bcdf767409..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/data_collection.py +++ /dev/null @@ -1,319 +0,0 @@ -""" -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, List, Mapping, Optional, cast - -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE - -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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: - """ - Return whether ``key`` matches the sensitive denylist using a partial, - case-insensitive substring match. - - :param extra_terms: additional deny terms (e.g. user-provided) to consider - alongside the built-in `_SENSITIVE_DENYLIST`. - """ - lowered = key.lower() - for term in _SENSITIVE_DENYLIST: - if term in lowered: - return True - if extra_terms: - for term in extra_terms: - if term and term.lower() in lowered: - return True - return False - - -def _apply_key_value_collection_filtering( - items: "Mapping[str, Any]", - behaviour: "KeyValueCollectionBehaviour", - substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, -) -> "Dict[str, Any]": - - if behaviour["mode"] == "off": - return {} - - result: "Dict[str, Any]" = {} - - if behaviour["mode"] == "allowlist": - for key, value in items.items(): - is_allowed = False - if isinstance(key, str): - lowered = key.lower() - is_allowed = any( - term and term.lower() in lowered - for term in behaviour.get("terms", []) - ) - if is_allowed and not _is_sensitive_key(key): - result[key] = value - else: - result[key] = substitute - return result - - # denylist behaviour - for key, value in items.items(): - if isinstance(key, str) and _is_sensitive_key( - key=key, extra_terms=behaviour.get("terms", []) - ): - result[key] = substitute - else: - result[key] = value - return result - - -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, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py deleted file mode 100644 index 795882e9ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/debug.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -import sys -import warnings -from logging import LogRecord - -from sentry_sdk import get_client -from sentry_sdk.client import _client_init_debug -from sentry_sdk.utils import logger - - -class _DebugFilter(logging.Filter): - def filter(self, record: "LogRecord") -> bool: - if _client_init_debug.get(False): - return True - - return get_client().options["debug"] - - -def init_debug_support() -> None: - if not logger.handlers: - configure_logger() - - -def configure_logger() -> None: - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.DEBUG) - logger.addFilter(_DebugFilter()) - - -def configure_debug_hub() -> None: - warnings.warn( - "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py deleted file mode 100644 index d2d4aae31a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/envelope.py +++ /dev/null @@ -1,332 +0,0 @@ -import io -import json -import mimetypes -from typing import TYPE_CHECKING - -from sentry_sdk.session import Session -from sentry_sdk.utils import capture_internal_exceptions, json_dumps - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, List, Optional, Union - - from sentry_sdk._types import Event, EventDataCategory - - -def parse_json(data: "Union[bytes, str]") -> "Any": - # on some python 3 versions this needs to be bytes - if isinstance(data, bytes): - data = data.decode("utf-8", "replace") - return json.loads(data) - - -class Envelope: - """ - Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints - documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, - each envelope may have at most one Item with type "event" or "transaction" (but not both). - """ - - def __init__( - self, - headers: "Optional[Dict[str, Any]]" = None, - items: "Optional[List[Item]]" = None, - ) -> None: - if headers is not None: - headers = dict(headers) - self.headers = headers or {} - if items is None: - items = [] - else: - items = list(items) - self.items = items - - @property - def description(self) -> str: - return "envelope with %s items (%s)" % ( - len(self.items), - ", ".join(x.data_category for x in self.items), - ) - - def add_event( - self, - event: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=event), type="event")) - - def add_transaction( - self, - transaction: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - - def add_profile( - self, - profile: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) - - def add_profile_chunk( - self, - profile_chunk: "Any", - ) -> None: - self.add_item( - Item( - payload=PayloadRef(json=profile_chunk), - type="profile_chunk", - headers={"platform": profile_chunk.get("platform", "python")}, - ) - ) - - def add_checkin( - self, - checkin: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) - - def add_session( - self, - session: "Union[Session, Any]", - ) -> None: - if isinstance(session, Session): - session = session.to_json() - self.add_item(Item(payload=PayloadRef(json=session), type="session")) - - def add_sessions( - self, - sessions: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) - - def add_item( - self, - item: "Item", - ) -> None: - self.items.append(item) - - def get_event(self) -> "Optional[Event]": - for items in self.items: - event = items.get_event() - if event is not None: - return event - return None - - def get_transaction_event(self) -> "Optional[Event]": - for item in self.items: - event = item.get_transaction_event() - if event is not None: - return event - return None - - def __iter__(self) -> "Iterator[Item]": - return iter(self.items) - - def serialize_into( - self, - f: "Any", - ) -> None: - f.write(json_dumps(self.headers)) - f.write(b"\n") - for item in self.items: - item.serialize_into(f) - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Envelope": - headers = parse_json(f.readline()) - items = [] - while 1: - item = Item.deserialize_from(f) - if item is None: - break - items.append(item) - return cls(headers=headers, items=items) - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Envelope": - return cls.deserialize_from(io.BytesIO(bytes)) - - def __repr__(self) -> str: - return "" % (self.headers, self.items) - - -class PayloadRef: - def __init__( - self, - bytes: "Optional[bytes]" = None, - path: "Optional[Union[bytes, str]]" = None, - json: "Optional[Any]" = None, - ) -> None: - self.json = json - self.bytes = bytes - self.path = path - - def get_bytes(self) -> bytes: - if self.bytes is None: - if self.path is not None: - with capture_internal_exceptions(): - with open(self.path, "rb") as f: - self.bytes = f.read() - elif self.json is not None: - self.bytes = json_dumps(self.json) - return self.bytes or b"" - - @property - def inferred_content_type(self) -> str: - if self.json is not None: - return "application/json" - elif self.path is not None: - path = self.path - if isinstance(path, bytes): - path = path.decode("utf-8", "replace") - ty = mimetypes.guess_type(path)[0] - if ty: - return ty - return "application/octet-stream" - - def __repr__(self) -> str: - return "" % (self.inferred_content_type,) - - -class Item: - def __init__( - self, - payload: "Union[bytes, str, PayloadRef]", - headers: "Optional[Dict[str, Any]]" = None, - type: "Optional[str]" = None, - content_type: "Optional[str]" = None, - filename: "Optional[str]" = None, - ): - if headers is not None: - headers = dict(headers) - elif headers is None: - headers = {} - self.headers = headers - if isinstance(payload, bytes): - payload = PayloadRef(bytes=payload) - elif isinstance(payload, str): - payload = PayloadRef(bytes=payload.encode("utf-8")) - else: - payload = payload - - if filename is not None: - headers["filename"] = filename - if type is not None: - headers["type"] = type - if content_type is not None: - headers["content_type"] = content_type - elif "content_type" not in headers: - headers["content_type"] = payload.inferred_content_type - - self.payload = payload - - def __repr__(self) -> str: - return "" % ( - self.headers, - self.payload, - self.data_category, - ) - - @property - def type(self) -> "Optional[str]": - return self.headers.get("type") - - @property - def data_category(self) -> "EventDataCategory": - ty = self.headers.get("type") - if ty == "session" or ty == "sessions": - return "session" - elif ty == "attachment": - return "attachment" - elif ty == "transaction": - return "transaction" - elif ty == "span": - return "span" - elif ty == "event": - return "error" - elif ty == "log": - return "log_item" - elif ty == "trace_metric": - return "trace_metric" - elif ty == "client_report": - return "internal" - elif ty == "profile": - return "profile" - elif ty == "profile_chunk": - return "profile_chunk" - elif ty == "check_in": - return "monitor" - else: - return "default" - - def get_bytes(self) -> bytes: - return self.payload.get_bytes() - - def get_event(self) -> "Optional[Event]": - """ - Returns an error event if there is one. - """ - if self.type == "event" and self.payload.json is not None: - return self.payload.json - return None - - def get_transaction_event(self) -> "Optional[Event]": - if self.type == "transaction" and self.payload.json is not None: - return self.payload.json - return None - - def serialize_into( - self, - f: "Any", - ) -> None: - headers = dict(self.headers) - bytes = self.get_bytes() - headers["length"] = len(bytes) - f.write(json_dumps(headers)) - f.write(b"\n") - f.write(bytes) - f.write(b"\n") - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Optional[Item]": - line = f.readline().rstrip() - if not line: - return None - headers = parse_json(line) - length = headers.get("length") - if length is not None: - payload = f.read(length) - f.readline() - else: - # if no length was specified we need to read up to the end of line - # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) - payload = f.readline().rstrip(b"\n") - if headers.get("type") in ("event", "transaction"): - rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) - else: - rv = cls(headers=headers, payload=payload) - return rv - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Optional[Item]": - return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py deleted file mode 100644 index 5eaa5e440b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/feature_flags.py +++ /dev/null @@ -1,75 +0,0 @@ -import copy -from threading import Lock -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import TypedDict - - FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) - - -DEFAULT_FLAG_CAPACITY = 100 - - -class FlagBuffer: - def __init__(self, capacity: int) -> None: - self.capacity = capacity - self.lock = Lock() - - # Buffer is private. The name is mangled to discourage use. If you use this attribute - # directly you're on your own! - self.__buffer = LRUCache(capacity) - - def clear(self) -> None: - self.__buffer = LRUCache(self.capacity) - - def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": - with self.lock: - buffer = FlagBuffer(self.capacity) - buffer.__buffer = copy.deepcopy(self.__buffer, memo) - return buffer - - def get(self) -> "list[FlagData]": - with self.lock: - return [ - {"flag": key, "result": value} for key, value in self.__buffer.get_all() - ] - - def set(self, flag: str, result: bool) -> None: - if isinstance(result, FlagBuffer): - # If someone were to insert `self` into `self` this would create a circular dependency - # on the lock. This is of course a deadlock. However, this is far outside the expected - # usage of this class. We guard against it here for completeness and to document this - # expected failure mode. - raise ValueError( - "FlagBuffer instances can not be inserted into the dictionary." - ) - - with self.lock: - self.__buffer.set(flag, result) - - -def add_feature_flag(flag: str, result: bool) -> None: - """ - Records a flag and its value to be sent on subsequent error events. - We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. - """ - client = sentry_sdk.get_client() - - flags = sentry_sdk.get_isolation_scope().flags - flags.set(flag, result) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.get_current_span() - if span and isinstance(span, sentry_sdk.traces.StreamedSpan): - span.set_attribute(f"flag.evaluation.{flag}", result) - - else: - span = sentry_sdk.get_current_span() - if span and isinstance(span, Span): - span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py deleted file mode 100644 index b17444d06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/hub.py +++ /dev/null @@ -1,741 +0,0 @@ -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import ( - get_client, - get_current_scope, - get_global_scope, - get_isolation_scope, -) -from sentry_sdk._compat import with_metaclass -from sentry_sdk.client import Client -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.scope import _ScopeManager -from sentry_sdk.tracing import ( - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.utils import ( - ContextVar, - logger, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.tracing import TransactionKwargs - - T = TypeVar("T") - -else: - - def overload(x: "T") -> "T": - return x - - -class SentryHubDeprecationWarning(DeprecationWarning): - """ - A custom deprecation warning to inform users that the Hub is deprecated. - """ - - _MESSAGE = ( - "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " - "Please consult our 1.x to 2.x migration guide for details on how to migrate " - "`Hub` usage to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" - ) - - def __init__(self, *_: object) -> None: - super().__init__(self._MESSAGE) - - -@contextmanager -def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": - """Utility function to suppress deprecation warnings for the Hub.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) - yield - - -_local = ContextVar("sentry_current_hub") - - -class HubMeta(type): - @property - def current(cls) -> "Hub": - """Returns the current instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - rv = _local.get(None) - if rv is None: - with _suppress_hub_deprecation_warning(): - # This will raise a deprecation warning; suppress it since we already warned above. - rv = Hub(GLOBAL_HUB) - _local.set(rv) - return rv - - @property - def main(cls) -> "Hub": - """Returns the main instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - return GLOBAL_HUB - - -class Hub(with_metaclass(HubMeta)): # type: ignore - """ - .. deprecated:: 2.0.0 - The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. - - The hub wraps the concurrency management of the SDK. Each thread has - its own hub but the hub might transfer with the flow of execution if - context vars are available. - - If the hub is used with a with statement it's temporarily activated. - """ - - _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] - _scope: "Optional[Scope]" = None - - # Mypy doesn't pick up on the metaclass. - - if TYPE_CHECKING: - current: "Hub" = None # type: ignore[assignment] - main: "Optional[Hub]" = None - - def __init__( - self, - client_or_hub: "Optional[Union[Hub, Client]]" = None, - scope: "Optional[Any]" = None, - ) -> None: - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - - current_scope = None - - if isinstance(client_or_hub, Hub): - client = get_client() - if scope is None: - # hub cloning is going on, we use a fork of the current/isolation scope for context manager - scope = get_isolation_scope().fork() - current_scope = get_current_scope().fork() - else: - client = client_or_hub - get_global_scope().set_client(client) - - if scope is None: # so there is no Hub cloning going on - # just the current isolation scope is used for context manager - scope = get_isolation_scope() - current_scope = get_current_scope() - - if current_scope is None: - # just the current current scope is used for context manager - current_scope = get_current_scope() - - self._stack = [(client, scope)] # type: ignore - self._last_event_id: "Optional[str]" = None - self._old_hubs: "List[Hub]" = [] - - self._old_current_scopes: "List[Scope]" = [] - self._old_isolation_scopes: "List[Scope]" = [] - self._current_scope: "Scope" = current_scope - self._scope: "Scope" = scope - - def __enter__(self) -> "Hub": - self._old_hubs.append(Hub.current) - _local.set(self) - - current_scope = get_current_scope() - self._old_current_scopes.append(current_scope) - scope._current_scope.set(self._current_scope) - - isolation_scope = get_isolation_scope() - self._old_isolation_scopes.append(isolation_scope) - scope._isolation_scope.set(self._scope) - - return self - - def __exit__( - self, - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[Any]", - ) -> None: - old = self._old_hubs.pop() - _local.set(old) - - old_current_scope = self._old_current_scopes.pop() - scope._current_scope.set(old_current_scope) - - old_isolation_scope = self._old_isolation_scopes.pop() - scope._isolation_scope.set(old_isolation_scope) - - def run( - self, - callback: "Callable[[], T]", - ) -> "T": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Runs a callback in the context of the hub. Alternatively the - with statement can be used on the hub directly. - """ - with self: - return callback() - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Any": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. - - Returns the integration for this hub by name or class. If there - is no client bound or the client does not have that integration - then `None` is returned. - - If the return value is not `None` the hub is guaranteed to have a - client attached. - """ - return get_client().get_integration(name_or_class) - - @property - def client(self) -> "Optional[BaseClient]": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Please use :py:func:`sentry_sdk.api.get_client` instead. - - Returns the current client on the hub. - """ - client = get_client() - - if not client.is_active(): - return None - - return client - - @property - def scope(self) -> "Scope": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Returns the current scope on the hub. - """ - return get_isolation_scope() - - def last_event_id(self) -> "Optional[str]": - """ - Returns the last event ID. - - .. deprecated:: 1.40.5 - This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. - """ - logger.warning( - "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." - ) - return self._last_event_id - - def bind_client( - self, - new: "Optional[BaseClient]", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.set_client` instead. - - Binds a new client to the hub. - """ - get_global_scope().set_client(new) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. - - Captures an event. - - Alias of :py:meth:`sentry_sdk.Scope.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - """ - last_event_id = get_current_scope().capture_event( - event, hint, scope=scope, **scope_kwargs - ) - - is_transaction = event.get("type") == "transaction" - if last_event_id is not None and not is_transaction: - self._last_event_id = last_event_id - - return last_event_id - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. - - Captures a message. - - Alias of :py:meth:`sentry_sdk.Scope.capture_message`. - - :param message: The string to send as the message to Sentry. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_message( - message, level=level, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. - - Captures an exception. - - Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_exception( - error, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. - - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_span` instead. - - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - """ - scope = get_current_scope() - return scope.start_span(instrumenter=instrumenter, **kwargs) - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. - - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. - """ - scope = get_current_scope() - - # For backwards compatibility, we allow passing the scope as the hub. - # We need a major release to make this nice. (if someone searches the code: deprecated) - # Type checking disabled for this line because deprecated keys are not allowed in the type signature. - kwargs["hub"] = scope # type: ignore - - return scope.start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - ) -> "Transaction": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. - - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers=environ_or_headers, op=op, name=name, source=source - ) - - @overload - def push_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def push_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def push_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - if callback is not None: - with self.push_scope() as scope: - callback(scope) - return None - - return _ScopeManager(self) - - def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pops a scope layer from the stack. - - Try to use the context manager :py:meth:`push_scope` instead. - """ - rv = self._stack.pop() - assert self._stack, "stack must have at least one layer" - return rv - - @overload - def configure_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def configure_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def configure_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - scope = get_isolation_scope() - - if continue_trace: - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - def start_session( - self, - session_mode: str = "application", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_session` instead. - - Starts a new session. - """ - get_isolation_scope().start_session( - session_mode=session_mode, - ) - - def end_session(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.end_session` instead. - - Ends the current session if there is one. - """ - get_isolation_scope().end_session() - - def stop_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. - - Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - get_isolation_scope().stop_auto_session_tracking() - - def resume_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. - - Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - get_isolation_scope().resume_auto_session_tracking() - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.flush` instead. - - Alias for :py:meth:`sentry_sdk.client._Client.flush` - """ - return get_client().flush(timeout=timeout, callback=callback) - - def get_traceparent(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. - - Returns the traceparent either from the active span or from the scope. - """ - current_scope = get_current_scope() - traceparent = current_scope.get_traceparent() - - if traceparent is None: - isolation_scope = get_isolation_scope() - traceparent = isolation_scope.get_traceparent() - - return traceparent - - def get_baggage(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. - - Returns Baggage either from the active span or from the scope. - """ - current_scope = get_current_scope() - baggage = current_scope.get_baggage() - - if baggage is None: - isolation_scope = get_isolation_scope() - baggage = isolation_scope.get_baggage() - - if baggage is not None: - return baggage.serialize() - - return None - - def iter_trace_propagation_headers( - self, span: "Optional[Span]" = None - ) -> "Generator[Tuple[str, str], None, None]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. - - Return HTTP headers which allow propagation of trace data. Data taken - from the span representing the request, if available, or the current - span on the scope if not. - """ - return get_current_scope().iter_trace_propagation_headers( - span=span, - ) - - def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. - - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - return get_current_scope().trace_propagation_meta( - span=span, - ) - - -with _suppress_hub_deprecation_warning(): - # Suppress deprecation warning for the Hub here, since we still always - # import this module. - GLOBAL_HUB = Hub() -_local.set(GLOBAL_HUB) - - -# Circular imports -from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py deleted file mode 100644 index 677d34a81e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/__init__.py +++ /dev/null @@ -1,352 +0,0 @@ -from abc import ABC, abstractmethod -from threading import Lock -from typing import TYPE_CHECKING - -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union - - -_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) - - -_installer_lock = Lock() - -# Set of all integration identifiers we have attempted to install -_processed_integrations: "Set[str]" = set() - -# Set of all integration identifiers we have actually installed -_installed_integrations: "Set[str]" = set() - - -def _generate_default_integrations_iterator( - integrations: "List[str]", - auto_enabling_integrations: "List[str]", -) -> "Callable[[bool], Iterator[Type[Integration]]]": - def iter_default_integrations( - with_auto_enabling_integrations: bool, - ) -> "Iterator[Type[Integration]]": - """Returns an iterator of the default integration classes:""" - from importlib import import_module - - if with_auto_enabling_integrations: - all_import_strings = integrations + auto_enabling_integrations - else: - all_import_strings = integrations - - for import_string in all_import_strings: - try: - module, cls = import_string.rsplit(".", 1) - yield getattr(import_module(module), cls) - except (DidNotEnable, SyntaxError) as e: - logger.debug( - "Did not import default integration %s: %s", import_string, e - ) - - if isinstance(iter_default_integrations.__doc__, str): - for import_string in integrations: - iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) - - return iter_default_integrations - - -_DEFAULT_INTEGRATIONS = [ - # stdlib/base runtime integrations - "sentry_sdk.integrations.argv.ArgvIntegration", - "sentry_sdk.integrations.atexit.AtexitIntegration", - "sentry_sdk.integrations.dedupe.DedupeIntegration", - "sentry_sdk.integrations.excepthook.ExcepthookIntegration", - "sentry_sdk.integrations.logging.LoggingIntegration", - "sentry_sdk.integrations.modules.ModulesIntegration", - "sentry_sdk.integrations.stdlib.StdlibIntegration", - "sentry_sdk.integrations.threading.ThreadingIntegration", -] - -_AUTO_ENABLING_INTEGRATIONS = [ - "sentry_sdk.integrations.aiohttp.AioHttpIntegration", - "sentry_sdk.integrations.anthropic.AnthropicIntegration", - "sentry_sdk.integrations.ariadne.AriadneIntegration", - "sentry_sdk.integrations.arq.ArqIntegration", - "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", - "sentry_sdk.integrations.boto3.Boto3Integration", - "sentry_sdk.integrations.bottle.BottleIntegration", - "sentry_sdk.integrations.celery.CeleryIntegration", - "sentry_sdk.integrations.chalice.ChaliceIntegration", - "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", - "sentry_sdk.integrations.cohere.CohereIntegration", - "sentry_sdk.integrations.django.DjangoIntegration", - "sentry_sdk.integrations.falcon.FalconIntegration", - "sentry_sdk.integrations.fastapi.FastApiIntegration", - "sentry_sdk.integrations.flask.FlaskIntegration", - "sentry_sdk.integrations.gql.GQLIntegration", - "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", - "sentry_sdk.integrations.graphene.GrapheneIntegration", - "sentry_sdk.integrations.httpx.HttpxIntegration", - "sentry_sdk.integrations.httpx2.Httpx2Integration", - "sentry_sdk.integrations.huey.HueyIntegration", - "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", - "sentry_sdk.integrations.langchain.LangchainIntegration", - "sentry_sdk.integrations.langgraph.LanggraphIntegration", - "sentry_sdk.integrations.litestar.LitestarIntegration", - "sentry_sdk.integrations.loguru.LoguruIntegration", - "sentry_sdk.integrations.mcp.MCPIntegration", - "sentry_sdk.integrations.openai.OpenAIIntegration", - "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", - "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", - "sentry_sdk.integrations.pymongo.PyMongoIntegration", - "sentry_sdk.integrations.pyramid.PyramidIntegration", - "sentry_sdk.integrations.quart.QuartIntegration", - "sentry_sdk.integrations.redis.RedisIntegration", - "sentry_sdk.integrations.rq.RqIntegration", - "sentry_sdk.integrations.sanic.SanicIntegration", - "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", - "sentry_sdk.integrations.starlette.StarletteIntegration", - "sentry_sdk.integrations.starlite.StarliteIntegration", - "sentry_sdk.integrations.strawberry.StrawberryIntegration", - "sentry_sdk.integrations.tornado.TornadoIntegration", -] - -iter_default_integrations = _generate_default_integrations_iterator( - integrations=_DEFAULT_INTEGRATIONS, - auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, -) - -del _generate_default_integrations_iterator - - -_MIN_VERSIONS = { - "aiohttp": (3, 4), - "aiomysql": (0, 3, 0), - "anthropic": (0, 16), - "ariadne": (0, 20), - "arq": (0, 23), - "asyncpg": (0, 23), - "beam": (2, 12), - "boto3": (1, 16), # botocore - "bottle": (0, 12), - "celery": (4, 4, 7), - "chalice": (1, 16, 0), - "clickhouse_driver": (0, 2, 0), - "cohere": (5, 4, 0), - "django": (1, 8), - "dramatiq": (1, 9), - "falcon": (1, 4), - "fastapi": (0, 79, 0), - "flask": (1, 1, 4), - "gql": (3, 4, 1), - "graphene": (3, 3), - "google_genai": (1, 29, 0), # google-genai - "grpc": (1, 32, 0), # grpcio - "httpx": (0, 16, 0), - "httpx2": (2, 0, 0), - "huggingface_hub": (0, 24, 7), - "langchain": (0, 1, 0), - "langgraph": (0, 6, 6), - "launchdarkly": (9, 8, 0), - "litellm": (1, 77, 5), - "loguru": (0, 7, 0), - "mcp": (1, 15, 0), - "openai": (1, 0, 0), - "openai_agents": (0, 0, 19), - "openfeature": (0, 7, 1), - "pydantic_ai": (1, 0, 0), - "pymongo": (3, 5, 0), - "pyreqwest": (0, 11, 6), - "quart": (0, 16, 0), - "ray": (2, 7, 0), - "requests": (2, 0, 0), - "rq": (0, 6), - "sanic": (0, 8), - "sqlalchemy": (1, 2), - "starlette": (0, 16), - "starlite": (1, 48), - "statsig": (0, 55, 3), - "strawberry": (0, 209, 5), - "tornado": (6, 0), - "typer": (0, 15), - "unleash": (6, 0, 1), -} - - -_INTEGRATION_DEACTIVATES = { - "langchain": {"openai", "anthropic", "google_genai"}, - "openai_agents": {"openai"}, - "pydantic_ai": {"openai", "anthropic"}, -} - - -def setup_integrations( - integrations: "Sequence[Integration]", - with_defaults: bool = True, - with_auto_enabling_integrations: bool = False, - disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, - options: "Optional[Dict[str, Any]]" = None, -) -> "Dict[str, Integration]": - """ - Given a list of integration instances, this installs them all. - - When `with_defaults` is set to `True` all default integrations are added - unless they were already provided before. - - `disabled_integrations` takes precedence over `with_defaults` and - `with_auto_enabling_integrations`. - - Some integrations are designed to automatically deactivate other integrations - in order to avoid conflicts and prevent duplicate telemetry from being collected. - For example, enabling the `langchain` integration will auto-deactivate both the - `openai` and `anthropic` integrations. - - Users can override this behavior by: - - Explicitly providing an integration in the `integrations=[]` list, or - - Disabling the higher-level integration via the `disabled_integrations` option. - """ - integrations = dict( - (integration.identifier, integration) for integration in integrations or () - ) - - logger.debug("Setting up integrations (with default = %s)", with_defaults) - - user_provided_integrations = set(integrations.keys()) - - # Integrations that will not be enabled - disabled_integrations = [ - integration if isinstance(integration, type) else type(integration) - for integration in disabled_integrations or [] - ] - - # Integrations that are not explicitly set up by the user. - used_as_default_integration = set() - - if with_defaults: - for integration_cls in iter_default_integrations( - with_auto_enabling_integrations - ): - if integration_cls.identifier not in integrations: - instance = integration_cls() - integrations[instance.identifier] = instance - used_as_default_integration.add(instance.identifier) - - disabled_integration_identifiers = { - integration.identifier for integration in disabled_integrations - } - - for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): - if ( - integration in integrations - and integration not in disabled_integration_identifiers - ): - for target in targets_to_deactivate: - if target not in user_provided_integrations: - for cls in iter_default_integrations(True): - if cls.identifier == target: - if cls not in disabled_integrations: - disabled_integrations.append(cls) - logger.debug( - "Auto-deactivating %s integration because %s integration is active", - target, - integration, - ) - - for identifier, integration in integrations.items(): - with _installer_lock: - if identifier not in _processed_integrations: - if type(integration) in disabled_integrations: - logger.debug("Ignoring integration %s", identifier) - else: - logger.debug( - "Setting up previously not enabled integration %s", identifier - ) - try: - type(integration).setup_once() - integration.setup_once_with_options(options) - except DidNotEnable as e: - if identifier not in used_as_default_integration: - raise - - logger.debug( - "Did not enable default integration %s: %s", identifier, e - ) - else: - _installed_integrations.add(identifier) - - _processed_integrations.add(identifier) - - integrations = { - identifier: integration - for identifier, integration in integrations.items() - if identifier in _installed_integrations - } - - for identifier in integrations: - logger.debug("Enabling integration %s", identifier) - - return integrations - - -def _check_minimum_version( - integration: "type[Integration]", - version: "Optional[tuple[int, ...]]", - package: "Optional[str]" = None, -) -> None: - package = package or integration.identifier - - if version is None: - raise DidNotEnable(f"Unparsable {package} version.") - - min_version = _MIN_VERSIONS.get(integration.identifier) - if min_version is None: - return - - if version < min_version: - raise DidNotEnable( - f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." - ) - - -class DidNotEnable(Exception): # noqa: N818 - """ - The integration could not be enabled due to a trivial user error like - `flask` not being installed for the `FlaskIntegration`. - - This exception is silently swallowed for default integrations, but reraised - for explicitly enabled integrations. - """ - - -class Integration(ABC): - """Baseclass for all integrations. - - To accept options for an integration, implement your own constructor that - saves those options on `self`. - """ - - install = None - """Legacy method, do not implement.""" - - identifier: "str" = None # type: ignore[assignment] - """String unique ID of integration type""" - - @staticmethod - @abstractmethod - def setup_once() -> None: - """ - Initialize the integration. - - This function is only called once, ever. Configuration is not available - at this point, so the only thing to do here is to hook into exception - handlers, and perhaps do monkeypatches. - - Inside those hooks `Integration.current` can be used to access the - instance again. - """ - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - """ - Called after setup_once in rare cases on the instance and with options since we don't have those available above. - """ - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py deleted file mode 100644 index 7ff4657013..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_asgi_common.py +++ /dev/null @@ -1,187 +0,0 @@ -import urllib -from enum import Enum -from typing import TYPE_CHECKING - -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.scope import should_send_default_pii - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from typing_extensions import Literal - - from sentry_sdk.utils import AnnotatedValue - - -class _RootPathInPath(Enum): - EXCLUDED = "excluded" - EITHER = "either" - - -def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": - """ - Extract headers from the ASGI scope, in the format that the Sentry protocol expects. - """ - headers: "Dict[str, str]" = {} - for raw_key, raw_value in asgi_scope["headers"]: - key = raw_key.decode("latin-1") - value = raw_value.decode("latin-1") - if key in headers: - headers[key] = headers[key] + ", " + value - else: - headers[key] = value - - return headers - - -def _get_path( - asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" -) -> "str": - if root_path_in_path is _RootPathInPath.EXCLUDED: - return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") - - # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 - path = asgi_scope["path"] - root_path = asgi_scope.get("root_path", "") - - if not root_path or path == root_path or path.startswith(root_path + "/"): - return path - - return root_path + path - - -def _get_url( - asgi_scope: "Dict[str, Any]", - default_scheme: "Literal['ws', 'http']", - host: "Optional[Union[AnnotatedValue, str]]", - path: str, -) -> str: - """ - Extract URL from the ASGI scope, without also including the querystring. - """ - scheme = asgi_scope.get("scheme", default_scheme) - - server = asgi_scope.get("server", None) - - if host: - return "%s://%s%s" % (scheme, host, path) - - if server is not None: - host, port = server - default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) - if port != default_port: - return "%s://%s:%s%s" % (scheme, host, port, path) - return "%s://%s%s" % (scheme, host, path) - return path - - -def _get_query(asgi_scope: "Any") -> "Any": - """ - Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. - """ - qs = asgi_scope.get("query_string") - if not qs: - return None - return urllib.parse.unquote(qs.decode("latin-1")) - - -def _get_ip(asgi_scope: "Any") -> str: - """ - Extract IP Address from the ASGI scope based on request headers with fallback to scope client. - """ - headers = _get_headers(asgi_scope) - try: - return headers["x-forwarded-for"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return headers["x-real-ip"] - except KeyError: - pass - - return asgi_scope.get("client")[0] - - -def _get_request_data( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "Dict[str, Any]": - """ - Returns data related to the HTTP request from the ASGI scope. - """ - request_data: "Dict[str, Any]" = {} - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - request_data["method"] = asgi_scope.get("method") - - headers = _get_headers(asgi_scope) - - request_data["headers"] = _filter_headers( - headers, - use_annotated_value=False, - ) - - request_data["query_string"] = _get_query(asgi_scope) - - request_data["url"] = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} - - return request_data - - -def _get_request_attributes( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "dict[str, Any]": - """ - Return attributes related to the HTTP request from the ASGI scope. - """ - attributes: "dict[str, Any]" = {} - - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - if asgi_scope.get("method"): - attributes["http.request.method"] = asgi_scope["method"].upper() - - headers = _get_headers(asgi_scope) - - filtered_headers = _filter_headers(headers, use_annotated_value=False) - for header, value in filtered_headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - if should_send_default_pii(): - query = _get_query(asgi_scope) - if query: - attributes["http.query"] = query - - path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) - attributes["url.path"] = path - - url_without_query_string = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=path, - ) - query_string = _get_query(asgi_scope) - attributes["url.full"] = ( - f"{url_without_query_string}?{query_string}" - if query_string is not None - else url_without_query_string - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - ip = _get_ip(asgi_scope) - attributes["client.address"] = ip - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py deleted file mode 100644 index ad0ab7c734..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/_wsgi_common.py +++ /dev/null @@ -1,282 +0,0 @@ -import json -from contextlib import contextmanager -from copy import deepcopy - -import sentry_sdk -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE -from sentry_sdk.data_collection import _apply_key_value_collection_filtering -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger - -try: - from django.http.request import RawPostDataException - - _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) -except ImportError: - RawPostDataException = None - _RAW_DATA_EXCEPTIONS = (ValueError,) - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union - - from sentry_sdk._types import Event, HttpStatusCodeRange - - -SENSITIVE_ENV_KEYS = ( - "REMOTE_ADDR", - "HTTP_X_FORWARDED_FOR", - "HTTP_SET_COOKIE", - "HTTP_COOKIE", - "HTTP_AUTHORIZATION", - "HTTP_PROXY_AUTHORIZATION", - "HTTP_X_API_KEY", - "HTTP_X_FORWARDED_FOR", - "HTTP_X_REAL_IP", -) - -SENSITIVE_HEADERS = tuple( - x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") -) - -DEFAULT_HTTP_METHODS_TO_CAPTURE = ( - "CONNECT", - "DELETE", - "GET", - # "HEAD", # do not capture HEAD requests by default - # "OPTIONS", # do not capture OPTIONS requests by default - "PATCH", - "POST", - "PUT", - "TRACE", -) - - -# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support -@contextmanager -def nullcontext() -> "Iterator[None]": - yield - - -def request_body_within_bounds( - client: "Optional[sentry_sdk.client.BaseClient]", content_length: int -) -> bool: - if client is None: - return False - - bodies = client.options["max_request_body_size"] - return not ( - bodies == "never" - or (bodies == "small" and content_length > 10**3) - or (bodies == "medium" and content_length > 10**4) - ) - - -class RequestExtractor: - """ - Base class for request extraction. - """ - - # It does not make sense to make this class an ABC because it is not used - # for typing, only so that child classes can inherit common methods from - # it. Only some child classes implement all methods that raise - # NotImplementedError in this class. - - def __init__(self, request: "Any") -> None: - self.request = request - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - - content_length = self.content_length() - request_info = event.get("request", {}) - - if should_send_default_pii(): - request_info["cookies"] = dict(self.cookies()) - - if not request_body_within_bounds(client, content_length): - data = AnnotatedValue.removed_because_over_size_limit() - else: - # First read the raw body data - # It is important to read this first because if it is Django - # it will cache the body and then we can read the cached version - # again in parsed_body() (or json() or wherever). - raw_data = None - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # If DjangoRestFramework is used it already read the body for us - # so reading it here will fail. We can ignore this. - pass - - parsed_body = self.parsed_body() - if parsed_body is not None: - data = parsed_body - elif raw_data: - data = AnnotatedValue.removed_because_raw_data() - else: - data = None - - if data is not None: - request_info["data"] = data - - event["request"] = deepcopy(request_info) - - def content_length(self) -> int: - try: - return int(self.env().get("CONTENT_LENGTH", 0)) - except ValueError: - return 0 - - def cookies(self) -> "MutableMapping[str, Any]": - raise NotImplementedError() - - def raw_data(self) -> "Optional[Union[str, bytes]]": - raise NotImplementedError() - - def form(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - form = self.form() - except Exception: - form = None - try: - files = self.files() - except Exception: - files = None - - if form or files: - data = {} - if form: - data = dict(form.items()) - if files: - for key in files.keys(): - data[key] = AnnotatedValue.removed_because_raw_data() - - return data - - return self.json() - - def is_json(self) -> bool: - return _is_json_content_type(self.env().get("CONTENT_TYPE")) - - def json(self) -> "Optional[Any]": - try: - if not self.is_json(): - return None - - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # The body might have already been read, in which case this will - # fail - raw_data = None - - if raw_data is None: - return None - - if isinstance(raw_data, str): - return json.loads(raw_data) - else: - return json.loads(raw_data.decode("utf-8")) - except ValueError: - pass - - return None - - def files(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def size_of_file(self, file: "Any") -> int: - raise NotImplementedError() - - def env(self) -> "Dict[str, Any]": - raise NotImplementedError() - - -def _is_json_content_type(ct: "Optional[str]") -> bool: - mt = (ct or "").split(";", 1)[0] - return ( - mt == "application/json" - or (mt.startswith("application/")) - and mt.endswith("+json") - ) - - -def _filter_headers( - headers: "Mapping[str, str]", - use_annotated_value: bool = True, -) -> "Mapping[str, Union[AnnotatedValue, str]]": - client_options = sentry_sdk.get_client().options - - if has_data_collection_enabled(client_options): - data_collection_configuration = client_options["data_collection"] - - filtered = _apply_key_value_collection_filtering( - items=headers, - behaviour=data_collection_configuration["http_headers"]["request"], - ) - - for key in filtered: - if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): - filtered[key] = SENSITIVE_DATA_SUBSTITUTE - - return filtered - else: - if should_send_default_pii(): - return headers - - substitute: "Union[AnnotatedValue, str]" = ( - SENSITIVE_DATA_SUBSTITUTE - if not use_annotated_value - else AnnotatedValue.removed_because_over_size_limit() - ) - - return { - k: ( - v - if k.upper().replace("-", "_") not in SENSITIVE_HEADERS - else substitute - ) - for k, v in headers.items() - } - - -def _in_http_status_code_range( - code: object, code_ranges: "list[HttpStatusCodeRange]" -) -> bool: - for target in code_ranges: - if isinstance(target, int): - if code == target: - return True - continue - - try: - if code in target: - return True - except TypeError: - logger.warning( - "failed_request_status_codes has to be a list of integers or containers" - ) - - return False - - -class HttpCodeRangeContainer: - """ - Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. - Used for backwards compatibility with the old `failed_request_status_codes` option. - """ - - def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: - self._code_ranges = code_ranges - - def __contains__(self, item: object) -> bool: - return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py deleted file mode 100644 index 59bae92a60..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiohttp.py +++ /dev/null @@ -1,509 +0,0 @@ -import sys -import weakref -from functools import wraps - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import ( - _filter_headers, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - NoOpStreamedSpan, - SegmentSource, - SpanStatus, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import ( - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - parse_url, - parse_version, - reraise, - transaction_from_function, -) - -try: - import asyncio - - from aiohttp import ClientSession, TraceConfig - from aiohttp import __version__ as AIOHTTP_VERSION - from aiohttp.web import Application, HTTPException, UrlDispatcher -except ImportError: - raise DidNotEnable("AIOHTTP not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Set - from types import SimpleNamespace - from typing import Any, ContextManager, Optional, Tuple, Union - - from aiohttp import TraceRequestEndParams, TraceRequestStartParams - from aiohttp.web_request import Request - from aiohttp.web_urldispatcher import UrlMappingMatchInfo - - from sentry_sdk._types import Attributes, Event, EventProcessor - from sentry_sdk.tracing import Span - from sentry_sdk.utils import ExcInfo - - -TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") - - -class AioHttpIntegration(Integration): - identifier = "aiohttp" - origin = f"auto.http.{identifier}" - - def __init__( - self, - transaction_style: str = "handler_name", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self._failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(AIOHTTP_VERSION) - _check_minimum_version(AioHttpIntegration, version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - # In the aiohttp integration, all of their HTTP responses are Exceptions. - # Because they have to be raised and handled by the framework, we need to - # register the exceptions as control flow exceptions so that we don't - # accidentally overwrite a status of "ok" with "error". - _register_control_flow_exception(HTTPException) - - ignore_logger("aiohttp.server") - - old_handle = Application._handle - - async def sentry_app_handle( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(AioHttpIntegration) - if integration is None: - return await old_handle(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - # Scope data will not leak between requests because aiohttp - # create a task to wrap each request. - scope.generate_propagation_context() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - headers = dict(request.headers) - - span_ctx: "ContextManager[Union[Span, StreamedSpan]]" - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context({"aiohttp_request": request}) - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, - use_annotated_value=False, - ).items(): - header_attributes[ - f"http.request.header.{header.lower()}" - ] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - url_attributes = {} - if should_send_default_pii(): - url_attributes["url.full"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - url_attributes["url.path"] = request.path - - if request.query_string: - url_attributes["url.query"] = request.query_string - - client_address_attributes = {} - if should_send_default_pii() and request.remote: - client_address_attributes["client.address"] = request.remote - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, request.remote - ) - - span_ctx = sentry_sdk.traces.start_span( - # If this name makes it to the UI, AIOHTTP's URL - # resolver did not find a route or died trying. - name="generic AIOHTTP request", - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": AioHttpIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE.value, - "http.request.method": request.method, - **url_attributes, - **client_address_attributes, - **header_attributes, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # If this transaction name makes it to the UI, AIOHTTP's - # URL resolver did not find a route or died trying. - name="generic AIOHTTP request", - source=TransactionSource.ROUTE, - origin=AioHttpIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"aiohttp_request": request}, - ) - - with span_ctx as span: - try: - response = await old_handle(self, request) - except HTTPException as e: - if isinstance(span, StreamedSpan) and not isinstance( - span, NoOpStreamedSpan - ): - span.set_attribute( - "http.response.status_code", e.status_code - ) - - if e.status_code >= 400: - span.status = SpanStatus.ERROR.value - else: - span.status = SpanStatus.OK.value - else: - # Since a NoOpStreamedSpan can end up here, we have to guard against it - # so this only gets set in the legacy transaction approach. - if not isinstance(span, NoOpStreamedSpan): - span.set_http_status(e.status_code) - - if ( - e.status_code - in integration._failed_request_status_codes - ): - _capture_exception() - raise - except (asyncio.CancelledError, ConnectionResetError): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR.value - else: - span.set_status(SPANSTATUS.CANCELLED) - raise - except Exception: - # This will probably map to a 500 but seems like we - # have no way to tell. Do not set span status. - reraise(*_capture_exception()) - - try: - # A valid response handler will return a valid response with a status. But, if the handler - # returns an invalid response (e.g. None), the line below will raise an AttributeError. - # Even though this is likely invalid, we need to handle this case to ensure we don't break - # the application. - response_status = response.status - except AttributeError: - pass - else: - if isinstance(span, StreamedSpan): - span.set_attribute( - "http.response.status_code", response_status - ) - span.status = ( - SpanStatus.ERROR.value - if response_status >= 400 - else SpanStatus.OK.value - ) - else: - span.set_http_status(response_status) - - return response - - Application._handle = sentry_app_handle - - old_urldispatcher_resolve = UrlDispatcher.resolve - - @wraps(old_urldispatcher_resolve) - async def sentry_urldispatcher_resolve( - self: "UrlDispatcher", request: "Request" - ) -> "UrlMappingMatchInfo": - rv = await old_urldispatcher_resolve(self, request) - - integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) - if integration is None: - return rv - - name = None - - try: - if integration.transaction_style == "handler_name": - name = transaction_from_function(rv.handler) - elif integration.transaction_style == "method_and_path_pattern": - route_info = rv.get_info() - pattern = route_info.get("path") or route_info.get("formatter") - name = "{} {}".format(request.method, pattern) - except Exception: - pass - - if name is not None: - current_span = sentry_sdk.get_current_span() - if isinstance(current_span, StreamedSpan) and not isinstance( - current_span, NoOpStreamedSpan - ): - current_span._segment.name = name - current_span._segment.set_attribute( - "sentry.span.source", - SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, - ) - else: - current_scope = sentry_sdk.get_current_scope() - current_scope.set_transaction_name( - name, - source=SOURCE_FOR_STYLE[integration.transaction_style], - ) - - return rv - - UrlDispatcher.resolve = sentry_urldispatcher_resolve - - old_client_session_init = ClientSession.__init__ - - @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) - def init(*args: "Any", **kwargs: "Any") -> None: - client_trace_configs = list(kwargs.get("trace_configs") or ()) - trace_config = create_trace_config() - client_trace_configs.append(trace_config) - - kwargs["trace_configs"] = client_trace_configs - return old_client_session_init(*args, **kwargs) - - ClientSession.__init__ = init - - -def create_trace_config() -> "TraceConfig": - async def on_request_start( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestStartParams", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(AioHttpIntegration) is None: - return - - method = params.method.upper() - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(params.url), sanitize=False) - - span_name = "%s %s" % ( - method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ) - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(client.options): - attributes: "Attributes" = { - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": AioHttpIntegration.origin, - "http.request.method": method, - } - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - attributes["url.path"] = params.url.path - - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) - else: - legacy_span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=span_name, - origin=AioHttpIntegration.origin, - ) - legacy_span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - legacy_span.set_data("url", parsed_url.url) - legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span = legacy_span - - if should_propagate_trace(client, str(params.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=params.url - ) - ) - if key == BAGGAGE_HEADER_NAME and params.headers.get( - BAGGAGE_HEADER_NAME - ): - # do not overwrite any existing baggage, just append to it - params.headers[key] += "," + value - else: - params.headers[key] = value - - trace_config_ctx.span = span - - async def on_request_end( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestEndParams", - ) -> None: - if trace_config_ctx.span is None: - return - - span = trace_config_ctx.span - status = int(params.response.status) - - if isinstance(span, StreamedSpan): - span.set_attribute("http.response.status_code", status) - span.status = ( - SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value - ) - - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.set_http_status(status) - span.set_data("reason", params.response.reason) - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - trace_config = TraceConfig() - - trace_config.on_request_start.append(on_request_start) - trace_config.on_request_end.append(on_request_end) - - return trace_config - - -def _make_request_processor( - weak_request: "weakref.ReferenceType[Request]", -) -> "EventProcessor": - def aiohttp_processor( - event: "Event", - hint: "dict[str, Tuple[type, BaseException, Any]]", - ) -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - request_info = event.setdefault("request", {}) - - request_info["url"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote} - request_info["headers"] = _filter_headers(dict(request.headers)) - - # Just attach raw data here if it is within bounds, if available. - # Unfortunately there's no way to get structured data from aiohttp - # without awaiting on some coroutine. - request_info["data"] = get_aiohttp_request_data(request) - - return event - - return aiohttp_processor - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "aiohttp", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - return exc_info - - -BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" - - -def get_aiohttp_request_data( - request: "Request", -) -> "Union[Optional[str], AnnotatedValue]": - bytes_body = request._read_bytes - - if bytes_body is not None: - # we have body to show - if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): - return AnnotatedValue.substituted_because_over_size_limit() - - encoding = request.charset or "utf-8" - return bytes_body.decode(encoding, "replace") - - if request.can_read_body: - # body exists but we can't show it - return BODY_NOT_READ_MESSAGE - - # request has no body - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py deleted file mode 100644 index 49459268e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aiomysql.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import annotations - -from typing import Any, Awaitable, Callable, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import aiomysql # type: ignore[import-not-found] - from aiomysql.connection import Connection # type: ignore[import-not-found] - from aiomysql.cursors import Cursor # type: ignore[import-not-found] -except ImportError: - raise DidNotEnable("aiomysql not installed.") - - -class AioMySQLIntegration(Integration): - identifier = "aiomysql" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AioMySQLIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - aiomysql_version = parse_version(aiomysql.__version__) - _check_minimum_version(AioMySQLIntegration, aiomysql_version) - - Cursor.execute = _wrap_execute(Cursor.execute) - Cursor.executemany = _wrap_executemany(Cursor.executemany) - - # Patch Connection._connect — this catches ALL connections: - # - aiomysql.connect() - # - aiomysql.create_pool() (pool.py does `from .connection import connect` - # which ultimately calls Connection._connect) - # - Reconnects - Connection._connect = _wrap_connect(Connection._connect) - - -T = TypeVar("T") - - -def _normalize_query(query: str | bytes | bytearray) -> str: - if isinstance(query, (bytes, bytearray)): - query = query.decode("utf-8", errors="replace") - return " ".join(query.split()) - - -def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.execute to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - - # Skip if flagged by executemany (avoids double-recording). - # Do NOT reset the flag here — it must stay True for the entire - # duration of executemany, which may call execute multiple times - # in a loop (non-INSERT fallback). Only _wrap_executemany's - # finally block should clear it. - if getattr(cursor, "_sentry_skip_next_execute", False): - return await f(*args, **kwargs) - - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = params if integration and integration._record_params else None - param_style = "pyformat" if params_list else None - - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=False, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.executemany to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = ( - seq_of_params if integration and integration._record_params else None - ) - param_style = "pyformat" if params_list else None - - # Prevent double-recording: _do_execute_many calls self.execute internally - cursor._sentry_skip_next_execute = True - try: - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=True, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - finally: - cursor._sentry_skip_next_execute = False - - return _inner - - -def _get_connection(cursor: Any) -> Any: - """Get the underlying connection from a cursor.""" - return getattr(cursor, "connection", None) - - -def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Connection._connect to capture connection spans.""" - - async def _inner(self: "Connection") -> T: - client = sentry_sdk.get_client() - if client.get_integration(AioMySQLIntegration) is None: - return await f(self) - - if has_span_streaming_enabled(client.options): - breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) - - span_attributes: dict[str, Any] = { - "sentry.op": OP.DB, - "sentry.origin": AioMySQLIntegration.origin, - } | breadcrumb_data - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=breadcrumb_data - ) - res = await f(self) - else: - connect_data = _get_connect_data(self) - - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AioMySQLIntegration.origin, - ) as span: - _set_db_data(span, self) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", - category="query", - data=connect_data, - ) - res = await f(self) - - return res - - return _inner - - -def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: - if use_streaming_keys: - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - data: dict[str, Any] = { - db_system: "mysql", - SPANDATA.DB_DRIVER_NAME: "aiomysql", - } - - host = getattr(conn, "host", None) - if host is not None: - data[SPANDATA.SERVER_ADDRESS] = host - - port = getattr(conn, "port", None) - if port is not None: - data[SPANDATA.SERVER_PORT] = port - - database = getattr(conn, "db", None) - if database is not None: - data[db_name] = database - - user = getattr(conn, "user", None) - if user is not None: - data[SPANDATA.DB_USER] = user - - return data - - -def _set_db_data(span: Any, conn: Any) -> None: - """Set database-related span data from connection object.""" - if isinstance(span, StreamedSpan): - set_value = span.set_attribute - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - set_value = span.set_data - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - set_value(db_system, "mysql") - set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") - - host = getattr(conn, "host", None) - if host is not None: - set_value(SPANDATA.SERVER_ADDRESS, host) - - port = getattr(conn, "port", None) - if port is not None: - set_value(SPANDATA.SERVER_PORT, port) - - database = getattr(conn, "db", None) - if database is not None: - set_value(db_name, database) - - user = getattr(conn, "user", None) - if user is not None: - set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py deleted file mode 100644 index dfa4aef34c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/anthropic.py +++ /dev/null @@ -1,1154 +0,0 @@ -import json -import sys -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_anthropic_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - package_version, - reraise, - safe_serialize, -) - -try: - try: - from anthropic import NotGiven - except ImportError: - NotGiven = None - - try: - from anthropic import Omit - except ImportError: - Omit = None - - from anthropic import AsyncStream, Stream - from anthropic.lib.streaming import ( - AsyncMessageStream, - AsyncMessageStreamManager, - MessageStream, - MessageStreamManager, - ) - from anthropic.resources import AsyncMessages, Messages - from anthropic.types import ( - ContentBlockDeltaEvent, - ContentBlockStartEvent, - ContentBlockStopEvent, - MessageDeltaEvent, - MessageStartEvent, - MessageStopEvent, - ) - - if TYPE_CHECKING: - from anthropic.types import MessageStreamEvent, TextBlockParam -except ImportError: - raise DidNotEnable("Anthropic not installed") - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Iterator, - Optional, - Union, - ) - - from anthropic.types import ( - MessageParam, - ModelParam, - RawMessageStreamEvent, - TextBlockParam, - ToolUnionParam, - ) - - from sentry_sdk._types import TextPart - - -class _RecordedUsage: - output_tokens: int = 0 - input_tokens: int = 0 - cache_write_input_tokens: "Optional[int]" = 0 - cache_read_input_tokens: "Optional[int]" = 0 - - -class _StreamSpanContext: - """ - Sets accumulated data on the stream's span and finishes the span on exit. - Is a no-op if the stream has no span set, i.e., when the span has already been finished. - """ - - def __init__( - self, - stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", - # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). - guaranteed_streaming_state: bool = False, - ) -> None: - self._stream = stream - self._guaranteed_streaming_state = guaranteed_streaming_state - - def __enter__(self) -> "_StreamSpanContext": - return self - - def __exit__( - self, - exc_type: "Optional[type[BaseException]]", - exc_val: "Optional[BaseException]", - exc_tb: "Optional[Any]", - ) -> None: - with capture_internal_exceptions(): - if not hasattr(self._stream, "_span"): - return - - if not self._guaranteed_streaming_state and not hasattr( - self._stream, "_model" - ): - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - return - - _set_streaming_output_data( - span=self._stream._span, - integration=self._stream._integration, - model=self._stream._model, - usage=self._stream._usage, - content_blocks=self._stream._content_blocks, - response_id=self._stream._response_id, - finish_reason=self._stream._finish_reason, - ) - - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - - -class AnthropicIntegration(Integration): - identifier = "anthropic" - origin = f"auto.ai.{identifier}" - - def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - version = package_version("anthropic") - _check_minimum_version(AnthropicIntegration, version) - - """ - client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. - Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. - The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, - information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. - - The span can be finished in two places: - - When the user exits the context manager or directly calls close(), the patched close() finishes the span. - - When iteration ends, the finally block in the _iterator wrapper finishes the span. - - Both paths may run. For example, the context manager exit can follow iterator exhaustion. - """ - Messages.create = _wrap_message_create(Messages.create) - Stream.close = _wrap_close(Stream.close) - - AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) - AsyncStream.close = _wrap_async_close(AsyncStream.close) - - """ - client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. - """ - Messages.stream = _wrap_message_stream(Messages.stream) - MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( - MessageStreamManager.__enter__ - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. - if not issubclass(MessageStream, Stream): - MessageStream.close = _wrap_close(MessageStream.close) - - AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) - AsyncMessageStreamManager.__aenter__ = ( - _wrap_async_message_stream_manager_aenter( - AsyncMessageStreamManager.__aenter__ - ) - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. - if not issubclass(AsyncMessageStream, AsyncStream): - AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "anthropic", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": - """ - Get token usage from the Anthropic response. - Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) - """ - input_tokens = 0 - output_tokens = 0 - cache_read_input_tokens = 0 - cache_write_input_tokens = 0 - if hasattr(result, "usage"): - usage = result.usage - if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): - input_tokens = usage.input_tokens - if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): - output_tokens = usage.output_tokens - if hasattr(usage, "cache_read_input_tokens") and isinstance( - usage.cache_read_input_tokens, int - ): - cache_read_input_tokens = usage.cache_read_input_tokens - if hasattr(usage, "cache_creation_input_tokens") and isinstance( - usage.cache_creation_input_tokens, int - ): - cache_write_input_tokens = usage.cache_creation_input_tokens - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens so downstream cost calculations - # (input_tokens - cached) don't produce negative values. - input_tokens += cache_read_input_tokens + cache_write_input_tokens - - return ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) - - -def _collect_ai_data( - event: "MessageStreamEvent", - model: "str | None", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": - """ - Collect model information, token usage, and collect content blocks from the AI streaming response. - """ - with capture_internal_exceptions(): - if hasattr(event, "type"): - if event.type == "content_block_start": - pass - elif event.type == "content_block_delta": - if hasattr(event.delta, "text"): - content_blocks.append(event.delta.text) - elif hasattr(event.delta, "partial_json"): - content_blocks.append(event.delta.partial_json) - elif event.type == "content_block_stop": - pass - - # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. - # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 - if event.type == "message_start": - model = event.message.model or model - response_id = event.message.id - - incoming_usage = event.message.usage - usage.output_tokens = incoming_usage.output_tokens - usage.input_tokens = incoming_usage.input_tokens - - usage.cache_write_input_tokens = getattr( - incoming_usage, "cache_creation_input_tokens", None - ) - usage.cache_read_input_tokens = getattr( - incoming_usage, "cache_read_input_tokens", None - ) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - # Counterintuitive, but message_delta contains cumulative token counts :) - if event.type == "message_delta": - usage.output_tokens = event.usage.output_tokens - - # Update other usage fields if they exist in the event - input_tokens = getattr(event.usage, "input_tokens", None) - if input_tokens is not None: - usage.input_tokens = input_tokens - - cache_creation_input_tokens = getattr( - event.usage, "cache_creation_input_tokens", None - ) - if cache_creation_input_tokens is not None: - usage.cache_write_input_tokens = cache_creation_input_tokens - - cache_read_input_tokens = getattr( - event.usage, "cache_read_input_tokens", None - ) - if cache_read_input_tokens is not None: - usage.cache_read_input_tokens = cache_read_input_tokens - # TODO: Record event.usage.server_tool_use - - if event.delta.stop_reason is not None: - finish_reason = event.delta.stop_reason - - return (model, usage, content_blocks, response_id, finish_reason) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - -def _transform_anthropic_content_block( - content_block: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Transform an Anthropic content block using the Anthropic-specific transformer, - with special handling for Anthropic's text-type documents. - """ - # Handle Anthropic's text-type documents specially (not covered by shared function) - if content_block.get("type") == "document": - source = content_block.get("source") - if isinstance(source, dict) and source.get("type") == "text": - return { - "type": "text", - "text": source.get("data", ""), - } - - # Use Anthropic-specific transformation - result = transform_anthropic_content_part(content_block) - return result if result is not None else content_block - - -def _transform_system_instructions( - system_instructions: "Union[str, Iterable[TextBlockParam]]", -) -> "list[TextPart]": - if isinstance(system_instructions, str): - return [ - { - "type": "text", - "content": system_instructions, - } - ] - - return [ - { - "type": "text", - "content": instruction["text"], - } - for instruction in system_instructions - if isinstance(instruction, dict) and "text" in instruction - ] - - -def _set_common_input_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - max_tokens: "int", - messages: "Iterable[MessageParam]", - model: "ModelParam", - system: "Optional[Union[str, Iterable[TextBlockParam]]]", - temperature: "Optional[float]", - top_k: "Optional[int]", - top_p: "Optional[float]", - tools: "Optional[Iterable[ToolUnionParam]]", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if ( - messages is not None - and len(messages) > 0 # type: ignore - and should_send_default_pii() - and integration.include_prompts - ): - if isinstance(system, str) or isinstance(system, Iterable): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system)), - ) - - normalized_messages = [] - for message in messages: - if ( - message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER - and "content" in message - and isinstance(message["content"], (list, tuple)) - ): - transformed_content = [] - for item in message["content"]: - # Skip tool_result items - they can contain images/documents - # with nested structures that are difficult to redact properly - if isinstance(item, dict) and item.get("type") == "tool_result": - continue - - # Transform content blocks (images, documents, etc.) - transformed_content.append( - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - ) - - # If there are non-tool-result items, add them as a message - if transformed_content: - normalized_messages.append( - { - "role": message.get("role"), - "content": transformed_content, - } - ) - else: - # Transform content for non-list messages or assistant messages - transformed_message = message.copy() - if "content" in transformed_message: - content = transformed_message["content"] - if isinstance(content, (list, tuple)): - transformed_message["content"] = [ - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - for item in content - ] - normalized_messages.append(transformed_message) - - role_normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(role_normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else role_normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - if model is not None and _is_given(model): - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - if top_k is not None and _is_given(top_k): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore - set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) - - -def _set_create_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "AnthropicIntegration", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=kwargs.get("max_tokens"), # type: ignore - messages=kwargs.get("messages"), # type: ignore - model=kwargs.get("model"), - system=kwargs.get("system"), - temperature=kwargs.get("temperature"), - top_k=kwargs.get("top_k"), - top_p=kwargs.get("top_p"), - tools=kwargs.get("tools"), - ) - - -def _wrap_synchronous_message_iterator( - stream: "Union[Stream, MessageStream]", - iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -async def _wrap_asynchronous_message_iterator( - stream: "Union[AsyncStream, AsyncMessageStream]", - iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - async for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -def _set_output_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - model: "str | None", - input_tokens: "int | None", - output_tokens: "int | None", - cache_read_input_tokens: "int | None", - cache_write_input_tokens: "int | None", - content_blocks: "list[Any]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> None: - """ - Set output data for the span based on the AI response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - if finish_reason is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in content_blocks: - if output["type"] == "text": - output_messages["response"].append(output["text"]) - elif output["type"] == "tool_use": - output_messages["tool"].append(output) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - record_token_usage( - span, - input_tokens=input_tokens, - output_tokens=output_tokens, - input_tokens_cached=cache_read_input_tokens, - input_tokens_cache_write=cache_write_input_tokens, - ) - - -def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, Stream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_synchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -async def _sentry_patched_create_async( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return await f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, AsyncStream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_asynchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -def _wrap_message_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return _sentry_patched_create_sync(f, *args, **kwargs) - - return _sentry_wrapped_create_sync - - -def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: - """ - Initialize fields for accumulating output on the Stream instance. - """ - if not hasattr(stream, "_model"): - stream._model = None - stream._usage = _RecordedUsage() - stream._content_blocks = [] - stream._response_id = None - stream._finish_reason = None - - -def _accumulate_event_data( - stream: "Union[Stream, MessageStream]", - event: "Union[RawMessageStreamEvent, MessageStreamEvent]", -) -> None: - """ - Update accumulated output from a single stream event. - """ - (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( - event, - stream._model, - stream._usage, - stream._content_blocks, - stream._response_id, - stream._finish_reason, - ) - - stream._model = model - stream._usage = usage - stream._content_blocks = content_blocks - stream._response_id = response_id - stream._finish_reason = finish_reason - - -def _set_streaming_output_data( - span: "Span", - integration: "AnthropicIntegration", - model: "Optional[str]", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "Optional[str]", - finish_reason: "Optional[str]", -) -> None: - """ - Set output attributes on the AI Client Span. - """ - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - response_id=response_id, - finish_reason=finish_reason, - ) - - -def _wrap_close( - f: "Callable[..., None]", -) -> "Callable[..., None]": - """ - Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. - """ - - def close(self: "Union[Stream, MessageStream]") -> None: - with _StreamSpanContext(self): - return f(self) - - return close - - -def _wrap_message_create_async(f: "Any") -> "Any": - @wraps(f) - async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return await _sentry_patched_create_async(f, *args, **kwargs) - - return _sentry_wrapped_create_async - - -def _wrap_async_close( - f: "Callable[..., Awaitable[None]]", -) -> "Callable[..., Awaitable[None]]": - """ - Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. - """ - - async def close(self: "AsyncStream") -> None: - with _StreamSpanContext(self): - return await f(self) - - return close - - -def _wrap_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_message_stream_manager_enter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": - if not hasattr(self, "_max_tokens"): - return f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return f(self) - - if self._messages is None: - return f(self) - - try: - iter(self._messages) - except TypeError: - return f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_synchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_enter - - -def _wrap_async_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream( - *args: "Any", **kwargs: "Any" - ) -> "AsyncMessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - async def _sentry_patched_aenter( - self: "AsyncMessageStreamManager", - ) -> "AsyncMessageStream": - if not hasattr(self, "_max_tokens"): - return await f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return await f(self) - - if self._messages is None: - return await f(self) - - try: - iter(self._messages) - except TypeError: - return await f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = await f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_asynchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_aenter - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different anthropic versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py deleted file mode 100644 index 0215ffa093..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/argv.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class ArgvIntegration(Integration): - identifier = "argv" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: - extra = event.setdefault("extra", {}) - # If some event processor decided to set extra to e.g. an - # `int`, don't crash. Not here. - if isinstance(extra, dict): - extra["sys.argv"] = sys.argv - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py deleted file mode 100644 index 1ce228d3a5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ariadne.py +++ /dev/null @@ -1,167 +0,0 @@ -from importlib import import_module - -import sentry_sdk -from sentry_sdk import capture_event, get_client -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - # importing like this is necessary due to name shadowing in ariadne - # (ariadne.graphql is also a function) - ariadne_graphql = import_module("ariadne.graphql") -except ImportError: - raise DidNotEnable("ariadne is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional - - from ariadne.types import ( # type: ignore - GraphQLError, - GraphQLResult, - GraphQLSchema, - QueryParser, - ) - from graphql.language.ast import DocumentNode - - from sentry_sdk._types import Event, EventProcessor - - -class AriadneIntegration(Integration): - identifier = "ariadne" - - @staticmethod - def setup_once() -> None: - version = package_version("ariadne") - _check_minimum_version(AriadneIntegration, version) - - ignore_logger("ariadne") - - _patch_graphql() - - -def _patch_graphql() -> None: - old_parse_query = ariadne_graphql.parse_query - old_handle_errors = ariadne_graphql.handle_graphql_errors - old_handle_query_result = ariadne_graphql.handle_query_result - - @ensure_integration_enabled(AriadneIntegration, old_parse_query) - def _sentry_patched_parse_query( - context_value: "Optional[Any]", - query_parser: "Optional[QueryParser]", - data: "Any", - ) -> "DocumentNode": - event_processor = _make_request_event_processor(data) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - result = old_parse_query(context_value, query_parser, data) - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_errors) - def _sentry_patched_handle_graphql_errors( - errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - result = old_handle_errors(errors, *args, **kwargs) - - event_processor = _make_response_event_processor(result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) - def _sentry_patched_handle_query_result( - result: "Any", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - query_result = old_handle_query_result(result, *args, **kwargs) - - event_processor = _make_response_event_processor(query_result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return query_result - - ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore - ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore - ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore - - -def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": - """Add request data and api_target to events.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - if not isinstance(data, dict): - return event - - with capture_internal_exceptions(): - try: - content_length = int( - (data.get("headers") or {}).get("Content-Length", 0) - ) - except (TypeError, ValueError): - return event - - if should_send_default_pii() and request_body_within_bounds( - get_client(), content_length - ): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - request_info["data"] = data - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - return inner - - -def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": - """Add response data to the event's response context.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii() and response.get("errors"): - contexts = event.setdefault("contexts", {}) - contexts["response"] = { - "data": response, - } - - return event - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py deleted file mode 100644 index da03bafb8b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/arq.py +++ /dev/null @@ -1,277 +0,0 @@ -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Transaction, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -try: - import arq.worker - from arq.connections import ArqRedis - from arq.version import VERSION as ARQ_VERSION - from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker -except ImportError: - raise DidNotEnable("Arq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from arq.cron import CronJob - from arq.jobs import Job - from arq.typing import WorkerCoroutine - from arq.worker import Function - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) - - -class ArqIntegration(Integration): - identifier = "arq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - try: - if isinstance(ARQ_VERSION, str): - version = parse_version(ARQ_VERSION) - else: - version = ARQ_VERSION.version[:2] - - except (TypeError, ValueError): - version = None - - _check_minimum_version(ArqIntegration, version) - - patch_enqueue_job() - patch_run_job() - patch_create_worker() - - _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore - - ignore_logger("arq.worker") - - -def patch_enqueue_job() -> None: - old_enqueue_job = ArqRedis.enqueue_job - original_kwdefaults = old_enqueue_job.__kwdefaults__ - - async def _sentry_enqueue_job( - self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" - ) -> "Optional[Job]": - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_enqueue_job(self, function, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=function, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_ARQ, - "sentry.origin": ArqIntegration.origin, - }, - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults - ArqRedis.enqueue_job = _sentry_enqueue_job - - -def patch_run_job() -> None: - old_run_job = Worker.run_job - - async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_run_job(self, job_id, score) - - with sentry_sdk.isolation_scope() as scope: - scope._name = "arq" - scope.clear_breadcrumbs() - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name="unknown arq task", - attributes={ - "sentry.op": OP.QUEUE_TASK_ARQ, - "sentry.origin": ArqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job_id, - }, - parent_span=None, - ): - return await old_run_job(self, job_id, score) - - transaction = Transaction( - name="unknown arq task", - status="ok", - op=OP.QUEUE_TASK_ARQ, - source=TransactionSource.TASK, - origin=ArqIntegration.origin, - ) - - with sentry_sdk.start_transaction(transaction): - return await old_run_job(self, job_id, score) - - Worker.run_job = _sentry_run_job - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - - if scope.transaction is not None: - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - scope.transaction.set_status(SPANSTATUS.ABORTED) - return - - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": ArqIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - if scope.transaction is not None: - scope.transaction.name = ctx["job_name"] - event["transaction"] = ctx["job_name"] - - tags = event.setdefault("tags", {}) - tags["arq_task_id"] = ctx["job_id"] - tags["arq_task_retry"] = ctx["job_try"] > 1 - extra = event.setdefault("extra", {}) - extra["arq-job"] = { - "task": ctx["job_name"], - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": ctx["job_try"], - } - - return event - - return event_processor - - -def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": - async def _sentry_coroutine( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(ArqIntegration) - if integration is None: - return await coroutine(ctx, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - scope = sentry_sdk.get_current_scope() - span = scope.streamed_span - if span is not None: - span.name = name - - scope.set_transaction_name(name) - - sentry_sdk.get_isolation_scope().add_event_processor( - _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) - ) - - try: - result = await coroutine(ctx, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_coroutine - - -def patch_create_worker() -> None: - old_create_worker = arq.worker.create_worker - - @ensure_integration_enabled(ArqIntegration, old_create_worker) - def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": - settings_cls = args[0] if args else kwargs.get("settings_cls") - - if isinstance(settings_cls, dict): - if "functions" in settings_cls: - settings_cls["functions"] = [ - _get_arq_function(func) - for func in settings_cls.get("functions", []) - ] - if "cron_jobs" in settings_cls: - settings_cls["cron_jobs"] = [ - _get_arq_cron_job(cron_job) - for cron_job in settings_cls.get("cron_jobs", []) - ] - - if hasattr(settings_cls, "functions"): - settings_cls.functions = [ # type: ignore[union-attr] - _get_arq_function(func) - for func in settings_cls.functions # type: ignore[union-attr] - ] - if hasattr(settings_cls, "cron_jobs"): - settings_cls.cron_jobs = [ # type: ignore[union-attr] - _get_arq_cron_job(cron_job) - for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] - ] - - if "functions" in kwargs: - kwargs["functions"] = [ - _get_arq_function(func) for func in kwargs.get("functions", []) - ] - if "cron_jobs" in kwargs: - kwargs["cron_jobs"] = [ - _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) - ] - - return old_create_worker(*args, **kwargs) - - arq.worker.create_worker = _sentry_create_worker - - -def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": - arq_func = arq.worker.func(func) - arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) - - return arq_func - - -def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": - cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) - - return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py deleted file mode 100644 index 8b1ff5e2a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asgi.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -An ASGI middleware. - -Based on Tom Christie's `sentry-asgi `. -""" - -import inspect -import sys -from copy import deepcopy -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._asgi_common import ( - _get_headers, - _get_ip, - _get_path, - _get_request_attributes, - _get_request_data, - _get_url, - _RootPathInPath, -) -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - SegmentSource, - StreamedSpan, -) -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - Transaction, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, - _get_installed_modules, - capture_internal_exceptions, - event_from_exception, - logger, - qualname_from_function, - reraise, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, ContextManager, Dict, Optional, Tuple, Union - - from sentry_sdk._types import Attributes, Event, Hint - from sentry_sdk.tracing import Span - - -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") - -_DEFAULT_TRANSACTION_NAME = "generic ASGI request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 -if sys.version_info >= (3, 14): - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": mechanism_type, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _looks_like_asgi3(app: "Any") -> bool: - """ - Try to figure out if an application object supports ASGI3. - - This is how uvicorn figures out the application version as well. - """ - if inspect.isclass(app): - return hasattr(app, "__await__") - elif inspect.isfunction(app): - return iscoroutinefunction(app) - else: - call = getattr(app, "__call__", None) # noqa - return iscoroutinefunction(call) - - -class SentryAsgiMiddleware: - __slots__ = ( - "app", - "__call__", - "transaction_style", - "mechanism_type", - "span_origin", - "http_methods_to_capture", - "root_path_in_path", - ) - - def __init__( - self, - app: "Any", - unsafe_context_data: bool = False, - transaction_style: str = "endpoint", - mechanism_type: str = "asgi", - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - asgi_version: "Optional[int]" = None, - root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, - ) -> None: - """ - Instrument an ASGI application with Sentry. Provides HTTP/websocket - data to sent events and basic handling for exceptions bubbling up - through the middleware. - - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. - """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - - asgi_middleware_while_using_starlette_or_fastapi = ( - mechanism_type == "asgi" and "starlette" in _get_installed_modules() - ) - if asgi_middleware_while_using_starlette_or_fastapi: - logger.warning( - "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " - "Please remove 'SentryAsgiMiddleware' from your project. " - "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." - ) - - self.transaction_style = transaction_style - self.mechanism_type = mechanism_type - self.span_origin = span_origin - self.app = app - self.http_methods_to_capture = http_methods_to_capture - self.root_path_in_path = root_path_in_path - - if asgi_version is None: - if _looks_like_asgi3(app): - asgi_version = 3 - else: - asgi_version = 2 - - if asgi_version == 3: - self.__call__ = self._run_asgi3 - elif asgi_version == 2: - self.__call__ = self._run_asgi2 # type: ignore - - def _capture_lifespan_exception(self, exc: Exception) -> None: - """Capture exceptions raise in application lifespan handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _capture_request_exception(self, exc: Exception) -> None: - """Capture exceptions raised in incoming request handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _run_asgi2(self, scope: "Any") -> "Any": - async def inner(receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=2) - - return inner - - async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=3) - - async def _run_app( - self, scope: "Any", receive: "Any", send: "Any", asgi_version: int - ) -> "Any": - is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) - is_lifespan = scope["type"] == "lifespan" - if is_recursive_asgi_middleware or is_lifespan: - try: - if asgi_version == 2: - return await self.app(scope)(receive, send) - else: - return await self.app(scope, receive, send) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_lifespan_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_lifespan_exception(exc) - reraise(*exc_info) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _asgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as sentry_scope: - with track_session(sentry_scope, session_mode="request"): - sentry_scope.clear_breadcrumbs() - sentry_scope._name = "asgi" - processor = partial(self.event_processor, asgi_scope=scope) - sentry_scope.add_event_processor(processor) - - ty = scope["type"] - ( - transaction_name, - transaction_source, - ) = self._get_transaction_name_and_source( - self.transaction_style, - scope, - ) - - method = scope.get("method", "").upper() - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - if span_streaming: - segment: "Optional[StreamedSpan]" = None - attributes: "Attributes" = { - "sentry.span.source": getattr( - transaction_source, "value", transaction_source - ), - "sentry.origin": self.span_origin, - "network.protocol.name": ty, - } - - if scope.get("client") and should_send_default_pii(): - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) - - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - sentry_sdk.traces.continue_trace(_get_headers(scope)) - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = f"{ty}.server" - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - else: - sentry_sdk.traces.new_trace() - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = OP.HTTP_SERVER - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - - span_ctx = segment or nullcontext() - - else: - transaction = None - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - transaction = continue_trace( - _get_headers(scope), - op="{}.server".format(ty), - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - - if transaction: - transaction.set_tag("asgi.type", ty) - - span_ctx = ( - sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"asgi_scope": scope}, - ) - if transaction is not None - else nullcontext() - ) - - with span_ctx as span: - if isinstance(span, StreamedSpan): - for attribute, value in _get_request_attributes( - scope, - root_path_in_path=self.root_path_in_path, - ).items(): - span.set_attribute(attribute, value) - - try: - - async def _sentry_wrapped_send( - event: "Dict[str, Any]", - ) -> "Any": - if span is not None: - is_http_response = ( - event.get("type") == "http.response.start" - and "status" in event - ) - if is_http_response: - if isinstance(span, StreamedSpan): - span.status = ( - "error" - if event["status"] >= 400 - else "ok" - ) - span.set_attribute( - "http.response.status_code", - event["status"], - ) - else: - span.set_http_status(event["status"]) - - return await send(event) - - if asgi_version == 2: - return await self.app(scope)( - receive, _sentry_wrapped_send - ) - else: - return await self.app( - scope, receive, _sentry_wrapped_send - ) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_request_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_request_exception(exc) - reraise(*exc_info) - - finally: - if isinstance(span, StreamedSpan): - already_set = ( - span is not None - and span.name != _DEFAULT_TRANSACTION_NAME - and span.get_attributes().get("sentry.span.source") - in [ - SegmentSource.COMPONENT.value, - SegmentSource.ROUTE.value, - SegmentSource.CUSTOM.value, - ] - ) - with capture_internal_exceptions(): - if not already_set: - name, source = ( - self._get_segment_name_and_source( - self.transaction_style, scope - ) - ) - span.name = name - span.set_attribute("sentry.span.source", source) - finally: - _asgi_middleware_applied.set(False) - - def event_processor( - self, event: "Event", hint: "Hint", asgi_scope: "Any" - ) -> "Optional[Event]": - request_data = event.get("request", {}) - request_data.update( - _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) - ) - event["request"] = deepcopy(request_data) - - # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) - transaction = event.get("transaction") - transaction_source = (event.get("transaction_info") or {}).get("source") - already_set = ( - transaction is not None - and transaction != _DEFAULT_TRANSACTION_NAME - and transaction_source - in [ - TransactionSource.COMPONENT, - TransactionSource.ROUTE, - TransactionSource.CUSTOM, - ] - ) - if not already_set: - name, source = self._get_transaction_name_and_source( - self.transaction_style, asgi_scope - ) - event["transaction"] = name - event["transaction_info"] = {"source": source} - - return event - - # Helper functions. - # - # Note: Those functions are not public API. If you want to mutate request - # data to your liking it's recommended to use the `before_send` callback - # for that. - - def _get_transaction_name_and_source( - self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SOURCE_FOR_STYLE[transaction_style] - ty = asgi_scope.get("type") - - if transaction_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = transaction_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - elif transaction_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - return name, source - - return name, source - - def _get_segment_name_and_source( - self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value - ty = asgi_scope.get("type") - - if segment_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = qualname_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - elif segment_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = SegmentSource.ROUTE.value - return name, source - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py deleted file mode 100644 index 4b3f6d330e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncio.py +++ /dev/null @@ -1,280 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import AsyncHttpTransport -from sentry_sdk.utils import ( - event_from_exception, - is_internal_task, - logger, - reraise, -) - -try: - import asyncio - from asyncio.tasks import Task -except ImportError: - raise DidNotEnable("asyncio not available") - -from typing import TYPE_CHECKING, Optional, Union - -if TYPE_CHECKING: - from collections.abc import Coroutine - from typing import Any, Callable, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T", bound=Callable[..., Any]) - - -def get_name(coro: "Any") -> str: - return ( - getattr(coro, "__qualname__", None) - or getattr(coro, "__name__", None) - or "coroutine without __name__" - ) - - -def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": - # Only __name__ and __qualname__ are copied from function to coroutine in CPython - return functools.partial( - functools.update_wrapper, - wrapped=wrapped, # type: ignore - assigned=("__name__", "__qualname__"), - updated=(), - ) - - -def patch_loop_close() -> None: - """Patch loop.close to flush pending events before shutdown.""" - # Atexit shutdown hook happens after the event loop is closed. - # Therefore, it is necessary to patch the loop.close method to ensure - # that pending events are flushed before the interpreter shuts down. - try: - loop = asyncio.get_running_loop() - except RuntimeError: - # No running loop → cannot patch now - return - - if getattr(loop, "_sentry_flush_patched", False): - return - - async def _flush() -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - try: - if not isinstance(client.transport, AsyncHttpTransport): - return - - await client.close_async() - except Exception: - logger.warning("Sentry flush failed during loop shutdown", exc_info=True) - - orig_close = loop.close - - def _patched_close() -> None: - try: - loop.run_until_complete(_flush()) - except Exception: - logger.debug( - "Could not flush Sentry events during loop close", exc_info=True - ) - finally: - orig_close() - - loop.close = _patched_close # type: ignore - loop._sentry_flush_patched = True # type: ignore - - -def _create_task_with_factory( - orig_task_factory: "Any", - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", -) -> "asyncio.Task[Any]": - task = None - - # Trying to use user set task factory (if there is one) - if orig_task_factory: - task = orig_task_factory(loop, coro, **kwargs) - - if task is None: - # The default task factory in `asyncio` does not have its own function - # but is just a couple of lines in `asyncio.base_events.create_task()` - # Those lines are copied here. - - # WARNING: - # If the default behavior of the task creation in asyncio changes, - # this will break! - task = Task(coro, loop=loop, **kwargs) - if task._source_traceback: # type: ignore - del task._source_traceback[-1] # type: ignore - - return task - - -def patch_asyncio() -> None: - orig_task_factory = None - try: - loop = asyncio.get_running_loop() - orig_task_factory = loop.get_task_factory() - - # Check if already patched - if getattr(orig_task_factory, "_is_sentry_task_factory", False): - return - - def _sentry_task_factory( - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", - ) -> "asyncio.Future[Any]": - # Check if this is an internal Sentry task - if is_internal_task(): - return _create_task_with_factory( - orig_task_factory, loop, coro, **kwargs - ) - - @_wrap_coroutine(coro) - async def _task_with_sentry_span_creation() -> "Any": - result = None - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncioIntegration) - task_spans = integration.task_spans if integration else False - - span_ctx: "Optional[Union[StreamedSpan, Span]]" = None - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope(): - if task_spans: - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=get_name(coro), - attributes={ - "sentry.op": OP.FUNCTION, - "sentry.origin": AsyncioIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.FUNCTION, - name=get_name(coro), - origin=AsyncioIntegration.origin, - ) - - with span_ctx if span_ctx else nullcontext(): - try: - result = await coro - except StopAsyncIteration as e: - raise e from None - except Exception: - reraise(*_capture_exception()) - - return result - - task = _create_task_with_factory( - orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs - ) - - # Set the task name to include the original coroutine's name - try: - task.set_name(f"{get_name(coro)} (Sentry-wrapped)") - except AttributeError: - # set_name might not be available in all Python versions - pass - - return task - - _sentry_task_factory._is_sentry_task_factory = True # type: ignore - loop.set_task_factory(_sentry_task_factory) # type: ignore - - except RuntimeError: - # When there is no running loop, we have nothing to patch. - logger.warning( - "There is no running asyncio loop so there is nothing Sentry can patch. " - "Please make sure you call sentry_sdk.init() within a running " - "asyncio loop for the AsyncioIntegration to work. " - "See https://docs.sentry.io/platforms/python/integrations/asyncio/" - ) - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - - integration = client.get_integration(AsyncioIntegration) - if integration is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "asyncio", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class AsyncioIntegration(Integration): - identifier = "asyncio" - origin = f"auto.function.{identifier}" - - def __init__(self, task_spans: bool = True) -> None: - self.task_spans = task_spans - - @staticmethod - def setup_once() -> None: - patch_asyncio() - patch_loop_close() - - -def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: - """ - Enable AsyncioIntegration with the provided options. - - This is useful in scenarios where Sentry needs to be initialized before - an event loop is set up, but you still want to instrument asyncio once there - is an event loop. In that case, you can sentry_sdk.init() early on without - the AsyncioIntegration and then, once the event loop has been set up, - execute: - - ```python - from sentry_sdk.integrations.asyncio import enable_asyncio_integration - - async def async_entrypoint(): - enable_asyncio_integration() - ``` - - Any arguments provided will be passed to AsyncioIntegration() as is. - - If AsyncioIntegration has already patched the current event loop, this - function won't have any effect. - - If AsyncioIntegration was provided in - sentry_sdk.init(disabled_integrations=[...]), this function will ignore that - and the integration will be enabled. - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # This function purposefully bypasses the integration machinery in - # integrations/__init__.py. _installed_integrations/_processed_integrations - # is used to prevent double patching the same module, but in the case of - # the AsyncioIntegration, we don't monkeypatch the standard library directly, - # we patch the currently running event loop, and we keep the record of doing - # that on the loop itself. - logger.debug("Setting up integration asyncio") - - integration = AsyncioIntegration(*args, **kwargs) - integration.setup_once() - - if "asyncio" not in client.integrations: - client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py deleted file mode 100644 index 186176d268..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/asyncpg.py +++ /dev/null @@ -1,312 +0,0 @@ -from __future__ import annotations - -import contextlib -import re -from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import asyncpg # type: ignore - from asyncpg.cursor import ( # type: ignore - BaseCursor, - Cursor, - CursorIterator, - ) - -except ImportError: - raise DidNotEnable("asyncpg not installed.") - - -class AsyncPGIntegration(Integration): - identifier = "asyncpg" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AsyncPGIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - # asyncpg.__version__ is a string containing the semantic version in the form of ".." - asyncpg_version = parse_version(asyncpg.__version__) - _check_minimum_version(AsyncPGIntegration, asyncpg_version) - - asyncpg.Connection.execute = _wrap_execute( - asyncpg.Connection.execute, - ) - - asyncpg.Connection._execute = _wrap_connection_method( - asyncpg.Connection._execute - ) - asyncpg.Connection._executemany = _wrap_connection_method( - asyncpg.Connection._executemany, executemany=True - ) - asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) - - BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) - BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) - - asyncpg.connect_utils._connect_addr = _wrap_connect_addr( - asyncpg.connect_utils._connect_addr - ) - - -T = TypeVar("T") - - -def _normalize_query(query: str) -> str: - return re.sub(r"\s+", " ", query).strip() - - -def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - # Avoid recording calls to _execute twice. - # Calls to Connection.execute with args also call - # Connection._execute, which is recorded separately - # args[0] = the connection object, args[1] is the query - if len(args) > 2: - return await f(*args, **kwargs) - - query = _normalize_query(args[1]) - with record_sql_queries( - cursor=None, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - span_origin=AsyncPGIntegration.origin, - ) as span: - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -SubCursor = TypeVar("SubCursor", bound=BaseCursor) - - -@contextlib.contextmanager -def _record( - cursor: "SubCursor | None", - query: str, - params_list: "tuple[Any, ...] | None", - *, - executemany: bool = False, -) -> "Iterator[Union[Span, StreamedSpan]]": - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncPGIntegration) - if integration is not None and not integration._record_params: - params_list = None - - param_style = "pyformat" if params_list else None - - query = _normalize_query(query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=params_list, - paramstyle=param_style, - executemany=executemany, - record_cursor_repr=cursor is not None, - span_origin=AsyncPGIntegration.origin, - ) as span: - yield span - - -def _wrap_connection_method( - f: "Callable[..., Awaitable[T]]", *, executemany: bool = False -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - query = args[1] - params_list = args[2] if len(args) > 2 else None - with _record(None, query, params_list, executemany=executemany) as span: - _set_db_data(span, args[0]) - - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_cursor_method( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - if type(cursor) is CursorIterator: - span_op_override_value = OP.DB_CURSOR_ITERATOR - elif type(cursor) is Cursor: - span_op_override_value = OP.DB_CURSOR_FETCH - else: - span_op_override_value = None - - query = _normalize_query(cursor._query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - record_cursor_repr=True, - span_origin=AsyncPGIntegration.origin, - span_op_override_value=span_op_override_value, - ) as span: - _set_db_data(span, cursor._connection) - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_connect_addr( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - user = kwargs["params"].user - database = kwargs["params"].database - addr = kwargs.get("addr") - - if has_span_streaming_enabled(client.options): - span_attributes = { - "sentry.op": OP.DB, - "sentry.origin": AsyncPGIntegration.origin, - SPANDATA.DB_SYSTEM_NAME: "postgresql", - SPANDATA.DB_USER: user, - SPANDATA.DB_NAMESPACE: database, - SPANDATA.DB_DRIVER_NAME: "asyncpg", - } - if addr: - try: - span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] - span_attributes[SPANDATA.SERVER_PORT] = addr[1] - except IndexError: - pass - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span_attributes - ) - res = await f(*args, **kwargs) - - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AsyncPGIntegration.origin, - ) as span: - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - span.set_data(SPANDATA.DB_NAME, database) - span.set_data(SPANDATA.DB_USER, user) - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span._data - ) - res = await f(*args, **kwargs) - - return res - - return _inner - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - addr = conn._addr - database = conn._params.database - user = conn._params.user - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") - if addr: - try: - span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_attribute(SPANDATA.DB_NAMESPACE, database) - - if user: - span.set_attribute(SPANDATA.DB_USER, user) - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_data(SPANDATA.DB_NAME, database) - - if user: - span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py deleted file mode 100644 index b573e76f09..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/atexit.py +++ /dev/null @@ -1,51 +0,0 @@ -import atexit -import os -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Optional - - -def default_callback(pending: int, timeout: int) -> None: - """This is the default shutdown callback that is set on the options. - It prints out a message to stderr that informs the user that some events - are still pending and the process is waiting for them to flush out. - """ - - def echo(msg: str) -> None: - sys.stderr.write(msg + "\n") - - echo("Sentry is attempting to send %i pending events" % pending) - echo("Waiting up to %s seconds" % timeout) - echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) - sys.stderr.flush() - - -class AtexitIntegration(Integration): - identifier = "atexit" - - def __init__(self, callback: "Optional[Any]" = None) -> None: - if callback is None: - callback = default_callback - self.callback = callback - - @staticmethod - def setup_once() -> None: - @atexit.register - def _shutdown() -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(AtexitIntegration) - - if integration is None: - return - - logger.debug("atexit: got shutdown signal") - logger.debug("atexit: shutting down client") - sentry_sdk.get_isolation_scope().end_session() - - client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py deleted file mode 100644 index c7fe77714a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/aws_lambda.py +++ /dev/null @@ -1,542 +0,0 @@ -import functools -import json -import re -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING -from urllib.parse import urlencode - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.cloud_resource_context import ( - CLOUD_PLATFORM, - CLOUD_PROVIDER, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - -# Constants -TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - - -def _wrap_init_error(init_error: "F") -> "F": - @ensure_integration_enabled(AwsLambdaIntegration, init_error) - def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - with capture_internal_exceptions(): - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - exc_info = sys.exc_info() - if exc_info and all(exc_info): - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - - else: - # Fall back to AWS lambdas JSON representation of the error - error_info = args[1] - if isinstance(error_info, str): - error_info = json.loads(error_info) - sentry_event = _event_from_error_json(error_info) - sentry_sdk.capture_event(sentry_event) - - return init_error(*args, **kwargs) - - return sentry_init_error # type: ignore - - -def _wrap_handler(handler: "F") -> "F": - @functools.wraps(handler) - def sentry_handler( - aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, - # `event` here is *likely* a dictionary, but also might be a number of - # other types (str, int, float, None). - # - # In some cases, it is a list (if the user is batch-invoking their - # function, for example), in which case we'll use the first entry as a - # representative from which to try pulling request data. (Presumably it - # will be the same for all events in the list, since they're all hitting - # the lambda in the same request.) - - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - - if integration is None: - return handler(aws_event, aws_context, *args, **kwargs) - - if isinstance(aws_event, list) and len(aws_event) >= 1: - request_data = aws_event[0] - batch_size = len(aws_event) - else: - request_data = aws_event - batch_size = 1 - - if not isinstance(request_data, dict): - # If we're not dealing with a dictionary, we won't be able to get - # headers, path, http method, etc in any case, so it's fine that - # this is empty - request_data = {} - - configured_time = aws_context.get_remaining_time_in_millis() - aws_region = aws_context.invoked_function_arn.split(":")[3] - - with sentry_sdk.isolation_scope() as scope: - timeout_thread = None - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - request_data, aws_context, configured_time - ) - ) - scope.set_tag("aws_region", aws_region) - if batch_size > 1: - scope.set_tag("batch_request", True) - scope.set_tag("batch_size", batch_size) - - # Starting the Timeout thread only if the configured time is greater than Timeout warning - # buffer and timeout_warning parameter is set True. - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = ( - configured_time - TIMEOUT_WARNING_BUFFER - ) / MILLIS_TO_SECONDS - - timeout_thread = TimeoutThread( - waiting_time, - configured_time / MILLIS_TO_SECONDS, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = request_data.get("headers", {}) - # Some AWS Services (ie. EventBridge) set headers as a list - # or None, so we must ensure it is a dict - if not isinstance(headers, dict): - headers = {} - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - additional_attributes: "dict[str, Any]" = {} - if "httpMethod" in request_data: - additional_attributes["http.request.method"] = request_data[ - "httpMethod" - ] - - if should_send_default_pii() and "queryStringParameters" in request_data: - qs = request_data["queryStringParameters"] - if qs: - additional_attributes["url.query"] = urlencode(qs) - - sampling_context = { - "aws_event": aws_event, - "aws_context": aws_context, - } - - function_name = aws_context.function_name - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_AWS, - "sentry.origin": AwsLambdaIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.region": aws_region, - "cloud.resource_id": aws_context.invoked_function_arn, - "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, - "cloud.provider": CLOUD_PROVIDER.AWS, - "faas.name": function_name, - "faas.invocation_id": aws_context.aws_request_id, - "faas.version": aws_context.function_version, - "aws.lambda.invoked_arn": aws_context.invoked_function_arn, - "aws.log.group.names": [aws_context.log_group_name], - "aws.log.stream.names": [aws_context.log_stream_name], - "messaging.batch.message_count": batch_size, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_AWS, - name=function_name, - source=TransactionSource.COMPONENT, - origin=AwsLambdaIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return handler(aws_event, aws_context, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - - return sentry_handler # type: ignore - - -def _drain_queue() -> None: - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - if integration is not None: - # Flush out the event queue before AWS kills the - # process. - client.flush() - - -class AwsLambdaIntegration(Integration): - identifier = "aws_lambda" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - lambda_bootstrap = get_lambda_bootstrap() - if not lambda_bootstrap: - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find bootstrap module)" - ) - return - - if not hasattr(lambda_bootstrap, "handle_event_request"): - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find handle_event_request)" - ) - return - - pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 - - if pre_37: - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_event_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - old_handle_http_request = lambda_bootstrap.handle_http_request - - def sentry_handle_http_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_http_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_http_request = sentry_handle_http_request - - # Patch to_json to drain the queue. This should work even when the - # SDK is initialized inside of the handler - - old_to_json = lambda_bootstrap.to_json - - def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return old_to_json(*args, **kwargs) - - lambda_bootstrap.to_json = sentry_to_json - else: - lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( - lambda_bootstrap.LambdaRuntimeClient.post_init_error - ) - - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( # type: ignore - lambda_runtime_client, request_handler, *args, **kwargs - ): - request_handler = _wrap_handler(request_handler) - return old_handle_event_request( - lambda_runtime_client, request_handler, *args, **kwargs - ) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - # Patch the runtime client to drain the queue. This should work - # even when the SDK is initialized inside of the handler - - def _wrap_post_function(f: "F") -> "F": - def inner(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return f(*args, **kwargs) - - return inner # type: ignore - - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result - ) - ) - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error - ) - ) - - -def get_lambda_bootstrap() -> "Optional[Any]": - # Python 3.7: If the bootstrap module is *already imported*, it is the - # one we actually want to use (no idea what's in __main__) - # - # Python 3.8: bootstrap is also importable, but will be the same file - # as __main__ imported under a different name: - # - # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ - # sys.modules['__main__'] is not sys.modules['bootstrap'] - # - # Python 3.9: bootstrap is in __main__.awslambdaricmain - # - # On container builds using the `aws-lambda-python-runtime-interface-client` - # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap - # - # Such a setup would then make all monkeypatches useless. - if "bootstrap" in sys.modules: - return sys.modules["bootstrap"] - elif "__main__" in sys.modules: - module = sys.modules["__main__"] - # python3.9 runtime - if hasattr(module, "awslambdaricmain") and hasattr( - module.awslambdaricmain, "bootstrap" - ): - return module.awslambdaricmain.bootstrap - elif hasattr(module, "bootstrap"): - # awslambdaric python module in container builds - return module.bootstrap - - # python3.8 runtime - return module - else: - return None - - -def _make_request_event_processor( - aws_event: "Any", aws_context: "Any", configured_timeout: "Any" -) -> "EventProcessor": - start_time = datetime.now(timezone.utc) - - def event_processor( - sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time - ) -> "Optional[Event]": - remaining_time_in_milis = aws_context.get_remaining_time_in_millis() - exec_duration = configured_timeout - remaining_time_in_milis - - extra = sentry_event.setdefault("extra", {}) - extra["lambda"] = { - "function_name": aws_context.function_name, - "function_version": aws_context.function_version, - "invoked_function_arn": aws_context.invoked_function_arn, - "aws_request_id": aws_context.aws_request_id, - "execution_duration_in_millis": exec_duration, - "remaining_time_in_millis": remaining_time_in_milis, - } - - extra["cloudwatch logs"] = { - "url": _get_cloudwatch_logs_url(aws_context, start_time), - "log_group": aws_context.log_group_name, - "log_stream": aws_context.log_stream_name, - } - - request = sentry_event.get("request", {}) - - if "httpMethod" in aws_event: - request["method"] = aws_event["httpMethod"] - - request["url"] = _get_url(aws_event, aws_context) - - if "queryStringParameters" in aws_event: - request["query_string"] = aws_event["queryStringParameters"] - - if "headers" in aws_event: - request["headers"] = _filter_headers(aws_event["headers"]) - - if should_send_default_pii(): - user_info = sentry_event.setdefault("user", {}) - - identity = aws_event.get("identity") - if identity is None: - identity = {} - - id = identity.get("userArn") - if id is not None: - user_info.setdefault("id", id) - - ip = identity.get("sourceIp") - if ip is not None: - user_info.setdefault("ip_address", ip) - - if "body" in aws_event: - request["data"] = aws_event.get("body", "") - else: - if aws_event.get("body", None): - # Unfortunately couldn't find a way to get structured body from AWS - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - sentry_event["request"] = deepcopy(request) - - return sentry_event - - return event_processor - - -def _get_url(aws_event: "Any", aws_context: "Any") -> str: - path = aws_event.get("path", None) - - headers = aws_event.get("headers") - if headers is None: - headers = {} - - host = headers.get("Host", None) - proto = headers.get("X-Forwarded-Proto", None) - if proto and host and path: - return "{}://{}{}".format(proto, host, path) - return "awslambda:///{}".format(aws_context.function_name) - - -def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: - """ - Generates a CloudWatchLogs console URL based on the context object - - Arguments: - aws_context {Any} -- context from lambda handler - - Returns: - str -- AWS Console URL to logs. - """ - formatstring = "%Y-%m-%dT%H:%M:%SZ" - region = environ.get("AWS_REGION", "") - - url = ( - "https://console.{domain}/cloudwatch/home?region={region}" - "#logEventViewer:group={log_group};stream={log_stream}" - ";start={start_time};end={end_time}" - ).format( - domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", - region=region, - log_group=aws_context.log_group_name, - log_stream=aws_context.log_stream_name, - start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), - end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( - formatstring - ), - ) - - return url - - -def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": - frames = [] - for frame in formatted_tb: - match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) - if match: - file_name, line_number, func_name = match.groups() - line_number = int(line_number) - frames.append( - { - "filename": file_name, - "function": func_name, - "lineno": line_number, - "vars": None, - "pre_context": None, - "context_line": None, - "post_context": None, - } - ) - return frames - - -def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": - """ - Converts the error JSON from AWS Lambda into a Sentry error event. - This is not a full fletched event, but better than nothing. - - This is an example of where AWS creates the error JSON: - https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 - """ - event: "Event" = { - "level": "error", - "exception": { - "values": [ - { - "type": error_json.get("errorType"), - "value": error_json.get("errorMessage"), - "stacktrace": { - "frames": _parse_formatted_traceback( - error_json.get("stackTrace", []) - ), - }, - "mechanism": { - "type": "aws_lambda", - "handled": False, - }, - } - ], - }, - } - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py deleted file mode 100644 index 31f45f73de..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/beam.py +++ /dev/null @@ -1,164 +0,0 @@ -import sys -import types -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) - - -WRAPPED_FUNC = "_wrapped_{}_" -INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py -USED_FUNC = "_sentry_used_" - - -class BeamIntegration(Integration): - identifier = "beam" - - @staticmethod - def setup_once() -> None: - from apache_beam.transforms.core import DoFn, ParDo # type: ignore - - ignore_logger("root") - ignore_logger("bundle_processor.create") - - function_patches = ["process", "start_bundle", "finish_bundle", "setup"] - for func_name in function_patches: - setattr( - DoFn, - INSPECT_FUNC.format(func_name), - _wrap_inspect_call(DoFn, func_name), - ) - - old_init = ParDo.__init__ - - def sentry_init_pardo( - self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Do not monkey patch init twice - if not getattr(self, "_sentry_is_patched", False): - for func_name in function_patches: - if not hasattr(fn, func_name): - continue - wrapped_func = WRAPPED_FUNC.format(func_name) - - # Check to see if inspect is set and process is not - # to avoid monkey patching process twice. - # Check to see if function is part of object for - # backwards compatibility. - process_func = getattr(fn, func_name) - inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) - if not getattr(inspect_func, USED_FUNC, False) and not getattr( - process_func, USED_FUNC, False - ): - setattr(fn, wrapped_func, process_func) - setattr(fn, func_name, _wrap_task_call(process_func)) - - self._sentry_is_patched = True - old_init(self, fn, *args, **kwargs) - - ParDo.__init__ = sentry_init_pardo - - -def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": - if not hasattr(cls, func_name): - return None - - def _inspect(self: "Any") -> "Any": - """ - Inspect function overrides the way Beam gets argspec. - """ - wrapped_func = WRAPPED_FUNC.format(func_name) - if hasattr(self, wrapped_func): - process_func = getattr(self, wrapped_func) - else: - process_func = getattr(self, func_name) - setattr(self, func_name, _wrap_task_call(process_func)) - setattr(self, wrapped_func, process_func) - - # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults - # (which uses Signatures internally) should be used instead. - try: - from apache_beam.transforms.core import get_function_args_defaults - - return get_function_args_defaults(process_func) - except ImportError: - from apache_beam.typehints.decorators import getfullargspec # type: ignore - - return getfullargspec(process_func) - - setattr(_inspect, USED_FUNC, True) - return _inspect - - -def _wrap_task_call(func: "F") -> "F": - """ - Wrap task call with a try catch to get exceptions. - """ - - @wraps(func) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - try: - gen = func(*args, **kwargs) - except Exception: - raise_exception() - - if not isinstance(gen, types.GeneratorType): - return gen - return _wrap_generator_call(gen) - - setattr(_inner, USED_FUNC, True) - return _inner # type: ignore - - -@ensure_integration_enabled(BeamIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - """ - Send Beam exception to Sentry. - """ - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "beam", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def raise_exception() -> None: - """ - Raise an exception. - """ - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) - reraise(*exc_info) - - -def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": - """ - Wrap the generator to handle any failures. - """ - while True: - try: - yield next(gen) - except StopIteration: - break - except Exception: - raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py deleted file mode 100644 index a7fdd99b21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/boto3.py +++ /dev/null @@ -1,187 +0,0 @@ -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_url, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Type, Union - - from botocore.model import ServiceId - -try: - from botocore import __version__ as BOTOCORE_VERSION - from botocore.awsrequest import AWSRequest - from botocore.client import BaseClient - from botocore.response import StreamingBody -except ImportError: - raise DidNotEnable("botocore is not installed") - - -class Boto3Integration(Integration): - identifier = "boto3" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTOCORE_VERSION) - _check_minimum_version(Boto3Integration, version, "botocore") - - orig_init = BaseClient.__init__ - - def sentry_patched_init( - self: "BaseClient", *args: "Any", **kwargs: "Any" - ) -> None: - orig_init(self, *args, **kwargs) - meta = self.meta - service_id = meta.service_model.service_id - meta.events.register( - "request-created", - partial(_sentry_request_created, service_id=service_id), - ) - meta.events.register("after-call", _sentry_after_call) - meta.events.register("after-call-error", _sentry_after_call_error) - - BaseClient.__init__ = sentry_patched_init # type: ignore - - -def _sentry_request_created( - service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" -) -> None: - description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) - - client = sentry_sdk.get_client() - if client.get_integration(Boto3Integration) is None: - return - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Boto3Integration.origin, - SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", - }, - ) - if request.url is not None and should_send_default_pii(): - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if request.method is not None: - span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=description, - origin=Boto3Integration.origin, - ) - - if request.url is not None: - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_data("aws.request.url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - span.set_tag("aws.service_id", service_id.hyphenize()) - span.set_tag("aws.operation_name", operation_name) - if request.method is not None: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - - # We do it in order for subsequent http calls/retries be - # attached to this span. - span.__enter__() - - # request.context is an open-ended data-structure - # where we can add anything useful in request life cycle. - request.context["_sentrysdk_span"] = span - - -def _sentry_after_call( - context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(None, None, None) - - body = parsed.get("Body") - if not isinstance(body, StreamingBody): - return - - streaming_span: "Union[Span, StreamedSpan]" - if isinstance(span, StreamedSpan): - streaming_span = sentry_sdk.traces.start_span( - name=span.name, - parent_span=span, - attributes={ - "sentry.op": OP.HTTP_CLIENT_STREAM, - "sentry.origin": Boto3Integration.origin, - }, - ) - else: - streaming_span = span.start_child( - op=OP.HTTP_CLIENT_STREAM, - name=span.description, - origin=Boto3Integration.origin, - ) - - orig_read = body.read - orig_close = body.close - - def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: - try: - ret = orig_read(*args, **kwargs) - if ret: - return ret - - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - return ret - except Exception: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - raise - - body.read = sentry_streaming_body_read # type: ignore - - def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - orig_close(*args, **kwargs) - - body.close = sentry_streaming_body_close # type: ignore - - -def _sentry_after_call_error( - context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py deleted file mode 100644 index 50f6ca2e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/bottle.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from collections.abc import Set - from typing import Any, Callable, Dict, Optional - - from bottle import FileUpload, FormsDict, LocalRequest # type: ignore - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - -try: - from bottle import ( - Bottle, - HTTPResponse, - Route, - ) - from bottle import ( - __version__ as BOTTLE_VERSION, - ) - from bottle import ( - request as bottle_request, - ) -except ImportError: - raise DidNotEnable("Bottle not installed") - - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class BottleIntegration(Integration): - identifier = "bottle" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTTLE_VERSION) - _check_minimum_version(BottleIntegration, version) - - old_app = Bottle.__call__ - - @ensure_integration_enabled(BottleIntegration, old_app) - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=BottleIntegration.origin, - ) - - return middleware(environ, start_response) - - Bottle.__call__ = sentry_patched_wsgi_app - - old_handle = Bottle._handle - - @functools.wraps(old_handle) - def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return old_handle(self, environ) - - scope = sentry_sdk.get_isolation_scope() - scope._name = "bottle" - scope.add_event_processor( - _make_request_event_processor(self, bottle_request, integration) - ) - res = old_handle(self, environ) - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - _set_segment_name_and_source( - transaction_style=integration.transaction_style - ) - - return res - - Bottle._handle = _patched_handle - - old_make_callback = Route._make_callback - - @functools.wraps(old_make_callback) - def patched_make_callback( - self: "Route", *args: object, **kwargs: object - ) -> "Any": - prepared_callback = old_make_callback(self, *args, **kwargs) - - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return prepared_callback - - def wrapped_callback(*args: object, **kwargs: object) -> "Any": - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) - - return res - - return wrapped_callback - - Route._make_callback = patched_make_callback - - -class BottleRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[str, str]": - return self.request.cookies - - def raw_data(self) -> bytes: - return self.request.body.read() - - def form(self) -> "FormsDict": - if self.is_json(): - return None - return self.request.forms.decode() - - def files(self) -> "Optional[Dict[str, str]]": - if self.is_json(): - return None - - return self.request.files - - def size_of_file(self, file: "FileUpload") -> int: - return file.content_length - - -def _set_segment_name_and_source(transaction_style: str) -> None: - try: - if transaction_style == "url": - name = bottle_request.route.rule or "bottle request" - else: - name = ( - bottle_request.route.name - or transaction_from_function(bottle_request.route.callback) - or "bottle request" - ) - - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], - ) - except RuntimeError: - pass - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "url": - try: - name = request.route.rule or "" - except RuntimeError: - pass - - elif transaction_style == "endpoint": - try: - name = ( - request.route.name - or transaction_from_function(request.route.callback) - or "" - ) - except RuntimeError: - pass - - event["transaction"] = name - event["transaction_info"] = { - "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - } - - -def _make_request_event_processor( - app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, request) - - with capture_internal_exceptions(): - BottleRequestExtractor(request).extract_into_event(event) - - return event - - return event_processor - - -def _capture_exception(exception: BaseException, handled: bool) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "bottle", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py deleted file mode 100644 index 532b13539b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/__init__.py +++ /dev/null @@ -1,612 +0,0 @@ -import sys -from collections.abc import Mapping -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import isolation_scope -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.celery.beat import ( - _patch_beat_apply_entry, - _patch_redbeat_apply_async, - _setup_celery_beat_signals, -) -from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource -from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, List, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import VERSION as CELERY_VERSION # type: ignore - from celery.app.task import Task # type: ignore - from celery.app.trace import task_has_custom - from celery.exceptions import ( # type: ignore - Ignore, - Reject, - Retry, - SoftTimeLimitExceeded, - ) - from kombu import Producer # type: ignore -except ImportError: - raise DidNotEnable("Celery not installed") - - -CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) - - -class CeleryIntegration(Integration): - identifier = "celery" - origin = f"auto.queue.{identifier}" - - def __init__( - self, - propagate_traces: bool = True, - monitor_beat_tasks: bool = False, - exclude_beat_tasks: "Optional[List[str]]" = None, - ) -> None: - self.propagate_traces = propagate_traces - self.monitor_beat_tasks = monitor_beat_tasks - self.exclude_beat_tasks = exclude_beat_tasks - - _patch_beat_apply_entry() - _patch_redbeat_apply_async() - _setup_celery_beat_signals(monitor_beat_tasks) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(CeleryIntegration, CELERY_VERSION) - - _patch_build_tracer() - _patch_task_apply_async() - _patch_celery_send_task() - _patch_worker_exit() - _patch_producer_publish() - - # This logger logs every status of every task that ran on the worker. - # Meaning that every task's breadcrumbs are full of stuff like "Task - # raised unexpected ". - ignore_logger("celery.worker.job") - ignore_logger("celery.app.trace") - - # This is stdout/err redirected to a logger, can't deal with this - # (need event_level=logging.WARN to reproduce) - ignore_logger("celery.redirected") - - -def _set_status(status: str) -> None: - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - - if span_streaming and scope.streamed_span is not None: - scope.streamed_span.status = "ok" if status == "ok" else "error" - elif not span_streaming and scope.span is not None: - scope.span.set_status(status) - - -def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return - - if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): - # ??? Doesn't map to anything - _set_status("aborted") - return - - _set_status("internal_error") - - if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): - return - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "celery", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - task: "Any", - uuid: "Any", - args: "Any", - kwargs: "Any", - request: "Optional[Any]" = None, -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["celery_task_id"] = uuid - extra = event.setdefault("extra", {}) - extra["celery-job"] = { - "task_name": task.name, - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - } - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): - event["fingerprint"] = [ - "celery", - "SoftTimeLimitExceeded", - getattr(task, "name", task), - ] - - return event - - return event_processor - - -def _update_celery_task_headers( - original_headers: "dict[str, Any]", - span: "Optional[Union[StreamedSpan, Span]]", - monitor_beat_tasks: bool, -) -> "dict[str, Any]": - """ - Updates the headers of the Celery task with the tracing information - and eventually Sentry Crons monitoring information for beat tasks. - """ - updated_headers = original_headers.copy() - with capture_internal_exceptions(): - # if span is None (when the task was started by Celery Beat) - # this will return the trace headers from the scope. - headers = dict( - sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) - ) - - if monitor_beat_tasks: - headers.update( - { - "sentry-monitor-start-timestamp-s": "%.9f" - % _now_seconds_since_epoch(), - } - ) - - # Add the time the task was enqueued to the headers - # This is used in the consumer to calculate the latency - updated_headers.update( - {"sentry-task-enqueued-time": _now_seconds_since_epoch()} - ) - - if headers: - existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) - sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) - - combined_baggage = sentry_baggage or existing_baggage - if sentry_baggage and existing_baggage: - # Merge incoming and sentry baggage, where the sentry trace information - # in the incoming baggage takes precedence and the third-party items - # are concatenated. - incoming = Baggage.from_incoming_header(existing_baggage) - combined = Baggage.from_incoming_header(sentry_baggage) - combined.sentry_items.update(incoming.sentry_items) - combined.third_party_items = ",".join( - [ - x - for x in [ - combined.third_party_items, - incoming.third_party_items, - ] - if x is not None and x != "" - ] - ) - combined_baggage = combined.serialize(include_third_party=True) - - updated_headers.update(headers) - if combined_baggage: - updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage - - # https://github.com/celery/celery/issues/4875 - # - # Need to setdefault the inner headers too since other - # tracing tools (dd-trace-py) also employ this exact - # workaround and we don't want to break them. - updated_headers.setdefault("headers", {}).update(headers) - if combined_baggage: - updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage - - # Add the Sentry options potentially added in `sentry_apply_entry` - # to the headers (done when auto-instrumenting Celery Beat tasks) - for key, value in updated_headers.items(): - if key.startswith("sentry-"): - updated_headers["headers"][key] = value - - # Preserve user-provided custom headers in the inner "headers" dict - # so they survive to task.request.headers on the worker (celery#4875). - for key, value in original_headers.items(): - if key != "headers" and key not in updated_headers["headers"]: - updated_headers["headers"][key] = value - - return updated_headers - - -class NoOpMgr: - def __enter__(self) -> None: - return None - - def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: - return None - - -def _wrap_task_run(f: "F") -> "F": - @wraps(f) - def apply_async(*args: "Any", **kwargs: "Any") -> "Any": - # Note: kwargs can contain headers=None, so no setdefault! - # Unsure which backend though. - client = sentry_sdk.get_client() - integration = client.get_integration(CeleryIntegration) - if integration is None: - return f(*args, **kwargs) - - kwarg_headers = kwargs.get("headers") or {} - propagate_traces = kwarg_headers.pop( - "sentry-propagate-traces", integration.propagate_traces - ) - - if not propagate_traces: - return f(*args, **kwargs) - - if isinstance(args[0], Task): - task_name: str = args[0].name - elif len(args) > 1 and isinstance(args[1], str): - task_name = args[1] - else: - task_name = "" - - span_streaming = has_span_streaming_enabled(client.options) - - task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" - - span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - if span_streaming: - if not task_started_from_beat and get_current_span() is not None: - span_mgr = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_CELERY, - "sentry.origin": CeleryIntegration.origin, - }, - ) - - else: - if not task_started_from_beat: - span_mgr = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_CELERY, - name=task_name, - origin=CeleryIntegration.origin, - ) - - with span_mgr as span: - kwargs["headers"] = _update_celery_task_headers( - kwarg_headers, span, integration.monitor_beat_tasks - ) - return f(*args, **kwargs) - - return apply_async # type: ignore - - -def _wrap_tracer(task: "Any", f: "F") -> "F": - # Need to wrap tracer for pushing the scope before prerun is sent, and - # popping it after postrun is sent. - # - # This is the reason we don't use signals for hooking in the first place. - # Also because in Celery 3, signal dispatch returns early if one handler - # crashes. - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - with isolation_scope() as scope: - scope._name = "celery" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - - task_name = getattr(task, "name", "") - - custom_sampling_context = {} - with capture_internal_exceptions(): - custom_sampling_context = { - "celery_job": { - "task": task_name, - # for some reason, args[1] is a list if non-empty but a - # tuple if empty - "args": list(args[1]), - "kwargs": args[2], - } - } - - span: "Union[Span, StreamedSpan]" - span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - - # Celery task objects are not a thing to be trusted. Even - # something such as attribute access can fail. - with capture_internal_exceptions(): - headers = args[3].get("headers") or {} - if span_streaming: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(custom_sampling_context) - span = sentry_sdk.traces.start_span( - name=task_name, - parent_span=None, # make this a segment - attributes={ - "sentry.origin": CeleryIntegration.origin, - "sentry.span.source": TransactionSource.TASK.value, - "sentry.op": OP.QUEUE_TASK_CELERY, - }, - ) - - span_ctx = span - - else: - span = continue_trace( - headers, - op=OP.QUEUE_TASK_CELERY, - name=task_name, - source=TransactionSource.TASK, - origin=CeleryIntegration.origin, - ) - span.set_status(SPANSTATUS.OK) - - span_ctx = sentry_sdk.start_transaction( - span, - custom_sampling_context=custom_sampling_context, - ) - - with span_ctx: - return f(*args, **kwargs) - - return _inner # type: ignore - - -def _set_messaging_destination_name( - task: "Any", span: "Union[StreamedSpan, Span]" -) -> None: - """Set "messaging.destination.name" tag for span""" - with capture_internal_exceptions(): - delivery_info = task.request.delivery_info - if delivery_info: - routing_key = delivery_info.get("routing_key") - if delivery_info.get("exchange") == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning the tasks - # are sent to the queue with the same name as the routing key. - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - else: - span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - -def _wrap_task_call(task: "Any", f: "F") -> "F": - # Need to wrap task call because the exception is caught before we get to - # see it. Also celery's reported stacktrace is untrustworthy. - - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - try: - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_PROCESS, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PROCESS, - name=task.name, - origin=CeleryIntegration.origin, - ) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - _set_messaging_destination_name(task, span) - - latency = None - with capture_internal_exceptions(): - if ( - task.request.headers is not None - and "sentry-task-enqueued-time" in task.request.headers - ): - latency = _now_seconds_since_epoch() - task.request.headers.pop( - "sentry-task-enqueued-time" - ) - - if latency is not None: - latency *= 1000 # milliseconds - set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) - - with capture_internal_exceptions(): - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries - ) - - with capture_internal_exceptions(): - with task.app.connection() as conn: - set_on_span( - SPANDATA.MESSAGING_SYSTEM, - conn.transport.driver_type, - ) - - return f(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(task, exc_info) - reraise(*exc_info) - - return _inner # type: ignore - - -def _patch_build_tracer() -> None: - import celery.app.trace as trace # type: ignore - - original_build_tracer = trace.build_tracer - - def sentry_build_tracer( - name: "Any", task: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not getattr(task, "_sentry_is_patched", False): - # determine whether Celery will use __call__ or run and patch - # accordingly - if task_has_custom(task, "__call__"): - type(task).__call__ = _wrap_task_call(task, type(task).__call__) - else: - task.run = _wrap_task_call(task, task.run) - - # `build_tracer` is apparently called for every task - # invocation. Can't wrap every celery task for every invocation - # or we will get infinitely nested wrapper functions. - task._sentry_is_patched = True - - return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) - - trace.build_tracer = sentry_build_tracer - - -def _patch_task_apply_async() -> None: - Task.apply_async = _wrap_task_run(Task.apply_async) - - -def _patch_celery_send_task() -> None: - from celery import Celery - - Celery.send_task = _wrap_task_run(Celery.send_task) - - -def _patch_worker_exit() -> None: - # Need to flush queue before worker shutdown because a crashing worker will - # call os._exit - from billiard.pool import Worker # type: ignore - - original_workloop = Worker.workloop - - def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": - try: - return original_workloop(*args, **kwargs) - finally: - with capture_internal_exceptions(): - if ( - sentry_sdk.get_client().get_integration(CeleryIntegration) - is not None - ): - sentry_sdk.flush() - - Worker.workloop = sentry_workloop - - -def _patch_producer_publish() -> None: - original_publish = Producer.publish - - def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return original_publish(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - kwargs_headers = kwargs.get("headers", {}) - if not isinstance(kwargs_headers, Mapping): - # Ensure kwargs_headers is a Mapping, so we can safely call get(). - # We don't expect this to happen, but it's better to be safe. Even - # if it does happen, only our instrumentation breaks. This line - # does not overwrite kwargs["headers"], so the original publish - # method will still work. - kwargs_headers = {} - - task_name = kwargs_headers.get("task") or "" - task_id = kwargs_headers.get("id") - retries = kwargs_headers.get("retries") - - routing_key = kwargs.get("routing_key") - exchange = kwargs.get("exchange") - - span: "Union[StreamedSpan, Span, None]" = None - if span_streaming: - if get_current_span() is not None: - span = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_PUBLISH, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PUBLISH, - name=task_name, - origin=CeleryIntegration.origin, - ) - - if span is None: - return original_publish(self, *args, **kwargs) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - if task_id is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) - - if exchange == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning messages are - # routed to the queue with the same name as the routing key. - set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - if retries is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type - ) - - return original_publish(self, *args, **kwargs) - - Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py deleted file mode 100644 index b5027d212a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/beat.py +++ /dev/null @@ -1,291 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.crons import MonitorStatus, capture_checkin -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.celery.utils import ( - _get_humanized_interval, - _now_seconds_since_epoch, -) -from sentry_sdk.utils import ( - logger, - match_regex_list, -) - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, TypeVar, Union - - from sentry_sdk._types import ( - MonitorConfig, - MonitorConfigScheduleType, - MonitorConfigScheduleUnit, - ) - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import Celery, Task # type: ignore - from celery.beat import Scheduler # type: ignore - from celery.schedules import crontab, schedule # type: ignore - from celery.signals import ( # type: ignore - task_failure, - task_retry, - task_success, - ) -except ImportError: - raise DidNotEnable("Celery not installed") - -try: - from redbeat.schedulers import RedBeatScheduler # type: ignore -except ImportError: - RedBeatScheduler = None - - -def _get_headers(task: "Task") -> "dict[str, Any]": - headers = task.request.get("headers") or {} - - # flatten nested headers - if "headers" in headers: - headers.update(headers["headers"]) - del headers["headers"] - - headers.update(task.request.get("properties") or {}) - - return headers - - -def _get_monitor_config( - celery_schedule: "Any", app: "Celery", monitor_name: str -) -> "MonitorConfig": - monitor_config: "MonitorConfig" = {} - schedule_type: "Optional[MonitorConfigScheduleType]" = None - schedule_value: "Optional[Union[str, int]]" = None - schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None - - if isinstance(celery_schedule, crontab): - schedule_type = "crontab" - schedule_value = ( - "{0._orig_minute} " - "{0._orig_hour} " - "{0._orig_day_of_month} " - "{0._orig_month_of_year} " - "{0._orig_day_of_week}".format(celery_schedule) - ) - elif isinstance(celery_schedule, schedule): - schedule_type = "interval" - (schedule_value, schedule_unit) = _get_humanized_interval( - celery_schedule.seconds - ) - - if schedule_unit == "second": - logger.warning( - "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", - monitor_name, - schedule_value, - ) - return {} - - else: - logger.warning( - "Celery schedule type '%s' not supported by Sentry Crons.", - type(celery_schedule), - ) - return {} - - monitor_config["schedule"] = {} - monitor_config["schedule"]["type"] = schedule_type - monitor_config["schedule"]["value"] = schedule_value - - if schedule_unit is not None: - monitor_config["schedule"]["unit"] = schedule_unit - - monitor_config["timezone"] = ( - ( - hasattr(celery_schedule, "tz") - and celery_schedule.tz is not None - and str(celery_schedule.tz) - ) - or app.timezone - or "UTC" - ) - - return monitor_config - - -def _apply_crons_data_to_schedule_entry( - scheduler: "Any", - schedule_entry: "Any", - integration: "sentry_sdk.integrations.celery.CeleryIntegration", -) -> None: - """ - Add Sentry Crons information to the schedule_entry headers. - """ - if not integration.monitor_beat_tasks: - return - - monitor_name = schedule_entry.name - - task_should_be_excluded = match_regex_list( - monitor_name, integration.exclude_beat_tasks - ) - if task_should_be_excluded: - return - - celery_schedule = schedule_entry.schedule - app = scheduler.app - - monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) - - is_supported_schedule = bool(monitor_config) - if not is_supported_schedule: - return - - headers = schedule_entry.options.pop("headers", {}) - headers.update( - { - "sentry-monitor-slug": monitor_name, - "sentry-monitor-config": monitor_config, - } - ) - - check_in_id = capture_checkin( - monitor_slug=monitor_name, - monitor_config=monitor_config, - status=MonitorStatus.IN_PROGRESS, - ) - headers.update({"sentry-monitor-check-in-id": check_in_id}) - - # Set the Sentry configuration in the options of the ScheduleEntry. - # Those will be picked up in `apply_async` and added to the headers. - schedule_entry.options["headers"] = headers - - -def _wrap_beat_scheduler( - original_function: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Makes sure that: - - a new Sentry trace is started for each task started by Celery Beat and - it is propagated to the task. - - the Sentry Crons information is set in the Celery Beat task's - headers so that is monitored with Sentry Crons. - - After the patched function is called, - Celery Beat will call apply_async to put the task in the queue. - """ - # Patch only once - # Can't use __name__ here, because some of our tests mock original_apply_entry - already_patched = "sentry_patched_scheduler" in str(original_function) - if already_patched: - return original_function - - from sentry_sdk.integrations.celery import CeleryIntegration - - def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(CeleryIntegration) - if integration is None: - return original_function(*args, **kwargs) - - # Tasks started by Celery Beat start a new Trace - scope = sentry_sdk.get_isolation_scope() - scope.set_new_propagation_context() - scope._name = "celery-beat" - - scheduler, schedule_entry = args - _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) - - return original_function(*args, **kwargs) - - return sentry_patched_scheduler - - -def _patch_beat_apply_entry() -> None: - Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) - - -def _patch_redbeat_apply_async() -> None: - if RedBeatScheduler is None: - return - - RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) - - -def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: - if monitor_beat_tasks: - task_success.connect(crons_task_success) - task_failure.connect(crons_task_failure) - task_retry.connect(crons_task_retry) - - -def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_success %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.OK, - ) - - -def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_failure %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) - - -def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_retry %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py deleted file mode 100644 index 8d181f1f24..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/celery/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import time -from typing import TYPE_CHECKING, cast - -if TYPE_CHECKING: - from typing import Tuple - - from sentry_sdk._types import MonitorConfigScheduleUnit - - -def _now_seconds_since_epoch() -> float: - # We cannot use `time.perf_counter()` when dealing with the duration - # of a Celery task, because the start of a Celery task and - # the end are recorded in different processes. - # Start happens in the Celery Beat process, - # the end in a Celery Worker process. - return time.time() - - -def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": - TIME_UNITS = ( # noqa: N806 - ("day", 60 * 60 * 24.0), - ("hour", 60 * 60.0), - ("minute", 60.0), - ) - - seconds = float(seconds) - for unit, divider in TIME_UNITS: - if seconds >= divider: - interval = int(seconds / divider) - return (interval, cast("MonitorConfigScheduleUnit", unit)) - - return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py deleted file mode 100644 index 9baa0e5cdd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/chalice.py +++ /dev/null @@ -1,188 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.aws_lambda import _make_request_event_processor -from sentry_sdk.traces import ( - SpanStatus, - StreamedSpan, - get_current_span, -) -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - parse_version, - reraise, -) - -try: - import chalice # type: ignore - from chalice import Chalice, ChaliceViewError - from chalice import __version__ as CHALICE_VERSION - from chalice.app import ( # type: ignore - EventSourceHandler as ChaliceEventSourceHandler, - ) -except ImportError: - raise DidNotEnable("Chalice is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, TypeVar - - F = TypeVar("F", bound=Callable[..., Any]) - - -class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore - def __call__(self, event: "Any", context: "Any") -> "Any": - client = sentry_sdk.get_client() - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor(event, context, configured_time) - ) - try: - return ChaliceEventSourceHandler.__call__(self, event, context) - except Exception: - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - client.flush() - reraise(*exc_info) - - -def _get_view_function_response( - app: "Any", view_function: "F", function_args: "Any" -) -> "F": - @wraps(view_function) - def wrapped_view_function(**function_args: "Any") -> "Any": - client = sentry_sdk.get_client() - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = app.lambda_context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor( - app.current_request.to_dict(), - app.lambda_context, - configured_time, - ) - ) - - if has_span_streaming_enabled(client.options): - current_span = get_current_span() - segment = None - if type(current_span) is StreamedSpan: - # A segment already exists (created by the AWS Lambda - # integration), so decorate it with Chalice attributes - # The AWS Lambda integration owns the span lifecycle - # (end + flush), but Chalice converts unhandled view exceptions - # into 500 responses, so the error must be captured here. - request_dict = app.current_request.to_dict() - headers = request_dict.get("headers", {}) - - header_attrs: "Dict[str, Any]" = {} - for header, value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attrs[f"http.request.header.{header.lower()}"] = value - - additional_attrs: "Dict[str, Any]" = {} - if "method" in request_dict: - additional_attrs["http.request.method"] = request_dict["method"] - - attributes = { - "sentry.origin": ChaliceIntegration.origin, - **header_attrs, - **additional_attrs, - } - - segment = current_span._segment - segment.set_attributes(attributes) - - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - if segment: - segment.status = SpanStatus.ERROR.value - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - if segment is None: - client.flush() - raise - else: - scope.set_transaction_name( - app.lambda_context.function_name, - source=TransactionSource.COMPONENT, - ) - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - client.flush() - raise - - return wrapped_view_function # type: ignore - - -class ChaliceIntegration(Integration): - identifier = "chalice" - origin = f"auto.function.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(CHALICE_VERSION) - - if version is None: - raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) - - if version < (1, 20): - old_get_view_function_response = Chalice._get_view_function_response - else: - from chalice.app import RestAPIEventHandler - - old_get_view_function_response = ( - RestAPIEventHandler._get_view_function_response - ) - - def sentry_event_response( - app: "Any", view_function: "F", function_args: "Dict[str, Any]" - ) -> "Any": - wrapped_view_function = _get_view_function_response( - app, view_function, function_args - ) - - return old_get_view_function_response( - app, wrapped_view_function, function_args - ) - - if version < (1, 20): - Chalice._get_view_function_response = sentry_event_response - else: - RestAPIEventHandler._get_view_function_response = sentry_event_response - # for everything else (like events) - chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py deleted file mode 100644 index e6b3009548..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/clickhouse_driver.py +++ /dev/null @@ -1,211 +0,0 @@ -import functools -from typing import TYPE_CHECKING, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Any, Callable, ParamSpec, Union -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - - -try: - from clickhouse_driver import VERSION # type: ignore[import-not-found] - from clickhouse_driver.client import Client # type: ignore[import-not-found] - from clickhouse_driver.connection import ( # type: ignore[import-not-found] - Connection, - ) - -except ImportError: - raise DidNotEnable("clickhouse-driver not installed.") - - -class ClickhouseDriverIntegration(Integration): - identifier = "clickhouse_driver" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(ClickhouseDriverIntegration, VERSION) - - # Every query is done using the Connection's `send_query` function - Connection.send_query = _wrap_start(Connection.send_query) - - # If the query contains parameters then the send_data function is used to send those parameters to clickhouse - _wrap_send_data() - - # Every query ends either with the Client's `receive_end_of_query` (no result expected) - # or its `receive_result` (result expected) - Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) - if hasattr(Client, "receive_end_of_insert_query"): - # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` - Client.receive_end_of_insert_query = _wrap_end( - Client.receive_end_of_insert_query - ) - Client.receive_result = _wrap_end(Client.receive_result) - - -P = ParamSpec("P") -T = TypeVar("T") - - -def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": - @functools.wraps(f) - def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(ClickhouseDriverIntegration) is None: - return f(*args, **kwargs) - - connection = args[0] - query = args[1] - query_id = args[2] if len(args) > 2 else kwargs.get("query_id") - params = args[3] if len(args) > 3 else kwargs.get("params") - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=query, # type: ignore - attributes={ - "sentry.op": OP.DB, - "sentry.origin": ClickhouseDriverIntegration.origin, - SPANDATA.DB_QUERY_TEXT: str(query), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=ClickhouseDriverIntegration.origin, - ) - - span.set_data("query", query) - - if query_id: - span.set_data("db.query_id", query_id) - - if params and should_send_default_pii(): - span.set_data("db.params", params) - - connection._sentry_span = span # type: ignore[attr-defined] - - _set_db_data(span, connection) - - # run the original code - ret = f(*args, **kwargs) - - return ret - - return _inner - - -def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": - def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": - res = f(*args, **kwargs) - instance = args[0] - span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] - - if span is None: - return res - - if isinstance(span, StreamedSpan): - span.end() - else: - if res is not None and should_send_default_pii(): - span.set_data("db.result", res) - - with capture_internal_exceptions(): - span.scope.add_breadcrumb( - message=span._data.pop("query"), category="query", data=span._data - ) - - span.finish() - - return res - - return _inner_end - - -def _wrap_send_data() -> None: - original_send_data = Client.send_data - - def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data - self, sample_block, data, types_check=False, columnar=False, *args, **kwargs - ): - span = getattr(self.connection, "_sentry_span", None) - - if isinstance(span, StreamedSpan): - _set_db_data(span, self.connection) - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - if span is not None: - _set_db_data(span, self.connection) - - if should_send_default_pii(): - db_params = span._data.get("db.params", []) - - if isinstance(data, (list, tuple)): - db_params.extend(data) - - else: # data is a generic iterator - orig_data = data - - # Wrap the generator to add items to db.params as they are yielded. - # This allows us to send the params to Sentry without needing to allocate - # memory for the entire generator at once. - def wrapped_generator() -> "Iterator[Any]": - for item in orig_data: - db_params.append(item) - yield item - - # Replace the original iterator with the wrapped one. - data = wrapped_generator() - - span.set_data("db.params", db_params) - - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - Client.send_data = _inner_send_data - - -def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") - span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) - - set_on_span = span.set_attribute - else: - span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") - span.set_data(SPANDATA.DB_NAME, connection.database) - - set_on_span = span.set_data - - set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") - set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) - set_on_span(SPANDATA.SERVER_PORT, connection.port) - set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py deleted file mode 100644 index f6285d0a9b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cloud_resource_context.py +++ /dev/null @@ -1,273 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import urllib3 - -from sentry_sdk.api import set_context -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Dict - - -CONTEXT_TYPE = "cloud_resource" - -HTTP_TIMEOUT = 2.0 - -AWS_METADATA_HOST = "169.254.169.254" -AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) -AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( - AWS_METADATA_HOST -) - -GCP_METADATA_HOST = "metadata.google.internal" -GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( - GCP_METADATA_HOST -) - - -class CLOUD_PROVIDER: # noqa: N801 - """ - Name of the cloud provider. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - ALIBABA = "alibaba_cloud" - AWS = "aws" - AZURE = "azure" - GCP = "gcp" - IBM = "ibm_cloud" - TENCENT = "tencent_cloud" - - -class CLOUD_PLATFORM: # noqa: N801 - """ - The cloud platform. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - AWS_EC2 = "aws_ec2" - AWS_LAMBDA = "aws_lambda" - GCP_COMPUTE_ENGINE = "gcp_compute_engine" - - -class CloudResourceContextIntegration(Integration): - """ - Adds cloud resource context to the Senty scope - """ - - identifier = "cloudresourcecontext" - - cloud_provider = "" - - aws_token = "" - http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) - - gcp_metadata = None - - def __init__(self, cloud_provider: str = "") -> None: - CloudResourceContextIntegration.cloud_provider = cloud_provider - - @classmethod - def _is_aws(cls) -> bool: - try: - r = cls.http.request( - "PUT", - AWS_TOKEN_URL, - headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, - ) - - if r.status != 200: - return False - - cls.aws_token = r.data.decode() - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking AWS metadata service: %s", str(e)) - return False - - @classmethod - def _get_aws_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.AWS, - "cloud.platform": CLOUD_PLATFORM.AWS_EC2, - } - - try: - r = cls.http.request( - "GET", - AWS_METADATA_URL, - headers={"X-aws-ec2-metadata-token": cls.aws_token}, - ) - - if r.status != 200: - return ctx - - data = json.loads(r.data.decode("utf-8")) - - try: - ctx["cloud.account.id"] = data["accountId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = data["availabilityZone"] - except Exception: - pass - - try: - ctx["cloud.region"] = data["region"] - except Exception: - pass - - try: - ctx["host.id"] = data["instanceId"] - except Exception: - pass - - try: - ctx["host.type"] = data["instanceType"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching AWS metadata: %s", str(e)) - - return ctx - - @classmethod - def _is_gcp(cls) -> bool: - try: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return False - - cls.gcp_metadata = json.loads(r.data.decode("utf-8")) - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking GCP metadata service: %s", str(e)) - return False - - @classmethod - def _get_gcp_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.GCP, - "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, - } - - gcp_metadata = cls.gcp_metadata - try: - if cls.gcp_metadata is None: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return ctx - - gcp_metadata = json.loads(r.data.decode("utf-8")) - cls.gcp_metadata = gcp_metadata - - try: - ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( - "/" - )[-1] - except Exception: - pass - - try: - # only populated in google cloud run - ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] - except Exception: - pass - - try: - ctx["host.id"] = gcp_metadata["instance"]["id"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching GCP metadata: %s", str(e)) - - return ctx - - @classmethod - def _get_cloud_provider(cls) -> str: - if cls._is_aws(): - return CLOUD_PROVIDER.AWS - - if cls._is_gcp(): - return CLOUD_PROVIDER.GCP - - return "" - - @classmethod - def _get_cloud_resource_context(cls) -> "Dict[str, str]": - cloud_provider = ( - cls.cloud_provider - if cls.cloud_provider != "" - else CloudResourceContextIntegration._get_cloud_provider() - ) - if cloud_provider in context_getters.keys(): - return context_getters[cloud_provider]() - - return {} - - @staticmethod - def setup_once() -> None: - cloud_provider = CloudResourceContextIntegration.cloud_provider - unsupported_cloud_provider = ( - cloud_provider != "" and cloud_provider not in context_getters.keys() - ) - - if unsupported_cloud_provider: - logger.warning( - "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", - CloudResourceContextIntegration.cloud_provider, - list(context_getters.keys()), - ) - - context = CloudResourceContextIntegration._get_cloud_resource_context() - if context != {}: - set_context(CONTEXT_TYPE, context) - - -# Map with the currently supported cloud providers -# mapping to functions extracting the context -context_getters = { - CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, - CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, -} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py deleted file mode 100644 index 7abf3f6808..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/cohere.py +++ /dev/null @@ -1,303 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise - -try: - from cohere import ( - ChatStreamEndEvent, - NonStreamedChatResponse, - ) - from cohere.base_client import BaseCohere - from cohere.client import Client - - if TYPE_CHECKING: - from cohere import StreamedChatResponse -except ImportError: - raise DidNotEnable("Cohere not installed") - -try: - # cohere 5.9.3+ - from cohere import StreamEndStreamedChatResponse -except ImportError: - from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse - - -COLLECTED_CHAT_PARAMS = { - "model": SPANDATA.AI_MODEL_ID, - "k": SPANDATA.AI_TOP_K, - "p": SPANDATA.AI_TOP_P, - "seed": SPANDATA.AI_SEED, - "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, - "raw_prompting": SPANDATA.AI_RAW_PROMPTING, -} - -COLLECTED_PII_CHAT_PARAMS = { - "tools": SPANDATA.AI_TOOLS, - "preamble": SPANDATA.AI_PREAMBLE, -} - -COLLECTED_CHAT_RESP_ATTRS = { - "generation_id": SPANDATA.AI_GENERATION_ID, - "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, - "finish_reason": SPANDATA.AI_FINISH_REASON, -} - -COLLECTED_PII_CHAT_RESP_ATTRS = { - "citations": SPANDATA.AI_CITATIONS, - "documents": SPANDATA.AI_DOCUMENTS, - "search_queries": SPANDATA.AI_SEARCH_QUERIES, - "search_results": SPANDATA.AI_SEARCH_RESULTS, - "tool_calls": SPANDATA.AI_TOOL_CALLS, -} - - -class CohereIntegration(Integration): - identifier = "cohere" - origin = f"auto.ai.{identifier}" - - def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) - Client.embed = _wrap_embed(Client.embed) - BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "cohere", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _end_span(span: "Any") -> None: - if isinstance(span, StreamedSpan): - span.end() - else: - span.__exit__(None, None, None) - - -def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": - def collect_chat_response_fields( - span: "Union[Span, StreamedSpan]", - res: "NonStreamedChatResponse", - include_pii: bool, - ) -> None: - if include_pii: - if hasattr(res, "text"): - set_data_normalized( - span, - SPANDATA.AI_RESPONSES, - [res.text], - ) - for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: - if hasattr(res, pii_attr): - set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) - - for attr in COLLECTED_CHAT_RESP_ATTRS: - if hasattr(res, attr): - set_data_normalized(span, "ai." + attr, getattr(res, attr)) - - if hasattr(res, "meta"): - if hasattr(res.meta, "billed_units"): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - output_tokens=res.meta.billed_units.output_tokens, - ) - elif hasattr(res.meta, "tokens"): - record_token_usage( - span, - input_tokens=res.meta.tokens.input_tokens, - output_tokens=res.meta.tokens.output_tokens, - ) - - if hasattr(res.meta, "warnings"): - set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) - - @wraps(f) - def new_chat(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if ( - integration is None - or "message" not in kwargs - or not isinstance(kwargs.get("message"), str) - ): - return f(*args, **kwargs) - - message = kwargs.get("message") - - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name="cohere.client.Chat", - attributes={ - "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - name="cohere.client.Chat", - origin=CohereIntegration.origin, - ) - span.__enter__() - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - - reraise(*exc_info) - - with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, - SPANDATA.AI_INPUT_MESSAGES, - list( - map( - lambda x: { - "role": getattr(x, "role", "").lower(), - "content": getattr(x, "message", ""), - }, - kwargs.get("chat_history", []), - ) - ) - + [{"role": "user", "content": message}], - ) - for k, v in COLLECTED_PII_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - - for k, v in COLLECTED_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - set_data_normalized(span, SPANDATA.AI_STREAMING, False) - - if streaming: - old_iterator = res - - def new_iterator() -> "Iterator[StreamedChatResponse]": - with capture_internal_exceptions(): - for x in old_iterator: - if isinstance(x, ChatStreamEndEvent) or isinstance( - x, StreamEndStreamedChatResponse - ): - collect_chat_response_fields( - span, - x.response, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - yield x - _end_span(span) - - return new_iterator() - elif isinstance(res, NonStreamedChatResponse): - collect_chat_response_fields( - span, - res, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - _end_span(span) - else: - set_data_normalized(span, "unknown_response", True) - _end_span(span) - return res - - return new_chat - - -def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - if integration is None: - return f(*args, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name="Cohere Embedding Creation", - attributes={ - "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span_ctx = get_start_span_function()( - op=consts.OP.COHERE_EMBEDDINGS_CREATE, - name="Cohere Embedding Creation", - origin=CohereIntegration.origin, - ) - - with span_ctx as span: - if "texts" in kwargs and ( - should_send_default_pii() and integration.include_prompts - ): - if isinstance(kwargs["texts"], str): - set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) - elif ( - isinstance(kwargs["texts"], list) - and len(kwargs["texts"]) > 0 - and isinstance(kwargs["texts"][0], str) - ): - set_data_normalized( - span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] - ) - - if "model" in kwargs: - set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - reraise(*exc_info) - if ( - hasattr(res, "meta") - and hasattr(res.meta, "billed_units") - and hasattr(res.meta.billed_units, "input_tokens") - ): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - total_tokens=res.meta.billed_units.input_tokens, - ) - return res - - return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py deleted file mode 100644 index a0e9014666..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dedupe.py +++ /dev/null @@ -1,62 +0,0 @@ -import weakref -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class DedupeIntegration(Integration): - identifier = "dedupe" - - def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if hint is None: - return event - - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return event - - exc_info = hint.get("exc_info", None) - if exc_info is None: - return event - - last_seen = integration._last_seen.get(None) - if last_seen is not None: - # last_seen is either a weakref or the original instance - last_seen = ( - last_seen() if isinstance(last_seen, weakref.ref) else last_seen - ) - - exc = exc_info[1] - if last_seen is exc: - logger.info("DedupeIntegration dropped duplicated error event %s", exc) - return None - - # we can only weakref non builtin types - try: - integration._last_seen.set(weakref.ref(exc)) - except TypeError: - integration._last_seen.set(exc) - - return event - - @staticmethod - def reset_last_seen() -> None: - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return - - integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py deleted file mode 100644 index 361b60079d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/__init__.py +++ /dev/null @@ -1,884 +0,0 @@ -import inspect -import sys -import threading -import weakref -from importlib import import_module - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANNAME -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - transaction_from_function, - walk_exception_chain, -) - -try: - from django import VERSION as DJANGO_VERSION - from django.conf import settings - from django.conf import settings as django_settings - from django.core import signals - from django.utils.functional import SimpleLazyObject - - try: - from django.urls import resolve - except ImportError: - from django.core.urlresolvers import resolve - - try: - from django.urls import Resolver404 - except ImportError: - from django.core.urlresolvers import Resolver404 - - # Only available in Django 3.0+ - try: - from django.core.handlers.asgi import ASGIRequest - except Exception: - ASGIRequest = None - -except ImportError: - raise DidNotEnable("Django not installed") - -from sentry_sdk.integrations.django.middleware import patch_django_middlewares -from sentry_sdk.integrations.django.signals_handlers import patch_signals -from sentry_sdk.integrations.django.tasks import patch_tasks -from sentry_sdk.integrations.django.templates import ( - get_template_frame_from_exception, - patch_templates, -) -from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER -from sentry_sdk.integrations.django.views import patch_views - -if DJANGO_VERSION[:2] > (1, 8): - from sentry_sdk.integrations.django.caching import patch_caching -else: - patch_caching = None # type: ignore - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from django.core.handlers.wsgi import WSGIRequest - from django.http.request import QueryDict - from django.http.response import HttpResponse - from django.utils.datastructures import MultiValueDict - - from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -if DJANGO_VERSION < (1, 10): - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated() - -else: - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated - - -TRANSACTION_STYLE_VALUES = ("function_name", "url") - - -class DjangoIntegration(Integration): - """ - Auto instrument a Django application. - - :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. - :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. - :param signals_spans: Whether to create spans for signals. Defaults to `True`. - :param signals_denylist: A list of signals to ignore when creating spans. - :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. - """ - - identifier = "django" - origin = f"auto.http.{identifier}" - origin_db = f"auto.db.{identifier}" - - transaction_style = "" - middleware_spans: "Optional[bool]" = None - signals_spans: "Optional[bool]" = None - cache_spans: "Optional[bool]" = None - signals_denylist: "list[signals.Signal]" = [] - - def __init__( - self, - transaction_style: str = "url", - middleware_spans: bool = False, - signals_spans: bool = True, - cache_spans: bool = False, - db_transaction_spans: bool = False, - signals_denylist: "Optional[list[signals.Signal]]" = None, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - - self.signals_spans = signals_spans - self.signals_denylist = signals_denylist or [] - - self.cache_spans = cache_spans - self.db_transaction_spans = db_transaction_spans - - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(DjangoIntegration, DJANGO_VERSION) - - install_sql_hook() - # Patch in our custom middleware. - - # logs an error for every 500 - ignore_logger("django.server") - ignore_logger("django.request") - - from django.core.handlers.wsgi import WSGIHandler - - old_app = WSGIHandler.__call__ - - @ensure_integration_enabled(DjangoIntegration, old_app) - def sentry_patched_wsgi_handler( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - bound_old_app = old_app.__get__(self, WSGIHandler) - - from django.conf import settings - - use_x_forwarded_for = settings.USE_X_FORWARDED_HOST - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - middleware = SentryWsgiMiddleware( - bound_old_app, - use_x_forwarded_for, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - WSGIHandler.__call__ = sentry_patched_wsgi_handler - - _patch_get_response() - - _patch_django_asgi_handler() - - signals.got_request_exception.connect(_got_request_exception) - - @add_global_event_processor - def process_django_templates( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_, exc_value, _) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - frame = get_template_frame_from_exception(exc_value) - if frame is not None: - frames = exception.get("stacktrace", {}).get("frames", []) - - for i in reversed(range(len(frames))): - f = frames[i] - if ( - f.get("function") in ("Parser.parse", "parse", "render") - and f.get("module") == "django.template.base" - ): - i += 1 - break - else: - i = len(frames) - - frames.insert(i, frame) - - return event - - @add_global_repr_processor - def _django_queryset_repr( - value: "Any", hint: "Dict[str, Any]" - ) -> "Union[NotImplementedType, str]": - try: - # Django 1.6 can fail to import `QuerySet` when Django settings - # have not yet been initialized. - # - # If we fail to import, return `NotImplemented`. It's at least - # unlikely that we have a query set in `value` when importing - # `QuerySet` fails. - from django.db.models.query import QuerySet - except Exception: - return NotImplemented - - if not isinstance(value, QuerySet) or value._result_cache: - return NotImplemented - - return "<%s from %s at 0x%x>" % ( - value.__class__.__name__, - value.__module__, - id(value), - ) - - _patch_channels() - patch_django_middlewares() - patch_views() - patch_templates() - patch_signals() - patch_tasks() - add_template_context_repr_sequence() - - if patch_caching is not None: - patch_caching() - - -_DRF_PATCHED = False -_DRF_PATCH_LOCK = threading.Lock() - - -def _patch_drf() -> None: - """ - Patch Django Rest Framework for more/better request data. DRF's request - type is a wrapper around Django's request type. The attribute we're - interested in is `request.data`, which is a cached property containing a - parsed request body. Reading a request body from that property is more - reliable than reading from any of Django's own properties, as those don't - hold payloads in memory and therefore can only be accessed once. - - We patch the Django request object to include a weak backreference to the - DRF request object, such that we can later use either in - `DjangoRequestExtractor`. - - This function is not called directly on SDK setup, because importing almost - any part of Django Rest Framework will try to access Django settings (where - `sentry_sdk.init()` might be called from in the first place). Instead we - run this function on every request and do the patching on the first - request. - """ - - global _DRF_PATCHED - - if _DRF_PATCHED: - # Double-checked locking - return - - with _DRF_PATCH_LOCK: - if _DRF_PATCHED: - return - - # We set this regardless of whether the code below succeeds or fails. - # There is no point in trying to patch again on the next request. - _DRF_PATCHED = True - - with capture_internal_exceptions(): - try: - from rest_framework.views import APIView # type: ignore - except ImportError: - pass - else: - old_drf_initial = APIView.initial - - def sentry_patched_drf_initial( - self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - with capture_internal_exceptions(): - request._request._sentry_drf_request_backref = weakref.ref( - request - ) - pass - return old_drf_initial(self, request, *args, **kwargs) - - APIView.initial = sentry_patched_drf_initial - - -def _patch_channels() -> None: - try: - from channels.http import AsgiHandler # type: ignore - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl - - patch_channels_asgi_handler_impl(AsgiHandler) - - -def _patch_django_asgi_handler() -> None: - try: - from django.core.handlers.asgi import ASGIHandler - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl - - patch_django_asgi_handler_impl(ASGIHandler) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" -) -> None: - try: - transaction_name = None - if transaction_style == "function_name": - fn = resolve(request.path).func - transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) - - elif transaction_style == "url": - if hasattr(request, "urlconf"): - transaction_name = LEGACY_RESOLVER.resolve( - request.path_info, urlconf=request.urlconf - ) - else: - transaction_name = LEGACY_RESOLVER.resolve(request.path_info) - - if transaction_name is None: - transaction_name = request.path_info - source = TransactionSource.URL - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name( - transaction_name, - source=source, - ) - except Resolver404: - urlconf = import_module(settings.ROOT_URLCONF) - # This exception only gets thrown when transaction_style is `function_name` - # So we don't check here what style is configured - if hasattr(urlconf, "handler404"): - handler = urlconf.handler404 - if isinstance(handler, str): - scope.transaction = handler - else: - scope.transaction = transaction_from_function( - getattr(handler, "view_class", handler) - ) - except Exception: - pass - - -def _before_get_response(request: "WSGIRequest") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return - - _patch_drf() - - scope = sentry_sdk.get_current_scope() - # Rely on WSGI middleware to start a trace - _set_transaction_name_and_source(scope, integration.transaction_style, request) - - scope.add_event_processor( - _make_wsgi_request_event_processor(weakref.ref(request), integration) - ) - - -def _attempt_resolve_again( - request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str -) -> None: - """ - Some django middlewares overwrite request.urlconf - so we need to respect that contract, - so we try to resolve the url again. - """ - if not hasattr(request, "urlconf"): - return - - _set_transaction_name_and_source(scope, transaction_style, request) - - -def _after_get_response(request: "WSGIRequest") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming and should_send_default_pii(): - user = getattr(request, "user", None) - - # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. - # Exit early if the user has not been materialized yet. - is_lazy = isinstance(user, SimpleLazyObject) - if is_lazy and hasattr(request, "_cached_user"): - user = request._cached_user - elif is_lazy: - return - - if user is None or not is_authenticated(user): - return - - user_info = {} - try: - user_info["id"] = str(user.pk) - except Exception: - pass - - try: - user_info["email"] = user.email - except Exception: - pass - - try: - user_info["username"] = user.get_username() - except Exception: - pass - - sentry_sdk.set_user(user_info) - - -def _patch_get_response() -> None: - """ - patch get_response, because at that point we have the Django request object - """ - from django.core.handlers.base import BaseHandler - - old_get_response = BaseHandler.get_response - - def sentry_patched_get_response( - self: "Any", request: "WSGIRequest" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - rv = old_get_response(self, request) - _after_get_response(request) - return rv - - BaseHandler.get_response = sentry_patched_get_response - - if hasattr(BaseHandler, "get_response_async"): - from sentry_sdk.integrations.django.asgi import patch_get_response_async - - patch_get_response_async(BaseHandler, _before_get_response) - - -def _make_wsgi_request_event_processor( - weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" -) -> "EventProcessor": - def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - request = weak_request() - if request is None: - return event - - django_3 = ASGIRequest is not None - if django_3 and type(request) == ASGIRequest: - # We have a `asgi_request_event_processor` for this. - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return wsgi_request_event_processor - - -def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if request is not None and integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - event, hint = event_from_exception( - sys.exc_info(), - client_options=client.options, - mechanism={"type": "django", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class DjangoRequestExtractor(RequestExtractor): - def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: - try: - drf_request = request._sentry_drf_request_backref() - if drf_request is not None: - request = drf_request - except AttributeError: - pass - self.request = request - - def env(self) -> "Dict[str, str]": - return self.request.META - - def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": - privacy_cookies = [ - django_settings.CSRF_COOKIE_NAME, - django_settings.SESSION_COOKIE_NAME, - ] - - clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} - for key, val in self.request.COOKIES.items(): - if key in privacy_cookies: - clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE - else: - clean_cookies[key] = val - - return clean_cookies - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "QueryDict": - return self.request.POST - - def files(self) -> "MultiValueDict": - return self.request.FILES - - def size_of_file(self, file: "Any") -> int: - return file.size - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - return self.request.data - except Exception: - return RequestExtractor.parsed_body(self) - - -def _set_user_info(request: "WSGIRequest", event: "Event") -> None: - user_info = event.setdefault("user", {}) - - user = getattr(request, "user", None) - - if user is None or not is_authenticated(user): - return - - try: - user_info.setdefault("id", str(user.pk)) - except Exception: - pass - - try: - user_info.setdefault("email", user.email) - except Exception: - pass - - try: - user_info.setdefault("username", user.get_username()) - except Exception: - pass - - -def install_sql_hook() -> None: - """If installed this causes Django's queries to be captured.""" - try: - from django.db.backends.utils import CursorWrapper - except ImportError: - from django.db.backends.util import CursorWrapper - - try: - # django 1.6 and 1.7 compatability - from django.db.backends import BaseDatabaseWrapper - except ImportError: - # django 1.8 or later - from django.db.backends.base.base import BaseDatabaseWrapper - - try: - real_execute = CursorWrapper.execute - real_executemany = CursorWrapper.executemany - real_connect = BaseDatabaseWrapper.connect - real_commit = BaseDatabaseWrapper._commit - real_rollback = BaseDatabaseWrapper._rollback - except AttributeError: - # This won't work on Django versions < 1.6 - return - - @ensure_integration_enabled(DjangoIntegration, real_execute) - def execute( - self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=params, - paramstyle="format", - executemany=False, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - result = real_execute(self, sql, params) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_executemany) - def executemany( - self: "CursorWrapper", sql: "Any", param_list: "List[Any]" - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=param_list, - paramstyle="format", - executemany=True, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - - result = real_executemany(self, sql, param_list) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_connect) - def connect(self: "BaseDatabaseWrapper") -> None: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message="connect", category="query") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="connect", - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self) - return real_connect(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - return real_connect(self) - - def _commit(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_commit(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_COMMIT, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_COMMIT, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - - def _rollback(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_rollback(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_ROLLBACK, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_ROLLBACK, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - - CursorWrapper.execute = execute - CursorWrapper.executemany = executemany - BaseDatabaseWrapper.connect = connect - BaseDatabaseWrapper._commit = _commit - BaseDatabaseWrapper._rollback = _rollback - ignore_logger("django.db.backends") - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", - cursor_or_db: "Any", - db_operation: "Optional[str]" = None, -) -> None: - db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db - vendor = db.vendor - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) - - if db_operation is not None: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) - else: - span.set_data(SPANDATA.DB_SYSTEM, vendor) - - if db_operation is not None: - span.set_data(SPANDATA.DB_OPERATION, db_operation) - - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` - # actually has a `connection` and the `connection` has a `get_dsn_parameters` - # attribute, only to throw an error once you actually want to call it. - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable - # function. - is_psycopg2 = ( - hasattr(cursor_or_db, "connection") - and hasattr(cursor_or_db.connection, "get_dsn_parameters") - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) - ) - if is_psycopg2: - connection_params = cursor_or_db.connection.get_dsn_parameters() - else: - try: - # psycopg3, only extract needed params as get_parameters - # can be slow because of the additional logic to filter out default - # values - connection_params = { - "dbname": cursor_or_db.connection.info.dbname, - "port": cursor_or_db.connection.info.port, - } - # PGhost returns host or base dir of UNIX socket as an absolute path - # starting with /, use it only when it contains host - pg_host = cursor_or_db.connection.info.host - if pg_host and not pg_host.startswith("/"): - connection_params["host"] = pg_host - except Exception: - connection_params = db.get_connection_params() - - db_name = connection_params.get("dbname") or connection_params.get("database") - - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - - set_on_span = span.set_attribute - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - set_on_span = span.set_data - - server_address = connection_params.get("host") - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = connection_params.get("port") - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, str(server_port)) - - server_socket_address = connection_params.get("unix_socket") - if server_socket_address is not None: - set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) - - -def add_template_context_repr_sequence() -> None: - try: - from django.template.context import BaseContext - - add_repr_sequence_type(BaseContext) - except Exception: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py deleted file mode 100644 index 43faffb5be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/asgi.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Instrumentation for Django 3.0 - -Since this file contains `async def` it is conditionally imported in -`sentry_sdk.integrations.django` (depending on the existence of -`django.core.handlers.asgi`. -""" - -import asyncio -import functools -import inspect -from typing import TYPE_CHECKING - -from django.core.handlers.wsgi import WSGIRequest - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, TypeVar, Union - - from django.core.handlers.asgi import ASGIRequest - from django.http.response import HttpResponse - - from sentry_sdk._types import Event, EventProcessor - - _F = TypeVar("_F", bound=Callable[..., Any]) - - -# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for -# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. -# The latter is replaced with the inspect.markcoroutinefunction decorator. -# Until 3.12 is the minimum supported Python version, provide a shim. -# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py -if hasattr(inspect, "markcoroutinefunction"): - iscoroutinefunction = inspect.iscoroutinefunction - markcoroutinefunction = inspect.markcoroutinefunction -else: - iscoroutinefunction = asyncio.iscoroutinefunction - - def markcoroutinefunction(func: "_F") -> "_F": - func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore - return func - - -def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": - def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - from sentry_sdk.integrations.django import ( - DjangoRequestExtractor, - _set_user_info, - ) - - if request is None: - return event - - if type(request) == WSGIRequest: - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return asgi_request_event_processor - - -def patch_django_asgi_handler_impl(cls: "Any") -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - )._run_asgi3 - - return await middleware(scope, receive, send) - - cls.__call__ = sentry_patched_asgi_handler - - modern_django_asgi_support = hasattr(cls, "create_request") - if modern_django_asgi_support: - old_create_request = cls.create_request - - @ensure_integration_enabled(DjangoIntegration, old_create_request) - def sentry_patched_create_request( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request, error_response = old_create_request(self, *args, **kwargs) - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_make_asgi_request_event_processor(request)) - - return request, error_response - - cls.create_request = sentry_patched_create_request - - -def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: - old_get_response_async = cls.get_response_async - - async def sentry_patched_get_response_async( - self: "Any", request: "Any" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - return await old_get_response_async(self, request) - - cls.get_response_async = sentry_patched_get_response_async - - -def patch_channels_asgi_handler_impl(cls: "Any") -> None: - import channels # type: ignore - - from sentry_sdk.integrations.django import DjangoIntegration - - if channels.__version__ < "3.0.0": - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, receive, send) - - middleware = SentryAsgiMiddleware( - lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - ) - - return await middleware(self.scope)(receive, send) # type: ignore - - cls.__call__ = sentry_patched_asgi_handler - - else: - # The ASGI handler in Channels >= 3 has the same signature as - # the Django handler. - patch_django_asgi_handler_impl(cls) - - -def wrap_async_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - async def sentry_wrapped_callback( - request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return await callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return await callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return await callback(request, *args, **kwargs) - - return sentry_wrapped_callback - - -def _asgi_middleware_mixin_factory( - _check_middleware_span: "Callable[..., Any]", -) -> "Any": - """ - Mixin class factory that generates a middleware mixin for handling requests - in async mode. - """ - - class SentryASGIMixin: - if TYPE_CHECKING: - _inner = None - - def __init__(self, get_response: "Callable[..., Any]") -> None: - self.get_response = get_response - self._acall_method = None - self._async_check() - - def _async_check(self) -> None: - """ - If get_response is a coroutine function, turns us into async mode so - a thread is not consumed during a whole request. - Taken from django.utils.deprecation::MiddlewareMixin._async_check - """ - if iscoroutinefunction(self.get_response): - markcoroutinefunction(self) - - def async_route_check(self) -> bool: - """ - Function that checks if we are in async mode, - and if we are forwards the handling of requests to __acall__ - """ - return iscoroutinefunction(self.get_response) - - async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": - f = self._acall_method - if f is None: - if hasattr(self._inner, "__acall__"): - self._acall_method = f = self._inner.__acall__ # type: ignore - else: - self._acall_method = f = self._inner - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return await f(*args, **kwargs) # type: ignore - - with middleware_span: - return await f(*args, **kwargs) # type: ignore - - return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py deleted file mode 100644 index faf1803c11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/caching.py +++ /dev/null @@ -1,264 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.core.cache import CacheHandler -from urllib3.util import parse_url as urlparse - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - -METHODS_TO_INSTRUMENT = [ - "set", - "set_many", - "get", - "get_many", -] - - -def _get_span_description( - method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" -) -> str: - return _key_as_string(_get_safe_key(method_name, args, kwargs)) - - -def _patch_cache_method( - cache: "CacheHandler", - method_name: str, - address: "Optional[str]", - port: "Optional[int]", -) -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - original_method = getattr(cache, method_name) - - @ensure_integration_enabled(DjangoIntegration, original_method) - def _instrument_call( - cache: "CacheHandler", - method_name: str, - original_method: "Callable[..., Any]", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - address: "Optional[str]", - port: "Optional[int]", - ) -> "Any": - is_set_operation = method_name.startswith("set") - is_get_method = method_name == "get" - is_get_many_method = method_name == "get_many" - - op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET - description = _get_span_description(method_name, args, kwargs) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": op, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_attribute(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - else: - with sentry_sdk.start_span( - op=op, - name=description, - origin=DjangoIntegration.origin, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_data(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_data(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - - @functools.wraps(original_method) - def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": - return _instrument_call( - cache, method_name, original_method, args, kwargs, address, port - ) - - setattr(cache, method_name, sentry_method) - - -def _patch_cache( - cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None -) -> None: - if not hasattr(cache, "_sentry_patched"): - for method_name in METHODS_TO_INSTRUMENT: - _patch_cache_method(cache, method_name, address, port) - cache._sentry_patched = True - - -def _get_address_port( - settings: "dict[str, Any]", -) -> "tuple[Optional[str], Optional[int]]": - location = settings.get("LOCATION") - - # TODO: location can also be an array of locations - # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis - # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 - if not isinstance(location, str): - return None, None - - if "://" in location: - parsed_url = urlparse(location) - # remove the username and password from URL to not leak sensitive data. - address = "{}://{}{}".format( - parsed_url.scheme or "", - parsed_url.hostname or "", - parsed_url.path or "", - ) - port = parsed_url.port - else: - address = location - port = None - - return address, int(port) if port is not None else None - - -def should_enable_cache_spans() -> bool: - from sentry_sdk.integrations.django import DjangoIntegration - - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - from django.conf import settings - - return integration is not None and ( - (client.spotlight is not None and settings.DEBUG is True) - or integration.cache_spans is True - ) - - -def patch_caching() -> None: - if not hasattr(CacheHandler, "_sentry_patched"): - if DJANGO_VERSION < (3, 2): - original_get_item = CacheHandler.__getitem__ - - @functools.wraps(original_get_item) - def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": - cache = original_get_item(self, alias) - - if should_enable_cache_spans(): - from django.conf import settings - - address, port = _get_address_port( - settings.CACHES[alias or "default"] - ) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.__getitem__ = sentry_get_item - CacheHandler._sentry_patched = True - - else: - original_create_connection = CacheHandler.create_connection - - @functools.wraps(original_create_connection) - def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": - cache = original_create_connection(self, alias) - - if should_enable_cache_spans(): - address, port = _get_address_port(self.settings[alias or "default"]) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.create_connection = sentry_create_connection - CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py deleted file mode 100644 index a14ec96ff5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/middleware.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Create spans from Django middleware invocations -""" - -from functools import wraps -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - F = TypeVar("F", bound=Callable[..., Any]) - -_import_string_should_wrap_middleware = ContextVar( - "import_string_should_wrap_middleware" -) - -DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) - -if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: - _asgi_middleware_mixin_factory = lambda _: object - iscoroutinefunction = lambda _: False -else: - from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction - - -def patch_django_middlewares() -> None: - from django.core.handlers import base - - old_import_string = base.import_string - - def sentry_patched_import_string(dotted_path: str) -> "Any": - rv = old_import_string(dotted_path) - - if _import_string_should_wrap_middleware.get(None): - rv = _wrap_middleware(rv, dotted_path) - - return rv - - base.import_string = sentry_patched_import_string - - old_load_middleware = base.BaseHandler.load_middleware - - def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": - _import_string_should_wrap_middleware.set(True) - try: - return old_load_middleware(*args, **kwargs) - finally: - _import_string_should_wrap_middleware.set(False) - - base.BaseHandler.load_middleware = sentry_patched_load_middleware - - -def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - def _check_middleware_span( - old_method: "Callable[..., Any]", - ) -> "Optional[Union[Span, StreamedSpan]]": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None or not integration.middleware_spans: - return None - - function_name = transaction_from_function(old_method) - - description = middleware_name - function_basename = getattr(old_method, "__name__", None) - if function_basename: - description = "{}.{}".format(description, function_basename) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - middleware_span: "Union[Span, StreamedSpan]" - if span_streaming: - middleware_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.MIDDLEWARE_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - else: - middleware_span = sentry_sdk.start_span( - op=OP.MIDDLEWARE_DJANGO, - name=description, - origin=DjangoIntegration.origin, - ) - middleware_span.set_tag("django.function_name", function_name) - middleware_span.set_tag("django.middleware_name", middleware_name) - - return middleware_span - - def _get_wrapped_method(old_method: "F") -> "F": - with capture_internal_exceptions(): - # Middleware hooks (e.g. `process_view`, `process_exception`) may be - # `async def` when the middleware is async. A synchronous wrapper - # would hide the coroutine from Django's `iscoroutinefunction` check, - # causing Django to call the hook synchronously and never await the - # returned coroutine. Wrap async hooks with an async wrapper so the - # wrapped method continues to report as a coroutine function. - if iscoroutinefunction is not None and iscoroutinefunction(old_method): - - async def async_sentry_wrapped_method( - *args: "Any", **kwargs: "Any" - ) -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return await old_method(*args, **kwargs) - - with middleware_span: - return await old_method(*args, **kwargs) - - sentry_wrapped_method = async_sentry_wrapped_method - - else: - - def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return old_method(*args, **kwargs) - - with middleware_span: - return old_method(*args, **kwargs) - - sentry_wrapped_method = sync_sentry_wrapped_method - - try: - # fails for __call__ of function on Python 2 (see py2.7-django-1.11) - sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) - - # Necessary for Django 3.1 - sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore - except Exception: - pass - - return sentry_wrapped_method # type: ignore - - return old_method - - class SentryWrappingMiddleware( - _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore - ): - sync_capable = getattr(middleware, "sync_capable", True) - async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( - middleware, "async_capable", False - ) - - def __init__( - self, - get_response: "Optional[Callable[..., Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> None: - if get_response: - self._inner = middleware(get_response, *args, **kwargs) - else: - self._inner = middleware(*args, **kwargs) - self.get_response = get_response - self._call_method = None - if self.async_capable: - super().__init__(get_response) - - # We need correct behavior for `hasattr()`, which we can only determine - # when we have an instance of the middleware we're wrapping. - def __getattr__(self, method_name: str) -> "Any": - if method_name not in ( - "process_request", - "process_view", - "process_template_response", - "process_response", - "process_exception", - ): - raise AttributeError() - - old_method = getattr(self._inner, method_name) - rv = _get_wrapped_method(old_method) - self.__dict__[method_name] = rv - return rv - - def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": - if hasattr(self, "async_route_check") and self.async_route_check(): - return self.__acall__(*args, **kwargs) - - f = self._call_method - if f is None: - self._call_method = f = self._inner.__call__ - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return f(*args, **kwargs) - - with middleware_span: - return f(*args, **kwargs) - - for attr in ( - "__name__", - "__module__", - "__qualname__", - ): - if hasattr(middleware, attr): - setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) - - return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py deleted file mode 100644 index 7140ead782..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/signals_handlers.py +++ /dev/null @@ -1,105 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from django.dispatch import Signal - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.django import DJANGO_VERSION -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Union - - -def _get_receiver_name(receiver: "Callable[..., Any]") -> str: - name = "" - - if hasattr(receiver, "__qualname__"): - name = receiver.__qualname__ - elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ - name = receiver.__name__ - elif hasattr( - receiver, "func" - ): # certain functions (like partials) dont have a name - if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): - name = "partial()" - - if ( - name == "" - ): # In case nothing was found, return the string representation (this is the slowest case) - return str(receiver) - - if hasattr(receiver, "__module__"): # prepend with module, if there is one - name = receiver.__module__ + "." + name - - return name - - -def patch_signals() -> None: - """ - Patch django signal receivers to create a span. - - This only wraps sync receivers. Django>=5.0 introduced async receivers, but - since we don't create transactions for ASGI Django, we don't wrap them. - """ - from sentry_sdk.integrations.django import DjangoIntegration - - old_live_receivers = Signal._live_receivers - - def _sentry_live_receivers( - self: "Signal", sender: "Any" - ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": - if DJANGO_VERSION >= (5, 0): - sync_receivers, async_receivers = old_live_receivers(self, sender) - else: - sync_receivers = old_live_receivers(self, sender) - async_receivers = [] - - def sentry_sync_receiver_wrapper( - receiver: "Callable[..., Any]", - ) -> "Callable[..., Any]": - @wraps(receiver) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - signal_name = _get_receiver_name(receiver) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - with sentry_sdk.traces.start_span( - name=signal_name, - attributes={ - "sentry.op": OP.EVENT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.CODE_FUNCTION_NAME: signal_name, - }, - ) as span: - return receiver(*args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.EVENT_DJANGO, - name=signal_name, - origin=DjangoIntegration.origin, - ) as span: - span.set_data("signal", signal_name) - return receiver(*args, **kwargs) - - return wrapper - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if ( - integration - and integration.signals_spans - and self not in integration.signals_denylist - ): - for idx, receiver in enumerate(sync_receivers): - sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) - - if DJANGO_VERSION >= (5, 0): - return sync_receivers, async_receivers - else: - return sync_receivers - - Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py deleted file mode 100644 index 5e23c258fb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/tasks.py +++ /dev/null @@ -1,52 +0,0 @@ -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import qualname_from_function - -try: - # django.tasks were added in Django 6.0 - from django.tasks.base import Task -except ImportError: - Task = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -def patch_tasks() -> None: - if Task is None: - return - - old_task_enqueue = Task.enqueue - - @wraps(old_task_enqueue) - def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return old_task_enqueue(self, *args, **kwargs) - - name = qualname_from_function(self.func) or "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_task_enqueue(self, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin - ): - return old_task_enqueue(self, *args, **kwargs) - - Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py deleted file mode 100644 index 5ab89d4a74..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/templates.py +++ /dev/null @@ -1,209 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.template import TemplateSyntaxError -from django.utils.safestring import mark_safe - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Optional, Tuple - -try: - # support Django 1.9 - from django.template.base import Origin -except ImportError: - # backward compatibility - from django.template.loader import LoaderOrigin as Origin - - -def get_template_frame_from_exception( - exc_value: "Optional[BaseException]", -) -> "Optional[Dict[str, Any]]": - # As of Django 1.9 or so the new template debug thing showed up. - if hasattr(exc_value, "template_debug"): - return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore - - # As of r16833 (Django) all exceptions may contain a - # ``django_template_source`` attribute (rather than the legacy - # ``TemplateSyntaxError.source`` check) - if hasattr(exc_value, "django_template_source"): - return _get_template_frame_from_source( - exc_value.django_template_source # type: ignore - ) - - if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): - source = exc_value.source - if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): - return _get_template_frame_from_source(source) # type: ignore - - return None - - -def _get_template_name_description(template_name: str) -> str: - if isinstance(template_name, (list, tuple)): - if template_name: - return "[{}, ...]".format(template_name[0]) - else: - return template_name - - -def patch_templates() -> None: - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - real_rendered_content = SimpleTemplateResponse.rendered_content - - @property # type: ignore - @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) - def rendered_content(self: "SimpleTemplateResponse") -> str: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(self.template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_rendered_content.fget(self) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(self.template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", self.context_data) - return real_rendered_content.fget(self) - - SimpleTemplateResponse.rendered_content = rendered_content - - if DJANGO_VERSION < (1, 7): - return - import django.shortcuts - - real_render = django.shortcuts.render - - @functools.wraps(real_render) - @ensure_integration_enabled(DjangoIntegration, real_render) - def render( - request: "django.http.HttpRequest", - template_name: str, - context: "Optional[Dict[str, Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> "django.http.HttpResponse": - # Inject trace meta tags into template context - context = context or {} - if "sentry_trace_meta" not in context: - context["sentry_trace_meta"] = mark_safe( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_render(request, template_name, context, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", context) - return real_render(request, template_name, context, *args, **kwargs) - - django.shortcuts.render = render - - -def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": - if debug is None: - return None - - lineno = debug["line"] - filename = debug["name"] - if filename is None: - filename = "" - - pre_context = [] - post_context = [] - context_line = None - - for i, line in debug["source_lines"]: - if i < lineno: - pre_context.append(line) - elif i > lineno: - post_context.append(line) - else: - context_line = line - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - "in_app": True, - } - - -def _linebreak_iter(template_source: str) -> "Iterator[int]": - yield 0 - p = template_source.find("\n") - while p >= 0: - yield p + 1 - p = template_source.find("\n", p + 1) - - -def _get_template_frame_from_source( - source: "Tuple[Origin, Tuple[int, int]]", -) -> "Optional[Dict[str, Any]]": - if not source: - return None - - origin, (start, end) = source - filename = getattr(origin, "loadname", None) - if filename is None: - filename = "" - template_source = origin.reload() - lineno = None - upto = 0 - pre_context = [] - post_context = [] - context_line = None - - for num, next in enumerate(_linebreak_iter(template_source)): - line = template_source[upto:next] - if start >= upto and end <= next: - lineno = num - context_line = line - elif lineno is None: - pre_context.append(line) - else: - post_context.append(line) - - upto = next - - if context_line is None or lineno is None: - return None - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py deleted file mode 100644 index 192f0765e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/transactions.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Copied from raven-python. - -Despite being called "legacy" in some places this resolver is very much still -in use. -""" - -import re -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from re import Pattern - from typing import Dict, List, Optional, Tuple, Union - - from django.urls.resolvers import URLPattern, URLResolver - -from django import VERSION as DJANGO_VERSION - -if DJANGO_VERSION >= (2, 0): - from django.urls.resolvers import RoutePattern -else: - RoutePattern = None - -try: - from django.urls import get_resolver -except ImportError: - from django.core.urlresolvers import get_resolver - - -def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": - """Utility method for django's deprecated resolver.regex""" - try: - regex = resolver_or_pattern.regex - except AttributeError: - regex = resolver_or_pattern.pattern.regex - return regex - - -class RavenResolver: - _new_style_group_matcher = re.compile( - r"<(?:([^>:]+):)?([^>]+)>" - ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 - _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") - _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") - _non_named_group_matcher = re.compile(r"\([^\)]+\)") - # [foo|bar|baz] - _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") - _camel_re = re.compile(r"([A-Z]+)([a-z])") - - _cache: "Dict[URLPattern, str]" = {} - - def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: - r""" - Clean up urlpattern regexes into something readable by humans: - - From: - > "^(?P\w+)/athletes/(?P\w+)/$" - - To: - > "{sport_slug}/athletes/{athlete_slug}/" - """ - # "new-style" path patterns can be parsed directly without turning them - # into regexes first - if ( - RoutePattern is not None - and hasattr(pattern, "pattern") - and isinstance(pattern.pattern, RoutePattern) - ): - return self._new_style_group_matcher.sub( - lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) - ) - - result = get_regex(pattern).pattern - - # remove optional params - # TODO(dcramer): it'd be nice to change these into [%s] but it currently - # conflicts with the other rules because we're doing regexp matches - # rather than parsing tokens - result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) - - # handle named groups first - result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) - - # handle non-named groups - result = self._non_named_group_matcher.sub("{var}", result) - - # handle optional params - result = self._either_option_matcher.sub(lambda m: m.group(1), result) - - # clean up any outstanding regex-y characters. - result = ( - result.replace("^", "") - .replace("$", "") - .replace("?", "") - .replace("\\A", "") - .replace("\\Z", "") - .replace("//", "/") - .replace("\\", "") - ) - - return result - - def _resolve( - self, - resolver: "URLResolver", - path: str, - parents: "Optional[List[URLResolver]]" = None, - ) -> "Optional[str]": - match = get_regex(resolver).search(path) # Django < 2.0 - - if not match: - return None - - if parents is None: - parents = [resolver] - elif resolver not in parents: - parents = parents + [resolver] - - new_path = path[match.end() :] - for pattern in resolver.url_patterns: - # this is an include() - if not pattern.callback: - match_ = self._resolve(pattern, new_path, parents) - if match_: - return match_ - continue - elif not get_regex(pattern).search(new_path): - continue - - try: - return self._cache[pattern] - except KeyError: - pass - - prefix = "".join(self._simplify(p) for p in parents) - result = prefix + self._simplify(pattern) - if not result.startswith("/"): - result = "/" + result - self._cache[pattern] = result - return result - - return None - - def resolve( - self, - path: str, - urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, - ) -> "Optional[str]": - resolver = get_resolver(urlconf) - match = self._resolve(resolver, path) - return match - - -LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py deleted file mode 100644 index cf3012a75e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/django/views.py +++ /dev/null @@ -1,127 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -try: - from asyncio import iscoroutinefunction -except ImportError: - iscoroutinefunction = None # type: ignore - - -try: - from sentry_sdk.integrations.django.asgi import wrap_async_view -except (ImportError, SyntaxError): - wrap_async_view = None # type: ignore - - -def patch_views() -> None: - from django.core.handlers.base import BaseHandler - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - old_make_view_atomic = BaseHandler.make_view_atomic - old_render = SimpleTemplateResponse.render - - def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="serialize response", - attributes={ - "sentry.op": OP.VIEW_RESPONSE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_render(self) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RESPONSE_RENDER, - name="serialize response", - origin=DjangoIntegration.origin, - ): - return old_render(self) - - @functools.wraps(old_make_view_atomic) - def sentry_patched_make_view_atomic( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - callback = old_make_view_atomic(self, *args, **kwargs) - - # XXX: The wrapper function is created for every request. Find more - # efficient way to wrap views (or build a cache?) - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is not None: - is_async_view = ( - iscoroutinefunction is not None - and wrap_async_view is not None - and iscoroutinefunction(callback) - ) - if is_async_view: - sentry_wrapped_callback = wrap_async_view(callback) - else: - sentry_wrapped_callback = _wrap_sync_view(callback) - - else: - sentry_wrapped_callback = callback - - return sentry_wrapped_callback - - SimpleTemplateResponse.render = sentry_patched_render - BaseHandler.make_view_atomic = sentry_patched_make_view_atomic - - -def _wrap_sync_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - # set the active thread id to the handler thread for sync views - # this isn't necessary for async views since that runs on main - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return callback(request, *args, **kwargs) - - return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py deleted file mode 100644 index 310766ee3a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/dramatiq.py +++ /dev/null @@ -1,246 +0,0 @@ -import json -from typing import TypeVar - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - event_from_exception, -) - -R = TypeVar("R") - -try: - from dramatiq.broker import Broker - from dramatiq.errors import Retry - from dramatiq.message import Message - from dramatiq.middleware import Middleware, default_middleware -except ImportError: - raise DidNotEnable("Dramatiq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Union - - from sentry_sdk._types import Event, Hint - - -class DramatiqIntegration(Integration): - """ - Dramatiq integration for Sentry - - Please make sure that you call `sentry_sdk.init` *before* initializing - your broker, as it monkey patches `Broker.__init__`. - - This integration was originally developed and maintained - by https://github.com/jacobsvante and later donated to the Sentry - project. - """ - - identifier = "dramatiq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_dramatiq_broker() - - -def _patch_dramatiq_broker() -> None: - original_broker__init__ = Broker.__init__ - - def sentry_patched_broker__init__( - self: "Broker", *args: "Any", **kw: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - - try: - middleware = kw.pop("middleware") - except KeyError: - # Unfortunately Broker and StubBroker allows middleware to be - # passed in as positional arguments, whilst RabbitmqBroker and - # RedisBroker does not. - if len(args) == 1: - middleware = args[0] - args = () - else: - middleware = None - - if middleware is None: - middleware = list(m() for m in default_middleware) - else: - middleware = list(middleware) - - if integration is not None: - middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] - middleware.insert(0, SentryMiddleware()) - - kw["middleware"] = middleware - original_broker__init__(self, *args, **kw) - - Broker.__init__ = sentry_patched_broker__init__ - - -class SentryMiddleware(Middleware): # type: ignore[misc] - """ - A Dramatiq middleware that automatically captures and sends - exceptions to Sentry. - - This is automatically added to every instantiated broker via the - DramatiqIntegration. - """ - - SENTRY_HEADERS_NAME = "_sentry_headers" - - def before_enqueue( - self, broker: "Broker", message: "Message[R]", delay: int - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - message.options[self.SENTRY_HEADERS_NAME] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - - def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DramatiqIntegration) - if integration is None: - return - - message._scope_manager = sentry_sdk.isolation_scope() - scope = message._scope_manager.__enter__() - scope.clear_breadcrumbs() - scope.set_extra("dramatiq_message_id", message.message_id) - scope.add_event_processor(_make_message_event_processor(message, integration)) - - sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} - if "retries" in message.options: - # start new trace in case of retrying - sentry_headers = {} - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(sentry_headers) - span = sentry_sdk.traces.start_span( - name=message.actor_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_DRAMATIQ, - "sentry.origin": DramatiqIntegration.origin, - "sentry.span.source": SegmentSource.TASK.value, - }, - parent_span=None, - ) - message._sentry_span_ctx = span - else: - transaction = continue_trace( - sentry_headers, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - origin=DramatiqIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - sentry_sdk.start_transaction( - transaction, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - ) - transaction.__enter__() - message._sentry_span_ctx = transaction - - def after_process_message( - self, - broker: "Broker", - message: "Message[R]", - *, - result: "Optional[Any]" = None, - exception: "Optional[Exception]" = None, - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - actor = broker.get_actor(message.actor_name) - throws = message.options.get("throws") or actor.options.get("throws") - - scope_manager = message._scope_manager - span_ctx = getattr(message, "_sentry_span_ctx", None) - if span_ctx is None: - return None - - is_event_capture_required = ( - exception is not None - and not (throws and isinstance(exception, throws)) - and not isinstance(exception, Retry) - ) - if not is_event_capture_required: - # normal transaction finish - span_ctx.__exit__(None, None, None) - scope_manager.__exit__(None, None, None) - return - - event, hint = event_from_exception( - exception, # type: ignore[arg-type] - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": DramatiqIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - # transaction error - span_ctx.__exit__(type(exception), exception, None) - scope_manager.__exit__(type(exception), exception, None) - - after_skip_message = after_process_message - - -def _make_message_event_processor( - message: "Message[R]", integration: "DramatiqIntegration" -) -> "Callable[[Event, Hint], Optional[Event]]": - def inner(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - DramatiqMessageExtractor(message).extract_into_event(event) - - return event - - return inner - - -class DramatiqMessageExtractor: - def __init__(self, message: "Message[R]") -> None: - self.message_data = dict(message.asdict()) - - def content_length(self) -> int: - return len(json.dumps(self.message_data)) - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - contexts = event.setdefault("contexts", {}) - request_info = contexts.setdefault("dramatiq", {}) - request_info["type"] = "dramatiq" - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - if not request_body_within_bounds(client, self.content_length()): - data = AnnotatedValue.removed_because_over_size_limit() - else: - data = self.message_data - - request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py deleted file mode 100644 index 6bbc61000d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/excepthook.py +++ /dev/null @@ -1,76 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - - -class ExcepthookIntegration(Integration): - identifier = "excepthook" - - always_run = False - - def __init__(self, always_run: bool = False) -> None: - if not isinstance(always_run, bool): - raise ValueError( - "Invalid value for always_run: %s (must be type boolean)" - % (always_run,) - ) - self.always_run = always_run - - @staticmethod - def setup_once() -> None: - sys.excepthook = _make_excepthook(sys.excepthook) - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - if _should_send(integration.always_run): - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "excepthook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook - - -def _should_send(always_run: bool = False) -> bool: - if always_run: - return True - - if hasattr(sys, "ps1"): - # Disable the excepthook for interactive Python shells, otherwise - # every typo gets sent to Sentry. - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py deleted file mode 100644 index 4473fcc435..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/executing.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - - -class ExecutingIntegration(Integration): - identifier = "executing" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - frame = tb.tb_frame - source = Source.for_frame(frame) - sentry_frame["function"] = source.code_qualname(frame.f_code) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py deleted file mode 100644 index 7a595bcf2a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/falcon.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - - from sentry_sdk._types import Event, EventProcessor - -# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` -# and `falcon.API` to `falcon.App` - -try: - import falcon # type: ignore - from falcon import __version__ as FALCON_VERSION -except ImportError: - raise DidNotEnable("Falcon not installed") - -try: - import falcon.app_helpers # type: ignore - - falcon_helpers = falcon.app_helpers - falcon_app_class = falcon.App - FALCON3 = True -except ImportError: - import falcon.api_helpers # type: ignore - - falcon_helpers = falcon.api_helpers - falcon_app_class = falcon.API - FALCON3 = False - - -_FALCON_UNSET: "Optional[object]" = None -if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ - with capture_internal_exceptions(): - from falcon.request import ( # type: ignore[import-not-found, no-redef] - _UNSET as _FALCON_UNSET, - ) - - -class FalconRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, Any]": - return self.request.env - - def cookies(self) -> "Dict[str, Any]": - return self.request.cookies - - def form(self) -> None: - return None # No such concept in Falcon - - def files(self) -> None: - return None # No such concept in Falcon - - def raw_data(self) -> "Optional[str]": - # As request data can only be read once we won't make this available - # to Sentry. Just send back a dummy string in case there was a - # content length. - # TODO(jmagnusson): Figure out if there's a way to support this - content_length = self.content_length() - if content_length > 0: - return "[REQUEST_CONTAINING_RAW_DATA]" - else: - return None - - def json(self) -> "Optional[Dict[str, Any]]": - # fallback to cached_media = None if self.request._media is not available - cached_media = None - with capture_internal_exceptions(): - # self.request._media is the cached self.request.media - # value. It is only available if self.request.media - # has already been accessed. Therefore, reading - # self.request._media will not exhaust the raw request - # stream (self.request.bounded_stream) because it has - # already been read if self.request._media is set. - cached_media = self.request._media - - if cached_media is not _FALCON_UNSET: - return cached_media - - return None - - -class SentryFalconMiddleware: - """Captures exceptions in Falcon requests and send to Sentry""" - - def process_request( - self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return - - scope = sentry_sdk.get_isolation_scope() - scope._name = "falcon" - scope.add_event_processor(_make_request_event_processor(req, integration)) - - def process_resource( - self, req: "Any", resp: "Any", resource: "Any", params: "Any" - ) -> None: - """ - Sets the segment name and source as the route is resolved when this runs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FalconIntegration) - if integration is None or not has_span_streaming_enabled(client.options): - return - - name_for_style = { - "uri_template": req.uri_template, - "path": req.path, - } - name = name_for_style[integration.transaction_style] - source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] - sentry_sdk.set_transaction_name(name, source) - - -TRANSACTION_STYLE_VALUES = ("uri_template", "path") - - -class FalconIntegration(Integration): - identifier = "falcon" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "uri_template") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - version = parse_version(FALCON_VERSION) - _check_minimum_version(FalconIntegration, version) - - _patch_wsgi_app() - _patch_handle_exception() - _patch_prepare_middleware() - - -def _patch_wsgi_app() -> None: - original_wsgi_app = falcon_app_class.__call__ - - def sentry_patched_wsgi_app( - self: "falcon.API", env: "Any", start_response: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return original_wsgi_app(self, env, start_response) - - sentry_wrapped = SentryWsgiMiddleware( - lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), - span_origin=FalconIntegration.origin, - ) - - return sentry_wrapped(env, start_response) - - falcon_app_class.__call__ = sentry_patched_wsgi_app - - -def _patch_handle_exception() -> None: - original_handle_exception = falcon_app_class._handle_exception - - @ensure_integration_enabled(FalconIntegration, original_handle_exception) - def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": - # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception - # method signature from `(ex, req, resp, params)` to - # `(req, resp, ex, params)` - ex = response = None - with capture_internal_exceptions(): - ex = next(argument for argument in args if isinstance(argument, Exception)) - response = next( - argument for argument in args if isinstance(argument, falcon.Response) - ) - - was_handled = original_handle_exception(self, *args) - - if ex is None or response is None: - # Both ex and response should have a non-None value at this point; otherwise, - # there is an error with the SDK that will have been captured in the - # capture_internal_exceptions block above. - return was_handled - - if _exception_leads_to_http_5xx(ex, response): - event, hint = event_from_exception( - ex, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "falcon", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return was_handled - - falcon_app_class._handle_exception = sentry_patched_handle_exception - - -def _patch_prepare_middleware() -> None: - original_prepare_middleware = falcon_helpers.prepare_middleware - - def sentry_patched_prepare_middleware( - middleware: "Any" = None, - independent_middleware: "Any" = False, - asgi: bool = False, - ) -> "Any": - if asgi: - # We don't support ASGI Falcon apps, so we don't patch anything here - return original_prepare_middleware(middleware, independent_middleware, asgi) - - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is not None: - middleware = [SentryFalconMiddleware()] + (middleware or []) - - # We intentionally omit the asgi argument here, since the default is False anyways, - # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. - return original_prepare_middleware(middleware, independent_middleware) - - falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware - - -def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: - is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( - "5" - ) - is_unhandled_error = not isinstance( - ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) - ) - - # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response - # at the stage where we capture it is listed as 200, even though we would expect to see a 500 - # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to - # only perform this check on Falcon 3+, despite the risk that some handled errors might be - # reported to Sentry as unhandled on Falcon 2. - return (is_server_error or is_unhandled_error) and ( - not FALCON3 or _has_http_5xx_status(response) - ) - - -def _has_http_5xx_status(response: "falcon.Response") -> bool: - return response.status.startswith("5") - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "falcon.Request" -) -> None: - name_for_style = { - "uri_template": request.uri_template, - "path": request.path, - } - event["transaction"] = name_for_style[transaction_style] - event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} - - -def _make_request_event_processor( - req: "falcon.Request", integration: "FalconIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, req) - - with capture_internal_exceptions(): - FalconRequestExtractor(req).extract_into_event(event) - - return event - - return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py deleted file mode 100644 index c7b97c88b1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/fastapi.py +++ /dev/null @@ -1,201 +0,0 @@ -import sys -from copy import deepcopy -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import transaction_from_function - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Dict - - from sentry_sdk._types import Event - -try: - from sentry_sdk.integrations.starlette import ( - StarletteIntegration, - StarletteRequestExtractor, - _get_cached_request_body_attribute, - ) -except DidNotEnable: - raise DidNotEnable("Starlette is not installed") - -try: - import fastapi # type: ignore -except ImportError: - raise DidNotEnable("FastAPI is not installed") - - -_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -class FastApiIntegration(StarletteIntegration): - identifier = "fastapi" - - @staticmethod - def setup_once() -> None: - patch_get_request_handler() - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or "" - - elif transaction_style == "url": - route = request.scope.get("route") - - if route: - # FastAPI >= 0.137 stores the prefix-resolved path on an - # effective_route_context in scope["fastapi"], while - # scope["route"].path holds the unprefixed original. - # Prefer the effective context path when available. - effective_route_context = request.scope.get("fastapi", {}).get( - "effective_route_context" - ) - context_path = getattr(effective_route_context, "path", None) - - if context_path: - name = context_path - else: - path = getattr(route, "path", None) - if path is not None: - name = path - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name(name, source=source) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FastApiIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, Dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = FastApiIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_get_request_handler() -> None: - old_get_request_handler = fastapi.routing.get_request_handler - - def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": - dependant = kwargs.get("dependant") - if ( - dependant - and dependant.call is not None - and not iscoroutinefunction(dependant.call) - # FastAPI >= 0.137 calls get_request_handler() on every request - # (router-tree traversal) rather than once at registration. Guard - # against accumulating _sentry_call wrappers on the shared - # dependant object, which would cause a RecursionError after ~987 - # requests as the call chain grows past Python's recursion limit. - and not getattr(dependant.call, "_sentry_is_patched", False) - ): - old_call = dependant.call - - @wraps(old_call) - def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": - current_scope = sentry_sdk.get_current_scope() - - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_call(*args, **kwargs) - - _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] - dependant.call = _sentry_call - - old_app = old_get_request_handler(*args, **kwargs) - - async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_app, *args, **kwargs) - - return _sentry_app - - fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py deleted file mode 100644 index 1902091fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/flask.py +++ /dev/null @@ -1,281 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Union - - from werkzeug.datastructures import FileStorage, ImmutableMultiDict - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - - -try: - import flask_login # type: ignore -except ImportError: - flask_login = None - -try: - from flask import Flask, Request # type: ignore - from flask import request as flask_request - from flask.signals import ( - before_render_template, - got_request_exception, - request_started, - ) - from markupsafe import Markup -except ImportError: - raise DidNotEnable("Flask is not installed") - -try: - import blinker # noqa -except ImportError: - raise DidNotEnable("blinker is not installed") - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class FlaskIntegration(Integration): - identifier = "flask" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - try: - from quart import Quart # type: ignore - - if Flask == Quart: - # This is Quart masquerading as Flask, don't enable the Flask - # integration. See https://github.com/getsentry/sentry-python/issues/2709 - raise DidNotEnable( - "This is not a Flask app but rather Quart pretending to be Flask" - ) - except ImportError: - pass - - version = package_version("flask") - _check_minimum_version(FlaskIntegration, version) - - before_render_template.connect(_add_sentry_trace) - request_started.connect(_request_started) - got_request_exception.connect(_capture_exception) - - old_app = Flask.__call__ - - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return old_app(self, environ, start_response) - - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=FlaskIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - Flask.__call__ = sentry_patched_wsgi_app - - -def _add_sentry_trace( - sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" -) -> None: - if "sentry_trace" in context: - return - - scope = sentry_sdk.get_current_scope() - trace_meta = Markup(scope.trace_propagation_meta()) - context["sentry_trace"] = trace_meta # for backwards compatibility - context["sentry_trace_meta"] = trace_meta - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - scope.set_transaction_name( - name_for_style[transaction_style], - source=SOURCE_FOR_STYLE[transaction_style], - ) - except Exception: - pass - - -def _request_started(app: "Flask", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return - - request = flask_request._get_current_object() - - # Set the transaction name and source here, - # but rely on WSGI middleware to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - scope.set_user(user_properties) - - evt_processor = _make_request_event_processor(app, request, integration) - scope.add_event_processor(evt_processor) - - -class FlaskRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[Any, Any]": - return { - k: v[0] if isinstance(v, list) and len(v) == 1 else v - for k, v in self.request.cookies.items() - } - - def raw_data(self) -> bytes: - return self.request.get_data() - - def form(self) -> "ImmutableMultiDict[str, Any]": - return self.request.form - - def files(self) -> "ImmutableMultiDict[str, Any]": - return self.request.files - - def is_json(self) -> bool: - return self.request.is_json - - def json(self) -> "Any": - return self.request.get_json(silent=True) - - def size_of_file(self, file: "FileStorage") -> int: - return file.content_length - - -def _make_request_event_processor( - app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - FlaskRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _add_user_to_event(event) - - return event - - return inner - - -@ensure_integration_enabled(FlaskIntegration) -def _capture_exception( - sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "flask", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_flask_user_properties() -> "Dict[str, str]": - if flask_login is None: - return {} - - user = flask_login.current_user - if user is None: - return {} - - properties = {} - - try: - user_id = user.get_id() - if user_id is not None: - properties["id"] = user_id - except AttributeError: - # might happen if: - # - flask_login could not be imported - # - flask_login is not configured - # - no user is logged in - pass - - # The following attribute accesses are ineffective for the general - # Flask-Login case, because the User interface of Flask-Login does not - # care about anything but the ID. However, Flask-User (based on - # Flask-Login) documents a few optional extra attributes. - # - # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names - try: - if user.email is not None: - properties["email"] = user.email - except Exception: - pass - - try: - if user.username is not None: - properties["username"] = user.username - except Exception: - pass - - return properties - - -def _add_user_to_event(event: "Event") -> None: - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - user_info = event.setdefault("user", {}) - for key, value in user_properties.items(): - user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py deleted file mode 100644 index 91a62b3a81..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gcp.py +++ /dev/null @@ -1,286 +0,0 @@ -import functools -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -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 -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -# Constants -TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -def _wrap_func(func: "F") -> "F": - @functools.wraps(func) - def sentry_func( - functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(GcpIntegration) - if integration is None: - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = environ.get("FUNCTION_TIMEOUT_SEC") - if not configured_time: - logger.debug( - "The configured timeout could not be fetched from Cloud Functions configuration." - ) - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = int(configured_time) - - initial_time = datetime.now(timezone.utc) - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - gcp_event, configured_time, initial_time - ) - ) - scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) - timeout_thread = None - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = configured_time - TIMEOUT_WARNING_BUFFER - - timeout_thread = TimeoutThread( - waiting_time, - configured_time, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = {} - header_attributes: "dict[str, Any]" = {} - if hasattr(gcp_event, "headers"): - headers = gcp_event.headers - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - additional_attributes = {} - 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" - ) - - sampling_context = { - "gcp_env": { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - }, - "gcp_event": gcp_event, - } - - function_name = environ.get("FUNCTION_NAME", "") - - if environ.get("GCP_PROJECT"): - additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") - - if environ.get("FUNCTION_IDENTITY"): - additional_attributes["faas.identity"] = environ.get( - "FUNCTION_IDENTITY" - ) - - if environ.get("ENTRY_POINT"): - additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_GCP, - "sentry.origin": GcpIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.provider": CLOUD_PROVIDER.GCP, - "faas.name": function_name, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_GCP, - name=environ.get("FUNCTION_NAME", ""), - source=TransactionSource.COMPONENT, - origin=GcpIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return func(functionhandler, gcp_event, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "gcp", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - # Flush out the event queue - client.flush() - - return sentry_func # type: ignore - - -class GcpIntegration(Integration): - identifier = "gcp" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - import __main__ as gcp_functions - - if not hasattr(gcp_functions, "worker_v1"): - logger.warning( - "GcpIntegration currently supports only Python 3.7 runtime environment." - ) - return - - worker1 = gcp_functions.worker_v1 - - worker1.FunctionHandler.invoke_user_function = _wrap_func( - worker1.FunctionHandler.invoke_user_function - ) - - -def _make_request_event_processor( - gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - final_time = datetime.now(timezone.utc) - time_diff = final_time - initial_time - - execution_duration_in_millis = time_diff / timedelta(milliseconds=1) - - extra = event.setdefault("extra", {}) - extra["google cloud functions"] = { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - "execution_duration_in_millis": execution_duration_in_millis, - "configured_timeout_in_seconds": configured_timeout, - } - - extra["google cloud logs"] = { - "url": _get_google_cloud_logs_url(final_time), - } - - request = event.get("request", {}) - - request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) - - if hasattr(gcp_event, "method"): - request["method"] = gcp_event.method - - if hasattr(gcp_event, "query_string"): - request["query_string"] = gcp_event.query_string.decode( - "utf-8", errors="replace" - ) - - if hasattr(gcp_event, "headers"): - request["headers"] = _filter_headers(gcp_event.headers) - - if should_send_default_pii(): - if hasattr(gcp_event, "data"): - request["data"] = gcp_event.data - else: - if hasattr(gcp_event, "data"): - # Unfortunately couldn't find a way to get structured body from GCP - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - event["request"] = deepcopy(request) - - return event - - return event_processor - - -def _get_google_cloud_logs_url(final_time: "datetime") -> str: - """ - Generates a Google Cloud Logs console URL based on the environment variables - Arguments: - final_time {datetime} -- Final time - Returns: - str -- Google Cloud Logs Console URL to logs. - """ - hour_ago = final_time - timedelta(hours=1) - formatstring = "%Y-%m-%dT%H:%M:%SZ" - - url = ( - "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" - "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" - "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" - "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" - "&interval=PT1H&scrollTimestamp={timestamp_end}" - ).format( - project=environ.get("GCP_PROJECT"), - function_name=environ.get("FUNCTION_NAME"), - region=environ.get("FUNCTION_REGION"), - timestamp_end=final_time.strftime(formatstring), - timestamp_start=hour_ago.strftime(formatstring), - ) - - return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py deleted file mode 100644 index 4be0f479bc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gnu_backtrace.py +++ /dev/null @@ -1,96 +0,0 @@ -import re -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - -# function is everything between index at @ -# and then we match on the @ plus the hex val -FUNCTION_RE = r"[^@]+?" -HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" - -_FRAME_RE_PATTERN = r""" -^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ -""".format( - FUNCTION_RE=FUNCTION_RE, - HEX_ADDRESS=HEX_ADDRESS, -) - -FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) - - -class GnuBacktraceIntegration(Integration): - identifier = "gnu_backtrace" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - return _process_gnu_backtrace(event, hint) - - -def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception in values: - frames = exception.get("stacktrace", {}).get("frames", []) - if not frames: - continue - - msg = exception.get("value", None) - if not msg: - continue - - additional_frames = [] - new_msg = [] - - for line in msg.splitlines(): - match = FRAME_RE.match(line) - if match: - additional_frames.append( - ( - int(match.group("index")), - { - "package": match.group("package") or None, - "function": match.group("function") or None, - "platform": "native", - }, - ) - ) - else: - # Put garbage lines back into message, not sure what to do with them. - new_msg.append(line) - - if additional_frames: - additional_frames.sort(key=lambda x: -x[0]) - for _, frame in additional_frames: - frames.append(frame) - - new_msg.append("") - exception["value"] = "\n".join(new_msg) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py deleted file mode 100644 index 45652c3f71..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/__init__.py +++ /dev/null @@ -1,457 +0,0 @@ -from functools import wraps -from typing import ( - Any, - AsyncIterator, - Callable, - Iterator, - List, -) - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import SPANSTATUS -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.genai.models import AsyncModels, Models -except ImportError: - raise DidNotEnable("google-genai not installed") - - -from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN -from .streaming import ( - accumulate_streaming_response, - set_span_data_for_streaming_response, -) -from .utils import ( - _capture_exception, - prepare_embed_content_args, - prepare_generate_content_args, - set_span_data_for_embed_request, - set_span_data_for_embed_response, - set_span_data_for_request, - set_span_data_for_response, -) - - -class GoogleGenAIIntegration(Integration): - identifier = IDENTIFIER - origin = ORIGIN - - def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Patch sync methods - Models.generate_content = _wrap_generate_content(Models.generate_content) - Models.generate_content_stream = _wrap_generate_content_stream( - Models.generate_content_stream - ) - Models.embed_content = _wrap_embed_content(Models.embed_content) - - # Patch async methods - AsyncModels.generate_content = _wrap_async_generate_content( - AsyncModels.generate_content - ) - AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( - AsyncModels.generate_content_stream - ) - AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) - - -def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = f(self, *args, **kwargs) - - # Create wrapper iterator to accumulate responses - def new_iterator() -> "Iterator[Any]": - chunks: "List[Any]" = [] - try: - for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_generate_content_stream - - -def _wrap_async_generate_content_stream( - f: "Callable[..., Any]", -) -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = await f(self, *args, **kwargs) - - # Create wrapper async iterator to accumulate responses - async def new_async_iterator() -> "AsyncIterator[Any]": - chunks: "List[Any]" = [] - try: - async for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_async_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_async_generate_content_stream - - -def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_generate_content - - -def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_async_generate_content - - -def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_embed_content - - -def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_embed_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py deleted file mode 100644 index 5b53ebf0e2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/consts.py +++ /dev/null @@ -1,16 +0,0 @@ -GEN_AI_SYSTEM = "gcp.gemini" - -# Mapping of tool attributes to their descriptions -# These are all tools that are available in the Google GenAI API -TOOL_ATTRIBUTES_MAP = { - "google_search_retrieval": "Google Search retrieval tool", - "google_search": "Google Search tool", - "retrieval": "Retrieval tool", - "enterprise_web_search": "Enterprise web search tool", - "google_maps": "Google Maps tool", - "code_execution": "Code execution tool", - "computer_use": "Computer use tool", -} - -IDENTIFIER = "google_genai" -ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py deleted file mode 100644 index 8414ea4f21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/streaming.py +++ /dev/null @@ -1,172 +0,0 @@ -from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union - -from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import ( - safe_serialize, -) - -from .utils import ( - UsageData, - extract_contents_text, - extract_finish_reasons, - extract_tool_calls, - extract_usage_data, -) - -if TYPE_CHECKING: - from google.genai.types import GenerateContentResponse - - from sentry_sdk.tracing import Span - - -class AccumulatedResponse(TypedDict): - id: "Optional[str]" - model: "Optional[str]" - text: str - finish_reasons: "List[str]" - tool_calls: "List[dict[str, Any]]" - usage_metadata: "Optional[UsageData]" - - -def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": - return UsageData( - input_tokens=max(self["input_tokens"], other["input_tokens"]), - output_tokens=max(self["output_tokens"], other["output_tokens"]), - input_tokens_cached=max( - self["input_tokens_cached"], other["input_tokens_cached"] - ), - output_tokens_reasoning=max( - self["output_tokens_reasoning"], other["output_tokens_reasoning"] - ), - total_tokens=max(self["total_tokens"], other["total_tokens"]), - ) - - -def accumulate_streaming_response( - chunks: "List[GenerateContentResponse]", -) -> "AccumulatedResponse": - """Accumulate streaming chunks into a single response-like object.""" - accumulated_text = [] - finish_reasons = [] - tool_calls = [] - usage_data = None - response_id = None - model = None - - for chunk in chunks: - # Extract text and tool calls - if getattr(chunk, "candidates", None): - for candidate in getattr(chunk, "candidates", []): - if hasattr(candidate, "content") and getattr( - candidate.content, "parts", [] - ): - extracted_text = extract_contents_text(candidate.content) - if extracted_text: - accumulated_text.append(extracted_text) - - extracted_finish_reasons = extract_finish_reasons(chunk) - if extracted_finish_reasons: - finish_reasons.extend(extracted_finish_reasons) - - extracted_tool_calls = extract_tool_calls(chunk) - if extracted_tool_calls: - tool_calls.extend(extracted_tool_calls) - - # Use last possible chunk, in case of interruption, and - # gracefully handle missing intermediate tokens by taking maximum - # with previous token reporting. - chunk_usage_data = extract_usage_data(chunk) - usage_data = ( - chunk_usage_data - if usage_data is None - else element_wise_usage_max(usage_data, chunk_usage_data) - ) - - accumulated_response = AccumulatedResponse( - text="".join(accumulated_text), - finish_reasons=finish_reasons, - tool_calls=tool_calls, - usage_metadata=usage_data, - id=response_id, - model=model, - ) - - return accumulated_response - - -def set_span_data_for_streaming_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - accumulated_response: "AccumulatedResponse", -) -> None: - """Set span data for accumulated streaming response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if ( - should_send_default_pii() - and integration.include_prompts - and accumulated_response.get("text") - ): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TEXT, - safe_serialize([accumulated_response["text"]]), - ) - - if accumulated_response.get("finish_reasons"): - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - accumulated_response["finish_reasons"], - ) - - if accumulated_response.get("tool_calls"): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(accumulated_response["tool_calls"]), - ) - - response_id = accumulated_response.get("id") - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - - response_model = accumulated_response.get("model") - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - if accumulated_response["usage_metadata"] is None: - return - - if accumulated_response["usage_metadata"]["input_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - accumulated_response["usage_metadata"]["input_tokens"], - ) - - if accumulated_response["usage_metadata"]["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - accumulated_response["usage_metadata"]["input_tokens_cached"], - ) - - if accumulated_response["usage_metadata"]["output_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - accumulated_response["usage_metadata"]["output_tokens"], - ) - - if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - accumulated_response["usage_metadata"]["output_tokens_reasoning"], - ) - - if accumulated_response["usage_metadata"]["total_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - accumulated_response["usage_metadata"]["total_tokens"], - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py deleted file mode 100644 index 464a812680..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/google_genai/utils.py +++ /dev/null @@ -1,1118 +0,0 @@ -import copy -import inspect -import json -from functools import wraps -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - TypedDict, - Union, -) - -from google.genai.types import Content, GenerateContentConfig, Part, PartDict - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.utils import ( - get_modality_from_mime_type, - normalize_message_roles, - set_data_normalized, - transform_google_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - safe_serialize, -) - -from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP - -if TYPE_CHECKING: - from google.genai.types import ( - ContentListUnion, - ContentUnion, - ContentUnionDict, - EmbedContentResponse, - GenerateContentResponse, - Model, - Tool, - ) - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -_is_PIL_available = False -try: - from PIL import Image as PILImage # type: ignore[import-not-found] - - _is_PIL_available = True -except ImportError: - pass - -# Keys to use when checking to see if a dict provided by the user -# is Part-like (as opposed to a Content or multi-turn conversation entry). -_PART_DICT_KEYS = PartDict.__optional_keys__ - - -class UsageData(TypedDict): - """Structure for token usage data.""" - - input_tokens: int - input_tokens_cached: int - output_tokens: int - output_tokens_reasoning: int - total_tokens: int - - -def extract_usage_data( - response: "Union[GenerateContentResponse, dict[str, Any]]", -) -> "UsageData": - """Extract usage data from response into a structured format. - - Args: - response: The GenerateContentResponse object or dictionary containing usage metadata - - Returns: - UsageData: Dictionary with input_tokens, input_tokens_cached, - output_tokens, and output_tokens_reasoning fields - """ - usage_data = UsageData( - input_tokens=0, - input_tokens_cached=0, - output_tokens=0, - output_tokens_reasoning=0, - total_tokens=0, - ) - - # Handle dictionary response (from streaming) - if isinstance(response, dict): - usage = response.get("usage_metadata", {}) - if not usage: - return usage_data - - prompt_tokens = usage.get("prompt_token_count", 0) or 0 - tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - cached_tokens = usage.get("cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - candidates_tokens = usage.get("candidates_token_count", 0) or 0 - # python-genai reports output and reasoning tokens separately - # reasoning should be sub-category of output tokens - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = usage.get("total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - if not hasattr(response, "usage_metadata"): - return usage_data - - usage = response.usage_metadata - - # Input tokens include both prompt and tool use prompt tokens - prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 - tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - # Cached input tokens - cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - # Reasoning tokens - reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - # output_tokens = candidates_tokens + reasoning_tokens - # google-genai reports output and reasoning tokens separately - candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = getattr(usage, "total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - -def _capture_exception(exc: "Any") -> None: - """Capture exception with Google GenAI mechanism.""" - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "google_genai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def get_model_name(model: "Union[str, Model]") -> str: - """Extract model name from model parameter.""" - if isinstance(model, str): - return model - # Handle case where model might be an object with a name attribute - if hasattr(model, "name"): - return str(model.name) - return str(model) - - -def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": - """Extract messages from contents parameter which can have various formats. - - Returns a list of message dictionaries in the format: - - System: {"role": "system", "content": "string"} - - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} - """ - if contents is None: - return [] - - messages = [] - - # Handle string case - if isinstance(contents, str): - return [{"role": "user", "content": contents}] - - # Handle list case - if isinstance(contents, list): - if contents and all(_is_part_like(item) for item in contents): - # All items are parts — merge into a single multi-part user message - content_parts = [] - for item in contents: - part = _extract_part_from_item(item) - if part is not None: - content_parts.append(part) - - return [{"role": "user", "content": content_parts}] - else: - # Multi-turn conversation or mixed content types - for item in contents: - item_messages = extract_contents_messages(item) - messages.extend(item_messages) - return messages - - # Handle dictionary case (ContentDict) - if isinstance(contents, dict): - role = contents.get("role", "user") - parts = contents.get("parts") - - if parts: - content_parts = [] - tool_messages = [] - - for part in parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - # Tool message - add separately - tool_messages.append(part_result) - else: - # Regular content part - content_parts.append(part_result) - - # Add main message if we have content parts - if content_parts: - # Normalize role: "model" -> "assistant" - normalized_role = "assistant" if role == "model" else role or "user" - messages.append({"role": normalized_role, "content": content_parts}) - - # Add tool messages - messages.extend(tool_messages) - elif "text" in contents: - messages.append( - { - "role": role, - "content": [{"text": contents["text"], "type": "text"}], - } - ) - elif "inline_data" in contents: - # The "data" will always be bytes (or bytes within a string), - # so if this is present, it's safe to automatically substitute with the placeholder - messages.append( - { - "inline_data": { - "mime_type": contents["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - ) - - return messages - - # Handle Content object - if hasattr(contents, "parts") and contents.parts: - role = getattr(contents, "role", None) or "user" - content_parts = [] - tool_messages = [] - - for part in contents.parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - tool_messages.append(part_result) - else: - content_parts.append(part_result) - - if content_parts: - normalized_role = "assistant" if role == "model" else role - messages.append({"role": normalized_role, "content": content_parts}) - - messages.extend(tool_messages) - return messages - - # Handle Part object directly - part_result = _extract_part_content(contents) - if part_result: - if isinstance(part_result, dict) and part_result.get("role") == "tool": - return [part_result] - else: - return [{"role": "user", "content": [part_result]}] - - # Handle PIL.Image.Image - if _is_PIL_available and isinstance(contents, PILImage.Image): - blob_part = _extract_pil_image(contents) - if blob_part: - return [{"role": "user", "content": [blob_part]}] - - # Handle File object - if hasattr(contents, "uri") and hasattr(contents, "mime_type"): - # File object - file_uri = getattr(contents, "uri", None) - mime_type = getattr(contents, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing - if file_uri is not None: - # Default to empty string if mime_type is None - if mime_type is None: - mime_type = "" - - blob_part = { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - return [{"role": "user", "content": [blob_part]}] - - # Handle direct text attribute - if hasattr(contents, "text") and contents.text: - return [ - {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} - ] - - return [] - - -def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": - """Extract content from a Part object or dict. - - Returns: - - dict for content part (text/blob) or tool message - - None if part should be skipped - """ - if part is None: - return None - - # Handle dict Part - if isinstance(part, dict): - # Check for function_response first (tool message) - if "function_response" in part: - return _extract_tool_message_from_part(part) - - if part.get("text"): - return {"text": part["text"], "type": "text"} - - # Try using Google-specific transform for dict formats (inline_data, file_data) - result = transform_google_content_part(part) - if result is not None: - # For inline_data with bytes data, substitute the content - if "inline_data" in part: - # inline_data.data will always be bytes, or a string containing base64-encoded bytes, - # so can automatically substitute without further checks - result["content"] = BLOB_DATA_SUBSTITUTE - return result - - return None - - # Handle Part object - # Check for function_response (tool message) - if hasattr(part, "function_response") and part.function_response: - return _extract_tool_message_from_part(part) - - # Handle text - if hasattr(part, "text") and part.text: - return {"text": part.text, "type": "text"} - - # Handle file_data - if hasattr(part, "file_data") and part.file_data: - file_data = part.file_data - file_uri = getattr(file_data, "file_uri", None) - mime_type = getattr(file_data, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) - if file_uri is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - # Handle inline_data - if hasattr(part, "inline_data") and part.inline_data: - inline_data = part.inline_data - data = getattr(inline_data, "data", None) - mime_type = getattr(inline_data, "mime_type", None) - # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) - if data is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - return None - - -def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": - """Extract tool message from a Part with function_response. - - Returns: - {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} - or None if not a valid tool message - """ - function_response = None - - if isinstance(part, dict): - function_response = part.get("function_response") - elif hasattr(part, "function_response"): - function_response = part.function_response - - if not function_response: - return None - - # Extract fields from function_response - tool_call_id = None - tool_name = None - output = None - - if isinstance(function_response, dict): - tool_call_id = function_response.get("id") - tool_name = function_response.get("name") - response_dict = function_response.get("response", {}) - # Prefer "output" key if present, otherwise use entire response - output = response_dict.get("output", response_dict) - else: - # FunctionResponse object - tool_call_id = getattr(function_response, "id", None) - tool_name = getattr(function_response, "name", None) - response_obj = getattr(function_response, "response", None) - if response_obj is None: - response_obj = {} - if isinstance(response_obj, dict): - output = response_obj.get("output", response_obj) - else: - output = response_obj - - if not tool_name: - return None - - return { - "role": "tool", - "content": { - "toolCallId": str(tool_call_id) if tool_call_id else None, - "toolName": str(tool_name), - "output": safe_serialize(output) if output is not None else None, - }, - } - - -def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": - """Extract blob part from PIL.Image.Image.""" - if not _is_PIL_available or not isinstance(image, PILImage.Image): - return None - - # Get format, default to JPEG - format_str = image.format or "JPEG" - suffix = format_str.lower() - mime_type = f"image/{suffix}" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _is_part_like(item: "Any") -> bool: - """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" - if isinstance(item, (str, Part)): - return True - if isinstance(item, (list, Content)): - return False - if isinstance(item, dict): - if "role" in item or "parts" in item: - return False - # Part objects that came in as plain dicts - return bool(_PART_DICT_KEYS & item.keys()) - # File objects - if hasattr(item, "uri"): - return True - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return True - return False - - -def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": - """Convert a single part-like item to a content part dict.""" - if isinstance(item, str): - return {"text": item, "type": "text"} - - # Handle bare inline_data dicts directly to preserve the raw format - if isinstance(item, dict) and "inline_data" in item: - return { - "inline_data": { - "mime_type": item["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - - # For other dicts and Part objects, use existing _extract_part_content - result = _extract_part_content(item) - if result is not None: - return result - - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return _extract_pil_image(item) - - # File objects - if hasattr(item, "uri") and hasattr(item, "mime_type"): - file_uri = getattr(item, "uri", None) - mime_type = getattr(item, "mime_type", None) or "" - if file_uri is not None: - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - return None - - -def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": - """Extract text from contents parameter which can have various formats. - - This is a compatibility function that extracts text from messages. - For new code, use extract_contents_messages instead. - """ - messages = extract_contents_messages(contents) - if not messages: - return None - - texts = [] - for message in messages: - content = message.get("content") - if isinstance(content, str): - texts.append(content) - elif isinstance(content, list): - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - texts.append(part.get("text", "")) - - return " ".join(texts) if texts else None - - -def _format_tools_for_span( - tools: "Iterable[Tool | Callable[..., Any]]", -) -> "Optional[List[dict[str, Any]]]": - """Format tools parameter for span data.""" - formatted_tools = [] - for tool in tools: - if callable(tool): - # Handle callable functions passed directly - formatted_tools.append( - { - "name": getattr(tool, "__name__", "unknown"), - "description": getattr(tool, "__doc__", None), - } - ) - elif ( - hasattr(tool, "function_declarations") - and tool.function_declarations is not None - ): - # Tool object with function declarations - for func_decl in tool.function_declarations: - formatted_tools.append( - { - "name": getattr(func_decl, "name", None), - "description": getattr(func_decl, "description", None), - } - ) - else: - # Check for predefined tool attributes - each of these tools - # is an attribute of the tool object, by default set to None - for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): - if getattr(tool, attr_name, None): - formatted_tools.append( - { - "name": attr_name, - "description": description, - } - ) - break - - return formatted_tools if formatted_tools else None - - -def extract_tool_calls( - response: "GenerateContentResponse", -) -> "Optional[List[dict[str, Any]]]": - """Extract tool/function calls from response candidates and automatic function calling history.""" - - tool_calls = [] - - # Extract from candidates, sometimes tool calls are nested under the content.parts object - if getattr(response, "candidates", []): - for candidate in response.candidates: - if not hasattr(candidate, "content") or not getattr( - candidate.content, "parts", [] - ): - continue - - for part in candidate.content.parts: - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if getattr(function_call, "args", None): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - # Extract from automatic_function_calling_history - # This is the history of tool calls made by the model - if getattr(response, "automatic_function_calling_history", None): - for content in response.automatic_function_calling_history: - if not getattr(content, "parts", None): - continue - - for part in getattr(content, "parts", []): - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if hasattr(function_call, "args"): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - return tool_calls if tool_calls else None - - -def _capture_tool_input( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" -) -> "dict[str, Any]": - """Capture tool input from args and kwargs.""" - tool_input = kwargs.copy() if kwargs else {} - - # If we have positional args, try to map them to the function signature - if args: - try: - sig = inspect.signature(tool) - param_names = list(sig.parameters.keys()) - for i, arg in enumerate(args): - if i < len(param_names): - tool_input[param_names[i]] = arg - except Exception: - # Fallback if we can't get the signature - tool_input["args"] = args - - return tool_input - - -def _create_tool_span( - tool_name: str, tool_doc: "Optional[str]" -) -> "Union[Span, StreamedSpan]": - """Create a span for tool execution.""" - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - if tool_doc: - span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=ORIGIN, - ) - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - if tool_doc: - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - -def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": - """Wrap a tool to emit execute_tool spans when called.""" - if not callable(tool): - # Not a callable function, return as-is (predefined tools) - return tool - - tool_name = getattr(tool, "__name__", "unknown") - tool_doc = tool.__doc__ - - if inspect.iscoroutinefunction(tool): - # Async function - @wraps(tool) - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = await tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return async_wrapped - else: - # Sync function - @wraps(tool) - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return sync_wrapped - - -def wrapped_config_with_tools( - config: "GenerateContentConfig", -) -> "GenerateContentConfig": - """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as - callable functions as a part of the config object.""" - - if not config or not getattr(config, "tools", None): - return config - - result = copy.copy(config) - result.tools = [wrapped_tool(tool) for tool in config.tools] - - return result - - -def _extract_response_text( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract text from response candidates.""" - - if not response or not getattr(response, "candidates", []): - return None - - texts = [] - for candidate in response.candidates: - if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): - continue - - if candidate.content is None or candidate.content.parts is None: - continue - - for part in candidate.content.parts: - if getattr(part, "text", None): - texts.append(part.text) - - return texts if texts else None - - -def extract_finish_reasons( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract finish reasons from response candidates.""" - if not response or not getattr(response, "candidates", []): - return None - - finish_reasons = [] - for candidate in response.candidates: - if getattr(candidate, "finish_reason", None): - # Convert enum value to string if necessary - reason = str(candidate.finish_reason) - # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") - if "." in reason: - reason = reason.split(".")[-1] - finish_reasons.append(reason) - - return finish_reasons if finish_reasons else None - - -def _transform_system_instruction_one_level( - system_instructions: "Union[ContentUnionDict, ContentUnion]", - can_be_content: bool, -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, str): - return [{"type": "text", "content": system_instructions}] - - if isinstance(system_instructions, Part) and system_instructions.text: - return [{"type": "text", "content": system_instructions.text}] - - if can_be_content and isinstance(system_instructions, Content): - if isinstance(system_instructions.parts, list): - for part in system_instructions.parts: - if isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - return text_parts - - if isinstance(system_instructions, dict) and system_instructions.get("text"): - return [{"type": "text", "content": system_instructions["text"]}] - - elif can_be_content and isinstance(system_instructions, dict): - parts = system_instructions.get("parts", []) - for part in parts: - if isinstance(part, Part) and isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - elif isinstance(part, dict) and isinstance(part.get("text"), str): - text_parts.append({"type": "text", "content": part["text"]}) - return text_parts - - return text_parts - - -def _transform_system_instructions( - system_instructions: "Union[ContentUnionDict, ContentUnion]", -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, list): - text_parts = list( - chain.from_iterable( - _transform_system_instruction_one_level( - instructions, can_be_content=False - ) - for instructions in system_instructions - ) - ) - - return text_parts - - return _transform_system_instruction_one_level( - system_instructions, can_be_content=True - ) - - -def set_span_data_for_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - model: str, - contents: "ContentListUnion", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for the request.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if kwargs.get("stream", False): - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - config: "Optional[GenerateContentConfig]" = kwargs.get("config") - - # Set input messages/prompts if PII is allowed - if should_send_default_pii() and integration.include_prompts: - messages = [] - - # Add system instruction if present - system_instructions = None - if config and hasattr(config, "system_instruction"): - system_instructions = config.system_instruction - elif isinstance(config, dict) and "system_instruction" in config: - system_instructions = config.get("system_instruction") - - if system_instructions is not None: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - # Extract messages from contents - contents_messages = extract_contents_messages(contents) - messages.extend(contents_messages) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Extract parameters directly from config (not nested under generation_config) - for param, span_key in [ - ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), - ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), - ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), - ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), - ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), - ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), - ("seed", SPANDATA.GEN_AI_REQUEST_SEED), - ]: - if hasattr(config, param): - value = getattr(config, param) - if value is not None: - set_on_span(span_key, value) - - # Set tools if available - if config is not None and hasattr(config, "tools"): - tools = config.tools - if tools: - formatted_tools = _format_tools_for_span(tools) - if formatted_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - formatted_tools, - unpack=False, - ) - - -def set_span_data_for_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "GenerateContentResponse", -) -> None: - """Set span data for the response.""" - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if should_send_default_pii() and integration.include_prompts: - response_texts = _extract_response_text(response) - if response_texts: - # Format as JSON string array as per documentation - set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) - - tool_calls = extract_tool_calls(response) - if tool_calls: - # Tool calls should be JSON serialized - set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) - - finish_reasons = extract_finish_reasons(response) - if finish_reasons: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons - ) - - if getattr(response, "response_id", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) - - if getattr(response, "model_version", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) - - usage_data = extract_usage_data(response) - - if usage_data["input_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) - - if usage_data["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage_data["input_tokens_cached"], - ) - - if usage_data["output_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) - - if usage_data["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage_data["output_tokens_reasoning"], - ) - - if usage_data["total_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) - - -def prepare_generate_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[Any, Any, str]": - """Extract and prepare common arguments for generate_content methods.""" - model = args[0] if args else kwargs.get("model", "unknown") - contents = args[1] if len(args) > 1 else kwargs.get("contents") - model_name = get_model_name(model) - - config = kwargs.get("config") - wrapped_config = wrapped_config_with_tools(config) - if wrapped_config is not config: - kwargs["config"] = wrapped_config - - return model, contents, model_name - - -def prepare_embed_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[str, Any]": - """Extract and prepare common arguments for embed_content methods. - - Returns: - tuple: (model_name, contents) - """ - model = kwargs.get("model", "unknown") - contents = kwargs.get("contents") - model_name = get_model_name(model) - - return model_name, contents - - -def set_span_data_for_embed_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - contents: "Any", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for embedding request.""" - # Include input contents if PII is allowed - if should_send_default_pii() and integration.include_prompts: - if contents: - # For embeddings, contents is typically a list of strings/texts - input_texts = [] - - # Handle various content formats - if isinstance(contents, str): - input_texts = [contents] - elif isinstance(contents, list): - for item in contents: - text = extract_contents_text(item) - if text: - input_texts.append(text) - else: - text = extract_contents_text(contents) - if text: - input_texts = [text] - - if input_texts: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - input_texts, - unpack=False, - ) - - -def set_span_data_for_embed_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "EmbedContentResponse", -) -> None: - """Set span data for embedding response.""" - if not response: - return - - # Extract token counts from embeddings statistics (Vertex AI only) - # Each embedding has its own statistics with token_count - if hasattr(response, "embeddings") and response.embeddings: - total_tokens = 0 - - for embedding in response.embeddings: - if hasattr(embedding, "statistics") and embedding.statistics: - token_count = getattr(embedding.statistics, "token_count", None) - if token_count is not None: - total_tokens += int(token_count) - - # Set token count if we found any - if total_tokens > 0: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) - else: - span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py deleted file mode 100644 index dcb60e9561..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/gql.py +++ /dev/null @@ -1,173 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -try: - import gql # type: ignore[import-not-found] - from gql.transport import ( # type: ignore[import-not-found] - AsyncTransport, - Transport, - ) - from gql.transport.exceptions import ( # type: ignore[import-not-found] - TransportQueryError, - ) - from graphql import ( - DocumentNode, - VariableDefinitionNode, - get_operation_ast, - print_ast, - ) - - try: - # gql 4.0+ - from gql import GraphQLRequest - except ImportError: - GraphQLRequest = None - -except ImportError: - raise DidNotEnable("gql is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Tuple, Union - - from sentry_sdk._types import Event, EventProcessor - - EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] - - -class GQLIntegration(Integration): - identifier = "gql" - - @staticmethod - def setup_once() -> None: - gql_version = parse_version(gql.__version__) - _check_minimum_version(GQLIntegration, gql_version) - - _patch_execute() - - -def _data_from_document(document: "DocumentNode") -> "EventDataType": - try: - operation_ast = get_operation_ast(document) - data: "EventDataType" = {"query": print_ast(document)} - - if operation_ast is not None: - data["variables"] = operation_ast.variable_definitions - if operation_ast.name is not None: - data["operationName"] = operation_ast.name.value - - return data - except (AttributeError, TypeError): - return dict() - - -def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: - """ - The RequestsHTTPTransport allows defining the HTTP method; all - other transports use POST. - """ - try: - return transport.method - except AttributeError: - return "POST" - - -def _request_info_from_transport( - transport: "Union[Transport, AsyncTransport, None]", -) -> "Dict[str, str]": - if transport is None: - return {} - - request_info = { - "method": _transport_method(transport), - } - - try: - request_info["url"] = transport.url - except AttributeError: - pass - - return request_info - - -def _patch_execute() -> None: - real_execute = gql.Client.execute - - # Maintain signature for backwards compatibility. - # gql.Client.execute() accepts a positional-only "request" - # parameter with version 4.0.0. - @ensure_integration_enabled(GQLIntegration, real_execute) - def sentry_patched_execute( - self: "gql.Client", - document: "DocumentNode", - *args: "Any", - **kwargs: "Any", - ) -> "Any": - scope = sentry_sdk.get_isolation_scope() - # document is a gql.GraphQLRequest with gql v4.0.0. - scope.add_event_processor(_make_gql_event_processor(self, document)) - - try: - # document is a gql.GraphQLRequest with gql v4.0.0. - return real_execute(self, document, *args, **kwargs) - except TransportQueryError as e: - event, hint = event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "gql", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint) - raise e - - gql.Client.execute = sentry_patched_execute - - -def _make_gql_event_processor( - client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" -) -> "EventProcessor": - def processor(event: "Event", hint: "dict[str, Any]") -> "Event": - try: - errors = hint["exc_info"][1].errors - except (AttributeError, KeyError): - errors = None - - request = event.setdefault("request", {}) - request.update( - { - "api_target": "graphql", - **_request_info_from_transport(client.transport), - } - ) - - if should_send_default_pii(): - if GraphQLRequest is not None and isinstance( - document_or_request, GraphQLRequest - ): - # In v4.0.0, gql moved to using GraphQLRequest instead of - # DocumentNode in execute - # https://github.com/graphql-python/gql/pull/556 - document = document_or_request.document - else: - document = document_or_request - - request["data"] = _data_from_document(document) - contexts = event.setdefault("contexts", {}) - response = contexts.setdefault("response", {}) - response.update( - { - "data": {"errors": errors}, - "type": response, - } - ) - - return event - - return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py deleted file mode 100644 index 4938a5c0f3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/graphene.py +++ /dev/null @@ -1,181 +0,0 @@ -from contextlib import contextmanager - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from graphene.types import schema as graphene_schema # type: ignore -except ImportError: - raise DidNotEnable("graphene is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Generator - from typing import Any, Dict, Union - - from graphene.language.source import Source # type: ignore - from graphql.execution import ExecutionResult - from graphql.type import GraphQLSchema - - from sentry_sdk._types import Event - - -class GrapheneIntegration(Integration): - identifier = "graphene" - - @staticmethod - def setup_once() -> None: - version = package_version("graphene") - _check_minimum_version(GrapheneIntegration, version) - - _patch_graphql() - - -def _patch_graphql() -> None: - old_graphql_sync = graphene_schema.graphql_sync - old_graphql_async = graphene_schema.graphql - - @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) - def _sentry_patched_graphql_sync( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = old_graphql_sync(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - async def _sentry_patched_graphql_async( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) - if integration is None: - return await old_graphql_async(schema, source, *args, **kwargs) - - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = await old_graphql_async(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - graphene_schema.graphql_sync = _sentry_patched_graphql_sync - graphene_schema.graphql = _sentry_patched_graphql_async - - -def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - if should_send_default_pii(): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - -@contextmanager -def graphql_span( - schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" -) -> "Generator[None, None, None]": - operation_name = kwargs.get("operation_name") or "" - - operation_type = "query" - op = OP.GRAPHQL_QUERY - if source.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif source.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - sentry_sdk.add_breadcrumb( - crumb={ - "data": { - "operation_name": operation_name, - "operation_type": operation_type, - }, - "category": "graphql.operation", - }, - ) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - additional_attributes = {} - if should_send_default_pii(): - additional_attributes["graphql.document"] = source - - _graphql_span = sentry_sdk.traces.start_span( - name=operation_name, - attributes={ - "sentry.op": op, - "graphql.operation.name": operation_name, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) - - if should_send_default_pii(): - _graphql_span.set_data("graphql.document", source) - _graphql_span.set_data("graphql.operation.name", operation_name) - _graphql_span.set_data("graphql.operation.type", operation_type) - - _graphql_span.__enter__() - - try: - yield - finally: - if is_span_streaming_enabled: - _graphql_span.end() # type: ignore - else: - _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py deleted file mode 100644 index bf74ff1351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/__init__.py +++ /dev/null @@ -1,188 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any, Optional, Sequence - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .client import ClientInterceptor -from .server import ServerInterceptor - -try: - import grpc - from grpc import Channel, Server, intercept_channel - from grpc.aio import Channel as AsyncChannel - from grpc.aio import Server as AsyncServer - - from .aio.client import ( - SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, - ) - from .aio.client import ( - SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, - ) - from .aio.server import ServerInterceptor as AsyncServerInterceptor -except ImportError: - raise DidNotEnable("grpcio is not installed.") - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from typing import Callable, ParamSpec -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - -P = ParamSpec("P") - -GRPC_VERSION = parse_version(grpc.__version__) - - -def _is_channel_intercepted(channel: "Channel") -> bool: - interceptor = getattr(channel, "_interceptor", None) - while interceptor is not None: - if isinstance(interceptor, ClientInterceptor): - return True - - inner_channel = getattr(channel, "_channel", None) - if inner_channel is None: - return False - - channel = inner_channel - interceptor = getattr(channel, "_interceptor", None) - - return False - - -def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - "Wrapper for synchronous secure and insecure channel." - - @wraps(func) - def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": - channel = func(*args, **kwargs) - if not _is_channel_intercepted(channel): - return intercept_channel(channel, ClientInterceptor()) - else: - return channel - - return patched_channel - - -def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - @wraps(func) - def patched_intercept_channel( - channel: "Channel", *interceptors: "grpc.ServerInterceptor" - ) -> "Channel": - if _is_channel_intercepted(channel): - interceptors = tuple( - [ - interceptor - for interceptor in interceptors - if not isinstance(interceptor, ClientInterceptor) - ] - ) - else: - interceptors = interceptors - return intercept_channel(channel, *interceptors) - - return patched_intercept_channel # type: ignore - - -def _wrap_channel_async( - func: "Callable[P, AsyncChannel]", -) -> "Callable[P, AsyncChannel]": - "Wrapper for asynchronous secure and insecure channel." - - @wraps(func) - def patched_channel( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Channel": - sentry_interceptors = [ - AsyncUnaryUnaryClientInterceptor(), - AsyncUnaryStreamClientIntercetor(), - ] - interceptors = [*sentry_interceptors, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_channel # type: ignore - - -def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": - """Wrapper for synchronous server.""" - - @wraps(func) - def patched_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - interceptors = [ - interceptor - for interceptor in interceptors or [] - if not isinstance(interceptor, ServerInterceptor) - ] - server_interceptor = ServerInterceptor() - interceptors = [server_interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_server # type: ignore - - -def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": - """Wrapper for asynchronous server.""" - - @wraps(func) - def patched_aio_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - server_interceptor = AsyncServerInterceptor() - interceptors = [ - server_interceptor, - *(interceptors or []), - ] - - try: - # We prefer interceptors as a list because of compatibility with - # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 - # However, prior to grpc 1.42.0, only tuples were accepted, so we - # have no choice there. - if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): - interceptors = tuple(interceptors) - except Exception: - pass - - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_aio_server # type: ignore - - -class GRPCIntegration(Integration): - identifier = "grpc" - - @staticmethod - def setup_once() -> None: - import grpc - - grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) - grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) - grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) - - grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) - grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) - - grpc.server = _wrap_sync_server(grpc.server) - grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py deleted file mode 100644 index 4d21815254..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .client import ClientInterceptor -from .server import ServerInterceptor - -__all__ = [ - "ClientInterceptor", - "ServerInterceptor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py deleted file mode 100644 index d07b7f19be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/client.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import Any, AsyncIterable, Callable, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.protobuf.message import Message - from grpc.aio import ( - ClientCallDetails, - Metadata, - UnaryStreamCall, - UnaryStreamClientInterceptor, - UnaryUnaryCall, - UnaryUnaryClientInterceptor, - ) -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor: - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - if client_call_details.metadata is None: - client_call_details = client_call_details._replace(metadata=Metadata()) - elif not isinstance(client_call_details.metadata, Metadata): - # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 - # See https://github.com/grpc/grpc/issues/34298. - client_call_details = client_call_details._replace( - metadata=Metadata.from_tuple(client_call_details.metadata) - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - client_call_details.metadata.add(key, value) - return client_call_details - - -class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore - async def intercept_unary_unary( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[UnaryUnaryCall, Message]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_data("code", status_code.name) - - return response - - -class SentryUnaryStreamClientInterceptor( - ClientInterceptor, - UnaryStreamClientInterceptor, # type: ignore -): - async def intercept_unary_stream( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - # status_code = await response.code() - # span.set_data("code", status_code) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py deleted file mode 100644 index 010337e98c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/aio/server.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from typing import Any, Optional - - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler - from grpc.aio import AbortError, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Callable[[ServicerContext], str] | None" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - async def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", - handler_call_details: "HandlerCallDetails", - ) -> "Optional[Awaitable[RpcMethodHandler]]": - handler = await continuation(handler_call_details) - if handler is None: - return None - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - if not handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.unary_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - with sentry_sdk.isolation_scope(): - name = ( - custom_find_name(context) if custom_find_name else method_name - ) - if not name: - return await handler(request, context) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - # What if the headers are empty? - sentry_sdk.traces.continue_trace( - dict(context.invocation_metadata()) - ) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - else: - # What if the headers are empty? - transaction = sentry_sdk.continue_trace( - dict(context.invocation_metadata()), - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - - elif not handler.request_streaming and handler.response_streaming: - handler_factory = grpc.unary_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.unary_stream(request, context): - yield r - - elif handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.stream_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - response = handler.stream_unary(request, context) - return await response - - elif handler.request_streaming and handler.response_streaming: - handler_factory = grpc.stream_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.stream_stream(request, context): - yield r - - return handler_factory( - wrapped, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py deleted file mode 100644 index 5384a0a78f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/client.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Iterator, Union - -try: - import grpc - from google.protobuf.message import Message - from grpc import Call, ClientCallDetails - from grpc._interceptor import _UnaryOutcome - from grpc.aio._interceptor import UnaryStreamCall -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor( - grpc.UnaryUnaryClientInterceptor, # type: ignore - grpc.UnaryStreamClientInterceptor, # type: ignore -): - def intercept_unary_unary( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "_UnaryOutcome": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_attribute( - SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name - ) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_data("code", response.code().name) - - return response - - def intercept_unary_stream( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[Iterator[Message], Call]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - response: "UnaryStreamCall" - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - metadata = ( - list(client_call_details.metadata) if client_call_details.metadata else [] - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - metadata.append((key, value)) - - client_call_details = grpc._interceptor._ClientCallDetails( - method=client_call_details.method, - timeout=client_call_details.timeout, - metadata=metadata, - credentials=client_call_details.credentials, - wait_for_ready=client_call_details.wait_for_ready, - compression=client_call_details.compression, - ) - - return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py deleted file mode 100644 index 9fdb975caf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py deleted file mode 100644 index 1cba1d4b85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/grpc/server.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Callable, Optional - - from google.protobuf.message import Message - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Optional[Callable[[ServicerContext], str]]" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", - handler_call_details: "HandlerCallDetails", - ) -> "RpcMethodHandler": - handler = continuation(handler_call_details) - if not handler or not handler.unary_unary: - return handler - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - def behavior(request: "Message", context: "ServicerContext") -> "Message": - with sentry_sdk.isolation_scope(): - name = custom_find_name(context) if custom_find_name else method_name - - if name: - metadata = dict(context.invocation_metadata()) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(metadata) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - transaction = sentry_sdk.continue_trace( - metadata, - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - return handler.unary_unary(request, context) - - return grpc.unary_unary_rpc_method_handler( - behavior, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py deleted file mode 100644 index a68f20b299..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx is not installed") - -__all__ = ["HttpxIntegration"] - - -class HttpxIntegration(Integration): - identifier = "httpx" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx_client() - _install_httpx_async_client() - - -def _install_httpx_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(HttpxIntegration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(HttpxIntegration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py deleted file mode 100644 index 25062aaa11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/httpx2.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx2 import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx2 is not installed") - -__all__ = ["Httpx2Integration"] - - -class Httpx2Integration(Integration): - identifier = "httpx2" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx2 has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx2_client() - _install_httpx2_async_client() - - -def _install_httpx2_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(Httpx2Integration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx2_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(Httpx2Integration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py deleted file mode 100644 index c3bbc8abcf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huey.py +++ /dev/null @@ -1,239 +0,0 @@ -import sys -from datetime import datetime -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, Hint - from sentry_sdk.utils import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - -try: - from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task - from huey.exceptions import CancelExecution, RetryTask, TaskLockedException -except ImportError: - raise DidNotEnable("Huey is not installed") - -try: - from huey.api import chord as HueyChord - from huey.api import group as HueyGroup -except ImportError: - HueyChord = None - HueyGroup = None - - -HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) - - -class HueyIntegration(Integration): - identifier = "huey" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_enqueue() - patch_execute() - _register_control_flow_exception( - [CancelExecution, RetryTask, TaskLockedException] - ) - - -def patch_enqueue() -> None: - old_enqueue = Huey.enqueue - - @ensure_integration_enabled(HueyIntegration, old_enqueue) - def _sentry_enqueue( - self: "Huey", item: "Any" - ) -> "Optional[Union[Result, ResultGroup]]": - if HueyChord is not None and isinstance(item, HueyChord): - span_name = "Huey Chord" - elif HueyGroup is not None and isinstance(item, HueyGroup): - span_name = "Huey Task Group" - else: - span_name = item.name - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - span_ctx = None - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_HUEY, - "sentry.origin": HueyIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_HUEY, - name=span_name, - origin=HueyIntegration.origin, - ) - - no_headers_types = (PeriodicTask,) + tuple( - t for t in [HueyGroup, HueyChord] if t is not None - ) - with span_ctx: - if not isinstance(item, no_headers_types): - # Attach trace propagation data to task kwargs. We do - # not do this for periodic tasks, as these don't - # really have an originating transaction. - # Additionally, we do not do this for Huey groups or chords, as enqueue will - # recursively call this method for each task within the list, resulting - # in the trace propagation data being attached to each task individually - # (which we want) - item.kwargs["sentry_headers"] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - return old_enqueue(self, item) - - Huey.enqueue = _sentry_enqueue - - -def _make_event_processor(task: "Any") -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["huey_task_id"] = task.id - tags["huey_task_retry"] = task.default_retries > task.retries - extra = event.setdefault("extra", {}) - extra["huey-job"] = { - "task": task.name, - "args": ( - task.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - task.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": (task.default_retries or 0) - task.retries, - } - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.ABORTED) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.OK - return - - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.ERROR - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": HueyIntegration.identifier, "handled": False}, - ) - scope.capture_event(event, hint=hint) - - -def _wrap_task_execute(func: "F") -> "F": - @ensure_integration_enabled(HueyIntegration, func) - def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": - try: - result = func(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_execute # type: ignore - - -def patch_execute() -> None: - old_execute = Huey._execute - - @ensure_integration_enabled(HueyIntegration, old_execute) - def _sentry_execute( - self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None - ) -> "Any": - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope._name = "huey" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task)) - - sentry_headers = task.kwargs.pop("sentry_headers", None) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - headers = sentry_headers or {} - sentry_sdk.traces.continue_trace(headers) - span_ctx = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_TASK_HUEY, - "sentry.origin": HueyIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - "messaging.message.id": task.id, - "messaging.message.system": "huey", - "messaging.message.retry.count": (task.default_retries or 0) - - task.retries, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - sentry_headers or {}, - name=task.name, - op=OP.QUEUE_TASK_HUEY, - source=TransactionSource.TASK, - origin=HueyIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - span_ctx = sentry_sdk.start_transaction(transaction) - - if not getattr(task, "_sentry_is_patched", False): - task.execute = _wrap_task_execute(task.execute) - task._sentry_is_patched = True - - with span_ctx: - return old_execute(self, task, timestamp) - - Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py deleted file mode 100644 index 835acc7279..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/huggingface_hub.py +++ /dev/null @@ -1,392 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - _set_span_data_attribute, - get_start_span_function, - set_data_normalized, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Union - - from sentry_sdk.tracing import Span - -try: - import huggingface_hub.inference._client -except ImportError: - raise DidNotEnable("Huggingface not installed") - - -class HuggingfaceHubIntegration(Integration): - identifier = "huggingface_hub" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "HuggingfaceHubIntegration", include_prompts: bool = True - ) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks - huggingface_hub.inference._client.InferenceClient.text_generation = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.text_generation, - OP.GEN_AI_TEXT_COMPLETION, - ) - ) - huggingface_hub.inference._client.InferenceClient.chat_completion = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.chat_completion, - OP.GEN_AI_CHAT, - ) - ) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "huggingface_hub", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": - @wraps(f) - def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) - if integration is None: - return f(*args, **kwargs) - - prompt = None - if "prompt" in kwargs: - prompt = kwargs["prompt"] - elif "messages" in kwargs: - prompt = kwargs["messages"] - elif len(args) >= 2: - if isinstance(args[1], str) or isinstance(args[1], list): - prompt = args[1] - - if prompt is None: - # invalid call, dont instrument, let it return error - return f(*args, **kwargs) - - client = args[0] - model = client.model or kwargs.get("model") or "" - operation_name = op.split(".")[-1] - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(sentry_sdk.get_client().options): - span = sentry_sdk.traces.start_span( - name=f"{operation_name} {model}", - attributes={ - "sentry.op": op, - "sentry.origin": HuggingfaceHubIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=op, - name=f"{operation_name} {model}", - origin=HuggingfaceHubIntegration.origin, - ) - span.__enter__() - - _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - - if model: - _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) - - # Input attributes - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False - ) - - attribute_mapping = { - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - } - - for attribute, span_attribute in attribute_mapping.items(): - value = kwargs.get(attribute, None) - if value is not None: - if isinstance(value, (int, float, bool, str)): - _set_span_data_attribute(span, span_attribute, value) - else: - set_data_normalized(span, span_attribute, value, unpack=False) - - # LLM Execution - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Output attributes - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - tool_calls = None - usage = None - - with capture_internal_exceptions(): - if isinstance(res, str) and res is not None: - response_text_buffer.append(res) - - if hasattr(res, "generated_text") and res.generated_text is not None: - response_text_buffer.append(res.generated_text) - - if hasattr(res, "model") and res.model is not None: - response_model = res.model - - if hasattr(res, "details") and hasattr(res.details, "finish_reason"): - finish_reason = res.details.finish_reason - - if ( - hasattr(res, "details") - and hasattr(res.details, "generated_tokens") - and res.details.generated_tokens is not None - ): - tokens_used = res.details.generated_tokens - - if hasattr(res, "usage") and res.usage is not None: - usage = res.usage - - if hasattr(res, "choices") and res.choices is not None: - for choice in res.choices: - if hasattr(choice, "finish_reason"): - finish_reason = choice.finish_reason - if hasattr(choice, "message") and hasattr( - choice.message, "tool_calls" - ): - tool_calls = choice.message.tool_calls - if ( - hasattr(choice, "message") - and hasattr(choice.message, "content") - and choice.message.content is not None - ): - response_text_buffer.append(choice.message.content) - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - elif tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - # If the response is not a generator (meaning a streaming response) - # we are done and can return the response - if not inspect.isgenerator(res): - span.__exit__(None, None, None) - return res - - if kwargs.get("details", False): - # text-generation stream output - def new_details_iterator() -> "Iterable[Any]": - finish_reason = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - - with capture_internal_exceptions(): - for chunk in res: - if ( - hasattr(chunk, "token") - and hasattr(chunk.token, "text") - and chunk.token.text is not None - ): - response_text_buffer.append(chunk.token.text) - - if hasattr(chunk, "details") and hasattr( - chunk.details, "finish_reason" - ): - finish_reason = chunk.details.finish_reason - - if ( - hasattr(chunk, "details") - and hasattr(chunk.details, "generated_tokens") - and chunk.details.generated_tokens is not None - ): - tokens_used = chunk.details.generated_tokens - - yield chunk - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - span.__exit__(None, None, None) - - return new_details_iterator() - - else: - # chat-completion stream output - def new_iterator() -> "Iterable[str]": - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tool_calls = None - usage = None - - with capture_internal_exceptions(): - for chunk in res: - if hasattr(chunk, "model") and chunk.model is not None: - response_model = chunk.model - - if hasattr(chunk, "usage") and chunk.usage is not None: - usage = chunk.usage - - if isinstance(chunk, str): - if chunk is not None: - response_text_buffer.append(chunk) - - if hasattr(chunk, "choices") and chunk.choices is not None: - for choice in chunk.choices: - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "content") - and choice.delta.content is not None - ): - response_text_buffer.append( - choice.delta.content - ) - - if ( - hasattr(choice, "finish_reason") - and choice.finish_reason is not None - ): - finish_reason = choice.finish_reason - - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "tool_calls") - and choice.delta.tool_calls is not None - ): - tool_calls = choice.delta.tool_calls - - yield chunk - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - - span.__exit__(None, None, None) - - return new_iterator() - - return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py deleted file mode 100644 index 9dcbb189ce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langchain.py +++ /dev/null @@ -1,1412 +0,0 @@ -import itertools -import json -import sys -import warnings -from collections import OrderedDict -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - _get_value, - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import capture_internal_exceptions, logger - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Dict, - Iterator, - List, - Optional, - Union, - ) - from uuid import UUID - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - - -try: - from langchain_core.agents import AgentFinish - from langchain_core.callbacks import ( - BaseCallbackHandler, - BaseCallbackManager, - Callbacks, - manager, - ) - from langchain_core.messages import BaseMessage - from langchain_core.outputs import LLMResult - -except ImportError: - raise DidNotEnable("langchain not installed") - - -try: - # >=v1 - from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] -except ImportError: - try: - # "Optional[str]": - ai_type = all_params.get("_type") - - if not ai_type or not isinstance(ai_type, str): - return None - - return ai_type - - -DATA_FIELDS = { - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, -} - - -def _transform_langchain_content_block( - content_block: "Dict[str, Any]", -) -> "Dict[str, Any]": - """ - Transform a LangChain content block using the shared transform_content_part function. - - Returns the original content block if transformation is not applicable - (e.g., for text blocks or unrecognized formats). - """ - result = transform_content_part(content_block) - return result if result is not None else content_block - - -def _transform_langchain_message_content(content: "Any") -> "Any": - """ - Transform LangChain message content, handling both string content and - list of content blocks. - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for block in content: - if isinstance(block, dict): - transformed.append(_transform_langchain_content_block(block)) - else: - transformed.append(block) - return transformed - - return content - - -def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": - system_instructions = [] - - for list_ in messages: - for message in list_: - # type of content: str | list[str | dict] | None - if message.type == "system" and isinstance(message.content, str): - system_instructions.append(message.content) - - elif message.type == "system" and isinstance(message.content, list): - for item in message.content: - if isinstance(item, str): - system_instructions.append(item) - - elif isinstance(item, dict) and item.get("type") == "text": - instruction = item.get("text") - if isinstance(instruction, str): - system_instructions.append(instruction) - - return system_instructions - - -def _transform_system_instructions( - system_instructions: "List[str]", -) -> "List[TextPart]": - return [ - { - "type": "text", - "content": instruction, - } - for instruction in system_instructions - ] - - -class LangchainIntegration(Integration): - identifier = "langchain" - origin = f"auto.ai.{identifier}" - - _ignored_exceptions: "set[type[Exception]]" = set() - - def __init__( - self: "LangchainIntegration", - include_prompts: bool = True, - max_spans: "Optional[int]" = None, - ) -> None: - self.include_prompts = include_prompts - self.max_spans = max_spans - - if max_spans is not None: - warnings.warn( - "The `max_spans` parameter of `LangchainIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - manager._configure = _wrap_configure(manager._configure) - - if AgentExecutor is not None: - AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) - AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) - - # Patch embeddings providers - _patch_embeddings_provider(OpenAIEmbeddings) - _patch_embeddings_provider(AzureOpenAIEmbeddings) - _patch_embeddings_provider(VertexAIEmbeddings) - _patch_embeddings_provider(BedrockEmbeddings) - _patch_embeddings_provider(CohereEmbeddings) - _patch_embeddings_provider(MistralAIEmbeddings) - _patch_embeddings_provider(HuggingFaceEmbeddings) - _patch_embeddings_provider(OllamaEmbeddings) - - -class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] - """Callback handler that creates Sentry spans.""" - - def __init__( - self, max_span_map_size: "Optional[int]", include_prompts: bool - ) -> None: - self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() - self.max_span_map_size = max_span_map_size - self.include_prompts = include_prompts - - def gc_span_map(self) -> None: - if self.max_span_map_size is not None: - while len(self.span_map) > self.max_span_map_size: - run_id, span = self.span_map.popitem(last=False) - self._exit_span(span, run_id) - - def _handle_error(self, run_id: "UUID", error: "Any") -> None: - is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) - - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if is_ignored: - span.__exit__(None, None, None) - else: - sentry_sdk.capture_exception( - error, span._scope if isinstance(span, StreamedSpan) else span.scope - ) - span.__exit__(type(error), error, error.__traceback__) - - del self.span_map[run_id] - - def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": - # Transform content to handle multimodal data (images, audio, video, files) - transformed_content = _transform_langchain_message_content(message.content) - parsed = {"role": message.type, "content": transformed_content} - parsed.update(message.additional_kwargs) - return parsed - - def _create_span( - self: "SentryLangchainCallback", - run_id: "UUID", - parent_id: "Optional[Any]", - op: str, - name: str, - origin: str, - ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span = None - if parent_id: - parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( - self.span_map.get(parent_id) - ) - if parent_span: - span = ( - sentry_sdk.traces.start_span( - parent_span=parent_span, - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if isinstance(parent_span, StreamedSpan) - else parent_span.start_child(op=op, name=name, origin=origin) - ) - - if span is None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span = ( - sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if span_streaming - else sentry_sdk.start_span(op=op, name=name, origin=origin) - ) - - span.__enter__() - self.span_map[run_id] = span - self.gc_span_map() - return span - - def _exit_span( - self: "SentryLangchainCallback", - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - run_id: "UUID", - ) -> None: - span.__exit__(None, None, None) - del self.span_map[run_id] - - def on_llm_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - prompts: "List[str]", - *, - run_id: "UUID", - tags: "Optional[List[str]]" = None, - parent_run_id: "Optional[UUID]" = None, - metadata: "Optional[Dict[str, Any]]" = None, - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - parent_run_id, - op=OP.GEN_AI_TEXT_COMPLETION, - name=f"text_completion {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") - - run_name = kwargs.get("name") - if run_name: - set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - if model: - set_on_span( - SPANDATA.GEN_AI_REQUEST_MODEL, - model, - ) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - normalized_messages = [ - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, - "content": {"type": "text", "text": prompt}, - } - for prompt in prompts - ] - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - messages: "List[List[BaseMessage]]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if model: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - system_instructions = _get_system_instructions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - normalized_messages = [] - for list_ in messages: - for message in list_: - if message.type == "system": - continue - - normalized_messages.append( - self._normalize_langchain_message(message) - ) - normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - try: - generation = response.generations[0][0] - except IndexError: - generation = None - - if generation is not None: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - - try: - response_model = generation.message.response_metadata.get( - "model_name" - ) - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - except AttributeError: - pass - - try: - finish_reason = generation.generation_info.get("finish_reason") - if finish_reason is not None: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - [finish_reason], - ) - except AttributeError: - pass - - try: - if should_send_default_pii() and self.include_prompts: - tool_calls = getattr(generation.message, "tool_calls", None) - if tool_calls is not None and tool_calls != []: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - except AttributeError: - pass - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM errors.""" - self._handle_error(run_id, error) - - def on_chat_model_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model errors.""" - self._handle_error(run_id, error) - - def on_agent_finish( - self: "SentryLangchainCallback", - finish: "AgentFinish", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() - ) - - self._exit_span(span, run_id) - - def on_tool_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - input_str: str, - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - tool_name = serialized.get("name") or kwargs.get("name") or "" - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - tool_description = serialized.get("description") - if tool_description is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_TOOL_INPUT, - kwargs.get("inputs", [input_str]), - ) - - def on_tool_end( - self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" - ) -> "Any": - """Run when tool ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) - - self._exit_span(span, run_id) - - def on_tool_error( - self, - error: "SentryLangchainCallback", - *args: "Union[Exception, KeyboardInterrupt]", - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool errors.""" - self._handle_error(run_id, error) - - -def _extract_tokens( - token_usage: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - if not token_usage: - return None, None, None - - input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( - token_usage, "input_tokens" - ) - output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( - token_usage, "output_tokens" - ) - total_tokens = _get_value(token_usage, "total_tokens") - - return input_tokens, output_tokens, total_tokens - - -def _extract_tokens_from_generations( - generations: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - """Extract token usage from response.generations structure.""" - if not generations: - return None, None, None - - total_input = 0 - total_output = 0 - total_total = 0 - - for gen_list in generations: - for gen in gen_list: - token_usage = _get_token_usage(gen) - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - total_input += input_tokens if input_tokens is not None else 0 - total_output += output_tokens if output_tokens is not None else 0 - total_total += total_tokens if total_tokens is not None else 0 - - return ( - total_input if total_input > 0 else None, - total_output if total_output > 0 else None, - total_total if total_total > 0 else None, - ) - - -def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": - """ - Check multiple paths to extract token usage from different objects. - """ - possible_names = ("usage", "token_usage", "usage_metadata") - - message = _get_value(obj, "message") - if message is not None: - for name in possible_names: - usage = _get_value(message, name) - if usage is not None: - return usage - - llm_output = _get_value(obj, "llm_output") - if llm_output is not None: - for name in possible_names: - usage = _get_value(llm_output, name) - if usage is not None: - return usage - - for name in possible_names: - usage = _get_value(obj, name) - if usage is not None: - return usage - - return None - - -def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: - token_usage = _get_token_usage(response) - if token_usage: - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - else: - input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( - response.generations - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) - - -def _get_request_data( - obj: "Any", args: "Any", kwargs: "Any" -) -> "tuple[Optional[str], Optional[List[Any]]]": - """ - Get the agent name and available tools for the agent. - """ - agent = getattr(obj, "agent", None) - runnable = getattr(agent, "runnable", None) - runnable_config = getattr(runnable, "config", {}) - tools = ( - getattr(obj, "tools", None) - or getattr(agent, "tools", None) - or runnable_config.get("tools") - or runnable_config.get("available_tools") - ) - tools = tools if tools and len(tools) > 0 else None - - try: - agent_name = None - if len(args) > 1: - agent_name = args[1].get("run_name") - if agent_name is None: - agent_name = runnable_config.get("run_name") - except Exception: - pass - - return (agent_name, tools) - - -def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": - """Parse and simplify tools into a cleaner format.""" - if not tools: - return None - - if not isinstance(tools, (list, tuple)): - return None - - simplified_tools = [] - for tool in tools: - try: - if isinstance(tool, dict): - if "function" in tool and isinstance(tool["function"], dict): - func = tool["function"] - simplified_tool = { - "name": func.get("name"), - "description": func.get("description"), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif "name" in tool: - simplified_tool = { - "name": tool.get("name"), - "description": tool.get("description"), - } - simplified_tools.append(simplified_tool) - else: - name = ( - tool.get("name") - or tool.get("tool_name") - or tool.get("function_name") - ) - if name: - simplified_tools.append( - { - "name": name, - "description": tool.get("description") - or tool.get("desc"), - } - ) - elif hasattr(tool, "name"): - simplified_tool = { - "name": getattr(tool, "name", None), - "description": getattr(tool, "description", None) - or getattr(tool, "desc", None), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif hasattr(tool, "__name__"): - simplified_tools.append( - { - "name": tool.__name__, - "description": getattr(tool, "__doc__", None), - } - ) - else: - tool_str = str(tool) - if tool_str and tool_str != "": - simplified_tools.append({"name": tool_str, "description": None}) - except Exception: - continue - - return simplified_tools if simplified_tools else None - - -def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: - """Set available tools data on a span if tools are provided.""" - if tools is not None: - simplified_tools = _simplify_langchain_tools(tools) - if simplified_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - simplified_tools, - unpack=False, - ) - - -def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_configure( - callback_manager_cls: type, - inheritable_callbacks: "Callbacks" = None, - local_callbacks: "Callbacks" = None, - *args: "Any", - **kwargs: "Any", - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(LangchainIntegration) - if integration is None: - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - local_callbacks = local_callbacks or [] - - # Handle each possible type of local_callbacks. For each type, we - # extract the list of callbacks to check for SentryLangchainCallback, - # and define a function that would add the SentryLangchainCallback - # to the existing callbacks list. - if isinstance(local_callbacks, BaseCallbackManager): - callbacks_list = local_callbacks.handlers - elif isinstance(local_callbacks, BaseCallbackHandler): - callbacks_list = [local_callbacks] - elif isinstance(local_callbacks, list): - callbacks_list = local_callbacks - else: - logger.debug("Unknown callback type: %s", local_callbacks) - # Just proceed with original function call - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - # Handle each possible type of inheritable_callbacks. - if isinstance(inheritable_callbacks, BaseCallbackManager): - inheritable_callbacks_list = inheritable_callbacks.handlers - elif isinstance(inheritable_callbacks, list): - inheritable_callbacks_list = inheritable_callbacks - else: - inheritable_callbacks_list = [] - - if not any( - isinstance(cb, SentryLangchainCallback) - for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) - ): - sentry_handler = SentryLangchainCallback( - integration.max_spans, - integration.include_prompts, - ) - if isinstance(local_callbacks, BaseCallbackManager): - local_callbacks = local_callbacks.copy() - local_callbacks.handlers = [ - *local_callbacks.handlers, - sentry_handler, - ] - elif isinstance(local_callbacks, BaseCallbackHandler): - local_callbacks = [local_callbacks, sentry_handler] - else: - local_callbacks = [*local_callbacks, sentry_handler] - - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - return new_configure - - -def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: False, - }, - ) as span: - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - else: - start_span_function = get_start_span_function() - - with start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) as span: - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - - return new_invoke - - -def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - else: - start_span_function = get_start_span_function() - - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - input = args[0].get("input") if len(args) >= 1 else None - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Run the agent - result = f(self, *args, **kwargs) - - old_iterator = result - - def new_iterator() -> "Iterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - async def new_iterator_async() -> "AsyncIterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - async for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - if str(type(result)) == "": - result = new_iterator_async() - else: - result = new_iterator() - - return result - - return new_stream - - -def _patch_embeddings_provider(provider_class: "Any") -> None: - """Patch an embeddings provider class with monitoring wrappers.""" - if provider_class is None: - return - - if hasattr(provider_class, "embed_documents"): - provider_class.embed_documents = _wrap_embedding_method( - provider_class.embed_documents - ) - if hasattr(provider_class, "embed_query"): - provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) - if hasattr(provider_class, "aembed_documents"): - provider_class.aembed_documents = _wrap_async_embedding_method( - provider_class.aembed_documents - ) - if hasattr(provider_class, "aembed_query"): - provider_class.aembed_query = _wrap_async_embedding_method( - provider_class.aembed_query - ) - - -def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap sync embedding methods (embed_documents and embed_query).""" - - @wraps(f) - def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - - return new_embedding_method - - -def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap async embedding methods (aembed_documents and aembed_query).""" - - @wraps(f) - async def new_async_embedding_method( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - - return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py deleted file mode 100644 index 3d3856a913..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/langgraph.py +++ /dev/null @@ -1,515 +0,0 @@ -from functools import wraps -from typing import Any, Callable, List, Optional - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration - -# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. -from sentry_sdk.integrations.langchain import LangchainIntegration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -try: - from langgraph.errors import GraphBubbleUp - from langgraph.graph import StateGraph - from langgraph.pregel import Pregel -except ImportError: - raise DidNotEnable("langgraph not installed") - - -class LanggraphIntegration(Integration): - identifier = "langgraph" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) - # LangGraph lets users create agents using a StateGraph or the Functional API. - # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and - # the functional API execute on a Pregel instance. Pregel is the runtime for the graph - # and the invocation happens on Pregel, so patching the invoke methods takes care of both. - # The streaming methods are not patched, because due to some internal reasons, LangGraph - # will automatically patch the streaming methods to run through invoke, and by doing this - # we prevent duplicate spans for invocations. - StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) - if hasattr(Pregel, "invoke"): - Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) - if hasattr(Pregel, "ainvoke"): - Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) - - -def _get_graph_name(graph_obj: "Any") -> "Optional[str]": - for attr in ["name", "graph_name", "__name__", "_name"]: - if hasattr(graph_obj, attr): - name = getattr(graph_obj, attr) - if name and isinstance(name, str): - return name - return None - - -def _normalize_langgraph_message(message: "Any") -> "Any": - if not hasattr(message, "content"): - return None - - parsed = {"role": getattr(message, "type", None), "content": message.content} - - for attr in [ - "name", - "tool_calls", - "function_call", - "tool_call_id", - "response_metadata", - ]: - if hasattr(message, attr): - value = getattr(message, attr) - if value is not None: - parsed[attr] = value - - return parsed - - -def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": - if not state: - return None - - messages = None - - if isinstance(state, dict): - messages = state.get("messages") - elif hasattr(state, "messages"): - messages = state.messages - elif hasattr(state, "get") and callable(state.get): - try: - messages = state.get("messages") - except Exception: - pass - - if not messages or not isinstance(messages, (list, tuple)): - return None - - normalized_messages = [] - for message in messages: - try: - normalized = _normalize_langgraph_message(message) - if normalized: - normalized_messages.append(normalized) - except Exception: - continue - - return normalized_messages if normalized_messages else None - - -def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None or has_span_streaming_enabled(client.options): - return f(self, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GEN_AI_CREATE_AGENT, - origin=LanggraphIntegration.origin, - ) as span: - compiled_graph = f(self, *args, **kwargs) - - compiled_graph_name = getattr(compiled_graph, "name", None) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) - - if compiled_graph_name: - span.description = f"create_agent {compiled_graph_name}" - else: - span.description = "create_agent" - - if kwargs.get("model", None) is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) - - tools = None - get_graph = getattr(compiled_graph, "get_graph", None) - if get_graph and callable(get_graph): - graph_obj = compiled_graph.get_graph() - nodes = getattr(graph_obj, "nodes", None) - if nodes and isinstance(nodes, dict): - tools_node = nodes.get("tools") - if tools_node: - data = getattr(tools_node, "data", None) - if data and hasattr(data, "tools_by_name"): - tools = list(data.tools_by_name.keys()) - - if tools is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) - - return compiled_graph - - return new_compile - - -def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - else: - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_invoke - - -def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles(input_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_ainvoke - - -def _get_new_messages( - input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" -) -> "Optional[List[Any]]": - """Extract only the new messages added during this invocation.""" - if not output_messages: - return None - - if not input_messages: - return output_messages - - # only return the new messages, aka the output messages that are not in the input messages - input_count = len(input_messages) - new_messages = ( - output_messages[input_count:] if len(output_messages) > input_count else [] - ) - - return new_messages if new_messages else None - - -def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": - if not messages: - return None - - for message in reversed(messages): - if isinstance(message, dict): - role = message.get("role") - if role in ["assistant", "ai"]: - content = message.get("content") - if content and isinstance(content, str): - return content - - return None - - -def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": - if not messages: - return None - - tool_calls = [] - for message in messages: - if isinstance(message, dict): - msg_tool_calls = message.get("tool_calls") - if msg_tool_calls and isinstance(msg_tool_calls, list): - tool_calls.extend(msg_tool_calls) - - return tool_calls if tool_calls else None - - -def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - input_tokens = 0 - output_tokens = 0 - total_tokens = 0 - - for message in messages: - response_metadata = message.get("response_metadata") - if response_metadata is None: - continue - - token_usage = response_metadata.get("token_usage") - if not token_usage: - continue - - input_tokens += int(token_usage.get("prompt_tokens", 0)) - output_tokens += int(token_usage.get("completion_tokens", 0)) - total_tokens += int(token_usage.get("total_tokens", 0)) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens > 0: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - total_tokens, - ) - - -def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - if len(messages) == 0: - return - - last_message = messages[-1] - response_metadata = last_message.get("response_metadata") - if response_metadata is None: - return - - model_name = response_metadata.get("model_name") - if model_name is None: - return - - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) - - -def _set_response_attributes( - span: "Any", - input_messages: "Optional[List[Any]]", - result: "Any", - integration: "LanggraphIntegration", -) -> None: - parsed_response_messages = _parse_langgraph_messages(result) - new_messages = _get_new_messages(input_messages, parsed_response_messages) - - if new_messages is None: - return - - _set_usage_data(span, new_messages) - _set_response_model_name(span, new_messages) - - if not (should_send_default_pii() and integration.include_prompts): - return - - llm_response_text = _extract_llm_response_text(new_messages) - if llm_response_text: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) - elif new_messages: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) - else: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) - - tool_calls = _extract_tool_calls(new_messages) - if tool_calls: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(tool_calls), - unpack=False, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py deleted file mode 100644 index 3c2c76450c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/launchdarkly.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - import ldclient - from ldclient.hook import Hook, Metadata - - if TYPE_CHECKING: - from typing import Any - - from ldclient import LDClient - from ldclient.evaluation import EvaluationDetail - from ldclient.hook import EvaluationSeriesContext -except ImportError: - raise DidNotEnable("LaunchDarkly is not installed") - - -class LaunchDarklyIntegration(Integration): - identifier = "launchdarkly" - - def __init__(self, ld_client: "LDClient | None" = None) -> None: - """ - :param client: An initialized LDClient instance. If a client is not provided, this - integration will attempt to use the shared global instance. - """ - try: - client = ld_client or ldclient.get() - except Exception as exc: - raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) - - if not client.is_initialized(): - raise DidNotEnable("LaunchDarkly client is not initialized.") - - # Register the flag collection hook with the LD client. - client.add_hook(LaunchDarklyHook()) - - @staticmethod - def setup_once() -> None: - pass - - -class LaunchDarklyHook(Hook): - @property - def metadata(self) -> "Metadata": - return Metadata(name="sentry-flag-auditor") - - def after_evaluation( - self, - series_context: "EvaluationSeriesContext", - data: "dict[Any, Any]", - detail: "EvaluationDetail", - ) -> "dict[Any, Any]": - if isinstance(detail.value, bool): - add_feature_flag(series_context.key, detail.value) - - return data - - def before_evaluation( - self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" - ) -> "dict[Any, Any]": - return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py deleted file mode 100644 index 49ead6b068..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litellm.py +++ /dev/null @@ -1,376 +0,0 @@ -import copy -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - set_data_normalized, - transform_openai_content_part, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from datetime import datetime - from typing import Any, Dict, List - -try: - import litellm # type: ignore[import-not-found] - from litellm import failure_callback, input_callback, success_callback -except ImportError: - raise DidNotEnable("LiteLLM not installed") - - -# Stash the span on a top-level key of the per-request kwargs dict litellm passes -# to every callback, so it lives and dies with the request. -_SPAN_KEY = "_sentry_span" - - -def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: - kwargs[_SPAN_KEY] = span - - -def _peek_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.get(_SPAN_KEY) - - -def _pop_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.pop(_SPAN_KEY, None) - - -def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": - """ - Convert the message parts from OpenAI format to the `gen_ai.request.messages` format - using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). - - Deep copies messages to avoid mutating original kwargs. - """ - # Deep copy to avoid mutating original messages from kwargs - messages = copy.deepcopy(messages) - - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_openai_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - message["content"] = transformed - return messages - - -def _input_callback(kwargs: "Dict[str, Any]") -> None: - """Handle the start of a request.""" - client = sentry_sdk.get_client() - integration = client.get_integration(LiteLLMIntegration) - - if integration is None: - return - - # Get key parameters - full_model = kwargs.get("model", "") - try: - model, provider, _, _ = litellm.get_llm_provider(full_model) - except Exception: - model = full_model - provider = "unknown" - - call_type = kwargs.get("call_type", None) - if call_type == "embedding" or call_type == "aembedding": - operation = "embeddings" - else: - operation = "chat" - - # Start a new span/transaction - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"{operation} {model}", - attributes={ - "sentry.op": ( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - "sentry.origin": LiteLLMIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - name=f"{operation} {model}", - origin=LiteLLMIntegration.origin, - ) - span.__enter__() - - _store_span(kwargs, span) - - # Set basic data - set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) - - # Record input/messages if allowed - if should_send_default_pii() and integration.include_prompts: - if operation == "embeddings": - # For embeddings, look for the 'input' parameter - embedding_input = kwargs.get("input") - if embedding_input: - scope = sentry_sdk.get_current_scope() - # Normalize to list format - input_list = ( - embedding_input - if isinstance(embedding_input, list) - else [embedding_input] - ) - client = sentry_sdk.get_client() - messages_data = ( - truncate_and_annotate_embedding_inputs(input_list, span, scope) - if should_truncate_gen_ai_input(client.options) - else input_list - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - messages_data, - unpack=False, - ) - else: - # For chat, look for the 'messages' parameter - messages = kwargs.get("messages", []) - if messages: - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages = _convert_message_parts(messages) - messages_data = ( - truncate_and_annotate_messages(messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Record other parameters - params = { - "model": SPANDATA.GEN_AI_REQUEST_MODEL, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - } - for key, attribute in params.items(): - value = kwargs.get(key) - if value is not None: - set_data_normalized(span, attribute, value) - - -async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: - return _input_callback(kwargs) - - -def _success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle successful completion.""" - - span = _peek_span(kwargs) - if span is None: - return - - integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) - if integration is None: - return - - try: - # Record model information - if hasattr(completion_response, "model"): - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model - ) - - # Record response content if allowed - if should_send_default_pii() and integration.include_prompts: - if hasattr(completion_response, "choices"): - response_messages = [] - for choice in completion_response.choices: - if hasattr(choice, "message"): - if hasattr(choice.message, "model_dump"): - response_messages.append(choice.message.model_dump()) - elif hasattr(choice.message, "dict"): - response_messages.append(choice.message.dict()) - else: - # Fallback for basic message objects - msg = {} - if hasattr(choice.message, "role"): - msg["role"] = choice.message.role - if hasattr(choice.message, "content"): - msg["content"] = choice.message.content - if hasattr(choice.message, "tool_calls"): - msg["tool_calls"] = choice.message.tool_calls - response_messages.append(msg) - - if response_messages: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages - ) - - # Record token usage - if hasattr(completion_response, "usage"): - usage = completion_response.usage - record_token_usage( - span, - input_tokens=getattr(usage, "prompt_tokens", None), - output_tokens=getattr(usage, "completion_tokens", None), - total_tokens=getattr(usage, "total_tokens", None), - ) - - finally: - is_streaming = kwargs.get("stream") - # Callback is fired multiple times when streaming a response. - # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 - if ( - is_streaming is not True - or "complete_streaming_response" in kwargs - or "async_complete_streaming_response" in kwargs - ): - span = _pop_span(kwargs) - if span is not None: - span.__exit__(None, None, None) - - -async def _async_success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - return _success_callback( - kwargs, - completion_response, - start_time, - end_time, - ) - - -def _failure_callback( - kwargs: "Dict[str, Any]", - exception: Exception, - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle request failure.""" - span = _pop_span(kwargs) - if span is None: - return - - try: - # Capture the exception - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "litellm", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - finally: - # Always finish the span and clean up - span.__exit__(type(exception), exception, None) - - -class LiteLLMIntegration(Integration): - """ - LiteLLM integration for Sentry. - - This integration automatically captures LiteLLM API calls and sends them to Sentry - for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM - supports, including OpenAI, Anthropic, Google, Cohere, and many others. - - Features: - - Automatic exception capture for all LiteLLM calls - - Token usage tracking across all providers - - Provider detection and attribution - - Input/output message capture (configurable) - - Streaming response support - - Cost tracking integration - - Usage: - - ```python - import litellm - import sentry_sdk - - # Initialize Sentry with the LiteLLM integration - sentry_sdk.init( - dsn="your-dsn", - send_default_pii=True - integrations=[ - sentry_sdk.integrations.LiteLLMIntegration( - include_prompts=True # Set to False to exclude message content - ) - ] - ) - - # All LiteLLM calls will now be monitored - response = litellm.completion( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": "Hello!"}] - ) - ``` - - Configuration: - - include_prompts (bool): Whether to include prompts and responses in spans. - Defaults to True. Set to False to exclude potentially sensitive data. - """ - - identifier = "litellm" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """Set up LiteLLM callbacks for monitoring.""" - litellm.input_callback = input_callback or [] - if _input_callback not in litellm.input_callback: - litellm.input_callback.append(_input_callback) - if _async_input_callback not in litellm.input_callback: - litellm.input_callback.append(_async_input_callback) - - litellm.success_callback = success_callback or [] - if _success_callback not in litellm.success_callback: - litellm.success_callback.append(_success_callback) - if _async_success_callback not in litellm.success_callback: - litellm.success_callback.append(_async_success_callback) - - litellm.failure_callback = failure_callback or [] - if _failure_callback not in litellm.failure_callback: - litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py deleted file mode 100644 index f0c90a7921..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/litestar.py +++ /dev/null @@ -1,364 +0,0 @@ -from collections.abc import Set -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from litestar import Litestar, Request # type: ignore - from litestar.data_extractors import ConnectionDataExtractor # type: ignore - from litestar.exceptions import HTTPException # type: ignore - from litestar.handlers.base import BaseRouteHandler # type: ignore - from litestar.middleware import DefineMiddleware # type: ignore - from litestar.routes.http import HTTPRoute # type: ignore -except ImportError: - raise DidNotEnable("Litestar is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from litestar.middleware import MiddlewareProtocol - from litestar.types import ( # type: ignore - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from litestar.types import ( - Scope as LitestarScope, - ) - from litestar.types.asgi_types import ASGIApp # type: ignore - - from sentry_sdk._types import Event, Hint - -_DEFAULT_TRANSACTION_NAME = "generic Litestar request" - - -class LitestarIntegration(Integration): - identifier = "litestar" - origin = f"auto.http.{identifier}" - - def __init__( - self, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. - # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following - # (among other things): - # 1. Logs them, some at least (such as 500s) as errors - # 2. Calls after_exception hooks - # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event - # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will - # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. - ignore_logger("litestar") - - -class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - def _capture_request_exception(self, exc: Exception) -> None: - """Avoid catching exceptions from request handlers. - - Those exceptions are already handled in Litestar.after_exception handler. - We still catch exceptions from application lifespan handlers. - """ - pass - - -def patch_app_init() -> None: - """ - Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. - See: - - https://docs.litestar.dev/2/usage/applications.html#after-exception - - https://docs.litestar.dev/2/usage/middleware/using-middleware.html - """ - old__init__ = Litestar.__init__ - - @ensure_integration_enabled(LitestarIntegration, old__init__) - def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: - kwargs["after_exception"] = [ - exception_handler, - *(kwargs.get("after_exception") or []), - ] - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Litestar.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryLitestarASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "LitestarScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(LitestarIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=middleware_name, - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR, - "sentry.origin": LitestarIntegration.origin, - }, - ) as middleware_span: - middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.traces.start_span( - name=getattr(receive, "__qualname__", str(receive)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.traces.start_span( - name=getattr(send, "__qualname__", str(send)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - else: - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR, - name=middleware_name, - origin=LitestarIntegration.origin, - ) as middleware_span: - middleware_span.set_tag("litestar.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_SEND, - name=getattr(send, "__qualname__", str(send)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type - elif hasattr(route_handler.fn, "value"): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = LitestarIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if isinstance(scope_user, dict): - return scope_user - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - return None - - -@ensure_integration_enabled(LitestarIntegration) -def exception_handler(exc: Exception, scope: "LitestarScope") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - if isinstance(exc, HTTPException): - integration = sentry_sdk.get_client().get_integration(LitestarIntegration) - if ( - integration is not None - and exc.status_code not in integration.failed_request_status_codes - ): - return - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": LitestarIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py deleted file mode 100644 index a310a0ced6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/logging.py +++ /dev/null @@ -1,476 +0,0 @@ -import logging -import sys -from datetime import datetime, timezone -from fnmatch import fnmatch -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.client import BaseClient -from sentry_sdk.integrations import Integration -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import ( - capture_internal_exceptions, - current_stacktrace, - event_from_exception, - has_logs_enabled, - safe_repr, - to_string, -) - -if TYPE_CHECKING: - from collections.abc import MutableMapping - from logging import LogRecord - from typing import Any, Dict, Optional - -DEFAULT_LEVEL = logging.INFO -DEFAULT_EVENT_LEVEL = logging.ERROR -LOGGING_TO_EVENT_LEVEL = { - logging.NOTSET: "notset", - logging.DEBUG: "debug", - logging.INFO: "info", - logging.WARN: "warning", # WARN is same a WARNING - logging.WARNING: "warning", - logging.ERROR: "error", - logging.FATAL: "fatal", - logging.CRITICAL: "fatal", # CRITICAL is same as FATAL -} - -# Map logging level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - logging.CRITICAL: 21, # fatal - logging.ERROR: 17, # error - logging.WARNING: 13, # warn - logging.INFO: 9, # info - logging.DEBUG: 5, # debug -} - - -# Capturing events from those loggers causes recursion errors. We cannot allow -# the user to unconditionally create events from those loggers under any -# circumstances. -# -# Note: Ignoring by logger name here is better than mucking with thread-locals. -# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. -# -# Events/breadcrumbs and Sentry Logs have separate ignore lists so that -# framework loggers silenced for events (e.g. django.server) can still be -# captured as Sentry Logs. -_IGNORED_LOGGERS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - -_IGNORED_LOGGERS_SENTRY_LOGS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - - -def ignore_logger( - name: str, -) -> None: - """This disables recording (both in breadcrumbs and as events) calls to - a logger of a specific name. Among other uses, many of our integrations - use this to prevent their actions being recorded as breadcrumbs. Exposed - to users as a way to quiet spammy loggers. - - This does **not** affect Sentry Logs — use - :py:func:`ignore_logger_for_sentry_logs` for that. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS.add(name) - - -def ignore_logger_for_sentry_logs( - name: str, -) -> None: - """This disables recording as Sentry Logs calls to a logger of a - specific name. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS_SENTRY_LOGS.add(name) - - -def unignore_logger( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger` call, re-enabling - recording of breadcrumbs and events for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS.discard(name) - - -def unignore_logger_for_sentry_logs( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, - re-enabling recording of Sentry Logs for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) - - -class LoggingIntegration(Integration): - identifier = "logging" - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - self._handler = None - self._breadcrumb_handler = None - self._sentry_logs_handler = None - - if level is not None: - self._breadcrumb_handler = BreadcrumbHandler(level=level) - - if sentry_logs_level is not None: - self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) - - if event_level is not None: - self._handler = EventHandler(level=event_level) - - def _handle_record(self, record: "LogRecord") -> None: - if self._handler is not None and record.levelno >= self._handler.level: - self._handler.handle(record) - - if ( - self._breadcrumb_handler is not None - and record.levelno >= self._breadcrumb_handler.level - ): - self._breadcrumb_handler.handle(record) - - def _handle_sentry_logs_record(self, record: "LogRecord") -> None: - if ( - self._sentry_logs_handler is not None - and record.levelno >= self._sentry_logs_handler.level - ): - self._sentry_logs_handler.handle(record) - - @staticmethod - def setup_once() -> None: - old_callhandlers = logging.Logger.callHandlers - - def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": - # keeping a local reference because the - # global might be discarded on shutdown - ignored_loggers = _IGNORED_LOGGERS - ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS - - try: - return old_callhandlers(self, record) - finally: - # This check is done twice, once also here before we even get - # the integration. Otherwise we have a high chance of getting - # into a recursion error when the integration is resolved - # (this also is slower). - name = record.name.strip() - - handle_events = ( - ignored_loggers is not None and name not in ignored_loggers - ) - handle_sentry_logs = ( - ignored_loggers_sentry_logs is not None - and name not in ignored_loggers_sentry_logs - ) - - if handle_events or handle_sentry_logs: - integration = sentry_sdk.get_client().get_integration( - LoggingIntegration - ) - if integration is not None: - if handle_events: - integration._handle_record(record) - if handle_sentry_logs: - integration._handle_sentry_logs_record(record) - - logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore - - -class _BaseHandler(logging.Handler): - COMMON_RECORD_ATTRS = frozenset( - ( - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "linenno", - "lineno", - "message", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack", - "tags", - "taskName", - "thread", - "threadName", - "stack_info", - ) - ) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - return LOGGING_TO_EVENT_LEVEL.get( - record.levelno, record.levelname.lower() if record.levelname else "" - ) - - def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": - return { - k: v - for k, v in vars(record).items() - if k not in self.COMMON_RECORD_ATTRS - and (not isinstance(k, str) or not k.startswith("_")) - } - - -class EventHandler(_BaseHandler): - """ - A logging handler that emits Sentry events for each log record - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - client_options = client.options - - # exc_info might be None or (None, None, None) - # - # exc_info may also be any falsy value due to Python stdlib being - # liberal with what it receives and Celery's billiard being "liberal" - # with what it sends. See - # https://github.com/getsentry/sentry-python/issues/904 - if record.exc_info and record.exc_info[0] is not None: - event, hint = event_from_exception( - record.exc_info, - client_options=client_options, - mechanism={"type": "logging", "handled": True}, - ) - elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: - event = {} - hint = {} - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=client_options[ - "include_local_variables" - ], - max_value_length=client_options["max_value_length"], - ), - "crashed": False, - "current": True, - } - ] - } - else: - event = {} - hint = {} - - hint["log_record"] = record - - level = self._logging_to_event_level(record) - if level in {"debug", "info", "warning", "error", "critical", "fatal"}: - event["level"] = level # type: ignore[typeddict-item] - event["logger"] = record.name - - if ( - sys.version_info < (3, 11) - and record.name == "py.warnings" - and record.msg == "%s" - ): - # warnings module on Python 3.10 and below sets record.msg to "%s" - # and record.args[0] to the actual warning message. - # This was fixed in https://github.com/python/cpython/pull/30975. - message = record.args[0] - params = () - else: - message = record.msg - params = record.args - - event["logentry"] = { - "message": to_string(message), - "formatted": record.getMessage(), - "params": params, - } - - event["extra"] = self._extra_from_record(record) - - sentry_sdk.capture_event(event, hint=hint) - - -# Legacy name -SentryHandler = EventHandler - - -class BreadcrumbHandler(_BaseHandler): - """ - A logging handler that records breadcrumbs for each log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - sentry_sdk.add_breadcrumb( - self._breadcrumb_from_record(record), hint={"log_record": record} - ) - - def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": - return { - "type": "log", - "level": self._logging_to_event_level(record), - "category": record.name, - "message": record.message, - "timestamp": datetime.fromtimestamp(record.created, timezone.utc), - "data": self._extra_from_record(record), - } - - -class SentryLogsHandler(_BaseHandler): - """ - A logging handler that records Sentry logs for each Python log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS_SENTRY_LOGS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - self._capture_log_from_record(client, record) - - def _capture_log_from_record( - self, client: "BaseClient", record: "LogRecord" - ) -> None: - otel_severity_number, otel_severity_text = _log_level_to_otel( - record.levelno, SEVERITY_TO_OTEL_SEVERITY - ) - project_root = client.options["project_root"] - - attrs: "Any" = self._extra_from_record(record) - attrs["sentry.origin"] = "auto.log.stdlib" - - parameters_set = False - if record.args is not None: - if isinstance(record.args, tuple): - parameters_set = bool(record.args) - for i, arg in enumerate(record.args): - attrs[f"sentry.message.parameter.{i}"] = ( - arg - if isinstance(arg, (str, float, int, bool)) - else safe_repr(arg) - ) - elif isinstance(record.args, dict): - parameters_set = bool(record.args) - for key, value in record.args.items(): - attrs[f"sentry.message.parameter.{key}"] = ( - value - if isinstance(value, (str, float, int, bool)) - else safe_repr(value) - ) - - if parameters_set and isinstance(record.msg, str): - # only include template if there is at least one - # sentry.message.parameter.X set - attrs["sentry.message.template"] = record.msg - - if record.lineno: - attrs["code.line.number"] = record.lineno - - if record.pathname: - if project_root is not None and record.pathname.startswith(project_root): - attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record.pathname - - if record.funcName: - attrs["code.function.name"] = record.funcName - - if record.thread: - attrs["thread.id"] = record.thread - if record.threadName: - attrs["thread.name"] = record.threadName - - if record.process: - attrs["process.pid"] = record.process - if record.processName: - attrs["process.executable.name"] = record.processName - if record.name: - attrs["logger.name"] = record.name - - # noinspection PyProtectedMember - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record.message, - "attributes": attrs, - "time_unix_nano": int(record.created * 1e9), - "trace_id": None, - "span_id": None, - }, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py deleted file mode 100644 index dbb724d9a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/loguru.py +++ /dev/null @@ -1,208 +0,0 @@ -import enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.logging import ( - BreadcrumbHandler, - EventHandler, - _BaseHandler, -) -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import has_logs_enabled, safe_repr - -if TYPE_CHECKING: - from logging import LogRecord - from typing import Any, Optional - -try: - import loguru - from loguru import logger - from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT - - if TYPE_CHECKING: - from loguru import Message -except ImportError: - raise DidNotEnable("LOGURU is not installed") - - -class LoggingLevels(enum.IntEnum): - TRACE = 5 - DEBUG = 10 - INFO = 20 - SUCCESS = 25 - WARNING = 30 - ERROR = 40 - CRITICAL = 50 - - -DEFAULT_LEVEL = LoggingLevels.INFO.value -DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value - - -SENTRY_LEVEL_FROM_LOGURU_LEVEL = { - "TRACE": "DEBUG", - "DEBUG": "DEBUG", - "INFO": "INFO", - "SUCCESS": "INFO", - "WARNING": "WARNING", - "ERROR": "ERROR", - "CRITICAL": "CRITICAL", -} - -# Map Loguru level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - LoggingLevels.CRITICAL: 21, # fatal - LoggingLevels.ERROR: 17, # error - LoggingLevels.WARNING: 13, # warn - LoggingLevels.SUCCESS: 11, # info - LoggingLevels.INFO: 9, # info - LoggingLevels.DEBUG: 5, # debug - LoggingLevels.TRACE: 1, # trace -} - - -class LoguruIntegration(Integration): - identifier = "loguru" - - level: "Optional[int]" = DEFAULT_LEVEL - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL - breadcrumb_format = DEFAULT_FORMAT - event_format = DEFAULT_FORMAT - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - LoguruIntegration.level = level - LoguruIntegration.event_level = event_level - LoguruIntegration.breadcrumb_format = breadcrumb_format - LoguruIntegration.event_format = event_format - LoguruIntegration.sentry_logs_level = sentry_logs_level - - @staticmethod - def setup_once() -> None: - if LoguruIntegration.level is not None: - logger.add( - LoguruBreadcrumbHandler(level=LoguruIntegration.level), - level=LoguruIntegration.level, - format=LoguruIntegration.breadcrumb_format, - ) - - if LoguruIntegration.event_level is not None: - logger.add( - LoguruEventHandler(level=LoguruIntegration.event_level), - level=LoguruIntegration.event_level, - format=LoguruIntegration.event_format, - ) - - if LoguruIntegration.sentry_logs_level is not None: - logger.add( - loguru_sentry_logs_handler, - level=LoguruIntegration.sentry_logs_level, - ) - - -class _LoguruBaseHandler(_BaseHandler): - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - if kwargs.get("level"): - kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( - kwargs.get("level", ""), DEFAULT_LEVEL - ) - - super().__init__(*args, **kwargs) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - try: - return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ - LoggingLevels(record.levelno).name - ].lower() - except (ValueError, KeyError): - return record.levelname.lower() if record.levelname else "" - - -class LoguruEventHandler(_LoguruBaseHandler, EventHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" - - pass - - -class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" - - pass - - -def loguru_sentry_logs_handler(message: "Message") -> None: - # This is intentionally a callable sink instead of a standard logging handler - # since otherwise we wouldn't get direct access to message.record - client = sentry_sdk.get_client() - - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - record = message.record - - if ( - LoguruIntegration.sentry_logs_level is None - or record["level"].no < LoguruIntegration.sentry_logs_level - ): - return - - otel_severity_number, otel_severity_text = _log_level_to_otel( - record["level"].no, SEVERITY_TO_OTEL_SEVERITY - ) - - attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} - - project_root = client.options["project_root"] - if record.get("file"): - if project_root is not None and record["file"].path.startswith(project_root): - attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record["file"].path - - if record.get("line") is not None: - attrs["code.line.number"] = record["line"] - - if record.get("function"): - attrs["code.function.name"] = record["function"] - - if record.get("thread"): - attrs["thread.name"] = record["thread"].name - attrs["thread.id"] = record["thread"].id - - if record.get("process"): - attrs["process.pid"] = record["process"].id - attrs["process.executable.name"] = record["process"].name - - if record.get("name"): - attrs["logger.name"] = record["name"] - - extra = record.get("extra") - if isinstance(extra, dict): - for key, value in extra.items(): - if isinstance(value, (str, int, float, bool)): - attrs[f"sentry.message.parameter.{key}"] = value - else: - attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record["message"], - "attributes": attrs, - "time_unix_nano": int(record["time"].timestamp() * 1e9), - "trace_id": None, - "span_id": None, - } - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py deleted file mode 100644 index 79381fe06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/mcp.py +++ /dev/null @@ -1,876 +0,0 @@ -""" -Sentry integration for MCP (Model Context Protocol) servers. - -This integration instruments MCP servers to create spans for tool, prompt, -and resource handler execution, and captures errors that occur during execution. - -Supports the low-level `mcp.server.lowlevel.Server` API. -""" - -import inspect -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import package_version, safe_serialize - -MCP_PACKAGE_VERSION = package_version("mcp") - -try: - from mcp.server.lowlevel import Server - from mcp.server.streamable_http import ( - StreamableHTTPServerTransport, - ) - - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] - request_ctx, - ) -except ImportError: - raise DidNotEnable("MCP SDK not installed") - -try: - from fastmcp import FastMCP # type: ignore[import-not-found] -except ImportError: - FastMCP = None - -if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - try: - from mcp.server.context import ( - ServerRequestContext, - ) - except ImportError: - ServerRequestContext = None # type: ignore[assignment,misc] -else: - ServerRequestContext = None # type: ignore[assignment,misc] - - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Optional, Tuple, Union - - from starlette.types import Receive, Scope, Send - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -class MCPIntegration(Integration): - identifier = "mcp" - origin = "auto.ai.mcp" - - def __init__(self, include_prompts: bool = True) -> None: - """ - Initialize the MCP integration. - - Args: - include_prompts: Whether to include prompts (tool results and prompt content) - in span data. Requires send_default_pii=True. Default is True. - """ - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """ - Patches MCP server classes to instrument handler execution. - """ - _patch_lowlevel_server() - _patch_handle_request() - - if FastMCP is not None: - _patch_fastmcp() - - -def _get_active_http_scopes( - ctx: "Optional[Any]" = None, -) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return None - - if ( - ctx is None - or not hasattr(ctx, "request") - or ctx.request is None - or "state" not in ctx.request.scope - ): - return None - - return ( - ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), - ctx.request.scope["state"].get("sentry_sdk.current_scope"), - ) - - -def _get_request_context_data( - ctx: "Optional[Any]" = None, -) -> "tuple[Optional[str], Optional[str], str]": - """ - Extract request ID, session ID, and MCP transport type from the request context. - - Returns: - Tuple of (request_id, session_id, mcp_transport). - - request_id: May be None if not available - - session_id: May be None if not available - - mcp_transport: "http", "sse", "stdio" - """ - request_id: "Optional[str]" = None - session_id: "Optional[str]" = None - mcp_transport: str = "stdio" - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return request_id, session_id, mcp_transport - - if ctx is not None: - request_id = ctx.request_id - if hasattr(ctx, "request") and ctx.request is not None: - request = ctx.request - # Detect transport type by checking request characteristics - if hasattr(request, "query_params") and request.query_params.get( - "session_id" - ): - # SSE transport uses query parameter - mcp_transport = "sse" - session_id = request.query_params.get("session_id") - elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): - # StreamableHTTP transport uses header - mcp_transport = "http" - session_id = request.headers.get("mcp-session-id") - - return request_id, session_id, mcp_transport - - -def _get_span_config( - handler_type: str, item_name: str -) -> "tuple[str, str, str, Optional[str]]": - """ - Get span configuration based on handler type. - - Returns: - Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) - Note: result_data_key is None for resources - """ - if handler_type == "tool": - span_data_key = SPANDATA.MCP_TOOL_NAME - mcp_method_name = "tools/call" - result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT - elif handler_type == "prompt": - span_data_key = SPANDATA.MCP_PROMPT_NAME - mcp_method_name = "prompts/get" - result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT - else: # resource - span_data_key = SPANDATA.MCP_RESOURCE_URI - mcp_method_name = "resources/read" - result_data_key = None # Resources don't capture result content - - span_name = f"{mcp_method_name} {item_name}" - return span_data_key, span_name, mcp_method_name, result_data_key - - -def _set_span_input_data( - span: "Union[StreamedSpan, Span]", - handler_name: str, - span_data_key: str, - mcp_method_name: str, - arguments: "dict[str, Any]", - request_id: "Optional[str]", - session_id: "Optional[str]", - mcp_transport: str, -) -> None: - """Set input span data for MCP handlers.""" - - # Set handler identifier - _set_span_data_attribute(span, span_data_key, handler_name) - _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) - - # Set transport/MCP transport type - _set_span_data_attribute( - span, - SPANDATA.NETWORK_TRANSPORT, - "pipe" if mcp_transport == "stdio" else "tcp", - ) - _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) - - # Set request_id if provided - if request_id: - _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) - - # Set session_id if provided - if session_id: - _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) - - # Set request arguments (excluding common request context objects) - for k, v in arguments.items(): - _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) - - -def _extract_tool_result_content(result: "Any") -> "Any": - """ - Extract meaningful content from MCP tool result. - - Tool handlers can return: - - CallToolResult (mcp v2+): Has .content list and optional .structured_content - - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) - - dict (StructuredContent): Return as-is - - list/Iterable (UnstructuredContent): Extract text from content blocks - """ - if result is None: - return None - - # Handle v2 CallToolResult-like objects (has .content list attribute) - if hasattr(result, "content") and isinstance( - getattr(result, "content", None), list - ): - # This is only present when a tool declares an output_schema - structured = getattr(result, "structured_content", None) - if structured is not None: - return structured - return _extract_text_from_content_blocks(result.content) - - # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) - if isinstance(result, tuple) and len(result) == 2: - # Return the structured content (2nd element) - return result[1] - - # Handle StructuredContent: dict - if isinstance(result, dict): - return result - - # Handle UnstructuredContent: iterable of ContentBlock objects - if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): - return _extract_text_from_content_blocks(result) - - return result - - -def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": - texts = [] - try: - for item in content_blocks: - if hasattr(item, "text"): - texts.append(item.text) - elif isinstance(item, dict) and "text" in item: - texts.append(item["text"]) - except Exception: - return content_blocks - return " ".join(texts) if texts else content_blocks - - -def _set_span_output_data( - span: "Union[StreamedSpan, Span]", - result: "Any", - result_data_key: "Optional[str]", - handler_type: str, -) -> None: - """Set output span data for MCP handlers.""" - if result is None: - return - - # Get integration to check PII settings - integration = sentry_sdk.get_client().get_integration(MCPIntegration) - if integration is None: - return - - # Check if we should include sensitive data - should_include_data = should_send_default_pii() and integration.include_prompts - - # For tools, extract the meaningful content - if handler_type == "tool": - extracted = _extract_tool_result_content(result) - if ( - extracted is not None - and should_include_data - and result_data_key is not None - ): - _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) - # Set content count if result is a dict - if isinstance(extracted, dict): - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) - ) - elif handler_type == "prompt": - # For prompts, count messages and set role/content only for single-message prompts - try: - messages: "Optional[list[str]]" = None - message_count = 0 - - # Check if result has messages attribute (GetPromptResult) - if hasattr(result, "messages") and result.messages: - messages = result.messages - message_count = len(messages) - # Also check if result is a dict with messages - elif isinstance(result, dict) and result.get("messages"): - messages = result["messages"] - message_count = len(messages) - - # Always set message count if we found messages - if message_count > 0: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count - ) - - # Only set role and content for single-message prompts if PII is allowed - if message_count == 1 and should_include_data and messages: - first_message = messages[0] - # Extract role - role = None - if hasattr(first_message, "role"): - role = first_message.role - elif isinstance(first_message, dict) and "role" in first_message: - role = first_message["role"] - - if role: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role - ) - - # Extract content text - content_text = None - if hasattr(first_message, "content"): - msg_content = first_message.content - # Content can be a TextContent object or similar - if hasattr(msg_content, "text"): - content_text = msg_content.text - elif isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - elif isinstance(first_message, dict) and "content" in first_message: - msg_content = first_message["content"] - if isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - - if content_text and result_data_key is not None: - _set_span_data_attribute(span, result_data_key, content_text) - except Exception: - # Silently ignore if we can't extract message info - pass - # Resources don't capture result content (result_data_key is None) - - -# Handler data preparation and wrapping - - -def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: - """Check if original_args contains a v2 ServerRequestContext as the first element.""" - return ( - ServerRequestContext is not None - and bool(original_args) - and isinstance(original_args[0], ServerRequestContext) - ) - - -def _extract_handler_data_from_params( - handler_type: str, - params: "Any", -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from a v2 typed params object. - - In MCP SDK v2, handlers receive (ctx, params) where params is a typed - Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). - """ - if handler_type == "tool": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - elif handler_type == "prompt": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - arguments = {"name": handler_name, **arguments} - else: # resource - handler_name = str(getattr(params, "uri", "unknown")) - arguments = {} - - return handler_name, arguments - - -def _extract_handler_data_from_args( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from v1 positional args. - - In MCP SDK v1, handlers receive positional args: - - Tool: (tool_name, arguments) - - Prompt: (name, arguments) - - Resource: (uri,) - """ - original_kwargs = original_kwargs or {} - - if handler_type == "tool": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - elif handler_type == "prompt": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - arguments = {"name": handler_name, **(arguments or {})} - - else: # resource - handler_name = "unknown" - if original_args: - handler_name = str(original_args[0]) - elif original_kwargs.get("uri"): - handler_name = str(original_kwargs["uri"]) - - arguments = {} - - return handler_name, arguments - - -def _prepare_handler_data( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - params: "Optional[Any]" = None, -) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": - """ - Prepare common handler data for both v1 and v2 MCP SDK. - - Args: - handler_type: "tool", "prompt", or "resource" - original_args: Original positional args (v1 path) - original_kwargs: Original keyword args (v1 path) - params: Typed params object from v2 ServerRequestContext path - - Returns: - Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) - """ - if params is not None: - handler_name, arguments = _extract_handler_data_from_params( - handler_type, params - ) - elif _is_v2_context(original_args): - handler_name = "unknown" - arguments = {} - else: - handler_name, arguments = _extract_handler_data_from_args( - handler_type, original_args, original_kwargs - ) - - span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( - handler_type, handler_name - ) - - return ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) - - -async def _handler_wrapper( - handler_type: str, - func: "Callable[..., Any]", - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - self: "Optional[Any]" = None, - force_await: bool = True, -) -> "Any": - """ - Wrapper for MCP handlers. - - Args: - handler_type: "tool", "prompt", or "resource" - func: The handler function to wrap - original_args: Original arguments passed to the handler - original_kwargs: Original keyword arguments passed to the handler - self: Optional instance for bound methods - """ - if original_kwargs is None: - original_kwargs = {} - - # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers - ctx: "Optional[Any]" = None - params: "Optional[Any]" = None - if ( - ServerRequestContext is not None - and original_args - and isinstance(original_args[0], ServerRequestContext) - ): - ctx = original_args[0] - params = original_args[1] if len(original_args) > 1 else None - - ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) = _prepare_handler_data( - handler_type, original_args, original_kwargs, params=params - ) - - scopes = _get_active_http_scopes(ctx=ctx) - - isolation_scope_context: "ContextManager[Any]" - current_scope_context: "ContextManager[Any]" - - if scopes is None: - isolation_scope_context = nullcontext() - current_scope_context = nullcontext() - else: - isolation_scope, current_scope = scopes - - isolation_scope_context = ( - nullcontext() - if isolation_scope is None - else sentry_sdk.scope.use_isolation_scope(isolation_scope) - ) - current_scope_context = ( - nullcontext() - if current_scope is None - else sentry_sdk.scope.use_scope(current_scope) - ) - - # Get request ID, session ID, and transport from context - request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - - # Start span and execute - with isolation_scope_context: - with current_scope_context: - span_mgr: "Union[Span, StreamedSpan]" - if span_streaming: - span_mgr = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.MCP_SERVER, - "sentry.origin": MCPIntegration.origin, - }, - ) - else: - span_mgr = get_start_span_function()( - op=OP.MCP_SERVER, - name=span_name, - origin=MCPIntegration.origin, - ) - - with span_mgr as span: - # Set input span data - _set_span_input_data( - span, - handler_name, - span_data_key, - mcp_method_name, - arguments, - request_id, - session_id, - mcp_transport, - ) - - # For resources, extract and set protocol - if handler_type == "resource": - uri = None - if params is not None: - uri = getattr(params, "uri", None) - - # v1 scenario - if ServerRequestContext is None: - if original_args: - uri = original_args[0] - else: - uri = original_kwargs.get("uri") - - protocol = None - if uri is not None and hasattr(uri, "scheme"): - protocol = uri.scheme - elif handler_name and "://" in handler_name: - protocol = handler_name.split("://")[0] - if protocol: - _set_span_data_attribute( - span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol - ) - - try: - # Execute the async handler - if self is not None: - original_args = (self, *original_args) - - result = func(*original_args, **original_kwargs) - if force_await or inspect.isawaitable(result): - result = await result - - except Exception as e: - # Set error flag for tools - if handler_type == "tool": - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True - ) - sentry_sdk.capture_exception(e) - raise - - _set_span_output_data(span, result, result_data_key, handler_type) - - return result - - -def _create_instrumented_decorator( - original_decorator: "Callable[..., Any]", - handler_type: str, - *decorator_args: "Any", - **decorator_kwargs: "Any", -) -> "Callable[..., Any]": - """ - Create an instrumented version of an MCP decorator. - - This function intercepts MCP decorators (like @server.call_tool()) and injects - Sentry instrumentation into the handler registration flow. The returned decorator - will: - 1. Receive the user's handler function - 2. Pass the instrumented version to the original MCP decorator - - This ensures that when the handler is called at runtime, it's already wrapped - with Sentry spans and metrics collection. - - Args: - original_decorator: The original MCP decorator method (e.g., Server.call_tool) - handler_type: "tool", "prompt", or "resource" - determines span configuration - decorator_args: Positional arguments to pass to the original decorator (e.g., self) - decorator_kwargs: Keyword arguments to pass to the original decorator - - Returns: - A decorator function that instruments handlers before registering them - """ - - def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(func) - async def wrapper(*args: "Any") -> "Any": - return await _handler_wrapper(handler_type, func, args, force_await=False) - - # Then register it with the original MCP decorator - return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) - - return instrumented_decorator - - -_METHOD_TO_HANDLER_TYPE = { - "tools/call": "tool", - "prompts/get": "prompt", - "resources/read": "resource", -} - -# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via -# the Server(...) constructor kwargs rather than add_request_handler. The in-tree -# high-level MCPServer also wires its handlers through these kwargs. -_KWARG_TO_HANDLER_TYPE = { - "on_call_tool": "tool", - "on_get_prompt": "prompt", - "on_read_resource": "resource", -} - - -def _patch_lowlevel_server() -> None: - """ - Patches the mcp.server.lowlevel.Server class to instrument handler execution. - """ - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - _patch_lowlevel_server_v2() - else: - _patch_lowlevel_server_v1() - - -def _patch_lowlevel_server_v1() -> None: - """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" - # Patch call_tool decorator - original_call_tool = Server.call_tool # type: ignore[attr-defined] - - def patched_call_tool( - self: "Server", **kwargs: "Any" - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.call_tool that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_call_tool, "tool", self, **kwargs - )(func) - - Server.call_tool = patched_call_tool # type: ignore[attr-defined] - - # Patch get_prompt decorator - original_get_prompt = Server.get_prompt # type: ignore[attr-defined] - - def patched_get_prompt( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.get_prompt that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_get_prompt, "prompt", self - )(func) - - Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] - - # Patch read_resource decorator - original_read_resource = Server.read_resource # type: ignore[attr-defined] - - def patched_read_resource( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.read_resource that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_read_resource, "resource", self - )(func) - - Server.read_resource = patched_read_resource # type: ignore[attr-defined] - - -def _wrap_v2_handler( - handler_type: str, handler: "Callable[..., Any]" -) -> "Callable[..., Any]": - """Wrap a v2 (ctx, params) handler with Sentry instrumentation. - - Idempotent: an already-wrapped handler is returned unchanged so handlers - registered through more than one path (e.g. MCPServer building a Server) - are not double-wrapped. - """ - if getattr(handler, "__sentry_mcp_wrapped__", False): - return handler - - @wraps(handler) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - return await _handler_wrapper( - handler_type, handler, args, kwargs, force_await=False - ) - - wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] - return wrapper - - -def _patch_lowlevel_server_v2() -> None: - """Patches the v2 Server to wrap tool/prompt/resource handlers. - - Handlers can be registered either via the Server(...) constructor kwargs - (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree - MCPServer and most lowlevel examples use — or via add_request_handler. - Both are patched. - """ - original_init = Server.__init__ - - @wraps(original_init) - def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: - for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): - handler = kwargs.get(kwarg) - if handler is not None: - kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) - original_init(self, *args, **kwargs) - - Server.__init__ = patched_init # type: ignore[method-assign] - - original_add_request_handler = Server.add_request_handler - - def patched_add_request_handler( - self: "Server", - method: str, - params_type: "Any", - handler: "Callable[..., Any]", - *args: "Any", - **kwargs: "Any", - ) -> None: - handler_type = _METHOD_TO_HANDLER_TYPE.get(method) - if handler_type is not None: - handler = _wrap_v2_handler(handler_type, handler) - - original_add_request_handler( - self, method, params_type, handler, *args, **kwargs - ) - - Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] - - -def _patch_handle_request() -> None: - original_handle_request = StreamableHTTPServerTransport.handle_request - - @wraps(original_handle_request) - async def patched_handle_request( - self: "StreamableHTTPServerTransport", - scope: "Scope", - receive: "Receive", - send: "Send", - ) -> None: - scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( - sentry_sdk.get_isolation_scope() - ) - scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() - await original_handle_request(self, scope, receive, send) - - StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] - - -def _patch_fastmcp() -> None: - """ - Patches the standalone fastmcp package's FastMCP class. - - The standalone fastmcp package (v2.14.0+) registers its own handlers for - prompts and resources directly, bypassing the Server decorators we patch. - This function patches the _get_prompt_mcp and _read_resource_mcp methods - to add instrumentation for those handlers. - """ - if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): - original_get_prompt_mcp = FastMCP._get_prompt_mcp - - @wraps(original_get_prompt_mcp) - async def patched_get_prompt_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "prompt", - original_get_prompt_mcp, - args, - kwargs, - self, - ) - - FastMCP._get_prompt_mcp = patched_get_prompt_mcp - - if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): - original_read_resource_mcp = FastMCP._read_resource_mcp - - @wraps(original_read_resource_mcp) - async def patched_read_resource_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "resource", - original_read_resource_mcp, - args, - kwargs, - self, - ) - - FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py deleted file mode 100644 index b6111492bb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/modules.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import _get_installed_modules - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - - -class ModulesIntegration(Integration): - identifier = "modules" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Any") -> "Event": - if event.get("type") == "transaction": - return event - - if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: - return event - - event["modules"] = _get_installed_modules() - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py deleted file mode 100644 index 186c665ed1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai.py +++ /dev/null @@ -1,1530 +0,0 @@ -import json -import sys -import time -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai._openai_completions_api import ( - _get_system_instructions as _get_system_instructions_completions, -) -from sentry_sdk.ai._openai_completions_api import ( - _get_text_items, - _transform_system_instructions, -) -from sentry_sdk.ai._openai_completions_api import ( - _is_system_instruction as _is_system_instruction_completions, -) -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions as _get_system_instructions_responses, -) -from sentry_sdk.ai._openai_responses_api import ( - _is_system_instruction as _is_system_instruction_responses, -) -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, - safe_serialize, -) - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Iterable, - Iterator, - List, - Optional, - Union, - ) - - from openai import Omit - from openai.types import CompletionUsage - from openai.types.responses import ( - ResponseInputParam, - ResponseStreamEvent, - SequenceNotStr, - ) - from openai.types.responses.response_usage import ResponseUsage - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -try: - try: - from openai import NotGiven - except ImportError: - NotGiven = None - - try: - from openai import Omit - except ImportError: - Omit = None - - from openai import AsyncStream, Stream - from openai.resources import AsyncEmbeddings, Embeddings - from openai.resources.chat.completions import AsyncCompletions, Completions - - if TYPE_CHECKING: - from openai.types.chat import ( - ChatCompletionChunk, - ChatCompletionMessageParam, - ) -except ImportError: - raise DidNotEnable("OpenAI not installed") - -RESPONSES_API_ENABLED = True -try: - # responses API support was introduced in v1.66.0 - from openai.resources.responses import AsyncResponses, Responses - from openai.types.responses.response_completed_event import ResponseCompletedEvent -except ImportError: - RESPONSES_API_ENABLED = False - - -class OpenAIIntegration(Integration): - identifier = "openai" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "OpenAIIntegration", - include_prompts: bool = True, - tiktoken_encoding_name: "Optional[str]" = None, - ) -> None: - self.include_prompts = include_prompts - - self.tiktoken_encoding = None - if tiktoken_encoding_name is not None: - import tiktoken # type: ignore - - self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) - - @staticmethod - def setup_once() -> None: - Completions.create = _wrap_chat_completion_create(Completions.create) - AsyncCompletions.create = _wrap_async_chat_completion_create( - AsyncCompletions.create - ) - - Embeddings.create = _wrap_embeddings_create(Embeddings.create) - AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) - - if RESPONSES_API_ENABLED: - Responses.create = _wrap_responses_create(Responses.create) - AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) - - def count_tokens(self: "OpenAIIntegration", s: str) -> int: - if self.tiktoken_encoding is None: - return 0 - try: - return len(self.tiktoken_encoding.encode_ordinary(s)) - except Exception: - return 0 - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _has_attr_and_is_int( - token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str -) -> bool: - return hasattr(token_usage, attr_name) and isinstance( - getattr(token_usage, attr_name, None), int - ) - - -def _calculate_completions_token_usage( - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - streaming_message_total_token_usage: "Optional[CompletionUsage]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Chat Completions API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - usage = None - - if streaming_message_total_token_usage is not None: - usage = streaming_message_total_token_usage - elif hasattr(response, "usage"): - usage = response.usage - - if usage is not None: - if _has_attr_and_is_int(usage, "prompt_tokens"): - input_tokens = usage.prompt_tokens - if _has_attr_and_is_int(usage, "completion_tokens"): - output_tokens = usage.completion_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "prompt_tokens_details"): - cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "completion_tokens_details"): - reasoning = getattr( - usage.completion_tokens_details, "reasoning_tokens", None - ) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in messages or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "choices") and response.choices is not None: - for choice in response.choices: - if hasattr(choice, "message") and hasattr(choice.message, "content"): - output_tokens += count_tokens(choice.message.content) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _calculate_responses_token_usage( - input: "Any", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Responses API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - - if hasattr(response, "usage"): - usage = response.usage - - if _has_attr_and_is_int(usage, "input_tokens"): - input_tokens = usage.input_tokens - if _has_attr_and_is_int(usage, "output_tokens"): - output_tokens = usage.output_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "input_tokens_details"): - cached = getattr(usage.input_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "output_tokens_details"): - reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in input or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - # Deliberate use of Completions function for both Completions and Responses input format. - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "output"): - for output_item in response.output: - if hasattr(output_item, "content"): - for content_item in output_item.content: - if hasattr(content_item, "text"): - output_tokens += count_tokens(content_item.text) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _set_responses_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") - messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_output_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - conversation = kwargs.get("conversation") - if conversation is not None and _is_given(conversation): - conversation_id: "Optional[str]" = None - if isinstance(conversation, str): - conversation_id = conversation - elif isinstance(conversation, dict): - conversation_id = conversation.get("id") - if conversation_id is not None: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - if not should_send_default_pii() or not integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if ( - messages is None - and explicit_instructions is not None - and _is_given(explicit_instructions) - ): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - [ - { - "type": "text", - "content": explicit_instructions, - } - ] - ), - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if messages is None: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - instructions_text_parts: "list[TextPart]" = [] - if explicit_instructions is not None and _is_given(explicit_instructions): - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions_responses(messages) - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - non_system_messages = [ - message for message in messages if not _is_system_instruction_responses(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - - -def _set_completions_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( - "messages" - ) - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - presence_penalty = kwargs.get("presence_penalty") - if presence_penalty is not None and _is_given(presence_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) - - frequency_penalty = kwargs.get("frequency_penalty") - if frequency_penalty is not None and _is_given(frequency_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - messages = list(messages) - kwargs["messages"] = messages - - system_instructions = _get_system_instructions_completions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - non_system_messages = [ - message - for message in messages - if not _is_system_instruction_completions(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - -def _set_embeddings_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( - "input" - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - return - - if isinstance(messages, str): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - return - - messages = list(messages) - kwargs["input"] = messages - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - -def _set_common_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - input: "Any", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - if hasattr(response, "model"): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) - - # Chat Completions API - if hasattr(response, "choices") and response.choices is not None: - if should_send_default_pii() and integration.include_prompts: - response_text = [ - choice.message.model_dump() - for choice in response.choices - if choice.message is not None - ] - if len(response_text) > 0: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) - - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - # Responses API - elif hasattr(response, "output"): - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in response.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - # Embeddings API (fallback for responses with neither choices nor output) - else: - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_synchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_asynchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _set_completions_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - _set_common_output_data( - span, - response, - messages, - integration, - finish_span, - ) - - -def _wrap_synchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Stream[ChatCompletionChunk]", - old_iterator: "Iterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "Iterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "AsyncStream[ChatCompletionChunk]", - old_iterator: "AsyncIterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "AsyncIterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - async for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -def _wrap_synchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "Stream[ResponseStreamEvent]", - old_iterator: "Iterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "Iterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "AsyncStream[ResponseStreamEvent]", - old_iterator: "AsyncIterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "AsyncIterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft: "Optional[float]" = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - async for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _set_responses_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _set_embeddings_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - return _new_sync_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - return await _new_async_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -async def _new_async_embeddings_create( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -def _wrap_embeddings_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_embeddings_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_synchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_asynchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _wrap_responses_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_responses_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_responses_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_responses_create(f, *args, **kwargs) - - return _sentry_patched_responses_async - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different openai versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py deleted file mode 100644 index 5895f53ad3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/__init__.py +++ /dev/null @@ -1,250 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .patches import ( - _create_run_streamed_wrapper, - _create_run_wrapper, - _execute_final_output, - _execute_handoffs, - _get_all_tools, - _get_model, - _patch_error_tracing, - _run_single_turn, - _run_single_turn_streamed, -) - -try: - # "agents" is too generic. If someone has an agents.py file in their project - # or another package that's importable via "agents", no ImportError would - # be thrown and the integration would enable itself even if openai-agents is - # not installed. That's why we're adding the second, more specific import - # after it, even if we don't use it. - import agents - from agents.run import AgentRunner - from agents.version import __version__ as OPENAI_AGENTS_VERSION - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -try: - # AgentRunner methods moved in v0.8 - # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop, turn_preparation, turn_resolution -except ImportError: - run_loop = None - turn_preparation = None - turn_resolution = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - from agents.run_internal.run_steps import SingleStepResult - - -def _patch_runner() -> None: - # Create the root span for one full agent run (including eventual handoffs) - # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around - # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. - agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run - ) - - # Patch streaming runner - agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run_streamed - ) - - -class OpenAIAgentsIntegration(Integration): - """ - NOTE: With version 0.8.0, the class methods below have been refactored to functions. - - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` - - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` - - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` - - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` - - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` - - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. - 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. - - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. - - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. - 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. - - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. - - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. - 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. - - Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. - Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. - An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. - Handoffs are a way to switch agent-wide configuration. - - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` - """ - - identifier = "openai_agents" - - @staticmethod - def setup_once() -> None: - _patch_error_tracing() - _patch_runner() - - library_version = parse_version(OPENAI_AGENTS_VERSION) - if library_version is not None and library_version >= ( - 0, - 8, - ): - if run_loop is not None: - - @wraps(run_loop.get_all_tools) - async def new_wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools( - run_loop.get_all_tools, agent, context_wrapper - ) - - agents.run.get_all_tools = new_wrapped_get_all_tools - - @wraps(run_loop.run_single_turn) - async def new_wrapped_run_single_turn( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn( - run_loop.run_single_turn, *args, **kwargs - ) - - agents.run.run_single_turn = new_wrapped_run_single_turn - - @wraps(run_loop.run_single_turn_streamed) - async def new_wrapped_run_single_turn_streamed( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - run_loop.run_single_turn_streamed, *args, **kwargs - ) - - agents.run.run_single_turn_streamed = ( - new_wrapped_run_single_turn_streamed - ) - - if turn_preparation is not None: - - @wraps(turn_preparation.get_model) - def new_wrapped_get_model( - agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(turn_preparation.get_model, agent, run_config) - - agents.run_internal.run_loop.get_model = new_wrapped_get_model - - if turn_resolution is not None: - original_execute_handoffs = turn_resolution.execute_handoffs - - @wraps(original_execute_handoffs) - async def new_wrapped_execute_handoffs( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs( - original_execute_handoffs, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_handoffs = ( - new_wrapped_execute_handoffs - ) - - original_execute_final_output = turn_resolution.execute_final_output - - @wraps(turn_resolution.execute_final_output) - async def new_wrapped_final_output( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_final_output = ( - new_wrapped_final_output - ) - - return - - original_get_all_tools = AgentRunner._get_all_tools - - @wraps(AgentRunner._get_all_tools.__func__) - async def old_wrapped_get_all_tools( - cls: "agents.Runner", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) - - agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) - - original_get_model = AgentRunner._get_model - - @wraps(AgentRunner._get_model.__func__) - def old_wrapped_get_model( - cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(original_get_model, agent, run_config) - - agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) - - original_run_single_turn = AgentRunner._run_single_turn - - @wraps(AgentRunner._run_single_turn.__func__) - async def old_wrapped_run_single_turn( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn(original_run_single_turn, *args, **kwargs) - - agents.run.AgentRunner._run_single_turn = classmethod( - old_wrapped_run_single_turn - ) - - original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed - - @wraps(AgentRunner._run_single_turn_streamed.__func__) - async def old_wrapped_run_single_turn_streamed( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - original_run_single_turn_streamed, *args, **kwargs - ) - - agents.run.AgentRunner._run_single_turn_streamed = classmethod( - old_wrapped_run_single_turn_streamed - ) - - original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs - - @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) - async def old_wrapped_execute_handoffs( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) - - agents._run_impl.RunImpl.execute_handoffs = classmethod( - old_wrapped_execute_handoffs - ) - - original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - - @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) - async def old_wrapped_final_output( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents._run_impl.RunImpl.execute_final_output = classmethod( - old_wrapped_final_output - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py deleted file mode 100644 index f5de978be0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py deleted file mode 100644 index 85d48f2d41..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .agent_run import ( - _execute_final_output, # noqa: F401 - _execute_handoffs, # noqa: F401 - _run_single_turn, # noqa: F401 - _run_single_turn_streamed, # noqa: F401 -) -from .error_tracing import _patch_error_tracing # noqa: F401 -from .models import _get_model # noqa: F401 -from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 -from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py deleted file mode 100644 index 71883b2eef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ /dev/null @@ -1,332 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import ( - handoff_span, - invoke_agent_span, - update_invoke_agent_span, -) - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, Union - - from agents.run_internal.run_steps import SingleStepResult - - from sentry_sdk.tracing import Span - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: - """Check if there's an active agent span for this context""" - return getattr(context_wrapper, "_sentry_current_agent", None) is not None - - -def _get_current_agent( - context_wrapper: "agents.RunContextWrapper", -) -> "Optional[agents.Agent]": - """Get the current agent from context wrapper""" - return getattr(context_wrapper, "_sentry_current_agent", None) - - -def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: - """Close the workflow span for streaming executions if it exists.""" - if agent and hasattr(agent, "_sentry_workflow_span"): - workflow_span = agent._sentry_workflow_span - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - -def _maybe_start_agent_span( - context_wrapper: "agents.RunContextWrapper", - agent: "agents.Agent", - should_run_agent_start_hooks: bool, - span_kwargs: "dict[str, Any]", - is_streaming: bool = False, -) -> "Optional[Union[Span, StreamedSpan]]": - """ - Start an agent invocation span if conditions are met. - Handles ending any existing span for a different agent. - - Returns the new span if started, or the existing span if conditions aren't met. - """ - if not (should_run_agent_start_hooks and agent and context_wrapper): - return getattr(context_wrapper, "_sentry_agent_span", None) - - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - # Store the agent on the context wrapper so we can access it later - context_wrapper._sentry_current_agent = agent - span = invoke_agent_span(context_wrapper, agent, span_kwargs) - context_wrapper._sentry_agent_span = span - agent._sentry_agent_span = span - - if not is_streaming: - return span - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - return span - - -async def _run_single_turn( - original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn that - - creates agent invocation spans if there is no already active agent invocation span. - - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. - """ - # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. - bindings = kwargs.get("bindings") - agent = ( - getattr(bindings, "public_agent", None) - if bindings is not None - else kwargs.get("agent") - ) - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) - - span = _maybe_start_agent_span( - context_wrapper, agent, should_run_agent_start_hooks, kwargs - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn(*args, **kwargs) - - try: - result = await original_run_single_turn(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - return result - - -async def _run_single_turn_streamed( - original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn_streamed that - - creates agent invocation spans for streaming if there is no already active agent invocation span. - - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. - - Note: Unlike _run_single_turn which uses keyword-only arguments (*,), - _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: - _run_single_turn_streamed( - streamed_result, # args[0] - bindings, # args[1] - hooks, # args[2] - context_wrapper, # args[3] - run_config, # args[4] - should_run_agent_start_hooks, # args[5] - tool_use_tracker, # args[6] - all_tools, # args[7] - server_conversation_tracker, # args[8] (optional) - ) - """ - streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") - # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. - agent_or_bindings = ( - args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) - ) - agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) - context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") - should_run_agent_start_hooks = bool( - args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) - ) - - span_kwargs: "dict[str, Any]" = {} - if streamed_result and hasattr(streamed_result, "input"): - span_kwargs["original_input"] = streamed_result.input - - span = _maybe_start_agent_span( - context_wrapper, - agent, - should_run_agent_start_hooks, - span_kwargs, - is_streaming=True, - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn_streamed(*args, **kwargs) - - try: - result = await original_run_single_turn_streamed(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - return result - - -async def _execute_handoffs( - original_execute_handoffs: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_handoffs that - - creates and manages handoff spans. - - ends the agent invocation span. - - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. - """ - - context_wrapper = kwargs.get("context_wrapper") - run_handoffs = kwargs.get("run_handoffs") - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - - # Create Sentry handoff span for the first handoff (agents library only processes the first one) - if run_handoffs: - first_handoff = run_handoffs[0] - handoff_agent_name = first_handoff.handoff.agent_name - handoff_span(context_wrapper, agent, handoff_agent_name) - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - # Call original method with all parameters - try: - return await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - # Call original method with all parameters - try: - result = await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result - - -async def _execute_final_output( - original_execute_final_output: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_final_output that - - ends the agent invocation span. - - ends the workflow span if the response is streamed. - """ - - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - context_wrapper = kwargs.get("context_wrapper") - final_output = kwargs.get("final_output") - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - try: - return await original_execute_final_output(*args, **kwargs) - finally: - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - - try: - result = await original_execute_final_output(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py deleted file mode 100644 index 68dadb3101..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/error_tracing.py +++ /dev/null @@ -1,74 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANSTATUS -from sentry_sdk.traces import SpanStatus -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -def _patch_error_tracing() -> None: - """ - Patches agents error tracing function to inject our span error logic - when a tool execution fails. - - In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span - In older versions, it was at: agents._utils.attach_error_to_current_span - - This works even when the module or function doesn't exist. - """ - error_tracing_module = None - - # Try newer location first (agents.util._error_tracing) - try: - from agents.util import _error_tracing - - error_tracing_module = _error_tracing - except (ImportError, AttributeError): - pass - - # Try older location (agents._utils) - if error_tracing_module is None: - try: - import agents._utils - - error_tracing_module = agents._utils - except (ImportError, AttributeError): - # Module doesn't exist in either location, nothing to patch - return - - # Check if the function exists - if not hasattr(error_tracing_module, "attach_error_to_current_span"): - return - - original_attach_error = error_tracing_module.attach_error_to_current_span - - @wraps(original_attach_error) - def sentry_attach_error_to_current_span( - error: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Wraps agents' error attachment to also set Sentry span status to error. - This allows us to properly track tool execution errors even though - the agents library swallows exceptions. - """ - # Set the current Sentry span to errored - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - current_span = sentry_sdk.get_current_scope().streamed_span - if current_span is not None: - current_span.status = SpanStatus.ERROR - else: - current_span = sentry_sdk.get_current_span() - if current_span is not None: - current_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # Call the original function - return original_attach_error(error, *args, **kwargs) - - error_tracing_module.attach_error_to_current_span = ( - sentry_attach_error_to_current_span - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py deleted file mode 100644 index 634c9fdca1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/models.py +++ /dev/null @@ -1,197 +0,0 @@ -import copy -import time -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_sentry_baggage_to_headers, - should_propagate_trace, -) -from sentry_sdk.utils import logger - -from ..spans import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - from sentry_sdk.tracing import Span - -try: - import agents - from agents.tool import HostedMCPTool -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _set_response_model_on_agent_span( - agent: "agents.Agent", response_model: "Optional[str]" -) -> None: - """Set the response model on the agent's invoke_agent span if available.""" - if response_model: - agent_span = getattr(agent, "_sentry_agent_span", None) - if agent_span: - if isinstance(agent_span, StreamedSpan): - agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - else: - agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - -def _inject_trace_propagation_headers( - hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" -) -> None: - headers = hosted_tool.tool_config.get("headers") - if headers is None: - headers = {} - hosted_tool.tool_config["headers"] = headers - - mcp_url = hosted_tool.tool_config.get("server_url") - if not mcp_url: - return - - if should_propagate_trace(sentry_sdk.get_client(), mcp_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( - key=key, value=value, mcp_url=mcp_url - ) - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(headers, value) - else: - headers[key] = value - - -def _get_model( - original_get_model: "Callable[..., agents.Model]", - agent: "agents.Agent", - run_config: "agents.RunConfig", -) -> "agents.Model": - """ - Responsible for - - creating and managing AI client spans. - - adding trace propagation headers to tools with type HostedMCPTool. - - setting the response model on agent invocation spans. - """ - # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) - # because we only patch its direct methods, all underlying data can remain unchanged. - model = copy.copy(original_get_model(agent, run_config)) - - # Capture the request model name for spans (agent.model can be None when using defaults) - request_model_name = model.model if hasattr(model, "model") else str(model) - agent._sentry_request_model = request_model_name - - # Wrap _fetch_response if it exists (for OpenAI models) to capture response model - if hasattr(model, "_fetch_response"): - original_fetch_response = model._fetch_response - - @wraps(original_fetch_response) - async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": - response = await original_fetch_response(*args, **kwargs) - if hasattr(response, "model") and response.model: - agent._sentry_response_model = str(response.model) - return response - - model._fetch_response = wrapped_fetch_response - - original_get_response = model.get_response - - @wraps(original_get_response) - async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": - mcp_tools = kwargs.get("tools") - hosted_tools = [] - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - result = await original_get_response(*args, **kwargs) - - # Get response model captured from _fetch_response and clean up - response_model = getattr(agent, "_sentry_response_model", None) - if response_model: - delattr(agent, "_sentry_response_model") - - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span(span, result, response_model, agent) - - return result - - model.get_response = wrapped_get_response - - # Also wrap stream_response for streaming support - if hasattr(model, "stream_response"): - original_stream_response = model.stream_response - - @wraps(original_stream_response) - async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": - span_kwargs = dict(kwargs) - if len(args) > 0: - span_kwargs["system_instructions"] = args[0] - if len(args) > 1: - span_kwargs["input"] = args[1] - - hosted_tools = [] - if len(args) > 3: - mcp_tools = args[3] - - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, span_kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - streaming_response = None - ttft_recorded = False - # Capture start time locally to avoid race conditions with concurrent requests - start_time = time.perf_counter() - - async for event in original_stream_response(*args, **kwargs): - # Detect first content token (text delta event) - if not ttft_recorded and hasattr(event, "delta"): - ttft = time.perf_counter() - start_time - set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) - ttft_recorded = True - - # Capture the full response from ResponseCompletedEvent - if hasattr(event, "response"): - streaming_response = event.response - yield event - - # Update span with response data (usage, output, model) - if streaming_response: - response_model = ( - str(streaming_response.model) - if hasattr(streaming_response, "model") - and streaming_response.model - else None - ) - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span( - span, streaming_response, response_model, agent - ) - - model.stream_response = wrapped_stream_response - - return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py deleted file mode 100644 index 5f9996595f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/runner.py +++ /dev/null @@ -1,221 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import agent_workflow_span, update_invoke_agent_span -from ..utils import _capture_exception - -try: - from agents.exceptions import AgentsException -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, AsyncIterator, Callable - - -def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run methods to - - create and manage a root span for the agent workflow runs. - - end the agent invocation span if an `AgentsException` is raised in `run()`. - - Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), - so it does not need to be wrapped separately. - """ - - @wraps(original_func) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - with agent_workflow_span(agent) as workflow_span: - # Set conversation ID on workflow span early so it's captured even on errors - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - run_result = await original_func(*args, **kwargs) - except AgentsException as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - - context_wrapper = getattr(exc.run_data, "context_wrapper", None) - if context_wrapper is not None: - invoke_agent_span = getattr( - context_wrapper, "_sentry_agent_span", None - ) - - if invoke_agent_span is not None and ( - ( - isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.end_timestamp is None - ) - or ( - not isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.timestamp is None - ) - ): - update_invoke_agent_span( - span=invoke_agent_span, - context=context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Invoke agent span is not finished in this case. - # This is much less likely to occur than other cases because - # AgentRunner.run() is "just" a while loop around _run_single_turn. - _capture_exception(exc) - reraise(*exc_info) - - invoke_agent_span = getattr( - run_result.context_wrapper, "_sentry_agent_span", None - ) - if not invoke_agent_span: - return run_result - - update_invoke_agent_span( - span=invoke_agent_span, - context=run_result.context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(None, None, None) - delattr(run_result.context_wrapper, "_sentry_agent_span") - return run_result - - return wrapper - - -def _create_run_streamed_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run_streamed method to - - create a root span for streaming agent workflow runs. - - end the workflow span if and only if the response stream is consumed or cancelled. - - Unlike run(), run_streamed() returns immediately with a RunResultStreaming object - while execution continues in a background task. The workflow span must stay open - throughout the streaming operation and close when streaming completes or is abandoned. - - Note: We don't use isolation_scope() here because it uses context variables that - cannot span async boundaries (the __enter__ and __exit__ would be called from - different async contexts, causing ValueError). - """ - - @wraps(original_func) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - # Capture conversation_id from kwargs if provided - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - # Start workflow span immediately (before run_streamed returns) - workflow_span = agent_workflow_span(agent) - workflow_span.__enter__() - - # Set conversation ID on workflow span early so it's captured even on errors - if conversation_id: - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - # Store span on agent for cleanup - agent._sentry_workflow_span = workflow_span - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - # Call original function to get RunResultStreaming - run_result = original_func(*args, **kwargs) - except Exception as exc: - # If run_streamed itself fails (not the background task), clean up immediately - workflow_span.__exit__(*sys.exc_info()) - _capture_exception(exc) - raise - - def _close_workflow_span() -> None: - if hasattr(agent, "_sentry_workflow_span"): - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - if hasattr(run_result, "stream_events"): - original_stream_events = run_result.stream_events - - @wraps(original_stream_events) - async def wrapped_stream_events( - *stream_args: "Any", **stream_kwargs: "Any" - ) -> "AsyncIterator[Any]": - try: - async for event in original_stream_events( - *stream_args, **stream_kwargs - ): - yield event - finally: - _close_workflow_span() - - run_result.stream_events = wrapped_stream_events - - if hasattr(run_result, "cancel"): - original_cancel = run_result.cancel - - @wraps(original_cancel) - def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": - try: - return original_cancel(*cancel_args, **cancel_kwargs) - finally: - _close_workflow_span() - - run_result.cancel = wrapped_cancel - - return run_result - - return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py deleted file mode 100644 index bd13b9d61a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/patches/tools.py +++ /dev/null @@ -1,70 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import execute_tool_span, update_execute_tool_span - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -async def _get_all_tools( - original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", -) -> "list[agents.Tool]": - """ - Responsible for creating and managing `gen_ai.execute_tool` spans. - """ - # Get the original tools - tools = await original_get_all_tools(agent, context_wrapper) - - wrapped_tools = [] - for tool in tools: - # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": - wrapped_tools.append(tool) - continue - - # Create a new FunctionTool with our wrapped invoke method - original_on_invoke = tool.on_invoke_tool - - def create_wrapped_invoke( - current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" - ) -> "Callable[..., Any]": - @wraps(current_on_invoke) - async def sentry_wrapped_on_invoke_tool( - *args: "Any", **kwargs: "Any" - ) -> "Any": - with execute_tool_span(current_tool, *args, **kwargs) as span: - # We can not capture exceptions in tool execution here because - # `_on_invoke_tool` is swallowing the exception here: - # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 - # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter - # I was unable to monkey patch it because those are evaluated at module import time - # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` - # because it is nested inside this import time code. As if they made it hard to patch on purpose... - result = await current_on_invoke(*args, **kwargs) - update_execute_tool_span(span, agent, current_tool, result) - - return result - - return sentry_wrapped_on_invoke_tool - - wrapped_tool = agents.FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), - strict_json_schema=tool.strict_json_schema, - is_enabled=tool.is_enabled, - ) - wrapped_tools.append(wrapped_tool) - - return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py deleted file mode 100644 index 08802a87a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .agent_workflow import agent_workflow_span # noqa: F401 -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .handoff import handoff_span # noqa: F401 -from .invoke_agent import ( - invoke_agent_span, # noqa: F401 - update_invoke_agent_span, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py deleted file mode 100644 index 758f06db8d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - from typing import Union - - import agents - - -def agent_workflow_span( - agent: "agents.Agent", -) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": - # Create a transaction or a span if an transaction is already active - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} - ) - - return span - - span = get_start_span_function()( - name=f"{agent.name} workflow", - origin=SPAN_ORIGIN, - ) - - return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py deleted file mode 100644 index f4f02cb674..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_input_data, - _set_output_data, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from agents import Agent - - -def ai_client_span( - agent: "Agent", get_response_kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - # TODO-anton: implement other types of operations. Now "chat" is hardcoded. - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - _set_agent_data(span, agent) - _set_input_data(span, get_response_kwargs) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - response: "Any", - response_model: "Optional[str]" = None, - agent: "Optional[Agent]" = None, -) -> None: - """Update AI client span with response data (works for streaming and non-streaming).""" - if hasattr(response, "usage") and response.usage: - _set_usage_data(span, response.usage) - - if hasattr(response, "output") and response.output: - _set_output_data(span, response) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - elif hasattr(response, "model") and response.model: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) - - # Set conversation ID from agent if available - if agent: - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py deleted file mode 100644 index fd3a430951..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def execute_tool_span( - tool: "agents.Tool", *args: "Any", **kwargs: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool.name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool.name, - SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool.name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - - set_on_span = span.set_data - - if should_send_default_pii(): - input = args[1] - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - agent: "agents.Agent", - tool: "agents.Tool", - result: "Any", -) -> None: - _set_agent_data(span, agent) - - if isinstance(result, str) and result.startswith( - "An error occurred while running the tool" - ): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if should_send_default_pii(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py deleted file mode 100644 index ea91464afb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - import agents - - -def handoff_span( - context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str -) -> None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"handoff from {from_agent.name} to {to_agent_name}", - attributes={ - "sentry.op": OP.GEN_AI_HANDOFF, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "handoff", - }, - ) as span: - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_HANDOFF, - name=f"handoff from {from_agent.name} to {to_agent_name}", - origin=SPAN_ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") - - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py deleted file mode 100644 index c21145ac4a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _set_usage_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def invoke_agent_span( - context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {agent.name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - start_span_function = get_start_span_function() - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent.name}", - origin=SPAN_ORIGIN, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - if should_send_default_pii(): - messages = [] - if agent.instructions: - message = ( - agent.instructions - if isinstance(agent.instructions, str) - else safe_serialize(agent.instructions) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "system", - } - ) - - original_input = kwargs.get("original_input") - if original_input is not None: - message = ( - original_input - if isinstance(original_input, str) - else safe_serialize(original_input) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "user", - } - ) - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - _set_agent_data(span, agent) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - context: "agents.RunContextWrapper", - agent: "agents.Agent", - output: "Any" = None, -) -> None: - # Add aggregated usage data from context_wrapper - if hasattr(context, "usage"): - _set_usage_data(span, context.usage) - - if should_send_default_pii(): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py deleted file mode 100644 index 224a5f66ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openai_agents/utils.py +++ /dev/null @@ -1,244 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai._openai_completions_api import _transform_system_instructions -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions, - _is_system_instruction, -) -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - normalize_message_role, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import should_truncate_gen_ai_input -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Union - - from agents import TResponseInputItem, Usage - - from sentry_sdk._types import TextPart - -try: - import agents - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai_agents", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span( - SPANDATA.GEN_AI_SYSTEM, "openai" - ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. - - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) - - if agent.model_settings.max_tokens: - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) - - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - if agent.model_settings.presence_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - agent.model_settings.presence_penalty, - ) - - if agent.model_settings.temperature: - set_on_span( - SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature - ) - - if agent.model_settings.top_p: - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) - - if agent.model_settings.frequency_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - agent.model_settings.frequency_penalty, - ) - - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage.input_tokens_details.cached_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage.output_tokens_details.reasoning_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) - - -def _set_input_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - get_response_kwargs: "dict[str, Any]", -) -> None: - if not should_send_default_pii(): - return - request_messages = [] - - messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) - - instructions_text_parts: "list[TextPart]" = [] - explicit_instructions = get_response_kwargs.get("system_instructions") - if explicit_instructions is not None: - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions(messages) - - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - non_system_messages = [ - message for message in messages if not _is_system_instruction(message) - ] - for message in non_system_messages: - if "role" in message: - normalized_role = normalize_message_role(message.get("role")) # type: ignore - content = message.get("content") # type: ignore - request_messages.append( - { - "role": normalized_role, - "content": ( - [{"type": "text", "text": content}] - if isinstance(content, str) - else content - ), - } - ) - else: - if message.get("type") == "function_call": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, - "content": [message], - } - ) - elif message.get("type") == "function_call_output": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, - "content": [message], - } - ) - - normalized_messages = normalize_message_roles(request_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - if not should_send_default_pii(): - return - - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in result.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - else: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py deleted file mode 100644 index 281604fe38..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/openfeature.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from openfeature import api - from openfeature.hook import Hook - - if TYPE_CHECKING: - from openfeature.hook import HookContext, HookHints -except ImportError: - raise DidNotEnable("OpenFeature is not installed") - - -class OpenFeatureIntegration(Integration): - identifier = "openfeature" - - @staticmethod - def setup_once() -> None: - # Register the hook within the global openfeature hooks list. - api.add_hooks(hooks=[OpenFeatureHook()]) - - -class OpenFeatureHook(Hook): - def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: - if isinstance(details.value, bool): - add_feature_flag(details.flag_key, details.value) - - def error( - self, hook_context: "HookContext", exception: Exception, hints: "HookHints" - ) -> None: - if isinstance(hook_context.default_value, bool): - add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py deleted file mode 100644 index d76b8058ee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor - -__all__ = [ - "SentryPropagator", - "SentrySpanProcessor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py deleted file mode 100644 index d6733036ea..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable - -try: - from opentelemetry.context import create_key -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -SENTRY_TRACE_KEY = create_key("sentry-trace") -SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py deleted file mode 100644 index 472e8181f9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/integration.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -IMPORTANT: The contents of this file are part of a proof of concept and as such -are experimental and not suitable for production use. They may be changed or -removed at any time without prior notice. -""" - -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - -try: - from opentelemetry import trace - from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -try: - from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] - DjangoInstrumentor, - ) -except ImportError: - DjangoInstrumentor = None - - -CONFIGURABLE_INSTRUMENTATIONS = { - DjangoInstrumentor: {"is_sql_commentor_enabled": True}, -} - - -class OpenTelemetryIntegration(Integration): - identifier = "opentelemetry" - - @staticmethod - def setup_once() -> None: - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if has_span_streaming_enabled(options): - logger.warning( - "[OTel] OpenTelemetryIntegration is not compatible with span streaming " - "(trace_lifecycle='stream') and will be disabled." - ) - return - - warnings.warn( - "OpenTelemetryIntegration is deprecated. " - "Please use OTLPIntegration instead: " - "https://docs.sentry.io/platforms/python/integrations/otlp/", - DeprecationWarning, - stacklevel=2, - ) - - _setup_sentry_tracing() - # _setup_instrumentors() - - logger.debug("[OTel] Finished setting up OpenTelemetry integration") - - -def _setup_sentry_tracing() -> None: - provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) - trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) - - -def _setup_instrumentors() -> None: - for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): - if instrumentor is None: - continue - instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py deleted file mode 100644 index 5b5f13861d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/propagator.py +++ /dev/null @@ -1,128 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data - -try: - from opentelemetry import trace - from opentelemetry.context import ( - Context, - get_current, - set_value, - ) - from opentelemetry.propagators.textmap import ( - CarrierT, - Getter, - Setter, - TextMapPropagator, - default_getter, - default_setter, - ) - from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Set - - -class SentryPropagator(TextMapPropagator): - """ - Propagates tracing headers for Sentry's tracing system in a way OTel understands. - """ - - def extract( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - getter: "Getter[CarrierT]" = default_getter, - ) -> "Context": - if context is None: - context = get_current() - - sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) - if not sentry_trace: - return context - - sentrytrace = extract_sentrytrace_data(sentry_trace[0]) - if not sentrytrace: - return context - - context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) - - trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] - - span_context = SpanContext( - trace_id=int(trace_id, 16), # type: ignore - span_id=int(span_id, 16), # type: ignore - # we simulate a sampled trace on the otel side and leave the sampling to sentry - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - - baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) - - if baggage_header: - baggage = Baggage.from_incoming_header(baggage_header[0]) - else: - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and frozen and won't be populated as head SDK. - baggage = Baggage(sentry_items={}) - - baggage.freeze() - context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) - - span = NonRecordingSpan(span_context) - modified_context = trace.set_span_in_context(span, context) - return modified_context - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - if context is None: - context = get_current() - - current_span = trace.get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - span_id = trace.format_span_id(current_span_context.span_id) - - span_map = SentrySpanProcessor.otel_span_map - sentry_span = span_map.get(span_id, None) - if not sentry_span: - return - - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) - - if sentry_span.containing_transaction: - baggage = sentry_span.containing_transaction.get_baggage() - if baggage: - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - @property - def fields(self) -> "Set[str]": - return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py deleted file mode 100644 index 1efd2ac455..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/opentelemetry/span_processor.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from time import time -from typing import TYPE_CHECKING, cast - -from urllib3.util import parse_url as urlparse - -from sentry_sdk import get_client, start_transaction -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing import Transaction - -try: - from opentelemetry.context import get_value - from opentelemetry.sdk.trace import ReadableSpan as OTelSpan - from opentelemetry.sdk.trace import SpanProcessor - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import ( - SpanKind, - format_span_id, - format_trace_id, - get_current_span, - ) - from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from opentelemetry import context as context_api - from opentelemetry.trace import SpanContext - - from sentry_sdk._types import Event, Hint - -OPEN_TELEMETRY_CONTEXT = "otel" -SPAN_MAX_TIME_OPEN_MINUTES = 10 -SPAN_ORIGIN = "auto.otel" - - -def link_trace_context_to_error_event( - event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" -) -> "Event": - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return event - - if hasattr(event, "type") and event["type"] == "transaction": - return event - - otel_span = get_current_span() - if not otel_span: - return event - - ctx = otel_span.get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return event - - sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) - if not sentry_span: - return event - - contexts = event.setdefault("contexts", {}) - contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) - - return event - - -class SentrySpanProcessor(SpanProcessor): - """ - Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. - """ - - # The mapping from otel span ids to sentry spans - otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} - - # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES - open_spans: "dict[int, set[str]]" = {} - - initialized: "bool" = False - - def __new__(cls) -> "SentrySpanProcessor": - if not hasattr(cls, "instance"): - cls.instance = super().__new__(cls) - - # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). - return cls.instance # type: ignore[misc] - - def __init__(self) -> None: - if self.initialized: - return - - @add_global_event_processor - def global_event_processor(event: "Event", hint: "Hint") -> "Event": - return link_trace_context_to_error_event(event, self.otel_span_map) - - self.initialized = True - - def _prune_old_spans(self: "SentrySpanProcessor") -> None: - """ - Prune spans that have been open for too long. - """ - current_time_minutes = int(time() / 60) - for span_start_minutes in list( - self.open_spans.keys() - ): # making a list because we change the dict - # prune empty open spans buckets - if self.open_spans[span_start_minutes] == set(): - self.open_spans.pop(span_start_minutes) - - # prune old buckets - elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: - for span_id in self.open_spans.pop(span_start_minutes): - self.otel_span_map.pop(span_id, None) - - def on_start( - self, - otel_span: "OTelSpan", - parent_context: "Optional[context_api.Context]" = None, - ) -> None: - client = get_client() - - if not client.parsed_dsn: - return - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - if self._is_sentry_span(otel_span): - return - - trace_data = self._get_trace_data( - span_context, otel_span.parent, parent_context - ) - - parent_span_id = trace_data["parent_span_id"] - sentry_parent_span = ( - self.otel_span_map.get(parent_span_id) if parent_span_id else None - ) - - start_timestamp = None - if otel_span.start_time is not None: - start_timestamp = datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span = None - if sentry_parent_span: - sentry_span = sentry_parent_span.start_child( - span_id=trace_data["span_id"], - name=otel_span.name, - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - else: - sentry_span = start_transaction( - name=otel_span.name, - span_id=trace_data["span_id"], - parent_span_id=parent_span_id, - trace_id=trace_data["trace_id"], - baggage=trace_data["baggage"], - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - - self.otel_span_map[trace_data["span_id"]] = sentry_span - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).add( - trace_data["span_id"] - ) - - self._prune_old_spans() - - def on_end(self, otel_span: "OTelSpan") -> None: - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - span_id = format_span_id(span_context.span_id) - sentry_span = self.otel_span_map.pop(span_id, None) - if not sentry_span: - return - - sentry_span.op = otel_span.name - - self._update_span_with_otel_status(sentry_span, otel_span) - - if isinstance(sentry_span, Transaction): - sentry_span.name = otel_span.name - sentry_span.set_context( - OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) - ) - self._update_transaction_with_otel_data(sentry_span, otel_span) - - else: - self._update_span_with_otel_data(sentry_span, otel_span) - - end_timestamp = None - if otel_span.end_time is not None: - end_timestamp = datetime.fromtimestamp( - otel_span.end_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span.finish(end_timestamp=end_timestamp) - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) - - self._prune_old_spans() - - def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: - """ - Break infinite loop: - HTTP requests to Sentry are caught by OTel and send again to Sentry. - """ - otel_span_url = None - if otel_span.attributes is not None: - otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) - otel_span_url = cast("Optional[str]", otel_span_url) - - parsed_dsn = get_client().parsed_dsn - dsn_url = parsed_dsn.netloc if parsed_dsn else None - - if otel_span_url and dsn_url and dsn_url in otel_span_url: - return True - - return False - - def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": - """ - Returns the OTel context for Sentry. - See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context - """ - ctx = {} - - if otel_span.attributes: - ctx["attributes"] = dict(otel_span.attributes) - - if otel_span.resource.attributes: - ctx["resource"] = dict(otel_span.resource.attributes) - - return ctx - - def _get_trace_data( - self, - span_context: "SpanContext", - parent_span_context: "Optional[SpanContext]", - parent_context: "Optional[context_api.Context]", - ) -> "dict[str, Any]": - """ - Extracts tracing information from one OTel span's context and its parent OTel context. - """ - trace_data: "dict[str, Any]" = {} - - span_id = format_span_id(span_context.span_id) - trace_data["span_id"] = span_id - - trace_id = format_trace_id(span_context.trace_id) - trace_data["trace_id"] = trace_id - - parent_span_id = ( - format_span_id(parent_span_context.span_id) if parent_span_context else None - ) - trace_data["parent_span_id"] = parent_span_id - - sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) - sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) - trace_data["parent_sampled"] = ( - sentry_trace_data["parent_sampled"] if sentry_trace_data else None - ) - - baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) - trace_data["baggage"] = baggage - - return trace_data - - def _update_span_with_otel_status( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Set the Sentry span status from the OTel span - """ - if otel_span.status.is_unset: - return - - if otel_span.status.is_ok: - sentry_span.set_status(SPANSTATUS.OK) - return - - sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - def _update_span_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Convert OTel span data and update the Sentry span with it. - This should eventually happen on the server when ingesting the spans. - """ - sentry_span.set_data("otel.kind", otel_span.kind) - - op = otel_span.name - description = otel_span.name - - if otel_span.attributes is not None: - for key, val in otel_span.attributes.items(): - sentry_span.set_data(key, val) - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - http_method = cast("Optional[str]", http_method) - - db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) - - if http_method: - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - description = http_method - - peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) - if peer_name: - description += " {}".format(peer_name) - - target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) - if target: - description += " {}".format(target) - - if not peer_name and not target: - url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) - url = cast("Optional[str]", url) - if url: - parsed_url = urlparse(url) - url = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path - ) - description += " {}".format(url) - - status_code = otel_span.attributes.get( - SpanAttributes.HTTP_STATUS_CODE, None - ) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - elif db_query: - op = "db" - statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) - statement = cast("Optional[str]", statement) - if statement: - description = statement - - sentry_span.op = op - sentry_span.description = description - - def _update_transaction_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - if otel_span.attributes is None: - return - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - - if http_method: - status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py deleted file mode 100644 index b91f2cfd21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/otlp.py +++ /dev/null @@ -1,223 +0,0 @@ -from sentry_sdk import capture_event, get_client -from sentry_sdk.consts import VERSION, EndpointType -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import register_external_propagation_context -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - event_from_exception, - logger, -) - -try: - from opentelemetry.context import ( - Context, - get_current, - get_value, - ) - from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter - from opentelemetry.propagate import set_global_textmap - from opentelemetry.propagators.textmap import ( - CarrierT, - Setter, - default_setter, - ) - from opentelemetry.sdk.trace import Span, TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - from opentelemetry.trace import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - SpanContext, - format_span_id, - format_trace_id, - get_current_span, - get_tracer_provider, - set_tracer_provider, - ) - - from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY - from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -except ImportError: - raise DidNotEnable("opentelemetry-distro[otlp] is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple - - -def otel_propagation_context() -> "Optional[Tuple[str, str]]": - """ - Get the (trace_id, span_id) from opentelemetry if exists. - """ - ctx = get_current_span().get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return None - - return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) - - -def setup_otlp_traces_exporter( - dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None -) -> None: - tracer_provider = get_tracer_provider() - - if not isinstance(tracer_provider, TracerProvider): - logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") - tracer_provider = TracerProvider() - set_tracer_provider(tracer_provider) - - endpoint = None - headers = None - if collector_url: - endpoint = collector_url - logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") - elif dsn: - auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") - endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) - headers = {"X-Sentry-Auth": auth.to_header()} - logger.debug(f"[OTLP] Sending traces to {endpoint}") - - otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) - span_processor = BatchSpanProcessor(otlp_exporter) - tracer_provider.add_span_processor(span_processor) - - -_sentry_patched_exception = False - - -def setup_capture_exceptions() -> None: - """ - Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. - """ - global _sentry_patched_exception - _original_record_exception = Span.record_exception - - if _sentry_patched_exception: - return - - def _sentry_patched_record_exception( - self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration and otlp_integration.capture_exceptions: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=get_client().options, - mechanism={"type": OTLPIntegration.identifier, "handled": False}, - ) - capture_event(event, hint=hint) - - _original_record_exception(self, exception, *args, **kwargs) - - Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] - _sentry_patched_exception = True - - -class SentryOTLPPropagator(SentryPropagator): - """ - We need to override the inject of the older propagator since that - is SpanProcessor based. - - !!! Note regarding baggage: - We cannot meaningfully populate a new baggage as a head SDK - when we are using OTLP since we don't have any sort of transaction semantic to - track state across a group of spans. - - For incoming baggage, we just pass it on as is so that case is correctly handled. - """ - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration is None: - return - - if context is None: - context = get_current() - - current_span = get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - sentry_trace = _to_traceparent(current_span_context) - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) - - baggage = get_value(SENTRY_BAGGAGE_KEY, context) - if baggage is not None and isinstance(baggage, Baggage): - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - -def _to_traceparent(span_context: "SpanContext") -> str: - """ - Helper method to generate the sentry-trace header. - """ - span_id = format_span_id(span_context.span_id) - trace_id = format_trace_id(span_context.trace_id) - sampled = span_context.trace_flags.sampled - - return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" - - -class OTLPIntegration(Integration): - """ - Automatically setup OTLP ingestion from the DSN. - - :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. - Set to False to setup the TracerProvider manually. - :param collector_url: URL of your own OpenTelemetry collector, defaults to None. - When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. - :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. - Set to False to configure propagators manually or to disable propagation. - :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. - Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. - """ - - identifier = "otlp" - - def __init__( - self, - setup_otlp_traces_exporter: bool = True, - collector_url: "Optional[str]" = None, - setup_propagator: bool = True, - capture_exceptions: bool = False, - ) -> None: - self.setup_otlp_traces_exporter = setup_otlp_traces_exporter - self.collector_url = collector_url - self.setup_propagator = setup_propagator - self.capture_exceptions = capture_exceptions - - @staticmethod - def setup_once() -> None: - logger.debug("[OTLP] Setting up trace linking for all events") - register_external_propagation_context(otel_propagation_context) - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if self.setup_otlp_traces_exporter: - logger.debug("[OTLP] Setting up OTLP exporter") - dsn: "Optional[str]" = options.get("dsn") if options else None - setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) - - if self.setup_propagator: - logger.debug("[OTLP] Setting up propagator for distributed tracing") - # TODO-neel better propagator support, chain with existing ones if possible instead of replacing - set_global_textmap(SentryOTLPPropagator()) - - setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py deleted file mode 100644 index 569e3e1fa5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pure_eval.py +++ /dev/null @@ -1,134 +0,0 @@ -import ast -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import serializer -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, List, Optional, Tuple - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - -try: - from pure_eval import Evaluator -except ImportError: - raise DidNotEnable("pure_eval is not installed") - -try: - # Used implicitly, just testing it's available - import asttokens # noqa -except ImportError: - raise DidNotEnable("asttokens is not installed") - - -class PureEvalIntegration(Integration): - identifier = "pure_eval" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - sentry_frame["vars"] = ( - pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] - ) - return event - - -def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": - source = Source.for_frame(frame) - if not source.tree: - return {} - - statements = source.statements_at_line(frame.f_lineno) - if not statements: - return {} - - scope = stmt = list(statements)[0] - while True: - # Get the parent first in case the original statement is already - # a function definition, e.g. if we're calling a decorator - # In that case we still want the surrounding scope, not that function - scope = scope.parent - if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): - break - - evaluator = Evaluator.from_frame(frame) - expressions = evaluator.interesting_expressions_grouped(scope) - - def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": - # Prioritise expressions with a node closer to the statement executed - # without being after that statement - # A higher return value is better - the expression will appear - # earlier in the list of values and is less likely to be trimmed - nodes, _value = expression - - def start(n: "ast.expr") -> "Tuple[int, int]": - return (n.lineno, n.col_offset) - - nodes_before_stmt = [ - node for node in nodes if start(node) < stmt.last_token.end - ] - if nodes_before_stmt: - # The position of the last node before or in the statement - return max(start(node) for node in nodes_before_stmt) - else: - # The position of the first node after the statement - # Negative means it's always lower priority than nodes that come before - # Less negative means closer to the statement and higher priority - lineno, col_offset = min(start(node) for node in nodes) - return (-lineno, -col_offset) - - # This adds the first_token and last_token attributes to nodes - atok = source.asttokens() - - expressions.sort(key=closeness, reverse=True) - vars = { - atok.get_text(nodes[0]): value - for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] - } - return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py deleted file mode 100644 index 81e7cf8090..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/__init__.py +++ /dev/null @@ -1,187 +0,0 @@ -import functools - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import capture_internal_exceptions, parse_version - -try: - import pydantic_ai # type: ignore # noqa: F401 - from pydantic_ai import Agent -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -from importlib.metadata import PackageNotFoundError, version -from typing import TYPE_CHECKING - -from .patches import ( - _patch_agent_run, - _patch_graph_nodes, - _patch_tool_execution, -) -from .spans.ai_client import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any - - from pydantic_ai import ModelRequestContext, RunContext - from pydantic_ai.capabilities import Hooks # type: ignore - from pydantic_ai.messages import ModelResponse # type: ignore - - -def register_hooks(hooks: "Hooks") -> None: - """ - Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. - """ - - @hooks.on.before_model_request # type: ignore - async def on_request( - ctx: "RunContext[None]", request_context: "ModelRequestContext" - ) -> "ModelRequestContext": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return request_context - - span = ai_client_span( - messages=request_context.messages, - agent=None, - model=request_context.model, - model_settings=request_context.model_settings, - ) - - run_context_metadata["_sentry_span"] = span - span.__enter__() - - return request_context - - @hooks.on.after_model_request # type: ignore - async def on_response( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - response: "ModelResponse", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return response - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - return response - - update_ai_client_span(span, response) - span.__exit__(None, None, None) - - return response - - @hooks.on.model_request_error # type: ignore - async def on_error( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - error: "Exception", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - - if not isinstance(run_context_metadata, dict): - raise error - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - raise error - - with capture_internal_exceptions(): - span.__exit__(type(error), error, error.__traceback__) - - raise error - - original_init = Agent.__init__ - - @functools.wraps(original_init) - def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: - caps = list(kwargs.get("capabilities") or []) - caps.append(hooks) - kwargs["capabilities"] = caps - - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {} # Used as shared reference between hooks - - return original_init(self, *args, **kwargs) - - Agent.__init__ = patched_init - - -class PydanticAIIntegration(Integration): - """ - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. - 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. - - Each run invocation has `RunContext` objects that are passed to the library hooks. - 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. - Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). - The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. - - The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that - instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. - """ - - identifier = "pydantic_ai" - origin = f"auto.ai.{identifier}" - using_request_hooks = False - - def __init__( - self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True - ) -> None: - """ - Initialize the Pydantic AI integration. - - Args: - include_prompts: Whether to include prompts and messages in span data. - Requires send_default_pii=True. Defaults to True. - handled_tool_exceptions: Capture tool call exceptions that Pydantic AI - internally prevents from bubbling up. - """ - self.include_prompts = include_prompts - self.handled_tool_call_exceptions = handled_tool_call_exceptions - - @staticmethod - def setup_once() -> None: - """ - Set up the pydantic-ai integration. - - This patches the key methods in pydantic-ai to create Sentry spans for: - - Agent invocations (Agent.run methods) - - Model requests (AI client calls) - - Tool executions - """ - _patch_agent_run() - _patch_tool_execution() - - PydanticAIIntegration.using_request_hooks = False - try: - PYDANTIC_AI_VERSION = version("pydantic-ai-slim") - except PackageNotFoundError: - return - - PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) - if PYDANTIC_AI_VERSION is None: - return - - # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c - if PYDANTIC_AI_VERSION < ( - 1, - 73, - ): - _patch_graph_nodes() - return - - try: - from pydantic_ai.capabilities import Hooks - except ImportError: - return - - PydanticAIIntegration.using_request_hooks = True - hooks = Hooks() - register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py deleted file mode 100644 index afa66dc47d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py deleted file mode 100644 index d0ea6242b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .agent_run import _patch_agent_run # noqa: F401 -from .graph_nodes import _patch_graph_nodes # noqa: F401 -from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py deleted file mode 100644 index 3039e40698..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ /dev/null @@ -1,198 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import invoke_agent_span, update_invoke_agent_span -from ..utils import _capture_exception, pop_agent, push_agent - -try: - from pydantic_ai.agent import Agent # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - -class _StreamingContextManagerWrapper: - """Wrapper for streaming methods that return async context managers.""" - - def __init__( - self, - agent: "Any", - original_ctx_manager: "Any", - user_prompt: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = True, - ) -> None: - self.agent = agent - self.original_ctx_manager = original_ctx_manager - self.user_prompt = user_prompt - self.model = model - self.model_settings = model_settings - self.is_streaming = is_streaming - self._isolation_scope: "Any" = None - self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None - self._result: "Any" = None - - async def __aenter__(self) -> "Any": - # Set up isolation scope and invoke_agent span - self._isolation_scope = sentry_sdk.isolation_scope() - self._isolation_scope.__enter__() - - # Create invoke_agent span (will be closed in __aexit__) - self._span = invoke_agent_span( - self.user_prompt, - self.agent, - self.model, - self.model_settings, - self.is_streaming, - ) - self._span.__enter__() - - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur - push_agent(self.agent, self.is_streaming) - - # Enter the original context manager - result = await self.original_ctx_manager.__aenter__() - self._result = result - return result - - async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: - try: - # Exit the original context manager first - await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) - - # Update span with result if successful - if exc_type is None and self._result and self._span is not None: - update_invoke_agent_span(self._span, self._result) - finally: - # Pop agent from contextvar stack - pop_agent() - - # Clean up invoke span - if self._span: - self._span.__exit__(exc_type, exc_val, exc_tb) - - # Clean up isolation scope - if self._isolation_scope: - self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) - - -def _create_run_wrapper( - original_func: "Callable[..., Any]", is_streaming: bool = False -) -> "Callable[..., Any]": - """ - Wraps the Agent.run method to create an invoke_agent span. - - Args: - original_func: The original run method - is_streaming: Whether this is a streaming method (for future use) - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Create invoke_agent span - with invoke_agent_span( - user_prompt, self, model, model_settings, is_streaming - ) as span: - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in finally even if exceptions occur - push_agent(self, is_streaming) - - try: - result = await original_func(self, *args, **kwargs) - - # Update span with result - update_invoke_agent_span(span, result) - - return result - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - finally: - # Pop agent from contextvar stack - pop_agent() - - return wrapper - - -def _create_streaming_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps run_stream method that returns an async context manager. - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Call original function to get the context manager - original_ctx_manager = original_func(self, *args, **kwargs) - - # Wrap it with our instrumentation - return _StreamingContextManagerWrapper( - agent=self, - original_ctx_manager=original_ctx_manager, - user_prompt=user_prompt, - model=model, - model_settings=model_settings, - is_streaming=True, - ) - - return wrapper - - -def _patch_agent_run() -> None: - """ - Patches the Agent run methods to create spans for agent execution. - - This patches both non-streaming (run, run_sync) and streaming - (run_stream, run_stream_events) methods. - """ - - # Store original methods - original_run = Agent.run - original_run_stream = Agent.run_stream - - # Wrap and apply patches for non-streaming methods - Agent.run = _create_run_wrapper(original_run, is_streaming=False) - - # Wrap and apply patches for streaming methods - Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py deleted file mode 100644 index afb10395f4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +++ /dev/null @@ -1,106 +0,0 @@ -from contextlib import asynccontextmanager -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import ( - ai_client_span, - update_ai_client_span, -) - -try: - from pydantic_ai._agent_graph import ModelRequestNode # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - -def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": - """Extract common data needed for creating chat spans. - - Returns: - Tuple of (messages, model, model_settings) - """ - # Extract model and settings from context - model = None - model_settings = None - if hasattr(ctx, "deps"): - model = getattr(ctx.deps, "model", None) - model_settings = getattr(ctx.deps, "model_settings", None) - - # Build full message list: history + current request - messages = [] - if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): - messages.extend(ctx.state.message_history) - - current_request = getattr(node, "request", None) - if current_request: - messages.append(current_request) - - return messages, model, model_settings - - -def _patch_graph_nodes() -> None: - """ - Patches the graph node execution to create appropriate spans. - - ModelRequestNode -> Creates ai_client span for model requests - CallToolsNode -> Handles tool calls (spans created in tool patching) - """ - - # Patch ModelRequestNode to create ai_client spans - original_model_request_run = ModelRequestNode.run - - @wraps(original_model_request_run) - async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - with ai_client_span(messages, None, model, model_settings) as span: - result = await original_model_request_run(self, ctx) - - # Extract response from result if available - model_response = None - if hasattr(result, "model_response"): - model_response = result.model_response - - update_ai_client_span(span, model_response) - return result - - ModelRequestNode.run = wrapped_model_request_run - - # Patch ModelRequestNode.stream for streaming requests - original_model_request_stream = ModelRequestNode.stream - - def create_wrapped_stream( - original_stream_method: "Callable[..., Any]", - ) -> "Callable[..., Any]": - """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" - - @asynccontextmanager - @wraps(original_stream_method) - async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - # Create chat span for streaming request - with ai_client_span(messages, None, model, model_settings) as span: - # Call the original stream method - async with original_stream_method(self, ctx) as stream: - yield stream - - # After streaming completes, update span with response data - # The ModelRequestNode stores the final response in _result - model_response = None - if hasattr(self, "_result") and self._result is not None: - # _result is a NextNode containing the model_response - if hasattr(self._result, "model_response"): - model_response = self._result.model_response - - update_ai_client_span(span, model_response) - - return wrapped_model_request_stream - - ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py deleted file mode 100644 index 5646b5d47c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ /dev/null @@ -1,177 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import execute_tool_span, update_execute_tool_span -from ..utils import _capture_exception, get_current_agent - -if TYPE_CHECKING: - from typing import Any - -try: - try: - from pydantic_ai.tool_manager import ToolManager # type: ignore - except ImportError: - from pydantic_ai._tool_manager import ToolManager # type: ignore - - from pydantic_ai.exceptions import ToolRetryError # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -def _patch_tool_execution() -> None: - if hasattr(ToolManager, "execute_tool_call"): - _patch_execute_tool_call() - - elif hasattr(ToolManager, "_call_tool"): - # older versions - _patch_call_tool() - - -def _patch_execute_tool_call() -> None: - original_execute_tool_call = ToolManager.execute_tool_call - - @wraps(original_execute_tool_call) - async def wrapped_execute_tool_call( - self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not validated or not hasattr(validated, "call"): - return await original_execute_tool_call(self, validated, *args, **kwargs) - - # Extract tool info before calling original - call = validated.call - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_execute_tool_call( - self, - validated, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - return await original_execute_tool_call(self, validated, *args, **kwargs) - - ToolManager.execute_tool_call = wrapped_execute_tool_call - - -def _patch_call_tool() -> None: - """ - Patch ToolManager._call_tool to create execute_tool spans. - - This is the single point where ALL tool calls flow through in pydantic_ai, - regardless of toolset type (function, MCP, combined, wrapper, etc.). - - By patching here, we avoid: - - Patching multiple toolset classes - - Dealing with signature mismatches from instrumented MCP servers - - Complex nested toolset handling - """ - original_call_tool = ToolManager._call_tool - - @wraps(original_call_tool) - async def wrapped_call_tool( - self: "Any", call: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Extract tool info before calling original - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_call_tool( - self, - call, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - # No span context - just call original - return await original_call_tool( - self, - call, - *args, - **kwargs, - ) - - ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py deleted file mode 100644 index 574046d645..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py deleted file mode 100644 index 27deb0c55c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ /dev/null @@ -1,331 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _get_model_name, - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, - get_current_agent, - get_is_streaming, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Dict, List, Union - - from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore - - from sentry_sdk._types import TextPart as SentryTextPart - -try: - from pydantic_ai.messages import ( - BaseToolCallPart, - BaseToolReturnPart, - BinaryContent, - ImageUrl, - SystemPromptPart, - TextPart, - ThinkingPart, - UserPromptPart, - ) -except ImportError: - # Fallback if these classes are not available - BaseToolCallPart = None - BaseToolReturnPart = None - SystemPromptPart = None - UserPromptPart = None - TextPart = None - ThinkingPart = None - BinaryContent = None - ImageUrl = None - - -def _transform_system_instructions( - permanent_instructions: "list[SystemPromptPart]", - current_instructions: "list[str]", -) -> "list[SentryTextPart]": - text_parts: "list[SentryTextPart]" = [ - { - "type": "text", - "content": instruction.content, - } - for instruction in permanent_instructions - ] - - text_parts.extend( - { - "type": "text", - "content": instruction, - } - for instruction in current_instructions - ) - - return text_parts - - -def _get_system_instructions( - messages: "list[ModelMessage]", -) -> "tuple[list[SystemPromptPart], list[str]]": - permanent_instructions = [] - current_instructions = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - if SystemPromptPart and isinstance(part, SystemPromptPart): - permanent_instructions.append(part) - - if hasattr(msg, "instructions") and msg.instructions is not None: - current_instructions.append(msg.instructions) - - return permanent_instructions, current_instructions - - -def _set_input_messages( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" -) -> None: - """Set input messages data on a span.""" - if not _should_send_prompts(): - return - - if not messages: - return - - permanent_instructions, current_instructions = _get_system_instructions(messages) - if len(permanent_instructions) > 0 or len(current_instructions) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - - try: - formatted_messages = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - role = "user" - # Use isinstance checks with proper base classes - if SystemPromptPart and isinstance(part, SystemPromptPart): - continue - elif ( - (TextPart and isinstance(part, TextPart)) - or (ThinkingPart and isinstance(part, ThinkingPart)) - or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) - ): - role = "assistant" - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - role = "tool" - - content: "List[Dict[str, Any] | str]" = [] - tool_calls = None - tool_call_id = None - - # Handle ToolCallPart (assistant requesting tool use) - if BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = {} - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - if tool_call_data: - tool_calls = [tool_call_data] - # Handle ToolReturnPart (tool result) - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - if hasattr(part, "tool_name"): - tool_call_id = part.tool_name - if hasattr(part, "content"): - content.append({"type": "text", "text": str(part.content)}) - # Handle regular content - elif hasattr(part, "content"): - if isinstance(part.content, str): - content.append({"type": "text", "text": part.content}) - elif isinstance(part.content, list): - for item in part.content: - if isinstance(item, str): - content.append({"type": "text", "text": item}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - else: - content.append(safe_serialize(item)) - else: - content.append({"type": "text", "text": str(part.content)}) - # Add message if we have content or tool calls - if content or tool_calls: - message: "Dict[str, Any]" = {"role": role} - if content: - message["content"] = content - if tool_calls: - message["tool_calls"] = tool_calls - if tool_call_id: - message["tool_call_id"] = tool_call_id - formatted_messages.append(message) - - if formatted_messages: - normalized_messages = normalize_message_roles(formatted_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - except Exception: - # If we fail to format messages, just skip it - pass - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" -) -> None: - """Set output data on a span.""" - if not _should_send_prompts(): - return - - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - - try: - # Extract text from ModelResponse - if hasattr(response, "parts"): - texts = [] - tool_calls = [] - - for part in response.parts: - if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): - texts.append(part.content) - elif BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = { - "type": "function", - } - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - tool_calls.append(tool_call_data) - - if texts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) - - if tool_calls: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) - ) - - except Exception: - # If we fail to format output, just skip it - pass - - -def ai_client_span( - messages: "Any", agent: "Any", model: "Any", model_settings: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for an AI client call (model request). - - Args: - messages: Full conversation history (list of messages) - agent: Agent object - model: Model object - model_settings: Model settings - """ - # Determine model name for span name - model_obj = model - if agent and hasattr(agent, "model"): - model_obj = agent.model - - model_name = _get_model_name(model_obj) or "unknown" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - # Set streaming flag from contextvar - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - - # Add available tools if agent is available - agent_obj = agent or get_current_agent() - _set_available_tools(span, agent_obj) - - # Set input messages (full conversation history) - if messages: - _set_input_messages(span, messages) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" -) -> None: - """Update the AI client span with response data.""" - if not span: - return - - # Set usage data if available - if model_response and hasattr(model_response, "usage"): - _set_usage_data(span, model_response.usage) - - # Set output data - _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py deleted file mode 100644 index 7648c1418a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _should_send_prompts - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from pydantic_ai._tool_manager import ToolDefinition # type: ignore - - -def execute_tool_span( - tool_name: str, - tool_args: "Any", - agent: "Any", - tool_definition: "Optional[ToolDefinition]" = None, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for tool execution. - - Args: - tool_name: The name of the tool being executed - tool_args: The arguments passed to the tool - agent: The agent executing the tool - tool_definition: The definition of the tool, if available - """ - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - set_on_span = span.set_data - - if tool_definition is not None and hasattr(tool_definition, "description"): - set_on_span( - SPANDATA.GEN_AI_TOOL_DESCRIPTION, - tool_definition.description, - ) - - _set_agent_data(span, agent) - - if _should_send_prompts() and tool_args is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - """Update the execute tool span with the result.""" - if not span: - return - - if not _should_send_prompts() or result is None: - return - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - else: - span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py deleted file mode 100644 index f0c68e85ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ /dev/null @@ -1,184 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, -) - -if TYPE_CHECKING: - from typing import Any, Union - -try: - from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore -except ImportError: - BinaryContent = None - ImageUrl = None - - -def invoke_agent_span( - user_prompt: "Any", - agent: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = False, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for invoking the agent.""" - # Determine agent name for span - name = "agent" - if agent and getattr(agent, "name", None): - name = agent.name - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - _set_available_tools(span, agent) - - # Add user prompt and system prompts if available and prompts are enabled - if _should_send_prompts(): - messages = [] - - # Add system prompts (both instructions and system_prompt) - system_texts = [] - - if agent: - # Check for system_prompt - system_prompts = getattr(agent, "_system_prompts", None) or [] - for prompt in system_prompts: - if isinstance(prompt, str): - system_texts.append(prompt) - - # Check for instructions (stored in _instructions) - instructions = getattr(agent, "_instructions", None) - if instructions: - if isinstance(instructions, str): - system_texts.append(instructions) - elif isinstance(instructions, (list, tuple)): - for instr in instructions: - if isinstance(instr, str): - system_texts.append(instr) - elif callable(instr): - # Skip dynamic/callable instructions - pass - - # Add all system texts as system messages - for system_text in system_texts: - messages.append( - { - "content": [{"text": system_text, "type": "text"}], - "role": "system", - } - ) - - # Add user prompt - if user_prompt: - if isinstance(user_prompt, str): - messages.append( - { - "content": [{"text": user_prompt, "type": "text"}], - "role": "user", - } - ) - elif isinstance(user_prompt, list): - # Handle list of user content - content = [] - for item in user_prompt: - if isinstance(item, str): - content.append({"text": item, "type": "text"}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - if content: - messages.append( - { - "content": content, - "role": "user", - } - ) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - result: "Any", -) -> None: - """Update and close the invoke agent span.""" - if not span or not result: - return - - # Extract output from result - output = getattr(result, "output", None) - - # Set response text if prompts are enabled - if _should_send_prompts() and output: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False - ) - - # Set model name from response if available - if hasattr(result, "response"): - try: - response = result.response - if hasattr(response, "model_name") and response.model_name: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - except Exception: - # If response access fails, continue without setting model name - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py deleted file mode 100644 index 330496c6b2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/spans/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Utility functions for PydanticAI span instrumentation.""" - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX -from sentry_sdk.ai.utils import get_modality_from_mime_type -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore - - -def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": - """Serialize an ImageUrl content item for span data. - - For data URLs containing base64-encoded images, the content is redacted. - For regular HTTP URLs, the URL string is preserved. - """ - url = str(item.url) - data_url_match = DATA_URL_BASE64_REGEX.match(url) - - if data_url_match: - return { - "type": "image", - "content": BLOB_DATA_SUBSTITUTE, - } - - return { - "type": "image", - "content": url, - } - - -def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": - """Serialize a BinaryContent item for span data, redacting the blob data.""" - return { - "type": "blob", - "modality": get_modality_from_mime_type(item.media_type), - "mime_type": item.media_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - usage: "Union[RequestUsage, RunUsage]", -) -> None: - """Set token usage data on a span. - - This function works with both RequestUsage (single request) and - RunUsage (agent run) objects from pydantic_ai. - - Args: - span: The Sentry span to set data on. - usage: RequestUsage or RunUsage object containing token usage information. - """ - if usage is None: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if hasattr(usage, "input_tokens") and usage.input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - - # Pydantic AI uses cache_read_tokens (not input_tokens_cached) - if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) - - # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) - if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - usage.cache_write_tokens, - ) - - if hasattr(usage, "output_tokens") and usage.output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - - if hasattr(usage, "total_tokens") and usage.total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py deleted file mode 100644 index 340dcf8953..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pydantic_ai/utils.py +++ /dev/null @@ -1,233 +0,0 @@ -from contextvars import ContextVar -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - -# Store the current agent context in a contextvar for re-entrant safety -# Using a list as a stack to support nested agent calls -_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( - "pydantic_ai_agent_context_stack", default=[] -) - - -def push_agent(agent: "Any", is_streaming: bool = False) -> None: - """Push an agent context onto the stack along with its streaming flag.""" - stack = _agent_context_stack.get().copy() - stack.append({"agent": agent, "is_streaming": is_streaming}) - _agent_context_stack.set(stack) - - -def pop_agent() -> None: - """Pop an agent context from the stack.""" - stack = _agent_context_stack.get().copy() - if stack: - stack.pop() - _agent_context_stack.set(stack) - - -def get_current_agent() -> "Any": - """Get the current agent from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1]["agent"] - return None - - -def get_is_streaming() -> bool: - """Get the streaming flag from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1].get("is_streaming", False) - return False - - -def _should_send_prompts() -> bool: - """ - Check if prompts should be sent to Sentry. - - This checks both send_default_pii and the include_prompts integration setting. - """ - if not should_send_default_pii(): - return False - - from . import PydanticAIIntegration - - # Get the integration instance from the client - integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) - - if integration is None: - return False - - return getattr(integration, "include_prompts", False) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set agent-related data on a span. - - Args: - span: The span to set data on - agent: Agent object (can be None, will try to get from contextvar if not provided) - """ - # Extract agent name from agent object or contextvar - agent_obj = agent - if not agent_obj: - # Try to get from contextvar - agent_obj = get_current_agent() - - if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - else: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - - -def _get_model_name(model_obj: "Any") -> "Optional[str]": - """Extract model name from a model object. - - Args: - model_obj: Model object to extract name from - - Returns: - Model name string or None if not found - """ - if not model_obj: - return None - - if hasattr(model_obj, "model_name"): - return model_obj.model_name - elif hasattr(model_obj, "name"): - try: - return model_obj.name() - except Exception: - return str(model_obj) - elif isinstance(model_obj, str): - return model_obj - else: - return str(model_obj) - - -def _set_model_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - model: "Any", - model_settings: "Any", -) -> None: - """Set model-related data on a span. - - Args: - span: The span to set data on - model: Model object (can be None, will try to get from agent if not provided) - model_settings: Model settings (can be None, will try to get from agent if not provided) - """ - # Try to get agent from contextvar if we need it - agent_obj = get_current_agent() - - # Extract model information - model_obj = model - if not model_obj and agent_obj and hasattr(agent_obj, "model"): - model_obj = agent_obj.model - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if model_obj: - # Set system from model - if hasattr(model_obj, "system"): - set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) - - # Set model name - model_name = _get_model_name(model_obj) - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Extract model settings - settings = model_settings - if not settings and agent_obj and hasattr(agent_obj, "model_settings"): - settings = agent_obj.model_settings - - if settings: - settings_map = { - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - } - - # ModelSettings is a TypedDict (dict at runtime), so use dict access - if isinstance(settings, dict): - for setting_name, spandata_key in settings_map.items(): - value = settings.get(setting_name) - if value is not None: - set_on_span(spandata_key, value) - else: - # Fallback for object-style settings - for setting_name, spandata_key in settings_map.items(): - if hasattr(settings, setting_name): - value = getattr(settings, setting_name) - if value is not None: - set_on_span(spandata_key, value) - - -def _set_available_tools( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set available tools data on a span from an agent's function toolset. - - Args: - span: The span to set data on - agent: Agent object with _function_toolset attribute - """ - if not agent or not hasattr(agent, "_function_toolset"): - return - - try: - tools = [] - # Get tools from the function toolset - if hasattr(agent._function_toolset, "tools"): - for tool_name, tool in agent._function_toolset.tools.items(): - tool_info = {"name": tool_name} - - # Add description from function_schema if available - if hasattr(tool, "function_schema"): - schema = tool.function_schema - if getattr(schema, "description", None): - tool_info["description"] = schema.description - - # Add parameters from json_schema - if getattr(schema, "json_schema", None): - tool_info["parameters"] = schema.json_schema - - tools.append(tool_info) - - if tools: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - else: - span.set_data( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - except Exception: - # If we can't extract tools, just skip it - pass - - -def _capture_exception(exc: "Any", handled: bool = False) -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pydantic_ai", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py deleted file mode 100644 index 2616f4d5a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pymongo.py +++ /dev/null @@ -1,262 +0,0 @@ -import copy -import json - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -try: - from pymongo import monitoring -except ImportError: - raise DidNotEnable("Pymongo not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pymongo.monitoring import ( - CommandFailedEvent, - CommandStartedEvent, - CommandSucceededEvent, - ) - - -SAFE_COMMAND_ATTRIBUTES = [ - "insert", - "ordered", - "find", - "limit", - "singleBatch", - "aggregate", - "createIndexes", - "indexes", - "delete", - "findAndModify", - "renameCollection", - "to", - "drop", -] - - -def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": - for key in command: - is_safe_field = key in SAFE_COMMAND_ATTRIBUTES - if is_safe_field: - # Skip if safe key - continue - - update_db_command = key == "update" and "findAndModify" not in command - if update_db_command: - # Also skip "update" db command because it is save. - # There is also an "update" key in the "findAndModify" command, which is NOT safe! - continue - - # Special stripping for documents - is_document = key == "documents" - if is_document: - for doc in command[key]: - for doc_key in doc: - doc[doc_key] = "%s" - continue - - # Special stripping for dict style fields - is_dict_field = key in ["filter", "query", "update"] - if is_dict_field: - for item_key in command[key]: - command[key][item_key] = "%s" - continue - - # For pipeline fields strip the `$match` dict - is_pipeline_field = key == "pipeline" - if is_pipeline_field: - for pipeline in command[key]: - for match_key in pipeline["$match"] if "$match" in pipeline else []: - pipeline["$match"][match_key] = "%s" - continue - - # Default stripping - command[key] = "%s" - - return command - - -def _get_db_data(event: "Any") -> "Dict[str, Any]": - data = {} - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - data[SPANDATA.DB_DRIVER_NAME] = "pymongo" - db_name = event.database_name - - server_address = event.connection_id[0] - if server_address is not None: - data[SPANDATA.SERVER_ADDRESS] = server_address - - server_port = event.connection_id[1] - if server_port is not None: - data[SPANDATA.SERVER_PORT] = server_port - - if is_span_streaming_enabled: - data["db.system.name"] = "mongodb" - - if db_name is not None: - data["db.namespace"] = db_name - else: - data[SPANDATA.DB_SYSTEM] = "mongodb" - - if db_name is not None: - data[SPANDATA.DB_NAME] = db_name - - return data - - -class CommandTracer(monitoring.CommandListener): - def __init__(self) -> None: - self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} - - def _operation_key( - self, - event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", - ) -> int: - return event.request_id - - def started(self, event: "CommandStartedEvent") -> None: - client = sentry_sdk.get_client() - if client.get_integration(PyMongoIntegration) is None: - return - - with capture_internal_exceptions(): - command = dict(copy.deepcopy(event.command)) - - command.pop("$db", None) - command.pop("$clusterTime", None) - command.pop("$signature", None) - - db_data = _get_db_data(event) - - collection_name = command.get(event.command_name) - operation_name = event.command_name - db_name = event.database_name - - lsid = command.pop("lsid", None) - if not should_send_default_pii(): - command = _strip_pii(command) - - query = json.dumps(command, default=str) - - if has_span_streaming_enabled(client.options): - span_first_data = { - "db.operation.name": operation_name, - "db.collection.name": collection_name, - SPANDATA.DB_QUERY_TEXT: query, - "sentry.op": OP.DB, - "sentry.origin": PyMongoIntegration.origin, - **db_data, - } - - span = sentry_sdk.traces.start_span( - name=query, attributes=span_first_data - ) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, - category="query", - type=OP.DB, - data=span_first_data, - ) - - else: - tags = { - "db.name": db_name, - SPANDATA.DB_SYSTEM: "mongodb", - SPANDATA.DB_DRIVER_NAME: "pymongo", - SPANDATA.DB_OPERATION: operation_name, - # The below is a deprecated field, but leaving for legacy reasons. - # The v2 spans will use `db.collection.name` instead. - SPANDATA.DB_MONGODB_COLLECTION: collection_name, - } - - try: - tags["net.peer.name"] = event.connection_id[0] - tags["net.peer.port"] = str(event.connection_id[1]) - except TypeError: - pass - - data: "Dict[str, Any]" = {"operation_ids": {}} - data["operation_ids"]["operation"] = event.operation_id - data["operation_ids"]["request"] = event.request_id - - data.update(db_data) - - try: - if lsid: - lsid_id = lsid["id"] - data["operation_ids"]["session"] = str(lsid_id) - except KeyError: - pass - - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=PyMongoIntegration.origin, - ) - - for tag, value in tags.items(): - # set the tag for backwards-compatibility. - # TODO: remove the set_tag call in the next major release! - span.set_tag(tag, value) - span.set_data(tag, value) - - for key, value in data.items(): - span.set_data(key, value) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, category="query", type=OP.DB, data=tags - ) - - self._ongoing_operations[self._operation_key(event)] = span.__enter__() - - def failed(self, event: "CommandFailedEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - # Ignoring NoOpStreamedSpan as it will always have a status of "ok" - if type(span) is StreamedSpan: - span.status = SpanStatus.ERROR - elif type(span) is Span: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - span.__exit__(None, None, None) - except KeyError: - return - - def succeeded(self, event: "CommandSucceededEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - if type(span) is Span: - span.set_status(SPANSTATUS.OK) - span.__exit__(None, None, None) - except KeyError: - pass - - -class PyMongoIntegration(Integration): - identifier = "pymongo" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py deleted file mode 100644 index 6837d8345c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyramid.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -import os -import sys -import weakref - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -try: - from pyramid.httpexceptions import HTTPException - from pyramid.request import Request -except ImportError: - raise DidNotEnable("Pyramid not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional - - from pyramid.response import Response - from webob.cookies import RequestCookies - from webob.request import _FieldStorageWithFile - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.utils import ExcInfo - - -if getattr(Request, "authenticated_userid", None): - - def authenticated_userid(request: "Request") -> "Optional[Any]": - return request.authenticated_userid - -else: - # bw-compat for pyramid < 1.5 - from pyramid.security import authenticated_userid # type: ignore - - -TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") - - -class PyramidIntegration(Integration): - identifier = "pyramid" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "route_name") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - from pyramid import router - - old_call_view = router._call_view - - @functools.wraps(old_call_view) - def sentry_patched_call_view( - registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - integration = client.get_integration(PyramidIntegration) - if integration is None: - return old_call_view(registry, request, *args, **kwargs) - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii() and has_span_streaming_enabled(client.options): - user_id = authenticated_userid(request) - if user_id: - scope.set_user({"id": user_id}) - - scope.add_event_processor( - _make_event_processor(weakref.ref(request), integration) - ) - - return old_call_view(registry, request, *args, **kwargs) - - router._call_view = sentry_patched_call_view - - if hasattr(Request, "invoke_exception_view"): - old_invoke_exception_view = Request.invoke_exception_view - - def sentry_patched_invoke_exception_view( - self: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - rv = old_invoke_exception_view(self, *args, **kwargs) - - if ( - self.exc_info - and all(self.exc_info) - and rv.status_int == 500 - and sentry_sdk.get_client().get_integration(PyramidIntegration) - is not None - ): - _capture_exception(self.exc_info) - - return rv - - Request.invoke_exception_view = sentry_patched_invoke_exception_view - - old_wsgi_call = router.Router.__call__ - - @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) - def sentry_patched_wsgi_call( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - def sentry_patched_inner_wsgi_call( - environ: "Dict[str, Any]", start_response: "Callable[..., Any]" - ) -> "Any": - try: - return old_wsgi_call(self, environ, start_response) - except Exception: - einfo = sys.exc_info() - _capture_exception(einfo) - reraise(*einfo) - - middleware = SentryWsgiMiddleware( - sentry_patched_inner_wsgi_call, - span_origin=PyramidIntegration.origin, - ) - return middleware(environ, start_response) - - router.Router.__call__ = sentry_patched_wsgi_call - - -@ensure_integration_enabled(PyramidIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - if exc_info[0] is None or issubclass(exc_info[0], HTTPException): - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pyramid", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "route_name": request.matched_route.name, - "route_pattern": request.matched_route.pattern, - } - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if is_span_streaming_enabled - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - scope.set_transaction_name( - name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -class PyramidRequestExtractor(RequestExtractor): - def url(self) -> str: - return self.request.path_url - - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "RequestCookies": - return self.request.cookies - - def raw_data(self) -> str: - return self.request.text - - def form(self) -> "Dict[str, str]": - return { - key: value - for key, value in self.request.POST.items() - if not getattr(value, "filename", None) - } - - def files(self) -> "Dict[str, _FieldStorageWithFile]": - return { - key: value - for key, value in self.request.POST.items() - if getattr(value, "filename", None) - } - - def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: - file = postdata.file - try: - return os.fstat(file.fileno()).st_size - except Exception: - return 0 - - -def _make_event_processor( - weak_request: "Callable[[], Request]", integration: "PyramidIntegration" -) -> "EventProcessor": - def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - PyramidRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_info = event.setdefault("user", {}) - user_info.setdefault("id", authenticated_userid(request)) - - return event - - return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py deleted file mode 100644 index aae68c4c10..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/pyreqwest.py +++ /dev/null @@ -1,197 +0,0 @@ -from contextlib import contextmanager -from typing import Any, Generator - -import sentry_sdk -from sentry_sdk import start_span -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - logger, - parse_url, -) - -try: - from pyreqwest.client import ( # type: ignore[import-not-found] - ClientBuilder, - SyncClientBuilder, - ) - from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] - from pyreqwest.request import ( # type: ignore[import-not-found] - OneOffRequestBuilder, - Request, - SyncOneOffRequestBuilder, - ) - from pyreqwest.response import ( # type: ignore[import-not-found] - Response, - SyncResponse, - ) -except ImportError: - raise DidNotEnable("pyreqwest not installed or incompatible version installed") - - -class PyreqwestIntegration(Integration): - identifier = "pyreqwest" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_pyreqwest() - - -def _patch_pyreqwest() -> None: - # Patch Client Builders - _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) - _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) - - # Patch Request Builders - _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) - _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) - - -def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: - if not hasattr(cls, method_name): - return - - original_method = getattr(cls, method_name) - - def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - if not getattr(self, "_sentry_instrumented", False): - integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) - if integration is not None: - self.with_middleware(middleware) - try: - self._sentry_instrumented = True - except (TypeError, AttributeError): - # In case the instance itself is immutable or doesn't allow extra attributes - pass - return original_method(self, *args, **kwargs) - - setattr(cls, method_name, sentry_patched_method) - - -@contextmanager -def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": PyreqwestIntegration.origin, - SPANDATA.HTTP_REQUEST_METHOD: request.method, - }, - ) as span: - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - return - - with start_span( - op=OP.HTTP_CLIENT, - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - origin=PyreqwestIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - -async def sentry_async_middleware( - request: "Request", next_handler: "Next" -) -> "Response": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return await next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = await next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response - - -def sentry_sync_middleware( - request: "Request", next_handler: "SyncNext" -) -> "SyncResponse": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py deleted file mode 100644 index 6a5603d825..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/quart.py +++ /dev/null @@ -1,292 +0,0 @@ -import asyncio -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Union - - from sentry_sdk._types import Event, EventProcessor - -try: - import quart_auth # type: ignore -except ImportError: - quart_auth = None - -try: - from quart import ( # type: ignore - Quart, - Request, - has_request_context, - has_websocket_context, - request, - websocket, - ) - from quart.signals import ( # type: ignore - got_background_exception, - got_request_exception, - got_websocket_exception, - request_started, - websocket_started, - ) -except ImportError: - raise DidNotEnable("Quart is not installed") -else: - # Quart 0.19 is based on Flask and hence no longer has a Scaffold - try: - from quart.scaffold import Scaffold # type: ignore - except ImportError: - from flask.sansio.scaffold import Scaffold # type: ignore - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class QuartIntegration(Integration): - identifier = "quart" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "endpoint") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - request_started.connect(_request_websocket_started) - websocket_started.connect(_request_websocket_started) - got_background_exception.connect(_capture_exception) - got_request_exception.connect(_capture_exception) - got_websocket_exception.connect(_capture_exception) - - patch_asgi_app() - patch_scaffold_route() - - -def patch_asgi_app() -> None: - old_app = Quart.__call__ - - async def sentry_patched_asgi_app( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - if sentry_sdk.get_client().get_integration(QuartIntegration) is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=QuartIntegration.origin, - asgi_version=3, - ) - return await middleware(scope, receive, send) - - Quart.__call__ = sentry_patched_asgi_app - - -def patch_scaffold_route() -> None: - # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 - if sys.version_info >= (3, 12): - iscoroutinefunction = inspect.iscoroutinefunction - else: - iscoroutinefunction = asyncio.iscoroutinefunction - - old_route = Scaffold.route - - def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": - old_decorator = old_route(*args, **kwargs) - - def decorator(old_func: "Any") -> "Any": - if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): - - @wraps(old_func) - @ensure_integration_enabled(QuartIntegration, old_func) - def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - span = get_current_span() - if span is not None and hasattr(span, "_segment"): - span._segment._update_active_thread() - else: - current_scope = sentry_sdk.get_current_scope() - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_func(*args, **kwargs) - - return old_decorator(_sentry_func) - - return old_decorator(old_func) - - return decorator - - Scaffold.route = _sentry_route - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if has_span_streaming_enabled(sentry_sdk.get_client().options) - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - - scope.set_transaction_name( - name=name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - if has_request_context(): - request_websocket = request._get_current_object() - if has_websocket_context(): - request_websocket = websocket._get_current_object() - - # Set the transaction name here, but rely on ASGI middleware - # to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket - ) - - scope = sentry_sdk.get_isolation_scope() - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - current_span = get_current_span() - if type(current_span) is StreamedSpan: - segment = current_span._segment - - segment.set_attribute("http.request.method", request_websocket.method) - header_attributes: "dict[str, Any]" = {} - - for header, header_value in _filter_headers( - dict(request_websocket.headers), use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - segment.set_attributes(header_attributes) - - if should_send_default_pii(): - segment.set_attribute("url.full", request_websocket.url) - segment.set_attribute( - "url.query", - request_websocket.query_string.decode("utf-8", errors="replace"), - ) - - user_properties = {} - if len(request_websocket.access_route) >= 1: - segment.set_attribute( - "client.address", request_websocket.access_route[0] - ) - user_properties["ip_address"] = request_websocket.access_route[0] - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_properties["id"] = current_user_id - - if user_properties: - existing_user_properties = scope._user or {} - scope.set_user({**existing_user_properties, **user_properties}) - - evt_processor = _make_request_event_processor(app, request_websocket, integration) - scope.add_event_processor(evt_processor) - - -def _make_request_event_processor( - app: "Quart", request: "Request", integration: "QuartIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - # TODO: Figure out what to do with request body. Methods on request - # are async, but event processors are not. - - request_info = event.setdefault("request", {}) - request_info["url"] = request.url - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - if len(request.access_route) >= 1: - request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_info = event.setdefault("user", {}) - user_info["id"] = current_user_id - - return event - - return inner - - -async def _capture_exception( - sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "quart", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_current_user_id_from_quart() -> "str | None": - if quart_auth is None: - return None - - if quart_auth.current_user is None: - return None - - try: - return quart_auth.current_user._auth_id - except Exception: - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py deleted file mode 100644 index f723a96f3c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/ray.py +++ /dev/null @@ -1,240 +0,0 @@ -import functools -import inspect -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - event_from_exception, - logger, - package_version, - qualname_from_function, - reraise, -) - -try: - import ray # type: ignore[import-not-found] - from ray import remote -except ImportError: - raise DidNotEnable("Ray not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional - - from sentry_sdk.utils import ExcInfo - - -def _check_sentry_initialized() -> None: - if sentry_sdk.get_client().is_active(): - return - - logger.debug( - "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." - ) - - -def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: - # Patching new_func signature to add the _sentry_tracing parameter to it - # Ray later inspects the signature and finds the unexpected parameter otherwise - signature = inspect.signature(func) - params = list(signature.parameters.values()) - sentry_tracing_param = inspect.Parameter( - "_sentry_tracing", - kind=inspect.Parameter.KEYWORD_ONLY, - default=None, - ) - - # Keyword only arguments are penultimate if function has variadic keyword arguments - if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: - params.insert(-1, sentry_tracing_param) - else: - params.append(sentry_tracing_param) - - func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] - - -def _patch_ray_remote() -> None: - old_remote = remote - - @functools.wraps(old_remote) - def new_remote( - f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" - ) -> "Callable[..., Any]": - if inspect.isclass(f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(f, *args, **kwargs) - - def wrapper(user_f: "Callable[..., Any]") -> "Any": - if inspect.isclass(user_f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(*args, **kwargs)(user_f) - - @functools.wraps(user_f) - def new_func( - *f_args: "Any", - _sentry_tracing: "Optional[dict[str, Any]]" = None, - **f_kwargs: "Any", - ) -> "Any": - _check_sentry_initialized() - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(_sentry_tracing or {}) - - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RAY, - "sentry.origin": RayIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - }, - parent_span=None, - ): - try: - result = user_f(*f_args, **f_kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - transaction = sentry_sdk.continue_trace( - _sentry_tracing or {}, - op=OP.QUEUE_TASK_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - source=TransactionSource.TASK, - ) - - with sentry_sdk.start_transaction(transaction) as transaction: - try: - result = user_f(*f_args, **f_kwargs) - transaction.set_status(SPANSTATUS.OK) - except Exception: - transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - _insert_sentry_tracing_in_signature(new_func) - - if f: - rv = old_remote(new_func) - else: - rv = old_remote(*args, **kwargs)(new_func) - old_remote_method = rv.remote - - def _remote_method_with_header_propagation( - *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Ray Client - """ - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_RAY, - "sentry.origin": RayIntegration.origin, - }, - ): - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - ) as span: - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - span.set_status(SPANSTATUS.OK) - except Exception: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - rv.remote = _remote_method_with_header_propagation - - return rv - - if f is not None: - return wrapper(f) - else: - return wrapper - - ray.remote = new_remote - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={ - "handled": False, - "type": RayIntegration.identifier, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class RayIntegration(Integration): - identifier = "ray" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = package_version("ray") - _check_minimum_version(RayIntegration, version) - - _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py deleted file mode 100644 index 7095721ed2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE -from sentry_sdk.integrations.redis.rb import _patch_rb -from sentry_sdk.integrations.redis.redis import _patch_redis -from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster -from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -class RedisIntegration(Integration): - identifier = "redis" - - def __init__( - self, - max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, - cache_prefixes: "Optional[list[str]]" = None, - ) -> None: - self.max_data_size = max_data_size - self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] - - if max_data_size is not None: - warnings.warn( - "The `max_data_size` parameter of `RedisIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - try: - from redis import StrictRedis, client - except ImportError: - raise DidNotEnable("Redis client not installed") - - _patch_redis(StrictRedis, client) - _patch_redis_cluster() - _patch_rb() - - try: - _patch_rediscluster() - except Exception: - logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py deleted file mode 100644 index 8fc3d0c3a9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_async_common.py +++ /dev/null @@ -1,177 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from redis.asyncio.client import Pipeline, StrictRedis - from redis.asyncio.cluster import ClusterPipeline, RedisCluster - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_async_pipeline( - pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return await old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - try: - command_seq = self._execution_strategy._command_queue - except AttributeError: - if is_cluster: - command_seq = self._command_stack - else: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.is_transaction, - command_seq, - ) - - return await old_execute(self, *args, **kwargs) - - pipeline_cls.execute = _sentry_execute # type: ignore - - -def patch_redis_async_client( - cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return await old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = await old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py deleted file mode 100644 index 58d686b099..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/_sync_common.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_pipeline( - pipeline_cls: "Any", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - command_seq = None - try: - command_seq = self._execution_strategy.command_queue - except AttributeError: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.transaction, - command_seq, - ) - - return old_execute(self, *args, **kwargs) - - pipeline_cls.execute = sentry_patched_execute - - -def patch_redis_client( - cls: "Any", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - """ - This function can be used to instrument custom redis client classes or - subclasses. - """ - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py deleted file mode 100644 index 0822c2c930..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/consts.py +++ /dev/null @@ -1,19 +0,0 @@ -SPAN_ORIGIN = "auto.db.redis" - -_SINGLE_KEY_COMMANDS = frozenset( - ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], -) -_MULTI_KEY_COMMANDS = frozenset( - [ - "del", - "touch", - "unlink", - "mget", - ], -) -_COMMANDS_INCLUDING_SENSITIVE_DATA = [ - "auth", -] -_MAX_NUM_ARGS = 10 # Trim argument lists to this many values -_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values -_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py deleted file mode 100644 index 35f20fddd9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/caches.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Code used for the Caches module in Sentry -""" - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -GET_COMMANDS = ("get", "mget") -SET_COMMANDS = ("set", "setex") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _get_op(name: str) -> "Optional[str]": - op = None - if name.lower() in GET_COMMANDS: - op = OP.CACHE_GET - elif name.lower() in SET_COMMANDS: - op = OP.CACHE_PUT - - return op - - -def _compile_cache_span_properties( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> "dict[str, Any]": - key = _get_safe_key(redis_command, args, kwargs) - key_as_string = _key_as_string(key) - keys_as_string = key_as_string.split(", ") - - is_cache_key = False - for prefix in integration.cache_prefixes: - for kee in keys_as_string: - if kee.startswith(prefix): - is_cache_key = True - break - if is_cache_key: - break - - value = None - if redis_command.lower() in SET_COMMANDS: - value = args[-1] - - properties = { - "op": _get_op(redis_command), - "description": _get_cache_span_description( - redis_command, args, kwargs, integration - ), - "key": key, - "key_as_string": key_as_string, - "redis_command": redis_command.lower(), - "is_cache_key": is_cache_key, - "value": value, - } - - return properties - - -def _get_cache_span_description( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> str: - description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_cache_data( - span: "Union[Span, StreamedSpan]", - redis_client: "Any", - properties: "dict[str, Any]", - return_value: "Optional[Any]", -) -> None: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - with capture_internal_exceptions(): - set_on_span(SPANDATA.CACHE_KEY, properties["key"]) - - if properties["redis_command"] in GET_COMMANDS: - if return_value is not None: - set_on_span(SPANDATA.CACHE_HIT, True) - size = ( - len(str(return_value).encode("utf-8")) - if not isinstance(return_value, bytes) - else len(return_value) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - else: - set_on_span(SPANDATA.CACHE_HIT, False) - - elif properties["redis_command"] in SET_COMMANDS: - if properties["value"] is not None: - size = ( - len(properties["value"].encode("utf-8")) - if not isinstance(properties["value"], bytes) - else len(properties["value"]) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - - try: - connection_params = redis_client.connection_pool.connection_kwargs - except AttributeError: - # If it is a cluster, there is no connection_pool attribute so we - # need to get the default node from the cluster instance - default_node = redis_client.get_default_node() - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - - host = connection_params.get("host") - if host is not None: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) - - port = connection_params.get("port") - if port is not None: - set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py deleted file mode 100644 index 69207bf6f6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/modules/queries.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Code used for the Queries module in Sentry -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_command -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import Redis - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _compile_db_span_properties( - integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" -) -> "dict[str, Any]": - description = _get_db_span_description(integration, redis_command, args) - - properties = { - "op": OP.DB_REDIS, - "description": description, - } - - return properties - - -def _get_db_span_description( - integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" -) -> str: - description = command_name - - with capture_internal_exceptions(): - description = _get_safe_command(command_name, args) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_db_data_on_span( - span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" -) -> None: - db = connection_params.get("db") - host = connection_params.get("host") - port = connection_params.get("port") - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) - - if host is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_attribute(SPANDATA.SERVER_PORT, port) - - else: - span.set_data(SPANDATA.DB_SYSTEM, "redis") - span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_data(SPANDATA.DB_NAME, str(db)) - - if host is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_data(SPANDATA.SERVER_PORT, port) - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" -) -> None: - try: - _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) - except AttributeError: - pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py deleted file mode 100644 index e2ce863fe8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/rb.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Instrumentation for Redis Blaster (rb) - -https://github.com/getsentry/rb -""" - -from sentry_sdk.integrations.redis._sync_common import patch_redis_client -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - - -def _patch_rb() -> None: - try: - import rb.clients # type: ignore - except ImportError: - pass - else: - patch_redis_client( - rb.clients.FanoutClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.MappingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.RoutingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py deleted file mode 100644 index e704c9bc6a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Instrumentation for Redis - -https://github.com/redis/redis-py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - -if TYPE_CHECKING: - from typing import Any, Sequence - - -def _get_redis_command_args(command: "Any") -> "Sequence[Any]": - return command[0] - - -def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 - patch_redis_client( - StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_pipeline( - client.Pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - try: - strict_pipeline = client.StrictPipeline - except AttributeError: - pass - else: - patch_redis_pipeline( - strict_pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - - try: - import redis.asyncio - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - redis.asyncio.client.StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_async_pipeline( - redis.asyncio.client.Pipeline, - False, - _get_redis_command_args, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py deleted file mode 100644 index b6c95e6abd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_cluster.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Instrumentation for RedisCluster -This is part of the main redis-py client. - -https://github.com/redis/redis-py/blob/master/redis/cluster.py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import RedisCluster - from redis.asyncio.cluster import ( - ClusterPipeline as AsyncClusterPipeline, - ) - from redis.asyncio.cluster import ( - RedisCluster as AsyncRedisCluster, - ) - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -def _set_async_cluster_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_instance: "AsyncRedisCluster[Any]", -) -> None: - default_node = async_redis_cluster_instance.get_default_node() - if default_node is not None and default_node.connection_kwargs is not None: - _set_db_data_on_span(span, default_node.connection_kwargs) - - -def _set_async_cluster_pipeline_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", -) -> None: - with capture_internal_exceptions(): - client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) - if client is None: - # In older redis-py versions, the AsyncClusterPipeline had a `_client` - # attr but it is private so potentially problematic and mypy does not - # recognize it - see - # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 - client = ( - async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] - ) - - _set_async_cluster_db_data( - span, - client, - ) - - -def _set_cluster_db_data( - span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" -) -> None: - default_node = redis_cluster_instance.get_default_node() - - if default_node is not None: - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - _set_db_data_on_span(span, connection_params) - - -def _patch_redis_cluster() -> None: - """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" - try: - from redis import RedisCluster, cluster - except ImportError: - pass - else: - patch_redis_client( - RedisCluster, - is_cluster=True, - set_db_data_fn=_set_cluster_db_data, - ) - patch_redis_pipeline( - cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_cluster_db_data, - ) - - try: - from redis.asyncio import cluster as async_cluster - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - async_cluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_async_cluster_db_data, - ) - patch_redis_async_pipeline( - async_cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_async_cluster_pipeline_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py deleted file mode 100644 index 3437aa1f2f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Instrumentation for redis-py-cluster -The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). - -https://github.com/grokzen/redis-py-cluster -""" - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command - - -def _patch_rediscluster() -> None: - try: - import rediscluster # type: ignore - except ImportError: - return - - patch_redis_client( - rediscluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - - # up to v1.3.6, __version__ attribute is a tuple - # from v2.0.0, __version__ is a string and VERSION a tuple - version = getattr(rediscluster, "VERSION", rediscluster.__version__) - - # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 - # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst - if (0, 2, 0) < version < (2, 0, 0): - pipeline_cls = rediscluster.pipeline.StrictClusterPipeline - patch_redis_client( - rediscluster.StrictRedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - else: - pipeline_cls = rediscluster.pipeline.ClusterPipeline - - patch_redis_pipeline( - pipeline_cls, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py deleted file mode 100644 index 7e04df9c69..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/redis/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations.redis.consts import ( - _COMMANDS_INCLUDING_SENSITIVE_DATA, - _MAX_NUM_ARGS, - _MAX_NUM_COMMANDS, - _MULTI_KEY_COMMANDS, - _SINGLE_KEY_COMMANDS, -) -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - -if TYPE_CHECKING: - from typing import Any, Optional, Sequence, Union - - -def _get_safe_command(name: str, args: "Sequence[Any]") -> str: - command_parts = [name] - - name_low = name.lower() - send_default_pii = should_send_default_pii() - - for i, arg in enumerate(args): - if i > _MAX_NUM_ARGS: - break - - if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - continue - - arg_is_the_key = i == 0 - if arg_is_the_key: - command_parts.append(repr(arg)) - else: - if send_default_pii: - command_parts.append(repr(arg)) - else: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - - command = " ".join(command_parts) - return command - - -def _safe_decode(key: "Any") -> str: - if isinstance(key, bytes): - try: - return key.decode() - except UnicodeDecodeError: - return "" - - return str(key) - - -def _key_as_string(key: "Any") -> str: - if isinstance(key, (dict, list, tuple)): - key = ", ".join(_safe_decode(x) for x in key) - elif isinstance(key, bytes): - key = _safe_decode(key) - elif key is None: - key = "" - else: - key = str(key) - - return key - - -def _get_safe_key( - method_name: str, - args: "Optional[tuple[Any, ...]]", - kwargs: "Optional[dict[str, Any]]", -) -> "Optional[tuple[str, ...]]": - """ - Gets the key (or keys) from the given method_name. - The method_name could be a redis command or a django caching command - """ - key = None - - if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: - # for example redis "mget" - key = tuple(args) - - elif args is not None and len(args) >= 1: - # for example django "set_many/get_many" or redis "get" - if isinstance(args[0], (dict, list, tuple)): - key = tuple(args[0]) - else: - key = (args[0],) - - elif kwargs is not None and "key" in kwargs: - # this is a legacy case for older versions of Django - if isinstance(kwargs["key"], (list, tuple)): - if len(kwargs["key"]) > 0: - key = tuple(kwargs["key"]) - else: - if kwargs["key"] is not None: - key = (kwargs["key"],) - - return key - - -def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": - return command.args - - -def _set_pipeline_data( - span: "Union[Span, StreamedSpan]", - is_cluster: bool, - get_command_args_fn: "Any", - is_transaction: bool, - commands_seq: "Sequence[Any]", -) -> None: - # TODO: Remove this whole function when removing transaction based tracing - if isinstance(span, StreamedSpan): - return - - span.set_tag("redis.is_cluster", is_cluster) - span.set_tag("redis.transaction", is_transaction) - - commands = [] - for i, arg in enumerate(commands_seq): - if i >= _MAX_NUM_COMMANDS: - break - - command = get_command_args_fn(arg) - commands.append(_get_safe_command(command[0], command[1:])) - - span.set_data( - "redis.commands", - { - "count": len(commands_seq), - "first_ten": commands, - }, - ) - - -def _set_client_data( - span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" -) -> None: - if isinstance(span, StreamedSpan): - if name: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) - else: - span.set_tag("redis.is_cluster", is_cluster) - if name: - span.set_tag("redis.command", name) - span.set_tag(SPANDATA.DB_OPERATION, name) - - if name and args: - name_low = name.lower() - if (name_low in _SINGLE_KEY_COMMANDS) or ( - name_low in _MULTI_KEY_COMMANDS and len(args) == 1 - ): - if isinstance(span, StreamedSpan): - span.set_attribute("db.redis.key", args[0]) - else: - span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py deleted file mode 100644 index edd48a9f67..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rq.py +++ /dev/null @@ -1,225 +0,0 @@ -import functools -import weakref - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - format_timestamp, - parse_version, -) - -try: - from rq.job import JobStatus - from rq.queue import Queue - from rq.timeouts import JobTimeoutException - from rq.version import VERSION as RQ_VERSION - from rq.worker import Worker -except ImportError: - raise DidNotEnable("RQ not installed") - -try: - from rq.worker import BaseWorker - - if not hasattr(BaseWorker, "perform_job"): - BaseWorker = None -except ImportError: - BaseWorker = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - from rq.job import Job - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - -class RqIntegration(Integration): - identifier = "rq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(RQ_VERSION) - _check_minimum_version(RqIntegration, version) - - # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly - # instead of Worker, so we need to patch BaseWorker to cover both. - # For older versions where BaseWorker doesn't exist or doesn't have - # perform_job, we patch Worker. - worker_cls = BaseWorker if BaseWorker is not None else Worker - - old_perform_job = worker_cls.perform_job - - @functools.wraps(old_perform_job) - def sentry_patched_perform_job( - self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" - ) -> bool: - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_perform_job(self, job, *args, **kwargs) - - with sentry_sdk.new_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(weakref.ref(job))) - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace( - job.meta.get("_sentry_trace_headers") or {} - ) - - Scope.set_custom_sampling_context({"rq_job": job}) - - func_name = None - with capture_internal_exceptions(): - func_name = job.func_name - - with sentry_sdk.traces.start_span( - name="unknown RQ task" if func_name is None else func_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RQ, - "sentry.origin": RqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job.id, - }, - parent_span=None, - ) as span: - if func_name is not None: - span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) - - rv = old_perform_job(self, job, *args, **kwargs) - else: - transaction = continue_trace( - job.meta.get("_sentry_trace_headers") or {}, - op=OP.QUEUE_TASK_RQ, - name="unknown RQ task", - source=TransactionSource.TASK, - origin=RqIntegration.origin, - ) - - with capture_internal_exceptions(): - transaction.name = job.func_name - - with sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"rq_job": job}, - ): - rv = old_perform_job(self, job, *args, **kwargs) - - if self.is_horse: - # We're inside of a forked process and RQ is - # about to call `os._exit`. Make sure that our - # events get sent out. - sentry_sdk.get_client().flush() - - return rv - - worker_cls.perform_job = sentry_patched_perform_job - - old_handle_exception = worker_cls.handle_exception - - def sentry_patched_handle_exception( - self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" - ) -> "Any": - retry = ( - hasattr(job, "retries_left") - and job.retries_left - and job.retries_left > 0 - ) - failed = job._status == JobStatus.FAILED or job.is_failed - if failed and not retry: - _capture_exception(exc_info) - - return old_handle_exception(self, job, *exc_info, **kwargs) - - worker_cls.handle_exception = sentry_patched_handle_exception - - old_enqueue_job = Queue.enqueue_job - - @functools.wraps(old_enqueue_job) - def sentry_patched_enqueue_job( - self: "Queue", job: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_enqueue_job(self, job, **kwargs) - - scope = sentry_sdk.get_current_scope() - span = ( - scope.streamed_span - if has_span_streaming_enabled(client.options) - else scope.span - ) - if span is not None: - job.meta["_sentry_trace_headers"] = dict( - scope.iter_trace_propagation_headers() - ) - - return old_enqueue_job(self, job, **kwargs) - - Queue.enqueue_job = sentry_patched_enqueue_job - - ignore_logger("rq.worker") - - -def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - job = weak_job() - if job is not None: - with capture_internal_exceptions(): - extra = event.setdefault("extra", {}) - rq_job = { - "job_id": job.id, - "func": job.func_name, - "args": ( - job.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - job.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "description": job.description, - } - - if job.enqueued_at: - rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) - if job.started_at: - rq_job["started_at"] = format_timestamp(job.started_at) - - extra["rq-job"] = rq_job - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], JobTimeoutException): - event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "rq", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py deleted file mode 100644 index 622e3c17af..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/rust_tracing.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -This integration ingests tracing data from native extensions written in Rust. - -Using it requires additional setup on the Rust side to accept a -`RustTracingLayer` Python object and register it with the `tracing-subscriber` -using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: -```rust -#[pyfunction] -pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { - tracing_subscriber::registry() - .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) - .init(); -} -``` - -Usage in Python would then look like: -``` -sentry_sdk.init( - dsn=sentry_dsn, - integrations=[ - RustTracingIntegration( - "demo_rust_extension", - demo_rust_extension.initialize_tracing, - event_type_mapping=event_type_mapping, - ) - ], -) -``` - -Each native extension requires its own integration. -""" - -import json -from enum import Enum, auto -from typing import Any, Callable, Dict, Optional, Union - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - - -class RustTracingLevel(Enum): - Trace = "TRACE" - Debug = "DEBUG" - Info = "INFO" - Warn = "WARN" - Error = "ERROR" - - -class EventTypeMapping(Enum): - Ignore = auto() - Exc = auto() - Breadcrumb = auto() - Event = auto() - - -def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": - level = RustTracingLevel(level) - if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): - return "debug" - elif level == RustTracingLevel.Info: - return "info" - elif level == RustTracingLevel.Warn: - return "warning" - elif level == RustTracingLevel.Error: - return "error" - else: - # Better this than crashing - return "info" - - -def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": - metadata = event.get("metadata", {}) - contexts = {} - - location = {} - for field in ["module_path", "file", "line"]: - if field in metadata: - location[field] = metadata[field] - if len(location) > 0: - contexts["rust_tracing_location"] = location - - fields = {} - for field in metadata.get("fields", []): - fields[field] = event.get(field) - if len(fields) > 0: - contexts["rust_tracing_fields"] = fields - - return contexts - - -def process_event(event: "Dict[str, Any]") -> None: - metadata = event.get("metadata", {}) - - logger = metadata.get("target") - level = tracing_level_to_sentry_level(metadata.get("level")) - message: "sentry_sdk._types.Any" = event.get("message") - contexts = extract_contexts(event) - - sentry_event: "sentry_sdk._types.Event" = { - "logger": logger, - "level": level, - "message": message, - "contexts": contexts, - } - - sentry_sdk.capture_event(sentry_event) - - -def process_exception(event: "Dict[str, Any]") -> None: - process_event(event) - - -def process_breadcrumb(event: "Dict[str, Any]") -> None: - level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) - message = event.get("message") - - sentry_sdk.add_breadcrumb(level=level, message=message) - - -def default_span_filter(metadata: "Dict[str, Any]") -> bool: - return RustTracingLevel(metadata.get("level")) in ( - RustTracingLevel.Error, - RustTracingLevel.Warn, - RustTracingLevel.Info, - ) - - -def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": - level = RustTracingLevel(metadata.get("level")) - if level == RustTracingLevel.Error: - return EventTypeMapping.Exc - elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): - return EventTypeMapping.Breadcrumb - elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): - return EventTypeMapping.Ignore - else: - return EventTypeMapping.Ignore - - -class RustTracingLayer: - def __init__( - self, - origin: str, - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.origin = origin - self.event_type_mapping = event_type_mapping - self.span_filter = span_filter - self.include_tracing_fields = include_tracing_fields - - def _include_tracing_fields(self) -> bool: - """ - By default, the values of tracing fields are not included in case they - contain PII. A user may override that by passing `True` for the - `include_tracing_fields` keyword argument of this integration or by - setting `send_default_pii` to `True` in their Sentry client options. - """ - return ( - should_send_default_pii() - if self.include_tracing_fields is None - else self.include_tracing_fields - ) - - def on_event(self, event: str, sentry_span: "SentrySpan") -> None: - deserialized_event = json.loads(event) - metadata = deserialized_event.get("metadata", {}) - - event_type = self.event_type_mapping(metadata) - if event_type == EventTypeMapping.Ignore: - return - elif event_type == EventTypeMapping.Exc: - process_exception(deserialized_event) - elif event_type == EventTypeMapping.Breadcrumb: - process_breadcrumb(deserialized_event) - elif event_type == EventTypeMapping.Event: - process_event(deserialized_event) - - def on_new_span( - self, attrs: str, span_id: str - ) -> "Optional[Union[SentrySpan, StreamedSpan]]": - attrs = json.loads(attrs) - metadata = attrs.get("metadata", {}) - - if not self.span_filter(metadata): - return None - - module_path = metadata.get("module_path") - name = metadata.get("name") - message = attrs.get("message") - - if message is not None: - sentry_span_name = message - elif module_path is not None and name is not None: - sentry_span_name = f"{module_path}::{name}" # noqa: E231 - elif name is not None: - sentry_span_name = name - else: - sentry_span_name = "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - sentry_span = sentry_sdk.traces.start_span( - name=sentry_span_name, - attributes={ - "sentry.op": "function", - "sentry.origin": self.origin, - }, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_attribute(field, attrs.get(field)) - else: - sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) - - return sentry_span - - sentry_span = sentry_sdk.start_span( - op="function", - name=sentry_span_name, - origin=self.origin, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_data(field, attrs.get(field)) - else: - sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) - - sentry_span.__enter__() - return sentry_span - - def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: - if sentry_span is None: - return - - sentry_span.__exit__(None, None, None) - - def on_record( - self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" - ) -> None: - if sentry_span is None: - return - - set_on_span = ( - sentry_span.set_attribute - if isinstance(sentry_span, StreamedSpan) - else sentry_span.set_data - ) - - deserialized_values = json.loads(values) - for key, value in deserialized_values.items(): - if self._include_tracing_fields(): - set_on_span(key, value) - else: - set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) - - -class RustTracingIntegration(Integration): - """ - Ingests tracing data from a Rust native extension's `tracing` instrumentation. - - If a project uses more than one Rust native extension, each one will need - its own instance of `RustTracingIntegration` with an initializer function - specific to that extension. - - Since all of the setup for this integration requires instance-specific state - which is not available in `setup_once()`, setup instead happens in `__init__()`. - """ - - def __init__( - self, - identifier: str, - initializer: "Callable[[RustTracingLayer], None]", - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.identifier = identifier - origin = f"auto.function.rust_tracing.{identifier}" - self.tracing_layer = RustTracingLayer( - origin, event_type_mapping, span_filter, include_tracing_fields - ) - - initializer(self.tracing_layer) - - @staticmethod - def setup_once() -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py deleted file mode 100644 index 908fceb0cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sanic.py +++ /dev/null @@ -1,431 +0,0 @@ -import sys -import warnings -import weakref -from inspect import isawaitable -from typing import TYPE_CHECKING -from urllib.parse import urlsplit - -import sentry_sdk -from sentry_sdk import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -if TYPE_CHECKING: - from collections.abc import Container - from typing import Any, Callable, Dict, Optional, Union - - from sanic.request import Request, RequestParameters - from sanic.response import BaseHTTPResponse - from sanic.router import Route - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -try: - from sanic import Sanic - from sanic import __version__ as SANIC_VERSION - from sanic.exceptions import SanicException - from sanic.handlers import ErrorHandler - from sanic.router import Router -except ImportError: - raise DidNotEnable("Sanic not installed") - -old_error_handler_lookup = ErrorHandler.lookup -old_handle_request = Sanic.handle_request -old_router_get = Router.get - -try: - # This method was introduced in Sanic v21.9 - old_startup = Sanic._startup -except AttributeError: - pass - - -class SanicIntegration(Integration): - identifier = "sanic" - origin = f"auto.http.{identifier}" - version: "Optional[tuple[int, ...]]" = None - - def __init__( - self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) - ) -> None: - """ - The unsampled_statuses parameter can be used to specify for which HTTP statuses the - transactions should not be sent to Sentry. By default, transactions are sent for all - HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all - HTTP statuses, including 404. - """ - self._unsampled_statuses = unsampled_statuses or set() - - @staticmethod - def setup_once() -> None: - SanicIntegration.version = parse_version(SANIC_VERSION) - _check_minimum_version(SanicIntegration, SanicIntegration.version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - if SANIC_VERSION.startswith("0.8."): - # Sanic 0.8 and older creates a logger named "root" and puts a - # stringified version of every exception in there (without exc_info), - # which our error deduplication can't detect. - # - # We explicitly check the version here because it is a very - # invasive step to ignore this logger and not necessary in newer - # versions at all. - # - # https://github.com/huge-success/sanic/issues/1332 - ignore_logger("root") - - if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): - _setup_legacy_sanic() - return - - _setup_sanic() - - -class SanicRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return dict(self.request.cookies) - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "RequestParameters": - return self.request.form - - def is_json(self) -> bool: - raise NotImplementedError() - - def json(self) -> "Optional[Any]": - return self.request.json - - def files(self) -> "RequestParameters": - return self.request.files - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) - - -def _setup_sanic() -> None: - Sanic._startup = _startup - ErrorHandler.lookup = _sentry_error_handler_lookup - - -def _setup_legacy_sanic() -> None: - Sanic.handle_request = _legacy_handle_request - Router.get = _legacy_router_get - ErrorHandler.lookup = _sentry_error_handler_lookup - - -async def _startup(self: "Sanic") -> None: - # This happens about as early in the lifecycle as possible, just after the - # Request object is created. The body has not yet been consumed. - self.signal("http.lifecycle.request")(_context_enter) - - # This happens after the handler is complete. In v21.9 this signal is not - # dispatched when there is an exception. Therefore we need to close out - # and call _context_exit from the custom exception handler as well. - # See https://github.com/sanic-org/sanic/issues/2297 - self.signal("http.lifecycle.response")(_context_exit) - - # This happens inside of request handling immediately after the route - # has been identified by the router. - self.signal("http.routing.after")(_set_transaction) - - # The above signals need to be declared before this can be called. - await old_startup(self) - - -async def _context_enter(request: "Request") -> None: - request.ctx._sentry_do_integration = ( - sentry_sdk.get_client().get_integration(SanicIntegration) is not None - ) - - if not request.ctx._sentry_do_integration: - return - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - weak_request = weakref.ref(request) - request.ctx._sentry_scope = sentry_sdk.isolation_scope() - scope = request.ctx._sentry_scope.__enter__() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - if is_span_streaming_enabled: - integration = client.get_integration(SanicIntegration) - if ( - isinstance(integration, SanicIntegration) - and integration._unsampled_statuses - ): - warnings.warn( - "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", - stacklevel=2, - ) - - sentry_sdk.traces.continue_trace(dict(request.headers)) - scope.set_custom_sampling_context({"sanic_request": request}) - - if should_send_default_pii() and request.remote_addr: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) - - span = sentry_sdk.traces.start_span( - # Unless the request results in a 404 error, the name and source - # will get overwritten in _set_transaction - name=request.path, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": SanicIntegration.origin, - "sentry.span.source": SegmentSource.URL.value, - }, - parent_span=None, - ) - request.ctx._sentry_root_span = span - else: - transaction = continue_trace( - dict(request.headers), - op=OP.HTTP_SERVER, - # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction - name=request.path, - source=TransactionSource.URL, - origin=SanicIntegration.origin, - ) - request.ctx._sentry_root_span = sentry_sdk.start_transaction( - transaction - ).__enter__() - - -async def _context_exit( - request: "Request", response: "Optional[BaseHTTPResponse]" = None -) -> None: - with capture_internal_exceptions(): - if not request.ctx._sentry_do_integration: - return - - integration = sentry_sdk.get_client().get_integration(SanicIntegration) - - response_status = None if response is None else response.status - - # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception - # happens while trying to end the transaction, we still attempt to exit the hub. - with capture_internal_exceptions(): - span = request.ctx._sentry_root_span - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes(request).items(): - span.set_attribute(attr, value) - if response_status is not None: - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) - span.status = "error" if response_status >= 400 else "ok" - - span.end() - - else: - span.set_http_status(response_status) - span.sampled &= ( - isinstance(integration, SanicIntegration) - and response_status not in integration._unsampled_statuses - ) - - span.__exit__(None, None, None) - - request.ctx._sentry_scope.__exit__(None, None, None) - - -async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: - if request.ctx._sentry_do_integration: - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - route_name = route.name.replace(request.app.name, "").strip(".") - scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) - - -def _sentry_error_handler_lookup( - self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" -) -> "Optional[object]": - _capture_exception(exception) - old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) - - if old_error_handler is None: - return None - - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return old_error_handler - - async def sentry_wrapped_error_handler( - request: "Request", exception: Exception - ) -> "Any": - try: - response = old_error_handler(request, exception) - if isawaitable(response): - response = await response - return response - except Exception: - # Report errors that occur in Sanic error handler. These - # exceptions will not even show up in Sanic's - # `sanic.exceptions` logger. - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - finally: - # As mentioned in previous comment in _startup, this can be removed - # after https://github.com/sanic-org/sanic/issues/2297 is resolved - if SanicIntegration.version and SanicIntegration.version == (21, 9): - await _context_exit(request) - - return sentry_wrapped_error_handler - - -async def _legacy_handle_request( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" -) -> "Any": - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return await old_handle_request(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - response = old_handle_request(self, request, *args, **kwargs) - if isawaitable(response): - response = await response - - return response - - -def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": - rv = old_router_get(self, *args) - if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: - with capture_internal_exceptions(): - scope = sentry_sdk.get_isolation_scope() - if SanicIntegration.version and SanicIntegration.version >= (21, 3): - # Sanic versions above and including 21.3 append the app name to the - # route name, and so we need to remove it from Route name so the - # transaction name is consistent across all versions - sanic_app_name = self.ctx.app.name - sanic_route = rv[0].name - - if sanic_route.startswith("%s." % sanic_app_name): - # We add a 1 to the len of the sanic_app_name because there is a dot - # that joins app name and the route name - # Format: app_name.route_name - sanic_route = sanic_route[len(sanic_app_name) + 1 :] - - scope.set_transaction_name( - sanic_route, source=TransactionSource.COMPONENT - ) - else: - scope.set_transaction_name( - rv[0].__name__, source=TransactionSource.COMPONENT - ) - - return rv - - -@ensure_integration_enabled(SanicIntegration) -def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "sanic", "handled": False}, - ) - - if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: - return - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_request_attributes(request: "Request") -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from a Sanic request. - """ - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - urlparts = urlsplit(request.url) - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.url - attributes["url.path"] = urlparts.path - - if urlparts.query: - attributes[SPANDATA.HTTP_QUERY] = urlparts.query - - if urlparts.scheme: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme - - if should_send_default_pii() and request.remote_addr: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr - - return attributes - - -def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": - def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - try: - if hint and issubclass(hint["exc_info"][0], SanicException): - return None - except KeyError: - pass - - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - extractor = SanicRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - urlparts = urlsplit(request.url) - - request_info["url"] = "%s://%s%s" % ( - urlparts.scheme, - urlparts.netloc, - urlparts.path, - ) - - request_info["query_string"] = urlparts.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_addr} - request_info["headers"] = _filter_headers(dict(request.headers)) - - return event - - return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py deleted file mode 100644 index 16f91b28ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/serverless.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import event_from_exception, reraise - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union, overload - - F = TypeVar("F", bound=Callable[..., Any]) - -else: - - def overload(x: "F") -> "F": - return x - - -@overload -def serverless_function(f: "F", flush: bool = True) -> "F": - pass - - -@overload -def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 - pass - - -def serverless_function( # noqa - f: "Optional[F]" = None, flush: bool = True -) -> "Union[F, Callable[[F], F]]": - def wrapper(f: "F") -> "F": - @wraps(f) - def inner(*args: "Any", **kwargs: "Any") -> "Any": - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - - try: - return f(*args, **kwargs) - except Exception: - _capture_and_reraise() - finally: - if flush: - sentry_sdk.flush() - - return inner # type: ignore - - if f is None: - return wrapper - else: - return wrapper(f) - - -def _capture_and_reraise() -> None: - exc_info = sys.exc_info() - client = sentry_sdk.get_client() - if client.is_active(): - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "serverless", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py deleted file mode 100644 index 775170fb9f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/socket.py +++ /dev/null @@ -1,142 +0,0 @@ -import socket - -import sentry_sdk -from sentry_sdk._types import MYPY -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if MYPY: - from socket import AddressFamily, SocketKind - from typing import List, Optional, Tuple, Union - -__all__ = ["SocketIntegration"] - - -class SocketIntegration(Integration): - identifier = "socket" - origin = f"auto.socket.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) - """ - _patch_create_connection() - _patch_getaddrinfo() - - -def _get_span_description( - host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" -) -> str: - try: - host = host.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - try: - port = port.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - description = "%s:%s" % (host, port) # type: ignore - return description - - -def _patch_create_connection() -> None: - real_create_connection = socket.create_connection - - def create_connection( - address: "Tuple[Optional[str], int]", - timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore - source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, - ) -> "socket.socket": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_create_connection(address, timeout, source_address) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(address[0], address[1]), - attributes={ - "sentry.op": OP.SOCKET_CONNECTION, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if address[0] is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) - span.set_attribute(SPANDATA.SERVER_PORT, address[1]) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_CONNECTION, - name=_get_span_description(address[0], address[1]), - origin=SocketIntegration.origin, - ) as span: - span.set_data("address", address) - span.set_data("timeout", timeout) - span.set_data("source_address", source_address) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - - socket.create_connection = create_connection # type: ignore - - -def _patch_getaddrinfo() -> None: - real_getaddrinfo = socket.getaddrinfo - - def getaddrinfo( - host: "Union[bytes, str, None]", - port: "Union[bytes, str, int, None]", - family: int = 0, - type: int = 0, - proto: int = 0, - flags: int = 0, - ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_getaddrinfo(host, port, family, type, proto, flags) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(host, port), - attributes={ - "sentry.op": OP.SOCKET_DNS, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if isinstance(host, str): - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - elif isinstance(host, bytes): - span.set_attribute( - SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") - ) - - if isinstance(port, int): - span.set_attribute(SPANDATA.SERVER_PORT, port) - elif port is not None: - try: - span.set_attribute(SPANDATA.SERVER_PORT, int(port)) - except (ValueError, TypeError): - pass - - return real_getaddrinfo(host, port, family, type, proto, flags) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_DNS, - name=_get_span_description(host, port), - origin=SocketIntegration.origin, - ) as span: - span.set_data("host", host) - span.set_data("port", port) - - return real_getaddrinfo(host, port, family, type, proto, flags) - - socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py deleted file mode 100644 index 10d94163c5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from sentry_sdk.integrations.spark.spark_driver import SparkIntegration -from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration - -__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py deleted file mode 100644 index a83532b6a6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_driver.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Optional - - from pyspark import SparkContext - - from sentry_sdk._types import Event, Hint - - -class SparkIntegration(Integration): - identifier = "spark" - - @staticmethod - def setup_once() -> None: - _setup_sentry_tracing() - - -def _set_app_properties() -> None: - """ - Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. - This allows worker integration to have access to app_name and application_id. - """ - from pyspark import SparkContext - - spark_context = SparkContext._active_spark_context - if spark_context: - spark_context.setLocalProperty( - "sentry_app_name", - spark_context.appName, - ) - spark_context.setLocalProperty( - "sentry_application_id", - spark_context.applicationId, - ) - - -def _start_sentry_listener(sc: "SparkContext") -> None: - """ - Start java gateway server to add custom `SparkListener` - """ - from pyspark.java_gateway import ensure_callback_server_started - - gw = sc._gateway - ensure_callback_server_started(gw) - listener = SentryListener() - sc._jsc.sc().addSparkListener(listener) - - -def _add_event_processor(sc: "SparkContext") -> None: - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - if sentry_sdk.get_client().get_integration(SparkIntegration) is None: - return event - - if sc._active_spark_context is None: - return event - - event.setdefault("user", {}).setdefault("id", sc.sparkUser()) - - event.setdefault("tags", {}).setdefault( - "executor.id", sc._conf.get("spark.executor.id") - ) - event["tags"].setdefault( - "spark-submit.deployMode", - sc._conf.get("spark.submit.deployMode"), - ) - event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) - event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) - event["tags"].setdefault("spark_version", sc.version) - event["tags"].setdefault("app_name", sc.appName) - event["tags"].setdefault("application_id", sc.applicationId) - event["tags"].setdefault("master", sc.master) - event["tags"].setdefault("spark_home", sc.sparkHome) - - event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) - - return event - - -def _activate_integration(sc: "SparkContext") -> None: - _start_sentry_listener(sc) - _set_app_properties() - _add_event_processor(sc) - - -def _patch_spark_context_init() -> None: - from pyspark import SparkContext - - spark_context_init = SparkContext._do_init - - @ensure_integration_enabled(SparkIntegration, spark_context_init) - def _sentry_patched_spark_context_init( - self: "SparkContext", *args: "Any", **kwargs: "Any" - ) -> "Optional[Any]": - rv = spark_context_init(self, *args, **kwargs) - _activate_integration(self) - return rv - - SparkContext._do_init = _sentry_patched_spark_context_init - - -def _setup_sentry_tracing() -> None: - from pyspark import SparkContext - - if SparkContext._active_spark_context is not None: - _activate_integration(SparkContext._active_spark_context) - return - _patch_spark_context_init() - - -class SparkListener: - def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 - pass - - def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklistedForStage( # noqa: N802 - self, - executorBlacklistedForStage: "Any", # noqa: N803 - ) -> None: - pass - - def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 - pass - - def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 - pass - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - pass - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 - pass - - def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 - pass - - class Java: - implements = ["org.apache.spark.scheduler.SparkListenerInterface"] - - -class SentryListener(SparkListener): - def _add_breadcrumb( - self, - level: str, - message: str, - data: "Optional[dict[str, Any]]" = None, - ) -> None: - sentry_sdk.get_isolation_scope().add_breadcrumb( - level=level, message=message, data=data - ) - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - message = "Job {} Started".format(jobStart.jobId()) - self._add_breadcrumb(level="info", message=message) - _set_app_properties() - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - level = "" - message = "" - data = {"result": jobEnd.jobResult().toString()} - - if jobEnd.jobResult().toString() == "JobSucceeded": - level = "info" - message = "Job {} Ended".format(jobEnd.jobId()) - else: - level = "warning" - message = "Job {} Failed".format(jobEnd.jobId()) - - self._add_breadcrumb(level=level, message=message, data=data) - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - stage_info = stageSubmitted.stageInfo() - message = "Stage {} Submitted".format(stage_info.stageId()) - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - self._add_breadcrumb(level="info", message=message, data=data) - _set_app_properties() - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - from py4j.protocol import Py4JJavaError # type: ignore - - stage_info = stageCompleted.stageInfo() - message = "" - level = "" - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - # Have to Try Except because stageInfo.failureReason() is typed with Scala Option - try: - data["reason"] = stage_info.failureReason().get() - message = "Stage {} Failed".format(stage_info.stageId()) - level = "warning" - except Py4JJavaError: - message = "Stage {} Completed".format(stage_info.stageId()) - level = "info" - - self._add_breadcrumb(level=level, message=message, data=data) - - -def _get_attempt_id(stage_info: "Any") -> "Optional[int]": - try: - return stage_info.attemptId() - except Exception: - pass - - try: - return stage_info.attemptNumber() - except Exception: - pass - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py deleted file mode 100644 index 5906472748..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/spark/spark_worker.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_hint_with_exc_info, - exc_info_from_error, - single_exception_from_error_tuple, - walk_exception_chain, -) - -if TYPE_CHECKING: - from typing import Any, Optional - - from sentry_sdk._types import Event, ExcInfo, Hint - - -class SparkWorkerIntegration(Integration): - identifier = "spark_worker" - - @staticmethod - def setup_once() -> None: - import pyspark.daemon as original_daemon - - original_daemon.worker_main = _sentry_worker_main - - -def _capture_exception(exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - - mechanism = {"type": "spark", "handled": False} - - exc_info = exc_info_from_error(exc_info) - - exc_type, exc_value, tb = exc_info - rv = [] - - # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - if exc_type not in (SystemExit, EOFError, ConnectionResetError): - rv.append( - single_exception_from_error_tuple( - exc_type, exc_value, tb, client.options, mechanism - ) - ) - - if rv: - rv.reverse() - hint = event_hint_with_exc_info(exc_info) - event: "Event" = {"level": "error", "exception": {"values": rv}} - - _tag_task_context() - - sentry_sdk.capture_event(event, hint=hint) - - -def _tag_task_context() -> None: - from pyspark.taskcontext import TaskContext - - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - integration = sentry_sdk.get_client().get_integration( - SparkWorkerIntegration - ) - task_context = TaskContext.get() - - if integration is None or task_context is None: - return event - - event.setdefault("tags", {}).setdefault( - "stageId", str(task_context.stageId()) - ) - event["tags"].setdefault("partitionId", str(task_context.partitionId())) - event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) - event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) - - if task_context._localProperties: - if "sentry_app_name" in task_context._localProperties: - event["tags"].setdefault( - "app_name", task_context._localProperties["sentry_app_name"] - ) - event["tags"].setdefault( - "application_id", - task_context._localProperties["sentry_application_id"], - ) - - if "callSite.short" in task_context._localProperties: - event.setdefault("extra", {}).setdefault( - "callSite", task_context._localProperties["callSite.short"] - ) - - return event - - -def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: - import pyspark.worker as original_worker - - try: - original_worker.main(*args, **kwargs) - except SystemExit: - if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py deleted file mode 100644 index 962fe4ee53..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sqlalchemy.py +++ /dev/null @@ -1,185 +0,0 @@ -from sentry_sdk.consts import SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - parse_version, -) - -try: - from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore - from sqlalchemy.engine import Engine # type: ignore - from sqlalchemy.event import listen # type: ignore -except ImportError: - raise DidNotEnable("SQLAlchemy not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional, Union - - -class SqlalchemyIntegration(Integration): - identifier = "sqlalchemy" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(SQLALCHEMY_VERSION) - _check_minimum_version(SqlalchemyIntegration, version) - - listen(Engine, "before_cursor_execute", _before_cursor_execute) - listen(Engine, "after_cursor_execute", _after_cursor_execute) - listen(Engine, "handle_error", _handle_error) - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _before_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - executemany: bool, - *args: "Any", -) -> None: - ctx_mgr = record_sql_queries( - cursor, - statement, - parameters, - paramstyle=context and context.dialect and context.dialect.paramstyle or None, - executemany=executemany, - span_origin=SqlalchemyIntegration.origin, - ) - context._sentry_sql_span_manager = ctx_mgr - - span = ctx_mgr.__enter__() - - if span is not None: - _set_db_data(span, conn) - context._sentry_sql_span = span - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _after_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - *args: "Any", -) -> None: - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - context, "_sentry_sql_span_manager", None - ) - - # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. - span: "Optional[Union[Span, StreamedSpan]]" = getattr( - context, "_sentry_sql_span", None - ) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if ctx_mgr is not None: - context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - if isinstance(span, Span): - with capture_internal_exceptions(): - add_query_source(span) - - -def _handle_error(context: "Any", *args: "Any") -> None: - execution_context = context.execution_context - if execution_context is None: - return - - span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) - - if span is not None: - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # _after_cursor_execute does not get called for crashing SQL stmts. Judging - # from SQLAlchemy codebase it does seem like any error coming into this - # handler is going to be fatal. - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - execution_context, "_sentry_sql_span_manager", None - ) - - if ctx_mgr is not None: - execution_context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - -# See: https://docs.sqlalchemy.org/en/20/dialects/index.html -def _get_db_system(name: str) -> "Optional[str]": - name = str(name) - - if "sqlite" in name: - return "sqlite" - - if "postgres" in name: - return "postgresql" - - if "mariadb" in name: - return "mariadb" - - if "mysql" in name: - return "mysql" - - if "oracle" in name: - return "oracle" - - return None - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - db_system = _get_db_system(conn.engine.name) - - if isinstance(span, StreamedSpan): - if db_system is not None: - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) - else: - if db_system is not None: - span.set_data(SPANDATA.DB_SYSTEM, db_system) - - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - try: - driver = conn.dialect.driver - if driver: - set_on_span(SPANDATA.DB_DRIVER_NAME, driver) - except Exception: - pass - - if conn.engine.url is None: - return - - db_name = conn.engine.url.database - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - server_address = conn.engine.url.host - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = conn.engine.url.port - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py deleted file mode 100644 index 3f9fbf4d8e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlette.py +++ /dev/null @@ -1,858 +0,0 @@ -import functools -import json -import sys -import warnings -from collections.abc import Set -from copy import deepcopy -from json import JSONDecodeError -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations._asgi_common import _RootPathInPath -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - HttpCodeRangeContainer, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Awaitable, - Callable, - Container, - Dict, - Optional, - Tuple, - Union, - ) - - from sentry_sdk._types import Event, HttpStatusCodeRange -try: - import starlette - from starlette import __version__ as STARLETTE_VERSION - from starlette.applications import Starlette - from starlette.datastructures import ( - UploadFile, - ) - from starlette.middleware import Middleware - from starlette.middleware.authentication import ( - AuthenticationMiddleware, - ) - from starlette.requests import Request - from starlette.routing import Match - from starlette.types import ASGIApp, Receive, Send - from starlette.types import Scope as StarletteScope -except ImportError: - raise DidNotEnable("Starlette is not installed") - -try: - # Starlette 0.20 - from starlette.middleware.exceptions import ExceptionMiddleware -except ImportError: - # Startlette 0.19.1 - from starlette.exceptions import ExceptionMiddleware # type: ignore - -try: - # Optional dependency of Starlette to parse form data. - try: - # python-multipart 0.0.13 and later - import python_multipart as multipart - except ImportError: - # python-multipart 0.0.12 and earlier - import multipart # type: ignore -except ImportError: - multipart = None # type: ignore[assignment] - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -_DEFAULT_TRANSACTION_NAME = "generic Starlette request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class StarletteIntegration(Integration): - identifier = "starlette" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "url", - failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - middleware_spans: bool = False, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ): - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - if isinstance(failed_request_status_codes, Set): - self.failed_request_status_codes: "Container[int]" = ( - failed_request_status_codes - ) - else: - warnings.warn( - "Passing a list or None for failed_request_status_codes is deprecated. " - "Please pass a set of int instead.", - DeprecationWarning, - stacklevel=2, - ) - - if failed_request_status_codes is None: - self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES - else: - self.failed_request_status_codes = HttpCodeRangeContainer( - failed_request_status_codes - ) - - @staticmethod - def setup_once() -> None: - version = parse_version(STARLETTE_VERSION) - - if version is None: - raise DidNotEnable( - "Unparsable Starlette version: {}".format(STARLETTE_VERSION) - ) - - patch_middlewares() - # Starlette tolerates both starting with: - # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. - root_path_in_path = ( - _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED - ) - patch_asgi_app(root_path_in_path=root_path_in_path) - patch_request_response() - - if version >= (0, 24): - patch_templates() - - -def _enable_span_for_middleware( - middleware_class: "Any", -) -> "Any": - old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ - - async def _create_span_call( - app: "Any", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - **kwargs: "Any", - ) -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await old_call(app, scope, receive, send, **kwargs) - - # Update transaction name with middleware name - name, source = _get_transaction_from_middleware(app, scope, integration) - - if name is not None: - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=source, - ) - - if not integration.middleware_spans: - return await old_call(app, scope, receive, send, **kwargs) - - middleware_name = app.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarletteIntegration.origin, - "middleware.name": middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarletteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlette.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await send(*args, **kwargs) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(app, scope, new_receive, new_send, **kwargs) - - not_yet_patched = old_call.__name__ not in [ - "_create_span_call", - "_sentry_authenticationmiddleware_call", - "_sentry_exceptionmiddleware_call", - ] - - if not_yet_patched: - middleware_class.__call__ = _create_span_call - - return middleware_class - - -def _serialize_request_body_data(data: "Any") -> str: - # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values - def _default(value: "Any") -> "Any": - if isinstance(value, AnnotatedValue): - return value.value - return str(value) - - return json.dumps(data, default=_default) - - -@ensure_integration_enabled(StarletteIntegration) -def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarletteIntegration.identifier, "handled": handled}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def patch_exception_middleware(middleware_class: "Any") -> None: - """ - Capture all exceptions in Starlette app and - also extract user information. - """ - old_middleware_init = middleware_class.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - - if not_yet_patched: - - def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: - old_middleware_init(self, *args, **kwargs) - - # Patch existing exception handlers - old_handlers = self._exception_handlers.copy() - - async def _sentry_patched_exception_handler( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration( - StarletteIntegration - ) - - exp = args[0] - - if integration is not None: - is_http_server_error = ( - hasattr(exp, "status_code") - and isinstance(exp.status_code, int) - and exp.status_code in integration.failed_request_status_codes - ) - if is_http_server_error: - _capture_exception(exp, handled=True) - - # Find a matching handler - old_handler = None - for cls in type(exp).__mro__: - if cls in old_handlers: - old_handler = old_handlers[cls] - break - - if old_handler is None: - return - - if _is_async_callable(old_handler): - return await old_handler(self, *args, **kwargs) - else: - return old_handler(self, *args, **kwargs) - - for key in self._exception_handlers.keys(): - self._exception_handlers[key] = _sentry_patched_exception_handler - - middleware_class.__init__ = _sentry_middleware_init - - old_call = middleware_class.__call__ - - async def _sentry_exceptionmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - # Also add the user (that was eventually set by be Authentication middle - # that was called before this middleware). This is done because the authentication - # middleware sets the user in the scope and then (in the same function) - # calls this exception middelware. In case there is no exception (or no handler - # for the type of exception occuring) then the exception bubbles up and setting the - # user information into the sentry scope is done in auth middleware and the - # ASGI middleware will then send everything to Sentry and this is fine. - # But if there is an exception happening that the exception middleware here - # has a handler for, it will send the exception directly to Sentry, so we need - # the user information right now. - # This is why we do it here. - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_exceptionmiddleware_call - - -@ensure_integration_enabled(StarletteIntegration) -def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: - """ - Extracts user information from the ASGI scope and - adds it to Sentry's scope. - """ - if "user" not in scope: - return - - if not should_send_default_pii(): - return - - user_info: "Dict[str, Any]" = {} - starlette_user = scope["user"] - - username = getattr(starlette_user, "username", None) - if username: - user_info.setdefault("username", starlette_user.username) - - user_id = getattr(starlette_user, "id", None) - if user_id: - user_info.setdefault("id", starlette_user.id) - - email = getattr(starlette_user, "email", None) - if email: - user_info.setdefault("email", starlette_user.email) - - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - -def patch_authentication_middleware(middleware_class: "Any") -> None: - """ - Add user information to Sentry scope. - """ - old_call = middleware_class.__call__ - - not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) - - if not_yet_patched: - - async def _sentry_authenticationmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_authenticationmiddleware_call - - -def patch_middlewares() -> None: - """ - Patches Starlettes `Middleware` class to record - spans for every middleware invoked. - """ - old_middleware_init = Middleware.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - if not_yet_patched: - - def _sentry_middleware_init( - self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - if cls == SentryAsgiMiddleware: - return old_middleware_init(self, cls, *args, **kwargs) - - span_enabled_cls = _enable_span_for_middleware(cls) - old_middleware_init(self, span_enabled_cls, *args, **kwargs) - - if cls == AuthenticationMiddleware: - patch_authentication_middleware(cls) - - if cls == ExceptionMiddleware: - patch_exception_middleware(cls) - - Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] - - -def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: - """ - Instrument Starlette ASGI app using the SentryAsgiMiddleware. - """ - old_app = Starlette.__call__ - - async def _sentry_patched_asgi_app( - self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StarletteIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - mechanism_type=StarletteIntegration.identifier, - transaction_style=integration.transaction_style, - span_origin=StarletteIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - asgi_version=3, - root_path_in_path=root_path_in_path, - ) - - return await middleware(scope, receive, send) - - Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] - - -# This was vendored in from Starlette to support Starlette 0.19.1 because -# this function was only introduced in 0.20.x -def _is_async_callable(obj: "Any") -> bool: - while isinstance(obj, functools.partial): - obj = obj.func - - return iscoroutinefunction(obj) or ( - callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] - ) - - -def _get_cached_request_body_attribute( - client: "sentry_sdk.client.BaseClient", request: "Request" -) -> "Optional[str]": - """ - Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. - """ - if "content-length" not in request.headers: - return None - - try: - content_length = int(request.headers["content-length"]) - except ValueError: - return None - - if content_length and not request_body_within_bounds(client, content_length): - return OVER_SIZE_LIMIT_SUBSTITUTE - - if hasattr(request, "_json"): - return json.dumps(request._json) - - formdata_body = getattr(request, "_form", None) - if formdata_body is None: - return None - - form_data = {} - for key, val in formdata_body.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = val if not is_file else "[Unparsable]" - - return json.dumps(form_data) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), - integration.transaction_style, - request, - ) - - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Add info from request to event - request_info = event.get("request", {}) - if info: - if "cookies" in info: - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_request_response() -> None: - old_request_response = starlette.routing.request_response - - def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": - old_func = func - - is_coroutine = _is_async_callable(old_func) - if is_coroutine: - - async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_func, *args, **kwargs) - - func = _sentry_async_func - - else: - - @functools.wraps(old_func) - def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(StarletteIntegration) - if integration is None: - return old_func(*args, **kwargs) - - current_scope = sentry_sdk.get_current_scope() - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming: - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - current_span._segment._update_active_thread() - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - request = args[0] - - _set_transaction_name_and_source( - sentry_scope, integration.transaction_style, request - ) - - extractor = StarletteRequestExtractor(request) - cookies = extractor.extract_cookies_from_request() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor( - event: "Event", hint: "dict[str, Any]" - ) -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if cookies: - request_info["cookies"] = cookies - - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - return old_func(*args, **kwargs) - - func = _sentry_sync_func - - return old_request_response(func) - - starlette.routing.request_response = _sentry_request_response - - -def patch_templates() -> None: - # If markupsafe is not installed, then Jinja2 is not installed - # (markupsafe is a dependency of Jinja2) - # In this case we do not need to patch the Jinja2Templates class - try: - from markupsafe import Markup - except ImportError: - return # Nothing to do - - # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a - try: - from starlette.templating import Jinja2Templates - except ImportError: - return - - old_jinja2templates_init = Jinja2Templates.__init__ - - not_yet_patched = "_sentry_jinja2templates_init" not in str( - old_jinja2templates_init - ) - - if not_yet_patched: - - def _sentry_jinja2templates_init( - self: "Jinja2Templates", *args: "Any", **kwargs: "Any" - ) -> None: - def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": - trace_meta = Markup( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - return { - "sentry_trace_meta": trace_meta, - } - - kwargs.setdefault("context_processors", []) - - if add_sentry_trace_meta not in kwargs["context_processors"]: - kwargs["context_processors"].append(add_sentry_trace_meta) - - return old_jinja2templates_init(self, *args, **kwargs) - - Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] - - -class StarletteRequestExtractor: - """ - Extracts useful information from the Starlette request - (like form data or cookies) and adds it to the Sentry event. - """ - - def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: - self.request = request - - def extract_cookies_from_request( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - cookies: "Optional[Dict[str, Any]]" = None - if should_send_default_pii(): - cookies = self.cookies() - - return cookies - - async def extract_request_info( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - client = sentry_sdk.get_client() - - request_info: "Dict[str, Any]" = {} - - with capture_internal_exceptions(): - # Add cookies - if should_send_default_pii(): - request_info["cookies"] = self.cookies() - - # If there is no body, just return the cookies - content_length = await self.content_length() - if not content_length: - return request_info - - # Add annotation if body is too big - if content_length and not request_body_within_bounds( - client, content_length - ): - request_info["data"] = AnnotatedValue.removed_because_over_size_limit() - return request_info - - # Add JSON body, if it is a JSON request - json = await self.json() - if json: - request_info["data"] = json - return request_info - - # Add form as key/value pairs, if request has form data - form = await self.form() - if form: - form_data = {} - for key, val in form.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = ( - val - if not is_file - else AnnotatedValue.removed_because_raw_data() - ) - - request_info["data"] = form_data - return request_info - - # Raw data, do not add body just an annotation - request_info["data"] = AnnotatedValue.removed_because_raw_data() - return request_info - - async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": - if "content-length" in self.request.headers: - return int(self.request.headers["content-length"]) - - return None - - def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": - return self.request.cookies - - async def form(self: "StarletteRequestExtractor") -> "Any": - if multipart is None: - return None - - # Parse the body first to get it cached, as Starlette does not cache form() as it - # does with body() and json() https://github.com/encode/starlette/discussions/1933 - # Calling `.form()` without calling `.body()` first will - # potentially break the users project. - await self.request.body() - - return await self.request.form() - - def is_json(self: "StarletteRequestExtractor") -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": - if not self.is_json(): - return None - try: - return await self.request.json() - except JSONDecodeError: - return None - - -def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": - router = scope.get("router") - if not router: - return None - - for route in router.routes: - match = route.matches(scope) - if match[0] == Match.FULL: - try: - return route.path - except AttributeError: - # routes added via app.host() won't have a path attribute - return scope.get("path") - - return None - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = None - source = SOURCE_FOR_STYLE[transaction_style] - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or None - - elif transaction_style == "url": - name = _transaction_name_from_router(request.scope) - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - scope.set_transaction_name(name, source=source) - - -def _get_transaction_from_middleware( - app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" -) -> "Tuple[Optional[str], Optional[str]]": - name = None - source = None - - if integration.transaction_style == "endpoint": - name = transaction_from_function(app.__class__) - source = TransactionSource.COMPONENT - elif integration.transaction_style == "url": - name = _transaction_name_from_router(asgi_scope) - source = TransactionSource.ROUTE - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py deleted file mode 100644 index 1eebd37e84..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/starlite.py +++ /dev/null @@ -1,314 +0,0 @@ -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from pydantic import BaseModel - from starlite import Request, Starlite, State # type: ignore - from starlite.handlers.base import BaseRouteHandler # type: ignore - from starlite.middleware import DefineMiddleware # type: ignore - from starlite.plugins.base import get_plugin_for_value # type: ignore - from starlite.routes.http import HTTPRoute # type: ignore - from starlite.utils import ( # type: ignore - ConnectionDataExtractor, - Ref, - is_async_callable, - ) -except ImportError: - raise DidNotEnable("Starlite is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from starlite import MiddlewareProtocol - from starlite.types import ( # type: ignore - ASGIApp, - Hint, - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from starlite.types import ( - Scope as StarliteScope, - ) - - from sentry_sdk._types import Event - - -_DEFAULT_TRANSACTION_NAME = "generic Starlite request" - - -class StarliteIntegration(Integration): - identifier = "starlite" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - -class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - -def patch_app_init() -> None: - """ - Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. - See: - - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception - - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ - """ - old__init__ = Starlite.__init__ - - @ensure_integration_enabled(StarliteIntegration, old__init__) - def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: - after_exception = kwargs.pop("after_exception", []) - kwargs.update( - after_exception=[ - exception_handler, - *( - after_exception - if isinstance(after_exception, list) - else [after_exception] - ), - ] - ) - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Starlite.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryStarliteASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "StarliteScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(StarliteIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarliteIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarliteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlite.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await receive(*args, **kwargs) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await send(message) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - elif isinstance(route_handler.fn, Ref): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = StarliteIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if not scope_user: - return None - if isinstance(scope_user, dict): - return scope_user - if isinstance(scope_user, BaseModel): - return scope_user.dict() - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - plugin = get_plugin_for_value(scope_user) - if plugin and not is_async_callable(plugin.to_dict): - return plugin.to_dict(scope_user) - - return None - - -@ensure_integration_enabled(StarliteIntegration) -def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarliteIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py deleted file mode 100644 index 746e09b36f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/statsig.py +++ /dev/null @@ -1,37 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.utils import parse_version - -try: - from statsig import statsig as statsig_module - from statsig.version import __version__ as STATSIG_VERSION -except ImportError: - raise DidNotEnable("statsig is not installed") - -if TYPE_CHECKING: - from statsig.statsig_user import StatsigUser - - -class StatsigIntegration(Integration): - identifier = "statsig" - - @staticmethod - def setup_once() -> None: - version = parse_version(STATSIG_VERSION) - _check_minimum_version(StatsigIntegration, version, "statsig") - - # Wrap and patch evaluation method(s) in the statsig module - old_check_gate = statsig_module.check_gate - - @wraps(old_check_gate) - def sentry_check_gate( - user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_check_gate(user, gate, *args, **kwargs) - add_feature_flag(gate, enabled) - return enabled - - statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py deleted file mode 100644 index 82f30f2dda..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/stdlib.py +++ /dev/null @@ -1,404 +0,0 @@ -import os -import platform -import subprocess -import sys -from http.client import HTTPConnection, HTTPResponse -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - EnvironHeaders, - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - is_sentry_url, - logger, - parse_url, - safe_repr, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from sentry_sdk._types import Event, Hint - - -_RUNTIME_CONTEXT: "dict[str, object]" = { - "name": platform.python_implementation(), - "version": "%s.%s.%s" % (sys.version_info[:3]), - "build": sys.version, -} - - -class StdlibIntegration(Integration): - identifier = "stdlib" - - @staticmethod - def setup_once() -> None: - _install_httplib() - _install_subprocess() - - @add_global_event_processor - def add_python_runtime_context( - event: "Event", hint: "Hint" - ) -> "Optional[Event]": - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is not None: - contexts = event.setdefault("contexts", {}) - if isinstance(contexts, dict) and "runtime" not in contexts: - contexts["runtime"] = _RUNTIME_CONTEXT - - return event - - -def _complete_span(span: "Union[Span, StreamedSpan]") -> None: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - -def _install_httplib() -> None: - real_putrequest = HTTPConnection.putrequest - real_getresponse = HTTPConnection.getresponse - real_read = HTTPResponse.read - real_close = HTTPResponse.close - - def putrequest( - self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - default_port = self.default_port - - # proxies go through set_tunnel - tunnel_host = getattr(self, "_tunnel_host", None) - if tunnel_host: - host = tunnel_host - port = getattr(self, "_tunnel_port", default_port) - else: - host = self.host - port = self.port - - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is None or is_sentry_url( - client, host - ): - return real_putrequest(self, method, url, *args, **kwargs) - - real_url = url - if real_url is None or not real_url.startswith(("http://", "https://")): - real_url = "%s://%s%s%s" % ( - default_port == 443 and "https" or "http", - host, - port != default_port and ":%s" % port or "", - url, - ) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(real_url, sanitize=False) - - span_streaming = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - attributes={ - "sentry.origin": "auto.http.stdlib.httplib", - "sentry.op": OP.HTTP_CLIENT, - SPANDATA.HTTP_REQUEST_METHOD: method, - }, - ) - - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - - set_on_span = span.set_attribute - - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - origin="auto.http.stdlib.httplib", - ) - - span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - - set_on_span = span.set_data - - # for proxies, these point to the proxy host/port - if tunnel_host: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) - set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) - - rv = real_putrequest(self, method, url, *args, **kwargs) - - if should_propagate_trace(client, real_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=key, value=value, real_url=real_url - ) - ) - self.putheader(key, value) - - self._sentrysdk_span = span # type: ignore[attr-defined] - - return rv - - def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": - span = getattr(self, "_sentrysdk_span", None) - - if span is None: - return real_getresponse(self, *args, **kwargs) - - try: - rv = real_getresponse(self, *args, **kwargs) - except BaseException: - _complete_span(span) - raise - - if isinstance(span, StreamedSpan): - status_code = int(rv.status) - span.status = "error" if status_code >= 400 else "ok" - span.set_attribute("http.response.status_code", status_code) - else: - span.set_http_status(int(rv.status)) - span.set_data("reason", rv.reason) - - # getresponse doesn't include actually reading the response body. This - # is done in read(). So if the metadata/headers suggest there's a body to - # read, don't finish the span just yet, but save it for ending it later. - has_body = rv.chunked or (rv.length is not None and rv.length > 0) - if has_body: - rv._sentrysdk_span = span # type: ignore[attr-defined] - else: - _complete_span(span) - - return rv - - def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": - try: - return real_read(self, *args, **kwargs) - finally: - span = getattr(self, "_sentrysdk_span", None) - # read() might be called multiple times to consume a single body, - # so we can't just end the span when read() is done. Instead, - # try to figure out whether the response body has been fully read. - if span and (self.fp is None or self.closed): - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - def close(self: "HTTPResponse") -> None: - # We patch close() as a best effort fallback in case the span is not - # ended yet in getresponse() or read(). - - try: - real_close(self) - finally: - span = getattr(self, "_sentrysdk_span", None) - if span is not None: - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - HTTPConnection.putrequest = putrequest # type: ignore[method-assign] - HTTPConnection.getresponse = getresponse # type: ignore[method-assign] - HTTPResponse.read = read # type: ignore[method-assign] - HTTPResponse.close = close # type: ignore[assignment,method-assign] - - -def _init_argument( - args: "List[Any]", - kwargs: "Dict[Any, Any]", - name: str, - position: int, - setdefault_callback: "Optional[Callable[[Any], Any]]" = None, -) -> "Any": - """ - given (*args, **kwargs) of a function call, retrieve (and optionally set a - default for) an argument by either name or position. - - This is useful for wrapping functions with complex type signatures and - extracting a few arguments without needing to redefine that function's - entire type signature. - """ - - if name in kwargs: - rv = kwargs[name] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - kwargs[name] = rv - elif position < len(args): - rv = args[position] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - args[position] = rv - else: - rv = setdefault_callback and setdefault_callback(None) - if rv is not None: - kwargs[name] = rv - - return rv - - -def _install_subprocess() -> None: - old_popen_init = subprocess.Popen.__init__ - - @ensure_integration_enabled(StdlibIntegration, old_popen_init) - def sentry_patched_popen_init( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> None: - # Convert from tuple to list to be able to set values. - a = list(a) - - args = _init_argument(a, kw, "args", 0) or [] - cwd = _init_argument(a, kw, "cwd", 9) - - # if args is not a list or tuple (and e.g. some iterator instead), - # let's not use it at all. There are too many things that can go wrong - # when trying to collect an iterator into a list and setting that list - # into `a` again. - # - # Also invocations where `args` is not a sequence are not actually - # legal. They just happen to work under CPython. - description = None - - if isinstance(args, (list, tuple)) and len(args) < 100: - with capture_internal_exceptions(): - description = " ".join(map(str, args)) - - if description is None: - description = safe_repr(args) - - env = None - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.SUBPROCESS, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.SUBPROCESS, - name=description, - origin="auto.subprocess.stdlib.subprocess", - ) - - with span: - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - if env is None: - env = _init_argument( - a, - kw, - "env", - 10, - lambda x: dict(x if x is not None else os.environ), - ) - env["SUBPROCESS_" + k.upper().replace("-", "_")] = v - - if cwd and isinstance(span, Span): - span.set_data("subprocess.cwd", cwd) - - rv = old_popen_init(self, *a, **kw) - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - else: - span.set_tag("subprocess.pid", self.pid) - - return rv - - subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore - - old_popen_wait = subprocess.Popen.wait - - @ensure_integration_enabled(StdlibIntegration, old_popen_wait) - def sentry_patched_popen_wait( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_WAIT, - attributes={ - "sentry.op": OP.SUBPROCESS_WAIT, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_wait(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_WAIT, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_wait(self, *a, **kw) - - subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore - - old_popen_communicate = subprocess.Popen.communicate - - @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) - def sentry_patched_popen_communicate( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_COMMUNICATE, - attributes={ - "sentry.op": OP.SUBPROCESS_COMMUNICATE, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_communicate(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_COMMUNICATE, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_communicate(self, *a, **kw) - - subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore - - -def get_subprocess_traceparent_headers() -> "EnvironHeaders": - return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py deleted file mode 100644 index 5f00e8bf6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/strawberry.py +++ /dev/null @@ -1,493 +0,0 @@ -import functools -import hashlib -import warnings -from inspect import isawaitable - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from functools import cached_property -except ImportError: - # The strawberry integration requires Python 3.8+. functools.cached_property - # was added in 3.8, so this check is technically not needed, but since this - # is an auto-enabling integration, we might get to executing this import in - # lower Python versions, so we need to deal with it. - raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") - -try: - from strawberry import Schema - from strawberry.extensions import SchemaExtension - from strawberry.extensions.tracing.utils import ( - should_skip_tracing as strawberry_should_skip_tracing, - ) - from strawberry.http import async_base_view, sync_base_view -except ImportError: - raise DidNotEnable("strawberry-graphql is not installed") - -try: - from strawberry.extensions.tracing import ( - SentryTracingExtension as StrawberrySentryAsyncExtension, - ) - from strawberry.extensions.tracing import ( - SentryTracingExtensionSync as StrawberrySentrySyncExtension, - ) -except ImportError: - StrawberrySentryAsyncExtension = None - StrawberrySentrySyncExtension = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Generator, List, Optional - - from graphql import GraphQLError, GraphQLResolveInfo - from strawberry.http import GraphQLHTTPResponse - from strawberry.types import ExecutionContext - - from sentry_sdk._types import Event, EventProcessor - - -ignore_logger("strawberry.execution") - - -class StrawberryIntegration(Integration): - identifier = "strawberry" - origin = f"auto.graphql.{identifier}" - - def __init__(self, async_execution: "Optional[bool]" = None) -> None: - if async_execution not in (None, False, True): - raise ValueError( - 'Invalid value for async_execution: "{}" (must be bool)'.format( - async_execution - ) - ) - self.async_execution = async_execution - - @staticmethod - def setup_once() -> None: - version = package_version("strawberry-graphql") - _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") - - _patch_schema_init() - _patch_views() - - -def _patch_schema_init() -> None: - old_schema_init = Schema.__init__ - - @functools.wraps(old_schema_init) - def _sentry_patched_schema_init( - self: "Schema", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) - if integration is None: - return old_schema_init(self, *args, **kwargs) - - extensions = kwargs.get("extensions") or [] - - should_use_async_extension: "Optional[bool]" = None - if integration.async_execution is not None: - should_use_async_extension = integration.async_execution - else: - # try to figure it out ourselves - should_use_async_extension = _guess_if_using_async(extensions) - - if should_use_async_extension is None: - warnings.warn( - "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", - stacklevel=2, - ) - should_use_async_extension = False - - # remove the built in strawberry sentry extension, if present - extensions = [ - extension - for extension in extensions - if extension - not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) - ] - - # add our extension - extensions = [ - SentryAsyncExtension if should_use_async_extension else SentrySyncExtension - ] + extensions - - kwargs["extensions"] = extensions - - return old_schema_init(self, *args, **kwargs) - - Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] - - -class SentryAsyncExtension(SchemaExtension): - def __init__( - self: "Any", - *, - execution_context: "Optional[ExecutionContext]" = None, - ) -> None: - if execution_context: - self.execution_context = execution_context - - @cached_property - def _resource_name(self) -> str: - query_hash = self.hash_query(self.execution_context.query) # type: ignore - - if self.execution_context.operation_name: - return "{}:{}".format(self.execution_context.operation_name, query_hash) - - return query_hash - - def hash_query(self, query: str) -> str: - return hashlib.md5(query.encode("utf-8")).hexdigest() - - def on_operation(self) -> "Generator[None, None, None]": - operation_name = self.execution_context.operation_name - - operation_type = "query" - op = OP.GRAPHQL_QUERY - - if self.execution_context.query is None: - self.execution_context.query = "" - - if self.execution_context.query.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif self.execution_context.query.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - description = operation_type - if operation_name: - description += " {}".format(operation_name) - - sentry_sdk.add_breadcrumb( - category="graphql.operation", - data={ - "operation_name": operation_name, - "operation_type": operation_type, - }, - ) - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_request_event_processor(self.execution_context) - scope.add_event_processor(event_processor) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - additional_attributes: "dict[str, Any]" = {} - - if should_send_default_pii(): - additional_attributes["graphql.document"] = self.execution_context.query - - if operation_name: - additional_attributes["graphql.operation.name"] = operation_name - - graphql_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": op, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - graphql_span = sentry_sdk.start_span( - op=op, - name=description, - origin=StrawberryIntegration.origin, - ) - graphql_span.__enter__() - - if type(graphql_span) is Span: - if should_send_default_pii(): - graphql_span.set_data("graphql.document", self.execution_context.query) - - graphql_span.set_data("graphql.operation.type", operation_type) - graphql_span.set_data("graphql.operation.name", operation_name) - # This attribute is being removed in streamed spans - graphql_span.set_data("graphql.resource_name", self._resource_name) - - yield - - if type(graphql_span) is StreamedSpan: - if self.execution_context.operation_name: - segment = graphql_span._segment - segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) - segment.set_attribute("sentry.op", op) - segment.name = self.execution_context.operation_name - elif isinstance(graphql_span, Span): - transaction = graphql_span.containing_transaction - if transaction and self.execution_context.operation_name: - transaction.name = self.execution_context.operation_name - transaction.source = TransactionSource.COMPONENT - transaction.op = op - - graphql_span.__exit__(None, None, None) - - def on_validate(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - validation_span = sentry_sdk.traces.start_span( - name="validation", - attributes={ - "sentry.op": OP.GRAPHQL_VALIDATE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - validation_span = sentry_sdk.start_span( - op=OP.GRAPHQL_VALIDATE, - name="validation", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during validation, we still need to close the span - try: - yield - finally: - if isinstance(validation_span, StreamedSpan): - validation_span.end() - else: - validation_span.finish() - - def on_parse(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - parsing_span = sentry_sdk.traces.start_span( - name="parsing", - attributes={ - "sentry.op": OP.GRAPHQL_PARSE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - parsing_span = sentry_sdk.start_span( - op=OP.GRAPHQL_PARSE, - name="parsing", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during parsing, we still need to close the span - try: - yield - finally: - if isinstance(parsing_span, StreamedSpan): - parsing_span.end() - else: - parsing_span.finish() - - def should_skip_tracing( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - info: "GraphQLResolveInfo", - ) -> bool: - return strawberry_should_skip_tracing(_next, info) - - async def _resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result - - async def resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return await self._resolve(_next, root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return await self._resolve(_next, root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return await self._resolve(_next, root, info, *args, **kwargs) - - -class SentrySyncExtension(SentryAsyncExtension): - def resolve( - self, - _next: "Callable[[Any, Any, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return _next(root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return _next(root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return _next(root, info, *args, **kwargs) - - -def _patch_views() -> None: - old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors - old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors - - def _sentry_patched_async_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_async_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - def _sentry_patched_sync_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_sync_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - @ensure_integration_enabled(StrawberryIntegration) - def _sentry_patched_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - if not errors: - return - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_response_event_processor(response_data) - scope.add_event_processor(event_processor) - - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": StrawberryIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_async_view_handle_errors - ) - sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_sync_view_handle_errors - ) - - -def _make_request_event_processor( - execution_context: "ExecutionContext", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - request_data = event.setdefault("request", {}) - request_data["api_target"] = "graphql" - - if not request_data.get("data"): - data: "dict[str, Any]" = {"query": execution_context.query} - if execution_context.variables: - data["variables"] = execution_context.variables - if execution_context.operation_name: - data["operationName"] = execution_context.operation_name - - request_data["data"] = data - - else: - try: - del event["request"]["data"] - except (KeyError, TypeError): - pass - - return event - - return inner - - -def _make_response_event_processor( - response_data: "GraphQLHTTPResponse", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - contexts = event.setdefault("contexts", {}) - contexts["response"] = {"data": response_data} - - return event - - return inner - - -def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": - if StrawberrySentryAsyncExtension in extensions: - return True - elif StrawberrySentrySyncExtension in extensions: - return False - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py deleted file mode 100644 index 4927c0e885..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/sys_exit.py +++ /dev/null @@ -1,65 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import NoReturn, Union - - -class SysExitIntegration(Integration): - """Captures sys.exit calls and sends them as events to Sentry. - - By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit - exceptions generated by sys.exit calls and send them to Sentry. - - This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and - non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. - Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. - """ - - identifier = "sys_exit" - - def __init__(self, *, capture_successful_exits: bool = False) -> None: - self._capture_successful_exits = capture_successful_exits - - @staticmethod - def setup_once() -> None: - SysExitIntegration._patch_sys_exit() - - @staticmethod - def _patch_sys_exit() -> None: - old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit - - @functools.wraps(old_exit) - def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": - # @ensure_integration_enabled ensures that this is non-None - integration = sentry_sdk.get_client().get_integration(SysExitIntegration) - if integration is None: - old_exit(__status) - - try: - old_exit(__status) - except SystemExit as e: - with capture_internal_exceptions(): - if integration._capture_successful_exits or __status not in ( - 0, - None, - ): - _capture_exception(e) - raise e - - sys.exit = sentry_patched_exit - - -def _capture_exception(exc: "SystemExit") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": SysExitIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py deleted file mode 100644 index f3ba046332..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/threading.py +++ /dev/null @@ -1,196 +0,0 @@ -import sys -import warnings -from concurrent.futures import Future, ThreadPoolExecutor -from functools import wraps -from threading import Thread, current_thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import use_isolation_scope, use_scope -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T", bound=Any) - - -class ThreadingIntegration(Integration): - identifier = "threading" - - def __init__( - self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True - ) -> None: - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - - self.propagate_scope = propagate_scope - - if propagate_hub is not None: - self.propagate_scope = propagate_hub - - @staticmethod - def setup_once() -> None: - old_start = Thread.start - - try: - from django import VERSION as django_version # noqa: N811 - except ImportError: - django_version = None - - try: - import channels # type: ignore[import-untyped] - - channels_version = channels.__version__ - except (ImportError, AttributeError): - channels_version = None - - is_async_emulated_with_threads = ( - sys.version_info < (3, 9) - and channels_version is not None - and channels_version < "4.0.0" - and django_version is not None - and django_version >= (3, 0) - and django_version < (4, 0) - ) - - @wraps(old_start) - def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return old_start(self, *a, **kw) - - if integration.propagate_scope: - if is_async_emulated_with_threads: - warnings.warn( - "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " - "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " - "Please either upgrade to Django channels 4.0+, use Django's async features " - "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", - stacklevel=2, - ) - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - - else: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - # Patching instance methods in `start()` creates a reference cycle if - # done in a naive way. See - # https://github.com/getsentry/sentry-python/pull/434 - # - # In threading module, using current_thread API will access current thread instance - # without holding it to avoid a reference cycle in an easier way. - with capture_internal_exceptions(): - new_run = _wrap_run( - isolation_scope, - current_scope, - getattr(self.run, "__func__", self.run), - ) - self.run = new_run # type: ignore - - return old_start(self, *a, **kw) - - Thread.start = sentry_start # type: ignore - ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore - ThreadPoolExecutor.submit, is_async_emulated_with_threads - ) - - -def _wrap_run( - isolation_scope_to_use: "Optional[sentry_sdk.Scope]", - current_scope_to_use: "Optional[sentry_sdk.Scope]", - old_run_func: "F", -) -> "F": - @wraps(old_run_func) - def run(*a: "Any", **kw: "Any") -> "Any": - def _run_old_run_func() -> "Any": - try: - self = current_thread() - return old_run_func(self, *a[1:], **kw) - except Exception: - reraise(*_capture_exception()) - - if isolation_scope_to_use is not None and current_scope_to_use is not None: - with use_isolation_scope(isolation_scope_to_use): - with use_scope(current_scope_to_use): - return _run_old_run_func() - else: - return _run_old_run_func() - - return run # type: ignore - - -def _wrap_threadpool_executor_submit( - func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool -) -> "Callable[..., Future[T]]": - """ - Wrap submit call to propagate scopes on task submission. - """ - - @wraps(func) - def sentry_submit( - self: "ThreadPoolExecutor", - fn: "Callable[..., T]", - *args: "Any", - **kwargs: "Any", - ) -> "Future[T]": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return func(self, fn, *args, **kwargs) - - if integration.propagate_scope and is_async_emulated_with_threads: - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - elif integration.propagate_scope: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": - if isolation_scope is not None and current_scope is not None: - with use_isolation_scope(isolation_scope): - with use_scope(current_scope): - return fn(*args, **kwargs) - - return fn(*args, **kwargs) - - return func(self, wrapped_fn, *args, **kwargs) - - return sentry_submit - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - if client.get_integration(ThreadingIntegration) is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "threading", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py deleted file mode 100644 index 859b0d0870..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/tornado.py +++ /dev/null @@ -1,320 +0,0 @@ -import contextlib -import weakref -from inspect import iscoroutinefunction - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - RequestExtractor, - _filter_headers, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from tornado import version_info as TORNADO_VERSION - from tornado.gen import coroutine - from tornado.web import HTTPError, RequestHandler -except ImportError: - raise DidNotEnable("Tornado not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.tracing import Span - - -class TornadoIntegration(Integration): - identifier = "tornado" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - - ignore_logger("tornado.access") - - old_execute = RequestHandler._execute - - awaitable = iscoroutinefunction(old_execute) - - if awaitable: - # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) - # In that case our method should be a coroutine function too - async def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - return await old_execute(self, *args, **kwargs) - - else: - - @coroutine # type: ignore - def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - result = yield from old_execute(self, *args, **kwargs) - return result - - RequestHandler._execute = sentry_execute_request_handler - - old_log_exception = RequestHandler.log_exception - - def sentry_log_exception( - self: "Any", - ty: type, - value: BaseException, - tb: "Any", - *args: "Any", - **kwargs: "Any", - ) -> "Optional[Any]": - _capture_exception(ty, value, tb) - return old_log_exception(self, ty, value, tb, *args, **kwargs) - - RequestHandler.log_exception = sentry_log_exception - - -_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" - - -@contextlib.contextmanager -def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": - integration = sentry_sdk.get_client().get_integration(TornadoIntegration) - - if integration is None: - yield - return - - weak_handler = weakref.ref(self) - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - headers = self.request.headers - - scope.clear_breadcrumbs() - processor = _make_event_processor(weak_handler) - scope.add_event_processor(processor) - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(dict(headers)) - scope.set_custom_sampling_context({"tornado_request": self.request}) - - if should_send_default_pii() and self.request.remote_ip: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_ROOT_SPAN_NAME, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": TornadoIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # Like with all other integrations, this is our - # fallback transaction in case there is no route. - # sentry_urldispatcher_resolve is responsible for - # setting a transaction name later. - name=_DEFAULT_ROOT_SPAN_NAME, - source=TransactionSource.ROUTE, - origin=TornadoIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"tornado_request": self.request}, - ) - - with span_ctx as span: - try: - yield - finally: - if type(span) is StreamedSpan: - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - self.request - ).items(): - span.set_attribute(attr, value) - - with capture_internal_exceptions(): - method = getattr(self, self.request.method.lower(), None) - if method is not None: - span_name = transaction_from_function(method) - if span_name: - span.name = span_name - span.set_attribute( - "sentry.span.source", - SegmentSource.COMPONENT, - ) - - with capture_internal_exceptions(): - status_int = self.get_status() - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) - span.status = "error" if status_int >= 400 else "ok" - - -def _get_request_attributes(request: "Any") -> "Dict[str, Any]": - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.full_url() - attributes["url.path"] = request.path - - if request.query: - attributes[SPANDATA.URL_QUERY] = request.query - - if request.protocol: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol - - if should_send_default_pii() and request.remote_ip: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip - - with capture_internal_exceptions(): - raw_data = _get_tornado_request_data(request) - body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data - if body_data is not None: - attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data - - return attributes - - -def _get_tornado_request_data( - request: "Any", -) -> "Union[Optional[str], AnnotatedValue]": - body = request.body - if not body: - return None - - if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): - return AnnotatedValue.substituted_because_over_size_limit() - - return body.decode("utf-8", "replace") - - -@ensure_integration_enabled(TornadoIntegration) -def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: - if isinstance(value, HTTPError): - return - - event, hint = event_from_exception( - (ty, value, tb), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "tornado", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - weak_handler: "Callable[[], RequestHandler]", -) -> "EventProcessor": - def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - handler = weak_handler() - if handler is None: - return event - - request = handler.request - - with capture_internal_exceptions(): - method = getattr(handler, handler.request.method.lower()) - event["transaction"] = transaction_from_function(method) or "" - event["transaction_info"] = {"source": TransactionSource.COMPONENT} - - with capture_internal_exceptions(): - extractor = TornadoRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - - request_info["url"] = "%s://%s%s" % ( - request.protocol, - request.host, - request.path, - ) - - request_info["query_string"] = request.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_ip} - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - try: - current_user = handler.current_user - except Exception: - current_user = None - - if current_user: - event.setdefault("user", {}).setdefault("is_authenticated", True) - - return event - - return tornado_processor - - -class TornadoRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return {k: v.value for k, v in self.request.cookies.items()} - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "Dict[str, Any]": - return { - k: [v.decode("latin1", "replace") for v in vs] - for k, vs in self.request.body_arguments.items() - } - - def is_json(self) -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - def files(self) -> "Dict[str, Any]": - return {k: v[0] for k, v in self.request.files.items() if v} - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py deleted file mode 100644 index 0449a8f10c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/trytond.py +++ /dev/null @@ -1,52 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.utils import ensure_integration_enabled, event_from_exception - -try: - from trytond.exceptions import TrytonException # type: ignore - from trytond.wsgi import app # type: ignore -except ImportError: - raise DidNotEnable("Trytond is not installed.") - -# TODO: trytond-worker, trytond-cron and trytond-admin intergations - - -class TrytondWSGIIntegration(Integration): - identifier = "trytond_wsgi" - origin = f"auto.http.{identifier}" - - def __init__(self) -> None: - pass - - @staticmethod - def setup_once() -> None: - app.wsgi_app = SentryWsgiMiddleware( - app.wsgi_app, - span_origin=TrytondWSGIIntegration.origin, - ) - - @ensure_integration_enabled(TrytondWSGIIntegration) - def error_handler(e: Exception) -> None: - if isinstance(e, TrytonException): - return - else: - client = sentry_sdk.get_client() - event, hint = event_from_exception( - e, - client_options=client.options, - mechanism={"type": "trytond", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - # Expected error handlers signature was changed - # when the error_handler decorator was introduced - # in Tryton-5.4 - if hasattr(app, "error_handler"): - - @app.error_handler - def _(app, request, e): # type: ignore - error_handler(e) - - else: - app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py deleted file mode 100644 index 497f0539ec..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/typer.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - -try: - import typer - from typer.main import except_hook -except ImportError: - raise DidNotEnable("Typer not installed") - - -class TyperIntegration(Integration): - identifier = "typer" - - @staticmethod - def setup_once() -> None: - typer.main.except_hook = _make_excepthook(except_hook) # type: ignore - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(TyperIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "typer", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py deleted file mode 100644 index 0316f6b88a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unleash.py +++ /dev/null @@ -1,33 +0,0 @@ -from functools import wraps -from typing import Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from UnleashClient import UnleashClient -except ImportError: - raise DidNotEnable("UnleashClient is not installed") - - -class UnleashIntegration(Integration): - identifier = "unleash" - - @staticmethod - def setup_once() -> None: - # Wrap and patch evaluation methods (class methods) - old_is_enabled = UnleashClient.is_enabled - - @wraps(old_is_enabled) - def sentry_is_enabled( - self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_is_enabled(self, feature, *args, **kwargs) - - # We have no way of knowing what type of unleash feature this is, so we have to treat - # it as a boolean / toggle feature. - add_feature_flag(feature, enabled) - - return enabled - - UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py deleted file mode 100644 index 2c7280a1f2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/unraisablehook.py +++ /dev/null @@ -1,50 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Callable - - -class UnraisablehookIntegration(Integration): - identifier = "unraisablehook" - - @staticmethod - def setup_once() -> None: - sys.unraisablehook = _make_unraisable(sys.unraisablehook) - - -def _make_unraisable( - old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", -) -> "Callable[[sys.UnraisableHookArgs], Any]": - def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: - integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_unraisablehook(unraisable) - - if unraisable.exc_value and unraisable.exc_traceback: - with capture_internal_exceptions(): - event, hint = event_from_exception( - ( - unraisable.exc_type, - unraisable.exc_value, - unraisable.exc_traceback, - ), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "unraisablehook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_unraisablehook(unraisable) - - return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py deleted file mode 100644 index e776ed915a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/integrations/wsgi.py +++ /dev/null @@ -1,427 +0,0 @@ -import sys -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._werkzeug import _get_headers, get_host -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - _filter_headers, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Iterator, - Optional, - Protocol, - Tuple, - TypeVar, - Union, - ) - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - WsgiResponseIter = TypeVar("WsgiResponseIter") - WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") - WsgiExcInfo = TypeVar("WsgiExcInfo") - - class StartResponse(Protocol): - def __call__( - self, - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, - ) -> "WsgiResponseIter": # type: ignore - pass - - -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") -_DEFAULT_TRANSACTION_NAME = "generic WSGI request" - - -def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: - return s.encode("latin1").decode(charset, errors) - - -def get_request_url( - environ: "Dict[str, str]", use_x_forwarded_for: bool = False -) -> str: - """Return the absolute URL without query string for the given WSGI - environment.""" - script_name = environ.get("SCRIPT_NAME", "").rstrip("/") - path_info = environ.get("PATH_INFO", "").lstrip("/") - path = f"{script_name}/{path_info}" - - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - return "%s://%s/%s" % ( - scheme, - get_host(environ, use_x_forwarded_for), - wsgi_decoding_dance(path).lstrip("/"), - ) - - -class SentryWsgiMiddleware: - __slots__ = ( - "app", - "use_x_forwarded_for", - "span_origin", - "http_methods_to_capture", - ) - - def __init__( - self, - app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", - use_x_forwarded_for: bool = False, - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - self.app = app - self.use_x_forwarded_for = use_x_forwarded_for - self.span_origin = span_origin - self.http_methods_to_capture = http_methods_to_capture - - def __call__( - self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "Any": - if _wsgi_middleware_applied.get(False): - return self.app(environ, start_response) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _wsgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope._name = "wsgi" - scope.add_event_processor( - _make_wsgi_event_processor( - environ, self.use_x_forwarded_for - ) - ) - - method = environ.get("REQUEST_METHOD", "").upper() - - span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None - if method in self.http_methods_to_capture: - if span_streaming: - sentry_sdk.traces.continue_trace( - dict(_get_headers(environ)) - ) - Scope.set_custom_sampling_context({"wsgi_environ": environ}) - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, client_ip - ) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_TRANSACTION_NAME, - attributes={ - "sentry.span.source": SegmentSource.ROUTE, - "sentry.origin": self.span_origin, - "sentry.op": OP.HTTP_SERVER, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - environ, - op=OP.HTTP_SERVER, - name=_DEFAULT_TRANSACTION_NAME, - source=TransactionSource.ROUTE, - origin=self.span_origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"wsgi_environ": environ}, - ) - - span_ctx = span_ctx or nullcontext() - - with span_ctx as span: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - environ, self.use_x_forwarded_for - ).items(): - span.set_attribute(attr, value) - - try: - response = self.app( - environ, - partial(_sentry_start_response, start_response, span), - ) - except BaseException: - reraise(*_capture_exception()) - finally: - _wsgi_middleware_applied.set(False) - - # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses - # is determined by a pointer equality check on the response object - # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). - # - # If we were to return a _ScopedResponse, this would cause the check to always fail - # since it's checking the files are exactly the same. - # - # To avoid this and ensure that the offloading mechanism works as expected when it's - # enabled, we check if the response is a file-like object (determined by the presence - # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, - # it would've been used in handling the file in the response). - # - # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, - # so we want to make sure we don't interfere with those either. - # - # If all conditions are met, we return the original response object directly, - # allowing uWSGI to handle it as intended. - if ( - environ.get("wsgi.file_wrapper") - and getattr(response, "fileno", None) is not None - ): - return response - - return _ScopedResponse(scope, response) - - -def _sentry_start_response( - old_start_response: "StartResponse", - span: "Optional[Union[Span, StreamedSpan]]", - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, -) -> "WsgiResponseIter": # type: ignore[type-var] - with capture_internal_exceptions(): - status_int = int(status.split(" ", 1)[0]) - if span is not None: - if isinstance(span, StreamedSpan): - span.status = "error" if status_int >= 400 else "ok" - span.set_attribute("http.response.status_code", status_int) - else: - span.set_http_status(status_int) - - if exc_info is None: - # The Django Rest Framework WSGI test client, and likely other - # (incorrect) implementations, cannot deal with the exc_info argument - # if one is present. Avoid providing a third argument if not necessary. - return old_start_response(status, response_headers) - else: - return old_start_response(status, response_headers, exc_info) - - -def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns our explicitly included environment variables we want to - capture (server name, port and remote addr if pii is enabled). - """ - keys = ["SERVER_NAME", "SERVER_PORT"] - if should_send_default_pii(): - # make debugging of proxy setup easier. Proxy headers are - # in headers. - keys += ["REMOTE_ADDR"] - - for key in keys: - if key in environ: - yield key, environ[key] - - -def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": - """ - Infer the user IP address from various headers. This cannot be used in - security sensitive situations since the value may be forged from a client, - but it's good enough for the event payload. - """ - try: - return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return environ["HTTP_X_REAL_IP"] - except KeyError: - pass - - return environ.get("REMOTE_ADDR") - - -def _capture_exception() -> "ExcInfo": - """ - Captures the current exception and sends it to Sentry. - Returns the ExcInfo tuple to it can be reraised afterwards. - """ - exc_info = sys.exc_info() - e = exc_info[1] - - # SystemExit(0) is the only uncaught exception that is expected behavior - should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) - if not should_skip_capture: - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "wsgi", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class _ScopedResponse: - """ - Users a separate scope for each response chunk. - - This will make WSGI apps more tolerant against: - - WSGI servers streaming responses from a different thread/from - different threads than the one that called start_response - - close() not being called - - WSGI servers streaming responses interleaved from the same thread - """ - - __slots__ = ("_response", "_scope") - - def __init__( - self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" - ) -> None: - self._scope = scope - self._response = response - - def __iter__(self) -> "Iterator[bytes]": - iterator = iter(self._response) - - while True: - with use_isolation_scope(self._scope): - try: - chunk = next(iterator) - except StopIteration: - break - except BaseException: - reraise(*_capture_exception()) - - yield chunk - - def close(self) -> None: - with use_isolation_scope(self._scope): - try: - self._response.close() # type: ignore - except AttributeError: - pass - except BaseException: - reraise(*_capture_exception()) - - -def _make_wsgi_event_processor( - environ: "Dict[str, str]", use_x_forwarded_for: bool -) -> "EventProcessor": - # It's a bit unfortunate that we have to extract and parse the request data - # from the environ so eagerly, but there are a few good reasons for this. - # - # We might be in a situation where the scope never gets torn down - # properly. In that case we will have an unnecessary strong reference to - # all objects in the environ (some of which may take a lot of memory) when - # we're really just interested in a few of them. - # - # Keeping the environment around for longer than the request lifecycle is - # also not necessarily something uWSGI can deal with: - # https://github.com/unbit/uwsgi/issues/1950 - - client_ip = get_client_ip(environ) - request_url = get_request_url(environ, use_x_forwarded_for) - query_string = environ.get("QUERY_STRING") - method = environ.get("REQUEST_METHOD") - env = dict(_get_environ(environ)) - headers = _filter_headers(dict(_get_headers(environ))) - - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - # if the code below fails halfway through we at least have some data - request_info = event.setdefault("request", {}) - - if should_send_default_pii(): - user_info = event.setdefault("user", {}) - if client_ip: - user_info.setdefault("ip_address", client_ip) - - request_info["url"] = request_url - request_info["query_string"] = query_string - request_info["method"] = method - request_info["env"] = env - request_info["headers"] = headers - - return event - - return event_processor - - -def _get_request_attributes( - environ: "Dict[str, str]", - use_x_forwarded_for: bool = False, -) -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from the WSGI environ. - """ - attributes: "dict[str, Any]" = {} - - method = environ.get("REQUEST_METHOD") - if method: - attributes["http.request.method"] = method.upper() - - headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - url_scheme = environ.get("wsgi.url_scheme") - if url_scheme: - attributes["network.protocol.name"] = url_scheme - - server_name = environ.get("SERVER_NAME") - if server_name: - attributes["server.address"] = server_name - - server_port = environ.get("SERVER_PORT") - if server_port: - try: - attributes["server.port"] = int(server_port) - except ValueError: - pass - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - attributes["client.address"] = client_ip - - query_string = environ.get("QUERY_STRING") - if query_string: - attributes["http.query"] = query_string - - path = environ.get("PATH_INFO", "") - if path: - attributes["url.path"] = path - - attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py deleted file mode 100644 index d7f4425dfd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/logger.py +++ /dev/null @@ -1,88 +0,0 @@ -# NOTE: this is the logger sentry exposes to users, not some generic logger. -import functools -import time -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk.utils import capture_internal_exceptions, format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes - - -OTEL_RANGES = [ - # ((severity level range), severity text) - # https://opentelemetry.io/docs/specs/otel/logs/data-model - ((1, 4), "trace"), - ((5, 8), "debug"), - ((9, 12), "info"), - ((13, 16), "warn"), - ((17, 20), "error"), - ((21, 24), "fatal"), -] - - -class _dict_default_key(dict): # type: ignore[type-arg] - """dict that returns the key if missing.""" - - def __missing__(self, key: str) -> str: - return "{" + key + "}" - - -def _capture_log( - severity_text: str, severity_number: int, template: str, **kwargs: "Any" -) -> None: - body = template - - attributes: "Attributes" = {} - - if "attributes" in kwargs: - provided_attributes = kwargs.pop("attributes") or {} - for attribute, value in provided_attributes.items(): - attributes[attribute] = format_attribute(value) - - for k, v in kwargs.items(): - attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) - - if kwargs: - # only attach template if there are parameters - attributes["sentry.message.template"] = format_attribute(template) - - with capture_internal_exceptions(): - body = template.format_map(_dict_default_key(kwargs)) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": severity_text, - "severity_number": severity_number, - "attributes": attributes, - "body": body, - "time_unix_nano": time.time_ns(), - "trace_id": None, - "span_id": None, - } - ) - - -trace = functools.partial(_capture_log, "trace", 1) -debug = functools.partial(_capture_log, "debug", 5) -info = functools.partial(_capture_log, "info", 9) -warning = functools.partial(_capture_log, "warn", 13) -error = functools.partial(_capture_log, "error", 17) -fatal = functools.partial(_capture_log, "fatal", 21) - - -def _otel_severity_text(otel_severity_number: int) -> str: - for (lower, upper), severity in OTEL_RANGES: - if lower <= otel_severity_number <= upper: - return severity - - return "default" - - -def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": - for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): - if level >= py_level: - return otel_severity_number, _otel_severity_text(otel_severity_number) - - return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py deleted file mode 100644 index 27b7468c0d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/metrics.py +++ /dev/null @@ -1,62 +0,0 @@ -import time -from typing import TYPE_CHECKING, Any, Optional - -import sentry_sdk -from sentry_sdk.utils import format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes, Metric, MetricType - - -def _capture_metric( - name: str, - metric_type: "MetricType", - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[Attributes]" = None, -) -> None: - attrs: "Attributes" = {} - - if attributes: - for k, v in attributes.items(): - attrs[k] = format_attribute(v) - - metric: "Metric" = { - "timestamp": time.time(), - "trace_id": None, - "span_id": None, - "name": name, - "type": metric_type, - "value": float(value), - "unit": unit, - "attributes": attrs, - } - - sentry_sdk.get_current_scope()._capture_metric(metric) - - -def count( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "counter", value, unit, attributes) - - -def gauge( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "gauge", value, unit, attributes) - - -def distribution( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py deleted file mode 100644 index d2ba298c35..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/monitor.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import time -import weakref -from threading import Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -MAX_DOWNSAMPLE_FACTOR = 10 - - -class Monitor: - """ - Performs health checks in a separate thread once every interval seconds - and updates the internal state. Other parts of the SDK only read this state - and act accordingly. - """ - - name = "sentry.monitor" - - _thread: "Optional[Thread]" - _thread_for_pid: "Optional[int]" - - def __init__( - self, transport: "sentry_sdk.transport.Transport", interval: float = 10 - ) -> None: - self.transport: "sentry_sdk.transport.Transport" = transport - self.interval: float = interval - - self._healthy = True - self._downsample_factor: int = 0 - self._running = True - self._reset_thread_state() - - # See https://github.com/getsentry/sentry-python/issues/6148. - # If os.fork() runs while another thread holds self._thread_lock, - # the child inherits the lock locked but the holding thread does - # not exist in the child, so the lock can never be released and - # _ensure_running deadlocks forever. Reinitialise the lock and - # cached thread/pid in the child so it starts clean regardless - # of inherited state. We bind via a WeakMethod so the - # permanently-registered fork handler does not pin this Monitor - # (and its Transport): register_at_fork has no unregister API. - # POSIX-only; Windows uses spawn. - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._thread = None - self._thread_lock = Lock() - self._thread_for_pid = None - - def _ensure_running(self) -> None: - """ - Check that the monitor has an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - while self._running: - time.sleep(self.interval) - if self._running: - self.run() - - thread = Thread(name=self.name, target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def run(self) -> None: - self.check_health() - self.set_downsample_factor() - - def set_downsample_factor(self) -> None: - if self._healthy: - if self._downsample_factor > 0: - logger.debug( - "[Monitor] health check positive, reverting to normal sampling" - ) - self._downsample_factor = 0 - else: - if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: - self._downsample_factor += 1 - logger.debug( - "[Monitor] health check negative, downsampling with a factor of %d", - self._downsample_factor, - ) - - def check_health(self) -> None: - """ - Perform the actual health checks, - currently only checks if the transport is rate-limited. - TODO: augment in the future with more checks. - """ - self._healthy = self.transport.is_healthy() - - def is_healthy(self) -> bool: - self._ensure_running() - return self._healthy - - @property - def downsample_factor(self) -> int: - self._ensure_running() - return self._downsample_factor - - def kill(self) -> None: - self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py deleted file mode 100644 index d562405295..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -from sentry_sdk.profiler.continuous_profiler import ( - start_profile_session, - start_profiler, - stop_profile_session, - stop_profiler, -) -from sentry_sdk.profiler.transaction_profiler import ( - MAX_PROFILE_DURATION_NS, - PROFILE_MINIMUM_SAMPLES, - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - has_profiling_enabled, - setup_profiler, - teardown_profiler, -) -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - MAX_STACK_DEPTH, - extract_frame, - extract_stack, - frame_id, - get_frame_name, -) - -__all__ = [ - "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` - "start_profiler", - "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` - "stop_profiler", - # DEPRECATED: The following was re-exported for backwards compatibility. It - # will be removed from sentry_sdk.profiler in a future release. - "MAX_PROFILE_DURATION_NS", - "PROFILE_MINIMUM_SAMPLES", - "Profile", - "Scheduler", - "ThreadScheduler", - "GeventScheduler", - "has_profiling_enabled", - "setup_profiler", - "teardown_profiler", - "DEFAULT_SAMPLING_FREQUENCY", - "MAX_STACK_DEPTH", - "get_frame_name", - "extract_frame", - "extract_stack", - "frame_id", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py deleted file mode 100644 index ed525f52bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/continuous_profiler.py +++ /dev/null @@ -1,703 +0,0 @@ -import atexit -import os -import random -import sys -import threading -import time -import uuid -import warnings -from collections import deque -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.consts import VERSION -from sentry_sdk.envelope import Envelope -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - is_gevent, - logger, - now, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union - - from typing_extensions import TypedDict - - from sentry_sdk._types import ContinuousProfilerMode, SDKInfo - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "timestamp": float, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - ThreadPool = None - - -_scheduler: "Optional[ContinuousScheduler]" = None - - -def setup_continuous_profiler( - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", -) -> bool: - global _scheduler - - already_initialized = _scheduler is not None - - if already_initialized: - logger.debug("[Profiling] Continuous Profiler is already setup") - teardown_continuous_profiler() - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventContinuousScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventContinuousScheduler.mode - else: - default_profiler_mode = ThreadContinuousScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - # TODO: deprecate this and just use the existing `profiler_mode` - experiments = options.get("_experiments", {}) - - profiler_mode = ( - experiments.get("continuous_profiling_mode") or default_profiler_mode - ) - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if profiler_mode == ThreadContinuousScheduler.mode: - _scheduler = ThreadContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - elif profiler_mode == GeventContinuousScheduler.mode: - _scheduler = GeventContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - else: - raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up continuous profiler in {mode} mode".format( - mode=_scheduler.mode - ) - ) - - if not already_initialized: - atexit.register(teardown_continuous_profiler) - - return True - - -def is_profile_session_sampled() -> bool: - if _scheduler is None: - return False - return _scheduler.sampled - - -def try_autostart_continuous_profiler() -> None: - # TODO: deprecate this as it'll be replaced by the auto lifecycle option - - if _scheduler is None: - return - - if not _scheduler.is_auto_start_enabled(): - return - - _scheduler.manual_start() - - -def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": - if _scheduler is None: - return None - - return _scheduler.auto_start() - - -def start_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_start() - - -def start_profile_session() -> None: - warnings.warn( - "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - start_profiler() - - -def stop_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_stop() - - -def stop_profile_session() -> None: - warnings.warn( - "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - stop_profiler() - - -def teardown_continuous_profiler() -> None: - stop_profiler() - - global _scheduler - _scheduler = None - - -def get_profiler_id() -> "Union[str, None]": - if _scheduler is None: - return None - return _scheduler.profiler_id - - -def determine_profile_session_sampling_decision( - sample_rate: "Union[float, None]", -) -> bool: - # `None` is treated as `0.0` - if not sample_rate: - return False - - return random.random() < float(sample_rate) - - -class ContinuousProfile: - active: bool = True - - def stop(self) -> None: - self.active = False - - -class ContinuousScheduler: - mode: "ContinuousProfilerMode" = "unknown" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.interval = 1.0 / frequency - self.options = options - self.sdk_info = sdk_info - self.capture_func = capture_func - - self.lifecycle = self.options.get("profile_lifecycle") - profile_session_sample_rate = self.options.get("profile_session_sample_rate") - self.sampled = determine_profile_session_sampling_decision( - profile_session_sample_rate - ) - - self.sampler = self.make_sampler() - self.buffer: "Optional[ProfileBuffer]" = None - self.pid: "Optional[int]" = None - - self.running = False - self.soft_shutdown = False - - self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) - self.active_profiles: "Set[ContinuousProfile]" = set() - - def is_auto_start_enabled(self) -> bool: - # Ensure that the scheduler only autostarts once per process. - # This is necessary because many web servers use forks to spawn - # additional processes. And the profiler is only spawned on the - # master process, then it often only profiles the main process - # and not the ones where the requests are being handled. - if self.pid == os.getpid(): - return False - - experiments = self.options.get("_experiments") - if not experiments: - return False - - return experiments.get("continuous_profiling_auto_start") - - def auto_start(self) -> "Union[ContinuousProfile, None]": - if not self.sampled: - return None - - if self.lifecycle != "trace": - return None - - logger.debug("[Profiling] Auto starting profiler") - - profile = ContinuousProfile() - - self.new_profiles.append(profile) - self.ensure_running() - - return profile - - def manual_start(self) -> None: - if not self.sampled: - return - - if self.lifecycle != "manual": - return - - self.ensure_running() - - def manual_stop(self) -> None: - if self.lifecycle != "manual": - return - - self.teardown() - - def ensure_running(self) -> None: - raise NotImplementedError - - def teardown(self) -> None: - raise NotImplementedError - - def pause(self) -> None: - raise NotImplementedError - - def reset_buffer(self) -> None: - self.buffer = ProfileBuffer( - self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func - ) - - @property - def profiler_id(self) -> "Union[str, None]": - if not self.running or self.buffer is None: - return None - return self.buffer.profiler_id - - def make_sampler(self) -> "Callable[..., bool]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - if self.lifecycle == "trace": - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - return True - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - inactive_profiles = [] - - for profile in self.active_profiles: - if not profile.active: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - else: - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - return _sample_stack - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.soft_shutdown = self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # the soft shutdown happens here to give it a chance - # for the profiler to be reused - if self.soft_shutdown: - self.running = False - - # make sure to explicitly exit the profiler here or there might - # be multiple profilers at once - break - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - buffer = self.buffer - if buffer is not None: - buffer.flush() - - -class ThreadContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ContinuousProfilerMode" = "thread" - name = "sentry.profiler.ThreadContinuousScheduler" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[threading.Thread]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -class GeventContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ContinuousProfilerMode" = "gevent" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[_ThreadPool]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -PROFILE_BUFFER_SECONDS = 60 - - -class ProfileBuffer: - def __init__( - self, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - buffer_size: int, - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.options = options - self.sdk_info = sdk_info - self.buffer_size = buffer_size - self.capture_func = capture_func - - self.profiler_id = uuid.uuid4().hex - self.chunk = ProfileChunk() - - # Make sure to use the same clock to compute a sample's monotonic timestamp - # to ensure the timestamps are correctly aligned. - self.start_monotonic_time = now() - - # Make sure the start timestamp is defined only once per profiler id. - # This prevents issues with clock drift within a single profiler session. - # - # Subtracting the start_monotonic_time here to find a fixed starting position - # for relative monotonic timestamps for each sample. - self.start_timestamp = ( - datetime.now(timezone.utc).timestamp() - self.start_monotonic_time - ) - - def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: - if self.should_flush(monotonic_time): - self.flush() - self.chunk = ProfileChunk() - self.start_monotonic_time = now() - - self.chunk.write(self.start_timestamp + monotonic_time, sample) - - def should_flush(self, monotonic_time: float) -> bool: - # If the delta between the new monotonic time and the start monotonic time - # exceeds the buffer size, it means we should flush the chunk - return monotonic_time - self.start_monotonic_time >= self.buffer_size - - def flush(self) -> None: - chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) - envelope = Envelope() - envelope.add_profile_chunk(chunk) - self.capture_func(envelope) - - -class ProfileChunk: - def __init__(self) -> None: - self.chunk_id = uuid.uuid4().hex - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - def write(self, ts: float, sample: "ExtractedSample") -> None: - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "timestamp": ts, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def to_json( - self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" - ) -> "Dict[str, Any]": - profile = { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - }, - } - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - payload = { - "chunk_id": self.chunk_id, - "client_sdk": { - "name": sdk_info["name"], - "version": VERSION, - }, - "platform": "python", - "profile": profile, - "profiler_id": profiler_id, - "version": "2", - } - - for key in "release", "environment", "dist": - if options[key] is not None: - payload[key] = str(options[key]).strip() - - return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py deleted file mode 100644 index fe65774c0b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/transaction_profiler.py +++ /dev/null @@ -1,802 +0,0 @@ -""" -This file is originally based on code from https://github.com/nylas/nylas-perftools, -which is published under the following license: - -The MIT License (MIT) - -Copyright (c) 2014 Nylas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import atexit -import os -import platform -import random -import sys -import threading -import time -import uuid -import warnings -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - capture_internal_exceptions, - get_current_thread_meta, - is_gevent, - is_valid_sample_rate, - logger, - nanosecond_time, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type - - from typing_extensions import TypedDict - - from sentry_sdk._types import Event, ProfilerMode, SamplingContext - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - ProcessedThreadMetadata, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "elapsed_since_start_ns": str, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - ProcessedProfile = TypedDict( - "ProcessedProfile", - { - "frames": List[ProcessedFrame], - "stacks": List[ProcessedStack], - "samples": List[ProcessedSample], - "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - - ThreadPool = None - - -_scheduler: "Optional[Scheduler]" = None - - -# The minimum number of unique samples that must exist in a profile to be -# considered valid. -PROFILE_MINIMUM_SAMPLES = 2 - - -def has_profiling_enabled(options: "Dict[str, Any]") -> bool: - profiles_sampler = options["profiles_sampler"] - if profiles_sampler is not None: - return True - - profiles_sample_rate = options["profiles_sample_rate"] - if profiles_sample_rate is not None and profiles_sample_rate > 0: - return True - - profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") - if profiles_sample_rate is not None: - logger.warning( - "_experiments['profiles_sample_rate'] is deprecated. " - "Please use the non-experimental profiles_sample_rate option " - "directly." - ) - if profiles_sample_rate > 0: - return True - - return False - - -def setup_profiler(options: "Dict[str, Any]") -> bool: - global _scheduler - - if _scheduler is not None: - logger.debug("[Profiling] Profiler is already setup") - return False - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventScheduler.mode - else: - default_profiler_mode = ThreadScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - profiler_mode = options.get("_experiments", {}).get("profiler_mode") - if profiler_mode is not None: - logger.warning( - "_experiments['profiler_mode'] is deprecated. Please use the " - "non-experimental profiler_mode option directly." - ) - profiler_mode = profiler_mode or default_profiler_mode - - if ( - profiler_mode == ThreadScheduler.mode - # for legacy reasons, we'll keep supporting sleep mode for this scheduler - or profiler_mode == "sleep" - ): - _scheduler = ThreadScheduler(frequency=frequency) - elif profiler_mode == GeventScheduler.mode: - _scheduler = GeventScheduler(frequency=frequency) - else: - raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) - ) - _scheduler.setup() - - atexit.register(teardown_profiler) - - return True - - -def teardown_profiler() -> None: - global _scheduler - - if _scheduler is not None: - _scheduler.teardown() - - _scheduler = None - - -MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds - - -class Profile: - def __init__( - self, - sampled: "Optional[bool]", - start_ns: int, - hub: "Optional[sentry_sdk.Hub]" = None, - scheduler: "Optional[Scheduler]" = None, - ) -> None: - self.scheduler = _scheduler if scheduler is None else scheduler - - self.event_id: str = uuid.uuid4().hex - - self.sampled: "Optional[bool]" = sampled - - # Various framework integrations are capable of overwriting the active thread id. - # If it is set to `None` at the end of the profile, we fall back to the default. - self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 - self.active_thread_id: "Optional[int]" = None - - try: - self.start_ns: int = start_ns - except AttributeError: - self.start_ns = 0 - - self.stop_ns: int = 0 - self.active: bool = False - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - self.unique_samples = 0 - - # Backwards compatibility with the old hub property - self._hub: "Optional[sentry_sdk.Hub]" = None - if hub is not None: - self._hub = hub - warnings.warn( - "The `hub` parameter is deprecated. Please do not use it.", - DeprecationWarning, - stacklevel=2, - ) - - def update_active_thread_id(self) -> None: - self.active_thread_id = get_current_thread_meta()[0] - logger.debug( - "[Profiling] updating active thread id to {tid}".format( - tid=self.active_thread_id - ) - ) - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the profile's sampling decision according to the following - precedence rules: - - 1. If the transaction to be profiled is not sampled, that decision - will be used, regardless of anything else. - - 2. Use `profiles_sample_rate` to decide. - """ - - # The corresponding transaction was not sampled, - # so don't generate a profile for it. - if not self.sampled: - logger.debug( - "[Profiling] Discarding profile because transaction is discarded." - ) - self.sampled = False - return - - # The profiler hasn't been properly initialized. - if self.scheduler is None: - logger.debug( - "[Profiling] Discarding profile because profiler was not started." - ) - self.sampled = False - return - - client = sentry_sdk.get_client() - if not client.is_active(): - self.sampled = False - return - - options = client.options - - if callable(options.get("profiles_sampler")): - sample_rate = options["profiles_sampler"](sampling_context) - elif options["profiles_sample_rate"] is not None: - sample_rate = options["profiles_sample_rate"] - else: - sample_rate = options["_experiments"].get("profiles_sample_rate") - - # The profiles_sample_rate option was not set, so profiling - # was never enabled. - if sample_rate is None: - logger.debug( - "[Profiling] Discarding profile because profiling was not enabled." - ) - self.sampled = False - return - - if not is_valid_sample_rate(sample_rate, source="Profiling"): - logger.warning( - "[Profiling] Discarding profile because of invalid sample rate." - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < float(sample_rate) - - if self.sampled: - logger.debug("[Profiling] Initializing profile") - else: - logger.debug( - "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( - sample_rate=float(sample_rate) - ) - ) - - def start(self) -> None: - if not self.sampled or self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Starting profile") - self.active = True - if not self.start_ns: - self.start_ns = nanosecond_time() - self.scheduler.start_profiling(self) - - def stop(self) -> None: - if not self.sampled or not self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Stopping profile") - self.active = False - self.stop_ns = nanosecond_time() - - def __enter__(self) -> "Profile": - scope = sentry_sdk.get_isolation_scope() - old_profile = scope.profile - scope.profile = self - - self._context_manager_state = (scope, old_profile) - - self.start() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - with capture_internal_exceptions(): - self.stop() - - scope, old_profile = self._context_manager_state - del self._context_manager_state - - scope.profile = old_profile - - def write(self, ts: int, sample: "ExtractedSample") -> None: - if not self.active: - return - - if ts < self.start_ns: - return - - offset = ts - self.start_ns - if offset > MAX_PROFILE_DURATION_NS: - self.stop() - return - - self.unique_samples += 1 - - elapsed_since_start_ns = str(offset) - - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "elapsed_since_start_ns": elapsed_since_start_ns, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def process(self) -> "ProcessedProfile": - # This collects the thread metadata at the end of a profile. Doing it - # this way means that any threads that terminate before the profile ends - # will not have any metadata associated with it. - thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - } - - return { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": thread_metadata, - } - - def to_json( - self, event_opt: "Event", options: "Dict[str, Any]" - ) -> "Dict[str, Any]": - profile = self.process() - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - return { - "environment": event_opt.get("environment"), - "event_id": self.event_id, - "platform": "python", - "profile": profile, - "release": event_opt.get("release", ""), - "timestamp": event_opt["start_timestamp"], - "version": "1", - "device": { - "architecture": platform.machine(), - }, - "os": { - "name": platform.system(), - "version": platform.release(), - }, - "runtime": { - "name": platform.python_implementation(), - "version": platform.python_version(), - }, - "transactions": [ - { - "id": event_opt["event_id"], - "name": event_opt["transaction"], - # we start the transaction before the profile and this is - # the transaction start time relative to the profile, so we - # hardcode it to 0 until we can start the profile before - "relative_start_ns": "0", - # use the duration of the profile instead of the transaction - # because we end the transaction after the profile - "relative_end_ns": str(self.stop_ns - self.start_ns), - "trace_id": event_opt["contexts"]["trace"]["trace_id"], - "active_thread_id": str( - self._default_active_thread_id - if self.active_thread_id is None - else self.active_thread_id - ), - } - ], - } - - def valid(self) -> bool: - client = sentry_sdk.get_client() - if not client.is_active(): - return False - - if not has_profiling_enabled(client.options): - return False - - if self.sampled is None or not self.sampled: - if client.transport: - client.transport.record_lost_event( - "sample_rate", data_category="profile" - ) - return False - - if self.unique_samples < PROFILE_MINIMUM_SAMPLES: - if client.transport: - client.transport.record_lost_event( - "insufficient_data", data_category="profile" - ) - logger.debug("[Profiling] Discarding profile because insufficient samples.") - return False - - return True - - @property - def hub(self) -> "Optional[sentry_sdk.Hub]": - warnings.warn( - "The `hub` attribute is deprecated. Please do not access it.", - DeprecationWarning, - stacklevel=2, - ) - return self._hub - - @hub.setter - def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: - warnings.warn( - "The `hub` attribute is deprecated. Please do not set it.", - DeprecationWarning, - stacklevel=2, - ) - self._hub = value - - -class Scheduler(ABC): - mode: "ProfilerMode" = "unknown" - - def __init__(self, frequency: int) -> None: - self.interval = 1.0 / frequency - - self.sampler = self.make_sampler() - - # cap the number of new profiles at any time so it does not grow infinitely - self.new_profiles: "Deque[Profile]" = deque(maxlen=128) - self.active_profiles: "Set[Profile]" = set() - - def __enter__(self) -> "Scheduler": - self.setup() - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self.teardown() - - @abstractmethod - def setup(self) -> None: - pass - - @abstractmethod - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - """ - Ensure the scheduler is running. By default, this method is a no-op. - The method should be overridden by any implementation for which it is - relevant. - """ - return None - - def start_profiling(self, profile: "Profile") -> None: - self.ensure_running() - self.new_profiles.append(profile) - - def make_sampler(self) -> "Callable[..., None]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - def _sample_stack(*args: "Any", **kwargs: "Any") -> None: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - # make sure to clear the cache if we're not profiling so we dont - # keep a reference to the last stack of frames around - return - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - now = nanosecond_time() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - - inactive_profiles = [] - - for profile in self.active_profiles: - if profile.active: - profile.write(now, sample) - else: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - return _sample_stack - - -class ThreadScheduler(Scheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ProfilerMode" = "thread" - name = "sentry.profiler.ThreadScheduler" - - def __init__(self, frequency: int) -> None: - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[threading.Thread]" = None - self.pid: "Optional[int]" = None - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - """ - Check that the profiler has an active thread to run in, and start one if - that's not the case. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self.running - will be False after running this function. - """ - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - -class GeventScheduler(Scheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ProfilerMode" = "gevent" - name = "sentry.profiler.GeventScheduler" - - def __init__(self, frequency: int) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[_ThreadPool]" = None - self.pid: "Optional[int]" = None - - # This intentionally uses the gevent patched threading.Lock. - # The lock will be required when first trying to start profiles - # as we need to spawn the profiler thread from the greenlets. - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py deleted file mode 100644 index e9f0fec07f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/profiler/utils.py +++ /dev/null @@ -1,186 +0,0 @@ -import os -from collections import deque -from typing import TYPE_CHECKING - -from sentry_sdk._compat import PY311 -from sentry_sdk.utils import filename_for_module - -if TYPE_CHECKING: - from types import FrameType - from typing import Deque, List, Optional, Sequence, Tuple - - from typing_extensions import TypedDict - - from sentry_sdk._lru_cache import LRUCache - - ThreadId = str - - ProcessedStack = List[int] - - ProcessedFrame = TypedDict( - "ProcessedFrame", - { - "abs_path": str, - "filename": Optional[str], - "function": str, - "lineno": int, - "module": Optional[str], - }, - ) - - ProcessedThreadMetadata = TypedDict( - "ProcessedThreadMetadata", - {"name": str}, - ) - - FrameId = Tuple[ - str, # abs_path - int, # lineno - str, # function - ] - FrameIds = Tuple[FrameId, ...] - - # The exact value of this id is not very meaningful. The purpose - # of this id is to give us a compact and unique identifier for a - # raw stack that can be used as a key to a dictionary so that it - # can be used during the sampled format generation. - StackId = Tuple[int, int] - - ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] - ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] - -# The default sampling frequency to use. This is set at 101 in order to -# mitigate the effects of lockstep sampling. -DEFAULT_SAMPLING_FREQUENCY = 101 - - -# We want to impose a stack depth limit so that samples aren't too large. -MAX_STACK_DEPTH = 128 - - -if PY311: - - def get_frame_name(frame: "FrameType") -> str: - return frame.f_code.co_qualname - -else: - - def get_frame_name(frame: "FrameType") -> str: - f_code = frame.f_code - co_varnames = f_code.co_varnames - - # co_name only contains the frame name. If the frame was a method, - # the class name will NOT be included. - name = f_code.co_name - - # if it was a method, we can get the class name by inspecting - # the f_locals for the `self` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `self` if its an instance method - co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals - ): - for cls in type(frame.f_locals["self"]).__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # if it was a class method, (decorated with `@classmethod`) - # we can get the class name by inspecting the f_locals for the `cls` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `cls` if its a class method - co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals - ): - for cls in frame.f_locals["cls"].__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # nothing we can do if it is a staticmethod (decorated with @staticmethod) - - # we've done all we can, time to give up and return what we have - return name - - -def frame_id(raw_frame: "FrameType") -> "FrameId": - return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) - - -def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": - abs_path = raw_frame.f_code.co_filename - - try: - module = raw_frame.f_globals["__name__"] - except Exception: - module = None - - # namedtuples can be many times slower when initialing - # and accessing attribute so we opt to use a tuple here instead - return { - # This originally was `os.path.abspath(abs_path)` but that had - # a large performance overhead. - # - # According to docs, this is equivalent to - # `os.path.normpath(os.path.join(os.getcwd(), path))`. - # The `os.getcwd()` call is slow here, so we precompute it. - # - # Additionally, since we are using normalized path already, - # we skip calling `os.path.normpath` entirely. - "abs_path": os.path.join(cwd, abs_path), - "module": module, - "filename": filename_for_module(module, abs_path) or None, - "function": fid[2], - "lineno": raw_frame.f_lineno, - } - - -def extract_stack( - raw_frame: "Optional[FrameType]", - cache: "LRUCache", - cwd: str, - max_stack_depth: int = MAX_STACK_DEPTH, -) -> "ExtractedStack": - """ - Extracts the stack starting the specified frame. The extracted stack - assumes the specified frame is the top of the stack, and works back - to the bottom of the stack. - - In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, - only the first `MAX_STACK_DEPTH` frames will be returned. - """ - - raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) - - while raw_frame is not None: - f_back = raw_frame.f_back - raw_frames.append(raw_frame) - raw_frame = f_back - - frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) - frames = [] - for i, fid in enumerate(frame_ids): - frame = cache.get(fid) - if frame is None: - frame = extract_frame(fid, raw_frames[i], cwd) - cache.set(fid, frame) - frames.append(frame) - - # Instead of mapping the stack into frame ids and hashing - # that as a tuple, we can directly hash the stack. - # This saves us from having to generate yet another list. - # Additionally, using the stack as the key directly is - # costly because the stack can be large, so we pre-hash - # the stack, and use the hash as the key as this will be - # needed a few times to improve performance. - # - # To Reduce the likelihood of hash collisions, we include - # the stack depth. This means that only stacks of the same - # depth can suffer from hash collisions. - stack_id = len(raw_frames), hash(frame_ids) - - return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py deleted file mode 100644 index 4fd22714cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scope.py +++ /dev/null @@ -1,2185 +0,0 @@ -import os -import platform -import sys -import warnings -from collections import deque -from contextlib import contextmanager -from copy import copy, deepcopy -from datetime import datetime, timezone -from enum import Enum -from functools import wraps -from itertools import chain -from typing import TYPE_CHECKING, cast - -import sentry_sdk -from sentry_sdk._types import AnnotatedValue -from sentry_sdk.attachments import Attachment -from sentry_sdk.consts import ( - DEFAULT_MAX_BREADCRUMBS, - FALSE_VALUES, - INSTRUMENTER, - SPANDATA, -) -from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.profiler.transaction_profiler import Profile -from sentry_sdk.session import Session -from sentry_sdk.traces import ( - _DEFAULT_PARENT_SPAN, - NoOpStreamedSpan, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.tracing_utils import ( - Baggage, - PropagationContext, - _make_sampling_decision, - has_span_streaming_enabled, - has_tracing_enabled, - is_ignored_span, -) -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exception, - capture_internal_exceptions, - datetime_from_isoformat, - disable_capture_event, - event_from_exception, - exc_info_from_error, - format_attribute, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - Deque, - Dict, - Generator, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - ) - - from typing_extensions import Unpack - - import sentry_sdk - from sentry_sdk._types import ( - Attributes, - AttributeValue, - Breadcrumb, - BreadcrumbHint, - ErrorProcessor, - Event, - EventProcessor, - ExcInfo, - Hint, - Log, - LogLevelStr, - Metric, - SamplingContext, - Type, - ) - from sentry_sdk.tracing import TransactionKwargs - - P = ParamSpec("P") - R = TypeVar("R") - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T") - - -# Holds data that will be added to **all** events sent by this process. -# In case this is a http server (think web framework) with multiple users -# the data will be added to events of all users. -# Typically this is used for process wide data such as the release. -_global_scope: "Optional[Scope]" = None - -# Holds data for the active request. -# This is used to isolate data for different requests or users. -# The isolation scope is usually created by integrations, but may also -# be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) - -# Holds data for the active span. -# This can be used to manually add additional data to a span. -_current_scope = ContextVar("current_scope", default=None) - -global_event_processors: "List[EventProcessor]" = [] - -# A function returning a (trace_id, span_id) tuple -# from an external tracing source (such as otel) -_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None - - -class ScopeType(Enum): - CURRENT = "current" - ISOLATION = "isolation" - GLOBAL = "global" - MERGED = "merged" - - -class _ScopeManager: - def __init__(self, hub: "Optional[Any]" = None) -> None: - self._old_scopes: "List[Scope]" = [] - - def __enter__(self) -> "Scope": - isolation_scope = Scope.get_isolation_scope() - - self._old_scopes.append(isolation_scope) - - forked_scope = isolation_scope.fork() - _isolation_scope.set(forked_scope) - - return forked_scope - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - old_scope = self._old_scopes.pop() - _isolation_scope.set(old_scope) - - -def add_global_event_processor(processor: "EventProcessor") -> None: - global_event_processors.append(processor) - - -def register_external_propagation_context( - fn: "Callable[[], Optional[Tuple[str, str]]]", -) -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = fn - - -def remove_external_propagation_context() -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = None - - -def get_external_propagation_context() -> "Optional[Tuple[str, str]]": - return ( - _external_propagation_context_fn() if _external_propagation_context_fn else None - ) - - -def has_external_propagation_context() -> bool: - return _external_propagation_context_fn is not None - - -def _attr_setter(fn: "Any") -> "Any": - return property(fset=fn, doc=fn.__doc__) - - -def _disable_capture(fn: "F") -> "F": - @wraps(fn) - def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": - if not self._should_capture: - return - try: - self._should_capture = False - return fn(self, *args, **kwargs) - finally: - self._should_capture = True - - return wrapper # type: ignore - - -class Scope: - """The scope holds extra information that should be sent with all - events that belong to it. - """ - - # NOTE: Even though it should not happen, the scope needs to not crash when - # accessed by multiple threads. It's fine if it's full of races, but those - # races should never make the user application crash. - # - # The same needs to hold for any accesses of the scope the SDK makes. - - __slots__ = ( - "_level", - "_name", - "_fingerprint", - # note that for legacy reasons, _transaction is the transaction *name*, - # not a Transaction object (the object is stored in _span) - "_transaction", - "_transaction_info", - "_user", - "_tags", - "_contexts", - "_extras", - "_breadcrumbs", - "_n_breadcrumbs_truncated", - "_gen_ai_original_message_count", - "_gen_ai_conversation_id", - "_event_processors", - "_error_processors", - "_should_capture", - "_span", - "_session", - "_attachments", - "_force_auto_session_tracking", - "_profile", - "_propagation_context", - "client", - "_type", - "_last_event_id", - "_flags", - "_attributes", - ) - - def __init__( - self, - ty: "Optional[ScopeType]" = None, - client: "Optional[sentry_sdk.Client]" = None, - ) -> None: - self._type = ty - - self._event_processors: "List[EventProcessor]" = [] - self._error_processors: "List[ErrorProcessor]" = [] - - self._name: "Optional[str]" = None - self._propagation_context: "Optional[PropagationContext]" = None - self._n_breadcrumbs_truncated: int = 0 - self._gen_ai_original_message_count: "Dict[str, int]" = {} - - self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() - - if client is not None: - self.set_client(client) - - self.clear() - - incoming_trace_information = self._load_trace_data_from_env() - self.generate_propagation_context(incoming_data=incoming_trace_information) - - def __copy__(self) -> "Scope": - """ - Returns a copy of this scope. - This also creates a copy of all referenced data structures. - """ - rv: "Scope" = object.__new__(self.__class__) - - rv._type = self._type - rv.client = self.client - rv._level = self._level - rv._name = self._name - rv._fingerprint = self._fingerprint - rv._transaction = self._transaction - rv._transaction_info = self._transaction_info.copy() - rv._user = self._user - - rv._tags = self._tags.copy() - rv._contexts = self._contexts.copy() - rv._extras = self._extras.copy() - - rv._breadcrumbs = copy(self._breadcrumbs) - rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated - rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() - rv._event_processors = self._event_processors.copy() - rv._error_processors = self._error_processors.copy() - rv._propagation_context = self._propagation_context - - rv._should_capture = self._should_capture - rv._span = self._span - rv._session = self._session - rv._force_auto_session_tracking = self._force_auto_session_tracking - rv._attachments = self._attachments.copy() - - rv._profile = self._profile - - rv._last_event_id = self._last_event_id - - rv._flags = deepcopy(self._flags) - - rv._attributes = self._attributes.copy() - - rv._gen_ai_conversation_id = self._gen_ai_conversation_id - - return rv - - @classmethod - def get_current_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the current scope. - """ - current_scope = _current_scope.get() - if current_scope is None: - current_scope = Scope(ty=ScopeType.CURRENT) - _current_scope.set(current_scope) - - return current_scope - - @classmethod - def set_current_scope(cls, new_current_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new current scope overwriting the existing current scope. - :param new_current_scope: The scope to set as the new current scope. - """ - _current_scope.set(new_current_scope) - - @classmethod - def get_isolation_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the isolation scope. - """ - isolation_scope = _isolation_scope.get() - if isolation_scope is None: - isolation_scope = Scope(ty=ScopeType.ISOLATION) - _isolation_scope.set(isolation_scope) - - return isolation_scope - - @classmethod - def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new isolation scope overwriting the existing isolation scope. - :param new_isolation_scope: The scope to set as the new isolation scope. - """ - _isolation_scope.set(new_isolation_scope) - - @classmethod - def get_global_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the global scope. - """ - global _global_scope - if _global_scope is None: - _global_scope = Scope(ty=ScopeType.GLOBAL) - - return _global_scope - - def set_global_attributes(self) -> None: - from sentry_sdk.client import SDK_INFO - - self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) - self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) - - self.set_attribute( - "process.runtime.name", - platform.python_implementation(), - ) - self.set_attribute( - "process.runtime.version", - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", - ) - - options = sentry_sdk.get_client().options - - server_name = options.get("server_name") - if server_name: - self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) - - environment = options.get("environment") - if environment: - self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) - - release = options.get("release") - if release: - self.set_attribute(SPANDATA.SENTRY_RELEASE, release) - - @classmethod - def last_event_id(cls) -> "Optional[str]": - """ - .. versionadded:: 2.2.0 - - Returns event ID of the event most recently captured by the isolation scope, or None if no event - has been captured. We do not consider events that are dropped, e.g. by a before_send hook. - Transactions also are not considered events in this context. - - The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; - whether the event is sent depends on the transport. The event could be sent later or not at all. - Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or - various other reasons. - """ - return cls.get_isolation_scope()._last_event_id - - def _merge_scopes( - self, - additional_scope: "Optional[Scope]" = None, - additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, - ) -> "Scope": - """ - Merges global, isolation and current scope into a new scope and - adds the given additional scope or additional scope kwargs to it. - """ - if additional_scope and additional_scope_kwargs: - raise TypeError("cannot provide scope and kwargs") - - final_scope = copy(_global_scope) if _global_scope is not None else Scope() - final_scope._type = ScopeType.MERGED - - isolation_scope = _isolation_scope.get() - if isolation_scope is not None: - final_scope.update_from_scope(isolation_scope) - - current_scope = _current_scope.get() - if current_scope is not None: - final_scope.update_from_scope(current_scope) - - if self != current_scope and self != isolation_scope: - final_scope.update_from_scope(self) - - if additional_scope is not None: - if callable(additional_scope): - additional_scope(final_scope) - else: - final_scope.update_from_scope(additional_scope) - - elif additional_scope_kwargs: - final_scope.update_from_kwargs(**additional_scope_kwargs) - - return final_scope - - @classmethod - def get_client(cls) -> "sentry_sdk.client.BaseClient": - """ - .. versionadded:: 2.0.0 - - Returns the currently used :py:class:`sentry_sdk.Client`. - This checks the current scope, the isolation scope and the global scope for a client. - If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. - """ - current_scope = _current_scope.get() - try: - client = current_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - isolation_scope = _isolation_scope.get() - try: - client = isolation_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - try: - client = _global_scope.client # type: ignore - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - return NonRecordingClient() - - def set_client( - self, client: "Optional[sentry_sdk.client.BaseClient]" = None - ) -> None: - """ - .. versionadded:: 2.0.0 - - Sets the client for this scope. - - :param client: The client to use in this scope. - If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. - - """ - if client is not None: - self.client = client - # We need a client to set the initial global attributes on the global - # scope since they mostly come from client options, so populate them - # as soon as a client is set - sentry_sdk.get_global_scope().set_global_attributes() - else: - self.client = NonRecordingClient() - - def fork(self) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns a fork of this scope. - """ - forked_scope = copy(self) - return forked_scope - - def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": - """ - Load Sentry trace id and baggage from environment variables. - Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". - """ - incoming_trace_information: "Optional[Dict[str, str]]" = None - - sentry_use_environment = ( - os.environ.get("SENTRY_USE_ENVIRONMENT") or "" - ).lower() - use_environment = sentry_use_environment not in FALSE_VALUES - if use_environment: - incoming_trace_information = {} - - if os.environ.get("SENTRY_TRACE"): - incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( - os.environ.get("SENTRY_TRACE") or "" - ) - - if os.environ.get("SENTRY_BAGGAGE"): - incoming_trace_information[BAGGAGE_HEADER_NAME] = ( - os.environ.get("SENTRY_BAGGAGE") or "" - ) - - return incoming_trace_information or None - - def set_new_propagation_context(self) -> None: - """ - Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. - """ - self._propagation_context = PropagationContext() - - def generate_propagation_context( - self, incoming_data: "Optional[Dict[str, str]]" = None - ) -> None: - """ - Makes sure the propagation context is set on the scope. - If there is `incoming_data` overwrite existing propagation context. - If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. - """ - if incoming_data is not None: - self._propagation_context = PropagationContext.from_incoming_data( - incoming_data - ) - - # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring - if self._type != ScopeType.CURRENT: - if self._propagation_context is None: - self.set_new_propagation_context() - - def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": - """ - Returns the Dynamic Sampling Context from the Propagation Context. - If not existing, creates a new one. - - Deprecated: Logic moved to PropagationContext, don't use directly. - """ - if self._propagation_context is None: - return None - - return self._propagation_context.dynamic_sampling_context - - def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - """ - Returns the Sentry "sentry-trace" header (aka the traceparent) from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().to_traceparent() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return traceparent from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_traceparent() - elif not span_streaming and self.span is not None: - return self.span._to_traceparent() - - # else return traceparent from the propagation context - return self.get_active_propagation_context().to_traceparent() - - def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": - """ - Returns the Sentry "baggage" header containing trace information from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().get_baggage() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return baggage from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_baggage() - elif not span_streaming and self.span is not None: - return self.span._to_baggage() - - # else return baggage from the propagation context - return self.get_active_propagation_context().get_baggage() - - def get_trace_context(self) -> "Dict[str, Any]": - """ - Returns the Sentry "trace" context from the Propagation Context. - """ - if ( - has_tracing_enabled(self.get_client().options) - and self._span is not None - and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) - ): - return self._span._get_trace_context() - - # if we are tracing externally (otel), those values take precedence - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - trace_id, span_id = external_propagation_context - return {"trace_id": trace_id, "span_id": span_id} - - propagation_context = self.get_active_propagation_context() - - return { - "trace_id": propagation_context.trace_id, - "span_id": propagation_context.span_id, - "parent_span_id": propagation_context.parent_span_id, - "dynamic_sampling_context": propagation_context.dynamic_sampling_context, - } - - def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: - """ - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - span = kwargs.pop("span", None) - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - meta = "" - - for name, content in self.iter_trace_propagation_headers(): - meta += f'' - - return meta - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. - Deprecated: use PropagationContext.iter_headers instead. - """ - if self._propagation_context is not None: - yield from self._propagation_context.iter_headers() - - def iter_trace_propagation_headers( - self, *args: "Any", **kwargs: "Any" - ) -> "Generator[Tuple[str, str], None, None]": - """ - Return HTTP headers which allow propagation of trace data. - - If a span is given, the trace data will taken from the span. - If no span is given, the trace data is taken from the scope. - """ - client = self.get_client() - if not client.options.get("propagate_traces"): - warnings.warn( - "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", - DeprecationWarning, - stacklevel=2, - ) - return - - span = kwargs.pop("span", None) - if not span: - span_streaming = has_span_streaming_enabled(client.options) - span = self.streamed_span if span_streaming else self.span - - if ( - has_tracing_enabled(client.options) - and span is not None - and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) - ): - for header in span._iter_headers(): - yield header - elif has_external_propagation_context(): - # when we have an external_propagation_context (otlp) - # we leave outgoing propagation to the propagator - return - else: - for header in self.get_active_propagation_context().iter_headers(): - yield header - - def get_active_propagation_context(self) -> "PropagationContext": - if self._propagation_context is not None: - return self._propagation_context - - current_scope = self.get_current_scope() - if current_scope._propagation_context is not None: - return current_scope._propagation_context - - isolation_scope = self.get_isolation_scope() - # should actually never happen, but just in case someone calls scope.clear - if isolation_scope._propagation_context is None: - isolation_scope._propagation_context = PropagationContext() - return isolation_scope._propagation_context - - @classmethod - def set_custom_sampling_context( - cls, custom_sampling_context: "dict[str, Any]" - ) -> None: - cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( - custom_sampling_context - ) - - def clear(self) -> None: - """Clears the entire scope.""" - self._level: "Optional[LogLevelStr]" = None - self._fingerprint: "Optional[List[str]]" = None - self._transaction: "Optional[str]" = None - self._transaction_info: "dict[str, str]" = {} - self._user: "Optional[Dict[str, Any]]" = None - - self._tags: "Dict[str, Any]" = {} - self._contexts: "Dict[str, Dict[str, Any]]" = {} - self._extras: "dict[str, Any]" = {} - self._attachments: "List[Attachment]" = [] - - self.clear_breadcrumbs() - self._should_capture: bool = True - - self._span: "Optional[Union[Span, StreamedSpan]]" = None - self._session: "Optional[Session]" = None - self._force_auto_session_tracking: "Optional[bool]" = None - - self._profile: "Optional[Profile]" = None - - self._propagation_context = None - - # self._last_event_id is only applicable to isolation scopes - self._last_event_id: "Optional[str]" = None - self._flags: "Optional[FlagBuffer]" = None - - self._attributes: "Attributes" = {} - - self._gen_ai_conversation_id: "Optional[str]" = None - - @_attr_setter - def level(self, value: "LogLevelStr") -> None: - """ - When set this overrides the level. - - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. - - :param value: The level to set. - """ - logger.warning( - "Deprecated: use .set_level() instead. This will be removed in the future." - ) - - self._level = value - - def set_level(self, value: "LogLevelStr") -> None: - """ - Sets the level for the scope. - - :param value: The level to set. - """ - self._level = value - - @_attr_setter - def fingerprint(self, value: "Optional[List[str]]") -> None: - """When set this overrides the default fingerprint.""" - self._fingerprint = value - - @property - def transaction(self) -> "Any": - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" - - # there is no span/transaction on the scope - if self._span is None: - return None - - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - # there is an orphan span on the scope - if self._span.containing_transaction is None: - return None - - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value: "Any") -> None: - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span: - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - if self._span.containing_transaction: - self._span.containing_transaction.name = value - - def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: - """Set the transaction name and optionally the transaction source.""" - self._transaction = name - if self._span: - if isinstance(self._span, NoOpStreamedSpan): - return - - elif isinstance(self._span, StreamedSpan): - self._span._segment.name = name - if source: - self._span._segment.set_attribute( - "sentry.span.source", getattr(source, "value", source) - ) - - elif self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source - - if source: - self._transaction_info["source"] = source - - @_attr_setter - def user(self, value: "Optional[Dict[str, Any]]") -> None: - """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" - warnings.warn( - "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", - DeprecationWarning, - stacklevel=2, - ) - self.set_user(value) - - def set_user(self, value: "Optional[Dict[str, Any]]") -> None: - """Sets a user for the scope.""" - self._user = value - - session = self.get_isolation_scope()._session - if session is not None: - session.update(user=value) - - @property - def span(self) -> "Optional[Span]": - """Get/set current tracing span or transaction.""" - return self._span if isinstance(self._span, Span) else None - - @span.setter - def span(self, span: "Optional[Span]") -> None: - self._span = span - # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source - - @property - def streamed_span(self) -> "Optional[StreamedSpan]": - """Get/set current tracing span.""" - return self._span if isinstance(self._span, StreamedSpan) else None - - @streamed_span.setter - def streamed_span(self, span: "Optional[StreamedSpan]") -> None: - self._span = span - - # Also set _transaction and _transaction_info in streaming mode as this - # is used for populating events and linking them to segments - if type(span) is StreamedSpan and span._is_segment(): - self._transaction = span.name - if span._attributes.get("sentry.span.source"): - self._transaction_info["source"] = str( - span._attributes["sentry.span.source"] - ) - - @property - def profile(self) -> "Optional[Profile]": - return self._profile - - @profile.setter - def profile(self, profile: "Optional[Profile]") -> None: - self._profile = profile - - def set_tag(self, key: str, value: "Any") -> None: - """ - Sets a tag for a key to a specific value. - - :param key: Key of the tag to set. - - :param value: Value of the tag to set. - """ - self._tags[key] = value - - def set_tags(self, tags: "Mapping[str, object]") -> None: - """Sets multiple tags at once. - - This method updates multiple tags at once. The tags are passed as a dictionary - or other mapping type. - - Calling this method is equivalent to calling `set_tag` on each key-value pair - in the mapping. If a tag key already exists in the scope, its value will be - updated. If the tag key does not exist in the scope, the key-value pair will - be added to the scope. - - This method only modifies tag keys in the `tags` mapping passed to the method. - `scope.set_tags({})` is, therefore, a no-op. - - :param tags: A mapping of tag keys to tag values to set. - """ - self._tags.update(tags) - - def remove_tag(self, key: str) -> None: - """ - Removes a specific tag. - - :param key: Key of the tag to remove. - """ - self._tags.pop(key, None) - - def set_context( - self, - key: str, - value: "Dict[str, Any]", - ) -> None: - """ - Binds a context at a certain key to a specific value. - """ - self._contexts[key] = value - - def remove_context( - self, - key: str, - ) -> None: - """Removes a context.""" - self._contexts.pop(key, None) - - def set_extra( - self, - key: str, - value: "Any", - ) -> None: - """Sets an extra key to a specific value.""" - self._extras[key] = value - - def remove_extra( - self, - key: str, - ) -> None: - """Removes a specific extra key.""" - self._extras.pop(key, None) - - def set_conversation_id(self, conversation_id: str) -> None: - """ - Sets the conversation ID for gen_ai spans. - - :param conversation_id: The conversation ID to set. - """ - self._gen_ai_conversation_id = conversation_id - - def get_conversation_id(self) -> "Optional[str]": - """ - Gets the conversation ID for gen_ai spans. - - :returns: The conversation ID, or None if not set. - """ - return self._gen_ai_conversation_id - - def remove_conversation_id(self) -> None: - """Removes the conversation ID.""" - self._gen_ai_conversation_id = None - - def clear_breadcrumbs(self) -> None: - """Clears breadcrumb buffer.""" - self._breadcrumbs: "Deque[Breadcrumb]" = deque() - self._n_breadcrumbs_truncated = 0 - - def add_attachment( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - """Adds an attachment to future events sent from this scope. - - The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. - """ - self._attachments.append( - Attachment( - bytes=bytes, - path=path, - filename=filename, - content_type=content_type, - add_to_transactions=add_to_transactions, - ) - ) - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - client = self.get_client() - - if not client.is_active(): - logger.info("Dropped breadcrumb because no client bound") - return - - before_breadcrumb = client.options.get("before_breadcrumb") - max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) - - crumb = dict(crumb or ()) - crumb.update(kwargs) - if not crumb: - return - - hint = dict(hint or ()) - - if crumb.get("timestamp") is None: - crumb["timestamp"] = datetime.now(timezone.utc) - if crumb.get("type") is None: - crumb["type"] = "default" - - if before_breadcrumb is not None: - new_crumb = before_breadcrumb(crumb, hint) - else: - new_crumb = crumb - - if new_crumb is not None: - self._breadcrumbs.append(new_crumb) - else: - logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) - - while len(self._breadcrumbs) > max_breadcrumbs: - self._breadcrumbs.popleft() - self._n_breadcrumbs_truncated += 1 - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - try_autostart_continuous_profiler() - - custom_sampling_context = custom_sampling_context or {} - - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs: "TransactionKwargs" = kwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - # update the sample rate in the dsc - if transaction.sample_rate is not None: - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction._baggage: - transaction._baggage.sentry_items["sample_rate"] = str( - transaction.sample_rate - ) - - if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - - transaction._continuous_profile = try_profile_lifecycle_trace_start() - - # Typically, the profiler is set when the transaction is created. But when - # using the auto lifecycle, the profiler isn't running when the first - # transaction is started. So make sure we update the profiler id on it. - if transaction._continuous_profile is not None: - transaction.set_profiler_id(get_profiler_id()) - - # we don't bother to keep spans if we already know we're not going to - # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) - - return transaction - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - warnings.warn( - "Scope.start_span is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return NoOpSpan() - - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - # get current span or transaction - span = self.span or self.get_isolation_scope().span - if isinstance(span, StreamedSpan): - # make mypy happy - return NoOpSpan() - - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - kwargs["trace_id"] = propagation_context.trace_id - - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) - - return span - - def start_streamed_span( - self, - name: str, - attributes: "Optional[Attributes]", - parent_span: "Optional[StreamedSpan]", - active: bool, - ) -> "StreamedSpan": - # TODO: rename to start_span once we drop the old API - if isinstance(parent_span, NoOpStreamedSpan): - # parent_span is only set if the user explicitly set it - logger.debug( - "Ignored parent span provided. Span will be parented to the " - "currently active span instead." - ) - - if parent_span is _DEFAULT_PARENT_SPAN or isinstance( - parent_span, NoOpStreamedSpan - ): - parent_span = self.streamed_span - - # If no eligible parent_span was provided and there is no currently - # active span, this is a segment - if parent_span is None: - propagation_context = self.get_active_propagation_context() - - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - scope=self, - unsampled_reason="ignored", - ) - - sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( - name, - attributes, - self, - ) - - if sample_rate is not None: - self._update_sample_rate(sample_rate) - - if sampled is False: - return NoOpStreamedSpan( - scope=self, - unsampled_reason=outcome, - ) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=None, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - parent_sampled=propagation_context.parent_sampled, - baggage=propagation_context.baggage, - sample_rand=sample_rand, - sample_rate=sample_rate, - ) - - # This is a child span; take propagation context from the parent span - with new_scope(): - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - unsampled_reason="ignored", - ) - - if isinstance(parent_span, NoOpStreamedSpan): - return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=parent_span._segment, - trace_id=parent_span.trace_id, - parent_span_id=parent_span.span_id, - parent_sampled=parent_span.sampled, - ) - - def _update_sample_rate(self, sample_rate: float) -> None: - # If we had to adjust the sample rate when setting the sampling decision - # for a span, it needs to be updated in the propagation context too - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(sample_rate) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", - ) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - self.generate_propagation_context(environ_or_headers) - - # generate_propagation_context ensures that the propagation_context is not None. - propagation_context = cast(PropagationContext, self._propagation_context) - - optional_kwargs = {} - if name: - optional_kwargs["name"] = name - if source: - optional_kwargs["source"] = source - - return Transaction( - op=op, - origin=origin, - baggage=propagation_context.baggage, - parent_sampled=propagation_context.parent_sampled, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - same_process_as_parent=False, - **optional_kwargs, - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures an event. - - Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - scope = self._merge_scopes(scope, scope_kwargs) - - event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) - - if event_id is not None and event.get("type") != "transaction": - self.get_isolation_scope()._last_event_id = event_id - - return event_id - - def _capture_log(self, log: "Optional[Log]") -> None: - if log is None: - return - - client = self.get_client() - if not has_logs_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" - ) - - client._capture_log(log, scope=merged_scope) - - def _capture_metric(self, metric: "Optional[Metric]") -> None: - if metric is None: - return - - client = self.get_client() - if not has_metrics_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" - ) - - client._capture_metric(metric, scope=merged_scope) - - def _capture_span(self, span: "Optional[StreamedSpan]") -> None: - if span is None: - return - - client = self.get_client() - if not has_span_streaming_enabled(client.options): - return - - merged_scope = self._merge_scopes() - client._capture_span(span, scope=merged_scope) - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures a message. - - :param message: The string to send as the message. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if level is None: - level = "info" - - event: "Event" = { - "message": message, - "level": level, - } - - return self.capture_event(event, scope=scope, **scope_kwargs) - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """Captures an exception. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if error is not None: - exc_info = exc_info_from_error(error) - else: - exc_info = sys.exc_info() - - event, hint = event_from_exception( - exc_info, client_options=self.get_client().options - ) - - try: - return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) - except Exception: - capture_internal_exception(sys.exc_info()) - - return None - - def start_session(self, *args: "Any", **kwargs: "Any") -> None: - """Starts a new session.""" - session_mode = kwargs.pop("session_mode", "application") - - self.end_session() - - client = self.get_client() - self._session = Session( - release=client.options.get("release"), - environment=client.options.get("environment"), - user=self._user, - session_mode=session_mode, - ) - - def end_session(self, *args: "Any", **kwargs: "Any") -> None: - """Ends the current session if there is one.""" - session = self._session - self._session = None - - if session is not None: - session.close() - self.get_client().capture_session(session) - - def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: - """Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - self.end_session() - self._force_auto_session_tracking = False - - def resume_auto_session_tracking(self) -> None: - """Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - self._force_auto_session_tracking = None - - def add_event_processor( - self, - func: "EventProcessor", - ) -> None: - """Register a scope local event processor on the scope. - - :param func: This function behaves like `before_send.` - """ - if len(self._event_processors) > 20: - logger.warning( - "Too many event processors on scope! Clearing list to free up some memory: %r", - self._event_processors, - ) - del self._event_processors[:] - - self._event_processors.append(func) - - def add_error_processor( - self, - func: "ErrorProcessor", - cls: "Optional[Type[BaseException]]" = None, - ) -> None: - """Register a scope local error processor on the scope. - - :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. - - :param cls: Optionally, only process exceptions of this type. - """ - if cls is not None: - cls_ = cls # For mypy. - real_func = func - - def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": - try: - is_inst = isinstance(exc_info[1], cls_) - except Exception: - is_inst = False - if is_inst: - return real_func(event, exc_info) - return event - - self._error_processors.append(func) - - def _apply_level_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._level is not None: - event["level"] = self._level - - def _apply_breadcrumbs_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - event.setdefault("breadcrumbs", {}) - - # This check is just for mypy - - if not isinstance(event["breadcrumbs"], AnnotatedValue): - event["breadcrumbs"].setdefault("values", []) - event["breadcrumbs"]["values"].extend(self._breadcrumbs) - - # Attempt to sort timestamps - try: - if not isinstance(event["breadcrumbs"], AnnotatedValue): - for crumb in event["breadcrumbs"]["values"]: - if isinstance(crumb["timestamp"], str): - crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) - - event["breadcrumbs"]["values"].sort( - key=lambda crumb: crumb["timestamp"] - ) - except Exception as err: - logger.debug("Error when sorting breadcrumbs", exc_info=err) - pass - - def _apply_user_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("user") is None and self._user is not None: - event["user"] = self._user - - def _apply_transaction_name_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction") is None and self._transaction is not None: - event["transaction"] = self._transaction - - def _apply_transaction_info_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction_info") is None and self._transaction_info is not None: - event["transaction_info"] = self._transaction_info - - def _apply_fingerprint_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("fingerprint") is None and self._fingerprint is not None: - event["fingerprint"] = self._fingerprint - - def _apply_extra_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._extras: - event.setdefault("extra", {}).update(self._extras) - - def _apply_tags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._tags: - event.setdefault("tags", {}).update(self._tags) - - def _apply_contexts_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._contexts: - event.setdefault("contexts", {}).update(self._contexts) - - contexts = event.setdefault("contexts", {}) - - # Add "trace" context - if contexts.get("trace") is None: - contexts["trace"] = self.get_trace_context() - - def _apply_flags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - flags = self.flags.get() - if len(flags) > 0: - event.setdefault("contexts", {}).setdefault("flags", {}).update( - {"values": flags} - ) - - def _apply_scope_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - # TODO: turn Logs, Metrics into actual classes - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - for attribute, value in self._attributes.items(): - if attribute not in attributes: - attributes[attribute] = value - - def _apply_user_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - if not should_send_default_pii() or self._user is None: - return - - for attribute_name, user_attribute in ( - ("user.id", "id"), - ("user.name", "username"), - ("user.email", "email"), - ("user.ip_address", "ip_address"), - ): - if ( - user_attribute in self._user - and attribute_name not in attributes - and self._user[user_attribute] is not None - ): - attributes[attribute_name] = self._user[user_attribute] - - def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": - logger.info("%s (%s) dropped event", ty, cause) - return None - - def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the error processors on the event and returns the modified event. - """ - exc_info = hint.get("exc_info") - if exc_info is not None: - error_processors = chain( - self.get_global_scope()._error_processors, - self.get_isolation_scope()._error_processors, - self.get_current_scope()._error_processors, - ) - - for error_processor in error_processors: - new_event = error_processor(event, exc_info) - if new_event is None: - return self._drop(error_processor, "error processor") - - event = new_event - - return event - - def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the event processors on the event and returns the modified event. - """ - ty = event.get("type") - is_check_in = ty == "check_in" - - if not is_check_in: - # Get scopes without creating them to prevent infinite recursion - isolation_scope = _isolation_scope.get() - current_scope = _current_scope.get() - - event_processors = chain( - global_event_processors, - _global_scope and _global_scope._event_processors or [], - isolation_scope and isolation_scope._event_processors or [], - current_scope and current_scope._event_processors or [], - ) - - for event_processor in event_processors: - new_event = event - with capture_internal_exceptions(): - new_event = event_processor(event, hint) - if new_event is None: - return self._drop(event_processor, "event processor") - event = new_event - - return event - - @_disable_capture - def apply_to_event( - self, - event: "Event", - hint: "Hint", - options: "Optional[Dict[str, Any]]" = None, - ) -> "Optional[Event]": - """Applies the information contained on the scope to the given event.""" - ty = event.get("type") - is_transaction = ty == "transaction" - is_check_in = ty == "check_in" - - # put all attachments into the hint. This lets callbacks play around - # with attachments. We also later pull this out of the hint when we - # create the envelope. - attachments_to_send = hint.get("attachments") or [] - for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: - attachments_to_send.append(attachment) - hint["attachments"] = attachments_to_send - - self._apply_contexts_to_event(event, hint, options) - - if is_check_in: - # Check-ins only support the trace context, strip all others - event["contexts"] = { - "trace": event.setdefault("contexts", {}).get("trace", {}) - } - - if not is_check_in: - self._apply_level_to_event(event, hint, options) - self._apply_fingerprint_to_event(event, hint, options) - self._apply_user_to_event(event, hint, options) - self._apply_transaction_name_to_event(event, hint, options) - self._apply_transaction_info_to_event(event, hint, options) - self._apply_tags_to_event(event, hint, options) - self._apply_extra_to_event(event, hint, options) - - if not is_transaction and not is_check_in: - self._apply_breadcrumbs_to_event(event, hint, options) - self._apply_flags_to_event(event, hint, options) - - event = self.run_error_processors(event, hint) - if event is None: - return None - - event = self.run_event_processors(event, hint) - if event is None: - return None - - return event - - @_disable_capture - def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: - # Attributes-based events and telemetry go through here (logs, metrics, - # spansV2) - if not isinstance(telemetry, StreamedSpan): - trace_context = self.get_trace_context() - trace_id = trace_context.get("trace_id") - if telemetry.get("trace_id") is None and trace_id is not None: - telemetry["trace_id"] = trace_id - - # span_id should only be populated if there's an active span. We can't - # use the trace_context here because it synthesizes a span_id if there - # isn't one - if telemetry.get("span_id") is None: - if self._span is not None and not isinstance( - self._span, (NoOpStreamedSpan, NoOpSpan) - ): - telemetry["span_id"] = self._span.span_id - else: - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - _, span_id = external_propagation_context - if span_id is not None: - telemetry["span_id"] = span_id - - self._apply_scope_attributes_to_telemetry(telemetry) - self._apply_user_attributes_to_telemetry(telemetry) - - def update_from_scope(self, scope: "Scope") -> None: - """Update the scope with another scope's data.""" - if scope._level is not None: - self._level = scope._level - if scope._fingerprint is not None: - self._fingerprint = scope._fingerprint - if scope._transaction is not None: - self._transaction = scope._transaction - if scope._transaction_info is not None: - self._transaction_info.update(scope._transaction_info) - if scope._user is not None: - self._user = scope._user - if scope._tags: - self._tags.update(scope._tags) - if scope._contexts: - self._contexts.update(scope._contexts) - if scope._extras: - self._extras.update(scope._extras) - if scope._breadcrumbs: - self._breadcrumbs.extend(scope._breadcrumbs) - if scope._n_breadcrumbs_truncated: - self._n_breadcrumbs_truncated = ( - self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated - ) - if scope._gen_ai_original_message_count: - self._gen_ai_original_message_count.update( - scope._gen_ai_original_message_count - ) - if scope._gen_ai_conversation_id: - self._gen_ai_conversation_id = scope._gen_ai_conversation_id - if scope._span: - self._span = scope._span - if scope._attachments: - self._attachments.extend(scope._attachments) - if scope._profile: - self._profile = scope._profile - if scope._propagation_context: - self._propagation_context = scope._propagation_context - if scope._session: - self._session = scope._session - if scope._flags: - if not self._flags: - self._flags = deepcopy(scope._flags) - else: - for flag in scope._flags.get(): - self._flags.set(flag["flag"], flag["result"]) - if scope._attributes: - self._attributes.update(scope._attributes) - - def update_from_kwargs( - self, - user: "Optional[Any]" = None, - level: "Optional[LogLevelStr]" = None, - extras: "Optional[Dict[str, Any]]" = None, - contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, - tags: "Optional[Dict[str, str]]" = None, - fingerprint: "Optional[List[str]]" = None, - attributes: "Optional[Attributes]" = None, - ) -> None: - """Update the scope's attributes.""" - if level is not None: - self._level = level - if user is not None: - self._user = user - if extras is not None: - self._extras.update(extras) - if contexts is not None: - self._contexts.update(contexts) - if tags is not None: - self._tags.update(tags) - if fingerprint is not None: - self._fingerprint = fingerprint - if attributes is not None: - self._attributes.update(attributes) - - def __repr__(self) -> str: - return "<%s id=%s name=%s type=%s>" % ( - self.__class__.__name__, - hex(id(self)), - self._name, - self._type, - ) - - @property - def flags(self) -> "FlagBuffer": - if self._flags is None: - max_flags = ( - self.get_client().options["_experiments"].get("max_flags") - or DEFAULT_FLAG_CAPACITY - ) - self._flags = FlagBuffer(capacity=max_flags) - return self._flags - - def set_attribute(self, attribute: str, value: "AttributeValue") -> None: - """ - Set an attribute on the scope. - - Any attributes-based telemetry (logs, metrics) captured while this scope - is active will inherit attributes set on the scope. - """ - self._attributes[attribute] = format_attribute(value) - - def remove_attribute(self, attribute: str) -> None: - """Remove an attribute if set on the scope. No-op if there is no such attribute.""" - try: - del self._attributes[attribute] - except KeyError: - pass - - -@contextmanager -def new_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current scope and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.new_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - new_scope = current_scope.fork() - token = _current_scope.set(new_scope) - - try: - yield new_scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `scope` and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - Suppose the variable `scope` contains a `Scope` object, which is not currently - the active scope. - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.use_scope(scope): - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # set given scope as current scope - token = _current_scope.set(scope) - - try: - yield scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def isolation_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current isolation scope and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # fork isolation scope - isolation_scope = Scope.get_isolation_scope() - new_isolation_scope = isolation_scope.fork() - isolation_token = _isolation_scope.set(new_isolation_scope) - - try: - yield new_isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `isolation_scope` and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # set given scope as isolation scope - isolation_token = _isolation_scope.set(isolation_scope) - - try: - yield isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -def should_send_default_pii() -> bool: - """Shortcut for `Scope.get_client().should_send_default_pii()`.""" - return Scope.get_client().should_send_default_pii() - - -# Circular imports -from sentry_sdk.client import NonRecordingClient - -if TYPE_CHECKING: - import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py deleted file mode 100644 index 6794491325..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/scrubber.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING, Dict, List, cast - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - iter_event_frames, -) - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event - - -DEFAULT_DENYLIST = [ - # stolen from relay - "password", - "passwd", - "secret", - "api_key", - "apikey", - "auth", - "credentials", - "mysql_pwd", - "privatekey", - "private_key", - "token", - "session", - # django - "csrftoken", - "sessionid", - # wsgi - "x_csrftoken", - "x_forwarded_for", - "set_cookie", - "cookie", - "authorization", - "proxy-authorization", - "x_api_key", - # other common names used in the wild - "aiohttp_session", # aiohttp - "connect.sid", # Express - "csrf_token", # Pyramid - "csrf", # (this is a cookie name used in accepted answers on stack overflow) - "_csrf", # Express - "_csrf_token", # Bottle - "PHPSESSID", # PHP - "_session", # Sanic - "symfony", # Symfony - "user_session", # Vue - "_xsrf", # Tornado - "XSRF-TOKEN", # Angular, Laravel -] - -DEFAULT_PII_DENYLIST = [ - "x_forwarded_for", - "x_real_ip", - "ip_address", - "remote_addr", -] - - -class EventScrubber: - def __init__( - self, - denylist: "Optional[List[str]]" = None, - recursive: bool = False, - send_default_pii: bool = False, - pii_denylist: "Optional[List[str]]" = None, - ) -> None: - """ - A scrubber that goes through the event payload and removes sensitive data configured through denylists. - - :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. - :param recursive: Whether to scrub the event payload recursively, default False. - :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. - :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. - """ - self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist - - if not send_default_pii: - pii_denylist = ( - DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist - ) - self.denylist += pii_denylist - - self.denylist = [x.lower() for x in self.denylist] - self.recursive = recursive - - def scrub_list(self, lst: object) -> None: - """ - If a list is passed to this method, the method recursively searches the list and any - nested lists for any dictionaries. The method calls scrub_dict on all dictionaries - it finds. - If the parameter passed to this method is not a list, the method does nothing. - """ - if not isinstance(lst, list): - return - - for v in lst: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_dict(self, d: object) -> None: - """ - If a dictionary is passed to this method, the method scrubs the dictionary of any - sensitive data. The method calls itself recursively on any nested dictionaries ( - including dictionaries nested in lists) if self.recursive is True. - This method does nothing if the parameter passed to it is not a dictionary. - """ - if not isinstance(d, dict): - return - - for k, v in d.items(): - # The cast is needed because mypy is not smart enough to figure out that k must be a - # string after the isinstance check. - if isinstance(k, str) and k.lower() in self.denylist: - d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() - elif self.recursive: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_request(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "request" in event: - if "headers" in event["request"]: - self.scrub_dict(event["request"]["headers"]) - if "cookies" in event["request"]: - self.scrub_dict(event["request"]["cookies"]) - if "data" in event["request"]: - self.scrub_dict(event["request"]["data"]) - - def scrub_extra(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "extra" in event: - self.scrub_dict(event["extra"]) - - def scrub_user(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "user" in event: - user = event["user"] - if "ip_address" in self.denylist and isinstance(user, dict): - user.pop("ip_address", None) - self.scrub_dict(user) - - def scrub_breadcrumbs(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "breadcrumbs" in event: - if ( - not isinstance(event["breadcrumbs"], AnnotatedValue) - and "values" in event["breadcrumbs"] - ): - for value in event["breadcrumbs"]["values"]: - if "data" in value: - self.scrub_dict(value["data"]) - - def scrub_frames(self, event: "Event") -> None: - with capture_internal_exceptions(): - for frame in iter_event_frames(event): - if "vars" in frame: - self.scrub_dict(frame["vars"]) - - def scrub_spans(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "spans" in event: - for span in cast(List[Dict[str, object]], event["spans"]): - if "data" in span: - self.scrub_dict(span["data"]) - - def scrub_event(self, event: "Event") -> None: - self.scrub_request(event) - self.scrub_extra(event) - self.scrub_user(event) - self.scrub_breadcrumbs(event) - self.scrub_frames(event) - self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py deleted file mode 100644 index 6bf6f6e70b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/serializer.py +++ /dev/null @@ -1,418 +0,0 @@ -import math -import sys -from array import array -from collections.abc import Mapping -from datetime import datetime -from typing import TYPE_CHECKING - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exception, - disable_capture_event, - format_timestamp, - safe_repr, - strip_string, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union - - from sentry_sdk._types import NotImplementedType - - Span = Dict[str, Any] - - ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] - Segment = Union[str, int] - - -# Bytes are technically not strings in Python 3, but we can serialize them -serializable_str_types = (str, bytes, bytearray, memoryview) - - -# Maximum length of JSON-serialized event payloads that can be safely sent -# before the server may reject the event due to its size. This is not intended -# to reflect actual values defined server-side, but rather only be an upper -# bound for events sent by the SDK. -# -# Can be overwritten if wanting to send more bytes, e.g. with a custom server. -# When changing this, keep in mind that events may be a little bit larger than -# this value due to attached metadata, so keep the number conservative. -MAX_EVENT_BYTES = 10**6 - -# Maximum depth and breadth of databags. Excess data will be trimmed. If -# max_request_body_size is "always", request bodies won't be trimmed. -MAX_DATABAG_DEPTH = 5 -MAX_DATABAG_BREADTH = 10 -CYCLE_MARKER = "" - - -global_repr_processors: "List[ReprProcessor]" = [] - - -def add_global_repr_processor(processor: "ReprProcessor") -> None: - global_repr_processors.append(processor) - - -sequence_types: "List[type]" = [tuple, list, set, frozenset, array] - - -def add_repr_sequence_type(ty: type) -> None: - sequence_types.append(ty) - - -class Memo: - __slots__ = ("_ids", "_objs") - - def __init__(self) -> None: - self._ids: "Dict[int, Any]" = {} - self._objs: "List[Any]" = [] - - def memoize(self, obj: "Any") -> "ContextManager[bool]": - self._objs.append(obj) - return self - - def __enter__(self) -> bool: - obj = self._objs[-1] - if id(obj) in self._ids: - return True - else: - self._ids[id(obj)] = obj - return False - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> None: - self._ids.pop(id(self._objs.pop()), None) - - -class _Serializer: - """Holds the state of a single serialize() call.""" - - __slots__ = ( - "memo", - "path", - "meta_stack", - "keep_request_bodies", - "max_value_length", - "is_vars", - "custom_repr", - ) - - def __init__( - self, - keep_request_bodies: bool, - max_value_length: "Optional[int]", - is_vars: bool, - custom_repr: "Optional[Callable[..., Optional[str]]]", - ) -> None: - self.memo = Memo() - self.path: "List[Segment]" = [] - self.meta_stack: "List[Dict[str, Any]]" = [] - self.keep_request_bodies = keep_request_bodies - self.max_value_length = max_value_length - self.is_vars = is_vars - self.custom_repr = custom_repr - - def _safe_repr_wrapper(self, value: "Any") -> str: - try: - repr_value = None - if self.custom_repr is not None: - repr_value = self.custom_repr(value) - return repr_value or safe_repr(value) - except Exception: - return safe_repr(value) - - def _annotate(self, **meta: "Any") -> None: - while len(self.meta_stack) <= len(self.path): - try: - segment = self.path[len(self.meta_stack) - 1] - node = self.meta_stack[-1].setdefault(str(segment), {}) - except IndexError: - node = {} - - self.meta_stack.append(node) - - self.meta_stack[-1].setdefault("", {}).update(meta) - - def _is_databag(self) -> "Optional[bool]": - """ - A databag is any value that we need to trim. - True for stuff like vars, request bodies, breadcrumbs and extra. - - :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". - """ - try: - if self.is_vars: - return True - - is_request_body = self._is_request_body() - if is_request_body in (True, None): - return is_request_body - - p0 = self.path[0] - if p0 == "breadcrumbs" and self.path[1] == "values": - self.path[2] - return True - - if p0 == "extra": - return True - - except IndexError: - return None - - return False - - def _is_span_attribute(self) -> "Optional[bool]": - try: - if self.path[0] == "spans" and self.path[2] == "data": - return True - except IndexError: - return None - - return False - - def _is_request_body(self) -> "Optional[bool]": - try: - if self.path[0] == "request" and self.path[1] == "data": - return True - except IndexError: - return None - - return False - - def _serialize_node( - self, - obj: "Any", - is_databag: "Optional[bool]" = None, - is_request_body: "Optional[bool]" = None, - should_repr_strings: "Optional[bool]" = None, - segment: "Optional[Segment]" = None, - remaining_breadth: "Optional[Union[int, float]]" = None, - remaining_depth: "Optional[Union[int, float]]" = None, - ) -> "Any": - if segment is not None: - self.path.append(segment) - - try: - with self.memo.memoize(obj) as result: - if result: - return CYCLE_MARKER - - return self._serialize_node_impl( - obj, - is_databag=is_databag, - is_request_body=is_request_body, - should_repr_strings=should_repr_strings, - remaining_depth=remaining_depth, - remaining_breadth=remaining_breadth, - ) - except BaseException: - capture_internal_exception(sys.exc_info()) - - if is_databag: - return "" - - return None - finally: - if segment is not None: - self.path.pop() - del self.meta_stack[len(self.path) + 1 :] - - def _flatten_annotated(self, obj: "Any") -> "Any": - if isinstance(obj, AnnotatedValue): - self._annotate(**obj.metadata) - obj = obj.value - return obj - - def _serialize_node_impl( - self, - obj: "Any", - is_databag: "Optional[bool]", - is_request_body: "Optional[bool]", - should_repr_strings: "Optional[bool]", - remaining_depth: "Optional[Union[float, int]]", - remaining_breadth: "Optional[Union[float, int]]", - ) -> "Any": - if isinstance(obj, AnnotatedValue): - should_repr_strings = False - if should_repr_strings is None: - should_repr_strings = self.is_vars - - if is_databag is None: - is_databag = self._is_databag() - - if is_request_body is None: - is_request_body = self._is_request_body() - - if is_databag: - if is_request_body and self.keep_request_bodies: - remaining_depth = float("inf") - remaining_breadth = float("inf") - else: - if remaining_depth is None: - remaining_depth = MAX_DATABAG_DEPTH - if remaining_breadth is None: - remaining_breadth = MAX_DATABAG_BREADTH - - obj = self._flatten_annotated(obj) - - if remaining_depth is not None and remaining_depth <= 0: - self._annotate(rem=[["!limit", "x"]]) - if is_databag: - return self._flatten_annotated( - strip_string( - self._safe_repr_wrapper(obj), max_length=self.max_value_length - ) - ) - return None - - is_span_attribute = self._is_span_attribute() - if (is_databag or is_span_attribute) and global_repr_processors: - hints = {"memo": self.memo, "remaining_depth": remaining_depth} - for processor in global_repr_processors: - result = processor(obj, hints) - if result is not NotImplemented: - return self._flatten_annotated(result) - - sentry_repr = getattr(type(obj), "__sentry_repr__", None) - - if obj is None or isinstance(obj, (bool, int, float)): - if should_repr_strings or ( - isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) - ): - return self._safe_repr_wrapper(obj) - else: - return obj - - elif callable(sentry_repr): - return sentry_repr(obj) - - elif isinstance(obj, datetime): - return ( - str(format_timestamp(obj)) - if not should_repr_strings - else self._safe_repr_wrapper(obj) - ) - - elif isinstance(obj, Mapping): - # Create temporary copy here to avoid calling too much code that - # might mutate our dictionary while we're still iterating over it. - obj = dict(obj.items()) - - rv_dict: "Dict[str, Any]" = {} - i = 0 - - for k, v in obj.items(): - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) - break - - str_k = str(k) - v = self._serialize_node( - v, - segment=str_k, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - rv_dict[str_k] = v - i += 1 - - return rv_dict - - elif not isinstance(obj, serializable_str_types) and isinstance( - obj, tuple(sequence_types) - ): - rv_list = [] - - for i, v in enumerate(obj): # type: ignore[arg-type] - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) # type: ignore[arg-type] - break - - rv_list.append( - self._serialize_node( - v, - segment=i, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - ) - - return rv_list - - if should_repr_strings: - obj = self._safe_repr_wrapper(obj) - else: - if isinstance(obj, bytes) or isinstance(obj, bytearray): - obj = obj.decode("utf-8", "replace") - - if not isinstance(obj, str): - obj = self._safe_repr_wrapper(obj) - - is_span_description = ( - len(self.path) == 3 - and self.path[0] == "spans" - and self.path[-1] == "description" - ) - if is_span_description: - return obj - - return self._flatten_annotated( - strip_string(obj, max_length=self.max_value_length) - ) - - -def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": - """ - A very smart serializer that takes a dict and emits a json-friendly dict. - Currently used for serializing the final Event and also prematurely while fetching the stack - local variables for each frame in a stacktrace. - - It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. - The algorithm itself is a recursive graph walk down the data structures it encounters. - - It has the following responsibilities: - * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. - * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. - * Annotating the payload with the _meta field whenever trimming happens. - - :param max_request_body_size: If set to "always", will never trim request bodies. - :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. - :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. - :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. - - """ - serializer = _Serializer( - keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", - max_value_length=kwargs.pop("max_value_length", None), - is_vars=kwargs.pop("is_vars", False), - custom_repr=kwargs.pop("custom_repr", None), - ) - - disable_capture_event.set(True) - try: - serialized_event = serializer._serialize_node(event, **kwargs) - if ( - not serializer.is_vars - and serializer.meta_stack - and isinstance(serialized_event, dict) - ): - serialized_event["_meta"] = serializer.meta_stack[0] - - return serialized_event - finally: - disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py deleted file mode 100644 index 3ffd071bbc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/session.py +++ /dev/null @@ -1,165 +0,0 @@ -import uuid -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from sentry_sdk._types import SessionStatus - - -def _minute_trunc(ts: "datetime") -> "datetime": - return ts.replace(second=0, microsecond=0) - - -def _make_uuid( - val: "Union[str, uuid.UUID]", -) -> "uuid.UUID": - if isinstance(val, uuid.UUID): - return val - return uuid.UUID(val) - - -class Session: - def __init__( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - session_mode: str = "application", - ) -> None: - if sid is None: - sid = uuid.uuid4() - if started is None: - started = datetime.now(timezone.utc) - if status is None: - status = "ok" - self.status = status - self.did: "Optional[str]" = None - self.started = started - self.release: "Optional[str]" = None - self.environment: "Optional[str]" = None - self.duration: "Optional[float]" = None - self.user_agent: "Optional[str]" = None - self.ip_address: "Optional[str]" = None - self.session_mode: str = session_mode - self.errors = 0 - - self.update( - sid=sid, - did=did, - timestamp=timestamp, - duration=duration, - release=release, - environment=environment, - user_agent=user_agent, - ip_address=ip_address, - errors=errors, - user=user, - ) - - @property - def truncated_started(self) -> "datetime": - return _minute_trunc(self.started) - - def update( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - ) -> None: - # If a user is supplied we pull some data form it - if user: - if ip_address is None: - ip_address = user.get("ip_address") - if did is None: - did = user.get("id") or user.get("email") or user.get("username") - - if sid is not None: - self.sid = _make_uuid(sid) - if did is not None: - self.did = str(did) - if timestamp is None: - timestamp = datetime.now(timezone.utc) - self.timestamp = timestamp - if started is not None: - self.started = started - if duration is not None: - self.duration = duration - if release is not None: - self.release = release - if environment is not None: - self.environment = environment - if ip_address is not None: - self.ip_address = ip_address - if user_agent is not None: - self.user_agent = user_agent - if errors is not None: - self.errors = errors - - if status is not None: - self.status = status - - def close( - self, - status: "Optional[SessionStatus]" = None, - ) -> "Any": - if status is None and self.status == "ok": - status = "exited" - if status is not None: - self.update(status=status) - - def get_json_attrs( - self, - with_user_info: "Optional[bool]" = True, - ) -> "Any": - attrs = {} - if self.release is not None: - attrs["release"] = self.release - if self.environment is not None: - attrs["environment"] = self.environment - if with_user_info: - if self.ip_address is not None: - attrs["ip_address"] = self.ip_address - if self.user_agent is not None: - attrs["user_agent"] = self.user_agent - return attrs - - def to_json(self) -> "Any": - rv: "Dict[str, Any]" = { - "sid": str(self.sid), - "init": True, - "started": format_timestamp(self.started), - "timestamp": format_timestamp(self.timestamp), - "status": self.status, - } - if self.errors: - rv["errors"] = self.errors - if self.did is not None: - rv["did"] = self.did - if self.duration is not None: - rv["duration"] = self.duration - attrs = self.get_json_attrs() - if attrs: - rv["attrs"] = attrs - return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py deleted file mode 100644 index aabf874fcd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/sessions.py +++ /dev/null @@ -1,262 +0,0 @@ -import os -import warnings -from contextlib import contextmanager -from threading import Event, Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.envelope import Envelope -from sentry_sdk.session import Session -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, List, Optional, Union - - -def is_auto_session_tracking_enabled( - hub: "Optional[sentry_sdk.Hub]" = None, -) -> "Union[Any, bool, None]": - """DEPRECATED: Utility function to find out if session tracking is enabled.""" - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - - should_track = hub.scope._force_auto_session_tracking - - if should_track is None: - client_options = hub.client.options if hub.client else {} - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking( - hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: Use track_session instead - Starts and stops a session automatically around a block. - """ - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "Use track_session instead.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - should_track = is_auto_session_tracking_enabled(hub) - if should_track: - hub.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - hub.end_session() - - -def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: - """ - DEPRECATED: Utility function to find out if session tracking is enabled. - """ - - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - return _is_auto_session_tracking_enabled(scope) - - -def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: - """ - Utility function to find out if session tracking is enabled. - """ - - should_track = scope._force_auto_session_tracking - if should_track is None: - client_options = sentry_sdk.get_client().options - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking_scope( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: This function is a deprecated alias for track_session. - Starts and stops a session automatically around a block. - """ - - warnings.warn( - "This function is a deprecated alias for track_session and will be removed in the next major release.", - DeprecationWarning, - stacklevel=2, - ) - - with track_session(scope, session_mode=session_mode): - yield - - -@contextmanager -def track_session( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """ - Start a new session in the provided scope, assuming session tracking is enabled. - This is a no-op context manager if session tracking is not enabled. - """ - - should_track = _is_auto_session_tracking_enabled(scope) - if should_track: - scope.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - scope.end_session() - - -TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") -MAX_ENVELOPE_ITEMS = 100 - - -def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": - return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} - - -class SessionFlusher: - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - flush_interval: int = 60, - ) -> None: - self.capture_func = capture_func - self.flush_interval = flush_interval - self.pending_sessions: "List[Any]" = [] - self.pending_aggregates: "Dict[Any, Any]" = {} - self._thread: "Optional[Thread]" = None - self._thread_lock = Lock() - self._aggregate_lock = Lock() - self._thread_for_pid: "Optional[int]" = None - self.__shutdown_requested = Event() - - def flush(self) -> None: - pending_sessions = self.pending_sessions - self.pending_sessions = [] - - with self._aggregate_lock: - pending_aggregates = self.pending_aggregates - self.pending_aggregates = {} - - envelope = Envelope() - for session in pending_sessions: - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_session(session) - - for attrs, states in pending_aggregates.items(): - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_sessions(make_aggregate_envelope(states, attrs)) - - if len(envelope.items) > 0: - self.capture_func(envelope) - - def _ensure_running(self) -> None: - """ - Check that we have an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - running = True - while running: - running = not self.__shutdown_requested.wait(self.flush_interval) - self.flush() - - thread = Thread(target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.__shutdown_requested.set() - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def add_aggregate_session( - self, - session: "Session", - ) -> None: - # NOTE on `session.did`: - # the protocol can deal with buckets that have a distinct-id, however - # in practice we expect the python SDK to have an extremely high cardinality - # here, effectively making aggregation useless, therefore we do not - # aggregate per-did. - - # For this part we can get away with using the global interpreter lock - with self._aggregate_lock: - attrs = session.get_json_attrs(with_user_info=False) - primary_key = tuple(sorted(attrs.items())) - secondary_key = session.truncated_started # (, session.did) - states = self.pending_aggregates.setdefault(primary_key, {}) - state = states.setdefault(secondary_key, {}) - - if "started" not in state: - state["started"] = format_timestamp(session.truncated_started) - # if session.did is not None: - # state["did"] = session.did - if session.status == "crashed": - state["crashed"] = state.get("crashed", 0) + 1 - elif session.status == "abnormal": - state["abnormal"] = state.get("abnormal", 0) + 1 - elif session.errors > 0: - state["errored"] = state.get("errored", 0) + 1 - else: - state["exited"] = state.get("exited", 0) + 1 - - def add_session( - self, - session: "Session", - ) -> None: - if session.session_mode == "request": - self.add_aggregate_session(session) - else: - self.pending_sessions.append(session.to_json()) - self._ensure_running() - - def kill(self) -> None: - self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py deleted file mode 100644 index 2dcc86bc47..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/spotlight.py +++ /dev/null @@ -1,327 +0,0 @@ -import io -import logging -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from itertools import chain, product -from typing import TYPE_CHECKING - -import urllib3 - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Self - -from sentry_sdk.envelope import Envelope -from sentry_sdk.utils import ( - capture_internal_exceptions, - env_to_bool, -) -from sentry_sdk.utils import ( - logger as sentry_logger, -) - -logger = logging.getLogger("spotlight") - - -DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" -DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" - - -class SpotlightClient: - """ - A client for sending envelopes to Sentry Spotlight. - - Implements exponential backoff retry logic per the SDK spec: - - Logs error at least once when server is unreachable - - Does not log for every failed envelope - - Uses exponential backoff to avoid hammering an unavailable server - - Never blocks normal Sentry operation - """ - - # Exponential backoff settings - INITIAL_RETRY_DELAY = 1.0 # Start with 1 second - MAX_RETRY_DELAY = 60.0 # Max 60 seconds - - def __init__(self, url: str) -> None: - self.url = url - self.http = urllib3.PoolManager() - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time: float = 0.0 - - def capture_envelope(self, envelope: "Envelope") -> None: - # Check if we're in backoff period - skip sending to avoid blocking - if self._last_error_time > 0: - time_since_error = time.time() - self._last_error_time - if time_since_error < self._retry_delay: - # Still in backoff period, skip this envelope - return - - body = io.BytesIO() - envelope.serialize_into(body) - try: - req = self.http.request( - url=self.url, - body=body.getvalue(), - method="POST", - headers={ - "Content-Type": "application/x-sentry-envelope", - }, - ) - req.close() - # Success - reset backoff state - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time = 0.0 - except Exception as e: - self._last_error_time = time.time() - - # Increase backoff delay exponentially first, so logged value matches actual wait - self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) - - # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) - sentry_logger.warning( - "Failed to send envelope to Spotlight at %s: %s. " - "Will retry after %.1f seconds.", - self.url, - e, - self._retry_delay, - ) - - -try: - from django.conf import settings - from django.http import HttpRequest, HttpResponse, HttpResponseServerError - from django.utils.deprecation import MiddlewareMixin - - SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" - SPOTLIGHT_JS_SNIPPET_PATTERN = ( - "\n" - '\n' - ) - SPOTLIGHT_ERROR_PAGE_SNIPPET = ( - '\n' - '\n' - ) - CHARSET_PREFIX = "charset=" - BODY_TAG_NAME = "body" - BODY_CLOSE_TAG_POSSIBILITIES = tuple( - "".format("".join(chars)) - for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) - ) - - class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] - _spotlight_script: "Optional[str]" = None - _spotlight_url: "Optional[str]" = None - - def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: - super().__init__(get_response) - - import sentry_sdk.api - - self.sentry_sdk = sentry_sdk.api - - spotlight_client = self.sentry_sdk.get_client().spotlight - if spotlight_client is None: - sentry_logger.warning( - "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." - ) - return None - # Spotlight URL has a trailing `/stream` part at the end so split it off - self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") - - @property - def spotlight_script(self: "Self") -> "Optional[str]": - if self._spotlight_url is not None and self._spotlight_script is None: - try: - spotlight_js_url = urllib.parse.urljoin( - self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH - ) - req = urllib.request.Request( - spotlight_js_url, - method="HEAD", - ) - urllib.request.urlopen(req) - self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( - spotlight_url=self._spotlight_url, - spotlight_js_url=spotlight_js_url, - ) - except urllib.error.URLError as err: - sentry_logger.debug( - "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", - spotlight_js_url, - exc_info=err, - ) - - return self._spotlight_script - - def process_response( - self: "Self", _request: "HttpRequest", response: "HttpResponse" - ) -> "Optional[HttpResponse]": - content_type_header = tuple( - p.strip() - for p in response.headers.get("Content-Type", "").lower().split(";") - ) - content_type = content_type_header[0] - if len(content_type_header) > 1 and content_type_header[1].startswith( - CHARSET_PREFIX - ): - encoding = content_type_header[1][len(CHARSET_PREFIX) :] - else: - encoding = "utf-8" - - if ( - self.spotlight_script is not None - and not response.streaming - and content_type == "text/html" - ): - content_length = len(response.content) - injection = self.spotlight_script.encode(encoding) - injection_site = next( - ( - idx - for idx in ( - response.content.rfind(body_variant.encode(encoding)) - for body_variant in BODY_CLOSE_TAG_POSSIBILITIES - ) - if idx > -1 - ), - content_length, - ) - - # This approach works even when we don't have a `` tag - response.content = ( - response.content[:injection_site] - + injection - + response.content[injection_site:] - ) - - if response.has_header("Content-Length"): - response.headers["Content-Length"] = content_length + len(injection) - - return response - - def process_exception( - self: "Self", _request: "HttpRequest", exception: Exception - ) -> "Optional[HttpResponseServerError]": - if not settings.DEBUG or not self._spotlight_url: - return None - - try: - spotlight = ( - urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") - ) - except urllib.error.URLError: - return None - else: - event_id = self.sentry_sdk.capture_exception(exception) - return HttpResponseServerError( - spotlight.replace( - "", - SPOTLIGHT_ERROR_PAGE_SNIPPET.format( - spotlight_url=self._spotlight_url, event_id=event_id - ), - ) - ) - -except ImportError: - settings = None - - -def _resolve_spotlight_url( - spotlight_config: "Any", sentry_logger: "Any" -) -> "Optional[str]": - """ - Resolve the Spotlight URL based on config and environment variable. - - Implements precedence rules per the SDK spec: - https://develop.sentry.dev/sdk/expected-features/spotlight/ - - Returns the resolved URL string, or None if Spotlight should be disabled. - """ - spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") - - # Parse env var to determine if it's a boolean or URL - spotlight_from_env: "Optional[bool]" = None - spotlight_env_url: "Optional[str]" = None - if spotlight_env_value: - parsed = env_to_bool(spotlight_env_value, strict=True) - if parsed is None: - # It's a URL string - spotlight_from_env = True - spotlight_env_url = spotlight_env_value - else: - spotlight_from_env = parsed - - # Apply precedence rules per spec: - # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules - if spotlight_config is False: - # Config explicitly disables spotlight - warn if env var was set - if spotlight_from_env: - sentry_logger.warning( - "Spotlight is disabled via spotlight=False config option, " - "ignoring SENTRY_SPOTLIGHT environment variable." - ) - return None - elif spotlight_config is True: - # Config enables spotlight with boolean true - # If env var has URL, use env var URL per spec - if spotlight_env_url: - return spotlight_env_url - else: - return DEFAULT_SPOTLIGHT_URL - elif isinstance(spotlight_config, str): - # Config has URL string - use config URL, warn if env var differs - if spotlight_env_value and spotlight_env_value != spotlight_config: - sentry_logger.warning( - "Spotlight URL from config (%s) takes precedence over " - "SENTRY_SPOTLIGHT environment variable (%s).", - spotlight_config, - spotlight_env_value, - ) - return spotlight_config - elif spotlight_config is None: - # No config - use env var - if spotlight_env_url: - return spotlight_env_url - elif spotlight_from_env: - return DEFAULT_SPOTLIGHT_URL - # else: stays None (disabled) - - return None - - -def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": - url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) - - if url is None: - return None - - # Only set up logging handler when spotlight is actually enabled - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.INFO) - - # Update options with resolved URL for consistency - options["spotlight"] = url - - with capture_internal_exceptions(): - if ( - settings is not None - and settings.DEBUG - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) - ): - middleware = settings.MIDDLEWARE - if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: - settings.MIDDLEWARE = type(middleware)( - chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) - ) - logger.info("Enabled Spotlight integration for Django") - - client = SpotlightClient(url) - logger.info("Enabled Spotlight using sidecar at %s", url) - - return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py deleted file mode 100644 index 5ee7e8460b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/traces.py +++ /dev/null @@ -1,843 +0,0 @@ -""" -EXPERIMENTAL. Do not use in production. - -The API in this file is only meant to be used in span streaming mode. - -You can enable span streaming mode via -sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). -""" - -import sys -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - capture_internal_exceptions, - format_attribute, - get_current_thread_meta, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Iterator, - Optional, - ParamSpec, - TypeVar, - Union, - overload, - ) - - from sentry_sdk._types import Attributes, AttributeValue, SpanJSON - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - - P = ParamSpec("P") - R = TypeVar("R") - - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -class SpanStatus(str, Enum): - OK = "ok" - ERROR = "error" - - def __str__(self) -> str: - return self.value - - -_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) - - -# Segment source, see -# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource -class SegmentSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_SEGMENT_SOURCES = [ - SegmentSource.URL, -] - - -SOURCE_FOR_STYLE = { - "endpoint": SegmentSource.COMPONENT, - "function_name": SegmentSource.COMPONENT, - "handler_name": SegmentSource.COMPONENT, - "method_and_path_pattern": SegmentSource.ROUTE, - "path": SegmentSource.URL, - "route_name": SegmentSource.COMPONENT, - "route_pattern": SegmentSource.ROUTE, - "uri_template": SegmentSource.ROUTE, - "url": SegmentSource.ROUTE, -} - - -# Sentinel value for an unset parent_span to be able to distinguish it from -# a None set by the user -_DEFAULT_PARENT_SPAN = object() - - -def start_span( - name: str, - attributes: "Optional[Attributes]" = None, - parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] - active: bool = True, -) -> "StreamedSpan": - """ - Start a span. - - EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() - instead. - - The span's parent, unless provided explicitly via the `parent_span` argument, - will be the current active span, if any. If there is none, this span will - become the root of a new span tree. If you explicitly want this span to be - top-level without a parent, set `parent_span=None`. - - `start_span()` can either be used as context manager or you can use the span - object it returns and explicitly end it via `span.end()`. The following is - equivalent: - - ```python - import sentry_sdk - - with sentry_sdk.traces.start_span(name="My Span"): - # do something - - # The span automatically finishes once the `with` block is exited - ``` - - ```python - import sentry_sdk - - span = sentry_sdk.traces.start_span(name="My Span") - # do something - span.end() - ``` - - To continue a trace from another service, call - `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. - - :param name: The name to identify this span by. - :type name: str - - :param attributes: Key-value attributes to set on the span from the start. - These will also be accessible in the traces sampler. - :type attributes: "Optional[Attributes]" - - :param parent_span: A span instance that the new span should consider its - parent. If not provided, the parent will be set to the currently active - span, if any. If set to `None`, this span will become a new root-level - span. - :type parent_span: "Optional[StreamedSpan]" - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to `False`. - :type active: bool - - :return: The span that has been started. - :rtype: StreamedSpan - """ - from sentry_sdk.tracing_utils import has_span_streaming_enabled - - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "sentry_sdk.start_transaction() and sentry_sdk.start_span() " - "instead.", - stacklevel=2, - ) - return NoOpStreamedSpan() - - return sentry_sdk.get_current_scope().start_streamed_span( - name, attributes, parent_span, active - ) - - -def continue_trace(incoming: "dict[str, Any]") -> None: - """ - Continue a trace from headers or environment variables. - - EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. - - This function sets the propagation context on the scope. Any span started - in the updated scope will belong under the trace extracted from the - provided propagation headers or environment variables. - - continue_trace() doesn't start any spans on its own. Use the start_span() - API for that. - """ - # This is set both on the isolation and the current scope for compatibility - # reasons. Conceptually, it belongs on the isolation scope, and it also - # used to be set there in non-span-first mode. But in span first mode, we - # start spans on the current scope, regardless of type, like JS does, so we - # need to set the propagation context there. - sentry_sdk.get_isolation_scope().generate_propagation_context( - incoming, - ) - sentry_sdk.get_current_scope().generate_propagation_context( - incoming, - ) - - -def new_trace() -> None: - """ - Resets the propagation context, forcing a new trace. - - EXPERIMENTAL. - - This function sets the propagation context on the scope. Any span started - in the updated scope will start its own trace. - - new_trace() doesn't start any spans on its own. Use the start_span() API - for that. - """ - sentry_sdk.get_isolation_scope().set_new_propagation_context() - sentry_sdk.get_current_scope().set_new_propagation_context() - - -class StreamedSpan: - """ - A span holds timing information of a block of code. - - Spans can have multiple child spans, thus forming a span tree. - - This is the Span First span implementation that streams spans. The original - transaction-based span implementation lives in tracing.Span. - """ - - __slots__ = ( - "_name", - "_attributes", - "_active", - "_span_id", - "_trace_id", - "_parent_span_id", - "_segment", - "_parent_sampled", - "_start_timestamp", - "_start_timestamp_monotonic_ns", - "_end_timestamp", - "_status", - "_scope", - "_previous_span_on_scope", - "_baggage", - "_sample_rand", - "_sample_rate", - "_continuous_profile", - ) - - def __init__( - self, - *, - name: str, - attributes: "Optional[Attributes]" = None, - active: bool = True, - scope: "sentry_sdk.Scope", - segment: "Optional[StreamedSpan]" = None, - trace_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - sample_rate: "Optional[float]" = None, - sample_rand: "Optional[float]" = None, - ): - self._name: str = name - self._active: bool = active - self._attributes: "Attributes" = { - "sentry.origin": "manual", - "sentry.trace_lifecycle": "stream", - } - - if attributes: - for attribute, value in attributes.items(): - self.set_attribute(attribute, value) - - self._scope = scope - - self._segment = segment or self - - self._trace_id: "Optional[str]" = trace_id - self._parent_span_id = parent_span_id - self._parent_sampled = parent_sampled - self._baggage = baggage - self._sample_rand = sample_rand - self._sample_rate = sample_rate - - self._start_timestamp = datetime.now(timezone.utc) - self._end_timestamp: "Optional[datetime]" = None - - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - - self._span_id: "Optional[str]" = None - - self._status = SpanStatus.OK.value - - self._update_active_thread() - - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._start_profile() - self._set_profile_id(get_profiler_id()) - - self._set_segment_attributes() - - self._start() - - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__}(" - f"name={self._name}, " - f"trace_id={self.trace_id}, " - f"span_id={self.span_id}, " - f"parent_span_id={self._parent_span_id}, " - f"active={self._active})>" - ) - - def __enter__(self) -> "StreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore - return - - if value is not None and should_be_treated_as_error(ty, value): - self.status = SpanStatus.ERROR.value - - self._end() - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - """ - Finish this span and queue it for sending. - - :param end_timestamp: End timestamp to use instead of current time. - :type end_timestamp: "Optional[Union[float, datetime]]" - """ - self._end(end_timestamp) - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self.end(end_timestamp) - - def _start(self) -> None: - if self._active: - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore. - return - - # Stop the profiler - if self._is_segment() and self._continuous_profile is not None: - with capture_internal_exceptions(): - self._continuous_profile.stop() - - # Detach from scope - if self._active: - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - # Set attributes from the segment. These are set on span end on purpose - # so that we have the best chance to capture the segment's final name - # (since it might change during its lifetime) - self.set_attribute("sentry.segment.id", self._segment.span_id) - self.set_attribute("sentry.segment.name", self._segment.name) - - # Set the end timestamp - if end_timestamp is not None: - if isinstance(end_timestamp, (float, int)): - try: - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - except Exception: - pass - - if isinstance(end_timestamp, datetime): - self._end_timestamp = end_timestamp - else: - logger.debug( - "[Tracing] Failed to set end_timestamp. Using current time instead." - ) - - if self._end_timestamp is None: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self._end_timestamp = self._start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # Finally, queue the span for sending to Sentry - self._scope._capture_span(self) - - def get_attributes(self) -> "Attributes": - return self._attributes - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - self._attributes[key] = format_attribute(value) - - def set_attributes(self, attributes: "Attributes") -> None: - for key, value in attributes.items(): - self.set_attribute(key, value) - - def remove_attribute(self, key: str) -> None: - try: - del self._attributes[key] - except KeyError: - pass - - @property - def status(self) -> "str": - return self._status - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - if isinstance(status, Enum): - status = status.value - - if status not in _VALID_SPAN_STATUSES: - logger.debug( - f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' - ) - return - - self._status = status - - @property - def name(self) -> str: - return self._name - - @name.setter - def name(self, name: str) -> None: - self._name = name - - @property - def active(self) -> bool: - return self._active - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @property - def sampled(self) -> "Optional[bool]": - return True - - @property - def start_timestamp(self) -> "Optional[datetime]": - return self._start_timestamp - - @property - def end_timestamp(self) -> "Optional[datetime]": - return self._end_timestamp - - def _is_segment(self) -> bool: - return self._segment is self - - def _update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - - if thread_id is not None: - self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_attribute(SPANDATA.THREAD_NAME, thread_name) - - def _dynamic_sampling_context(self) -> "dict[str, str]": - return self._segment._get_baggage().dynamic_sampling_context() - - def _to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def _to_baggage(self) -> "Optional[Baggage]": - if self._segment: - return self._segment._get_baggage() - return None - - def _get_baggage(self) -> "Baggage": - """ - Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with - the segment. - - The first time a new baggage with Sentry items is made, it will be frozen. - """ - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_segment(self) - - return self._baggage - - def _iter_headers(self) -> "Iterator[tuple[str, str]]": - if not self._segment: - return - - yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() - - baggage = self._segment._get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def _get_trace_context(self) -> "dict[str, Any]": - # Even if spans themselves are not event-based anymore, we need this - # to populate trace context on events - context: "dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self._parent_span_id, - "dynamic_sampling_context": self._dynamic_sampling_context(), - } - - if "sentry.op" in self._attributes: - context["op"] = self._attributes["sentry.op"] - if "sentry.origin" in self._attributes: - context["origin"] = self._attributes["sentry.origin"] - - return context - - def _set_profile_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_attribute("sentry.profiler_id", profiler_id) - - def _start_profile(self) -> None: - if not self._is_segment(): - return - - try_autostart_continuous_profiler() - - self._continuous_profile = try_profile_lifecycle_trace_start() - - def _set_segment_attributes(self) -> None: - if not self._is_segment(): - return - - client = sentry_sdk.get_client() - - self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") - self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) - self.set_attribute( - SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) - ) - - if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: - self.set_attribute( - SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() - ) - - def _to_json(self) -> "SpanJSON": - res: "SpanJSON" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "name": self._name if self._name is not None else "", - "status": self._status, - "is_segment": self._is_segment(), - "start_timestamp": self._start_timestamp.timestamp(), - } - - if self._end_timestamp: - res["end_timestamp"] = self._end_timestamp.timestamp() - - if self._parent_span_id: - res["parent_span_id"] = self._parent_span_id - - res["attributes"] = {k: v for k, v in self._attributes.items()} - - return res - - -class NoOpStreamedSpan(StreamedSpan): - __slots__ = ( - "_finished", - "_unsampled_reason", - ) - - def __init__( - self, - unsampled_reason: "Optional[str]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - ) -> None: - self._scope = scope # type: ignore[assignment] - self._unsampled_reason = unsampled_reason - - self._finished = False - - self._start() - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}(sampled={self.sampled})>" - - def __enter__(self) -> "NoOpStreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self._end() - - def _start(self) -> None: - if self._scope is None: - return - - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._finished: - return - - if self._unsampled_reason is not None: - client = sentry_sdk.get_client() - if client.is_active() and client.transport: - logger.debug( - f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" - ) - client.transport.record_lost_event( - reason=self._unsampled_reason, - data_category="span", - quantity=1, - ) - - if self._scope and hasattr(self, "_previous_span_on_scope"): - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - self._finished = True - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - self._end() - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self._end() - - def get_attributes(self) -> "Attributes": - return {} - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - pass - - def set_attributes(self, attributes: "Attributes") -> None: - pass - - def remove_attribute(self, key: str) -> None: - pass - - def _is_segment(self) -> bool: - return self._scope is not None - - @property - def status(self) -> "str": - return SpanStatus.OK.value - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - pass - - @property - def name(self) -> str: - return "" - - @name.setter - def name(self, value: str) -> None: - pass - - @property - def active(self) -> bool: - return True - - @property - def span_id(self) -> str: - return "0000000000000000" - - @property - def trace_id(self) -> str: - return "00000000000000000000000000000000" - - @property - def sampled(self) -> "Optional[bool]": - return False - - @property - def start_timestamp(self) -> "Optional[datetime]": - return None - - @property - def end_timestamp(self) -> "Optional[datetime]": - return None - - -if TYPE_CHECKING: - - @overload - def trace( - func: "Callable[P, R]", - ) -> "Callable[P, R]": ... - - @overload - def trace( - func: None = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a span around a function call. - - EXPERIMENTAL. Use @sentry_sdk.trace instead. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to False. - :type active: bool - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import ( - create_streaming_span_decorator, - ) - - decorator = create_streaming_span_decorator( - name=name, - attributes=attributes, - active=active, - ) - - if func: - return decorator(func) - else: - return decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[StreamedSpan]": - """ - Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. - - This function will only return a non-`None` value when the streaming trace lifecycle is enabled. - To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.streamed_span - return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py deleted file mode 100644 index 1790e13fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing.py +++ /dev/null @@ -1,1475 +0,0 @@ -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE -from sentry_sdk.profiler.continuous_profiler import get_profiler_id -from sentry_sdk.utils import ( - capture_internal_exceptions, - get_current_thread_meta, - is_valid_sample_rate, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping - from typing import ( - Any, - Dict, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - overload, - ) - - from typing_extensions import TypedDict, Unpack - - P = ParamSpec("P") - R = TypeVar("R") - - from sentry_sdk._types import ( - Event, - MeasurementUnit, - MeasurementValue, - SamplingContext, - ) - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - from sentry_sdk.profiler.transaction_profiler import Profile - - class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - - op: str - """ - The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - """ - - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - - hub: "Optional[sentry_sdk.Hub]" - """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" - - status: str - """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - - containing_transaction: "Optional[Transaction]" - """The transaction that this span belongs to.""" - - start_timestamp: "Optional[Union[datetime, float]]" - """ - The timestamp when the span started. If omitted, the current time - will be used. - """ - - scope: "sentry_sdk.Scope" - """The scope to use for this span. If not provided, we use the current scope.""" - - origin: str - """ - The origin of the span. - See https://develop.sentry.dev/sdk/performance/trace-origin/ - Default "manual". - """ - - name: str - """A string describing what operation is being performed within the span/transaction.""" - - class TransactionKwargs(SpanKwargs, total=False): - source: str - """ - A string describing the source of the transaction name. This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. - Default "custom". - """ - - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - - ProfileContext = TypedDict( - "ProfileContext", - { - "profiler_id": str, - }, - ) - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -# Transaction source -# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations -class TransactionSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_TRANSACTION_SOURCES = [ - TransactionSource.URL, -] - -SOURCE_FOR_STYLE = { - "endpoint": TransactionSource.COMPONENT, - "function_name": TransactionSource.COMPONENT, - "handler_name": TransactionSource.COMPONENT, - "method_and_path_pattern": TransactionSource.ROUTE, - "path": TransactionSource.URL, - "route_name": TransactionSource.COMPONENT, - "route_pattern": TransactionSource.ROUTE, - "uri_template": TransactionSource.ROUTE, - "url": TransactionSource.ROUTE, -} - - -def get_span_status_from_http_code(http_status_code: int) -> str: - """ - Returns the Sentry status corresponding to the given HTTP status code. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - if http_status_code < 400: - return SPANSTATUS.OK - - elif 400 <= http_status_code < 500: - if http_status_code == 403: - return SPANSTATUS.PERMISSION_DENIED - elif http_status_code == 404: - return SPANSTATUS.NOT_FOUND - elif http_status_code == 429: - return SPANSTATUS.RESOURCE_EXHAUSTED - elif http_status_code == 413: - return SPANSTATUS.FAILED_PRECONDITION - elif http_status_code == 401: - return SPANSTATUS.UNAUTHENTICATED - elif http_status_code == 409: - return SPANSTATUS.ALREADY_EXISTS - else: - return SPANSTATUS.INVALID_ARGUMENT - - elif 500 <= http_status_code < 600: - if http_status_code == 504: - return SPANSTATUS.DEADLINE_EXCEEDED - elif http_status_code == 501: - return SPANSTATUS.UNIMPLEMENTED - elif http_status_code == 503: - return SPANSTATUS.UNAVAILABLE - else: - return SPANSTATUS.INTERNAL_ERROR - - return SPANSTATUS.UNKNOWN_ERROR - - -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans", "dropped_spans") - - def __init__(self, maxlen: int) -> None: - # FIXME: this is `maxlen - 1` only to preserve historical behavior - # enforced by tests. - # Either this should be changed to `maxlen` or the JS SDK implementation - # should be changed to match a consistent interpretation of what maxlen - # limits: either transaction+spans or only child spans. - self.maxlen = maxlen - 1 - self.spans: "List[Span]" = [] - self.dropped_spans: int = 0 - - def add(self, span: "Span") -> None: - if len(self.spans) > self.maxlen: - span._span_recorder = None - self.dropped_spans += 1 - else: - self.spans.append(span) - - -class Span: - """A span holds timing information of a block of code. - Spans can have multiple child spans thus forming a span tree. - - :param trace_id: The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - :param span_id: The span ID of this span. If omitted, a new span ID will be generated. - :param parent_span_id: The span ID of the parent span, if applicable. - :param same_process_as_parent: Whether this span is in the same process as the parent span. - :param sampled: Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - :param op: The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - :param description: A description of what operation is being performed within the span. - - .. deprecated:: 2.15.0 - Please use the `name` parameter, instead. - :param name: A string describing what operation is being performed within the span. - :param hub: The hub to use for this span. - - .. deprecated:: 2.0.0 - Please use the `scope` parameter, instead. - :param status: The span's status. Possible values are listed at - https://develop.sentry.dev/sdk/event-payloads/span/ - :param containing_transaction: The transaction that this span belongs to. - :param start_timestamp: The timestamp when the span started. If omitted, the current time - will be used. - :param scope: The scope to use for this span. If not provided, we use the current scope. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "same_process_as_parent", - "sampled", - "op", - "description", - "_measurements", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "hub", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - "_flags", - "_flags_capacity", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - same_process_as_parent: bool = True, - sampled: "Optional[bool]" = None, - op: "Optional[str]" = None, - description: "Optional[str]" = None, - hub: "Optional[sentry_sdk.Hub]" = None, # deprecated - status: "Optional[str]" = None, - containing_transaction: "Optional[Transaction]" = None, - start_timestamp: "Optional[Union[datetime, float]]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - origin: str = "manual", - name: "Optional[str]" = None, - ) -> None: - self._trace_id = trace_id - self._span_id = span_id - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = name or description - self.status = status - self.hub = hub # backwards compatibility - self.scope = scope - self.origin = origin - self._measurements: "Dict[str, MeasurementValue]" = {} - self._tags: "MutableMapping[str, str]" = {} - self._data: "Dict[str, Any]" = {} - self._containing_transaction = containing_transaction - self._flags: "Dict[str, bool]" = {} - self._flags_capacity = 10 - - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - - if start_timestamp is None: - start_timestamp = datetime.now(timezone.utc) - elif isinstance(start_timestamp, float): - start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) - self.start_timestamp = start_timestamp - try: - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp: "Optional[datetime]" = None - - self._span_recorder: "Optional[_SpanRecorder]" = None - - self.update_active_thread() - self.set_profiler_id(get_profiler_id()) - - # TODO this should really live on the Transaction class rather than the Span - # class - def init_span_recorder(self, maxlen: int) -> None: - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - def __repr__(self) -> str: - return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.op, - self.description, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.origin, - ) - ) - - def __enter__(self) -> "Span": - scope = self.scope or sentry_sdk.get_current_scope() - old_span = scope.span - scope.span = self - self._context_manager_state = (scope, old_span) - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - with capture_internal_exceptions(): - scope, old_span = self._context_manager_state - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self) -> "Optional[Transaction]": - """The ``Transaction`` that this span belongs to. - The ``Transaction`` is the root of the span tree, - so one could also think of this ``Transaction`` as the "root span".""" - - # this is a getter rather than a regular attribute so that transactions - # can return `self` here instead (as a way to prevent them circularly - # referencing themselves) - return self._containing_transaction - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a sub-span from the current span or transaction. - - Takes the same arguments as the initializer of :py:class:`Span`. The - trace id, sampling decision, transaction pointer, and span recorder are - inherited from the current span/transaction. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - kwargs.setdefault("sampled", self.sampled) - - child = Span( - trace_id=self.trace_id, - parent_span_id=self.span_id, - containing_transaction=self.containing_transaction, - **kwargs, - ) - - span_recorder = ( - self.containing_transaction and self.containing_transaction._span_recorder - ) - if span_recorder: - span_recorder.add(child) - - return child - - @classmethod - def continue_from_environ( - cls, - environ: "Mapping[str, str]", - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a Transaction with the given params, then add in data pulled from - the ``sentry-trace`` and ``baggage`` headers from the environ (if any) - before returning the Transaction. - - This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` - in that it assumes header names in the form ``HTTP_HEADER_NAME`` - - such as you would get from a WSGI/ASGI environ - - rather than the form ``header-name``. - - :param environ: The ASGI/WSGI environ to pull information from. - """ - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers: "Mapping[str, str]", - *, - _sample_rand: "Optional[str]" = None, - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a transaction with the given params (including any data pulled from - the ``sentry-trace`` and ``baggage`` headers). - - :param headers: The dictionary with the HTTP headers to pull information from. - :param _sample_rand: If provided, we override the sample_rand value from the - incoming headers with this value. (internal use only) - """ - logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header( - headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand - ) - kwargs.update({BAGGAGE_HEADER_NAME: baggage}) - - sentrytrace_kwargs = extract_sentrytrace_data( - headers.get(SENTRY_TRACE_HEADER_NAME) - ) - - if sentrytrace_kwargs is not None: - kwargs.update(sentrytrace_kwargs) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. - If the span's containing transaction doesn't yet have a ``baggage`` value, - this will cause one to be generated and stored. - """ - if not self.containing_transaction: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - return - - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.containing_transaction.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - @classmethod - def from_traceparent( - cls, - traceparent: "Optional[str]", - **kwargs: "Any", - ) -> "Optional[Transaction]": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def to_baggage(self) -> "Optional[Baggage]": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with this ``Span``, if any. (Taken from the root of the span tree.) - """ - if self.containing_transaction: - return self.containing_transaction.get_baggage() - return None - - def set_tag(self, key: str, value: "Any") -> None: - self._tags[key] = value - - def set_data(self, key: str, value: "Any") -> None: - self._data[key] = value - - def update_data(self, data: "Dict[str, Any]") -> None: - self._data.update(data) - - def set_flag(self, flag: str, result: bool) -> None: - if len(self._flags) < self._flags_capacity: - self._flags[flag] = result - - def set_status(self, value: str) -> None: - self.status = value - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_thread( - self, thread_id: "Optional[int]", thread_name: "Optional[str]" - ) -> None: - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) - - def set_profiler_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status: int) -> None: - self.set_tag( - "http.status_code", str(http_status) - ) # TODO-neel remove in major, we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self) -> bool: - return self.status == "ok" - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ - if self.timestamp is not None: - # This span is already finished, ignore. - return None - - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp - else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - except AttributeError: - self.timestamp = datetime.now(timezone.utc) - - scope = scope or sentry_sdk.get_current_scope() - - # Copy conversation_id from scope to span data if this is an AI span - conversation_id = scope.get_conversation_id() - if conversation_id: - has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data - is_ai_span_op = self.op is not None and ( - self.op.startswith("ai.") or self.op.startswith("gen_ai.") - ) - if has_ai_op or is_ai_span_op: - self.set_data("gen_ai.conversation.id", conversation_id) - - maybe_create_breadcrumbs_from_span(scope, self) - - return None - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the span.""" - - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "same_process_as_parent": self.same_process_as_parent, - "op": self.op, - "description": self.description, - "start_timestamp": self.start_timestamp, - "timestamp": self.timestamp, - "origin": self.origin, - } - - if self.status: - rv["status"] = self.status - # TODO-neel remove redundant tag in major - self._tags["status"] = self.status - - if len(self._measurements) > 0: - rv["measurements"] = self._measurements - - tags = self._tags - if tags: - rv["tags"] = tags - - data = {} - data.update(self._flags) - data.update(self._data) - if data: - rv["data"] = data - - return rv - - def get_trace_context(self) -> "Any": - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "op": self.op, - "description": self.description, - "origin": self.origin, - } - if self.status: - rv["status"] = self.status - - if self.containing_transaction: - rv["dynamic_sampling_context"] = ( - self.containing_transaction.get_baggage().dynamic_sampling_context() - ) - - data = {} - - thread_id = self._data.get(SPANDATA.THREAD_ID) - if thread_id is not None: - data["thread.id"] = thread_id - - thread_name = self._data.get(SPANDATA.THREAD_NAME) - if thread_name is not None: - data["thread.name"] = thread_name - - if data: - rv["data"] = data - - return rv - - def get_profile_context(self) -> "Optional[ProfileContext]": - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - def update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -class Transaction(Span): - """The Transaction is the root element that holds all the spans - for Sentry performance instrumentation. - - :param name: Identifier of the transaction. - Will show up in the Sentry UI. - :param parent_sampled: Whether the parent transaction was sampled. - If True this transaction will be kept, if False it will be discarded. - :param baggage: The W3C baggage header value. - (see https://www.w3.org/TR/baggage/) - :param source: A string describing the source of the transaction name. - This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations - for more information. Default "custom". - :param kwargs: Additional arguments to be passed to the Span constructor. - See :py:class:`sentry_sdk.tracing.Span` for available arguments. - """ - - __slots__ = ( - "name", - "source", - "parent_sampled", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_measurements", - "_contexts", - "_profile", - "_continuous_profile", - "_baggage", - "_sample_rand", - ) - - def __init__( # type: ignore[misc] - self, - name: str = "", - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - source: str = TransactionSource.CUSTOM, - **kwargs: "Unpack[SpanKwargs]", - ) -> None: - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate: "Optional[float]" = None - self.parent_sampled = parent_sampled - self._measurements: "Dict[str, MeasurementValue]" = {} - self._contexts: "Dict[str, Any]" = {} - self._profile: "Optional[Profile]" = None - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._baggage = baggage - - baggage_sample_rand = ( - None if self._baggage is None else self._baggage._sample_rand() - ) - if baggage_sample_rand is not None: - self._sample_rand = baggage_sample_rand - else: - self._sample_rand = _generate_sample_rand(self.trace_id) - - def __repr__(self) -> str: - return ( - "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.name, - self.op, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.source, - self.origin, - ) - ) - - def _possibly_started(self) -> bool: - """Returns whether the transaction might have been started. - - If this returns False, we know that the transaction was not started - with sentry_sdk.start_transaction, and therefore the transaction will - be discarded. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self) -> "Transaction": - if not self._possibly_started(): - logger.debug( - "Transaction was entered without being started with sentry_sdk.start_transaction." - "The transaction will not be sent to Sentry. To fix, start the transaction by" - "passing it to sentry_sdk.start_transaction." - ) - - super().__enter__() - - if self._profile is not None: - self._profile.__enter__() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - - if self._continuous_profile is not None: - self._continuous_profile.stop() - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self) -> "Transaction": - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def _get_scope_from_finish_args( - self, - scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - ) -> "Optional[sentry_sdk.Scope]": - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - - def _get_log_representation(self) -> str: - return "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. - - :param scope: The Scope to use for this transaction. - If not provided, the current Scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args(scope, hub) - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be None - if self.sampled is False: - logger.debug("Discarding transaction because sampled = False") - else: - logger.debug( - "Discarding transaction because it was not started with sentry_sdk.start_transaction" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - if client.transport and has_tracing_enabled(client.options): - if client.monitor and client.monitor.downsample_factor > 0: - reason = "backpressure" - else: - reason = "sample_rate" - - client.transport.record_lost_event(reason, data_category="transaction") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) - if ( - status_code is not None - and status_code in client.options["trace_ignore_status_codes"] - ): - logger.debug( - "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( - transaction_description=self._get_log_representation(), - status_code=self._data[SPANDATA.HTTP_STATUS_CODE], - trace_ignore_status_codes=client.options[ - "trace_ignore_status_codes" - ], - ) - ) - if client.transport: - client.transport.record_lost_event( - "event_processor", data_category="transaction" - ) - - num_spans = len(self._span_recorder.spans) + 1 - client.transport.record_lost_event( - "event_processor", data_category="span", quantity=num_spans - ) - - self.sampled = False - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - if self.sampled is None: - logger.warning("Discarding transaction without sampling decision.") - - return None - - finished_spans = [] - has_gen_ai_span = False - if client.options.get("stream_gen_ai_spans", True): - for span in self._span_recorder.spans: - if span.timestamp is None: - continue - - if isinstance(span.op, str) and span.op.startswith("gen_ai."): - has_gen_ai_span = True - - finished_spans.append(span.to_json()) - else: - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - len_diff = len(self._span_recorder.spans) - len(finished_spans) - dropped_spans = len_diff + self._span_recorder.dropped_spans - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - self._span_recorder = None - - contexts = {} - contexts.update(self._contexts) - contexts.update({"trace": self.get_trace_context()}) - profile_context = self.get_profile_context() - if profile_context is not None: - contexts.update({"profile": profile_context}) - - event: "Event" = { - "type": "transaction", - "transaction": self.name, - "transaction_info": {"source": self.source}, - "contexts": contexts, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": finished_spans, - } - - if dropped_spans > 0: - event["_dropped_spans"] = dropped_spans - - if has_gen_ai_span: - event["_has_gen_ai_span"] = True - - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - - event["measurements"] = self._measurements - - return scope.capture_event(event) - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - """Sets a context. Transactions can have multiple contexts - and they should follow the format described in the "Contexts Interface" - documentation. - - :param key: The name of the context. - :param value: The information about the context. - """ - self._contexts[key] = value - - def set_http_status(self, http_status: int) -> None: - """Sets the status of the Transaction according to the given HTTP status. - - :param http_status: The HTTP status code.""" - super().set_http_status(http_status) - self.set_context("response", {"status_code": http_status}) - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the transaction.""" - rv = super().to_json() - - rv["name"] = self.name - rv["source"] = self.source - rv["sampled"] = self.sampled - - return rv - - def get_trace_context(self) -> "Any": - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self) -> "Baggage": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with the Transaction. - - The first time a new baggage with Sentry items is made, - it will be frozen.""" - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_transaction(self) - - return self._baggage - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the transaction's sampling decision, according to the following - precedence rules: - - 1. If a sampling decision is passed to `start_transaction` - (`start_transaction(name: "my transaction", sampled: True)`), that - decision will be used, regardless of anything else - - 2. If `traces_sampler` is defined, its decision will be used. It can - choose to keep or ignore any parent sampling decision, or use the - sampling context data to make its own decision or to choose a sample - rate for the transaction. - - 3. If `traces_sampler` is not defined, but there's a parent sampling - decision, the parent sampling decision will be used. - - 4. If `traces_sampler` is not defined and there's no parent sampling - decision, `traces_sample_rate` will be used. - """ - client = sentry_sdk.get_client() - - transaction_description = self._get_log_representation() - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) - # default inheritance behavior - else ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning( - "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( - transaction_description=transaction_description, - ) - ) - self.sampled = False - return - - self.sample_rate = float(sample_rate) - - if client.monitor: - self.sample_rate /= 2**client.monitor.downsample_factor - - # if the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - if not self.sample_rate: - logger.debug( - "[Tracing] Discarding {transaction_description} because {reason}".format( - transaction_description=transaction_description, - reason=( - "traces_sampler returned 0 or False" - if callable(client.options.get("traces_sampler")) - else "traces_sample_rate is set to 0" - ), - ) - ) - self.sampled = False - return - - # Now we roll the dice. - self.sampled = self._sample_rand < self.sample_rate - - if self.sampled: - logger.debug( - "[Tracing] Starting {transaction_description}".format( - transaction_description=transaction_description, - ) - ) - else: - logger.debug( - "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( - transaction_description=transaction_description, - sample_rate=self.sample_rate, - ) - ) - - # Private aliases matching StreamedSpan's private API - _get_baggage = get_baggage - _get_trace_context = get_trace_context - - -class NoOpSpan(Span): - def __repr__(self) -> str: - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self) -> "Optional[Transaction]": - return None - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "NoOpSpan": - return NoOpSpan() - - def to_traceparent(self) -> str: - return "" - - def to_baggage(self) -> "Optional[Baggage]": - return None - - def get_baggage(self) -> "Optional[Baggage]": - return None - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - return iter(()) - - def set_tag(self, key: str, value: "Any") -> None: - pass - - def set_data(self, key: str, value: "Any") -> None: - pass - - def update_data(self, data: "Dict[str, Any]") -> None: - pass - - def set_status(self, value: str) -> None: - pass - - def set_http_status(self, http_status: int) -> None: - pass - - def is_success(self) -> bool: - return True - - def to_json(self) -> "Dict[str, Any]": - return {} - - def get_trace_context(self) -> "Any": - return {} - - def get_profile_context(self) -> "Any": - return {} - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ - pass - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - pass - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - pass - - def init_span_recorder(self, maxlen: int) -> None: - pass - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - pass - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _get_baggage = get_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -if TYPE_CHECKING: - - @overload - def trace( - func: None = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": - # Handles: @trace() and @trace(op="custom") - pass - - @overload - def trace(func: "Callable[P, R]") -> "Callable[P, R]": - # Handles: @trace - pass - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a child span around a function call. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, a default operation will - be assigned based on the template. - :type op: str or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP, SPANTEMPLATE - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - op=OP.DB_QUERY, - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - - # With a custom template - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def calculate_interest_rate(amount, rate, years): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import create_span_decorator - - decorator = create_span_decorator( - op=op, - name=name, - attributes=attributes, - template=template, - ) - - if func: - return decorator(func) - else: - return decorator - - -# Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - _generate_sample_rand, - extract_sentrytrace_data, - has_tracing_enabled, - maybe_create_breadcrumbs_from_span, -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py deleted file mode 100644 index c2e34a795b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/tracing_utils.py +++ /dev/null @@ -1,1694 +0,0 @@ -import contextlib -import functools -import inspect -import os -import re -import sys -import uuid -import warnings -from collections.abc import Mapping, MutableMapping -from datetime import datetime, timedelta, timezone -from random import Random -from urllib.parse import quote, unquote - -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE -from sentry_sdk.utils import ( - _is_external_source, - _is_in_project_root, - _module_in_list, - capture_internal_exceptions, - filename_for_module, - is_sentry_url, - is_valid_sample_rate, - logger, - match_regex_list, - qualname_from_function, - safe_repr, - to_string, - try_convert, -) - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union - - from sentry_sdk._types import Attributes - - -SENTRY_TRACE_REGEX = re.compile( - "^[ \t]*" # whitespace - "([0-9a-f]{32})?" # trace_id - "-?([0-9a-f]{16})?" # span_id - "-?([01])?" # sampled - "[ \t]*$" # whitespace -) - - -# This is a normal base64 regex, modified to reflect that fact that we strip the -# trailing = or == off -base64_stripped = ( - # any of the characters in the base64 "alphabet", in multiples of 4 - "([a-zA-Z0-9+/]{4})*" - # either nothing or 2 or 3 base64-alphabet characters (see - # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for - # why there's never only 1 extra character) - "([a-zA-Z0-9+/]{2,3})?" -) - - -class EnvironHeaders(Mapping): # type: ignore - def __init__( - self, - environ: "Mapping[str, str]", - prefix: str = "HTTP_", - ) -> None: - self.environ = environ - self.prefix = prefix - - def __getitem__(self, key: str) -> "Optional[Any]": - return self.environ[self.prefix + key.replace("-", "_").upper()] - - def __len__(self) -> int: - return sum(1 for _ in iter(self)) - - def __iter__(self) -> "Generator[str, None, None]": - for k in self.environ: - if not isinstance(k, str): - continue - - k = k.replace("-", "_").upper() - if not k.startswith(self.prefix): - continue - - yield k[len(self.prefix) :] - - -def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: - """ - Returns True if either traces_sample_rate or traces_sampler is - defined and enable_tracing is set and not false. - """ - if options is None: - return False - - return bool( - options.get("enable_tracing") is not False - and ( - options.get("traces_sample_rate") is not None - or options.get("traces_sampler") is not None - ) - ) - - -def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return False - - return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" - - -def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return True - - return not options.get( - "stream_gen_ai_spans", True - ) and not has_span_streaming_enabled(options) - - -@contextlib.contextmanager -def record_sql_queries( - cursor: "Any", - query: "Any", - params_list: "Any", - paramstyle: "Optional[str]", - executemany: bool, - record_cursor_repr: bool = False, - span_origin: str = "manual", - span_op_override_value: "Optional[str]" = None, -) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": - # TODO: Bring back capturing of params by default - client = sentry_sdk.get_client() - if client.options["_experiments"].get("record_sql_params", False): - if not params_list or params_list == [None]: - params_list = None - - if paramstyle == "pyformat": - paramstyle = "format" - else: - params_list = None - paramstyle = None - - query = _format_sql(cursor, query) - - data = {} - if params_list is not None: - data["db.params"] = params_list - if paramstyle is not None: - data["db.paramstyle"] = paramstyle - if executemany: - data["db.executemany"] = True - if record_cursor_repr and cursor is not None: - data["db.cursor"] = cursor - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message=query, category="query", data=data) - - if has_span_streaming_enabled(client.options): - additional_attributes = {} - if query is not None: - additional_attributes["db.query.text"] = query - - with sentry_sdk.traces.start_span( - name="" if query is None else query, - attributes={ - "sentry.origin": span_origin, - "sentry.op": span_op_override_value - if span_op_override_value - else OP.DB, - **additional_attributes, - }, - ) as span: - yield span - else: - with sentry_sdk.start_span( - op=span_op_override_value if span_op_override_value is not None else OP.DB, - name=query, - origin=span_origin, - ) as span: - for k, v in data.items(): - span.set_data(k, v) - yield span - - -def maybe_create_breadcrumbs_from_span( - scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" -) -> None: - if span.op == OP.DB_REDIS: - scope.add_breadcrumb( - message=span.description, type="redis", category="redis", data=span._tags - ) - - elif span.op == OP.HTTP_CLIENT: - level = None - status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) - if status_code: - if 500 <= status_code <= 599: - level = "error" - elif 400 <= status_code <= 499: - level = "warning" - - if level: - scope.add_breadcrumb( - type="http", category="httplib", data=span._data, level=level - ) - else: - scope.add_breadcrumb(type="http", category="httplib", data=span._data) - - elif span.op == "subprocess": - scope.add_breadcrumb( - type="subprocess", - category="subprocess", - message=span.description, - data=span._data, - ) - - -def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": - try: - return frame.f_code.co_filename - except Exception: - return None - - -def _should_be_included( - is_sentry_sdk_frame: bool, - namespace: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", - abs_path: "Optional[str]", - project_root: "Optional[str]", -) -> bool: - # in_app_include takes precedence over in_app_exclude - should_be_included = _module_in_list(namespace, in_app_include) - should_be_excluded = _is_external_source(abs_path) or _module_in_list( - namespace, in_app_exclude - ) - return not is_sentry_sdk_frame and ( - should_be_included - or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) - ) - - -def add_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", - project_root: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", -) -> None: - """ - Adds OTel compatible source code information to the span - """ - # Find the correct frame - frame: "Union[FrameType, None]" = sys._getframe() - while frame is not None: - abs_path = _get_frame_module_abs_path(frame) - - try: - namespace: "Optional[str]" = frame.f_globals.get("__name__") - except Exception: - namespace = None - - is_sentry_sdk_frame = namespace is not None and namespace.startswith( - "sentry_sdk." - ) - - should_be_included = _should_be_included( - is_sentry_sdk_frame=is_sentry_sdk_frame, - namespace=namespace, - in_app_include=in_app_include, - in_app_exclude=in_app_exclude, - abs_path=abs_path, - project_root=project_root, - ) - if should_be_included: - break - - frame = frame.f_back - else: - frame = None - - # Set the data - if frame is not None: - try: - lineno = frame.f_lineno - except Exception: - lineno = None - if lineno is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_LINENO, lineno) - else: - span.set_attribute("code.line.number", lineno) - - try: - namespace = frame.f_globals.get("__name__") - except Exception: - namespace = None - if namespace is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_NAMESPACE, namespace) - else: - span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) - - filepath = _get_frame_module_abs_path(frame) - if filepath is not None: - if namespace is not None: - in_app_path = filename_for_module(namespace, filepath) - elif project_root is not None and filepath.startswith(project_root): - in_app_path = filepath.replace(project_root, "").lstrip(os.sep) - else: - in_app_path = filepath - - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) - else: - if in_app_path is not None: - span.set_attribute("code.file.path", in_app_path) - - try: - code_function = frame.f_code.co_name - except Exception: - code_function = None - - if code_function is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - else: - span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - - -def add_query_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a database query span - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_query_source = client.options.get("enable_db_query_source", True) - if not should_add_query_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("db_query_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def add_http_request_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a span for an outgoing HTTP request - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_request_source = client.options.get("enable_http_request_source", True) - if not should_add_request_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("http_request_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def extract_sentrytrace_data( - header: "Optional[str]", -) -> "Optional[Dict[str, Union[str, bool, None]]]": - """ - Given a `sentry-trace` header string, return a dictionary of data. - """ - if not header: - return None - - if "," in header: - # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. - parts = [part.strip() for part in header.split(",")] - header = next((part for part in parts if part), "") - - if not header: - return None - - if header.startswith("00-") and header.endswith("-00"): - header = header[3:-3] - - match = SENTRY_TRACE_REGEX.match(header) - if not match: - return None - - trace_id, parent_span_id, sampled_str = match.groups() - parent_sampled = None - - if trace_id: - trace_id = "{:032x}".format(int(trace_id, 16)) - if parent_span_id: - parent_span_id = "{:016x}".format(int(parent_span_id, 16)) - if sampled_str: - parent_sampled = sampled_str != "0" - - return { - "trace_id": trace_id, - "parent_span_id": parent_span_id, - "parent_sampled": parent_sampled, - } - - -def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": - real_sql = None - - # If we're using psycopg2, it could be that we're - # looking at a query that uses Composed objects. Use psycopg2's mogrify - # function to format the query. We lose per-parameter trimming but gain - # accuracy in formatting. - try: - if hasattr(cursor, "mogrify"): - real_sql = cursor.mogrify(sql) - if isinstance(real_sql, bytes): - real_sql = real_sql.decode(cursor.connection.encoding) - except Exception: - real_sql = None - - return real_sql or to_string(sql) - - -class PropagationContext: - """ - The PropagationContext represents the data of a trace in Sentry. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "parent_sampled", - "baggage", - "custom_sampling_context", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - dynamic_sampling_context: "Optional[Dict[str, str]]" = None, - baggage: "Optional[Baggage]" = None, - ) -> None: - self._trace_id = trace_id - """The trace id of the Sentry trace.""" - - self._span_id = span_id - """The span id of the currently executing span.""" - - self.parent_span_id = parent_span_id - """The id of the parent span that started this span. - The parent span could also be a span in an upstream service.""" - - self.parent_sampled = parent_sampled - """Boolean indicator if the parent span was sampled. - Important when the parent span originated in an upstream service, - because we want to sample the whole trace, or nothing from the trace.""" - - self.baggage = baggage - """Parsed baggage header that is used for dynamic sampling decisions.""" - - """DEPRECATED this only exists for backwards compat of constructor.""" - if baggage is None and dynamic_sampling_context is not None: - self.baggage = Baggage(dynamic_sampling_context) - - self.custom_sampling_context: "Optional[dict[str, Any]]" = None - - @classmethod - def from_incoming_data( - cls, incoming_data: "Dict[str, Any]" - ) -> "PropagationContext": - propagation_context = PropagationContext() - normalized_data = normalize_incoming_data(incoming_data) - - sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) - sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) - - # nothing to propagate if no sentry-trace - if sentrytrace_data is None: - return propagation_context - - baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) - baggage = ( - Baggage.from_incoming_header(baggage_header) if baggage_header else None - ) - - if not _should_continue_trace(baggage): - return propagation_context - - propagation_context.update(sentrytrace_data) - if baggage: - propagation_context.baggage = baggage - - propagation_context._fill_sample_rand() - - return propagation_context - - @property - def trace_id(self) -> str: - """The trace id of the Sentry trace.""" - if not self._trace_id: - # New trace, don't fill in sample_rand - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - """The span id of the currently executed span.""" - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - @property - def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": - return self.get_baggage().dynamic_sampling_context() - - def to_traceparent(self) -> str: - return f"{self.trace_id}-{self.span_id}" - - def get_baggage(self) -> "Baggage": - if self.baggage is None: - self.baggage = Baggage.populate_from_propagation_context(self) - return self.baggage - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. - """ - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def update(self, other_dict: "Dict[str, Any]") -> None: - """ - Updates the PropagationContext with data from the given dictionary. - """ - for key, value in other_dict.items(): - try: - setattr(self, key, value) - except AttributeError: - pass - - def _set_custom_sampling_context( - self, custom_sampling_context: "dict[str, Any]" - ) -> None: - self.custom_sampling_context = custom_sampling_context - - def __repr__(self) -> str: - return "".format( - self._trace_id, - self._span_id, - self.parent_span_id, - self.parent_sampled, - self.baggage, - ) - - def _fill_sample_rand(self) -> None: - """ - Ensure that there is a valid sample_rand value in the baggage. - - If there is a valid sample_rand value in the baggage, we keep it. - Otherwise, we generate a sample_rand value according to the following: - - - If we have a parent_sampled value and a sample_rate in the DSC, we compute - a sample_rand value randomly in the range: - - [0, sample_rate) if parent_sampled is True, - - or, in the range [sample_rate, 1) if parent_sampled is False. - - - If either parent_sampled or sample_rate is missing, we generate a random - value in the range [0, 1). - - The sample_rand is deterministically generated from the trace_id, if present. - - This function does nothing if there is no baggage. - """ - if self.baggage is None: - return - - sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) - if sample_rand is not None and 0 <= sample_rand < 1: - # sample_rand is present and valid, so don't overwrite it - return - - # Get the sample rate and compute the transformation that will map the random value - # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). - sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) - lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) - - try: - sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) - except ValueError: - # ValueError is raised if the interval is invalid, i.e. lower >= upper. - # lower >= upper might happen if the incoming trace's sampled flag - # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. - # We cannot generate a sensible sample_rand value in this case. - logger.debug( - f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " - f"and sample_rate={sample_rate}." - ) - return - - self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 - - def _sample_rand(self) -> "Optional[str]": - """Convenience method to get the sample_rand value from the baggage.""" - if self.baggage is None: - return None - - return self.baggage.sentry_items.get("sample_rand") - - -class Baggage: - """ - The W3C Baggage header information (see https://www.w3.org/TR/baggage/). - - Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. - Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but - it is the caller's responsibility to enforce this restriction. - """ - - __slots__ = ("sentry_items", "third_party_items", "mutable") - - SENTRY_PREFIX = "sentry-" - SENTRY_PREFIX_REGEX = re.compile("^sentry-") - - def __init__( - self, - sentry_items: "Dict[str, str]", - third_party_items: str = "", - mutable: bool = True, - ): - self.sentry_items = sentry_items - self.third_party_items = third_party_items - self.mutable = mutable - - @classmethod - def from_incoming_header( - cls, - header: "Optional[str]", - *, - _sample_rand: "Optional[str]" = None, - ) -> "Baggage": - """ - freeze if incoming header already has sentry baggage - """ - sentry_items = {} - third_party_items = "" - mutable = True - - if header: - for item in header.split(","): - if "=" not in item: - continue - - with capture_internal_exceptions(): - item = item.strip() - key, val = item.split("=", 1) - if Baggage.SENTRY_PREFIX_REGEX.match(key): - baggage_key = unquote(key.split("-")[1]) - sentry_items[baggage_key] = unquote(val) - mutable = False - else: - third_party_items += ("," if third_party_items else "") + item - - if _sample_rand is not None: - sentry_items["sample_rand"] = str(_sample_rand) - mutable = False - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": - """ - Deprecated: use populate_from_propagation_context - """ - if scope._propagation_context is None: - return Baggage({}) - - return Baggage.populate_from_propagation_context(scope._propagation_context) - - @classmethod - def populate_from_propagation_context( - cls, propagation_context: "PropagationContext" - ) -> "Baggage": - sentry_items: "Dict[str, str]" = {} - third_party_items = "" - mutable = False - - client = sentry_sdk.get_client() - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options - - sentry_items["trace_id"] = propagation_context.trace_id - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if options.get("traces_sample_rate"): - sentry_items["sample_rate"] = str(options["traces_sample_rate"]) - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def populate_from_transaction( - cls, transaction: "sentry_sdk.tracing.Transaction" - ) -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - transaction.name - and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES - ): - sentry_items["transaction"] = transaction.name - - if transaction.sample_rate is not None: - sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction.sampled is not None: - sentry_items["sampled"] = "true" if transaction.sampled else "false" - - # there's an existing baggage but it was mutable, - # which is why we are creating this new baggage. - # However, if by chance the user put some sentry items in there, give them precedence. - if transaction._baggage and transaction._baggage.sentry_items: - sentry_items.update(transaction._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - @classmethod - def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = segment.trace_id - sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - segment.get_attributes().get("sentry.span.source") - not in LOW_QUALITY_SEGMENT_SOURCES - ) and segment._name: - sentry_items["transaction"] = segment._name - - if segment._sample_rate is not None: - sentry_items["sample_rate"] = str(segment._sample_rate) - - if segment.sampled is not None: - sentry_items["sampled"] = "true" if segment.sampled else "false" - - # There's an existing baggage but it was mutable, which is why we are - # creating this new baggage. - # However, if by chance the user put some sentry items in there, give - # them precedence. - if segment._baggage and segment._baggage.sentry_items: - sentry_items.update(segment._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - def freeze(self) -> None: - self.mutable = False - - def dynamic_sampling_context(self) -> "Dict[str, str]": - header = {} - - for key, item in self.sentry_items.items(): - header[key] = item - - return header - - def serialize(self, include_third_party: bool = False) -> str: - items = [] - - for key, val in self.sentry_items.items(): - with capture_internal_exceptions(): - item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) - items.append(item) - - if include_third_party: - items.append(self.third_party_items) - - return ",".join(items) - - @staticmethod - def strip_sentry_baggage(header: str) -> str: - """Remove Sentry baggage from the given header. - - Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. - """ - return ",".join( - ( - item - for item in header.split(",") - if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) - ) - ) - - def _sample_rand(self) -> "Optional[float]": - """Convenience method to get the sample_rand value from the sentry_items. - - We validate the value and parse it as a float before returning it. The value is considered - valid if it is a float in the range [0, 1). - """ - sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) - - if sample_rand is not None and 0.0 <= sample_rand < 1.0: - return sample_rand - - return None - - def __repr__(self) -> str: - return f'' - - -def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: - """ - Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. - """ - trace_propagation_targets = client.options["trace_propagation_targets"] - - if is_sentry_url(client, url): - return False - - return match_regex_list(url, trace_propagation_targets, substring_matching=True) - - -def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": - """ - Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. - """ - data = {} - for key, value in incoming_data.items(): - if key.startswith("HTTP_"): - key = key[5:] - - key = key.replace("_", "-").lower() - data[key] = value - - return data - - -def create_span_decorator( - op: "Optional[Union[str, OP]]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Any": - """ - Create a span decorator that can wrap both sync and async functions. - - :param op: The operation type for the span. - :type op: str or :py:class:`sentry_sdk.consts.OP` or None - :param name: The name of the span. - :type name: str or None - :param attributes: Additional attributes to set on the span. - :type attributes: dict or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - """ - from sentry_sdk.scope import should_send_default_pii - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return await f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return await f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = await f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def create_streaming_span_decorator( - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Any": - """ - Create a span creating decorator that can wrap both sync and async functions. - """ - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - result = await f(*args, **kwargs) - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - return f(*args, **kwargs) - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - -def _generate_sample_rand( - trace_id: "Optional[str]", - *, - interval: "tuple[float, float]" = (0.0, 1.0), -) -> float: - """Generate a sample_rand value from a trace ID. - - The generated value will be pseudorandomly chosen from the provided - interval. Specifically, given (lower, upper) = interval, the generated - value will be in the range [lower, upper). The value has 6-digit precision, - so when printing with .6f, the value will never be rounded up. - - The pseudorandom number generator is seeded with the trace ID. - """ - lower, upper = interval - if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly - raise ValueError("Invalid interval: lower must be less than upper") - - rng = Random(trace_id) - lower_scaled = int(lower * 1_000_000) - upper_scaled = int(upper * 1_000_000) - try: - sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) - except ValueError: - # In some corner cases it might happen that the range is too small - # In that case, just take the lower bound - sample_rand_scaled = lower_scaled - - return sample_rand_scaled / 1_000_000 - - -def _sample_rand_range( - parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" -) -> "tuple[float, float]": - """ - Compute the lower (inclusive) and upper (exclusive) bounds of the range of values - that a generated sample_rand value must fall into, given the parent_sampled and - sample_rate values. - """ - if parent_sampled is None or sample_rate is None: - return 0.0, 1.0 - elif parent_sampled is True: - return 0.0, sample_rate - else: # parent_sampled is False - return sample_rate, 1.0 - - -def _get_value(source: "Any", key: str) -> "Optional[Any]": - """ - Gets a value from a source object. The source can be a dict or an object. - It is checked for dictionary keys and object attributes. - """ - value = None - if isinstance(source, dict): - value = source.get(key) - else: - if hasattr(source, key): - try: - value = getattr(source, key) - except Exception: - value = None - return value - - -def _get_span_name( - template: "Union[str, SPANTEMPLATE]", - name: str, - kwargs: "Optional[dict[str, Any]]" = None, -) -> str: - """ - Get the name of the span based on the template and the name. - """ - span_name = name - - if template == SPANTEMPLATE.AI_CHAT: - model = None - if kwargs: - for key in ("model", "model_name"): - if kwargs.get(key) and isinstance(kwargs[key], str): - model = kwargs[key] - break - - span_name = f"chat {model}" if model else "chat" - - elif template == SPANTEMPLATE.AI_AGENT: - span_name = f"invoke_agent {name}" - - elif template == SPANTEMPLATE.AI_TOOL: - span_name = f"execute_tool {name}" - - return span_name - - -def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: - """ - Get the operation of the span based on the template. - """ - mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { - SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, - SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, - SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, - } - op = mapping.get(template, OP.FUNCTION) - - return str(op) - - -def _get_input_attributes( - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Get input attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - mapping = { - "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), - "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), - "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), - "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), - "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), - "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), - "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), - "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), - } - - def _set_from_key(key: str, value: "Any") -> None: - if key in mapping: - (attribute, data_type) = mapping[key] - if value is not None and isinstance(value, data_type): - attributes[attribute] = value - - for key, value in list(kwargs.items()): - if key == "prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "user", "content": value} - ) - continue - - if key == "system_prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "system", "content": value} - ) - continue - - _set_from_key(key, value) - - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) - - # Coerce to string - if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) - - return attributes - - -def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": - """ - Get usage attributes. - """ - attributes = {} - - def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: - for key in keys: - value = _get_value(usage, key) - if value is not None and isinstance(value, int): - attributes[attribute] = value - - _set_from_keys( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - ("prompt_tokens", "input_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - ("completion_tokens", "output_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - ("total_tokens",), - ) - - return attributes - - -def _get_output_attributes( - template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> "dict[str, Any]": - """ - Get output attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - with capture_internal_exceptions(): - # Usage from result, result.usage, and result.metadata.usage - usage_candidates = [result] - - usage = _get_value(result, "usage") - usage_candidates.append(usage) - - meta = _get_value(result, "metadata") - usage = _get_value(meta, "usage") - usage_candidates.append(usage) - - for usage_candidate in usage_candidates: - if usage_candidate is not None: - attributes.update(_get_usage_attributes(usage_candidate)) - - # Response model - model_name = _get_value(result, "model") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - model_name = _get_value(result, "model_name") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - # Tool output - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) - - return attributes - - -def _set_input_attributes( - span: "Span", - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - name: str, - f: "Any", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> None: - """ - Set span input attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param f: The wrapped function. - :param args: The arguments to the wrapped function. - :param kwargs: The keyword arguments to the wrapped function. - """ - attributes: "dict[str, Any]" = {} - - if template == SPANTEMPLATE.AI_AGENT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: name, - } - elif template == SPANTEMPLATE.AI_CHAT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - } - elif template == SPANTEMPLATE.AI_TOOL: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: name, - } - - docstring = f.__doc__ - if docstring is not None: - attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - - attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.update_data(attributes or {}) - - -def _set_output_attributes( - span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> None: - """ - Set span output attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param result: The result of the wrapped function. - """ - span.update_data(_get_output_attributes(template, send_pii, result) or {}) - - -def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: - """ - Check if we should continue the incoming trace according to the strict_trace_continuation spec. - https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation - """ - - client = sentry_sdk.get_client() - parsed_dsn = client.parsed_dsn - client_org_id = parsed_dsn.org_id if parsed_dsn else None - baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None - - if ( - client_org_id is not None - and baggage_org_id is not None - and client_org_id != baggage_org_id - ): - logger.debug( - f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - strict_trace_continuation: bool = client.options.get( - "strict_trace_continuation", False - ) - if strict_trace_continuation: - if (baggage_org_id is not None and client_org_id is None) or ( - baggage_org_id is None and client_org_id is not None - ): - logger.debug( - f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - return True - - -def add_sentry_baggage_to_headers( - headers: "MutableMapping[str, str]", sentry_baggage: str -) -> None: - """Add the Sentry baggage to the headers. - - This function directly mutates the provided headers. The provided sentry_baggage - is appended to the existing baggage. If the baggage already contains Sentry items, - they are stripped out first. - """ - existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") - stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) - - separator = "," if len(stripped_existing_baggage) > 0 else "" - - headers[BAGGAGE_HEADER_NAME] = ( - stripped_existing_baggage + separator + sentry_baggage - ) - - -def _make_sampling_decision( - name: str, - attributes: "Optional[Attributes]", - scope: "sentry_sdk.Scope", -) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": - """ - Decide whether a span should be sampled. - - Returns a tuple with: - 1. the sampling decision - 2. the effective sample rate - 3. the sample rand - 4. the reason for not sampling the span, if unsampled - """ - client = sentry_sdk.get_client() - - if not has_tracing_enabled(client.options): - return False, None, None, None - - propagation_context = scope.get_active_propagation_context() - - sample_rand = None - if propagation_context.baggage is not None: - sample_rand = propagation_context.baggage._sample_rand() - if sample_rand is None: - sample_rand = _generate_sample_rand(propagation_context.trace_id) - - # If there's a traces_sampler, use that; otherwise use traces_sample_rate - traces_sampler_defined = callable(client.options.get("traces_sampler")) - if traces_sampler_defined: - sampling_context = { - "span_context": { - "name": name, - "trace_id": propagation_context.trace_id, - "parent_span_id": propagation_context.parent_span_id, - "parent_sampled": propagation_context.parent_sampled, - "attributes": dict(attributes) if attributes else {}, - }, - } - - if propagation_context.custom_sampling_context: - sampling_context.update(propagation_context.custom_sampling_context) - - sample_rate = client.options["traces_sampler"](sampling_context) - else: - if propagation_context.parent_sampled is not None: - sample_rate = propagation_context.parent_sampled - else: - sample_rate = client.options["traces_sample_rate"] - - # Validate whether the sample_rate we got is actually valid. Since - # traces_sampler is user-provided, it could return anything. - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") - return False, None, None, "sample_rate" - - sample_rate = float(sample_rate) - if not sample_rate: - if traces_sampler_defined: - reason = "traces_sampler returned 0 or False" - else: - reason = "traces_sample_rate is set to 0" - - logger.debug(f"[Tracing] Discarding {name} because {reason}") - return False, 0.0, None, "sample_rate" - - # Adjust sample rate if we're under backpressure - sample_rate_before_backpressure = sample_rate - if client.monitor: - sample_rate /= 2**client.monitor.downsample_factor - - if not sample_rate: - logger.debug(f"[Tracing] Discarding {name} because backpressure") - return False, 0.0, None, "backpressure" - - # Make the actual decision - sampled = sample_rand < sample_rate - - if sampled: - logger.debug(f"[Tracing] Starting {name}") - outcome = None - - else: - # Determine why exactly the span will not be sampled. If we've lowered - # the effective sample_rate because of backpressure, check whether the - # span would've been sampled if backpressure wasn't active. If that's the - # case, backpressure is the actual reason, otherwise just pure sampling - # rate. - if ( - sample_rate_before_backpressure != sample_rate - and sample_rand < sample_rate_before_backpressure - ): - logger.debug(f"[Tracing] Discarding {name} because backpressure") - outcome = "backpressure" - - else: - logger.debug( - f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" - ) - outcome = "sample_rate" - - return sampled, sample_rate, sample_rand, outcome - - -def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: - """Determine if a span fits one of the rules in ignore_spans.""" - client = sentry_sdk.get_client() - ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") - - if not ignore_spans: - return False - - def _matches(rule: "Any", value: "Any") -> bool: - if isinstance(rule, Pattern): - if isinstance(value, str): - return bool(rule.fullmatch(value)) - else: - return False - - return rule == value - - for rule in ignore_spans: - if isinstance(rule, (str, Pattern)): - if _matches(rule, name): - return True - - elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): - name_matches = True - attributes_match = True - - if "name" in rule: - name_matches = _matches(rule["name"], name) - - if "attributes" in rule: - attributes = attributes or {} - - for attribute, value in rule["attributes"].items(): - if attribute not in attributes or not _matches( - value, attributes[attribute] - ): - attributes_match = False - break - - if name_matches and attributes_match: - return True - - return False - - -# Circular imports -from sentry_sdk.traces import ( - LOW_QUALITY_SEGMENT_SOURCES, - StreamedSpan, -) -from sentry_sdk.traces import ( - start_span as start_streaming_span, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - LOW_QUALITY_TRANSACTION_SOURCES, - SENTRY_TRACE_HEADER_NAME, - Span, -) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py deleted file mode 100644 index 2b71fee429..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/transport.py +++ /dev/null @@ -1,1255 +0,0 @@ -import asyncio -import gzip -import io -import json -import logging -import os -import socket -import ssl -import time -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from urllib.request import getproxies - -try: - import brotli # type: ignore -except ImportError: - brotli = None - -try: - import httpcore -except ImportError: - httpcore = None # type: ignore[assignment,unused-ignore] - -try: - import h2 # noqa: F401 - - HTTP2_ENABLED = httpcore is not None -except ImportError: - HTTP2_ENABLED = False - -try: - import anyio # noqa: F401 - - ASYNC_TRANSPORT_AVAILABLE = httpcore is not None -except ImportError: - ASYNC_TRANSPORT_AVAILABLE = False - -from typing import TYPE_CHECKING, Dict, List, cast - -import certifi -import urllib3 - -import sentry_sdk -from sentry_sdk.consts import EndpointType -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - logger, - mark_sentry_task_internal, -) -from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - DefaultDict, - Iterable, - Mapping, - Optional, - Self, - Tuple, - Type, - Union, - ) - - from urllib3.poolmanager import PoolManager, ProxyManager - - from sentry_sdk._types import Event, EventDataCategory - -KEEP_ALIVE_SOCKET_OPTIONS = [] -for option in [ - (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 -]: - try: - KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) - except AttributeError: - # a specific option might not be available on specific systems, - # e.g. TCP_KEEPIDLE doesn't exist on macOS - pass - - -def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": - """Case-insensitive header lookup for httpcore-style responses.""" - header_lower = header.lower() - return next( - ( - val.decode("ascii") - for key, val in response.headers - if key.decode("ascii").lower() == header_lower - ), - None, - ) - - -class Transport(ABC): - """Baseclass for all transports. - - A transport is used to send an event to sentry. - """ - - parsed_dsn: "Optional[Dsn]" = None - - def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: - self.options = options - if options and options["dsn"]: - self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) - else: - self.parsed_dsn = None - - def capture_event(self: "Self", event: "Event") -> None: - """ - DEPRECATED: Please use capture_envelope instead. - - This gets invoked with the event dictionary when an event should - be sent to sentry. - """ - - warnings.warn( - "capture_event is deprecated, please use capture_envelope instead!", - DeprecationWarning, - stacklevel=2, - ) - - envelope = Envelope() - envelope.add_event(event) - self.capture_envelope(envelope) - - @abstractmethod - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - """ - Send an envelope to Sentry. - - Envelopes are a data container format that can hold any type of data - submitted to Sentry. We use it to send all event data (including errors, - transactions, crons check-ins, etc.) to Sentry. - """ - pass - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Any]" = None, - ) -> None: - """ - Wait `timeout` seconds for the current events to be sent out. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def kill(self: "Self") -> None: - """ - Forcefully kills the transport. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - """This increments a counter for event loss by reason and - data category by the given positive-int quantity (default 1). - - If an item is provided, the data category and quantity are - extracted from the item, and the values passed for - data_category and quantity are ignored. - - When recording a lost transaction via data_category="transaction", - the calling code should also record the lost spans via this method. - When recording lost spans, `quantity` should be set to the number - of contained spans, plus one for the transaction itself. When - passing an Item containing a transaction via the `item` parameter, - this method automatically records the lost spans. - """ - return None - - def is_healthy(self: "Self") -> bool: - return True - - -def _parse_rate_limits( - header: str, now: "Optional[datetime]" = None -) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": - if now is None: - now = datetime.now(timezone.utc) - - for limit in header.split(","): - try: - parameters = limit.strip().split(":") - retry_after_val, categories = parameters[:2] - - retry_after = now + timedelta(seconds=int(retry_after_val)) - for category in categories and categories.split(";") or (None,): - yield category, retry_after # type: ignore - except (LookupError, ValueError): - continue - - -class HttpTransportCore(Transport): - """Shared base class for sync and async transports.""" - - TIMEOUT = 30 # seconds - - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - from sentry_sdk.consts import VERSION - - Transport.__init__(self, options) - assert self.parsed_dsn is not None - self.options: "Dict[str, Any]" = options - self._worker = self._create_worker(options) - self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) - self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} - # We only use this Retry() class for the `get_retry_after` method it exposes - self._retry = urllib3.util.Retry() - self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( - defaultdict(int) - ) - self._last_client_report_sent = time.time() - - self._pool = self._make_pool() - - # Backwards compatibility for deprecated `self.hub_class` attribute - self._hub_cls = sentry_sdk.Hub - - experiments = options.get("_experiments", {}) - compression_level = experiments.get( - "transport_compression_level", - experiments.get("transport_zlib_compression_level"), - ) - compression_algo = experiments.get( - "transport_compression_algo", - ( - "gzip" - # if only compression level is set, assume gzip for backwards compatibility - # if we don't have brotli available, fallback to gzip - if compression_level is not None or brotli is None - else "br" - ), - ) - - if compression_algo == "br" and brotli is None: - logger.warning( - "You asked for brotli compression without the Brotli module, falling back to gzip -9" - ) - compression_algo = "gzip" - compression_level = None - - if compression_algo not in ("br", "gzip"): - logger.warning( - "Unknown compression algo %s, disabling compression", compression_algo - ) - self._compression_level = 0 - self._compression_algo = None - else: - self._compression_algo = compression_algo - - if compression_level is not None: - self._compression_level = compression_level - elif self._compression_algo == "gzip": - self._compression_level = 9 - elif self._compression_algo == "br": - self._compression_level = 4 - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - raise NotImplementedError() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - if not self.options["send_client_reports"]: - return - - if item is not None: - data_category = item.data_category - quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). - - if data_category == "transaction": - # Also record the lost spans - event = item.get_transaction_event() or {} - - # +1 for the transaction itself - span_count = ( - len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 - ) - self.record_lost_event(reason, "span", quantity=span_count) - - elif data_category == "log_item" and item: - # Also record size of lost logs in bytes - bytes_size = len(item.get_bytes()) - self.record_lost_event(reason, "log_byte", quantity=bytes_size) - - elif data_category == "attachment": - # quantity of 0 is actually 1 as we do not want to count - # empty attachments as actually empty. - quantity = len(item.get_bytes()) or 1 - - elif data_category is None: - raise TypeError("data category not provided") - - self._discarded_events[data_category, reason] += quantity - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return response.headers.get(header) - - def _update_rate_limits( - self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" - ) -> None: - # new sentries with more rate limit insights. We honor this header - # no matter of the status code to update our internal rate limits. - header = self._get_header_value(response, "x-sentry-rate-limits") - if header: - logger.warning("Rate-limited via x-sentry-rate-limits") - self._disabled_until.update(_parse_rate_limits(header)) - - # old sentries only communicate global rate limit hits via the - # retry-after header on 429. This header can also be emitted on new - # sentries if a proxy in front wants to globally slow things down. - elif response.status == 429: - logger.warning("Rate-limited via 429") - retry_after_value = self._get_header_value(response, "Retry-After") - retry_after = ( - self._retry.parse_retry_after(retry_after_value) - if retry_after_value is not None - else None - ) or 60 - self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( - seconds=retry_after - ) - - def _handle_request_error( - self: "Self", - envelope: "Optional[Envelope]", - loss_reason: str = "network", - record_reason: str = "network_error", - ) -> None: - def record_loss(reason: str) -> None: - if envelope is None: - self.record_lost_event(reason, data_category="error") - else: - for item in envelope.items: - self.record_lost_event(reason, item=item) - - self.on_dropped_event(loss_reason) - record_loss(record_reason) - - def _handle_response( - self: "Self", - response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", - envelope: "Optional[Envelope]", - ) -> None: - self._update_rate_limits(response) - - if response.status == 413: - size_exceeded_message = ( - "HTTP 413: Event dropped due to exceeded envelope size limit" - ) - response_message = getattr( - response, "data", getattr(response, "content", None) - ) - if response_message is not None: - size_exceeded_message += f" (body: {response_message})" - - logger.error(size_exceeded_message) - self._handle_request_error( - envelope=envelope, loss_reason="status_413", record_reason="send_error" - ) - - elif response.status == 429: - # if we hit a 429. Something was rate limited but we already - # acted on this in `self._update_rate_limits`. Note that we - # do not want to record event loss here as we will have recorded - # an outcome in relay already. - self.on_dropped_event("status_429") - pass - - elif response.status >= 300 or response.status < 200: - logger.error( - "Unexpected status code: %s (body: %s)", - response.status, - getattr(response, "data", getattr(response, "content", None)), - ) - self._handle_request_error( - envelope=envelope, loss_reason="status_{}".format(response.status) - ) - - def _update_headers( - self: "Self", - headers: "Dict[str, str]", - ) -> None: - headers.update( - { - "User-Agent": str(self._auth.client), - "X-Sentry-Auth": str(self._auth.to_header()), - } - ) - - def on_dropped_event(self: "Self", _reason: str) -> None: - return None - - def _fetch_pending_client_report( - self: "Self", force: bool = False, interval: int = 60 - ) -> "Optional[Item]": - if not self.options["send_client_reports"]: - return None - - if not (force or self._last_client_report_sent < time.time() - interval): - return None - - discarded_events = self._discarded_events - self._discarded_events = defaultdict(int) - self._last_client_report_sent = time.time() - - if not discarded_events: - return None - - return Item( - PayloadRef( - json={ - "timestamp": time.time(), - "discarded_events": [ - {"reason": reason, "category": category, "quantity": quantity} - for ( - (category, reason), - quantity, - ) in discarded_events.items() - ], - } - ), - type="client_report", - ) - - def _check_disabled(self, category: str) -> bool: - def _disabled(bucket: "Any") -> bool: - ts = self._disabled_until.get(bucket) - return ts is not None and ts > datetime.now(timezone.utc) - - return _disabled(category) or _disabled(None) - - def _is_rate_limited(self: "Self") -> bool: - return any( - ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() - ) - - def _is_worker_full(self: "Self") -> bool: - return self._worker.full() - - def is_healthy(self: "Self") -> bool: - return not (self._is_worker_full() or self._is_rate_limited()) - - def _prepare_envelope( - self: "Self", envelope: "Envelope" - ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": - # remove all items from the envelope which are over quota - new_items = [] - for item in envelope.items: - if self._check_disabled(item.data_category): - if item.data_category in ("transaction", "error", "default", "statsd"): - self.on_dropped_event("self_rate_limits") - self.record_lost_event("ratelimit_backoff", item=item) - else: - new_items.append(item) - - # Since we're modifying the envelope here make a copy so that others - # that hold references do not see their envelope modified. - envelope = Envelope(headers=envelope.headers, items=new_items) - - if not envelope.items: - return None - - # since we're already in the business of sending out an envelope here - # check if we have one pending for the stats session envelopes so we - # can attach it to this enveloped scheduled for sending. This will - # currently typically attach the client report to the most recent - # session update. - client_report_item = self._fetch_pending_client_report(interval=30) - if client_report_item is not None: - envelope.items.append(client_report_item) - - content_encoding, body = self._serialize_envelope(envelope) - - assert self.parsed_dsn is not None - logger.debug( - "Sending envelope [%s] project:%s host:%s", - envelope.description, - self.parsed_dsn.project_id, - self.parsed_dsn.host, - ) - - headers: "Dict[str, str]" = { - "Content-Type": "application/x-sentry-envelope", - } - if content_encoding: - headers["Content-Encoding"] = content_encoding - - return envelope, body, headers - - def _serialize_envelope( - self: "Self", envelope: "Envelope" - ) -> "tuple[Optional[str], io.BytesIO]": - content_encoding = None - body = io.BytesIO() - if self._compression_level == 0 or self._compression_algo is None: - envelope.serialize_into(body) - else: - content_encoding = self._compression_algo - if self._compression_algo == "br" and brotli is not None: - body.write( - brotli.compress( - envelope.serialize(), quality=self._compression_level - ) - ) - else: # assume gzip as we sanitize the algo value in init - with gzip.GzipFile( - fileobj=body, mode="w", compresslevel=self._compression_level - ) as f: - envelope.serialize_into(f) - - return content_encoding, body - - def _get_httpcore_pool_options( - self: "Self", http2: bool = False - ) -> "Dict[str, Any]": - """Shared pool options for httpcore-based transports (Http2 and Async).""" - options: "Dict[str, Any]" = { - "http2": http2, - "retries": 3, - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - ssl_context = ssl.create_default_context() - ssl_context.load_verify_locations( - self.options["ca_certs"] - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") - key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") - if cert_file is not None: - ssl_context.load_cert_chain(cert_file, key_file) - - options["ssl_context"] = ssl_context - return options - - def _resolve_proxy(self: "Self") -> "Optional[str]": - """Resolve proxy URL from options and environment. Returns proxy URL or None.""" - if self.parsed_dsn is None: - return None - - no_proxy = self._in_no_proxy(self.parsed_dsn) - proxy = None - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - return proxy or None - - @property - def _timeout_extensions(self: "Self") -> "Dict[str, Any]": - return { - "timeout": { - "pool": self.TIMEOUT, - "connect": self.TIMEOUT, - "write": self.TIMEOUT, - "read": self.TIMEOUT, - } - } - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - raise NotImplementedError() - - def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: - no_proxy = getproxies().get("no") - if not no_proxy: - return False - for host in no_proxy.split(","): - host = host.strip() - if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): - return True - return False - - def _make_pool( - self: "Self", - ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - raise NotImplementedError() - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": - raise NotImplementedError() - - def kill(self: "Self") -> None: - logger.debug("Killing HTTP transport") - self._worker.kill() - - -# Keep BaseHttpTransport as an alias for backwards compatibility -# and for the sync transport implementation -class BaseHttpTransport(HttpTransportCore): - """The base HTTP transport (synchronous).""" - - def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - response.close() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return BackgroundWorker(queue_size=options["transport_queue_size"]) - - def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def capture_envelope( - self, - envelope: "Envelope", - ) -> None: - def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - self._send_envelope(envelope) - self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - self._worker.flush(timeout, callback) - - @staticmethod - def _warn_hub_cls() -> None: - """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" - warnings.warn( - "The `hub_cls` attribute is deprecated and will be removed in a future release.", - DeprecationWarning, - stacklevel=3, - ) - - @property - def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - return self._hub_cls - - @hub_cls.setter - def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - self._hub_cls = value - - -class HttpTransport(BaseHttpTransport): - if TYPE_CHECKING: - _pool: "Union[PoolManager, ProxyManager]" - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - num_pools = self.options.get("_experiments", {}).get("transport_num_pools") - options = { - "num_pools": 2 if num_pools is None else int(num_pools), - "cert_reqs": "CERT_REQUIRED", - "timeout": urllib3.Timeout(total=self.TIMEOUT), - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if self.options["keep_alive"]: - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - options["ca_certs"] = ( - self.options["ca_certs"] # User-provided bundle from the SDK init - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - - options["cert_file"] = self.options["cert_file"] or os.environ.get( - "CLIENT_CERT_FILE" - ) - options["key_file"] = self.options["key_file"] or os.environ.get( - "CLIENT_KEY_FILE" - ) - - return options - - def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = None - no_proxy = self._in_no_proxy(self.parsed_dsn) - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - use_socks_proxy = True - try: - # Check if PySocks dependency is available - from urllib3.contrib.socks import SOCKSProxyManager - except ImportError: - use_socks_proxy = False - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", - proxy, - ) - - if use_socks_proxy: - return SOCKSProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - else: - return urllib3.ProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "urllib3.BaseHTTPResponse": - return self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - body=body, - headers=headers, - ) - - -class AsyncHttpTransport(HttpTransportCore): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - if not ASYNC_TRANSPORT_AVAILABLE: - raise RuntimeError( - "AsyncHttpTransport requires httpcore[asyncio]. " - "Install it with: pip install sentry-sdk[asyncio]" - ) - super().__init__(options) - # Requires event loop at init time - self.loop = asyncio.get_running_loop() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return AsyncWorker(queue_size=options["transport_queue_size"]) - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - async def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - await self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - async def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = await self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - await response.aclose() - - async def _request( # type: ignore[override] - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - return await self._pool.request( # type: ignore[misc,unused-ignore] - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - - async def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def _capture_envelope(self: "Self", envelope: "Envelope") -> None: - async def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - await self._send_envelope(envelope) - await self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - # Synchronous entry point - if self.loop and self.loop.is_running(): - self.loop.call_soon_threadsafe(self._capture_envelope, envelope) - else: - # The event loop is no longer running - logger.warning("Async Transport is not running in an event loop.") - self.on_dropped_event("internal_sdk_error") - for item in envelope.items: - self.record_lost_event("internal_sdk_error", item=item) - - def flush( # type: ignore[override] - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> "Optional[asyncio.Task[None]]": - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] - return None - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=HTTP2_ENABLED - and self.parsed_dsn is not None - and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - socks_opts = opts.copy() - if "socket_options" in socks_opts: - socket_options = socks_opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) - - return httpcore.AsyncConnectionPool(**opts) - - def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] - logger.debug("Killing HTTP transport") - self._worker.kill() - try: - # Return the pool cleanup task so caller can await it if needed - with mark_sentry_task_internal(): - return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] - except RuntimeError: - logger.warning("Event loop not running, aborting kill.") - return None - - -if not HTTP2_ENABLED: - # Sorry, no Http2Transport for you - class Http2Transport(HttpTransport): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - super().__init__(options) - logger.warning( - "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." - ) - -else: - - class Http2Transport(BaseHttpTransport): # type: ignore - """The HTTP2 transport based on httpcore.""" - - TIMEOUT = 15 - - if TYPE_CHECKING: - _pool: """Union[ - httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool - ]""" - - def _get_header_value( - self: "Self", response: "httpcore.Response", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - response = self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - return response - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - if "socket_options" in opts: - socket_options = opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.SOCKSProxy(proxy_url=proxy, **opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.HTTPProxy(proxy_url=proxy, **opts) - - return httpcore.ConnectionPool(**opts) - - -class _EnvelopePrinterTransport(Transport): - """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" - - def __init__(self, transport: "Transport") -> None: - Transport.__init__(self, options=transport.options) - self._inner = transport - self.parsed_dsn = transport.parsed_dsn - - self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") - self.envelope_logger.setLevel(logging.INFO) - self.envelope_logger.propagate = False - if not self.envelope_logger.handlers: - handler = logging.StreamHandler() - handler.setLevel(logging.INFO) - handler.setFormatter(logging.Formatter("%(message)s")) - self.envelope_logger.addHandler(handler) - - @property # type: ignore[misc] - def __class__(self) -> type: - return self._inner.__class__ - - def capture_envelope(self, envelope: "Envelope") -> None: - try: - self.envelope_logger.info("--- Sentry Envelope ---") - self.envelope_logger.info( - "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) - ) - for item in envelope.items: - self.envelope_logger.info(" Item type: %s", item.type) - self.envelope_logger.info( - " Item headers: %s", - json.dumps(item.headers, indent=2, default=str), - ) - try: - payload = json.loads(item.get_bytes()) - self.envelope_logger.info( - " Payload:\n%s", - json.dumps(payload, indent=2, default=str), - ) - except (ValueError, TypeError): - self.envelope_logger.info( - " Payload: ", - len(item.get_bytes()), - ) - self.envelope_logger.info("--- End Envelope ---") - except Exception: - pass - - self._inner.capture_envelope(envelope) - - def flush( - self, - timeout: float, - callback: "Optional[Any]" = None, - ) -> "Any": - return self._inner.flush(timeout, callback) - - def kill(self) -> "Any": - return self._inner.kill() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - self._inner.record_lost_event(reason, data_category, item, quantity=quantity) - - def is_healthy(self) -> bool: - return self._inner.is_healthy() - - def __getattr__(self, name: str) -> "Any": - return getattr(self._inner, name) - - -class _FunctionTransport(Transport): - """ - DEPRECATED: Users wishing to provide a custom transport should subclass - the Transport class, rather than providing a function. - """ - - def __init__( - self, - func: "Callable[[Event], None]", - ) -> None: - Transport.__init__(self) - self._func = func - - def capture_event( - self, - event: "Event", - ) -> None: - self._func(event) - return None - - def capture_envelope(self, envelope: "Envelope") -> None: - # Since function transports expect to be called with an event, we need - # to iterate over the envelope and call the function for each event, via - # the deprecated capture_event method. - event = envelope.get_event() - if event is not None: - self.capture_event(event) - - -def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": - ref_transport = options["transport"] - - use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) - use_async_transport = options.get("_experiments", {}).get("transport_async", False) - async_integration = any( - integration.__class__.__name__ == "AsyncioIntegration" - for integration in options.get("integrations") or [] - ) - - # By default, we use the http transport class - transport_cls: "Type[Transport]" = ( - Http2Transport if use_http2_transport else HttpTransport - ) - - if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: - try: - asyncio.get_running_loop() - if async_integration: - if use_http2_transport: - logger.warning( - "HTTP/2 transport is not supported with async transport. " - "Ignoring transport_http2 experiment." - ) - transport_cls = AsyncHttpTransport - else: - logger.warning( - "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." - ) - except RuntimeError: - # No event loop running, fall back to sync transport - logger.warning("No event loop running, falling back to sync transport.") - elif use_async_transport: - logger.warning( - "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." - ) - - transport: "Optional[Transport]" = None - - if isinstance(ref_transport, Transport): - transport = ref_transport - elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): - transport_cls = ref_transport - elif callable(ref_transport): - warnings.warn( - "Function transports are deprecated and will be removed in a future release." - "Please provide a Transport instance or subclass, instead.", - DeprecationWarning, - stacklevel=2, - ) - transport = _FunctionTransport(ref_transport) - - # if a transport class is given only instantiate it if the dsn is not - # empty or None - if transport is None and options["dsn"]: - transport = transport_cls(options) - - if transport is not None and os.environ.get( - "SENTRY_PRINT_ENVELOPES", "" - ).lower() in ("1", "true", "yes"): - transport = _EnvelopePrinterTransport(transport) - - return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py deleted file mode 100644 index dff91e3719..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/types.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This module contains type definitions for the Sentry SDK's public API. -The types are re-exported from the internal module `sentry_sdk._types`. - -Disclaimer: Since types are a form of documentation, type definitions -may change in minor releases. Removing a type would be considered a -breaking change, and so we will only remove type definitions in major -releases. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Re-export types to make them available in the public API - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - EventDataCategory, - Hint, - Log, - Metric, - MonitorConfig, - SamplingContext, - ) -else: - from typing import Any - - # The lines below allow the types to be imported from outside `if TYPE_CHECKING` - # guards. The types in this module are only intended to be used for type hints. - Breadcrumb = Any - BreadcrumbHint = Any - Event = Any - EventDataCategory = Any - Hint = Any - Log = Any - MonitorConfig = Any - SamplingContext = Any - Metric = Any - - -__all__ = ( - "Breadcrumb", - "BreadcrumbHint", - "Event", - "EventDataCategory", - "Hint", - "Log", - "MonitorConfig", - "SamplingContext", - "Metric", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py deleted file mode 100644 index 0963015351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/utils.py +++ /dev/null @@ -1,2178 +0,0 @@ -import base64 -import copy -import json -import linecache -import logging -import math -import os -import random -import re -import subprocess -import sys -import threading -import time -from collections import namedtuple -from contextlib import contextmanager -from datetime import datetime, timezone -from decimal import Decimal -from functools import partial, partialmethod, wraps -from numbers import Real -from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit - -try: - # Python 3.11 - from builtins import BaseExceptionGroup -except ImportError: - # Python 3.10 and below - BaseExceptionGroup = None # type: ignore - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._compat import PY37 -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue -from sentry_sdk.consts import ( - DEFAULT_ADD_FULL_STACK, - DEFAULT_MAX_STACK_FRAMES, - EndpointType, -) - -if TYPE_CHECKING: - from types import FrameType, TracebackType - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Iterator, - List, - NoReturn, - Optional, - ParamSpec, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from gevent.hub import Hub - - from sentry_sdk._types import ( - AttributeValue, - Event, - ExcInfo, - Hint, - Log, - Metric, - SerializedAttributeValue, - SpanJSON, - ) - - P = ParamSpec("P") - R = TypeVar("R") - - -epoch = datetime(1970, 1, 1) - -# The logger is created here but initialized in the debug support module -logger = logging.getLogger("sentry_sdk.errors") - -_installed_modules = None - -BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") - -FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) -TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) - -MAX_STACK_FRAMES = 2000 -"""Maximum number of stack frames to send to Sentry. - -If we have more than this number of stack frames, we will stop processing -the stacktrace to avoid getting stuck in a long-lasting loop. This value -exceeds the default sys.getrecursionlimit() of 1000, so users will only -be affected by this limit if they have a custom recursion limit. -""" - - -def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": - """Casts an ENV variable value to boolean using the constants defined above. - In strict mode, it may return None if the value doesn't match any of the predefined values. - """ - normalized = str(value).lower() if value is not None else None - - if normalized in FALSY_ENV_VALUES: - return False - - if normalized in TRUTHY_ENV_VALUES: - return True - - return None if strict else bool(value) - - -def json_dumps(data: "Any") -> bytes: - """Serialize data into a compact JSON representation encoded as UTF-8.""" - return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") - - -def get_git_revision() -> "Optional[str]": - try: - with open(os.path.devnull, "w+") as null: - # prevent command prompt windows from popping up on windows - startupinfo = None - if sys.platform == "win32" or sys.platform == "cygwin": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - revision = ( - subprocess.Popen( - ["git", "rev-parse", "HEAD"], - startupinfo=startupinfo, - stdout=subprocess.PIPE, - stderr=null, - stdin=null, - ) - .communicate()[0] - .strip() - .decode("utf-8") - ) - except (OSError, IOError, FileNotFoundError): - return None - - return revision - - -def get_default_release() -> "Optional[str]": - """Try to guess a default release.""" - release = os.environ.get("SENTRY_RELEASE") - if release: - return release - - release = get_git_revision() - if release: - return release - - for var in ( - "HEROKU_BUILD_COMMIT", - "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility - "SOURCE_VERSION", - "CODEBUILD_RESOLVED_SOURCE_VERSION", - "CIRCLE_SHA1", - "GAE_DEPLOYMENT_ID", - "K_REVISION", - ): - release = os.environ.get(var) - if release: - return release - return None - - -def get_sdk_name(installed_integrations: "List[str]") -> str: - """Return the SDK name including the name of the used web framework.""" - - # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier - # here because if django is not installed the integration is not accessible. - framework_integrations = [ - "django", - "flask", - "fastapi", - "bottle", - "falcon", - "quart", - "sanic", - "starlette", - "litestar", - "starlite", - "chalice", - "serverless", - "pyramid", - "tornado", - "aiohttp", - "aws_lambda", - "gcp", - "beam", - "asgi", - "wsgi", - ] - - for integration in framework_integrations: - if integration in installed_integrations: - return "sentry.python.{}".format(integration) - - return "sentry.python" - - -class CaptureInternalException: - __slots__ = () - - def __enter__(self) -> "ContextManager[Any]": - return self - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> bool: - if ty is not None and value is not None: - capture_internal_exception((ty, value, tb)) - - return True - - -_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() - - -def capture_internal_exceptions() -> "ContextManager[Any]": - return _CAPTURE_INTERNAL_EXCEPTION - - -def capture_internal_exception(exc_info: "ExcInfo") -> None: - """ - Capture an exception that is likely caused by a bug in the SDK - itself. - - These exceptions do not end up in Sentry and are just logged instead. - """ - if sentry_sdk.get_client().is_active(): - logger.error("Internal error in sentry_sdk", exc_info=exc_info) - - -def to_timestamp(value: "datetime") -> float: - return (value - epoch).total_seconds() - - -def format_timestamp(value: "datetime") -> str: - """Formats a timestamp in RFC 3339 format. - - Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. - """ - utctime = value.astimezone(timezone.utc) - - # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for - # several years now), and isoformat is slightly different. - return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - -ISO_TZ_SEPARATORS = frozenset(("+", "-")) - - -def datetime_from_isoformat(value: str) -> "datetime": - try: - result = datetime.fromisoformat(value) - except (AttributeError, ValueError): - # py 3.6 - timestamp_format = ( - "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" - ) - if value.endswith("Z"): - value = value[:-1] + "+0000" - - if value[-6] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - value = value[:-3] + value[-2:] - elif value[-5] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - - result = datetime.strptime(value, timestamp_format) - return result.astimezone(timezone.utc) - - -def event_hint_with_exc_info( - exc_info: "Optional[ExcInfo]" = None, -) -> "Dict[str, Optional[ExcInfo]]": - """Creates a hint with the exc info filled in.""" - if exc_info is None: - exc_info = sys.exc_info() - else: - exc_info = exc_info_from_error(exc_info) - if exc_info[0] is None: - exc_info = None - return {"exc_info": exc_info} - - -class BadDsn(ValueError): - """Raised on invalid DSNs.""" - - -class Dsn: - """Represents a DSN.""" - - ORG_ID_REGEX = re.compile(r"^o(\d+)\.") - - def __init__( - self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None - ) -> None: - if isinstance(value, Dsn): - self.__dict__ = dict(value.__dict__) - return - parts = urlsplit(str(value)) - - if parts.scheme not in ("http", "https"): - raise BadDsn("Unsupported scheme %r" % parts.scheme) - self.scheme = parts.scheme - - if parts.hostname is None: - raise BadDsn("Missing hostname") - - self.host = parts.hostname - - if org_id is not None: - self.org_id: "Optional[str]" = org_id - else: - org_id_match = Dsn.ORG_ID_REGEX.match(self.host) - self.org_id = org_id_match.group(1) if org_id_match else None - - if parts.port is None: - self.port: int = self.scheme == "https" and 443 or 80 - else: - self.port = parts.port - - if not parts.username: - raise BadDsn("Missing public key") - - self.public_key = parts.username - self.secret_key = parts.password - - path = parts.path.rsplit("/", 1) - - try: - self.project_id = str(int(path.pop())) - except (ValueError, TypeError): - raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) - - self.path = "/".join(path) + "/" - - @property - def netloc(self) -> str: - """The netloc part of a DSN.""" - rv = self.host - if (self.scheme, self.port) not in (("http", 80), ("https", 443)): - rv = "%s:%s" % (rv, self.port) - return rv - - def to_auth(self, client: "Optional[Any]" = None) -> "Auth": - """Returns the auth info object for this dsn.""" - return Auth( - scheme=self.scheme, - host=self.netloc, - path=self.path, - project_id=self.project_id, - public_key=self.public_key, - secret_key=self.secret_key, - client=client, - ) - - def __str__(self) -> str: - return "%s://%s%s@%s%s%s" % ( - self.scheme, - self.public_key, - self.secret_key and "@" + self.secret_key or "", - self.netloc, - self.path, - self.project_id, - ) - - -class Auth: - """Helper object that represents the auth info.""" - - def __init__( - self, - scheme: str, - host: str, - project_id: str, - public_key: str, - secret_key: "Optional[str]" = None, - version: int = 7, - client: "Optional[Any]" = None, - path: str = "/", - ) -> None: - self.scheme = scheme - self.host = host - self.path = path - self.project_id = project_id - self.public_key = public_key - self.secret_key = secret_key - self.version = version - self.client = client - - def get_api_url( - self, - type: "EndpointType" = EndpointType.ENVELOPE, - ) -> str: - """Returns the API url for storing events.""" - return "%s://%s%sapi/%s/%s/" % ( - self.scheme, - self.host, - self.path, - self.project_id, - type.value, - ) - - def to_header(self) -> str: - """Returns the auth header a string.""" - rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] - if self.client is not None: - rv.append(("sentry_client", self.client)) - if self.secret_key is not None: - rv.append(("sentry_secret", self.secret_key)) - return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) - - -def get_type_name(cls: "Optional[type]") -> "Optional[str]": - return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) - - -def get_type_module(cls: "Optional[type]") -> "Optional[str]": - mod = getattr(cls, "__module__", None) - if mod not in (None, "builtins", "__builtins__"): - return mod - return None - - -def should_hide_frame(frame: "FrameType") -> bool: - try: - mod = frame.f_globals["__name__"] - if mod.startswith("sentry_sdk."): - return True - except (AttributeError, KeyError): - pass - - for flag_name in "__traceback_hide__", "__tracebackhide__": - try: - if frame.f_locals[flag_name]: - return True - except Exception: - pass - - return False - - -def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": - tb_: "Optional[TracebackType]" = tb - while tb_ is not None: - if not should_hide_frame(tb_.tb_frame): - yield tb_ - tb_ = tb_.tb_next - - -def get_lines_from_file( - filename: str, - lineno: int, - max_length: "Optional[int]" = None, - loader: "Optional[Any]" = None, - module: "Optional[str]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - context_lines = 5 - source = None - if loader is not None and hasattr(loader, "get_source"): - try: - source_str: "Optional[str]" = loader.get_source(module) - except (ImportError, IOError): - source_str = None - if source_str is not None: - source = source_str.splitlines() - - if source is None: - try: - source = linecache.getlines(filename) - except (OSError, IOError): - return [], None, [] - - if not source: - return [], None, [] - - lower_bound = max(0, lineno - context_lines) - upper_bound = min(lineno + 1 + context_lines, len(source)) - - try: - pre_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[lower_bound:lineno] - ] - context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) - post_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[(lineno + 1) : upper_bound] - ] - return pre_context, context_line, post_context - except IndexError: - # the file may have changed since it was loaded into memory - return [], None, [] - - -def get_source_context( - frame: "FrameType", - tb_lineno: "Optional[int]", - max_value_length: "Optional[int]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - try: - abs_path: "Optional[str]" = frame.f_code.co_filename - except Exception: - abs_path = None - try: - module = frame.f_globals["__name__"] - except Exception: - return [], None, [] - try: - loader = frame.f_globals["__loader__"] - except Exception: - loader = None - - if tb_lineno is not None and abs_path: - lineno = tb_lineno - 1 - return get_lines_from_file( - abs_path, lineno, max_value_length, loader=loader, module=module - ) - - return [], None, [] - - -def safe_str(value: "Any") -> str: - try: - return str(value) - except Exception: - return safe_repr(value) - - -def safe_repr(value: "Any") -> str: - try: - return repr(value) - except Exception: - return "" - - -def filename_for_module( - module: "Optional[str]", abs_path: "Optional[str]" -) -> "Optional[str]": - if not abs_path or not module: - return abs_path - - try: - if abs_path.endswith(".pyc"): - abs_path = abs_path[:-1] - - base_module = module.split(".", 1)[0] - if base_module == module: - return os.path.basename(abs_path) - - base_module_path = sys.modules[base_module].__file__ - if not base_module_path: - return abs_path - - return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( - os.sep - ) - except Exception: - return abs_path - - -def serialize_frame( - frame: "FrameType", - tb_lineno: "Optional[int]" = None, - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, -) -> "Dict[str, Any]": - f_code = getattr(frame, "f_code", None) - if not f_code: - abs_path = None - function = None - else: - abs_path = frame.f_code.co_filename - function = frame.f_code.co_name - try: - module = frame.f_globals["__name__"] - except Exception: - module = None - - if tb_lineno is None: - tb_lineno = frame.f_lineno - - try: - os_abs_path = os.path.abspath(abs_path) if abs_path else None - except Exception: - os_abs_path = None - - rv: "Dict[str, Any]" = { - "filename": filename_for_module(module, abs_path) or None, - "abs_path": os_abs_path, - "function": function or "", - "module": module, - "lineno": tb_lineno, - } - - if include_source_context: - rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( - frame, tb_lineno, max_value_length - ) - - if include_local_variables: - from sentry_sdk.serializer import serialize - - rv["vars"] = serialize( - dict(frame.f_locals), is_vars=True, custom_repr=custom_repr - ) - - return rv - - -def current_stacktrace( - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, -) -> "Dict[str, Any]": - __tracebackhide__ = True - frames = [] - - f: "Optional[FrameType]" = sys._getframe() - while f is not None: - if not should_hide_frame(f): - frames.append( - serialize_frame( - f, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - ) - ) - f = f.f_back - - frames.reverse() - - return {"frames": frames} - - -def get_errno(exc_value: BaseException) -> "Optional[Any]": - return getattr(exc_value, "errno", None) - - -def get_error_message(exc_value: "Optional[BaseException]") -> str: - message: str = safe_str( - getattr(exc_value, "message", "") - or getattr(exc_value, "detail", "") - or safe_str(exc_value) - ) - - # __notes__ should be a list of strings when notes are added - # via add_note, but can be anything else if __notes__ is set - # directly. We only support strings in __notes__, since that - # is the correct use. - notes: object = getattr(exc_value, "__notes__", None) - if isinstance(notes, list) and len(notes) > 0: - message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) - - return message - - -def single_exception_from_error_tuple( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: "Optional[int]" = None, - parent_id: "Optional[int]" = None, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "Dict[str, Any]": - """ - Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - """ - exception_value: "Dict[str, Any]" = {} - exception_value["mechanism"] = ( - mechanism.copy() if mechanism else {"type": "generic", "handled": True} - ) - if exception_id is not None: - exception_value["mechanism"]["exception_id"] = exception_id - - if exc_value is not None: - errno = get_errno(exc_value) - else: - errno = None - - if errno is not None: - exception_value["mechanism"].setdefault("meta", {}).setdefault( - "errno", {} - ).setdefault("number", errno) - - if source is not None: - exception_value["mechanism"]["source"] = source - - is_root_exception = exception_id == 0 - if not is_root_exception and parent_id is not None: - exception_value["mechanism"]["parent_id"] = parent_id - exception_value["mechanism"]["type"] = "chained" - - if is_root_exception and "type" not in exception_value["mechanism"]: - exception_value["mechanism"]["type"] = "generic" - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - if is_exception_group: - exception_value["mechanism"]["is_exception_group"] = True - - exception_value["module"] = get_type_module(exc_type) - exception_value["type"] = get_type_name(exc_type) - exception_value["value"] = get_error_message(exc_value) - - if client_options is None: - include_local_variables = True - include_source_context = True - max_value_length = None # fallback - custom_repr = None - else: - include_local_variables = client_options["include_local_variables"] - include_source_context = client_options["include_source_context"] - max_value_length = client_options["max_value_length"] - custom_repr = client_options.get("custom_repr") - - frames: "List[Dict[str, Any]]" = [ - serialize_frame( - tb.tb_frame, - tb_lineno=tb.tb_lineno, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - custom_repr=custom_repr, - ) - # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on - # processing a super-long stacktrace. - for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) - ] - - if len(frames) > MAX_STACK_FRAMES: - # If we have more frames than the limit, we remove the stacktrace completely. - # We don't trim the stacktrace here because we have not processed the whole - # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would - # intelligently trim by removing frames in the middle of the stacktrace, but - # since we don't have the whole stacktrace, we can't do that. Instead, we - # drop the entire stacktrace. - exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( - value=None - ) - - elif frames: - if not full_stack: - new_frames = frames - else: - new_frames = merge_stack_frames(frames, full_stack, client_options) - - exception_value["stacktrace"] = {"frames": new_frames} - - return exception_value - - -HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") - -if HAS_CHAINED_EXCEPTIONS: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - exc_type, exc_value, tb = exc_info - - seen_exceptions = [] - seen_exception_ids: "Set[int]" = set() - - while ( - exc_type is not None - and exc_value is not None - and id(exc_value) not in seen_exception_ids - ): - yield exc_type, exc_value, tb - - # Avoid hashing random types we don't know anything - # about. Use the list to keep a ref so that the `id` is - # not used for another object. - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - if exc_value.__suppress_context__: - cause = exc_value.__cause__ - else: - cause = exc_value.__context__ - if cause is None: - break - exc_type = type(cause) - exc_value = cause - tb = getattr(cause, "__traceback__", None) - -else: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - yield exc_info - - -def exceptions_from_error( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: int = 0, - parent_id: int = 0, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, - seen_exceptions: "Optional[list[BaseException]]" = None, - seen_exception_ids: "Optional[Set[int]]" = None, -) -> "Tuple[int, List[Dict[str, Any]]]": - """ - Creates the list of exceptions. - This can include chained exceptions and exceptions from an ExceptionGroup. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - - Args: - exception_id (int): - - Sequential counter for assigning ``mechanism.exception_id`` - to each processed exception. Is NOT the result of calling `id()` on the exception itself. - - parent_id (int): - - The ``mechanism.exception_id`` of the parent exception. - - Written into ``mechanism.parent_id`` in the event payload so Sentry can - reconstruct the exception tree. - - Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` - values for cycle detection. - """ - - if seen_exception_ids is None: - seen_exception_ids = set() - - if seen_exceptions is None: - seen_exceptions = [] - - if exc_value is not None and id(exc_value) in seen_exception_ids: - return (exception_id, []) - - if exc_value is not None: - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - parent = single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source=source, - full_stack=full_stack, - ) - exceptions = [parent] - - parent_id = exception_id - exception_id += 1 - - should_supress_context = ( - hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore - ) - if should_supress_context: - # Add direct cause. - # The field `__cause__` is set when raised with the exception (using the `from` keyword). - exception_has_cause = ( - exc_value - and hasattr(exc_value, "__cause__") - and exc_value.__cause__ is not None - ) - if exception_has_cause: - cause = exc_value.__cause__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(cause), - exc_value=cause, - tb=getattr(cause, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__cause__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - else: - # Add indirect cause. - # The field `__context__` is assigned if another exception occurs while handling the exception. - exception_has_content = ( - exc_value - and hasattr(exc_value, "__context__") - and exc_value.__context__ is not None - ) - if exception_has_content: - context = exc_value.__context__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(context), - exc_value=context, - tb=getattr(context, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__context__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - # Add exceptions from an ExceptionGroup. - is_exception_group = exc_value and hasattr(exc_value, "exceptions") - if is_exception_group: - for idx, e in enumerate(exc_value.exceptions): # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(e), - exc_value=e, - tb=getattr(e, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source="exceptions[%s]" % idx, - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - return (exception_id, exceptions) - - -def exceptions_from_error_tuple( - exc_info: "ExcInfo", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "List[Dict[str, Any]]": - exc_type, exc_value, tb = exc_info - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - - if is_exception_group: - (_, exceptions) = exceptions_from_error( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=0, - parent_id=0, - full_stack=full_stack, - ) - - else: - exceptions = [] - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - exceptions.append( - single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - full_stack=full_stack, - ) - ) - - exceptions.reverse() - - return exceptions - - -def to_string(value: str) -> str: - try: - return str(value) - except UnicodeDecodeError: - return repr(value)[1:-1] - - -def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": - if "stacktrace" in event: - yield event["stacktrace"] - if "threads" in event: - for thread in event["threads"].get("values") or (): - if "stacktrace" in thread: - yield thread["stacktrace"] - if "exception" in event: - for exception in event["exception"].get("values") or (): - if isinstance(exception, dict) and "stacktrace" in exception: - yield exception["stacktrace"] - - -def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - for frame in stacktrace.get("frames") or (): - yield frame - - -def handle_in_app( - event: "Event", - in_app_exclude: "Optional[List[str]]" = None, - in_app_include: "Optional[List[str]]" = None, - project_root: "Optional[str]" = None, -) -> "Event": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - set_in_app_in_frames( - stacktrace.get("frames"), - in_app_exclude=in_app_exclude, - in_app_include=in_app_include, - project_root=project_root, - ) - - return event - - -def set_in_app_in_frames( - frames: "Any", - in_app_exclude: "Optional[List[str]]", - in_app_include: "Optional[List[str]]", - project_root: "Optional[str]" = None, -) -> "Optional[Any]": - if not frames: - return None - - for frame in frames: - # if frame has already been marked as in_app, skip it - current_in_app = frame.get("in_app") - if current_in_app is not None: - continue - - module = frame.get("module") - - # check if module in frame is in the list of modules to include - if _module_in_list(module, in_app_include): - frame["in_app"] = True - continue - - # check if module in frame is in the list of modules to exclude - if _module_in_list(module, in_app_exclude): - frame["in_app"] = False - continue - - # if frame has no abs_path, skip further checks - abs_path = frame.get("abs_path") - if abs_path is None: - continue - - if _is_external_source(abs_path): - frame["in_app"] = False - continue - - if _is_in_project_root(abs_path, project_root): - frame["in_app"] = True - continue - - return frames - - -def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": - if isinstance(error, tuple) and len(error) == 3: - exc_type, exc_value, tb = error - elif isinstance(error, BaseException): - tb = getattr(error, "__traceback__", None) - if tb is not None: - exc_type = type(error) - exc_value = error - else: - exc_type, exc_value, tb = sys.exc_info() - if exc_value is not error: - tb = None - exc_value = error - exc_type = type(error) - - else: - raise ValueError("Expected Exception object to report, got %s!" % type(error)) - - exc_info = (exc_type, exc_value, tb) - - if TYPE_CHECKING: - # This cast is safe because exc_type and exc_value are either both - # None or both not None. - exc_info = cast(ExcInfo, exc_info) - - return exc_info - - -def merge_stack_frames( - frames: "List[Dict[str, Any]]", - full_stack: "List[Dict[str, Any]]", - client_options: "Optional[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Add the missing frames from full_stack to frames and return the merged list. - """ - frame_ids = { - ( - frame["abs_path"], - frame["context_line"], - frame["lineno"], - frame["function"], - ) - for frame in frames - } - - new_frames = [ - stackframe - for stackframe in full_stack - if ( - stackframe["abs_path"], - stackframe["context_line"], - stackframe["lineno"], - stackframe["function"], - ) - not in frame_ids - ] - new_frames.extend(frames) - - # Limit the number of frames - max_stack_frames = ( - client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) - if client_options - else None - ) - if max_stack_frames is not None: - new_frames = new_frames[len(new_frames) - max_stack_frames :] - - return new_frames - - -def event_from_exception( - exc_info: "Union[BaseException, ExcInfo]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, -) -> "Tuple[Event, Dict[str, Any]]": - exc_info = exc_info_from_error(exc_info) - hint = event_hint_with_exc_info(exc_info) - - if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): - full_stack = current_stacktrace( - include_local_variables=client_options["include_local_variables"], - max_value_length=client_options["max_value_length"], - )["frames"] - else: - full_stack = None - - return ( - { - "level": "error", - "exception": { - "values": exceptions_from_error_tuple( - exc_info, client_options, mechanism, full_stack - ) - }, - }, - hint, - ) - - -def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: - if name is None: - return False - - if not items: - return False - - for item in items: - if item == name or name.startswith(item + "."): - return True - - return False - - -def _is_external_source(abs_path: "Optional[str]") -> bool: - # check if frame is in 'site-packages' or 'dist-packages' - if abs_path is None: - return False - - external_source = ( - re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None - ) - return external_source - - -def _is_in_project_root( - abs_path: "Optional[str]", project_root: "Optional[str]" -) -> bool: - if abs_path is None or project_root is None: - return False - - # check if path is in the project root - if abs_path.startswith(project_root): - return True - - return False - - -def _truncate_by_bytes(string: str, max_bytes: int) -> str: - """ - Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. - """ - truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") - - return truncated + "..." - - -def _get_size_in_bytes(value: str) -> "Optional[int]": - try: - return len(value.encode("utf-8")) - except (UnicodeEncodeError, UnicodeDecodeError): - return None - - -def strip_string( - value: str, max_length: "Optional[int]" = None -) -> "Union[AnnotatedValue, str]": - if not value or max_length is None: - return value - - byte_size = _get_size_in_bytes(value) - text_size = len(value) - - if byte_size is not None and byte_size > max_length: - # truncate to max_length bytes, preserving code points - truncated_value = _truncate_by_bytes(value, max_length) - elif text_size is not None and text_size > max_length: - # fallback to truncating by string length - truncated_value = value[: max_length - 3] + "..." - else: - return value - - return AnnotatedValue( - value=truncated_value, - metadata={ - "len": byte_size or text_size, - "rem": [["!limit", "x", max_length - 3, max_length]], - }, - ) - - -def parse_version(version: str) -> "Optional[Tuple[int, ...]]": - """ - Parses a version string into a tuple of integers. - This uses the parsing loging from PEP 440: - https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions - """ - VERSION_PATTERN = r""" # noqa: N806 - v? - (?: - (?:(?P[0-9]+)!)? # epoch - (?P[0-9]+(?:\.[0-9]+)*) # release segment - (?P
                                          # pre-release
-                [-_\.]?
-                (?P(a|b|c|rc|alpha|beta|pre|preview))
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-            (?P                                         # post release
-                (?:-(?P[0-9]+))
-                |
-                (?:
-                    [-_\.]?
-                    (?Ppost|rev|r)
-                    [-_\.]?
-                    (?P[0-9]+)?
-                )
-            )?
-            (?P                                          # dev release
-                [-_\.]?
-                (?Pdev)
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-        )
-        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
-    """
-
-    pattern = re.compile(
-        r"^\s*" + VERSION_PATTERN + r"\s*$",
-        re.VERBOSE | re.IGNORECASE,
-    )
-
-    try:
-        release = pattern.match(version).groupdict()["release"]  # type: ignore
-        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
-    except (TypeError, ValueError, AttributeError):
-        return None
-
-    return release_tuple
-
-
-def _is_contextvars_broken() -> bool:
-    """
-    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
-    """
-    try:
-        import gevent
-        from gevent.monkey import is_object_patched
-
-        # Get the MAJOR and MINOR version numbers of Gevent
-        version_tuple = tuple(
-            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
-        )
-        if is_object_patched("threading", "local"):
-            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
-            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
-            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
-            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
-            # for contextvars, is able to patch both thread locals and contextvars, in
-            # that case, check if contextvars are effectively patched.
-            if (
-                # Gevent 20.9.0+
-                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
-                # Gevent 20.5.0+ or Python < 3.7
-                or (is_object_patched("contextvars", "ContextVar"))
-            ):
-                return False
-
-            return True
-    except ImportError:
-        pass
-
-    try:
-        import greenlet
-        from eventlet.patcher import is_monkey_patched  # type: ignore
-
-        greenlet_version = parse_version(greenlet.__version__)
-
-        if greenlet_version is None:
-            logger.error(
-                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
-            )
-            return False
-
-        if is_monkey_patched("thread") and greenlet_version < (0, 5):
-            return True
-    except ImportError:
-        pass
-
-    return False
-
-
-def _make_threadlocal_contextvars(local: type) -> type:
-    class ContextVar:
-        # Super-limited impl of ContextVar
-
-        def __init__(self, name: str, default: "Any" = None) -> None:
-            self._name = name
-            self._default = default
-            self._local = local()
-            self._original_local = local()
-
-        def get(self, default: "Any" = None) -> "Any":
-            return getattr(self._local, "value", default or self._default)
-
-        def set(self, value: "Any") -> "Any":
-            token = str(random.getrandbits(64))
-            original_value = self.get()
-            setattr(self._original_local, token, original_value)
-            self._local.value = value
-            return token
-
-        def reset(self, token: "Any") -> None:
-            self._local.value = getattr(self._original_local, token)
-            # delete the original value (this way it works in Python 3.6+)
-            del self._original_local.__dict__[token]
-
-    return ContextVar
-
-
-def _get_contextvars() -> "Tuple[bool, type]":
-    """
-    Figure out the "right" contextvars installation to use. Returns a
-    `contextvars.ContextVar`-like class with a limited API.
-
-    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
-    """
-    if not _is_contextvars_broken():
-        # aiocontextvars is a PyPI package that ensures that the contextvars
-        # backport (also a PyPI package) works with asyncio under Python 3.6
-        #
-        # Import it if available.
-        if sys.version_info < (3, 7):
-            # `aiocontextvars` is absolutely required for functional
-            # contextvars on Python 3.6.
-            try:
-                from aiocontextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-        else:
-            # On Python 3.7 contextvars are functional.
-            try:
-                from contextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-
-    # Fall back to basic thread-local usage.
-
-    from threading import local
-
-    return False, _make_threadlocal_contextvars(local)
-
-
-HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
-
-CONTEXTVARS_ERROR_MESSAGE = """
-
-With asyncio/ASGI applications, the Sentry SDK requires a functional
-installation of `contextvars` to avoid leaking scope/context data across
-requests.
-
-Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
-"""
-
-_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
-
-# These exceptions won't set the span status to error if they occur. Use
-# register_control_flow_exception to add to this list
-_control_flow_exception_classes: "set[type]" = set()
-
-
-def is_internal_task() -> bool:
-    return _is_sentry_internal_task.get()
-
-
-@contextmanager
-def mark_sentry_task_internal() -> "Generator[None, None, None]":
-    """Context manager to mark a task as Sentry internal."""
-    token = _is_sentry_internal_task.set(True)
-    try:
-        yield
-    finally:
-        _is_sentry_internal_task.reset(token)
-
-
-def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
-    func_qualname: "Optional[str]" = None
-
-    prefix, suffix = "", ""
-
-    if isinstance(func, partial) and hasattr(func.func, "__name__"):
-        prefix, suffix = "partial()"
-        func = func.func
-    else:
-        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
-        # https://github.com/python/cpython/pull/16600
-        partial_method = getattr(func, "_partialmethod", None) or getattr(
-            func, "__partialmethod__", None
-        )
-        if isinstance(partial_method, partialmethod):
-            prefix, suffix = "partialmethod()"
-            func = partial_method.func
-
-    if hasattr(func, "__qualname__"):
-        func_qualname = func.__qualname__
-    elif hasattr(func, "__name__"):
-        func_qualname = func.__name__
-
-    if func_qualname is not None:
-        if hasattr(func, "__module__") and isinstance(func.__module__, str):
-            func_qualname = func.__module__ + "." + func_qualname
-        func_qualname = prefix + func_qualname + suffix
-
-    return func_qualname
-
-
-def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    return qualname_from_function(func)
-
-
-disable_capture_event = ContextVar("disable_capture_event")
-
-
-class ServerlessTimeoutWarning(Exception):  # noqa: N818
-    """Raised when a serverless method is about to reach its timeout."""
-
-    pass
-
-
-class TimeoutThread(threading.Thread):
-    """Creates a Thread which runs (sleeps) for a time duration equal to
-    waiting_time and raises a custom ServerlessTimeout exception.
-    """
-
-    def __init__(
-        self,
-        waiting_time: float,
-        configured_timeout: int,
-        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
-        current_scope: "Optional[sentry_sdk.Scope]" = None,
-    ) -> None:
-        threading.Thread.__init__(self)
-        self.waiting_time = waiting_time
-        self.configured_timeout = configured_timeout
-
-        self.isolation_scope = isolation_scope
-        self.current_scope = current_scope
-
-        self._stop_event = threading.Event()
-
-    def stop(self) -> None:
-        self._stop_event.set()
-
-    def _capture_exception(self) -> "ExcInfo":
-        exc_info = sys.exc_info()
-
-        client = sentry_sdk.get_client()
-        event, hint = event_from_exception(
-            exc_info,
-            client_options=client.options,
-            mechanism={"type": "threading", "handled": False},
-        )
-        sentry_sdk.capture_event(event, hint=hint)
-
-        return exc_info
-
-    def run(self) -> None:
-        self._stop_event.wait(self.waiting_time)
-
-        if self._stop_event.is_set():
-            return
-
-        integer_configured_timeout = int(self.configured_timeout)
-
-        # Setting up the exact integer value of configured time(in seconds)
-        if integer_configured_timeout < self.configured_timeout:
-            integer_configured_timeout = integer_configured_timeout + 1
-
-        # Raising Exception after timeout duration is reached
-        if self.isolation_scope is not None and self.current_scope is not None:
-            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
-                with sentry_sdk.scope.use_scope(self.current_scope):
-                    try:
-                        raise ServerlessTimeoutWarning(
-                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                                integer_configured_timeout
-                            )
-                        )
-                    except Exception:
-                        reraise(*self._capture_exception())
-
-        raise ServerlessTimeoutWarning(
-            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                integer_configured_timeout
-            )
-        )
-
-
-def to_base64(original: str) -> "Optional[str]":
-    """
-    Convert a string to base64, via UTF-8. Returns None on invalid input.
-    """
-    base64_string = None
-
-    try:
-        utf8_bytes = original.encode("UTF-8")
-        base64_bytes = base64.b64encode(utf8_bytes)
-        base64_string = base64_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
-
-    return base64_string
-
-
-def from_base64(base64_string: str) -> "Optional[str]":
-    """
-    Convert a string from base64, via UTF-8. Returns None on invalid input.
-    """
-    utf8_string = None
-
-    try:
-        only_valid_chars = BASE64_ALPHABET.match(base64_string)
-        assert only_valid_chars
-
-        base64_bytes = base64_string.encode("UTF-8")
-        utf8_bytes = base64.b64decode(base64_bytes)
-        utf8_string = utf8_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning(
-            "Unable to decode {b64} from base64:".format(b64=base64_string), err
-        )
-
-    return utf8_string
-
-
-Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
-
-
-def sanitize_url(
-    url: str,
-    remove_authority: bool = True,
-    remove_query_values: bool = True,
-    split: bool = False,
-) -> "Union[str, Components]":
-    """
-    Removes the authority and query parameter values from a given URL.
-    """
-    parsed_url = urlsplit(url)
-    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
-
-    # strip username:password (netloc can be usr:pwd@example.com)
-    if remove_authority:
-        netloc_parts = parsed_url.netloc.split("@")
-        if len(netloc_parts) > 1:
-            netloc = "%s:%s@%s" % (
-                SENSITIVE_DATA_SUBSTITUTE,
-                SENSITIVE_DATA_SUBSTITUTE,
-                netloc_parts[-1],
-            )
-        else:
-            netloc = parsed_url.netloc
-    else:
-        netloc = parsed_url.netloc
-
-    # strip values from query string
-    if remove_query_values:
-        query_string = unquote(
-            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
-        )
-    else:
-        query_string = parsed_url.query
-
-    components = Components(
-        scheme=parsed_url.scheme,
-        netloc=netloc,
-        query=query_string,
-        path=parsed_url.path,
-        fragment=parsed_url.fragment,
-    )
-
-    if split:
-        return components
-    else:
-        return urlunsplit(components)
-
-
-ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
-
-
-def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
-    """
-    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
-    parameters will be sanitized to remove sensitive data. The autority (username and password)
-    in the URL will always be removed.
-    """
-    parsed_url = sanitize_url(
-        url, remove_authority=True, remove_query_values=sanitize, split=True
-    )
-
-    base_url = urlunsplit(
-        Components(
-            scheme=parsed_url.scheme,  # type: ignore
-            netloc=parsed_url.netloc,  # type: ignore
-            query="",
-            path=parsed_url.path,  # type: ignore
-            fragment="",
-        )
-    )
-
-    return ParsedUrl(
-        url=base_url,
-        query=parsed_url.query,  # type: ignore
-        fragment=parsed_url.fragment,  # type: ignore
-    )
-
-
-def is_valid_sample_rate(rate: "Any", source: str) -> bool:
-    """
-    Checks the given sample rate to make sure it is valid type and value (a
-    boolean or a number between 0 and 1, inclusive).
-    """
-
-    # both booleans and NaN are instances of Real, so a) checking for Real
-    # checks for the possibility of a boolean also, and b) we have to check
-    # separately for NaN and Decimal does not derive from Real so need to check that too
-    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
-                source=source, rate=rate, type=type(rate)
-            )
-        )
-        return False
-
-    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
-    rate = float(rate)
-    if rate < 0 or rate > 1:
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
-                source=source, rate=rate
-            )
-        )
-        return False
-
-    return True
-
-
-def match_regex_list(
-    item: str,
-    regex_list: "Optional[List[str]]" = None,
-    substring_matching: bool = False,
-) -> bool:
-    if regex_list is None:
-        return False
-
-    for item_matcher in regex_list:
-        if not substring_matching and item_matcher[-1] != "$":
-            item_matcher += "$"
-
-        matched = re.search(item_matcher, item)
-        if matched:
-            return True
-
-    return False
-
-
-def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
-    """
-    Determines whether the given URL matches the Sentry DSN.
-    """
-    return (
-        client is not None
-        and client.transport is not None
-        and client.transport.parsed_dsn is not None
-        and client.transport.parsed_dsn.netloc in url
-    )
-
-
-def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
-    try:
-        from importlib import metadata
-
-        yielded = set()
-        for dist in metadata.distributions():
-            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
-            # `metadata` values may be `None`, see:
-            # https://github.com/python/cpython/issues/91216
-            # and
-            # https://github.com/python/importlib_metadata/issues/371
-            if name is not None:
-                normalized_name = _normalize_module_name(name)
-                if dist.version is not None and normalized_name not in yielded:
-                    yield normalized_name, dist.version
-                    yielded.add(normalized_name)
-
-    except ImportError:
-        # < py3.8
-        try:
-            import pkg_resources
-        except ImportError:
-            return
-
-        for info in pkg_resources.working_set:
-            yield _normalize_module_name(info.key), info.version
-
-
-def _normalize_module_name(name: str) -> str:
-    return name.lower()
-
-
-def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
-    # https://peps.python.org/pep-0503/#normalized-names
-    return re.sub(r"[-_.]+", "-", name)
-
-
-def _get_installed_modules() -> "Dict[str, str]":
-    global _installed_modules
-    if _installed_modules is None:
-        _installed_modules = dict(_generate_installed_modules())
-    return _installed_modules
-
-
-def package_version(package: str) -> "Optional[Tuple[int, ...]]":
-    normalized_package = _normalize_module_name(
-        _replace_hyphens_dots_and_underscores_with_dashes(package)
-    )
-
-    installed_packages = {
-        _replace_hyphens_dots_and_underscores_with_dashes(module): v
-        for module, v in _get_installed_modules().items()
-    }
-    version = installed_packages.get(normalized_package)
-    if version is None:
-        return None
-
-    return parse_version(version)
-
-
-def reraise(
-    tp: "Optional[Type[BaseException]]",
-    value: "Optional[BaseException]",
-    tb: "Optional[Any]" = None,
-) -> "NoReturn":
-    assert value is not None
-    if value.__traceback__ is not tb:
-        raise value.with_traceback(tb)
-    raise value
-
-
-def _no_op(*_a: "Any", **_k: "Any") -> None:
-    """No-op function for ensure_integration_enabled."""
-    pass
-
-
-if TYPE_CHECKING:
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-        original_function: "Callable[P, R]",
-    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
-
-
-def ensure_integration_enabled(
-    integration: "type[sentry_sdk.integrations.Integration]",
-    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
-) -> "Callable[[Callable[P, R]], Callable[P, R]]":
-    """
-    Ensures a given integration is enabled prior to calling a Sentry-patched function.
-
-    The function takes as its parameters the integration that must be enabled and the original
-    function that the SDK is patching. The function returns a function that takes the
-    decorated (Sentry-patched) function as its parameter, and returns a function that, when
-    called, checks whether the given integration is enabled. If the integration is enabled, the
-    function calls the decorated, Sentry-patched function. If the integration is not enabled,
-    the original function is called.
-
-    The function also takes care of preserving the original function's signature and docstring.
-
-    Example usage:
-
-    ```python
-    @ensure_integration_enabled(MyIntegration, my_function)
-    def patch_my_function():
-        with sentry_sdk.start_transaction(...):
-            return my_function()
-    ```
-    """
-    if TYPE_CHECKING:
-        # Type hint to ensure the default function has the right typing. The overloads
-        # ensure the default _no_op function is only used when R is None.
-        original_function = cast(Callable[P, R], original_function)
-
-    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
-        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
-            if sentry_sdk.get_client().get_integration(integration) is None:
-                return original_function(*args, **kwargs)
-
-            return sentry_patched_function(*args, **kwargs)
-
-        if original_function is _no_op:
-            return wraps(sentry_patched_function)(runner)
-
-        return wraps(original_function)(runner)
-
-    return patcher
-
-
-if PY37:
-
-    def nanosecond_time() -> int:
-        return time.perf_counter_ns()
-
-else:
-
-    def nanosecond_time() -> int:
-        return int(time.perf_counter() * 1e9)
-
-
-def now() -> float:
-    return time.perf_counter()
-
-
-try:
-    from gevent import get_hub as get_gevent_hub
-    from gevent.monkey import is_module_patched
-except ImportError:
-    # it's not great that the signatures are different, get_hub can't return None
-    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
-    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
-        return None
-
-    def is_module_patched(mod_name: str) -> bool:
-        # unable to import from gevent means no modules have been patched
-        return False
-
-
-def is_gevent() -> bool:
-    return is_module_patched("threading") or is_module_patched("_thread")
-
-
-def get_current_thread_meta(
-    thread: "Optional[threading.Thread]" = None,
-) -> "Tuple[Optional[int], Optional[str]]":
-    """
-    Try to get the id of the current thread, with various fall backs.
-    """
-
-    # if a thread is specified, that takes priority
-    if thread is not None:
-        try:
-            thread_id = thread.ident
-            thread_name = thread.name
-            if thread_id is not None:
-                return thread_id, thread_name
-        except AttributeError:
-            pass
-
-    # if the app is using gevent, we should look at the gevent hub first
-    # as the id there differs from what the threading module reports
-    if is_gevent():
-        gevent_hub = get_gevent_hub()
-        if gevent_hub is not None:
-            try:
-                # this is undocumented, so wrap it in try except to be safe
-                return gevent_hub.thread_ident, None
-            except AttributeError:
-                pass
-
-    # use the current thread's id if possible
-    try:
-        thread = threading.current_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # if we can't get the current thread id, fall back to the main thread id
-    try:
-        thread = threading.main_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # we've tried everything, time to give up
-    return None, None
-
-
-def _register_control_flow_exception(
-    exc_type: "Union[type, list[type], tuple[type], set[type]]",
-) -> None:
-    if isinstance(exc_type, (list, tuple, set)):
-        _control_flow_exception_classes.update(exc_type)
-    else:
-        _control_flow_exception_classes.add(exc_type)
-
-
-def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
-    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
-        # https://docs.python.org/3/library/exceptions.html#SystemExit
-        return False
-
-    if issubclass(ty, tuple(_control_flow_exception_classes)):
-        return False
-
-    return True
-
-
-if TYPE_CHECKING:
-    T = TypeVar("T")
-
-
-def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
-    """
-    Attempt to convert from an unknown type to a specific type, using the
-    given function. Return None if the conversion fails, i.e. if the function
-    raises an exception.
-    """
-    try:
-        if isinstance(value, convert_func):  # type: ignore
-            return value
-    except TypeError:
-        pass
-
-    try:
-        return convert_func(value)
-    except Exception:
-        return None
-
-
-def safe_serialize(data: "Any") -> str:
-    """Safely serialize to a readable string."""
-
-    def serialize_item(
-        item: "Any",
-    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
-        if callable(item):
-            try:
-                module = getattr(item, "__module__", None)
-                qualname = getattr(item, "__qualname__", None)
-                name = getattr(item, "__name__", "anonymous")
-
-                if module and qualname:
-                    full_path = f"{module}.{qualname}"
-                elif module and name:
-                    full_path = f"{module}.{name}"
-                else:
-                    full_path = name
-
-                return f""
-            except Exception:
-                return f""
-        elif isinstance(item, dict):
-            return {k: serialize_item(v) for k, v in item.items()}
-        elif isinstance(item, (list, tuple)):
-            return [serialize_item(x) for x in item]
-        elif hasattr(item, "__dict__"):
-            try:
-                attrs = {
-                    k: serialize_item(v)
-                    for k, v in vars(item).items()
-                    if not k.startswith("_")
-                }
-                return f"<{type(item).__name__} {attrs}>"
-            except Exception:
-                return repr(item)
-        else:
-            return item
-
-    try:
-        serialized = serialize_item(data)
-        return (
-            json.dumps(serialized, default=str)
-            if not isinstance(serialized, str)
-            else serialized
-        )
-    except Exception:
-        return str(data)
-
-
-def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(
-        options.get("enable_logs", False)
-        or options["_experiments"].get("enable_logs", False)
-    )
-
-
-def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return "data_collection" in options.get("_experiments", {})
-
-
-def get_before_send_log(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_log") or options["_experiments"].get(
-        "before_send_log"
-    )
-
-
-def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(options.get("enable_metrics", True))
-
-
-def get_before_send_metric(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_metric") or options["_experiments"].get(
-        "before_send_metric"
-    )
-
-
-def get_before_send_span(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
-    if options is None:
-        return None
-
-    return options["_experiments"].get("before_send_span")
-
-
-def format_attribute(val: "Any") -> "AttributeValue":
-    """
-    Turn unsupported attribute value types into an AttributeValue.
-
-    We do this as soon as a user-provided attribute is set, to prevent spans,
-    logs, metrics and similar from having live references to various objects.
-
-    Note: This is not the final attribute value format. Before they're sent,
-    they're serialized further into the actual format the protocol expects:
-    https://develop.sentry.dev/sdk/telemetry/attributes/
-    """
-    if isinstance(val, (bool, int, float, str)):
-        return val
-
-    if isinstance(val, (list, tuple)) and not val:
-        return []
-    elif isinstance(val, list):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return copy.deepcopy(val)
-    elif isinstance(val, tuple):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return list(val)
-
-    return safe_repr(val)
-
-
-def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
-    """Serialize attribute value to the transport format."""
-    if isinstance(val, bool):
-        return {"value": val, "type": "boolean"}
-    if isinstance(val, int):
-        return {"value": val, "type": "integer"}
-    if isinstance(val, float):
-        return {"value": val, "type": "double"}
-    if isinstance(val, str):
-        return {"value": val, "type": "string"}
-
-    if isinstance(val, list):
-        if not val:
-            return {"value": [], "type": "array"}
-
-        # Only lists of elements of a single type are supported
-        ty = type(val[0])
-        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
-            return {"value": val, "type": "array"}
-
-    # Coerce to string if we don't know what to do with the value. This should
-    # never happen as we pre-format early in format_attribute, but let's be safe.
-    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py
deleted file mode 100644
index 5eb9b23130..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/sentry_sdk/worker.py
+++ /dev/null
@@ -1,308 +0,0 @@
-import asyncio
-import os
-import threading
-from abc import ABC, abstractmethod
-from time import sleep, time
-from typing import TYPE_CHECKING
-
-from sentry_sdk._queue import FullError, Queue
-from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
-from sentry_sdk.utils import logger, mark_sentry_task_internal
-
-if TYPE_CHECKING:
-    from typing import Any, Callable, Optional
-
-
-_TERMINATOR = object()
-
-
-class Worker(ABC):
-    """Base class for all workers."""
-
-    @property
-    @abstractmethod
-    def is_alive(self) -> bool:
-        """Whether the worker is alive and running."""
-        pass
-
-    @abstractmethod
-    def kill(self) -> None:
-        """Kill the worker. It will not process any more events."""
-        pass
-
-    def flush(
-        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
-    ) -> None:
-        """Flush the worker, blocking until done or timeout is reached."""
-        return None
-
-    @abstractmethod
-    def full(self) -> bool:
-        """Whether the worker's queue is full."""
-        pass
-
-    @abstractmethod
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        """Schedule a callback. Returns True if queued, False if full."""
-        pass
-
-
-class BackgroundWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Queue" = Queue(queue_size)
-        self._lock = threading.Lock()
-        self._thread: "Optional[threading.Thread]" = None
-        self._thread_for_pid: "Optional[int]" = None
-
-    @property
-    def is_alive(self) -> bool:
-        if self._thread_for_pid != os.getpid():
-            return False
-        if not self._thread:
-            return False
-        return self._thread.is_alive()
-
-    def _ensure_thread(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    def _timed_queue_join(self, timeout: float) -> bool:
-        deadline = time() + timeout
-        queue = self._queue
-
-        queue.all_tasks_done.acquire()
-
-        try:
-            while queue.unfinished_tasks:
-                delay = deadline - time()
-                if delay <= 0:
-                    return False
-                queue.all_tasks_done.wait(timeout=delay)
-
-            return True
-        finally:
-            queue.all_tasks_done.release()
-
-    def start(self) -> None:
-        with self._lock:
-            if not self.is_alive:
-                self._thread = threading.Thread(
-                    target=self._target, name="sentry-sdk.BackgroundWorker"
-                )
-                self._thread.daemon = True
-                try:
-                    self._thread.start()
-                    self._thread_for_pid = os.getpid()
-                except RuntimeError:
-                    # At this point we can no longer start because the interpreter
-                    # is already shutting down.  Sadly at this point we can no longer
-                    # send out events.
-                    self._thread = None
-
-    def kill(self) -> None:
-        """
-        Kill worker thread. Returns immediately. Not useful for
-        waiting on shutdown for events, use `flush` for that.
-        """
-        logger.debug("background worker got kill request")
-        with self._lock:
-            if self._thread:
-                try:
-                    self._queue.put_nowait(_TERMINATOR)
-                except FullError:
-                    logger.debug("background worker queue full, kill failed")
-
-                self._thread = None
-                self._thread_for_pid = None
-
-    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
-        logger.debug("background worker got flush request")
-        with self._lock:
-            if self.is_alive and timeout > 0.0:
-                self._wait_flush(timeout, callback)
-        logger.debug("background worker flushed")
-
-    def full(self) -> bool:
-        return self._queue.full()
-
-    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
-        initial_timeout = min(0.1, timeout)
-        if not self._timed_queue_join(initial_timeout):
-            pending = self._queue.qsize() + 1
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            if not self._timed_queue_join(timeout - initial_timeout):
-                pending = self._queue.qsize() + 1
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_thread()
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except FullError:
-            return False
-
-    def _target(self) -> None:
-        while True:
-            callback = self._queue.get()
-            try:
-                if callback is _TERMINATOR:
-                    break
-                try:
-                    callback()
-                except Exception:
-                    logger.error("Failed processing job", exc_info=True)
-            finally:
-                self._queue.task_done()
-            sleep(0)
-
-
-class AsyncWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Optional[asyncio.Queue[Any]]" = None
-        self._queue_size = queue_size
-        self._task: "Optional[asyncio.Task[None]]" = None
-        # Event loop needs to remain in the same process
-        self._task_for_pid: "Optional[int]" = None
-        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
-        # Track active callback tasks so they have a strong reference and can be cancelled on kill
-        self._active_tasks: "set[asyncio.Task[None]]" = set()
-
-    @property
-    def is_alive(self) -> bool:
-        if self._task_for_pid != os.getpid():
-            return False
-        if not self._task or not self._loop:
-            return False
-        return self._loop.is_running() and not self._task.done()
-
-    def kill(self) -> None:
-        if self._task:
-            # Cancel the main consumer task to prevent duplicate consumers
-            self._task.cancel()
-            # Also cancel any active callback tasks
-            # Avoid modifying the set while cancelling tasks
-            tasks_to_cancel = set(self._active_tasks)
-            for task in tasks_to_cancel:
-                task.cancel()
-            self._active_tasks.clear()
-            self._loop = None
-            self._task = None
-            self._task_for_pid = None
-
-    def start(self) -> None:
-        if not self.is_alive:
-            try:
-                self._loop = asyncio.get_running_loop()
-                # Always create a fresh queue on start to avoid stale items
-                self._queue = asyncio.Queue(maxsize=self._queue_size)
-                with mark_sentry_task_internal():
-                    self._task = self._loop.create_task(self._target())
-                self._task_for_pid = os.getpid()
-            except RuntimeError:
-                # There is no event loop running
-                logger.warning("No event loop running, async worker not started")
-                self._loop = None
-                self._task = None
-                self._task_for_pid = None
-
-    def full(self) -> bool:
-        if self._queue is None:
-            return True
-        return self._queue.full()
-
-    def _ensure_task(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    async def _wait_flush(
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> None:
-        if not self._loop or not self._loop.is_running() or self._queue is None:
-            return
-
-        initial_timeout = min(0.1, timeout)
-
-        # Timeout on the join
-        try:
-            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
-        except asyncio.TimeoutError:
-            pending = self._queue.qsize() + len(self._active_tasks)
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            try:
-                remaining_timeout = timeout - initial_timeout
-                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
-            except asyncio.TimeoutError:
-                pending = self._queue.qsize() + len(self._active_tasks)
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def flush(  # type: ignore[override]
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> "Optional[asyncio.Task[None]]":
-        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
-            with mark_sentry_task_internal():
-                return self._loop.create_task(self._wait_flush(timeout, callback))
-        return None
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_task()
-        if self._queue is None:
-            return False
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except asyncio.QueueFull:
-            return False
-
-    async def _target(self) -> None:
-        if self._queue is None:
-            return
-        try:
-            while True:
-                callback = await self._queue.get()
-                if callback is _TERMINATOR:
-                    self._queue.task_done()
-                    break
-                # Firing tasks instead of awaiting them allows for concurrent requests
-                with mark_sentry_task_internal():
-                    task = asyncio.create_task(self._process_callback(callback))
-                # Create a strong reference to the task so it can be cancelled on kill
-                # and does not get garbage collected while running
-                self._active_tasks.add(task)
-                # Capture queue ref at dispatch time so done callbacks use the
-                # correct queue even if kill()/start() replace self._queue.
-                queue_ref = self._queue
-                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
-                # Yield to let the event loop run other tasks
-                await asyncio.sleep(0)
-        except asyncio.CancelledError:
-            pass  # Expected during kill()
-
-    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
-        # Callback is an async coroutine, need to await it
-        await callback()
-
-    def _on_task_complete(
-        self,
-        task: "asyncio.Task[None]",
-        queue: "Optional[asyncio.Queue[Any]]" = None,
-    ) -> None:
-        try:
-            task.result()
-        except asyncio.CancelledError:
-            pass  # Task was cancelled, expected during shutdown
-        except Exception:
-            logger.error("Failed processing job", exc_info=True)
-        finally:
-            # Mark the task as done and remove it from the active tasks set
-            # Use the queue reference captured at dispatch time, not self._queue,
-            # to avoid calling task_done() on a different queue after kill()/start().
-            if queue is not None:
-                queue.task_done()
-            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER
deleted file mode 100644
index 5c69047b2e..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/INSTALLER
+++ /dev/null
@@ -1 +0,0 @@
-uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA
deleted file mode 100644
index 9c7a4703f8..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/METADATA
+++ /dev/null
@@ -1,163 +0,0 @@
-Metadata-Version: 2.4
-Name: urllib3
-Version: 2.7.0
-Summary: HTTP library with thread-safe connection pooling, file post, and more.
-Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
-Project-URL: Documentation, https://urllib3.readthedocs.io
-Project-URL: Code, https://github.com/urllib3/urllib3
-Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
-Author-email: Andrey Petrov 
-Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
-License-Expression: MIT
-License-File: LICENSE.txt
-Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: 3.14
-Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.10
-Provides-Extra: brotli
-Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
-Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
-Provides-Extra: h2
-Requires-Dist: h2<5,>=4; extra == 'h2'
-Provides-Extra: socks
-Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
-Provides-Extra: zstd
-Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
-Description-Content-Type: text/markdown
-
-

- -![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) - -

- -

- PyPI Version - Python Versions - Join our Discord - Coverage Status - Build Status on GitHub - Documentation Status
- OpenSSF Scorecard - SLSA 3 - CII Best Practices -

- -urllib3 is a powerful, *user-friendly* HTTP client for Python. -urllib3 brings many critical features that are missing from the Python -standard libraries: - -- Thread safety. -- Connection pooling. -- Client-side SSL/TLS verification. -- File uploads with multipart encoding. -- Helpers for retrying requests and dealing with HTTP redirects. -- Support for gzip, deflate, brotli, and zstd encoding. -- Proxy support for HTTP and SOCKS. -- 100% test coverage. - -... and many more features, but most importantly: Our maintainers have a 15+ -year track record of maintaining urllib3 with the highest code standards and -attention to security and safety. - -[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) -and you should too. - - -## Installing - -urllib3 can be installed with [pip](https://pip.pypa.io): - -```bash -$ python -m pip install urllib3 -``` - -Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): - -```bash -$ git clone https://github.com/urllib3/urllib3.git -$ cd urllib3 -$ pip install . -``` - -## Getting Started - -urllib3 is easy to use: - -```python3 ->>> import urllib3 ->>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") ->>> resp.status -200 ->>> resp.data -b"User-agent: *\nDisallow: /deny\n" -``` - -urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). - - -## Community - -urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and -collaborating with other contributors. Drop by and say hello 👋 - - -## Contributing - -urllib3 happily accepts contributions. Please see our -[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) -for some tips on getting started. - - -## Security Disclosures - -To report a security vulnerability, please use the -[Tidelift security contact](https://tidelift.com/security). -Tidelift will coordinate the fix and disclosure with maintainers. - - -## Maintainers - -Meet our maintainers since 2008: - -- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) -- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) -- [@pquentin](https://github.com/pquentin) (Quentin Pradet) -- [@theacodes](https://github.com/theacodes) (Thea Flowers) -- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) -- [@lukasa](https://github.com/lukasa) (Cory Benfield) -- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) -- [@shazow](https://github.com/shazow) (Andrey Petrov) - -👋 - - -## Sponsorship - -If your company benefits from this library, please consider [sponsoring its -development](https://urllib3.readthedocs.io/en/latest/sponsors.html). - - -## For Enterprise - -Professional support for urllib3 is available as part of the [Tidelift -Subscription][1]. Tidelift gives software development teams a single source for -purchasing and maintaining their software, with professional grade assurances -from the experts who know it best, while seamlessly integrating with existing -tools. - -[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD deleted file mode 100644 index 8c4f9c0b4b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/RECORD +++ /dev/null @@ -1,44 +0,0 @@ -urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 -urllib3-2.7.0.dist-info/RECORD,, -urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 -urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 -urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 -urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 -urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 -urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 -urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 -urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 -urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 -urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 -urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 -urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 -urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 -urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 -urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 -urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 -urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 -urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 -urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 -urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 -urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 -urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 -urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 -urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 -urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 -urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 -urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 -urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 -urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 -urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 -urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 -urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 -urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 -urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 -urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 -urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 -urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 -urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 -urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL deleted file mode 100644 index b1b94fd58e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.29.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt deleted file mode 100644 index e6183d0276..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3-2.7.0.dist-info/licenses/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2008-2020 Andrey Petrov and contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py deleted file mode 100644 index 3fe782c8a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/__init__.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more -""" - -from __future__ import annotations - -# Set default logging handler to avoid "No handler found" warnings. -import logging -import sys -import typing -import warnings -from logging import NullHandler - -from . import exceptions -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._version import __version__ -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .poolmanager import PoolManager, ProxyManager, proxy_from_url -from .response import BaseHTTPResponse, HTTPResponse -from .util.request import make_headers -from .util.retry import Retry -from .util.timeout import Timeout - -# Ensure that Python is compiled with OpenSSL 1.1.1+ -# If the 'ssl' module isn't available at all that's -# fine, we only care if the module is available. -try: - import ssl -except ImportError: - pass -else: - if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: - warnings.warn( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/3020", - exceptions.NotOpenSSLWarning, - ) - elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: - raise ImportError( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/2168" - ) - -__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" -__license__ = "MIT" -__version__ = __version__ - -__all__ = ( - "HTTPConnectionPool", - "HTTPHeaderDict", - "HTTPSConnectionPool", - "PoolManager", - "ProxyManager", - "HTTPResponse", - "Retry", - "Timeout", - "add_stderr_logger", - "connection_from_url", - "disable_warnings", - "encode_multipart_formdata", - "make_headers", - "proxy_from_url", - "request", - "BaseHTTPResponse", -) - -logging.getLogger(__name__).addHandler(NullHandler()) - - -def add_stderr_logger( - level: int = logging.DEBUG, -) -> logging.StreamHandler[typing.TextIO]: - """ - Helper for quickly adding a StreamHandler to the logger. Useful for - debugging. - - Returns the handler after adding it. - """ - # This method needs to be in this __init__.py to get the __name__ correct - # even if urllib3 is vendored within another package. - logger = logging.getLogger(__name__) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) - logger.addHandler(handler) - logger.setLevel(level) - logger.debug("Added a stderr logging handler to logger: %s", __name__) - return handler - - -# ... Clean up. -del NullHandler - - -# All warning filters *must* be appended unless you're really certain that they -# shouldn't be: otherwise, it's very hard for users to use most Python -# mechanisms to silence them. -# SecurityWarning's always go off by default. -warnings.simplefilter("always", exceptions.SecurityWarning, append=True) -# InsecurePlatformWarning's don't vary between requests, so we keep it default. -warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) - - -def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: - """ - Helper for quickly disabling all urllib3 warnings. - """ - warnings.simplefilter("ignore", category) - - -_DEFAULT_POOL = PoolManager() - - -def request( - method: str, - url: str, - *, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - preload_content: bool | None = True, - decode_content: bool | None = True, - redirect: bool | None = True, - retries: Retry | bool | int | None = None, - timeout: Timeout | float | int | None = 3, - json: typing.Any | None = None, -) -> BaseHTTPResponse: - """ - A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. - Therefore, its side effects could be shared across dependencies relying on it. - To avoid side effects create a new ``PoolManager`` instance and use it instead. - The method does not accept low-level ``**urlopen_kw`` keyword arguments. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - - return _DEFAULT_POOL.request( - method, - url, - body=body, - fields=fields, - headers=headers, - preload_content=preload_content, - decode_content=decode_content, - redirect=redirect, - retries=retries, - timeout=timeout, - json=json, - ) - - -if sys.platform == "emscripten": - from .contrib.emscripten import inject_into_urllib3 # noqa: 401 - - inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py deleted file mode 100644 index 992ec1657a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_base_connection.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -import typing - -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from .util.url import Url - -_TYPE_BODY = typing.Union[ - bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str -] - - -class ProxyConfig(typing.NamedTuple): - ssl_context: ssl.SSLContext | None - use_forwarding_for_https: bool - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - - -class _ResponseOptions(typing.NamedTuple): - # TODO: Remove this in favor of a better - # HTTP request/response lifecycle tracking. - request_method: str - request_url: str - preload_content: bool - decode_content: bool - enforce_content_length: bool - - -if typing.TYPE_CHECKING: - import ssl - from typing import Protocol - - from .response import BaseHTTPResponse - - class BaseHTTPConnection(Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - host: str - port: int - timeout: None | ( - float - ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool - proxy_is_verified: bool | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: ... - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: ... - - def connect(self) -> None: ... - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: ... - - def getresponse(self) -> BaseHTTPResponse: ... - - def close(self) -> None: ... - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - - class BaseHTTPSConnection(BaseHTTPConnection, Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - # Certificate verification methods - cert_reqs: int | str | None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - ssl_context: ssl.SSLContext | None - - # Trusted CAs - ca_certs: str | None - ca_cert_dir: str | None - ca_cert_data: None | str | bytes - - # TLS version - ssl_minimum_version: int | None - ssl_maximum_version: int | None - ssl_version: int | str | None # Deprecated - - # Client certificates - cert_file: str | None - key_file: str | None - key_password: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py deleted file mode 100644 index ee9ca662b6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_collections.py +++ /dev/null @@ -1,486 +0,0 @@ -from __future__ import annotations - -import typing -from collections import OrderedDict -from enum import Enum, auto -from threading import RLock - -if typing.TYPE_CHECKING: - # We can only import Protocol if TYPE_CHECKING because it's a development - # dependency, and is not available at runtime. - from typing import Protocol - - from typing_extensions import Self - - class HasGettableStringKeys(Protocol): - def keys(self) -> typing.Iterator[str]: ... - - def __getitem__(self, key: str) -> str: ... - - -__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] - - -# Key type -_KT = typing.TypeVar("_KT") -# Value type -_VT = typing.TypeVar("_VT") -# Default type -_DT = typing.TypeVar("_DT") - -ValidHTTPHeaderSource = typing.Union[ - "HTTPHeaderDict", - typing.Mapping[str, str], - typing.Iterable[tuple[str, str]], - "HasGettableStringKeys", -] - - -class _Sentinel(Enum): - not_passed = auto() - - -def ensure_can_construct_http_header_dict( - potential: object, -) -> ValidHTTPHeaderSource | None: - if isinstance(potential, HTTPHeaderDict): - return potential - elif isinstance(potential, typing.Mapping): - # Full runtime checking of the contents of a Mapping is expensive, so for the - # purposes of typechecking, we assume that any Mapping is the right shape. - return typing.cast(typing.Mapping[str, str], potential) - elif isinstance(potential, typing.Iterable): - # Similarly to Mapping, full runtime checking of the contents of an Iterable is - # expensive, so for the purposes of typechecking, we assume that any Iterable - # is the right shape. - return typing.cast(typing.Iterable[tuple[str, str]], potential) - elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): - return typing.cast("HasGettableStringKeys", potential) - else: - return None - - -class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): - """ - Provides a thread-safe dict-like container which maintains up to - ``maxsize`` keys while throwing away the least-recently-used keys beyond - ``maxsize``. - - :param maxsize: - Maximum number of recent elements to retain. - - :param dispose_func: - Every time an item is evicted from the container, - ``dispose_func(value)`` is called. Callback which will get called - """ - - _container: typing.OrderedDict[_KT, _VT] - _maxsize: int - dispose_func: typing.Callable[[_VT], None] | None - lock: RLock - - def __init__( - self, - maxsize: int = 10, - dispose_func: typing.Callable[[_VT], None] | None = None, - ) -> None: - super().__init__() - self._maxsize = maxsize - self.dispose_func = dispose_func - self._container = OrderedDict() - self.lock = RLock() - - def __getitem__(self, key: _KT) -> _VT: - # Re-insert the item, moving it to the end of the eviction line. - with self.lock: - item = self._container.pop(key) - self._container[key] = item - return item - - def __setitem__(self, key: _KT, value: _VT) -> None: - evicted_item = None - with self.lock: - # Possibly evict the existing value of 'key' - try: - # If the key exists, we'll overwrite it, which won't change the - # size of the pool. Because accessing a key should move it to - # the end of the eviction line, we pop it out first. - evicted_item = key, self._container.pop(key) - self._container[key] = value - except KeyError: - # When the key does not exist, we insert the value first so that - # evicting works in all cases, including when self._maxsize is 0 - self._container[key] = value - if len(self._container) > self._maxsize: - # If we didn't evict an existing value, and we've hit our maximum - # size, then we have to evict the least recently used item from - # the beginning of the container. - evicted_item = self._container.popitem(last=False) - - # After releasing the lock on the pool, dispose of any evicted value. - if evicted_item is not None and self.dispose_func: - _, evicted_value = evicted_item - self.dispose_func(evicted_value) - - def __delitem__(self, key: _KT) -> None: - with self.lock: - value = self._container.pop(key) - - if self.dispose_func: - self.dispose_func(value) - - def __len__(self) -> int: - with self.lock: - return len(self._container) - - def __iter__(self) -> typing.NoReturn: - raise NotImplementedError( - "Iteration over this class is unlikely to be threadsafe." - ) - - def clear(self) -> None: - with self.lock: - # Copy pointers to all values, then wipe the mapping - values = list(self._container.values()) - self._container.clear() - - if self.dispose_func: - for value in values: - self.dispose_func(value) - - def keys(self) -> set[_KT]: # type: ignore[override] - with self.lock: - return set(self._container.keys()) - - -class HTTPHeaderDictItemView(set[tuple[str, str]]): - """ - HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of - address. - - If we directly try to get an item with a particular name, we will get a string - back that is the concatenated version of all the values: - - >>> d['X-Header-Name'] - 'Value1, Value2, Value3' - - However, if we iterate over an HTTPHeaderDict's items, we will optionally combine - these values based on whether combine=True was called when building up the dictionary - - >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) - >>> d.add("A", "2", combine=True) - >>> d.add("B", "bar") - >>> list(d.items()) - [ - ('A', '1, 2'), - ('B', 'foo'), - ('B', 'bar'), - ] - - This class conforms to the interface required by the MutableMapping ABC while - also giving us the nonstandard iteration behavior we want; items with duplicate - keys, ordered by time of first insertion. - """ - - _headers: HTTPHeaderDict - - def __init__(self, headers: HTTPHeaderDict) -> None: - self._headers = headers - - def __len__(self) -> int: - return len(list(self._headers.iteritems())) - - def __iter__(self) -> typing.Iterator[tuple[str, str]]: - return self._headers.iteritems() - - def __contains__(self, item: object) -> bool: - if isinstance(item, tuple) and len(item) == 2: - passed_key, passed_val = item - if isinstance(passed_key, str) and isinstance(passed_val, str): - return self._headers._has_value_for_header(passed_key, passed_val) - return False - - -class HTTPHeaderDict(typing.MutableMapping[str, str]): - """ - :param headers: - An iterable of field-value pairs. Must not contain multiple field names - when compared case-insensitively. - - :param kwargs: - Additional field-value pairs to pass in to ``dict.update``. - - A ``dict`` like container for storing HTTP Headers. - - Field names are stored and compared case-insensitively in compliance with - RFC 7230. Iteration provides the first case-sensitive key seen for each - case-insensitive pair. - - Using ``__setitem__`` syntax overwrites fields that compare equal - case-insensitively in order to maintain ``dict``'s api. For fields that - compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` - in a loop. - - If multiple fields that are equal case-insensitively are passed to the - constructor or ``.update``, the behavior is undefined and some will be - lost. - - >>> headers = HTTPHeaderDict() - >>> headers.add('Set-Cookie', 'foo=bar') - >>> headers.add('set-cookie', 'baz=quxx') - >>> headers['content-length'] = '7' - >>> headers['SET-cookie'] - 'foo=bar, baz=quxx' - >>> headers['Content-Length'] - '7' - """ - - _container: typing.MutableMapping[str, list[str]] - - def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): - super().__init__() - self._container = {} # 'dict' is insert-ordered - if headers is not None: - if isinstance(headers, HTTPHeaderDict): - self._copy_from(headers) - else: - self.extend(headers) - if kwargs: - self.extend(kwargs) - - def __setitem__(self, key: str, val: str) -> None: - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - self._container[key.lower()] = [key, val] - - def __getitem__(self, key: str) -> str: - if isinstance(key, bytes): - key = key.decode("latin-1") - val = self._container[key.lower()] - return ", ".join(val[1:]) - - def __delitem__(self, key: str) -> None: - if isinstance(key, bytes): - key = key.decode("latin-1") - del self._container[key.lower()] - - def __contains__(self, key: object) -> bool: - if isinstance(key, bytes): - key = key.decode("latin-1") - if isinstance(key, str): - return key.lower() in self._container - return False - - def setdefault(self, key: str, default: str = "") -> str: - return super().setdefault(key, default) - - def __eq__(self, other: object) -> bool: - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return False - else: - other_as_http_header_dict = type(self)(maybe_constructable) - - return {k.lower(): v for k, v in self.itermerged()} == { - k.lower(): v for k, v in other_as_http_header_dict.itermerged() - } - - def __ne__(self, other: object) -> bool: - return not self.__eq__(other) - - def __len__(self) -> int: - return len(self._container) - - def __iter__(self) -> typing.Iterator[str]: - # Only provide the originally cased names - for vals in self._container.values(): - yield vals[0] - - def discard(self, key: str) -> None: - try: - del self[key] - except KeyError: - pass - - def add(self, key: str, val: str, *, combine: bool = False) -> None: - """Adds a (name, value) pair, doesn't overwrite the value if it already - exists. - - If this is called with combine=True, instead of adding a new header value - as a distinct item during iteration, this will instead append the value to - any existing header value with a comma. If no existing header value exists - for the key, then the value will simply be added, ignoring the combine parameter. - - >>> headers = HTTPHeaderDict(foo='bar') - >>> headers.add('Foo', 'baz') - >>> headers['foo'] - 'bar, baz' - >>> list(headers.items()) - [('foo', 'bar'), ('foo', 'baz')] - >>> headers.add('foo', 'quz', combine=True) - >>> list(headers.items()) - [('foo', 'bar, baz, quz')] - """ - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - key_lower = key.lower() - new_vals = [key, val] - # Keep the common case aka no item present as fast as possible - vals = self._container.setdefault(key_lower, new_vals) - if new_vals is not vals: - # if there are values here, then there is at least the initial - # key/value pair - assert len(vals) >= 2 - if combine: - vals[-1] = vals[-1] + ", " + val - else: - vals.append(val) - - def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: - """Generic import function for any type of header-like object. - Adapted version of MutableMapping.update in order to insert items - with self.add instead of self.__setitem__ - """ - if len(args) > 1: - raise TypeError( - f"extend() takes at most 1 positional arguments ({len(args)} given)" - ) - other = args[0] if len(args) >= 1 else () - - if isinstance(other, HTTPHeaderDict): - for key, val in other.iteritems(): - self.add(key, val) - elif isinstance(other, typing.Mapping): - for key, val in other.items(): - self.add(key, val) - elif isinstance(other, typing.Iterable): - for key, value in other: - self.add(key, value) - elif hasattr(other, "keys") and hasattr(other, "__getitem__"): - # THIS IS NOT A TYPESAFE BRANCH - # In this branch, the object has a `keys` attr but is not a Mapping or any of - # the other types indicated in the method signature. We do some stuff with - # it as though it partially implements the Mapping interface, but we're not - # doing that stuff safely AT ALL. - for key in other.keys(): - self.add(key, other[key]) - - for key, value in kwargs.items(): - self.add(key, value) - - @typing.overload - def getlist(self, key: str) -> list[str]: ... - - @typing.overload - def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... - - def getlist( - self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed - ) -> list[str] | _DT: - """Returns a list of all the values for the named field. Returns an - empty list if the key doesn't exist.""" - if isinstance(key, bytes): - key = key.decode("latin-1") - try: - vals = self._container[key.lower()] - except KeyError: - if default is _Sentinel.not_passed: - # _DT is unbound; empty list is instance of List[str] - return [] - # _DT is bound; default is instance of _DT - return default - else: - # _DT may or may not be bound; vals[1:] is instance of List[str], which - # meets our external interface requirement of `Union[List[str], _DT]`. - return vals[1:] - - def _prepare_for_method_change(self) -> Self: - """ - Remove content-specific header fields before changing the request - method to GET or HEAD according to RFC 9110, Section 15.4. - """ - content_specific_headers = [ - "Content-Encoding", - "Content-Language", - "Content-Location", - "Content-Type", - "Content-Length", - "Digest", - "Last-Modified", - ] - for header in content_specific_headers: - self.discard(header) - return self - - # Backwards compatibility for httplib - getheaders = getlist - getallmatchingheaders = getlist - iget = getlist - - # Backwards compatibility for http.cookiejar - get_all = getlist - - def __repr__(self) -> str: - return f"{type(self).__name__}({dict(self.itermerged())})" - - def _copy_from(self, other: HTTPHeaderDict) -> None: - for key in other: - val = other.getlist(key) - self._container[key.lower()] = [key, *val] - - def copy(self) -> Self: - clone = type(self)() - clone._copy_from(self) - return clone - - def iteritems(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all header lines, including duplicate ones.""" - for key in self: - vals = self._container[key.lower()] - for val in vals[1:]: - yield vals[0], val - - def itermerged(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all headers, merging duplicate ones together.""" - for key in self: - val = self._container[key.lower()] - yield val[0], ", ".join(val[1:]) - - def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] - return HTTPHeaderDictItemView(self) - - def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: - if header_name in self: - return potential_value in self._container[header_name.lower()][1:] - return False - - def __ior__(self, other: object) -> HTTPHeaderDict: - # Supports extending a header dict in-place using operator |= - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - self.extend(maybe_constructable) - return self - - def __or__(self, other: object) -> Self: - # Supports merging header dicts using operator | - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = self.copy() - result.extend(maybe_constructable) - return result - - def __ror__(self, other: object) -> Self: - # Supports merging header dicts using operator | when other is on left side - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = type(self)(maybe_constructable) - result.extend(self) - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py deleted file mode 100644 index 297c271bf4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_request_methods.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -import json as _json -import typing -from urllib.parse import urlencode - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .response import BaseHTTPResponse - -__all__ = ["RequestMethods"] - -_TYPE_ENCODE_URL_FIELDS = typing.Union[ - typing.Sequence[tuple[str, typing.Union[str, bytes]]], - typing.Mapping[str, typing.Union[str, bytes]], -] - - -class RequestMethods: - """ - Convenience mixin for classes who implement a :meth:`urlopen` method, such - as :class:`urllib3.HTTPConnectionPool` and - :class:`urllib3.PoolManager`. - - Provides behavior for making common types of HTTP request methods and - decides which type of request field encoding to use. - - Specifically, - - :meth:`.request_encode_url` is for sending requests whose fields are - encoded in the URL (such as GET, HEAD, DELETE). - - :meth:`.request_encode_body` is for sending requests whose fields are - encoded in the *body* of the request using multipart or www-form-urlencoded - (such as for POST, PUT, PATCH). - - :meth:`.request` is for making any kind of request, it will look up the - appropriate encoding format and use one of the above two methods to make - the request. - - Initializer parameters: - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - """ - - _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} - - def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: - self.headers = headers or {} - - def urlopen( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **kw: typing.Any, - ) -> BaseHTTPResponse: # Abstract - raise NotImplementedError( - "Classes extending RequestMethods must implement " - "their own ``urlopen`` method." - ) - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - json: typing.Any | None = None, - **urlopen_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the appropriate encoding of - ``fields`` based on the ``method`` used. - - This is a convenience method that requires the least amount of manual - effort. It can be used in most situations, while still having the - option to drop down to more specific methods when necessary, such as - :meth:`request_encode_url`, :meth:`request_encode_body`, - or even the lowest level :meth:`urlopen`. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the URL or request body, depending on ``method``. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - method = method.upper() - - if json is not None and body is not None: - raise TypeError( - "request got values for both 'body' and 'json' parameters which are mutually exclusive" - ) - - if json is not None: - if headers is None: - headers = self.headers - - if not ("content-type" in map(str.lower, headers.keys())): - headers = HTTPHeaderDict(headers) - headers["Content-Type"] = "application/json" - - body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( - "utf-8" - ) - - if body is not None: - urlopen_kw["body"] = body - - if method in self._encode_url_methods: - return self.request_encode_url( - method, - url, - fields=fields, # type: ignore[arg-type] - headers=headers, - **urlopen_kw, - ) - else: - return self.request_encode_body( - method, url, fields=fields, headers=headers, **urlopen_kw - ) - - def request_encode_url( - self, - method: str, - url: str, - fields: _TYPE_ENCODE_URL_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the url. This is useful for request methods like GET, HEAD, DELETE, etc. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the URL. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": headers} - extra_kw.update(urlopen_kw) - - if fields: - url += "?" + urlencode(fields) - - return self.urlopen(method, url, **extra_kw) - - def request_encode_body( - self, - method: str, - url: str, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the body. This is useful for request methods like POST, PUT, PATCH, etc. - - When ``encode_multipart=True`` (default), then - :func:`urllib3.encode_multipart_formdata` is used to encode - the payload with the appropriate content type. Otherwise - :func:`urllib.parse.urlencode` is used with the - 'application/x-www-form-urlencoded' content type. - - Multipart encoding must be used when posting files, and it's reasonably - safe to use it in other times too. However, it may break request - signing, such as with OAuth. - - Supports an optional ``fields`` parameter of key/value strings AND - key/filetuple. A filetuple is a (filename, data, MIME type) tuple where - the MIME type is optional. For example:: - - fields = { - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), - 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - } - - When uploading a file, providing a filename (the first parameter of the - tuple) is optional but recommended to best mimic behavior of browsers. - - Note that if ``headers`` are supplied, the 'Content-Type' header will - be overwritten because it depends on the dynamic random boundary string - which is used to compose the body of the request. The random boundary - string can be explicitly set with the ``multipart_boundary`` parameter. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param encode_multipart: - If True, encode the ``fields`` using the multipart/form-data MIME - format. - - :param multipart_boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} - body: bytes | str - - if fields: - if "body" in urlopen_kw: - raise TypeError( - "request got values for both 'fields' and 'body', can only specify one." - ) - - if encode_multipart: - body, content_type = encode_multipart_formdata( - fields, boundary=multipart_boundary - ) - else: - body, content_type = ( - urlencode(fields), # type: ignore[arg-type] - "application/x-www-form-urlencoded", - ) - - extra_kw["body"] = body - extra_kw["headers"].setdefault("Content-Type", content_type) - - extra_kw.update(urlopen_kw) - - return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py deleted file mode 100644 index bfed03985b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/_version.py +++ /dev/null @@ -1,24 +0,0 @@ -# file generated by vcs-versioning -# don't change, don't track in version control -from __future__ import annotations - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -version: str -__version__: str -__version_tuple__: tuple[int | str, ...] -version_tuple: tuple[int | str, ...] -commit_id: str | None -__commit_id__: str | None - -__version__ = version = '2.7.0' -__version_tuple__ = version_tuple = (2, 7, 0) - -__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py deleted file mode 100644 index 84e1dab945..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connection.py +++ /dev/null @@ -1,1099 +0,0 @@ -from __future__ import annotations - -import datetime -import http.client -import logging -import os -import re -import socket -import sys -import threading -import typing -import warnings -from http.client import HTTPConnection as _HTTPConnection -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from .response import HTTPResponse - from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT - from .util.ssltransport import SSLTransport - -from ._collections import HTTPHeaderDict -from .http2 import probe as http2_probe -from .util.response import assert_header_parsing -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout -from .util.util import to_str -from .util.wait import wait_for_read - -try: # Compiled with SSL? - import ssl - - BaseSSLError = ssl.SSLError -except (ImportError, AttributeError): - ssl = None # type: ignore[assignment] - - class BaseSSLError(BaseException): # type: ignore[no-redef] - pass - - -from ._base_connection import _TYPE_BODY -from ._base_connection import ProxyConfig as ProxyConfig -from ._base_connection import _ResponseOptions as _ResponseOptions -from ._version import __version__ -from .exceptions import ( - ConnectTimeoutError, - HeaderParsingError, - NameResolutionError, - NewConnectionError, - ProxyError, - SystemTimeWarning, -) -from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ -from .util.request import body_to_chunks -from .util.ssl_ import assert_fingerprint as _assert_fingerprint -from .util.ssl_ import ( - create_urllib3_context, - is_ipaddress, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .util.ssl_match_hostname import CertificateError, match_hostname -from .util.url import Url - -# Not a no-op, we're adding this to the namespace so it can be imported. -ConnectionError = ConnectionError -BrokenPipeError = BrokenPipeError - - -log = logging.getLogger(__name__) - -port_by_scheme = {"http": 80, "https": 443} - -# When it comes time to update this value as a part of regular maintenance -# (ie test_recent_date is failing) update it to ~6 months before the current date. -RECENT_DATE = datetime.date(2025, 1, 1) - -_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") - - -class HTTPConnection(_HTTPConnection): - """ - Based on :class:`http.client.HTTPConnection` but provides an extra constructor - backwards-compatibility layer between older and newer Pythons. - - Additional keyword parameters are used to configure attributes of the connection. - Accepted parameters include: - - - ``source_address``: Set the source address for the current connection. - - ``socket_options``: Set specific options on the underlying socket. If not specified, then - defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling - Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. - - For example, if you wish to enable TCP Keep Alive in addition to the defaults, - you might pass: - - .. code-block:: python - - HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ] - - Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). - """ - - default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] - - #: Disable Nagle's algorithm by default. - #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` - default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - ] - - #: Whether this connection verifies the host's certificate. - is_verified: bool = False - - #: Whether this proxy connection verified the proxy host's certificate. - # If no proxy is currently connected to the value will be ``None``. - proxy_is_verified: bool | None = None - - blocksize: int - source_address: tuple[str, int] | None - socket_options: connection._TYPE_SOCKET_OPTIONS | None - - _has_connected_to_proxy: bool - _response_options: _ResponseOptions | None - _tunnel_host: str | None - _tunnel_port: int | None - _tunnel_scheme: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - super().__init__( - host=host, - port=port, - timeout=Timeout.resolve_default_timeout(timeout), - source_address=source_address, - blocksize=blocksize, - ) - self.socket_options = socket_options - self.proxy = proxy - self.proxy_config = proxy_config - - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host: str | None = None - self._tunnel_port: int | None = None - self._tunnel_scheme: str | None = None - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __repr__(self) -> str: - return f"<{self} at {id(self):#x}>" - - @property - def host(self) -> str: - """ - Getter method to remove any trailing dots that indicate the hostname is an FQDN. - - In general, SSL certificates don't include the trailing dot indicating a - fully-qualified domain name, and thus, they don't validate properly when - checked against a domain name that includes the dot. In addition, some - servers may not expect to receive the trailing dot when provided. - - However, the hostname with trailing dot is critical to DNS resolution; doing a - lookup with the trailing dot will properly only resolve the appropriate FQDN, - whereas a lookup without a trailing dot will search the system's search domain - list. Thus, it's important to keep the original host around for use only in - those cases where it's appropriate (i.e., when doing DNS lookup to establish the - actual TCP connection across which we're going to send HTTP requests). - """ - return self._dns_host.rstrip(".") - - @host.setter - def host(self, value: str) -> None: - """ - Setter for the `host` property. - - We assume that only urllib3 uses the _dns_host attribute; httplib itself - only uses `host`, and it seems reasonable that other libraries follow suit. - """ - self._dns_host = value - - def _new_conn(self) -> socket.socket: - """Establish a socket connection and set nodelay settings on it. - - :return: New socket connection. - """ - try: - sock = connection.create_connection( - (self._dns_host, self.port), - self.timeout, - source_address=self.source_address, - socket_options=self.socket_options, - ) - except socket.gaierror as e: - raise NameResolutionError(self.host, self, e) from e - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except OSError as e: - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - sys.audit("http.client.connect", self, self.host, self.port) - - return sock - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - if scheme not in ("http", "https"): - raise ValueError( - f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" - ) - super().set_tunnel(host, port=port, headers=headers) - self._tunnel_scheme = scheme - - if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): - # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. - # When using connection_from_host, host will come without brackets. - def _wrap_ipv6(self, ip: bytes) -> bytes: - if b":" in ip and ip[0] != b"["[0]: - return b"[" + ip + b"]" - return ip - - if sys.version_info < (3, 11, 9): - # `_tunnel` copied from 3.11.13 backporting - # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 - # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 - def _tunnel(self) -> None: - _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] - connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - while True: - line = response.fp.readline(_MAXLINE + 1) - if len(line) > _MAXLINE: - raise http.client.LineTooLong("header line") - if not line: - # for sites which EOF without sending a trailer - break - if line in (b"\r\n", b"\n", b""): - break - - if self.debuglevel > 0: - print("header:", line.decode()) - finally: - response.close() - - elif (3, 12) <= sys.version_info < (3, 12, 3): - # `_tunnel` copied from 3.12.11 backporting - # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 - def _tunnel(self) -> None: # noqa: F811 - connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] - - if self.debuglevel > 0: - for header in self._raw_proxy_headers: - print("header:", header.decode()) - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - - finally: - response.close() - - def connect(self) -> None: - self.sock = self._new_conn() - if self._tunnel_host: - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - # TODO: Fix tunnel so it doesn't depend on self.sock state. - self._tunnel() - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - if self._has_connected_to_proxy: - self.proxy_is_verified = False - - @property - def is_closed(self) -> bool: - return self.sock is None - - @property - def is_connected(self) -> bool: - if self.sock is None: - return False - return not wait_for_read(self.sock, timeout=0.0) - - @property - def has_connected_to_proxy(self) -> bool: - return self._has_connected_to_proxy - - @property - def proxy_is_forwarding(self) -> bool: - """ - Return True if a forwarding proxy is configured, else return False - """ - return bool(self.proxy) and self._tunnel_host is None - - @property - def proxy_is_tunneling(self) -> bool: - """ - Return True if a tunneling proxy is configured, else return False - """ - return self._tunnel_host is not None - - def close(self) -> None: - try: - super().close() - finally: - # Reset all stateful properties so connection - # can be re-used without leaking prior configs. - self.sock = None - self.is_verified = False - self.proxy_is_verified = None - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host = None - self._tunnel_port = None - self._tunnel_scheme = None - - def putrequest( - self, - method: str, - url: str, - skip_host: bool = False, - skip_accept_encoding: bool = False, - ) -> None: - """""" - # Empty docstring because the indentation of CPython's implementation - # is broken but we don't want this method in our documentation. - match = _CONTAINS_CONTROL_CHAR_RE.search(method) - if match: - raise ValueError( - f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" - ) - - return super().putrequest( - method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding - ) - - def putheader(self, header: str, *values: str) -> None: # type: ignore[override] - """""" - if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): - super().putheader(header, *values) - elif to_str(header.lower()) not in SKIPPABLE_HEADERS: - skippable_headers = "', '".join( - [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] - ) - raise ValueError( - f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" - ) - - # `request` method's signature intentionally violates LSP. - # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - # Update the inner socket's timeout value to send the request. - # This only triggers if the connection is re-used. - if self.sock is not None: - self.sock.settimeout(self.timeout) - - # Store these values to be fed into the HTTPResponse - # object later. TODO: Remove this in favor of a real - # HTTP lifecycle mechanism. - - # We have to store these before we call .request() - # because sometimes we can still salvage a response - # off the wire even if we aren't able to completely - # send the request body. - self._response_options = _ResponseOptions( - request_method=method, - request_url=url, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - if headers is None: - headers = {} - header_keys = frozenset(to_str(k.lower()) for k in headers) - skip_accept_encoding = "accept-encoding" in header_keys - skip_host = "host" in header_keys - self.putrequest( - method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host - ) - - # Transform the body into an iterable of sendall()-able chunks - # and detect if an explicit Content-Length is doable. - chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) - chunks = chunks_and_cl.chunks - content_length = chunks_and_cl.content_length - - # When chunked is explicit set to 'True' we respect that. - if chunked: - if "transfer-encoding" not in header_keys: - self.putheader("Transfer-Encoding", "chunked") - else: - # Detect whether a framing mechanism is already in use. If so - # we respect that value, otherwise we pick chunked vs content-length - # depending on the type of 'body'. - if "content-length" in header_keys: - chunked = False - elif "transfer-encoding" in header_keys: - chunked = True - - # Otherwise we go off the recommendation of 'body_to_chunks()'. - else: - chunked = False - if content_length is None: - if chunks is not None: - chunked = True - self.putheader("Transfer-Encoding", "chunked") - else: - self.putheader("Content-Length", str(content_length)) - - # Now that framing headers are out of the way we send all the other headers. - if "user-agent" not in header_keys: - self.putheader("User-Agent", _get_default_user_agent()) - for header, value in headers.items(): - self.putheader(header, value) - self.endheaders() - - # If we're given a body we start sending that in chunks. - if chunks is not None: - for chunk in chunks: - # Sending empty chunks isn't allowed for TE: chunked - # as it indicates the end of the body. - if not chunk: - continue - if isinstance(chunk, str): - chunk = chunk.encode("utf-8") - if chunked: - self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) - else: - self.send(chunk) - - # Regardless of whether we have a body or not, if we're in - # chunked mode we want to send an explicit empty chunk. - if chunked: - self.send(b"0\r\n\r\n") - - def request_chunked( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - ) -> None: - """ - Alternative to the common request method, which sends the - body with chunked encoding and not as one block - """ - warnings.warn( - "HTTPConnection.request_chunked() is deprecated and will be removed " - "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", - category=FutureWarning, - stacklevel=2, - ) - self.request(method, url, body=body, headers=headers, chunked=True) - - def getresponse( # type: ignore[override] - self, - ) -> HTTPResponse: - """ - Get the response from the server. - - If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. - - If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. - """ - # Raise the same error as http.client.HTTPConnection - if self._response_options is None: - raise ResponseNotReady() - - # Reset this attribute for being used again. - resp_options = self._response_options - self._response_options = None - - # Since the connection's timeout value may have been updated - # we need to set the timeout on the socket. - self.sock.settimeout(self.timeout) - - # This is needed here to avoid circular import errors - from .response import HTTPResponse - - # Save a reference to the shutdown function before ownership is passed - # to httplib_response - # TODO should we implement it everywhere? - _shutdown = getattr(self.sock, "shutdown", None) - - # Get the response from http.client.HTTPConnection - httplib_response = super().getresponse() - - try: - assert_header_parsing(httplib_response.msg) - except (HeaderParsingError, TypeError) as hpe: - log.warning( - "Failed to parse headers (url=%s): %s", - _url_from_connection(self, resp_options.request_url), - hpe, - exc_info=True, - ) - - headers = HTTPHeaderDict(httplib_response.msg.items()) - - response = HTTPResponse( - body=httplib_response, - headers=headers, - status=httplib_response.status, - version=httplib_response.version, - version_string=getattr(self, "_http_vsn_str", "HTTP/?"), - reason=httplib_response.reason, - preload_content=resp_options.preload_content, - decode_content=resp_options.decode_content, - original_response=httplib_response, - enforce_content_length=resp_options.enforce_content_length, - request_method=resp_options.request_method, - request_url=resp_options.request_url, - sock_shutdown=_shutdown, - ) - return response - - -class HTTPSConnection(HTTPConnection): - """ - Many of the parameters to this constructor are passed to the underlying SSL - socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. - """ - - default_port = port_by_scheme["https"] # type: ignore[misc] - - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_fingerprint: str | None = None - _connect_callback: typing.Callable[..., None] | None = None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - # cert_reqs depends on ssl_context so calculate last. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - self.cert_reqs = cert_reqs - self._connect_callback = None - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - """ - This method should only be called once, before the connection is used. - """ - warnings.warn( - "HTTPSConnection.set_cert() is deprecated and will be removed " - "in urllib3 v3.0. Instead provide the parameters to the " - "HTTPSConnection constructor.", - category=FutureWarning, - stacklevel=2, - ) - - # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also - # have an SSLContext object in which case we'll use its verify_mode. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - def connect(self) -> None: - # Today we don't need to be doing this step before the /actual/ socket - # connection, however in the future we'll need to decide whether to - # create a new socket or re-use an existing "shared" socket as a part - # of the HTTP/2 handshake dance. - if self._tunnel_host is not None and self._tunnel_port is not None: - probe_http2_host = self._tunnel_host - probe_http2_port = self._tunnel_port - else: - probe_http2_host = self.host - probe_http2_port = self.port - - # Check if the target origin supports HTTP/2. - # If the value comes back as 'None' it means that the current thread - # is probing for HTTP/2 support. Otherwise, we're waiting for another - # probe to complete, or we get a value right away. - target_supports_http2: bool | None - if "h2" in ssl_.ALPN_PROTOCOLS: - target_supports_http2 = http2_probe.acquire_and_get( - host=probe_http2_host, port=probe_http2_port - ) - else: - # If HTTP/2 isn't going to be offered it doesn't matter if - # the target supports HTTP/2. Don't want to make a probe. - target_supports_http2 = False - - if self._connect_callback is not None: - self._connect_callback( - "before connect", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - try: - sock: socket.socket | ssl.SSLSocket - self.sock = sock = self._new_conn() - server_hostname: str = self.host - tls_in_tls = False - - # Do we need to establish a tunnel? - if self.proxy_is_tunneling: - # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. - if self._tunnel_scheme == "https": - # _connect_tls_proxy will verify and assign proxy_is_verified - self.sock = sock = self._connect_tls_proxy(self.host, sock) - tls_in_tls = True - elif self._tunnel_scheme == "http": - self.proxy_is_verified = False - - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - self._tunnel() - # Override the host with the one we're requesting data from. - server_hostname = typing.cast(str, self._tunnel_host) - - if self.server_hostname is not None: - server_hostname = self.server_hostname - - is_time_off = datetime.date.today() < RECENT_DATE - if is_time_off: - warnings.warn( - ( - f"System time is way off (before {RECENT_DATE}). This will probably " - "lead to SSL verification errors" - ), - SystemTimeWarning, - ) - - # Remove trailing '.' from fqdn hostnames to allow certificate validation - server_hostname_rm_dot = server_hostname.rstrip(".") - - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock=sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - server_hostname=server_hostname_rm_dot, - ssl_context=self.ssl_context, - tls_in_tls=tls_in_tls, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ) - self.sock = sock_and_verified.socket - - # If an error occurs during connection/handshake we may need to release - # our lock so another connection can probe the origin. - except BaseException: - if self._connect_callback is not None: - self._connect_callback( - "after connect failure", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - if target_supports_http2 is None: - http2_probe.set_and_release( - host=probe_http2_host, port=probe_http2_port, supports_http2=None - ) - raise - - # If this connection doesn't know if the origin supports HTTP/2 - # we report back to the HTTP/2 probe our result. - if target_supports_http2 is None: - supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" - http2_probe.set_and_release( - host=probe_http2_host, - port=probe_http2_port, - supports_http2=supports_http2, - ) - - # Forwarding proxies can never have a verified target since - # the proxy is the one doing the verification. Should instead - # use a CONNECT tunnel in order to verify the target. - # See: https://github.com/urllib3/urllib3/issues/3267. - if self.proxy_is_forwarding: - self.is_verified = False - else: - self.is_verified = sock_and_verified.is_verified - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - # Set `self.proxy_is_verified` unless it's already set while - # establishing a tunnel. - if self._has_connected_to_proxy and self.proxy_is_verified is None: - self.proxy_is_verified = sock_and_verified.is_verified - - def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: - """ - Establish a TLS connection to the proxy using the provided SSL context. - """ - # `_connect_tls_proxy` is called when self._tunnel_host is truthy. - proxy_config = typing.cast(ProxyConfig, self.proxy_config) - ssl_context = proxy_config.ssl_context - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - server_hostname=hostname, - ssl_context=ssl_context, - assert_hostname=proxy_config.assert_hostname, - assert_fingerprint=proxy_config.assert_fingerprint, - # Features that aren't implemented for proxies yet: - cert_file=None, - key_file=None, - key_password=None, - tls_in_tls=False, - ) - self.proxy_is_verified = sock_and_verified.is_verified - return sock_and_verified.socket # type: ignore[return-value] - - -class _WrappedAndVerifiedSocket(typing.NamedTuple): - """ - Wrapped socket and whether the connection is - verified after the TLS handshake - """ - - socket: ssl.SSLSocket | SSLTransport - is_verified: bool - - -def _ssl_wrap_socket_and_match_hostname( - sock: socket.socket, - *, - cert_reqs: None | str | int, - ssl_version: None | str | int, - ssl_minimum_version: int | None, - ssl_maximum_version: int | None, - cert_file: str | None, - key_file: str | None, - key_password: str | None, - ca_certs: str | None, - ca_cert_dir: str | None, - ca_cert_data: None | str | bytes, - assert_hostname: None | str | typing.Literal[False], - assert_fingerprint: str | None, - server_hostname: str | None, - ssl_context: ssl.SSLContext | None, - tls_in_tls: bool = False, -) -> _WrappedAndVerifiedSocket: - """Logic for constructing an SSLContext from all TLS parameters, passing - that down into ssl_wrap_socket, and then doing certificate verification - either via hostname or fingerprint. This function exists to guarantee - that both proxies and targets have the same behavior when connecting via TLS. - """ - default_ssl_context = False - if ssl_context is None: - default_ssl_context = True - context = create_urllib3_context( - ssl_version=resolve_ssl_version(ssl_version), - ssl_minimum_version=ssl_minimum_version, - ssl_maximum_version=ssl_maximum_version, - cert_reqs=resolve_cert_reqs(cert_reqs), - ) - else: - context = ssl_context - - context.verify_mode = resolve_cert_reqs(cert_reqs) - - # In some cases, we want to verify hostnames ourselves - if ( - # `ssl` can't verify fingerprints or alternate hostnames - assert_fingerprint - or assert_hostname - # assert_hostname can be set to False to disable hostname checking - or assert_hostname is False - # We still support OpenSSL 1.0.2, which prevents us from verifying - # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 - or ssl_.IS_PYOPENSSL - or not ssl_.HAS_NEVER_CHECK_COMMON_NAME - ): - context.check_hostname = False - - # Try to load OS default certs if none are given. We need to do the hasattr() check - # for custom pyOpenSSL SSLContext objects because they don't support - # load_default_certs(). - if ( - not ca_certs - and not ca_cert_dir - and not ca_cert_data - and default_ssl_context - and hasattr(context, "load_default_certs") - ): - context.load_default_certs() - - # Ensure that IPv6 addresses are in the proper format and don't have a - # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses - # and interprets them as DNS hostnames. - if server_hostname is not None: - normalized = server_hostname.strip("[]") - if "%" in normalized: - normalized = normalized[: normalized.rfind("%")] - if is_ipaddress(normalized): - server_hostname = normalized - - ssl_sock = ssl_wrap_socket( - sock=sock, - keyfile=key_file, - certfile=cert_file, - key_password=key_password, - ca_certs=ca_certs, - ca_cert_dir=ca_cert_dir, - ca_cert_data=ca_cert_data, - server_hostname=server_hostname, - ssl_context=context, - tls_in_tls=tls_in_tls, - ) - - try: - if assert_fingerprint: - _assert_fingerprint( - ssl_sock.getpeercert(binary_form=True), assert_fingerprint - ) - elif ( - context.verify_mode != ssl.CERT_NONE - and not context.check_hostname - and assert_hostname is not False - ): - cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] - - # Need to signal to our match_hostname whether to use 'commonName' or not. - # If we're using our own constructed SSLContext we explicitly set 'False' - # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. - if default_ssl_context: - hostname_checks_common_name = False - else: - hostname_checks_common_name = ( - getattr(context, "hostname_checks_common_name", False) or False - ) - - _match_hostname( - cert, - assert_hostname or server_hostname, # type: ignore[arg-type] - hostname_checks_common_name, - ) - - return _WrappedAndVerifiedSocket( - socket=ssl_sock, - is_verified=context.verify_mode == ssl.CERT_REQUIRED - or bool(assert_fingerprint), - ) - except BaseException: - ssl_sock.close() - raise - - -def _match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - asserted_hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - # Our upstream implementation of ssl.match_hostname() - # only applies this normalization to IP addresses so it doesn't - # match DNS SANs so we do the same thing! - stripped_hostname = asserted_hostname.strip("[]") - if is_ipaddress(stripped_hostname): - asserted_hostname = stripped_hostname - - try: - match_hostname(cert, asserted_hostname, hostname_checks_common_name) - except CertificateError as e: - log.warning( - "Certificate did not match expected hostname: %s. Certificate: %s", - asserted_hostname, - cert, - ) - # Add cert to exception and reraise so client code can inspect - # the cert when catching the exception, if they want to - e._peer_cert = cert # type: ignore[attr-defined] - raise - - -def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: - # Look for the phrase 'wrong version number', if found - # then we should warn the user that we're very sure that - # this proxy is HTTP-only and they have a configuration issue. - error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) - is_likely_http_proxy = ( - "wrong version number" in error_normalized - or "unknown protocol" in error_normalized - or "record layer failure" in error_normalized - ) - http_proxy_warning = ( - ". Your proxy appears to only use HTTP and not HTTPS, " - "try changing your proxy URL to be HTTP. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#https-proxy-error-http-proxy" - ) - new_err = ProxyError( - f"Unable to connect to proxy" - f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", - err, - ) - new_err.__cause__ = err - return new_err - - -def _get_default_user_agent() -> str: - return f"python-urllib3/{__version__}" - - -class DummyConnection: - """Used to detect a failed ConnectionCls import.""" - - -if not ssl: - HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 - - -VerifiedHTTPSConnection = HTTPSConnection - - -def _url_from_connection( - conn: HTTPConnection | HTTPSConnection, path: str | None = None -) -> str: - """Returns the URL from a given connection. This is mainly used for testing and logging.""" - - scheme = "https" if isinstance(conn, HTTPSConnection) else "http" - - return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py deleted file mode 100644 index 70fbc5e725..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/connectionpool.py +++ /dev/null @@ -1,1191 +0,0 @@ -from __future__ import annotations - -import errno -import logging -import queue -import sys -import typing -import warnings -import weakref -from socket import timeout as SocketTimeout -from types import TracebackType - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._request_methods import RequestMethods -from .connection import ( - BaseSSLError, - BrokenPipeError, - DummyConnection, - HTTPConnection, - HTTPException, - HTTPSConnection, - ProxyConfig, - _wrap_proxy_error, -) -from .connection import port_by_scheme as port_by_scheme -from .exceptions import ( - ClosedPoolError, - EmptyPoolError, - FullPoolError, - HostChangedError, - InsecureRequestWarning, - LocationValueError, - MaxRetryError, - NewConnectionError, - ProtocolError, - ProxyError, - ReadTimeoutError, - SSLError, - TimeoutError, -) -from .response import BaseHTTPResponse -from .util.connection import is_connection_dropped -from .util.proxy import connection_requires_http_tunnel -from .util.request import _TYPE_BODY_POSITION, set_file_position -from .util.retry import Retry -from .util.ssl_match_hostname import CertificateError -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout -from .util.url import Url, _encode_target -from .util.url import _normalize_host as normalize_host -from .util.url import parse_url -from .util.util import to_str - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - - from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - -_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] - - -# Pool objects -class ConnectionPool: - """ - Base class for all connection pools, such as - :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. - - .. note:: - ConnectionPool.urlopen() does not normalize or percent-encode target URIs - which is useful if your target server doesn't support percent-encoded - target URIs. - """ - - scheme: str | None = None - QueueCls = queue.LifoQueue - - def __init__(self, host: str, port: int | None = None) -> None: - if not host: - raise LocationValueError("No host specified.") - - self.host = _normalize_host(host, scheme=self.scheme) - self.port = port - - # This property uses 'normalize_host()' (not '_normalize_host()') - # to avoid removing square braces around IPv6 addresses. - # This value is sent to `HTTPConnection.set_tunnel()` if called - # because square braces are required for HTTP CONNECT tunneling. - self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.close() - # Return False to re-raise any potential exceptions - return False - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - - -# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 -_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} - - -class HTTPConnectionPool(ConnectionPool, RequestMethods): - """ - Thread-safe connection pool for one host. - - :param host: - Host used for this HTTP Connection (e.g. "localhost"), passed into - :class:`http.client.HTTPConnection`. - - :param port: - Port used for this HTTP Connection (None is equivalent to 80), passed - into :class:`http.client.HTTPConnection`. - - :param timeout: - Socket timeout in seconds for each individual connection. This can - be a float or integer, which sets the timeout for the HTTP request, - or an instance of :class:`urllib3.util.Timeout` which gives you more - fine-grained control over request timeouts. After the constructor has - been parsed, this is always a `urllib3.util.Timeout` object. - - :param maxsize: - Number of connections to save that can be reused. More than 1 is useful - in multithreaded situations. If ``block`` is set to False, more - connections will be created but they will not be saved once they've - been used. - - :param block: - If set to True, no more than ``maxsize`` connections will be used at - a time. When no free connections are available, the call will block - until a connection has been released. This is a useful side effect for - particular multithreaded situations where one does not want to use more - than maxsize connections per host to prevent flooding. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param retries: - Retry configuration to use by default with requests in this pool. - - :param _proxy: - Parsed proxy URL, should not be used directly, instead, see - :class:`urllib3.ProxyManager` - - :param _proxy_headers: - A dictionary with proxy headers, should not be used directly, - instead, see :class:`urllib3.ProxyManager` - - :param \\**conn_kw: - Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, - :class:`urllib3.connection.HTTPSConnection` instances. - """ - - scheme = "http" - ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - _proxy_config: ProxyConfig | None = None, - **conn_kw: typing.Any, - ): - ConnectionPool.__init__(self, host, port) - RequestMethods.__init__(self, headers) - - if not isinstance(timeout, Timeout): - timeout = Timeout.from_float(timeout) - - if retries is None: - retries = Retry.DEFAULT - - self.timeout = timeout - self.retries = retries - - self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) - self.block = block - - self.proxy = _proxy - self.proxy_headers = _proxy_headers or {} - self.proxy_config = _proxy_config - - # Fill the queue up so that doing get() on it will block properly - for _ in range(maxsize): - self.pool.put(None) - - # These are mostly for testing and debugging purposes. - self.num_connections = 0 - self.num_requests = 0 - self.conn_kw = conn_kw - - if self.proxy: - # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. - # Defaulting `socket_options` to an empty list avoids it defaulting to - # ``HTTPConnection.default_socket_options``. - self.conn_kw.setdefault("socket_options", []) - - self.conn_kw["proxy"] = self.proxy - self.conn_kw["proxy_config"] = self.proxy_config - - # Do not pass 'self' as callback to 'finalize'. - # Then the 'finalize' would keep an endless living (leak) to self. - # By just passing a reference to the pool allows the garbage collector - # to free self if nobody else has a reference to it. - pool = self.pool - - # Close all the HTTPConnections in the pool before the - # HTTPConnectionPool object is garbage collected. - weakref.finalize(self, _close_pool_connections, pool) - - def _new_conn(self) -> BaseHTTPConnection: - """ - Return a fresh :class:`HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTP connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "80", - ) - - conn = self.ConnectionCls( - host=self.host, - port=self.port, - timeout=self.timeout.connect_timeout, - **self.conn_kw, - ) - return conn - - def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: - """ - Get a connection. Will return a pooled connection if one is available. - - If no connections are available and :prop:`.block` is ``False``, then a - fresh connection is returned. - - :param timeout: - Seconds to wait before giving up and raising - :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and - :prop:`.block` is ``True``. - """ - conn = None - - if self.pool is None: - raise ClosedPoolError(self, "Pool is closed.") - - try: - conn = self.pool.get(block=self.block, timeout=timeout) - - except AttributeError: # self.pool is None - raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: - - except queue.Empty: - if self.block: - raise EmptyPoolError( - self, - "Pool is empty and a new connection can't be opened due to blocking mode.", - ) from None - pass # Oh well, we'll create a new connection then - - # If this is a persistent connection, check if it got disconnected - if conn and is_connection_dropped(conn): - log.debug("Resetting dropped connection: %s", self.host) - conn.close() - - return conn or self._new_conn() - - def _put_conn(self, conn: BaseHTTPConnection | None) -> None: - """ - Put a connection back into the pool. - - :param conn: - Connection object for the current host and port as returned by - :meth:`._new_conn` or :meth:`._get_conn`. - - If the pool is already full, the connection is closed and discarded - because we exceeded maxsize. If connections are discarded frequently, - then maxsize should be increased. - - If the pool is closed, then the connection will be closed and discarded. - """ - if self.pool is not None: - try: - self.pool.put(conn, block=False) - return # Everything is dandy, done. - except AttributeError: - # self.pool is None. - pass - except queue.Full: - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - if self.block: - # This should never happen if you got the conn from self._get_conn - raise FullPoolError( - self, - "Pool reached maximum size and no more connections are allowed.", - ) from None - - log.warning( - "Connection pool is full, discarding connection: %s. Connection pool size: %s", - self.host, - self.pool.qsize(), - ) - - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - - def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: - # Nothing to do for HTTP connections. - pass - - def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: - """Helper that always returns a :class:`urllib3.util.Timeout`""" - if timeout is _DEFAULT_TIMEOUT: - return self.timeout.clone() - - if isinstance(timeout, Timeout): - return timeout.clone() - else: - # User passed us an int/float. This is for backwards compatibility, - # can be removed later - return Timeout.from_float(timeout) - - def _raise_timeout( - self, - err: BaseSSLError | OSError | SocketTimeout, - url: str, - timeout_value: _TYPE_TIMEOUT | None, - ) -> None: - """Is the error actually a timeout? Will raise a ReadTimeout or pass""" - - if isinstance(err, SocketTimeout): - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - # See the above comment about EAGAIN in Python 3. - if hasattr(err, "errno") and err.errno in _blocking_errnos: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - def _make_request( - self, - conn: BaseHTTPConnection, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | None = None, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - chunked: bool = False, - response_conn: BaseHTTPConnection | None = None, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> BaseHTTPResponse: - """ - Perform a request on a given urllib connection object taken from our - pool. - - :param conn: - a connection from one of our connection pools - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - Pass ``None`` to retry until you receive a response. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param response_conn: - Set this to ``None`` if you will handle releasing the connection or - set the connection to have the response release it. - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - self.num_requests += 1 - - timeout_obj = self._get_timeout(timeout) - timeout_obj.start_connect() - conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) - - try: - # Trigger any extra validation we need to do. - try: - self._validate_conn(conn) - except (SocketTimeout, BaseSSLError) as e: - self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) - raise - - # _validate_conn() starts the connection to an HTTPS proxy - # so we need to wrap errors with 'ProxyError' here too. - except ( - OSError, - NewConnectionError, - TimeoutError, - BaseSSLError, - CertificateError, - SSLError, - ) as e: - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - # If the connection didn't successfully connect to it's proxy - # then there - if isinstance( - new_e, (OSError, NewConnectionError, TimeoutError, SSLError) - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - raise new_e - - # conn.request() calls http.client.*.request, not the method in - # urllib3.request. It also calls makefile (recv) on the socket. - try: - conn.request( - method, - url, - body=body, - headers=headers, - chunked=chunked, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - # We are swallowing BrokenPipeError (errno.EPIPE) since the server is - # legitimately able to close the connection after sending a valid response. - # With this behaviour, the received response is still readable. - except BrokenPipeError: - pass - except OSError as e: - # MacOS/Linux - # EPROTOTYPE and ECONNRESET are needed on macOS - # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ - # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. - if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: - raise - - # Reset the timeout for the recv() on the socket - read_timeout = timeout_obj.read_timeout - - if not conn.is_closed: - # In Python 3 socket.py will catch EAGAIN and return None when you - # try and read into the file pointer created by http.client, which - # instead raises a BadStatusLine exception. Instead of catching - # the exception and assuming all BadStatusLine exceptions are read - # timeouts, check for a zero timeout before making the request. - if read_timeout == 0: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={read_timeout})" - ) - conn.timeout = read_timeout - - # Receive the response from the server - try: - response = conn.getresponse() - except (BaseSSLError, OSError) as e: - self._raise_timeout(err=e, url=url, timeout_value=read_timeout) - raise - - # Set properties that are used by the pooling layer. - response.retries = retries - response._connection = response_conn # type: ignore[attr-defined] - response._pool = self # type: ignore[attr-defined] - - log.debug( - '%s://%s:%s "%s %s %s" %s %s', - self.scheme, - self.host, - self.port, - method, - url, - response.version_string, - response.status, - response.length_remaining, - ) - - return response - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - if self.pool is None: - return - # Disable access to the pool - old_pool, self.pool = self.pool, None - - # Close all the HTTPConnections in the pool. - _close_pool_connections(old_pool) - - def is_same_host(self, url: str) -> bool: - """ - Check if the given ``url`` is a member of the same host as this - connection pool. - """ - if url.startswith("/"): - return True - - # TODO: Add optional support for socket.gethostbyname checking. - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - if host is not None: - host = _normalize_host(host, scheme=scheme) - - # Use explicit default port for comparison when none is given - if self.port and not port: - port = port_by_scheme.get(scheme) - elif not self.port and port == port_by_scheme.get(scheme): - port = None - - return (scheme, host, port) == (self.scheme, self.host, self.port) - - def urlopen( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - redirect: bool = True, - assert_same_host: bool = True, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - pool_timeout: int | None = None, - release_conn: bool | None = None, - chunked: bool = False, - body_pos: _TYPE_BODY_POSITION | None = None, - preload_content: bool = True, - decode_content: bool = True, - **response_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Get a connection from the pool and perform an HTTP request. This is the - lowest level call for making a request, so you'll need to specify all - the raw details. - - .. note:: - - More commonly, it's appropriate to use a convenience method - such as :meth:`request`. - - .. note:: - - `release_conn` will only behave as expected if - `preload_content=False` because we want to make - `preload_content=False` the default behaviour someday soon without - breaking backwards compatibility. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param assert_same_host: - If ``True``, will make sure that the host of the pool requests is - consistent else will raise HostChangedError. When ``False``, you can - use the pool on an HTTP proxy and request foreign hosts. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param pool_timeout: - If set and the pool is set to block=True, then this method will - block for ``pool_timeout`` seconds and raise EmptyPoolError if no - connection is available within the time period. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param release_conn: - If False, then the urlopen call will not release the connection - back into the pool once a response is received (but will release if - you read the entire contents of the response such as when - `preload_content=True`). This is useful if you're not preloading - the response's content immediately. You will need to call - ``r.release_conn()`` on the response ``r`` to return the connection - back into the pool. If None, it takes the value of ``preload_content`` - which defaults to ``True``. - - :param bool chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param int body_pos: - Position to seek to in file-like body in the event of a retry or - redirect. Typically this won't need to be set because urllib3 will - auto-populate the value when needed. - """ - # Ensure that the URL we're connecting to is properly encoded - if url.startswith("/"): - # URLs starting with / are inherently schemeless. - url = to_str(_encode_target(url)) - destination_scheme = None - else: - parsed_url = parse_url(url) - destination_scheme = parsed_url.scheme - url = to_str(parsed_url.url) - - if headers is None: - headers = self.headers - - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect, default=self.retries) - - if release_conn is None: - release_conn = preload_content - - # Check host - if assert_same_host and not self.is_same_host(url): - raise HostChangedError(self, url, retries) - - conn = None - - # Track whether `conn` needs to be released before - # returning/raising/recursing. Update this variable if necessary, and - # leave `release_conn` constant throughout the function. That way, if - # the function recurses, the original value of `release_conn` will be - # passed down into the recursive call, and its value will be respected. - # - # See issue #651 [1] for details. - # - # [1] - release_this_conn = release_conn - - http_tunnel_required = connection_requires_http_tunnel( - self.proxy, self.proxy_config, destination_scheme - ) - - # Merge the proxy headers. Only done when not using HTTP CONNECT. We - # have to copy the headers dict so we can safely change it without those - # changes being reflected in anyone else's copy. - if not http_tunnel_required: - headers = headers.copy() # type: ignore[attr-defined] - headers.update(self.proxy_headers) # type: ignore[union-attr] - - # Must keep the exception bound to a separate variable or else Python 3 - # complains about UnboundLocalError. - err = None - - # Keep track of whether we cleanly exited the except block. This - # ensures we do proper cleanup in finally. - clean_exit = False - - # Rewind body position, if needed. Record current position - # for future rewinds in the event of a redirect/retry. - body_pos = set_file_position(body, body_pos) - - try: - # Request a connection from the queue. - timeout_obj = self._get_timeout(timeout) - conn = self._get_conn(timeout=pool_timeout) - - conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] - - # Is this a closed/new connection that requires CONNECT tunnelling? - if self.proxy is not None and http_tunnel_required and conn.is_closed: - try: - self._prepare_proxy(conn) - except (BaseSSLError, OSError, SocketTimeout) as e: - self._raise_timeout( - err=e, url=self.proxy.url, timeout_value=conn.timeout - ) - raise - - # If we're going to release the connection in ``finally:``, then - # the response doesn't need to know about the connection. Otherwise - # it will also try to release it and we'll have a double-release - # mess. - response_conn = conn if not release_conn else None - - # Make the request on the HTTPConnection object - response = self._make_request( - conn, - method, - url, - timeout=timeout_obj, - body=body, - headers=headers, - chunked=chunked, - retries=retries, - response_conn=response_conn, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Everything went great! - clean_exit = True - - except EmptyPoolError: - # Didn't get a connection from the pool, no need to clean up - clean_exit = True - release_this_conn = False - raise - - except ( - TimeoutError, - HTTPException, - OSError, - ProtocolError, - BaseSSLError, - SSLError, - CertificateError, - ProxyError, - ) as e: - # Discard the connection for these exceptions. It will be - # replaced during the next _get_conn() call. - clean_exit = False - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - if isinstance( - new_e, - ( - OSError, - NewConnectionError, - TimeoutError, - SSLError, - HTTPException, - ), - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - elif isinstance(new_e, (OSError, HTTPException)): - new_e = ProtocolError("Connection aborted.", new_e) - - retries = retries.increment( - method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] - ) - retries.sleep() - - # Keep track of the error for the retry warning. - err = e - - finally: - if not clean_exit: - # We hit some kind of exception, handled or otherwise. We need - # to throw the connection away unless explicitly told not to. - # Close the connection, set the variable to None, and make sure - # we put the None back in the pool to avoid leaking it. - if conn: - conn.close() - conn = None - release_this_conn = True - - if release_this_conn: - # Put the connection back to be reused. If the connection is - # expired then it will be None, which will get replaced with a - # fresh connection during _get_conn. - self._put_conn(conn) - - if not conn: - # Try again - log.warning( - "Retrying (%r) after connection broken by '%r': %s", retries, err, url - ) - return self.urlopen( - method, - url, - body, - headers, - retries, - redirect, - assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Handle redirect? - redirect_location = redirect and response.get_redirect_location() - if redirect_location: - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - body = None - headers = HTTPHeaderDict(headers)._prepare_for_method_change() - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # self.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not self.is_same_host( - redirect_location - ): - new_headers = headers.copy() # type: ignore[union-attr] - for header in headers: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - headers = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep_for_retry(response) - log.debug("Redirecting %s -> %s", url, redirect_location) - return self.urlopen( - method, - redirect_location, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Check if we should retry the HTTP response. - has_retry_after = bool(response.headers.get("Retry-After")) - if retries.is_retry(method, response.status, has_retry_after): - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_status: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep(response) - log.debug("Retry: %s", url) - return self.urlopen( - method, - url, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - return response - - -class HTTPSConnectionPool(HTTPConnectionPool): - """ - Same as :class:`.HTTPConnectionPool`, but HTTPS. - - :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, - ``assert_hostname`` and ``host`` in this order to verify connections. - If ``assert_hostname`` is False, no verification is done. - - The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, - ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` - is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade - the connection socket into an SSL socket. - """ - - scheme = "https" - ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - ssl_version: int | str | None = None, - ssl_minimum_version: ssl.TLSVersion | None = None, - ssl_maximum_version: ssl.TLSVersion | None = None, - assert_hostname: str | typing.Literal[False] | None = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - **conn_kw: typing.Any, - ) -> None: - super().__init__( - host, - port, - timeout, - maxsize, - block, - headers, - retries, - _proxy, - _proxy_headers, - **conn_kw, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.ca_certs = ca_certs - self.ca_cert_dir = ca_cert_dir - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - - def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] - """Establishes a tunnel connection through HTTP CONNECT.""" - if self.proxy and self.proxy.scheme == "https": - tunnel_scheme = "https" - else: - tunnel_scheme = "http" - - conn.set_tunnel( - scheme=tunnel_scheme, - host=self._tunnel_host, - port=self.port, - headers=self.proxy_headers, - ) - conn.connect() - - def _new_conn(self) -> BaseHTTPSConnection: - """ - Return a fresh :class:`urllib3.connection.HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTPS connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "443", - ) - - if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] - raise ImportError( - "Can't connect to HTTPS URL because the SSL module is not available." - ) - - actual_host: str = self.host - actual_port = self.port - if self.proxy is not None and self.proxy.host is not None: - actual_host = self.proxy.host - actual_port = self.proxy.port - - return self.ConnectionCls( - host=actual_host, - port=actual_port, - timeout=self.timeout.connect_timeout, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - **self.conn_kw, - ) - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - super()._validate_conn(conn) - - # Force connect early to allow us to validate the connection. - if conn.is_closed: - conn.connect() - - # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 - if not conn.is_verified and not conn.proxy_is_verified: - warnings.warn( - ( - f"Unverified HTTPS request is being made to host '{conn.host}'. " - "Adding certificate verification is strongly advised. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#tls-warnings" - ), - InsecureRequestWarning, - ) - - -def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: - """ - Given a url, return an :class:`.ConnectionPool` instance of its host. - - This is a shortcut for not having to parse out the scheme, host, and port - of the url before creating an :class:`.ConnectionPool` instance. - - :param url: - Absolute URL string that must include the scheme. Port is optional. - - :param \\**kw: - Passes additional parameters to the constructor of the appropriate - :class:`.ConnectionPool`. Useful for specifying things like - timeout, maxsize, headers, etc. - - Example:: - - >>> conn = connection_from_url('http://google.com/') - >>> r = conn.request('GET', '/') - """ - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - port = port or port_by_scheme.get(scheme, 80) - if scheme == "https": - return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - else: - return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - """ - Normalize hosts for comparisons and use with sockets. - """ - - host = normalize_host(host, scheme) - - # httplib doesn't like it when we include brackets in IPv6 addresses - # Specifically, if we include brackets but also pass the port then - # httplib crazily doubles up the square brackets on the Host header. - # Instead, we need to make sure we never pass ``None`` as the port. - # However, for backward compatibility reasons we can't actually - # *assert* that. See http://bugs.python.org/issue28539 - if host and host.startswith("[") and host.endswith("]"): - host = host[1:-1] - return host - - -def _url_from_pool( - pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None -) -> str: - """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" - return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url - - -def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: - """Drains a queue of connections and closes each one.""" - try: - while True: - conn = pool.get(block=False) - if conn: - conn.close() - except queue.Empty: - pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py deleted file mode 100644 index e5b62b25e9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import urllib3.connection - -from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection - - -def inject_into_urllib3() -> None: - # override connection classes to use emscripten specific classes - # n.b. mypy complains about the overriding of classes below - # if it isn't ignored - HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection - HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection - urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] - urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] - urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py deleted file mode 100644 index 63f79dd3be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/connection.py +++ /dev/null @@ -1,260 +0,0 @@ -from __future__ import annotations - -import os -import typing - -# use http.client.HTTPException for consistency with non-emscripten -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady - -from ..._base_connection import _TYPE_BODY -from ...connection import HTTPConnection, ProxyConfig, port_by_scheme -from ...exceptions import TimeoutError -from ...response import BaseHTTPResponse -from ...util.connection import _TYPE_SOCKET_OPTIONS -from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from ...util.url import Url -from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request -from .request import EmscriptenRequest -from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - - -class EmscriptenHTTPConnection: - default_port: typing.ClassVar[int] = port_by_scheme["http"] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - timeout: None | (float) - - host: str - port: int - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool = False - proxy_is_verified: bool | None = None - - response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper - _response: EmscriptenResponse | None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - self.host = host - self.port = port - self.timeout = timeout if isinstance(timeout, float) else 0.0 - self.scheme = "http" - self._closed = True - self._response = None - # ignore these things because we don't - # have control over that stuff - self.proxy = None - self.proxy_config = None - self.blocksize = blocksize - self.source_address = None - self.socket_options = None - self.is_verified = False - - def set_tunnel( - self, - host: str, - port: int | None = 0, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - pass - - def connect(self) -> None: - pass - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - self._closed = False - if url.startswith("/"): - if self.port is not None: - port = f":{self.port}" - else: - port = "" - # no scheme / host / port included, make a full url - url = f"{self.scheme}://{self.host}{port}{url}" - request = EmscriptenRequest( - url=url, - method=method, - timeout=self.timeout if self.timeout else 0, - decode_content=decode_content, - ) - request.set_body(body) - if headers: - for k, v in headers.items(): - request.set_header(k, v) - self._response = None - try: - if not preload_content: - self._response = send_streaming_request(request) - if self._response is None: - self._response = send_request(request) - except _TimeoutError as e: - raise TimeoutError(e.message) from e - except _RequestError as e: - raise HTTPException(e.message) from e - - def getresponse(self) -> BaseHTTPResponse: - if self._response is not None: - return EmscriptenHttpResponseWrapper( - internal_response=self._response, - url=self._response.request.url, - connection=self, - ) - else: - raise ResponseNotReady() - - def close(self) -> None: - self._closed = True - self._response = None - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - return self._closed - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - return True - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - return False - - -class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): - default_port = port_by_scheme["https"] - # all this is basically ignored, as browser handles https - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - cert_file: str | None - key_file: str | None - key_password: str | None - ssl_context: typing.Any | None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None = None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: ( - None | _TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: typing.Any | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - self.scheme = "https" - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - self.cert_reqs = None - - # The browser will automatically verify all requests. - # We have no control over that setting. - self.is_verified = True - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - pass - - -# verify that this class implements BaseHTTP(s) connection correctly -if typing.TYPE_CHECKING: - _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) - _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js deleted file mode 100644 index faf141e1fa..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/emscripten_fetch_worker.js +++ /dev/null @@ -1,110 +0,0 @@ -let Status = { - SUCCESS_HEADER: -1, - SUCCESS_EOF: -2, - ERROR_TIMEOUT: -3, - ERROR_EXCEPTION: -4, -}; - -let connections = new Map(); -let nextConnectionID = 1; -const encoder = new TextEncoder(); - -self.addEventListener("message", async function (event) { - if (event.data.close) { - let connectionID = event.data.close; - connections.delete(connectionID); - return; - } else if (event.data.getMore) { - let connectionID = event.data.getMore; - let { curOffset, value, reader, intBuffer, byteBuffer } = - connections.get(connectionID); - // if we still have some in buffer, then just send it back straight away - if (!value || curOffset >= value.length) { - // read another buffer if required - try { - let readResponse = await reader.read(); - - if (readResponse.done) { - // read everything - clear connection and return - connections.delete(connectionID); - Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); - Atomics.notify(intBuffer, 0); - // finished reading successfully - // return from event handler - return; - } - curOffset = 0; - connections.get(connectionID).value = readResponse.value; - value = readResponse.value; - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } - - // send as much buffer as we can - let curLen = value.length - curOffset; - if (curLen > byteBuffer.length) { - curLen = byteBuffer.length; - } - byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); - - Atomics.store(intBuffer, 0, curLen); // store current length in bytes - Atomics.notify(intBuffer, 0); - curOffset += curLen; - connections.get(connectionID).curOffset = curOffset; - - return; - } else { - // start fetch - let connectionID = nextConnectionID; - nextConnectionID += 1; - const intBuffer = new Int32Array(event.data.buffer); - const byteBuffer = new Uint8Array(event.data.buffer, 8); - try { - const response = await fetch(event.data.url, event.data.fetchParams); - // return the headers first via textencoder - var headers = []; - for (const pair of response.headers.entries()) { - headers.push([pair[0], pair[1]]); - } - let headerObj = { - headers: headers, - status: response.status, - connectionID, - }; - const headerText = JSON.stringify(headerObj); - let headerBytes = encoder.encode(headerText); - let written = headerBytes.length; - byteBuffer.set(headerBytes); - intBuffer[1] = written; - // make a connection - connections.set(connectionID, { - reader: response.body.getReader(), - intBuffer: intBuffer, - byteBuffer: byteBuffer, - value: undefined, - curOffset: 0, - }); - // set header ready - Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); - Atomics.notify(intBuffer, 0); - // all fetching after this goes through a new postmessage call with getMore - // this allows for parallel requests - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } -}); -self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py deleted file mode 100644 index 612cfddc4c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/fetch.py +++ /dev/null @@ -1,726 +0,0 @@ -""" -Support for streaming http requests in emscripten. - -A few caveats - - -If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled -https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md -*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the -JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case -timeouts and streaming should just work. - -Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. - -This approach has several caveats: - -Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. -Streaming only works if you're running pyodide in a web worker. - -Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch -operation, so it requires that you have crossOriginIsolation enabled, by serving over https -(or from localhost) with the two headers below set: - - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp - -You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in -JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole -request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. - -Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once -control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. - -NB: in this code, there are a lot of JavaScript objects. They are named js_* -to make it clear what type of object they are. -""" - -from __future__ import annotations - -import io -import json -from email.parser import Parser -from importlib.resources import files -from typing import TYPE_CHECKING, Any - -import js # type: ignore[import-not-found] -from pyodide.ffi import ( # type: ignore[import-not-found] - JsArray, - JsException, - JsProxy, - to_js, -) - -if TYPE_CHECKING: - from typing_extensions import Buffer - -from .request import EmscriptenRequest -from .response import EmscriptenResponse - -""" -There are some headers that trigger unintended CORS preflight requests. -See also https://github.com/koenvo/pyodide-http/issues/22 -""" -HEADERS_TO_IGNORE = ("user-agent",) - -SUCCESS_HEADER = -1 -SUCCESS_EOF = -2 -ERROR_TIMEOUT = -3 -ERROR_EXCEPTION = -4 - - -class _RequestError(Exception): - def __init__( - self, - message: str | None = None, - *, - request: EmscriptenRequest | None = None, - response: EmscriptenResponse | None = None, - ): - self.request = request - self.response = response - self.message = message - super().__init__(self.message) - - -class _StreamingError(_RequestError): - pass - - -class _TimeoutError(_RequestError): - pass - - -def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: - return to_js(dict_val, dict_converter=js.Object.fromEntries) - - -class _ReadStream(io.RawIOBase): - def __init__( - self, - int_buffer: JsArray, - byte_buffer: JsArray, - timeout: float, - worker: JsProxy, - connection_id: int, - request: EmscriptenRequest, - ): - self.int_buffer = int_buffer - self.byte_buffer = byte_buffer - self.read_pos = 0 - self.read_len = 0 - self.connection_id = connection_id - self.worker = worker - self.timeout = int(1000 * timeout) if timeout > 0 else None - self.is_live = True - self._is_closed = False - self.request: EmscriptenRequest | None = request - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.int_buffer = None - self.byte_buffer = None - self._is_closed = True - self.request = None - if self.is_live: - self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) - self.is_live = False - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def readinto(self, byte_obj: Buffer) -> int: - if not self.int_buffer: - raise _StreamingError( - "No buffer for stream in _ReadStream.readinto", - request=self.request, - response=None, - ) - if self.read_len == 0: - # wait for the worker to send something - js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) - self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) - if ( - js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) - == "timed-out" - ): - raise _TimeoutError - data_len = self.int_buffer[0] - if data_len > 0: - self.read_len = data_len - self.read_pos = 0 - elif data_len == ERROR_EXCEPTION: - string_len = self.int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", - request=self.request, - response=None, - ) - else: - # EOF, free the buffers and return zero - # and free the request - self.is_live = False - self.close() - return 0 - # copy from int32array to python bytes - ret_length = min(self.read_len, len(memoryview(byte_obj))) - subarray = self.byte_buffer.subarray( - self.read_pos, self.read_pos + ret_length - ).to_py() - memoryview(byte_obj)[0:ret_length] = subarray - self.read_len -= ret_length - self.read_pos += ret_length - return ret_length - - -class _StreamingFetcher: - def __init__(self) -> None: - # make web-worker and data buffer on startup - self.streaming_ready = False - streaming_worker_code = ( - files(__package__) - .joinpath("emscripten_fetch_worker.js") - .read_text(encoding="utf-8") - ) - js_data_blob = js.Blob.new( - to_js([streaming_worker_code], create_pyproxies=False), - _obj_from_dict({"type": "application/javascript"}), - ) - - def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: - def onMsg(e: JsProxy) -> None: - self.streaming_ready = True - js_resolve_fn(e) - - def onErr(e: JsProxy) -> None: - js_reject_fn(e) # Defensive: never happens in ci - - self.js_worker.onmessage = onMsg - self.js_worker.onerror = onErr - - js_data_url = js.URL.createObjectURL(js_data_blob) - self.js_worker = js.globalThis.Worker.new(js_data_url) - self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) - - def send(self, request: EmscriptenRequest) -> EmscriptenResponse: - headers = { - k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE - } - - body = request.body - fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} - # start the request off in the worker - timeout = int(1000 * request.timeout) if request.timeout > 0 else None - js_shared_buffer = js.SharedArrayBuffer.new(1048576) - js_int_buffer = js.Int32Array.new(js_shared_buffer) - js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) - - js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) - js.Atomics.notify(js_int_buffer, 0) - js_absolute_url = js.URL.new(request.url, js.location).href - self.js_worker.postMessage( - _obj_from_dict( - { - "buffer": js_shared_buffer, - "url": js_absolute_url, - "fetchParams": fetch_data, - } - ) - ) - # wait for the worker to send something - js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) - if js_int_buffer[0] == ERROR_TIMEOUT: - raise _TimeoutError( - "Timeout connecting to streaming request", - request=request, - response=None, - ) - elif js_int_buffer[0] == SUCCESS_HEADER: - # got response - # header length is in second int of intBuffer - string_len = js_int_buffer[1] - # decode the rest to a JSON string - js_decoder = js.TextDecoder.new() - # this does a copy (the slice) because decode can't work on shared array - # for some silly reason - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - # get it as an object - response_obj = json.loads(json_str) - return EmscriptenResponse( - request=request, - status_code=response_obj["status"], - headers=response_obj["headers"], - body=_ReadStream( - js_int_buffer, - js_byte_buffer, - request.timeout, - self.js_worker, - response_obj["connectionID"], - request, - ), - ) - elif js_int_buffer[0] == ERROR_EXCEPTION: - string_len = js_int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", request=request, response=None - ) - else: - raise _StreamingError( - f"Unknown status from worker in fetch: {js_int_buffer[0]}", - request=request, - response=None, - ) - - -class _JSPIReadStream(io.RawIOBase): - """ - A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch - response. This requires support for WebAssembly JavaScript Promise Integration - in the containing browser, and for pyodide to be launched via runPythonAsync. - - :param js_read_stream: - The JavaScript stream reader - - :param timeout: - Timeout in seconds - - :param request: - The request we're handling - - :param response: - The response this stream relates to - - :param js_abort_controller: - A JavaScript AbortController object, used for timeouts - """ - - def __init__( - self, - js_read_stream: Any, - timeout: float, - request: EmscriptenRequest, - response: EmscriptenResponse, - js_abort_controller: Any, # JavaScript AbortController for timeouts - ): - self.js_read_stream = js_read_stream - self.timeout = timeout - self._is_closed = False - self._is_done = False - self.request: EmscriptenRequest | None = request - self.response: EmscriptenResponse | None = response - self.current_buffer = None - self.current_buffer_pos = 0 - self.js_abort_controller = js_abort_controller - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.js_read_stream.cancel() - self.js_read_stream = None - self._is_closed = True - self._is_done = True - self.request = None - self.response = None - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def _get_next_buffer(self) -> bool: - result_js = _run_sync_with_timeout( - self.js_read_stream.read(), - self.timeout, - self.js_abort_controller, - request=self.request, - response=self.response, - ) - if result_js.done: - self._is_done = True - return False - else: - self.current_buffer = result_js.value.to_py() - self.current_buffer_pos = 0 - return True - - def readinto(self, byte_obj: Buffer) -> int: - if self.current_buffer is None: - if not self._get_next_buffer() or self.current_buffer is None: - self.close() - return 0 - ret_length = min( - len(byte_obj), len(self.current_buffer) - self.current_buffer_pos - ) - byte_obj[0:ret_length] = self.current_buffer[ - self.current_buffer_pos : self.current_buffer_pos + ret_length - ] - self.current_buffer_pos += ret_length - if self.current_buffer_pos == len(self.current_buffer): - self.current_buffer = None - return ret_length - - -# check if we are in a worker or not -def is_in_browser_main_thread() -> bool: - return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window - - -def is_cross_origin_isolated() -> bool: - return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated - - -def is_in_node() -> bool: - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - and hasattr(js.process.release, "name") - and js.process.release.name == "node" - ) - - -def is_worker_available() -> bool: - return hasattr(js, "Worker") and hasattr(js, "Blob") - - -_fetcher: _StreamingFetcher | None = None - -if is_worker_available() and ( - (is_cross_origin_isolated() and not is_in_browser_main_thread()) - and (not is_in_node()) -): - _fetcher = _StreamingFetcher() -else: - _fetcher = None - - -NODE_JSPI_ERROR = ( - "urllib3 only works in Node.js with pyodide.runPythonAsync" - " and requires the flag --experimental-wasm-stack-switching in " - " versions of node <24." -) - - -def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: - if has_jspi(): - return send_jspi_request(request, True) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - - if _fetcher and streaming_ready(): - return _fetcher.send(request) - else: - _show_streaming_warning() - return None - - -_SHOWN_TIMEOUT_WARNING = False - - -def _show_timeout_warning() -> None: - global _SHOWN_TIMEOUT_WARNING - if not _SHOWN_TIMEOUT_WARNING: - _SHOWN_TIMEOUT_WARNING = True - message = "Warning: Timeout is not available on main browser thread" - js.console.warn(message) - - -_SHOWN_STREAMING_WARNING = False - - -def _show_streaming_warning() -> None: - global _SHOWN_STREAMING_WARNING - if not _SHOWN_STREAMING_WARNING: - _SHOWN_STREAMING_WARNING = True - message = "Can't stream HTTP requests because: \n" - if not is_cross_origin_isolated(): - message += " Page is not cross-origin isolated\n" - if is_in_browser_main_thread(): - message += " Python is running in main browser thread\n" - if not is_worker_available(): - message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in - if streaming_ready() is False: - message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch -is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" - from js import console - - console.warn(message) - - -def send_request(request: EmscriptenRequest) -> EmscriptenResponse: - if has_jspi(): - return send_jspi_request(request, False) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - try: - js_xhr = js.XMLHttpRequest.new() - - if not is_in_browser_main_thread(): - js_xhr.responseType = "arraybuffer" - if request.timeout: - js_xhr.timeout = int(request.timeout * 1000) - else: - js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") - if request.timeout: - # timeout isn't available on the main thread - show a warning in console - # if it is set - _show_timeout_warning() - - js_xhr.open(request.method, request.url, False) - for name, value in request.headers.items(): - if name.lower() not in HEADERS_TO_IGNORE: - js_xhr.setRequestHeader(name, value) - - js_xhr.send(to_js(request.body)) - - headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) - - if not is_in_browser_main_thread(): - body = js_xhr.response.to_py().tobytes() - else: - body = js_xhr.response.encode("ISO-8859-15") - return EmscriptenResponse( - status_code=js_xhr.status, headers=headers, body=body, request=request - ) - except JsException as err: - if err.name == "TimeoutError": - raise _TimeoutError(err.message, request=request) - elif err.name == "NetworkError": - raise _RequestError(err.message, request=request) - else: - # general http error - raise _RequestError(err.message, request=request) - - -def send_jspi_request( - request: EmscriptenRequest, streaming: bool -) -> EmscriptenResponse: - """ - Send a request using WebAssembly JavaScript Promise Integration - to wrap the asynchronous JavaScript fetch api (experimental). - - :param request: - Request to send - - :param streaming: - Whether to stream the response - - :return: The response object - :rtype: EmscriptenResponse - """ - timeout = request.timeout - js_abort_controller = js.AbortController.new() - headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} - req_body = request.body - fetch_data = { - "headers": headers, - "body": to_js(req_body), - "method": request.method, - "signal": js_abort_controller.signal, - } - # Node.js returns the whole response (unlike opaqueredirect in browsers), - # so urllib3 can set `redirect: manual` to control redirects itself. - # https://stackoverflow.com/a/78524615 - if _is_node_js(): - fetch_data["redirect"] = "manual" - # Call JavaScript fetch (async api, returns a promise) - fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) - # Now suspend WebAssembly until we resolve that promise - # or time out. - response_js = _run_sync_with_timeout( - fetcher_promise_js, - timeout, - js_abort_controller, - request=request, - response=None, - ) - headers = {} - header_iter = response_js.headers.entries() - while True: - iter_value_js = header_iter.next() - if getattr(iter_value_js, "done", False): - break - else: - headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) - status_code = response_js.status - body: bytes | io.RawIOBase = b"" - - response = EmscriptenResponse( - status_code=status_code, headers=headers, body=b"", request=request - ) - if streaming: - # get via inputstream - if response_js.body is not None: - # get a reader from the fetch response - body_stream_js = response_js.body.getReader() - body = _JSPIReadStream( - body_stream_js, timeout, request, response, js_abort_controller - ) - else: - # get directly via arraybuffer - # n.b. this is another async JavaScript call. - body = _run_sync_with_timeout( - response_js.arrayBuffer(), - timeout, - js_abort_controller, - request=request, - response=response, - ).to_py() - response.body = body - return response - - -def _run_sync_with_timeout( - promise: Any, - timeout: float, - js_abort_controller: Any, - request: EmscriptenRequest | None, - response: EmscriptenResponse | None, -) -> Any: - """ - Await a JavaScript promise synchronously with a timeout which is implemented - via the AbortController - - :param promise: - Javascript promise to await - - :param timeout: - Timeout in seconds - - :param js_abort_controller: - A JavaScript AbortController object, used on timeout - - :param request: - The request being handled - - :param response: - The response being handled (if it exists yet) - - :raises _TimeoutError: If the request times out - :raises _RequestError: If the request raises a JavaScript exception - - :return: The result of awaiting the promise. - """ - timer_id = None - if timeout > 0: - timer_id = js.setTimeout( - js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) - ) - try: - from pyodide.ffi import run_sync - - # run_sync here uses WebAssembly JavaScript Promise Integration to - # suspend python until the JavaScript promise resolves. - return run_sync(promise) - except JsException as err: - if err.name == "AbortError": - raise _TimeoutError( - message="Request timed out", request=request, response=response - ) - else: - raise _RequestError(message=err.message, request=request, response=response) - finally: - if timer_id is not None: - js.clearTimeout(timer_id) - - -def has_jspi() -> bool: - """ - Return true if jspi can be used. - - This requires both browser support and also WebAssembly - to be in the correct state - i.e. that the javascript - call into python was async not sync. - - :return: True if jspi can be used. - :rtype: bool - """ - try: - from pyodide.ffi import can_run_sync, run_sync # noqa: F401 - - return bool(can_run_sync()) - except ImportError: - return False - - -def _is_node_js() -> bool: - """ - Check if we are in Node.js. - - :return: True if we are in Node.js. - :rtype: bool - """ - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - # According to the Node.js documentation, the release name is always "node". - and js.process.release.name == "node" - ) - - -def streaming_ready() -> bool | None: - if _fetcher: - return _fetcher.streaming_ready - else: - return None # no fetcher, return None to signify that - - -async def wait_for_streaming_ready() -> bool: - if _fetcher: - await _fetcher.js_worker_ready_promise - return True - else: - return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py deleted file mode 100644 index e692e692bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/request.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from ..._base_connection import _TYPE_BODY - - -@dataclass -class EmscriptenRequest: - method: str - url: str - params: dict[str, str] | None = None - body: _TYPE_BODY | None = None - headers: dict[str, str] = field(default_factory=dict) - timeout: float = 0 - decode_content: bool = True - - def set_header(self, name: str, value: str) -> None: - self.headers[name.capitalize()] = value - - def set_body(self, body: _TYPE_BODY | None) -> None: - self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py deleted file mode 100644 index ec1e1dbe83..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/emscripten/response.py +++ /dev/null @@ -1,281 +0,0 @@ -from __future__ import annotations - -import json as _json -import logging -import typing -from contextlib import contextmanager -from dataclasses import dataclass -from http.client import HTTPException as HTTPException -from io import BytesIO, IOBase - -from ...exceptions import InvalidHeader, TimeoutError -from ...response import BaseHTTPResponse -from ...util.retry import Retry -from .request import EmscriptenRequest - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - - -@dataclass -class EmscriptenResponse: - status_code: int - headers: dict[str, str] - body: IOBase | bytes - request: EmscriptenRequest - - -class EmscriptenHttpResponseWrapper(BaseHTTPResponse): - def __init__( - self, - internal_response: EmscriptenResponse, - url: str | None = None, - connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, - ): - self._pool = None # set by pool class - self._body = None - self._uncached_read_occurred = False - self._response = internal_response - self._url = url - self._connection = connection - self._closed = False - super().__init__( - headers=internal_response.headers, - status=internal_response.status_code, - request_url=url, - version=0, - version_string="HTTP/?", - reason="", - decode_content=True, - ) - self.length_remaining = self._init_length(self._response.request.method) - self.length_is_certain = False - - @property - def url(self) -> str | None: - return self._url - - @url.setter - def url(self, url: str | None) -> None: - self._url = url - - @property - def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: - return self._connection - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - while True: - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - else: - break - - def _init_length(self, request_method: str | None) -> int | None: - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Check for responses that shouldn't include a body - if ( - self.status in (204, 304) - or 100 <= self.status < 200 - or request_method == "HEAD" - ): - length = 0 - - return length - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, # ignored because browser decodes always - cache_content: bool = False, - ) -> bytes: - if ( - self._closed - or self._response is None - or (isinstance(self._response.body, IOBase) and self._response.body.closed) - ): - return b"" - - with self._error_catcher(): - # body has been preloaded as a string by XmlHttpRequest - if not isinstance(self._response.body, IOBase): - self.length_remaining = len(self._response.body) - self.length_is_certain = True - # wrap body in IOStream - self._response.body = BytesIO(self._response.body) - if amt is not None and amt >= 0: - # don't cache partial content - cache_content = False - data = self._response.body.read(amt) - self._uncached_read_occurred = True - else: # read all we can (and cache it) - data = self._response.body.read() - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - self._uncached_read_occurred = True - if self.length_remaining is not None: - self.length_remaining = max(self.length_remaining - len(data), 0) - if len(data) == 0 or ( - self.length_is_certain and self.length_remaining == 0 - ): - # definitely finished reading, close response stream - self._response.body.close() - return typing.cast(bytes, data) - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Generator[bytes]: - # chunked is handled by browser - while True: - bytes = self.read(amt, decode_content) - if not bytes: - break - yield bytes - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - self.close() - - @property - def data(self) -> bytes: - if self._body: - return self._body - else: - return self.read(cache_content=True) - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - def close(self) -> None: - if not self._closed: - if isinstance(self._response.body, IOBase): - self._response.body.close() - if self._connection: - self._connection.close() - self._connection = None - self._closed = True - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch Emscripten specific exceptions thrown by fetch.py, - instead re-raising urllib3 variants, so that low-level exceptions - are not leaked in the high-level api. - - On exit, release the connection back to the pool. - """ - from .fetch import _RequestError, _TimeoutError # avoid circular import - - clean_exit = False - - try: - yield - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - except _TimeoutError as e: - raise TimeoutError(str(e)) - except _RequestError as e: - raise HTTPException(str(e)) - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now - if ( - isinstance(self._response.body, IOBase) - and not self._response.body.closed - ): - self._response.body.close() - # release the connection back to the pool - self.release_conn() - else: - # If we have read everything from the response stream, - # return the connection back to the pool. - if ( - isinstance(self._response.body, IOBase) - and self._response.body.closed - ): - self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py deleted file mode 100644 index f06b859992..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/pyopenssl.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Module for using pyOpenSSL as a TLS backend. This module was relevant before -the standard library ``ssl`` module supported SNI, but now that we've dropped -support for Python 2.7 all relevant Python versions support SNI so -**this module is no longer recommended**. - -This needs the following packages installed: - -* `pyOpenSSL`_ (tested with 19.0.0) -* `cryptography`_ (minimum 2.3, from pyopenssl) -* `idna`_ (minimum 2.1, from cryptography) - -However, pyOpenSSL depends on cryptography, so while we use all three directly here we -end up having relatively few packages required. - -You can install them with the following command: - -.. code-block:: bash - - $ python -m pip install pyopenssl cryptography idna - -To activate certificate checking, call -:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code -before you begin making HTTP requests. This can be done in a ``sitecustomize`` -module, or at any other time before your application begins using ``urllib3``, -like this: - -.. code-block:: python - - try: - import urllib3.contrib.pyopenssl - urllib3.contrib.pyopenssl.inject_into_urllib3() - except ImportError: - pass - -.. _pyopenssl: https://www.pyopenssl.org -.. _cryptography: https://cryptography.io -.. _idna: https://github.com/kjd/idna -""" - -from __future__ import annotations - -import OpenSSL.SSL # type: ignore[import-not-found] -from cryptography import x509 - -try: - from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] -except ImportError: - # UnsupportedExtension is gone in cryptography >= 2.1.0 - class UnsupportedExtension(Exception): # type: ignore[no-redef] - pass - - -import logging -import ssl -import typing -from io import BytesIO -from socket import socket as socket_cls - -from .. import util - -if typing.TYPE_CHECKING: - from OpenSSL.crypto import X509 # type: ignore[import-not-found] - - -__all__ = ["inject_into_urllib3", "extract_from_urllib3"] - -# Map from urllib3 to PyOpenSSL compatible parameter-values. -_openssl_versions: dict[int, int] = { - util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, -} - -if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD - -if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD - - -_stdlib_to_openssl_verify = { - ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, - ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, - ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER - + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, -} -_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} - -# The SSLvX values are the most likely to be missing in the future -# but we check them all just to be sure. -_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( - OpenSSL.SSL, "OP_NO_SSLv3", 0 -) -_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) -_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) -_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) -_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) - -_openssl_to_ssl_minimum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, - ssl.TLSVersion.TLSv1_3: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), - ssl.TLSVersion.MAXIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), -} -_openssl_to_ssl_maximum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 - | _OP_NO_TLSv1 - | _OP_NO_TLSv1_1 - | _OP_NO_TLSv1_2 - | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, -} - -# OpenSSL will only write 16K at a time -SSL_WRITE_BLOCKSIZE = 16384 - -orig_util_SSLContext = util.ssl_.SSLContext - - -log = logging.getLogger(__name__) - - -def inject_into_urllib3() -> None: - "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." - - _validate_dependencies_met() - - util.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.IS_PYOPENSSL = True - util.ssl_.IS_PYOPENSSL = True - - -def extract_from_urllib3() -> None: - "Undo monkey-patching by :func:`inject_into_urllib3`." - - util.SSLContext = orig_util_SSLContext - util.ssl_.SSLContext = orig_util_SSLContext - util.IS_PYOPENSSL = False - util.ssl_.IS_PYOPENSSL = False - - -def _validate_dependencies_met() -> None: - """ - Verifies that PyOpenSSL's package-level dependencies have been met. - Throws `ImportError` if they are not met. - """ - # Method added in `cryptography==1.1`; not available in older versions - from cryptography.x509.extensions import Extensions - - if getattr(Extensions, "get_extension_for_class", None) is None: - raise ImportError( - "'cryptography' module missing required functionality. " - "Try upgrading to v1.3.4 or newer." - ) - - # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 - # attribute is only present on those versions. - from OpenSSL.crypto import X509 - - x509 = X509() - if getattr(x509, "_x509", None) is None: - raise ImportError( - "'pyOpenSSL' module missing required functionality. " - "Try upgrading to v0.14 or newer." - ) - - -def _dnsname_to_stdlib(name: str) -> str | None: - """ - Converts a dNSName SubjectAlternativeName field to the form used by the - standard library on the given Python version. - - Cryptography produces a dNSName as a unicode string that was idna-decoded - from ASCII bytes. We need to idna-encode that string to get it back, and - then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib - uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). - - If the name cannot be idna-encoded then we return None signalling that - the name given should be skipped. - """ - - def idna_encode(name: str) -> bytes | None: - """ - Borrowed wholesale from the Python Cryptography Project. It turns out - that we can't just safely call `idna.encode`: it can explode for - wildcard names. This avoids that problem. - """ - import idna - - try: - for prefix in ["*.", "."]: - if name.startswith(prefix): - name = name[len(prefix) :] - return prefix.encode("ascii") + idna.encode(name) - return idna.encode(name) - except idna.core.IDNAError: - return None - - # Don't send IPv6 addresses through the IDNA encoder. - if ":" in name: - return name - - encoded_name = idna_encode(name) - if encoded_name is None: - return None - return encoded_name.decode("utf-8") - - -def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: - """ - Given an PyOpenSSL certificate, provides all the subject alternative names. - """ - cert = peer_cert.to_cryptography() - - # We want to find the SAN extension. Ask Cryptography to locate it (it's - # faster than looping in Python) - try: - ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value - except x509.ExtensionNotFound: - # No such extension, return the empty list. - return [] - except ( - x509.DuplicateExtension, - UnsupportedExtension, - x509.UnsupportedGeneralNameType, - UnicodeError, - ) as e: - # A problem has been found with the quality of the certificate. Assume - # no SAN field is present. - log.warning( - "A problem was encountered with the certificate that prevented " - "urllib3 from finding the SubjectAlternativeName field. This can " - "affect certificate validation. The error was %s", - e, - ) - return [] - - # We want to return dNSName and iPAddress fields. We need to cast the IPs - # back to strings because the match_hostname function wants them as - # strings. - # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 - # decoded. This is pretty frustrating, but that's what the standard library - # does with certificates, and so we need to attempt to do the same. - # We also want to skip over names which cannot be idna encoded. - names = [ - ("DNS", name) - for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) - if name is not None - ] - names.extend( - ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) - ) - - return names - - -class WrappedSocket: - """API-compatibility wrapper for Python OpenSSL's Connection-class.""" - - def __init__( - self, - connection: OpenSSL.SSL.Connection, - socket: socket_cls, - suppress_ragged_eofs: bool = True, - ) -> None: - self.connection = connection - self.socket = socket - self.suppress_ragged_eofs = suppress_ragged_eofs - self._io_refs = 0 - self._closed = False - - def fileno(self) -> int: - return self.socket.fileno() - - # Copy-pasted from Python 3.5 source code - def _decref_socketios(self) -> None: - if self._io_refs > 0: - self._io_refs -= 1 - if self._closed: - self.close() - - def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: - try: - data = self.connection.recv(*args, **kwargs) - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return b"" - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return b"" - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - else: - return data # type: ignore[no-any-return] - - def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: - try: - return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return 0 - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return 0 - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv_into(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - - def settimeout(self, timeout: float) -> None: - return self.socket.settimeout(timeout) - - def _send_until_done(self, data: bytes) -> int: - while True: - try: - return self.connection.send(data) # type: ignore[no-any-return] - except OpenSSL.SSL.WantWriteError as e: - if not util.wait_for_write(self.socket, self.socket.gettimeout()): - raise TimeoutError() from e - continue - except OpenSSL.SSL.SysCallError as e: - raise OSError(e.args[0], str(e)) from e - - def sendall(self, data: bytes) -> None: - total_sent = 0 - while total_sent < len(data): - sent = self._send_until_done( - data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] - ) - total_sent += sent - - def shutdown(self, how: int) -> None: - try: - self.connection.shutdown() - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"shutdown error: {e!r}") from e - - def close(self) -> None: - self._closed = True - if self._io_refs <= 0: - self._real_close() - - def _real_close(self) -> None: - try: - return self.connection.close() # type: ignore[no-any-return] - except OpenSSL.SSL.Error: - return - - def getpeercert( - self, binary_form: bool = False - ) -> dict[str, list[typing.Any]] | None: - x509 = self.connection.get_peer_certificate() - - if not x509: - return x509 # type: ignore[no-any-return] - - if binary_form: - return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] - - return { - "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] - "subjectAltName": get_subj_alt_name(x509), - } - - def version(self) -> str: - return self.connection.get_protocol_version_name() # type: ignore[no-any-return] - - def selected_alpn_protocol(self) -> str | None: - alpn_proto = self.connection.get_alpn_proto_negotiated() - return alpn_proto.decode() if alpn_proto else None - - -WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] - - -class PyOpenSSLContext: - """ - I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible - for translating the interface of the standard library ``SSLContext`` object - to calls into PyOpenSSL. - """ - - def __init__(self, protocol: int) -> None: - self.protocol = _openssl_versions[protocol] - self._ctx = OpenSSL.SSL.Context(self.protocol) - self._options = 0 - self.check_hostname = False - self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED - self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED - self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST - - @property - def options(self) -> int: - return self._options - - @options.setter - def options(self, value: int) -> None: - self._options = value - self._set_ctx_options() - - @property - def verify_flags(self) -> int: - return self._verify_flags - - @verify_flags.setter - def verify_flags(self, value: int) -> None: - self._verify_flags = value - self._ctx.get_cert_store().set_flags(self._verify_flags) - - @property - def verify_mode(self) -> int: - return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] - - @verify_mode.setter - def verify_mode(self, value: ssl.VerifyMode) -> None: - self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) - - def set_default_verify_paths(self) -> None: - self._ctx.set_default_verify_paths() - - def set_ciphers(self, ciphers: bytes | str) -> None: - if isinstance(ciphers, str): - ciphers = ciphers.encode("utf-8") - self._ctx.set_cipher_list(ciphers) - - def load_verify_locations( - self, - cafile: str | None = None, - capath: str | None = None, - cadata: bytes | None = None, - ) -> None: - if cafile is not None: - cafile = cafile.encode("utf-8") # type: ignore[assignment] - if capath is not None: - capath = capath.encode("utf-8") # type: ignore[assignment] - try: - self._ctx.load_verify_locations(cafile, capath) - if cadata is not None: - self._ctx.load_verify_locations(BytesIO(cadata)) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e - - def load_cert_chain( - self, - certfile: str, - keyfile: str | None = None, - password: str | None = None, - ) -> None: - try: - self._ctx.use_certificate_chain_file(certfile) - if password is not None: - if not isinstance(password, bytes): - password = password.encode("utf-8") # type: ignore[assignment] - self._ctx.set_passwd_cb(lambda *_: password) - self._ctx.use_privatekey_file(keyfile or certfile) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e - - def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: - protocols = [util.util.to_bytes(p, "ascii") for p in protocols] - return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] - - def wrap_socket( - self, - sock: socket_cls, - server_side: bool = False, - do_handshake_on_connect: bool = True, - suppress_ragged_eofs: bool = True, - server_hostname: bytes | str | None = None, - ) -> WrappedSocket: - cnx = OpenSSL.SSL.Connection(self._ctx, sock) - - # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 - if server_hostname and not util.ssl_.is_ipaddress(server_hostname): - if isinstance(server_hostname, str): - server_hostname = server_hostname.encode("utf-8") - cnx.set_tlsext_host_name(server_hostname) - - cnx.set_connect_state() - - while True: - try: - cnx.do_handshake() - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(sock, sock.gettimeout()): - raise TimeoutError("select timed out") from e - continue - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"bad handshake: {e!r}") from e - break - - return WrappedSocket(cnx, sock) - - def _set_ctx_options(self) -> None: - self._ctx.set_options( - self._options - | _openssl_to_ssl_minimum_version[self._minimum_version] - | _openssl_to_ssl_maximum_version[self._maximum_version] - ) - - @property - def minimum_version(self) -> int: - return self._minimum_version - - @minimum_version.setter - def minimum_version(self, minimum_version: int) -> None: - self._minimum_version = minimum_version - self._set_ctx_options() - - @property - def maximum_version(self) -> int: - return self._maximum_version - - @maximum_version.setter - def maximum_version(self, maximum_version: int) -> None: - self._maximum_version = maximum_version - self._set_ctx_options() - - -def _verify_callback( - cnx: OpenSSL.SSL.Connection, - x509: X509, - err_no: int, - err_depth: int, - return_code: int, -) -> bool: - return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py deleted file mode 100644 index d37da8fc20..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/contrib/socks.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -This module contains provisional support for SOCKS proxies from within -urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and -SOCKS5. To enable its functionality, either install PySocks or install this -module with the ``socks`` extra. - -The SOCKS implementation supports the full range of urllib3 features. It also -supports the following SOCKS features: - -- SOCKS4A (``proxy_url='socks4a://...``) -- SOCKS4 (``proxy_url='socks4://...``) -- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) -- SOCKS5 with local DNS (``proxy_url='socks5://...``) -- Usernames and passwords for the SOCKS proxy - -.. note:: - It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in - your ``proxy_url`` to ensure that DNS resolution is done from the remote - server instead of client-side when connecting to a domain name. - -SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 -supports IPv4, IPv6, and domain names. - -When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` -will be sent as the ``userid`` section of the SOCKS request: - -.. code-block:: python - - proxy_url="socks4a://@proxy-host" - -When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion -of the ``proxy_url`` will be sent as the username/password to authenticate -with the proxy: - -.. code-block:: python - - proxy_url="socks5h://:@proxy-host" - -""" - -from __future__ import annotations - -try: - import socks # type: ignore[import-untyped] -except ImportError: - import warnings - - from ..exceptions import DependencyWarning - - warnings.warn( - ( - "SOCKS support in urllib3 requires the installation of optional " - "dependencies: specifically, PySocks. For more information, see " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" - ), - DependencyWarning, - ) - raise - -import typing -from socket import timeout as SocketTimeout - -from ..connection import HTTPConnection, HTTPSConnection -from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from ..exceptions import ConnectTimeoutError, NewConnectionError -from ..poolmanager import PoolManager -from ..util.url import parse_url - -try: - import ssl -except ImportError: - ssl = None # type: ignore[assignment] - - -class _TYPE_SOCKS_OPTIONS(typing.TypedDict): - socks_version: int - proxy_host: str | None - proxy_port: str | None - username: str | None - password: str | None - rdns: bool - - -class SOCKSConnection(HTTPConnection): - """ - A plain-text HTTP connection that connects via a SOCKS proxy. - """ - - def __init__( - self, - _socks_options: _TYPE_SOCKS_OPTIONS, - *args: typing.Any, - **kwargs: typing.Any, - ) -> None: - self._socks_options = _socks_options - super().__init__(*args, **kwargs) - - def _new_conn(self) -> socks.socksocket: - """ - Establish a new connection via the SOCKS proxy. - """ - extra_kw: dict[str, typing.Any] = {} - if self.source_address: - extra_kw["source_address"] = self.source_address - - if self.socket_options: - extra_kw["socket_options"] = self.socket_options - - try: - conn = socks.create_connection( - (self.host, self.port), - proxy_type=self._socks_options["socks_version"], - proxy_addr=self._socks_options["proxy_host"], - proxy_port=self._socks_options["proxy_port"], - proxy_username=self._socks_options["username"], - proxy_password=self._socks_options["password"], - proxy_rdns=self._socks_options["rdns"], - timeout=self.timeout, - **extra_kw, - ) - - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except socks.ProxyError as e: - # This is fragile as hell, but it seems to be the only way to raise - # useful errors here. - if e.socket_err: - error = e.socket_err - if isinstance(error, SocketTimeout): - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - else: - # Adding `from e` messes with coverage somehow, so it's omitted. - # See #2386. - raise NewConnectionError( - self, f"Failed to establish a new connection: {error}" - ) - else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - except OSError as e: # Defensive: PySocks should catch all these. - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - return conn - - -# We don't need to duplicate the Verified/Unverified distinction from -# urllib3/connection.py here because the HTTPSConnection will already have been -# correctly set to either the Verified or Unverified form by that module. This -# means the SOCKSHTTPSConnection will automatically be the correct type. -class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): - pass - - -class SOCKSHTTPConnectionPool(HTTPConnectionPool): - ConnectionCls = SOCKSConnection - - -class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): - ConnectionCls = SOCKSHTTPSConnection - - -class SOCKSProxyManager(PoolManager): - """ - A version of the urllib3 ProxyManager that routes connections via the - defined SOCKS proxy. - """ - - pool_classes_by_scheme = { - "http": SOCKSHTTPConnectionPool, - "https": SOCKSHTTPSConnectionPool, - } - - def __init__( - self, - proxy_url: str, - username: str | None = None, - password: str | None = None, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ): - parsed = parse_url(proxy_url) - - if username is None and password is None and parsed.auth is not None: - split = parsed.auth.split(":") - if len(split) == 2: - username, password = split - if parsed.scheme == "socks5": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = False - elif parsed.scheme == "socks5h": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = True - elif parsed.scheme == "socks4": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = False - elif parsed.scheme == "socks4a": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = True - else: - raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") - - self.proxy_url = proxy_url - - socks_options = { - "socks_version": socks_version, - "proxy_host": parsed.host, - "proxy_port": parsed.port, - "username": username, - "password": password, - "rdns": rdns, - } - connection_pool_kw["_socks_options"] = socks_options - - super().__init__(num_pools, headers, **connection_pool_kw) - - self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py deleted file mode 100644 index 3d7e9b93d3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/exceptions.py +++ /dev/null @@ -1,335 +0,0 @@ -from __future__ import annotations - -import socket -import typing -import warnings -from email.errors import MessageDefect -from http.client import IncompleteRead as httplib_IncompleteRead - -if typing.TYPE_CHECKING: - from .connection import HTTPConnection - from .connectionpool import ConnectionPool - from .response import HTTPResponse - from .util.retry import Retry - -# Base Exceptions - - -class HTTPError(Exception): - """Base exception used by this module.""" - - -class HTTPWarning(Warning): - """Base warning used by this module.""" - - -_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] - - -class PoolError(HTTPError): - """Base exception for errors caused within a pool.""" - - def __init__(self, pool: ConnectionPool, message: str) -> None: - self.pool = pool - self._message = message - super().__init__(f"{pool}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - -class RequestError(PoolError): - """Base exception for PoolErrors that have associated URLs.""" - - def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: - self.url = url - super().__init__(pool, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self._message) - - -class SSLError(HTTPError): - """Raised when SSL certificate fails in an HTTPS connection.""" - - -class ProxyError(HTTPError): - """Raised when the connection to a proxy fails.""" - - # The original error is also available as __cause__. - original_error: Exception - - def __init__(self, message: str, error: Exception) -> None: - super().__init__(message, error) - self.original_error = error - - -class DecodeError(HTTPError): - """Raised when automatic decoding based on Content-Type fails.""" - - -class ProtocolError(HTTPError): - """Raised when something unexpected happens mid-request/response.""" - - -#: Renamed to ProtocolError but aliased for backwards compatibility. -ConnectionError = ProtocolError - - -# Leaf Exceptions - - -class MaxRetryError(RequestError): - """Raised when the maximum number of retries is exceeded. - - :param pool: The connection pool - :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` - :param str url: The requested Url - :param reason: The underlying error - :type reason: :class:`Exception` - - """ - - def __init__( - self, pool: ConnectionPool, url: str | None, reason: Exception | None = None - ) -> None: - self.reason = reason - - message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" - - super().__init__(pool, url, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self.reason) - - -class HostChangedError(RequestError): - """Raised when an existing pool gets a request for a foreign host.""" - - def __init__( - self, pool: ConnectionPool, url: str, retries: Retry | int = 3 - ) -> None: - message = f"Tried to open a foreign host with url: {url}" - super().__init__(pool, url, message) - self.retries = retries - - -class TimeoutStateError(HTTPError): - """Raised when passing an invalid state to a timeout""" - - -class TimeoutError(HTTPError): - """Raised when a socket timeout error occurs. - - Catching this error will catch both :exc:`ReadTimeoutErrors - ` and :exc:`ConnectTimeoutErrors `. - """ - - -class ReadTimeoutError(TimeoutError, RequestError): - """Raised when a socket timeout occurs while receiving data from a server""" - - -# This timeout error does not have a URL attached and needs to inherit from the -# base HTTPError -class ConnectTimeoutError(TimeoutError): - """Raised when a socket timeout occurs while connecting to a server""" - - -class NewConnectionError(ConnectTimeoutError, HTTPError): - """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" - - def __init__(self, conn: HTTPConnection, message: str) -> None: - self.conn = conn - self._message = message - super().__init__(f"{conn}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - @property - def pool(self) -> HTTPConnection: - warnings.warn( - "The 'pool' property is deprecated and will be removed " - "in urllib3 v3.0. Use 'conn' instead.", - FutureWarning, - stacklevel=2, - ) - - return self.conn - - -class NameResolutionError(NewConnectionError): - """Raised when host name resolution fails.""" - - def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): - message = f"Failed to resolve '{host}' ({reason})" - self._host = host - self._reason = reason - super().__init__(conn, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (self._host, None, self._reason) - - -class EmptyPoolError(PoolError): - """Raised when a pool runs out of connections and no more are allowed.""" - - -class FullPoolError(PoolError): - """Raised when we try to add a connection to a full pool in blocking mode.""" - - -class ClosedPoolError(PoolError): - """Raised when a request enters a pool after the pool has been closed.""" - - -class LocationValueError(ValueError, HTTPError): - """Raised when there is something wrong with a given URL input.""" - - -class LocationParseError(LocationValueError): - """Raised when get_host or similar fails to parse the URL input.""" - - def __init__(self, location: str) -> None: - message = f"Failed to parse: {location}" - super().__init__(message) - - self.location = location - - -class URLSchemeUnknown(LocationValueError): - """Raised when a URL input has an unsupported scheme.""" - - def __init__(self, scheme: str): - message = f"Not supported URL scheme {scheme}" - super().__init__(message) - - self.scheme = scheme - - -class ResponseError(HTTPError): - """Used as a container for an error reason supplied in a MaxRetryError.""" - - GENERIC_ERROR = "too many error responses" - SPECIFIC_ERROR = "too many {status_code} error responses" - - -class SecurityWarning(HTTPWarning): - """Warned when performing security reducing actions""" - - -class InsecureRequestWarning(SecurityWarning): - """Warned when making an unverified HTTPS request.""" - - -class NotOpenSSLWarning(SecurityWarning): - """Warned when using unsupported SSL library""" - - -class SystemTimeWarning(SecurityWarning): - """Warned when system time is suspected to be wrong""" - - -class InsecurePlatformWarning(SecurityWarning): - """Warned when certain TLS/SSL configuration is not available on a platform.""" - - -class DependencyWarning(HTTPWarning): - """ - Warned when an attempt is made to import a module with missing optional - dependencies. - """ - - -class ResponseNotChunked(ProtocolError, ValueError): - """Response needs to be chunked in order to read it as chunks.""" - - -class BodyNotHttplibCompatible(HTTPError): - """ - Body should be :class:`http.client.HTTPResponse` like - (have an fp attribute which returns raw chunks) for read_chunked(). - """ - - -class IncompleteRead(HTTPError, httplib_IncompleteRead): - """ - Response length doesn't match expected Content-Length - - Subclass of :class:`http.client.IncompleteRead` to allow int value - for ``partial`` to avoid creating large objects on streamed reads. - """ - - partial: int # type: ignore[assignment] - expected: int - - def __init__(self, partial: int, expected: int) -> None: - self.partial = partial - self.expected = expected - - def __repr__(self) -> str: - return "IncompleteRead(%i bytes read, %i more expected)" % ( - self.partial, - self.expected, - ) - - -class InvalidChunkLength(HTTPError, httplib_IncompleteRead): - """Invalid chunk length in a chunked response.""" - - def __init__(self, response: HTTPResponse, length: bytes) -> None: - self.partial: int = response.tell() # type: ignore[assignment] - self.expected: int | None = response.length_remaining - self.response = response - self.length = length - - def __repr__(self) -> str: - return "InvalidChunkLength(got length %r, %i bytes read)" % ( - self.length, - self.partial, - ) - - -class InvalidHeader(HTTPError): - """The header provided was somehow invalid.""" - - -class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): - """ProxyManager does not support the supplied scheme""" - - # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. - - def __init__(self, scheme: str | None) -> None: - # 'localhost' is here because our URL parser parses - # localhost:8080 -> scheme=localhost, remove if we fix this. - if scheme == "localhost": - scheme = None - if scheme is None: - message = "Proxy URL had no scheme, should start with http:// or https://" - else: - message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" - super().__init__(message) - - -class ProxySchemeUnsupported(ValueError): - """Fetching HTTPS resources through HTTPS proxies is unsupported""" - - -class HeaderParsingError(HTTPError): - """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" - - def __init__( - self, defects: list[MessageDefect], unparsed_data: bytes | str | None - ) -> None: - message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" - super().__init__(message) - - -class UnrewindableBodyError(HTTPError): - """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py deleted file mode 100644 index fe68e17732..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/fields.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import email.utils -import mimetypes -import typing - -_TYPE_FIELD_VALUE = typing.Union[str, bytes] -_TYPE_FIELD_VALUE_TUPLE = typing.Union[ - _TYPE_FIELD_VALUE, - tuple[str, _TYPE_FIELD_VALUE], - tuple[str, _TYPE_FIELD_VALUE, str], -] - - -def guess_content_type( - filename: str | None, default: str = "application/octet-stream" -) -> str: - """ - Guess the "Content-Type" of a file. - - :param filename: - The filename to guess the "Content-Type" of using :mod:`mimetypes`. - :param default: - If no "Content-Type" can be guessed, default to `default`. - """ - if filename: - return mimetypes.guess_type(filename)[0] or default - return default - - -def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Helper function to format and quote a single header parameter using the - strategy defined in RFC 2231. - - Particularly useful for header parameters which might contain - non-ASCII values, like file names. This follows - `RFC 2388 Section 4.4 `_. - - :param name: - The name of the parameter, a string expected to be ASCII only. - :param value: - The value of the parameter, provided as ``bytes`` or `str``. - :returns: - An RFC-2231-formatted unicode string. - - .. deprecated:: 2.0.0 - Will be removed in urllib3 v3.0. This is not valid for - ``multipart/form-data`` header parameters. - """ - import warnings - - warnings.warn( - "'format_header_param_rfc2231' is insecure, deprecated and will be " - "removed in urllib3 v3.0. This is not valid for " - "multipart/form-data header parameters.", - FutureWarning, - stacklevel=2, - ) - - if isinstance(value, bytes): - value = value.decode("utf-8") - - if not any(ch in value for ch in '"\\\r\n'): - result = f'{name}="{value}"' - try: - result.encode("ascii") - except (UnicodeEncodeError, UnicodeDecodeError): - pass - else: - return result - - value = email.utils.encode_rfc2231(value, "utf-8") - value = f"{name}*={value}" - - return value - - -def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Format and quote a single multipart header parameter. - - This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching - the behavior of current browser and curl versions. Values are - assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are - percent encoded. - - .. _WHATWG HTML Standard: - https://html.spec.whatwg.org/multipage/ - form-control-infrastructure.html#multipart-form-data - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - :returns: - A string ``name="value"`` with the escaped value. - - .. versionchanged:: 2.0.0 - Matches the WHATWG HTML Standard as of 2021/06/10. Control - characters are no longer percent encoded. - - .. versionchanged:: 2.0.0 - Renamed from ``format_header_param_html5`` and - ``format_header_param``. The old names will be removed in - urllib3 v3.0. - """ - if isinstance(value, bytes): - value = value.decode("utf-8") - - # percent encode \n \r " - value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) - return f'{name}="{value}"' - - -def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param_html5' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -class RequestField: - """ - A data container for request body parameters. - - :param name: - The name of this request field. Must be unicode. - :param data: - The data/value body. - :param filename: - An optional filename of the request field. Must be unicode. - :param headers: - An optional dict-like object of headers to initially use for the field. - - .. versionchanged:: 2.0.0 - The ``header_formatter`` parameter is deprecated and will - be removed in urllib3 v3.0. - """ - - def __init__( - self, - name: str, - data: _TYPE_FIELD_VALUE, - filename: str | None = None, - headers: typing.Mapping[str, str] | None = None, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ): - self._name = name - self._filename = filename - self.data = data - self.headers: dict[str, str | None] = {} - if headers: - self.headers = dict(headers) - - if header_formatter is not None: - import warnings - - warnings.warn( - "The 'header_formatter' parameter is deprecated and " - "will be removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - self.header_formatter = header_formatter - else: - self.header_formatter = format_multipart_header_param - - @classmethod - def from_tuples( - cls, - fieldname: str, - value: _TYPE_FIELD_VALUE_TUPLE, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ) -> RequestField: - """ - A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. - - Supports constructing :class:`~urllib3.fields.RequestField` from - parameter of key/value strings AND key/filetuple. A filetuple is a - (filename, data, MIME type) tuple where the MIME type is optional. - For example:: - - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - - Field names and filenames must be unicode. - """ - filename: str | None - content_type: str | None - data: _TYPE_FIELD_VALUE - - if isinstance(value, tuple): - if len(value) == 3: - filename, data, content_type = value - else: - filename, data = value - content_type = guess_content_type(filename) - else: - filename = None - content_type = None - data = value - - request_param = cls( - fieldname, data, filename=filename, header_formatter=header_formatter - ) - request_param.make_multipart(content_type=content_type) - - return request_param - - def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Override this method to change how each multipart header - parameter is formatted. By default, this calls - :func:`format_multipart_header_param`. - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - - :meta public: - """ - return self.header_formatter(name, value) - - def _render_parts( - self, - header_parts: ( - dict[str, _TYPE_FIELD_VALUE | None] - | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] - ), - ) -> str: - """ - Helper function to format and quote a single header. - - Useful for single headers that are composed of multiple items. E.g., - 'Content-Disposition' fields. - - :param header_parts: - A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format - as `k1="v1"; k2="v2"; ...`. - """ - iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] - - parts = [] - if isinstance(header_parts, dict): - iterable = header_parts.items() - else: - iterable = header_parts - - for name, value in iterable: - if value is not None: - parts.append(self._render_part(name, value)) - - return "; ".join(parts) - - def render_headers(self) -> str: - """ - Renders the headers for this request field. - """ - lines = [] - - sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] - for sort_key in sort_keys: - if self.headers.get(sort_key, False): - lines.append(f"{sort_key}: {self.headers[sort_key]}") - - for header_name, header_value in self.headers.items(): - if header_name not in sort_keys: - if header_value: - lines.append(f"{header_name}: {header_value}") - - lines.append("\r\n") - return "\r\n".join(lines) - - def make_multipart( - self, - content_disposition: str | None = None, - content_type: str | None = None, - content_location: str | None = None, - ) -> None: - """ - Makes this request field into a multipart request field. - - This method overrides "Content-Disposition", "Content-Type" and - "Content-Location" headers to the request parameter. - - :param content_disposition: - The 'Content-Disposition' of the request body. Defaults to 'form-data' - :param content_type: - The 'Content-Type' of the request body. - :param content_location: - The 'Content-Location' of the request body. - - """ - content_disposition = (content_disposition or "form-data") + "; ".join( - [ - "", - self._render_parts( - (("name", self._name), ("filename", self._filename)) - ), - ] - ) - - self.headers["Content-Disposition"] = content_disposition - self.headers["Content-Type"] = content_type - self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py deleted file mode 100644 index 14f70b05b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/filepost.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -import binascii -import codecs -import os -import typing -from io import BytesIO - -from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField - -writer = codecs.lookup("utf-8")[3] - -_TYPE_FIELDS_SEQUENCE = typing.Sequence[ - typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] -] -_TYPE_FIELDS = typing.Union[ - _TYPE_FIELDS_SEQUENCE, - typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], -] - - -def choose_boundary() -> str: - """ - Our embarrassingly-simple replacement for mimetools.choose_boundary. - """ - return binascii.hexlify(os.urandom(16)).decode() - - -def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: - """ - Iterate over fields. - - Supports list of (k, v) tuples and dicts, and lists of - :class:`~urllib3.fields.RequestField`. - - """ - iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] - - if isinstance(fields, typing.Mapping): - iterable = fields.items() - else: - iterable = fields - - for field in iterable: - if isinstance(field, RequestField): - yield field - else: - yield RequestField.from_tuples(*field) - - -def encode_multipart_formdata( - fields: _TYPE_FIELDS, boundary: str | None = None -) -> tuple[bytes, str]: - """ - Encode a dictionary of ``fields`` using the multipart/form-data MIME format. - - :param fields: - Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). - Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. - - :param boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - body = BytesIO() - if boundary is None: - boundary = choose_boundary() - - for field in iter_field_objects(fields): - body.write(f"--{boundary}\r\n".encode("latin-1")) - - writer(body).write(field.render_headers()) - data = field.data - - if isinstance(data, int): - data = str(data) # Backwards compatibility - - if isinstance(data, str): - writer(body).write(data) - else: - body.write(data) - - body.write(b"\r\n") - - body.write(f"--{boundary}--\r\n".encode("latin-1")) - - content_type = f"multipart/form-data; boundary={boundary}" - - return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py deleted file mode 100644 index 133e1d8f23..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from importlib.metadata import version - -__all__ = [ - "inject_into_urllib3", - "extract_from_urllib3", -] - -import typing - -orig_HTTPSConnection: typing.Any = None - - -def inject_into_urllib3() -> None: - # First check if h2 version is valid - h2_version = version("h2") - if not h2_version.startswith("4."): - raise ImportError( - "urllib3 v2 supports h2 version 4.x.x, currently " - f"the 'h2' module is compiled with {h2_version!r}. " - "See: https://github.com/urllib3/urllib3/issues/3290" - ) - - # Import here to avoid circular dependencies. - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - from .connection import HTTP2Connection - - global orig_HTTPSConnection - orig_HTTPSConnection = urllib3_connection.HTTPSConnection - - HTTPSConnectionPool.ConnectionCls = HTTP2Connection - urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] - - # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. - urllib3_util.ALPN_PROTOCOLS = ["h2"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] - - -def extract_from_urllib3() -> None: - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - - HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection - urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] - - urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py deleted file mode 100644 index 0a026da0a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/connection.py +++ /dev/null @@ -1,356 +0,0 @@ -from __future__ import annotations - -import logging -import re -import threading -import types -import typing - -import h2.config -import h2.connection -import h2.events - -from .._base_connection import _TYPE_BODY -from .._collections import HTTPHeaderDict -from ..connection import HTTPSConnection, _get_default_user_agent -from ..exceptions import ConnectionError -from ..response import BaseHTTPResponse - -orig_HTTPSConnection = HTTPSConnection - -T = typing.TypeVar("T") - -log = logging.getLogger(__name__) - -RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") -RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") - - -def _is_legal_header_name(name: bytes) -> bool: - """ - "An implementation that validates fields according to the definitions in Sections - 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not - include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - - `http.client._is_legal_header_name` does not validate the field name according to the - HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. - - This does not allow for the `:` character in the header name, so should not - be used to validate pseudo-headers. - """ - return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) - - -def _is_illegal_header_value(value: bytes) -> bool: - """ - "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed - (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field - value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, - 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - """ - return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) - - -class _LockedObject(typing.Generic[T]): - """ - A wrapper class that hides a specific object behind a lock. - The goal here is to provide a simple way to protect access to an object - that cannot safely be simultaneously accessed from multiple threads. The - intended use of this class is simple: take hold of it with a context - manager, which returns the protected object. - """ - - __slots__ = ( - "lock", - "_obj", - ) - - def __init__(self, obj: T): - self.lock = threading.RLock() - self._obj = obj - - def __enter__(self) -> T: - self.lock.acquire() - return self._obj - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> None: - self.lock.release() - - -class HTTP2Connection(HTTPSConnection): - def __init__( - self, host: str, port: int | None = None, **kwargs: typing.Any - ) -> None: - self._h2_conn = self._new_h2_conn() - self._h2_stream: int | None = None - self._headers: list[tuple[bytes, bytes]] = [] - - if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: - raise NotImplementedError("Proxies aren't supported with HTTP/2") - - super().__init__(host, port, **kwargs) - - if self._tunnel_host is not None: - raise NotImplementedError("Tunneling isn't supported with HTTP/2") - - def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: - config = h2.config.H2Configuration(client_side=True) - return _LockedObject(h2.connection.H2Connection(config=config)) - - def connect(self) -> None: - super().connect() - with self._h2_conn as conn: - conn.initiate_connection() - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - def putrequest( # type: ignore[override] - self, - method: str, - url: str, - **kwargs: typing.Any, - ) -> None: - """putrequest - This deviates from the HTTPConnection method signature since we never need to override - sending accept-encoding headers or the host header. - """ - if "skip_host" in kwargs: - raise NotImplementedError("`skip_host` isn't supported") - if "skip_accept_encoding" in kwargs: - raise NotImplementedError("`skip_accept_encoding` isn't supported") - - self._request_url = url or "/" - self._validate_path(url) # type: ignore[attr-defined] - - if ":" in self.host: - authority = f"[{self.host}]:{self.port or 443}" - else: - authority = f"{self.host}:{self.port or 443}" - - self._headers.append((b":scheme", b"https")) - self._headers.append((b":method", method.encode())) - self._headers.append((b":authority", authority.encode())) - self._headers.append((b":path", url.encode())) - - with self._h2_conn as conn: - self._h2_stream = conn.get_next_available_stream_id() - - def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] - # TODO SKIPPABLE_HEADERS from urllib3 are ignored. - header = header.encode() if isinstance(header, str) else header - header = header.lower() # A lot of upstream code uses capitalized headers. - if not _is_legal_header_name(header): - raise ValueError(f"Illegal header name {str(header)}") - - for value in values: - value = value.encode() if isinstance(value, str) else value - if _is_illegal_header_value(value): - raise ValueError(f"Illegal header value {str(value)}") - self._headers.append((header, value)) - - def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - conn.send_headers( - stream_id=self._h2_stream, - headers=self._headers, - end_stream=(message_body is None), - ) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - self._headers = [] # Reset headers for the next request. - - def send(self, data: typing.Any) -> None: - """Send data to the server. - `data` can be: `str`, `bytes`, an iterable, or file-like objects - that support a .read() method. - """ - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - if hasattr(data, "read"): # file-like objects - while True: - chunk = data.read(self.blocksize) - if not chunk: - break - if isinstance(chunk, str): - chunk = chunk.encode() - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - return - - if isinstance(data, str): # str -> bytes - data = data.encode() - - try: - if isinstance(data, bytes): - conn.send_data(self._h2_stream, data, end_stream=True) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - else: - for chunk in data: - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - except TypeError: - raise TypeError( - "`data` should be str, bytes, iterable, or file. got %r" - % type(data) - ) - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - raise NotImplementedError( - "HTTP/2 does not support setting up a tunnel through a proxy" - ) - - def getresponse( # type: ignore[override] - self, - ) -> HTTP2Response: - status = None - data = bytearray() - with self._h2_conn as conn: - end_stream = False - while not end_stream: - # TODO: Arbitrary read value. - if received_data := self.sock.recv(65535): - events = conn.receive_data(received_data) - for event in events: - if isinstance(event, h2.events.ResponseReceived): - headers = HTTPHeaderDict() - for header, value in event.headers: - if header == b":status": - status = int(value.decode()) - else: - headers.add( - header.decode("ascii"), value.decode("ascii") - ) - - elif isinstance(event, h2.events.DataReceived): - data += event.data - conn.acknowledge_received_data( - event.flow_controlled_length, event.stream_id - ) - - elif isinstance(event, h2.events.StreamEnded): - end_stream = True - - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - assert status is not None - return HTTP2Response( - status=status, - headers=headers, - request_url=self._request_url, - data=bytes(data), - ) - - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - **kwargs: typing.Any, - ) -> None: - """Send an HTTP/2 request""" - if "chunked" in kwargs: - # TODO this is often present from upstream. - # raise NotImplementedError("`chunked` isn't supported with HTTP/2") - pass - - if self.sock is not None: - self.sock.settimeout(self.timeout) - - self.putrequest(method, url) - - headers = headers or {} - for k, v in headers.items(): - if k.lower() == "transfer-encoding" and v == "chunked": - continue - else: - self.putheader(k, v) - - if b"user-agent" not in dict(self._headers): - self.putheader(b"user-agent", _get_default_user_agent()) - - if body: - self.endheaders(message_body=body) - self.send(body) - else: - self.endheaders() - - def close(self) -> None: - with self._h2_conn as conn: - try: - conn.close_connection() - if data := conn.data_to_send(): - self.sock.sendall(data) - except Exception: - pass - - # Reset all our HTTP/2 connection state. - self._h2_conn = self._new_h2_conn() - self._h2_stream = None - self._headers = [] - - super().close() - - -class HTTP2Response(BaseHTTPResponse): - # TODO: This is a woefully incomplete response object, but works for non-streaming. - def __init__( - self, - status: int, - headers: HTTPHeaderDict, - request_url: str, - data: bytes, - decode_content: bool = False, # TODO: support decoding - ) -> None: - super().__init__( - status=status, - headers=headers, - # Following CPython, we map HTTP versions to major * 10 + minor integers - version=20, - version_string="HTTP/2", - # No reason phrase in HTTP/2 - reason=None, - decode_content=decode_content, - request_url=request_url, - ) - self._data = data - self.length_remaining = 0 - - @property - def data(self) -> bytes: - return self._data - - def get_redirect_location(self) -> None: - return None - - def close(self) -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py deleted file mode 100644 index 9ea900764f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/http2/probe.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import threading - - -class _HTTP2ProbeCache: - __slots__ = ( - "_lock", - "_cache_locks", - "_cache_values", - ) - - def __init__(self) -> None: - self._lock = threading.Lock() - self._cache_locks: dict[tuple[str, int], threading.RLock] = {} - self._cache_values: dict[tuple[str, int], bool | None] = {} - - def acquire_and_get(self, host: str, port: int) -> bool | None: - # By the end of this block we know that - # _cache_[values,locks] is available. - value = None - with self._lock: - key = (host, port) - try: - value = self._cache_values[key] - # If it's a known value we return right away. - if value is not None: - return value - except KeyError: - self._cache_locks[key] = threading.RLock() - self._cache_values[key] = None - - # If the value is unknown, we acquire the lock to signal - # to the requesting thread that the probe is in progress - # or that the current thread needs to return their findings. - key_lock = self._cache_locks[key] - key_lock.acquire() - try: - # If the by the time we get the lock the value has been - # updated we want to return the updated value. - value = self._cache_values[key] - - # In case an exception like KeyboardInterrupt is raised here. - except BaseException as e: # Defensive: - assert not isinstance(e, KeyError) # KeyError shouldn't be possible. - key_lock.release() - raise - - return value - - def set_and_release( - self, host: str, port: int, supports_http2: bool | None - ) -> None: - key = (host, port) - key_lock = self._cache_locks[key] - with key_lock: # Uses an RLock, so can be locked again from same thread. - if supports_http2 is None and self._cache_values[key] is not None: - raise ValueError( - "Cannot reset HTTP/2 support for origin after value has been set." - ) # Defensive: not expected in normal usage - - self._cache_values[key] = supports_http2 - key_lock.release() - - def _values(self) -> dict[tuple[str, int], bool | None]: - """This function is for testing purposes only. Gets the current state of the probe cache""" - with self._lock: - return {k: v for k, v in self._cache_values.items()} - - def _reset(self) -> None: - """This function is for testing purposes only. Reset the cache values""" - with self._lock: - self._cache_locks = {} - self._cache_values = {} - - -_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() - -set_and_release = _HTTP2_PROBE_CACHE.set_and_release -acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get -_values = _HTTP2_PROBE_CACHE._values -_reset = _HTTP2_PROBE_CACHE._reset - -__all__ = [ - "set_and_release", - "acquire_and_get", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py deleted file mode 100644 index 8f2c56745c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/poolmanager.py +++ /dev/null @@ -1,653 +0,0 @@ -from __future__ import annotations - -import functools -import logging -import typing -import warnings -from types import TracebackType -from urllib.parse import urljoin - -from ._collections import HTTPHeaderDict, RecentlyUsedContainer -from ._request_methods import RequestMethods -from .connection import ProxyConfig -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme -from .exceptions import ( - LocationValueError, - MaxRetryError, - ProxySchemeUnknown, - URLSchemeUnknown, -) -from .response import BaseHTTPResponse -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.proxy import connection_requires_http_tunnel -from .util.retry import Retry -from .util.timeout import Timeout -from .util.url import Url, parse_url - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - -__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] - - -log = logging.getLogger(__name__) - -SSL_KEYWORDS = ( - "key_file", - "cert_file", - "cert_reqs", - "ca_certs", - "ca_cert_data", - "ssl_version", - "ssl_minimum_version", - "ssl_maximum_version", - "ca_cert_dir", - "ssl_context", - "key_password", - "server_hostname", -) -# Default value for `blocksize` - a new parameter introduced to -# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 -_DEFAULT_BLOCKSIZE = 16384 - - -class PoolKey(typing.NamedTuple): - """ - All known keyword arguments that could be provided to the pool manager, its - pools, or the underlying connections. - - All custom key schemes should include the fields in this key at a minimum. - """ - - key_scheme: str - key_host: str - key_port: int | None - key_timeout: Timeout | float | int | None - key_retries: Retry | bool | int | None - key_block: bool | None - key_source_address: tuple[str, int] | None - key_key_file: str | None - key_key_password: str | None - key_cert_file: str | None - key_cert_reqs: str | None - key_ca_certs: str | None - key_ca_cert_data: str | bytes | None - key_ssl_version: int | str | None - key_ssl_minimum_version: ssl.TLSVersion | None - key_ssl_maximum_version: ssl.TLSVersion | None - key_ca_cert_dir: str | None - key_ssl_context: ssl.SSLContext | None - key_maxsize: int | None - key_headers: frozenset[tuple[str, str]] | None - key__proxy: Url | None - key__proxy_headers: frozenset[tuple[str, str]] | None - key__proxy_config: ProxyConfig | None - key_socket_options: _TYPE_SOCKET_OPTIONS | None - key__socks_options: frozenset[tuple[str, str]] | None - key_assert_hostname: bool | str | None - key_assert_fingerprint: str | None - key_server_hostname: str | None - key_blocksize: int | None - - -def _default_key_normalizer( - key_class: type[PoolKey], request_context: dict[str, typing.Any] -) -> PoolKey: - """ - Create a pool key out of a request context dictionary. - - According to RFC 3986, both the scheme and host are case-insensitive. - Therefore, this function normalizes both before constructing the pool - key for an HTTPS request. If you wish to change this behaviour, provide - alternate callables to ``key_fn_by_scheme``. - - :param key_class: - The class to use when constructing the key. This should be a namedtuple - with the ``scheme`` and ``host`` keys at a minimum. - :type key_class: namedtuple - :param request_context: - A dictionary-like object that contain the context for a request. - :type request_context: dict - - :return: A namedtuple that can be used as a connection pool key. - :rtype: PoolKey - """ - # Since we mutate the dictionary, make a copy first - context = request_context.copy() - context["scheme"] = context["scheme"].lower() - context["host"] = context["host"].lower() - - # These are both dictionaries and need to be transformed into frozensets - for key in ("headers", "_proxy_headers", "_socks_options"): - if key in context and context[key] is not None: - context[key] = frozenset(context[key].items()) - - # The socket_options key may be a list and needs to be transformed into a - # tuple. - socket_opts = context.get("socket_options") - if socket_opts is not None: - context["socket_options"] = tuple(socket_opts) - - # Map the kwargs to the names in the namedtuple - this is necessary since - # namedtuples can't have fields starting with '_'. - for key in list(context.keys()): - context["key_" + key] = context.pop(key) - - # Default to ``None`` for keys missing from the context - for field in key_class._fields: - if field not in context: - context[field] = None - - # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context - if context.get("key_blocksize") is None: - context["key_blocksize"] = _DEFAULT_BLOCKSIZE - - return key_class(**context) - - -#: A dictionary that maps a scheme to a callable that creates a pool key. -#: This can be used to alter the way pool keys are constructed, if desired. -#: Each PoolManager makes a copy of this dictionary so they can be configured -#: globally here, or individually on the instance. -key_fn_by_scheme = { - "http": functools.partial(_default_key_normalizer, PoolKey), - "https": functools.partial(_default_key_normalizer, PoolKey), -} - -pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} - - -class PoolManager(RequestMethods): - """ - Allows for arbitrary requests while transparently keeping track of - necessary connection pools for you. - - :param num_pools: - Number of connection pools to cache before discarding the least - recently used pool. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param \\**connection_pool_kw: - Additional parameters are used to create fresh - :class:`urllib3.connectionpool.ConnectionPool` instances. - - Example: - - .. code-block:: python - - import urllib3 - - http = urllib3.PoolManager(num_pools=2) - - resp1 = http.request("GET", "https://google.com/") - resp2 = http.request("GET", "https://google.com/mail") - resp3 = http.request("GET", "https://yahoo.com/") - - print(len(http.pools)) - # 2 - - """ - - proxy: Url | None = None - proxy_config: ProxyConfig | None = None - - def __init__( - self, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - super().__init__(headers) - # PoolManager handles redirects itself in PoolManager.urlopen(). - # It always passes redirect=False to the underlying connection pool to - # suppress per-pool redirect handling. If the user supplied a non-Retry - # value (int/bool/etc) for retries and we let the pool normalize it - # while redirect=False, the resulting Retry object would have redirect - # handling disabled, which can interfere with PoolManager's own - # redirect logic. Normalize here so redirects remain governed solely by - # PoolManager logic. - if "retries" in connection_pool_kw: - retries = connection_pool_kw["retries"] - if not isinstance(retries, Retry): - retries = Retry.from_int(retries) - connection_pool_kw = connection_pool_kw.copy() - connection_pool_kw["retries"] = retries - self.connection_pool_kw = connection_pool_kw - - self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] - self.pools = RecentlyUsedContainer(num_pools) - - # Locally set the pool classes and keys so other PoolManagers can - # override them. - self.pool_classes_by_scheme = pool_classes_by_scheme - self.key_fn_by_scheme = key_fn_by_scheme.copy() - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.clear() - # Return False to re-raise any potential exceptions - return False - - def _new_pool( - self, - scheme: str, - host: str, - port: int, - request_context: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and - any additional pool keyword arguments. - - If ``request_context`` is provided, it is provided as keyword arguments - to the pool class used. This method is used to actually create the - connection pools handed out by :meth:`connection_from_url` and - companion methods. It is intended to be overridden for customization. - """ - pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] - if request_context is None: - request_context = self.connection_pool_kw.copy() - - # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly - # set to 'None' in the request_context. - if request_context.get("blocksize") is None: - request_context["blocksize"] = _DEFAULT_BLOCKSIZE - - # Although the context has everything necessary to create the pool, - # this function has historically only used the scheme, host, and port - # in the positional args. When an API change is acceptable these can - # be removed. - for key in ("scheme", "host", "port"): - request_context.pop(key, None) - - if scheme == "http": - for kw in SSL_KEYWORDS: - request_context.pop(kw, None) - - return pool_cls(host, port, **request_context) - - def clear(self) -> None: - """ - Empty our store of pools and direct them all to close. - - This will not affect in-flight connections, but they will not be - re-used after completion. - """ - self.pools.clear() - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. - - If ``port`` isn't given, it will be derived from the ``scheme`` using - ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is - provided, it is merged with the instance's ``connection_pool_kw`` - variable and used to create the new connection pool, if one is - needed. - """ - - if not host: - raise LocationValueError("No host specified.") - - request_context = self._merge_pool_kwargs(pool_kwargs) - request_context["scheme"] = scheme or "http" - if not port: - port = port_by_scheme.get(request_context["scheme"].lower(), 80) - request_context["port"] = port - request_context["host"] = host - - return self.connection_from_context(request_context) - - def connection_from_context( - self, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. - - ``request_context`` must at least contain the ``scheme`` key and its - value must be a key in ``key_fn_by_scheme`` instance variable. - """ - if "strict" in request_context: - warnings.warn( - "The 'strict' parameter is no longer needed on Python 3+. " - "This will raise an error in urllib3 v3.0.", - FutureWarning, - ) - request_context.pop("strict") - - scheme = request_context["scheme"].lower() - pool_key_constructor = self.key_fn_by_scheme.get(scheme) - if not pool_key_constructor: - raise URLSchemeUnknown(scheme) - pool_key = pool_key_constructor(request_context) - - return self.connection_from_pool_key(pool_key, request_context=request_context) - - def connection_from_pool_key( - self, pool_key: PoolKey, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. - - ``pool_key`` should be a namedtuple that only contains immutable - objects. At a minimum it must have the ``scheme``, ``host``, and - ``port`` fields. - """ - with self.pools.lock: - # If the scheme, host, or port doesn't match existing open - # connections, open a new ConnectionPool. - pool = self.pools.get(pool_key) - if pool: - return pool - - # Make a fresh ConnectionPool of the desired type - scheme = request_context["scheme"] - host = request_context["host"] - port = request_context["port"] - pool = self._new_pool(scheme, host, port, request_context=request_context) - self.pools[pool_key] = pool - - return pool - - def connection_from_url( - self, url: str, pool_kwargs: dict[str, typing.Any] | None = None - ) -> HTTPConnectionPool: - """ - Similar to :func:`urllib3.connectionpool.connection_from_url`. - - If ``pool_kwargs`` is not provided and a new pool needs to be - constructed, ``self.connection_pool_kw`` is used to initialize - the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` - is provided, it is used instead. Note that if a new pool does not - need to be created for the request, the provided ``pool_kwargs`` are - not used. - """ - u = parse_url(url) - return self.connection_from_host( - u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs - ) - - def _merge_pool_kwargs( - self, override: dict[str, typing.Any] | None - ) -> dict[str, typing.Any]: - """ - Merge a dictionary of override values for self.connection_pool_kw. - - This does not modify self.connection_pool_kw and returns a new dict. - Any keys in the override dictionary with a value of ``None`` are - removed from the merged dictionary. - """ - base_pool_kwargs = self.connection_pool_kw.copy() - if override: - for key, value in override.items(): - if value is None: - try: - del base_pool_kwargs[key] - except KeyError: - pass - else: - base_pool_kwargs[key] = value - return base_pool_kwargs - - def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: - """ - Indicates if the proxy requires the complete destination URL in the - request. Normally this is only needed when not using an HTTP CONNECT - tunnel. - """ - if self.proxy is None: - return False - - return not connection_requires_http_tunnel( - self.proxy, self.proxy_config, parsed_url.scheme - ) - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - """ - Same as :meth:`urllib3.HTTPConnectionPool.urlopen` - with custom cross-host redirect logic and only sends the request-uri - portion of the ``url``. - - The given ``url`` parameter must be absolute, such that an appropriate - :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. - """ - u = parse_url(url) - - if u.scheme is None: - warnings.warn( - "URLs without a scheme (ie 'https://') are deprecated and will raise an error " - "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " - "start with 'https://' or 'http://'. Read more in this issue: " - "https://github.com/urllib3/urllib3/issues/2920", - category=FutureWarning, - stacklevel=2, - ) - - conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - - kw["assert_same_host"] = False - kw["redirect"] = False - - if "headers" not in kw: - kw["headers"] = self.headers - - if self._proxy_requires_url_absolute_form(u): - response = conn.urlopen(method, url, **kw) - else: - response = conn.urlopen(method, u.request_uri, **kw) - - redirect_location = redirect and response.get_redirect_location() - if not redirect_location: - return response - - # Support relative URLs for redirecting. - redirect_location = urljoin(url, redirect_location) - - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - kw["body"] = None - kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() - - retries = kw.get("retries", response.retries) - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect) - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # conn.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not conn.is_same_host( - redirect_location - ): - new_headers = kw["headers"].copy() - for header in kw["headers"]: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - kw["headers"] = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=conn) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - kw["retries"] = retries - kw["redirect"] = redirect - - log.info("Redirecting %s -> %s", url, redirect_location) - - response.drain_conn() - return self.urlopen(method, redirect_location, **kw) - - -class ProxyManager(PoolManager): - """ - Behaves just like :class:`PoolManager`, but sends all requests through - the defined proxy, using the CONNECT method for HTTPS URLs. - - :param proxy_url: - The URL of the proxy to be used. - - :param proxy_headers: - A dictionary containing headers that will be sent to the proxy. In case - of HTTP they are being sent with each request, while in the - HTTPS/CONNECT case they are sent only once. Could be used for proxy - authentication. - - :param proxy_ssl_context: - The proxy SSL context is used to establish the TLS connection to the - proxy when using HTTPS proxies. - - :param use_forwarding_for_https: - (Defaults to False) If set to True will forward requests to the HTTPS - proxy to be made on behalf of the client instead of creating a TLS - tunnel via the CONNECT method. **Enabling this flag means that request - and response headers and content will be visible from the HTTPS proxy** - whereas tunneling keeps request and response headers and content - private. IP address, target hostname, SNI, and port are always visible - to an HTTPS proxy even when this flag is disabled. - - :param proxy_assert_hostname: - The hostname of the certificate to verify against. - - :param proxy_assert_fingerprint: - The fingerprint of the certificate to verify against. - - Example: - - .. code-block:: python - - import urllib3 - - proxy = urllib3.ProxyManager("https://localhost:3128/") - - resp1 = proxy.request("GET", "http://google.com/") - resp2 = proxy.request("GET", "http://httpbin.org/") - - # One pool was shared by both plain HTTP requests. - print(len(proxy.pools)) - # 1 - - resp3 = proxy.request("GET", "https://httpbin.org/") - resp4 = proxy.request("GET", "https://twitter.com/") - - # A separate pool was added for each HTTPS target. - print(len(proxy.pools)) - # 3 - - """ - - def __init__( - self, - proxy_url: str, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - proxy_headers: typing.Mapping[str, str] | None = None, - proxy_ssl_context: ssl.SSLContext | None = None, - use_forwarding_for_https: bool = False, - proxy_assert_hostname: None | str | typing.Literal[False] = None, - proxy_assert_fingerprint: str | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - if isinstance(proxy_url, HTTPConnectionPool): - str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" - else: - str_proxy_url = proxy_url - proxy = parse_url(str_proxy_url) - - if proxy.scheme not in ("http", "https"): - raise ProxySchemeUnknown(proxy.scheme) - - if not proxy.port: - port = port_by_scheme.get(proxy.scheme, 80) - proxy = proxy._replace(port=port) - - self.proxy = proxy - self.proxy_headers = proxy_headers or {} - self.proxy_ssl_context = proxy_ssl_context - self.proxy_config = ProxyConfig( - proxy_ssl_context, - use_forwarding_for_https, - proxy_assert_hostname, - proxy_assert_fingerprint, - ) - - connection_pool_kw["_proxy"] = self.proxy - connection_pool_kw["_proxy_headers"] = self.proxy_headers - connection_pool_kw["_proxy_config"] = self.proxy_config - - super().__init__(num_pools, headers, **connection_pool_kw) - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - if scheme == "https": - return super().connection_from_host( - host, port, scheme, pool_kwargs=pool_kwargs - ) - - return super().connection_from_host( - self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] - ) - - def _set_proxy_headers( - self, url: str, headers: typing.Mapping[str, str] | None = None - ) -> typing.Mapping[str, str]: - """ - Sets headers needed by proxies: specifically, the Accept and Host - headers. Only sets headers not provided by the user. - """ - headers_ = {"Accept": "*/*"} - - netloc = parse_url(url).netloc - if netloc: - headers_["Host"] = netloc - - if headers: - headers_.update(headers) - return headers_ - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." - u = parse_url(url) - if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): - # For connections using HTTP CONNECT, httplib sets the necessary - # headers on the CONNECT to the proxy. If we're not using CONNECT, - # we'll definitely need to set 'Host' at the very least. - headers = kw.get("headers", self.headers) - kw["headers"] = self._set_proxy_headers(url, headers) - - return super().urlopen(method, url, redirect=redirect, **kw) - - -def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: - return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed deleted file mode 100644 index 5f3ea3d919..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/py.typed +++ /dev/null @@ -1,2 +0,0 @@ -# Instruct type checkers to look for inline type annotations in this package. -# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py deleted file mode 100644 index e9246b75e3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/response.py +++ /dev/null @@ -1,1493 +0,0 @@ -from __future__ import annotations - -import collections -import io -import json as _json -import logging -import socket -import sys -import typing -import warnings -import zlib -from contextlib import contextmanager -from http.client import HTTPMessage as _HttplibHTTPMessage -from http.client import HTTPResponse as _HttplibHTTPResponse -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from ._base_connection import BaseHTTPConnection - -try: - try: - import brotlicffi as brotli # type: ignore[import-not-found] - except ImportError: - import brotli # type: ignore[import-not-found] -except ImportError: - brotli = None - -from . import util -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .connection import BaseSSLError, HTTPConnection, HTTPException -from .exceptions import ( - BodyNotHttplibCompatible, - DecodeError, - DependencyWarning, - HTTPError, - IncompleteRead, - InvalidChunkLength, - InvalidHeader, - ProtocolError, - ReadTimeoutError, - ResponseNotChunked, - SSLError, -) -from .util.response import is_fp_closed, is_response_to_head -from .util.retry import Retry - -if typing.TYPE_CHECKING: - from .connectionpool import HTTPConnectionPool - -log = logging.getLogger(__name__) - - -class ContentDecoder: - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - raise NotImplementedError() - - @property - def has_unconsumed_tail(self) -> bool: - raise NotImplementedError() - - def flush(self) -> bytes: - raise NotImplementedError() - - -class DeflateDecoder(ContentDecoder): - def __init__(self) -> None: - self._first_try = True - self._first_try_data = b"" - self._unfed_data = b"" - self._obj = zlib.decompressobj() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - data = self._unfed_data + data - self._unfed_data = b"" - if not data and not self._obj.unconsumed_tail: - return data - original_max_length = max_length - if original_max_length < 0: - max_length = 0 - elif original_max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unfed_data = data - return b"" - - # Subsequent calls always reuse `self._obj`. zlib requires - # passing the unconsumed tail if decompression is to continue. - if not self._first_try: - return self._obj.decompress( - self._obj.unconsumed_tail + data, max_length=max_length - ) - - # First call tries with RFC 1950 ZLIB format. - self._first_try_data += data - try: - decompressed = self._obj.decompress(data, max_length=max_length) - if decompressed: - self._first_try = False - self._first_try_data = b"" - return decompressed - # On failure, it falls back to RFC 1951 DEFLATE format. - except zlib.error: - self._first_try = False - self._obj = zlib.decompressobj(-zlib.MAX_WBITS) - try: - return self.decompress( - self._first_try_data, max_length=original_max_length - ) - finally: - self._first_try_data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unfed_data) or ( - bool(self._obj.unconsumed_tail) and not self._first_try - ) - - def flush(self) -> bytes: - return self._obj.flush() - - -class GzipDecoderState: - FIRST_MEMBER = 0 - OTHER_MEMBERS = 1 - SWALLOW_DATA = 2 - - -class GzipDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - self._state = GzipDecoderState.FIRST_MEMBER - self._unconsumed_tail = b"" - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - ret = bytearray() - if self._state == GzipDecoderState.SWALLOW_DATA: - return bytes(ret) - - if max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unconsumed_tail += data - return b"" - - # zlib requires passing the unconsumed tail to the subsequent - # call if decompression is to continue. - data = self._unconsumed_tail + data - if not data and self._obj.eof: - return bytes(ret) - - while True: - try: - ret += self._obj.decompress( - data, max_length=max(max_length - len(ret), 0) - ) - except zlib.error: - previous_state = self._state - # Ignore data after the first error - self._state = GzipDecoderState.SWALLOW_DATA - self._unconsumed_tail = b"" - if previous_state == GzipDecoderState.OTHER_MEMBERS: - # Allow trailing garbage acceptable in other gzip clients - return bytes(ret) - raise - - self._unconsumed_tail = data = ( - self._obj.unconsumed_tail or self._obj.unused_data - ) - if max_length > 0 and len(ret) >= max_length: - break - - if not data: - return bytes(ret) - # When the end of a gzip member is reached, a new decompressor - # must be created for unused (possibly future) data. - if self._obj.eof: - self._state = GzipDecoderState.OTHER_MEMBERS - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - - return bytes(ret) - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unconsumed_tail) - - def flush(self) -> bytes: - return self._obj.flush() - - -if brotli is not None: - - class BrotliDecoder(ContentDecoder): - # Supports both 'brotlipy' and 'Brotli' packages - # since they share an import name. The top branches - # are for 'brotlipy' and bottom branches for 'Brotli' - def __init__(self) -> None: - self._obj = brotli.Decompressor() - if hasattr(self._obj, "decompress"): - setattr(self, "_decompress", self._obj.decompress) - else: - setattr(self, "_decompress", self._obj.process) - - # Requires Brotli >= 1.2.0 for `output_buffer_limit`. - def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: - raise NotImplementedError() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - try: - if max_length > 0: - return self._decompress(data, output_buffer_limit=max_length) - else: - return self._decompress(data) - except TypeError: - # Fallback for Brotli/brotlicffi/brotlipy versions without - # the `output_buffer_limit` parameter. - warnings.warn( - "Brotli >= 1.2.0 is required to prevent decompression bombs.", - DependencyWarning, - ) - return self._decompress(data) - - @property - def has_unconsumed_tail(self) -> bool: - try: - return not self._obj.can_accept_more_data() - except AttributeError: - return False - - def flush(self) -> bytes: - if hasattr(self._obj, "flush"): - return self._obj.flush() # type: ignore[no-any-return] - return b"" - - -try: - if sys.version_info >= (3, 14): - from compression import zstd - else: - from backports import zstd -except ImportError: - HAS_ZSTD = False -else: - HAS_ZSTD = True - - class ZstdDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zstd.ZstdDecompressor() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if not data and not self.has_unconsumed_tail: - return b"" - if self._obj.eof: - data = self._obj.unused_data + data - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress(data, max_length=max_length) - length = len(part) - data_parts = [part] - # Every loop iteration is supposed to read data from a separate frame. - # The loop breaks when: - # - enough data is read; - # - no more unused data is available; - # - end of the last read frame has not been reached (i.e., - # more data has to be fed). - while ( - self._obj.eof - and self._obj.unused_data - and (max_length < 0 or length < max_length) - ): - unused_data = self._obj.unused_data - if not self._obj.needs_input: - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress( - unused_data, - max_length=(max_length - length) if max_length > 0 else -1, - ) - if part_length := len(part): - data_parts.append(part) - length += part_length - elif self._obj.needs_input: - break - return b"".join(data_parts) - - @property - def has_unconsumed_tail(self) -> bool: - return not (self._obj.needs_input or self._obj.eof) or bool( - self._obj.unused_data - ) - - def flush(self) -> bytes: - if not self._obj.eof: - raise DecodeError("Zstandard data is incomplete") - return b"" - - -class MultiDecoder(ContentDecoder): - """ - From RFC7231: - If one or more encodings have been applied to a representation, the - sender that applied the encodings MUST generate a Content-Encoding - header field that lists the content codings in the order in which - they were applied. - """ - - # Maximum allowed number of chained HTTP encodings in the - # Content-Encoding header. - max_decode_links = 5 - - def __init__(self, modes: str) -> None: - encodings = [m.strip() for m in modes.split(",")] - if len(encodings) > self.max_decode_links: - raise DecodeError( - "Too many content encodings in the chain: " - f"{len(encodings)} > {self.max_decode_links}" - ) - self._decoders = [_get_decoder(e) for e in encodings] - - def flush(self) -> bytes: - return self._decoders[0].flush() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if max_length <= 0: - for d in reversed(self._decoders): - data = d.decompress(data) - return data - - ret = bytearray() - # Every while loop iteration goes through all decoders once. - # It exits when enough data is read or no more data can be read. - # It is possible that the while loop iteration does not produce - # any data because we retrieve up to `max_length` from every - # decoder, and the amount of bytes may be insufficient for the - # next decoder to produce enough/any output. - while True: - any_data = False - for d in reversed(self._decoders): - data = d.decompress(data, max_length=max_length - len(ret)) - if data: - any_data = True - # We should not break when no data is returned because - # next decoders may produce data even with empty input. - ret += data - if not any_data or len(ret) >= max_length: - return bytes(ret) - data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return any(d.has_unconsumed_tail for d in self._decoders) - - -def _get_decoder(mode: str) -> ContentDecoder: - if "," in mode: - return MultiDecoder(mode) - - # According to RFC 9110 section 8.4.1.3, recipients should - # consider x-gzip equivalent to gzip - if mode in ("gzip", "x-gzip"): - return GzipDecoder() - - if brotli is not None and mode == "br": - return BrotliDecoder() - - if HAS_ZSTD and mode == "zstd": - return ZstdDecoder() - - return DeflateDecoder() - - -class BytesQueueBuffer: - """Memory-efficient bytes buffer - - To return decoded data in read() and still follow the BufferedIOBase API, we need a - buffer to always return the correct amount of bytes. - - This buffer should be filled using calls to put() - - Our maximum memory usage is determined by the sum of the size of: - - * self.buffer, which contains the full data - * the largest chunk that we will copy in get() - """ - - def __init__(self) -> None: - self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() - self._size: int = 0 - - def __len__(self) -> int: - return self._size - - def put(self, data: bytes) -> None: - self.buffer.append(data) - self._size += len(data) - - def get(self, n: int) -> bytes: - if n == 0: - return b"" - elif not self.buffer: - raise RuntimeError("buffer is empty") - elif n < 0: - raise ValueError("n should be > 0") - - if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): - self._size -= n - return self.buffer.popleft() - - fetched = 0 - ret = io.BytesIO() - while fetched < n: - remaining = n - fetched - chunk = self.buffer.popleft() - chunk_length = len(chunk) - if remaining < chunk_length: - chunk = memoryview(chunk) - left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] - ret.write(left_chunk) - self.buffer.appendleft(right_chunk) - self._size -= remaining - break - else: - ret.write(chunk) - self._size -= chunk_length - fetched += chunk_length - - if not self.buffer: - break - - return ret.getvalue() - - def get_all(self) -> bytes: - buffer = self.buffer - if not buffer: - assert self._size == 0 - return b"" - if len(buffer) == 1: - result = buffer.pop() - if isinstance(result, memoryview): - result = result.tobytes() - else: - ret = io.BytesIO() - ret.writelines(buffer.popleft() for _ in range(len(buffer))) - result = ret.getvalue() - self._size = 0 - return result - - -class BaseHTTPResponse(io.IOBase): - CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] - if brotli is not None: - CONTENT_DECODERS += ["br"] - if HAS_ZSTD: - CONTENT_DECODERS += ["zstd"] - REDIRECT_STATUSES = [301, 302, 303, 307, 308] - - DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) - if brotli is not None: - DECODER_ERROR_CLASSES += (brotli.error,) - - if HAS_ZSTD: - DECODER_ERROR_CLASSES += (zstd.ZstdError,) - - def __init__( - self, - *, - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int, - version: int, - version_string: str, - reason: str | None, - decode_content: bool, - request_url: str | None, - retries: Retry | None = None, - ) -> None: - if isinstance(headers, HTTPHeaderDict): - self.headers = headers - else: - self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] - self.status = status - self.version = version - self.version_string = version_string - self.reason = reason - self.decode_content = decode_content - self._has_decoded_content = False - self._request_url: str | None = request_url - self.retries = retries - - self.chunked = False - tr_enc = self.headers.get("transfer-encoding", "").lower() - # Don't incur the penalty of creating a list and then discarding it - encodings = (enc.strip() for enc in tr_enc.split(",")) - if "chunked" in encodings: - self.chunked = True - - self._decoder: ContentDecoder | None = None - self.length_remaining: int | None - - def get_redirect_location(self) -> str | None | typing.Literal[False]: - """ - Should we redirect and where to? - - :returns: Truthy redirect location string if we got a redirect status - code and valid location. ``None`` if redirect status and no - location. ``False`` if not a redirect status code. - """ - if self.status in self.REDIRECT_STATUSES: - return self.headers.get("location") - return False - - @property - def data(self) -> bytes: - raise NotImplementedError() - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - @property - def url(self) -> str | None: - raise NotImplementedError() - - @url.setter - def url(self, url: str | None) -> None: - raise NotImplementedError() - - @property - def connection(self) -> BaseHTTPConnection | None: - raise NotImplementedError() - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - if retries is not None and retries.history: - self.url = retries.history[-1].redirect_location - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - raise NotImplementedError() - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - raise NotImplementedError() - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def release_conn(self) -> None: - raise NotImplementedError() - - def drain_conn(self) -> None: - raise NotImplementedError() - - def shutdown(self) -> None: - raise NotImplementedError() - - def close(self) -> None: - raise NotImplementedError() - - def _init_decoder(self) -> None: - """ - Set-up the _decoder attribute if necessary. - """ - # Note: content-encoding value should be case-insensitive, per RFC 7230 - # Section 3.2 - content_encoding = self.headers.get("content-encoding", "").lower() - if self._decoder is None: - if content_encoding in self.CONTENT_DECODERS: - self._decoder = _get_decoder(content_encoding) - elif "," in content_encoding: - encodings = [ - e.strip() - for e in content_encoding.split(",") - if e.strip() in self.CONTENT_DECODERS - ] - if encodings: - self._decoder = _get_decoder(content_encoding) - - def _decode( - self, - data: bytes, - decode_content: bool | None, - flush_decoder: bool, - max_length: int | None = None, - ) -> bytes: - """ - Decode the data passed in and potentially flush the decoder. - """ - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - if max_length is None or flush_decoder: - max_length = -1 - - try: - if self._decoder: - data = self._decoder.decompress(data, max_length=max_length) - self._has_decoded_content = True - except self.DECODER_ERROR_CLASSES as e: - content_encoding = self.headers.get("content-encoding", "").lower() - raise DecodeError( - "Received response with content-encoding: %s, but " - "failed to decode it." % content_encoding, - e, - ) from e - if flush_decoder: - data += self._flush_decoder() - - return data - - def _flush_decoder(self) -> bytes: - """ - Flushes the decoder. Should only be called if the decoder is actually - being used. - """ - if self._decoder: - return self._decoder.decompress(b"") + self._decoder.flush() - return b"" - - # Compatibility methods for `io` module - def readinto(self, b: bytearray | memoryview[int]) -> int: - temp = self.read(len(b)) - if len(temp) == 0: - return 0 - else: - b[: len(temp)] = temp - return len(temp) - - # Methods used by dependent libraries - def getheaders(self) -> HTTPHeaderDict: - return self.headers - - def getheader(self, name: str, default: str | None = None) -> str | None: - return self.headers.get(name, default) - - # Compatibility method for http.cookiejar - def info(self) -> HTTPHeaderDict: - return self.headers - - def geturl(self) -> str | None: - return self.url - - -class HTTPResponse(BaseHTTPResponse): - """ - HTTP Response container. - - Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is - loaded and decoded on-demand when the ``data`` property is accessed. This - class is also compatible with the Python standard library's :mod:`io` - module, and can hence be treated as a readable object in the context of that - framework. - - Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param original_response: - When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` - object, it's convenient to include the original for debug purposes. It's - otherwise unused. - - :param retries: - The retries contains the last :class:`~urllib3.util.retry.Retry` that - was used during the request. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - - def __init__( - self, - body: _TYPE_BODY = "", - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int = 0, - version: int = 0, - version_string: str = "HTTP/?", - reason: str | None = None, - preload_content: bool = True, - decode_content: bool = True, - original_response: _HttplibHTTPResponse | None = None, - pool: HTTPConnectionPool | None = None, - connection: HTTPConnection | None = None, - msg: _HttplibHTTPMessage | None = None, - retries: Retry | None = None, - enforce_content_length: bool = True, - request_method: str | None = None, - request_url: str | None = None, - auto_close: bool = True, - sock_shutdown: typing.Callable[[int], None] | None = None, - ) -> None: - super().__init__( - headers=headers, - status=status, - version=version, - version_string=version_string, - reason=reason, - decode_content=decode_content, - request_url=request_url, - retries=retries, - ) - - self.enforce_content_length = enforce_content_length - self.auto_close = auto_close - - self._body = None - self._uncached_read_occurred = False - self._fp: _HttplibHTTPResponse | None = None - self._original_response = original_response - self._fp_bytes_read = 0 - self.msg = msg - - if body and isinstance(body, (str, bytes)): - self._body = body - - self._pool = pool - self._connection = connection - - if hasattr(body, "read"): - self._fp = body # type: ignore[assignment] - self._sock_shutdown = sock_shutdown - - # Are we using the chunked-style of transfer encoding? - self.chunk_left: int | None = None - - # Determine length of response - self.length_remaining = self._init_length(request_method) - - # Used to return the correct amount of bytes for partial read()s - self._decoded_buffer = BytesQueueBuffer() - - # If requested, preload the body. - if preload_content and not self._body: - self._body = self.read(decode_content=decode_content) - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - """ - Read and discard any remaining HTTP response data in the response connection. - - Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. - """ - try: - self._raw_read() - except (HTTPError, OSError, BaseSSLError, HTTPException): - pass - if self._has_decoded_content: - # `_raw_read` skips decompression, so we should clean up the - # decoder to avoid keeping unnecessary data in memory. - self._decoded_buffer = BytesQueueBuffer() - self._decoder = None - - @property - def data(self) -> bytes: - # For backwards-compat with earlier urllib3 0.4 and earlier. - if self._body: - return self._body # type: ignore[return-value] - - if self._fp: - return self.read(cache_content=True) - - return None # type: ignore[return-value] - - @property - def connection(self) -> HTTPConnection | None: - return self._connection - - def isclosed(self) -> bool: - return is_fp_closed(self._fp) - - def tell(self) -> int: - """ - Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:`HTTPResponse.read` - if bytes are encoded on the wire (e.g, compressed). - """ - return self._fp_bytes_read - - def _init_length(self, request_method: str | None) -> int | None: - """ - Set initial length value for Response content if available. - """ - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - if self.chunked: - # This Response will fail with an IncompleteRead if it can't be - # received as chunked. This method falls back to attempt reading - # the response before raising an exception. - log.warning( - "Received response with both Content-Length and " - "Transfer-Encoding set. This is expressly forbidden " - "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " - "attempting to process response as Transfer-Encoding: " - "chunked." - ) - return None - - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Convert status to int for comparison - # In some cases, httplib returns a status of "_UNKNOWN" - try: - status = int(self.status) - except ValueError: - status = 0 - - # Check for responses that shouldn't include a body - if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": - length = 0 - - return length - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch low-level python exceptions, instead re-raising urllib3 - variants, so that low-level exceptions are not leaked in the - high-level api. - - On exit, release the connection back to the pool. - """ - clean_exit = False - - try: - try: - yield - - except SocketTimeout as e: - # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but - # there is yet no clean way to get at it from this context. - raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] - - except BaseSSLError as e: - # SSL errors related to framing/MAC get wrapped and reraised here - raise SSLError(e) from e - - except IncompleteRead as e: - if ( - e.expected is not None - and e.partial is not None - and e.expected == -e.partial - ): - arg = "Response may not contain content." - else: - arg = f"Connection broken: {e!r}" - raise ProtocolError(arg, e) from e - - except (HTTPException, OSError) as e: - raise ProtocolError(f"Connection broken: {e!r}", e) from e - - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now to ensure that the connection is - # released back to the pool. - if self._original_response: - self._original_response.close() - - # Closing the response may not actually be sufficient to close - # everything, so if we have a hold of the connection close that - # too. - if self._connection: - self._connection.close() - - # If we hold the original response but it's closed now, we should - # return the connection back to the pool. - if self._original_response and self._original_response.isclosed(): - self.release_conn() - - def _fp_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Read a response with the thought that reading the number of bytes - larger than can fit in a 32-bit int at a time via SSL in some - known cases leads to an overflow error that has to be prevented - if `amt` or `self.length_remaining` indicate that a problem may - happen. - - This happens to urllib3 injected with pyOpenSSL-backed SSL-support. - """ - assert self._fp - c_int_max = 2**31 - 1 - if ( - (amt and amt > c_int_max) - or ( - amt is None - and self.length_remaining - and self.length_remaining > c_int_max - ) - ) and util.IS_PYOPENSSL: - if read1: - return self._fp.read1(c_int_max) - buffer = io.BytesIO() - # Besides `max_chunk_amt` being a maximum chunk size, it - # affects memory overhead of reading a response by this - # method in CPython. - # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum - # chunk size that does not lead to an overflow error, but - # 256 MiB is a compromise. - max_chunk_amt = 2**28 - while amt is None or amt != 0: - if amt is not None: - chunk_amt = min(amt, max_chunk_amt) - amt -= chunk_amt - else: - chunk_amt = max_chunk_amt - data = self._fp.read(chunk_amt) - if not data: - break - buffer.write(data) - del data # to reduce peak memory usage by `max_chunk_amt`. - return buffer.getvalue() - elif read1: - return self._fp.read1(amt) if amt is not None else self._fp.read1() - else: - # StringIO doesn't like amt=None - return self._fp.read(amt) if amt is not None else self._fp.read() - - def _raw_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Reads `amt` of bytes from the socket. - """ - if self._fp is None: - return None # type: ignore[return-value] - - fp_closed = getattr(self._fp, "closed", False) - - with self._error_catcher(): - data = self._fp_read(amt, read1=read1) if not fp_closed else b"" - if amt is not None and amt != 0 and not data: - # Platform-specific: Buggy versions of Python. - # Close the connection when no data is returned - # - # This is redundant to what httplib/http.client _should_ - # already do. However, versions of python released before - # December 15, 2012 (http://bugs.python.org/issue16298) do - # not properly close the connection in all cases. There is - # no harm in redundantly calling close. - self._fp.close() - if ( - self.enforce_content_length - and self.length_remaining is not None - and self.length_remaining != 0 - ): - # This is an edge case that httplib failed to cover due - # to concerns of backward compatibility. We're - # addressing it here to make sure IncompleteRead is - # raised during streaming, so all calls with incorrect - # Content-Length are caught. - raise IncompleteRead(self._fp_bytes_read, self.length_remaining) - elif read1 and ( - (amt != 0 and not data) or self.length_remaining == len(data) - ): - # All data has been read, but `self._fp.read1` in - # CPython 3.12 and older doesn't always close - # `http.client.HTTPResponse`, so we close it here. - # See https://github.com/python/cpython/issues/113199 - self._fp.close() - - if data: - self._fp_bytes_read += len(data) - if self.length_remaining is not None: - self.length_remaining -= len(data) - return data - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - """ - Similar to :meth:`http.client.HTTPResponse.read`, but with two additional - parameters: ``decode_content`` and ``cache_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param cache_content: - If True, will save the returned data such that the same result is - returned despite of the state of the underlying file object. This - is useful if you want the ``.data`` property to continue working - after having ``.read()`` the file object. (Overridden if ``amt`` is - set.) - """ - self._init_decoder() - if decode_content is None: - decode_content = self.decode_content - - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - elif amt is not None: - cache_content = False - - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and len(self._decoded_buffer) < amt - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) >= amt: - return self._decoded_buffer.get(amt) - - data = self._raw_read(amt) - if not cache_content: - self._uncached_read_occurred = True - - flush_decoder = amt is None or (amt != 0 and not data) - - if ( - not data - and len(self._decoded_buffer) == 0 - and not (self._decoder and self._decoder.has_unconsumed_tail) - ): - return data - - if amt is None: - data = self._decode(data, decode_content, flush_decoder) - # It's possible that there is buffered decoded data after a - # partial read. - if decode_content and len(self._decoded_buffer) > 0: - self._decoded_buffer.put(data) - data = self._decoded_buffer.get_all() - - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - # do not waste memory on buffer when not decoding - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - - while len(self._decoded_buffer) < amt and data: - # TODO make sure to initially read enough data to get past the headers - # For example, the GZ file header takes 10 bytes, we don't want to read - # it one byte at a time - data = self._raw_read(amt) - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - data = self._decoded_buffer.get(amt) - - return data - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - """ - Similar to ``http.client.HTTPResponse.read1`` and documented - in :meth:`io.BufferedReader.read1`, but with an additional parameter: - ``decode_content``. - - :param amt: - How much of the content to read. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if decode_content is None: - decode_content = self.decode_content - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - # try and respond without going to the network - if self._has_decoded_content: - if not decode_content: - raise RuntimeError( - "Calling read1(decode_content=False) is not supported after " - "read1(decode_content=True) was called." - ) - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and (amt is None or len(self._decoded_buffer) < amt) - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=( - amt - len(self._decoded_buffer) if amt is not None else None - ), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) > 0: - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - if amt == 0: - return b"" - - # FIXME, this method's type doesn't say returning None is possible - data = self._raw_read(amt, read1=True) - self._uncached_read_occurred = True - if not decode_content or data is None: - return data - - self._init_decoder() - while True: - flush_decoder = not data - decoded_data = self._decode( - data, decode_content, flush_decoder, max_length=amt - ) - self._decoded_buffer.put(decoded_data) - if decoded_data or flush_decoder: - break - data = self._raw_read(8192, read1=True) - - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if amt == 0: - return - - if self.chunked and self.supports_chunked_reads(): - yield from self.read_chunked(amt, decode_content=decode_content) - else: - while ( - not is_fp_closed(self._fp) - or len(self._decoded_buffer) > 0 - or (self._decoder and self._decoder.has_unconsumed_tail) - ): - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - - # Overrides from io.IOBase - def readable(self) -> bool: - return True - - def shutdown(self) -> None: - if not self._sock_shutdown: - raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") - if self._connection is None: - raise RuntimeError( - "Cannot shutdown as connection has already been released to the pool" - ) - self._sock_shutdown(socket.SHUT_RD) - - def close(self) -> None: - self._sock_shutdown = None - - if not self.closed and self._fp: - self._fp.close() - - if self._connection: - self._connection.close() - - if not self.auto_close: - io.IOBase.close(self) - - @property - def closed(self) -> bool: - if not self.auto_close: - return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] - elif self._fp is None: - return True - elif hasattr(self._fp, "isclosed"): - return self._fp.isclosed() - elif hasattr(self._fp, "closed"): - return self._fp.closed - else: - return True - - def fileno(self) -> int: - if self._fp is None: - raise OSError("HTTPResponse has no file to get a fileno from") - elif hasattr(self._fp, "fileno"): - return self._fp.fileno() - else: - raise OSError( - "The file-like object this HTTPResponse is wrapped " - "around has no file descriptor" - ) - - def flush(self) -> None: - if ( - self._fp is not None - and hasattr(self._fp, "flush") - and not getattr(self._fp, "closed", False) - ): - return self._fp.flush() - - def supports_chunked_reads(self) -> bool: - """ - Checks if the underlying file-like object looks like a - :class:`http.client.HTTPResponse` object. We do this by testing for - the fp attribute. If it is present we assume it returns raw chunks as - processed by read_chunked(). - """ - return hasattr(self._fp, "fp") - - def _update_chunk_length(self) -> None: - # First, we'll figure out length of a chunk and then - # we'll try to read it from socket. - if self.chunk_left is not None: - return None - line = self._fp.fp.readline() # type: ignore[union-attr] - line = line.split(b";", 1)[0] - try: - self.chunk_left = int(line, 16) - except ValueError: - self.close() - if line: - # Invalid chunked protocol response, abort. - raise InvalidChunkLength(self, line) from None - else: - # Truncated at start of next chunk - raise ProtocolError("Response ended prematurely") from None - - def _handle_chunk(self, amt: int | None) -> bytes: - returned_chunk = None - if amt is None: - chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - returned_chunk = chunk - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - elif self.chunk_left is not None and amt < self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self.chunk_left = self.chunk_left - amt - returned_chunk = value - elif amt == self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - returned_chunk = value - else: # amt > self.chunk_left - returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - return returned_chunk # type: ignore[no-any-return] - - def read_chunked( - self, amt: int | None = None, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - Similar to :meth:`HTTPResponse.read`, but with an additional - parameter: ``decode_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - self._init_decoder() - # FIXME: Rewrite this method and make it a class with a better structured logic. - if not self.chunked: - raise ResponseNotChunked( - "Response is not chunked. " - "Header 'transfer-encoding: chunked' is missing." - ) - if not self.supports_chunked_reads(): - raise BodyNotHttplibCompatible( - "Body should be http.client.HTTPResponse like. " - "It should have have an fp attribute which returns raw chunks." - ) - - with self._error_catcher(): - # Don't bother reading the body of a HEAD request. - if self._original_response and is_response_to_head(self._original_response): - self._original_response.close() - return None - - # If a response is already read and closed - # then return immediately. - if self._fp.fp is None: # type: ignore[union-attr] - return None - - if amt == 0: - return - elif amt and amt < 0: - # Negative numbers and `None` should be treated the same, - # but httplib handles only `None` correctly. - amt = None - - while True: - # First, check if any data is left in the decoder's buffer. - if self._decoder and self._decoder.has_unconsumed_tail: - chunk = b"" - else: - self._update_chunk_length() - self._uncached_read_occurred = True - if self.chunk_left == 0: - break - chunk = self._handle_chunk(amt) - decoded = self._decode( - chunk, - decode_content=decode_content, - flush_decoder=False, - max_length=amt, - ) - if decoded: - yield decoded - - if decode_content: - # On CPython and PyPy, we should never need to flush the - # decoder. However, on Jython we *might* need to, so - # lets defensively do it anyway. - decoded = self._flush_decoder() - if decoded: # Platform-specific: Jython. - yield decoded - - # Chunk content ends with \r\n: discard it. - while self._fp is not None: - line = self._fp.fp.readline() - if not line: - # Some sites may not end with '\r\n'. - break - if line == b"\r\n": - break - - # We read everything; close the "file". - if self._original_response: - self._original_response.close() - - @property - def url(self) -> str | None: - """ - Returns the URL that was the source of this response. - If the request that generated this response redirected, this method - will return the final redirect location. - """ - return self._request_url - - @url.setter - def url(self, url: str | None) -> None: - self._request_url = url - - def __iter__(self) -> typing.Iterator[bytes]: - buffer: list[bytes] = [] - for chunk in self.stream(decode_content=True): - if b"\n" in chunk: - chunks = chunk.split(b"\n") - yield b"".join(buffer) + chunks[0] + b"\n" - for x in chunks[1:-1]: - yield x + b"\n" - if chunks[-1]: - buffer = [chunks[-1]] - else: - buffer = [] - else: - buffer.append(chunk) - if buffer: - yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py deleted file mode 100644 index 534126033c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# For backwards compatibility, provide imports that used to be here. -from __future__ import annotations - -from .connection import is_connection_dropped -from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers -from .response import is_fp_closed -from .retry import Retry -from .ssl_ import ( - ALPN_PROTOCOLS, - IS_PYOPENSSL, - SSLContext, - assert_fingerprint, - create_urllib3_context, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .timeout import Timeout -from .url import Url, parse_url -from .wait import wait_for_read, wait_for_write - -__all__ = ( - "IS_PYOPENSSL", - "SSLContext", - "ALPN_PROTOCOLS", - "Retry", - "Timeout", - "Url", - "assert_fingerprint", - "create_urllib3_context", - "is_connection_dropped", - "is_fp_closed", - "parse_url", - "make_headers", - "resolve_cert_reqs", - "resolve_ssl_version", - "ssl_wrap_socket", - "wait_for_read", - "wait_for_write", - "SKIP_HEADER", - "SKIPPABLE_HEADERS", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py deleted file mode 100644 index f92519ee91..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/connection.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import annotations - -import socket -import typing - -from ..exceptions import LocationParseError -from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT - -_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] - -if typing.TYPE_CHECKING: - from .._base_connection import BaseHTTPConnection - - -def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific - """ - Returns True if the connection is dropped and should be closed. - :param conn: :class:`urllib3.connection.HTTPConnection` object. - """ - return not conn.is_connected - - -# This function is copied from socket.py in the Python 2.7 standard -# library test suite. Added to its signature is only `socket_options`. -# One additional modification is that we avoid binding to IPv6 servers -# discovered in DNS if the system doesn't have IPv6 functionality. -def create_connection( - address: tuple[str, int], - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, -) -> socket.socket: - """Connect to *address* and return the socket object. - - Convenience function. Connect to *address* (a 2-tuple ``(host, - port)``) and return the socket object. Passing the optional - *timeout* parameter will set the timeout on the socket instance - before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`socket.getdefaulttimeout` - is used. If *source_address* is set it must be a tuple of (host, port) - for the socket to bind as a source address before making the connection. - An host of '' or port 0 tells the OS to use the default. - """ - - host, port = address - if host.startswith("["): - host = host.strip("[]") - err = None - - # Using the value from allowed_gai_family() in the context of getaddrinfo lets - # us select whether to work with IPv4 DNS records, IPv6 records, or both. - # The original create_connection function always returns all records. - family = allowed_gai_family() - - try: - host.encode("idna") - except UnicodeError: - raise LocationParseError(f"'{host}', label empty or too long") from None - - for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = None - try: - sock = socket.socket(af, socktype, proto) - - # If provided, set socket level options before connecting. - _set_socket_options(sock, socket_options) - - if timeout is not _DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(sa) - # Break explicitly a reference cycle - err = None - return sock - - except OSError as _: - err = _ - if sock is not None: - sock.close() - - if err is not None: - try: - raise err - finally: - # Break explicitly a reference cycle - err = None - else: - raise OSError("getaddrinfo returns an empty list") - - -def _set_socket_options( - sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None -) -> None: - if options is None: - return - - for opt in options: - sock.setsockopt(*opt) - - -def allowed_gai_family() -> socket.AddressFamily: - """This function is designed to work in the context of - getaddrinfo, where family=socket.AF_UNSPEC is the default and - will perform a DNS search for both IPv6 and IPv4 records.""" - - family = socket.AF_INET - if HAS_IPV6: - family = socket.AF_UNSPEC - return family - - -def _has_ipv6(host: str) -> bool: - """Returns True if the system can bind an IPv6 address.""" - sock = None - has_ipv6 = False - - if socket.has_ipv6: - # has_ipv6 returns true if cPython was compiled with IPv6 support. - # It does not tell us if the system has IPv6 support enabled. To - # determine that we must bind to an IPv6 address. - # https://github.com/urllib3/urllib3/pull/611 - # https://bugs.python.org/issue658327 - try: - sock = socket.socket(socket.AF_INET6) - sock.bind((host, 0)) - has_ipv6 = True - except Exception: - pass - - if sock: - sock.close() - return has_ipv6 - - -HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py deleted file mode 100644 index 908fc6621d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/proxy.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import typing - -from .url import Url - -if typing.TYPE_CHECKING: - from ..connection import ProxyConfig - - -def connection_requires_http_tunnel( - proxy_url: Url | None = None, - proxy_config: ProxyConfig | None = None, - destination_scheme: str | None = None, -) -> bool: - """ - Returns True if the connection requires an HTTP CONNECT through the proxy. - - :param URL proxy_url: - URL of the proxy. - :param ProxyConfig proxy_config: - Proxy configuration from poolmanager.py - :param str destination_scheme: - The scheme of the destination. (i.e https, http, etc) - """ - # If we're not using a proxy, no way to use a tunnel. - if proxy_url is None: - return False - - # HTTP destinations never require tunneling, we always forward. - if destination_scheme == "http": - return False - - # Support for forwarding with HTTPS proxies and HTTPS destinations. - if ( - proxy_url.scheme == "https" - and proxy_config - and proxy_config.use_forwarding_for_https - ): - return False - - # Otherwise always use a tunnel. - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py deleted file mode 100644 index 6c2372ba7e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/request.py +++ /dev/null @@ -1,263 +0,0 @@ -from __future__ import annotations - -import io -import sys -import typing -from base64 import b64encode -from enum import Enum - -from ..exceptions import UnrewindableBodyError -from .util import to_bytes - -if typing.TYPE_CHECKING: - from typing import Final - -# Pass as a value within ``headers`` to skip -# emitting some HTTP headers that are added automatically. -# The only headers that are supported are ``Accept-Encoding``, -# ``Host``, and ``User-Agent``. -SKIP_HEADER = "@@@SKIP_HEADER@@@" -SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) - -ACCEPT_ENCODING = "gzip,deflate" -try: - try: - import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 - except ImportError: - import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",br" - -try: - if sys.version_info >= (3, 14): - from compression import zstd as _unused_module_zstd # noqa: F401 - else: - from backports import zstd as _unused_module_zstd # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",zstd" - - -class _TYPE_FAILEDTELL(Enum): - token = 0 - - -_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token - -_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] - -# When sending a request with these methods we aren't expecting -# a body so don't need to set an explicit 'Content-Length: 0' -# The reason we do this in the negative instead of tracking methods -# which 'should' have a body is because unknown methods should be -# treated as if they were 'POST' which *does* expect a body. -_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} - - -def make_headers( - keep_alive: bool | None = None, - accept_encoding: bool | list[str] | str | None = None, - user_agent: str | None = None, - basic_auth: str | None = None, - proxy_basic_auth: str | None = None, - disable_cache: bool | None = None, -) -> dict[str, str]: - """ - Shortcuts for generating request headers. - - :param keep_alive: - If ``True``, adds 'connection: keep-alive' header. - - :param accept_encoding: - Can be a boolean, list, or string. - ``True`` translates to 'gzip,deflate'. If the dependencies for - Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or - Zstandard (the ``backports.zstd`` package for Python before 3.14) - algorithms are installed, then their encodings are - included in the string ('br' and 'zstd', respectively). - List will get joined by comma. - String will be used as provided. - - :param user_agent: - String representing the user-agent you want, such as - "python-urllib3/0.6" - - :param basic_auth: - Colon-separated username:password string for 'authorization: basic ...' - auth header. - - :param proxy_basic_auth: - Colon-separated username:password string for 'proxy-authorization: basic ...' - auth header. - - :param disable_cache: - If ``True``, adds 'cache-control: no-cache' header. - - Example: - - .. code-block:: python - - import urllib3 - - print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) - # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} - print(urllib3.util.make_headers(accept_encoding=True)) - # {'accept-encoding': 'gzip,deflate'} - """ - headers: dict[str, str] = {} - if accept_encoding: - if isinstance(accept_encoding, str): - pass - elif isinstance(accept_encoding, list): - accept_encoding = ",".join(accept_encoding) - else: - accept_encoding = ACCEPT_ENCODING - headers["accept-encoding"] = accept_encoding - - if user_agent: - headers["user-agent"] = user_agent - - if keep_alive: - headers["connection"] = "keep-alive" - - if basic_auth: - headers["authorization"] = ( - f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" - ) - - if proxy_basic_auth: - headers["proxy-authorization"] = ( - f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" - ) - - if disable_cache: - headers["cache-control"] = "no-cache" - - return headers - - -def set_file_position( - body: typing.Any, pos: _TYPE_BODY_POSITION | None -) -> _TYPE_BODY_POSITION | None: - """ - If a position is provided, move file to that point. - Otherwise, we'll attempt to record a position for future use. - """ - if pos is not None: - rewind_body(body, pos) - elif getattr(body, "tell", None) is not None: - try: - pos = body.tell() - except OSError: - # This differentiates from None, allowing us to catch - # a failed `tell()` later when trying to rewind the body. - pos = _FAILEDTELL - - return pos - - -def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: - """ - Attempt to rewind body to a certain position. - Primarily used for request redirects and retries. - - :param body: - File-like object that supports seek. - - :param int pos: - Position to seek to in file. - """ - body_seek = getattr(body, "seek", None) - if body_seek is not None and isinstance(body_pos, int): - try: - body_seek(body_pos) - except OSError as e: - raise UnrewindableBodyError( - "An error occurred when rewinding request body for redirect/retry." - ) from e - elif body_pos is _FAILEDTELL: - raise UnrewindableBodyError( - "Unable to record file position for rewinding " - "request body during a redirect/retry." - ) - else: - raise ValueError( - f"body_pos must be of type integer, instead it was {type(body_pos)}." - ) - - -class ChunksAndContentLength(typing.NamedTuple): - chunks: typing.Iterable[bytes] | None - content_length: int | None - - -def body_to_chunks( - body: typing.Any | None, method: str, blocksize: int -) -> ChunksAndContentLength: - """Takes the HTTP request method, body, and blocksize and - transforms them into an iterable of chunks to pass to - socket.sendall() and an optional 'Content-Length' header. - - A 'Content-Length' of 'None' indicates the length of the body - can't be determined so should use 'Transfer-Encoding: chunked' - for framing instead. - """ - - chunks: typing.Iterable[bytes] | None - content_length: int | None - - # No body, we need to make a recommendation on 'Content-Length' - # based on whether that request method is expected to have - # a body or not. - if body is None: - chunks = None - if method.upper() not in _METHODS_NOT_EXPECTING_BODY: - content_length = 0 - else: - content_length = None - - # Bytes or strings become bytes - elif isinstance(body, (str, bytes)): - chunks = (to_bytes(body),) - content_length = len(chunks[0]) - - # File-like object, TODO: use seek() and tell() for length? - elif hasattr(body, "read"): - - def chunk_readable() -> typing.Iterable[bytes]: - encode = isinstance(body, io.TextIOBase) - while True: - datablock = body.read(blocksize) - if not datablock: - break - if encode: - datablock = datablock.encode("utf-8") - yield datablock - - chunks = chunk_readable() - content_length = None - - # Otherwise we need to start checking via duck-typing. - else: - try: - # Check if the body implements the buffer API. - mv = memoryview(body) - except TypeError: - try: - # Check if the body is an iterable - chunks = iter(body) - content_length = None - except TypeError: - raise TypeError( - f"'body' must be a bytes-like object, file-like " - f"object, or iterable. Instead was {body!r}" - ) from None - else: - # Since it implements the buffer API can be passed directly to socket.sendall() - chunks = (body,) - content_length = mv.nbytes - - return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py deleted file mode 100644 index 0f4578696f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/response.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import http.client as httplib -from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect - -from ..exceptions import HeaderParsingError - - -def is_fp_closed(obj: object) -> bool: - """ - Checks whether a given file-like object is closed. - - :param obj: - The file-like object to check. - """ - - try: - # Check `isclosed()` first, in case Python3 doesn't set `closed`. - # GH Issue #928 - return obj.isclosed() # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check via the official file-like-object way. - return obj.closed # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check if the object is a container for another file-like object that - # gets released on exhaustion (e.g. HTTPResponse). - return obj.fp is None # type: ignore[attr-defined] - except AttributeError: - pass - - raise ValueError("Unable to determine whether fp is closed.") - - -def assert_header_parsing(headers: httplib.HTTPMessage) -> None: - """ - Asserts whether all headers have been successfully parsed. - Extracts encountered errors from the result of parsing headers. - - Only works on Python 3. - - :param http.client.HTTPMessage headers: Headers to verify. - - :raises urllib3.exceptions.HeaderParsingError: - If parsing errors are found. - """ - - # This will fail silently if we pass in the wrong kind of parameter. - # To make debugging easier add an explicit check. - if not isinstance(headers, httplib.HTTPMessage): - raise TypeError(f"expected httplib.Message, got {type(headers)}.") - - unparsed_data = None - - # get_payload is actually email.message.Message.get_payload; - # we're only interested in the result if it's not a multipart message - if not headers.is_multipart(): - payload = headers.get_payload() - - if isinstance(payload, (bytes, str)): - unparsed_data = payload - - # httplib is assuming a response body is available - # when parsing headers even when httplib only sends - # header data to parse_headers() This results in - # defects on multipart responses in particular. - # See: https://github.com/urllib3/urllib3/issues/800 - - # So we ignore the following defects: - # - StartBoundaryNotFoundDefect: - # The claimed start boundary was never found. - # - MultipartInvariantViolationDefect: - # A message claimed to be a multipart but no subparts were found. - defects = [ - defect - for defect in headers.defects - if not isinstance( - defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) - ) - ] - - if defects or unparsed_data: - raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) - - -def is_response_to_head(response: httplib.HTTPResponse) -> bool: - """ - Checks whether the request of a response has been a HEAD-request. - - :param http.client.HTTPResponse response: - Response to check if the originating request - used 'HEAD' as a method. - """ - # FIXME: Can we do this somehow without accessing private httplib _method? - method_str = response._method # type: str # type: ignore[attr-defined] - return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py deleted file mode 100644 index 7649898e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/retry.py +++ /dev/null @@ -1,557 +0,0 @@ -from __future__ import annotations - -import email -import logging -import random -import re -import time -import typing -from itertools import takewhile -from types import TracebackType - -from ..exceptions import ( - ConnectTimeoutError, - InvalidHeader, - MaxRetryError, - ProtocolError, - ProxyError, - ReadTimeoutError, - ResponseError, -) -from .util import reraise - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from ..connectionpool import ConnectionPool - from ..response import BaseHTTPResponse - -log = logging.getLogger(__name__) - - -# Data structure for representing the metadata of requests that result in a retry. -class RequestHistory(typing.NamedTuple): - method: str | None - url: str | None - error: Exception | None - status: int | None - redirect_location: str | None - - -class Retry: - """Retry configuration. - - Each retry attempt will create a new Retry object with updated values, so - they can be safely reused. - - Retries can be defined as a default for a pool: - - .. code-block:: python - - retries = Retry(connect=5, read=2, redirect=5) - http = PoolManager(retries=retries) - response = http.request("GET", "https://example.com/") - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=Retry(10)) - - Retries can be disabled by passing ``False``: - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=False) - - Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless - retries are disabled, in which case the causing exception will be raised. - - :param int total: - Total number of retries to allow. Takes precedence over other counts. - - Set to ``None`` to remove this constraint and fall back on other - counts. - - Set to ``0`` to fail on the first retry. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int connect: - How many connection-related errors to retry on. - - These are errors raised before the request is sent to the remote server, - which we assume has not triggered the server to process the request. - - Set to ``0`` to fail on the first retry of this type. - - :param int read: - How many times to retry on read errors. - - These errors are raised after the request was sent to the server, so the - request may have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - :param int redirect: - How many redirects to perform. Limit this to avoid infinite redirect - loops. - - A redirect is a HTTP response with a status code 301, 302, 303, 307 or - 308. - - Set to ``0`` to fail on the first retry of this type. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int status: - How many times to retry on bad status codes. - - These are retries made on responses, where status code matches - ``status_forcelist``. - - Set to ``0`` to fail on the first retry of this type. - - :param int other: - How many times to retry on other errors. - - Other errors are errors that are not connect, read, redirect or status errors. - These errors might be raised after the request was sent to the server, so the - request might have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - If ``total`` is not set, it's a good idea to set this to 0 to account - for unexpected edge cases and avoid infinite retry loops. - - :param Collection allowed_methods: - Set of uppercased HTTP method verbs that we should retry on. - - By default, we only retry on methods which are considered to be - idempotent (multiple requests with the same parameters end with the - same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. - - Set to a ``None`` value to retry on any verb. - - :param Collection status_forcelist: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``allowed_methods`` - and the response status code is in ``status_forcelist``. - - By default, this is disabled with ``None``. - - :param float backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). urllib3 will sleep for:: - - {backoff factor} * (2 ** ({number of previous retries})) - - seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: - - random.uniform(0, {backoff jitter}) - - seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will - sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever - be longer than `backoff_max`. - - By default, backoff is disabled (factor set to 0). - - :param float backoff_max: - The maximum backoff time (in seconds) between retry attempts. - This value caps the computed backoff from `backoff_factor`. - - :param float backoff_jitter: - Random jitter amount (in seconds) added to the computed backoff. - Jitter is sampled uniformly from `0` to `backoff_jitter`. - - :param bool raise_on_redirect: Whether, if the number of redirects is - exhausted, to raise a MaxRetryError, or to return a response with a - response code in the 3xx range. - - :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: - whether we should raise an exception, or return a response, - if status falls in ``status_forcelist`` range and retries have - been exhausted. - - :param tuple history: The history of the request encountered during - each call to :meth:`~Retry.increment`. The list is in the order - the requests occurred. Each list item is of class :class:`RequestHistory`. - - :param bool respect_retry_after_header: - Whether to respect Retry-After header on status codes defined as - :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. - - :param Collection remove_headers_on_redirect: - Sequence of headers to remove from the request when a response - indicating a redirect is returned before firing off the redirected - request. - - :param int retry_after_max: Number of seconds to allow as the maximum for - Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. - Any Retry-After headers larger than this value will be limited to this - value. - """ - - #: Default methods to be used for ``allowed_methods`` - DEFAULT_ALLOWED_METHODS = frozenset( - ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] - ) - - #: Default status codes to be used for ``status_forcelist`` - RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - - #: Default headers to be used for ``remove_headers_on_redirect`` - DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( - ["Cookie", "Authorization", "Proxy-Authorization"] - ) - - #: Default maximum backoff time. - DEFAULT_BACKOFF_MAX = 120 - - # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. - #: Default maximum allowed value for Retry-After headers in seconds - DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 - - # Backward compatibility; assigned outside of the class. - DEFAULT: typing.ClassVar[Retry] - - def __init__( - self, - total: bool | int | None = 10, - connect: int | None = None, - read: int | None = None, - redirect: bool | int | None = None, - status: int | None = None, - other: int | None = None, - allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, - status_forcelist: typing.Collection[int] | None = None, - backoff_factor: float = 0, - backoff_max: float = DEFAULT_BACKOFF_MAX, - raise_on_redirect: bool = True, - raise_on_status: bool = True, - history: tuple[RequestHistory, ...] | None = None, - respect_retry_after_header: bool = True, - remove_headers_on_redirect: typing.Collection[ - str - ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, - backoff_jitter: float = 0.0, - retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, - ) -> None: - self.total = total - self.connect = connect - self.read = read - self.status = status - self.other = other - - if redirect is False or total is False: - redirect = 0 - raise_on_redirect = False - - self.redirect = redirect - self.status_forcelist = status_forcelist or set() - self.allowed_methods = allowed_methods - self.backoff_factor = backoff_factor - self.backoff_max = backoff_max - self.retry_after_max = retry_after_max - self.raise_on_redirect = raise_on_redirect - self.raise_on_status = raise_on_status - self.history = history or () - self.respect_retry_after_header = respect_retry_after_header - self.remove_headers_on_redirect = frozenset( - h.lower() for h in remove_headers_on_redirect - ) - self.backoff_jitter = backoff_jitter - - def new(self, **kw: typing.Any) -> Self: - params = dict( - total=self.total, - connect=self.connect, - read=self.read, - redirect=self.redirect, - status=self.status, - other=self.other, - allowed_methods=self.allowed_methods, - status_forcelist=self.status_forcelist, - backoff_factor=self.backoff_factor, - backoff_max=self.backoff_max, - retry_after_max=self.retry_after_max, - raise_on_redirect=self.raise_on_redirect, - raise_on_status=self.raise_on_status, - history=self.history, - remove_headers_on_redirect=self.remove_headers_on_redirect, - respect_retry_after_header=self.respect_retry_after_header, - backoff_jitter=self.backoff_jitter, - ) - - params.update(kw) - return type(self)(**params) # type: ignore[arg-type] - - @classmethod - def from_int( - cls, - retries: Retry | bool | int | None, - redirect: bool | int | None = True, - default: Retry | bool | int | None = None, - ) -> Retry: - """Backwards-compatibility for the old retries format.""" - if retries is None: - retries = default if default is not None else cls.DEFAULT - - if isinstance(retries, Retry): - return retries - - redirect = bool(redirect) and None - new_retries = cls(retries, redirect=redirect) - log.debug("Converted retries value: %r -> %r", retries, new_retries) - return new_retries - - def get_backoff_time(self) -> float: - """Formula for computing the current backoff - - :rtype: float - """ - # We want to consider only the last consecutive errors sequence (Ignore redirects). - consecutive_errors_len = len( - list( - takewhile(lambda x: x.redirect_location is None, reversed(self.history)) - ) - ) - if consecutive_errors_len <= 1: - return 0 - - backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) - if self.backoff_jitter != 0.0: - backoff_value += random.random() * self.backoff_jitter - return float(max(0, min(self.backoff_max, backoff_value))) - - def parse_retry_after(self, retry_after: str) -> float: - seconds: float - # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 - if re.match(r"^\s*[0-9]+\s*$", retry_after): - seconds = int(retry_after) - else: - retry_date_tuple = email.utils.parsedate_tz(retry_after) - if retry_date_tuple is None: - raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") - - retry_date = email.utils.mktime_tz(retry_date_tuple) - seconds = retry_date - time.time() - - seconds = max(seconds, 0) - - # Check the seconds do not exceed the specified maximum - if seconds > self.retry_after_max: - seconds = self.retry_after_max - - return seconds - - def get_retry_after(self, response: BaseHTTPResponse) -> float | None: - """Get the value of Retry-After in seconds.""" - - retry_after = response.headers.get("Retry-After") - - if retry_after is None: - return None - - return self.parse_retry_after(retry_after) - - def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: - retry_after = self.get_retry_after(response) - if retry_after: - time.sleep(retry_after) - return True - - return False - - def _sleep_backoff(self) -> None: - backoff = self.get_backoff_time() - if backoff <= 0: - return - time.sleep(backoff) - - def sleep(self, response: BaseHTTPResponse | None = None) -> None: - """Sleep between retry attempts. - - This method will respect a server's ``Retry-After`` response header - and sleep the duration of the time requested. If that is not present, it - will use an exponential backoff. By default, the backoff factor is 0 and - this method will return immediately. - """ - - if self.respect_retry_after_header and response: - slept = self.sleep_for_retry(response) - if slept: - return - - self._sleep_backoff() - - def _is_connection_error(self, err: Exception) -> bool: - """Errors when we're fairly sure that the server did not receive the - request, so it should be safe to retry. - """ - if isinstance(err, ProxyError): - err = err.original_error - return isinstance(err, ConnectTimeoutError) - - def _is_read_error(self, err: Exception) -> bool: - """Errors that occur after the request has been started, so we should - assume that the server began processing it. - """ - return isinstance(err, (ReadTimeoutError, ProtocolError)) - - def _is_method_retryable(self, method: str) -> bool: - """Checks if a given HTTP method should be retried upon, depending if - it is included in the allowed_methods - """ - if self.allowed_methods and method.upper() not in self.allowed_methods: - return False - return True - - def is_retry( - self, method: str, status_code: int, has_retry_after: bool = False - ) -> bool: - """Is this method/status code retryable? (Based on allowlists and control - variables such as the number of total retries to allow, whether to - respect the Retry-After header, whether this header is present, and - whether the returned status code is on the list of status codes to - be retried upon on the presence of the aforementioned header) - """ - if not self._is_method_retryable(method): - return False - - if self.status_forcelist and status_code in self.status_forcelist: - return True - - return bool( - self.total - and self.respect_retry_after_header - and has_retry_after - and (status_code in self.RETRY_AFTER_STATUS_CODES) - ) - - def is_exhausted(self) -> bool: - """Are we out of retries?""" - retry_counts = [ - x - for x in ( - self.total, - self.connect, - self.read, - self.redirect, - self.status, - self.other, - ) - if x - ] - if not retry_counts: - return False - - return min(retry_counts) < 0 - - def increment( - self, - method: str | None = None, - url: str | None = None, - response: BaseHTTPResponse | None = None, - error: Exception | None = None, - _pool: ConnectionPool | None = None, - _stacktrace: TracebackType | None = None, - ) -> Self: - """Return a new Retry object with incremented retry counters. - - :param response: A response object, or None, if the server did not - return a response. - :type response: :class:`~urllib3.response.BaseHTTPResponse` - :param Exception error: An error encountered during the request, or - None if the response was received successfully. - - :return: A new ``Retry`` object. - """ - if self.total is False and error: - # Disabled, indicate to re-raise the error. - raise reraise(type(error), error, _stacktrace) - - total = self.total - if total is not None: - total -= 1 - - connect = self.connect - read = self.read - redirect = self.redirect - status_count = self.status - other = self.other - cause = "unknown" - status = None - redirect_location = None - - if error and self._is_connection_error(error): - # Connect retry? - if connect is False: - raise reraise(type(error), error, _stacktrace) - elif connect is not None: - connect -= 1 - - elif error and self._is_read_error(error): - # Read retry? - if read is False or method is None or not self._is_method_retryable(method): - raise reraise(type(error), error, _stacktrace) - elif read is not None: - read -= 1 - - elif error: - # Other retry? - if other is not None: - other -= 1 - - elif response and response.get_redirect_location(): - # Redirect retry? - if redirect is not None: - redirect -= 1 - cause = "too many redirects" - response_redirect_location = response.get_redirect_location() - if response_redirect_location: - redirect_location = response_redirect_location - status = response.status - - else: - # Incrementing because of a server error like a 500 in - # status_forcelist and the given method is in the allowed_methods - cause = ResponseError.GENERIC_ERROR - if response and response.status: - if status_count is not None: - status_count -= 1 - cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) - status = response.status - - history = self.history + ( - RequestHistory(method, url, error, status, redirect_location), - ) - - new_retry = self.new( - total=total, - connect=connect, - read=read, - redirect=redirect, - status=status_count, - other=other, - history=history, - ) - - if new_retry.is_exhausted(): - reason = error or ResponseError(cause) - raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] - - log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) - - return new_retry - - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(total={self.total}, connect={self.connect}, " - f"read={self.read}, redirect={self.redirect}, status={self.status})" - ) - - -# For backwards compatibility (equivalent to pre-v1.9): -Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py deleted file mode 100644 index e66549a76c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_.py +++ /dev/null @@ -1,477 +0,0 @@ -from __future__ import annotations - -import hashlib -import hmac -import os -import socket -import sys -import typing -import warnings -from binascii import unhexlify - -from ..exceptions import ProxySchemeUnsupported, SSLError -from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE - -SSLContext = None -SSLTransport = None -HAS_NEVER_CHECK_COMMON_NAME = False -IS_PYOPENSSL = False -ALPN_PROTOCOLS = ["http/1.1"] - -_TYPE_VERSION_INFO = tuple[int, int, int, str, int] - -# Maps the length of a digest to a possible hash function producing this digest -HASHFUNC_MAP = { - length: getattr(hashlib, algorithm, None) - for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) -} - - -def _is_has_never_check_common_name_reliable( - openssl_version: str, -) -> bool: - # As of May 2023, all released versions of LibreSSL fail to reject certificates with - # only common names, see https://github.com/urllib3/urllib3/pull/3024 - is_openssl = openssl_version.startswith("OpenSSL ") - - return is_openssl - - -if typing.TYPE_CHECKING: - from ssl import VerifyMode - from typing import TypedDict - - from .ssltransport import SSLTransport as SSLTransportType - - class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): - subjectAltName: tuple[tuple[str, str], ...] - subject: tuple[tuple[tuple[str, str], ...], ...] - serialNumber: str - - -# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' -_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} - -try: # Do we have ssl at all? - import ssl - from ssl import ( # type: ignore[assignment] - CERT_REQUIRED, - HAS_NEVER_CHECK_COMMON_NAME, - OP_NO_COMPRESSION, - OP_NO_TICKET, - OPENSSL_VERSION, - PROTOCOL_TLS, - PROTOCOL_TLS_CLIENT, - VERIFY_X509_PARTIAL_CHAIN, - VERIFY_X509_STRICT, - OP_NO_SSLv2, - OP_NO_SSLv3, - SSLContext, - TLSVersion, - ) - - PROTOCOL_SSLv23 = PROTOCOL_TLS - - # Setting SSLContext.hostname_checks_common_name = False didn't work with - # LibreSSL, check details in the used function. - if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( - OPENSSL_VERSION, - ): # Defensive: - HAS_NEVER_CHECK_COMMON_NAME = False - - # Need to be careful here in case old TLS versions get - # removed in future 'ssl' module implementations. - for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): - try: - _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( - TLSVersion, attr - ) - except AttributeError: # Defensive: - continue - - from .ssltransport import SSLTransport # type: ignore[assignment] -except ImportError: - OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] - OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] - OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] - OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] - PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] - PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] - VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] - VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] - - -_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] - - -def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: - """ - Checks if given fingerprint matches the supplied certificate. - - :param cert: - Certificate as bytes object. - :param fingerprint: - Fingerprint as string of hexdigits, can be interspersed by colons. - """ - - if cert is None: - raise SSLError("No certificate for the peer.") - - fingerprint = fingerprint.replace(":", "").lower() - digest_length = len(fingerprint) - if digest_length not in HASHFUNC_MAP: - raise SSLError(f"Fingerprint of invalid length: {fingerprint}") - hashfunc = HASHFUNC_MAP.get(digest_length) - if hashfunc is None: - raise SSLError( - f"Hash function implementation unavailable for fingerprint length: {digest_length}" - ) - - # We need encode() here for py32; works on py2 and p33. - fingerprint_bytes = unhexlify(fingerprint.encode()) - - cert_digest = hashfunc(cert).digest() - - if not hmac.compare_digest(cert_digest, fingerprint_bytes): - raise SSLError( - f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' - ) - - -def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: - """ - Resolves the argument to a numeric constant, which can be passed to - the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_REQUIRED`. - If given a string it is assumed to be the name of the constant in the - :mod:`ssl` module or its abbreviation. - (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. - If it's neither `None` nor a string we assume it is already the numeric - constant which can directly be passed to wrap_socket. - """ - if candidate is None: - return CERT_REQUIRED - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "CERT_" + candidate) - return res # type: ignore[no-any-return] - - return candidate # type: ignore[return-value] - - -def resolve_ssl_version(candidate: None | int | str) -> int: - """ - like resolve_cert_reqs - """ - if candidate is None: - return PROTOCOL_TLS - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "PROTOCOL_" + candidate) - return typing.cast(int, res) - - return candidate - - -def create_urllib3_context( - ssl_version: int | None = None, - cert_reqs: int | None = None, - options: int | None = None, - ciphers: str | None = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - verify_flags: int | None = None, -) -> ssl.SSLContext: - """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. - - :param ssl_version: - The desired protocol version to use. This will default to - PROTOCOL_SSLv23 which will negotiate the highest protocol that both - the server and your installation of OpenSSL support. - - This parameter is deprecated instead use 'ssl_minimum_version'. - :param ssl_minimum_version: - The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - :param ssl_maximum_version: - The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the - default value. - :param cert_reqs: - Whether to require the certificate verification. This defaults to - ``ssl.CERT_REQUIRED``. - :param options: - Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, - ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. - :param ciphers: - Which cipher suites to allow the server to select. Defaults to either system configured - ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. - :param verify_flags: - The flags for certificate verification operations. These default to - ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. - :returns: - Constructed SSLContext object with specified options - :rtype: SSLContext - """ - if SSLContext is None: - raise TypeError("Can't create an SSLContext object without an ssl module") - - # This means 'ssl_version' was specified as an exact value. - if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): - # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' - # to avoid conflicts. - if ssl_minimum_version is not None or ssl_maximum_version is not None: - raise ValueError( - "Can't specify both 'ssl_version' and either " - "'ssl_minimum_version' or 'ssl_maximum_version'" - ) - - # 'ssl_version' is deprecated and will be removed in the future. - else: - # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. - ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MINIMUM_SUPPORTED - ) - ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MAXIMUM_SUPPORTED - ) - - # This warning message is pushing users to use 'ssl_minimum_version' - # instead of both min/max. Best practice is to only set the minimum version and - # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' - warnings.warn( - "'ssl_version' option is deprecated and will be " - "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", - category=FutureWarning, - stacklevel=2, - ) - - context = SSLContext(PROTOCOL_TLS_CLIENT) - if ssl_minimum_version is not None: - context.minimum_version = ssl_minimum_version - else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here - context.minimum_version = TLSVersion.TLSv1_2 - - if ssl_maximum_version is not None: - context.maximum_version = ssl_maximum_version - - # Unless we're given ciphers defer to either system ciphers in - # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. - if ciphers: - context.set_ciphers(ciphers) - - # Setting the default here, as we may have no ssl module on import - cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if options is None: - options = 0 - # SSLv2 is easily broken and is considered harmful and dangerous - options |= OP_NO_SSLv2 - # SSLv3 has several problems and is now dangerous - options |= OP_NO_SSLv3 - # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ - # (issue #309) - options |= OP_NO_COMPRESSION - # TLSv1.2 only. Unless set explicitly, do not request tickets. - # This may save some bandwidth on wire, and although the ticket is encrypted, - # there is a risk associated with it being on wire, - # if the server is not rotating its ticketing keys properly. - options |= OP_NO_TICKET - - context.options |= options - - if verify_flags is None: - verify_flags = 0 - # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN - # and VERIFY_X509_STRICT so we do the same - if sys.version_info >= (3, 13): - verify_flags |= VERIFY_X509_PARTIAL_CHAIN - verify_flags |= VERIFY_X509_STRICT - - context.verify_flags |= verify_flags - - # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is - # necessary for conditional client cert authentication with TLS 1.3. - # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using - # an SSLContext created by pyOpenSSL. - if getattr(context, "post_handshake_auth", None) is not None: - context.post_handshake_auth = True - - # The order of the below lines setting verify_mode and check_hostname - # matter due to safe-guards SSLContext has to prevent an SSLContext with - # check_hostname=True, verify_mode=NONE/OPTIONAL. - # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own - # 'ssl.match_hostname()' implementation. - if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: - context.verify_mode = cert_reqs - context.check_hostname = True - else: - context.check_hostname = False - context.verify_mode = cert_reqs - - context.hostname_checks_common_name = False - - if "SSLKEYLOGFILE" in os.environ: - sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) - else: - sslkeylogfile = None - if sslkeylogfile: - context.keylog_filename = sslkeylogfile - - return context - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: typing.Literal[False] = ..., -) -> ssl.SSLSocket: ... - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: bool = ..., -) -> ssl.SSLSocket | SSLTransportType: ... - - -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = None, - certfile: str | None = None, - cert_reqs: int | None = None, - ca_certs: str | None = None, - server_hostname: str | None = None, - ssl_version: int | None = None, - ciphers: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_cert_dir: str | None = None, - key_password: str | None = None, - ca_cert_data: None | str | bytes = None, - tls_in_tls: bool = False, -) -> ssl.SSLSocket | SSLTransportType: - """ - All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and - ca_cert_dir have the same meaning as they do when using - :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, - :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. - - :param server_hostname: - When SNI is supported, the expected hostname of the certificate - :param ssl_context: - A pre-made :class:`SSLContext` object. If none is provided, one will - be created using :func:`create_urllib3_context`. - :param ciphers: - A string of ciphers we wish the client to support. - :param ca_cert_dir: - A directory containing CA certificates in multiple separate files, as - supported by OpenSSL's -CApath flag or the capath argument to - SSLContext.load_verify_locations(). - :param key_password: - Optional password if the keyfile is encrypted. - :param ca_cert_data: - Optional string containing CA certificates in PEM format suitable for - passing as the cadata parameter to SSLContext.load_verify_locations() - :param tls_in_tls: - Use SSLTransport to wrap the existing socket. - """ - context = ssl_context - if context is None: - # Note: This branch of code and all the variables in it are only used in tests. - # We should consider deprecating and removing this code. - context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) - - if ca_certs or ca_cert_dir or ca_cert_data: - try: - context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) - except OSError as e: - raise SSLError(e) from e - - elif ssl_context is None and hasattr(context, "load_default_certs"): - # try to load OS default certs; works well on Windows. - context.load_default_certs() - - # Attempt to detect if we get the goofy behavior of the - # keyfile being encrypted and OpenSSL asking for the - # passphrase via the terminal and instead error out. - if keyfile and key_password is None and _is_key_file_encrypted(keyfile): - raise SSLError("Client private key is encrypted, password is required") - - if certfile: - if key_password is None: - context.load_cert_chain(certfile, keyfile) - else: - context.load_cert_chain(certfile, keyfile, key_password) - - context.set_alpn_protocols(ALPN_PROTOCOLS) - - ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) - return ssl_sock - - -def is_ipaddress(hostname: str | bytes) -> bool: - """Detects whether the hostname given is an IPv4 or IPv6 address. - Also detects IPv6 addresses with Zone IDs. - - :param str hostname: Hostname to examine. - :return: True if the hostname is an IP address, False otherwise. - """ - if isinstance(hostname, bytes): - # IDN A-label bytes are ASCII compatible. - hostname = hostname.decode("ascii") - return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) - - -def _is_key_file_encrypted(key_file: str) -> bool: - """Detects if a key file is encrypted or not.""" - with open(key_file) as f: - for line in f: - # Look for Proc-Type: 4,ENCRYPTED - if "ENCRYPTED" in line: - return True - - return False - - -def _ssl_wrap_socket_impl( - sock: socket.socket, - ssl_context: ssl.SSLContext, - tls_in_tls: bool, - server_hostname: str | None = None, -) -> ssl.SSLSocket | SSLTransportType: - if tls_in_tls: - if not SSLTransport: - # Import error, ssl is not available. - raise ProxySchemeUnsupported( - "TLS in TLS requires support for the 'ssl' module" - ) - - SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) - return SSLTransport(sock, ssl_context, server_hostname) - - return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py deleted file mode 100644 index 94994f25ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssl_match_hostname.py +++ /dev/null @@ -1,153 +0,0 @@ -"""The match_hostname() function from Python 3.5, essential when using SSL.""" - -# Note: This file is under the PSF license as the code comes from the python -# stdlib. http://docs.python.org/3/license.html -# It is modified to remove commonName support. - -from __future__ import annotations - -import ipaddress -import re -import typing -from ipaddress import IPv4Address, IPv6Address - -if typing.TYPE_CHECKING: - from .ssl_ import _TYPE_PEER_CERT_RET_DICT - -__version__ = "3.5.0.1" - - -class CertificateError(ValueError): - pass - - -def _dnsname_match( - dn: typing.Any, hostname: str, max_wildcards: int = 1 -) -> typing.Match[str] | None | bool: - """Matching according to RFC 6125, section 6.4.3 - - http://tools.ietf.org/html/rfc6125#section-6.4.3 - """ - pats = [] - if not dn: - return False - - # Ported from python3-syntax: - # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r".") - leftmost = parts[0] - remainder = parts[1:] - - wildcards = leftmost.count("*") - if wildcards > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survey of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn) - ) - - # speed up common case w/o wildcards - if not wildcards: - return bool(dn.lower() == hostname.lower()) - - # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. - if leftmost == "*": - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append("[^.]+") - elif leftmost.startswith("xn--") or hostname.startswith("xn--"): - # RFC 6125, section 6.4.3, subitem 3. - # The client SHOULD NOT attempt to match a presented identifier - # where the wildcard character is embedded within an A-label or - # U-label of an internationalized domain name. - pats.append(re.escape(leftmost)) - else: - # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) - - # add the remaining fragments, ignore any wildcards - for frag in remainder: - pats.append(re.escape(frag)) - - pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) - return pat.match(hostname) - - -def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: - """Exact matching of IP addresses. - - RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded - bytes of the IP address. An IP version 4 address is 4 octets, and an IP - version 6 address is 16 octets. [...] A reference identity of type IP-ID - matches if the address is identical to an iPAddress value of the - subjectAltName extension of the certificate." - """ - # OpenSSL may add a trailing newline to a subjectAltName's IP address - # Divergence from upstream: ipaddress can't handle byte str - ip = ipaddress.ip_address(ipname.rstrip()) - return bool(ip.packed == host_ip.packed) - - -def match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError( - "empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED" - ) - - try: - host_ip = ipaddress.ip_address(hostname) - except ValueError: - # Not an IP address (common case) - host_ip = None - dnsnames = [] - san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) - key: str - value: str - for key, value in san: - if key == "DNS": - if host_ip is None and _dnsname_match(value, hostname): - return - dnsnames.append(value) - elif key == "IP Address": - if host_ip is not None and _ipaddress_match(value, host_ip): - return - dnsnames.append(value) - - # We only check 'commonName' if it's enabled and we're not verifying - # an IP address. IP addresses aren't valid within 'commonName'. - if hostname_checks_common_name and host_ip is None and not dnsnames: - for sub in cert.get("subject", ()): - for key, value in sub: - if key == "commonName": - if _dnsname_match(value, hostname): - return - dnsnames.append( - value - ) # Defensive: for older PyPy and OpenSSL versions - - if len(dnsnames) > 1: - raise CertificateError( - "hostname %r " - "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) - ) - elif len(dnsnames) == 1: - raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") - else: - raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py deleted file mode 100644 index 6d59bc3bce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/ssltransport.py +++ /dev/null @@ -1,271 +0,0 @@ -from __future__ import annotations - -import io -import socket -import ssl -import typing - -from ..exceptions import ProxySchemeUnsupported - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT - - -_WriteBuffer = typing.Union[bytearray, memoryview] -_ReturnValue = typing.TypeVar("_ReturnValue") - -SSL_BLOCKSIZE = 16384 - - -class SSLTransport: - """ - The SSLTransport wraps an existing socket and establishes an SSL connection. - - Contrary to Python's implementation of SSLSocket, it allows you to chain - multiple TLS connections together. It's particularly useful if you need to - implement TLS within TLS. - - The class supports most of the socket API operations. - """ - - @staticmethod - def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: - """ - Raises a ProxySchemeUnsupported if the provided ssl_context can't be used - for TLS in TLS. - - The only requirement is that the ssl_context provides the 'wrap_bio' - methods. - """ - - if not hasattr(ssl_context, "wrap_bio"): - raise ProxySchemeUnsupported( - "TLS in TLS requires SSLContext.wrap_bio() which isn't " - "available on non-native SSLContext" - ) - - def __init__( - self, - socket: socket.socket, - ssl_context: ssl.SSLContext, - server_hostname: str | None = None, - suppress_ragged_eofs: bool = True, - ) -> None: - """ - Create an SSLTransport around socket using the provided ssl_context. - """ - self.incoming = ssl.MemoryBIO() - self.outgoing = ssl.MemoryBIO() - - self.suppress_ragged_eofs = suppress_ragged_eofs - self.socket = socket - - self.sslobj = ssl_context.wrap_bio( - self.incoming, self.outgoing, server_hostname=server_hostname - ) - - # Perform initial handshake. - self._ssl_io_loop(self.sslobj.do_handshake) - - def __enter__(self) -> Self: - return self - - def __exit__(self, *_: typing.Any) -> None: - self.close() - - def fileno(self) -> int: - return self.socket.fileno() - - def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: - return self._wrap_ssl_read(len, buffer) - - def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv") - return self._wrap_ssl_read(buflen) - - def recv_into( - self, - buffer: _WriteBuffer, - nbytes: int | None = None, - flags: int = 0, - ) -> None | int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv_into") - if nbytes is None: - nbytes = len(buffer) - return self.read(nbytes, buffer) - - def sendall(self, data: bytes, flags: int = 0) -> None: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to sendall") - count = 0 - with memoryview(data) as view, view.cast("B") as byte_view: - amount = len(byte_view) - while count < amount: - v = self.send(byte_view[count:]) - count += v - - def send(self, data: bytes, flags: int = 0) -> int: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to send") - return self._ssl_io_loop(self.sslobj.write, data) - - def makefile( - self, - mode: str, - buffering: int | None = None, - *, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: - """ - Python's httpclient uses makefile and buffered io when reading HTTP - messages and we need to support it. - - This is unfortunately a copy and paste of socket.py makefile with small - changes to point to the socket directly. - """ - if not set(mode) <= {"r", "w", "b"}: - raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") - - writing = "w" in mode - reading = "r" in mode or not writing - assert reading or writing - binary = "b" in mode - rawmode = "" - if reading: - rawmode += "r" - if writing: - rawmode += "w" - raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] - self.socket._io_refs += 1 # type: ignore[attr-defined] - if buffering is None: - buffering = -1 - if buffering < 0: - buffering = io.DEFAULT_BUFFER_SIZE - if buffering == 0: - if not binary: - raise ValueError("unbuffered streams must be binary") - return raw - buffer: typing.BinaryIO - if reading and writing: - buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] - elif reading: - buffer = io.BufferedReader(raw, buffering) - else: - assert writing - buffer = io.BufferedWriter(raw, buffering) - if binary: - return buffer - text = io.TextIOWrapper(buffer, encoding, errors, newline) - text.mode = mode # type: ignore[misc] - return text - - def unwrap(self) -> None: - self._ssl_io_loop(self.sslobj.unwrap) - - def close(self) -> None: - self.socket.close() - - @typing.overload - def getpeercert( - self, binary_form: typing.Literal[False] = ... - ) -> _TYPE_PEER_CERT_RET_DICT | None: ... - - @typing.overload - def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... - - def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: - return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] - - def version(self) -> str | None: - return self.sslobj.version() - - def cipher(self) -> tuple[str, str, int] | None: - return self.sslobj.cipher() - - def selected_alpn_protocol(self) -> str | None: - return self.sslobj.selected_alpn_protocol() - - def shared_ciphers(self) -> list[tuple[str, str, int]] | None: - return self.sslobj.shared_ciphers() - - def compression(self) -> str | None: - return self.sslobj.compression() - - def settimeout(self, value: float | None) -> None: - self.socket.settimeout(value) - - def gettimeout(self) -> float | None: - return self.socket.gettimeout() - - def _decref_socketios(self) -> None: - self.socket._decref_socketios() # type: ignore[attr-defined] - - def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: - try: - return self._ssl_io_loop(self.sslobj.read, len, buffer) - except ssl.SSLError as e: - if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: - return 0 # eof, return 0. - else: - raise - - # func is sslobj.do_handshake or sslobj.unwrap - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... - - # func is sslobj.write, arg1 is data - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... - - # func is sslobj.read, arg1 is len, arg2 is buffer - @typing.overload - def _ssl_io_loop( - self, - func: typing.Callable[[int, bytearray | None], bytes], - arg1: int, - arg2: bytearray | None, - ) -> bytes: ... - - def _ssl_io_loop( - self, - func: typing.Callable[..., _ReturnValue], - arg1: None | bytes | int = None, - arg2: bytearray | None = None, - ) -> _ReturnValue: - """Performs an I/O loop between incoming/outgoing and the socket.""" - should_loop = True - ret = None - - while should_loop: - errno = None - try: - if arg1 is None and arg2 is None: - ret = func() - elif arg2 is None: - ret = func(arg1) - else: - ret = func(arg1, arg2) - except ssl.SSLError as e: - if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): - # WANT_READ, and WANT_WRITE are expected, others are not. - raise e - errno = e.errno - - buf = self.outgoing.read() - self.socket.sendall(buf) - - if errno is None: - should_loop = False - elif errno == ssl.SSL_ERROR_WANT_READ: - buf = self.socket.recv(SSL_BLOCKSIZE) - if buf: - self.incoming.write(buf) - else: - self.incoming.write_eof() - return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py deleted file mode 100644 index 4bb1be11d9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/timeout.py +++ /dev/null @@ -1,275 +0,0 @@ -from __future__ import annotations - -import time -import typing -from enum import Enum -from socket import getdefaulttimeout - -from ..exceptions import TimeoutStateError - -if typing.TYPE_CHECKING: - from typing import Final - - -class _TYPE_DEFAULT(Enum): - # This value should never be passed to socket.settimeout() so for safety we use a -1. - # socket.settimout() raises a ValueError for negative values. - token = -1 - - -_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token - -_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] - - -class Timeout: - """Timeout configuration. - - Timeouts can be defined as a default for a pool: - - .. code-block:: python - - import urllib3 - - timeout = urllib3.util.Timeout(connect=2.0, read=7.0) - - http = urllib3.PoolManager(timeout=timeout) - - resp = http.request("GET", "https://example.com/") - - print(resp.status) - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", timeout=Timeout(10)) - - Timeouts can be disabled by setting all the parameters to ``None``: - - .. code-block:: python - - no_timeout = Timeout(connect=None, read=None) - response = http.request("GET", "https://example.com/", timeout=no_timeout) - - - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: int, float, or None - - :param connect: - The maximum amount of time (in seconds) to wait for a connection - attempt to a server to succeed. Omitting the parameter will default the - connect timeout to the system default, probably `the global default - timeout in socket.py - `_. - None will set an infinite timeout for connection attempts. - - :type connect: int, float, or None - - :param read: - The maximum amount of time (in seconds) to wait between consecutive - read operations for a response from the server. Omitting the parameter - will default the read timeout to the system default, probably `the - global default timeout in socket.py - `_. - None will set an infinite timeout. - - :type read: int, float, or None - - .. note:: - - Many factors can affect the total amount of time for urllib3 to return - an HTTP response. - - For example, Python's DNS resolver does not obey the timeout specified - on the socket. Other factors that can affect total request time include - high CPU load, high swap, the program running at a low priority level, - or other behaviors. - - In addition, the read and total timeouts only measure the time between - read operations on the socket connecting the client and the server, - not the total amount of time for the request to return a complete - response. For most requests, the timeout is raised because the server - has not sent the first byte in the specified time. This is not always - the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not trigger, even though the request will take - several minutes to complete. - """ - - #: A sentinel object representing the default timeout value - DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT - - def __init__( - self, - total: _TYPE_TIMEOUT = None, - connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - ) -> None: - self._connect = self._validate_timeout(connect, "connect") - self._read = self._validate_timeout(read, "read") - self.total = self._validate_timeout(total, "total") - self._start_connect: float | None = None - - def __repr__(self) -> str: - return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" - - # __str__ provided for backwards compatibility - __str__ = __repr__ - - @staticmethod - def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: - return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout - - @classmethod - def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: - """Check that a timeout attribute is valid. - - :param value: The timeout value to validate - :param name: The name of the timeout attribute to validate. This is - used to specify in error messages. - :return: The validated and casted version of the given value. - :raises ValueError: If it is a numeric value less than or equal to - zero, or the type is not an integer, float, or None. - """ - if value is None or value is _DEFAULT_TIMEOUT: - return value - - if isinstance(value, bool): - raise ValueError( - "Timeout cannot be a boolean value. It must " - "be an int, float or None." - ) - try: - float(value) - except (TypeError, ValueError): - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - try: - if value <= 0: - raise ValueError( - "Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than or equal to 0." % (name, value) - ) - except TypeError: - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - return value - - @classmethod - def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: - """Create a new Timeout from a legacy timeout value. - - The timeout value used by httplib.py sets the same timeout on the - connect(), and recv() socket requests. This creates a :class:`Timeout` - object that sets the individual timeouts to the ``timeout`` value - passed to this function. - - :param timeout: The legacy timeout value. - :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None - :return: Timeout object - :rtype: :class:`Timeout` - """ - return Timeout(read=timeout, connect=timeout) - - def clone(self) -> Timeout: - """Create a copy of the timeout object - - Timeout properties are stored per-pool but each request needs a fresh - Timeout object to ensure each one has its own start/stop configured. - - :return: a copy of the timeout object - :rtype: :class:`Timeout` - """ - # We can't use copy.deepcopy because that will also create a new object - # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to - # detect the user default. - return Timeout(connect=self._connect, read=self._read, total=self.total) - - def start_connect(self) -> float: - """Start the timeout clock, used during a connect() attempt - - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to start a timer that has been started already. - """ - if self._start_connect is not None: - raise TimeoutStateError("Timeout timer has already been started.") - self._start_connect = time.monotonic() - return self._start_connect - - def get_connect_duration(self) -> float: - """Gets the time elapsed since the call to :meth:`start_connect`. - - :return: Elapsed time in seconds. - :rtype: float - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to get duration for a timer that hasn't been started. - """ - if self._start_connect is None: - raise TimeoutStateError( - "Can't get connect duration for timer that has not started." - ) - return time.monotonic() - self._start_connect - - @property - def connect_timeout(self) -> _TYPE_TIMEOUT: - """Get the value to use when setting a connection timeout. - - This will be a positive float or integer, the value None - (never timeout), or the default system timeout. - - :return: Connect timeout. - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - """ - if self.total is None: - return self._connect - - if self._connect is None or self._connect is _DEFAULT_TIMEOUT: - return self.total - - return min(self._connect, self.total) # type: ignore[type-var] - - @property - def read_timeout(self) -> float | None: - """Get the value for the read timeout. - - This assumes some time has elapsed in the connection timeout and - computes the read timeout appropriately. - - If self.total is set, the read timeout is dependent on the amount of - time taken by the connect timeout. If the connection time has not been - established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be - raised. - - :return: Value to use for the read timeout. - :rtype: int, float or None - :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` - has not yet been called on this object. - """ - if ( - self.total is not None - and self.total is not _DEFAULT_TIMEOUT - and self._read is not None - and self._read is not _DEFAULT_TIMEOUT - ): - # In case the connect timeout has not yet been established. - if self._start_connect is None: - return self._read - return max(0, min(self.total - self.get_connect_duration(), self._read)) - elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: - return max(0, self.total - self.get_connect_duration()) - else: - return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py deleted file mode 100644 index db057f17be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/url.py +++ /dev/null @@ -1,469 +0,0 @@ -from __future__ import annotations - -import re -import typing - -from ..exceptions import LocationParseError -from .util import to_str - -# We only want to normalize urls with an HTTP(S) scheme. -# urllib3 infers URLs without a scheme (None) to be http. -_NORMALIZABLE_SCHEMES = ("http", "https", None) - -# Almost all of these patterns were derived from the -# 'rfc3986' module: https://github.com/python-hyper/rfc3986 -_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") -_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") -_URI_RE = re.compile( - r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" - r"(?://([^\\/?#]*))?" - r"([^?#]*)" - r"(?:\?([^#]*))?" - r"(?:#(.*))?$", - re.UNICODE | re.DOTALL, -) - -_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" -_HEX_PAT = "[0-9A-Fa-f]{1,4}" -_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) -_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} -_variations = [ - # 6( h16 ":" ) ls32 - "(?:%(hex)s:){6}%(ls32)s", - # "::" 5( h16 ":" ) ls32 - "::(?:%(hex)s:){5}%(ls32)s", - # [ h16 ] "::" 4( h16 ":" ) ls32 - "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", - # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 - "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", - # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 - "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", - # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 - "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", - # [ *4( h16 ":" ) h16 ] "::" ls32 - "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", - # [ *5( h16 ":" ) h16 ] "::" h16 - "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", - # [ *6( h16 ":" ) h16 ] "::" - "(?:(?:%(hex)s:){0,6}%(hex)s)?::", -] - -_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" -_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" -_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" -_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" -_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" -_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") - -_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") -_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") -_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") -_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") -_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") - -_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( - _REG_NAME_PAT, - _IPV4_PAT, - _IPV6_ADDRZ_PAT, -) -_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) - -_UNRESERVED_CHARS = set( - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" -) -_SUB_DELIM_CHARS = set("!$&'()*+,;=") -_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} -_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} -_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} - - -class Url( - typing.NamedTuple( - "Url", - [ - ("scheme", typing.Optional[str]), - ("auth", typing.Optional[str]), - ("host", typing.Optional[str]), - ("port", typing.Optional[int]), - ("path", typing.Optional[str]), - ("query", typing.Optional[str]), - ("fragment", typing.Optional[str]), - ], - ) -): - """ - Data structure for representing an HTTP URL. Used as a return value for - :func:`parse_url`. Both the scheme and host are normalized as they are - both case-insensitive according to RFC 3986. - """ - - def __new__( # type: ignore[no-untyped-def] - cls, - scheme: str | None = None, - auth: str | None = None, - host: str | None = None, - port: int | None = None, - path: str | None = None, - query: str | None = None, - fragment: str | None = None, - ): - if path and not path.startswith("/"): - path = "/" + path - if scheme is not None: - scheme = scheme.lower() - return super().__new__(cls, scheme, auth, host, port, path, query, fragment) - - @property - def hostname(self) -> str | None: - """For backwards-compatibility with urlparse. We're nice like that.""" - return self.host - - @property - def request_uri(self) -> str: - """Absolute path including the query string.""" - uri = self.path or "/" - - if self.query is not None: - uri += "?" + self.query - - return uri - - @property - def authority(self) -> str | None: - """ - Authority component as defined in RFC 3986 3.2. - This includes userinfo (auth), host and port. - - i.e. - userinfo@host:port - """ - userinfo = self.auth - netloc = self.netloc - if netloc is None or userinfo is None: - return netloc - else: - return f"{userinfo}@{netloc}" - - @property - def netloc(self) -> str | None: - """ - Network location including host and port. - - If you need the equivalent of urllib.parse's ``netloc``, - use the ``authority`` property instead. - """ - if self.host is None: - return None - if self.port: - return f"{self.host}:{self.port}" - return self.host - - @property - def url(self) -> str: - """ - Convert self into a url - - This function should more or less round-trip with :func:`.parse_url`. The - returned url may not be exactly the same as the url inputted to - :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls - with a blank port will have : removed). - - Example: - - .. code-block:: python - - import urllib3 - - U = urllib3.util.parse_url("https://google.com/mail/") - - print(U.url) - # "https://google.com/mail/" - - print( urllib3.util.Url("https", "username:password", - "host.com", 80, "/path", "query", "fragment" - ).url - ) - # "https://username:password@host.com:80/path?query#fragment" - """ - scheme, auth, host, port, path, query, fragment = self - url = "" - - # We use "is not None" we want things to happen with empty strings (or 0 port) - if scheme is not None: - url += scheme + "://" - if auth is not None: - url += auth + "@" - if host is not None: - url += host - if port is not None: - url += ":" + str(port) - if path is not None: - url += path - if query is not None: - url += "?" + query - if fragment is not None: - url += "#" + fragment - - return url - - def __str__(self) -> str: - return self.url - - -@typing.overload -def _encode_invalid_chars( - component: str, allowed_chars: typing.Container[str] -) -> str: # Abstract - ... - - -@typing.overload -def _encode_invalid_chars( - component: None, allowed_chars: typing.Container[str] -) -> None: # Abstract - ... - - -def _encode_invalid_chars( - component: str | None, allowed_chars: typing.Container[str] -) -> str | None: - """Percent-encodes a URI component without reapplying - onto an already percent-encoded component. - """ - if component is None: - return component - - component = to_str(component) - - # Normalize existing percent-encoded bytes. - # Try to see if the component we're encoding is already percent-encoded - # so we can skip all '%' characters but still encode all others. - component, percent_encodings = _PERCENT_RE.subn( - lambda match: match.group(0).upper(), component - ) - - uri_bytes = component.encode("utf-8", "surrogatepass") - is_percent_encoded = percent_encodings == uri_bytes.count(b"%") - encoded_component = bytearray() - - for i in range(0, len(uri_bytes)): - # Will return a single character bytestring - byte = uri_bytes[i : i + 1] - byte_ord = ord(byte) - if (is_percent_encoded and byte == b"%") or ( - byte_ord < 128 and byte.decode() in allowed_chars - ): - encoded_component += byte - continue - encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) - - return encoded_component.decode() - - -def _remove_path_dot_segments(path: str) -> str: - # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code - segments = path.split("/") # Turn the path into a list of segments - output = [] # Initialize the variable to use to store output - - for segment in segments: - # '.' is the current directory, so ignore it, it is superfluous - if segment == ".": - continue - # Anything other than '..', should be appended to the output - if segment != "..": - output.append(segment) - # In this case segment == '..', if we can, we should pop the last - # element - elif output: - output.pop() - - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if path.startswith("/") and (not output or output[0]): - output.insert(0, "") - - # If the path starts with '/.' or '/..' ensure we add one more empty - # string to add a trailing '/' - if path.endswith(("/.", "/..")): - output.append("") - - return "/".join(output) - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - if host: - if scheme in _NORMALIZABLE_SCHEMES: - is_ipv6 = _IPV6_ADDRZ_RE.match(host) - if is_ipv6: - # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as - # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID - # separator as necessary to return a valid RFC 4007 scoped IP. - match = _ZONE_ID_RE.search(host) - if match: - start, end = match.span(1) - zone_id = host[start:end] - - if zone_id.startswith("%25") and zone_id != "%25": - zone_id = zone_id[3:] - else: - zone_id = zone_id[1:] - zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) - return f"{host[:start].lower()}%{zone_id}{host[end:]}" - else: - return host.lower() - elif not _IPV4_RE.match(host): - return to_str( - b".".join([_idna_encode(label) for label in host.split(".")]), - "ascii", - ) - return host - - -def _idna_encode(name: str) -> bytes: - if not name.isascii(): - try: - import idna - except ImportError: - raise LocationParseError( - "Unable to parse URL without the 'idna' module" - ) from None - - try: - return idna.encode(name.lower(), strict=True, std3_rules=True) - except idna.IDNAError: - raise LocationParseError( - f"Name '{name}' is not a valid IDNA label" - ) from None - - return name.lower().encode("ascii") - - -def _encode_target(target: str) -> str: - """Percent-encodes a request target so that there are no invalid characters - - Pre-condition for this function is that 'target' must start with '/'. - If that is the case then _TARGET_RE will always produce a match. - """ - match = _TARGET_RE.match(target) - if not match: # Defensive: - raise LocationParseError(f"{target!r} is not a valid request URI") - - path, query = match.groups() - encoded_target = _encode_invalid_chars(path, _PATH_CHARS) - if query is not None: - query = _encode_invalid_chars(query, _QUERY_CHARS) - encoded_target += "?" + query - return encoded_target - - -def parse_url(url: str) -> Url: - """ - Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is - performed to parse incomplete urls. Fields not provided will be None. - This parser is RFC 3986 and RFC 6874 compliant. - - The parser logic and helper functions are based heavily on - work done in the ``rfc3986`` module. - - :param str url: URL to parse into a :class:`.Url` namedtuple. - - Partly backwards-compatible with :mod:`urllib.parse`. - - Example: - - .. code-block:: python - - import urllib3 - - print( urllib3.util.parse_url('http://google.com/mail/')) - # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) - - print( urllib3.util.parse_url('google.com:80')) - # Url(scheme=None, host='google.com', port=80, path=None, ...) - - print( urllib3.util.parse_url('/foo?bar')) - # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) - """ - if not url: - # Empty - return Url() - - source_url = url - if not _SCHEME_RE.search(url): - url = "//" + url - - scheme: str | None - authority: str | None - auth: str | None - host: str | None - port: str | None - port_int: int | None - path: str | None - query: str | None - fragment: str | None - - try: - scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] - normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES - - if scheme: - scheme = scheme.lower() - - if authority: - auth, _, host_port = authority.rpartition("@") - auth = auth or None - host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] - if auth and normalize_uri: - auth = _encode_invalid_chars(auth, _USERINFO_CHARS) - if port == "": - port = None - else: - auth, host, port = None, None, None - - if port is not None: - port_int = int(port) - if not (0 <= port_int <= 65535): - raise LocationParseError(url) - else: - port_int = None - - host = _normalize_host(host, scheme) - - if normalize_uri and path: - path = _remove_path_dot_segments(path) - path = _encode_invalid_chars(path, _PATH_CHARS) - if normalize_uri and query: - query = _encode_invalid_chars(query, _QUERY_CHARS) - if normalize_uri and fragment: - fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) - - except (ValueError, AttributeError) as e: - raise LocationParseError(source_url) from e - - # For the sake of backwards compatibility we put empty - # string values for path if there are any defined values - # beyond the path in the URL. - # TODO: Remove this when we break backwards compatibility. - if not path: - if query is not None or fragment is not None: - path = "" - else: - path = None - - return Url( - scheme=scheme, - auth=auth, - host=host, - port=port_int, - path=path, - query=query, - fragment=fragment, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py deleted file mode 100644 index 35c77e4025..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/util.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import typing -from types import TracebackType - - -def to_bytes( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> bytes: - if isinstance(x, bytes): - return x - elif not isinstance(x, str): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.encode(encoding or "utf-8", errors=errors or "strict") - return x.encode() - - -def to_str( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> str: - if isinstance(x, str): - return x - elif not isinstance(x, bytes): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.decode(encoding or "utf-8", errors=errors or "strict") - return x.decode() - - -def reraise( - tp: type[BaseException] | None, - value: BaseException, - tb: TracebackType | None = None, -) -> typing.NoReturn: - try: - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - finally: - value = None # type: ignore[assignment] - tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py deleted file mode 100644 index aeca0c7ad5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionDenylist/urllib3/util/wait.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import select -import socket -from functools import partial - -__all__ = ["wait_for_read", "wait_for_write"] - - -# How should we wait on sockets? -# -# There are two types of APIs you can use for waiting on sockets: the fancy -# modern stateful APIs like epoll/kqueue, and the older stateless APIs like -# select/poll. The stateful APIs are more efficient when you have a lots of -# sockets to keep track of, because you can set them up once and then use them -# lots of times. But we only ever want to wait on a single socket at a time -# and don't want to keep track of state, so the stateless APIs are actually -# more efficient. So we want to use select() or poll(). -# -# Now, how do we choose between select() and poll()? On traditional Unixes, -# select() has a strange calling convention that makes it slow, or fail -# altogether, for high-numbered file descriptors. The point of poll() is to fix -# that, so on Unixes, we prefer poll(). -# -# On Windows, there is no poll() (or at least Python doesn't provide a wrapper -# for it), but that's OK, because on Windows, select() doesn't have this -# strange calling convention; plain select() works fine. -# -# So: on Windows we use select(), and everywhere else we use poll(). We also -# fall back to select() in case poll() is somehow broken or missing. - - -def select_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - rcheck = [] - wcheck = [] - if read: - rcheck.append(sock) - if write: - wcheck.append(sock) - # When doing a non-blocking connect, most systems signal success by - # marking the socket writable. Windows, though, signals success by marked - # it as "exceptional". We paper over the difference by checking the write - # sockets for both conditions. (The stdlib selectors module does the same - # thing.) - fn = partial(select.select, rcheck, wcheck, wcheck) - rready, wready, xready = fn(timeout) - return bool(rready or wready or xready) - - -def poll_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - mask = 0 - if read: - mask |= select.POLLIN - if write: - mask |= select.POLLOUT - poll_obj = select.poll() - poll_obj.register(sock, mask) - - # For some reason, poll() takes timeout in milliseconds - def do_poll(t: float | None) -> list[tuple[int, int]]: - if t is not None: - t *= 1000 - return poll_obj.poll(t) - - return bool(do_poll(timeout)) - - -def _have_working_poll() -> bool: - # Apparently some systems have a select.poll that fails as soon as you try - # to use it, either due to strange configuration or broken monkeypatching - # from libraries like eventlet/greenlet. - try: - poll_obj = select.poll() - poll_obj.poll(0) - except (AttributeError, OSError): - return False - else: - return True - - -def wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - # We delay choosing which implementation to use until the first time we're - # called. We could do it at import time, but then we might make the wrong - # decision if someone goes wild with monkeypatching select.poll after - # we're imported. - global wait_for_socket - if _have_working_poll(): - wait_for_socket = poll_wait_for_socket - elif hasattr(select, "select"): - wait_for_socket = select_wait_for_socket - return wait_for_socket(sock, read, write, timeout) - - -def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for reading to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, read=True, timeout=timeout) - - -def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for writing to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA deleted file mode 100644 index 0d4f101491..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/METADATA +++ /dev/null @@ -1,78 +0,0 @@ -Metadata-Version: 2.4 -Name: certifi -Version: 2026.6.17 -Summary: Python package for providing Mozilla's CA Bundle. -Home-page: https://github.com/certifi/python-certifi -Author: Kenneth Reitz -Author-email: me@kennethreitz.com -License: MPL-2.0 -Project-URL: Source, https://github.com/certifi/python-certifi -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) -Classifier: Natural Language :: English -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Requires-Python: >=3.7 -License-File: LICENSE -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: home-page -Dynamic: license -Dynamic: license-file -Dynamic: project-url -Dynamic: requires-python -Dynamic: summary - -Certifi: Python SSL Certificates -================================ - -Certifi provides Mozilla's carefully curated collection of Root Certificates for -validating the trustworthiness of SSL certificates while verifying the identity -of TLS hosts. It has been extracted from the `Requests`_ project. - -Installation ------------- - -``certifi`` is available on PyPI. Simply install it with ``pip``:: - - $ pip install certifi - -Usage ------ - -To reference the installed certificate authority (CA) bundle, you can use the -built-in function:: - - >>> import certifi - - >>> certifi.where() - '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' - -Or from the command line:: - - $ python -m certifi - /usr/local/lib/python3.7/site-packages/certifi/cacert.pem - -Enjoy! - -.. _`Requests`: https://requests.readthedocs.io/en/latest/ - -Addition/Removal of Certificates --------------------------------- - -Certifi does not support any addition/removal or other modification of the -CA trust store content. This project is intended to provide a reliable and -highly portable root of trust to python deployments. Look to upstream projects -for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD deleted file mode 100644 index 13b0ce7da4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/RECORD +++ /dev/null @@ -1,12 +0,0 @@ -certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 -certifi-2026.6.17.dist-info/RECORD,, -certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 -certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 -certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 -certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 -certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 -certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 -certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 -certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL deleted file mode 100644 index 14a883f292..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (82.0.1) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE deleted file mode 100644 index 62b076cdee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/licenses/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -This package contains a modified version of ca-bundle.crt: - -ca-bundle.crt -- Bundle of CA Root Certificates - -This is a bundle of X.509 certificates of public Certificate Authorities -(CA). These were automatically extracted from Mozilla's root certificates -file (certdata.txt). This file can be found in the mozilla source tree: -https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt -It contains the certificates in PEM format and therefore -can be directly used with curl / libcurl / php_curl, or with -an Apache+mod_ssl webserver for SSL client authentication. -Just configure this file as the SSLCACertificateFile.# - -***** BEGIN LICENSE BLOCK ***** -This Source Code Form is subject to the terms of the Mozilla Public License, -v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain -one at http://mozilla.org/MPL/2.0/. - -***** END LICENSE BLOCK ***** -@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt deleted file mode 100644 index 963eac530b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi-2026.6.17.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py deleted file mode 100644 index ed9a74b7a0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .core import contents, where - -__all__ = ["contents", "where"] -__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py deleted file mode 100644 index 8945b5da85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/__main__.py +++ /dev/null @@ -1,12 +0,0 @@ -import argparse - -from certifi import contents, where - -parser = argparse.ArgumentParser() -parser.add_argument("-c", "--contents", action="store_true") -args = parser.parse_args() - -if args.contents: - print(contents()) -else: - print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem deleted file mode 100644 index 1c2dbfeb68..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/cacert.pem +++ /dev/null @@ -1,3863 +0,0 @@ - -# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Label: "COMODO ECC Certification Authority" -# Serial: 41578283867086692638256921589707938090 -# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 -# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 -# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT -IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw -MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N -T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR -FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J -cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW -BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm -fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv -GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" -# Serial: 80544274841616 -# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 -# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 -# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG -EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 -MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl -cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR -dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB -pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM -b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm -aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz -IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT -lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz -AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 -VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG -ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 -BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG -AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M -U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh -bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C -+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F -uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 -XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Label: "Microsec e-Szigno Root CA 2009" -# Serial: 14014712776195784473 -# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 -# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e -# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G -CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y -OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx -FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp -Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP -kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc -cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U -fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 -N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC -xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 -+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM -Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG -SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h -mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk -ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c -2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t -HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Label: "GlobalSign Root CA - R3" -# Serial: 4835703278459759426209954 -# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 -# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad -# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 -MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 -RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT -gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm -KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd -QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ -XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o -LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU -RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp -jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK -6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX -mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs -Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH -WD9f ------END CERTIFICATE----- - -# Issuer: CN=Izenpe.com O=IZENPE S.A. -# Subject: CN=Izenpe.com O=IZENPE S.A. -# Label: "Izenpe.com" -# Serial: 917563065490389241595536686991402621 -# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 -# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 -# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f ------BEGIN CERTIFICATE----- -MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 -MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 -ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD -VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j -b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq -scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO -xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H -LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX -uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD -yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ -JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q -rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN -BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L -hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB -QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ -HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu -Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg -QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB -BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx -MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA -A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb -laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 -awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo -JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw -LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT -VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk -LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb -UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ -QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ -naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls -QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE----- - -# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Label: "Go Daddy Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 -# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b -# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT -EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp -ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz -NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH -EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE -AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD -E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH -/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy -DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh -GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR -tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA -AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX -WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu -9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr -gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo -2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI -4uJEvlz36hz1 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 -# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e -# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Services Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 -# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f -# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs -ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD -VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy -ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy -dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p -OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 -8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K -Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe -hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk -6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q -AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI -bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB -ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z -qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn -0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN -sSi6 ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA" -# Serial: 279744 -# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 -# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e -# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM -MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D -ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU -cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 -WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg -Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw -IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH -UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM -TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU -BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM -kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x -AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV -HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y -sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL -I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 -J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY -VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Label: "TWCA Root Certification Authority" -# Serial: 1 -# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 -# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 -# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES -MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU -V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz -WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO -LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE -AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH -K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX -RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z -rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx -3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq -hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC -MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls -XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D -lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn -aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ -YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Label: "Security Communication RootCA2" -# Serial: 0 -# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 -# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 -# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl -MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe -U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX -DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy -dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj -YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV -OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr -zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM -VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ -hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO -ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw -awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs -OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF -coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc -okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 -t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy -1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ -SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Label: "Actalis Authentication Root CA" -# Serial: 6271844772424770508 -# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 -# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac -# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE -BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w -MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC -SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 -ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv -UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX -4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 -KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ -gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb -rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ -51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F -be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe -KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F -v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn -fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 -jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz -ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL -e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 -jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz -WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V -SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j -pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX -X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok -fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R -K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU -ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU -LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT -LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 2 Root CA" -# Serial: 2 -# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 -# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 -# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr -6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV -L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 -1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx -MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ -QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB -arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr -Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi -FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS -P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN -9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz -uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h -9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t -OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo -+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 -KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 -DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us -H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ -I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 -5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h -3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz -Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 3 Root CA" -# Serial: 2 -# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec -# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 -# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y -ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E -N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 -tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX -0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c -/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X -KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY -zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS -O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D -34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP -K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv -Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj -QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS -IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 -HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa -O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv -033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u -dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE -kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 -3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD -u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq -4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 3" -# Serial: 1 -# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef -# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 -# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN -8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ -RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 -hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 -ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM -EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 -A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy -WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ -1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 -6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT -91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p -TpPDpFQUWw== ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 2009" -# Serial: 623603 -# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f -# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 -# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 ------BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha -ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM -HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 -UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 -tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R -ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM -lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp -/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G -A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy -MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl -cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js -L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL -BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni -acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K -zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 -PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y -Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 EV 2009" -# Serial: 623604 -# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 -# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 -# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw -NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV -BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn -ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 -3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z -qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR -p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 -HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw -ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea -HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw -Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh -c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E -RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt -dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku -Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp -3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF -CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na -xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX -KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE----- - -# Issuer: CN=CA Disig Root R2 O=Disig a.s. -# Subject: CN=CA Disig Root R2 O=Disig a.s. -# Label: "CA Disig Root R2" -# Serial: 10572350602393338211 -# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 -# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 -# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV -BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu -MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy -MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx -EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe -NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH -PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I -x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe -QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR -yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO -QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 -H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ -QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD -i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs -nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 -rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI -hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM -tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf -GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb -lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka -+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal -TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i -nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 -gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr -G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os -zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x -L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE----- - -# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Label: "ACCVRAIZ1" -# Serial: 6828503384748696800 -# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 -# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 -# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 ------BEGIN CERTIFICATE----- -MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE -AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw -CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ -BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND -VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb -qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY -HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo -G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA -lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr -IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ -0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH -k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 -4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO -m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa -cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl -uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI -KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls -ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG -AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 -VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT -VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG -CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA -cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA -QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA -7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA -cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA -QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA -czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu -aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt -aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud -DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF -BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp -D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU -JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m -AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD -vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms -tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH -7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h -I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA -h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF -d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H -pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE----- - -# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA Global Root CA" -# Serial: 3262 -# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 -# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 -# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx -EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT -VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 -NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT -B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF -10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz -0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh -MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH -zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc -46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 -yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi -laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP -oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA -BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE -qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm -4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL -1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF -H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo -RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ -nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh -15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW -6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW -nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j -wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz -aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy -KwbQBM0= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 2" -# Serial: 1 -# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a -# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 -# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd -AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC -FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi -1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq -jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ -wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ -WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy -NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC -uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw -IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 -g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP -BSeOE6Fuwg== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot 2011 O=Atos -# Subject: CN=Atos TrustedRoot 2011 O=Atos -# Label: "Atos TrustedRoot 2011" -# Serial: 6643877497813316402 -# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 -# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 -# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE -AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG -EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM -FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC -REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp -Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM -VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ -SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ -4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L -cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi -eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG -A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 -DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j -vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP -DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc -maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D -lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv -KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 1 G3" -# Serial: 687049649626669250736271037606554624078720034195 -# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab -# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 -# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 -MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV -wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe -rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 -68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh -4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp -UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o -abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc -3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G -KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt -hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO -Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt -zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD -ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC -MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 -cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN -qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 -YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv -b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 -8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k -NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj -ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp -q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt -nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 2 G3" -# Serial: 390156079458959257446133169266079962026824725800 -# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 -# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 -# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 -MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf -qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW -n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym -c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ -O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 -o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j -IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq -IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz -8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh -vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l -7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG -cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD -ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC -roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga -W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n -lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE -+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV -csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd -dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg -KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM -HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 -WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 3 G3" -# Serial: 268090761170461462463995952157327242137089239581 -# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 -# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d -# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 -MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR -/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu -FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR -U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c -ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR -FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k -A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw -eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl -sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp -VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q -A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ -ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD -ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI -FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv -oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg -u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP -0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf -3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl -8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ -DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN -PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ -ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G2" -# Serial: 15385348160840213938643033620894905419 -# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d -# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f -# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 ------BEGIN CERTIFICATE----- -MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA -n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc -biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp -EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA -bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu -YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB -AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW -BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI -QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I -0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni -lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 -B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv -ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo -IhNzbM8m9Yop5w== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G3" -# Serial: 15459312981008553731928384953135426796 -# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb -# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 -# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg -RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf -Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q -RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD -AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY -JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv -6pZjamVFkpUBtA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G2" -# Serial: 4293743540046975378534879503202253541 -# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 -# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 -# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f ------BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH -MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI -2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx -1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ -q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz -tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ -vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV -5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY -1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 -NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG -Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 -8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe -pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G3" -# Serial: 7089244469030293291760083333884364146 -# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca -# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e -# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 ------BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe -Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw -EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x -IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG -fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO -Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx -AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ -oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 -sycX ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Trusted Root G4" -# Serial: 7451500558977370777930084869016614236 -# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 -# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 -# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 ------BEGIN CERTIFICATE----- -MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg -RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y -ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If -xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV -ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO -DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ -jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ -CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi -EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM -fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY -uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK -chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t -9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD -ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 -SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd -+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc -fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa -sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N -cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N -0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie -4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI -r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 -/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm -gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE----- - -# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Label: "COMODO RSA Certification Authority" -# Serial: 101909084537582093308941363524873193117 -# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 -# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 -# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 ------BEGIN CERTIFICATE----- -MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB -hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV -BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT -EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR -6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X -pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC -9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV -/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf -Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z -+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w -qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah -SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC -u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf -Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq -crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E -FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl -wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM -4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV -2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna -FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ -CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK -boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke -jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL -S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb -QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl -0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB -NVOFBkpdn627G190 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Label: "USERTrust RSA Certification Authority" -# Serial: 2645093764781058787591871645665788717 -# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 -# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e -# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 ------BEGIN CERTIFICATE----- -MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB -iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl -cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV -BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw -MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV -BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B -3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY -tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ -Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 -VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT -79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 -c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT -Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l -c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee -UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE -Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd -BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G -A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF -Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO -VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 -ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs -8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR -iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze -Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ -XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ -qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB -VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB -L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG -jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Label: "USERTrust ECC Certification Authority" -# Serial: 123013823720199481456569720443997572134 -# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 -# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 -# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a ------BEGIN CERTIFICATE----- -MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL -MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl -eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT -JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT -Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg -VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo -I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng -o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G -A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB -zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW -RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Label: "GlobalSign ECC Root CA - R5" -# Serial: 32785792099990507226680698011560947931244 -# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 -# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa -# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 ------BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc -8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke -hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI -KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg -515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO -xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Label: "IdenTrust Commercial Root CA 1" -# Serial: 13298821034946342390520003877796839426 -# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 -# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 -# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu -VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw -MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw -JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT -3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU -+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp -S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 -bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi -T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL -vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK -Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK -dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT -c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv -l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N -iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD -ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH -6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt -LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 -nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 -+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK -W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT -AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq -l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG -4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ -mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A -7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Label: "IdenTrust Public Sector Root CA 1" -# Serial: 13298821034946342390521976156843933698 -# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba -# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd -# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu -VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN -MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 -MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 -ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy -RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS -bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF -/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R -3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw -EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy -9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V -GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ -2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV -WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD -W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN -AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj -t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV -DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 -TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G -lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW -mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df -WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 -+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ -tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA -GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv -8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE----- - -# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority -# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority -# Label: "CFCA EV ROOT" -# Serial: 407555286 -# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 -# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 -# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD -TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx -MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j -aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP -T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 -sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL -TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 -/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp -7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz -EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt -hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP -a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot -aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg -TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV -PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv -cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL -tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd -BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT -ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL -jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS -ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy -P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 -xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d -Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN -5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe -/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z -AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ -5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GB CA" -# Serial: 157768595616588414422159278966750757568 -# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d -# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed -# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt -MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg -Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i -YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x -CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG -b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh -bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 -HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx -WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX -1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk -u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P -99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r -M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB -BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh -cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 -gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO -ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf -aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic -Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE----- - -# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Label: "SZAFIR ROOT CA2" -# Serial: 357043034767186914217277344587386743377558296292 -# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 -# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de -# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 -ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw -NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L -cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg -Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN -QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT -3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw -3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 -3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 -BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN -XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF -AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw -8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG -nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP -oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy -d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg -LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA 2" -# Serial: 44979900017204383099463764357512596969 -# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 -# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 -# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 ------BEGIN CERTIFICATE----- -MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB -gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu -QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG -A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz -OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ -VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 -b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA -DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn -0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB -OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE -fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E -Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m -o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i -sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW -OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez -Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS -adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n -3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ -F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf -CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 -XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm -djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ -WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb -AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq -P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko -b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj -XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P -5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi -DrW5viSP ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce -# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 -# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 ------BEGIN CERTIFICATE----- -MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix -DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k -IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT -N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v -dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG -A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh -ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx -QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA -4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 -AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 -4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C -ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV -9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD -gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 -Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq -NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko -LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc -Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd -ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I -XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI -M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot -9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V -Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea -j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh -X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ -l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf -bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 -pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK -e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 -vm9qp/UsQu0yrbYhnr68 ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef -# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 -# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 ------BEGIN CERTIFICATE----- -MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN -BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl -bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv -b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ -BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj -YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 -MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 -dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg -QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa -jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi -C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep -lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof -TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X1 O=Internet Security Research Group -# Subject: CN=ISRG Root X1 O=Internet Security Research Group -# Label: "ISRG Root X1" -# Serial: 172886928669790476064670243504169061120 -# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e -# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 -# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- - -# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Label: "AC RAIZ FNMT-RCM" -# Serial: 485876308206448804701554682760554759 -# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d -# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 -# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx -CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ -WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ -BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG -Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ -yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf -BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz -WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF -tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z -374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC -IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL -mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 -wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS -MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 -ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet -UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H -YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 -LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD -nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 -RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM -LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf -77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N -JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm -fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp -6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp -1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B -9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok -RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv -uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 1 O=Amazon -# Subject: CN=Amazon Root CA 1 O=Amazon -# Label: "Amazon Root CA 1" -# Serial: 143266978916655856878034712317230054538369994 -# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 -# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 -# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 2 O=Amazon -# Subject: CN=Amazon Root CA 2 O=Amazon -# Label: "Amazon Root CA 2" -# Serial: 143266982885963551818349160658925006970653239 -# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 -# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a -# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 3 O=Amazon -# Subject: CN=Amazon Root CA 3 O=Amazon -# Label: "Amazon Root CA 3" -# Serial: 143266986699090766294700635381230934788665930 -# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 -# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e -# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl -ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr -ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr -BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM -YyRIHN8wfdVoOw== ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 4 O=Amazon -# Subject: CN=Amazon Root CA 4 O=Amazon -# Label: "Amazon Root CA 4" -# Serial: 143266989758080763974105200630763877849284878 -# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd -# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be -# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi -9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk -M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB -MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw -CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW -1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- - -# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" -# Serial: 1 -# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 -# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca -# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 ------BEGIN CERTIFICATE----- -MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx -GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp -bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w -KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 -BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy -dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG -EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll -IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU -QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT -TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg -LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 -a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr -LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr -N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X -YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ -iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f -AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH -V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh -AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf -IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 -lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c -8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf -lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE----- - -# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Label: "GDCA TrustAUTH R5 ROOT" -# Serial: 9009899650740120186 -# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 -# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 -# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 ------BEGIN CERTIFICATE----- -MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE -BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ -IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 -MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV -BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w -HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj -Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj -TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u -KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj -qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm -MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 -ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP -zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk -L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC -jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA -HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC -AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg -p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm -DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 -COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry -L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf -JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg -IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io -2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV -09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ -XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq -T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe -MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Label: "SSL.com Root Certification Authority RSA" -# Serial: 8875640296558310041 -# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 -# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb -# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 ------BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE -BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK -DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz -OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R -xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX -qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC -C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 -6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh -/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF -YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E -JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc -US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 -ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm -+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi -M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G -A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV -cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc -Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs -PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ -q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 -cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr -a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I -H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y -K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu -nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf -oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY -Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com Root Certification Authority ECC" -# Serial: 8495723813297216424 -# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e -# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a -# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 ------BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz -WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 -b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS -b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI -7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg -CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud -EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD -VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T -kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ -gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority RSA R2" -# Serial: 6248227494352943350 -# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 -# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a -# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c ------BEGIN CERTIFICATE----- -MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV -BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE -CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy -MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G -A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD -DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq -M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf -OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa -4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 -HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR -aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA -b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ -Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV -PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO -pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu -UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY -MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV -HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 -9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW -s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 -Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg -cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM -79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz -/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt -ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm -Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK -QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ -w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi -S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 -mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority ECC" -# Serial: 3182246526754555285 -# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 -# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d -# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 ------BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx -NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv -bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA -VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku -WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX -5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ -ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg -h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Label: "GlobalSign Root CA - R6" -# Serial: 1417766617973444989252670301619537 -# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae -# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 -# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg -MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx -MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET -MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI -xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k -ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD -aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw -LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw -1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX -k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 -SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h -bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n -WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY -rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce -MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu -bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN -nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt -Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 -55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj -vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf -cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz -oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp -nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs -pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v -JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R -8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 -5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GC CA" -# Serial: 44084345621038548146064804565436152554 -# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 -# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 -# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d ------BEGIN CERTIFICATE----- -MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw -CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 -bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg -Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ -BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu -ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS -b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni -eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W -p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T -rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV -57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg -Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE----- - -# Issuer: CN=UCA Global G2 Root O=UniTrust -# Subject: CN=UCA Global G2 Root O=UniTrust -# Label: "UCA Global G2 Root" -# Serial: 124779693093741543919145257850076631279 -# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 -# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a -# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH -bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x -CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds -b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr -b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 -kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm -VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R -VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc -C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj -tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY -D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv -j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl -NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 -iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP -O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV -ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj -L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 -1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl -1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU -b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV -PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj -y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb -EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg -DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI -+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy -YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX -UB+K+wb1whnw0A== ------END CERTIFICATE----- - -# Issuer: CN=UCA Extended Validation Root O=UniTrust -# Subject: CN=UCA Extended Validation Root O=UniTrust -# Label: "UCA Extended Validation Root" -# Serial: 106100277556486529736699587978573607008 -# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 -# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a -# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF -eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx -MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV -BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog -D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS -sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop -O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk -sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi -c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj -VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz -KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ -TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G -sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs -1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD -fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN -l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR -ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ -VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 -c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp -4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s -t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj -2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO -vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C -xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx -cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM -fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax ------END CERTIFICATE----- - -# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Label: "Certigna Root CA" -# Serial: 269714418870597844693661054334862075617 -# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 -# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 -# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 ------BEGIN CERTIFICATE----- -MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw -WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw -MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x -MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD -VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX -BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO -ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M -CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu -I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm -TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh -C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf -ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz -IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT -Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k -JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 -hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB -GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of -1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov -L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo -dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr -aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq -hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L -6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG -HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 -0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB -lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi -o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 -gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v -faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 -Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh -jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw -3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign Root CA - G1" -# Serial: 235931866688319308814040 -# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac -# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c -# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 ------BEGIN CERTIFICATE----- -MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD -VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU -ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH -MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO -MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv -Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz -f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO -8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq -d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM -tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt -Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB -o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x -PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM -wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d -GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH -6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby -RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx -iN66zB+Afko= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign ECC Root CA - G3" -# Serial: 287880440101571086945156 -# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 -# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 -# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b ------BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG -EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo -bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g -RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ -TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s -b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw -djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 -WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS -fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB -zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB -CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD -+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Label: "emSign Root CA - C1" -# Serial: 825510296613316004955058 -# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 -# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 -# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f ------BEGIN CERTIFICATE----- -MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG -A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg -SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v -dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ -BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ -HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH -3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH -GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c -xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 -aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq -TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 -/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 -kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG -YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT -+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo -WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Label: "emSign ECC Root CA - C3" -# Serial: 582948710642506000014504 -# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 -# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 -# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 ------BEGIN CERTIFICATE----- -MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG -EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx -IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND -IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci -MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti -sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O -BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB -Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c -3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J -0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== ------END CERTIFICATE----- - -# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Label: "Hongkong Post Root CA 3" -# Serial: 46170865288971385588281144162979347873371282084 -# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 -# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 -# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 ------BEGIN CERTIFICATE----- -MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL -BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ -SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n -a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 -NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT -CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u -Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO -dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI -VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV -9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY -2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY -vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt -bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb -x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ -l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK -TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj -Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e -i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw -DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG -7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk -MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr -gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk -GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS -3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm -Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ -l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c -JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP -L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa -LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG -mpv0 ------END CERTIFICATE----- - -# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft ECC Root Certificate Authority 2017" -# Serial: 136839042543790627607696632466672567020 -# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 -# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 -# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 ------BEGIN CERTIFICATE----- -MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD -VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw -MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV -UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy -b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR -ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb -hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 -FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV -L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB -iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE----- - -# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft RSA Root Certificate Authority 2017" -# Serial: 40975477897264996090493496164228220339 -# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 -# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 -# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 ------BEGIN CERTIFICATE----- -MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl -MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw -NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG -EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N -aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ -Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 -ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 -HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm -gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ -jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc -aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG -YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 -W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K -UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH -+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q -W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC -LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC -gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 -tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh -SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 -TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 -pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR -xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp -GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 -dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN -AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB -RA+GsCyRxj3qrg+E ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Label: "e-Szigno Root CA 2017" -# Serial: 411379200276854331539784714 -# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 -# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 -# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 ------BEGIN CERTIFICATE----- -MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV -BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk -LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv -b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ -BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg -THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v -IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv -xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H -Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB -eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo -jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ -+efcMQ== ------END CERTIFICATE----- - -# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Label: "certSIGN Root CA G2" -# Serial: 313609486401300475190 -# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 -# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 -# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV -BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g -Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ -BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ -R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF -dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw -vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ -uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp -n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs -cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW -xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P -rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF -DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx -DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy -LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C -eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ -d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq -kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC -b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl -qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 -OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c -NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk -ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO -pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj -03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk -PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE -1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX -QRBdJ3NghVdJIgc= ------END CERTIFICATE----- - -# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Label: "NAVER Global Root Certification Authority" -# Serial: 9013692873798656336226253319739695165984492813 -# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b -# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 -# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 ------BEGIN CERTIFICATE----- -MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM -BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG -T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx -CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD -b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA -iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH -38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE -HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz -kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP -szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq -vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf -nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG -YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo -0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a -CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K -AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I -36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB -Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN -qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj -cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm -+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL -hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe -lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 -p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 -piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR -LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX -5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO -dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul -9XXeifdy ------END CERTIFICATE----- - -# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" -# Serial: 131542671362353147877283741781055151509 -# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb -# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a -# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb ------BEGIN CERTIFICATE----- -MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw -CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw -FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S -Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 -MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL -DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS -QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH -sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK -Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu -SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC -MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy -v+c= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Label: "GlobalSign Root R46" -# Serial: 1552617688466950547958867513931858518042577 -# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef -# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 -# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA -MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD -VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy -MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt -c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ -OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG -vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud -316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo -0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE -y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF -zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE -+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN -I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs -x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa -ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC -4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 -7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg -JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti -2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk -pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF -FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt -rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk -ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 -u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP -4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 -N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 -vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Label: "GlobalSign Root E46" -# Serial: 1552617690338932563915843282459653771421763 -# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f -# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 -# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 ------BEGIN CERTIFICATE----- -MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx -CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD -ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw -MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex -HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq -R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd -yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ -7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 -+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE----- - -# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Label: "ANF Secure Server Root CA" -# Serial: 996390341000653745 -# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 -# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 -# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV -BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk -YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV -BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN -MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF -UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD -VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v -dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj -cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q -yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH -2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX -H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL -zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR -p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz -W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ -SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn -LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 -n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B -u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj -o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC -AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L -9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej -rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK -pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 -vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq -OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ -/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 -2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI -+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 -MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo -tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE----- - -# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum EC-384 CA" -# Serial: 160250656287871593594747141429395092468 -# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 -# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed -# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 ------BEGIN CERTIFICATE----- -MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw -CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw -JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT -EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 -WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT -LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX -BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE -KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm -Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 -EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J -UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn -nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Root CA" -# Serial: 40870380103424195783807378461123655149 -# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 -# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 -# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd ------BEGIN CERTIFICATE----- -MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 -MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu -MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV -BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw -MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg -U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo -b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ -n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q -p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq -NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF -8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 -HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa -mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi -7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF -ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P -qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ -v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 -Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 -vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD -ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 -WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo -zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR -5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ -GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf -5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq -0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D -P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM -qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP -0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf -E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE----- - -# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Label: "TunTrust Root CA" -# Serial: 108534058042236574382096126452369648152337120275 -# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 -# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb -# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL -BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg -Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv -b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG -EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u -IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ -n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd -2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF -VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ -GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF -li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU -r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 -eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb -MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg -jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB -7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW -5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE -ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 -90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z -xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu -QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 -FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH -22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP -xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn -dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 -Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b -nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ -CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH -u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj -d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS RSA Root CA 2021" -# Serial: 76817823531813593706434026085292783742 -# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 -# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d -# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs -MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg -Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL -MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl -YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv -b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l -mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE -4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv -a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M -pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw -Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b -LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY -AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB -AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq -E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr -W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ -CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU -X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 -f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja -H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP -JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P -zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt -jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 -/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT -BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 -aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW -xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU -63ZTGI0RmLo= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS ECC Root CA 2021" -# Serial: 137515985548005187474074462014555733966 -# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 -# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 -# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 ------BEGIN CERTIFICATE----- -MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw -CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh -cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v -dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG -A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj -aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg -Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 -KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y -STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD -AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw -SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN -nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE----- - -# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" -# Serial: 1977337328857672817 -# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 -# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe -# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE -BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h -cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 -MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg -Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 -thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM -cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG -L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i -NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h -X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b -m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy -Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja -EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T -KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF -6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh -OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc -tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd -IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j -b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC -AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw -ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m -iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF -Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ -hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P -Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE -EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV -1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t -CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR -5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw -f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 -ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK -GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE----- - -# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus ECC Root CA" -# Serial: 630369271402956006249506845124680065938238527194 -# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 -# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 -# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 ------BEGIN CERTIFICATE----- -MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw -RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY -BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz -MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u -LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 -v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd -e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw -V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA -AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG -GJTO ------END CERTIFICATE----- - -# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus Root CA" -# Serial: 387574501246983434957692974888460947164905180485 -# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc -# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 -# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 ------BEGIN CERTIFICATE----- -MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL -BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x -FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx -MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s -THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc -IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU -AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ -GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 -8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH -flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt -J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim -0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN -pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ -UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW -OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB -AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet -8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd -nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j -bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM -Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv -TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS -S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr -I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 -b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB -UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P -Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven -sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X2 O=Internet Security Research Group -# Subject: CN=ISRG Root X2 O=Internet Security Research Group -# Label: "ISRG Root X2" -# Serial: 87493402998870891108772069816698636114 -# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 -# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af -# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 ------BEGIN CERTIFICATE----- -MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw -CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg -R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 -MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT -ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw -EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW -+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 -ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI -zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW -tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 -/q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE----- - -# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Label: "HiPKI Root CA - G1" -# Serial: 60966262342023497858655262305426234976 -# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 -# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 -# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc ------BEGIN CERTIFICATE----- -MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP -MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 -ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa -Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 -YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw -qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv -Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 -lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz -Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ -KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK -FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj -HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr -y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ -/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM -a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 -fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG -SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi -7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc -SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza -ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc -XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg -iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho -L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF -Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr -kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ -vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU -YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Label: "GlobalSign ECC Root CA - R4" -# Serial: 159662223612894884239637590694 -# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc -# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 -# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 ------BEGIN CERTIFICATE----- -MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD -VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw -MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g -UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT -BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx -uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV -HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ -+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 -bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R1 O=Google Trust Services LLC -# Subject: CN=GTS Root R1 O=Google Trust Services LLC -# Label: "GTS Root R1" -# Serial: 159662320309726417404178440727 -# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 -# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a -# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw -MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp -Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo -27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w -Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw -TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl -qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH -szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 -Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk -MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 -wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p -aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN -VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID -AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb -C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe -QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy -h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 -7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J -ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef -MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ -Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT -6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ -0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm -2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb -bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R3 O=Google Trust Services LLC -# Subject: CN=GTS Root R3 O=Google Trust Services LLC -# Label: "GTS Root R3" -# Serial: 159662495401136852707857743206 -# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 -# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 -# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G -jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 -4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 -VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm -ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R4 O=Google Trust Services LLC -# Subject: CN=GTS Root R4 O=Google Trust Services LLC -# Label: "GTS Root R4" -# Serial: 159662532700760215368942768210 -# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 -# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 -# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi -QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR -HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D -9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 -p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD ------END CERTIFICATE----- - -# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj -# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj -# Label: "Telia Root CA v2" -# Serial: 7288924052977061235122729490515358 -# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 -# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd -# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx -CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE -AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 -NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ -MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq -AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 -vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 -lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD -n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT -7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o -6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC -TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 -WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R -DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI -pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj -YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy -rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ -8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi -0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM -A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS -SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K -TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF -6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er -3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt -Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT -VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW -ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA -rBPuUBQemMc= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 1 2020" -# Serial: 165870826978392376648679885835942448534 -# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed -# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 -# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 -NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS -zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 -QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ -VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW -wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV -dWNbFJWcHwHP2NVypw87 ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 1 2020" -# Serial: 126288379621884218666039612629459926992 -# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e -# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 -# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 -NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC -/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD -wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 -OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA -y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb -gfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS ECC P384 Root G5" -# Serial: 13129116028163249804115411775095713523 -# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed -# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee -# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 ------BEGIN CERTIFICATE----- -MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp -Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 -MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ -bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS -7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp -0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS -B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 -BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ -LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 -DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS RSA4096 Root G5" -# Serial: 11930366277458970227240571539258396554 -# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 -# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 -# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN -MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT -HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN -NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs -IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ -ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 -2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp -wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM -pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD -nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po -sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx -Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd -Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX -KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe -XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL -tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv -TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN -AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw -GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H -PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF -O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ -REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik -AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv -/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ -p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw -MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF -qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK -ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root R1 O=Certainly -# Subject: CN=Certainly Root R1 O=Certainly -# Label: "Certainly Root R1" -# Serial: 188833316161142517227353805653483829216 -# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 -# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af -# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw -PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy -dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 -YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 -1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT -vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed -aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 -1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 -r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 -cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ -wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ -6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA -2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH -Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR -eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB -/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u -d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr -PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d -8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi -1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd -rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di -taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 -lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj -yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn -Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy -yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n -wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 -OV+KmalBWQewLK8= ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root E1 O=Certainly -# Subject: CN=Certainly Root E1 O=Certainly -# Label: "Certainly Root E1" -# Serial: 8168531406727139161245376702891150584 -# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 -# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b -# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 ------BEGIN CERTIFICATE----- -MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw -CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu -bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ -BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s -eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK -+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 -QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 -hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm -ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG -BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE----- - -# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Label: "Security Communication ECC RootCA1" -# Serial: 15446673492073852651 -# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 -# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 -# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 ------BEGIN CERTIFICATE----- -MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT -AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD -VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx -NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT -HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 -IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl -dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK -ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu -9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O -be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA1" -# Serial: 113562791157148395269083148143378328608 -# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 -# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a -# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU -MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI -T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz -MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF -SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh -bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z -xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ -spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 -58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR -at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll -5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq -nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK -V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ -pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO -z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn -jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ -WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF -7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 -YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli -awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u -+2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 -X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN -SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo -P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI -+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz -znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 -eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 -YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy -r/6zcCwupvI= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA2" -# Serial: 58605626836079930195615843123109055211 -# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c -# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 -# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 ------BEGIN CERTIFICATE----- -MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw -CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ -VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy -MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ -TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS -b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B -IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ -+kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK -sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA -94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B -43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root E46" -# Serial: 88989738453351742415770396670917916916 -# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 -# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a -# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 ------BEGIN CERTIFICATE----- -MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw -CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T -ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN -MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG -A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT -ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC -WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ -6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B -Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa -qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q -4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root R46" -# Serial: 156256931880233212765902055439220583700 -# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 -# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 -# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 ------BEGIN CERTIFICATE----- -MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf -MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD -Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw -HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY -MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp -YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa -ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz -SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf -iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X -ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 -IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS -VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE -SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu -+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt -8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L -HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt -zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P -AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c -mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ -YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 -gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA -Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB -JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX -DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui -TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 -dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 -LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp -0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY -QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS RSA Root CA 2022" -# Serial: 148535279242832292258835760425842727825 -# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da -# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca -# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed ------BEGIN CERTIFICATE----- -MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO -MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD -DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX -DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw -b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP -L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY -t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins -S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 -PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO -L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 -R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w -dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS -+YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS -d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG -AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f -gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z -NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt -hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM -QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf -R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ -DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW -P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy -lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq -bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w -AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q -r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji -Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU -98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS ECC Root CA 2022" -# Serial: 26605119622390491762507526719404364228 -# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 -# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 -# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 ------BEGIN CERTIFICATE----- -MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT -U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 -MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh -dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm -acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN -SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME -GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW -uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp -15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN -b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA ECC TLS 2021" -# Serial: 81873346711060652204712539181482831616 -# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 -# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd -# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 ------BEGIN CERTIFICATE----- -MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w -LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w -CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 -MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF -Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI -zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X -tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 -AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 -KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD -aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu -CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo -9H1/IISpQuQo ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA RSA TLS 2021" -# Serial: 111436099570196163832749341232207667876 -# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 -# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 -# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f ------BEGIN CERTIFICATE----- -MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM -MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx -MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 -MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD -QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z -4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv -Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ -kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs -GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln -nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh -3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD -0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy -geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 -ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB -c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI -pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU -dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB -DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS -4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs -o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ -qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw -xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM -rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 -AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR -0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY -o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 -dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE -oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G3" -# Serial: 576386314500428537169965010905813481816650257167 -# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 -# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 -# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 ------BEGIN CERTIFICATE----- -MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM -BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe -Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw -IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU -cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS -T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK -AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 -nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep -qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA -yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs -hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX -zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv -kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT -f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA -uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB -o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih -MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E -BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 -wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 -XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 -JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j -ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV -VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx -xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on -AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d -7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj -gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV -+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo -FGWsJwt0ivKH ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G4" -# Serial: 451799571007117016466790293371524403291602933463 -# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb -# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a -# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c ------BEGIN CERTIFICATE----- -MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw -WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y -MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD -VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz -dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx -s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw -LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij -YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD -pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE -AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR -UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj -/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS ECC Root 2020" -# Serial: 72082518505882327255703894282316633856 -# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd -# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec -# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 ------BEGIN CERTIFICATE----- -MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw -CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH -bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw -MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx -JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE -AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O -tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP -f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA -MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di -z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn -27iQ7t0l ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS RSA Root 2023" -# Serial: 44676229530606711399881795178081572759 -# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 -# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 -# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj -MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 -eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy -MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC -REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG -A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 -cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV -cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA -U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 -Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug -BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy -8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J -co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg -8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 -rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 -mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg -+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX -gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 -p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ -pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm -9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw -M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd -GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ -CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t -xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ -w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK -L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj -X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q -ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm -dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= ------END CERTIFICATE----- - -# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA CYBER Root CA" -# Serial: 85076849864375384482682434040119489222 -# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 -# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 -# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ -MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 -IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 -WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO -LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg -Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P -40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF -avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ -34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i -JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu -j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf -Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP -2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA -S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA -oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC -kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW -5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd -BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB -AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t -tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn -68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn -TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t -RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx -f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI -Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz -8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 -NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX -xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 -t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA14" -# Serial: 575790784512929437950770173562378038616896959179 -# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 -# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f -# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM -BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u -LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw -NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD -eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS -b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ -FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg -vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy -6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo -/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J -kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ -0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib -y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac -18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs -0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB -SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL -ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk -86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E -rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib -ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT -zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS -DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 -2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo -FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy -K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 -dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl -Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB -365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c -JRNItX+S ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA15" -# Serial: 126083514594751269499665114766174399806381178503 -# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 -# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d -# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a ------BEGIN CERTIFICATE----- -MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw -UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM -dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy -NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl -cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 -IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 -wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR -ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT -9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp -4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 -bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 2 2023" -# Serial: 153168538924886464690566649552453098598 -# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 -# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 -# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw -OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr -i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE -gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 -k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT -Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl -2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U -cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP -/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS -uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ -0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N -DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ -XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 -GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI -FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n -riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR -VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc -LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn -4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD -hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG -koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 -ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS -Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 -knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ -hJ65bvspmZDogNOfJA== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS ECC Root CA" -# Serial: 310892014698942880364840003424242768478804666567 -# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c -# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 -# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 ------BEGIN CERTIFICATE----- -MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw -WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw -NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE -ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB -c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ -AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp -guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw -DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 -L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR -OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS RSA Root CA" -# Serial: 160405846464868906657516898462547310235378010780 -# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 -# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa -# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 ------BEGIN CERTIFICATE----- -MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM -BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN -MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG -A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 -c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC -AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ -NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ -Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 -HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 -ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb -xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX -i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ -UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j -TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT -bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 -S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT -MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 -Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 -iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt -7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp -2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ -g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj -pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M -pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP -XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe -SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 -ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy -323imttUQ/hHWKNddBWcwauwxzQ= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 2 2023" -# Serial: 139766439402180512324132425437959641711 -# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 -# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b -# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw -OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK -F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE -7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe -EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 -lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb -RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV -jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc -jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx -TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ -ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk -hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF -NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH -kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 -QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 -pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q -3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU -t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX -cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 -ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT -2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs -7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP -gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst -Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh -XBxvWHZks/wCuPWdCg== ------END CERTIFICATE----- - -# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Label: "SwissSign RSA TLS Root CA 2022 - 1" -# Serial: 388078645722908516278762308316089881486363258315 -# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 -# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce -# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 ------BEGIN CERTIFICATE----- -MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE -AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx -MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT -d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg -MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX -vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 -LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX -5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE -EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt -/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x -0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 -KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM -0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd -OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta -clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK -wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 -DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL -BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 -10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz -Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ -iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc -gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM -ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF -LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp -zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td -Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 -rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO -gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Label: "OISTE Server Root ECC G1" -# Serial: 47819833811561661340092227008453318557 -# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 -# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 -# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 ------BEGIN CERTIFICATE----- -MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw -CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY -T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy -NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp -b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy -cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N -2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 -TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C -tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR -QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD -YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Label: "OISTE Server Root RSA G1" -# Serial: 113845518112613905024960613408179309848 -# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 -# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e -# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL -MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE -AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 -MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k -YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM -vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b -rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk -ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z -O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R -tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS -jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh -sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho -mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu -+zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR -i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT -kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU -8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 -zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 -I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG -5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 -qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP -AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk -gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs -YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 -9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome -/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 -J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 -wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy -BiElxky8j3C7DOReIoMt0r7+hVu05L0= ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Label: "e-Szigno TLS Root CA 2023" -# Serial: 71934828665710877219916191754 -# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 -# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe -# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 ------BEGIN CERTIFICATE----- -MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU -TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow -dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy -b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T -emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE -AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS -AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v -SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K -ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI -zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt -y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl -C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 -uWWL ------END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py deleted file mode 100644 index 1c9661cc7c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/core.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -certifi.py -~~~~~~~~~~ - -This module returns the installation location of cacert.pem or its contents. -""" -import sys -import atexit - -def exit_cacert_ctx() -> None: - _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] - - -if sys.version_info >= (3, 11): - - from importlib.resources import as_file, files - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the file - # in cases where we're inside of a zipimport situation until someone - # actually calls where(), but we don't want to re-extract the file - # on every call of where(), so we'll do it once then store it in a - # global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you to - # manage the cleanup of this file, so it doesn't actually return a - # path, it returns a context manager that will give you the path - # when you enter it and will do any cleanup when you leave it. In - # the common case of not needing a temporary file, it will just - # return the file system location and the __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") - -else: - - from importlib.resources import path as get_path, read_text - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the - # file in cases where we're inside of a zipimport situation until - # someone actually calls where(), but we don't want to re-extract - # the file on every call of where(), so we'll do it once then store - # it in a global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you - # to manage the cleanup of this file, so it doesn't actually - # return a path, it returns a context manager that will give - # you the path when you enter it and will do any cleanup when - # you leave it. In the common case of not needing a temporary - # file, it will just return the file system location and the - # __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = get_path("certifi", "cacert.pem") - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/certifi/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py deleted file mode 100644 index 8ce8d739c9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from sentry_sdk import metrics, profiler - -from sentry_sdk.scope import Scope # isort: skip -from sentry_sdk.client import Client # isort: skip -from sentry_sdk.consts import VERSION -from sentry_sdk.transport import HttpTransport, Transport - -from sentry_sdk.api import * # noqa # isort: skip - -__all__ = [ # noqa - "Hub", - "Scope", - "Client", - "Transport", - "HttpTransport", - "VERSION", - "integrations", - # From sentry_sdk.api - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "logger", - "metrics", - "profiler", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - -# Initialize the debug support after everything is loaded -from sentry_sdk.debug import init_debug_support - -init_debug_support() -del init_debug_support - -# circular imports -from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py deleted file mode 100644 index 565fac2a2d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_batcher.py +++ /dev/null @@ -1,184 +0,0 @@ -import os -import random -import threading -import weakref -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Generic, TypeVar - -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - -T = TypeVar("T") - - -class Batcher(Generic[T]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "" - CONTENT_TYPE = "" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - self._buffer: "list[T]" = [] - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._flush_event: "threading.Event" = threading.Event() - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._buffer = [] - self._running = True - self._lock = threading.Lock() - self._active = threading.local() - self._flush_event = threading.Event() - self._flusher = None - self._flusher_pid = None - - def _ensure_thread(self) -> bool: - """For forking processes we might need to restart this thread. - This ensures that our process actually has that thread running. - """ - if not self._running: - return False - - pid = os.getpid() - if self._flusher_pid == pid: - return True - - with self._lock: - # Recheck to make sure another thread didn't get here and start the - # the flusher in the meantime - if self._flusher_pid == pid: - return True - - self._flusher_pid = pid - - self._flusher = threading.Thread(target=self._flush_loop) - self._flusher.daemon = True - - try: - self._flusher.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return False - - return True - - def _flush_loop(self) -> None: - # Mark the flush-loop thread as active for its entire lifetime so - # that any re-entrant add() triggered by GC warnings during wait(), - # flush(), or Event operations is silently dropped instead of - # deadlocking on internal locks. - self._active.flag = True - while self._running: - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) - self._flush_event.clear() - self._flush() - - def add(self, item: "T") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - if len(self._buffer) >= self.MAX_BEFORE_DROP: - self._record_lost(item) - return None - - self._buffer.append(item) - if len(self._buffer) >= self.MAX_BEFORE_FLUSH: - self._flush_event.set() - finally: - self._active.flag = False - - def kill(self) -> None: - if self._flusher is None: - return - - self._running = False - self._flush_event.set() - self._flusher = None - - def flush(self) -> None: - was_active = getattr(self._active, "flag", False) - self._active.flag = True - try: - self._flush() - finally: - self._active.flag = was_active - - def _add_to_envelope(self, envelope: "Envelope") -> None: - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": len(self._buffer), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(item) for item in self._buffer - ], - } - ), - ) - ) - - def _flush(self) -> "Optional[Envelope]": - envelope = Envelope( - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} - ) - with self._lock: - if len(self._buffer) == 0: - return None - - self._add_to_envelope(envelope) - self._buffer.clear() - - self._capture_func(envelope) - return envelope - - def _record_lost(self, item: "T") -> None: - pass - - @staticmethod - def _to_transport_format(item: "T") -> "Any": - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py deleted file mode 100644 index f62175c09f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_compat.py +++ /dev/null @@ -1,92 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, TypeVar - - T = TypeVar("T") - - -PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 -PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 -PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 -PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 - - -def with_metaclass(meta: "Any", *bases: "Any") -> "Any": - class MetaClass(type): - def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": - return meta(name, bases, d) - - return type.__new__(MetaClass, "temporary_class", (), {}) - - -def check_uwsgi_thread_support() -> bool: - # We check two things here: - # - # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if - # that's the case. - # - # 2. Additionally, if uWSGI is running in preforking mode (default), it needs - # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This - # is because any background threads spawned before the main process is - # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if - # --enable-threads is on. One has to explicitly provide - # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython - # after-fork hooks that take care of cleaning up stale thread data. - try: - from uwsgi import opt # type: ignore - except ImportError: - return True - - from sentry_sdk.consts import FALSE_VALUES - - def enabled(option: str) -> bool: - value = opt.get(option, False) - if isinstance(value, bool): - return value - - if isinstance(value, bytes): - try: - value = value.decode() - except Exception: - pass - - return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] - - # When `threads` is passed in as a uwsgi option, - # `enable-threads` is implied on. - threads_enabled = "threads" in opt or enabled("enable-threads") - fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") - lazy_mode = enabled("lazy-apps") or enabled("lazy") - - if lazy_mode and not threads_enabled: - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI without thread support. " - "This might lead to unexpected issues. " - 'Please run uWSGI with "--enable-threads" for full support.' - ) - ) - - return False - - elif not lazy_mode and (not threads_enabled or not fork_hooks_on): - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI in preforking mode without " - "thread support. This might lead to crashing workers. " - 'Please run uWSGI with both "--enable-threads" and ' - '"--py-call-uwsgi-fork-hooks" for full support.' - ) - ) - - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py deleted file mode 100644 index 923fcf6df8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_init_implementation.py +++ /dev/null @@ -1,78 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -import sentry_sdk - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional - - import sentry_sdk.consts - - -class _InitGuard: - _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( - "Using the return value of sentry_sdk.init as a context manager " - "and manually calling the __enter__ and __exit__ methods on the " - "return value are deprecated. We are no longer maintaining this " - "functionality, and we will remove it in the next major release." - ) - - def __init__(self, client: "sentry_sdk.Client") -> None: - self._client = client - - def __enter__(self) -> "_InitGuard": - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - c = self._client - if c is not None: - c.close() - - -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass - - -def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": - """Initializes the SDK and optionally integrations. - - This takes the same arguments as the client constructor. - """ - client = sentry_sdk.Client(*args, **kwargs) - sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() - rv = _InitGuard(client) - return rv - - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `init` is a type to - # have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `ContextManager[Any]` to tell static analyzers about the return type. - - class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 - pass - -else: - # Alias `init` for actual usage. Go through the lambda indirection to throw - # PyCharm off of the weakly typed signature (it would otherwise discover - # both the weakly typed signature of `_init` and our faked `init` type). - - init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py deleted file mode 100644 index 0719932ee9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_log_batcher.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Item, PayloadRef -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Log - - -class LogBatcher(Batcher["Log"]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "log" - CONTENT_TYPE = "application/vnd.sentry.items.log+json" - - @staticmethod - def _to_transport_format(item: "Log") -> "Any": - if "sentry.severity_number" not in item["attributes"]: - item["attributes"]["sentry.severity_number"] = item["severity_number"] - if "sentry.severity_text" not in item["attributes"]: - item["attributes"]["sentry.severity_text"] = item["severity_text"] - - res = { - "timestamp": int(item["time_unix_nano"]) / 1.0e9, - "level": str(item["severity_text"]), - "body": str(item["body"]), - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - return res - - def _record_lost(self, item: "Log") -> None: - # Construct log envelope item without sending it to report lost bytes - log_item = Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": 1, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [self._to_transport_format(item)], - } - ), - ) - - self._record_lost_func( - reason="queue_overflow", - data_category="log_item", - item=log_item, - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py deleted file mode 100644 index 16c238bcab..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_lru_cache.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -_SENTINEL = object() - - -class LRUCache: - def __init__(self, max_size: int) -> None: - if max_size <= 0: - raise AssertionError(f"invalid max_size: {max_size}") - self.max_size = max_size - self._data: "dict[Any, Any]" = {} - self.hits = self.misses = 0 - self.full = False - - def set(self, key: "Any", value: "Any") -> None: - current = self._data.pop(key, _SENTINEL) - if current is not _SENTINEL: - self._data[key] = value - elif self.full: - self._data.pop(next(iter(self._data))) - self._data[key] = value - else: - self._data[key] = value - self.full = len(self._data) >= self.max_size - - def get(self, key: "Any", default: "Any" = None) -> "Any": - try: - ret = self._data.pop(key) - except KeyError: - self.misses += 1 - ret = default - else: - self.hits += 1 - self._data[key] = ret - - return ret - - def get_all(self) -> "list[tuple[Any, Any]]": - return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py deleted file mode 100644 index 06bce1a282..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_metrics_batcher.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Metric - - -class MetricsBatcher(Batcher["Metric"]): - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 10_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "trace_metric" - CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" - - @staticmethod - def _to_transport_format(item: "Metric") -> "Any": - res = { - "timestamp": item["timestamp"], - "name": item["name"], - "type": item["type"], - "value": item["value"], - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - if item.get("unit") is not None: - res["unit"] = item["unit"] - - return res - - def _record_lost(self, item: "Metric") -> None: - self._record_lost_func( - reason="queue_overflow", - data_category="trace_metric", - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py deleted file mode 100644 index c28c8de9ac..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_queue.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') -with Lock swapped out for RLock to avoid a deadlock while garbage collecting. - -https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py - - -See also -https://codewithoutrules.com/2017/08/16/concurrency-python/ -https://bugs.python.org/issue14976 -https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 - -We also vendor the code to evade eventlet's broken monkeypatching, see -https://github.com/getsentry/sentry-python/pull/484 - - -Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; - -All Rights Reserved - - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - -""" - -import threading -from collections import deque -from time import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - -__all__ = ["EmptyError", "FullError", "Queue"] - - -class EmptyError(Exception): - "Exception raised by Queue.get(block=0)/get_nowait()." - - pass - - -class FullError(Exception): - "Exception raised by Queue.put(block=0)/put_nowait()." - - pass - - -class Queue: - """Create a queue object with a given maximum size. - - If maxsize is <= 0, the queue size is infinite. - """ - - def __init__(self, maxsize=0): - self.maxsize = maxsize - self._init(maxsize) - - # mutex must be held whenever the queue is mutating. All methods - # that acquire mutex must release it before returning. mutex - # is shared between the three conditions, so acquiring and - # releasing the conditions also acquires and releases mutex. - self.mutex = threading.RLock() - - # Notify not_empty whenever an item is added to the queue; a - # thread waiting to get is notified then. - self.not_empty = threading.Condition(self.mutex) - - # Notify not_full whenever an item is removed from the queue; - # a thread waiting to put is notified then. - self.not_full = threading.Condition(self.mutex) - - # Notify all_tasks_done whenever the number of unfinished tasks - # drops to zero; thread waiting to join() is notified to resume - self.all_tasks_done = threading.Condition(self.mutex) - self.unfinished_tasks = 0 - - def task_done(self): - """Indicate that a formerly enqueued task is complete. - - Used by Queue consumer threads. For each get() used to fetch a task, - a subsequent call to task_done() tells the queue that the processing - on the task is complete. - - If a join() is currently blocking, it will resume when all items - have been processed (meaning that a task_done() call was received - for every item that had been put() into the queue). - - Raises a ValueError if called more times than there were items - placed in the queue. - """ - with self.all_tasks_done: - unfinished = self.unfinished_tasks - 1 - if unfinished <= 0: - if unfinished < 0: - raise ValueError("task_done() called too many times") - self.all_tasks_done.notify_all() - self.unfinished_tasks = unfinished - - def join(self): - """Blocks until all items in the Queue have been gotten and processed. - - The count of unfinished tasks goes up whenever an item is added to the - queue. The count goes down whenever a consumer thread calls task_done() - to indicate the item was retrieved and all work on it is complete. - - When the count of unfinished tasks drops to zero, join() unblocks. - """ - with self.all_tasks_done: - while self.unfinished_tasks: - self.all_tasks_done.wait() - - def qsize(self): - """Return the approximate size of the queue (not reliable!).""" - with self.mutex: - return self._qsize() - - def empty(self): - """Return True if the queue is empty, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() == 0 - as a direct substitute, but be aware that either approach risks a race - condition where a queue can grow before the result of empty() or - qsize() can be used. - - To create code that needs to wait for all queued tasks to be - completed, the preferred technique is to use the join() method. - """ - with self.mutex: - return not self._qsize() - - def full(self): - """Return True if the queue is full, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() >= n - as a direct substitute, but be aware that either approach risks a race - condition where a queue can shrink before the result of full() or - qsize() can be used. - """ - with self.mutex: - return 0 < self.maxsize <= self._qsize() - - def put(self, item, block=True, timeout=None): - """Put an item into the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until a free slot is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the FullError exception if no free slot was available within that time. - Otherwise ('block' is false), put an item on the queue if a free slot - is immediately available, else raise the FullError exception ('timeout' - is ignored in that case). - """ - with self.not_full: - if self.maxsize > 0: - if not block: - if self._qsize() >= self.maxsize: - raise FullError() - elif timeout is None: - while self._qsize() >= self.maxsize: - self.not_full.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while self._qsize() >= self.maxsize: - remaining = endtime - time() - if remaining <= 0.0: - raise FullError() - self.not_full.wait(remaining) - self._put(item) - self.unfinished_tasks += 1 - self.not_empty.notify() - - def get(self, block=True, timeout=None): - """Remove and return an item from the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until an item is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the EmptyError exception if no item was available within that time. - Otherwise ('block' is false), return an item if one is immediately - available, else raise the EmptyError exception ('timeout' is ignored - in that case). - """ - with self.not_empty: - if not block: - if not self._qsize(): - raise EmptyError() - elif timeout is None: - while not self._qsize(): - self.not_empty.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while not self._qsize(): - remaining = endtime - time() - if remaining <= 0.0: - raise EmptyError() - self.not_empty.wait(remaining) - item = self._get() - self.not_full.notify() - return item - - def put_nowait(self, item): - """Put an item into the queue without blocking. - - Only enqueue the item if a free slot is immediately available. - Otherwise raise the FullError exception. - """ - return self.put(item, block=False) - - def get_nowait(self): - """Remove and return an item from the queue without blocking. - - Only get an item if one is immediately available. Otherwise - raise the EmptyError exception. - """ - return self.get(block=False) - - # Override these methods to implement other queue organizations - # (e.g. stack or priority queue). - # These will only be called with appropriate locks held - - # Initialize the queue representation - def _init(self, maxsize): - self.queue: "Any" = deque() - - def _qsize(self): - return len(self.queue) - - # Put a new item in the queue - def _put(self, item): - self.queue.append(item) - - # Get an item from the queue - def _get(self): - return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py deleted file mode 100644 index 79285c3386..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_span_batcher.py +++ /dev/null @@ -1,234 +0,0 @@ -import os -import random -import threading -import time -import weakref -from collections import defaultdict -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp, serialize_attribute - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - from sentry_sdk._types import SpanJSON - - -class SpanBatcher(Batcher["SpanJSON"]): - # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is - # a bit of a buffer for spans that appear between the trigger to flush - # and actually flushing the buffer. - # - # The max limits are all per trace (per bucket). - MAX_ENVELOPE_SIZE = 1000 # spans - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 2000 - MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB - - FLUSH_WAIT_TIME = 5.0 - - TYPE = "span" - CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - # Spans from different traces cannot be emitted in the same envelope - # since the envelope contains a shared trace header. That's why we bucket - # by trace_id, so that we can then send the buckets each in its own - # envelope. - # trace_id -> span buffer - self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) - self._running_size: dict[str, int] = defaultdict(lambda: 0) - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._last_full_flush: float = time.monotonic() # drives time-based flushes - self._flush_event = threading.Event() - self._pending_flush: set[str] = set() # buckets to be flushed - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._span_buffer = defaultdict(list) - self._running_size = defaultdict(lambda: 0) - self._running = True - - self._lock = threading.Lock() - self._active = threading.local() - - self._last_full_flush = time.monotonic() - self._flush_event = threading.Event() - self._pending_flush = set() - - self._flusher = None - self._flusher_pid = None - - def _flush_loop(self) -> None: - self._active.flag = True - while self._running: - jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 - self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) - self._flush_event.clear() - - self._flush(only_pending=True) - - if ( - time.monotonic() - self._last_full_flush - >= self.FLUSH_WAIT_TIME + jitter - ): - self._flush() - self._last_full_flush = time.monotonic() - - def add(self, span: "SpanJSON") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - size = len(self._span_buffer[span["trace_id"]]) - if size >= self.MAX_BEFORE_DROP: - self._record_lost_func( - reason="queue_overflow", - data_category="span", - quantity=1, - ) - return None - - self._span_buffer[span["trace_id"]].append(span) - self._running_size[span["trace_id"]] += self._estimate_size(span) - - if ( - size + 1 >= self.MAX_BEFORE_FLUSH - or self._running_size[span["trace_id"]] - >= self.MAX_BYTES_BEFORE_FLUSH - ): - self._pending_flush.add(span["trace_id"]) - notify = True - else: - notify = False - - if notify: - self._flush_event.set() - finally: - self._active.flag = False - - @staticmethod - def _estimate_size(item: "SpanJSON") -> int: - # Rough estimate of serialized span size that's quick to compute. - # 210 is the rough size of the payload without attributes, and then we - # estimate the attributes separately. - estimate = 210 - for value in (item.get("attributes") or {}).values(): - estimate += 50 - - if isinstance(value, str): - estimate += len(value) - else: - estimate += len(str(value)) - - return estimate - - @staticmethod - def _to_transport_format(item: "SpanJSON") -> "Any": - res = {k: v for k, v in item.items() if k not in ("_segment_span",)} - - if item.get("attributes"): - res["attributes"] = { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - } - else: - del res["attributes"] - - return res - - def _flush(self, only_pending: bool = False) -> None: - with self._lock: - if only_pending: - buckets = list(self._pending_flush) - else: - # flush whole buffer, e.g. if the SDK is shutting down - buckets = list(self._span_buffer.keys()) - - self._pending_flush.clear() - - if not buckets: - return - - envelopes = [] - - for bucket_id in buckets: - spans = self._span_buffer.get(bucket_id) - if not spans: - continue - - dsc = spans[0]["_segment_span"]._dynamic_sampling_context() - - # Max per envelope is 1000, so if we happen to have more than - # 1000 spans in one bucket, we'll need to separate them. - for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): - end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) - - envelope = Envelope( - headers={ - "sent_at": format_timestamp(datetime.now(timezone.utc)), - "trace": dsc, - } - ) - - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": end - start, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(spans[j]) - for j in range(start, end) - ], - } - ), - ) - ) - - envelopes.append(envelope) - - del self._span_buffer[bucket_id] - del self._running_size[bucket_id] - - for envelope in envelopes: - self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py deleted file mode 100644 index fbd2578048..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_types.py +++ /dev/null @@ -1,481 +0,0 @@ -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING, TypeVar, Union - -# Re-exported for compat, since code out there in the wild might use this variable. -MYPY = TYPE_CHECKING - - -SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" -BLOB_DATA_SUBSTITUTE = "[Blob substitute]" -OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" -UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" - - -class AnnotatedValue: - """ - Meta information for a data field in the event payload. - This is to tell Relay that we have tampered with the fields value. - See: - https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 - """ - - __slots__ = ("value", "metadata") - - def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: - self.value = value - self.metadata = metadata - - def __eq__(self, other: "Any") -> bool: - if not isinstance(other, AnnotatedValue): - return False - - return self.value == other.value and self.metadata == other.metadata - - def __str__(self: "AnnotatedValue") -> str: - return str({"value": str(self.value), "metadata": str(self.metadata)}) - - def __len__(self: "AnnotatedValue") -> int: - if self.value is not None: - return len(self.value) - else: - return 0 - - @classmethod - def removed_because_raw_data(cls) -> "AnnotatedValue": - """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" - # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition - # to span-first - return AnnotatedValue( - value="", - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_raw_data(cls) -> "AnnotatedValue": - """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" - return AnnotatedValue( - value=UNPARSABLE_RAW_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": - """ - The actual value was removed because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition - # to span-first - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_over_size_limit( - cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE - ) -> "AnnotatedValue": - """ - The actual value was replaced because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": - """The actual value was removed because it contained sensitive information.""" - return AnnotatedValue( - value=SENSITIVE_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) - "s", # The fields original value was substituted - ] - ] - }, - ) - - -T = TypeVar("T") -Annotated = Union[AnnotatedValue, T] - - -if TYPE_CHECKING: - from collections.abc import Container, MutableMapping, Sequence - from datetime import datetime - from types import TracebackType - from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - - class SDKInfo(TypedDict): - name: str - version: str - packages: "Sequence[Mapping[str, str]]" - - class KeyValueCollectionBehaviour(TypedDict): - mode: 'Literal["off", "denylist", "allowlist"]' - 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): - request: "KeyValueCollectionBehaviour" - - class HttpHeadersCollectionBehaviour(TypedDict): - request: "KeyValueCollectionBehaviour" - - class DataCollectionUserOptions(TypedDict, total=False): - user_info: bool - cookies: "KeyValueCollectionBehaviour" - http_headers: "HttpHeadersCollectionUserOptions" - http_bodies: "List[str]" - query_params: "KeyValueCollectionBehaviour" - graphql: "GraphQLCollectionUserOptions" - gen_ai: "GenAICollectionUserOptions" - database_query_data: bool - queues: bool - 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_query_data: bool - queues: bool - 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"] - - DurationUnit = Literal[ - "nanosecond", - "microsecond", - "millisecond", - "second", - "minute", - "hour", - "day", - "week", - ] - - InformationUnit = Literal[ - "bit", - "byte", - "kilobyte", - "kibibyte", - "megabyte", - "mebibyte", - "gigabyte", - "gibibyte", - "terabyte", - "tebibyte", - "petabyte", - "pebibyte", - "exabyte", - "exbibyte", - ] - - FractionUnit = Literal["ratio", "percent"] - MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] - - MeasurementValue = TypedDict( - "MeasurementValue", - { - "value": float, - "unit": NotRequired[Optional[MeasurementUnit]], - }, - ) - - Event = TypedDict( - "Event", - { - "breadcrumbs": Annotated[ - dict[Literal["values"], list[dict[str, Any]]] - ], # TODO: We can expand on this type - "check_in_id": str, - "contexts": dict[str, dict[str, object]], - "dist": str, - "duration": Optional[float], - "environment": Optional[str], - "errors": list[dict[str, Any]], # TODO: We can expand on this type - "event_id": str, - "exception": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "extra": MutableMapping[str, object], - "fingerprint": list[str], - "level": LogLevelStr, - "logentry": Mapping[str, object], - "logger": str, - "measurements": dict[str, MeasurementValue], - "message": str, - "modules": dict[str, str], - "monitor_config": Mapping[str, object], - "monitor_slug": Optional[str], - "platform": Literal["python"], - "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports - "release": Optional[str], - "request": dict[str, object], - "sdk": Mapping[str, object], - "server_name": str, - "spans": Annotated[list[dict[str, object]]], - "stacktrace": dict[ - str, object - ], # We access this key in the code, but I am unsure whether we ever set it - "start_timestamp": datetime, - "status": Optional[str], - "tags": MutableMapping[ - str, str - ], # Tags must be less than 200 characters each - "threads": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "timestamp": Optional[datetime], # Must be set before sending the event - "transaction": str, - "transaction_info": Mapping[str, Any], # TODO: We can expand on this type - "type": Literal["check_in", "transaction"], - "user": dict[str, object], - "_dropped_spans": int, - "_has_gen_ai_span": bool, - }, - total=False, - ) - - ExcInfo = Union[ - tuple[Type[BaseException], BaseException, Optional[TracebackType]], - tuple[None, None, None], - ] - - # TODO: Make a proper type definition for this (PRs welcome!) - Hint = Dict[str, Any] - - AttributeValue = ( - str - | bool - | float - | int - | list[str] - | list[bool] - | list[float] - | list[int] - | tuple[str, ...] - | tuple[bool, ...] - | tuple[float, ...] - | tuple[int, ...] - ) - Attributes = dict[str, AttributeValue] - - SerializedAttributeValue = TypedDict( - # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types - "SerializedAttributeValue", - { - "type": Literal[ - "string", - "boolean", - "double", - "integer", - "array", - ], - "value": AttributeValue, - }, - ) - - Log = TypedDict( - "Log", - { - "severity_text": str, - "severity_number": int, - "body": str, - "attributes": Attributes, - "time_unix_nano": int, - "trace_id": Optional[str], - "span_id": Optional[str], - }, - ) - - MetricType = Literal["counter", "gauge", "distribution"] - MetricUnit = Union[DurationUnit, InformationUnit, str] - - Metric = TypedDict( - "Metric", - { - "timestamp": float, - "trace_id": Optional[str], - "span_id": Optional[str], - "name": str, - "type": MetricType, - "value": float, - "unit": Optional[MetricUnit], - "attributes": Attributes, - }, - ) - - MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] - - SpanJSON = TypedDict( - "SpanJSON", - { - "trace_id": str, - "span_id": str, - "parent_span_id": NotRequired[str], - "name": str, - "status": str, - "is_segment": bool, - "start_timestamp": float, - "end_timestamp": NotRequired[float], - "attributes": NotRequired[Attributes], - "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], - }, - ) - - # TODO: Make a proper type definition for this (PRs welcome!) - Breadcrumb = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - BreadcrumbHint = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - SamplingContext = Dict[str, Any] - - EventProcessor = Callable[[Event, Hint], Optional[Event]] - ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] - BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] - TransactionProcessor = Callable[[Event, Hint], Optional[Event]] - LogProcessor = Callable[[Log, Hint], Optional[Log]] - - TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] - - # https://github.com/python/mypy/issues/5710 - NotImplementedType = Any - - EventDataCategory = Literal[ - "default", - "error", - "crash", - "transaction", - "security", - "attachment", - "session", - "internal", - "profile", - "profile_chunk", - "monitor", - "span", - "log_item", - "log_byte", - "trace_metric", - ] - SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] - - ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] - ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] - - MonitorConfigScheduleType = Literal["crontab", "interval"] - MonitorConfigScheduleUnit = Literal[ - "year", - "month", - "week", - "day", - "hour", - "minute", - "second", # not supported in Sentry and will result in a warning - ] - - MonitorConfigSchedule = TypedDict( - "MonitorConfigSchedule", - { - "type": MonitorConfigScheduleType, - "value": Union[int, str], - "unit": MonitorConfigScheduleUnit, - }, - total=False, - ) - - MonitorConfig = TypedDict( - "MonitorConfig", - { - "schedule": MonitorConfigSchedule, - "timezone": str, - "checkin_margin": int, - "max_runtime": int, - "failure_issue_threshold": int, - "recovery_threshold": int, - "owner": str, - }, - total=False, - ) - - HttpStatusCodeRange = Union[int, Container[int]] - - class TextPart(TypedDict): - type: Literal["text"] - content: str - - IgnoreSpansName = Union[str, Pattern[str]] - IgnoreSpansContext = TypedDict( - "IgnoreSpansContext", - {"name": IgnoreSpansName, "attributes": Attributes}, - total=False, - ) - IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py deleted file mode 100644 index 98f932267f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/_werkzeug.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Copyright (c) 2007 by the Pallets team. - -Some rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, Iterator, Optional, Tuple - - -# -# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` -# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 -# -# We need this function because Django does not give us a "pure" http header -# dict. So we might as well use it for all WSGI integrations. -# -def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns only proper HTTP headers. - """ - for key, value in environ.items(): - key = str(key) - if key.startswith("HTTP_") and key not in ( - "HTTP_CONTENT_TYPE", - "HTTP_CONTENT_LENGTH", - ): - yield key[5:].replace("_", "-").title(), value - elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): - yield key.replace("_", "-").title(), value - - -def _strip_default_port(host: str, scheme: "Optional[str]") -> str: - """Strip the port from the host if it's the default for the scheme.""" - if scheme == "http" and host.endswith(":80"): - return host[:-3] - if scheme == "https" and host.endswith(":443"): - return host[:-4] - return host - - -# `get_host` comes from `werkzeug.wsgi.get_host` -# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 - - -def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: - """ - Return the host for the given WSGI environment. - """ - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: - return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) - elif environ.get("HTTP_HOST"): - return _strip_default_port(environ["HTTP_HOST"], scheme) - elif environ.get("SERVER_NAME"): - # SERVER_NAME/SERVER_PORT describe the internal server, so use - # wsgi.url_scheme (not the forwarded scheme) for port decisions. - rv = environ["SERVER_NAME"] - if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( - ("https", "443"), - ("http", "80"), - ): - rv += ":" + environ["SERVER_PORT"] - return rv - else: - # In spite of the WSGI spec, SERVER_NAME might not be present. - return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py deleted file mode 100644 index 404e57ff1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .utils import ( - GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 - GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 - normalize_message_role, # noqa: F401 - normalize_message_roles, # noqa: F401 - set_conversation_id, # noqa: F401 - set_data_normalized, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py deleted file mode 100644 index b5eb8c55ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_completions_api.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections.abc import Iterable -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.chat import ( - ChatCompletionContentPartParam, - ChatCompletionMessageParam, - ChatCompletionSystemMessageParam, - ) - - from sentry_sdk._types import TextPart - - -def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: - return isinstance(message, dict) and message.get("role") == "system" - - -def _get_system_instructions( - messages: "Iterable[ChatCompletionMessageParam]", -) -> "list[ChatCompletionMessageParam]": - if not isinstance(messages, Iterable): - return [] - - return [message for message in messages if _is_system_instruction(message)] - - -def _get_text_items( - content: "Union[str, Iterable[ChatCompletionContentPartParam]]", -) -> "list[str]": - if isinstance(content, str): - return [content] - - if not isinstance(content, Iterable): - return [] - - text_items = [] - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - text = part.get("text", None) - if text is not None: - text_items.append(text) - - return text_items - - -def _transform_system_instructions( - system_instructions: "list[ChatCompletionSystemMessageParam]", -) -> "list[TextPart]": - instruction_text_parts: "list[TextPart]" = [] - - for instruction in system_instructions: - if not isinstance(instruction, dict): - continue - - content = instruction.get("content") - if content is None: - continue - - text_parts: "list[TextPart]" = [ - {"type": "text", "content": text} for text in _get_text_items(content) - ] - instruction_text_parts += text_parts - - return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py deleted file mode 100644 index 8f751c3248..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/_openai_responses_api.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.responses import ResponseInputItemParam, ResponseInputParam - - -def _is_system_instruction(message: "ResponseInputItemParam") -> bool: - if not isinstance(message, dict) or not message.get("role") == "system": - return False - - return "type" not in message or message["type"] == "message" - - -def _get_system_instructions( - messages: "Union[str, ResponseInputParam]", -) -> "list[ResponseInputItemParam]": - if not isinstance(messages, list): - return [] - - return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py deleted file mode 100644 index 35ee4bd788..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/consts.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - -# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." -DATA_URL_BASE64_REGEX = re.compile( - r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py deleted file mode 100644 index d8840ad451..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/monitoring.py +++ /dev/null @@ -1,147 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk.utils -from sentry_sdk import start_span -from sentry_sdk.ai.utils import _set_span_data_attribute -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, TypeVar, Union - - F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) - -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) - - -def set_ai_pipeline_name(name: "Optional[str]") -> None: - _ai_pipeline_name.set(name) - - -def get_ai_pipeline_name() -> "Optional[str]": - return _ai_pipeline_name.get() - - -def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": - def decorator(f: "F") -> "F": - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return await f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = await f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - if inspect.iscoroutinefunction(f): - return wraps(f)(async_wrapped) # type: ignore - else: - return wraps(f)(sync_wrapped) # type: ignore - - return decorator - - -def record_token_usage( - span: "Union[Span, StreamedSpan]", - input_tokens: "Optional[int]" = None, - input_tokens_cached: "Optional[int]" = None, - input_tokens_cache_write: "Optional[int]" = None, - output_tokens: "Optional[int]" = None, - output_tokens_reasoning: "Optional[int]" = None, - total_tokens: "Optional[int]" = None, -) -> None: - # TODO: move pipeline name elsewhere - ai_pipeline_name = get_ai_pipeline_name() - if ai_pipeline_name: - _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) - - if input_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if input_tokens_cached is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - input_tokens_cached, - ) - - if input_tokens_cache_write is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - input_tokens_cache_write, - ) - - if output_tokens is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens - ) - - if output_tokens_reasoning is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - output_tokens_reasoning, - ) - - if total_tokens is None and input_tokens is not None and output_tokens is not None: - total_tokens = input_tokens + output_tokens - - if total_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py deleted file mode 100644 index 7b1ca9324b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/ai/utils.py +++ /dev/null @@ -1,782 +0,0 @@ -import inspect -import json -from copy import deepcopy -from typing import TYPE_CHECKING - -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Tuple, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import logger - -MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB -# Maximum characters when only a single message is left after bytes truncation -MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 - - -class GEN_AI_ALLOWED_MESSAGE_ROLES: - SYSTEM = "system" - USER = "user" - ASSISTANT = "assistant" - TOOL = "tool" - - -GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { - GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], - GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], - GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], - GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], -} - -GEN_AI_MESSAGE_ROLE_MAPPING = {} -for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): - for source_role in source_roles: - GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role - - -def parse_data_uri(url: str) -> "Tuple[str, str]": - """ - Parse a data URI and return (mime_type, content). - - Data URI format (RFC 2397): data:[][;base64], - - Examples: - data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") - data:text/plain,Hello → ("text/plain", "Hello") - data:;base64,SGVsbG8= → ("", "SGVsbG8=") - - Raises: - ValueError: If the URL is not a valid data URI (missing comma separator) - """ - if "," not in url: - raise ValueError("Invalid data URI: missing comma separator") - - header, content = url.split(",", 1) - - # Extract mime type from header - # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" - # Remove "data:" prefix, then take everything before the first semicolon - if header.startswith("data:"): - mime_part = header[5:] # Remove "data:" prefix - else: - mime_part = header - - mime_type = mime_part.split(";")[0] - - return mime_type, content - - -def get_modality_from_mime_type(mime_type: str) -> str: - """ - Infer the content modality from a MIME type string. - - Args: - mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") - - Returns: - One of: "image", "audio", "video", or "document" - Defaults to "image" for unknown or empty MIME types. - - Examples: - "image/jpeg" -> "image" - "audio/mp3" -> "audio" - "video/mp4" -> "video" - "application/pdf" -> "document" - "text/plain" -> "document" - """ - if not mime_type: - return "image" # Default fallback - - mime_lower = mime_type.lower() - if mime_lower.startswith("image/"): - return "image" - elif mime_lower.startswith("audio/"): - return "audio" - elif mime_lower.startswith("video/"): - return "video" - elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): - return "document" - else: - return "image" # Default fallback for unknown types - - -def transform_openai_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an OpenAI/LiteLLM content part to Sentry's standardized format. - - This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. - - Input format: - - {"type": "image_url", "image_url": {"url": "..."}} - - {"type": "image_url", "image_url": "..."} (string shorthand) - - Output format (one of): - - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from OpenAI/LiteLLM - - Returns: - A transformed dictionary in standardized format, or None if the format - is not OpenAI image_url format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type != "image_url": - return None - - image_url_data = content_part.get("image_url") - if isinstance(image_url_data, str): - url = image_url_data - elif isinstance(image_url_data, dict): - url = image_url_data.get("url", "") - else: - return None - - if not url: - return None - - # Check if it's a data URI (base64 encoded) - if url.startswith("data:"): - try: - mime_type, content = parse_data_uri(url) - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": content, - } - except ValueError: - # If parsing fails, return as URI - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - else: - # Regular URL - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - - -def transform_anthropic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an Anthropic content part to Sentry's standardized format. - - This handles the Anthropic image and document formats with source dictionaries. - - Input format: - - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} - - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} - - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} - - {"type": "document", "source": {...}} (same source formats) - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from Anthropic - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Anthropic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "document") or "source" not in content_part: - return None - - source = content_part.get("source") - if not isinstance(source, dict): - return None - - source_type = source.get("type") - media_type = source.get("media_type", "") - modality = ( - "document" - if block_type == "document" - else get_modality_from_mime_type(media_type) - ) - - if source_type == "base64": - return { - "type": "blob", - "modality": modality, - "mime_type": media_type, - "content": source.get("data", ""), - } - elif source_type == "url": - return { - "type": "uri", - "modality": modality, - "mime_type": media_type, - "uri": source.get("url", ""), - } - elif source_type == "file": - return { - "type": "file", - "modality": modality, - "mime_type": media_type, - "file_id": source.get("file_id", ""), - } - - return None - - -def transform_google_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a Google GenAI content part to Sentry's standardized format. - - This handles the Google GenAI inline_data and file_data formats. - - Input format: - - {"inline_data": {"mime_type": "...", "data": "..."}} - - {"file_data": {"mime_type": "...", "file_uri": "..."}} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from Google GenAI - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Google format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Handle Google inline_data format - if "inline_data" in content_part: - inline_data = content_part.get("inline_data") - if isinstance(inline_data, dict): - mime_type = inline_data.get("mime_type", "") - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": inline_data.get("data", ""), - } - return None - - # Handle Google file_data format - if "file_data" in content_part: - file_data = content_part.get("file_data") - if isinstance(file_data, dict): - mime_type = file_data.get("mime_type", "") - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_data.get("file_uri", ""), - } - return None - - return None - - -def transform_generic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a generic/LangChain-style content part to Sentry's standardized format. - - This handles generic formats where the type indicates the modality and - the data is provided via direct base64, url, or file_id fields. - - Input format: - - {"type": "image", "base64": "...", "mime_type": "..."} - - {"type": "audio", "url": "...", "mime_type": "..."} - - {"type": "video", "base64": "...", "mime_type": "..."} - - {"type": "file", "file_id": "...", "mime_type": "..."} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part in generic format - - Returns: - A transformed dictionary in standardized format, or None if the format - is not generic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "audio", "video", "file"): - return None - - # Ensure it's not Anthropic format (which also uses type: "image") - if "source" in content_part: - return None - - mime_type = content_part.get("mime_type", "") - modality = block_type if block_type != "file" else "document" - - # Check for base64 encoded content - if "base64" in content_part: - return { - "type": "blob", - "modality": modality, - "mime_type": mime_type, - "content": content_part.get("base64", ""), - } - # Check for URL reference - elif "url" in content_part: - return { - "type": "uri", - "modality": modality, - "mime_type": mime_type, - "uri": content_part.get("url", ""), - } - # Check for file_id reference - elif "file_id" in content_part: - return { - "type": "file", - "modality": modality, - "mime_type": mime_type, - "file_id": content_part.get("file_id", ""), - } - - return None - - -def transform_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a content part from various AI SDK formats to Sentry's standardized format. - - This is a heuristic dispatcher that detects the format and delegates to the - appropriate SDK-specific transformer. For direct SDK integration, prefer using - the specific transformers directly: - - transform_openai_content_part() for OpenAI/LiteLLM - - transform_anthropic_content_part() for Anthropic - - transform_google_content_part() for Google GenAI - - transform_generic_content_part() for LangChain and other generic formats - - Detection order: - 1. OpenAI: type == "image_url" - 2. Google: "inline_data" or "file_data" keys present - 3. Anthropic: type in ("image", "document") with "source" key - 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from an AI SDK - - Returns: - A transformed dictionary in standardized format, or None if the format - is unrecognized or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Try OpenAI format first (most common, clear indicator) - result = transform_openai_content_part(content_part) - if result is not None: - return result - - # Try Google format (unique keys make it easy to detect) - result = transform_google_content_part(content_part) - if result is not None: - return result - - # Try Anthropic format (has "source" key) - result = transform_anthropic_content_part(content_part) - if result is not None: - return result - - # Try generic format as fallback - result = transform_generic_content_part(content_part) - if result is not None: - return result - - # Unrecognized format - return None - - -def transform_message_content(content: "Any") -> "Any": - """ - Transform message content, handling both string content and list of content blocks. - - For list content, each item is transformed using transform_content_part(). - Items that cannot be transformed (return None) are kept as-is. - - Args: - content: Message content - can be a string, list of content blocks, or other - - Returns: - - String content: returned as-is - - List content: list with each transformable item converted to standardized format - - Other: returned as-is - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - return transformed - - return content - - -def _normalize_data(data: "Any", unpack: bool = True) -> "Any": - # convert pydantic data (e.g. OpenAI v1+) to json compatible format - if hasattr(data, "model_dump"): - # Check if it's a class (type) rather than an instance - # Model classes can be passed as arguments (e.g., for schema definitions) - if inspect.isclass(data): - return f"" - - try: - return _normalize_data(data.model_dump(), unpack=unpack) - except Exception as e: - logger.warning("Could not convert pydantic data to JSON: %s", e) - return data if isinstance(data, (int, float, bool, str)) else str(data) - - if isinstance(data, list): - if unpack and len(data) == 1: - return _normalize_data(data[0], unpack=unpack) # remove empty dimensions - return list(_normalize_data(x, unpack=unpack) for x in data) - - if isinstance(data, dict): - return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} - - return data if isinstance(data, (int, float, bool, str)) else str(data) - - -def set_data_normalized( - span: "Union[Span, StreamedSpan]", - key: str, - value: "Any", - unpack: bool = True, -) -> None: - normalized = _normalize_data(value, unpack=unpack) - if isinstance(normalized, (int, float, bool, str)): - _set_span_data_attribute(span, key, normalized) - else: - _set_span_data_attribute(span, key, json.dumps(normalized)) - - -def _set_span_data_attribute( - span: "Union[Span, StreamedSpan]", key: str, value: "Any" -) -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(key, value) - else: - span.set_data(key, value) - - -def normalize_message_role(role: str) -> str: - """ - Normalize a message role to one of the 4 allowed gen_ai role values. - Maps "ai" -> "assistant" and keeps other standard roles unchanged. - """ - return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) - - -def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": - """ - Normalize roles in a list of messages to use standard gen_ai role values. - Creates a deep copy to avoid modifying the original messages. - """ - normalized_messages = [] - for message in messages: - if not isinstance(message, dict): - normalized_messages.append(message) - continue - normalized_message = message.copy() - if "role" in message: - normalized_message["role"] = normalize_message_role(message["role"]) - normalized_messages.append(normalized_message) - - return normalized_messages - - -def get_start_span_function() -> "Callable[..., Any]": - current_span = sentry_sdk.get_current_span() - - transaction_exists = ( - current_span is not None and current_span.containing_transaction is not None - ) - return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction - - -def _truncate_single_message_content_if_present( - message: "Dict[str, Any]", max_chars: int -) -> "Dict[str, Any]": - """ - Truncate a message's content to at most `max_chars` characters and append an - ellipsis if truncation occurs. - """ - if not isinstance(message, dict) or "content" not in message: - return message - content = message["content"] - - if isinstance(content, str): - if len(content) <= max_chars: - return message - message["content"] = content[:max_chars] + "..." - return message - - if isinstance(content, list): - remaining = max_chars - for item in content: - if isinstance(item, dict) and "text" in item: - text = item["text"] - if isinstance(text, str): - if len(text) > remaining: - item["text"] = text[:remaining] + "..." - remaining = 0 - else: - remaining -= len(text) - return message - - return message - - -def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: - """ - Find the index of the first message that would exceed the max bytes limit. - Compute the individual message sizes, and return the index of the first message from the back - of the list that would exceed the max bytes limit. - """ - running_sum = 0 - for idx in range(len(messages) - 1, -1, -1): - size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) - running_sum += size - if running_sum > max_bytes: - return idx + 1 - - return 0 - - -def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: - """ - Some content blocks contain an image_url property with base64 content as its value. - This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. - """ - if item.get("type") != "image_url": - return False - - image_url_val = item.get("image_url") - image_url = ( - image_url_val.get("url", "") - if isinstance(image_url_val, dict) - else (image_url_val or "") - ) - data_url_match = DATA_URL_BASE64_REGEX.match(image_url) - - return bool(data_url_match) - - -def redact_blob_message_parts( - messages: "List[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Redact blob message parts from the messages by replacing blob content with "[Filtered]". - - This function creates a deep copy of messages that contain blob content to avoid - mutating the original message dictionaries. Messages without blob content are - returned as-is to minimize copying overhead. - - e.g: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "data:image/jpeg;base64,..." - } - ] - } - becomes: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "[Filtered]" - } - ] - } - """ - - # First pass: check if any message contains blob content - has_blobs = False - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and ( - item.get("type") == "blob" or _is_image_type_with_blob_content(item) - ): - has_blobs = True - break - if has_blobs: - break - - # If no blobs found, return original messages to avoid unnecessary copying - if not has_blobs: - return messages - - # Deep copy messages to avoid mutating the original - messages_copy = deepcopy(messages) - - # Second pass: redact blob content in the copy - for message in messages_copy: - if not isinstance(message, dict): - continue - - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict): - if item.get("type") == "blob": - item["content"] = BLOB_DATA_SUBSTITUTE - elif _is_image_type_with_blob_content(item): - if isinstance(item["image_url"], dict): - item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE - else: - item["image_url"] = BLOB_DATA_SUBSTITUTE - - return messages_copy - - -def truncate_messages_by_size( - messages: "List[Dict[str, Any]]", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Tuple[List[Dict[str, Any]], int]": - """ - Returns a truncated messages list, consisting of - - the last message, with its content truncated to `max_single_message_chars` characters, - if the last message's size exceeds `max_bytes` bytes; otherwise, - - the maximum number of messages, starting from the end of the `messages` list, whose total - serialized size does not exceed `max_bytes` bytes. - - In the single message case, the serialized message size may exceed `max_bytes`, because - truncation is based only on character count in that case. - """ - serialized_json = json.dumps(messages, separators=(",", ":")) - current_size = len(serialized_json.encode("utf-8")) - - if current_size <= max_bytes: - return messages, 0 - - truncation_index = _find_truncation_index(messages, max_bytes) - if truncation_index < len(messages): - truncated_messages = messages[truncation_index:] - else: - truncation_index = len(messages) - 1 - truncated_messages = messages[-1:] - - if len(truncated_messages) == 1: - truncated_messages[0] = _truncate_single_message_content_if_present( - deepcopy(truncated_messages[0]), max_chars=max_single_message_chars - ) - - return truncated_messages, truncation_index - - -def truncate_and_annotate_messages( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_message = _truncate_single_message_content_if_present( - deepcopy(messages[-1]), max_chars=max_single_message_chars - ) - if len(messages) > 1: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return [truncated_message] - - -def truncate_and_annotate_embedding_inputs( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) - if removed_count > 0: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return truncated_messages - - -def set_conversation_id(conversation_id: str) -> None: - """ - Set the conversation_id in the scope. - """ - scope = sentry_sdk.get_current_scope() - scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py deleted file mode 100644 index 5556b11ace..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/api.py +++ /dev/null @@ -1,573 +0,0 @@ -import inspect -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import Client, tracing_utils -from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.crons import monitor -from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.traces import get_current_span as _get_current_streamed_span -from sentry_sdk.tracing import NoOpSpan, Transaction, trace - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Optional, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - MeasurementUnit, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span, TransactionKwargs - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) -else: - - def overload(x: "T") -> "T": - return x - - -# When changing this, update __all__ in __init__.py too -__all__ = [ - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - - -def scopemethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, - inspect.getdoc(getattr(Scope, f.__name__)), - ) - return f - - -def clientmethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, - inspect.getdoc(getattr(Client, f.__name__)), - ) - return f - - -@scopemethod -def get_client() -> "BaseClient": - return Scope.get_client() - - -def is_initialized() -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether Sentry has been initialized or not. - - If a client is available and the client is active - (meaning it is configured to send data) then - Sentry is initialized. - """ - return get_client().is_active() - - -@scopemethod -def get_global_scope() -> "Scope": - return Scope.get_global_scope() - - -@scopemethod -def get_isolation_scope() -> "Scope": - return Scope.get_isolation_scope() - - -@scopemethod -def get_current_scope() -> "Scope": - return Scope.get_current_scope() - - -@scopemethod -def last_event_id() -> "Optional[str]": - """ - See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding - this method's limitations. - """ - return Scope.last_event_id() - - -@scopemethod -def capture_event( - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) - - -@scopemethod -def capture_message( - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_message( - message, level, scope=scope, **scope_kwargs - ) - - -@scopemethod -def capture_exception( - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) - - -@scopemethod -def add_attachment( - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, -) -> None: - return get_isolation_scope().add_attachment( - bytes, filename, path, content_type, add_to_transactions - ) - - -@scopemethod -def add_breadcrumb( - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", -) -> None: - return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - -@overload -def configure_scope() -> "ContextManager[Scope]": - pass - - -@overload -def configure_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def configure_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - warnings.warn( - "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", - DeprecationWarning, - stacklevel=2, - ) - - scope = get_isolation_scope() - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - -@overload -def push_scope() -> "ContextManager[Scope]": - pass - - -@overload -def push_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def push_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - warnings.warn( - "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", - DeprecationWarning, - stacklevel=2, - ) - - if callback is not None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with push_scope() as scope: - callback(scope) - return None - - return _ScopeManager() - - -@scopemethod -def set_attribute(attribute: str, value: "Any") -> None: - """ - Set an attribute. - - Any attributes-based telemetry (logs, metrics) captured in this scope will - include this attribute. - """ - return get_isolation_scope().set_attribute(attribute, value) - - -@scopemethod -def remove_attribute(attribute: str) -> None: - """ - Remove an attribute. - - If the attribute doesn't exist, this function will not have any effect and - it will also not raise an exception. - """ - return get_isolation_scope().remove_attribute(attribute) - - -@scopemethod -def set_tag(key: str, value: "Any") -> None: - return get_isolation_scope().set_tag(key, value) - - -@scopemethod -def set_tags(tags: "Mapping[str, object]") -> None: - return get_isolation_scope().set_tags(tags) - - -@scopemethod -def set_context(key: str, value: "Dict[str, Any]") -> None: - return get_isolation_scope().set_context(key, value) - - -@scopemethod -def set_extra(key: str, value: "Any") -> None: - return get_isolation_scope().set_extra(key, value) - - -@scopemethod -def set_user(value: "Optional[Dict[str, Any]]") -> None: - return get_isolation_scope().set_user(value) - - -@scopemethod -def set_level(value: "LogLevelStr") -> None: - return get_isolation_scope().set_level(value) - - -@clientmethod -def flush( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return get_client().flush(timeout=timeout, callback=callback) - - -@clientmethod -async def flush_async( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return await get_client().flush_async(timeout=timeout, callback=callback) - - -@scopemethod -def start_span( - **kwargs: "Any", -) -> "Span": - return get_current_scope().start_span(**kwargs) - - -@scopemethod -def start_transaction( - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", -) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction on the current scope. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - return get_current_scope().start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - -def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - transaction = get_current_scope().transaction - if transaction is not None: - transaction.set_measurement(name, value, unit) - - -def get_current_span( - scope: "Optional[Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - return tracing_utils.get_current_span(scope) - - -def get_traceparent() -> "Optional[str]": - """ - Returns the traceparent either from the active span or from the scope. - """ - return get_current_scope().get_traceparent() - - -def get_baggage() -> "Optional[str]": - """ - Returns Baggage either from the active span or from the scope. - """ - baggage = get_current_scope().get_baggage() - if baggage is not None: - return baggage.serialize() - - return None - - -def continue_trace( - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", -) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers, op, name, source, origin - ) - - -@scopemethod -def start_session( - session_mode: str = "application", -) -> None: - return get_isolation_scope().start_session(session_mode=session_mode) - - -@scopemethod -def end_session() -> None: - return get_isolation_scope().end_session() - - -@scopemethod -def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: - return get_current_scope().set_transaction_name(name, source) - - -def update_current_span( - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, - data: "Optional[dict[str, Any]]" = None, -) -> None: - """ - Update the current active span with the provided parameters. - - This function allows you to modify properties of the currently active span. - If no span is currently active, this function will do nothing. - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, the span's operation will - remain unchanged. - :type op: str or None - - :param name: The human-readable name/description for the span. This provides - more specific details about what the span represents (e.g., "GET /api/users", - "SELECT * FROM users"). If not provided, the span's name will remain unchanged. - :type name: str or None - - :param data: A dictionary of key-value pairs to add as data to the span. This - data will be merged with any existing span data. If not provided, - no data will be added. - - .. deprecated:: 2.35.0 - Use ``attributes`` instead. The ``data`` parameter will be removed - in a future version. - :type data: dict[str, Union[str, int, float, bool]] or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes will be merged with any existing span data. If not provided, - no attributes will be added. - :type attributes: dict[str, Union[str, int, float, bool]] or None - - :returns: None - - .. versionadded:: 2.35.0 - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP - - sentry_sdk.update_current_span( - op=OP.FUNCTION, - name="process_user_data", - attributes={"user_id": 123, "batch_size": 50} - ) - """ - if isinstance(_get_current_streamed_span(), StreamedSpan): - warnings.warn( - "The `update_current_span` API isn't available in streaming mode. " - "Retrieve the current span with get_current_span() and use its API " - "directly.", - DeprecationWarning, - stacklevel=2, - ) - return - - current_span = get_current_span() - - if current_span is None: - return - - if op is not None: - current_span.op = op - - if name is not None: - # internally it is still description - current_span.description = name - - if data is not None and attributes is not None: - raise ValueError( - "Cannot provide both `data` and `attributes`. Please use only `attributes`." - ) - - if data is not None: - warnings.warn( - "The `data` parameter is deprecated. Please use `attributes` instead.", - DeprecationWarning, - stacklevel=2, - ) - attributes = data - - if attributes is not None: - current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py deleted file mode 100644 index 4d69d3acf2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/attachments.py +++ /dev/null @@ -1,71 +0,0 @@ -import mimetypes -import os -from typing import TYPE_CHECKING - -from sentry_sdk.envelope import Item, PayloadRef - -if TYPE_CHECKING: - from typing import Callable, Optional, Union - - -class Attachment: - """Additional files/data to send along with an event. - - This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. - config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with - all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are - captured within the ``Scope``. - - To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for - ``add_attachment`` are the same as the parameters for this class's constructor. - - :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless - ``path`` is provided. - :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. - :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. - :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` - parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. - :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. - """ - - def __init__( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - if bytes is None and path is None: - raise TypeError("path or raw bytes required for attachment") - if filename is None and path is not None: - filename = os.path.basename(path) - if filename is None: - raise TypeError("filename is required for attachment") - if content_type is None: - content_type = mimetypes.guess_type(filename)[0] - self.bytes = bytes - self.filename = filename - self.path = path - self.content_type = content_type - self.add_to_transactions = add_to_transactions - - def to_envelope_item(self) -> "Item": - """Returns an envelope item for this attachment.""" - payload: "Union[None, PayloadRef, bytes]" = None - if self.bytes is not None: - if callable(self.bytes): - payload = self.bytes() - else: - payload = self.bytes - else: - payload = PayloadRef(path=self.path) - return Item( - payload=payload, - type="attachment", - content_type=self.content_type, - filename=self.filename, - ) - - def __repr__(self) -> str: - return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py deleted file mode 100644 index 92cb42277e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/client.py +++ /dev/null @@ -1,1479 +0,0 @@ -import json -import os -import platform -import random -import socket -import sys -import uuid -import warnings -from collections.abc import Iterable, Mapping -from datetime import datetime, timezone -from importlib import import_module -from typing import TYPE_CHECKING, Dict, List, cast, overload - -from sentry_sdk._compat import check_uwsgi_thread_support -from sentry_sdk._metrics_batcher import MetricsBatcher -from sentry_sdk._span_batcher import SpanBatcher -from sentry_sdk.consts import ( - DEFAULT_MAX_VALUE_LENGTH, - DEFAULT_OPTIONS, - INSTRUMENTER, - SPANDATA, - SPANSTATUS, - VERSION, - ClientConstructor, -) -from sentry_sdk.data_collection import ( - _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 -from sentry_sdk.monitor import Monitor -from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler -from sentry_sdk.profiler.transaction_profiler import ( - Profile, - has_profiling_enabled, - setup_profiler, -) -from sentry_sdk.scrubber import EventScrubber -from sentry_sdk.serializer import serialize -from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import trace -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import ( - AsyncHttpTransport, - HttpTransportCore, - make_transport, -) -from sentry_sdk.utils import ( - AnnotatedValue, - ContextVar, - capture_internal_exceptions, - current_stacktrace, - datetime_from_isoformat, - env_to_bool, - format_timestamp, - get_before_send_log, - get_before_send_metric, - get_before_send_span, - get_default_release, - get_sdk_name, - get_type_name, - handle_in_app, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union - - from sentry_sdk._log_batcher import LogBatcher - from sentry_sdk._metrics_batcher import MetricsBatcher - from sentry_sdk._types import ( - Event, - EventDataCategory, - Hint, - Log, - Metric, - SDKInfo, - SerializedAttributeValue, - ) - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.session import Session - from sentry_sdk.spotlight import SpotlightClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Item, Transport - from sentry_sdk.utils import Dsn - - I = TypeVar("I", bound=Integration) # noqa: E741 - -_client_init_debug = ContextVar("client_init_debug") - - -SDK_INFO: "SDKInfo" = { - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() - "version": VERSION, - "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], -} - - -def _serialized_v1_attribute_to_serialized_v2_attribute( - attribute_value: "Any", -) -> "Optional[SerializedAttributeValue]": - if isinstance(attribute_value, bool): - return { - "value": attribute_value, - "type": "boolean", - } - - if isinstance(attribute_value, int): - return { - "value": attribute_value, - "type": "integer", - } - - if isinstance(attribute_value, float): - return { - "value": attribute_value, - "type": "double", - } - - if isinstance(attribute_value, str): - return { - "value": attribute_value, - "type": "string", - } - - if isinstance(attribute_value, list): - if not attribute_value: - return {"value": [], "type": "array"} - - ty = type(attribute_value[0]) - if ty in (int, str, bool, float) and all( - type(v) is ty for v in attribute_value - ): - return { - "value": attribute_value, - "type": "array", - } - - # Types returned when the serializer for V1 span attributes recurses into some container types. - if isinstance(attribute_value, (dict, list)): - return { - "value": json.dumps(attribute_value), - "type": "string", - } - - return None - - -def _serialized_v1_span_to_serialized_v2_span( - span: "dict[str, Any]", event: "Event" -) -> "dict[str, Any]": - # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". - res: "dict[str, Any]" = { - "status": SpanStatus.OK.value, - "is_segment": False, - } - - if "trace_id" in span: - res["trace_id"] = span["trace_id"] - - if "span_id" in span: - res["span_id"] = span["span_id"] - - if "description" in span: - description = span["description"] - - if description is None and "op" in span: - description = span["op"] - - res["name"] = description - - if "start_timestamp" in span: - start_timestamp = None - try: - start_timestamp = datetime_from_isoformat(span["start_timestamp"]) - except Exception: - pass - - if start_timestamp is not None: - res["start_timestamp"] = start_timestamp.timestamp() - - if "timestamp" in span: - end_timestamp = None - try: - end_timestamp = datetime_from_isoformat(span["timestamp"]) - except Exception: - pass - - if end_timestamp is not None: - res["end_timestamp"] = end_timestamp.timestamp() - - if "parent_span_id" in span: - res["parent_span_id"] = span["parent_span_id"] - - if "status" in span and span["status"] != SPANSTATUS.OK: - res["status"] = "error" - - attributes: "Dict[str, Any]" = {} - - if "op" in span: - attributes["sentry.op"] = span["op"] - if "origin" in span: - attributes["sentry.origin"] = span["origin"] - - span_data = span.get("data") - if isinstance(span_data, dict): - attributes.update(span_data) - - span_tags = span.get("tags") - if isinstance(span_tags, dict): - attributes.update(span_tags) - - # See Scope._apply_user_attributes_to_telemetry() for user attributes. - user = event.get("user") - if isinstance(user, dict): - if "id" in user: - attributes["user.id"] = user["id"] - if "username" in user: - attributes["user.name"] = user["username"] - if "email" in user: - attributes["user.email"] = user["email"] - - # See Scope.set_global_attributes() for release, environment, and SDK metadata. - if "release" in event: - attributes["sentry.release"] = event["release"] - if "environment" in event: - attributes["sentry.environment"] = event["environment"] - if "server_name" in event: - attributes["server.address"] = event["server_name"] - if "transaction" in event: - attributes["sentry.segment.name"] = event["transaction"] - - trace_context = event.get("contexts", {}).get("trace", {}) - if "span_id" in trace_context: - attributes["sentry.segment.id"] = trace_context["span_id"] - - sdk_info = event.get("sdk") - if isinstance(sdk_info, dict): - if "name" in sdk_info: - attributes["sentry.sdk.name"] = sdk_info["name"] - if "version" in sdk_info: - attributes["sentry.sdk.version"] = sdk_info["version"] - - attributes["process.runtime.name"] = platform.python_implementation() - attributes["process.runtime.version"] = ( - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ) - - if not attributes: - return res - - res["attributes"] = {} - for key, value in attributes.items(): - converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) - if converted_value is None: - continue - - res["attributes"][key] = converted_value - - # Remove redundant attribute, as status is stored in the status field. - if "status" in res["attributes"]: - del res["attributes"]["status"] - - return res - - -def _split_gen_ai_spans( - event_opt: "Event", -) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": - if "spans" not in event_opt: - return None - - spans: "Any" = event_opt["spans"] - if isinstance(spans, AnnotatedValue): - spans = spans.value - - if not isinstance(spans, Iterable): - return None - - non_gen_ai_spans = [] - gen_ai_spans = [] - for span in spans: - if not isinstance(span, dict): - non_gen_ai_spans.append(span) - continue - - span_op = span.get("op") - if isinstance(span_op, str) and span_op.startswith("gen_ai."): - gen_ai_spans.append(span) - else: - non_gen_ai_spans.append(span) - - return non_gen_ai_spans, gen_ai_spans - - -def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": - if args and (isinstance(args[0], (bytes, str)) or args[0] is None): - dsn: "Optional[str]" = args[0] - args = args[1:] - else: - dsn = None - - if len(args) > 1: - raise TypeError("Only single positional argument is expected") - - rv = dict(DEFAULT_OPTIONS) - options = dict(*args, **kwargs) - if dsn is not None and options.get("dsn") is None: - options["dsn"] = dsn - - for key, value in options.items(): - if key not in rv: - raise TypeError("Unknown option %r" % (key,)) - - rv[key] = value - - if rv["dsn"] is None: - rv["dsn"] = os.environ.get("SENTRY_DSN") - - if rv["release"] is None: - rv["release"] = get_default_release() - - if rv["environment"] is None: - rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" - - if rv["debug"] is None: - rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False - - if rv["server_name"] is None and hasattr(socket, "gethostname"): - rv["server_name"] = socket.gethostname() - - if rv["instrumenter"] is None: - rv["instrumenter"] = INSTRUMENTER.SENTRY - - if rv["project_root"] is None: - try: - project_root = os.getcwd() - except Exception: - project_root = None - - rv["project_root"] = project_root - - 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"] - ) - - if rv["socket_options"] and not isinstance(rv["socket_options"], list): - logger.warning( - "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." - ) - rv["socket_options"] = None - - if rv["keep_alive"] is None: - rv["keep_alive"] = ( - env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False - ) - - if rv["enable_tracing"] is not None: - warnings.warn( - "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", - DeprecationWarning, - stacklevel=2, - ) - - if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): - warnings.warn( - "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", - stacklevel=2, - ) - - return rv - - -try: - # Python 3.6+ - module_not_found_error = ModuleNotFoundError -except Exception: - # Older Python versions - module_not_found_error = ImportError # type: ignore - - -class BaseClient: - """ - .. versionadded:: 2.0.0 - - The basic definition of a client that is used for sending data to Sentry. - """ - - spotlight: "Optional[SpotlightClient]" = None - - def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: - self.options: "Dict[str, Any]" = ( - options if options is not None else DEFAULT_OPTIONS - ) - - self.transport: "Optional[Transport]" = None - self.monitor: "Optional[Monitor]" = None - self.log_batcher: "Optional[LogBatcher]" = None - self.metrics_batcher: "Optional[MetricsBatcher]" = None - self.span_batcher: "Optional[SpanBatcher]" = None - self.integrations: "dict[str, Integration]" = {} - - def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": - return {"options": {}} - - def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: - pass - - @property - def dsn(self) -> "Optional[str]": - return None - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - return None - - def should_send_default_pii(self) -> bool: - return False - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return False - - def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - return None - - def _capture_log(self, log: "Log", scope: "Scope") -> None: - pass - - def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: - pass - - def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: - pass - - def capture_session(self, *args: "Any", **kwargs: "Any") -> None: - return None - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, name_or_class: "Union[str, type[Integration]]" - ) -> "Optional[Integration]": - return None - - def close(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def flush(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def close_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def __enter__(self) -> "BaseClient": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - return None - - -class NonRecordingClient(BaseClient): - """ - .. versionadded:: 2.0.0 - - A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. - """ - - pass - - -class _Client(BaseClient): - """ - The client is internally responsible for capturing the events and - forwarding them to sentry through the configured transport. It takes - the client options as keyword arguments and optionally the DSN as first - argument. - - Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) - """ - - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - super(_Client, self).__init__(options=get_options(*args, **kwargs)) - self._init_impl() - - def __getstate__(self) -> "Any": - return {"options": self.options} - - def __setstate__(self, state: "Any") -> None: - self.options = state["options"] - self._init_impl() - - def _setup_instrumentation( - self, functions_to_trace: "Sequence[Dict[str, str]]" - ) -> None: - """ - Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. - """ - for function in functions_to_trace: - class_name = None - function_qualname = function["qualified_name"] - - if "." not in function_qualname: - logger.warning( - "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", - function_qualname, - ) - continue - - module_name, function_name = function_qualname.rsplit(".", 1) - - try: - # Try to import module and function - # ex: "mymodule.submodule.funcname" - - module_obj = import_module(module_name) - function_obj = getattr(module_obj, function_name) - setattr(module_obj, function_name, trace(function_obj)) - logger.debug("Enabled tracing for %s", function_qualname) - except module_not_found_error: - try: - # Try to import a class - # ex: "mymodule.submodule.MyClassName.member_function" - - module_name, class_name = module_name.rsplit(".", 1) - module_obj = import_module(module_name) - class_obj = getattr(module_obj, class_name) - function_obj = getattr(class_obj, function_name) - function_type = type(class_obj.__dict__[function_name]) - traced_function = trace(function_obj) - - if function_type in (staticmethod, classmethod): - traced_function = staticmethod(traced_function) - - setattr(class_obj, function_name, traced_function) - setattr(module_obj, class_name, class_obj) - logger.debug("Enabled tracing for %s", function_qualname) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - def _init_impl(self) -> None: - old_debug = _client_init_debug.get(False) - - def _capture_envelope(envelope: "Envelope") -> None: - if self.spotlight is not None: - self.spotlight.capture_envelope(envelope) - if self.transport is not None: - self.transport.capture_envelope(envelope) - - def _record_lost_event( - reason: str, - data_category: "EventDataCategory", - item: "Optional[Item]" = None, - quantity: int = 1, - ) -> None: - if self.transport is not None: - self.transport.record_lost_event( - reason=reason, - data_category=data_category, - item=item, - quantity=quantity, - ) - - try: - _client_init_debug.set(self.options["debug"]) - self.transport = make_transport(self.options) - - self.monitor = None - if self.transport: - if self.options["enable_backpressure_handling"]: - self.monitor = Monitor(self.transport) - - # Setup Spotlight before creating batchers so _capture_envelope can use it. - # setup_spotlight handles all config/env var resolution per the SDK spec. - from sentry_sdk.spotlight import setup_spotlight - - self.spotlight = setup_spotlight(self.options) - if self.spotlight is not None and not self.options["dsn"]: - sample_all = lambda *_args, **_kwargs: 1.0 - self.options["send_default_pii"] = True - 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, - ) - - self.session_flusher = SessionFlusher(capture_func=_capture_envelope) - - self.log_batcher = None - - if has_logs_enabled(self.options): - from sentry_sdk._log_batcher import LogBatcher - - self.log_batcher = LogBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.metrics_batcher = None - if has_metrics_enabled(self.options): - self.metrics_batcher = MetricsBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.span_batcher = None - if has_span_streaming_enabled(self.options): - self.span_batcher = SpanBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - max_request_body_size = ("always", "never", "small", "medium") - if self.options["max_request_body_size"] not in max_request_body_size: - raise ValueError( - "Invalid value for max_request_body_size. Must be one of {}".format( - max_request_body_size - ) - ) - - if self.options["_experiments"].get("otel_powered_performance", False): - logger.debug( - "[OTel] Enabling experimental OTel-powered performance monitoring." - ) - self.options["instrumenter"] = INSTRUMENTER.OTEL - if ( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - not in _DEFAULT_INTEGRATIONS - ): - _DEFAULT_INTEGRATIONS.append( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", - ) - - self.integrations = setup_integrations( - self.options["integrations"], - with_defaults=self.options["default_integrations"], - with_auto_enabling_integrations=self.options[ - "auto_enabling_integrations" - ], - disabled_integrations=self.options["disabled_integrations"], - options=self.options, - ) - - sdk_name = get_sdk_name(list(self.integrations.keys())) - SDK_INFO["name"] = sdk_name - logger.debug("Setting SDK name to '%s'", sdk_name) - - if has_profiling_enabled(self.options): - try: - setup_profiler(self.options) - except Exception as e: - logger.debug("Can not set up profiler. (%s)", e) - else: - try: - setup_continuous_profiler( - self.options, - sdk_info=SDK_INFO, - capture_func=_capture_envelope, - ) - except Exception as e: - logger.debug("Can not set up continuous profiler. (%s)", e) - - finally: - _client_init_debug.set(old_debug) - - self._setup_instrumentation(self.options.get("functions_to_trace", [])) - - if ( - self.monitor - or self.log_batcher - or self.metrics_batcher - or self.span_batcher - or has_profiling_enabled(self.options) - or isinstance(self.transport, HttpTransportCore) - ): - # If we have anything on that could spawn a background thread, we - # need to check if it's safe to use them. - check_uwsgi_thread_support() - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return True - - def should_send_default_pii(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. - """ - return self.options.get("send_default_pii") or False - - @property - def dsn(self) -> "Optional[str]": - """Returns the configured DSN as string.""" - return self.options["dsn"] - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - """Returns the configured parsed DSN object.""" - return self.transport.parsed_dsn if self.transport else None - - def _prepare_event( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]", - ) -> "Optional[Event]": - previous_total_spans: "Optional[int]" = None - previous_total_breadcrumbs: "Optional[int]" = None - - if event.get("timestamp") is None: - event["timestamp"] = datetime.now(timezone.utc) - - is_transaction = event.get("type") == "transaction" - - if scope is not None: - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - event_ = scope.apply_to_event(event, hint, self.options) - - # one of the event/error processors returned None - if event_ is None: - if self.transport: - self.transport.record_lost_event( - "event_processor", - data_category=("transaction" if is_transaction else "error"), - ) - if is_transaction: - self.transport.record_lost_event( - "event_processor", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - return None - - event = event_ - spans_delta = spans_before - len( - cast(List[Dict[str, object]], event.get("spans", [])) - ) - span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - - if is_transaction and self.transport is not None: - if spans_delta > 0: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) - if span_recorder_dropped_spans > 0: - self.transport.record_lost_event( - "buffer_overflow", - data_category="span", - quantity=span_recorder_dropped_spans, - ) - - dropped_spans: int = span_recorder_dropped_spans + spans_delta - if dropped_spans > 0: - previous_total_spans = spans_before + dropped_spans - if scope._n_breadcrumbs_truncated > 0: - breadcrumbs = event.get("breadcrumbs", {}) - values = ( - breadcrumbs.get("values", []) - if not isinstance(breadcrumbs, AnnotatedValue) - else [] - ) - previous_total_breadcrumbs = ( - len(values) + scope._n_breadcrumbs_truncated - ) - - if ( - not is_transaction - and self.options["attach_stacktrace"] - and "exception" not in event - and "stacktrace" not in event - and "threads" not in event - ): - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=self.options.get( - "include_local_variables", True - ), - max_value_length=self.options.get( - "max_value_length", DEFAULT_MAX_VALUE_LENGTH - ), - ), - "crashed": False, - "current": True, - } - ] - } - - for key in "release", "environment", "server_name", "dist": - if event.get(key) is None and self.options[key] is not None: - event[key] = str(self.options[key]).strip() - if event.get("sdk") is None: - sdk_info = dict(SDK_INFO) - sdk_info["integrations"] = sorted(self.integrations.keys()) - event["sdk"] = sdk_info - - if event.get("platform") is None: - event["platform"] = "python" - - event = handle_in_app( - event, - self.options["in_app_exclude"], - self.options["in_app_include"], - self.options["project_root"], - ) - - if event is not None: - event_scrubber = self.options["event_scrubber"] - if event_scrubber: - event_scrubber.scrub_event(event) - - if scope is not None and scope._gen_ai_original_message_count: - spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) - if isinstance(spans, list): - for span in spans: - span_id = span.get("span_id", None) - span_data = span.get("data", {}) - if ( - span_id - and span_id in scope._gen_ai_original_message_count - and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data - ): - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], - {"len": scope._gen_ai_original_message_count[span_id]}, - ) - if previous_total_spans is not None: - event["spans"] = AnnotatedValue( - event.get("spans", []), {"len": previous_total_spans} - ) - if previous_total_breadcrumbs is not None: - event["breadcrumbs"] = AnnotatedValue( - event.get("breadcrumbs", {"values": []}), - {"len": previous_total_breadcrumbs}, - ) - - # Postprocess the event here so that annotated types do - # generally not surface in before_send - if event is not None: - event = cast( - "Event", - serialize( - cast("Dict[str, Any]", event), - max_request_body_size=self.options.get("max_request_body_size"), - max_value_length=self.options.get("max_value_length"), - custom_repr=self.options.get("custom_repr"), - ), - ) - - before_send = self.options["before_send"] - if ( - before_send is not None - and event is not None - and event.get("type") != "transaction" - ): - new_event = None - with capture_internal_exceptions(): - new_event = before_send(event, hint or {}) - if new_event is None: - logger.info("before send dropped event") - if self.transport: - self.transport.record_lost_event( - "before_send", data_category="error" - ) - - # If this is an exception, reset the DedupeIntegration. It still - # remembers the dropped exception as the last exception, meaning - # that if the same exception happens again and is not dropped - # in before_send, it'd get dropped by DedupeIntegration. - if event.get("exception"): - DedupeIntegration.reset_last_seen() - - event = new_event - - before_send_transaction = self.options["before_send_transaction"] - if ( - before_send_transaction is not None - and event is not None - and event.get("type") == "transaction" - ): - new_event = None - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - with capture_internal_exceptions(): - new_event = before_send_transaction(event, hint or {}) - if new_event is None: - logger.info("before send transaction dropped event") - if self.transport: - self.transport.record_lost_event( - reason="before_send", data_category="transaction" - ) - self.transport.record_lost_event( - reason="before_send", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - else: - spans_delta = spans_before - len(new_event.get("spans", [])) - if spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - reason="before_send", data_category="span", quantity=spans_delta - ) - - event = new_event - - return event - - def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: - exc_info = hint.get("exc_info") - if exc_info is None: - return False - - error = exc_info[0] - error_type_name = get_type_name(exc_info[0]) - error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) - - for ignored_error in self.options["ignore_errors"]: - # String types are matched against the type name in the - # exception only - if isinstance(ignored_error, str): - if ignored_error == error_full_name or ignored_error == error_type_name: - return True - else: - if issubclass(error, ignored_error): - return True - - return False - - def _should_capture( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]" = None, - ) -> bool: - # Transactions are sampled independent of error events. - is_transaction = event.get("type") == "transaction" - if is_transaction: - return True - - ignoring_prevents_recursion = scope is not None and not scope._should_capture - if ignoring_prevents_recursion: - return False - - ignored_by_config_option = self._is_ignored_error(event, hint) - if ignored_by_config_option: - return False - - return True - - def _should_sample_error( - self, - event: "Event", - hint: "Hint", - ) -> bool: - error_sampler = self.options.get("error_sampler", None) - - if callable(error_sampler): - with capture_internal_exceptions(): - sample_rate = error_sampler(event, hint) - else: - sample_rate = self.options["sample_rate"] - - try: - not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate - except NameError: - logger.warning( - "The provided error_sampler raised an error. Defaulting to sampling the event." - ) - - # If the error_sampler raised an error, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - except TypeError: - parameter, verb = ( - ("error_sampler", "returned") - if callable(error_sampler) - else ("sample_rate", "contains") - ) - logger.warning( - "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." - % (parameter, verb, repr(sample_rate)) - ) - - # If the sample_rate has an invalid value, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - - if not_in_sample_rate: - # because we will not sample this event, record a "lost event". - if self.transport: - self.transport.record_lost_event("sample_rate", data_category="error") - - return False - - return True - - def _update_session_from_event( - self, - session: "Session", - event: "Event", - ) -> None: - crashed = False - errored = False - user_agent = None - - exceptions = (event.get("exception") or {}).get("values") - if exceptions: - errored = True - for error in exceptions: - if isinstance(error, AnnotatedValue): - error = error.value or {} - mechanism = error.get("mechanism") - if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: - crashed = True - break - - user = event.get("user") - - if session.user_agent is None: - headers = (event.get("request") or {}).get("headers") - headers_dict = headers if isinstance(headers, dict) else {} - for k, v in headers_dict.items(): - if k.lower() == "user-agent": - user_agent = v - break - - session.update( - status="crashed" if crashed else None, - user=user, - user_agent=user_agent, - errors=session.errors + (errored or crashed), - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - ) -> "Optional[str]": - """Captures an event. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - - :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. - """ - hint = dict(hint or ()) - - if not self._should_capture(event, hint, scope): - return None - - profile = event.pop("profile", None) - - event_id = event.get("event_id") - if event_id is None: - event["event_id"] = event_id = uuid.uuid4().hex - - span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) - event_opt = self._prepare_event(event, hint, scope) - if event_opt is None: - return None - - # whenever we capture an event we also check if the session needs - # to be updated based on that information. - session = scope._session if scope else None - if session: - self._update_session_from_event(session, event) - - is_transaction = event_opt.get("type") == "transaction" - is_checkin = event_opt.get("type") == "check_in" - - if ( - not is_transaction - and not is_checkin - and not self._should_sample_error(event, hint) - ): - return None - - attachments = hint.get("attachments") - - trace_context = event_opt.get("contexts", {}).get("trace") or {} - dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) - - headers: "dict[str, object]" = { - "event_id": event_opt["event_id"], - "sent_at": format_timestamp(datetime.now(timezone.utc)), - } - - if dynamic_sampling_context: - headers["trace"] = dynamic_sampling_context - - envelope = Envelope(headers=headers) - - if is_transaction and isinstance(profile, Profile): - envelope.add_profile(profile.to_json(event_opt, self.options)) - - if is_transaction and not span_recorder_has_gen_ai_span: - envelope.add_transaction(event_opt) - elif is_transaction: - split_spans = _split_gen_ai_spans(event_opt) - if split_spans is None or not split_spans[1]: - envelope.add_transaction(event_opt) - else: - non_gen_ai_spans, gen_ai_spans = split_spans - - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) - - converted_gen_ai_spans = [ - _serialized_v1_span_to_serialized_v2_span(span, event_opt) - for span in gen_ai_spans - if isinstance(span, dict) - ] - - envelope.add_item( - Item( - type=SpanBatcher.TYPE, - content_type=SpanBatcher.CONTENT_TYPE, - headers={ - "item_count": len(converted_gen_ai_spans), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": converted_gen_ai_spans, - }, - ), - ) - ) - - elif is_checkin: - envelope.add_checkin(event_opt) - else: - envelope.add_event(event_opt) - - for attachment in attachments or (): - envelope.add_item(attachment.to_envelope_item()) - - return_value = None - if self.spotlight: - self.spotlight.capture_envelope(envelope) - return_value = event_id - - if self.transport is not None: - self.transport.capture_envelope(envelope) - return_value = event_id - - return return_value - - def _capture_telemetry( - self, - telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", - ty: str, - scope: "Scope", - ) -> None: - """ - Capture attributes-based telemetry (logs, metrics, streamed spans). - - Apply any attributes set on the scope to it, and run the user's - before_send_{telemetry} on it, if applicable. - """ - if telemetry is None: - return - - scope.apply_to_telemetry(telemetry) - - before_send = None - - if ty == "log": - before_send = get_before_send_log(self.options) - serialized = telemetry - - elif ty == "metric": - before_send = get_before_send_metric(self.options) - serialized = telemetry - - elif ty == "span": - before_send = get_before_send_span(self.options) - serialized = telemetry._to_json() # type: ignore[union-attr] - - if before_send is not None: - serialized = before_send(serialized, {}) # type: ignore[arg-type] - - if ty in ("log", "metric"): - # Logs and metrics can be dropped in their respective - # before_send, so if we get None, don't queue them for sending. - if serialized is None: - return - - elif ty == "span" and isinstance(telemetry, StreamedSpan): - # Spans can't be dropped in before_send_span by design. They can - # be altered though (e.g. to sanitize). Only allow changes to - # name and attributes. - if isinstance(serialized, dict) and "name" in serialized: - telemetry.name = serialized["name"] # type: ignore[typeddict-item] - telemetry._attributes = {} - for k, v in (serialized.get("attributes") or {}).items(): - telemetry.set_attribute(k, v) - - else: - logger.debug( - "[Tracing] Invalid return value from before_send_span. Keeping original span." - ) - - serialized = telemetry._to_json() - - batcher = None - if ty == "log": - batcher = self.log_batcher - - elif ty == "metric": - batcher = self.metrics_batcher - - elif ty == "span": - # We need a reference to the segment span in the batcher to populate - # the dynamic sampling context (DSC) - serialized["_segment_span"] = telemetry._segment # type: ignore - batcher = self.span_batcher - - if batcher is not None: - batcher.add(serialized) # type: ignore - - def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: - self._capture_telemetry(log, "log", scope) - - def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: - self._capture_telemetry(metric, "metric", scope) - - def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: - self._capture_telemetry(span, "span", scope) - - def capture_session( - self, - session: "Session", - ) -> None: - if not session.release: - logger.info("Discarded session update because of missing release") - else: - self.session_flusher.add_session(session) - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Optional[Integration]": - """Returns the integration for this client by name or class. - If the client does not have that integration then `None` is returned. - """ - if isinstance(name_or_class, str): - integration_name = name_or_class - elif name_or_class.identifier is not None: - integration_name = name_or_class.identifier - else: - raise ValueError("Integration has no name") - - return self.integrations.get(integration_name) - - def _has_async_transport(self) -> bool: - """Check if the current transport is async.""" - return isinstance(self.transport, AsyncHttpTransport) - - @property - def _batchers(self) -> "tuple[Any, ...]": - return tuple( - b - for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) - if b is not None - ) - - def _close_components(self) -> None: - """Kill all client components in the correct order.""" - self.session_flusher.kill() - for b in self._batchers: - b.kill() - if self.monitor: - self.monitor.kill() - - def _flush_components(self) -> None: - """Flush all client components.""" - self.session_flusher.flush() - for b in self._batchers: - b.flush() - - def close( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush`. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "close() used with AsyncHttpTransport. Use close_async() instead.", - stacklevel=2, - ) - self._flush_components() - else: - self.flush(timeout=timeout, callback=callback) - self._close_components() - self.transport.kill() - self.transport = None - - async def close_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush_async`. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "close_async() used with non-async transport, aborting. Please use close() instead." - ) - return - await self.flush_async(timeout=timeout, callback=callback) - self._close_components() - kill_task = self.transport.kill() # type: ignore - if kill_task is not None: - await kill_task - self.transport = None - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "flush() used with AsyncHttpTransport. Use flush_async() instead.", - stacklevel=2, - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - - self.transport.flush(timeout=timeout, callback=callback) - - async def flush_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "flush_async() used with non-async transport, aborting. Please use flush() instead." - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore - if flush_task is not None: - await flush_task - - def __enter__(self) -> "_Client": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - self.close() - - async def __aenter__(self) -> "_Client": - return self - - async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - await self.close_async() - - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `get_options` is a - # type to have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `Dict[str, Any]` to tell static analyzers about the return type. - - class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 - pass - - class Client(ClientConstructor, _Client): - pass - -else: - # Alias `get_options` for actual usage. Go through the lambda indirection - # to throw PyCharm off of the weakly typed signature (it would otherwise - # discover both the weakly typed signature of `_init` and our faked `init` - # type). - - get_options = (lambda: _get_options)() - Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py deleted file mode 100644 index 759898f6ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/consts.py +++ /dev/null @@ -1,1802 +0,0 @@ -import itertools -from enum import Enum -from typing import TYPE_CHECKING - -DEFAULT_MAX_VALUE_LENGTH = None - -DEFAULT_MAX_STACK_FRAMES = 100 -DEFAULT_ADD_FULL_STACK = False - - -# Also needs to be at the top to prevent circular import -class EndpointType(Enum): - """ - The type of an endpoint. This is an enum, rather than a constant, for historical reasons - (the old /store endpoint). The enum also preserve future compatibility, in case we ever - have a new endpoint. - """ - - ENVELOPE = "envelope" - OTLP_TRACES = "integration/otlp/v1/traces" - - -class CompressionAlgo(Enum): - GZIP = "gzip" - BROTLI = "br" - - -if TYPE_CHECKING: - from typing import ( - AbstractSet, - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - ) - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - from sentry_sdk._types import ( - BreadcrumbProcessor, - ContinuousProfilerMode, - DataCollectionUserOptions, - Event, - EventProcessor, - Hint, - IgnoreSpansConfig, - Log, - Metric, - ProfilerMode, - SpanJSON, - TracesSampler, - TransactionProcessor, - ) - - # Experiments are feature flags to enable and disable certain unstable SDK - # functionality. Changing them from the defaults (`None`) in production - # code is highly discouraged. They are not subject to any stability - # guarantees such as the ones from semantic versioning. - Experiments = TypedDict( - "Experiments", - { - "max_spans": Optional[int], - "max_flags": Optional[int], - "record_sql_params": Optional[bool], - "continuous_profiling_auto_start": Optional[bool], - "continuous_profiling_mode": Optional[ContinuousProfilerMode], - "otel_powered_performance": Optional[bool], - "transport_zlib_compression_level": Optional[int], - "transport_compression_level": Optional[int], - "transport_compression_algo": Optional[CompressionAlgo], - "transport_num_pools": Optional[int], - "transport_http2": Optional[bool], - "transport_async": Optional[bool], - "enable_logs": Optional[bool], - "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], - "enable_metrics": Optional[bool], - "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], - "trace_lifecycle": Optional[Literal["static", "stream"]], - "ignore_spans": Optional[IgnoreSpansConfig], - "before_send_span": Optional[ - Callable[[SpanJSON, Hint], Optional[SpanJSON]] - ], - "suppress_asgi_chained_exceptions": Optional[bool], - "data_collection": Optional[DataCollectionUserOptions], - }, - total=False, - ) - -DEFAULT_QUEUE_SIZE = 100 -DEFAULT_MAX_BREADCRUMBS = 100 -MATCH_ALL = r".*" - -FALSE_VALUES = [ - "false", - "no", - "off", - "n", - "0", -] - - -class SPANTEMPLATE(str, Enum): - DEFAULT = "default" - AI_AGENT = "ai_agent" - AI_TOOL = "ai_tool" - AI_CHAT = "ai_chat" - - def __str__(self) -> str: - return self.value - - -class INSTRUMENTER: - SENTRY = "sentry" - OTEL = "otel" - - -class SPANNAME: - DB_COMMIT = "COMMIT" - DB_ROLLBACK = "ROLLBACK" - - -class SPANDATA: - """ - Additional information describing the type of the span. - See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ - """ - - AI_CITATIONS = "ai.citations" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - References or sources cited by the AI model in its response. - Example: ["Smith et al. 2020", "Jones 2019"] - """ - - AI_DOCUMENTS = "ai.documents" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Documents or content chunks used as context for the AI model. - Example: ["doc1.txt", "doc2.pdf"] - """ - - AI_FINISH_REASON = "ai.finish_reason" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. - - The reason why the model stopped generating. - Example: "length" - """ - - AI_FREQUENCY_PENALTY = "ai.frequency_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_FUNCTION_CALL = "ai.function_call" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_GENERATION_ID = "ai.generation_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. - - Unique identifier for the completion. - Example: "gen_123abc" - """ - - AI_INPUT_MESSAGES = "ai.input_messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. - - The input messages to an LLM call. - Example: [{"role": "user", "message": "hello"}] - """ - - AI_LOGIT_BIAS = "ai.logit_bias" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the logit bias - """ - - AI_METADATA = "ai.metadata" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Extra metadata passed to an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_MODEL_ID = "ai.model_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. - - The unique descriptor of the model being executed. - Example: gpt-4 - """ - - AI_PIPELINE_NAME = "ai.pipeline.name" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. - - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - AI_PREAMBLE = "ai.preamble" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the preamble parameter. - Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. - Example: "You are now a clown." - """ - - AI_PRESENCE_PENALTY = "ai.presence_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_RAW_PROMPTING = "ai.raw_prompting" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Minimize pre-processing done to the prompt sent to the LLM. - Example: true - """ - - AI_RESPONSE_FORMAT = "ai.response_format" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the format of the response - """ - - AI_RESPONSES = "ai.responses" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. - - The responses to an AI model call. Always as a list. - Example: ["hello", "world"] - """ - - AI_SEARCH_QUERIES = "ai.search_queries" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Queries used to search for relevant context or documents. - Example: ["climate change effects", "renewable energy"] - """ - - AI_SEARCH_REQUIRED = "ai.is_search_required" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Boolean indicating if the model needs to perform a search. - Example: true - """ - - AI_SEARCH_RESULTS = "ai.search_results" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Results returned from search queries for context. - Example: ["Result 1", "Result 2"] - """ - - AI_SEED = "ai.seed" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. - - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: 123.45 - """ - - AI_STREAMING = "ai.streaming" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. - - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - AI_TAGS = "ai.tags" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Tags that describe an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_TEMPERATURE = "ai.temperature" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. - - For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. - Example: 0.5 - """ - - AI_TEXTS = "ai.texts" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Raw text inputs provided to the model. - Example: ["What is machine learning?"] - """ - - AI_TOP_K = "ai.top_k" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. - - For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. - Example: 35 - """ - - AI_TOP_P = "ai.top_p" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. - - For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. - Example: 0.5 - """ - - AI_TOOL_CALLS = "ai.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_TOOLS = "ai.tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. - - For an AI model call, the functions that are available - """ - - AI_WARNINGS = "ai.warnings" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Warning messages generated during model execution. - Example: ["Token limit exceeded"] - """ - - CACHE_HIT = "cache.hit" - """ - A boolean indicating whether the requested data was found in the cache. - Example: true - """ - - CACHE_ITEM_SIZE = "cache.item_size" - """ - The size of the requested data in bytes. - Example: 58 - """ - - CACHE_KEY = "cache.key" - """ - The key of the requested data. - Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 - """ - - CLIENT_ADDRESS = "client.address" - """ - Client address of the network connection - IP address or Unix domain socket name. - Example: "10.1.2.80" - """ - - CODE_FILEPATH = "code.filepath" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FILE_PATH instead. - - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FILE_PATH = "code.file.path" - """ - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FUNCTION = "code.function" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead. - - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_FUNCTION_NAME = "code.function.name" - """ - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_LINENO = "code.lineno" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_LINE_NUMBER instead. - - The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. - Example: 42 - """ - - CODE_LINE_NUMBER = "code.line.number" - """ - The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. - Example: 42 - """ - - CODE_NAMESPACE = "code.namespace" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. - - The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. - Example: "http.handler" - """ - - DB_MONGODB_COLLECTION = "db.mongodb.collection" - """ - The MongoDB collection being accessed within the database. - See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes - Example: public.users; customers - """ - - DB_NAME = "db.name" - """ - .. deprecated:: - This attribute is deprecated. Use DB_NAMESPACE instead. - - The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - Example: myDatabase - """ - - DB_NAMESPACE = "db.namespace" - """ - The name of the database being accessed. - Example: "customers" - """ - - DB_DRIVER_NAME = "db.driver.name" - """ - The name of the database driver being used for the connection. - Example: "psycopg2" - """ - - DB_OPERATION = "db.operation" - """ - .. deprecated:: - This attribute is deprecated. Use DB_OPERATION_NAME instead. - - The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: findAndModify, HMSET, SELECT - """ - - DB_OPERATION_NAME = "db.operation.name" - """ - The name of the operation being executed. - Example: "SELECT" - """ - - DB_SYSTEM = "db.system" - """ - .. deprecated:: - This attribute is deprecated. Use DB_SYSTEM_NAME instead. - - An identifier for the database management system (DBMS) product being used. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: postgresql - """ - - DB_QUERY_TEXT = "db.query.text" - """ - The database query being executed. - Example: "SELECT * FROM users WHERE id = $1" - """ - - DB_SYSTEM_NAME = "db.system.name" - """ - An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. - Example: "postgresql" - """ - - DB_USER = "db.user" - """ - The name of the database user used for connecting to the database. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: my_user - """ - - GEN_AI_AGENT_NAME = "gen_ai.agent.name" - """ - The name of the agent being used. - Example: "ResearchAssistant" - """ - - GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" - """ - The unique identifier for the conversation/thread with the AI model. - Example: "conv_abc123" - """ - - GEN_AI_CHOICE = "gen_ai.choice" - """ - The model's response message. - Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" - """ - - GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" - """ - The input to the embeddings operation. - Example: "Hello!" - """ - - GEN_AI_FUNCTION_ID = "gen_ai.function_id" - """ - Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. - Example: "my-awesome-function" - """ - - GEN_AI_OPERATION_NAME = "gen_ai.operation.name" - """ - The name of the operation being performed. - Example: "chat" - """ - - GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" - """ - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" - """ - The reason why the model stopped generating. - Example: "COMPLETE" - """ - - GEN_AI_RESPONSE_ID = "gen_ai.response.id" - """ - Unique identifier for the completion. - Example: "gen_123abc" - """ - - GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" - """ - Exact model identifier used to generate the response - Example: gpt-4o-mini-2024-07-18 - """ - - GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" - """ - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The model's response text messages. - Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] - """ - - GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" - """ - The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. - Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] - """ - - GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" - """ - The time it took to receive the first token from the model. - Example: 0.1 - """ - - GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The tool calls in the model's response. - Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] - """ - - GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. - - The available tools for the model. - Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] - """ - - GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" - """ - The list of source system tool definitions available to the GenAI agent or model. - Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] - """ - - GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" - """ - The frequency penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" - """ - The maximum number of tokens to generate in the response. - Example: 2048 - """ - - GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" - """ - The system instructions passed to the model. - Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] - """ - - GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. - - The messages passed to the model. The "content" can be a string or an array of objects. - Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] - """ - - GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" - """ - The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". - Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] - """ - - GEN_AI_REQUEST_MODEL = "gen_ai.request.model" - """ - The model identifier being used for the request. - Example: "gpt-4-turbo" - """ - - GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" - """ - The presence penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_SEED = "gen_ai.request.seed" - """ - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: "1234567890" - """ - - GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" - """ - The temperature parameter used to control randomness in the output. - Example: 0.7 - """ - - GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" - """ - Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). - Example: 35 - """ - - GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" - """ - The top_p parameter used to control diversity via nucleus sampling. - Example: 1.0 - """ - - GEN_AI_SYSTEM = "gen_ai.system" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. - - The name of the AI system being used. - Example: "openai" - """ - - GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" - """ - The Generative AI provider as identified by the client or server instrumentation. - Example: "openai" - """ - - GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" - """ - The description of the tool being used. - Example: "Searches the web for current information about a topic" - """ - - GEN_AI_TOOL_INPUT = "gen_ai.tool.input" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. - - The input of the tool being used. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" - """ - The arguments of the tool call. It has to be a stringified version of the arguments to the tool. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_NAME = "gen_ai.tool.name" - """ - The name of the tool being used. - Example: "web_search" - """ - - GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. - - The output of the tool being used. - Example: "rainy, 57°F" - """ - - GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" - """ - The result of the tool call. It has to be a stringified version of the result of the tool. - Example: "rainy, 57°F" - """ - - GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" - """ - The number of tokens in the input. - Example: 150 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" - """ - The number of cached tokens in the input. - Example: 50 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" - """ - The number of tokens written to the cache when processing the AI input (prompt). - Example: 100 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" - """ - The number of tokens in the output. - Example: 250 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" - """ - The number of tokens used for reasoning in the output. - Example: 75 - """ - - GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" - """ - The total number of tokens used (input + output). - Example: 400 - """ - - GEN_AI_USER_MESSAGE = "gen_ai.user.message" - """ - The user message passed to the model. - Example: "What's the weather in Paris?" - """ - - HTTP_FRAGMENT = "http.fragment" - """ - The Fragments present in the URL. - Example: #foo=bar - """ - - HTTP_METHOD = "http.method" - """ - .. deprecated:: - This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. - - The HTTP method used. - Example: GET - """ - - HTTP_REQUEST_BODY_DATA = "http.request.body.data" - """ - HTTP request body data. Can be given as string or structural data of any format. - Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" - """ - - HTTP_REQUEST_HEADER = "http.request.header" - """ - Prefix for HTTP request header attributes. The header name (lowercased) is - appended to form the full attribute key. - Example: "http.request.header.content-type" - """ - - HTTP_REQUEST_METHOD = "http.request.method" - """ - The HTTP method used. - Example: GET - """ - - HTTP_QUERY = "http.query" - """ - The Query string present in the URL. - Example: ?foo=bar&bar=baz - """ - - HTTP_STATUS_CODE = "http.response.status_code" - """ - The HTTP status code as an integer. - Example: 418 - """ - - MESSAGING_DESTINATION_NAME = "messaging.destination.name" - """ - The destination name where the message is being consumed from, - e.g. the queue name or topic. - """ - - MESSAGING_MESSAGE_ID = "messaging.message.id" - """ - The message's identifier. - """ - - MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" - """ - The latency between when the task was enqueued and when it was started to be processed. - """ - - MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" - """ - Number of retries/attempts to process a message. - """ - - MESSAGING_SYSTEM = "messaging.system" - """ - The messaging system's name, e.g. `kafka`, `aws_sqs` - """ - - MIDDLEWARE_NAME = "middleware.name" - """ - The middleware's name, e.g. `AuthenticationMiddleware` - """ - - NETWORK_PROTOCOL_NAME = "network.protocol.name" - """ - The application layer protocol name used for the network connection. - Example: "http", "https" - """ - - NETWORK_PEER_ADDRESS = "network.peer.address" - """ - Peer address of the network connection - IP address or Unix domain socket name. - Example: 10.1.2.80, /tmp/my.sock, localhost - """ - - NETWORK_PEER_PORT = "network.peer.port" - """ - Peer port number of the network connection. - Example: 6379 - """ - - NETWORK_TRANSPORT = "network.transport" - """ - The transport protocol used for the network connection. - Example: "tcp", "udp", "unix" - """ - - PROCESS_PID = "process.pid" - """ - The process ID of the running process. - Example: 12345 - """ - - PROCESS_COMMAND_ARGS = "process.command_args" - """ - All the command arguments (including the command/executable itself) as received by the process. - Example: ["cmd/otecol","--config=config.yaml"] - """ - - PROFILER_ID = "profiler_id" - """ - Label identifying the profiler id that the span occurred in. This should be a string. - Example: "5249fbada8d5416482c2f6e47e337372" - """ - - RPC_METHOD = "rpc.method" - """ - The fully-qualified logical name of the method from the RPC interface perspective. - Example: "com.example.ExampleService/exampleMethod" - """ - - RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" - """ - Status code of the RPC returned by the RPC server or generated by the client. - Example: "DEADLINE_EXCEEDED" - """ - - SERVER_ADDRESS = "server.address" - """ - Name of the database host. - Example: example.com - """ - - SERVER_PORT = "server.port" - """ - Logical server port number - Example: 80; 8080; 443 - """ - - SERVER_SOCKET_ADDRESS = "server.socket.address" - """ - Physical server IP address or Unix socket address. - Example: 10.5.3.2 - """ - - SERVER_SOCKET_PORT = "server.socket.port" - """ - Physical server port. - Recommended: If different than server.port. - Example: 16456 - """ - - THREAD_ID = "thread.id" - """ - Identifier of a thread from where the span originated. This should be a string. - Example: "7972576320" - """ - - THREAD_NAME = "thread.name" - """ - Label identifying a thread from where the span originated. This should be a string. - Example: "MainThread" - """ - - USER_EMAIL = "user.email" - """ - User email address. - Example: "test@example.com" - """ - - USER_ID = "user.id" - """ - Unique identifier of the user. - Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" - """ - - USER_IP_ADDRESS = "user.ip_address" - """ - The IP address of the user that triggered the request. - Example: "10.1.2.80" - """ - - USER_NAME = "user.name" - """ - Short name or login/username of the user. - Example: "j.smith" - """ - - URL_FULL = "url.full" - """ - The URL of the resource that was fetched. - Example: "https://example.com/test?foo=bar#buzz" - """ - - URL_FRAGMENT = "url.fragment" - """ - The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. - Example: "details" - """ - - URL_PATH = "url.path" - """ - The URI path component. - Example: "/foo" - """ - - URL_QUERY = "url.query" - """ - The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. - Example: "foo=bar&bar=baz" - """ - - MCP_TOOL_NAME = "mcp.tool.name" - """ - The name of the MCP tool being called. - Example: "get_weather" - """ - - MCP_PROMPT_NAME = "mcp.prompt.name" - """ - The name of the MCP prompt being retrieved. - Example: "code_review" - """ - - MCP_RESOURCE_URI = "mcp.resource.uri" - """ - The URI of the MCP resource being accessed. - Example: "file:///path/to/resource" - """ - - MCP_METHOD_NAME = "mcp.method.name" - """ - The MCP protocol method name being called. - Example: "tools/call", "prompts/get", "resources/read" - """ - - MCP_REQUEST_ID = "mcp.request.id" - """ - The unique identifier for the MCP request. - Example: "req_123abc" - """ - - MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" - """ - The result/output content from an MCP tool execution. - Example: "The weather is sunny" - """ - - MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" - """ - The number of items/keys in the MCP tool result. - Example: 5 - """ - - MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" - """ - Whether the MCP tool execution resulted in an error. - Example: True - """ - - MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" - """ - The message content from an MCP prompt retrieval. - Example: "Review the following code..." - """ - - MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" - """ - The role of the message in an MCP prompt retrieval (only set for single-message prompts). - Example: "user", "assistant", "system" - """ - - MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" - """ - The number of messages in an MCP prompt result. - Example: 1, 3 - """ - - MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" - """ - The protocol/scheme of the MCP resource URI. - Example: "file", "http", "https" - """ - - MCP_TRANSPORT = "mcp.transport" - """ - The transport method used for MCP communication. - Example: "http", "sse", "stdio" - """ - - MCP_SESSION_ID = "mcp.session.id" - """ - The session identifier for the MCP connection. - Example: "a1b2c3d4e5f6" - """ - - SENTRY_DIST = "sentry.dist" - """ - The Sentry dist. - Example: "1.0" - """ - - SENTRY_ENVIRONMENT = "sentry.environment" - """ - The Sentry environment. - Example: "prod" - """ - - SENTRY_RELEASE = "sentry.release" - """ - The Sentry release. - Example: "1.2.3" - """ - - SENTRY_PLATFORM = "sentry.platform" - """ - The sdk platform that generated the event. - Example: "python" - """ - - SENTRY_SDK_NAME = "sentry.sdk.name" - """ - The name of the SDK. - Example: "python" - """ - - SENTRY_SDK_VERSION = "sentry.sdk.version" - """ - The SDK version. - Example: "1.2.3" - """ - - SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" - """ - A list of names identifying enabled integrations. - Example: ["AtexitIntegration", "StdlibIntegration"] - """ - - -class SPANSTATUS: - """ - The status of a Sentry span. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - - ABORTED = "aborted" - ALREADY_EXISTS = "already_exists" - CANCELLED = "cancelled" - DATA_LOSS = "data_loss" - DEADLINE_EXCEEDED = "deadline_exceeded" - FAILED_PRECONDITION = "failed_precondition" - INTERNAL_ERROR = "internal_error" - INVALID_ARGUMENT = "invalid_argument" - NOT_FOUND = "not_found" - OK = "ok" - OUT_OF_RANGE = "out_of_range" - PERMISSION_DENIED = "permission_denied" - RESOURCE_EXHAUSTED = "resource_exhausted" - UNAUTHENTICATED = "unauthenticated" - UNAVAILABLE = "unavailable" - UNIMPLEMENTED = "unimplemented" - UNKNOWN_ERROR = "unknown_error" - - -class OP: - ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" - CACHE_GET = "cache.get" - CACHE_PUT = "cache.put" - COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" - COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" - DB = "db" - DB_CURSOR_ITERATOR = "db.cursor.iter" - DB_CURSOR_FETCH = "db.cursor.fetch" - DB_REDIS = "db.redis" - EVENT_DJANGO = "event.django" - FUNCTION = "function" - FUNCTION_AWS = "function.aws" - FUNCTION_GCP = "function.gcp" - GEN_AI_CHAT = "gen_ai.chat" - GEN_AI_CREATE_AGENT = "gen_ai.create_agent" - GEN_AI_EMBEDDINGS = "gen_ai.embeddings" - GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" - GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" - GEN_AI_HANDOFF = "gen_ai.handoff" - GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" - GEN_AI_RESPONSES = "gen_ai.responses" - GRAPHQL_EXECUTE = "graphql.execute" - GRAPHQL_MUTATION = "graphql.mutation" - GRAPHQL_PARSE = "graphql.parse" - GRAPHQL_RESOLVE = "graphql.resolve" - GRAPHQL_SUBSCRIPTION = "graphql.subscription" - GRAPHQL_QUERY = "graphql.query" - GRAPHQL_VALIDATE = "graphql.validate" - GRPC_CLIENT = "grpc.client" - GRPC_SERVER = "grpc.server" - HTTP_CLIENT = "http.client" - HTTP_CLIENT_STREAM = "http.client.stream" - HTTP_SERVER = "http.server" - MIDDLEWARE_DJANGO = "middleware.django" - MIDDLEWARE_LITESTAR = "middleware.litestar" - MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" - MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" - MIDDLEWARE_STARLETTE = "middleware.starlette" - MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" - MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" - MIDDLEWARE_STARLITE = "middleware.starlite" - MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" - MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" - HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( - "ai.chat_completions.create.huggingface_hub" - ) - QUEUE_PROCESS = "queue.process" - QUEUE_PUBLISH = "queue.publish" - QUEUE_SUBMIT_ARQ = "queue.submit.arq" - QUEUE_TASK_ARQ = "queue.task.arq" - QUEUE_SUBMIT_CELERY = "queue.submit.celery" - QUEUE_TASK_CELERY = "queue.task.celery" - QUEUE_TASK_RQ = "queue.task.rq" - QUEUE_SUBMIT_HUEY = "queue.submit.huey" - QUEUE_TASK_HUEY = "queue.task.huey" - QUEUE_SUBMIT_RAY = "queue.submit.ray" - QUEUE_TASK_RAY = "queue.task.ray" - QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" - QUEUE_SUBMIT_DJANGO = "queue.submit.django" - SUBPROCESS = "subprocess" - SUBPROCESS_WAIT = "subprocess.wait" - SUBPROCESS_COMMUNICATE = "subprocess.communicate" - TEMPLATE_RENDER = "template.render" - VIEW_RENDER = "view.render" - VIEW_RESPONSE_RENDER = "view.response.render" - WEBSOCKET_SERVER = "websocket.server" - SOCKET_CONNECTION = "socket.connection" - SOCKET_DNS = "socket.dns" - MCP_SERVER = "mcp.server" - - -# This type exists to trick mypy and PyCharm into thinking `init` and `Client` -# take these arguments (even though they take opaque **kwargs) -class ClientConstructor: - def __init__( - self, - dsn: "Optional[str]" = None, - *, - max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - server_name: "Optional[str]" = None, - shutdown_timeout: float = 2, - integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 - in_app_include: "List[str]" = [], # noqa: B006 - in_app_exclude: "List[str]" = [], # noqa: B006 - default_integrations: bool = True, - dist: "Optional[str]" = None, - transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, - transport_queue_size: int = DEFAULT_QUEUE_SIZE, - sample_rate: float = 1.0, - send_default_pii: "Optional[bool]" = None, - http_proxy: "Optional[str]" = None, - https_proxy: "Optional[str]" = None, - ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 - max_request_body_size: str = "medium", - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, - keep_alive: "Optional[bool]" = None, - before_send: "Optional[EventProcessor]" = None, - before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, - debug: "Optional[bool]" = None, - attach_stacktrace: bool = False, - ca_certs: "Optional[str]" = None, - propagate_traces: bool = True, - traces_sample_rate: "Optional[float]" = None, - traces_sampler: "Optional[TracesSampler]" = None, - profiles_sample_rate: "Optional[float]" = None, - profiles_sampler: "Optional[TracesSampler]" = None, - profiler_mode: "Optional[ProfilerMode]" = None, - profile_lifecycle: 'Literal["manual", "trace"]' = "manual", - profile_session_sample_rate: "Optional[float]" = None, - auto_enabling_integrations: bool = True, - disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, - auto_session_tracking: bool = True, - send_client_reports: bool = True, - _experiments: "Experiments" = {}, # noqa: B006 - proxy_headers: "Optional[Dict[str, str]]" = None, - instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, - before_send_transaction: "Optional[TransactionProcessor]" = None, - project_root: "Optional[str]" = None, - enable_tracing: "Optional[bool]" = None, - include_local_variables: "Optional[bool]" = True, - include_source_context: "Optional[bool]" = True, - trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 - MATCH_ALL - ], - functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 - event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, - max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, - enable_backpressure_handling: bool = True, - error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, - enable_db_query_source: bool = True, - db_query_source_threshold_ms: int = 100, - enable_http_request_source: bool = True, - http_request_source_threshold_ms: int = 100, - spotlight: "Optional[Union[bool, str]]" = None, - cert_file: "Optional[str]" = None, - key_file: "Optional[str]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, - add_full_stack: bool = DEFAULT_ADD_FULL_STACK, - max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, - enable_logs: bool = False, - before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, - trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), - enable_metrics: bool = True, - before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, - org_id: "Optional[str]" = None, - strict_trace_continuation: bool = False, - stream_gen_ai_spans: bool = True, - ) -> None: - """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. - - :param dsn: The DSN tells the SDK where to send the events. - - If this option is not set, the SDK will just not send any data. - - The `dsn` config option takes precedence over the environment variable. - - Learn more about `DSN utilization `_. - - :param debug: Turns debug mode on or off. - - When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes - wrong with event sending. - - The default is always `False`. It's generally not recommended to turn it on in production because of the - increase in log output. - - The `debug` config option takes precedence over the environment variable. - - :param release: Sets the release. - - If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to - manually set it to guarantee that the release is in sync with your deploy integrations. - - Release names are strings, but some formats are detected by Sentry and might be rendered differently. - - See `the releases documentation `_ to learn how the SDK tries to - automatically configure a release. - - The `release` config option takes precedence over the environment variable. - - Learn more about how to send release data so Sentry can tell you about regressions between releases and - identify the potential source in `the product documentation `_. - - :param environment: Sets the environment. This string is freeform and set to `production` by default. - - A release can be associated with more than one environment to separate them in the UI (think `staging` vs - `production` or similar). - - The `environment` config option takes precedence over the environment variable. - - :param dist: The distribution of the application. - - Distributions are used to disambiguate build or deployment variants of the same release of an application. - - The dist can be for example a build number. - - :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. - - The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of - error events will be sent. - - Events are picked randomly. - - :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. - - This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and - which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number - between `0.0` and `1.0`, inclusive. - - The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to - send the event with the given probability. - - If this configuration option is specified, the `sample_rate` option is ignored. - - :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. - - Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're - sent to Sentry. - - By default, all errors are sent. - - :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. - - This defaults to `100`, but you can set this to any number. - - However, you should be aware that Sentry has a `maximum payload size `_ - and any events exceeding that payload size will be dropped. - - :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. - - Stack traces are always attached to exceptions; however, when this option is set, stack traces are also - sent with messages. - - This option means that stack traces appear next to all log messages. - - Grouping in Sentry is different for events with stack traces and without. As a result, you will get new - groups as you enable or disable this flag for certain events. - - :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) - `_ is added by active integrations. - - If you enable this option, be sure to manually remove what you don't want to send using our features for - managing `Sensitive Data `_. - - :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and - passwords from a `denylist`. - - It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. - - See how to `configure the scrubber here `_. - - :param include_source_context: When enabled, source context will be included in events sent to Sentry. - - This source context includes the five lines of code above and below the line of code where an error - happened. - - :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with - the event to help with debugging. - - :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the - moment an error occurs. - - But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will - be included in the stack trace sent to Sentry. - - :param max_stack_frames: This option limits the number of stack frames that will be captured when - `add_full_stack` is enabled. - - :param server_name: This option can be used to supply a server name. - - When provided, the name of the server is sent along and persisted in the event. - - For many integrations, the server name actually corresponds to the device hostname, even in situations - where the machine is not actually a server. - - :param project_root: The full path to the root directory of your application. - - The `project_root` is used to mark frames in a stack trace either as being in your application or outside - of the application. - - :param in_app_include: A list of string prefixes of module names that belong to the app. - - This option takes precedence over `in_app_exclude`. - - Sentry differentiates stack frames that are directly related to your application ("in application") from - stack frames that come from other packages such as the standard library, frameworks, or other dependencies. - - The application package is automatically marked as `inApp`. - - The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are - displayed by default. - - :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to - third-party packages. - - Modules considered not part of the app will be hidden from stack traces by default. - - This option can be overridden using `in_app_include`. - - :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. - It can be set to one of the following values: - - - `never`: Request bodies are never sent. - - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically - 4KB). - - `medium`: Medium and small requests will be captured (typically 10KB). - - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. - - Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ - expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of - how you configure this option. - - :param max_value_length: The number of characters after which the values containing text in the event payload - will be truncated. - - WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be - dropped by Sentry. - - :param ca_certs: A path to an alternative CA bundle file in PEM-format. - - :param send_client_reports: Set this boolean to `False` to disable sending of client reports. - - Client reports allow the client to send status reports about itself to Sentry, such as information about - events that were dropped before being sent. - - :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) - `_. - - This setting can be used to override the default config options for a specific auto-enabling integration - or to add an integration that is not auto-enabled. - - :param disabled_integrations: List of integrations that will be disabled. - - This setting can be used to explicitly turn off specific `auto-enabling integrations (list) - `_ or - `default `_ integrations. - - :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) - `_ should be enabled. - - When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding - framework/library is detected. - - :param default_integrations: Configures whether `default integrations - `_ should be enabled. - - Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling - integrations**, unless they are specifically added in the `integrations` option, described above. - - :param before_send: This function is called with an SDK-specific message or error event object, and can return - a modified event object, or `null` to skip reporting the event. - - This can be used, for instance, for manual PII stripping before sending. - - By the time `before_send` is executed, all scope data has already been applied to the event. Further - modification of the scope won't have any effect. - - :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can - return a modified transaction event object, or `null` to skip reporting the event. - - One way this might be used is for manual PII stripping before sending. - - :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb - is added to the scope. - - When nothing is returned from the function, the breadcrumb is dropped. - - To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. - - The callback typically gets a second argument (called a "hint") which contains the original object from - which the breadcrumb was created to further customize what the breadcrumb should look like. - - :param transport: Switches out the transport used to send events. - - How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to - send it through some more complex setup that requires proxy authentication. - - :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to - flush. - - :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. - - This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs - support a separate HTTPS proxy. - - SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix - systems, the `http_proxy` environment variable will be picked up. - - :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. - - This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for - HTTPS requests at all times. - - :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded - to `urllib3`'s `ProxyManager `_. - - :param shutdown_timeout: Controls how many seconds to wait before shutting down. - - Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending - events. The default is SDK specific but typically around two seconds. - - Setting this value too low may cause problems for sending events from command line applications. - - Setting the value too high will cause the application to block for a long time for users experiencing - network connectivity problems. - - :param keep_alive: Determines whether to keep the connection alive between requests. - - This can be useful in environments where you encounter frequent network issues such as connection resets. - - :param cert_file: Path to the client certificate to use. - - If set, supersedes the `CLIENT_CERT_FILE` environment variable. - - :param key_file: Path to the key file to use. - - If set, supersedes the `CLIENT_KEY_FILE` environment variable. - - :param socket_options: An optional list of socket options to use. - - These provide fine-grained, low-level control over the way the SDK connects to Sentry. - - If provided, the options will override the default `urllib3` `socket options - `_. - - :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction - will be sent to Sentry. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - Either this or `traces_sampler` must be defined to enable tracing. - - If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have - another service (for example a JS frontend) that makes requests to your service that include trace - information, those traces will be continued and thus transactions will be sent to Sentry. - - If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces - will be started and no incoming traces will be continued. - - :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be - sent to Sentry. - - It will automatically be passed information about the transaction and the context in which it's being - created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being - sent). - - Can also be used for filtering transactions, by returning `0` for those that are unwanted. - - Either this or `traces_sample_rate` must be defined to enable tracing. - - :param trace_propagation_targets: An optional property that controls which downstream services receive tracing - data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. - - The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. - - If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to - that request. - - String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ - a string provided through the option. - - If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the - instrumented client. - - :param functions_to_trace: An optional list of functions that should be set up for tracing. - - For each function in the list, a span will be created when the function is executed. - - Functions in the list are represented as strings containing the fully qualified name of the function. - - This is a convenient option, making it possible to have one central place for configuring what functions - to trace, instead of having custom instrumentation scattered all over your code base. - - To learn more, see the `Custom Instrumentation `_ documentation. - - :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health - checks on the SDK. - - If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second - intervals until recovery. - - This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. - - This option is enabled by default. - - :param enable_db_query_source: When enabled, the source location will be added to database queries. - - :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database - queries. - - The query location will be added to the query for queries slower than the specified threshold. - - :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. - - :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an - outgoing HTTP request. - - The request location will be added to the request for requests slower than the specified threshold. - - :param custom_repr: A custom `repr `_ function to run - while serializing an object. - - Use this to control how your custom objects and classes are visible in Sentry. - - Return a string for that repr value to be used or `None` to continue serializing how Sentry would have - done it anyway. - - :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled - transaction will be profiled. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be - profiled. - - :param profiles_sampler: - - :param profiler_mode: - - :param profile_lifecycle: - - :param profile_session_sample_rate: - - :param enable_tracing: - - :param propagate_traces: - - :param auto_session_tracking: - - :param spotlight: - - :param instrumenter: - - :param enable_logs: Set `enable_logs` to True to enable the SDK to emit - Sentry logs. Defaults to False. - - :param before_send_log: An optional function to modify or filter out logs - before they're sent to Sentry. Any modifications to the log in this - function will be retained. If the function returns None, the log will - not be sent to Sentry. - - :param trace_ignore_status_codes: An optional property that disables tracing for - HTTP requests with certain status codes. - - Requests are not traced if the status code is contained in the provided set. - - If `trace_ignore_status_codes` is not provided, requests with any status code - may be traced. - - This option has no effect in span streaming mode (`trace_lifecycle="stream"`). - - :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the - `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. - - If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. - - The client's organization ID is extracted from the DSN or can be set with the `org_id` option. - If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. - This is useful to prevent traces of unknown third-party services from being continued in your application. - - :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases - but you can provide it explicitly for self-hosted and Relay setups. This value is used for - trace propagation and for features like `strict_trace_continuation`. - - :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to - reduce downstream data loss. - - :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. - - ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations - collect automatically, superseding `send_default_pii`. Passing a dict under - `_experiments={"data_collection": {...}}` opts into the feature; 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 - changes nothing. Restrict collection per category (user identity, cookies, HTTP - headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source - context). If `send_default_pii` is also set, `data_collection` takes precedence. - - Example:: - - sentry_sdk.init( - dsn="...", - _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, - ) - - See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. - """ - pass - - -def _get_default_options() -> "dict[str, Any]": - import inspect - - a = inspect.getfullargspec(ClientConstructor.__init__) - defaults = a.defaults or () - kwonlydefaults = a.kwonlydefaults or {} - - return dict( - itertools.chain( - zip(a.args[-len(defaults) :], defaults), - kwonlydefaults.items(), - ) - ) - - -DEFAULT_OPTIONS = _get_default_options() -del _get_default_options - - -VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py deleted file mode 100644 index b3287703b9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.crons.api import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.crons.decorator import monitor - -__all__ = [ - "capture_checkin", - "MonitorStatus", - "monitor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py deleted file mode 100644 index 6ea3e36b6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/api.py +++ /dev/null @@ -1,60 +0,0 @@ -import uuid -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, MonitorConfig - - -def _create_check_in_event( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration_s: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> "Event": - options = sentry_sdk.get_client().options - check_in_id = check_in_id or uuid.uuid4().hex - - check_in: "Event" = { - "type": "check_in", - "monitor_slug": monitor_slug, - "check_in_id": check_in_id, - "status": status, - "duration": duration_s, - "environment": options.get("environment", None), - "release": options.get("release", None), - } - - if monitor_config: - check_in["monitor_config"] = monitor_config - - return check_in - - -def capture_checkin( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> str: - check_in_event = _create_check_in_event( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=status, - duration_s=duration, - monitor_config=monitor_config, - ) - - sentry_sdk.capture_event(check_in_event) - - logger.debug( - f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" - ) - - return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py deleted file mode 100644 index be686b4539..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/consts.py +++ /dev/null @@ -1,4 +0,0 @@ -class MonitorStatus: - IN_PROGRESS = "in_progress" - OK = "ok" - ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py deleted file mode 100644 index b13d350e15..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/crons/decorator.py +++ /dev/null @@ -1,137 +0,0 @@ -from functools import wraps -from inspect import iscoroutinefunction -from typing import TYPE_CHECKING - -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from types import TracebackType - from typing import ( - Any, - Optional, - ParamSpec, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from sentry_sdk._types import MonitorConfig - - P = ParamSpec("P") - R = TypeVar("R") - - -class monitor: # noqa: N801 - """ - Decorator/context manager to capture checkin events for a monitor. - - Usage (as decorator): - ``` - import sentry_sdk - - app = Celery() - - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` - - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. - - Usage (as context manager): - ``` - import sentry_sdk - - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` - """ - - def __init__( - self, - monitor_slug: "Optional[str]" = None, - monitor_config: "Optional[MonitorConfig]" = None, - ) -> None: - self.monitor_slug = monitor_slug - self.monitor_config = monitor_config - - def __enter__(self) -> None: - self.start_timestamp = now() - self.check_in_id = capture_checkin( - monitor_slug=self.monitor_slug, - status=MonitorStatus.IN_PROGRESS, - monitor_config=self.monitor_config, - ) - - def __exit__( - self, - exc_type: "Optional[Type[BaseException]]", - exc_value: "Optional[BaseException]", - traceback: "Optional[TracebackType]", - ) -> None: - duration_s = now() - self.start_timestamp - - if exc_type is None and exc_value is None and traceback is None: - status = MonitorStatus.OK - else: - status = MonitorStatus.ERROR - - capture_checkin( - monitor_slug=self.monitor_slug, - check_in_id=self.check_in_id, - status=status, - duration=duration_s, - monitor_config=self.monitor_config, - ) - - if TYPE_CHECKING: - - @overload - def __call__( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - # Unfortunately, mypy does not give us any reliable way to type check the - # return value of an Awaitable (i.e. async function) for this overload, - # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. - ... - - @overload - def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... - - def __call__( - self, - fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", - ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": - if iscoroutinefunction(fn): - return self._async_wrapper(fn) - - else: - if TYPE_CHECKING: - fn = cast("Callable[P, R]", fn) - return self._sync_wrapper(fn) - - def _async_wrapper( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - @wraps(fn) - async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return await fn(*args, **kwargs) - - return inner - - def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": - @wraps(fn) - def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return fn(*args, **kwargs) - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py deleted file mode 100644 index bcdf767409..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/data_collection.py +++ /dev/null @@ -1,319 +0,0 @@ -""" -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, List, Mapping, Optional, cast - -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE - -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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: - """ - Return whether ``key`` matches the sensitive denylist using a partial, - case-insensitive substring match. - - :param extra_terms: additional deny terms (e.g. user-provided) to consider - alongside the built-in `_SENSITIVE_DENYLIST`. - """ - lowered = key.lower() - for term in _SENSITIVE_DENYLIST: - if term in lowered: - return True - if extra_terms: - for term in extra_terms: - if term and term.lower() in lowered: - return True - return False - - -def _apply_key_value_collection_filtering( - items: "Mapping[str, Any]", - behaviour: "KeyValueCollectionBehaviour", - substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, -) -> "Dict[str, Any]": - - if behaviour["mode"] == "off": - return {} - - result: "Dict[str, Any]" = {} - - if behaviour["mode"] == "allowlist": - for key, value in items.items(): - is_allowed = False - if isinstance(key, str): - lowered = key.lower() - is_allowed = any( - term and term.lower() in lowered - for term in behaviour.get("terms", []) - ) - if is_allowed and not _is_sensitive_key(key): - result[key] = value - else: - result[key] = substitute - return result - - # denylist behaviour - for key, value in items.items(): - if isinstance(key, str) and _is_sensitive_key( - key=key, extra_terms=behaviour.get("terms", []) - ): - result[key] = substitute - else: - result[key] = value - return result - - -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, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py deleted file mode 100644 index 795882e9ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/debug.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -import sys -import warnings -from logging import LogRecord - -from sentry_sdk import get_client -from sentry_sdk.client import _client_init_debug -from sentry_sdk.utils import logger - - -class _DebugFilter(logging.Filter): - def filter(self, record: "LogRecord") -> bool: - if _client_init_debug.get(False): - return True - - return get_client().options["debug"] - - -def init_debug_support() -> None: - if not logger.handlers: - configure_logger() - - -def configure_logger() -> None: - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.DEBUG) - logger.addFilter(_DebugFilter()) - - -def configure_debug_hub() -> None: - warnings.warn( - "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py deleted file mode 100644 index d2d4aae31a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/envelope.py +++ /dev/null @@ -1,332 +0,0 @@ -import io -import json -import mimetypes -from typing import TYPE_CHECKING - -from sentry_sdk.session import Session -from sentry_sdk.utils import capture_internal_exceptions, json_dumps - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, List, Optional, Union - - from sentry_sdk._types import Event, EventDataCategory - - -def parse_json(data: "Union[bytes, str]") -> "Any": - # on some python 3 versions this needs to be bytes - if isinstance(data, bytes): - data = data.decode("utf-8", "replace") - return json.loads(data) - - -class Envelope: - """ - Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints - documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, - each envelope may have at most one Item with type "event" or "transaction" (but not both). - """ - - def __init__( - self, - headers: "Optional[Dict[str, Any]]" = None, - items: "Optional[List[Item]]" = None, - ) -> None: - if headers is not None: - headers = dict(headers) - self.headers = headers or {} - if items is None: - items = [] - else: - items = list(items) - self.items = items - - @property - def description(self) -> str: - return "envelope with %s items (%s)" % ( - len(self.items), - ", ".join(x.data_category for x in self.items), - ) - - def add_event( - self, - event: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=event), type="event")) - - def add_transaction( - self, - transaction: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - - def add_profile( - self, - profile: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) - - def add_profile_chunk( - self, - profile_chunk: "Any", - ) -> None: - self.add_item( - Item( - payload=PayloadRef(json=profile_chunk), - type="profile_chunk", - headers={"platform": profile_chunk.get("platform", "python")}, - ) - ) - - def add_checkin( - self, - checkin: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) - - def add_session( - self, - session: "Union[Session, Any]", - ) -> None: - if isinstance(session, Session): - session = session.to_json() - self.add_item(Item(payload=PayloadRef(json=session), type="session")) - - def add_sessions( - self, - sessions: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) - - def add_item( - self, - item: "Item", - ) -> None: - self.items.append(item) - - def get_event(self) -> "Optional[Event]": - for items in self.items: - event = items.get_event() - if event is not None: - return event - return None - - def get_transaction_event(self) -> "Optional[Event]": - for item in self.items: - event = item.get_transaction_event() - if event is not None: - return event - return None - - def __iter__(self) -> "Iterator[Item]": - return iter(self.items) - - def serialize_into( - self, - f: "Any", - ) -> None: - f.write(json_dumps(self.headers)) - f.write(b"\n") - for item in self.items: - item.serialize_into(f) - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Envelope": - headers = parse_json(f.readline()) - items = [] - while 1: - item = Item.deserialize_from(f) - if item is None: - break - items.append(item) - return cls(headers=headers, items=items) - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Envelope": - return cls.deserialize_from(io.BytesIO(bytes)) - - def __repr__(self) -> str: - return "" % (self.headers, self.items) - - -class PayloadRef: - def __init__( - self, - bytes: "Optional[bytes]" = None, - path: "Optional[Union[bytes, str]]" = None, - json: "Optional[Any]" = None, - ) -> None: - self.json = json - self.bytes = bytes - self.path = path - - def get_bytes(self) -> bytes: - if self.bytes is None: - if self.path is not None: - with capture_internal_exceptions(): - with open(self.path, "rb") as f: - self.bytes = f.read() - elif self.json is not None: - self.bytes = json_dumps(self.json) - return self.bytes or b"" - - @property - def inferred_content_type(self) -> str: - if self.json is not None: - return "application/json" - elif self.path is not None: - path = self.path - if isinstance(path, bytes): - path = path.decode("utf-8", "replace") - ty = mimetypes.guess_type(path)[0] - if ty: - return ty - return "application/octet-stream" - - def __repr__(self) -> str: - return "" % (self.inferred_content_type,) - - -class Item: - def __init__( - self, - payload: "Union[bytes, str, PayloadRef]", - headers: "Optional[Dict[str, Any]]" = None, - type: "Optional[str]" = None, - content_type: "Optional[str]" = None, - filename: "Optional[str]" = None, - ): - if headers is not None: - headers = dict(headers) - elif headers is None: - headers = {} - self.headers = headers - if isinstance(payload, bytes): - payload = PayloadRef(bytes=payload) - elif isinstance(payload, str): - payload = PayloadRef(bytes=payload.encode("utf-8")) - else: - payload = payload - - if filename is not None: - headers["filename"] = filename - if type is not None: - headers["type"] = type - if content_type is not None: - headers["content_type"] = content_type - elif "content_type" not in headers: - headers["content_type"] = payload.inferred_content_type - - self.payload = payload - - def __repr__(self) -> str: - return "" % ( - self.headers, - self.payload, - self.data_category, - ) - - @property - def type(self) -> "Optional[str]": - return self.headers.get("type") - - @property - def data_category(self) -> "EventDataCategory": - ty = self.headers.get("type") - if ty == "session" or ty == "sessions": - return "session" - elif ty == "attachment": - return "attachment" - elif ty == "transaction": - return "transaction" - elif ty == "span": - return "span" - elif ty == "event": - return "error" - elif ty == "log": - return "log_item" - elif ty == "trace_metric": - return "trace_metric" - elif ty == "client_report": - return "internal" - elif ty == "profile": - return "profile" - elif ty == "profile_chunk": - return "profile_chunk" - elif ty == "check_in": - return "monitor" - else: - return "default" - - def get_bytes(self) -> bytes: - return self.payload.get_bytes() - - def get_event(self) -> "Optional[Event]": - """ - Returns an error event if there is one. - """ - if self.type == "event" and self.payload.json is not None: - return self.payload.json - return None - - def get_transaction_event(self) -> "Optional[Event]": - if self.type == "transaction" and self.payload.json is not None: - return self.payload.json - return None - - def serialize_into( - self, - f: "Any", - ) -> None: - headers = dict(self.headers) - bytes = self.get_bytes() - headers["length"] = len(bytes) - f.write(json_dumps(headers)) - f.write(b"\n") - f.write(bytes) - f.write(b"\n") - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Optional[Item]": - line = f.readline().rstrip() - if not line: - return None - headers = parse_json(line) - length = headers.get("length") - if length is not None: - payload = f.read(length) - f.readline() - else: - # if no length was specified we need to read up to the end of line - # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) - payload = f.readline().rstrip(b"\n") - if headers.get("type") in ("event", "transaction"): - rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) - else: - rv = cls(headers=headers, payload=payload) - return rv - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Optional[Item]": - return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py deleted file mode 100644 index 5eaa5e440b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/feature_flags.py +++ /dev/null @@ -1,75 +0,0 @@ -import copy -from threading import Lock -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import TypedDict - - FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) - - -DEFAULT_FLAG_CAPACITY = 100 - - -class FlagBuffer: - def __init__(self, capacity: int) -> None: - self.capacity = capacity - self.lock = Lock() - - # Buffer is private. The name is mangled to discourage use. If you use this attribute - # directly you're on your own! - self.__buffer = LRUCache(capacity) - - def clear(self) -> None: - self.__buffer = LRUCache(self.capacity) - - def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": - with self.lock: - buffer = FlagBuffer(self.capacity) - buffer.__buffer = copy.deepcopy(self.__buffer, memo) - return buffer - - def get(self) -> "list[FlagData]": - with self.lock: - return [ - {"flag": key, "result": value} for key, value in self.__buffer.get_all() - ] - - def set(self, flag: str, result: bool) -> None: - if isinstance(result, FlagBuffer): - # If someone were to insert `self` into `self` this would create a circular dependency - # on the lock. This is of course a deadlock. However, this is far outside the expected - # usage of this class. We guard against it here for completeness and to document this - # expected failure mode. - raise ValueError( - "FlagBuffer instances can not be inserted into the dictionary." - ) - - with self.lock: - self.__buffer.set(flag, result) - - -def add_feature_flag(flag: str, result: bool) -> None: - """ - Records a flag and its value to be sent on subsequent error events. - We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. - """ - client = sentry_sdk.get_client() - - flags = sentry_sdk.get_isolation_scope().flags - flags.set(flag, result) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.get_current_span() - if span and isinstance(span, sentry_sdk.traces.StreamedSpan): - span.set_attribute(f"flag.evaluation.{flag}", result) - - else: - span = sentry_sdk.get_current_span() - if span and isinstance(span, Span): - span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py deleted file mode 100644 index b17444d06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/hub.py +++ /dev/null @@ -1,741 +0,0 @@ -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import ( - get_client, - get_current_scope, - get_global_scope, - get_isolation_scope, -) -from sentry_sdk._compat import with_metaclass -from sentry_sdk.client import Client -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.scope import _ScopeManager -from sentry_sdk.tracing import ( - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.utils import ( - ContextVar, - logger, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.tracing import TransactionKwargs - - T = TypeVar("T") - -else: - - def overload(x: "T") -> "T": - return x - - -class SentryHubDeprecationWarning(DeprecationWarning): - """ - A custom deprecation warning to inform users that the Hub is deprecated. - """ - - _MESSAGE = ( - "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " - "Please consult our 1.x to 2.x migration guide for details on how to migrate " - "`Hub` usage to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" - ) - - def __init__(self, *_: object) -> None: - super().__init__(self._MESSAGE) - - -@contextmanager -def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": - """Utility function to suppress deprecation warnings for the Hub.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) - yield - - -_local = ContextVar("sentry_current_hub") - - -class HubMeta(type): - @property - def current(cls) -> "Hub": - """Returns the current instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - rv = _local.get(None) - if rv is None: - with _suppress_hub_deprecation_warning(): - # This will raise a deprecation warning; suppress it since we already warned above. - rv = Hub(GLOBAL_HUB) - _local.set(rv) - return rv - - @property - def main(cls) -> "Hub": - """Returns the main instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - return GLOBAL_HUB - - -class Hub(with_metaclass(HubMeta)): # type: ignore - """ - .. deprecated:: 2.0.0 - The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. - - The hub wraps the concurrency management of the SDK. Each thread has - its own hub but the hub might transfer with the flow of execution if - context vars are available. - - If the hub is used with a with statement it's temporarily activated. - """ - - _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] - _scope: "Optional[Scope]" = None - - # Mypy doesn't pick up on the metaclass. - - if TYPE_CHECKING: - current: "Hub" = None # type: ignore[assignment] - main: "Optional[Hub]" = None - - def __init__( - self, - client_or_hub: "Optional[Union[Hub, Client]]" = None, - scope: "Optional[Any]" = None, - ) -> None: - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - - current_scope = None - - if isinstance(client_or_hub, Hub): - client = get_client() - if scope is None: - # hub cloning is going on, we use a fork of the current/isolation scope for context manager - scope = get_isolation_scope().fork() - current_scope = get_current_scope().fork() - else: - client = client_or_hub - get_global_scope().set_client(client) - - if scope is None: # so there is no Hub cloning going on - # just the current isolation scope is used for context manager - scope = get_isolation_scope() - current_scope = get_current_scope() - - if current_scope is None: - # just the current current scope is used for context manager - current_scope = get_current_scope() - - self._stack = [(client, scope)] # type: ignore - self._last_event_id: "Optional[str]" = None - self._old_hubs: "List[Hub]" = [] - - self._old_current_scopes: "List[Scope]" = [] - self._old_isolation_scopes: "List[Scope]" = [] - self._current_scope: "Scope" = current_scope - self._scope: "Scope" = scope - - def __enter__(self) -> "Hub": - self._old_hubs.append(Hub.current) - _local.set(self) - - current_scope = get_current_scope() - self._old_current_scopes.append(current_scope) - scope._current_scope.set(self._current_scope) - - isolation_scope = get_isolation_scope() - self._old_isolation_scopes.append(isolation_scope) - scope._isolation_scope.set(self._scope) - - return self - - def __exit__( - self, - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[Any]", - ) -> None: - old = self._old_hubs.pop() - _local.set(old) - - old_current_scope = self._old_current_scopes.pop() - scope._current_scope.set(old_current_scope) - - old_isolation_scope = self._old_isolation_scopes.pop() - scope._isolation_scope.set(old_isolation_scope) - - def run( - self, - callback: "Callable[[], T]", - ) -> "T": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Runs a callback in the context of the hub. Alternatively the - with statement can be used on the hub directly. - """ - with self: - return callback() - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Any": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. - - Returns the integration for this hub by name or class. If there - is no client bound or the client does not have that integration - then `None` is returned. - - If the return value is not `None` the hub is guaranteed to have a - client attached. - """ - return get_client().get_integration(name_or_class) - - @property - def client(self) -> "Optional[BaseClient]": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Please use :py:func:`sentry_sdk.api.get_client` instead. - - Returns the current client on the hub. - """ - client = get_client() - - if not client.is_active(): - return None - - return client - - @property - def scope(self) -> "Scope": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Returns the current scope on the hub. - """ - return get_isolation_scope() - - def last_event_id(self) -> "Optional[str]": - """ - Returns the last event ID. - - .. deprecated:: 1.40.5 - This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. - """ - logger.warning( - "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." - ) - return self._last_event_id - - def bind_client( - self, - new: "Optional[BaseClient]", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.set_client` instead. - - Binds a new client to the hub. - """ - get_global_scope().set_client(new) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. - - Captures an event. - - Alias of :py:meth:`sentry_sdk.Scope.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - """ - last_event_id = get_current_scope().capture_event( - event, hint, scope=scope, **scope_kwargs - ) - - is_transaction = event.get("type") == "transaction" - if last_event_id is not None and not is_transaction: - self._last_event_id = last_event_id - - return last_event_id - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. - - Captures a message. - - Alias of :py:meth:`sentry_sdk.Scope.capture_message`. - - :param message: The string to send as the message to Sentry. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_message( - message, level=level, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. - - Captures an exception. - - Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_exception( - error, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. - - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_span` instead. - - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - """ - scope = get_current_scope() - return scope.start_span(instrumenter=instrumenter, **kwargs) - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. - - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. - """ - scope = get_current_scope() - - # For backwards compatibility, we allow passing the scope as the hub. - # We need a major release to make this nice. (if someone searches the code: deprecated) - # Type checking disabled for this line because deprecated keys are not allowed in the type signature. - kwargs["hub"] = scope # type: ignore - - return scope.start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - ) -> "Transaction": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. - - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers=environ_or_headers, op=op, name=name, source=source - ) - - @overload - def push_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def push_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def push_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - if callback is not None: - with self.push_scope() as scope: - callback(scope) - return None - - return _ScopeManager(self) - - def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pops a scope layer from the stack. - - Try to use the context manager :py:meth:`push_scope` instead. - """ - rv = self._stack.pop() - assert self._stack, "stack must have at least one layer" - return rv - - @overload - def configure_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def configure_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def configure_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - scope = get_isolation_scope() - - if continue_trace: - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - def start_session( - self, - session_mode: str = "application", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_session` instead. - - Starts a new session. - """ - get_isolation_scope().start_session( - session_mode=session_mode, - ) - - def end_session(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.end_session` instead. - - Ends the current session if there is one. - """ - get_isolation_scope().end_session() - - def stop_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. - - Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - get_isolation_scope().stop_auto_session_tracking() - - def resume_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. - - Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - get_isolation_scope().resume_auto_session_tracking() - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.flush` instead. - - Alias for :py:meth:`sentry_sdk.client._Client.flush` - """ - return get_client().flush(timeout=timeout, callback=callback) - - def get_traceparent(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. - - Returns the traceparent either from the active span or from the scope. - """ - current_scope = get_current_scope() - traceparent = current_scope.get_traceparent() - - if traceparent is None: - isolation_scope = get_isolation_scope() - traceparent = isolation_scope.get_traceparent() - - return traceparent - - def get_baggage(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. - - Returns Baggage either from the active span or from the scope. - """ - current_scope = get_current_scope() - baggage = current_scope.get_baggage() - - if baggage is None: - isolation_scope = get_isolation_scope() - baggage = isolation_scope.get_baggage() - - if baggage is not None: - return baggage.serialize() - - return None - - def iter_trace_propagation_headers( - self, span: "Optional[Span]" = None - ) -> "Generator[Tuple[str, str], None, None]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. - - Return HTTP headers which allow propagation of trace data. Data taken - from the span representing the request, if available, or the current - span on the scope if not. - """ - return get_current_scope().iter_trace_propagation_headers( - span=span, - ) - - def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. - - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - return get_current_scope().trace_propagation_meta( - span=span, - ) - - -with _suppress_hub_deprecation_warning(): - # Suppress deprecation warning for the Hub here, since we still always - # import this module. - GLOBAL_HUB = Hub() -_local.set(GLOBAL_HUB) - - -# Circular imports -from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py deleted file mode 100644 index 677d34a81e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/__init__.py +++ /dev/null @@ -1,352 +0,0 @@ -from abc import ABC, abstractmethod -from threading import Lock -from typing import TYPE_CHECKING - -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union - - -_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) - - -_installer_lock = Lock() - -# Set of all integration identifiers we have attempted to install -_processed_integrations: "Set[str]" = set() - -# Set of all integration identifiers we have actually installed -_installed_integrations: "Set[str]" = set() - - -def _generate_default_integrations_iterator( - integrations: "List[str]", - auto_enabling_integrations: "List[str]", -) -> "Callable[[bool], Iterator[Type[Integration]]]": - def iter_default_integrations( - with_auto_enabling_integrations: bool, - ) -> "Iterator[Type[Integration]]": - """Returns an iterator of the default integration classes:""" - from importlib import import_module - - if with_auto_enabling_integrations: - all_import_strings = integrations + auto_enabling_integrations - else: - all_import_strings = integrations - - for import_string in all_import_strings: - try: - module, cls = import_string.rsplit(".", 1) - yield getattr(import_module(module), cls) - except (DidNotEnable, SyntaxError) as e: - logger.debug( - "Did not import default integration %s: %s", import_string, e - ) - - if isinstance(iter_default_integrations.__doc__, str): - for import_string in integrations: - iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) - - return iter_default_integrations - - -_DEFAULT_INTEGRATIONS = [ - # stdlib/base runtime integrations - "sentry_sdk.integrations.argv.ArgvIntegration", - "sentry_sdk.integrations.atexit.AtexitIntegration", - "sentry_sdk.integrations.dedupe.DedupeIntegration", - "sentry_sdk.integrations.excepthook.ExcepthookIntegration", - "sentry_sdk.integrations.logging.LoggingIntegration", - "sentry_sdk.integrations.modules.ModulesIntegration", - "sentry_sdk.integrations.stdlib.StdlibIntegration", - "sentry_sdk.integrations.threading.ThreadingIntegration", -] - -_AUTO_ENABLING_INTEGRATIONS = [ - "sentry_sdk.integrations.aiohttp.AioHttpIntegration", - "sentry_sdk.integrations.anthropic.AnthropicIntegration", - "sentry_sdk.integrations.ariadne.AriadneIntegration", - "sentry_sdk.integrations.arq.ArqIntegration", - "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", - "sentry_sdk.integrations.boto3.Boto3Integration", - "sentry_sdk.integrations.bottle.BottleIntegration", - "sentry_sdk.integrations.celery.CeleryIntegration", - "sentry_sdk.integrations.chalice.ChaliceIntegration", - "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", - "sentry_sdk.integrations.cohere.CohereIntegration", - "sentry_sdk.integrations.django.DjangoIntegration", - "sentry_sdk.integrations.falcon.FalconIntegration", - "sentry_sdk.integrations.fastapi.FastApiIntegration", - "sentry_sdk.integrations.flask.FlaskIntegration", - "sentry_sdk.integrations.gql.GQLIntegration", - "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", - "sentry_sdk.integrations.graphene.GrapheneIntegration", - "sentry_sdk.integrations.httpx.HttpxIntegration", - "sentry_sdk.integrations.httpx2.Httpx2Integration", - "sentry_sdk.integrations.huey.HueyIntegration", - "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", - "sentry_sdk.integrations.langchain.LangchainIntegration", - "sentry_sdk.integrations.langgraph.LanggraphIntegration", - "sentry_sdk.integrations.litestar.LitestarIntegration", - "sentry_sdk.integrations.loguru.LoguruIntegration", - "sentry_sdk.integrations.mcp.MCPIntegration", - "sentry_sdk.integrations.openai.OpenAIIntegration", - "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", - "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", - "sentry_sdk.integrations.pymongo.PyMongoIntegration", - "sentry_sdk.integrations.pyramid.PyramidIntegration", - "sentry_sdk.integrations.quart.QuartIntegration", - "sentry_sdk.integrations.redis.RedisIntegration", - "sentry_sdk.integrations.rq.RqIntegration", - "sentry_sdk.integrations.sanic.SanicIntegration", - "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", - "sentry_sdk.integrations.starlette.StarletteIntegration", - "sentry_sdk.integrations.starlite.StarliteIntegration", - "sentry_sdk.integrations.strawberry.StrawberryIntegration", - "sentry_sdk.integrations.tornado.TornadoIntegration", -] - -iter_default_integrations = _generate_default_integrations_iterator( - integrations=_DEFAULT_INTEGRATIONS, - auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, -) - -del _generate_default_integrations_iterator - - -_MIN_VERSIONS = { - "aiohttp": (3, 4), - "aiomysql": (0, 3, 0), - "anthropic": (0, 16), - "ariadne": (0, 20), - "arq": (0, 23), - "asyncpg": (0, 23), - "beam": (2, 12), - "boto3": (1, 16), # botocore - "bottle": (0, 12), - "celery": (4, 4, 7), - "chalice": (1, 16, 0), - "clickhouse_driver": (0, 2, 0), - "cohere": (5, 4, 0), - "django": (1, 8), - "dramatiq": (1, 9), - "falcon": (1, 4), - "fastapi": (0, 79, 0), - "flask": (1, 1, 4), - "gql": (3, 4, 1), - "graphene": (3, 3), - "google_genai": (1, 29, 0), # google-genai - "grpc": (1, 32, 0), # grpcio - "httpx": (0, 16, 0), - "httpx2": (2, 0, 0), - "huggingface_hub": (0, 24, 7), - "langchain": (0, 1, 0), - "langgraph": (0, 6, 6), - "launchdarkly": (9, 8, 0), - "litellm": (1, 77, 5), - "loguru": (0, 7, 0), - "mcp": (1, 15, 0), - "openai": (1, 0, 0), - "openai_agents": (0, 0, 19), - "openfeature": (0, 7, 1), - "pydantic_ai": (1, 0, 0), - "pymongo": (3, 5, 0), - "pyreqwest": (0, 11, 6), - "quart": (0, 16, 0), - "ray": (2, 7, 0), - "requests": (2, 0, 0), - "rq": (0, 6), - "sanic": (0, 8), - "sqlalchemy": (1, 2), - "starlette": (0, 16), - "starlite": (1, 48), - "statsig": (0, 55, 3), - "strawberry": (0, 209, 5), - "tornado": (6, 0), - "typer": (0, 15), - "unleash": (6, 0, 1), -} - - -_INTEGRATION_DEACTIVATES = { - "langchain": {"openai", "anthropic", "google_genai"}, - "openai_agents": {"openai"}, - "pydantic_ai": {"openai", "anthropic"}, -} - - -def setup_integrations( - integrations: "Sequence[Integration]", - with_defaults: bool = True, - with_auto_enabling_integrations: bool = False, - disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, - options: "Optional[Dict[str, Any]]" = None, -) -> "Dict[str, Integration]": - """ - Given a list of integration instances, this installs them all. - - When `with_defaults` is set to `True` all default integrations are added - unless they were already provided before. - - `disabled_integrations` takes precedence over `with_defaults` and - `with_auto_enabling_integrations`. - - Some integrations are designed to automatically deactivate other integrations - in order to avoid conflicts and prevent duplicate telemetry from being collected. - For example, enabling the `langchain` integration will auto-deactivate both the - `openai` and `anthropic` integrations. - - Users can override this behavior by: - - Explicitly providing an integration in the `integrations=[]` list, or - - Disabling the higher-level integration via the `disabled_integrations` option. - """ - integrations = dict( - (integration.identifier, integration) for integration in integrations or () - ) - - logger.debug("Setting up integrations (with default = %s)", with_defaults) - - user_provided_integrations = set(integrations.keys()) - - # Integrations that will not be enabled - disabled_integrations = [ - integration if isinstance(integration, type) else type(integration) - for integration in disabled_integrations or [] - ] - - # Integrations that are not explicitly set up by the user. - used_as_default_integration = set() - - if with_defaults: - for integration_cls in iter_default_integrations( - with_auto_enabling_integrations - ): - if integration_cls.identifier not in integrations: - instance = integration_cls() - integrations[instance.identifier] = instance - used_as_default_integration.add(instance.identifier) - - disabled_integration_identifiers = { - integration.identifier for integration in disabled_integrations - } - - for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): - if ( - integration in integrations - and integration not in disabled_integration_identifiers - ): - for target in targets_to_deactivate: - if target not in user_provided_integrations: - for cls in iter_default_integrations(True): - if cls.identifier == target: - if cls not in disabled_integrations: - disabled_integrations.append(cls) - logger.debug( - "Auto-deactivating %s integration because %s integration is active", - target, - integration, - ) - - for identifier, integration in integrations.items(): - with _installer_lock: - if identifier not in _processed_integrations: - if type(integration) in disabled_integrations: - logger.debug("Ignoring integration %s", identifier) - else: - logger.debug( - "Setting up previously not enabled integration %s", identifier - ) - try: - type(integration).setup_once() - integration.setup_once_with_options(options) - except DidNotEnable as e: - if identifier not in used_as_default_integration: - raise - - logger.debug( - "Did not enable default integration %s: %s", identifier, e - ) - else: - _installed_integrations.add(identifier) - - _processed_integrations.add(identifier) - - integrations = { - identifier: integration - for identifier, integration in integrations.items() - if identifier in _installed_integrations - } - - for identifier in integrations: - logger.debug("Enabling integration %s", identifier) - - return integrations - - -def _check_minimum_version( - integration: "type[Integration]", - version: "Optional[tuple[int, ...]]", - package: "Optional[str]" = None, -) -> None: - package = package or integration.identifier - - if version is None: - raise DidNotEnable(f"Unparsable {package} version.") - - min_version = _MIN_VERSIONS.get(integration.identifier) - if min_version is None: - return - - if version < min_version: - raise DidNotEnable( - f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." - ) - - -class DidNotEnable(Exception): # noqa: N818 - """ - The integration could not be enabled due to a trivial user error like - `flask` not being installed for the `FlaskIntegration`. - - This exception is silently swallowed for default integrations, but reraised - for explicitly enabled integrations. - """ - - -class Integration(ABC): - """Baseclass for all integrations. - - To accept options for an integration, implement your own constructor that - saves those options on `self`. - """ - - install = None - """Legacy method, do not implement.""" - - identifier: "str" = None # type: ignore[assignment] - """String unique ID of integration type""" - - @staticmethod - @abstractmethod - def setup_once() -> None: - """ - Initialize the integration. - - This function is only called once, ever. Configuration is not available - at this point, so the only thing to do here is to hook into exception - handlers, and perhaps do monkeypatches. - - Inside those hooks `Integration.current` can be used to access the - instance again. - """ - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - """ - Called after setup_once in rare cases on the instance and with options since we don't have those available above. - """ - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py deleted file mode 100644 index 7ff4657013..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_asgi_common.py +++ /dev/null @@ -1,187 +0,0 @@ -import urllib -from enum import Enum -from typing import TYPE_CHECKING - -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.scope import should_send_default_pii - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from typing_extensions import Literal - - from sentry_sdk.utils import AnnotatedValue - - -class _RootPathInPath(Enum): - EXCLUDED = "excluded" - EITHER = "either" - - -def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": - """ - Extract headers from the ASGI scope, in the format that the Sentry protocol expects. - """ - headers: "Dict[str, str]" = {} - for raw_key, raw_value in asgi_scope["headers"]: - key = raw_key.decode("latin-1") - value = raw_value.decode("latin-1") - if key in headers: - headers[key] = headers[key] + ", " + value - else: - headers[key] = value - - return headers - - -def _get_path( - asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" -) -> "str": - if root_path_in_path is _RootPathInPath.EXCLUDED: - return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") - - # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 - path = asgi_scope["path"] - root_path = asgi_scope.get("root_path", "") - - if not root_path or path == root_path or path.startswith(root_path + "/"): - return path - - return root_path + path - - -def _get_url( - asgi_scope: "Dict[str, Any]", - default_scheme: "Literal['ws', 'http']", - host: "Optional[Union[AnnotatedValue, str]]", - path: str, -) -> str: - """ - Extract URL from the ASGI scope, without also including the querystring. - """ - scheme = asgi_scope.get("scheme", default_scheme) - - server = asgi_scope.get("server", None) - - if host: - return "%s://%s%s" % (scheme, host, path) - - if server is not None: - host, port = server - default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) - if port != default_port: - return "%s://%s:%s%s" % (scheme, host, port, path) - return "%s://%s%s" % (scheme, host, path) - return path - - -def _get_query(asgi_scope: "Any") -> "Any": - """ - Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. - """ - qs = asgi_scope.get("query_string") - if not qs: - return None - return urllib.parse.unquote(qs.decode("latin-1")) - - -def _get_ip(asgi_scope: "Any") -> str: - """ - Extract IP Address from the ASGI scope based on request headers with fallback to scope client. - """ - headers = _get_headers(asgi_scope) - try: - return headers["x-forwarded-for"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return headers["x-real-ip"] - except KeyError: - pass - - return asgi_scope.get("client")[0] - - -def _get_request_data( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "Dict[str, Any]": - """ - Returns data related to the HTTP request from the ASGI scope. - """ - request_data: "Dict[str, Any]" = {} - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - request_data["method"] = asgi_scope.get("method") - - headers = _get_headers(asgi_scope) - - request_data["headers"] = _filter_headers( - headers, - use_annotated_value=False, - ) - - request_data["query_string"] = _get_query(asgi_scope) - - request_data["url"] = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} - - return request_data - - -def _get_request_attributes( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "dict[str, Any]": - """ - Return attributes related to the HTTP request from the ASGI scope. - """ - attributes: "dict[str, Any]" = {} - - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - if asgi_scope.get("method"): - attributes["http.request.method"] = asgi_scope["method"].upper() - - headers = _get_headers(asgi_scope) - - filtered_headers = _filter_headers(headers, use_annotated_value=False) - for header, value in filtered_headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - if should_send_default_pii(): - query = _get_query(asgi_scope) - if query: - attributes["http.query"] = query - - path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) - attributes["url.path"] = path - - url_without_query_string = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=path, - ) - query_string = _get_query(asgi_scope) - attributes["url.full"] = ( - f"{url_without_query_string}?{query_string}" - if query_string is not None - else url_without_query_string - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - ip = _get_ip(asgi_scope) - attributes["client.address"] = ip - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py deleted file mode 100644 index ad0ab7c734..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/_wsgi_common.py +++ /dev/null @@ -1,282 +0,0 @@ -import json -from contextlib import contextmanager -from copy import deepcopy - -import sentry_sdk -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE -from sentry_sdk.data_collection import _apply_key_value_collection_filtering -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger - -try: - from django.http.request import RawPostDataException - - _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) -except ImportError: - RawPostDataException = None - _RAW_DATA_EXCEPTIONS = (ValueError,) - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union - - from sentry_sdk._types import Event, HttpStatusCodeRange - - -SENSITIVE_ENV_KEYS = ( - "REMOTE_ADDR", - "HTTP_X_FORWARDED_FOR", - "HTTP_SET_COOKIE", - "HTTP_COOKIE", - "HTTP_AUTHORIZATION", - "HTTP_PROXY_AUTHORIZATION", - "HTTP_X_API_KEY", - "HTTP_X_FORWARDED_FOR", - "HTTP_X_REAL_IP", -) - -SENSITIVE_HEADERS = tuple( - x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") -) - -DEFAULT_HTTP_METHODS_TO_CAPTURE = ( - "CONNECT", - "DELETE", - "GET", - # "HEAD", # do not capture HEAD requests by default - # "OPTIONS", # do not capture OPTIONS requests by default - "PATCH", - "POST", - "PUT", - "TRACE", -) - - -# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support -@contextmanager -def nullcontext() -> "Iterator[None]": - yield - - -def request_body_within_bounds( - client: "Optional[sentry_sdk.client.BaseClient]", content_length: int -) -> bool: - if client is None: - return False - - bodies = client.options["max_request_body_size"] - return not ( - bodies == "never" - or (bodies == "small" and content_length > 10**3) - or (bodies == "medium" and content_length > 10**4) - ) - - -class RequestExtractor: - """ - Base class for request extraction. - """ - - # It does not make sense to make this class an ABC because it is not used - # for typing, only so that child classes can inherit common methods from - # it. Only some child classes implement all methods that raise - # NotImplementedError in this class. - - def __init__(self, request: "Any") -> None: - self.request = request - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - - content_length = self.content_length() - request_info = event.get("request", {}) - - if should_send_default_pii(): - request_info["cookies"] = dict(self.cookies()) - - if not request_body_within_bounds(client, content_length): - data = AnnotatedValue.removed_because_over_size_limit() - else: - # First read the raw body data - # It is important to read this first because if it is Django - # it will cache the body and then we can read the cached version - # again in parsed_body() (or json() or wherever). - raw_data = None - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # If DjangoRestFramework is used it already read the body for us - # so reading it here will fail. We can ignore this. - pass - - parsed_body = self.parsed_body() - if parsed_body is not None: - data = parsed_body - elif raw_data: - data = AnnotatedValue.removed_because_raw_data() - else: - data = None - - if data is not None: - request_info["data"] = data - - event["request"] = deepcopy(request_info) - - def content_length(self) -> int: - try: - return int(self.env().get("CONTENT_LENGTH", 0)) - except ValueError: - return 0 - - def cookies(self) -> "MutableMapping[str, Any]": - raise NotImplementedError() - - def raw_data(self) -> "Optional[Union[str, bytes]]": - raise NotImplementedError() - - def form(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - form = self.form() - except Exception: - form = None - try: - files = self.files() - except Exception: - files = None - - if form or files: - data = {} - if form: - data = dict(form.items()) - if files: - for key in files.keys(): - data[key] = AnnotatedValue.removed_because_raw_data() - - return data - - return self.json() - - def is_json(self) -> bool: - return _is_json_content_type(self.env().get("CONTENT_TYPE")) - - def json(self) -> "Optional[Any]": - try: - if not self.is_json(): - return None - - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # The body might have already been read, in which case this will - # fail - raw_data = None - - if raw_data is None: - return None - - if isinstance(raw_data, str): - return json.loads(raw_data) - else: - return json.loads(raw_data.decode("utf-8")) - except ValueError: - pass - - return None - - def files(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def size_of_file(self, file: "Any") -> int: - raise NotImplementedError() - - def env(self) -> "Dict[str, Any]": - raise NotImplementedError() - - -def _is_json_content_type(ct: "Optional[str]") -> bool: - mt = (ct or "").split(";", 1)[0] - return ( - mt == "application/json" - or (mt.startswith("application/")) - and mt.endswith("+json") - ) - - -def _filter_headers( - headers: "Mapping[str, str]", - use_annotated_value: bool = True, -) -> "Mapping[str, Union[AnnotatedValue, str]]": - client_options = sentry_sdk.get_client().options - - if has_data_collection_enabled(client_options): - data_collection_configuration = client_options["data_collection"] - - filtered = _apply_key_value_collection_filtering( - items=headers, - behaviour=data_collection_configuration["http_headers"]["request"], - ) - - for key in filtered: - if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): - filtered[key] = SENSITIVE_DATA_SUBSTITUTE - - return filtered - else: - if should_send_default_pii(): - return headers - - substitute: "Union[AnnotatedValue, str]" = ( - SENSITIVE_DATA_SUBSTITUTE - if not use_annotated_value - else AnnotatedValue.removed_because_over_size_limit() - ) - - return { - k: ( - v - if k.upper().replace("-", "_") not in SENSITIVE_HEADERS - else substitute - ) - for k, v in headers.items() - } - - -def _in_http_status_code_range( - code: object, code_ranges: "list[HttpStatusCodeRange]" -) -> bool: - for target in code_ranges: - if isinstance(target, int): - if code == target: - return True - continue - - try: - if code in target: - return True - except TypeError: - logger.warning( - "failed_request_status_codes has to be a list of integers or containers" - ) - - return False - - -class HttpCodeRangeContainer: - """ - Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. - Used for backwards compatibility with the old `failed_request_status_codes` option. - """ - - def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: - self._code_ranges = code_ranges - - def __contains__(self, item: object) -> bool: - return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py deleted file mode 100644 index 59bae92a60..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiohttp.py +++ /dev/null @@ -1,509 +0,0 @@ -import sys -import weakref -from functools import wraps - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import ( - _filter_headers, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - NoOpStreamedSpan, - SegmentSource, - SpanStatus, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import ( - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - parse_url, - parse_version, - reraise, - transaction_from_function, -) - -try: - import asyncio - - from aiohttp import ClientSession, TraceConfig - from aiohttp import __version__ as AIOHTTP_VERSION - from aiohttp.web import Application, HTTPException, UrlDispatcher -except ImportError: - raise DidNotEnable("AIOHTTP not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Set - from types import SimpleNamespace - from typing import Any, ContextManager, Optional, Tuple, Union - - from aiohttp import TraceRequestEndParams, TraceRequestStartParams - from aiohttp.web_request import Request - from aiohttp.web_urldispatcher import UrlMappingMatchInfo - - from sentry_sdk._types import Attributes, Event, EventProcessor - from sentry_sdk.tracing import Span - from sentry_sdk.utils import ExcInfo - - -TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") - - -class AioHttpIntegration(Integration): - identifier = "aiohttp" - origin = f"auto.http.{identifier}" - - def __init__( - self, - transaction_style: str = "handler_name", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self._failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(AIOHTTP_VERSION) - _check_minimum_version(AioHttpIntegration, version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - # In the aiohttp integration, all of their HTTP responses are Exceptions. - # Because they have to be raised and handled by the framework, we need to - # register the exceptions as control flow exceptions so that we don't - # accidentally overwrite a status of "ok" with "error". - _register_control_flow_exception(HTTPException) - - ignore_logger("aiohttp.server") - - old_handle = Application._handle - - async def sentry_app_handle( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(AioHttpIntegration) - if integration is None: - return await old_handle(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - # Scope data will not leak between requests because aiohttp - # create a task to wrap each request. - scope.generate_propagation_context() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - headers = dict(request.headers) - - span_ctx: "ContextManager[Union[Span, StreamedSpan]]" - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context({"aiohttp_request": request}) - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, - use_annotated_value=False, - ).items(): - header_attributes[ - f"http.request.header.{header.lower()}" - ] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - url_attributes = {} - if should_send_default_pii(): - url_attributes["url.full"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - url_attributes["url.path"] = request.path - - if request.query_string: - url_attributes["url.query"] = request.query_string - - client_address_attributes = {} - if should_send_default_pii() and request.remote: - client_address_attributes["client.address"] = request.remote - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, request.remote - ) - - span_ctx = sentry_sdk.traces.start_span( - # If this name makes it to the UI, AIOHTTP's URL - # resolver did not find a route or died trying. - name="generic AIOHTTP request", - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": AioHttpIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE.value, - "http.request.method": request.method, - **url_attributes, - **client_address_attributes, - **header_attributes, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # If this transaction name makes it to the UI, AIOHTTP's - # URL resolver did not find a route or died trying. - name="generic AIOHTTP request", - source=TransactionSource.ROUTE, - origin=AioHttpIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"aiohttp_request": request}, - ) - - with span_ctx as span: - try: - response = await old_handle(self, request) - except HTTPException as e: - if isinstance(span, StreamedSpan) and not isinstance( - span, NoOpStreamedSpan - ): - span.set_attribute( - "http.response.status_code", e.status_code - ) - - if e.status_code >= 400: - span.status = SpanStatus.ERROR.value - else: - span.status = SpanStatus.OK.value - else: - # Since a NoOpStreamedSpan can end up here, we have to guard against it - # so this only gets set in the legacy transaction approach. - if not isinstance(span, NoOpStreamedSpan): - span.set_http_status(e.status_code) - - if ( - e.status_code - in integration._failed_request_status_codes - ): - _capture_exception() - raise - except (asyncio.CancelledError, ConnectionResetError): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR.value - else: - span.set_status(SPANSTATUS.CANCELLED) - raise - except Exception: - # This will probably map to a 500 but seems like we - # have no way to tell. Do not set span status. - reraise(*_capture_exception()) - - try: - # A valid response handler will return a valid response with a status. But, if the handler - # returns an invalid response (e.g. None), the line below will raise an AttributeError. - # Even though this is likely invalid, we need to handle this case to ensure we don't break - # the application. - response_status = response.status - except AttributeError: - pass - else: - if isinstance(span, StreamedSpan): - span.set_attribute( - "http.response.status_code", response_status - ) - span.status = ( - SpanStatus.ERROR.value - if response_status >= 400 - else SpanStatus.OK.value - ) - else: - span.set_http_status(response_status) - - return response - - Application._handle = sentry_app_handle - - old_urldispatcher_resolve = UrlDispatcher.resolve - - @wraps(old_urldispatcher_resolve) - async def sentry_urldispatcher_resolve( - self: "UrlDispatcher", request: "Request" - ) -> "UrlMappingMatchInfo": - rv = await old_urldispatcher_resolve(self, request) - - integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) - if integration is None: - return rv - - name = None - - try: - if integration.transaction_style == "handler_name": - name = transaction_from_function(rv.handler) - elif integration.transaction_style == "method_and_path_pattern": - route_info = rv.get_info() - pattern = route_info.get("path") or route_info.get("formatter") - name = "{} {}".format(request.method, pattern) - except Exception: - pass - - if name is not None: - current_span = sentry_sdk.get_current_span() - if isinstance(current_span, StreamedSpan) and not isinstance( - current_span, NoOpStreamedSpan - ): - current_span._segment.name = name - current_span._segment.set_attribute( - "sentry.span.source", - SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, - ) - else: - current_scope = sentry_sdk.get_current_scope() - current_scope.set_transaction_name( - name, - source=SOURCE_FOR_STYLE[integration.transaction_style], - ) - - return rv - - UrlDispatcher.resolve = sentry_urldispatcher_resolve - - old_client_session_init = ClientSession.__init__ - - @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) - def init(*args: "Any", **kwargs: "Any") -> None: - client_trace_configs = list(kwargs.get("trace_configs") or ()) - trace_config = create_trace_config() - client_trace_configs.append(trace_config) - - kwargs["trace_configs"] = client_trace_configs - return old_client_session_init(*args, **kwargs) - - ClientSession.__init__ = init - - -def create_trace_config() -> "TraceConfig": - async def on_request_start( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestStartParams", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(AioHttpIntegration) is None: - return - - method = params.method.upper() - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(params.url), sanitize=False) - - span_name = "%s %s" % ( - method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ) - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(client.options): - attributes: "Attributes" = { - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": AioHttpIntegration.origin, - "http.request.method": method, - } - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - attributes["url.path"] = params.url.path - - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) - else: - legacy_span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=span_name, - origin=AioHttpIntegration.origin, - ) - legacy_span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - legacy_span.set_data("url", parsed_url.url) - legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span = legacy_span - - if should_propagate_trace(client, str(params.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=params.url - ) - ) - if key == BAGGAGE_HEADER_NAME and params.headers.get( - BAGGAGE_HEADER_NAME - ): - # do not overwrite any existing baggage, just append to it - params.headers[key] += "," + value - else: - params.headers[key] = value - - trace_config_ctx.span = span - - async def on_request_end( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestEndParams", - ) -> None: - if trace_config_ctx.span is None: - return - - span = trace_config_ctx.span - status = int(params.response.status) - - if isinstance(span, StreamedSpan): - span.set_attribute("http.response.status_code", status) - span.status = ( - SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value - ) - - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.set_http_status(status) - span.set_data("reason", params.response.reason) - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - trace_config = TraceConfig() - - trace_config.on_request_start.append(on_request_start) - trace_config.on_request_end.append(on_request_end) - - return trace_config - - -def _make_request_processor( - weak_request: "weakref.ReferenceType[Request]", -) -> "EventProcessor": - def aiohttp_processor( - event: "Event", - hint: "dict[str, Tuple[type, BaseException, Any]]", - ) -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - request_info = event.setdefault("request", {}) - - request_info["url"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote} - request_info["headers"] = _filter_headers(dict(request.headers)) - - # Just attach raw data here if it is within bounds, if available. - # Unfortunately there's no way to get structured data from aiohttp - # without awaiting on some coroutine. - request_info["data"] = get_aiohttp_request_data(request) - - return event - - return aiohttp_processor - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "aiohttp", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - return exc_info - - -BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" - - -def get_aiohttp_request_data( - request: "Request", -) -> "Union[Optional[str], AnnotatedValue]": - bytes_body = request._read_bytes - - if bytes_body is not None: - # we have body to show - if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): - return AnnotatedValue.substituted_because_over_size_limit() - - encoding = request.charset or "utf-8" - return bytes_body.decode(encoding, "replace") - - if request.can_read_body: - # body exists but we can't show it - return BODY_NOT_READ_MESSAGE - - # request has no body - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py deleted file mode 100644 index 49459268e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aiomysql.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import annotations - -from typing import Any, Awaitable, Callable, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import aiomysql # type: ignore[import-not-found] - from aiomysql.connection import Connection # type: ignore[import-not-found] - from aiomysql.cursors import Cursor # type: ignore[import-not-found] -except ImportError: - raise DidNotEnable("aiomysql not installed.") - - -class AioMySQLIntegration(Integration): - identifier = "aiomysql" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AioMySQLIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - aiomysql_version = parse_version(aiomysql.__version__) - _check_minimum_version(AioMySQLIntegration, aiomysql_version) - - Cursor.execute = _wrap_execute(Cursor.execute) - Cursor.executemany = _wrap_executemany(Cursor.executemany) - - # Patch Connection._connect — this catches ALL connections: - # - aiomysql.connect() - # - aiomysql.create_pool() (pool.py does `from .connection import connect` - # which ultimately calls Connection._connect) - # - Reconnects - Connection._connect = _wrap_connect(Connection._connect) - - -T = TypeVar("T") - - -def _normalize_query(query: str | bytes | bytearray) -> str: - if isinstance(query, (bytes, bytearray)): - query = query.decode("utf-8", errors="replace") - return " ".join(query.split()) - - -def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.execute to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - - # Skip if flagged by executemany (avoids double-recording). - # Do NOT reset the flag here — it must stay True for the entire - # duration of executemany, which may call execute multiple times - # in a loop (non-INSERT fallback). Only _wrap_executemany's - # finally block should clear it. - if getattr(cursor, "_sentry_skip_next_execute", False): - return await f(*args, **kwargs) - - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = params if integration and integration._record_params else None - param_style = "pyformat" if params_list else None - - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=False, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.executemany to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = ( - seq_of_params if integration and integration._record_params else None - ) - param_style = "pyformat" if params_list else None - - # Prevent double-recording: _do_execute_many calls self.execute internally - cursor._sentry_skip_next_execute = True - try: - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=True, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - finally: - cursor._sentry_skip_next_execute = False - - return _inner - - -def _get_connection(cursor: Any) -> Any: - """Get the underlying connection from a cursor.""" - return getattr(cursor, "connection", None) - - -def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Connection._connect to capture connection spans.""" - - async def _inner(self: "Connection") -> T: - client = sentry_sdk.get_client() - if client.get_integration(AioMySQLIntegration) is None: - return await f(self) - - if has_span_streaming_enabled(client.options): - breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) - - span_attributes: dict[str, Any] = { - "sentry.op": OP.DB, - "sentry.origin": AioMySQLIntegration.origin, - } | breadcrumb_data - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=breadcrumb_data - ) - res = await f(self) - else: - connect_data = _get_connect_data(self) - - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AioMySQLIntegration.origin, - ) as span: - _set_db_data(span, self) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", - category="query", - data=connect_data, - ) - res = await f(self) - - return res - - return _inner - - -def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: - if use_streaming_keys: - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - data: dict[str, Any] = { - db_system: "mysql", - SPANDATA.DB_DRIVER_NAME: "aiomysql", - } - - host = getattr(conn, "host", None) - if host is not None: - data[SPANDATA.SERVER_ADDRESS] = host - - port = getattr(conn, "port", None) - if port is not None: - data[SPANDATA.SERVER_PORT] = port - - database = getattr(conn, "db", None) - if database is not None: - data[db_name] = database - - user = getattr(conn, "user", None) - if user is not None: - data[SPANDATA.DB_USER] = user - - return data - - -def _set_db_data(span: Any, conn: Any) -> None: - """Set database-related span data from connection object.""" - if isinstance(span, StreamedSpan): - set_value = span.set_attribute - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - set_value = span.set_data - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - set_value(db_system, "mysql") - set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") - - host = getattr(conn, "host", None) - if host is not None: - set_value(SPANDATA.SERVER_ADDRESS, host) - - port = getattr(conn, "port", None) - if port is not None: - set_value(SPANDATA.SERVER_PORT, port) - - database = getattr(conn, "db", None) - if database is not None: - set_value(db_name, database) - - user = getattr(conn, "user", None) - if user is not None: - set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py deleted file mode 100644 index dfa4aef34c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/anthropic.py +++ /dev/null @@ -1,1154 +0,0 @@ -import json -import sys -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_anthropic_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - package_version, - reraise, - safe_serialize, -) - -try: - try: - from anthropic import NotGiven - except ImportError: - NotGiven = None - - try: - from anthropic import Omit - except ImportError: - Omit = None - - from anthropic import AsyncStream, Stream - from anthropic.lib.streaming import ( - AsyncMessageStream, - AsyncMessageStreamManager, - MessageStream, - MessageStreamManager, - ) - from anthropic.resources import AsyncMessages, Messages - from anthropic.types import ( - ContentBlockDeltaEvent, - ContentBlockStartEvent, - ContentBlockStopEvent, - MessageDeltaEvent, - MessageStartEvent, - MessageStopEvent, - ) - - if TYPE_CHECKING: - from anthropic.types import MessageStreamEvent, TextBlockParam -except ImportError: - raise DidNotEnable("Anthropic not installed") - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Iterator, - Optional, - Union, - ) - - from anthropic.types import ( - MessageParam, - ModelParam, - RawMessageStreamEvent, - TextBlockParam, - ToolUnionParam, - ) - - from sentry_sdk._types import TextPart - - -class _RecordedUsage: - output_tokens: int = 0 - input_tokens: int = 0 - cache_write_input_tokens: "Optional[int]" = 0 - cache_read_input_tokens: "Optional[int]" = 0 - - -class _StreamSpanContext: - """ - Sets accumulated data on the stream's span and finishes the span on exit. - Is a no-op if the stream has no span set, i.e., when the span has already been finished. - """ - - def __init__( - self, - stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", - # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). - guaranteed_streaming_state: bool = False, - ) -> None: - self._stream = stream - self._guaranteed_streaming_state = guaranteed_streaming_state - - def __enter__(self) -> "_StreamSpanContext": - return self - - def __exit__( - self, - exc_type: "Optional[type[BaseException]]", - exc_val: "Optional[BaseException]", - exc_tb: "Optional[Any]", - ) -> None: - with capture_internal_exceptions(): - if not hasattr(self._stream, "_span"): - return - - if not self._guaranteed_streaming_state and not hasattr( - self._stream, "_model" - ): - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - return - - _set_streaming_output_data( - span=self._stream._span, - integration=self._stream._integration, - model=self._stream._model, - usage=self._stream._usage, - content_blocks=self._stream._content_blocks, - response_id=self._stream._response_id, - finish_reason=self._stream._finish_reason, - ) - - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - - -class AnthropicIntegration(Integration): - identifier = "anthropic" - origin = f"auto.ai.{identifier}" - - def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - version = package_version("anthropic") - _check_minimum_version(AnthropicIntegration, version) - - """ - client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. - Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. - The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, - information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. - - The span can be finished in two places: - - When the user exits the context manager or directly calls close(), the patched close() finishes the span. - - When iteration ends, the finally block in the _iterator wrapper finishes the span. - - Both paths may run. For example, the context manager exit can follow iterator exhaustion. - """ - Messages.create = _wrap_message_create(Messages.create) - Stream.close = _wrap_close(Stream.close) - - AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) - AsyncStream.close = _wrap_async_close(AsyncStream.close) - - """ - client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. - """ - Messages.stream = _wrap_message_stream(Messages.stream) - MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( - MessageStreamManager.__enter__ - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. - if not issubclass(MessageStream, Stream): - MessageStream.close = _wrap_close(MessageStream.close) - - AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) - AsyncMessageStreamManager.__aenter__ = ( - _wrap_async_message_stream_manager_aenter( - AsyncMessageStreamManager.__aenter__ - ) - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. - if not issubclass(AsyncMessageStream, AsyncStream): - AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "anthropic", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": - """ - Get token usage from the Anthropic response. - Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) - """ - input_tokens = 0 - output_tokens = 0 - cache_read_input_tokens = 0 - cache_write_input_tokens = 0 - if hasattr(result, "usage"): - usage = result.usage - if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): - input_tokens = usage.input_tokens - if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): - output_tokens = usage.output_tokens - if hasattr(usage, "cache_read_input_tokens") and isinstance( - usage.cache_read_input_tokens, int - ): - cache_read_input_tokens = usage.cache_read_input_tokens - if hasattr(usage, "cache_creation_input_tokens") and isinstance( - usage.cache_creation_input_tokens, int - ): - cache_write_input_tokens = usage.cache_creation_input_tokens - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens so downstream cost calculations - # (input_tokens - cached) don't produce negative values. - input_tokens += cache_read_input_tokens + cache_write_input_tokens - - return ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) - - -def _collect_ai_data( - event: "MessageStreamEvent", - model: "str | None", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": - """ - Collect model information, token usage, and collect content blocks from the AI streaming response. - """ - with capture_internal_exceptions(): - if hasattr(event, "type"): - if event.type == "content_block_start": - pass - elif event.type == "content_block_delta": - if hasattr(event.delta, "text"): - content_blocks.append(event.delta.text) - elif hasattr(event.delta, "partial_json"): - content_blocks.append(event.delta.partial_json) - elif event.type == "content_block_stop": - pass - - # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. - # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 - if event.type == "message_start": - model = event.message.model or model - response_id = event.message.id - - incoming_usage = event.message.usage - usage.output_tokens = incoming_usage.output_tokens - usage.input_tokens = incoming_usage.input_tokens - - usage.cache_write_input_tokens = getattr( - incoming_usage, "cache_creation_input_tokens", None - ) - usage.cache_read_input_tokens = getattr( - incoming_usage, "cache_read_input_tokens", None - ) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - # Counterintuitive, but message_delta contains cumulative token counts :) - if event.type == "message_delta": - usage.output_tokens = event.usage.output_tokens - - # Update other usage fields if they exist in the event - input_tokens = getattr(event.usage, "input_tokens", None) - if input_tokens is not None: - usage.input_tokens = input_tokens - - cache_creation_input_tokens = getattr( - event.usage, "cache_creation_input_tokens", None - ) - if cache_creation_input_tokens is not None: - usage.cache_write_input_tokens = cache_creation_input_tokens - - cache_read_input_tokens = getattr( - event.usage, "cache_read_input_tokens", None - ) - if cache_read_input_tokens is not None: - usage.cache_read_input_tokens = cache_read_input_tokens - # TODO: Record event.usage.server_tool_use - - if event.delta.stop_reason is not None: - finish_reason = event.delta.stop_reason - - return (model, usage, content_blocks, response_id, finish_reason) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - -def _transform_anthropic_content_block( - content_block: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Transform an Anthropic content block using the Anthropic-specific transformer, - with special handling for Anthropic's text-type documents. - """ - # Handle Anthropic's text-type documents specially (not covered by shared function) - if content_block.get("type") == "document": - source = content_block.get("source") - if isinstance(source, dict) and source.get("type") == "text": - return { - "type": "text", - "text": source.get("data", ""), - } - - # Use Anthropic-specific transformation - result = transform_anthropic_content_part(content_block) - return result if result is not None else content_block - - -def _transform_system_instructions( - system_instructions: "Union[str, Iterable[TextBlockParam]]", -) -> "list[TextPart]": - if isinstance(system_instructions, str): - return [ - { - "type": "text", - "content": system_instructions, - } - ] - - return [ - { - "type": "text", - "content": instruction["text"], - } - for instruction in system_instructions - if isinstance(instruction, dict) and "text" in instruction - ] - - -def _set_common_input_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - max_tokens: "int", - messages: "Iterable[MessageParam]", - model: "ModelParam", - system: "Optional[Union[str, Iterable[TextBlockParam]]]", - temperature: "Optional[float]", - top_k: "Optional[int]", - top_p: "Optional[float]", - tools: "Optional[Iterable[ToolUnionParam]]", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if ( - messages is not None - and len(messages) > 0 # type: ignore - and should_send_default_pii() - and integration.include_prompts - ): - if isinstance(system, str) or isinstance(system, Iterable): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system)), - ) - - normalized_messages = [] - for message in messages: - if ( - message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER - and "content" in message - and isinstance(message["content"], (list, tuple)) - ): - transformed_content = [] - for item in message["content"]: - # Skip tool_result items - they can contain images/documents - # with nested structures that are difficult to redact properly - if isinstance(item, dict) and item.get("type") == "tool_result": - continue - - # Transform content blocks (images, documents, etc.) - transformed_content.append( - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - ) - - # If there are non-tool-result items, add them as a message - if transformed_content: - normalized_messages.append( - { - "role": message.get("role"), - "content": transformed_content, - } - ) - else: - # Transform content for non-list messages or assistant messages - transformed_message = message.copy() - if "content" in transformed_message: - content = transformed_message["content"] - if isinstance(content, (list, tuple)): - transformed_message["content"] = [ - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - for item in content - ] - normalized_messages.append(transformed_message) - - role_normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(role_normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else role_normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - if model is not None and _is_given(model): - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - if top_k is not None and _is_given(top_k): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore - set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) - - -def _set_create_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "AnthropicIntegration", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=kwargs.get("max_tokens"), # type: ignore - messages=kwargs.get("messages"), # type: ignore - model=kwargs.get("model"), - system=kwargs.get("system"), - temperature=kwargs.get("temperature"), - top_k=kwargs.get("top_k"), - top_p=kwargs.get("top_p"), - tools=kwargs.get("tools"), - ) - - -def _wrap_synchronous_message_iterator( - stream: "Union[Stream, MessageStream]", - iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -async def _wrap_asynchronous_message_iterator( - stream: "Union[AsyncStream, AsyncMessageStream]", - iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - async for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -def _set_output_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - model: "str | None", - input_tokens: "int | None", - output_tokens: "int | None", - cache_read_input_tokens: "int | None", - cache_write_input_tokens: "int | None", - content_blocks: "list[Any]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> None: - """ - Set output data for the span based on the AI response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - if finish_reason is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in content_blocks: - if output["type"] == "text": - output_messages["response"].append(output["text"]) - elif output["type"] == "tool_use": - output_messages["tool"].append(output) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - record_token_usage( - span, - input_tokens=input_tokens, - output_tokens=output_tokens, - input_tokens_cached=cache_read_input_tokens, - input_tokens_cache_write=cache_write_input_tokens, - ) - - -def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, Stream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_synchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -async def _sentry_patched_create_async( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return await f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, AsyncStream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_asynchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -def _wrap_message_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return _sentry_patched_create_sync(f, *args, **kwargs) - - return _sentry_wrapped_create_sync - - -def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: - """ - Initialize fields for accumulating output on the Stream instance. - """ - if not hasattr(stream, "_model"): - stream._model = None - stream._usage = _RecordedUsage() - stream._content_blocks = [] - stream._response_id = None - stream._finish_reason = None - - -def _accumulate_event_data( - stream: "Union[Stream, MessageStream]", - event: "Union[RawMessageStreamEvent, MessageStreamEvent]", -) -> None: - """ - Update accumulated output from a single stream event. - """ - (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( - event, - stream._model, - stream._usage, - stream._content_blocks, - stream._response_id, - stream._finish_reason, - ) - - stream._model = model - stream._usage = usage - stream._content_blocks = content_blocks - stream._response_id = response_id - stream._finish_reason = finish_reason - - -def _set_streaming_output_data( - span: "Span", - integration: "AnthropicIntegration", - model: "Optional[str]", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "Optional[str]", - finish_reason: "Optional[str]", -) -> None: - """ - Set output attributes on the AI Client Span. - """ - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - response_id=response_id, - finish_reason=finish_reason, - ) - - -def _wrap_close( - f: "Callable[..., None]", -) -> "Callable[..., None]": - """ - Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. - """ - - def close(self: "Union[Stream, MessageStream]") -> None: - with _StreamSpanContext(self): - return f(self) - - return close - - -def _wrap_message_create_async(f: "Any") -> "Any": - @wraps(f) - async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return await _sentry_patched_create_async(f, *args, **kwargs) - - return _sentry_wrapped_create_async - - -def _wrap_async_close( - f: "Callable[..., Awaitable[None]]", -) -> "Callable[..., Awaitable[None]]": - """ - Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. - """ - - async def close(self: "AsyncStream") -> None: - with _StreamSpanContext(self): - return await f(self) - - return close - - -def _wrap_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_message_stream_manager_enter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": - if not hasattr(self, "_max_tokens"): - return f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return f(self) - - if self._messages is None: - return f(self) - - try: - iter(self._messages) - except TypeError: - return f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_synchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_enter - - -def _wrap_async_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream( - *args: "Any", **kwargs: "Any" - ) -> "AsyncMessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - async def _sentry_patched_aenter( - self: "AsyncMessageStreamManager", - ) -> "AsyncMessageStream": - if not hasattr(self, "_max_tokens"): - return await f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return await f(self) - - if self._messages is None: - return await f(self) - - try: - iter(self._messages) - except TypeError: - return await f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = await f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_asynchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_aenter - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different anthropic versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py deleted file mode 100644 index 0215ffa093..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/argv.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class ArgvIntegration(Integration): - identifier = "argv" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: - extra = event.setdefault("extra", {}) - # If some event processor decided to set extra to e.g. an - # `int`, don't crash. Not here. - if isinstance(extra, dict): - extra["sys.argv"] = sys.argv - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py deleted file mode 100644 index 1ce228d3a5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ariadne.py +++ /dev/null @@ -1,167 +0,0 @@ -from importlib import import_module - -import sentry_sdk -from sentry_sdk import capture_event, get_client -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - # importing like this is necessary due to name shadowing in ariadne - # (ariadne.graphql is also a function) - ariadne_graphql = import_module("ariadne.graphql") -except ImportError: - raise DidNotEnable("ariadne is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional - - from ariadne.types import ( # type: ignore - GraphQLError, - GraphQLResult, - GraphQLSchema, - QueryParser, - ) - from graphql.language.ast import DocumentNode - - from sentry_sdk._types import Event, EventProcessor - - -class AriadneIntegration(Integration): - identifier = "ariadne" - - @staticmethod - def setup_once() -> None: - version = package_version("ariadne") - _check_minimum_version(AriadneIntegration, version) - - ignore_logger("ariadne") - - _patch_graphql() - - -def _patch_graphql() -> None: - old_parse_query = ariadne_graphql.parse_query - old_handle_errors = ariadne_graphql.handle_graphql_errors - old_handle_query_result = ariadne_graphql.handle_query_result - - @ensure_integration_enabled(AriadneIntegration, old_parse_query) - def _sentry_patched_parse_query( - context_value: "Optional[Any]", - query_parser: "Optional[QueryParser]", - data: "Any", - ) -> "DocumentNode": - event_processor = _make_request_event_processor(data) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - result = old_parse_query(context_value, query_parser, data) - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_errors) - def _sentry_patched_handle_graphql_errors( - errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - result = old_handle_errors(errors, *args, **kwargs) - - event_processor = _make_response_event_processor(result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) - def _sentry_patched_handle_query_result( - result: "Any", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - query_result = old_handle_query_result(result, *args, **kwargs) - - event_processor = _make_response_event_processor(query_result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return query_result - - ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore - ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore - ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore - - -def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": - """Add request data and api_target to events.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - if not isinstance(data, dict): - return event - - with capture_internal_exceptions(): - try: - content_length = int( - (data.get("headers") or {}).get("Content-Length", 0) - ) - except (TypeError, ValueError): - return event - - if should_send_default_pii() and request_body_within_bounds( - get_client(), content_length - ): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - request_info["data"] = data - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - return inner - - -def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": - """Add response data to the event's response context.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii() and response.get("errors"): - contexts = event.setdefault("contexts", {}) - contexts["response"] = { - "data": response, - } - - return event - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py deleted file mode 100644 index da03bafb8b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/arq.py +++ /dev/null @@ -1,277 +0,0 @@ -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Transaction, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -try: - import arq.worker - from arq.connections import ArqRedis - from arq.version import VERSION as ARQ_VERSION - from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker -except ImportError: - raise DidNotEnable("Arq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from arq.cron import CronJob - from arq.jobs import Job - from arq.typing import WorkerCoroutine - from arq.worker import Function - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) - - -class ArqIntegration(Integration): - identifier = "arq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - try: - if isinstance(ARQ_VERSION, str): - version = parse_version(ARQ_VERSION) - else: - version = ARQ_VERSION.version[:2] - - except (TypeError, ValueError): - version = None - - _check_minimum_version(ArqIntegration, version) - - patch_enqueue_job() - patch_run_job() - patch_create_worker() - - _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore - - ignore_logger("arq.worker") - - -def patch_enqueue_job() -> None: - old_enqueue_job = ArqRedis.enqueue_job - original_kwdefaults = old_enqueue_job.__kwdefaults__ - - async def _sentry_enqueue_job( - self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" - ) -> "Optional[Job]": - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_enqueue_job(self, function, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=function, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_ARQ, - "sentry.origin": ArqIntegration.origin, - }, - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults - ArqRedis.enqueue_job = _sentry_enqueue_job - - -def patch_run_job() -> None: - old_run_job = Worker.run_job - - async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_run_job(self, job_id, score) - - with sentry_sdk.isolation_scope() as scope: - scope._name = "arq" - scope.clear_breadcrumbs() - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name="unknown arq task", - attributes={ - "sentry.op": OP.QUEUE_TASK_ARQ, - "sentry.origin": ArqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job_id, - }, - parent_span=None, - ): - return await old_run_job(self, job_id, score) - - transaction = Transaction( - name="unknown arq task", - status="ok", - op=OP.QUEUE_TASK_ARQ, - source=TransactionSource.TASK, - origin=ArqIntegration.origin, - ) - - with sentry_sdk.start_transaction(transaction): - return await old_run_job(self, job_id, score) - - Worker.run_job = _sentry_run_job - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - - if scope.transaction is not None: - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - scope.transaction.set_status(SPANSTATUS.ABORTED) - return - - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": ArqIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - if scope.transaction is not None: - scope.transaction.name = ctx["job_name"] - event["transaction"] = ctx["job_name"] - - tags = event.setdefault("tags", {}) - tags["arq_task_id"] = ctx["job_id"] - tags["arq_task_retry"] = ctx["job_try"] > 1 - extra = event.setdefault("extra", {}) - extra["arq-job"] = { - "task": ctx["job_name"], - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": ctx["job_try"], - } - - return event - - return event_processor - - -def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": - async def _sentry_coroutine( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(ArqIntegration) - if integration is None: - return await coroutine(ctx, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - scope = sentry_sdk.get_current_scope() - span = scope.streamed_span - if span is not None: - span.name = name - - scope.set_transaction_name(name) - - sentry_sdk.get_isolation_scope().add_event_processor( - _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) - ) - - try: - result = await coroutine(ctx, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_coroutine - - -def patch_create_worker() -> None: - old_create_worker = arq.worker.create_worker - - @ensure_integration_enabled(ArqIntegration, old_create_worker) - def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": - settings_cls = args[0] if args else kwargs.get("settings_cls") - - if isinstance(settings_cls, dict): - if "functions" in settings_cls: - settings_cls["functions"] = [ - _get_arq_function(func) - for func in settings_cls.get("functions", []) - ] - if "cron_jobs" in settings_cls: - settings_cls["cron_jobs"] = [ - _get_arq_cron_job(cron_job) - for cron_job in settings_cls.get("cron_jobs", []) - ] - - if hasattr(settings_cls, "functions"): - settings_cls.functions = [ # type: ignore[union-attr] - _get_arq_function(func) - for func in settings_cls.functions # type: ignore[union-attr] - ] - if hasattr(settings_cls, "cron_jobs"): - settings_cls.cron_jobs = [ # type: ignore[union-attr] - _get_arq_cron_job(cron_job) - for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] - ] - - if "functions" in kwargs: - kwargs["functions"] = [ - _get_arq_function(func) for func in kwargs.get("functions", []) - ] - if "cron_jobs" in kwargs: - kwargs["cron_jobs"] = [ - _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) - ] - - return old_create_worker(*args, **kwargs) - - arq.worker.create_worker = _sentry_create_worker - - -def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": - arq_func = arq.worker.func(func) - arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) - - return arq_func - - -def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": - cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) - - return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py deleted file mode 100644 index 8b1ff5e2a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asgi.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -An ASGI middleware. - -Based on Tom Christie's `sentry-asgi `. -""" - -import inspect -import sys -from copy import deepcopy -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._asgi_common import ( - _get_headers, - _get_ip, - _get_path, - _get_request_attributes, - _get_request_data, - _get_url, - _RootPathInPath, -) -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - SegmentSource, - StreamedSpan, -) -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - Transaction, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, - _get_installed_modules, - capture_internal_exceptions, - event_from_exception, - logger, - qualname_from_function, - reraise, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, ContextManager, Dict, Optional, Tuple, Union - - from sentry_sdk._types import Attributes, Event, Hint - from sentry_sdk.tracing import Span - - -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") - -_DEFAULT_TRANSACTION_NAME = "generic ASGI request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 -if sys.version_info >= (3, 14): - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": mechanism_type, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _looks_like_asgi3(app: "Any") -> bool: - """ - Try to figure out if an application object supports ASGI3. - - This is how uvicorn figures out the application version as well. - """ - if inspect.isclass(app): - return hasattr(app, "__await__") - elif inspect.isfunction(app): - return iscoroutinefunction(app) - else: - call = getattr(app, "__call__", None) # noqa - return iscoroutinefunction(call) - - -class SentryAsgiMiddleware: - __slots__ = ( - "app", - "__call__", - "transaction_style", - "mechanism_type", - "span_origin", - "http_methods_to_capture", - "root_path_in_path", - ) - - def __init__( - self, - app: "Any", - unsafe_context_data: bool = False, - transaction_style: str = "endpoint", - mechanism_type: str = "asgi", - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - asgi_version: "Optional[int]" = None, - root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, - ) -> None: - """ - Instrument an ASGI application with Sentry. Provides HTTP/websocket - data to sent events and basic handling for exceptions bubbling up - through the middleware. - - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. - """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - - asgi_middleware_while_using_starlette_or_fastapi = ( - mechanism_type == "asgi" and "starlette" in _get_installed_modules() - ) - if asgi_middleware_while_using_starlette_or_fastapi: - logger.warning( - "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " - "Please remove 'SentryAsgiMiddleware' from your project. " - "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." - ) - - self.transaction_style = transaction_style - self.mechanism_type = mechanism_type - self.span_origin = span_origin - self.app = app - self.http_methods_to_capture = http_methods_to_capture - self.root_path_in_path = root_path_in_path - - if asgi_version is None: - if _looks_like_asgi3(app): - asgi_version = 3 - else: - asgi_version = 2 - - if asgi_version == 3: - self.__call__ = self._run_asgi3 - elif asgi_version == 2: - self.__call__ = self._run_asgi2 # type: ignore - - def _capture_lifespan_exception(self, exc: Exception) -> None: - """Capture exceptions raise in application lifespan handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _capture_request_exception(self, exc: Exception) -> None: - """Capture exceptions raised in incoming request handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _run_asgi2(self, scope: "Any") -> "Any": - async def inner(receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=2) - - return inner - - async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=3) - - async def _run_app( - self, scope: "Any", receive: "Any", send: "Any", asgi_version: int - ) -> "Any": - is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) - is_lifespan = scope["type"] == "lifespan" - if is_recursive_asgi_middleware or is_lifespan: - try: - if asgi_version == 2: - return await self.app(scope)(receive, send) - else: - return await self.app(scope, receive, send) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_lifespan_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_lifespan_exception(exc) - reraise(*exc_info) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _asgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as sentry_scope: - with track_session(sentry_scope, session_mode="request"): - sentry_scope.clear_breadcrumbs() - sentry_scope._name = "asgi" - processor = partial(self.event_processor, asgi_scope=scope) - sentry_scope.add_event_processor(processor) - - ty = scope["type"] - ( - transaction_name, - transaction_source, - ) = self._get_transaction_name_and_source( - self.transaction_style, - scope, - ) - - method = scope.get("method", "").upper() - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - if span_streaming: - segment: "Optional[StreamedSpan]" = None - attributes: "Attributes" = { - "sentry.span.source": getattr( - transaction_source, "value", transaction_source - ), - "sentry.origin": self.span_origin, - "network.protocol.name": ty, - } - - if scope.get("client") and should_send_default_pii(): - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) - - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - sentry_sdk.traces.continue_trace(_get_headers(scope)) - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = f"{ty}.server" - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - else: - sentry_sdk.traces.new_trace() - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = OP.HTTP_SERVER - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - - span_ctx = segment or nullcontext() - - else: - transaction = None - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - transaction = continue_trace( - _get_headers(scope), - op="{}.server".format(ty), - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - - if transaction: - transaction.set_tag("asgi.type", ty) - - span_ctx = ( - sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"asgi_scope": scope}, - ) - if transaction is not None - else nullcontext() - ) - - with span_ctx as span: - if isinstance(span, StreamedSpan): - for attribute, value in _get_request_attributes( - scope, - root_path_in_path=self.root_path_in_path, - ).items(): - span.set_attribute(attribute, value) - - try: - - async def _sentry_wrapped_send( - event: "Dict[str, Any]", - ) -> "Any": - if span is not None: - is_http_response = ( - event.get("type") == "http.response.start" - and "status" in event - ) - if is_http_response: - if isinstance(span, StreamedSpan): - span.status = ( - "error" - if event["status"] >= 400 - else "ok" - ) - span.set_attribute( - "http.response.status_code", - event["status"], - ) - else: - span.set_http_status(event["status"]) - - return await send(event) - - if asgi_version == 2: - return await self.app(scope)( - receive, _sentry_wrapped_send - ) - else: - return await self.app( - scope, receive, _sentry_wrapped_send - ) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_request_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_request_exception(exc) - reraise(*exc_info) - - finally: - if isinstance(span, StreamedSpan): - already_set = ( - span is not None - and span.name != _DEFAULT_TRANSACTION_NAME - and span.get_attributes().get("sentry.span.source") - in [ - SegmentSource.COMPONENT.value, - SegmentSource.ROUTE.value, - SegmentSource.CUSTOM.value, - ] - ) - with capture_internal_exceptions(): - if not already_set: - name, source = ( - self._get_segment_name_and_source( - self.transaction_style, scope - ) - ) - span.name = name - span.set_attribute("sentry.span.source", source) - finally: - _asgi_middleware_applied.set(False) - - def event_processor( - self, event: "Event", hint: "Hint", asgi_scope: "Any" - ) -> "Optional[Event]": - request_data = event.get("request", {}) - request_data.update( - _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) - ) - event["request"] = deepcopy(request_data) - - # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) - transaction = event.get("transaction") - transaction_source = (event.get("transaction_info") or {}).get("source") - already_set = ( - transaction is not None - and transaction != _DEFAULT_TRANSACTION_NAME - and transaction_source - in [ - TransactionSource.COMPONENT, - TransactionSource.ROUTE, - TransactionSource.CUSTOM, - ] - ) - if not already_set: - name, source = self._get_transaction_name_and_source( - self.transaction_style, asgi_scope - ) - event["transaction"] = name - event["transaction_info"] = {"source": source} - - return event - - # Helper functions. - # - # Note: Those functions are not public API. If you want to mutate request - # data to your liking it's recommended to use the `before_send` callback - # for that. - - def _get_transaction_name_and_source( - self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SOURCE_FOR_STYLE[transaction_style] - ty = asgi_scope.get("type") - - if transaction_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = transaction_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - elif transaction_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - return name, source - - return name, source - - def _get_segment_name_and_source( - self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value - ty = asgi_scope.get("type") - - if segment_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = qualname_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - elif segment_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = SegmentSource.ROUTE.value - return name, source - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py deleted file mode 100644 index 4b3f6d330e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncio.py +++ /dev/null @@ -1,280 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import AsyncHttpTransport -from sentry_sdk.utils import ( - event_from_exception, - is_internal_task, - logger, - reraise, -) - -try: - import asyncio - from asyncio.tasks import Task -except ImportError: - raise DidNotEnable("asyncio not available") - -from typing import TYPE_CHECKING, Optional, Union - -if TYPE_CHECKING: - from collections.abc import Coroutine - from typing import Any, Callable, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T", bound=Callable[..., Any]) - - -def get_name(coro: "Any") -> str: - return ( - getattr(coro, "__qualname__", None) - or getattr(coro, "__name__", None) - or "coroutine without __name__" - ) - - -def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": - # Only __name__ and __qualname__ are copied from function to coroutine in CPython - return functools.partial( - functools.update_wrapper, - wrapped=wrapped, # type: ignore - assigned=("__name__", "__qualname__"), - updated=(), - ) - - -def patch_loop_close() -> None: - """Patch loop.close to flush pending events before shutdown.""" - # Atexit shutdown hook happens after the event loop is closed. - # Therefore, it is necessary to patch the loop.close method to ensure - # that pending events are flushed before the interpreter shuts down. - try: - loop = asyncio.get_running_loop() - except RuntimeError: - # No running loop → cannot patch now - return - - if getattr(loop, "_sentry_flush_patched", False): - return - - async def _flush() -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - try: - if not isinstance(client.transport, AsyncHttpTransport): - return - - await client.close_async() - except Exception: - logger.warning("Sentry flush failed during loop shutdown", exc_info=True) - - orig_close = loop.close - - def _patched_close() -> None: - try: - loop.run_until_complete(_flush()) - except Exception: - logger.debug( - "Could not flush Sentry events during loop close", exc_info=True - ) - finally: - orig_close() - - loop.close = _patched_close # type: ignore - loop._sentry_flush_patched = True # type: ignore - - -def _create_task_with_factory( - orig_task_factory: "Any", - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", -) -> "asyncio.Task[Any]": - task = None - - # Trying to use user set task factory (if there is one) - if orig_task_factory: - task = orig_task_factory(loop, coro, **kwargs) - - if task is None: - # The default task factory in `asyncio` does not have its own function - # but is just a couple of lines in `asyncio.base_events.create_task()` - # Those lines are copied here. - - # WARNING: - # If the default behavior of the task creation in asyncio changes, - # this will break! - task = Task(coro, loop=loop, **kwargs) - if task._source_traceback: # type: ignore - del task._source_traceback[-1] # type: ignore - - return task - - -def patch_asyncio() -> None: - orig_task_factory = None - try: - loop = asyncio.get_running_loop() - orig_task_factory = loop.get_task_factory() - - # Check if already patched - if getattr(orig_task_factory, "_is_sentry_task_factory", False): - return - - def _sentry_task_factory( - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", - ) -> "asyncio.Future[Any]": - # Check if this is an internal Sentry task - if is_internal_task(): - return _create_task_with_factory( - orig_task_factory, loop, coro, **kwargs - ) - - @_wrap_coroutine(coro) - async def _task_with_sentry_span_creation() -> "Any": - result = None - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncioIntegration) - task_spans = integration.task_spans if integration else False - - span_ctx: "Optional[Union[StreamedSpan, Span]]" = None - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope(): - if task_spans: - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=get_name(coro), - attributes={ - "sentry.op": OP.FUNCTION, - "sentry.origin": AsyncioIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.FUNCTION, - name=get_name(coro), - origin=AsyncioIntegration.origin, - ) - - with span_ctx if span_ctx else nullcontext(): - try: - result = await coro - except StopAsyncIteration as e: - raise e from None - except Exception: - reraise(*_capture_exception()) - - return result - - task = _create_task_with_factory( - orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs - ) - - # Set the task name to include the original coroutine's name - try: - task.set_name(f"{get_name(coro)} (Sentry-wrapped)") - except AttributeError: - # set_name might not be available in all Python versions - pass - - return task - - _sentry_task_factory._is_sentry_task_factory = True # type: ignore - loop.set_task_factory(_sentry_task_factory) # type: ignore - - except RuntimeError: - # When there is no running loop, we have nothing to patch. - logger.warning( - "There is no running asyncio loop so there is nothing Sentry can patch. " - "Please make sure you call sentry_sdk.init() within a running " - "asyncio loop for the AsyncioIntegration to work. " - "See https://docs.sentry.io/platforms/python/integrations/asyncio/" - ) - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - - integration = client.get_integration(AsyncioIntegration) - if integration is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "asyncio", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class AsyncioIntegration(Integration): - identifier = "asyncio" - origin = f"auto.function.{identifier}" - - def __init__(self, task_spans: bool = True) -> None: - self.task_spans = task_spans - - @staticmethod - def setup_once() -> None: - patch_asyncio() - patch_loop_close() - - -def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: - """ - Enable AsyncioIntegration with the provided options. - - This is useful in scenarios where Sentry needs to be initialized before - an event loop is set up, but you still want to instrument asyncio once there - is an event loop. In that case, you can sentry_sdk.init() early on without - the AsyncioIntegration and then, once the event loop has been set up, - execute: - - ```python - from sentry_sdk.integrations.asyncio import enable_asyncio_integration - - async def async_entrypoint(): - enable_asyncio_integration() - ``` - - Any arguments provided will be passed to AsyncioIntegration() as is. - - If AsyncioIntegration has already patched the current event loop, this - function won't have any effect. - - If AsyncioIntegration was provided in - sentry_sdk.init(disabled_integrations=[...]), this function will ignore that - and the integration will be enabled. - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # This function purposefully bypasses the integration machinery in - # integrations/__init__.py. _installed_integrations/_processed_integrations - # is used to prevent double patching the same module, but in the case of - # the AsyncioIntegration, we don't monkeypatch the standard library directly, - # we patch the currently running event loop, and we keep the record of doing - # that on the loop itself. - logger.debug("Setting up integration asyncio") - - integration = AsyncioIntegration(*args, **kwargs) - integration.setup_once() - - if "asyncio" not in client.integrations: - client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py deleted file mode 100644 index 186176d268..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/asyncpg.py +++ /dev/null @@ -1,312 +0,0 @@ -from __future__ import annotations - -import contextlib -import re -from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import asyncpg # type: ignore - from asyncpg.cursor import ( # type: ignore - BaseCursor, - Cursor, - CursorIterator, - ) - -except ImportError: - raise DidNotEnable("asyncpg not installed.") - - -class AsyncPGIntegration(Integration): - identifier = "asyncpg" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AsyncPGIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - # asyncpg.__version__ is a string containing the semantic version in the form of ".." - asyncpg_version = parse_version(asyncpg.__version__) - _check_minimum_version(AsyncPGIntegration, asyncpg_version) - - asyncpg.Connection.execute = _wrap_execute( - asyncpg.Connection.execute, - ) - - asyncpg.Connection._execute = _wrap_connection_method( - asyncpg.Connection._execute - ) - asyncpg.Connection._executemany = _wrap_connection_method( - asyncpg.Connection._executemany, executemany=True - ) - asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) - - BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) - BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) - - asyncpg.connect_utils._connect_addr = _wrap_connect_addr( - asyncpg.connect_utils._connect_addr - ) - - -T = TypeVar("T") - - -def _normalize_query(query: str) -> str: - return re.sub(r"\s+", " ", query).strip() - - -def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - # Avoid recording calls to _execute twice. - # Calls to Connection.execute with args also call - # Connection._execute, which is recorded separately - # args[0] = the connection object, args[1] is the query - if len(args) > 2: - return await f(*args, **kwargs) - - query = _normalize_query(args[1]) - with record_sql_queries( - cursor=None, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - span_origin=AsyncPGIntegration.origin, - ) as span: - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -SubCursor = TypeVar("SubCursor", bound=BaseCursor) - - -@contextlib.contextmanager -def _record( - cursor: "SubCursor | None", - query: str, - params_list: "tuple[Any, ...] | None", - *, - executemany: bool = False, -) -> "Iterator[Union[Span, StreamedSpan]]": - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncPGIntegration) - if integration is not None and not integration._record_params: - params_list = None - - param_style = "pyformat" if params_list else None - - query = _normalize_query(query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=params_list, - paramstyle=param_style, - executemany=executemany, - record_cursor_repr=cursor is not None, - span_origin=AsyncPGIntegration.origin, - ) as span: - yield span - - -def _wrap_connection_method( - f: "Callable[..., Awaitable[T]]", *, executemany: bool = False -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - query = args[1] - params_list = args[2] if len(args) > 2 else None - with _record(None, query, params_list, executemany=executemany) as span: - _set_db_data(span, args[0]) - - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_cursor_method( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - if type(cursor) is CursorIterator: - span_op_override_value = OP.DB_CURSOR_ITERATOR - elif type(cursor) is Cursor: - span_op_override_value = OP.DB_CURSOR_FETCH - else: - span_op_override_value = None - - query = _normalize_query(cursor._query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - record_cursor_repr=True, - span_origin=AsyncPGIntegration.origin, - span_op_override_value=span_op_override_value, - ) as span: - _set_db_data(span, cursor._connection) - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_connect_addr( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - user = kwargs["params"].user - database = kwargs["params"].database - addr = kwargs.get("addr") - - if has_span_streaming_enabled(client.options): - span_attributes = { - "sentry.op": OP.DB, - "sentry.origin": AsyncPGIntegration.origin, - SPANDATA.DB_SYSTEM_NAME: "postgresql", - SPANDATA.DB_USER: user, - SPANDATA.DB_NAMESPACE: database, - SPANDATA.DB_DRIVER_NAME: "asyncpg", - } - if addr: - try: - span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] - span_attributes[SPANDATA.SERVER_PORT] = addr[1] - except IndexError: - pass - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span_attributes - ) - res = await f(*args, **kwargs) - - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AsyncPGIntegration.origin, - ) as span: - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - span.set_data(SPANDATA.DB_NAME, database) - span.set_data(SPANDATA.DB_USER, user) - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span._data - ) - res = await f(*args, **kwargs) - - return res - - return _inner - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - addr = conn._addr - database = conn._params.database - user = conn._params.user - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") - if addr: - try: - span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_attribute(SPANDATA.DB_NAMESPACE, database) - - if user: - span.set_attribute(SPANDATA.DB_USER, user) - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_data(SPANDATA.DB_NAME, database) - - if user: - span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py deleted file mode 100644 index b573e76f09..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/atexit.py +++ /dev/null @@ -1,51 +0,0 @@ -import atexit -import os -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Optional - - -def default_callback(pending: int, timeout: int) -> None: - """This is the default shutdown callback that is set on the options. - It prints out a message to stderr that informs the user that some events - are still pending and the process is waiting for them to flush out. - """ - - def echo(msg: str) -> None: - sys.stderr.write(msg + "\n") - - echo("Sentry is attempting to send %i pending events" % pending) - echo("Waiting up to %s seconds" % timeout) - echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) - sys.stderr.flush() - - -class AtexitIntegration(Integration): - identifier = "atexit" - - def __init__(self, callback: "Optional[Any]" = None) -> None: - if callback is None: - callback = default_callback - self.callback = callback - - @staticmethod - def setup_once() -> None: - @atexit.register - def _shutdown() -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(AtexitIntegration) - - if integration is None: - return - - logger.debug("atexit: got shutdown signal") - logger.debug("atexit: shutting down client") - sentry_sdk.get_isolation_scope().end_session() - - client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py deleted file mode 100644 index c7fe77714a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/aws_lambda.py +++ /dev/null @@ -1,542 +0,0 @@ -import functools -import json -import re -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING -from urllib.parse import urlencode - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.cloud_resource_context import ( - CLOUD_PLATFORM, - CLOUD_PROVIDER, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - -# Constants -TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - - -def _wrap_init_error(init_error: "F") -> "F": - @ensure_integration_enabled(AwsLambdaIntegration, init_error) - def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - with capture_internal_exceptions(): - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - exc_info = sys.exc_info() - if exc_info and all(exc_info): - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - - else: - # Fall back to AWS lambdas JSON representation of the error - error_info = args[1] - if isinstance(error_info, str): - error_info = json.loads(error_info) - sentry_event = _event_from_error_json(error_info) - sentry_sdk.capture_event(sentry_event) - - return init_error(*args, **kwargs) - - return sentry_init_error # type: ignore - - -def _wrap_handler(handler: "F") -> "F": - @functools.wraps(handler) - def sentry_handler( - aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, - # `event` here is *likely* a dictionary, but also might be a number of - # other types (str, int, float, None). - # - # In some cases, it is a list (if the user is batch-invoking their - # function, for example), in which case we'll use the first entry as a - # representative from which to try pulling request data. (Presumably it - # will be the same for all events in the list, since they're all hitting - # the lambda in the same request.) - - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - - if integration is None: - return handler(aws_event, aws_context, *args, **kwargs) - - if isinstance(aws_event, list) and len(aws_event) >= 1: - request_data = aws_event[0] - batch_size = len(aws_event) - else: - request_data = aws_event - batch_size = 1 - - if not isinstance(request_data, dict): - # If we're not dealing with a dictionary, we won't be able to get - # headers, path, http method, etc in any case, so it's fine that - # this is empty - request_data = {} - - configured_time = aws_context.get_remaining_time_in_millis() - aws_region = aws_context.invoked_function_arn.split(":")[3] - - with sentry_sdk.isolation_scope() as scope: - timeout_thread = None - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - request_data, aws_context, configured_time - ) - ) - scope.set_tag("aws_region", aws_region) - if batch_size > 1: - scope.set_tag("batch_request", True) - scope.set_tag("batch_size", batch_size) - - # Starting the Timeout thread only if the configured time is greater than Timeout warning - # buffer and timeout_warning parameter is set True. - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = ( - configured_time - TIMEOUT_WARNING_BUFFER - ) / MILLIS_TO_SECONDS - - timeout_thread = TimeoutThread( - waiting_time, - configured_time / MILLIS_TO_SECONDS, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = request_data.get("headers", {}) - # Some AWS Services (ie. EventBridge) set headers as a list - # or None, so we must ensure it is a dict - if not isinstance(headers, dict): - headers = {} - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - additional_attributes: "dict[str, Any]" = {} - if "httpMethod" in request_data: - additional_attributes["http.request.method"] = request_data[ - "httpMethod" - ] - - if should_send_default_pii() and "queryStringParameters" in request_data: - qs = request_data["queryStringParameters"] - if qs: - additional_attributes["url.query"] = urlencode(qs) - - sampling_context = { - "aws_event": aws_event, - "aws_context": aws_context, - } - - function_name = aws_context.function_name - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_AWS, - "sentry.origin": AwsLambdaIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.region": aws_region, - "cloud.resource_id": aws_context.invoked_function_arn, - "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, - "cloud.provider": CLOUD_PROVIDER.AWS, - "faas.name": function_name, - "faas.invocation_id": aws_context.aws_request_id, - "faas.version": aws_context.function_version, - "aws.lambda.invoked_arn": aws_context.invoked_function_arn, - "aws.log.group.names": [aws_context.log_group_name], - "aws.log.stream.names": [aws_context.log_stream_name], - "messaging.batch.message_count": batch_size, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_AWS, - name=function_name, - source=TransactionSource.COMPONENT, - origin=AwsLambdaIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return handler(aws_event, aws_context, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - - return sentry_handler # type: ignore - - -def _drain_queue() -> None: - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - if integration is not None: - # Flush out the event queue before AWS kills the - # process. - client.flush() - - -class AwsLambdaIntegration(Integration): - identifier = "aws_lambda" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - lambda_bootstrap = get_lambda_bootstrap() - if not lambda_bootstrap: - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find bootstrap module)" - ) - return - - if not hasattr(lambda_bootstrap, "handle_event_request"): - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find handle_event_request)" - ) - return - - pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 - - if pre_37: - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_event_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - old_handle_http_request = lambda_bootstrap.handle_http_request - - def sentry_handle_http_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_http_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_http_request = sentry_handle_http_request - - # Patch to_json to drain the queue. This should work even when the - # SDK is initialized inside of the handler - - old_to_json = lambda_bootstrap.to_json - - def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return old_to_json(*args, **kwargs) - - lambda_bootstrap.to_json = sentry_to_json - else: - lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( - lambda_bootstrap.LambdaRuntimeClient.post_init_error - ) - - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( # type: ignore - lambda_runtime_client, request_handler, *args, **kwargs - ): - request_handler = _wrap_handler(request_handler) - return old_handle_event_request( - lambda_runtime_client, request_handler, *args, **kwargs - ) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - # Patch the runtime client to drain the queue. This should work - # even when the SDK is initialized inside of the handler - - def _wrap_post_function(f: "F") -> "F": - def inner(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return f(*args, **kwargs) - - return inner # type: ignore - - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result - ) - ) - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error - ) - ) - - -def get_lambda_bootstrap() -> "Optional[Any]": - # Python 3.7: If the bootstrap module is *already imported*, it is the - # one we actually want to use (no idea what's in __main__) - # - # Python 3.8: bootstrap is also importable, but will be the same file - # as __main__ imported under a different name: - # - # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ - # sys.modules['__main__'] is not sys.modules['bootstrap'] - # - # Python 3.9: bootstrap is in __main__.awslambdaricmain - # - # On container builds using the `aws-lambda-python-runtime-interface-client` - # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap - # - # Such a setup would then make all monkeypatches useless. - if "bootstrap" in sys.modules: - return sys.modules["bootstrap"] - elif "__main__" in sys.modules: - module = sys.modules["__main__"] - # python3.9 runtime - if hasattr(module, "awslambdaricmain") and hasattr( - module.awslambdaricmain, "bootstrap" - ): - return module.awslambdaricmain.bootstrap - elif hasattr(module, "bootstrap"): - # awslambdaric python module in container builds - return module.bootstrap - - # python3.8 runtime - return module - else: - return None - - -def _make_request_event_processor( - aws_event: "Any", aws_context: "Any", configured_timeout: "Any" -) -> "EventProcessor": - start_time = datetime.now(timezone.utc) - - def event_processor( - sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time - ) -> "Optional[Event]": - remaining_time_in_milis = aws_context.get_remaining_time_in_millis() - exec_duration = configured_timeout - remaining_time_in_milis - - extra = sentry_event.setdefault("extra", {}) - extra["lambda"] = { - "function_name": aws_context.function_name, - "function_version": aws_context.function_version, - "invoked_function_arn": aws_context.invoked_function_arn, - "aws_request_id": aws_context.aws_request_id, - "execution_duration_in_millis": exec_duration, - "remaining_time_in_millis": remaining_time_in_milis, - } - - extra["cloudwatch logs"] = { - "url": _get_cloudwatch_logs_url(aws_context, start_time), - "log_group": aws_context.log_group_name, - "log_stream": aws_context.log_stream_name, - } - - request = sentry_event.get("request", {}) - - if "httpMethod" in aws_event: - request["method"] = aws_event["httpMethod"] - - request["url"] = _get_url(aws_event, aws_context) - - if "queryStringParameters" in aws_event: - request["query_string"] = aws_event["queryStringParameters"] - - if "headers" in aws_event: - request["headers"] = _filter_headers(aws_event["headers"]) - - if should_send_default_pii(): - user_info = sentry_event.setdefault("user", {}) - - identity = aws_event.get("identity") - if identity is None: - identity = {} - - id = identity.get("userArn") - if id is not None: - user_info.setdefault("id", id) - - ip = identity.get("sourceIp") - if ip is not None: - user_info.setdefault("ip_address", ip) - - if "body" in aws_event: - request["data"] = aws_event.get("body", "") - else: - if aws_event.get("body", None): - # Unfortunately couldn't find a way to get structured body from AWS - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - sentry_event["request"] = deepcopy(request) - - return sentry_event - - return event_processor - - -def _get_url(aws_event: "Any", aws_context: "Any") -> str: - path = aws_event.get("path", None) - - headers = aws_event.get("headers") - if headers is None: - headers = {} - - host = headers.get("Host", None) - proto = headers.get("X-Forwarded-Proto", None) - if proto and host and path: - return "{}://{}{}".format(proto, host, path) - return "awslambda:///{}".format(aws_context.function_name) - - -def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: - """ - Generates a CloudWatchLogs console URL based on the context object - - Arguments: - aws_context {Any} -- context from lambda handler - - Returns: - str -- AWS Console URL to logs. - """ - formatstring = "%Y-%m-%dT%H:%M:%SZ" - region = environ.get("AWS_REGION", "") - - url = ( - "https://console.{domain}/cloudwatch/home?region={region}" - "#logEventViewer:group={log_group};stream={log_stream}" - ";start={start_time};end={end_time}" - ).format( - domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", - region=region, - log_group=aws_context.log_group_name, - log_stream=aws_context.log_stream_name, - start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), - end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( - formatstring - ), - ) - - return url - - -def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": - frames = [] - for frame in formatted_tb: - match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) - if match: - file_name, line_number, func_name = match.groups() - line_number = int(line_number) - frames.append( - { - "filename": file_name, - "function": func_name, - "lineno": line_number, - "vars": None, - "pre_context": None, - "context_line": None, - "post_context": None, - } - ) - return frames - - -def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": - """ - Converts the error JSON from AWS Lambda into a Sentry error event. - This is not a full fletched event, but better than nothing. - - This is an example of where AWS creates the error JSON: - https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 - """ - event: "Event" = { - "level": "error", - "exception": { - "values": [ - { - "type": error_json.get("errorType"), - "value": error_json.get("errorMessage"), - "stacktrace": { - "frames": _parse_formatted_traceback( - error_json.get("stackTrace", []) - ), - }, - "mechanism": { - "type": "aws_lambda", - "handled": False, - }, - } - ], - }, - } - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py deleted file mode 100644 index 31f45f73de..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/beam.py +++ /dev/null @@ -1,164 +0,0 @@ -import sys -import types -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) - - -WRAPPED_FUNC = "_wrapped_{}_" -INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py -USED_FUNC = "_sentry_used_" - - -class BeamIntegration(Integration): - identifier = "beam" - - @staticmethod - def setup_once() -> None: - from apache_beam.transforms.core import DoFn, ParDo # type: ignore - - ignore_logger("root") - ignore_logger("bundle_processor.create") - - function_patches = ["process", "start_bundle", "finish_bundle", "setup"] - for func_name in function_patches: - setattr( - DoFn, - INSPECT_FUNC.format(func_name), - _wrap_inspect_call(DoFn, func_name), - ) - - old_init = ParDo.__init__ - - def sentry_init_pardo( - self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Do not monkey patch init twice - if not getattr(self, "_sentry_is_patched", False): - for func_name in function_patches: - if not hasattr(fn, func_name): - continue - wrapped_func = WRAPPED_FUNC.format(func_name) - - # Check to see if inspect is set and process is not - # to avoid monkey patching process twice. - # Check to see if function is part of object for - # backwards compatibility. - process_func = getattr(fn, func_name) - inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) - if not getattr(inspect_func, USED_FUNC, False) and not getattr( - process_func, USED_FUNC, False - ): - setattr(fn, wrapped_func, process_func) - setattr(fn, func_name, _wrap_task_call(process_func)) - - self._sentry_is_patched = True - old_init(self, fn, *args, **kwargs) - - ParDo.__init__ = sentry_init_pardo - - -def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": - if not hasattr(cls, func_name): - return None - - def _inspect(self: "Any") -> "Any": - """ - Inspect function overrides the way Beam gets argspec. - """ - wrapped_func = WRAPPED_FUNC.format(func_name) - if hasattr(self, wrapped_func): - process_func = getattr(self, wrapped_func) - else: - process_func = getattr(self, func_name) - setattr(self, func_name, _wrap_task_call(process_func)) - setattr(self, wrapped_func, process_func) - - # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults - # (which uses Signatures internally) should be used instead. - try: - from apache_beam.transforms.core import get_function_args_defaults - - return get_function_args_defaults(process_func) - except ImportError: - from apache_beam.typehints.decorators import getfullargspec # type: ignore - - return getfullargspec(process_func) - - setattr(_inspect, USED_FUNC, True) - return _inspect - - -def _wrap_task_call(func: "F") -> "F": - """ - Wrap task call with a try catch to get exceptions. - """ - - @wraps(func) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - try: - gen = func(*args, **kwargs) - except Exception: - raise_exception() - - if not isinstance(gen, types.GeneratorType): - return gen - return _wrap_generator_call(gen) - - setattr(_inner, USED_FUNC, True) - return _inner # type: ignore - - -@ensure_integration_enabled(BeamIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - """ - Send Beam exception to Sentry. - """ - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "beam", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def raise_exception() -> None: - """ - Raise an exception. - """ - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) - reraise(*exc_info) - - -def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": - """ - Wrap the generator to handle any failures. - """ - while True: - try: - yield next(gen) - except StopIteration: - break - except Exception: - raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py deleted file mode 100644 index a7fdd99b21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/boto3.py +++ /dev/null @@ -1,187 +0,0 @@ -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_url, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Type, Union - - from botocore.model import ServiceId - -try: - from botocore import __version__ as BOTOCORE_VERSION - from botocore.awsrequest import AWSRequest - from botocore.client import BaseClient - from botocore.response import StreamingBody -except ImportError: - raise DidNotEnable("botocore is not installed") - - -class Boto3Integration(Integration): - identifier = "boto3" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTOCORE_VERSION) - _check_minimum_version(Boto3Integration, version, "botocore") - - orig_init = BaseClient.__init__ - - def sentry_patched_init( - self: "BaseClient", *args: "Any", **kwargs: "Any" - ) -> None: - orig_init(self, *args, **kwargs) - meta = self.meta - service_id = meta.service_model.service_id - meta.events.register( - "request-created", - partial(_sentry_request_created, service_id=service_id), - ) - meta.events.register("after-call", _sentry_after_call) - meta.events.register("after-call-error", _sentry_after_call_error) - - BaseClient.__init__ = sentry_patched_init # type: ignore - - -def _sentry_request_created( - service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" -) -> None: - description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) - - client = sentry_sdk.get_client() - if client.get_integration(Boto3Integration) is None: - return - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Boto3Integration.origin, - SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", - }, - ) - if request.url is not None and should_send_default_pii(): - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if request.method is not None: - span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=description, - origin=Boto3Integration.origin, - ) - - if request.url is not None: - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_data("aws.request.url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - span.set_tag("aws.service_id", service_id.hyphenize()) - span.set_tag("aws.operation_name", operation_name) - if request.method is not None: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - - # We do it in order for subsequent http calls/retries be - # attached to this span. - span.__enter__() - - # request.context is an open-ended data-structure - # where we can add anything useful in request life cycle. - request.context["_sentrysdk_span"] = span - - -def _sentry_after_call( - context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(None, None, None) - - body = parsed.get("Body") - if not isinstance(body, StreamingBody): - return - - streaming_span: "Union[Span, StreamedSpan]" - if isinstance(span, StreamedSpan): - streaming_span = sentry_sdk.traces.start_span( - name=span.name, - parent_span=span, - attributes={ - "sentry.op": OP.HTTP_CLIENT_STREAM, - "sentry.origin": Boto3Integration.origin, - }, - ) - else: - streaming_span = span.start_child( - op=OP.HTTP_CLIENT_STREAM, - name=span.description, - origin=Boto3Integration.origin, - ) - - orig_read = body.read - orig_close = body.close - - def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: - try: - ret = orig_read(*args, **kwargs) - if ret: - return ret - - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - return ret - except Exception: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - raise - - body.read = sentry_streaming_body_read # type: ignore - - def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - orig_close(*args, **kwargs) - - body.close = sentry_streaming_body_close # type: ignore - - -def _sentry_after_call_error( - context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py deleted file mode 100644 index 50f6ca2e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/bottle.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from collections.abc import Set - from typing import Any, Callable, Dict, Optional - - from bottle import FileUpload, FormsDict, LocalRequest # type: ignore - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - -try: - from bottle import ( - Bottle, - HTTPResponse, - Route, - ) - from bottle import ( - __version__ as BOTTLE_VERSION, - ) - from bottle import ( - request as bottle_request, - ) -except ImportError: - raise DidNotEnable("Bottle not installed") - - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class BottleIntegration(Integration): - identifier = "bottle" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTTLE_VERSION) - _check_minimum_version(BottleIntegration, version) - - old_app = Bottle.__call__ - - @ensure_integration_enabled(BottleIntegration, old_app) - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=BottleIntegration.origin, - ) - - return middleware(environ, start_response) - - Bottle.__call__ = sentry_patched_wsgi_app - - old_handle = Bottle._handle - - @functools.wraps(old_handle) - def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return old_handle(self, environ) - - scope = sentry_sdk.get_isolation_scope() - scope._name = "bottle" - scope.add_event_processor( - _make_request_event_processor(self, bottle_request, integration) - ) - res = old_handle(self, environ) - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - _set_segment_name_and_source( - transaction_style=integration.transaction_style - ) - - return res - - Bottle._handle = _patched_handle - - old_make_callback = Route._make_callback - - @functools.wraps(old_make_callback) - def patched_make_callback( - self: "Route", *args: object, **kwargs: object - ) -> "Any": - prepared_callback = old_make_callback(self, *args, **kwargs) - - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return prepared_callback - - def wrapped_callback(*args: object, **kwargs: object) -> "Any": - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) - - return res - - return wrapped_callback - - Route._make_callback = patched_make_callback - - -class BottleRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[str, str]": - return self.request.cookies - - def raw_data(self) -> bytes: - return self.request.body.read() - - def form(self) -> "FormsDict": - if self.is_json(): - return None - return self.request.forms.decode() - - def files(self) -> "Optional[Dict[str, str]]": - if self.is_json(): - return None - - return self.request.files - - def size_of_file(self, file: "FileUpload") -> int: - return file.content_length - - -def _set_segment_name_and_source(transaction_style: str) -> None: - try: - if transaction_style == "url": - name = bottle_request.route.rule or "bottle request" - else: - name = ( - bottle_request.route.name - or transaction_from_function(bottle_request.route.callback) - or "bottle request" - ) - - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], - ) - except RuntimeError: - pass - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "url": - try: - name = request.route.rule or "" - except RuntimeError: - pass - - elif transaction_style == "endpoint": - try: - name = ( - request.route.name - or transaction_from_function(request.route.callback) - or "" - ) - except RuntimeError: - pass - - event["transaction"] = name - event["transaction_info"] = { - "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - } - - -def _make_request_event_processor( - app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, request) - - with capture_internal_exceptions(): - BottleRequestExtractor(request).extract_into_event(event) - - return event - - return event_processor - - -def _capture_exception(exception: BaseException, handled: bool) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "bottle", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py deleted file mode 100644 index 532b13539b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/__init__.py +++ /dev/null @@ -1,612 +0,0 @@ -import sys -from collections.abc import Mapping -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import isolation_scope -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.celery.beat import ( - _patch_beat_apply_entry, - _patch_redbeat_apply_async, - _setup_celery_beat_signals, -) -from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource -from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, List, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import VERSION as CELERY_VERSION # type: ignore - from celery.app.task import Task # type: ignore - from celery.app.trace import task_has_custom - from celery.exceptions import ( # type: ignore - Ignore, - Reject, - Retry, - SoftTimeLimitExceeded, - ) - from kombu import Producer # type: ignore -except ImportError: - raise DidNotEnable("Celery not installed") - - -CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) - - -class CeleryIntegration(Integration): - identifier = "celery" - origin = f"auto.queue.{identifier}" - - def __init__( - self, - propagate_traces: bool = True, - monitor_beat_tasks: bool = False, - exclude_beat_tasks: "Optional[List[str]]" = None, - ) -> None: - self.propagate_traces = propagate_traces - self.monitor_beat_tasks = monitor_beat_tasks - self.exclude_beat_tasks = exclude_beat_tasks - - _patch_beat_apply_entry() - _patch_redbeat_apply_async() - _setup_celery_beat_signals(monitor_beat_tasks) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(CeleryIntegration, CELERY_VERSION) - - _patch_build_tracer() - _patch_task_apply_async() - _patch_celery_send_task() - _patch_worker_exit() - _patch_producer_publish() - - # This logger logs every status of every task that ran on the worker. - # Meaning that every task's breadcrumbs are full of stuff like "Task - # raised unexpected ". - ignore_logger("celery.worker.job") - ignore_logger("celery.app.trace") - - # This is stdout/err redirected to a logger, can't deal with this - # (need event_level=logging.WARN to reproduce) - ignore_logger("celery.redirected") - - -def _set_status(status: str) -> None: - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - - if span_streaming and scope.streamed_span is not None: - scope.streamed_span.status = "ok" if status == "ok" else "error" - elif not span_streaming and scope.span is not None: - scope.span.set_status(status) - - -def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return - - if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): - # ??? Doesn't map to anything - _set_status("aborted") - return - - _set_status("internal_error") - - if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): - return - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "celery", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - task: "Any", - uuid: "Any", - args: "Any", - kwargs: "Any", - request: "Optional[Any]" = None, -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["celery_task_id"] = uuid - extra = event.setdefault("extra", {}) - extra["celery-job"] = { - "task_name": task.name, - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - } - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): - event["fingerprint"] = [ - "celery", - "SoftTimeLimitExceeded", - getattr(task, "name", task), - ] - - return event - - return event_processor - - -def _update_celery_task_headers( - original_headers: "dict[str, Any]", - span: "Optional[Union[StreamedSpan, Span]]", - monitor_beat_tasks: bool, -) -> "dict[str, Any]": - """ - Updates the headers of the Celery task with the tracing information - and eventually Sentry Crons monitoring information for beat tasks. - """ - updated_headers = original_headers.copy() - with capture_internal_exceptions(): - # if span is None (when the task was started by Celery Beat) - # this will return the trace headers from the scope. - headers = dict( - sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) - ) - - if monitor_beat_tasks: - headers.update( - { - "sentry-monitor-start-timestamp-s": "%.9f" - % _now_seconds_since_epoch(), - } - ) - - # Add the time the task was enqueued to the headers - # This is used in the consumer to calculate the latency - updated_headers.update( - {"sentry-task-enqueued-time": _now_seconds_since_epoch()} - ) - - if headers: - existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) - sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) - - combined_baggage = sentry_baggage or existing_baggage - if sentry_baggage and existing_baggage: - # Merge incoming and sentry baggage, where the sentry trace information - # in the incoming baggage takes precedence and the third-party items - # are concatenated. - incoming = Baggage.from_incoming_header(existing_baggage) - combined = Baggage.from_incoming_header(sentry_baggage) - combined.sentry_items.update(incoming.sentry_items) - combined.third_party_items = ",".join( - [ - x - for x in [ - combined.third_party_items, - incoming.third_party_items, - ] - if x is not None and x != "" - ] - ) - combined_baggage = combined.serialize(include_third_party=True) - - updated_headers.update(headers) - if combined_baggage: - updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage - - # https://github.com/celery/celery/issues/4875 - # - # Need to setdefault the inner headers too since other - # tracing tools (dd-trace-py) also employ this exact - # workaround and we don't want to break them. - updated_headers.setdefault("headers", {}).update(headers) - if combined_baggage: - updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage - - # Add the Sentry options potentially added in `sentry_apply_entry` - # to the headers (done when auto-instrumenting Celery Beat tasks) - for key, value in updated_headers.items(): - if key.startswith("sentry-"): - updated_headers["headers"][key] = value - - # Preserve user-provided custom headers in the inner "headers" dict - # so they survive to task.request.headers on the worker (celery#4875). - for key, value in original_headers.items(): - if key != "headers" and key not in updated_headers["headers"]: - updated_headers["headers"][key] = value - - return updated_headers - - -class NoOpMgr: - def __enter__(self) -> None: - return None - - def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: - return None - - -def _wrap_task_run(f: "F") -> "F": - @wraps(f) - def apply_async(*args: "Any", **kwargs: "Any") -> "Any": - # Note: kwargs can contain headers=None, so no setdefault! - # Unsure which backend though. - client = sentry_sdk.get_client() - integration = client.get_integration(CeleryIntegration) - if integration is None: - return f(*args, **kwargs) - - kwarg_headers = kwargs.get("headers") or {} - propagate_traces = kwarg_headers.pop( - "sentry-propagate-traces", integration.propagate_traces - ) - - if not propagate_traces: - return f(*args, **kwargs) - - if isinstance(args[0], Task): - task_name: str = args[0].name - elif len(args) > 1 and isinstance(args[1], str): - task_name = args[1] - else: - task_name = "" - - span_streaming = has_span_streaming_enabled(client.options) - - task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" - - span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - if span_streaming: - if not task_started_from_beat and get_current_span() is not None: - span_mgr = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_CELERY, - "sentry.origin": CeleryIntegration.origin, - }, - ) - - else: - if not task_started_from_beat: - span_mgr = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_CELERY, - name=task_name, - origin=CeleryIntegration.origin, - ) - - with span_mgr as span: - kwargs["headers"] = _update_celery_task_headers( - kwarg_headers, span, integration.monitor_beat_tasks - ) - return f(*args, **kwargs) - - return apply_async # type: ignore - - -def _wrap_tracer(task: "Any", f: "F") -> "F": - # Need to wrap tracer for pushing the scope before prerun is sent, and - # popping it after postrun is sent. - # - # This is the reason we don't use signals for hooking in the first place. - # Also because in Celery 3, signal dispatch returns early if one handler - # crashes. - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - with isolation_scope() as scope: - scope._name = "celery" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - - task_name = getattr(task, "name", "") - - custom_sampling_context = {} - with capture_internal_exceptions(): - custom_sampling_context = { - "celery_job": { - "task": task_name, - # for some reason, args[1] is a list if non-empty but a - # tuple if empty - "args": list(args[1]), - "kwargs": args[2], - } - } - - span: "Union[Span, StreamedSpan]" - span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - - # Celery task objects are not a thing to be trusted. Even - # something such as attribute access can fail. - with capture_internal_exceptions(): - headers = args[3].get("headers") or {} - if span_streaming: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(custom_sampling_context) - span = sentry_sdk.traces.start_span( - name=task_name, - parent_span=None, # make this a segment - attributes={ - "sentry.origin": CeleryIntegration.origin, - "sentry.span.source": TransactionSource.TASK.value, - "sentry.op": OP.QUEUE_TASK_CELERY, - }, - ) - - span_ctx = span - - else: - span = continue_trace( - headers, - op=OP.QUEUE_TASK_CELERY, - name=task_name, - source=TransactionSource.TASK, - origin=CeleryIntegration.origin, - ) - span.set_status(SPANSTATUS.OK) - - span_ctx = sentry_sdk.start_transaction( - span, - custom_sampling_context=custom_sampling_context, - ) - - with span_ctx: - return f(*args, **kwargs) - - return _inner # type: ignore - - -def _set_messaging_destination_name( - task: "Any", span: "Union[StreamedSpan, Span]" -) -> None: - """Set "messaging.destination.name" tag for span""" - with capture_internal_exceptions(): - delivery_info = task.request.delivery_info - if delivery_info: - routing_key = delivery_info.get("routing_key") - if delivery_info.get("exchange") == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning the tasks - # are sent to the queue with the same name as the routing key. - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - else: - span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - -def _wrap_task_call(task: "Any", f: "F") -> "F": - # Need to wrap task call because the exception is caught before we get to - # see it. Also celery's reported stacktrace is untrustworthy. - - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - try: - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_PROCESS, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PROCESS, - name=task.name, - origin=CeleryIntegration.origin, - ) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - _set_messaging_destination_name(task, span) - - latency = None - with capture_internal_exceptions(): - if ( - task.request.headers is not None - and "sentry-task-enqueued-time" in task.request.headers - ): - latency = _now_seconds_since_epoch() - task.request.headers.pop( - "sentry-task-enqueued-time" - ) - - if latency is not None: - latency *= 1000 # milliseconds - set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) - - with capture_internal_exceptions(): - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries - ) - - with capture_internal_exceptions(): - with task.app.connection() as conn: - set_on_span( - SPANDATA.MESSAGING_SYSTEM, - conn.transport.driver_type, - ) - - return f(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(task, exc_info) - reraise(*exc_info) - - return _inner # type: ignore - - -def _patch_build_tracer() -> None: - import celery.app.trace as trace # type: ignore - - original_build_tracer = trace.build_tracer - - def sentry_build_tracer( - name: "Any", task: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not getattr(task, "_sentry_is_patched", False): - # determine whether Celery will use __call__ or run and patch - # accordingly - if task_has_custom(task, "__call__"): - type(task).__call__ = _wrap_task_call(task, type(task).__call__) - else: - task.run = _wrap_task_call(task, task.run) - - # `build_tracer` is apparently called for every task - # invocation. Can't wrap every celery task for every invocation - # or we will get infinitely nested wrapper functions. - task._sentry_is_patched = True - - return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) - - trace.build_tracer = sentry_build_tracer - - -def _patch_task_apply_async() -> None: - Task.apply_async = _wrap_task_run(Task.apply_async) - - -def _patch_celery_send_task() -> None: - from celery import Celery - - Celery.send_task = _wrap_task_run(Celery.send_task) - - -def _patch_worker_exit() -> None: - # Need to flush queue before worker shutdown because a crashing worker will - # call os._exit - from billiard.pool import Worker # type: ignore - - original_workloop = Worker.workloop - - def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": - try: - return original_workloop(*args, **kwargs) - finally: - with capture_internal_exceptions(): - if ( - sentry_sdk.get_client().get_integration(CeleryIntegration) - is not None - ): - sentry_sdk.flush() - - Worker.workloop = sentry_workloop - - -def _patch_producer_publish() -> None: - original_publish = Producer.publish - - def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return original_publish(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - kwargs_headers = kwargs.get("headers", {}) - if not isinstance(kwargs_headers, Mapping): - # Ensure kwargs_headers is a Mapping, so we can safely call get(). - # We don't expect this to happen, but it's better to be safe. Even - # if it does happen, only our instrumentation breaks. This line - # does not overwrite kwargs["headers"], so the original publish - # method will still work. - kwargs_headers = {} - - task_name = kwargs_headers.get("task") or "" - task_id = kwargs_headers.get("id") - retries = kwargs_headers.get("retries") - - routing_key = kwargs.get("routing_key") - exchange = kwargs.get("exchange") - - span: "Union[StreamedSpan, Span, None]" = None - if span_streaming: - if get_current_span() is not None: - span = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_PUBLISH, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PUBLISH, - name=task_name, - origin=CeleryIntegration.origin, - ) - - if span is None: - return original_publish(self, *args, **kwargs) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - if task_id is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) - - if exchange == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning messages are - # routed to the queue with the same name as the routing key. - set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - if retries is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type - ) - - return original_publish(self, *args, **kwargs) - - Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py deleted file mode 100644 index b5027d212a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/beat.py +++ /dev/null @@ -1,291 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.crons import MonitorStatus, capture_checkin -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.celery.utils import ( - _get_humanized_interval, - _now_seconds_since_epoch, -) -from sentry_sdk.utils import ( - logger, - match_regex_list, -) - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, TypeVar, Union - - from sentry_sdk._types import ( - MonitorConfig, - MonitorConfigScheduleType, - MonitorConfigScheduleUnit, - ) - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import Celery, Task # type: ignore - from celery.beat import Scheduler # type: ignore - from celery.schedules import crontab, schedule # type: ignore - from celery.signals import ( # type: ignore - task_failure, - task_retry, - task_success, - ) -except ImportError: - raise DidNotEnable("Celery not installed") - -try: - from redbeat.schedulers import RedBeatScheduler # type: ignore -except ImportError: - RedBeatScheduler = None - - -def _get_headers(task: "Task") -> "dict[str, Any]": - headers = task.request.get("headers") or {} - - # flatten nested headers - if "headers" in headers: - headers.update(headers["headers"]) - del headers["headers"] - - headers.update(task.request.get("properties") or {}) - - return headers - - -def _get_monitor_config( - celery_schedule: "Any", app: "Celery", monitor_name: str -) -> "MonitorConfig": - monitor_config: "MonitorConfig" = {} - schedule_type: "Optional[MonitorConfigScheduleType]" = None - schedule_value: "Optional[Union[str, int]]" = None - schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None - - if isinstance(celery_schedule, crontab): - schedule_type = "crontab" - schedule_value = ( - "{0._orig_minute} " - "{0._orig_hour} " - "{0._orig_day_of_month} " - "{0._orig_month_of_year} " - "{0._orig_day_of_week}".format(celery_schedule) - ) - elif isinstance(celery_schedule, schedule): - schedule_type = "interval" - (schedule_value, schedule_unit) = _get_humanized_interval( - celery_schedule.seconds - ) - - if schedule_unit == "second": - logger.warning( - "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", - monitor_name, - schedule_value, - ) - return {} - - else: - logger.warning( - "Celery schedule type '%s' not supported by Sentry Crons.", - type(celery_schedule), - ) - return {} - - monitor_config["schedule"] = {} - monitor_config["schedule"]["type"] = schedule_type - monitor_config["schedule"]["value"] = schedule_value - - if schedule_unit is not None: - monitor_config["schedule"]["unit"] = schedule_unit - - monitor_config["timezone"] = ( - ( - hasattr(celery_schedule, "tz") - and celery_schedule.tz is not None - and str(celery_schedule.tz) - ) - or app.timezone - or "UTC" - ) - - return monitor_config - - -def _apply_crons_data_to_schedule_entry( - scheduler: "Any", - schedule_entry: "Any", - integration: "sentry_sdk.integrations.celery.CeleryIntegration", -) -> None: - """ - Add Sentry Crons information to the schedule_entry headers. - """ - if not integration.monitor_beat_tasks: - return - - monitor_name = schedule_entry.name - - task_should_be_excluded = match_regex_list( - monitor_name, integration.exclude_beat_tasks - ) - if task_should_be_excluded: - return - - celery_schedule = schedule_entry.schedule - app = scheduler.app - - monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) - - is_supported_schedule = bool(monitor_config) - if not is_supported_schedule: - return - - headers = schedule_entry.options.pop("headers", {}) - headers.update( - { - "sentry-monitor-slug": monitor_name, - "sentry-monitor-config": monitor_config, - } - ) - - check_in_id = capture_checkin( - monitor_slug=monitor_name, - monitor_config=monitor_config, - status=MonitorStatus.IN_PROGRESS, - ) - headers.update({"sentry-monitor-check-in-id": check_in_id}) - - # Set the Sentry configuration in the options of the ScheduleEntry. - # Those will be picked up in `apply_async` and added to the headers. - schedule_entry.options["headers"] = headers - - -def _wrap_beat_scheduler( - original_function: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Makes sure that: - - a new Sentry trace is started for each task started by Celery Beat and - it is propagated to the task. - - the Sentry Crons information is set in the Celery Beat task's - headers so that is monitored with Sentry Crons. - - After the patched function is called, - Celery Beat will call apply_async to put the task in the queue. - """ - # Patch only once - # Can't use __name__ here, because some of our tests mock original_apply_entry - already_patched = "sentry_patched_scheduler" in str(original_function) - if already_patched: - return original_function - - from sentry_sdk.integrations.celery import CeleryIntegration - - def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(CeleryIntegration) - if integration is None: - return original_function(*args, **kwargs) - - # Tasks started by Celery Beat start a new Trace - scope = sentry_sdk.get_isolation_scope() - scope.set_new_propagation_context() - scope._name = "celery-beat" - - scheduler, schedule_entry = args - _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) - - return original_function(*args, **kwargs) - - return sentry_patched_scheduler - - -def _patch_beat_apply_entry() -> None: - Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) - - -def _patch_redbeat_apply_async() -> None: - if RedBeatScheduler is None: - return - - RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) - - -def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: - if monitor_beat_tasks: - task_success.connect(crons_task_success) - task_failure.connect(crons_task_failure) - task_retry.connect(crons_task_retry) - - -def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_success %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.OK, - ) - - -def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_failure %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) - - -def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_retry %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py deleted file mode 100644 index 8d181f1f24..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/celery/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import time -from typing import TYPE_CHECKING, cast - -if TYPE_CHECKING: - from typing import Tuple - - from sentry_sdk._types import MonitorConfigScheduleUnit - - -def _now_seconds_since_epoch() -> float: - # We cannot use `time.perf_counter()` when dealing with the duration - # of a Celery task, because the start of a Celery task and - # the end are recorded in different processes. - # Start happens in the Celery Beat process, - # the end in a Celery Worker process. - return time.time() - - -def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": - TIME_UNITS = ( # noqa: N806 - ("day", 60 * 60 * 24.0), - ("hour", 60 * 60.0), - ("minute", 60.0), - ) - - seconds = float(seconds) - for unit, divider in TIME_UNITS: - if seconds >= divider: - interval = int(seconds / divider) - return (interval, cast("MonitorConfigScheduleUnit", unit)) - - return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py deleted file mode 100644 index 9baa0e5cdd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/chalice.py +++ /dev/null @@ -1,188 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.aws_lambda import _make_request_event_processor -from sentry_sdk.traces import ( - SpanStatus, - StreamedSpan, - get_current_span, -) -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - parse_version, - reraise, -) - -try: - import chalice # type: ignore - from chalice import Chalice, ChaliceViewError - from chalice import __version__ as CHALICE_VERSION - from chalice.app import ( # type: ignore - EventSourceHandler as ChaliceEventSourceHandler, - ) -except ImportError: - raise DidNotEnable("Chalice is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, TypeVar - - F = TypeVar("F", bound=Callable[..., Any]) - - -class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore - def __call__(self, event: "Any", context: "Any") -> "Any": - client = sentry_sdk.get_client() - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor(event, context, configured_time) - ) - try: - return ChaliceEventSourceHandler.__call__(self, event, context) - except Exception: - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - client.flush() - reraise(*exc_info) - - -def _get_view_function_response( - app: "Any", view_function: "F", function_args: "Any" -) -> "F": - @wraps(view_function) - def wrapped_view_function(**function_args: "Any") -> "Any": - client = sentry_sdk.get_client() - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = app.lambda_context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor( - app.current_request.to_dict(), - app.lambda_context, - configured_time, - ) - ) - - if has_span_streaming_enabled(client.options): - current_span = get_current_span() - segment = None - if type(current_span) is StreamedSpan: - # A segment already exists (created by the AWS Lambda - # integration), so decorate it with Chalice attributes - # The AWS Lambda integration owns the span lifecycle - # (end + flush), but Chalice converts unhandled view exceptions - # into 500 responses, so the error must be captured here. - request_dict = app.current_request.to_dict() - headers = request_dict.get("headers", {}) - - header_attrs: "Dict[str, Any]" = {} - for header, value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attrs[f"http.request.header.{header.lower()}"] = value - - additional_attrs: "Dict[str, Any]" = {} - if "method" in request_dict: - additional_attrs["http.request.method"] = request_dict["method"] - - attributes = { - "sentry.origin": ChaliceIntegration.origin, - **header_attrs, - **additional_attrs, - } - - segment = current_span._segment - segment.set_attributes(attributes) - - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - if segment: - segment.status = SpanStatus.ERROR.value - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - if segment is None: - client.flush() - raise - else: - scope.set_transaction_name( - app.lambda_context.function_name, - source=TransactionSource.COMPONENT, - ) - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - client.flush() - raise - - return wrapped_view_function # type: ignore - - -class ChaliceIntegration(Integration): - identifier = "chalice" - origin = f"auto.function.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(CHALICE_VERSION) - - if version is None: - raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) - - if version < (1, 20): - old_get_view_function_response = Chalice._get_view_function_response - else: - from chalice.app import RestAPIEventHandler - - old_get_view_function_response = ( - RestAPIEventHandler._get_view_function_response - ) - - def sentry_event_response( - app: "Any", view_function: "F", function_args: "Dict[str, Any]" - ) -> "Any": - wrapped_view_function = _get_view_function_response( - app, view_function, function_args - ) - - return old_get_view_function_response( - app, wrapped_view_function, function_args - ) - - if version < (1, 20): - Chalice._get_view_function_response = sentry_event_response - else: - RestAPIEventHandler._get_view_function_response = sentry_event_response - # for everything else (like events) - chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py deleted file mode 100644 index e6b3009548..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/clickhouse_driver.py +++ /dev/null @@ -1,211 +0,0 @@ -import functools -from typing import TYPE_CHECKING, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Any, Callable, ParamSpec, Union -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - - -try: - from clickhouse_driver import VERSION # type: ignore[import-not-found] - from clickhouse_driver.client import Client # type: ignore[import-not-found] - from clickhouse_driver.connection import ( # type: ignore[import-not-found] - Connection, - ) - -except ImportError: - raise DidNotEnable("clickhouse-driver not installed.") - - -class ClickhouseDriverIntegration(Integration): - identifier = "clickhouse_driver" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(ClickhouseDriverIntegration, VERSION) - - # Every query is done using the Connection's `send_query` function - Connection.send_query = _wrap_start(Connection.send_query) - - # If the query contains parameters then the send_data function is used to send those parameters to clickhouse - _wrap_send_data() - - # Every query ends either with the Client's `receive_end_of_query` (no result expected) - # or its `receive_result` (result expected) - Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) - if hasattr(Client, "receive_end_of_insert_query"): - # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` - Client.receive_end_of_insert_query = _wrap_end( - Client.receive_end_of_insert_query - ) - Client.receive_result = _wrap_end(Client.receive_result) - - -P = ParamSpec("P") -T = TypeVar("T") - - -def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": - @functools.wraps(f) - def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(ClickhouseDriverIntegration) is None: - return f(*args, **kwargs) - - connection = args[0] - query = args[1] - query_id = args[2] if len(args) > 2 else kwargs.get("query_id") - params = args[3] if len(args) > 3 else kwargs.get("params") - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=query, # type: ignore - attributes={ - "sentry.op": OP.DB, - "sentry.origin": ClickhouseDriverIntegration.origin, - SPANDATA.DB_QUERY_TEXT: str(query), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=ClickhouseDriverIntegration.origin, - ) - - span.set_data("query", query) - - if query_id: - span.set_data("db.query_id", query_id) - - if params and should_send_default_pii(): - span.set_data("db.params", params) - - connection._sentry_span = span # type: ignore[attr-defined] - - _set_db_data(span, connection) - - # run the original code - ret = f(*args, **kwargs) - - return ret - - return _inner - - -def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": - def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": - res = f(*args, **kwargs) - instance = args[0] - span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] - - if span is None: - return res - - if isinstance(span, StreamedSpan): - span.end() - else: - if res is not None and should_send_default_pii(): - span.set_data("db.result", res) - - with capture_internal_exceptions(): - span.scope.add_breadcrumb( - message=span._data.pop("query"), category="query", data=span._data - ) - - span.finish() - - return res - - return _inner_end - - -def _wrap_send_data() -> None: - original_send_data = Client.send_data - - def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data - self, sample_block, data, types_check=False, columnar=False, *args, **kwargs - ): - span = getattr(self.connection, "_sentry_span", None) - - if isinstance(span, StreamedSpan): - _set_db_data(span, self.connection) - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - if span is not None: - _set_db_data(span, self.connection) - - if should_send_default_pii(): - db_params = span._data.get("db.params", []) - - if isinstance(data, (list, tuple)): - db_params.extend(data) - - else: # data is a generic iterator - orig_data = data - - # Wrap the generator to add items to db.params as they are yielded. - # This allows us to send the params to Sentry without needing to allocate - # memory for the entire generator at once. - def wrapped_generator() -> "Iterator[Any]": - for item in orig_data: - db_params.append(item) - yield item - - # Replace the original iterator with the wrapped one. - data = wrapped_generator() - - span.set_data("db.params", db_params) - - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - Client.send_data = _inner_send_data - - -def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") - span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) - - set_on_span = span.set_attribute - else: - span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") - span.set_data(SPANDATA.DB_NAME, connection.database) - - set_on_span = span.set_data - - set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") - set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) - set_on_span(SPANDATA.SERVER_PORT, connection.port) - set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py deleted file mode 100644 index f6285d0a9b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cloud_resource_context.py +++ /dev/null @@ -1,273 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import urllib3 - -from sentry_sdk.api import set_context -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Dict - - -CONTEXT_TYPE = "cloud_resource" - -HTTP_TIMEOUT = 2.0 - -AWS_METADATA_HOST = "169.254.169.254" -AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) -AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( - AWS_METADATA_HOST -) - -GCP_METADATA_HOST = "metadata.google.internal" -GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( - GCP_METADATA_HOST -) - - -class CLOUD_PROVIDER: # noqa: N801 - """ - Name of the cloud provider. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - ALIBABA = "alibaba_cloud" - AWS = "aws" - AZURE = "azure" - GCP = "gcp" - IBM = "ibm_cloud" - TENCENT = "tencent_cloud" - - -class CLOUD_PLATFORM: # noqa: N801 - """ - The cloud platform. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - AWS_EC2 = "aws_ec2" - AWS_LAMBDA = "aws_lambda" - GCP_COMPUTE_ENGINE = "gcp_compute_engine" - - -class CloudResourceContextIntegration(Integration): - """ - Adds cloud resource context to the Senty scope - """ - - identifier = "cloudresourcecontext" - - cloud_provider = "" - - aws_token = "" - http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) - - gcp_metadata = None - - def __init__(self, cloud_provider: str = "") -> None: - CloudResourceContextIntegration.cloud_provider = cloud_provider - - @classmethod - def _is_aws(cls) -> bool: - try: - r = cls.http.request( - "PUT", - AWS_TOKEN_URL, - headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, - ) - - if r.status != 200: - return False - - cls.aws_token = r.data.decode() - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking AWS metadata service: %s", str(e)) - return False - - @classmethod - def _get_aws_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.AWS, - "cloud.platform": CLOUD_PLATFORM.AWS_EC2, - } - - try: - r = cls.http.request( - "GET", - AWS_METADATA_URL, - headers={"X-aws-ec2-metadata-token": cls.aws_token}, - ) - - if r.status != 200: - return ctx - - data = json.loads(r.data.decode("utf-8")) - - try: - ctx["cloud.account.id"] = data["accountId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = data["availabilityZone"] - except Exception: - pass - - try: - ctx["cloud.region"] = data["region"] - except Exception: - pass - - try: - ctx["host.id"] = data["instanceId"] - except Exception: - pass - - try: - ctx["host.type"] = data["instanceType"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching AWS metadata: %s", str(e)) - - return ctx - - @classmethod - def _is_gcp(cls) -> bool: - try: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return False - - cls.gcp_metadata = json.loads(r.data.decode("utf-8")) - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking GCP metadata service: %s", str(e)) - return False - - @classmethod - def _get_gcp_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.GCP, - "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, - } - - gcp_metadata = cls.gcp_metadata - try: - if cls.gcp_metadata is None: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return ctx - - gcp_metadata = json.loads(r.data.decode("utf-8")) - cls.gcp_metadata = gcp_metadata - - try: - ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( - "/" - )[-1] - except Exception: - pass - - try: - # only populated in google cloud run - ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] - except Exception: - pass - - try: - ctx["host.id"] = gcp_metadata["instance"]["id"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching GCP metadata: %s", str(e)) - - return ctx - - @classmethod - def _get_cloud_provider(cls) -> str: - if cls._is_aws(): - return CLOUD_PROVIDER.AWS - - if cls._is_gcp(): - return CLOUD_PROVIDER.GCP - - return "" - - @classmethod - def _get_cloud_resource_context(cls) -> "Dict[str, str]": - cloud_provider = ( - cls.cloud_provider - if cls.cloud_provider != "" - else CloudResourceContextIntegration._get_cloud_provider() - ) - if cloud_provider in context_getters.keys(): - return context_getters[cloud_provider]() - - return {} - - @staticmethod - def setup_once() -> None: - cloud_provider = CloudResourceContextIntegration.cloud_provider - unsupported_cloud_provider = ( - cloud_provider != "" and cloud_provider not in context_getters.keys() - ) - - if unsupported_cloud_provider: - logger.warning( - "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", - CloudResourceContextIntegration.cloud_provider, - list(context_getters.keys()), - ) - - context = CloudResourceContextIntegration._get_cloud_resource_context() - if context != {}: - set_context(CONTEXT_TYPE, context) - - -# Map with the currently supported cloud providers -# mapping to functions extracting the context -context_getters = { - CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, - CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, -} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py deleted file mode 100644 index 7abf3f6808..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/cohere.py +++ /dev/null @@ -1,303 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise - -try: - from cohere import ( - ChatStreamEndEvent, - NonStreamedChatResponse, - ) - from cohere.base_client import BaseCohere - from cohere.client import Client - - if TYPE_CHECKING: - from cohere import StreamedChatResponse -except ImportError: - raise DidNotEnable("Cohere not installed") - -try: - # cohere 5.9.3+ - from cohere import StreamEndStreamedChatResponse -except ImportError: - from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse - - -COLLECTED_CHAT_PARAMS = { - "model": SPANDATA.AI_MODEL_ID, - "k": SPANDATA.AI_TOP_K, - "p": SPANDATA.AI_TOP_P, - "seed": SPANDATA.AI_SEED, - "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, - "raw_prompting": SPANDATA.AI_RAW_PROMPTING, -} - -COLLECTED_PII_CHAT_PARAMS = { - "tools": SPANDATA.AI_TOOLS, - "preamble": SPANDATA.AI_PREAMBLE, -} - -COLLECTED_CHAT_RESP_ATTRS = { - "generation_id": SPANDATA.AI_GENERATION_ID, - "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, - "finish_reason": SPANDATA.AI_FINISH_REASON, -} - -COLLECTED_PII_CHAT_RESP_ATTRS = { - "citations": SPANDATA.AI_CITATIONS, - "documents": SPANDATA.AI_DOCUMENTS, - "search_queries": SPANDATA.AI_SEARCH_QUERIES, - "search_results": SPANDATA.AI_SEARCH_RESULTS, - "tool_calls": SPANDATA.AI_TOOL_CALLS, -} - - -class CohereIntegration(Integration): - identifier = "cohere" - origin = f"auto.ai.{identifier}" - - def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) - Client.embed = _wrap_embed(Client.embed) - BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "cohere", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _end_span(span: "Any") -> None: - if isinstance(span, StreamedSpan): - span.end() - else: - span.__exit__(None, None, None) - - -def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": - def collect_chat_response_fields( - span: "Union[Span, StreamedSpan]", - res: "NonStreamedChatResponse", - include_pii: bool, - ) -> None: - if include_pii: - if hasattr(res, "text"): - set_data_normalized( - span, - SPANDATA.AI_RESPONSES, - [res.text], - ) - for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: - if hasattr(res, pii_attr): - set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) - - for attr in COLLECTED_CHAT_RESP_ATTRS: - if hasattr(res, attr): - set_data_normalized(span, "ai." + attr, getattr(res, attr)) - - if hasattr(res, "meta"): - if hasattr(res.meta, "billed_units"): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - output_tokens=res.meta.billed_units.output_tokens, - ) - elif hasattr(res.meta, "tokens"): - record_token_usage( - span, - input_tokens=res.meta.tokens.input_tokens, - output_tokens=res.meta.tokens.output_tokens, - ) - - if hasattr(res.meta, "warnings"): - set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) - - @wraps(f) - def new_chat(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if ( - integration is None - or "message" not in kwargs - or not isinstance(kwargs.get("message"), str) - ): - return f(*args, **kwargs) - - message = kwargs.get("message") - - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name="cohere.client.Chat", - attributes={ - "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - name="cohere.client.Chat", - origin=CohereIntegration.origin, - ) - span.__enter__() - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - - reraise(*exc_info) - - with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, - SPANDATA.AI_INPUT_MESSAGES, - list( - map( - lambda x: { - "role": getattr(x, "role", "").lower(), - "content": getattr(x, "message", ""), - }, - kwargs.get("chat_history", []), - ) - ) - + [{"role": "user", "content": message}], - ) - for k, v in COLLECTED_PII_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - - for k, v in COLLECTED_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - set_data_normalized(span, SPANDATA.AI_STREAMING, False) - - if streaming: - old_iterator = res - - def new_iterator() -> "Iterator[StreamedChatResponse]": - with capture_internal_exceptions(): - for x in old_iterator: - if isinstance(x, ChatStreamEndEvent) or isinstance( - x, StreamEndStreamedChatResponse - ): - collect_chat_response_fields( - span, - x.response, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - yield x - _end_span(span) - - return new_iterator() - elif isinstance(res, NonStreamedChatResponse): - collect_chat_response_fields( - span, - res, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - _end_span(span) - else: - set_data_normalized(span, "unknown_response", True) - _end_span(span) - return res - - return new_chat - - -def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - if integration is None: - return f(*args, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name="Cohere Embedding Creation", - attributes={ - "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span_ctx = get_start_span_function()( - op=consts.OP.COHERE_EMBEDDINGS_CREATE, - name="Cohere Embedding Creation", - origin=CohereIntegration.origin, - ) - - with span_ctx as span: - if "texts" in kwargs and ( - should_send_default_pii() and integration.include_prompts - ): - if isinstance(kwargs["texts"], str): - set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) - elif ( - isinstance(kwargs["texts"], list) - and len(kwargs["texts"]) > 0 - and isinstance(kwargs["texts"][0], str) - ): - set_data_normalized( - span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] - ) - - if "model" in kwargs: - set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - reraise(*exc_info) - if ( - hasattr(res, "meta") - and hasattr(res.meta, "billed_units") - and hasattr(res.meta.billed_units, "input_tokens") - ): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - total_tokens=res.meta.billed_units.input_tokens, - ) - return res - - return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py deleted file mode 100644 index a0e9014666..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dedupe.py +++ /dev/null @@ -1,62 +0,0 @@ -import weakref -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class DedupeIntegration(Integration): - identifier = "dedupe" - - def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if hint is None: - return event - - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return event - - exc_info = hint.get("exc_info", None) - if exc_info is None: - return event - - last_seen = integration._last_seen.get(None) - if last_seen is not None: - # last_seen is either a weakref or the original instance - last_seen = ( - last_seen() if isinstance(last_seen, weakref.ref) else last_seen - ) - - exc = exc_info[1] - if last_seen is exc: - logger.info("DedupeIntegration dropped duplicated error event %s", exc) - return None - - # we can only weakref non builtin types - try: - integration._last_seen.set(weakref.ref(exc)) - except TypeError: - integration._last_seen.set(exc) - - return event - - @staticmethod - def reset_last_seen() -> None: - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return - - integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py deleted file mode 100644 index 361b60079d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/__init__.py +++ /dev/null @@ -1,884 +0,0 @@ -import inspect -import sys -import threading -import weakref -from importlib import import_module - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANNAME -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - transaction_from_function, - walk_exception_chain, -) - -try: - from django import VERSION as DJANGO_VERSION - from django.conf import settings - from django.conf import settings as django_settings - from django.core import signals - from django.utils.functional import SimpleLazyObject - - try: - from django.urls import resolve - except ImportError: - from django.core.urlresolvers import resolve - - try: - from django.urls import Resolver404 - except ImportError: - from django.core.urlresolvers import Resolver404 - - # Only available in Django 3.0+ - try: - from django.core.handlers.asgi import ASGIRequest - except Exception: - ASGIRequest = None - -except ImportError: - raise DidNotEnable("Django not installed") - -from sentry_sdk.integrations.django.middleware import patch_django_middlewares -from sentry_sdk.integrations.django.signals_handlers import patch_signals -from sentry_sdk.integrations.django.tasks import patch_tasks -from sentry_sdk.integrations.django.templates import ( - get_template_frame_from_exception, - patch_templates, -) -from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER -from sentry_sdk.integrations.django.views import patch_views - -if DJANGO_VERSION[:2] > (1, 8): - from sentry_sdk.integrations.django.caching import patch_caching -else: - patch_caching = None # type: ignore - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from django.core.handlers.wsgi import WSGIRequest - from django.http.request import QueryDict - from django.http.response import HttpResponse - from django.utils.datastructures import MultiValueDict - - from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -if DJANGO_VERSION < (1, 10): - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated() - -else: - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated - - -TRANSACTION_STYLE_VALUES = ("function_name", "url") - - -class DjangoIntegration(Integration): - """ - Auto instrument a Django application. - - :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. - :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. - :param signals_spans: Whether to create spans for signals. Defaults to `True`. - :param signals_denylist: A list of signals to ignore when creating spans. - :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. - """ - - identifier = "django" - origin = f"auto.http.{identifier}" - origin_db = f"auto.db.{identifier}" - - transaction_style = "" - middleware_spans: "Optional[bool]" = None - signals_spans: "Optional[bool]" = None - cache_spans: "Optional[bool]" = None - signals_denylist: "list[signals.Signal]" = [] - - def __init__( - self, - transaction_style: str = "url", - middleware_spans: bool = False, - signals_spans: bool = True, - cache_spans: bool = False, - db_transaction_spans: bool = False, - signals_denylist: "Optional[list[signals.Signal]]" = None, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - - self.signals_spans = signals_spans - self.signals_denylist = signals_denylist or [] - - self.cache_spans = cache_spans - self.db_transaction_spans = db_transaction_spans - - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(DjangoIntegration, DJANGO_VERSION) - - install_sql_hook() - # Patch in our custom middleware. - - # logs an error for every 500 - ignore_logger("django.server") - ignore_logger("django.request") - - from django.core.handlers.wsgi import WSGIHandler - - old_app = WSGIHandler.__call__ - - @ensure_integration_enabled(DjangoIntegration, old_app) - def sentry_patched_wsgi_handler( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - bound_old_app = old_app.__get__(self, WSGIHandler) - - from django.conf import settings - - use_x_forwarded_for = settings.USE_X_FORWARDED_HOST - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - middleware = SentryWsgiMiddleware( - bound_old_app, - use_x_forwarded_for, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - WSGIHandler.__call__ = sentry_patched_wsgi_handler - - _patch_get_response() - - _patch_django_asgi_handler() - - signals.got_request_exception.connect(_got_request_exception) - - @add_global_event_processor - def process_django_templates( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_, exc_value, _) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - frame = get_template_frame_from_exception(exc_value) - if frame is not None: - frames = exception.get("stacktrace", {}).get("frames", []) - - for i in reversed(range(len(frames))): - f = frames[i] - if ( - f.get("function") in ("Parser.parse", "parse", "render") - and f.get("module") == "django.template.base" - ): - i += 1 - break - else: - i = len(frames) - - frames.insert(i, frame) - - return event - - @add_global_repr_processor - def _django_queryset_repr( - value: "Any", hint: "Dict[str, Any]" - ) -> "Union[NotImplementedType, str]": - try: - # Django 1.6 can fail to import `QuerySet` when Django settings - # have not yet been initialized. - # - # If we fail to import, return `NotImplemented`. It's at least - # unlikely that we have a query set in `value` when importing - # `QuerySet` fails. - from django.db.models.query import QuerySet - except Exception: - return NotImplemented - - if not isinstance(value, QuerySet) or value._result_cache: - return NotImplemented - - return "<%s from %s at 0x%x>" % ( - value.__class__.__name__, - value.__module__, - id(value), - ) - - _patch_channels() - patch_django_middlewares() - patch_views() - patch_templates() - patch_signals() - patch_tasks() - add_template_context_repr_sequence() - - if patch_caching is not None: - patch_caching() - - -_DRF_PATCHED = False -_DRF_PATCH_LOCK = threading.Lock() - - -def _patch_drf() -> None: - """ - Patch Django Rest Framework for more/better request data. DRF's request - type is a wrapper around Django's request type. The attribute we're - interested in is `request.data`, which is a cached property containing a - parsed request body. Reading a request body from that property is more - reliable than reading from any of Django's own properties, as those don't - hold payloads in memory and therefore can only be accessed once. - - We patch the Django request object to include a weak backreference to the - DRF request object, such that we can later use either in - `DjangoRequestExtractor`. - - This function is not called directly on SDK setup, because importing almost - any part of Django Rest Framework will try to access Django settings (where - `sentry_sdk.init()` might be called from in the first place). Instead we - run this function on every request and do the patching on the first - request. - """ - - global _DRF_PATCHED - - if _DRF_PATCHED: - # Double-checked locking - return - - with _DRF_PATCH_LOCK: - if _DRF_PATCHED: - return - - # We set this regardless of whether the code below succeeds or fails. - # There is no point in trying to patch again on the next request. - _DRF_PATCHED = True - - with capture_internal_exceptions(): - try: - from rest_framework.views import APIView # type: ignore - except ImportError: - pass - else: - old_drf_initial = APIView.initial - - def sentry_patched_drf_initial( - self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - with capture_internal_exceptions(): - request._request._sentry_drf_request_backref = weakref.ref( - request - ) - pass - return old_drf_initial(self, request, *args, **kwargs) - - APIView.initial = sentry_patched_drf_initial - - -def _patch_channels() -> None: - try: - from channels.http import AsgiHandler # type: ignore - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl - - patch_channels_asgi_handler_impl(AsgiHandler) - - -def _patch_django_asgi_handler() -> None: - try: - from django.core.handlers.asgi import ASGIHandler - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl - - patch_django_asgi_handler_impl(ASGIHandler) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" -) -> None: - try: - transaction_name = None - if transaction_style == "function_name": - fn = resolve(request.path).func - transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) - - elif transaction_style == "url": - if hasattr(request, "urlconf"): - transaction_name = LEGACY_RESOLVER.resolve( - request.path_info, urlconf=request.urlconf - ) - else: - transaction_name = LEGACY_RESOLVER.resolve(request.path_info) - - if transaction_name is None: - transaction_name = request.path_info - source = TransactionSource.URL - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name( - transaction_name, - source=source, - ) - except Resolver404: - urlconf = import_module(settings.ROOT_URLCONF) - # This exception only gets thrown when transaction_style is `function_name` - # So we don't check here what style is configured - if hasattr(urlconf, "handler404"): - handler = urlconf.handler404 - if isinstance(handler, str): - scope.transaction = handler - else: - scope.transaction = transaction_from_function( - getattr(handler, "view_class", handler) - ) - except Exception: - pass - - -def _before_get_response(request: "WSGIRequest") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return - - _patch_drf() - - scope = sentry_sdk.get_current_scope() - # Rely on WSGI middleware to start a trace - _set_transaction_name_and_source(scope, integration.transaction_style, request) - - scope.add_event_processor( - _make_wsgi_request_event_processor(weakref.ref(request), integration) - ) - - -def _attempt_resolve_again( - request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str -) -> None: - """ - Some django middlewares overwrite request.urlconf - so we need to respect that contract, - so we try to resolve the url again. - """ - if not hasattr(request, "urlconf"): - return - - _set_transaction_name_and_source(scope, transaction_style, request) - - -def _after_get_response(request: "WSGIRequest") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming and should_send_default_pii(): - user = getattr(request, "user", None) - - # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. - # Exit early if the user has not been materialized yet. - is_lazy = isinstance(user, SimpleLazyObject) - if is_lazy and hasattr(request, "_cached_user"): - user = request._cached_user - elif is_lazy: - return - - if user is None or not is_authenticated(user): - return - - user_info = {} - try: - user_info["id"] = str(user.pk) - except Exception: - pass - - try: - user_info["email"] = user.email - except Exception: - pass - - try: - user_info["username"] = user.get_username() - except Exception: - pass - - sentry_sdk.set_user(user_info) - - -def _patch_get_response() -> None: - """ - patch get_response, because at that point we have the Django request object - """ - from django.core.handlers.base import BaseHandler - - old_get_response = BaseHandler.get_response - - def sentry_patched_get_response( - self: "Any", request: "WSGIRequest" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - rv = old_get_response(self, request) - _after_get_response(request) - return rv - - BaseHandler.get_response = sentry_patched_get_response - - if hasattr(BaseHandler, "get_response_async"): - from sentry_sdk.integrations.django.asgi import patch_get_response_async - - patch_get_response_async(BaseHandler, _before_get_response) - - -def _make_wsgi_request_event_processor( - weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" -) -> "EventProcessor": - def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - request = weak_request() - if request is None: - return event - - django_3 = ASGIRequest is not None - if django_3 and type(request) == ASGIRequest: - # We have a `asgi_request_event_processor` for this. - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return wsgi_request_event_processor - - -def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if request is not None and integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - event, hint = event_from_exception( - sys.exc_info(), - client_options=client.options, - mechanism={"type": "django", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class DjangoRequestExtractor(RequestExtractor): - def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: - try: - drf_request = request._sentry_drf_request_backref() - if drf_request is not None: - request = drf_request - except AttributeError: - pass - self.request = request - - def env(self) -> "Dict[str, str]": - return self.request.META - - def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": - privacy_cookies = [ - django_settings.CSRF_COOKIE_NAME, - django_settings.SESSION_COOKIE_NAME, - ] - - clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} - for key, val in self.request.COOKIES.items(): - if key in privacy_cookies: - clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE - else: - clean_cookies[key] = val - - return clean_cookies - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "QueryDict": - return self.request.POST - - def files(self) -> "MultiValueDict": - return self.request.FILES - - def size_of_file(self, file: "Any") -> int: - return file.size - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - return self.request.data - except Exception: - return RequestExtractor.parsed_body(self) - - -def _set_user_info(request: "WSGIRequest", event: "Event") -> None: - user_info = event.setdefault("user", {}) - - user = getattr(request, "user", None) - - if user is None or not is_authenticated(user): - return - - try: - user_info.setdefault("id", str(user.pk)) - except Exception: - pass - - try: - user_info.setdefault("email", user.email) - except Exception: - pass - - try: - user_info.setdefault("username", user.get_username()) - except Exception: - pass - - -def install_sql_hook() -> None: - """If installed this causes Django's queries to be captured.""" - try: - from django.db.backends.utils import CursorWrapper - except ImportError: - from django.db.backends.util import CursorWrapper - - try: - # django 1.6 and 1.7 compatability - from django.db.backends import BaseDatabaseWrapper - except ImportError: - # django 1.8 or later - from django.db.backends.base.base import BaseDatabaseWrapper - - try: - real_execute = CursorWrapper.execute - real_executemany = CursorWrapper.executemany - real_connect = BaseDatabaseWrapper.connect - real_commit = BaseDatabaseWrapper._commit - real_rollback = BaseDatabaseWrapper._rollback - except AttributeError: - # This won't work on Django versions < 1.6 - return - - @ensure_integration_enabled(DjangoIntegration, real_execute) - def execute( - self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=params, - paramstyle="format", - executemany=False, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - result = real_execute(self, sql, params) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_executemany) - def executemany( - self: "CursorWrapper", sql: "Any", param_list: "List[Any]" - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=param_list, - paramstyle="format", - executemany=True, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - - result = real_executemany(self, sql, param_list) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_connect) - def connect(self: "BaseDatabaseWrapper") -> None: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message="connect", category="query") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="connect", - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self) - return real_connect(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - return real_connect(self) - - def _commit(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_commit(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_COMMIT, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_COMMIT, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - - def _rollback(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_rollback(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_ROLLBACK, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_ROLLBACK, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - - CursorWrapper.execute = execute - CursorWrapper.executemany = executemany - BaseDatabaseWrapper.connect = connect - BaseDatabaseWrapper._commit = _commit - BaseDatabaseWrapper._rollback = _rollback - ignore_logger("django.db.backends") - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", - cursor_or_db: "Any", - db_operation: "Optional[str]" = None, -) -> None: - db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db - vendor = db.vendor - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) - - if db_operation is not None: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) - else: - span.set_data(SPANDATA.DB_SYSTEM, vendor) - - if db_operation is not None: - span.set_data(SPANDATA.DB_OPERATION, db_operation) - - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` - # actually has a `connection` and the `connection` has a `get_dsn_parameters` - # attribute, only to throw an error once you actually want to call it. - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable - # function. - is_psycopg2 = ( - hasattr(cursor_or_db, "connection") - and hasattr(cursor_or_db.connection, "get_dsn_parameters") - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) - ) - if is_psycopg2: - connection_params = cursor_or_db.connection.get_dsn_parameters() - else: - try: - # psycopg3, only extract needed params as get_parameters - # can be slow because of the additional logic to filter out default - # values - connection_params = { - "dbname": cursor_or_db.connection.info.dbname, - "port": cursor_or_db.connection.info.port, - } - # PGhost returns host or base dir of UNIX socket as an absolute path - # starting with /, use it only when it contains host - pg_host = cursor_or_db.connection.info.host - if pg_host and not pg_host.startswith("/"): - connection_params["host"] = pg_host - except Exception: - connection_params = db.get_connection_params() - - db_name = connection_params.get("dbname") or connection_params.get("database") - - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - - set_on_span = span.set_attribute - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - set_on_span = span.set_data - - server_address = connection_params.get("host") - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = connection_params.get("port") - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, str(server_port)) - - server_socket_address = connection_params.get("unix_socket") - if server_socket_address is not None: - set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) - - -def add_template_context_repr_sequence() -> None: - try: - from django.template.context import BaseContext - - add_repr_sequence_type(BaseContext) - except Exception: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py deleted file mode 100644 index 43faffb5be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/asgi.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Instrumentation for Django 3.0 - -Since this file contains `async def` it is conditionally imported in -`sentry_sdk.integrations.django` (depending on the existence of -`django.core.handlers.asgi`. -""" - -import asyncio -import functools -import inspect -from typing import TYPE_CHECKING - -from django.core.handlers.wsgi import WSGIRequest - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, TypeVar, Union - - from django.core.handlers.asgi import ASGIRequest - from django.http.response import HttpResponse - - from sentry_sdk._types import Event, EventProcessor - - _F = TypeVar("_F", bound=Callable[..., Any]) - - -# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for -# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. -# The latter is replaced with the inspect.markcoroutinefunction decorator. -# Until 3.12 is the minimum supported Python version, provide a shim. -# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py -if hasattr(inspect, "markcoroutinefunction"): - iscoroutinefunction = inspect.iscoroutinefunction - markcoroutinefunction = inspect.markcoroutinefunction -else: - iscoroutinefunction = asyncio.iscoroutinefunction - - def markcoroutinefunction(func: "_F") -> "_F": - func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore - return func - - -def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": - def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - from sentry_sdk.integrations.django import ( - DjangoRequestExtractor, - _set_user_info, - ) - - if request is None: - return event - - if type(request) == WSGIRequest: - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return asgi_request_event_processor - - -def patch_django_asgi_handler_impl(cls: "Any") -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - )._run_asgi3 - - return await middleware(scope, receive, send) - - cls.__call__ = sentry_patched_asgi_handler - - modern_django_asgi_support = hasattr(cls, "create_request") - if modern_django_asgi_support: - old_create_request = cls.create_request - - @ensure_integration_enabled(DjangoIntegration, old_create_request) - def sentry_patched_create_request( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request, error_response = old_create_request(self, *args, **kwargs) - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_make_asgi_request_event_processor(request)) - - return request, error_response - - cls.create_request = sentry_patched_create_request - - -def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: - old_get_response_async = cls.get_response_async - - async def sentry_patched_get_response_async( - self: "Any", request: "Any" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - return await old_get_response_async(self, request) - - cls.get_response_async = sentry_patched_get_response_async - - -def patch_channels_asgi_handler_impl(cls: "Any") -> None: - import channels # type: ignore - - from sentry_sdk.integrations.django import DjangoIntegration - - if channels.__version__ < "3.0.0": - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, receive, send) - - middleware = SentryAsgiMiddleware( - lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - ) - - return await middleware(self.scope)(receive, send) # type: ignore - - cls.__call__ = sentry_patched_asgi_handler - - else: - # The ASGI handler in Channels >= 3 has the same signature as - # the Django handler. - patch_django_asgi_handler_impl(cls) - - -def wrap_async_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - async def sentry_wrapped_callback( - request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return await callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return await callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return await callback(request, *args, **kwargs) - - return sentry_wrapped_callback - - -def _asgi_middleware_mixin_factory( - _check_middleware_span: "Callable[..., Any]", -) -> "Any": - """ - Mixin class factory that generates a middleware mixin for handling requests - in async mode. - """ - - class SentryASGIMixin: - if TYPE_CHECKING: - _inner = None - - def __init__(self, get_response: "Callable[..., Any]") -> None: - self.get_response = get_response - self._acall_method = None - self._async_check() - - def _async_check(self) -> None: - """ - If get_response is a coroutine function, turns us into async mode so - a thread is not consumed during a whole request. - Taken from django.utils.deprecation::MiddlewareMixin._async_check - """ - if iscoroutinefunction(self.get_response): - markcoroutinefunction(self) - - def async_route_check(self) -> bool: - """ - Function that checks if we are in async mode, - and if we are forwards the handling of requests to __acall__ - """ - return iscoroutinefunction(self.get_response) - - async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": - f = self._acall_method - if f is None: - if hasattr(self._inner, "__acall__"): - self._acall_method = f = self._inner.__acall__ # type: ignore - else: - self._acall_method = f = self._inner - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return await f(*args, **kwargs) # type: ignore - - with middleware_span: - return await f(*args, **kwargs) # type: ignore - - return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py deleted file mode 100644 index faf1803c11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/caching.py +++ /dev/null @@ -1,264 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.core.cache import CacheHandler -from urllib3.util import parse_url as urlparse - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - -METHODS_TO_INSTRUMENT = [ - "set", - "set_many", - "get", - "get_many", -] - - -def _get_span_description( - method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" -) -> str: - return _key_as_string(_get_safe_key(method_name, args, kwargs)) - - -def _patch_cache_method( - cache: "CacheHandler", - method_name: str, - address: "Optional[str]", - port: "Optional[int]", -) -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - original_method = getattr(cache, method_name) - - @ensure_integration_enabled(DjangoIntegration, original_method) - def _instrument_call( - cache: "CacheHandler", - method_name: str, - original_method: "Callable[..., Any]", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - address: "Optional[str]", - port: "Optional[int]", - ) -> "Any": - is_set_operation = method_name.startswith("set") - is_get_method = method_name == "get" - is_get_many_method = method_name == "get_many" - - op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET - description = _get_span_description(method_name, args, kwargs) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": op, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_attribute(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - else: - with sentry_sdk.start_span( - op=op, - name=description, - origin=DjangoIntegration.origin, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_data(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_data(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - - @functools.wraps(original_method) - def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": - return _instrument_call( - cache, method_name, original_method, args, kwargs, address, port - ) - - setattr(cache, method_name, sentry_method) - - -def _patch_cache( - cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None -) -> None: - if not hasattr(cache, "_sentry_patched"): - for method_name in METHODS_TO_INSTRUMENT: - _patch_cache_method(cache, method_name, address, port) - cache._sentry_patched = True - - -def _get_address_port( - settings: "dict[str, Any]", -) -> "tuple[Optional[str], Optional[int]]": - location = settings.get("LOCATION") - - # TODO: location can also be an array of locations - # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis - # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 - if not isinstance(location, str): - return None, None - - if "://" in location: - parsed_url = urlparse(location) - # remove the username and password from URL to not leak sensitive data. - address = "{}://{}{}".format( - parsed_url.scheme or "", - parsed_url.hostname or "", - parsed_url.path or "", - ) - port = parsed_url.port - else: - address = location - port = None - - return address, int(port) if port is not None else None - - -def should_enable_cache_spans() -> bool: - from sentry_sdk.integrations.django import DjangoIntegration - - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - from django.conf import settings - - return integration is not None and ( - (client.spotlight is not None and settings.DEBUG is True) - or integration.cache_spans is True - ) - - -def patch_caching() -> None: - if not hasattr(CacheHandler, "_sentry_patched"): - if DJANGO_VERSION < (3, 2): - original_get_item = CacheHandler.__getitem__ - - @functools.wraps(original_get_item) - def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": - cache = original_get_item(self, alias) - - if should_enable_cache_spans(): - from django.conf import settings - - address, port = _get_address_port( - settings.CACHES[alias or "default"] - ) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.__getitem__ = sentry_get_item - CacheHandler._sentry_patched = True - - else: - original_create_connection = CacheHandler.create_connection - - @functools.wraps(original_create_connection) - def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": - cache = original_create_connection(self, alias) - - if should_enable_cache_spans(): - address, port = _get_address_port(self.settings[alias or "default"]) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.create_connection = sentry_create_connection - CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py deleted file mode 100644 index a14ec96ff5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/middleware.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Create spans from Django middleware invocations -""" - -from functools import wraps -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - F = TypeVar("F", bound=Callable[..., Any]) - -_import_string_should_wrap_middleware = ContextVar( - "import_string_should_wrap_middleware" -) - -DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) - -if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: - _asgi_middleware_mixin_factory = lambda _: object - iscoroutinefunction = lambda _: False -else: - from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction - - -def patch_django_middlewares() -> None: - from django.core.handlers import base - - old_import_string = base.import_string - - def sentry_patched_import_string(dotted_path: str) -> "Any": - rv = old_import_string(dotted_path) - - if _import_string_should_wrap_middleware.get(None): - rv = _wrap_middleware(rv, dotted_path) - - return rv - - base.import_string = sentry_patched_import_string - - old_load_middleware = base.BaseHandler.load_middleware - - def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": - _import_string_should_wrap_middleware.set(True) - try: - return old_load_middleware(*args, **kwargs) - finally: - _import_string_should_wrap_middleware.set(False) - - base.BaseHandler.load_middleware = sentry_patched_load_middleware - - -def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - def _check_middleware_span( - old_method: "Callable[..., Any]", - ) -> "Optional[Union[Span, StreamedSpan]]": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None or not integration.middleware_spans: - return None - - function_name = transaction_from_function(old_method) - - description = middleware_name - function_basename = getattr(old_method, "__name__", None) - if function_basename: - description = "{}.{}".format(description, function_basename) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - middleware_span: "Union[Span, StreamedSpan]" - if span_streaming: - middleware_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.MIDDLEWARE_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - else: - middleware_span = sentry_sdk.start_span( - op=OP.MIDDLEWARE_DJANGO, - name=description, - origin=DjangoIntegration.origin, - ) - middleware_span.set_tag("django.function_name", function_name) - middleware_span.set_tag("django.middleware_name", middleware_name) - - return middleware_span - - def _get_wrapped_method(old_method: "F") -> "F": - with capture_internal_exceptions(): - # Middleware hooks (e.g. `process_view`, `process_exception`) may be - # `async def` when the middleware is async. A synchronous wrapper - # would hide the coroutine from Django's `iscoroutinefunction` check, - # causing Django to call the hook synchronously and never await the - # returned coroutine. Wrap async hooks with an async wrapper so the - # wrapped method continues to report as a coroutine function. - if iscoroutinefunction is not None and iscoroutinefunction(old_method): - - async def async_sentry_wrapped_method( - *args: "Any", **kwargs: "Any" - ) -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return await old_method(*args, **kwargs) - - with middleware_span: - return await old_method(*args, **kwargs) - - sentry_wrapped_method = async_sentry_wrapped_method - - else: - - def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return old_method(*args, **kwargs) - - with middleware_span: - return old_method(*args, **kwargs) - - sentry_wrapped_method = sync_sentry_wrapped_method - - try: - # fails for __call__ of function on Python 2 (see py2.7-django-1.11) - sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) - - # Necessary for Django 3.1 - sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore - except Exception: - pass - - return sentry_wrapped_method # type: ignore - - return old_method - - class SentryWrappingMiddleware( - _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore - ): - sync_capable = getattr(middleware, "sync_capable", True) - async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( - middleware, "async_capable", False - ) - - def __init__( - self, - get_response: "Optional[Callable[..., Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> None: - if get_response: - self._inner = middleware(get_response, *args, **kwargs) - else: - self._inner = middleware(*args, **kwargs) - self.get_response = get_response - self._call_method = None - if self.async_capable: - super().__init__(get_response) - - # We need correct behavior for `hasattr()`, which we can only determine - # when we have an instance of the middleware we're wrapping. - def __getattr__(self, method_name: str) -> "Any": - if method_name not in ( - "process_request", - "process_view", - "process_template_response", - "process_response", - "process_exception", - ): - raise AttributeError() - - old_method = getattr(self._inner, method_name) - rv = _get_wrapped_method(old_method) - self.__dict__[method_name] = rv - return rv - - def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": - if hasattr(self, "async_route_check") and self.async_route_check(): - return self.__acall__(*args, **kwargs) - - f = self._call_method - if f is None: - self._call_method = f = self._inner.__call__ - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return f(*args, **kwargs) - - with middleware_span: - return f(*args, **kwargs) - - for attr in ( - "__name__", - "__module__", - "__qualname__", - ): - if hasattr(middleware, attr): - setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) - - return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py deleted file mode 100644 index 7140ead782..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/signals_handlers.py +++ /dev/null @@ -1,105 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from django.dispatch import Signal - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.django import DJANGO_VERSION -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Union - - -def _get_receiver_name(receiver: "Callable[..., Any]") -> str: - name = "" - - if hasattr(receiver, "__qualname__"): - name = receiver.__qualname__ - elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ - name = receiver.__name__ - elif hasattr( - receiver, "func" - ): # certain functions (like partials) dont have a name - if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): - name = "partial()" - - if ( - name == "" - ): # In case nothing was found, return the string representation (this is the slowest case) - return str(receiver) - - if hasattr(receiver, "__module__"): # prepend with module, if there is one - name = receiver.__module__ + "." + name - - return name - - -def patch_signals() -> None: - """ - Patch django signal receivers to create a span. - - This only wraps sync receivers. Django>=5.0 introduced async receivers, but - since we don't create transactions for ASGI Django, we don't wrap them. - """ - from sentry_sdk.integrations.django import DjangoIntegration - - old_live_receivers = Signal._live_receivers - - def _sentry_live_receivers( - self: "Signal", sender: "Any" - ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": - if DJANGO_VERSION >= (5, 0): - sync_receivers, async_receivers = old_live_receivers(self, sender) - else: - sync_receivers = old_live_receivers(self, sender) - async_receivers = [] - - def sentry_sync_receiver_wrapper( - receiver: "Callable[..., Any]", - ) -> "Callable[..., Any]": - @wraps(receiver) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - signal_name = _get_receiver_name(receiver) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - with sentry_sdk.traces.start_span( - name=signal_name, - attributes={ - "sentry.op": OP.EVENT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.CODE_FUNCTION_NAME: signal_name, - }, - ) as span: - return receiver(*args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.EVENT_DJANGO, - name=signal_name, - origin=DjangoIntegration.origin, - ) as span: - span.set_data("signal", signal_name) - return receiver(*args, **kwargs) - - return wrapper - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if ( - integration - and integration.signals_spans - and self not in integration.signals_denylist - ): - for idx, receiver in enumerate(sync_receivers): - sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) - - if DJANGO_VERSION >= (5, 0): - return sync_receivers, async_receivers - else: - return sync_receivers - - Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py deleted file mode 100644 index 5e23c258fb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/tasks.py +++ /dev/null @@ -1,52 +0,0 @@ -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import qualname_from_function - -try: - # django.tasks were added in Django 6.0 - from django.tasks.base import Task -except ImportError: - Task = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -def patch_tasks() -> None: - if Task is None: - return - - old_task_enqueue = Task.enqueue - - @wraps(old_task_enqueue) - def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return old_task_enqueue(self, *args, **kwargs) - - name = qualname_from_function(self.func) or "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_task_enqueue(self, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin - ): - return old_task_enqueue(self, *args, **kwargs) - - Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py deleted file mode 100644 index 5ab89d4a74..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/templates.py +++ /dev/null @@ -1,209 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.template import TemplateSyntaxError -from django.utils.safestring import mark_safe - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Optional, Tuple - -try: - # support Django 1.9 - from django.template.base import Origin -except ImportError: - # backward compatibility - from django.template.loader import LoaderOrigin as Origin - - -def get_template_frame_from_exception( - exc_value: "Optional[BaseException]", -) -> "Optional[Dict[str, Any]]": - # As of Django 1.9 or so the new template debug thing showed up. - if hasattr(exc_value, "template_debug"): - return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore - - # As of r16833 (Django) all exceptions may contain a - # ``django_template_source`` attribute (rather than the legacy - # ``TemplateSyntaxError.source`` check) - if hasattr(exc_value, "django_template_source"): - return _get_template_frame_from_source( - exc_value.django_template_source # type: ignore - ) - - if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): - source = exc_value.source - if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): - return _get_template_frame_from_source(source) # type: ignore - - return None - - -def _get_template_name_description(template_name: str) -> str: - if isinstance(template_name, (list, tuple)): - if template_name: - return "[{}, ...]".format(template_name[0]) - else: - return template_name - - -def patch_templates() -> None: - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - real_rendered_content = SimpleTemplateResponse.rendered_content - - @property # type: ignore - @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) - def rendered_content(self: "SimpleTemplateResponse") -> str: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(self.template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_rendered_content.fget(self) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(self.template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", self.context_data) - return real_rendered_content.fget(self) - - SimpleTemplateResponse.rendered_content = rendered_content - - if DJANGO_VERSION < (1, 7): - return - import django.shortcuts - - real_render = django.shortcuts.render - - @functools.wraps(real_render) - @ensure_integration_enabled(DjangoIntegration, real_render) - def render( - request: "django.http.HttpRequest", - template_name: str, - context: "Optional[Dict[str, Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> "django.http.HttpResponse": - # Inject trace meta tags into template context - context = context or {} - if "sentry_trace_meta" not in context: - context["sentry_trace_meta"] = mark_safe( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_render(request, template_name, context, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", context) - return real_render(request, template_name, context, *args, **kwargs) - - django.shortcuts.render = render - - -def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": - if debug is None: - return None - - lineno = debug["line"] - filename = debug["name"] - if filename is None: - filename = "" - - pre_context = [] - post_context = [] - context_line = None - - for i, line in debug["source_lines"]: - if i < lineno: - pre_context.append(line) - elif i > lineno: - post_context.append(line) - else: - context_line = line - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - "in_app": True, - } - - -def _linebreak_iter(template_source: str) -> "Iterator[int]": - yield 0 - p = template_source.find("\n") - while p >= 0: - yield p + 1 - p = template_source.find("\n", p + 1) - - -def _get_template_frame_from_source( - source: "Tuple[Origin, Tuple[int, int]]", -) -> "Optional[Dict[str, Any]]": - if not source: - return None - - origin, (start, end) = source - filename = getattr(origin, "loadname", None) - if filename is None: - filename = "" - template_source = origin.reload() - lineno = None - upto = 0 - pre_context = [] - post_context = [] - context_line = None - - for num, next in enumerate(_linebreak_iter(template_source)): - line = template_source[upto:next] - if start >= upto and end <= next: - lineno = num - context_line = line - elif lineno is None: - pre_context.append(line) - else: - post_context.append(line) - - upto = next - - if context_line is None or lineno is None: - return None - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py deleted file mode 100644 index 192f0765e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/transactions.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Copied from raven-python. - -Despite being called "legacy" in some places this resolver is very much still -in use. -""" - -import re -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from re import Pattern - from typing import Dict, List, Optional, Tuple, Union - - from django.urls.resolvers import URLPattern, URLResolver - -from django import VERSION as DJANGO_VERSION - -if DJANGO_VERSION >= (2, 0): - from django.urls.resolvers import RoutePattern -else: - RoutePattern = None - -try: - from django.urls import get_resolver -except ImportError: - from django.core.urlresolvers import get_resolver - - -def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": - """Utility method for django's deprecated resolver.regex""" - try: - regex = resolver_or_pattern.regex - except AttributeError: - regex = resolver_or_pattern.pattern.regex - return regex - - -class RavenResolver: - _new_style_group_matcher = re.compile( - r"<(?:([^>:]+):)?([^>]+)>" - ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 - _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") - _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") - _non_named_group_matcher = re.compile(r"\([^\)]+\)") - # [foo|bar|baz] - _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") - _camel_re = re.compile(r"([A-Z]+)([a-z])") - - _cache: "Dict[URLPattern, str]" = {} - - def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: - r""" - Clean up urlpattern regexes into something readable by humans: - - From: - > "^(?P\w+)/athletes/(?P\w+)/$" - - To: - > "{sport_slug}/athletes/{athlete_slug}/" - """ - # "new-style" path patterns can be parsed directly without turning them - # into regexes first - if ( - RoutePattern is not None - and hasattr(pattern, "pattern") - and isinstance(pattern.pattern, RoutePattern) - ): - return self._new_style_group_matcher.sub( - lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) - ) - - result = get_regex(pattern).pattern - - # remove optional params - # TODO(dcramer): it'd be nice to change these into [%s] but it currently - # conflicts with the other rules because we're doing regexp matches - # rather than parsing tokens - result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) - - # handle named groups first - result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) - - # handle non-named groups - result = self._non_named_group_matcher.sub("{var}", result) - - # handle optional params - result = self._either_option_matcher.sub(lambda m: m.group(1), result) - - # clean up any outstanding regex-y characters. - result = ( - result.replace("^", "") - .replace("$", "") - .replace("?", "") - .replace("\\A", "") - .replace("\\Z", "") - .replace("//", "/") - .replace("\\", "") - ) - - return result - - def _resolve( - self, - resolver: "URLResolver", - path: str, - parents: "Optional[List[URLResolver]]" = None, - ) -> "Optional[str]": - match = get_regex(resolver).search(path) # Django < 2.0 - - if not match: - return None - - if parents is None: - parents = [resolver] - elif resolver not in parents: - parents = parents + [resolver] - - new_path = path[match.end() :] - for pattern in resolver.url_patterns: - # this is an include() - if not pattern.callback: - match_ = self._resolve(pattern, new_path, parents) - if match_: - return match_ - continue - elif not get_regex(pattern).search(new_path): - continue - - try: - return self._cache[pattern] - except KeyError: - pass - - prefix = "".join(self._simplify(p) for p in parents) - result = prefix + self._simplify(pattern) - if not result.startswith("/"): - result = "/" + result - self._cache[pattern] = result - return result - - return None - - def resolve( - self, - path: str, - urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, - ) -> "Optional[str]": - resolver = get_resolver(urlconf) - match = self._resolve(resolver, path) - return match - - -LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py deleted file mode 100644 index cf3012a75e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/django/views.py +++ /dev/null @@ -1,127 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -try: - from asyncio import iscoroutinefunction -except ImportError: - iscoroutinefunction = None # type: ignore - - -try: - from sentry_sdk.integrations.django.asgi import wrap_async_view -except (ImportError, SyntaxError): - wrap_async_view = None # type: ignore - - -def patch_views() -> None: - from django.core.handlers.base import BaseHandler - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - old_make_view_atomic = BaseHandler.make_view_atomic - old_render = SimpleTemplateResponse.render - - def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="serialize response", - attributes={ - "sentry.op": OP.VIEW_RESPONSE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_render(self) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RESPONSE_RENDER, - name="serialize response", - origin=DjangoIntegration.origin, - ): - return old_render(self) - - @functools.wraps(old_make_view_atomic) - def sentry_patched_make_view_atomic( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - callback = old_make_view_atomic(self, *args, **kwargs) - - # XXX: The wrapper function is created for every request. Find more - # efficient way to wrap views (or build a cache?) - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is not None: - is_async_view = ( - iscoroutinefunction is not None - and wrap_async_view is not None - and iscoroutinefunction(callback) - ) - if is_async_view: - sentry_wrapped_callback = wrap_async_view(callback) - else: - sentry_wrapped_callback = _wrap_sync_view(callback) - - else: - sentry_wrapped_callback = callback - - return sentry_wrapped_callback - - SimpleTemplateResponse.render = sentry_patched_render - BaseHandler.make_view_atomic = sentry_patched_make_view_atomic - - -def _wrap_sync_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - # set the active thread id to the handler thread for sync views - # this isn't necessary for async views since that runs on main - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return callback(request, *args, **kwargs) - - return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py deleted file mode 100644 index 310766ee3a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/dramatiq.py +++ /dev/null @@ -1,246 +0,0 @@ -import json -from typing import TypeVar - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - event_from_exception, -) - -R = TypeVar("R") - -try: - from dramatiq.broker import Broker - from dramatiq.errors import Retry - from dramatiq.message import Message - from dramatiq.middleware import Middleware, default_middleware -except ImportError: - raise DidNotEnable("Dramatiq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Union - - from sentry_sdk._types import Event, Hint - - -class DramatiqIntegration(Integration): - """ - Dramatiq integration for Sentry - - Please make sure that you call `sentry_sdk.init` *before* initializing - your broker, as it monkey patches `Broker.__init__`. - - This integration was originally developed and maintained - by https://github.com/jacobsvante and later donated to the Sentry - project. - """ - - identifier = "dramatiq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_dramatiq_broker() - - -def _patch_dramatiq_broker() -> None: - original_broker__init__ = Broker.__init__ - - def sentry_patched_broker__init__( - self: "Broker", *args: "Any", **kw: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - - try: - middleware = kw.pop("middleware") - except KeyError: - # Unfortunately Broker and StubBroker allows middleware to be - # passed in as positional arguments, whilst RabbitmqBroker and - # RedisBroker does not. - if len(args) == 1: - middleware = args[0] - args = () - else: - middleware = None - - if middleware is None: - middleware = list(m() for m in default_middleware) - else: - middleware = list(middleware) - - if integration is not None: - middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] - middleware.insert(0, SentryMiddleware()) - - kw["middleware"] = middleware - original_broker__init__(self, *args, **kw) - - Broker.__init__ = sentry_patched_broker__init__ - - -class SentryMiddleware(Middleware): # type: ignore[misc] - """ - A Dramatiq middleware that automatically captures and sends - exceptions to Sentry. - - This is automatically added to every instantiated broker via the - DramatiqIntegration. - """ - - SENTRY_HEADERS_NAME = "_sentry_headers" - - def before_enqueue( - self, broker: "Broker", message: "Message[R]", delay: int - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - message.options[self.SENTRY_HEADERS_NAME] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - - def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DramatiqIntegration) - if integration is None: - return - - message._scope_manager = sentry_sdk.isolation_scope() - scope = message._scope_manager.__enter__() - scope.clear_breadcrumbs() - scope.set_extra("dramatiq_message_id", message.message_id) - scope.add_event_processor(_make_message_event_processor(message, integration)) - - sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} - if "retries" in message.options: - # start new trace in case of retrying - sentry_headers = {} - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(sentry_headers) - span = sentry_sdk.traces.start_span( - name=message.actor_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_DRAMATIQ, - "sentry.origin": DramatiqIntegration.origin, - "sentry.span.source": SegmentSource.TASK.value, - }, - parent_span=None, - ) - message._sentry_span_ctx = span - else: - transaction = continue_trace( - sentry_headers, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - origin=DramatiqIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - sentry_sdk.start_transaction( - transaction, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - ) - transaction.__enter__() - message._sentry_span_ctx = transaction - - def after_process_message( - self, - broker: "Broker", - message: "Message[R]", - *, - result: "Optional[Any]" = None, - exception: "Optional[Exception]" = None, - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - actor = broker.get_actor(message.actor_name) - throws = message.options.get("throws") or actor.options.get("throws") - - scope_manager = message._scope_manager - span_ctx = getattr(message, "_sentry_span_ctx", None) - if span_ctx is None: - return None - - is_event_capture_required = ( - exception is not None - and not (throws and isinstance(exception, throws)) - and not isinstance(exception, Retry) - ) - if not is_event_capture_required: - # normal transaction finish - span_ctx.__exit__(None, None, None) - scope_manager.__exit__(None, None, None) - return - - event, hint = event_from_exception( - exception, # type: ignore[arg-type] - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": DramatiqIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - # transaction error - span_ctx.__exit__(type(exception), exception, None) - scope_manager.__exit__(type(exception), exception, None) - - after_skip_message = after_process_message - - -def _make_message_event_processor( - message: "Message[R]", integration: "DramatiqIntegration" -) -> "Callable[[Event, Hint], Optional[Event]]": - def inner(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - DramatiqMessageExtractor(message).extract_into_event(event) - - return event - - return inner - - -class DramatiqMessageExtractor: - def __init__(self, message: "Message[R]") -> None: - self.message_data = dict(message.asdict()) - - def content_length(self) -> int: - return len(json.dumps(self.message_data)) - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - contexts = event.setdefault("contexts", {}) - request_info = contexts.setdefault("dramatiq", {}) - request_info["type"] = "dramatiq" - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - if not request_body_within_bounds(client, self.content_length()): - data = AnnotatedValue.removed_because_over_size_limit() - else: - data = self.message_data - - request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py deleted file mode 100644 index 6bbc61000d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/excepthook.py +++ /dev/null @@ -1,76 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - - -class ExcepthookIntegration(Integration): - identifier = "excepthook" - - always_run = False - - def __init__(self, always_run: bool = False) -> None: - if not isinstance(always_run, bool): - raise ValueError( - "Invalid value for always_run: %s (must be type boolean)" - % (always_run,) - ) - self.always_run = always_run - - @staticmethod - def setup_once() -> None: - sys.excepthook = _make_excepthook(sys.excepthook) - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - if _should_send(integration.always_run): - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "excepthook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook - - -def _should_send(always_run: bool = False) -> bool: - if always_run: - return True - - if hasattr(sys, "ps1"): - # Disable the excepthook for interactive Python shells, otherwise - # every typo gets sent to Sentry. - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py deleted file mode 100644 index 4473fcc435..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/executing.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - - -class ExecutingIntegration(Integration): - identifier = "executing" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - frame = tb.tb_frame - source = Source.for_frame(frame) - sentry_frame["function"] = source.code_qualname(frame.f_code) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py deleted file mode 100644 index 7a595bcf2a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/falcon.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - - from sentry_sdk._types import Event, EventProcessor - -# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` -# and `falcon.API` to `falcon.App` - -try: - import falcon # type: ignore - from falcon import __version__ as FALCON_VERSION -except ImportError: - raise DidNotEnable("Falcon not installed") - -try: - import falcon.app_helpers # type: ignore - - falcon_helpers = falcon.app_helpers - falcon_app_class = falcon.App - FALCON3 = True -except ImportError: - import falcon.api_helpers # type: ignore - - falcon_helpers = falcon.api_helpers - falcon_app_class = falcon.API - FALCON3 = False - - -_FALCON_UNSET: "Optional[object]" = None -if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ - with capture_internal_exceptions(): - from falcon.request import ( # type: ignore[import-not-found, no-redef] - _UNSET as _FALCON_UNSET, - ) - - -class FalconRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, Any]": - return self.request.env - - def cookies(self) -> "Dict[str, Any]": - return self.request.cookies - - def form(self) -> None: - return None # No such concept in Falcon - - def files(self) -> None: - return None # No such concept in Falcon - - def raw_data(self) -> "Optional[str]": - # As request data can only be read once we won't make this available - # to Sentry. Just send back a dummy string in case there was a - # content length. - # TODO(jmagnusson): Figure out if there's a way to support this - content_length = self.content_length() - if content_length > 0: - return "[REQUEST_CONTAINING_RAW_DATA]" - else: - return None - - def json(self) -> "Optional[Dict[str, Any]]": - # fallback to cached_media = None if self.request._media is not available - cached_media = None - with capture_internal_exceptions(): - # self.request._media is the cached self.request.media - # value. It is only available if self.request.media - # has already been accessed. Therefore, reading - # self.request._media will not exhaust the raw request - # stream (self.request.bounded_stream) because it has - # already been read if self.request._media is set. - cached_media = self.request._media - - if cached_media is not _FALCON_UNSET: - return cached_media - - return None - - -class SentryFalconMiddleware: - """Captures exceptions in Falcon requests and send to Sentry""" - - def process_request( - self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return - - scope = sentry_sdk.get_isolation_scope() - scope._name = "falcon" - scope.add_event_processor(_make_request_event_processor(req, integration)) - - def process_resource( - self, req: "Any", resp: "Any", resource: "Any", params: "Any" - ) -> None: - """ - Sets the segment name and source as the route is resolved when this runs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FalconIntegration) - if integration is None or not has_span_streaming_enabled(client.options): - return - - name_for_style = { - "uri_template": req.uri_template, - "path": req.path, - } - name = name_for_style[integration.transaction_style] - source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] - sentry_sdk.set_transaction_name(name, source) - - -TRANSACTION_STYLE_VALUES = ("uri_template", "path") - - -class FalconIntegration(Integration): - identifier = "falcon" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "uri_template") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - version = parse_version(FALCON_VERSION) - _check_minimum_version(FalconIntegration, version) - - _patch_wsgi_app() - _patch_handle_exception() - _patch_prepare_middleware() - - -def _patch_wsgi_app() -> None: - original_wsgi_app = falcon_app_class.__call__ - - def sentry_patched_wsgi_app( - self: "falcon.API", env: "Any", start_response: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return original_wsgi_app(self, env, start_response) - - sentry_wrapped = SentryWsgiMiddleware( - lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), - span_origin=FalconIntegration.origin, - ) - - return sentry_wrapped(env, start_response) - - falcon_app_class.__call__ = sentry_patched_wsgi_app - - -def _patch_handle_exception() -> None: - original_handle_exception = falcon_app_class._handle_exception - - @ensure_integration_enabled(FalconIntegration, original_handle_exception) - def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": - # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception - # method signature from `(ex, req, resp, params)` to - # `(req, resp, ex, params)` - ex = response = None - with capture_internal_exceptions(): - ex = next(argument for argument in args if isinstance(argument, Exception)) - response = next( - argument for argument in args if isinstance(argument, falcon.Response) - ) - - was_handled = original_handle_exception(self, *args) - - if ex is None or response is None: - # Both ex and response should have a non-None value at this point; otherwise, - # there is an error with the SDK that will have been captured in the - # capture_internal_exceptions block above. - return was_handled - - if _exception_leads_to_http_5xx(ex, response): - event, hint = event_from_exception( - ex, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "falcon", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return was_handled - - falcon_app_class._handle_exception = sentry_patched_handle_exception - - -def _patch_prepare_middleware() -> None: - original_prepare_middleware = falcon_helpers.prepare_middleware - - def sentry_patched_prepare_middleware( - middleware: "Any" = None, - independent_middleware: "Any" = False, - asgi: bool = False, - ) -> "Any": - if asgi: - # We don't support ASGI Falcon apps, so we don't patch anything here - return original_prepare_middleware(middleware, independent_middleware, asgi) - - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is not None: - middleware = [SentryFalconMiddleware()] + (middleware or []) - - # We intentionally omit the asgi argument here, since the default is False anyways, - # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. - return original_prepare_middleware(middleware, independent_middleware) - - falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware - - -def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: - is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( - "5" - ) - is_unhandled_error = not isinstance( - ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) - ) - - # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response - # at the stage where we capture it is listed as 200, even though we would expect to see a 500 - # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to - # only perform this check on Falcon 3+, despite the risk that some handled errors might be - # reported to Sentry as unhandled on Falcon 2. - return (is_server_error or is_unhandled_error) and ( - not FALCON3 or _has_http_5xx_status(response) - ) - - -def _has_http_5xx_status(response: "falcon.Response") -> bool: - return response.status.startswith("5") - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "falcon.Request" -) -> None: - name_for_style = { - "uri_template": request.uri_template, - "path": request.path, - } - event["transaction"] = name_for_style[transaction_style] - event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} - - -def _make_request_event_processor( - req: "falcon.Request", integration: "FalconIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, req) - - with capture_internal_exceptions(): - FalconRequestExtractor(req).extract_into_event(event) - - return event - - return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py deleted file mode 100644 index c7b97c88b1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/fastapi.py +++ /dev/null @@ -1,201 +0,0 @@ -import sys -from copy import deepcopy -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import transaction_from_function - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Dict - - from sentry_sdk._types import Event - -try: - from sentry_sdk.integrations.starlette import ( - StarletteIntegration, - StarletteRequestExtractor, - _get_cached_request_body_attribute, - ) -except DidNotEnable: - raise DidNotEnable("Starlette is not installed") - -try: - import fastapi # type: ignore -except ImportError: - raise DidNotEnable("FastAPI is not installed") - - -_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -class FastApiIntegration(StarletteIntegration): - identifier = "fastapi" - - @staticmethod - def setup_once() -> None: - patch_get_request_handler() - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or "" - - elif transaction_style == "url": - route = request.scope.get("route") - - if route: - # FastAPI >= 0.137 stores the prefix-resolved path on an - # effective_route_context in scope["fastapi"], while - # scope["route"].path holds the unprefixed original. - # Prefer the effective context path when available. - effective_route_context = request.scope.get("fastapi", {}).get( - "effective_route_context" - ) - context_path = getattr(effective_route_context, "path", None) - - if context_path: - name = context_path - else: - path = getattr(route, "path", None) - if path is not None: - name = path - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name(name, source=source) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FastApiIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, Dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = FastApiIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_get_request_handler() -> None: - old_get_request_handler = fastapi.routing.get_request_handler - - def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": - dependant = kwargs.get("dependant") - if ( - dependant - and dependant.call is not None - and not iscoroutinefunction(dependant.call) - # FastAPI >= 0.137 calls get_request_handler() on every request - # (router-tree traversal) rather than once at registration. Guard - # against accumulating _sentry_call wrappers on the shared - # dependant object, which would cause a RecursionError after ~987 - # requests as the call chain grows past Python's recursion limit. - and not getattr(dependant.call, "_sentry_is_patched", False) - ): - old_call = dependant.call - - @wraps(old_call) - def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": - current_scope = sentry_sdk.get_current_scope() - - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_call(*args, **kwargs) - - _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] - dependant.call = _sentry_call - - old_app = old_get_request_handler(*args, **kwargs) - - async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_app, *args, **kwargs) - - return _sentry_app - - fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py deleted file mode 100644 index 1902091fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/flask.py +++ /dev/null @@ -1,281 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Union - - from werkzeug.datastructures import FileStorage, ImmutableMultiDict - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - - -try: - import flask_login # type: ignore -except ImportError: - flask_login = None - -try: - from flask import Flask, Request # type: ignore - from flask import request as flask_request - from flask.signals import ( - before_render_template, - got_request_exception, - request_started, - ) - from markupsafe import Markup -except ImportError: - raise DidNotEnable("Flask is not installed") - -try: - import blinker # noqa -except ImportError: - raise DidNotEnable("blinker is not installed") - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class FlaskIntegration(Integration): - identifier = "flask" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - try: - from quart import Quart # type: ignore - - if Flask == Quart: - # This is Quart masquerading as Flask, don't enable the Flask - # integration. See https://github.com/getsentry/sentry-python/issues/2709 - raise DidNotEnable( - "This is not a Flask app but rather Quart pretending to be Flask" - ) - except ImportError: - pass - - version = package_version("flask") - _check_minimum_version(FlaskIntegration, version) - - before_render_template.connect(_add_sentry_trace) - request_started.connect(_request_started) - got_request_exception.connect(_capture_exception) - - old_app = Flask.__call__ - - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return old_app(self, environ, start_response) - - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=FlaskIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - Flask.__call__ = sentry_patched_wsgi_app - - -def _add_sentry_trace( - sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" -) -> None: - if "sentry_trace" in context: - return - - scope = sentry_sdk.get_current_scope() - trace_meta = Markup(scope.trace_propagation_meta()) - context["sentry_trace"] = trace_meta # for backwards compatibility - context["sentry_trace_meta"] = trace_meta - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - scope.set_transaction_name( - name_for_style[transaction_style], - source=SOURCE_FOR_STYLE[transaction_style], - ) - except Exception: - pass - - -def _request_started(app: "Flask", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return - - request = flask_request._get_current_object() - - # Set the transaction name and source here, - # but rely on WSGI middleware to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - scope.set_user(user_properties) - - evt_processor = _make_request_event_processor(app, request, integration) - scope.add_event_processor(evt_processor) - - -class FlaskRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[Any, Any]": - return { - k: v[0] if isinstance(v, list) and len(v) == 1 else v - for k, v in self.request.cookies.items() - } - - def raw_data(self) -> bytes: - return self.request.get_data() - - def form(self) -> "ImmutableMultiDict[str, Any]": - return self.request.form - - def files(self) -> "ImmutableMultiDict[str, Any]": - return self.request.files - - def is_json(self) -> bool: - return self.request.is_json - - def json(self) -> "Any": - return self.request.get_json(silent=True) - - def size_of_file(self, file: "FileStorage") -> int: - return file.content_length - - -def _make_request_event_processor( - app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - FlaskRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _add_user_to_event(event) - - return event - - return inner - - -@ensure_integration_enabled(FlaskIntegration) -def _capture_exception( - sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "flask", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_flask_user_properties() -> "Dict[str, str]": - if flask_login is None: - return {} - - user = flask_login.current_user - if user is None: - return {} - - properties = {} - - try: - user_id = user.get_id() - if user_id is not None: - properties["id"] = user_id - except AttributeError: - # might happen if: - # - flask_login could not be imported - # - flask_login is not configured - # - no user is logged in - pass - - # The following attribute accesses are ineffective for the general - # Flask-Login case, because the User interface of Flask-Login does not - # care about anything but the ID. However, Flask-User (based on - # Flask-Login) documents a few optional extra attributes. - # - # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names - try: - if user.email is not None: - properties["email"] = user.email - except Exception: - pass - - try: - if user.username is not None: - properties["username"] = user.username - except Exception: - pass - - return properties - - -def _add_user_to_event(event: "Event") -> None: - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - user_info = event.setdefault("user", {}) - for key, value in user_properties.items(): - user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py deleted file mode 100644 index 91a62b3a81..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gcp.py +++ /dev/null @@ -1,286 +0,0 @@ -import functools -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -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 -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -# Constants -TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -def _wrap_func(func: "F") -> "F": - @functools.wraps(func) - def sentry_func( - functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(GcpIntegration) - if integration is None: - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = environ.get("FUNCTION_TIMEOUT_SEC") - if not configured_time: - logger.debug( - "The configured timeout could not be fetched from Cloud Functions configuration." - ) - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = int(configured_time) - - initial_time = datetime.now(timezone.utc) - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - gcp_event, configured_time, initial_time - ) - ) - scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) - timeout_thread = None - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = configured_time - TIMEOUT_WARNING_BUFFER - - timeout_thread = TimeoutThread( - waiting_time, - configured_time, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = {} - header_attributes: "dict[str, Any]" = {} - if hasattr(gcp_event, "headers"): - headers = gcp_event.headers - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - additional_attributes = {} - 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" - ) - - sampling_context = { - "gcp_env": { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - }, - "gcp_event": gcp_event, - } - - function_name = environ.get("FUNCTION_NAME", "") - - if environ.get("GCP_PROJECT"): - additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") - - if environ.get("FUNCTION_IDENTITY"): - additional_attributes["faas.identity"] = environ.get( - "FUNCTION_IDENTITY" - ) - - if environ.get("ENTRY_POINT"): - additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_GCP, - "sentry.origin": GcpIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.provider": CLOUD_PROVIDER.GCP, - "faas.name": function_name, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_GCP, - name=environ.get("FUNCTION_NAME", ""), - source=TransactionSource.COMPONENT, - origin=GcpIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return func(functionhandler, gcp_event, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "gcp", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - # Flush out the event queue - client.flush() - - return sentry_func # type: ignore - - -class GcpIntegration(Integration): - identifier = "gcp" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - import __main__ as gcp_functions - - if not hasattr(gcp_functions, "worker_v1"): - logger.warning( - "GcpIntegration currently supports only Python 3.7 runtime environment." - ) - return - - worker1 = gcp_functions.worker_v1 - - worker1.FunctionHandler.invoke_user_function = _wrap_func( - worker1.FunctionHandler.invoke_user_function - ) - - -def _make_request_event_processor( - gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - final_time = datetime.now(timezone.utc) - time_diff = final_time - initial_time - - execution_duration_in_millis = time_diff / timedelta(milliseconds=1) - - extra = event.setdefault("extra", {}) - extra["google cloud functions"] = { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - "execution_duration_in_millis": execution_duration_in_millis, - "configured_timeout_in_seconds": configured_timeout, - } - - extra["google cloud logs"] = { - "url": _get_google_cloud_logs_url(final_time), - } - - request = event.get("request", {}) - - request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) - - if hasattr(gcp_event, "method"): - request["method"] = gcp_event.method - - if hasattr(gcp_event, "query_string"): - request["query_string"] = gcp_event.query_string.decode( - "utf-8", errors="replace" - ) - - if hasattr(gcp_event, "headers"): - request["headers"] = _filter_headers(gcp_event.headers) - - if should_send_default_pii(): - if hasattr(gcp_event, "data"): - request["data"] = gcp_event.data - else: - if hasattr(gcp_event, "data"): - # Unfortunately couldn't find a way to get structured body from GCP - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - event["request"] = deepcopy(request) - - return event - - return event_processor - - -def _get_google_cloud_logs_url(final_time: "datetime") -> str: - """ - Generates a Google Cloud Logs console URL based on the environment variables - Arguments: - final_time {datetime} -- Final time - Returns: - str -- Google Cloud Logs Console URL to logs. - """ - hour_ago = final_time - timedelta(hours=1) - formatstring = "%Y-%m-%dT%H:%M:%SZ" - - url = ( - "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" - "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" - "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" - "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" - "&interval=PT1H&scrollTimestamp={timestamp_end}" - ).format( - project=environ.get("GCP_PROJECT"), - function_name=environ.get("FUNCTION_NAME"), - region=environ.get("FUNCTION_REGION"), - timestamp_end=final_time.strftime(formatstring), - timestamp_start=hour_ago.strftime(formatstring), - ) - - return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py deleted file mode 100644 index 4be0f479bc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gnu_backtrace.py +++ /dev/null @@ -1,96 +0,0 @@ -import re -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - -# function is everything between index at @ -# and then we match on the @ plus the hex val -FUNCTION_RE = r"[^@]+?" -HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" - -_FRAME_RE_PATTERN = r""" -^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ -""".format( - FUNCTION_RE=FUNCTION_RE, - HEX_ADDRESS=HEX_ADDRESS, -) - -FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) - - -class GnuBacktraceIntegration(Integration): - identifier = "gnu_backtrace" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - return _process_gnu_backtrace(event, hint) - - -def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception in values: - frames = exception.get("stacktrace", {}).get("frames", []) - if not frames: - continue - - msg = exception.get("value", None) - if not msg: - continue - - additional_frames = [] - new_msg = [] - - for line in msg.splitlines(): - match = FRAME_RE.match(line) - if match: - additional_frames.append( - ( - int(match.group("index")), - { - "package": match.group("package") or None, - "function": match.group("function") or None, - "platform": "native", - }, - ) - ) - else: - # Put garbage lines back into message, not sure what to do with them. - new_msg.append(line) - - if additional_frames: - additional_frames.sort(key=lambda x: -x[0]) - for _, frame in additional_frames: - frames.append(frame) - - new_msg.append("") - exception["value"] = "\n".join(new_msg) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py deleted file mode 100644 index 45652c3f71..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/__init__.py +++ /dev/null @@ -1,457 +0,0 @@ -from functools import wraps -from typing import ( - Any, - AsyncIterator, - Callable, - Iterator, - List, -) - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import SPANSTATUS -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.genai.models import AsyncModels, Models -except ImportError: - raise DidNotEnable("google-genai not installed") - - -from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN -from .streaming import ( - accumulate_streaming_response, - set_span_data_for_streaming_response, -) -from .utils import ( - _capture_exception, - prepare_embed_content_args, - prepare_generate_content_args, - set_span_data_for_embed_request, - set_span_data_for_embed_response, - set_span_data_for_request, - set_span_data_for_response, -) - - -class GoogleGenAIIntegration(Integration): - identifier = IDENTIFIER - origin = ORIGIN - - def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Patch sync methods - Models.generate_content = _wrap_generate_content(Models.generate_content) - Models.generate_content_stream = _wrap_generate_content_stream( - Models.generate_content_stream - ) - Models.embed_content = _wrap_embed_content(Models.embed_content) - - # Patch async methods - AsyncModels.generate_content = _wrap_async_generate_content( - AsyncModels.generate_content - ) - AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( - AsyncModels.generate_content_stream - ) - AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) - - -def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = f(self, *args, **kwargs) - - # Create wrapper iterator to accumulate responses - def new_iterator() -> "Iterator[Any]": - chunks: "List[Any]" = [] - try: - for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_generate_content_stream - - -def _wrap_async_generate_content_stream( - f: "Callable[..., Any]", -) -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = await f(self, *args, **kwargs) - - # Create wrapper async iterator to accumulate responses - async def new_async_iterator() -> "AsyncIterator[Any]": - chunks: "List[Any]" = [] - try: - async for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_async_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_async_generate_content_stream - - -def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_generate_content - - -def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_async_generate_content - - -def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_embed_content - - -def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_embed_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py deleted file mode 100644 index 5b53ebf0e2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/consts.py +++ /dev/null @@ -1,16 +0,0 @@ -GEN_AI_SYSTEM = "gcp.gemini" - -# Mapping of tool attributes to their descriptions -# These are all tools that are available in the Google GenAI API -TOOL_ATTRIBUTES_MAP = { - "google_search_retrieval": "Google Search retrieval tool", - "google_search": "Google Search tool", - "retrieval": "Retrieval tool", - "enterprise_web_search": "Enterprise web search tool", - "google_maps": "Google Maps tool", - "code_execution": "Code execution tool", - "computer_use": "Computer use tool", -} - -IDENTIFIER = "google_genai" -ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py deleted file mode 100644 index 8414ea4f21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/streaming.py +++ /dev/null @@ -1,172 +0,0 @@ -from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union - -from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import ( - safe_serialize, -) - -from .utils import ( - UsageData, - extract_contents_text, - extract_finish_reasons, - extract_tool_calls, - extract_usage_data, -) - -if TYPE_CHECKING: - from google.genai.types import GenerateContentResponse - - from sentry_sdk.tracing import Span - - -class AccumulatedResponse(TypedDict): - id: "Optional[str]" - model: "Optional[str]" - text: str - finish_reasons: "List[str]" - tool_calls: "List[dict[str, Any]]" - usage_metadata: "Optional[UsageData]" - - -def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": - return UsageData( - input_tokens=max(self["input_tokens"], other["input_tokens"]), - output_tokens=max(self["output_tokens"], other["output_tokens"]), - input_tokens_cached=max( - self["input_tokens_cached"], other["input_tokens_cached"] - ), - output_tokens_reasoning=max( - self["output_tokens_reasoning"], other["output_tokens_reasoning"] - ), - total_tokens=max(self["total_tokens"], other["total_tokens"]), - ) - - -def accumulate_streaming_response( - chunks: "List[GenerateContentResponse]", -) -> "AccumulatedResponse": - """Accumulate streaming chunks into a single response-like object.""" - accumulated_text = [] - finish_reasons = [] - tool_calls = [] - usage_data = None - response_id = None - model = None - - for chunk in chunks: - # Extract text and tool calls - if getattr(chunk, "candidates", None): - for candidate in getattr(chunk, "candidates", []): - if hasattr(candidate, "content") and getattr( - candidate.content, "parts", [] - ): - extracted_text = extract_contents_text(candidate.content) - if extracted_text: - accumulated_text.append(extracted_text) - - extracted_finish_reasons = extract_finish_reasons(chunk) - if extracted_finish_reasons: - finish_reasons.extend(extracted_finish_reasons) - - extracted_tool_calls = extract_tool_calls(chunk) - if extracted_tool_calls: - tool_calls.extend(extracted_tool_calls) - - # Use last possible chunk, in case of interruption, and - # gracefully handle missing intermediate tokens by taking maximum - # with previous token reporting. - chunk_usage_data = extract_usage_data(chunk) - usage_data = ( - chunk_usage_data - if usage_data is None - else element_wise_usage_max(usage_data, chunk_usage_data) - ) - - accumulated_response = AccumulatedResponse( - text="".join(accumulated_text), - finish_reasons=finish_reasons, - tool_calls=tool_calls, - usage_metadata=usage_data, - id=response_id, - model=model, - ) - - return accumulated_response - - -def set_span_data_for_streaming_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - accumulated_response: "AccumulatedResponse", -) -> None: - """Set span data for accumulated streaming response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if ( - should_send_default_pii() - and integration.include_prompts - and accumulated_response.get("text") - ): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TEXT, - safe_serialize([accumulated_response["text"]]), - ) - - if accumulated_response.get("finish_reasons"): - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - accumulated_response["finish_reasons"], - ) - - if accumulated_response.get("tool_calls"): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(accumulated_response["tool_calls"]), - ) - - response_id = accumulated_response.get("id") - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - - response_model = accumulated_response.get("model") - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - if accumulated_response["usage_metadata"] is None: - return - - if accumulated_response["usage_metadata"]["input_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - accumulated_response["usage_metadata"]["input_tokens"], - ) - - if accumulated_response["usage_metadata"]["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - accumulated_response["usage_metadata"]["input_tokens_cached"], - ) - - if accumulated_response["usage_metadata"]["output_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - accumulated_response["usage_metadata"]["output_tokens"], - ) - - if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - accumulated_response["usage_metadata"]["output_tokens_reasoning"], - ) - - if accumulated_response["usage_metadata"]["total_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - accumulated_response["usage_metadata"]["total_tokens"], - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py deleted file mode 100644 index 464a812680..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/google_genai/utils.py +++ /dev/null @@ -1,1118 +0,0 @@ -import copy -import inspect -import json -from functools import wraps -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - TypedDict, - Union, -) - -from google.genai.types import Content, GenerateContentConfig, Part, PartDict - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.utils import ( - get_modality_from_mime_type, - normalize_message_roles, - set_data_normalized, - transform_google_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - safe_serialize, -) - -from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP - -if TYPE_CHECKING: - from google.genai.types import ( - ContentListUnion, - ContentUnion, - ContentUnionDict, - EmbedContentResponse, - GenerateContentResponse, - Model, - Tool, - ) - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -_is_PIL_available = False -try: - from PIL import Image as PILImage # type: ignore[import-not-found] - - _is_PIL_available = True -except ImportError: - pass - -# Keys to use when checking to see if a dict provided by the user -# is Part-like (as opposed to a Content or multi-turn conversation entry). -_PART_DICT_KEYS = PartDict.__optional_keys__ - - -class UsageData(TypedDict): - """Structure for token usage data.""" - - input_tokens: int - input_tokens_cached: int - output_tokens: int - output_tokens_reasoning: int - total_tokens: int - - -def extract_usage_data( - response: "Union[GenerateContentResponse, dict[str, Any]]", -) -> "UsageData": - """Extract usage data from response into a structured format. - - Args: - response: The GenerateContentResponse object or dictionary containing usage metadata - - Returns: - UsageData: Dictionary with input_tokens, input_tokens_cached, - output_tokens, and output_tokens_reasoning fields - """ - usage_data = UsageData( - input_tokens=0, - input_tokens_cached=0, - output_tokens=0, - output_tokens_reasoning=0, - total_tokens=0, - ) - - # Handle dictionary response (from streaming) - if isinstance(response, dict): - usage = response.get("usage_metadata", {}) - if not usage: - return usage_data - - prompt_tokens = usage.get("prompt_token_count", 0) or 0 - tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - cached_tokens = usage.get("cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - candidates_tokens = usage.get("candidates_token_count", 0) or 0 - # python-genai reports output and reasoning tokens separately - # reasoning should be sub-category of output tokens - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = usage.get("total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - if not hasattr(response, "usage_metadata"): - return usage_data - - usage = response.usage_metadata - - # Input tokens include both prompt and tool use prompt tokens - prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 - tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - # Cached input tokens - cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - # Reasoning tokens - reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - # output_tokens = candidates_tokens + reasoning_tokens - # google-genai reports output and reasoning tokens separately - candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = getattr(usage, "total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - -def _capture_exception(exc: "Any") -> None: - """Capture exception with Google GenAI mechanism.""" - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "google_genai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def get_model_name(model: "Union[str, Model]") -> str: - """Extract model name from model parameter.""" - if isinstance(model, str): - return model - # Handle case where model might be an object with a name attribute - if hasattr(model, "name"): - return str(model.name) - return str(model) - - -def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": - """Extract messages from contents parameter which can have various formats. - - Returns a list of message dictionaries in the format: - - System: {"role": "system", "content": "string"} - - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} - """ - if contents is None: - return [] - - messages = [] - - # Handle string case - if isinstance(contents, str): - return [{"role": "user", "content": contents}] - - # Handle list case - if isinstance(contents, list): - if contents and all(_is_part_like(item) for item in contents): - # All items are parts — merge into a single multi-part user message - content_parts = [] - for item in contents: - part = _extract_part_from_item(item) - if part is not None: - content_parts.append(part) - - return [{"role": "user", "content": content_parts}] - else: - # Multi-turn conversation or mixed content types - for item in contents: - item_messages = extract_contents_messages(item) - messages.extend(item_messages) - return messages - - # Handle dictionary case (ContentDict) - if isinstance(contents, dict): - role = contents.get("role", "user") - parts = contents.get("parts") - - if parts: - content_parts = [] - tool_messages = [] - - for part in parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - # Tool message - add separately - tool_messages.append(part_result) - else: - # Regular content part - content_parts.append(part_result) - - # Add main message if we have content parts - if content_parts: - # Normalize role: "model" -> "assistant" - normalized_role = "assistant" if role == "model" else role or "user" - messages.append({"role": normalized_role, "content": content_parts}) - - # Add tool messages - messages.extend(tool_messages) - elif "text" in contents: - messages.append( - { - "role": role, - "content": [{"text": contents["text"], "type": "text"}], - } - ) - elif "inline_data" in contents: - # The "data" will always be bytes (or bytes within a string), - # so if this is present, it's safe to automatically substitute with the placeholder - messages.append( - { - "inline_data": { - "mime_type": contents["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - ) - - return messages - - # Handle Content object - if hasattr(contents, "parts") and contents.parts: - role = getattr(contents, "role", None) or "user" - content_parts = [] - tool_messages = [] - - for part in contents.parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - tool_messages.append(part_result) - else: - content_parts.append(part_result) - - if content_parts: - normalized_role = "assistant" if role == "model" else role - messages.append({"role": normalized_role, "content": content_parts}) - - messages.extend(tool_messages) - return messages - - # Handle Part object directly - part_result = _extract_part_content(contents) - if part_result: - if isinstance(part_result, dict) and part_result.get("role") == "tool": - return [part_result] - else: - return [{"role": "user", "content": [part_result]}] - - # Handle PIL.Image.Image - if _is_PIL_available and isinstance(contents, PILImage.Image): - blob_part = _extract_pil_image(contents) - if blob_part: - return [{"role": "user", "content": [blob_part]}] - - # Handle File object - if hasattr(contents, "uri") and hasattr(contents, "mime_type"): - # File object - file_uri = getattr(contents, "uri", None) - mime_type = getattr(contents, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing - if file_uri is not None: - # Default to empty string if mime_type is None - if mime_type is None: - mime_type = "" - - blob_part = { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - return [{"role": "user", "content": [blob_part]}] - - # Handle direct text attribute - if hasattr(contents, "text") and contents.text: - return [ - {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} - ] - - return [] - - -def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": - """Extract content from a Part object or dict. - - Returns: - - dict for content part (text/blob) or tool message - - None if part should be skipped - """ - if part is None: - return None - - # Handle dict Part - if isinstance(part, dict): - # Check for function_response first (tool message) - if "function_response" in part: - return _extract_tool_message_from_part(part) - - if part.get("text"): - return {"text": part["text"], "type": "text"} - - # Try using Google-specific transform for dict formats (inline_data, file_data) - result = transform_google_content_part(part) - if result is not None: - # For inline_data with bytes data, substitute the content - if "inline_data" in part: - # inline_data.data will always be bytes, or a string containing base64-encoded bytes, - # so can automatically substitute without further checks - result["content"] = BLOB_DATA_SUBSTITUTE - return result - - return None - - # Handle Part object - # Check for function_response (tool message) - if hasattr(part, "function_response") and part.function_response: - return _extract_tool_message_from_part(part) - - # Handle text - if hasattr(part, "text") and part.text: - return {"text": part.text, "type": "text"} - - # Handle file_data - if hasattr(part, "file_data") and part.file_data: - file_data = part.file_data - file_uri = getattr(file_data, "file_uri", None) - mime_type = getattr(file_data, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) - if file_uri is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - # Handle inline_data - if hasattr(part, "inline_data") and part.inline_data: - inline_data = part.inline_data - data = getattr(inline_data, "data", None) - mime_type = getattr(inline_data, "mime_type", None) - # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) - if data is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - return None - - -def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": - """Extract tool message from a Part with function_response. - - Returns: - {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} - or None if not a valid tool message - """ - function_response = None - - if isinstance(part, dict): - function_response = part.get("function_response") - elif hasattr(part, "function_response"): - function_response = part.function_response - - if not function_response: - return None - - # Extract fields from function_response - tool_call_id = None - tool_name = None - output = None - - if isinstance(function_response, dict): - tool_call_id = function_response.get("id") - tool_name = function_response.get("name") - response_dict = function_response.get("response", {}) - # Prefer "output" key if present, otherwise use entire response - output = response_dict.get("output", response_dict) - else: - # FunctionResponse object - tool_call_id = getattr(function_response, "id", None) - tool_name = getattr(function_response, "name", None) - response_obj = getattr(function_response, "response", None) - if response_obj is None: - response_obj = {} - if isinstance(response_obj, dict): - output = response_obj.get("output", response_obj) - else: - output = response_obj - - if not tool_name: - return None - - return { - "role": "tool", - "content": { - "toolCallId": str(tool_call_id) if tool_call_id else None, - "toolName": str(tool_name), - "output": safe_serialize(output) if output is not None else None, - }, - } - - -def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": - """Extract blob part from PIL.Image.Image.""" - if not _is_PIL_available or not isinstance(image, PILImage.Image): - return None - - # Get format, default to JPEG - format_str = image.format or "JPEG" - suffix = format_str.lower() - mime_type = f"image/{suffix}" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _is_part_like(item: "Any") -> bool: - """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" - if isinstance(item, (str, Part)): - return True - if isinstance(item, (list, Content)): - return False - if isinstance(item, dict): - if "role" in item or "parts" in item: - return False - # Part objects that came in as plain dicts - return bool(_PART_DICT_KEYS & item.keys()) - # File objects - if hasattr(item, "uri"): - return True - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return True - return False - - -def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": - """Convert a single part-like item to a content part dict.""" - if isinstance(item, str): - return {"text": item, "type": "text"} - - # Handle bare inline_data dicts directly to preserve the raw format - if isinstance(item, dict) and "inline_data" in item: - return { - "inline_data": { - "mime_type": item["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - - # For other dicts and Part objects, use existing _extract_part_content - result = _extract_part_content(item) - if result is not None: - return result - - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return _extract_pil_image(item) - - # File objects - if hasattr(item, "uri") and hasattr(item, "mime_type"): - file_uri = getattr(item, "uri", None) - mime_type = getattr(item, "mime_type", None) or "" - if file_uri is not None: - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - return None - - -def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": - """Extract text from contents parameter which can have various formats. - - This is a compatibility function that extracts text from messages. - For new code, use extract_contents_messages instead. - """ - messages = extract_contents_messages(contents) - if not messages: - return None - - texts = [] - for message in messages: - content = message.get("content") - if isinstance(content, str): - texts.append(content) - elif isinstance(content, list): - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - texts.append(part.get("text", "")) - - return " ".join(texts) if texts else None - - -def _format_tools_for_span( - tools: "Iterable[Tool | Callable[..., Any]]", -) -> "Optional[List[dict[str, Any]]]": - """Format tools parameter for span data.""" - formatted_tools = [] - for tool in tools: - if callable(tool): - # Handle callable functions passed directly - formatted_tools.append( - { - "name": getattr(tool, "__name__", "unknown"), - "description": getattr(tool, "__doc__", None), - } - ) - elif ( - hasattr(tool, "function_declarations") - and tool.function_declarations is not None - ): - # Tool object with function declarations - for func_decl in tool.function_declarations: - formatted_tools.append( - { - "name": getattr(func_decl, "name", None), - "description": getattr(func_decl, "description", None), - } - ) - else: - # Check for predefined tool attributes - each of these tools - # is an attribute of the tool object, by default set to None - for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): - if getattr(tool, attr_name, None): - formatted_tools.append( - { - "name": attr_name, - "description": description, - } - ) - break - - return formatted_tools if formatted_tools else None - - -def extract_tool_calls( - response: "GenerateContentResponse", -) -> "Optional[List[dict[str, Any]]]": - """Extract tool/function calls from response candidates and automatic function calling history.""" - - tool_calls = [] - - # Extract from candidates, sometimes tool calls are nested under the content.parts object - if getattr(response, "candidates", []): - for candidate in response.candidates: - if not hasattr(candidate, "content") or not getattr( - candidate.content, "parts", [] - ): - continue - - for part in candidate.content.parts: - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if getattr(function_call, "args", None): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - # Extract from automatic_function_calling_history - # This is the history of tool calls made by the model - if getattr(response, "automatic_function_calling_history", None): - for content in response.automatic_function_calling_history: - if not getattr(content, "parts", None): - continue - - for part in getattr(content, "parts", []): - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if hasattr(function_call, "args"): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - return tool_calls if tool_calls else None - - -def _capture_tool_input( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" -) -> "dict[str, Any]": - """Capture tool input from args and kwargs.""" - tool_input = kwargs.copy() if kwargs else {} - - # If we have positional args, try to map them to the function signature - if args: - try: - sig = inspect.signature(tool) - param_names = list(sig.parameters.keys()) - for i, arg in enumerate(args): - if i < len(param_names): - tool_input[param_names[i]] = arg - except Exception: - # Fallback if we can't get the signature - tool_input["args"] = args - - return tool_input - - -def _create_tool_span( - tool_name: str, tool_doc: "Optional[str]" -) -> "Union[Span, StreamedSpan]": - """Create a span for tool execution.""" - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - if tool_doc: - span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=ORIGIN, - ) - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - if tool_doc: - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - -def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": - """Wrap a tool to emit execute_tool spans when called.""" - if not callable(tool): - # Not a callable function, return as-is (predefined tools) - return tool - - tool_name = getattr(tool, "__name__", "unknown") - tool_doc = tool.__doc__ - - if inspect.iscoroutinefunction(tool): - # Async function - @wraps(tool) - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = await tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return async_wrapped - else: - # Sync function - @wraps(tool) - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return sync_wrapped - - -def wrapped_config_with_tools( - config: "GenerateContentConfig", -) -> "GenerateContentConfig": - """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as - callable functions as a part of the config object.""" - - if not config or not getattr(config, "tools", None): - return config - - result = copy.copy(config) - result.tools = [wrapped_tool(tool) for tool in config.tools] - - return result - - -def _extract_response_text( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract text from response candidates.""" - - if not response or not getattr(response, "candidates", []): - return None - - texts = [] - for candidate in response.candidates: - if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): - continue - - if candidate.content is None or candidate.content.parts is None: - continue - - for part in candidate.content.parts: - if getattr(part, "text", None): - texts.append(part.text) - - return texts if texts else None - - -def extract_finish_reasons( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract finish reasons from response candidates.""" - if not response or not getattr(response, "candidates", []): - return None - - finish_reasons = [] - for candidate in response.candidates: - if getattr(candidate, "finish_reason", None): - # Convert enum value to string if necessary - reason = str(candidate.finish_reason) - # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") - if "." in reason: - reason = reason.split(".")[-1] - finish_reasons.append(reason) - - return finish_reasons if finish_reasons else None - - -def _transform_system_instruction_one_level( - system_instructions: "Union[ContentUnionDict, ContentUnion]", - can_be_content: bool, -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, str): - return [{"type": "text", "content": system_instructions}] - - if isinstance(system_instructions, Part) and system_instructions.text: - return [{"type": "text", "content": system_instructions.text}] - - if can_be_content and isinstance(system_instructions, Content): - if isinstance(system_instructions.parts, list): - for part in system_instructions.parts: - if isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - return text_parts - - if isinstance(system_instructions, dict) and system_instructions.get("text"): - return [{"type": "text", "content": system_instructions["text"]}] - - elif can_be_content and isinstance(system_instructions, dict): - parts = system_instructions.get("parts", []) - for part in parts: - if isinstance(part, Part) and isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - elif isinstance(part, dict) and isinstance(part.get("text"), str): - text_parts.append({"type": "text", "content": part["text"]}) - return text_parts - - return text_parts - - -def _transform_system_instructions( - system_instructions: "Union[ContentUnionDict, ContentUnion]", -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, list): - text_parts = list( - chain.from_iterable( - _transform_system_instruction_one_level( - instructions, can_be_content=False - ) - for instructions in system_instructions - ) - ) - - return text_parts - - return _transform_system_instruction_one_level( - system_instructions, can_be_content=True - ) - - -def set_span_data_for_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - model: str, - contents: "ContentListUnion", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for the request.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if kwargs.get("stream", False): - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - config: "Optional[GenerateContentConfig]" = kwargs.get("config") - - # Set input messages/prompts if PII is allowed - if should_send_default_pii() and integration.include_prompts: - messages = [] - - # Add system instruction if present - system_instructions = None - if config and hasattr(config, "system_instruction"): - system_instructions = config.system_instruction - elif isinstance(config, dict) and "system_instruction" in config: - system_instructions = config.get("system_instruction") - - if system_instructions is not None: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - # Extract messages from contents - contents_messages = extract_contents_messages(contents) - messages.extend(contents_messages) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Extract parameters directly from config (not nested under generation_config) - for param, span_key in [ - ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), - ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), - ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), - ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), - ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), - ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), - ("seed", SPANDATA.GEN_AI_REQUEST_SEED), - ]: - if hasattr(config, param): - value = getattr(config, param) - if value is not None: - set_on_span(span_key, value) - - # Set tools if available - if config is not None and hasattr(config, "tools"): - tools = config.tools - if tools: - formatted_tools = _format_tools_for_span(tools) - if formatted_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - formatted_tools, - unpack=False, - ) - - -def set_span_data_for_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "GenerateContentResponse", -) -> None: - """Set span data for the response.""" - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if should_send_default_pii() and integration.include_prompts: - response_texts = _extract_response_text(response) - if response_texts: - # Format as JSON string array as per documentation - set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) - - tool_calls = extract_tool_calls(response) - if tool_calls: - # Tool calls should be JSON serialized - set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) - - finish_reasons = extract_finish_reasons(response) - if finish_reasons: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons - ) - - if getattr(response, "response_id", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) - - if getattr(response, "model_version", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) - - usage_data = extract_usage_data(response) - - if usage_data["input_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) - - if usage_data["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage_data["input_tokens_cached"], - ) - - if usage_data["output_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) - - if usage_data["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage_data["output_tokens_reasoning"], - ) - - if usage_data["total_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) - - -def prepare_generate_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[Any, Any, str]": - """Extract and prepare common arguments for generate_content methods.""" - model = args[0] if args else kwargs.get("model", "unknown") - contents = args[1] if len(args) > 1 else kwargs.get("contents") - model_name = get_model_name(model) - - config = kwargs.get("config") - wrapped_config = wrapped_config_with_tools(config) - if wrapped_config is not config: - kwargs["config"] = wrapped_config - - return model, contents, model_name - - -def prepare_embed_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[str, Any]": - """Extract and prepare common arguments for embed_content methods. - - Returns: - tuple: (model_name, contents) - """ - model = kwargs.get("model", "unknown") - contents = kwargs.get("contents") - model_name = get_model_name(model) - - return model_name, contents - - -def set_span_data_for_embed_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - contents: "Any", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for embedding request.""" - # Include input contents if PII is allowed - if should_send_default_pii() and integration.include_prompts: - if contents: - # For embeddings, contents is typically a list of strings/texts - input_texts = [] - - # Handle various content formats - if isinstance(contents, str): - input_texts = [contents] - elif isinstance(contents, list): - for item in contents: - text = extract_contents_text(item) - if text: - input_texts.append(text) - else: - text = extract_contents_text(contents) - if text: - input_texts = [text] - - if input_texts: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - input_texts, - unpack=False, - ) - - -def set_span_data_for_embed_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "EmbedContentResponse", -) -> None: - """Set span data for embedding response.""" - if not response: - return - - # Extract token counts from embeddings statistics (Vertex AI only) - # Each embedding has its own statistics with token_count - if hasattr(response, "embeddings") and response.embeddings: - total_tokens = 0 - - for embedding in response.embeddings: - if hasattr(embedding, "statistics") and embedding.statistics: - token_count = getattr(embedding.statistics, "token_count", None) - if token_count is not None: - total_tokens += int(token_count) - - # Set token count if we found any - if total_tokens > 0: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) - else: - span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py deleted file mode 100644 index dcb60e9561..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/gql.py +++ /dev/null @@ -1,173 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -try: - import gql # type: ignore[import-not-found] - from gql.transport import ( # type: ignore[import-not-found] - AsyncTransport, - Transport, - ) - from gql.transport.exceptions import ( # type: ignore[import-not-found] - TransportQueryError, - ) - from graphql import ( - DocumentNode, - VariableDefinitionNode, - get_operation_ast, - print_ast, - ) - - try: - # gql 4.0+ - from gql import GraphQLRequest - except ImportError: - GraphQLRequest = None - -except ImportError: - raise DidNotEnable("gql is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Tuple, Union - - from sentry_sdk._types import Event, EventProcessor - - EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] - - -class GQLIntegration(Integration): - identifier = "gql" - - @staticmethod - def setup_once() -> None: - gql_version = parse_version(gql.__version__) - _check_minimum_version(GQLIntegration, gql_version) - - _patch_execute() - - -def _data_from_document(document: "DocumentNode") -> "EventDataType": - try: - operation_ast = get_operation_ast(document) - data: "EventDataType" = {"query": print_ast(document)} - - if operation_ast is not None: - data["variables"] = operation_ast.variable_definitions - if operation_ast.name is not None: - data["operationName"] = operation_ast.name.value - - return data - except (AttributeError, TypeError): - return dict() - - -def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: - """ - The RequestsHTTPTransport allows defining the HTTP method; all - other transports use POST. - """ - try: - return transport.method - except AttributeError: - return "POST" - - -def _request_info_from_transport( - transport: "Union[Transport, AsyncTransport, None]", -) -> "Dict[str, str]": - if transport is None: - return {} - - request_info = { - "method": _transport_method(transport), - } - - try: - request_info["url"] = transport.url - except AttributeError: - pass - - return request_info - - -def _patch_execute() -> None: - real_execute = gql.Client.execute - - # Maintain signature for backwards compatibility. - # gql.Client.execute() accepts a positional-only "request" - # parameter with version 4.0.0. - @ensure_integration_enabled(GQLIntegration, real_execute) - def sentry_patched_execute( - self: "gql.Client", - document: "DocumentNode", - *args: "Any", - **kwargs: "Any", - ) -> "Any": - scope = sentry_sdk.get_isolation_scope() - # document is a gql.GraphQLRequest with gql v4.0.0. - scope.add_event_processor(_make_gql_event_processor(self, document)) - - try: - # document is a gql.GraphQLRequest with gql v4.0.0. - return real_execute(self, document, *args, **kwargs) - except TransportQueryError as e: - event, hint = event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "gql", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint) - raise e - - gql.Client.execute = sentry_patched_execute - - -def _make_gql_event_processor( - client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" -) -> "EventProcessor": - def processor(event: "Event", hint: "dict[str, Any]") -> "Event": - try: - errors = hint["exc_info"][1].errors - except (AttributeError, KeyError): - errors = None - - request = event.setdefault("request", {}) - request.update( - { - "api_target": "graphql", - **_request_info_from_transport(client.transport), - } - ) - - if should_send_default_pii(): - if GraphQLRequest is not None and isinstance( - document_or_request, GraphQLRequest - ): - # In v4.0.0, gql moved to using GraphQLRequest instead of - # DocumentNode in execute - # https://github.com/graphql-python/gql/pull/556 - document = document_or_request.document - else: - document = document_or_request - - request["data"] = _data_from_document(document) - contexts = event.setdefault("contexts", {}) - response = contexts.setdefault("response", {}) - response.update( - { - "data": {"errors": errors}, - "type": response, - } - ) - - return event - - return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py deleted file mode 100644 index 4938a5c0f3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/graphene.py +++ /dev/null @@ -1,181 +0,0 @@ -from contextlib import contextmanager - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from graphene.types import schema as graphene_schema # type: ignore -except ImportError: - raise DidNotEnable("graphene is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Generator - from typing import Any, Dict, Union - - from graphene.language.source import Source # type: ignore - from graphql.execution import ExecutionResult - from graphql.type import GraphQLSchema - - from sentry_sdk._types import Event - - -class GrapheneIntegration(Integration): - identifier = "graphene" - - @staticmethod - def setup_once() -> None: - version = package_version("graphene") - _check_minimum_version(GrapheneIntegration, version) - - _patch_graphql() - - -def _patch_graphql() -> None: - old_graphql_sync = graphene_schema.graphql_sync - old_graphql_async = graphene_schema.graphql - - @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) - def _sentry_patched_graphql_sync( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = old_graphql_sync(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - async def _sentry_patched_graphql_async( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) - if integration is None: - return await old_graphql_async(schema, source, *args, **kwargs) - - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = await old_graphql_async(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - graphene_schema.graphql_sync = _sentry_patched_graphql_sync - graphene_schema.graphql = _sentry_patched_graphql_async - - -def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - if should_send_default_pii(): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - -@contextmanager -def graphql_span( - schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" -) -> "Generator[None, None, None]": - operation_name = kwargs.get("operation_name") or "" - - operation_type = "query" - op = OP.GRAPHQL_QUERY - if source.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif source.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - sentry_sdk.add_breadcrumb( - crumb={ - "data": { - "operation_name": operation_name, - "operation_type": operation_type, - }, - "category": "graphql.operation", - }, - ) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - additional_attributes = {} - if should_send_default_pii(): - additional_attributes["graphql.document"] = source - - _graphql_span = sentry_sdk.traces.start_span( - name=operation_name, - attributes={ - "sentry.op": op, - "graphql.operation.name": operation_name, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) - - if should_send_default_pii(): - _graphql_span.set_data("graphql.document", source) - _graphql_span.set_data("graphql.operation.name", operation_name) - _graphql_span.set_data("graphql.operation.type", operation_type) - - _graphql_span.__enter__() - - try: - yield - finally: - if is_span_streaming_enabled: - _graphql_span.end() # type: ignore - else: - _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py deleted file mode 100644 index bf74ff1351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/__init__.py +++ /dev/null @@ -1,188 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any, Optional, Sequence - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .client import ClientInterceptor -from .server import ServerInterceptor - -try: - import grpc - from grpc import Channel, Server, intercept_channel - from grpc.aio import Channel as AsyncChannel - from grpc.aio import Server as AsyncServer - - from .aio.client import ( - SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, - ) - from .aio.client import ( - SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, - ) - from .aio.server import ServerInterceptor as AsyncServerInterceptor -except ImportError: - raise DidNotEnable("grpcio is not installed.") - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from typing import Callable, ParamSpec -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - -P = ParamSpec("P") - -GRPC_VERSION = parse_version(grpc.__version__) - - -def _is_channel_intercepted(channel: "Channel") -> bool: - interceptor = getattr(channel, "_interceptor", None) - while interceptor is not None: - if isinstance(interceptor, ClientInterceptor): - return True - - inner_channel = getattr(channel, "_channel", None) - if inner_channel is None: - return False - - channel = inner_channel - interceptor = getattr(channel, "_interceptor", None) - - return False - - -def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - "Wrapper for synchronous secure and insecure channel." - - @wraps(func) - def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": - channel = func(*args, **kwargs) - if not _is_channel_intercepted(channel): - return intercept_channel(channel, ClientInterceptor()) - else: - return channel - - return patched_channel - - -def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - @wraps(func) - def patched_intercept_channel( - channel: "Channel", *interceptors: "grpc.ServerInterceptor" - ) -> "Channel": - if _is_channel_intercepted(channel): - interceptors = tuple( - [ - interceptor - for interceptor in interceptors - if not isinstance(interceptor, ClientInterceptor) - ] - ) - else: - interceptors = interceptors - return intercept_channel(channel, *interceptors) - - return patched_intercept_channel # type: ignore - - -def _wrap_channel_async( - func: "Callable[P, AsyncChannel]", -) -> "Callable[P, AsyncChannel]": - "Wrapper for asynchronous secure and insecure channel." - - @wraps(func) - def patched_channel( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Channel": - sentry_interceptors = [ - AsyncUnaryUnaryClientInterceptor(), - AsyncUnaryStreamClientIntercetor(), - ] - interceptors = [*sentry_interceptors, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_channel # type: ignore - - -def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": - """Wrapper for synchronous server.""" - - @wraps(func) - def patched_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - interceptors = [ - interceptor - for interceptor in interceptors or [] - if not isinstance(interceptor, ServerInterceptor) - ] - server_interceptor = ServerInterceptor() - interceptors = [server_interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_server # type: ignore - - -def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": - """Wrapper for asynchronous server.""" - - @wraps(func) - def patched_aio_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - server_interceptor = AsyncServerInterceptor() - interceptors = [ - server_interceptor, - *(interceptors or []), - ] - - try: - # We prefer interceptors as a list because of compatibility with - # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 - # However, prior to grpc 1.42.0, only tuples were accepted, so we - # have no choice there. - if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): - interceptors = tuple(interceptors) - except Exception: - pass - - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_aio_server # type: ignore - - -class GRPCIntegration(Integration): - identifier = "grpc" - - @staticmethod - def setup_once() -> None: - import grpc - - grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) - grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) - grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) - - grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) - grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) - - grpc.server = _wrap_sync_server(grpc.server) - grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py deleted file mode 100644 index 4d21815254..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .client import ClientInterceptor -from .server import ServerInterceptor - -__all__ = [ - "ClientInterceptor", - "ServerInterceptor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py deleted file mode 100644 index d07b7f19be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/client.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import Any, AsyncIterable, Callable, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.protobuf.message import Message - from grpc.aio import ( - ClientCallDetails, - Metadata, - UnaryStreamCall, - UnaryStreamClientInterceptor, - UnaryUnaryCall, - UnaryUnaryClientInterceptor, - ) -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor: - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - if client_call_details.metadata is None: - client_call_details = client_call_details._replace(metadata=Metadata()) - elif not isinstance(client_call_details.metadata, Metadata): - # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 - # See https://github.com/grpc/grpc/issues/34298. - client_call_details = client_call_details._replace( - metadata=Metadata.from_tuple(client_call_details.metadata) - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - client_call_details.metadata.add(key, value) - return client_call_details - - -class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore - async def intercept_unary_unary( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[UnaryUnaryCall, Message]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_data("code", status_code.name) - - return response - - -class SentryUnaryStreamClientInterceptor( - ClientInterceptor, - UnaryStreamClientInterceptor, # type: ignore -): - async def intercept_unary_stream( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - # status_code = await response.code() - # span.set_data("code", status_code) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py deleted file mode 100644 index 010337e98c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/aio/server.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from typing import Any, Optional - - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler - from grpc.aio import AbortError, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Callable[[ServicerContext], str] | None" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - async def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", - handler_call_details: "HandlerCallDetails", - ) -> "Optional[Awaitable[RpcMethodHandler]]": - handler = await continuation(handler_call_details) - if handler is None: - return None - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - if not handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.unary_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - with sentry_sdk.isolation_scope(): - name = ( - custom_find_name(context) if custom_find_name else method_name - ) - if not name: - return await handler(request, context) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - # What if the headers are empty? - sentry_sdk.traces.continue_trace( - dict(context.invocation_metadata()) - ) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - else: - # What if the headers are empty? - transaction = sentry_sdk.continue_trace( - dict(context.invocation_metadata()), - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - - elif not handler.request_streaming and handler.response_streaming: - handler_factory = grpc.unary_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.unary_stream(request, context): - yield r - - elif handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.stream_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - response = handler.stream_unary(request, context) - return await response - - elif handler.request_streaming and handler.response_streaming: - handler_factory = grpc.stream_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.stream_stream(request, context): - yield r - - return handler_factory( - wrapped, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py deleted file mode 100644 index 5384a0a78f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/client.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Iterator, Union - -try: - import grpc - from google.protobuf.message import Message - from grpc import Call, ClientCallDetails - from grpc._interceptor import _UnaryOutcome - from grpc.aio._interceptor import UnaryStreamCall -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor( - grpc.UnaryUnaryClientInterceptor, # type: ignore - grpc.UnaryStreamClientInterceptor, # type: ignore -): - def intercept_unary_unary( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "_UnaryOutcome": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_attribute( - SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name - ) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_data("code", response.code().name) - - return response - - def intercept_unary_stream( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[Iterator[Message], Call]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - response: "UnaryStreamCall" - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - metadata = ( - list(client_call_details.metadata) if client_call_details.metadata else [] - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - metadata.append((key, value)) - - client_call_details = grpc._interceptor._ClientCallDetails( - method=client_call_details.method, - timeout=client_call_details.timeout, - metadata=metadata, - credentials=client_call_details.credentials, - wait_for_ready=client_call_details.wait_for_ready, - compression=client_call_details.compression, - ) - - return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py deleted file mode 100644 index 9fdb975caf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py deleted file mode 100644 index 1cba1d4b85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/grpc/server.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Callable, Optional - - from google.protobuf.message import Message - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Optional[Callable[[ServicerContext], str]]" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", - handler_call_details: "HandlerCallDetails", - ) -> "RpcMethodHandler": - handler = continuation(handler_call_details) - if not handler or not handler.unary_unary: - return handler - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - def behavior(request: "Message", context: "ServicerContext") -> "Message": - with sentry_sdk.isolation_scope(): - name = custom_find_name(context) if custom_find_name else method_name - - if name: - metadata = dict(context.invocation_metadata()) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(metadata) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - transaction = sentry_sdk.continue_trace( - metadata, - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - return handler.unary_unary(request, context) - - return grpc.unary_unary_rpc_method_handler( - behavior, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py deleted file mode 100644 index a68f20b299..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx is not installed") - -__all__ = ["HttpxIntegration"] - - -class HttpxIntegration(Integration): - identifier = "httpx" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx_client() - _install_httpx_async_client() - - -def _install_httpx_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(HttpxIntegration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(HttpxIntegration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py deleted file mode 100644 index 25062aaa11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/httpx2.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx2 import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx2 is not installed") - -__all__ = ["Httpx2Integration"] - - -class Httpx2Integration(Integration): - identifier = "httpx2" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx2 has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx2_client() - _install_httpx2_async_client() - - -def _install_httpx2_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(Httpx2Integration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx2_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(Httpx2Integration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py deleted file mode 100644 index c3bbc8abcf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huey.py +++ /dev/null @@ -1,239 +0,0 @@ -import sys -from datetime import datetime -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, Hint - from sentry_sdk.utils import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - -try: - from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task - from huey.exceptions import CancelExecution, RetryTask, TaskLockedException -except ImportError: - raise DidNotEnable("Huey is not installed") - -try: - from huey.api import chord as HueyChord - from huey.api import group as HueyGroup -except ImportError: - HueyChord = None - HueyGroup = None - - -HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) - - -class HueyIntegration(Integration): - identifier = "huey" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_enqueue() - patch_execute() - _register_control_flow_exception( - [CancelExecution, RetryTask, TaskLockedException] - ) - - -def patch_enqueue() -> None: - old_enqueue = Huey.enqueue - - @ensure_integration_enabled(HueyIntegration, old_enqueue) - def _sentry_enqueue( - self: "Huey", item: "Any" - ) -> "Optional[Union[Result, ResultGroup]]": - if HueyChord is not None and isinstance(item, HueyChord): - span_name = "Huey Chord" - elif HueyGroup is not None and isinstance(item, HueyGroup): - span_name = "Huey Task Group" - else: - span_name = item.name - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - span_ctx = None - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_HUEY, - "sentry.origin": HueyIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_HUEY, - name=span_name, - origin=HueyIntegration.origin, - ) - - no_headers_types = (PeriodicTask,) + tuple( - t for t in [HueyGroup, HueyChord] if t is not None - ) - with span_ctx: - if not isinstance(item, no_headers_types): - # Attach trace propagation data to task kwargs. We do - # not do this for periodic tasks, as these don't - # really have an originating transaction. - # Additionally, we do not do this for Huey groups or chords, as enqueue will - # recursively call this method for each task within the list, resulting - # in the trace propagation data being attached to each task individually - # (which we want) - item.kwargs["sentry_headers"] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - return old_enqueue(self, item) - - Huey.enqueue = _sentry_enqueue - - -def _make_event_processor(task: "Any") -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["huey_task_id"] = task.id - tags["huey_task_retry"] = task.default_retries > task.retries - extra = event.setdefault("extra", {}) - extra["huey-job"] = { - "task": task.name, - "args": ( - task.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - task.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": (task.default_retries or 0) - task.retries, - } - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.ABORTED) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.OK - return - - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.ERROR - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": HueyIntegration.identifier, "handled": False}, - ) - scope.capture_event(event, hint=hint) - - -def _wrap_task_execute(func: "F") -> "F": - @ensure_integration_enabled(HueyIntegration, func) - def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": - try: - result = func(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_execute # type: ignore - - -def patch_execute() -> None: - old_execute = Huey._execute - - @ensure_integration_enabled(HueyIntegration, old_execute) - def _sentry_execute( - self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None - ) -> "Any": - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope._name = "huey" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task)) - - sentry_headers = task.kwargs.pop("sentry_headers", None) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - headers = sentry_headers or {} - sentry_sdk.traces.continue_trace(headers) - span_ctx = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_TASK_HUEY, - "sentry.origin": HueyIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - "messaging.message.id": task.id, - "messaging.message.system": "huey", - "messaging.message.retry.count": (task.default_retries or 0) - - task.retries, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - sentry_headers or {}, - name=task.name, - op=OP.QUEUE_TASK_HUEY, - source=TransactionSource.TASK, - origin=HueyIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - span_ctx = sentry_sdk.start_transaction(transaction) - - if not getattr(task, "_sentry_is_patched", False): - task.execute = _wrap_task_execute(task.execute) - task._sentry_is_patched = True - - with span_ctx: - return old_execute(self, task, timestamp) - - Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py deleted file mode 100644 index 835acc7279..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/huggingface_hub.py +++ /dev/null @@ -1,392 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - _set_span_data_attribute, - get_start_span_function, - set_data_normalized, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Union - - from sentry_sdk.tracing import Span - -try: - import huggingface_hub.inference._client -except ImportError: - raise DidNotEnable("Huggingface not installed") - - -class HuggingfaceHubIntegration(Integration): - identifier = "huggingface_hub" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "HuggingfaceHubIntegration", include_prompts: bool = True - ) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks - huggingface_hub.inference._client.InferenceClient.text_generation = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.text_generation, - OP.GEN_AI_TEXT_COMPLETION, - ) - ) - huggingface_hub.inference._client.InferenceClient.chat_completion = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.chat_completion, - OP.GEN_AI_CHAT, - ) - ) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "huggingface_hub", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": - @wraps(f) - def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) - if integration is None: - return f(*args, **kwargs) - - prompt = None - if "prompt" in kwargs: - prompt = kwargs["prompt"] - elif "messages" in kwargs: - prompt = kwargs["messages"] - elif len(args) >= 2: - if isinstance(args[1], str) or isinstance(args[1], list): - prompt = args[1] - - if prompt is None: - # invalid call, dont instrument, let it return error - return f(*args, **kwargs) - - client = args[0] - model = client.model or kwargs.get("model") or "" - operation_name = op.split(".")[-1] - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(sentry_sdk.get_client().options): - span = sentry_sdk.traces.start_span( - name=f"{operation_name} {model}", - attributes={ - "sentry.op": op, - "sentry.origin": HuggingfaceHubIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=op, - name=f"{operation_name} {model}", - origin=HuggingfaceHubIntegration.origin, - ) - span.__enter__() - - _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - - if model: - _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) - - # Input attributes - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False - ) - - attribute_mapping = { - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - } - - for attribute, span_attribute in attribute_mapping.items(): - value = kwargs.get(attribute, None) - if value is not None: - if isinstance(value, (int, float, bool, str)): - _set_span_data_attribute(span, span_attribute, value) - else: - set_data_normalized(span, span_attribute, value, unpack=False) - - # LLM Execution - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Output attributes - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - tool_calls = None - usage = None - - with capture_internal_exceptions(): - if isinstance(res, str) and res is not None: - response_text_buffer.append(res) - - if hasattr(res, "generated_text") and res.generated_text is not None: - response_text_buffer.append(res.generated_text) - - if hasattr(res, "model") and res.model is not None: - response_model = res.model - - if hasattr(res, "details") and hasattr(res.details, "finish_reason"): - finish_reason = res.details.finish_reason - - if ( - hasattr(res, "details") - and hasattr(res.details, "generated_tokens") - and res.details.generated_tokens is not None - ): - tokens_used = res.details.generated_tokens - - if hasattr(res, "usage") and res.usage is not None: - usage = res.usage - - if hasattr(res, "choices") and res.choices is not None: - for choice in res.choices: - if hasattr(choice, "finish_reason"): - finish_reason = choice.finish_reason - if hasattr(choice, "message") and hasattr( - choice.message, "tool_calls" - ): - tool_calls = choice.message.tool_calls - if ( - hasattr(choice, "message") - and hasattr(choice.message, "content") - and choice.message.content is not None - ): - response_text_buffer.append(choice.message.content) - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - elif tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - # If the response is not a generator (meaning a streaming response) - # we are done and can return the response - if not inspect.isgenerator(res): - span.__exit__(None, None, None) - return res - - if kwargs.get("details", False): - # text-generation stream output - def new_details_iterator() -> "Iterable[Any]": - finish_reason = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - - with capture_internal_exceptions(): - for chunk in res: - if ( - hasattr(chunk, "token") - and hasattr(chunk.token, "text") - and chunk.token.text is not None - ): - response_text_buffer.append(chunk.token.text) - - if hasattr(chunk, "details") and hasattr( - chunk.details, "finish_reason" - ): - finish_reason = chunk.details.finish_reason - - if ( - hasattr(chunk, "details") - and hasattr(chunk.details, "generated_tokens") - and chunk.details.generated_tokens is not None - ): - tokens_used = chunk.details.generated_tokens - - yield chunk - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - span.__exit__(None, None, None) - - return new_details_iterator() - - else: - # chat-completion stream output - def new_iterator() -> "Iterable[str]": - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tool_calls = None - usage = None - - with capture_internal_exceptions(): - for chunk in res: - if hasattr(chunk, "model") and chunk.model is not None: - response_model = chunk.model - - if hasattr(chunk, "usage") and chunk.usage is not None: - usage = chunk.usage - - if isinstance(chunk, str): - if chunk is not None: - response_text_buffer.append(chunk) - - if hasattr(chunk, "choices") and chunk.choices is not None: - for choice in chunk.choices: - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "content") - and choice.delta.content is not None - ): - response_text_buffer.append( - choice.delta.content - ) - - if ( - hasattr(choice, "finish_reason") - and choice.finish_reason is not None - ): - finish_reason = choice.finish_reason - - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "tool_calls") - and choice.delta.tool_calls is not None - ): - tool_calls = choice.delta.tool_calls - - yield chunk - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - - span.__exit__(None, None, None) - - return new_iterator() - - return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py deleted file mode 100644 index 9dcbb189ce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langchain.py +++ /dev/null @@ -1,1412 +0,0 @@ -import itertools -import json -import sys -import warnings -from collections import OrderedDict -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - _get_value, - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import capture_internal_exceptions, logger - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Dict, - Iterator, - List, - Optional, - Union, - ) - from uuid import UUID - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - - -try: - from langchain_core.agents import AgentFinish - from langchain_core.callbacks import ( - BaseCallbackHandler, - BaseCallbackManager, - Callbacks, - manager, - ) - from langchain_core.messages import BaseMessage - from langchain_core.outputs import LLMResult - -except ImportError: - raise DidNotEnable("langchain not installed") - - -try: - # >=v1 - from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] -except ImportError: - try: - # "Optional[str]": - ai_type = all_params.get("_type") - - if not ai_type or not isinstance(ai_type, str): - return None - - return ai_type - - -DATA_FIELDS = { - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, -} - - -def _transform_langchain_content_block( - content_block: "Dict[str, Any]", -) -> "Dict[str, Any]": - """ - Transform a LangChain content block using the shared transform_content_part function. - - Returns the original content block if transformation is not applicable - (e.g., for text blocks or unrecognized formats). - """ - result = transform_content_part(content_block) - return result if result is not None else content_block - - -def _transform_langchain_message_content(content: "Any") -> "Any": - """ - Transform LangChain message content, handling both string content and - list of content blocks. - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for block in content: - if isinstance(block, dict): - transformed.append(_transform_langchain_content_block(block)) - else: - transformed.append(block) - return transformed - - return content - - -def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": - system_instructions = [] - - for list_ in messages: - for message in list_: - # type of content: str | list[str | dict] | None - if message.type == "system" and isinstance(message.content, str): - system_instructions.append(message.content) - - elif message.type == "system" and isinstance(message.content, list): - for item in message.content: - if isinstance(item, str): - system_instructions.append(item) - - elif isinstance(item, dict) and item.get("type") == "text": - instruction = item.get("text") - if isinstance(instruction, str): - system_instructions.append(instruction) - - return system_instructions - - -def _transform_system_instructions( - system_instructions: "List[str]", -) -> "List[TextPart]": - return [ - { - "type": "text", - "content": instruction, - } - for instruction in system_instructions - ] - - -class LangchainIntegration(Integration): - identifier = "langchain" - origin = f"auto.ai.{identifier}" - - _ignored_exceptions: "set[type[Exception]]" = set() - - def __init__( - self: "LangchainIntegration", - include_prompts: bool = True, - max_spans: "Optional[int]" = None, - ) -> None: - self.include_prompts = include_prompts - self.max_spans = max_spans - - if max_spans is not None: - warnings.warn( - "The `max_spans` parameter of `LangchainIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - manager._configure = _wrap_configure(manager._configure) - - if AgentExecutor is not None: - AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) - AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) - - # Patch embeddings providers - _patch_embeddings_provider(OpenAIEmbeddings) - _patch_embeddings_provider(AzureOpenAIEmbeddings) - _patch_embeddings_provider(VertexAIEmbeddings) - _patch_embeddings_provider(BedrockEmbeddings) - _patch_embeddings_provider(CohereEmbeddings) - _patch_embeddings_provider(MistralAIEmbeddings) - _patch_embeddings_provider(HuggingFaceEmbeddings) - _patch_embeddings_provider(OllamaEmbeddings) - - -class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] - """Callback handler that creates Sentry spans.""" - - def __init__( - self, max_span_map_size: "Optional[int]", include_prompts: bool - ) -> None: - self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() - self.max_span_map_size = max_span_map_size - self.include_prompts = include_prompts - - def gc_span_map(self) -> None: - if self.max_span_map_size is not None: - while len(self.span_map) > self.max_span_map_size: - run_id, span = self.span_map.popitem(last=False) - self._exit_span(span, run_id) - - def _handle_error(self, run_id: "UUID", error: "Any") -> None: - is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) - - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if is_ignored: - span.__exit__(None, None, None) - else: - sentry_sdk.capture_exception( - error, span._scope if isinstance(span, StreamedSpan) else span.scope - ) - span.__exit__(type(error), error, error.__traceback__) - - del self.span_map[run_id] - - def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": - # Transform content to handle multimodal data (images, audio, video, files) - transformed_content = _transform_langchain_message_content(message.content) - parsed = {"role": message.type, "content": transformed_content} - parsed.update(message.additional_kwargs) - return parsed - - def _create_span( - self: "SentryLangchainCallback", - run_id: "UUID", - parent_id: "Optional[Any]", - op: str, - name: str, - origin: str, - ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span = None - if parent_id: - parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( - self.span_map.get(parent_id) - ) - if parent_span: - span = ( - sentry_sdk.traces.start_span( - parent_span=parent_span, - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if isinstance(parent_span, StreamedSpan) - else parent_span.start_child(op=op, name=name, origin=origin) - ) - - if span is None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span = ( - sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if span_streaming - else sentry_sdk.start_span(op=op, name=name, origin=origin) - ) - - span.__enter__() - self.span_map[run_id] = span - self.gc_span_map() - return span - - def _exit_span( - self: "SentryLangchainCallback", - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - run_id: "UUID", - ) -> None: - span.__exit__(None, None, None) - del self.span_map[run_id] - - def on_llm_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - prompts: "List[str]", - *, - run_id: "UUID", - tags: "Optional[List[str]]" = None, - parent_run_id: "Optional[UUID]" = None, - metadata: "Optional[Dict[str, Any]]" = None, - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - parent_run_id, - op=OP.GEN_AI_TEXT_COMPLETION, - name=f"text_completion {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") - - run_name = kwargs.get("name") - if run_name: - set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - if model: - set_on_span( - SPANDATA.GEN_AI_REQUEST_MODEL, - model, - ) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - normalized_messages = [ - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, - "content": {"type": "text", "text": prompt}, - } - for prompt in prompts - ] - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - messages: "List[List[BaseMessage]]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if model: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - system_instructions = _get_system_instructions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - normalized_messages = [] - for list_ in messages: - for message in list_: - if message.type == "system": - continue - - normalized_messages.append( - self._normalize_langchain_message(message) - ) - normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - try: - generation = response.generations[0][0] - except IndexError: - generation = None - - if generation is not None: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - - try: - response_model = generation.message.response_metadata.get( - "model_name" - ) - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - except AttributeError: - pass - - try: - finish_reason = generation.generation_info.get("finish_reason") - if finish_reason is not None: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - [finish_reason], - ) - except AttributeError: - pass - - try: - if should_send_default_pii() and self.include_prompts: - tool_calls = getattr(generation.message, "tool_calls", None) - if tool_calls is not None and tool_calls != []: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - except AttributeError: - pass - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM errors.""" - self._handle_error(run_id, error) - - def on_chat_model_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model errors.""" - self._handle_error(run_id, error) - - def on_agent_finish( - self: "SentryLangchainCallback", - finish: "AgentFinish", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() - ) - - self._exit_span(span, run_id) - - def on_tool_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - input_str: str, - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - tool_name = serialized.get("name") or kwargs.get("name") or "" - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - tool_description = serialized.get("description") - if tool_description is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_TOOL_INPUT, - kwargs.get("inputs", [input_str]), - ) - - def on_tool_end( - self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" - ) -> "Any": - """Run when tool ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) - - self._exit_span(span, run_id) - - def on_tool_error( - self, - error: "SentryLangchainCallback", - *args: "Union[Exception, KeyboardInterrupt]", - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool errors.""" - self._handle_error(run_id, error) - - -def _extract_tokens( - token_usage: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - if not token_usage: - return None, None, None - - input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( - token_usage, "input_tokens" - ) - output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( - token_usage, "output_tokens" - ) - total_tokens = _get_value(token_usage, "total_tokens") - - return input_tokens, output_tokens, total_tokens - - -def _extract_tokens_from_generations( - generations: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - """Extract token usage from response.generations structure.""" - if not generations: - return None, None, None - - total_input = 0 - total_output = 0 - total_total = 0 - - for gen_list in generations: - for gen in gen_list: - token_usage = _get_token_usage(gen) - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - total_input += input_tokens if input_tokens is not None else 0 - total_output += output_tokens if output_tokens is not None else 0 - total_total += total_tokens if total_tokens is not None else 0 - - return ( - total_input if total_input > 0 else None, - total_output if total_output > 0 else None, - total_total if total_total > 0 else None, - ) - - -def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": - """ - Check multiple paths to extract token usage from different objects. - """ - possible_names = ("usage", "token_usage", "usage_metadata") - - message = _get_value(obj, "message") - if message is not None: - for name in possible_names: - usage = _get_value(message, name) - if usage is not None: - return usage - - llm_output = _get_value(obj, "llm_output") - if llm_output is not None: - for name in possible_names: - usage = _get_value(llm_output, name) - if usage is not None: - return usage - - for name in possible_names: - usage = _get_value(obj, name) - if usage is not None: - return usage - - return None - - -def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: - token_usage = _get_token_usage(response) - if token_usage: - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - else: - input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( - response.generations - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) - - -def _get_request_data( - obj: "Any", args: "Any", kwargs: "Any" -) -> "tuple[Optional[str], Optional[List[Any]]]": - """ - Get the agent name and available tools for the agent. - """ - agent = getattr(obj, "agent", None) - runnable = getattr(agent, "runnable", None) - runnable_config = getattr(runnable, "config", {}) - tools = ( - getattr(obj, "tools", None) - or getattr(agent, "tools", None) - or runnable_config.get("tools") - or runnable_config.get("available_tools") - ) - tools = tools if tools and len(tools) > 0 else None - - try: - agent_name = None - if len(args) > 1: - agent_name = args[1].get("run_name") - if agent_name is None: - agent_name = runnable_config.get("run_name") - except Exception: - pass - - return (agent_name, tools) - - -def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": - """Parse and simplify tools into a cleaner format.""" - if not tools: - return None - - if not isinstance(tools, (list, tuple)): - return None - - simplified_tools = [] - for tool in tools: - try: - if isinstance(tool, dict): - if "function" in tool and isinstance(tool["function"], dict): - func = tool["function"] - simplified_tool = { - "name": func.get("name"), - "description": func.get("description"), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif "name" in tool: - simplified_tool = { - "name": tool.get("name"), - "description": tool.get("description"), - } - simplified_tools.append(simplified_tool) - else: - name = ( - tool.get("name") - or tool.get("tool_name") - or tool.get("function_name") - ) - if name: - simplified_tools.append( - { - "name": name, - "description": tool.get("description") - or tool.get("desc"), - } - ) - elif hasattr(tool, "name"): - simplified_tool = { - "name": getattr(tool, "name", None), - "description": getattr(tool, "description", None) - or getattr(tool, "desc", None), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif hasattr(tool, "__name__"): - simplified_tools.append( - { - "name": tool.__name__, - "description": getattr(tool, "__doc__", None), - } - ) - else: - tool_str = str(tool) - if tool_str and tool_str != "": - simplified_tools.append({"name": tool_str, "description": None}) - except Exception: - continue - - return simplified_tools if simplified_tools else None - - -def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: - """Set available tools data on a span if tools are provided.""" - if tools is not None: - simplified_tools = _simplify_langchain_tools(tools) - if simplified_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - simplified_tools, - unpack=False, - ) - - -def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_configure( - callback_manager_cls: type, - inheritable_callbacks: "Callbacks" = None, - local_callbacks: "Callbacks" = None, - *args: "Any", - **kwargs: "Any", - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(LangchainIntegration) - if integration is None: - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - local_callbacks = local_callbacks or [] - - # Handle each possible type of local_callbacks. For each type, we - # extract the list of callbacks to check for SentryLangchainCallback, - # and define a function that would add the SentryLangchainCallback - # to the existing callbacks list. - if isinstance(local_callbacks, BaseCallbackManager): - callbacks_list = local_callbacks.handlers - elif isinstance(local_callbacks, BaseCallbackHandler): - callbacks_list = [local_callbacks] - elif isinstance(local_callbacks, list): - callbacks_list = local_callbacks - else: - logger.debug("Unknown callback type: %s", local_callbacks) - # Just proceed with original function call - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - # Handle each possible type of inheritable_callbacks. - if isinstance(inheritable_callbacks, BaseCallbackManager): - inheritable_callbacks_list = inheritable_callbacks.handlers - elif isinstance(inheritable_callbacks, list): - inheritable_callbacks_list = inheritable_callbacks - else: - inheritable_callbacks_list = [] - - if not any( - isinstance(cb, SentryLangchainCallback) - for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) - ): - sentry_handler = SentryLangchainCallback( - integration.max_spans, - integration.include_prompts, - ) - if isinstance(local_callbacks, BaseCallbackManager): - local_callbacks = local_callbacks.copy() - local_callbacks.handlers = [ - *local_callbacks.handlers, - sentry_handler, - ] - elif isinstance(local_callbacks, BaseCallbackHandler): - local_callbacks = [local_callbacks, sentry_handler] - else: - local_callbacks = [*local_callbacks, sentry_handler] - - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - return new_configure - - -def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: False, - }, - ) as span: - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - else: - start_span_function = get_start_span_function() - - with start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) as span: - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - - return new_invoke - - -def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - else: - start_span_function = get_start_span_function() - - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - input = args[0].get("input") if len(args) >= 1 else None - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Run the agent - result = f(self, *args, **kwargs) - - old_iterator = result - - def new_iterator() -> "Iterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - async def new_iterator_async() -> "AsyncIterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - async for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - if str(type(result)) == "": - result = new_iterator_async() - else: - result = new_iterator() - - return result - - return new_stream - - -def _patch_embeddings_provider(provider_class: "Any") -> None: - """Patch an embeddings provider class with monitoring wrappers.""" - if provider_class is None: - return - - if hasattr(provider_class, "embed_documents"): - provider_class.embed_documents = _wrap_embedding_method( - provider_class.embed_documents - ) - if hasattr(provider_class, "embed_query"): - provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) - if hasattr(provider_class, "aembed_documents"): - provider_class.aembed_documents = _wrap_async_embedding_method( - provider_class.aembed_documents - ) - if hasattr(provider_class, "aembed_query"): - provider_class.aembed_query = _wrap_async_embedding_method( - provider_class.aembed_query - ) - - -def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap sync embedding methods (embed_documents and embed_query).""" - - @wraps(f) - def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - - return new_embedding_method - - -def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap async embedding methods (aembed_documents and aembed_query).""" - - @wraps(f) - async def new_async_embedding_method( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - - return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py deleted file mode 100644 index 3d3856a913..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/langgraph.py +++ /dev/null @@ -1,515 +0,0 @@ -from functools import wraps -from typing import Any, Callable, List, Optional - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration - -# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. -from sentry_sdk.integrations.langchain import LangchainIntegration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -try: - from langgraph.errors import GraphBubbleUp - from langgraph.graph import StateGraph - from langgraph.pregel import Pregel -except ImportError: - raise DidNotEnable("langgraph not installed") - - -class LanggraphIntegration(Integration): - identifier = "langgraph" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) - # LangGraph lets users create agents using a StateGraph or the Functional API. - # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and - # the functional API execute on a Pregel instance. Pregel is the runtime for the graph - # and the invocation happens on Pregel, so patching the invoke methods takes care of both. - # The streaming methods are not patched, because due to some internal reasons, LangGraph - # will automatically patch the streaming methods to run through invoke, and by doing this - # we prevent duplicate spans for invocations. - StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) - if hasattr(Pregel, "invoke"): - Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) - if hasattr(Pregel, "ainvoke"): - Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) - - -def _get_graph_name(graph_obj: "Any") -> "Optional[str]": - for attr in ["name", "graph_name", "__name__", "_name"]: - if hasattr(graph_obj, attr): - name = getattr(graph_obj, attr) - if name and isinstance(name, str): - return name - return None - - -def _normalize_langgraph_message(message: "Any") -> "Any": - if not hasattr(message, "content"): - return None - - parsed = {"role": getattr(message, "type", None), "content": message.content} - - for attr in [ - "name", - "tool_calls", - "function_call", - "tool_call_id", - "response_metadata", - ]: - if hasattr(message, attr): - value = getattr(message, attr) - if value is not None: - parsed[attr] = value - - return parsed - - -def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": - if not state: - return None - - messages = None - - if isinstance(state, dict): - messages = state.get("messages") - elif hasattr(state, "messages"): - messages = state.messages - elif hasattr(state, "get") and callable(state.get): - try: - messages = state.get("messages") - except Exception: - pass - - if not messages or not isinstance(messages, (list, tuple)): - return None - - normalized_messages = [] - for message in messages: - try: - normalized = _normalize_langgraph_message(message) - if normalized: - normalized_messages.append(normalized) - except Exception: - continue - - return normalized_messages if normalized_messages else None - - -def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None or has_span_streaming_enabled(client.options): - return f(self, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GEN_AI_CREATE_AGENT, - origin=LanggraphIntegration.origin, - ) as span: - compiled_graph = f(self, *args, **kwargs) - - compiled_graph_name = getattr(compiled_graph, "name", None) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) - - if compiled_graph_name: - span.description = f"create_agent {compiled_graph_name}" - else: - span.description = "create_agent" - - if kwargs.get("model", None) is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) - - tools = None - get_graph = getattr(compiled_graph, "get_graph", None) - if get_graph and callable(get_graph): - graph_obj = compiled_graph.get_graph() - nodes = getattr(graph_obj, "nodes", None) - if nodes and isinstance(nodes, dict): - tools_node = nodes.get("tools") - if tools_node: - data = getattr(tools_node, "data", None) - if data and hasattr(data, "tools_by_name"): - tools = list(data.tools_by_name.keys()) - - if tools is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) - - return compiled_graph - - return new_compile - - -def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - else: - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_invoke - - -def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles(input_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_ainvoke - - -def _get_new_messages( - input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" -) -> "Optional[List[Any]]": - """Extract only the new messages added during this invocation.""" - if not output_messages: - return None - - if not input_messages: - return output_messages - - # only return the new messages, aka the output messages that are not in the input messages - input_count = len(input_messages) - new_messages = ( - output_messages[input_count:] if len(output_messages) > input_count else [] - ) - - return new_messages if new_messages else None - - -def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": - if not messages: - return None - - for message in reversed(messages): - if isinstance(message, dict): - role = message.get("role") - if role in ["assistant", "ai"]: - content = message.get("content") - if content and isinstance(content, str): - return content - - return None - - -def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": - if not messages: - return None - - tool_calls = [] - for message in messages: - if isinstance(message, dict): - msg_tool_calls = message.get("tool_calls") - if msg_tool_calls and isinstance(msg_tool_calls, list): - tool_calls.extend(msg_tool_calls) - - return tool_calls if tool_calls else None - - -def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - input_tokens = 0 - output_tokens = 0 - total_tokens = 0 - - for message in messages: - response_metadata = message.get("response_metadata") - if response_metadata is None: - continue - - token_usage = response_metadata.get("token_usage") - if not token_usage: - continue - - input_tokens += int(token_usage.get("prompt_tokens", 0)) - output_tokens += int(token_usage.get("completion_tokens", 0)) - total_tokens += int(token_usage.get("total_tokens", 0)) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens > 0: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - total_tokens, - ) - - -def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - if len(messages) == 0: - return - - last_message = messages[-1] - response_metadata = last_message.get("response_metadata") - if response_metadata is None: - return - - model_name = response_metadata.get("model_name") - if model_name is None: - return - - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) - - -def _set_response_attributes( - span: "Any", - input_messages: "Optional[List[Any]]", - result: "Any", - integration: "LanggraphIntegration", -) -> None: - parsed_response_messages = _parse_langgraph_messages(result) - new_messages = _get_new_messages(input_messages, parsed_response_messages) - - if new_messages is None: - return - - _set_usage_data(span, new_messages) - _set_response_model_name(span, new_messages) - - if not (should_send_default_pii() and integration.include_prompts): - return - - llm_response_text = _extract_llm_response_text(new_messages) - if llm_response_text: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) - elif new_messages: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) - else: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) - - tool_calls = _extract_tool_calls(new_messages) - if tool_calls: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(tool_calls), - unpack=False, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py deleted file mode 100644 index 3c2c76450c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/launchdarkly.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - import ldclient - from ldclient.hook import Hook, Metadata - - if TYPE_CHECKING: - from typing import Any - - from ldclient import LDClient - from ldclient.evaluation import EvaluationDetail - from ldclient.hook import EvaluationSeriesContext -except ImportError: - raise DidNotEnable("LaunchDarkly is not installed") - - -class LaunchDarklyIntegration(Integration): - identifier = "launchdarkly" - - def __init__(self, ld_client: "LDClient | None" = None) -> None: - """ - :param client: An initialized LDClient instance. If a client is not provided, this - integration will attempt to use the shared global instance. - """ - try: - client = ld_client or ldclient.get() - except Exception as exc: - raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) - - if not client.is_initialized(): - raise DidNotEnable("LaunchDarkly client is not initialized.") - - # Register the flag collection hook with the LD client. - client.add_hook(LaunchDarklyHook()) - - @staticmethod - def setup_once() -> None: - pass - - -class LaunchDarklyHook(Hook): - @property - def metadata(self) -> "Metadata": - return Metadata(name="sentry-flag-auditor") - - def after_evaluation( - self, - series_context: "EvaluationSeriesContext", - data: "dict[Any, Any]", - detail: "EvaluationDetail", - ) -> "dict[Any, Any]": - if isinstance(detail.value, bool): - add_feature_flag(series_context.key, detail.value) - - return data - - def before_evaluation( - self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" - ) -> "dict[Any, Any]": - return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py deleted file mode 100644 index 49ead6b068..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litellm.py +++ /dev/null @@ -1,376 +0,0 @@ -import copy -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - set_data_normalized, - transform_openai_content_part, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from datetime import datetime - from typing import Any, Dict, List - -try: - import litellm # type: ignore[import-not-found] - from litellm import failure_callback, input_callback, success_callback -except ImportError: - raise DidNotEnable("LiteLLM not installed") - - -# Stash the span on a top-level key of the per-request kwargs dict litellm passes -# to every callback, so it lives and dies with the request. -_SPAN_KEY = "_sentry_span" - - -def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: - kwargs[_SPAN_KEY] = span - - -def _peek_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.get(_SPAN_KEY) - - -def _pop_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.pop(_SPAN_KEY, None) - - -def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": - """ - Convert the message parts from OpenAI format to the `gen_ai.request.messages` format - using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). - - Deep copies messages to avoid mutating original kwargs. - """ - # Deep copy to avoid mutating original messages from kwargs - messages = copy.deepcopy(messages) - - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_openai_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - message["content"] = transformed - return messages - - -def _input_callback(kwargs: "Dict[str, Any]") -> None: - """Handle the start of a request.""" - client = sentry_sdk.get_client() - integration = client.get_integration(LiteLLMIntegration) - - if integration is None: - return - - # Get key parameters - full_model = kwargs.get("model", "") - try: - model, provider, _, _ = litellm.get_llm_provider(full_model) - except Exception: - model = full_model - provider = "unknown" - - call_type = kwargs.get("call_type", None) - if call_type == "embedding" or call_type == "aembedding": - operation = "embeddings" - else: - operation = "chat" - - # Start a new span/transaction - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"{operation} {model}", - attributes={ - "sentry.op": ( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - "sentry.origin": LiteLLMIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - name=f"{operation} {model}", - origin=LiteLLMIntegration.origin, - ) - span.__enter__() - - _store_span(kwargs, span) - - # Set basic data - set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) - - # Record input/messages if allowed - if should_send_default_pii() and integration.include_prompts: - if operation == "embeddings": - # For embeddings, look for the 'input' parameter - embedding_input = kwargs.get("input") - if embedding_input: - scope = sentry_sdk.get_current_scope() - # Normalize to list format - input_list = ( - embedding_input - if isinstance(embedding_input, list) - else [embedding_input] - ) - client = sentry_sdk.get_client() - messages_data = ( - truncate_and_annotate_embedding_inputs(input_list, span, scope) - if should_truncate_gen_ai_input(client.options) - else input_list - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - messages_data, - unpack=False, - ) - else: - # For chat, look for the 'messages' parameter - messages = kwargs.get("messages", []) - if messages: - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages = _convert_message_parts(messages) - messages_data = ( - truncate_and_annotate_messages(messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Record other parameters - params = { - "model": SPANDATA.GEN_AI_REQUEST_MODEL, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - } - for key, attribute in params.items(): - value = kwargs.get(key) - if value is not None: - set_data_normalized(span, attribute, value) - - -async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: - return _input_callback(kwargs) - - -def _success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle successful completion.""" - - span = _peek_span(kwargs) - if span is None: - return - - integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) - if integration is None: - return - - try: - # Record model information - if hasattr(completion_response, "model"): - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model - ) - - # Record response content if allowed - if should_send_default_pii() and integration.include_prompts: - if hasattr(completion_response, "choices"): - response_messages = [] - for choice in completion_response.choices: - if hasattr(choice, "message"): - if hasattr(choice.message, "model_dump"): - response_messages.append(choice.message.model_dump()) - elif hasattr(choice.message, "dict"): - response_messages.append(choice.message.dict()) - else: - # Fallback for basic message objects - msg = {} - if hasattr(choice.message, "role"): - msg["role"] = choice.message.role - if hasattr(choice.message, "content"): - msg["content"] = choice.message.content - if hasattr(choice.message, "tool_calls"): - msg["tool_calls"] = choice.message.tool_calls - response_messages.append(msg) - - if response_messages: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages - ) - - # Record token usage - if hasattr(completion_response, "usage"): - usage = completion_response.usage - record_token_usage( - span, - input_tokens=getattr(usage, "prompt_tokens", None), - output_tokens=getattr(usage, "completion_tokens", None), - total_tokens=getattr(usage, "total_tokens", None), - ) - - finally: - is_streaming = kwargs.get("stream") - # Callback is fired multiple times when streaming a response. - # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 - if ( - is_streaming is not True - or "complete_streaming_response" in kwargs - or "async_complete_streaming_response" in kwargs - ): - span = _pop_span(kwargs) - if span is not None: - span.__exit__(None, None, None) - - -async def _async_success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - return _success_callback( - kwargs, - completion_response, - start_time, - end_time, - ) - - -def _failure_callback( - kwargs: "Dict[str, Any]", - exception: Exception, - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle request failure.""" - span = _pop_span(kwargs) - if span is None: - return - - try: - # Capture the exception - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "litellm", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - finally: - # Always finish the span and clean up - span.__exit__(type(exception), exception, None) - - -class LiteLLMIntegration(Integration): - """ - LiteLLM integration for Sentry. - - This integration automatically captures LiteLLM API calls and sends them to Sentry - for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM - supports, including OpenAI, Anthropic, Google, Cohere, and many others. - - Features: - - Automatic exception capture for all LiteLLM calls - - Token usage tracking across all providers - - Provider detection and attribution - - Input/output message capture (configurable) - - Streaming response support - - Cost tracking integration - - Usage: - - ```python - import litellm - import sentry_sdk - - # Initialize Sentry with the LiteLLM integration - sentry_sdk.init( - dsn="your-dsn", - send_default_pii=True - integrations=[ - sentry_sdk.integrations.LiteLLMIntegration( - include_prompts=True # Set to False to exclude message content - ) - ] - ) - - # All LiteLLM calls will now be monitored - response = litellm.completion( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": "Hello!"}] - ) - ``` - - Configuration: - - include_prompts (bool): Whether to include prompts and responses in spans. - Defaults to True. Set to False to exclude potentially sensitive data. - """ - - identifier = "litellm" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """Set up LiteLLM callbacks for monitoring.""" - litellm.input_callback = input_callback or [] - if _input_callback not in litellm.input_callback: - litellm.input_callback.append(_input_callback) - if _async_input_callback not in litellm.input_callback: - litellm.input_callback.append(_async_input_callback) - - litellm.success_callback = success_callback or [] - if _success_callback not in litellm.success_callback: - litellm.success_callback.append(_success_callback) - if _async_success_callback not in litellm.success_callback: - litellm.success_callback.append(_async_success_callback) - - litellm.failure_callback = failure_callback or [] - if _failure_callback not in litellm.failure_callback: - litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py deleted file mode 100644 index f0c90a7921..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/litestar.py +++ /dev/null @@ -1,364 +0,0 @@ -from collections.abc import Set -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from litestar import Litestar, Request # type: ignore - from litestar.data_extractors import ConnectionDataExtractor # type: ignore - from litestar.exceptions import HTTPException # type: ignore - from litestar.handlers.base import BaseRouteHandler # type: ignore - from litestar.middleware import DefineMiddleware # type: ignore - from litestar.routes.http import HTTPRoute # type: ignore -except ImportError: - raise DidNotEnable("Litestar is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from litestar.middleware import MiddlewareProtocol - from litestar.types import ( # type: ignore - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from litestar.types import ( - Scope as LitestarScope, - ) - from litestar.types.asgi_types import ASGIApp # type: ignore - - from sentry_sdk._types import Event, Hint - -_DEFAULT_TRANSACTION_NAME = "generic Litestar request" - - -class LitestarIntegration(Integration): - identifier = "litestar" - origin = f"auto.http.{identifier}" - - def __init__( - self, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. - # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following - # (among other things): - # 1. Logs them, some at least (such as 500s) as errors - # 2. Calls after_exception hooks - # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event - # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will - # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. - ignore_logger("litestar") - - -class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - def _capture_request_exception(self, exc: Exception) -> None: - """Avoid catching exceptions from request handlers. - - Those exceptions are already handled in Litestar.after_exception handler. - We still catch exceptions from application lifespan handlers. - """ - pass - - -def patch_app_init() -> None: - """ - Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. - See: - - https://docs.litestar.dev/2/usage/applications.html#after-exception - - https://docs.litestar.dev/2/usage/middleware/using-middleware.html - """ - old__init__ = Litestar.__init__ - - @ensure_integration_enabled(LitestarIntegration, old__init__) - def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: - kwargs["after_exception"] = [ - exception_handler, - *(kwargs.get("after_exception") or []), - ] - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Litestar.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryLitestarASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "LitestarScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(LitestarIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=middleware_name, - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR, - "sentry.origin": LitestarIntegration.origin, - }, - ) as middleware_span: - middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.traces.start_span( - name=getattr(receive, "__qualname__", str(receive)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.traces.start_span( - name=getattr(send, "__qualname__", str(send)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - else: - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR, - name=middleware_name, - origin=LitestarIntegration.origin, - ) as middleware_span: - middleware_span.set_tag("litestar.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_SEND, - name=getattr(send, "__qualname__", str(send)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type - elif hasattr(route_handler.fn, "value"): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = LitestarIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if isinstance(scope_user, dict): - return scope_user - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - return None - - -@ensure_integration_enabled(LitestarIntegration) -def exception_handler(exc: Exception, scope: "LitestarScope") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - if isinstance(exc, HTTPException): - integration = sentry_sdk.get_client().get_integration(LitestarIntegration) - if ( - integration is not None - and exc.status_code not in integration.failed_request_status_codes - ): - return - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": LitestarIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py deleted file mode 100644 index a310a0ced6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/logging.py +++ /dev/null @@ -1,476 +0,0 @@ -import logging -import sys -from datetime import datetime, timezone -from fnmatch import fnmatch -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.client import BaseClient -from sentry_sdk.integrations import Integration -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import ( - capture_internal_exceptions, - current_stacktrace, - event_from_exception, - has_logs_enabled, - safe_repr, - to_string, -) - -if TYPE_CHECKING: - from collections.abc import MutableMapping - from logging import LogRecord - from typing import Any, Dict, Optional - -DEFAULT_LEVEL = logging.INFO -DEFAULT_EVENT_LEVEL = logging.ERROR -LOGGING_TO_EVENT_LEVEL = { - logging.NOTSET: "notset", - logging.DEBUG: "debug", - logging.INFO: "info", - logging.WARN: "warning", # WARN is same a WARNING - logging.WARNING: "warning", - logging.ERROR: "error", - logging.FATAL: "fatal", - logging.CRITICAL: "fatal", # CRITICAL is same as FATAL -} - -# Map logging level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - logging.CRITICAL: 21, # fatal - logging.ERROR: 17, # error - logging.WARNING: 13, # warn - logging.INFO: 9, # info - logging.DEBUG: 5, # debug -} - - -# Capturing events from those loggers causes recursion errors. We cannot allow -# the user to unconditionally create events from those loggers under any -# circumstances. -# -# Note: Ignoring by logger name here is better than mucking with thread-locals. -# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. -# -# Events/breadcrumbs and Sentry Logs have separate ignore lists so that -# framework loggers silenced for events (e.g. django.server) can still be -# captured as Sentry Logs. -_IGNORED_LOGGERS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - -_IGNORED_LOGGERS_SENTRY_LOGS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - - -def ignore_logger( - name: str, -) -> None: - """This disables recording (both in breadcrumbs and as events) calls to - a logger of a specific name. Among other uses, many of our integrations - use this to prevent their actions being recorded as breadcrumbs. Exposed - to users as a way to quiet spammy loggers. - - This does **not** affect Sentry Logs — use - :py:func:`ignore_logger_for_sentry_logs` for that. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS.add(name) - - -def ignore_logger_for_sentry_logs( - name: str, -) -> None: - """This disables recording as Sentry Logs calls to a logger of a - specific name. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS_SENTRY_LOGS.add(name) - - -def unignore_logger( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger` call, re-enabling - recording of breadcrumbs and events for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS.discard(name) - - -def unignore_logger_for_sentry_logs( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, - re-enabling recording of Sentry Logs for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) - - -class LoggingIntegration(Integration): - identifier = "logging" - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - self._handler = None - self._breadcrumb_handler = None - self._sentry_logs_handler = None - - if level is not None: - self._breadcrumb_handler = BreadcrumbHandler(level=level) - - if sentry_logs_level is not None: - self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) - - if event_level is not None: - self._handler = EventHandler(level=event_level) - - def _handle_record(self, record: "LogRecord") -> None: - if self._handler is not None and record.levelno >= self._handler.level: - self._handler.handle(record) - - if ( - self._breadcrumb_handler is not None - and record.levelno >= self._breadcrumb_handler.level - ): - self._breadcrumb_handler.handle(record) - - def _handle_sentry_logs_record(self, record: "LogRecord") -> None: - if ( - self._sentry_logs_handler is not None - and record.levelno >= self._sentry_logs_handler.level - ): - self._sentry_logs_handler.handle(record) - - @staticmethod - def setup_once() -> None: - old_callhandlers = logging.Logger.callHandlers - - def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": - # keeping a local reference because the - # global might be discarded on shutdown - ignored_loggers = _IGNORED_LOGGERS - ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS - - try: - return old_callhandlers(self, record) - finally: - # This check is done twice, once also here before we even get - # the integration. Otherwise we have a high chance of getting - # into a recursion error when the integration is resolved - # (this also is slower). - name = record.name.strip() - - handle_events = ( - ignored_loggers is not None and name not in ignored_loggers - ) - handle_sentry_logs = ( - ignored_loggers_sentry_logs is not None - and name not in ignored_loggers_sentry_logs - ) - - if handle_events or handle_sentry_logs: - integration = sentry_sdk.get_client().get_integration( - LoggingIntegration - ) - if integration is not None: - if handle_events: - integration._handle_record(record) - if handle_sentry_logs: - integration._handle_sentry_logs_record(record) - - logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore - - -class _BaseHandler(logging.Handler): - COMMON_RECORD_ATTRS = frozenset( - ( - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "linenno", - "lineno", - "message", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack", - "tags", - "taskName", - "thread", - "threadName", - "stack_info", - ) - ) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - return LOGGING_TO_EVENT_LEVEL.get( - record.levelno, record.levelname.lower() if record.levelname else "" - ) - - def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": - return { - k: v - for k, v in vars(record).items() - if k not in self.COMMON_RECORD_ATTRS - and (not isinstance(k, str) or not k.startswith("_")) - } - - -class EventHandler(_BaseHandler): - """ - A logging handler that emits Sentry events for each log record - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - client_options = client.options - - # exc_info might be None or (None, None, None) - # - # exc_info may also be any falsy value due to Python stdlib being - # liberal with what it receives and Celery's billiard being "liberal" - # with what it sends. See - # https://github.com/getsentry/sentry-python/issues/904 - if record.exc_info and record.exc_info[0] is not None: - event, hint = event_from_exception( - record.exc_info, - client_options=client_options, - mechanism={"type": "logging", "handled": True}, - ) - elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: - event = {} - hint = {} - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=client_options[ - "include_local_variables" - ], - max_value_length=client_options["max_value_length"], - ), - "crashed": False, - "current": True, - } - ] - } - else: - event = {} - hint = {} - - hint["log_record"] = record - - level = self._logging_to_event_level(record) - if level in {"debug", "info", "warning", "error", "critical", "fatal"}: - event["level"] = level # type: ignore[typeddict-item] - event["logger"] = record.name - - if ( - sys.version_info < (3, 11) - and record.name == "py.warnings" - and record.msg == "%s" - ): - # warnings module on Python 3.10 and below sets record.msg to "%s" - # and record.args[0] to the actual warning message. - # This was fixed in https://github.com/python/cpython/pull/30975. - message = record.args[0] - params = () - else: - message = record.msg - params = record.args - - event["logentry"] = { - "message": to_string(message), - "formatted": record.getMessage(), - "params": params, - } - - event["extra"] = self._extra_from_record(record) - - sentry_sdk.capture_event(event, hint=hint) - - -# Legacy name -SentryHandler = EventHandler - - -class BreadcrumbHandler(_BaseHandler): - """ - A logging handler that records breadcrumbs for each log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - sentry_sdk.add_breadcrumb( - self._breadcrumb_from_record(record), hint={"log_record": record} - ) - - def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": - return { - "type": "log", - "level": self._logging_to_event_level(record), - "category": record.name, - "message": record.message, - "timestamp": datetime.fromtimestamp(record.created, timezone.utc), - "data": self._extra_from_record(record), - } - - -class SentryLogsHandler(_BaseHandler): - """ - A logging handler that records Sentry logs for each Python log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS_SENTRY_LOGS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - self._capture_log_from_record(client, record) - - def _capture_log_from_record( - self, client: "BaseClient", record: "LogRecord" - ) -> None: - otel_severity_number, otel_severity_text = _log_level_to_otel( - record.levelno, SEVERITY_TO_OTEL_SEVERITY - ) - project_root = client.options["project_root"] - - attrs: "Any" = self._extra_from_record(record) - attrs["sentry.origin"] = "auto.log.stdlib" - - parameters_set = False - if record.args is not None: - if isinstance(record.args, tuple): - parameters_set = bool(record.args) - for i, arg in enumerate(record.args): - attrs[f"sentry.message.parameter.{i}"] = ( - arg - if isinstance(arg, (str, float, int, bool)) - else safe_repr(arg) - ) - elif isinstance(record.args, dict): - parameters_set = bool(record.args) - for key, value in record.args.items(): - attrs[f"sentry.message.parameter.{key}"] = ( - value - if isinstance(value, (str, float, int, bool)) - else safe_repr(value) - ) - - if parameters_set and isinstance(record.msg, str): - # only include template if there is at least one - # sentry.message.parameter.X set - attrs["sentry.message.template"] = record.msg - - if record.lineno: - attrs["code.line.number"] = record.lineno - - if record.pathname: - if project_root is not None and record.pathname.startswith(project_root): - attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record.pathname - - if record.funcName: - attrs["code.function.name"] = record.funcName - - if record.thread: - attrs["thread.id"] = record.thread - if record.threadName: - attrs["thread.name"] = record.threadName - - if record.process: - attrs["process.pid"] = record.process - if record.processName: - attrs["process.executable.name"] = record.processName - if record.name: - attrs["logger.name"] = record.name - - # noinspection PyProtectedMember - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record.message, - "attributes": attrs, - "time_unix_nano": int(record.created * 1e9), - "trace_id": None, - "span_id": None, - }, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py deleted file mode 100644 index dbb724d9a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/loguru.py +++ /dev/null @@ -1,208 +0,0 @@ -import enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.logging import ( - BreadcrumbHandler, - EventHandler, - _BaseHandler, -) -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import has_logs_enabled, safe_repr - -if TYPE_CHECKING: - from logging import LogRecord - from typing import Any, Optional - -try: - import loguru - from loguru import logger - from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT - - if TYPE_CHECKING: - from loguru import Message -except ImportError: - raise DidNotEnable("LOGURU is not installed") - - -class LoggingLevels(enum.IntEnum): - TRACE = 5 - DEBUG = 10 - INFO = 20 - SUCCESS = 25 - WARNING = 30 - ERROR = 40 - CRITICAL = 50 - - -DEFAULT_LEVEL = LoggingLevels.INFO.value -DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value - - -SENTRY_LEVEL_FROM_LOGURU_LEVEL = { - "TRACE": "DEBUG", - "DEBUG": "DEBUG", - "INFO": "INFO", - "SUCCESS": "INFO", - "WARNING": "WARNING", - "ERROR": "ERROR", - "CRITICAL": "CRITICAL", -} - -# Map Loguru level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - LoggingLevels.CRITICAL: 21, # fatal - LoggingLevels.ERROR: 17, # error - LoggingLevels.WARNING: 13, # warn - LoggingLevels.SUCCESS: 11, # info - LoggingLevels.INFO: 9, # info - LoggingLevels.DEBUG: 5, # debug - LoggingLevels.TRACE: 1, # trace -} - - -class LoguruIntegration(Integration): - identifier = "loguru" - - level: "Optional[int]" = DEFAULT_LEVEL - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL - breadcrumb_format = DEFAULT_FORMAT - event_format = DEFAULT_FORMAT - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - LoguruIntegration.level = level - LoguruIntegration.event_level = event_level - LoguruIntegration.breadcrumb_format = breadcrumb_format - LoguruIntegration.event_format = event_format - LoguruIntegration.sentry_logs_level = sentry_logs_level - - @staticmethod - def setup_once() -> None: - if LoguruIntegration.level is not None: - logger.add( - LoguruBreadcrumbHandler(level=LoguruIntegration.level), - level=LoguruIntegration.level, - format=LoguruIntegration.breadcrumb_format, - ) - - if LoguruIntegration.event_level is not None: - logger.add( - LoguruEventHandler(level=LoguruIntegration.event_level), - level=LoguruIntegration.event_level, - format=LoguruIntegration.event_format, - ) - - if LoguruIntegration.sentry_logs_level is not None: - logger.add( - loguru_sentry_logs_handler, - level=LoguruIntegration.sentry_logs_level, - ) - - -class _LoguruBaseHandler(_BaseHandler): - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - if kwargs.get("level"): - kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( - kwargs.get("level", ""), DEFAULT_LEVEL - ) - - super().__init__(*args, **kwargs) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - try: - return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ - LoggingLevels(record.levelno).name - ].lower() - except (ValueError, KeyError): - return record.levelname.lower() if record.levelname else "" - - -class LoguruEventHandler(_LoguruBaseHandler, EventHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" - - pass - - -class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" - - pass - - -def loguru_sentry_logs_handler(message: "Message") -> None: - # This is intentionally a callable sink instead of a standard logging handler - # since otherwise we wouldn't get direct access to message.record - client = sentry_sdk.get_client() - - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - record = message.record - - if ( - LoguruIntegration.sentry_logs_level is None - or record["level"].no < LoguruIntegration.sentry_logs_level - ): - return - - otel_severity_number, otel_severity_text = _log_level_to_otel( - record["level"].no, SEVERITY_TO_OTEL_SEVERITY - ) - - attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} - - project_root = client.options["project_root"] - if record.get("file"): - if project_root is not None and record["file"].path.startswith(project_root): - attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record["file"].path - - if record.get("line") is not None: - attrs["code.line.number"] = record["line"] - - if record.get("function"): - attrs["code.function.name"] = record["function"] - - if record.get("thread"): - attrs["thread.name"] = record["thread"].name - attrs["thread.id"] = record["thread"].id - - if record.get("process"): - attrs["process.pid"] = record["process"].id - attrs["process.executable.name"] = record["process"].name - - if record.get("name"): - attrs["logger.name"] = record["name"] - - extra = record.get("extra") - if isinstance(extra, dict): - for key, value in extra.items(): - if isinstance(value, (str, int, float, bool)): - attrs[f"sentry.message.parameter.{key}"] = value - else: - attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record["message"], - "attributes": attrs, - "time_unix_nano": int(record["time"].timestamp() * 1e9), - "trace_id": None, - "span_id": None, - } - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py deleted file mode 100644 index 79381fe06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/mcp.py +++ /dev/null @@ -1,876 +0,0 @@ -""" -Sentry integration for MCP (Model Context Protocol) servers. - -This integration instruments MCP servers to create spans for tool, prompt, -and resource handler execution, and captures errors that occur during execution. - -Supports the low-level `mcp.server.lowlevel.Server` API. -""" - -import inspect -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import package_version, safe_serialize - -MCP_PACKAGE_VERSION = package_version("mcp") - -try: - from mcp.server.lowlevel import Server - from mcp.server.streamable_http import ( - StreamableHTTPServerTransport, - ) - - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] - request_ctx, - ) -except ImportError: - raise DidNotEnable("MCP SDK not installed") - -try: - from fastmcp import FastMCP # type: ignore[import-not-found] -except ImportError: - FastMCP = None - -if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - try: - from mcp.server.context import ( - ServerRequestContext, - ) - except ImportError: - ServerRequestContext = None # type: ignore[assignment,misc] -else: - ServerRequestContext = None # type: ignore[assignment,misc] - - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Optional, Tuple, Union - - from starlette.types import Receive, Scope, Send - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -class MCPIntegration(Integration): - identifier = "mcp" - origin = "auto.ai.mcp" - - def __init__(self, include_prompts: bool = True) -> None: - """ - Initialize the MCP integration. - - Args: - include_prompts: Whether to include prompts (tool results and prompt content) - in span data. Requires send_default_pii=True. Default is True. - """ - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """ - Patches MCP server classes to instrument handler execution. - """ - _patch_lowlevel_server() - _patch_handle_request() - - if FastMCP is not None: - _patch_fastmcp() - - -def _get_active_http_scopes( - ctx: "Optional[Any]" = None, -) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return None - - if ( - ctx is None - or not hasattr(ctx, "request") - or ctx.request is None - or "state" not in ctx.request.scope - ): - return None - - return ( - ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), - ctx.request.scope["state"].get("sentry_sdk.current_scope"), - ) - - -def _get_request_context_data( - ctx: "Optional[Any]" = None, -) -> "tuple[Optional[str], Optional[str], str]": - """ - Extract request ID, session ID, and MCP transport type from the request context. - - Returns: - Tuple of (request_id, session_id, mcp_transport). - - request_id: May be None if not available - - session_id: May be None if not available - - mcp_transport: "http", "sse", "stdio" - """ - request_id: "Optional[str]" = None - session_id: "Optional[str]" = None - mcp_transport: str = "stdio" - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return request_id, session_id, mcp_transport - - if ctx is not None: - request_id = ctx.request_id - if hasattr(ctx, "request") and ctx.request is not None: - request = ctx.request - # Detect transport type by checking request characteristics - if hasattr(request, "query_params") and request.query_params.get( - "session_id" - ): - # SSE transport uses query parameter - mcp_transport = "sse" - session_id = request.query_params.get("session_id") - elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): - # StreamableHTTP transport uses header - mcp_transport = "http" - session_id = request.headers.get("mcp-session-id") - - return request_id, session_id, mcp_transport - - -def _get_span_config( - handler_type: str, item_name: str -) -> "tuple[str, str, str, Optional[str]]": - """ - Get span configuration based on handler type. - - Returns: - Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) - Note: result_data_key is None for resources - """ - if handler_type == "tool": - span_data_key = SPANDATA.MCP_TOOL_NAME - mcp_method_name = "tools/call" - result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT - elif handler_type == "prompt": - span_data_key = SPANDATA.MCP_PROMPT_NAME - mcp_method_name = "prompts/get" - result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT - else: # resource - span_data_key = SPANDATA.MCP_RESOURCE_URI - mcp_method_name = "resources/read" - result_data_key = None # Resources don't capture result content - - span_name = f"{mcp_method_name} {item_name}" - return span_data_key, span_name, mcp_method_name, result_data_key - - -def _set_span_input_data( - span: "Union[StreamedSpan, Span]", - handler_name: str, - span_data_key: str, - mcp_method_name: str, - arguments: "dict[str, Any]", - request_id: "Optional[str]", - session_id: "Optional[str]", - mcp_transport: str, -) -> None: - """Set input span data for MCP handlers.""" - - # Set handler identifier - _set_span_data_attribute(span, span_data_key, handler_name) - _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) - - # Set transport/MCP transport type - _set_span_data_attribute( - span, - SPANDATA.NETWORK_TRANSPORT, - "pipe" if mcp_transport == "stdio" else "tcp", - ) - _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) - - # Set request_id if provided - if request_id: - _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) - - # Set session_id if provided - if session_id: - _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) - - # Set request arguments (excluding common request context objects) - for k, v in arguments.items(): - _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) - - -def _extract_tool_result_content(result: "Any") -> "Any": - """ - Extract meaningful content from MCP tool result. - - Tool handlers can return: - - CallToolResult (mcp v2+): Has .content list and optional .structured_content - - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) - - dict (StructuredContent): Return as-is - - list/Iterable (UnstructuredContent): Extract text from content blocks - """ - if result is None: - return None - - # Handle v2 CallToolResult-like objects (has .content list attribute) - if hasattr(result, "content") and isinstance( - getattr(result, "content", None), list - ): - # This is only present when a tool declares an output_schema - structured = getattr(result, "structured_content", None) - if structured is not None: - return structured - return _extract_text_from_content_blocks(result.content) - - # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) - if isinstance(result, tuple) and len(result) == 2: - # Return the structured content (2nd element) - return result[1] - - # Handle StructuredContent: dict - if isinstance(result, dict): - return result - - # Handle UnstructuredContent: iterable of ContentBlock objects - if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): - return _extract_text_from_content_blocks(result) - - return result - - -def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": - texts = [] - try: - for item in content_blocks: - if hasattr(item, "text"): - texts.append(item.text) - elif isinstance(item, dict) and "text" in item: - texts.append(item["text"]) - except Exception: - return content_blocks - return " ".join(texts) if texts else content_blocks - - -def _set_span_output_data( - span: "Union[StreamedSpan, Span]", - result: "Any", - result_data_key: "Optional[str]", - handler_type: str, -) -> None: - """Set output span data for MCP handlers.""" - if result is None: - return - - # Get integration to check PII settings - integration = sentry_sdk.get_client().get_integration(MCPIntegration) - if integration is None: - return - - # Check if we should include sensitive data - should_include_data = should_send_default_pii() and integration.include_prompts - - # For tools, extract the meaningful content - if handler_type == "tool": - extracted = _extract_tool_result_content(result) - if ( - extracted is not None - and should_include_data - and result_data_key is not None - ): - _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) - # Set content count if result is a dict - if isinstance(extracted, dict): - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) - ) - elif handler_type == "prompt": - # For prompts, count messages and set role/content only for single-message prompts - try: - messages: "Optional[list[str]]" = None - message_count = 0 - - # Check if result has messages attribute (GetPromptResult) - if hasattr(result, "messages") and result.messages: - messages = result.messages - message_count = len(messages) - # Also check if result is a dict with messages - elif isinstance(result, dict) and result.get("messages"): - messages = result["messages"] - message_count = len(messages) - - # Always set message count if we found messages - if message_count > 0: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count - ) - - # Only set role and content for single-message prompts if PII is allowed - if message_count == 1 and should_include_data and messages: - first_message = messages[0] - # Extract role - role = None - if hasattr(first_message, "role"): - role = first_message.role - elif isinstance(first_message, dict) and "role" in first_message: - role = first_message["role"] - - if role: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role - ) - - # Extract content text - content_text = None - if hasattr(first_message, "content"): - msg_content = first_message.content - # Content can be a TextContent object or similar - if hasattr(msg_content, "text"): - content_text = msg_content.text - elif isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - elif isinstance(first_message, dict) and "content" in first_message: - msg_content = first_message["content"] - if isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - - if content_text and result_data_key is not None: - _set_span_data_attribute(span, result_data_key, content_text) - except Exception: - # Silently ignore if we can't extract message info - pass - # Resources don't capture result content (result_data_key is None) - - -# Handler data preparation and wrapping - - -def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: - """Check if original_args contains a v2 ServerRequestContext as the first element.""" - return ( - ServerRequestContext is not None - and bool(original_args) - and isinstance(original_args[0], ServerRequestContext) - ) - - -def _extract_handler_data_from_params( - handler_type: str, - params: "Any", -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from a v2 typed params object. - - In MCP SDK v2, handlers receive (ctx, params) where params is a typed - Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). - """ - if handler_type == "tool": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - elif handler_type == "prompt": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - arguments = {"name": handler_name, **arguments} - else: # resource - handler_name = str(getattr(params, "uri", "unknown")) - arguments = {} - - return handler_name, arguments - - -def _extract_handler_data_from_args( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from v1 positional args. - - In MCP SDK v1, handlers receive positional args: - - Tool: (tool_name, arguments) - - Prompt: (name, arguments) - - Resource: (uri,) - """ - original_kwargs = original_kwargs or {} - - if handler_type == "tool": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - elif handler_type == "prompt": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - arguments = {"name": handler_name, **(arguments or {})} - - else: # resource - handler_name = "unknown" - if original_args: - handler_name = str(original_args[0]) - elif original_kwargs.get("uri"): - handler_name = str(original_kwargs["uri"]) - - arguments = {} - - return handler_name, arguments - - -def _prepare_handler_data( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - params: "Optional[Any]" = None, -) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": - """ - Prepare common handler data for both v1 and v2 MCP SDK. - - Args: - handler_type: "tool", "prompt", or "resource" - original_args: Original positional args (v1 path) - original_kwargs: Original keyword args (v1 path) - params: Typed params object from v2 ServerRequestContext path - - Returns: - Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) - """ - if params is not None: - handler_name, arguments = _extract_handler_data_from_params( - handler_type, params - ) - elif _is_v2_context(original_args): - handler_name = "unknown" - arguments = {} - else: - handler_name, arguments = _extract_handler_data_from_args( - handler_type, original_args, original_kwargs - ) - - span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( - handler_type, handler_name - ) - - return ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) - - -async def _handler_wrapper( - handler_type: str, - func: "Callable[..., Any]", - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - self: "Optional[Any]" = None, - force_await: bool = True, -) -> "Any": - """ - Wrapper for MCP handlers. - - Args: - handler_type: "tool", "prompt", or "resource" - func: The handler function to wrap - original_args: Original arguments passed to the handler - original_kwargs: Original keyword arguments passed to the handler - self: Optional instance for bound methods - """ - if original_kwargs is None: - original_kwargs = {} - - # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers - ctx: "Optional[Any]" = None - params: "Optional[Any]" = None - if ( - ServerRequestContext is not None - and original_args - and isinstance(original_args[0], ServerRequestContext) - ): - ctx = original_args[0] - params = original_args[1] if len(original_args) > 1 else None - - ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) = _prepare_handler_data( - handler_type, original_args, original_kwargs, params=params - ) - - scopes = _get_active_http_scopes(ctx=ctx) - - isolation_scope_context: "ContextManager[Any]" - current_scope_context: "ContextManager[Any]" - - if scopes is None: - isolation_scope_context = nullcontext() - current_scope_context = nullcontext() - else: - isolation_scope, current_scope = scopes - - isolation_scope_context = ( - nullcontext() - if isolation_scope is None - else sentry_sdk.scope.use_isolation_scope(isolation_scope) - ) - current_scope_context = ( - nullcontext() - if current_scope is None - else sentry_sdk.scope.use_scope(current_scope) - ) - - # Get request ID, session ID, and transport from context - request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - - # Start span and execute - with isolation_scope_context: - with current_scope_context: - span_mgr: "Union[Span, StreamedSpan]" - if span_streaming: - span_mgr = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.MCP_SERVER, - "sentry.origin": MCPIntegration.origin, - }, - ) - else: - span_mgr = get_start_span_function()( - op=OP.MCP_SERVER, - name=span_name, - origin=MCPIntegration.origin, - ) - - with span_mgr as span: - # Set input span data - _set_span_input_data( - span, - handler_name, - span_data_key, - mcp_method_name, - arguments, - request_id, - session_id, - mcp_transport, - ) - - # For resources, extract and set protocol - if handler_type == "resource": - uri = None - if params is not None: - uri = getattr(params, "uri", None) - - # v1 scenario - if ServerRequestContext is None: - if original_args: - uri = original_args[0] - else: - uri = original_kwargs.get("uri") - - protocol = None - if uri is not None and hasattr(uri, "scheme"): - protocol = uri.scheme - elif handler_name and "://" in handler_name: - protocol = handler_name.split("://")[0] - if protocol: - _set_span_data_attribute( - span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol - ) - - try: - # Execute the async handler - if self is not None: - original_args = (self, *original_args) - - result = func(*original_args, **original_kwargs) - if force_await or inspect.isawaitable(result): - result = await result - - except Exception as e: - # Set error flag for tools - if handler_type == "tool": - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True - ) - sentry_sdk.capture_exception(e) - raise - - _set_span_output_data(span, result, result_data_key, handler_type) - - return result - - -def _create_instrumented_decorator( - original_decorator: "Callable[..., Any]", - handler_type: str, - *decorator_args: "Any", - **decorator_kwargs: "Any", -) -> "Callable[..., Any]": - """ - Create an instrumented version of an MCP decorator. - - This function intercepts MCP decorators (like @server.call_tool()) and injects - Sentry instrumentation into the handler registration flow. The returned decorator - will: - 1. Receive the user's handler function - 2. Pass the instrumented version to the original MCP decorator - - This ensures that when the handler is called at runtime, it's already wrapped - with Sentry spans and metrics collection. - - Args: - original_decorator: The original MCP decorator method (e.g., Server.call_tool) - handler_type: "tool", "prompt", or "resource" - determines span configuration - decorator_args: Positional arguments to pass to the original decorator (e.g., self) - decorator_kwargs: Keyword arguments to pass to the original decorator - - Returns: - A decorator function that instruments handlers before registering them - """ - - def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(func) - async def wrapper(*args: "Any") -> "Any": - return await _handler_wrapper(handler_type, func, args, force_await=False) - - # Then register it with the original MCP decorator - return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) - - return instrumented_decorator - - -_METHOD_TO_HANDLER_TYPE = { - "tools/call": "tool", - "prompts/get": "prompt", - "resources/read": "resource", -} - -# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via -# the Server(...) constructor kwargs rather than add_request_handler. The in-tree -# high-level MCPServer also wires its handlers through these kwargs. -_KWARG_TO_HANDLER_TYPE = { - "on_call_tool": "tool", - "on_get_prompt": "prompt", - "on_read_resource": "resource", -} - - -def _patch_lowlevel_server() -> None: - """ - Patches the mcp.server.lowlevel.Server class to instrument handler execution. - """ - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - _patch_lowlevel_server_v2() - else: - _patch_lowlevel_server_v1() - - -def _patch_lowlevel_server_v1() -> None: - """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" - # Patch call_tool decorator - original_call_tool = Server.call_tool # type: ignore[attr-defined] - - def patched_call_tool( - self: "Server", **kwargs: "Any" - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.call_tool that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_call_tool, "tool", self, **kwargs - )(func) - - Server.call_tool = patched_call_tool # type: ignore[attr-defined] - - # Patch get_prompt decorator - original_get_prompt = Server.get_prompt # type: ignore[attr-defined] - - def patched_get_prompt( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.get_prompt that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_get_prompt, "prompt", self - )(func) - - Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] - - # Patch read_resource decorator - original_read_resource = Server.read_resource # type: ignore[attr-defined] - - def patched_read_resource( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.read_resource that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_read_resource, "resource", self - )(func) - - Server.read_resource = patched_read_resource # type: ignore[attr-defined] - - -def _wrap_v2_handler( - handler_type: str, handler: "Callable[..., Any]" -) -> "Callable[..., Any]": - """Wrap a v2 (ctx, params) handler with Sentry instrumentation. - - Idempotent: an already-wrapped handler is returned unchanged so handlers - registered through more than one path (e.g. MCPServer building a Server) - are not double-wrapped. - """ - if getattr(handler, "__sentry_mcp_wrapped__", False): - return handler - - @wraps(handler) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - return await _handler_wrapper( - handler_type, handler, args, kwargs, force_await=False - ) - - wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] - return wrapper - - -def _patch_lowlevel_server_v2() -> None: - """Patches the v2 Server to wrap tool/prompt/resource handlers. - - Handlers can be registered either via the Server(...) constructor kwargs - (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree - MCPServer and most lowlevel examples use — or via add_request_handler. - Both are patched. - """ - original_init = Server.__init__ - - @wraps(original_init) - def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: - for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): - handler = kwargs.get(kwarg) - if handler is not None: - kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) - original_init(self, *args, **kwargs) - - Server.__init__ = patched_init # type: ignore[method-assign] - - original_add_request_handler = Server.add_request_handler - - def patched_add_request_handler( - self: "Server", - method: str, - params_type: "Any", - handler: "Callable[..., Any]", - *args: "Any", - **kwargs: "Any", - ) -> None: - handler_type = _METHOD_TO_HANDLER_TYPE.get(method) - if handler_type is not None: - handler = _wrap_v2_handler(handler_type, handler) - - original_add_request_handler( - self, method, params_type, handler, *args, **kwargs - ) - - Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] - - -def _patch_handle_request() -> None: - original_handle_request = StreamableHTTPServerTransport.handle_request - - @wraps(original_handle_request) - async def patched_handle_request( - self: "StreamableHTTPServerTransport", - scope: "Scope", - receive: "Receive", - send: "Send", - ) -> None: - scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( - sentry_sdk.get_isolation_scope() - ) - scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() - await original_handle_request(self, scope, receive, send) - - StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] - - -def _patch_fastmcp() -> None: - """ - Patches the standalone fastmcp package's FastMCP class. - - The standalone fastmcp package (v2.14.0+) registers its own handlers for - prompts and resources directly, bypassing the Server decorators we patch. - This function patches the _get_prompt_mcp and _read_resource_mcp methods - to add instrumentation for those handlers. - """ - if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): - original_get_prompt_mcp = FastMCP._get_prompt_mcp - - @wraps(original_get_prompt_mcp) - async def patched_get_prompt_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "prompt", - original_get_prompt_mcp, - args, - kwargs, - self, - ) - - FastMCP._get_prompt_mcp = patched_get_prompt_mcp - - if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): - original_read_resource_mcp = FastMCP._read_resource_mcp - - @wraps(original_read_resource_mcp) - async def patched_read_resource_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "resource", - original_read_resource_mcp, - args, - kwargs, - self, - ) - - FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py deleted file mode 100644 index b6111492bb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/modules.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import _get_installed_modules - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - - -class ModulesIntegration(Integration): - identifier = "modules" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Any") -> "Event": - if event.get("type") == "transaction": - return event - - if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: - return event - - event["modules"] = _get_installed_modules() - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py deleted file mode 100644 index 186c665ed1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai.py +++ /dev/null @@ -1,1530 +0,0 @@ -import json -import sys -import time -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai._openai_completions_api import ( - _get_system_instructions as _get_system_instructions_completions, -) -from sentry_sdk.ai._openai_completions_api import ( - _get_text_items, - _transform_system_instructions, -) -from sentry_sdk.ai._openai_completions_api import ( - _is_system_instruction as _is_system_instruction_completions, -) -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions as _get_system_instructions_responses, -) -from sentry_sdk.ai._openai_responses_api import ( - _is_system_instruction as _is_system_instruction_responses, -) -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, - safe_serialize, -) - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Iterable, - Iterator, - List, - Optional, - Union, - ) - - from openai import Omit - from openai.types import CompletionUsage - from openai.types.responses import ( - ResponseInputParam, - ResponseStreamEvent, - SequenceNotStr, - ) - from openai.types.responses.response_usage import ResponseUsage - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -try: - try: - from openai import NotGiven - except ImportError: - NotGiven = None - - try: - from openai import Omit - except ImportError: - Omit = None - - from openai import AsyncStream, Stream - from openai.resources import AsyncEmbeddings, Embeddings - from openai.resources.chat.completions import AsyncCompletions, Completions - - if TYPE_CHECKING: - from openai.types.chat import ( - ChatCompletionChunk, - ChatCompletionMessageParam, - ) -except ImportError: - raise DidNotEnable("OpenAI not installed") - -RESPONSES_API_ENABLED = True -try: - # responses API support was introduced in v1.66.0 - from openai.resources.responses import AsyncResponses, Responses - from openai.types.responses.response_completed_event import ResponseCompletedEvent -except ImportError: - RESPONSES_API_ENABLED = False - - -class OpenAIIntegration(Integration): - identifier = "openai" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "OpenAIIntegration", - include_prompts: bool = True, - tiktoken_encoding_name: "Optional[str]" = None, - ) -> None: - self.include_prompts = include_prompts - - self.tiktoken_encoding = None - if tiktoken_encoding_name is not None: - import tiktoken # type: ignore - - self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) - - @staticmethod - def setup_once() -> None: - Completions.create = _wrap_chat_completion_create(Completions.create) - AsyncCompletions.create = _wrap_async_chat_completion_create( - AsyncCompletions.create - ) - - Embeddings.create = _wrap_embeddings_create(Embeddings.create) - AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) - - if RESPONSES_API_ENABLED: - Responses.create = _wrap_responses_create(Responses.create) - AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) - - def count_tokens(self: "OpenAIIntegration", s: str) -> int: - if self.tiktoken_encoding is None: - return 0 - try: - return len(self.tiktoken_encoding.encode_ordinary(s)) - except Exception: - return 0 - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _has_attr_and_is_int( - token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str -) -> bool: - return hasattr(token_usage, attr_name) and isinstance( - getattr(token_usage, attr_name, None), int - ) - - -def _calculate_completions_token_usage( - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - streaming_message_total_token_usage: "Optional[CompletionUsage]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Chat Completions API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - usage = None - - if streaming_message_total_token_usage is not None: - usage = streaming_message_total_token_usage - elif hasattr(response, "usage"): - usage = response.usage - - if usage is not None: - if _has_attr_and_is_int(usage, "prompt_tokens"): - input_tokens = usage.prompt_tokens - if _has_attr_and_is_int(usage, "completion_tokens"): - output_tokens = usage.completion_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "prompt_tokens_details"): - cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "completion_tokens_details"): - reasoning = getattr( - usage.completion_tokens_details, "reasoning_tokens", None - ) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in messages or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "choices") and response.choices is not None: - for choice in response.choices: - if hasattr(choice, "message") and hasattr(choice.message, "content"): - output_tokens += count_tokens(choice.message.content) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _calculate_responses_token_usage( - input: "Any", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Responses API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - - if hasattr(response, "usage"): - usage = response.usage - - if _has_attr_and_is_int(usage, "input_tokens"): - input_tokens = usage.input_tokens - if _has_attr_and_is_int(usage, "output_tokens"): - output_tokens = usage.output_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "input_tokens_details"): - cached = getattr(usage.input_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "output_tokens_details"): - reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in input or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - # Deliberate use of Completions function for both Completions and Responses input format. - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "output"): - for output_item in response.output: - if hasattr(output_item, "content"): - for content_item in output_item.content: - if hasattr(content_item, "text"): - output_tokens += count_tokens(content_item.text) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _set_responses_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") - messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_output_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - conversation = kwargs.get("conversation") - if conversation is not None and _is_given(conversation): - conversation_id: "Optional[str]" = None - if isinstance(conversation, str): - conversation_id = conversation - elif isinstance(conversation, dict): - conversation_id = conversation.get("id") - if conversation_id is not None: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - if not should_send_default_pii() or not integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if ( - messages is None - and explicit_instructions is not None - and _is_given(explicit_instructions) - ): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - [ - { - "type": "text", - "content": explicit_instructions, - } - ] - ), - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if messages is None: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - instructions_text_parts: "list[TextPart]" = [] - if explicit_instructions is not None and _is_given(explicit_instructions): - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions_responses(messages) - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - non_system_messages = [ - message for message in messages if not _is_system_instruction_responses(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - - -def _set_completions_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( - "messages" - ) - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - presence_penalty = kwargs.get("presence_penalty") - if presence_penalty is not None and _is_given(presence_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) - - frequency_penalty = kwargs.get("frequency_penalty") - if frequency_penalty is not None and _is_given(frequency_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - messages = list(messages) - kwargs["messages"] = messages - - system_instructions = _get_system_instructions_completions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - non_system_messages = [ - message - for message in messages - if not _is_system_instruction_completions(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - -def _set_embeddings_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( - "input" - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - return - - if isinstance(messages, str): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - return - - messages = list(messages) - kwargs["input"] = messages - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - -def _set_common_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - input: "Any", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - if hasattr(response, "model"): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) - - # Chat Completions API - if hasattr(response, "choices") and response.choices is not None: - if should_send_default_pii() and integration.include_prompts: - response_text = [ - choice.message.model_dump() - for choice in response.choices - if choice.message is not None - ] - if len(response_text) > 0: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) - - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - # Responses API - elif hasattr(response, "output"): - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in response.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - # Embeddings API (fallback for responses with neither choices nor output) - else: - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_synchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_asynchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _set_completions_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - _set_common_output_data( - span, - response, - messages, - integration, - finish_span, - ) - - -def _wrap_synchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Stream[ChatCompletionChunk]", - old_iterator: "Iterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "Iterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "AsyncStream[ChatCompletionChunk]", - old_iterator: "AsyncIterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "AsyncIterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - async for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -def _wrap_synchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "Stream[ResponseStreamEvent]", - old_iterator: "Iterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "Iterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "AsyncStream[ResponseStreamEvent]", - old_iterator: "AsyncIterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "AsyncIterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft: "Optional[float]" = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - async for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _set_responses_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _set_embeddings_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - return _new_sync_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - return await _new_async_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -async def _new_async_embeddings_create( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -def _wrap_embeddings_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_embeddings_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_synchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_asynchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _wrap_responses_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_responses_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_responses_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_responses_create(f, *args, **kwargs) - - return _sentry_patched_responses_async - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different openai versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py deleted file mode 100644 index 5895f53ad3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/__init__.py +++ /dev/null @@ -1,250 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .patches import ( - _create_run_streamed_wrapper, - _create_run_wrapper, - _execute_final_output, - _execute_handoffs, - _get_all_tools, - _get_model, - _patch_error_tracing, - _run_single_turn, - _run_single_turn_streamed, -) - -try: - # "agents" is too generic. If someone has an agents.py file in their project - # or another package that's importable via "agents", no ImportError would - # be thrown and the integration would enable itself even if openai-agents is - # not installed. That's why we're adding the second, more specific import - # after it, even if we don't use it. - import agents - from agents.run import AgentRunner - from agents.version import __version__ as OPENAI_AGENTS_VERSION - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -try: - # AgentRunner methods moved in v0.8 - # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop, turn_preparation, turn_resolution -except ImportError: - run_loop = None - turn_preparation = None - turn_resolution = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - from agents.run_internal.run_steps import SingleStepResult - - -def _patch_runner() -> None: - # Create the root span for one full agent run (including eventual handoffs) - # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around - # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. - agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run - ) - - # Patch streaming runner - agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run_streamed - ) - - -class OpenAIAgentsIntegration(Integration): - """ - NOTE: With version 0.8.0, the class methods below have been refactored to functions. - - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` - - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` - - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` - - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` - - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` - - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. - 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. - - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. - - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. - 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. - - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. - - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. - 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. - - Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. - Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. - An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. - Handoffs are a way to switch agent-wide configuration. - - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` - """ - - identifier = "openai_agents" - - @staticmethod - def setup_once() -> None: - _patch_error_tracing() - _patch_runner() - - library_version = parse_version(OPENAI_AGENTS_VERSION) - if library_version is not None and library_version >= ( - 0, - 8, - ): - if run_loop is not None: - - @wraps(run_loop.get_all_tools) - async def new_wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools( - run_loop.get_all_tools, agent, context_wrapper - ) - - agents.run.get_all_tools = new_wrapped_get_all_tools - - @wraps(run_loop.run_single_turn) - async def new_wrapped_run_single_turn( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn( - run_loop.run_single_turn, *args, **kwargs - ) - - agents.run.run_single_turn = new_wrapped_run_single_turn - - @wraps(run_loop.run_single_turn_streamed) - async def new_wrapped_run_single_turn_streamed( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - run_loop.run_single_turn_streamed, *args, **kwargs - ) - - agents.run.run_single_turn_streamed = ( - new_wrapped_run_single_turn_streamed - ) - - if turn_preparation is not None: - - @wraps(turn_preparation.get_model) - def new_wrapped_get_model( - agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(turn_preparation.get_model, agent, run_config) - - agents.run_internal.run_loop.get_model = new_wrapped_get_model - - if turn_resolution is not None: - original_execute_handoffs = turn_resolution.execute_handoffs - - @wraps(original_execute_handoffs) - async def new_wrapped_execute_handoffs( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs( - original_execute_handoffs, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_handoffs = ( - new_wrapped_execute_handoffs - ) - - original_execute_final_output = turn_resolution.execute_final_output - - @wraps(turn_resolution.execute_final_output) - async def new_wrapped_final_output( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_final_output = ( - new_wrapped_final_output - ) - - return - - original_get_all_tools = AgentRunner._get_all_tools - - @wraps(AgentRunner._get_all_tools.__func__) - async def old_wrapped_get_all_tools( - cls: "agents.Runner", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) - - agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) - - original_get_model = AgentRunner._get_model - - @wraps(AgentRunner._get_model.__func__) - def old_wrapped_get_model( - cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(original_get_model, agent, run_config) - - agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) - - original_run_single_turn = AgentRunner._run_single_turn - - @wraps(AgentRunner._run_single_turn.__func__) - async def old_wrapped_run_single_turn( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn(original_run_single_turn, *args, **kwargs) - - agents.run.AgentRunner._run_single_turn = classmethod( - old_wrapped_run_single_turn - ) - - original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed - - @wraps(AgentRunner._run_single_turn_streamed.__func__) - async def old_wrapped_run_single_turn_streamed( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - original_run_single_turn_streamed, *args, **kwargs - ) - - agents.run.AgentRunner._run_single_turn_streamed = classmethod( - old_wrapped_run_single_turn_streamed - ) - - original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs - - @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) - async def old_wrapped_execute_handoffs( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) - - agents._run_impl.RunImpl.execute_handoffs = classmethod( - old_wrapped_execute_handoffs - ) - - original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - - @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) - async def old_wrapped_final_output( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents._run_impl.RunImpl.execute_final_output = classmethod( - old_wrapped_final_output - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py deleted file mode 100644 index f5de978be0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py deleted file mode 100644 index 85d48f2d41..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .agent_run import ( - _execute_final_output, # noqa: F401 - _execute_handoffs, # noqa: F401 - _run_single_turn, # noqa: F401 - _run_single_turn_streamed, # noqa: F401 -) -from .error_tracing import _patch_error_tracing # noqa: F401 -from .models import _get_model # noqa: F401 -from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 -from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py deleted file mode 100644 index 71883b2eef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ /dev/null @@ -1,332 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import ( - handoff_span, - invoke_agent_span, - update_invoke_agent_span, -) - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, Union - - from agents.run_internal.run_steps import SingleStepResult - - from sentry_sdk.tracing import Span - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: - """Check if there's an active agent span for this context""" - return getattr(context_wrapper, "_sentry_current_agent", None) is not None - - -def _get_current_agent( - context_wrapper: "agents.RunContextWrapper", -) -> "Optional[agents.Agent]": - """Get the current agent from context wrapper""" - return getattr(context_wrapper, "_sentry_current_agent", None) - - -def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: - """Close the workflow span for streaming executions if it exists.""" - if agent and hasattr(agent, "_sentry_workflow_span"): - workflow_span = agent._sentry_workflow_span - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - -def _maybe_start_agent_span( - context_wrapper: "agents.RunContextWrapper", - agent: "agents.Agent", - should_run_agent_start_hooks: bool, - span_kwargs: "dict[str, Any]", - is_streaming: bool = False, -) -> "Optional[Union[Span, StreamedSpan]]": - """ - Start an agent invocation span if conditions are met. - Handles ending any existing span for a different agent. - - Returns the new span if started, or the existing span if conditions aren't met. - """ - if not (should_run_agent_start_hooks and agent and context_wrapper): - return getattr(context_wrapper, "_sentry_agent_span", None) - - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - # Store the agent on the context wrapper so we can access it later - context_wrapper._sentry_current_agent = agent - span = invoke_agent_span(context_wrapper, agent, span_kwargs) - context_wrapper._sentry_agent_span = span - agent._sentry_agent_span = span - - if not is_streaming: - return span - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - return span - - -async def _run_single_turn( - original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn that - - creates agent invocation spans if there is no already active agent invocation span. - - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. - """ - # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. - bindings = kwargs.get("bindings") - agent = ( - getattr(bindings, "public_agent", None) - if bindings is not None - else kwargs.get("agent") - ) - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) - - span = _maybe_start_agent_span( - context_wrapper, agent, should_run_agent_start_hooks, kwargs - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn(*args, **kwargs) - - try: - result = await original_run_single_turn(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - return result - - -async def _run_single_turn_streamed( - original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn_streamed that - - creates agent invocation spans for streaming if there is no already active agent invocation span. - - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. - - Note: Unlike _run_single_turn which uses keyword-only arguments (*,), - _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: - _run_single_turn_streamed( - streamed_result, # args[0] - bindings, # args[1] - hooks, # args[2] - context_wrapper, # args[3] - run_config, # args[4] - should_run_agent_start_hooks, # args[5] - tool_use_tracker, # args[6] - all_tools, # args[7] - server_conversation_tracker, # args[8] (optional) - ) - """ - streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") - # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. - agent_or_bindings = ( - args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) - ) - agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) - context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") - should_run_agent_start_hooks = bool( - args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) - ) - - span_kwargs: "dict[str, Any]" = {} - if streamed_result and hasattr(streamed_result, "input"): - span_kwargs["original_input"] = streamed_result.input - - span = _maybe_start_agent_span( - context_wrapper, - agent, - should_run_agent_start_hooks, - span_kwargs, - is_streaming=True, - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn_streamed(*args, **kwargs) - - try: - result = await original_run_single_turn_streamed(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - return result - - -async def _execute_handoffs( - original_execute_handoffs: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_handoffs that - - creates and manages handoff spans. - - ends the agent invocation span. - - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. - """ - - context_wrapper = kwargs.get("context_wrapper") - run_handoffs = kwargs.get("run_handoffs") - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - - # Create Sentry handoff span for the first handoff (agents library only processes the first one) - if run_handoffs: - first_handoff = run_handoffs[0] - handoff_agent_name = first_handoff.handoff.agent_name - handoff_span(context_wrapper, agent, handoff_agent_name) - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - # Call original method with all parameters - try: - return await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - # Call original method with all parameters - try: - result = await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result - - -async def _execute_final_output( - original_execute_final_output: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_final_output that - - ends the agent invocation span. - - ends the workflow span if the response is streamed. - """ - - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - context_wrapper = kwargs.get("context_wrapper") - final_output = kwargs.get("final_output") - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - try: - return await original_execute_final_output(*args, **kwargs) - finally: - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - - try: - result = await original_execute_final_output(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py deleted file mode 100644 index 68dadb3101..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/error_tracing.py +++ /dev/null @@ -1,74 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANSTATUS -from sentry_sdk.traces import SpanStatus -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -def _patch_error_tracing() -> None: - """ - Patches agents error tracing function to inject our span error logic - when a tool execution fails. - - In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span - In older versions, it was at: agents._utils.attach_error_to_current_span - - This works even when the module or function doesn't exist. - """ - error_tracing_module = None - - # Try newer location first (agents.util._error_tracing) - try: - from agents.util import _error_tracing - - error_tracing_module = _error_tracing - except (ImportError, AttributeError): - pass - - # Try older location (agents._utils) - if error_tracing_module is None: - try: - import agents._utils - - error_tracing_module = agents._utils - except (ImportError, AttributeError): - # Module doesn't exist in either location, nothing to patch - return - - # Check if the function exists - if not hasattr(error_tracing_module, "attach_error_to_current_span"): - return - - original_attach_error = error_tracing_module.attach_error_to_current_span - - @wraps(original_attach_error) - def sentry_attach_error_to_current_span( - error: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Wraps agents' error attachment to also set Sentry span status to error. - This allows us to properly track tool execution errors even though - the agents library swallows exceptions. - """ - # Set the current Sentry span to errored - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - current_span = sentry_sdk.get_current_scope().streamed_span - if current_span is not None: - current_span.status = SpanStatus.ERROR - else: - current_span = sentry_sdk.get_current_span() - if current_span is not None: - current_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # Call the original function - return original_attach_error(error, *args, **kwargs) - - error_tracing_module.attach_error_to_current_span = ( - sentry_attach_error_to_current_span - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py deleted file mode 100644 index 634c9fdca1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/models.py +++ /dev/null @@ -1,197 +0,0 @@ -import copy -import time -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_sentry_baggage_to_headers, - should_propagate_trace, -) -from sentry_sdk.utils import logger - -from ..spans import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - from sentry_sdk.tracing import Span - -try: - import agents - from agents.tool import HostedMCPTool -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _set_response_model_on_agent_span( - agent: "agents.Agent", response_model: "Optional[str]" -) -> None: - """Set the response model on the agent's invoke_agent span if available.""" - if response_model: - agent_span = getattr(agent, "_sentry_agent_span", None) - if agent_span: - if isinstance(agent_span, StreamedSpan): - agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - else: - agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - -def _inject_trace_propagation_headers( - hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" -) -> None: - headers = hosted_tool.tool_config.get("headers") - if headers is None: - headers = {} - hosted_tool.tool_config["headers"] = headers - - mcp_url = hosted_tool.tool_config.get("server_url") - if not mcp_url: - return - - if should_propagate_trace(sentry_sdk.get_client(), mcp_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( - key=key, value=value, mcp_url=mcp_url - ) - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(headers, value) - else: - headers[key] = value - - -def _get_model( - original_get_model: "Callable[..., agents.Model]", - agent: "agents.Agent", - run_config: "agents.RunConfig", -) -> "agents.Model": - """ - Responsible for - - creating and managing AI client spans. - - adding trace propagation headers to tools with type HostedMCPTool. - - setting the response model on agent invocation spans. - """ - # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) - # because we only patch its direct methods, all underlying data can remain unchanged. - model = copy.copy(original_get_model(agent, run_config)) - - # Capture the request model name for spans (agent.model can be None when using defaults) - request_model_name = model.model if hasattr(model, "model") else str(model) - agent._sentry_request_model = request_model_name - - # Wrap _fetch_response if it exists (for OpenAI models) to capture response model - if hasattr(model, "_fetch_response"): - original_fetch_response = model._fetch_response - - @wraps(original_fetch_response) - async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": - response = await original_fetch_response(*args, **kwargs) - if hasattr(response, "model") and response.model: - agent._sentry_response_model = str(response.model) - return response - - model._fetch_response = wrapped_fetch_response - - original_get_response = model.get_response - - @wraps(original_get_response) - async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": - mcp_tools = kwargs.get("tools") - hosted_tools = [] - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - result = await original_get_response(*args, **kwargs) - - # Get response model captured from _fetch_response and clean up - response_model = getattr(agent, "_sentry_response_model", None) - if response_model: - delattr(agent, "_sentry_response_model") - - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span(span, result, response_model, agent) - - return result - - model.get_response = wrapped_get_response - - # Also wrap stream_response for streaming support - if hasattr(model, "stream_response"): - original_stream_response = model.stream_response - - @wraps(original_stream_response) - async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": - span_kwargs = dict(kwargs) - if len(args) > 0: - span_kwargs["system_instructions"] = args[0] - if len(args) > 1: - span_kwargs["input"] = args[1] - - hosted_tools = [] - if len(args) > 3: - mcp_tools = args[3] - - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, span_kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - streaming_response = None - ttft_recorded = False - # Capture start time locally to avoid race conditions with concurrent requests - start_time = time.perf_counter() - - async for event in original_stream_response(*args, **kwargs): - # Detect first content token (text delta event) - if not ttft_recorded and hasattr(event, "delta"): - ttft = time.perf_counter() - start_time - set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) - ttft_recorded = True - - # Capture the full response from ResponseCompletedEvent - if hasattr(event, "response"): - streaming_response = event.response - yield event - - # Update span with response data (usage, output, model) - if streaming_response: - response_model = ( - str(streaming_response.model) - if hasattr(streaming_response, "model") - and streaming_response.model - else None - ) - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span( - span, streaming_response, response_model, agent - ) - - model.stream_response = wrapped_stream_response - - return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py deleted file mode 100644 index 5f9996595f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/runner.py +++ /dev/null @@ -1,221 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import agent_workflow_span, update_invoke_agent_span -from ..utils import _capture_exception - -try: - from agents.exceptions import AgentsException -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, AsyncIterator, Callable - - -def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run methods to - - create and manage a root span for the agent workflow runs. - - end the agent invocation span if an `AgentsException` is raised in `run()`. - - Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), - so it does not need to be wrapped separately. - """ - - @wraps(original_func) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - with agent_workflow_span(agent) as workflow_span: - # Set conversation ID on workflow span early so it's captured even on errors - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - run_result = await original_func(*args, **kwargs) - except AgentsException as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - - context_wrapper = getattr(exc.run_data, "context_wrapper", None) - if context_wrapper is not None: - invoke_agent_span = getattr( - context_wrapper, "_sentry_agent_span", None - ) - - if invoke_agent_span is not None and ( - ( - isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.end_timestamp is None - ) - or ( - not isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.timestamp is None - ) - ): - update_invoke_agent_span( - span=invoke_agent_span, - context=context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Invoke agent span is not finished in this case. - # This is much less likely to occur than other cases because - # AgentRunner.run() is "just" a while loop around _run_single_turn. - _capture_exception(exc) - reraise(*exc_info) - - invoke_agent_span = getattr( - run_result.context_wrapper, "_sentry_agent_span", None - ) - if not invoke_agent_span: - return run_result - - update_invoke_agent_span( - span=invoke_agent_span, - context=run_result.context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(None, None, None) - delattr(run_result.context_wrapper, "_sentry_agent_span") - return run_result - - return wrapper - - -def _create_run_streamed_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run_streamed method to - - create a root span for streaming agent workflow runs. - - end the workflow span if and only if the response stream is consumed or cancelled. - - Unlike run(), run_streamed() returns immediately with a RunResultStreaming object - while execution continues in a background task. The workflow span must stay open - throughout the streaming operation and close when streaming completes or is abandoned. - - Note: We don't use isolation_scope() here because it uses context variables that - cannot span async boundaries (the __enter__ and __exit__ would be called from - different async contexts, causing ValueError). - """ - - @wraps(original_func) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - # Capture conversation_id from kwargs if provided - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - # Start workflow span immediately (before run_streamed returns) - workflow_span = agent_workflow_span(agent) - workflow_span.__enter__() - - # Set conversation ID on workflow span early so it's captured even on errors - if conversation_id: - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - # Store span on agent for cleanup - agent._sentry_workflow_span = workflow_span - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - # Call original function to get RunResultStreaming - run_result = original_func(*args, **kwargs) - except Exception as exc: - # If run_streamed itself fails (not the background task), clean up immediately - workflow_span.__exit__(*sys.exc_info()) - _capture_exception(exc) - raise - - def _close_workflow_span() -> None: - if hasattr(agent, "_sentry_workflow_span"): - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - if hasattr(run_result, "stream_events"): - original_stream_events = run_result.stream_events - - @wraps(original_stream_events) - async def wrapped_stream_events( - *stream_args: "Any", **stream_kwargs: "Any" - ) -> "AsyncIterator[Any]": - try: - async for event in original_stream_events( - *stream_args, **stream_kwargs - ): - yield event - finally: - _close_workflow_span() - - run_result.stream_events = wrapped_stream_events - - if hasattr(run_result, "cancel"): - original_cancel = run_result.cancel - - @wraps(original_cancel) - def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": - try: - return original_cancel(*cancel_args, **cancel_kwargs) - finally: - _close_workflow_span() - - run_result.cancel = wrapped_cancel - - return run_result - - return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py deleted file mode 100644 index bd13b9d61a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/patches/tools.py +++ /dev/null @@ -1,70 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import execute_tool_span, update_execute_tool_span - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -async def _get_all_tools( - original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", -) -> "list[agents.Tool]": - """ - Responsible for creating and managing `gen_ai.execute_tool` spans. - """ - # Get the original tools - tools = await original_get_all_tools(agent, context_wrapper) - - wrapped_tools = [] - for tool in tools: - # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": - wrapped_tools.append(tool) - continue - - # Create a new FunctionTool with our wrapped invoke method - original_on_invoke = tool.on_invoke_tool - - def create_wrapped_invoke( - current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" - ) -> "Callable[..., Any]": - @wraps(current_on_invoke) - async def sentry_wrapped_on_invoke_tool( - *args: "Any", **kwargs: "Any" - ) -> "Any": - with execute_tool_span(current_tool, *args, **kwargs) as span: - # We can not capture exceptions in tool execution here because - # `_on_invoke_tool` is swallowing the exception here: - # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 - # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter - # I was unable to monkey patch it because those are evaluated at module import time - # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` - # because it is nested inside this import time code. As if they made it hard to patch on purpose... - result = await current_on_invoke(*args, **kwargs) - update_execute_tool_span(span, agent, current_tool, result) - - return result - - return sentry_wrapped_on_invoke_tool - - wrapped_tool = agents.FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), - strict_json_schema=tool.strict_json_schema, - is_enabled=tool.is_enabled, - ) - wrapped_tools.append(wrapped_tool) - - return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py deleted file mode 100644 index 08802a87a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .agent_workflow import agent_workflow_span # noqa: F401 -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .handoff import handoff_span # noqa: F401 -from .invoke_agent import ( - invoke_agent_span, # noqa: F401 - update_invoke_agent_span, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py deleted file mode 100644 index 758f06db8d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - from typing import Union - - import agents - - -def agent_workflow_span( - agent: "agents.Agent", -) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": - # Create a transaction or a span if an transaction is already active - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} - ) - - return span - - span = get_start_span_function()( - name=f"{agent.name} workflow", - origin=SPAN_ORIGIN, - ) - - return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py deleted file mode 100644 index f4f02cb674..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_input_data, - _set_output_data, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from agents import Agent - - -def ai_client_span( - agent: "Agent", get_response_kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - # TODO-anton: implement other types of operations. Now "chat" is hardcoded. - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - _set_agent_data(span, agent) - _set_input_data(span, get_response_kwargs) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - response: "Any", - response_model: "Optional[str]" = None, - agent: "Optional[Agent]" = None, -) -> None: - """Update AI client span with response data (works for streaming and non-streaming).""" - if hasattr(response, "usage") and response.usage: - _set_usage_data(span, response.usage) - - if hasattr(response, "output") and response.output: - _set_output_data(span, response) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - elif hasattr(response, "model") and response.model: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) - - # Set conversation ID from agent if available - if agent: - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py deleted file mode 100644 index fd3a430951..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def execute_tool_span( - tool: "agents.Tool", *args: "Any", **kwargs: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool.name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool.name, - SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool.name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - - set_on_span = span.set_data - - if should_send_default_pii(): - input = args[1] - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - agent: "agents.Agent", - tool: "agents.Tool", - result: "Any", -) -> None: - _set_agent_data(span, agent) - - if isinstance(result, str) and result.startswith( - "An error occurred while running the tool" - ): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if should_send_default_pii(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py deleted file mode 100644 index ea91464afb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - import agents - - -def handoff_span( - context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str -) -> None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"handoff from {from_agent.name} to {to_agent_name}", - attributes={ - "sentry.op": OP.GEN_AI_HANDOFF, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "handoff", - }, - ) as span: - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_HANDOFF, - name=f"handoff from {from_agent.name} to {to_agent_name}", - origin=SPAN_ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") - - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py deleted file mode 100644 index c21145ac4a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _set_usage_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def invoke_agent_span( - context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {agent.name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - start_span_function = get_start_span_function() - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent.name}", - origin=SPAN_ORIGIN, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - if should_send_default_pii(): - messages = [] - if agent.instructions: - message = ( - agent.instructions - if isinstance(agent.instructions, str) - else safe_serialize(agent.instructions) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "system", - } - ) - - original_input = kwargs.get("original_input") - if original_input is not None: - message = ( - original_input - if isinstance(original_input, str) - else safe_serialize(original_input) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "user", - } - ) - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - _set_agent_data(span, agent) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - context: "agents.RunContextWrapper", - agent: "agents.Agent", - output: "Any" = None, -) -> None: - # Add aggregated usage data from context_wrapper - if hasattr(context, "usage"): - _set_usage_data(span, context.usage) - - if should_send_default_pii(): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py deleted file mode 100644 index 224a5f66ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openai_agents/utils.py +++ /dev/null @@ -1,244 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai._openai_completions_api import _transform_system_instructions -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions, - _is_system_instruction, -) -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - normalize_message_role, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import should_truncate_gen_ai_input -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Union - - from agents import TResponseInputItem, Usage - - from sentry_sdk._types import TextPart - -try: - import agents - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai_agents", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span( - SPANDATA.GEN_AI_SYSTEM, "openai" - ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. - - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) - - if agent.model_settings.max_tokens: - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) - - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - if agent.model_settings.presence_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - agent.model_settings.presence_penalty, - ) - - if agent.model_settings.temperature: - set_on_span( - SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature - ) - - if agent.model_settings.top_p: - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) - - if agent.model_settings.frequency_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - agent.model_settings.frequency_penalty, - ) - - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage.input_tokens_details.cached_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage.output_tokens_details.reasoning_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) - - -def _set_input_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - get_response_kwargs: "dict[str, Any]", -) -> None: - if not should_send_default_pii(): - return - request_messages = [] - - messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) - - instructions_text_parts: "list[TextPart]" = [] - explicit_instructions = get_response_kwargs.get("system_instructions") - if explicit_instructions is not None: - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions(messages) - - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - non_system_messages = [ - message for message in messages if not _is_system_instruction(message) - ] - for message in non_system_messages: - if "role" in message: - normalized_role = normalize_message_role(message.get("role")) # type: ignore - content = message.get("content") # type: ignore - request_messages.append( - { - "role": normalized_role, - "content": ( - [{"type": "text", "text": content}] - if isinstance(content, str) - else content - ), - } - ) - else: - if message.get("type") == "function_call": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, - "content": [message], - } - ) - elif message.get("type") == "function_call_output": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, - "content": [message], - } - ) - - normalized_messages = normalize_message_roles(request_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - if not should_send_default_pii(): - return - - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in result.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - else: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py deleted file mode 100644 index 281604fe38..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/openfeature.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from openfeature import api - from openfeature.hook import Hook - - if TYPE_CHECKING: - from openfeature.hook import HookContext, HookHints -except ImportError: - raise DidNotEnable("OpenFeature is not installed") - - -class OpenFeatureIntegration(Integration): - identifier = "openfeature" - - @staticmethod - def setup_once() -> None: - # Register the hook within the global openfeature hooks list. - api.add_hooks(hooks=[OpenFeatureHook()]) - - -class OpenFeatureHook(Hook): - def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: - if isinstance(details.value, bool): - add_feature_flag(details.flag_key, details.value) - - def error( - self, hook_context: "HookContext", exception: Exception, hints: "HookHints" - ) -> None: - if isinstance(hook_context.default_value, bool): - add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py deleted file mode 100644 index d76b8058ee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor - -__all__ = [ - "SentryPropagator", - "SentrySpanProcessor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py deleted file mode 100644 index d6733036ea..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable - -try: - from opentelemetry.context import create_key -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -SENTRY_TRACE_KEY = create_key("sentry-trace") -SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py deleted file mode 100644 index 472e8181f9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/integration.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -IMPORTANT: The contents of this file are part of a proof of concept and as such -are experimental and not suitable for production use. They may be changed or -removed at any time without prior notice. -""" - -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - -try: - from opentelemetry import trace - from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -try: - from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] - DjangoInstrumentor, - ) -except ImportError: - DjangoInstrumentor = None - - -CONFIGURABLE_INSTRUMENTATIONS = { - DjangoInstrumentor: {"is_sql_commentor_enabled": True}, -} - - -class OpenTelemetryIntegration(Integration): - identifier = "opentelemetry" - - @staticmethod - def setup_once() -> None: - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if has_span_streaming_enabled(options): - logger.warning( - "[OTel] OpenTelemetryIntegration is not compatible with span streaming " - "(trace_lifecycle='stream') and will be disabled." - ) - return - - warnings.warn( - "OpenTelemetryIntegration is deprecated. " - "Please use OTLPIntegration instead: " - "https://docs.sentry.io/platforms/python/integrations/otlp/", - DeprecationWarning, - stacklevel=2, - ) - - _setup_sentry_tracing() - # _setup_instrumentors() - - logger.debug("[OTel] Finished setting up OpenTelemetry integration") - - -def _setup_sentry_tracing() -> None: - provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) - trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) - - -def _setup_instrumentors() -> None: - for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): - if instrumentor is None: - continue - instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py deleted file mode 100644 index 5b5f13861d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/propagator.py +++ /dev/null @@ -1,128 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data - -try: - from opentelemetry import trace - from opentelemetry.context import ( - Context, - get_current, - set_value, - ) - from opentelemetry.propagators.textmap import ( - CarrierT, - Getter, - Setter, - TextMapPropagator, - default_getter, - default_setter, - ) - from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Set - - -class SentryPropagator(TextMapPropagator): - """ - Propagates tracing headers for Sentry's tracing system in a way OTel understands. - """ - - def extract( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - getter: "Getter[CarrierT]" = default_getter, - ) -> "Context": - if context is None: - context = get_current() - - sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) - if not sentry_trace: - return context - - sentrytrace = extract_sentrytrace_data(sentry_trace[0]) - if not sentrytrace: - return context - - context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) - - trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] - - span_context = SpanContext( - trace_id=int(trace_id, 16), # type: ignore - span_id=int(span_id, 16), # type: ignore - # we simulate a sampled trace on the otel side and leave the sampling to sentry - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - - baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) - - if baggage_header: - baggage = Baggage.from_incoming_header(baggage_header[0]) - else: - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and frozen and won't be populated as head SDK. - baggage = Baggage(sentry_items={}) - - baggage.freeze() - context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) - - span = NonRecordingSpan(span_context) - modified_context = trace.set_span_in_context(span, context) - return modified_context - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - if context is None: - context = get_current() - - current_span = trace.get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - span_id = trace.format_span_id(current_span_context.span_id) - - span_map = SentrySpanProcessor.otel_span_map - sentry_span = span_map.get(span_id, None) - if not sentry_span: - return - - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) - - if sentry_span.containing_transaction: - baggage = sentry_span.containing_transaction.get_baggage() - if baggage: - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - @property - def fields(self) -> "Set[str]": - return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py deleted file mode 100644 index 1efd2ac455..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/opentelemetry/span_processor.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from time import time -from typing import TYPE_CHECKING, cast - -from urllib3.util import parse_url as urlparse - -from sentry_sdk import get_client, start_transaction -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing import Transaction - -try: - from opentelemetry.context import get_value - from opentelemetry.sdk.trace import ReadableSpan as OTelSpan - from opentelemetry.sdk.trace import SpanProcessor - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import ( - SpanKind, - format_span_id, - format_trace_id, - get_current_span, - ) - from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from opentelemetry import context as context_api - from opentelemetry.trace import SpanContext - - from sentry_sdk._types import Event, Hint - -OPEN_TELEMETRY_CONTEXT = "otel" -SPAN_MAX_TIME_OPEN_MINUTES = 10 -SPAN_ORIGIN = "auto.otel" - - -def link_trace_context_to_error_event( - event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" -) -> "Event": - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return event - - if hasattr(event, "type") and event["type"] == "transaction": - return event - - otel_span = get_current_span() - if not otel_span: - return event - - ctx = otel_span.get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return event - - sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) - if not sentry_span: - return event - - contexts = event.setdefault("contexts", {}) - contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) - - return event - - -class SentrySpanProcessor(SpanProcessor): - """ - Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. - """ - - # The mapping from otel span ids to sentry spans - otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} - - # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES - open_spans: "dict[int, set[str]]" = {} - - initialized: "bool" = False - - def __new__(cls) -> "SentrySpanProcessor": - if not hasattr(cls, "instance"): - cls.instance = super().__new__(cls) - - # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). - return cls.instance # type: ignore[misc] - - def __init__(self) -> None: - if self.initialized: - return - - @add_global_event_processor - def global_event_processor(event: "Event", hint: "Hint") -> "Event": - return link_trace_context_to_error_event(event, self.otel_span_map) - - self.initialized = True - - def _prune_old_spans(self: "SentrySpanProcessor") -> None: - """ - Prune spans that have been open for too long. - """ - current_time_minutes = int(time() / 60) - for span_start_minutes in list( - self.open_spans.keys() - ): # making a list because we change the dict - # prune empty open spans buckets - if self.open_spans[span_start_minutes] == set(): - self.open_spans.pop(span_start_minutes) - - # prune old buckets - elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: - for span_id in self.open_spans.pop(span_start_minutes): - self.otel_span_map.pop(span_id, None) - - def on_start( - self, - otel_span: "OTelSpan", - parent_context: "Optional[context_api.Context]" = None, - ) -> None: - client = get_client() - - if not client.parsed_dsn: - return - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - if self._is_sentry_span(otel_span): - return - - trace_data = self._get_trace_data( - span_context, otel_span.parent, parent_context - ) - - parent_span_id = trace_data["parent_span_id"] - sentry_parent_span = ( - self.otel_span_map.get(parent_span_id) if parent_span_id else None - ) - - start_timestamp = None - if otel_span.start_time is not None: - start_timestamp = datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span = None - if sentry_parent_span: - sentry_span = sentry_parent_span.start_child( - span_id=trace_data["span_id"], - name=otel_span.name, - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - else: - sentry_span = start_transaction( - name=otel_span.name, - span_id=trace_data["span_id"], - parent_span_id=parent_span_id, - trace_id=trace_data["trace_id"], - baggage=trace_data["baggage"], - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - - self.otel_span_map[trace_data["span_id"]] = sentry_span - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).add( - trace_data["span_id"] - ) - - self._prune_old_spans() - - def on_end(self, otel_span: "OTelSpan") -> None: - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - span_id = format_span_id(span_context.span_id) - sentry_span = self.otel_span_map.pop(span_id, None) - if not sentry_span: - return - - sentry_span.op = otel_span.name - - self._update_span_with_otel_status(sentry_span, otel_span) - - if isinstance(sentry_span, Transaction): - sentry_span.name = otel_span.name - sentry_span.set_context( - OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) - ) - self._update_transaction_with_otel_data(sentry_span, otel_span) - - else: - self._update_span_with_otel_data(sentry_span, otel_span) - - end_timestamp = None - if otel_span.end_time is not None: - end_timestamp = datetime.fromtimestamp( - otel_span.end_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span.finish(end_timestamp=end_timestamp) - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) - - self._prune_old_spans() - - def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: - """ - Break infinite loop: - HTTP requests to Sentry are caught by OTel and send again to Sentry. - """ - otel_span_url = None - if otel_span.attributes is not None: - otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) - otel_span_url = cast("Optional[str]", otel_span_url) - - parsed_dsn = get_client().parsed_dsn - dsn_url = parsed_dsn.netloc if parsed_dsn else None - - if otel_span_url and dsn_url and dsn_url in otel_span_url: - return True - - return False - - def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": - """ - Returns the OTel context for Sentry. - See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context - """ - ctx = {} - - if otel_span.attributes: - ctx["attributes"] = dict(otel_span.attributes) - - if otel_span.resource.attributes: - ctx["resource"] = dict(otel_span.resource.attributes) - - return ctx - - def _get_trace_data( - self, - span_context: "SpanContext", - parent_span_context: "Optional[SpanContext]", - parent_context: "Optional[context_api.Context]", - ) -> "dict[str, Any]": - """ - Extracts tracing information from one OTel span's context and its parent OTel context. - """ - trace_data: "dict[str, Any]" = {} - - span_id = format_span_id(span_context.span_id) - trace_data["span_id"] = span_id - - trace_id = format_trace_id(span_context.trace_id) - trace_data["trace_id"] = trace_id - - parent_span_id = ( - format_span_id(parent_span_context.span_id) if parent_span_context else None - ) - trace_data["parent_span_id"] = parent_span_id - - sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) - sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) - trace_data["parent_sampled"] = ( - sentry_trace_data["parent_sampled"] if sentry_trace_data else None - ) - - baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) - trace_data["baggage"] = baggage - - return trace_data - - def _update_span_with_otel_status( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Set the Sentry span status from the OTel span - """ - if otel_span.status.is_unset: - return - - if otel_span.status.is_ok: - sentry_span.set_status(SPANSTATUS.OK) - return - - sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - def _update_span_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Convert OTel span data and update the Sentry span with it. - This should eventually happen on the server when ingesting the spans. - """ - sentry_span.set_data("otel.kind", otel_span.kind) - - op = otel_span.name - description = otel_span.name - - if otel_span.attributes is not None: - for key, val in otel_span.attributes.items(): - sentry_span.set_data(key, val) - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - http_method = cast("Optional[str]", http_method) - - db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) - - if http_method: - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - description = http_method - - peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) - if peer_name: - description += " {}".format(peer_name) - - target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) - if target: - description += " {}".format(target) - - if not peer_name and not target: - url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) - url = cast("Optional[str]", url) - if url: - parsed_url = urlparse(url) - url = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path - ) - description += " {}".format(url) - - status_code = otel_span.attributes.get( - SpanAttributes.HTTP_STATUS_CODE, None - ) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - elif db_query: - op = "db" - statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) - statement = cast("Optional[str]", statement) - if statement: - description = statement - - sentry_span.op = op - sentry_span.description = description - - def _update_transaction_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - if otel_span.attributes is None: - return - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - - if http_method: - status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py deleted file mode 100644 index b91f2cfd21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/otlp.py +++ /dev/null @@ -1,223 +0,0 @@ -from sentry_sdk import capture_event, get_client -from sentry_sdk.consts import VERSION, EndpointType -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import register_external_propagation_context -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - event_from_exception, - logger, -) - -try: - from opentelemetry.context import ( - Context, - get_current, - get_value, - ) - from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter - from opentelemetry.propagate import set_global_textmap - from opentelemetry.propagators.textmap import ( - CarrierT, - Setter, - default_setter, - ) - from opentelemetry.sdk.trace import Span, TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - from opentelemetry.trace import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - SpanContext, - format_span_id, - format_trace_id, - get_current_span, - get_tracer_provider, - set_tracer_provider, - ) - - from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY - from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -except ImportError: - raise DidNotEnable("opentelemetry-distro[otlp] is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple - - -def otel_propagation_context() -> "Optional[Tuple[str, str]]": - """ - Get the (trace_id, span_id) from opentelemetry if exists. - """ - ctx = get_current_span().get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return None - - return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) - - -def setup_otlp_traces_exporter( - dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None -) -> None: - tracer_provider = get_tracer_provider() - - if not isinstance(tracer_provider, TracerProvider): - logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") - tracer_provider = TracerProvider() - set_tracer_provider(tracer_provider) - - endpoint = None - headers = None - if collector_url: - endpoint = collector_url - logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") - elif dsn: - auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") - endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) - headers = {"X-Sentry-Auth": auth.to_header()} - logger.debug(f"[OTLP] Sending traces to {endpoint}") - - otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) - span_processor = BatchSpanProcessor(otlp_exporter) - tracer_provider.add_span_processor(span_processor) - - -_sentry_patched_exception = False - - -def setup_capture_exceptions() -> None: - """ - Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. - """ - global _sentry_patched_exception - _original_record_exception = Span.record_exception - - if _sentry_patched_exception: - return - - def _sentry_patched_record_exception( - self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration and otlp_integration.capture_exceptions: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=get_client().options, - mechanism={"type": OTLPIntegration.identifier, "handled": False}, - ) - capture_event(event, hint=hint) - - _original_record_exception(self, exception, *args, **kwargs) - - Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] - _sentry_patched_exception = True - - -class SentryOTLPPropagator(SentryPropagator): - """ - We need to override the inject of the older propagator since that - is SpanProcessor based. - - !!! Note regarding baggage: - We cannot meaningfully populate a new baggage as a head SDK - when we are using OTLP since we don't have any sort of transaction semantic to - track state across a group of spans. - - For incoming baggage, we just pass it on as is so that case is correctly handled. - """ - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration is None: - return - - if context is None: - context = get_current() - - current_span = get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - sentry_trace = _to_traceparent(current_span_context) - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) - - baggage = get_value(SENTRY_BAGGAGE_KEY, context) - if baggage is not None and isinstance(baggage, Baggage): - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - -def _to_traceparent(span_context: "SpanContext") -> str: - """ - Helper method to generate the sentry-trace header. - """ - span_id = format_span_id(span_context.span_id) - trace_id = format_trace_id(span_context.trace_id) - sampled = span_context.trace_flags.sampled - - return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" - - -class OTLPIntegration(Integration): - """ - Automatically setup OTLP ingestion from the DSN. - - :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. - Set to False to setup the TracerProvider manually. - :param collector_url: URL of your own OpenTelemetry collector, defaults to None. - When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. - :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. - Set to False to configure propagators manually or to disable propagation. - :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. - Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. - """ - - identifier = "otlp" - - def __init__( - self, - setup_otlp_traces_exporter: bool = True, - collector_url: "Optional[str]" = None, - setup_propagator: bool = True, - capture_exceptions: bool = False, - ) -> None: - self.setup_otlp_traces_exporter = setup_otlp_traces_exporter - self.collector_url = collector_url - self.setup_propagator = setup_propagator - self.capture_exceptions = capture_exceptions - - @staticmethod - def setup_once() -> None: - logger.debug("[OTLP] Setting up trace linking for all events") - register_external_propagation_context(otel_propagation_context) - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if self.setup_otlp_traces_exporter: - logger.debug("[OTLP] Setting up OTLP exporter") - dsn: "Optional[str]" = options.get("dsn") if options else None - setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) - - if self.setup_propagator: - logger.debug("[OTLP] Setting up propagator for distributed tracing") - # TODO-neel better propagator support, chain with existing ones if possible instead of replacing - set_global_textmap(SentryOTLPPropagator()) - - setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py deleted file mode 100644 index 569e3e1fa5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pure_eval.py +++ /dev/null @@ -1,134 +0,0 @@ -import ast -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import serializer -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, List, Optional, Tuple - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - -try: - from pure_eval import Evaluator -except ImportError: - raise DidNotEnable("pure_eval is not installed") - -try: - # Used implicitly, just testing it's available - import asttokens # noqa -except ImportError: - raise DidNotEnable("asttokens is not installed") - - -class PureEvalIntegration(Integration): - identifier = "pure_eval" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - sentry_frame["vars"] = ( - pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] - ) - return event - - -def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": - source = Source.for_frame(frame) - if not source.tree: - return {} - - statements = source.statements_at_line(frame.f_lineno) - if not statements: - return {} - - scope = stmt = list(statements)[0] - while True: - # Get the parent first in case the original statement is already - # a function definition, e.g. if we're calling a decorator - # In that case we still want the surrounding scope, not that function - scope = scope.parent - if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): - break - - evaluator = Evaluator.from_frame(frame) - expressions = evaluator.interesting_expressions_grouped(scope) - - def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": - # Prioritise expressions with a node closer to the statement executed - # without being after that statement - # A higher return value is better - the expression will appear - # earlier in the list of values and is less likely to be trimmed - nodes, _value = expression - - def start(n: "ast.expr") -> "Tuple[int, int]": - return (n.lineno, n.col_offset) - - nodes_before_stmt = [ - node for node in nodes if start(node) < stmt.last_token.end - ] - if nodes_before_stmt: - # The position of the last node before or in the statement - return max(start(node) for node in nodes_before_stmt) - else: - # The position of the first node after the statement - # Negative means it's always lower priority than nodes that come before - # Less negative means closer to the statement and higher priority - lineno, col_offset = min(start(node) for node in nodes) - return (-lineno, -col_offset) - - # This adds the first_token and last_token attributes to nodes - atok = source.asttokens() - - expressions.sort(key=closeness, reverse=True) - vars = { - atok.get_text(nodes[0]): value - for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] - } - return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py deleted file mode 100644 index 81e7cf8090..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/__init__.py +++ /dev/null @@ -1,187 +0,0 @@ -import functools - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import capture_internal_exceptions, parse_version - -try: - import pydantic_ai # type: ignore # noqa: F401 - from pydantic_ai import Agent -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -from importlib.metadata import PackageNotFoundError, version -from typing import TYPE_CHECKING - -from .patches import ( - _patch_agent_run, - _patch_graph_nodes, - _patch_tool_execution, -) -from .spans.ai_client import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any - - from pydantic_ai import ModelRequestContext, RunContext - from pydantic_ai.capabilities import Hooks # type: ignore - from pydantic_ai.messages import ModelResponse # type: ignore - - -def register_hooks(hooks: "Hooks") -> None: - """ - Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. - """ - - @hooks.on.before_model_request # type: ignore - async def on_request( - ctx: "RunContext[None]", request_context: "ModelRequestContext" - ) -> "ModelRequestContext": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return request_context - - span = ai_client_span( - messages=request_context.messages, - agent=None, - model=request_context.model, - model_settings=request_context.model_settings, - ) - - run_context_metadata["_sentry_span"] = span - span.__enter__() - - return request_context - - @hooks.on.after_model_request # type: ignore - async def on_response( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - response: "ModelResponse", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return response - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - return response - - update_ai_client_span(span, response) - span.__exit__(None, None, None) - - return response - - @hooks.on.model_request_error # type: ignore - async def on_error( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - error: "Exception", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - - if not isinstance(run_context_metadata, dict): - raise error - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - raise error - - with capture_internal_exceptions(): - span.__exit__(type(error), error, error.__traceback__) - - raise error - - original_init = Agent.__init__ - - @functools.wraps(original_init) - def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: - caps = list(kwargs.get("capabilities") or []) - caps.append(hooks) - kwargs["capabilities"] = caps - - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {} # Used as shared reference between hooks - - return original_init(self, *args, **kwargs) - - Agent.__init__ = patched_init - - -class PydanticAIIntegration(Integration): - """ - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. - 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. - - Each run invocation has `RunContext` objects that are passed to the library hooks. - 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. - Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). - The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. - - The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that - instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. - """ - - identifier = "pydantic_ai" - origin = f"auto.ai.{identifier}" - using_request_hooks = False - - def __init__( - self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True - ) -> None: - """ - Initialize the Pydantic AI integration. - - Args: - include_prompts: Whether to include prompts and messages in span data. - Requires send_default_pii=True. Defaults to True. - handled_tool_exceptions: Capture tool call exceptions that Pydantic AI - internally prevents from bubbling up. - """ - self.include_prompts = include_prompts - self.handled_tool_call_exceptions = handled_tool_call_exceptions - - @staticmethod - def setup_once() -> None: - """ - Set up the pydantic-ai integration. - - This patches the key methods in pydantic-ai to create Sentry spans for: - - Agent invocations (Agent.run methods) - - Model requests (AI client calls) - - Tool executions - """ - _patch_agent_run() - _patch_tool_execution() - - PydanticAIIntegration.using_request_hooks = False - try: - PYDANTIC_AI_VERSION = version("pydantic-ai-slim") - except PackageNotFoundError: - return - - PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) - if PYDANTIC_AI_VERSION is None: - return - - # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c - if PYDANTIC_AI_VERSION < ( - 1, - 73, - ): - _patch_graph_nodes() - return - - try: - from pydantic_ai.capabilities import Hooks - except ImportError: - return - - PydanticAIIntegration.using_request_hooks = True - hooks = Hooks() - register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py deleted file mode 100644 index afa66dc47d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py deleted file mode 100644 index d0ea6242b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .agent_run import _patch_agent_run # noqa: F401 -from .graph_nodes import _patch_graph_nodes # noqa: F401 -from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py deleted file mode 100644 index 3039e40698..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ /dev/null @@ -1,198 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import invoke_agent_span, update_invoke_agent_span -from ..utils import _capture_exception, pop_agent, push_agent - -try: - from pydantic_ai.agent import Agent # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - -class _StreamingContextManagerWrapper: - """Wrapper for streaming methods that return async context managers.""" - - def __init__( - self, - agent: "Any", - original_ctx_manager: "Any", - user_prompt: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = True, - ) -> None: - self.agent = agent - self.original_ctx_manager = original_ctx_manager - self.user_prompt = user_prompt - self.model = model - self.model_settings = model_settings - self.is_streaming = is_streaming - self._isolation_scope: "Any" = None - self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None - self._result: "Any" = None - - async def __aenter__(self) -> "Any": - # Set up isolation scope and invoke_agent span - self._isolation_scope = sentry_sdk.isolation_scope() - self._isolation_scope.__enter__() - - # Create invoke_agent span (will be closed in __aexit__) - self._span = invoke_agent_span( - self.user_prompt, - self.agent, - self.model, - self.model_settings, - self.is_streaming, - ) - self._span.__enter__() - - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur - push_agent(self.agent, self.is_streaming) - - # Enter the original context manager - result = await self.original_ctx_manager.__aenter__() - self._result = result - return result - - async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: - try: - # Exit the original context manager first - await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) - - # Update span with result if successful - if exc_type is None and self._result and self._span is not None: - update_invoke_agent_span(self._span, self._result) - finally: - # Pop agent from contextvar stack - pop_agent() - - # Clean up invoke span - if self._span: - self._span.__exit__(exc_type, exc_val, exc_tb) - - # Clean up isolation scope - if self._isolation_scope: - self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) - - -def _create_run_wrapper( - original_func: "Callable[..., Any]", is_streaming: bool = False -) -> "Callable[..., Any]": - """ - Wraps the Agent.run method to create an invoke_agent span. - - Args: - original_func: The original run method - is_streaming: Whether this is a streaming method (for future use) - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Create invoke_agent span - with invoke_agent_span( - user_prompt, self, model, model_settings, is_streaming - ) as span: - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in finally even if exceptions occur - push_agent(self, is_streaming) - - try: - result = await original_func(self, *args, **kwargs) - - # Update span with result - update_invoke_agent_span(span, result) - - return result - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - finally: - # Pop agent from contextvar stack - pop_agent() - - return wrapper - - -def _create_streaming_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps run_stream method that returns an async context manager. - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Call original function to get the context manager - original_ctx_manager = original_func(self, *args, **kwargs) - - # Wrap it with our instrumentation - return _StreamingContextManagerWrapper( - agent=self, - original_ctx_manager=original_ctx_manager, - user_prompt=user_prompt, - model=model, - model_settings=model_settings, - is_streaming=True, - ) - - return wrapper - - -def _patch_agent_run() -> None: - """ - Patches the Agent run methods to create spans for agent execution. - - This patches both non-streaming (run, run_sync) and streaming - (run_stream, run_stream_events) methods. - """ - - # Store original methods - original_run = Agent.run - original_run_stream = Agent.run_stream - - # Wrap and apply patches for non-streaming methods - Agent.run = _create_run_wrapper(original_run, is_streaming=False) - - # Wrap and apply patches for streaming methods - Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py deleted file mode 100644 index afb10395f4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +++ /dev/null @@ -1,106 +0,0 @@ -from contextlib import asynccontextmanager -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import ( - ai_client_span, - update_ai_client_span, -) - -try: - from pydantic_ai._agent_graph import ModelRequestNode # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - -def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": - """Extract common data needed for creating chat spans. - - Returns: - Tuple of (messages, model, model_settings) - """ - # Extract model and settings from context - model = None - model_settings = None - if hasattr(ctx, "deps"): - model = getattr(ctx.deps, "model", None) - model_settings = getattr(ctx.deps, "model_settings", None) - - # Build full message list: history + current request - messages = [] - if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): - messages.extend(ctx.state.message_history) - - current_request = getattr(node, "request", None) - if current_request: - messages.append(current_request) - - return messages, model, model_settings - - -def _patch_graph_nodes() -> None: - """ - Patches the graph node execution to create appropriate spans. - - ModelRequestNode -> Creates ai_client span for model requests - CallToolsNode -> Handles tool calls (spans created in tool patching) - """ - - # Patch ModelRequestNode to create ai_client spans - original_model_request_run = ModelRequestNode.run - - @wraps(original_model_request_run) - async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - with ai_client_span(messages, None, model, model_settings) as span: - result = await original_model_request_run(self, ctx) - - # Extract response from result if available - model_response = None - if hasattr(result, "model_response"): - model_response = result.model_response - - update_ai_client_span(span, model_response) - return result - - ModelRequestNode.run = wrapped_model_request_run - - # Patch ModelRequestNode.stream for streaming requests - original_model_request_stream = ModelRequestNode.stream - - def create_wrapped_stream( - original_stream_method: "Callable[..., Any]", - ) -> "Callable[..., Any]": - """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" - - @asynccontextmanager - @wraps(original_stream_method) - async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - # Create chat span for streaming request - with ai_client_span(messages, None, model, model_settings) as span: - # Call the original stream method - async with original_stream_method(self, ctx) as stream: - yield stream - - # After streaming completes, update span with response data - # The ModelRequestNode stores the final response in _result - model_response = None - if hasattr(self, "_result") and self._result is not None: - # _result is a NextNode containing the model_response - if hasattr(self._result, "model_response"): - model_response = self._result.model_response - - update_ai_client_span(span, model_response) - - return wrapped_model_request_stream - - ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py deleted file mode 100644 index 5646b5d47c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ /dev/null @@ -1,177 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import execute_tool_span, update_execute_tool_span -from ..utils import _capture_exception, get_current_agent - -if TYPE_CHECKING: - from typing import Any - -try: - try: - from pydantic_ai.tool_manager import ToolManager # type: ignore - except ImportError: - from pydantic_ai._tool_manager import ToolManager # type: ignore - - from pydantic_ai.exceptions import ToolRetryError # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -def _patch_tool_execution() -> None: - if hasattr(ToolManager, "execute_tool_call"): - _patch_execute_tool_call() - - elif hasattr(ToolManager, "_call_tool"): - # older versions - _patch_call_tool() - - -def _patch_execute_tool_call() -> None: - original_execute_tool_call = ToolManager.execute_tool_call - - @wraps(original_execute_tool_call) - async def wrapped_execute_tool_call( - self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not validated or not hasattr(validated, "call"): - return await original_execute_tool_call(self, validated, *args, **kwargs) - - # Extract tool info before calling original - call = validated.call - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_execute_tool_call( - self, - validated, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - return await original_execute_tool_call(self, validated, *args, **kwargs) - - ToolManager.execute_tool_call = wrapped_execute_tool_call - - -def _patch_call_tool() -> None: - """ - Patch ToolManager._call_tool to create execute_tool spans. - - This is the single point where ALL tool calls flow through in pydantic_ai, - regardless of toolset type (function, MCP, combined, wrapper, etc.). - - By patching here, we avoid: - - Patching multiple toolset classes - - Dealing with signature mismatches from instrumented MCP servers - - Complex nested toolset handling - """ - original_call_tool = ToolManager._call_tool - - @wraps(original_call_tool) - async def wrapped_call_tool( - self: "Any", call: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Extract tool info before calling original - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_call_tool( - self, - call, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - # No span context - just call original - return await original_call_tool( - self, - call, - *args, - **kwargs, - ) - - ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py deleted file mode 100644 index 574046d645..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py deleted file mode 100644 index 27deb0c55c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ /dev/null @@ -1,331 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _get_model_name, - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, - get_current_agent, - get_is_streaming, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Dict, List, Union - - from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore - - from sentry_sdk._types import TextPart as SentryTextPart - -try: - from pydantic_ai.messages import ( - BaseToolCallPart, - BaseToolReturnPart, - BinaryContent, - ImageUrl, - SystemPromptPart, - TextPart, - ThinkingPart, - UserPromptPart, - ) -except ImportError: - # Fallback if these classes are not available - BaseToolCallPart = None - BaseToolReturnPart = None - SystemPromptPart = None - UserPromptPart = None - TextPart = None - ThinkingPart = None - BinaryContent = None - ImageUrl = None - - -def _transform_system_instructions( - permanent_instructions: "list[SystemPromptPart]", - current_instructions: "list[str]", -) -> "list[SentryTextPart]": - text_parts: "list[SentryTextPart]" = [ - { - "type": "text", - "content": instruction.content, - } - for instruction in permanent_instructions - ] - - text_parts.extend( - { - "type": "text", - "content": instruction, - } - for instruction in current_instructions - ) - - return text_parts - - -def _get_system_instructions( - messages: "list[ModelMessage]", -) -> "tuple[list[SystemPromptPart], list[str]]": - permanent_instructions = [] - current_instructions = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - if SystemPromptPart and isinstance(part, SystemPromptPart): - permanent_instructions.append(part) - - if hasattr(msg, "instructions") and msg.instructions is not None: - current_instructions.append(msg.instructions) - - return permanent_instructions, current_instructions - - -def _set_input_messages( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" -) -> None: - """Set input messages data on a span.""" - if not _should_send_prompts(): - return - - if not messages: - return - - permanent_instructions, current_instructions = _get_system_instructions(messages) - if len(permanent_instructions) > 0 or len(current_instructions) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - - try: - formatted_messages = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - role = "user" - # Use isinstance checks with proper base classes - if SystemPromptPart and isinstance(part, SystemPromptPart): - continue - elif ( - (TextPart and isinstance(part, TextPart)) - or (ThinkingPart and isinstance(part, ThinkingPart)) - or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) - ): - role = "assistant" - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - role = "tool" - - content: "List[Dict[str, Any] | str]" = [] - tool_calls = None - tool_call_id = None - - # Handle ToolCallPart (assistant requesting tool use) - if BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = {} - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - if tool_call_data: - tool_calls = [tool_call_data] - # Handle ToolReturnPart (tool result) - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - if hasattr(part, "tool_name"): - tool_call_id = part.tool_name - if hasattr(part, "content"): - content.append({"type": "text", "text": str(part.content)}) - # Handle regular content - elif hasattr(part, "content"): - if isinstance(part.content, str): - content.append({"type": "text", "text": part.content}) - elif isinstance(part.content, list): - for item in part.content: - if isinstance(item, str): - content.append({"type": "text", "text": item}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - else: - content.append(safe_serialize(item)) - else: - content.append({"type": "text", "text": str(part.content)}) - # Add message if we have content or tool calls - if content or tool_calls: - message: "Dict[str, Any]" = {"role": role} - if content: - message["content"] = content - if tool_calls: - message["tool_calls"] = tool_calls - if tool_call_id: - message["tool_call_id"] = tool_call_id - formatted_messages.append(message) - - if formatted_messages: - normalized_messages = normalize_message_roles(formatted_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - except Exception: - # If we fail to format messages, just skip it - pass - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" -) -> None: - """Set output data on a span.""" - if not _should_send_prompts(): - return - - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - - try: - # Extract text from ModelResponse - if hasattr(response, "parts"): - texts = [] - tool_calls = [] - - for part in response.parts: - if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): - texts.append(part.content) - elif BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = { - "type": "function", - } - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - tool_calls.append(tool_call_data) - - if texts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) - - if tool_calls: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) - ) - - except Exception: - # If we fail to format output, just skip it - pass - - -def ai_client_span( - messages: "Any", agent: "Any", model: "Any", model_settings: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for an AI client call (model request). - - Args: - messages: Full conversation history (list of messages) - agent: Agent object - model: Model object - model_settings: Model settings - """ - # Determine model name for span name - model_obj = model - if agent and hasattr(agent, "model"): - model_obj = agent.model - - model_name = _get_model_name(model_obj) or "unknown" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - # Set streaming flag from contextvar - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - - # Add available tools if agent is available - agent_obj = agent or get_current_agent() - _set_available_tools(span, agent_obj) - - # Set input messages (full conversation history) - if messages: - _set_input_messages(span, messages) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" -) -> None: - """Update the AI client span with response data.""" - if not span: - return - - # Set usage data if available - if model_response and hasattr(model_response, "usage"): - _set_usage_data(span, model_response.usage) - - # Set output data - _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py deleted file mode 100644 index 7648c1418a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _should_send_prompts - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from pydantic_ai._tool_manager import ToolDefinition # type: ignore - - -def execute_tool_span( - tool_name: str, - tool_args: "Any", - agent: "Any", - tool_definition: "Optional[ToolDefinition]" = None, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for tool execution. - - Args: - tool_name: The name of the tool being executed - tool_args: The arguments passed to the tool - agent: The agent executing the tool - tool_definition: The definition of the tool, if available - """ - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - set_on_span = span.set_data - - if tool_definition is not None and hasattr(tool_definition, "description"): - set_on_span( - SPANDATA.GEN_AI_TOOL_DESCRIPTION, - tool_definition.description, - ) - - _set_agent_data(span, agent) - - if _should_send_prompts() and tool_args is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - """Update the execute tool span with the result.""" - if not span: - return - - if not _should_send_prompts() or result is None: - return - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - else: - span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py deleted file mode 100644 index f0c68e85ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ /dev/null @@ -1,184 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, -) - -if TYPE_CHECKING: - from typing import Any, Union - -try: - from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore -except ImportError: - BinaryContent = None - ImageUrl = None - - -def invoke_agent_span( - user_prompt: "Any", - agent: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = False, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for invoking the agent.""" - # Determine agent name for span - name = "agent" - if agent and getattr(agent, "name", None): - name = agent.name - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - _set_available_tools(span, agent) - - # Add user prompt and system prompts if available and prompts are enabled - if _should_send_prompts(): - messages = [] - - # Add system prompts (both instructions and system_prompt) - system_texts = [] - - if agent: - # Check for system_prompt - system_prompts = getattr(agent, "_system_prompts", None) or [] - for prompt in system_prompts: - if isinstance(prompt, str): - system_texts.append(prompt) - - # Check for instructions (stored in _instructions) - instructions = getattr(agent, "_instructions", None) - if instructions: - if isinstance(instructions, str): - system_texts.append(instructions) - elif isinstance(instructions, (list, tuple)): - for instr in instructions: - if isinstance(instr, str): - system_texts.append(instr) - elif callable(instr): - # Skip dynamic/callable instructions - pass - - # Add all system texts as system messages - for system_text in system_texts: - messages.append( - { - "content": [{"text": system_text, "type": "text"}], - "role": "system", - } - ) - - # Add user prompt - if user_prompt: - if isinstance(user_prompt, str): - messages.append( - { - "content": [{"text": user_prompt, "type": "text"}], - "role": "user", - } - ) - elif isinstance(user_prompt, list): - # Handle list of user content - content = [] - for item in user_prompt: - if isinstance(item, str): - content.append({"text": item, "type": "text"}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - if content: - messages.append( - { - "content": content, - "role": "user", - } - ) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - result: "Any", -) -> None: - """Update and close the invoke agent span.""" - if not span or not result: - return - - # Extract output from result - output = getattr(result, "output", None) - - # Set response text if prompts are enabled - if _should_send_prompts() and output: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False - ) - - # Set model name from response if available - if hasattr(result, "response"): - try: - response = result.response - if hasattr(response, "model_name") and response.model_name: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - except Exception: - # If response access fails, continue without setting model name - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py deleted file mode 100644 index 330496c6b2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/spans/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Utility functions for PydanticAI span instrumentation.""" - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX -from sentry_sdk.ai.utils import get_modality_from_mime_type -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore - - -def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": - """Serialize an ImageUrl content item for span data. - - For data URLs containing base64-encoded images, the content is redacted. - For regular HTTP URLs, the URL string is preserved. - """ - url = str(item.url) - data_url_match = DATA_URL_BASE64_REGEX.match(url) - - if data_url_match: - return { - "type": "image", - "content": BLOB_DATA_SUBSTITUTE, - } - - return { - "type": "image", - "content": url, - } - - -def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": - """Serialize a BinaryContent item for span data, redacting the blob data.""" - return { - "type": "blob", - "modality": get_modality_from_mime_type(item.media_type), - "mime_type": item.media_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - usage: "Union[RequestUsage, RunUsage]", -) -> None: - """Set token usage data on a span. - - This function works with both RequestUsage (single request) and - RunUsage (agent run) objects from pydantic_ai. - - Args: - span: The Sentry span to set data on. - usage: RequestUsage or RunUsage object containing token usage information. - """ - if usage is None: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if hasattr(usage, "input_tokens") and usage.input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - - # Pydantic AI uses cache_read_tokens (not input_tokens_cached) - if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) - - # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) - if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - usage.cache_write_tokens, - ) - - if hasattr(usage, "output_tokens") and usage.output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - - if hasattr(usage, "total_tokens") and usage.total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py deleted file mode 100644 index 340dcf8953..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pydantic_ai/utils.py +++ /dev/null @@ -1,233 +0,0 @@ -from contextvars import ContextVar -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - -# Store the current agent context in a contextvar for re-entrant safety -# Using a list as a stack to support nested agent calls -_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( - "pydantic_ai_agent_context_stack", default=[] -) - - -def push_agent(agent: "Any", is_streaming: bool = False) -> None: - """Push an agent context onto the stack along with its streaming flag.""" - stack = _agent_context_stack.get().copy() - stack.append({"agent": agent, "is_streaming": is_streaming}) - _agent_context_stack.set(stack) - - -def pop_agent() -> None: - """Pop an agent context from the stack.""" - stack = _agent_context_stack.get().copy() - if stack: - stack.pop() - _agent_context_stack.set(stack) - - -def get_current_agent() -> "Any": - """Get the current agent from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1]["agent"] - return None - - -def get_is_streaming() -> bool: - """Get the streaming flag from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1].get("is_streaming", False) - return False - - -def _should_send_prompts() -> bool: - """ - Check if prompts should be sent to Sentry. - - This checks both send_default_pii and the include_prompts integration setting. - """ - if not should_send_default_pii(): - return False - - from . import PydanticAIIntegration - - # Get the integration instance from the client - integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) - - if integration is None: - return False - - return getattr(integration, "include_prompts", False) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set agent-related data on a span. - - Args: - span: The span to set data on - agent: Agent object (can be None, will try to get from contextvar if not provided) - """ - # Extract agent name from agent object or contextvar - agent_obj = agent - if not agent_obj: - # Try to get from contextvar - agent_obj = get_current_agent() - - if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - else: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - - -def _get_model_name(model_obj: "Any") -> "Optional[str]": - """Extract model name from a model object. - - Args: - model_obj: Model object to extract name from - - Returns: - Model name string or None if not found - """ - if not model_obj: - return None - - if hasattr(model_obj, "model_name"): - return model_obj.model_name - elif hasattr(model_obj, "name"): - try: - return model_obj.name() - except Exception: - return str(model_obj) - elif isinstance(model_obj, str): - return model_obj - else: - return str(model_obj) - - -def _set_model_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - model: "Any", - model_settings: "Any", -) -> None: - """Set model-related data on a span. - - Args: - span: The span to set data on - model: Model object (can be None, will try to get from agent if not provided) - model_settings: Model settings (can be None, will try to get from agent if not provided) - """ - # Try to get agent from contextvar if we need it - agent_obj = get_current_agent() - - # Extract model information - model_obj = model - if not model_obj and agent_obj and hasattr(agent_obj, "model"): - model_obj = agent_obj.model - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if model_obj: - # Set system from model - if hasattr(model_obj, "system"): - set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) - - # Set model name - model_name = _get_model_name(model_obj) - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Extract model settings - settings = model_settings - if not settings and agent_obj and hasattr(agent_obj, "model_settings"): - settings = agent_obj.model_settings - - if settings: - settings_map = { - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - } - - # ModelSettings is a TypedDict (dict at runtime), so use dict access - if isinstance(settings, dict): - for setting_name, spandata_key in settings_map.items(): - value = settings.get(setting_name) - if value is not None: - set_on_span(spandata_key, value) - else: - # Fallback for object-style settings - for setting_name, spandata_key in settings_map.items(): - if hasattr(settings, setting_name): - value = getattr(settings, setting_name) - if value is not None: - set_on_span(spandata_key, value) - - -def _set_available_tools( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set available tools data on a span from an agent's function toolset. - - Args: - span: The span to set data on - agent: Agent object with _function_toolset attribute - """ - if not agent or not hasattr(agent, "_function_toolset"): - return - - try: - tools = [] - # Get tools from the function toolset - if hasattr(agent._function_toolset, "tools"): - for tool_name, tool in agent._function_toolset.tools.items(): - tool_info = {"name": tool_name} - - # Add description from function_schema if available - if hasattr(tool, "function_schema"): - schema = tool.function_schema - if getattr(schema, "description", None): - tool_info["description"] = schema.description - - # Add parameters from json_schema - if getattr(schema, "json_schema", None): - tool_info["parameters"] = schema.json_schema - - tools.append(tool_info) - - if tools: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - else: - span.set_data( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - except Exception: - # If we can't extract tools, just skip it - pass - - -def _capture_exception(exc: "Any", handled: bool = False) -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pydantic_ai", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py deleted file mode 100644 index 2616f4d5a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pymongo.py +++ /dev/null @@ -1,262 +0,0 @@ -import copy -import json - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -try: - from pymongo import monitoring -except ImportError: - raise DidNotEnable("Pymongo not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pymongo.monitoring import ( - CommandFailedEvent, - CommandStartedEvent, - CommandSucceededEvent, - ) - - -SAFE_COMMAND_ATTRIBUTES = [ - "insert", - "ordered", - "find", - "limit", - "singleBatch", - "aggregate", - "createIndexes", - "indexes", - "delete", - "findAndModify", - "renameCollection", - "to", - "drop", -] - - -def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": - for key in command: - is_safe_field = key in SAFE_COMMAND_ATTRIBUTES - if is_safe_field: - # Skip if safe key - continue - - update_db_command = key == "update" and "findAndModify" not in command - if update_db_command: - # Also skip "update" db command because it is save. - # There is also an "update" key in the "findAndModify" command, which is NOT safe! - continue - - # Special stripping for documents - is_document = key == "documents" - if is_document: - for doc in command[key]: - for doc_key in doc: - doc[doc_key] = "%s" - continue - - # Special stripping for dict style fields - is_dict_field = key in ["filter", "query", "update"] - if is_dict_field: - for item_key in command[key]: - command[key][item_key] = "%s" - continue - - # For pipeline fields strip the `$match` dict - is_pipeline_field = key == "pipeline" - if is_pipeline_field: - for pipeline in command[key]: - for match_key in pipeline["$match"] if "$match" in pipeline else []: - pipeline["$match"][match_key] = "%s" - continue - - # Default stripping - command[key] = "%s" - - return command - - -def _get_db_data(event: "Any") -> "Dict[str, Any]": - data = {} - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - data[SPANDATA.DB_DRIVER_NAME] = "pymongo" - db_name = event.database_name - - server_address = event.connection_id[0] - if server_address is not None: - data[SPANDATA.SERVER_ADDRESS] = server_address - - server_port = event.connection_id[1] - if server_port is not None: - data[SPANDATA.SERVER_PORT] = server_port - - if is_span_streaming_enabled: - data["db.system.name"] = "mongodb" - - if db_name is not None: - data["db.namespace"] = db_name - else: - data[SPANDATA.DB_SYSTEM] = "mongodb" - - if db_name is not None: - data[SPANDATA.DB_NAME] = db_name - - return data - - -class CommandTracer(monitoring.CommandListener): - def __init__(self) -> None: - self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} - - def _operation_key( - self, - event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", - ) -> int: - return event.request_id - - def started(self, event: "CommandStartedEvent") -> None: - client = sentry_sdk.get_client() - if client.get_integration(PyMongoIntegration) is None: - return - - with capture_internal_exceptions(): - command = dict(copy.deepcopy(event.command)) - - command.pop("$db", None) - command.pop("$clusterTime", None) - command.pop("$signature", None) - - db_data = _get_db_data(event) - - collection_name = command.get(event.command_name) - operation_name = event.command_name - db_name = event.database_name - - lsid = command.pop("lsid", None) - if not should_send_default_pii(): - command = _strip_pii(command) - - query = json.dumps(command, default=str) - - if has_span_streaming_enabled(client.options): - span_first_data = { - "db.operation.name": operation_name, - "db.collection.name": collection_name, - SPANDATA.DB_QUERY_TEXT: query, - "sentry.op": OP.DB, - "sentry.origin": PyMongoIntegration.origin, - **db_data, - } - - span = sentry_sdk.traces.start_span( - name=query, attributes=span_first_data - ) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, - category="query", - type=OP.DB, - data=span_first_data, - ) - - else: - tags = { - "db.name": db_name, - SPANDATA.DB_SYSTEM: "mongodb", - SPANDATA.DB_DRIVER_NAME: "pymongo", - SPANDATA.DB_OPERATION: operation_name, - # The below is a deprecated field, but leaving for legacy reasons. - # The v2 spans will use `db.collection.name` instead. - SPANDATA.DB_MONGODB_COLLECTION: collection_name, - } - - try: - tags["net.peer.name"] = event.connection_id[0] - tags["net.peer.port"] = str(event.connection_id[1]) - except TypeError: - pass - - data: "Dict[str, Any]" = {"operation_ids": {}} - data["operation_ids"]["operation"] = event.operation_id - data["operation_ids"]["request"] = event.request_id - - data.update(db_data) - - try: - if lsid: - lsid_id = lsid["id"] - data["operation_ids"]["session"] = str(lsid_id) - except KeyError: - pass - - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=PyMongoIntegration.origin, - ) - - for tag, value in tags.items(): - # set the tag for backwards-compatibility. - # TODO: remove the set_tag call in the next major release! - span.set_tag(tag, value) - span.set_data(tag, value) - - for key, value in data.items(): - span.set_data(key, value) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, category="query", type=OP.DB, data=tags - ) - - self._ongoing_operations[self._operation_key(event)] = span.__enter__() - - def failed(self, event: "CommandFailedEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - # Ignoring NoOpStreamedSpan as it will always have a status of "ok" - if type(span) is StreamedSpan: - span.status = SpanStatus.ERROR - elif type(span) is Span: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - span.__exit__(None, None, None) - except KeyError: - return - - def succeeded(self, event: "CommandSucceededEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - if type(span) is Span: - span.set_status(SPANSTATUS.OK) - span.__exit__(None, None, None) - except KeyError: - pass - - -class PyMongoIntegration(Integration): - identifier = "pymongo" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py deleted file mode 100644 index 6837d8345c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyramid.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -import os -import sys -import weakref - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -try: - from pyramid.httpexceptions import HTTPException - from pyramid.request import Request -except ImportError: - raise DidNotEnable("Pyramid not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional - - from pyramid.response import Response - from webob.cookies import RequestCookies - from webob.request import _FieldStorageWithFile - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.utils import ExcInfo - - -if getattr(Request, "authenticated_userid", None): - - def authenticated_userid(request: "Request") -> "Optional[Any]": - return request.authenticated_userid - -else: - # bw-compat for pyramid < 1.5 - from pyramid.security import authenticated_userid # type: ignore - - -TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") - - -class PyramidIntegration(Integration): - identifier = "pyramid" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "route_name") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - from pyramid import router - - old_call_view = router._call_view - - @functools.wraps(old_call_view) - def sentry_patched_call_view( - registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - integration = client.get_integration(PyramidIntegration) - if integration is None: - return old_call_view(registry, request, *args, **kwargs) - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii() and has_span_streaming_enabled(client.options): - user_id = authenticated_userid(request) - if user_id: - scope.set_user({"id": user_id}) - - scope.add_event_processor( - _make_event_processor(weakref.ref(request), integration) - ) - - return old_call_view(registry, request, *args, **kwargs) - - router._call_view = sentry_patched_call_view - - if hasattr(Request, "invoke_exception_view"): - old_invoke_exception_view = Request.invoke_exception_view - - def sentry_patched_invoke_exception_view( - self: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - rv = old_invoke_exception_view(self, *args, **kwargs) - - if ( - self.exc_info - and all(self.exc_info) - and rv.status_int == 500 - and sentry_sdk.get_client().get_integration(PyramidIntegration) - is not None - ): - _capture_exception(self.exc_info) - - return rv - - Request.invoke_exception_view = sentry_patched_invoke_exception_view - - old_wsgi_call = router.Router.__call__ - - @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) - def sentry_patched_wsgi_call( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - def sentry_patched_inner_wsgi_call( - environ: "Dict[str, Any]", start_response: "Callable[..., Any]" - ) -> "Any": - try: - return old_wsgi_call(self, environ, start_response) - except Exception: - einfo = sys.exc_info() - _capture_exception(einfo) - reraise(*einfo) - - middleware = SentryWsgiMiddleware( - sentry_patched_inner_wsgi_call, - span_origin=PyramidIntegration.origin, - ) - return middleware(environ, start_response) - - router.Router.__call__ = sentry_patched_wsgi_call - - -@ensure_integration_enabled(PyramidIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - if exc_info[0] is None or issubclass(exc_info[0], HTTPException): - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pyramid", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "route_name": request.matched_route.name, - "route_pattern": request.matched_route.pattern, - } - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if is_span_streaming_enabled - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - scope.set_transaction_name( - name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -class PyramidRequestExtractor(RequestExtractor): - def url(self) -> str: - return self.request.path_url - - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "RequestCookies": - return self.request.cookies - - def raw_data(self) -> str: - return self.request.text - - def form(self) -> "Dict[str, str]": - return { - key: value - for key, value in self.request.POST.items() - if not getattr(value, "filename", None) - } - - def files(self) -> "Dict[str, _FieldStorageWithFile]": - return { - key: value - for key, value in self.request.POST.items() - if getattr(value, "filename", None) - } - - def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: - file = postdata.file - try: - return os.fstat(file.fileno()).st_size - except Exception: - return 0 - - -def _make_event_processor( - weak_request: "Callable[[], Request]", integration: "PyramidIntegration" -) -> "EventProcessor": - def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - PyramidRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_info = event.setdefault("user", {}) - user_info.setdefault("id", authenticated_userid(request)) - - return event - - return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py deleted file mode 100644 index aae68c4c10..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/pyreqwest.py +++ /dev/null @@ -1,197 +0,0 @@ -from contextlib import contextmanager -from typing import Any, Generator - -import sentry_sdk -from sentry_sdk import start_span -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - logger, - parse_url, -) - -try: - from pyreqwest.client import ( # type: ignore[import-not-found] - ClientBuilder, - SyncClientBuilder, - ) - from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] - from pyreqwest.request import ( # type: ignore[import-not-found] - OneOffRequestBuilder, - Request, - SyncOneOffRequestBuilder, - ) - from pyreqwest.response import ( # type: ignore[import-not-found] - Response, - SyncResponse, - ) -except ImportError: - raise DidNotEnable("pyreqwest not installed or incompatible version installed") - - -class PyreqwestIntegration(Integration): - identifier = "pyreqwest" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_pyreqwest() - - -def _patch_pyreqwest() -> None: - # Patch Client Builders - _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) - _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) - - # Patch Request Builders - _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) - _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) - - -def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: - if not hasattr(cls, method_name): - return - - original_method = getattr(cls, method_name) - - def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - if not getattr(self, "_sentry_instrumented", False): - integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) - if integration is not None: - self.with_middleware(middleware) - try: - self._sentry_instrumented = True - except (TypeError, AttributeError): - # In case the instance itself is immutable or doesn't allow extra attributes - pass - return original_method(self, *args, **kwargs) - - setattr(cls, method_name, sentry_patched_method) - - -@contextmanager -def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": PyreqwestIntegration.origin, - SPANDATA.HTTP_REQUEST_METHOD: request.method, - }, - ) as span: - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - return - - with start_span( - op=OP.HTTP_CLIENT, - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - origin=PyreqwestIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - -async def sentry_async_middleware( - request: "Request", next_handler: "Next" -) -> "Response": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return await next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = await next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response - - -def sentry_sync_middleware( - request: "Request", next_handler: "SyncNext" -) -> "SyncResponse": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py deleted file mode 100644 index 6a5603d825..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/quart.py +++ /dev/null @@ -1,292 +0,0 @@ -import asyncio -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Union - - from sentry_sdk._types import Event, EventProcessor - -try: - import quart_auth # type: ignore -except ImportError: - quart_auth = None - -try: - from quart import ( # type: ignore - Quart, - Request, - has_request_context, - has_websocket_context, - request, - websocket, - ) - from quart.signals import ( # type: ignore - got_background_exception, - got_request_exception, - got_websocket_exception, - request_started, - websocket_started, - ) -except ImportError: - raise DidNotEnable("Quart is not installed") -else: - # Quart 0.19 is based on Flask and hence no longer has a Scaffold - try: - from quart.scaffold import Scaffold # type: ignore - except ImportError: - from flask.sansio.scaffold import Scaffold # type: ignore - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class QuartIntegration(Integration): - identifier = "quart" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "endpoint") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - request_started.connect(_request_websocket_started) - websocket_started.connect(_request_websocket_started) - got_background_exception.connect(_capture_exception) - got_request_exception.connect(_capture_exception) - got_websocket_exception.connect(_capture_exception) - - patch_asgi_app() - patch_scaffold_route() - - -def patch_asgi_app() -> None: - old_app = Quart.__call__ - - async def sentry_patched_asgi_app( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - if sentry_sdk.get_client().get_integration(QuartIntegration) is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=QuartIntegration.origin, - asgi_version=3, - ) - return await middleware(scope, receive, send) - - Quart.__call__ = sentry_patched_asgi_app - - -def patch_scaffold_route() -> None: - # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 - if sys.version_info >= (3, 12): - iscoroutinefunction = inspect.iscoroutinefunction - else: - iscoroutinefunction = asyncio.iscoroutinefunction - - old_route = Scaffold.route - - def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": - old_decorator = old_route(*args, **kwargs) - - def decorator(old_func: "Any") -> "Any": - if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): - - @wraps(old_func) - @ensure_integration_enabled(QuartIntegration, old_func) - def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - span = get_current_span() - if span is not None and hasattr(span, "_segment"): - span._segment._update_active_thread() - else: - current_scope = sentry_sdk.get_current_scope() - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_func(*args, **kwargs) - - return old_decorator(_sentry_func) - - return old_decorator(old_func) - - return decorator - - Scaffold.route = _sentry_route - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if has_span_streaming_enabled(sentry_sdk.get_client().options) - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - - scope.set_transaction_name( - name=name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - if has_request_context(): - request_websocket = request._get_current_object() - if has_websocket_context(): - request_websocket = websocket._get_current_object() - - # Set the transaction name here, but rely on ASGI middleware - # to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket - ) - - scope = sentry_sdk.get_isolation_scope() - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - current_span = get_current_span() - if type(current_span) is StreamedSpan: - segment = current_span._segment - - segment.set_attribute("http.request.method", request_websocket.method) - header_attributes: "dict[str, Any]" = {} - - for header, header_value in _filter_headers( - dict(request_websocket.headers), use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - segment.set_attributes(header_attributes) - - if should_send_default_pii(): - segment.set_attribute("url.full", request_websocket.url) - segment.set_attribute( - "url.query", - request_websocket.query_string.decode("utf-8", errors="replace"), - ) - - user_properties = {} - if len(request_websocket.access_route) >= 1: - segment.set_attribute( - "client.address", request_websocket.access_route[0] - ) - user_properties["ip_address"] = request_websocket.access_route[0] - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_properties["id"] = current_user_id - - if user_properties: - existing_user_properties = scope._user or {} - scope.set_user({**existing_user_properties, **user_properties}) - - evt_processor = _make_request_event_processor(app, request_websocket, integration) - scope.add_event_processor(evt_processor) - - -def _make_request_event_processor( - app: "Quart", request: "Request", integration: "QuartIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - # TODO: Figure out what to do with request body. Methods on request - # are async, but event processors are not. - - request_info = event.setdefault("request", {}) - request_info["url"] = request.url - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - if len(request.access_route) >= 1: - request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_info = event.setdefault("user", {}) - user_info["id"] = current_user_id - - return event - - return inner - - -async def _capture_exception( - sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "quart", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_current_user_id_from_quart() -> "str | None": - if quart_auth is None: - return None - - if quart_auth.current_user is None: - return None - - try: - return quart_auth.current_user._auth_id - except Exception: - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py deleted file mode 100644 index f723a96f3c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/ray.py +++ /dev/null @@ -1,240 +0,0 @@ -import functools -import inspect -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - event_from_exception, - logger, - package_version, - qualname_from_function, - reraise, -) - -try: - import ray # type: ignore[import-not-found] - from ray import remote -except ImportError: - raise DidNotEnable("Ray not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional - - from sentry_sdk.utils import ExcInfo - - -def _check_sentry_initialized() -> None: - if sentry_sdk.get_client().is_active(): - return - - logger.debug( - "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." - ) - - -def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: - # Patching new_func signature to add the _sentry_tracing parameter to it - # Ray later inspects the signature and finds the unexpected parameter otherwise - signature = inspect.signature(func) - params = list(signature.parameters.values()) - sentry_tracing_param = inspect.Parameter( - "_sentry_tracing", - kind=inspect.Parameter.KEYWORD_ONLY, - default=None, - ) - - # Keyword only arguments are penultimate if function has variadic keyword arguments - if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: - params.insert(-1, sentry_tracing_param) - else: - params.append(sentry_tracing_param) - - func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] - - -def _patch_ray_remote() -> None: - old_remote = remote - - @functools.wraps(old_remote) - def new_remote( - f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" - ) -> "Callable[..., Any]": - if inspect.isclass(f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(f, *args, **kwargs) - - def wrapper(user_f: "Callable[..., Any]") -> "Any": - if inspect.isclass(user_f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(*args, **kwargs)(user_f) - - @functools.wraps(user_f) - def new_func( - *f_args: "Any", - _sentry_tracing: "Optional[dict[str, Any]]" = None, - **f_kwargs: "Any", - ) -> "Any": - _check_sentry_initialized() - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(_sentry_tracing or {}) - - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RAY, - "sentry.origin": RayIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - }, - parent_span=None, - ): - try: - result = user_f(*f_args, **f_kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - transaction = sentry_sdk.continue_trace( - _sentry_tracing or {}, - op=OP.QUEUE_TASK_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - source=TransactionSource.TASK, - ) - - with sentry_sdk.start_transaction(transaction) as transaction: - try: - result = user_f(*f_args, **f_kwargs) - transaction.set_status(SPANSTATUS.OK) - except Exception: - transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - _insert_sentry_tracing_in_signature(new_func) - - if f: - rv = old_remote(new_func) - else: - rv = old_remote(*args, **kwargs)(new_func) - old_remote_method = rv.remote - - def _remote_method_with_header_propagation( - *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Ray Client - """ - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_RAY, - "sentry.origin": RayIntegration.origin, - }, - ): - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - ) as span: - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - span.set_status(SPANSTATUS.OK) - except Exception: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - rv.remote = _remote_method_with_header_propagation - - return rv - - if f is not None: - return wrapper(f) - else: - return wrapper - - ray.remote = new_remote - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={ - "handled": False, - "type": RayIntegration.identifier, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class RayIntegration(Integration): - identifier = "ray" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = package_version("ray") - _check_minimum_version(RayIntegration, version) - - _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py deleted file mode 100644 index 7095721ed2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE -from sentry_sdk.integrations.redis.rb import _patch_rb -from sentry_sdk.integrations.redis.redis import _patch_redis -from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster -from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -class RedisIntegration(Integration): - identifier = "redis" - - def __init__( - self, - max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, - cache_prefixes: "Optional[list[str]]" = None, - ) -> None: - self.max_data_size = max_data_size - self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] - - if max_data_size is not None: - warnings.warn( - "The `max_data_size` parameter of `RedisIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - try: - from redis import StrictRedis, client - except ImportError: - raise DidNotEnable("Redis client not installed") - - _patch_redis(StrictRedis, client) - _patch_redis_cluster() - _patch_rb() - - try: - _patch_rediscluster() - except Exception: - logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py deleted file mode 100644 index 8fc3d0c3a9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_async_common.py +++ /dev/null @@ -1,177 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from redis.asyncio.client import Pipeline, StrictRedis - from redis.asyncio.cluster import ClusterPipeline, RedisCluster - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_async_pipeline( - pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return await old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - try: - command_seq = self._execution_strategy._command_queue - except AttributeError: - if is_cluster: - command_seq = self._command_stack - else: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.is_transaction, - command_seq, - ) - - return await old_execute(self, *args, **kwargs) - - pipeline_cls.execute = _sentry_execute # type: ignore - - -def patch_redis_async_client( - cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return await old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = await old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py deleted file mode 100644 index 58d686b099..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/_sync_common.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_pipeline( - pipeline_cls: "Any", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - command_seq = None - try: - command_seq = self._execution_strategy.command_queue - except AttributeError: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.transaction, - command_seq, - ) - - return old_execute(self, *args, **kwargs) - - pipeline_cls.execute = sentry_patched_execute - - -def patch_redis_client( - cls: "Any", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - """ - This function can be used to instrument custom redis client classes or - subclasses. - """ - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py deleted file mode 100644 index 0822c2c930..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/consts.py +++ /dev/null @@ -1,19 +0,0 @@ -SPAN_ORIGIN = "auto.db.redis" - -_SINGLE_KEY_COMMANDS = frozenset( - ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], -) -_MULTI_KEY_COMMANDS = frozenset( - [ - "del", - "touch", - "unlink", - "mget", - ], -) -_COMMANDS_INCLUDING_SENSITIVE_DATA = [ - "auth", -] -_MAX_NUM_ARGS = 10 # Trim argument lists to this many values -_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values -_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py deleted file mode 100644 index 35f20fddd9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/caches.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Code used for the Caches module in Sentry -""" - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -GET_COMMANDS = ("get", "mget") -SET_COMMANDS = ("set", "setex") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _get_op(name: str) -> "Optional[str]": - op = None - if name.lower() in GET_COMMANDS: - op = OP.CACHE_GET - elif name.lower() in SET_COMMANDS: - op = OP.CACHE_PUT - - return op - - -def _compile_cache_span_properties( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> "dict[str, Any]": - key = _get_safe_key(redis_command, args, kwargs) - key_as_string = _key_as_string(key) - keys_as_string = key_as_string.split(", ") - - is_cache_key = False - for prefix in integration.cache_prefixes: - for kee in keys_as_string: - if kee.startswith(prefix): - is_cache_key = True - break - if is_cache_key: - break - - value = None - if redis_command.lower() in SET_COMMANDS: - value = args[-1] - - properties = { - "op": _get_op(redis_command), - "description": _get_cache_span_description( - redis_command, args, kwargs, integration - ), - "key": key, - "key_as_string": key_as_string, - "redis_command": redis_command.lower(), - "is_cache_key": is_cache_key, - "value": value, - } - - return properties - - -def _get_cache_span_description( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> str: - description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_cache_data( - span: "Union[Span, StreamedSpan]", - redis_client: "Any", - properties: "dict[str, Any]", - return_value: "Optional[Any]", -) -> None: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - with capture_internal_exceptions(): - set_on_span(SPANDATA.CACHE_KEY, properties["key"]) - - if properties["redis_command"] in GET_COMMANDS: - if return_value is not None: - set_on_span(SPANDATA.CACHE_HIT, True) - size = ( - len(str(return_value).encode("utf-8")) - if not isinstance(return_value, bytes) - else len(return_value) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - else: - set_on_span(SPANDATA.CACHE_HIT, False) - - elif properties["redis_command"] in SET_COMMANDS: - if properties["value"] is not None: - size = ( - len(properties["value"].encode("utf-8")) - if not isinstance(properties["value"], bytes) - else len(properties["value"]) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - - try: - connection_params = redis_client.connection_pool.connection_kwargs - except AttributeError: - # If it is a cluster, there is no connection_pool attribute so we - # need to get the default node from the cluster instance - default_node = redis_client.get_default_node() - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - - host = connection_params.get("host") - if host is not None: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) - - port = connection_params.get("port") - if port is not None: - set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py deleted file mode 100644 index 69207bf6f6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/modules/queries.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Code used for the Queries module in Sentry -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_command -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import Redis - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _compile_db_span_properties( - integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" -) -> "dict[str, Any]": - description = _get_db_span_description(integration, redis_command, args) - - properties = { - "op": OP.DB_REDIS, - "description": description, - } - - return properties - - -def _get_db_span_description( - integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" -) -> str: - description = command_name - - with capture_internal_exceptions(): - description = _get_safe_command(command_name, args) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_db_data_on_span( - span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" -) -> None: - db = connection_params.get("db") - host = connection_params.get("host") - port = connection_params.get("port") - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) - - if host is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_attribute(SPANDATA.SERVER_PORT, port) - - else: - span.set_data(SPANDATA.DB_SYSTEM, "redis") - span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_data(SPANDATA.DB_NAME, str(db)) - - if host is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_data(SPANDATA.SERVER_PORT, port) - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" -) -> None: - try: - _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) - except AttributeError: - pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py deleted file mode 100644 index e2ce863fe8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/rb.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Instrumentation for Redis Blaster (rb) - -https://github.com/getsentry/rb -""" - -from sentry_sdk.integrations.redis._sync_common import patch_redis_client -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - - -def _patch_rb() -> None: - try: - import rb.clients # type: ignore - except ImportError: - pass - else: - patch_redis_client( - rb.clients.FanoutClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.MappingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.RoutingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py deleted file mode 100644 index e704c9bc6a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Instrumentation for Redis - -https://github.com/redis/redis-py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - -if TYPE_CHECKING: - from typing import Any, Sequence - - -def _get_redis_command_args(command: "Any") -> "Sequence[Any]": - return command[0] - - -def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 - patch_redis_client( - StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_pipeline( - client.Pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - try: - strict_pipeline = client.StrictPipeline - except AttributeError: - pass - else: - patch_redis_pipeline( - strict_pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - - try: - import redis.asyncio - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - redis.asyncio.client.StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_async_pipeline( - redis.asyncio.client.Pipeline, - False, - _get_redis_command_args, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py deleted file mode 100644 index b6c95e6abd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_cluster.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Instrumentation for RedisCluster -This is part of the main redis-py client. - -https://github.com/redis/redis-py/blob/master/redis/cluster.py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import RedisCluster - from redis.asyncio.cluster import ( - ClusterPipeline as AsyncClusterPipeline, - ) - from redis.asyncio.cluster import ( - RedisCluster as AsyncRedisCluster, - ) - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -def _set_async_cluster_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_instance: "AsyncRedisCluster[Any]", -) -> None: - default_node = async_redis_cluster_instance.get_default_node() - if default_node is not None and default_node.connection_kwargs is not None: - _set_db_data_on_span(span, default_node.connection_kwargs) - - -def _set_async_cluster_pipeline_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", -) -> None: - with capture_internal_exceptions(): - client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) - if client is None: - # In older redis-py versions, the AsyncClusterPipeline had a `_client` - # attr but it is private so potentially problematic and mypy does not - # recognize it - see - # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 - client = ( - async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] - ) - - _set_async_cluster_db_data( - span, - client, - ) - - -def _set_cluster_db_data( - span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" -) -> None: - default_node = redis_cluster_instance.get_default_node() - - if default_node is not None: - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - _set_db_data_on_span(span, connection_params) - - -def _patch_redis_cluster() -> None: - """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" - try: - from redis import RedisCluster, cluster - except ImportError: - pass - else: - patch_redis_client( - RedisCluster, - is_cluster=True, - set_db_data_fn=_set_cluster_db_data, - ) - patch_redis_pipeline( - cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_cluster_db_data, - ) - - try: - from redis.asyncio import cluster as async_cluster - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - async_cluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_async_cluster_db_data, - ) - patch_redis_async_pipeline( - async_cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_async_cluster_pipeline_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py deleted file mode 100644 index 3437aa1f2f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Instrumentation for redis-py-cluster -The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). - -https://github.com/grokzen/redis-py-cluster -""" - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command - - -def _patch_rediscluster() -> None: - try: - import rediscluster # type: ignore - except ImportError: - return - - patch_redis_client( - rediscluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - - # up to v1.3.6, __version__ attribute is a tuple - # from v2.0.0, __version__ is a string and VERSION a tuple - version = getattr(rediscluster, "VERSION", rediscluster.__version__) - - # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 - # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst - if (0, 2, 0) < version < (2, 0, 0): - pipeline_cls = rediscluster.pipeline.StrictClusterPipeline - patch_redis_client( - rediscluster.StrictRedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - else: - pipeline_cls = rediscluster.pipeline.ClusterPipeline - - patch_redis_pipeline( - pipeline_cls, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py deleted file mode 100644 index 7e04df9c69..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/redis/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations.redis.consts import ( - _COMMANDS_INCLUDING_SENSITIVE_DATA, - _MAX_NUM_ARGS, - _MAX_NUM_COMMANDS, - _MULTI_KEY_COMMANDS, - _SINGLE_KEY_COMMANDS, -) -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - -if TYPE_CHECKING: - from typing import Any, Optional, Sequence, Union - - -def _get_safe_command(name: str, args: "Sequence[Any]") -> str: - command_parts = [name] - - name_low = name.lower() - send_default_pii = should_send_default_pii() - - for i, arg in enumerate(args): - if i > _MAX_NUM_ARGS: - break - - if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - continue - - arg_is_the_key = i == 0 - if arg_is_the_key: - command_parts.append(repr(arg)) - else: - if send_default_pii: - command_parts.append(repr(arg)) - else: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - - command = " ".join(command_parts) - return command - - -def _safe_decode(key: "Any") -> str: - if isinstance(key, bytes): - try: - return key.decode() - except UnicodeDecodeError: - return "" - - return str(key) - - -def _key_as_string(key: "Any") -> str: - if isinstance(key, (dict, list, tuple)): - key = ", ".join(_safe_decode(x) for x in key) - elif isinstance(key, bytes): - key = _safe_decode(key) - elif key is None: - key = "" - else: - key = str(key) - - return key - - -def _get_safe_key( - method_name: str, - args: "Optional[tuple[Any, ...]]", - kwargs: "Optional[dict[str, Any]]", -) -> "Optional[tuple[str, ...]]": - """ - Gets the key (or keys) from the given method_name. - The method_name could be a redis command or a django caching command - """ - key = None - - if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: - # for example redis "mget" - key = tuple(args) - - elif args is not None and len(args) >= 1: - # for example django "set_many/get_many" or redis "get" - if isinstance(args[0], (dict, list, tuple)): - key = tuple(args[0]) - else: - key = (args[0],) - - elif kwargs is not None and "key" in kwargs: - # this is a legacy case for older versions of Django - if isinstance(kwargs["key"], (list, tuple)): - if len(kwargs["key"]) > 0: - key = tuple(kwargs["key"]) - else: - if kwargs["key"] is not None: - key = (kwargs["key"],) - - return key - - -def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": - return command.args - - -def _set_pipeline_data( - span: "Union[Span, StreamedSpan]", - is_cluster: bool, - get_command_args_fn: "Any", - is_transaction: bool, - commands_seq: "Sequence[Any]", -) -> None: - # TODO: Remove this whole function when removing transaction based tracing - if isinstance(span, StreamedSpan): - return - - span.set_tag("redis.is_cluster", is_cluster) - span.set_tag("redis.transaction", is_transaction) - - commands = [] - for i, arg in enumerate(commands_seq): - if i >= _MAX_NUM_COMMANDS: - break - - command = get_command_args_fn(arg) - commands.append(_get_safe_command(command[0], command[1:])) - - span.set_data( - "redis.commands", - { - "count": len(commands_seq), - "first_ten": commands, - }, - ) - - -def _set_client_data( - span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" -) -> None: - if isinstance(span, StreamedSpan): - if name: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) - else: - span.set_tag("redis.is_cluster", is_cluster) - if name: - span.set_tag("redis.command", name) - span.set_tag(SPANDATA.DB_OPERATION, name) - - if name and args: - name_low = name.lower() - if (name_low in _SINGLE_KEY_COMMANDS) or ( - name_low in _MULTI_KEY_COMMANDS and len(args) == 1 - ): - if isinstance(span, StreamedSpan): - span.set_attribute("db.redis.key", args[0]) - else: - span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py deleted file mode 100644 index edd48a9f67..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rq.py +++ /dev/null @@ -1,225 +0,0 @@ -import functools -import weakref - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - format_timestamp, - parse_version, -) - -try: - from rq.job import JobStatus - from rq.queue import Queue - from rq.timeouts import JobTimeoutException - from rq.version import VERSION as RQ_VERSION - from rq.worker import Worker -except ImportError: - raise DidNotEnable("RQ not installed") - -try: - from rq.worker import BaseWorker - - if not hasattr(BaseWorker, "perform_job"): - BaseWorker = None -except ImportError: - BaseWorker = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - from rq.job import Job - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - -class RqIntegration(Integration): - identifier = "rq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(RQ_VERSION) - _check_minimum_version(RqIntegration, version) - - # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly - # instead of Worker, so we need to patch BaseWorker to cover both. - # For older versions where BaseWorker doesn't exist or doesn't have - # perform_job, we patch Worker. - worker_cls = BaseWorker if BaseWorker is not None else Worker - - old_perform_job = worker_cls.perform_job - - @functools.wraps(old_perform_job) - def sentry_patched_perform_job( - self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" - ) -> bool: - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_perform_job(self, job, *args, **kwargs) - - with sentry_sdk.new_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(weakref.ref(job))) - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace( - job.meta.get("_sentry_trace_headers") or {} - ) - - Scope.set_custom_sampling_context({"rq_job": job}) - - func_name = None - with capture_internal_exceptions(): - func_name = job.func_name - - with sentry_sdk.traces.start_span( - name="unknown RQ task" if func_name is None else func_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RQ, - "sentry.origin": RqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job.id, - }, - parent_span=None, - ) as span: - if func_name is not None: - span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) - - rv = old_perform_job(self, job, *args, **kwargs) - else: - transaction = continue_trace( - job.meta.get("_sentry_trace_headers") or {}, - op=OP.QUEUE_TASK_RQ, - name="unknown RQ task", - source=TransactionSource.TASK, - origin=RqIntegration.origin, - ) - - with capture_internal_exceptions(): - transaction.name = job.func_name - - with sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"rq_job": job}, - ): - rv = old_perform_job(self, job, *args, **kwargs) - - if self.is_horse: - # We're inside of a forked process and RQ is - # about to call `os._exit`. Make sure that our - # events get sent out. - sentry_sdk.get_client().flush() - - return rv - - worker_cls.perform_job = sentry_patched_perform_job - - old_handle_exception = worker_cls.handle_exception - - def sentry_patched_handle_exception( - self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" - ) -> "Any": - retry = ( - hasattr(job, "retries_left") - and job.retries_left - and job.retries_left > 0 - ) - failed = job._status == JobStatus.FAILED or job.is_failed - if failed and not retry: - _capture_exception(exc_info) - - return old_handle_exception(self, job, *exc_info, **kwargs) - - worker_cls.handle_exception = sentry_patched_handle_exception - - old_enqueue_job = Queue.enqueue_job - - @functools.wraps(old_enqueue_job) - def sentry_patched_enqueue_job( - self: "Queue", job: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_enqueue_job(self, job, **kwargs) - - scope = sentry_sdk.get_current_scope() - span = ( - scope.streamed_span - if has_span_streaming_enabled(client.options) - else scope.span - ) - if span is not None: - job.meta["_sentry_trace_headers"] = dict( - scope.iter_trace_propagation_headers() - ) - - return old_enqueue_job(self, job, **kwargs) - - Queue.enqueue_job = sentry_patched_enqueue_job - - ignore_logger("rq.worker") - - -def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - job = weak_job() - if job is not None: - with capture_internal_exceptions(): - extra = event.setdefault("extra", {}) - rq_job = { - "job_id": job.id, - "func": job.func_name, - "args": ( - job.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - job.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "description": job.description, - } - - if job.enqueued_at: - rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) - if job.started_at: - rq_job["started_at"] = format_timestamp(job.started_at) - - extra["rq-job"] = rq_job - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], JobTimeoutException): - event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "rq", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py deleted file mode 100644 index 622e3c17af..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/rust_tracing.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -This integration ingests tracing data from native extensions written in Rust. - -Using it requires additional setup on the Rust side to accept a -`RustTracingLayer` Python object and register it with the `tracing-subscriber` -using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: -```rust -#[pyfunction] -pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { - tracing_subscriber::registry() - .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) - .init(); -} -``` - -Usage in Python would then look like: -``` -sentry_sdk.init( - dsn=sentry_dsn, - integrations=[ - RustTracingIntegration( - "demo_rust_extension", - demo_rust_extension.initialize_tracing, - event_type_mapping=event_type_mapping, - ) - ], -) -``` - -Each native extension requires its own integration. -""" - -import json -from enum import Enum, auto -from typing import Any, Callable, Dict, Optional, Union - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - - -class RustTracingLevel(Enum): - Trace = "TRACE" - Debug = "DEBUG" - Info = "INFO" - Warn = "WARN" - Error = "ERROR" - - -class EventTypeMapping(Enum): - Ignore = auto() - Exc = auto() - Breadcrumb = auto() - Event = auto() - - -def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": - level = RustTracingLevel(level) - if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): - return "debug" - elif level == RustTracingLevel.Info: - return "info" - elif level == RustTracingLevel.Warn: - return "warning" - elif level == RustTracingLevel.Error: - return "error" - else: - # Better this than crashing - return "info" - - -def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": - metadata = event.get("metadata", {}) - contexts = {} - - location = {} - for field in ["module_path", "file", "line"]: - if field in metadata: - location[field] = metadata[field] - if len(location) > 0: - contexts["rust_tracing_location"] = location - - fields = {} - for field in metadata.get("fields", []): - fields[field] = event.get(field) - if len(fields) > 0: - contexts["rust_tracing_fields"] = fields - - return contexts - - -def process_event(event: "Dict[str, Any]") -> None: - metadata = event.get("metadata", {}) - - logger = metadata.get("target") - level = tracing_level_to_sentry_level(metadata.get("level")) - message: "sentry_sdk._types.Any" = event.get("message") - contexts = extract_contexts(event) - - sentry_event: "sentry_sdk._types.Event" = { - "logger": logger, - "level": level, - "message": message, - "contexts": contexts, - } - - sentry_sdk.capture_event(sentry_event) - - -def process_exception(event: "Dict[str, Any]") -> None: - process_event(event) - - -def process_breadcrumb(event: "Dict[str, Any]") -> None: - level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) - message = event.get("message") - - sentry_sdk.add_breadcrumb(level=level, message=message) - - -def default_span_filter(metadata: "Dict[str, Any]") -> bool: - return RustTracingLevel(metadata.get("level")) in ( - RustTracingLevel.Error, - RustTracingLevel.Warn, - RustTracingLevel.Info, - ) - - -def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": - level = RustTracingLevel(metadata.get("level")) - if level == RustTracingLevel.Error: - return EventTypeMapping.Exc - elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): - return EventTypeMapping.Breadcrumb - elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): - return EventTypeMapping.Ignore - else: - return EventTypeMapping.Ignore - - -class RustTracingLayer: - def __init__( - self, - origin: str, - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.origin = origin - self.event_type_mapping = event_type_mapping - self.span_filter = span_filter - self.include_tracing_fields = include_tracing_fields - - def _include_tracing_fields(self) -> bool: - """ - By default, the values of tracing fields are not included in case they - contain PII. A user may override that by passing `True` for the - `include_tracing_fields` keyword argument of this integration or by - setting `send_default_pii` to `True` in their Sentry client options. - """ - return ( - should_send_default_pii() - if self.include_tracing_fields is None - else self.include_tracing_fields - ) - - def on_event(self, event: str, sentry_span: "SentrySpan") -> None: - deserialized_event = json.loads(event) - metadata = deserialized_event.get("metadata", {}) - - event_type = self.event_type_mapping(metadata) - if event_type == EventTypeMapping.Ignore: - return - elif event_type == EventTypeMapping.Exc: - process_exception(deserialized_event) - elif event_type == EventTypeMapping.Breadcrumb: - process_breadcrumb(deserialized_event) - elif event_type == EventTypeMapping.Event: - process_event(deserialized_event) - - def on_new_span( - self, attrs: str, span_id: str - ) -> "Optional[Union[SentrySpan, StreamedSpan]]": - attrs = json.loads(attrs) - metadata = attrs.get("metadata", {}) - - if not self.span_filter(metadata): - return None - - module_path = metadata.get("module_path") - name = metadata.get("name") - message = attrs.get("message") - - if message is not None: - sentry_span_name = message - elif module_path is not None and name is not None: - sentry_span_name = f"{module_path}::{name}" # noqa: E231 - elif name is not None: - sentry_span_name = name - else: - sentry_span_name = "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - sentry_span = sentry_sdk.traces.start_span( - name=sentry_span_name, - attributes={ - "sentry.op": "function", - "sentry.origin": self.origin, - }, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_attribute(field, attrs.get(field)) - else: - sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) - - return sentry_span - - sentry_span = sentry_sdk.start_span( - op="function", - name=sentry_span_name, - origin=self.origin, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_data(field, attrs.get(field)) - else: - sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) - - sentry_span.__enter__() - return sentry_span - - def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: - if sentry_span is None: - return - - sentry_span.__exit__(None, None, None) - - def on_record( - self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" - ) -> None: - if sentry_span is None: - return - - set_on_span = ( - sentry_span.set_attribute - if isinstance(sentry_span, StreamedSpan) - else sentry_span.set_data - ) - - deserialized_values = json.loads(values) - for key, value in deserialized_values.items(): - if self._include_tracing_fields(): - set_on_span(key, value) - else: - set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) - - -class RustTracingIntegration(Integration): - """ - Ingests tracing data from a Rust native extension's `tracing` instrumentation. - - If a project uses more than one Rust native extension, each one will need - its own instance of `RustTracingIntegration` with an initializer function - specific to that extension. - - Since all of the setup for this integration requires instance-specific state - which is not available in `setup_once()`, setup instead happens in `__init__()`. - """ - - def __init__( - self, - identifier: str, - initializer: "Callable[[RustTracingLayer], None]", - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.identifier = identifier - origin = f"auto.function.rust_tracing.{identifier}" - self.tracing_layer = RustTracingLayer( - origin, event_type_mapping, span_filter, include_tracing_fields - ) - - initializer(self.tracing_layer) - - @staticmethod - def setup_once() -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py deleted file mode 100644 index 908fceb0cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sanic.py +++ /dev/null @@ -1,431 +0,0 @@ -import sys -import warnings -import weakref -from inspect import isawaitable -from typing import TYPE_CHECKING -from urllib.parse import urlsplit - -import sentry_sdk -from sentry_sdk import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -if TYPE_CHECKING: - from collections.abc import Container - from typing import Any, Callable, Dict, Optional, Union - - from sanic.request import Request, RequestParameters - from sanic.response import BaseHTTPResponse - from sanic.router import Route - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -try: - from sanic import Sanic - from sanic import __version__ as SANIC_VERSION - from sanic.exceptions import SanicException - from sanic.handlers import ErrorHandler - from sanic.router import Router -except ImportError: - raise DidNotEnable("Sanic not installed") - -old_error_handler_lookup = ErrorHandler.lookup -old_handle_request = Sanic.handle_request -old_router_get = Router.get - -try: - # This method was introduced in Sanic v21.9 - old_startup = Sanic._startup -except AttributeError: - pass - - -class SanicIntegration(Integration): - identifier = "sanic" - origin = f"auto.http.{identifier}" - version: "Optional[tuple[int, ...]]" = None - - def __init__( - self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) - ) -> None: - """ - The unsampled_statuses parameter can be used to specify for which HTTP statuses the - transactions should not be sent to Sentry. By default, transactions are sent for all - HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all - HTTP statuses, including 404. - """ - self._unsampled_statuses = unsampled_statuses or set() - - @staticmethod - def setup_once() -> None: - SanicIntegration.version = parse_version(SANIC_VERSION) - _check_minimum_version(SanicIntegration, SanicIntegration.version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - if SANIC_VERSION.startswith("0.8."): - # Sanic 0.8 and older creates a logger named "root" and puts a - # stringified version of every exception in there (without exc_info), - # which our error deduplication can't detect. - # - # We explicitly check the version here because it is a very - # invasive step to ignore this logger and not necessary in newer - # versions at all. - # - # https://github.com/huge-success/sanic/issues/1332 - ignore_logger("root") - - if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): - _setup_legacy_sanic() - return - - _setup_sanic() - - -class SanicRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return dict(self.request.cookies) - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "RequestParameters": - return self.request.form - - def is_json(self) -> bool: - raise NotImplementedError() - - def json(self) -> "Optional[Any]": - return self.request.json - - def files(self) -> "RequestParameters": - return self.request.files - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) - - -def _setup_sanic() -> None: - Sanic._startup = _startup - ErrorHandler.lookup = _sentry_error_handler_lookup - - -def _setup_legacy_sanic() -> None: - Sanic.handle_request = _legacy_handle_request - Router.get = _legacy_router_get - ErrorHandler.lookup = _sentry_error_handler_lookup - - -async def _startup(self: "Sanic") -> None: - # This happens about as early in the lifecycle as possible, just after the - # Request object is created. The body has not yet been consumed. - self.signal("http.lifecycle.request")(_context_enter) - - # This happens after the handler is complete. In v21.9 this signal is not - # dispatched when there is an exception. Therefore we need to close out - # and call _context_exit from the custom exception handler as well. - # See https://github.com/sanic-org/sanic/issues/2297 - self.signal("http.lifecycle.response")(_context_exit) - - # This happens inside of request handling immediately after the route - # has been identified by the router. - self.signal("http.routing.after")(_set_transaction) - - # The above signals need to be declared before this can be called. - await old_startup(self) - - -async def _context_enter(request: "Request") -> None: - request.ctx._sentry_do_integration = ( - sentry_sdk.get_client().get_integration(SanicIntegration) is not None - ) - - if not request.ctx._sentry_do_integration: - return - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - weak_request = weakref.ref(request) - request.ctx._sentry_scope = sentry_sdk.isolation_scope() - scope = request.ctx._sentry_scope.__enter__() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - if is_span_streaming_enabled: - integration = client.get_integration(SanicIntegration) - if ( - isinstance(integration, SanicIntegration) - and integration._unsampled_statuses - ): - warnings.warn( - "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", - stacklevel=2, - ) - - sentry_sdk.traces.continue_trace(dict(request.headers)) - scope.set_custom_sampling_context({"sanic_request": request}) - - if should_send_default_pii() and request.remote_addr: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) - - span = sentry_sdk.traces.start_span( - # Unless the request results in a 404 error, the name and source - # will get overwritten in _set_transaction - name=request.path, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": SanicIntegration.origin, - "sentry.span.source": SegmentSource.URL.value, - }, - parent_span=None, - ) - request.ctx._sentry_root_span = span - else: - transaction = continue_trace( - dict(request.headers), - op=OP.HTTP_SERVER, - # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction - name=request.path, - source=TransactionSource.URL, - origin=SanicIntegration.origin, - ) - request.ctx._sentry_root_span = sentry_sdk.start_transaction( - transaction - ).__enter__() - - -async def _context_exit( - request: "Request", response: "Optional[BaseHTTPResponse]" = None -) -> None: - with capture_internal_exceptions(): - if not request.ctx._sentry_do_integration: - return - - integration = sentry_sdk.get_client().get_integration(SanicIntegration) - - response_status = None if response is None else response.status - - # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception - # happens while trying to end the transaction, we still attempt to exit the hub. - with capture_internal_exceptions(): - span = request.ctx._sentry_root_span - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes(request).items(): - span.set_attribute(attr, value) - if response_status is not None: - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) - span.status = "error" if response_status >= 400 else "ok" - - span.end() - - else: - span.set_http_status(response_status) - span.sampled &= ( - isinstance(integration, SanicIntegration) - and response_status not in integration._unsampled_statuses - ) - - span.__exit__(None, None, None) - - request.ctx._sentry_scope.__exit__(None, None, None) - - -async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: - if request.ctx._sentry_do_integration: - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - route_name = route.name.replace(request.app.name, "").strip(".") - scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) - - -def _sentry_error_handler_lookup( - self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" -) -> "Optional[object]": - _capture_exception(exception) - old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) - - if old_error_handler is None: - return None - - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return old_error_handler - - async def sentry_wrapped_error_handler( - request: "Request", exception: Exception - ) -> "Any": - try: - response = old_error_handler(request, exception) - if isawaitable(response): - response = await response - return response - except Exception: - # Report errors that occur in Sanic error handler. These - # exceptions will not even show up in Sanic's - # `sanic.exceptions` logger. - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - finally: - # As mentioned in previous comment in _startup, this can be removed - # after https://github.com/sanic-org/sanic/issues/2297 is resolved - if SanicIntegration.version and SanicIntegration.version == (21, 9): - await _context_exit(request) - - return sentry_wrapped_error_handler - - -async def _legacy_handle_request( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" -) -> "Any": - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return await old_handle_request(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - response = old_handle_request(self, request, *args, **kwargs) - if isawaitable(response): - response = await response - - return response - - -def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": - rv = old_router_get(self, *args) - if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: - with capture_internal_exceptions(): - scope = sentry_sdk.get_isolation_scope() - if SanicIntegration.version and SanicIntegration.version >= (21, 3): - # Sanic versions above and including 21.3 append the app name to the - # route name, and so we need to remove it from Route name so the - # transaction name is consistent across all versions - sanic_app_name = self.ctx.app.name - sanic_route = rv[0].name - - if sanic_route.startswith("%s." % sanic_app_name): - # We add a 1 to the len of the sanic_app_name because there is a dot - # that joins app name and the route name - # Format: app_name.route_name - sanic_route = sanic_route[len(sanic_app_name) + 1 :] - - scope.set_transaction_name( - sanic_route, source=TransactionSource.COMPONENT - ) - else: - scope.set_transaction_name( - rv[0].__name__, source=TransactionSource.COMPONENT - ) - - return rv - - -@ensure_integration_enabled(SanicIntegration) -def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "sanic", "handled": False}, - ) - - if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: - return - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_request_attributes(request: "Request") -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from a Sanic request. - """ - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - urlparts = urlsplit(request.url) - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.url - attributes["url.path"] = urlparts.path - - if urlparts.query: - attributes[SPANDATA.HTTP_QUERY] = urlparts.query - - if urlparts.scheme: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme - - if should_send_default_pii() and request.remote_addr: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr - - return attributes - - -def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": - def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - try: - if hint and issubclass(hint["exc_info"][0], SanicException): - return None - except KeyError: - pass - - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - extractor = SanicRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - urlparts = urlsplit(request.url) - - request_info["url"] = "%s://%s%s" % ( - urlparts.scheme, - urlparts.netloc, - urlparts.path, - ) - - request_info["query_string"] = urlparts.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_addr} - request_info["headers"] = _filter_headers(dict(request.headers)) - - return event - - return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py deleted file mode 100644 index 16f91b28ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/serverless.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import event_from_exception, reraise - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union, overload - - F = TypeVar("F", bound=Callable[..., Any]) - -else: - - def overload(x: "F") -> "F": - return x - - -@overload -def serverless_function(f: "F", flush: bool = True) -> "F": - pass - - -@overload -def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 - pass - - -def serverless_function( # noqa - f: "Optional[F]" = None, flush: bool = True -) -> "Union[F, Callable[[F], F]]": - def wrapper(f: "F") -> "F": - @wraps(f) - def inner(*args: "Any", **kwargs: "Any") -> "Any": - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - - try: - return f(*args, **kwargs) - except Exception: - _capture_and_reraise() - finally: - if flush: - sentry_sdk.flush() - - return inner # type: ignore - - if f is None: - return wrapper - else: - return wrapper(f) - - -def _capture_and_reraise() -> None: - exc_info = sys.exc_info() - client = sentry_sdk.get_client() - if client.is_active(): - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "serverless", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py deleted file mode 100644 index 775170fb9f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/socket.py +++ /dev/null @@ -1,142 +0,0 @@ -import socket - -import sentry_sdk -from sentry_sdk._types import MYPY -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if MYPY: - from socket import AddressFamily, SocketKind - from typing import List, Optional, Tuple, Union - -__all__ = ["SocketIntegration"] - - -class SocketIntegration(Integration): - identifier = "socket" - origin = f"auto.socket.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) - """ - _patch_create_connection() - _patch_getaddrinfo() - - -def _get_span_description( - host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" -) -> str: - try: - host = host.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - try: - port = port.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - description = "%s:%s" % (host, port) # type: ignore - return description - - -def _patch_create_connection() -> None: - real_create_connection = socket.create_connection - - def create_connection( - address: "Tuple[Optional[str], int]", - timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore - source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, - ) -> "socket.socket": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_create_connection(address, timeout, source_address) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(address[0], address[1]), - attributes={ - "sentry.op": OP.SOCKET_CONNECTION, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if address[0] is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) - span.set_attribute(SPANDATA.SERVER_PORT, address[1]) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_CONNECTION, - name=_get_span_description(address[0], address[1]), - origin=SocketIntegration.origin, - ) as span: - span.set_data("address", address) - span.set_data("timeout", timeout) - span.set_data("source_address", source_address) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - - socket.create_connection = create_connection # type: ignore - - -def _patch_getaddrinfo() -> None: - real_getaddrinfo = socket.getaddrinfo - - def getaddrinfo( - host: "Union[bytes, str, None]", - port: "Union[bytes, str, int, None]", - family: int = 0, - type: int = 0, - proto: int = 0, - flags: int = 0, - ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_getaddrinfo(host, port, family, type, proto, flags) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(host, port), - attributes={ - "sentry.op": OP.SOCKET_DNS, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if isinstance(host, str): - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - elif isinstance(host, bytes): - span.set_attribute( - SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") - ) - - if isinstance(port, int): - span.set_attribute(SPANDATA.SERVER_PORT, port) - elif port is not None: - try: - span.set_attribute(SPANDATA.SERVER_PORT, int(port)) - except (ValueError, TypeError): - pass - - return real_getaddrinfo(host, port, family, type, proto, flags) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_DNS, - name=_get_span_description(host, port), - origin=SocketIntegration.origin, - ) as span: - span.set_data("host", host) - span.set_data("port", port) - - return real_getaddrinfo(host, port, family, type, proto, flags) - - socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py deleted file mode 100644 index 10d94163c5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from sentry_sdk.integrations.spark.spark_driver import SparkIntegration -from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration - -__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py deleted file mode 100644 index a83532b6a6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_driver.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Optional - - from pyspark import SparkContext - - from sentry_sdk._types import Event, Hint - - -class SparkIntegration(Integration): - identifier = "spark" - - @staticmethod - def setup_once() -> None: - _setup_sentry_tracing() - - -def _set_app_properties() -> None: - """ - Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. - This allows worker integration to have access to app_name and application_id. - """ - from pyspark import SparkContext - - spark_context = SparkContext._active_spark_context - if spark_context: - spark_context.setLocalProperty( - "sentry_app_name", - spark_context.appName, - ) - spark_context.setLocalProperty( - "sentry_application_id", - spark_context.applicationId, - ) - - -def _start_sentry_listener(sc: "SparkContext") -> None: - """ - Start java gateway server to add custom `SparkListener` - """ - from pyspark.java_gateway import ensure_callback_server_started - - gw = sc._gateway - ensure_callback_server_started(gw) - listener = SentryListener() - sc._jsc.sc().addSparkListener(listener) - - -def _add_event_processor(sc: "SparkContext") -> None: - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - if sentry_sdk.get_client().get_integration(SparkIntegration) is None: - return event - - if sc._active_spark_context is None: - return event - - event.setdefault("user", {}).setdefault("id", sc.sparkUser()) - - event.setdefault("tags", {}).setdefault( - "executor.id", sc._conf.get("spark.executor.id") - ) - event["tags"].setdefault( - "spark-submit.deployMode", - sc._conf.get("spark.submit.deployMode"), - ) - event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) - event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) - event["tags"].setdefault("spark_version", sc.version) - event["tags"].setdefault("app_name", sc.appName) - event["tags"].setdefault("application_id", sc.applicationId) - event["tags"].setdefault("master", sc.master) - event["tags"].setdefault("spark_home", sc.sparkHome) - - event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) - - return event - - -def _activate_integration(sc: "SparkContext") -> None: - _start_sentry_listener(sc) - _set_app_properties() - _add_event_processor(sc) - - -def _patch_spark_context_init() -> None: - from pyspark import SparkContext - - spark_context_init = SparkContext._do_init - - @ensure_integration_enabled(SparkIntegration, spark_context_init) - def _sentry_patched_spark_context_init( - self: "SparkContext", *args: "Any", **kwargs: "Any" - ) -> "Optional[Any]": - rv = spark_context_init(self, *args, **kwargs) - _activate_integration(self) - return rv - - SparkContext._do_init = _sentry_patched_spark_context_init - - -def _setup_sentry_tracing() -> None: - from pyspark import SparkContext - - if SparkContext._active_spark_context is not None: - _activate_integration(SparkContext._active_spark_context) - return - _patch_spark_context_init() - - -class SparkListener: - def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 - pass - - def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklistedForStage( # noqa: N802 - self, - executorBlacklistedForStage: "Any", # noqa: N803 - ) -> None: - pass - - def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 - pass - - def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 - pass - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - pass - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 - pass - - def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 - pass - - class Java: - implements = ["org.apache.spark.scheduler.SparkListenerInterface"] - - -class SentryListener(SparkListener): - def _add_breadcrumb( - self, - level: str, - message: str, - data: "Optional[dict[str, Any]]" = None, - ) -> None: - sentry_sdk.get_isolation_scope().add_breadcrumb( - level=level, message=message, data=data - ) - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - message = "Job {} Started".format(jobStart.jobId()) - self._add_breadcrumb(level="info", message=message) - _set_app_properties() - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - level = "" - message = "" - data = {"result": jobEnd.jobResult().toString()} - - if jobEnd.jobResult().toString() == "JobSucceeded": - level = "info" - message = "Job {} Ended".format(jobEnd.jobId()) - else: - level = "warning" - message = "Job {} Failed".format(jobEnd.jobId()) - - self._add_breadcrumb(level=level, message=message, data=data) - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - stage_info = stageSubmitted.stageInfo() - message = "Stage {} Submitted".format(stage_info.stageId()) - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - self._add_breadcrumb(level="info", message=message, data=data) - _set_app_properties() - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - from py4j.protocol import Py4JJavaError # type: ignore - - stage_info = stageCompleted.stageInfo() - message = "" - level = "" - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - # Have to Try Except because stageInfo.failureReason() is typed with Scala Option - try: - data["reason"] = stage_info.failureReason().get() - message = "Stage {} Failed".format(stage_info.stageId()) - level = "warning" - except Py4JJavaError: - message = "Stage {} Completed".format(stage_info.stageId()) - level = "info" - - self._add_breadcrumb(level=level, message=message, data=data) - - -def _get_attempt_id(stage_info: "Any") -> "Optional[int]": - try: - return stage_info.attemptId() - except Exception: - pass - - try: - return stage_info.attemptNumber() - except Exception: - pass - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py deleted file mode 100644 index 5906472748..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/spark/spark_worker.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_hint_with_exc_info, - exc_info_from_error, - single_exception_from_error_tuple, - walk_exception_chain, -) - -if TYPE_CHECKING: - from typing import Any, Optional - - from sentry_sdk._types import Event, ExcInfo, Hint - - -class SparkWorkerIntegration(Integration): - identifier = "spark_worker" - - @staticmethod - def setup_once() -> None: - import pyspark.daemon as original_daemon - - original_daemon.worker_main = _sentry_worker_main - - -def _capture_exception(exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - - mechanism = {"type": "spark", "handled": False} - - exc_info = exc_info_from_error(exc_info) - - exc_type, exc_value, tb = exc_info - rv = [] - - # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - if exc_type not in (SystemExit, EOFError, ConnectionResetError): - rv.append( - single_exception_from_error_tuple( - exc_type, exc_value, tb, client.options, mechanism - ) - ) - - if rv: - rv.reverse() - hint = event_hint_with_exc_info(exc_info) - event: "Event" = {"level": "error", "exception": {"values": rv}} - - _tag_task_context() - - sentry_sdk.capture_event(event, hint=hint) - - -def _tag_task_context() -> None: - from pyspark.taskcontext import TaskContext - - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - integration = sentry_sdk.get_client().get_integration( - SparkWorkerIntegration - ) - task_context = TaskContext.get() - - if integration is None or task_context is None: - return event - - event.setdefault("tags", {}).setdefault( - "stageId", str(task_context.stageId()) - ) - event["tags"].setdefault("partitionId", str(task_context.partitionId())) - event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) - event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) - - if task_context._localProperties: - if "sentry_app_name" in task_context._localProperties: - event["tags"].setdefault( - "app_name", task_context._localProperties["sentry_app_name"] - ) - event["tags"].setdefault( - "application_id", - task_context._localProperties["sentry_application_id"], - ) - - if "callSite.short" in task_context._localProperties: - event.setdefault("extra", {}).setdefault( - "callSite", task_context._localProperties["callSite.short"] - ) - - return event - - -def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: - import pyspark.worker as original_worker - - try: - original_worker.main(*args, **kwargs) - except SystemExit: - if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py deleted file mode 100644 index 962fe4ee53..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sqlalchemy.py +++ /dev/null @@ -1,185 +0,0 @@ -from sentry_sdk.consts import SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - parse_version, -) - -try: - from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore - from sqlalchemy.engine import Engine # type: ignore - from sqlalchemy.event import listen # type: ignore -except ImportError: - raise DidNotEnable("SQLAlchemy not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional, Union - - -class SqlalchemyIntegration(Integration): - identifier = "sqlalchemy" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(SQLALCHEMY_VERSION) - _check_minimum_version(SqlalchemyIntegration, version) - - listen(Engine, "before_cursor_execute", _before_cursor_execute) - listen(Engine, "after_cursor_execute", _after_cursor_execute) - listen(Engine, "handle_error", _handle_error) - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _before_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - executemany: bool, - *args: "Any", -) -> None: - ctx_mgr = record_sql_queries( - cursor, - statement, - parameters, - paramstyle=context and context.dialect and context.dialect.paramstyle or None, - executemany=executemany, - span_origin=SqlalchemyIntegration.origin, - ) - context._sentry_sql_span_manager = ctx_mgr - - span = ctx_mgr.__enter__() - - if span is not None: - _set_db_data(span, conn) - context._sentry_sql_span = span - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _after_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - *args: "Any", -) -> None: - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - context, "_sentry_sql_span_manager", None - ) - - # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. - span: "Optional[Union[Span, StreamedSpan]]" = getattr( - context, "_sentry_sql_span", None - ) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if ctx_mgr is not None: - context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - if isinstance(span, Span): - with capture_internal_exceptions(): - add_query_source(span) - - -def _handle_error(context: "Any", *args: "Any") -> None: - execution_context = context.execution_context - if execution_context is None: - return - - span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) - - if span is not None: - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # _after_cursor_execute does not get called for crashing SQL stmts. Judging - # from SQLAlchemy codebase it does seem like any error coming into this - # handler is going to be fatal. - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - execution_context, "_sentry_sql_span_manager", None - ) - - if ctx_mgr is not None: - execution_context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - -# See: https://docs.sqlalchemy.org/en/20/dialects/index.html -def _get_db_system(name: str) -> "Optional[str]": - name = str(name) - - if "sqlite" in name: - return "sqlite" - - if "postgres" in name: - return "postgresql" - - if "mariadb" in name: - return "mariadb" - - if "mysql" in name: - return "mysql" - - if "oracle" in name: - return "oracle" - - return None - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - db_system = _get_db_system(conn.engine.name) - - if isinstance(span, StreamedSpan): - if db_system is not None: - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) - else: - if db_system is not None: - span.set_data(SPANDATA.DB_SYSTEM, db_system) - - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - try: - driver = conn.dialect.driver - if driver: - set_on_span(SPANDATA.DB_DRIVER_NAME, driver) - except Exception: - pass - - if conn.engine.url is None: - return - - db_name = conn.engine.url.database - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - server_address = conn.engine.url.host - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = conn.engine.url.port - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py deleted file mode 100644 index 3f9fbf4d8e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlette.py +++ /dev/null @@ -1,858 +0,0 @@ -import functools -import json -import sys -import warnings -from collections.abc import Set -from copy import deepcopy -from json import JSONDecodeError -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations._asgi_common import _RootPathInPath -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - HttpCodeRangeContainer, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Awaitable, - Callable, - Container, - Dict, - Optional, - Tuple, - Union, - ) - - from sentry_sdk._types import Event, HttpStatusCodeRange -try: - import starlette - from starlette import __version__ as STARLETTE_VERSION - from starlette.applications import Starlette - from starlette.datastructures import ( - UploadFile, - ) - from starlette.middleware import Middleware - from starlette.middleware.authentication import ( - AuthenticationMiddleware, - ) - from starlette.requests import Request - from starlette.routing import Match - from starlette.types import ASGIApp, Receive, Send - from starlette.types import Scope as StarletteScope -except ImportError: - raise DidNotEnable("Starlette is not installed") - -try: - # Starlette 0.20 - from starlette.middleware.exceptions import ExceptionMiddleware -except ImportError: - # Startlette 0.19.1 - from starlette.exceptions import ExceptionMiddleware # type: ignore - -try: - # Optional dependency of Starlette to parse form data. - try: - # python-multipart 0.0.13 and later - import python_multipart as multipart - except ImportError: - # python-multipart 0.0.12 and earlier - import multipart # type: ignore -except ImportError: - multipart = None # type: ignore[assignment] - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -_DEFAULT_TRANSACTION_NAME = "generic Starlette request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class StarletteIntegration(Integration): - identifier = "starlette" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "url", - failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - middleware_spans: bool = False, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ): - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - if isinstance(failed_request_status_codes, Set): - self.failed_request_status_codes: "Container[int]" = ( - failed_request_status_codes - ) - else: - warnings.warn( - "Passing a list or None for failed_request_status_codes is deprecated. " - "Please pass a set of int instead.", - DeprecationWarning, - stacklevel=2, - ) - - if failed_request_status_codes is None: - self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES - else: - self.failed_request_status_codes = HttpCodeRangeContainer( - failed_request_status_codes - ) - - @staticmethod - def setup_once() -> None: - version = parse_version(STARLETTE_VERSION) - - if version is None: - raise DidNotEnable( - "Unparsable Starlette version: {}".format(STARLETTE_VERSION) - ) - - patch_middlewares() - # Starlette tolerates both starting with: - # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. - root_path_in_path = ( - _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED - ) - patch_asgi_app(root_path_in_path=root_path_in_path) - patch_request_response() - - if version >= (0, 24): - patch_templates() - - -def _enable_span_for_middleware( - middleware_class: "Any", -) -> "Any": - old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ - - async def _create_span_call( - app: "Any", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - **kwargs: "Any", - ) -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await old_call(app, scope, receive, send, **kwargs) - - # Update transaction name with middleware name - name, source = _get_transaction_from_middleware(app, scope, integration) - - if name is not None: - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=source, - ) - - if not integration.middleware_spans: - return await old_call(app, scope, receive, send, **kwargs) - - middleware_name = app.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarletteIntegration.origin, - "middleware.name": middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarletteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlette.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await send(*args, **kwargs) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(app, scope, new_receive, new_send, **kwargs) - - not_yet_patched = old_call.__name__ not in [ - "_create_span_call", - "_sentry_authenticationmiddleware_call", - "_sentry_exceptionmiddleware_call", - ] - - if not_yet_patched: - middleware_class.__call__ = _create_span_call - - return middleware_class - - -def _serialize_request_body_data(data: "Any") -> str: - # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values - def _default(value: "Any") -> "Any": - if isinstance(value, AnnotatedValue): - return value.value - return str(value) - - return json.dumps(data, default=_default) - - -@ensure_integration_enabled(StarletteIntegration) -def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarletteIntegration.identifier, "handled": handled}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def patch_exception_middleware(middleware_class: "Any") -> None: - """ - Capture all exceptions in Starlette app and - also extract user information. - """ - old_middleware_init = middleware_class.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - - if not_yet_patched: - - def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: - old_middleware_init(self, *args, **kwargs) - - # Patch existing exception handlers - old_handlers = self._exception_handlers.copy() - - async def _sentry_patched_exception_handler( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration( - StarletteIntegration - ) - - exp = args[0] - - if integration is not None: - is_http_server_error = ( - hasattr(exp, "status_code") - and isinstance(exp.status_code, int) - and exp.status_code in integration.failed_request_status_codes - ) - if is_http_server_error: - _capture_exception(exp, handled=True) - - # Find a matching handler - old_handler = None - for cls in type(exp).__mro__: - if cls in old_handlers: - old_handler = old_handlers[cls] - break - - if old_handler is None: - return - - if _is_async_callable(old_handler): - return await old_handler(self, *args, **kwargs) - else: - return old_handler(self, *args, **kwargs) - - for key in self._exception_handlers.keys(): - self._exception_handlers[key] = _sentry_patched_exception_handler - - middleware_class.__init__ = _sentry_middleware_init - - old_call = middleware_class.__call__ - - async def _sentry_exceptionmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - # Also add the user (that was eventually set by be Authentication middle - # that was called before this middleware). This is done because the authentication - # middleware sets the user in the scope and then (in the same function) - # calls this exception middelware. In case there is no exception (or no handler - # for the type of exception occuring) then the exception bubbles up and setting the - # user information into the sentry scope is done in auth middleware and the - # ASGI middleware will then send everything to Sentry and this is fine. - # But if there is an exception happening that the exception middleware here - # has a handler for, it will send the exception directly to Sentry, so we need - # the user information right now. - # This is why we do it here. - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_exceptionmiddleware_call - - -@ensure_integration_enabled(StarletteIntegration) -def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: - """ - Extracts user information from the ASGI scope and - adds it to Sentry's scope. - """ - if "user" not in scope: - return - - if not should_send_default_pii(): - return - - user_info: "Dict[str, Any]" = {} - starlette_user = scope["user"] - - username = getattr(starlette_user, "username", None) - if username: - user_info.setdefault("username", starlette_user.username) - - user_id = getattr(starlette_user, "id", None) - if user_id: - user_info.setdefault("id", starlette_user.id) - - email = getattr(starlette_user, "email", None) - if email: - user_info.setdefault("email", starlette_user.email) - - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - -def patch_authentication_middleware(middleware_class: "Any") -> None: - """ - Add user information to Sentry scope. - """ - old_call = middleware_class.__call__ - - not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) - - if not_yet_patched: - - async def _sentry_authenticationmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_authenticationmiddleware_call - - -def patch_middlewares() -> None: - """ - Patches Starlettes `Middleware` class to record - spans for every middleware invoked. - """ - old_middleware_init = Middleware.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - if not_yet_patched: - - def _sentry_middleware_init( - self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - if cls == SentryAsgiMiddleware: - return old_middleware_init(self, cls, *args, **kwargs) - - span_enabled_cls = _enable_span_for_middleware(cls) - old_middleware_init(self, span_enabled_cls, *args, **kwargs) - - if cls == AuthenticationMiddleware: - patch_authentication_middleware(cls) - - if cls == ExceptionMiddleware: - patch_exception_middleware(cls) - - Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] - - -def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: - """ - Instrument Starlette ASGI app using the SentryAsgiMiddleware. - """ - old_app = Starlette.__call__ - - async def _sentry_patched_asgi_app( - self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StarletteIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - mechanism_type=StarletteIntegration.identifier, - transaction_style=integration.transaction_style, - span_origin=StarletteIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - asgi_version=3, - root_path_in_path=root_path_in_path, - ) - - return await middleware(scope, receive, send) - - Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] - - -# This was vendored in from Starlette to support Starlette 0.19.1 because -# this function was only introduced in 0.20.x -def _is_async_callable(obj: "Any") -> bool: - while isinstance(obj, functools.partial): - obj = obj.func - - return iscoroutinefunction(obj) or ( - callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] - ) - - -def _get_cached_request_body_attribute( - client: "sentry_sdk.client.BaseClient", request: "Request" -) -> "Optional[str]": - """ - Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. - """ - if "content-length" not in request.headers: - return None - - try: - content_length = int(request.headers["content-length"]) - except ValueError: - return None - - if content_length and not request_body_within_bounds(client, content_length): - return OVER_SIZE_LIMIT_SUBSTITUTE - - if hasattr(request, "_json"): - return json.dumps(request._json) - - formdata_body = getattr(request, "_form", None) - if formdata_body is None: - return None - - form_data = {} - for key, val in formdata_body.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = val if not is_file else "[Unparsable]" - - return json.dumps(form_data) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), - integration.transaction_style, - request, - ) - - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Add info from request to event - request_info = event.get("request", {}) - if info: - if "cookies" in info: - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_request_response() -> None: - old_request_response = starlette.routing.request_response - - def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": - old_func = func - - is_coroutine = _is_async_callable(old_func) - if is_coroutine: - - async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_func, *args, **kwargs) - - func = _sentry_async_func - - else: - - @functools.wraps(old_func) - def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(StarletteIntegration) - if integration is None: - return old_func(*args, **kwargs) - - current_scope = sentry_sdk.get_current_scope() - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming: - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - current_span._segment._update_active_thread() - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - request = args[0] - - _set_transaction_name_and_source( - sentry_scope, integration.transaction_style, request - ) - - extractor = StarletteRequestExtractor(request) - cookies = extractor.extract_cookies_from_request() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor( - event: "Event", hint: "dict[str, Any]" - ) -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if cookies: - request_info["cookies"] = cookies - - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - return old_func(*args, **kwargs) - - func = _sentry_sync_func - - return old_request_response(func) - - starlette.routing.request_response = _sentry_request_response - - -def patch_templates() -> None: - # If markupsafe is not installed, then Jinja2 is not installed - # (markupsafe is a dependency of Jinja2) - # In this case we do not need to patch the Jinja2Templates class - try: - from markupsafe import Markup - except ImportError: - return # Nothing to do - - # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a - try: - from starlette.templating import Jinja2Templates - except ImportError: - return - - old_jinja2templates_init = Jinja2Templates.__init__ - - not_yet_patched = "_sentry_jinja2templates_init" not in str( - old_jinja2templates_init - ) - - if not_yet_patched: - - def _sentry_jinja2templates_init( - self: "Jinja2Templates", *args: "Any", **kwargs: "Any" - ) -> None: - def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": - trace_meta = Markup( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - return { - "sentry_trace_meta": trace_meta, - } - - kwargs.setdefault("context_processors", []) - - if add_sentry_trace_meta not in kwargs["context_processors"]: - kwargs["context_processors"].append(add_sentry_trace_meta) - - return old_jinja2templates_init(self, *args, **kwargs) - - Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] - - -class StarletteRequestExtractor: - """ - Extracts useful information from the Starlette request - (like form data or cookies) and adds it to the Sentry event. - """ - - def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: - self.request = request - - def extract_cookies_from_request( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - cookies: "Optional[Dict[str, Any]]" = None - if should_send_default_pii(): - cookies = self.cookies() - - return cookies - - async def extract_request_info( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - client = sentry_sdk.get_client() - - request_info: "Dict[str, Any]" = {} - - with capture_internal_exceptions(): - # Add cookies - if should_send_default_pii(): - request_info["cookies"] = self.cookies() - - # If there is no body, just return the cookies - content_length = await self.content_length() - if not content_length: - return request_info - - # Add annotation if body is too big - if content_length and not request_body_within_bounds( - client, content_length - ): - request_info["data"] = AnnotatedValue.removed_because_over_size_limit() - return request_info - - # Add JSON body, if it is a JSON request - json = await self.json() - if json: - request_info["data"] = json - return request_info - - # Add form as key/value pairs, if request has form data - form = await self.form() - if form: - form_data = {} - for key, val in form.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = ( - val - if not is_file - else AnnotatedValue.removed_because_raw_data() - ) - - request_info["data"] = form_data - return request_info - - # Raw data, do not add body just an annotation - request_info["data"] = AnnotatedValue.removed_because_raw_data() - return request_info - - async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": - if "content-length" in self.request.headers: - return int(self.request.headers["content-length"]) - - return None - - def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": - return self.request.cookies - - async def form(self: "StarletteRequestExtractor") -> "Any": - if multipart is None: - return None - - # Parse the body first to get it cached, as Starlette does not cache form() as it - # does with body() and json() https://github.com/encode/starlette/discussions/1933 - # Calling `.form()` without calling `.body()` first will - # potentially break the users project. - await self.request.body() - - return await self.request.form() - - def is_json(self: "StarletteRequestExtractor") -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": - if not self.is_json(): - return None - try: - return await self.request.json() - except JSONDecodeError: - return None - - -def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": - router = scope.get("router") - if not router: - return None - - for route in router.routes: - match = route.matches(scope) - if match[0] == Match.FULL: - try: - return route.path - except AttributeError: - # routes added via app.host() won't have a path attribute - return scope.get("path") - - return None - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = None - source = SOURCE_FOR_STYLE[transaction_style] - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or None - - elif transaction_style == "url": - name = _transaction_name_from_router(request.scope) - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - scope.set_transaction_name(name, source=source) - - -def _get_transaction_from_middleware( - app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" -) -> "Tuple[Optional[str], Optional[str]]": - name = None - source = None - - if integration.transaction_style == "endpoint": - name = transaction_from_function(app.__class__) - source = TransactionSource.COMPONENT - elif integration.transaction_style == "url": - name = _transaction_name_from_router(asgi_scope) - source = TransactionSource.ROUTE - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py deleted file mode 100644 index 1eebd37e84..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/starlite.py +++ /dev/null @@ -1,314 +0,0 @@ -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from pydantic import BaseModel - from starlite import Request, Starlite, State # type: ignore - from starlite.handlers.base import BaseRouteHandler # type: ignore - from starlite.middleware import DefineMiddleware # type: ignore - from starlite.plugins.base import get_plugin_for_value # type: ignore - from starlite.routes.http import HTTPRoute # type: ignore - from starlite.utils import ( # type: ignore - ConnectionDataExtractor, - Ref, - is_async_callable, - ) -except ImportError: - raise DidNotEnable("Starlite is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from starlite import MiddlewareProtocol - from starlite.types import ( # type: ignore - ASGIApp, - Hint, - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from starlite.types import ( - Scope as StarliteScope, - ) - - from sentry_sdk._types import Event - - -_DEFAULT_TRANSACTION_NAME = "generic Starlite request" - - -class StarliteIntegration(Integration): - identifier = "starlite" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - -class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - -def patch_app_init() -> None: - """ - Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. - See: - - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception - - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ - """ - old__init__ = Starlite.__init__ - - @ensure_integration_enabled(StarliteIntegration, old__init__) - def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: - after_exception = kwargs.pop("after_exception", []) - kwargs.update( - after_exception=[ - exception_handler, - *( - after_exception - if isinstance(after_exception, list) - else [after_exception] - ), - ] - ) - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Starlite.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryStarliteASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "StarliteScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(StarliteIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarliteIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarliteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlite.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await receive(*args, **kwargs) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await send(message) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - elif isinstance(route_handler.fn, Ref): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = StarliteIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if not scope_user: - return None - if isinstance(scope_user, dict): - return scope_user - if isinstance(scope_user, BaseModel): - return scope_user.dict() - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - plugin = get_plugin_for_value(scope_user) - if plugin and not is_async_callable(plugin.to_dict): - return plugin.to_dict(scope_user) - - return None - - -@ensure_integration_enabled(StarliteIntegration) -def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarliteIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py deleted file mode 100644 index 746e09b36f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/statsig.py +++ /dev/null @@ -1,37 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.utils import parse_version - -try: - from statsig import statsig as statsig_module - from statsig.version import __version__ as STATSIG_VERSION -except ImportError: - raise DidNotEnable("statsig is not installed") - -if TYPE_CHECKING: - from statsig.statsig_user import StatsigUser - - -class StatsigIntegration(Integration): - identifier = "statsig" - - @staticmethod - def setup_once() -> None: - version = parse_version(STATSIG_VERSION) - _check_minimum_version(StatsigIntegration, version, "statsig") - - # Wrap and patch evaluation method(s) in the statsig module - old_check_gate = statsig_module.check_gate - - @wraps(old_check_gate) - def sentry_check_gate( - user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_check_gate(user, gate, *args, **kwargs) - add_feature_flag(gate, enabled) - return enabled - - statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py deleted file mode 100644 index 82f30f2dda..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/stdlib.py +++ /dev/null @@ -1,404 +0,0 @@ -import os -import platform -import subprocess -import sys -from http.client import HTTPConnection, HTTPResponse -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - EnvironHeaders, - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - is_sentry_url, - logger, - parse_url, - safe_repr, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from sentry_sdk._types import Event, Hint - - -_RUNTIME_CONTEXT: "dict[str, object]" = { - "name": platform.python_implementation(), - "version": "%s.%s.%s" % (sys.version_info[:3]), - "build": sys.version, -} - - -class StdlibIntegration(Integration): - identifier = "stdlib" - - @staticmethod - def setup_once() -> None: - _install_httplib() - _install_subprocess() - - @add_global_event_processor - def add_python_runtime_context( - event: "Event", hint: "Hint" - ) -> "Optional[Event]": - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is not None: - contexts = event.setdefault("contexts", {}) - if isinstance(contexts, dict) and "runtime" not in contexts: - contexts["runtime"] = _RUNTIME_CONTEXT - - return event - - -def _complete_span(span: "Union[Span, StreamedSpan]") -> None: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - -def _install_httplib() -> None: - real_putrequest = HTTPConnection.putrequest - real_getresponse = HTTPConnection.getresponse - real_read = HTTPResponse.read - real_close = HTTPResponse.close - - def putrequest( - self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - default_port = self.default_port - - # proxies go through set_tunnel - tunnel_host = getattr(self, "_tunnel_host", None) - if tunnel_host: - host = tunnel_host - port = getattr(self, "_tunnel_port", default_port) - else: - host = self.host - port = self.port - - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is None or is_sentry_url( - client, host - ): - return real_putrequest(self, method, url, *args, **kwargs) - - real_url = url - if real_url is None or not real_url.startswith(("http://", "https://")): - real_url = "%s://%s%s%s" % ( - default_port == 443 and "https" or "http", - host, - port != default_port and ":%s" % port or "", - url, - ) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(real_url, sanitize=False) - - span_streaming = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - attributes={ - "sentry.origin": "auto.http.stdlib.httplib", - "sentry.op": OP.HTTP_CLIENT, - SPANDATA.HTTP_REQUEST_METHOD: method, - }, - ) - - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - - set_on_span = span.set_attribute - - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - origin="auto.http.stdlib.httplib", - ) - - span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - - set_on_span = span.set_data - - # for proxies, these point to the proxy host/port - if tunnel_host: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) - set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) - - rv = real_putrequest(self, method, url, *args, **kwargs) - - if should_propagate_trace(client, real_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=key, value=value, real_url=real_url - ) - ) - self.putheader(key, value) - - self._sentrysdk_span = span # type: ignore[attr-defined] - - return rv - - def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": - span = getattr(self, "_sentrysdk_span", None) - - if span is None: - return real_getresponse(self, *args, **kwargs) - - try: - rv = real_getresponse(self, *args, **kwargs) - except BaseException: - _complete_span(span) - raise - - if isinstance(span, StreamedSpan): - status_code = int(rv.status) - span.status = "error" if status_code >= 400 else "ok" - span.set_attribute("http.response.status_code", status_code) - else: - span.set_http_status(int(rv.status)) - span.set_data("reason", rv.reason) - - # getresponse doesn't include actually reading the response body. This - # is done in read(). So if the metadata/headers suggest there's a body to - # read, don't finish the span just yet, but save it for ending it later. - has_body = rv.chunked or (rv.length is not None and rv.length > 0) - if has_body: - rv._sentrysdk_span = span # type: ignore[attr-defined] - else: - _complete_span(span) - - return rv - - def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": - try: - return real_read(self, *args, **kwargs) - finally: - span = getattr(self, "_sentrysdk_span", None) - # read() might be called multiple times to consume a single body, - # so we can't just end the span when read() is done. Instead, - # try to figure out whether the response body has been fully read. - if span and (self.fp is None or self.closed): - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - def close(self: "HTTPResponse") -> None: - # We patch close() as a best effort fallback in case the span is not - # ended yet in getresponse() or read(). - - try: - real_close(self) - finally: - span = getattr(self, "_sentrysdk_span", None) - if span is not None: - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - HTTPConnection.putrequest = putrequest # type: ignore[method-assign] - HTTPConnection.getresponse = getresponse # type: ignore[method-assign] - HTTPResponse.read = read # type: ignore[method-assign] - HTTPResponse.close = close # type: ignore[assignment,method-assign] - - -def _init_argument( - args: "List[Any]", - kwargs: "Dict[Any, Any]", - name: str, - position: int, - setdefault_callback: "Optional[Callable[[Any], Any]]" = None, -) -> "Any": - """ - given (*args, **kwargs) of a function call, retrieve (and optionally set a - default for) an argument by either name or position. - - This is useful for wrapping functions with complex type signatures and - extracting a few arguments without needing to redefine that function's - entire type signature. - """ - - if name in kwargs: - rv = kwargs[name] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - kwargs[name] = rv - elif position < len(args): - rv = args[position] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - args[position] = rv - else: - rv = setdefault_callback and setdefault_callback(None) - if rv is not None: - kwargs[name] = rv - - return rv - - -def _install_subprocess() -> None: - old_popen_init = subprocess.Popen.__init__ - - @ensure_integration_enabled(StdlibIntegration, old_popen_init) - def sentry_patched_popen_init( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> None: - # Convert from tuple to list to be able to set values. - a = list(a) - - args = _init_argument(a, kw, "args", 0) or [] - cwd = _init_argument(a, kw, "cwd", 9) - - # if args is not a list or tuple (and e.g. some iterator instead), - # let's not use it at all. There are too many things that can go wrong - # when trying to collect an iterator into a list and setting that list - # into `a` again. - # - # Also invocations where `args` is not a sequence are not actually - # legal. They just happen to work under CPython. - description = None - - if isinstance(args, (list, tuple)) and len(args) < 100: - with capture_internal_exceptions(): - description = " ".join(map(str, args)) - - if description is None: - description = safe_repr(args) - - env = None - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.SUBPROCESS, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.SUBPROCESS, - name=description, - origin="auto.subprocess.stdlib.subprocess", - ) - - with span: - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - if env is None: - env = _init_argument( - a, - kw, - "env", - 10, - lambda x: dict(x if x is not None else os.environ), - ) - env["SUBPROCESS_" + k.upper().replace("-", "_")] = v - - if cwd and isinstance(span, Span): - span.set_data("subprocess.cwd", cwd) - - rv = old_popen_init(self, *a, **kw) - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - else: - span.set_tag("subprocess.pid", self.pid) - - return rv - - subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore - - old_popen_wait = subprocess.Popen.wait - - @ensure_integration_enabled(StdlibIntegration, old_popen_wait) - def sentry_patched_popen_wait( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_WAIT, - attributes={ - "sentry.op": OP.SUBPROCESS_WAIT, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_wait(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_WAIT, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_wait(self, *a, **kw) - - subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore - - old_popen_communicate = subprocess.Popen.communicate - - @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) - def sentry_patched_popen_communicate( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_COMMUNICATE, - attributes={ - "sentry.op": OP.SUBPROCESS_COMMUNICATE, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_communicate(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_COMMUNICATE, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_communicate(self, *a, **kw) - - subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore - - -def get_subprocess_traceparent_headers() -> "EnvironHeaders": - return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py deleted file mode 100644 index 5f00e8bf6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/strawberry.py +++ /dev/null @@ -1,493 +0,0 @@ -import functools -import hashlib -import warnings -from inspect import isawaitable - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from functools import cached_property -except ImportError: - # The strawberry integration requires Python 3.8+. functools.cached_property - # was added in 3.8, so this check is technically not needed, but since this - # is an auto-enabling integration, we might get to executing this import in - # lower Python versions, so we need to deal with it. - raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") - -try: - from strawberry import Schema - from strawberry.extensions import SchemaExtension - from strawberry.extensions.tracing.utils import ( - should_skip_tracing as strawberry_should_skip_tracing, - ) - from strawberry.http import async_base_view, sync_base_view -except ImportError: - raise DidNotEnable("strawberry-graphql is not installed") - -try: - from strawberry.extensions.tracing import ( - SentryTracingExtension as StrawberrySentryAsyncExtension, - ) - from strawberry.extensions.tracing import ( - SentryTracingExtensionSync as StrawberrySentrySyncExtension, - ) -except ImportError: - StrawberrySentryAsyncExtension = None - StrawberrySentrySyncExtension = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Generator, List, Optional - - from graphql import GraphQLError, GraphQLResolveInfo - from strawberry.http import GraphQLHTTPResponse - from strawberry.types import ExecutionContext - - from sentry_sdk._types import Event, EventProcessor - - -ignore_logger("strawberry.execution") - - -class StrawberryIntegration(Integration): - identifier = "strawberry" - origin = f"auto.graphql.{identifier}" - - def __init__(self, async_execution: "Optional[bool]" = None) -> None: - if async_execution not in (None, False, True): - raise ValueError( - 'Invalid value for async_execution: "{}" (must be bool)'.format( - async_execution - ) - ) - self.async_execution = async_execution - - @staticmethod - def setup_once() -> None: - version = package_version("strawberry-graphql") - _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") - - _patch_schema_init() - _patch_views() - - -def _patch_schema_init() -> None: - old_schema_init = Schema.__init__ - - @functools.wraps(old_schema_init) - def _sentry_patched_schema_init( - self: "Schema", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) - if integration is None: - return old_schema_init(self, *args, **kwargs) - - extensions = kwargs.get("extensions") or [] - - should_use_async_extension: "Optional[bool]" = None - if integration.async_execution is not None: - should_use_async_extension = integration.async_execution - else: - # try to figure it out ourselves - should_use_async_extension = _guess_if_using_async(extensions) - - if should_use_async_extension is None: - warnings.warn( - "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", - stacklevel=2, - ) - should_use_async_extension = False - - # remove the built in strawberry sentry extension, if present - extensions = [ - extension - for extension in extensions - if extension - not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) - ] - - # add our extension - extensions = [ - SentryAsyncExtension if should_use_async_extension else SentrySyncExtension - ] + extensions - - kwargs["extensions"] = extensions - - return old_schema_init(self, *args, **kwargs) - - Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] - - -class SentryAsyncExtension(SchemaExtension): - def __init__( - self: "Any", - *, - execution_context: "Optional[ExecutionContext]" = None, - ) -> None: - if execution_context: - self.execution_context = execution_context - - @cached_property - def _resource_name(self) -> str: - query_hash = self.hash_query(self.execution_context.query) # type: ignore - - if self.execution_context.operation_name: - return "{}:{}".format(self.execution_context.operation_name, query_hash) - - return query_hash - - def hash_query(self, query: str) -> str: - return hashlib.md5(query.encode("utf-8")).hexdigest() - - def on_operation(self) -> "Generator[None, None, None]": - operation_name = self.execution_context.operation_name - - operation_type = "query" - op = OP.GRAPHQL_QUERY - - if self.execution_context.query is None: - self.execution_context.query = "" - - if self.execution_context.query.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif self.execution_context.query.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - description = operation_type - if operation_name: - description += " {}".format(operation_name) - - sentry_sdk.add_breadcrumb( - category="graphql.operation", - data={ - "operation_name": operation_name, - "operation_type": operation_type, - }, - ) - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_request_event_processor(self.execution_context) - scope.add_event_processor(event_processor) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - additional_attributes: "dict[str, Any]" = {} - - if should_send_default_pii(): - additional_attributes["graphql.document"] = self.execution_context.query - - if operation_name: - additional_attributes["graphql.operation.name"] = operation_name - - graphql_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": op, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - graphql_span = sentry_sdk.start_span( - op=op, - name=description, - origin=StrawberryIntegration.origin, - ) - graphql_span.__enter__() - - if type(graphql_span) is Span: - if should_send_default_pii(): - graphql_span.set_data("graphql.document", self.execution_context.query) - - graphql_span.set_data("graphql.operation.type", operation_type) - graphql_span.set_data("graphql.operation.name", operation_name) - # This attribute is being removed in streamed spans - graphql_span.set_data("graphql.resource_name", self._resource_name) - - yield - - if type(graphql_span) is StreamedSpan: - if self.execution_context.operation_name: - segment = graphql_span._segment - segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) - segment.set_attribute("sentry.op", op) - segment.name = self.execution_context.operation_name - elif isinstance(graphql_span, Span): - transaction = graphql_span.containing_transaction - if transaction and self.execution_context.operation_name: - transaction.name = self.execution_context.operation_name - transaction.source = TransactionSource.COMPONENT - transaction.op = op - - graphql_span.__exit__(None, None, None) - - def on_validate(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - validation_span = sentry_sdk.traces.start_span( - name="validation", - attributes={ - "sentry.op": OP.GRAPHQL_VALIDATE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - validation_span = sentry_sdk.start_span( - op=OP.GRAPHQL_VALIDATE, - name="validation", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during validation, we still need to close the span - try: - yield - finally: - if isinstance(validation_span, StreamedSpan): - validation_span.end() - else: - validation_span.finish() - - def on_parse(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - parsing_span = sentry_sdk.traces.start_span( - name="parsing", - attributes={ - "sentry.op": OP.GRAPHQL_PARSE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - parsing_span = sentry_sdk.start_span( - op=OP.GRAPHQL_PARSE, - name="parsing", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during parsing, we still need to close the span - try: - yield - finally: - if isinstance(parsing_span, StreamedSpan): - parsing_span.end() - else: - parsing_span.finish() - - def should_skip_tracing( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - info: "GraphQLResolveInfo", - ) -> bool: - return strawberry_should_skip_tracing(_next, info) - - async def _resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result - - async def resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return await self._resolve(_next, root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return await self._resolve(_next, root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return await self._resolve(_next, root, info, *args, **kwargs) - - -class SentrySyncExtension(SentryAsyncExtension): - def resolve( - self, - _next: "Callable[[Any, Any, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return _next(root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return _next(root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return _next(root, info, *args, **kwargs) - - -def _patch_views() -> None: - old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors - old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors - - def _sentry_patched_async_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_async_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - def _sentry_patched_sync_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_sync_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - @ensure_integration_enabled(StrawberryIntegration) - def _sentry_patched_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - if not errors: - return - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_response_event_processor(response_data) - scope.add_event_processor(event_processor) - - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": StrawberryIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_async_view_handle_errors - ) - sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_sync_view_handle_errors - ) - - -def _make_request_event_processor( - execution_context: "ExecutionContext", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - request_data = event.setdefault("request", {}) - request_data["api_target"] = "graphql" - - if not request_data.get("data"): - data: "dict[str, Any]" = {"query": execution_context.query} - if execution_context.variables: - data["variables"] = execution_context.variables - if execution_context.operation_name: - data["operationName"] = execution_context.operation_name - - request_data["data"] = data - - else: - try: - del event["request"]["data"] - except (KeyError, TypeError): - pass - - return event - - return inner - - -def _make_response_event_processor( - response_data: "GraphQLHTTPResponse", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - contexts = event.setdefault("contexts", {}) - contexts["response"] = {"data": response_data} - - return event - - return inner - - -def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": - if StrawberrySentryAsyncExtension in extensions: - return True - elif StrawberrySentrySyncExtension in extensions: - return False - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py deleted file mode 100644 index 4927c0e885..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/sys_exit.py +++ /dev/null @@ -1,65 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import NoReturn, Union - - -class SysExitIntegration(Integration): - """Captures sys.exit calls and sends them as events to Sentry. - - By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit - exceptions generated by sys.exit calls and send them to Sentry. - - This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and - non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. - Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. - """ - - identifier = "sys_exit" - - def __init__(self, *, capture_successful_exits: bool = False) -> None: - self._capture_successful_exits = capture_successful_exits - - @staticmethod - def setup_once() -> None: - SysExitIntegration._patch_sys_exit() - - @staticmethod - def _patch_sys_exit() -> None: - old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit - - @functools.wraps(old_exit) - def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": - # @ensure_integration_enabled ensures that this is non-None - integration = sentry_sdk.get_client().get_integration(SysExitIntegration) - if integration is None: - old_exit(__status) - - try: - old_exit(__status) - except SystemExit as e: - with capture_internal_exceptions(): - if integration._capture_successful_exits or __status not in ( - 0, - None, - ): - _capture_exception(e) - raise e - - sys.exit = sentry_patched_exit - - -def _capture_exception(exc: "SystemExit") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": SysExitIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py deleted file mode 100644 index f3ba046332..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/threading.py +++ /dev/null @@ -1,196 +0,0 @@ -import sys -import warnings -from concurrent.futures import Future, ThreadPoolExecutor -from functools import wraps -from threading import Thread, current_thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import use_isolation_scope, use_scope -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T", bound=Any) - - -class ThreadingIntegration(Integration): - identifier = "threading" - - def __init__( - self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True - ) -> None: - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - - self.propagate_scope = propagate_scope - - if propagate_hub is not None: - self.propagate_scope = propagate_hub - - @staticmethod - def setup_once() -> None: - old_start = Thread.start - - try: - from django import VERSION as django_version # noqa: N811 - except ImportError: - django_version = None - - try: - import channels # type: ignore[import-untyped] - - channels_version = channels.__version__ - except (ImportError, AttributeError): - channels_version = None - - is_async_emulated_with_threads = ( - sys.version_info < (3, 9) - and channels_version is not None - and channels_version < "4.0.0" - and django_version is not None - and django_version >= (3, 0) - and django_version < (4, 0) - ) - - @wraps(old_start) - def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return old_start(self, *a, **kw) - - if integration.propagate_scope: - if is_async_emulated_with_threads: - warnings.warn( - "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " - "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " - "Please either upgrade to Django channels 4.0+, use Django's async features " - "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", - stacklevel=2, - ) - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - - else: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - # Patching instance methods in `start()` creates a reference cycle if - # done in a naive way. See - # https://github.com/getsentry/sentry-python/pull/434 - # - # In threading module, using current_thread API will access current thread instance - # without holding it to avoid a reference cycle in an easier way. - with capture_internal_exceptions(): - new_run = _wrap_run( - isolation_scope, - current_scope, - getattr(self.run, "__func__", self.run), - ) - self.run = new_run # type: ignore - - return old_start(self, *a, **kw) - - Thread.start = sentry_start # type: ignore - ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore - ThreadPoolExecutor.submit, is_async_emulated_with_threads - ) - - -def _wrap_run( - isolation_scope_to_use: "Optional[sentry_sdk.Scope]", - current_scope_to_use: "Optional[sentry_sdk.Scope]", - old_run_func: "F", -) -> "F": - @wraps(old_run_func) - def run(*a: "Any", **kw: "Any") -> "Any": - def _run_old_run_func() -> "Any": - try: - self = current_thread() - return old_run_func(self, *a[1:], **kw) - except Exception: - reraise(*_capture_exception()) - - if isolation_scope_to_use is not None and current_scope_to_use is not None: - with use_isolation_scope(isolation_scope_to_use): - with use_scope(current_scope_to_use): - return _run_old_run_func() - else: - return _run_old_run_func() - - return run # type: ignore - - -def _wrap_threadpool_executor_submit( - func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool -) -> "Callable[..., Future[T]]": - """ - Wrap submit call to propagate scopes on task submission. - """ - - @wraps(func) - def sentry_submit( - self: "ThreadPoolExecutor", - fn: "Callable[..., T]", - *args: "Any", - **kwargs: "Any", - ) -> "Future[T]": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return func(self, fn, *args, **kwargs) - - if integration.propagate_scope and is_async_emulated_with_threads: - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - elif integration.propagate_scope: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": - if isolation_scope is not None and current_scope is not None: - with use_isolation_scope(isolation_scope): - with use_scope(current_scope): - return fn(*args, **kwargs) - - return fn(*args, **kwargs) - - return func(self, wrapped_fn, *args, **kwargs) - - return sentry_submit - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - if client.get_integration(ThreadingIntegration) is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "threading", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py deleted file mode 100644 index 859b0d0870..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/tornado.py +++ /dev/null @@ -1,320 +0,0 @@ -import contextlib -import weakref -from inspect import iscoroutinefunction - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - RequestExtractor, - _filter_headers, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from tornado import version_info as TORNADO_VERSION - from tornado.gen import coroutine - from tornado.web import HTTPError, RequestHandler -except ImportError: - raise DidNotEnable("Tornado not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.tracing import Span - - -class TornadoIntegration(Integration): - identifier = "tornado" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - - ignore_logger("tornado.access") - - old_execute = RequestHandler._execute - - awaitable = iscoroutinefunction(old_execute) - - if awaitable: - # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) - # In that case our method should be a coroutine function too - async def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - return await old_execute(self, *args, **kwargs) - - else: - - @coroutine # type: ignore - def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - result = yield from old_execute(self, *args, **kwargs) - return result - - RequestHandler._execute = sentry_execute_request_handler - - old_log_exception = RequestHandler.log_exception - - def sentry_log_exception( - self: "Any", - ty: type, - value: BaseException, - tb: "Any", - *args: "Any", - **kwargs: "Any", - ) -> "Optional[Any]": - _capture_exception(ty, value, tb) - return old_log_exception(self, ty, value, tb, *args, **kwargs) - - RequestHandler.log_exception = sentry_log_exception - - -_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" - - -@contextlib.contextmanager -def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": - integration = sentry_sdk.get_client().get_integration(TornadoIntegration) - - if integration is None: - yield - return - - weak_handler = weakref.ref(self) - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - headers = self.request.headers - - scope.clear_breadcrumbs() - processor = _make_event_processor(weak_handler) - scope.add_event_processor(processor) - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(dict(headers)) - scope.set_custom_sampling_context({"tornado_request": self.request}) - - if should_send_default_pii() and self.request.remote_ip: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_ROOT_SPAN_NAME, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": TornadoIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # Like with all other integrations, this is our - # fallback transaction in case there is no route. - # sentry_urldispatcher_resolve is responsible for - # setting a transaction name later. - name=_DEFAULT_ROOT_SPAN_NAME, - source=TransactionSource.ROUTE, - origin=TornadoIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"tornado_request": self.request}, - ) - - with span_ctx as span: - try: - yield - finally: - if type(span) is StreamedSpan: - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - self.request - ).items(): - span.set_attribute(attr, value) - - with capture_internal_exceptions(): - method = getattr(self, self.request.method.lower(), None) - if method is not None: - span_name = transaction_from_function(method) - if span_name: - span.name = span_name - span.set_attribute( - "sentry.span.source", - SegmentSource.COMPONENT, - ) - - with capture_internal_exceptions(): - status_int = self.get_status() - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) - span.status = "error" if status_int >= 400 else "ok" - - -def _get_request_attributes(request: "Any") -> "Dict[str, Any]": - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.full_url() - attributes["url.path"] = request.path - - if request.query: - attributes[SPANDATA.URL_QUERY] = request.query - - if request.protocol: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol - - if should_send_default_pii() and request.remote_ip: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip - - with capture_internal_exceptions(): - raw_data = _get_tornado_request_data(request) - body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data - if body_data is not None: - attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data - - return attributes - - -def _get_tornado_request_data( - request: "Any", -) -> "Union[Optional[str], AnnotatedValue]": - body = request.body - if not body: - return None - - if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): - return AnnotatedValue.substituted_because_over_size_limit() - - return body.decode("utf-8", "replace") - - -@ensure_integration_enabled(TornadoIntegration) -def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: - if isinstance(value, HTTPError): - return - - event, hint = event_from_exception( - (ty, value, tb), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "tornado", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - weak_handler: "Callable[[], RequestHandler]", -) -> "EventProcessor": - def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - handler = weak_handler() - if handler is None: - return event - - request = handler.request - - with capture_internal_exceptions(): - method = getattr(handler, handler.request.method.lower()) - event["transaction"] = transaction_from_function(method) or "" - event["transaction_info"] = {"source": TransactionSource.COMPONENT} - - with capture_internal_exceptions(): - extractor = TornadoRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - - request_info["url"] = "%s://%s%s" % ( - request.protocol, - request.host, - request.path, - ) - - request_info["query_string"] = request.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_ip} - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - try: - current_user = handler.current_user - except Exception: - current_user = None - - if current_user: - event.setdefault("user", {}).setdefault("is_authenticated", True) - - return event - - return tornado_processor - - -class TornadoRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return {k: v.value for k, v in self.request.cookies.items()} - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "Dict[str, Any]": - return { - k: [v.decode("latin1", "replace") for v in vs] - for k, vs in self.request.body_arguments.items() - } - - def is_json(self) -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - def files(self) -> "Dict[str, Any]": - return {k: v[0] for k, v in self.request.files.items() if v} - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py deleted file mode 100644 index 0449a8f10c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/trytond.py +++ /dev/null @@ -1,52 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.utils import ensure_integration_enabled, event_from_exception - -try: - from trytond.exceptions import TrytonException # type: ignore - from trytond.wsgi import app # type: ignore -except ImportError: - raise DidNotEnable("Trytond is not installed.") - -# TODO: trytond-worker, trytond-cron and trytond-admin intergations - - -class TrytondWSGIIntegration(Integration): - identifier = "trytond_wsgi" - origin = f"auto.http.{identifier}" - - def __init__(self) -> None: - pass - - @staticmethod - def setup_once() -> None: - app.wsgi_app = SentryWsgiMiddleware( - app.wsgi_app, - span_origin=TrytondWSGIIntegration.origin, - ) - - @ensure_integration_enabled(TrytondWSGIIntegration) - def error_handler(e: Exception) -> None: - if isinstance(e, TrytonException): - return - else: - client = sentry_sdk.get_client() - event, hint = event_from_exception( - e, - client_options=client.options, - mechanism={"type": "trytond", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - # Expected error handlers signature was changed - # when the error_handler decorator was introduced - # in Tryton-5.4 - if hasattr(app, "error_handler"): - - @app.error_handler - def _(app, request, e): # type: ignore - error_handler(e) - - else: - app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py deleted file mode 100644 index 497f0539ec..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/typer.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - -try: - import typer - from typer.main import except_hook -except ImportError: - raise DidNotEnable("Typer not installed") - - -class TyperIntegration(Integration): - identifier = "typer" - - @staticmethod - def setup_once() -> None: - typer.main.except_hook = _make_excepthook(except_hook) # type: ignore - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(TyperIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "typer", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py deleted file mode 100644 index 0316f6b88a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unleash.py +++ /dev/null @@ -1,33 +0,0 @@ -from functools import wraps -from typing import Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from UnleashClient import UnleashClient -except ImportError: - raise DidNotEnable("UnleashClient is not installed") - - -class UnleashIntegration(Integration): - identifier = "unleash" - - @staticmethod - def setup_once() -> None: - # Wrap and patch evaluation methods (class methods) - old_is_enabled = UnleashClient.is_enabled - - @wraps(old_is_enabled) - def sentry_is_enabled( - self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_is_enabled(self, feature, *args, **kwargs) - - # We have no way of knowing what type of unleash feature this is, so we have to treat - # it as a boolean / toggle feature. - add_feature_flag(feature, enabled) - - return enabled - - UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py deleted file mode 100644 index 2c7280a1f2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/unraisablehook.py +++ /dev/null @@ -1,50 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Callable - - -class UnraisablehookIntegration(Integration): - identifier = "unraisablehook" - - @staticmethod - def setup_once() -> None: - sys.unraisablehook = _make_unraisable(sys.unraisablehook) - - -def _make_unraisable( - old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", -) -> "Callable[[sys.UnraisableHookArgs], Any]": - def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: - integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_unraisablehook(unraisable) - - if unraisable.exc_value and unraisable.exc_traceback: - with capture_internal_exceptions(): - event, hint = event_from_exception( - ( - unraisable.exc_type, - unraisable.exc_value, - unraisable.exc_traceback, - ), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "unraisablehook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_unraisablehook(unraisable) - - return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py deleted file mode 100644 index e776ed915a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/integrations/wsgi.py +++ /dev/null @@ -1,427 +0,0 @@ -import sys -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._werkzeug import _get_headers, get_host -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - _filter_headers, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Iterator, - Optional, - Protocol, - Tuple, - TypeVar, - Union, - ) - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - WsgiResponseIter = TypeVar("WsgiResponseIter") - WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") - WsgiExcInfo = TypeVar("WsgiExcInfo") - - class StartResponse(Protocol): - def __call__( - self, - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, - ) -> "WsgiResponseIter": # type: ignore - pass - - -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") -_DEFAULT_TRANSACTION_NAME = "generic WSGI request" - - -def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: - return s.encode("latin1").decode(charset, errors) - - -def get_request_url( - environ: "Dict[str, str]", use_x_forwarded_for: bool = False -) -> str: - """Return the absolute URL without query string for the given WSGI - environment.""" - script_name = environ.get("SCRIPT_NAME", "").rstrip("/") - path_info = environ.get("PATH_INFO", "").lstrip("/") - path = f"{script_name}/{path_info}" - - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - return "%s://%s/%s" % ( - scheme, - get_host(environ, use_x_forwarded_for), - wsgi_decoding_dance(path).lstrip("/"), - ) - - -class SentryWsgiMiddleware: - __slots__ = ( - "app", - "use_x_forwarded_for", - "span_origin", - "http_methods_to_capture", - ) - - def __init__( - self, - app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", - use_x_forwarded_for: bool = False, - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - self.app = app - self.use_x_forwarded_for = use_x_forwarded_for - self.span_origin = span_origin - self.http_methods_to_capture = http_methods_to_capture - - def __call__( - self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "Any": - if _wsgi_middleware_applied.get(False): - return self.app(environ, start_response) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _wsgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope._name = "wsgi" - scope.add_event_processor( - _make_wsgi_event_processor( - environ, self.use_x_forwarded_for - ) - ) - - method = environ.get("REQUEST_METHOD", "").upper() - - span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None - if method in self.http_methods_to_capture: - if span_streaming: - sentry_sdk.traces.continue_trace( - dict(_get_headers(environ)) - ) - Scope.set_custom_sampling_context({"wsgi_environ": environ}) - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, client_ip - ) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_TRANSACTION_NAME, - attributes={ - "sentry.span.source": SegmentSource.ROUTE, - "sentry.origin": self.span_origin, - "sentry.op": OP.HTTP_SERVER, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - environ, - op=OP.HTTP_SERVER, - name=_DEFAULT_TRANSACTION_NAME, - source=TransactionSource.ROUTE, - origin=self.span_origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"wsgi_environ": environ}, - ) - - span_ctx = span_ctx or nullcontext() - - with span_ctx as span: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - environ, self.use_x_forwarded_for - ).items(): - span.set_attribute(attr, value) - - try: - response = self.app( - environ, - partial(_sentry_start_response, start_response, span), - ) - except BaseException: - reraise(*_capture_exception()) - finally: - _wsgi_middleware_applied.set(False) - - # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses - # is determined by a pointer equality check on the response object - # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). - # - # If we were to return a _ScopedResponse, this would cause the check to always fail - # since it's checking the files are exactly the same. - # - # To avoid this and ensure that the offloading mechanism works as expected when it's - # enabled, we check if the response is a file-like object (determined by the presence - # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, - # it would've been used in handling the file in the response). - # - # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, - # so we want to make sure we don't interfere with those either. - # - # If all conditions are met, we return the original response object directly, - # allowing uWSGI to handle it as intended. - if ( - environ.get("wsgi.file_wrapper") - and getattr(response, "fileno", None) is not None - ): - return response - - return _ScopedResponse(scope, response) - - -def _sentry_start_response( - old_start_response: "StartResponse", - span: "Optional[Union[Span, StreamedSpan]]", - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, -) -> "WsgiResponseIter": # type: ignore[type-var] - with capture_internal_exceptions(): - status_int = int(status.split(" ", 1)[0]) - if span is not None: - if isinstance(span, StreamedSpan): - span.status = "error" if status_int >= 400 else "ok" - span.set_attribute("http.response.status_code", status_int) - else: - span.set_http_status(status_int) - - if exc_info is None: - # The Django Rest Framework WSGI test client, and likely other - # (incorrect) implementations, cannot deal with the exc_info argument - # if one is present. Avoid providing a third argument if not necessary. - return old_start_response(status, response_headers) - else: - return old_start_response(status, response_headers, exc_info) - - -def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns our explicitly included environment variables we want to - capture (server name, port and remote addr if pii is enabled). - """ - keys = ["SERVER_NAME", "SERVER_PORT"] - if should_send_default_pii(): - # make debugging of proxy setup easier. Proxy headers are - # in headers. - keys += ["REMOTE_ADDR"] - - for key in keys: - if key in environ: - yield key, environ[key] - - -def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": - """ - Infer the user IP address from various headers. This cannot be used in - security sensitive situations since the value may be forged from a client, - but it's good enough for the event payload. - """ - try: - return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return environ["HTTP_X_REAL_IP"] - except KeyError: - pass - - return environ.get("REMOTE_ADDR") - - -def _capture_exception() -> "ExcInfo": - """ - Captures the current exception and sends it to Sentry. - Returns the ExcInfo tuple to it can be reraised afterwards. - """ - exc_info = sys.exc_info() - e = exc_info[1] - - # SystemExit(0) is the only uncaught exception that is expected behavior - should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) - if not should_skip_capture: - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "wsgi", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class _ScopedResponse: - """ - Users a separate scope for each response chunk. - - This will make WSGI apps more tolerant against: - - WSGI servers streaming responses from a different thread/from - different threads than the one that called start_response - - close() not being called - - WSGI servers streaming responses interleaved from the same thread - """ - - __slots__ = ("_response", "_scope") - - def __init__( - self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" - ) -> None: - self._scope = scope - self._response = response - - def __iter__(self) -> "Iterator[bytes]": - iterator = iter(self._response) - - while True: - with use_isolation_scope(self._scope): - try: - chunk = next(iterator) - except StopIteration: - break - except BaseException: - reraise(*_capture_exception()) - - yield chunk - - def close(self) -> None: - with use_isolation_scope(self._scope): - try: - self._response.close() # type: ignore - except AttributeError: - pass - except BaseException: - reraise(*_capture_exception()) - - -def _make_wsgi_event_processor( - environ: "Dict[str, str]", use_x_forwarded_for: bool -) -> "EventProcessor": - # It's a bit unfortunate that we have to extract and parse the request data - # from the environ so eagerly, but there are a few good reasons for this. - # - # We might be in a situation where the scope never gets torn down - # properly. In that case we will have an unnecessary strong reference to - # all objects in the environ (some of which may take a lot of memory) when - # we're really just interested in a few of them. - # - # Keeping the environment around for longer than the request lifecycle is - # also not necessarily something uWSGI can deal with: - # https://github.com/unbit/uwsgi/issues/1950 - - client_ip = get_client_ip(environ) - request_url = get_request_url(environ, use_x_forwarded_for) - query_string = environ.get("QUERY_STRING") - method = environ.get("REQUEST_METHOD") - env = dict(_get_environ(environ)) - headers = _filter_headers(dict(_get_headers(environ))) - - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - # if the code below fails halfway through we at least have some data - request_info = event.setdefault("request", {}) - - if should_send_default_pii(): - user_info = event.setdefault("user", {}) - if client_ip: - user_info.setdefault("ip_address", client_ip) - - request_info["url"] = request_url - request_info["query_string"] = query_string - request_info["method"] = method - request_info["env"] = env - request_info["headers"] = headers - - return event - - return event_processor - - -def _get_request_attributes( - environ: "Dict[str, str]", - use_x_forwarded_for: bool = False, -) -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from the WSGI environ. - """ - attributes: "dict[str, Any]" = {} - - method = environ.get("REQUEST_METHOD") - if method: - attributes["http.request.method"] = method.upper() - - headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - url_scheme = environ.get("wsgi.url_scheme") - if url_scheme: - attributes["network.protocol.name"] = url_scheme - - server_name = environ.get("SERVER_NAME") - if server_name: - attributes["server.address"] = server_name - - server_port = environ.get("SERVER_PORT") - if server_port: - try: - attributes["server.port"] = int(server_port) - except ValueError: - pass - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - attributes["client.address"] = client_ip - - query_string = environ.get("QUERY_STRING") - if query_string: - attributes["http.query"] = query_string - - path = environ.get("PATH_INFO", "") - if path: - attributes["url.path"] = path - - attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py deleted file mode 100644 index d7f4425dfd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/logger.py +++ /dev/null @@ -1,88 +0,0 @@ -# NOTE: this is the logger sentry exposes to users, not some generic logger. -import functools -import time -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk.utils import capture_internal_exceptions, format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes - - -OTEL_RANGES = [ - # ((severity level range), severity text) - # https://opentelemetry.io/docs/specs/otel/logs/data-model - ((1, 4), "trace"), - ((5, 8), "debug"), - ((9, 12), "info"), - ((13, 16), "warn"), - ((17, 20), "error"), - ((21, 24), "fatal"), -] - - -class _dict_default_key(dict): # type: ignore[type-arg] - """dict that returns the key if missing.""" - - def __missing__(self, key: str) -> str: - return "{" + key + "}" - - -def _capture_log( - severity_text: str, severity_number: int, template: str, **kwargs: "Any" -) -> None: - body = template - - attributes: "Attributes" = {} - - if "attributes" in kwargs: - provided_attributes = kwargs.pop("attributes") or {} - for attribute, value in provided_attributes.items(): - attributes[attribute] = format_attribute(value) - - for k, v in kwargs.items(): - attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) - - if kwargs: - # only attach template if there are parameters - attributes["sentry.message.template"] = format_attribute(template) - - with capture_internal_exceptions(): - body = template.format_map(_dict_default_key(kwargs)) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": severity_text, - "severity_number": severity_number, - "attributes": attributes, - "body": body, - "time_unix_nano": time.time_ns(), - "trace_id": None, - "span_id": None, - } - ) - - -trace = functools.partial(_capture_log, "trace", 1) -debug = functools.partial(_capture_log, "debug", 5) -info = functools.partial(_capture_log, "info", 9) -warning = functools.partial(_capture_log, "warn", 13) -error = functools.partial(_capture_log, "error", 17) -fatal = functools.partial(_capture_log, "fatal", 21) - - -def _otel_severity_text(otel_severity_number: int) -> str: - for (lower, upper), severity in OTEL_RANGES: - if lower <= otel_severity_number <= upper: - return severity - - return "default" - - -def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": - for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): - if level >= py_level: - return otel_severity_number, _otel_severity_text(otel_severity_number) - - return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py deleted file mode 100644 index 27b7468c0d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/metrics.py +++ /dev/null @@ -1,62 +0,0 @@ -import time -from typing import TYPE_CHECKING, Any, Optional - -import sentry_sdk -from sentry_sdk.utils import format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes, Metric, MetricType - - -def _capture_metric( - name: str, - metric_type: "MetricType", - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[Attributes]" = None, -) -> None: - attrs: "Attributes" = {} - - if attributes: - for k, v in attributes.items(): - attrs[k] = format_attribute(v) - - metric: "Metric" = { - "timestamp": time.time(), - "trace_id": None, - "span_id": None, - "name": name, - "type": metric_type, - "value": float(value), - "unit": unit, - "attributes": attrs, - } - - sentry_sdk.get_current_scope()._capture_metric(metric) - - -def count( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "counter", value, unit, attributes) - - -def gauge( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "gauge", value, unit, attributes) - - -def distribution( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py deleted file mode 100644 index d2ba298c35..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/monitor.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import time -import weakref -from threading import Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -MAX_DOWNSAMPLE_FACTOR = 10 - - -class Monitor: - """ - Performs health checks in a separate thread once every interval seconds - and updates the internal state. Other parts of the SDK only read this state - and act accordingly. - """ - - name = "sentry.monitor" - - _thread: "Optional[Thread]" - _thread_for_pid: "Optional[int]" - - def __init__( - self, transport: "sentry_sdk.transport.Transport", interval: float = 10 - ) -> None: - self.transport: "sentry_sdk.transport.Transport" = transport - self.interval: float = interval - - self._healthy = True - self._downsample_factor: int = 0 - self._running = True - self._reset_thread_state() - - # See https://github.com/getsentry/sentry-python/issues/6148. - # If os.fork() runs while another thread holds self._thread_lock, - # the child inherits the lock locked but the holding thread does - # not exist in the child, so the lock can never be released and - # _ensure_running deadlocks forever. Reinitialise the lock and - # cached thread/pid in the child so it starts clean regardless - # of inherited state. We bind via a WeakMethod so the - # permanently-registered fork handler does not pin this Monitor - # (and its Transport): register_at_fork has no unregister API. - # POSIX-only; Windows uses spawn. - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._thread = None - self._thread_lock = Lock() - self._thread_for_pid = None - - def _ensure_running(self) -> None: - """ - Check that the monitor has an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - while self._running: - time.sleep(self.interval) - if self._running: - self.run() - - thread = Thread(name=self.name, target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def run(self) -> None: - self.check_health() - self.set_downsample_factor() - - def set_downsample_factor(self) -> None: - if self._healthy: - if self._downsample_factor > 0: - logger.debug( - "[Monitor] health check positive, reverting to normal sampling" - ) - self._downsample_factor = 0 - else: - if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: - self._downsample_factor += 1 - logger.debug( - "[Monitor] health check negative, downsampling with a factor of %d", - self._downsample_factor, - ) - - def check_health(self) -> None: - """ - Perform the actual health checks, - currently only checks if the transport is rate-limited. - TODO: augment in the future with more checks. - """ - self._healthy = self.transport.is_healthy() - - def is_healthy(self) -> bool: - self._ensure_running() - return self._healthy - - @property - def downsample_factor(self) -> int: - self._ensure_running() - return self._downsample_factor - - def kill(self) -> None: - self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py deleted file mode 100644 index d562405295..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -from sentry_sdk.profiler.continuous_profiler import ( - start_profile_session, - start_profiler, - stop_profile_session, - stop_profiler, -) -from sentry_sdk.profiler.transaction_profiler import ( - MAX_PROFILE_DURATION_NS, - PROFILE_MINIMUM_SAMPLES, - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - has_profiling_enabled, - setup_profiler, - teardown_profiler, -) -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - MAX_STACK_DEPTH, - extract_frame, - extract_stack, - frame_id, - get_frame_name, -) - -__all__ = [ - "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` - "start_profiler", - "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` - "stop_profiler", - # DEPRECATED: The following was re-exported for backwards compatibility. It - # will be removed from sentry_sdk.profiler in a future release. - "MAX_PROFILE_DURATION_NS", - "PROFILE_MINIMUM_SAMPLES", - "Profile", - "Scheduler", - "ThreadScheduler", - "GeventScheduler", - "has_profiling_enabled", - "setup_profiler", - "teardown_profiler", - "DEFAULT_SAMPLING_FREQUENCY", - "MAX_STACK_DEPTH", - "get_frame_name", - "extract_frame", - "extract_stack", - "frame_id", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py deleted file mode 100644 index ed525f52bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/continuous_profiler.py +++ /dev/null @@ -1,703 +0,0 @@ -import atexit -import os -import random -import sys -import threading -import time -import uuid -import warnings -from collections import deque -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.consts import VERSION -from sentry_sdk.envelope import Envelope -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - is_gevent, - logger, - now, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union - - from typing_extensions import TypedDict - - from sentry_sdk._types import ContinuousProfilerMode, SDKInfo - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "timestamp": float, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - ThreadPool = None - - -_scheduler: "Optional[ContinuousScheduler]" = None - - -def setup_continuous_profiler( - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", -) -> bool: - global _scheduler - - already_initialized = _scheduler is not None - - if already_initialized: - logger.debug("[Profiling] Continuous Profiler is already setup") - teardown_continuous_profiler() - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventContinuousScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventContinuousScheduler.mode - else: - default_profiler_mode = ThreadContinuousScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - # TODO: deprecate this and just use the existing `profiler_mode` - experiments = options.get("_experiments", {}) - - profiler_mode = ( - experiments.get("continuous_profiling_mode") or default_profiler_mode - ) - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if profiler_mode == ThreadContinuousScheduler.mode: - _scheduler = ThreadContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - elif profiler_mode == GeventContinuousScheduler.mode: - _scheduler = GeventContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - else: - raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up continuous profiler in {mode} mode".format( - mode=_scheduler.mode - ) - ) - - if not already_initialized: - atexit.register(teardown_continuous_profiler) - - return True - - -def is_profile_session_sampled() -> bool: - if _scheduler is None: - return False - return _scheduler.sampled - - -def try_autostart_continuous_profiler() -> None: - # TODO: deprecate this as it'll be replaced by the auto lifecycle option - - if _scheduler is None: - return - - if not _scheduler.is_auto_start_enabled(): - return - - _scheduler.manual_start() - - -def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": - if _scheduler is None: - return None - - return _scheduler.auto_start() - - -def start_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_start() - - -def start_profile_session() -> None: - warnings.warn( - "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - start_profiler() - - -def stop_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_stop() - - -def stop_profile_session() -> None: - warnings.warn( - "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - stop_profiler() - - -def teardown_continuous_profiler() -> None: - stop_profiler() - - global _scheduler - _scheduler = None - - -def get_profiler_id() -> "Union[str, None]": - if _scheduler is None: - return None - return _scheduler.profiler_id - - -def determine_profile_session_sampling_decision( - sample_rate: "Union[float, None]", -) -> bool: - # `None` is treated as `0.0` - if not sample_rate: - return False - - return random.random() < float(sample_rate) - - -class ContinuousProfile: - active: bool = True - - def stop(self) -> None: - self.active = False - - -class ContinuousScheduler: - mode: "ContinuousProfilerMode" = "unknown" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.interval = 1.0 / frequency - self.options = options - self.sdk_info = sdk_info - self.capture_func = capture_func - - self.lifecycle = self.options.get("profile_lifecycle") - profile_session_sample_rate = self.options.get("profile_session_sample_rate") - self.sampled = determine_profile_session_sampling_decision( - profile_session_sample_rate - ) - - self.sampler = self.make_sampler() - self.buffer: "Optional[ProfileBuffer]" = None - self.pid: "Optional[int]" = None - - self.running = False - self.soft_shutdown = False - - self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) - self.active_profiles: "Set[ContinuousProfile]" = set() - - def is_auto_start_enabled(self) -> bool: - # Ensure that the scheduler only autostarts once per process. - # This is necessary because many web servers use forks to spawn - # additional processes. And the profiler is only spawned on the - # master process, then it often only profiles the main process - # and not the ones where the requests are being handled. - if self.pid == os.getpid(): - return False - - experiments = self.options.get("_experiments") - if not experiments: - return False - - return experiments.get("continuous_profiling_auto_start") - - def auto_start(self) -> "Union[ContinuousProfile, None]": - if not self.sampled: - return None - - if self.lifecycle != "trace": - return None - - logger.debug("[Profiling] Auto starting profiler") - - profile = ContinuousProfile() - - self.new_profiles.append(profile) - self.ensure_running() - - return profile - - def manual_start(self) -> None: - if not self.sampled: - return - - if self.lifecycle != "manual": - return - - self.ensure_running() - - def manual_stop(self) -> None: - if self.lifecycle != "manual": - return - - self.teardown() - - def ensure_running(self) -> None: - raise NotImplementedError - - def teardown(self) -> None: - raise NotImplementedError - - def pause(self) -> None: - raise NotImplementedError - - def reset_buffer(self) -> None: - self.buffer = ProfileBuffer( - self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func - ) - - @property - def profiler_id(self) -> "Union[str, None]": - if not self.running or self.buffer is None: - return None - return self.buffer.profiler_id - - def make_sampler(self) -> "Callable[..., bool]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - if self.lifecycle == "trace": - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - return True - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - inactive_profiles = [] - - for profile in self.active_profiles: - if not profile.active: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - else: - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - return _sample_stack - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.soft_shutdown = self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # the soft shutdown happens here to give it a chance - # for the profiler to be reused - if self.soft_shutdown: - self.running = False - - # make sure to explicitly exit the profiler here or there might - # be multiple profilers at once - break - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - buffer = self.buffer - if buffer is not None: - buffer.flush() - - -class ThreadContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ContinuousProfilerMode" = "thread" - name = "sentry.profiler.ThreadContinuousScheduler" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[threading.Thread]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -class GeventContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ContinuousProfilerMode" = "gevent" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[_ThreadPool]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -PROFILE_BUFFER_SECONDS = 60 - - -class ProfileBuffer: - def __init__( - self, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - buffer_size: int, - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.options = options - self.sdk_info = sdk_info - self.buffer_size = buffer_size - self.capture_func = capture_func - - self.profiler_id = uuid.uuid4().hex - self.chunk = ProfileChunk() - - # Make sure to use the same clock to compute a sample's monotonic timestamp - # to ensure the timestamps are correctly aligned. - self.start_monotonic_time = now() - - # Make sure the start timestamp is defined only once per profiler id. - # This prevents issues with clock drift within a single profiler session. - # - # Subtracting the start_monotonic_time here to find a fixed starting position - # for relative monotonic timestamps for each sample. - self.start_timestamp = ( - datetime.now(timezone.utc).timestamp() - self.start_monotonic_time - ) - - def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: - if self.should_flush(monotonic_time): - self.flush() - self.chunk = ProfileChunk() - self.start_monotonic_time = now() - - self.chunk.write(self.start_timestamp + monotonic_time, sample) - - def should_flush(self, monotonic_time: float) -> bool: - # If the delta between the new monotonic time and the start monotonic time - # exceeds the buffer size, it means we should flush the chunk - return monotonic_time - self.start_monotonic_time >= self.buffer_size - - def flush(self) -> None: - chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) - envelope = Envelope() - envelope.add_profile_chunk(chunk) - self.capture_func(envelope) - - -class ProfileChunk: - def __init__(self) -> None: - self.chunk_id = uuid.uuid4().hex - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - def write(self, ts: float, sample: "ExtractedSample") -> None: - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "timestamp": ts, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def to_json( - self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" - ) -> "Dict[str, Any]": - profile = { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - }, - } - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - payload = { - "chunk_id": self.chunk_id, - "client_sdk": { - "name": sdk_info["name"], - "version": VERSION, - }, - "platform": "python", - "profile": profile, - "profiler_id": profiler_id, - "version": "2", - } - - for key in "release", "environment", "dist": - if options[key] is not None: - payload[key] = str(options[key]).strip() - - return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py deleted file mode 100644 index fe65774c0b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/transaction_profiler.py +++ /dev/null @@ -1,802 +0,0 @@ -""" -This file is originally based on code from https://github.com/nylas/nylas-perftools, -which is published under the following license: - -The MIT License (MIT) - -Copyright (c) 2014 Nylas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import atexit -import os -import platform -import random -import sys -import threading -import time -import uuid -import warnings -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - capture_internal_exceptions, - get_current_thread_meta, - is_gevent, - is_valid_sample_rate, - logger, - nanosecond_time, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type - - from typing_extensions import TypedDict - - from sentry_sdk._types import Event, ProfilerMode, SamplingContext - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - ProcessedThreadMetadata, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "elapsed_since_start_ns": str, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - ProcessedProfile = TypedDict( - "ProcessedProfile", - { - "frames": List[ProcessedFrame], - "stacks": List[ProcessedStack], - "samples": List[ProcessedSample], - "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - - ThreadPool = None - - -_scheduler: "Optional[Scheduler]" = None - - -# The minimum number of unique samples that must exist in a profile to be -# considered valid. -PROFILE_MINIMUM_SAMPLES = 2 - - -def has_profiling_enabled(options: "Dict[str, Any]") -> bool: - profiles_sampler = options["profiles_sampler"] - if profiles_sampler is not None: - return True - - profiles_sample_rate = options["profiles_sample_rate"] - if profiles_sample_rate is not None and profiles_sample_rate > 0: - return True - - profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") - if profiles_sample_rate is not None: - logger.warning( - "_experiments['profiles_sample_rate'] is deprecated. " - "Please use the non-experimental profiles_sample_rate option " - "directly." - ) - if profiles_sample_rate > 0: - return True - - return False - - -def setup_profiler(options: "Dict[str, Any]") -> bool: - global _scheduler - - if _scheduler is not None: - logger.debug("[Profiling] Profiler is already setup") - return False - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventScheduler.mode - else: - default_profiler_mode = ThreadScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - profiler_mode = options.get("_experiments", {}).get("profiler_mode") - if profiler_mode is not None: - logger.warning( - "_experiments['profiler_mode'] is deprecated. Please use the " - "non-experimental profiler_mode option directly." - ) - profiler_mode = profiler_mode or default_profiler_mode - - if ( - profiler_mode == ThreadScheduler.mode - # for legacy reasons, we'll keep supporting sleep mode for this scheduler - or profiler_mode == "sleep" - ): - _scheduler = ThreadScheduler(frequency=frequency) - elif profiler_mode == GeventScheduler.mode: - _scheduler = GeventScheduler(frequency=frequency) - else: - raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) - ) - _scheduler.setup() - - atexit.register(teardown_profiler) - - return True - - -def teardown_profiler() -> None: - global _scheduler - - if _scheduler is not None: - _scheduler.teardown() - - _scheduler = None - - -MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds - - -class Profile: - def __init__( - self, - sampled: "Optional[bool]", - start_ns: int, - hub: "Optional[sentry_sdk.Hub]" = None, - scheduler: "Optional[Scheduler]" = None, - ) -> None: - self.scheduler = _scheduler if scheduler is None else scheduler - - self.event_id: str = uuid.uuid4().hex - - self.sampled: "Optional[bool]" = sampled - - # Various framework integrations are capable of overwriting the active thread id. - # If it is set to `None` at the end of the profile, we fall back to the default. - self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 - self.active_thread_id: "Optional[int]" = None - - try: - self.start_ns: int = start_ns - except AttributeError: - self.start_ns = 0 - - self.stop_ns: int = 0 - self.active: bool = False - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - self.unique_samples = 0 - - # Backwards compatibility with the old hub property - self._hub: "Optional[sentry_sdk.Hub]" = None - if hub is not None: - self._hub = hub - warnings.warn( - "The `hub` parameter is deprecated. Please do not use it.", - DeprecationWarning, - stacklevel=2, - ) - - def update_active_thread_id(self) -> None: - self.active_thread_id = get_current_thread_meta()[0] - logger.debug( - "[Profiling] updating active thread id to {tid}".format( - tid=self.active_thread_id - ) - ) - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the profile's sampling decision according to the following - precedence rules: - - 1. If the transaction to be profiled is not sampled, that decision - will be used, regardless of anything else. - - 2. Use `profiles_sample_rate` to decide. - """ - - # The corresponding transaction was not sampled, - # so don't generate a profile for it. - if not self.sampled: - logger.debug( - "[Profiling] Discarding profile because transaction is discarded." - ) - self.sampled = False - return - - # The profiler hasn't been properly initialized. - if self.scheduler is None: - logger.debug( - "[Profiling] Discarding profile because profiler was not started." - ) - self.sampled = False - return - - client = sentry_sdk.get_client() - if not client.is_active(): - self.sampled = False - return - - options = client.options - - if callable(options.get("profiles_sampler")): - sample_rate = options["profiles_sampler"](sampling_context) - elif options["profiles_sample_rate"] is not None: - sample_rate = options["profiles_sample_rate"] - else: - sample_rate = options["_experiments"].get("profiles_sample_rate") - - # The profiles_sample_rate option was not set, so profiling - # was never enabled. - if sample_rate is None: - logger.debug( - "[Profiling] Discarding profile because profiling was not enabled." - ) - self.sampled = False - return - - if not is_valid_sample_rate(sample_rate, source="Profiling"): - logger.warning( - "[Profiling] Discarding profile because of invalid sample rate." - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < float(sample_rate) - - if self.sampled: - logger.debug("[Profiling] Initializing profile") - else: - logger.debug( - "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( - sample_rate=float(sample_rate) - ) - ) - - def start(self) -> None: - if not self.sampled or self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Starting profile") - self.active = True - if not self.start_ns: - self.start_ns = nanosecond_time() - self.scheduler.start_profiling(self) - - def stop(self) -> None: - if not self.sampled or not self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Stopping profile") - self.active = False - self.stop_ns = nanosecond_time() - - def __enter__(self) -> "Profile": - scope = sentry_sdk.get_isolation_scope() - old_profile = scope.profile - scope.profile = self - - self._context_manager_state = (scope, old_profile) - - self.start() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - with capture_internal_exceptions(): - self.stop() - - scope, old_profile = self._context_manager_state - del self._context_manager_state - - scope.profile = old_profile - - def write(self, ts: int, sample: "ExtractedSample") -> None: - if not self.active: - return - - if ts < self.start_ns: - return - - offset = ts - self.start_ns - if offset > MAX_PROFILE_DURATION_NS: - self.stop() - return - - self.unique_samples += 1 - - elapsed_since_start_ns = str(offset) - - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "elapsed_since_start_ns": elapsed_since_start_ns, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def process(self) -> "ProcessedProfile": - # This collects the thread metadata at the end of a profile. Doing it - # this way means that any threads that terminate before the profile ends - # will not have any metadata associated with it. - thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - } - - return { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": thread_metadata, - } - - def to_json( - self, event_opt: "Event", options: "Dict[str, Any]" - ) -> "Dict[str, Any]": - profile = self.process() - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - return { - "environment": event_opt.get("environment"), - "event_id": self.event_id, - "platform": "python", - "profile": profile, - "release": event_opt.get("release", ""), - "timestamp": event_opt["start_timestamp"], - "version": "1", - "device": { - "architecture": platform.machine(), - }, - "os": { - "name": platform.system(), - "version": platform.release(), - }, - "runtime": { - "name": platform.python_implementation(), - "version": platform.python_version(), - }, - "transactions": [ - { - "id": event_opt["event_id"], - "name": event_opt["transaction"], - # we start the transaction before the profile and this is - # the transaction start time relative to the profile, so we - # hardcode it to 0 until we can start the profile before - "relative_start_ns": "0", - # use the duration of the profile instead of the transaction - # because we end the transaction after the profile - "relative_end_ns": str(self.stop_ns - self.start_ns), - "trace_id": event_opt["contexts"]["trace"]["trace_id"], - "active_thread_id": str( - self._default_active_thread_id - if self.active_thread_id is None - else self.active_thread_id - ), - } - ], - } - - def valid(self) -> bool: - client = sentry_sdk.get_client() - if not client.is_active(): - return False - - if not has_profiling_enabled(client.options): - return False - - if self.sampled is None or not self.sampled: - if client.transport: - client.transport.record_lost_event( - "sample_rate", data_category="profile" - ) - return False - - if self.unique_samples < PROFILE_MINIMUM_SAMPLES: - if client.transport: - client.transport.record_lost_event( - "insufficient_data", data_category="profile" - ) - logger.debug("[Profiling] Discarding profile because insufficient samples.") - return False - - return True - - @property - def hub(self) -> "Optional[sentry_sdk.Hub]": - warnings.warn( - "The `hub` attribute is deprecated. Please do not access it.", - DeprecationWarning, - stacklevel=2, - ) - return self._hub - - @hub.setter - def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: - warnings.warn( - "The `hub` attribute is deprecated. Please do not set it.", - DeprecationWarning, - stacklevel=2, - ) - self._hub = value - - -class Scheduler(ABC): - mode: "ProfilerMode" = "unknown" - - def __init__(self, frequency: int) -> None: - self.interval = 1.0 / frequency - - self.sampler = self.make_sampler() - - # cap the number of new profiles at any time so it does not grow infinitely - self.new_profiles: "Deque[Profile]" = deque(maxlen=128) - self.active_profiles: "Set[Profile]" = set() - - def __enter__(self) -> "Scheduler": - self.setup() - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self.teardown() - - @abstractmethod - def setup(self) -> None: - pass - - @abstractmethod - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - """ - Ensure the scheduler is running. By default, this method is a no-op. - The method should be overridden by any implementation for which it is - relevant. - """ - return None - - def start_profiling(self, profile: "Profile") -> None: - self.ensure_running() - self.new_profiles.append(profile) - - def make_sampler(self) -> "Callable[..., None]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - def _sample_stack(*args: "Any", **kwargs: "Any") -> None: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - # make sure to clear the cache if we're not profiling so we dont - # keep a reference to the last stack of frames around - return - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - now = nanosecond_time() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - - inactive_profiles = [] - - for profile in self.active_profiles: - if profile.active: - profile.write(now, sample) - else: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - return _sample_stack - - -class ThreadScheduler(Scheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ProfilerMode" = "thread" - name = "sentry.profiler.ThreadScheduler" - - def __init__(self, frequency: int) -> None: - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[threading.Thread]" = None - self.pid: "Optional[int]" = None - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - """ - Check that the profiler has an active thread to run in, and start one if - that's not the case. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self.running - will be False after running this function. - """ - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - -class GeventScheduler(Scheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ProfilerMode" = "gevent" - name = "sentry.profiler.GeventScheduler" - - def __init__(self, frequency: int) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[_ThreadPool]" = None - self.pid: "Optional[int]" = None - - # This intentionally uses the gevent patched threading.Lock. - # The lock will be required when first trying to start profiles - # as we need to spawn the profiler thread from the greenlets. - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py deleted file mode 100644 index e9f0fec07f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/profiler/utils.py +++ /dev/null @@ -1,186 +0,0 @@ -import os -from collections import deque -from typing import TYPE_CHECKING - -from sentry_sdk._compat import PY311 -from sentry_sdk.utils import filename_for_module - -if TYPE_CHECKING: - from types import FrameType - from typing import Deque, List, Optional, Sequence, Tuple - - from typing_extensions import TypedDict - - from sentry_sdk._lru_cache import LRUCache - - ThreadId = str - - ProcessedStack = List[int] - - ProcessedFrame = TypedDict( - "ProcessedFrame", - { - "abs_path": str, - "filename": Optional[str], - "function": str, - "lineno": int, - "module": Optional[str], - }, - ) - - ProcessedThreadMetadata = TypedDict( - "ProcessedThreadMetadata", - {"name": str}, - ) - - FrameId = Tuple[ - str, # abs_path - int, # lineno - str, # function - ] - FrameIds = Tuple[FrameId, ...] - - # The exact value of this id is not very meaningful. The purpose - # of this id is to give us a compact and unique identifier for a - # raw stack that can be used as a key to a dictionary so that it - # can be used during the sampled format generation. - StackId = Tuple[int, int] - - ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] - ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] - -# The default sampling frequency to use. This is set at 101 in order to -# mitigate the effects of lockstep sampling. -DEFAULT_SAMPLING_FREQUENCY = 101 - - -# We want to impose a stack depth limit so that samples aren't too large. -MAX_STACK_DEPTH = 128 - - -if PY311: - - def get_frame_name(frame: "FrameType") -> str: - return frame.f_code.co_qualname - -else: - - def get_frame_name(frame: "FrameType") -> str: - f_code = frame.f_code - co_varnames = f_code.co_varnames - - # co_name only contains the frame name. If the frame was a method, - # the class name will NOT be included. - name = f_code.co_name - - # if it was a method, we can get the class name by inspecting - # the f_locals for the `self` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `self` if its an instance method - co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals - ): - for cls in type(frame.f_locals["self"]).__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # if it was a class method, (decorated with `@classmethod`) - # we can get the class name by inspecting the f_locals for the `cls` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `cls` if its a class method - co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals - ): - for cls in frame.f_locals["cls"].__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # nothing we can do if it is a staticmethod (decorated with @staticmethod) - - # we've done all we can, time to give up and return what we have - return name - - -def frame_id(raw_frame: "FrameType") -> "FrameId": - return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) - - -def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": - abs_path = raw_frame.f_code.co_filename - - try: - module = raw_frame.f_globals["__name__"] - except Exception: - module = None - - # namedtuples can be many times slower when initialing - # and accessing attribute so we opt to use a tuple here instead - return { - # This originally was `os.path.abspath(abs_path)` but that had - # a large performance overhead. - # - # According to docs, this is equivalent to - # `os.path.normpath(os.path.join(os.getcwd(), path))`. - # The `os.getcwd()` call is slow here, so we precompute it. - # - # Additionally, since we are using normalized path already, - # we skip calling `os.path.normpath` entirely. - "abs_path": os.path.join(cwd, abs_path), - "module": module, - "filename": filename_for_module(module, abs_path) or None, - "function": fid[2], - "lineno": raw_frame.f_lineno, - } - - -def extract_stack( - raw_frame: "Optional[FrameType]", - cache: "LRUCache", - cwd: str, - max_stack_depth: int = MAX_STACK_DEPTH, -) -> "ExtractedStack": - """ - Extracts the stack starting the specified frame. The extracted stack - assumes the specified frame is the top of the stack, and works back - to the bottom of the stack. - - In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, - only the first `MAX_STACK_DEPTH` frames will be returned. - """ - - raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) - - while raw_frame is not None: - f_back = raw_frame.f_back - raw_frames.append(raw_frame) - raw_frame = f_back - - frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) - frames = [] - for i, fid in enumerate(frame_ids): - frame = cache.get(fid) - if frame is None: - frame = extract_frame(fid, raw_frames[i], cwd) - cache.set(fid, frame) - frames.append(frame) - - # Instead of mapping the stack into frame ids and hashing - # that as a tuple, we can directly hash the stack. - # This saves us from having to generate yet another list. - # Additionally, using the stack as the key directly is - # costly because the stack can be large, so we pre-hash - # the stack, and use the hash as the key as this will be - # needed a few times to improve performance. - # - # To Reduce the likelihood of hash collisions, we include - # the stack depth. This means that only stacks of the same - # depth can suffer from hash collisions. - stack_id = len(raw_frames), hash(frame_ids) - - return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py deleted file mode 100644 index 4fd22714cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scope.py +++ /dev/null @@ -1,2185 +0,0 @@ -import os -import platform -import sys -import warnings -from collections import deque -from contextlib import contextmanager -from copy import copy, deepcopy -from datetime import datetime, timezone -from enum import Enum -from functools import wraps -from itertools import chain -from typing import TYPE_CHECKING, cast - -import sentry_sdk -from sentry_sdk._types import AnnotatedValue -from sentry_sdk.attachments import Attachment -from sentry_sdk.consts import ( - DEFAULT_MAX_BREADCRUMBS, - FALSE_VALUES, - INSTRUMENTER, - SPANDATA, -) -from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.profiler.transaction_profiler import Profile -from sentry_sdk.session import Session -from sentry_sdk.traces import ( - _DEFAULT_PARENT_SPAN, - NoOpStreamedSpan, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.tracing_utils import ( - Baggage, - PropagationContext, - _make_sampling_decision, - has_span_streaming_enabled, - has_tracing_enabled, - is_ignored_span, -) -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exception, - capture_internal_exceptions, - datetime_from_isoformat, - disable_capture_event, - event_from_exception, - exc_info_from_error, - format_attribute, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - Deque, - Dict, - Generator, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - ) - - from typing_extensions import Unpack - - import sentry_sdk - from sentry_sdk._types import ( - Attributes, - AttributeValue, - Breadcrumb, - BreadcrumbHint, - ErrorProcessor, - Event, - EventProcessor, - ExcInfo, - Hint, - Log, - LogLevelStr, - Metric, - SamplingContext, - Type, - ) - from sentry_sdk.tracing import TransactionKwargs - - P = ParamSpec("P") - R = TypeVar("R") - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T") - - -# Holds data that will be added to **all** events sent by this process. -# In case this is a http server (think web framework) with multiple users -# the data will be added to events of all users. -# Typically this is used for process wide data such as the release. -_global_scope: "Optional[Scope]" = None - -# Holds data for the active request. -# This is used to isolate data for different requests or users. -# The isolation scope is usually created by integrations, but may also -# be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) - -# Holds data for the active span. -# This can be used to manually add additional data to a span. -_current_scope = ContextVar("current_scope", default=None) - -global_event_processors: "List[EventProcessor]" = [] - -# A function returning a (trace_id, span_id) tuple -# from an external tracing source (such as otel) -_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None - - -class ScopeType(Enum): - CURRENT = "current" - ISOLATION = "isolation" - GLOBAL = "global" - MERGED = "merged" - - -class _ScopeManager: - def __init__(self, hub: "Optional[Any]" = None) -> None: - self._old_scopes: "List[Scope]" = [] - - def __enter__(self) -> "Scope": - isolation_scope = Scope.get_isolation_scope() - - self._old_scopes.append(isolation_scope) - - forked_scope = isolation_scope.fork() - _isolation_scope.set(forked_scope) - - return forked_scope - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - old_scope = self._old_scopes.pop() - _isolation_scope.set(old_scope) - - -def add_global_event_processor(processor: "EventProcessor") -> None: - global_event_processors.append(processor) - - -def register_external_propagation_context( - fn: "Callable[[], Optional[Tuple[str, str]]]", -) -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = fn - - -def remove_external_propagation_context() -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = None - - -def get_external_propagation_context() -> "Optional[Tuple[str, str]]": - return ( - _external_propagation_context_fn() if _external_propagation_context_fn else None - ) - - -def has_external_propagation_context() -> bool: - return _external_propagation_context_fn is not None - - -def _attr_setter(fn: "Any") -> "Any": - return property(fset=fn, doc=fn.__doc__) - - -def _disable_capture(fn: "F") -> "F": - @wraps(fn) - def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": - if not self._should_capture: - return - try: - self._should_capture = False - return fn(self, *args, **kwargs) - finally: - self._should_capture = True - - return wrapper # type: ignore - - -class Scope: - """The scope holds extra information that should be sent with all - events that belong to it. - """ - - # NOTE: Even though it should not happen, the scope needs to not crash when - # accessed by multiple threads. It's fine if it's full of races, but those - # races should never make the user application crash. - # - # The same needs to hold for any accesses of the scope the SDK makes. - - __slots__ = ( - "_level", - "_name", - "_fingerprint", - # note that for legacy reasons, _transaction is the transaction *name*, - # not a Transaction object (the object is stored in _span) - "_transaction", - "_transaction_info", - "_user", - "_tags", - "_contexts", - "_extras", - "_breadcrumbs", - "_n_breadcrumbs_truncated", - "_gen_ai_original_message_count", - "_gen_ai_conversation_id", - "_event_processors", - "_error_processors", - "_should_capture", - "_span", - "_session", - "_attachments", - "_force_auto_session_tracking", - "_profile", - "_propagation_context", - "client", - "_type", - "_last_event_id", - "_flags", - "_attributes", - ) - - def __init__( - self, - ty: "Optional[ScopeType]" = None, - client: "Optional[sentry_sdk.Client]" = None, - ) -> None: - self._type = ty - - self._event_processors: "List[EventProcessor]" = [] - self._error_processors: "List[ErrorProcessor]" = [] - - self._name: "Optional[str]" = None - self._propagation_context: "Optional[PropagationContext]" = None - self._n_breadcrumbs_truncated: int = 0 - self._gen_ai_original_message_count: "Dict[str, int]" = {} - - self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() - - if client is not None: - self.set_client(client) - - self.clear() - - incoming_trace_information = self._load_trace_data_from_env() - self.generate_propagation_context(incoming_data=incoming_trace_information) - - def __copy__(self) -> "Scope": - """ - Returns a copy of this scope. - This also creates a copy of all referenced data structures. - """ - rv: "Scope" = object.__new__(self.__class__) - - rv._type = self._type - rv.client = self.client - rv._level = self._level - rv._name = self._name - rv._fingerprint = self._fingerprint - rv._transaction = self._transaction - rv._transaction_info = self._transaction_info.copy() - rv._user = self._user - - rv._tags = self._tags.copy() - rv._contexts = self._contexts.copy() - rv._extras = self._extras.copy() - - rv._breadcrumbs = copy(self._breadcrumbs) - rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated - rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() - rv._event_processors = self._event_processors.copy() - rv._error_processors = self._error_processors.copy() - rv._propagation_context = self._propagation_context - - rv._should_capture = self._should_capture - rv._span = self._span - rv._session = self._session - rv._force_auto_session_tracking = self._force_auto_session_tracking - rv._attachments = self._attachments.copy() - - rv._profile = self._profile - - rv._last_event_id = self._last_event_id - - rv._flags = deepcopy(self._flags) - - rv._attributes = self._attributes.copy() - - rv._gen_ai_conversation_id = self._gen_ai_conversation_id - - return rv - - @classmethod - def get_current_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the current scope. - """ - current_scope = _current_scope.get() - if current_scope is None: - current_scope = Scope(ty=ScopeType.CURRENT) - _current_scope.set(current_scope) - - return current_scope - - @classmethod - def set_current_scope(cls, new_current_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new current scope overwriting the existing current scope. - :param new_current_scope: The scope to set as the new current scope. - """ - _current_scope.set(new_current_scope) - - @classmethod - def get_isolation_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the isolation scope. - """ - isolation_scope = _isolation_scope.get() - if isolation_scope is None: - isolation_scope = Scope(ty=ScopeType.ISOLATION) - _isolation_scope.set(isolation_scope) - - return isolation_scope - - @classmethod - def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new isolation scope overwriting the existing isolation scope. - :param new_isolation_scope: The scope to set as the new isolation scope. - """ - _isolation_scope.set(new_isolation_scope) - - @classmethod - def get_global_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the global scope. - """ - global _global_scope - if _global_scope is None: - _global_scope = Scope(ty=ScopeType.GLOBAL) - - return _global_scope - - def set_global_attributes(self) -> None: - from sentry_sdk.client import SDK_INFO - - self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) - self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) - - self.set_attribute( - "process.runtime.name", - platform.python_implementation(), - ) - self.set_attribute( - "process.runtime.version", - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", - ) - - options = sentry_sdk.get_client().options - - server_name = options.get("server_name") - if server_name: - self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) - - environment = options.get("environment") - if environment: - self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) - - release = options.get("release") - if release: - self.set_attribute(SPANDATA.SENTRY_RELEASE, release) - - @classmethod - def last_event_id(cls) -> "Optional[str]": - """ - .. versionadded:: 2.2.0 - - Returns event ID of the event most recently captured by the isolation scope, or None if no event - has been captured. We do not consider events that are dropped, e.g. by a before_send hook. - Transactions also are not considered events in this context. - - The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; - whether the event is sent depends on the transport. The event could be sent later or not at all. - Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or - various other reasons. - """ - return cls.get_isolation_scope()._last_event_id - - def _merge_scopes( - self, - additional_scope: "Optional[Scope]" = None, - additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, - ) -> "Scope": - """ - Merges global, isolation and current scope into a new scope and - adds the given additional scope or additional scope kwargs to it. - """ - if additional_scope and additional_scope_kwargs: - raise TypeError("cannot provide scope and kwargs") - - final_scope = copy(_global_scope) if _global_scope is not None else Scope() - final_scope._type = ScopeType.MERGED - - isolation_scope = _isolation_scope.get() - if isolation_scope is not None: - final_scope.update_from_scope(isolation_scope) - - current_scope = _current_scope.get() - if current_scope is not None: - final_scope.update_from_scope(current_scope) - - if self != current_scope and self != isolation_scope: - final_scope.update_from_scope(self) - - if additional_scope is not None: - if callable(additional_scope): - additional_scope(final_scope) - else: - final_scope.update_from_scope(additional_scope) - - elif additional_scope_kwargs: - final_scope.update_from_kwargs(**additional_scope_kwargs) - - return final_scope - - @classmethod - def get_client(cls) -> "sentry_sdk.client.BaseClient": - """ - .. versionadded:: 2.0.0 - - Returns the currently used :py:class:`sentry_sdk.Client`. - This checks the current scope, the isolation scope and the global scope for a client. - If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. - """ - current_scope = _current_scope.get() - try: - client = current_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - isolation_scope = _isolation_scope.get() - try: - client = isolation_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - try: - client = _global_scope.client # type: ignore - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - return NonRecordingClient() - - def set_client( - self, client: "Optional[sentry_sdk.client.BaseClient]" = None - ) -> None: - """ - .. versionadded:: 2.0.0 - - Sets the client for this scope. - - :param client: The client to use in this scope. - If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. - - """ - if client is not None: - self.client = client - # We need a client to set the initial global attributes on the global - # scope since they mostly come from client options, so populate them - # as soon as a client is set - sentry_sdk.get_global_scope().set_global_attributes() - else: - self.client = NonRecordingClient() - - def fork(self) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns a fork of this scope. - """ - forked_scope = copy(self) - return forked_scope - - def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": - """ - Load Sentry trace id and baggage from environment variables. - Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". - """ - incoming_trace_information: "Optional[Dict[str, str]]" = None - - sentry_use_environment = ( - os.environ.get("SENTRY_USE_ENVIRONMENT") or "" - ).lower() - use_environment = sentry_use_environment not in FALSE_VALUES - if use_environment: - incoming_trace_information = {} - - if os.environ.get("SENTRY_TRACE"): - incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( - os.environ.get("SENTRY_TRACE") or "" - ) - - if os.environ.get("SENTRY_BAGGAGE"): - incoming_trace_information[BAGGAGE_HEADER_NAME] = ( - os.environ.get("SENTRY_BAGGAGE") or "" - ) - - return incoming_trace_information or None - - def set_new_propagation_context(self) -> None: - """ - Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. - """ - self._propagation_context = PropagationContext() - - def generate_propagation_context( - self, incoming_data: "Optional[Dict[str, str]]" = None - ) -> None: - """ - Makes sure the propagation context is set on the scope. - If there is `incoming_data` overwrite existing propagation context. - If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. - """ - if incoming_data is not None: - self._propagation_context = PropagationContext.from_incoming_data( - incoming_data - ) - - # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring - if self._type != ScopeType.CURRENT: - if self._propagation_context is None: - self.set_new_propagation_context() - - def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": - """ - Returns the Dynamic Sampling Context from the Propagation Context. - If not existing, creates a new one. - - Deprecated: Logic moved to PropagationContext, don't use directly. - """ - if self._propagation_context is None: - return None - - return self._propagation_context.dynamic_sampling_context - - def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - """ - Returns the Sentry "sentry-trace" header (aka the traceparent) from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().to_traceparent() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return traceparent from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_traceparent() - elif not span_streaming and self.span is not None: - return self.span._to_traceparent() - - # else return traceparent from the propagation context - return self.get_active_propagation_context().to_traceparent() - - def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": - """ - Returns the Sentry "baggage" header containing trace information from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().get_baggage() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return baggage from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_baggage() - elif not span_streaming and self.span is not None: - return self.span._to_baggage() - - # else return baggage from the propagation context - return self.get_active_propagation_context().get_baggage() - - def get_trace_context(self) -> "Dict[str, Any]": - """ - Returns the Sentry "trace" context from the Propagation Context. - """ - if ( - has_tracing_enabled(self.get_client().options) - and self._span is not None - and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) - ): - return self._span._get_trace_context() - - # if we are tracing externally (otel), those values take precedence - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - trace_id, span_id = external_propagation_context - return {"trace_id": trace_id, "span_id": span_id} - - propagation_context = self.get_active_propagation_context() - - return { - "trace_id": propagation_context.trace_id, - "span_id": propagation_context.span_id, - "parent_span_id": propagation_context.parent_span_id, - "dynamic_sampling_context": propagation_context.dynamic_sampling_context, - } - - def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: - """ - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - span = kwargs.pop("span", None) - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - meta = "" - - for name, content in self.iter_trace_propagation_headers(): - meta += f'' - - return meta - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. - Deprecated: use PropagationContext.iter_headers instead. - """ - if self._propagation_context is not None: - yield from self._propagation_context.iter_headers() - - def iter_trace_propagation_headers( - self, *args: "Any", **kwargs: "Any" - ) -> "Generator[Tuple[str, str], None, None]": - """ - Return HTTP headers which allow propagation of trace data. - - If a span is given, the trace data will taken from the span. - If no span is given, the trace data is taken from the scope. - """ - client = self.get_client() - if not client.options.get("propagate_traces"): - warnings.warn( - "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", - DeprecationWarning, - stacklevel=2, - ) - return - - span = kwargs.pop("span", None) - if not span: - span_streaming = has_span_streaming_enabled(client.options) - span = self.streamed_span if span_streaming else self.span - - if ( - has_tracing_enabled(client.options) - and span is not None - and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) - ): - for header in span._iter_headers(): - yield header - elif has_external_propagation_context(): - # when we have an external_propagation_context (otlp) - # we leave outgoing propagation to the propagator - return - else: - for header in self.get_active_propagation_context().iter_headers(): - yield header - - def get_active_propagation_context(self) -> "PropagationContext": - if self._propagation_context is not None: - return self._propagation_context - - current_scope = self.get_current_scope() - if current_scope._propagation_context is not None: - return current_scope._propagation_context - - isolation_scope = self.get_isolation_scope() - # should actually never happen, but just in case someone calls scope.clear - if isolation_scope._propagation_context is None: - isolation_scope._propagation_context = PropagationContext() - return isolation_scope._propagation_context - - @classmethod - def set_custom_sampling_context( - cls, custom_sampling_context: "dict[str, Any]" - ) -> None: - cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( - custom_sampling_context - ) - - def clear(self) -> None: - """Clears the entire scope.""" - self._level: "Optional[LogLevelStr]" = None - self._fingerprint: "Optional[List[str]]" = None - self._transaction: "Optional[str]" = None - self._transaction_info: "dict[str, str]" = {} - self._user: "Optional[Dict[str, Any]]" = None - - self._tags: "Dict[str, Any]" = {} - self._contexts: "Dict[str, Dict[str, Any]]" = {} - self._extras: "dict[str, Any]" = {} - self._attachments: "List[Attachment]" = [] - - self.clear_breadcrumbs() - self._should_capture: bool = True - - self._span: "Optional[Union[Span, StreamedSpan]]" = None - self._session: "Optional[Session]" = None - self._force_auto_session_tracking: "Optional[bool]" = None - - self._profile: "Optional[Profile]" = None - - self._propagation_context = None - - # self._last_event_id is only applicable to isolation scopes - self._last_event_id: "Optional[str]" = None - self._flags: "Optional[FlagBuffer]" = None - - self._attributes: "Attributes" = {} - - self._gen_ai_conversation_id: "Optional[str]" = None - - @_attr_setter - def level(self, value: "LogLevelStr") -> None: - """ - When set this overrides the level. - - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. - - :param value: The level to set. - """ - logger.warning( - "Deprecated: use .set_level() instead. This will be removed in the future." - ) - - self._level = value - - def set_level(self, value: "LogLevelStr") -> None: - """ - Sets the level for the scope. - - :param value: The level to set. - """ - self._level = value - - @_attr_setter - def fingerprint(self, value: "Optional[List[str]]") -> None: - """When set this overrides the default fingerprint.""" - self._fingerprint = value - - @property - def transaction(self) -> "Any": - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" - - # there is no span/transaction on the scope - if self._span is None: - return None - - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - # there is an orphan span on the scope - if self._span.containing_transaction is None: - return None - - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value: "Any") -> None: - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span: - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - if self._span.containing_transaction: - self._span.containing_transaction.name = value - - def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: - """Set the transaction name and optionally the transaction source.""" - self._transaction = name - if self._span: - if isinstance(self._span, NoOpStreamedSpan): - return - - elif isinstance(self._span, StreamedSpan): - self._span._segment.name = name - if source: - self._span._segment.set_attribute( - "sentry.span.source", getattr(source, "value", source) - ) - - elif self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source - - if source: - self._transaction_info["source"] = source - - @_attr_setter - def user(self, value: "Optional[Dict[str, Any]]") -> None: - """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" - warnings.warn( - "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", - DeprecationWarning, - stacklevel=2, - ) - self.set_user(value) - - def set_user(self, value: "Optional[Dict[str, Any]]") -> None: - """Sets a user for the scope.""" - self._user = value - - session = self.get_isolation_scope()._session - if session is not None: - session.update(user=value) - - @property - def span(self) -> "Optional[Span]": - """Get/set current tracing span or transaction.""" - return self._span if isinstance(self._span, Span) else None - - @span.setter - def span(self, span: "Optional[Span]") -> None: - self._span = span - # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source - - @property - def streamed_span(self) -> "Optional[StreamedSpan]": - """Get/set current tracing span.""" - return self._span if isinstance(self._span, StreamedSpan) else None - - @streamed_span.setter - def streamed_span(self, span: "Optional[StreamedSpan]") -> None: - self._span = span - - # Also set _transaction and _transaction_info in streaming mode as this - # is used for populating events and linking them to segments - if type(span) is StreamedSpan and span._is_segment(): - self._transaction = span.name - if span._attributes.get("sentry.span.source"): - self._transaction_info["source"] = str( - span._attributes["sentry.span.source"] - ) - - @property - def profile(self) -> "Optional[Profile]": - return self._profile - - @profile.setter - def profile(self, profile: "Optional[Profile]") -> None: - self._profile = profile - - def set_tag(self, key: str, value: "Any") -> None: - """ - Sets a tag for a key to a specific value. - - :param key: Key of the tag to set. - - :param value: Value of the tag to set. - """ - self._tags[key] = value - - def set_tags(self, tags: "Mapping[str, object]") -> None: - """Sets multiple tags at once. - - This method updates multiple tags at once. The tags are passed as a dictionary - or other mapping type. - - Calling this method is equivalent to calling `set_tag` on each key-value pair - in the mapping. If a tag key already exists in the scope, its value will be - updated. If the tag key does not exist in the scope, the key-value pair will - be added to the scope. - - This method only modifies tag keys in the `tags` mapping passed to the method. - `scope.set_tags({})` is, therefore, a no-op. - - :param tags: A mapping of tag keys to tag values to set. - """ - self._tags.update(tags) - - def remove_tag(self, key: str) -> None: - """ - Removes a specific tag. - - :param key: Key of the tag to remove. - """ - self._tags.pop(key, None) - - def set_context( - self, - key: str, - value: "Dict[str, Any]", - ) -> None: - """ - Binds a context at a certain key to a specific value. - """ - self._contexts[key] = value - - def remove_context( - self, - key: str, - ) -> None: - """Removes a context.""" - self._contexts.pop(key, None) - - def set_extra( - self, - key: str, - value: "Any", - ) -> None: - """Sets an extra key to a specific value.""" - self._extras[key] = value - - def remove_extra( - self, - key: str, - ) -> None: - """Removes a specific extra key.""" - self._extras.pop(key, None) - - def set_conversation_id(self, conversation_id: str) -> None: - """ - Sets the conversation ID for gen_ai spans. - - :param conversation_id: The conversation ID to set. - """ - self._gen_ai_conversation_id = conversation_id - - def get_conversation_id(self) -> "Optional[str]": - """ - Gets the conversation ID for gen_ai spans. - - :returns: The conversation ID, or None if not set. - """ - return self._gen_ai_conversation_id - - def remove_conversation_id(self) -> None: - """Removes the conversation ID.""" - self._gen_ai_conversation_id = None - - def clear_breadcrumbs(self) -> None: - """Clears breadcrumb buffer.""" - self._breadcrumbs: "Deque[Breadcrumb]" = deque() - self._n_breadcrumbs_truncated = 0 - - def add_attachment( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - """Adds an attachment to future events sent from this scope. - - The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. - """ - self._attachments.append( - Attachment( - bytes=bytes, - path=path, - filename=filename, - content_type=content_type, - add_to_transactions=add_to_transactions, - ) - ) - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - client = self.get_client() - - if not client.is_active(): - logger.info("Dropped breadcrumb because no client bound") - return - - before_breadcrumb = client.options.get("before_breadcrumb") - max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) - - crumb = dict(crumb or ()) - crumb.update(kwargs) - if not crumb: - return - - hint = dict(hint or ()) - - if crumb.get("timestamp") is None: - crumb["timestamp"] = datetime.now(timezone.utc) - if crumb.get("type") is None: - crumb["type"] = "default" - - if before_breadcrumb is not None: - new_crumb = before_breadcrumb(crumb, hint) - else: - new_crumb = crumb - - if new_crumb is not None: - self._breadcrumbs.append(new_crumb) - else: - logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) - - while len(self._breadcrumbs) > max_breadcrumbs: - self._breadcrumbs.popleft() - self._n_breadcrumbs_truncated += 1 - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - try_autostart_continuous_profiler() - - custom_sampling_context = custom_sampling_context or {} - - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs: "TransactionKwargs" = kwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - # update the sample rate in the dsc - if transaction.sample_rate is not None: - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction._baggage: - transaction._baggage.sentry_items["sample_rate"] = str( - transaction.sample_rate - ) - - if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - - transaction._continuous_profile = try_profile_lifecycle_trace_start() - - # Typically, the profiler is set when the transaction is created. But when - # using the auto lifecycle, the profiler isn't running when the first - # transaction is started. So make sure we update the profiler id on it. - if transaction._continuous_profile is not None: - transaction.set_profiler_id(get_profiler_id()) - - # we don't bother to keep spans if we already know we're not going to - # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) - - return transaction - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - warnings.warn( - "Scope.start_span is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return NoOpSpan() - - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - # get current span or transaction - span = self.span or self.get_isolation_scope().span - if isinstance(span, StreamedSpan): - # make mypy happy - return NoOpSpan() - - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - kwargs["trace_id"] = propagation_context.trace_id - - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) - - return span - - def start_streamed_span( - self, - name: str, - attributes: "Optional[Attributes]", - parent_span: "Optional[StreamedSpan]", - active: bool, - ) -> "StreamedSpan": - # TODO: rename to start_span once we drop the old API - if isinstance(parent_span, NoOpStreamedSpan): - # parent_span is only set if the user explicitly set it - logger.debug( - "Ignored parent span provided. Span will be parented to the " - "currently active span instead." - ) - - if parent_span is _DEFAULT_PARENT_SPAN or isinstance( - parent_span, NoOpStreamedSpan - ): - parent_span = self.streamed_span - - # If no eligible parent_span was provided and there is no currently - # active span, this is a segment - if parent_span is None: - propagation_context = self.get_active_propagation_context() - - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - scope=self, - unsampled_reason="ignored", - ) - - sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( - name, - attributes, - self, - ) - - if sample_rate is not None: - self._update_sample_rate(sample_rate) - - if sampled is False: - return NoOpStreamedSpan( - scope=self, - unsampled_reason=outcome, - ) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=None, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - parent_sampled=propagation_context.parent_sampled, - baggage=propagation_context.baggage, - sample_rand=sample_rand, - sample_rate=sample_rate, - ) - - # This is a child span; take propagation context from the parent span - with new_scope(): - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - unsampled_reason="ignored", - ) - - if isinstance(parent_span, NoOpStreamedSpan): - return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=parent_span._segment, - trace_id=parent_span.trace_id, - parent_span_id=parent_span.span_id, - parent_sampled=parent_span.sampled, - ) - - def _update_sample_rate(self, sample_rate: float) -> None: - # If we had to adjust the sample rate when setting the sampling decision - # for a span, it needs to be updated in the propagation context too - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(sample_rate) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", - ) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - self.generate_propagation_context(environ_or_headers) - - # generate_propagation_context ensures that the propagation_context is not None. - propagation_context = cast(PropagationContext, self._propagation_context) - - optional_kwargs = {} - if name: - optional_kwargs["name"] = name - if source: - optional_kwargs["source"] = source - - return Transaction( - op=op, - origin=origin, - baggage=propagation_context.baggage, - parent_sampled=propagation_context.parent_sampled, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - same_process_as_parent=False, - **optional_kwargs, - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures an event. - - Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - scope = self._merge_scopes(scope, scope_kwargs) - - event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) - - if event_id is not None and event.get("type") != "transaction": - self.get_isolation_scope()._last_event_id = event_id - - return event_id - - def _capture_log(self, log: "Optional[Log]") -> None: - if log is None: - return - - client = self.get_client() - if not has_logs_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" - ) - - client._capture_log(log, scope=merged_scope) - - def _capture_metric(self, metric: "Optional[Metric]") -> None: - if metric is None: - return - - client = self.get_client() - if not has_metrics_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" - ) - - client._capture_metric(metric, scope=merged_scope) - - def _capture_span(self, span: "Optional[StreamedSpan]") -> None: - if span is None: - return - - client = self.get_client() - if not has_span_streaming_enabled(client.options): - return - - merged_scope = self._merge_scopes() - client._capture_span(span, scope=merged_scope) - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures a message. - - :param message: The string to send as the message. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if level is None: - level = "info" - - event: "Event" = { - "message": message, - "level": level, - } - - return self.capture_event(event, scope=scope, **scope_kwargs) - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """Captures an exception. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if error is not None: - exc_info = exc_info_from_error(error) - else: - exc_info = sys.exc_info() - - event, hint = event_from_exception( - exc_info, client_options=self.get_client().options - ) - - try: - return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) - except Exception: - capture_internal_exception(sys.exc_info()) - - return None - - def start_session(self, *args: "Any", **kwargs: "Any") -> None: - """Starts a new session.""" - session_mode = kwargs.pop("session_mode", "application") - - self.end_session() - - client = self.get_client() - self._session = Session( - release=client.options.get("release"), - environment=client.options.get("environment"), - user=self._user, - session_mode=session_mode, - ) - - def end_session(self, *args: "Any", **kwargs: "Any") -> None: - """Ends the current session if there is one.""" - session = self._session - self._session = None - - if session is not None: - session.close() - self.get_client().capture_session(session) - - def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: - """Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - self.end_session() - self._force_auto_session_tracking = False - - def resume_auto_session_tracking(self) -> None: - """Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - self._force_auto_session_tracking = None - - def add_event_processor( - self, - func: "EventProcessor", - ) -> None: - """Register a scope local event processor on the scope. - - :param func: This function behaves like `before_send.` - """ - if len(self._event_processors) > 20: - logger.warning( - "Too many event processors on scope! Clearing list to free up some memory: %r", - self._event_processors, - ) - del self._event_processors[:] - - self._event_processors.append(func) - - def add_error_processor( - self, - func: "ErrorProcessor", - cls: "Optional[Type[BaseException]]" = None, - ) -> None: - """Register a scope local error processor on the scope. - - :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. - - :param cls: Optionally, only process exceptions of this type. - """ - if cls is not None: - cls_ = cls # For mypy. - real_func = func - - def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": - try: - is_inst = isinstance(exc_info[1], cls_) - except Exception: - is_inst = False - if is_inst: - return real_func(event, exc_info) - return event - - self._error_processors.append(func) - - def _apply_level_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._level is not None: - event["level"] = self._level - - def _apply_breadcrumbs_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - event.setdefault("breadcrumbs", {}) - - # This check is just for mypy - - if not isinstance(event["breadcrumbs"], AnnotatedValue): - event["breadcrumbs"].setdefault("values", []) - event["breadcrumbs"]["values"].extend(self._breadcrumbs) - - # Attempt to sort timestamps - try: - if not isinstance(event["breadcrumbs"], AnnotatedValue): - for crumb in event["breadcrumbs"]["values"]: - if isinstance(crumb["timestamp"], str): - crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) - - event["breadcrumbs"]["values"].sort( - key=lambda crumb: crumb["timestamp"] - ) - except Exception as err: - logger.debug("Error when sorting breadcrumbs", exc_info=err) - pass - - def _apply_user_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("user") is None and self._user is not None: - event["user"] = self._user - - def _apply_transaction_name_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction") is None and self._transaction is not None: - event["transaction"] = self._transaction - - def _apply_transaction_info_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction_info") is None and self._transaction_info is not None: - event["transaction_info"] = self._transaction_info - - def _apply_fingerprint_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("fingerprint") is None and self._fingerprint is not None: - event["fingerprint"] = self._fingerprint - - def _apply_extra_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._extras: - event.setdefault("extra", {}).update(self._extras) - - def _apply_tags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._tags: - event.setdefault("tags", {}).update(self._tags) - - def _apply_contexts_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._contexts: - event.setdefault("contexts", {}).update(self._contexts) - - contexts = event.setdefault("contexts", {}) - - # Add "trace" context - if contexts.get("trace") is None: - contexts["trace"] = self.get_trace_context() - - def _apply_flags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - flags = self.flags.get() - if len(flags) > 0: - event.setdefault("contexts", {}).setdefault("flags", {}).update( - {"values": flags} - ) - - def _apply_scope_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - # TODO: turn Logs, Metrics into actual classes - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - for attribute, value in self._attributes.items(): - if attribute not in attributes: - attributes[attribute] = value - - def _apply_user_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - if not should_send_default_pii() or self._user is None: - return - - for attribute_name, user_attribute in ( - ("user.id", "id"), - ("user.name", "username"), - ("user.email", "email"), - ("user.ip_address", "ip_address"), - ): - if ( - user_attribute in self._user - and attribute_name not in attributes - and self._user[user_attribute] is not None - ): - attributes[attribute_name] = self._user[user_attribute] - - def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": - logger.info("%s (%s) dropped event", ty, cause) - return None - - def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the error processors on the event and returns the modified event. - """ - exc_info = hint.get("exc_info") - if exc_info is not None: - error_processors = chain( - self.get_global_scope()._error_processors, - self.get_isolation_scope()._error_processors, - self.get_current_scope()._error_processors, - ) - - for error_processor in error_processors: - new_event = error_processor(event, exc_info) - if new_event is None: - return self._drop(error_processor, "error processor") - - event = new_event - - return event - - def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the event processors on the event and returns the modified event. - """ - ty = event.get("type") - is_check_in = ty == "check_in" - - if not is_check_in: - # Get scopes without creating them to prevent infinite recursion - isolation_scope = _isolation_scope.get() - current_scope = _current_scope.get() - - event_processors = chain( - global_event_processors, - _global_scope and _global_scope._event_processors or [], - isolation_scope and isolation_scope._event_processors or [], - current_scope and current_scope._event_processors or [], - ) - - for event_processor in event_processors: - new_event = event - with capture_internal_exceptions(): - new_event = event_processor(event, hint) - if new_event is None: - return self._drop(event_processor, "event processor") - event = new_event - - return event - - @_disable_capture - def apply_to_event( - self, - event: "Event", - hint: "Hint", - options: "Optional[Dict[str, Any]]" = None, - ) -> "Optional[Event]": - """Applies the information contained on the scope to the given event.""" - ty = event.get("type") - is_transaction = ty == "transaction" - is_check_in = ty == "check_in" - - # put all attachments into the hint. This lets callbacks play around - # with attachments. We also later pull this out of the hint when we - # create the envelope. - attachments_to_send = hint.get("attachments") or [] - for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: - attachments_to_send.append(attachment) - hint["attachments"] = attachments_to_send - - self._apply_contexts_to_event(event, hint, options) - - if is_check_in: - # Check-ins only support the trace context, strip all others - event["contexts"] = { - "trace": event.setdefault("contexts", {}).get("trace", {}) - } - - if not is_check_in: - self._apply_level_to_event(event, hint, options) - self._apply_fingerprint_to_event(event, hint, options) - self._apply_user_to_event(event, hint, options) - self._apply_transaction_name_to_event(event, hint, options) - self._apply_transaction_info_to_event(event, hint, options) - self._apply_tags_to_event(event, hint, options) - self._apply_extra_to_event(event, hint, options) - - if not is_transaction and not is_check_in: - self._apply_breadcrumbs_to_event(event, hint, options) - self._apply_flags_to_event(event, hint, options) - - event = self.run_error_processors(event, hint) - if event is None: - return None - - event = self.run_event_processors(event, hint) - if event is None: - return None - - return event - - @_disable_capture - def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: - # Attributes-based events and telemetry go through here (logs, metrics, - # spansV2) - if not isinstance(telemetry, StreamedSpan): - trace_context = self.get_trace_context() - trace_id = trace_context.get("trace_id") - if telemetry.get("trace_id") is None and trace_id is not None: - telemetry["trace_id"] = trace_id - - # span_id should only be populated if there's an active span. We can't - # use the trace_context here because it synthesizes a span_id if there - # isn't one - if telemetry.get("span_id") is None: - if self._span is not None and not isinstance( - self._span, (NoOpStreamedSpan, NoOpSpan) - ): - telemetry["span_id"] = self._span.span_id - else: - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - _, span_id = external_propagation_context - if span_id is not None: - telemetry["span_id"] = span_id - - self._apply_scope_attributes_to_telemetry(telemetry) - self._apply_user_attributes_to_telemetry(telemetry) - - def update_from_scope(self, scope: "Scope") -> None: - """Update the scope with another scope's data.""" - if scope._level is not None: - self._level = scope._level - if scope._fingerprint is not None: - self._fingerprint = scope._fingerprint - if scope._transaction is not None: - self._transaction = scope._transaction - if scope._transaction_info is not None: - self._transaction_info.update(scope._transaction_info) - if scope._user is not None: - self._user = scope._user - if scope._tags: - self._tags.update(scope._tags) - if scope._contexts: - self._contexts.update(scope._contexts) - if scope._extras: - self._extras.update(scope._extras) - if scope._breadcrumbs: - self._breadcrumbs.extend(scope._breadcrumbs) - if scope._n_breadcrumbs_truncated: - self._n_breadcrumbs_truncated = ( - self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated - ) - if scope._gen_ai_original_message_count: - self._gen_ai_original_message_count.update( - scope._gen_ai_original_message_count - ) - if scope._gen_ai_conversation_id: - self._gen_ai_conversation_id = scope._gen_ai_conversation_id - if scope._span: - self._span = scope._span - if scope._attachments: - self._attachments.extend(scope._attachments) - if scope._profile: - self._profile = scope._profile - if scope._propagation_context: - self._propagation_context = scope._propagation_context - if scope._session: - self._session = scope._session - if scope._flags: - if not self._flags: - self._flags = deepcopy(scope._flags) - else: - for flag in scope._flags.get(): - self._flags.set(flag["flag"], flag["result"]) - if scope._attributes: - self._attributes.update(scope._attributes) - - def update_from_kwargs( - self, - user: "Optional[Any]" = None, - level: "Optional[LogLevelStr]" = None, - extras: "Optional[Dict[str, Any]]" = None, - contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, - tags: "Optional[Dict[str, str]]" = None, - fingerprint: "Optional[List[str]]" = None, - attributes: "Optional[Attributes]" = None, - ) -> None: - """Update the scope's attributes.""" - if level is not None: - self._level = level - if user is not None: - self._user = user - if extras is not None: - self._extras.update(extras) - if contexts is not None: - self._contexts.update(contexts) - if tags is not None: - self._tags.update(tags) - if fingerprint is not None: - self._fingerprint = fingerprint - if attributes is not None: - self._attributes.update(attributes) - - def __repr__(self) -> str: - return "<%s id=%s name=%s type=%s>" % ( - self.__class__.__name__, - hex(id(self)), - self._name, - self._type, - ) - - @property - def flags(self) -> "FlagBuffer": - if self._flags is None: - max_flags = ( - self.get_client().options["_experiments"].get("max_flags") - or DEFAULT_FLAG_CAPACITY - ) - self._flags = FlagBuffer(capacity=max_flags) - return self._flags - - def set_attribute(self, attribute: str, value: "AttributeValue") -> None: - """ - Set an attribute on the scope. - - Any attributes-based telemetry (logs, metrics) captured while this scope - is active will inherit attributes set on the scope. - """ - self._attributes[attribute] = format_attribute(value) - - def remove_attribute(self, attribute: str) -> None: - """Remove an attribute if set on the scope. No-op if there is no such attribute.""" - try: - del self._attributes[attribute] - except KeyError: - pass - - -@contextmanager -def new_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current scope and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.new_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - new_scope = current_scope.fork() - token = _current_scope.set(new_scope) - - try: - yield new_scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `scope` and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - Suppose the variable `scope` contains a `Scope` object, which is not currently - the active scope. - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.use_scope(scope): - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # set given scope as current scope - token = _current_scope.set(scope) - - try: - yield scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def isolation_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current isolation scope and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # fork isolation scope - isolation_scope = Scope.get_isolation_scope() - new_isolation_scope = isolation_scope.fork() - isolation_token = _isolation_scope.set(new_isolation_scope) - - try: - yield new_isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `isolation_scope` and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # set given scope as isolation scope - isolation_token = _isolation_scope.set(isolation_scope) - - try: - yield isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -def should_send_default_pii() -> bool: - """Shortcut for `Scope.get_client().should_send_default_pii()`.""" - return Scope.get_client().should_send_default_pii() - - -# Circular imports -from sentry_sdk.client import NonRecordingClient - -if TYPE_CHECKING: - import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py deleted file mode 100644 index 6794491325..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/scrubber.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING, Dict, List, cast - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - iter_event_frames, -) - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event - - -DEFAULT_DENYLIST = [ - # stolen from relay - "password", - "passwd", - "secret", - "api_key", - "apikey", - "auth", - "credentials", - "mysql_pwd", - "privatekey", - "private_key", - "token", - "session", - # django - "csrftoken", - "sessionid", - # wsgi - "x_csrftoken", - "x_forwarded_for", - "set_cookie", - "cookie", - "authorization", - "proxy-authorization", - "x_api_key", - # other common names used in the wild - "aiohttp_session", # aiohttp - "connect.sid", # Express - "csrf_token", # Pyramid - "csrf", # (this is a cookie name used in accepted answers on stack overflow) - "_csrf", # Express - "_csrf_token", # Bottle - "PHPSESSID", # PHP - "_session", # Sanic - "symfony", # Symfony - "user_session", # Vue - "_xsrf", # Tornado - "XSRF-TOKEN", # Angular, Laravel -] - -DEFAULT_PII_DENYLIST = [ - "x_forwarded_for", - "x_real_ip", - "ip_address", - "remote_addr", -] - - -class EventScrubber: - def __init__( - self, - denylist: "Optional[List[str]]" = None, - recursive: bool = False, - send_default_pii: bool = False, - pii_denylist: "Optional[List[str]]" = None, - ) -> None: - """ - A scrubber that goes through the event payload and removes sensitive data configured through denylists. - - :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. - :param recursive: Whether to scrub the event payload recursively, default False. - :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. - :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. - """ - self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist - - if not send_default_pii: - pii_denylist = ( - DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist - ) - self.denylist += pii_denylist - - self.denylist = [x.lower() for x in self.denylist] - self.recursive = recursive - - def scrub_list(self, lst: object) -> None: - """ - If a list is passed to this method, the method recursively searches the list and any - nested lists for any dictionaries. The method calls scrub_dict on all dictionaries - it finds. - If the parameter passed to this method is not a list, the method does nothing. - """ - if not isinstance(lst, list): - return - - for v in lst: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_dict(self, d: object) -> None: - """ - If a dictionary is passed to this method, the method scrubs the dictionary of any - sensitive data. The method calls itself recursively on any nested dictionaries ( - including dictionaries nested in lists) if self.recursive is True. - This method does nothing if the parameter passed to it is not a dictionary. - """ - if not isinstance(d, dict): - return - - for k, v in d.items(): - # The cast is needed because mypy is not smart enough to figure out that k must be a - # string after the isinstance check. - if isinstance(k, str) and k.lower() in self.denylist: - d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() - elif self.recursive: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_request(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "request" in event: - if "headers" in event["request"]: - self.scrub_dict(event["request"]["headers"]) - if "cookies" in event["request"]: - self.scrub_dict(event["request"]["cookies"]) - if "data" in event["request"]: - self.scrub_dict(event["request"]["data"]) - - def scrub_extra(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "extra" in event: - self.scrub_dict(event["extra"]) - - def scrub_user(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "user" in event: - user = event["user"] - if "ip_address" in self.denylist and isinstance(user, dict): - user.pop("ip_address", None) - self.scrub_dict(user) - - def scrub_breadcrumbs(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "breadcrumbs" in event: - if ( - not isinstance(event["breadcrumbs"], AnnotatedValue) - and "values" in event["breadcrumbs"] - ): - for value in event["breadcrumbs"]["values"]: - if "data" in value: - self.scrub_dict(value["data"]) - - def scrub_frames(self, event: "Event") -> None: - with capture_internal_exceptions(): - for frame in iter_event_frames(event): - if "vars" in frame: - self.scrub_dict(frame["vars"]) - - def scrub_spans(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "spans" in event: - for span in cast(List[Dict[str, object]], event["spans"]): - if "data" in span: - self.scrub_dict(span["data"]) - - def scrub_event(self, event: "Event") -> None: - self.scrub_request(event) - self.scrub_extra(event) - self.scrub_user(event) - self.scrub_breadcrumbs(event) - self.scrub_frames(event) - self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py deleted file mode 100644 index 6bf6f6e70b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/serializer.py +++ /dev/null @@ -1,418 +0,0 @@ -import math -import sys -from array import array -from collections.abc import Mapping -from datetime import datetime -from typing import TYPE_CHECKING - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exception, - disable_capture_event, - format_timestamp, - safe_repr, - strip_string, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union - - from sentry_sdk._types import NotImplementedType - - Span = Dict[str, Any] - - ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] - Segment = Union[str, int] - - -# Bytes are technically not strings in Python 3, but we can serialize them -serializable_str_types = (str, bytes, bytearray, memoryview) - - -# Maximum length of JSON-serialized event payloads that can be safely sent -# before the server may reject the event due to its size. This is not intended -# to reflect actual values defined server-side, but rather only be an upper -# bound for events sent by the SDK. -# -# Can be overwritten if wanting to send more bytes, e.g. with a custom server. -# When changing this, keep in mind that events may be a little bit larger than -# this value due to attached metadata, so keep the number conservative. -MAX_EVENT_BYTES = 10**6 - -# Maximum depth and breadth of databags. Excess data will be trimmed. If -# max_request_body_size is "always", request bodies won't be trimmed. -MAX_DATABAG_DEPTH = 5 -MAX_DATABAG_BREADTH = 10 -CYCLE_MARKER = "" - - -global_repr_processors: "List[ReprProcessor]" = [] - - -def add_global_repr_processor(processor: "ReprProcessor") -> None: - global_repr_processors.append(processor) - - -sequence_types: "List[type]" = [tuple, list, set, frozenset, array] - - -def add_repr_sequence_type(ty: type) -> None: - sequence_types.append(ty) - - -class Memo: - __slots__ = ("_ids", "_objs") - - def __init__(self) -> None: - self._ids: "Dict[int, Any]" = {} - self._objs: "List[Any]" = [] - - def memoize(self, obj: "Any") -> "ContextManager[bool]": - self._objs.append(obj) - return self - - def __enter__(self) -> bool: - obj = self._objs[-1] - if id(obj) in self._ids: - return True - else: - self._ids[id(obj)] = obj - return False - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> None: - self._ids.pop(id(self._objs.pop()), None) - - -class _Serializer: - """Holds the state of a single serialize() call.""" - - __slots__ = ( - "memo", - "path", - "meta_stack", - "keep_request_bodies", - "max_value_length", - "is_vars", - "custom_repr", - ) - - def __init__( - self, - keep_request_bodies: bool, - max_value_length: "Optional[int]", - is_vars: bool, - custom_repr: "Optional[Callable[..., Optional[str]]]", - ) -> None: - self.memo = Memo() - self.path: "List[Segment]" = [] - self.meta_stack: "List[Dict[str, Any]]" = [] - self.keep_request_bodies = keep_request_bodies - self.max_value_length = max_value_length - self.is_vars = is_vars - self.custom_repr = custom_repr - - def _safe_repr_wrapper(self, value: "Any") -> str: - try: - repr_value = None - if self.custom_repr is not None: - repr_value = self.custom_repr(value) - return repr_value or safe_repr(value) - except Exception: - return safe_repr(value) - - def _annotate(self, **meta: "Any") -> None: - while len(self.meta_stack) <= len(self.path): - try: - segment = self.path[len(self.meta_stack) - 1] - node = self.meta_stack[-1].setdefault(str(segment), {}) - except IndexError: - node = {} - - self.meta_stack.append(node) - - self.meta_stack[-1].setdefault("", {}).update(meta) - - def _is_databag(self) -> "Optional[bool]": - """ - A databag is any value that we need to trim. - True for stuff like vars, request bodies, breadcrumbs and extra. - - :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". - """ - try: - if self.is_vars: - return True - - is_request_body = self._is_request_body() - if is_request_body in (True, None): - return is_request_body - - p0 = self.path[0] - if p0 == "breadcrumbs" and self.path[1] == "values": - self.path[2] - return True - - if p0 == "extra": - return True - - except IndexError: - return None - - return False - - def _is_span_attribute(self) -> "Optional[bool]": - try: - if self.path[0] == "spans" and self.path[2] == "data": - return True - except IndexError: - return None - - return False - - def _is_request_body(self) -> "Optional[bool]": - try: - if self.path[0] == "request" and self.path[1] == "data": - return True - except IndexError: - return None - - return False - - def _serialize_node( - self, - obj: "Any", - is_databag: "Optional[bool]" = None, - is_request_body: "Optional[bool]" = None, - should_repr_strings: "Optional[bool]" = None, - segment: "Optional[Segment]" = None, - remaining_breadth: "Optional[Union[int, float]]" = None, - remaining_depth: "Optional[Union[int, float]]" = None, - ) -> "Any": - if segment is not None: - self.path.append(segment) - - try: - with self.memo.memoize(obj) as result: - if result: - return CYCLE_MARKER - - return self._serialize_node_impl( - obj, - is_databag=is_databag, - is_request_body=is_request_body, - should_repr_strings=should_repr_strings, - remaining_depth=remaining_depth, - remaining_breadth=remaining_breadth, - ) - except BaseException: - capture_internal_exception(sys.exc_info()) - - if is_databag: - return "" - - return None - finally: - if segment is not None: - self.path.pop() - del self.meta_stack[len(self.path) + 1 :] - - def _flatten_annotated(self, obj: "Any") -> "Any": - if isinstance(obj, AnnotatedValue): - self._annotate(**obj.metadata) - obj = obj.value - return obj - - def _serialize_node_impl( - self, - obj: "Any", - is_databag: "Optional[bool]", - is_request_body: "Optional[bool]", - should_repr_strings: "Optional[bool]", - remaining_depth: "Optional[Union[float, int]]", - remaining_breadth: "Optional[Union[float, int]]", - ) -> "Any": - if isinstance(obj, AnnotatedValue): - should_repr_strings = False - if should_repr_strings is None: - should_repr_strings = self.is_vars - - if is_databag is None: - is_databag = self._is_databag() - - if is_request_body is None: - is_request_body = self._is_request_body() - - if is_databag: - if is_request_body and self.keep_request_bodies: - remaining_depth = float("inf") - remaining_breadth = float("inf") - else: - if remaining_depth is None: - remaining_depth = MAX_DATABAG_DEPTH - if remaining_breadth is None: - remaining_breadth = MAX_DATABAG_BREADTH - - obj = self._flatten_annotated(obj) - - if remaining_depth is not None and remaining_depth <= 0: - self._annotate(rem=[["!limit", "x"]]) - if is_databag: - return self._flatten_annotated( - strip_string( - self._safe_repr_wrapper(obj), max_length=self.max_value_length - ) - ) - return None - - is_span_attribute = self._is_span_attribute() - if (is_databag or is_span_attribute) and global_repr_processors: - hints = {"memo": self.memo, "remaining_depth": remaining_depth} - for processor in global_repr_processors: - result = processor(obj, hints) - if result is not NotImplemented: - return self._flatten_annotated(result) - - sentry_repr = getattr(type(obj), "__sentry_repr__", None) - - if obj is None or isinstance(obj, (bool, int, float)): - if should_repr_strings or ( - isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) - ): - return self._safe_repr_wrapper(obj) - else: - return obj - - elif callable(sentry_repr): - return sentry_repr(obj) - - elif isinstance(obj, datetime): - return ( - str(format_timestamp(obj)) - if not should_repr_strings - else self._safe_repr_wrapper(obj) - ) - - elif isinstance(obj, Mapping): - # Create temporary copy here to avoid calling too much code that - # might mutate our dictionary while we're still iterating over it. - obj = dict(obj.items()) - - rv_dict: "Dict[str, Any]" = {} - i = 0 - - for k, v in obj.items(): - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) - break - - str_k = str(k) - v = self._serialize_node( - v, - segment=str_k, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - rv_dict[str_k] = v - i += 1 - - return rv_dict - - elif not isinstance(obj, serializable_str_types) and isinstance( - obj, tuple(sequence_types) - ): - rv_list = [] - - for i, v in enumerate(obj): # type: ignore[arg-type] - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) # type: ignore[arg-type] - break - - rv_list.append( - self._serialize_node( - v, - segment=i, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - ) - - return rv_list - - if should_repr_strings: - obj = self._safe_repr_wrapper(obj) - else: - if isinstance(obj, bytes) or isinstance(obj, bytearray): - obj = obj.decode("utf-8", "replace") - - if not isinstance(obj, str): - obj = self._safe_repr_wrapper(obj) - - is_span_description = ( - len(self.path) == 3 - and self.path[0] == "spans" - and self.path[-1] == "description" - ) - if is_span_description: - return obj - - return self._flatten_annotated( - strip_string(obj, max_length=self.max_value_length) - ) - - -def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": - """ - A very smart serializer that takes a dict and emits a json-friendly dict. - Currently used for serializing the final Event and also prematurely while fetching the stack - local variables for each frame in a stacktrace. - - It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. - The algorithm itself is a recursive graph walk down the data structures it encounters. - - It has the following responsibilities: - * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. - * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. - * Annotating the payload with the _meta field whenever trimming happens. - - :param max_request_body_size: If set to "always", will never trim request bodies. - :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. - :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. - :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. - - """ - serializer = _Serializer( - keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", - max_value_length=kwargs.pop("max_value_length", None), - is_vars=kwargs.pop("is_vars", False), - custom_repr=kwargs.pop("custom_repr", None), - ) - - disable_capture_event.set(True) - try: - serialized_event = serializer._serialize_node(event, **kwargs) - if ( - not serializer.is_vars - and serializer.meta_stack - and isinstance(serialized_event, dict) - ): - serialized_event["_meta"] = serializer.meta_stack[0] - - return serialized_event - finally: - disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py deleted file mode 100644 index 3ffd071bbc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/session.py +++ /dev/null @@ -1,165 +0,0 @@ -import uuid -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from sentry_sdk._types import SessionStatus - - -def _minute_trunc(ts: "datetime") -> "datetime": - return ts.replace(second=0, microsecond=0) - - -def _make_uuid( - val: "Union[str, uuid.UUID]", -) -> "uuid.UUID": - if isinstance(val, uuid.UUID): - return val - return uuid.UUID(val) - - -class Session: - def __init__( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - session_mode: str = "application", - ) -> None: - if sid is None: - sid = uuid.uuid4() - if started is None: - started = datetime.now(timezone.utc) - if status is None: - status = "ok" - self.status = status - self.did: "Optional[str]" = None - self.started = started - self.release: "Optional[str]" = None - self.environment: "Optional[str]" = None - self.duration: "Optional[float]" = None - self.user_agent: "Optional[str]" = None - self.ip_address: "Optional[str]" = None - self.session_mode: str = session_mode - self.errors = 0 - - self.update( - sid=sid, - did=did, - timestamp=timestamp, - duration=duration, - release=release, - environment=environment, - user_agent=user_agent, - ip_address=ip_address, - errors=errors, - user=user, - ) - - @property - def truncated_started(self) -> "datetime": - return _minute_trunc(self.started) - - def update( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - ) -> None: - # If a user is supplied we pull some data form it - if user: - if ip_address is None: - ip_address = user.get("ip_address") - if did is None: - did = user.get("id") or user.get("email") or user.get("username") - - if sid is not None: - self.sid = _make_uuid(sid) - if did is not None: - self.did = str(did) - if timestamp is None: - timestamp = datetime.now(timezone.utc) - self.timestamp = timestamp - if started is not None: - self.started = started - if duration is not None: - self.duration = duration - if release is not None: - self.release = release - if environment is not None: - self.environment = environment - if ip_address is not None: - self.ip_address = ip_address - if user_agent is not None: - self.user_agent = user_agent - if errors is not None: - self.errors = errors - - if status is not None: - self.status = status - - def close( - self, - status: "Optional[SessionStatus]" = None, - ) -> "Any": - if status is None and self.status == "ok": - status = "exited" - if status is not None: - self.update(status=status) - - def get_json_attrs( - self, - with_user_info: "Optional[bool]" = True, - ) -> "Any": - attrs = {} - if self.release is not None: - attrs["release"] = self.release - if self.environment is not None: - attrs["environment"] = self.environment - if with_user_info: - if self.ip_address is not None: - attrs["ip_address"] = self.ip_address - if self.user_agent is not None: - attrs["user_agent"] = self.user_agent - return attrs - - def to_json(self) -> "Any": - rv: "Dict[str, Any]" = { - "sid": str(self.sid), - "init": True, - "started": format_timestamp(self.started), - "timestamp": format_timestamp(self.timestamp), - "status": self.status, - } - if self.errors: - rv["errors"] = self.errors - if self.did is not None: - rv["did"] = self.did - if self.duration is not None: - rv["duration"] = self.duration - attrs = self.get_json_attrs() - if attrs: - rv["attrs"] = attrs - return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py deleted file mode 100644 index aabf874fcd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/sessions.py +++ /dev/null @@ -1,262 +0,0 @@ -import os -import warnings -from contextlib import contextmanager -from threading import Event, Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.envelope import Envelope -from sentry_sdk.session import Session -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, List, Optional, Union - - -def is_auto_session_tracking_enabled( - hub: "Optional[sentry_sdk.Hub]" = None, -) -> "Union[Any, bool, None]": - """DEPRECATED: Utility function to find out if session tracking is enabled.""" - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - - should_track = hub.scope._force_auto_session_tracking - - if should_track is None: - client_options = hub.client.options if hub.client else {} - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking( - hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: Use track_session instead - Starts and stops a session automatically around a block. - """ - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "Use track_session instead.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - should_track = is_auto_session_tracking_enabled(hub) - if should_track: - hub.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - hub.end_session() - - -def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: - """ - DEPRECATED: Utility function to find out if session tracking is enabled. - """ - - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - return _is_auto_session_tracking_enabled(scope) - - -def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: - """ - Utility function to find out if session tracking is enabled. - """ - - should_track = scope._force_auto_session_tracking - if should_track is None: - client_options = sentry_sdk.get_client().options - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking_scope( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: This function is a deprecated alias for track_session. - Starts and stops a session automatically around a block. - """ - - warnings.warn( - "This function is a deprecated alias for track_session and will be removed in the next major release.", - DeprecationWarning, - stacklevel=2, - ) - - with track_session(scope, session_mode=session_mode): - yield - - -@contextmanager -def track_session( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """ - Start a new session in the provided scope, assuming session tracking is enabled. - This is a no-op context manager if session tracking is not enabled. - """ - - should_track = _is_auto_session_tracking_enabled(scope) - if should_track: - scope.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - scope.end_session() - - -TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") -MAX_ENVELOPE_ITEMS = 100 - - -def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": - return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} - - -class SessionFlusher: - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - flush_interval: int = 60, - ) -> None: - self.capture_func = capture_func - self.flush_interval = flush_interval - self.pending_sessions: "List[Any]" = [] - self.pending_aggregates: "Dict[Any, Any]" = {} - self._thread: "Optional[Thread]" = None - self._thread_lock = Lock() - self._aggregate_lock = Lock() - self._thread_for_pid: "Optional[int]" = None - self.__shutdown_requested = Event() - - def flush(self) -> None: - pending_sessions = self.pending_sessions - self.pending_sessions = [] - - with self._aggregate_lock: - pending_aggregates = self.pending_aggregates - self.pending_aggregates = {} - - envelope = Envelope() - for session in pending_sessions: - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_session(session) - - for attrs, states in pending_aggregates.items(): - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_sessions(make_aggregate_envelope(states, attrs)) - - if len(envelope.items) > 0: - self.capture_func(envelope) - - def _ensure_running(self) -> None: - """ - Check that we have an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - running = True - while running: - running = not self.__shutdown_requested.wait(self.flush_interval) - self.flush() - - thread = Thread(target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.__shutdown_requested.set() - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def add_aggregate_session( - self, - session: "Session", - ) -> None: - # NOTE on `session.did`: - # the protocol can deal with buckets that have a distinct-id, however - # in practice we expect the python SDK to have an extremely high cardinality - # here, effectively making aggregation useless, therefore we do not - # aggregate per-did. - - # For this part we can get away with using the global interpreter lock - with self._aggregate_lock: - attrs = session.get_json_attrs(with_user_info=False) - primary_key = tuple(sorted(attrs.items())) - secondary_key = session.truncated_started # (, session.did) - states = self.pending_aggregates.setdefault(primary_key, {}) - state = states.setdefault(secondary_key, {}) - - if "started" not in state: - state["started"] = format_timestamp(session.truncated_started) - # if session.did is not None: - # state["did"] = session.did - if session.status == "crashed": - state["crashed"] = state.get("crashed", 0) + 1 - elif session.status == "abnormal": - state["abnormal"] = state.get("abnormal", 0) + 1 - elif session.errors > 0: - state["errored"] = state.get("errored", 0) + 1 - else: - state["exited"] = state.get("exited", 0) + 1 - - def add_session( - self, - session: "Session", - ) -> None: - if session.session_mode == "request": - self.add_aggregate_session(session) - else: - self.pending_sessions.append(session.to_json()) - self._ensure_running() - - def kill(self) -> None: - self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py deleted file mode 100644 index 2dcc86bc47..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/spotlight.py +++ /dev/null @@ -1,327 +0,0 @@ -import io -import logging -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from itertools import chain, product -from typing import TYPE_CHECKING - -import urllib3 - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Self - -from sentry_sdk.envelope import Envelope -from sentry_sdk.utils import ( - capture_internal_exceptions, - env_to_bool, -) -from sentry_sdk.utils import ( - logger as sentry_logger, -) - -logger = logging.getLogger("spotlight") - - -DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" -DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" - - -class SpotlightClient: - """ - A client for sending envelopes to Sentry Spotlight. - - Implements exponential backoff retry logic per the SDK spec: - - Logs error at least once when server is unreachable - - Does not log for every failed envelope - - Uses exponential backoff to avoid hammering an unavailable server - - Never blocks normal Sentry operation - """ - - # Exponential backoff settings - INITIAL_RETRY_DELAY = 1.0 # Start with 1 second - MAX_RETRY_DELAY = 60.0 # Max 60 seconds - - def __init__(self, url: str) -> None: - self.url = url - self.http = urllib3.PoolManager() - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time: float = 0.0 - - def capture_envelope(self, envelope: "Envelope") -> None: - # Check if we're in backoff period - skip sending to avoid blocking - if self._last_error_time > 0: - time_since_error = time.time() - self._last_error_time - if time_since_error < self._retry_delay: - # Still in backoff period, skip this envelope - return - - body = io.BytesIO() - envelope.serialize_into(body) - try: - req = self.http.request( - url=self.url, - body=body.getvalue(), - method="POST", - headers={ - "Content-Type": "application/x-sentry-envelope", - }, - ) - req.close() - # Success - reset backoff state - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time = 0.0 - except Exception as e: - self._last_error_time = time.time() - - # Increase backoff delay exponentially first, so logged value matches actual wait - self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) - - # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) - sentry_logger.warning( - "Failed to send envelope to Spotlight at %s: %s. " - "Will retry after %.1f seconds.", - self.url, - e, - self._retry_delay, - ) - - -try: - from django.conf import settings - from django.http import HttpRequest, HttpResponse, HttpResponseServerError - from django.utils.deprecation import MiddlewareMixin - - SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" - SPOTLIGHT_JS_SNIPPET_PATTERN = ( - "\n" - '\n' - ) - SPOTLIGHT_ERROR_PAGE_SNIPPET = ( - '\n' - '\n' - ) - CHARSET_PREFIX = "charset=" - BODY_TAG_NAME = "body" - BODY_CLOSE_TAG_POSSIBILITIES = tuple( - "".format("".join(chars)) - for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) - ) - - class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] - _spotlight_script: "Optional[str]" = None - _spotlight_url: "Optional[str]" = None - - def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: - super().__init__(get_response) - - import sentry_sdk.api - - self.sentry_sdk = sentry_sdk.api - - spotlight_client = self.sentry_sdk.get_client().spotlight - if spotlight_client is None: - sentry_logger.warning( - "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." - ) - return None - # Spotlight URL has a trailing `/stream` part at the end so split it off - self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") - - @property - def spotlight_script(self: "Self") -> "Optional[str]": - if self._spotlight_url is not None and self._spotlight_script is None: - try: - spotlight_js_url = urllib.parse.urljoin( - self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH - ) - req = urllib.request.Request( - spotlight_js_url, - method="HEAD", - ) - urllib.request.urlopen(req) - self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( - spotlight_url=self._spotlight_url, - spotlight_js_url=spotlight_js_url, - ) - except urllib.error.URLError as err: - sentry_logger.debug( - "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", - spotlight_js_url, - exc_info=err, - ) - - return self._spotlight_script - - def process_response( - self: "Self", _request: "HttpRequest", response: "HttpResponse" - ) -> "Optional[HttpResponse]": - content_type_header = tuple( - p.strip() - for p in response.headers.get("Content-Type", "").lower().split(";") - ) - content_type = content_type_header[0] - if len(content_type_header) > 1 and content_type_header[1].startswith( - CHARSET_PREFIX - ): - encoding = content_type_header[1][len(CHARSET_PREFIX) :] - else: - encoding = "utf-8" - - if ( - self.spotlight_script is not None - and not response.streaming - and content_type == "text/html" - ): - content_length = len(response.content) - injection = self.spotlight_script.encode(encoding) - injection_site = next( - ( - idx - for idx in ( - response.content.rfind(body_variant.encode(encoding)) - for body_variant in BODY_CLOSE_TAG_POSSIBILITIES - ) - if idx > -1 - ), - content_length, - ) - - # This approach works even when we don't have a `` tag - response.content = ( - response.content[:injection_site] - + injection - + response.content[injection_site:] - ) - - if response.has_header("Content-Length"): - response.headers["Content-Length"] = content_length + len(injection) - - return response - - def process_exception( - self: "Self", _request: "HttpRequest", exception: Exception - ) -> "Optional[HttpResponseServerError]": - if not settings.DEBUG or not self._spotlight_url: - return None - - try: - spotlight = ( - urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") - ) - except urllib.error.URLError: - return None - else: - event_id = self.sentry_sdk.capture_exception(exception) - return HttpResponseServerError( - spotlight.replace( - "", - SPOTLIGHT_ERROR_PAGE_SNIPPET.format( - spotlight_url=self._spotlight_url, event_id=event_id - ), - ) - ) - -except ImportError: - settings = None - - -def _resolve_spotlight_url( - spotlight_config: "Any", sentry_logger: "Any" -) -> "Optional[str]": - """ - Resolve the Spotlight URL based on config and environment variable. - - Implements precedence rules per the SDK spec: - https://develop.sentry.dev/sdk/expected-features/spotlight/ - - Returns the resolved URL string, or None if Spotlight should be disabled. - """ - spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") - - # Parse env var to determine if it's a boolean or URL - spotlight_from_env: "Optional[bool]" = None - spotlight_env_url: "Optional[str]" = None - if spotlight_env_value: - parsed = env_to_bool(spotlight_env_value, strict=True) - if parsed is None: - # It's a URL string - spotlight_from_env = True - spotlight_env_url = spotlight_env_value - else: - spotlight_from_env = parsed - - # Apply precedence rules per spec: - # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules - if spotlight_config is False: - # Config explicitly disables spotlight - warn if env var was set - if spotlight_from_env: - sentry_logger.warning( - "Spotlight is disabled via spotlight=False config option, " - "ignoring SENTRY_SPOTLIGHT environment variable." - ) - return None - elif spotlight_config is True: - # Config enables spotlight with boolean true - # If env var has URL, use env var URL per spec - if spotlight_env_url: - return spotlight_env_url - else: - return DEFAULT_SPOTLIGHT_URL - elif isinstance(spotlight_config, str): - # Config has URL string - use config URL, warn if env var differs - if spotlight_env_value and spotlight_env_value != spotlight_config: - sentry_logger.warning( - "Spotlight URL from config (%s) takes precedence over " - "SENTRY_SPOTLIGHT environment variable (%s).", - spotlight_config, - spotlight_env_value, - ) - return spotlight_config - elif spotlight_config is None: - # No config - use env var - if spotlight_env_url: - return spotlight_env_url - elif spotlight_from_env: - return DEFAULT_SPOTLIGHT_URL - # else: stays None (disabled) - - return None - - -def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": - url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) - - if url is None: - return None - - # Only set up logging handler when spotlight is actually enabled - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.INFO) - - # Update options with resolved URL for consistency - options["spotlight"] = url - - with capture_internal_exceptions(): - if ( - settings is not None - and settings.DEBUG - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) - ): - middleware = settings.MIDDLEWARE - if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: - settings.MIDDLEWARE = type(middleware)( - chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) - ) - logger.info("Enabled Spotlight integration for Django") - - client = SpotlightClient(url) - logger.info("Enabled Spotlight using sidecar at %s", url) - - return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py deleted file mode 100644 index 5ee7e8460b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/traces.py +++ /dev/null @@ -1,843 +0,0 @@ -""" -EXPERIMENTAL. Do not use in production. - -The API in this file is only meant to be used in span streaming mode. - -You can enable span streaming mode via -sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). -""" - -import sys -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - capture_internal_exceptions, - format_attribute, - get_current_thread_meta, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Iterator, - Optional, - ParamSpec, - TypeVar, - Union, - overload, - ) - - from sentry_sdk._types import Attributes, AttributeValue, SpanJSON - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - - P = ParamSpec("P") - R = TypeVar("R") - - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -class SpanStatus(str, Enum): - OK = "ok" - ERROR = "error" - - def __str__(self) -> str: - return self.value - - -_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) - - -# Segment source, see -# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource -class SegmentSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_SEGMENT_SOURCES = [ - SegmentSource.URL, -] - - -SOURCE_FOR_STYLE = { - "endpoint": SegmentSource.COMPONENT, - "function_name": SegmentSource.COMPONENT, - "handler_name": SegmentSource.COMPONENT, - "method_and_path_pattern": SegmentSource.ROUTE, - "path": SegmentSource.URL, - "route_name": SegmentSource.COMPONENT, - "route_pattern": SegmentSource.ROUTE, - "uri_template": SegmentSource.ROUTE, - "url": SegmentSource.ROUTE, -} - - -# Sentinel value for an unset parent_span to be able to distinguish it from -# a None set by the user -_DEFAULT_PARENT_SPAN = object() - - -def start_span( - name: str, - attributes: "Optional[Attributes]" = None, - parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] - active: bool = True, -) -> "StreamedSpan": - """ - Start a span. - - EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() - instead. - - The span's parent, unless provided explicitly via the `parent_span` argument, - will be the current active span, if any. If there is none, this span will - become the root of a new span tree. If you explicitly want this span to be - top-level without a parent, set `parent_span=None`. - - `start_span()` can either be used as context manager or you can use the span - object it returns and explicitly end it via `span.end()`. The following is - equivalent: - - ```python - import sentry_sdk - - with sentry_sdk.traces.start_span(name="My Span"): - # do something - - # The span automatically finishes once the `with` block is exited - ``` - - ```python - import sentry_sdk - - span = sentry_sdk.traces.start_span(name="My Span") - # do something - span.end() - ``` - - To continue a trace from another service, call - `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. - - :param name: The name to identify this span by. - :type name: str - - :param attributes: Key-value attributes to set on the span from the start. - These will also be accessible in the traces sampler. - :type attributes: "Optional[Attributes]" - - :param parent_span: A span instance that the new span should consider its - parent. If not provided, the parent will be set to the currently active - span, if any. If set to `None`, this span will become a new root-level - span. - :type parent_span: "Optional[StreamedSpan]" - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to `False`. - :type active: bool - - :return: The span that has been started. - :rtype: StreamedSpan - """ - from sentry_sdk.tracing_utils import has_span_streaming_enabled - - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "sentry_sdk.start_transaction() and sentry_sdk.start_span() " - "instead.", - stacklevel=2, - ) - return NoOpStreamedSpan() - - return sentry_sdk.get_current_scope().start_streamed_span( - name, attributes, parent_span, active - ) - - -def continue_trace(incoming: "dict[str, Any]") -> None: - """ - Continue a trace from headers or environment variables. - - EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. - - This function sets the propagation context on the scope. Any span started - in the updated scope will belong under the trace extracted from the - provided propagation headers or environment variables. - - continue_trace() doesn't start any spans on its own. Use the start_span() - API for that. - """ - # This is set both on the isolation and the current scope for compatibility - # reasons. Conceptually, it belongs on the isolation scope, and it also - # used to be set there in non-span-first mode. But in span first mode, we - # start spans on the current scope, regardless of type, like JS does, so we - # need to set the propagation context there. - sentry_sdk.get_isolation_scope().generate_propagation_context( - incoming, - ) - sentry_sdk.get_current_scope().generate_propagation_context( - incoming, - ) - - -def new_trace() -> None: - """ - Resets the propagation context, forcing a new trace. - - EXPERIMENTAL. - - This function sets the propagation context on the scope. Any span started - in the updated scope will start its own trace. - - new_trace() doesn't start any spans on its own. Use the start_span() API - for that. - """ - sentry_sdk.get_isolation_scope().set_new_propagation_context() - sentry_sdk.get_current_scope().set_new_propagation_context() - - -class StreamedSpan: - """ - A span holds timing information of a block of code. - - Spans can have multiple child spans, thus forming a span tree. - - This is the Span First span implementation that streams spans. The original - transaction-based span implementation lives in tracing.Span. - """ - - __slots__ = ( - "_name", - "_attributes", - "_active", - "_span_id", - "_trace_id", - "_parent_span_id", - "_segment", - "_parent_sampled", - "_start_timestamp", - "_start_timestamp_monotonic_ns", - "_end_timestamp", - "_status", - "_scope", - "_previous_span_on_scope", - "_baggage", - "_sample_rand", - "_sample_rate", - "_continuous_profile", - ) - - def __init__( - self, - *, - name: str, - attributes: "Optional[Attributes]" = None, - active: bool = True, - scope: "sentry_sdk.Scope", - segment: "Optional[StreamedSpan]" = None, - trace_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - sample_rate: "Optional[float]" = None, - sample_rand: "Optional[float]" = None, - ): - self._name: str = name - self._active: bool = active - self._attributes: "Attributes" = { - "sentry.origin": "manual", - "sentry.trace_lifecycle": "stream", - } - - if attributes: - for attribute, value in attributes.items(): - self.set_attribute(attribute, value) - - self._scope = scope - - self._segment = segment or self - - self._trace_id: "Optional[str]" = trace_id - self._parent_span_id = parent_span_id - self._parent_sampled = parent_sampled - self._baggage = baggage - self._sample_rand = sample_rand - self._sample_rate = sample_rate - - self._start_timestamp = datetime.now(timezone.utc) - self._end_timestamp: "Optional[datetime]" = None - - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - - self._span_id: "Optional[str]" = None - - self._status = SpanStatus.OK.value - - self._update_active_thread() - - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._start_profile() - self._set_profile_id(get_profiler_id()) - - self._set_segment_attributes() - - self._start() - - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__}(" - f"name={self._name}, " - f"trace_id={self.trace_id}, " - f"span_id={self.span_id}, " - f"parent_span_id={self._parent_span_id}, " - f"active={self._active})>" - ) - - def __enter__(self) -> "StreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore - return - - if value is not None and should_be_treated_as_error(ty, value): - self.status = SpanStatus.ERROR.value - - self._end() - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - """ - Finish this span and queue it for sending. - - :param end_timestamp: End timestamp to use instead of current time. - :type end_timestamp: "Optional[Union[float, datetime]]" - """ - self._end(end_timestamp) - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self.end(end_timestamp) - - def _start(self) -> None: - if self._active: - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore. - return - - # Stop the profiler - if self._is_segment() and self._continuous_profile is not None: - with capture_internal_exceptions(): - self._continuous_profile.stop() - - # Detach from scope - if self._active: - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - # Set attributes from the segment. These are set on span end on purpose - # so that we have the best chance to capture the segment's final name - # (since it might change during its lifetime) - self.set_attribute("sentry.segment.id", self._segment.span_id) - self.set_attribute("sentry.segment.name", self._segment.name) - - # Set the end timestamp - if end_timestamp is not None: - if isinstance(end_timestamp, (float, int)): - try: - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - except Exception: - pass - - if isinstance(end_timestamp, datetime): - self._end_timestamp = end_timestamp - else: - logger.debug( - "[Tracing] Failed to set end_timestamp. Using current time instead." - ) - - if self._end_timestamp is None: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self._end_timestamp = self._start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # Finally, queue the span for sending to Sentry - self._scope._capture_span(self) - - def get_attributes(self) -> "Attributes": - return self._attributes - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - self._attributes[key] = format_attribute(value) - - def set_attributes(self, attributes: "Attributes") -> None: - for key, value in attributes.items(): - self.set_attribute(key, value) - - def remove_attribute(self, key: str) -> None: - try: - del self._attributes[key] - except KeyError: - pass - - @property - def status(self) -> "str": - return self._status - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - if isinstance(status, Enum): - status = status.value - - if status not in _VALID_SPAN_STATUSES: - logger.debug( - f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' - ) - return - - self._status = status - - @property - def name(self) -> str: - return self._name - - @name.setter - def name(self, name: str) -> None: - self._name = name - - @property - def active(self) -> bool: - return self._active - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @property - def sampled(self) -> "Optional[bool]": - return True - - @property - def start_timestamp(self) -> "Optional[datetime]": - return self._start_timestamp - - @property - def end_timestamp(self) -> "Optional[datetime]": - return self._end_timestamp - - def _is_segment(self) -> bool: - return self._segment is self - - def _update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - - if thread_id is not None: - self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_attribute(SPANDATA.THREAD_NAME, thread_name) - - def _dynamic_sampling_context(self) -> "dict[str, str]": - return self._segment._get_baggage().dynamic_sampling_context() - - def _to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def _to_baggage(self) -> "Optional[Baggage]": - if self._segment: - return self._segment._get_baggage() - return None - - def _get_baggage(self) -> "Baggage": - """ - Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with - the segment. - - The first time a new baggage with Sentry items is made, it will be frozen. - """ - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_segment(self) - - return self._baggage - - def _iter_headers(self) -> "Iterator[tuple[str, str]]": - if not self._segment: - return - - yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() - - baggage = self._segment._get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def _get_trace_context(self) -> "dict[str, Any]": - # Even if spans themselves are not event-based anymore, we need this - # to populate trace context on events - context: "dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self._parent_span_id, - "dynamic_sampling_context": self._dynamic_sampling_context(), - } - - if "sentry.op" in self._attributes: - context["op"] = self._attributes["sentry.op"] - if "sentry.origin" in self._attributes: - context["origin"] = self._attributes["sentry.origin"] - - return context - - def _set_profile_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_attribute("sentry.profiler_id", profiler_id) - - def _start_profile(self) -> None: - if not self._is_segment(): - return - - try_autostart_continuous_profiler() - - self._continuous_profile = try_profile_lifecycle_trace_start() - - def _set_segment_attributes(self) -> None: - if not self._is_segment(): - return - - client = sentry_sdk.get_client() - - self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") - self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) - self.set_attribute( - SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) - ) - - if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: - self.set_attribute( - SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() - ) - - def _to_json(self) -> "SpanJSON": - res: "SpanJSON" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "name": self._name if self._name is not None else "", - "status": self._status, - "is_segment": self._is_segment(), - "start_timestamp": self._start_timestamp.timestamp(), - } - - if self._end_timestamp: - res["end_timestamp"] = self._end_timestamp.timestamp() - - if self._parent_span_id: - res["parent_span_id"] = self._parent_span_id - - res["attributes"] = {k: v for k, v in self._attributes.items()} - - return res - - -class NoOpStreamedSpan(StreamedSpan): - __slots__ = ( - "_finished", - "_unsampled_reason", - ) - - def __init__( - self, - unsampled_reason: "Optional[str]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - ) -> None: - self._scope = scope # type: ignore[assignment] - self._unsampled_reason = unsampled_reason - - self._finished = False - - self._start() - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}(sampled={self.sampled})>" - - def __enter__(self) -> "NoOpStreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self._end() - - def _start(self) -> None: - if self._scope is None: - return - - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._finished: - return - - if self._unsampled_reason is not None: - client = sentry_sdk.get_client() - if client.is_active() and client.transport: - logger.debug( - f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" - ) - client.transport.record_lost_event( - reason=self._unsampled_reason, - data_category="span", - quantity=1, - ) - - if self._scope and hasattr(self, "_previous_span_on_scope"): - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - self._finished = True - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - self._end() - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self._end() - - def get_attributes(self) -> "Attributes": - return {} - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - pass - - def set_attributes(self, attributes: "Attributes") -> None: - pass - - def remove_attribute(self, key: str) -> None: - pass - - def _is_segment(self) -> bool: - return self._scope is not None - - @property - def status(self) -> "str": - return SpanStatus.OK.value - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - pass - - @property - def name(self) -> str: - return "" - - @name.setter - def name(self, value: str) -> None: - pass - - @property - def active(self) -> bool: - return True - - @property - def span_id(self) -> str: - return "0000000000000000" - - @property - def trace_id(self) -> str: - return "00000000000000000000000000000000" - - @property - def sampled(self) -> "Optional[bool]": - return False - - @property - def start_timestamp(self) -> "Optional[datetime]": - return None - - @property - def end_timestamp(self) -> "Optional[datetime]": - return None - - -if TYPE_CHECKING: - - @overload - def trace( - func: "Callable[P, R]", - ) -> "Callable[P, R]": ... - - @overload - def trace( - func: None = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a span around a function call. - - EXPERIMENTAL. Use @sentry_sdk.trace instead. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to False. - :type active: bool - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import ( - create_streaming_span_decorator, - ) - - decorator = create_streaming_span_decorator( - name=name, - attributes=attributes, - active=active, - ) - - if func: - return decorator(func) - else: - return decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[StreamedSpan]": - """ - Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. - - This function will only return a non-`None` value when the streaming trace lifecycle is enabled. - To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.streamed_span - return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py deleted file mode 100644 index 1790e13fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing.py +++ /dev/null @@ -1,1475 +0,0 @@ -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE -from sentry_sdk.profiler.continuous_profiler import get_profiler_id -from sentry_sdk.utils import ( - capture_internal_exceptions, - get_current_thread_meta, - is_valid_sample_rate, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping - from typing import ( - Any, - Dict, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - overload, - ) - - from typing_extensions import TypedDict, Unpack - - P = ParamSpec("P") - R = TypeVar("R") - - from sentry_sdk._types import ( - Event, - MeasurementUnit, - MeasurementValue, - SamplingContext, - ) - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - from sentry_sdk.profiler.transaction_profiler import Profile - - class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - - op: str - """ - The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - """ - - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - - hub: "Optional[sentry_sdk.Hub]" - """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" - - status: str - """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - - containing_transaction: "Optional[Transaction]" - """The transaction that this span belongs to.""" - - start_timestamp: "Optional[Union[datetime, float]]" - """ - The timestamp when the span started. If omitted, the current time - will be used. - """ - - scope: "sentry_sdk.Scope" - """The scope to use for this span. If not provided, we use the current scope.""" - - origin: str - """ - The origin of the span. - See https://develop.sentry.dev/sdk/performance/trace-origin/ - Default "manual". - """ - - name: str - """A string describing what operation is being performed within the span/transaction.""" - - class TransactionKwargs(SpanKwargs, total=False): - source: str - """ - A string describing the source of the transaction name. This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. - Default "custom". - """ - - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - - ProfileContext = TypedDict( - "ProfileContext", - { - "profiler_id": str, - }, - ) - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -# Transaction source -# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations -class TransactionSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_TRANSACTION_SOURCES = [ - TransactionSource.URL, -] - -SOURCE_FOR_STYLE = { - "endpoint": TransactionSource.COMPONENT, - "function_name": TransactionSource.COMPONENT, - "handler_name": TransactionSource.COMPONENT, - "method_and_path_pattern": TransactionSource.ROUTE, - "path": TransactionSource.URL, - "route_name": TransactionSource.COMPONENT, - "route_pattern": TransactionSource.ROUTE, - "uri_template": TransactionSource.ROUTE, - "url": TransactionSource.ROUTE, -} - - -def get_span_status_from_http_code(http_status_code: int) -> str: - """ - Returns the Sentry status corresponding to the given HTTP status code. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - if http_status_code < 400: - return SPANSTATUS.OK - - elif 400 <= http_status_code < 500: - if http_status_code == 403: - return SPANSTATUS.PERMISSION_DENIED - elif http_status_code == 404: - return SPANSTATUS.NOT_FOUND - elif http_status_code == 429: - return SPANSTATUS.RESOURCE_EXHAUSTED - elif http_status_code == 413: - return SPANSTATUS.FAILED_PRECONDITION - elif http_status_code == 401: - return SPANSTATUS.UNAUTHENTICATED - elif http_status_code == 409: - return SPANSTATUS.ALREADY_EXISTS - else: - return SPANSTATUS.INVALID_ARGUMENT - - elif 500 <= http_status_code < 600: - if http_status_code == 504: - return SPANSTATUS.DEADLINE_EXCEEDED - elif http_status_code == 501: - return SPANSTATUS.UNIMPLEMENTED - elif http_status_code == 503: - return SPANSTATUS.UNAVAILABLE - else: - return SPANSTATUS.INTERNAL_ERROR - - return SPANSTATUS.UNKNOWN_ERROR - - -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans", "dropped_spans") - - def __init__(self, maxlen: int) -> None: - # FIXME: this is `maxlen - 1` only to preserve historical behavior - # enforced by tests. - # Either this should be changed to `maxlen` or the JS SDK implementation - # should be changed to match a consistent interpretation of what maxlen - # limits: either transaction+spans or only child spans. - self.maxlen = maxlen - 1 - self.spans: "List[Span]" = [] - self.dropped_spans: int = 0 - - def add(self, span: "Span") -> None: - if len(self.spans) > self.maxlen: - span._span_recorder = None - self.dropped_spans += 1 - else: - self.spans.append(span) - - -class Span: - """A span holds timing information of a block of code. - Spans can have multiple child spans thus forming a span tree. - - :param trace_id: The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - :param span_id: The span ID of this span. If omitted, a new span ID will be generated. - :param parent_span_id: The span ID of the parent span, if applicable. - :param same_process_as_parent: Whether this span is in the same process as the parent span. - :param sampled: Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - :param op: The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - :param description: A description of what operation is being performed within the span. - - .. deprecated:: 2.15.0 - Please use the `name` parameter, instead. - :param name: A string describing what operation is being performed within the span. - :param hub: The hub to use for this span. - - .. deprecated:: 2.0.0 - Please use the `scope` parameter, instead. - :param status: The span's status. Possible values are listed at - https://develop.sentry.dev/sdk/event-payloads/span/ - :param containing_transaction: The transaction that this span belongs to. - :param start_timestamp: The timestamp when the span started. If omitted, the current time - will be used. - :param scope: The scope to use for this span. If not provided, we use the current scope. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "same_process_as_parent", - "sampled", - "op", - "description", - "_measurements", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "hub", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - "_flags", - "_flags_capacity", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - same_process_as_parent: bool = True, - sampled: "Optional[bool]" = None, - op: "Optional[str]" = None, - description: "Optional[str]" = None, - hub: "Optional[sentry_sdk.Hub]" = None, # deprecated - status: "Optional[str]" = None, - containing_transaction: "Optional[Transaction]" = None, - start_timestamp: "Optional[Union[datetime, float]]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - origin: str = "manual", - name: "Optional[str]" = None, - ) -> None: - self._trace_id = trace_id - self._span_id = span_id - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = name or description - self.status = status - self.hub = hub # backwards compatibility - self.scope = scope - self.origin = origin - self._measurements: "Dict[str, MeasurementValue]" = {} - self._tags: "MutableMapping[str, str]" = {} - self._data: "Dict[str, Any]" = {} - self._containing_transaction = containing_transaction - self._flags: "Dict[str, bool]" = {} - self._flags_capacity = 10 - - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - - if start_timestamp is None: - start_timestamp = datetime.now(timezone.utc) - elif isinstance(start_timestamp, float): - start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) - self.start_timestamp = start_timestamp - try: - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp: "Optional[datetime]" = None - - self._span_recorder: "Optional[_SpanRecorder]" = None - - self.update_active_thread() - self.set_profiler_id(get_profiler_id()) - - # TODO this should really live on the Transaction class rather than the Span - # class - def init_span_recorder(self, maxlen: int) -> None: - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - def __repr__(self) -> str: - return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.op, - self.description, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.origin, - ) - ) - - def __enter__(self) -> "Span": - scope = self.scope or sentry_sdk.get_current_scope() - old_span = scope.span - scope.span = self - self._context_manager_state = (scope, old_span) - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - with capture_internal_exceptions(): - scope, old_span = self._context_manager_state - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self) -> "Optional[Transaction]": - """The ``Transaction`` that this span belongs to. - The ``Transaction`` is the root of the span tree, - so one could also think of this ``Transaction`` as the "root span".""" - - # this is a getter rather than a regular attribute so that transactions - # can return `self` here instead (as a way to prevent them circularly - # referencing themselves) - return self._containing_transaction - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a sub-span from the current span or transaction. - - Takes the same arguments as the initializer of :py:class:`Span`. The - trace id, sampling decision, transaction pointer, and span recorder are - inherited from the current span/transaction. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - kwargs.setdefault("sampled", self.sampled) - - child = Span( - trace_id=self.trace_id, - parent_span_id=self.span_id, - containing_transaction=self.containing_transaction, - **kwargs, - ) - - span_recorder = ( - self.containing_transaction and self.containing_transaction._span_recorder - ) - if span_recorder: - span_recorder.add(child) - - return child - - @classmethod - def continue_from_environ( - cls, - environ: "Mapping[str, str]", - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a Transaction with the given params, then add in data pulled from - the ``sentry-trace`` and ``baggage`` headers from the environ (if any) - before returning the Transaction. - - This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` - in that it assumes header names in the form ``HTTP_HEADER_NAME`` - - such as you would get from a WSGI/ASGI environ - - rather than the form ``header-name``. - - :param environ: The ASGI/WSGI environ to pull information from. - """ - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers: "Mapping[str, str]", - *, - _sample_rand: "Optional[str]" = None, - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a transaction with the given params (including any data pulled from - the ``sentry-trace`` and ``baggage`` headers). - - :param headers: The dictionary with the HTTP headers to pull information from. - :param _sample_rand: If provided, we override the sample_rand value from the - incoming headers with this value. (internal use only) - """ - logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header( - headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand - ) - kwargs.update({BAGGAGE_HEADER_NAME: baggage}) - - sentrytrace_kwargs = extract_sentrytrace_data( - headers.get(SENTRY_TRACE_HEADER_NAME) - ) - - if sentrytrace_kwargs is not None: - kwargs.update(sentrytrace_kwargs) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. - If the span's containing transaction doesn't yet have a ``baggage`` value, - this will cause one to be generated and stored. - """ - if not self.containing_transaction: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - return - - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.containing_transaction.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - @classmethod - def from_traceparent( - cls, - traceparent: "Optional[str]", - **kwargs: "Any", - ) -> "Optional[Transaction]": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def to_baggage(self) -> "Optional[Baggage]": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with this ``Span``, if any. (Taken from the root of the span tree.) - """ - if self.containing_transaction: - return self.containing_transaction.get_baggage() - return None - - def set_tag(self, key: str, value: "Any") -> None: - self._tags[key] = value - - def set_data(self, key: str, value: "Any") -> None: - self._data[key] = value - - def update_data(self, data: "Dict[str, Any]") -> None: - self._data.update(data) - - def set_flag(self, flag: str, result: bool) -> None: - if len(self._flags) < self._flags_capacity: - self._flags[flag] = result - - def set_status(self, value: str) -> None: - self.status = value - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_thread( - self, thread_id: "Optional[int]", thread_name: "Optional[str]" - ) -> None: - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) - - def set_profiler_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status: int) -> None: - self.set_tag( - "http.status_code", str(http_status) - ) # TODO-neel remove in major, we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self) -> bool: - return self.status == "ok" - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ - if self.timestamp is not None: - # This span is already finished, ignore. - return None - - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp - else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - except AttributeError: - self.timestamp = datetime.now(timezone.utc) - - scope = scope or sentry_sdk.get_current_scope() - - # Copy conversation_id from scope to span data if this is an AI span - conversation_id = scope.get_conversation_id() - if conversation_id: - has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data - is_ai_span_op = self.op is not None and ( - self.op.startswith("ai.") or self.op.startswith("gen_ai.") - ) - if has_ai_op or is_ai_span_op: - self.set_data("gen_ai.conversation.id", conversation_id) - - maybe_create_breadcrumbs_from_span(scope, self) - - return None - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the span.""" - - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "same_process_as_parent": self.same_process_as_parent, - "op": self.op, - "description": self.description, - "start_timestamp": self.start_timestamp, - "timestamp": self.timestamp, - "origin": self.origin, - } - - if self.status: - rv["status"] = self.status - # TODO-neel remove redundant tag in major - self._tags["status"] = self.status - - if len(self._measurements) > 0: - rv["measurements"] = self._measurements - - tags = self._tags - if tags: - rv["tags"] = tags - - data = {} - data.update(self._flags) - data.update(self._data) - if data: - rv["data"] = data - - return rv - - def get_trace_context(self) -> "Any": - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "op": self.op, - "description": self.description, - "origin": self.origin, - } - if self.status: - rv["status"] = self.status - - if self.containing_transaction: - rv["dynamic_sampling_context"] = ( - self.containing_transaction.get_baggage().dynamic_sampling_context() - ) - - data = {} - - thread_id = self._data.get(SPANDATA.THREAD_ID) - if thread_id is not None: - data["thread.id"] = thread_id - - thread_name = self._data.get(SPANDATA.THREAD_NAME) - if thread_name is not None: - data["thread.name"] = thread_name - - if data: - rv["data"] = data - - return rv - - def get_profile_context(self) -> "Optional[ProfileContext]": - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - def update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -class Transaction(Span): - """The Transaction is the root element that holds all the spans - for Sentry performance instrumentation. - - :param name: Identifier of the transaction. - Will show up in the Sentry UI. - :param parent_sampled: Whether the parent transaction was sampled. - If True this transaction will be kept, if False it will be discarded. - :param baggage: The W3C baggage header value. - (see https://www.w3.org/TR/baggage/) - :param source: A string describing the source of the transaction name. - This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations - for more information. Default "custom". - :param kwargs: Additional arguments to be passed to the Span constructor. - See :py:class:`sentry_sdk.tracing.Span` for available arguments. - """ - - __slots__ = ( - "name", - "source", - "parent_sampled", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_measurements", - "_contexts", - "_profile", - "_continuous_profile", - "_baggage", - "_sample_rand", - ) - - def __init__( # type: ignore[misc] - self, - name: str = "", - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - source: str = TransactionSource.CUSTOM, - **kwargs: "Unpack[SpanKwargs]", - ) -> None: - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate: "Optional[float]" = None - self.parent_sampled = parent_sampled - self._measurements: "Dict[str, MeasurementValue]" = {} - self._contexts: "Dict[str, Any]" = {} - self._profile: "Optional[Profile]" = None - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._baggage = baggage - - baggage_sample_rand = ( - None if self._baggage is None else self._baggage._sample_rand() - ) - if baggage_sample_rand is not None: - self._sample_rand = baggage_sample_rand - else: - self._sample_rand = _generate_sample_rand(self.trace_id) - - def __repr__(self) -> str: - return ( - "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.name, - self.op, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.source, - self.origin, - ) - ) - - def _possibly_started(self) -> bool: - """Returns whether the transaction might have been started. - - If this returns False, we know that the transaction was not started - with sentry_sdk.start_transaction, and therefore the transaction will - be discarded. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self) -> "Transaction": - if not self._possibly_started(): - logger.debug( - "Transaction was entered without being started with sentry_sdk.start_transaction." - "The transaction will not be sent to Sentry. To fix, start the transaction by" - "passing it to sentry_sdk.start_transaction." - ) - - super().__enter__() - - if self._profile is not None: - self._profile.__enter__() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - - if self._continuous_profile is not None: - self._continuous_profile.stop() - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self) -> "Transaction": - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def _get_scope_from_finish_args( - self, - scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - ) -> "Optional[sentry_sdk.Scope]": - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - - def _get_log_representation(self) -> str: - return "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. - - :param scope: The Scope to use for this transaction. - If not provided, the current Scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args(scope, hub) - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be None - if self.sampled is False: - logger.debug("Discarding transaction because sampled = False") - else: - logger.debug( - "Discarding transaction because it was not started with sentry_sdk.start_transaction" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - if client.transport and has_tracing_enabled(client.options): - if client.monitor and client.monitor.downsample_factor > 0: - reason = "backpressure" - else: - reason = "sample_rate" - - client.transport.record_lost_event(reason, data_category="transaction") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) - if ( - status_code is not None - and status_code in client.options["trace_ignore_status_codes"] - ): - logger.debug( - "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( - transaction_description=self._get_log_representation(), - status_code=self._data[SPANDATA.HTTP_STATUS_CODE], - trace_ignore_status_codes=client.options[ - "trace_ignore_status_codes" - ], - ) - ) - if client.transport: - client.transport.record_lost_event( - "event_processor", data_category="transaction" - ) - - num_spans = len(self._span_recorder.spans) + 1 - client.transport.record_lost_event( - "event_processor", data_category="span", quantity=num_spans - ) - - self.sampled = False - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - if self.sampled is None: - logger.warning("Discarding transaction without sampling decision.") - - return None - - finished_spans = [] - has_gen_ai_span = False - if client.options.get("stream_gen_ai_spans", True): - for span in self._span_recorder.spans: - if span.timestamp is None: - continue - - if isinstance(span.op, str) and span.op.startswith("gen_ai."): - has_gen_ai_span = True - - finished_spans.append(span.to_json()) - else: - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - len_diff = len(self._span_recorder.spans) - len(finished_spans) - dropped_spans = len_diff + self._span_recorder.dropped_spans - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - self._span_recorder = None - - contexts = {} - contexts.update(self._contexts) - contexts.update({"trace": self.get_trace_context()}) - profile_context = self.get_profile_context() - if profile_context is not None: - contexts.update({"profile": profile_context}) - - event: "Event" = { - "type": "transaction", - "transaction": self.name, - "transaction_info": {"source": self.source}, - "contexts": contexts, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": finished_spans, - } - - if dropped_spans > 0: - event["_dropped_spans"] = dropped_spans - - if has_gen_ai_span: - event["_has_gen_ai_span"] = True - - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - - event["measurements"] = self._measurements - - return scope.capture_event(event) - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - """Sets a context. Transactions can have multiple contexts - and they should follow the format described in the "Contexts Interface" - documentation. - - :param key: The name of the context. - :param value: The information about the context. - """ - self._contexts[key] = value - - def set_http_status(self, http_status: int) -> None: - """Sets the status of the Transaction according to the given HTTP status. - - :param http_status: The HTTP status code.""" - super().set_http_status(http_status) - self.set_context("response", {"status_code": http_status}) - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the transaction.""" - rv = super().to_json() - - rv["name"] = self.name - rv["source"] = self.source - rv["sampled"] = self.sampled - - return rv - - def get_trace_context(self) -> "Any": - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self) -> "Baggage": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with the Transaction. - - The first time a new baggage with Sentry items is made, - it will be frozen.""" - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_transaction(self) - - return self._baggage - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the transaction's sampling decision, according to the following - precedence rules: - - 1. If a sampling decision is passed to `start_transaction` - (`start_transaction(name: "my transaction", sampled: True)`), that - decision will be used, regardless of anything else - - 2. If `traces_sampler` is defined, its decision will be used. It can - choose to keep or ignore any parent sampling decision, or use the - sampling context data to make its own decision or to choose a sample - rate for the transaction. - - 3. If `traces_sampler` is not defined, but there's a parent sampling - decision, the parent sampling decision will be used. - - 4. If `traces_sampler` is not defined and there's no parent sampling - decision, `traces_sample_rate` will be used. - """ - client = sentry_sdk.get_client() - - transaction_description = self._get_log_representation() - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) - # default inheritance behavior - else ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning( - "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( - transaction_description=transaction_description, - ) - ) - self.sampled = False - return - - self.sample_rate = float(sample_rate) - - if client.monitor: - self.sample_rate /= 2**client.monitor.downsample_factor - - # if the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - if not self.sample_rate: - logger.debug( - "[Tracing] Discarding {transaction_description} because {reason}".format( - transaction_description=transaction_description, - reason=( - "traces_sampler returned 0 or False" - if callable(client.options.get("traces_sampler")) - else "traces_sample_rate is set to 0" - ), - ) - ) - self.sampled = False - return - - # Now we roll the dice. - self.sampled = self._sample_rand < self.sample_rate - - if self.sampled: - logger.debug( - "[Tracing] Starting {transaction_description}".format( - transaction_description=transaction_description, - ) - ) - else: - logger.debug( - "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( - transaction_description=transaction_description, - sample_rate=self.sample_rate, - ) - ) - - # Private aliases matching StreamedSpan's private API - _get_baggage = get_baggage - _get_trace_context = get_trace_context - - -class NoOpSpan(Span): - def __repr__(self) -> str: - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self) -> "Optional[Transaction]": - return None - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "NoOpSpan": - return NoOpSpan() - - def to_traceparent(self) -> str: - return "" - - def to_baggage(self) -> "Optional[Baggage]": - return None - - def get_baggage(self) -> "Optional[Baggage]": - return None - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - return iter(()) - - def set_tag(self, key: str, value: "Any") -> None: - pass - - def set_data(self, key: str, value: "Any") -> None: - pass - - def update_data(self, data: "Dict[str, Any]") -> None: - pass - - def set_status(self, value: str) -> None: - pass - - def set_http_status(self, http_status: int) -> None: - pass - - def is_success(self) -> bool: - return True - - def to_json(self) -> "Dict[str, Any]": - return {} - - def get_trace_context(self) -> "Any": - return {} - - def get_profile_context(self) -> "Any": - return {} - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ - pass - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - pass - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - pass - - def init_span_recorder(self, maxlen: int) -> None: - pass - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - pass - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _get_baggage = get_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -if TYPE_CHECKING: - - @overload - def trace( - func: None = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": - # Handles: @trace() and @trace(op="custom") - pass - - @overload - def trace(func: "Callable[P, R]") -> "Callable[P, R]": - # Handles: @trace - pass - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a child span around a function call. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, a default operation will - be assigned based on the template. - :type op: str or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP, SPANTEMPLATE - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - op=OP.DB_QUERY, - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - - # With a custom template - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def calculate_interest_rate(amount, rate, years): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import create_span_decorator - - decorator = create_span_decorator( - op=op, - name=name, - attributes=attributes, - template=template, - ) - - if func: - return decorator(func) - else: - return decorator - - -# Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - _generate_sample_rand, - extract_sentrytrace_data, - has_tracing_enabled, - maybe_create_breadcrumbs_from_span, -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py deleted file mode 100644 index c2e34a795b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/tracing_utils.py +++ /dev/null @@ -1,1694 +0,0 @@ -import contextlib -import functools -import inspect -import os -import re -import sys -import uuid -import warnings -from collections.abc import Mapping, MutableMapping -from datetime import datetime, timedelta, timezone -from random import Random -from urllib.parse import quote, unquote - -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE -from sentry_sdk.utils import ( - _is_external_source, - _is_in_project_root, - _module_in_list, - capture_internal_exceptions, - filename_for_module, - is_sentry_url, - is_valid_sample_rate, - logger, - match_regex_list, - qualname_from_function, - safe_repr, - to_string, - try_convert, -) - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union - - from sentry_sdk._types import Attributes - - -SENTRY_TRACE_REGEX = re.compile( - "^[ \t]*" # whitespace - "([0-9a-f]{32})?" # trace_id - "-?([0-9a-f]{16})?" # span_id - "-?([01])?" # sampled - "[ \t]*$" # whitespace -) - - -# This is a normal base64 regex, modified to reflect that fact that we strip the -# trailing = or == off -base64_stripped = ( - # any of the characters in the base64 "alphabet", in multiples of 4 - "([a-zA-Z0-9+/]{4})*" - # either nothing or 2 or 3 base64-alphabet characters (see - # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for - # why there's never only 1 extra character) - "([a-zA-Z0-9+/]{2,3})?" -) - - -class EnvironHeaders(Mapping): # type: ignore - def __init__( - self, - environ: "Mapping[str, str]", - prefix: str = "HTTP_", - ) -> None: - self.environ = environ - self.prefix = prefix - - def __getitem__(self, key: str) -> "Optional[Any]": - return self.environ[self.prefix + key.replace("-", "_").upper()] - - def __len__(self) -> int: - return sum(1 for _ in iter(self)) - - def __iter__(self) -> "Generator[str, None, None]": - for k in self.environ: - if not isinstance(k, str): - continue - - k = k.replace("-", "_").upper() - if not k.startswith(self.prefix): - continue - - yield k[len(self.prefix) :] - - -def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: - """ - Returns True if either traces_sample_rate or traces_sampler is - defined and enable_tracing is set and not false. - """ - if options is None: - return False - - return bool( - options.get("enable_tracing") is not False - and ( - options.get("traces_sample_rate") is not None - or options.get("traces_sampler") is not None - ) - ) - - -def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return False - - return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" - - -def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return True - - return not options.get( - "stream_gen_ai_spans", True - ) and not has_span_streaming_enabled(options) - - -@contextlib.contextmanager -def record_sql_queries( - cursor: "Any", - query: "Any", - params_list: "Any", - paramstyle: "Optional[str]", - executemany: bool, - record_cursor_repr: bool = False, - span_origin: str = "manual", - span_op_override_value: "Optional[str]" = None, -) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": - # TODO: Bring back capturing of params by default - client = sentry_sdk.get_client() - if client.options["_experiments"].get("record_sql_params", False): - if not params_list or params_list == [None]: - params_list = None - - if paramstyle == "pyformat": - paramstyle = "format" - else: - params_list = None - paramstyle = None - - query = _format_sql(cursor, query) - - data = {} - if params_list is not None: - data["db.params"] = params_list - if paramstyle is not None: - data["db.paramstyle"] = paramstyle - if executemany: - data["db.executemany"] = True - if record_cursor_repr and cursor is not None: - data["db.cursor"] = cursor - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message=query, category="query", data=data) - - if has_span_streaming_enabled(client.options): - additional_attributes = {} - if query is not None: - additional_attributes["db.query.text"] = query - - with sentry_sdk.traces.start_span( - name="" if query is None else query, - attributes={ - "sentry.origin": span_origin, - "sentry.op": span_op_override_value - if span_op_override_value - else OP.DB, - **additional_attributes, - }, - ) as span: - yield span - else: - with sentry_sdk.start_span( - op=span_op_override_value if span_op_override_value is not None else OP.DB, - name=query, - origin=span_origin, - ) as span: - for k, v in data.items(): - span.set_data(k, v) - yield span - - -def maybe_create_breadcrumbs_from_span( - scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" -) -> None: - if span.op == OP.DB_REDIS: - scope.add_breadcrumb( - message=span.description, type="redis", category="redis", data=span._tags - ) - - elif span.op == OP.HTTP_CLIENT: - level = None - status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) - if status_code: - if 500 <= status_code <= 599: - level = "error" - elif 400 <= status_code <= 499: - level = "warning" - - if level: - scope.add_breadcrumb( - type="http", category="httplib", data=span._data, level=level - ) - else: - scope.add_breadcrumb(type="http", category="httplib", data=span._data) - - elif span.op == "subprocess": - scope.add_breadcrumb( - type="subprocess", - category="subprocess", - message=span.description, - data=span._data, - ) - - -def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": - try: - return frame.f_code.co_filename - except Exception: - return None - - -def _should_be_included( - is_sentry_sdk_frame: bool, - namespace: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", - abs_path: "Optional[str]", - project_root: "Optional[str]", -) -> bool: - # in_app_include takes precedence over in_app_exclude - should_be_included = _module_in_list(namespace, in_app_include) - should_be_excluded = _is_external_source(abs_path) or _module_in_list( - namespace, in_app_exclude - ) - return not is_sentry_sdk_frame and ( - should_be_included - or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) - ) - - -def add_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", - project_root: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", -) -> None: - """ - Adds OTel compatible source code information to the span - """ - # Find the correct frame - frame: "Union[FrameType, None]" = sys._getframe() - while frame is not None: - abs_path = _get_frame_module_abs_path(frame) - - try: - namespace: "Optional[str]" = frame.f_globals.get("__name__") - except Exception: - namespace = None - - is_sentry_sdk_frame = namespace is not None and namespace.startswith( - "sentry_sdk." - ) - - should_be_included = _should_be_included( - is_sentry_sdk_frame=is_sentry_sdk_frame, - namespace=namespace, - in_app_include=in_app_include, - in_app_exclude=in_app_exclude, - abs_path=abs_path, - project_root=project_root, - ) - if should_be_included: - break - - frame = frame.f_back - else: - frame = None - - # Set the data - if frame is not None: - try: - lineno = frame.f_lineno - except Exception: - lineno = None - if lineno is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_LINENO, lineno) - else: - span.set_attribute("code.line.number", lineno) - - try: - namespace = frame.f_globals.get("__name__") - except Exception: - namespace = None - if namespace is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_NAMESPACE, namespace) - else: - span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) - - filepath = _get_frame_module_abs_path(frame) - if filepath is not None: - if namespace is not None: - in_app_path = filename_for_module(namespace, filepath) - elif project_root is not None and filepath.startswith(project_root): - in_app_path = filepath.replace(project_root, "").lstrip(os.sep) - else: - in_app_path = filepath - - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) - else: - if in_app_path is not None: - span.set_attribute("code.file.path", in_app_path) - - try: - code_function = frame.f_code.co_name - except Exception: - code_function = None - - if code_function is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - else: - span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - - -def add_query_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a database query span - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_query_source = client.options.get("enable_db_query_source", True) - if not should_add_query_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("db_query_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def add_http_request_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a span for an outgoing HTTP request - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_request_source = client.options.get("enable_http_request_source", True) - if not should_add_request_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("http_request_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def extract_sentrytrace_data( - header: "Optional[str]", -) -> "Optional[Dict[str, Union[str, bool, None]]]": - """ - Given a `sentry-trace` header string, return a dictionary of data. - """ - if not header: - return None - - if "," in header: - # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. - parts = [part.strip() for part in header.split(",")] - header = next((part for part in parts if part), "") - - if not header: - return None - - if header.startswith("00-") and header.endswith("-00"): - header = header[3:-3] - - match = SENTRY_TRACE_REGEX.match(header) - if not match: - return None - - trace_id, parent_span_id, sampled_str = match.groups() - parent_sampled = None - - if trace_id: - trace_id = "{:032x}".format(int(trace_id, 16)) - if parent_span_id: - parent_span_id = "{:016x}".format(int(parent_span_id, 16)) - if sampled_str: - parent_sampled = sampled_str != "0" - - return { - "trace_id": trace_id, - "parent_span_id": parent_span_id, - "parent_sampled": parent_sampled, - } - - -def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": - real_sql = None - - # If we're using psycopg2, it could be that we're - # looking at a query that uses Composed objects. Use psycopg2's mogrify - # function to format the query. We lose per-parameter trimming but gain - # accuracy in formatting. - try: - if hasattr(cursor, "mogrify"): - real_sql = cursor.mogrify(sql) - if isinstance(real_sql, bytes): - real_sql = real_sql.decode(cursor.connection.encoding) - except Exception: - real_sql = None - - return real_sql or to_string(sql) - - -class PropagationContext: - """ - The PropagationContext represents the data of a trace in Sentry. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "parent_sampled", - "baggage", - "custom_sampling_context", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - dynamic_sampling_context: "Optional[Dict[str, str]]" = None, - baggage: "Optional[Baggage]" = None, - ) -> None: - self._trace_id = trace_id - """The trace id of the Sentry trace.""" - - self._span_id = span_id - """The span id of the currently executing span.""" - - self.parent_span_id = parent_span_id - """The id of the parent span that started this span. - The parent span could also be a span in an upstream service.""" - - self.parent_sampled = parent_sampled - """Boolean indicator if the parent span was sampled. - Important when the parent span originated in an upstream service, - because we want to sample the whole trace, or nothing from the trace.""" - - self.baggage = baggage - """Parsed baggage header that is used for dynamic sampling decisions.""" - - """DEPRECATED this only exists for backwards compat of constructor.""" - if baggage is None and dynamic_sampling_context is not None: - self.baggage = Baggage(dynamic_sampling_context) - - self.custom_sampling_context: "Optional[dict[str, Any]]" = None - - @classmethod - def from_incoming_data( - cls, incoming_data: "Dict[str, Any]" - ) -> "PropagationContext": - propagation_context = PropagationContext() - normalized_data = normalize_incoming_data(incoming_data) - - sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) - sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) - - # nothing to propagate if no sentry-trace - if sentrytrace_data is None: - return propagation_context - - baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) - baggage = ( - Baggage.from_incoming_header(baggage_header) if baggage_header else None - ) - - if not _should_continue_trace(baggage): - return propagation_context - - propagation_context.update(sentrytrace_data) - if baggage: - propagation_context.baggage = baggage - - propagation_context._fill_sample_rand() - - return propagation_context - - @property - def trace_id(self) -> str: - """The trace id of the Sentry trace.""" - if not self._trace_id: - # New trace, don't fill in sample_rand - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - """The span id of the currently executed span.""" - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - @property - def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": - return self.get_baggage().dynamic_sampling_context() - - def to_traceparent(self) -> str: - return f"{self.trace_id}-{self.span_id}" - - def get_baggage(self) -> "Baggage": - if self.baggage is None: - self.baggage = Baggage.populate_from_propagation_context(self) - return self.baggage - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. - """ - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def update(self, other_dict: "Dict[str, Any]") -> None: - """ - Updates the PropagationContext with data from the given dictionary. - """ - for key, value in other_dict.items(): - try: - setattr(self, key, value) - except AttributeError: - pass - - def _set_custom_sampling_context( - self, custom_sampling_context: "dict[str, Any]" - ) -> None: - self.custom_sampling_context = custom_sampling_context - - def __repr__(self) -> str: - return "".format( - self._trace_id, - self._span_id, - self.parent_span_id, - self.parent_sampled, - self.baggage, - ) - - def _fill_sample_rand(self) -> None: - """ - Ensure that there is a valid sample_rand value in the baggage. - - If there is a valid sample_rand value in the baggage, we keep it. - Otherwise, we generate a sample_rand value according to the following: - - - If we have a parent_sampled value and a sample_rate in the DSC, we compute - a sample_rand value randomly in the range: - - [0, sample_rate) if parent_sampled is True, - - or, in the range [sample_rate, 1) if parent_sampled is False. - - - If either parent_sampled or sample_rate is missing, we generate a random - value in the range [0, 1). - - The sample_rand is deterministically generated from the trace_id, if present. - - This function does nothing if there is no baggage. - """ - if self.baggage is None: - return - - sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) - if sample_rand is not None and 0 <= sample_rand < 1: - # sample_rand is present and valid, so don't overwrite it - return - - # Get the sample rate and compute the transformation that will map the random value - # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). - sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) - lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) - - try: - sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) - except ValueError: - # ValueError is raised if the interval is invalid, i.e. lower >= upper. - # lower >= upper might happen if the incoming trace's sampled flag - # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. - # We cannot generate a sensible sample_rand value in this case. - logger.debug( - f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " - f"and sample_rate={sample_rate}." - ) - return - - self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 - - def _sample_rand(self) -> "Optional[str]": - """Convenience method to get the sample_rand value from the baggage.""" - if self.baggage is None: - return None - - return self.baggage.sentry_items.get("sample_rand") - - -class Baggage: - """ - The W3C Baggage header information (see https://www.w3.org/TR/baggage/). - - Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. - Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but - it is the caller's responsibility to enforce this restriction. - """ - - __slots__ = ("sentry_items", "third_party_items", "mutable") - - SENTRY_PREFIX = "sentry-" - SENTRY_PREFIX_REGEX = re.compile("^sentry-") - - def __init__( - self, - sentry_items: "Dict[str, str]", - third_party_items: str = "", - mutable: bool = True, - ): - self.sentry_items = sentry_items - self.third_party_items = third_party_items - self.mutable = mutable - - @classmethod - def from_incoming_header( - cls, - header: "Optional[str]", - *, - _sample_rand: "Optional[str]" = None, - ) -> "Baggage": - """ - freeze if incoming header already has sentry baggage - """ - sentry_items = {} - third_party_items = "" - mutable = True - - if header: - for item in header.split(","): - if "=" not in item: - continue - - with capture_internal_exceptions(): - item = item.strip() - key, val = item.split("=", 1) - if Baggage.SENTRY_PREFIX_REGEX.match(key): - baggage_key = unquote(key.split("-")[1]) - sentry_items[baggage_key] = unquote(val) - mutable = False - else: - third_party_items += ("," if third_party_items else "") + item - - if _sample_rand is not None: - sentry_items["sample_rand"] = str(_sample_rand) - mutable = False - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": - """ - Deprecated: use populate_from_propagation_context - """ - if scope._propagation_context is None: - return Baggage({}) - - return Baggage.populate_from_propagation_context(scope._propagation_context) - - @classmethod - def populate_from_propagation_context( - cls, propagation_context: "PropagationContext" - ) -> "Baggage": - sentry_items: "Dict[str, str]" = {} - third_party_items = "" - mutable = False - - client = sentry_sdk.get_client() - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options - - sentry_items["trace_id"] = propagation_context.trace_id - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if options.get("traces_sample_rate"): - sentry_items["sample_rate"] = str(options["traces_sample_rate"]) - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def populate_from_transaction( - cls, transaction: "sentry_sdk.tracing.Transaction" - ) -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - transaction.name - and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES - ): - sentry_items["transaction"] = transaction.name - - if transaction.sample_rate is not None: - sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction.sampled is not None: - sentry_items["sampled"] = "true" if transaction.sampled else "false" - - # there's an existing baggage but it was mutable, - # which is why we are creating this new baggage. - # However, if by chance the user put some sentry items in there, give them precedence. - if transaction._baggage and transaction._baggage.sentry_items: - sentry_items.update(transaction._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - @classmethod - def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = segment.trace_id - sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - segment.get_attributes().get("sentry.span.source") - not in LOW_QUALITY_SEGMENT_SOURCES - ) and segment._name: - sentry_items["transaction"] = segment._name - - if segment._sample_rate is not None: - sentry_items["sample_rate"] = str(segment._sample_rate) - - if segment.sampled is not None: - sentry_items["sampled"] = "true" if segment.sampled else "false" - - # There's an existing baggage but it was mutable, which is why we are - # creating this new baggage. - # However, if by chance the user put some sentry items in there, give - # them precedence. - if segment._baggage and segment._baggage.sentry_items: - sentry_items.update(segment._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - def freeze(self) -> None: - self.mutable = False - - def dynamic_sampling_context(self) -> "Dict[str, str]": - header = {} - - for key, item in self.sentry_items.items(): - header[key] = item - - return header - - def serialize(self, include_third_party: bool = False) -> str: - items = [] - - for key, val in self.sentry_items.items(): - with capture_internal_exceptions(): - item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) - items.append(item) - - if include_third_party: - items.append(self.third_party_items) - - return ",".join(items) - - @staticmethod - def strip_sentry_baggage(header: str) -> str: - """Remove Sentry baggage from the given header. - - Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. - """ - return ",".join( - ( - item - for item in header.split(",") - if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) - ) - ) - - def _sample_rand(self) -> "Optional[float]": - """Convenience method to get the sample_rand value from the sentry_items. - - We validate the value and parse it as a float before returning it. The value is considered - valid if it is a float in the range [0, 1). - """ - sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) - - if sample_rand is not None and 0.0 <= sample_rand < 1.0: - return sample_rand - - return None - - def __repr__(self) -> str: - return f'' - - -def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: - """ - Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. - """ - trace_propagation_targets = client.options["trace_propagation_targets"] - - if is_sentry_url(client, url): - return False - - return match_regex_list(url, trace_propagation_targets, substring_matching=True) - - -def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": - """ - Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. - """ - data = {} - for key, value in incoming_data.items(): - if key.startswith("HTTP_"): - key = key[5:] - - key = key.replace("_", "-").lower() - data[key] = value - - return data - - -def create_span_decorator( - op: "Optional[Union[str, OP]]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Any": - """ - Create a span decorator that can wrap both sync and async functions. - - :param op: The operation type for the span. - :type op: str or :py:class:`sentry_sdk.consts.OP` or None - :param name: The name of the span. - :type name: str or None - :param attributes: Additional attributes to set on the span. - :type attributes: dict or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - """ - from sentry_sdk.scope import should_send_default_pii - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return await f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return await f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = await f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def create_streaming_span_decorator( - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Any": - """ - Create a span creating decorator that can wrap both sync and async functions. - """ - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - result = await f(*args, **kwargs) - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - return f(*args, **kwargs) - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - -def _generate_sample_rand( - trace_id: "Optional[str]", - *, - interval: "tuple[float, float]" = (0.0, 1.0), -) -> float: - """Generate a sample_rand value from a trace ID. - - The generated value will be pseudorandomly chosen from the provided - interval. Specifically, given (lower, upper) = interval, the generated - value will be in the range [lower, upper). The value has 6-digit precision, - so when printing with .6f, the value will never be rounded up. - - The pseudorandom number generator is seeded with the trace ID. - """ - lower, upper = interval - if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly - raise ValueError("Invalid interval: lower must be less than upper") - - rng = Random(trace_id) - lower_scaled = int(lower * 1_000_000) - upper_scaled = int(upper * 1_000_000) - try: - sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) - except ValueError: - # In some corner cases it might happen that the range is too small - # In that case, just take the lower bound - sample_rand_scaled = lower_scaled - - return sample_rand_scaled / 1_000_000 - - -def _sample_rand_range( - parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" -) -> "tuple[float, float]": - """ - Compute the lower (inclusive) and upper (exclusive) bounds of the range of values - that a generated sample_rand value must fall into, given the parent_sampled and - sample_rate values. - """ - if parent_sampled is None or sample_rate is None: - return 0.0, 1.0 - elif parent_sampled is True: - return 0.0, sample_rate - else: # parent_sampled is False - return sample_rate, 1.0 - - -def _get_value(source: "Any", key: str) -> "Optional[Any]": - """ - Gets a value from a source object. The source can be a dict or an object. - It is checked for dictionary keys and object attributes. - """ - value = None - if isinstance(source, dict): - value = source.get(key) - else: - if hasattr(source, key): - try: - value = getattr(source, key) - except Exception: - value = None - return value - - -def _get_span_name( - template: "Union[str, SPANTEMPLATE]", - name: str, - kwargs: "Optional[dict[str, Any]]" = None, -) -> str: - """ - Get the name of the span based on the template and the name. - """ - span_name = name - - if template == SPANTEMPLATE.AI_CHAT: - model = None - if kwargs: - for key in ("model", "model_name"): - if kwargs.get(key) and isinstance(kwargs[key], str): - model = kwargs[key] - break - - span_name = f"chat {model}" if model else "chat" - - elif template == SPANTEMPLATE.AI_AGENT: - span_name = f"invoke_agent {name}" - - elif template == SPANTEMPLATE.AI_TOOL: - span_name = f"execute_tool {name}" - - return span_name - - -def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: - """ - Get the operation of the span based on the template. - """ - mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { - SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, - SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, - SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, - } - op = mapping.get(template, OP.FUNCTION) - - return str(op) - - -def _get_input_attributes( - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Get input attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - mapping = { - "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), - "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), - "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), - "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), - "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), - "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), - "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), - "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), - } - - def _set_from_key(key: str, value: "Any") -> None: - if key in mapping: - (attribute, data_type) = mapping[key] - if value is not None and isinstance(value, data_type): - attributes[attribute] = value - - for key, value in list(kwargs.items()): - if key == "prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "user", "content": value} - ) - continue - - if key == "system_prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "system", "content": value} - ) - continue - - _set_from_key(key, value) - - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) - - # Coerce to string - if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) - - return attributes - - -def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": - """ - Get usage attributes. - """ - attributes = {} - - def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: - for key in keys: - value = _get_value(usage, key) - if value is not None and isinstance(value, int): - attributes[attribute] = value - - _set_from_keys( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - ("prompt_tokens", "input_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - ("completion_tokens", "output_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - ("total_tokens",), - ) - - return attributes - - -def _get_output_attributes( - template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> "dict[str, Any]": - """ - Get output attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - with capture_internal_exceptions(): - # Usage from result, result.usage, and result.metadata.usage - usage_candidates = [result] - - usage = _get_value(result, "usage") - usage_candidates.append(usage) - - meta = _get_value(result, "metadata") - usage = _get_value(meta, "usage") - usage_candidates.append(usage) - - for usage_candidate in usage_candidates: - if usage_candidate is not None: - attributes.update(_get_usage_attributes(usage_candidate)) - - # Response model - model_name = _get_value(result, "model") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - model_name = _get_value(result, "model_name") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - # Tool output - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) - - return attributes - - -def _set_input_attributes( - span: "Span", - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - name: str, - f: "Any", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> None: - """ - Set span input attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param f: The wrapped function. - :param args: The arguments to the wrapped function. - :param kwargs: The keyword arguments to the wrapped function. - """ - attributes: "dict[str, Any]" = {} - - if template == SPANTEMPLATE.AI_AGENT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: name, - } - elif template == SPANTEMPLATE.AI_CHAT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - } - elif template == SPANTEMPLATE.AI_TOOL: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: name, - } - - docstring = f.__doc__ - if docstring is not None: - attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - - attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.update_data(attributes or {}) - - -def _set_output_attributes( - span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> None: - """ - Set span output attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param result: The result of the wrapped function. - """ - span.update_data(_get_output_attributes(template, send_pii, result) or {}) - - -def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: - """ - Check if we should continue the incoming trace according to the strict_trace_continuation spec. - https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation - """ - - client = sentry_sdk.get_client() - parsed_dsn = client.parsed_dsn - client_org_id = parsed_dsn.org_id if parsed_dsn else None - baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None - - if ( - client_org_id is not None - and baggage_org_id is not None - and client_org_id != baggage_org_id - ): - logger.debug( - f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - strict_trace_continuation: bool = client.options.get( - "strict_trace_continuation", False - ) - if strict_trace_continuation: - if (baggage_org_id is not None and client_org_id is None) or ( - baggage_org_id is None and client_org_id is not None - ): - logger.debug( - f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - return True - - -def add_sentry_baggage_to_headers( - headers: "MutableMapping[str, str]", sentry_baggage: str -) -> None: - """Add the Sentry baggage to the headers. - - This function directly mutates the provided headers. The provided sentry_baggage - is appended to the existing baggage. If the baggage already contains Sentry items, - they are stripped out first. - """ - existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") - stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) - - separator = "," if len(stripped_existing_baggage) > 0 else "" - - headers[BAGGAGE_HEADER_NAME] = ( - stripped_existing_baggage + separator + sentry_baggage - ) - - -def _make_sampling_decision( - name: str, - attributes: "Optional[Attributes]", - scope: "sentry_sdk.Scope", -) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": - """ - Decide whether a span should be sampled. - - Returns a tuple with: - 1. the sampling decision - 2. the effective sample rate - 3. the sample rand - 4. the reason for not sampling the span, if unsampled - """ - client = sentry_sdk.get_client() - - if not has_tracing_enabled(client.options): - return False, None, None, None - - propagation_context = scope.get_active_propagation_context() - - sample_rand = None - if propagation_context.baggage is not None: - sample_rand = propagation_context.baggage._sample_rand() - if sample_rand is None: - sample_rand = _generate_sample_rand(propagation_context.trace_id) - - # If there's a traces_sampler, use that; otherwise use traces_sample_rate - traces_sampler_defined = callable(client.options.get("traces_sampler")) - if traces_sampler_defined: - sampling_context = { - "span_context": { - "name": name, - "trace_id": propagation_context.trace_id, - "parent_span_id": propagation_context.parent_span_id, - "parent_sampled": propagation_context.parent_sampled, - "attributes": dict(attributes) if attributes else {}, - }, - } - - if propagation_context.custom_sampling_context: - sampling_context.update(propagation_context.custom_sampling_context) - - sample_rate = client.options["traces_sampler"](sampling_context) - else: - if propagation_context.parent_sampled is not None: - sample_rate = propagation_context.parent_sampled - else: - sample_rate = client.options["traces_sample_rate"] - - # Validate whether the sample_rate we got is actually valid. Since - # traces_sampler is user-provided, it could return anything. - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") - return False, None, None, "sample_rate" - - sample_rate = float(sample_rate) - if not sample_rate: - if traces_sampler_defined: - reason = "traces_sampler returned 0 or False" - else: - reason = "traces_sample_rate is set to 0" - - logger.debug(f"[Tracing] Discarding {name} because {reason}") - return False, 0.0, None, "sample_rate" - - # Adjust sample rate if we're under backpressure - sample_rate_before_backpressure = sample_rate - if client.monitor: - sample_rate /= 2**client.monitor.downsample_factor - - if not sample_rate: - logger.debug(f"[Tracing] Discarding {name} because backpressure") - return False, 0.0, None, "backpressure" - - # Make the actual decision - sampled = sample_rand < sample_rate - - if sampled: - logger.debug(f"[Tracing] Starting {name}") - outcome = None - - else: - # Determine why exactly the span will not be sampled. If we've lowered - # the effective sample_rate because of backpressure, check whether the - # span would've been sampled if backpressure wasn't active. If that's the - # case, backpressure is the actual reason, otherwise just pure sampling - # rate. - if ( - sample_rate_before_backpressure != sample_rate - and sample_rand < sample_rate_before_backpressure - ): - logger.debug(f"[Tracing] Discarding {name} because backpressure") - outcome = "backpressure" - - else: - logger.debug( - f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" - ) - outcome = "sample_rate" - - return sampled, sample_rate, sample_rand, outcome - - -def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: - """Determine if a span fits one of the rules in ignore_spans.""" - client = sentry_sdk.get_client() - ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") - - if not ignore_spans: - return False - - def _matches(rule: "Any", value: "Any") -> bool: - if isinstance(rule, Pattern): - if isinstance(value, str): - return bool(rule.fullmatch(value)) - else: - return False - - return rule == value - - for rule in ignore_spans: - if isinstance(rule, (str, Pattern)): - if _matches(rule, name): - return True - - elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): - name_matches = True - attributes_match = True - - if "name" in rule: - name_matches = _matches(rule["name"], name) - - if "attributes" in rule: - attributes = attributes or {} - - for attribute, value in rule["attributes"].items(): - if attribute not in attributes or not _matches( - value, attributes[attribute] - ): - attributes_match = False - break - - if name_matches and attributes_match: - return True - - return False - - -# Circular imports -from sentry_sdk.traces import ( - LOW_QUALITY_SEGMENT_SOURCES, - StreamedSpan, -) -from sentry_sdk.traces import ( - start_span as start_streaming_span, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - LOW_QUALITY_TRANSACTION_SOURCES, - SENTRY_TRACE_HEADER_NAME, - Span, -) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py deleted file mode 100644 index 2b71fee429..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/transport.py +++ /dev/null @@ -1,1255 +0,0 @@ -import asyncio -import gzip -import io -import json -import logging -import os -import socket -import ssl -import time -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from urllib.request import getproxies - -try: - import brotli # type: ignore -except ImportError: - brotli = None - -try: - import httpcore -except ImportError: - httpcore = None # type: ignore[assignment,unused-ignore] - -try: - import h2 # noqa: F401 - - HTTP2_ENABLED = httpcore is not None -except ImportError: - HTTP2_ENABLED = False - -try: - import anyio # noqa: F401 - - ASYNC_TRANSPORT_AVAILABLE = httpcore is not None -except ImportError: - ASYNC_TRANSPORT_AVAILABLE = False - -from typing import TYPE_CHECKING, Dict, List, cast - -import certifi -import urllib3 - -import sentry_sdk -from sentry_sdk.consts import EndpointType -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - logger, - mark_sentry_task_internal, -) -from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - DefaultDict, - Iterable, - Mapping, - Optional, - Self, - Tuple, - Type, - Union, - ) - - from urllib3.poolmanager import PoolManager, ProxyManager - - from sentry_sdk._types import Event, EventDataCategory - -KEEP_ALIVE_SOCKET_OPTIONS = [] -for option in [ - (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 -]: - try: - KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) - except AttributeError: - # a specific option might not be available on specific systems, - # e.g. TCP_KEEPIDLE doesn't exist on macOS - pass - - -def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": - """Case-insensitive header lookup for httpcore-style responses.""" - header_lower = header.lower() - return next( - ( - val.decode("ascii") - for key, val in response.headers - if key.decode("ascii").lower() == header_lower - ), - None, - ) - - -class Transport(ABC): - """Baseclass for all transports. - - A transport is used to send an event to sentry. - """ - - parsed_dsn: "Optional[Dsn]" = None - - def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: - self.options = options - if options and options["dsn"]: - self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) - else: - self.parsed_dsn = None - - def capture_event(self: "Self", event: "Event") -> None: - """ - DEPRECATED: Please use capture_envelope instead. - - This gets invoked with the event dictionary when an event should - be sent to sentry. - """ - - warnings.warn( - "capture_event is deprecated, please use capture_envelope instead!", - DeprecationWarning, - stacklevel=2, - ) - - envelope = Envelope() - envelope.add_event(event) - self.capture_envelope(envelope) - - @abstractmethod - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - """ - Send an envelope to Sentry. - - Envelopes are a data container format that can hold any type of data - submitted to Sentry. We use it to send all event data (including errors, - transactions, crons check-ins, etc.) to Sentry. - """ - pass - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Any]" = None, - ) -> None: - """ - Wait `timeout` seconds for the current events to be sent out. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def kill(self: "Self") -> None: - """ - Forcefully kills the transport. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - """This increments a counter for event loss by reason and - data category by the given positive-int quantity (default 1). - - If an item is provided, the data category and quantity are - extracted from the item, and the values passed for - data_category and quantity are ignored. - - When recording a lost transaction via data_category="transaction", - the calling code should also record the lost spans via this method. - When recording lost spans, `quantity` should be set to the number - of contained spans, plus one for the transaction itself. When - passing an Item containing a transaction via the `item` parameter, - this method automatically records the lost spans. - """ - return None - - def is_healthy(self: "Self") -> bool: - return True - - -def _parse_rate_limits( - header: str, now: "Optional[datetime]" = None -) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": - if now is None: - now = datetime.now(timezone.utc) - - for limit in header.split(","): - try: - parameters = limit.strip().split(":") - retry_after_val, categories = parameters[:2] - - retry_after = now + timedelta(seconds=int(retry_after_val)) - for category in categories and categories.split(";") or (None,): - yield category, retry_after # type: ignore - except (LookupError, ValueError): - continue - - -class HttpTransportCore(Transport): - """Shared base class for sync and async transports.""" - - TIMEOUT = 30 # seconds - - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - from sentry_sdk.consts import VERSION - - Transport.__init__(self, options) - assert self.parsed_dsn is not None - self.options: "Dict[str, Any]" = options - self._worker = self._create_worker(options) - self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) - self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} - # We only use this Retry() class for the `get_retry_after` method it exposes - self._retry = urllib3.util.Retry() - self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( - defaultdict(int) - ) - self._last_client_report_sent = time.time() - - self._pool = self._make_pool() - - # Backwards compatibility for deprecated `self.hub_class` attribute - self._hub_cls = sentry_sdk.Hub - - experiments = options.get("_experiments", {}) - compression_level = experiments.get( - "transport_compression_level", - experiments.get("transport_zlib_compression_level"), - ) - compression_algo = experiments.get( - "transport_compression_algo", - ( - "gzip" - # if only compression level is set, assume gzip for backwards compatibility - # if we don't have brotli available, fallback to gzip - if compression_level is not None or brotli is None - else "br" - ), - ) - - if compression_algo == "br" and brotli is None: - logger.warning( - "You asked for brotli compression without the Brotli module, falling back to gzip -9" - ) - compression_algo = "gzip" - compression_level = None - - if compression_algo not in ("br", "gzip"): - logger.warning( - "Unknown compression algo %s, disabling compression", compression_algo - ) - self._compression_level = 0 - self._compression_algo = None - else: - self._compression_algo = compression_algo - - if compression_level is not None: - self._compression_level = compression_level - elif self._compression_algo == "gzip": - self._compression_level = 9 - elif self._compression_algo == "br": - self._compression_level = 4 - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - raise NotImplementedError() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - if not self.options["send_client_reports"]: - return - - if item is not None: - data_category = item.data_category - quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). - - if data_category == "transaction": - # Also record the lost spans - event = item.get_transaction_event() or {} - - # +1 for the transaction itself - span_count = ( - len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 - ) - self.record_lost_event(reason, "span", quantity=span_count) - - elif data_category == "log_item" and item: - # Also record size of lost logs in bytes - bytes_size = len(item.get_bytes()) - self.record_lost_event(reason, "log_byte", quantity=bytes_size) - - elif data_category == "attachment": - # quantity of 0 is actually 1 as we do not want to count - # empty attachments as actually empty. - quantity = len(item.get_bytes()) or 1 - - elif data_category is None: - raise TypeError("data category not provided") - - self._discarded_events[data_category, reason] += quantity - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return response.headers.get(header) - - def _update_rate_limits( - self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" - ) -> None: - # new sentries with more rate limit insights. We honor this header - # no matter of the status code to update our internal rate limits. - header = self._get_header_value(response, "x-sentry-rate-limits") - if header: - logger.warning("Rate-limited via x-sentry-rate-limits") - self._disabled_until.update(_parse_rate_limits(header)) - - # old sentries only communicate global rate limit hits via the - # retry-after header on 429. This header can also be emitted on new - # sentries if a proxy in front wants to globally slow things down. - elif response.status == 429: - logger.warning("Rate-limited via 429") - retry_after_value = self._get_header_value(response, "Retry-After") - retry_after = ( - self._retry.parse_retry_after(retry_after_value) - if retry_after_value is not None - else None - ) or 60 - self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( - seconds=retry_after - ) - - def _handle_request_error( - self: "Self", - envelope: "Optional[Envelope]", - loss_reason: str = "network", - record_reason: str = "network_error", - ) -> None: - def record_loss(reason: str) -> None: - if envelope is None: - self.record_lost_event(reason, data_category="error") - else: - for item in envelope.items: - self.record_lost_event(reason, item=item) - - self.on_dropped_event(loss_reason) - record_loss(record_reason) - - def _handle_response( - self: "Self", - response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", - envelope: "Optional[Envelope]", - ) -> None: - self._update_rate_limits(response) - - if response.status == 413: - size_exceeded_message = ( - "HTTP 413: Event dropped due to exceeded envelope size limit" - ) - response_message = getattr( - response, "data", getattr(response, "content", None) - ) - if response_message is not None: - size_exceeded_message += f" (body: {response_message})" - - logger.error(size_exceeded_message) - self._handle_request_error( - envelope=envelope, loss_reason="status_413", record_reason="send_error" - ) - - elif response.status == 429: - # if we hit a 429. Something was rate limited but we already - # acted on this in `self._update_rate_limits`. Note that we - # do not want to record event loss here as we will have recorded - # an outcome in relay already. - self.on_dropped_event("status_429") - pass - - elif response.status >= 300 or response.status < 200: - logger.error( - "Unexpected status code: %s (body: %s)", - response.status, - getattr(response, "data", getattr(response, "content", None)), - ) - self._handle_request_error( - envelope=envelope, loss_reason="status_{}".format(response.status) - ) - - def _update_headers( - self: "Self", - headers: "Dict[str, str]", - ) -> None: - headers.update( - { - "User-Agent": str(self._auth.client), - "X-Sentry-Auth": str(self._auth.to_header()), - } - ) - - def on_dropped_event(self: "Self", _reason: str) -> None: - return None - - def _fetch_pending_client_report( - self: "Self", force: bool = False, interval: int = 60 - ) -> "Optional[Item]": - if not self.options["send_client_reports"]: - return None - - if not (force or self._last_client_report_sent < time.time() - interval): - return None - - discarded_events = self._discarded_events - self._discarded_events = defaultdict(int) - self._last_client_report_sent = time.time() - - if not discarded_events: - return None - - return Item( - PayloadRef( - json={ - "timestamp": time.time(), - "discarded_events": [ - {"reason": reason, "category": category, "quantity": quantity} - for ( - (category, reason), - quantity, - ) in discarded_events.items() - ], - } - ), - type="client_report", - ) - - def _check_disabled(self, category: str) -> bool: - def _disabled(bucket: "Any") -> bool: - ts = self._disabled_until.get(bucket) - return ts is not None and ts > datetime.now(timezone.utc) - - return _disabled(category) or _disabled(None) - - def _is_rate_limited(self: "Self") -> bool: - return any( - ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() - ) - - def _is_worker_full(self: "Self") -> bool: - return self._worker.full() - - def is_healthy(self: "Self") -> bool: - return not (self._is_worker_full() or self._is_rate_limited()) - - def _prepare_envelope( - self: "Self", envelope: "Envelope" - ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": - # remove all items from the envelope which are over quota - new_items = [] - for item in envelope.items: - if self._check_disabled(item.data_category): - if item.data_category in ("transaction", "error", "default", "statsd"): - self.on_dropped_event("self_rate_limits") - self.record_lost_event("ratelimit_backoff", item=item) - else: - new_items.append(item) - - # Since we're modifying the envelope here make a copy so that others - # that hold references do not see their envelope modified. - envelope = Envelope(headers=envelope.headers, items=new_items) - - if not envelope.items: - return None - - # since we're already in the business of sending out an envelope here - # check if we have one pending for the stats session envelopes so we - # can attach it to this enveloped scheduled for sending. This will - # currently typically attach the client report to the most recent - # session update. - client_report_item = self._fetch_pending_client_report(interval=30) - if client_report_item is not None: - envelope.items.append(client_report_item) - - content_encoding, body = self._serialize_envelope(envelope) - - assert self.parsed_dsn is not None - logger.debug( - "Sending envelope [%s] project:%s host:%s", - envelope.description, - self.parsed_dsn.project_id, - self.parsed_dsn.host, - ) - - headers: "Dict[str, str]" = { - "Content-Type": "application/x-sentry-envelope", - } - if content_encoding: - headers["Content-Encoding"] = content_encoding - - return envelope, body, headers - - def _serialize_envelope( - self: "Self", envelope: "Envelope" - ) -> "tuple[Optional[str], io.BytesIO]": - content_encoding = None - body = io.BytesIO() - if self._compression_level == 0 or self._compression_algo is None: - envelope.serialize_into(body) - else: - content_encoding = self._compression_algo - if self._compression_algo == "br" and brotli is not None: - body.write( - brotli.compress( - envelope.serialize(), quality=self._compression_level - ) - ) - else: # assume gzip as we sanitize the algo value in init - with gzip.GzipFile( - fileobj=body, mode="w", compresslevel=self._compression_level - ) as f: - envelope.serialize_into(f) - - return content_encoding, body - - def _get_httpcore_pool_options( - self: "Self", http2: bool = False - ) -> "Dict[str, Any]": - """Shared pool options for httpcore-based transports (Http2 and Async).""" - options: "Dict[str, Any]" = { - "http2": http2, - "retries": 3, - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - ssl_context = ssl.create_default_context() - ssl_context.load_verify_locations( - self.options["ca_certs"] - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") - key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") - if cert_file is not None: - ssl_context.load_cert_chain(cert_file, key_file) - - options["ssl_context"] = ssl_context - return options - - def _resolve_proxy(self: "Self") -> "Optional[str]": - """Resolve proxy URL from options and environment. Returns proxy URL or None.""" - if self.parsed_dsn is None: - return None - - no_proxy = self._in_no_proxy(self.parsed_dsn) - proxy = None - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - return proxy or None - - @property - def _timeout_extensions(self: "Self") -> "Dict[str, Any]": - return { - "timeout": { - "pool": self.TIMEOUT, - "connect": self.TIMEOUT, - "write": self.TIMEOUT, - "read": self.TIMEOUT, - } - } - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - raise NotImplementedError() - - def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: - no_proxy = getproxies().get("no") - if not no_proxy: - return False - for host in no_proxy.split(","): - host = host.strip() - if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): - return True - return False - - def _make_pool( - self: "Self", - ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - raise NotImplementedError() - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": - raise NotImplementedError() - - def kill(self: "Self") -> None: - logger.debug("Killing HTTP transport") - self._worker.kill() - - -# Keep BaseHttpTransport as an alias for backwards compatibility -# and for the sync transport implementation -class BaseHttpTransport(HttpTransportCore): - """The base HTTP transport (synchronous).""" - - def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - response.close() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return BackgroundWorker(queue_size=options["transport_queue_size"]) - - def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def capture_envelope( - self, - envelope: "Envelope", - ) -> None: - def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - self._send_envelope(envelope) - self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - self._worker.flush(timeout, callback) - - @staticmethod - def _warn_hub_cls() -> None: - """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" - warnings.warn( - "The `hub_cls` attribute is deprecated and will be removed in a future release.", - DeprecationWarning, - stacklevel=3, - ) - - @property - def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - return self._hub_cls - - @hub_cls.setter - def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - self._hub_cls = value - - -class HttpTransport(BaseHttpTransport): - if TYPE_CHECKING: - _pool: "Union[PoolManager, ProxyManager]" - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - num_pools = self.options.get("_experiments", {}).get("transport_num_pools") - options = { - "num_pools": 2 if num_pools is None else int(num_pools), - "cert_reqs": "CERT_REQUIRED", - "timeout": urllib3.Timeout(total=self.TIMEOUT), - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if self.options["keep_alive"]: - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - options["ca_certs"] = ( - self.options["ca_certs"] # User-provided bundle from the SDK init - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - - options["cert_file"] = self.options["cert_file"] or os.environ.get( - "CLIENT_CERT_FILE" - ) - options["key_file"] = self.options["key_file"] or os.environ.get( - "CLIENT_KEY_FILE" - ) - - return options - - def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = None - no_proxy = self._in_no_proxy(self.parsed_dsn) - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - use_socks_proxy = True - try: - # Check if PySocks dependency is available - from urllib3.contrib.socks import SOCKSProxyManager - except ImportError: - use_socks_proxy = False - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", - proxy, - ) - - if use_socks_proxy: - return SOCKSProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - else: - return urllib3.ProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "urllib3.BaseHTTPResponse": - return self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - body=body, - headers=headers, - ) - - -class AsyncHttpTransport(HttpTransportCore): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - if not ASYNC_TRANSPORT_AVAILABLE: - raise RuntimeError( - "AsyncHttpTransport requires httpcore[asyncio]. " - "Install it with: pip install sentry-sdk[asyncio]" - ) - super().__init__(options) - # Requires event loop at init time - self.loop = asyncio.get_running_loop() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return AsyncWorker(queue_size=options["transport_queue_size"]) - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - async def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - await self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - async def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = await self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - await response.aclose() - - async def _request( # type: ignore[override] - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - return await self._pool.request( # type: ignore[misc,unused-ignore] - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - - async def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def _capture_envelope(self: "Self", envelope: "Envelope") -> None: - async def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - await self._send_envelope(envelope) - await self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - # Synchronous entry point - if self.loop and self.loop.is_running(): - self.loop.call_soon_threadsafe(self._capture_envelope, envelope) - else: - # The event loop is no longer running - logger.warning("Async Transport is not running in an event loop.") - self.on_dropped_event("internal_sdk_error") - for item in envelope.items: - self.record_lost_event("internal_sdk_error", item=item) - - def flush( # type: ignore[override] - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> "Optional[asyncio.Task[None]]": - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] - return None - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=HTTP2_ENABLED - and self.parsed_dsn is not None - and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - socks_opts = opts.copy() - if "socket_options" in socks_opts: - socket_options = socks_opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) - - return httpcore.AsyncConnectionPool(**opts) - - def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] - logger.debug("Killing HTTP transport") - self._worker.kill() - try: - # Return the pool cleanup task so caller can await it if needed - with mark_sentry_task_internal(): - return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] - except RuntimeError: - logger.warning("Event loop not running, aborting kill.") - return None - - -if not HTTP2_ENABLED: - # Sorry, no Http2Transport for you - class Http2Transport(HttpTransport): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - super().__init__(options) - logger.warning( - "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." - ) - -else: - - class Http2Transport(BaseHttpTransport): # type: ignore - """The HTTP2 transport based on httpcore.""" - - TIMEOUT = 15 - - if TYPE_CHECKING: - _pool: """Union[ - httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool - ]""" - - def _get_header_value( - self: "Self", response: "httpcore.Response", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - response = self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - return response - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - if "socket_options" in opts: - socket_options = opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.SOCKSProxy(proxy_url=proxy, **opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.HTTPProxy(proxy_url=proxy, **opts) - - return httpcore.ConnectionPool(**opts) - - -class _EnvelopePrinterTransport(Transport): - """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" - - def __init__(self, transport: "Transport") -> None: - Transport.__init__(self, options=transport.options) - self._inner = transport - self.parsed_dsn = transport.parsed_dsn - - self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") - self.envelope_logger.setLevel(logging.INFO) - self.envelope_logger.propagate = False - if not self.envelope_logger.handlers: - handler = logging.StreamHandler() - handler.setLevel(logging.INFO) - handler.setFormatter(logging.Formatter("%(message)s")) - self.envelope_logger.addHandler(handler) - - @property # type: ignore[misc] - def __class__(self) -> type: - return self._inner.__class__ - - def capture_envelope(self, envelope: "Envelope") -> None: - try: - self.envelope_logger.info("--- Sentry Envelope ---") - self.envelope_logger.info( - "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) - ) - for item in envelope.items: - self.envelope_logger.info(" Item type: %s", item.type) - self.envelope_logger.info( - " Item headers: %s", - json.dumps(item.headers, indent=2, default=str), - ) - try: - payload = json.loads(item.get_bytes()) - self.envelope_logger.info( - " Payload:\n%s", - json.dumps(payload, indent=2, default=str), - ) - except (ValueError, TypeError): - self.envelope_logger.info( - " Payload: ", - len(item.get_bytes()), - ) - self.envelope_logger.info("--- End Envelope ---") - except Exception: - pass - - self._inner.capture_envelope(envelope) - - def flush( - self, - timeout: float, - callback: "Optional[Any]" = None, - ) -> "Any": - return self._inner.flush(timeout, callback) - - def kill(self) -> "Any": - return self._inner.kill() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - self._inner.record_lost_event(reason, data_category, item, quantity=quantity) - - def is_healthy(self) -> bool: - return self._inner.is_healthy() - - def __getattr__(self, name: str) -> "Any": - return getattr(self._inner, name) - - -class _FunctionTransport(Transport): - """ - DEPRECATED: Users wishing to provide a custom transport should subclass - the Transport class, rather than providing a function. - """ - - def __init__( - self, - func: "Callable[[Event], None]", - ) -> None: - Transport.__init__(self) - self._func = func - - def capture_event( - self, - event: "Event", - ) -> None: - self._func(event) - return None - - def capture_envelope(self, envelope: "Envelope") -> None: - # Since function transports expect to be called with an event, we need - # to iterate over the envelope and call the function for each event, via - # the deprecated capture_event method. - event = envelope.get_event() - if event is not None: - self.capture_event(event) - - -def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": - ref_transport = options["transport"] - - use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) - use_async_transport = options.get("_experiments", {}).get("transport_async", False) - async_integration = any( - integration.__class__.__name__ == "AsyncioIntegration" - for integration in options.get("integrations") or [] - ) - - # By default, we use the http transport class - transport_cls: "Type[Transport]" = ( - Http2Transport if use_http2_transport else HttpTransport - ) - - if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: - try: - asyncio.get_running_loop() - if async_integration: - if use_http2_transport: - logger.warning( - "HTTP/2 transport is not supported with async transport. " - "Ignoring transport_http2 experiment." - ) - transport_cls = AsyncHttpTransport - else: - logger.warning( - "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." - ) - except RuntimeError: - # No event loop running, fall back to sync transport - logger.warning("No event loop running, falling back to sync transport.") - elif use_async_transport: - logger.warning( - "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." - ) - - transport: "Optional[Transport]" = None - - if isinstance(ref_transport, Transport): - transport = ref_transport - elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): - transport_cls = ref_transport - elif callable(ref_transport): - warnings.warn( - "Function transports are deprecated and will be removed in a future release." - "Please provide a Transport instance or subclass, instead.", - DeprecationWarning, - stacklevel=2, - ) - transport = _FunctionTransport(ref_transport) - - # if a transport class is given only instantiate it if the dsn is not - # empty or None - if transport is None and options["dsn"]: - transport = transport_cls(options) - - if transport is not None and os.environ.get( - "SENTRY_PRINT_ENVELOPES", "" - ).lower() in ("1", "true", "yes"): - transport = _EnvelopePrinterTransport(transport) - - return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py deleted file mode 100644 index dff91e3719..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/types.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This module contains type definitions for the Sentry SDK's public API. -The types are re-exported from the internal module `sentry_sdk._types`. - -Disclaimer: Since types are a form of documentation, type definitions -may change in minor releases. Removing a type would be considered a -breaking change, and so we will only remove type definitions in major -releases. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Re-export types to make them available in the public API - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - EventDataCategory, - Hint, - Log, - Metric, - MonitorConfig, - SamplingContext, - ) -else: - from typing import Any - - # The lines below allow the types to be imported from outside `if TYPE_CHECKING` - # guards. The types in this module are only intended to be used for type hints. - Breadcrumb = Any - BreadcrumbHint = Any - Event = Any - EventDataCategory = Any - Hint = Any - Log = Any - MonitorConfig = Any - SamplingContext = Any - Metric = Any - - -__all__ = ( - "Breadcrumb", - "BreadcrumbHint", - "Event", - "EventDataCategory", - "Hint", - "Log", - "MonitorConfig", - "SamplingContext", - "Metric", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py deleted file mode 100644 index 0963015351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/utils.py +++ /dev/null @@ -1,2178 +0,0 @@ -import base64 -import copy -import json -import linecache -import logging -import math -import os -import random -import re -import subprocess -import sys -import threading -import time -from collections import namedtuple -from contextlib import contextmanager -from datetime import datetime, timezone -from decimal import Decimal -from functools import partial, partialmethod, wraps -from numbers import Real -from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit - -try: - # Python 3.11 - from builtins import BaseExceptionGroup -except ImportError: - # Python 3.10 and below - BaseExceptionGroup = None # type: ignore - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._compat import PY37 -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue -from sentry_sdk.consts import ( - DEFAULT_ADD_FULL_STACK, - DEFAULT_MAX_STACK_FRAMES, - EndpointType, -) - -if TYPE_CHECKING: - from types import FrameType, TracebackType - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Iterator, - List, - NoReturn, - Optional, - ParamSpec, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from gevent.hub import Hub - - from sentry_sdk._types import ( - AttributeValue, - Event, - ExcInfo, - Hint, - Log, - Metric, - SerializedAttributeValue, - SpanJSON, - ) - - P = ParamSpec("P") - R = TypeVar("R") - - -epoch = datetime(1970, 1, 1) - -# The logger is created here but initialized in the debug support module -logger = logging.getLogger("sentry_sdk.errors") - -_installed_modules = None - -BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") - -FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) -TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) - -MAX_STACK_FRAMES = 2000 -"""Maximum number of stack frames to send to Sentry. - -If we have more than this number of stack frames, we will stop processing -the stacktrace to avoid getting stuck in a long-lasting loop. This value -exceeds the default sys.getrecursionlimit() of 1000, so users will only -be affected by this limit if they have a custom recursion limit. -""" - - -def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": - """Casts an ENV variable value to boolean using the constants defined above. - In strict mode, it may return None if the value doesn't match any of the predefined values. - """ - normalized = str(value).lower() if value is not None else None - - if normalized in FALSY_ENV_VALUES: - return False - - if normalized in TRUTHY_ENV_VALUES: - return True - - return None if strict else bool(value) - - -def json_dumps(data: "Any") -> bytes: - """Serialize data into a compact JSON representation encoded as UTF-8.""" - return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") - - -def get_git_revision() -> "Optional[str]": - try: - with open(os.path.devnull, "w+") as null: - # prevent command prompt windows from popping up on windows - startupinfo = None - if sys.platform == "win32" or sys.platform == "cygwin": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - revision = ( - subprocess.Popen( - ["git", "rev-parse", "HEAD"], - startupinfo=startupinfo, - stdout=subprocess.PIPE, - stderr=null, - stdin=null, - ) - .communicate()[0] - .strip() - .decode("utf-8") - ) - except (OSError, IOError, FileNotFoundError): - return None - - return revision - - -def get_default_release() -> "Optional[str]": - """Try to guess a default release.""" - release = os.environ.get("SENTRY_RELEASE") - if release: - return release - - release = get_git_revision() - if release: - return release - - for var in ( - "HEROKU_BUILD_COMMIT", - "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility - "SOURCE_VERSION", - "CODEBUILD_RESOLVED_SOURCE_VERSION", - "CIRCLE_SHA1", - "GAE_DEPLOYMENT_ID", - "K_REVISION", - ): - release = os.environ.get(var) - if release: - return release - return None - - -def get_sdk_name(installed_integrations: "List[str]") -> str: - """Return the SDK name including the name of the used web framework.""" - - # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier - # here because if django is not installed the integration is not accessible. - framework_integrations = [ - "django", - "flask", - "fastapi", - "bottle", - "falcon", - "quart", - "sanic", - "starlette", - "litestar", - "starlite", - "chalice", - "serverless", - "pyramid", - "tornado", - "aiohttp", - "aws_lambda", - "gcp", - "beam", - "asgi", - "wsgi", - ] - - for integration in framework_integrations: - if integration in installed_integrations: - return "sentry.python.{}".format(integration) - - return "sentry.python" - - -class CaptureInternalException: - __slots__ = () - - def __enter__(self) -> "ContextManager[Any]": - return self - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> bool: - if ty is not None and value is not None: - capture_internal_exception((ty, value, tb)) - - return True - - -_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() - - -def capture_internal_exceptions() -> "ContextManager[Any]": - return _CAPTURE_INTERNAL_EXCEPTION - - -def capture_internal_exception(exc_info: "ExcInfo") -> None: - """ - Capture an exception that is likely caused by a bug in the SDK - itself. - - These exceptions do not end up in Sentry and are just logged instead. - """ - if sentry_sdk.get_client().is_active(): - logger.error("Internal error in sentry_sdk", exc_info=exc_info) - - -def to_timestamp(value: "datetime") -> float: - return (value - epoch).total_seconds() - - -def format_timestamp(value: "datetime") -> str: - """Formats a timestamp in RFC 3339 format. - - Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. - """ - utctime = value.astimezone(timezone.utc) - - # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for - # several years now), and isoformat is slightly different. - return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - -ISO_TZ_SEPARATORS = frozenset(("+", "-")) - - -def datetime_from_isoformat(value: str) -> "datetime": - try: - result = datetime.fromisoformat(value) - except (AttributeError, ValueError): - # py 3.6 - timestamp_format = ( - "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" - ) - if value.endswith("Z"): - value = value[:-1] + "+0000" - - if value[-6] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - value = value[:-3] + value[-2:] - elif value[-5] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - - result = datetime.strptime(value, timestamp_format) - return result.astimezone(timezone.utc) - - -def event_hint_with_exc_info( - exc_info: "Optional[ExcInfo]" = None, -) -> "Dict[str, Optional[ExcInfo]]": - """Creates a hint with the exc info filled in.""" - if exc_info is None: - exc_info = sys.exc_info() - else: - exc_info = exc_info_from_error(exc_info) - if exc_info[0] is None: - exc_info = None - return {"exc_info": exc_info} - - -class BadDsn(ValueError): - """Raised on invalid DSNs.""" - - -class Dsn: - """Represents a DSN.""" - - ORG_ID_REGEX = re.compile(r"^o(\d+)\.") - - def __init__( - self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None - ) -> None: - if isinstance(value, Dsn): - self.__dict__ = dict(value.__dict__) - return - parts = urlsplit(str(value)) - - if parts.scheme not in ("http", "https"): - raise BadDsn("Unsupported scheme %r" % parts.scheme) - self.scheme = parts.scheme - - if parts.hostname is None: - raise BadDsn("Missing hostname") - - self.host = parts.hostname - - if org_id is not None: - self.org_id: "Optional[str]" = org_id - else: - org_id_match = Dsn.ORG_ID_REGEX.match(self.host) - self.org_id = org_id_match.group(1) if org_id_match else None - - if parts.port is None: - self.port: int = self.scheme == "https" and 443 or 80 - else: - self.port = parts.port - - if not parts.username: - raise BadDsn("Missing public key") - - self.public_key = parts.username - self.secret_key = parts.password - - path = parts.path.rsplit("/", 1) - - try: - self.project_id = str(int(path.pop())) - except (ValueError, TypeError): - raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) - - self.path = "/".join(path) + "/" - - @property - def netloc(self) -> str: - """The netloc part of a DSN.""" - rv = self.host - if (self.scheme, self.port) not in (("http", 80), ("https", 443)): - rv = "%s:%s" % (rv, self.port) - return rv - - def to_auth(self, client: "Optional[Any]" = None) -> "Auth": - """Returns the auth info object for this dsn.""" - return Auth( - scheme=self.scheme, - host=self.netloc, - path=self.path, - project_id=self.project_id, - public_key=self.public_key, - secret_key=self.secret_key, - client=client, - ) - - def __str__(self) -> str: - return "%s://%s%s@%s%s%s" % ( - self.scheme, - self.public_key, - self.secret_key and "@" + self.secret_key or "", - self.netloc, - self.path, - self.project_id, - ) - - -class Auth: - """Helper object that represents the auth info.""" - - def __init__( - self, - scheme: str, - host: str, - project_id: str, - public_key: str, - secret_key: "Optional[str]" = None, - version: int = 7, - client: "Optional[Any]" = None, - path: str = "/", - ) -> None: - self.scheme = scheme - self.host = host - self.path = path - self.project_id = project_id - self.public_key = public_key - self.secret_key = secret_key - self.version = version - self.client = client - - def get_api_url( - self, - type: "EndpointType" = EndpointType.ENVELOPE, - ) -> str: - """Returns the API url for storing events.""" - return "%s://%s%sapi/%s/%s/" % ( - self.scheme, - self.host, - self.path, - self.project_id, - type.value, - ) - - def to_header(self) -> str: - """Returns the auth header a string.""" - rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] - if self.client is not None: - rv.append(("sentry_client", self.client)) - if self.secret_key is not None: - rv.append(("sentry_secret", self.secret_key)) - return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) - - -def get_type_name(cls: "Optional[type]") -> "Optional[str]": - return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) - - -def get_type_module(cls: "Optional[type]") -> "Optional[str]": - mod = getattr(cls, "__module__", None) - if mod not in (None, "builtins", "__builtins__"): - return mod - return None - - -def should_hide_frame(frame: "FrameType") -> bool: - try: - mod = frame.f_globals["__name__"] - if mod.startswith("sentry_sdk."): - return True - except (AttributeError, KeyError): - pass - - for flag_name in "__traceback_hide__", "__tracebackhide__": - try: - if frame.f_locals[flag_name]: - return True - except Exception: - pass - - return False - - -def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": - tb_: "Optional[TracebackType]" = tb - while tb_ is not None: - if not should_hide_frame(tb_.tb_frame): - yield tb_ - tb_ = tb_.tb_next - - -def get_lines_from_file( - filename: str, - lineno: int, - max_length: "Optional[int]" = None, - loader: "Optional[Any]" = None, - module: "Optional[str]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - context_lines = 5 - source = None - if loader is not None and hasattr(loader, "get_source"): - try: - source_str: "Optional[str]" = loader.get_source(module) - except (ImportError, IOError): - source_str = None - if source_str is not None: - source = source_str.splitlines() - - if source is None: - try: - source = linecache.getlines(filename) - except (OSError, IOError): - return [], None, [] - - if not source: - return [], None, [] - - lower_bound = max(0, lineno - context_lines) - upper_bound = min(lineno + 1 + context_lines, len(source)) - - try: - pre_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[lower_bound:lineno] - ] - context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) - post_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[(lineno + 1) : upper_bound] - ] - return pre_context, context_line, post_context - except IndexError: - # the file may have changed since it was loaded into memory - return [], None, [] - - -def get_source_context( - frame: "FrameType", - tb_lineno: "Optional[int]", - max_value_length: "Optional[int]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - try: - abs_path: "Optional[str]" = frame.f_code.co_filename - except Exception: - abs_path = None - try: - module = frame.f_globals["__name__"] - except Exception: - return [], None, [] - try: - loader = frame.f_globals["__loader__"] - except Exception: - loader = None - - if tb_lineno is not None and abs_path: - lineno = tb_lineno - 1 - return get_lines_from_file( - abs_path, lineno, max_value_length, loader=loader, module=module - ) - - return [], None, [] - - -def safe_str(value: "Any") -> str: - try: - return str(value) - except Exception: - return safe_repr(value) - - -def safe_repr(value: "Any") -> str: - try: - return repr(value) - except Exception: - return "" - - -def filename_for_module( - module: "Optional[str]", abs_path: "Optional[str]" -) -> "Optional[str]": - if not abs_path or not module: - return abs_path - - try: - if abs_path.endswith(".pyc"): - abs_path = abs_path[:-1] - - base_module = module.split(".", 1)[0] - if base_module == module: - return os.path.basename(abs_path) - - base_module_path = sys.modules[base_module].__file__ - if not base_module_path: - return abs_path - - return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( - os.sep - ) - except Exception: - return abs_path - - -def serialize_frame( - frame: "FrameType", - tb_lineno: "Optional[int]" = None, - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, -) -> "Dict[str, Any]": - f_code = getattr(frame, "f_code", None) - if not f_code: - abs_path = None - function = None - else: - abs_path = frame.f_code.co_filename - function = frame.f_code.co_name - try: - module = frame.f_globals["__name__"] - except Exception: - module = None - - if tb_lineno is None: - tb_lineno = frame.f_lineno - - try: - os_abs_path = os.path.abspath(abs_path) if abs_path else None - except Exception: - os_abs_path = None - - rv: "Dict[str, Any]" = { - "filename": filename_for_module(module, abs_path) or None, - "abs_path": os_abs_path, - "function": function or "", - "module": module, - "lineno": tb_lineno, - } - - if include_source_context: - rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( - frame, tb_lineno, max_value_length - ) - - if include_local_variables: - from sentry_sdk.serializer import serialize - - rv["vars"] = serialize( - dict(frame.f_locals), is_vars=True, custom_repr=custom_repr - ) - - return rv - - -def current_stacktrace( - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, -) -> "Dict[str, Any]": - __tracebackhide__ = True - frames = [] - - f: "Optional[FrameType]" = sys._getframe() - while f is not None: - if not should_hide_frame(f): - frames.append( - serialize_frame( - f, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - ) - ) - f = f.f_back - - frames.reverse() - - return {"frames": frames} - - -def get_errno(exc_value: BaseException) -> "Optional[Any]": - return getattr(exc_value, "errno", None) - - -def get_error_message(exc_value: "Optional[BaseException]") -> str: - message: str = safe_str( - getattr(exc_value, "message", "") - or getattr(exc_value, "detail", "") - or safe_str(exc_value) - ) - - # __notes__ should be a list of strings when notes are added - # via add_note, but can be anything else if __notes__ is set - # directly. We only support strings in __notes__, since that - # is the correct use. - notes: object = getattr(exc_value, "__notes__", None) - if isinstance(notes, list) and len(notes) > 0: - message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) - - return message - - -def single_exception_from_error_tuple( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: "Optional[int]" = None, - parent_id: "Optional[int]" = None, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "Dict[str, Any]": - """ - Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - """ - exception_value: "Dict[str, Any]" = {} - exception_value["mechanism"] = ( - mechanism.copy() if mechanism else {"type": "generic", "handled": True} - ) - if exception_id is not None: - exception_value["mechanism"]["exception_id"] = exception_id - - if exc_value is not None: - errno = get_errno(exc_value) - else: - errno = None - - if errno is not None: - exception_value["mechanism"].setdefault("meta", {}).setdefault( - "errno", {} - ).setdefault("number", errno) - - if source is not None: - exception_value["mechanism"]["source"] = source - - is_root_exception = exception_id == 0 - if not is_root_exception and parent_id is not None: - exception_value["mechanism"]["parent_id"] = parent_id - exception_value["mechanism"]["type"] = "chained" - - if is_root_exception and "type" not in exception_value["mechanism"]: - exception_value["mechanism"]["type"] = "generic" - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - if is_exception_group: - exception_value["mechanism"]["is_exception_group"] = True - - exception_value["module"] = get_type_module(exc_type) - exception_value["type"] = get_type_name(exc_type) - exception_value["value"] = get_error_message(exc_value) - - if client_options is None: - include_local_variables = True - include_source_context = True - max_value_length = None # fallback - custom_repr = None - else: - include_local_variables = client_options["include_local_variables"] - include_source_context = client_options["include_source_context"] - max_value_length = client_options["max_value_length"] - custom_repr = client_options.get("custom_repr") - - frames: "List[Dict[str, Any]]" = [ - serialize_frame( - tb.tb_frame, - tb_lineno=tb.tb_lineno, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - custom_repr=custom_repr, - ) - # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on - # processing a super-long stacktrace. - for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) - ] - - if len(frames) > MAX_STACK_FRAMES: - # If we have more frames than the limit, we remove the stacktrace completely. - # We don't trim the stacktrace here because we have not processed the whole - # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would - # intelligently trim by removing frames in the middle of the stacktrace, but - # since we don't have the whole stacktrace, we can't do that. Instead, we - # drop the entire stacktrace. - exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( - value=None - ) - - elif frames: - if not full_stack: - new_frames = frames - else: - new_frames = merge_stack_frames(frames, full_stack, client_options) - - exception_value["stacktrace"] = {"frames": new_frames} - - return exception_value - - -HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") - -if HAS_CHAINED_EXCEPTIONS: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - exc_type, exc_value, tb = exc_info - - seen_exceptions = [] - seen_exception_ids: "Set[int]" = set() - - while ( - exc_type is not None - and exc_value is not None - and id(exc_value) not in seen_exception_ids - ): - yield exc_type, exc_value, tb - - # Avoid hashing random types we don't know anything - # about. Use the list to keep a ref so that the `id` is - # not used for another object. - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - if exc_value.__suppress_context__: - cause = exc_value.__cause__ - else: - cause = exc_value.__context__ - if cause is None: - break - exc_type = type(cause) - exc_value = cause - tb = getattr(cause, "__traceback__", None) - -else: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - yield exc_info - - -def exceptions_from_error( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: int = 0, - parent_id: int = 0, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, - seen_exceptions: "Optional[list[BaseException]]" = None, - seen_exception_ids: "Optional[Set[int]]" = None, -) -> "Tuple[int, List[Dict[str, Any]]]": - """ - Creates the list of exceptions. - This can include chained exceptions and exceptions from an ExceptionGroup. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - - Args: - exception_id (int): - - Sequential counter for assigning ``mechanism.exception_id`` - to each processed exception. Is NOT the result of calling `id()` on the exception itself. - - parent_id (int): - - The ``mechanism.exception_id`` of the parent exception. - - Written into ``mechanism.parent_id`` in the event payload so Sentry can - reconstruct the exception tree. - - Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` - values for cycle detection. - """ - - if seen_exception_ids is None: - seen_exception_ids = set() - - if seen_exceptions is None: - seen_exceptions = [] - - if exc_value is not None and id(exc_value) in seen_exception_ids: - return (exception_id, []) - - if exc_value is not None: - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - parent = single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source=source, - full_stack=full_stack, - ) - exceptions = [parent] - - parent_id = exception_id - exception_id += 1 - - should_supress_context = ( - hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore - ) - if should_supress_context: - # Add direct cause. - # The field `__cause__` is set when raised with the exception (using the `from` keyword). - exception_has_cause = ( - exc_value - and hasattr(exc_value, "__cause__") - and exc_value.__cause__ is not None - ) - if exception_has_cause: - cause = exc_value.__cause__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(cause), - exc_value=cause, - tb=getattr(cause, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__cause__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - else: - # Add indirect cause. - # The field `__context__` is assigned if another exception occurs while handling the exception. - exception_has_content = ( - exc_value - and hasattr(exc_value, "__context__") - and exc_value.__context__ is not None - ) - if exception_has_content: - context = exc_value.__context__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(context), - exc_value=context, - tb=getattr(context, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__context__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - # Add exceptions from an ExceptionGroup. - is_exception_group = exc_value and hasattr(exc_value, "exceptions") - if is_exception_group: - for idx, e in enumerate(exc_value.exceptions): # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(e), - exc_value=e, - tb=getattr(e, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source="exceptions[%s]" % idx, - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - return (exception_id, exceptions) - - -def exceptions_from_error_tuple( - exc_info: "ExcInfo", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "List[Dict[str, Any]]": - exc_type, exc_value, tb = exc_info - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - - if is_exception_group: - (_, exceptions) = exceptions_from_error( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=0, - parent_id=0, - full_stack=full_stack, - ) - - else: - exceptions = [] - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - exceptions.append( - single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - full_stack=full_stack, - ) - ) - - exceptions.reverse() - - return exceptions - - -def to_string(value: str) -> str: - try: - return str(value) - except UnicodeDecodeError: - return repr(value)[1:-1] - - -def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": - if "stacktrace" in event: - yield event["stacktrace"] - if "threads" in event: - for thread in event["threads"].get("values") or (): - if "stacktrace" in thread: - yield thread["stacktrace"] - if "exception" in event: - for exception in event["exception"].get("values") or (): - if isinstance(exception, dict) and "stacktrace" in exception: - yield exception["stacktrace"] - - -def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - for frame in stacktrace.get("frames") or (): - yield frame - - -def handle_in_app( - event: "Event", - in_app_exclude: "Optional[List[str]]" = None, - in_app_include: "Optional[List[str]]" = None, - project_root: "Optional[str]" = None, -) -> "Event": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - set_in_app_in_frames( - stacktrace.get("frames"), - in_app_exclude=in_app_exclude, - in_app_include=in_app_include, - project_root=project_root, - ) - - return event - - -def set_in_app_in_frames( - frames: "Any", - in_app_exclude: "Optional[List[str]]", - in_app_include: "Optional[List[str]]", - project_root: "Optional[str]" = None, -) -> "Optional[Any]": - if not frames: - return None - - for frame in frames: - # if frame has already been marked as in_app, skip it - current_in_app = frame.get("in_app") - if current_in_app is not None: - continue - - module = frame.get("module") - - # check if module in frame is in the list of modules to include - if _module_in_list(module, in_app_include): - frame["in_app"] = True - continue - - # check if module in frame is in the list of modules to exclude - if _module_in_list(module, in_app_exclude): - frame["in_app"] = False - continue - - # if frame has no abs_path, skip further checks - abs_path = frame.get("abs_path") - if abs_path is None: - continue - - if _is_external_source(abs_path): - frame["in_app"] = False - continue - - if _is_in_project_root(abs_path, project_root): - frame["in_app"] = True - continue - - return frames - - -def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": - if isinstance(error, tuple) and len(error) == 3: - exc_type, exc_value, tb = error - elif isinstance(error, BaseException): - tb = getattr(error, "__traceback__", None) - if tb is not None: - exc_type = type(error) - exc_value = error - else: - exc_type, exc_value, tb = sys.exc_info() - if exc_value is not error: - tb = None - exc_value = error - exc_type = type(error) - - else: - raise ValueError("Expected Exception object to report, got %s!" % type(error)) - - exc_info = (exc_type, exc_value, tb) - - if TYPE_CHECKING: - # This cast is safe because exc_type and exc_value are either both - # None or both not None. - exc_info = cast(ExcInfo, exc_info) - - return exc_info - - -def merge_stack_frames( - frames: "List[Dict[str, Any]]", - full_stack: "List[Dict[str, Any]]", - client_options: "Optional[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Add the missing frames from full_stack to frames and return the merged list. - """ - frame_ids = { - ( - frame["abs_path"], - frame["context_line"], - frame["lineno"], - frame["function"], - ) - for frame in frames - } - - new_frames = [ - stackframe - for stackframe in full_stack - if ( - stackframe["abs_path"], - stackframe["context_line"], - stackframe["lineno"], - stackframe["function"], - ) - not in frame_ids - ] - new_frames.extend(frames) - - # Limit the number of frames - max_stack_frames = ( - client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) - if client_options - else None - ) - if max_stack_frames is not None: - new_frames = new_frames[len(new_frames) - max_stack_frames :] - - return new_frames - - -def event_from_exception( - exc_info: "Union[BaseException, ExcInfo]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, -) -> "Tuple[Event, Dict[str, Any]]": - exc_info = exc_info_from_error(exc_info) - hint = event_hint_with_exc_info(exc_info) - - if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): - full_stack = current_stacktrace( - include_local_variables=client_options["include_local_variables"], - max_value_length=client_options["max_value_length"], - )["frames"] - else: - full_stack = None - - return ( - { - "level": "error", - "exception": { - "values": exceptions_from_error_tuple( - exc_info, client_options, mechanism, full_stack - ) - }, - }, - hint, - ) - - -def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: - if name is None: - return False - - if not items: - return False - - for item in items: - if item == name or name.startswith(item + "."): - return True - - return False - - -def _is_external_source(abs_path: "Optional[str]") -> bool: - # check if frame is in 'site-packages' or 'dist-packages' - if abs_path is None: - return False - - external_source = ( - re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None - ) - return external_source - - -def _is_in_project_root( - abs_path: "Optional[str]", project_root: "Optional[str]" -) -> bool: - if abs_path is None or project_root is None: - return False - - # check if path is in the project root - if abs_path.startswith(project_root): - return True - - return False - - -def _truncate_by_bytes(string: str, max_bytes: int) -> str: - """ - Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. - """ - truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") - - return truncated + "..." - - -def _get_size_in_bytes(value: str) -> "Optional[int]": - try: - return len(value.encode("utf-8")) - except (UnicodeEncodeError, UnicodeDecodeError): - return None - - -def strip_string( - value: str, max_length: "Optional[int]" = None -) -> "Union[AnnotatedValue, str]": - if not value or max_length is None: - return value - - byte_size = _get_size_in_bytes(value) - text_size = len(value) - - if byte_size is not None and byte_size > max_length: - # truncate to max_length bytes, preserving code points - truncated_value = _truncate_by_bytes(value, max_length) - elif text_size is not None and text_size > max_length: - # fallback to truncating by string length - truncated_value = value[: max_length - 3] + "..." - else: - return value - - return AnnotatedValue( - value=truncated_value, - metadata={ - "len": byte_size or text_size, - "rem": [["!limit", "x", max_length - 3, max_length]], - }, - ) - - -def parse_version(version: str) -> "Optional[Tuple[int, ...]]": - """ - Parses a version string into a tuple of integers. - This uses the parsing loging from PEP 440: - https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions - """ - VERSION_PATTERN = r""" # noqa: N806 - v? - (?: - (?:(?P[0-9]+)!)? # epoch - (?P[0-9]+(?:\.[0-9]+)*) # release segment - (?P
                                          # pre-release
-                [-_\.]?
-                (?P(a|b|c|rc|alpha|beta|pre|preview))
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-            (?P                                         # post release
-                (?:-(?P[0-9]+))
-                |
-                (?:
-                    [-_\.]?
-                    (?Ppost|rev|r)
-                    [-_\.]?
-                    (?P[0-9]+)?
-                )
-            )?
-            (?P                                          # dev release
-                [-_\.]?
-                (?Pdev)
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-        )
-        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
-    """
-
-    pattern = re.compile(
-        r"^\s*" + VERSION_PATTERN + r"\s*$",
-        re.VERBOSE | re.IGNORECASE,
-    )
-
-    try:
-        release = pattern.match(version).groupdict()["release"]  # type: ignore
-        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
-    except (TypeError, ValueError, AttributeError):
-        return None
-
-    return release_tuple
-
-
-def _is_contextvars_broken() -> bool:
-    """
-    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
-    """
-    try:
-        import gevent
-        from gevent.monkey import is_object_patched
-
-        # Get the MAJOR and MINOR version numbers of Gevent
-        version_tuple = tuple(
-            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
-        )
-        if is_object_patched("threading", "local"):
-            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
-            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
-            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
-            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
-            # for contextvars, is able to patch both thread locals and contextvars, in
-            # that case, check if contextvars are effectively patched.
-            if (
-                # Gevent 20.9.0+
-                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
-                # Gevent 20.5.0+ or Python < 3.7
-                or (is_object_patched("contextvars", "ContextVar"))
-            ):
-                return False
-
-            return True
-    except ImportError:
-        pass
-
-    try:
-        import greenlet
-        from eventlet.patcher import is_monkey_patched  # type: ignore
-
-        greenlet_version = parse_version(greenlet.__version__)
-
-        if greenlet_version is None:
-            logger.error(
-                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
-            )
-            return False
-
-        if is_monkey_patched("thread") and greenlet_version < (0, 5):
-            return True
-    except ImportError:
-        pass
-
-    return False
-
-
-def _make_threadlocal_contextvars(local: type) -> type:
-    class ContextVar:
-        # Super-limited impl of ContextVar
-
-        def __init__(self, name: str, default: "Any" = None) -> None:
-            self._name = name
-            self._default = default
-            self._local = local()
-            self._original_local = local()
-
-        def get(self, default: "Any" = None) -> "Any":
-            return getattr(self._local, "value", default or self._default)
-
-        def set(self, value: "Any") -> "Any":
-            token = str(random.getrandbits(64))
-            original_value = self.get()
-            setattr(self._original_local, token, original_value)
-            self._local.value = value
-            return token
-
-        def reset(self, token: "Any") -> None:
-            self._local.value = getattr(self._original_local, token)
-            # delete the original value (this way it works in Python 3.6+)
-            del self._original_local.__dict__[token]
-
-    return ContextVar
-
-
-def _get_contextvars() -> "Tuple[bool, type]":
-    """
-    Figure out the "right" contextvars installation to use. Returns a
-    `contextvars.ContextVar`-like class with a limited API.
-
-    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
-    """
-    if not _is_contextvars_broken():
-        # aiocontextvars is a PyPI package that ensures that the contextvars
-        # backport (also a PyPI package) works with asyncio under Python 3.6
-        #
-        # Import it if available.
-        if sys.version_info < (3, 7):
-            # `aiocontextvars` is absolutely required for functional
-            # contextvars on Python 3.6.
-            try:
-                from aiocontextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-        else:
-            # On Python 3.7 contextvars are functional.
-            try:
-                from contextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-
-    # Fall back to basic thread-local usage.
-
-    from threading import local
-
-    return False, _make_threadlocal_contextvars(local)
-
-
-HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
-
-CONTEXTVARS_ERROR_MESSAGE = """
-
-With asyncio/ASGI applications, the Sentry SDK requires a functional
-installation of `contextvars` to avoid leaking scope/context data across
-requests.
-
-Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
-"""
-
-_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
-
-# These exceptions won't set the span status to error if they occur. Use
-# register_control_flow_exception to add to this list
-_control_flow_exception_classes: "set[type]" = set()
-
-
-def is_internal_task() -> bool:
-    return _is_sentry_internal_task.get()
-
-
-@contextmanager
-def mark_sentry_task_internal() -> "Generator[None, None, None]":
-    """Context manager to mark a task as Sentry internal."""
-    token = _is_sentry_internal_task.set(True)
-    try:
-        yield
-    finally:
-        _is_sentry_internal_task.reset(token)
-
-
-def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
-    func_qualname: "Optional[str]" = None
-
-    prefix, suffix = "", ""
-
-    if isinstance(func, partial) and hasattr(func.func, "__name__"):
-        prefix, suffix = "partial()"
-        func = func.func
-    else:
-        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
-        # https://github.com/python/cpython/pull/16600
-        partial_method = getattr(func, "_partialmethod", None) or getattr(
-            func, "__partialmethod__", None
-        )
-        if isinstance(partial_method, partialmethod):
-            prefix, suffix = "partialmethod()"
-            func = partial_method.func
-
-    if hasattr(func, "__qualname__"):
-        func_qualname = func.__qualname__
-    elif hasattr(func, "__name__"):
-        func_qualname = func.__name__
-
-    if func_qualname is not None:
-        if hasattr(func, "__module__") and isinstance(func.__module__, str):
-            func_qualname = func.__module__ + "." + func_qualname
-        func_qualname = prefix + func_qualname + suffix
-
-    return func_qualname
-
-
-def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    return qualname_from_function(func)
-
-
-disable_capture_event = ContextVar("disable_capture_event")
-
-
-class ServerlessTimeoutWarning(Exception):  # noqa: N818
-    """Raised when a serverless method is about to reach its timeout."""
-
-    pass
-
-
-class TimeoutThread(threading.Thread):
-    """Creates a Thread which runs (sleeps) for a time duration equal to
-    waiting_time and raises a custom ServerlessTimeout exception.
-    """
-
-    def __init__(
-        self,
-        waiting_time: float,
-        configured_timeout: int,
-        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
-        current_scope: "Optional[sentry_sdk.Scope]" = None,
-    ) -> None:
-        threading.Thread.__init__(self)
-        self.waiting_time = waiting_time
-        self.configured_timeout = configured_timeout
-
-        self.isolation_scope = isolation_scope
-        self.current_scope = current_scope
-
-        self._stop_event = threading.Event()
-
-    def stop(self) -> None:
-        self._stop_event.set()
-
-    def _capture_exception(self) -> "ExcInfo":
-        exc_info = sys.exc_info()
-
-        client = sentry_sdk.get_client()
-        event, hint = event_from_exception(
-            exc_info,
-            client_options=client.options,
-            mechanism={"type": "threading", "handled": False},
-        )
-        sentry_sdk.capture_event(event, hint=hint)
-
-        return exc_info
-
-    def run(self) -> None:
-        self._stop_event.wait(self.waiting_time)
-
-        if self._stop_event.is_set():
-            return
-
-        integer_configured_timeout = int(self.configured_timeout)
-
-        # Setting up the exact integer value of configured time(in seconds)
-        if integer_configured_timeout < self.configured_timeout:
-            integer_configured_timeout = integer_configured_timeout + 1
-
-        # Raising Exception after timeout duration is reached
-        if self.isolation_scope is not None and self.current_scope is not None:
-            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
-                with sentry_sdk.scope.use_scope(self.current_scope):
-                    try:
-                        raise ServerlessTimeoutWarning(
-                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                                integer_configured_timeout
-                            )
-                        )
-                    except Exception:
-                        reraise(*self._capture_exception())
-
-        raise ServerlessTimeoutWarning(
-            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                integer_configured_timeout
-            )
-        )
-
-
-def to_base64(original: str) -> "Optional[str]":
-    """
-    Convert a string to base64, via UTF-8. Returns None on invalid input.
-    """
-    base64_string = None
-
-    try:
-        utf8_bytes = original.encode("UTF-8")
-        base64_bytes = base64.b64encode(utf8_bytes)
-        base64_string = base64_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
-
-    return base64_string
-
-
-def from_base64(base64_string: str) -> "Optional[str]":
-    """
-    Convert a string from base64, via UTF-8. Returns None on invalid input.
-    """
-    utf8_string = None
-
-    try:
-        only_valid_chars = BASE64_ALPHABET.match(base64_string)
-        assert only_valid_chars
-
-        base64_bytes = base64_string.encode("UTF-8")
-        utf8_bytes = base64.b64decode(base64_bytes)
-        utf8_string = utf8_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning(
-            "Unable to decode {b64} from base64:".format(b64=base64_string), err
-        )
-
-    return utf8_string
-
-
-Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
-
-
-def sanitize_url(
-    url: str,
-    remove_authority: bool = True,
-    remove_query_values: bool = True,
-    split: bool = False,
-) -> "Union[str, Components]":
-    """
-    Removes the authority and query parameter values from a given URL.
-    """
-    parsed_url = urlsplit(url)
-    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
-
-    # strip username:password (netloc can be usr:pwd@example.com)
-    if remove_authority:
-        netloc_parts = parsed_url.netloc.split("@")
-        if len(netloc_parts) > 1:
-            netloc = "%s:%s@%s" % (
-                SENSITIVE_DATA_SUBSTITUTE,
-                SENSITIVE_DATA_SUBSTITUTE,
-                netloc_parts[-1],
-            )
-        else:
-            netloc = parsed_url.netloc
-    else:
-        netloc = parsed_url.netloc
-
-    # strip values from query string
-    if remove_query_values:
-        query_string = unquote(
-            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
-        )
-    else:
-        query_string = parsed_url.query
-
-    components = Components(
-        scheme=parsed_url.scheme,
-        netloc=netloc,
-        query=query_string,
-        path=parsed_url.path,
-        fragment=parsed_url.fragment,
-    )
-
-    if split:
-        return components
-    else:
-        return urlunsplit(components)
-
-
-ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
-
-
-def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
-    """
-    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
-    parameters will be sanitized to remove sensitive data. The autority (username and password)
-    in the URL will always be removed.
-    """
-    parsed_url = sanitize_url(
-        url, remove_authority=True, remove_query_values=sanitize, split=True
-    )
-
-    base_url = urlunsplit(
-        Components(
-            scheme=parsed_url.scheme,  # type: ignore
-            netloc=parsed_url.netloc,  # type: ignore
-            query="",
-            path=parsed_url.path,  # type: ignore
-            fragment="",
-        )
-    )
-
-    return ParsedUrl(
-        url=base_url,
-        query=parsed_url.query,  # type: ignore
-        fragment=parsed_url.fragment,  # type: ignore
-    )
-
-
-def is_valid_sample_rate(rate: "Any", source: str) -> bool:
-    """
-    Checks the given sample rate to make sure it is valid type and value (a
-    boolean or a number between 0 and 1, inclusive).
-    """
-
-    # both booleans and NaN are instances of Real, so a) checking for Real
-    # checks for the possibility of a boolean also, and b) we have to check
-    # separately for NaN and Decimal does not derive from Real so need to check that too
-    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
-                source=source, rate=rate, type=type(rate)
-            )
-        )
-        return False
-
-    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
-    rate = float(rate)
-    if rate < 0 or rate > 1:
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
-                source=source, rate=rate
-            )
-        )
-        return False
-
-    return True
-
-
-def match_regex_list(
-    item: str,
-    regex_list: "Optional[List[str]]" = None,
-    substring_matching: bool = False,
-) -> bool:
-    if regex_list is None:
-        return False
-
-    for item_matcher in regex_list:
-        if not substring_matching and item_matcher[-1] != "$":
-            item_matcher += "$"
-
-        matched = re.search(item_matcher, item)
-        if matched:
-            return True
-
-    return False
-
-
-def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
-    """
-    Determines whether the given URL matches the Sentry DSN.
-    """
-    return (
-        client is not None
-        and client.transport is not None
-        and client.transport.parsed_dsn is not None
-        and client.transport.parsed_dsn.netloc in url
-    )
-
-
-def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
-    try:
-        from importlib import metadata
-
-        yielded = set()
-        for dist in metadata.distributions():
-            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
-            # `metadata` values may be `None`, see:
-            # https://github.com/python/cpython/issues/91216
-            # and
-            # https://github.com/python/importlib_metadata/issues/371
-            if name is not None:
-                normalized_name = _normalize_module_name(name)
-                if dist.version is not None and normalized_name not in yielded:
-                    yield normalized_name, dist.version
-                    yielded.add(normalized_name)
-
-    except ImportError:
-        # < py3.8
-        try:
-            import pkg_resources
-        except ImportError:
-            return
-
-        for info in pkg_resources.working_set:
-            yield _normalize_module_name(info.key), info.version
-
-
-def _normalize_module_name(name: str) -> str:
-    return name.lower()
-
-
-def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
-    # https://peps.python.org/pep-0503/#normalized-names
-    return re.sub(r"[-_.]+", "-", name)
-
-
-def _get_installed_modules() -> "Dict[str, str]":
-    global _installed_modules
-    if _installed_modules is None:
-        _installed_modules = dict(_generate_installed_modules())
-    return _installed_modules
-
-
-def package_version(package: str) -> "Optional[Tuple[int, ...]]":
-    normalized_package = _normalize_module_name(
-        _replace_hyphens_dots_and_underscores_with_dashes(package)
-    )
-
-    installed_packages = {
-        _replace_hyphens_dots_and_underscores_with_dashes(module): v
-        for module, v in _get_installed_modules().items()
-    }
-    version = installed_packages.get(normalized_package)
-    if version is None:
-        return None
-
-    return parse_version(version)
-
-
-def reraise(
-    tp: "Optional[Type[BaseException]]",
-    value: "Optional[BaseException]",
-    tb: "Optional[Any]" = None,
-) -> "NoReturn":
-    assert value is not None
-    if value.__traceback__ is not tb:
-        raise value.with_traceback(tb)
-    raise value
-
-
-def _no_op(*_a: "Any", **_k: "Any") -> None:
-    """No-op function for ensure_integration_enabled."""
-    pass
-
-
-if TYPE_CHECKING:
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-        original_function: "Callable[P, R]",
-    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
-
-
-def ensure_integration_enabled(
-    integration: "type[sentry_sdk.integrations.Integration]",
-    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
-) -> "Callable[[Callable[P, R]], Callable[P, R]]":
-    """
-    Ensures a given integration is enabled prior to calling a Sentry-patched function.
-
-    The function takes as its parameters the integration that must be enabled and the original
-    function that the SDK is patching. The function returns a function that takes the
-    decorated (Sentry-patched) function as its parameter, and returns a function that, when
-    called, checks whether the given integration is enabled. If the integration is enabled, the
-    function calls the decorated, Sentry-patched function. If the integration is not enabled,
-    the original function is called.
-
-    The function also takes care of preserving the original function's signature and docstring.
-
-    Example usage:
-
-    ```python
-    @ensure_integration_enabled(MyIntegration, my_function)
-    def patch_my_function():
-        with sentry_sdk.start_transaction(...):
-            return my_function()
-    ```
-    """
-    if TYPE_CHECKING:
-        # Type hint to ensure the default function has the right typing. The overloads
-        # ensure the default _no_op function is only used when R is None.
-        original_function = cast(Callable[P, R], original_function)
-
-    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
-        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
-            if sentry_sdk.get_client().get_integration(integration) is None:
-                return original_function(*args, **kwargs)
-
-            return sentry_patched_function(*args, **kwargs)
-
-        if original_function is _no_op:
-            return wraps(sentry_patched_function)(runner)
-
-        return wraps(original_function)(runner)
-
-    return patcher
-
-
-if PY37:
-
-    def nanosecond_time() -> int:
-        return time.perf_counter_ns()
-
-else:
-
-    def nanosecond_time() -> int:
-        return int(time.perf_counter() * 1e9)
-
-
-def now() -> float:
-    return time.perf_counter()
-
-
-try:
-    from gevent import get_hub as get_gevent_hub
-    from gevent.monkey import is_module_patched
-except ImportError:
-    # it's not great that the signatures are different, get_hub can't return None
-    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
-    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
-        return None
-
-    def is_module_patched(mod_name: str) -> bool:
-        # unable to import from gevent means no modules have been patched
-        return False
-
-
-def is_gevent() -> bool:
-    return is_module_patched("threading") or is_module_patched("_thread")
-
-
-def get_current_thread_meta(
-    thread: "Optional[threading.Thread]" = None,
-) -> "Tuple[Optional[int], Optional[str]]":
-    """
-    Try to get the id of the current thread, with various fall backs.
-    """
-
-    # if a thread is specified, that takes priority
-    if thread is not None:
-        try:
-            thread_id = thread.ident
-            thread_name = thread.name
-            if thread_id is not None:
-                return thread_id, thread_name
-        except AttributeError:
-            pass
-
-    # if the app is using gevent, we should look at the gevent hub first
-    # as the id there differs from what the threading module reports
-    if is_gevent():
-        gevent_hub = get_gevent_hub()
-        if gevent_hub is not None:
-            try:
-                # this is undocumented, so wrap it in try except to be safe
-                return gevent_hub.thread_ident, None
-            except AttributeError:
-                pass
-
-    # use the current thread's id if possible
-    try:
-        thread = threading.current_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # if we can't get the current thread id, fall back to the main thread id
-    try:
-        thread = threading.main_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # we've tried everything, time to give up
-    return None, None
-
-
-def _register_control_flow_exception(
-    exc_type: "Union[type, list[type], tuple[type], set[type]]",
-) -> None:
-    if isinstance(exc_type, (list, tuple, set)):
-        _control_flow_exception_classes.update(exc_type)
-    else:
-        _control_flow_exception_classes.add(exc_type)
-
-
-def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
-    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
-        # https://docs.python.org/3/library/exceptions.html#SystemExit
-        return False
-
-    if issubclass(ty, tuple(_control_flow_exception_classes)):
-        return False
-
-    return True
-
-
-if TYPE_CHECKING:
-    T = TypeVar("T")
-
-
-def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
-    """
-    Attempt to convert from an unknown type to a specific type, using the
-    given function. Return None if the conversion fails, i.e. if the function
-    raises an exception.
-    """
-    try:
-        if isinstance(value, convert_func):  # type: ignore
-            return value
-    except TypeError:
-        pass
-
-    try:
-        return convert_func(value)
-    except Exception:
-        return None
-
-
-def safe_serialize(data: "Any") -> str:
-    """Safely serialize to a readable string."""
-
-    def serialize_item(
-        item: "Any",
-    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
-        if callable(item):
-            try:
-                module = getattr(item, "__module__", None)
-                qualname = getattr(item, "__qualname__", None)
-                name = getattr(item, "__name__", "anonymous")
-
-                if module and qualname:
-                    full_path = f"{module}.{qualname}"
-                elif module and name:
-                    full_path = f"{module}.{name}"
-                else:
-                    full_path = name
-
-                return f""
-            except Exception:
-                return f""
-        elif isinstance(item, dict):
-            return {k: serialize_item(v) for k, v in item.items()}
-        elif isinstance(item, (list, tuple)):
-            return [serialize_item(x) for x in item]
-        elif hasattr(item, "__dict__"):
-            try:
-                attrs = {
-                    k: serialize_item(v)
-                    for k, v in vars(item).items()
-                    if not k.startswith("_")
-                }
-                return f"<{type(item).__name__} {attrs}>"
-            except Exception:
-                return repr(item)
-        else:
-            return item
-
-    try:
-        serialized = serialize_item(data)
-        return (
-            json.dumps(serialized, default=str)
-            if not isinstance(serialized, str)
-            else serialized
-        )
-    except Exception:
-        return str(data)
-
-
-def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(
-        options.get("enable_logs", False)
-        or options["_experiments"].get("enable_logs", False)
-    )
-
-
-def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return "data_collection" in options.get("_experiments", {})
-
-
-def get_before_send_log(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_log") or options["_experiments"].get(
-        "before_send_log"
-    )
-
-
-def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(options.get("enable_metrics", True))
-
-
-def get_before_send_metric(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_metric") or options["_experiments"].get(
-        "before_send_metric"
-    )
-
-
-def get_before_send_span(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
-    if options is None:
-        return None
-
-    return options["_experiments"].get("before_send_span")
-
-
-def format_attribute(val: "Any") -> "AttributeValue":
-    """
-    Turn unsupported attribute value types into an AttributeValue.
-
-    We do this as soon as a user-provided attribute is set, to prevent spans,
-    logs, metrics and similar from having live references to various objects.
-
-    Note: This is not the final attribute value format. Before they're sent,
-    they're serialized further into the actual format the protocol expects:
-    https://develop.sentry.dev/sdk/telemetry/attributes/
-    """
-    if isinstance(val, (bool, int, float, str)):
-        return val
-
-    if isinstance(val, (list, tuple)) and not val:
-        return []
-    elif isinstance(val, list):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return copy.deepcopy(val)
-    elif isinstance(val, tuple):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return list(val)
-
-    return safe_repr(val)
-
-
-def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
-    """Serialize attribute value to the transport format."""
-    if isinstance(val, bool):
-        return {"value": val, "type": "boolean"}
-    if isinstance(val, int):
-        return {"value": val, "type": "integer"}
-    if isinstance(val, float):
-        return {"value": val, "type": "double"}
-    if isinstance(val, str):
-        return {"value": val, "type": "string"}
-
-    if isinstance(val, list):
-        if not val:
-            return {"value": [], "type": "array"}
-
-        # Only lists of elements of a single type are supported
-        ty = type(val[0])
-        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
-            return {"value": val, "type": "array"}
-
-    # Coerce to string if we don't know what to do with the value. This should
-    # never happen as we pre-format early in format_attribute, but let's be safe.
-    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py
deleted file mode 100644
index 5eb9b23130..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/sentry_sdk/worker.py
+++ /dev/null
@@ -1,308 +0,0 @@
-import asyncio
-import os
-import threading
-from abc import ABC, abstractmethod
-from time import sleep, time
-from typing import TYPE_CHECKING
-
-from sentry_sdk._queue import FullError, Queue
-from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
-from sentry_sdk.utils import logger, mark_sentry_task_internal
-
-if TYPE_CHECKING:
-    from typing import Any, Callable, Optional
-
-
-_TERMINATOR = object()
-
-
-class Worker(ABC):
-    """Base class for all workers."""
-
-    @property
-    @abstractmethod
-    def is_alive(self) -> bool:
-        """Whether the worker is alive and running."""
-        pass
-
-    @abstractmethod
-    def kill(self) -> None:
-        """Kill the worker. It will not process any more events."""
-        pass
-
-    def flush(
-        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
-    ) -> None:
-        """Flush the worker, blocking until done or timeout is reached."""
-        return None
-
-    @abstractmethod
-    def full(self) -> bool:
-        """Whether the worker's queue is full."""
-        pass
-
-    @abstractmethod
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        """Schedule a callback. Returns True if queued, False if full."""
-        pass
-
-
-class BackgroundWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Queue" = Queue(queue_size)
-        self._lock = threading.Lock()
-        self._thread: "Optional[threading.Thread]" = None
-        self._thread_for_pid: "Optional[int]" = None
-
-    @property
-    def is_alive(self) -> bool:
-        if self._thread_for_pid != os.getpid():
-            return False
-        if not self._thread:
-            return False
-        return self._thread.is_alive()
-
-    def _ensure_thread(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    def _timed_queue_join(self, timeout: float) -> bool:
-        deadline = time() + timeout
-        queue = self._queue
-
-        queue.all_tasks_done.acquire()
-
-        try:
-            while queue.unfinished_tasks:
-                delay = deadline - time()
-                if delay <= 0:
-                    return False
-                queue.all_tasks_done.wait(timeout=delay)
-
-            return True
-        finally:
-            queue.all_tasks_done.release()
-
-    def start(self) -> None:
-        with self._lock:
-            if not self.is_alive:
-                self._thread = threading.Thread(
-                    target=self._target, name="sentry-sdk.BackgroundWorker"
-                )
-                self._thread.daemon = True
-                try:
-                    self._thread.start()
-                    self._thread_for_pid = os.getpid()
-                except RuntimeError:
-                    # At this point we can no longer start because the interpreter
-                    # is already shutting down.  Sadly at this point we can no longer
-                    # send out events.
-                    self._thread = None
-
-    def kill(self) -> None:
-        """
-        Kill worker thread. Returns immediately. Not useful for
-        waiting on shutdown for events, use `flush` for that.
-        """
-        logger.debug("background worker got kill request")
-        with self._lock:
-            if self._thread:
-                try:
-                    self._queue.put_nowait(_TERMINATOR)
-                except FullError:
-                    logger.debug("background worker queue full, kill failed")
-
-                self._thread = None
-                self._thread_for_pid = None
-
-    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
-        logger.debug("background worker got flush request")
-        with self._lock:
-            if self.is_alive and timeout > 0.0:
-                self._wait_flush(timeout, callback)
-        logger.debug("background worker flushed")
-
-    def full(self) -> bool:
-        return self._queue.full()
-
-    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
-        initial_timeout = min(0.1, timeout)
-        if not self._timed_queue_join(initial_timeout):
-            pending = self._queue.qsize() + 1
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            if not self._timed_queue_join(timeout - initial_timeout):
-                pending = self._queue.qsize() + 1
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_thread()
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except FullError:
-            return False
-
-    def _target(self) -> None:
-        while True:
-            callback = self._queue.get()
-            try:
-                if callback is _TERMINATOR:
-                    break
-                try:
-                    callback()
-                except Exception:
-                    logger.error("Failed processing job", exc_info=True)
-            finally:
-                self._queue.task_done()
-            sleep(0)
-
-
-class AsyncWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Optional[asyncio.Queue[Any]]" = None
-        self._queue_size = queue_size
-        self._task: "Optional[asyncio.Task[None]]" = None
-        # Event loop needs to remain in the same process
-        self._task_for_pid: "Optional[int]" = None
-        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
-        # Track active callback tasks so they have a strong reference and can be cancelled on kill
-        self._active_tasks: "set[asyncio.Task[None]]" = set()
-
-    @property
-    def is_alive(self) -> bool:
-        if self._task_for_pid != os.getpid():
-            return False
-        if not self._task or not self._loop:
-            return False
-        return self._loop.is_running() and not self._task.done()
-
-    def kill(self) -> None:
-        if self._task:
-            # Cancel the main consumer task to prevent duplicate consumers
-            self._task.cancel()
-            # Also cancel any active callback tasks
-            # Avoid modifying the set while cancelling tasks
-            tasks_to_cancel = set(self._active_tasks)
-            for task in tasks_to_cancel:
-                task.cancel()
-            self._active_tasks.clear()
-            self._loop = None
-            self._task = None
-            self._task_for_pid = None
-
-    def start(self) -> None:
-        if not self.is_alive:
-            try:
-                self._loop = asyncio.get_running_loop()
-                # Always create a fresh queue on start to avoid stale items
-                self._queue = asyncio.Queue(maxsize=self._queue_size)
-                with mark_sentry_task_internal():
-                    self._task = self._loop.create_task(self._target())
-                self._task_for_pid = os.getpid()
-            except RuntimeError:
-                # There is no event loop running
-                logger.warning("No event loop running, async worker not started")
-                self._loop = None
-                self._task = None
-                self._task_for_pid = None
-
-    def full(self) -> bool:
-        if self._queue is None:
-            return True
-        return self._queue.full()
-
-    def _ensure_task(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    async def _wait_flush(
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> None:
-        if not self._loop or not self._loop.is_running() or self._queue is None:
-            return
-
-        initial_timeout = min(0.1, timeout)
-
-        # Timeout on the join
-        try:
-            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
-        except asyncio.TimeoutError:
-            pending = self._queue.qsize() + len(self._active_tasks)
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            try:
-                remaining_timeout = timeout - initial_timeout
-                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
-            except asyncio.TimeoutError:
-                pending = self._queue.qsize() + len(self._active_tasks)
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def flush(  # type: ignore[override]
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> "Optional[asyncio.Task[None]]":
-        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
-            with mark_sentry_task_internal():
-                return self._loop.create_task(self._wait_flush(timeout, callback))
-        return None
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_task()
-        if self._queue is None:
-            return False
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except asyncio.QueueFull:
-            return False
-
-    async def _target(self) -> None:
-        if self._queue is None:
-            return
-        try:
-            while True:
-                callback = await self._queue.get()
-                if callback is _TERMINATOR:
-                    self._queue.task_done()
-                    break
-                # Firing tasks instead of awaiting them allows for concurrent requests
-                with mark_sentry_task_internal():
-                    task = asyncio.create_task(self._process_callback(callback))
-                # Create a strong reference to the task so it can be cancelled on kill
-                # and does not get garbage collected while running
-                self._active_tasks.add(task)
-                # Capture queue ref at dispatch time so done callbacks use the
-                # correct queue even if kill()/start() replace self._queue.
-                queue_ref = self._queue
-                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
-                # Yield to let the event loop run other tasks
-                await asyncio.sleep(0)
-        except asyncio.CancelledError:
-            pass  # Expected during kill()
-
-    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
-        # Callback is an async coroutine, need to await it
-        await callback()
-
-    def _on_task_complete(
-        self,
-        task: "asyncio.Task[None]",
-        queue: "Optional[asyncio.Queue[Any]]" = None,
-    ) -> None:
-        try:
-            task.result()
-        except asyncio.CancelledError:
-            pass  # Task was cancelled, expected during shutdown
-        except Exception:
-            logger.error("Failed processing job", exc_info=True)
-        finally:
-            # Mark the task as done and remove it from the active tasks set
-            # Use the queue reference captured at dispatch time, not self._queue,
-            # to avoid calling task_done() on a different queue after kill()/start().
-            if queue is not None:
-                queue.task_done()
-            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER
deleted file mode 100644
index 5c69047b2e..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/INSTALLER
+++ /dev/null
@@ -1 +0,0 @@
-uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA
deleted file mode 100644
index 9c7a4703f8..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/METADATA
+++ /dev/null
@@ -1,163 +0,0 @@
-Metadata-Version: 2.4
-Name: urllib3
-Version: 2.7.0
-Summary: HTTP library with thread-safe connection pooling, file post, and more.
-Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
-Project-URL: Documentation, https://urllib3.readthedocs.io
-Project-URL: Code, https://github.com/urllib3/urllib3
-Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
-Author-email: Andrey Petrov 
-Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
-License-Expression: MIT
-License-File: LICENSE.txt
-Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: 3.14
-Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.10
-Provides-Extra: brotli
-Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
-Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
-Provides-Extra: h2
-Requires-Dist: h2<5,>=4; extra == 'h2'
-Provides-Extra: socks
-Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
-Provides-Extra: zstd
-Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
-Description-Content-Type: text/markdown
-
-

- -![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) - -

- -

- PyPI Version - Python Versions - Join our Discord - Coverage Status - Build Status on GitHub - Documentation Status
- OpenSSF Scorecard - SLSA 3 - CII Best Practices -

- -urllib3 is a powerful, *user-friendly* HTTP client for Python. -urllib3 brings many critical features that are missing from the Python -standard libraries: - -- Thread safety. -- Connection pooling. -- Client-side SSL/TLS verification. -- File uploads with multipart encoding. -- Helpers for retrying requests and dealing with HTTP redirects. -- Support for gzip, deflate, brotli, and zstd encoding. -- Proxy support for HTTP and SOCKS. -- 100% test coverage. - -... and many more features, but most importantly: Our maintainers have a 15+ -year track record of maintaining urllib3 with the highest code standards and -attention to security and safety. - -[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) -and you should too. - - -## Installing - -urllib3 can be installed with [pip](https://pip.pypa.io): - -```bash -$ python -m pip install urllib3 -``` - -Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): - -```bash -$ git clone https://github.com/urllib3/urllib3.git -$ cd urllib3 -$ pip install . -``` - -## Getting Started - -urllib3 is easy to use: - -```python3 ->>> import urllib3 ->>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") ->>> resp.status -200 ->>> resp.data -b"User-agent: *\nDisallow: /deny\n" -``` - -urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). - - -## Community - -urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and -collaborating with other contributors. Drop by and say hello 👋 - - -## Contributing - -urllib3 happily accepts contributions. Please see our -[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) -for some tips on getting started. - - -## Security Disclosures - -To report a security vulnerability, please use the -[Tidelift security contact](https://tidelift.com/security). -Tidelift will coordinate the fix and disclosure with maintainers. - - -## Maintainers - -Meet our maintainers since 2008: - -- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) -- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) -- [@pquentin](https://github.com/pquentin) (Quentin Pradet) -- [@theacodes](https://github.com/theacodes) (Thea Flowers) -- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) -- [@lukasa](https://github.com/lukasa) (Cory Benfield) -- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) -- [@shazow](https://github.com/shazow) (Andrey Petrov) - -👋 - - -## Sponsorship - -If your company benefits from this library, please consider [sponsoring its -development](https://urllib3.readthedocs.io/en/latest/sponsors.html). - - -## For Enterprise - -Professional support for urllib3 is available as part of the [Tidelift -Subscription][1]. Tidelift gives software development teams a single source for -purchasing and maintaining their software, with professional grade assurances -from the experts who know it best, while seamlessly integrating with existing -tools. - -[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD deleted file mode 100644 index 8c4f9c0b4b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/RECORD +++ /dev/null @@ -1,44 +0,0 @@ -urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 -urllib3-2.7.0.dist-info/RECORD,, -urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 -urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 -urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 -urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 -urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 -urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 -urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 -urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 -urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 -urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 -urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 -urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 -urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 -urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 -urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 -urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 -urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 -urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 -urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 -urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 -urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 -urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 -urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 -urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 -urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 -urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 -urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 -urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 -urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 -urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 -urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 -urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 -urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 -urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 -urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 -urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 -urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 -urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 -urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL deleted file mode 100644 index b1b94fd58e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.29.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt deleted file mode 100644 index e6183d0276..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3-2.7.0.dist-info/licenses/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2008-2020 Andrey Petrov and contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py deleted file mode 100644 index 3fe782c8a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/__init__.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more -""" - -from __future__ import annotations - -# Set default logging handler to avoid "No handler found" warnings. -import logging -import sys -import typing -import warnings -from logging import NullHandler - -from . import exceptions -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._version import __version__ -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .poolmanager import PoolManager, ProxyManager, proxy_from_url -from .response import BaseHTTPResponse, HTTPResponse -from .util.request import make_headers -from .util.retry import Retry -from .util.timeout import Timeout - -# Ensure that Python is compiled with OpenSSL 1.1.1+ -# If the 'ssl' module isn't available at all that's -# fine, we only care if the module is available. -try: - import ssl -except ImportError: - pass -else: - if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: - warnings.warn( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/3020", - exceptions.NotOpenSSLWarning, - ) - elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: - raise ImportError( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/2168" - ) - -__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" -__license__ = "MIT" -__version__ = __version__ - -__all__ = ( - "HTTPConnectionPool", - "HTTPHeaderDict", - "HTTPSConnectionPool", - "PoolManager", - "ProxyManager", - "HTTPResponse", - "Retry", - "Timeout", - "add_stderr_logger", - "connection_from_url", - "disable_warnings", - "encode_multipart_formdata", - "make_headers", - "proxy_from_url", - "request", - "BaseHTTPResponse", -) - -logging.getLogger(__name__).addHandler(NullHandler()) - - -def add_stderr_logger( - level: int = logging.DEBUG, -) -> logging.StreamHandler[typing.TextIO]: - """ - Helper for quickly adding a StreamHandler to the logger. Useful for - debugging. - - Returns the handler after adding it. - """ - # This method needs to be in this __init__.py to get the __name__ correct - # even if urllib3 is vendored within another package. - logger = logging.getLogger(__name__) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) - logger.addHandler(handler) - logger.setLevel(level) - logger.debug("Added a stderr logging handler to logger: %s", __name__) - return handler - - -# ... Clean up. -del NullHandler - - -# All warning filters *must* be appended unless you're really certain that they -# shouldn't be: otherwise, it's very hard for users to use most Python -# mechanisms to silence them. -# SecurityWarning's always go off by default. -warnings.simplefilter("always", exceptions.SecurityWarning, append=True) -# InsecurePlatformWarning's don't vary between requests, so we keep it default. -warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) - - -def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: - """ - Helper for quickly disabling all urllib3 warnings. - """ - warnings.simplefilter("ignore", category) - - -_DEFAULT_POOL = PoolManager() - - -def request( - method: str, - url: str, - *, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - preload_content: bool | None = True, - decode_content: bool | None = True, - redirect: bool | None = True, - retries: Retry | bool | int | None = None, - timeout: Timeout | float | int | None = 3, - json: typing.Any | None = None, -) -> BaseHTTPResponse: - """ - A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. - Therefore, its side effects could be shared across dependencies relying on it. - To avoid side effects create a new ``PoolManager`` instance and use it instead. - The method does not accept low-level ``**urlopen_kw`` keyword arguments. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - - return _DEFAULT_POOL.request( - method, - url, - body=body, - fields=fields, - headers=headers, - preload_content=preload_content, - decode_content=decode_content, - redirect=redirect, - retries=retries, - timeout=timeout, - json=json, - ) - - -if sys.platform == "emscripten": - from .contrib.emscripten import inject_into_urllib3 # noqa: 401 - - inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py deleted file mode 100644 index 992ec1657a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_base_connection.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -import typing - -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from .util.url import Url - -_TYPE_BODY = typing.Union[ - bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str -] - - -class ProxyConfig(typing.NamedTuple): - ssl_context: ssl.SSLContext | None - use_forwarding_for_https: bool - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - - -class _ResponseOptions(typing.NamedTuple): - # TODO: Remove this in favor of a better - # HTTP request/response lifecycle tracking. - request_method: str - request_url: str - preload_content: bool - decode_content: bool - enforce_content_length: bool - - -if typing.TYPE_CHECKING: - import ssl - from typing import Protocol - - from .response import BaseHTTPResponse - - class BaseHTTPConnection(Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - host: str - port: int - timeout: None | ( - float - ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool - proxy_is_verified: bool | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: ... - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: ... - - def connect(self) -> None: ... - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: ... - - def getresponse(self) -> BaseHTTPResponse: ... - - def close(self) -> None: ... - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - - class BaseHTTPSConnection(BaseHTTPConnection, Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - # Certificate verification methods - cert_reqs: int | str | None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - ssl_context: ssl.SSLContext | None - - # Trusted CAs - ca_certs: str | None - ca_cert_dir: str | None - ca_cert_data: None | str | bytes - - # TLS version - ssl_minimum_version: int | None - ssl_maximum_version: int | None - ssl_version: int | str | None # Deprecated - - # Client certificates - cert_file: str | None - key_file: str | None - key_password: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py deleted file mode 100644 index ee9ca662b6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_collections.py +++ /dev/null @@ -1,486 +0,0 @@ -from __future__ import annotations - -import typing -from collections import OrderedDict -from enum import Enum, auto -from threading import RLock - -if typing.TYPE_CHECKING: - # We can only import Protocol if TYPE_CHECKING because it's a development - # dependency, and is not available at runtime. - from typing import Protocol - - from typing_extensions import Self - - class HasGettableStringKeys(Protocol): - def keys(self) -> typing.Iterator[str]: ... - - def __getitem__(self, key: str) -> str: ... - - -__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] - - -# Key type -_KT = typing.TypeVar("_KT") -# Value type -_VT = typing.TypeVar("_VT") -# Default type -_DT = typing.TypeVar("_DT") - -ValidHTTPHeaderSource = typing.Union[ - "HTTPHeaderDict", - typing.Mapping[str, str], - typing.Iterable[tuple[str, str]], - "HasGettableStringKeys", -] - - -class _Sentinel(Enum): - not_passed = auto() - - -def ensure_can_construct_http_header_dict( - potential: object, -) -> ValidHTTPHeaderSource | None: - if isinstance(potential, HTTPHeaderDict): - return potential - elif isinstance(potential, typing.Mapping): - # Full runtime checking of the contents of a Mapping is expensive, so for the - # purposes of typechecking, we assume that any Mapping is the right shape. - return typing.cast(typing.Mapping[str, str], potential) - elif isinstance(potential, typing.Iterable): - # Similarly to Mapping, full runtime checking of the contents of an Iterable is - # expensive, so for the purposes of typechecking, we assume that any Iterable - # is the right shape. - return typing.cast(typing.Iterable[tuple[str, str]], potential) - elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): - return typing.cast("HasGettableStringKeys", potential) - else: - return None - - -class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): - """ - Provides a thread-safe dict-like container which maintains up to - ``maxsize`` keys while throwing away the least-recently-used keys beyond - ``maxsize``. - - :param maxsize: - Maximum number of recent elements to retain. - - :param dispose_func: - Every time an item is evicted from the container, - ``dispose_func(value)`` is called. Callback which will get called - """ - - _container: typing.OrderedDict[_KT, _VT] - _maxsize: int - dispose_func: typing.Callable[[_VT], None] | None - lock: RLock - - def __init__( - self, - maxsize: int = 10, - dispose_func: typing.Callable[[_VT], None] | None = None, - ) -> None: - super().__init__() - self._maxsize = maxsize - self.dispose_func = dispose_func - self._container = OrderedDict() - self.lock = RLock() - - def __getitem__(self, key: _KT) -> _VT: - # Re-insert the item, moving it to the end of the eviction line. - with self.lock: - item = self._container.pop(key) - self._container[key] = item - return item - - def __setitem__(self, key: _KT, value: _VT) -> None: - evicted_item = None - with self.lock: - # Possibly evict the existing value of 'key' - try: - # If the key exists, we'll overwrite it, which won't change the - # size of the pool. Because accessing a key should move it to - # the end of the eviction line, we pop it out first. - evicted_item = key, self._container.pop(key) - self._container[key] = value - except KeyError: - # When the key does not exist, we insert the value first so that - # evicting works in all cases, including when self._maxsize is 0 - self._container[key] = value - if len(self._container) > self._maxsize: - # If we didn't evict an existing value, and we've hit our maximum - # size, then we have to evict the least recently used item from - # the beginning of the container. - evicted_item = self._container.popitem(last=False) - - # After releasing the lock on the pool, dispose of any evicted value. - if evicted_item is not None and self.dispose_func: - _, evicted_value = evicted_item - self.dispose_func(evicted_value) - - def __delitem__(self, key: _KT) -> None: - with self.lock: - value = self._container.pop(key) - - if self.dispose_func: - self.dispose_func(value) - - def __len__(self) -> int: - with self.lock: - return len(self._container) - - def __iter__(self) -> typing.NoReturn: - raise NotImplementedError( - "Iteration over this class is unlikely to be threadsafe." - ) - - def clear(self) -> None: - with self.lock: - # Copy pointers to all values, then wipe the mapping - values = list(self._container.values()) - self._container.clear() - - if self.dispose_func: - for value in values: - self.dispose_func(value) - - def keys(self) -> set[_KT]: # type: ignore[override] - with self.lock: - return set(self._container.keys()) - - -class HTTPHeaderDictItemView(set[tuple[str, str]]): - """ - HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of - address. - - If we directly try to get an item with a particular name, we will get a string - back that is the concatenated version of all the values: - - >>> d['X-Header-Name'] - 'Value1, Value2, Value3' - - However, if we iterate over an HTTPHeaderDict's items, we will optionally combine - these values based on whether combine=True was called when building up the dictionary - - >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) - >>> d.add("A", "2", combine=True) - >>> d.add("B", "bar") - >>> list(d.items()) - [ - ('A', '1, 2'), - ('B', 'foo'), - ('B', 'bar'), - ] - - This class conforms to the interface required by the MutableMapping ABC while - also giving us the nonstandard iteration behavior we want; items with duplicate - keys, ordered by time of first insertion. - """ - - _headers: HTTPHeaderDict - - def __init__(self, headers: HTTPHeaderDict) -> None: - self._headers = headers - - def __len__(self) -> int: - return len(list(self._headers.iteritems())) - - def __iter__(self) -> typing.Iterator[tuple[str, str]]: - return self._headers.iteritems() - - def __contains__(self, item: object) -> bool: - if isinstance(item, tuple) and len(item) == 2: - passed_key, passed_val = item - if isinstance(passed_key, str) and isinstance(passed_val, str): - return self._headers._has_value_for_header(passed_key, passed_val) - return False - - -class HTTPHeaderDict(typing.MutableMapping[str, str]): - """ - :param headers: - An iterable of field-value pairs. Must not contain multiple field names - when compared case-insensitively. - - :param kwargs: - Additional field-value pairs to pass in to ``dict.update``. - - A ``dict`` like container for storing HTTP Headers. - - Field names are stored and compared case-insensitively in compliance with - RFC 7230. Iteration provides the first case-sensitive key seen for each - case-insensitive pair. - - Using ``__setitem__`` syntax overwrites fields that compare equal - case-insensitively in order to maintain ``dict``'s api. For fields that - compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` - in a loop. - - If multiple fields that are equal case-insensitively are passed to the - constructor or ``.update``, the behavior is undefined and some will be - lost. - - >>> headers = HTTPHeaderDict() - >>> headers.add('Set-Cookie', 'foo=bar') - >>> headers.add('set-cookie', 'baz=quxx') - >>> headers['content-length'] = '7' - >>> headers['SET-cookie'] - 'foo=bar, baz=quxx' - >>> headers['Content-Length'] - '7' - """ - - _container: typing.MutableMapping[str, list[str]] - - def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): - super().__init__() - self._container = {} # 'dict' is insert-ordered - if headers is not None: - if isinstance(headers, HTTPHeaderDict): - self._copy_from(headers) - else: - self.extend(headers) - if kwargs: - self.extend(kwargs) - - def __setitem__(self, key: str, val: str) -> None: - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - self._container[key.lower()] = [key, val] - - def __getitem__(self, key: str) -> str: - if isinstance(key, bytes): - key = key.decode("latin-1") - val = self._container[key.lower()] - return ", ".join(val[1:]) - - def __delitem__(self, key: str) -> None: - if isinstance(key, bytes): - key = key.decode("latin-1") - del self._container[key.lower()] - - def __contains__(self, key: object) -> bool: - if isinstance(key, bytes): - key = key.decode("latin-1") - if isinstance(key, str): - return key.lower() in self._container - return False - - def setdefault(self, key: str, default: str = "") -> str: - return super().setdefault(key, default) - - def __eq__(self, other: object) -> bool: - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return False - else: - other_as_http_header_dict = type(self)(maybe_constructable) - - return {k.lower(): v for k, v in self.itermerged()} == { - k.lower(): v for k, v in other_as_http_header_dict.itermerged() - } - - def __ne__(self, other: object) -> bool: - return not self.__eq__(other) - - def __len__(self) -> int: - return len(self._container) - - def __iter__(self) -> typing.Iterator[str]: - # Only provide the originally cased names - for vals in self._container.values(): - yield vals[0] - - def discard(self, key: str) -> None: - try: - del self[key] - except KeyError: - pass - - def add(self, key: str, val: str, *, combine: bool = False) -> None: - """Adds a (name, value) pair, doesn't overwrite the value if it already - exists. - - If this is called with combine=True, instead of adding a new header value - as a distinct item during iteration, this will instead append the value to - any existing header value with a comma. If no existing header value exists - for the key, then the value will simply be added, ignoring the combine parameter. - - >>> headers = HTTPHeaderDict(foo='bar') - >>> headers.add('Foo', 'baz') - >>> headers['foo'] - 'bar, baz' - >>> list(headers.items()) - [('foo', 'bar'), ('foo', 'baz')] - >>> headers.add('foo', 'quz', combine=True) - >>> list(headers.items()) - [('foo', 'bar, baz, quz')] - """ - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - key_lower = key.lower() - new_vals = [key, val] - # Keep the common case aka no item present as fast as possible - vals = self._container.setdefault(key_lower, new_vals) - if new_vals is not vals: - # if there are values here, then there is at least the initial - # key/value pair - assert len(vals) >= 2 - if combine: - vals[-1] = vals[-1] + ", " + val - else: - vals.append(val) - - def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: - """Generic import function for any type of header-like object. - Adapted version of MutableMapping.update in order to insert items - with self.add instead of self.__setitem__ - """ - if len(args) > 1: - raise TypeError( - f"extend() takes at most 1 positional arguments ({len(args)} given)" - ) - other = args[0] if len(args) >= 1 else () - - if isinstance(other, HTTPHeaderDict): - for key, val in other.iteritems(): - self.add(key, val) - elif isinstance(other, typing.Mapping): - for key, val in other.items(): - self.add(key, val) - elif isinstance(other, typing.Iterable): - for key, value in other: - self.add(key, value) - elif hasattr(other, "keys") and hasattr(other, "__getitem__"): - # THIS IS NOT A TYPESAFE BRANCH - # In this branch, the object has a `keys` attr but is not a Mapping or any of - # the other types indicated in the method signature. We do some stuff with - # it as though it partially implements the Mapping interface, but we're not - # doing that stuff safely AT ALL. - for key in other.keys(): - self.add(key, other[key]) - - for key, value in kwargs.items(): - self.add(key, value) - - @typing.overload - def getlist(self, key: str) -> list[str]: ... - - @typing.overload - def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... - - def getlist( - self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed - ) -> list[str] | _DT: - """Returns a list of all the values for the named field. Returns an - empty list if the key doesn't exist.""" - if isinstance(key, bytes): - key = key.decode("latin-1") - try: - vals = self._container[key.lower()] - except KeyError: - if default is _Sentinel.not_passed: - # _DT is unbound; empty list is instance of List[str] - return [] - # _DT is bound; default is instance of _DT - return default - else: - # _DT may or may not be bound; vals[1:] is instance of List[str], which - # meets our external interface requirement of `Union[List[str], _DT]`. - return vals[1:] - - def _prepare_for_method_change(self) -> Self: - """ - Remove content-specific header fields before changing the request - method to GET or HEAD according to RFC 9110, Section 15.4. - """ - content_specific_headers = [ - "Content-Encoding", - "Content-Language", - "Content-Location", - "Content-Type", - "Content-Length", - "Digest", - "Last-Modified", - ] - for header in content_specific_headers: - self.discard(header) - return self - - # Backwards compatibility for httplib - getheaders = getlist - getallmatchingheaders = getlist - iget = getlist - - # Backwards compatibility for http.cookiejar - get_all = getlist - - def __repr__(self) -> str: - return f"{type(self).__name__}({dict(self.itermerged())})" - - def _copy_from(self, other: HTTPHeaderDict) -> None: - for key in other: - val = other.getlist(key) - self._container[key.lower()] = [key, *val] - - def copy(self) -> Self: - clone = type(self)() - clone._copy_from(self) - return clone - - def iteritems(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all header lines, including duplicate ones.""" - for key in self: - vals = self._container[key.lower()] - for val in vals[1:]: - yield vals[0], val - - def itermerged(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all headers, merging duplicate ones together.""" - for key in self: - val = self._container[key.lower()] - yield val[0], ", ".join(val[1:]) - - def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] - return HTTPHeaderDictItemView(self) - - def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: - if header_name in self: - return potential_value in self._container[header_name.lower()][1:] - return False - - def __ior__(self, other: object) -> HTTPHeaderDict: - # Supports extending a header dict in-place using operator |= - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - self.extend(maybe_constructable) - return self - - def __or__(self, other: object) -> Self: - # Supports merging header dicts using operator | - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = self.copy() - result.extend(maybe_constructable) - return result - - def __ror__(self, other: object) -> Self: - # Supports merging header dicts using operator | when other is on left side - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = type(self)(maybe_constructable) - result.extend(self) - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py deleted file mode 100644 index 297c271bf4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_request_methods.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -import json as _json -import typing -from urllib.parse import urlencode - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .response import BaseHTTPResponse - -__all__ = ["RequestMethods"] - -_TYPE_ENCODE_URL_FIELDS = typing.Union[ - typing.Sequence[tuple[str, typing.Union[str, bytes]]], - typing.Mapping[str, typing.Union[str, bytes]], -] - - -class RequestMethods: - """ - Convenience mixin for classes who implement a :meth:`urlopen` method, such - as :class:`urllib3.HTTPConnectionPool` and - :class:`urllib3.PoolManager`. - - Provides behavior for making common types of HTTP request methods and - decides which type of request field encoding to use. - - Specifically, - - :meth:`.request_encode_url` is for sending requests whose fields are - encoded in the URL (such as GET, HEAD, DELETE). - - :meth:`.request_encode_body` is for sending requests whose fields are - encoded in the *body* of the request using multipart or www-form-urlencoded - (such as for POST, PUT, PATCH). - - :meth:`.request` is for making any kind of request, it will look up the - appropriate encoding format and use one of the above two methods to make - the request. - - Initializer parameters: - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - """ - - _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} - - def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: - self.headers = headers or {} - - def urlopen( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **kw: typing.Any, - ) -> BaseHTTPResponse: # Abstract - raise NotImplementedError( - "Classes extending RequestMethods must implement " - "their own ``urlopen`` method." - ) - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - json: typing.Any | None = None, - **urlopen_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the appropriate encoding of - ``fields`` based on the ``method`` used. - - This is a convenience method that requires the least amount of manual - effort. It can be used in most situations, while still having the - option to drop down to more specific methods when necessary, such as - :meth:`request_encode_url`, :meth:`request_encode_body`, - or even the lowest level :meth:`urlopen`. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the URL or request body, depending on ``method``. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - method = method.upper() - - if json is not None and body is not None: - raise TypeError( - "request got values for both 'body' and 'json' parameters which are mutually exclusive" - ) - - if json is not None: - if headers is None: - headers = self.headers - - if not ("content-type" in map(str.lower, headers.keys())): - headers = HTTPHeaderDict(headers) - headers["Content-Type"] = "application/json" - - body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( - "utf-8" - ) - - if body is not None: - urlopen_kw["body"] = body - - if method in self._encode_url_methods: - return self.request_encode_url( - method, - url, - fields=fields, # type: ignore[arg-type] - headers=headers, - **urlopen_kw, - ) - else: - return self.request_encode_body( - method, url, fields=fields, headers=headers, **urlopen_kw - ) - - def request_encode_url( - self, - method: str, - url: str, - fields: _TYPE_ENCODE_URL_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the url. This is useful for request methods like GET, HEAD, DELETE, etc. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the URL. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": headers} - extra_kw.update(urlopen_kw) - - if fields: - url += "?" + urlencode(fields) - - return self.urlopen(method, url, **extra_kw) - - def request_encode_body( - self, - method: str, - url: str, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the body. This is useful for request methods like POST, PUT, PATCH, etc. - - When ``encode_multipart=True`` (default), then - :func:`urllib3.encode_multipart_formdata` is used to encode - the payload with the appropriate content type. Otherwise - :func:`urllib.parse.urlencode` is used with the - 'application/x-www-form-urlencoded' content type. - - Multipart encoding must be used when posting files, and it's reasonably - safe to use it in other times too. However, it may break request - signing, such as with OAuth. - - Supports an optional ``fields`` parameter of key/value strings AND - key/filetuple. A filetuple is a (filename, data, MIME type) tuple where - the MIME type is optional. For example:: - - fields = { - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), - 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - } - - When uploading a file, providing a filename (the first parameter of the - tuple) is optional but recommended to best mimic behavior of browsers. - - Note that if ``headers`` are supplied, the 'Content-Type' header will - be overwritten because it depends on the dynamic random boundary string - which is used to compose the body of the request. The random boundary - string can be explicitly set with the ``multipart_boundary`` parameter. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param encode_multipart: - If True, encode the ``fields`` using the multipart/form-data MIME - format. - - :param multipart_boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} - body: bytes | str - - if fields: - if "body" in urlopen_kw: - raise TypeError( - "request got values for both 'fields' and 'body', can only specify one." - ) - - if encode_multipart: - body, content_type = encode_multipart_formdata( - fields, boundary=multipart_boundary - ) - else: - body, content_type = ( - urlencode(fields), # type: ignore[arg-type] - "application/x-www-form-urlencoded", - ) - - extra_kw["body"] = body - extra_kw["headers"].setdefault("Content-Type", content_type) - - extra_kw.update(urlopen_kw) - - return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py deleted file mode 100644 index bfed03985b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/_version.py +++ /dev/null @@ -1,24 +0,0 @@ -# file generated by vcs-versioning -# don't change, don't track in version control -from __future__ import annotations - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -version: str -__version__: str -__version_tuple__: tuple[int | str, ...] -version_tuple: tuple[int | str, ...] -commit_id: str | None -__commit_id__: str | None - -__version__ = version = '2.7.0' -__version_tuple__ = version_tuple = (2, 7, 0) - -__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py deleted file mode 100644 index 84e1dab945..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connection.py +++ /dev/null @@ -1,1099 +0,0 @@ -from __future__ import annotations - -import datetime -import http.client -import logging -import os -import re -import socket -import sys -import threading -import typing -import warnings -from http.client import HTTPConnection as _HTTPConnection -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from .response import HTTPResponse - from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT - from .util.ssltransport import SSLTransport - -from ._collections import HTTPHeaderDict -from .http2 import probe as http2_probe -from .util.response import assert_header_parsing -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout -from .util.util import to_str -from .util.wait import wait_for_read - -try: # Compiled with SSL? - import ssl - - BaseSSLError = ssl.SSLError -except (ImportError, AttributeError): - ssl = None # type: ignore[assignment] - - class BaseSSLError(BaseException): # type: ignore[no-redef] - pass - - -from ._base_connection import _TYPE_BODY -from ._base_connection import ProxyConfig as ProxyConfig -from ._base_connection import _ResponseOptions as _ResponseOptions -from ._version import __version__ -from .exceptions import ( - ConnectTimeoutError, - HeaderParsingError, - NameResolutionError, - NewConnectionError, - ProxyError, - SystemTimeWarning, -) -from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ -from .util.request import body_to_chunks -from .util.ssl_ import assert_fingerprint as _assert_fingerprint -from .util.ssl_ import ( - create_urllib3_context, - is_ipaddress, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .util.ssl_match_hostname import CertificateError, match_hostname -from .util.url import Url - -# Not a no-op, we're adding this to the namespace so it can be imported. -ConnectionError = ConnectionError -BrokenPipeError = BrokenPipeError - - -log = logging.getLogger(__name__) - -port_by_scheme = {"http": 80, "https": 443} - -# When it comes time to update this value as a part of regular maintenance -# (ie test_recent_date is failing) update it to ~6 months before the current date. -RECENT_DATE = datetime.date(2025, 1, 1) - -_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") - - -class HTTPConnection(_HTTPConnection): - """ - Based on :class:`http.client.HTTPConnection` but provides an extra constructor - backwards-compatibility layer between older and newer Pythons. - - Additional keyword parameters are used to configure attributes of the connection. - Accepted parameters include: - - - ``source_address``: Set the source address for the current connection. - - ``socket_options``: Set specific options on the underlying socket. If not specified, then - defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling - Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. - - For example, if you wish to enable TCP Keep Alive in addition to the defaults, - you might pass: - - .. code-block:: python - - HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ] - - Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). - """ - - default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] - - #: Disable Nagle's algorithm by default. - #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` - default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - ] - - #: Whether this connection verifies the host's certificate. - is_verified: bool = False - - #: Whether this proxy connection verified the proxy host's certificate. - # If no proxy is currently connected to the value will be ``None``. - proxy_is_verified: bool | None = None - - blocksize: int - source_address: tuple[str, int] | None - socket_options: connection._TYPE_SOCKET_OPTIONS | None - - _has_connected_to_proxy: bool - _response_options: _ResponseOptions | None - _tunnel_host: str | None - _tunnel_port: int | None - _tunnel_scheme: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - super().__init__( - host=host, - port=port, - timeout=Timeout.resolve_default_timeout(timeout), - source_address=source_address, - blocksize=blocksize, - ) - self.socket_options = socket_options - self.proxy = proxy - self.proxy_config = proxy_config - - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host: str | None = None - self._tunnel_port: int | None = None - self._tunnel_scheme: str | None = None - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __repr__(self) -> str: - return f"<{self} at {id(self):#x}>" - - @property - def host(self) -> str: - """ - Getter method to remove any trailing dots that indicate the hostname is an FQDN. - - In general, SSL certificates don't include the trailing dot indicating a - fully-qualified domain name, and thus, they don't validate properly when - checked against a domain name that includes the dot. In addition, some - servers may not expect to receive the trailing dot when provided. - - However, the hostname with trailing dot is critical to DNS resolution; doing a - lookup with the trailing dot will properly only resolve the appropriate FQDN, - whereas a lookup without a trailing dot will search the system's search domain - list. Thus, it's important to keep the original host around for use only in - those cases where it's appropriate (i.e., when doing DNS lookup to establish the - actual TCP connection across which we're going to send HTTP requests). - """ - return self._dns_host.rstrip(".") - - @host.setter - def host(self, value: str) -> None: - """ - Setter for the `host` property. - - We assume that only urllib3 uses the _dns_host attribute; httplib itself - only uses `host`, and it seems reasonable that other libraries follow suit. - """ - self._dns_host = value - - def _new_conn(self) -> socket.socket: - """Establish a socket connection and set nodelay settings on it. - - :return: New socket connection. - """ - try: - sock = connection.create_connection( - (self._dns_host, self.port), - self.timeout, - source_address=self.source_address, - socket_options=self.socket_options, - ) - except socket.gaierror as e: - raise NameResolutionError(self.host, self, e) from e - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except OSError as e: - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - sys.audit("http.client.connect", self, self.host, self.port) - - return sock - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - if scheme not in ("http", "https"): - raise ValueError( - f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" - ) - super().set_tunnel(host, port=port, headers=headers) - self._tunnel_scheme = scheme - - if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): - # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. - # When using connection_from_host, host will come without brackets. - def _wrap_ipv6(self, ip: bytes) -> bytes: - if b":" in ip and ip[0] != b"["[0]: - return b"[" + ip + b"]" - return ip - - if sys.version_info < (3, 11, 9): - # `_tunnel` copied from 3.11.13 backporting - # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 - # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 - def _tunnel(self) -> None: - _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] - connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - while True: - line = response.fp.readline(_MAXLINE + 1) - if len(line) > _MAXLINE: - raise http.client.LineTooLong("header line") - if not line: - # for sites which EOF without sending a trailer - break - if line in (b"\r\n", b"\n", b""): - break - - if self.debuglevel > 0: - print("header:", line.decode()) - finally: - response.close() - - elif (3, 12) <= sys.version_info < (3, 12, 3): - # `_tunnel` copied from 3.12.11 backporting - # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 - def _tunnel(self) -> None: # noqa: F811 - connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] - - if self.debuglevel > 0: - for header in self._raw_proxy_headers: - print("header:", header.decode()) - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - - finally: - response.close() - - def connect(self) -> None: - self.sock = self._new_conn() - if self._tunnel_host: - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - # TODO: Fix tunnel so it doesn't depend on self.sock state. - self._tunnel() - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - if self._has_connected_to_proxy: - self.proxy_is_verified = False - - @property - def is_closed(self) -> bool: - return self.sock is None - - @property - def is_connected(self) -> bool: - if self.sock is None: - return False - return not wait_for_read(self.sock, timeout=0.0) - - @property - def has_connected_to_proxy(self) -> bool: - return self._has_connected_to_proxy - - @property - def proxy_is_forwarding(self) -> bool: - """ - Return True if a forwarding proxy is configured, else return False - """ - return bool(self.proxy) and self._tunnel_host is None - - @property - def proxy_is_tunneling(self) -> bool: - """ - Return True if a tunneling proxy is configured, else return False - """ - return self._tunnel_host is not None - - def close(self) -> None: - try: - super().close() - finally: - # Reset all stateful properties so connection - # can be re-used without leaking prior configs. - self.sock = None - self.is_verified = False - self.proxy_is_verified = None - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host = None - self._tunnel_port = None - self._tunnel_scheme = None - - def putrequest( - self, - method: str, - url: str, - skip_host: bool = False, - skip_accept_encoding: bool = False, - ) -> None: - """""" - # Empty docstring because the indentation of CPython's implementation - # is broken but we don't want this method in our documentation. - match = _CONTAINS_CONTROL_CHAR_RE.search(method) - if match: - raise ValueError( - f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" - ) - - return super().putrequest( - method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding - ) - - def putheader(self, header: str, *values: str) -> None: # type: ignore[override] - """""" - if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): - super().putheader(header, *values) - elif to_str(header.lower()) not in SKIPPABLE_HEADERS: - skippable_headers = "', '".join( - [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] - ) - raise ValueError( - f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" - ) - - # `request` method's signature intentionally violates LSP. - # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - # Update the inner socket's timeout value to send the request. - # This only triggers if the connection is re-used. - if self.sock is not None: - self.sock.settimeout(self.timeout) - - # Store these values to be fed into the HTTPResponse - # object later. TODO: Remove this in favor of a real - # HTTP lifecycle mechanism. - - # We have to store these before we call .request() - # because sometimes we can still salvage a response - # off the wire even if we aren't able to completely - # send the request body. - self._response_options = _ResponseOptions( - request_method=method, - request_url=url, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - if headers is None: - headers = {} - header_keys = frozenset(to_str(k.lower()) for k in headers) - skip_accept_encoding = "accept-encoding" in header_keys - skip_host = "host" in header_keys - self.putrequest( - method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host - ) - - # Transform the body into an iterable of sendall()-able chunks - # and detect if an explicit Content-Length is doable. - chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) - chunks = chunks_and_cl.chunks - content_length = chunks_and_cl.content_length - - # When chunked is explicit set to 'True' we respect that. - if chunked: - if "transfer-encoding" not in header_keys: - self.putheader("Transfer-Encoding", "chunked") - else: - # Detect whether a framing mechanism is already in use. If so - # we respect that value, otherwise we pick chunked vs content-length - # depending on the type of 'body'. - if "content-length" in header_keys: - chunked = False - elif "transfer-encoding" in header_keys: - chunked = True - - # Otherwise we go off the recommendation of 'body_to_chunks()'. - else: - chunked = False - if content_length is None: - if chunks is not None: - chunked = True - self.putheader("Transfer-Encoding", "chunked") - else: - self.putheader("Content-Length", str(content_length)) - - # Now that framing headers are out of the way we send all the other headers. - if "user-agent" not in header_keys: - self.putheader("User-Agent", _get_default_user_agent()) - for header, value in headers.items(): - self.putheader(header, value) - self.endheaders() - - # If we're given a body we start sending that in chunks. - if chunks is not None: - for chunk in chunks: - # Sending empty chunks isn't allowed for TE: chunked - # as it indicates the end of the body. - if not chunk: - continue - if isinstance(chunk, str): - chunk = chunk.encode("utf-8") - if chunked: - self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) - else: - self.send(chunk) - - # Regardless of whether we have a body or not, if we're in - # chunked mode we want to send an explicit empty chunk. - if chunked: - self.send(b"0\r\n\r\n") - - def request_chunked( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - ) -> None: - """ - Alternative to the common request method, which sends the - body with chunked encoding and not as one block - """ - warnings.warn( - "HTTPConnection.request_chunked() is deprecated and will be removed " - "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", - category=FutureWarning, - stacklevel=2, - ) - self.request(method, url, body=body, headers=headers, chunked=True) - - def getresponse( # type: ignore[override] - self, - ) -> HTTPResponse: - """ - Get the response from the server. - - If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. - - If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. - """ - # Raise the same error as http.client.HTTPConnection - if self._response_options is None: - raise ResponseNotReady() - - # Reset this attribute for being used again. - resp_options = self._response_options - self._response_options = None - - # Since the connection's timeout value may have been updated - # we need to set the timeout on the socket. - self.sock.settimeout(self.timeout) - - # This is needed here to avoid circular import errors - from .response import HTTPResponse - - # Save a reference to the shutdown function before ownership is passed - # to httplib_response - # TODO should we implement it everywhere? - _shutdown = getattr(self.sock, "shutdown", None) - - # Get the response from http.client.HTTPConnection - httplib_response = super().getresponse() - - try: - assert_header_parsing(httplib_response.msg) - except (HeaderParsingError, TypeError) as hpe: - log.warning( - "Failed to parse headers (url=%s): %s", - _url_from_connection(self, resp_options.request_url), - hpe, - exc_info=True, - ) - - headers = HTTPHeaderDict(httplib_response.msg.items()) - - response = HTTPResponse( - body=httplib_response, - headers=headers, - status=httplib_response.status, - version=httplib_response.version, - version_string=getattr(self, "_http_vsn_str", "HTTP/?"), - reason=httplib_response.reason, - preload_content=resp_options.preload_content, - decode_content=resp_options.decode_content, - original_response=httplib_response, - enforce_content_length=resp_options.enforce_content_length, - request_method=resp_options.request_method, - request_url=resp_options.request_url, - sock_shutdown=_shutdown, - ) - return response - - -class HTTPSConnection(HTTPConnection): - """ - Many of the parameters to this constructor are passed to the underlying SSL - socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. - """ - - default_port = port_by_scheme["https"] # type: ignore[misc] - - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_fingerprint: str | None = None - _connect_callback: typing.Callable[..., None] | None = None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - # cert_reqs depends on ssl_context so calculate last. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - self.cert_reqs = cert_reqs - self._connect_callback = None - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - """ - This method should only be called once, before the connection is used. - """ - warnings.warn( - "HTTPSConnection.set_cert() is deprecated and will be removed " - "in urllib3 v3.0. Instead provide the parameters to the " - "HTTPSConnection constructor.", - category=FutureWarning, - stacklevel=2, - ) - - # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also - # have an SSLContext object in which case we'll use its verify_mode. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - def connect(self) -> None: - # Today we don't need to be doing this step before the /actual/ socket - # connection, however in the future we'll need to decide whether to - # create a new socket or re-use an existing "shared" socket as a part - # of the HTTP/2 handshake dance. - if self._tunnel_host is not None and self._tunnel_port is not None: - probe_http2_host = self._tunnel_host - probe_http2_port = self._tunnel_port - else: - probe_http2_host = self.host - probe_http2_port = self.port - - # Check if the target origin supports HTTP/2. - # If the value comes back as 'None' it means that the current thread - # is probing for HTTP/2 support. Otherwise, we're waiting for another - # probe to complete, or we get a value right away. - target_supports_http2: bool | None - if "h2" in ssl_.ALPN_PROTOCOLS: - target_supports_http2 = http2_probe.acquire_and_get( - host=probe_http2_host, port=probe_http2_port - ) - else: - # If HTTP/2 isn't going to be offered it doesn't matter if - # the target supports HTTP/2. Don't want to make a probe. - target_supports_http2 = False - - if self._connect_callback is not None: - self._connect_callback( - "before connect", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - try: - sock: socket.socket | ssl.SSLSocket - self.sock = sock = self._new_conn() - server_hostname: str = self.host - tls_in_tls = False - - # Do we need to establish a tunnel? - if self.proxy_is_tunneling: - # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. - if self._tunnel_scheme == "https": - # _connect_tls_proxy will verify and assign proxy_is_verified - self.sock = sock = self._connect_tls_proxy(self.host, sock) - tls_in_tls = True - elif self._tunnel_scheme == "http": - self.proxy_is_verified = False - - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - self._tunnel() - # Override the host with the one we're requesting data from. - server_hostname = typing.cast(str, self._tunnel_host) - - if self.server_hostname is not None: - server_hostname = self.server_hostname - - is_time_off = datetime.date.today() < RECENT_DATE - if is_time_off: - warnings.warn( - ( - f"System time is way off (before {RECENT_DATE}). This will probably " - "lead to SSL verification errors" - ), - SystemTimeWarning, - ) - - # Remove trailing '.' from fqdn hostnames to allow certificate validation - server_hostname_rm_dot = server_hostname.rstrip(".") - - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock=sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - server_hostname=server_hostname_rm_dot, - ssl_context=self.ssl_context, - tls_in_tls=tls_in_tls, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ) - self.sock = sock_and_verified.socket - - # If an error occurs during connection/handshake we may need to release - # our lock so another connection can probe the origin. - except BaseException: - if self._connect_callback is not None: - self._connect_callback( - "after connect failure", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - if target_supports_http2 is None: - http2_probe.set_and_release( - host=probe_http2_host, port=probe_http2_port, supports_http2=None - ) - raise - - # If this connection doesn't know if the origin supports HTTP/2 - # we report back to the HTTP/2 probe our result. - if target_supports_http2 is None: - supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" - http2_probe.set_and_release( - host=probe_http2_host, - port=probe_http2_port, - supports_http2=supports_http2, - ) - - # Forwarding proxies can never have a verified target since - # the proxy is the one doing the verification. Should instead - # use a CONNECT tunnel in order to verify the target. - # See: https://github.com/urllib3/urllib3/issues/3267. - if self.proxy_is_forwarding: - self.is_verified = False - else: - self.is_verified = sock_and_verified.is_verified - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - # Set `self.proxy_is_verified` unless it's already set while - # establishing a tunnel. - if self._has_connected_to_proxy and self.proxy_is_verified is None: - self.proxy_is_verified = sock_and_verified.is_verified - - def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: - """ - Establish a TLS connection to the proxy using the provided SSL context. - """ - # `_connect_tls_proxy` is called when self._tunnel_host is truthy. - proxy_config = typing.cast(ProxyConfig, self.proxy_config) - ssl_context = proxy_config.ssl_context - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - server_hostname=hostname, - ssl_context=ssl_context, - assert_hostname=proxy_config.assert_hostname, - assert_fingerprint=proxy_config.assert_fingerprint, - # Features that aren't implemented for proxies yet: - cert_file=None, - key_file=None, - key_password=None, - tls_in_tls=False, - ) - self.proxy_is_verified = sock_and_verified.is_verified - return sock_and_verified.socket # type: ignore[return-value] - - -class _WrappedAndVerifiedSocket(typing.NamedTuple): - """ - Wrapped socket and whether the connection is - verified after the TLS handshake - """ - - socket: ssl.SSLSocket | SSLTransport - is_verified: bool - - -def _ssl_wrap_socket_and_match_hostname( - sock: socket.socket, - *, - cert_reqs: None | str | int, - ssl_version: None | str | int, - ssl_minimum_version: int | None, - ssl_maximum_version: int | None, - cert_file: str | None, - key_file: str | None, - key_password: str | None, - ca_certs: str | None, - ca_cert_dir: str | None, - ca_cert_data: None | str | bytes, - assert_hostname: None | str | typing.Literal[False], - assert_fingerprint: str | None, - server_hostname: str | None, - ssl_context: ssl.SSLContext | None, - tls_in_tls: bool = False, -) -> _WrappedAndVerifiedSocket: - """Logic for constructing an SSLContext from all TLS parameters, passing - that down into ssl_wrap_socket, and then doing certificate verification - either via hostname or fingerprint. This function exists to guarantee - that both proxies and targets have the same behavior when connecting via TLS. - """ - default_ssl_context = False - if ssl_context is None: - default_ssl_context = True - context = create_urllib3_context( - ssl_version=resolve_ssl_version(ssl_version), - ssl_minimum_version=ssl_minimum_version, - ssl_maximum_version=ssl_maximum_version, - cert_reqs=resolve_cert_reqs(cert_reqs), - ) - else: - context = ssl_context - - context.verify_mode = resolve_cert_reqs(cert_reqs) - - # In some cases, we want to verify hostnames ourselves - if ( - # `ssl` can't verify fingerprints or alternate hostnames - assert_fingerprint - or assert_hostname - # assert_hostname can be set to False to disable hostname checking - or assert_hostname is False - # We still support OpenSSL 1.0.2, which prevents us from verifying - # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 - or ssl_.IS_PYOPENSSL - or not ssl_.HAS_NEVER_CHECK_COMMON_NAME - ): - context.check_hostname = False - - # Try to load OS default certs if none are given. We need to do the hasattr() check - # for custom pyOpenSSL SSLContext objects because they don't support - # load_default_certs(). - if ( - not ca_certs - and not ca_cert_dir - and not ca_cert_data - and default_ssl_context - and hasattr(context, "load_default_certs") - ): - context.load_default_certs() - - # Ensure that IPv6 addresses are in the proper format and don't have a - # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses - # and interprets them as DNS hostnames. - if server_hostname is not None: - normalized = server_hostname.strip("[]") - if "%" in normalized: - normalized = normalized[: normalized.rfind("%")] - if is_ipaddress(normalized): - server_hostname = normalized - - ssl_sock = ssl_wrap_socket( - sock=sock, - keyfile=key_file, - certfile=cert_file, - key_password=key_password, - ca_certs=ca_certs, - ca_cert_dir=ca_cert_dir, - ca_cert_data=ca_cert_data, - server_hostname=server_hostname, - ssl_context=context, - tls_in_tls=tls_in_tls, - ) - - try: - if assert_fingerprint: - _assert_fingerprint( - ssl_sock.getpeercert(binary_form=True), assert_fingerprint - ) - elif ( - context.verify_mode != ssl.CERT_NONE - and not context.check_hostname - and assert_hostname is not False - ): - cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] - - # Need to signal to our match_hostname whether to use 'commonName' or not. - # If we're using our own constructed SSLContext we explicitly set 'False' - # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. - if default_ssl_context: - hostname_checks_common_name = False - else: - hostname_checks_common_name = ( - getattr(context, "hostname_checks_common_name", False) or False - ) - - _match_hostname( - cert, - assert_hostname or server_hostname, # type: ignore[arg-type] - hostname_checks_common_name, - ) - - return _WrappedAndVerifiedSocket( - socket=ssl_sock, - is_verified=context.verify_mode == ssl.CERT_REQUIRED - or bool(assert_fingerprint), - ) - except BaseException: - ssl_sock.close() - raise - - -def _match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - asserted_hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - # Our upstream implementation of ssl.match_hostname() - # only applies this normalization to IP addresses so it doesn't - # match DNS SANs so we do the same thing! - stripped_hostname = asserted_hostname.strip("[]") - if is_ipaddress(stripped_hostname): - asserted_hostname = stripped_hostname - - try: - match_hostname(cert, asserted_hostname, hostname_checks_common_name) - except CertificateError as e: - log.warning( - "Certificate did not match expected hostname: %s. Certificate: %s", - asserted_hostname, - cert, - ) - # Add cert to exception and reraise so client code can inspect - # the cert when catching the exception, if they want to - e._peer_cert = cert # type: ignore[attr-defined] - raise - - -def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: - # Look for the phrase 'wrong version number', if found - # then we should warn the user that we're very sure that - # this proxy is HTTP-only and they have a configuration issue. - error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) - is_likely_http_proxy = ( - "wrong version number" in error_normalized - or "unknown protocol" in error_normalized - or "record layer failure" in error_normalized - ) - http_proxy_warning = ( - ". Your proxy appears to only use HTTP and not HTTPS, " - "try changing your proxy URL to be HTTP. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#https-proxy-error-http-proxy" - ) - new_err = ProxyError( - f"Unable to connect to proxy" - f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", - err, - ) - new_err.__cause__ = err - return new_err - - -def _get_default_user_agent() -> str: - return f"python-urllib3/{__version__}" - - -class DummyConnection: - """Used to detect a failed ConnectionCls import.""" - - -if not ssl: - HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 - - -VerifiedHTTPSConnection = HTTPSConnection - - -def _url_from_connection( - conn: HTTPConnection | HTTPSConnection, path: str | None = None -) -> str: - """Returns the URL from a given connection. This is mainly used for testing and logging.""" - - scheme = "https" if isinstance(conn, HTTPSConnection) else "http" - - return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py deleted file mode 100644 index 70fbc5e725..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/connectionpool.py +++ /dev/null @@ -1,1191 +0,0 @@ -from __future__ import annotations - -import errno -import logging -import queue -import sys -import typing -import warnings -import weakref -from socket import timeout as SocketTimeout -from types import TracebackType - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._request_methods import RequestMethods -from .connection import ( - BaseSSLError, - BrokenPipeError, - DummyConnection, - HTTPConnection, - HTTPException, - HTTPSConnection, - ProxyConfig, - _wrap_proxy_error, -) -from .connection import port_by_scheme as port_by_scheme -from .exceptions import ( - ClosedPoolError, - EmptyPoolError, - FullPoolError, - HostChangedError, - InsecureRequestWarning, - LocationValueError, - MaxRetryError, - NewConnectionError, - ProtocolError, - ProxyError, - ReadTimeoutError, - SSLError, - TimeoutError, -) -from .response import BaseHTTPResponse -from .util.connection import is_connection_dropped -from .util.proxy import connection_requires_http_tunnel -from .util.request import _TYPE_BODY_POSITION, set_file_position -from .util.retry import Retry -from .util.ssl_match_hostname import CertificateError -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout -from .util.url import Url, _encode_target -from .util.url import _normalize_host as normalize_host -from .util.url import parse_url -from .util.util import to_str - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - - from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - -_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] - - -# Pool objects -class ConnectionPool: - """ - Base class for all connection pools, such as - :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. - - .. note:: - ConnectionPool.urlopen() does not normalize or percent-encode target URIs - which is useful if your target server doesn't support percent-encoded - target URIs. - """ - - scheme: str | None = None - QueueCls = queue.LifoQueue - - def __init__(self, host: str, port: int | None = None) -> None: - if not host: - raise LocationValueError("No host specified.") - - self.host = _normalize_host(host, scheme=self.scheme) - self.port = port - - # This property uses 'normalize_host()' (not '_normalize_host()') - # to avoid removing square braces around IPv6 addresses. - # This value is sent to `HTTPConnection.set_tunnel()` if called - # because square braces are required for HTTP CONNECT tunneling. - self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.close() - # Return False to re-raise any potential exceptions - return False - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - - -# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 -_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} - - -class HTTPConnectionPool(ConnectionPool, RequestMethods): - """ - Thread-safe connection pool for one host. - - :param host: - Host used for this HTTP Connection (e.g. "localhost"), passed into - :class:`http.client.HTTPConnection`. - - :param port: - Port used for this HTTP Connection (None is equivalent to 80), passed - into :class:`http.client.HTTPConnection`. - - :param timeout: - Socket timeout in seconds for each individual connection. This can - be a float or integer, which sets the timeout for the HTTP request, - or an instance of :class:`urllib3.util.Timeout` which gives you more - fine-grained control over request timeouts. After the constructor has - been parsed, this is always a `urllib3.util.Timeout` object. - - :param maxsize: - Number of connections to save that can be reused. More than 1 is useful - in multithreaded situations. If ``block`` is set to False, more - connections will be created but they will not be saved once they've - been used. - - :param block: - If set to True, no more than ``maxsize`` connections will be used at - a time. When no free connections are available, the call will block - until a connection has been released. This is a useful side effect for - particular multithreaded situations where one does not want to use more - than maxsize connections per host to prevent flooding. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param retries: - Retry configuration to use by default with requests in this pool. - - :param _proxy: - Parsed proxy URL, should not be used directly, instead, see - :class:`urllib3.ProxyManager` - - :param _proxy_headers: - A dictionary with proxy headers, should not be used directly, - instead, see :class:`urllib3.ProxyManager` - - :param \\**conn_kw: - Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, - :class:`urllib3.connection.HTTPSConnection` instances. - """ - - scheme = "http" - ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - _proxy_config: ProxyConfig | None = None, - **conn_kw: typing.Any, - ): - ConnectionPool.__init__(self, host, port) - RequestMethods.__init__(self, headers) - - if not isinstance(timeout, Timeout): - timeout = Timeout.from_float(timeout) - - if retries is None: - retries = Retry.DEFAULT - - self.timeout = timeout - self.retries = retries - - self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) - self.block = block - - self.proxy = _proxy - self.proxy_headers = _proxy_headers or {} - self.proxy_config = _proxy_config - - # Fill the queue up so that doing get() on it will block properly - for _ in range(maxsize): - self.pool.put(None) - - # These are mostly for testing and debugging purposes. - self.num_connections = 0 - self.num_requests = 0 - self.conn_kw = conn_kw - - if self.proxy: - # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. - # Defaulting `socket_options` to an empty list avoids it defaulting to - # ``HTTPConnection.default_socket_options``. - self.conn_kw.setdefault("socket_options", []) - - self.conn_kw["proxy"] = self.proxy - self.conn_kw["proxy_config"] = self.proxy_config - - # Do not pass 'self' as callback to 'finalize'. - # Then the 'finalize' would keep an endless living (leak) to self. - # By just passing a reference to the pool allows the garbage collector - # to free self if nobody else has a reference to it. - pool = self.pool - - # Close all the HTTPConnections in the pool before the - # HTTPConnectionPool object is garbage collected. - weakref.finalize(self, _close_pool_connections, pool) - - def _new_conn(self) -> BaseHTTPConnection: - """ - Return a fresh :class:`HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTP connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "80", - ) - - conn = self.ConnectionCls( - host=self.host, - port=self.port, - timeout=self.timeout.connect_timeout, - **self.conn_kw, - ) - return conn - - def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: - """ - Get a connection. Will return a pooled connection if one is available. - - If no connections are available and :prop:`.block` is ``False``, then a - fresh connection is returned. - - :param timeout: - Seconds to wait before giving up and raising - :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and - :prop:`.block` is ``True``. - """ - conn = None - - if self.pool is None: - raise ClosedPoolError(self, "Pool is closed.") - - try: - conn = self.pool.get(block=self.block, timeout=timeout) - - except AttributeError: # self.pool is None - raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: - - except queue.Empty: - if self.block: - raise EmptyPoolError( - self, - "Pool is empty and a new connection can't be opened due to blocking mode.", - ) from None - pass # Oh well, we'll create a new connection then - - # If this is a persistent connection, check if it got disconnected - if conn and is_connection_dropped(conn): - log.debug("Resetting dropped connection: %s", self.host) - conn.close() - - return conn or self._new_conn() - - def _put_conn(self, conn: BaseHTTPConnection | None) -> None: - """ - Put a connection back into the pool. - - :param conn: - Connection object for the current host and port as returned by - :meth:`._new_conn` or :meth:`._get_conn`. - - If the pool is already full, the connection is closed and discarded - because we exceeded maxsize. If connections are discarded frequently, - then maxsize should be increased. - - If the pool is closed, then the connection will be closed and discarded. - """ - if self.pool is not None: - try: - self.pool.put(conn, block=False) - return # Everything is dandy, done. - except AttributeError: - # self.pool is None. - pass - except queue.Full: - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - if self.block: - # This should never happen if you got the conn from self._get_conn - raise FullPoolError( - self, - "Pool reached maximum size and no more connections are allowed.", - ) from None - - log.warning( - "Connection pool is full, discarding connection: %s. Connection pool size: %s", - self.host, - self.pool.qsize(), - ) - - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - - def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: - # Nothing to do for HTTP connections. - pass - - def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: - """Helper that always returns a :class:`urllib3.util.Timeout`""" - if timeout is _DEFAULT_TIMEOUT: - return self.timeout.clone() - - if isinstance(timeout, Timeout): - return timeout.clone() - else: - # User passed us an int/float. This is for backwards compatibility, - # can be removed later - return Timeout.from_float(timeout) - - def _raise_timeout( - self, - err: BaseSSLError | OSError | SocketTimeout, - url: str, - timeout_value: _TYPE_TIMEOUT | None, - ) -> None: - """Is the error actually a timeout? Will raise a ReadTimeout or pass""" - - if isinstance(err, SocketTimeout): - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - # See the above comment about EAGAIN in Python 3. - if hasattr(err, "errno") and err.errno in _blocking_errnos: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - def _make_request( - self, - conn: BaseHTTPConnection, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | None = None, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - chunked: bool = False, - response_conn: BaseHTTPConnection | None = None, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> BaseHTTPResponse: - """ - Perform a request on a given urllib connection object taken from our - pool. - - :param conn: - a connection from one of our connection pools - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - Pass ``None`` to retry until you receive a response. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param response_conn: - Set this to ``None`` if you will handle releasing the connection or - set the connection to have the response release it. - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - self.num_requests += 1 - - timeout_obj = self._get_timeout(timeout) - timeout_obj.start_connect() - conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) - - try: - # Trigger any extra validation we need to do. - try: - self._validate_conn(conn) - except (SocketTimeout, BaseSSLError) as e: - self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) - raise - - # _validate_conn() starts the connection to an HTTPS proxy - # so we need to wrap errors with 'ProxyError' here too. - except ( - OSError, - NewConnectionError, - TimeoutError, - BaseSSLError, - CertificateError, - SSLError, - ) as e: - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - # If the connection didn't successfully connect to it's proxy - # then there - if isinstance( - new_e, (OSError, NewConnectionError, TimeoutError, SSLError) - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - raise new_e - - # conn.request() calls http.client.*.request, not the method in - # urllib3.request. It also calls makefile (recv) on the socket. - try: - conn.request( - method, - url, - body=body, - headers=headers, - chunked=chunked, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - # We are swallowing BrokenPipeError (errno.EPIPE) since the server is - # legitimately able to close the connection after sending a valid response. - # With this behaviour, the received response is still readable. - except BrokenPipeError: - pass - except OSError as e: - # MacOS/Linux - # EPROTOTYPE and ECONNRESET are needed on macOS - # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ - # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. - if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: - raise - - # Reset the timeout for the recv() on the socket - read_timeout = timeout_obj.read_timeout - - if not conn.is_closed: - # In Python 3 socket.py will catch EAGAIN and return None when you - # try and read into the file pointer created by http.client, which - # instead raises a BadStatusLine exception. Instead of catching - # the exception and assuming all BadStatusLine exceptions are read - # timeouts, check for a zero timeout before making the request. - if read_timeout == 0: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={read_timeout})" - ) - conn.timeout = read_timeout - - # Receive the response from the server - try: - response = conn.getresponse() - except (BaseSSLError, OSError) as e: - self._raise_timeout(err=e, url=url, timeout_value=read_timeout) - raise - - # Set properties that are used by the pooling layer. - response.retries = retries - response._connection = response_conn # type: ignore[attr-defined] - response._pool = self # type: ignore[attr-defined] - - log.debug( - '%s://%s:%s "%s %s %s" %s %s', - self.scheme, - self.host, - self.port, - method, - url, - response.version_string, - response.status, - response.length_remaining, - ) - - return response - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - if self.pool is None: - return - # Disable access to the pool - old_pool, self.pool = self.pool, None - - # Close all the HTTPConnections in the pool. - _close_pool_connections(old_pool) - - def is_same_host(self, url: str) -> bool: - """ - Check if the given ``url`` is a member of the same host as this - connection pool. - """ - if url.startswith("/"): - return True - - # TODO: Add optional support for socket.gethostbyname checking. - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - if host is not None: - host = _normalize_host(host, scheme=scheme) - - # Use explicit default port for comparison when none is given - if self.port and not port: - port = port_by_scheme.get(scheme) - elif not self.port and port == port_by_scheme.get(scheme): - port = None - - return (scheme, host, port) == (self.scheme, self.host, self.port) - - def urlopen( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - redirect: bool = True, - assert_same_host: bool = True, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - pool_timeout: int | None = None, - release_conn: bool | None = None, - chunked: bool = False, - body_pos: _TYPE_BODY_POSITION | None = None, - preload_content: bool = True, - decode_content: bool = True, - **response_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Get a connection from the pool and perform an HTTP request. This is the - lowest level call for making a request, so you'll need to specify all - the raw details. - - .. note:: - - More commonly, it's appropriate to use a convenience method - such as :meth:`request`. - - .. note:: - - `release_conn` will only behave as expected if - `preload_content=False` because we want to make - `preload_content=False` the default behaviour someday soon without - breaking backwards compatibility. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param assert_same_host: - If ``True``, will make sure that the host of the pool requests is - consistent else will raise HostChangedError. When ``False``, you can - use the pool on an HTTP proxy and request foreign hosts. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param pool_timeout: - If set and the pool is set to block=True, then this method will - block for ``pool_timeout`` seconds and raise EmptyPoolError if no - connection is available within the time period. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param release_conn: - If False, then the urlopen call will not release the connection - back into the pool once a response is received (but will release if - you read the entire contents of the response such as when - `preload_content=True`). This is useful if you're not preloading - the response's content immediately. You will need to call - ``r.release_conn()`` on the response ``r`` to return the connection - back into the pool. If None, it takes the value of ``preload_content`` - which defaults to ``True``. - - :param bool chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param int body_pos: - Position to seek to in file-like body in the event of a retry or - redirect. Typically this won't need to be set because urllib3 will - auto-populate the value when needed. - """ - # Ensure that the URL we're connecting to is properly encoded - if url.startswith("/"): - # URLs starting with / are inherently schemeless. - url = to_str(_encode_target(url)) - destination_scheme = None - else: - parsed_url = parse_url(url) - destination_scheme = parsed_url.scheme - url = to_str(parsed_url.url) - - if headers is None: - headers = self.headers - - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect, default=self.retries) - - if release_conn is None: - release_conn = preload_content - - # Check host - if assert_same_host and not self.is_same_host(url): - raise HostChangedError(self, url, retries) - - conn = None - - # Track whether `conn` needs to be released before - # returning/raising/recursing. Update this variable if necessary, and - # leave `release_conn` constant throughout the function. That way, if - # the function recurses, the original value of `release_conn` will be - # passed down into the recursive call, and its value will be respected. - # - # See issue #651 [1] for details. - # - # [1] - release_this_conn = release_conn - - http_tunnel_required = connection_requires_http_tunnel( - self.proxy, self.proxy_config, destination_scheme - ) - - # Merge the proxy headers. Only done when not using HTTP CONNECT. We - # have to copy the headers dict so we can safely change it without those - # changes being reflected in anyone else's copy. - if not http_tunnel_required: - headers = headers.copy() # type: ignore[attr-defined] - headers.update(self.proxy_headers) # type: ignore[union-attr] - - # Must keep the exception bound to a separate variable or else Python 3 - # complains about UnboundLocalError. - err = None - - # Keep track of whether we cleanly exited the except block. This - # ensures we do proper cleanup in finally. - clean_exit = False - - # Rewind body position, if needed. Record current position - # for future rewinds in the event of a redirect/retry. - body_pos = set_file_position(body, body_pos) - - try: - # Request a connection from the queue. - timeout_obj = self._get_timeout(timeout) - conn = self._get_conn(timeout=pool_timeout) - - conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] - - # Is this a closed/new connection that requires CONNECT tunnelling? - if self.proxy is not None and http_tunnel_required and conn.is_closed: - try: - self._prepare_proxy(conn) - except (BaseSSLError, OSError, SocketTimeout) as e: - self._raise_timeout( - err=e, url=self.proxy.url, timeout_value=conn.timeout - ) - raise - - # If we're going to release the connection in ``finally:``, then - # the response doesn't need to know about the connection. Otherwise - # it will also try to release it and we'll have a double-release - # mess. - response_conn = conn if not release_conn else None - - # Make the request on the HTTPConnection object - response = self._make_request( - conn, - method, - url, - timeout=timeout_obj, - body=body, - headers=headers, - chunked=chunked, - retries=retries, - response_conn=response_conn, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Everything went great! - clean_exit = True - - except EmptyPoolError: - # Didn't get a connection from the pool, no need to clean up - clean_exit = True - release_this_conn = False - raise - - except ( - TimeoutError, - HTTPException, - OSError, - ProtocolError, - BaseSSLError, - SSLError, - CertificateError, - ProxyError, - ) as e: - # Discard the connection for these exceptions. It will be - # replaced during the next _get_conn() call. - clean_exit = False - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - if isinstance( - new_e, - ( - OSError, - NewConnectionError, - TimeoutError, - SSLError, - HTTPException, - ), - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - elif isinstance(new_e, (OSError, HTTPException)): - new_e = ProtocolError("Connection aborted.", new_e) - - retries = retries.increment( - method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] - ) - retries.sleep() - - # Keep track of the error for the retry warning. - err = e - - finally: - if not clean_exit: - # We hit some kind of exception, handled or otherwise. We need - # to throw the connection away unless explicitly told not to. - # Close the connection, set the variable to None, and make sure - # we put the None back in the pool to avoid leaking it. - if conn: - conn.close() - conn = None - release_this_conn = True - - if release_this_conn: - # Put the connection back to be reused. If the connection is - # expired then it will be None, which will get replaced with a - # fresh connection during _get_conn. - self._put_conn(conn) - - if not conn: - # Try again - log.warning( - "Retrying (%r) after connection broken by '%r': %s", retries, err, url - ) - return self.urlopen( - method, - url, - body, - headers, - retries, - redirect, - assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Handle redirect? - redirect_location = redirect and response.get_redirect_location() - if redirect_location: - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - body = None - headers = HTTPHeaderDict(headers)._prepare_for_method_change() - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # self.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not self.is_same_host( - redirect_location - ): - new_headers = headers.copy() # type: ignore[union-attr] - for header in headers: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - headers = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep_for_retry(response) - log.debug("Redirecting %s -> %s", url, redirect_location) - return self.urlopen( - method, - redirect_location, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Check if we should retry the HTTP response. - has_retry_after = bool(response.headers.get("Retry-After")) - if retries.is_retry(method, response.status, has_retry_after): - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_status: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep(response) - log.debug("Retry: %s", url) - return self.urlopen( - method, - url, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - return response - - -class HTTPSConnectionPool(HTTPConnectionPool): - """ - Same as :class:`.HTTPConnectionPool`, but HTTPS. - - :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, - ``assert_hostname`` and ``host`` in this order to verify connections. - If ``assert_hostname`` is False, no verification is done. - - The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, - ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` - is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade - the connection socket into an SSL socket. - """ - - scheme = "https" - ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - ssl_version: int | str | None = None, - ssl_minimum_version: ssl.TLSVersion | None = None, - ssl_maximum_version: ssl.TLSVersion | None = None, - assert_hostname: str | typing.Literal[False] | None = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - **conn_kw: typing.Any, - ) -> None: - super().__init__( - host, - port, - timeout, - maxsize, - block, - headers, - retries, - _proxy, - _proxy_headers, - **conn_kw, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.ca_certs = ca_certs - self.ca_cert_dir = ca_cert_dir - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - - def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] - """Establishes a tunnel connection through HTTP CONNECT.""" - if self.proxy and self.proxy.scheme == "https": - tunnel_scheme = "https" - else: - tunnel_scheme = "http" - - conn.set_tunnel( - scheme=tunnel_scheme, - host=self._tunnel_host, - port=self.port, - headers=self.proxy_headers, - ) - conn.connect() - - def _new_conn(self) -> BaseHTTPSConnection: - """ - Return a fresh :class:`urllib3.connection.HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTPS connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "443", - ) - - if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] - raise ImportError( - "Can't connect to HTTPS URL because the SSL module is not available." - ) - - actual_host: str = self.host - actual_port = self.port - if self.proxy is not None and self.proxy.host is not None: - actual_host = self.proxy.host - actual_port = self.proxy.port - - return self.ConnectionCls( - host=actual_host, - port=actual_port, - timeout=self.timeout.connect_timeout, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - **self.conn_kw, - ) - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - super()._validate_conn(conn) - - # Force connect early to allow us to validate the connection. - if conn.is_closed: - conn.connect() - - # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 - if not conn.is_verified and not conn.proxy_is_verified: - warnings.warn( - ( - f"Unverified HTTPS request is being made to host '{conn.host}'. " - "Adding certificate verification is strongly advised. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#tls-warnings" - ), - InsecureRequestWarning, - ) - - -def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: - """ - Given a url, return an :class:`.ConnectionPool` instance of its host. - - This is a shortcut for not having to parse out the scheme, host, and port - of the url before creating an :class:`.ConnectionPool` instance. - - :param url: - Absolute URL string that must include the scheme. Port is optional. - - :param \\**kw: - Passes additional parameters to the constructor of the appropriate - :class:`.ConnectionPool`. Useful for specifying things like - timeout, maxsize, headers, etc. - - Example:: - - >>> conn = connection_from_url('http://google.com/') - >>> r = conn.request('GET', '/') - """ - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - port = port or port_by_scheme.get(scheme, 80) - if scheme == "https": - return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - else: - return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - """ - Normalize hosts for comparisons and use with sockets. - """ - - host = normalize_host(host, scheme) - - # httplib doesn't like it when we include brackets in IPv6 addresses - # Specifically, if we include brackets but also pass the port then - # httplib crazily doubles up the square brackets on the Host header. - # Instead, we need to make sure we never pass ``None`` as the port. - # However, for backward compatibility reasons we can't actually - # *assert* that. See http://bugs.python.org/issue28539 - if host and host.startswith("[") and host.endswith("]"): - host = host[1:-1] - return host - - -def _url_from_pool( - pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None -) -> str: - """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" - return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url - - -def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: - """Drains a queue of connections and closes each one.""" - try: - while True: - conn = pool.get(block=False) - if conn: - conn.close() - except queue.Empty: - pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py deleted file mode 100644 index e5b62b25e9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import urllib3.connection - -from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection - - -def inject_into_urllib3() -> None: - # override connection classes to use emscripten specific classes - # n.b. mypy complains about the overriding of classes below - # if it isn't ignored - HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection - HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection - urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] - urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] - urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py deleted file mode 100644 index 63f79dd3be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/connection.py +++ /dev/null @@ -1,260 +0,0 @@ -from __future__ import annotations - -import os -import typing - -# use http.client.HTTPException for consistency with non-emscripten -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady - -from ..._base_connection import _TYPE_BODY -from ...connection import HTTPConnection, ProxyConfig, port_by_scheme -from ...exceptions import TimeoutError -from ...response import BaseHTTPResponse -from ...util.connection import _TYPE_SOCKET_OPTIONS -from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from ...util.url import Url -from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request -from .request import EmscriptenRequest -from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - - -class EmscriptenHTTPConnection: - default_port: typing.ClassVar[int] = port_by_scheme["http"] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - timeout: None | (float) - - host: str - port: int - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool = False - proxy_is_verified: bool | None = None - - response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper - _response: EmscriptenResponse | None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - self.host = host - self.port = port - self.timeout = timeout if isinstance(timeout, float) else 0.0 - self.scheme = "http" - self._closed = True - self._response = None - # ignore these things because we don't - # have control over that stuff - self.proxy = None - self.proxy_config = None - self.blocksize = blocksize - self.source_address = None - self.socket_options = None - self.is_verified = False - - def set_tunnel( - self, - host: str, - port: int | None = 0, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - pass - - def connect(self) -> None: - pass - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - self._closed = False - if url.startswith("/"): - if self.port is not None: - port = f":{self.port}" - else: - port = "" - # no scheme / host / port included, make a full url - url = f"{self.scheme}://{self.host}{port}{url}" - request = EmscriptenRequest( - url=url, - method=method, - timeout=self.timeout if self.timeout else 0, - decode_content=decode_content, - ) - request.set_body(body) - if headers: - for k, v in headers.items(): - request.set_header(k, v) - self._response = None - try: - if not preload_content: - self._response = send_streaming_request(request) - if self._response is None: - self._response = send_request(request) - except _TimeoutError as e: - raise TimeoutError(e.message) from e - except _RequestError as e: - raise HTTPException(e.message) from e - - def getresponse(self) -> BaseHTTPResponse: - if self._response is not None: - return EmscriptenHttpResponseWrapper( - internal_response=self._response, - url=self._response.request.url, - connection=self, - ) - else: - raise ResponseNotReady() - - def close(self) -> None: - self._closed = True - self._response = None - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - return self._closed - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - return True - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - return False - - -class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): - default_port = port_by_scheme["https"] - # all this is basically ignored, as browser handles https - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - cert_file: str | None - key_file: str | None - key_password: str | None - ssl_context: typing.Any | None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None = None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: ( - None | _TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: typing.Any | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - self.scheme = "https" - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - self.cert_reqs = None - - # The browser will automatically verify all requests. - # We have no control over that setting. - self.is_verified = True - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - pass - - -# verify that this class implements BaseHTTP(s) connection correctly -if typing.TYPE_CHECKING: - _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) - _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js deleted file mode 100644 index faf141e1fa..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/emscripten_fetch_worker.js +++ /dev/null @@ -1,110 +0,0 @@ -let Status = { - SUCCESS_HEADER: -1, - SUCCESS_EOF: -2, - ERROR_TIMEOUT: -3, - ERROR_EXCEPTION: -4, -}; - -let connections = new Map(); -let nextConnectionID = 1; -const encoder = new TextEncoder(); - -self.addEventListener("message", async function (event) { - if (event.data.close) { - let connectionID = event.data.close; - connections.delete(connectionID); - return; - } else if (event.data.getMore) { - let connectionID = event.data.getMore; - let { curOffset, value, reader, intBuffer, byteBuffer } = - connections.get(connectionID); - // if we still have some in buffer, then just send it back straight away - if (!value || curOffset >= value.length) { - // read another buffer if required - try { - let readResponse = await reader.read(); - - if (readResponse.done) { - // read everything - clear connection and return - connections.delete(connectionID); - Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); - Atomics.notify(intBuffer, 0); - // finished reading successfully - // return from event handler - return; - } - curOffset = 0; - connections.get(connectionID).value = readResponse.value; - value = readResponse.value; - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } - - // send as much buffer as we can - let curLen = value.length - curOffset; - if (curLen > byteBuffer.length) { - curLen = byteBuffer.length; - } - byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); - - Atomics.store(intBuffer, 0, curLen); // store current length in bytes - Atomics.notify(intBuffer, 0); - curOffset += curLen; - connections.get(connectionID).curOffset = curOffset; - - return; - } else { - // start fetch - let connectionID = nextConnectionID; - nextConnectionID += 1; - const intBuffer = new Int32Array(event.data.buffer); - const byteBuffer = new Uint8Array(event.data.buffer, 8); - try { - const response = await fetch(event.data.url, event.data.fetchParams); - // return the headers first via textencoder - var headers = []; - for (const pair of response.headers.entries()) { - headers.push([pair[0], pair[1]]); - } - let headerObj = { - headers: headers, - status: response.status, - connectionID, - }; - const headerText = JSON.stringify(headerObj); - let headerBytes = encoder.encode(headerText); - let written = headerBytes.length; - byteBuffer.set(headerBytes); - intBuffer[1] = written; - // make a connection - connections.set(connectionID, { - reader: response.body.getReader(), - intBuffer: intBuffer, - byteBuffer: byteBuffer, - value: undefined, - curOffset: 0, - }); - // set header ready - Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); - Atomics.notify(intBuffer, 0); - // all fetching after this goes through a new postmessage call with getMore - // this allows for parallel requests - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } -}); -self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py deleted file mode 100644 index 612cfddc4c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/fetch.py +++ /dev/null @@ -1,726 +0,0 @@ -""" -Support for streaming http requests in emscripten. - -A few caveats - - -If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled -https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md -*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the -JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case -timeouts and streaming should just work. - -Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. - -This approach has several caveats: - -Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. -Streaming only works if you're running pyodide in a web worker. - -Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch -operation, so it requires that you have crossOriginIsolation enabled, by serving over https -(or from localhost) with the two headers below set: - - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp - -You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in -JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole -request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. - -Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once -control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. - -NB: in this code, there are a lot of JavaScript objects. They are named js_* -to make it clear what type of object they are. -""" - -from __future__ import annotations - -import io -import json -from email.parser import Parser -from importlib.resources import files -from typing import TYPE_CHECKING, Any - -import js # type: ignore[import-not-found] -from pyodide.ffi import ( # type: ignore[import-not-found] - JsArray, - JsException, - JsProxy, - to_js, -) - -if TYPE_CHECKING: - from typing_extensions import Buffer - -from .request import EmscriptenRequest -from .response import EmscriptenResponse - -""" -There are some headers that trigger unintended CORS preflight requests. -See also https://github.com/koenvo/pyodide-http/issues/22 -""" -HEADERS_TO_IGNORE = ("user-agent",) - -SUCCESS_HEADER = -1 -SUCCESS_EOF = -2 -ERROR_TIMEOUT = -3 -ERROR_EXCEPTION = -4 - - -class _RequestError(Exception): - def __init__( - self, - message: str | None = None, - *, - request: EmscriptenRequest | None = None, - response: EmscriptenResponse | None = None, - ): - self.request = request - self.response = response - self.message = message - super().__init__(self.message) - - -class _StreamingError(_RequestError): - pass - - -class _TimeoutError(_RequestError): - pass - - -def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: - return to_js(dict_val, dict_converter=js.Object.fromEntries) - - -class _ReadStream(io.RawIOBase): - def __init__( - self, - int_buffer: JsArray, - byte_buffer: JsArray, - timeout: float, - worker: JsProxy, - connection_id: int, - request: EmscriptenRequest, - ): - self.int_buffer = int_buffer - self.byte_buffer = byte_buffer - self.read_pos = 0 - self.read_len = 0 - self.connection_id = connection_id - self.worker = worker - self.timeout = int(1000 * timeout) if timeout > 0 else None - self.is_live = True - self._is_closed = False - self.request: EmscriptenRequest | None = request - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.int_buffer = None - self.byte_buffer = None - self._is_closed = True - self.request = None - if self.is_live: - self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) - self.is_live = False - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def readinto(self, byte_obj: Buffer) -> int: - if not self.int_buffer: - raise _StreamingError( - "No buffer for stream in _ReadStream.readinto", - request=self.request, - response=None, - ) - if self.read_len == 0: - # wait for the worker to send something - js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) - self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) - if ( - js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) - == "timed-out" - ): - raise _TimeoutError - data_len = self.int_buffer[0] - if data_len > 0: - self.read_len = data_len - self.read_pos = 0 - elif data_len == ERROR_EXCEPTION: - string_len = self.int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", - request=self.request, - response=None, - ) - else: - # EOF, free the buffers and return zero - # and free the request - self.is_live = False - self.close() - return 0 - # copy from int32array to python bytes - ret_length = min(self.read_len, len(memoryview(byte_obj))) - subarray = self.byte_buffer.subarray( - self.read_pos, self.read_pos + ret_length - ).to_py() - memoryview(byte_obj)[0:ret_length] = subarray - self.read_len -= ret_length - self.read_pos += ret_length - return ret_length - - -class _StreamingFetcher: - def __init__(self) -> None: - # make web-worker and data buffer on startup - self.streaming_ready = False - streaming_worker_code = ( - files(__package__) - .joinpath("emscripten_fetch_worker.js") - .read_text(encoding="utf-8") - ) - js_data_blob = js.Blob.new( - to_js([streaming_worker_code], create_pyproxies=False), - _obj_from_dict({"type": "application/javascript"}), - ) - - def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: - def onMsg(e: JsProxy) -> None: - self.streaming_ready = True - js_resolve_fn(e) - - def onErr(e: JsProxy) -> None: - js_reject_fn(e) # Defensive: never happens in ci - - self.js_worker.onmessage = onMsg - self.js_worker.onerror = onErr - - js_data_url = js.URL.createObjectURL(js_data_blob) - self.js_worker = js.globalThis.Worker.new(js_data_url) - self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) - - def send(self, request: EmscriptenRequest) -> EmscriptenResponse: - headers = { - k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE - } - - body = request.body - fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} - # start the request off in the worker - timeout = int(1000 * request.timeout) if request.timeout > 0 else None - js_shared_buffer = js.SharedArrayBuffer.new(1048576) - js_int_buffer = js.Int32Array.new(js_shared_buffer) - js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) - - js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) - js.Atomics.notify(js_int_buffer, 0) - js_absolute_url = js.URL.new(request.url, js.location).href - self.js_worker.postMessage( - _obj_from_dict( - { - "buffer": js_shared_buffer, - "url": js_absolute_url, - "fetchParams": fetch_data, - } - ) - ) - # wait for the worker to send something - js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) - if js_int_buffer[0] == ERROR_TIMEOUT: - raise _TimeoutError( - "Timeout connecting to streaming request", - request=request, - response=None, - ) - elif js_int_buffer[0] == SUCCESS_HEADER: - # got response - # header length is in second int of intBuffer - string_len = js_int_buffer[1] - # decode the rest to a JSON string - js_decoder = js.TextDecoder.new() - # this does a copy (the slice) because decode can't work on shared array - # for some silly reason - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - # get it as an object - response_obj = json.loads(json_str) - return EmscriptenResponse( - request=request, - status_code=response_obj["status"], - headers=response_obj["headers"], - body=_ReadStream( - js_int_buffer, - js_byte_buffer, - request.timeout, - self.js_worker, - response_obj["connectionID"], - request, - ), - ) - elif js_int_buffer[0] == ERROR_EXCEPTION: - string_len = js_int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", request=request, response=None - ) - else: - raise _StreamingError( - f"Unknown status from worker in fetch: {js_int_buffer[0]}", - request=request, - response=None, - ) - - -class _JSPIReadStream(io.RawIOBase): - """ - A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch - response. This requires support for WebAssembly JavaScript Promise Integration - in the containing browser, and for pyodide to be launched via runPythonAsync. - - :param js_read_stream: - The JavaScript stream reader - - :param timeout: - Timeout in seconds - - :param request: - The request we're handling - - :param response: - The response this stream relates to - - :param js_abort_controller: - A JavaScript AbortController object, used for timeouts - """ - - def __init__( - self, - js_read_stream: Any, - timeout: float, - request: EmscriptenRequest, - response: EmscriptenResponse, - js_abort_controller: Any, # JavaScript AbortController for timeouts - ): - self.js_read_stream = js_read_stream - self.timeout = timeout - self._is_closed = False - self._is_done = False - self.request: EmscriptenRequest | None = request - self.response: EmscriptenResponse | None = response - self.current_buffer = None - self.current_buffer_pos = 0 - self.js_abort_controller = js_abort_controller - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.js_read_stream.cancel() - self.js_read_stream = None - self._is_closed = True - self._is_done = True - self.request = None - self.response = None - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def _get_next_buffer(self) -> bool: - result_js = _run_sync_with_timeout( - self.js_read_stream.read(), - self.timeout, - self.js_abort_controller, - request=self.request, - response=self.response, - ) - if result_js.done: - self._is_done = True - return False - else: - self.current_buffer = result_js.value.to_py() - self.current_buffer_pos = 0 - return True - - def readinto(self, byte_obj: Buffer) -> int: - if self.current_buffer is None: - if not self._get_next_buffer() or self.current_buffer is None: - self.close() - return 0 - ret_length = min( - len(byte_obj), len(self.current_buffer) - self.current_buffer_pos - ) - byte_obj[0:ret_length] = self.current_buffer[ - self.current_buffer_pos : self.current_buffer_pos + ret_length - ] - self.current_buffer_pos += ret_length - if self.current_buffer_pos == len(self.current_buffer): - self.current_buffer = None - return ret_length - - -# check if we are in a worker or not -def is_in_browser_main_thread() -> bool: - return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window - - -def is_cross_origin_isolated() -> bool: - return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated - - -def is_in_node() -> bool: - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - and hasattr(js.process.release, "name") - and js.process.release.name == "node" - ) - - -def is_worker_available() -> bool: - return hasattr(js, "Worker") and hasattr(js, "Blob") - - -_fetcher: _StreamingFetcher | None = None - -if is_worker_available() and ( - (is_cross_origin_isolated() and not is_in_browser_main_thread()) - and (not is_in_node()) -): - _fetcher = _StreamingFetcher() -else: - _fetcher = None - - -NODE_JSPI_ERROR = ( - "urllib3 only works in Node.js with pyodide.runPythonAsync" - " and requires the flag --experimental-wasm-stack-switching in " - " versions of node <24." -) - - -def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: - if has_jspi(): - return send_jspi_request(request, True) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - - if _fetcher and streaming_ready(): - return _fetcher.send(request) - else: - _show_streaming_warning() - return None - - -_SHOWN_TIMEOUT_WARNING = False - - -def _show_timeout_warning() -> None: - global _SHOWN_TIMEOUT_WARNING - if not _SHOWN_TIMEOUT_WARNING: - _SHOWN_TIMEOUT_WARNING = True - message = "Warning: Timeout is not available on main browser thread" - js.console.warn(message) - - -_SHOWN_STREAMING_WARNING = False - - -def _show_streaming_warning() -> None: - global _SHOWN_STREAMING_WARNING - if not _SHOWN_STREAMING_WARNING: - _SHOWN_STREAMING_WARNING = True - message = "Can't stream HTTP requests because: \n" - if not is_cross_origin_isolated(): - message += " Page is not cross-origin isolated\n" - if is_in_browser_main_thread(): - message += " Python is running in main browser thread\n" - if not is_worker_available(): - message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in - if streaming_ready() is False: - message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch -is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" - from js import console - - console.warn(message) - - -def send_request(request: EmscriptenRequest) -> EmscriptenResponse: - if has_jspi(): - return send_jspi_request(request, False) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - try: - js_xhr = js.XMLHttpRequest.new() - - if not is_in_browser_main_thread(): - js_xhr.responseType = "arraybuffer" - if request.timeout: - js_xhr.timeout = int(request.timeout * 1000) - else: - js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") - if request.timeout: - # timeout isn't available on the main thread - show a warning in console - # if it is set - _show_timeout_warning() - - js_xhr.open(request.method, request.url, False) - for name, value in request.headers.items(): - if name.lower() not in HEADERS_TO_IGNORE: - js_xhr.setRequestHeader(name, value) - - js_xhr.send(to_js(request.body)) - - headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) - - if not is_in_browser_main_thread(): - body = js_xhr.response.to_py().tobytes() - else: - body = js_xhr.response.encode("ISO-8859-15") - return EmscriptenResponse( - status_code=js_xhr.status, headers=headers, body=body, request=request - ) - except JsException as err: - if err.name == "TimeoutError": - raise _TimeoutError(err.message, request=request) - elif err.name == "NetworkError": - raise _RequestError(err.message, request=request) - else: - # general http error - raise _RequestError(err.message, request=request) - - -def send_jspi_request( - request: EmscriptenRequest, streaming: bool -) -> EmscriptenResponse: - """ - Send a request using WebAssembly JavaScript Promise Integration - to wrap the asynchronous JavaScript fetch api (experimental). - - :param request: - Request to send - - :param streaming: - Whether to stream the response - - :return: The response object - :rtype: EmscriptenResponse - """ - timeout = request.timeout - js_abort_controller = js.AbortController.new() - headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} - req_body = request.body - fetch_data = { - "headers": headers, - "body": to_js(req_body), - "method": request.method, - "signal": js_abort_controller.signal, - } - # Node.js returns the whole response (unlike opaqueredirect in browsers), - # so urllib3 can set `redirect: manual` to control redirects itself. - # https://stackoverflow.com/a/78524615 - if _is_node_js(): - fetch_data["redirect"] = "manual" - # Call JavaScript fetch (async api, returns a promise) - fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) - # Now suspend WebAssembly until we resolve that promise - # or time out. - response_js = _run_sync_with_timeout( - fetcher_promise_js, - timeout, - js_abort_controller, - request=request, - response=None, - ) - headers = {} - header_iter = response_js.headers.entries() - while True: - iter_value_js = header_iter.next() - if getattr(iter_value_js, "done", False): - break - else: - headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) - status_code = response_js.status - body: bytes | io.RawIOBase = b"" - - response = EmscriptenResponse( - status_code=status_code, headers=headers, body=b"", request=request - ) - if streaming: - # get via inputstream - if response_js.body is not None: - # get a reader from the fetch response - body_stream_js = response_js.body.getReader() - body = _JSPIReadStream( - body_stream_js, timeout, request, response, js_abort_controller - ) - else: - # get directly via arraybuffer - # n.b. this is another async JavaScript call. - body = _run_sync_with_timeout( - response_js.arrayBuffer(), - timeout, - js_abort_controller, - request=request, - response=response, - ).to_py() - response.body = body - return response - - -def _run_sync_with_timeout( - promise: Any, - timeout: float, - js_abort_controller: Any, - request: EmscriptenRequest | None, - response: EmscriptenResponse | None, -) -> Any: - """ - Await a JavaScript promise synchronously with a timeout which is implemented - via the AbortController - - :param promise: - Javascript promise to await - - :param timeout: - Timeout in seconds - - :param js_abort_controller: - A JavaScript AbortController object, used on timeout - - :param request: - The request being handled - - :param response: - The response being handled (if it exists yet) - - :raises _TimeoutError: If the request times out - :raises _RequestError: If the request raises a JavaScript exception - - :return: The result of awaiting the promise. - """ - timer_id = None - if timeout > 0: - timer_id = js.setTimeout( - js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) - ) - try: - from pyodide.ffi import run_sync - - # run_sync here uses WebAssembly JavaScript Promise Integration to - # suspend python until the JavaScript promise resolves. - return run_sync(promise) - except JsException as err: - if err.name == "AbortError": - raise _TimeoutError( - message="Request timed out", request=request, response=response - ) - else: - raise _RequestError(message=err.message, request=request, response=response) - finally: - if timer_id is not None: - js.clearTimeout(timer_id) - - -def has_jspi() -> bool: - """ - Return true if jspi can be used. - - This requires both browser support and also WebAssembly - to be in the correct state - i.e. that the javascript - call into python was async not sync. - - :return: True if jspi can be used. - :rtype: bool - """ - try: - from pyodide.ffi import can_run_sync, run_sync # noqa: F401 - - return bool(can_run_sync()) - except ImportError: - return False - - -def _is_node_js() -> bool: - """ - Check if we are in Node.js. - - :return: True if we are in Node.js. - :rtype: bool - """ - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - # According to the Node.js documentation, the release name is always "node". - and js.process.release.name == "node" - ) - - -def streaming_ready() -> bool | None: - if _fetcher: - return _fetcher.streaming_ready - else: - return None # no fetcher, return None to signify that - - -async def wait_for_streaming_ready() -> bool: - if _fetcher: - await _fetcher.js_worker_ready_promise - return True - else: - return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py deleted file mode 100644 index e692e692bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/request.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from ..._base_connection import _TYPE_BODY - - -@dataclass -class EmscriptenRequest: - method: str - url: str - params: dict[str, str] | None = None - body: _TYPE_BODY | None = None - headers: dict[str, str] = field(default_factory=dict) - timeout: float = 0 - decode_content: bool = True - - def set_header(self, name: str, value: str) -> None: - self.headers[name.capitalize()] = value - - def set_body(self, body: _TYPE_BODY | None) -> None: - self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py deleted file mode 100644 index ec1e1dbe83..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/emscripten/response.py +++ /dev/null @@ -1,281 +0,0 @@ -from __future__ import annotations - -import json as _json -import logging -import typing -from contextlib import contextmanager -from dataclasses import dataclass -from http.client import HTTPException as HTTPException -from io import BytesIO, IOBase - -from ...exceptions import InvalidHeader, TimeoutError -from ...response import BaseHTTPResponse -from ...util.retry import Retry -from .request import EmscriptenRequest - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - - -@dataclass -class EmscriptenResponse: - status_code: int - headers: dict[str, str] - body: IOBase | bytes - request: EmscriptenRequest - - -class EmscriptenHttpResponseWrapper(BaseHTTPResponse): - def __init__( - self, - internal_response: EmscriptenResponse, - url: str | None = None, - connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, - ): - self._pool = None # set by pool class - self._body = None - self._uncached_read_occurred = False - self._response = internal_response - self._url = url - self._connection = connection - self._closed = False - super().__init__( - headers=internal_response.headers, - status=internal_response.status_code, - request_url=url, - version=0, - version_string="HTTP/?", - reason="", - decode_content=True, - ) - self.length_remaining = self._init_length(self._response.request.method) - self.length_is_certain = False - - @property - def url(self) -> str | None: - return self._url - - @url.setter - def url(self, url: str | None) -> None: - self._url = url - - @property - def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: - return self._connection - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - while True: - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - else: - break - - def _init_length(self, request_method: str | None) -> int | None: - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Check for responses that shouldn't include a body - if ( - self.status in (204, 304) - or 100 <= self.status < 200 - or request_method == "HEAD" - ): - length = 0 - - return length - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, # ignored because browser decodes always - cache_content: bool = False, - ) -> bytes: - if ( - self._closed - or self._response is None - or (isinstance(self._response.body, IOBase) and self._response.body.closed) - ): - return b"" - - with self._error_catcher(): - # body has been preloaded as a string by XmlHttpRequest - if not isinstance(self._response.body, IOBase): - self.length_remaining = len(self._response.body) - self.length_is_certain = True - # wrap body in IOStream - self._response.body = BytesIO(self._response.body) - if amt is not None and amt >= 0: - # don't cache partial content - cache_content = False - data = self._response.body.read(amt) - self._uncached_read_occurred = True - else: # read all we can (and cache it) - data = self._response.body.read() - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - self._uncached_read_occurred = True - if self.length_remaining is not None: - self.length_remaining = max(self.length_remaining - len(data), 0) - if len(data) == 0 or ( - self.length_is_certain and self.length_remaining == 0 - ): - # definitely finished reading, close response stream - self._response.body.close() - return typing.cast(bytes, data) - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Generator[bytes]: - # chunked is handled by browser - while True: - bytes = self.read(amt, decode_content) - if not bytes: - break - yield bytes - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - self.close() - - @property - def data(self) -> bytes: - if self._body: - return self._body - else: - return self.read(cache_content=True) - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - def close(self) -> None: - if not self._closed: - if isinstance(self._response.body, IOBase): - self._response.body.close() - if self._connection: - self._connection.close() - self._connection = None - self._closed = True - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch Emscripten specific exceptions thrown by fetch.py, - instead re-raising urllib3 variants, so that low-level exceptions - are not leaked in the high-level api. - - On exit, release the connection back to the pool. - """ - from .fetch import _RequestError, _TimeoutError # avoid circular import - - clean_exit = False - - try: - yield - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - except _TimeoutError as e: - raise TimeoutError(str(e)) - except _RequestError as e: - raise HTTPException(str(e)) - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now - if ( - isinstance(self._response.body, IOBase) - and not self._response.body.closed - ): - self._response.body.close() - # release the connection back to the pool - self.release_conn() - else: - # If we have read everything from the response stream, - # return the connection back to the pool. - if ( - isinstance(self._response.body, IOBase) - and self._response.body.closed - ): - self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py deleted file mode 100644 index f06b859992..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/pyopenssl.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Module for using pyOpenSSL as a TLS backend. This module was relevant before -the standard library ``ssl`` module supported SNI, but now that we've dropped -support for Python 2.7 all relevant Python versions support SNI so -**this module is no longer recommended**. - -This needs the following packages installed: - -* `pyOpenSSL`_ (tested with 19.0.0) -* `cryptography`_ (minimum 2.3, from pyopenssl) -* `idna`_ (minimum 2.1, from cryptography) - -However, pyOpenSSL depends on cryptography, so while we use all three directly here we -end up having relatively few packages required. - -You can install them with the following command: - -.. code-block:: bash - - $ python -m pip install pyopenssl cryptography idna - -To activate certificate checking, call -:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code -before you begin making HTTP requests. This can be done in a ``sitecustomize`` -module, or at any other time before your application begins using ``urllib3``, -like this: - -.. code-block:: python - - try: - import urllib3.contrib.pyopenssl - urllib3.contrib.pyopenssl.inject_into_urllib3() - except ImportError: - pass - -.. _pyopenssl: https://www.pyopenssl.org -.. _cryptography: https://cryptography.io -.. _idna: https://github.com/kjd/idna -""" - -from __future__ import annotations - -import OpenSSL.SSL # type: ignore[import-not-found] -from cryptography import x509 - -try: - from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] -except ImportError: - # UnsupportedExtension is gone in cryptography >= 2.1.0 - class UnsupportedExtension(Exception): # type: ignore[no-redef] - pass - - -import logging -import ssl -import typing -from io import BytesIO -from socket import socket as socket_cls - -from .. import util - -if typing.TYPE_CHECKING: - from OpenSSL.crypto import X509 # type: ignore[import-not-found] - - -__all__ = ["inject_into_urllib3", "extract_from_urllib3"] - -# Map from urllib3 to PyOpenSSL compatible parameter-values. -_openssl_versions: dict[int, int] = { - util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, -} - -if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD - -if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD - - -_stdlib_to_openssl_verify = { - ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, - ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, - ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER - + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, -} -_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} - -# The SSLvX values are the most likely to be missing in the future -# but we check them all just to be sure. -_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( - OpenSSL.SSL, "OP_NO_SSLv3", 0 -) -_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) -_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) -_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) -_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) - -_openssl_to_ssl_minimum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, - ssl.TLSVersion.TLSv1_3: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), - ssl.TLSVersion.MAXIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), -} -_openssl_to_ssl_maximum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 - | _OP_NO_TLSv1 - | _OP_NO_TLSv1_1 - | _OP_NO_TLSv1_2 - | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, -} - -# OpenSSL will only write 16K at a time -SSL_WRITE_BLOCKSIZE = 16384 - -orig_util_SSLContext = util.ssl_.SSLContext - - -log = logging.getLogger(__name__) - - -def inject_into_urllib3() -> None: - "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." - - _validate_dependencies_met() - - util.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.IS_PYOPENSSL = True - util.ssl_.IS_PYOPENSSL = True - - -def extract_from_urllib3() -> None: - "Undo monkey-patching by :func:`inject_into_urllib3`." - - util.SSLContext = orig_util_SSLContext - util.ssl_.SSLContext = orig_util_SSLContext - util.IS_PYOPENSSL = False - util.ssl_.IS_PYOPENSSL = False - - -def _validate_dependencies_met() -> None: - """ - Verifies that PyOpenSSL's package-level dependencies have been met. - Throws `ImportError` if they are not met. - """ - # Method added in `cryptography==1.1`; not available in older versions - from cryptography.x509.extensions import Extensions - - if getattr(Extensions, "get_extension_for_class", None) is None: - raise ImportError( - "'cryptography' module missing required functionality. " - "Try upgrading to v1.3.4 or newer." - ) - - # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 - # attribute is only present on those versions. - from OpenSSL.crypto import X509 - - x509 = X509() - if getattr(x509, "_x509", None) is None: - raise ImportError( - "'pyOpenSSL' module missing required functionality. " - "Try upgrading to v0.14 or newer." - ) - - -def _dnsname_to_stdlib(name: str) -> str | None: - """ - Converts a dNSName SubjectAlternativeName field to the form used by the - standard library on the given Python version. - - Cryptography produces a dNSName as a unicode string that was idna-decoded - from ASCII bytes. We need to idna-encode that string to get it back, and - then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib - uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). - - If the name cannot be idna-encoded then we return None signalling that - the name given should be skipped. - """ - - def idna_encode(name: str) -> bytes | None: - """ - Borrowed wholesale from the Python Cryptography Project. It turns out - that we can't just safely call `idna.encode`: it can explode for - wildcard names. This avoids that problem. - """ - import idna - - try: - for prefix in ["*.", "."]: - if name.startswith(prefix): - name = name[len(prefix) :] - return prefix.encode("ascii") + idna.encode(name) - return idna.encode(name) - except idna.core.IDNAError: - return None - - # Don't send IPv6 addresses through the IDNA encoder. - if ":" in name: - return name - - encoded_name = idna_encode(name) - if encoded_name is None: - return None - return encoded_name.decode("utf-8") - - -def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: - """ - Given an PyOpenSSL certificate, provides all the subject alternative names. - """ - cert = peer_cert.to_cryptography() - - # We want to find the SAN extension. Ask Cryptography to locate it (it's - # faster than looping in Python) - try: - ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value - except x509.ExtensionNotFound: - # No such extension, return the empty list. - return [] - except ( - x509.DuplicateExtension, - UnsupportedExtension, - x509.UnsupportedGeneralNameType, - UnicodeError, - ) as e: - # A problem has been found with the quality of the certificate. Assume - # no SAN field is present. - log.warning( - "A problem was encountered with the certificate that prevented " - "urllib3 from finding the SubjectAlternativeName field. This can " - "affect certificate validation. The error was %s", - e, - ) - return [] - - # We want to return dNSName and iPAddress fields. We need to cast the IPs - # back to strings because the match_hostname function wants them as - # strings. - # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 - # decoded. This is pretty frustrating, but that's what the standard library - # does with certificates, and so we need to attempt to do the same. - # We also want to skip over names which cannot be idna encoded. - names = [ - ("DNS", name) - for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) - if name is not None - ] - names.extend( - ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) - ) - - return names - - -class WrappedSocket: - """API-compatibility wrapper for Python OpenSSL's Connection-class.""" - - def __init__( - self, - connection: OpenSSL.SSL.Connection, - socket: socket_cls, - suppress_ragged_eofs: bool = True, - ) -> None: - self.connection = connection - self.socket = socket - self.suppress_ragged_eofs = suppress_ragged_eofs - self._io_refs = 0 - self._closed = False - - def fileno(self) -> int: - return self.socket.fileno() - - # Copy-pasted from Python 3.5 source code - def _decref_socketios(self) -> None: - if self._io_refs > 0: - self._io_refs -= 1 - if self._closed: - self.close() - - def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: - try: - data = self.connection.recv(*args, **kwargs) - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return b"" - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return b"" - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - else: - return data # type: ignore[no-any-return] - - def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: - try: - return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return 0 - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return 0 - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv_into(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - - def settimeout(self, timeout: float) -> None: - return self.socket.settimeout(timeout) - - def _send_until_done(self, data: bytes) -> int: - while True: - try: - return self.connection.send(data) # type: ignore[no-any-return] - except OpenSSL.SSL.WantWriteError as e: - if not util.wait_for_write(self.socket, self.socket.gettimeout()): - raise TimeoutError() from e - continue - except OpenSSL.SSL.SysCallError as e: - raise OSError(e.args[0], str(e)) from e - - def sendall(self, data: bytes) -> None: - total_sent = 0 - while total_sent < len(data): - sent = self._send_until_done( - data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] - ) - total_sent += sent - - def shutdown(self, how: int) -> None: - try: - self.connection.shutdown() - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"shutdown error: {e!r}") from e - - def close(self) -> None: - self._closed = True - if self._io_refs <= 0: - self._real_close() - - def _real_close(self) -> None: - try: - return self.connection.close() # type: ignore[no-any-return] - except OpenSSL.SSL.Error: - return - - def getpeercert( - self, binary_form: bool = False - ) -> dict[str, list[typing.Any]] | None: - x509 = self.connection.get_peer_certificate() - - if not x509: - return x509 # type: ignore[no-any-return] - - if binary_form: - return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] - - return { - "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] - "subjectAltName": get_subj_alt_name(x509), - } - - def version(self) -> str: - return self.connection.get_protocol_version_name() # type: ignore[no-any-return] - - def selected_alpn_protocol(self) -> str | None: - alpn_proto = self.connection.get_alpn_proto_negotiated() - return alpn_proto.decode() if alpn_proto else None - - -WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] - - -class PyOpenSSLContext: - """ - I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible - for translating the interface of the standard library ``SSLContext`` object - to calls into PyOpenSSL. - """ - - def __init__(self, protocol: int) -> None: - self.protocol = _openssl_versions[protocol] - self._ctx = OpenSSL.SSL.Context(self.protocol) - self._options = 0 - self.check_hostname = False - self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED - self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED - self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST - - @property - def options(self) -> int: - return self._options - - @options.setter - def options(self, value: int) -> None: - self._options = value - self._set_ctx_options() - - @property - def verify_flags(self) -> int: - return self._verify_flags - - @verify_flags.setter - def verify_flags(self, value: int) -> None: - self._verify_flags = value - self._ctx.get_cert_store().set_flags(self._verify_flags) - - @property - def verify_mode(self) -> int: - return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] - - @verify_mode.setter - def verify_mode(self, value: ssl.VerifyMode) -> None: - self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) - - def set_default_verify_paths(self) -> None: - self._ctx.set_default_verify_paths() - - def set_ciphers(self, ciphers: bytes | str) -> None: - if isinstance(ciphers, str): - ciphers = ciphers.encode("utf-8") - self._ctx.set_cipher_list(ciphers) - - def load_verify_locations( - self, - cafile: str | None = None, - capath: str | None = None, - cadata: bytes | None = None, - ) -> None: - if cafile is not None: - cafile = cafile.encode("utf-8") # type: ignore[assignment] - if capath is not None: - capath = capath.encode("utf-8") # type: ignore[assignment] - try: - self._ctx.load_verify_locations(cafile, capath) - if cadata is not None: - self._ctx.load_verify_locations(BytesIO(cadata)) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e - - def load_cert_chain( - self, - certfile: str, - keyfile: str | None = None, - password: str | None = None, - ) -> None: - try: - self._ctx.use_certificate_chain_file(certfile) - if password is not None: - if not isinstance(password, bytes): - password = password.encode("utf-8") # type: ignore[assignment] - self._ctx.set_passwd_cb(lambda *_: password) - self._ctx.use_privatekey_file(keyfile or certfile) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e - - def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: - protocols = [util.util.to_bytes(p, "ascii") for p in protocols] - return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] - - def wrap_socket( - self, - sock: socket_cls, - server_side: bool = False, - do_handshake_on_connect: bool = True, - suppress_ragged_eofs: bool = True, - server_hostname: bytes | str | None = None, - ) -> WrappedSocket: - cnx = OpenSSL.SSL.Connection(self._ctx, sock) - - # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 - if server_hostname and not util.ssl_.is_ipaddress(server_hostname): - if isinstance(server_hostname, str): - server_hostname = server_hostname.encode("utf-8") - cnx.set_tlsext_host_name(server_hostname) - - cnx.set_connect_state() - - while True: - try: - cnx.do_handshake() - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(sock, sock.gettimeout()): - raise TimeoutError("select timed out") from e - continue - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"bad handshake: {e!r}") from e - break - - return WrappedSocket(cnx, sock) - - def _set_ctx_options(self) -> None: - self._ctx.set_options( - self._options - | _openssl_to_ssl_minimum_version[self._minimum_version] - | _openssl_to_ssl_maximum_version[self._maximum_version] - ) - - @property - def minimum_version(self) -> int: - return self._minimum_version - - @minimum_version.setter - def minimum_version(self, minimum_version: int) -> None: - self._minimum_version = minimum_version - self._set_ctx_options() - - @property - def maximum_version(self) -> int: - return self._maximum_version - - @maximum_version.setter - def maximum_version(self, maximum_version: int) -> None: - self._maximum_version = maximum_version - self._set_ctx_options() - - -def _verify_callback( - cnx: OpenSSL.SSL.Connection, - x509: X509, - err_no: int, - err_depth: int, - return_code: int, -) -> bool: - return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py deleted file mode 100644 index d37da8fc20..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/contrib/socks.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -This module contains provisional support for SOCKS proxies from within -urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and -SOCKS5. To enable its functionality, either install PySocks or install this -module with the ``socks`` extra. - -The SOCKS implementation supports the full range of urllib3 features. It also -supports the following SOCKS features: - -- SOCKS4A (``proxy_url='socks4a://...``) -- SOCKS4 (``proxy_url='socks4://...``) -- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) -- SOCKS5 with local DNS (``proxy_url='socks5://...``) -- Usernames and passwords for the SOCKS proxy - -.. note:: - It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in - your ``proxy_url`` to ensure that DNS resolution is done from the remote - server instead of client-side when connecting to a domain name. - -SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 -supports IPv4, IPv6, and domain names. - -When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` -will be sent as the ``userid`` section of the SOCKS request: - -.. code-block:: python - - proxy_url="socks4a://@proxy-host" - -When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion -of the ``proxy_url`` will be sent as the username/password to authenticate -with the proxy: - -.. code-block:: python - - proxy_url="socks5h://:@proxy-host" - -""" - -from __future__ import annotations - -try: - import socks # type: ignore[import-untyped] -except ImportError: - import warnings - - from ..exceptions import DependencyWarning - - warnings.warn( - ( - "SOCKS support in urllib3 requires the installation of optional " - "dependencies: specifically, PySocks. For more information, see " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" - ), - DependencyWarning, - ) - raise - -import typing -from socket import timeout as SocketTimeout - -from ..connection import HTTPConnection, HTTPSConnection -from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from ..exceptions import ConnectTimeoutError, NewConnectionError -from ..poolmanager import PoolManager -from ..util.url import parse_url - -try: - import ssl -except ImportError: - ssl = None # type: ignore[assignment] - - -class _TYPE_SOCKS_OPTIONS(typing.TypedDict): - socks_version: int - proxy_host: str | None - proxy_port: str | None - username: str | None - password: str | None - rdns: bool - - -class SOCKSConnection(HTTPConnection): - """ - A plain-text HTTP connection that connects via a SOCKS proxy. - """ - - def __init__( - self, - _socks_options: _TYPE_SOCKS_OPTIONS, - *args: typing.Any, - **kwargs: typing.Any, - ) -> None: - self._socks_options = _socks_options - super().__init__(*args, **kwargs) - - def _new_conn(self) -> socks.socksocket: - """ - Establish a new connection via the SOCKS proxy. - """ - extra_kw: dict[str, typing.Any] = {} - if self.source_address: - extra_kw["source_address"] = self.source_address - - if self.socket_options: - extra_kw["socket_options"] = self.socket_options - - try: - conn = socks.create_connection( - (self.host, self.port), - proxy_type=self._socks_options["socks_version"], - proxy_addr=self._socks_options["proxy_host"], - proxy_port=self._socks_options["proxy_port"], - proxy_username=self._socks_options["username"], - proxy_password=self._socks_options["password"], - proxy_rdns=self._socks_options["rdns"], - timeout=self.timeout, - **extra_kw, - ) - - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except socks.ProxyError as e: - # This is fragile as hell, but it seems to be the only way to raise - # useful errors here. - if e.socket_err: - error = e.socket_err - if isinstance(error, SocketTimeout): - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - else: - # Adding `from e` messes with coverage somehow, so it's omitted. - # See #2386. - raise NewConnectionError( - self, f"Failed to establish a new connection: {error}" - ) - else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - except OSError as e: # Defensive: PySocks should catch all these. - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - return conn - - -# We don't need to duplicate the Verified/Unverified distinction from -# urllib3/connection.py here because the HTTPSConnection will already have been -# correctly set to either the Verified or Unverified form by that module. This -# means the SOCKSHTTPSConnection will automatically be the correct type. -class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): - pass - - -class SOCKSHTTPConnectionPool(HTTPConnectionPool): - ConnectionCls = SOCKSConnection - - -class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): - ConnectionCls = SOCKSHTTPSConnection - - -class SOCKSProxyManager(PoolManager): - """ - A version of the urllib3 ProxyManager that routes connections via the - defined SOCKS proxy. - """ - - pool_classes_by_scheme = { - "http": SOCKSHTTPConnectionPool, - "https": SOCKSHTTPSConnectionPool, - } - - def __init__( - self, - proxy_url: str, - username: str | None = None, - password: str | None = None, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ): - parsed = parse_url(proxy_url) - - if username is None and password is None and parsed.auth is not None: - split = parsed.auth.split(":") - if len(split) == 2: - username, password = split - if parsed.scheme == "socks5": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = False - elif parsed.scheme == "socks5h": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = True - elif parsed.scheme == "socks4": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = False - elif parsed.scheme == "socks4a": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = True - else: - raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") - - self.proxy_url = proxy_url - - socks_options = { - "socks_version": socks_version, - "proxy_host": parsed.host, - "proxy_port": parsed.port, - "username": username, - "password": password, - "rdns": rdns, - } - connection_pool_kw["_socks_options"] = socks_options - - super().__init__(num_pools, headers, **connection_pool_kw) - - self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py deleted file mode 100644 index 3d7e9b93d3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/exceptions.py +++ /dev/null @@ -1,335 +0,0 @@ -from __future__ import annotations - -import socket -import typing -import warnings -from email.errors import MessageDefect -from http.client import IncompleteRead as httplib_IncompleteRead - -if typing.TYPE_CHECKING: - from .connection import HTTPConnection - from .connectionpool import ConnectionPool - from .response import HTTPResponse - from .util.retry import Retry - -# Base Exceptions - - -class HTTPError(Exception): - """Base exception used by this module.""" - - -class HTTPWarning(Warning): - """Base warning used by this module.""" - - -_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] - - -class PoolError(HTTPError): - """Base exception for errors caused within a pool.""" - - def __init__(self, pool: ConnectionPool, message: str) -> None: - self.pool = pool - self._message = message - super().__init__(f"{pool}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - -class RequestError(PoolError): - """Base exception for PoolErrors that have associated URLs.""" - - def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: - self.url = url - super().__init__(pool, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self._message) - - -class SSLError(HTTPError): - """Raised when SSL certificate fails in an HTTPS connection.""" - - -class ProxyError(HTTPError): - """Raised when the connection to a proxy fails.""" - - # The original error is also available as __cause__. - original_error: Exception - - def __init__(self, message: str, error: Exception) -> None: - super().__init__(message, error) - self.original_error = error - - -class DecodeError(HTTPError): - """Raised when automatic decoding based on Content-Type fails.""" - - -class ProtocolError(HTTPError): - """Raised when something unexpected happens mid-request/response.""" - - -#: Renamed to ProtocolError but aliased for backwards compatibility. -ConnectionError = ProtocolError - - -# Leaf Exceptions - - -class MaxRetryError(RequestError): - """Raised when the maximum number of retries is exceeded. - - :param pool: The connection pool - :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` - :param str url: The requested Url - :param reason: The underlying error - :type reason: :class:`Exception` - - """ - - def __init__( - self, pool: ConnectionPool, url: str | None, reason: Exception | None = None - ) -> None: - self.reason = reason - - message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" - - super().__init__(pool, url, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self.reason) - - -class HostChangedError(RequestError): - """Raised when an existing pool gets a request for a foreign host.""" - - def __init__( - self, pool: ConnectionPool, url: str, retries: Retry | int = 3 - ) -> None: - message = f"Tried to open a foreign host with url: {url}" - super().__init__(pool, url, message) - self.retries = retries - - -class TimeoutStateError(HTTPError): - """Raised when passing an invalid state to a timeout""" - - -class TimeoutError(HTTPError): - """Raised when a socket timeout error occurs. - - Catching this error will catch both :exc:`ReadTimeoutErrors - ` and :exc:`ConnectTimeoutErrors `. - """ - - -class ReadTimeoutError(TimeoutError, RequestError): - """Raised when a socket timeout occurs while receiving data from a server""" - - -# This timeout error does not have a URL attached and needs to inherit from the -# base HTTPError -class ConnectTimeoutError(TimeoutError): - """Raised when a socket timeout occurs while connecting to a server""" - - -class NewConnectionError(ConnectTimeoutError, HTTPError): - """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" - - def __init__(self, conn: HTTPConnection, message: str) -> None: - self.conn = conn - self._message = message - super().__init__(f"{conn}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - @property - def pool(self) -> HTTPConnection: - warnings.warn( - "The 'pool' property is deprecated and will be removed " - "in urllib3 v3.0. Use 'conn' instead.", - FutureWarning, - stacklevel=2, - ) - - return self.conn - - -class NameResolutionError(NewConnectionError): - """Raised when host name resolution fails.""" - - def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): - message = f"Failed to resolve '{host}' ({reason})" - self._host = host - self._reason = reason - super().__init__(conn, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (self._host, None, self._reason) - - -class EmptyPoolError(PoolError): - """Raised when a pool runs out of connections and no more are allowed.""" - - -class FullPoolError(PoolError): - """Raised when we try to add a connection to a full pool in blocking mode.""" - - -class ClosedPoolError(PoolError): - """Raised when a request enters a pool after the pool has been closed.""" - - -class LocationValueError(ValueError, HTTPError): - """Raised when there is something wrong with a given URL input.""" - - -class LocationParseError(LocationValueError): - """Raised when get_host or similar fails to parse the URL input.""" - - def __init__(self, location: str) -> None: - message = f"Failed to parse: {location}" - super().__init__(message) - - self.location = location - - -class URLSchemeUnknown(LocationValueError): - """Raised when a URL input has an unsupported scheme.""" - - def __init__(self, scheme: str): - message = f"Not supported URL scheme {scheme}" - super().__init__(message) - - self.scheme = scheme - - -class ResponseError(HTTPError): - """Used as a container for an error reason supplied in a MaxRetryError.""" - - GENERIC_ERROR = "too many error responses" - SPECIFIC_ERROR = "too many {status_code} error responses" - - -class SecurityWarning(HTTPWarning): - """Warned when performing security reducing actions""" - - -class InsecureRequestWarning(SecurityWarning): - """Warned when making an unverified HTTPS request.""" - - -class NotOpenSSLWarning(SecurityWarning): - """Warned when using unsupported SSL library""" - - -class SystemTimeWarning(SecurityWarning): - """Warned when system time is suspected to be wrong""" - - -class InsecurePlatformWarning(SecurityWarning): - """Warned when certain TLS/SSL configuration is not available on a platform.""" - - -class DependencyWarning(HTTPWarning): - """ - Warned when an attempt is made to import a module with missing optional - dependencies. - """ - - -class ResponseNotChunked(ProtocolError, ValueError): - """Response needs to be chunked in order to read it as chunks.""" - - -class BodyNotHttplibCompatible(HTTPError): - """ - Body should be :class:`http.client.HTTPResponse` like - (have an fp attribute which returns raw chunks) for read_chunked(). - """ - - -class IncompleteRead(HTTPError, httplib_IncompleteRead): - """ - Response length doesn't match expected Content-Length - - Subclass of :class:`http.client.IncompleteRead` to allow int value - for ``partial`` to avoid creating large objects on streamed reads. - """ - - partial: int # type: ignore[assignment] - expected: int - - def __init__(self, partial: int, expected: int) -> None: - self.partial = partial - self.expected = expected - - def __repr__(self) -> str: - return "IncompleteRead(%i bytes read, %i more expected)" % ( - self.partial, - self.expected, - ) - - -class InvalidChunkLength(HTTPError, httplib_IncompleteRead): - """Invalid chunk length in a chunked response.""" - - def __init__(self, response: HTTPResponse, length: bytes) -> None: - self.partial: int = response.tell() # type: ignore[assignment] - self.expected: int | None = response.length_remaining - self.response = response - self.length = length - - def __repr__(self) -> str: - return "InvalidChunkLength(got length %r, %i bytes read)" % ( - self.length, - self.partial, - ) - - -class InvalidHeader(HTTPError): - """The header provided was somehow invalid.""" - - -class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): - """ProxyManager does not support the supplied scheme""" - - # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. - - def __init__(self, scheme: str | None) -> None: - # 'localhost' is here because our URL parser parses - # localhost:8080 -> scheme=localhost, remove if we fix this. - if scheme == "localhost": - scheme = None - if scheme is None: - message = "Proxy URL had no scheme, should start with http:// or https://" - else: - message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" - super().__init__(message) - - -class ProxySchemeUnsupported(ValueError): - """Fetching HTTPS resources through HTTPS proxies is unsupported""" - - -class HeaderParsingError(HTTPError): - """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" - - def __init__( - self, defects: list[MessageDefect], unparsed_data: bytes | str | None - ) -> None: - message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" - super().__init__(message) - - -class UnrewindableBodyError(HTTPError): - """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py deleted file mode 100644 index fe68e17732..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/fields.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import email.utils -import mimetypes -import typing - -_TYPE_FIELD_VALUE = typing.Union[str, bytes] -_TYPE_FIELD_VALUE_TUPLE = typing.Union[ - _TYPE_FIELD_VALUE, - tuple[str, _TYPE_FIELD_VALUE], - tuple[str, _TYPE_FIELD_VALUE, str], -] - - -def guess_content_type( - filename: str | None, default: str = "application/octet-stream" -) -> str: - """ - Guess the "Content-Type" of a file. - - :param filename: - The filename to guess the "Content-Type" of using :mod:`mimetypes`. - :param default: - If no "Content-Type" can be guessed, default to `default`. - """ - if filename: - return mimetypes.guess_type(filename)[0] or default - return default - - -def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Helper function to format and quote a single header parameter using the - strategy defined in RFC 2231. - - Particularly useful for header parameters which might contain - non-ASCII values, like file names. This follows - `RFC 2388 Section 4.4 `_. - - :param name: - The name of the parameter, a string expected to be ASCII only. - :param value: - The value of the parameter, provided as ``bytes`` or `str``. - :returns: - An RFC-2231-formatted unicode string. - - .. deprecated:: 2.0.0 - Will be removed in urllib3 v3.0. This is not valid for - ``multipart/form-data`` header parameters. - """ - import warnings - - warnings.warn( - "'format_header_param_rfc2231' is insecure, deprecated and will be " - "removed in urllib3 v3.0. This is not valid for " - "multipart/form-data header parameters.", - FutureWarning, - stacklevel=2, - ) - - if isinstance(value, bytes): - value = value.decode("utf-8") - - if not any(ch in value for ch in '"\\\r\n'): - result = f'{name}="{value}"' - try: - result.encode("ascii") - except (UnicodeEncodeError, UnicodeDecodeError): - pass - else: - return result - - value = email.utils.encode_rfc2231(value, "utf-8") - value = f"{name}*={value}" - - return value - - -def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Format and quote a single multipart header parameter. - - This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching - the behavior of current browser and curl versions. Values are - assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are - percent encoded. - - .. _WHATWG HTML Standard: - https://html.spec.whatwg.org/multipage/ - form-control-infrastructure.html#multipart-form-data - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - :returns: - A string ``name="value"`` with the escaped value. - - .. versionchanged:: 2.0.0 - Matches the WHATWG HTML Standard as of 2021/06/10. Control - characters are no longer percent encoded. - - .. versionchanged:: 2.0.0 - Renamed from ``format_header_param_html5`` and - ``format_header_param``. The old names will be removed in - urllib3 v3.0. - """ - if isinstance(value, bytes): - value = value.decode("utf-8") - - # percent encode \n \r " - value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) - return f'{name}="{value}"' - - -def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param_html5' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -class RequestField: - """ - A data container for request body parameters. - - :param name: - The name of this request field. Must be unicode. - :param data: - The data/value body. - :param filename: - An optional filename of the request field. Must be unicode. - :param headers: - An optional dict-like object of headers to initially use for the field. - - .. versionchanged:: 2.0.0 - The ``header_formatter`` parameter is deprecated and will - be removed in urllib3 v3.0. - """ - - def __init__( - self, - name: str, - data: _TYPE_FIELD_VALUE, - filename: str | None = None, - headers: typing.Mapping[str, str] | None = None, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ): - self._name = name - self._filename = filename - self.data = data - self.headers: dict[str, str | None] = {} - if headers: - self.headers = dict(headers) - - if header_formatter is not None: - import warnings - - warnings.warn( - "The 'header_formatter' parameter is deprecated and " - "will be removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - self.header_formatter = header_formatter - else: - self.header_formatter = format_multipart_header_param - - @classmethod - def from_tuples( - cls, - fieldname: str, - value: _TYPE_FIELD_VALUE_TUPLE, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ) -> RequestField: - """ - A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. - - Supports constructing :class:`~urllib3.fields.RequestField` from - parameter of key/value strings AND key/filetuple. A filetuple is a - (filename, data, MIME type) tuple where the MIME type is optional. - For example:: - - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - - Field names and filenames must be unicode. - """ - filename: str | None - content_type: str | None - data: _TYPE_FIELD_VALUE - - if isinstance(value, tuple): - if len(value) == 3: - filename, data, content_type = value - else: - filename, data = value - content_type = guess_content_type(filename) - else: - filename = None - content_type = None - data = value - - request_param = cls( - fieldname, data, filename=filename, header_formatter=header_formatter - ) - request_param.make_multipart(content_type=content_type) - - return request_param - - def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Override this method to change how each multipart header - parameter is formatted. By default, this calls - :func:`format_multipart_header_param`. - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - - :meta public: - """ - return self.header_formatter(name, value) - - def _render_parts( - self, - header_parts: ( - dict[str, _TYPE_FIELD_VALUE | None] - | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] - ), - ) -> str: - """ - Helper function to format and quote a single header. - - Useful for single headers that are composed of multiple items. E.g., - 'Content-Disposition' fields. - - :param header_parts: - A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format - as `k1="v1"; k2="v2"; ...`. - """ - iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] - - parts = [] - if isinstance(header_parts, dict): - iterable = header_parts.items() - else: - iterable = header_parts - - for name, value in iterable: - if value is not None: - parts.append(self._render_part(name, value)) - - return "; ".join(parts) - - def render_headers(self) -> str: - """ - Renders the headers for this request field. - """ - lines = [] - - sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] - for sort_key in sort_keys: - if self.headers.get(sort_key, False): - lines.append(f"{sort_key}: {self.headers[sort_key]}") - - for header_name, header_value in self.headers.items(): - if header_name not in sort_keys: - if header_value: - lines.append(f"{header_name}: {header_value}") - - lines.append("\r\n") - return "\r\n".join(lines) - - def make_multipart( - self, - content_disposition: str | None = None, - content_type: str | None = None, - content_location: str | None = None, - ) -> None: - """ - Makes this request field into a multipart request field. - - This method overrides "Content-Disposition", "Content-Type" and - "Content-Location" headers to the request parameter. - - :param content_disposition: - The 'Content-Disposition' of the request body. Defaults to 'form-data' - :param content_type: - The 'Content-Type' of the request body. - :param content_location: - The 'Content-Location' of the request body. - - """ - content_disposition = (content_disposition or "form-data") + "; ".join( - [ - "", - self._render_parts( - (("name", self._name), ("filename", self._filename)) - ), - ] - ) - - self.headers["Content-Disposition"] = content_disposition - self.headers["Content-Type"] = content_type - self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py deleted file mode 100644 index 14f70b05b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/filepost.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -import binascii -import codecs -import os -import typing -from io import BytesIO - -from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField - -writer = codecs.lookup("utf-8")[3] - -_TYPE_FIELDS_SEQUENCE = typing.Sequence[ - typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] -] -_TYPE_FIELDS = typing.Union[ - _TYPE_FIELDS_SEQUENCE, - typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], -] - - -def choose_boundary() -> str: - """ - Our embarrassingly-simple replacement for mimetools.choose_boundary. - """ - return binascii.hexlify(os.urandom(16)).decode() - - -def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: - """ - Iterate over fields. - - Supports list of (k, v) tuples and dicts, and lists of - :class:`~urllib3.fields.RequestField`. - - """ - iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] - - if isinstance(fields, typing.Mapping): - iterable = fields.items() - else: - iterable = fields - - for field in iterable: - if isinstance(field, RequestField): - yield field - else: - yield RequestField.from_tuples(*field) - - -def encode_multipart_formdata( - fields: _TYPE_FIELDS, boundary: str | None = None -) -> tuple[bytes, str]: - """ - Encode a dictionary of ``fields`` using the multipart/form-data MIME format. - - :param fields: - Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). - Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. - - :param boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - body = BytesIO() - if boundary is None: - boundary = choose_boundary() - - for field in iter_field_objects(fields): - body.write(f"--{boundary}\r\n".encode("latin-1")) - - writer(body).write(field.render_headers()) - data = field.data - - if isinstance(data, int): - data = str(data) # Backwards compatibility - - if isinstance(data, str): - writer(body).write(data) - else: - body.write(data) - - body.write(b"\r\n") - - body.write(f"--{boundary}--\r\n".encode("latin-1")) - - content_type = f"multipart/form-data; boundary={boundary}" - - return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py deleted file mode 100644 index 133e1d8f23..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from importlib.metadata import version - -__all__ = [ - "inject_into_urllib3", - "extract_from_urllib3", -] - -import typing - -orig_HTTPSConnection: typing.Any = None - - -def inject_into_urllib3() -> None: - # First check if h2 version is valid - h2_version = version("h2") - if not h2_version.startswith("4."): - raise ImportError( - "urllib3 v2 supports h2 version 4.x.x, currently " - f"the 'h2' module is compiled with {h2_version!r}. " - "See: https://github.com/urllib3/urllib3/issues/3290" - ) - - # Import here to avoid circular dependencies. - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - from .connection import HTTP2Connection - - global orig_HTTPSConnection - orig_HTTPSConnection = urllib3_connection.HTTPSConnection - - HTTPSConnectionPool.ConnectionCls = HTTP2Connection - urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] - - # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. - urllib3_util.ALPN_PROTOCOLS = ["h2"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] - - -def extract_from_urllib3() -> None: - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - - HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection - urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] - - urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py deleted file mode 100644 index 0a026da0a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/connection.py +++ /dev/null @@ -1,356 +0,0 @@ -from __future__ import annotations - -import logging -import re -import threading -import types -import typing - -import h2.config -import h2.connection -import h2.events - -from .._base_connection import _TYPE_BODY -from .._collections import HTTPHeaderDict -from ..connection import HTTPSConnection, _get_default_user_agent -from ..exceptions import ConnectionError -from ..response import BaseHTTPResponse - -orig_HTTPSConnection = HTTPSConnection - -T = typing.TypeVar("T") - -log = logging.getLogger(__name__) - -RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") -RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") - - -def _is_legal_header_name(name: bytes) -> bool: - """ - "An implementation that validates fields according to the definitions in Sections - 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not - include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - - `http.client._is_legal_header_name` does not validate the field name according to the - HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. - - This does not allow for the `:` character in the header name, so should not - be used to validate pseudo-headers. - """ - return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) - - -def _is_illegal_header_value(value: bytes) -> bool: - """ - "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed - (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field - value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, - 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - """ - return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) - - -class _LockedObject(typing.Generic[T]): - """ - A wrapper class that hides a specific object behind a lock. - The goal here is to provide a simple way to protect access to an object - that cannot safely be simultaneously accessed from multiple threads. The - intended use of this class is simple: take hold of it with a context - manager, which returns the protected object. - """ - - __slots__ = ( - "lock", - "_obj", - ) - - def __init__(self, obj: T): - self.lock = threading.RLock() - self._obj = obj - - def __enter__(self) -> T: - self.lock.acquire() - return self._obj - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> None: - self.lock.release() - - -class HTTP2Connection(HTTPSConnection): - def __init__( - self, host: str, port: int | None = None, **kwargs: typing.Any - ) -> None: - self._h2_conn = self._new_h2_conn() - self._h2_stream: int | None = None - self._headers: list[tuple[bytes, bytes]] = [] - - if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: - raise NotImplementedError("Proxies aren't supported with HTTP/2") - - super().__init__(host, port, **kwargs) - - if self._tunnel_host is not None: - raise NotImplementedError("Tunneling isn't supported with HTTP/2") - - def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: - config = h2.config.H2Configuration(client_side=True) - return _LockedObject(h2.connection.H2Connection(config=config)) - - def connect(self) -> None: - super().connect() - with self._h2_conn as conn: - conn.initiate_connection() - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - def putrequest( # type: ignore[override] - self, - method: str, - url: str, - **kwargs: typing.Any, - ) -> None: - """putrequest - This deviates from the HTTPConnection method signature since we never need to override - sending accept-encoding headers or the host header. - """ - if "skip_host" in kwargs: - raise NotImplementedError("`skip_host` isn't supported") - if "skip_accept_encoding" in kwargs: - raise NotImplementedError("`skip_accept_encoding` isn't supported") - - self._request_url = url or "/" - self._validate_path(url) # type: ignore[attr-defined] - - if ":" in self.host: - authority = f"[{self.host}]:{self.port or 443}" - else: - authority = f"{self.host}:{self.port or 443}" - - self._headers.append((b":scheme", b"https")) - self._headers.append((b":method", method.encode())) - self._headers.append((b":authority", authority.encode())) - self._headers.append((b":path", url.encode())) - - with self._h2_conn as conn: - self._h2_stream = conn.get_next_available_stream_id() - - def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] - # TODO SKIPPABLE_HEADERS from urllib3 are ignored. - header = header.encode() if isinstance(header, str) else header - header = header.lower() # A lot of upstream code uses capitalized headers. - if not _is_legal_header_name(header): - raise ValueError(f"Illegal header name {str(header)}") - - for value in values: - value = value.encode() if isinstance(value, str) else value - if _is_illegal_header_value(value): - raise ValueError(f"Illegal header value {str(value)}") - self._headers.append((header, value)) - - def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - conn.send_headers( - stream_id=self._h2_stream, - headers=self._headers, - end_stream=(message_body is None), - ) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - self._headers = [] # Reset headers for the next request. - - def send(self, data: typing.Any) -> None: - """Send data to the server. - `data` can be: `str`, `bytes`, an iterable, or file-like objects - that support a .read() method. - """ - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - if hasattr(data, "read"): # file-like objects - while True: - chunk = data.read(self.blocksize) - if not chunk: - break - if isinstance(chunk, str): - chunk = chunk.encode() - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - return - - if isinstance(data, str): # str -> bytes - data = data.encode() - - try: - if isinstance(data, bytes): - conn.send_data(self._h2_stream, data, end_stream=True) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - else: - for chunk in data: - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - except TypeError: - raise TypeError( - "`data` should be str, bytes, iterable, or file. got %r" - % type(data) - ) - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - raise NotImplementedError( - "HTTP/2 does not support setting up a tunnel through a proxy" - ) - - def getresponse( # type: ignore[override] - self, - ) -> HTTP2Response: - status = None - data = bytearray() - with self._h2_conn as conn: - end_stream = False - while not end_stream: - # TODO: Arbitrary read value. - if received_data := self.sock.recv(65535): - events = conn.receive_data(received_data) - for event in events: - if isinstance(event, h2.events.ResponseReceived): - headers = HTTPHeaderDict() - for header, value in event.headers: - if header == b":status": - status = int(value.decode()) - else: - headers.add( - header.decode("ascii"), value.decode("ascii") - ) - - elif isinstance(event, h2.events.DataReceived): - data += event.data - conn.acknowledge_received_data( - event.flow_controlled_length, event.stream_id - ) - - elif isinstance(event, h2.events.StreamEnded): - end_stream = True - - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - assert status is not None - return HTTP2Response( - status=status, - headers=headers, - request_url=self._request_url, - data=bytes(data), - ) - - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - **kwargs: typing.Any, - ) -> None: - """Send an HTTP/2 request""" - if "chunked" in kwargs: - # TODO this is often present from upstream. - # raise NotImplementedError("`chunked` isn't supported with HTTP/2") - pass - - if self.sock is not None: - self.sock.settimeout(self.timeout) - - self.putrequest(method, url) - - headers = headers or {} - for k, v in headers.items(): - if k.lower() == "transfer-encoding" and v == "chunked": - continue - else: - self.putheader(k, v) - - if b"user-agent" not in dict(self._headers): - self.putheader(b"user-agent", _get_default_user_agent()) - - if body: - self.endheaders(message_body=body) - self.send(body) - else: - self.endheaders() - - def close(self) -> None: - with self._h2_conn as conn: - try: - conn.close_connection() - if data := conn.data_to_send(): - self.sock.sendall(data) - except Exception: - pass - - # Reset all our HTTP/2 connection state. - self._h2_conn = self._new_h2_conn() - self._h2_stream = None - self._headers = [] - - super().close() - - -class HTTP2Response(BaseHTTPResponse): - # TODO: This is a woefully incomplete response object, but works for non-streaming. - def __init__( - self, - status: int, - headers: HTTPHeaderDict, - request_url: str, - data: bytes, - decode_content: bool = False, # TODO: support decoding - ) -> None: - super().__init__( - status=status, - headers=headers, - # Following CPython, we map HTTP versions to major * 10 + minor integers - version=20, - version_string="HTTP/2", - # No reason phrase in HTTP/2 - reason=None, - decode_content=decode_content, - request_url=request_url, - ) - self._data = data - self.length_remaining = 0 - - @property - def data(self) -> bytes: - return self._data - - def get_redirect_location(self) -> None: - return None - - def close(self) -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py deleted file mode 100644 index 9ea900764f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/http2/probe.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import threading - - -class _HTTP2ProbeCache: - __slots__ = ( - "_lock", - "_cache_locks", - "_cache_values", - ) - - def __init__(self) -> None: - self._lock = threading.Lock() - self._cache_locks: dict[tuple[str, int], threading.RLock] = {} - self._cache_values: dict[tuple[str, int], bool | None] = {} - - def acquire_and_get(self, host: str, port: int) -> bool | None: - # By the end of this block we know that - # _cache_[values,locks] is available. - value = None - with self._lock: - key = (host, port) - try: - value = self._cache_values[key] - # If it's a known value we return right away. - if value is not None: - return value - except KeyError: - self._cache_locks[key] = threading.RLock() - self._cache_values[key] = None - - # If the value is unknown, we acquire the lock to signal - # to the requesting thread that the probe is in progress - # or that the current thread needs to return their findings. - key_lock = self._cache_locks[key] - key_lock.acquire() - try: - # If the by the time we get the lock the value has been - # updated we want to return the updated value. - value = self._cache_values[key] - - # In case an exception like KeyboardInterrupt is raised here. - except BaseException as e: # Defensive: - assert not isinstance(e, KeyError) # KeyError shouldn't be possible. - key_lock.release() - raise - - return value - - def set_and_release( - self, host: str, port: int, supports_http2: bool | None - ) -> None: - key = (host, port) - key_lock = self._cache_locks[key] - with key_lock: # Uses an RLock, so can be locked again from same thread. - if supports_http2 is None and self._cache_values[key] is not None: - raise ValueError( - "Cannot reset HTTP/2 support for origin after value has been set." - ) # Defensive: not expected in normal usage - - self._cache_values[key] = supports_http2 - key_lock.release() - - def _values(self) -> dict[tuple[str, int], bool | None]: - """This function is for testing purposes only. Gets the current state of the probe cache""" - with self._lock: - return {k: v for k, v in self._cache_values.items()} - - def _reset(self) -> None: - """This function is for testing purposes only. Reset the cache values""" - with self._lock: - self._cache_locks = {} - self._cache_values = {} - - -_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() - -set_and_release = _HTTP2_PROBE_CACHE.set_and_release -acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get -_values = _HTTP2_PROBE_CACHE._values -_reset = _HTTP2_PROBE_CACHE._reset - -__all__ = [ - "set_and_release", - "acquire_and_get", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py deleted file mode 100644 index 8f2c56745c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/poolmanager.py +++ /dev/null @@ -1,653 +0,0 @@ -from __future__ import annotations - -import functools -import logging -import typing -import warnings -from types import TracebackType -from urllib.parse import urljoin - -from ._collections import HTTPHeaderDict, RecentlyUsedContainer -from ._request_methods import RequestMethods -from .connection import ProxyConfig -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme -from .exceptions import ( - LocationValueError, - MaxRetryError, - ProxySchemeUnknown, - URLSchemeUnknown, -) -from .response import BaseHTTPResponse -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.proxy import connection_requires_http_tunnel -from .util.retry import Retry -from .util.timeout import Timeout -from .util.url import Url, parse_url - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - -__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] - - -log = logging.getLogger(__name__) - -SSL_KEYWORDS = ( - "key_file", - "cert_file", - "cert_reqs", - "ca_certs", - "ca_cert_data", - "ssl_version", - "ssl_minimum_version", - "ssl_maximum_version", - "ca_cert_dir", - "ssl_context", - "key_password", - "server_hostname", -) -# Default value for `blocksize` - a new parameter introduced to -# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 -_DEFAULT_BLOCKSIZE = 16384 - - -class PoolKey(typing.NamedTuple): - """ - All known keyword arguments that could be provided to the pool manager, its - pools, or the underlying connections. - - All custom key schemes should include the fields in this key at a minimum. - """ - - key_scheme: str - key_host: str - key_port: int | None - key_timeout: Timeout | float | int | None - key_retries: Retry | bool | int | None - key_block: bool | None - key_source_address: tuple[str, int] | None - key_key_file: str | None - key_key_password: str | None - key_cert_file: str | None - key_cert_reqs: str | None - key_ca_certs: str | None - key_ca_cert_data: str | bytes | None - key_ssl_version: int | str | None - key_ssl_minimum_version: ssl.TLSVersion | None - key_ssl_maximum_version: ssl.TLSVersion | None - key_ca_cert_dir: str | None - key_ssl_context: ssl.SSLContext | None - key_maxsize: int | None - key_headers: frozenset[tuple[str, str]] | None - key__proxy: Url | None - key__proxy_headers: frozenset[tuple[str, str]] | None - key__proxy_config: ProxyConfig | None - key_socket_options: _TYPE_SOCKET_OPTIONS | None - key__socks_options: frozenset[tuple[str, str]] | None - key_assert_hostname: bool | str | None - key_assert_fingerprint: str | None - key_server_hostname: str | None - key_blocksize: int | None - - -def _default_key_normalizer( - key_class: type[PoolKey], request_context: dict[str, typing.Any] -) -> PoolKey: - """ - Create a pool key out of a request context dictionary. - - According to RFC 3986, both the scheme and host are case-insensitive. - Therefore, this function normalizes both before constructing the pool - key for an HTTPS request. If you wish to change this behaviour, provide - alternate callables to ``key_fn_by_scheme``. - - :param key_class: - The class to use when constructing the key. This should be a namedtuple - with the ``scheme`` and ``host`` keys at a minimum. - :type key_class: namedtuple - :param request_context: - A dictionary-like object that contain the context for a request. - :type request_context: dict - - :return: A namedtuple that can be used as a connection pool key. - :rtype: PoolKey - """ - # Since we mutate the dictionary, make a copy first - context = request_context.copy() - context["scheme"] = context["scheme"].lower() - context["host"] = context["host"].lower() - - # These are both dictionaries and need to be transformed into frozensets - for key in ("headers", "_proxy_headers", "_socks_options"): - if key in context and context[key] is not None: - context[key] = frozenset(context[key].items()) - - # The socket_options key may be a list and needs to be transformed into a - # tuple. - socket_opts = context.get("socket_options") - if socket_opts is not None: - context["socket_options"] = tuple(socket_opts) - - # Map the kwargs to the names in the namedtuple - this is necessary since - # namedtuples can't have fields starting with '_'. - for key in list(context.keys()): - context["key_" + key] = context.pop(key) - - # Default to ``None`` for keys missing from the context - for field in key_class._fields: - if field not in context: - context[field] = None - - # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context - if context.get("key_blocksize") is None: - context["key_blocksize"] = _DEFAULT_BLOCKSIZE - - return key_class(**context) - - -#: A dictionary that maps a scheme to a callable that creates a pool key. -#: This can be used to alter the way pool keys are constructed, if desired. -#: Each PoolManager makes a copy of this dictionary so they can be configured -#: globally here, or individually on the instance. -key_fn_by_scheme = { - "http": functools.partial(_default_key_normalizer, PoolKey), - "https": functools.partial(_default_key_normalizer, PoolKey), -} - -pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} - - -class PoolManager(RequestMethods): - """ - Allows for arbitrary requests while transparently keeping track of - necessary connection pools for you. - - :param num_pools: - Number of connection pools to cache before discarding the least - recently used pool. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param \\**connection_pool_kw: - Additional parameters are used to create fresh - :class:`urllib3.connectionpool.ConnectionPool` instances. - - Example: - - .. code-block:: python - - import urllib3 - - http = urllib3.PoolManager(num_pools=2) - - resp1 = http.request("GET", "https://google.com/") - resp2 = http.request("GET", "https://google.com/mail") - resp3 = http.request("GET", "https://yahoo.com/") - - print(len(http.pools)) - # 2 - - """ - - proxy: Url | None = None - proxy_config: ProxyConfig | None = None - - def __init__( - self, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - super().__init__(headers) - # PoolManager handles redirects itself in PoolManager.urlopen(). - # It always passes redirect=False to the underlying connection pool to - # suppress per-pool redirect handling. If the user supplied a non-Retry - # value (int/bool/etc) for retries and we let the pool normalize it - # while redirect=False, the resulting Retry object would have redirect - # handling disabled, which can interfere with PoolManager's own - # redirect logic. Normalize here so redirects remain governed solely by - # PoolManager logic. - if "retries" in connection_pool_kw: - retries = connection_pool_kw["retries"] - if not isinstance(retries, Retry): - retries = Retry.from_int(retries) - connection_pool_kw = connection_pool_kw.copy() - connection_pool_kw["retries"] = retries - self.connection_pool_kw = connection_pool_kw - - self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] - self.pools = RecentlyUsedContainer(num_pools) - - # Locally set the pool classes and keys so other PoolManagers can - # override them. - self.pool_classes_by_scheme = pool_classes_by_scheme - self.key_fn_by_scheme = key_fn_by_scheme.copy() - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.clear() - # Return False to re-raise any potential exceptions - return False - - def _new_pool( - self, - scheme: str, - host: str, - port: int, - request_context: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and - any additional pool keyword arguments. - - If ``request_context`` is provided, it is provided as keyword arguments - to the pool class used. This method is used to actually create the - connection pools handed out by :meth:`connection_from_url` and - companion methods. It is intended to be overridden for customization. - """ - pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] - if request_context is None: - request_context = self.connection_pool_kw.copy() - - # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly - # set to 'None' in the request_context. - if request_context.get("blocksize") is None: - request_context["blocksize"] = _DEFAULT_BLOCKSIZE - - # Although the context has everything necessary to create the pool, - # this function has historically only used the scheme, host, and port - # in the positional args. When an API change is acceptable these can - # be removed. - for key in ("scheme", "host", "port"): - request_context.pop(key, None) - - if scheme == "http": - for kw in SSL_KEYWORDS: - request_context.pop(kw, None) - - return pool_cls(host, port, **request_context) - - def clear(self) -> None: - """ - Empty our store of pools and direct them all to close. - - This will not affect in-flight connections, but they will not be - re-used after completion. - """ - self.pools.clear() - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. - - If ``port`` isn't given, it will be derived from the ``scheme`` using - ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is - provided, it is merged with the instance's ``connection_pool_kw`` - variable and used to create the new connection pool, if one is - needed. - """ - - if not host: - raise LocationValueError("No host specified.") - - request_context = self._merge_pool_kwargs(pool_kwargs) - request_context["scheme"] = scheme or "http" - if not port: - port = port_by_scheme.get(request_context["scheme"].lower(), 80) - request_context["port"] = port - request_context["host"] = host - - return self.connection_from_context(request_context) - - def connection_from_context( - self, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. - - ``request_context`` must at least contain the ``scheme`` key and its - value must be a key in ``key_fn_by_scheme`` instance variable. - """ - if "strict" in request_context: - warnings.warn( - "The 'strict' parameter is no longer needed on Python 3+. " - "This will raise an error in urllib3 v3.0.", - FutureWarning, - ) - request_context.pop("strict") - - scheme = request_context["scheme"].lower() - pool_key_constructor = self.key_fn_by_scheme.get(scheme) - if not pool_key_constructor: - raise URLSchemeUnknown(scheme) - pool_key = pool_key_constructor(request_context) - - return self.connection_from_pool_key(pool_key, request_context=request_context) - - def connection_from_pool_key( - self, pool_key: PoolKey, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. - - ``pool_key`` should be a namedtuple that only contains immutable - objects. At a minimum it must have the ``scheme``, ``host``, and - ``port`` fields. - """ - with self.pools.lock: - # If the scheme, host, or port doesn't match existing open - # connections, open a new ConnectionPool. - pool = self.pools.get(pool_key) - if pool: - return pool - - # Make a fresh ConnectionPool of the desired type - scheme = request_context["scheme"] - host = request_context["host"] - port = request_context["port"] - pool = self._new_pool(scheme, host, port, request_context=request_context) - self.pools[pool_key] = pool - - return pool - - def connection_from_url( - self, url: str, pool_kwargs: dict[str, typing.Any] | None = None - ) -> HTTPConnectionPool: - """ - Similar to :func:`urllib3.connectionpool.connection_from_url`. - - If ``pool_kwargs`` is not provided and a new pool needs to be - constructed, ``self.connection_pool_kw`` is used to initialize - the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` - is provided, it is used instead. Note that if a new pool does not - need to be created for the request, the provided ``pool_kwargs`` are - not used. - """ - u = parse_url(url) - return self.connection_from_host( - u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs - ) - - def _merge_pool_kwargs( - self, override: dict[str, typing.Any] | None - ) -> dict[str, typing.Any]: - """ - Merge a dictionary of override values for self.connection_pool_kw. - - This does not modify self.connection_pool_kw and returns a new dict. - Any keys in the override dictionary with a value of ``None`` are - removed from the merged dictionary. - """ - base_pool_kwargs = self.connection_pool_kw.copy() - if override: - for key, value in override.items(): - if value is None: - try: - del base_pool_kwargs[key] - except KeyError: - pass - else: - base_pool_kwargs[key] = value - return base_pool_kwargs - - def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: - """ - Indicates if the proxy requires the complete destination URL in the - request. Normally this is only needed when not using an HTTP CONNECT - tunnel. - """ - if self.proxy is None: - return False - - return not connection_requires_http_tunnel( - self.proxy, self.proxy_config, parsed_url.scheme - ) - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - """ - Same as :meth:`urllib3.HTTPConnectionPool.urlopen` - with custom cross-host redirect logic and only sends the request-uri - portion of the ``url``. - - The given ``url`` parameter must be absolute, such that an appropriate - :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. - """ - u = parse_url(url) - - if u.scheme is None: - warnings.warn( - "URLs without a scheme (ie 'https://') are deprecated and will raise an error " - "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " - "start with 'https://' or 'http://'. Read more in this issue: " - "https://github.com/urllib3/urllib3/issues/2920", - category=FutureWarning, - stacklevel=2, - ) - - conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - - kw["assert_same_host"] = False - kw["redirect"] = False - - if "headers" not in kw: - kw["headers"] = self.headers - - if self._proxy_requires_url_absolute_form(u): - response = conn.urlopen(method, url, **kw) - else: - response = conn.urlopen(method, u.request_uri, **kw) - - redirect_location = redirect and response.get_redirect_location() - if not redirect_location: - return response - - # Support relative URLs for redirecting. - redirect_location = urljoin(url, redirect_location) - - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - kw["body"] = None - kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() - - retries = kw.get("retries", response.retries) - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect) - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # conn.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not conn.is_same_host( - redirect_location - ): - new_headers = kw["headers"].copy() - for header in kw["headers"]: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - kw["headers"] = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=conn) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - kw["retries"] = retries - kw["redirect"] = redirect - - log.info("Redirecting %s -> %s", url, redirect_location) - - response.drain_conn() - return self.urlopen(method, redirect_location, **kw) - - -class ProxyManager(PoolManager): - """ - Behaves just like :class:`PoolManager`, but sends all requests through - the defined proxy, using the CONNECT method for HTTPS URLs. - - :param proxy_url: - The URL of the proxy to be used. - - :param proxy_headers: - A dictionary containing headers that will be sent to the proxy. In case - of HTTP they are being sent with each request, while in the - HTTPS/CONNECT case they are sent only once. Could be used for proxy - authentication. - - :param proxy_ssl_context: - The proxy SSL context is used to establish the TLS connection to the - proxy when using HTTPS proxies. - - :param use_forwarding_for_https: - (Defaults to False) If set to True will forward requests to the HTTPS - proxy to be made on behalf of the client instead of creating a TLS - tunnel via the CONNECT method. **Enabling this flag means that request - and response headers and content will be visible from the HTTPS proxy** - whereas tunneling keeps request and response headers and content - private. IP address, target hostname, SNI, and port are always visible - to an HTTPS proxy even when this flag is disabled. - - :param proxy_assert_hostname: - The hostname of the certificate to verify against. - - :param proxy_assert_fingerprint: - The fingerprint of the certificate to verify against. - - Example: - - .. code-block:: python - - import urllib3 - - proxy = urllib3.ProxyManager("https://localhost:3128/") - - resp1 = proxy.request("GET", "http://google.com/") - resp2 = proxy.request("GET", "http://httpbin.org/") - - # One pool was shared by both plain HTTP requests. - print(len(proxy.pools)) - # 1 - - resp3 = proxy.request("GET", "https://httpbin.org/") - resp4 = proxy.request("GET", "https://twitter.com/") - - # A separate pool was added for each HTTPS target. - print(len(proxy.pools)) - # 3 - - """ - - def __init__( - self, - proxy_url: str, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - proxy_headers: typing.Mapping[str, str] | None = None, - proxy_ssl_context: ssl.SSLContext | None = None, - use_forwarding_for_https: bool = False, - proxy_assert_hostname: None | str | typing.Literal[False] = None, - proxy_assert_fingerprint: str | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - if isinstance(proxy_url, HTTPConnectionPool): - str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" - else: - str_proxy_url = proxy_url - proxy = parse_url(str_proxy_url) - - if proxy.scheme not in ("http", "https"): - raise ProxySchemeUnknown(proxy.scheme) - - if not proxy.port: - port = port_by_scheme.get(proxy.scheme, 80) - proxy = proxy._replace(port=port) - - self.proxy = proxy - self.proxy_headers = proxy_headers or {} - self.proxy_ssl_context = proxy_ssl_context - self.proxy_config = ProxyConfig( - proxy_ssl_context, - use_forwarding_for_https, - proxy_assert_hostname, - proxy_assert_fingerprint, - ) - - connection_pool_kw["_proxy"] = self.proxy - connection_pool_kw["_proxy_headers"] = self.proxy_headers - connection_pool_kw["_proxy_config"] = self.proxy_config - - super().__init__(num_pools, headers, **connection_pool_kw) - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - if scheme == "https": - return super().connection_from_host( - host, port, scheme, pool_kwargs=pool_kwargs - ) - - return super().connection_from_host( - self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] - ) - - def _set_proxy_headers( - self, url: str, headers: typing.Mapping[str, str] | None = None - ) -> typing.Mapping[str, str]: - """ - Sets headers needed by proxies: specifically, the Accept and Host - headers. Only sets headers not provided by the user. - """ - headers_ = {"Accept": "*/*"} - - netloc = parse_url(url).netloc - if netloc: - headers_["Host"] = netloc - - if headers: - headers_.update(headers) - return headers_ - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." - u = parse_url(url) - if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): - # For connections using HTTP CONNECT, httplib sets the necessary - # headers on the CONNECT to the proxy. If we're not using CONNECT, - # we'll definitely need to set 'Host' at the very least. - headers = kw.get("headers", self.headers) - kw["headers"] = self._set_proxy_headers(url, headers) - - return super().urlopen(method, url, redirect=redirect, **kw) - - -def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: - return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed deleted file mode 100644 index 5f3ea3d919..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/py.typed +++ /dev/null @@ -1,2 +0,0 @@ -# Instruct type checkers to look for inline type annotations in this package. -# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py deleted file mode 100644 index e9246b75e3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/response.py +++ /dev/null @@ -1,1493 +0,0 @@ -from __future__ import annotations - -import collections -import io -import json as _json -import logging -import socket -import sys -import typing -import warnings -import zlib -from contextlib import contextmanager -from http.client import HTTPMessage as _HttplibHTTPMessage -from http.client import HTTPResponse as _HttplibHTTPResponse -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from ._base_connection import BaseHTTPConnection - -try: - try: - import brotlicffi as brotli # type: ignore[import-not-found] - except ImportError: - import brotli # type: ignore[import-not-found] -except ImportError: - brotli = None - -from . import util -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .connection import BaseSSLError, HTTPConnection, HTTPException -from .exceptions import ( - BodyNotHttplibCompatible, - DecodeError, - DependencyWarning, - HTTPError, - IncompleteRead, - InvalidChunkLength, - InvalidHeader, - ProtocolError, - ReadTimeoutError, - ResponseNotChunked, - SSLError, -) -from .util.response import is_fp_closed, is_response_to_head -from .util.retry import Retry - -if typing.TYPE_CHECKING: - from .connectionpool import HTTPConnectionPool - -log = logging.getLogger(__name__) - - -class ContentDecoder: - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - raise NotImplementedError() - - @property - def has_unconsumed_tail(self) -> bool: - raise NotImplementedError() - - def flush(self) -> bytes: - raise NotImplementedError() - - -class DeflateDecoder(ContentDecoder): - def __init__(self) -> None: - self._first_try = True - self._first_try_data = b"" - self._unfed_data = b"" - self._obj = zlib.decompressobj() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - data = self._unfed_data + data - self._unfed_data = b"" - if not data and not self._obj.unconsumed_tail: - return data - original_max_length = max_length - if original_max_length < 0: - max_length = 0 - elif original_max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unfed_data = data - return b"" - - # Subsequent calls always reuse `self._obj`. zlib requires - # passing the unconsumed tail if decompression is to continue. - if not self._first_try: - return self._obj.decompress( - self._obj.unconsumed_tail + data, max_length=max_length - ) - - # First call tries with RFC 1950 ZLIB format. - self._first_try_data += data - try: - decompressed = self._obj.decompress(data, max_length=max_length) - if decompressed: - self._first_try = False - self._first_try_data = b"" - return decompressed - # On failure, it falls back to RFC 1951 DEFLATE format. - except zlib.error: - self._first_try = False - self._obj = zlib.decompressobj(-zlib.MAX_WBITS) - try: - return self.decompress( - self._first_try_data, max_length=original_max_length - ) - finally: - self._first_try_data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unfed_data) or ( - bool(self._obj.unconsumed_tail) and not self._first_try - ) - - def flush(self) -> bytes: - return self._obj.flush() - - -class GzipDecoderState: - FIRST_MEMBER = 0 - OTHER_MEMBERS = 1 - SWALLOW_DATA = 2 - - -class GzipDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - self._state = GzipDecoderState.FIRST_MEMBER - self._unconsumed_tail = b"" - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - ret = bytearray() - if self._state == GzipDecoderState.SWALLOW_DATA: - return bytes(ret) - - if max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unconsumed_tail += data - return b"" - - # zlib requires passing the unconsumed tail to the subsequent - # call if decompression is to continue. - data = self._unconsumed_tail + data - if not data and self._obj.eof: - return bytes(ret) - - while True: - try: - ret += self._obj.decompress( - data, max_length=max(max_length - len(ret), 0) - ) - except zlib.error: - previous_state = self._state - # Ignore data after the first error - self._state = GzipDecoderState.SWALLOW_DATA - self._unconsumed_tail = b"" - if previous_state == GzipDecoderState.OTHER_MEMBERS: - # Allow trailing garbage acceptable in other gzip clients - return bytes(ret) - raise - - self._unconsumed_tail = data = ( - self._obj.unconsumed_tail or self._obj.unused_data - ) - if max_length > 0 and len(ret) >= max_length: - break - - if not data: - return bytes(ret) - # When the end of a gzip member is reached, a new decompressor - # must be created for unused (possibly future) data. - if self._obj.eof: - self._state = GzipDecoderState.OTHER_MEMBERS - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - - return bytes(ret) - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unconsumed_tail) - - def flush(self) -> bytes: - return self._obj.flush() - - -if brotli is not None: - - class BrotliDecoder(ContentDecoder): - # Supports both 'brotlipy' and 'Brotli' packages - # since they share an import name. The top branches - # are for 'brotlipy' and bottom branches for 'Brotli' - def __init__(self) -> None: - self._obj = brotli.Decompressor() - if hasattr(self._obj, "decompress"): - setattr(self, "_decompress", self._obj.decompress) - else: - setattr(self, "_decompress", self._obj.process) - - # Requires Brotli >= 1.2.0 for `output_buffer_limit`. - def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: - raise NotImplementedError() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - try: - if max_length > 0: - return self._decompress(data, output_buffer_limit=max_length) - else: - return self._decompress(data) - except TypeError: - # Fallback for Brotli/brotlicffi/brotlipy versions without - # the `output_buffer_limit` parameter. - warnings.warn( - "Brotli >= 1.2.0 is required to prevent decompression bombs.", - DependencyWarning, - ) - return self._decompress(data) - - @property - def has_unconsumed_tail(self) -> bool: - try: - return not self._obj.can_accept_more_data() - except AttributeError: - return False - - def flush(self) -> bytes: - if hasattr(self._obj, "flush"): - return self._obj.flush() # type: ignore[no-any-return] - return b"" - - -try: - if sys.version_info >= (3, 14): - from compression import zstd - else: - from backports import zstd -except ImportError: - HAS_ZSTD = False -else: - HAS_ZSTD = True - - class ZstdDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zstd.ZstdDecompressor() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if not data and not self.has_unconsumed_tail: - return b"" - if self._obj.eof: - data = self._obj.unused_data + data - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress(data, max_length=max_length) - length = len(part) - data_parts = [part] - # Every loop iteration is supposed to read data from a separate frame. - # The loop breaks when: - # - enough data is read; - # - no more unused data is available; - # - end of the last read frame has not been reached (i.e., - # more data has to be fed). - while ( - self._obj.eof - and self._obj.unused_data - and (max_length < 0 or length < max_length) - ): - unused_data = self._obj.unused_data - if not self._obj.needs_input: - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress( - unused_data, - max_length=(max_length - length) if max_length > 0 else -1, - ) - if part_length := len(part): - data_parts.append(part) - length += part_length - elif self._obj.needs_input: - break - return b"".join(data_parts) - - @property - def has_unconsumed_tail(self) -> bool: - return not (self._obj.needs_input or self._obj.eof) or bool( - self._obj.unused_data - ) - - def flush(self) -> bytes: - if not self._obj.eof: - raise DecodeError("Zstandard data is incomplete") - return b"" - - -class MultiDecoder(ContentDecoder): - """ - From RFC7231: - If one or more encodings have been applied to a representation, the - sender that applied the encodings MUST generate a Content-Encoding - header field that lists the content codings in the order in which - they were applied. - """ - - # Maximum allowed number of chained HTTP encodings in the - # Content-Encoding header. - max_decode_links = 5 - - def __init__(self, modes: str) -> None: - encodings = [m.strip() for m in modes.split(",")] - if len(encodings) > self.max_decode_links: - raise DecodeError( - "Too many content encodings in the chain: " - f"{len(encodings)} > {self.max_decode_links}" - ) - self._decoders = [_get_decoder(e) for e in encodings] - - def flush(self) -> bytes: - return self._decoders[0].flush() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if max_length <= 0: - for d in reversed(self._decoders): - data = d.decompress(data) - return data - - ret = bytearray() - # Every while loop iteration goes through all decoders once. - # It exits when enough data is read or no more data can be read. - # It is possible that the while loop iteration does not produce - # any data because we retrieve up to `max_length` from every - # decoder, and the amount of bytes may be insufficient for the - # next decoder to produce enough/any output. - while True: - any_data = False - for d in reversed(self._decoders): - data = d.decompress(data, max_length=max_length - len(ret)) - if data: - any_data = True - # We should not break when no data is returned because - # next decoders may produce data even with empty input. - ret += data - if not any_data or len(ret) >= max_length: - return bytes(ret) - data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return any(d.has_unconsumed_tail for d in self._decoders) - - -def _get_decoder(mode: str) -> ContentDecoder: - if "," in mode: - return MultiDecoder(mode) - - # According to RFC 9110 section 8.4.1.3, recipients should - # consider x-gzip equivalent to gzip - if mode in ("gzip", "x-gzip"): - return GzipDecoder() - - if brotli is not None and mode == "br": - return BrotliDecoder() - - if HAS_ZSTD and mode == "zstd": - return ZstdDecoder() - - return DeflateDecoder() - - -class BytesQueueBuffer: - """Memory-efficient bytes buffer - - To return decoded data in read() and still follow the BufferedIOBase API, we need a - buffer to always return the correct amount of bytes. - - This buffer should be filled using calls to put() - - Our maximum memory usage is determined by the sum of the size of: - - * self.buffer, which contains the full data - * the largest chunk that we will copy in get() - """ - - def __init__(self) -> None: - self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() - self._size: int = 0 - - def __len__(self) -> int: - return self._size - - def put(self, data: bytes) -> None: - self.buffer.append(data) - self._size += len(data) - - def get(self, n: int) -> bytes: - if n == 0: - return b"" - elif not self.buffer: - raise RuntimeError("buffer is empty") - elif n < 0: - raise ValueError("n should be > 0") - - if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): - self._size -= n - return self.buffer.popleft() - - fetched = 0 - ret = io.BytesIO() - while fetched < n: - remaining = n - fetched - chunk = self.buffer.popleft() - chunk_length = len(chunk) - if remaining < chunk_length: - chunk = memoryview(chunk) - left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] - ret.write(left_chunk) - self.buffer.appendleft(right_chunk) - self._size -= remaining - break - else: - ret.write(chunk) - self._size -= chunk_length - fetched += chunk_length - - if not self.buffer: - break - - return ret.getvalue() - - def get_all(self) -> bytes: - buffer = self.buffer - if not buffer: - assert self._size == 0 - return b"" - if len(buffer) == 1: - result = buffer.pop() - if isinstance(result, memoryview): - result = result.tobytes() - else: - ret = io.BytesIO() - ret.writelines(buffer.popleft() for _ in range(len(buffer))) - result = ret.getvalue() - self._size = 0 - return result - - -class BaseHTTPResponse(io.IOBase): - CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] - if brotli is not None: - CONTENT_DECODERS += ["br"] - if HAS_ZSTD: - CONTENT_DECODERS += ["zstd"] - REDIRECT_STATUSES = [301, 302, 303, 307, 308] - - DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) - if brotli is not None: - DECODER_ERROR_CLASSES += (brotli.error,) - - if HAS_ZSTD: - DECODER_ERROR_CLASSES += (zstd.ZstdError,) - - def __init__( - self, - *, - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int, - version: int, - version_string: str, - reason: str | None, - decode_content: bool, - request_url: str | None, - retries: Retry | None = None, - ) -> None: - if isinstance(headers, HTTPHeaderDict): - self.headers = headers - else: - self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] - self.status = status - self.version = version - self.version_string = version_string - self.reason = reason - self.decode_content = decode_content - self._has_decoded_content = False - self._request_url: str | None = request_url - self.retries = retries - - self.chunked = False - tr_enc = self.headers.get("transfer-encoding", "").lower() - # Don't incur the penalty of creating a list and then discarding it - encodings = (enc.strip() for enc in tr_enc.split(",")) - if "chunked" in encodings: - self.chunked = True - - self._decoder: ContentDecoder | None = None - self.length_remaining: int | None - - def get_redirect_location(self) -> str | None | typing.Literal[False]: - """ - Should we redirect and where to? - - :returns: Truthy redirect location string if we got a redirect status - code and valid location. ``None`` if redirect status and no - location. ``False`` if not a redirect status code. - """ - if self.status in self.REDIRECT_STATUSES: - return self.headers.get("location") - return False - - @property - def data(self) -> bytes: - raise NotImplementedError() - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - @property - def url(self) -> str | None: - raise NotImplementedError() - - @url.setter - def url(self, url: str | None) -> None: - raise NotImplementedError() - - @property - def connection(self) -> BaseHTTPConnection | None: - raise NotImplementedError() - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - if retries is not None and retries.history: - self.url = retries.history[-1].redirect_location - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - raise NotImplementedError() - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - raise NotImplementedError() - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def release_conn(self) -> None: - raise NotImplementedError() - - def drain_conn(self) -> None: - raise NotImplementedError() - - def shutdown(self) -> None: - raise NotImplementedError() - - def close(self) -> None: - raise NotImplementedError() - - def _init_decoder(self) -> None: - """ - Set-up the _decoder attribute if necessary. - """ - # Note: content-encoding value should be case-insensitive, per RFC 7230 - # Section 3.2 - content_encoding = self.headers.get("content-encoding", "").lower() - if self._decoder is None: - if content_encoding in self.CONTENT_DECODERS: - self._decoder = _get_decoder(content_encoding) - elif "," in content_encoding: - encodings = [ - e.strip() - for e in content_encoding.split(",") - if e.strip() in self.CONTENT_DECODERS - ] - if encodings: - self._decoder = _get_decoder(content_encoding) - - def _decode( - self, - data: bytes, - decode_content: bool | None, - flush_decoder: bool, - max_length: int | None = None, - ) -> bytes: - """ - Decode the data passed in and potentially flush the decoder. - """ - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - if max_length is None or flush_decoder: - max_length = -1 - - try: - if self._decoder: - data = self._decoder.decompress(data, max_length=max_length) - self._has_decoded_content = True - except self.DECODER_ERROR_CLASSES as e: - content_encoding = self.headers.get("content-encoding", "").lower() - raise DecodeError( - "Received response with content-encoding: %s, but " - "failed to decode it." % content_encoding, - e, - ) from e - if flush_decoder: - data += self._flush_decoder() - - return data - - def _flush_decoder(self) -> bytes: - """ - Flushes the decoder. Should only be called if the decoder is actually - being used. - """ - if self._decoder: - return self._decoder.decompress(b"") + self._decoder.flush() - return b"" - - # Compatibility methods for `io` module - def readinto(self, b: bytearray | memoryview[int]) -> int: - temp = self.read(len(b)) - if len(temp) == 0: - return 0 - else: - b[: len(temp)] = temp - return len(temp) - - # Methods used by dependent libraries - def getheaders(self) -> HTTPHeaderDict: - return self.headers - - def getheader(self, name: str, default: str | None = None) -> str | None: - return self.headers.get(name, default) - - # Compatibility method for http.cookiejar - def info(self) -> HTTPHeaderDict: - return self.headers - - def geturl(self) -> str | None: - return self.url - - -class HTTPResponse(BaseHTTPResponse): - """ - HTTP Response container. - - Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is - loaded and decoded on-demand when the ``data`` property is accessed. This - class is also compatible with the Python standard library's :mod:`io` - module, and can hence be treated as a readable object in the context of that - framework. - - Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param original_response: - When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` - object, it's convenient to include the original for debug purposes. It's - otherwise unused. - - :param retries: - The retries contains the last :class:`~urllib3.util.retry.Retry` that - was used during the request. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - - def __init__( - self, - body: _TYPE_BODY = "", - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int = 0, - version: int = 0, - version_string: str = "HTTP/?", - reason: str | None = None, - preload_content: bool = True, - decode_content: bool = True, - original_response: _HttplibHTTPResponse | None = None, - pool: HTTPConnectionPool | None = None, - connection: HTTPConnection | None = None, - msg: _HttplibHTTPMessage | None = None, - retries: Retry | None = None, - enforce_content_length: bool = True, - request_method: str | None = None, - request_url: str | None = None, - auto_close: bool = True, - sock_shutdown: typing.Callable[[int], None] | None = None, - ) -> None: - super().__init__( - headers=headers, - status=status, - version=version, - version_string=version_string, - reason=reason, - decode_content=decode_content, - request_url=request_url, - retries=retries, - ) - - self.enforce_content_length = enforce_content_length - self.auto_close = auto_close - - self._body = None - self._uncached_read_occurred = False - self._fp: _HttplibHTTPResponse | None = None - self._original_response = original_response - self._fp_bytes_read = 0 - self.msg = msg - - if body and isinstance(body, (str, bytes)): - self._body = body - - self._pool = pool - self._connection = connection - - if hasattr(body, "read"): - self._fp = body # type: ignore[assignment] - self._sock_shutdown = sock_shutdown - - # Are we using the chunked-style of transfer encoding? - self.chunk_left: int | None = None - - # Determine length of response - self.length_remaining = self._init_length(request_method) - - # Used to return the correct amount of bytes for partial read()s - self._decoded_buffer = BytesQueueBuffer() - - # If requested, preload the body. - if preload_content and not self._body: - self._body = self.read(decode_content=decode_content) - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - """ - Read and discard any remaining HTTP response data in the response connection. - - Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. - """ - try: - self._raw_read() - except (HTTPError, OSError, BaseSSLError, HTTPException): - pass - if self._has_decoded_content: - # `_raw_read` skips decompression, so we should clean up the - # decoder to avoid keeping unnecessary data in memory. - self._decoded_buffer = BytesQueueBuffer() - self._decoder = None - - @property - def data(self) -> bytes: - # For backwards-compat with earlier urllib3 0.4 and earlier. - if self._body: - return self._body # type: ignore[return-value] - - if self._fp: - return self.read(cache_content=True) - - return None # type: ignore[return-value] - - @property - def connection(self) -> HTTPConnection | None: - return self._connection - - def isclosed(self) -> bool: - return is_fp_closed(self._fp) - - def tell(self) -> int: - """ - Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:`HTTPResponse.read` - if bytes are encoded on the wire (e.g, compressed). - """ - return self._fp_bytes_read - - def _init_length(self, request_method: str | None) -> int | None: - """ - Set initial length value for Response content if available. - """ - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - if self.chunked: - # This Response will fail with an IncompleteRead if it can't be - # received as chunked. This method falls back to attempt reading - # the response before raising an exception. - log.warning( - "Received response with both Content-Length and " - "Transfer-Encoding set. This is expressly forbidden " - "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " - "attempting to process response as Transfer-Encoding: " - "chunked." - ) - return None - - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Convert status to int for comparison - # In some cases, httplib returns a status of "_UNKNOWN" - try: - status = int(self.status) - except ValueError: - status = 0 - - # Check for responses that shouldn't include a body - if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": - length = 0 - - return length - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch low-level python exceptions, instead re-raising urllib3 - variants, so that low-level exceptions are not leaked in the - high-level api. - - On exit, release the connection back to the pool. - """ - clean_exit = False - - try: - try: - yield - - except SocketTimeout as e: - # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but - # there is yet no clean way to get at it from this context. - raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] - - except BaseSSLError as e: - # SSL errors related to framing/MAC get wrapped and reraised here - raise SSLError(e) from e - - except IncompleteRead as e: - if ( - e.expected is not None - and e.partial is not None - and e.expected == -e.partial - ): - arg = "Response may not contain content." - else: - arg = f"Connection broken: {e!r}" - raise ProtocolError(arg, e) from e - - except (HTTPException, OSError) as e: - raise ProtocolError(f"Connection broken: {e!r}", e) from e - - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now to ensure that the connection is - # released back to the pool. - if self._original_response: - self._original_response.close() - - # Closing the response may not actually be sufficient to close - # everything, so if we have a hold of the connection close that - # too. - if self._connection: - self._connection.close() - - # If we hold the original response but it's closed now, we should - # return the connection back to the pool. - if self._original_response and self._original_response.isclosed(): - self.release_conn() - - def _fp_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Read a response with the thought that reading the number of bytes - larger than can fit in a 32-bit int at a time via SSL in some - known cases leads to an overflow error that has to be prevented - if `amt` or `self.length_remaining` indicate that a problem may - happen. - - This happens to urllib3 injected with pyOpenSSL-backed SSL-support. - """ - assert self._fp - c_int_max = 2**31 - 1 - if ( - (amt and amt > c_int_max) - or ( - amt is None - and self.length_remaining - and self.length_remaining > c_int_max - ) - ) and util.IS_PYOPENSSL: - if read1: - return self._fp.read1(c_int_max) - buffer = io.BytesIO() - # Besides `max_chunk_amt` being a maximum chunk size, it - # affects memory overhead of reading a response by this - # method in CPython. - # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum - # chunk size that does not lead to an overflow error, but - # 256 MiB is a compromise. - max_chunk_amt = 2**28 - while amt is None or amt != 0: - if amt is not None: - chunk_amt = min(amt, max_chunk_amt) - amt -= chunk_amt - else: - chunk_amt = max_chunk_amt - data = self._fp.read(chunk_amt) - if not data: - break - buffer.write(data) - del data # to reduce peak memory usage by `max_chunk_amt`. - return buffer.getvalue() - elif read1: - return self._fp.read1(amt) if amt is not None else self._fp.read1() - else: - # StringIO doesn't like amt=None - return self._fp.read(amt) if amt is not None else self._fp.read() - - def _raw_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Reads `amt` of bytes from the socket. - """ - if self._fp is None: - return None # type: ignore[return-value] - - fp_closed = getattr(self._fp, "closed", False) - - with self._error_catcher(): - data = self._fp_read(amt, read1=read1) if not fp_closed else b"" - if amt is not None and amt != 0 and not data: - # Platform-specific: Buggy versions of Python. - # Close the connection when no data is returned - # - # This is redundant to what httplib/http.client _should_ - # already do. However, versions of python released before - # December 15, 2012 (http://bugs.python.org/issue16298) do - # not properly close the connection in all cases. There is - # no harm in redundantly calling close. - self._fp.close() - if ( - self.enforce_content_length - and self.length_remaining is not None - and self.length_remaining != 0 - ): - # This is an edge case that httplib failed to cover due - # to concerns of backward compatibility. We're - # addressing it here to make sure IncompleteRead is - # raised during streaming, so all calls with incorrect - # Content-Length are caught. - raise IncompleteRead(self._fp_bytes_read, self.length_remaining) - elif read1 and ( - (amt != 0 and not data) or self.length_remaining == len(data) - ): - # All data has been read, but `self._fp.read1` in - # CPython 3.12 and older doesn't always close - # `http.client.HTTPResponse`, so we close it here. - # See https://github.com/python/cpython/issues/113199 - self._fp.close() - - if data: - self._fp_bytes_read += len(data) - if self.length_remaining is not None: - self.length_remaining -= len(data) - return data - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - """ - Similar to :meth:`http.client.HTTPResponse.read`, but with two additional - parameters: ``decode_content`` and ``cache_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param cache_content: - If True, will save the returned data such that the same result is - returned despite of the state of the underlying file object. This - is useful if you want the ``.data`` property to continue working - after having ``.read()`` the file object. (Overridden if ``amt`` is - set.) - """ - self._init_decoder() - if decode_content is None: - decode_content = self.decode_content - - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - elif amt is not None: - cache_content = False - - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and len(self._decoded_buffer) < amt - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) >= amt: - return self._decoded_buffer.get(amt) - - data = self._raw_read(amt) - if not cache_content: - self._uncached_read_occurred = True - - flush_decoder = amt is None or (amt != 0 and not data) - - if ( - not data - and len(self._decoded_buffer) == 0 - and not (self._decoder and self._decoder.has_unconsumed_tail) - ): - return data - - if amt is None: - data = self._decode(data, decode_content, flush_decoder) - # It's possible that there is buffered decoded data after a - # partial read. - if decode_content and len(self._decoded_buffer) > 0: - self._decoded_buffer.put(data) - data = self._decoded_buffer.get_all() - - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - # do not waste memory on buffer when not decoding - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - - while len(self._decoded_buffer) < amt and data: - # TODO make sure to initially read enough data to get past the headers - # For example, the GZ file header takes 10 bytes, we don't want to read - # it one byte at a time - data = self._raw_read(amt) - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - data = self._decoded_buffer.get(amt) - - return data - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - """ - Similar to ``http.client.HTTPResponse.read1`` and documented - in :meth:`io.BufferedReader.read1`, but with an additional parameter: - ``decode_content``. - - :param amt: - How much of the content to read. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if decode_content is None: - decode_content = self.decode_content - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - # try and respond without going to the network - if self._has_decoded_content: - if not decode_content: - raise RuntimeError( - "Calling read1(decode_content=False) is not supported after " - "read1(decode_content=True) was called." - ) - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and (amt is None or len(self._decoded_buffer) < amt) - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=( - amt - len(self._decoded_buffer) if amt is not None else None - ), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) > 0: - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - if amt == 0: - return b"" - - # FIXME, this method's type doesn't say returning None is possible - data = self._raw_read(amt, read1=True) - self._uncached_read_occurred = True - if not decode_content or data is None: - return data - - self._init_decoder() - while True: - flush_decoder = not data - decoded_data = self._decode( - data, decode_content, flush_decoder, max_length=amt - ) - self._decoded_buffer.put(decoded_data) - if decoded_data or flush_decoder: - break - data = self._raw_read(8192, read1=True) - - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if amt == 0: - return - - if self.chunked and self.supports_chunked_reads(): - yield from self.read_chunked(amt, decode_content=decode_content) - else: - while ( - not is_fp_closed(self._fp) - or len(self._decoded_buffer) > 0 - or (self._decoder and self._decoder.has_unconsumed_tail) - ): - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - - # Overrides from io.IOBase - def readable(self) -> bool: - return True - - def shutdown(self) -> None: - if not self._sock_shutdown: - raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") - if self._connection is None: - raise RuntimeError( - "Cannot shutdown as connection has already been released to the pool" - ) - self._sock_shutdown(socket.SHUT_RD) - - def close(self) -> None: - self._sock_shutdown = None - - if not self.closed and self._fp: - self._fp.close() - - if self._connection: - self._connection.close() - - if not self.auto_close: - io.IOBase.close(self) - - @property - def closed(self) -> bool: - if not self.auto_close: - return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] - elif self._fp is None: - return True - elif hasattr(self._fp, "isclosed"): - return self._fp.isclosed() - elif hasattr(self._fp, "closed"): - return self._fp.closed - else: - return True - - def fileno(self) -> int: - if self._fp is None: - raise OSError("HTTPResponse has no file to get a fileno from") - elif hasattr(self._fp, "fileno"): - return self._fp.fileno() - else: - raise OSError( - "The file-like object this HTTPResponse is wrapped " - "around has no file descriptor" - ) - - def flush(self) -> None: - if ( - self._fp is not None - and hasattr(self._fp, "flush") - and not getattr(self._fp, "closed", False) - ): - return self._fp.flush() - - def supports_chunked_reads(self) -> bool: - """ - Checks if the underlying file-like object looks like a - :class:`http.client.HTTPResponse` object. We do this by testing for - the fp attribute. If it is present we assume it returns raw chunks as - processed by read_chunked(). - """ - return hasattr(self._fp, "fp") - - def _update_chunk_length(self) -> None: - # First, we'll figure out length of a chunk and then - # we'll try to read it from socket. - if self.chunk_left is not None: - return None - line = self._fp.fp.readline() # type: ignore[union-attr] - line = line.split(b";", 1)[0] - try: - self.chunk_left = int(line, 16) - except ValueError: - self.close() - if line: - # Invalid chunked protocol response, abort. - raise InvalidChunkLength(self, line) from None - else: - # Truncated at start of next chunk - raise ProtocolError("Response ended prematurely") from None - - def _handle_chunk(self, amt: int | None) -> bytes: - returned_chunk = None - if amt is None: - chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - returned_chunk = chunk - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - elif self.chunk_left is not None and amt < self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self.chunk_left = self.chunk_left - amt - returned_chunk = value - elif amt == self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - returned_chunk = value - else: # amt > self.chunk_left - returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - return returned_chunk # type: ignore[no-any-return] - - def read_chunked( - self, amt: int | None = None, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - Similar to :meth:`HTTPResponse.read`, but with an additional - parameter: ``decode_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - self._init_decoder() - # FIXME: Rewrite this method and make it a class with a better structured logic. - if not self.chunked: - raise ResponseNotChunked( - "Response is not chunked. " - "Header 'transfer-encoding: chunked' is missing." - ) - if not self.supports_chunked_reads(): - raise BodyNotHttplibCompatible( - "Body should be http.client.HTTPResponse like. " - "It should have have an fp attribute which returns raw chunks." - ) - - with self._error_catcher(): - # Don't bother reading the body of a HEAD request. - if self._original_response and is_response_to_head(self._original_response): - self._original_response.close() - return None - - # If a response is already read and closed - # then return immediately. - if self._fp.fp is None: # type: ignore[union-attr] - return None - - if amt == 0: - return - elif amt and amt < 0: - # Negative numbers and `None` should be treated the same, - # but httplib handles only `None` correctly. - amt = None - - while True: - # First, check if any data is left in the decoder's buffer. - if self._decoder and self._decoder.has_unconsumed_tail: - chunk = b"" - else: - self._update_chunk_length() - self._uncached_read_occurred = True - if self.chunk_left == 0: - break - chunk = self._handle_chunk(amt) - decoded = self._decode( - chunk, - decode_content=decode_content, - flush_decoder=False, - max_length=amt, - ) - if decoded: - yield decoded - - if decode_content: - # On CPython and PyPy, we should never need to flush the - # decoder. However, on Jython we *might* need to, so - # lets defensively do it anyway. - decoded = self._flush_decoder() - if decoded: # Platform-specific: Jython. - yield decoded - - # Chunk content ends with \r\n: discard it. - while self._fp is not None: - line = self._fp.fp.readline() - if not line: - # Some sites may not end with '\r\n'. - break - if line == b"\r\n": - break - - # We read everything; close the "file". - if self._original_response: - self._original_response.close() - - @property - def url(self) -> str | None: - """ - Returns the URL that was the source of this response. - If the request that generated this response redirected, this method - will return the final redirect location. - """ - return self._request_url - - @url.setter - def url(self, url: str | None) -> None: - self._request_url = url - - def __iter__(self) -> typing.Iterator[bytes]: - buffer: list[bytes] = [] - for chunk in self.stream(decode_content=True): - if b"\n" in chunk: - chunks = chunk.split(b"\n") - yield b"".join(buffer) + chunks[0] + b"\n" - for x in chunks[1:-1]: - yield x + b"\n" - if chunks[-1]: - buffer = [chunks[-1]] - else: - buffer = [] - else: - buffer.append(chunk) - if buffer: - yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py deleted file mode 100644 index 534126033c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# For backwards compatibility, provide imports that used to be here. -from __future__ import annotations - -from .connection import is_connection_dropped -from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers -from .response import is_fp_closed -from .retry import Retry -from .ssl_ import ( - ALPN_PROTOCOLS, - IS_PYOPENSSL, - SSLContext, - assert_fingerprint, - create_urllib3_context, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .timeout import Timeout -from .url import Url, parse_url -from .wait import wait_for_read, wait_for_write - -__all__ = ( - "IS_PYOPENSSL", - "SSLContext", - "ALPN_PROTOCOLS", - "Retry", - "Timeout", - "Url", - "assert_fingerprint", - "create_urllib3_context", - "is_connection_dropped", - "is_fp_closed", - "parse_url", - "make_headers", - "resolve_cert_reqs", - "resolve_ssl_version", - "ssl_wrap_socket", - "wait_for_read", - "wait_for_write", - "SKIP_HEADER", - "SKIPPABLE_HEADERS", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py deleted file mode 100644 index f92519ee91..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/connection.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import annotations - -import socket -import typing - -from ..exceptions import LocationParseError -from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT - -_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] - -if typing.TYPE_CHECKING: - from .._base_connection import BaseHTTPConnection - - -def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific - """ - Returns True if the connection is dropped and should be closed. - :param conn: :class:`urllib3.connection.HTTPConnection` object. - """ - return not conn.is_connected - - -# This function is copied from socket.py in the Python 2.7 standard -# library test suite. Added to its signature is only `socket_options`. -# One additional modification is that we avoid binding to IPv6 servers -# discovered in DNS if the system doesn't have IPv6 functionality. -def create_connection( - address: tuple[str, int], - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, -) -> socket.socket: - """Connect to *address* and return the socket object. - - Convenience function. Connect to *address* (a 2-tuple ``(host, - port)``) and return the socket object. Passing the optional - *timeout* parameter will set the timeout on the socket instance - before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`socket.getdefaulttimeout` - is used. If *source_address* is set it must be a tuple of (host, port) - for the socket to bind as a source address before making the connection. - An host of '' or port 0 tells the OS to use the default. - """ - - host, port = address - if host.startswith("["): - host = host.strip("[]") - err = None - - # Using the value from allowed_gai_family() in the context of getaddrinfo lets - # us select whether to work with IPv4 DNS records, IPv6 records, or both. - # The original create_connection function always returns all records. - family = allowed_gai_family() - - try: - host.encode("idna") - except UnicodeError: - raise LocationParseError(f"'{host}', label empty or too long") from None - - for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = None - try: - sock = socket.socket(af, socktype, proto) - - # If provided, set socket level options before connecting. - _set_socket_options(sock, socket_options) - - if timeout is not _DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(sa) - # Break explicitly a reference cycle - err = None - return sock - - except OSError as _: - err = _ - if sock is not None: - sock.close() - - if err is not None: - try: - raise err - finally: - # Break explicitly a reference cycle - err = None - else: - raise OSError("getaddrinfo returns an empty list") - - -def _set_socket_options( - sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None -) -> None: - if options is None: - return - - for opt in options: - sock.setsockopt(*opt) - - -def allowed_gai_family() -> socket.AddressFamily: - """This function is designed to work in the context of - getaddrinfo, where family=socket.AF_UNSPEC is the default and - will perform a DNS search for both IPv6 and IPv4 records.""" - - family = socket.AF_INET - if HAS_IPV6: - family = socket.AF_UNSPEC - return family - - -def _has_ipv6(host: str) -> bool: - """Returns True if the system can bind an IPv6 address.""" - sock = None - has_ipv6 = False - - if socket.has_ipv6: - # has_ipv6 returns true if cPython was compiled with IPv6 support. - # It does not tell us if the system has IPv6 support enabled. To - # determine that we must bind to an IPv6 address. - # https://github.com/urllib3/urllib3/pull/611 - # https://bugs.python.org/issue658327 - try: - sock = socket.socket(socket.AF_INET6) - sock.bind((host, 0)) - has_ipv6 = True - except Exception: - pass - - if sock: - sock.close() - return has_ipv6 - - -HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py deleted file mode 100644 index 908fc6621d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/proxy.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import typing - -from .url import Url - -if typing.TYPE_CHECKING: - from ..connection import ProxyConfig - - -def connection_requires_http_tunnel( - proxy_url: Url | None = None, - proxy_config: ProxyConfig | None = None, - destination_scheme: str | None = None, -) -> bool: - """ - Returns True if the connection requires an HTTP CONNECT through the proxy. - - :param URL proxy_url: - URL of the proxy. - :param ProxyConfig proxy_config: - Proxy configuration from poolmanager.py - :param str destination_scheme: - The scheme of the destination. (i.e https, http, etc) - """ - # If we're not using a proxy, no way to use a tunnel. - if proxy_url is None: - return False - - # HTTP destinations never require tunneling, we always forward. - if destination_scheme == "http": - return False - - # Support for forwarding with HTTPS proxies and HTTPS destinations. - if ( - proxy_url.scheme == "https" - and proxy_config - and proxy_config.use_forwarding_for_https - ): - return False - - # Otherwise always use a tunnel. - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py deleted file mode 100644 index 6c2372ba7e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/request.py +++ /dev/null @@ -1,263 +0,0 @@ -from __future__ import annotations - -import io -import sys -import typing -from base64 import b64encode -from enum import Enum - -from ..exceptions import UnrewindableBodyError -from .util import to_bytes - -if typing.TYPE_CHECKING: - from typing import Final - -# Pass as a value within ``headers`` to skip -# emitting some HTTP headers that are added automatically. -# The only headers that are supported are ``Accept-Encoding``, -# ``Host``, and ``User-Agent``. -SKIP_HEADER = "@@@SKIP_HEADER@@@" -SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) - -ACCEPT_ENCODING = "gzip,deflate" -try: - try: - import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 - except ImportError: - import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",br" - -try: - if sys.version_info >= (3, 14): - from compression import zstd as _unused_module_zstd # noqa: F401 - else: - from backports import zstd as _unused_module_zstd # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",zstd" - - -class _TYPE_FAILEDTELL(Enum): - token = 0 - - -_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token - -_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] - -# When sending a request with these methods we aren't expecting -# a body so don't need to set an explicit 'Content-Length: 0' -# The reason we do this in the negative instead of tracking methods -# which 'should' have a body is because unknown methods should be -# treated as if they were 'POST' which *does* expect a body. -_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} - - -def make_headers( - keep_alive: bool | None = None, - accept_encoding: bool | list[str] | str | None = None, - user_agent: str | None = None, - basic_auth: str | None = None, - proxy_basic_auth: str | None = None, - disable_cache: bool | None = None, -) -> dict[str, str]: - """ - Shortcuts for generating request headers. - - :param keep_alive: - If ``True``, adds 'connection: keep-alive' header. - - :param accept_encoding: - Can be a boolean, list, or string. - ``True`` translates to 'gzip,deflate'. If the dependencies for - Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or - Zstandard (the ``backports.zstd`` package for Python before 3.14) - algorithms are installed, then their encodings are - included in the string ('br' and 'zstd', respectively). - List will get joined by comma. - String will be used as provided. - - :param user_agent: - String representing the user-agent you want, such as - "python-urllib3/0.6" - - :param basic_auth: - Colon-separated username:password string for 'authorization: basic ...' - auth header. - - :param proxy_basic_auth: - Colon-separated username:password string for 'proxy-authorization: basic ...' - auth header. - - :param disable_cache: - If ``True``, adds 'cache-control: no-cache' header. - - Example: - - .. code-block:: python - - import urllib3 - - print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) - # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} - print(urllib3.util.make_headers(accept_encoding=True)) - # {'accept-encoding': 'gzip,deflate'} - """ - headers: dict[str, str] = {} - if accept_encoding: - if isinstance(accept_encoding, str): - pass - elif isinstance(accept_encoding, list): - accept_encoding = ",".join(accept_encoding) - else: - accept_encoding = ACCEPT_ENCODING - headers["accept-encoding"] = accept_encoding - - if user_agent: - headers["user-agent"] = user_agent - - if keep_alive: - headers["connection"] = "keep-alive" - - if basic_auth: - headers["authorization"] = ( - f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" - ) - - if proxy_basic_auth: - headers["proxy-authorization"] = ( - f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" - ) - - if disable_cache: - headers["cache-control"] = "no-cache" - - return headers - - -def set_file_position( - body: typing.Any, pos: _TYPE_BODY_POSITION | None -) -> _TYPE_BODY_POSITION | None: - """ - If a position is provided, move file to that point. - Otherwise, we'll attempt to record a position for future use. - """ - if pos is not None: - rewind_body(body, pos) - elif getattr(body, "tell", None) is not None: - try: - pos = body.tell() - except OSError: - # This differentiates from None, allowing us to catch - # a failed `tell()` later when trying to rewind the body. - pos = _FAILEDTELL - - return pos - - -def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: - """ - Attempt to rewind body to a certain position. - Primarily used for request redirects and retries. - - :param body: - File-like object that supports seek. - - :param int pos: - Position to seek to in file. - """ - body_seek = getattr(body, "seek", None) - if body_seek is not None and isinstance(body_pos, int): - try: - body_seek(body_pos) - except OSError as e: - raise UnrewindableBodyError( - "An error occurred when rewinding request body for redirect/retry." - ) from e - elif body_pos is _FAILEDTELL: - raise UnrewindableBodyError( - "Unable to record file position for rewinding " - "request body during a redirect/retry." - ) - else: - raise ValueError( - f"body_pos must be of type integer, instead it was {type(body_pos)}." - ) - - -class ChunksAndContentLength(typing.NamedTuple): - chunks: typing.Iterable[bytes] | None - content_length: int | None - - -def body_to_chunks( - body: typing.Any | None, method: str, blocksize: int -) -> ChunksAndContentLength: - """Takes the HTTP request method, body, and blocksize and - transforms them into an iterable of chunks to pass to - socket.sendall() and an optional 'Content-Length' header. - - A 'Content-Length' of 'None' indicates the length of the body - can't be determined so should use 'Transfer-Encoding: chunked' - for framing instead. - """ - - chunks: typing.Iterable[bytes] | None - content_length: int | None - - # No body, we need to make a recommendation on 'Content-Length' - # based on whether that request method is expected to have - # a body or not. - if body is None: - chunks = None - if method.upper() not in _METHODS_NOT_EXPECTING_BODY: - content_length = 0 - else: - content_length = None - - # Bytes or strings become bytes - elif isinstance(body, (str, bytes)): - chunks = (to_bytes(body),) - content_length = len(chunks[0]) - - # File-like object, TODO: use seek() and tell() for length? - elif hasattr(body, "read"): - - def chunk_readable() -> typing.Iterable[bytes]: - encode = isinstance(body, io.TextIOBase) - while True: - datablock = body.read(blocksize) - if not datablock: - break - if encode: - datablock = datablock.encode("utf-8") - yield datablock - - chunks = chunk_readable() - content_length = None - - # Otherwise we need to start checking via duck-typing. - else: - try: - # Check if the body implements the buffer API. - mv = memoryview(body) - except TypeError: - try: - # Check if the body is an iterable - chunks = iter(body) - content_length = None - except TypeError: - raise TypeError( - f"'body' must be a bytes-like object, file-like " - f"object, or iterable. Instead was {body!r}" - ) from None - else: - # Since it implements the buffer API can be passed directly to socket.sendall() - chunks = (body,) - content_length = mv.nbytes - - return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py deleted file mode 100644 index 0f4578696f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/response.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import http.client as httplib -from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect - -from ..exceptions import HeaderParsingError - - -def is_fp_closed(obj: object) -> bool: - """ - Checks whether a given file-like object is closed. - - :param obj: - The file-like object to check. - """ - - try: - # Check `isclosed()` first, in case Python3 doesn't set `closed`. - # GH Issue #928 - return obj.isclosed() # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check via the official file-like-object way. - return obj.closed # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check if the object is a container for another file-like object that - # gets released on exhaustion (e.g. HTTPResponse). - return obj.fp is None # type: ignore[attr-defined] - except AttributeError: - pass - - raise ValueError("Unable to determine whether fp is closed.") - - -def assert_header_parsing(headers: httplib.HTTPMessage) -> None: - """ - Asserts whether all headers have been successfully parsed. - Extracts encountered errors from the result of parsing headers. - - Only works on Python 3. - - :param http.client.HTTPMessage headers: Headers to verify. - - :raises urllib3.exceptions.HeaderParsingError: - If parsing errors are found. - """ - - # This will fail silently if we pass in the wrong kind of parameter. - # To make debugging easier add an explicit check. - if not isinstance(headers, httplib.HTTPMessage): - raise TypeError(f"expected httplib.Message, got {type(headers)}.") - - unparsed_data = None - - # get_payload is actually email.message.Message.get_payload; - # we're only interested in the result if it's not a multipart message - if not headers.is_multipart(): - payload = headers.get_payload() - - if isinstance(payload, (bytes, str)): - unparsed_data = payload - - # httplib is assuming a response body is available - # when parsing headers even when httplib only sends - # header data to parse_headers() This results in - # defects on multipart responses in particular. - # See: https://github.com/urllib3/urllib3/issues/800 - - # So we ignore the following defects: - # - StartBoundaryNotFoundDefect: - # The claimed start boundary was never found. - # - MultipartInvariantViolationDefect: - # A message claimed to be a multipart but no subparts were found. - defects = [ - defect - for defect in headers.defects - if not isinstance( - defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) - ) - ] - - if defects or unparsed_data: - raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) - - -def is_response_to_head(response: httplib.HTTPResponse) -> bool: - """ - Checks whether the request of a response has been a HEAD-request. - - :param http.client.HTTPResponse response: - Response to check if the originating request - used 'HEAD' as a method. - """ - # FIXME: Can we do this somehow without accessing private httplib _method? - method_str = response._method # type: str # type: ignore[attr-defined] - return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py deleted file mode 100644 index 7649898e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/retry.py +++ /dev/null @@ -1,557 +0,0 @@ -from __future__ import annotations - -import email -import logging -import random -import re -import time -import typing -from itertools import takewhile -from types import TracebackType - -from ..exceptions import ( - ConnectTimeoutError, - InvalidHeader, - MaxRetryError, - ProtocolError, - ProxyError, - ReadTimeoutError, - ResponseError, -) -from .util import reraise - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from ..connectionpool import ConnectionPool - from ..response import BaseHTTPResponse - -log = logging.getLogger(__name__) - - -# Data structure for representing the metadata of requests that result in a retry. -class RequestHistory(typing.NamedTuple): - method: str | None - url: str | None - error: Exception | None - status: int | None - redirect_location: str | None - - -class Retry: - """Retry configuration. - - Each retry attempt will create a new Retry object with updated values, so - they can be safely reused. - - Retries can be defined as a default for a pool: - - .. code-block:: python - - retries = Retry(connect=5, read=2, redirect=5) - http = PoolManager(retries=retries) - response = http.request("GET", "https://example.com/") - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=Retry(10)) - - Retries can be disabled by passing ``False``: - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=False) - - Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless - retries are disabled, in which case the causing exception will be raised. - - :param int total: - Total number of retries to allow. Takes precedence over other counts. - - Set to ``None`` to remove this constraint and fall back on other - counts. - - Set to ``0`` to fail on the first retry. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int connect: - How many connection-related errors to retry on. - - These are errors raised before the request is sent to the remote server, - which we assume has not triggered the server to process the request. - - Set to ``0`` to fail on the first retry of this type. - - :param int read: - How many times to retry on read errors. - - These errors are raised after the request was sent to the server, so the - request may have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - :param int redirect: - How many redirects to perform. Limit this to avoid infinite redirect - loops. - - A redirect is a HTTP response with a status code 301, 302, 303, 307 or - 308. - - Set to ``0`` to fail on the first retry of this type. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int status: - How many times to retry on bad status codes. - - These are retries made on responses, where status code matches - ``status_forcelist``. - - Set to ``0`` to fail on the first retry of this type. - - :param int other: - How many times to retry on other errors. - - Other errors are errors that are not connect, read, redirect or status errors. - These errors might be raised after the request was sent to the server, so the - request might have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - If ``total`` is not set, it's a good idea to set this to 0 to account - for unexpected edge cases and avoid infinite retry loops. - - :param Collection allowed_methods: - Set of uppercased HTTP method verbs that we should retry on. - - By default, we only retry on methods which are considered to be - idempotent (multiple requests with the same parameters end with the - same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. - - Set to a ``None`` value to retry on any verb. - - :param Collection status_forcelist: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``allowed_methods`` - and the response status code is in ``status_forcelist``. - - By default, this is disabled with ``None``. - - :param float backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). urllib3 will sleep for:: - - {backoff factor} * (2 ** ({number of previous retries})) - - seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: - - random.uniform(0, {backoff jitter}) - - seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will - sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever - be longer than `backoff_max`. - - By default, backoff is disabled (factor set to 0). - - :param float backoff_max: - The maximum backoff time (in seconds) between retry attempts. - This value caps the computed backoff from `backoff_factor`. - - :param float backoff_jitter: - Random jitter amount (in seconds) added to the computed backoff. - Jitter is sampled uniformly from `0` to `backoff_jitter`. - - :param bool raise_on_redirect: Whether, if the number of redirects is - exhausted, to raise a MaxRetryError, or to return a response with a - response code in the 3xx range. - - :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: - whether we should raise an exception, or return a response, - if status falls in ``status_forcelist`` range and retries have - been exhausted. - - :param tuple history: The history of the request encountered during - each call to :meth:`~Retry.increment`. The list is in the order - the requests occurred. Each list item is of class :class:`RequestHistory`. - - :param bool respect_retry_after_header: - Whether to respect Retry-After header on status codes defined as - :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. - - :param Collection remove_headers_on_redirect: - Sequence of headers to remove from the request when a response - indicating a redirect is returned before firing off the redirected - request. - - :param int retry_after_max: Number of seconds to allow as the maximum for - Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. - Any Retry-After headers larger than this value will be limited to this - value. - """ - - #: Default methods to be used for ``allowed_methods`` - DEFAULT_ALLOWED_METHODS = frozenset( - ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] - ) - - #: Default status codes to be used for ``status_forcelist`` - RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - - #: Default headers to be used for ``remove_headers_on_redirect`` - DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( - ["Cookie", "Authorization", "Proxy-Authorization"] - ) - - #: Default maximum backoff time. - DEFAULT_BACKOFF_MAX = 120 - - # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. - #: Default maximum allowed value for Retry-After headers in seconds - DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 - - # Backward compatibility; assigned outside of the class. - DEFAULT: typing.ClassVar[Retry] - - def __init__( - self, - total: bool | int | None = 10, - connect: int | None = None, - read: int | None = None, - redirect: bool | int | None = None, - status: int | None = None, - other: int | None = None, - allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, - status_forcelist: typing.Collection[int] | None = None, - backoff_factor: float = 0, - backoff_max: float = DEFAULT_BACKOFF_MAX, - raise_on_redirect: bool = True, - raise_on_status: bool = True, - history: tuple[RequestHistory, ...] | None = None, - respect_retry_after_header: bool = True, - remove_headers_on_redirect: typing.Collection[ - str - ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, - backoff_jitter: float = 0.0, - retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, - ) -> None: - self.total = total - self.connect = connect - self.read = read - self.status = status - self.other = other - - if redirect is False or total is False: - redirect = 0 - raise_on_redirect = False - - self.redirect = redirect - self.status_forcelist = status_forcelist or set() - self.allowed_methods = allowed_methods - self.backoff_factor = backoff_factor - self.backoff_max = backoff_max - self.retry_after_max = retry_after_max - self.raise_on_redirect = raise_on_redirect - self.raise_on_status = raise_on_status - self.history = history or () - self.respect_retry_after_header = respect_retry_after_header - self.remove_headers_on_redirect = frozenset( - h.lower() for h in remove_headers_on_redirect - ) - self.backoff_jitter = backoff_jitter - - def new(self, **kw: typing.Any) -> Self: - params = dict( - total=self.total, - connect=self.connect, - read=self.read, - redirect=self.redirect, - status=self.status, - other=self.other, - allowed_methods=self.allowed_methods, - status_forcelist=self.status_forcelist, - backoff_factor=self.backoff_factor, - backoff_max=self.backoff_max, - retry_after_max=self.retry_after_max, - raise_on_redirect=self.raise_on_redirect, - raise_on_status=self.raise_on_status, - history=self.history, - remove_headers_on_redirect=self.remove_headers_on_redirect, - respect_retry_after_header=self.respect_retry_after_header, - backoff_jitter=self.backoff_jitter, - ) - - params.update(kw) - return type(self)(**params) # type: ignore[arg-type] - - @classmethod - def from_int( - cls, - retries: Retry | bool | int | None, - redirect: bool | int | None = True, - default: Retry | bool | int | None = None, - ) -> Retry: - """Backwards-compatibility for the old retries format.""" - if retries is None: - retries = default if default is not None else cls.DEFAULT - - if isinstance(retries, Retry): - return retries - - redirect = bool(redirect) and None - new_retries = cls(retries, redirect=redirect) - log.debug("Converted retries value: %r -> %r", retries, new_retries) - return new_retries - - def get_backoff_time(self) -> float: - """Formula for computing the current backoff - - :rtype: float - """ - # We want to consider only the last consecutive errors sequence (Ignore redirects). - consecutive_errors_len = len( - list( - takewhile(lambda x: x.redirect_location is None, reversed(self.history)) - ) - ) - if consecutive_errors_len <= 1: - return 0 - - backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) - if self.backoff_jitter != 0.0: - backoff_value += random.random() * self.backoff_jitter - return float(max(0, min(self.backoff_max, backoff_value))) - - def parse_retry_after(self, retry_after: str) -> float: - seconds: float - # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 - if re.match(r"^\s*[0-9]+\s*$", retry_after): - seconds = int(retry_after) - else: - retry_date_tuple = email.utils.parsedate_tz(retry_after) - if retry_date_tuple is None: - raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") - - retry_date = email.utils.mktime_tz(retry_date_tuple) - seconds = retry_date - time.time() - - seconds = max(seconds, 0) - - # Check the seconds do not exceed the specified maximum - if seconds > self.retry_after_max: - seconds = self.retry_after_max - - return seconds - - def get_retry_after(self, response: BaseHTTPResponse) -> float | None: - """Get the value of Retry-After in seconds.""" - - retry_after = response.headers.get("Retry-After") - - if retry_after is None: - return None - - return self.parse_retry_after(retry_after) - - def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: - retry_after = self.get_retry_after(response) - if retry_after: - time.sleep(retry_after) - return True - - return False - - def _sleep_backoff(self) -> None: - backoff = self.get_backoff_time() - if backoff <= 0: - return - time.sleep(backoff) - - def sleep(self, response: BaseHTTPResponse | None = None) -> None: - """Sleep between retry attempts. - - This method will respect a server's ``Retry-After`` response header - and sleep the duration of the time requested. If that is not present, it - will use an exponential backoff. By default, the backoff factor is 0 and - this method will return immediately. - """ - - if self.respect_retry_after_header and response: - slept = self.sleep_for_retry(response) - if slept: - return - - self._sleep_backoff() - - def _is_connection_error(self, err: Exception) -> bool: - """Errors when we're fairly sure that the server did not receive the - request, so it should be safe to retry. - """ - if isinstance(err, ProxyError): - err = err.original_error - return isinstance(err, ConnectTimeoutError) - - def _is_read_error(self, err: Exception) -> bool: - """Errors that occur after the request has been started, so we should - assume that the server began processing it. - """ - return isinstance(err, (ReadTimeoutError, ProtocolError)) - - def _is_method_retryable(self, method: str) -> bool: - """Checks if a given HTTP method should be retried upon, depending if - it is included in the allowed_methods - """ - if self.allowed_methods and method.upper() not in self.allowed_methods: - return False - return True - - def is_retry( - self, method: str, status_code: int, has_retry_after: bool = False - ) -> bool: - """Is this method/status code retryable? (Based on allowlists and control - variables such as the number of total retries to allow, whether to - respect the Retry-After header, whether this header is present, and - whether the returned status code is on the list of status codes to - be retried upon on the presence of the aforementioned header) - """ - if not self._is_method_retryable(method): - return False - - if self.status_forcelist and status_code in self.status_forcelist: - return True - - return bool( - self.total - and self.respect_retry_after_header - and has_retry_after - and (status_code in self.RETRY_AFTER_STATUS_CODES) - ) - - def is_exhausted(self) -> bool: - """Are we out of retries?""" - retry_counts = [ - x - for x in ( - self.total, - self.connect, - self.read, - self.redirect, - self.status, - self.other, - ) - if x - ] - if not retry_counts: - return False - - return min(retry_counts) < 0 - - def increment( - self, - method: str | None = None, - url: str | None = None, - response: BaseHTTPResponse | None = None, - error: Exception | None = None, - _pool: ConnectionPool | None = None, - _stacktrace: TracebackType | None = None, - ) -> Self: - """Return a new Retry object with incremented retry counters. - - :param response: A response object, or None, if the server did not - return a response. - :type response: :class:`~urllib3.response.BaseHTTPResponse` - :param Exception error: An error encountered during the request, or - None if the response was received successfully. - - :return: A new ``Retry`` object. - """ - if self.total is False and error: - # Disabled, indicate to re-raise the error. - raise reraise(type(error), error, _stacktrace) - - total = self.total - if total is not None: - total -= 1 - - connect = self.connect - read = self.read - redirect = self.redirect - status_count = self.status - other = self.other - cause = "unknown" - status = None - redirect_location = None - - if error and self._is_connection_error(error): - # Connect retry? - if connect is False: - raise reraise(type(error), error, _stacktrace) - elif connect is not None: - connect -= 1 - - elif error and self._is_read_error(error): - # Read retry? - if read is False or method is None or not self._is_method_retryable(method): - raise reraise(type(error), error, _stacktrace) - elif read is not None: - read -= 1 - - elif error: - # Other retry? - if other is not None: - other -= 1 - - elif response and response.get_redirect_location(): - # Redirect retry? - if redirect is not None: - redirect -= 1 - cause = "too many redirects" - response_redirect_location = response.get_redirect_location() - if response_redirect_location: - redirect_location = response_redirect_location - status = response.status - - else: - # Incrementing because of a server error like a 500 in - # status_forcelist and the given method is in the allowed_methods - cause = ResponseError.GENERIC_ERROR - if response and response.status: - if status_count is not None: - status_count -= 1 - cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) - status = response.status - - history = self.history + ( - RequestHistory(method, url, error, status, redirect_location), - ) - - new_retry = self.new( - total=total, - connect=connect, - read=read, - redirect=redirect, - status=status_count, - other=other, - history=history, - ) - - if new_retry.is_exhausted(): - reason = error or ResponseError(cause) - raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] - - log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) - - return new_retry - - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(total={self.total}, connect={self.connect}, " - f"read={self.read}, redirect={self.redirect}, status={self.status})" - ) - - -# For backwards compatibility (equivalent to pre-v1.9): -Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py deleted file mode 100644 index e66549a76c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_.py +++ /dev/null @@ -1,477 +0,0 @@ -from __future__ import annotations - -import hashlib -import hmac -import os -import socket -import sys -import typing -import warnings -from binascii import unhexlify - -from ..exceptions import ProxySchemeUnsupported, SSLError -from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE - -SSLContext = None -SSLTransport = None -HAS_NEVER_CHECK_COMMON_NAME = False -IS_PYOPENSSL = False -ALPN_PROTOCOLS = ["http/1.1"] - -_TYPE_VERSION_INFO = tuple[int, int, int, str, int] - -# Maps the length of a digest to a possible hash function producing this digest -HASHFUNC_MAP = { - length: getattr(hashlib, algorithm, None) - for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) -} - - -def _is_has_never_check_common_name_reliable( - openssl_version: str, -) -> bool: - # As of May 2023, all released versions of LibreSSL fail to reject certificates with - # only common names, see https://github.com/urllib3/urllib3/pull/3024 - is_openssl = openssl_version.startswith("OpenSSL ") - - return is_openssl - - -if typing.TYPE_CHECKING: - from ssl import VerifyMode - from typing import TypedDict - - from .ssltransport import SSLTransport as SSLTransportType - - class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): - subjectAltName: tuple[tuple[str, str], ...] - subject: tuple[tuple[tuple[str, str], ...], ...] - serialNumber: str - - -# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' -_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} - -try: # Do we have ssl at all? - import ssl - from ssl import ( # type: ignore[assignment] - CERT_REQUIRED, - HAS_NEVER_CHECK_COMMON_NAME, - OP_NO_COMPRESSION, - OP_NO_TICKET, - OPENSSL_VERSION, - PROTOCOL_TLS, - PROTOCOL_TLS_CLIENT, - VERIFY_X509_PARTIAL_CHAIN, - VERIFY_X509_STRICT, - OP_NO_SSLv2, - OP_NO_SSLv3, - SSLContext, - TLSVersion, - ) - - PROTOCOL_SSLv23 = PROTOCOL_TLS - - # Setting SSLContext.hostname_checks_common_name = False didn't work with - # LibreSSL, check details in the used function. - if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( - OPENSSL_VERSION, - ): # Defensive: - HAS_NEVER_CHECK_COMMON_NAME = False - - # Need to be careful here in case old TLS versions get - # removed in future 'ssl' module implementations. - for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): - try: - _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( - TLSVersion, attr - ) - except AttributeError: # Defensive: - continue - - from .ssltransport import SSLTransport # type: ignore[assignment] -except ImportError: - OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] - OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] - OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] - OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] - PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] - PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] - VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] - VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] - - -_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] - - -def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: - """ - Checks if given fingerprint matches the supplied certificate. - - :param cert: - Certificate as bytes object. - :param fingerprint: - Fingerprint as string of hexdigits, can be interspersed by colons. - """ - - if cert is None: - raise SSLError("No certificate for the peer.") - - fingerprint = fingerprint.replace(":", "").lower() - digest_length = len(fingerprint) - if digest_length not in HASHFUNC_MAP: - raise SSLError(f"Fingerprint of invalid length: {fingerprint}") - hashfunc = HASHFUNC_MAP.get(digest_length) - if hashfunc is None: - raise SSLError( - f"Hash function implementation unavailable for fingerprint length: {digest_length}" - ) - - # We need encode() here for py32; works on py2 and p33. - fingerprint_bytes = unhexlify(fingerprint.encode()) - - cert_digest = hashfunc(cert).digest() - - if not hmac.compare_digest(cert_digest, fingerprint_bytes): - raise SSLError( - f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' - ) - - -def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: - """ - Resolves the argument to a numeric constant, which can be passed to - the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_REQUIRED`. - If given a string it is assumed to be the name of the constant in the - :mod:`ssl` module or its abbreviation. - (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. - If it's neither `None` nor a string we assume it is already the numeric - constant which can directly be passed to wrap_socket. - """ - if candidate is None: - return CERT_REQUIRED - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "CERT_" + candidate) - return res # type: ignore[no-any-return] - - return candidate # type: ignore[return-value] - - -def resolve_ssl_version(candidate: None | int | str) -> int: - """ - like resolve_cert_reqs - """ - if candidate is None: - return PROTOCOL_TLS - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "PROTOCOL_" + candidate) - return typing.cast(int, res) - - return candidate - - -def create_urllib3_context( - ssl_version: int | None = None, - cert_reqs: int | None = None, - options: int | None = None, - ciphers: str | None = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - verify_flags: int | None = None, -) -> ssl.SSLContext: - """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. - - :param ssl_version: - The desired protocol version to use. This will default to - PROTOCOL_SSLv23 which will negotiate the highest protocol that both - the server and your installation of OpenSSL support. - - This parameter is deprecated instead use 'ssl_minimum_version'. - :param ssl_minimum_version: - The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - :param ssl_maximum_version: - The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the - default value. - :param cert_reqs: - Whether to require the certificate verification. This defaults to - ``ssl.CERT_REQUIRED``. - :param options: - Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, - ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. - :param ciphers: - Which cipher suites to allow the server to select. Defaults to either system configured - ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. - :param verify_flags: - The flags for certificate verification operations. These default to - ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. - :returns: - Constructed SSLContext object with specified options - :rtype: SSLContext - """ - if SSLContext is None: - raise TypeError("Can't create an SSLContext object without an ssl module") - - # This means 'ssl_version' was specified as an exact value. - if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): - # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' - # to avoid conflicts. - if ssl_minimum_version is not None or ssl_maximum_version is not None: - raise ValueError( - "Can't specify both 'ssl_version' and either " - "'ssl_minimum_version' or 'ssl_maximum_version'" - ) - - # 'ssl_version' is deprecated and will be removed in the future. - else: - # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. - ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MINIMUM_SUPPORTED - ) - ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MAXIMUM_SUPPORTED - ) - - # This warning message is pushing users to use 'ssl_minimum_version' - # instead of both min/max. Best practice is to only set the minimum version and - # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' - warnings.warn( - "'ssl_version' option is deprecated and will be " - "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", - category=FutureWarning, - stacklevel=2, - ) - - context = SSLContext(PROTOCOL_TLS_CLIENT) - if ssl_minimum_version is not None: - context.minimum_version = ssl_minimum_version - else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here - context.minimum_version = TLSVersion.TLSv1_2 - - if ssl_maximum_version is not None: - context.maximum_version = ssl_maximum_version - - # Unless we're given ciphers defer to either system ciphers in - # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. - if ciphers: - context.set_ciphers(ciphers) - - # Setting the default here, as we may have no ssl module on import - cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if options is None: - options = 0 - # SSLv2 is easily broken and is considered harmful and dangerous - options |= OP_NO_SSLv2 - # SSLv3 has several problems and is now dangerous - options |= OP_NO_SSLv3 - # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ - # (issue #309) - options |= OP_NO_COMPRESSION - # TLSv1.2 only. Unless set explicitly, do not request tickets. - # This may save some bandwidth on wire, and although the ticket is encrypted, - # there is a risk associated with it being on wire, - # if the server is not rotating its ticketing keys properly. - options |= OP_NO_TICKET - - context.options |= options - - if verify_flags is None: - verify_flags = 0 - # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN - # and VERIFY_X509_STRICT so we do the same - if sys.version_info >= (3, 13): - verify_flags |= VERIFY_X509_PARTIAL_CHAIN - verify_flags |= VERIFY_X509_STRICT - - context.verify_flags |= verify_flags - - # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is - # necessary for conditional client cert authentication with TLS 1.3. - # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using - # an SSLContext created by pyOpenSSL. - if getattr(context, "post_handshake_auth", None) is not None: - context.post_handshake_auth = True - - # The order of the below lines setting verify_mode and check_hostname - # matter due to safe-guards SSLContext has to prevent an SSLContext with - # check_hostname=True, verify_mode=NONE/OPTIONAL. - # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own - # 'ssl.match_hostname()' implementation. - if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: - context.verify_mode = cert_reqs - context.check_hostname = True - else: - context.check_hostname = False - context.verify_mode = cert_reqs - - context.hostname_checks_common_name = False - - if "SSLKEYLOGFILE" in os.environ: - sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) - else: - sslkeylogfile = None - if sslkeylogfile: - context.keylog_filename = sslkeylogfile - - return context - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: typing.Literal[False] = ..., -) -> ssl.SSLSocket: ... - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: bool = ..., -) -> ssl.SSLSocket | SSLTransportType: ... - - -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = None, - certfile: str | None = None, - cert_reqs: int | None = None, - ca_certs: str | None = None, - server_hostname: str | None = None, - ssl_version: int | None = None, - ciphers: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_cert_dir: str | None = None, - key_password: str | None = None, - ca_cert_data: None | str | bytes = None, - tls_in_tls: bool = False, -) -> ssl.SSLSocket | SSLTransportType: - """ - All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and - ca_cert_dir have the same meaning as they do when using - :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, - :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. - - :param server_hostname: - When SNI is supported, the expected hostname of the certificate - :param ssl_context: - A pre-made :class:`SSLContext` object. If none is provided, one will - be created using :func:`create_urllib3_context`. - :param ciphers: - A string of ciphers we wish the client to support. - :param ca_cert_dir: - A directory containing CA certificates in multiple separate files, as - supported by OpenSSL's -CApath flag or the capath argument to - SSLContext.load_verify_locations(). - :param key_password: - Optional password if the keyfile is encrypted. - :param ca_cert_data: - Optional string containing CA certificates in PEM format suitable for - passing as the cadata parameter to SSLContext.load_verify_locations() - :param tls_in_tls: - Use SSLTransport to wrap the existing socket. - """ - context = ssl_context - if context is None: - # Note: This branch of code and all the variables in it are only used in tests. - # We should consider deprecating and removing this code. - context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) - - if ca_certs or ca_cert_dir or ca_cert_data: - try: - context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) - except OSError as e: - raise SSLError(e) from e - - elif ssl_context is None and hasattr(context, "load_default_certs"): - # try to load OS default certs; works well on Windows. - context.load_default_certs() - - # Attempt to detect if we get the goofy behavior of the - # keyfile being encrypted and OpenSSL asking for the - # passphrase via the terminal and instead error out. - if keyfile and key_password is None and _is_key_file_encrypted(keyfile): - raise SSLError("Client private key is encrypted, password is required") - - if certfile: - if key_password is None: - context.load_cert_chain(certfile, keyfile) - else: - context.load_cert_chain(certfile, keyfile, key_password) - - context.set_alpn_protocols(ALPN_PROTOCOLS) - - ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) - return ssl_sock - - -def is_ipaddress(hostname: str | bytes) -> bool: - """Detects whether the hostname given is an IPv4 or IPv6 address. - Also detects IPv6 addresses with Zone IDs. - - :param str hostname: Hostname to examine. - :return: True if the hostname is an IP address, False otherwise. - """ - if isinstance(hostname, bytes): - # IDN A-label bytes are ASCII compatible. - hostname = hostname.decode("ascii") - return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) - - -def _is_key_file_encrypted(key_file: str) -> bool: - """Detects if a key file is encrypted or not.""" - with open(key_file) as f: - for line in f: - # Look for Proc-Type: 4,ENCRYPTED - if "ENCRYPTED" in line: - return True - - return False - - -def _ssl_wrap_socket_impl( - sock: socket.socket, - ssl_context: ssl.SSLContext, - tls_in_tls: bool, - server_hostname: str | None = None, -) -> ssl.SSLSocket | SSLTransportType: - if tls_in_tls: - if not SSLTransport: - # Import error, ssl is not available. - raise ProxySchemeUnsupported( - "TLS in TLS requires support for the 'ssl' module" - ) - - SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) - return SSLTransport(sock, ssl_context, server_hostname) - - return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py deleted file mode 100644 index 94994f25ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssl_match_hostname.py +++ /dev/null @@ -1,153 +0,0 @@ -"""The match_hostname() function from Python 3.5, essential when using SSL.""" - -# Note: This file is under the PSF license as the code comes from the python -# stdlib. http://docs.python.org/3/license.html -# It is modified to remove commonName support. - -from __future__ import annotations - -import ipaddress -import re -import typing -from ipaddress import IPv4Address, IPv6Address - -if typing.TYPE_CHECKING: - from .ssl_ import _TYPE_PEER_CERT_RET_DICT - -__version__ = "3.5.0.1" - - -class CertificateError(ValueError): - pass - - -def _dnsname_match( - dn: typing.Any, hostname: str, max_wildcards: int = 1 -) -> typing.Match[str] | None | bool: - """Matching according to RFC 6125, section 6.4.3 - - http://tools.ietf.org/html/rfc6125#section-6.4.3 - """ - pats = [] - if not dn: - return False - - # Ported from python3-syntax: - # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r".") - leftmost = parts[0] - remainder = parts[1:] - - wildcards = leftmost.count("*") - if wildcards > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survey of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn) - ) - - # speed up common case w/o wildcards - if not wildcards: - return bool(dn.lower() == hostname.lower()) - - # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. - if leftmost == "*": - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append("[^.]+") - elif leftmost.startswith("xn--") or hostname.startswith("xn--"): - # RFC 6125, section 6.4.3, subitem 3. - # The client SHOULD NOT attempt to match a presented identifier - # where the wildcard character is embedded within an A-label or - # U-label of an internationalized domain name. - pats.append(re.escape(leftmost)) - else: - # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) - - # add the remaining fragments, ignore any wildcards - for frag in remainder: - pats.append(re.escape(frag)) - - pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) - return pat.match(hostname) - - -def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: - """Exact matching of IP addresses. - - RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded - bytes of the IP address. An IP version 4 address is 4 octets, and an IP - version 6 address is 16 octets. [...] A reference identity of type IP-ID - matches if the address is identical to an iPAddress value of the - subjectAltName extension of the certificate." - """ - # OpenSSL may add a trailing newline to a subjectAltName's IP address - # Divergence from upstream: ipaddress can't handle byte str - ip = ipaddress.ip_address(ipname.rstrip()) - return bool(ip.packed == host_ip.packed) - - -def match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError( - "empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED" - ) - - try: - host_ip = ipaddress.ip_address(hostname) - except ValueError: - # Not an IP address (common case) - host_ip = None - dnsnames = [] - san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) - key: str - value: str - for key, value in san: - if key == "DNS": - if host_ip is None and _dnsname_match(value, hostname): - return - dnsnames.append(value) - elif key == "IP Address": - if host_ip is not None and _ipaddress_match(value, host_ip): - return - dnsnames.append(value) - - # We only check 'commonName' if it's enabled and we're not verifying - # an IP address. IP addresses aren't valid within 'commonName'. - if hostname_checks_common_name and host_ip is None and not dnsnames: - for sub in cert.get("subject", ()): - for key, value in sub: - if key == "commonName": - if _dnsname_match(value, hostname): - return - dnsnames.append( - value - ) # Defensive: for older PyPy and OpenSSL versions - - if len(dnsnames) > 1: - raise CertificateError( - "hostname %r " - "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) - ) - elif len(dnsnames) == 1: - raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") - else: - raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py deleted file mode 100644 index 6d59bc3bce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/ssltransport.py +++ /dev/null @@ -1,271 +0,0 @@ -from __future__ import annotations - -import io -import socket -import ssl -import typing - -from ..exceptions import ProxySchemeUnsupported - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT - - -_WriteBuffer = typing.Union[bytearray, memoryview] -_ReturnValue = typing.TypeVar("_ReturnValue") - -SSL_BLOCKSIZE = 16384 - - -class SSLTransport: - """ - The SSLTransport wraps an existing socket and establishes an SSL connection. - - Contrary to Python's implementation of SSLSocket, it allows you to chain - multiple TLS connections together. It's particularly useful if you need to - implement TLS within TLS. - - The class supports most of the socket API operations. - """ - - @staticmethod - def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: - """ - Raises a ProxySchemeUnsupported if the provided ssl_context can't be used - for TLS in TLS. - - The only requirement is that the ssl_context provides the 'wrap_bio' - methods. - """ - - if not hasattr(ssl_context, "wrap_bio"): - raise ProxySchemeUnsupported( - "TLS in TLS requires SSLContext.wrap_bio() which isn't " - "available on non-native SSLContext" - ) - - def __init__( - self, - socket: socket.socket, - ssl_context: ssl.SSLContext, - server_hostname: str | None = None, - suppress_ragged_eofs: bool = True, - ) -> None: - """ - Create an SSLTransport around socket using the provided ssl_context. - """ - self.incoming = ssl.MemoryBIO() - self.outgoing = ssl.MemoryBIO() - - self.suppress_ragged_eofs = suppress_ragged_eofs - self.socket = socket - - self.sslobj = ssl_context.wrap_bio( - self.incoming, self.outgoing, server_hostname=server_hostname - ) - - # Perform initial handshake. - self._ssl_io_loop(self.sslobj.do_handshake) - - def __enter__(self) -> Self: - return self - - def __exit__(self, *_: typing.Any) -> None: - self.close() - - def fileno(self) -> int: - return self.socket.fileno() - - def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: - return self._wrap_ssl_read(len, buffer) - - def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv") - return self._wrap_ssl_read(buflen) - - def recv_into( - self, - buffer: _WriteBuffer, - nbytes: int | None = None, - flags: int = 0, - ) -> None | int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv_into") - if nbytes is None: - nbytes = len(buffer) - return self.read(nbytes, buffer) - - def sendall(self, data: bytes, flags: int = 0) -> None: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to sendall") - count = 0 - with memoryview(data) as view, view.cast("B") as byte_view: - amount = len(byte_view) - while count < amount: - v = self.send(byte_view[count:]) - count += v - - def send(self, data: bytes, flags: int = 0) -> int: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to send") - return self._ssl_io_loop(self.sslobj.write, data) - - def makefile( - self, - mode: str, - buffering: int | None = None, - *, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: - """ - Python's httpclient uses makefile and buffered io when reading HTTP - messages and we need to support it. - - This is unfortunately a copy and paste of socket.py makefile with small - changes to point to the socket directly. - """ - if not set(mode) <= {"r", "w", "b"}: - raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") - - writing = "w" in mode - reading = "r" in mode or not writing - assert reading or writing - binary = "b" in mode - rawmode = "" - if reading: - rawmode += "r" - if writing: - rawmode += "w" - raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] - self.socket._io_refs += 1 # type: ignore[attr-defined] - if buffering is None: - buffering = -1 - if buffering < 0: - buffering = io.DEFAULT_BUFFER_SIZE - if buffering == 0: - if not binary: - raise ValueError("unbuffered streams must be binary") - return raw - buffer: typing.BinaryIO - if reading and writing: - buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] - elif reading: - buffer = io.BufferedReader(raw, buffering) - else: - assert writing - buffer = io.BufferedWriter(raw, buffering) - if binary: - return buffer - text = io.TextIOWrapper(buffer, encoding, errors, newline) - text.mode = mode # type: ignore[misc] - return text - - def unwrap(self) -> None: - self._ssl_io_loop(self.sslobj.unwrap) - - def close(self) -> None: - self.socket.close() - - @typing.overload - def getpeercert( - self, binary_form: typing.Literal[False] = ... - ) -> _TYPE_PEER_CERT_RET_DICT | None: ... - - @typing.overload - def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... - - def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: - return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] - - def version(self) -> str | None: - return self.sslobj.version() - - def cipher(self) -> tuple[str, str, int] | None: - return self.sslobj.cipher() - - def selected_alpn_protocol(self) -> str | None: - return self.sslobj.selected_alpn_protocol() - - def shared_ciphers(self) -> list[tuple[str, str, int]] | None: - return self.sslobj.shared_ciphers() - - def compression(self) -> str | None: - return self.sslobj.compression() - - def settimeout(self, value: float | None) -> None: - self.socket.settimeout(value) - - def gettimeout(self) -> float | None: - return self.socket.gettimeout() - - def _decref_socketios(self) -> None: - self.socket._decref_socketios() # type: ignore[attr-defined] - - def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: - try: - return self._ssl_io_loop(self.sslobj.read, len, buffer) - except ssl.SSLError as e: - if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: - return 0 # eof, return 0. - else: - raise - - # func is sslobj.do_handshake or sslobj.unwrap - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... - - # func is sslobj.write, arg1 is data - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... - - # func is sslobj.read, arg1 is len, arg2 is buffer - @typing.overload - def _ssl_io_loop( - self, - func: typing.Callable[[int, bytearray | None], bytes], - arg1: int, - arg2: bytearray | None, - ) -> bytes: ... - - def _ssl_io_loop( - self, - func: typing.Callable[..., _ReturnValue], - arg1: None | bytes | int = None, - arg2: bytearray | None = None, - ) -> _ReturnValue: - """Performs an I/O loop between incoming/outgoing and the socket.""" - should_loop = True - ret = None - - while should_loop: - errno = None - try: - if arg1 is None and arg2 is None: - ret = func() - elif arg2 is None: - ret = func(arg1) - else: - ret = func(arg1, arg2) - except ssl.SSLError as e: - if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): - # WANT_READ, and WANT_WRITE are expected, others are not. - raise e - errno = e.errno - - buf = self.outgoing.read() - self.socket.sendall(buf) - - if errno is None: - should_loop = False - elif errno == ssl.SSL_ERROR_WANT_READ: - buf = self.socket.recv(SSL_BLOCKSIZE) - if buf: - self.incoming.write(buf) - else: - self.incoming.write_eof() - return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py deleted file mode 100644 index 4bb1be11d9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/timeout.py +++ /dev/null @@ -1,275 +0,0 @@ -from __future__ import annotations - -import time -import typing -from enum import Enum -from socket import getdefaulttimeout - -from ..exceptions import TimeoutStateError - -if typing.TYPE_CHECKING: - from typing import Final - - -class _TYPE_DEFAULT(Enum): - # This value should never be passed to socket.settimeout() so for safety we use a -1. - # socket.settimout() raises a ValueError for negative values. - token = -1 - - -_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token - -_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] - - -class Timeout: - """Timeout configuration. - - Timeouts can be defined as a default for a pool: - - .. code-block:: python - - import urllib3 - - timeout = urllib3.util.Timeout(connect=2.0, read=7.0) - - http = urllib3.PoolManager(timeout=timeout) - - resp = http.request("GET", "https://example.com/") - - print(resp.status) - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", timeout=Timeout(10)) - - Timeouts can be disabled by setting all the parameters to ``None``: - - .. code-block:: python - - no_timeout = Timeout(connect=None, read=None) - response = http.request("GET", "https://example.com/", timeout=no_timeout) - - - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: int, float, or None - - :param connect: - The maximum amount of time (in seconds) to wait for a connection - attempt to a server to succeed. Omitting the parameter will default the - connect timeout to the system default, probably `the global default - timeout in socket.py - `_. - None will set an infinite timeout for connection attempts. - - :type connect: int, float, or None - - :param read: - The maximum amount of time (in seconds) to wait between consecutive - read operations for a response from the server. Omitting the parameter - will default the read timeout to the system default, probably `the - global default timeout in socket.py - `_. - None will set an infinite timeout. - - :type read: int, float, or None - - .. note:: - - Many factors can affect the total amount of time for urllib3 to return - an HTTP response. - - For example, Python's DNS resolver does not obey the timeout specified - on the socket. Other factors that can affect total request time include - high CPU load, high swap, the program running at a low priority level, - or other behaviors. - - In addition, the read and total timeouts only measure the time between - read operations on the socket connecting the client and the server, - not the total amount of time for the request to return a complete - response. For most requests, the timeout is raised because the server - has not sent the first byte in the specified time. This is not always - the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not trigger, even though the request will take - several minutes to complete. - """ - - #: A sentinel object representing the default timeout value - DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT - - def __init__( - self, - total: _TYPE_TIMEOUT = None, - connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - ) -> None: - self._connect = self._validate_timeout(connect, "connect") - self._read = self._validate_timeout(read, "read") - self.total = self._validate_timeout(total, "total") - self._start_connect: float | None = None - - def __repr__(self) -> str: - return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" - - # __str__ provided for backwards compatibility - __str__ = __repr__ - - @staticmethod - def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: - return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout - - @classmethod - def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: - """Check that a timeout attribute is valid. - - :param value: The timeout value to validate - :param name: The name of the timeout attribute to validate. This is - used to specify in error messages. - :return: The validated and casted version of the given value. - :raises ValueError: If it is a numeric value less than or equal to - zero, or the type is not an integer, float, or None. - """ - if value is None or value is _DEFAULT_TIMEOUT: - return value - - if isinstance(value, bool): - raise ValueError( - "Timeout cannot be a boolean value. It must " - "be an int, float or None." - ) - try: - float(value) - except (TypeError, ValueError): - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - try: - if value <= 0: - raise ValueError( - "Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than or equal to 0." % (name, value) - ) - except TypeError: - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - return value - - @classmethod - def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: - """Create a new Timeout from a legacy timeout value. - - The timeout value used by httplib.py sets the same timeout on the - connect(), and recv() socket requests. This creates a :class:`Timeout` - object that sets the individual timeouts to the ``timeout`` value - passed to this function. - - :param timeout: The legacy timeout value. - :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None - :return: Timeout object - :rtype: :class:`Timeout` - """ - return Timeout(read=timeout, connect=timeout) - - def clone(self) -> Timeout: - """Create a copy of the timeout object - - Timeout properties are stored per-pool but each request needs a fresh - Timeout object to ensure each one has its own start/stop configured. - - :return: a copy of the timeout object - :rtype: :class:`Timeout` - """ - # We can't use copy.deepcopy because that will also create a new object - # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to - # detect the user default. - return Timeout(connect=self._connect, read=self._read, total=self.total) - - def start_connect(self) -> float: - """Start the timeout clock, used during a connect() attempt - - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to start a timer that has been started already. - """ - if self._start_connect is not None: - raise TimeoutStateError("Timeout timer has already been started.") - self._start_connect = time.monotonic() - return self._start_connect - - def get_connect_duration(self) -> float: - """Gets the time elapsed since the call to :meth:`start_connect`. - - :return: Elapsed time in seconds. - :rtype: float - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to get duration for a timer that hasn't been started. - """ - if self._start_connect is None: - raise TimeoutStateError( - "Can't get connect duration for timer that has not started." - ) - return time.monotonic() - self._start_connect - - @property - def connect_timeout(self) -> _TYPE_TIMEOUT: - """Get the value to use when setting a connection timeout. - - This will be a positive float or integer, the value None - (never timeout), or the default system timeout. - - :return: Connect timeout. - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - """ - if self.total is None: - return self._connect - - if self._connect is None or self._connect is _DEFAULT_TIMEOUT: - return self.total - - return min(self._connect, self.total) # type: ignore[type-var] - - @property - def read_timeout(self) -> float | None: - """Get the value for the read timeout. - - This assumes some time has elapsed in the connection timeout and - computes the read timeout appropriately. - - If self.total is set, the read timeout is dependent on the amount of - time taken by the connect timeout. If the connection time has not been - established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be - raised. - - :return: Value to use for the read timeout. - :rtype: int, float or None - :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` - has not yet been called on this object. - """ - if ( - self.total is not None - and self.total is not _DEFAULT_TIMEOUT - and self._read is not None - and self._read is not _DEFAULT_TIMEOUT - ): - # In case the connect timeout has not yet been established. - if self._start_connect is None: - return self._read - return max(0, min(self.total - self.get_connect_duration(), self._read)) - elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: - return max(0, self.total - self.get_connect_duration()) - else: - return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py deleted file mode 100644 index db057f17be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/url.py +++ /dev/null @@ -1,469 +0,0 @@ -from __future__ import annotations - -import re -import typing - -from ..exceptions import LocationParseError -from .util import to_str - -# We only want to normalize urls with an HTTP(S) scheme. -# urllib3 infers URLs without a scheme (None) to be http. -_NORMALIZABLE_SCHEMES = ("http", "https", None) - -# Almost all of these patterns were derived from the -# 'rfc3986' module: https://github.com/python-hyper/rfc3986 -_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") -_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") -_URI_RE = re.compile( - r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" - r"(?://([^\\/?#]*))?" - r"([^?#]*)" - r"(?:\?([^#]*))?" - r"(?:#(.*))?$", - re.UNICODE | re.DOTALL, -) - -_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" -_HEX_PAT = "[0-9A-Fa-f]{1,4}" -_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) -_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} -_variations = [ - # 6( h16 ":" ) ls32 - "(?:%(hex)s:){6}%(ls32)s", - # "::" 5( h16 ":" ) ls32 - "::(?:%(hex)s:){5}%(ls32)s", - # [ h16 ] "::" 4( h16 ":" ) ls32 - "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", - # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 - "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", - # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 - "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", - # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 - "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", - # [ *4( h16 ":" ) h16 ] "::" ls32 - "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", - # [ *5( h16 ":" ) h16 ] "::" h16 - "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", - # [ *6( h16 ":" ) h16 ] "::" - "(?:(?:%(hex)s:){0,6}%(hex)s)?::", -] - -_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" -_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" -_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" -_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" -_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" -_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") - -_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") -_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") -_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") -_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") -_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") - -_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( - _REG_NAME_PAT, - _IPV4_PAT, - _IPV6_ADDRZ_PAT, -) -_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) - -_UNRESERVED_CHARS = set( - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" -) -_SUB_DELIM_CHARS = set("!$&'()*+,;=") -_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} -_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} -_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} - - -class Url( - typing.NamedTuple( - "Url", - [ - ("scheme", typing.Optional[str]), - ("auth", typing.Optional[str]), - ("host", typing.Optional[str]), - ("port", typing.Optional[int]), - ("path", typing.Optional[str]), - ("query", typing.Optional[str]), - ("fragment", typing.Optional[str]), - ], - ) -): - """ - Data structure for representing an HTTP URL. Used as a return value for - :func:`parse_url`. Both the scheme and host are normalized as they are - both case-insensitive according to RFC 3986. - """ - - def __new__( # type: ignore[no-untyped-def] - cls, - scheme: str | None = None, - auth: str | None = None, - host: str | None = None, - port: int | None = None, - path: str | None = None, - query: str | None = None, - fragment: str | None = None, - ): - if path and not path.startswith("/"): - path = "/" + path - if scheme is not None: - scheme = scheme.lower() - return super().__new__(cls, scheme, auth, host, port, path, query, fragment) - - @property - def hostname(self) -> str | None: - """For backwards-compatibility with urlparse. We're nice like that.""" - return self.host - - @property - def request_uri(self) -> str: - """Absolute path including the query string.""" - uri = self.path or "/" - - if self.query is not None: - uri += "?" + self.query - - return uri - - @property - def authority(self) -> str | None: - """ - Authority component as defined in RFC 3986 3.2. - This includes userinfo (auth), host and port. - - i.e. - userinfo@host:port - """ - userinfo = self.auth - netloc = self.netloc - if netloc is None or userinfo is None: - return netloc - else: - return f"{userinfo}@{netloc}" - - @property - def netloc(self) -> str | None: - """ - Network location including host and port. - - If you need the equivalent of urllib.parse's ``netloc``, - use the ``authority`` property instead. - """ - if self.host is None: - return None - if self.port: - return f"{self.host}:{self.port}" - return self.host - - @property - def url(self) -> str: - """ - Convert self into a url - - This function should more or less round-trip with :func:`.parse_url`. The - returned url may not be exactly the same as the url inputted to - :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls - with a blank port will have : removed). - - Example: - - .. code-block:: python - - import urllib3 - - U = urllib3.util.parse_url("https://google.com/mail/") - - print(U.url) - # "https://google.com/mail/" - - print( urllib3.util.Url("https", "username:password", - "host.com", 80, "/path", "query", "fragment" - ).url - ) - # "https://username:password@host.com:80/path?query#fragment" - """ - scheme, auth, host, port, path, query, fragment = self - url = "" - - # We use "is not None" we want things to happen with empty strings (or 0 port) - if scheme is not None: - url += scheme + "://" - if auth is not None: - url += auth + "@" - if host is not None: - url += host - if port is not None: - url += ":" + str(port) - if path is not None: - url += path - if query is not None: - url += "?" + query - if fragment is not None: - url += "#" + fragment - - return url - - def __str__(self) -> str: - return self.url - - -@typing.overload -def _encode_invalid_chars( - component: str, allowed_chars: typing.Container[str] -) -> str: # Abstract - ... - - -@typing.overload -def _encode_invalid_chars( - component: None, allowed_chars: typing.Container[str] -) -> None: # Abstract - ... - - -def _encode_invalid_chars( - component: str | None, allowed_chars: typing.Container[str] -) -> str | None: - """Percent-encodes a URI component without reapplying - onto an already percent-encoded component. - """ - if component is None: - return component - - component = to_str(component) - - # Normalize existing percent-encoded bytes. - # Try to see if the component we're encoding is already percent-encoded - # so we can skip all '%' characters but still encode all others. - component, percent_encodings = _PERCENT_RE.subn( - lambda match: match.group(0).upper(), component - ) - - uri_bytes = component.encode("utf-8", "surrogatepass") - is_percent_encoded = percent_encodings == uri_bytes.count(b"%") - encoded_component = bytearray() - - for i in range(0, len(uri_bytes)): - # Will return a single character bytestring - byte = uri_bytes[i : i + 1] - byte_ord = ord(byte) - if (is_percent_encoded and byte == b"%") or ( - byte_ord < 128 and byte.decode() in allowed_chars - ): - encoded_component += byte - continue - encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) - - return encoded_component.decode() - - -def _remove_path_dot_segments(path: str) -> str: - # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code - segments = path.split("/") # Turn the path into a list of segments - output = [] # Initialize the variable to use to store output - - for segment in segments: - # '.' is the current directory, so ignore it, it is superfluous - if segment == ".": - continue - # Anything other than '..', should be appended to the output - if segment != "..": - output.append(segment) - # In this case segment == '..', if we can, we should pop the last - # element - elif output: - output.pop() - - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if path.startswith("/") and (not output or output[0]): - output.insert(0, "") - - # If the path starts with '/.' or '/..' ensure we add one more empty - # string to add a trailing '/' - if path.endswith(("/.", "/..")): - output.append("") - - return "/".join(output) - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - if host: - if scheme in _NORMALIZABLE_SCHEMES: - is_ipv6 = _IPV6_ADDRZ_RE.match(host) - if is_ipv6: - # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as - # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID - # separator as necessary to return a valid RFC 4007 scoped IP. - match = _ZONE_ID_RE.search(host) - if match: - start, end = match.span(1) - zone_id = host[start:end] - - if zone_id.startswith("%25") and zone_id != "%25": - zone_id = zone_id[3:] - else: - zone_id = zone_id[1:] - zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) - return f"{host[:start].lower()}%{zone_id}{host[end:]}" - else: - return host.lower() - elif not _IPV4_RE.match(host): - return to_str( - b".".join([_idna_encode(label) for label in host.split(".")]), - "ascii", - ) - return host - - -def _idna_encode(name: str) -> bytes: - if not name.isascii(): - try: - import idna - except ImportError: - raise LocationParseError( - "Unable to parse URL without the 'idna' module" - ) from None - - try: - return idna.encode(name.lower(), strict=True, std3_rules=True) - except idna.IDNAError: - raise LocationParseError( - f"Name '{name}' is not a valid IDNA label" - ) from None - - return name.lower().encode("ascii") - - -def _encode_target(target: str) -> str: - """Percent-encodes a request target so that there are no invalid characters - - Pre-condition for this function is that 'target' must start with '/'. - If that is the case then _TARGET_RE will always produce a match. - """ - match = _TARGET_RE.match(target) - if not match: # Defensive: - raise LocationParseError(f"{target!r} is not a valid request URI") - - path, query = match.groups() - encoded_target = _encode_invalid_chars(path, _PATH_CHARS) - if query is not None: - query = _encode_invalid_chars(query, _QUERY_CHARS) - encoded_target += "?" + query - return encoded_target - - -def parse_url(url: str) -> Url: - """ - Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is - performed to parse incomplete urls. Fields not provided will be None. - This parser is RFC 3986 and RFC 6874 compliant. - - The parser logic and helper functions are based heavily on - work done in the ``rfc3986`` module. - - :param str url: URL to parse into a :class:`.Url` namedtuple. - - Partly backwards-compatible with :mod:`urllib.parse`. - - Example: - - .. code-block:: python - - import urllib3 - - print( urllib3.util.parse_url('http://google.com/mail/')) - # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) - - print( urllib3.util.parse_url('google.com:80')) - # Url(scheme=None, host='google.com', port=80, path=None, ...) - - print( urllib3.util.parse_url('/foo?bar')) - # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) - """ - if not url: - # Empty - return Url() - - source_url = url - if not _SCHEME_RE.search(url): - url = "//" + url - - scheme: str | None - authority: str | None - auth: str | None - host: str | None - port: str | None - port_int: int | None - path: str | None - query: str | None - fragment: str | None - - try: - scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] - normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES - - if scheme: - scheme = scheme.lower() - - if authority: - auth, _, host_port = authority.rpartition("@") - auth = auth or None - host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] - if auth and normalize_uri: - auth = _encode_invalid_chars(auth, _USERINFO_CHARS) - if port == "": - port = None - else: - auth, host, port = None, None, None - - if port is not None: - port_int = int(port) - if not (0 <= port_int <= 65535): - raise LocationParseError(url) - else: - port_int = None - - host = _normalize_host(host, scheme) - - if normalize_uri and path: - path = _remove_path_dot_segments(path) - path = _encode_invalid_chars(path, _PATH_CHARS) - if normalize_uri and query: - query = _encode_invalid_chars(query, _QUERY_CHARS) - if normalize_uri and fragment: - fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) - - except (ValueError, AttributeError) as e: - raise LocationParseError(source_url) from e - - # For the sake of backwards compatibility we put empty - # string values for path if there are any defined values - # beyond the path in the URL. - # TODO: Remove this when we break backwards compatibility. - if not path: - if query is not None or fragment is not None: - path = "" - else: - path = None - - return Url( - scheme=scheme, - auth=auth, - host=host, - port=port_int, - path=path, - query=query, - fragment=fragment, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py deleted file mode 100644 index 35c77e4025..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/util.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import typing -from types import TracebackType - - -def to_bytes( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> bytes: - if isinstance(x, bytes): - return x - elif not isinstance(x, str): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.encode(encoding or "utf-8", errors=errors or "strict") - return x.encode() - - -def to_str( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> str: - if isinstance(x, str): - return x - elif not isinstance(x, bytes): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.decode(encoding or "utf-8", errors=errors or "strict") - return x.decode() - - -def reraise( - tp: type[BaseException] | None, - value: BaseException, - tb: TracebackType | None = None, -) -> typing.NoReturn: - try: - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - finally: - value = None # type: ignore[assignment] - tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py deleted file mode 100644 index aeca0c7ad5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkDataCollectionOff/urllib3/util/wait.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import select -import socket -from functools import partial - -__all__ = ["wait_for_read", "wait_for_write"] - - -# How should we wait on sockets? -# -# There are two types of APIs you can use for waiting on sockets: the fancy -# modern stateful APIs like epoll/kqueue, and the older stateless APIs like -# select/poll. The stateful APIs are more efficient when you have a lots of -# sockets to keep track of, because you can set them up once and then use them -# lots of times. But we only ever want to wait on a single socket at a time -# and don't want to keep track of state, so the stateless APIs are actually -# more efficient. So we want to use select() or poll(). -# -# Now, how do we choose between select() and poll()? On traditional Unixes, -# select() has a strange calling convention that makes it slow, or fail -# altogether, for high-numbered file descriptors. The point of poll() is to fix -# that, so on Unixes, we prefer poll(). -# -# On Windows, there is no poll() (or at least Python doesn't provide a wrapper -# for it), but that's OK, because on Windows, select() doesn't have this -# strange calling convention; plain select() works fine. -# -# So: on Windows we use select(), and everywhere else we use poll(). We also -# fall back to select() in case poll() is somehow broken or missing. - - -def select_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - rcheck = [] - wcheck = [] - if read: - rcheck.append(sock) - if write: - wcheck.append(sock) - # When doing a non-blocking connect, most systems signal success by - # marking the socket writable. Windows, though, signals success by marked - # it as "exceptional". We paper over the difference by checking the write - # sockets for both conditions. (The stdlib selectors module does the same - # thing.) - fn = partial(select.select, rcheck, wcheck, wcheck) - rready, wready, xready = fn(timeout) - return bool(rready or wready or xready) - - -def poll_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - mask = 0 - if read: - mask |= select.POLLIN - if write: - mask |= select.POLLOUT - poll_obj = select.poll() - poll_obj.register(sock, mask) - - # For some reason, poll() takes timeout in milliseconds - def do_poll(t: float | None) -> list[tuple[int, int]]: - if t is not None: - t *= 1000 - return poll_obj.poll(t) - - return bool(do_poll(timeout)) - - -def _have_working_poll() -> bool: - # Apparently some systems have a select.poll that fails as soon as you try - # to use it, either due to strange configuration or broken monkeypatching - # from libraries like eventlet/greenlet. - try: - poll_obj = select.poll() - poll_obj.poll(0) - except (AttributeError, OSError): - return False - else: - return True - - -def wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - # We delay choosing which implementation to use until the first time we're - # called. We could do it at import time, but then we might make the wrong - # decision if someone goes wild with monkeypatching select.poll after - # we're imported. - global wait_for_socket - if _have_working_poll(): - wait_for_socket = poll_wait_for_socket - elif hasattr(select, "select"): - wait_for_socket = select_wait_for_socket - return wait_for_socket(sock, read, write, timeout) - - -def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for reading to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, read=True, timeout=timeout) - - -def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for writing to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER deleted file mode 100644 index 5c69047b2e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -uv \ No newline at end of file diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA deleted file mode 100644 index 0d4f101491..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/METADATA +++ /dev/null @@ -1,78 +0,0 @@ -Metadata-Version: 2.4 -Name: certifi -Version: 2026.6.17 -Summary: Python package for providing Mozilla's CA Bundle. -Home-page: https://github.com/certifi/python-certifi -Author: Kenneth Reitz -Author-email: me@kennethreitz.com -License: MPL-2.0 -Project-URL: Source, https://github.com/certifi/python-certifi -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) -Classifier: Natural Language :: English -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Requires-Python: >=3.7 -License-File: LICENSE -Dynamic: author -Dynamic: author-email -Dynamic: classifier -Dynamic: description -Dynamic: home-page -Dynamic: license -Dynamic: license-file -Dynamic: project-url -Dynamic: requires-python -Dynamic: summary - -Certifi: Python SSL Certificates -================================ - -Certifi provides Mozilla's carefully curated collection of Root Certificates for -validating the trustworthiness of SSL certificates while verifying the identity -of TLS hosts. It has been extracted from the `Requests`_ project. - -Installation ------------- - -``certifi`` is available on PyPI. Simply install it with ``pip``:: - - $ pip install certifi - -Usage ------ - -To reference the installed certificate authority (CA) bundle, you can use the -built-in function:: - - >>> import certifi - - >>> certifi.where() - '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' - -Or from the command line:: - - $ python -m certifi - /usr/local/lib/python3.7/site-packages/certifi/cacert.pem - -Enjoy! - -.. _`Requests`: https://requests.readthedocs.io/en/latest/ - -Addition/Removal of Certificates --------------------------------- - -Certifi does not support any addition/removal or other modification of the -CA trust store content. This project is intended to provide a reliable and -highly portable root of trust to python deployments. Look to upstream projects -for methods to use alternate trust. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD deleted file mode 100644 index 13b0ce7da4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/RECORD +++ /dev/null @@ -1,12 +0,0 @@ -certifi-2026.6.17.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 -certifi-2026.6.17.dist-info/RECORD,, -certifi-2026.6.17.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 -certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 -certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 -certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 -certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 -certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 -certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 -certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL deleted file mode 100644 index 14a883f292..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (82.0.1) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE deleted file mode 100644 index 62b076cdee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/licenses/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -This package contains a modified version of ca-bundle.crt: - -ca-bundle.crt -- Bundle of CA Root Certificates - -This is a bundle of X.509 certificates of public Certificate Authorities -(CA). These were automatically extracted from Mozilla's root certificates -file (certdata.txt). This file can be found in the mozilla source tree: -https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt -It contains the certificates in PEM format and therefore -can be directly used with curl / libcurl / php_curl, or with -an Apache+mod_ssl webserver for SSL client authentication. -Just configure this file as the SSLCACertificateFile.# - -***** BEGIN LICENSE BLOCK ***** -This Source Code Form is subject to the terms of the Mozilla Public License, -v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain -one at http://mozilla.org/MPL/2.0/. - -***** END LICENSE BLOCK ***** -@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt deleted file mode 100644 index 963eac530b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi-2026.6.17.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -certifi diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py deleted file mode 100644 index ed9a74b7a0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .core import contents, where - -__all__ = ["contents", "where"] -__version__ = "2026.06.17" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py deleted file mode 100644 index 8945b5da85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/__main__.py +++ /dev/null @@ -1,12 +0,0 @@ -import argparse - -from certifi import contents, where - -parser = argparse.ArgumentParser() -parser.add_argument("-c", "--contents", action="store_true") -args = parser.parse_args() - -if args.contents: - print(contents()) -else: - print(where()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem deleted file mode 100644 index 1c2dbfeb68..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/cacert.pem +++ /dev/null @@ -1,3863 +0,0 @@ - -# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited -# Label: "COMODO ECC Certification Authority" -# Serial: 41578283867086692638256921589707938090 -# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 -# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 -# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT -IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw -MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N -T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR -FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J -cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW -BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm -fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv -GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) -# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" -# Serial: 80544274841616 -# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 -# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 -# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG -EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 -MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl -cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR -dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB -pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM -b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm -aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz -IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT -lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz -AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 -VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG -ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 -BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG -AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M -U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh -bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C -+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F -uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 -XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. -# Label: "Microsec e-Szigno Root CA 2009" -# Serial: 14014712776195784473 -# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 -# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e -# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G -CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y -OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx -FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp -Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP -kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc -cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U -fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 -N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC -xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 -+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM -Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG -SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h -mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk -ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c -2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t -HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 -# Label: "GlobalSign Root CA - R3" -# Serial: 4835703278459759426209954 -# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 -# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad -# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 -MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 -RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT -gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm -KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd -QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ -XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw -DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o -LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU -RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp -jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK -6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX -mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs -Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH -WD9f ------END CERTIFICATE----- - -# Issuer: CN=Izenpe.com O=IZENPE S.A. -# Subject: CN=Izenpe.com O=IZENPE S.A. -# Label: "Izenpe.com" -# Serial: 917563065490389241595536686991402621 -# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 -# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 -# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f ------BEGIN CERTIFICATE----- -MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 -MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 -ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD -VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j -b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq -scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO -xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H -LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX -uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD -yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ -JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q -rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN -BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L -hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB -QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ -HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu -Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg -QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB -BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx -MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA -A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb -laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 -awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo -JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw -LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT -VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk -LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb -UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ -QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ -naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls -QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE----- - -# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. -# Label: "Go Daddy Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 -# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b -# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT -EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp -ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz -NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH -EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE -AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD -E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH -/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy -DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh -GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR -tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA -AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX -WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu -9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr -gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo -2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI -4uJEvlz36hz1 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 -# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e -# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. -# Label: "Starfield Services Root Certificate Authority - G2" -# Serial: 0 -# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 -# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f -# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs -ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD -VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy -ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy -dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p -OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 -8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K -Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe -hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk -6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q -AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI -bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB -ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z -qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn -0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN -sSi6 ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA" -# Serial: 279744 -# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 -# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e -# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM -MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D -ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU -cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 -WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg -Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw -IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH -UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM -TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU -BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM -kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x -AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV -HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y -sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL -I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 -J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY -VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA -# Label: "TWCA Root Certification Authority" -# Serial: 1 -# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 -# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 -# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES -MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU -V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz -WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO -LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE -AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH -K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX -RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z -rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx -3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq -hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC -MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls -XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D -lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn -aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ -YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 -# Label: "Security Communication RootCA2" -# Serial: 0 -# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 -# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 -# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl -MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe -U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX -DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy -dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj -YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV -OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr -zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM -VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ -hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO -ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw -awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs -OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF -coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc -okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 -t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy -1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ -SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 -# Label: "Actalis Authentication Root CA" -# Serial: 6271844772424770508 -# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 -# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac -# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE -BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w -MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC -SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 -ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv -UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX -4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 -KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ -gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb -rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ -51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F -be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe -KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F -v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn -fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 -jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz -ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL -e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 -jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz -WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V -SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j -pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX -X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok -fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R -K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU -ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU -LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT -LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 2 Root CA" -# Serial: 2 -# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 -# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 -# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr -6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV -L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 -1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx -MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ -QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB -arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr -Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi -FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS -P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN -9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz -uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h -9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t -OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo -+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 -KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 -DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us -H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ -I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 -5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h -3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz -Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE----- - -# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 -# Label: "Buypass Class 3 Root CA" -# Serial: 2 -# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec -# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 -# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y -ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E -N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 -tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX -0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c -/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X -KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY -zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS -O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D -34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP -K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv -Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj -QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS -IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 -HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa -O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv -033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u -dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE -kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 -3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD -u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq -4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 3" -# Serial: 1 -# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef -# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 -# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN -8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ -RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 -hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 -ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM -EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 -A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy -WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ -1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 -6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT -91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p -TpPDpFQUWw== ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 2009" -# Serial: 623603 -# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f -# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 -# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 ------BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha -ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM -HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 -UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 -tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R -ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM -lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp -/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G -A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy -MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl -cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js -L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL -BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni -acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K -zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 -PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y -Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH -# Label: "D-TRUST Root Class 3 CA 2 EV 2009" -# Serial: 623604 -# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 -# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 -# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw -NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV -BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn -ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 -3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z -qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR -p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 -HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw -ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea -HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw -Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh -c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E -RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt -dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku -Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp -3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF -CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na -xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX -KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE----- - -# Issuer: CN=CA Disig Root R2 O=Disig a.s. -# Subject: CN=CA Disig Root R2 O=Disig a.s. -# Label: "CA Disig Root R2" -# Serial: 10572350602393338211 -# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 -# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 -# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV -BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu -MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy -MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx -EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe -NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH -PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I -x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe -QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR -yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO -QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 -H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ -QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD -i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs -nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 -rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI -hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM -tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf -GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb -lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka -+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal -TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i -nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 -gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr -G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os -zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x -L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE----- - -# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV -# Label: "ACCVRAIZ1" -# Serial: 6828503384748696800 -# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 -# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 -# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 ------BEGIN CERTIFICATE----- -MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE -AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw -CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ -BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND -VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb -qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY -HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo -G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA -lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr -IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ -0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH -k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 -4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO -m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa -cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl -uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI -KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls -ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG -AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 -VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT -VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG -CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA -cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA -QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA -7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA -cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA -QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA -czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu -aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt -aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud -DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF -BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp -D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU -JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m -AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD -vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms -tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH -7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h -I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA -h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF -d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H -pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE----- - -# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA Global Root CA" -# Serial: 3262 -# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 -# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 -# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx -EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT -VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 -NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT -B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF -10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz -0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh -MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH -zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc -46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 -yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi -laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP -oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA -BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE -qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm -4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL -1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF -H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo -RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ -nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh -15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW -6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW -nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j -wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz -aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy -KwbQBM0= ------END CERTIFICATE----- - -# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center -# Label: "T-TeleSec GlobalRoot Class 2" -# Serial: 1 -# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a -# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 -# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd -AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC -FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi -1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq -jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ -wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ -WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy -NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC -uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw -IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 -g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP -BSeOE6Fuwg== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot 2011 O=Atos -# Subject: CN=Atos TrustedRoot 2011 O=Atos -# Label: "Atos TrustedRoot 2011" -# Serial: 6643877497813316402 -# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 -# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 -# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE -AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG -EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM -FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC -REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp -Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM -VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ -SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ -4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L -cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi -eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG -A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 -DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j -vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP -DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc -maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D -lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv -KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 1 G3" -# Serial: 687049649626669250736271037606554624078720034195 -# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab -# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 -# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 -MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV -wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe -rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 -68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh -4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp -UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o -abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc -3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G -KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt -hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO -Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt -zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD -ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC -MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 -cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN -qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 -YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv -b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 -8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k -NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj -ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp -q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt -nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 2 G3" -# Serial: 390156079458959257446133169266079962026824725800 -# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 -# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 -# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 -MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf -qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW -n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym -c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ -O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 -o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j -IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq -IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz -8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh -vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l -7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG -cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD -ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC -roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga -W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n -lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE -+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV -csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd -dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg -KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM -HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 -WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE----- - -# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited -# Label: "QuoVadis Root CA 3 G3" -# Serial: 268090761170461462463995952157327242137089239581 -# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 -# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d -# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 -MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR -/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu -FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR -U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c -ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR -FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k -A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw -eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl -sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp -VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q -A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ -ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD -ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI -FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv -oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg -u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP -0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf -3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl -8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ -DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN -PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ -ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G2" -# Serial: 15385348160840213938643033620894905419 -# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d -# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f -# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 ------BEGIN CERTIFICATE----- -MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA -n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc -biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp -EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA -bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu -YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB -AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW -BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI -QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I -0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni -lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 -B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv -ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo -IhNzbM8m9Yop5w== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Assured ID Root G3" -# Serial: 15459312981008553731928384953135426796 -# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb -# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 -# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg -RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf -Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q -RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD -AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY -JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv -6pZjamVFkpUBtA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G2" -# Serial: 4293743540046975378534879503202253541 -# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 -# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 -# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f ------BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH -MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI -2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx -1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ -q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz -tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ -vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV -5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY -1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 -NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG -Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 -8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe -pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Global Root G3" -# Serial: 7089244469030293291760083333884364146 -# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca -# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e -# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 ------BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe -Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw -EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x -IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG -fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO -Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx -AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ -oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 -sycX ------END CERTIFICATE----- - -# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com -# Label: "DigiCert Trusted Root G4" -# Serial: 7451500558977370777930084869016614236 -# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 -# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 -# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 ------BEGIN CERTIFICATE----- -MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg -RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu -Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y -ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If -xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV -ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO -DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ -jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ -CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi -EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM -fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY -uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK -chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t -9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD -ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 -SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd -+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc -fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa -sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N -cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N -0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie -4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI -r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 -/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm -gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE----- - -# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited -# Label: "COMODO RSA Certification Authority" -# Serial: 101909084537582093308941363524873193117 -# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 -# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 -# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 ------BEGIN CERTIFICATE----- -MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB -hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV -BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT -EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR -6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X -pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC -9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV -/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf -Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z -+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w -qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah -SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC -u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf -Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq -crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E -FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl -wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM -4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV -2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna -FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ -CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK -boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke -jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL -S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb -QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl -0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB -NVOFBkpdn627G190 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network -# Label: "USERTrust RSA Certification Authority" -# Serial: 2645093764781058787591871645665788717 -# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 -# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e -# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 ------BEGIN CERTIFICATE----- -MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB -iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl -cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV -BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw -MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV -BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B -3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY -tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ -Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 -VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT -79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 -c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT -Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l -c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee -UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE -Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd -BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G -A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF -Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO -VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 -ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs -8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR -iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze -Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ -XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ -qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB -VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB -L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG -jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE----- - -# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network -# Label: "USERTrust ECC Certification Authority" -# Serial: 123013823720199481456569720443997572134 -# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 -# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 -# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a ------BEGIN CERTIFICATE----- -MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL -MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl -eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT -JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx -MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT -Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg -VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo -I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng -o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G -A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB -zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW -RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 -# Label: "GlobalSign ECC Root CA - R5" -# Serial: 32785792099990507226680698011560947931244 -# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 -# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa -# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 ------BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc -8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke -hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI -KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg -515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO -xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust -# Label: "IdenTrust Commercial Root CA 1" -# Serial: 13298821034946342390520003877796839426 -# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 -# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 -# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu -VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw -MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw -JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT -3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU -+ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp -S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 -bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi -T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL -vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK -Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK -dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT -c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv -l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N -iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD -ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH -6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt -LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 -nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 -+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK -W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT -AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq -l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG -4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ -mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A -7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE----- - -# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust -# Label: "IdenTrust Public Sector Root CA 1" -# Serial: 13298821034946342390521976156843933698 -# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba -# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd -# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu -VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN -MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 -MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 -ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy -RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS -bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF -/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R -3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw -EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy -9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V -GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ -2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV -WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD -W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN -AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj -t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV -DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 -TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G -lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW -mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df -WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 -+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ -tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA -GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv -8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE----- - -# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority -# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority -# Label: "CFCA EV ROOT" -# Serial: 407555286 -# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 -# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 -# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD -TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx -MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j -aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP -T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 -sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL -TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 -/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp -7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz -EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt -hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP -a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot -aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg -TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV -PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv -cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL -tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd -BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT -ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL -jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS -ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy -P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 -xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d -Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN -5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe -/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z -AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ -5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GB CA" -# Serial: 157768595616588414422159278966750757568 -# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d -# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed -# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt -MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg -Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i -YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x -CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG -b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh -bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 -HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx -WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX -1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk -u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P -99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r -M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB -BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh -cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 -gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO -ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf -aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic -Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE----- - -# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. -# Label: "SZAFIR ROOT CA2" -# Serial: 357043034767186914217277344587386743377558296292 -# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 -# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de -# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 -ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw -NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L -cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg -Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN -QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT -3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw -3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 -3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 -BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN -XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF -AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw -8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG -nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP -oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy -d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg -LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Network CA 2" -# Serial: 44979900017204383099463764357512596969 -# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 -# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 -# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 ------BEGIN CERTIFICATE----- -MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB -gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu -QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG -A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz -OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ -VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 -b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA -DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn -0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB -OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE -fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E -Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m -o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i -sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW -OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez -Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS -adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n -3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ -F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf -CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 -XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm -djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ -WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb -AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq -P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko -b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj -XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P -5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi -DrW5viSP ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce -# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 -# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 ------BEGIN CERTIFICATE----- -MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix -DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k -IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT -N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v -dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG -A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh -ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx -QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA -4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 -AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 -4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C -ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV -9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD -gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 -Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq -NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko -LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc -Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd -ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I -XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI -M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot -9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V -Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea -j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh -X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ -l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf -bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 -pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK -e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 -vm9qp/UsQu0yrbYhnr68 ------END CERTIFICATE----- - -# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" -# Serial: 0 -# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef -# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 -# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 ------BEGIN CERTIFICATE----- -MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN -BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl -bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv -b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ -BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj -YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 -MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 -dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg -QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa -jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi -C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep -lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof -TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X1 O=Internet Security Research Group -# Subject: CN=ISRG Root X1 O=Internet Security Research Group -# Label: "ISRG Root X1" -# Serial: 172886928669790476064670243504169061120 -# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e -# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 -# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- - -# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM -# Label: "AC RAIZ FNMT-RCM" -# Serial: 485876308206448804701554682760554759 -# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d -# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 -# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx -CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ -WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ -BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG -Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ -yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf -BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz -WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF -tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z -374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC -IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL -mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 -wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS -MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 -ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet -UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H -YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 -LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD -nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 -RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM -LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf -77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N -JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm -fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp -6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp -1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B -9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok -RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv -uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 1 O=Amazon -# Subject: CN=Amazon Root CA 1 O=Amazon -# Label: "Amazon Root CA 1" -# Serial: 143266978916655856878034712317230054538369994 -# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 -# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 -# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 2 O=Amazon -# Subject: CN=Amazon Root CA 2 O=Amazon -# Label: "Amazon Root CA 2" -# Serial: 143266982885963551818349160658925006970653239 -# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 -# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a -# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 3 O=Amazon -# Subject: CN=Amazon Root CA 3 O=Amazon -# Label: "Amazon Root CA 3" -# Serial: 143266986699090766294700635381230934788665930 -# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 -# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e -# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl -ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr -ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr -BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM -YyRIHN8wfdVoOw== ------END CERTIFICATE----- - -# Issuer: CN=Amazon Root CA 4 O=Amazon -# Subject: CN=Amazon Root CA 4 O=Amazon -# Label: "Amazon Root CA 4" -# Serial: 143266989758080763974105200630763877849284878 -# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd -# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be -# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi -9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk -M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB -MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw -CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW -1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- - -# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM -# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" -# Serial: 1 -# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 -# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca -# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 ------BEGIN CERTIFICATE----- -MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx -GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp -bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w -KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 -BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy -dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG -EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll -IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU -QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT -TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg -LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 -a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr -LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr -N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X -YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ -iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f -AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH -V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh -AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf -IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 -lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c -8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf -lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE----- - -# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. -# Label: "GDCA TrustAUTH R5 ROOT" -# Serial: 9009899650740120186 -# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 -# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 -# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 ------BEGIN CERTIFICATE----- -MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE -BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ -IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 -MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV -BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w -HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj -Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj -TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u -KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj -qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm -MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 -ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP -zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk -L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC -jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA -HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC -AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg -p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm -DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 -COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry -L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf -JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg -IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io -2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV -09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ -XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq -T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe -MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation -# Label: "SSL.com Root Certification Authority RSA" -# Serial: 8875640296558310041 -# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 -# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb -# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 ------BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE -BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK -DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz -OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R -xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX -qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC -C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 -6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh -/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF -YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E -JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc -US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 -ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm -+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi -M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G -A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV -cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc -Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs -PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ -q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 -cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr -a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I -H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y -K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu -nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf -oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY -Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com Root Certification Authority ECC" -# Serial: 8495723813297216424 -# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e -# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a -# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 ------BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz -WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 -b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS -b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI -7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg -CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud -EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD -VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T -kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ -gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority RSA R2" -# Serial: 6248227494352943350 -# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 -# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a -# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c ------BEGIN CERTIFICATE----- -MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV -BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE -CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy -MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G -A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD -DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq -M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf -OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa -4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 -HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR -aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA -b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ -Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV -PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO -pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu -UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY -MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV -HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 -9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW -s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 -Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg -cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM -79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz -/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt -ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm -Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK -QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ -w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi -S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 -mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE----- - -# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation -# Label: "SSL.com EV Root Certification Authority ECC" -# Serial: 3182246526754555285 -# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 -# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d -# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 ------BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx -NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv -bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA -VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku -WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX -5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ -ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg -h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 -# Label: "GlobalSign Root CA - R6" -# Serial: 1417766617973444989252670301619537 -# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae -# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 -# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg -MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx -MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET -MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI -xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k -ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD -aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw -LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw -1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX -k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 -SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h -bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n -WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY -rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce -MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu -bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN -nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt -Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 -55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj -vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf -cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz -oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp -nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs -pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v -JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R -8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 -5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE----- - -# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GC CA" -# Serial: 44084345621038548146064804565436152554 -# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 -# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 -# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d ------BEGIN CERTIFICATE----- -MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw -CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 -bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg -Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ -BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu -ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS -b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni -eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W -p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T -rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV -57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg -Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE----- - -# Issuer: CN=UCA Global G2 Root O=UniTrust -# Subject: CN=UCA Global G2 Root O=UniTrust -# Label: "UCA Global G2 Root" -# Serial: 124779693093741543919145257850076631279 -# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 -# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a -# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH -bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x -CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds -b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr -b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 -kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm -VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R -VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc -C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj -tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY -D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv -j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl -NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 -iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP -O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV -ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj -L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 -1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl -1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU -b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV -PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj -y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb -EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg -DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI -+Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy -YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX -UB+K+wb1whnw0A== ------END CERTIFICATE----- - -# Issuer: CN=UCA Extended Validation Root O=UniTrust -# Subject: CN=UCA Extended Validation Root O=UniTrust -# Label: "UCA Extended Validation Root" -# Serial: 106100277556486529736699587978573607008 -# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 -# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a -# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH -MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF -eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx -MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV -BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog -D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS -sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop -O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk -sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi -c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj -VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz -KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ -TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G -sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs -1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD -fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN -l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR -ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ -VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 -c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp -4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s -t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj -2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO -vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C -xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx -cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM -fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax ------END CERTIFICATE----- - -# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 -# Label: "Certigna Root CA" -# Serial: 269714418870597844693661054334862075617 -# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 -# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 -# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 ------BEGIN CERTIFICATE----- -MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw -WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw -MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x -MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD -VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX -BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO -ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M -CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu -I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm -TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh -C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf -ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz -IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT -Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k -JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 -hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB -GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of -1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov -L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo -dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr -aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq -hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L -6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG -HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 -0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB -lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi -o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 -gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v -faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 -Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh -jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw -3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign Root CA - G1" -# Serial: 235931866688319308814040 -# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac -# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c -# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 ------BEGIN CERTIFICATE----- -MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD -VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU -ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH -MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO -MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv -Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz -f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO -8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq -d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM -tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt -Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB -o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x -PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM -wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d -GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH -6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby -RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx -iN66zB+Afko= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI -# Label: "emSign ECC Root CA - G3" -# Serial: 287880440101571086945156 -# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 -# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 -# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b ------BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG -EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo -bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g -RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ -TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s -b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw -djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 -WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS -fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB -zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB -CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD -+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE----- - -# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI -# Label: "emSign Root CA - C1" -# Serial: 825510296613316004955058 -# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 -# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 -# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f ------BEGIN CERTIFICATE----- -MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG -A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg -SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v -dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ -BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ -HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH -3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH -GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c -xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 -aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq -TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 -/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 -kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG -YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT -+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo -WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= ------END CERTIFICATE----- - -# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI -# Label: "emSign ECC Root CA - C3" -# Serial: 582948710642506000014504 -# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 -# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 -# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 ------BEGIN CERTIFICATE----- -MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG -EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx -IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw -MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln -biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND -IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci -MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti -sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O -BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB -Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c -3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J -0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== ------END CERTIFICATE----- - -# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post -# Label: "Hongkong Post Root CA 3" -# Serial: 46170865288971385588281144162979347873371282084 -# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 -# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 -# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 ------BEGIN CERTIFICATE----- -MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL -BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ -SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n -a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 -NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT -CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u -Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK -AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO -dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI -VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV -9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY -2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY -vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt -bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb -x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ -l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK -TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj -Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e -i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw -DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG -7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk -MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr -gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk -GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS -3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm -Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ -l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c -JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP -L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa -LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG -mpv0 ------END CERTIFICATE----- - -# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft ECC Root Certificate Authority 2017" -# Serial: 136839042543790627607696632466672567020 -# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 -# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 -# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 ------BEGIN CERTIFICATE----- -MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw -CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD -VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw -MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV -UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy -b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR -ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb -hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 -FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV -L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB -iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE----- - -# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation -# Label: "Microsoft RSA Root Certificate Authority 2017" -# Serial: 40975477897264996090493496164228220339 -# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 -# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 -# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 ------BEGIN CERTIFICATE----- -MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl -MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw -NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 -IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG -EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N -aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ -Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 -ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 -HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm -gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ -jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc -aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG -YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 -W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K -UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH -+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q -W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC -LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC -gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 -tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh -SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 -TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 -pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR -xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp -GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 -dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN -AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB -RA+GsCyRxj3qrg+E ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. -# Label: "e-Szigno Root CA 2017" -# Serial: 411379200276854331539784714 -# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 -# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 -# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 ------BEGIN CERTIFICATE----- -MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV -BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk -LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv -b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ -BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg -THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v -IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv -xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H -Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB -eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo -jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ -+efcMQ== ------END CERTIFICATE----- - -# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 -# Label: "certSIGN Root CA G2" -# Serial: 313609486401300475190 -# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 -# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 -# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV -BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g -Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ -BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ -R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF -dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw -vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ -uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp -n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs -cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW -xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P -rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF -DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx -DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy -LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C -eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ -d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq -kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC -b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl -qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 -OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c -NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk -ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO -pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj -03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk -PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE -1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX -QRBdJ3NghVdJIgc= ------END CERTIFICATE----- - -# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. -# Label: "NAVER Global Root Certification Authority" -# Serial: 9013692873798656336226253319739695165984492813 -# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b -# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 -# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 ------BEGIN CERTIFICATE----- -MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM -BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG -T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 -aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx -CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD -b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA -iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH -38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE -HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz -kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP -szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq -vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf -nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG -YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo -0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a -CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K -AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I -36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB -Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN -qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj -cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm -+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL -hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe -lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 -p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 -piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR -LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX -5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO -dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul -9XXeifdy ------END CERTIFICATE----- - -# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres -# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" -# Serial: 131542671362353147877283741781055151509 -# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb -# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a -# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb ------BEGIN CERTIFICATE----- -MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw -CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw -FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S -Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 -MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL -DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS -QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH -sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK -Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu -SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC -MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy -v+c= ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa -# Label: "GlobalSign Root R46" -# Serial: 1552617688466950547958867513931858518042577 -# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef -# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 -# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA -MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD -VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy -MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt -c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ -OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG -vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud -316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo -0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE -y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF -zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE -+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN -I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs -x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa -ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC -4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 -7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg -JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti -2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk -pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF -FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt -rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk -ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 -u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP -4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 -N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 -vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa -# Label: "GlobalSign Root E46" -# Serial: 1552617690338932563915843282459653771421763 -# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f -# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 -# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 ------BEGIN CERTIFICATE----- -MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx -CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD -ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw -MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex -HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq -R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd -yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ -7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 -+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE----- - -# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz -# Label: "ANF Secure Server Root CA" -# Serial: 996390341000653745 -# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 -# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 -# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV -BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk -YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV -BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN -MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF -UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD -VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v -dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj -cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q -yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH -2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX -H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL -zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR -p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz -W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ -SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn -LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 -n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B -u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj -o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC -AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L -9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej -rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK -pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 -vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq -OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ -/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 -2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI -+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 -MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo -tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE----- - -# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum EC-384 CA" -# Serial: 160250656287871593594747141429395092468 -# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 -# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed -# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 ------BEGIN CERTIFICATE----- -MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw -CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw -JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT -EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 -WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT -LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX -BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE -KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm -Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 -EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J -UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn -nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE----- - -# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority -# Label: "Certum Trusted Root CA" -# Serial: 40870380103424195783807378461123655149 -# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 -# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 -# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd ------BEGIN CERTIFICATE----- -MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 -MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu -MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV -BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw -MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg -U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo -b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ -n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q -p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq -NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF -8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 -HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa -mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi -7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF -ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P -qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ -v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 -Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 -vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD -ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 -WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo -zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR -5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ -GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf -5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq -0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D -P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM -qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP -0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf -E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE----- - -# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique -# Label: "TunTrust Root CA" -# Serial: 108534058042236574382096126452369648152337120275 -# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 -# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb -# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL -BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg -Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv -b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG -EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u -IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ -n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd -2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF -VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ -GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF -li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU -r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 -eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb -MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg -jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB -7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW -5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE -ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 -90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z -xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu -QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 -FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH -22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP -xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn -dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 -Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b -nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ -CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH -u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj -d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS RSA Root CA 2021" -# Serial: 76817823531813593706434026085292783742 -# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 -# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d -# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs -MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg -Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL -MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl -YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv -b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l -mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE -4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv -a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M -pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw -Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b -LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY -AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB -AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq -E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr -W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ -CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU -X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 -f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja -H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP -JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P -zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt -jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 -/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT -BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 -aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW -xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU -63ZTGI0RmLo= ------END CERTIFICATE----- - -# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA -# Label: "HARICA TLS ECC Root CA 2021" -# Serial: 137515985548005187474074462014555733966 -# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 -# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 -# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 ------BEGIN CERTIFICATE----- -MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw -CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh -cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v -dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG -A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj -aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg -Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 -KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y -STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD -AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw -SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN -nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE----- - -# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 -# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" -# Serial: 1977337328857672817 -# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 -# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe -# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE -BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h -cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 -MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg -Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 -thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM -cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG -L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i -NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h -X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b -m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy -Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja -EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T -KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF -6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh -OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc -tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd -IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j -b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC -AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw -ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m -iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF -Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ -hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P -Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE -EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV -1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t -CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR -5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw -f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 -ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK -GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE----- - -# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus ECC Root CA" -# Serial: 630369271402956006249506845124680065938238527194 -# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 -# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 -# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 ------BEGIN CERTIFICATE----- -MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw -RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY -BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz -MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u -LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 -v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd -e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw -V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA -AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG -GJTO ------END CERTIFICATE----- - -# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. -# Label: "vTrus Root CA" -# Serial: 387574501246983434957692974888460947164905180485 -# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc -# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 -# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 ------BEGIN CERTIFICATE----- -MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL -BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x -FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx -MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s -THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc -IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU -AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ -GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 -8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH -flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt -J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim -0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN -pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ -UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW -OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB -AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet -8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd -nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j -bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM -Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv -TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS -S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr -I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 -b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB -UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P -Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven -sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= ------END CERTIFICATE----- - -# Issuer: CN=ISRG Root X2 O=Internet Security Research Group -# Subject: CN=ISRG Root X2 O=Internet Security Research Group -# Label: "ISRG Root X2" -# Serial: 87493402998870891108772069816698636114 -# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 -# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af -# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 ------BEGIN CERTIFICATE----- -MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw -CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg -R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 -MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT -ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw -EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW -+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 -ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI -zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW -tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 -/q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE----- - -# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. -# Label: "HiPKI Root CA - G1" -# Serial: 60966262342023497858655262305426234976 -# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 -# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 -# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc ------BEGIN CERTIFICATE----- -MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP -MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 -ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa -Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 -YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw -qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv -Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 -lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz -Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ -KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK -FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj -HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr -y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ -/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM -a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 -fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG -SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi -7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc -SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza -ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc -XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg -iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho -L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF -Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr -kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ -vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU -YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE----- - -# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 -# Label: "GlobalSign ECC Root CA - R4" -# Serial: 159662223612894884239637590694 -# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc -# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 -# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 ------BEGIN CERTIFICATE----- -MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD -VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw -MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g -UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT -BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx -uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV -HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ -+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 -bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R1 O=Google Trust Services LLC -# Subject: CN=GTS Root R1 O=Google Trust Services LLC -# Label: "GTS Root R1" -# Serial: 159662320309726417404178440727 -# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 -# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a -# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw -CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU -MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw -MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp -Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo -27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w -Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw -TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl -qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH -szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 -Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk -MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 -wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p -aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN -VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID -AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E -FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb -C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe -QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy -h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 -7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J -ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef -MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ -Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT -6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ -0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm -2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb -bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R3 O=Google Trust Services LLC -# Subject: CN=GTS Root R3 O=Google Trust Services LLC -# Label: "GTS Root R3" -# Serial: 159662495401136852707857743206 -# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 -# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 -# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G -jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 -4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 -VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm -ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X ------END CERTIFICATE----- - -# Issuer: CN=GTS Root R4 O=Google Trust Services LLC -# Subject: CN=GTS Root R4 O=Google Trust Services LLC -# Label: "GTS Root R4" -# Serial: 159662532700760215368942768210 -# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 -# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 -# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD -VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG -A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw -WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz -IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi -QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR -HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D -9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 -p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD ------END CERTIFICATE----- - -# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj -# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj -# Label: "Telia Root CA v2" -# Serial: 7288924052977061235122729490515358 -# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 -# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd -# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx -CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE -AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 -NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ -MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq -AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 -vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 -lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD -n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT -7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o -6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC -TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 -WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R -DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI -pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj -YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy -rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ -8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi -0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM -A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS -SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K -TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF -6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er -3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt -Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT -VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW -ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA -rBPuUBQemMc= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 1 2020" -# Serial: 165870826978392376648679885835942448534 -# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed -# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 -# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 -NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS -zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 -QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ -VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW -wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV -dWNbFJWcHwHP2NVypw87 ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 1 2020" -# Serial: 126288379621884218666039612629459926992 -# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e -# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 -# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 -NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC -/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD -wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 -OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA -y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb -gfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS ECC P384 Root G5" -# Serial: 13129116028163249804115411775095713523 -# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed -# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee -# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 ------BEGIN CERTIFICATE----- -MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp -Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 -MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ -bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS -7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp -0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS -B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 -BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ -LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 -DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE----- - -# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. -# Label: "DigiCert TLS RSA4096 Root G5" -# Serial: 11930366277458970227240571539258396554 -# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 -# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 -# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN -MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT -HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN -NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs -IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ -ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 -2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp -wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM -pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD -nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po -sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx -Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd -Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX -KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe -XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL -tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv -TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN -AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw -GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H -PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF -O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ -REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik -AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv -/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ -p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw -MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF -qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK -ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root R1 O=Certainly -# Subject: CN=Certainly Root R1 O=Certainly -# Label: "Certainly Root R1" -# Serial: 188833316161142517227353805653483829216 -# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 -# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af -# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw -PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy -dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 -MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 -YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 -1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT -vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed -aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 -1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 -r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 -cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ -wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ -6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA -2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH -Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR -eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB -/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u -d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr -PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d -8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi -1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd -rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di -taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 -lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj -yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn -Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy -yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n -wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 -OV+KmalBWQewLK8= ------END CERTIFICATE----- - -# Issuer: CN=Certainly Root E1 O=Certainly -# Subject: CN=Certainly Root E1 O=Certainly -# Label: "Certainly Root E1" -# Serial: 8168531406727139161245376702891150584 -# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 -# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b -# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 ------BEGIN CERTIFICATE----- -MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw -CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu -bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ -BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s -eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK -+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 -QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 -hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm -ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG -BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE----- - -# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. -# Label: "Security Communication ECC RootCA1" -# Serial: 15446673492073852651 -# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 -# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 -# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 ------BEGIN CERTIFICATE----- -MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT -AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD -VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx -NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT -HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 -IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi -AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl -dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK -ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu -9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O -be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA1" -# Serial: 113562791157148395269083148143378328608 -# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 -# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a -# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU -MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI -T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz -MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF -SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh -bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z -xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ -spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 -58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR -at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll -5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq -nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK -V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ -pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO -z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn -jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ -WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF -7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 -YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli -awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u -+2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 -X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN -SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo -P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI -+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz -znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 -eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 -YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy -r/6zcCwupvI= ------END CERTIFICATE----- - -# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY -# Label: "BJCA Global Root CA2" -# Serial: 58605626836079930195615843123109055211 -# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c -# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 -# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 ------BEGIN CERTIFICATE----- -MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw -CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ -VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy -MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ -TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS -b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B -IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ -+kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK -sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA -94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B -43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root E46" -# Serial: 88989738453351742415770396670917916916 -# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 -# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a -# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 ------BEGIN CERTIFICATE----- -MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw -CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T -ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN -MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG -A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT -ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC -WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ -6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B -Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa -qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q -4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== ------END CERTIFICATE----- - -# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited -# Label: "Sectigo Public Server Authentication Root R46" -# Serial: 156256931880233212765902055439220583700 -# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 -# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 -# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 ------BEGIN CERTIFICATE----- -MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf -MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD -Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw -HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY -MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp -YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa -ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz -SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf -iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X -ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 -IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS -VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE -SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu -+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt -8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L -HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt -zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P -AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c -mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ -YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 -gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA -Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB -JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX -DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui -TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 -dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 -LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp -0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY -QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS RSA Root CA 2022" -# Serial: 148535279242832292258835760425842727825 -# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da -# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca -# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed ------BEGIN CERTIFICATE----- -MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO -MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD -DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX -DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw -b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP -L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY -t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins -S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 -PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO -L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 -R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w -dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS -+YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS -d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG -AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f -gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z -NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt -hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM -QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf -R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ -DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW -P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy -lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq -bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w -AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q -r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji -Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU -98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE----- - -# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation -# Label: "SSL.com TLS ECC Root CA 2022" -# Serial: 26605119622390491762507526719404364228 -# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 -# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 -# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 ------BEGIN CERTIFICATE----- -MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT -U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 -MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh -dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm -acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN -SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME -GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW -uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp -15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN -b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA ECC TLS 2021" -# Serial: 81873346711060652204712539181482831616 -# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 -# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd -# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 ------BEGIN CERTIFICATE----- -MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w -LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w -CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 -MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF -Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI -zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X -tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 -AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 -KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD -aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu -CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo -9H1/IISpQuQo ------END CERTIFICATE----- - -# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos -# Label: "Atos TrustedRoot Root CA RSA TLS 2021" -# Serial: 111436099570196163832749341232207667876 -# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 -# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 -# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f ------BEGIN CERTIFICATE----- -MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM -MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx -MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 -MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD -QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z -4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv -Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ -kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs -GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln -nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh -3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD -0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy -geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 -ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB -c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI -pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU -dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB -DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS -4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs -o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ -qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw -xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM -rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 -AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR -0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY -o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 -dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE -oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G3" -# Serial: 576386314500428537169965010905813481816650257167 -# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 -# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 -# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 ------BEGIN CERTIFICATE----- -MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM -BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe -Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw -IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU -cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS -T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK -AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 -nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep -qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA -yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs -hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX -zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv -kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT -f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA -uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB -o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih -MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E -BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 -wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 -XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 -JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j -ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV -VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx -xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on -AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d -7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj -gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV -+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo -FGWsJwt0ivKH ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. -# Label: "TrustAsia Global Root CA G4" -# Serial: 451799571007117016466790293371524403291602933463 -# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb -# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a -# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c ------BEGIN CERTIFICATE----- -MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw -WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y -MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD -VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz -dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx -s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw -LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij -YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD -pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE -AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR -UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj -/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS ECC Root 2020" -# Serial: 72082518505882327255703894282316633856 -# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd -# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec -# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 ------BEGIN CERTIFICATE----- -MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw -CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH -bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw -MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx -JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE -AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O -tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP -f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA -MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di -z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn -27iQ7t0l ------END CERTIFICATE----- - -# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH -# Label: "Telekom Security TLS RSA Root 2023" -# Serial: 44676229530606711399881795178081572759 -# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 -# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 -# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj -MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 -eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy -MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC -REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG -A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 -cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV -cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA -U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 -Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug -BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy -8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J -co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg -8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 -rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 -mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg -+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX -gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 -p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ -pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm -9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw -M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd -GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ -CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t -xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ -w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK -L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj -X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q -ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm -dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= ------END CERTIFICATE----- - -# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA -# Label: "TWCA CYBER Root CA" -# Serial: 85076849864375384482682434040119489222 -# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 -# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 -# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ -MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 -IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 -WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO -LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg -Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P -40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF -avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ -34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i -JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu -j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf -Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP -2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA -S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA -oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC -kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW -5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd -BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB -AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t -tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn -68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn -TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t -RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx -f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI -Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz -8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 -NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX -xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 -t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA14" -# Serial: 575790784512929437950770173562378038616896959179 -# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 -# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f -# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM -BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u -LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw -NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD -eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS -b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ -FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg -vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy -6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo -/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J -kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ -0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib -y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac -18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs -0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB -SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL -ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk -86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E -rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib -ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT -zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS -DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 -2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo -FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy -K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 -dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl -Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB -365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c -JRNItX+S ------END CERTIFICATE----- - -# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. -# Label: "SecureSign Root CA15" -# Serial: 126083514594751269499665114766174399806381178503 -# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 -# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d -# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a ------BEGIN CERTIFICATE----- -MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw -UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM -dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy -NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl -cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 -IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 -wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR -ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT -9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp -4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 -bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST BR Root CA 2 2023" -# Serial: 153168538924886464690566649552453098598 -# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 -# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 -# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw -OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr -i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE -gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 -k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT -Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl -2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U -cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP -/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS -uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ -0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N -DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ -XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 -GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI -FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n -riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR -VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc -LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn -4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD -hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG -koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 -ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS -Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 -knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ -hJ65bvspmZDogNOfJA== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS ECC Root CA" -# Serial: 310892014698942880364840003424242768478804666567 -# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c -# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 -# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 ------BEGIN CERTIFICATE----- -MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw -WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs -IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw -NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE -ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB -c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ -AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp -guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw -DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 -L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR -OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== ------END CERTIFICATE----- - -# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. -# Label: "TrustAsia TLS RSA Root CA" -# Serial: 160405846464868906657516898462547310235378010780 -# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 -# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa -# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 ------BEGIN CERTIFICATE----- -MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM -BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp -ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN -MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG -A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 -c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC -AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ -NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ -Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 -HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 -ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb -xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX -i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ -UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j -TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT -bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 -S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT -MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 -Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 -iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt -7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp -2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ -g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj -pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M -pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP -XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe -SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 -ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy -323imttUQ/hHWKNddBWcwauwxzQ= ------END CERTIFICATE----- - -# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH -# Label: "D-TRUST EV Root CA 2 2023" -# Serial: 139766439402180512324132425437959641711 -# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 -# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b -# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce ------BEGIN CERTIFICATE----- -MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI -MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE -LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw -OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi -MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK -F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE -7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe -EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 -lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb -RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV -jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc -jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx -TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ -ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk -hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF -NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH -kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y -XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 -QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 -pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q -3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU -t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX -cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 -ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT -2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs -7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP -gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst -Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh -XBxvWHZks/wCuPWdCg== ------END CERTIFICATE----- - -# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG -# Label: "SwissSign RSA TLS Root CA 2022 - 1" -# Serial: 388078645722908516278762308316089881486363258315 -# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 -# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce -# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 ------BEGIN CERTIFICATE----- -MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE -AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx -MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT -d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg -MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX -vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 -LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX -5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE -EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt -/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x -0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 -KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM -0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd -OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta -clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK -wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 -DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL -BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 -10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz -Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ -iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc -gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM -ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF -LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp -zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td -Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 -rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO -gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation -# Label: "OISTE Server Root ECC G1" -# Serial: 47819833811561661340092227008453318557 -# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 -# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 -# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 ------BEGIN CERTIFICATE----- -MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw -CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY -T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy -NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp -b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy -cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N -2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 -TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C -tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR -QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD -YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= ------END CERTIFICATE----- - -# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation -# Label: "OISTE Server Root RSA G1" -# Serial: 113845518112613905024960613408179309848 -# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 -# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e -# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL -MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE -AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 -MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k -YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM -vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b -rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk -ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z -O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R -tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS -jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh -sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho -mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu -+zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR -i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT -kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU -8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 -zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 -I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG -5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 -qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP -AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk -gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs -YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 -9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome -/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 -J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 -wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy -BiElxky8j3C7DOReIoMt0r7+hVu05L0= ------END CERTIFICATE----- - -# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. -# Label: "e-Szigno TLS Root CA 2023" -# Serial: 71934828665710877219916191754 -# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 -# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe -# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 ------BEGIN CERTIFICATE----- -MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD -VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 -ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU -TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow -dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy -b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T -emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE -AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS -AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v -SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K -ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI -zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt -y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl -C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 -uWWL ------END CERTIFICATE----- diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py deleted file mode 100644 index 1c9661cc7c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/core.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -certifi.py -~~~~~~~~~~ - -This module returns the installation location of cacert.pem or its contents. -""" -import sys -import atexit - -def exit_cacert_ctx() -> None: - _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] - - -if sys.version_info >= (3, 11): - - from importlib.resources import as_file, files - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the file - # in cases where we're inside of a zipimport situation until someone - # actually calls where(), but we don't want to re-extract the file - # on every call of where(), so we'll do it once then store it in a - # global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you to - # manage the cleanup of this file, so it doesn't actually return a - # path, it returns a context manager that will give you the path - # when you enter it and will do any cleanup when you leave it. In - # the common case of not needing a temporary file, it will just - # return the file system location and the __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") - -else: - - from importlib.resources import path as get_path, read_text - - _CACERT_CTX = None - _CACERT_PATH = None - - def where() -> str: - # This is slightly terrible, but we want to delay extracting the - # file in cases where we're inside of a zipimport situation until - # someone actually calls where(), but we don't want to re-extract - # the file on every call of where(), so we'll do it once then store - # it in a global variable. - global _CACERT_CTX - global _CACERT_PATH - if _CACERT_PATH is None: - # This is slightly janky, the importlib.resources API wants you - # to manage the cleanup of this file, so it doesn't actually - # return a path, it returns a context manager that will give - # you the path when you enter it and will do any cleanup when - # you leave it. In the common case of not needing a temporary - # file, it will just return the file system location and the - # __exit__() is a no-op. - # - # We also have to hold onto the actual context manager, because - # it will do the cleanup whenever it gets garbage collected, so - # we will also store that at the global level as well. - _CACERT_CTX = get_path("certifi", "cacert.pem") - _CACERT_PATH = str(_CACERT_CTX.__enter__()) - atexit.register(exit_cacert_ctx) - - return _CACERT_PATH - - def contents() -> str: - return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/certifi/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py deleted file mode 100644 index 8ce8d739c9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from sentry_sdk import metrics, profiler - -from sentry_sdk.scope import Scope # isort: skip -from sentry_sdk.client import Client # isort: skip -from sentry_sdk.consts import VERSION -from sentry_sdk.transport import HttpTransport, Transport - -from sentry_sdk.api import * # noqa # isort: skip - -__all__ = [ # noqa - "Hub", - "Scope", - "Client", - "Transport", - "HttpTransport", - "VERSION", - "integrations", - # From sentry_sdk.api - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "logger", - "metrics", - "profiler", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - -# Initialize the debug support after everything is loaded -from sentry_sdk.debug import init_debug_support - -init_debug_support() -del init_debug_support - -# circular imports -from sentry_sdk.hub import Hub diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py deleted file mode 100644 index 565fac2a2d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_batcher.py +++ /dev/null @@ -1,184 +0,0 @@ -import os -import random -import threading -import weakref -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Generic, TypeVar - -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - -T = TypeVar("T") - - -class Batcher(Generic[T]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "" - CONTENT_TYPE = "" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - self._buffer: "list[T]" = [] - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._flush_event: "threading.Event" = threading.Event() - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._buffer = [] - self._running = True - self._lock = threading.Lock() - self._active = threading.local() - self._flush_event = threading.Event() - self._flusher = None - self._flusher_pid = None - - def _ensure_thread(self) -> bool: - """For forking processes we might need to restart this thread. - This ensures that our process actually has that thread running. - """ - if not self._running: - return False - - pid = os.getpid() - if self._flusher_pid == pid: - return True - - with self._lock: - # Recheck to make sure another thread didn't get here and start the - # the flusher in the meantime - if self._flusher_pid == pid: - return True - - self._flusher_pid = pid - - self._flusher = threading.Thread(target=self._flush_loop) - self._flusher.daemon = True - - try: - self._flusher.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return False - - return True - - def _flush_loop(self) -> None: - # Mark the flush-loop thread as active for its entire lifetime so - # that any re-entrant add() triggered by GC warnings during wait(), - # flush(), or Event operations is silently dropped instead of - # deadlocking on internal locks. - self._active.flag = True - while self._running: - self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) - self._flush_event.clear() - self._flush() - - def add(self, item: "T") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - if len(self._buffer) >= self.MAX_BEFORE_DROP: - self._record_lost(item) - return None - - self._buffer.append(item) - if len(self._buffer) >= self.MAX_BEFORE_FLUSH: - self._flush_event.set() - finally: - self._active.flag = False - - def kill(self) -> None: - if self._flusher is None: - return - - self._running = False - self._flush_event.set() - self._flusher = None - - def flush(self) -> None: - was_active = getattr(self._active, "flag", False) - self._active.flag = True - try: - self._flush() - finally: - self._active.flag = was_active - - def _add_to_envelope(self, envelope: "Envelope") -> None: - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": len(self._buffer), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(item) for item in self._buffer - ], - } - ), - ) - ) - - def _flush(self) -> "Optional[Envelope]": - envelope = Envelope( - headers={"sent_at": format_timestamp(datetime.now(timezone.utc))} - ) - with self._lock: - if len(self._buffer) == 0: - return None - - self._add_to_envelope(envelope) - self._buffer.clear() - - self._capture_func(envelope) - return envelope - - def _record_lost(self, item: "T") -> None: - pass - - @staticmethod - def _to_transport_format(item: "T") -> "Any": - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py deleted file mode 100644 index f62175c09f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_compat.py +++ /dev/null @@ -1,92 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, TypeVar - - T = TypeVar("T") - - -PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 -PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8 -PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 -PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 - - -def with_metaclass(meta: "Any", *bases: "Any") -> "Any": - class MetaClass(type): - def __new__(metacls: "Any", name: "Any", this_bases: "Any", d: "Any") -> "Any": - return meta(name, bases, d) - - return type.__new__(MetaClass, "temporary_class", (), {}) - - -def check_uwsgi_thread_support() -> bool: - # We check two things here: - # - # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if - # that's the case. - # - # 2. Additionally, if uWSGI is running in preforking mode (default), it needs - # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This - # is because any background threads spawned before the main process is - # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if - # --enable-threads is on. One has to explicitly provide - # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython - # after-fork hooks that take care of cleaning up stale thread data. - try: - from uwsgi import opt # type: ignore - except ImportError: - return True - - from sentry_sdk.consts import FALSE_VALUES - - def enabled(option: str) -> bool: - value = opt.get(option, False) - if isinstance(value, bool): - return value - - if isinstance(value, bytes): - try: - value = value.decode() - except Exception: - pass - - return value and str(value).lower() not in FALSE_VALUES # type: ignore[return-value] - - # When `threads` is passed in as a uwsgi option, - # `enable-threads` is implied on. - threads_enabled = "threads" in opt or enabled("enable-threads") - fork_hooks_on = enabled("py-call-uwsgi-fork-hooks") - lazy_mode = enabled("lazy-apps") or enabled("lazy") - - if lazy_mode and not threads_enabled: - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI without thread support. " - "This might lead to unexpected issues. " - 'Please run uWSGI with "--enable-threads" for full support.' - ) - ) - - return False - - elif not lazy_mode and (not threads_enabled or not fork_hooks_on): - from warnings import warn - - warn( - Warning( - "IMPORTANT: " - "We detected the use of uWSGI in preforking mode without " - "thread support. This might lead to crashing workers. " - 'Please run uWSGI with both "--enable-threads" and ' - '"--py-call-uwsgi-fork-hooks" for full support.' - ) - ) - - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py deleted file mode 100644 index 923fcf6df8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_init_implementation.py +++ /dev/null @@ -1,78 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -import sentry_sdk - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional - - import sentry_sdk.consts - - -class _InitGuard: - _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = ( - "Using the return value of sentry_sdk.init as a context manager " - "and manually calling the __enter__ and __exit__ methods on the " - "return value are deprecated. We are no longer maintaining this " - "functionality, and we will remove it in the next major release." - ) - - def __init__(self, client: "sentry_sdk.Client") -> None: - self._client = client - - def __enter__(self) -> "_InitGuard": - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - warnings.warn( - self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE, - stacklevel=2, - category=DeprecationWarning, - ) - - c = self._client - if c is not None: - c.close() - - -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass - - -def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": - """Initializes the SDK and optionally integrations. - - This takes the same arguments as the client constructor. - """ - client = sentry_sdk.Client(*args, **kwargs) - sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() - rv = _InitGuard(client) - return rv - - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `init` is a type to - # have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `ContextManager[Any]` to tell static analyzers about the return type. - - class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801 - pass - -else: - # Alias `init` for actual usage. Go through the lambda indirection to throw - # PyCharm off of the weakly typed signature (it would otherwise discover - # both the weakly typed signature of `_init` and our faked `init` type). - - init = (lambda: _init)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py deleted file mode 100644 index 0719932ee9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_log_batcher.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Item, PayloadRef -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Log - - -class LogBatcher(Batcher["Log"]): - MAX_BEFORE_FLUSH = 100 - MAX_BEFORE_DROP = 1_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "log" - CONTENT_TYPE = "application/vnd.sentry.items.log+json" - - @staticmethod - def _to_transport_format(item: "Log") -> "Any": - if "sentry.severity_number" not in item["attributes"]: - item["attributes"]["sentry.severity_number"] = item["severity_number"] - if "sentry.severity_text" not in item["attributes"]: - item["attributes"]["sentry.severity_text"] = item["severity_text"] - - res = { - "timestamp": int(item["time_unix_nano"]) / 1.0e9, - "level": str(item["severity_text"]), - "body": str(item["body"]), - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - return res - - def _record_lost(self, item: "Log") -> None: - # Construct log envelope item without sending it to report lost bytes - log_item = Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": 1, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [self._to_transport_format(item)], - } - ), - ) - - self._record_lost_func( - reason="queue_overflow", - data_category="log_item", - item=log_item, - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py deleted file mode 100644 index 16c238bcab..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_lru_cache.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -_SENTINEL = object() - - -class LRUCache: - def __init__(self, max_size: int) -> None: - if max_size <= 0: - raise AssertionError(f"invalid max_size: {max_size}") - self.max_size = max_size - self._data: "dict[Any, Any]" = {} - self.hits = self.misses = 0 - self.full = False - - def set(self, key: "Any", value: "Any") -> None: - current = self._data.pop(key, _SENTINEL) - if current is not _SENTINEL: - self._data[key] = value - elif self.full: - self._data.pop(next(iter(self._data))) - self._data[key] = value - else: - self._data[key] = value - self.full = len(self._data) >= self.max_size - - def get(self, key: "Any", default: "Any" = None) -> "Any": - try: - ret = self._data.pop(key) - except KeyError: - self.misses += 1 - ret = default - else: - self.hits += 1 - self._data[key] = ret - - return ret - - def get_all(self) -> "list[tuple[Any, Any]]": - return list(self._data.items()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py deleted file mode 100644 index 06bce1a282..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_metrics_batcher.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.utils import serialize_attribute - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Metric - - -class MetricsBatcher(Batcher["Metric"]): - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 10_000 - FLUSH_WAIT_TIME = 5.0 - - TYPE = "trace_metric" - CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json" - - @staticmethod - def _to_transport_format(item: "Metric") -> "Any": - res = { - "timestamp": item["timestamp"], - "name": item["name"], - "type": item["type"], - "value": item["value"], - "attributes": { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - }, - } - - if item.get("trace_id") is not None: - res["trace_id"] = item["trace_id"] - - if item.get("span_id") is not None: - res["span_id"] = item["span_id"] - - if item.get("unit") is not None: - res["unit"] = item["unit"] - - return res - - def _record_lost(self, item: "Metric") -> None: - self._record_lost_func( - reason="queue_overflow", - data_category="trace_metric", - quantity=1, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py deleted file mode 100644 index c28c8de9ac..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_queue.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -A fork of Python 3.6's stdlib queue (found in Pythons 'cpython/Lib/queue.py') -with Lock swapped out for RLock to avoid a deadlock while garbage collecting. - -https://github.com/python/cpython/blob/v3.6.12/Lib/queue.py - - -See also -https://codewithoutrules.com/2017/08/16/concurrency-python/ -https://bugs.python.org/issue14976 -https://github.com/sqlalchemy/sqlalchemy/blob/4eb747b61f0c1b1c25bdee3856d7195d10a0c227/lib/sqlalchemy/queue.py#L1 - -We also vendor the code to evade eventlet's broken monkeypatching, see -https://github.com/getsentry/sentry-python/pull/484 - - -Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; - -All Rights Reserved - - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative version -prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - -""" - -import threading -from collections import deque -from time import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - -__all__ = ["EmptyError", "FullError", "Queue"] - - -class EmptyError(Exception): - "Exception raised by Queue.get(block=0)/get_nowait()." - - pass - - -class FullError(Exception): - "Exception raised by Queue.put(block=0)/put_nowait()." - - pass - - -class Queue: - """Create a queue object with a given maximum size. - - If maxsize is <= 0, the queue size is infinite. - """ - - def __init__(self, maxsize=0): - self.maxsize = maxsize - self._init(maxsize) - - # mutex must be held whenever the queue is mutating. All methods - # that acquire mutex must release it before returning. mutex - # is shared between the three conditions, so acquiring and - # releasing the conditions also acquires and releases mutex. - self.mutex = threading.RLock() - - # Notify not_empty whenever an item is added to the queue; a - # thread waiting to get is notified then. - self.not_empty = threading.Condition(self.mutex) - - # Notify not_full whenever an item is removed from the queue; - # a thread waiting to put is notified then. - self.not_full = threading.Condition(self.mutex) - - # Notify all_tasks_done whenever the number of unfinished tasks - # drops to zero; thread waiting to join() is notified to resume - self.all_tasks_done = threading.Condition(self.mutex) - self.unfinished_tasks = 0 - - def task_done(self): - """Indicate that a formerly enqueued task is complete. - - Used by Queue consumer threads. For each get() used to fetch a task, - a subsequent call to task_done() tells the queue that the processing - on the task is complete. - - If a join() is currently blocking, it will resume when all items - have been processed (meaning that a task_done() call was received - for every item that had been put() into the queue). - - Raises a ValueError if called more times than there were items - placed in the queue. - """ - with self.all_tasks_done: - unfinished = self.unfinished_tasks - 1 - if unfinished <= 0: - if unfinished < 0: - raise ValueError("task_done() called too many times") - self.all_tasks_done.notify_all() - self.unfinished_tasks = unfinished - - def join(self): - """Blocks until all items in the Queue have been gotten and processed. - - The count of unfinished tasks goes up whenever an item is added to the - queue. The count goes down whenever a consumer thread calls task_done() - to indicate the item was retrieved and all work on it is complete. - - When the count of unfinished tasks drops to zero, join() unblocks. - """ - with self.all_tasks_done: - while self.unfinished_tasks: - self.all_tasks_done.wait() - - def qsize(self): - """Return the approximate size of the queue (not reliable!).""" - with self.mutex: - return self._qsize() - - def empty(self): - """Return True if the queue is empty, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() == 0 - as a direct substitute, but be aware that either approach risks a race - condition where a queue can grow before the result of empty() or - qsize() can be used. - - To create code that needs to wait for all queued tasks to be - completed, the preferred technique is to use the join() method. - """ - with self.mutex: - return not self._qsize() - - def full(self): - """Return True if the queue is full, False otherwise (not reliable!). - - This method is likely to be removed at some point. Use qsize() >= n - as a direct substitute, but be aware that either approach risks a race - condition where a queue can shrink before the result of full() or - qsize() can be used. - """ - with self.mutex: - return 0 < self.maxsize <= self._qsize() - - def put(self, item, block=True, timeout=None): - """Put an item into the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until a free slot is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the FullError exception if no free slot was available within that time. - Otherwise ('block' is false), put an item on the queue if a free slot - is immediately available, else raise the FullError exception ('timeout' - is ignored in that case). - """ - with self.not_full: - if self.maxsize > 0: - if not block: - if self._qsize() >= self.maxsize: - raise FullError() - elif timeout is None: - while self._qsize() >= self.maxsize: - self.not_full.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while self._qsize() >= self.maxsize: - remaining = endtime - time() - if remaining <= 0.0: - raise FullError() - self.not_full.wait(remaining) - self._put(item) - self.unfinished_tasks += 1 - self.not_empty.notify() - - def get(self, block=True, timeout=None): - """Remove and return an item from the queue. - - If optional args 'block' is true and 'timeout' is None (the default), - block if necessary until an item is available. If 'timeout' is - a non-negative number, it blocks at most 'timeout' seconds and raises - the EmptyError exception if no item was available within that time. - Otherwise ('block' is false), return an item if one is immediately - available, else raise the EmptyError exception ('timeout' is ignored - in that case). - """ - with self.not_empty: - if not block: - if not self._qsize(): - raise EmptyError() - elif timeout is None: - while not self._qsize(): - self.not_empty.wait() - elif timeout < 0: - raise ValueError("'timeout' must be a non-negative number") - else: - endtime = time() + timeout - while not self._qsize(): - remaining = endtime - time() - if remaining <= 0.0: - raise EmptyError() - self.not_empty.wait(remaining) - item = self._get() - self.not_full.notify() - return item - - def put_nowait(self, item): - """Put an item into the queue without blocking. - - Only enqueue the item if a free slot is immediately available. - Otherwise raise the FullError exception. - """ - return self.put(item, block=False) - - def get_nowait(self): - """Remove and return an item from the queue without blocking. - - Only get an item if one is immediately available. Otherwise - raise the EmptyError exception. - """ - return self.get(block=False) - - # Override these methods to implement other queue organizations - # (e.g. stack or priority queue). - # These will only be called with appropriate locks held - - # Initialize the queue representation - def _init(self, maxsize): - self.queue: "Any" = deque() - - def _qsize(self): - return len(self.queue) - - # Put a new item in the queue - def _put(self, item): - self.queue.append(item) - - # Get an item from the queue - def _get(self): - return self.queue.popleft() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py deleted file mode 100644 index 79285c3386..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_span_batcher.py +++ /dev/null @@ -1,234 +0,0 @@ -import os -import random -import threading -import time -import weakref -from collections import defaultdict -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._batcher import Batcher -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import format_timestamp, serialize_attribute - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - from sentry_sdk._types import SpanJSON - - -class SpanBatcher(Batcher["SpanJSON"]): - # MAX_BEFORE_FLUSH should be lower than MAX_BEFORE_DROP, so that there is - # a bit of a buffer for spans that appear between the trigger to flush - # and actually flushing the buffer. - # - # The max limits are all per trace (per bucket). - MAX_ENVELOPE_SIZE = 1000 # spans - MAX_BEFORE_FLUSH = 1000 - MAX_BEFORE_DROP = 2000 - MAX_BYTES_BEFORE_FLUSH = 5 * 1024 * 1024 # 5 MB - - FLUSH_WAIT_TIME = 5.0 - - TYPE = "span" - CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json" - - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - record_lost_func: "Callable[..., None]", - ) -> None: - # Spans from different traces cannot be emitted in the same envelope - # since the envelope contains a shared trace header. That's why we bucket - # by trace_id, so that we can then send the buckets each in its own - # envelope. - # trace_id -> span buffer - self._span_buffer: dict[str, list["SpanJSON"]] = defaultdict(list) - self._running_size: dict[str, int] = defaultdict(lambda: 0) - self._capture_func = capture_func - self._record_lost_func = record_lost_func - self._running = True - self._lock = threading.Lock() - self._active: "threading.local" = threading.local() - - self._last_full_flush: float = time.monotonic() # drives time-based flushes - self._flush_event = threading.Event() - self._pending_flush: set[str] = set() # buckets to be flushed - - self._flusher: "Optional[threading.Thread]" = None - self._flusher_pid: "Optional[int]" = None - - # See https://github.com/getsentry/sentry-python/blob/051cc01640a29bfd64b1f1e2e3414c02f027dd1b/sentry_sdk/monitor.py#L41-L50 - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._span_buffer = defaultdict(list) - self._running_size = defaultdict(lambda: 0) - self._running = True - - self._lock = threading.Lock() - self._active = threading.local() - - self._last_full_flush = time.monotonic() - self._flush_event = threading.Event() - self._pending_flush = set() - - self._flusher = None - self._flusher_pid = None - - def _flush_loop(self) -> None: - self._active.flag = True - while self._running: - jitter = random.random() * self.FLUSH_WAIT_TIME * 0.1 - self._flush_event.wait(timeout=self.FLUSH_WAIT_TIME + jitter) - self._flush_event.clear() - - self._flush(only_pending=True) - - if ( - time.monotonic() - self._last_full_flush - >= self.FLUSH_WAIT_TIME + jitter - ): - self._flush() - self._last_full_flush = time.monotonic() - - def add(self, span: "SpanJSON") -> None: - # Bail out if the current thread is already executing batcher code. - # This prevents deadlocks when code running inside the batcher (e.g. - # _add_to_envelope during flush, or _flush_event.wait/set) triggers - # a GC-emitted warning that routes back through the logging - # integration into add(). - if getattr(self._active, "flag", False): - return None - - self._active.flag = True - - try: - if not self._ensure_thread() or self._flusher is None: - return None - - with self._lock: - size = len(self._span_buffer[span["trace_id"]]) - if size >= self.MAX_BEFORE_DROP: - self._record_lost_func( - reason="queue_overflow", - data_category="span", - quantity=1, - ) - return None - - self._span_buffer[span["trace_id"]].append(span) - self._running_size[span["trace_id"]] += self._estimate_size(span) - - if ( - size + 1 >= self.MAX_BEFORE_FLUSH - or self._running_size[span["trace_id"]] - >= self.MAX_BYTES_BEFORE_FLUSH - ): - self._pending_flush.add(span["trace_id"]) - notify = True - else: - notify = False - - if notify: - self._flush_event.set() - finally: - self._active.flag = False - - @staticmethod - def _estimate_size(item: "SpanJSON") -> int: - # Rough estimate of serialized span size that's quick to compute. - # 210 is the rough size of the payload without attributes, and then we - # estimate the attributes separately. - estimate = 210 - for value in (item.get("attributes") or {}).values(): - estimate += 50 - - if isinstance(value, str): - estimate += len(value) - else: - estimate += len(str(value)) - - return estimate - - @staticmethod - def _to_transport_format(item: "SpanJSON") -> "Any": - res = {k: v for k, v in item.items() if k not in ("_segment_span",)} - - if item.get("attributes"): - res["attributes"] = { - k: serialize_attribute(v) for (k, v) in item["attributes"].items() - } - else: - del res["attributes"] - - return res - - def _flush(self, only_pending: bool = False) -> None: - with self._lock: - if only_pending: - buckets = list(self._pending_flush) - else: - # flush whole buffer, e.g. if the SDK is shutting down - buckets = list(self._span_buffer.keys()) - - self._pending_flush.clear() - - if not buckets: - return - - envelopes = [] - - for bucket_id in buckets: - spans = self._span_buffer.get(bucket_id) - if not spans: - continue - - dsc = spans[0]["_segment_span"]._dynamic_sampling_context() - - # Max per envelope is 1000, so if we happen to have more than - # 1000 spans in one bucket, we'll need to separate them. - for start in range(0, len(spans), self.MAX_ENVELOPE_SIZE): - end = min(start + self.MAX_ENVELOPE_SIZE, len(spans)) - - envelope = Envelope( - headers={ - "sent_at": format_timestamp(datetime.now(timezone.utc)), - "trace": dsc, - } - ) - - envelope.add_item( - Item( - type=self.TYPE, - content_type=self.CONTENT_TYPE, - headers={ - "item_count": end - start, - }, - payload=PayloadRef( - json={ - "version": 2, - "items": [ - self._to_transport_format(spans[j]) - for j in range(start, end) - ], - } - ), - ) - ) - - envelopes.append(envelope) - - del self._span_buffer[bucket_id] - del self._running_size[bucket_id] - - for envelope in envelopes: - self._capture_func(envelope) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py deleted file mode 100644 index fbd2578048..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_types.py +++ /dev/null @@ -1,481 +0,0 @@ -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING, TypeVar, Union - -# Re-exported for compat, since code out there in the wild might use this variable. -MYPY = TYPE_CHECKING - - -SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" -BLOB_DATA_SUBSTITUTE = "[Blob substitute]" -OVER_SIZE_LIMIT_SUBSTITUTE = "[Exceeds maximum size]" -UNPARSABLE_RAW_DATA_SUBSTITUTE = "[Unparsable]" - - -class AnnotatedValue: - """ - Meta information for a data field in the event payload. - This is to tell Relay that we have tampered with the fields value. - See: - https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423 - """ - - __slots__ = ("value", "metadata") - - def __init__(self, value: "Optional[Any]", metadata: "Dict[str, Any]") -> None: - self.value = value - self.metadata = metadata - - def __eq__(self, other: "Any") -> bool: - if not isinstance(other, AnnotatedValue): - return False - - return self.value == other.value and self.metadata == other.metadata - - def __str__(self: "AnnotatedValue") -> str: - return str({"value": str(self.value), "metadata": str(self.metadata)}) - - def __len__(self: "AnnotatedValue") -> int: - if self.value is not None: - return len(self.value) - else: - return 0 - - @classmethod - def removed_because_raw_data(cls) -> "AnnotatedValue": - """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form.""" - # This is the legacy approach - we want to transition over to `substituted_because_raw_data` after we completely transition - # to span-first - return AnnotatedValue( - value="", - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_raw_data(cls) -> "AnnotatedValue": - """The value was replaced because it could not be parsed. This is done for request body values that are not json nor a form.""" - return AnnotatedValue( - value=UNPARSABLE_RAW_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!raw", # Unparsable raw data - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def removed_because_over_size_limit(cls, value: "Any" = "") -> "AnnotatedValue": - """ - The actual value was removed because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - # This is the legacy approach - we want to transition over to `substituted_because_over_size_limit` after we completely transition - # to span-first - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "x", # The fields original value was removed - ] - ] - }, - ) - - @classmethod - def substituted_because_over_size_limit( - cls, value: "Any" = OVER_SIZE_LIMIT_SUBSTITUTE - ) -> "AnnotatedValue": - """ - The actual value was replaced because the size of the field exceeded the configured maximum size, - for example specified with the max_request_body_size sdk option. - """ - return AnnotatedValue( - value=value, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of configured maximum size - "s", # The fields original value was substituted - ] - ] - }, - ) - - @classmethod - def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue": - """The actual value was removed because it contained sensitive information.""" - return AnnotatedValue( - value=SENSITIVE_DATA_SUBSTITUTE, - metadata={ - "rem": [ # Remark - [ - "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) - "s", # The fields original value was substituted - ] - ] - }, - ) - - -T = TypeVar("T") -Annotated = Union[AnnotatedValue, T] - - -if TYPE_CHECKING: - from collections.abc import Container, MutableMapping, Sequence - from datetime import datetime - from types import TracebackType - from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - - class SDKInfo(TypedDict): - name: str - version: str - packages: "Sequence[Mapping[str, str]]" - - class KeyValueCollectionBehaviour(TypedDict): - mode: 'Literal["off", "denylist", "allowlist"]' - 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 HttpHeadersCollectionUserOptions(TypedDict, total=False): - request: "KeyValueCollectionBehaviour" - - class HttpHeadersCollectionBehaviour(TypedDict): - request: "KeyValueCollectionBehaviour" - - class DataCollectionUserOptions(TypedDict, total=False): - user_info: bool - cookies: "KeyValueCollectionBehaviour" - http_headers: "HttpHeadersCollectionUserOptions" - http_bodies: "List[str]" - query_params: "KeyValueCollectionBehaviour" - graphql: "GraphQLCollectionUserOptions" - gen_ai: "GenAICollectionUserOptions" - database_query_data: bool - queues: bool - 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_query_data: bool - queues: bool - 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"] - - DurationUnit = Literal[ - "nanosecond", - "microsecond", - "millisecond", - "second", - "minute", - "hour", - "day", - "week", - ] - - InformationUnit = Literal[ - "bit", - "byte", - "kilobyte", - "kibibyte", - "megabyte", - "mebibyte", - "gigabyte", - "gibibyte", - "terabyte", - "tebibyte", - "petabyte", - "pebibyte", - "exabyte", - "exbibyte", - ] - - FractionUnit = Literal["ratio", "percent"] - MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] - - MeasurementValue = TypedDict( - "MeasurementValue", - { - "value": float, - "unit": NotRequired[Optional[MeasurementUnit]], - }, - ) - - Event = TypedDict( - "Event", - { - "breadcrumbs": Annotated[ - dict[Literal["values"], list[dict[str, Any]]] - ], # TODO: We can expand on this type - "check_in_id": str, - "contexts": dict[str, dict[str, object]], - "dist": str, - "duration": Optional[float], - "environment": Optional[str], - "errors": list[dict[str, Any]], # TODO: We can expand on this type - "event_id": str, - "exception": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "extra": MutableMapping[str, object], - "fingerprint": list[str], - "level": LogLevelStr, - "logentry": Mapping[str, object], - "logger": str, - "measurements": dict[str, MeasurementValue], - "message": str, - "modules": dict[str, str], - "monitor_config": Mapping[str, object], - "monitor_slug": Optional[str], - "platform": Literal["python"], - "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports - "release": Optional[str], - "request": dict[str, object], - "sdk": Mapping[str, object], - "server_name": str, - "spans": Annotated[list[dict[str, object]]], - "stacktrace": dict[ - str, object - ], # We access this key in the code, but I am unsure whether we ever set it - "start_timestamp": datetime, - "status": Optional[str], - "tags": MutableMapping[ - str, str - ], # Tags must be less than 200 characters each - "threads": dict[ - Literal["values"], list[dict[str, Any]] - ], # TODO: We can expand on this type - "timestamp": Optional[datetime], # Must be set before sending the event - "transaction": str, - "transaction_info": Mapping[str, Any], # TODO: We can expand on this type - "type": Literal["check_in", "transaction"], - "user": dict[str, object], - "_dropped_spans": int, - "_has_gen_ai_span": bool, - }, - total=False, - ) - - ExcInfo = Union[ - tuple[Type[BaseException], BaseException, Optional[TracebackType]], - tuple[None, None, None], - ] - - # TODO: Make a proper type definition for this (PRs welcome!) - Hint = Dict[str, Any] - - AttributeValue = ( - str - | bool - | float - | int - | list[str] - | list[bool] - | list[float] - | list[int] - | tuple[str, ...] - | tuple[bool, ...] - | tuple[float, ...] - | tuple[int, ...] - ) - Attributes = dict[str, AttributeValue] - - SerializedAttributeValue = TypedDict( - # https://develop.sentry.dev/sdk/telemetry/attributes/#supported-types - "SerializedAttributeValue", - { - "type": Literal[ - "string", - "boolean", - "double", - "integer", - "array", - ], - "value": AttributeValue, - }, - ) - - Log = TypedDict( - "Log", - { - "severity_text": str, - "severity_number": int, - "body": str, - "attributes": Attributes, - "time_unix_nano": int, - "trace_id": Optional[str], - "span_id": Optional[str], - }, - ) - - MetricType = Literal["counter", "gauge", "distribution"] - MetricUnit = Union[DurationUnit, InformationUnit, str] - - Metric = TypedDict( - "Metric", - { - "timestamp": float, - "trace_id": Optional[str], - "span_id": Optional[str], - "name": str, - "type": MetricType, - "value": float, - "unit": Optional[MetricUnit], - "attributes": Attributes, - }, - ) - - MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] - - SpanJSON = TypedDict( - "SpanJSON", - { - "trace_id": str, - "span_id": str, - "parent_span_id": NotRequired[str], - "name": str, - "status": str, - "is_segment": bool, - "start_timestamp": float, - "end_timestamp": NotRequired[float], - "attributes": NotRequired[Attributes], - "_segment_span": NotRequired["sentry_sdk.traces.StreamedSpan"], - }, - ) - - # TODO: Make a proper type definition for this (PRs welcome!) - Breadcrumb = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - BreadcrumbHint = Dict[str, Any] - - # TODO: Make a proper type definition for this (PRs welcome!) - SamplingContext = Dict[str, Any] - - EventProcessor = Callable[[Event, Hint], Optional[Event]] - ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] - BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] - TransactionProcessor = Callable[[Event, Hint], Optional[Event]] - LogProcessor = Callable[[Log, Hint], Optional[Log]] - - TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] - - # https://github.com/python/mypy/issues/5710 - NotImplementedType = Any - - EventDataCategory = Literal[ - "default", - "error", - "crash", - "transaction", - "security", - "attachment", - "session", - "internal", - "profile", - "profile_chunk", - "monitor", - "span", - "log_item", - "log_byte", - "trace_metric", - ] - SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] - - ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] - ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] - - MonitorConfigScheduleType = Literal["crontab", "interval"] - MonitorConfigScheduleUnit = Literal[ - "year", - "month", - "week", - "day", - "hour", - "minute", - "second", # not supported in Sentry and will result in a warning - ] - - MonitorConfigSchedule = TypedDict( - "MonitorConfigSchedule", - { - "type": MonitorConfigScheduleType, - "value": Union[int, str], - "unit": MonitorConfigScheduleUnit, - }, - total=False, - ) - - MonitorConfig = TypedDict( - "MonitorConfig", - { - "schedule": MonitorConfigSchedule, - "timezone": str, - "checkin_margin": int, - "max_runtime": int, - "failure_issue_threshold": int, - "recovery_threshold": int, - "owner": str, - }, - total=False, - ) - - HttpStatusCodeRange = Union[int, Container[int]] - - class TextPart(TypedDict): - type: Literal["text"] - content: str - - IgnoreSpansName = Union[str, Pattern[str]] - IgnoreSpansContext = TypedDict( - "IgnoreSpansContext", - {"name": IgnoreSpansName, "attributes": Attributes}, - total=False, - ) - IgnoreSpansConfig = list[Union[IgnoreSpansName, IgnoreSpansContext]] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py deleted file mode 100644 index 98f932267f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/_werkzeug.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Copyright (c) 2007 by the Pallets team. - -Some rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, Iterator, Optional, Tuple - - -# -# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` -# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 -# -# We need this function because Django does not give us a "pure" http header -# dict. So we might as well use it for all WSGI integrations. -# -def _get_headers(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns only proper HTTP headers. - """ - for key, value in environ.items(): - key = str(key) - if key.startswith("HTTP_") and key not in ( - "HTTP_CONTENT_TYPE", - "HTTP_CONTENT_LENGTH", - ): - yield key[5:].replace("_", "-").title(), value - elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): - yield key.replace("_", "-").title(), value - - -def _strip_default_port(host: str, scheme: "Optional[str]") -> str: - """Strip the port from the host if it's the default for the scheme.""" - if scheme == "http" and host.endswith(":80"): - return host[:-3] - if scheme == "https" and host.endswith(":443"): - return host[:-4] - return host - - -# `get_host` comes from `werkzeug.wsgi.get_host` -# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 - - -def get_host(environ: "Dict[str, str]", use_x_forwarded_for: bool = False) -> str: - """ - Return the host for the given WSGI environment. - """ - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: - return _strip_default_port(environ["HTTP_X_FORWARDED_HOST"], scheme) - elif environ.get("HTTP_HOST"): - return _strip_default_port(environ["HTTP_HOST"], scheme) - elif environ.get("SERVER_NAME"): - # SERVER_NAME/SERVER_PORT describe the internal server, so use - # wsgi.url_scheme (not the forwarded scheme) for port decisions. - rv = environ["SERVER_NAME"] - if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( - ("https", "443"), - ("http", "80"), - ): - rv += ":" + environ["SERVER_PORT"] - return rv - else: - # In spite of the WSGI spec, SERVER_NAME might not be present. - return "unknown" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py deleted file mode 100644 index 404e57ff1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .utils import ( - GEN_AI_MESSAGE_ROLE_MAPPING, # noqa: F401 - GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING, # noqa: F401 - normalize_message_role, # noqa: F401 - normalize_message_roles, # noqa: F401 - set_conversation_id, # noqa: F401 - set_data_normalized, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py deleted file mode 100644 index b5eb8c55ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_completions_api.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections.abc import Iterable -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.chat import ( - ChatCompletionContentPartParam, - ChatCompletionMessageParam, - ChatCompletionSystemMessageParam, - ) - - from sentry_sdk._types import TextPart - - -def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool: - return isinstance(message, dict) and message.get("role") == "system" - - -def _get_system_instructions( - messages: "Iterable[ChatCompletionMessageParam]", -) -> "list[ChatCompletionMessageParam]": - if not isinstance(messages, Iterable): - return [] - - return [message for message in messages if _is_system_instruction(message)] - - -def _get_text_items( - content: "Union[str, Iterable[ChatCompletionContentPartParam]]", -) -> "list[str]": - if isinstance(content, str): - return [content] - - if not isinstance(content, Iterable): - return [] - - text_items = [] - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - text = part.get("text", None) - if text is not None: - text_items.append(text) - - return text_items - - -def _transform_system_instructions( - system_instructions: "list[ChatCompletionSystemMessageParam]", -) -> "list[TextPart]": - instruction_text_parts: "list[TextPart]" = [] - - for instruction in system_instructions: - if not isinstance(instruction, dict): - continue - - content = instruction.get("content") - if content is None: - continue - - text_parts: "list[TextPart]" = [ - {"type": "text", "content": text} for text in _get_text_items(content) - ] - instruction_text_parts += text_parts - - return instruction_text_parts diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py deleted file mode 100644 index 8f751c3248..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/_openai_responses_api.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union - - from openai.types.responses import ResponseInputItemParam, ResponseInputParam - - -def _is_system_instruction(message: "ResponseInputItemParam") -> bool: - if not isinstance(message, dict) or not message.get("role") == "system": - return False - - return "type" not in message or message["type"] == "message" - - -def _get_system_instructions( - messages: "Union[str, ResponseInputParam]", -) -> "list[ResponseInputItemParam]": - if not isinstance(messages, list): - return [] - - return [message for message in messages if _is_system_instruction(message)] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py deleted file mode 100644 index 35ee4bd788..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/consts.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - -# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..." -DATA_URL_BASE64_REGEX = re.compile( - r"^data:(?:[a-zA-Z0-9][a-zA-Z0-9.+\-]*/[a-zA-Z0-9][a-zA-Z0-9.+\-]*)(?:;[a-zA-Z0-9\-]+=[^;,]*)*;base64,(?:[A-Za-z0-9+/\-_]+={0,2})$" -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py deleted file mode 100644 index d8840ad451..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/monitoring.py +++ /dev/null @@ -1,147 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk.utils -from sentry_sdk import start_span -from sentry_sdk.ai.utils import _set_span_data_attribute -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, TypeVar, Union - - F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) - -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) - - -def set_ai_pipeline_name(name: "Optional[str]") -> None: - _ai_pipeline_name.set(name) - - -def get_ai_pipeline_name() -> "Optional[str]": - return _ai_pipeline_name.get() - - -def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": - def decorator(f: "F") -> "F": - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return await f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = await f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "ai_monitoring", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - if inspect.iscoroutinefunction(f): - return wraps(f)(async_wrapped) # type: ignore - else: - return wraps(f)(sync_wrapped) # type: ignore - - return decorator - - -def record_token_usage( - span: "Union[Span, StreamedSpan]", - input_tokens: "Optional[int]" = None, - input_tokens_cached: "Optional[int]" = None, - input_tokens_cache_write: "Optional[int]" = None, - output_tokens: "Optional[int]" = None, - output_tokens_reasoning: "Optional[int]" = None, - total_tokens: "Optional[int]" = None, -) -> None: - # TODO: move pipeline name elsewhere - ai_pipeline_name = get_ai_pipeline_name() - if ai_pipeline_name: - _set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name) - - if input_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if input_tokens_cached is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - input_tokens_cached, - ) - - if input_tokens_cache_write is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - input_tokens_cache_write, - ) - - if output_tokens is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens - ) - - if output_tokens_reasoning is not None: - _set_span_data_attribute( - span, - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - output_tokens_reasoning, - ) - - if total_tokens is None and input_tokens is not None and output_tokens is not None: - total_tokens = input_tokens + output_tokens - - if total_tokens is not None: - _set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py deleted file mode 100644 index 7b1ca9324b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/ai/utils.py +++ /dev/null @@ -1,782 +0,0 @@ -import inspect -import json -from copy import deepcopy -from typing import TYPE_CHECKING - -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Tuple, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import logger - -MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB -# Maximum characters when only a single message is left after bytes truncation -MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000 - - -class GEN_AI_ALLOWED_MESSAGE_ROLES: - SYSTEM = "system" - USER = "user" - ASSISTANT = "assistant" - TOOL = "tool" - - -GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = { - GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"], - GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"], - GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"], - GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"], -} - -GEN_AI_MESSAGE_ROLE_MAPPING = {} -for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items(): - for source_role in source_roles: - GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role - - -def parse_data_uri(url: str) -> "Tuple[str, str]": - """ - Parse a data URI and return (mime_type, content). - - Data URI format (RFC 2397): data:[][;base64], - - Examples: - data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...") - data:text/plain,Hello → ("text/plain", "Hello") - data:;base64,SGVsbG8= → ("", "SGVsbG8=") - - Raises: - ValueError: If the URL is not a valid data URI (missing comma separator) - """ - if "," not in url: - raise ValueError("Invalid data URI: missing comma separator") - - header, content = url.split(",", 1) - - # Extract mime type from header - # Format: "data:[;param1][;param2]..." e.g. "data:image/jpeg;base64" - # Remove "data:" prefix, then take everything before the first semicolon - if header.startswith("data:"): - mime_part = header[5:] # Remove "data:" prefix - else: - mime_part = header - - mime_type = mime_part.split(";")[0] - - return mime_type, content - - -def get_modality_from_mime_type(mime_type: str) -> str: - """ - Infer the content modality from a MIME type string. - - Args: - mime_type: A MIME type string (e.g., "image/jpeg", "audio/mp3") - - Returns: - One of: "image", "audio", "video", or "document" - Defaults to "image" for unknown or empty MIME types. - - Examples: - "image/jpeg" -> "image" - "audio/mp3" -> "audio" - "video/mp4" -> "video" - "application/pdf" -> "document" - "text/plain" -> "document" - """ - if not mime_type: - return "image" # Default fallback - - mime_lower = mime_type.lower() - if mime_lower.startswith("image/"): - return "image" - elif mime_lower.startswith("audio/"): - return "audio" - elif mime_lower.startswith("video/"): - return "video" - elif mime_lower.startswith("application/") or mime_lower.startswith("text/"): - return "document" - else: - return "image" # Default fallback for unknown types - - -def transform_openai_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an OpenAI/LiteLLM content part to Sentry's standardized format. - - This handles the OpenAI image_url format used by OpenAI and LiteLLM SDKs. - - Input format: - - {"type": "image_url", "image_url": {"url": "..."}} - - {"type": "image_url", "image_url": "..."} (string shorthand) - - Output format (one of): - - {"type": "blob", "modality": "image", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "image", "mime_type": "", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from OpenAI/LiteLLM - - Returns: - A transformed dictionary in standardized format, or None if the format - is not OpenAI image_url format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type != "image_url": - return None - - image_url_data = content_part.get("image_url") - if isinstance(image_url_data, str): - url = image_url_data - elif isinstance(image_url_data, dict): - url = image_url_data.get("url", "") - else: - return None - - if not url: - return None - - # Check if it's a data URI (base64 encoded) - if url.startswith("data:"): - try: - mime_type, content = parse_data_uri(url) - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": content, - } - except ValueError: - # If parsing fails, return as URI - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - else: - # Regular URL - return { - "type": "uri", - "modality": "image", - "mime_type": "", - "uri": url, - } - - -def transform_anthropic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform an Anthropic content part to Sentry's standardized format. - - This handles the Anthropic image and document formats with source dictionaries. - - Input format: - - {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} - - {"type": "image", "source": {"type": "url", "media_type": "...", "url": "..."}} - - {"type": "image", "source": {"type": "file", "media_type": "...", "file_id": "..."}} - - {"type": "document", "source": {...}} (same source formats) - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from Anthropic - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Anthropic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "document") or "source" not in content_part: - return None - - source = content_part.get("source") - if not isinstance(source, dict): - return None - - source_type = source.get("type") - media_type = source.get("media_type", "") - modality = ( - "document" - if block_type == "document" - else get_modality_from_mime_type(media_type) - ) - - if source_type == "base64": - return { - "type": "blob", - "modality": modality, - "mime_type": media_type, - "content": source.get("data", ""), - } - elif source_type == "url": - return { - "type": "uri", - "modality": modality, - "mime_type": media_type, - "uri": source.get("url", ""), - } - elif source_type == "file": - return { - "type": "file", - "modality": modality, - "mime_type": media_type, - "file_id": source.get("file_id", ""), - } - - return None - - -def transform_google_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a Google GenAI content part to Sentry's standardized format. - - This handles the Google GenAI inline_data and file_data formats. - - Input format: - - {"inline_data": {"mime_type": "...", "data": "..."}} - - {"file_data": {"mime_type": "...", "file_uri": "..."}} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - Args: - content_part: A dictionary representing a content part from Google GenAI - - Returns: - A transformed dictionary in standardized format, or None if the format - is not Google format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Handle Google inline_data format - if "inline_data" in content_part: - inline_data = content_part.get("inline_data") - if isinstance(inline_data, dict): - mime_type = inline_data.get("mime_type", "") - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": inline_data.get("data", ""), - } - return None - - # Handle Google file_data format - if "file_data" in content_part: - file_data = content_part.get("file_data") - if isinstance(file_data, dict): - mime_type = file_data.get("mime_type", "") - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_data.get("file_uri", ""), - } - return None - - return None - - -def transform_generic_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a generic/LangChain-style content part to Sentry's standardized format. - - This handles generic formats where the type indicates the modality and - the data is provided via direct base64, url, or file_id fields. - - Input format: - - {"type": "image", "base64": "...", "mime_type": "..."} - - {"type": "audio", "url": "...", "mime_type": "..."} - - {"type": "video", "base64": "...", "mime_type": "..."} - - {"type": "file", "file_id": "...", "mime_type": "..."} - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part in generic format - - Returns: - A transformed dictionary in standardized format, or None if the format - is not generic format or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - block_type = content_part.get("type") - - if block_type not in ("image", "audio", "video", "file"): - return None - - # Ensure it's not Anthropic format (which also uses type: "image") - if "source" in content_part: - return None - - mime_type = content_part.get("mime_type", "") - modality = block_type if block_type != "file" else "document" - - # Check for base64 encoded content - if "base64" in content_part: - return { - "type": "blob", - "modality": modality, - "mime_type": mime_type, - "content": content_part.get("base64", ""), - } - # Check for URL reference - elif "url" in content_part: - return { - "type": "uri", - "modality": modality, - "mime_type": mime_type, - "uri": content_part.get("url", ""), - } - # Check for file_id reference - elif "file_id" in content_part: - return { - "type": "file", - "modality": modality, - "mime_type": mime_type, - "file_id": content_part.get("file_id", ""), - } - - return None - - -def transform_content_part( - content_part: "Dict[str, Any]", -) -> "Optional[Dict[str, Any]]": - """ - Transform a content part from various AI SDK formats to Sentry's standardized format. - - This is a heuristic dispatcher that detects the format and delegates to the - appropriate SDK-specific transformer. For direct SDK integration, prefer using - the specific transformers directly: - - transform_openai_content_part() for OpenAI/LiteLLM - - transform_anthropic_content_part() for Anthropic - - transform_google_content_part() for Google GenAI - - transform_generic_content_part() for LangChain and other generic formats - - Detection order: - 1. OpenAI: type == "image_url" - 2. Google: "inline_data" or "file_data" keys present - 3. Anthropic: type in ("image", "document") with "source" key - 4. Generic: type in ("image", "audio", "video", "file") with base64/url/file_id - - Output format (one of): - - {"type": "blob", "modality": "...", "mime_type": "...", "content": "..."} - - {"type": "uri", "modality": "...", "mime_type": "...", "uri": "..."} - - {"type": "file", "modality": "...", "mime_type": "...", "file_id": "..."} - - Args: - content_part: A dictionary representing a content part from an AI SDK - - Returns: - A transformed dictionary in standardized format, or None if the format - is unrecognized or transformation fails. - """ - if not isinstance(content_part, dict): - return None - - # Try OpenAI format first (most common, clear indicator) - result = transform_openai_content_part(content_part) - if result is not None: - return result - - # Try Google format (unique keys make it easy to detect) - result = transform_google_content_part(content_part) - if result is not None: - return result - - # Try Anthropic format (has "source" key) - result = transform_anthropic_content_part(content_part) - if result is not None: - return result - - # Try generic format as fallback - result = transform_generic_content_part(content_part) - if result is not None: - return result - - # Unrecognized format - return None - - -def transform_message_content(content: "Any") -> "Any": - """ - Transform message content, handling both string content and list of content blocks. - - For list content, each item is transformed using transform_content_part(). - Items that cannot be transformed (return None) are kept as-is. - - Args: - content: Message content - can be a string, list of content blocks, or other - - Returns: - - String content: returned as-is - - List content: list with each transformable item converted to standardized format - - Other: returned as-is - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - return transformed - - return content - - -def _normalize_data(data: "Any", unpack: bool = True) -> "Any": - # convert pydantic data (e.g. OpenAI v1+) to json compatible format - if hasattr(data, "model_dump"): - # Check if it's a class (type) rather than an instance - # Model classes can be passed as arguments (e.g., for schema definitions) - if inspect.isclass(data): - return f"" - - try: - return _normalize_data(data.model_dump(), unpack=unpack) - except Exception as e: - logger.warning("Could not convert pydantic data to JSON: %s", e) - return data if isinstance(data, (int, float, bool, str)) else str(data) - - if isinstance(data, list): - if unpack and len(data) == 1: - return _normalize_data(data[0], unpack=unpack) # remove empty dimensions - return list(_normalize_data(x, unpack=unpack) for x in data) - - if isinstance(data, dict): - return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} - - return data if isinstance(data, (int, float, bool, str)) else str(data) - - -def set_data_normalized( - span: "Union[Span, StreamedSpan]", - key: str, - value: "Any", - unpack: bool = True, -) -> None: - normalized = _normalize_data(value, unpack=unpack) - if isinstance(normalized, (int, float, bool, str)): - _set_span_data_attribute(span, key, normalized) - else: - _set_span_data_attribute(span, key, json.dumps(normalized)) - - -def _set_span_data_attribute( - span: "Union[Span, StreamedSpan]", key: str, value: "Any" -) -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(key, value) - else: - span.set_data(key, value) - - -def normalize_message_role(role: str) -> str: - """ - Normalize a message role to one of the 4 allowed gen_ai role values. - Maps "ai" -> "assistant" and keeps other standard roles unchanged. - """ - return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role) - - -def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, Any]]": - """ - Normalize roles in a list of messages to use standard gen_ai role values. - Creates a deep copy to avoid modifying the original messages. - """ - normalized_messages = [] - for message in messages: - if not isinstance(message, dict): - normalized_messages.append(message) - continue - normalized_message = message.copy() - if "role" in message: - normalized_message["role"] = normalize_message_role(message["role"]) - normalized_messages.append(normalized_message) - - return normalized_messages - - -def get_start_span_function() -> "Callable[..., Any]": - current_span = sentry_sdk.get_current_span() - - transaction_exists = ( - current_span is not None and current_span.containing_transaction is not None - ) - return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction - - -def _truncate_single_message_content_if_present( - message: "Dict[str, Any]", max_chars: int -) -> "Dict[str, Any]": - """ - Truncate a message's content to at most `max_chars` characters and append an - ellipsis if truncation occurs. - """ - if not isinstance(message, dict) or "content" not in message: - return message - content = message["content"] - - if isinstance(content, str): - if len(content) <= max_chars: - return message - message["content"] = content[:max_chars] + "..." - return message - - if isinstance(content, list): - remaining = max_chars - for item in content: - if isinstance(item, dict) and "text" in item: - text = item["text"] - if isinstance(text, str): - if len(text) > remaining: - item["text"] = text[:remaining] + "..." - remaining = 0 - else: - remaining -= len(text) - return message - - return message - - -def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int: - """ - Find the index of the first message that would exceed the max bytes limit. - Compute the individual message sizes, and return the index of the first message from the back - of the list that would exceed the max bytes limit. - """ - running_sum = 0 - for idx in range(len(messages) - 1, -1, -1): - size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8")) - running_sum += size - if running_sum > max_bytes: - return idx + 1 - - return 0 - - -def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool: - """ - Some content blocks contain an image_url property with base64 content as its value. - This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content. - """ - if item.get("type") != "image_url": - return False - - image_url_val = item.get("image_url") - image_url = ( - image_url_val.get("url", "") - if isinstance(image_url_val, dict) - else (image_url_val or "") - ) - data_url_match = DATA_URL_BASE64_REGEX.match(image_url) - - return bool(data_url_match) - - -def redact_blob_message_parts( - messages: "List[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Redact blob message parts from the messages by replacing blob content with "[Filtered]". - - This function creates a deep copy of messages that contain blob content to avoid - mutating the original message dictionaries. Messages without blob content are - returned as-is to minimize copying overhead. - - e.g: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "data:image/jpeg;base64,..." - } - ] - } - becomes: - { - "role": "user", - "content": [ - { - "text": "How many ponies do you see in the image?", - "type": "text" - }, - { - "type": "blob", - "modality": "image", - "mime_type": "image/jpeg", - "content": "[Filtered]" - } - ] - } - """ - - # First pass: check if any message contains blob content - has_blobs = False - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and ( - item.get("type") == "blob" or _is_image_type_with_blob_content(item) - ): - has_blobs = True - break - if has_blobs: - break - - # If no blobs found, return original messages to avoid unnecessary copying - if not has_blobs: - return messages - - # Deep copy messages to avoid mutating the original - messages_copy = deepcopy(messages) - - # Second pass: redact blob content in the copy - for message in messages_copy: - if not isinstance(message, dict): - continue - - content = message.get("content") - if isinstance(content, list): - for item in content: - if isinstance(item, dict): - if item.get("type") == "blob": - item["content"] = BLOB_DATA_SUBSTITUTE - elif _is_image_type_with_blob_content(item): - if isinstance(item["image_url"], dict): - item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE - else: - item["image_url"] = BLOB_DATA_SUBSTITUTE - - return messages_copy - - -def truncate_messages_by_size( - messages: "List[Dict[str, Any]]", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Tuple[List[Dict[str, Any]], int]": - """ - Returns a truncated messages list, consisting of - - the last message, with its content truncated to `max_single_message_chars` characters, - if the last message's size exceeds `max_bytes` bytes; otherwise, - - the maximum number of messages, starting from the end of the `messages` list, whose total - serialized size does not exceed `max_bytes` bytes. - - In the single message case, the serialized message size may exceed `max_bytes`, because - truncation is based only on character count in that case. - """ - serialized_json = json.dumps(messages, separators=(",", ":")) - current_size = len(serialized_json.encode("utf-8")) - - if current_size <= max_bytes: - return messages, 0 - - truncation_index = _find_truncation_index(messages, max_bytes) - if truncation_index < len(messages): - truncated_messages = messages[truncation_index:] - else: - truncation_index = len(messages) - 1 - truncated_messages = messages[-1:] - - if len(truncated_messages) == 1: - truncated_messages[0] = _truncate_single_message_content_if_present( - deepcopy(truncated_messages[0]), max_chars=max_single_message_chars - ) - - return truncated_messages, truncation_index - - -def truncate_and_annotate_messages( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_message = _truncate_single_message_content_if_present( - deepcopy(messages[-1]), max_chars=max_single_message_chars - ) - if len(messages) > 1: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return [truncated_message] - - -def truncate_and_annotate_embedding_inputs( - messages: "Optional[List[Dict[str, Any]]]", - span: "Any", - scope: "Any", - max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES, -) -> "Optional[List[Dict[str, Any]]]": - if not messages: - return None - - messages = redact_blob_message_parts(messages) - - truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes) - if removed_count > 0: - scope._gen_ai_original_message_count[span.span_id] = len(messages) - - return truncated_messages - - -def set_conversation_id(conversation_id: str) -> None: - """ - Set the conversation_id in the scope. - """ - scope = sentry_sdk.get_current_scope() - scope.set_conversation_id(conversation_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py deleted file mode 100644 index 5556b11ace..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/api.py +++ /dev/null @@ -1,573 +0,0 @@ -import inspect -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import Client, tracing_utils -from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.crons import monitor -from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.traces import get_current_span as _get_current_streamed_span -from sentry_sdk.tracing import NoOpSpan, Transaction, trace - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Optional, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - MeasurementUnit, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span, TransactionKwargs - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) -else: - - def overload(x: "T") -> "T": - return x - - -# When changing this, update __all__ in __init__.py too -__all__ = [ - "init", - "add_attachment", - "add_breadcrumb", - "capture_event", - "capture_exception", - "capture_message", - "configure_scope", - "continue_trace", - "flush", - "flush_async", - "get_baggage", - "get_client", - "get_global_scope", - "get_isolation_scope", - "get_current_scope", - "get_current_span", - "get_traceparent", - "is_initialized", - "isolation_scope", - "last_event_id", - "new_scope", - "push_scope", - "remove_attribute", - "set_attribute", - "set_context", - "set_extra", - "set_level", - "set_measurement", - "set_tag", - "set_tags", - "set_user", - "start_span", - "start_transaction", - "trace", - "monitor", - "start_session", - "end_session", - "set_transaction_name", - "update_current_span", -] - - -def scopemethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Scope.%s`" % f.__name__, - inspect.getdoc(getattr(Scope, f.__name__)), - ) - return f - - -def clientmethod(f: "F") -> "F": - f.__doc__ = "%s\n\n%s" % ( - "Alias for :py:meth:`sentry_sdk.Client.%s`" % f.__name__, - inspect.getdoc(getattr(Client, f.__name__)), - ) - return f - - -@scopemethod -def get_client() -> "BaseClient": - return Scope.get_client() - - -def is_initialized() -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether Sentry has been initialized or not. - - If a client is available and the client is active - (meaning it is configured to send data) then - Sentry is initialized. - """ - return get_client().is_active() - - -@scopemethod -def get_global_scope() -> "Scope": - return Scope.get_global_scope() - - -@scopemethod -def get_isolation_scope() -> "Scope": - return Scope.get_isolation_scope() - - -@scopemethod -def get_current_scope() -> "Scope": - return Scope.get_current_scope() - - -@scopemethod -def last_event_id() -> "Optional[str]": - """ - See :py:meth:`sentry_sdk.Scope.last_event_id` documentation regarding - this method's limitations. - """ - return Scope.last_event_id() - - -@scopemethod -def capture_event( - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_event(event, hint, scope=scope, **scope_kwargs) - - -@scopemethod -def capture_message( - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_message( - message, level, scope=scope, **scope_kwargs - ) - - -@scopemethod -def capture_exception( - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Any]" = None, - **scope_kwargs: "Any", -) -> "Optional[str]": - return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) - - -@scopemethod -def add_attachment( - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, -) -> None: - return get_isolation_scope().add_attachment( - bytes, filename, path, content_type, add_to_transactions - ) - - -@scopemethod -def add_breadcrumb( - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", -) -> None: - return get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - -@overload -def configure_scope() -> "ContextManager[Scope]": - pass - - -@overload -def configure_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def configure_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - warnings.warn( - "sentry_sdk.configure_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-configuring", - DeprecationWarning, - stacklevel=2, - ) - - scope = get_isolation_scope() - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - -@overload -def push_scope() -> "ContextManager[Scope]": - pass - - -@overload -def push_scope( # noqa: F811 - callback: "Callable[[Scope], None]", -) -> None: - pass - - -def push_scope( # noqa: F811 - callback: "Optional[Callable[[Scope], None]]" = None, -) -> "Optional[ContextManager[Scope]]": - """ - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - warnings.warn( - "sentry_sdk.push_scope is deprecated and will be removed in the next major version. " - "Please consult our migration guide to learn how to migrate to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x#scope-pushing", - DeprecationWarning, - stacklevel=2, - ) - - if callback is not None: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with push_scope() as scope: - callback(scope) - return None - - return _ScopeManager() - - -@scopemethod -def set_attribute(attribute: str, value: "Any") -> None: - """ - Set an attribute. - - Any attributes-based telemetry (logs, metrics) captured in this scope will - include this attribute. - """ - return get_isolation_scope().set_attribute(attribute, value) - - -@scopemethod -def remove_attribute(attribute: str) -> None: - """ - Remove an attribute. - - If the attribute doesn't exist, this function will not have any effect and - it will also not raise an exception. - """ - return get_isolation_scope().remove_attribute(attribute) - - -@scopemethod -def set_tag(key: str, value: "Any") -> None: - return get_isolation_scope().set_tag(key, value) - - -@scopemethod -def set_tags(tags: "Mapping[str, object]") -> None: - return get_isolation_scope().set_tags(tags) - - -@scopemethod -def set_context(key: str, value: "Dict[str, Any]") -> None: - return get_isolation_scope().set_context(key, value) - - -@scopemethod -def set_extra(key: str, value: "Any") -> None: - return get_isolation_scope().set_extra(key, value) - - -@scopemethod -def set_user(value: "Optional[Dict[str, Any]]") -> None: - return get_isolation_scope().set_user(value) - - -@scopemethod -def set_level(value: "LogLevelStr") -> None: - return get_isolation_scope().set_level(value) - - -@clientmethod -def flush( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return get_client().flush(timeout=timeout, callback=callback) - - -@clientmethod -async def flush_async( - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, -) -> None: - return await get_client().flush_async(timeout=timeout, callback=callback) - - -@scopemethod -def start_span( - **kwargs: "Any", -) -> "Span": - return get_current_scope().start_span(**kwargs) - - -@scopemethod -def start_transaction( - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", -) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction on the current scope. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - return get_current_scope().start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - -def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - transaction = get_current_scope().transaction - if transaction is not None: - transaction.set_measurement(name, value, unit) - - -def get_current_span( - scope: "Optional[Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - return tracing_utils.get_current_span(scope) - - -def get_traceparent() -> "Optional[str]": - """ - Returns the traceparent either from the active span or from the scope. - """ - return get_current_scope().get_traceparent() - - -def get_baggage() -> "Optional[str]": - """ - Returns Baggage either from the active span or from the scope. - """ - baggage = get_current_scope().get_baggage() - if baggage is not None: - return baggage.serialize() - - return None - - -def continue_trace( - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", -) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers, op, name, source, origin - ) - - -@scopemethod -def start_session( - session_mode: str = "application", -) -> None: - return get_isolation_scope().start_session(session_mode=session_mode) - - -@scopemethod -def end_session() -> None: - return get_isolation_scope().end_session() - - -@scopemethod -def set_transaction_name(name: str, source: "Optional[str]" = None) -> None: - return get_current_scope().set_transaction_name(name, source) - - -def update_current_span( - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Union[str, int, float, bool]]]" = None, - data: "Optional[dict[str, Any]]" = None, -) -> None: - """ - Update the current active span with the provided parameters. - - This function allows you to modify properties of the currently active span. - If no span is currently active, this function will do nothing. - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, the span's operation will - remain unchanged. - :type op: str or None - - :param name: The human-readable name/description for the span. This provides - more specific details about what the span represents (e.g., "GET /api/users", - "SELECT * FROM users"). If not provided, the span's name will remain unchanged. - :type name: str or None - - :param data: A dictionary of key-value pairs to add as data to the span. This - data will be merged with any existing span data. If not provided, - no data will be added. - - .. deprecated:: 2.35.0 - Use ``attributes`` instead. The ``data`` parameter will be removed - in a future version. - :type data: dict[str, Union[str, int, float, bool]] or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes will be merged with any existing span data. If not provided, - no attributes will be added. - :type attributes: dict[str, Union[str, int, float, bool]] or None - - :returns: None - - .. versionadded:: 2.35.0 - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP - - sentry_sdk.update_current_span( - op=OP.FUNCTION, - name="process_user_data", - attributes={"user_id": 123, "batch_size": 50} - ) - """ - if isinstance(_get_current_streamed_span(), StreamedSpan): - warnings.warn( - "The `update_current_span` API isn't available in streaming mode. " - "Retrieve the current span with get_current_span() and use its API " - "directly.", - DeprecationWarning, - stacklevel=2, - ) - return - - current_span = get_current_span() - - if current_span is None: - return - - if op is not None: - current_span.op = op - - if name is not None: - # internally it is still description - current_span.description = name - - if data is not None and attributes is not None: - raise ValueError( - "Cannot provide both `data` and `attributes`. Please use only `attributes`." - ) - - if data is not None: - warnings.warn( - "The `data` parameter is deprecated. Please use `attributes` instead.", - DeprecationWarning, - stacklevel=2, - ) - attributes = data - - if attributes is not None: - current_span.update_data(attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py deleted file mode 100644 index 4d69d3acf2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/attachments.py +++ /dev/null @@ -1,71 +0,0 @@ -import mimetypes -import os -from typing import TYPE_CHECKING - -from sentry_sdk.envelope import Item, PayloadRef - -if TYPE_CHECKING: - from typing import Callable, Optional, Union - - -class Attachment: - """Additional files/data to send along with an event. - - This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. - config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with - all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are - captured within the ``Scope``. - - To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for - ``add_attachment`` are the same as the parameters for this class's constructor. - - :param bytes: Raw bytes of the attachment, or a function that returns the raw bytes. Must be provided unless - ``path`` is provided. - :param filename: The filename of the attachment. Must be provided unless ``path`` is provided. - :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. - :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` - parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. - :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. - """ - - def __init__( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - if bytes is None and path is None: - raise TypeError("path or raw bytes required for attachment") - if filename is None and path is not None: - filename = os.path.basename(path) - if filename is None: - raise TypeError("filename is required for attachment") - if content_type is None: - content_type = mimetypes.guess_type(filename)[0] - self.bytes = bytes - self.filename = filename - self.path = path - self.content_type = content_type - self.add_to_transactions = add_to_transactions - - def to_envelope_item(self) -> "Item": - """Returns an envelope item for this attachment.""" - payload: "Union[None, PayloadRef, bytes]" = None - if self.bytes is not None: - if callable(self.bytes): - payload = self.bytes() - else: - payload = self.bytes - else: - payload = PayloadRef(path=self.path) - return Item( - payload=payload, - type="attachment", - content_type=self.content_type, - filename=self.filename, - ) - - def __repr__(self) -> str: - return "" % (self.filename,) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py deleted file mode 100644 index 92cb42277e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/client.py +++ /dev/null @@ -1,1479 +0,0 @@ -import json -import os -import platform -import random -import socket -import sys -import uuid -import warnings -from collections.abc import Iterable, Mapping -from datetime import datetime, timezone -from importlib import import_module -from typing import TYPE_CHECKING, Dict, List, cast, overload - -from sentry_sdk._compat import check_uwsgi_thread_support -from sentry_sdk._metrics_batcher import MetricsBatcher -from sentry_sdk._span_batcher import SpanBatcher -from sentry_sdk.consts import ( - DEFAULT_MAX_VALUE_LENGTH, - DEFAULT_OPTIONS, - INSTRUMENTER, - SPANDATA, - SPANSTATUS, - VERSION, - ClientConstructor, -) -from sentry_sdk.data_collection import ( - _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 -from sentry_sdk.monitor import Monitor -from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler -from sentry_sdk.profiler.transaction_profiler import ( - Profile, - has_profiling_enabled, - setup_profiler, -) -from sentry_sdk.scrubber import EventScrubber -from sentry_sdk.serializer import serialize -from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import trace -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import ( - AsyncHttpTransport, - HttpTransportCore, - make_transport, -) -from sentry_sdk.utils import ( - AnnotatedValue, - ContextVar, - capture_internal_exceptions, - current_stacktrace, - datetime_from_isoformat, - env_to_bool, - format_timestamp, - get_before_send_log, - get_before_send_metric, - get_before_send_span, - get_default_release, - get_sdk_name, - get_type_name, - handle_in_app, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union - - from sentry_sdk._log_batcher import LogBatcher - from sentry_sdk._metrics_batcher import MetricsBatcher - from sentry_sdk._types import ( - Event, - EventDataCategory, - Hint, - Log, - Metric, - SDKInfo, - SerializedAttributeValue, - ) - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.session import Session - from sentry_sdk.spotlight import SpotlightClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Item, Transport - from sentry_sdk.utils import Dsn - - I = TypeVar("I", bound=Integration) # noqa: E741 - -_client_init_debug = ContextVar("client_init_debug") - - -SDK_INFO: "SDKInfo" = { - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() - "version": VERSION, - "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], -} - - -def _serialized_v1_attribute_to_serialized_v2_attribute( - attribute_value: "Any", -) -> "Optional[SerializedAttributeValue]": - if isinstance(attribute_value, bool): - return { - "value": attribute_value, - "type": "boolean", - } - - if isinstance(attribute_value, int): - return { - "value": attribute_value, - "type": "integer", - } - - if isinstance(attribute_value, float): - return { - "value": attribute_value, - "type": "double", - } - - if isinstance(attribute_value, str): - return { - "value": attribute_value, - "type": "string", - } - - if isinstance(attribute_value, list): - if not attribute_value: - return {"value": [], "type": "array"} - - ty = type(attribute_value[0]) - if ty in (int, str, bool, float) and all( - type(v) is ty for v in attribute_value - ): - return { - "value": attribute_value, - "type": "array", - } - - # Types returned when the serializer for V1 span attributes recurses into some container types. - if isinstance(attribute_value, (dict, list)): - return { - "value": json.dumps(attribute_value), - "type": "string", - } - - return None - - -def _serialized_v1_span_to_serialized_v2_span( - span: "dict[str, Any]", event: "Event" -) -> "dict[str, Any]": - # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". - res: "dict[str, Any]" = { - "status": SpanStatus.OK.value, - "is_segment": False, - } - - if "trace_id" in span: - res["trace_id"] = span["trace_id"] - - if "span_id" in span: - res["span_id"] = span["span_id"] - - if "description" in span: - description = span["description"] - - if description is None and "op" in span: - description = span["op"] - - res["name"] = description - - if "start_timestamp" in span: - start_timestamp = None - try: - start_timestamp = datetime_from_isoformat(span["start_timestamp"]) - except Exception: - pass - - if start_timestamp is not None: - res["start_timestamp"] = start_timestamp.timestamp() - - if "timestamp" in span: - end_timestamp = None - try: - end_timestamp = datetime_from_isoformat(span["timestamp"]) - except Exception: - pass - - if end_timestamp is not None: - res["end_timestamp"] = end_timestamp.timestamp() - - if "parent_span_id" in span: - res["parent_span_id"] = span["parent_span_id"] - - if "status" in span and span["status"] != SPANSTATUS.OK: - res["status"] = "error" - - attributes: "Dict[str, Any]" = {} - - if "op" in span: - attributes["sentry.op"] = span["op"] - if "origin" in span: - attributes["sentry.origin"] = span["origin"] - - span_data = span.get("data") - if isinstance(span_data, dict): - attributes.update(span_data) - - span_tags = span.get("tags") - if isinstance(span_tags, dict): - attributes.update(span_tags) - - # See Scope._apply_user_attributes_to_telemetry() for user attributes. - user = event.get("user") - if isinstance(user, dict): - if "id" in user: - attributes["user.id"] = user["id"] - if "username" in user: - attributes["user.name"] = user["username"] - if "email" in user: - attributes["user.email"] = user["email"] - - # See Scope.set_global_attributes() for release, environment, and SDK metadata. - if "release" in event: - attributes["sentry.release"] = event["release"] - if "environment" in event: - attributes["sentry.environment"] = event["environment"] - if "server_name" in event: - attributes["server.address"] = event["server_name"] - if "transaction" in event: - attributes["sentry.segment.name"] = event["transaction"] - - trace_context = event.get("contexts", {}).get("trace", {}) - if "span_id" in trace_context: - attributes["sentry.segment.id"] = trace_context["span_id"] - - sdk_info = event.get("sdk") - if isinstance(sdk_info, dict): - if "name" in sdk_info: - attributes["sentry.sdk.name"] = sdk_info["name"] - if "version" in sdk_info: - attributes["sentry.sdk.version"] = sdk_info["version"] - - attributes["process.runtime.name"] = platform.python_implementation() - attributes["process.runtime.version"] = ( - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ) - - if not attributes: - return res - - res["attributes"] = {} - for key, value in attributes.items(): - converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) - if converted_value is None: - continue - - res["attributes"][key] = converted_value - - # Remove redundant attribute, as status is stored in the status field. - if "status" in res["attributes"]: - del res["attributes"]["status"] - - return res - - -def _split_gen_ai_spans( - event_opt: "Event", -) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": - if "spans" not in event_opt: - return None - - spans: "Any" = event_opt["spans"] - if isinstance(spans, AnnotatedValue): - spans = spans.value - - if not isinstance(spans, Iterable): - return None - - non_gen_ai_spans = [] - gen_ai_spans = [] - for span in spans: - if not isinstance(span, dict): - non_gen_ai_spans.append(span) - continue - - span_op = span.get("op") - if isinstance(span_op, str) and span_op.startswith("gen_ai."): - gen_ai_spans.append(span) - else: - non_gen_ai_spans.append(span) - - return non_gen_ai_spans, gen_ai_spans - - -def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": - if args and (isinstance(args[0], (bytes, str)) or args[0] is None): - dsn: "Optional[str]" = args[0] - args = args[1:] - else: - dsn = None - - if len(args) > 1: - raise TypeError("Only single positional argument is expected") - - rv = dict(DEFAULT_OPTIONS) - options = dict(*args, **kwargs) - if dsn is not None and options.get("dsn") is None: - options["dsn"] = dsn - - for key, value in options.items(): - if key not in rv: - raise TypeError("Unknown option %r" % (key,)) - - rv[key] = value - - if rv["dsn"] is None: - rv["dsn"] = os.environ.get("SENTRY_DSN") - - if rv["release"] is None: - rv["release"] = get_default_release() - - if rv["environment"] is None: - rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" - - if rv["debug"] is None: - rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False - - if rv["server_name"] is None and hasattr(socket, "gethostname"): - rv["server_name"] = socket.gethostname() - - if rv["instrumenter"] is None: - rv["instrumenter"] = INSTRUMENTER.SENTRY - - if rv["project_root"] is None: - try: - project_root = os.getcwd() - except Exception: - project_root = None - - rv["project_root"] = project_root - - 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"] - ) - - if rv["socket_options"] and not isinstance(rv["socket_options"], list): - logger.warning( - "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format." - ) - rv["socket_options"] = None - - if rv["keep_alive"] is None: - rv["keep_alive"] = ( - env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False - ) - - if rv["enable_tracing"] is not None: - warnings.warn( - "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", - DeprecationWarning, - stacklevel=2, - ) - - if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): - warnings.warn( - "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", - stacklevel=2, - ) - - return rv - - -try: - # Python 3.6+ - module_not_found_error = ModuleNotFoundError -except Exception: - # Older Python versions - module_not_found_error = ImportError # type: ignore - - -class BaseClient: - """ - .. versionadded:: 2.0.0 - - The basic definition of a client that is used for sending data to Sentry. - """ - - spotlight: "Optional[SpotlightClient]" = None - - def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: - self.options: "Dict[str, Any]" = ( - options if options is not None else DEFAULT_OPTIONS - ) - - self.transport: "Optional[Transport]" = None - self.monitor: "Optional[Monitor]" = None - self.log_batcher: "Optional[LogBatcher]" = None - self.metrics_batcher: "Optional[MetricsBatcher]" = None - self.span_batcher: "Optional[SpanBatcher]" = None - self.integrations: "dict[str, Integration]" = {} - - def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": - return {"options": {}} - - def __setstate__(self, *args: "Any", **kwargs: "Any") -> None: - pass - - @property - def dsn(self) -> "Optional[str]": - return None - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - return None - - def should_send_default_pii(self) -> bool: - return False - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return False - - def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - return None - - def _capture_log(self, log: "Log", scope: "Scope") -> None: - pass - - def _capture_metric(self, metric: "Metric", scope: "Scope") -> None: - pass - - def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None: - pass - - def capture_session(self, *args: "Any", **kwargs: "Any") -> None: - return None - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, name_or_class: "Union[str, type[Integration]]" - ) -> "Optional[Integration]": - return None - - def close(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def flush(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def close_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - async def flush_async(self, *args: "Any", **kwargs: "Any") -> None: - return None - - def __enter__(self) -> "BaseClient": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - return None - - -class NonRecordingClient(BaseClient): - """ - .. versionadded:: 2.0.0 - - A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized. - """ - - pass - - -class _Client(BaseClient): - """ - The client is internally responsible for capturing the events and - forwarding them to sentry through the configured transport. It takes - the client options as keyword arguments and optionally the DSN as first - argument. - - Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support) - """ - - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - super(_Client, self).__init__(options=get_options(*args, **kwargs)) - self._init_impl() - - def __getstate__(self) -> "Any": - return {"options": self.options} - - def __setstate__(self, state: "Any") -> None: - self.options = state["options"] - self._init_impl() - - def _setup_instrumentation( - self, functions_to_trace: "Sequence[Dict[str, str]]" - ) -> None: - """ - Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator. - """ - for function in functions_to_trace: - class_name = None - function_qualname = function["qualified_name"] - - if "." not in function_qualname: - logger.warning( - "Can not enable tracing for '%s'. Please provide the fully qualified name including the module (e.g. 'mymodule.my_function').", - function_qualname, - ) - continue - - module_name, function_name = function_qualname.rsplit(".", 1) - - try: - # Try to import module and function - # ex: "mymodule.submodule.funcname" - - module_obj = import_module(module_name) - function_obj = getattr(module_obj, function_name) - setattr(module_obj, function_name, trace(function_obj)) - logger.debug("Enabled tracing for %s", function_qualname) - except module_not_found_error: - try: - # Try to import a class - # ex: "mymodule.submodule.MyClassName.member_function" - - module_name, class_name = module_name.rsplit(".", 1) - module_obj = import_module(module_name) - class_obj = getattr(module_obj, class_name) - function_obj = getattr(class_obj, function_name) - function_type = type(class_obj.__dict__[function_name]) - traced_function = trace(function_obj) - - if function_type in (staticmethod, classmethod): - traced_function = staticmethod(traced_function) - - setattr(class_obj, function_name, traced_function) - setattr(module_obj, class_name, class_obj) - logger.debug("Enabled tracing for %s", function_qualname) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - except Exception as e: - logger.warning( - "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.", - function_qualname, - e, - ) - - def _init_impl(self) -> None: - old_debug = _client_init_debug.get(False) - - def _capture_envelope(envelope: "Envelope") -> None: - if self.spotlight is not None: - self.spotlight.capture_envelope(envelope) - if self.transport is not None: - self.transport.capture_envelope(envelope) - - def _record_lost_event( - reason: str, - data_category: "EventDataCategory", - item: "Optional[Item]" = None, - quantity: int = 1, - ) -> None: - if self.transport is not None: - self.transport.record_lost_event( - reason=reason, - data_category=data_category, - item=item, - quantity=quantity, - ) - - try: - _client_init_debug.set(self.options["debug"]) - self.transport = make_transport(self.options) - - self.monitor = None - if self.transport: - if self.options["enable_backpressure_handling"]: - self.monitor = Monitor(self.transport) - - # Setup Spotlight before creating batchers so _capture_envelope can use it. - # setup_spotlight handles all config/env var resolution per the SDK spec. - from sentry_sdk.spotlight import setup_spotlight - - self.spotlight = setup_spotlight(self.options) - if self.spotlight is not None and not self.options["dsn"]: - sample_all = lambda *_args, **_kwargs: 1.0 - self.options["send_default_pii"] = True - 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, - ) - - self.session_flusher = SessionFlusher(capture_func=_capture_envelope) - - self.log_batcher = None - - if has_logs_enabled(self.options): - from sentry_sdk._log_batcher import LogBatcher - - self.log_batcher = LogBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.metrics_batcher = None - if has_metrics_enabled(self.options): - self.metrics_batcher = MetricsBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - self.span_batcher = None - if has_span_streaming_enabled(self.options): - self.span_batcher = SpanBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) - - max_request_body_size = ("always", "never", "small", "medium") - if self.options["max_request_body_size"] not in max_request_body_size: - raise ValueError( - "Invalid value for max_request_body_size. Must be one of {}".format( - max_request_body_size - ) - ) - - if self.options["_experiments"].get("otel_powered_performance", False): - logger.debug( - "[OTel] Enabling experimental OTel-powered performance monitoring." - ) - self.options["instrumenter"] = INSTRUMENTER.OTEL - if ( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - not in _DEFAULT_INTEGRATIONS - ): - _DEFAULT_INTEGRATIONS.append( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", - ) - - self.integrations = setup_integrations( - self.options["integrations"], - with_defaults=self.options["default_integrations"], - with_auto_enabling_integrations=self.options[ - "auto_enabling_integrations" - ], - disabled_integrations=self.options["disabled_integrations"], - options=self.options, - ) - - sdk_name = get_sdk_name(list(self.integrations.keys())) - SDK_INFO["name"] = sdk_name - logger.debug("Setting SDK name to '%s'", sdk_name) - - if has_profiling_enabled(self.options): - try: - setup_profiler(self.options) - except Exception as e: - logger.debug("Can not set up profiler. (%s)", e) - else: - try: - setup_continuous_profiler( - self.options, - sdk_info=SDK_INFO, - capture_func=_capture_envelope, - ) - except Exception as e: - logger.debug("Can not set up continuous profiler. (%s)", e) - - finally: - _client_init_debug.set(old_debug) - - self._setup_instrumentation(self.options.get("functions_to_trace", [])) - - if ( - self.monitor - or self.log_batcher - or self.metrics_batcher - or self.span_batcher - or has_profiling_enabled(self.options) - or isinstance(self.transport, HttpTransportCore) - ): - # If we have anything on that could spawn a background thread, we - # need to check if it's safe to use them. - check_uwsgi_thread_support() - - def is_active(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client is active (able to send data to Sentry) - """ - return True - - def should_send_default_pii(self) -> bool: - """ - .. versionadded:: 2.0.0 - - Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry. - """ - return self.options.get("send_default_pii") or False - - @property - def dsn(self) -> "Optional[str]": - """Returns the configured DSN as string.""" - return self.options["dsn"] - - @property - def parsed_dsn(self) -> "Optional[Dsn]": - """Returns the configured parsed DSN object.""" - return self.transport.parsed_dsn if self.transport else None - - def _prepare_event( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]", - ) -> "Optional[Event]": - previous_total_spans: "Optional[int]" = None - previous_total_breadcrumbs: "Optional[int]" = None - - if event.get("timestamp") is None: - event["timestamp"] = datetime.now(timezone.utc) - - is_transaction = event.get("type") == "transaction" - - if scope is not None: - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - event_ = scope.apply_to_event(event, hint, self.options) - - # one of the event/error processors returned None - if event_ is None: - if self.transport: - self.transport.record_lost_event( - "event_processor", - data_category=("transaction" if is_transaction else "error"), - ) - if is_transaction: - self.transport.record_lost_event( - "event_processor", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - return None - - event = event_ - spans_delta = spans_before - len( - cast(List[Dict[str, object]], event.get("spans", [])) - ) - span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - - if is_transaction and self.transport is not None: - if spans_delta > 0: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) - if span_recorder_dropped_spans > 0: - self.transport.record_lost_event( - "buffer_overflow", - data_category="span", - quantity=span_recorder_dropped_spans, - ) - - dropped_spans: int = span_recorder_dropped_spans + spans_delta - if dropped_spans > 0: - previous_total_spans = spans_before + dropped_spans - if scope._n_breadcrumbs_truncated > 0: - breadcrumbs = event.get("breadcrumbs", {}) - values = ( - breadcrumbs.get("values", []) - if not isinstance(breadcrumbs, AnnotatedValue) - else [] - ) - previous_total_breadcrumbs = ( - len(values) + scope._n_breadcrumbs_truncated - ) - - if ( - not is_transaction - and self.options["attach_stacktrace"] - and "exception" not in event - and "stacktrace" not in event - and "threads" not in event - ): - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=self.options.get( - "include_local_variables", True - ), - max_value_length=self.options.get( - "max_value_length", DEFAULT_MAX_VALUE_LENGTH - ), - ), - "crashed": False, - "current": True, - } - ] - } - - for key in "release", "environment", "server_name", "dist": - if event.get(key) is None and self.options[key] is not None: - event[key] = str(self.options[key]).strip() - if event.get("sdk") is None: - sdk_info = dict(SDK_INFO) - sdk_info["integrations"] = sorted(self.integrations.keys()) - event["sdk"] = sdk_info - - if event.get("platform") is None: - event["platform"] = "python" - - event = handle_in_app( - event, - self.options["in_app_exclude"], - self.options["in_app_include"], - self.options["project_root"], - ) - - if event is not None: - event_scrubber = self.options["event_scrubber"] - if event_scrubber: - event_scrubber.scrub_event(event) - - if scope is not None and scope._gen_ai_original_message_count: - spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", []) - if isinstance(spans, list): - for span in spans: - span_id = span.get("span_id", None) - span_data = span.get("data", {}) - if ( - span_id - and span_id in scope._gen_ai_original_message_count - and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data - ): - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue( - span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], - {"len": scope._gen_ai_original_message_count[span_id]}, - ) - if previous_total_spans is not None: - event["spans"] = AnnotatedValue( - event.get("spans", []), {"len": previous_total_spans} - ) - if previous_total_breadcrumbs is not None: - event["breadcrumbs"] = AnnotatedValue( - event.get("breadcrumbs", {"values": []}), - {"len": previous_total_breadcrumbs}, - ) - - # Postprocess the event here so that annotated types do - # generally not surface in before_send - if event is not None: - event = cast( - "Event", - serialize( - cast("Dict[str, Any]", event), - max_request_body_size=self.options.get("max_request_body_size"), - max_value_length=self.options.get("max_value_length"), - custom_repr=self.options.get("custom_repr"), - ), - ) - - before_send = self.options["before_send"] - if ( - before_send is not None - and event is not None - and event.get("type") != "transaction" - ): - new_event = None - with capture_internal_exceptions(): - new_event = before_send(event, hint or {}) - if new_event is None: - logger.info("before send dropped event") - if self.transport: - self.transport.record_lost_event( - "before_send", data_category="error" - ) - - # If this is an exception, reset the DedupeIntegration. It still - # remembers the dropped exception as the last exception, meaning - # that if the same exception happens again and is not dropped - # in before_send, it'd get dropped by DedupeIntegration. - if event.get("exception"): - DedupeIntegration.reset_last_seen() - - event = new_event - - before_send_transaction = self.options["before_send_transaction"] - if ( - before_send_transaction is not None - and event is not None - and event.get("type") == "transaction" - ): - new_event = None - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - with capture_internal_exceptions(): - new_event = before_send_transaction(event, hint or {}) - if new_event is None: - logger.info("before send transaction dropped event") - if self.transport: - self.transport.record_lost_event( - reason="before_send", data_category="transaction" - ) - self.transport.record_lost_event( - reason="before_send", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - else: - spans_delta = spans_before - len(new_event.get("spans", [])) - if spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - reason="before_send", data_category="span", quantity=spans_delta - ) - - event = new_event - - return event - - def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: - exc_info = hint.get("exc_info") - if exc_info is None: - return False - - error = exc_info[0] - error_type_name = get_type_name(exc_info[0]) - error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) - - for ignored_error in self.options["ignore_errors"]: - # String types are matched against the type name in the - # exception only - if isinstance(ignored_error, str): - if ignored_error == error_full_name or ignored_error == error_type_name: - return True - else: - if issubclass(error, ignored_error): - return True - - return False - - def _should_capture( - self, - event: "Event", - hint: "Hint", - scope: "Optional[Scope]" = None, - ) -> bool: - # Transactions are sampled independent of error events. - is_transaction = event.get("type") == "transaction" - if is_transaction: - return True - - ignoring_prevents_recursion = scope is not None and not scope._should_capture - if ignoring_prevents_recursion: - return False - - ignored_by_config_option = self._is_ignored_error(event, hint) - if ignored_by_config_option: - return False - - return True - - def _should_sample_error( - self, - event: "Event", - hint: "Hint", - ) -> bool: - error_sampler = self.options.get("error_sampler", None) - - if callable(error_sampler): - with capture_internal_exceptions(): - sample_rate = error_sampler(event, hint) - else: - sample_rate = self.options["sample_rate"] - - try: - not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate - except NameError: - logger.warning( - "The provided error_sampler raised an error. Defaulting to sampling the event." - ) - - # If the error_sampler raised an error, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - except TypeError: - parameter, verb = ( - ("error_sampler", "returned") - if callable(error_sampler) - else ("sample_rate", "contains") - ) - logger.warning( - "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event." - % (parameter, verb, repr(sample_rate)) - ) - - # If the sample_rate has an invalid value, we should sample the event, since the default behavior - # (when no sample_rate or error_sampler is provided) is to sample all events. - not_in_sample_rate = False - - if not_in_sample_rate: - # because we will not sample this event, record a "lost event". - if self.transport: - self.transport.record_lost_event("sample_rate", data_category="error") - - return False - - return True - - def _update_session_from_event( - self, - session: "Session", - event: "Event", - ) -> None: - crashed = False - errored = False - user_agent = None - - exceptions = (event.get("exception") or {}).get("values") - if exceptions: - errored = True - for error in exceptions: - if isinstance(error, AnnotatedValue): - error = error.value or {} - mechanism = error.get("mechanism") - if isinstance(mechanism, Mapping) and mechanism.get("handled") is False: - crashed = True - break - - user = event.get("user") - - if session.user_agent is None: - headers = (event.get("request") or {}).get("headers") - headers_dict = headers if isinstance(headers, dict) else {} - for k, v in headers_dict.items(): - if k.lower() == "user-agent": - user_agent = v - break - - session.update( - status="crashed" if crashed else None, - user=user, - user_agent=user_agent, - errors=session.errors + (errored or crashed), - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - ) -> "Optional[str]": - """Captures an event. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - - :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. - """ - hint = dict(hint or ()) - - if not self._should_capture(event, hint, scope): - return None - - profile = event.pop("profile", None) - - event_id = event.get("event_id") - if event_id is None: - event["event_id"] = event_id = uuid.uuid4().hex - - span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) - event_opt = self._prepare_event(event, hint, scope) - if event_opt is None: - return None - - # whenever we capture an event we also check if the session needs - # to be updated based on that information. - session = scope._session if scope else None - if session: - self._update_session_from_event(session, event) - - is_transaction = event_opt.get("type") == "transaction" - is_checkin = event_opt.get("type") == "check_in" - - if ( - not is_transaction - and not is_checkin - and not self._should_sample_error(event, hint) - ): - return None - - attachments = hint.get("attachments") - - trace_context = event_opt.get("contexts", {}).get("trace") or {} - dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {}) - - headers: "dict[str, object]" = { - "event_id": event_opt["event_id"], - "sent_at": format_timestamp(datetime.now(timezone.utc)), - } - - if dynamic_sampling_context: - headers["trace"] = dynamic_sampling_context - - envelope = Envelope(headers=headers) - - if is_transaction and isinstance(profile, Profile): - envelope.add_profile(profile.to_json(event_opt, self.options)) - - if is_transaction and not span_recorder_has_gen_ai_span: - envelope.add_transaction(event_opt) - elif is_transaction: - split_spans = _split_gen_ai_spans(event_opt) - if split_spans is None or not split_spans[1]: - envelope.add_transaction(event_opt) - else: - non_gen_ai_spans, gen_ai_spans = split_spans - - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) - - converted_gen_ai_spans = [ - _serialized_v1_span_to_serialized_v2_span(span, event_opt) - for span in gen_ai_spans - if isinstance(span, dict) - ] - - envelope.add_item( - Item( - type=SpanBatcher.TYPE, - content_type=SpanBatcher.CONTENT_TYPE, - headers={ - "item_count": len(converted_gen_ai_spans), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": converted_gen_ai_spans, - }, - ), - ) - ) - - elif is_checkin: - envelope.add_checkin(event_opt) - else: - envelope.add_event(event_opt) - - for attachment in attachments or (): - envelope.add_item(attachment.to_envelope_item()) - - return_value = None - if self.spotlight: - self.spotlight.capture_envelope(envelope) - return_value = event_id - - if self.transport is not None: - self.transport.capture_envelope(envelope) - return_value = event_id - - return return_value - - def _capture_telemetry( - self, - telemetry: "Optional[Union[Log, Metric, StreamedSpan]]", - ty: str, - scope: "Scope", - ) -> None: - """ - Capture attributes-based telemetry (logs, metrics, streamed spans). - - Apply any attributes set on the scope to it, and run the user's - before_send_{telemetry} on it, if applicable. - """ - if telemetry is None: - return - - scope.apply_to_telemetry(telemetry) - - before_send = None - - if ty == "log": - before_send = get_before_send_log(self.options) - serialized = telemetry - - elif ty == "metric": - before_send = get_before_send_metric(self.options) - serialized = telemetry - - elif ty == "span": - before_send = get_before_send_span(self.options) - serialized = telemetry._to_json() # type: ignore[union-attr] - - if before_send is not None: - serialized = before_send(serialized, {}) # type: ignore[arg-type] - - if ty in ("log", "metric"): - # Logs and metrics can be dropped in their respective - # before_send, so if we get None, don't queue them for sending. - if serialized is None: - return - - elif ty == "span" and isinstance(telemetry, StreamedSpan): - # Spans can't be dropped in before_send_span by design. They can - # be altered though (e.g. to sanitize). Only allow changes to - # name and attributes. - if isinstance(serialized, dict) and "name" in serialized: - telemetry.name = serialized["name"] # type: ignore[typeddict-item] - telemetry._attributes = {} - for k, v in (serialized.get("attributes") or {}).items(): - telemetry.set_attribute(k, v) - - else: - logger.debug( - "[Tracing] Invalid return value from before_send_span. Keeping original span." - ) - - serialized = telemetry._to_json() - - batcher = None - if ty == "log": - batcher = self.log_batcher - - elif ty == "metric": - batcher = self.metrics_batcher - - elif ty == "span": - # We need a reference to the segment span in the batcher to populate - # the dynamic sampling context (DSC) - serialized["_segment_span"] = telemetry._segment # type: ignore - batcher = self.span_batcher - - if batcher is not None: - batcher.add(serialized) # type: ignore - - def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None: - self._capture_telemetry(log, "log", scope) - - def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None: - self._capture_telemetry(metric, "metric", scope) - - def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None: - self._capture_telemetry(span, "span", scope) - - def capture_session( - self, - session: "Session", - ) -> None: - if not session.release: - logger.info("Discarded session update because of missing release") - else: - self.session_flusher.add_session(session) - - if TYPE_CHECKING: - - @overload - def get_integration(self, name_or_class: str) -> "Optional[Integration]": ... - - @overload - def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ... - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Optional[Integration]": - """Returns the integration for this client by name or class. - If the client does not have that integration then `None` is returned. - """ - if isinstance(name_or_class, str): - integration_name = name_or_class - elif name_or_class.identifier is not None: - integration_name = name_or_class.identifier - else: - raise ValueError("Integration has no name") - - return self.integrations.get(integration_name) - - def _has_async_transport(self) -> bool: - """Check if the current transport is async.""" - return isinstance(self.transport, AsyncHttpTransport) - - @property - def _batchers(self) -> "tuple[Any, ...]": - return tuple( - b - for b in (self.log_batcher, self.metrics_batcher, self.span_batcher) - if b is not None - ) - - def _close_components(self) -> None: - """Kill all client components in the correct order.""" - self.session_flusher.kill() - for b in self._batchers: - b.kill() - if self.monitor: - self.monitor.kill() - - def _flush_components(self) -> None: - """Flush all client components.""" - self.session_flusher.flush() - for b in self._batchers: - b.flush() - - def close( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush`. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "close() used with AsyncHttpTransport. Use close_async() instead.", - stacklevel=2, - ) - self._flush_components() - else: - self.flush(timeout=timeout, callback=callback) - self._close_components() - self.transport.kill() - self.transport = None - - async def close_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously close the client and shut down the transport. Arguments have the same - semantics as :py:meth:`Client.flush_async`. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "close_async() used with non-async transport, aborting. Please use close() instead." - ) - return - await self.flush_async(timeout=timeout, callback=callback) - self._close_components() - kill_task = self.transport.kill() # type: ignore - if kill_task is not None: - await kill_task - self.transport = None - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if self._has_async_transport(): - warnings.warn( - "flush() used with AsyncHttpTransport. Use flush_async() instead.", - stacklevel=2, - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - - self.transport.flush(timeout=timeout, callback=callback) - - async def flush_async( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - Asynchronously wait for the current events to be sent. - - :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. - - :param callback: Is invoked with the number of pending events and the configured timeout. - """ - if self.transport is not None: - if not self._has_async_transport(): - logger.debug( - "flush_async() used with non-async transport, aborting. Please use flush() instead." - ) - return - if timeout is None: - timeout = self.options["shutdown_timeout"] - self._flush_components() - flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore - if flush_task is not None: - await flush_task - - def __enter__(self) -> "_Client": - return self - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - self.close() - - async def __aenter__(self) -> "_Client": - return self - - async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - await self.close_async() - - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Make mypy, PyCharm and other static analyzers think `get_options` is a - # type to have nicer autocompletion for params. - # - # Use `ClientConstructor` to define the argument types of `init` and - # `Dict[str, Any]` to tell static analyzers about the return type. - - class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 - pass - - class Client(ClientConstructor, _Client): - pass - -else: - # Alias `get_options` for actual usage. Go through the lambda indirection - # to throw PyCharm off of the weakly typed signature (it would otherwise - # discover both the weakly typed signature of `_init` and our faked `init` - # type). - - get_options = (lambda: _get_options)() - Client = (lambda: _Client)() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py deleted file mode 100644 index 759898f6ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/consts.py +++ /dev/null @@ -1,1802 +0,0 @@ -import itertools -from enum import Enum -from typing import TYPE_CHECKING - -DEFAULT_MAX_VALUE_LENGTH = None - -DEFAULT_MAX_STACK_FRAMES = 100 -DEFAULT_ADD_FULL_STACK = False - - -# Also needs to be at the top to prevent circular import -class EndpointType(Enum): - """ - The type of an endpoint. This is an enum, rather than a constant, for historical reasons - (the old /store endpoint). The enum also preserve future compatibility, in case we ever - have a new endpoint. - """ - - ENVELOPE = "envelope" - OTLP_TRACES = "integration/otlp/v1/traces" - - -class CompressionAlgo(Enum): - GZIP = "gzip" - BROTLI = "br" - - -if TYPE_CHECKING: - from typing import ( - AbstractSet, - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - ) - - from typing_extensions import Literal, TypedDict - - import sentry_sdk - from sentry_sdk._types import ( - BreadcrumbProcessor, - ContinuousProfilerMode, - DataCollectionUserOptions, - Event, - EventProcessor, - Hint, - IgnoreSpansConfig, - Log, - Metric, - ProfilerMode, - SpanJSON, - TracesSampler, - TransactionProcessor, - ) - - # Experiments are feature flags to enable and disable certain unstable SDK - # functionality. Changing them from the defaults (`None`) in production - # code is highly discouraged. They are not subject to any stability - # guarantees such as the ones from semantic versioning. - Experiments = TypedDict( - "Experiments", - { - "max_spans": Optional[int], - "max_flags": Optional[int], - "record_sql_params": Optional[bool], - "continuous_profiling_auto_start": Optional[bool], - "continuous_profiling_mode": Optional[ContinuousProfilerMode], - "otel_powered_performance": Optional[bool], - "transport_zlib_compression_level": Optional[int], - "transport_compression_level": Optional[int], - "transport_compression_algo": Optional[CompressionAlgo], - "transport_num_pools": Optional[int], - "transport_http2": Optional[bool], - "transport_async": Optional[bool], - "enable_logs": Optional[bool], - "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], - "enable_metrics": Optional[bool], - "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], - "trace_lifecycle": Optional[Literal["static", "stream"]], - "ignore_spans": Optional[IgnoreSpansConfig], - "before_send_span": Optional[ - Callable[[SpanJSON, Hint], Optional[SpanJSON]] - ], - "suppress_asgi_chained_exceptions": Optional[bool], - "data_collection": Optional[DataCollectionUserOptions], - }, - total=False, - ) - -DEFAULT_QUEUE_SIZE = 100 -DEFAULT_MAX_BREADCRUMBS = 100 -MATCH_ALL = r".*" - -FALSE_VALUES = [ - "false", - "no", - "off", - "n", - "0", -] - - -class SPANTEMPLATE(str, Enum): - DEFAULT = "default" - AI_AGENT = "ai_agent" - AI_TOOL = "ai_tool" - AI_CHAT = "ai_chat" - - def __str__(self) -> str: - return self.value - - -class INSTRUMENTER: - SENTRY = "sentry" - OTEL = "otel" - - -class SPANNAME: - DB_COMMIT = "COMMIT" - DB_ROLLBACK = "ROLLBACK" - - -class SPANDATA: - """ - Additional information describing the type of the span. - See: https://develop.sentry.dev/sdk/performance/span-data-conventions/ - """ - - AI_CITATIONS = "ai.citations" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - References or sources cited by the AI model in its response. - Example: ["Smith et al. 2020", "Jones 2019"] - """ - - AI_DOCUMENTS = "ai.documents" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Documents or content chunks used as context for the AI model. - Example: ["doc1.txt", "doc2.pdf"] - """ - - AI_FINISH_REASON = "ai.finish_reason" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. - - The reason why the model stopped generating. - Example: "length" - """ - - AI_FREQUENCY_PENALTY = "ai.frequency_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_FUNCTION_CALL = "ai.function_call" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_GENERATION_ID = "ai.generation_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. - - Unique identifier for the completion. - Example: "gen_123abc" - """ - - AI_INPUT_MESSAGES = "ai.input_messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. - - The input messages to an LLM call. - Example: [{"role": "user", "message": "hello"}] - """ - - AI_LOGIT_BIAS = "ai.logit_bias" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the logit bias - """ - - AI_METADATA = "ai.metadata" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Extra metadata passed to an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_MODEL_ID = "ai.model_id" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. - - The unique descriptor of the model being executed. - Example: gpt-4 - """ - - AI_PIPELINE_NAME = "ai.pipeline.name" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. - - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - AI_PREAMBLE = "ai.preamble" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the preamble parameter. - Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. - Example: "You are now a clown." - """ - - AI_PRESENCE_PENALTY = "ai.presence_penalty" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. - - Used to reduce repetitiveness of generated tokens. - Example: 0.5 - """ - - AI_RAW_PROMPTING = "ai.raw_prompting" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Minimize pre-processing done to the prompt sent to the LLM. - Example: true - """ - - AI_RESPONSE_FORMAT = "ai.response_format" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - For an AI model call, the format of the response - """ - - AI_RESPONSES = "ai.responses" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. - - The responses to an AI model call. Always as a list. - Example: ["hello", "world"] - """ - - AI_SEARCH_QUERIES = "ai.search_queries" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Queries used to search for relevant context or documents. - Example: ["climate change effects", "renewable energy"] - """ - - AI_SEARCH_REQUIRED = "ai.is_search_required" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Boolean indicating if the model needs to perform a search. - Example: true - """ - - AI_SEARCH_RESULTS = "ai.search_results" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Results returned from search queries for context. - Example: ["Result 1", "Result 2"] - """ - - AI_SEED = "ai.seed" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. - - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: 123.45 - """ - - AI_STREAMING = "ai.streaming" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. - - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - AI_TAGS = "ai.tags" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Tags that describe an AI pipeline step. - Example: {"executed_function": "add_integers"} - """ - - AI_TEMPERATURE = "ai.temperature" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. - - For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. - Example: 0.5 - """ - - AI_TEXTS = "ai.texts" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Raw text inputs provided to the model. - Example: ["What is machine learning?"] - """ - - AI_TOP_K = "ai.top_k" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. - - For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. - Example: 35 - """ - - AI_TOP_P = "ai.top_p" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. - - For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. - Example: 0.5 - """ - - AI_TOOL_CALLS = "ai.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. - - For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls - """ - - AI_TOOLS = "ai.tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. - - For an AI model call, the functions that are available - """ - - AI_WARNINGS = "ai.warnings" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_* attributes instead. - - Warning messages generated during model execution. - Example: ["Token limit exceeded"] - """ - - CACHE_HIT = "cache.hit" - """ - A boolean indicating whether the requested data was found in the cache. - Example: true - """ - - CACHE_ITEM_SIZE = "cache.item_size" - """ - The size of the requested data in bytes. - Example: 58 - """ - - CACHE_KEY = "cache.key" - """ - The key of the requested data. - Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 - """ - - CLIENT_ADDRESS = "client.address" - """ - Client address of the network connection - IP address or Unix domain socket name. - Example: "10.1.2.80" - """ - - CODE_FILEPATH = "code.filepath" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FILE_PATH instead. - - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FILE_PATH = "code.file.path" - """ - The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - Example: "/app/myapplication/http/handler/server.py" - """ - - CODE_FUNCTION = "code.function" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead. - - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_FUNCTION_NAME = "code.function.name" - """ - The method or function name, or equivalent (usually rightmost part of the code unit's name). - Example: "server_request" - """ - - CODE_LINENO = "code.lineno" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_LINE_NUMBER instead. - - The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. - Example: 42 - """ - - CODE_LINE_NUMBER = "code.line.number" - """ - The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. - Example: 42 - """ - - CODE_NAMESPACE = "code.namespace" - """ - .. deprecated:: - This attribute is deprecated. Use CODE_FUNCTION_NAME instead; the namespace should be included within the function name. - - The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. - Example: "http.handler" - """ - - DB_MONGODB_COLLECTION = "db.mongodb.collection" - """ - The MongoDB collection being accessed within the database. - See: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/mongodb.md#attributes - Example: public.users; customers - """ - - DB_NAME = "db.name" - """ - .. deprecated:: - This attribute is deprecated. Use DB_NAMESPACE instead. - - The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - Example: myDatabase - """ - - DB_NAMESPACE = "db.namespace" - """ - The name of the database being accessed. - Example: "customers" - """ - - DB_DRIVER_NAME = "db.driver.name" - """ - The name of the database driver being used for the connection. - Example: "psycopg2" - """ - - DB_OPERATION = "db.operation" - """ - .. deprecated:: - This attribute is deprecated. Use DB_OPERATION_NAME instead. - - The name of the operation being executed, e.g. the MongoDB command name such as findAndModify, or the SQL keyword. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: findAndModify, HMSET, SELECT - """ - - DB_OPERATION_NAME = "db.operation.name" - """ - The name of the operation being executed. - Example: "SELECT" - """ - - DB_SYSTEM = "db.system" - """ - .. deprecated:: - This attribute is deprecated. Use DB_SYSTEM_NAME instead. - - An identifier for the database management system (DBMS) product being used. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: postgresql - """ - - DB_QUERY_TEXT = "db.query.text" - """ - The database query being executed. - Example: "SELECT * FROM users WHERE id = $1" - """ - - DB_SYSTEM_NAME = "db.system.name" - """ - An identifier for the database management system (DBMS) product being used. See OpenTelemetry's list of well-known DBMS identifiers. - Example: "postgresql" - """ - - DB_USER = "db.user" - """ - The name of the database user used for connecting to the database. - See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - Example: my_user - """ - - GEN_AI_AGENT_NAME = "gen_ai.agent.name" - """ - The name of the agent being used. - Example: "ResearchAssistant" - """ - - GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" - """ - The unique identifier for the conversation/thread with the AI model. - Example: "conv_abc123" - """ - - GEN_AI_CHOICE = "gen_ai.choice" - """ - The model's response message. - Example: "The weather in Paris is rainy and overcast, with temperatures around 57°F" - """ - - GEN_AI_EMBEDDINGS_INPUT = "gen_ai.embeddings.input" - """ - The input to the embeddings operation. - Example: "Hello!" - """ - - GEN_AI_FUNCTION_ID = "gen_ai.function_id" - """ - Framework-specific tracing label for the execution of a function or other unit of execution in a generative AI system. - Example: "my-awesome-function" - """ - - GEN_AI_OPERATION_NAME = "gen_ai.operation.name" - """ - The name of the operation being performed. - Example: "chat" - """ - - GEN_AI_PIPELINE_NAME = "gen_ai.pipeline.name" - """ - Name of the AI pipeline or chain being executed. - Example: "qa-pipeline" - """ - - GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" - """ - The reason why the model stopped generating. - Example: "COMPLETE" - """ - - GEN_AI_RESPONSE_ID = "gen_ai.response.id" - """ - Unique identifier for the completion. - Example: "gen_123abc" - """ - - GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" - """ - Exact model identifier used to generate the response - Example: gpt-4o-mini-2024-07-18 - """ - - GEN_AI_RESPONSE_STREAMING = "gen_ai.response.streaming" - """ - Whether or not the AI model call's response was streamed back asynchronously - Example: true - """ - - GEN_AI_RESPONSE_TEXT = "gen_ai.response.text" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The model's response text messages. - Example: ["The weather in Paris is rainy and overcast, with temperatures around 57°F", "The weather in London is sunny and warm, with temperatures around 65°F"] - """ - - GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" - """ - The model's response messages. It has to be a stringified version of an array of message objects, which can include text responses and tool calls. - Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] - """ - - GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" - """ - The time it took to receive the first token from the model. - Example: 0.1 - """ - - GEN_AI_RESPONSE_TOOL_CALLS = "gen_ai.response.tool_calls" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_OUTPUT_MESSAGES instead. - - The tool calls in the model's response. - Example: [{"name": "get_weather", "arguments": {"location": "Paris"}}] - """ - - GEN_AI_REQUEST_AVAILABLE_TOOLS = "gen_ai.request.available_tools" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_DEFINITIONS instead. - - The available tools for the model. - Example: [{"name": "get_weather", "description": "Get the weather for a given location"}, {"name": "get_news", "description": "Get the news for a given topic"}] - """ - - GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" - """ - The list of source system tool definitions available to the GenAI agent or model. - Example: [{"type": "function", "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}, "required": ["location", "unit"]}}] - """ - - GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty" - """ - The frequency penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens" - """ - The maximum number of tokens to generate in the response. - Example: 2048 - """ - - GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" - """ - The system instructions passed to the model. - Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] - """ - - GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_INPUT_MESSAGES instead. - - The messages passed to the model. The "content" can be a string or an array of objects. - Example: [{role: "system", "content: "Generate a random number."}, {"role": "user", "content": [{"text": "Generate a random number between 0 and 10.", "type": "text"}]}] - """ - - GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" - """ - The messages passed to the model. It has to be a stringified version of an array of objects. Role values must be "user", "assistant", "tool", or "system". - Example: [{"role": "user", "parts": [{"type": "text", "content": "Weather in Paris?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "name": "get_weather", "arguments": {"location": "Paris"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_VSPygqKTWdrhaFErNvMV18Yl", "result": "rainy, 57°F"}]}] - """ - - GEN_AI_REQUEST_MODEL = "gen_ai.request.model" - """ - The model identifier being used for the request. - Example: "gpt-4-turbo" - """ - - GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty" - """ - The presence penalty parameter used to reduce repetitiveness of generated tokens. - Example: 0.1 - """ - - GEN_AI_REQUEST_SEED = "gen_ai.request.seed" - """ - The seed, ideally models given the same seed and same other parameters will produce the exact same output. - Example: "1234567890" - """ - - GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" - """ - The temperature parameter used to control randomness in the output. - Example: 0.7 - """ - - GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" - """ - Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). - Example: 35 - """ - - GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" - """ - The top_p parameter used to control diversity via nucleus sampling. - Example: 1.0 - """ - - GEN_AI_SYSTEM = "gen_ai.system" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_PROVIDER_NAME instead. - - The name of the AI system being used. - Example: "openai" - """ - - GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" - """ - The Generative AI provider as identified by the client or server instrumentation. - Example: "openai" - """ - - GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" - """ - The description of the tool being used. - Example: "Searches the web for current information about a topic" - """ - - GEN_AI_TOOL_INPUT = "gen_ai.tool.input" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_ARGUMENTS instead. - - The input of the tool being used. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" - """ - The arguments of the tool call. It has to be a stringified version of the arguments to the tool. - Example: {"location": "Paris"} - """ - - GEN_AI_TOOL_NAME = "gen_ai.tool.name" - """ - The name of the tool being used. - Example: "web_search" - """ - - GEN_AI_TOOL_OUTPUT = "gen_ai.tool.output" - """ - .. deprecated:: - This attribute is deprecated. Use GEN_AI_TOOL_CALL_RESULT instead. - - The output of the tool being used. - Example: "rainy, 57°F" - """ - - GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" - """ - The result of the tool call. It has to be a stringified version of the result of the tool. - Example: "rainy, 57°F" - """ - - GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" - """ - The number of tokens in the input. - Example: 150 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHED = "gen_ai.usage.input_tokens.cached" - """ - The number of cached tokens in the input. - Example: 50 - """ - - GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE = "gen_ai.usage.input_tokens.cache_write" - """ - The number of tokens written to the cache when processing the AI input (prompt). - Example: 100 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" - """ - The number of tokens in the output. - Example: 250 - """ - - GEN_AI_USAGE_OUTPUT_TOKENS_REASONING = "gen_ai.usage.output_tokens.reasoning" - """ - The number of tokens used for reasoning in the output. - Example: 75 - """ - - GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" - """ - The total number of tokens used (input + output). - Example: 400 - """ - - GEN_AI_USER_MESSAGE = "gen_ai.user.message" - """ - The user message passed to the model. - Example: "What's the weather in Paris?" - """ - - HTTP_FRAGMENT = "http.fragment" - """ - The Fragments present in the URL. - Example: #foo=bar - """ - - HTTP_METHOD = "http.method" - """ - .. deprecated:: - This attribute is deprecated. Use HTTP_REQUEST_METHOD instead. - - The HTTP method used. - Example: GET - """ - - HTTP_REQUEST_BODY_DATA = "http.request.body.data" - """ - HTTP request body data. Can be given as string or structural data of any format. - Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" - """ - - HTTP_REQUEST_HEADER = "http.request.header" - """ - Prefix for HTTP request header attributes. The header name (lowercased) is - appended to form the full attribute key. - Example: "http.request.header.content-type" - """ - - HTTP_REQUEST_METHOD = "http.request.method" - """ - The HTTP method used. - Example: GET - """ - - HTTP_QUERY = "http.query" - """ - The Query string present in the URL. - Example: ?foo=bar&bar=baz - """ - - HTTP_STATUS_CODE = "http.response.status_code" - """ - The HTTP status code as an integer. - Example: 418 - """ - - MESSAGING_DESTINATION_NAME = "messaging.destination.name" - """ - The destination name where the message is being consumed from, - e.g. the queue name or topic. - """ - - MESSAGING_MESSAGE_ID = "messaging.message.id" - """ - The message's identifier. - """ - - MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" - """ - The latency between when the task was enqueued and when it was started to be processed. - """ - - MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" - """ - Number of retries/attempts to process a message. - """ - - MESSAGING_SYSTEM = "messaging.system" - """ - The messaging system's name, e.g. `kafka`, `aws_sqs` - """ - - MIDDLEWARE_NAME = "middleware.name" - """ - The middleware's name, e.g. `AuthenticationMiddleware` - """ - - NETWORK_PROTOCOL_NAME = "network.protocol.name" - """ - The application layer protocol name used for the network connection. - Example: "http", "https" - """ - - NETWORK_PEER_ADDRESS = "network.peer.address" - """ - Peer address of the network connection - IP address or Unix domain socket name. - Example: 10.1.2.80, /tmp/my.sock, localhost - """ - - NETWORK_PEER_PORT = "network.peer.port" - """ - Peer port number of the network connection. - Example: 6379 - """ - - NETWORK_TRANSPORT = "network.transport" - """ - The transport protocol used for the network connection. - Example: "tcp", "udp", "unix" - """ - - PROCESS_PID = "process.pid" - """ - The process ID of the running process. - Example: 12345 - """ - - PROCESS_COMMAND_ARGS = "process.command_args" - """ - All the command arguments (including the command/executable itself) as received by the process. - Example: ["cmd/otecol","--config=config.yaml"] - """ - - PROFILER_ID = "profiler_id" - """ - Label identifying the profiler id that the span occurred in. This should be a string. - Example: "5249fbada8d5416482c2f6e47e337372" - """ - - RPC_METHOD = "rpc.method" - """ - The fully-qualified logical name of the method from the RPC interface perspective. - Example: "com.example.ExampleService/exampleMethod" - """ - - RPC_RESPONSE_STATUS_CODE = "rpc.response.status_code" - """ - Status code of the RPC returned by the RPC server or generated by the client. - Example: "DEADLINE_EXCEEDED" - """ - - SERVER_ADDRESS = "server.address" - """ - Name of the database host. - Example: example.com - """ - - SERVER_PORT = "server.port" - """ - Logical server port number - Example: 80; 8080; 443 - """ - - SERVER_SOCKET_ADDRESS = "server.socket.address" - """ - Physical server IP address or Unix socket address. - Example: 10.5.3.2 - """ - - SERVER_SOCKET_PORT = "server.socket.port" - """ - Physical server port. - Recommended: If different than server.port. - Example: 16456 - """ - - THREAD_ID = "thread.id" - """ - Identifier of a thread from where the span originated. This should be a string. - Example: "7972576320" - """ - - THREAD_NAME = "thread.name" - """ - Label identifying a thread from where the span originated. This should be a string. - Example: "MainThread" - """ - - USER_EMAIL = "user.email" - """ - User email address. - Example: "test@example.com" - """ - - USER_ID = "user.id" - """ - Unique identifier of the user. - Example: "S-1-5-21-202424912787-2692429404-2351956786-1000" - """ - - USER_IP_ADDRESS = "user.ip_address" - """ - The IP address of the user that triggered the request. - Example: "10.1.2.80" - """ - - USER_NAME = "user.name" - """ - Short name or login/username of the user. - Example: "j.smith" - """ - - URL_FULL = "url.full" - """ - The URL of the resource that was fetched. - Example: "https://example.com/test?foo=bar#buzz" - """ - - URL_FRAGMENT = "url.fragment" - """ - The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does. - Example: "details" - """ - - URL_PATH = "url.path" - """ - The URI path component. - Example: "/foo" - """ - - URL_QUERY = "url.query" - """ - The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does. - Example: "foo=bar&bar=baz" - """ - - MCP_TOOL_NAME = "mcp.tool.name" - """ - The name of the MCP tool being called. - Example: "get_weather" - """ - - MCP_PROMPT_NAME = "mcp.prompt.name" - """ - The name of the MCP prompt being retrieved. - Example: "code_review" - """ - - MCP_RESOURCE_URI = "mcp.resource.uri" - """ - The URI of the MCP resource being accessed. - Example: "file:///path/to/resource" - """ - - MCP_METHOD_NAME = "mcp.method.name" - """ - The MCP protocol method name being called. - Example: "tools/call", "prompts/get", "resources/read" - """ - - MCP_REQUEST_ID = "mcp.request.id" - """ - The unique identifier for the MCP request. - Example: "req_123abc" - """ - - MCP_TOOL_RESULT_CONTENT = "mcp.tool.result.content" - """ - The result/output content from an MCP tool execution. - Example: "The weather is sunny" - """ - - MCP_TOOL_RESULT_CONTENT_COUNT = "mcp.tool.result.content_count" - """ - The number of items/keys in the MCP tool result. - Example: 5 - """ - - MCP_TOOL_RESULT_IS_ERROR = "mcp.tool.result.is_error" - """ - Whether the MCP tool execution resulted in an error. - Example: True - """ - - MCP_PROMPT_RESULT_MESSAGE_CONTENT = "mcp.prompt.result.message_content" - """ - The message content from an MCP prompt retrieval. - Example: "Review the following code..." - """ - - MCP_PROMPT_RESULT_MESSAGE_ROLE = "mcp.prompt.result.message_role" - """ - The role of the message in an MCP prompt retrieval (only set for single-message prompts). - Example: "user", "assistant", "system" - """ - - MCP_PROMPT_RESULT_MESSAGE_COUNT = "mcp.prompt.result.message_count" - """ - The number of messages in an MCP prompt result. - Example: 1, 3 - """ - - MCP_RESOURCE_PROTOCOL = "mcp.resource.protocol" - """ - The protocol/scheme of the MCP resource URI. - Example: "file", "http", "https" - """ - - MCP_TRANSPORT = "mcp.transport" - """ - The transport method used for MCP communication. - Example: "http", "sse", "stdio" - """ - - MCP_SESSION_ID = "mcp.session.id" - """ - The session identifier for the MCP connection. - Example: "a1b2c3d4e5f6" - """ - - SENTRY_DIST = "sentry.dist" - """ - The Sentry dist. - Example: "1.0" - """ - - SENTRY_ENVIRONMENT = "sentry.environment" - """ - The Sentry environment. - Example: "prod" - """ - - SENTRY_RELEASE = "sentry.release" - """ - The Sentry release. - Example: "1.2.3" - """ - - SENTRY_PLATFORM = "sentry.platform" - """ - The sdk platform that generated the event. - Example: "python" - """ - - SENTRY_SDK_NAME = "sentry.sdk.name" - """ - The name of the SDK. - Example: "python" - """ - - SENTRY_SDK_VERSION = "sentry.sdk.version" - """ - The SDK version. - Example: "1.2.3" - """ - - SENTRY_SDK_INTEGRATIONS = "sentry.sdk.integrations" - """ - A list of names identifying enabled integrations. - Example: ["AtexitIntegration", "StdlibIntegration"] - """ - - -class SPANSTATUS: - """ - The status of a Sentry span. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - - ABORTED = "aborted" - ALREADY_EXISTS = "already_exists" - CANCELLED = "cancelled" - DATA_LOSS = "data_loss" - DEADLINE_EXCEEDED = "deadline_exceeded" - FAILED_PRECONDITION = "failed_precondition" - INTERNAL_ERROR = "internal_error" - INVALID_ARGUMENT = "invalid_argument" - NOT_FOUND = "not_found" - OK = "ok" - OUT_OF_RANGE = "out_of_range" - PERMISSION_DENIED = "permission_denied" - RESOURCE_EXHAUSTED = "resource_exhausted" - UNAUTHENTICATED = "unauthenticated" - UNAVAILABLE = "unavailable" - UNIMPLEMENTED = "unimplemented" - UNKNOWN_ERROR = "unknown_error" - - -class OP: - ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" - CACHE_GET = "cache.get" - CACHE_PUT = "cache.put" - COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere" - COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere" - DB = "db" - DB_CURSOR_ITERATOR = "db.cursor.iter" - DB_CURSOR_FETCH = "db.cursor.fetch" - DB_REDIS = "db.redis" - EVENT_DJANGO = "event.django" - FUNCTION = "function" - FUNCTION_AWS = "function.aws" - FUNCTION_GCP = "function.gcp" - GEN_AI_CHAT = "gen_ai.chat" - GEN_AI_CREATE_AGENT = "gen_ai.create_agent" - GEN_AI_EMBEDDINGS = "gen_ai.embeddings" - GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool" - GEN_AI_TEXT_COMPLETION = "gen_ai.text_completion" - GEN_AI_HANDOFF = "gen_ai.handoff" - GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent" - GEN_AI_RESPONSES = "gen_ai.responses" - GRAPHQL_EXECUTE = "graphql.execute" - GRAPHQL_MUTATION = "graphql.mutation" - GRAPHQL_PARSE = "graphql.parse" - GRAPHQL_RESOLVE = "graphql.resolve" - GRAPHQL_SUBSCRIPTION = "graphql.subscription" - GRAPHQL_QUERY = "graphql.query" - GRAPHQL_VALIDATE = "graphql.validate" - GRPC_CLIENT = "grpc.client" - GRPC_SERVER = "grpc.server" - HTTP_CLIENT = "http.client" - HTTP_CLIENT_STREAM = "http.client.stream" - HTTP_SERVER = "http.server" - MIDDLEWARE_DJANGO = "middleware.django" - MIDDLEWARE_LITESTAR = "middleware.litestar" - MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" - MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" - MIDDLEWARE_STARLETTE = "middleware.starlette" - MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" - MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" - MIDDLEWARE_STARLITE = "middleware.starlite" - MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" - MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" - HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( - "ai.chat_completions.create.huggingface_hub" - ) - QUEUE_PROCESS = "queue.process" - QUEUE_PUBLISH = "queue.publish" - QUEUE_SUBMIT_ARQ = "queue.submit.arq" - QUEUE_TASK_ARQ = "queue.task.arq" - QUEUE_SUBMIT_CELERY = "queue.submit.celery" - QUEUE_TASK_CELERY = "queue.task.celery" - QUEUE_TASK_RQ = "queue.task.rq" - QUEUE_SUBMIT_HUEY = "queue.submit.huey" - QUEUE_TASK_HUEY = "queue.task.huey" - QUEUE_SUBMIT_RAY = "queue.submit.ray" - QUEUE_TASK_RAY = "queue.task.ray" - QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" - QUEUE_SUBMIT_DJANGO = "queue.submit.django" - SUBPROCESS = "subprocess" - SUBPROCESS_WAIT = "subprocess.wait" - SUBPROCESS_COMMUNICATE = "subprocess.communicate" - TEMPLATE_RENDER = "template.render" - VIEW_RENDER = "view.render" - VIEW_RESPONSE_RENDER = "view.response.render" - WEBSOCKET_SERVER = "websocket.server" - SOCKET_CONNECTION = "socket.connection" - SOCKET_DNS = "socket.dns" - MCP_SERVER = "mcp.server" - - -# This type exists to trick mypy and PyCharm into thinking `init` and `Client` -# take these arguments (even though they take opaque **kwargs) -class ClientConstructor: - def __init__( - self, - dsn: "Optional[str]" = None, - *, - max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - server_name: "Optional[str]" = None, - shutdown_timeout: float = 2, - integrations: "Sequence[sentry_sdk.integrations.Integration]" = [], # noqa: B006 - in_app_include: "List[str]" = [], # noqa: B006 - in_app_exclude: "List[str]" = [], # noqa: B006 - default_integrations: bool = True, - dist: "Optional[str]" = None, - transport: "Optional[Union[sentry_sdk.transport.Transport, Type[sentry_sdk.transport.Transport], Callable[[Event], None]]]" = None, - transport_queue_size: int = DEFAULT_QUEUE_SIZE, - sample_rate: float = 1.0, - send_default_pii: "Optional[bool]" = None, - http_proxy: "Optional[str]" = None, - https_proxy: "Optional[str]" = None, - ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006 - max_request_body_size: str = "medium", - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None, - keep_alive: "Optional[bool]" = None, - before_send: "Optional[EventProcessor]" = None, - before_breadcrumb: "Optional[BreadcrumbProcessor]" = None, - debug: "Optional[bool]" = None, - attach_stacktrace: bool = False, - ca_certs: "Optional[str]" = None, - propagate_traces: bool = True, - traces_sample_rate: "Optional[float]" = None, - traces_sampler: "Optional[TracesSampler]" = None, - profiles_sample_rate: "Optional[float]" = None, - profiles_sampler: "Optional[TracesSampler]" = None, - profiler_mode: "Optional[ProfilerMode]" = None, - profile_lifecycle: 'Literal["manual", "trace"]' = "manual", - profile_session_sample_rate: "Optional[float]" = None, - auto_enabling_integrations: bool = True, - disabled_integrations: "Optional[Sequence[sentry_sdk.integrations.Integration]]" = None, - auto_session_tracking: bool = True, - send_client_reports: bool = True, - _experiments: "Experiments" = {}, # noqa: B006 - proxy_headers: "Optional[Dict[str, str]]" = None, - instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, - before_send_transaction: "Optional[TransactionProcessor]" = None, - project_root: "Optional[str]" = None, - enable_tracing: "Optional[bool]" = None, - include_local_variables: "Optional[bool]" = True, - include_source_context: "Optional[bool]" = True, - trace_propagation_targets: "Optional[Sequence[str]]" = [ # noqa: B006 - MATCH_ALL - ], - functions_to_trace: "Sequence[Dict[str, str]]" = [], # noqa: B006 - event_scrubber: "Optional[sentry_sdk.scrubber.EventScrubber]" = None, - max_value_length: "Optional[int]" = DEFAULT_MAX_VALUE_LENGTH, - enable_backpressure_handling: bool = True, - error_sampler: "Optional[Callable[[Event, Hint], Union[float, bool]]]" = None, - enable_db_query_source: bool = True, - db_query_source_threshold_ms: int = 100, - enable_http_request_source: bool = True, - http_request_source_threshold_ms: int = 100, - spotlight: "Optional[Union[bool, str]]" = None, - cert_file: "Optional[str]" = None, - key_file: "Optional[str]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, - add_full_stack: bool = DEFAULT_ADD_FULL_STACK, - max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, - enable_logs: bool = False, - before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, - trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), - enable_metrics: bool = True, - before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, - org_id: "Optional[str]" = None, - strict_trace_continuation: bool = False, - stream_gen_ai_spans: bool = True, - ) -> None: - """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. - - :param dsn: The DSN tells the SDK where to send the events. - - If this option is not set, the SDK will just not send any data. - - The `dsn` config option takes precedence over the environment variable. - - Learn more about `DSN utilization `_. - - :param debug: Turns debug mode on or off. - - When `True`, the SDK will attempt to print out debugging information. This can be useful if something goes - wrong with event sending. - - The default is always `False`. It's generally not recommended to turn it on in production because of the - increase in log output. - - The `debug` config option takes precedence over the environment variable. - - :param release: Sets the release. - - If not set, the SDK will try to automatically configure a release out of the box but it's a better idea to - manually set it to guarantee that the release is in sync with your deploy integrations. - - Release names are strings, but some formats are detected by Sentry and might be rendered differently. - - See `the releases documentation `_ to learn how the SDK tries to - automatically configure a release. - - The `release` config option takes precedence over the environment variable. - - Learn more about how to send release data so Sentry can tell you about regressions between releases and - identify the potential source in `the product documentation `_. - - :param environment: Sets the environment. This string is freeform and set to `production` by default. - - A release can be associated with more than one environment to separate them in the UI (think `staging` vs - `production` or similar). - - The `environment` config option takes precedence over the environment variable. - - :param dist: The distribution of the application. - - Distributions are used to disambiguate build or deployment variants of the same release of an application. - - The dist can be for example a build number. - - :param sample_rate: Configures the sample rate for error events, in the range of `0.0` to `1.0`. - - The default is `1.0`, which means that 100% of error events will be sent. If set to `0.1`, only 10% of - error events will be sent. - - Events are picked randomly. - - :param error_sampler: Dynamically configures the sample rate for error events on a per-event basis. - - This configuration option accepts a function, which takes two parameters (the `event` and the `hint`), and - which returns a boolean (indicating whether the event should be sent to Sentry) or a floating-point number - between `0.0` and `1.0`, inclusive. - - The number indicates the probability the event is sent to Sentry; the SDK will randomly decide whether to - send the event with the given probability. - - If this configuration option is specified, the `sample_rate` option is ignored. - - :param ignore_errors: A list of exception class names that shouldn't be sent to Sentry. - - Errors that are an instance of these exceptions or a subclass of them, will be filtered out before they're - sent to Sentry. - - By default, all errors are sent. - - :param max_breadcrumbs: This variable controls the total amount of breadcrumbs that should be captured. - - This defaults to `100`, but you can set this to any number. - - However, you should be aware that Sentry has a `maximum payload size `_ - and any events exceeding that payload size will be dropped. - - :param attach_stacktrace: When enabled, stack traces are automatically attached to all messages logged. - - Stack traces are always attached to exceptions; however, when this option is set, stack traces are also - sent with messages. - - This option means that stack traces appear next to all log messages. - - Grouping in Sentry is different for events with stack traces and without. As a result, you will get new - groups as you enable or disable this flag for certain events. - - :param send_default_pii: If this flag is enabled, `certain personally identifiable information (PII) - `_ is added by active integrations. - - If you enable this option, be sure to manually remove what you don't want to send using our features for - managing `Sensitive Data `_. - - :param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and - passwords from a `denylist`. - - It can additionally be used to scrub from another `pii_denylist` if `send_default_pii` is disabled. - - See how to `configure the scrubber here `_. - - :param include_source_context: When enabled, source context will be included in events sent to Sentry. - - This source context includes the five lines of code above and below the line of code where an error - happened. - - :param include_local_variables: When enabled, the SDK will capture a snapshot of local variables to send with - the event to help with debugging. - - :param add_full_stack: When capturing errors, Sentry stack traces typically only include frames that start the - moment an error occurs. - - But if the `add_full_stack` option is enabled (set to `True`), all frames from the start of execution will - be included in the stack trace sent to Sentry. - - :param max_stack_frames: This option limits the number of stack frames that will be captured when - `add_full_stack` is enabled. - - :param server_name: This option can be used to supply a server name. - - When provided, the name of the server is sent along and persisted in the event. - - For many integrations, the server name actually corresponds to the device hostname, even in situations - where the machine is not actually a server. - - :param project_root: The full path to the root directory of your application. - - The `project_root` is used to mark frames in a stack trace either as being in your application or outside - of the application. - - :param in_app_include: A list of string prefixes of module names that belong to the app. - - This option takes precedence over `in_app_exclude`. - - Sentry differentiates stack frames that are directly related to your application ("in application") from - stack frames that come from other packages such as the standard library, frameworks, or other dependencies. - - The application package is automatically marked as `inApp`. - - The difference is visible in [sentry.io](https://sentry.io), where only the "in application" frames are - displayed by default. - - :param in_app_exclude: A list of string prefixes of module names that do not belong to the app, but rather to - third-party packages. - - Modules considered not part of the app will be hidden from stack traces by default. - - This option can be overridden using `in_app_include`. - - :param max_request_body_size: This parameter controls whether integrations should capture HTTP request bodies. - It can be set to one of the following values: - - - `never`: Request bodies are never sent. - - `small`: Only small request bodies will be captured. The cutoff for small depends on the SDK (typically - 4KB). - - `medium`: Medium and small requests will be captured (typically 10KB). - - `always`: The SDK will always capture the request body as long as Sentry can make sense of it. - - Please note that the Sentry server [limits HTTP request body size](https://develop.sentry.dev/sdk/ - expected-features/data-handling/#variable-size). The server always enforces its size limit, regardless of - how you configure this option. - - :param max_value_length: The number of characters after which the values containing text in the event payload - will be truncated. - - WARNING: If the value you set for this is exceptionally large, the event may exceed 1 MiB and will be - dropped by Sentry. - - :param ca_certs: A path to an alternative CA bundle file in PEM-format. - - :param send_client_reports: Set this boolean to `False` to disable sending of client reports. - - Client reports allow the client to send status reports about itself to Sentry, such as information about - events that were dropped before being sent. - - :param integrations: List of integrations to enable in addition to `auto-enabling integrations (overview) - `_. - - This setting can be used to override the default config options for a specific auto-enabling integration - or to add an integration that is not auto-enabled. - - :param disabled_integrations: List of integrations that will be disabled. - - This setting can be used to explicitly turn off specific `auto-enabling integrations (list) - `_ or - `default `_ integrations. - - :param auto_enabling_integrations: Configures whether `auto-enabling integrations (configuration) - `_ should be enabled. - - When set to `False`, no auto-enabling integrations will be enabled by default, even if the corresponding - framework/library is detected. - - :param default_integrations: Configures whether `default integrations - `_ should be enabled. - - Setting `default_integrations` to `False` disables all default integrations **as well as all auto-enabling - integrations**, unless they are specifically added in the `integrations` option, described above. - - :param before_send: This function is called with an SDK-specific message or error event object, and can return - a modified event object, or `null` to skip reporting the event. - - This can be used, for instance, for manual PII stripping before sending. - - By the time `before_send` is executed, all scope data has already been applied to the event. Further - modification of the scope won't have any effect. - - :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can - return a modified transaction event object, or `null` to skip reporting the event. - - One way this might be used is for manual PII stripping before sending. - - :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb - is added to the scope. - - When nothing is returned from the function, the breadcrumb is dropped. - - To pass the breadcrumb through, return the first argument, which contains the breadcrumb object. - - The callback typically gets a second argument (called a "hint") which contains the original object from - which the breadcrumb was created to further customize what the breadcrumb should look like. - - :param transport: Switches out the transport used to send events. - - How this works depends on the SDK. It can, for instance, be used to capture events for unit-testing or to - send it through some more complex setup that requires proxy authentication. - - :param transport_queue_size: The maximum number of events that will be queued before the transport is forced to - flush. - - :param http_proxy: When set, a proxy can be configured that should be used for outbound requests. - - This is also used for HTTPS requests unless a separate `https_proxy` is configured. However, not all SDKs - support a separate HTTPS proxy. - - SDKs will attempt to default to the system-wide configured proxy, if possible. For instance, on Unix - systems, the `http_proxy` environment variable will be picked up. - - :param https_proxy: Configures a separate proxy for outgoing HTTPS requests. - - This value might not be supported by all SDKs. When not supported the `http-proxy` value is also used for - HTTPS requests at all times. - - :param proxy_headers: A dict containing additional proxy headers (usually for authentication) to be forwarded - to `urllib3`'s `ProxyManager `_. - - :param shutdown_timeout: Controls how many seconds to wait before shutting down. - - Sentry SDKs send events from a background queue. This queue is given a certain amount to drain pending - events. The default is SDK specific but typically around two seconds. - - Setting this value too low may cause problems for sending events from command line applications. - - Setting the value too high will cause the application to block for a long time for users experiencing - network connectivity problems. - - :param keep_alive: Determines whether to keep the connection alive between requests. - - This can be useful in environments where you encounter frequent network issues such as connection resets. - - :param cert_file: Path to the client certificate to use. - - If set, supersedes the `CLIENT_CERT_FILE` environment variable. - - :param key_file: Path to the key file to use. - - If set, supersedes the `CLIENT_KEY_FILE` environment variable. - - :param socket_options: An optional list of socket options to use. - - These provide fine-grained, low-level control over the way the SDK connects to Sentry. - - If provided, the options will override the default `urllib3` `socket options - `_. - - :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction - will be sent to Sentry. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - Either this or `traces_sampler` must be defined to enable tracing. - - If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have - another service (for example a JS frontend) that makes requests to your service that include trace - information, those traces will be continued and thus transactions will be sent to Sentry. - - If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces - will be started and no incoming traces will be continued. - - :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be - sent to Sentry. - - It will automatically be passed information about the transaction and the context in which it's being - created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being - sent). - - Can also be used for filtering transactions, by returning `0` for those that are unwanted. - - Either this or `traces_sample_rate` must be defined to enable tracing. - - :param trace_propagation_targets: An optional property that controls which downstream services receive tracing - data, in the form of a `sentry-trace` and a `baggage` header attached to any outgoing HTTP requests. - - The option may contain a list of strings or regex against which the URLs of outgoing requests are matched. - - If one of the entries in the list matches the URL of an outgoing request, trace data will be attached to - that request. - - String entries do not have to be full matches, meaning the URL of a request is matched when it _contains_ - a string provided through the option. - - If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the - instrumented client. - - :param functions_to_trace: An optional list of functions that should be set up for tracing. - - For each function in the list, a span will be created when the function is executed. - - Functions in the list are represented as strings containing the fully qualified name of the function. - - This is a convenient option, making it possible to have one central place for configuring what functions - to trace, instead of having custom instrumentation scattered all over your code base. - - To learn more, see the `Custom Instrumentation `_ documentation. - - :param enable_backpressure_handling: When enabled, a new monitor thread will be spawned to perform health - checks on the SDK. - - If the system is unhealthy, the SDK will keep halving the `traces_sample_rate` set by you in 10 second - intervals until recovery. - - This down sampling helps ensure that the system stays stable and reduces SDK overhead under high load. - - This option is enabled by default. - - :param enable_db_query_source: When enabled, the source location will be added to database queries. - - :param db_query_source_threshold_ms: The threshold in milliseconds for adding the source location to database - queries. - - The query location will be added to the query for queries slower than the specified threshold. - - :param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests. - - :param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an - outgoing HTTP request. - - The request location will be added to the request for requests slower than the specified threshold. - - :param custom_repr: A custom `repr `_ function to run - while serializing an object. - - Use this to control how your custom objects and classes are visible in Sentry. - - Return a string for that repr value to be used or `None` to continue serializing how Sentry would have - done it anyway. - - :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled - transaction will be profiled. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be - profiled. - - :param profiles_sampler: - - :param profiler_mode: - - :param profile_lifecycle: - - :param profile_session_sample_rate: - - :param enable_tracing: - - :param propagate_traces: - - :param auto_session_tracking: - - :param spotlight: - - :param instrumenter: - - :param enable_logs: Set `enable_logs` to True to enable the SDK to emit - Sentry logs. Defaults to False. - - :param before_send_log: An optional function to modify or filter out logs - before they're sent to Sentry. Any modifications to the log in this - function will be retained. If the function returns None, the log will - not be sent to Sentry. - - :param trace_ignore_status_codes: An optional property that disables tracing for - HTTP requests with certain status codes. - - Requests are not traced if the status code is contained in the provided set. - - If `trace_ignore_status_codes` is not provided, requests with any status code - may be traced. - - This option has no effect in span streaming mode (`trace_lifecycle="stream"`). - - :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the - `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. - - If set to `False`, consistency of `org_id` will only be enforced if both are present. If either are missing, the trace will be continued. - - The client's organization ID is extracted from the DSN or can be set with the `org_id` option. - If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. - This is useful to prevent traces of unknown third-party services from being continued in your application. - - :param org_id: An optional organization ID. The SDK will try to extract if from the DSN in most cases - but you can provide it explicitly for self-hosted and Relay setups. This value is used for - trace propagation and for features like `strict_trace_continuation`. - - :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to - reduce downstream data loss. - - :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. - - ``data_collection`` (EXPERIMENTAL): structured configuration controlling what data integrations - collect automatically, superseding `send_default_pii`. Passing a dict under - `_experiments={"data_collection": {...}}` opts into the feature; 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 - changes nothing. Restrict collection per category (user identity, cookies, HTTP - headers/bodies, query params, generative AI inputs/outputs, stack frame variables, source - context). If `send_default_pii` is also set, `data_collection` takes precedence. - - Example:: - - sentry_sdk.init( - dsn="...", - _experiments={"data_collection": {"user_info": False, "http_bodies": []}}, - ) - - See https://docs.sentry.io/platforms/python/configuration/options/#data_collection for more details. - """ - pass - - -def _get_default_options() -> "dict[str, Any]": - import inspect - - a = inspect.getfullargspec(ClientConstructor.__init__) - defaults = a.defaults or () - kwonlydefaults = a.kwonlydefaults or {} - - return dict( - itertools.chain( - zip(a.args[-len(defaults) :], defaults), - kwonlydefaults.items(), - ) - ) - - -DEFAULT_OPTIONS = _get_default_options() -del _get_default_options - - -VERSION = "2.64.0" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py deleted file mode 100644 index b3287703b9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.crons.api import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.crons.decorator import monitor - -__all__ = [ - "capture_checkin", - "MonitorStatus", - "monitor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py deleted file mode 100644 index 6ea3e36b6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/api.py +++ /dev/null @@ -1,60 +0,0 @@ -import uuid -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, MonitorConfig - - -def _create_check_in_event( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration_s: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> "Event": - options = sentry_sdk.get_client().options - check_in_id = check_in_id or uuid.uuid4().hex - - check_in: "Event" = { - "type": "check_in", - "monitor_slug": monitor_slug, - "check_in_id": check_in_id, - "status": status, - "duration": duration_s, - "environment": options.get("environment", None), - "release": options.get("release", None), - } - - if monitor_config: - check_in["monitor_config"] = monitor_config - - return check_in - - -def capture_checkin( - monitor_slug: "Optional[str]" = None, - check_in_id: "Optional[str]" = None, - status: "Optional[str]" = None, - duration: "Optional[float]" = None, - monitor_config: "Optional[MonitorConfig]" = None, -) -> str: - check_in_event = _create_check_in_event( - monitor_slug=monitor_slug, - check_in_id=check_in_id, - status=status, - duration_s=duration, - monitor_config=monitor_config, - ) - - sentry_sdk.capture_event(check_in_event) - - logger.debug( - f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}" - ) - - return check_in_event["check_in_id"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py deleted file mode 100644 index be686b4539..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/consts.py +++ /dev/null @@ -1,4 +0,0 @@ -class MonitorStatus: - IN_PROGRESS = "in_progress" - OK = "ok" - ERROR = "error" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py deleted file mode 100644 index b13d350e15..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/crons/decorator.py +++ /dev/null @@ -1,137 +0,0 @@ -from functools import wraps -from inspect import iscoroutinefunction -from typing import TYPE_CHECKING - -from sentry_sdk.crons import capture_checkin -from sentry_sdk.crons.consts import MonitorStatus -from sentry_sdk.utils import now - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from types import TracebackType - from typing import ( - Any, - Optional, - ParamSpec, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from sentry_sdk._types import MonitorConfig - - P = ParamSpec("P") - R = TypeVar("R") - - -class monitor: # noqa: N801 - """ - Decorator/context manager to capture checkin events for a monitor. - - Usage (as decorator): - ``` - import sentry_sdk - - app = Celery() - - @app.task - @sentry_sdk.monitor(monitor_slug='my-fancy-slug') - def test(arg): - print(arg) - ``` - - This does not have to be used with Celery, but if you do use it with celery, - put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator. - - Usage (as context manager): - ``` - import sentry_sdk - - def test(arg): - with sentry_sdk.monitor(monitor_slug='my-fancy-slug'): - print(arg) - ``` - """ - - def __init__( - self, - monitor_slug: "Optional[str]" = None, - monitor_config: "Optional[MonitorConfig]" = None, - ) -> None: - self.monitor_slug = monitor_slug - self.monitor_config = monitor_config - - def __enter__(self) -> None: - self.start_timestamp = now() - self.check_in_id = capture_checkin( - monitor_slug=self.monitor_slug, - status=MonitorStatus.IN_PROGRESS, - monitor_config=self.monitor_config, - ) - - def __exit__( - self, - exc_type: "Optional[Type[BaseException]]", - exc_value: "Optional[BaseException]", - traceback: "Optional[TracebackType]", - ) -> None: - duration_s = now() - self.start_timestamp - - if exc_type is None and exc_value is None and traceback is None: - status = MonitorStatus.OK - else: - status = MonitorStatus.ERROR - - capture_checkin( - monitor_slug=self.monitor_slug, - check_in_id=self.check_in_id, - status=status, - duration=duration_s, - monitor_config=self.monitor_config, - ) - - if TYPE_CHECKING: - - @overload - def __call__( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - # Unfortunately, mypy does not give us any reliable way to type check the - # return value of an Awaitable (i.e. async function) for this overload, - # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]]. - ... - - @overload - def __call__(self, fn: "Callable[P, R]") -> "Callable[P, R]": ... - - def __call__( - self, - fn: "Union[Callable[P, R], Callable[P, Awaitable[Any]]]", - ) -> "Union[Callable[P, R], Callable[P, Awaitable[Any]]]": - if iscoroutinefunction(fn): - return self._async_wrapper(fn) - - else: - if TYPE_CHECKING: - fn = cast("Callable[P, R]", fn) - return self._sync_wrapper(fn) - - def _async_wrapper( - self, fn: "Callable[P, Awaitable[Any]]" - ) -> "Callable[P, Awaitable[Any]]": - @wraps(fn) - async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return await fn(*args, **kwargs) - - return inner - - def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": - @wraps(fn) - def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": - with self: - return fn(*args, **kwargs) - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py deleted file mode 100644 index bcdf767409..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/data_collection.py +++ /dev/null @@ -1,319 +0,0 @@ -""" -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, List, Mapping, Optional, cast - -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE - -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 _is_sensitive_key(key: str, extra_terms: "Optional[List[str]]" = None) -> bool: - """ - Return whether ``key`` matches the sensitive denylist using a partial, - case-insensitive substring match. - - :param extra_terms: additional deny terms (e.g. user-provided) to consider - alongside the built-in `_SENSITIVE_DENYLIST`. - """ - lowered = key.lower() - for term in _SENSITIVE_DENYLIST: - if term in lowered: - return True - if extra_terms: - for term in extra_terms: - if term and term.lower() in lowered: - return True - return False - - -def _apply_key_value_collection_filtering( - items: "Mapping[str, Any]", - behaviour: "KeyValueCollectionBehaviour", - substitute: "Any" = SENSITIVE_DATA_SUBSTITUTE, -) -> "Dict[str, Any]": - - if behaviour["mode"] == "off": - return {} - - result: "Dict[str, Any]" = {} - - if behaviour["mode"] == "allowlist": - for key, value in items.items(): - is_allowed = False - if isinstance(key, str): - lowered = key.lower() - is_allowed = any( - term and term.lower() in lowered - for term in behaviour.get("terms", []) - ) - if is_allowed and not _is_sensitive_key(key): - result[key] = value - else: - result[key] = substitute - return result - - # denylist behaviour - for key, value in items.items(): - if isinstance(key, str) and _is_sensitive_key( - key=key, extra_terms=behaviour.get("terms", []) - ): - result[key] = substitute - else: - result[key] = value - return result - - -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, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py deleted file mode 100644 index 795882e9ef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/debug.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -import sys -import warnings -from logging import LogRecord - -from sentry_sdk import get_client -from sentry_sdk.client import _client_init_debug -from sentry_sdk.utils import logger - - -class _DebugFilter(logging.Filter): - def filter(self, record: "LogRecord") -> bool: - if _client_init_debug.get(False): - return True - - return get_client().options["debug"] - - -def init_debug_support() -> None: - if not logger.handlers: - configure_logger() - - -def configure_logger() -> None: - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.DEBUG) - logger.addFilter(_DebugFilter()) - - -def configure_debug_hub() -> None: - warnings.warn( - "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py deleted file mode 100644 index d2d4aae31a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/envelope.py +++ /dev/null @@ -1,332 +0,0 @@ -import io -import json -import mimetypes -from typing import TYPE_CHECKING - -from sentry_sdk.session import Session -from sentry_sdk.utils import capture_internal_exceptions, json_dumps - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, List, Optional, Union - - from sentry_sdk._types import Event, EventDataCategory - - -def parse_json(data: "Union[bytes, str]") -> "Any": - # on some python 3 versions this needs to be bytes - if isinstance(data, bytes): - data = data.decode("utf-8", "replace") - return json.loads(data) - - -class Envelope: - """ - Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints - documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, - each envelope may have at most one Item with type "event" or "transaction" (but not both). - """ - - def __init__( - self, - headers: "Optional[Dict[str, Any]]" = None, - items: "Optional[List[Item]]" = None, - ) -> None: - if headers is not None: - headers = dict(headers) - self.headers = headers or {} - if items is None: - items = [] - else: - items = list(items) - self.items = items - - @property - def description(self) -> str: - return "envelope with %s items (%s)" % ( - len(self.items), - ", ".join(x.data_category for x in self.items), - ) - - def add_event( - self, - event: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=event), type="event")) - - def add_transaction( - self, - transaction: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - - def add_profile( - self, - profile: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) - - def add_profile_chunk( - self, - profile_chunk: "Any", - ) -> None: - self.add_item( - Item( - payload=PayloadRef(json=profile_chunk), - type="profile_chunk", - headers={"platform": profile_chunk.get("platform", "python")}, - ) - ) - - def add_checkin( - self, - checkin: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in")) - - def add_session( - self, - session: "Union[Session, Any]", - ) -> None: - if isinstance(session, Session): - session = session.to_json() - self.add_item(Item(payload=PayloadRef(json=session), type="session")) - - def add_sessions( - self, - sessions: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions")) - - def add_item( - self, - item: "Item", - ) -> None: - self.items.append(item) - - def get_event(self) -> "Optional[Event]": - for items in self.items: - event = items.get_event() - if event is not None: - return event - return None - - def get_transaction_event(self) -> "Optional[Event]": - for item in self.items: - event = item.get_transaction_event() - if event is not None: - return event - return None - - def __iter__(self) -> "Iterator[Item]": - return iter(self.items) - - def serialize_into( - self, - f: "Any", - ) -> None: - f.write(json_dumps(self.headers)) - f.write(b"\n") - for item in self.items: - item.serialize_into(f) - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Envelope": - headers = parse_json(f.readline()) - items = [] - while 1: - item = Item.deserialize_from(f) - if item is None: - break - items.append(item) - return cls(headers=headers, items=items) - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Envelope": - return cls.deserialize_from(io.BytesIO(bytes)) - - def __repr__(self) -> str: - return "" % (self.headers, self.items) - - -class PayloadRef: - def __init__( - self, - bytes: "Optional[bytes]" = None, - path: "Optional[Union[bytes, str]]" = None, - json: "Optional[Any]" = None, - ) -> None: - self.json = json - self.bytes = bytes - self.path = path - - def get_bytes(self) -> bytes: - if self.bytes is None: - if self.path is not None: - with capture_internal_exceptions(): - with open(self.path, "rb") as f: - self.bytes = f.read() - elif self.json is not None: - self.bytes = json_dumps(self.json) - return self.bytes or b"" - - @property - def inferred_content_type(self) -> str: - if self.json is not None: - return "application/json" - elif self.path is not None: - path = self.path - if isinstance(path, bytes): - path = path.decode("utf-8", "replace") - ty = mimetypes.guess_type(path)[0] - if ty: - return ty - return "application/octet-stream" - - def __repr__(self) -> str: - return "" % (self.inferred_content_type,) - - -class Item: - def __init__( - self, - payload: "Union[bytes, str, PayloadRef]", - headers: "Optional[Dict[str, Any]]" = None, - type: "Optional[str]" = None, - content_type: "Optional[str]" = None, - filename: "Optional[str]" = None, - ): - if headers is not None: - headers = dict(headers) - elif headers is None: - headers = {} - self.headers = headers - if isinstance(payload, bytes): - payload = PayloadRef(bytes=payload) - elif isinstance(payload, str): - payload = PayloadRef(bytes=payload.encode("utf-8")) - else: - payload = payload - - if filename is not None: - headers["filename"] = filename - if type is not None: - headers["type"] = type - if content_type is not None: - headers["content_type"] = content_type - elif "content_type" not in headers: - headers["content_type"] = payload.inferred_content_type - - self.payload = payload - - def __repr__(self) -> str: - return "" % ( - self.headers, - self.payload, - self.data_category, - ) - - @property - def type(self) -> "Optional[str]": - return self.headers.get("type") - - @property - def data_category(self) -> "EventDataCategory": - ty = self.headers.get("type") - if ty == "session" or ty == "sessions": - return "session" - elif ty == "attachment": - return "attachment" - elif ty == "transaction": - return "transaction" - elif ty == "span": - return "span" - elif ty == "event": - return "error" - elif ty == "log": - return "log_item" - elif ty == "trace_metric": - return "trace_metric" - elif ty == "client_report": - return "internal" - elif ty == "profile": - return "profile" - elif ty == "profile_chunk": - return "profile_chunk" - elif ty == "check_in": - return "monitor" - else: - return "default" - - def get_bytes(self) -> bytes: - return self.payload.get_bytes() - - def get_event(self) -> "Optional[Event]": - """ - Returns an error event if there is one. - """ - if self.type == "event" and self.payload.json is not None: - return self.payload.json - return None - - def get_transaction_event(self) -> "Optional[Event]": - if self.type == "transaction" and self.payload.json is not None: - return self.payload.json - return None - - def serialize_into( - self, - f: "Any", - ) -> None: - headers = dict(self.headers) - bytes = self.get_bytes() - headers["length"] = len(bytes) - f.write(json_dumps(headers)) - f.write(b"\n") - f.write(bytes) - f.write(b"\n") - - def serialize(self) -> bytes: - out = io.BytesIO() - self.serialize_into(out) - return out.getvalue() - - @classmethod - def deserialize_from( - cls, - f: "Any", - ) -> "Optional[Item]": - line = f.readline().rstrip() - if not line: - return None - headers = parse_json(line) - length = headers.get("length") - if length is not None: - payload = f.read(length) - f.readline() - else: - # if no length was specified we need to read up to the end of line - # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) - payload = f.readline().rstrip(b"\n") - if headers.get("type") in ("event", "transaction"): - rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) - else: - rv = cls(headers=headers, payload=payload) - return rv - - @classmethod - def deserialize( - cls, - bytes: bytes, - ) -> "Optional[Item]": - return cls.deserialize_from(io.BytesIO(bytes)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py deleted file mode 100644 index 5eaa5e440b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/feature_flags.py +++ /dev/null @@ -1,75 +0,0 @@ -import copy -from threading import Lock -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import TypedDict - - FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) - - -DEFAULT_FLAG_CAPACITY = 100 - - -class FlagBuffer: - def __init__(self, capacity: int) -> None: - self.capacity = capacity - self.lock = Lock() - - # Buffer is private. The name is mangled to discourage use. If you use this attribute - # directly you're on your own! - self.__buffer = LRUCache(capacity) - - def clear(self) -> None: - self.__buffer = LRUCache(self.capacity) - - def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer": - with self.lock: - buffer = FlagBuffer(self.capacity) - buffer.__buffer = copy.deepcopy(self.__buffer, memo) - return buffer - - def get(self) -> "list[FlagData]": - with self.lock: - return [ - {"flag": key, "result": value} for key, value in self.__buffer.get_all() - ] - - def set(self, flag: str, result: bool) -> None: - if isinstance(result, FlagBuffer): - # If someone were to insert `self` into `self` this would create a circular dependency - # on the lock. This is of course a deadlock. However, this is far outside the expected - # usage of this class. We guard against it here for completeness and to document this - # expected failure mode. - raise ValueError( - "FlagBuffer instances can not be inserted into the dictionary." - ) - - with self.lock: - self.__buffer.set(flag, result) - - -def add_feature_flag(flag: str, result: bool) -> None: - """ - Records a flag and its value to be sent on subsequent error events. - We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. - """ - client = sentry_sdk.get_client() - - flags = sentry_sdk.get_isolation_scope().flags - flags.set(flag, result) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.get_current_span() - if span and isinstance(span, sentry_sdk.traces.StreamedSpan): - span.set_attribute(f"flag.evaluation.{flag}", result) - - else: - span = sentry_sdk.get_current_span() - if span and isinstance(span, Span): - span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py deleted file mode 100644 index b17444d06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/hub.py +++ /dev/null @@ -1,741 +0,0 @@ -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import ( - get_client, - get_current_scope, - get_global_scope, - get_isolation_scope, -) -from sentry_sdk._compat import with_metaclass -from sentry_sdk.client import Client -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.scope import _ScopeManager -from sentry_sdk.tracing import ( - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.utils import ( - ContextVar, - logger, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.tracing import TransactionKwargs - - T = TypeVar("T") - -else: - - def overload(x: "T") -> "T": - return x - - -class SentryHubDeprecationWarning(DeprecationWarning): - """ - A custom deprecation warning to inform users that the Hub is deprecated. - """ - - _MESSAGE = ( - "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " - "Please consult our 1.x to 2.x migration guide for details on how to migrate " - "`Hub` usage to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" - ) - - def __init__(self, *_: object) -> None: - super().__init__(self._MESSAGE) - - -@contextmanager -def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": - """Utility function to suppress deprecation warnings for the Hub.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) - yield - - -_local = ContextVar("sentry_current_hub") - - -class HubMeta(type): - @property - def current(cls) -> "Hub": - """Returns the current instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - rv = _local.get(None) - if rv is None: - with _suppress_hub_deprecation_warning(): - # This will raise a deprecation warning; suppress it since we already warned above. - rv = Hub(GLOBAL_HUB) - _local.set(rv) - return rv - - @property - def main(cls) -> "Hub": - """Returns the main instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - return GLOBAL_HUB - - -class Hub(with_metaclass(HubMeta)): # type: ignore - """ - .. deprecated:: 2.0.0 - The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. - - The hub wraps the concurrency management of the SDK. Each thread has - its own hub but the hub might transfer with the flow of execution if - context vars are available. - - If the hub is used with a with statement it's temporarily activated. - """ - - _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] - _scope: "Optional[Scope]" = None - - # Mypy doesn't pick up on the metaclass. - - if TYPE_CHECKING: - current: "Hub" = None # type: ignore[assignment] - main: "Optional[Hub]" = None - - def __init__( - self, - client_or_hub: "Optional[Union[Hub, Client]]" = None, - scope: "Optional[Any]" = None, - ) -> None: - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - - current_scope = None - - if isinstance(client_or_hub, Hub): - client = get_client() - if scope is None: - # hub cloning is going on, we use a fork of the current/isolation scope for context manager - scope = get_isolation_scope().fork() - current_scope = get_current_scope().fork() - else: - client = client_or_hub - get_global_scope().set_client(client) - - if scope is None: # so there is no Hub cloning going on - # just the current isolation scope is used for context manager - scope = get_isolation_scope() - current_scope = get_current_scope() - - if current_scope is None: - # just the current current scope is used for context manager - current_scope = get_current_scope() - - self._stack = [(client, scope)] # type: ignore - self._last_event_id: "Optional[str]" = None - self._old_hubs: "List[Hub]" = [] - - self._old_current_scopes: "List[Scope]" = [] - self._old_isolation_scopes: "List[Scope]" = [] - self._current_scope: "Scope" = current_scope - self._scope: "Scope" = scope - - def __enter__(self) -> "Hub": - self._old_hubs.append(Hub.current) - _local.set(self) - - current_scope = get_current_scope() - self._old_current_scopes.append(current_scope) - scope._current_scope.set(self._current_scope) - - isolation_scope = get_isolation_scope() - self._old_isolation_scopes.append(isolation_scope) - scope._isolation_scope.set(self._scope) - - return self - - def __exit__( - self, - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[Any]", - ) -> None: - old = self._old_hubs.pop() - _local.set(old) - - old_current_scope = self._old_current_scopes.pop() - scope._current_scope.set(old_current_scope) - - old_isolation_scope = self._old_isolation_scopes.pop() - scope._isolation_scope.set(old_isolation_scope) - - def run( - self, - callback: "Callable[[], T]", - ) -> "T": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Runs a callback in the context of the hub. Alternatively the - with statement can be used on the hub directly. - """ - with self: - return callback() - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Any": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. - - Returns the integration for this hub by name or class. If there - is no client bound or the client does not have that integration - then `None` is returned. - - If the return value is not `None` the hub is guaranteed to have a - client attached. - """ - return get_client().get_integration(name_or_class) - - @property - def client(self) -> "Optional[BaseClient]": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Please use :py:func:`sentry_sdk.api.get_client` instead. - - Returns the current client on the hub. - """ - client = get_client() - - if not client.is_active(): - return None - - return client - - @property - def scope(self) -> "Scope": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Returns the current scope on the hub. - """ - return get_isolation_scope() - - def last_event_id(self) -> "Optional[str]": - """ - Returns the last event ID. - - .. deprecated:: 1.40.5 - This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. - """ - logger.warning( - "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." - ) - return self._last_event_id - - def bind_client( - self, - new: "Optional[BaseClient]", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.set_client` instead. - - Binds a new client to the hub. - """ - get_global_scope().set_client(new) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. - - Captures an event. - - Alias of :py:meth:`sentry_sdk.Scope.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - """ - last_event_id = get_current_scope().capture_event( - event, hint, scope=scope, **scope_kwargs - ) - - is_transaction = event.get("type") == "transaction" - if last_event_id is not None and not is_transaction: - self._last_event_id = last_event_id - - return last_event_id - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. - - Captures a message. - - Alias of :py:meth:`sentry_sdk.Scope.capture_message`. - - :param message: The string to send as the message to Sentry. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_message( - message, level=level, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. - - Captures an exception. - - Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_exception( - error, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. - - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_span` instead. - - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - """ - scope = get_current_scope() - return scope.start_span(instrumenter=instrumenter, **kwargs) - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. - - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. - """ - scope = get_current_scope() - - # For backwards compatibility, we allow passing the scope as the hub. - # We need a major release to make this nice. (if someone searches the code: deprecated) - # Type checking disabled for this line because deprecated keys are not allowed in the type signature. - kwargs["hub"] = scope # type: ignore - - return scope.start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - ) -> "Transaction": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. - - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers=environ_or_headers, op=op, name=name, source=source - ) - - @overload - def push_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def push_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def push_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - if callback is not None: - with self.push_scope() as scope: - callback(scope) - return None - - return _ScopeManager(self) - - def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pops a scope layer from the stack. - - Try to use the context manager :py:meth:`push_scope` instead. - """ - rv = self._stack.pop() - assert self._stack, "stack must have at least one layer" - return rv - - @overload - def configure_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def configure_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def configure_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - scope = get_isolation_scope() - - if continue_trace: - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - def start_session( - self, - session_mode: str = "application", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_session` instead. - - Starts a new session. - """ - get_isolation_scope().start_session( - session_mode=session_mode, - ) - - def end_session(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.end_session` instead. - - Ends the current session if there is one. - """ - get_isolation_scope().end_session() - - def stop_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. - - Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - get_isolation_scope().stop_auto_session_tracking() - - def resume_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. - - Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - get_isolation_scope().resume_auto_session_tracking() - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.flush` instead. - - Alias for :py:meth:`sentry_sdk.client._Client.flush` - """ - return get_client().flush(timeout=timeout, callback=callback) - - def get_traceparent(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. - - Returns the traceparent either from the active span or from the scope. - """ - current_scope = get_current_scope() - traceparent = current_scope.get_traceparent() - - if traceparent is None: - isolation_scope = get_isolation_scope() - traceparent = isolation_scope.get_traceparent() - - return traceparent - - def get_baggage(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. - - Returns Baggage either from the active span or from the scope. - """ - current_scope = get_current_scope() - baggage = current_scope.get_baggage() - - if baggage is None: - isolation_scope = get_isolation_scope() - baggage = isolation_scope.get_baggage() - - if baggage is not None: - return baggage.serialize() - - return None - - def iter_trace_propagation_headers( - self, span: "Optional[Span]" = None - ) -> "Generator[Tuple[str, str], None, None]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. - - Return HTTP headers which allow propagation of trace data. Data taken - from the span representing the request, if available, or the current - span on the scope if not. - """ - return get_current_scope().iter_trace_propagation_headers( - span=span, - ) - - def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. - - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - return get_current_scope().trace_propagation_meta( - span=span, - ) - - -with _suppress_hub_deprecation_warning(): - # Suppress deprecation warning for the Hub here, since we still always - # import this module. - GLOBAL_HUB = Hub() -_local.set(GLOBAL_HUB) - - -# Circular imports -from sentry_sdk import scope diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py deleted file mode 100644 index 677d34a81e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/__init__.py +++ /dev/null @@ -1,352 +0,0 @@ -from abc import ABC, abstractmethod -from threading import Lock -from typing import TYPE_CHECKING - -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type, Union - - -_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600)) - - -_installer_lock = Lock() - -# Set of all integration identifiers we have attempted to install -_processed_integrations: "Set[str]" = set() - -# Set of all integration identifiers we have actually installed -_installed_integrations: "Set[str]" = set() - - -def _generate_default_integrations_iterator( - integrations: "List[str]", - auto_enabling_integrations: "List[str]", -) -> "Callable[[bool], Iterator[Type[Integration]]]": - def iter_default_integrations( - with_auto_enabling_integrations: bool, - ) -> "Iterator[Type[Integration]]": - """Returns an iterator of the default integration classes:""" - from importlib import import_module - - if with_auto_enabling_integrations: - all_import_strings = integrations + auto_enabling_integrations - else: - all_import_strings = integrations - - for import_string in all_import_strings: - try: - module, cls = import_string.rsplit(".", 1) - yield getattr(import_module(module), cls) - except (DidNotEnable, SyntaxError) as e: - logger.debug( - "Did not import default integration %s: %s", import_string, e - ) - - if isinstance(iter_default_integrations.__doc__, str): - for import_string in integrations: - iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) - - return iter_default_integrations - - -_DEFAULT_INTEGRATIONS = [ - # stdlib/base runtime integrations - "sentry_sdk.integrations.argv.ArgvIntegration", - "sentry_sdk.integrations.atexit.AtexitIntegration", - "sentry_sdk.integrations.dedupe.DedupeIntegration", - "sentry_sdk.integrations.excepthook.ExcepthookIntegration", - "sentry_sdk.integrations.logging.LoggingIntegration", - "sentry_sdk.integrations.modules.ModulesIntegration", - "sentry_sdk.integrations.stdlib.StdlibIntegration", - "sentry_sdk.integrations.threading.ThreadingIntegration", -] - -_AUTO_ENABLING_INTEGRATIONS = [ - "sentry_sdk.integrations.aiohttp.AioHttpIntegration", - "sentry_sdk.integrations.anthropic.AnthropicIntegration", - "sentry_sdk.integrations.ariadne.AriadneIntegration", - "sentry_sdk.integrations.arq.ArqIntegration", - "sentry_sdk.integrations.asyncpg.AsyncPGIntegration", - "sentry_sdk.integrations.boto3.Boto3Integration", - "sentry_sdk.integrations.bottle.BottleIntegration", - "sentry_sdk.integrations.celery.CeleryIntegration", - "sentry_sdk.integrations.chalice.ChaliceIntegration", - "sentry_sdk.integrations.clickhouse_driver.ClickhouseDriverIntegration", - "sentry_sdk.integrations.cohere.CohereIntegration", - "sentry_sdk.integrations.django.DjangoIntegration", - "sentry_sdk.integrations.falcon.FalconIntegration", - "sentry_sdk.integrations.fastapi.FastApiIntegration", - "sentry_sdk.integrations.flask.FlaskIntegration", - "sentry_sdk.integrations.gql.GQLIntegration", - "sentry_sdk.integrations.google_genai.GoogleGenAIIntegration", - "sentry_sdk.integrations.graphene.GrapheneIntegration", - "sentry_sdk.integrations.httpx.HttpxIntegration", - "sentry_sdk.integrations.httpx2.Httpx2Integration", - "sentry_sdk.integrations.huey.HueyIntegration", - "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", - "sentry_sdk.integrations.langchain.LangchainIntegration", - "sentry_sdk.integrations.langgraph.LanggraphIntegration", - "sentry_sdk.integrations.litestar.LitestarIntegration", - "sentry_sdk.integrations.loguru.LoguruIntegration", - "sentry_sdk.integrations.mcp.MCPIntegration", - "sentry_sdk.integrations.openai.OpenAIIntegration", - "sentry_sdk.integrations.openai_agents.OpenAIAgentsIntegration", - "sentry_sdk.integrations.pydantic_ai.PydanticAIIntegration", - "sentry_sdk.integrations.pymongo.PyMongoIntegration", - "sentry_sdk.integrations.pyramid.PyramidIntegration", - "sentry_sdk.integrations.quart.QuartIntegration", - "sentry_sdk.integrations.redis.RedisIntegration", - "sentry_sdk.integrations.rq.RqIntegration", - "sentry_sdk.integrations.sanic.SanicIntegration", - "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", - "sentry_sdk.integrations.starlette.StarletteIntegration", - "sentry_sdk.integrations.starlite.StarliteIntegration", - "sentry_sdk.integrations.strawberry.StrawberryIntegration", - "sentry_sdk.integrations.tornado.TornadoIntegration", -] - -iter_default_integrations = _generate_default_integrations_iterator( - integrations=_DEFAULT_INTEGRATIONS, - auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, -) - -del _generate_default_integrations_iterator - - -_MIN_VERSIONS = { - "aiohttp": (3, 4), - "aiomysql": (0, 3, 0), - "anthropic": (0, 16), - "ariadne": (0, 20), - "arq": (0, 23), - "asyncpg": (0, 23), - "beam": (2, 12), - "boto3": (1, 16), # botocore - "bottle": (0, 12), - "celery": (4, 4, 7), - "chalice": (1, 16, 0), - "clickhouse_driver": (0, 2, 0), - "cohere": (5, 4, 0), - "django": (1, 8), - "dramatiq": (1, 9), - "falcon": (1, 4), - "fastapi": (0, 79, 0), - "flask": (1, 1, 4), - "gql": (3, 4, 1), - "graphene": (3, 3), - "google_genai": (1, 29, 0), # google-genai - "grpc": (1, 32, 0), # grpcio - "httpx": (0, 16, 0), - "httpx2": (2, 0, 0), - "huggingface_hub": (0, 24, 7), - "langchain": (0, 1, 0), - "langgraph": (0, 6, 6), - "launchdarkly": (9, 8, 0), - "litellm": (1, 77, 5), - "loguru": (0, 7, 0), - "mcp": (1, 15, 0), - "openai": (1, 0, 0), - "openai_agents": (0, 0, 19), - "openfeature": (0, 7, 1), - "pydantic_ai": (1, 0, 0), - "pymongo": (3, 5, 0), - "pyreqwest": (0, 11, 6), - "quart": (0, 16, 0), - "ray": (2, 7, 0), - "requests": (2, 0, 0), - "rq": (0, 6), - "sanic": (0, 8), - "sqlalchemy": (1, 2), - "starlette": (0, 16), - "starlite": (1, 48), - "statsig": (0, 55, 3), - "strawberry": (0, 209, 5), - "tornado": (6, 0), - "typer": (0, 15), - "unleash": (6, 0, 1), -} - - -_INTEGRATION_DEACTIVATES = { - "langchain": {"openai", "anthropic", "google_genai"}, - "openai_agents": {"openai"}, - "pydantic_ai": {"openai", "anthropic"}, -} - - -def setup_integrations( - integrations: "Sequence[Integration]", - with_defaults: bool = True, - with_auto_enabling_integrations: bool = False, - disabled_integrations: "Optional[Sequence[Union[type[Integration], Integration]]]" = None, - options: "Optional[Dict[str, Any]]" = None, -) -> "Dict[str, Integration]": - """ - Given a list of integration instances, this installs them all. - - When `with_defaults` is set to `True` all default integrations are added - unless they were already provided before. - - `disabled_integrations` takes precedence over `with_defaults` and - `with_auto_enabling_integrations`. - - Some integrations are designed to automatically deactivate other integrations - in order to avoid conflicts and prevent duplicate telemetry from being collected. - For example, enabling the `langchain` integration will auto-deactivate both the - `openai` and `anthropic` integrations. - - Users can override this behavior by: - - Explicitly providing an integration in the `integrations=[]` list, or - - Disabling the higher-level integration via the `disabled_integrations` option. - """ - integrations = dict( - (integration.identifier, integration) for integration in integrations or () - ) - - logger.debug("Setting up integrations (with default = %s)", with_defaults) - - user_provided_integrations = set(integrations.keys()) - - # Integrations that will not be enabled - disabled_integrations = [ - integration if isinstance(integration, type) else type(integration) - for integration in disabled_integrations or [] - ] - - # Integrations that are not explicitly set up by the user. - used_as_default_integration = set() - - if with_defaults: - for integration_cls in iter_default_integrations( - with_auto_enabling_integrations - ): - if integration_cls.identifier not in integrations: - instance = integration_cls() - integrations[instance.identifier] = instance - used_as_default_integration.add(instance.identifier) - - disabled_integration_identifiers = { - integration.identifier for integration in disabled_integrations - } - - for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items(): - if ( - integration in integrations - and integration not in disabled_integration_identifiers - ): - for target in targets_to_deactivate: - if target not in user_provided_integrations: - for cls in iter_default_integrations(True): - if cls.identifier == target: - if cls not in disabled_integrations: - disabled_integrations.append(cls) - logger.debug( - "Auto-deactivating %s integration because %s integration is active", - target, - integration, - ) - - for identifier, integration in integrations.items(): - with _installer_lock: - if identifier not in _processed_integrations: - if type(integration) in disabled_integrations: - logger.debug("Ignoring integration %s", identifier) - else: - logger.debug( - "Setting up previously not enabled integration %s", identifier - ) - try: - type(integration).setup_once() - integration.setup_once_with_options(options) - except DidNotEnable as e: - if identifier not in used_as_default_integration: - raise - - logger.debug( - "Did not enable default integration %s: %s", identifier, e - ) - else: - _installed_integrations.add(identifier) - - _processed_integrations.add(identifier) - - integrations = { - identifier: integration - for identifier, integration in integrations.items() - if identifier in _installed_integrations - } - - for identifier in integrations: - logger.debug("Enabling integration %s", identifier) - - return integrations - - -def _check_minimum_version( - integration: "type[Integration]", - version: "Optional[tuple[int, ...]]", - package: "Optional[str]" = None, -) -> None: - package = package or integration.identifier - - if version is None: - raise DidNotEnable(f"Unparsable {package} version.") - - min_version = _MIN_VERSIONS.get(integration.identifier) - if min_version is None: - return - - if version < min_version: - raise DidNotEnable( - f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." - ) - - -class DidNotEnable(Exception): # noqa: N818 - """ - The integration could not be enabled due to a trivial user error like - `flask` not being installed for the `FlaskIntegration`. - - This exception is silently swallowed for default integrations, but reraised - for explicitly enabled integrations. - """ - - -class Integration(ABC): - """Baseclass for all integrations. - - To accept options for an integration, implement your own constructor that - saves those options on `self`. - """ - - install = None - """Legacy method, do not implement.""" - - identifier: "str" = None # type: ignore[assignment] - """String unique ID of integration type""" - - @staticmethod - @abstractmethod - def setup_once() -> None: - """ - Initialize the integration. - - This function is only called once, ever. Configuration is not available - at this point, so the only thing to do here is to hook into exception - handlers, and perhaps do monkeypatches. - - Inside those hooks `Integration.current` can be used to access the - instance again. - """ - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - """ - Called after setup_once in rare cases on the instance and with options since we don't have those available above. - """ - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py deleted file mode 100644 index 7ff4657013..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_asgi_common.py +++ /dev/null @@ -1,187 +0,0 @@ -import urllib -from enum import Enum -from typing import TYPE_CHECKING - -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.scope import should_send_default_pii - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from typing_extensions import Literal - - from sentry_sdk.utils import AnnotatedValue - - -class _RootPathInPath(Enum): - EXCLUDED = "excluded" - EITHER = "either" - - -def _get_headers(asgi_scope: "Any") -> "Dict[str, str]": - """ - Extract headers from the ASGI scope, in the format that the Sentry protocol expects. - """ - headers: "Dict[str, str]" = {} - for raw_key, raw_value in asgi_scope["headers"]: - key = raw_key.decode("latin-1") - value = raw_value.decode("latin-1") - if key in headers: - headers[key] = headers[key] + ", " + value - else: - headers[key] = value - - return headers - - -def _get_path( - asgi_scope: "Dict[str, Any]", root_path_in_path: "_RootPathInPath" -) -> "str": - if root_path_in_path is _RootPathInPath.EXCLUDED: - return asgi_scope.get("root_path", "") + asgi_scope.get("path", "") - - # Inverse of https://github.com/Kludex/starlette/blob/de970d7b3facb853eb7ad077decbf3d94f2aab6c/starlette/_utils.py#L96 - path = asgi_scope["path"] - root_path = asgi_scope.get("root_path", "") - - if not root_path or path == root_path or path.startswith(root_path + "/"): - return path - - return root_path + path - - -def _get_url( - asgi_scope: "Dict[str, Any]", - default_scheme: "Literal['ws', 'http']", - host: "Optional[Union[AnnotatedValue, str]]", - path: str, -) -> str: - """ - Extract URL from the ASGI scope, without also including the querystring. - """ - scheme = asgi_scope.get("scheme", default_scheme) - - server = asgi_scope.get("server", None) - - if host: - return "%s://%s%s" % (scheme, host, path) - - if server is not None: - host, port = server - default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(scheme) - if port != default_port: - return "%s://%s:%s%s" % (scheme, host, port, path) - return "%s://%s%s" % (scheme, host, path) - return path - - -def _get_query(asgi_scope: "Any") -> "Any": - """ - Extract querystring from the ASGI scope, in the format that the Sentry protocol expects. - """ - qs = asgi_scope.get("query_string") - if not qs: - return None - return urllib.parse.unquote(qs.decode("latin-1")) - - -def _get_ip(asgi_scope: "Any") -> str: - """ - Extract IP Address from the ASGI scope based on request headers with fallback to scope client. - """ - headers = _get_headers(asgi_scope) - try: - return headers["x-forwarded-for"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return headers["x-real-ip"] - except KeyError: - pass - - return asgi_scope.get("client")[0] - - -def _get_request_data( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "Dict[str, Any]": - """ - Returns data related to the HTTP request from the ASGI scope. - """ - request_data: "Dict[str, Any]" = {} - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - request_data["method"] = asgi_scope.get("method") - - headers = _get_headers(asgi_scope) - - request_data["headers"] = _filter_headers( - headers, - use_annotated_value=False, - ) - - request_data["query_string"] = _get_query(asgi_scope) - - request_data["url"] = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=_get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path), - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} - - return request_data - - -def _get_request_attributes( - asgi_scope: "Any", - root_path_in_path: "_RootPathInPath", -) -> "dict[str, Any]": - """ - Return attributes related to the HTTP request from the ASGI scope. - """ - attributes: "dict[str, Any]" = {} - - ty = asgi_scope["type"] - if ty in ("http", "websocket"): - if asgi_scope.get("method"): - attributes["http.request.method"] = asgi_scope["method"].upper() - - headers = _get_headers(asgi_scope) - - filtered_headers = _filter_headers(headers, use_annotated_value=False) - for header, value in filtered_headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - if should_send_default_pii(): - query = _get_query(asgi_scope) - if query: - attributes["http.query"] = query - - path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path) - attributes["url.path"] = path - - url_without_query_string = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - headers.get("host"), - path=path, - ) - query_string = _get_query(asgi_scope) - attributes["url.full"] = ( - f"{url_without_query_string}?{query_string}" - if query_string is not None - else url_without_query_string - ) - - client = asgi_scope.get("client") - if client and should_send_default_pii(): - ip = _get_ip(asgi_scope) - attributes["client.address"] = ip - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py deleted file mode 100644 index ad0ab7c734..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/_wsgi_common.py +++ /dev/null @@ -1,282 +0,0 @@ -import json -from contextlib import contextmanager -from copy import deepcopy - -import sentry_sdk -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE -from sentry_sdk.data_collection import _apply_key_value_collection_filtering -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue, has_data_collection_enabled, logger - -try: - from django.http.request import RawPostDataException - - _RAW_DATA_EXCEPTIONS = (RawPostDataException, ValueError) -except ImportError: - RawPostDataException = None - _RAW_DATA_EXCEPTIONS = (ValueError,) - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Mapping, MutableMapping, Optional, Union - - from sentry_sdk._types import Event, HttpStatusCodeRange - - -SENSITIVE_ENV_KEYS = ( - "REMOTE_ADDR", - "HTTP_X_FORWARDED_FOR", - "HTTP_SET_COOKIE", - "HTTP_COOKIE", - "HTTP_AUTHORIZATION", - "HTTP_PROXY_AUTHORIZATION", - "HTTP_X_API_KEY", - "HTTP_X_FORWARDED_FOR", - "HTTP_X_REAL_IP", -) - -SENSITIVE_HEADERS = tuple( - x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_") -) - -DEFAULT_HTTP_METHODS_TO_CAPTURE = ( - "CONNECT", - "DELETE", - "GET", - # "HEAD", # do not capture HEAD requests by default - # "OPTIONS", # do not capture OPTIONS requests by default - "PATCH", - "POST", - "PUT", - "TRACE", -) - - -# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support -@contextmanager -def nullcontext() -> "Iterator[None]": - yield - - -def request_body_within_bounds( - client: "Optional[sentry_sdk.client.BaseClient]", content_length: int -) -> bool: - if client is None: - return False - - bodies = client.options["max_request_body_size"] - return not ( - bodies == "never" - or (bodies == "small" and content_length > 10**3) - or (bodies == "medium" and content_length > 10**4) - ) - - -class RequestExtractor: - """ - Base class for request extraction. - """ - - # It does not make sense to make this class an ABC because it is not used - # for typing, only so that child classes can inherit common methods from - # it. Only some child classes implement all methods that raise - # NotImplementedError in this class. - - def __init__(self, request: "Any") -> None: - self.request = request - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - - content_length = self.content_length() - request_info = event.get("request", {}) - - if should_send_default_pii(): - request_info["cookies"] = dict(self.cookies()) - - if not request_body_within_bounds(client, content_length): - data = AnnotatedValue.removed_because_over_size_limit() - else: - # First read the raw body data - # It is important to read this first because if it is Django - # it will cache the body and then we can read the cached version - # again in parsed_body() (or json() or wherever). - raw_data = None - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # If DjangoRestFramework is used it already read the body for us - # so reading it here will fail. We can ignore this. - pass - - parsed_body = self.parsed_body() - if parsed_body is not None: - data = parsed_body - elif raw_data: - data = AnnotatedValue.removed_because_raw_data() - else: - data = None - - if data is not None: - request_info["data"] = data - - event["request"] = deepcopy(request_info) - - def content_length(self) -> int: - try: - return int(self.env().get("CONTENT_LENGTH", 0)) - except ValueError: - return 0 - - def cookies(self) -> "MutableMapping[str, Any]": - raise NotImplementedError() - - def raw_data(self) -> "Optional[Union[str, bytes]]": - raise NotImplementedError() - - def form(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - form = self.form() - except Exception: - form = None - try: - files = self.files() - except Exception: - files = None - - if form or files: - data = {} - if form: - data = dict(form.items()) - if files: - for key in files.keys(): - data[key] = AnnotatedValue.removed_because_raw_data() - - return data - - return self.json() - - def is_json(self) -> bool: - return _is_json_content_type(self.env().get("CONTENT_TYPE")) - - def json(self) -> "Optional[Any]": - try: - if not self.is_json(): - return None - - try: - raw_data = self.raw_data() - except _RAW_DATA_EXCEPTIONS: - # The body might have already been read, in which case this will - # fail - raw_data = None - - if raw_data is None: - return None - - if isinstance(raw_data, str): - return json.loads(raw_data) - else: - return json.loads(raw_data.decode("utf-8")) - except ValueError: - pass - - return None - - def files(self) -> "Optional[Dict[str, Any]]": - raise NotImplementedError() - - def size_of_file(self, file: "Any") -> int: - raise NotImplementedError() - - def env(self) -> "Dict[str, Any]": - raise NotImplementedError() - - -def _is_json_content_type(ct: "Optional[str]") -> bool: - mt = (ct or "").split(";", 1)[0] - return ( - mt == "application/json" - or (mt.startswith("application/")) - and mt.endswith("+json") - ) - - -def _filter_headers( - headers: "Mapping[str, str]", - use_annotated_value: bool = True, -) -> "Mapping[str, Union[AnnotatedValue, str]]": - client_options = sentry_sdk.get_client().options - - if has_data_collection_enabled(client_options): - data_collection_configuration = client_options["data_collection"] - - filtered = _apply_key_value_collection_filtering( - items=headers, - behaviour=data_collection_configuration["http_headers"]["request"], - ) - - for key in filtered: - if isinstance(key, str) and key.lower() in ("cookie", "set-cookie"): - filtered[key] = SENSITIVE_DATA_SUBSTITUTE - - return filtered - else: - if should_send_default_pii(): - return headers - - substitute: "Union[AnnotatedValue, str]" = ( - SENSITIVE_DATA_SUBSTITUTE - if not use_annotated_value - else AnnotatedValue.removed_because_over_size_limit() - ) - - return { - k: ( - v - if k.upper().replace("-", "_") not in SENSITIVE_HEADERS - else substitute - ) - for k, v in headers.items() - } - - -def _in_http_status_code_range( - code: object, code_ranges: "list[HttpStatusCodeRange]" -) -> bool: - for target in code_ranges: - if isinstance(target, int): - if code == target: - return True - continue - - try: - if code in target: - return True - except TypeError: - logger.warning( - "failed_request_status_codes has to be a list of integers or containers" - ) - - return False - - -class HttpCodeRangeContainer: - """ - Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int]. - Used for backwards compatibility with the old `failed_request_status_codes` option. - """ - - def __init__(self, code_ranges: "list[HttpStatusCodeRange]") -> None: - self._code_ranges = code_ranges - - def __contains__(self, item: object) -> bool: - return _in_http_status_code_range(item, self._code_ranges) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py deleted file mode 100644 index 59bae92a60..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiohttp.py +++ /dev/null @@ -1,509 +0,0 @@ -import sys -import weakref -from functools import wraps - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import ( - _filter_headers, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - NoOpStreamedSpan, - SegmentSource, - SpanStatus, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import ( - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - parse_url, - parse_version, - reraise, - transaction_from_function, -) - -try: - import asyncio - - from aiohttp import ClientSession, TraceConfig - from aiohttp import __version__ as AIOHTTP_VERSION - from aiohttp.web import Application, HTTPException, UrlDispatcher -except ImportError: - raise DidNotEnable("AIOHTTP not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Set - from types import SimpleNamespace - from typing import Any, ContextManager, Optional, Tuple, Union - - from aiohttp import TraceRequestEndParams, TraceRequestStartParams - from aiohttp.web_request import Request - from aiohttp.web_urldispatcher import UrlMappingMatchInfo - - from sentry_sdk._types import Attributes, Event, EventProcessor - from sentry_sdk.tracing import Span - from sentry_sdk.utils import ExcInfo - - -TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern") - - -class AioHttpIntegration(Integration): - identifier = "aiohttp" - origin = f"auto.http.{identifier}" - - def __init__( - self, - transaction_style: str = "handler_name", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self._failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(AIOHTTP_VERSION) - _check_minimum_version(AioHttpIntegration, version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - # In the aiohttp integration, all of their HTTP responses are Exceptions. - # Because they have to be raised and handled by the framework, we need to - # register the exceptions as control flow exceptions so that we don't - # accidentally overwrite a status of "ok" with "error". - _register_control_flow_exception(HTTPException) - - ignore_logger("aiohttp.server") - - old_handle = Application._handle - - async def sentry_app_handle( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(AioHttpIntegration) - if integration is None: - return await old_handle(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - # Scope data will not leak between requests because aiohttp - # create a task to wrap each request. - scope.generate_propagation_context() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - headers = dict(request.headers) - - span_ctx: "ContextManager[Union[Span, StreamedSpan]]" - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context({"aiohttp_request": request}) - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, - use_annotated_value=False, - ).items(): - header_attributes[ - f"http.request.header.{header.lower()}" - ] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - url_attributes = {} - if should_send_default_pii(): - url_attributes["url.full"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - url_attributes["url.path"] = request.path - - if request.query_string: - url_attributes["url.query"] = request.query_string - - client_address_attributes = {} - if should_send_default_pii() and request.remote: - client_address_attributes["client.address"] = request.remote - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, request.remote - ) - - span_ctx = sentry_sdk.traces.start_span( - # If this name makes it to the UI, AIOHTTP's URL - # resolver did not find a route or died trying. - name="generic AIOHTTP request", - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": AioHttpIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE.value, - "http.request.method": request.method, - **url_attributes, - **client_address_attributes, - **header_attributes, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # If this transaction name makes it to the UI, AIOHTTP's - # URL resolver did not find a route or died trying. - name="generic AIOHTTP request", - source=TransactionSource.ROUTE, - origin=AioHttpIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"aiohttp_request": request}, - ) - - with span_ctx as span: - try: - response = await old_handle(self, request) - except HTTPException as e: - if isinstance(span, StreamedSpan) and not isinstance( - span, NoOpStreamedSpan - ): - span.set_attribute( - "http.response.status_code", e.status_code - ) - - if e.status_code >= 400: - span.status = SpanStatus.ERROR.value - else: - span.status = SpanStatus.OK.value - else: - # Since a NoOpStreamedSpan can end up here, we have to guard against it - # so this only gets set in the legacy transaction approach. - if not isinstance(span, NoOpStreamedSpan): - span.set_http_status(e.status_code) - - if ( - e.status_code - in integration._failed_request_status_codes - ): - _capture_exception() - raise - except (asyncio.CancelledError, ConnectionResetError): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR.value - else: - span.set_status(SPANSTATUS.CANCELLED) - raise - except Exception: - # This will probably map to a 500 but seems like we - # have no way to tell. Do not set span status. - reraise(*_capture_exception()) - - try: - # A valid response handler will return a valid response with a status. But, if the handler - # returns an invalid response (e.g. None), the line below will raise an AttributeError. - # Even though this is likely invalid, we need to handle this case to ensure we don't break - # the application. - response_status = response.status - except AttributeError: - pass - else: - if isinstance(span, StreamedSpan): - span.set_attribute( - "http.response.status_code", response_status - ) - span.status = ( - SpanStatus.ERROR.value - if response_status >= 400 - else SpanStatus.OK.value - ) - else: - span.set_http_status(response_status) - - return response - - Application._handle = sentry_app_handle - - old_urldispatcher_resolve = UrlDispatcher.resolve - - @wraps(old_urldispatcher_resolve) - async def sentry_urldispatcher_resolve( - self: "UrlDispatcher", request: "Request" - ) -> "UrlMappingMatchInfo": - rv = await old_urldispatcher_resolve(self, request) - - integration = sentry_sdk.get_client().get_integration(AioHttpIntegration) - if integration is None: - return rv - - name = None - - try: - if integration.transaction_style == "handler_name": - name = transaction_from_function(rv.handler) - elif integration.transaction_style == "method_and_path_pattern": - route_info = rv.get_info() - pattern = route_info.get("path") or route_info.get("formatter") - name = "{} {}".format(request.method, pattern) - except Exception: - pass - - if name is not None: - current_span = sentry_sdk.get_current_span() - if isinstance(current_span, StreamedSpan) and not isinstance( - current_span, NoOpStreamedSpan - ): - current_span._segment.name = name - current_span._segment.set_attribute( - "sentry.span.source", - SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value, - ) - else: - current_scope = sentry_sdk.get_current_scope() - current_scope.set_transaction_name( - name, - source=SOURCE_FOR_STYLE[integration.transaction_style], - ) - - return rv - - UrlDispatcher.resolve = sentry_urldispatcher_resolve - - old_client_session_init = ClientSession.__init__ - - @ensure_integration_enabled(AioHttpIntegration, old_client_session_init) - def init(*args: "Any", **kwargs: "Any") -> None: - client_trace_configs = list(kwargs.get("trace_configs") or ()) - trace_config = create_trace_config() - client_trace_configs.append(trace_config) - - kwargs["trace_configs"] = client_trace_configs - return old_client_session_init(*args, **kwargs) - - ClientSession.__init__ = init - - -def create_trace_config() -> "TraceConfig": - async def on_request_start( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestStartParams", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(AioHttpIntegration) is None: - return - - method = params.method.upper() - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(params.url), sanitize=False) - - span_name = "%s %s" % ( - method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ) - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(client.options): - attributes: "Attributes" = { - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": AioHttpIntegration.origin, - "http.request.method": method, - } - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - attributes["url.path"] = params.url.path - - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes) - else: - legacy_span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=span_name, - origin=AioHttpIntegration.origin, - ) - legacy_span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - legacy_span.set_data("url", parsed_url.url) - legacy_span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - legacy_span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span = legacy_span - - if should_propagate_trace(client, str(params.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=params.url - ) - ) - if key == BAGGAGE_HEADER_NAME and params.headers.get( - BAGGAGE_HEADER_NAME - ): - # do not overwrite any existing baggage, just append to it - params.headers[key] += "," + value - else: - params.headers[key] = value - - trace_config_ctx.span = span - - async def on_request_end( - session: "ClientSession", - trace_config_ctx: "SimpleNamespace", - params: "TraceRequestEndParams", - ) -> None: - if trace_config_ctx.span is None: - return - - span = trace_config_ctx.span - status = int(params.response.status) - - if isinstance(span, StreamedSpan): - span.set_attribute("http.response.status_code", status) - span.status = ( - SpanStatus.ERROR.value if status >= 400 else SpanStatus.OK.value - ) - - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.set_http_status(status) - span.set_data("reason", params.response.reason) - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - trace_config = TraceConfig() - - trace_config.on_request_start.append(on_request_start) - trace_config.on_request_end.append(on_request_end) - - return trace_config - - -def _make_request_processor( - weak_request: "weakref.ReferenceType[Request]", -) -> "EventProcessor": - def aiohttp_processor( - event: "Event", - hint: "dict[str, Tuple[type, BaseException, Any]]", - ) -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - request_info = event.setdefault("request", {}) - - request_info["url"] = "%s://%s%s" % ( - request.scheme, - request.host, - request.path, - ) - - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote} - request_info["headers"] = _filter_headers(dict(request.headers)) - - # Just attach raw data here if it is within bounds, if available. - # Unfortunately there's no way to get structured data from aiohttp - # without awaiting on some coroutine. - request_info["data"] = get_aiohttp_request_data(request) - - return event - - return aiohttp_processor - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "aiohttp", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - return exc_info - - -BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]" - - -def get_aiohttp_request_data( - request: "Request", -) -> "Union[Optional[str], AnnotatedValue]": - bytes_body = request._read_bytes - - if bytes_body is not None: - # we have body to show - if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)): - return AnnotatedValue.substituted_because_over_size_limit() - - encoding = request.charset or "utf-8" - return bytes_body.decode(encoding, "replace") - - if request.can_read_body: - # body exists but we can't show it - return BODY_NOT_READ_MESSAGE - - # request has no body - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py deleted file mode 100644 index 49459268e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aiomysql.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import annotations - -from typing import Any, Awaitable, Callable, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import aiomysql # type: ignore[import-not-found] - from aiomysql.connection import Connection # type: ignore[import-not-found] - from aiomysql.cursors import Cursor # type: ignore[import-not-found] -except ImportError: - raise DidNotEnable("aiomysql not installed.") - - -class AioMySQLIntegration(Integration): - identifier = "aiomysql" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AioMySQLIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - aiomysql_version = parse_version(aiomysql.__version__) - _check_minimum_version(AioMySQLIntegration, aiomysql_version) - - Cursor.execute = _wrap_execute(Cursor.execute) - Cursor.executemany = _wrap_executemany(Cursor.executemany) - - # Patch Connection._connect — this catches ALL connections: - # - aiomysql.connect() - # - aiomysql.create_pool() (pool.py does `from .connection import connect` - # which ultimately calls Connection._connect) - # - Reconnects - Connection._connect = _wrap_connect(Connection._connect) - - -T = TypeVar("T") - - -def _normalize_query(query: str | bytes | bytearray) -> str: - if isinstance(query, (bytes, bytearray)): - query = query.decode("utf-8", errors="replace") - return " ".join(query.split()) - - -def _wrap_execute(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.execute to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - - # Skip if flagged by executemany (avoids double-recording). - # Do NOT reset the flag here — it must stay True for the entire - # duration of executemany, which may call execute multiple times - # in a loop (non-INSERT fallback). Only _wrap_executemany's - # finally block should clear it. - if getattr(cursor, "_sentry_skip_next_execute", False): - return await f(*args, **kwargs) - - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = params if integration and integration._record_params else None - param_style = "pyformat" if params_list else None - - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=False, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_executemany(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Cursor.executemany to capture SQL queries.""" - - async def _inner(*args: Any, **kwargs: Any) -> T: - if sentry_sdk.get_client().get_integration(AioMySQLIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - query = args[1] if len(args) > 1 else kwargs.get("query", "") - query_str = _normalize_query(query) - seq_of_params = args[2] if len(args) > 2 else kwargs.get("args") - - conn = _get_connection(cursor) - - integration = sentry_sdk.get_client().get_integration(AioMySQLIntegration) - params_list = ( - seq_of_params if integration and integration._record_params else None - ) - param_style = "pyformat" if params_list else None - - # Prevent double-recording: _do_execute_many calls self.execute internally - cursor._sentry_skip_next_execute = True - try: - with record_sql_queries( - cursor=None, - query=query_str, - params_list=params_list, - paramstyle=param_style, - executemany=True, - span_origin=AioMySQLIntegration.origin, - ) as span: - if conn: - _set_db_data(span, conn) - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - finally: - cursor._sentry_skip_next_execute = False - - return _inner - - -def _get_connection(cursor: Any) -> Any: - """Get the underlying connection from a cursor.""" - return getattr(cursor, "connection", None) - - -def _wrap_connect(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: - """Wrap Connection._connect to capture connection spans.""" - - async def _inner(self: "Connection") -> T: - client = sentry_sdk.get_client() - if client.get_integration(AioMySQLIntegration) is None: - return await f(self) - - if has_span_streaming_enabled(client.options): - breadcrumb_data = _get_connect_data(self, use_streaming_keys=True) - - span_attributes: dict[str, Any] = { - "sentry.op": OP.DB, - "sentry.origin": AioMySQLIntegration.origin, - } | breadcrumb_data - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=breadcrumb_data - ) - res = await f(self) - else: - connect_data = _get_connect_data(self) - - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AioMySQLIntegration.origin, - ) as span: - _set_db_data(span, self) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", - category="query", - data=connect_data, - ) - res = await f(self) - - return res - - return _inner - - -def _get_connect_data(conn: Any, *, use_streaming_keys: bool = False) -> dict[str, Any]: - if use_streaming_keys: - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - data: dict[str, Any] = { - db_system: "mysql", - SPANDATA.DB_DRIVER_NAME: "aiomysql", - } - - host = getattr(conn, "host", None) - if host is not None: - data[SPANDATA.SERVER_ADDRESS] = host - - port = getattr(conn, "port", None) - if port is not None: - data[SPANDATA.SERVER_PORT] = port - - database = getattr(conn, "db", None) - if database is not None: - data[db_name] = database - - user = getattr(conn, "user", None) - if user is not None: - data[SPANDATA.DB_USER] = user - - return data - - -def _set_db_data(span: Any, conn: Any) -> None: - """Set database-related span data from connection object.""" - if isinstance(span, StreamedSpan): - set_value = span.set_attribute - db_system = SPANDATA.DB_SYSTEM_NAME - db_name = SPANDATA.DB_NAMESPACE - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - set_value = span.set_data - db_system = SPANDATA.DB_SYSTEM - db_name = SPANDATA.DB_NAME - - set_value(db_system, "mysql") - set_value(SPANDATA.DB_DRIVER_NAME, "aiomysql") - - host = getattr(conn, "host", None) - if host is not None: - set_value(SPANDATA.SERVER_ADDRESS, host) - - port = getattr(conn, "port", None) - if port is not None: - set_value(SPANDATA.SERVER_PORT, port) - - database = getattr(conn, "db", None) - if database is not None: - set_value(db_name, database) - - user = getattr(conn, "user", None) - if user is not None: - set_value(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py deleted file mode 100644 index dfa4aef34c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/anthropic.py +++ /dev/null @@ -1,1154 +0,0 @@ -import json -import sys -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_anthropic_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - package_version, - reraise, - safe_serialize, -) - -try: - try: - from anthropic import NotGiven - except ImportError: - NotGiven = None - - try: - from anthropic import Omit - except ImportError: - Omit = None - - from anthropic import AsyncStream, Stream - from anthropic.lib.streaming import ( - AsyncMessageStream, - AsyncMessageStreamManager, - MessageStream, - MessageStreamManager, - ) - from anthropic.resources import AsyncMessages, Messages - from anthropic.types import ( - ContentBlockDeltaEvent, - ContentBlockStartEvent, - ContentBlockStopEvent, - MessageDeltaEvent, - MessageStartEvent, - MessageStopEvent, - ) - - if TYPE_CHECKING: - from anthropic.types import MessageStreamEvent, TextBlockParam -except ImportError: - raise DidNotEnable("Anthropic not installed") - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Iterator, - Optional, - Union, - ) - - from anthropic.types import ( - MessageParam, - ModelParam, - RawMessageStreamEvent, - TextBlockParam, - ToolUnionParam, - ) - - from sentry_sdk._types import TextPart - - -class _RecordedUsage: - output_tokens: int = 0 - input_tokens: int = 0 - cache_write_input_tokens: "Optional[int]" = 0 - cache_read_input_tokens: "Optional[int]" = 0 - - -class _StreamSpanContext: - """ - Sets accumulated data on the stream's span and finishes the span on exit. - Is a no-op if the stream has no span set, i.e., when the span has already been finished. - """ - - def __init__( - self, - stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]", - # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set). - guaranteed_streaming_state: bool = False, - ) -> None: - self._stream = stream - self._guaranteed_streaming_state = guaranteed_streaming_state - - def __enter__(self) -> "_StreamSpanContext": - return self - - def __exit__( - self, - exc_type: "Optional[type[BaseException]]", - exc_val: "Optional[BaseException]", - exc_tb: "Optional[Any]", - ) -> None: - with capture_internal_exceptions(): - if not hasattr(self._stream, "_span"): - return - - if not self._guaranteed_streaming_state and not hasattr( - self._stream, "_model" - ): - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - return - - _set_streaming_output_data( - span=self._stream._span, - integration=self._stream._integration, - model=self._stream._model, - usage=self._stream._usage, - content_blocks=self._stream._content_blocks, - response_id=self._stream._response_id, - finish_reason=self._stream._finish_reason, - ) - - self._stream._span.__exit__(exc_type, exc_val, exc_tb) - del self._stream._span - - -class AnthropicIntegration(Integration): - identifier = "anthropic" - origin = f"auto.ai.{identifier}" - - def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - version = package_version("anthropic") - _check_minimum_version(AnthropicIntegration, version) - - """ - client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol. - Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol. - The private _iterator variable and the close() method are patched. During iteration over the _iterator generator, - information from intercepted events is accumulated and used to populate output attributes on the AI Client Span. - - The span can be finished in two places: - - When the user exits the context manager or directly calls close(), the patched close() finishes the span. - - When iteration ends, the finally block in the _iterator wrapper finishes the span. - - Both paths may run. For example, the context manager exit can follow iterator exhaustion. - """ - Messages.create = _wrap_message_create(Messages.create) - Stream.close = _wrap_close(Stream.close) - - AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) - AsyncStream.close = _wrap_async_close(AsyncStream.close) - - """ - client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above. - """ - Messages.stream = _wrap_message_stream(Messages.stream) - MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( - MessageStreamManager.__enter__ - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # MessageStream inherits from Stream, so patching Stream is sufficient on these versions. - if not issubclass(MessageStream, Stream): - MessageStream.close = _wrap_close(MessageStream.close) - - AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) - AsyncMessageStreamManager.__aenter__ = ( - _wrap_async_message_stream_manager_aenter( - AsyncMessageStreamManager.__aenter__ - ) - ) - - # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a - # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions. - if not issubclass(AsyncMessageStream, AsyncStream): - AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "anthropic", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]": - """ - Get token usage from the Anthropic response. - Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens) - """ - input_tokens = 0 - output_tokens = 0 - cache_read_input_tokens = 0 - cache_write_input_tokens = 0 - if hasattr(result, "usage"): - usage = result.usage - if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): - input_tokens = usage.input_tokens - if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): - output_tokens = usage.output_tokens - if hasattr(usage, "cache_read_input_tokens") and isinstance( - usage.cache_read_input_tokens, int - ): - cache_read_input_tokens = usage.cache_read_input_tokens - if hasattr(usage, "cache_creation_input_tokens") and isinstance( - usage.cache_creation_input_tokens, int - ): - cache_write_input_tokens = usage.cache_creation_input_tokens - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens so downstream cost calculations - # (input_tokens - cached) don't produce negative values. - input_tokens += cache_read_input_tokens + cache_write_input_tokens - - return ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) - - -def _collect_ai_data( - event: "MessageStreamEvent", - model: "str | None", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": - """ - Collect model information, token usage, and collect content blocks from the AI streaming response. - """ - with capture_internal_exceptions(): - if hasattr(event, "type"): - if event.type == "content_block_start": - pass - elif event.type == "content_block_delta": - if hasattr(event.delta, "text"): - content_blocks.append(event.delta.text) - elif hasattr(event.delta, "partial_json"): - content_blocks.append(event.delta.partial_json) - elif event.type == "content_block_stop": - pass - - # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens. - # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 - if event.type == "message_start": - model = event.message.model or model - response_id = event.message.id - - incoming_usage = event.message.usage - usage.output_tokens = incoming_usage.output_tokens - usage.input_tokens = incoming_usage.input_tokens - - usage.cache_write_input_tokens = getattr( - incoming_usage, "cache_creation_input_tokens", None - ) - usage.cache_read_input_tokens = getattr( - incoming_usage, "cache_read_input_tokens", None - ) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - # Counterintuitive, but message_delta contains cumulative token counts :) - if event.type == "message_delta": - usage.output_tokens = event.usage.output_tokens - - # Update other usage fields if they exist in the event - input_tokens = getattr(event.usage, "input_tokens", None) - if input_tokens is not None: - usage.input_tokens = input_tokens - - cache_creation_input_tokens = getattr( - event.usage, "cache_creation_input_tokens", None - ) - if cache_creation_input_tokens is not None: - usage.cache_write_input_tokens = cache_creation_input_tokens - - cache_read_input_tokens = getattr( - event.usage, "cache_read_input_tokens", None - ) - if cache_read_input_tokens is not None: - usage.cache_read_input_tokens = cache_read_input_tokens - # TODO: Record event.usage.server_tool_use - - if event.delta.stop_reason is not None: - finish_reason = event.delta.stop_reason - - return (model, usage, content_blocks, response_id, finish_reason) - - return ( - model, - usage, - content_blocks, - response_id, - finish_reason, - ) - - -def _transform_anthropic_content_block( - content_block: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Transform an Anthropic content block using the Anthropic-specific transformer, - with special handling for Anthropic's text-type documents. - """ - # Handle Anthropic's text-type documents specially (not covered by shared function) - if content_block.get("type") == "document": - source = content_block.get("source") - if isinstance(source, dict) and source.get("type") == "text": - return { - "type": "text", - "text": source.get("data", ""), - } - - # Use Anthropic-specific transformation - result = transform_anthropic_content_part(content_block) - return result if result is not None else content_block - - -def _transform_system_instructions( - system_instructions: "Union[str, Iterable[TextBlockParam]]", -) -> "list[TextPart]": - if isinstance(system_instructions, str): - return [ - { - "type": "text", - "content": system_instructions, - } - ] - - return [ - { - "type": "text", - "content": instruction["text"], - } - for instruction in system_instructions - if isinstance(instruction, dict) and "text" in instruction - ] - - -def _set_common_input_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - max_tokens: "int", - messages: "Iterable[MessageParam]", - model: "ModelParam", - system: "Optional[Union[str, Iterable[TextBlockParam]]]", - temperature: "Optional[float]", - top_k: "Optional[int]", - top_p: "Optional[float]", - tools: "Optional[Iterable[ToolUnionParam]]", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic") - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if ( - messages is not None - and len(messages) > 0 # type: ignore - and should_send_default_pii() - and integration.include_prompts - ): - if isinstance(system, str) or isinstance(system, Iterable): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system)), - ) - - normalized_messages = [] - for message in messages: - if ( - message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER - and "content" in message - and isinstance(message["content"], (list, tuple)) - ): - transformed_content = [] - for item in message["content"]: - # Skip tool_result items - they can contain images/documents - # with nested structures that are difficult to redact properly - if isinstance(item, dict) and item.get("type") == "tool_result": - continue - - # Transform content blocks (images, documents, etc.) - transformed_content.append( - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - ) - - # If there are non-tool-result items, add them as a message - if transformed_content: - normalized_messages.append( - { - "role": message.get("role"), - "content": transformed_content, - } - ) - else: - # Transform content for non-list messages or assistant messages - transformed_message = message.copy() - if "content" in transformed_message: - content = transformed_message["content"] - if isinstance(content, (list, tuple)): - transformed_message["content"] = [ - _transform_anthropic_content_block(item) - if isinstance(item, dict) - else item - for item in content - ] - normalized_messages.append(transformed_message) - - role_normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(role_normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else role_normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - if model is not None and _is_given(model): - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - if top_k is not None and _is_given(top_k): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore - set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) - - -def _set_create_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "AnthropicIntegration", -) -> None: - """ - Set input data for the span based on the provided keyword arguments for the anthropic message creation. - """ - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=kwargs.get("max_tokens"), # type: ignore - messages=kwargs.get("messages"), # type: ignore - model=kwargs.get("model"), - system=kwargs.get("system"), - temperature=kwargs.get("temperature"), - top_k=kwargs.get("top_k"), - top_p=kwargs.get("top_p"), - tools=kwargs.get("tools"), - ) - - -def _wrap_synchronous_message_iterator( - stream: "Union[Stream, MessageStream]", - iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -async def _wrap_asynchronous_message_iterator( - stream: "Union[AsyncStream, AsyncMessageStream]", - iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", -) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span unless the span has already been closed in the close() patch. - """ - with _StreamSpanContext(stream, guaranteed_streaming_state=True): - async for event in iterator: - # Message and content types are aliases for corresponding Raw* types, introduced in - # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - if not isinstance( - event, - ( - MessageStartEvent, - MessageDeltaEvent, - MessageStopEvent, - ContentBlockStartEvent, - ContentBlockDeltaEvent, - ContentBlockStopEvent, - ), - ): - yield event - continue - - _accumulate_event_data(stream, event) - yield event - - -def _set_output_data( - span: "Union[Span, StreamedSpan]", - integration: "AnthropicIntegration", - model: "str | None", - input_tokens: "int | None", - output_tokens: "int | None", - cache_read_input_tokens: "int | None", - cache_write_input_tokens: "int | None", - content_blocks: "list[Any]", - response_id: "str | None" = None, - finish_reason: "str | None" = None, -) -> None: - """ - Set output data for the span based on the AI response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, model) - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - if finish_reason is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in content_blocks: - if output["type"] == "text": - output_messages["response"].append(output["text"]) - elif output["type"] == "tool_use": - output_messages["tool"].append(output) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - record_token_usage( - span, - input_tokens=input_tokens, - output_tokens=output_tokens, - input_tokens_cached=cache_read_input_tokens, - input_tokens_cache_write=cache_write_input_tokens, - ) - - -def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, Stream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_synchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -async def _sentry_patched_create_async( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Creates and manages an AI Client Span for both non-streaming and streaming calls. - """ - integration = kwargs.pop("integration") - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - return await f(*args, **kwargs) - - model = kwargs.get("model", "") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - _set_create_input_data(span, kwargs, integration) - - try: - result = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - if isinstance(result, AsyncStream): - result._span = span - result._integration = integration - - _initialize_data_accumulation_state(result) - result._iterator = _wrap_asynchronous_message_iterator( - result, - result._iterator, - ) - - return result - - with capture_internal_exceptions(): - if hasattr(result, "content"): - ( - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_write_input_tokens, - ) = _get_token_usage(result) - - content_blocks = [] - for content_block in result.content: - if hasattr(content_block, "to_dict"): - content_blocks.append(content_block.to_dict()) - elif hasattr(content_block, "model_dump"): - content_blocks.append(content_block.model_dump()) - elif hasattr(content_block, "text"): - content_blocks.append({"type": "text", "text": content_block.text}) - - _set_output_data( - span=span, - integration=integration, - model=getattr(result, "model", None), - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_input_tokens=cache_read_input_tokens, - cache_write_input_tokens=cache_write_input_tokens, - content_blocks=content_blocks, - response_id=getattr(result, "id", None), - finish_reason=getattr(result, "stop_reason", None), - ) - elif isinstance(span, Span): - span.set_data("unknown_response", True) - - span.__exit__(None, None, None) - - return result - - -def _wrap_message_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return _sentry_patched_create_sync(f, *args, **kwargs) - - return _sentry_wrapped_create_sync - - -def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None: - """ - Initialize fields for accumulating output on the Stream instance. - """ - if not hasattr(stream, "_model"): - stream._model = None - stream._usage = _RecordedUsage() - stream._content_blocks = [] - stream._response_id = None - stream._finish_reason = None - - -def _accumulate_event_data( - stream: "Union[Stream, MessageStream]", - event: "Union[RawMessageStreamEvent, MessageStreamEvent]", -) -> None: - """ - Update accumulated output from a single stream event. - """ - (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data( - event, - stream._model, - stream._usage, - stream._content_blocks, - stream._response_id, - stream._finish_reason, - ) - - stream._model = model - stream._usage = usage - stream._content_blocks = content_blocks - stream._response_id = response_id - stream._finish_reason = finish_reason - - -def _set_streaming_output_data( - span: "Span", - integration: "AnthropicIntegration", - model: "Optional[str]", - usage: "_RecordedUsage", - content_blocks: "list[str]", - response_id: "Optional[str]", - finish_reason: "Optional[str]", -) -> None: - """ - Set output attributes on the AI Client Span. - """ - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - response_id=response_id, - finish_reason=finish_reason, - ) - - -def _wrap_close( - f: "Callable[..., None]", -) -> "Callable[..., None]": - """ - Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first. - """ - - def close(self: "Union[Stream, MessageStream]") -> None: - with _StreamSpanContext(self): - return f(self) - - return close - - -def _wrap_message_create_async(f: "Any") -> "Any": - @wraps(f) - async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - kwargs["integration"] = integration - - return await _sentry_patched_create_async(f, *args, **kwargs) - - return _sentry_wrapped_create_async - - -def _wrap_async_close( - f: "Callable[..., Awaitable[None]]", -) -> "Callable[..., Awaitable[None]]": - """ - Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first. - """ - - async def close(self: "AsyncStream") -> None: - with _StreamSpanContext(self): - return await f(self) - - return close - - -def _wrap_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_message_stream_manager_enter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": - if not hasattr(self, "_max_tokens"): - return f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return f(self) - - if self._messages is None: - return f(self) - - try: - iter(self._messages) - except TypeError: - return f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_synchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_enter - - -def _wrap_async_message_stream(f: "Any") -> "Any": - """ - Attaches user-provided arguments to the returned context manager. - The attributes are set on AI Client Spans in the patch for the context manager. - """ - - @wraps(f) - def _sentry_patched_stream( - *args: "Any", **kwargs: "Any" - ) -> "AsyncMessageStreamManager": - stream_manager = f(*args, **kwargs) - - stream_manager._max_tokens = kwargs.get("max_tokens") - stream_manager._messages = kwargs.get("messages") - stream_manager._model = kwargs.get("model") - stream_manager._system = kwargs.get("system") - stream_manager._temperature = kwargs.get("temperature") - stream_manager._top_k = kwargs.get("top_k") - stream_manager._top_p = kwargs.get("top_p") - stream_manager._tools = kwargs.get("tools") - - return stream_manager - - return _sentry_patched_stream - - -def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": - """ - Creates and manages AI Client Spans. - """ - - @wraps(f) - async def _sentry_patched_aenter( - self: "AsyncMessageStreamManager", - ) -> "AsyncMessageStream": - if not hasattr(self, "_max_tokens"): - return await f(self) - - client = sentry_sdk.get_client() - integration = client.get_integration(AnthropicIntegration) - - if integration is None: - return await f(self) - - if self._messages is None: - return await f(self) - - try: - iter(self._messages) - except TypeError: - return await f(self) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name="chat" if self._model is None else f"chat {self._model}".strip(), - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": AnthropicIntegration.origin, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name="chat" if self._model is None else f"chat {self._model}".strip(), - origin=AnthropicIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=self._max_tokens, - messages=self._messages, - model=self._model, - system=self._system, - temperature=self._temperature, - top_k=self._top_k, - top_p=self._top_p, - tools=self._tools, - ) - - try: - stream = await f(self) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - stream._span = span - stream._integration = integration - - _initialize_data_accumulation_state(stream) - stream._iterator = _wrap_asynchronous_message_iterator( - stream, - stream._iterator, - ) - - return stream - - return _sentry_patched_aenter - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different anthropic versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py deleted file mode 100644 index 0215ffa093..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/argv.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class ArgvIntegration(Integration): - identifier = "argv" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ArgvIntegration) is not None: - extra = event.setdefault("extra", {}) - # If some event processor decided to set extra to e.g. an - # `int`, don't crash. Not here. - if isinstance(extra, dict): - extra["sys.argv"] = sys.argv - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py deleted file mode 100644 index 1ce228d3a5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ariadne.py +++ /dev/null @@ -1,167 +0,0 @@ -from importlib import import_module - -import sentry_sdk -from sentry_sdk import capture_event, get_client -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - # importing like this is necessary due to name shadowing in ariadne - # (ariadne.graphql is also a function) - ariadne_graphql = import_module("ariadne.graphql") -except ImportError: - raise DidNotEnable("ariadne is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional - - from ariadne.types import ( # type: ignore - GraphQLError, - GraphQLResult, - GraphQLSchema, - QueryParser, - ) - from graphql.language.ast import DocumentNode - - from sentry_sdk._types import Event, EventProcessor - - -class AriadneIntegration(Integration): - identifier = "ariadne" - - @staticmethod - def setup_once() -> None: - version = package_version("ariadne") - _check_minimum_version(AriadneIntegration, version) - - ignore_logger("ariadne") - - _patch_graphql() - - -def _patch_graphql() -> None: - old_parse_query = ariadne_graphql.parse_query - old_handle_errors = ariadne_graphql.handle_graphql_errors - old_handle_query_result = ariadne_graphql.handle_query_result - - @ensure_integration_enabled(AriadneIntegration, old_parse_query) - def _sentry_patched_parse_query( - context_value: "Optional[Any]", - query_parser: "Optional[QueryParser]", - data: "Any", - ) -> "DocumentNode": - event_processor = _make_request_event_processor(data) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - result = old_parse_query(context_value, query_parser, data) - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_errors) - def _sentry_patched_handle_graphql_errors( - errors: "List[GraphQLError]", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - result = old_handle_errors(errors, *args, **kwargs) - - event_processor = _make_response_event_processor(result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return result - - @ensure_integration_enabled(AriadneIntegration, old_handle_query_result) - def _sentry_patched_handle_query_result( - result: "Any", *args: "Any", **kwargs: "Any" - ) -> "GraphQLResult": - query_result = old_handle_query_result(result, *args, **kwargs) - - event_processor = _make_response_event_processor(query_result[1]) - sentry_sdk.get_isolation_scope().add_event_processor(event_processor) - - client = get_client() - if client.is_active(): - with capture_internal_exceptions(): - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": AriadneIntegration.identifier, - "handled": False, - }, - ) - capture_event(event, hint=hint) - - return query_result - - ariadne_graphql.parse_query = _sentry_patched_parse_query # type: ignore - ariadne_graphql.handle_graphql_errors = _sentry_patched_handle_graphql_errors # type: ignore - ariadne_graphql.handle_query_result = _sentry_patched_handle_query_result # type: ignore - - -def _make_request_event_processor(data: "GraphQLSchema") -> "EventProcessor": - """Add request data and api_target to events.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - if not isinstance(data, dict): - return event - - with capture_internal_exceptions(): - try: - content_length = int( - (data.get("headers") or {}).get("Content-Length", 0) - ) - except (TypeError, ValueError): - return event - - if should_send_default_pii() and request_body_within_bounds( - get_client(), content_length - ): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - request_info["data"] = data - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - return inner - - -def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcessor": - """Add response data to the event's response context.""" - - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii() and response.get("errors"): - contexts = event.setdefault("contexts", {}) - contexts["response"] = { - "data": response, - } - - return event - - return inner diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py deleted file mode 100644 index da03bafb8b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/arq.py +++ /dev/null @@ -1,277 +0,0 @@ -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Transaction, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -try: - import arq.worker - from arq.connections import ArqRedis - from arq.version import VERSION as ARQ_VERSION - from arq.worker import JobExecutionFailed, Retry, RetryJob, Worker -except ImportError: - raise DidNotEnable("Arq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from arq.cron import CronJob - from arq.jobs import Job - from arq.typing import WorkerCoroutine - from arq.worker import Function - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob) - - -class ArqIntegration(Integration): - identifier = "arq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - try: - if isinstance(ARQ_VERSION, str): - version = parse_version(ARQ_VERSION) - else: - version = ARQ_VERSION.version[:2] - - except (TypeError, ValueError): - version = None - - _check_minimum_version(ArqIntegration, version) - - patch_enqueue_job() - patch_run_job() - patch_create_worker() - - _register_control_flow_exception(ARQ_CONTROL_FLOW_EXCEPTIONS) # type: ignore - - ignore_logger("arq.worker") - - -def patch_enqueue_job() -> None: - old_enqueue_job = ArqRedis.enqueue_job - original_kwdefaults = old_enqueue_job.__kwdefaults__ - - async def _sentry_enqueue_job( - self: "ArqRedis", function: str, *args: "Any", **kwargs: "Any" - ) -> "Optional[Job]": - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_enqueue_job(self, function, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=function, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_ARQ, - "sentry.origin": ArqIntegration.origin, - }, - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin - ): - return await old_enqueue_job(self, function, *args, **kwargs) - - _sentry_enqueue_job.__kwdefaults__ = original_kwdefaults - ArqRedis.enqueue_job = _sentry_enqueue_job - - -def patch_run_job() -> None: - old_run_job = Worker.run_job - - async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: - client = sentry_sdk.get_client() - if client.get_integration(ArqIntegration) is None: - return await old_run_job(self, job_id, score) - - with sentry_sdk.isolation_scope() as scope: - scope._name = "arq" - scope.clear_breadcrumbs() - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name="unknown arq task", - attributes={ - "sentry.op": OP.QUEUE_TASK_ARQ, - "sentry.origin": ArqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job_id, - }, - parent_span=None, - ): - return await old_run_job(self, job_id, score) - - transaction = Transaction( - name="unknown arq task", - status="ok", - op=OP.QUEUE_TASK_ARQ, - source=TransactionSource.TASK, - origin=ArqIntegration.origin, - ) - - with sentry_sdk.start_transaction(transaction): - return await old_run_job(self, job_id, score) - - Worker.run_job = _sentry_run_job - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - - if scope.transaction is not None: - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - scope.transaction.set_status(SPANSTATUS.ABORTED) - return - - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": ArqIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - if scope.transaction is not None: - scope.transaction.name = ctx["job_name"] - event["transaction"] = ctx["job_name"] - - tags = event.setdefault("tags", {}) - tags["arq_task_id"] = ctx["job_id"] - tags["arq_task_retry"] = ctx["job_try"] > 1 - extra = event.setdefault("extra", {}) - extra["arq-job"] = { - "task": ctx["job_name"], - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": ctx["job_try"], - } - - return event - - return event_processor - - -def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": - async def _sentry_coroutine( - ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(ArqIntegration) - if integration is None: - return await coroutine(ctx, *args, **kwargs) - - if has_span_streaming_enabled(client.options): - scope = sentry_sdk.get_current_scope() - span = scope.streamed_span - if span is not None: - span.name = name - - scope.set_transaction_name(name) - - sentry_sdk.get_isolation_scope().add_event_processor( - _make_event_processor({**ctx, "job_name": name}, *args, **kwargs) - ) - - try: - result = await coroutine(ctx, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_coroutine - - -def patch_create_worker() -> None: - old_create_worker = arq.worker.create_worker - - @ensure_integration_enabled(ArqIntegration, old_create_worker) - def _sentry_create_worker(*args: "Any", **kwargs: "Any") -> "Worker": - settings_cls = args[0] if args else kwargs.get("settings_cls") - - if isinstance(settings_cls, dict): - if "functions" in settings_cls: - settings_cls["functions"] = [ - _get_arq_function(func) - for func in settings_cls.get("functions", []) - ] - if "cron_jobs" in settings_cls: - settings_cls["cron_jobs"] = [ - _get_arq_cron_job(cron_job) - for cron_job in settings_cls.get("cron_jobs", []) - ] - - if hasattr(settings_cls, "functions"): - settings_cls.functions = [ # type: ignore[union-attr] - _get_arq_function(func) - for func in settings_cls.functions # type: ignore[union-attr] - ] - if hasattr(settings_cls, "cron_jobs"): - settings_cls.cron_jobs = [ # type: ignore[union-attr] - _get_arq_cron_job(cron_job) - for cron_job in (settings_cls.cron_jobs or []) # type: ignore[union-attr] - ] - - if "functions" in kwargs: - kwargs["functions"] = [ - _get_arq_function(func) for func in kwargs.get("functions", []) - ] - if "cron_jobs" in kwargs: - kwargs["cron_jobs"] = [ - _get_arq_cron_job(cron_job) for cron_job in kwargs.get("cron_jobs", []) - ] - - return old_create_worker(*args, **kwargs) - - arq.worker.create_worker = _sentry_create_worker - - -def _get_arq_function(func: "Union[str, Function, WorkerCoroutine]") -> "Function": - arq_func = arq.worker.func(func) - arq_func.coroutine = _wrap_coroutine(arq_func.name, arq_func.coroutine) - - return arq_func - - -def _get_arq_cron_job(cron_job: "CronJob") -> "CronJob": - cron_job.coroutine = _wrap_coroutine(cron_job.name, cron_job.coroutine) - - return cron_job diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py deleted file mode 100644 index 8b1ff5e2a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asgi.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -An ASGI middleware. - -Based on Tom Christie's `sentry-asgi `. -""" - -import inspect -import sys -from copy import deepcopy -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._asgi_common import ( - _get_headers, - _get_ip, - _get_path, - _get_request_attributes, - _get_request_data, - _get_url, - _RootPathInPath, -) -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import ( - SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, -) -from sentry_sdk.traces import ( - SegmentSource, - StreamedSpan, -) -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - Transaction, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, - _get_installed_modules, - capture_internal_exceptions, - event_from_exception, - logger, - qualname_from_function, - reraise, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, ContextManager, Dict, Optional, Tuple, Union - - from sentry_sdk._types import Attributes, Event, Hint - from sentry_sdk.tracing import Span - - -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") - -_DEFAULT_TRANSACTION_NAME = "generic ASGI request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13 -if sys.version_info >= (3, 14): - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": mechanism_type, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _looks_like_asgi3(app: "Any") -> bool: - """ - Try to figure out if an application object supports ASGI3. - - This is how uvicorn figures out the application version as well. - """ - if inspect.isclass(app): - return hasattr(app, "__await__") - elif inspect.isfunction(app): - return iscoroutinefunction(app) - else: - call = getattr(app, "__call__", None) # noqa - return iscoroutinefunction(call) - - -class SentryAsgiMiddleware: - __slots__ = ( - "app", - "__call__", - "transaction_style", - "mechanism_type", - "span_origin", - "http_methods_to_capture", - "root_path_in_path", - ) - - def __init__( - self, - app: "Any", - unsafe_context_data: bool = False, - transaction_style: str = "endpoint", - mechanism_type: str = "asgi", - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - asgi_version: "Optional[int]" = None, - root_path_in_path: "_RootPathInPath" = _RootPathInPath.EXCLUDED, - ) -> None: - """ - Instrument an ASGI application with Sentry. Provides HTTP/websocket - data to sent events and basic handling for exceptions bubbling up - through the middleware. - - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. - """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - - asgi_middleware_while_using_starlette_or_fastapi = ( - mechanism_type == "asgi" and "starlette" in _get_installed_modules() - ) - if asgi_middleware_while_using_starlette_or_fastapi: - logger.warning( - "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. " - "Please remove 'SentryAsgiMiddleware' from your project. " - "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information." - ) - - self.transaction_style = transaction_style - self.mechanism_type = mechanism_type - self.span_origin = span_origin - self.app = app - self.http_methods_to_capture = http_methods_to_capture - self.root_path_in_path = root_path_in_path - - if asgi_version is None: - if _looks_like_asgi3(app): - asgi_version = 3 - else: - asgi_version = 2 - - if asgi_version == 3: - self.__call__ = self._run_asgi3 - elif asgi_version == 2: - self.__call__ = self._run_asgi2 # type: ignore - - def _capture_lifespan_exception(self, exc: Exception) -> None: - """Capture exceptions raise in application lifespan handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _capture_request_exception(self, exc: Exception) -> None: - """Capture exceptions raised in incoming request handlers. - - The separate function is needed to support overriding in derived integrations that use different catching mechanisms. - """ - return _capture_exception(exc=exc, mechanism_type=self.mechanism_type) - - def _run_asgi2(self, scope: "Any") -> "Any": - async def inner(receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=2) - - return inner - - async def _run_asgi3(self, scope: "Any", receive: "Any", send: "Any") -> "Any": - return await self._run_app(scope, receive, send, asgi_version=3) - - async def _run_app( - self, scope: "Any", receive: "Any", send: "Any", asgi_version: int - ) -> "Any": - is_recursive_asgi_middleware = _asgi_middleware_applied.get(False) - is_lifespan = scope["type"] == "lifespan" - if is_recursive_asgi_middleware or is_lifespan: - try: - if asgi_version == 2: - return await self.app(scope)(receive, send) - else: - return await self.app(scope, receive, send) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_lifespan_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_lifespan_exception(exc) - reraise(*exc_info) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _asgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as sentry_scope: - with track_session(sentry_scope, session_mode="request"): - sentry_scope.clear_breadcrumbs() - sentry_scope._name = "asgi" - processor = partial(self.event_processor, asgi_scope=scope) - sentry_scope.add_event_processor(processor) - - ty = scope["type"] - ( - transaction_name, - transaction_source, - ) = self._get_transaction_name_and_source( - self.transaction_style, - scope, - ) - - method = scope.get("method", "").upper() - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - if span_streaming: - segment: "Optional[StreamedSpan]" = None - attributes: "Attributes" = { - "sentry.span.source": getattr( - transaction_source, "value", transaction_source - ), - "sentry.origin": self.span_origin, - "network.protocol.name": ty, - } - - if scope.get("client") and should_send_default_pii(): - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) - - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - sentry_sdk.traces.continue_trace(_get_headers(scope)) - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = f"{ty}.server" - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - else: - sentry_sdk.traces.new_trace() - - Scope.set_custom_sampling_context({"asgi_scope": scope}) - - attributes["sentry.op"] = OP.HTTP_SERVER - segment = sentry_sdk.traces.start_span( - name=transaction_name, - attributes=attributes, - parent_span=None, - ) - - span_ctx = segment or nullcontext() - - else: - transaction = None - if ty in ("http", "websocket"): - if ( - ty == "websocket" - or method in self.http_methods_to_capture - ): - transaction = continue_trace( - _get_headers(scope), - op="{}.server".format(ty), - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - - if transaction: - transaction.set_tag("asgi.type", ty) - - span_ctx = ( - sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"asgi_scope": scope}, - ) - if transaction is not None - else nullcontext() - ) - - with span_ctx as span: - if isinstance(span, StreamedSpan): - for attribute, value in _get_request_attributes( - scope, - root_path_in_path=self.root_path_in_path, - ).items(): - span.set_attribute(attribute, value) - - try: - - async def _sentry_wrapped_send( - event: "Dict[str, Any]", - ) -> "Any": - if span is not None: - is_http_response = ( - event.get("type") == "http.response.start" - and "status" in event - ) - if is_http_response: - if isinstance(span, StreamedSpan): - span.status = ( - "error" - if event["status"] >= 400 - else "ok" - ) - span.set_attribute( - "http.response.status_code", - event["status"], - ) - else: - span.set_http_status(event["status"]) - - return await send(event) - - if asgi_version == 2: - return await self.app(scope)( - receive, _sentry_wrapped_send - ) - else: - return await self.app( - scope, receive, _sentry_wrapped_send - ) - - except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_request_exception(exc) - raise exc from None - - exc_info = sys.exc_info() - with capture_internal_exceptions(): - self._capture_request_exception(exc) - reraise(*exc_info) - - finally: - if isinstance(span, StreamedSpan): - already_set = ( - span is not None - and span.name != _DEFAULT_TRANSACTION_NAME - and span.get_attributes().get("sentry.span.source") - in [ - SegmentSource.COMPONENT.value, - SegmentSource.ROUTE.value, - SegmentSource.CUSTOM.value, - ] - ) - with capture_internal_exceptions(): - if not already_set: - name, source = ( - self._get_segment_name_and_source( - self.transaction_style, scope - ) - ) - span.name = name - span.set_attribute("sentry.span.source", source) - finally: - _asgi_middleware_applied.set(False) - - def event_processor( - self, event: "Event", hint: "Hint", asgi_scope: "Any" - ) -> "Optional[Event]": - request_data = event.get("request", {}) - request_data.update( - _get_request_data(asgi_scope, root_path_in_path=self.root_path_in_path) - ) - event["request"] = deepcopy(request_data) - - # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) - transaction = event.get("transaction") - transaction_source = (event.get("transaction_info") or {}).get("source") - already_set = ( - transaction is not None - and transaction != _DEFAULT_TRANSACTION_NAME - and transaction_source - in [ - TransactionSource.COMPONENT, - TransactionSource.ROUTE, - TransactionSource.CUSTOM, - ] - ) - if not already_set: - name, source = self._get_transaction_name_and_source( - self.transaction_style, asgi_scope - ) - event["transaction"] = name - event["transaction_info"] = {"source": source} - - return event - - # Helper functions. - # - # Note: Those functions are not public API. If you want to mutate request - # data to your liking it's recommended to use the `before_send` callback - # for that. - - def _get_transaction_name_and_source( - self: "SentryAsgiMiddleware", transaction_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SOURCE_FOR_STYLE[transaction_style] - ty = asgi_scope.get("type") - - if transaction_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = transaction_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - elif transaction_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = TransactionSource.URL - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - return name, source - - return name, source - - def _get_segment_name_and_source( - self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" - ) -> "Tuple[str, str]": - name = None - source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value - ty = asgi_scope.get("type") - - if segment_style == "endpoint": - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - name = qualname_from_function(endpoint) or "" - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - elif segment_style == "url": - # FastAPI includes the route object in the scope to let Sentry extract the - # path from it for the transaction name - route = asgi_scope.get("route") - if route: - path = getattr(route, "path", None) - if path is not None: - name = path - else: - name = _get_url( - asgi_scope, - "http" if ty == "http" else "ws", - host=None, - path=_get_path( - asgi_scope=asgi_scope, root_path_in_path=self.root_path_in_path - ), - ) - source = SegmentSource.URL.value - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = SegmentSource.ROUTE.value - return name, source - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py deleted file mode 100644 index 4b3f6d330e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncio.py +++ /dev/null @@ -1,280 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.transport import AsyncHttpTransport -from sentry_sdk.utils import ( - event_from_exception, - is_internal_task, - logger, - reraise, -) - -try: - import asyncio - from asyncio.tasks import Task -except ImportError: - raise DidNotEnable("asyncio not available") - -from typing import TYPE_CHECKING, Optional, Union - -if TYPE_CHECKING: - from collections.abc import Coroutine - from typing import Any, Callable, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T", bound=Callable[..., Any]) - - -def get_name(coro: "Any") -> str: - return ( - getattr(coro, "__qualname__", None) - or getattr(coro, "__name__", None) - or "coroutine without __name__" - ) - - -def _wrap_coroutine(wrapped: "Coroutine[Any, Any, Any]") -> "Callable[[T], T]": - # Only __name__ and __qualname__ are copied from function to coroutine in CPython - return functools.partial( - functools.update_wrapper, - wrapped=wrapped, # type: ignore - assigned=("__name__", "__qualname__"), - updated=(), - ) - - -def patch_loop_close() -> None: - """Patch loop.close to flush pending events before shutdown.""" - # Atexit shutdown hook happens after the event loop is closed. - # Therefore, it is necessary to patch the loop.close method to ensure - # that pending events are flushed before the interpreter shuts down. - try: - loop = asyncio.get_running_loop() - except RuntimeError: - # No running loop → cannot patch now - return - - if getattr(loop, "_sentry_flush_patched", False): - return - - async def _flush() -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - try: - if not isinstance(client.transport, AsyncHttpTransport): - return - - await client.close_async() - except Exception: - logger.warning("Sentry flush failed during loop shutdown", exc_info=True) - - orig_close = loop.close - - def _patched_close() -> None: - try: - loop.run_until_complete(_flush()) - except Exception: - logger.debug( - "Could not flush Sentry events during loop close", exc_info=True - ) - finally: - orig_close() - - loop.close = _patched_close # type: ignore - loop._sentry_flush_patched = True # type: ignore - - -def _create_task_with_factory( - orig_task_factory: "Any", - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", -) -> "asyncio.Task[Any]": - task = None - - # Trying to use user set task factory (if there is one) - if orig_task_factory: - task = orig_task_factory(loop, coro, **kwargs) - - if task is None: - # The default task factory in `asyncio` does not have its own function - # but is just a couple of lines in `asyncio.base_events.create_task()` - # Those lines are copied here. - - # WARNING: - # If the default behavior of the task creation in asyncio changes, - # this will break! - task = Task(coro, loop=loop, **kwargs) - if task._source_traceback: # type: ignore - del task._source_traceback[-1] # type: ignore - - return task - - -def patch_asyncio() -> None: - orig_task_factory = None - try: - loop = asyncio.get_running_loop() - orig_task_factory = loop.get_task_factory() - - # Check if already patched - if getattr(orig_task_factory, "_is_sentry_task_factory", False): - return - - def _sentry_task_factory( - loop: "asyncio.AbstractEventLoop", - coro: "Coroutine[Any, Any, Any]", - **kwargs: "Any", - ) -> "asyncio.Future[Any]": - # Check if this is an internal Sentry task - if is_internal_task(): - return _create_task_with_factory( - orig_task_factory, loop, coro, **kwargs - ) - - @_wrap_coroutine(coro) - async def _task_with_sentry_span_creation() -> "Any": - result = None - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncioIntegration) - task_spans = integration.task_spans if integration else False - - span_ctx: "Optional[Union[StreamedSpan, Span]]" = None - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope(): - if task_spans: - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=get_name(coro), - attributes={ - "sentry.op": OP.FUNCTION, - "sentry.origin": AsyncioIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.FUNCTION, - name=get_name(coro), - origin=AsyncioIntegration.origin, - ) - - with span_ctx if span_ctx else nullcontext(): - try: - result = await coro - except StopAsyncIteration as e: - raise e from None - except Exception: - reraise(*_capture_exception()) - - return result - - task = _create_task_with_factory( - orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs - ) - - # Set the task name to include the original coroutine's name - try: - task.set_name(f"{get_name(coro)} (Sentry-wrapped)") - except AttributeError: - # set_name might not be available in all Python versions - pass - - return task - - _sentry_task_factory._is_sentry_task_factory = True # type: ignore - loop.set_task_factory(_sentry_task_factory) # type: ignore - - except RuntimeError: - # When there is no running loop, we have nothing to patch. - logger.warning( - "There is no running asyncio loop so there is nothing Sentry can patch. " - "Please make sure you call sentry_sdk.init() within a running " - "asyncio loop for the AsyncioIntegration to work. " - "See https://docs.sentry.io/platforms/python/integrations/asyncio/" - ) - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - - integration = client.get_integration(AsyncioIntegration) - if integration is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "asyncio", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class AsyncioIntegration(Integration): - identifier = "asyncio" - origin = f"auto.function.{identifier}" - - def __init__(self, task_spans: bool = True) -> None: - self.task_spans = task_spans - - @staticmethod - def setup_once() -> None: - patch_asyncio() - patch_loop_close() - - -def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: - """ - Enable AsyncioIntegration with the provided options. - - This is useful in scenarios where Sentry needs to be initialized before - an event loop is set up, but you still want to instrument asyncio once there - is an event loop. In that case, you can sentry_sdk.init() early on without - the AsyncioIntegration and then, once the event loop has been set up, - execute: - - ```python - from sentry_sdk.integrations.asyncio import enable_asyncio_integration - - async def async_entrypoint(): - enable_asyncio_integration() - ``` - - Any arguments provided will be passed to AsyncioIntegration() as is. - - If AsyncioIntegration has already patched the current event loop, this - function won't have any effect. - - If AsyncioIntegration was provided in - sentry_sdk.init(disabled_integrations=[...]), this function will ignore that - and the integration will be enabled. - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # This function purposefully bypasses the integration machinery in - # integrations/__init__.py. _installed_integrations/_processed_integrations - # is used to prevent double patching the same module, but in the case of - # the AsyncioIntegration, we don't monkeypatch the standard library directly, - # we patch the currently running event loop, and we keep the record of doing - # that on the loop itself. - logger.debug("Setting up integration asyncio") - - integration = AsyncioIntegration(*args, **kwargs) - integration.setup_once() - - if "asyncio" not in client.integrations: - client.integrations["asyncio"] = integration diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py deleted file mode 100644 index 186176d268..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/asyncpg.py +++ /dev/null @@ -1,312 +0,0 @@ -from __future__ import annotations - -import contextlib -import re -from typing import Any, Awaitable, Callable, Iterator, TypeVar, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_version, -) - -try: - import asyncpg # type: ignore - from asyncpg.cursor import ( # type: ignore - BaseCursor, - Cursor, - CursorIterator, - ) - -except ImportError: - raise DidNotEnable("asyncpg not installed.") - - -class AsyncPGIntegration(Integration): - identifier = "asyncpg" - origin = f"auto.db.{identifier}" - _record_params = False - - def __init__(self, *, record_params: bool = False): - AsyncPGIntegration._record_params = record_params - - @staticmethod - def setup_once() -> None: - # asyncpg.__version__ is a string containing the semantic version in the form of ".." - asyncpg_version = parse_version(asyncpg.__version__) - _check_minimum_version(AsyncPGIntegration, asyncpg_version) - - asyncpg.Connection.execute = _wrap_execute( - asyncpg.Connection.execute, - ) - - asyncpg.Connection._execute = _wrap_connection_method( - asyncpg.Connection._execute - ) - asyncpg.Connection._executemany = _wrap_connection_method( - asyncpg.Connection._executemany, executemany=True - ) - asyncpg.Connection.prepare = _wrap_connection_method(asyncpg.Connection.prepare) - - BaseCursor._bind_exec = _wrap_cursor_method(BaseCursor._bind_exec) - BaseCursor._exec = _wrap_cursor_method(BaseCursor._exec) - - asyncpg.connect_utils._connect_addr = _wrap_connect_addr( - asyncpg.connect_utils._connect_addr - ) - - -T = TypeVar("T") - - -def _normalize_query(query: str) -> str: - return re.sub(r"\s+", " ", query).strip() - - -def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - # Avoid recording calls to _execute twice. - # Calls to Connection.execute with args also call - # Connection._execute, which is recorded separately - # args[0] = the connection object, args[1] is the query - if len(args) > 2: - return await f(*args, **kwargs) - - query = _normalize_query(args[1]) - with record_sql_queries( - cursor=None, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - span_origin=AsyncPGIntegration.origin, - ) as span: - res = await f(*args, **kwargs) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -SubCursor = TypeVar("SubCursor", bound=BaseCursor) - - -@contextlib.contextmanager -def _record( - cursor: "SubCursor | None", - query: str, - params_list: "tuple[Any, ...] | None", - *, - executemany: bool = False, -) -> "Iterator[Union[Span, StreamedSpan]]": - client = sentry_sdk.get_client() - integration = client.get_integration(AsyncPGIntegration) - if integration is not None and not integration._record_params: - params_list = None - - param_style = "pyformat" if params_list else None - - query = _normalize_query(query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=params_list, - paramstyle=param_style, - executemany=executemany, - record_cursor_repr=cursor is not None, - span_origin=AsyncPGIntegration.origin, - ) as span: - yield span - - -def _wrap_connection_method( - f: "Callable[..., Awaitable[T]]", *, executemany: bool = False -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - query = args[1] - params_list = args[2] if len(args) > 2 else None - with _record(None, query, params_list, executemany=executemany) as span: - _set_db_data(span, args[0]) - - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_cursor_method( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - cursor = args[0] - if type(cursor) is CursorIterator: - span_op_override_value = OP.DB_CURSOR_ITERATOR - elif type(cursor) is Cursor: - span_op_override_value = OP.DB_CURSOR_FETCH - else: - span_op_override_value = None - - query = _normalize_query(cursor._query) - with record_sql_queries( - cursor=cursor, - query=query, - params_list=None, - paramstyle=None, - executemany=False, - record_cursor_repr=True, - span_origin=AsyncPGIntegration.origin, - span_op_override_value=span_op_override_value, - ) as span: - _set_db_data(span, cursor._connection) - res = await f(*args, **kwargs) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return res - - return _inner - - -def _wrap_connect_addr( - f: "Callable[..., Awaitable[T]]", -) -> "Callable[..., Awaitable[T]]": - async def _inner(*args: "Any", **kwargs: "Any") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(AsyncPGIntegration) is None: - return await f(*args, **kwargs) - - user = kwargs["params"].user - database = kwargs["params"].database - addr = kwargs.get("addr") - - if has_span_streaming_enabled(client.options): - span_attributes = { - "sentry.op": OP.DB, - "sentry.origin": AsyncPGIntegration.origin, - SPANDATA.DB_SYSTEM_NAME: "postgresql", - SPANDATA.DB_USER: user, - SPANDATA.DB_NAMESPACE: database, - SPANDATA.DB_DRIVER_NAME: "asyncpg", - } - if addr: - try: - span_attributes[SPANDATA.SERVER_ADDRESS] = addr[0] - span_attributes[SPANDATA.SERVER_PORT] = addr[1] - except IndexError: - pass - - with sentry_sdk.traces.start_span( - name="connect", attributes=span_attributes - ) as span: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span_attributes - ) - res = await f(*args, **kwargs) - - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=AsyncPGIntegration.origin, - ) as span: - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - span.set_data(SPANDATA.DB_NAME, database) - span.set_data(SPANDATA.DB_USER, user) - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message="connect", category="query", data=span._data - ) - res = await f(*args, **kwargs) - - return res - - return _inner - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - addr = conn._addr - database = conn._params.database - user = conn._params.user - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "postgresql") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "asyncpg") - if addr: - try: - span.set_attribute(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_attribute(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_attribute(SPANDATA.DB_NAMESPACE, database) - - if user: - span.set_attribute(SPANDATA.DB_USER, user) - else: - # Remove this else block once we've completely migrated to streamed spans - # The use of deprecated attributes here is to ensure backwards compatibility - span.set_data(SPANDATA.DB_SYSTEM, "postgresql") - span.set_data(SPANDATA.DB_DRIVER_NAME, "asyncpg") - - if addr: - try: - span.set_data(SPANDATA.SERVER_ADDRESS, addr[0]) - span.set_data(SPANDATA.SERVER_PORT, addr[1]) - except IndexError: - pass - - if database: - span.set_data(SPANDATA.DB_NAME, database) - - if user: - span.set_data(SPANDATA.DB_USER, user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py deleted file mode 100644 index b573e76f09..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/atexit.py +++ /dev/null @@ -1,51 +0,0 @@ -import atexit -import os -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Optional - - -def default_callback(pending: int, timeout: int) -> None: - """This is the default shutdown callback that is set on the options. - It prints out a message to stderr that informs the user that some events - are still pending and the process is waiting for them to flush out. - """ - - def echo(msg: str) -> None: - sys.stderr.write(msg + "\n") - - echo("Sentry is attempting to send %i pending events" % pending) - echo("Waiting up to %s seconds" % timeout) - echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C")) - sys.stderr.flush() - - -class AtexitIntegration(Integration): - identifier = "atexit" - - def __init__(self, callback: "Optional[Any]" = None) -> None: - if callback is None: - callback = default_callback - self.callback = callback - - @staticmethod - def setup_once() -> None: - @atexit.register - def _shutdown() -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(AtexitIntegration) - - if integration is None: - return - - logger.debug("atexit: got shutdown signal") - logger.debug("atexit: shutting down client") - sentry_sdk.get_isolation_scope().end_session() - - client.close(callback=integration.callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py deleted file mode 100644 index c7fe77714a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/aws_lambda.py +++ /dev/null @@ -1,542 +0,0 @@ -import functools -import json -import re -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING -from urllib.parse import urlencode - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.cloud_resource_context import ( - CLOUD_PLATFORM, - CLOUD_PROVIDER, -) -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - -# Constants -TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - - -def _wrap_init_error(init_error: "F") -> "F": - @ensure_integration_enabled(AwsLambdaIntegration, init_error) - def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - with capture_internal_exceptions(): - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - exc_info = sys.exc_info() - if exc_info and all(exc_info): - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - - else: - # Fall back to AWS lambdas JSON representation of the error - error_info = args[1] - if isinstance(error_info, str): - error_info = json.loads(error_info) - sentry_event = _event_from_error_json(error_info) - sentry_sdk.capture_event(sentry_event) - - return init_error(*args, **kwargs) - - return sentry_init_error # type: ignore - - -def _wrap_handler(handler: "F") -> "F": - @functools.wraps(handler) - def sentry_handler( - aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, - # `event` here is *likely* a dictionary, but also might be a number of - # other types (str, int, float, None). - # - # In some cases, it is a list (if the user is batch-invoking their - # function, for example), in which case we'll use the first entry as a - # representative from which to try pulling request data. (Presumably it - # will be the same for all events in the list, since they're all hitting - # the lambda in the same request.) - - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - - if integration is None: - return handler(aws_event, aws_context, *args, **kwargs) - - if isinstance(aws_event, list) and len(aws_event) >= 1: - request_data = aws_event[0] - batch_size = len(aws_event) - else: - request_data = aws_event - batch_size = 1 - - if not isinstance(request_data, dict): - # If we're not dealing with a dictionary, we won't be able to get - # headers, path, http method, etc in any case, so it's fine that - # this is empty - request_data = {} - - configured_time = aws_context.get_remaining_time_in_millis() - aws_region = aws_context.invoked_function_arn.split(":")[3] - - with sentry_sdk.isolation_scope() as scope: - timeout_thread = None - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - request_data, aws_context, configured_time - ) - ) - scope.set_tag("aws_region", aws_region) - if batch_size > 1: - scope.set_tag("batch_request", True) - scope.set_tag("batch_size", batch_size) - - # Starting the Timeout thread only if the configured time is greater than Timeout warning - # buffer and timeout_warning parameter is set True. - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = ( - configured_time - TIMEOUT_WARNING_BUFFER - ) / MILLIS_TO_SECONDS - - timeout_thread = TimeoutThread( - waiting_time, - configured_time / MILLIS_TO_SECONDS, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = request_data.get("headers", {}) - # Some AWS Services (ie. EventBridge) set headers as a list - # or None, so we must ensure it is a dict - if not isinstance(headers, dict): - headers = {} - - header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - additional_attributes: "dict[str, Any]" = {} - if "httpMethod" in request_data: - additional_attributes["http.request.method"] = request_data[ - "httpMethod" - ] - - if should_send_default_pii() and "queryStringParameters" in request_data: - qs = request_data["queryStringParameters"] - if qs: - additional_attributes["url.query"] = urlencode(qs) - - sampling_context = { - "aws_event": aws_event, - "aws_context": aws_context, - } - - function_name = aws_context.function_name - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_AWS, - "sentry.origin": AwsLambdaIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.region": aws_region, - "cloud.resource_id": aws_context.invoked_function_arn, - "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, - "cloud.provider": CLOUD_PROVIDER.AWS, - "faas.name": function_name, - "faas.invocation_id": aws_context.aws_request_id, - "faas.version": aws_context.function_version, - "aws.lambda.invoked_arn": aws_context.invoked_function_arn, - "aws.log.group.names": [aws_context.log_group_name], - "aws.log.stream.names": [aws_context.log_stream_name], - "messaging.batch.message_count": batch_size, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_AWS, - name=function_name, - source=TransactionSource.COMPONENT, - origin=AwsLambdaIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return handler(aws_event, aws_context, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - - return sentry_handler # type: ignore - - -def _drain_queue() -> None: - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - if integration is not None: - # Flush out the event queue before AWS kills the - # process. - client.flush() - - -class AwsLambdaIntegration(Integration): - identifier = "aws_lambda" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - lambda_bootstrap = get_lambda_bootstrap() - if not lambda_bootstrap: - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find bootstrap module)" - ) - return - - if not hasattr(lambda_bootstrap, "handle_event_request"): - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find handle_event_request)" - ) - return - - pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 - - if pre_37: - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_event_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - old_handle_http_request = lambda_bootstrap.handle_http_request - - def sentry_handle_http_request( - request_handler: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request_handler = _wrap_handler(request_handler) - return old_handle_http_request(request_handler, *args, **kwargs) - - lambda_bootstrap.handle_http_request = sentry_handle_http_request - - # Patch to_json to drain the queue. This should work even when the - # SDK is initialized inside of the handler - - old_to_json = lambda_bootstrap.to_json - - def sentry_to_json(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return old_to_json(*args, **kwargs) - - lambda_bootstrap.to_json = sentry_to_json - else: - lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( - lambda_bootstrap.LambdaRuntimeClient.post_init_error - ) - - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( # type: ignore - lambda_runtime_client, request_handler, *args, **kwargs - ): - request_handler = _wrap_handler(request_handler) - return old_handle_event_request( - lambda_runtime_client, request_handler, *args, **kwargs - ) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - # Patch the runtime client to drain the queue. This should work - # even when the SDK is initialized inside of the handler - - def _wrap_post_function(f: "F") -> "F": - def inner(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return f(*args, **kwargs) - - return inner # type: ignore - - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result - ) - ) - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error - ) - ) - - -def get_lambda_bootstrap() -> "Optional[Any]": - # Python 3.7: If the bootstrap module is *already imported*, it is the - # one we actually want to use (no idea what's in __main__) - # - # Python 3.8: bootstrap is also importable, but will be the same file - # as __main__ imported under a different name: - # - # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ - # sys.modules['__main__'] is not sys.modules['bootstrap'] - # - # Python 3.9: bootstrap is in __main__.awslambdaricmain - # - # On container builds using the `aws-lambda-python-runtime-interface-client` - # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap - # - # Such a setup would then make all monkeypatches useless. - if "bootstrap" in sys.modules: - return sys.modules["bootstrap"] - elif "__main__" in sys.modules: - module = sys.modules["__main__"] - # python3.9 runtime - if hasattr(module, "awslambdaricmain") and hasattr( - module.awslambdaricmain, "bootstrap" - ): - return module.awslambdaricmain.bootstrap - elif hasattr(module, "bootstrap"): - # awslambdaric python module in container builds - return module.bootstrap - - # python3.8 runtime - return module - else: - return None - - -def _make_request_event_processor( - aws_event: "Any", aws_context: "Any", configured_timeout: "Any" -) -> "EventProcessor": - start_time = datetime.now(timezone.utc) - - def event_processor( - sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time - ) -> "Optional[Event]": - remaining_time_in_milis = aws_context.get_remaining_time_in_millis() - exec_duration = configured_timeout - remaining_time_in_milis - - extra = sentry_event.setdefault("extra", {}) - extra["lambda"] = { - "function_name": aws_context.function_name, - "function_version": aws_context.function_version, - "invoked_function_arn": aws_context.invoked_function_arn, - "aws_request_id": aws_context.aws_request_id, - "execution_duration_in_millis": exec_duration, - "remaining_time_in_millis": remaining_time_in_milis, - } - - extra["cloudwatch logs"] = { - "url": _get_cloudwatch_logs_url(aws_context, start_time), - "log_group": aws_context.log_group_name, - "log_stream": aws_context.log_stream_name, - } - - request = sentry_event.get("request", {}) - - if "httpMethod" in aws_event: - request["method"] = aws_event["httpMethod"] - - request["url"] = _get_url(aws_event, aws_context) - - if "queryStringParameters" in aws_event: - request["query_string"] = aws_event["queryStringParameters"] - - if "headers" in aws_event: - request["headers"] = _filter_headers(aws_event["headers"]) - - if should_send_default_pii(): - user_info = sentry_event.setdefault("user", {}) - - identity = aws_event.get("identity") - if identity is None: - identity = {} - - id = identity.get("userArn") - if id is not None: - user_info.setdefault("id", id) - - ip = identity.get("sourceIp") - if ip is not None: - user_info.setdefault("ip_address", ip) - - if "body" in aws_event: - request["data"] = aws_event.get("body", "") - else: - if aws_event.get("body", None): - # Unfortunately couldn't find a way to get structured body from AWS - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - sentry_event["request"] = deepcopy(request) - - return sentry_event - - return event_processor - - -def _get_url(aws_event: "Any", aws_context: "Any") -> str: - path = aws_event.get("path", None) - - headers = aws_event.get("headers") - if headers is None: - headers = {} - - host = headers.get("Host", None) - proto = headers.get("X-Forwarded-Proto", None) - if proto and host and path: - return "{}://{}{}".format(proto, host, path) - return "awslambda:///{}".format(aws_context.function_name) - - -def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: - """ - Generates a CloudWatchLogs console URL based on the context object - - Arguments: - aws_context {Any} -- context from lambda handler - - Returns: - str -- AWS Console URL to logs. - """ - formatstring = "%Y-%m-%dT%H:%M:%SZ" - region = environ.get("AWS_REGION", "") - - url = ( - "https://console.{domain}/cloudwatch/home?region={region}" - "#logEventViewer:group={log_group};stream={log_stream}" - ";start={start_time};end={end_time}" - ).format( - domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", - region=region, - log_group=aws_context.log_group_name, - log_stream=aws_context.log_stream_name, - start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), - end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( - formatstring - ), - ) - - return url - - -def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": - frames = [] - for frame in formatted_tb: - match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) - if match: - file_name, line_number, func_name = match.groups() - line_number = int(line_number) - frames.append( - { - "filename": file_name, - "function": func_name, - "lineno": line_number, - "vars": None, - "pre_context": None, - "context_line": None, - "post_context": None, - } - ) - return frames - - -def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": - """ - Converts the error JSON from AWS Lambda into a Sentry error event. - This is not a full fletched event, but better than nothing. - - This is an example of where AWS creates the error JSON: - https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 - """ - event: "Event" = { - "level": "error", - "exception": { - "values": [ - { - "type": error_json.get("errorType"), - "value": error_json.get("errorMessage"), - "stacktrace": { - "frames": _parse_formatted_traceback( - error_json.get("stackTrace", []) - ), - }, - "mechanism": { - "type": "aws_lambda", - "handled": False, - }, - } - ], - }, - } - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py deleted file mode 100644 index 31f45f73de..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/beam.py +++ /dev/null @@ -1,164 +0,0 @@ -import sys -import types -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, TypeVar - - from sentry_sdk._types import ExcInfo - - T = TypeVar("T") - F = TypeVar("F", bound=Callable[..., Any]) - - -WRAPPED_FUNC = "_wrapped_{}_" -INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py -USED_FUNC = "_sentry_used_" - - -class BeamIntegration(Integration): - identifier = "beam" - - @staticmethod - def setup_once() -> None: - from apache_beam.transforms.core import DoFn, ParDo # type: ignore - - ignore_logger("root") - ignore_logger("bundle_processor.create") - - function_patches = ["process", "start_bundle", "finish_bundle", "setup"] - for func_name in function_patches: - setattr( - DoFn, - INSPECT_FUNC.format(func_name), - _wrap_inspect_call(DoFn, func_name), - ) - - old_init = ParDo.__init__ - - def sentry_init_pardo( - self: "ParDo", fn: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Do not monkey patch init twice - if not getattr(self, "_sentry_is_patched", False): - for func_name in function_patches: - if not hasattr(fn, func_name): - continue - wrapped_func = WRAPPED_FUNC.format(func_name) - - # Check to see if inspect is set and process is not - # to avoid monkey patching process twice. - # Check to see if function is part of object for - # backwards compatibility. - process_func = getattr(fn, func_name) - inspect_func = getattr(fn, INSPECT_FUNC.format(func_name)) - if not getattr(inspect_func, USED_FUNC, False) and not getattr( - process_func, USED_FUNC, False - ): - setattr(fn, wrapped_func, process_func) - setattr(fn, func_name, _wrap_task_call(process_func)) - - self._sentry_is_patched = True - old_init(self, fn, *args, **kwargs) - - ParDo.__init__ = sentry_init_pardo - - -def _wrap_inspect_call(cls: "Any", func_name: "Any") -> "Any": - if not hasattr(cls, func_name): - return None - - def _inspect(self: "Any") -> "Any": - """ - Inspect function overrides the way Beam gets argspec. - """ - wrapped_func = WRAPPED_FUNC.format(func_name) - if hasattr(self, wrapped_func): - process_func = getattr(self, wrapped_func) - else: - process_func = getattr(self, func_name) - setattr(self, func_name, _wrap_task_call(process_func)) - setattr(self, wrapped_func, process_func) - - # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults - # (which uses Signatures internally) should be used instead. - try: - from apache_beam.transforms.core import get_function_args_defaults - - return get_function_args_defaults(process_func) - except ImportError: - from apache_beam.typehints.decorators import getfullargspec # type: ignore - - return getfullargspec(process_func) - - setattr(_inspect, USED_FUNC, True) - return _inspect - - -def _wrap_task_call(func: "F") -> "F": - """ - Wrap task call with a try catch to get exceptions. - """ - - @wraps(func) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - try: - gen = func(*args, **kwargs) - except Exception: - raise_exception() - - if not isinstance(gen, types.GeneratorType): - return gen - return _wrap_generator_call(gen) - - setattr(_inner, USED_FUNC, True) - return _inner # type: ignore - - -@ensure_integration_enabled(BeamIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - """ - Send Beam exception to Sentry. - """ - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "beam", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def raise_exception() -> None: - """ - Raise an exception. - """ - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) - reraise(*exc_info) - - -def _wrap_generator_call(gen: "Iterator[T]") -> "Iterator[T]": - """ - Wrap the generator to handle any failures. - """ - while True: - try: - yield next(gen) - except StopIteration: - break - except Exception: - raise_exception() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py deleted file mode 100644 index a7fdd99b21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/boto3.py +++ /dev/null @@ -1,187 +0,0 @@ -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - parse_url, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Type, Union - - from botocore.model import ServiceId - -try: - from botocore import __version__ as BOTOCORE_VERSION - from botocore.awsrequest import AWSRequest - from botocore.client import BaseClient - from botocore.response import StreamingBody -except ImportError: - raise DidNotEnable("botocore is not installed") - - -class Boto3Integration(Integration): - identifier = "boto3" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTOCORE_VERSION) - _check_minimum_version(Boto3Integration, version, "botocore") - - orig_init = BaseClient.__init__ - - def sentry_patched_init( - self: "BaseClient", *args: "Any", **kwargs: "Any" - ) -> None: - orig_init(self, *args, **kwargs) - meta = self.meta - service_id = meta.service_model.service_id - meta.events.register( - "request-created", - partial(_sentry_request_created, service_id=service_id), - ) - meta.events.register("after-call", _sentry_after_call) - meta.events.register("after-call-error", _sentry_after_call_error) - - BaseClient.__init__ = sentry_patched_init # type: ignore - - -def _sentry_request_created( - service_id: "ServiceId", request: "AWSRequest", operation_name: str, **kwargs: "Any" -) -> None: - description = "aws.%s.%s" % (service_id.hyphenize(), operation_name) - - client = sentry_sdk.get_client() - if client.get_integration(Boto3Integration) is None: - return - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Boto3Integration.origin, - SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}", - }, - ) - if request.url is not None and should_send_default_pii(): - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if request.method is not None: - span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method) - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name=description, - origin=Boto3Integration.origin, - ) - - if request.url is not None: - with capture_internal_exceptions(): - parsed_url = parse_url(request.url, sanitize=False) - span.set_data("aws.request.url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - span.set_tag("aws.service_id", service_id.hyphenize()) - span.set_tag("aws.operation_name", operation_name) - if request.method is not None: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - - # We do it in order for subsequent http calls/retries be - # attached to this span. - span.__enter__() - - # request.context is an open-ended data-structure - # where we can add anything useful in request life cycle. - request.context["_sentrysdk_span"] = span - - -def _sentry_after_call( - context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(None, None, None) - - body = parsed.get("Body") - if not isinstance(body, StreamingBody): - return - - streaming_span: "Union[Span, StreamedSpan]" - if isinstance(span, StreamedSpan): - streaming_span = sentry_sdk.traces.start_span( - name=span.name, - parent_span=span, - attributes={ - "sentry.op": OP.HTTP_CLIENT_STREAM, - "sentry.origin": Boto3Integration.origin, - }, - ) - else: - streaming_span = span.start_child( - op=OP.HTTP_CLIENT_STREAM, - name=span.description, - origin=Boto3Integration.origin, - ) - - orig_read = body.read - orig_close = body.close - - def sentry_streaming_body_read(*args: "Any", **kwargs: "Any") -> bytes: - try: - ret = orig_read(*args, **kwargs) - if ret: - return ret - - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - return ret - except Exception: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - raise - - body.read = sentry_streaming_body_read # type: ignore - - def sentry_streaming_body_close(*args: "Any", **kwargs: "Any") -> None: - if isinstance(streaming_span, StreamedSpan): - streaming_span.end() - else: - streaming_span.finish() - orig_close(*args, **kwargs) - - body.close = sentry_streaming_body_close # type: ignore - - -def _sentry_after_call_error( - context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any" -) -> None: - span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) - - # Span could be absent if the integration is disabled. - if span is None: - return - span.__exit__(type(exception), exception, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py deleted file mode 100644 index 50f6ca2e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/bottle.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, - _check_minimum_version, -) -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from collections.abc import Set - from typing import Any, Callable, Dict, Optional - - from bottle import FileUpload, FormsDict, LocalRequest # type: ignore - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - -try: - from bottle import ( - Bottle, - HTTPResponse, - Route, - ) - from bottle import ( - __version__ as BOTTLE_VERSION, - ) - from bottle import ( - request as bottle_request, - ) -except ImportError: - raise DidNotEnable("Bottle not installed") - - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class BottleIntegration(Integration): - identifier = "bottle" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - *, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - version = parse_version(BOTTLE_VERSION) - _check_minimum_version(BottleIntegration, version) - - old_app = Bottle.__call__ - - @ensure_integration_enabled(BottleIntegration, old_app) - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=BottleIntegration.origin, - ) - - return middleware(environ, start_response) - - Bottle.__call__ = sentry_patched_wsgi_app - - old_handle = Bottle._handle - - @functools.wraps(old_handle) - def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return old_handle(self, environ) - - scope = sentry_sdk.get_isolation_scope() - scope._name = "bottle" - scope.add_event_processor( - _make_request_event_processor(self, bottle_request, integration) - ) - res = old_handle(self, environ) - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - _set_segment_name_and_source( - transaction_style=integration.transaction_style - ) - - return res - - Bottle._handle = _patched_handle - - old_make_callback = Route._make_callback - - @functools.wraps(old_make_callback) - def patched_make_callback( - self: "Route", *args: object, **kwargs: object - ) -> "Any": - prepared_callback = old_make_callback(self, *args, **kwargs) - - integration = sentry_sdk.get_client().get_integration(BottleIntegration) - if integration is None: - return prepared_callback - - def wrapped_callback(*args: object, **kwargs: object) -> "Any": - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) - - return res - - return wrapped_callback - - Route._make_callback = patched_make_callback - - -class BottleRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[str, str]": - return self.request.cookies - - def raw_data(self) -> bytes: - return self.request.body.read() - - def form(self) -> "FormsDict": - if self.is_json(): - return None - return self.request.forms.decode() - - def files(self) -> "Optional[Dict[str, str]]": - if self.is_json(): - return None - - return self.request.files - - def size_of_file(self, file: "FileUpload") -> int: - return file.content_length - - -def _set_segment_name_and_source(transaction_style: str) -> None: - try: - if transaction_style == "url": - name = bottle_request.route.rule or "bottle request" - else: - name = ( - bottle_request.route.name - or transaction_from_function(bottle_request.route.callback) - or "bottle request" - ) - - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], - ) - except RuntimeError: - pass - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "url": - try: - name = request.route.rule or "" - except RuntimeError: - pass - - elif transaction_style == "endpoint": - try: - name = ( - request.route.name - or transaction_from_function(request.route.callback) - or "" - ) - except RuntimeError: - pass - - event["transaction"] = name - event["transaction_info"] = { - "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - } - - -def _make_request_event_processor( - app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, request) - - with capture_internal_exceptions(): - BottleRequestExtractor(request).extract_into_event(event) - - return event - - return event_processor - - -def _capture_exception(exception: BaseException, handled: bool) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "bottle", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py deleted file mode 100644 index 532b13539b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/__init__.py +++ /dev/null @@ -1,612 +0,0 @@ -import sys -from collections.abc import Mapping -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import isolation_scope -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.celery.beat import ( - _patch_beat_apply_entry, - _patch_redbeat_apply_async, - _setup_celery_beat_signals, -) -from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource -from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, List, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import VERSION as CELERY_VERSION # type: ignore - from celery.app.task import Task # type: ignore - from celery.app.trace import task_has_custom - from celery.exceptions import ( # type: ignore - Ignore, - Reject, - Retry, - SoftTimeLimitExceeded, - ) - from kombu import Producer # type: ignore -except ImportError: - raise DidNotEnable("Celery not installed") - - -CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) - - -class CeleryIntegration(Integration): - identifier = "celery" - origin = f"auto.queue.{identifier}" - - def __init__( - self, - propagate_traces: bool = True, - monitor_beat_tasks: bool = False, - exclude_beat_tasks: "Optional[List[str]]" = None, - ) -> None: - self.propagate_traces = propagate_traces - self.monitor_beat_tasks = monitor_beat_tasks - self.exclude_beat_tasks = exclude_beat_tasks - - _patch_beat_apply_entry() - _patch_redbeat_apply_async() - _setup_celery_beat_signals(monitor_beat_tasks) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(CeleryIntegration, CELERY_VERSION) - - _patch_build_tracer() - _patch_task_apply_async() - _patch_celery_send_task() - _patch_worker_exit() - _patch_producer_publish() - - # This logger logs every status of every task that ran on the worker. - # Meaning that every task's breadcrumbs are full of stuff like "Task - # raised unexpected ". - ignore_logger("celery.worker.job") - ignore_logger("celery.app.trace") - - # This is stdout/err redirected to a logger, can't deal with this - # (need event_level=logging.WARN to reproduce) - ignore_logger("celery.redirected") - - -def _set_status(status: str) -> None: - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - - if span_streaming and scope.streamed_span is not None: - scope.streamed_span.status = "ok" if status == "ok" else "error" - elif not span_streaming and scope.span is not None: - scope.span.set_status(status) - - -def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return - - if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS): - # ??? Doesn't map to anything - _set_status("aborted") - return - - _set_status("internal_error") - - if hasattr(task, "throws") and isinstance(exc_info[1], task.throws): - return - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "celery", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - task: "Any", - uuid: "Any", - args: "Any", - kwargs: "Any", - request: "Optional[Any]" = None, -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["celery_task_id"] = uuid - extra = event.setdefault("extra", {}) - extra["celery-job"] = { - "task_name": task.name, - "args": ( - args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE - ), - } - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded): - event["fingerprint"] = [ - "celery", - "SoftTimeLimitExceeded", - getattr(task, "name", task), - ] - - return event - - return event_processor - - -def _update_celery_task_headers( - original_headers: "dict[str, Any]", - span: "Optional[Union[StreamedSpan, Span]]", - monitor_beat_tasks: bool, -) -> "dict[str, Any]": - """ - Updates the headers of the Celery task with the tracing information - and eventually Sentry Crons monitoring information for beat tasks. - """ - updated_headers = original_headers.copy() - with capture_internal_exceptions(): - # if span is None (when the task was started by Celery Beat) - # this will return the trace headers from the scope. - headers = dict( - sentry_sdk.get_isolation_scope().iter_trace_propagation_headers(span=span) - ) - - if monitor_beat_tasks: - headers.update( - { - "sentry-monitor-start-timestamp-s": "%.9f" - % _now_seconds_since_epoch(), - } - ) - - # Add the time the task was enqueued to the headers - # This is used in the consumer to calculate the latency - updated_headers.update( - {"sentry-task-enqueued-time": _now_seconds_since_epoch()} - ) - - if headers: - existing_baggage = updated_headers.get(BAGGAGE_HEADER_NAME) - sentry_baggage = headers.get(BAGGAGE_HEADER_NAME) - - combined_baggage = sentry_baggage or existing_baggage - if sentry_baggage and existing_baggage: - # Merge incoming and sentry baggage, where the sentry trace information - # in the incoming baggage takes precedence and the third-party items - # are concatenated. - incoming = Baggage.from_incoming_header(existing_baggage) - combined = Baggage.from_incoming_header(sentry_baggage) - combined.sentry_items.update(incoming.sentry_items) - combined.third_party_items = ",".join( - [ - x - for x in [ - combined.third_party_items, - incoming.third_party_items, - ] - if x is not None and x != "" - ] - ) - combined_baggage = combined.serialize(include_third_party=True) - - updated_headers.update(headers) - if combined_baggage: - updated_headers[BAGGAGE_HEADER_NAME] = combined_baggage - - # https://github.com/celery/celery/issues/4875 - # - # Need to setdefault the inner headers too since other - # tracing tools (dd-trace-py) also employ this exact - # workaround and we don't want to break them. - updated_headers.setdefault("headers", {}).update(headers) - if combined_baggage: - updated_headers["headers"][BAGGAGE_HEADER_NAME] = combined_baggage - - # Add the Sentry options potentially added in `sentry_apply_entry` - # to the headers (done when auto-instrumenting Celery Beat tasks) - for key, value in updated_headers.items(): - if key.startswith("sentry-"): - updated_headers["headers"][key] = value - - # Preserve user-provided custom headers in the inner "headers" dict - # so they survive to task.request.headers on the worker (celery#4875). - for key, value in original_headers.items(): - if key != "headers" and key not in updated_headers["headers"]: - updated_headers["headers"][key] = value - - return updated_headers - - -class NoOpMgr: - def __enter__(self) -> None: - return None - - def __exit__(self, exc_type: "Any", exc_value: "Any", traceback: "Any") -> None: - return None - - -def _wrap_task_run(f: "F") -> "F": - @wraps(f) - def apply_async(*args: "Any", **kwargs: "Any") -> "Any": - # Note: kwargs can contain headers=None, so no setdefault! - # Unsure which backend though. - client = sentry_sdk.get_client() - integration = client.get_integration(CeleryIntegration) - if integration is None: - return f(*args, **kwargs) - - kwarg_headers = kwargs.get("headers") or {} - propagate_traces = kwarg_headers.pop( - "sentry-propagate-traces", integration.propagate_traces - ) - - if not propagate_traces: - return f(*args, **kwargs) - - if isinstance(args[0], Task): - task_name: str = args[0].name - elif len(args) > 1 and isinstance(args[1], str): - task_name = args[1] - else: - task_name = "" - - span_streaming = has_span_streaming_enabled(client.options) - - task_started_from_beat = sentry_sdk.get_isolation_scope()._name == "celery-beat" - - span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - if span_streaming: - if not task_started_from_beat and get_current_span() is not None: - span_mgr = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_CELERY, - "sentry.origin": CeleryIntegration.origin, - }, - ) - - else: - if not task_started_from_beat: - span_mgr = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_CELERY, - name=task_name, - origin=CeleryIntegration.origin, - ) - - with span_mgr as span: - kwargs["headers"] = _update_celery_task_headers( - kwarg_headers, span, integration.monitor_beat_tasks - ) - return f(*args, **kwargs) - - return apply_async # type: ignore - - -def _wrap_tracer(task: "Any", f: "F") -> "F": - # Need to wrap tracer for pushing the scope before prerun is sent, and - # popping it after postrun is sent. - # - # This is the reason we don't use signals for hooking in the first place. - # Also because in Celery 3, signal dispatch returns early if one handler - # crashes. - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - with isolation_scope() as scope: - scope._name = "celery" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - - task_name = getattr(task, "name", "") - - custom_sampling_context = {} - with capture_internal_exceptions(): - custom_sampling_context = { - "celery_job": { - "task": task_name, - # for some reason, args[1] is a list if non-empty but a - # tuple if empty - "args": list(args[1]), - "kwargs": args[2], - } - } - - span: "Union[Span, StreamedSpan]" - span_ctx: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr() - - # Celery task objects are not a thing to be trusted. Even - # something such as attribute access can fail. - with capture_internal_exceptions(): - headers = args[3].get("headers") or {} - if span_streaming: - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(custom_sampling_context) - span = sentry_sdk.traces.start_span( - name=task_name, - parent_span=None, # make this a segment - attributes={ - "sentry.origin": CeleryIntegration.origin, - "sentry.span.source": TransactionSource.TASK.value, - "sentry.op": OP.QUEUE_TASK_CELERY, - }, - ) - - span_ctx = span - - else: - span = continue_trace( - headers, - op=OP.QUEUE_TASK_CELERY, - name=task_name, - source=TransactionSource.TASK, - origin=CeleryIntegration.origin, - ) - span.set_status(SPANSTATUS.OK) - - span_ctx = sentry_sdk.start_transaction( - span, - custom_sampling_context=custom_sampling_context, - ) - - with span_ctx: - return f(*args, **kwargs) - - return _inner # type: ignore - - -def _set_messaging_destination_name( - task: "Any", span: "Union[StreamedSpan, Span]" -) -> None: - """Set "messaging.destination.name" tag for span""" - with capture_internal_exceptions(): - delivery_info = task.request.delivery_info - if delivery_info: - routing_key = delivery_info.get("routing_key") - if delivery_info.get("exchange") == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning the tasks - # are sent to the queue with the same name as the routing key. - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - else: - span.set_data(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - -def _wrap_task_call(task: "Any", f: "F") -> "F": - # Need to wrap task call because the exception is caught before we get to - # see it. Also celery's reported stacktrace is untrustworthy. - - @wraps(f) - def _inner(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return f(*args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - try: - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_PROCESS, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PROCESS, - name=task.name, - origin=CeleryIntegration.origin, - ) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - _set_messaging_destination_name(task, span) - - latency = None - with capture_internal_exceptions(): - if ( - task.request.headers is not None - and "sentry-task-enqueued-time" in task.request.headers - ): - latency = _now_seconds_since_epoch() - task.request.headers.pop( - "sentry-task-enqueued-time" - ) - - if latency is not None: - latency *= 1000 # milliseconds - set_on_span(SPANDATA.MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) - - with capture_internal_exceptions(): - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task.request.id) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, task.request.retries - ) - - with capture_internal_exceptions(): - with task.app.connection() as conn: - set_on_span( - SPANDATA.MESSAGING_SYSTEM, - conn.transport.driver_type, - ) - - return f(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(task, exc_info) - reraise(*exc_info) - - return _inner # type: ignore - - -def _patch_build_tracer() -> None: - import celery.app.trace as trace # type: ignore - - original_build_tracer = trace.build_tracer - - def sentry_build_tracer( - name: "Any", task: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not getattr(task, "_sentry_is_patched", False): - # determine whether Celery will use __call__ or run and patch - # accordingly - if task_has_custom(task, "__call__"): - type(task).__call__ = _wrap_task_call(task, type(task).__call__) - else: - task.run = _wrap_task_call(task, task.run) - - # `build_tracer` is apparently called for every task - # invocation. Can't wrap every celery task for every invocation - # or we will get infinitely nested wrapper functions. - task._sentry_is_patched = True - - return _wrap_tracer(task, original_build_tracer(name, task, *args, **kwargs)) - - trace.build_tracer = sentry_build_tracer - - -def _patch_task_apply_async() -> None: - Task.apply_async = _wrap_task_run(Task.apply_async) - - -def _patch_celery_send_task() -> None: - from celery import Celery - - Celery.send_task = _wrap_task_run(Celery.send_task) - - -def _patch_worker_exit() -> None: - # Need to flush queue before worker shutdown because a crashing worker will - # call os._exit - from billiard.pool import Worker # type: ignore - - original_workloop = Worker.workloop - - def sentry_workloop(*args: "Any", **kwargs: "Any") -> "Any": - try: - return original_workloop(*args, **kwargs) - finally: - with capture_internal_exceptions(): - if ( - sentry_sdk.get_client().get_integration(CeleryIntegration) - is not None - ): - sentry_sdk.flush() - - Worker.workloop = sentry_workloop - - -def _patch_producer_publish() -> None: - original_publish = Producer.publish - - def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(CeleryIntegration) is None: - return original_publish(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - kwargs_headers = kwargs.get("headers", {}) - if not isinstance(kwargs_headers, Mapping): - # Ensure kwargs_headers is a Mapping, so we can safely call get(). - # We don't expect this to happen, but it's better to be safe. Even - # if it does happen, only our instrumentation breaks. This line - # does not overwrite kwargs["headers"], so the original publish - # method will still work. - kwargs_headers = {} - - task_name = kwargs_headers.get("task") or "" - task_id = kwargs_headers.get("id") - retries = kwargs_headers.get("retries") - - routing_key = kwargs.get("routing_key") - exchange = kwargs.get("exchange") - - span: "Union[StreamedSpan, Span, None]" = None - if span_streaming: - if get_current_span() is not None: - span = sentry_sdk.traces.start_span( - name=task_name, - attributes={ - "sentry.op": OP.QUEUE_PUBLISH, - "sentry.origin": CeleryIntegration.origin, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.QUEUE_PUBLISH, - name=task_name, - origin=CeleryIntegration.origin, - ) - - if span is None: - return original_publish(self, *args, **kwargs) - - with span: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - if task_id is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_ID, task_id) - - if exchange == "" and routing_key is not None: - # Empty exchange indicates the default exchange, meaning messages are - # routed to the queue with the same name as the routing key. - set_on_span(SPANDATA.MESSAGING_DESTINATION_NAME, routing_key) - - if retries is not None: - set_on_span(SPANDATA.MESSAGING_MESSAGE_RETRY_COUNT, retries) - - with capture_internal_exceptions(): - set_on_span( - SPANDATA.MESSAGING_SYSTEM, self.connection.transport.driver_type - ) - - return original_publish(self, *args, **kwargs) - - Producer.publish = sentry_publish diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py deleted file mode 100644 index b5027d212a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/beat.py +++ /dev/null @@ -1,291 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.crons import MonitorStatus, capture_checkin -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.celery.utils import ( - _get_humanized_interval, - _now_seconds_since_epoch, -) -from sentry_sdk.utils import ( - logger, - match_regex_list, -) - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, TypeVar, Union - - from sentry_sdk._types import ( - MonitorConfig, - MonitorConfigScheduleType, - MonitorConfigScheduleUnit, - ) - - F = TypeVar("F", bound=Callable[..., Any]) - - -try: - from celery import Celery, Task # type: ignore - from celery.beat import Scheduler # type: ignore - from celery.schedules import crontab, schedule # type: ignore - from celery.signals import ( # type: ignore - task_failure, - task_retry, - task_success, - ) -except ImportError: - raise DidNotEnable("Celery not installed") - -try: - from redbeat.schedulers import RedBeatScheduler # type: ignore -except ImportError: - RedBeatScheduler = None - - -def _get_headers(task: "Task") -> "dict[str, Any]": - headers = task.request.get("headers") or {} - - # flatten nested headers - if "headers" in headers: - headers.update(headers["headers"]) - del headers["headers"] - - headers.update(task.request.get("properties") or {}) - - return headers - - -def _get_monitor_config( - celery_schedule: "Any", app: "Celery", monitor_name: str -) -> "MonitorConfig": - monitor_config: "MonitorConfig" = {} - schedule_type: "Optional[MonitorConfigScheduleType]" = None - schedule_value: "Optional[Union[str, int]]" = None - schedule_unit: "Optional[MonitorConfigScheduleUnit]" = None - - if isinstance(celery_schedule, crontab): - schedule_type = "crontab" - schedule_value = ( - "{0._orig_minute} " - "{0._orig_hour} " - "{0._orig_day_of_month} " - "{0._orig_month_of_year} " - "{0._orig_day_of_week}".format(celery_schedule) - ) - elif isinstance(celery_schedule, schedule): - schedule_type = "interval" - (schedule_value, schedule_unit) = _get_humanized_interval( - celery_schedule.seconds - ) - - if schedule_unit == "second": - logger.warning( - "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.", - monitor_name, - schedule_value, - ) - return {} - - else: - logger.warning( - "Celery schedule type '%s' not supported by Sentry Crons.", - type(celery_schedule), - ) - return {} - - monitor_config["schedule"] = {} - monitor_config["schedule"]["type"] = schedule_type - monitor_config["schedule"]["value"] = schedule_value - - if schedule_unit is not None: - monitor_config["schedule"]["unit"] = schedule_unit - - monitor_config["timezone"] = ( - ( - hasattr(celery_schedule, "tz") - and celery_schedule.tz is not None - and str(celery_schedule.tz) - ) - or app.timezone - or "UTC" - ) - - return monitor_config - - -def _apply_crons_data_to_schedule_entry( - scheduler: "Any", - schedule_entry: "Any", - integration: "sentry_sdk.integrations.celery.CeleryIntegration", -) -> None: - """ - Add Sentry Crons information to the schedule_entry headers. - """ - if not integration.monitor_beat_tasks: - return - - monitor_name = schedule_entry.name - - task_should_be_excluded = match_regex_list( - monitor_name, integration.exclude_beat_tasks - ) - if task_should_be_excluded: - return - - celery_schedule = schedule_entry.schedule - app = scheduler.app - - monitor_config = _get_monitor_config(celery_schedule, app, monitor_name) - - is_supported_schedule = bool(monitor_config) - if not is_supported_schedule: - return - - headers = schedule_entry.options.pop("headers", {}) - headers.update( - { - "sentry-monitor-slug": monitor_name, - "sentry-monitor-config": monitor_config, - } - ) - - check_in_id = capture_checkin( - monitor_slug=monitor_name, - monitor_config=monitor_config, - status=MonitorStatus.IN_PROGRESS, - ) - headers.update({"sentry-monitor-check-in-id": check_in_id}) - - # Set the Sentry configuration in the options of the ScheduleEntry. - # Those will be picked up in `apply_async` and added to the headers. - schedule_entry.options["headers"] = headers - - -def _wrap_beat_scheduler( - original_function: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Makes sure that: - - a new Sentry trace is started for each task started by Celery Beat and - it is propagated to the task. - - the Sentry Crons information is set in the Celery Beat task's - headers so that is monitored with Sentry Crons. - - After the patched function is called, - Celery Beat will call apply_async to put the task in the queue. - """ - # Patch only once - # Can't use __name__ here, because some of our tests mock original_apply_entry - already_patched = "sentry_patched_scheduler" in str(original_function) - if already_patched: - return original_function - - from sentry_sdk.integrations.celery import CeleryIntegration - - def sentry_patched_scheduler(*args: "Any", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(CeleryIntegration) - if integration is None: - return original_function(*args, **kwargs) - - # Tasks started by Celery Beat start a new Trace - scope = sentry_sdk.get_isolation_scope() - scope.set_new_propagation_context() - scope._name = "celery-beat" - - scheduler, schedule_entry = args - _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration) - - return original_function(*args, **kwargs) - - return sentry_patched_scheduler - - -def _patch_beat_apply_entry() -> None: - Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) - - -def _patch_redbeat_apply_async() -> None: - if RedBeatScheduler is None: - return - - RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) - - -def _setup_celery_beat_signals(monitor_beat_tasks: bool) -> None: - if monitor_beat_tasks: - task_success.connect(crons_task_success) - task_failure.connect(crons_task_failure) - task_retry.connect(crons_task_retry) - - -def crons_task_success(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_success %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.OK, - ) - - -def crons_task_failure(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_failure %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) - - -def crons_task_retry(sender: "Task", **kwargs: "dict[Any, Any]") -> None: - logger.debug("celery_task_retry %s", sender) - headers = _get_headers(sender) - - if "sentry-monitor-slug" not in headers: - return - - monitor_config = headers.get("sentry-monitor-config", {}) - - start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s") - - capture_checkin( - monitor_slug=headers["sentry-monitor-slug"], - monitor_config=monitor_config, - check_in_id=headers["sentry-monitor-check-in-id"], - duration=( - _now_seconds_since_epoch() - float(start_timestamp_s) - if start_timestamp_s - else None - ), - status=MonitorStatus.ERROR, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py deleted file mode 100644 index 8d181f1f24..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/celery/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import time -from typing import TYPE_CHECKING, cast - -if TYPE_CHECKING: - from typing import Tuple - - from sentry_sdk._types import MonitorConfigScheduleUnit - - -def _now_seconds_since_epoch() -> float: - # We cannot use `time.perf_counter()` when dealing with the duration - # of a Celery task, because the start of a Celery task and - # the end are recorded in different processes. - # Start happens in the Celery Beat process, - # the end in a Celery Worker process. - return time.time() - - -def _get_humanized_interval(seconds: float) -> "Tuple[int, MonitorConfigScheduleUnit]": - TIME_UNITS = ( # noqa: N806 - ("day", 60 * 60 * 24.0), - ("hour", 60 * 60.0), - ("minute", 60.0), - ) - - seconds = float(seconds) - for unit, divider in TIME_UNITS: - if seconds >= divider: - interval = int(seconds / divider) - return (interval, cast("MonitorConfigScheduleUnit", unit)) - - return (int(seconds), "second") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py deleted file mode 100644 index 9baa0e5cdd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/chalice.py +++ /dev/null @@ -1,188 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.aws_lambda import _make_request_event_processor -from sentry_sdk.traces import ( - SpanStatus, - StreamedSpan, - get_current_span, -) -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - parse_version, - reraise, -) - -try: - import chalice # type: ignore - from chalice import Chalice, ChaliceViewError - from chalice import __version__ as CHALICE_VERSION - from chalice.app import ( # type: ignore - EventSourceHandler as ChaliceEventSourceHandler, - ) -except ImportError: - raise DidNotEnable("Chalice is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, TypeVar - - F = TypeVar("F", bound=Callable[..., Any]) - - -class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore - def __call__(self, event: "Any", context: "Any") -> "Any": - client = sentry_sdk.get_client() - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor(event, context, configured_time) - ) - try: - return ChaliceEventSourceHandler.__call__(self, event, context) - except Exception: - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - client.flush() - reraise(*exc_info) - - -def _get_view_function_response( - app: "Any", view_function: "F", function_args: "Any" -) -> "F": - @wraps(view_function) - def wrapped_view_function(**function_args: "Any") -> "Any": - client = sentry_sdk.get_client() - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - configured_time = app.lambda_context.get_remaining_time_in_millis() - scope.add_event_processor( - _make_request_event_processor( - app.current_request.to_dict(), - app.lambda_context, - configured_time, - ) - ) - - if has_span_streaming_enabled(client.options): - current_span = get_current_span() - segment = None - if type(current_span) is StreamedSpan: - # A segment already exists (created by the AWS Lambda - # integration), so decorate it with Chalice attributes - # The AWS Lambda integration owns the span lifecycle - # (end + flush), but Chalice converts unhandled view exceptions - # into 500 responses, so the error must be captured here. - request_dict = app.current_request.to_dict() - headers = request_dict.get("headers", {}) - - header_attrs: "Dict[str, Any]" = {} - for header, value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attrs[f"http.request.header.{header.lower()}"] = value - - additional_attrs: "Dict[str, Any]" = {} - if "method" in request_dict: - additional_attrs["http.request.method"] = request_dict["method"] - - attributes = { - "sentry.origin": ChaliceIntegration.origin, - **header_attrs, - **additional_attrs, - } - - segment = current_span._segment - segment.set_attributes(attributes) - - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - if segment: - segment.status = SpanStatus.ERROR.value - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - if segment is None: - client.flush() - raise - else: - scope.set_transaction_name( - app.lambda_context.function_name, - source=TransactionSource.COMPONENT, - ) - try: - return view_function(**function_args) - except Exception as exc: - if isinstance(exc, ChaliceViewError): - raise - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "chalice", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - client.flush() - raise - - return wrapped_view_function # type: ignore - - -class ChaliceIntegration(Integration): - identifier = "chalice" - origin = f"auto.function.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(CHALICE_VERSION) - - if version is None: - raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION)) - - if version < (1, 20): - old_get_view_function_response = Chalice._get_view_function_response - else: - from chalice.app import RestAPIEventHandler - - old_get_view_function_response = ( - RestAPIEventHandler._get_view_function_response - ) - - def sentry_event_response( - app: "Any", view_function: "F", function_args: "Dict[str, Any]" - ) -> "Any": - wrapped_view_function = _get_view_function_response( - app, view_function, function_args - ) - - return old_get_view_function_response( - app, wrapped_view_function, function_args - ) - - if version < (1, 20): - Chalice._get_view_function_response = sentry_event_response - else: - RestAPIEventHandler._get_view_function_response = sentry_event_response - # for everything else (like events) - chalice.app.EventSourceHandler = EventSourceHandler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py deleted file mode 100644 index e6b3009548..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/clickhouse_driver.py +++ /dev/null @@ -1,211 +0,0 @@ -import functools -from typing import TYPE_CHECKING, TypeVar - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Any, Callable, ParamSpec, Union -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - - -try: - from clickhouse_driver import VERSION # type: ignore[import-not-found] - from clickhouse_driver.client import Client # type: ignore[import-not-found] - from clickhouse_driver.connection import ( # type: ignore[import-not-found] - Connection, - ) - -except ImportError: - raise DidNotEnable("clickhouse-driver not installed.") - - -class ClickhouseDriverIntegration(Integration): - identifier = "clickhouse_driver" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(ClickhouseDriverIntegration, VERSION) - - # Every query is done using the Connection's `send_query` function - Connection.send_query = _wrap_start(Connection.send_query) - - # If the query contains parameters then the send_data function is used to send those parameters to clickhouse - _wrap_send_data() - - # Every query ends either with the Client's `receive_end_of_query` (no result expected) - # or its `receive_result` (result expected) - Client.receive_end_of_query = _wrap_end(Client.receive_end_of_query) - if hasattr(Client, "receive_end_of_insert_query"): - # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` - Client.receive_end_of_insert_query = _wrap_end( - Client.receive_end_of_insert_query - ) - Client.receive_result = _wrap_end(Client.receive_result) - - -P = ParamSpec("P") -T = TypeVar("T") - - -def _wrap_start(f: "Callable[P, T]") -> "Callable[P, T]": - @functools.wraps(f) - def _inner(*args: "P.args", **kwargs: "P.kwargs") -> "T": - client = sentry_sdk.get_client() - if client.get_integration(ClickhouseDriverIntegration) is None: - return f(*args, **kwargs) - - connection = args[0] - query = args[1] - query_id = args[2] if len(args) > 2 else kwargs.get("query_id") - params = args[3] if len(args) > 3 else kwargs.get("params") - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=query, # type: ignore - attributes={ - "sentry.op": OP.DB, - "sentry.origin": ClickhouseDriverIntegration.origin, - SPANDATA.DB_QUERY_TEXT: str(query), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=ClickhouseDriverIntegration.origin, - ) - - span.set_data("query", query) - - if query_id: - span.set_data("db.query_id", query_id) - - if params and should_send_default_pii(): - span.set_data("db.params", params) - - connection._sentry_span = span # type: ignore[attr-defined] - - _set_db_data(span, connection) - - # run the original code - ret = f(*args, **kwargs) - - return ret - - return _inner - - -def _wrap_end(f: "Callable[P, T]") -> "Callable[P, T]": - def _inner_end(*args: "P.args", **kwargs: "P.kwargs") -> "T": - res = f(*args, **kwargs) - instance = args[0] - span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined] - - if span is None: - return res - - if isinstance(span, StreamedSpan): - span.end() - else: - if res is not None and should_send_default_pii(): - span.set_data("db.result", res) - - with capture_internal_exceptions(): - span.scope.add_breadcrumb( - message=span._data.pop("query"), category="query", data=span._data - ) - - span.finish() - - return res - - return _inner_end - - -def _wrap_send_data() -> None: - original_send_data = Client.send_data - - def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data - self, sample_block, data, types_check=False, columnar=False, *args, **kwargs - ): - span = getattr(self.connection, "_sentry_span", None) - - if isinstance(span, StreamedSpan): - _set_db_data(span, self.connection) - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - if span is not None: - _set_db_data(span, self.connection) - - if should_send_default_pii(): - db_params = span._data.get("db.params", []) - - if isinstance(data, (list, tuple)): - db_params.extend(data) - - else: # data is a generic iterator - orig_data = data - - # Wrap the generator to add items to db.params as they are yielded. - # This allows us to send the params to Sentry without needing to allocate - # memory for the entire generator at once. - def wrapped_generator() -> "Iterator[Any]": - for item in orig_data: - db_params.append(item) - yield item - - # Replace the original iterator with the wrapped one. - data = wrapped_generator() - - span.set_data("db.params", db_params) - - return original_send_data( - self, sample_block, data, types_check, columnar, *args, **kwargs - ) - - Client.send_data = _inner_send_data - - -def _set_db_data(span: "Union[Span, StreamedSpan]", connection: "Connection") -> None: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "clickhouse") - span.set_attribute(SPANDATA.DB_NAMESPACE, connection.database) - - set_on_span = span.set_attribute - else: - span.set_data(SPANDATA.DB_SYSTEM, "clickhouse") - span.set_data(SPANDATA.DB_NAME, connection.database) - - set_on_span = span.set_data - - set_on_span(SPANDATA.DB_DRIVER_NAME, "clickhouse-driver") - set_on_span(SPANDATA.SERVER_ADDRESS, connection.host) - set_on_span(SPANDATA.SERVER_PORT, connection.port) - set_on_span(SPANDATA.DB_USER, connection.user) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py deleted file mode 100644 index f6285d0a9b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cloud_resource_context.py +++ /dev/null @@ -1,273 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import urllib3 - -from sentry_sdk.api import set_context -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Dict - - -CONTEXT_TYPE = "cloud_resource" - -HTTP_TIMEOUT = 2.0 - -AWS_METADATA_HOST = "169.254.169.254" -AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) -AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( - AWS_METADATA_HOST -) - -GCP_METADATA_HOST = "metadata.google.internal" -GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( - GCP_METADATA_HOST -) - - -class CLOUD_PROVIDER: # noqa: N801 - """ - Name of the cloud provider. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - ALIBABA = "alibaba_cloud" - AWS = "aws" - AZURE = "azure" - GCP = "gcp" - IBM = "ibm_cloud" - TENCENT = "tencent_cloud" - - -class CLOUD_PLATFORM: # noqa: N801 - """ - The cloud platform. - see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ - """ - - AWS_EC2 = "aws_ec2" - AWS_LAMBDA = "aws_lambda" - GCP_COMPUTE_ENGINE = "gcp_compute_engine" - - -class CloudResourceContextIntegration(Integration): - """ - Adds cloud resource context to the Senty scope - """ - - identifier = "cloudresourcecontext" - - cloud_provider = "" - - aws_token = "" - http = urllib3.PoolManager(timeout=HTTP_TIMEOUT) - - gcp_metadata = None - - def __init__(self, cloud_provider: str = "") -> None: - CloudResourceContextIntegration.cloud_provider = cloud_provider - - @classmethod - def _is_aws(cls) -> bool: - try: - r = cls.http.request( - "PUT", - AWS_TOKEN_URL, - headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, - ) - - if r.status != 200: - return False - - cls.aws_token = r.data.decode() - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking AWS metadata service: %s", str(e)) - return False - - @classmethod - def _get_aws_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.AWS, - "cloud.platform": CLOUD_PLATFORM.AWS_EC2, - } - - try: - r = cls.http.request( - "GET", - AWS_METADATA_URL, - headers={"X-aws-ec2-metadata-token": cls.aws_token}, - ) - - if r.status != 200: - return ctx - - data = json.loads(r.data.decode("utf-8")) - - try: - ctx["cloud.account.id"] = data["accountId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = data["availabilityZone"] - except Exception: - pass - - try: - ctx["cloud.region"] = data["region"] - except Exception: - pass - - try: - ctx["host.id"] = data["instanceId"] - except Exception: - pass - - try: - ctx["host.type"] = data["instanceType"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching AWS metadata: %s", str(e)) - - return ctx - - @classmethod - def _is_gcp(cls) -> bool: - try: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return False - - cls.gcp_metadata = json.loads(r.data.decode("utf-8")) - return True - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - return False - except Exception as e: - logger.debug("Error checking GCP metadata service: %s", str(e)) - return False - - @classmethod - def _get_gcp_context(cls) -> "Dict[str, str]": - ctx = { - "cloud.provider": CLOUD_PROVIDER.GCP, - "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, - } - - gcp_metadata = cls.gcp_metadata - try: - if cls.gcp_metadata is None: - r = cls.http.request( - "GET", - GCP_METADATA_URL, - headers={"Metadata-Flavor": "Google"}, - ) - - if r.status != 200: - return ctx - - gcp_metadata = json.loads(r.data.decode("utf-8")) - cls.gcp_metadata = gcp_metadata - - try: - ctx["cloud.account.id"] = gcp_metadata["project"]["projectId"] - except Exception: - pass - - try: - ctx["cloud.availability_zone"] = gcp_metadata["instance"]["zone"].split( - "/" - )[-1] - except Exception: - pass - - try: - # only populated in google cloud run - ctx["cloud.region"] = gcp_metadata["instance"]["region"].split("/")[-1] - except Exception: - pass - - try: - ctx["host.id"] = gcp_metadata["instance"]["id"] - except Exception: - pass - - except urllib3.exceptions.TimeoutError: - logger.debug( - "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT - ) - except Exception as e: - logger.debug("Error fetching GCP metadata: %s", str(e)) - - return ctx - - @classmethod - def _get_cloud_provider(cls) -> str: - if cls._is_aws(): - return CLOUD_PROVIDER.AWS - - if cls._is_gcp(): - return CLOUD_PROVIDER.GCP - - return "" - - @classmethod - def _get_cloud_resource_context(cls) -> "Dict[str, str]": - cloud_provider = ( - cls.cloud_provider - if cls.cloud_provider != "" - else CloudResourceContextIntegration._get_cloud_provider() - ) - if cloud_provider in context_getters.keys(): - return context_getters[cloud_provider]() - - return {} - - @staticmethod - def setup_once() -> None: - cloud_provider = CloudResourceContextIntegration.cloud_provider - unsupported_cloud_provider = ( - cloud_provider != "" and cloud_provider not in context_getters.keys() - ) - - if unsupported_cloud_provider: - logger.warning( - "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", - CloudResourceContextIntegration.cloud_provider, - list(context_getters.keys()), - ) - - context = CloudResourceContextIntegration._get_cloud_resource_context() - if context != {}: - set_context(CONTEXT_TYPE, context) - - -# Map with the currently supported cloud providers -# mapping to functions extracting the context -context_getters = { - CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, - CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, -} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py deleted file mode 100644 index 7abf3f6808..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/cohere.py +++ /dev/null @@ -1,303 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterator, Union - - from sentry_sdk.tracing import Span - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise - -try: - from cohere import ( - ChatStreamEndEvent, - NonStreamedChatResponse, - ) - from cohere.base_client import BaseCohere - from cohere.client import Client - - if TYPE_CHECKING: - from cohere import StreamedChatResponse -except ImportError: - raise DidNotEnable("Cohere not installed") - -try: - # cohere 5.9.3+ - from cohere import StreamEndStreamedChatResponse -except ImportError: - from cohere import StreamedChatResponse_StreamEnd as StreamEndStreamedChatResponse - - -COLLECTED_CHAT_PARAMS = { - "model": SPANDATA.AI_MODEL_ID, - "k": SPANDATA.AI_TOP_K, - "p": SPANDATA.AI_TOP_P, - "seed": SPANDATA.AI_SEED, - "frequency_penalty": SPANDATA.AI_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.AI_PRESENCE_PENALTY, - "raw_prompting": SPANDATA.AI_RAW_PROMPTING, -} - -COLLECTED_PII_CHAT_PARAMS = { - "tools": SPANDATA.AI_TOOLS, - "preamble": SPANDATA.AI_PREAMBLE, -} - -COLLECTED_CHAT_RESP_ATTRS = { - "generation_id": SPANDATA.AI_GENERATION_ID, - "is_search_required": SPANDATA.AI_SEARCH_REQUIRED, - "finish_reason": SPANDATA.AI_FINISH_REASON, -} - -COLLECTED_PII_CHAT_RESP_ATTRS = { - "citations": SPANDATA.AI_CITATIONS, - "documents": SPANDATA.AI_DOCUMENTS, - "search_queries": SPANDATA.AI_SEARCH_QUERIES, - "search_results": SPANDATA.AI_SEARCH_RESULTS, - "tool_calls": SPANDATA.AI_TOOL_CALLS, -} - - -class CohereIntegration(Integration): - identifier = "cohere" - origin = f"auto.ai.{identifier}" - - def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - BaseCohere.chat = _wrap_chat(BaseCohere.chat, streaming=False) - Client.embed = _wrap_embed(Client.embed) - BaseCohere.chat_stream = _wrap_chat(BaseCohere.chat_stream, streaming=True) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "cohere", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _end_span(span: "Any") -> None: - if isinstance(span, StreamedSpan): - span.end() - else: - span.__exit__(None, None, None) - - -def _wrap_chat(f: "Callable[..., Any]", streaming: bool) -> "Callable[..., Any]": - def collect_chat_response_fields( - span: "Union[Span, StreamedSpan]", - res: "NonStreamedChatResponse", - include_pii: bool, - ) -> None: - if include_pii: - if hasattr(res, "text"): - set_data_normalized( - span, - SPANDATA.AI_RESPONSES, - [res.text], - ) - for pii_attr in COLLECTED_PII_CHAT_RESP_ATTRS: - if hasattr(res, pii_attr): - set_data_normalized(span, "ai." + pii_attr, getattr(res, pii_attr)) - - for attr in COLLECTED_CHAT_RESP_ATTRS: - if hasattr(res, attr): - set_data_normalized(span, "ai." + attr, getattr(res, attr)) - - if hasattr(res, "meta"): - if hasattr(res.meta, "billed_units"): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - output_tokens=res.meta.billed_units.output_tokens, - ) - elif hasattr(res.meta, "tokens"): - record_token_usage( - span, - input_tokens=res.meta.tokens.input_tokens, - output_tokens=res.meta.tokens.output_tokens, - ) - - if hasattr(res.meta, "warnings"): - set_data_normalized(span, SPANDATA.AI_WARNINGS, res.meta.warnings) - - @wraps(f) - def new_chat(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if ( - integration is None - or "message" not in kwargs - or not isinstance(kwargs.get("message"), str) - ): - return f(*args, **kwargs) - - message = kwargs.get("message") - - if is_span_streaming_enabled: - span = sentry_sdk.traces.start_span( - name="cohere.client.Chat", - attributes={ - "sentry.op": consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.COHERE_CHAT_COMPLETIONS_CREATE, - name="cohere.client.Chat", - origin=CohereIntegration.origin, - ) - span.__enter__() - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - - reraise(*exc_info) - - with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, - SPANDATA.AI_INPUT_MESSAGES, - list( - map( - lambda x: { - "role": getattr(x, "role", "").lower(), - "content": getattr(x, "message", ""), - }, - kwargs.get("chat_history", []), - ) - ) - + [{"role": "user", "content": message}], - ) - for k, v in COLLECTED_PII_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - - for k, v in COLLECTED_CHAT_PARAMS.items(): - if k in kwargs: - set_data_normalized(span, v, kwargs[k]) - set_data_normalized(span, SPANDATA.AI_STREAMING, False) - - if streaming: - old_iterator = res - - def new_iterator() -> "Iterator[StreamedChatResponse]": - with capture_internal_exceptions(): - for x in old_iterator: - if isinstance(x, ChatStreamEndEvent) or isinstance( - x, StreamEndStreamedChatResponse - ): - collect_chat_response_fields( - span, - x.response, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - yield x - _end_span(span) - - return new_iterator() - elif isinstance(res, NonStreamedChatResponse): - collect_chat_response_fields( - span, - res, - include_pii=should_send_default_pii() - and integration.include_prompts, - ) - _end_span(span) - else: - set_data_normalized(span, "unknown_response", True) - _end_span(span) - return res - - return new_chat - - -def _wrap_embed(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(CohereIntegration) - if integration is None: - return f(*args, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name="Cohere Embedding Creation", - attributes={ - "sentry.op": consts.OP.COHERE_EMBEDDINGS_CREATE, - "sentry.origin": CohereIntegration.origin, - }, - ) - else: - span_ctx = get_start_span_function()( - op=consts.OP.COHERE_EMBEDDINGS_CREATE, - name="Cohere Embedding Creation", - origin=CohereIntegration.origin, - ) - - with span_ctx as span: - if "texts" in kwargs and ( - should_send_default_pii() and integration.include_prompts - ): - if isinstance(kwargs["texts"], str): - set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) - elif ( - isinstance(kwargs["texts"], list) - and len(kwargs["texts"]) > 0 - and isinstance(kwargs["texts"][0], str) - ): - set_data_normalized( - span, SPANDATA.AI_INPUT_MESSAGES, kwargs["texts"] - ) - - if "model" in kwargs: - set_data_normalized(span, SPANDATA.AI_MODEL_ID, kwargs["model"]) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - reraise(*exc_info) - if ( - hasattr(res, "meta") - and hasattr(res.meta, "billed_units") - and hasattr(res.meta.billed_units, "input_tokens") - ): - record_token_usage( - span, - input_tokens=res.meta.billed_units.input_tokens, - total_tokens=res.meta.billed_units.input_tokens, - ) - return res - - return new_embed diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py deleted file mode 100644 index a0e9014666..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dedupe.py +++ /dev/null @@ -1,62 +0,0 @@ -import weakref -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - - -class DedupeIntegration(Integration): - identifier = "dedupe" - - def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - if hint is None: - return event - - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return event - - exc_info = hint.get("exc_info", None) - if exc_info is None: - return event - - last_seen = integration._last_seen.get(None) - if last_seen is not None: - # last_seen is either a weakref or the original instance - last_seen = ( - last_seen() if isinstance(last_seen, weakref.ref) else last_seen - ) - - exc = exc_info[1] - if last_seen is exc: - logger.info("DedupeIntegration dropped duplicated error event %s", exc) - return None - - # we can only weakref non builtin types - try: - integration._last_seen.set(weakref.ref(exc)) - except TypeError: - integration._last_seen.set(exc) - - return event - - @staticmethod - def reset_last_seen() -> None: - integration = sentry_sdk.get_client().get_integration(DedupeIntegration) - if integration is None: - return - - integration._last_seen.set(None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py deleted file mode 100644 index 361b60079d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/__init__.py +++ /dev/null @@ -1,884 +0,0 @@ -import inspect -import sys -import threading -import weakref -from importlib import import_module - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANNAME -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import ( - add_query_source, - has_span_streaming_enabled, - record_sql_queries, -) -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - SENSITIVE_DATA_SUBSTITUTE, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - transaction_from_function, - walk_exception_chain, -) - -try: - from django import VERSION as DJANGO_VERSION - from django.conf import settings - from django.conf import settings as django_settings - from django.core import signals - from django.utils.functional import SimpleLazyObject - - try: - from django.urls import resolve - except ImportError: - from django.core.urlresolvers import resolve - - try: - from django.urls import Resolver404 - except ImportError: - from django.core.urlresolvers import Resolver404 - - # Only available in Django 3.0+ - try: - from django.core.handlers.asgi import ASGIRequest - except Exception: - ASGIRequest = None - -except ImportError: - raise DidNotEnable("Django not installed") - -from sentry_sdk.integrations.django.middleware import patch_django_middlewares -from sentry_sdk.integrations.django.signals_handlers import patch_signals -from sentry_sdk.integrations.django.tasks import patch_tasks -from sentry_sdk.integrations.django.templates import ( - get_template_frame_from_exception, - patch_templates, -) -from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER -from sentry_sdk.integrations.django.views import patch_views - -if DJANGO_VERSION[:2] > (1, 8): - from sentry_sdk.integrations.django.caching import patch_caching -else: - patch_caching = None # type: ignore - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from django.core.handlers.wsgi import WSGIRequest - from django.http.request import QueryDict - from django.http.response import HttpResponse - from django.utils.datastructures import MultiValueDict - - from sentry_sdk._types import Event, EventProcessor, Hint, NotImplementedType - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -if DJANGO_VERSION < (1, 10): - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated() - -else: - - def is_authenticated(request_user: "Any") -> bool: - return request_user.is_authenticated - - -TRANSACTION_STYLE_VALUES = ("function_name", "url") - - -class DjangoIntegration(Integration): - """ - Auto instrument a Django application. - - :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`. - :param middleware_spans: Whether to create spans for middleware. Defaults to `False`. - :param signals_spans: Whether to create spans for signals. Defaults to `True`. - :param signals_denylist: A list of signals to ignore when creating spans. - :param cache_spans: Whether to create spans for cache operations. Defaults to `False`. - """ - - identifier = "django" - origin = f"auto.http.{identifier}" - origin_db = f"auto.db.{identifier}" - - transaction_style = "" - middleware_spans: "Optional[bool]" = None - signals_spans: "Optional[bool]" = None - cache_spans: "Optional[bool]" = None - signals_denylist: "list[signals.Signal]" = [] - - def __init__( - self, - transaction_style: str = "url", - middleware_spans: bool = False, - signals_spans: bool = True, - cache_spans: bool = False, - db_transaction_spans: bool = False, - signals_denylist: "Optional[list[signals.Signal]]" = None, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - - self.signals_spans = signals_spans - self.signals_denylist = signals_denylist or [] - - self.cache_spans = cache_spans - self.db_transaction_spans = db_transaction_spans - - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - _check_minimum_version(DjangoIntegration, DJANGO_VERSION) - - install_sql_hook() - # Patch in our custom middleware. - - # logs an error for every 500 - ignore_logger("django.server") - ignore_logger("django.request") - - from django.core.handlers.wsgi import WSGIHandler - - old_app = WSGIHandler.__call__ - - @ensure_integration_enabled(DjangoIntegration, old_app) - def sentry_patched_wsgi_handler( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - bound_old_app = old_app.__get__(self, WSGIHandler) - - from django.conf import settings - - use_x_forwarded_for = settings.USE_X_FORWARDED_HOST - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - middleware = SentryWsgiMiddleware( - bound_old_app, - use_x_forwarded_for, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - WSGIHandler.__call__ = sentry_patched_wsgi_handler - - _patch_get_response() - - _patch_django_asgi_handler() - - signals.got_request_exception.connect(_got_request_exception) - - @add_global_event_processor - def process_django_templates( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_, exc_value, _) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - frame = get_template_frame_from_exception(exc_value) - if frame is not None: - frames = exception.get("stacktrace", {}).get("frames", []) - - for i in reversed(range(len(frames))): - f = frames[i] - if ( - f.get("function") in ("Parser.parse", "parse", "render") - and f.get("module") == "django.template.base" - ): - i += 1 - break - else: - i = len(frames) - - frames.insert(i, frame) - - return event - - @add_global_repr_processor - def _django_queryset_repr( - value: "Any", hint: "Dict[str, Any]" - ) -> "Union[NotImplementedType, str]": - try: - # Django 1.6 can fail to import `QuerySet` when Django settings - # have not yet been initialized. - # - # If we fail to import, return `NotImplemented`. It's at least - # unlikely that we have a query set in `value` when importing - # `QuerySet` fails. - from django.db.models.query import QuerySet - except Exception: - return NotImplemented - - if not isinstance(value, QuerySet) or value._result_cache: - return NotImplemented - - return "<%s from %s at 0x%x>" % ( - value.__class__.__name__, - value.__module__, - id(value), - ) - - _patch_channels() - patch_django_middlewares() - patch_views() - patch_templates() - patch_signals() - patch_tasks() - add_template_context_repr_sequence() - - if patch_caching is not None: - patch_caching() - - -_DRF_PATCHED = False -_DRF_PATCH_LOCK = threading.Lock() - - -def _patch_drf() -> None: - """ - Patch Django Rest Framework for more/better request data. DRF's request - type is a wrapper around Django's request type. The attribute we're - interested in is `request.data`, which is a cached property containing a - parsed request body. Reading a request body from that property is more - reliable than reading from any of Django's own properties, as those don't - hold payloads in memory and therefore can only be accessed once. - - We patch the Django request object to include a weak backreference to the - DRF request object, such that we can later use either in - `DjangoRequestExtractor`. - - This function is not called directly on SDK setup, because importing almost - any part of Django Rest Framework will try to access Django settings (where - `sentry_sdk.init()` might be called from in the first place). Instead we - run this function on every request and do the patching on the first - request. - """ - - global _DRF_PATCHED - - if _DRF_PATCHED: - # Double-checked locking - return - - with _DRF_PATCH_LOCK: - if _DRF_PATCHED: - return - - # We set this regardless of whether the code below succeeds or fails. - # There is no point in trying to patch again on the next request. - _DRF_PATCHED = True - - with capture_internal_exceptions(): - try: - from rest_framework.views import APIView # type: ignore - except ImportError: - pass - else: - old_drf_initial = APIView.initial - - def sentry_patched_drf_initial( - self: "APIView", request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - with capture_internal_exceptions(): - request._request._sentry_drf_request_backref = weakref.ref( - request - ) - pass - return old_drf_initial(self, request, *args, **kwargs) - - APIView.initial = sentry_patched_drf_initial - - -def _patch_channels() -> None: - try: - from channels.http import AsgiHandler # type: ignore - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl - - patch_channels_asgi_handler_impl(AsgiHandler) - - -def _patch_django_asgi_handler() -> None: - try: - from django.core.handlers.asgi import ASGIHandler - except ImportError: - return - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl - - patch_django_asgi_handler_impl(ASGIHandler) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" -) -> None: - try: - transaction_name = None - if transaction_style == "function_name": - fn = resolve(request.path).func - transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) - - elif transaction_style == "url": - if hasattr(request, "urlconf"): - transaction_name = LEGACY_RESOLVER.resolve( - request.path_info, urlconf=request.urlconf - ) - else: - transaction_name = LEGACY_RESOLVER.resolve(request.path_info) - - if transaction_name is None: - transaction_name = request.path_info - source = TransactionSource.URL - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name( - transaction_name, - source=source, - ) - except Resolver404: - urlconf = import_module(settings.ROOT_URLCONF) - # This exception only gets thrown when transaction_style is `function_name` - # So we don't check here what style is configured - if hasattr(urlconf, "handler404"): - handler = urlconf.handler404 - if isinstance(handler, str): - scope.transaction = handler - else: - scope.transaction = transaction_from_function( - getattr(handler, "view_class", handler) - ) - except Exception: - pass - - -def _before_get_response(request: "WSGIRequest") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return - - _patch_drf() - - scope = sentry_sdk.get_current_scope() - # Rely on WSGI middleware to start a trace - _set_transaction_name_and_source(scope, integration.transaction_style, request) - - scope.add_event_processor( - _make_wsgi_request_event_processor(weakref.ref(request), integration) - ) - - -def _attempt_resolve_again( - request: "WSGIRequest", scope: "sentry_sdk.Scope", transaction_style: str -) -> None: - """ - Some django middlewares overwrite request.urlconf - so we need to respect that contract, - so we try to resolve the url again. - """ - if not hasattr(request, "urlconf"): - return - - _set_transaction_name_and_source(scope, transaction_style, request) - - -def _after_get_response(request: "WSGIRequest") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming and should_send_default_pii(): - user = getattr(request, "user", None) - - # Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation. - # Exit early if the user has not been materialized yet. - is_lazy = isinstance(user, SimpleLazyObject) - if is_lazy and hasattr(request, "_cached_user"): - user = request._cached_user - elif is_lazy: - return - - if user is None or not is_authenticated(user): - return - - user_info = {} - try: - user_info["id"] = str(user.pk) - except Exception: - pass - - try: - user_info["email"] = user.email - except Exception: - pass - - try: - user_info["username"] = user.get_username() - except Exception: - pass - - sentry_sdk.set_user(user_info) - - -def _patch_get_response() -> None: - """ - patch get_response, because at that point we have the Django request object - """ - from django.core.handlers.base import BaseHandler - - old_get_response = BaseHandler.get_response - - def sentry_patched_get_response( - self: "Any", request: "WSGIRequest" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - rv = old_get_response(self, request) - _after_get_response(request) - return rv - - BaseHandler.get_response = sentry_patched_get_response - - if hasattr(BaseHandler, "get_response_async"): - from sentry_sdk.integrations.django.asgi import patch_get_response_async - - patch_get_response_async(BaseHandler, _before_get_response) - - -def _make_wsgi_request_event_processor( - weak_request: "Callable[[], WSGIRequest]", integration: "DjangoIntegration" -) -> "EventProcessor": - def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - request = weak_request() - if request is None: - return event - - django_3 = ASGIRequest is not None - if django_3 and type(request) == ASGIRequest: - # We have a `asgi_request_event_processor` for this. - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return wsgi_request_event_processor - - -def _got_request_exception(request: "WSGIRequest" = None, **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - if integration is None: - return - - if request is not None and integration.transaction_style == "url": - scope = sentry_sdk.get_current_scope() - _attempt_resolve_again(request, scope, integration.transaction_style) - - event, hint = event_from_exception( - sys.exc_info(), - client_options=client.options, - mechanism={"type": "django", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class DjangoRequestExtractor(RequestExtractor): - def __init__(self, request: "Union[WSGIRequest, ASGIRequest]") -> None: - try: - drf_request = request._sentry_drf_request_backref() - if drf_request is not None: - request = drf_request - except AttributeError: - pass - self.request = request - - def env(self) -> "Dict[str, str]": - return self.request.META - - def cookies(self) -> "Dict[str, Union[str, AnnotatedValue]]": - privacy_cookies = [ - django_settings.CSRF_COOKIE_NAME, - django_settings.SESSION_COOKIE_NAME, - ] - - clean_cookies: "Dict[str, Union[str, AnnotatedValue]]" = {} - for key, val in self.request.COOKIES.items(): - if key in privacy_cookies: - clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE - else: - clean_cookies[key] = val - - return clean_cookies - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "QueryDict": - return self.request.POST - - def files(self) -> "MultiValueDict": - return self.request.FILES - - def size_of_file(self, file: "Any") -> int: - return file.size - - def parsed_body(self) -> "Optional[Dict[str, Any]]": - try: - return self.request.data - except Exception: - return RequestExtractor.parsed_body(self) - - -def _set_user_info(request: "WSGIRequest", event: "Event") -> None: - user_info = event.setdefault("user", {}) - - user = getattr(request, "user", None) - - if user is None or not is_authenticated(user): - return - - try: - user_info.setdefault("id", str(user.pk)) - except Exception: - pass - - try: - user_info.setdefault("email", user.email) - except Exception: - pass - - try: - user_info.setdefault("username", user.get_username()) - except Exception: - pass - - -def install_sql_hook() -> None: - """If installed this causes Django's queries to be captured.""" - try: - from django.db.backends.utils import CursorWrapper - except ImportError: - from django.db.backends.util import CursorWrapper - - try: - # django 1.6 and 1.7 compatability - from django.db.backends import BaseDatabaseWrapper - except ImportError: - # django 1.8 or later - from django.db.backends.base.base import BaseDatabaseWrapper - - try: - real_execute = CursorWrapper.execute - real_executemany = CursorWrapper.executemany - real_connect = BaseDatabaseWrapper.connect - real_commit = BaseDatabaseWrapper._commit - real_rollback = BaseDatabaseWrapper._rollback - except AttributeError: - # This won't work on Django versions < 1.6 - return - - @ensure_integration_enabled(DjangoIntegration, real_execute) - def execute( - self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=params, - paramstyle="format", - executemany=False, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - result = real_execute(self, sql, params) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_executemany) - def executemany( - self: "CursorWrapper", sql: "Any", param_list: "List[Any]" - ) -> "Any": - with record_sql_queries( - cursor=self.cursor, - query=sql, - params_list=param_list, - paramstyle="format", - executemany=True, - span_origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - - result = real_executemany(self, sql, param_list) - - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if not isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - return result - - @ensure_integration_enabled(DjangoIntegration, real_connect) - def connect(self: "BaseDatabaseWrapper") -> None: - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message="connect", category="query") - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="connect", - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self) - return real_connect(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - return real_connect(self) - - def _commit(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_commit(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_COMMIT, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_COMMIT, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) - - def _rollback(self: "BaseDatabaseWrapper") -> None: - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - - if integration is None or not integration.db_transaction_spans: - return real_rollback(self) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=SPANNAME.DB_ROLLBACK, - attributes={ - "sentry.op": OP.DB, - "sentry.origin": DjangoIntegration.origin_db, - }, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - else: - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_ROLLBACK, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) - - CursorWrapper.execute = execute - CursorWrapper.executemany = executemany - BaseDatabaseWrapper.connect = connect - BaseDatabaseWrapper._commit = _commit - BaseDatabaseWrapper._rollback = _rollback - ignore_logger("django.db.backends") - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", - cursor_or_db: "Any", - db_operation: "Optional[str]" = None, -) -> None: - db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db - vendor = db.vendor - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) - - if db_operation is not None: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) - else: - span.set_data(SPANDATA.DB_SYSTEM, vendor) - - if db_operation is not None: - span.set_data(SPANDATA.DB_OPERATION, db_operation) - - # Some custom backends override `__getattr__`, making it look like `cursor_or_db` - # actually has a `connection` and the `connection` has a `get_dsn_parameters` - # attribute, only to throw an error once you actually want to call it. - # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable - # function. - is_psycopg2 = ( - hasattr(cursor_or_db, "connection") - and hasattr(cursor_or_db.connection, "get_dsn_parameters") - and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters) - ) - if is_psycopg2: - connection_params = cursor_or_db.connection.get_dsn_parameters() - else: - try: - # psycopg3, only extract needed params as get_parameters - # can be slow because of the additional logic to filter out default - # values - connection_params = { - "dbname": cursor_or_db.connection.info.dbname, - "port": cursor_or_db.connection.info.port, - } - # PGhost returns host or base dir of UNIX socket as an absolute path - # starting with /, use it only when it contains host - pg_host = cursor_or_db.connection.info.host - if pg_host and not pg_host.startswith("/"): - connection_params["host"] = pg_host - except Exception: - connection_params = db.get_connection_params() - - db_name = connection_params.get("dbname") or connection_params.get("database") - - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - - set_on_span = span.set_attribute - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - set_on_span = span.set_data - - server_address = connection_params.get("host") - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = connection_params.get("port") - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, str(server_port)) - - server_socket_address = connection_params.get("unix_socket") - if server_socket_address is not None: - set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) - - -def add_template_context_repr_sequence() -> None: - try: - from django.template.context import BaseContext - - add_repr_sequence_type(BaseContext) - except Exception: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py deleted file mode 100644 index 43faffb5be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/asgi.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Instrumentation for Django 3.0 - -Since this file contains `async def` it is conditionally imported in -`sentry_sdk.integrations.django` (depending on the existence of -`django.core.handlers.asgi`. -""" - -import asyncio -import functools -import inspect -from typing import TYPE_CHECKING - -from django.core.handlers.wsgi import WSGIRequest - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, TypeVar, Union - - from django.core.handlers.asgi import ASGIRequest - from django.http.response import HttpResponse - - from sentry_sdk._types import Event, EventProcessor - - _F = TypeVar("_F", bound=Callable[..., Any]) - - -# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for -# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. -# The latter is replaced with the inspect.markcoroutinefunction decorator. -# Until 3.12 is the minimum supported Python version, provide a shim. -# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py -if hasattr(inspect, "markcoroutinefunction"): - iscoroutinefunction = inspect.iscoroutinefunction - markcoroutinefunction = inspect.markcoroutinefunction -else: - iscoroutinefunction = asyncio.iscoroutinefunction - - def markcoroutinefunction(func: "_F") -> "_F": - func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore - return func - - -def _make_asgi_request_event_processor(request: "ASGIRequest") -> "EventProcessor": - def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - from sentry_sdk.integrations.django import ( - DjangoRequestExtractor, - _set_user_info, - ) - - if request is None: - return event - - if type(request) == WSGIRequest: - return event - - with capture_internal_exceptions(): - DjangoRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _set_user_info(request, event) - - return event - - return asgi_request_event_processor - - -def patch_django_asgi_handler_impl(cls: "Any") -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - )._run_asgi3 - - return await middleware(scope, receive, send) - - cls.__call__ = sentry_patched_asgi_handler - - modern_django_asgi_support = hasattr(cls, "create_request") - if modern_django_asgi_support: - old_create_request = cls.create_request - - @ensure_integration_enabled(DjangoIntegration, old_create_request) - def sentry_patched_create_request( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - request, error_response = old_create_request(self, *args, **kwargs) - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_make_asgi_request_event_processor(request)) - - return request, error_response - - cls.create_request = sentry_patched_create_request - - -def patch_get_response_async(cls: "Any", _before_get_response: "Any") -> None: - old_get_response_async = cls.get_response_async - - async def sentry_patched_get_response_async( - self: "Any", request: "Any" - ) -> "Union[HttpResponse, BaseException]": - _before_get_response(request) - return await old_get_response_async(self, request) - - cls.get_response_async = sentry_patched_get_response_async - - -def patch_channels_asgi_handler_impl(cls: "Any") -> None: - import channels # type: ignore - - from sentry_sdk.integrations.django import DjangoIntegration - - if channels.__version__ < "3.0.0": - old_app = cls.__call__ - - async def sentry_patched_asgi_handler( - self: "Any", receive: "Any", send: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return await old_app(self, receive, send) - - middleware = SentryAsgiMiddleware( - lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, - span_origin=DjangoIntegration.origin, - http_methods_to_capture=integration.http_methods_to_capture, - ) - - return await middleware(self.scope)(receive, send) # type: ignore - - cls.__call__ = sentry_patched_asgi_handler - - else: - # The ASGI handler in Channels >= 3 has the same signature as - # the Django handler. - patch_django_asgi_handler_impl(cls) - - -def wrap_async_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - async def sentry_wrapped_callback( - request: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return await callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return await callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return await callback(request, *args, **kwargs) - - return sentry_wrapped_callback - - -def _asgi_middleware_mixin_factory( - _check_middleware_span: "Callable[..., Any]", -) -> "Any": - """ - Mixin class factory that generates a middleware mixin for handling requests - in async mode. - """ - - class SentryASGIMixin: - if TYPE_CHECKING: - _inner = None - - def __init__(self, get_response: "Callable[..., Any]") -> None: - self.get_response = get_response - self._acall_method = None - self._async_check() - - def _async_check(self) -> None: - """ - If get_response is a coroutine function, turns us into async mode so - a thread is not consumed during a whole request. - Taken from django.utils.deprecation::MiddlewareMixin._async_check - """ - if iscoroutinefunction(self.get_response): - markcoroutinefunction(self) - - def async_route_check(self) -> bool: - """ - Function that checks if we are in async mode, - and if we are forwards the handling of requests to __acall__ - """ - return iscoroutinefunction(self.get_response) - - async def __acall__(self, *args: "Any", **kwargs: "Any") -> "Any": - f = self._acall_method - if f is None: - if hasattr(self._inner, "__acall__"): - self._acall_method = f = self._inner.__acall__ # type: ignore - else: - self._acall_method = f = self._inner - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return await f(*args, **kwargs) # type: ignore - - with middleware_span: - return await f(*args, **kwargs) # type: ignore - - return SentryASGIMixin diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py deleted file mode 100644 index faf1803c11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/caching.py +++ /dev/null @@ -1,264 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.core.cache import CacheHandler -from urllib3.util import parse_url as urlparse - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional - - -METHODS_TO_INSTRUMENT = [ - "set", - "set_many", - "get", - "get_many", -] - - -def _get_span_description( - method_name: str, args: "tuple[Any]", kwargs: "dict[str, Any]" -) -> str: - return _key_as_string(_get_safe_key(method_name, args, kwargs)) - - -def _patch_cache_method( - cache: "CacheHandler", - method_name: str, - address: "Optional[str]", - port: "Optional[int]", -) -> None: - from sentry_sdk.integrations.django import DjangoIntegration - - original_method = getattr(cache, method_name) - - @ensure_integration_enabled(DjangoIntegration, original_method) - def _instrument_call( - cache: "CacheHandler", - method_name: str, - original_method: "Callable[..., Any]", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - address: "Optional[str]", - port: "Optional[int]", - ) -> "Any": - is_set_operation = method_name.startswith("set") - is_get_method = method_name == "get" - is_get_many_method = method_name == "get_many" - - op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET - description = _get_span_description(method_name, args, kwargs) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": op, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_attribute(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_attribute(SPANDATA.CACHE_HIT, True) - else: - span.set_attribute(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - else: - with sentry_sdk.start_span( - op=op, - name=description, - origin=DjangoIntegration.origin, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_data(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_data(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value - - @functools.wraps(original_method) - def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": - return _instrument_call( - cache, method_name, original_method, args, kwargs, address, port - ) - - setattr(cache, method_name, sentry_method) - - -def _patch_cache( - cache: "CacheHandler", address: "Optional[str]" = None, port: "Optional[int]" = None -) -> None: - if not hasattr(cache, "_sentry_patched"): - for method_name in METHODS_TO_INSTRUMENT: - _patch_cache_method(cache, method_name, address, port) - cache._sentry_patched = True - - -def _get_address_port( - settings: "dict[str, Any]", -) -> "tuple[Optional[str], Optional[int]]": - location = settings.get("LOCATION") - - # TODO: location can also be an array of locations - # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis - # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 - if not isinstance(location, str): - return None, None - - if "://" in location: - parsed_url = urlparse(location) - # remove the username and password from URL to not leak sensitive data. - address = "{}://{}{}".format( - parsed_url.scheme or "", - parsed_url.hostname or "", - parsed_url.path or "", - ) - port = parsed_url.port - else: - address = location - port = None - - return address, int(port) if port is not None else None - - -def should_enable_cache_spans() -> bool: - from sentry_sdk.integrations.django import DjangoIntegration - - client = sentry_sdk.get_client() - integration = client.get_integration(DjangoIntegration) - from django.conf import settings - - return integration is not None and ( - (client.spotlight is not None and settings.DEBUG is True) - or integration.cache_spans is True - ) - - -def patch_caching() -> None: - if not hasattr(CacheHandler, "_sentry_patched"): - if DJANGO_VERSION < (3, 2): - original_get_item = CacheHandler.__getitem__ - - @functools.wraps(original_get_item) - def sentry_get_item(self: "CacheHandler", alias: str) -> "Any": - cache = original_get_item(self, alias) - - if should_enable_cache_spans(): - from django.conf import settings - - address, port = _get_address_port( - settings.CACHES[alias or "default"] - ) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.__getitem__ = sentry_get_item - CacheHandler._sentry_patched = True - - else: - original_create_connection = CacheHandler.create_connection - - @functools.wraps(original_create_connection) - def sentry_create_connection(self: "CacheHandler", alias: str) -> "Any": - cache = original_create_connection(self, alias) - - if should_enable_cache_spans(): - address, port = _get_address_port(self.settings[alias or "default"]) - - _patch_cache(cache, address, port) - - return cache - - CacheHandler.create_connection = sentry_create_connection - CacheHandler._sentry_patched = True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py deleted file mode 100644 index a14ec96ff5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/middleware.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Create spans from Django middleware invocations -""" - -from functools import wraps -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - F = TypeVar("F", bound=Callable[..., Any]) - -_import_string_should_wrap_middleware = ContextVar( - "import_string_should_wrap_middleware" -) - -DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1) - -if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE: - _asgi_middleware_mixin_factory = lambda _: object - iscoroutinefunction = lambda _: False -else: - from .asgi import _asgi_middleware_mixin_factory, iscoroutinefunction - - -def patch_django_middlewares() -> None: - from django.core.handlers import base - - old_import_string = base.import_string - - def sentry_patched_import_string(dotted_path: str) -> "Any": - rv = old_import_string(dotted_path) - - if _import_string_should_wrap_middleware.get(None): - rv = _wrap_middleware(rv, dotted_path) - - return rv - - base.import_string = sentry_patched_import_string - - old_load_middleware = base.BaseHandler.load_middleware - - def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": - _import_string_should_wrap_middleware.set(True) - try: - return old_load_middleware(*args, **kwargs) - finally: - _import_string_should_wrap_middleware.set(False) - - base.BaseHandler.load_middleware = sentry_patched_load_middleware - - -def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - def _check_middleware_span( - old_method: "Callable[..., Any]", - ) -> "Optional[Union[Span, StreamedSpan]]": - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None or not integration.middleware_spans: - return None - - function_name = transaction_from_function(old_method) - - description = middleware_name - function_basename = getattr(old_method, "__name__", None) - if function_basename: - description = "{}.{}".format(description, function_basename) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - middleware_span: "Union[Span, StreamedSpan]" - if span_streaming: - middleware_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.MIDDLEWARE_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - else: - middleware_span = sentry_sdk.start_span( - op=OP.MIDDLEWARE_DJANGO, - name=description, - origin=DjangoIntegration.origin, - ) - middleware_span.set_tag("django.function_name", function_name) - middleware_span.set_tag("django.middleware_name", middleware_name) - - return middleware_span - - def _get_wrapped_method(old_method: "F") -> "F": - with capture_internal_exceptions(): - # Middleware hooks (e.g. `process_view`, `process_exception`) may be - # `async def` when the middleware is async. A synchronous wrapper - # would hide the coroutine from Django's `iscoroutinefunction` check, - # causing Django to call the hook synchronously and never await the - # returned coroutine. Wrap async hooks with an async wrapper so the - # wrapped method continues to report as a coroutine function. - if iscoroutinefunction is not None and iscoroutinefunction(old_method): - - async def async_sentry_wrapped_method( - *args: "Any", **kwargs: "Any" - ) -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return await old_method(*args, **kwargs) - - with middleware_span: - return await old_method(*args, **kwargs) - - sentry_wrapped_method = async_sentry_wrapped_method - - else: - - def sync_sentry_wrapped_method(*args: "Any", **kwargs: "Any") -> "Any": - middleware_span = _check_middleware_span(old_method) - - if middleware_span is None: - return old_method(*args, **kwargs) - - with middleware_span: - return old_method(*args, **kwargs) - - sentry_wrapped_method = sync_sentry_wrapped_method - - try: - # fails for __call__ of function on Python 2 (see py2.7-django-1.11) - sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method) - - # Necessary for Django 3.1 - sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore - except Exception: - pass - - return sentry_wrapped_method # type: ignore - - return old_method - - class SentryWrappingMiddleware( - _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore - ): - sync_capable = getattr(middleware, "sync_capable", True) - async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr( - middleware, "async_capable", False - ) - - def __init__( - self, - get_response: "Optional[Callable[..., Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> None: - if get_response: - self._inner = middleware(get_response, *args, **kwargs) - else: - self._inner = middleware(*args, **kwargs) - self.get_response = get_response - self._call_method = None - if self.async_capable: - super().__init__(get_response) - - # We need correct behavior for `hasattr()`, which we can only determine - # when we have an instance of the middleware we're wrapping. - def __getattr__(self, method_name: str) -> "Any": - if method_name not in ( - "process_request", - "process_view", - "process_template_response", - "process_response", - "process_exception", - ): - raise AttributeError() - - old_method = getattr(self._inner, method_name) - rv = _get_wrapped_method(old_method) - self.__dict__[method_name] = rv - return rv - - def __call__(self, *args: "Any", **kwargs: "Any") -> "Any": - if hasattr(self, "async_route_check") and self.async_route_check(): - return self.__acall__(*args, **kwargs) - - f = self._call_method - if f is None: - self._call_method = f = self._inner.__call__ - - middleware_span = _check_middleware_span(old_method=f) - - if middleware_span is None: - return f(*args, **kwargs) - - with middleware_span: - return f(*args, **kwargs) - - for attr in ( - "__name__", - "__module__", - "__qualname__", - ): - if hasattr(middleware, attr): - setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr)) - - return SentryWrappingMiddleware diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py deleted file mode 100644 index 7140ead782..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/signals_handlers.py +++ /dev/null @@ -1,105 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from django.dispatch import Signal - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.django import DJANGO_VERSION -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Union - - -def _get_receiver_name(receiver: "Callable[..., Any]") -> str: - name = "" - - if hasattr(receiver, "__qualname__"): - name = receiver.__qualname__ - elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__ - name = receiver.__name__ - elif hasattr( - receiver, "func" - ): # certain functions (like partials) dont have a name - if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"): - name = "partial()" - - if ( - name == "" - ): # In case nothing was found, return the string representation (this is the slowest case) - return str(receiver) - - if hasattr(receiver, "__module__"): # prepend with module, if there is one - name = receiver.__module__ + "." + name - - return name - - -def patch_signals() -> None: - """ - Patch django signal receivers to create a span. - - This only wraps sync receivers. Django>=5.0 introduced async receivers, but - since we don't create transactions for ASGI Django, we don't wrap them. - """ - from sentry_sdk.integrations.django import DjangoIntegration - - old_live_receivers = Signal._live_receivers - - def _sentry_live_receivers( - self: "Signal", sender: "Any" - ) -> "Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]": - if DJANGO_VERSION >= (5, 0): - sync_receivers, async_receivers = old_live_receivers(self, sender) - else: - sync_receivers = old_live_receivers(self, sender) - async_receivers = [] - - def sentry_sync_receiver_wrapper( - receiver: "Callable[..., Any]", - ) -> "Callable[..., Any]": - @wraps(receiver) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - signal_name = _get_receiver_name(receiver) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - with sentry_sdk.traces.start_span( - name=signal_name, - attributes={ - "sentry.op": OP.EVENT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - SPANDATA.CODE_FUNCTION_NAME: signal_name, - }, - ) as span: - return receiver(*args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.EVENT_DJANGO, - name=signal_name, - origin=DjangoIntegration.origin, - ) as span: - span.set_data("signal", signal_name) - return receiver(*args, **kwargs) - - return wrapper - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if ( - integration - and integration.signals_spans - and self not in integration.signals_denylist - ): - for idx, receiver in enumerate(sync_receivers): - sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver) - - if DJANGO_VERSION >= (5, 0): - return sync_receivers, async_receivers - else: - return sync_receivers - - Signal._live_receivers = _sentry_live_receivers diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py deleted file mode 100644 index 5e23c258fb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/tasks.py +++ /dev/null @@ -1,52 +0,0 @@ -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import qualname_from_function - -try: - # django.tasks were added in Django 6.0 - from django.tasks.base import Task -except ImportError: - Task = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -def patch_tasks() -> None: - if Task is None: - return - - old_task_enqueue = Task.enqueue - - @wraps(old_task_enqueue) - def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is None: - return old_task_enqueue(self, *args, **kwargs) - - name = qualname_from_function(self.func) or "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_DJANGO, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_task_enqueue(self, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin - ): - return old_task_enqueue(self, *args, **kwargs) - - Task.enqueue = _sentry_enqueue diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py deleted file mode 100644 index 5ab89d4a74..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/templates.py +++ /dev/null @@ -1,209 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -from django import VERSION as DJANGO_VERSION -from django.template import TemplateSyntaxError -from django.utils.safestring import mark_safe - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Optional, Tuple - -try: - # support Django 1.9 - from django.template.base import Origin -except ImportError: - # backward compatibility - from django.template.loader import LoaderOrigin as Origin - - -def get_template_frame_from_exception( - exc_value: "Optional[BaseException]", -) -> "Optional[Dict[str, Any]]": - # As of Django 1.9 or so the new template debug thing showed up. - if hasattr(exc_value, "template_debug"): - return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore - - # As of r16833 (Django) all exceptions may contain a - # ``django_template_source`` attribute (rather than the legacy - # ``TemplateSyntaxError.source`` check) - if hasattr(exc_value, "django_template_source"): - return _get_template_frame_from_source( - exc_value.django_template_source # type: ignore - ) - - if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): - source = exc_value.source - if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): - return _get_template_frame_from_source(source) # type: ignore - - return None - - -def _get_template_name_description(template_name: str) -> str: - if isinstance(template_name, (list, tuple)): - if template_name: - return "[{}, ...]".format(template_name[0]) - else: - return template_name - - -def patch_templates() -> None: - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - real_rendered_content = SimpleTemplateResponse.rendered_content - - @property # type: ignore - @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) - def rendered_content(self: "SimpleTemplateResponse") -> str: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(self.template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_rendered_content.fget(self) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(self.template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", self.context_data) - return real_rendered_content.fget(self) - - SimpleTemplateResponse.rendered_content = rendered_content - - if DJANGO_VERSION < (1, 7): - return - import django.shortcuts - - real_render = django.shortcuts.render - - @functools.wraps(real_render) - @ensure_integration_enabled(DjangoIntegration, real_render) - def render( - request: "django.http.HttpRequest", - template_name: str, - context: "Optional[Dict[str, Any]]" = None, - *args: "Any", - **kwargs: "Any", - ) -> "django.http.HttpResponse": - # Inject trace meta tags into template context - context = context or {} - if "sentry_trace_meta" not in context: - context["sentry_trace_meta"] = mark_safe( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=_get_template_name_description(template_name), - attributes={ - "sentry.op": OP.TEMPLATE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ) as span: - return real_render(request, template_name, context, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", context) - return real_render(request, template_name, context, *args, **kwargs) - - django.shortcuts.render = render - - -def _get_template_frame_from_debug(debug: "Dict[str, Any]") -> "Dict[str, Any]": - if debug is None: - return None - - lineno = debug["line"] - filename = debug["name"] - if filename is None: - filename = "" - - pre_context = [] - post_context = [] - context_line = None - - for i, line in debug["source_lines"]: - if i < lineno: - pre_context.append(line) - elif i > lineno: - post_context.append(line) - else: - context_line = line - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - "in_app": True, - } - - -def _linebreak_iter(template_source: str) -> "Iterator[int]": - yield 0 - p = template_source.find("\n") - while p >= 0: - yield p + 1 - p = template_source.find("\n", p + 1) - - -def _get_template_frame_from_source( - source: "Tuple[Origin, Tuple[int, int]]", -) -> "Optional[Dict[str, Any]]": - if not source: - return None - - origin, (start, end) = source - filename = getattr(origin, "loadname", None) - if filename is None: - filename = "" - template_source = origin.reload() - lineno = None - upto = 0 - pre_context = [] - post_context = [] - context_line = None - - for num, next in enumerate(_linebreak_iter(template_source)): - line = template_source[upto:next] - if start >= upto and end <= next: - lineno = num - context_line = line - elif lineno is None: - pre_context.append(line) - else: - post_context.append(line) - - upto = next - - if context_line is None or lineno is None: - return None - - return { - "filename": filename, - "lineno": lineno, - "pre_context": pre_context[-5:], - "post_context": post_context[:5], - "context_line": context_line, - } diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py deleted file mode 100644 index 192f0765e6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/transactions.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Copied from raven-python. - -Despite being called "legacy" in some places this resolver is very much still -in use. -""" - -import re -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from re import Pattern - from typing import Dict, List, Optional, Tuple, Union - - from django.urls.resolvers import URLPattern, URLResolver - -from django import VERSION as DJANGO_VERSION - -if DJANGO_VERSION >= (2, 0): - from django.urls.resolvers import RoutePattern -else: - RoutePattern = None - -try: - from django.urls import get_resolver -except ImportError: - from django.core.urlresolvers import get_resolver - - -def get_regex(resolver_or_pattern: "Union[URLPattern, URLResolver]") -> "Pattern[str]": - """Utility method for django's deprecated resolver.regex""" - try: - regex = resolver_or_pattern.regex - except AttributeError: - regex = resolver_or_pattern.pattern.regex - return regex - - -class RavenResolver: - _new_style_group_matcher = re.compile( - r"<(?:([^>:]+):)?([^>]+)>" - ) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247 - _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)") - _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+") - _non_named_group_matcher = re.compile(r"\([^\)]+\)") - # [foo|bar|baz] - _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") - _camel_re = re.compile(r"([A-Z]+)([a-z])") - - _cache: "Dict[URLPattern, str]" = {} - - def _simplify(self, pattern: "Union[URLPattern, URLResolver]") -> str: - r""" - Clean up urlpattern regexes into something readable by humans: - - From: - > "^(?P\w+)/athletes/(?P\w+)/$" - - To: - > "{sport_slug}/athletes/{athlete_slug}/" - """ - # "new-style" path patterns can be parsed directly without turning them - # into regexes first - if ( - RoutePattern is not None - and hasattr(pattern, "pattern") - and isinstance(pattern.pattern, RoutePattern) - ): - return self._new_style_group_matcher.sub( - lambda m: "{%s}" % m.group(2), str(pattern.pattern._route) - ) - - result = get_regex(pattern).pattern - - # remove optional params - # TODO(dcramer): it'd be nice to change these into [%s] but it currently - # conflicts with the other rules because we're doing regexp matches - # rather than parsing tokens - result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result) - - # handle named groups first - result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result) - - # handle non-named groups - result = self._non_named_group_matcher.sub("{var}", result) - - # handle optional params - result = self._either_option_matcher.sub(lambda m: m.group(1), result) - - # clean up any outstanding regex-y characters. - result = ( - result.replace("^", "") - .replace("$", "") - .replace("?", "") - .replace("\\A", "") - .replace("\\Z", "") - .replace("//", "/") - .replace("\\", "") - ) - - return result - - def _resolve( - self, - resolver: "URLResolver", - path: str, - parents: "Optional[List[URLResolver]]" = None, - ) -> "Optional[str]": - match = get_regex(resolver).search(path) # Django < 2.0 - - if not match: - return None - - if parents is None: - parents = [resolver] - elif resolver not in parents: - parents = parents + [resolver] - - new_path = path[match.end() :] - for pattern in resolver.url_patterns: - # this is an include() - if not pattern.callback: - match_ = self._resolve(pattern, new_path, parents) - if match_: - return match_ - continue - elif not get_regex(pattern).search(new_path): - continue - - try: - return self._cache[pattern] - except KeyError: - pass - - prefix = "".join(self._simplify(p) for p in parents) - result = prefix + self._simplify(pattern) - if not result.startswith("/"): - result = "/" + result - self._cache[pattern] = result - return result - - return None - - def resolve( - self, - path: str, - urlconf: "Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]" = None, - ) -> "Optional[str]": - resolver = get_resolver(urlconf) - match = self._resolve(resolver, path) - return match - - -LEGACY_RESOLVER = RavenResolver() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py deleted file mode 100644 index cf3012a75e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/django/views.py +++ /dev/null @@ -1,127 +0,0 @@ -import functools -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -try: - from asyncio import iscoroutinefunction -except ImportError: - iscoroutinefunction = None # type: ignore - - -try: - from sentry_sdk.integrations.django.asgi import wrap_async_view -except (ImportError, SyntaxError): - wrap_async_view = None # type: ignore - - -def patch_views() -> None: - from django.core.handlers.base import BaseHandler - from django.template.response import SimpleTemplateResponse - - from sentry_sdk.integrations.django import DjangoIntegration - - old_make_view_atomic = BaseHandler.make_view_atomic - old_render = SimpleTemplateResponse.render - - def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="serialize response", - attributes={ - "sentry.op": OP.VIEW_RESPONSE_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return old_render(self) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RESPONSE_RENDER, - name="serialize response", - origin=DjangoIntegration.origin, - ): - return old_render(self) - - @functools.wraps(old_make_view_atomic) - def sentry_patched_make_view_atomic( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - callback = old_make_view_atomic(self, *args, **kwargs) - - # XXX: The wrapper function is created for every request. Find more - # efficient way to wrap views (or build a cache?) - - integration = sentry_sdk.get_client().get_integration(DjangoIntegration) - if integration is not None: - is_async_view = ( - iscoroutinefunction is not None - and wrap_async_view is not None - and iscoroutinefunction(callback) - ) - if is_async_view: - sentry_wrapped_callback = wrap_async_view(callback) - else: - sentry_wrapped_callback = _wrap_sync_view(callback) - - else: - sentry_wrapped_callback = callback - - return sentry_wrapped_callback - - SimpleTemplateResponse.render = sentry_patched_render - BaseHandler.make_view_atomic = sentry_patched_make_view_atomic - - -def _wrap_sync_view(callback: "Any") -> "Any": - from sentry_sdk.integrations.django import DjangoIntegration - - @functools.wraps(callback) - def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - current_scope = sentry_sdk.get_current_scope() - if span_streaming: - current_span = current_scope.streamed_span - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - else: - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - # set the active thread id to the handler thread for sync views - # this isn't necessary for async views since that runs on main - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - integration = client.get_integration(DjangoIntegration) - if not integration or not integration.middleware_spans: - return callback(request, *args, **kwargs) - - if span_streaming: - with sentry_sdk.traces.start_span( - name=request.resolver_match.view_name, - attributes={ - "sentry.op": OP.VIEW_RENDER, - "sentry.origin": DjangoIntegration.origin, - }, - ): - return callback(request, *args, **kwargs) - else: - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return callback(request, *args, **kwargs) - - return sentry_wrapped_callback diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py deleted file mode 100644 index 310766ee3a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/dramatiq.py +++ /dev/null @@ -1,246 +0,0 @@ -import json -from typing import TypeVar - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import request_body_within_bounds -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - event_from_exception, -) - -R = TypeVar("R") - -try: - from dramatiq.broker import Broker - from dramatiq.errors import Retry - from dramatiq.message import Message - from dramatiq.middleware import Middleware, default_middleware -except ImportError: - raise DidNotEnable("Dramatiq is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Union - - from sentry_sdk._types import Event, Hint - - -class DramatiqIntegration(Integration): - """ - Dramatiq integration for Sentry - - Please make sure that you call `sentry_sdk.init` *before* initializing - your broker, as it monkey patches `Broker.__init__`. - - This integration was originally developed and maintained - by https://github.com/jacobsvante and later donated to the Sentry - project. - """ - - identifier = "dramatiq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_dramatiq_broker() - - -def _patch_dramatiq_broker() -> None: - original_broker__init__ = Broker.__init__ - - def sentry_patched_broker__init__( - self: "Broker", *args: "Any", **kw: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - - try: - middleware = kw.pop("middleware") - except KeyError: - # Unfortunately Broker and StubBroker allows middleware to be - # passed in as positional arguments, whilst RabbitmqBroker and - # RedisBroker does not. - if len(args) == 1: - middleware = args[0] - args = () - else: - middleware = None - - if middleware is None: - middleware = list(m() for m in default_middleware) - else: - middleware = list(middleware) - - if integration is not None: - middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)] - middleware.insert(0, SentryMiddleware()) - - kw["middleware"] = middleware - original_broker__init__(self, *args, **kw) - - Broker.__init__ = sentry_patched_broker__init__ - - -class SentryMiddleware(Middleware): # type: ignore[misc] - """ - A Dramatiq middleware that automatically captures and sends - exceptions to Sentry. - - This is automatically added to every instantiated broker via the - DramatiqIntegration. - """ - - SENTRY_HEADERS_NAME = "_sentry_headers" - - def before_enqueue( - self, broker: "Broker", message: "Message[R]", delay: int - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - message.options[self.SENTRY_HEADERS_NAME] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - - def before_process_message(self, broker: "Broker", message: "Message[R]") -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(DramatiqIntegration) - if integration is None: - return - - message._scope_manager = sentry_sdk.isolation_scope() - scope = message._scope_manager.__enter__() - scope.clear_breadcrumbs() - scope.set_extra("dramatiq_message_id", message.message_id) - scope.add_event_processor(_make_message_event_processor(message, integration)) - - sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {} - if "retries" in message.options: - # start new trace in case of retrying - sentry_headers = {} - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(sentry_headers) - span = sentry_sdk.traces.start_span( - name=message.actor_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_DRAMATIQ, - "sentry.origin": DramatiqIntegration.origin, - "sentry.span.source": SegmentSource.TASK.value, - }, - parent_span=None, - ) - message._sentry_span_ctx = span - else: - transaction = continue_trace( - sentry_headers, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - origin=DramatiqIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - sentry_sdk.start_transaction( - transaction, - name=message.actor_name, - op=OP.QUEUE_TASK_DRAMATIQ, - source=TransactionSource.TASK, - ) - transaction.__enter__() - message._sentry_span_ctx = transaction - - def after_process_message( - self, - broker: "Broker", - message: "Message[R]", - *, - result: "Optional[Any]" = None, - exception: "Optional[Exception]" = None, - ) -> None: - integration = sentry_sdk.get_client().get_integration(DramatiqIntegration) - if integration is None: - return - - actor = broker.get_actor(message.actor_name) - throws = message.options.get("throws") or actor.options.get("throws") - - scope_manager = message._scope_manager - span_ctx = getattr(message, "_sentry_span_ctx", None) - if span_ctx is None: - return None - - is_event_capture_required = ( - exception is not None - and not (throws and isinstance(exception, throws)) - and not isinstance(exception, Retry) - ) - if not is_event_capture_required: - # normal transaction finish - span_ctx.__exit__(None, None, None) - scope_manager.__exit__(None, None, None) - return - - event, hint = event_from_exception( - exception, # type: ignore[arg-type] - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": DramatiqIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - # transaction error - span_ctx.__exit__(type(exception), exception, None) - scope_manager.__exit__(type(exception), exception, None) - - after_skip_message = after_process_message - - -def _make_message_event_processor( - message: "Message[R]", integration: "DramatiqIntegration" -) -> "Callable[[Event, Hint], Optional[Event]]": - def inner(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - DramatiqMessageExtractor(message).extract_into_event(event) - - return event - - return inner - - -class DramatiqMessageExtractor: - def __init__(self, message: "Message[R]") -> None: - self.message_data = dict(message.asdict()) - - def content_length(self) -> int: - return len(json.dumps(self.message_data)) - - def extract_into_event(self, event: "Event") -> None: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - contexts = event.setdefault("contexts", {}) - request_info = contexts.setdefault("dramatiq", {}) - request_info["type"] = "dramatiq" - - data: "Optional[Union[AnnotatedValue, Dict[str, Any]]]" = None - if not request_body_within_bounds(client, self.content_length()): - data = AnnotatedValue.removed_because_over_size_limit() - else: - data = self.message_data - - request_info["data"] = data diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py deleted file mode 100644 index 6bbc61000d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/excepthook.py +++ /dev/null @@ -1,76 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - - -class ExcepthookIntegration(Integration): - identifier = "excepthook" - - always_run = False - - def __init__(self, always_run: bool = False) -> None: - if not isinstance(always_run, bool): - raise ValueError( - "Invalid value for always_run: %s (must be type boolean)" - % (always_run,) - ) - self.always_run = always_run - - @staticmethod - def setup_once() -> None: - sys.excepthook = _make_excepthook(sys.excepthook) - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - if _should_send(integration.always_run): - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "excepthook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook - - -def _should_send(always_run: bool = False) -> bool: - if always_run: - return True - - if hasattr(sys, "ps1"): - # Disable the excepthook for interactive Python shells, otherwise - # every typo gets sent to Sentry. - return False - - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py deleted file mode 100644 index 4473fcc435..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/executing.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - - -class ExecutingIntegration(Integration): - identifier = "executing" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - frame = tb.tb_frame - source = Source.for_frame(frame) - sentry_frame["function"] = source.code_qualname(frame.f_code) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py deleted file mode 100644 index 7a595bcf2a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/falcon.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - - from sentry_sdk._types import Event, EventProcessor - -# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers` -# and `falcon.API` to `falcon.App` - -try: - import falcon # type: ignore - from falcon import __version__ as FALCON_VERSION -except ImportError: - raise DidNotEnable("Falcon not installed") - -try: - import falcon.app_helpers # type: ignore - - falcon_helpers = falcon.app_helpers - falcon_app_class = falcon.App - FALCON3 = True -except ImportError: - import falcon.api_helpers # type: ignore - - falcon_helpers = falcon.api_helpers - falcon_app_class = falcon.API - FALCON3 = False - - -_FALCON_UNSET: "Optional[object]" = None -if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+ - with capture_internal_exceptions(): - from falcon.request import ( # type: ignore[import-not-found, no-redef] - _UNSET as _FALCON_UNSET, - ) - - -class FalconRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, Any]": - return self.request.env - - def cookies(self) -> "Dict[str, Any]": - return self.request.cookies - - def form(self) -> None: - return None # No such concept in Falcon - - def files(self) -> None: - return None # No such concept in Falcon - - def raw_data(self) -> "Optional[str]": - # As request data can only be read once we won't make this available - # to Sentry. Just send back a dummy string in case there was a - # content length. - # TODO(jmagnusson): Figure out if there's a way to support this - content_length = self.content_length() - if content_length > 0: - return "[REQUEST_CONTAINING_RAW_DATA]" - else: - return None - - def json(self) -> "Optional[Dict[str, Any]]": - # fallback to cached_media = None if self.request._media is not available - cached_media = None - with capture_internal_exceptions(): - # self.request._media is the cached self.request.media - # value. It is only available if self.request.media - # has already been accessed. Therefore, reading - # self.request._media will not exhaust the raw request - # stream (self.request.bounded_stream) because it has - # already been read if self.request._media is set. - cached_media = self.request._media - - if cached_media is not _FALCON_UNSET: - return cached_media - - return None - - -class SentryFalconMiddleware: - """Captures exceptions in Falcon requests and send to Sentry""" - - def process_request( - self, req: "Any", resp: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return - - scope = sentry_sdk.get_isolation_scope() - scope._name = "falcon" - scope.add_event_processor(_make_request_event_processor(req, integration)) - - def process_resource( - self, req: "Any", resp: "Any", resource: "Any", params: "Any" - ) -> None: - """ - Sets the segment name and source as the route is resolved when this runs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FalconIntegration) - if integration is None or not has_span_streaming_enabled(client.options): - return - - name_for_style = { - "uri_template": req.uri_template, - "path": req.path, - } - name = name_for_style[integration.transaction_style] - source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style] - sentry_sdk.set_transaction_name(name, source) - - -TRANSACTION_STYLE_VALUES = ("uri_template", "path") - - -class FalconIntegration(Integration): - identifier = "falcon" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "uri_template") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - version = parse_version(FALCON_VERSION) - _check_minimum_version(FalconIntegration, version) - - _patch_wsgi_app() - _patch_handle_exception() - _patch_prepare_middleware() - - -def _patch_wsgi_app() -> None: - original_wsgi_app = falcon_app_class.__call__ - - def sentry_patched_wsgi_app( - self: "falcon.API", env: "Any", start_response: "Any" - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is None: - return original_wsgi_app(self, env, start_response) - - sentry_wrapped = SentryWsgiMiddleware( - lambda envi, start_resp: original_wsgi_app(self, envi, start_resp), - span_origin=FalconIntegration.origin, - ) - - return sentry_wrapped(env, start_response) - - falcon_app_class.__call__ = sentry_patched_wsgi_app - - -def _patch_handle_exception() -> None: - original_handle_exception = falcon_app_class._handle_exception - - @ensure_integration_enabled(FalconIntegration, original_handle_exception) - def sentry_patched_handle_exception(self: "falcon.API", *args: "Any") -> "Any": - # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception - # method signature from `(ex, req, resp, params)` to - # `(req, resp, ex, params)` - ex = response = None - with capture_internal_exceptions(): - ex = next(argument for argument in args if isinstance(argument, Exception)) - response = next( - argument for argument in args if isinstance(argument, falcon.Response) - ) - - was_handled = original_handle_exception(self, *args) - - if ex is None or response is None: - # Both ex and response should have a non-None value at this point; otherwise, - # there is an error with the SDK that will have been captured in the - # capture_internal_exceptions block above. - return was_handled - - if _exception_leads_to_http_5xx(ex, response): - event, hint = event_from_exception( - ex, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "falcon", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return was_handled - - falcon_app_class._handle_exception = sentry_patched_handle_exception - - -def _patch_prepare_middleware() -> None: - original_prepare_middleware = falcon_helpers.prepare_middleware - - def sentry_patched_prepare_middleware( - middleware: "Any" = None, - independent_middleware: "Any" = False, - asgi: bool = False, - ) -> "Any": - if asgi: - # We don't support ASGI Falcon apps, so we don't patch anything here - return original_prepare_middleware(middleware, independent_middleware, asgi) - - integration = sentry_sdk.get_client().get_integration(FalconIntegration) - if integration is not None: - middleware = [SentryFalconMiddleware()] + (middleware or []) - - # We intentionally omit the asgi argument here, since the default is False anyways, - # and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions. - return original_prepare_middleware(middleware, independent_middleware) - - falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware - - -def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool: - is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith( - "5" - ) - is_unhandled_error = not isinstance( - ex, (falcon.HTTPError, falcon.http_status.HTTPStatus) - ) - - # We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response - # at the stage where we capture it is listed as 200, even though we would expect to see a 500 - # status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to - # only perform this check on Falcon 3+, despite the risk that some handled errors might be - # reported to Sentry as unhandled on Falcon 2. - return (is_server_error or is_unhandled_error) and ( - not FALCON3 or _has_http_5xx_status(response) - ) - - -def _has_http_5xx_status(response: "falcon.Response") -> bool: - return response.status.startswith("5") - - -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "falcon.Request" -) -> None: - name_for_style = { - "uri_template": request.uri_template, - "path": request.path, - } - event["transaction"] = name_for_style[transaction_style] - event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} - - -def _make_request_event_processor( - req: "falcon.Request", integration: "FalconIntegration" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, req) - - with capture_internal_exceptions(): - FalconRequestExtractor(req).extract_into_event(event) - - return event - - return event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py deleted file mode 100644 index c7b97c88b1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/fastapi.py +++ /dev/null @@ -1,201 +0,0 @@ -import sys -from copy import deepcopy -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import transaction_from_function - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Dict - - from sentry_sdk._types import Event - -try: - from sentry_sdk.integrations.starlette import ( - StarletteIntegration, - StarletteRequestExtractor, - _get_cached_request_body_attribute, - ) -except DidNotEnable: - raise DidNotEnable("Starlette is not installed") - -try: - import fastapi # type: ignore -except ImportError: - raise DidNotEnable("FastAPI is not installed") - - -_DEFAULT_TRANSACTION_NAME = "generic FastAPI request" - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -class FastApiIntegration(StarletteIntegration): - identifier = "fastapi" - - @staticmethod - def setup_once() -> None: - patch_get_request_handler() - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or "" - - elif transaction_style == "url": - route = request.scope.get("route") - - if route: - # FastAPI >= 0.137 stores the prefix-resolved path on an - # effective_route_context in scope["fastapi"], while - # scope["route"].path holds the unprefixed original. - # Prefer the effective context path when available. - effective_route_context = request.scope.get("fastapi", {}).get( - "effective_route_context" - ) - context_path = getattr(effective_route_context, "path", None) - - if context_path: - name = context_path - else: - path = getattr(route, "path", None) - if path is not None: - name = path - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - else: - source = SOURCE_FOR_STYLE[transaction_style] - - scope.set_transaction_name(name, source=source) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(FastApiIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, Dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = FastApiIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_get_request_handler() -> None: - old_get_request_handler = fastapi.routing.get_request_handler - - def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": - dependant = kwargs.get("dependant") - if ( - dependant - and dependant.call is not None - and not iscoroutinefunction(dependant.call) - # FastAPI >= 0.137 calls get_request_handler() on every request - # (router-tree traversal) rather than once at registration. Guard - # against accumulating _sentry_call wrappers on the shared - # dependant object, which would cause a RecursionError after ~987 - # requests as the call chain grows past Python's recursion limit. - and not getattr(dependant.call, "_sentry_is_patched", False) - ): - old_call = dependant.call - - @wraps(old_call) - def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": - current_scope = sentry_sdk.get_current_scope() - - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_call(*args, **kwargs) - - _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] - dependant.call = _sentry_call - - old_app = old_get_request_handler(*args, **kwargs) - - async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_app, *args, **kwargs) - - return _sentry_app - - fastapi.routing.get_request_handler = _sentry_get_request_handler diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py deleted file mode 100644 index 1902091fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/flask.py +++ /dev/null @@ -1,281 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - RequestExtractor, -) -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Union - - from werkzeug.datastructures import FileStorage, ImmutableMultiDict - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - - -try: - import flask_login # type: ignore -except ImportError: - flask_login = None - -try: - from flask import Flask, Request # type: ignore - from flask import request as flask_request - from flask.signals import ( - before_render_template, - got_request_exception, - request_started, - ) - from markupsafe import Markup -except ImportError: - raise DidNotEnable("Flask is not installed") - -try: - import blinker # noqa -except ImportError: - raise DidNotEnable("blinker is not installed") - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class FlaskIntegration(Integration): - identifier = "flask" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "endpoint", - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - @staticmethod - def setup_once() -> None: - try: - from quart import Quart # type: ignore - - if Flask == Quart: - # This is Quart masquerading as Flask, don't enable the Flask - # integration. See https://github.com/getsentry/sentry-python/issues/2709 - raise DidNotEnable( - "This is not a Flask app but rather Quart pretending to be Flask" - ) - except ImportError: - pass - - version = package_version("flask") - _check_minimum_version(FlaskIntegration, version) - - before_render_template.connect(_add_sentry_trace) - request_started.connect(_request_started) - got_request_exception.connect(_capture_exception) - - old_app = Flask.__call__ - - def sentry_patched_wsgi_app( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return old_app(self, environ, start_response) - - middleware = SentryWsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=FlaskIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - ) - return middleware(environ, start_response) - - Flask.__call__ = sentry_patched_wsgi_app - - -def _add_sentry_trace( - sender: "Flask", template: "Any", context: "Dict[str, Any]", **extra: "Any" -) -> None: - if "sentry_trace" in context: - return - - scope = sentry_sdk.get_current_scope() - trace_meta = Markup(scope.trace_propagation_meta()) - context["sentry_trace"] = trace_meta # for backwards compatibility - context["sentry_trace_meta"] = trace_meta - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - scope.set_transaction_name( - name_for_style[transaction_style], - source=SOURCE_FOR_STYLE[transaction_style], - ) - except Exception: - pass - - -def _request_started(app: "Flask", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(FlaskIntegration) - if integration is None: - return - - request = flask_request._get_current_object() - - # Set the transaction name and source here, - # but rely on WSGI middleware to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - scope.set_user(user_properties) - - evt_processor = _make_request_event_processor(app, request, integration) - scope.add_event_processor(evt_processor) - - -class FlaskRequestExtractor(RequestExtractor): - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "Dict[Any, Any]": - return { - k: v[0] if isinstance(v, list) and len(v) == 1 else v - for k, v in self.request.cookies.items() - } - - def raw_data(self) -> bytes: - return self.request.get_data() - - def form(self) -> "ImmutableMultiDict[str, Any]": - return self.request.form - - def files(self) -> "ImmutableMultiDict[str, Any]": - return self.request.files - - def is_json(self) -> bool: - return self.request.is_json - - def json(self) -> "Any": - return self.request.get_json(silent=True) - - def size_of_file(self, file: "FileStorage") -> int: - return file.content_length - - -def _make_request_event_processor( - app: "Flask", request: "Callable[[], Request]", integration: "FlaskIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - FlaskRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - _add_user_to_event(event) - - return event - - return inner - - -@ensure_integration_enabled(FlaskIntegration) -def _capture_exception( - sender: "Flask", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "flask", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_flask_user_properties() -> "Dict[str, str]": - if flask_login is None: - return {} - - user = flask_login.current_user - if user is None: - return {} - - properties = {} - - try: - user_id = user.get_id() - if user_id is not None: - properties["id"] = user_id - except AttributeError: - # might happen if: - # - flask_login could not be imported - # - flask_login is not configured - # - no user is logged in - pass - - # The following attribute accesses are ineffective for the general - # Flask-Login case, because the User interface of Flask-Login does not - # care about anything but the ID. However, Flask-User (based on - # Flask-Login) documents a few optional extra attributes. - # - # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names - try: - if user.email is not None: - properties["email"] = user.email - except Exception: - pass - - try: - if user.username is not None: - properties["username"] = user.username - except Exception: - pass - - return properties - - -def _add_user_to_event(event: "Event") -> None: - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - user_info = event.setdefault("user", {}) - for key, value in user_properties.items(): - user_info.setdefault(key, value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py deleted file mode 100644 index 91a62b3a81..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gcp.py +++ /dev/null @@ -1,286 +0,0 @@ -import functools -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -from os import environ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP -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 -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -# Constants -TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - - -def _wrap_func(func: "F") -> "F": - @functools.wraps(func) - def sentry_func( - functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(GcpIntegration) - if integration is None: - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = environ.get("FUNCTION_TIMEOUT_SEC") - if not configured_time: - logger.debug( - "The configured timeout could not be fetched from Cloud Functions configuration." - ) - return func(functionhandler, gcp_event, *args, **kwargs) - - configured_time = int(configured_time) - - initial_time = datetime.now(timezone.utc) - - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - gcp_event, configured_time, initial_time - ) - ) - scope.set_tag("gcp_region", environ.get("FUNCTION_REGION")) - timeout_thread = None - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = configured_time - TIMEOUT_WARNING_BUFFER - - timeout_thread = TimeoutThread( - waiting_time, - configured_time, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = {} - header_attributes: "dict[str, Any]" = {} - if hasattr(gcp_event, "headers"): - headers = gcp_event.headers - for header, header_value in _filter_headers( - headers, use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - # header_value will always be a string because we set `use_annotated_value` to false above - header_value - ) - - additional_attributes = {} - 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" - ) - - sampling_context = { - "gcp_env": { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - }, - "gcp_event": gcp_event, - } - - function_name = environ.get("FUNCTION_NAME", "") - - if environ.get("GCP_PROJECT"): - additional_attributes["gcp.project.id"] = environ.get("GCP_PROJECT") - - if environ.get("FUNCTION_IDENTITY"): - additional_attributes["faas.identity"] = environ.get( - "FUNCTION_IDENTITY" - ) - - if environ.get("ENTRY_POINT"): - additional_attributes["faas.entry_point"] = environ.get("ENTRY_POINT") - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace(headers) - Scope.set_custom_sampling_context(sampling_context) - span_ctx = sentry_sdk.traces.start_span( - name=function_name, - parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_GCP, - "sentry.origin": GcpIntegration.origin, - "sentry.span.source": SegmentSource.COMPONENT, - "cloud.provider": CLOUD_PROVIDER.GCP, - "faas.name": function_name, - **header_attributes, - **additional_attributes, - }, - ) - else: - transaction = continue_trace( - headers, - op=OP.FUNCTION_GCP, - name=environ.get("FUNCTION_NAME", ""), - source=TransactionSource.COMPONENT, - origin=GcpIntegration.origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, custom_sampling_context=sampling_context - ) - - with span_ctx: - try: - return func(functionhandler, gcp_event, *args, **kwargs) - except Exception: - exc_info = sys.exc_info() - sentry_event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "gcp", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - # Flush out the event queue - client.flush() - - return sentry_func # type: ignore - - -class GcpIntegration(Integration): - identifier = "gcp" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - import __main__ as gcp_functions - - if not hasattr(gcp_functions, "worker_v1"): - logger.warning( - "GcpIntegration currently supports only Python 3.7 runtime environment." - ) - return - - worker1 = gcp_functions.worker_v1 - - worker1.FunctionHandler.invoke_user_function = _wrap_func( - worker1.FunctionHandler.invoke_user_function - ) - - -def _make_request_event_processor( - gcp_event: "Any", configured_timeout: "Any", initial_time: "Any" -) -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - final_time = datetime.now(timezone.utc) - time_diff = final_time - initial_time - - execution_duration_in_millis = time_diff / timedelta(milliseconds=1) - - extra = event.setdefault("extra", {}) - extra["google cloud functions"] = { - "function_name": environ.get("FUNCTION_NAME"), - "function_entry_point": environ.get("ENTRY_POINT"), - "function_identity": environ.get("FUNCTION_IDENTITY"), - "function_region": environ.get("FUNCTION_REGION"), - "function_project": environ.get("GCP_PROJECT"), - "execution_duration_in_millis": execution_duration_in_millis, - "configured_timeout_in_seconds": configured_timeout, - } - - extra["google cloud logs"] = { - "url": _get_google_cloud_logs_url(final_time), - } - - request = event.get("request", {}) - - request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME")) - - if hasattr(gcp_event, "method"): - request["method"] = gcp_event.method - - if hasattr(gcp_event, "query_string"): - request["query_string"] = gcp_event.query_string.decode( - "utf-8", errors="replace" - ) - - if hasattr(gcp_event, "headers"): - request["headers"] = _filter_headers(gcp_event.headers) - - if should_send_default_pii(): - if hasattr(gcp_event, "data"): - request["data"] = gcp_event.data - else: - if hasattr(gcp_event, "data"): - # Unfortunately couldn't find a way to get structured body from GCP - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - event["request"] = deepcopy(request) - - return event - - return event_processor - - -def _get_google_cloud_logs_url(final_time: "datetime") -> str: - """ - Generates a Google Cloud Logs console URL based on the environment variables - Arguments: - final_time {datetime} -- Final time - Returns: - str -- Google Cloud Logs Console URL to logs. - """ - hour_ago = final_time - timedelta(hours=1) - formatstring = "%Y-%m-%dT%H:%M:%SZ" - - url = ( - "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function" - "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false" - "×tamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true" - "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}" - "&interval=PT1H&scrollTimestamp={timestamp_end}" - ).format( - project=environ.get("GCP_PROJECT"), - function_name=environ.get("FUNCTION_NAME"), - region=environ.get("FUNCTION_REGION"), - timestamp_end=final_time.strftime(formatstring), - timestamp_start=hour_ago.strftime(formatstring), - ) - - return url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py deleted file mode 100644 index 4be0f479bc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gnu_backtrace.py +++ /dev/null @@ -1,96 +0,0 @@ -import re -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - -# function is everything between index at @ -# and then we match on the @ plus the hex val -FUNCTION_RE = r"[^@]+?" -HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+" - -_FRAME_RE_PATTERN = r""" -^(?P\d+)\.\s+(?P{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P.+))?$ -""".format( - FUNCTION_RE=FUNCTION_RE, - HEX_ADDRESS=HEX_ADDRESS, -) - -FRAME_RE = re.compile(_FRAME_RE_PATTERN, re.MULTILINE | re.VERBOSE) - - -class GnuBacktraceIntegration(Integration): - identifier = "gnu_backtrace" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - return _process_gnu_backtrace(event, hint) - - -def _process_gnu_backtrace(event: "Event", hint: "dict[str, Any]") -> "Event": - if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception in values: - frames = exception.get("stacktrace", {}).get("frames", []) - if not frames: - continue - - msg = exception.get("value", None) - if not msg: - continue - - additional_frames = [] - new_msg = [] - - for line in msg.splitlines(): - match = FRAME_RE.match(line) - if match: - additional_frames.append( - ( - int(match.group("index")), - { - "package": match.group("package") or None, - "function": match.group("function") or None, - "platform": "native", - }, - ) - ) - else: - # Put garbage lines back into message, not sure what to do with them. - new_msg.append(line) - - if additional_frames: - additional_frames.sort(key=lambda x: -x[0]) - for _, frame in additional_frames: - frames.append(frame) - - new_msg.append("") - exception["value"] = "\n".join(new_msg) - - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py deleted file mode 100644 index 45652c3f71..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/__init__.py +++ /dev/null @@ -1,457 +0,0 @@ -from functools import wraps -from typing import ( - Any, - AsyncIterator, - Callable, - Iterator, - List, -) - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import SPANSTATUS -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.genai.models import AsyncModels, Models -except ImportError: - raise DidNotEnable("google-genai not installed") - - -from .consts import GEN_AI_SYSTEM, IDENTIFIER, ORIGIN -from .streaming import ( - accumulate_streaming_response, - set_span_data_for_streaming_response, -) -from .utils import ( - _capture_exception, - prepare_embed_content_args, - prepare_generate_content_args, - set_span_data_for_embed_request, - set_span_data_for_embed_response, - set_span_data_for_request, - set_span_data_for_response, -) - - -class GoogleGenAIIntegration(Integration): - identifier = IDENTIFIER - origin = ORIGIN - - def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Patch sync methods - Models.generate_content = _wrap_generate_content(Models.generate_content) - Models.generate_content_stream = _wrap_generate_content_stream( - Models.generate_content_stream - ) - Models.embed_content = _wrap_embed_content(Models.embed_content) - - # Patch async methods - AsyncModels.generate_content = _wrap_async_generate_content( - AsyncModels.generate_content - ) - AsyncModels.generate_content_stream = _wrap_async_generate_content_stream( - AsyncModels.generate_content_stream - ) - AsyncModels.embed_content = _wrap_async_embed_content(AsyncModels.embed_content) - - -def _wrap_generate_content_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = f(self, *args, **kwargs) - - # Create wrapper iterator to accumulate responses - def new_iterator() -> "Iterator[Any]": - chunks: "List[Any]" = [] - try: - for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_generate_content_stream - - -def _wrap_async_generate_content_stream( - f: "Callable[..., Any]", -) -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content_stream( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - _model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - chat_span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - else: - chat_span = get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) - chat_span.__enter__() - - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - chat_span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - set_span_data_for_request(chat_span, integration, model_name, contents, kwargs) - - try: - stream = await f(self, *args, **kwargs) - - # Create wrapper async iterator to accumulate responses - async def new_async_iterator() -> "AsyncIterator[Any]": - chunks: "List[Any]" = [] - try: - async for chunk in stream: - chunks.append(chunk) - yield chunk - except Exception as exc: - _capture_exception(exc) - if isinstance(chat_span, StreamedSpan): - chat_span.status = SpanStatus.ERROR - else: - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - finally: - # Accumulate all chunks and set final response data on spans - if chunks: - accumulated_response = accumulate_streaming_response(chunks) - set_span_data_for_streaming_response( - chat_span, integration, accumulated_response - ) - chat_span.__exit__(None, None, None) - - return new_async_iterator() - - except Exception as exc: - _capture_exception(exc) - chat_span.__exit__(None, None, None) - raise - - return new_async_generate_content_stream - - -def _wrap_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_generate_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_generate_content - - -def _wrap_async_generate_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_generate_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model, contents, model_name = prepare_generate_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as chat_span: - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.status = SpanStatus.ERROR - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=ORIGIN, - ) as chat_span: - chat_span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - chat_span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - chat_span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_request( - chat_span, integration, model_name, contents, kwargs - ) - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - chat_span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_response(chat_span, integration, response) - - return response - - return new_async_generate_content - - -def _wrap_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_embed_content(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_embed_content - - -def _wrap_async_embed_content(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_async_embed_content( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(GoogleGenAIIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name, contents = prepare_embed_content_args(args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - SPANDATA.GEN_AI_SYSTEM: GEN_AI_SYSTEM, - SPANDATA.GEN_AI_REQUEST_MODEL: model_name, - }, - ) as span: - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.status = SpanStatus.ERROR - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - else: - with get_start_span_function()( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}", - origin=ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - span.set_data(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - set_span_data_for_embed_request(span, integration, contents, kwargs) - - try: - response = await f(self, *args, **kwargs) - except Exception as exc: - _capture_exception(exc) - span.set_status(SPANSTATUS.INTERNAL_ERROR) - raise - - set_span_data_for_embed_response(span, integration, response) - - return response - - return new_async_embed_content diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py deleted file mode 100644 index 5b53ebf0e2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/consts.py +++ /dev/null @@ -1,16 +0,0 @@ -GEN_AI_SYSTEM = "gcp.gemini" - -# Mapping of tool attributes to their descriptions -# These are all tools that are available in the Google GenAI API -TOOL_ATTRIBUTES_MAP = { - "google_search_retrieval": "Google Search retrieval tool", - "google_search": "Google Search tool", - "retrieval": "Retrieval tool", - "enterprise_web_search": "Enterprise web search tool", - "google_maps": "Google Maps tool", - "code_execution": "Code execution tool", - "computer_use": "Computer use tool", -} - -IDENTIFIER = "google_genai" -ORIGIN = f"auto.ai.{IDENTIFIER}" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py deleted file mode 100644 index 8414ea4f21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/streaming.py +++ /dev/null @@ -1,172 +0,0 @@ -from typing import TYPE_CHECKING, Any, List, Optional, TypedDict, Union - -from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import ( - safe_serialize, -) - -from .utils import ( - UsageData, - extract_contents_text, - extract_finish_reasons, - extract_tool_calls, - extract_usage_data, -) - -if TYPE_CHECKING: - from google.genai.types import GenerateContentResponse - - from sentry_sdk.tracing import Span - - -class AccumulatedResponse(TypedDict): - id: "Optional[str]" - model: "Optional[str]" - text: str - finish_reasons: "List[str]" - tool_calls: "List[dict[str, Any]]" - usage_metadata: "Optional[UsageData]" - - -def element_wise_usage_max(self: "UsageData", other: "UsageData") -> "UsageData": - return UsageData( - input_tokens=max(self["input_tokens"], other["input_tokens"]), - output_tokens=max(self["output_tokens"], other["output_tokens"]), - input_tokens_cached=max( - self["input_tokens_cached"], other["input_tokens_cached"] - ), - output_tokens_reasoning=max( - self["output_tokens_reasoning"], other["output_tokens_reasoning"] - ), - total_tokens=max(self["total_tokens"], other["total_tokens"]), - ) - - -def accumulate_streaming_response( - chunks: "List[GenerateContentResponse]", -) -> "AccumulatedResponse": - """Accumulate streaming chunks into a single response-like object.""" - accumulated_text = [] - finish_reasons = [] - tool_calls = [] - usage_data = None - response_id = None - model = None - - for chunk in chunks: - # Extract text and tool calls - if getattr(chunk, "candidates", None): - for candidate in getattr(chunk, "candidates", []): - if hasattr(candidate, "content") and getattr( - candidate.content, "parts", [] - ): - extracted_text = extract_contents_text(candidate.content) - if extracted_text: - accumulated_text.append(extracted_text) - - extracted_finish_reasons = extract_finish_reasons(chunk) - if extracted_finish_reasons: - finish_reasons.extend(extracted_finish_reasons) - - extracted_tool_calls = extract_tool_calls(chunk) - if extracted_tool_calls: - tool_calls.extend(extracted_tool_calls) - - # Use last possible chunk, in case of interruption, and - # gracefully handle missing intermediate tokens by taking maximum - # with previous token reporting. - chunk_usage_data = extract_usage_data(chunk) - usage_data = ( - chunk_usage_data - if usage_data is None - else element_wise_usage_max(usage_data, chunk_usage_data) - ) - - accumulated_response = AccumulatedResponse( - text="".join(accumulated_text), - finish_reasons=finish_reasons, - tool_calls=tool_calls, - usage_metadata=usage_data, - id=response_id, - model=model, - ) - - return accumulated_response - - -def set_span_data_for_streaming_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - accumulated_response: "AccumulatedResponse", -) -> None: - """Set span data for accumulated streaming response.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if ( - should_send_default_pii() - and integration.include_prompts - and accumulated_response.get("text") - ): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TEXT, - safe_serialize([accumulated_response["text"]]), - ) - - if accumulated_response.get("finish_reasons"): - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - accumulated_response["finish_reasons"], - ) - - if accumulated_response.get("tool_calls"): - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(accumulated_response["tool_calls"]), - ) - - response_id = accumulated_response.get("id") - if response_id is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) - - response_model = accumulated_response.get("model") - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - if accumulated_response["usage_metadata"] is None: - return - - if accumulated_response["usage_metadata"]["input_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - accumulated_response["usage_metadata"]["input_tokens"], - ) - - if accumulated_response["usage_metadata"]["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - accumulated_response["usage_metadata"]["input_tokens_cached"], - ) - - if accumulated_response["usage_metadata"]["output_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - accumulated_response["usage_metadata"]["output_tokens"], - ) - - if accumulated_response["usage_metadata"]["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - accumulated_response["usage_metadata"]["output_tokens_reasoning"], - ) - - if accumulated_response["usage_metadata"]["total_tokens"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - accumulated_response["usage_metadata"]["total_tokens"], - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py deleted file mode 100644 index 464a812680..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/google_genai/utils.py +++ /dev/null @@ -1,1118 +0,0 @@ -import copy -import inspect -import json -from functools import wraps -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - TypedDict, - Union, -) - -from google.genai.types import Content, GenerateContentConfig, Part, PartDict - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.utils import ( - get_modality_from_mime_type, - normalize_message_roles, - set_data_normalized, - transform_google_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - safe_serialize, -) - -from .consts import GEN_AI_SYSTEM, ORIGIN, TOOL_ATTRIBUTES_MAP - -if TYPE_CHECKING: - from google.genai.types import ( - ContentListUnion, - ContentUnion, - ContentUnionDict, - EmbedContentResponse, - GenerateContentResponse, - Model, - Tool, - ) - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -_is_PIL_available = False -try: - from PIL import Image as PILImage # type: ignore[import-not-found] - - _is_PIL_available = True -except ImportError: - pass - -# Keys to use when checking to see if a dict provided by the user -# is Part-like (as opposed to a Content or multi-turn conversation entry). -_PART_DICT_KEYS = PartDict.__optional_keys__ - - -class UsageData(TypedDict): - """Structure for token usage data.""" - - input_tokens: int - input_tokens_cached: int - output_tokens: int - output_tokens_reasoning: int - total_tokens: int - - -def extract_usage_data( - response: "Union[GenerateContentResponse, dict[str, Any]]", -) -> "UsageData": - """Extract usage data from response into a structured format. - - Args: - response: The GenerateContentResponse object or dictionary containing usage metadata - - Returns: - UsageData: Dictionary with input_tokens, input_tokens_cached, - output_tokens, and output_tokens_reasoning fields - """ - usage_data = UsageData( - input_tokens=0, - input_tokens_cached=0, - output_tokens=0, - output_tokens_reasoning=0, - total_tokens=0, - ) - - # Handle dictionary response (from streaming) - if isinstance(response, dict): - usage = response.get("usage_metadata", {}) - if not usage: - return usage_data - - prompt_tokens = usage.get("prompt_token_count", 0) or 0 - tool_use_prompt_tokens = usage.get("tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - cached_tokens = usage.get("cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - reasoning_tokens = usage.get("thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - candidates_tokens = usage.get("candidates_token_count", 0) or 0 - # python-genai reports output and reasoning tokens separately - # reasoning should be sub-category of output tokens - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = usage.get("total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - if not hasattr(response, "usage_metadata"): - return usage_data - - usage = response.usage_metadata - - # Input tokens include both prompt and tool use prompt tokens - prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0 - tool_use_prompt_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0 - usage_data["input_tokens"] = prompt_tokens + tool_use_prompt_tokens - - # Cached input tokens - cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0 - usage_data["input_tokens_cached"] = cached_tokens - - # Reasoning tokens - reasoning_tokens = getattr(usage, "thoughts_token_count", 0) or 0 - usage_data["output_tokens_reasoning"] = reasoning_tokens - - # output_tokens = candidates_tokens + reasoning_tokens - # google-genai reports output and reasoning tokens separately - candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0 - usage_data["output_tokens"] = candidates_tokens + reasoning_tokens - - total_tokens = getattr(usage, "total_token_count", 0) or 0 - usage_data["total_tokens"] = total_tokens - - return usage_data - - -def _capture_exception(exc: "Any") -> None: - """Capture exception with Google GenAI mechanism.""" - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "google_genai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def get_model_name(model: "Union[str, Model]") -> str: - """Extract model name from model parameter.""" - if isinstance(model, str): - return model - # Handle case where model might be an object with a name attribute - if hasattr(model, "name"): - return str(model.name) - return str(model) - - -def extract_contents_messages(contents: "ContentListUnion") -> "List[Dict[str, Any]]": - """Extract messages from contents parameter which can have various formats. - - Returns a list of message dictionaries in the format: - - System: {"role": "system", "content": "string"} - - User/Assistant: {"role": "user"|"assistant", "content": [{"text": "...", "type": "text"}, ...]} - """ - if contents is None: - return [] - - messages = [] - - # Handle string case - if isinstance(contents, str): - return [{"role": "user", "content": contents}] - - # Handle list case - if isinstance(contents, list): - if contents and all(_is_part_like(item) for item in contents): - # All items are parts — merge into a single multi-part user message - content_parts = [] - for item in contents: - part = _extract_part_from_item(item) - if part is not None: - content_parts.append(part) - - return [{"role": "user", "content": content_parts}] - else: - # Multi-turn conversation or mixed content types - for item in contents: - item_messages = extract_contents_messages(item) - messages.extend(item_messages) - return messages - - # Handle dictionary case (ContentDict) - if isinstance(contents, dict): - role = contents.get("role", "user") - parts = contents.get("parts") - - if parts: - content_parts = [] - tool_messages = [] - - for part in parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - # Tool message - add separately - tool_messages.append(part_result) - else: - # Regular content part - content_parts.append(part_result) - - # Add main message if we have content parts - if content_parts: - # Normalize role: "model" -> "assistant" - normalized_role = "assistant" if role == "model" else role or "user" - messages.append({"role": normalized_role, "content": content_parts}) - - # Add tool messages - messages.extend(tool_messages) - elif "text" in contents: - messages.append( - { - "role": role, - "content": [{"text": contents["text"], "type": "text"}], - } - ) - elif "inline_data" in contents: - # The "data" will always be bytes (or bytes within a string), - # so if this is present, it's safe to automatically substitute with the placeholder - messages.append( - { - "inline_data": { - "mime_type": contents["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - ) - - return messages - - # Handle Content object - if hasattr(contents, "parts") and contents.parts: - role = getattr(contents, "role", None) or "user" - content_parts = [] - tool_messages = [] - - for part in contents.parts: - part_result = _extract_part_content(part) - if part_result is None: - continue - - if isinstance(part_result, dict) and part_result.get("role") == "tool": - tool_messages.append(part_result) - else: - content_parts.append(part_result) - - if content_parts: - normalized_role = "assistant" if role == "model" else role - messages.append({"role": normalized_role, "content": content_parts}) - - messages.extend(tool_messages) - return messages - - # Handle Part object directly - part_result = _extract_part_content(contents) - if part_result: - if isinstance(part_result, dict) and part_result.get("role") == "tool": - return [part_result] - else: - return [{"role": "user", "content": [part_result]}] - - # Handle PIL.Image.Image - if _is_PIL_available and isinstance(contents, PILImage.Image): - blob_part = _extract_pil_image(contents) - if blob_part: - return [{"role": "user", "content": [blob_part]}] - - # Handle File object - if hasattr(contents, "uri") and hasattr(contents, "mime_type"): - # File object - file_uri = getattr(contents, "uri", None) - mime_type = getattr(contents, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing - if file_uri is not None: - # Default to empty string if mime_type is None - if mime_type is None: - mime_type = "" - - blob_part = { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - return [{"role": "user", "content": [blob_part]}] - - # Handle direct text attribute - if hasattr(contents, "text") and contents.text: - return [ - {"role": "user", "content": [{"text": str(contents.text), "type": "text"}]} - ] - - return [] - - -def _extract_part_content(part: "Any") -> "Optional[dict[str, Any]]": - """Extract content from a Part object or dict. - - Returns: - - dict for content part (text/blob) or tool message - - None if part should be skipped - """ - if part is None: - return None - - # Handle dict Part - if isinstance(part, dict): - # Check for function_response first (tool message) - if "function_response" in part: - return _extract_tool_message_from_part(part) - - if part.get("text"): - return {"text": part["text"], "type": "text"} - - # Try using Google-specific transform for dict formats (inline_data, file_data) - result = transform_google_content_part(part) - if result is not None: - # For inline_data with bytes data, substitute the content - if "inline_data" in part: - # inline_data.data will always be bytes, or a string containing base64-encoded bytes, - # so can automatically substitute without further checks - result["content"] = BLOB_DATA_SUBSTITUTE - return result - - return None - - # Handle Part object - # Check for function_response (tool message) - if hasattr(part, "function_response") and part.function_response: - return _extract_tool_message_from_part(part) - - # Handle text - if hasattr(part, "text") and part.text: - return {"text": part.text, "type": "text"} - - # Handle file_data - if hasattr(part, "file_data") and part.file_data: - file_data = part.file_data - file_uri = getattr(file_data, "file_uri", None) - mime_type = getattr(file_data, "mime_type", None) - # Process if we have file_uri, even if mime_type is missing (consistent with dict handling) - if file_uri is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - # Handle inline_data - if hasattr(part, "inline_data") and part.inline_data: - inline_data = part.inline_data - data = getattr(inline_data, "data", None) - mime_type = getattr(inline_data, "mime_type", None) - # Process if we have data, even if mime_type is missing/empty (consistent with dict handling) - if data is not None: - # Default to empty string if mime_type is None (consistent with transform_google_content_part) - if mime_type is None: - mime_type = "" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - return None - - -def _extract_tool_message_from_part(part: "Any") -> "Optional[dict[str, Any]]": - """Extract tool message from a Part with function_response. - - Returns: - {"role": "tool", "content": {"toolCallId": "...", "toolName": "...", "output": "..."}} - or None if not a valid tool message - """ - function_response = None - - if isinstance(part, dict): - function_response = part.get("function_response") - elif hasattr(part, "function_response"): - function_response = part.function_response - - if not function_response: - return None - - # Extract fields from function_response - tool_call_id = None - tool_name = None - output = None - - if isinstance(function_response, dict): - tool_call_id = function_response.get("id") - tool_name = function_response.get("name") - response_dict = function_response.get("response", {}) - # Prefer "output" key if present, otherwise use entire response - output = response_dict.get("output", response_dict) - else: - # FunctionResponse object - tool_call_id = getattr(function_response, "id", None) - tool_name = getattr(function_response, "name", None) - response_obj = getattr(function_response, "response", None) - if response_obj is None: - response_obj = {} - if isinstance(response_obj, dict): - output = response_obj.get("output", response_obj) - else: - output = response_obj - - if not tool_name: - return None - - return { - "role": "tool", - "content": { - "toolCallId": str(tool_call_id) if tool_call_id else None, - "toolName": str(tool_name), - "output": safe_serialize(output) if output is not None else None, - }, - } - - -def _extract_pil_image(image: "Any") -> "Optional[dict[str, Any]]": - """Extract blob part from PIL.Image.Image.""" - if not _is_PIL_available or not isinstance(image, PILImage.Image): - return None - - # Get format, default to JPEG - format_str = image.format or "JPEG" - suffix = format_str.lower() - mime_type = f"image/{suffix}" - - return { - "type": "blob", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _is_part_like(item: "Any") -> bool: - """Check if item is a part-like value (PartUnionDict) rather than a Content/multi-turn entry.""" - if isinstance(item, (str, Part)): - return True - if isinstance(item, (list, Content)): - return False - if isinstance(item, dict): - if "role" in item or "parts" in item: - return False - # Part objects that came in as plain dicts - return bool(_PART_DICT_KEYS & item.keys()) - # File objects - if hasattr(item, "uri"): - return True - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return True - return False - - -def _extract_part_from_item(item: "Any") -> "Optional[dict[str, Any]]": - """Convert a single part-like item to a content part dict.""" - if isinstance(item, str): - return {"text": item, "type": "text"} - - # Handle bare inline_data dicts directly to preserve the raw format - if isinstance(item, dict) and "inline_data" in item: - return { - "inline_data": { - "mime_type": item["inline_data"].get("mime_type", ""), - "data": BLOB_DATA_SUBSTITUTE, - } - } - - # For other dicts and Part objects, use existing _extract_part_content - result = _extract_part_content(item) - if result is not None: - return result - - # PIL.Image - if _is_PIL_available and isinstance(item, PILImage.Image): - return _extract_pil_image(item) - - # File objects - if hasattr(item, "uri") and hasattr(item, "mime_type"): - file_uri = getattr(item, "uri", None) - mime_type = getattr(item, "mime_type", None) or "" - if file_uri is not None: - return { - "type": "uri", - "modality": get_modality_from_mime_type(mime_type), - "mime_type": mime_type, - "uri": file_uri, - } - - return None - - -def extract_contents_text(contents: "ContentListUnion") -> "Optional[str]": - """Extract text from contents parameter which can have various formats. - - This is a compatibility function that extracts text from messages. - For new code, use extract_contents_messages instead. - """ - messages = extract_contents_messages(contents) - if not messages: - return None - - texts = [] - for message in messages: - content = message.get("content") - if isinstance(content, str): - texts.append(content) - elif isinstance(content, list): - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - texts.append(part.get("text", "")) - - return " ".join(texts) if texts else None - - -def _format_tools_for_span( - tools: "Iterable[Tool | Callable[..., Any]]", -) -> "Optional[List[dict[str, Any]]]": - """Format tools parameter for span data.""" - formatted_tools = [] - for tool in tools: - if callable(tool): - # Handle callable functions passed directly - formatted_tools.append( - { - "name": getattr(tool, "__name__", "unknown"), - "description": getattr(tool, "__doc__", None), - } - ) - elif ( - hasattr(tool, "function_declarations") - and tool.function_declarations is not None - ): - # Tool object with function declarations - for func_decl in tool.function_declarations: - formatted_tools.append( - { - "name": getattr(func_decl, "name", None), - "description": getattr(func_decl, "description", None), - } - ) - else: - # Check for predefined tool attributes - each of these tools - # is an attribute of the tool object, by default set to None - for attr_name, description in TOOL_ATTRIBUTES_MAP.items(): - if getattr(tool, attr_name, None): - formatted_tools.append( - { - "name": attr_name, - "description": description, - } - ) - break - - return formatted_tools if formatted_tools else None - - -def extract_tool_calls( - response: "GenerateContentResponse", -) -> "Optional[List[dict[str, Any]]]": - """Extract tool/function calls from response candidates and automatic function calling history.""" - - tool_calls = [] - - # Extract from candidates, sometimes tool calls are nested under the content.parts object - if getattr(response, "candidates", []): - for candidate in response.candidates: - if not hasattr(candidate, "content") or not getattr( - candidate.content, "parts", [] - ): - continue - - for part in candidate.content.parts: - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if getattr(function_call, "args", None): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - # Extract from automatic_function_calling_history - # This is the history of tool calls made by the model - if getattr(response, "automatic_function_calling_history", None): - for content in response.automatic_function_calling_history: - if not getattr(content, "parts", None): - continue - - for part in getattr(content, "parts", []): - if getattr(part, "function_call", None): - function_call = part.function_call - tool_call = { - "name": getattr(function_call, "name", None), - "type": "function_call", - } - - # Extract arguments if available - if hasattr(function_call, "args"): - tool_call["arguments"] = safe_serialize(function_call.args) - - tool_calls.append(tool_call) - - return tool_calls if tool_calls else None - - -def _capture_tool_input( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]", tool: "Tool" -) -> "dict[str, Any]": - """Capture tool input from args and kwargs.""" - tool_input = kwargs.copy() if kwargs else {} - - # If we have positional args, try to map them to the function signature - if args: - try: - sig = inspect.signature(tool) - param_names = list(sig.parameters.keys()) - for i, arg in enumerate(args): - if i < len(param_names): - tool_input[param_names[i]] = arg - except Exception: - # Fallback if we can't get the signature - tool_input["args"] = args - - return tool_input - - -def _create_tool_span( - tool_name: str, tool_doc: "Optional[str]" -) -> "Union[Span, StreamedSpan]": - """Create a span for tool execution.""" - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": ORIGIN, - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - if tool_doc: - span.set_attribute(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=ORIGIN, - ) - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - if tool_doc: - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_doc) - return span - - -def wrapped_tool(tool: "Tool | Callable[..., Any]") -> "Tool | Callable[..., Any]": - """Wrap a tool to emit execute_tool spans when called.""" - if not callable(tool): - # Not a callable function, return as-is (predefined tools) - return tool - - tool_name = getattr(tool, "__name__", "unknown") - tool_doc = tool.__doc__ - - if inspect.iscoroutinefunction(tool): - # Async function - @wraps(tool) - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = await tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return async_wrapped - else: - # Sync function - @wraps(tool) - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - with _create_tool_span(tool_name, tool_doc) as span: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - # Capture tool input - tool_input = _capture_tool_input(args, kwargs, tool) - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_input)) - - try: - result = tool(*args, **kwargs) - - # Capture tool output - with capture_internal_exceptions(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - - return result - except Exception as exc: - _capture_exception(exc) - raise - - return sync_wrapped - - -def wrapped_config_with_tools( - config: "GenerateContentConfig", -) -> "GenerateContentConfig": - """Wrap tools in config to emit execute_tool spans. Tools are sometimes passed directly as - callable functions as a part of the config object.""" - - if not config or not getattr(config, "tools", None): - return config - - result = copy.copy(config) - result.tools = [wrapped_tool(tool) for tool in config.tools] - - return result - - -def _extract_response_text( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract text from response candidates.""" - - if not response or not getattr(response, "candidates", []): - return None - - texts = [] - for candidate in response.candidates: - if not hasattr(candidate, "content") or not hasattr(candidate.content, "parts"): - continue - - if candidate.content is None or candidate.content.parts is None: - continue - - for part in candidate.content.parts: - if getattr(part, "text", None): - texts.append(part.text) - - return texts if texts else None - - -def extract_finish_reasons( - response: "GenerateContentResponse", -) -> "Optional[List[str]]": - """Extract finish reasons from response candidates.""" - if not response or not getattr(response, "candidates", []): - return None - - finish_reasons = [] - for candidate in response.candidates: - if getattr(candidate, "finish_reason", None): - # Convert enum value to string if necessary - reason = str(candidate.finish_reason) - # Remove enum prefix if present (e.g., "FinishReason.STOP" -> "STOP") - if "." in reason: - reason = reason.split(".")[-1] - finish_reasons.append(reason) - - return finish_reasons if finish_reasons else None - - -def _transform_system_instruction_one_level( - system_instructions: "Union[ContentUnionDict, ContentUnion]", - can_be_content: bool, -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, str): - return [{"type": "text", "content": system_instructions}] - - if isinstance(system_instructions, Part) and system_instructions.text: - return [{"type": "text", "content": system_instructions.text}] - - if can_be_content and isinstance(system_instructions, Content): - if isinstance(system_instructions.parts, list): - for part in system_instructions.parts: - if isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - return text_parts - - if isinstance(system_instructions, dict) and system_instructions.get("text"): - return [{"type": "text", "content": system_instructions["text"]}] - - elif can_be_content and isinstance(system_instructions, dict): - parts = system_instructions.get("parts", []) - for part in parts: - if isinstance(part, Part) and isinstance(part.text, str): - text_parts.append({"type": "text", "content": part.text}) - elif isinstance(part, dict) and isinstance(part.get("text"), str): - text_parts.append({"type": "text", "content": part["text"]}) - return text_parts - - return text_parts - - -def _transform_system_instructions( - system_instructions: "Union[ContentUnionDict, ContentUnion]", -) -> "list[TextPart]": - text_parts: "list[TextPart]" = [] - - if isinstance(system_instructions, list): - text_parts = list( - chain.from_iterable( - _transform_system_instruction_one_level( - instructions, can_be_content=False - ) - for instructions in system_instructions - ) - ) - - return text_parts - - return _transform_system_instruction_one_level( - system_instructions, can_be_content=True - ) - - -def set_span_data_for_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - model: str, - contents: "ContentListUnion", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for the request.""" - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_SYSTEM, GEN_AI_SYSTEM) - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if kwargs.get("stream", False): - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - config: "Optional[GenerateContentConfig]" = kwargs.get("config") - - # Set input messages/prompts if PII is allowed - if should_send_default_pii() and integration.include_prompts: - messages = [] - - # Add system instruction if present - system_instructions = None - if config and hasattr(config, "system_instruction"): - system_instructions = config.system_instruction - elif isinstance(config, dict) and "system_instruction" in config: - system_instructions = config.get("system_instruction") - - if system_instructions is not None: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - # Extract messages from contents - contents_messages = extract_contents_messages(contents) - messages.extend(contents_messages) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Extract parameters directly from config (not nested under generation_config) - for param, span_key in [ - ("temperature", SPANDATA.GEN_AI_REQUEST_TEMPERATURE), - ("top_p", SPANDATA.GEN_AI_REQUEST_TOP_P), - ("top_k", SPANDATA.GEN_AI_REQUEST_TOP_K), - ("max_output_tokens", SPANDATA.GEN_AI_REQUEST_MAX_TOKENS), - ("presence_penalty", SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY), - ("frequency_penalty", SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY), - ("seed", SPANDATA.GEN_AI_REQUEST_SEED), - ]: - if hasattr(config, param): - value = getattr(config, param) - if value is not None: - set_on_span(span_key, value) - - # Set tools if available - if config is not None and hasattr(config, "tools"): - tools = config.tools - if tools: - formatted_tools = _format_tools_for_span(tools) - if formatted_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - formatted_tools, - unpack=False, - ) - - -def set_span_data_for_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "GenerateContentResponse", -) -> None: - """Set span data for the response.""" - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - if should_send_default_pii() and integration.include_prompts: - response_texts = _extract_response_text(response) - if response_texts: - # Format as JSON string array as per documentation - set_on_span(SPANDATA.GEN_AI_RESPONSE_TEXT, safe_serialize(response_texts)) - - tool_calls = extract_tool_calls(response) - if tool_calls: - # Tool calls should be JSON serialized - set_on_span(SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)) - - finish_reasons = extract_finish_reasons(response) - if finish_reasons: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons - ) - - if getattr(response, "response_id", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response.response_id) - - if getattr(response, "model_version", None): - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_version) - - usage_data = extract_usage_data(response) - - if usage_data["input_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage_data["input_tokens"]) - - if usage_data["input_tokens_cached"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage_data["input_tokens_cached"], - ) - - if usage_data["output_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage_data["output_tokens"]) - - if usage_data["output_tokens_reasoning"]: - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage_data["output_tokens_reasoning"], - ) - - if usage_data["total_tokens"]: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage_data["total_tokens"]) - - -def prepare_generate_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[Any, Any, str]": - """Extract and prepare common arguments for generate_content methods.""" - model = args[0] if args else kwargs.get("model", "unknown") - contents = args[1] if len(args) > 1 else kwargs.get("contents") - model_name = get_model_name(model) - - config = kwargs.get("config") - wrapped_config = wrapped_config_with_tools(config) - if wrapped_config is not config: - kwargs["config"] = wrapped_config - - return model, contents, model_name - - -def prepare_embed_content_args( - args: "tuple[Any, ...]", kwargs: "dict[str, Any]" -) -> "tuple[str, Any]": - """Extract and prepare common arguments for embed_content methods. - - Returns: - tuple: (model_name, contents) - """ - model = kwargs.get("model", "unknown") - contents = kwargs.get("contents") - model_name = get_model_name(model) - - return model_name, contents - - -def set_span_data_for_embed_request( - span: "Union[Span, StreamedSpan]", - integration: "Any", - contents: "Any", - kwargs: "dict[str, Any]", -) -> None: - """Set span data for embedding request.""" - # Include input contents if PII is allowed - if should_send_default_pii() and integration.include_prompts: - if contents: - # For embeddings, contents is typically a list of strings/texts - input_texts = [] - - # Handle various content formats - if isinstance(contents, str): - input_texts = [contents] - elif isinstance(contents, list): - for item in contents: - text = extract_contents_text(item) - if text: - input_texts.append(text) - else: - text = extract_contents_text(contents) - if text: - input_texts = [text] - - if input_texts: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - input_texts, - unpack=False, - ) - - -def set_span_data_for_embed_response( - span: "Union[Span, StreamedSpan]", - integration: "Any", - response: "EmbedContentResponse", -) -> None: - """Set span data for embedding response.""" - if not response: - return - - # Extract token counts from embeddings statistics (Vertex AI only) - # Each embedding has its own statistics with token_count - if hasattr(response, "embeddings") and response.embeddings: - total_tokens = 0 - - for embedding in response.embeddings: - if hasattr(embedding, "statistics") and embedding.statistics: - token_count = getattr(embedding.statistics, "token_count", None) - if token_count is not None: - total_tokens += int(token_count) - - # Set token count if we found any - if total_tokens > 0: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) - else: - span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py deleted file mode 100644 index dcb60e9561..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/gql.py +++ /dev/null @@ -1,173 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - parse_version, -) - -try: - import gql # type: ignore[import-not-found] - from gql.transport import ( # type: ignore[import-not-found] - AsyncTransport, - Transport, - ) - from gql.transport.exceptions import ( # type: ignore[import-not-found] - TransportQueryError, - ) - from graphql import ( - DocumentNode, - VariableDefinitionNode, - get_operation_ast, - print_ast, - ) - - try: - # gql 4.0+ - from gql import GraphQLRequest - except ImportError: - GraphQLRequest = None - -except ImportError: - raise DidNotEnable("gql is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Tuple, Union - - from sentry_sdk._types import Event, EventProcessor - - EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] - - -class GQLIntegration(Integration): - identifier = "gql" - - @staticmethod - def setup_once() -> None: - gql_version = parse_version(gql.__version__) - _check_minimum_version(GQLIntegration, gql_version) - - _patch_execute() - - -def _data_from_document(document: "DocumentNode") -> "EventDataType": - try: - operation_ast = get_operation_ast(document) - data: "EventDataType" = {"query": print_ast(document)} - - if operation_ast is not None: - data["variables"] = operation_ast.variable_definitions - if operation_ast.name is not None: - data["operationName"] = operation_ast.name.value - - return data - except (AttributeError, TypeError): - return dict() - - -def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str: - """ - The RequestsHTTPTransport allows defining the HTTP method; all - other transports use POST. - """ - try: - return transport.method - except AttributeError: - return "POST" - - -def _request_info_from_transport( - transport: "Union[Transport, AsyncTransport, None]", -) -> "Dict[str, str]": - if transport is None: - return {} - - request_info = { - "method": _transport_method(transport), - } - - try: - request_info["url"] = transport.url - except AttributeError: - pass - - return request_info - - -def _patch_execute() -> None: - real_execute = gql.Client.execute - - # Maintain signature for backwards compatibility. - # gql.Client.execute() accepts a positional-only "request" - # parameter with version 4.0.0. - @ensure_integration_enabled(GQLIntegration, real_execute) - def sentry_patched_execute( - self: "gql.Client", - document: "DocumentNode", - *args: "Any", - **kwargs: "Any", - ) -> "Any": - scope = sentry_sdk.get_isolation_scope() - # document is a gql.GraphQLRequest with gql v4.0.0. - scope.add_event_processor(_make_gql_event_processor(self, document)) - - try: - # document is a gql.GraphQLRequest with gql v4.0.0. - return real_execute(self, document, *args, **kwargs) - except TransportQueryError as e: - event, hint = event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "gql", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint) - raise e - - gql.Client.execute = sentry_patched_execute - - -def _make_gql_event_processor( - client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]" -) -> "EventProcessor": - def processor(event: "Event", hint: "dict[str, Any]") -> "Event": - try: - errors = hint["exc_info"][1].errors - except (AttributeError, KeyError): - errors = None - - request = event.setdefault("request", {}) - request.update( - { - "api_target": "graphql", - **_request_info_from_transport(client.transport), - } - ) - - if should_send_default_pii(): - if GraphQLRequest is not None and isinstance( - document_or_request, GraphQLRequest - ): - # In v4.0.0, gql moved to using GraphQLRequest instead of - # DocumentNode in execute - # https://github.com/graphql-python/gql/pull/556 - document = document_or_request.document - else: - document = document_or_request - - request["data"] = _data_from_document(document) - contexts = event.setdefault("contexts", {}) - response = contexts.setdefault("response", {}) - response.update( - { - "data": {"errors": errors}, - "type": response, - } - ) - - return event - - return processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py deleted file mode 100644 index 4938a5c0f3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/graphene.py +++ /dev/null @@ -1,181 +0,0 @@ -from contextlib import contextmanager - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from graphene.types import schema as graphene_schema # type: ignore -except ImportError: - raise DidNotEnable("graphene is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Generator - from typing import Any, Dict, Union - - from graphene.language.source import Source # type: ignore - from graphql.execution import ExecutionResult - from graphql.type import GraphQLSchema - - from sentry_sdk._types import Event - - -class GrapheneIntegration(Integration): - identifier = "graphene" - - @staticmethod - def setup_once() -> None: - version = package_version("graphene") - _check_minimum_version(GrapheneIntegration, version) - - _patch_graphql() - - -def _patch_graphql() -> None: - old_graphql_sync = graphene_schema.graphql_sync - old_graphql_async = graphene_schema.graphql - - @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync) - def _sentry_patched_graphql_sync( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = old_graphql_sync(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - async def _sentry_patched_graphql_async( - schema: "GraphQLSchema", - source: "Union[str, Source]", - *args: "Any", - **kwargs: "Any", - ) -> "ExecutionResult": - integration = sentry_sdk.get_client().get_integration(GrapheneIntegration) - if integration is None: - return await old_graphql_async(schema, source, *args, **kwargs) - - scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) - - with graphql_span(schema, source, kwargs): - result = await old_graphql_async(schema, source, *args, **kwargs) - - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - for error in result.errors or []: - event, hint = event_from_exception( - error, - client_options=client.options, - mechanism={ - "type": GrapheneIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - return result - - graphene_schema.graphql_sync = _sentry_patched_graphql_sync - graphene_schema.graphql = _sentry_patched_graphql_async - - -def _event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - if should_send_default_pii(): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" - - elif event.get("request", {}).get("data"): - del event["request"]["data"] - - return event - - -@contextmanager -def graphql_span( - schema: "GraphQLSchema", source: "Union[str, Source]", kwargs: "Dict[str, Any]" -) -> "Generator[None, None, None]": - operation_name = kwargs.get("operation_name") or "" - - operation_type = "query" - op = OP.GRAPHQL_QUERY - if source.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif source.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - sentry_sdk.add_breadcrumb( - crumb={ - "data": { - "operation_name": operation_name, - "operation_type": operation_type, - }, - "category": "graphql.operation", - }, - ) - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - additional_attributes = {} - if should_send_default_pii(): - additional_attributes["graphql.document"] = source - - _graphql_span = sentry_sdk.traces.start_span( - name=operation_name, - attributes={ - "sentry.op": op, - "graphql.operation.name": operation_name, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - _graphql_span = sentry_sdk.start_span(op=op, name=operation_name) - - if should_send_default_pii(): - _graphql_span.set_data("graphql.document", source) - _graphql_span.set_data("graphql.operation.name", operation_name) - _graphql_span.set_data("graphql.operation.type", operation_type) - - _graphql_span.__enter__() - - try: - yield - finally: - if is_span_streaming_enabled: - _graphql_span.end() # type: ignore - else: - _graphql_span.__exit__(None, None, None) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py deleted file mode 100644 index bf74ff1351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/__init__.py +++ /dev/null @@ -1,188 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any, Optional, Sequence - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .client import ClientInterceptor -from .server import ServerInterceptor - -try: - import grpc - from grpc import Channel, Server, intercept_channel - from grpc.aio import Channel as AsyncChannel - from grpc.aio import Server as AsyncServer - - from .aio.client import ( - SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor, - ) - from .aio.client import ( - SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor, - ) - from .aio.server import ServerInterceptor as AsyncServerInterceptor -except ImportError: - raise DidNotEnable("grpcio is not installed.") - -# Hack to get new Python features working in older versions -# without introducing a hard dependency on `typing_extensions` -# from: https://stackoverflow.com/a/71944042/300572 -if TYPE_CHECKING: - from typing import Callable, ParamSpec -else: - # Fake ParamSpec - class ParamSpec: - def __init__(self, _): - self.args = None - self.kwargs = None - - # Callable[anything] will return None - class _Callable: - def __getitem__(self, _): - return None - - # Make instances - Callable = _Callable() - -P = ParamSpec("P") - -GRPC_VERSION = parse_version(grpc.__version__) - - -def _is_channel_intercepted(channel: "Channel") -> bool: - interceptor = getattr(channel, "_interceptor", None) - while interceptor is not None: - if isinstance(interceptor, ClientInterceptor): - return True - - inner_channel = getattr(channel, "_channel", None) - if inner_channel is None: - return False - - channel = inner_channel - interceptor = getattr(channel, "_interceptor", None) - - return False - - -def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - "Wrapper for synchronous secure and insecure channel." - - @wraps(func) - def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel": - channel = func(*args, **kwargs) - if not _is_channel_intercepted(channel): - return intercept_channel(channel, ClientInterceptor()) - else: - return channel - - return patched_channel - - -def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channel]": - @wraps(func) - def patched_intercept_channel( - channel: "Channel", *interceptors: "grpc.ServerInterceptor" - ) -> "Channel": - if _is_channel_intercepted(channel): - interceptors = tuple( - [ - interceptor - for interceptor in interceptors - if not isinstance(interceptor, ClientInterceptor) - ] - ) - else: - interceptors = interceptors - return intercept_channel(channel, *interceptors) - - return patched_intercept_channel # type: ignore - - -def _wrap_channel_async( - func: "Callable[P, AsyncChannel]", -) -> "Callable[P, AsyncChannel]": - "Wrapper for asynchronous secure and insecure channel." - - @wraps(func) - def patched_channel( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.aio.ClientInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Channel": - sentry_interceptors = [ - AsyncUnaryUnaryClientInterceptor(), - AsyncUnaryStreamClientIntercetor(), - ] - interceptors = [*sentry_interceptors, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_channel # type: ignore - - -def _wrap_sync_server(func: "Callable[P, Server]") -> "Callable[P, Server]": - """Wrapper for synchronous server.""" - - @wraps(func) - def patched_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - interceptors = [ - interceptor - for interceptor in interceptors or [] - if not isinstance(interceptor, ServerInterceptor) - ] - server_interceptor = ServerInterceptor() - interceptors = [server_interceptor, *(interceptors or [])] - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_server # type: ignore - - -def _wrap_async_server(func: "Callable[P, AsyncServer]") -> "Callable[P, AsyncServer]": - """Wrapper for asynchronous server.""" - - @wraps(func) - def patched_aio_server( # type: ignore - *args: "P.args", - interceptors: "Optional[Sequence[grpc.ServerInterceptor]]" = None, - **kwargs: "P.kwargs", - ) -> "Server": - server_interceptor = AsyncServerInterceptor() - interceptors = [ - server_interceptor, - *(interceptors or []), - ] - - try: - # We prefer interceptors as a list because of compatibility with - # opentelemetry https://github.com/getsentry/sentry-python/issues/4389 - # However, prior to grpc 1.42.0, only tuples were accepted, so we - # have no choice there. - if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0): - interceptors = tuple(interceptors) - except Exception: - pass - - return func(*args, interceptors=interceptors, **kwargs) # type: ignore - - return patched_aio_server # type: ignore - - -class GRPCIntegration(Integration): - identifier = "grpc" - - @staticmethod - def setup_once() -> None: - import grpc - - grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel) - grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel) - grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel) - - grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel) - grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel) - - grpc.server = _wrap_sync_server(grpc.server) - grpc.aio.server = _wrap_async_server(grpc.aio.server) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py deleted file mode 100644 index 4d21815254..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .client import ClientInterceptor -from .server import ServerInterceptor - -__all__ = [ - "ClientInterceptor", - "ServerInterceptor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py deleted file mode 100644 index d07b7f19be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/client.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import Any, AsyncIterable, Callable, Union - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -try: - from google.protobuf.message import Message - from grpc.aio import ( - ClientCallDetails, - Metadata, - UnaryStreamCall, - UnaryStreamClientInterceptor, - UnaryUnaryCall, - UnaryUnaryClientInterceptor, - ) -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor: - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - if client_call_details.metadata is None: - client_call_details = client_call_details._replace(metadata=Metadata()) - elif not isinstance(client_call_details.metadata, Metadata): - # This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0 - # See https://github.com/grpc/grpc/issues/34298. - client_call_details = client_call_details._replace( - metadata=Metadata.from_tuple(client_call_details.metadata) - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - client_call_details.metadata.add(key, value) - return client_call_details - - -class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore - async def intercept_unary_unary( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryUnaryCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[UnaryUnaryCall, Message]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_attribute(SPANDATA.RPC_RESPONSE_STATUS_CODE, status_code.name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - status_code = await response.code() - span.set_data("code", status_code.name) - - return response - - -class SentryUnaryStreamClientInterceptor( - ClientInterceptor, - UnaryStreamClientInterceptor, # type: ignore -): - async def intercept_unary_stream( - self, - continuation: "Callable[[ClientCallDetails, Message], UnaryStreamCall]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[AsyncIterable[Any], UnaryStreamCall]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method.decode(), - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method.decode(), - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method.decode(), - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = await continuation(client_call_details, request) - # status_code = await response.code() - # span.set_data("code", status_code) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py deleted file mode 100644 index 010337e98c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/aio/server.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - from typing import Any, Optional - - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler - from grpc.aio import AbortError, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Callable[[ServicerContext], str] | None" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - async def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]", - handler_call_details: "HandlerCallDetails", - ) -> "Optional[Awaitable[RpcMethodHandler]]": - handler = await continuation(handler_call_details) - if handler is None: - return None - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - if not handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.unary_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - with sentry_sdk.isolation_scope(): - name = ( - custom_find_name(context) if custom_find_name else method_name - ) - if not name: - return await handler(request, context) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - # What if the headers are empty? - sentry_sdk.traces.continue_trace( - dict(context.invocation_metadata()) - ) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - else: - # What if the headers are empty? - transaction = sentry_sdk.continue_trace( - dict(context.invocation_metadata()), - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return await handler.unary_unary(request, context) - except AbortError: - raise - except Exception as exc: - event, hint = event_from_exception( - exc, - mechanism={"type": "grpc", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - raise - - elif not handler.request_streaming and handler.response_streaming: - handler_factory = grpc.unary_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.unary_stream(request, context): - yield r - - elif handler.request_streaming and not handler.response_streaming: - handler_factory = grpc.stream_unary_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": - response = handler.stream_unary(request, context) - return await response - - elif handler.request_streaming and handler.response_streaming: - handler_factory = grpc.stream_stream_rpc_method_handler - - async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type: ignore - async for r in handler.stream_stream(request, context): - yield r - - return handler_factory( - wrapped, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py deleted file mode 100644 index 5384a0a78f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/client.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Iterator, Union - -try: - import grpc - from google.protobuf.message import Message - from grpc import Call, ClientCallDetails - from grpc._interceptor import _UnaryOutcome - from grpc.aio._interceptor import UnaryStreamCall -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ClientInterceptor( - grpc.UnaryUnaryClientInterceptor, # type: ignore - grpc.UnaryStreamClientInterceptor, # type: ignore -): - def intercept_unary_unary( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "_UnaryOutcome": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary unary call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_attribute( - SPANDATA.RPC_RESPONSE_STATUS_CODE, response.code().name - ) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary unary call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary unary") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - span.set_data("code", response.code().name) - - return response - - def intercept_unary_stream( - self: "ClientInterceptor", - continuation: "Callable[[ClientCallDetails, Message], Union[Iterable[Any], UnaryStreamCall]]", - client_call_details: "ClientCallDetails", - request: "Message", - ) -> "Union[Iterator[Message], Call]": - method = client_call_details.method - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - response: "UnaryStreamCall" - if span_streaming: - with sentry_sdk.traces.start_span( - name="unary stream call to %s" % method, - attributes={ - "sentry.op": OP.GRPC_CLIENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.RPC_METHOD: method, - }, - ) as span: - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - else: - with sentry_sdk.start_span( - op=OP.GRPC_CLIENT, - name="unary stream call to %s" % method, - origin=SPAN_ORIGIN, - ) as span: - span.set_data("type", "unary stream") - span.set_data("method", method) - - client_call_details = ( - self._update_client_call_details_metadata_from_scope( - client_call_details - ) - ) - - response = continuation(client_call_details, request) - # Setting code on unary-stream leads to execution getting stuck - # span.set_data("code", response.code().name) - - return response - - @staticmethod - def _update_client_call_details_metadata_from_scope( - client_call_details: "ClientCallDetails", - ) -> "ClientCallDetails": - metadata = ( - list(client_call_details.metadata) if client_call_details.metadata else [] - ) - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - metadata.append((key, value)) - - client_call_details = grpc._interceptor._ClientCallDetails( - method=client_call_details.method, - timeout=client_call_details.timeout, - metadata=metadata, - credentials=client_call_details.credentials, - wait_for_ready=client_call_details.wait_for_ready, - compression=client_call_details.compression, - ) - - return client_call_details diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py deleted file mode 100644 index 9fdb975caf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.grpc.grpc" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py deleted file mode 100644 index 1cba1d4b85..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/grpc/server.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Callable, Optional - - from google.protobuf.message import Message - -try: - import grpc - from grpc import HandlerCallDetails, RpcMethodHandler, ServicerContext -except ImportError: - raise DidNotEnable("grpcio is not installed") - - -class ServerInterceptor(grpc.ServerInterceptor): # type: ignore - def __init__( - self: "ServerInterceptor", - find_name: "Optional[Callable[[ServicerContext], str]]" = None, - ) -> None: - self._custom_find_name = find_name - - super().__init__() - - def intercept_service( - self: "ServerInterceptor", - continuation: "Callable[[HandlerCallDetails], RpcMethodHandler]", - handler_call_details: "HandlerCallDetails", - ) -> "RpcMethodHandler": - handler = continuation(handler_call_details) - if not handler or not handler.unary_unary: - return handler - - method_name = handler_call_details.method - custom_find_name = self._custom_find_name - - def behavior(request: "Message", context: "ServicerContext") -> "Message": - with sentry_sdk.isolation_scope(): - name = custom_find_name(context) if custom_find_name else method_name - - if name: - metadata = dict(context.invocation_metadata()) - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(metadata) - - with sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": OP.GRPC_SERVER, - "sentry.span.source": SegmentSource.CUSTOM.value, - "sentry.origin": SPAN_ORIGIN, - }, - parent_span=None, - ): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - transaction = sentry_sdk.continue_trace( - metadata, - op=OP.GRPC_SERVER, - name=name, - source=TransactionSource.CUSTOM, - origin=SPAN_ORIGIN, - ) - - with sentry_sdk.start_transaction(transaction=transaction): - try: - return handler.unary_unary(request, context) - except BaseException as e: - raise e - else: - return handler.unary_unary(request, context) - - return grpc.unary_unary_rpc_method_handler( - behavior, - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py deleted file mode 100644 index a68f20b299..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx is not installed") - -__all__ = ["HttpxIntegration"] - - -class HttpxIntegration(Integration): - identifier = "httpx" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx_client() - _install_httpx_async_client() - - -def _install_httpx_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(HttpxIntegration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(HttpxIntegration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": HttpxIntegration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=HttpxIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py deleted file mode 100644 index 25062aaa11..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/httpx2.py +++ /dev/null @@ -1,263 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - logger, - parse_url, -) - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Attributes - - -try: - from httpx2 import AsyncClient, Client, Request, Response -except ImportError: - raise DidNotEnable("httpx2 is not installed") - -__all__ = ["Httpx2Integration"] - - -class Httpx2Integration(Integration): - identifier = "httpx2" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - httpx2 has its own transport layer and can be customized when needed, - so patch Client.send and AsyncClient.send to support both synchronous and async interfaces. - """ - _install_httpx2_client() - _install_httpx2_async_client() - - -def _install_httpx2_client() -> None: - real_send = Client.send - - @ensure_integration_enabled(Httpx2Integration, real_send) - def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - Client.send = send # type: ignore - - -def _install_httpx2_async_client() -> None: - real_send = AsyncClient.send - - async def send( - self: "AsyncClient", request: "Request", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - if client.get_integration(Httpx2Integration) is None: - return await real_send(self, request, **kwargs) - - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - try: - rv = await real_send(self, request, **kwargs) - - streamed_span.status = "error" if rv.status_code >= 400 else "ok" - attributes["http.response.status_code"] = rv.status_code - finally: - streamed_span.set_attributes(attributes) - - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) - else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(client, str(request.url)): - for ( - key, - value, - ) in ( - sentry_sdk.get_current_scope().iter_trace_propagation_headers() - ): - logger.debug( - f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}." - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - rv = await real_send(self, request, **kwargs) - - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) - - with capture_internal_exceptions(): - add_http_request_source(span) - - return rv - - AsyncClient.send = send # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py deleted file mode 100644 index c3bbc8abcf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huey.py +++ /dev/null @@ -1,239 +0,0 @@ -import sys -from datetime import datetime -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.api import continue_trace, get_baggage, get_traceparent -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, SpanStatus, StreamedSpan -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - _register_control_flow_exception, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union - - from sentry_sdk._types import Event, EventProcessor, Hint - from sentry_sdk.utils import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - -try: - from huey.api import Huey, PeriodicTask, Result, ResultGroup, Task - from huey.exceptions import CancelExecution, RetryTask, TaskLockedException -except ImportError: - raise DidNotEnable("Huey is not installed") - -try: - from huey.api import chord as HueyChord - from huey.api import group as HueyGroup -except ImportError: - HueyChord = None - HueyGroup = None - - -HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException) - - -class HueyIntegration(Integration): - identifier = "huey" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_enqueue() - patch_execute() - _register_control_flow_exception( - [CancelExecution, RetryTask, TaskLockedException] - ) - - -def patch_enqueue() -> None: - old_enqueue = Huey.enqueue - - @ensure_integration_enabled(HueyIntegration, old_enqueue) - def _sentry_enqueue( - self: "Huey", item: "Any" - ) -> "Optional[Union[Result, ResultGroup]]": - if HueyChord is not None and isinstance(item, HueyChord): - span_name = "Huey Chord" - elif HueyGroup is not None and isinstance(item, HueyGroup): - span_name = "Huey Task Group" - else: - span_name = item.name - - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - span_ctx = None - if is_span_streaming_enabled: - span_ctx = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_HUEY, - "sentry.origin": HueyIntegration.origin, - }, - ) - else: - span_ctx = sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_HUEY, - name=span_name, - origin=HueyIntegration.origin, - ) - - no_headers_types = (PeriodicTask,) + tuple( - t for t in [HueyGroup, HueyChord] if t is not None - ) - with span_ctx: - if not isinstance(item, no_headers_types): - # Attach trace propagation data to task kwargs. We do - # not do this for periodic tasks, as these don't - # really have an originating transaction. - # Additionally, we do not do this for Huey groups or chords, as enqueue will - # recursively call this method for each task within the list, resulting - # in the trace propagation data being attached to each task individually - # (which we want) - item.kwargs["sentry_headers"] = { - BAGGAGE_HEADER_NAME: get_baggage(), - SENTRY_TRACE_HEADER_NAME: get_traceparent(), - } - return old_enqueue(self, item) - - Huey.enqueue = _sentry_enqueue - - -def _make_event_processor(task: "Any") -> "EventProcessor": - def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - tags = event.setdefault("tags", {}) - tags["huey_task_id"] = task.id - tags["huey_task_retry"] = task.default_retries > task.retries - extra = event.setdefault("extra", {}) - extra["huey-job"] = { - "task": task.name, - "args": ( - task.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - task.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "retry": (task.default_retries or 0) - task.retries, - } - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if exc_info[0] in HUEY_CONTROL_FLOW_EXCEPTIONS: - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.ABORTED) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.OK - return - - if not is_span_streaming_enabled: - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - elif type(scope._span) is StreamedSpan: - scope._span._segment.status = SpanStatus.ERROR - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": HueyIntegration.identifier, "handled": False}, - ) - scope.capture_event(event, hint=hint) - - -def _wrap_task_execute(func: "F") -> "F": - @ensure_integration_enabled(HueyIntegration, func) - def _sentry_execute(*args: "Any", **kwargs: "Any") -> "Any": - try: - result = func(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - return _sentry_execute # type: ignore - - -def patch_execute() -> None: - old_execute = Huey._execute - - @ensure_integration_enabled(HueyIntegration, old_execute) - def _sentry_execute( - self: "Huey", task: "Task", timestamp: "Optional[datetime]" = None - ) -> "Any": - with sentry_sdk.isolation_scope() as scope: - with capture_internal_exceptions(): - scope._name = "huey" - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(task)) - - sentry_headers = task.kwargs.pop("sentry_headers", None) - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - - if is_span_streaming_enabled: - headers = sentry_headers or {} - sentry_sdk.traces.continue_trace(headers) - span_ctx = sentry_sdk.traces.start_span( - name=task.name, - attributes={ - "sentry.op": OP.QUEUE_TASK_HUEY, - "sentry.origin": HueyIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - "messaging.message.id": task.id, - "messaging.message.system": "huey", - "messaging.message.retry.count": (task.default_retries or 0) - - task.retries, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - sentry_headers or {}, - name=task.name, - op=OP.QUEUE_TASK_HUEY, - source=TransactionSource.TASK, - origin=HueyIntegration.origin, - ) - transaction.set_status(SPANSTATUS.OK) - span_ctx = sentry_sdk.start_transaction(transaction) - - if not getattr(task, "_sentry_is_patched", False): - task.execute = _wrap_task_execute(task.execute) - task._sentry_is_patched = True - - with span_ctx: - return old_execute(self, task, timestamp) - - Huey._execute = _sentry_execute diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py deleted file mode 100644 index 835acc7279..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/huggingface_hub.py +++ /dev/null @@ -1,392 +0,0 @@ -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - _set_span_data_attribute, - get_start_span_function, - set_data_normalized, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Union - - from sentry_sdk.tracing import Span - -try: - import huggingface_hub.inference._client -except ImportError: - raise DidNotEnable("Huggingface not installed") - - -class HuggingfaceHubIntegration(Integration): - identifier = "huggingface_hub" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "HuggingfaceHubIntegration", include_prompts: bool = True - ) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - # Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks - huggingface_hub.inference._client.InferenceClient.text_generation = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.text_generation, - OP.GEN_AI_TEXT_COMPLETION, - ) - ) - huggingface_hub.inference._client.InferenceClient.chat_completion = ( - _wrap_huggingface_task( - huggingface_hub.inference._client.InferenceClient.chat_completion, - OP.GEN_AI_CHAT, - ) - ) - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "huggingface_hub", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _wrap_huggingface_task(f: "Callable[..., Any]", op: str) -> "Callable[..., Any]": - @wraps(f) - def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) - if integration is None: - return f(*args, **kwargs) - - prompt = None - if "prompt" in kwargs: - prompt = kwargs["prompt"] - elif "messages" in kwargs: - prompt = kwargs["messages"] - elif len(args) >= 2: - if isinstance(args[1], str) or isinstance(args[1], list): - prompt = args[1] - - if prompt is None: - # invalid call, dont instrument, let it return error - return f(*args, **kwargs) - - client = args[0] - model = client.model or kwargs.get("model") or "" - operation_name = op.split(".")[-1] - - span: "Union[Span, StreamedSpan]" - if has_span_streaming_enabled(sentry_sdk.get_client().options): - span = sentry_sdk.traces.start_span( - name=f"{operation_name} {model}", - attributes={ - "sentry.op": op, - "sentry.origin": HuggingfaceHubIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=op, - name=f"{operation_name} {model}", - origin=HuggingfaceHubIntegration.origin, - ) - span.__enter__() - - _set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - - if model: - _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) - - # Input attributes - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False - ) - - attribute_mapping = { - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - } - - for attribute, span_attribute in attribute_mapping.items(): - value = kwargs.get(attribute, None) - if value is not None: - if isinstance(value, (int, float, bool, str)): - _set_span_data_attribute(span, span_attribute, value) - else: - set_data_normalized(span, span_attribute, value, unpack=False) - - # LLM Execution - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(e) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Output attributes - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - tool_calls = None - usage = None - - with capture_internal_exceptions(): - if isinstance(res, str) and res is not None: - response_text_buffer.append(res) - - if hasattr(res, "generated_text") and res.generated_text is not None: - response_text_buffer.append(res.generated_text) - - if hasattr(res, "model") and res.model is not None: - response_model = res.model - - if hasattr(res, "details") and hasattr(res.details, "finish_reason"): - finish_reason = res.details.finish_reason - - if ( - hasattr(res, "details") - and hasattr(res.details, "generated_tokens") - and res.details.generated_tokens is not None - ): - tokens_used = res.details.generated_tokens - - if hasattr(res, "usage") and res.usage is not None: - usage = res.usage - - if hasattr(res, "choices") and res.choices is not None: - for choice in res.choices: - if hasattr(choice, "finish_reason"): - finish_reason = choice.finish_reason - if hasattr(choice, "message") and hasattr( - choice.message, "tool_calls" - ): - tool_calls = choice.message.tool_calls - if ( - hasattr(choice, "message") - and hasattr(choice.message, "content") - and choice.message.content is not None - ): - response_text_buffer.append(choice.message.content) - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - elif tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - # If the response is not a generator (meaning a streaming response) - # we are done and can return the response - if not inspect.isgenerator(res): - span.__exit__(None, None, None) - return res - - if kwargs.get("details", False): - # text-generation stream output - def new_details_iterator() -> "Iterable[Any]": - finish_reason = None - response_text_buffer: "list[str]" = [] - tokens_used = 0 - - with capture_internal_exceptions(): - for chunk in res: - if ( - hasattr(chunk, "token") - and hasattr(chunk.token, "text") - and chunk.token.text is not None - ): - response_text_buffer.append(chunk.token.text) - - if hasattr(chunk, "details") and hasattr( - chunk.details, "finish_reason" - ): - finish_reason = chunk.details.finish_reason - - if ( - hasattr(chunk, "details") - and hasattr(chunk.details, "generated_tokens") - and chunk.details.generated_tokens is not None - ): - tokens_used = chunk.details.generated_tokens - - yield chunk - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - span.__exit__(None, None, None) - - return new_details_iterator() - - else: - # chat-completion stream output - def new_iterator() -> "Iterable[str]": - finish_reason = None - response_model = None - response_text_buffer: "list[str]" = [] - tool_calls = None - usage = None - - with capture_internal_exceptions(): - for chunk in res: - if hasattr(chunk, "model") and chunk.model is not None: - response_model = chunk.model - - if hasattr(chunk, "usage") and chunk.usage is not None: - usage = chunk.usage - - if isinstance(chunk, str): - if chunk is not None: - response_text_buffer.append(chunk) - - if hasattr(chunk, "choices") and chunk.choices is not None: - for choice in chunk.choices: - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "content") - and choice.delta.content is not None - ): - response_text_buffer.append( - choice.delta.content - ) - - if ( - hasattr(choice, "finish_reason") - and choice.finish_reason is not None - ): - finish_reason = choice.finish_reason - - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "tool_calls") - and choice.delta.tool_calls is not None - ): - tool_calls = choice.delta.tool_calls - - yield chunk - - if response_model is not None: - _set_span_data_attribute( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - - span.__exit__(None, None, None) - - return new_iterator() - - return new_huggingface_task diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py deleted file mode 100644 index 9dcbb189ce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langchain.py +++ /dev/null @@ -1,1412 +0,0 @@ -import itertools -import json -import sys -import warnings -from collections import OrderedDict -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - get_start_span_function, - normalize_message_roles, - set_data_normalized, - transform_content_part, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - _get_value, - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import capture_internal_exceptions, logger - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Dict, - Iterator, - List, - Optional, - Union, - ) - from uuid import UUID - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - - -try: - from langchain_core.agents import AgentFinish - from langchain_core.callbacks import ( - BaseCallbackHandler, - BaseCallbackManager, - Callbacks, - manager, - ) - from langchain_core.messages import BaseMessage - from langchain_core.outputs import LLMResult - -except ImportError: - raise DidNotEnable("langchain not installed") - - -try: - # >=v1 - from langchain_classic.agents import AgentExecutor # type: ignore[import-not-found] -except ImportError: - try: - # "Optional[str]": - ai_type = all_params.get("_type") - - if not ai_type or not isinstance(ai_type, str): - return None - - return ai_type - - -DATA_FIELDS = { - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, -} - - -def _transform_langchain_content_block( - content_block: "Dict[str, Any]", -) -> "Dict[str, Any]": - """ - Transform a LangChain content block using the shared transform_content_part function. - - Returns the original content block if transformation is not applicable - (e.g., for text blocks or unrecognized formats). - """ - result = transform_content_part(content_block) - return result if result is not None else content_block - - -def _transform_langchain_message_content(content: "Any") -> "Any": - """ - Transform LangChain message content, handling both string content and - list of content blocks. - """ - if isinstance(content, str): - return content - - if isinstance(content, (list, tuple)): - transformed = [] - for block in content: - if isinstance(block, dict): - transformed.append(_transform_langchain_content_block(block)) - else: - transformed.append(block) - return transformed - - return content - - -def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": - system_instructions = [] - - for list_ in messages: - for message in list_: - # type of content: str | list[str | dict] | None - if message.type == "system" and isinstance(message.content, str): - system_instructions.append(message.content) - - elif message.type == "system" and isinstance(message.content, list): - for item in message.content: - if isinstance(item, str): - system_instructions.append(item) - - elif isinstance(item, dict) and item.get("type") == "text": - instruction = item.get("text") - if isinstance(instruction, str): - system_instructions.append(instruction) - - return system_instructions - - -def _transform_system_instructions( - system_instructions: "List[str]", -) -> "List[TextPart]": - return [ - { - "type": "text", - "content": instruction, - } - for instruction in system_instructions - ] - - -class LangchainIntegration(Integration): - identifier = "langchain" - origin = f"auto.ai.{identifier}" - - _ignored_exceptions: "set[type[Exception]]" = set() - - def __init__( - self: "LangchainIntegration", - include_prompts: bool = True, - max_spans: "Optional[int]" = None, - ) -> None: - self.include_prompts = include_prompts - self.max_spans = max_spans - - if max_spans is not None: - warnings.warn( - "The `max_spans` parameter of `LangchainIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - manager._configure = _wrap_configure(manager._configure) - - if AgentExecutor is not None: - AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) - AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) - - # Patch embeddings providers - _patch_embeddings_provider(OpenAIEmbeddings) - _patch_embeddings_provider(AzureOpenAIEmbeddings) - _patch_embeddings_provider(VertexAIEmbeddings) - _patch_embeddings_provider(BedrockEmbeddings) - _patch_embeddings_provider(CohereEmbeddings) - _patch_embeddings_provider(MistralAIEmbeddings) - _patch_embeddings_provider(HuggingFaceEmbeddings) - _patch_embeddings_provider(OllamaEmbeddings) - - -class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] - """Callback handler that creates Sentry spans.""" - - def __init__( - self, max_span_map_size: "Optional[int]", include_prompts: bool - ) -> None: - self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() - self.max_span_map_size = max_span_map_size - self.include_prompts = include_prompts - - def gc_span_map(self) -> None: - if self.max_span_map_size is not None: - while len(self.span_map) > self.max_span_map_size: - run_id, span = self.span_map.popitem(last=False) - self._exit_span(span, run_id) - - def _handle_error(self, run_id: "UUID", error: "Any") -> None: - is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) - - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if is_ignored: - span.__exit__(None, None, None) - else: - sentry_sdk.capture_exception( - error, span._scope if isinstance(span, StreamedSpan) else span.scope - ) - span.__exit__(type(error), error, error.__traceback__) - - del self.span_map[run_id] - - def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": - # Transform content to handle multimodal data (images, audio, video, files) - transformed_content = _transform_langchain_message_content(message.content) - parsed = {"role": message.type, "content": transformed_content} - parsed.update(message.additional_kwargs) - return parsed - - def _create_span( - self: "SentryLangchainCallback", - run_id: "UUID", - parent_id: "Optional[Any]", - op: str, - name: str, - origin: str, - ) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span = None - if parent_id: - parent_span: "Optional[Union[sentry_sdk.tracing.Span, StreamedSpan]]" = ( - self.span_map.get(parent_id) - ) - if parent_span: - span = ( - sentry_sdk.traces.start_span( - parent_span=parent_span, - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if isinstance(parent_span, StreamedSpan) - else parent_span.start_child(op=op, name=name, origin=origin) - ) - - if span is None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span = ( - sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": origin, - }, - ) - if span_streaming - else sentry_sdk.start_span(op=op, name=name, origin=origin) - ) - - span.__enter__() - self.span_map[run_id] = span - self.gc_span_map() - return span - - def _exit_span( - self: "SentryLangchainCallback", - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - run_id: "UUID", - ) -> None: - span.__exit__(None, None, None) - del self.span_map[run_id] - - def on_llm_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - prompts: "List[str]", - *, - run_id: "UUID", - tags: "Optional[List[str]]" = None, - parent_run_id: "Optional[UUID]" = None, - metadata: "Optional[Dict[str, Any]]" = None, - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - parent_run_id, - op=OP.GEN_AI_TEXT_COMPLETION, - name=f"text_completion {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "text_completion") - - run_name = kwargs.get("name") - if run_name: - set_on_span(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - if model: - set_on_span( - SPANDATA.GEN_AI_REQUEST_MODEL, - model, - ) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - normalized_messages = [ - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, - "content": {"type": "text", "text": prompt}, - } - for prompt in prompts - ] - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - messages: "List[List[BaseMessage]]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - all_params = kwargs.get("invocation_params", {}) - all_params.update(serialized.get("kwargs", {})) - - model = ( - all_params.get("model") - or all_params.get("model_name") - or all_params.get("model_id") - or "" - ) - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_CHAT, - name=f"chat {model}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - if model: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - ai_system = _get_ai_system(all_params) - if ai_system: - set_on_span(SPANDATA.GEN_AI_SYSTEM, ai_system) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - for key, attribute in DATA_FIELDS.items(): - if key in all_params and all_params[key] is not None: - set_data_normalized(span, attribute, all_params[key], unpack=False) - - _set_tools_on_span(span, all_params.get("tools")) - - if should_send_default_pii() and self.include_prompts: - system_instructions = _get_system_instructions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - normalized_messages = [] - for list_ in messages: - for message in list_: - if message.type == "system": - continue - - normalized_messages.append( - self._normalize_langchain_message(message) - ) - normalized_messages = normalize_message_roles(normalized_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - def on_chat_model_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_end( - self: "SentryLangchainCallback", - response: "LLMResult", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - try: - generation = response.generations[0][0] - except IndexError: - generation = None - - if generation is not None: - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - - try: - response_model = generation.message.response_metadata.get( - "model_name" - ) - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - except AttributeError: - pass - - try: - finish_reason = generation.generation_info.get("finish_reason") - if finish_reason is not None: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - [finish_reason], - ) - except AttributeError: - pass - - try: - if should_send_default_pii() and self.include_prompts: - tool_calls = getattr(generation.message, "tool_calls", None) - if tool_calls is not None and tool_calls != []: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, - ) - except AttributeError: - pass - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - [[x.text for x in list_] for list_ in response.generations], - ) - - _record_token_usage(span, response) - self._exit_span(span, run_id) - - def on_llm_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when LLM errors.""" - self._handle_error(run_id, error) - - def on_chat_model_error( - self: "SentryLangchainCallback", - error: "Union[Exception, KeyboardInterrupt]", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when Chat Model errors.""" - self._handle_error(run_id, error) - - def on_agent_finish( - self: "SentryLangchainCallback", - finish: "AgentFinish", - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() - ) - - self._exit_span(span, run_id) - - def on_tool_start( - self: "SentryLangchainCallback", - serialized: "Dict[str, Any]", - input_str: str, - *, - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool starts running.""" - with capture_internal_exceptions(): - if not run_id: - return - - tool_name = serialized.get("name") or kwargs.get("name") or "" - - span = self._create_span( - run_id, - kwargs.get("parent_run_id"), - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}".strip(), - origin=LangchainIntegration.origin, - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - set_on_span(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - tool_description = serialized.get("description") - if tool_description is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) - - agent_metadata = kwargs.get("metadata") - if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"]) - - run_name = kwargs.get("name") - if run_name: - set_on_span( - SPANDATA.GEN_AI_FUNCTION_ID, - run_name, - ) - - if should_send_default_pii() and self.include_prompts: - set_data_normalized( - span, - SPANDATA.GEN_AI_TOOL_INPUT, - kwargs.get("inputs", [input_str]), - ) - - def on_tool_end( - self: "SentryLangchainCallback", output: str, *, run_id: "UUID", **kwargs: "Any" - ) -> "Any": - """Run when tool ends running.""" - with capture_internal_exceptions(): - if not run_id or run_id not in self.span_map: - return - - span = self.span_map[run_id] - - if should_send_default_pii() and self.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) - - self._exit_span(span, run_id) - - def on_tool_error( - self, - error: "SentryLangchainCallback", - *args: "Union[Exception, KeyboardInterrupt]", - run_id: "UUID", - **kwargs: "Any", - ) -> "Any": - """Run when tool errors.""" - self._handle_error(run_id, error) - - -def _extract_tokens( - token_usage: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - if not token_usage: - return None, None, None - - input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value( - token_usage, "input_tokens" - ) - output_tokens = _get_value(token_usage, "completion_tokens") or _get_value( - token_usage, "output_tokens" - ) - total_tokens = _get_value(token_usage, "total_tokens") - - return input_tokens, output_tokens, total_tokens - - -def _extract_tokens_from_generations( - generations: "Any", -) -> "tuple[Optional[int], Optional[int], Optional[int]]": - """Extract token usage from response.generations structure.""" - if not generations: - return None, None, None - - total_input = 0 - total_output = 0 - total_total = 0 - - for gen_list in generations: - for gen in gen_list: - token_usage = _get_token_usage(gen) - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - total_input += input_tokens if input_tokens is not None else 0 - total_output += output_tokens if output_tokens is not None else 0 - total_total += total_tokens if total_tokens is not None else 0 - - return ( - total_input if total_input > 0 else None, - total_output if total_output > 0 else None, - total_total if total_total > 0 else None, - ) - - -def _get_token_usage(obj: "Any") -> "Optional[Dict[str, Any]]": - """ - Check multiple paths to extract token usage from different objects. - """ - possible_names = ("usage", "token_usage", "usage_metadata") - - message = _get_value(obj, "message") - if message is not None: - for name in possible_names: - usage = _get_value(message, name) - if usage is not None: - return usage - - llm_output = _get_value(obj, "llm_output") - if llm_output is not None: - for name in possible_names: - usage = _get_value(llm_output, name) - if usage is not None: - return usage - - for name in possible_names: - usage = _get_value(obj, name) - if usage is not None: - return usage - - return None - - -def _record_token_usage(span: "Union[Span, StreamedSpan]", response: "Any") -> None: - token_usage = _get_token_usage(response) - if token_usage: - input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage) - else: - input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations( - response.generations - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) - - -def _get_request_data( - obj: "Any", args: "Any", kwargs: "Any" -) -> "tuple[Optional[str], Optional[List[Any]]]": - """ - Get the agent name and available tools for the agent. - """ - agent = getattr(obj, "agent", None) - runnable = getattr(agent, "runnable", None) - runnable_config = getattr(runnable, "config", {}) - tools = ( - getattr(obj, "tools", None) - or getattr(agent, "tools", None) - or runnable_config.get("tools") - or runnable_config.get("available_tools") - ) - tools = tools if tools and len(tools) > 0 else None - - try: - agent_name = None - if len(args) > 1: - agent_name = args[1].get("run_name") - if agent_name is None: - agent_name = runnable_config.get("run_name") - except Exception: - pass - - return (agent_name, tools) - - -def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": - """Parse and simplify tools into a cleaner format.""" - if not tools: - return None - - if not isinstance(tools, (list, tuple)): - return None - - simplified_tools = [] - for tool in tools: - try: - if isinstance(tool, dict): - if "function" in tool and isinstance(tool["function"], dict): - func = tool["function"] - simplified_tool = { - "name": func.get("name"), - "description": func.get("description"), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif "name" in tool: - simplified_tool = { - "name": tool.get("name"), - "description": tool.get("description"), - } - simplified_tools.append(simplified_tool) - else: - name = ( - tool.get("name") - or tool.get("tool_name") - or tool.get("function_name") - ) - if name: - simplified_tools.append( - { - "name": name, - "description": tool.get("description") - or tool.get("desc"), - } - ) - elif hasattr(tool, "name"): - simplified_tool = { - "name": getattr(tool, "name", None), - "description": getattr(tool, "description", None) - or getattr(tool, "desc", None), - } - if simplified_tool["name"]: - simplified_tools.append(simplified_tool) - elif hasattr(tool, "__name__"): - simplified_tools.append( - { - "name": tool.__name__, - "description": getattr(tool, "__doc__", None), - } - ) - else: - tool_str = str(tool) - if tool_str and tool_str != "": - simplified_tools.append({"name": tool_str, "description": None}) - except Exception: - continue - - return simplified_tools if simplified_tools else None - - -def _set_tools_on_span(span: "Union[Span, StreamedSpan]", tools: "Any") -> None: - """Set available tools data on a span if tools are provided.""" - if tools is not None: - simplified_tools = _simplify_langchain_tools(tools) - if simplified_tools: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - simplified_tools, - unpack=False, - ) - - -def _wrap_configure(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_configure( - callback_manager_cls: type, - inheritable_callbacks: "Callbacks" = None, - local_callbacks: "Callbacks" = None, - *args: "Any", - **kwargs: "Any", - ) -> "Any": - integration = sentry_sdk.get_client().get_integration(LangchainIntegration) - if integration is None: - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - local_callbacks = local_callbacks or [] - - # Handle each possible type of local_callbacks. For each type, we - # extract the list of callbacks to check for SentryLangchainCallback, - # and define a function that would add the SentryLangchainCallback - # to the existing callbacks list. - if isinstance(local_callbacks, BaseCallbackManager): - callbacks_list = local_callbacks.handlers - elif isinstance(local_callbacks, BaseCallbackHandler): - callbacks_list = [local_callbacks] - elif isinstance(local_callbacks, list): - callbacks_list = local_callbacks - else: - logger.debug("Unknown callback type: %s", local_callbacks) - # Just proceed with original function call - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - # Handle each possible type of inheritable_callbacks. - if isinstance(inheritable_callbacks, BaseCallbackManager): - inheritable_callbacks_list = inheritable_callbacks.handlers - elif isinstance(inheritable_callbacks, list): - inheritable_callbacks_list = inheritable_callbacks - else: - inheritable_callbacks_list = [] - - if not any( - isinstance(cb, SentryLangchainCallback) - for cb in itertools.chain(callbacks_list, inheritable_callbacks_list) - ): - sentry_handler = SentryLangchainCallback( - integration.max_spans, - integration.include_prompts, - ) - if isinstance(local_callbacks, BaseCallbackManager): - local_callbacks = local_callbacks.copy() - local_callbacks.handlers = [ - *local_callbacks.handlers, - sentry_handler, - ] - elif isinstance(local_callbacks, BaseCallbackHandler): - local_callbacks = [local_callbacks, sentry_handler] - else: - local_callbacks = [*local_callbacks, sentry_handler] - - return f( - callback_manager_cls, - inheritable_callbacks, - local_callbacks, - *args, - **kwargs, - ) - - return new_configure - - -def _wrap_agent_executor_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: False, - }, - ) as span: - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - else: - start_span_function = get_start_span_function() - - with start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) as span: - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - - _set_tools_on_span(span, tools) - - # Run the agent - result = f(self, *args, **kwargs) - - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - return result - - return new_invoke - - -def _wrap_agent_executor_stream(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - run_name, tools = _get_request_data(self, args, kwargs) - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_RESPONSE_STREAMING: True, - }, - ) - - if run_name: - span.set_attribute(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - else: - start_span_function = get_start_span_function() - - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {run_name}" if run_name else "invoke_agent", - origin=LangchainIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - if run_name: - span.set_data(SPANDATA.GEN_AI_FUNCTION_ID, run_name) - - _set_tools_on_span(span, tools) - - input = args[0].get("input") if len(args) >= 1 else None - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Run the agent - result = f(self, *args, **kwargs) - - old_iterator = result - - def new_iterator() -> "Iterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - async def new_iterator_async() -> "AsyncIterator[Any]": - exc_info: "tuple[Any, Any, Any]" = (None, None, None) - try: - async for event in old_iterator: - yield event - - try: - output = event.get("output") - except Exception: - output = None - - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - - span.__exit__(None, None, None) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span.__exit__(*exc_info) - raise - - if str(type(result)) == "": - result = new_iterator_async() - else: - result = new_iterator() - - return result - - return new_stream - - -def _patch_embeddings_provider(provider_class: "Any") -> None: - """Patch an embeddings provider class with monitoring wrappers.""" - if provider_class is None: - return - - if hasattr(provider_class, "embed_documents"): - provider_class.embed_documents = _wrap_embedding_method( - provider_class.embed_documents - ) - if hasattr(provider_class, "embed_query"): - provider_class.embed_query = _wrap_embedding_method(provider_class.embed_query) - if hasattr(provider_class, "aembed_documents"): - provider_class.aembed_documents = _wrap_async_embedding_method( - provider_class.aembed_documents - ) - if hasattr(provider_class, "aembed_query"): - provider_class.aembed_query = _wrap_async_embedding_method( - provider_class.aembed_query - ) - - -def _wrap_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap sync embedding methods (embed_documents and embed_query).""" - - @wraps(f) - def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = f(self, *args, **kwargs) - return result - - return new_embedding_method - - -def _wrap_async_embedding_method(f: "Callable[..., Any]") -> "Callable[..., Any]": - """Wrap async embedding methods (aembed_documents and aembed_query).""" - - @wraps(f) - async def new_async_embedding_method( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LangchainIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - model_name = getattr(self, "model", None) or getattr(self, "model_name", None) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model_name}" if model_name else "embeddings", - attributes={ - "sentry.op": OP.GEN_AI_EMBEDDINGS, - "sentry.origin": LangchainIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "embeddings", - }, - ) as span: - if model_name: - span.set_attribute(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model_name}" if model_name else "embeddings", - origin=LangchainIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - if model_name: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Capture input if PII is allowed - if ( - should_send_default_pii() - and integration.include_prompts - and len(args) > 0 - ): - input_data = args[0] - # Normalize to list format - texts = input_data if isinstance(input_data, list) else [input_data] - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, texts, unpack=False - ) - - result = await f(self, *args, **kwargs) - return result - - return new_async_embedding_method diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py deleted file mode 100644 index 3d3856a913..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/langgraph.py +++ /dev/null @@ -1,515 +0,0 @@ -from functools import wraps -from typing import Any, Callable, List, Optional - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration - -# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. -from sentry_sdk.integrations.langchain import LangchainIntegration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -try: - from langgraph.errors import GraphBubbleUp - from langgraph.graph import StateGraph - from langgraph.pregel import Pregel -except ImportError: - raise DidNotEnable("langgraph not installed") - - -class LanggraphIntegration(Integration): - identifier = "langgraph" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) - # LangGraph lets users create agents using a StateGraph or the Functional API. - # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and - # the functional API execute on a Pregel instance. Pregel is the runtime for the graph - # and the invocation happens on Pregel, so patching the invoke methods takes care of both. - # The streaming methods are not patched, because due to some internal reasons, LangGraph - # will automatically patch the streaming methods to run through invoke, and by doing this - # we prevent duplicate spans for invocations. - StateGraph.compile = _wrap_state_graph_compile(StateGraph.compile) - if hasattr(Pregel, "invoke"): - Pregel.invoke = _wrap_pregel_invoke(Pregel.invoke) - if hasattr(Pregel, "ainvoke"): - Pregel.ainvoke = _wrap_pregel_ainvoke(Pregel.ainvoke) - - -def _get_graph_name(graph_obj: "Any") -> "Optional[str]": - for attr in ["name", "graph_name", "__name__", "_name"]: - if hasattr(graph_obj, attr): - name = getattr(graph_obj, attr) - if name and isinstance(name, str): - return name - return None - - -def _normalize_langgraph_message(message: "Any") -> "Any": - if not hasattr(message, "content"): - return None - - parsed = {"role": getattr(message, "type", None), "content": message.content} - - for attr in [ - "name", - "tool_calls", - "function_call", - "tool_call_id", - "response_metadata", - ]: - if hasattr(message, attr): - value = getattr(message, attr) - if value is not None: - parsed[attr] = value - - return parsed - - -def _parse_langgraph_messages(state: "Any") -> "Optional[List[Any]]": - if not state: - return None - - messages = None - - if isinstance(state, dict): - messages = state.get("messages") - elif hasattr(state, "messages"): - messages = state.messages - elif hasattr(state, "get") and callable(state.get): - try: - messages = state.get("messages") - except Exception: - pass - - if not messages or not isinstance(messages, (list, tuple)): - return None - - normalized_messages = [] - for message in messages: - try: - normalized = _normalize_langgraph_message(message) - if normalized: - normalized_messages.append(normalized) - except Exception: - continue - - return normalized_messages if normalized_messages else None - - -def _wrap_state_graph_compile(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_compile(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None or has_span_streaming_enabled(client.options): - return f(self, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GEN_AI_CREATE_AGENT, - origin=LanggraphIntegration.origin, - ) as span: - compiled_graph = f(self, *args, **kwargs) - - compiled_graph_name = getattr(compiled_graph, "name", None) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "create_agent") - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, compiled_graph_name) - - if compiled_graph_name: - span.description = f"create_agent {compiled_graph_name}" - else: - span.description = "create_agent" - - if kwargs.get("model", None) is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, kwargs.get("model")) - - tools = None - get_graph = getattr(compiled_graph, "get_graph", None) - if get_graph and callable(get_graph): - graph_obj = compiled_graph.get_graph() - nodes = getattr(graph_obj, "nodes", None) - if nodes and isinstance(nodes, dict): - tools_node = nodes.get("tools") - if tools_node: - data = getattr(tools_node, "data", None) - if data and hasattr(data, "tools_by_name"): - tools = list(data.tools_by_name.keys()) - - if tools is not None: - span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools) - - return compiled_graph - - return new_compile - - -def _wrap_pregel_invoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - else: - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - # Store input messages to later compare with output - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_invoke - - -def _wrap_pregel_ainvoke(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(LanggraphIntegration) - if integration is None: - return await f(self, *args, **kwargs) - - graph_name = _get_graph_name(self) - span_name = ( - f"invoke_agent {graph_name}".strip() if graph_name else "invoke_agent" - ) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": LanggraphIntegration.origin, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) as span: - if graph_name: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles( - input_messages - ) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - with get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, - origin=LanggraphIntegration.origin, - ) as span: - if graph_name: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, graph_name) - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, graph_name) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts - ): - input_messages = _parse_langgraph_messages(args[0]) - if input_messages: - normalized_input_messages = normalize_message_roles(input_messages) - - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages( - normalized_input_messages, span, scope - ) - if should_truncate_gen_ai_input(client.options) - else normalized_input_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - result = await f(self, *args, **kwargs) - - _set_response_attributes(span, input_messages, result, integration) - - return result - - return new_ainvoke - - -def _get_new_messages( - input_messages: "Optional[List[Any]]", output_messages: "Optional[List[Any]]" -) -> "Optional[List[Any]]": - """Extract only the new messages added during this invocation.""" - if not output_messages: - return None - - if not input_messages: - return output_messages - - # only return the new messages, aka the output messages that are not in the input messages - input_count = len(input_messages) - new_messages = ( - output_messages[input_count:] if len(output_messages) > input_count else [] - ) - - return new_messages if new_messages else None - - -def _extract_llm_response_text(messages: "Optional[List[Any]]") -> "Optional[str]": - if not messages: - return None - - for message in reversed(messages): - if isinstance(message, dict): - role = message.get("role") - if role in ["assistant", "ai"]: - content = message.get("content") - if content and isinstance(content, str): - return content - - return None - - -def _extract_tool_calls(messages: "Optional[List[Any]]") -> "Optional[List[Any]]": - if not messages: - return None - - tool_calls = [] - for message in messages: - if isinstance(message, dict): - msg_tool_calls = message.get("tool_calls") - if msg_tool_calls and isinstance(msg_tool_calls, list): - tool_calls.extend(msg_tool_calls) - - return tool_calls if tool_calls else None - - -def _set_usage_data(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - input_tokens = 0 - output_tokens = 0 - total_tokens = 0 - - for message in messages: - response_metadata = message.get("response_metadata") - if response_metadata is None: - continue - - token_usage = response_metadata.get("token_usage") - if not token_usage: - continue - - input_tokens += int(token_usage.get("prompt_tokens", 0)) - output_tokens += int(token_usage.get("completion_tokens", 0)) - total_tokens += int(token_usage.get("total_tokens", 0)) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if input_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens) - - if output_tokens > 0: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens) - - if total_tokens > 0: - set_on_span( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - total_tokens, - ) - - -def _set_response_model_name(span: "sentry_sdk.tracing.Span", messages: "Any") -> None: - if len(messages) == 0: - return - - last_message = messages[-1] - response_metadata = last_message.get("response_metadata") - if response_metadata is None: - return - - model_name = response_metadata.get("model_name") - if model_name is None: - return - - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, model_name) - - -def _set_response_attributes( - span: "Any", - input_messages: "Optional[List[Any]]", - result: "Any", - integration: "LanggraphIntegration", -) -> None: - parsed_response_messages = _parse_langgraph_messages(result) - new_messages = _get_new_messages(input_messages, parsed_response_messages) - - if new_messages is None: - return - - _set_usage_data(span, new_messages) - _set_response_model_name(span, new_messages) - - if not (should_send_default_pii() and integration.include_prompts): - return - - llm_response_text = _extract_llm_response_text(new_messages) - if llm_response_text: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, llm_response_text) - elif new_messages: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, new_messages) - else: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, result) - - tool_calls = _extract_tool_calls(new_messages) - if tool_calls: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(tool_calls), - unpack=False, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py deleted file mode 100644 index 3c2c76450c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/launchdarkly.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - import ldclient - from ldclient.hook import Hook, Metadata - - if TYPE_CHECKING: - from typing import Any - - from ldclient import LDClient - from ldclient.evaluation import EvaluationDetail - from ldclient.hook import EvaluationSeriesContext -except ImportError: - raise DidNotEnable("LaunchDarkly is not installed") - - -class LaunchDarklyIntegration(Integration): - identifier = "launchdarkly" - - def __init__(self, ld_client: "LDClient | None" = None) -> None: - """ - :param client: An initialized LDClient instance. If a client is not provided, this - integration will attempt to use the shared global instance. - """ - try: - client = ld_client or ldclient.get() - except Exception as exc: - raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) - - if not client.is_initialized(): - raise DidNotEnable("LaunchDarkly client is not initialized.") - - # Register the flag collection hook with the LD client. - client.add_hook(LaunchDarklyHook()) - - @staticmethod - def setup_once() -> None: - pass - - -class LaunchDarklyHook(Hook): - @property - def metadata(self) -> "Metadata": - return Metadata(name="sentry-flag-auditor") - - def after_evaluation( - self, - series_context: "EvaluationSeriesContext", - data: "dict[Any, Any]", - detail: "EvaluationDetail", - ) -> "dict[Any, Any]": - if isinstance(detail.value, bool): - add_feature_flag(series_context.key, detail.value) - - return data - - def before_evaluation( - self, series_context: "EvaluationSeriesContext", data: "dict[Any, Any]" - ) -> "dict[Any, Any]": - return data # No-op. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py deleted file mode 100644 index 49ead6b068..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litellm.py +++ /dev/null @@ -1,376 +0,0 @@ -import copy -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - set_data_normalized, - transform_openai_content_part, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import event_from_exception - -if TYPE_CHECKING: - from datetime import datetime - from typing import Any, Dict, List - -try: - import litellm # type: ignore[import-not-found] - from litellm import failure_callback, input_callback, success_callback -except ImportError: - raise DidNotEnable("LiteLLM not installed") - - -# Stash the span on a top-level key of the per-request kwargs dict litellm passes -# to every callback, so it lives and dies with the request. -_SPAN_KEY = "_sentry_span" - - -def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None: - kwargs[_SPAN_KEY] = span - - -def _peek_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.get(_SPAN_KEY) - - -def _pop_span(kwargs: "Dict[str, Any]") -> "Any": - return kwargs.pop(_SPAN_KEY, None) - - -def _convert_message_parts(messages: "List[Dict[str, Any]]") -> "List[Dict[str, Any]]": - """ - Convert the message parts from OpenAI format to the `gen_ai.request.messages` format - using the OpenAI-specific transformer (LiteLLM uses OpenAI's message format). - - Deep copies messages to avoid mutating original kwargs. - """ - # Deep copy to avoid mutating original messages from kwargs - messages = copy.deepcopy(messages) - - for message in messages: - if not isinstance(message, dict): - continue - content = message.get("content") - if isinstance(content, (list, tuple)): - transformed = [] - for item in content: - if isinstance(item, dict): - result = transform_openai_content_part(item) - # If transformation succeeded, use the result; otherwise keep original - transformed.append(result if result is not None else item) - else: - transformed.append(item) - message["content"] = transformed - return messages - - -def _input_callback(kwargs: "Dict[str, Any]") -> None: - """Handle the start of a request.""" - client = sentry_sdk.get_client() - integration = client.get_integration(LiteLLMIntegration) - - if integration is None: - return - - # Get key parameters - full_model = kwargs.get("model", "") - try: - model, provider, _, _ = litellm.get_llm_provider(full_model) - except Exception: - model = full_model - provider = "unknown" - - call_type = kwargs.get("call_type", None) - if call_type == "embedding" or call_type == "aembedding": - operation = "embeddings" - else: - operation = "chat" - - # Start a new span/transaction - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"{operation} {model}", - attributes={ - "sentry.op": ( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - "sentry.origin": LiteLLMIntegration.origin, - }, - ) - else: - span = get_start_span_function()( - op=( - consts.OP.GEN_AI_CHAT - if operation == "chat" - else consts.OP.GEN_AI_EMBEDDINGS - ), - name=f"{operation} {model}", - origin=LiteLLMIntegration.origin, - ) - span.__enter__() - - _store_span(kwargs, span) - - # Set basic data - set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, provider) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) - - # Record input/messages if allowed - if should_send_default_pii() and integration.include_prompts: - if operation == "embeddings": - # For embeddings, look for the 'input' parameter - embedding_input = kwargs.get("input") - if embedding_input: - scope = sentry_sdk.get_current_scope() - # Normalize to list format - input_list = ( - embedding_input - if isinstance(embedding_input, list) - else [embedding_input] - ) - client = sentry_sdk.get_client() - messages_data = ( - truncate_and_annotate_embedding_inputs(input_list, span, scope) - if should_truncate_gen_ai_input(client.options) - else input_list - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_EMBEDDINGS_INPUT, - messages_data, - unpack=False, - ) - else: - # For chat, look for the 'messages' parameter - messages = kwargs.get("messages", []) - if messages: - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages = _convert_message_parts(messages) - messages_data = ( - truncate_and_annotate_messages(messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - # Record other parameters - params = { - "model": SPANDATA.GEN_AI_REQUEST_MODEL, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - } - for key, attribute in params.items(): - value = kwargs.get(key) - if value is not None: - set_data_normalized(span, attribute, value) - - -async def _async_input_callback(kwargs: "Dict[str, Any]") -> None: - return _input_callback(kwargs) - - -def _success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle successful completion.""" - - span = _peek_span(kwargs) - if span is None: - return - - integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration) - if integration is None: - return - - try: - # Record model information - if hasattr(completion_response, "model"): - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model - ) - - # Record response content if allowed - if should_send_default_pii() and integration.include_prompts: - if hasattr(completion_response, "choices"): - response_messages = [] - for choice in completion_response.choices: - if hasattr(choice, "message"): - if hasattr(choice.message, "model_dump"): - response_messages.append(choice.message.model_dump()) - elif hasattr(choice.message, "dict"): - response_messages.append(choice.message.dict()) - else: - # Fallback for basic message objects - msg = {} - if hasattr(choice.message, "role"): - msg["role"] = choice.message.role - if hasattr(choice.message, "content"): - msg["content"] = choice.message.content - if hasattr(choice.message, "tool_calls"): - msg["tool_calls"] = choice.message.tool_calls - response_messages.append(msg) - - if response_messages: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages - ) - - # Record token usage - if hasattr(completion_response, "usage"): - usage = completion_response.usage - record_token_usage( - span, - input_tokens=getattr(usage, "prompt_tokens", None), - output_tokens=getattr(usage, "completion_tokens", None), - total_tokens=getattr(usage, "total_tokens", None), - ) - - finally: - is_streaming = kwargs.get("stream") - # Callback is fired multiple times when streaming a response. - # Streaming flag checked at https://github.com/BerriAI/litellm/blob/33c3f13443eaf990ac8c6e3da78bddbc2b7d0e7a/litellm/litellm_core_utils/litellm_logging.py#L1603 - if ( - is_streaming is not True - or "complete_streaming_response" in kwargs - or "async_complete_streaming_response" in kwargs - ): - span = _pop_span(kwargs) - if span is not None: - span.__exit__(None, None, None) - - -async def _async_success_callback( - kwargs: "Dict[str, Any]", - completion_response: "Any", - start_time: "datetime", - end_time: "datetime", -) -> None: - return _success_callback( - kwargs, - completion_response, - start_time, - end_time, - ) - - -def _failure_callback( - kwargs: "Dict[str, Any]", - exception: Exception, - start_time: "datetime", - end_time: "datetime", -) -> None: - """Handle request failure.""" - span = _pop_span(kwargs) - if span is None: - return - - try: - # Capture the exception - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "litellm", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - finally: - # Always finish the span and clean up - span.__exit__(type(exception), exception, None) - - -class LiteLLMIntegration(Integration): - """ - LiteLLM integration for Sentry. - - This integration automatically captures LiteLLM API calls and sends them to Sentry - for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM - supports, including OpenAI, Anthropic, Google, Cohere, and many others. - - Features: - - Automatic exception capture for all LiteLLM calls - - Token usage tracking across all providers - - Provider detection and attribution - - Input/output message capture (configurable) - - Streaming response support - - Cost tracking integration - - Usage: - - ```python - import litellm - import sentry_sdk - - # Initialize Sentry with the LiteLLM integration - sentry_sdk.init( - dsn="your-dsn", - send_default_pii=True - integrations=[ - sentry_sdk.integrations.LiteLLMIntegration( - include_prompts=True # Set to False to exclude message content - ) - ] - ) - - # All LiteLLM calls will now be monitored - response = litellm.completion( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": "Hello!"}] - ) - ``` - - Configuration: - - include_prompts (bool): Whether to include prompts and responses in spans. - Defaults to True. Set to False to exclude potentially sensitive data. - """ - - identifier = "litellm" - origin = f"auto.ai.{identifier}" - - def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """Set up LiteLLM callbacks for monitoring.""" - litellm.input_callback = input_callback or [] - if _input_callback not in litellm.input_callback: - litellm.input_callback.append(_input_callback) - if _async_input_callback not in litellm.input_callback: - litellm.input_callback.append(_async_input_callback) - - litellm.success_callback = success_callback or [] - if _success_callback not in litellm.success_callback: - litellm.success_callback.append(_success_callback) - if _async_success_callback not in litellm.success_callback: - litellm.success_callback.append(_async_success_callback) - - litellm.failure_callback = failure_callback or [] - if _failure_callback not in litellm.failure_callback: - litellm.failure_callback.append(_failure_callback) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py deleted file mode 100644 index f0c90a7921..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/litestar.py +++ /dev/null @@ -1,364 +0,0 @@ -from collections.abc import Set -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from litestar import Litestar, Request # type: ignore - from litestar.data_extractors import ConnectionDataExtractor # type: ignore - from litestar.exceptions import HTTPException # type: ignore - from litestar.handlers.base import BaseRouteHandler # type: ignore - from litestar.middleware import DefineMiddleware # type: ignore - from litestar.routes.http import HTTPRoute # type: ignore -except ImportError: - raise DidNotEnable("Litestar is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from litestar.middleware import MiddlewareProtocol - from litestar.types import ( # type: ignore - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from litestar.types import ( - Scope as LitestarScope, - ) - from litestar.types.asgi_types import ASGIApp # type: ignore - - from sentry_sdk._types import Event, Hint - -_DEFAULT_TRANSACTION_NAME = "generic Litestar request" - - -class LitestarIntegration(Integration): - identifier = "litestar" - origin = f"auto.http.{identifier}" - - def __init__( - self, - failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - ) -> None: - self.failed_request_status_codes = failed_request_status_codes - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. - # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following - # (among other things): - # 1. Logs them, some at least (such as 500s) as errors - # 2. Calls after_exception hooks - # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event - # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will - # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. - ignore_logger("litestar") - - -class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - def _capture_request_exception(self, exc: Exception) -> None: - """Avoid catching exceptions from request handlers. - - Those exceptions are already handled in Litestar.after_exception handler. - We still catch exceptions from application lifespan handlers. - """ - pass - - -def patch_app_init() -> None: - """ - Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. - See: - - https://docs.litestar.dev/2/usage/applications.html#after-exception - - https://docs.litestar.dev/2/usage/middleware/using-middleware.html - """ - old__init__ = Litestar.__init__ - - @ensure_integration_enabled(LitestarIntegration, old__init__) - def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: - kwargs["after_exception"] = [ - exception_handler, - *(kwargs.get("after_exception") or []), - ] - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Litestar.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryLitestarASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "LitestarScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(LitestarIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=middleware_name, - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR, - "sentry.origin": LitestarIntegration.origin, - }, - ) as middleware_span: - middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.traces.start_span( - name=getattr(receive, "__qualname__", str(receive)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.traces.start_span( - name=getattr(send, "__qualname__", str(send)), - attributes={ - "sentry.op": OP.MIDDLEWARE_LITESTAR_SEND, - "sentry.origin": LitestarIntegration.origin, - }, - ) as span: - span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - else: - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR, - name=middleware_name, - origin=LitestarIntegration.origin, - ) as middleware_span: - middleware_span.set_tag("litestar.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if client.get_integration(LitestarIntegration) is None: - return await receive(*args, **kwargs) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if client.get_integration(LitestarIntegration) is None: - return await send(message) - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_LITESTAR_SEND, - name=getattr(send, "__qualname__", str(send)), - origin=LitestarIntegration.origin, - ) as span: - span.set_tag("litestar.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type - elif hasattr(route_handler.fn, "value"): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = LitestarIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if isinstance(scope_user, dict): - return scope_user - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - return None - - -@ensure_integration_enabled(LitestarIntegration) -def exception_handler(exc: Exception, scope: "LitestarScope") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - if isinstance(exc, HTTPException): - integration = sentry_sdk.get_client().get_integration(LitestarIntegration) - if ( - integration is not None - and exc.status_code not in integration.failed_request_status_codes - ): - return - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": LitestarIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py deleted file mode 100644 index a310a0ced6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/logging.py +++ /dev/null @@ -1,476 +0,0 @@ -import logging -import sys -from datetime import datetime, timezone -from fnmatch import fnmatch -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.client import BaseClient -from sentry_sdk.integrations import Integration -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import ( - capture_internal_exceptions, - current_stacktrace, - event_from_exception, - has_logs_enabled, - safe_repr, - to_string, -) - -if TYPE_CHECKING: - from collections.abc import MutableMapping - from logging import LogRecord - from typing import Any, Dict, Optional - -DEFAULT_LEVEL = logging.INFO -DEFAULT_EVENT_LEVEL = logging.ERROR -LOGGING_TO_EVENT_LEVEL = { - logging.NOTSET: "notset", - logging.DEBUG: "debug", - logging.INFO: "info", - logging.WARN: "warning", # WARN is same a WARNING - logging.WARNING: "warning", - logging.ERROR: "error", - logging.FATAL: "fatal", - logging.CRITICAL: "fatal", # CRITICAL is same as FATAL -} - -# Map logging level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - logging.CRITICAL: 21, # fatal - logging.ERROR: 17, # error - logging.WARNING: 13, # warn - logging.INFO: 9, # info - logging.DEBUG: 5, # debug -} - - -# Capturing events from those loggers causes recursion errors. We cannot allow -# the user to unconditionally create events from those loggers under any -# circumstances. -# -# Note: Ignoring by logger name here is better than mucking with thread-locals. -# We do not necessarily know whether thread-locals work 100% correctly in the user's environment. -# -# Events/breadcrumbs and Sentry Logs have separate ignore lists so that -# framework loggers silenced for events (e.g. django.server) can still be -# captured as Sentry Logs. -_IGNORED_LOGGERS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - -_IGNORED_LOGGERS_SENTRY_LOGS = set( - ["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"] -) - - -def ignore_logger( - name: str, -) -> None: - """This disables recording (both in breadcrumbs and as events) calls to - a logger of a specific name. Among other uses, many of our integrations - use this to prevent their actions being recorded as breadcrumbs. Exposed - to users as a way to quiet spammy loggers. - - This does **not** affect Sentry Logs — use - :py:func:`ignore_logger_for_sentry_logs` for that. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS.add(name) - - -def ignore_logger_for_sentry_logs( - name: str, -) -> None: - """This disables recording as Sentry Logs calls to a logger of a - specific name. - - :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``). - """ - _IGNORED_LOGGERS_SENTRY_LOGS.add(name) - - -def unignore_logger( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger` call, re-enabling - recording of breadcrumbs and events for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS.discard(name) - - -def unignore_logger_for_sentry_logs( - name: str, -) -> None: - """Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call, - re-enabling recording of Sentry Logs for the named logger. - - :param name: The name of the logger to unignore. - """ - _IGNORED_LOGGERS_SENTRY_LOGS.discard(name) - - -class LoggingIntegration(Integration): - identifier = "logging" - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - self._handler = None - self._breadcrumb_handler = None - self._sentry_logs_handler = None - - if level is not None: - self._breadcrumb_handler = BreadcrumbHandler(level=level) - - if sentry_logs_level is not None: - self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level) - - if event_level is not None: - self._handler = EventHandler(level=event_level) - - def _handle_record(self, record: "LogRecord") -> None: - if self._handler is not None and record.levelno >= self._handler.level: - self._handler.handle(record) - - if ( - self._breadcrumb_handler is not None - and record.levelno >= self._breadcrumb_handler.level - ): - self._breadcrumb_handler.handle(record) - - def _handle_sentry_logs_record(self, record: "LogRecord") -> None: - if ( - self._sentry_logs_handler is not None - and record.levelno >= self._sentry_logs_handler.level - ): - self._sentry_logs_handler.handle(record) - - @staticmethod - def setup_once() -> None: - old_callhandlers = logging.Logger.callHandlers - - def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any": - # keeping a local reference because the - # global might be discarded on shutdown - ignored_loggers = _IGNORED_LOGGERS - ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS - - try: - return old_callhandlers(self, record) - finally: - # This check is done twice, once also here before we even get - # the integration. Otherwise we have a high chance of getting - # into a recursion error when the integration is resolved - # (this also is slower). - name = record.name.strip() - - handle_events = ( - ignored_loggers is not None and name not in ignored_loggers - ) - handle_sentry_logs = ( - ignored_loggers_sentry_logs is not None - and name not in ignored_loggers_sentry_logs - ) - - if handle_events or handle_sentry_logs: - integration = sentry_sdk.get_client().get_integration( - LoggingIntegration - ) - if integration is not None: - if handle_events: - integration._handle_record(record) - if handle_sentry_logs: - integration._handle_sentry_logs_record(record) - - logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore - - -class _BaseHandler(logging.Handler): - COMMON_RECORD_ATTRS = frozenset( - ( - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "linenno", - "lineno", - "message", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack", - "tags", - "taskName", - "thread", - "threadName", - "stack_info", - ) - ) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - return LOGGING_TO_EVENT_LEVEL.get( - record.levelno, record.levelname.lower() if record.levelname else "" - ) - - def _extra_from_record(self, record: "LogRecord") -> "MutableMapping[str, object]": - return { - k: v - for k, v in vars(record).items() - if k not in self.COMMON_RECORD_ATTRS - and (not isinstance(k, str) or not k.startswith("_")) - } - - -class EventHandler(_BaseHandler): - """ - A logging handler that emits Sentry events for each log record - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - client_options = client.options - - # exc_info might be None or (None, None, None) - # - # exc_info may also be any falsy value due to Python stdlib being - # liberal with what it receives and Celery's billiard being "liberal" - # with what it sends. See - # https://github.com/getsentry/sentry-python/issues/904 - if record.exc_info and record.exc_info[0] is not None: - event, hint = event_from_exception( - record.exc_info, - client_options=client_options, - mechanism={"type": "logging", "handled": True}, - ) - elif (record.exc_info and record.exc_info[0] is None) or record.stack_info: - event = {} - hint = {} - with capture_internal_exceptions(): - event["threads"] = { - "values": [ - { - "stacktrace": current_stacktrace( - include_local_variables=client_options[ - "include_local_variables" - ], - max_value_length=client_options["max_value_length"], - ), - "crashed": False, - "current": True, - } - ] - } - else: - event = {} - hint = {} - - hint["log_record"] = record - - level = self._logging_to_event_level(record) - if level in {"debug", "info", "warning", "error", "critical", "fatal"}: - event["level"] = level # type: ignore[typeddict-item] - event["logger"] = record.name - - if ( - sys.version_info < (3, 11) - and record.name == "py.warnings" - and record.msg == "%s" - ): - # warnings module on Python 3.10 and below sets record.msg to "%s" - # and record.args[0] to the actual warning message. - # This was fixed in https://github.com/python/cpython/pull/30975. - message = record.args[0] - params = () - else: - message = record.msg - params = record.args - - event["logentry"] = { - "message": to_string(message), - "formatted": record.getMessage(), - "params": params, - } - - event["extra"] = self._extra_from_record(record) - - sentry_sdk.capture_event(event, hint=hint) - - -# Legacy name -SentryHandler = EventHandler - - -class BreadcrumbHandler(_BaseHandler): - """ - A logging handler that records breadcrumbs for each log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - return self._emit(record) - - def _emit(self, record: "LogRecord") -> None: - if not self._can_record(record): - return - - sentry_sdk.add_breadcrumb( - self._breadcrumb_from_record(record), hint={"log_record": record} - ) - - def _breadcrumb_from_record(self, record: "LogRecord") -> "Dict[str, Any]": - return { - "type": "log", - "level": self._logging_to_event_level(record), - "category": record.name, - "message": record.message, - "timestamp": datetime.fromtimestamp(record.created, timezone.utc), - "data": self._extra_from_record(record), - } - - -class SentryLogsHandler(_BaseHandler): - """ - A logging handler that records Sentry logs for each Python log record. - - Note that you do not have to use this class if the logging integration is enabled, which it is by default. - """ - - def _can_record(self, record: "LogRecord") -> bool: - """Prevents ignored loggers from recording""" - for logger in _IGNORED_LOGGERS_SENTRY_LOGS: - if fnmatch(record.name.strip(), logger): - return False - return True - - def emit(self, record: "LogRecord") -> "Any": - with capture_internal_exceptions(): - self.format(record) - if not self._can_record(record): - return - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - self._capture_log_from_record(client, record) - - def _capture_log_from_record( - self, client: "BaseClient", record: "LogRecord" - ) -> None: - otel_severity_number, otel_severity_text = _log_level_to_otel( - record.levelno, SEVERITY_TO_OTEL_SEVERITY - ) - project_root = client.options["project_root"] - - attrs: "Any" = self._extra_from_record(record) - attrs["sentry.origin"] = "auto.log.stdlib" - - parameters_set = False - if record.args is not None: - if isinstance(record.args, tuple): - parameters_set = bool(record.args) - for i, arg in enumerate(record.args): - attrs[f"sentry.message.parameter.{i}"] = ( - arg - if isinstance(arg, (str, float, int, bool)) - else safe_repr(arg) - ) - elif isinstance(record.args, dict): - parameters_set = bool(record.args) - for key, value in record.args.items(): - attrs[f"sentry.message.parameter.{key}"] = ( - value - if isinstance(value, (str, float, int, bool)) - else safe_repr(value) - ) - - if parameters_set and isinstance(record.msg, str): - # only include template if there is at least one - # sentry.message.parameter.X set - attrs["sentry.message.template"] = record.msg - - if record.lineno: - attrs["code.line.number"] = record.lineno - - if record.pathname: - if project_root is not None and record.pathname.startswith(project_root): - attrs["code.file.path"] = record.pathname[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record.pathname - - if record.funcName: - attrs["code.function.name"] = record.funcName - - if record.thread: - attrs["thread.id"] = record.thread - if record.threadName: - attrs["thread.name"] = record.threadName - - if record.process: - attrs["process.pid"] = record.process - if record.processName: - attrs["process.executable.name"] = record.processName - if record.name: - attrs["logger.name"] = record.name - - # noinspection PyProtectedMember - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record.message, - "attributes": attrs, - "time_unix_nano": int(record.created * 1e9), - "trace_id": None, - "span_id": None, - }, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py deleted file mode 100644 index dbb724d9a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/loguru.py +++ /dev/null @@ -1,208 +0,0 @@ -import enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.logging import ( - BreadcrumbHandler, - EventHandler, - _BaseHandler, -) -from sentry_sdk.logger import _log_level_to_otel -from sentry_sdk.utils import has_logs_enabled, safe_repr - -if TYPE_CHECKING: - from logging import LogRecord - from typing import Any, Optional - -try: - import loguru - from loguru import logger - from loguru._defaults import LOGURU_FORMAT as DEFAULT_FORMAT - - if TYPE_CHECKING: - from loguru import Message -except ImportError: - raise DidNotEnable("LOGURU is not installed") - - -class LoggingLevels(enum.IntEnum): - TRACE = 5 - DEBUG = 10 - INFO = 20 - SUCCESS = 25 - WARNING = 30 - ERROR = 40 - CRITICAL = 50 - - -DEFAULT_LEVEL = LoggingLevels.INFO.value -DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value - - -SENTRY_LEVEL_FROM_LOGURU_LEVEL = { - "TRACE": "DEBUG", - "DEBUG": "DEBUG", - "INFO": "INFO", - "SUCCESS": "INFO", - "WARNING": "WARNING", - "ERROR": "ERROR", - "CRITICAL": "CRITICAL", -} - -# Map Loguru level numbers to corresponding OTel level numbers -SEVERITY_TO_OTEL_SEVERITY = { - LoggingLevels.CRITICAL: 21, # fatal - LoggingLevels.ERROR: 17, # error - LoggingLevels.WARNING: 13, # warn - LoggingLevels.SUCCESS: 11, # info - LoggingLevels.INFO: 9, # info - LoggingLevels.DEBUG: 5, # debug - LoggingLevels.TRACE: 1, # trace -} - - -class LoguruIntegration(Integration): - identifier = "loguru" - - level: "Optional[int]" = DEFAULT_LEVEL - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL - breadcrumb_format = DEFAULT_FORMAT - event_format = DEFAULT_FORMAT - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL - - def __init__( - self, - level: "Optional[int]" = DEFAULT_LEVEL, - event_level: "Optional[int]" = DEFAULT_EVENT_LEVEL, - breadcrumb_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - event_format: "str | loguru.FormatFunction" = DEFAULT_FORMAT, - sentry_logs_level: "Optional[int]" = DEFAULT_LEVEL, - ) -> None: - LoguruIntegration.level = level - LoguruIntegration.event_level = event_level - LoguruIntegration.breadcrumb_format = breadcrumb_format - LoguruIntegration.event_format = event_format - LoguruIntegration.sentry_logs_level = sentry_logs_level - - @staticmethod - def setup_once() -> None: - if LoguruIntegration.level is not None: - logger.add( - LoguruBreadcrumbHandler(level=LoguruIntegration.level), - level=LoguruIntegration.level, - format=LoguruIntegration.breadcrumb_format, - ) - - if LoguruIntegration.event_level is not None: - logger.add( - LoguruEventHandler(level=LoguruIntegration.event_level), - level=LoguruIntegration.event_level, - format=LoguruIntegration.event_format, - ) - - if LoguruIntegration.sentry_logs_level is not None: - logger.add( - loguru_sentry_logs_handler, - level=LoguruIntegration.sentry_logs_level, - ) - - -class _LoguruBaseHandler(_BaseHandler): - def __init__(self, *args: "Any", **kwargs: "Any") -> None: - if kwargs.get("level"): - kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( - kwargs.get("level", ""), DEFAULT_LEVEL - ) - - super().__init__(*args, **kwargs) - - def _logging_to_event_level(self, record: "LogRecord") -> str: - try: - return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ - LoggingLevels(record.levelno).name - ].lower() - except (ValueError, KeyError): - return record.levelname.lower() if record.levelname else "" - - -class LoguruEventHandler(_LoguruBaseHandler, EventHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" - - pass - - -class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): - """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" - - pass - - -def loguru_sentry_logs_handler(message: "Message") -> None: - # This is intentionally a callable sink instead of a standard logging handler - # since otherwise we wouldn't get direct access to message.record - client = sentry_sdk.get_client() - - if not client.is_active(): - return - - if not has_logs_enabled(client.options): - return - - record = message.record - - if ( - LoguruIntegration.sentry_logs_level is None - or record["level"].no < LoguruIntegration.sentry_logs_level - ): - return - - otel_severity_number, otel_severity_text = _log_level_to_otel( - record["level"].no, SEVERITY_TO_OTEL_SEVERITY - ) - - attrs: "dict[str, Any]" = {"sentry.origin": "auto.log.loguru"} - - project_root = client.options["project_root"] - if record.get("file"): - if project_root is not None and record["file"].path.startswith(project_root): - attrs["code.file.path"] = record["file"].path[len(project_root) + 1 :] - else: - attrs["code.file.path"] = record["file"].path - - if record.get("line") is not None: - attrs["code.line.number"] = record["line"] - - if record.get("function"): - attrs["code.function.name"] = record["function"] - - if record.get("thread"): - attrs["thread.name"] = record["thread"].name - attrs["thread.id"] = record["thread"].id - - if record.get("process"): - attrs["process.pid"] = record["process"].id - attrs["process.executable.name"] = record["process"].name - - if record.get("name"): - attrs["logger.name"] = record["name"] - - extra = record.get("extra") - if isinstance(extra, dict): - for key, value in extra.items(): - if isinstance(value, (str, int, float, bool)): - attrs[f"sentry.message.parameter.{key}"] = value - else: - attrs[f"sentry.message.parameter.{key}"] = safe_repr(value) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": otel_severity_text, - "severity_number": otel_severity_number, - "body": record["message"], - "attributes": attrs, - "time_unix_nano": int(record["time"].timestamp() * 1e9), - "trace_id": None, - "span_id": None, - } - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py deleted file mode 100644 index 79381fe06e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/mcp.py +++ /dev/null @@ -1,876 +0,0 @@ -""" -Sentry integration for MCP (Model Context Protocol) servers. - -This integration instruments MCP servers to create spans for tool, prompt, -and resource handler execution, and captures errors that occur during execution. - -Supports the low-level `mcp.server.lowlevel.Server` API. -""" - -import inspect -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import package_version, safe_serialize - -MCP_PACKAGE_VERSION = package_version("mcp") - -try: - from mcp.server.lowlevel import Server - from mcp.server.streamable_http import ( - StreamableHTTPServerTransport, - ) - - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - from mcp.server.lowlevel.server import ( # type: ignore[attr-defined] - request_ctx, - ) -except ImportError: - raise DidNotEnable("MCP SDK not installed") - -try: - from fastmcp import FastMCP # type: ignore[import-not-found] -except ImportError: - FastMCP = None - -if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - try: - from mcp.server.context import ( - ServerRequestContext, - ) - except ImportError: - ServerRequestContext = None # type: ignore[assignment,misc] -else: - ServerRequestContext = None # type: ignore[assignment,misc] - - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Optional, Tuple, Union - - from starlette.types import Receive, Scope, Send - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -class MCPIntegration(Integration): - identifier = "mcp" - origin = "auto.ai.mcp" - - def __init__(self, include_prompts: bool = True) -> None: - """ - Initialize the MCP integration. - - Args: - include_prompts: Whether to include prompts (tool results and prompt content) - in span data. Requires send_default_pii=True. Default is True. - """ - self.include_prompts = include_prompts - - @staticmethod - def setup_once() -> None: - """ - Patches MCP server classes to instrument handler execution. - """ - _patch_lowlevel_server() - _patch_handle_request() - - if FastMCP is not None: - _patch_fastmcp() - - -def _get_active_http_scopes( - ctx: "Optional[Any]" = None, -) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return None - - if ( - ctx is None - or not hasattr(ctx, "request") - or ctx.request is None - or "state" not in ctx.request.scope - ): - return None - - return ( - ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), - ctx.request.scope["state"].get("sentry_sdk.current_scope"), - ) - - -def _get_request_context_data( - ctx: "Optional[Any]" = None, -) -> "tuple[Optional[str], Optional[str], str]": - """ - Extract request ID, session ID, and MCP transport type from the request context. - - Returns: - Tuple of (request_id, session_id, mcp_transport). - - request_id: May be None if not available - - session_id: May be None if not available - - mcp_transport: "http", "sse", "stdio" - """ - request_id: "Optional[str]" = None - session_id: "Optional[str]" = None - mcp_transport: str = "stdio" - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): - if ctx is None: - try: - ctx = request_ctx.get() - except LookupError: - return request_id, session_id, mcp_transport - - if ctx is not None: - request_id = ctx.request_id - if hasattr(ctx, "request") and ctx.request is not None: - request = ctx.request - # Detect transport type by checking request characteristics - if hasattr(request, "query_params") and request.query_params.get( - "session_id" - ): - # SSE transport uses query parameter - mcp_transport = "sse" - session_id = request.query_params.get("session_id") - elif hasattr(request, "headers") and request.headers.get("mcp-session-id"): - # StreamableHTTP transport uses header - mcp_transport = "http" - session_id = request.headers.get("mcp-session-id") - - return request_id, session_id, mcp_transport - - -def _get_span_config( - handler_type: str, item_name: str -) -> "tuple[str, str, str, Optional[str]]": - """ - Get span configuration based on handler type. - - Returns: - Tuple of (span_data_key, span_name, mcp_method_name, result_data_key) - Note: result_data_key is None for resources - """ - if handler_type == "tool": - span_data_key = SPANDATA.MCP_TOOL_NAME - mcp_method_name = "tools/call" - result_data_key = SPANDATA.MCP_TOOL_RESULT_CONTENT - elif handler_type == "prompt": - span_data_key = SPANDATA.MCP_PROMPT_NAME - mcp_method_name = "prompts/get" - result_data_key = SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT - else: # resource - span_data_key = SPANDATA.MCP_RESOURCE_URI - mcp_method_name = "resources/read" - result_data_key = None # Resources don't capture result content - - span_name = f"{mcp_method_name} {item_name}" - return span_data_key, span_name, mcp_method_name, result_data_key - - -def _set_span_input_data( - span: "Union[StreamedSpan, Span]", - handler_name: str, - span_data_key: str, - mcp_method_name: str, - arguments: "dict[str, Any]", - request_id: "Optional[str]", - session_id: "Optional[str]", - mcp_transport: str, -) -> None: - """Set input span data for MCP handlers.""" - - # Set handler identifier - _set_span_data_attribute(span, span_data_key, handler_name) - _set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name) - - # Set transport/MCP transport type - _set_span_data_attribute( - span, - SPANDATA.NETWORK_TRANSPORT, - "pipe" if mcp_transport == "stdio" else "tcp", - ) - _set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport) - - # Set request_id if provided - if request_id: - _set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id) - - # Set session_id if provided - if session_id: - _set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id) - - # Set request arguments (excluding common request context objects) - for k, v in arguments.items(): - _set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v)) - - -def _extract_tool_result_content(result: "Any") -> "Any": - """ - Extract meaningful content from MCP tool result. - - Tool handlers can return: - - CallToolResult (mcp v2+): Has .content list and optional .structured_content - - tuple (UnstructuredContent, StructuredContent): Return the structured content (dict) - - dict (StructuredContent): Return as-is - - list/Iterable (UnstructuredContent): Extract text from content blocks - """ - if result is None: - return None - - # Handle v2 CallToolResult-like objects (has .content list attribute) - if hasattr(result, "content") and isinstance( - getattr(result, "content", None), list - ): - # This is only present when a tool declares an output_schema - structured = getattr(result, "structured_content", None) - if structured is not None: - return structured - return _extract_text_from_content_blocks(result.content) - - # Handle CombinationContent: tuple of (UnstructuredContent, StructuredContent) - if isinstance(result, tuple) and len(result) == 2: - # Return the structured content (2nd element) - return result[1] - - # Handle StructuredContent: dict - if isinstance(result, dict): - return result - - # Handle UnstructuredContent: iterable of ContentBlock objects - if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): - return _extract_text_from_content_blocks(result) - - return result - - -def _extract_text_from_content_blocks(content_blocks: "Any") -> "Any": - texts = [] - try: - for item in content_blocks: - if hasattr(item, "text"): - texts.append(item.text) - elif isinstance(item, dict) and "text" in item: - texts.append(item["text"]) - except Exception: - return content_blocks - return " ".join(texts) if texts else content_blocks - - -def _set_span_output_data( - span: "Union[StreamedSpan, Span]", - result: "Any", - result_data_key: "Optional[str]", - handler_type: str, -) -> None: - """Set output span data for MCP handlers.""" - if result is None: - return - - # Get integration to check PII settings - integration = sentry_sdk.get_client().get_integration(MCPIntegration) - if integration is None: - return - - # Check if we should include sensitive data - should_include_data = should_send_default_pii() and integration.include_prompts - - # For tools, extract the meaningful content - if handler_type == "tool": - extracted = _extract_tool_result_content(result) - if ( - extracted is not None - and should_include_data - and result_data_key is not None - ): - _set_span_data_attribute(span, result_data_key, safe_serialize(extracted)) - # Set content count if result is a dict - if isinstance(extracted, dict): - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted) - ) - elif handler_type == "prompt": - # For prompts, count messages and set role/content only for single-message prompts - try: - messages: "Optional[list[str]]" = None - message_count = 0 - - # Check if result has messages attribute (GetPromptResult) - if hasattr(result, "messages") and result.messages: - messages = result.messages - message_count = len(messages) - # Also check if result is a dict with messages - elif isinstance(result, dict) and result.get("messages"): - messages = result["messages"] - message_count = len(messages) - - # Always set message count if we found messages - if message_count > 0: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count - ) - - # Only set role and content for single-message prompts if PII is allowed - if message_count == 1 and should_include_data and messages: - first_message = messages[0] - # Extract role - role = None - if hasattr(first_message, "role"): - role = first_message.role - elif isinstance(first_message, dict) and "role" in first_message: - role = first_message["role"] - - if role: - _set_span_data_attribute( - span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role - ) - - # Extract content text - content_text = None - if hasattr(first_message, "content"): - msg_content = first_message.content - # Content can be a TextContent object or similar - if hasattr(msg_content, "text"): - content_text = msg_content.text - elif isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - elif isinstance(first_message, dict) and "content" in first_message: - msg_content = first_message["content"] - if isinstance(msg_content, dict) and "text" in msg_content: - content_text = msg_content["text"] - elif isinstance(msg_content, str): - content_text = msg_content - - if content_text and result_data_key is not None: - _set_span_data_attribute(span, result_data_key, content_text) - except Exception: - # Silently ignore if we can't extract message info - pass - # Resources don't capture result content (result_data_key is None) - - -# Handler data preparation and wrapping - - -def _is_v2_context(original_args: "tuple[Any, ...]") -> bool: - """Check if original_args contains a v2 ServerRequestContext as the first element.""" - return ( - ServerRequestContext is not None - and bool(original_args) - and isinstance(original_args[0], ServerRequestContext) - ) - - -def _extract_handler_data_from_params( - handler_type: str, - params: "Any", -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from a v2 typed params object. - - In MCP SDK v2, handlers receive (ctx, params) where params is a typed - Pydantic model (CallToolRequestParams, GetPromptRequestParams, etc.). - """ - if handler_type == "tool": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - elif handler_type == "prompt": - handler_name = getattr(params, "name", "unknown") - arguments = getattr(params, "arguments", None) or {} - arguments = {"name": handler_name, **arguments} - else: # resource - handler_name = str(getattr(params, "uri", "unknown")) - arguments = {} - - return handler_name, arguments - - -def _extract_handler_data_from_args( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, -) -> "tuple[str, dict[str, Any]]": - """ - Extract handler name and arguments from v1 positional args. - - In MCP SDK v1, handlers receive positional args: - - Tool: (tool_name, arguments) - - Prompt: (name, arguments) - - Resource: (uri,) - """ - original_kwargs = original_kwargs or {} - - if handler_type == "tool": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - elif handler_type == "prompt": - if original_args: - handler_name = original_args[0] - elif original_kwargs.get("name"): - handler_name = original_kwargs["name"] - - arguments = {} - if len(original_args) > 1: - arguments = original_args[1] - elif original_kwargs.get("arguments"): - arguments = original_kwargs["arguments"] - - arguments = {"name": handler_name, **(arguments or {})} - - else: # resource - handler_name = "unknown" - if original_args: - handler_name = str(original_args[0]) - elif original_kwargs.get("uri"): - handler_name = str(original_kwargs["uri"]) - - arguments = {} - - return handler_name, arguments - - -def _prepare_handler_data( - handler_type: str, - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - params: "Optional[Any]" = None, -) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]": - """ - Prepare common handler data for both v1 and v2 MCP SDK. - - Args: - handler_type: "tool", "prompt", or "resource" - original_args: Original positional args (v1 path) - original_kwargs: Original keyword args (v1 path) - params: Typed params object from v2 ServerRequestContext path - - Returns: - Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key) - """ - if params is not None: - handler_name, arguments = _extract_handler_data_from_params( - handler_type, params - ) - elif _is_v2_context(original_args): - handler_name = "unknown" - arguments = {} - else: - handler_name, arguments = _extract_handler_data_from_args( - handler_type, original_args, original_kwargs - ) - - span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config( - handler_type, handler_name - ) - - return ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) - - -async def _handler_wrapper( - handler_type: str, - func: "Callable[..., Any]", - original_args: "tuple[Any, ...]", - original_kwargs: "Optional[dict[str, Any]]" = None, - self: "Optional[Any]" = None, - force_await: bool = True, -) -> "Any": - """ - Wrapper for MCP handlers. - - Args: - handler_type: "tool", "prompt", or "resource" - func: The handler function to wrap - original_args: Original arguments passed to the handler - original_kwargs: Original keyword arguments passed to the handler - self: Optional instance for bound methods - """ - if original_kwargs is None: - original_kwargs = {} - - # Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers - ctx: "Optional[Any]" = None - params: "Optional[Any]" = None - if ( - ServerRequestContext is not None - and original_args - and isinstance(original_args[0], ServerRequestContext) - ): - ctx = original_args[0] - params = original_args[1] if len(original_args) > 1 else None - - ( - handler_name, - arguments, - span_data_key, - span_name, - mcp_method_name, - result_data_key, - ) = _prepare_handler_data( - handler_type, original_args, original_kwargs, params=params - ) - - scopes = _get_active_http_scopes(ctx=ctx) - - isolation_scope_context: "ContextManager[Any]" - current_scope_context: "ContextManager[Any]" - - if scopes is None: - isolation_scope_context = nullcontext() - current_scope_context = nullcontext() - else: - isolation_scope, current_scope = scopes - - isolation_scope_context = ( - nullcontext() - if isolation_scope is None - else sentry_sdk.scope.use_isolation_scope(isolation_scope) - ) - current_scope_context = ( - nullcontext() - if current_scope is None - else sentry_sdk.scope.use_scope(current_scope) - ) - - # Get request ID, session ID, and transport from context - request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - - # Start span and execute - with isolation_scope_context: - with current_scope_context: - span_mgr: "Union[Span, StreamedSpan]" - if span_streaming: - span_mgr = sentry_sdk.traces.start_span( - name=span_name, - attributes={ - "sentry.op": OP.MCP_SERVER, - "sentry.origin": MCPIntegration.origin, - }, - ) - else: - span_mgr = get_start_span_function()( - op=OP.MCP_SERVER, - name=span_name, - origin=MCPIntegration.origin, - ) - - with span_mgr as span: - # Set input span data - _set_span_input_data( - span, - handler_name, - span_data_key, - mcp_method_name, - arguments, - request_id, - session_id, - mcp_transport, - ) - - # For resources, extract and set protocol - if handler_type == "resource": - uri = None - if params is not None: - uri = getattr(params, "uri", None) - - # v1 scenario - if ServerRequestContext is None: - if original_args: - uri = original_args[0] - else: - uri = original_kwargs.get("uri") - - protocol = None - if uri is not None and hasattr(uri, "scheme"): - protocol = uri.scheme - elif handler_name and "://" in handler_name: - protocol = handler_name.split("://")[0] - if protocol: - _set_span_data_attribute( - span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol - ) - - try: - # Execute the async handler - if self is not None: - original_args = (self, *original_args) - - result = func(*original_args, **original_kwargs) - if force_await or inspect.isawaitable(result): - result = await result - - except Exception as e: - # Set error flag for tools - if handler_type == "tool": - _set_span_data_attribute( - span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True - ) - sentry_sdk.capture_exception(e) - raise - - _set_span_output_data(span, result, result_data_key, handler_type) - - return result - - -def _create_instrumented_decorator( - original_decorator: "Callable[..., Any]", - handler_type: str, - *decorator_args: "Any", - **decorator_kwargs: "Any", -) -> "Callable[..., Any]": - """ - Create an instrumented version of an MCP decorator. - - This function intercepts MCP decorators (like @server.call_tool()) and injects - Sentry instrumentation into the handler registration flow. The returned decorator - will: - 1. Receive the user's handler function - 2. Pass the instrumented version to the original MCP decorator - - This ensures that when the handler is called at runtime, it's already wrapped - with Sentry spans and metrics collection. - - Args: - original_decorator: The original MCP decorator method (e.g., Server.call_tool) - handler_type: "tool", "prompt", or "resource" - determines span configuration - decorator_args: Positional arguments to pass to the original decorator (e.g., self) - decorator_kwargs: Keyword arguments to pass to the original decorator - - Returns: - A decorator function that instruments handlers before registering them - """ - - def instrumented_decorator(func: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(func) - async def wrapper(*args: "Any") -> "Any": - return await _handler_wrapper(handler_type, func, args, force_await=False) - - # Then register it with the original MCP decorator - return original_decorator(*decorator_args, **decorator_kwargs)(wrapper) - - return instrumented_decorator - - -_METHOD_TO_HANDLER_TYPE = { - "tools/call": "tool", - "prompts/get": "prompt", - "resources/read": "resource", -} - -# In MCP SDK v2, tool/prompt/resource handlers are most commonly registered via -# the Server(...) constructor kwargs rather than add_request_handler. The in-tree -# high-level MCPServer also wires its handlers through these kwargs. -_KWARG_TO_HANDLER_TYPE = { - "on_call_tool": "tool", - "on_get_prompt": "prompt", - "on_read_resource": "resource", -} - - -def _patch_lowlevel_server() -> None: - """ - Patches the mcp.server.lowlevel.Server class to instrument handler execution. - """ - if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION >= (2, 0, 0): - _patch_lowlevel_server_v2() - else: - _patch_lowlevel_server_v1() - - -def _patch_lowlevel_server_v1() -> None: - """Patches v1 Server decorator methods (call_tool, get_prompt, read_resource).""" - # Patch call_tool decorator - original_call_tool = Server.call_tool # type: ignore[attr-defined] - - def patched_call_tool( - self: "Server", **kwargs: "Any" - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.call_tool that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_call_tool, "tool", self, **kwargs - )(func) - - Server.call_tool = patched_call_tool # type: ignore[attr-defined] - - # Patch get_prompt decorator - original_get_prompt = Server.get_prompt # type: ignore[attr-defined] - - def patched_get_prompt( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.get_prompt that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_get_prompt, "prompt", self - )(func) - - Server.get_prompt = patched_get_prompt # type: ignore[attr-defined] - - # Patch read_resource decorator - original_read_resource = Server.read_resource # type: ignore[attr-defined] - - def patched_read_resource( - self: "Server", - ) -> "Callable[[Callable[..., Any]], Callable[..., Any]]": - """Patched version of Server.read_resource that adds Sentry instrumentation.""" - return lambda func: _create_instrumented_decorator( - original_read_resource, "resource", self - )(func) - - Server.read_resource = patched_read_resource # type: ignore[attr-defined] - - -def _wrap_v2_handler( - handler_type: str, handler: "Callable[..., Any]" -) -> "Callable[..., Any]": - """Wrap a v2 (ctx, params) handler with Sentry instrumentation. - - Idempotent: an already-wrapped handler is returned unchanged so handlers - registered through more than one path (e.g. MCPServer building a Server) - are not double-wrapped. - """ - if getattr(handler, "__sentry_mcp_wrapped__", False): - return handler - - @wraps(handler) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - return await _handler_wrapper( - handler_type, handler, args, kwargs, force_await=False - ) - - wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined] - return wrapper - - -def _patch_lowlevel_server_v2() -> None: - """Patches the v2 Server to wrap tool/prompt/resource handlers. - - Handlers can be registered either via the Server(...) constructor kwargs - (on_call_tool/on_get_prompt/on_read_resource) — the path the in-tree - MCPServer and most lowlevel examples use — or via add_request_handler. - Both are patched. - """ - original_init = Server.__init__ - - @wraps(original_init) - def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None: - for kwarg, handler_type in _KWARG_TO_HANDLER_TYPE.items(): - handler = kwargs.get(kwarg) - if handler is not None: - kwargs[kwarg] = _wrap_v2_handler(handler_type, handler) - original_init(self, *args, **kwargs) - - Server.__init__ = patched_init # type: ignore[method-assign] - - original_add_request_handler = Server.add_request_handler - - def patched_add_request_handler( - self: "Server", - method: str, - params_type: "Any", - handler: "Callable[..., Any]", - *args: "Any", - **kwargs: "Any", - ) -> None: - handler_type = _METHOD_TO_HANDLER_TYPE.get(method) - if handler_type is not None: - handler = _wrap_v2_handler(handler_type, handler) - - original_add_request_handler( - self, method, params_type, handler, *args, **kwargs - ) - - Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign] - - -def _patch_handle_request() -> None: - original_handle_request = StreamableHTTPServerTransport.handle_request - - @wraps(original_handle_request) - async def patched_handle_request( - self: "StreamableHTTPServerTransport", - scope: "Scope", - receive: "Receive", - send: "Send", - ) -> None: - scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = ( - sentry_sdk.get_isolation_scope() - ) - scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope() - await original_handle_request(self, scope, receive, send) - - StreamableHTTPServerTransport.handle_request = patched_handle_request # type: ignore[method-assign] - - -def _patch_fastmcp() -> None: - """ - Patches the standalone fastmcp package's FastMCP class. - - The standalone fastmcp package (v2.14.0+) registers its own handlers for - prompts and resources directly, bypassing the Server decorators we patch. - This function patches the _get_prompt_mcp and _read_resource_mcp methods - to add instrumentation for those handlers. - """ - if FastMCP is not None and hasattr(FastMCP, "_get_prompt_mcp"): - original_get_prompt_mcp = FastMCP._get_prompt_mcp - - @wraps(original_get_prompt_mcp) - async def patched_get_prompt_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "prompt", - original_get_prompt_mcp, - args, - kwargs, - self, - ) - - FastMCP._get_prompt_mcp = patched_get_prompt_mcp - - if FastMCP is not None and hasattr(FastMCP, "_read_resource_mcp"): - original_read_resource_mcp = FastMCP._read_resource_mcp - - @wraps(original_read_resource_mcp) - async def patched_read_resource_mcp( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - return await _handler_wrapper( - "resource", - original_read_resource_mcp, - args, - kwargs, - self, - ) - - FastMCP._read_resource_mcp = patched_read_resource_mcp diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py deleted file mode 100644 index b6111492bb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/modules.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import _get_installed_modules - -if TYPE_CHECKING: - from typing import Any - - from sentry_sdk._types import Event - - -class ModulesIntegration(Integration): - identifier = "modules" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def processor(event: "Event", hint: "Any") -> "Event": - if event.get("type") == "transaction": - return event - - if sentry_sdk.get_client().get_integration(ModulesIntegration) is None: - return event - - event["modules"] = _get_installed_modules() - return event diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py deleted file mode 100644 index 186c665ed1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai.py +++ /dev/null @@ -1,1530 +0,0 @@ -import json -import sys -import time -from collections.abc import Iterable -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import consts -from sentry_sdk.ai._openai_completions_api import ( - _get_system_instructions as _get_system_instructions_completions, -) -from sentry_sdk.ai._openai_completions_api import ( - _get_text_items, - _transform_system_instructions, -) -from sentry_sdk.ai._openai_completions_api import ( - _is_system_instruction as _is_system_instruction_completions, -) -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions as _get_system_instructions_responses, -) -from sentry_sdk.ai._openai_responses_api import ( - _is_system_instruction as _is_system_instruction_responses, -) -from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_embedding_inputs, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - reraise, - safe_serialize, -) - -if TYPE_CHECKING: - from typing import ( - Any, - AsyncIterator, - Callable, - Iterable, - Iterator, - List, - Optional, - Union, - ) - - from openai import Omit - from openai.types import CompletionUsage - from openai.types.responses import ( - ResponseInputParam, - ResponseStreamEvent, - SequenceNotStr, - ) - from openai.types.responses.response_usage import ResponseUsage - - from sentry_sdk._types import TextPart - from sentry_sdk.tracing import Span - -try: - try: - from openai import NotGiven - except ImportError: - NotGiven = None - - try: - from openai import Omit - except ImportError: - Omit = None - - from openai import AsyncStream, Stream - from openai.resources import AsyncEmbeddings, Embeddings - from openai.resources.chat.completions import AsyncCompletions, Completions - - if TYPE_CHECKING: - from openai.types.chat import ( - ChatCompletionChunk, - ChatCompletionMessageParam, - ) -except ImportError: - raise DidNotEnable("OpenAI not installed") - -RESPONSES_API_ENABLED = True -try: - # responses API support was introduced in v1.66.0 - from openai.resources.responses import AsyncResponses, Responses - from openai.types.responses.response_completed_event import ResponseCompletedEvent -except ImportError: - RESPONSES_API_ENABLED = False - - -class OpenAIIntegration(Integration): - identifier = "openai" - origin = f"auto.ai.{identifier}" - - def __init__( - self: "OpenAIIntegration", - include_prompts: bool = True, - tiktoken_encoding_name: "Optional[str]" = None, - ) -> None: - self.include_prompts = include_prompts - - self.tiktoken_encoding = None - if tiktoken_encoding_name is not None: - import tiktoken # type: ignore - - self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) - - @staticmethod - def setup_once() -> None: - Completions.create = _wrap_chat_completion_create(Completions.create) - AsyncCompletions.create = _wrap_async_chat_completion_create( - AsyncCompletions.create - ) - - Embeddings.create = _wrap_embeddings_create(Embeddings.create) - AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create) - - if RESPONSES_API_ENABLED: - Responses.create = _wrap_responses_create(Responses.create) - AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create) - - def count_tokens(self: "OpenAIIntegration", s: str) -> int: - if self.tiktoken_encoding is None: - return 0 - try: - return len(self.tiktoken_encoding.encode_ordinary(s)) - except Exception: - return 0 - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _has_attr_and_is_int( - token_usage: "Union[CompletionUsage, ResponseUsage]", attr_name: str -) -> bool: - return hasattr(token_usage, attr_name) and isinstance( - getattr(token_usage, attr_name, None), int - ) - - -def _calculate_completions_token_usage( - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - streaming_message_total_token_usage: "Optional[CompletionUsage]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Chat Completions API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - usage = None - - if streaming_message_total_token_usage is not None: - usage = streaming_message_total_token_usage - elif hasattr(response, "usage"): - usage = response.usage - - if usage is not None: - if _has_attr_and_is_int(usage, "prompt_tokens"): - input_tokens = usage.prompt_tokens - if _has_attr_and_is_int(usage, "completion_tokens"): - output_tokens = usage.completion_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "prompt_tokens_details"): - cached = getattr(usage.prompt_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "completion_tokens_details"): - reasoning = getattr( - usage.completion_tokens_details, "reasoning_tokens", None - ) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in messages or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "choices") and response.choices is not None: - for choice in response.choices: - if hasattr(choice, "message") and hasattr(choice.message, "content"): - output_tokens += count_tokens(choice.message.content) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _calculate_responses_token_usage( - input: "Any", - response: "Any", - span: "Union[Span, StreamedSpan]", - streaming_message_responses: "Optional[List[str]]", - count_tokens: "Callable[..., Any]", -) -> None: - """Extract and record token usage from a Responses API response.""" - input_tokens: "Optional[int]" = 0 - input_tokens_cached: "Optional[int]" = 0 - output_tokens: "Optional[int]" = 0 - output_tokens_reasoning: "Optional[int]" = 0 - total_tokens: "Optional[int]" = 0 - - if hasattr(response, "usage"): - usage = response.usage - - if _has_attr_and_is_int(usage, "input_tokens"): - input_tokens = usage.input_tokens - if _has_attr_and_is_int(usage, "output_tokens"): - output_tokens = usage.output_tokens - if _has_attr_and_is_int(usage, "total_tokens"): - total_tokens = usage.total_tokens - - if hasattr(usage, "input_tokens_details"): - cached = getattr(usage.input_tokens_details, "cached_tokens", None) - if isinstance(cached, int): - input_tokens_cached = cached - - if hasattr(usage, "output_tokens_details"): - reasoning = getattr(usage.output_tokens_details, "reasoning_tokens", None) - if isinstance(reasoning, int): - output_tokens_reasoning = reasoning - - # Manually count input tokens - if input_tokens == 0: - for message in input or []: - if isinstance(message, str): - input_tokens += count_tokens(message) - continue - elif isinstance(message, dict): - message_content = message.get("content") - if message_content is None: - continue - # Deliberate use of Completions function for both Completions and Responses input format. - text_items = _get_text_items(message_content) - input_tokens += sum(count_tokens(text) for text in text_items) - continue - - # Manually count output tokens - if output_tokens == 0: - if streaming_message_responses is not None: - for message in streaming_message_responses: - output_tokens += count_tokens(message) - elif hasattr(response, "output"): - for output_item in response.output: - if hasattr(output_item, "content"): - for content_item in output_item.content: - if hasattr(content_item, "text"): - output_tokens += count_tokens(content_item.text) - - # Do not set token data if it is 0 - input_tokens = input_tokens or None - input_tokens_cached = input_tokens_cached or None - output_tokens = output_tokens or None - output_tokens_reasoning = output_tokens_reasoning or None - total_tokens = total_tokens or None - - record_token_usage( - span, - input_tokens=input_tokens, - input_tokens_cached=input_tokens_cached, - output_tokens=output_tokens, - output_tokens_reasoning=output_tokens_reasoning, - total_tokens=total_tokens, - ) - - -def _set_responses_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions") - messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input") - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_output_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - conversation = kwargs.get("conversation") - if conversation is not None and _is_given(conversation): - conversation_id: "Optional[str]" = None - if isinstance(conversation, str): - conversation_id = conversation - elif isinstance(conversation, dict): - conversation_id = conversation.get("id") - if conversation_id is not None: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - if not should_send_default_pii() or not integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if ( - messages is None - and explicit_instructions is not None - and _is_given(explicit_instructions) - ): - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - [ - { - "type": "text", - "content": explicit_instructions, - } - ] - ), - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - if messages is None: - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - instructions_text_parts: "list[TextPart]" = [] - if explicit_instructions is not None and _is_given(explicit_instructions): - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions_responses(messages) - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - return - - non_system_messages = [ - message for message in messages if not _is_system_instruction_responses(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") - - -def _set_completions_api_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get( - "messages" - ) - - tools = kwargs.get("tools") - if tools is not None and _is_given(tools) and len(tools) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - max_tokens = kwargs.get("max_tokens") - if max_tokens is not None and _is_given(max_tokens): - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) - - presence_penalty = kwargs.get("presence_penalty") - if presence_penalty is not None and _is_given(presence_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty) - - frequency_penalty = kwargs.get("frequency_penalty") - if frequency_penalty is not None and _is_given(frequency_penalty): - set_on_span(SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty) - - temperature = kwargs.get("temperature") - if temperature is not None and _is_given(temperature): - set_on_span(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) - - top_p = kwargs.get("top_p") - if top_p is not None and _is_given(top_p): - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - if isinstance(messages, str): - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - return - - messages = list(messages) - kwargs["messages"] = messages - - system_instructions = _get_system_instructions_completions(messages) - if len(system_instructions) > 0: - set_on_span( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), - ) - - non_system_messages = [ - message - for message in messages - if not _is_system_instruction_completions(message) - ] - if len(non_system_messages) > 0: - normalized_messages = normalize_message_roles(non_system_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - -def _set_embeddings_input_data( - span: "Union[Span, StreamedSpan]", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", -) -> None: - messages: "Union[str, SequenceNotStr[str], Iterable[int], Iterable[Iterable[int]]]" = kwargs.get( - "input" - ) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - model = kwargs.get("model") - if model is not None: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - if ( - not should_send_default_pii() - or not integration.include_prompts - or messages is None - ): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - return - - if isinstance(messages, str): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - normalized_messages = normalize_message_roles([messages]) # type: ignore - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - return - - # dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197 - if not isinstance(messages, Iterable) or isinstance(messages, dict): - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - return - - messages = list(messages) - kwargs["input"] = messages - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_embedding_inputs(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False - ) - - set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") - - -def _set_common_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - input: "Any", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - if hasattr(response, "model"): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model) - - # Chat Completions API - if hasattr(response, "choices") and response.choices is not None: - if should_send_default_pii() and integration.include_prompts: - response_text = [ - choice.message.model_dump() - for choice in response.choices - if choice.message is not None - ] - if len(response_text) > 0: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_text) - - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - # Responses API - elif hasattr(response, "output"): - if should_send_default_pii() and integration.include_prompts: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in response.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], - unpack=False, - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) - - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - # Embeddings API (fallback for responses with neither choices nor output) - else: - _calculate_completions_token_usage( - messages=input, - response=response, - span=span, - streaming_message_responses=None, - streaming_message_total_token_usage=None, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _new_sync_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_synchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_chat_completion(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - if "messages" not in kwargs: - # invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - try: - iter(kwargs["messages"]) - except TypeError: - # invalid call (in all versions), messages must be iterable - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/completions.py#L585 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"chat {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_CHAT, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_CHAT, - name=f"chat {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_completions_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - response._iterator = _wrap_asynchronous_completions_chunk_iterator( - span=span, - integration=integration, - start_time=start_time, - messages=messages, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_completions_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _set_completions_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - messages = kwargs.get("messages") - - if messages is not None and isinstance(messages, str): - messages = [messages] - - _set_common_output_data( - span, - response, - messages, - integration, - finish_span, - ) - - -def _wrap_synchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "Stream[ChatCompletionChunk]", - old_iterator: "Iterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "Iterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_completions_chunk_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - messages: "Optional[Iterable[ChatCompletionMessageParam]]", - response: "AsyncStream[ChatCompletionChunk]", - old_iterator: "AsyncIterator[ChatCompletionChunk]", - finish_span: "bool", -) -> "AsyncIterator[ChatCompletionChunk]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - streaming_message_total_token_usage = None - - async for x in old_iterator: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) - - with capture_internal_exceptions(): - if hasattr(x, "choices") and x.choices is not None: - choice_index = 0 - for choice in x.choices: - if hasattr(choice, "delta") and hasattr(choice.delta, "content"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - content = choice.delta.content - if len(data_buf) <= choice_index: - data_buf.append([]) - data_buf[choice_index].append(content or "") - choice_index += 1 - if hasattr(x, "usage"): - streaming_message_total_token_usage = x.usage - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - all_responses = None - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - _calculate_completions_token_usage( - messages=messages, - response=response, - span=span, - streaming_message_responses=all_responses, - streaming_message_total_token_usage=streaming_message_total_token_usage, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -def _wrap_synchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "Stream[ResponseStreamEvent]", - old_iterator: "Iterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "Iterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - - if finish_span: - span.__exit__(None, None, None) - - -async def _wrap_asynchronous_responses_event_iterator( - span: "Union[Span, StreamedSpan]", - integration: "OpenAIIntegration", - start_time: "Optional[float]", - input: "Optional[Union[str, ResponseInputParam]]", - response: "AsyncStream[ResponseStreamEvent]", - old_iterator: "AsyncIterator[ResponseStreamEvent]", - finish_span: "bool", -) -> "AsyncIterator[ResponseStreamEvent]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Compute token count based on inputs and outputs using tiktoken if token counts are not in the model response. - Responsible for closing the AI Client Span if instructed to by the `finish_span` argument. - """ - ttft: "Optional[float]" = None - data_buf: "list[list[str]]" = [] # one for each choice - - count_tokens_manually = True - async for x in old_iterator: - with capture_internal_exceptions(): - if hasattr(x, "delta"): - if start_time is not None and ttft is None: - ttft = time.perf_counter() - start_time - if len(data_buf) == 0: - data_buf.append([]) - data_buf[0].append(x.delta or "") - - if isinstance(x, ResponseCompletedEvent): - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, x.response.model) - - _calculate_responses_token_usage( - input=input, - response=x.response, - span=span, - streaming_message_responses=None, - count_tokens=integration.count_tokens, - ) - count_tokens_manually = False - - yield x - - with capture_internal_exceptions(): - if ttft is not None: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft - ) - if len(data_buf) > 0: - all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) - if count_tokens_manually: - _calculate_responses_token_usage( - input=input, - response=response, - span=span, - streaming_message_responses=all_responses, - count_tokens=integration.count_tokens, - ) - if finish_span: - span.__exit__(None, None, None) - - -def _set_responses_api_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _set_embeddings_output_data( - span: "Union[Span, StreamedSpan]", - response: "Any", - kwargs: "dict[str, Any]", - integration: "OpenAIIntegration", - finish_span: bool = True, -) -> None: - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - _set_common_output_data( - span, - response, - input, - integration, - finish_span, - ) - - -def _wrap_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - return _new_sync_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_chat_completion_create(f: "Callable[..., Any]") -> "Callable[..., Any]": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) - - return await _new_async_chat_completion(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_embeddings_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -async def _new_async_embeddings_create( - f: "Any", *args: "Any", **kwargs: "Any" -) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=f"embeddings {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_EMBEDDINGS, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - }, - ) as span: - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - else: - with get_start_span_function()( - op=consts.OP.GEN_AI_EMBEDDINGS, - name=f"embeddings {model}", - origin=OpenAIIntegration.origin, - ) as span: - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - _set_embeddings_input_data(span, kwargs, integration) - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - - _set_embeddings_output_data( - span, response, kwargs, integration, finish_span=False - ) - - return response - - -def _wrap_embeddings_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_embeddings_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_embeddings_create(f, *args, **kwargs) - - return _sentry_patched_create_async - - -def _new_sync_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, Stream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_synchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -async def _new_async_responses_create(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - model = kwargs.get("model") - - # Same bool handling as in https://github.com/openai/openai-python/blob/acd0c54d8a68efeedde0e5b4e6c310eef1ce7867/src/openai/resources/responses/responses.py#L940 - is_streaming_response = kwargs.get("stream", False) or False - - if has_span_streaming_enabled(client.options): - span = sentry_sdk.traces.start_span( - name=f"responses {model}", - attributes={ - "sentry.op": consts.OP.GEN_AI_RESPONSES, - "sentry.origin": OpenAIIntegration.origin, - SPANDATA.GEN_AI_SYSTEM: "openai", - SPANDATA.GEN_AI_RESPONSE_STREAMING: is_streaming_response, - }, - ) - else: - span = get_start_span_function()( - op=consts.OP.GEN_AI_RESPONSES, - name=f"responses {model}", - origin=OpenAIIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, is_streaming_response) - - _set_responses_api_input_data(span, kwargs, integration) - - start_time = time.perf_counter() - - try: - response = await f(*args, **kwargs) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - span.__exit__(*exc_info) - reraise(*exc_info) - - # Attribute check to fail gracefully if the attribute is not present in future `openai` versions. - if isinstance(response, AsyncStream) and hasattr(response, "_iterator"): - input = kwargs.get("input") - - if input is not None and isinstance(input, str): - input = [input] - - response._iterator = _wrap_asynchronous_responses_event_iterator( - span=span, - integration=integration, - start_time=start_time, - input=input, - response=response, - old_iterator=response._iterator, - finish_span=True, - ) - else: - _set_responses_api_output_data( - span, response, kwargs, integration, finish_span=True - ) - - return response - - -def _wrap_responses_create(f: "Any") -> "Any": - @wraps(f) - def _sentry_patched_create_sync(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) - - return _new_sync_responses_create(f, *args, **kwargs) - - return _sentry_patched_create_sync - - -def _wrap_async_responses_create(f: "Any") -> "Any": - @wraps(f) - async def _sentry_patched_responses_async(*args: "Any", **kwargs: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _new_async_responses_create(f, *args, **kwargs) - - return _sentry_patched_responses_async - - -def _is_given(obj: "Any") -> bool: - """ - Check for givenness safely across different openai versions. - """ - if NotGiven is not None and isinstance(obj, NotGiven): - return False - if Omit is not None and isinstance(obj, Omit): - return False - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py deleted file mode 100644 index 5895f53ad3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/__init__.py +++ /dev/null @@ -1,250 +0,0 @@ -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import parse_version - -from .patches import ( - _create_run_streamed_wrapper, - _create_run_wrapper, - _execute_final_output, - _execute_handoffs, - _get_all_tools, - _get_model, - _patch_error_tracing, - _run_single_turn, - _run_single_turn_streamed, -) - -try: - # "agents" is too generic. If someone has an agents.py file in their project - # or another package that's importable via "agents", no ImportError would - # be thrown and the integration would enable itself even if openai-agents is - # not installed. That's why we're adding the second, more specific import - # after it, even if we don't use it. - import agents - from agents.run import AgentRunner - from agents.version import __version__ as OPENAI_AGENTS_VERSION - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -try: - # AgentRunner methods moved in v0.8 - # https://github.com/openai/openai-agents-python/commit/3ce7c24d349b77bb750062b7e0e856d9ff48a5d5#diff-7470b3a5c5cbe2fcbb2703dc24f326f45a5819d853be2b1f395d122d278cd911 - from agents.run_internal import run_loop, turn_preparation, turn_resolution -except ImportError: - run_loop = None - turn_preparation = None - turn_resolution = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - from agents.run_internal.run_steps import SingleStepResult - - -def _patch_runner() -> None: - # Create the root span for one full agent run (including eventual handoffs) - # Note agents.run.DEFAULT_AGENT_RUNNER.run_sync is a wrapper around - # agents.run.DEFAULT_AGENT_RUNNER.run. It does not need to be wrapped separately. - agents.run.DEFAULT_AGENT_RUNNER.run = _create_run_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run - ) - - # Patch streaming runner - agents.run.DEFAULT_AGENT_RUNNER.run_streamed = _create_run_streamed_wrapper( - agents.run.DEFAULT_AGENT_RUNNER.run_streamed - ) - - -class OpenAIAgentsIntegration(Integration): - """ - NOTE: With version 0.8.0, the class methods below have been refactored to functions. - - `AgentRunner._get_model()` -> `agents.run_internal.turn_preparation.get_model()` - - `AgentRunner._get_all_tools()` -> `agents.run_internal.turn_preparation.get_all_tools()` - - `AgentRunner._run_single_turn()` -> `agents.run_internal.run_loop.run_single_turn()` - - `RunImpl.execute_handoffs()` -> `agents.run_internal.turn_resolution.execute_handoffs()` - - `RunImpl.execute_final_output()` -> `agents.run_internal.turn_resolution.execute_final_output()` - - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every Responses API call. - 2. The user passes the agent instance to a Runner with `run()` and `run_streamed()` methods. The latter can be used to incrementally receive progress. - - `Runner.run()` and `Runner.run_streamed()` are thin wrappers for `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()`. - - `DEFAULT_AGENT_RUNNER.run()` and `DEFAULT_AGENT_RUNNER.run_streamed()` are patched in `_patch_runner()` with `_create_run_wrapper()` and `_create_run_streamed_wrapper()`, respectively. - 3. In a loop, the agent repeatedly calls the Responses API, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - A Model instance is created at the start of the loop by calling the `Runner._get_model()`. We patch the Model instance using `patches._get_model()`. - - Available tools are also deteremined at the start of the loop, with `Runner._get_all_tools()`. We patch Tool instances by iterating through the returned tools in `patches._get_all_tools()`. - - In each loop iteration, `run_single_turn()` or `run_single_turn_streamed()` is responsible for calling the Responses API, patched with `patches._run_single_turn()` and `patches._run_single_turn_streamed()`. - 4. On loop termination, `RunImpl.execute_final_output()` is called. The function is patched with `patches._execute_final_output()`. - - Local tools are run based on the return value from the Responses API as a post-API call step in the above loop. - Hosted MCP Tools are run as part of the Responses API call, and involve OpenAI reaching out to an external MCP server. - An agent can handoff to another agent, also directed by the return value of the Responses API and run post-API call in the loop. - Handoffs are a way to switch agent-wide configuration. - - Handoffs are executed by calling `RunImpl.execute_handoffs()`. The method is patched with `patches._execute_handoffs()` - """ - - identifier = "openai_agents" - - @staticmethod - def setup_once() -> None: - _patch_error_tracing() - _patch_runner() - - library_version = parse_version(OPENAI_AGENTS_VERSION) - if library_version is not None and library_version >= ( - 0, - 8, - ): - if run_loop is not None: - - @wraps(run_loop.get_all_tools) - async def new_wrapped_get_all_tools( - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools( - run_loop.get_all_tools, agent, context_wrapper - ) - - agents.run.get_all_tools = new_wrapped_get_all_tools - - @wraps(run_loop.run_single_turn) - async def new_wrapped_run_single_turn( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn( - run_loop.run_single_turn, *args, **kwargs - ) - - agents.run.run_single_turn = new_wrapped_run_single_turn - - @wraps(run_loop.run_single_turn_streamed) - async def new_wrapped_run_single_turn_streamed( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - run_loop.run_single_turn_streamed, *args, **kwargs - ) - - agents.run.run_single_turn_streamed = ( - new_wrapped_run_single_turn_streamed - ) - - if turn_preparation is not None: - - @wraps(turn_preparation.get_model) - def new_wrapped_get_model( - agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(turn_preparation.get_model, agent, run_config) - - agents.run_internal.run_loop.get_model = new_wrapped_get_model - - if turn_resolution is not None: - original_execute_handoffs = turn_resolution.execute_handoffs - - @wraps(original_execute_handoffs) - async def new_wrapped_execute_handoffs( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs( - original_execute_handoffs, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_handoffs = ( - new_wrapped_execute_handoffs - ) - - original_execute_final_output = turn_resolution.execute_final_output - - @wraps(turn_resolution.execute_final_output) - async def new_wrapped_final_output( - *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents.run_internal.turn_resolution.execute_final_output = ( - new_wrapped_final_output - ) - - return - - original_get_all_tools = AgentRunner._get_all_tools - - @wraps(AgentRunner._get_all_tools.__func__) - async def old_wrapped_get_all_tools( - cls: "agents.Runner", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", - ) -> "list[agents.Tool]": - return await _get_all_tools(original_get_all_tools, agent, context_wrapper) - - agents.run.AgentRunner._get_all_tools = classmethod(old_wrapped_get_all_tools) - - original_get_model = AgentRunner._get_model - - @wraps(AgentRunner._get_model.__func__) - def old_wrapped_get_model( - cls: "agents.Runner", agent: "agents.Agent", run_config: "agents.RunConfig" - ) -> "agents.Model": - return _get_model(original_get_model, agent, run_config) - - agents.run.AgentRunner._get_model = classmethod(old_wrapped_get_model) - - original_run_single_turn = AgentRunner._run_single_turn - - @wraps(AgentRunner._run_single_turn.__func__) - async def old_wrapped_run_single_turn( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn(original_run_single_turn, *args, **kwargs) - - agents.run.AgentRunner._run_single_turn = classmethod( - old_wrapped_run_single_turn - ) - - original_run_single_turn_streamed = AgentRunner._run_single_turn_streamed - - @wraps(AgentRunner._run_single_turn_streamed.__func__) - async def old_wrapped_run_single_turn_streamed( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _run_single_turn_streamed( - original_run_single_turn_streamed, *args, **kwargs - ) - - agents.run.AgentRunner._run_single_turn_streamed = classmethod( - old_wrapped_run_single_turn_streamed - ) - - original_execute_handoffs = agents._run_impl.RunImpl.execute_handoffs - - @wraps(agents._run_impl.RunImpl.execute_handoffs.__func__) - async def old_wrapped_execute_handoffs( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_handoffs(original_execute_handoffs, *args, **kwargs) - - agents._run_impl.RunImpl.execute_handoffs = classmethod( - old_wrapped_execute_handoffs - ) - - original_execute_final_output = agents._run_impl.RunImpl.execute_final_output - - @wraps(agents._run_impl.RunImpl.execute_final_output.__func__) - async def old_wrapped_final_output( - cls: "agents.Runner", *args: "Any", **kwargs: "Any" - ) -> "SingleStepResult": - return await _execute_final_output( - original_execute_final_output, *args, **kwargs - ) - - agents._run_impl.RunImpl.execute_final_output = classmethod( - old_wrapped_final_output - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py deleted file mode 100644 index f5de978be0..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.openai_agents" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py deleted file mode 100644 index 85d48f2d41..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .agent_run import ( - _execute_final_output, # noqa: F401 - _execute_handoffs, # noqa: F401 - _run_single_turn, # noqa: F401 - _run_single_turn_streamed, # noqa: F401 -) -from .error_tracing import _patch_error_tracing # noqa: F401 -from .models import _get_model # noqa: F401 -from .runner import _create_run_streamed_wrapper, _create_run_wrapper # noqa: F401 -from .tools import _get_all_tools # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py deleted file mode 100644 index 71883b2eef..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/agent_run.py +++ /dev/null @@ -1,332 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import ( - handoff_span, - invoke_agent_span, - update_invoke_agent_span, -) - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Optional, Union - - from agents.run_internal.run_steps import SingleStepResult - - from sentry_sdk.tracing import Span - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _has_active_agent_span(context_wrapper: "agents.RunContextWrapper") -> bool: - """Check if there's an active agent span for this context""" - return getattr(context_wrapper, "_sentry_current_agent", None) is not None - - -def _get_current_agent( - context_wrapper: "agents.RunContextWrapper", -) -> "Optional[agents.Agent]": - """Get the current agent from context wrapper""" - return getattr(context_wrapper, "_sentry_current_agent", None) - - -def _close_streaming_workflow_span(agent: "Optional[agents.Agent]") -> None: - """Close the workflow span for streaming executions if it exists.""" - if agent and hasattr(agent, "_sentry_workflow_span"): - workflow_span = agent._sentry_workflow_span - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - -def _maybe_start_agent_span( - context_wrapper: "agents.RunContextWrapper", - agent: "agents.Agent", - should_run_agent_start_hooks: bool, - span_kwargs: "dict[str, Any]", - is_streaming: bool = False, -) -> "Optional[Union[Span, StreamedSpan]]": - """ - Start an agent invocation span if conditions are met. - Handles ending any existing span for a different agent. - - Returns the new span if started, or the existing span if conditions aren't met. - """ - if not (should_run_agent_start_hooks and agent and context_wrapper): - return getattr(context_wrapper, "_sentry_agent_span", None) - - # End any existing span for a different agent - if _has_active_agent_span(context_wrapper): - current_agent = _get_current_agent(context_wrapper) - if current_agent and current_agent != agent: - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - # Store the agent on the context wrapper so we can access it later - context_wrapper._sentry_current_agent = agent - span = invoke_agent_span(context_wrapper, agent, span_kwargs) - context_wrapper._sentry_agent_span = span - agent._sentry_agent_span = span - - if not is_streaming: - return span - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - return span - - -async def _run_single_turn( - original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn that - - creates agent invocation spans if there is no already active agent invocation span. - - ends the agent invocation span if and only if an exception is raised in `_run_single_turn()`. - """ - # openai-agents >= 0.14 passes `bindings: AgentBindings` instead of `agent`. - bindings = kwargs.get("bindings") - agent = ( - getattr(bindings, "public_agent", None) - if bindings is not None - else kwargs.get("agent") - ) - context_wrapper = kwargs.get("context_wrapper") - should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False) - - span = _maybe_start_agent_span( - context_wrapper, agent, should_run_agent_start_hooks, kwargs - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn(*args, **kwargs) - - try: - result = await original_run_single_turn(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - return result - - -async def _run_single_turn_streamed( - original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched _run_single_turn_streamed that - - creates agent invocation spans for streaming if there is no already active agent invocation span. - - ends the agent invocation span if and only if `_run_single_turn_streamed()` raises an exception. - - Note: Unlike _run_single_turn which uses keyword-only arguments (*,), - _run_single_turn_streamed uses positional arguments. The call signature =v0.14 is: - _run_single_turn_streamed( - streamed_result, # args[0] - bindings, # args[1] - hooks, # args[2] - context_wrapper, # args[3] - run_config, # args[4] - should_run_agent_start_hooks, # args[5] - tool_use_tracker, # args[6] - all_tools, # args[7] - server_conversation_tracker, # args[8] (optional) - ) - """ - streamed_result = args[0] if len(args) > 0 else kwargs.get("streamed_result") - # openai-agents >= 0.14 passes `bindings: AgentBindings` at args[1] instead of `agent`. - agent_or_bindings = ( - args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent")) - ) - agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings) - context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper") - should_run_agent_start_hooks = bool( - args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False) - ) - - span_kwargs: "dict[str, Any]" = {} - if streamed_result and hasattr(streamed_result, "input"): - span_kwargs["original_input"] = streamed_result.input - - span = _maybe_start_agent_span( - context_wrapper, - agent, - should_run_agent_start_hooks, - span_kwargs, - is_streaming=True, - ) - - if ( - span is None - or (isinstance(span, StreamedSpan) and span.end_timestamp is not None) - or (not isinstance(span, StreamedSpan) and span.timestamp is not None) - ): - return await original_run_single_turn_streamed(*args, **kwargs) - - try: - result = await original_run_single_turn_streamed(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - return result - - -async def _execute_handoffs( - original_execute_handoffs: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_handoffs that - - creates and manages handoff spans. - - ends the agent invocation span. - - ends the workflow span if the response is streamed and an exception is raised in `execute_handoffs()`. - """ - - context_wrapper = kwargs.get("context_wrapper") - run_handoffs = kwargs.get("run_handoffs") - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - - # Create Sentry handoff span for the first handoff (agents library only processes the first one) - if run_handoffs: - first_handoff = run_handoffs[0] - handoff_agent_name = first_handoff.handoff.agent_name - handoff_span(context_wrapper, agent, handoff_agent_name) - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - # Call original method with all parameters - try: - return await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - reraise(*exc_info) - - # Call original method with all parameters - try: - result = await original_execute_handoffs(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span(span=span, context=context_wrapper, agent=agent) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result - - -async def _execute_final_output( - original_execute_final_output: "Callable[..., SingleStepResult]", - *args: "Any", - **kwargs: "Any", -) -> "SingleStepResult": - """ - Patched execute_final_output that - - ends the agent invocation span. - - ends the workflow span if the response is streamed. - """ - - # openai-agents >= 0.14 renamed `agent` to `public_agent`. - agent = kwargs.get("public_agent", kwargs.get("agent")) - context_wrapper = kwargs.get("context_wrapper") - final_output = kwargs.get("final_output") - - if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper): - try: - return await original_execute_final_output(*args, **kwargs) - finally: - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - - try: - result = await original_execute_final_output(*args, **kwargs) - except Exception: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper) - _close_streaming_workflow_span(agent) - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - - span = getattr(context_wrapper, "_sentry_agent_span", None) - if span: - update_invoke_agent_span( - span=span, context=context_wrapper, agent=agent, output=final_output - ) - span.__exit__(None, None, None) - delattr(context_wrapper, "_sentry_agent_span") - - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py deleted file mode 100644 index 68dadb3101..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/error_tracing.py +++ /dev/null @@ -1,74 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANSTATUS -from sentry_sdk.traces import SpanStatus -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if TYPE_CHECKING: - from typing import Any - - -def _patch_error_tracing() -> None: - """ - Patches agents error tracing function to inject our span error logic - when a tool execution fails. - - In newer versions, the function is at: agents.util._error_tracing.attach_error_to_current_span - In older versions, it was at: agents._utils.attach_error_to_current_span - - This works even when the module or function doesn't exist. - """ - error_tracing_module = None - - # Try newer location first (agents.util._error_tracing) - try: - from agents.util import _error_tracing - - error_tracing_module = _error_tracing - except (ImportError, AttributeError): - pass - - # Try older location (agents._utils) - if error_tracing_module is None: - try: - import agents._utils - - error_tracing_module = agents._utils - except (ImportError, AttributeError): - # Module doesn't exist in either location, nothing to patch - return - - # Check if the function exists - if not hasattr(error_tracing_module, "attach_error_to_current_span"): - return - - original_attach_error = error_tracing_module.attach_error_to_current_span - - @wraps(original_attach_error) - def sentry_attach_error_to_current_span( - error: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Wraps agents' error attachment to also set Sentry span status to error. - This allows us to properly track tool execution errors even though - the agents library swallows exceptions. - """ - # Set the current Sentry span to errored - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - current_span = sentry_sdk.get_current_scope().streamed_span - if current_span is not None: - current_span.status = SpanStatus.ERROR - else: - current_span = sentry_sdk.get_current_span() - if current_span is not None: - current_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # Call the original function - return original_attach_error(error, *args, **kwargs) - - error_tracing_module.attach_error_to_current_span = ( - sentry_attach_error_to_current_span - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py deleted file mode 100644 index 634c9fdca1..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/models.py +++ /dev/null @@ -1,197 +0,0 @@ -import copy -import time -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_sentry_baggage_to_headers, - should_propagate_trace, -) -from sentry_sdk.utils import logger - -from ..spans import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - from sentry_sdk.tracing import Span - -try: - import agents - from agents.tool import HostedMCPTool -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _set_response_model_on_agent_span( - agent: "agents.Agent", response_model: "Optional[str]" -) -> None: - """Set the response model on the agent's invoke_agent span if available.""" - if response_model: - agent_span = getattr(agent, "_sentry_agent_span", None) - if agent_span: - if isinstance(agent_span, StreamedSpan): - agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - else: - agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - -def _inject_trace_propagation_headers( - hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]" -) -> None: - headers = hosted_tool.tool_config.get("headers") - if headers is None: - headers = {} - hosted_tool.tool_config["headers"] = headers - - mcp_url = hosted_tool.tool_config.get("server_url") - if not mcp_url: - return - - if should_propagate_trace(sentry_sdk.get_client(), mcp_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format( - key=key, value=value, mcp_url=mcp_url - ) - ) - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(headers, value) - else: - headers[key] = value - - -def _get_model( - original_get_model: "Callable[..., agents.Model]", - agent: "agents.Agent", - run_config: "agents.RunConfig", -) -> "agents.Model": - """ - Responsible for - - creating and managing AI client spans. - - adding trace propagation headers to tools with type HostedMCPTool. - - setting the response model on agent invocation spans. - """ - # copy the model to double patching its methods. We use copy on purpose here (instead of deepcopy) - # because we only patch its direct methods, all underlying data can remain unchanged. - model = copy.copy(original_get_model(agent, run_config)) - - # Capture the request model name for spans (agent.model can be None when using defaults) - request_model_name = model.model if hasattr(model, "model") else str(model) - agent._sentry_request_model = request_model_name - - # Wrap _fetch_response if it exists (for OpenAI models) to capture response model - if hasattr(model, "_fetch_response"): - original_fetch_response = model._fetch_response - - @wraps(original_fetch_response) - async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any": - response = await original_fetch_response(*args, **kwargs) - if hasattr(response, "model") and response.model: - agent._sentry_response_model = str(response.model) - return response - - model._fetch_response = wrapped_fetch_response - - original_get_response = model.get_response - - @wraps(original_get_response) - async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any": - mcp_tools = kwargs.get("tools") - hosted_tools = [] - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - result = await original_get_response(*args, **kwargs) - - # Get response model captured from _fetch_response and clean up - response_model = getattr(agent, "_sentry_response_model", None) - if response_model: - delattr(agent, "_sentry_response_model") - - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span(span, result, response_model, agent) - - return result - - model.get_response = wrapped_get_response - - # Also wrap stream_response for streaming support - if hasattr(model, "stream_response"): - original_stream_response = model.stream_response - - @wraps(original_stream_response) - async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": - span_kwargs = dict(kwargs) - if len(args) > 0: - span_kwargs["system_instructions"] = args[0] - if len(args) > 1: - span_kwargs["input"] = args[1] - - hosted_tools = [] - if len(args) > 3: - mcp_tools = args[3] - - if mcp_tools is not None: - hosted_tools = [ - tool for tool in mcp_tools if isinstance(tool, HostedMCPTool) - ] - - with ai_client_span(agent, span_kwargs) as span: - for hosted_tool in hosted_tools: - _inject_trace_propagation_headers(hosted_tool, span=span) - - set_on_span = ( - span.set_attribute - if isinstance(span, StreamedSpan) - else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - streaming_response = None - ttft_recorded = False - # Capture start time locally to avoid race conditions with concurrent requests - start_time = time.perf_counter() - - async for event in original_stream_response(*args, **kwargs): - # Detect first content token (text delta event) - if not ttft_recorded and hasattr(event, "delta"): - ttft = time.perf_counter() - start_time - set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft) - ttft_recorded = True - - # Capture the full response from ResponseCompletedEvent - if hasattr(event, "response"): - streaming_response = event.response - yield event - - # Update span with response data (usage, output, model) - if streaming_response: - response_model = ( - str(streaming_response.model) - if hasattr(streaming_response, "model") - and streaming_response.model - else None - ) - _set_response_model_on_agent_span(agent, response_model) - update_ai_client_span( - span, streaming_response, response_model, agent - ) - - model.stream_response = wrapped_stream_response - - return model diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py deleted file mode 100644 index 5f9996595f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/runner.py +++ /dev/null @@ -1,221 +0,0 @@ -import sys -from functools import wraps - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import agent_workflow_span, update_invoke_agent_span -from ..utils import _capture_exception - -try: - from agents.exceptions import AgentsException -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, AsyncIterator, Callable - - -def _create_run_wrapper(original_func: "Callable[..., Any]") -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run methods to - - create and manage a root span for the agent workflow runs. - - end the agent invocation span if an `AgentsException` is raised in `run()`. - - Note agents.Runner.run_sync() is a wrapper around agents.Runner.run(), - so it does not need to be wrapped separately. - """ - - @wraps(original_func) - async def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - with agent_workflow_span(agent) as workflow_span: - # Set conversation ID on workflow span early so it's captured even on errors - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - run_result = await original_func(*args, **kwargs) - except AgentsException as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - - context_wrapper = getattr(exc.run_data, "context_wrapper", None) - if context_wrapper is not None: - invoke_agent_span = getattr( - context_wrapper, "_sentry_agent_span", None - ) - - if invoke_agent_span is not None and ( - ( - isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.end_timestamp is None - ) - or ( - not isinstance(invoke_agent_span, StreamedSpan) - and invoke_agent_span.timestamp is None - ) - ): - update_invoke_agent_span( - span=invoke_agent_span, - context=context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(*exc_info) - delattr(context_wrapper, "_sentry_agent_span") - reraise(*exc_info) - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Invoke agent span is not finished in this case. - # This is much less likely to occur than other cases because - # AgentRunner.run() is "just" a while loop around _run_single_turn. - _capture_exception(exc) - reraise(*exc_info) - - invoke_agent_span = getattr( - run_result.context_wrapper, "_sentry_agent_span", None - ) - if not invoke_agent_span: - return run_result - - update_invoke_agent_span( - span=invoke_agent_span, - context=run_result.context_wrapper, - agent=agent, - ) - - invoke_agent_span.__exit__(None, None, None) - delattr(run_result.context_wrapper, "_sentry_agent_span") - return run_result - - return wrapper - - -def _create_run_streamed_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps the agents.Runner.run_streamed method to - - create a root span for streaming agent workflow runs. - - end the workflow span if and only if the response stream is consumed or cancelled. - - Unlike run(), run_streamed() returns immediately with a RunResultStreaming object - while execution continues in a background task. The workflow span must stay open - throughout the streaming operation and close when streaming completes or is abandoned. - - Note: We don't use isolation_scope() here because it uses context variables that - cannot span async boundaries (the __enter__ and __exit__ would be called from - different async contexts, causing ValueError). - """ - - @wraps(original_func) - def wrapper(*args: "Any", **kwargs: "Any") -> "Any": - # Clone agent because agent invocation spans are attached per run. - if "starting_agent" in kwargs: - agent = kwargs["starting_agent"].clone() - else: - agent = args[0].clone() - - # Capture conversation_id from kwargs if provided - conversation_id = kwargs.get("conversation_id") - if conversation_id: - agent._sentry_conversation_id = conversation_id - - # Start workflow span immediately (before run_streamed returns) - workflow_span = agent_workflow_span(agent) - workflow_span.__enter__() - - # Set conversation ID on workflow span early so it's captured even on errors - if conversation_id: - if isinstance(workflow_span, StreamedSpan): - workflow_span.set_attribute( - SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id - ) - else: - workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - - # Store span on agent for cleanup - agent._sentry_workflow_span = workflow_span - - if "starting_agent" in kwargs: - kwargs["starting_agent"] = agent - else: - args = (agent, *args[1:]) - - try: - # Call original function to get RunResultStreaming - run_result = original_func(*args, **kwargs) - except Exception as exc: - # If run_streamed itself fails (not the background task), clean up immediately - workflow_span.__exit__(*sys.exc_info()) - _capture_exception(exc) - raise - - def _close_workflow_span() -> None: - if hasattr(agent, "_sentry_workflow_span"): - workflow_span.__exit__(*sys.exc_info()) - delattr(agent, "_sentry_workflow_span") - - if hasattr(run_result, "stream_events"): - original_stream_events = run_result.stream_events - - @wraps(original_stream_events) - async def wrapped_stream_events( - *stream_args: "Any", **stream_kwargs: "Any" - ) -> "AsyncIterator[Any]": - try: - async for event in original_stream_events( - *stream_args, **stream_kwargs - ): - yield event - finally: - _close_workflow_span() - - run_result.stream_events = wrapped_stream_events - - if hasattr(run_result, "cancel"): - original_cancel = run_result.cancel - - @wraps(original_cancel) - def wrapped_cancel(*cancel_args: "Any", **cancel_kwargs: "Any") -> "Any": - try: - return original_cancel(*cancel_args, **cancel_kwargs) - finally: - _close_workflow_span() - - run_result.cancel = wrapped_cancel - - return run_result - - return wrapper diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py deleted file mode 100644 index bd13b9d61a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/patches/tools.py +++ /dev/null @@ -1,70 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import execute_tool_span, update_execute_tool_span - -if TYPE_CHECKING: - from typing import Any, Awaitable, Callable - -try: - import agents -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -async def _get_all_tools( - original_get_all_tools: "Callable[..., Awaitable[list[agents.Tool]]]", - agent: "agents.Agent", - context_wrapper: "agents.RunContextWrapper", -) -> "list[agents.Tool]": - """ - Responsible for creating and managing `gen_ai.execute_tool` spans. - """ - # Get the original tools - tools = await original_get_all_tools(agent, context_wrapper) - - wrapped_tools = [] - for tool in tools: - # Wrap only the function tools (for now) - if tool.__class__.__name__ != "FunctionTool": - wrapped_tools.append(tool) - continue - - # Create a new FunctionTool with our wrapped invoke method - original_on_invoke = tool.on_invoke_tool - - def create_wrapped_invoke( - current_tool: "agents.Tool", current_on_invoke: "Callable[..., Any]" - ) -> "Callable[..., Any]": - @wraps(current_on_invoke) - async def sentry_wrapped_on_invoke_tool( - *args: "Any", **kwargs: "Any" - ) -> "Any": - with execute_tool_span(current_tool, *args, **kwargs) as span: - # We can not capture exceptions in tool execution here because - # `_on_invoke_tool` is swallowing the exception here: - # https://github.com/openai/openai-agents-python/blob/main/src/agents/tool.py#L409-L422 - # And because function_tool is a decorator with `default_tool_error_function` set as a default parameter - # I was unable to monkey patch it because those are evaluated at module import time - # and the SDK is too late to patch it. I was also unable to patch `_on_invoke_tool_impl` - # because it is nested inside this import time code. As if they made it hard to patch on purpose... - result = await current_on_invoke(*args, **kwargs) - update_execute_tool_span(span, agent, current_tool, result) - - return result - - return sentry_wrapped_on_invoke_tool - - wrapped_tool = agents.FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=create_wrapped_invoke(tool, original_on_invoke), - strict_json_schema=tool.strict_json_schema, - is_enabled=tool.is_enabled, - ) - wrapped_tools.append(wrapped_tool) - - return wrapped_tools diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py deleted file mode 100644 index 08802a87a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .agent_workflow import agent_workflow_span # noqa: F401 -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .handoff import handoff_span # noqa: F401 -from .invoke_agent import ( - invoke_agent_span, # noqa: F401 - update_invoke_agent_span, # noqa: F401 -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py deleted file mode 100644 index 758f06db8d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import get_start_span_function -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - from typing import Union - - import agents - - -def agent_workflow_span( - agent: "agents.Agent", -) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]": - # Create a transaction or a span if an transaction is already active - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN} - ) - - return span - - span = get_start_span_function()( - name=f"{agent.name} workflow", - origin=SPAN_ORIGIN, - ) - - return span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py deleted file mode 100644 index f4f02cb674..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/ai_client.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_input_data, - _set_output_data, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from agents import Agent - - -def ai_client_span( - agent: "Agent", get_response_kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - # TODO-anton: implement other types of operations. Now "chat" is hardcoded. - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - - _set_agent_data(span, agent) - _set_input_data(span, get_response_kwargs) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - response: "Any", - response_model: "Optional[str]" = None, - agent: "Optional[Agent]" = None, -) -> None: - """Update AI client span with response data (works for streaming and non-streaming).""" - if hasattr(response, "usage") and response.usage: - _set_usage_data(span, response.usage) - - if hasattr(response, "output") and response.output: - _set_output_data(span, response) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if response_model is not None: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - elif hasattr(response, "model") and response.model: - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model)) - - # Set conversation ID from agent if available - if agent: - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py deleted file mode 100644 index fd3a430951..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def execute_tool_span( - tool: "agents.Tool", *args: "Any", **kwargs: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool.name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool.name, - SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool.name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name) - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description) - - set_on_span = span.set_data - - if should_send_default_pii(): - input = args[1] - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - agent: "agents.Agent", - tool: "agents.Tool", - result: "Any", -) -> None: - _set_agent_data(span, agent) - - if isinstance(result, str) and result.startswith( - "An error occurred while running the tool" - ): - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if should_send_default_pii(): - set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py deleted file mode 100644 index ea91464afb..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/handoff.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -from ..consts import SPAN_ORIGIN - -if TYPE_CHECKING: - import agents - - -def handoff_span( - context: "agents.RunContextWrapper", from_agent: "agents.Agent", to_agent_name: str -) -> None: - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"handoff from {from_agent.name} to {to_agent_name}", - attributes={ - "sentry.op": OP.GEN_AI_HANDOFF, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "handoff", - }, - ) as span: - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - with sentry_sdk.start_span( - op=OP.GEN_AI_HANDOFF, - name=f"handoff from {from_agent.name} to {to_agent_name}", - origin=SPAN_ORIGIN, - ) as span: - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "handoff") - - # Add conversation ID from agent - conv_id = getattr(from_agent, "_sentry_conversation_id", None) - if conv_id: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py deleted file mode 100644 index c21145ac4a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _set_usage_data - -if TYPE_CHECKING: - from typing import Any, Union - - import agents - - -def invoke_agent_span( - context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {agent.name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - start_span_function = get_start_span_function() - span = start_span_function( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent.name}", - origin=SPAN_ORIGIN, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - if should_send_default_pii(): - messages = [] - if agent.instructions: - message = ( - agent.instructions - if isinstance(agent.instructions, str) - else safe_serialize(agent.instructions) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "system", - } - ) - - original_input = kwargs.get("original_input") - if original_input is not None: - message = ( - original_input - if isinstance(original_input, str) - else safe_serialize(original_input) - ) - messages.append( - { - "content": [{"text": message, "type": "text"}], - "role": "user", - } - ) - - if len(messages) > 0: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - _set_agent_data(span, agent) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - context: "agents.RunContextWrapper", - agent: "agents.Agent", - output: "Any" = None, -) -> None: - # Add aggregated usage data from context_wrapper - if hasattr(context, "usage"): - _set_usage_data(span, context.usage) - - if should_send_default_pii(): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) - - # Add conversation ID from agent - conv_id = getattr(agent, "_sentry_conversation_id", None) - if conv_id: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) - else: - span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py deleted file mode 100644 index 224a5f66ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openai_agents/utils.py +++ /dev/null @@ -1,244 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai._openai_completions_api import _transform_system_instructions -from sentry_sdk.ai._openai_responses_api import ( - _get_system_instructions, - _is_system_instruction, -) -from sentry_sdk.ai.utils import ( - GEN_AI_ALLOWED_MESSAGE_ROLES, - normalize_message_role, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import should_truncate_gen_ai_input -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Union - - from agents import TResponseInputItem, Usage - - from sentry_sdk._types import TextPart - -try: - import agents - -except ImportError: - raise DidNotEnable("OpenAI Agents not installed") - - -def _capture_exception(exc: "Any") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "openai_agents", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "agents.Agent" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - set_on_span( - SPANDATA.GEN_AI_SYSTEM, "openai" - ) # See footnote for https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-system for explanation why. - - set_on_span(SPANDATA.GEN_AI_AGENT_NAME, agent.name) - - if agent.model_settings.max_tokens: - set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, agent.model_settings.max_tokens) - - # Get model name from agent.model or fall back to request model (for when agent.model is None/default) - model_name = None - if agent.model: - model_name = agent.model.model if hasattr(agent.model, "model") else agent.model - elif hasattr(agent, "_sentry_request_model"): - model_name = agent._sentry_request_model - - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - if agent.model_settings.presence_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - agent.model_settings.presence_penalty, - ) - - if agent.model_settings.temperature: - set_on_span( - SPANDATA.GEN_AI_REQUEST_TEMPERATURE, agent.model_settings.temperature - ) - - if agent.model_settings.top_p: - set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, agent.model_settings.top_p) - - if agent.model_settings.frequency_penalty: - set_on_span( - SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - agent.model_settings.frequency_penalty, - ) - - if len(agent.tools) > 0: - set_on_span( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - safe_serialize([vars(tool) for tool in agent.tools]), - ) - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", usage: "Usage" -) -> None: - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - usage.input_tokens_details.cached_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - set_on_span( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING, - usage.output_tokens_details.reasoning_tokens, - ) - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) - - -def _set_input_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - get_response_kwargs: "dict[str, Any]", -) -> None: - if not should_send_default_pii(): - return - request_messages = [] - - messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", []) - - instructions_text_parts: "list[TextPart]" = [] - explicit_instructions = get_response_kwargs.get("system_instructions") - if explicit_instructions is not None: - instructions_text_parts.append( - { - "type": "text", - "content": explicit_instructions, - } - ) - - system_instructions = _get_system_instructions(messages) - - # Deliberate use of function accepting completions API type because - # of shared structure FOR THIS PURPOSE ONLY. - instructions_text_parts += _transform_system_instructions(system_instructions) - - if len(instructions_text_parts) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(instructions_text_parts), - ) - - non_system_messages = [ - message for message in messages if not _is_system_instruction(message) - ] - for message in non_system_messages: - if "role" in message: - normalized_role = normalize_message_role(message.get("role")) # type: ignore - content = message.get("content") # type: ignore - request_messages.append( - { - "role": normalized_role, - "content": ( - [{"type": "text", "text": content}] - if isinstance(content, str) - else content - ), - } - ) - else: - if message.get("type") == "function_call": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT, - "content": [message], - } - ) - elif message.get("type") == "function_call_output": # type: ignore - request_messages.append( - { - "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL, - "content": [message], - } - ) - - normalized_messages = normalize_message_roles(request_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - if not should_send_default_pii(): - return - - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } - - for output in result.output: - if output.type == "function_call": - output_messages["tool"].append(output.dict()) - elif output.type == "message": - for output_message in output.content: - try: - output_messages["response"].append(output_message.text) - except AttributeError: - # Unknown output message type, just return the json - output_messages["response"].append(output_message.dict()) - - if len(output_messages["tool"]) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - else: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - safe_serialize(output_messages["tool"]), - ) - - if len(output_messages["response"]) > 0: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"] - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py deleted file mode 100644 index 281604fe38..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/openfeature.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from openfeature import api - from openfeature.hook import Hook - - if TYPE_CHECKING: - from openfeature.hook import HookContext, HookHints -except ImportError: - raise DidNotEnable("OpenFeature is not installed") - - -class OpenFeatureIntegration(Integration): - identifier = "openfeature" - - @staticmethod - def setup_once() -> None: - # Register the hook within the global openfeature hooks list. - api.add_hooks(hooks=[OpenFeatureHook()]) - - -class OpenFeatureHook(Hook): - def after(self, hook_context: "Any", details: "Any", hints: "Any") -> None: - if isinstance(details.value, bool): - add_feature_flag(details.flag_key, details.value) - - def error( - self, hook_context: "HookContext", exception: Exception, hints: "HookHints" - ) -> None: - if isinstance(hook_context.default_value, bool): - add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py deleted file mode 100644 index d76b8058ee..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor - -__all__ = [ - "SentryPropagator", - "SentrySpanProcessor", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py deleted file mode 100644 index d6733036ea..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable - -try: - from opentelemetry.context import create_key -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -SENTRY_TRACE_KEY = create_key("sentry-trace") -SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py deleted file mode 100644 index 472e8181f9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/integration.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -IMPORTANT: The contents of this file are part of a proof of concept and as such -are experimental and not suitable for production use. They may be changed or -removed at any time without prior notice. -""" - -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - -try: - from opentelemetry import trace - from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -try: - from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] - DjangoInstrumentor, - ) -except ImportError: - DjangoInstrumentor = None - - -CONFIGURABLE_INSTRUMENTATIONS = { - DjangoInstrumentor: {"is_sql_commentor_enabled": True}, -} - - -class OpenTelemetryIntegration(Integration): - identifier = "opentelemetry" - - @staticmethod - def setup_once() -> None: - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if has_span_streaming_enabled(options): - logger.warning( - "[OTel] OpenTelemetryIntegration is not compatible with span streaming " - "(trace_lifecycle='stream') and will be disabled." - ) - return - - warnings.warn( - "OpenTelemetryIntegration is deprecated. " - "Please use OTLPIntegration instead: " - "https://docs.sentry.io/platforms/python/integrations/otlp/", - DeprecationWarning, - stacklevel=2, - ) - - _setup_sentry_tracing() - # _setup_instrumentors() - - logger.debug("[OTel] Finished setting up OpenTelemetry integration") - - -def _setup_sentry_tracing() -> None: - provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) - trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) - - -def _setup_instrumentors() -> None: - for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): - if instrumentor is None: - continue - instrumentor().instrument(**kwargs) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py deleted file mode 100644 index 5b5f13861d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/propagator.py +++ /dev/null @@ -1,128 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data - -try: - from opentelemetry import trace - from opentelemetry.context import ( - Context, - get_current, - set_value, - ) - from opentelemetry.propagators.textmap import ( - CarrierT, - Getter, - Setter, - TextMapPropagator, - default_getter, - default_setter, - ) - from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Set - - -class SentryPropagator(TextMapPropagator): - """ - Propagates tracing headers for Sentry's tracing system in a way OTel understands. - """ - - def extract( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - getter: "Getter[CarrierT]" = default_getter, - ) -> "Context": - if context is None: - context = get_current() - - sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) - if not sentry_trace: - return context - - sentrytrace = extract_sentrytrace_data(sentry_trace[0]) - if not sentrytrace: - return context - - context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) - - trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] - - span_context = SpanContext( - trace_id=int(trace_id, 16), # type: ignore - span_id=int(span_id, 16), # type: ignore - # we simulate a sampled trace on the otel side and leave the sampling to sentry - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - - baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) - - if baggage_header: - baggage = Baggage.from_incoming_header(baggage_header[0]) - else: - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and frozen and won't be populated as head SDK. - baggage = Baggage(sentry_items={}) - - baggage.freeze() - context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) - - span = NonRecordingSpan(span_context) - modified_context = trace.set_span_in_context(span, context) - return modified_context - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - if context is None: - context = get_current() - - current_span = trace.get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - span_id = trace.format_span_id(current_span_context.span_id) - - span_map = SentrySpanProcessor.otel_span_map - sentry_span = span_map.get(span_id, None) - if not sentry_span: - return - - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) - - if sentry_span.containing_transaction: - baggage = sentry_span.containing_transaction.get_baggage() - if baggage: - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - @property - def fields(self) -> "Set[str]": - return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py deleted file mode 100644 index 1efd2ac455..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/opentelemetry/span_processor.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from time import time -from typing import TYPE_CHECKING, cast - -from urllib3.util import parse_url as urlparse - -from sentry_sdk import get_client, start_transaction -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing import Transaction - -try: - from opentelemetry.context import get_value - from opentelemetry.sdk.trace import ReadableSpan as OTelSpan - from opentelemetry.sdk.trace import SpanProcessor - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import ( - SpanKind, - format_span_id, - format_trace_id, - get_current_span, - ) - from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from opentelemetry import context as context_api - from opentelemetry.trace import SpanContext - - from sentry_sdk._types import Event, Hint - -OPEN_TELEMETRY_CONTEXT = "otel" -SPAN_MAX_TIME_OPEN_MINUTES = 10 -SPAN_ORIGIN = "auto.otel" - - -def link_trace_context_to_error_event( - event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" -) -> "Event": - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return event - - if hasattr(event, "type") and event["type"] == "transaction": - return event - - otel_span = get_current_span() - if not otel_span: - return event - - ctx = otel_span.get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return event - - sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) - if not sentry_span: - return event - - contexts = event.setdefault("contexts", {}) - contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) - - return event - - -class SentrySpanProcessor(SpanProcessor): - """ - Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. - """ - - # The mapping from otel span ids to sentry spans - otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} - - # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES - open_spans: "dict[int, set[str]]" = {} - - initialized: "bool" = False - - def __new__(cls) -> "SentrySpanProcessor": - if not hasattr(cls, "instance"): - cls.instance = super().__new__(cls) - - # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). - return cls.instance # type: ignore[misc] - - def __init__(self) -> None: - if self.initialized: - return - - @add_global_event_processor - def global_event_processor(event: "Event", hint: "Hint") -> "Event": - return link_trace_context_to_error_event(event, self.otel_span_map) - - self.initialized = True - - def _prune_old_spans(self: "SentrySpanProcessor") -> None: - """ - Prune spans that have been open for too long. - """ - current_time_minutes = int(time() / 60) - for span_start_minutes in list( - self.open_spans.keys() - ): # making a list because we change the dict - # prune empty open spans buckets - if self.open_spans[span_start_minutes] == set(): - self.open_spans.pop(span_start_minutes) - - # prune old buckets - elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: - for span_id in self.open_spans.pop(span_start_minutes): - self.otel_span_map.pop(span_id, None) - - def on_start( - self, - otel_span: "OTelSpan", - parent_context: "Optional[context_api.Context]" = None, - ) -> None: - client = get_client() - - if not client.parsed_dsn: - return - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - if self._is_sentry_span(otel_span): - return - - trace_data = self._get_trace_data( - span_context, otel_span.parent, parent_context - ) - - parent_span_id = trace_data["parent_span_id"] - sentry_parent_span = ( - self.otel_span_map.get(parent_span_id) if parent_span_id else None - ) - - start_timestamp = None - if otel_span.start_time is not None: - start_timestamp = datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span = None - if sentry_parent_span: - sentry_span = sentry_parent_span.start_child( - span_id=trace_data["span_id"], - name=otel_span.name, - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - else: - sentry_span = start_transaction( - name=otel_span.name, - span_id=trace_data["span_id"], - parent_span_id=parent_span_id, - trace_id=trace_data["trace_id"], - baggage=trace_data["baggage"], - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - - self.otel_span_map[trace_data["span_id"]] = sentry_span - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).add( - trace_data["span_id"] - ) - - self._prune_old_spans() - - def on_end(self, otel_span: "OTelSpan") -> None: - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - span_id = format_span_id(span_context.span_id) - sentry_span = self.otel_span_map.pop(span_id, None) - if not sentry_span: - return - - sentry_span.op = otel_span.name - - self._update_span_with_otel_status(sentry_span, otel_span) - - if isinstance(sentry_span, Transaction): - sentry_span.name = otel_span.name - sentry_span.set_context( - OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) - ) - self._update_transaction_with_otel_data(sentry_span, otel_span) - - else: - self._update_span_with_otel_data(sentry_span, otel_span) - - end_timestamp = None - if otel_span.end_time is not None: - end_timestamp = datetime.fromtimestamp( - otel_span.end_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span.finish(end_timestamp=end_timestamp) - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) - - self._prune_old_spans() - - def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: - """ - Break infinite loop: - HTTP requests to Sentry are caught by OTel and send again to Sentry. - """ - otel_span_url = None - if otel_span.attributes is not None: - otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) - otel_span_url = cast("Optional[str]", otel_span_url) - - parsed_dsn = get_client().parsed_dsn - dsn_url = parsed_dsn.netloc if parsed_dsn else None - - if otel_span_url and dsn_url and dsn_url in otel_span_url: - return True - - return False - - def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": - """ - Returns the OTel context for Sentry. - See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context - """ - ctx = {} - - if otel_span.attributes: - ctx["attributes"] = dict(otel_span.attributes) - - if otel_span.resource.attributes: - ctx["resource"] = dict(otel_span.resource.attributes) - - return ctx - - def _get_trace_data( - self, - span_context: "SpanContext", - parent_span_context: "Optional[SpanContext]", - parent_context: "Optional[context_api.Context]", - ) -> "dict[str, Any]": - """ - Extracts tracing information from one OTel span's context and its parent OTel context. - """ - trace_data: "dict[str, Any]" = {} - - span_id = format_span_id(span_context.span_id) - trace_data["span_id"] = span_id - - trace_id = format_trace_id(span_context.trace_id) - trace_data["trace_id"] = trace_id - - parent_span_id = ( - format_span_id(parent_span_context.span_id) if parent_span_context else None - ) - trace_data["parent_span_id"] = parent_span_id - - sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) - sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) - trace_data["parent_sampled"] = ( - sentry_trace_data["parent_sampled"] if sentry_trace_data else None - ) - - baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) - trace_data["baggage"] = baggage - - return trace_data - - def _update_span_with_otel_status( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Set the Sentry span status from the OTel span - """ - if otel_span.status.is_unset: - return - - if otel_span.status.is_ok: - sentry_span.set_status(SPANSTATUS.OK) - return - - sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - def _update_span_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Convert OTel span data and update the Sentry span with it. - This should eventually happen on the server when ingesting the spans. - """ - sentry_span.set_data("otel.kind", otel_span.kind) - - op = otel_span.name - description = otel_span.name - - if otel_span.attributes is not None: - for key, val in otel_span.attributes.items(): - sentry_span.set_data(key, val) - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - http_method = cast("Optional[str]", http_method) - - db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) - - if http_method: - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - description = http_method - - peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) - if peer_name: - description += " {}".format(peer_name) - - target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) - if target: - description += " {}".format(target) - - if not peer_name and not target: - url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) - url = cast("Optional[str]", url) - if url: - parsed_url = urlparse(url) - url = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path - ) - description += " {}".format(url) - - status_code = otel_span.attributes.get( - SpanAttributes.HTTP_STATUS_CODE, None - ) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - elif db_query: - op = "db" - statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) - statement = cast("Optional[str]", statement) - if statement: - description = statement - - sentry_span.op = op - sentry_span.description = description - - def _update_transaction_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - if otel_span.attributes is None: - return - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - - if http_method: - status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - sentry_span.op = op diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py deleted file mode 100644 index b91f2cfd21..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/otlp.py +++ /dev/null @@ -1,223 +0,0 @@ -from sentry_sdk import capture_event, get_client -from sentry_sdk.consts import VERSION, EndpointType -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import register_external_propagation_context -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - event_from_exception, - logger, -) - -try: - from opentelemetry.context import ( - Context, - get_current, - get_value, - ) - from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter - from opentelemetry.propagate import set_global_textmap - from opentelemetry.propagators.textmap import ( - CarrierT, - Setter, - default_setter, - ) - from opentelemetry.sdk.trace import Span, TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - from opentelemetry.trace import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - SpanContext, - format_span_id, - format_trace_id, - get_current_span, - get_tracer_provider, - set_tracer_provider, - ) - - from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY - from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -except ImportError: - raise DidNotEnable("opentelemetry-distro[otlp] is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple - - -def otel_propagation_context() -> "Optional[Tuple[str, str]]": - """ - Get the (trace_id, span_id) from opentelemetry if exists. - """ - ctx = get_current_span().get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return None - - return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id)) - - -def setup_otlp_traces_exporter( - dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None -) -> None: - tracer_provider = get_tracer_provider() - - if not isinstance(tracer_provider, TracerProvider): - logger.debug("[OTLP] No TracerProvider configured by user, creating a new one") - tracer_provider = TracerProvider() - set_tracer_provider(tracer_provider) - - endpoint = None - headers = None - if collector_url: - endpoint = collector_url - logger.debug(f"[OTLP] Sending traces to collector at {endpoint}") - elif dsn: - auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}") - endpoint = auth.get_api_url(EndpointType.OTLP_TRACES) - headers = {"X-Sentry-Auth": auth.to_header()} - logger.debug(f"[OTLP] Sending traces to {endpoint}") - - otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers) - span_processor = BatchSpanProcessor(otlp_exporter) - tracer_provider.add_span_processor(span_processor) - - -_sentry_patched_exception = False - - -def setup_capture_exceptions() -> None: - """ - Intercept otel's Span.record_exception to automatically capture those exceptions in Sentry. - """ - global _sentry_patched_exception - _original_record_exception = Span.record_exception - - if _sentry_patched_exception: - return - - def _sentry_patched_record_exception( - self: "Span", exception: "BaseException", *args: "Any", **kwargs: "Any" - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration and otlp_integration.capture_exceptions: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=get_client().options, - mechanism={"type": OTLPIntegration.identifier, "handled": False}, - ) - capture_event(event, hint=hint) - - _original_record_exception(self, exception, *args, **kwargs) - - Span.record_exception = _sentry_patched_record_exception # type: ignore[method-assign] - _sentry_patched_exception = True - - -class SentryOTLPPropagator(SentryPropagator): - """ - We need to override the inject of the older propagator since that - is SpanProcessor based. - - !!! Note regarding baggage: - We cannot meaningfully populate a new baggage as a head SDK - when we are using OTLP since we don't have any sort of transaction semantic to - track state across a group of spans. - - For incoming baggage, we just pass it on as is so that case is correctly handled. - """ - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - otlp_integration = get_client().get_integration(OTLPIntegration) - if otlp_integration is None: - return - - if context is None: - context = get_current() - - current_span = get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - sentry_trace = _to_traceparent(current_span_context) - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace) - - baggage = get_value(SENTRY_BAGGAGE_KEY, context) - if baggage is not None and isinstance(baggage, Baggage): - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - -def _to_traceparent(span_context: "SpanContext") -> str: - """ - Helper method to generate the sentry-trace header. - """ - span_id = format_span_id(span_context.span_id) - trace_id = format_trace_id(span_context.trace_id) - sampled = span_context.trace_flags.sampled - - return f"{trace_id}-{span_id}-{'1' if sampled else '0'}" - - -class OTLPIntegration(Integration): - """ - Automatically setup OTLP ingestion from the DSN. - - :param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True. - Set to False to setup the TracerProvider manually. - :param collector_url: URL of your own OpenTelemetry collector, defaults to None. - When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN. - :param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True. - Set to False to configure propagators manually or to disable propagation. - :param capture_exceptions: Intercept and capture exceptions on the OpenTelemetry Span in Sentry as well, defaults to False. - Set to True to turn on capturing but be aware that since Sentry captures most exceptions, duplicate exceptions might be dropped by DedupeIntegration in many cases. - """ - - identifier = "otlp" - - def __init__( - self, - setup_otlp_traces_exporter: bool = True, - collector_url: "Optional[str]" = None, - setup_propagator: bool = True, - capture_exceptions: bool = False, - ) -> None: - self.setup_otlp_traces_exporter = setup_otlp_traces_exporter - self.collector_url = collector_url - self.setup_propagator = setup_propagator - self.capture_exceptions = capture_exceptions - - @staticmethod - def setup_once() -> None: - logger.debug("[OTLP] Setting up trace linking for all events") - register_external_propagation_context(otel_propagation_context) - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if self.setup_otlp_traces_exporter: - logger.debug("[OTLP] Setting up OTLP exporter") - dsn: "Optional[str]" = options.get("dsn") if options else None - setup_otlp_traces_exporter(dsn, collector_url=self.collector_url) - - if self.setup_propagator: - logger.debug("[OTLP] Setting up propagator for distributed tracing") - # TODO-neel better propagator support, chain with existing ones if possible instead of replacing - set_global_textmap(SentryOTLPPropagator()) - - setup_capture_exceptions() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py deleted file mode 100644 index 569e3e1fa5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pure_eval.py +++ /dev/null @@ -1,134 +0,0 @@ -import ast -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk import serializer -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import iter_stacks, walk_exception_chain - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, List, Optional, Tuple - - from sentry_sdk._types import Event, Hint - -try: - from executing import Source -except ImportError: - raise DidNotEnable("executing is not installed") - -try: - from pure_eval import Evaluator -except ImportError: - raise DidNotEnable("pure_eval is not installed") - -try: - # Used implicitly, just testing it's available - import asttokens # noqa -except ImportError: - raise DidNotEnable("asttokens is not installed") - - -class PureEvalIntegration(Integration): - identifier = "pure_eval" - - @staticmethod - def setup_once() -> None: - @add_global_event_processor - def add_executing_info( - event: "Event", hint: "Optional[Hint]" - ) -> "Optional[Event]": - if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None: - return event - - if hint is None: - return event - - exc_info = hint.get("exc_info", None) - - if exc_info is None: - return event - - exception = event.get("exception", None) - - if exception is None: - return event - - values = exception.get("values", None) - - if values is None: - return event - - for exception, (_exc_type, _exc_value, exc_tb) in zip( - reversed(values), walk_exception_chain(exc_info) - ): - sentry_frames = [ - frame - for frame in exception.get("stacktrace", {}).get("frames", []) - if frame.get("function") - ] - tbs = list(iter_stacks(exc_tb)) - if len(sentry_frames) != len(tbs): - continue - - for sentry_frame, tb in zip(sentry_frames, tbs): - sentry_frame["vars"] = ( - pure_eval_frame(tb.tb_frame) or sentry_frame["vars"] - ) - return event - - -def pure_eval_frame(frame: "FrameType") -> "Dict[str, Any]": - source = Source.for_frame(frame) - if not source.tree: - return {} - - statements = source.statements_at_line(frame.f_lineno) - if not statements: - return {} - - scope = stmt = list(statements)[0] - while True: - # Get the parent first in case the original statement is already - # a function definition, e.g. if we're calling a decorator - # In that case we still want the surrounding scope, not that function - scope = scope.parent - if isinstance(scope, (ast.FunctionDef, ast.ClassDef, ast.Module)): - break - - evaluator = Evaluator.from_frame(frame) - expressions = evaluator.interesting_expressions_grouped(scope) - - def closeness(expression: "Tuple[List[Any], Any]") -> "Tuple[int, int]": - # Prioritise expressions with a node closer to the statement executed - # without being after that statement - # A higher return value is better - the expression will appear - # earlier in the list of values and is less likely to be trimmed - nodes, _value = expression - - def start(n: "ast.expr") -> "Tuple[int, int]": - return (n.lineno, n.col_offset) - - nodes_before_stmt = [ - node for node in nodes if start(node) < stmt.last_token.end - ] - if nodes_before_stmt: - # The position of the last node before or in the statement - return max(start(node) for node in nodes_before_stmt) - else: - # The position of the first node after the statement - # Negative means it's always lower priority than nodes that come before - # Less negative means closer to the statement and higher priority - lineno, col_offset = min(start(node) for node in nodes) - return (-lineno, -col_offset) - - # This adds the first_token and last_token attributes to nodes - atok = source.asttokens() - - expressions.sort(key=closeness, reverse=True) - vars = { - atok.get_text(nodes[0]): value - for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH] - } - return serializer.serialize(vars, is_vars=True) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py deleted file mode 100644 index 81e7cf8090..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/__init__.py +++ /dev/null @@ -1,187 +0,0 @@ -import functools - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import capture_internal_exceptions, parse_version - -try: - import pydantic_ai # type: ignore # noqa: F401 - from pydantic_ai import Agent -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -from importlib.metadata import PackageNotFoundError, version -from typing import TYPE_CHECKING - -from .patches import ( - _patch_agent_run, - _patch_graph_nodes, - _patch_tool_execution, -) -from .spans.ai_client import ai_client_span, update_ai_client_span - -if TYPE_CHECKING: - from typing import Any - - from pydantic_ai import ModelRequestContext, RunContext - from pydantic_ai.capabilities import Hooks # type: ignore - from pydantic_ai.messages import ModelResponse # type: ignore - - -def register_hooks(hooks: "Hooks") -> None: - """ - Creates hooks for chat model calls and register the hooks by adding the hooks to the `capabilities` argument passed to `Agent.__init__()`. - """ - - @hooks.on.before_model_request # type: ignore - async def on_request( - ctx: "RunContext[None]", request_context: "ModelRequestContext" - ) -> "ModelRequestContext": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return request_context - - span = ai_client_span( - messages=request_context.messages, - agent=None, - model=request_context.model, - model_settings=request_context.model_settings, - ) - - run_context_metadata["_sentry_span"] = span - span.__enter__() - - return request_context - - @hooks.on.after_model_request # type: ignore - async def on_response( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - response: "ModelResponse", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - if not isinstance(run_context_metadata, dict): - return response - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - return response - - update_ai_client_span(span, response) - span.__exit__(None, None, None) - - return response - - @hooks.on.model_request_error # type: ignore - async def on_error( - ctx: "RunContext[None]", - *, - request_context: "ModelRequestContext", - error: "Exception", - ) -> "ModelResponse": - run_context_metadata = ctx.metadata - - if not isinstance(run_context_metadata, dict): - raise error - - span = run_context_metadata.pop("_sentry_span", None) - if span is None: - raise error - - with capture_internal_exceptions(): - span.__exit__(type(error), error, error.__traceback__) - - raise error - - original_init = Agent.__init__ - - @functools.wraps(original_init) - def patched_init(self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any") -> None: - caps = list(kwargs.get("capabilities") or []) - caps.append(hooks) - kwargs["capabilities"] = caps - - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {} # Used as shared reference between hooks - - return original_init(self, *args, **kwargs) - - Agent.__init__ = patched_init - - -class PydanticAIIntegration(Integration): - """ - Typical interaction with the library: - 1. The user creates an Agent instance with configuration, including system instructions sent to every model call. - 2. The user calls `Agent.run()` or `Agent.run_stream()` to start an agent run. The latter can be used to incrementally receive progress. - - Each run invocation has `RunContext` objects that are passed to the library hooks. - 3. In a loop, the agent repeatedly calls the model, maintaining a conversation history that includes previous messages and tool results, which is passed to each call. - - Internally, Pydantic AI maintains an execution graph in which ModelRequestNode are responsible for model calls, including retries. - Hooks using the decorators provided by `pydantic_ai.capabilities` create and manage spans for model calls when these hooks are available (newer library versions). - The span is created in `on_request` and stored in the metadata of the `RunContext` object shared with `on_response` and `on_error`. - - The metadata dictionary on the RunContext instance is initialized with `{"_sentry_span": None}` in the `_create_run_wrapper()` and `_create_streaming_wrapper()` wrappers that - instrument `Agent.run()` and `Agent.run_stream()`, respectively. A non-empty dictionary is required for the metadata object to be a shared reference between hooks. - """ - - identifier = "pydantic_ai" - origin = f"auto.ai.{identifier}" - using_request_hooks = False - - def __init__( - self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True - ) -> None: - """ - Initialize the Pydantic AI integration. - - Args: - include_prompts: Whether to include prompts and messages in span data. - Requires send_default_pii=True. Defaults to True. - handled_tool_exceptions: Capture tool call exceptions that Pydantic AI - internally prevents from bubbling up. - """ - self.include_prompts = include_prompts - self.handled_tool_call_exceptions = handled_tool_call_exceptions - - @staticmethod - def setup_once() -> None: - """ - Set up the pydantic-ai integration. - - This patches the key methods in pydantic-ai to create Sentry spans for: - - Agent invocations (Agent.run methods) - - Model requests (AI client calls) - - Tool executions - """ - _patch_agent_run() - _patch_tool_execution() - - PydanticAIIntegration.using_request_hooks = False - try: - PYDANTIC_AI_VERSION = version("pydantic-ai-slim") - except PackageNotFoundError: - return - - PYDANTIC_AI_VERSION = parse_version(PYDANTIC_AI_VERSION) - if PYDANTIC_AI_VERSION is None: - return - - # ModelRequestContext.model added in https://github.com/pydantic/pydantic-ai/commit/f1260dfe09907f17688eee1646daf898fc428d4c - if PYDANTIC_AI_VERSION < ( - 1, - 73, - ): - _patch_graph_nodes() - return - - try: - from pydantic_ai.capabilities import Hooks - except ImportError: - return - - PydanticAIIntegration.using_request_hooks = True - hooks = Hooks() - register_hooks(hooks) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py deleted file mode 100644 index afa66dc47d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/consts.py +++ /dev/null @@ -1 +0,0 @@ -SPAN_ORIGIN = "auto.ai.pydantic_ai" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py deleted file mode 100644 index d0ea6242b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .agent_run import _patch_agent_run # noqa: F401 -from .graph_nodes import _patch_graph_nodes # noqa: F401 -from .tools import _patch_tool_execution # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py deleted file mode 100644 index 3039e40698..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +++ /dev/null @@ -1,198 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import invoke_agent_span, update_invoke_agent_span -from ..utils import _capture_exception, pop_agent, push_agent - -try: - from pydantic_ai.agent import Agent # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, Union - - -class _StreamingContextManagerWrapper: - """Wrapper for streaming methods that return async context managers.""" - - def __init__( - self, - agent: "Any", - original_ctx_manager: "Any", - user_prompt: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = True, - ) -> None: - self.agent = agent - self.original_ctx_manager = original_ctx_manager - self.user_prompt = user_prompt - self.model = model - self.model_settings = model_settings - self.is_streaming = is_streaming - self._isolation_scope: "Any" = None - self._span: "Optional[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]]" = None - self._result: "Any" = None - - async def __aenter__(self) -> "Any": - # Set up isolation scope and invoke_agent span - self._isolation_scope = sentry_sdk.isolation_scope() - self._isolation_scope.__enter__() - - # Create invoke_agent span (will be closed in __aexit__) - self._span = invoke_agent_span( - self.user_prompt, - self.agent, - self.model, - self.model_settings, - self.is_streaming, - ) - self._span.__enter__() - - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in __aexit__ even if exceptions occur - push_agent(self.agent, self.is_streaming) - - # Enter the original context manager - result = await self.original_ctx_manager.__aenter__() - self._result = result - return result - - async def __aexit__(self, exc_type: "Any", exc_val: "Any", exc_tb: "Any") -> None: - try: - # Exit the original context manager first - await self.original_ctx_manager.__aexit__(exc_type, exc_val, exc_tb) - - # Update span with result if successful - if exc_type is None and self._result and self._span is not None: - update_invoke_agent_span(self._span, self._result) - finally: - # Pop agent from contextvar stack - pop_agent() - - # Clean up invoke span - if self._span: - self._span.__exit__(exc_type, exc_val, exc_tb) - - # Clean up isolation scope - if self._isolation_scope: - self._isolation_scope.__exit__(exc_type, exc_val, exc_tb) - - -def _create_run_wrapper( - original_func: "Callable[..., Any]", is_streaming: bool = False -) -> "Callable[..., Any]": - """ - Wraps the Agent.run method to create an invoke_agent span. - - Args: - original_func: The original run method - is_streaming: Whether this is a streaming method (for future use) - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Isolate each workflow so that when agents are run in asyncio tasks they - # don't touch each other's scopes - with sentry_sdk.isolation_scope(): - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Create invoke_agent span - with invoke_agent_span( - user_prompt, self, model, model_settings, is_streaming - ) as span: - # Push agent to contextvar stack after span is successfully created and entered - # This ensures proper pairing with pop_agent() in finally even if exceptions occur - push_agent(self, is_streaming) - - try: - result = await original_func(self, *args, **kwargs) - - # Update span with result - update_invoke_agent_span(span, result) - - return result - except Exception as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc) - reraise(*exc_info) - finally: - # Pop agent from contextvar stack - pop_agent() - - return wrapper - - -def _create_streaming_wrapper( - original_func: "Callable[..., Any]", -) -> "Callable[..., Any]": - """ - Wraps run_stream method that returns an async context manager. - """ - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) # Required to avoid circular import - - @wraps(original_func) - def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - # Extract parameters for the span - user_prompt = kwargs.get("user_prompt") or (args[0] if args else None) - model = kwargs.get("model") - model_settings = kwargs.get("model_settings") - - if PydanticAIIntegration.using_request_hooks: - metadata = kwargs.get("metadata") - if metadata is None: - kwargs["metadata"] = {"_sentry_span": None} - - # Call original function to get the context manager - original_ctx_manager = original_func(self, *args, **kwargs) - - # Wrap it with our instrumentation - return _StreamingContextManagerWrapper( - agent=self, - original_ctx_manager=original_ctx_manager, - user_prompt=user_prompt, - model=model, - model_settings=model_settings, - is_streaming=True, - ) - - return wrapper - - -def _patch_agent_run() -> None: - """ - Patches the Agent run methods to create spans for agent execution. - - This patches both non-streaming (run, run_sync) and streaming - (run_stream, run_stream_events) methods. - """ - - # Store original methods - original_run = Agent.run - original_run_stream = Agent.run_stream - - # Wrap and apply patches for non-streaming methods - Agent.run = _create_run_wrapper(original_run, is_streaming=False) - - # Wrap and apply patches for streaming methods - Agent.run_stream = _create_streaming_wrapper(original_run_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py deleted file mode 100644 index afb10395f4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +++ /dev/null @@ -1,106 +0,0 @@ -from contextlib import asynccontextmanager -from functools import wraps - -from sentry_sdk.integrations import DidNotEnable - -from ..spans import ( - ai_client_span, - update_ai_client_span, -) - -try: - from pydantic_ai._agent_graph import ModelRequestNode # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - -def _extract_span_data(node: "Any", ctx: "Any") -> "tuple[list[Any], Any, Any]": - """Extract common data needed for creating chat spans. - - Returns: - Tuple of (messages, model, model_settings) - """ - # Extract model and settings from context - model = None - model_settings = None - if hasattr(ctx, "deps"): - model = getattr(ctx.deps, "model", None) - model_settings = getattr(ctx.deps, "model_settings", None) - - # Build full message list: history + current request - messages = [] - if hasattr(ctx, "state") and hasattr(ctx.state, "message_history"): - messages.extend(ctx.state.message_history) - - current_request = getattr(node, "request", None) - if current_request: - messages.append(current_request) - - return messages, model, model_settings - - -def _patch_graph_nodes() -> None: - """ - Patches the graph node execution to create appropriate spans. - - ModelRequestNode -> Creates ai_client span for model requests - CallToolsNode -> Handles tool calls (spans created in tool patching) - """ - - # Patch ModelRequestNode to create ai_client spans - original_model_request_run = ModelRequestNode.run - - @wraps(original_model_request_run) - async def wrapped_model_request_run(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - with ai_client_span(messages, None, model, model_settings) as span: - result = await original_model_request_run(self, ctx) - - # Extract response from result if available - model_response = None - if hasattr(result, "model_response"): - model_response = result.model_response - - update_ai_client_span(span, model_response) - return result - - ModelRequestNode.run = wrapped_model_request_run - - # Patch ModelRequestNode.stream for streaming requests - original_model_request_stream = ModelRequestNode.stream - - def create_wrapped_stream( - original_stream_method: "Callable[..., Any]", - ) -> "Callable[..., Any]": - """Create a wrapper for ModelRequestNode.stream that creates chat spans.""" - - @asynccontextmanager - @wraps(original_stream_method) - async def wrapped_model_request_stream(self: "Any", ctx: "Any") -> "Any": - messages, model, model_settings = _extract_span_data(self, ctx) - - # Create chat span for streaming request - with ai_client_span(messages, None, model, model_settings) as span: - # Call the original stream method - async with original_stream_method(self, ctx) as stream: - yield stream - - # After streaming completes, update span with response data - # The ModelRequestNode stores the final response in _result - model_response = None - if hasattr(self, "_result") and self._result is not None: - # _result is a NextNode containing the model_response - if hasattr(self._result, "model_response"): - model_response = self._result.model_response - - update_ai_client_span(span, model_response) - - return wrapped_model_request_stream - - ModelRequestNode.stream = create_wrapped_stream(original_model_request_stream) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py deleted file mode 100644 index 5646b5d47c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/patches/tools.py +++ /dev/null @@ -1,177 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.utils import capture_internal_exceptions, reraise - -from ..spans import execute_tool_span, update_execute_tool_span -from ..utils import _capture_exception, get_current_agent - -if TYPE_CHECKING: - from typing import Any - -try: - try: - from pydantic_ai.tool_manager import ToolManager # type: ignore - except ImportError: - from pydantic_ai._tool_manager import ToolManager # type: ignore - - from pydantic_ai.exceptions import ToolRetryError # type: ignore -except ImportError: - raise DidNotEnable("pydantic-ai not installed") - - -def _patch_tool_execution() -> None: - if hasattr(ToolManager, "execute_tool_call"): - _patch_execute_tool_call() - - elif hasattr(ToolManager, "_call_tool"): - # older versions - _patch_call_tool() - - -def _patch_execute_tool_call() -> None: - original_execute_tool_call = ToolManager.execute_tool_call - - @wraps(original_execute_tool_call) - async def wrapped_execute_tool_call( - self: "Any", validated: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - if not validated or not hasattr(validated, "call"): - return await original_execute_tool_call(self, validated, *args, **kwargs) - - # Extract tool info before calling original - call = validated.call - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_execute_tool_call( - self, - validated, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - return await original_execute_tool_call(self, validated, *args, **kwargs) - - ToolManager.execute_tool_call = wrapped_execute_tool_call - - -def _patch_call_tool() -> None: - """ - Patch ToolManager._call_tool to create execute_tool spans. - - This is the single point where ALL tool calls flow through in pydantic_ai, - regardless of toolset type (function, MCP, combined, wrapper, etc.). - - By patching here, we avoid: - - Patching multiple toolset classes - - Dealing with signature mismatches from instrumented MCP servers - - Complex nested toolset handling - """ - original_call_tool = ToolManager._call_tool - - @wraps(original_call_tool) - async def wrapped_call_tool( - self: "Any", call: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Extract tool info before calling original - name = call.tool_name - tool = self.tools.get(name) if self.tools else None - selected_tool_definition = getattr(tool, "tool_def", None) - - # Get agent from contextvar - agent = get_current_agent() - - if agent and tool: - try: - args_dict = call.args_as_dict() - except Exception: - args_dict = call.args if isinstance(call.args, dict) else {} - - # Create execute_tool span - # Nesting is handled by isolation_scope() to ensure proper parent-child relationships - with sentry_sdk.isolation_scope(): - with execute_tool_span( - name, - args_dict, - agent, - tool_definition=selected_tool_definition, - ) as span: - try: - result = await original_call_tool( - self, - call, - *args, - **kwargs, - ) - update_execute_tool_span(span, result) - return result - except ToolRetryError as exc: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - # Avoid circular import due to multi-file integration structure - from sentry_sdk.integrations.pydantic_ai import ( - PydanticAIIntegration, - ) - - integration = sentry_sdk.get_client().get_integration( - PydanticAIIntegration - ) - if ( - integration is not None - and integration.handled_tool_call_exceptions - ): - _capture_exception(exc, handled=True) - reraise(*exc_info) - - # No span context - just call original - return await original_call_tool( - self, - call, - *args, - **kwargs, - ) - - ToolManager._call_tool = wrapped_call_tool diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py deleted file mode 100644 index 574046d645..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ai_client import ai_client_span, update_ai_client_span # noqa: F401 -from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401 -from .invoke_agent import invoke_agent_span, update_invoke_agent_span # noqa: F401 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py deleted file mode 100644 index 27deb0c55c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ /dev/null @@ -1,331 +0,0 @@ -import json -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _get_model_name, - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, - get_current_agent, - get_is_streaming, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, - _set_usage_data, -) - -if TYPE_CHECKING: - from typing import Any, Dict, List, Union - - from pydantic_ai.messages import ModelMessage, SystemPromptPart # type: ignore - - from sentry_sdk._types import TextPart as SentryTextPart - -try: - from pydantic_ai.messages import ( - BaseToolCallPart, - BaseToolReturnPart, - BinaryContent, - ImageUrl, - SystemPromptPart, - TextPart, - ThinkingPart, - UserPromptPart, - ) -except ImportError: - # Fallback if these classes are not available - BaseToolCallPart = None - BaseToolReturnPart = None - SystemPromptPart = None - UserPromptPart = None - TextPart = None - ThinkingPart = None - BinaryContent = None - ImageUrl = None - - -def _transform_system_instructions( - permanent_instructions: "list[SystemPromptPart]", - current_instructions: "list[str]", -) -> "list[SentryTextPart]": - text_parts: "list[SentryTextPart]" = [ - { - "type": "text", - "content": instruction.content, - } - for instruction in permanent_instructions - ] - - text_parts.extend( - { - "type": "text", - "content": instruction, - } - for instruction in current_instructions - ) - - return text_parts - - -def _get_system_instructions( - messages: "list[ModelMessage]", -) -> "tuple[list[SystemPromptPart], list[str]]": - permanent_instructions = [] - current_instructions = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - if SystemPromptPart and isinstance(part, SystemPromptPart): - permanent_instructions.append(part) - - if hasattr(msg, "instructions") and msg.instructions is not None: - current_instructions.append(msg.instructions) - - return permanent_instructions, current_instructions - - -def _set_input_messages( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" -) -> None: - """Set input messages data on a span.""" - if not _should_send_prompts(): - return - - if not messages: - return - - permanent_instructions, current_instructions = _get_system_instructions(messages) - if len(permanent_instructions) > 0 or len(current_instructions) > 0: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - else: - span.set_data( - SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps( - _transform_system_instructions( - permanent_instructions, current_instructions - ) - ), - ) - - try: - formatted_messages = [] - - for msg in messages: - if hasattr(msg, "parts"): - for part in msg.parts: - role = "user" - # Use isinstance checks with proper base classes - if SystemPromptPart and isinstance(part, SystemPromptPart): - continue - elif ( - (TextPart and isinstance(part, TextPart)) - or (ThinkingPart and isinstance(part, ThinkingPart)) - or (BaseToolCallPart and isinstance(part, BaseToolCallPart)) - ): - role = "assistant" - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - role = "tool" - - content: "List[Dict[str, Any] | str]" = [] - tool_calls = None - tool_call_id = None - - # Handle ToolCallPart (assistant requesting tool use) - if BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = {} - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - if tool_call_data: - tool_calls = [tool_call_data] - # Handle ToolReturnPart (tool result) - elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart): - if hasattr(part, "tool_name"): - tool_call_id = part.tool_name - if hasattr(part, "content"): - content.append({"type": "text", "text": str(part.content)}) - # Handle regular content - elif hasattr(part, "content"): - if isinstance(part.content, str): - content.append({"type": "text", "text": part.content}) - elif isinstance(part.content, list): - for item in part.content: - if isinstance(item, str): - content.append({"type": "text", "text": item}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - else: - content.append(safe_serialize(item)) - else: - content.append({"type": "text", "text": str(part.content)}) - # Add message if we have content or tool calls - if content or tool_calls: - message: "Dict[str, Any]" = {"role": role} - if content: - message["content"] = content - if tool_calls: - message["tool_calls"] = tool_calls - if tool_call_id: - message["tool_call_id"] = tool_call_id - formatted_messages.append(message) - - if formatted_messages: - normalized_messages = normalize_message_roles(formatted_messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - except Exception: - # If we fail to format messages, just skip it - pass - - -def _set_output_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" -) -> None: - """Set output data on a span.""" - if not _should_send_prompts(): - return - - if not response: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - - try: - # Extract text from ModelResponse - if hasattr(response, "parts"): - texts = [] - tool_calls = [] - - for part in response.parts: - if TextPart and isinstance(part, TextPart) and hasattr(part, "content"): - texts.append(part.content) - elif BaseToolCallPart and isinstance(part, BaseToolCallPart): - tool_call_data = { - "type": "function", - } - if hasattr(part, "tool_name"): - tool_call_data["name"] = part.tool_name - if hasattr(part, "args"): - tool_call_data["arguments"] = safe_serialize(part.args) - tool_calls.append(tool_call_data) - - if texts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts) - - if tool_calls: - set_on_span( - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls) - ) - - except Exception: - # If we fail to format output, just skip it - pass - - -def ai_client_span( - messages: "Any", agent: "Any", model: "Any", model_settings: "Any" -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for an AI client call (model request). - - Args: - messages: Full conversation history (list of messages) - agent: Agent object - model: Model object - model_settings: Model settings - """ - # Determine model name for span name - model_obj = model - if agent and hasattr(agent, "model"): - model_obj = agent.model - - model_name = _get_model_name(model_obj) or "unknown" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"chat {model_name}", - attributes={ - "sentry.op": OP.GEN_AI_CHAT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(), - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_CHAT, - name=f"chat {model_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - # Set streaming flag from contextvar - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - - # Add available tools if agent is available - agent_obj = agent or get_current_agent() - _set_available_tools(span, agent_obj) - - # Set input messages (full conversation history) - if messages: - _set_input_messages(span, messages) - - return span - - -def update_ai_client_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", model_response: "Any" -) -> None: - """Update the AI client span with response data.""" - if not span: - return - - # Set usage data if available - if model_response and hasattr(model_response, "usage"): - _set_usage_data(span, model_response.usage) - - # Set output data - _set_output_data(span, model_response) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py deleted file mode 100644 index 7648c1418a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import safe_serialize - -from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _should_send_prompts - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from pydantic_ai._tool_manager import ToolDefinition # type: ignore - - -def execute_tool_span( - tool_name: str, - tool_args: "Any", - agent: "Any", - tool_definition: "Optional[ToolDefinition]" = None, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for tool execution. - - Args: - tool_name: The name of the tool being executed - tool_args: The arguments passed to the tool - agent: The agent executing the tool - tool_definition: The definition of the tool, if available - """ - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"execute_tool {tool_name}", - attributes={ - "sentry.op": OP.GEN_AI_EXECUTE_TOOL, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: tool_name, - }, - ) - - set_on_span = span.set_attribute - else: - span = sentry_sdk.start_span( - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool") - span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name) - - set_on_span = span.set_data - - if tool_definition is not None and hasattr(tool_definition, "description"): - set_on_span( - SPANDATA.GEN_AI_TOOL_DESCRIPTION, - tool_definition.description, - ) - - _set_agent_data(span, agent) - - if _should_send_prompts() and tool_args is not None: - set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) - - return span - - -def update_execute_tool_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" -) -> None: - """Update the execute tool span with the result.""" - if not span: - return - - if not _should_send_prompts() or result is None: - return - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) - else: - span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result)) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py deleted file mode 100644 index f0c68e85ba..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ /dev/null @@ -1,184 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.ai.utils import ( - get_start_span_function, - normalize_message_roles, - set_data_normalized, - truncate_and_annotate_messages, -) -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing_utils import ( - has_span_streaming_enabled, - should_truncate_gen_ai_input, -) - -from ..consts import SPAN_ORIGIN -from ..utils import ( - _set_agent_data, - _set_available_tools, - _set_model_data, - _should_send_prompts, -) -from .utils import ( - _serialize_binary_content_item, - _serialize_image_url_item, -) - -if TYPE_CHECKING: - from typing import Any, Union - -try: - from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore -except ImportError: - BinaryContent = None - ImageUrl = None - - -def invoke_agent_span( - user_prompt: "Any", - agent: "Any", - model: "Any", - model_settings: "Any", - is_streaming: bool = False, -) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]": - """Create a span for invoking the agent.""" - # Determine agent name for span - name = "agent" - if agent and getattr(agent, "name", None): - name = agent.name - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - span = sentry_sdk.traces.start_span( - name=f"invoke_agent {name}", - attributes={ - "sentry.op": OP.GEN_AI_INVOKE_AGENT, - "sentry.origin": SPAN_ORIGIN, - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - }, - ) - else: - span = get_start_span_function()( - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {name}", - origin=SPAN_ORIGIN, - ) - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - - _set_agent_data(span, agent) - _set_model_data(span, model, model_settings) - _set_available_tools(span, agent) - - # Add user prompt and system prompts if available and prompts are enabled - if _should_send_prompts(): - messages = [] - - # Add system prompts (both instructions and system_prompt) - system_texts = [] - - if agent: - # Check for system_prompt - system_prompts = getattr(agent, "_system_prompts", None) or [] - for prompt in system_prompts: - if isinstance(prompt, str): - system_texts.append(prompt) - - # Check for instructions (stored in _instructions) - instructions = getattr(agent, "_instructions", None) - if instructions: - if isinstance(instructions, str): - system_texts.append(instructions) - elif isinstance(instructions, (list, tuple)): - for instr in instructions: - if isinstance(instr, str): - system_texts.append(instr) - elif callable(instr): - # Skip dynamic/callable instructions - pass - - # Add all system texts as system messages - for system_text in system_texts: - messages.append( - { - "content": [{"text": system_text, "type": "text"}], - "role": "system", - } - ) - - # Add user prompt - if user_prompt: - if isinstance(user_prompt, str): - messages.append( - { - "content": [{"text": user_prompt, "type": "text"}], - "role": "user", - } - ) - elif isinstance(user_prompt, list): - # Handle list of user content - content = [] - for item in user_prompt: - if isinstance(item, str): - content.append({"text": item, "type": "text"}) - elif ImageUrl and isinstance(item, ImageUrl): - content.append(_serialize_image_url_item(item)) - elif BinaryContent and isinstance(item, BinaryContent): - content.append(_serialize_binary_content_item(item)) - if content: - messages.append( - { - "content": content, - "role": "user", - } - ) - - if messages: - normalized_messages = normalize_message_roles(messages) - client = sentry_sdk.get_client() - scope = sentry_sdk.get_current_scope() - messages_data = ( - truncate_and_annotate_messages(normalized_messages, span, scope) - if should_truncate_gen_ai_input(client.options) - else normalized_messages - ) - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False - ) - - return span - - -def update_invoke_agent_span( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - result: "Any", -) -> None: - """Update and close the invoke agent span.""" - if not span or not result: - return - - # Extract output from result - output = getattr(result, "output", None) - - # Set response text if prompts are enabled - if _should_send_prompts() and output: - set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False - ) - - # Set model name from response if available - if hasattr(result, "response"): - try: - response = result.response - if hasattr(response, "model_name") and response.model_name: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name - ) - else: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name) - except Exception: - # If response access fails, continue without setting model name - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py deleted file mode 100644 index 330496c6b2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/spans/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Utility functions for PydanticAI span instrumentation.""" - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import BLOB_DATA_SUBSTITUTE -from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX -from sentry_sdk.ai.utils import get_modality_from_mime_type -from sentry_sdk.consts import SPANDATA -from sentry_sdk.traces import StreamedSpan - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore - - -def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]": - """Serialize an ImageUrl content item for span data. - - For data URLs containing base64-encoded images, the content is redacted. - For regular HTTP URLs, the URL string is preserved. - """ - url = str(item.url) - data_url_match = DATA_URL_BASE64_REGEX.match(url) - - if data_url_match: - return { - "type": "image", - "content": BLOB_DATA_SUBSTITUTE, - } - - return { - "type": "image", - "content": url, - } - - -def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]": - """Serialize a BinaryContent item for span data, redacting the blob data.""" - return { - "type": "blob", - "modality": get_modality_from_mime_type(item.media_type), - "mime_type": item.media_type, - "content": BLOB_DATA_SUBSTITUTE, - } - - -def _set_usage_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - usage: "Union[RequestUsage, RunUsage]", -) -> None: - """Set token usage data on a span. - - This function works with both RequestUsage (single request) and - RunUsage (agent run) objects from pydantic_ai. - - Args: - span: The Sentry span to set data on. - usage: RequestUsage or RunUsage object containing token usage information. - """ - if usage is None: - return - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if hasattr(usage, "input_tokens") and usage.input_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens) - - # Pydantic AI uses cache_read_tokens (not input_tokens_cached) - if hasattr(usage, "cache_read_tokens") and usage.cache_read_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, usage.cache_read_tokens) - - # Pydantic AI uses cache_write_tokens (not input_tokens_cache_write) - if hasattr(usage, "cache_write_tokens") and usage.cache_write_tokens is not None: - set_on_span( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE, - usage.cache_write_tokens, - ) - - if hasattr(usage, "output_tokens") and usage.output_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens) - - if hasattr(usage, "total_tokens") and usage.total_tokens is not None: - set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py deleted file mode 100644 index 340dcf8953..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pydantic_ai/utils.py +++ /dev/null @@ -1,233 +0,0 @@ -from contextvars import ContextVar -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import event_from_exception, safe_serialize - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - -# Store the current agent context in a contextvar for re-entrant safety -# Using a list as a stack to support nested agent calls -_agent_context_stack: "ContextVar[list[dict[str, Any]]]" = ContextVar( - "pydantic_ai_agent_context_stack", default=[] -) - - -def push_agent(agent: "Any", is_streaming: bool = False) -> None: - """Push an agent context onto the stack along with its streaming flag.""" - stack = _agent_context_stack.get().copy() - stack.append({"agent": agent, "is_streaming": is_streaming}) - _agent_context_stack.set(stack) - - -def pop_agent() -> None: - """Pop an agent context from the stack.""" - stack = _agent_context_stack.get().copy() - if stack: - stack.pop() - _agent_context_stack.set(stack) - - -def get_current_agent() -> "Any": - """Get the current agent from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1]["agent"] - return None - - -def get_is_streaming() -> bool: - """Get the streaming flag from the contextvar stack.""" - stack = _agent_context_stack.get() - if stack: - return stack[-1].get("is_streaming", False) - return False - - -def _should_send_prompts() -> bool: - """ - Check if prompts should be sent to Sentry. - - This checks both send_default_pii and the include_prompts integration setting. - """ - if not should_send_default_pii(): - return False - - from . import PydanticAIIntegration - - # Get the integration instance from the client - integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) - - if integration is None: - return False - - return getattr(integration, "include_prompts", False) - - -def _set_agent_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set agent-related data on a span. - - Args: - span: The span to set data on - agent: Agent object (can be None, will try to get from contextvar if not provided) - """ - # Extract agent name from agent object or contextvar - agent_obj = agent - if not agent_obj: - # Try to get from contextvar - agent_obj = get_current_agent() - - if agent_obj and hasattr(agent_obj, "name") and agent_obj.name: - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - else: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name) - - -def _get_model_name(model_obj: "Any") -> "Optional[str]": - """Extract model name from a model object. - - Args: - model_obj: Model object to extract name from - - Returns: - Model name string or None if not found - """ - if not model_obj: - return None - - if hasattr(model_obj, "model_name"): - return model_obj.model_name - elif hasattr(model_obj, "name"): - try: - return model_obj.name() - except Exception: - return str(model_obj) - elif isinstance(model_obj, str): - return model_obj - else: - return str(model_obj) - - -def _set_model_data( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", - model: "Any", - model_settings: "Any", -) -> None: - """Set model-related data on a span. - - Args: - span: The span to set data on - model: Model object (can be None, will try to get from agent if not provided) - model_settings: Model settings (can be None, will try to get from agent if not provided) - """ - # Try to get agent from contextvar if we need it - agent_obj = get_current_agent() - - # Extract model information - model_obj = model - if not model_obj and agent_obj and hasattr(agent_obj, "model"): - model_obj = agent_obj.model - - set_on_span = ( - span.set_attribute if isinstance(span, StreamedSpan) else span.set_data - ) - - if model_obj: - # Set system from model - if hasattr(model_obj, "system"): - set_on_span(SPANDATA.GEN_AI_SYSTEM, model_obj.system) - - # Set model name - model_name = _get_model_name(model_obj) - if model_name: - set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model_name) - - # Extract model settings - settings = model_settings - if not settings and agent_obj and hasattr(agent_obj, "model_settings"): - settings = agent_obj.model_settings - - if settings: - settings_map = { - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - } - - # ModelSettings is a TypedDict (dict at runtime), so use dict access - if isinstance(settings, dict): - for setting_name, spandata_key in settings_map.items(): - value = settings.get(setting_name) - if value is not None: - set_on_span(spandata_key, value) - else: - # Fallback for object-style settings - for setting_name, spandata_key in settings_map.items(): - if hasattr(settings, setting_name): - value = getattr(settings, setting_name) - if value is not None: - set_on_span(spandata_key, value) - - -def _set_available_tools( - span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", agent: "Any" -) -> None: - """Set available tools data on a span from an agent's function toolset. - - Args: - span: The span to set data on - agent: Agent object with _function_toolset attribute - """ - if not agent or not hasattr(agent, "_function_toolset"): - return - - try: - tools = [] - # Get tools from the function toolset - if hasattr(agent._function_toolset, "tools"): - for tool_name, tool in agent._function_toolset.tools.items(): - tool_info = {"name": tool_name} - - # Add description from function_schema if available - if hasattr(tool, "function_schema"): - schema = tool.function_schema - if getattr(schema, "description", None): - tool_info["description"] = schema.description - - # Add parameters from json_schema - if getattr(schema, "json_schema", None): - tool_info["parameters"] = schema.json_schema - - tools.append(tool_info) - - if tools: - if isinstance(span, StreamedSpan): - span.set_attribute( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - else: - span.set_data( - SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) - ) - except Exception: - # If we can't extract tools, just skip it - pass - - -def _capture_exception(exc: "Any", handled: bool = False) -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pydantic_ai", "handled": handled}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py deleted file mode 100644 index 2616f4d5a3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pymongo.py +++ /dev/null @@ -1,262 +0,0 @@ -import copy -import json - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -try: - from pymongo import monitoring -except ImportError: - raise DidNotEnable("Pymongo not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Union - - from pymongo.monitoring import ( - CommandFailedEvent, - CommandStartedEvent, - CommandSucceededEvent, - ) - - -SAFE_COMMAND_ATTRIBUTES = [ - "insert", - "ordered", - "find", - "limit", - "singleBatch", - "aggregate", - "createIndexes", - "indexes", - "delete", - "findAndModify", - "renameCollection", - "to", - "drop", -] - - -def _strip_pii(command: "Dict[str, Any]") -> "Dict[str, Any]": - for key in command: - is_safe_field = key in SAFE_COMMAND_ATTRIBUTES - if is_safe_field: - # Skip if safe key - continue - - update_db_command = key == "update" and "findAndModify" not in command - if update_db_command: - # Also skip "update" db command because it is save. - # There is also an "update" key in the "findAndModify" command, which is NOT safe! - continue - - # Special stripping for documents - is_document = key == "documents" - if is_document: - for doc in command[key]: - for doc_key in doc: - doc[doc_key] = "%s" - continue - - # Special stripping for dict style fields - is_dict_field = key in ["filter", "query", "update"] - if is_dict_field: - for item_key in command[key]: - command[key][item_key] = "%s" - continue - - # For pipeline fields strip the `$match` dict - is_pipeline_field = key == "pipeline" - if is_pipeline_field: - for pipeline in command[key]: - for match_key in pipeline["$match"] if "$match" in pipeline else []: - pipeline["$match"][match_key] = "%s" - continue - - # Default stripping - command[key] = "%s" - - return command - - -def _get_db_data(event: "Any") -> "Dict[str, Any]": - data = {} - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - data[SPANDATA.DB_DRIVER_NAME] = "pymongo" - db_name = event.database_name - - server_address = event.connection_id[0] - if server_address is not None: - data[SPANDATA.SERVER_ADDRESS] = server_address - - server_port = event.connection_id[1] - if server_port is not None: - data[SPANDATA.SERVER_PORT] = server_port - - if is_span_streaming_enabled: - data["db.system.name"] = "mongodb" - - if db_name is not None: - data["db.namespace"] = db_name - else: - data[SPANDATA.DB_SYSTEM] = "mongodb" - - if db_name is not None: - data[SPANDATA.DB_NAME] = db_name - - return data - - -class CommandTracer(monitoring.CommandListener): - def __init__(self) -> None: - self._ongoing_operations: "Dict[int, Union[Span, StreamedSpan]]" = {} - - def _operation_key( - self, - event: "Union[CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent]", - ) -> int: - return event.request_id - - def started(self, event: "CommandStartedEvent") -> None: - client = sentry_sdk.get_client() - if client.get_integration(PyMongoIntegration) is None: - return - - with capture_internal_exceptions(): - command = dict(copy.deepcopy(event.command)) - - command.pop("$db", None) - command.pop("$clusterTime", None) - command.pop("$signature", None) - - db_data = _get_db_data(event) - - collection_name = command.get(event.command_name) - operation_name = event.command_name - db_name = event.database_name - - lsid = command.pop("lsid", None) - if not should_send_default_pii(): - command = _strip_pii(command) - - query = json.dumps(command, default=str) - - if has_span_streaming_enabled(client.options): - span_first_data = { - "db.operation.name": operation_name, - "db.collection.name": collection_name, - SPANDATA.DB_QUERY_TEXT: query, - "sentry.op": OP.DB, - "sentry.origin": PyMongoIntegration.origin, - **db_data, - } - - span = sentry_sdk.traces.start_span( - name=query, attributes=span_first_data - ) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, - category="query", - type=OP.DB, - data=span_first_data, - ) - - else: - tags = { - "db.name": db_name, - SPANDATA.DB_SYSTEM: "mongodb", - SPANDATA.DB_DRIVER_NAME: "pymongo", - SPANDATA.DB_OPERATION: operation_name, - # The below is a deprecated field, but leaving for legacy reasons. - # The v2 spans will use `db.collection.name` instead. - SPANDATA.DB_MONGODB_COLLECTION: collection_name, - } - - try: - tags["net.peer.name"] = event.connection_id[0] - tags["net.peer.port"] = str(event.connection_id[1]) - except TypeError: - pass - - data: "Dict[str, Any]" = {"operation_ids": {}} - data["operation_ids"]["operation"] = event.operation_id - data["operation_ids"]["request"] = event.request_id - - data.update(db_data) - - try: - if lsid: - lsid_id = lsid["id"] - data["operation_ids"]["session"] = str(lsid_id) - except KeyError: - pass - - span = sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=PyMongoIntegration.origin, - ) - - for tag, value in tags.items(): - # set the tag for backwards-compatibility. - # TODO: remove the set_tag call in the next major release! - span.set_tag(tag, value) - span.set_data(tag, value) - - for key, value in data.items(): - span.set_data(key, value) - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb( - message=query, category="query", type=OP.DB, data=tags - ) - - self._ongoing_operations[self._operation_key(event)] = span.__enter__() - - def failed(self, event: "CommandFailedEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - # Ignoring NoOpStreamedSpan as it will always have a status of "ok" - if type(span) is StreamedSpan: - span.status = SpanStatus.ERROR - elif type(span) is Span: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - span.__exit__(None, None, None) - except KeyError: - return - - def succeeded(self, event: "CommandSucceededEvent") -> None: - if sentry_sdk.get_client().get_integration(PyMongoIntegration) is None: - return - - try: - span = self._ongoing_operations.pop(self._operation_key(event)) - if type(span) is Span: - span.set_status(SPANSTATUS.OK) - span.__exit__(None, None, None) - except KeyError: - pass - - -class PyMongoIntegration(Integration): - identifier = "pymongo" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - monitoring.register(CommandTracer()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py deleted file mode 100644 index 6837d8345c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyramid.py +++ /dev/null @@ -1,239 +0,0 @@ -import functools -import os -import sys -import weakref - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - reraise, -) - -try: - from pyramid.httpexceptions import HTTPException - from pyramid.request import Request -except ImportError: - raise DidNotEnable("Pyramid not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional - - from pyramid.response import Response - from webob.cookies import RequestCookies - from webob.request import _FieldStorageWithFile - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.integrations.wsgi import _ScopedResponse - from sentry_sdk.utils import ExcInfo - - -if getattr(Request, "authenticated_userid", None): - - def authenticated_userid(request: "Request") -> "Optional[Any]": - return request.authenticated_userid - -else: - # bw-compat for pyramid < 1.5 - from pyramid.security import authenticated_userid # type: ignore - - -TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") - - -class PyramidIntegration(Integration): - identifier = "pyramid" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "route_name") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - from pyramid import router - - old_call_view = router._call_view - - @functools.wraps(old_call_view) - def sentry_patched_call_view( - registry: "Any", request: "Request", *args: "Any", **kwargs: "Any" - ) -> "Response": - client = sentry_sdk.get_client() - integration = client.get_integration(PyramidIntegration) - if integration is None: - return old_call_view(registry, request, *args, **kwargs) - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - - scope = sentry_sdk.get_isolation_scope() - - if should_send_default_pii() and has_span_streaming_enabled(client.options): - user_id = authenticated_userid(request) - if user_id: - scope.set_user({"id": user_id}) - - scope.add_event_processor( - _make_event_processor(weakref.ref(request), integration) - ) - - return old_call_view(registry, request, *args, **kwargs) - - router._call_view = sentry_patched_call_view - - if hasattr(Request, "invoke_exception_view"): - old_invoke_exception_view = Request.invoke_exception_view - - def sentry_patched_invoke_exception_view( - self: "Request", *args: "Any", **kwargs: "Any" - ) -> "Any": - rv = old_invoke_exception_view(self, *args, **kwargs) - - if ( - self.exc_info - and all(self.exc_info) - and rv.status_int == 500 - and sentry_sdk.get_client().get_integration(PyramidIntegration) - is not None - ): - _capture_exception(self.exc_info) - - return rv - - Request.invoke_exception_view = sentry_patched_invoke_exception_view - - old_wsgi_call = router.Router.__call__ - - @ensure_integration_enabled(PyramidIntegration, old_wsgi_call) - def sentry_patched_wsgi_call( - self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "_ScopedResponse": - def sentry_patched_inner_wsgi_call( - environ: "Dict[str, Any]", start_response: "Callable[..., Any]" - ) -> "Any": - try: - return old_wsgi_call(self, environ, start_response) - except Exception: - einfo = sys.exc_info() - _capture_exception(einfo) - reraise(*einfo) - - middleware = SentryWsgiMiddleware( - sentry_patched_inner_wsgi_call, - span_origin=PyramidIntegration.origin, - ) - return middleware(environ, start_response) - - router.Router.__call__ = sentry_patched_wsgi_call - - -@ensure_integration_enabled(PyramidIntegration) -def _capture_exception(exc_info: "ExcInfo") -> None: - if exc_info[0] is None or issubclass(exc_info[0], HTTPException): - return - - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "pyramid", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "route_name": request.matched_route.name, - "route_pattern": request.matched_route.pattern, - } - is_span_streaming_enabled = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if is_span_streaming_enabled - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - scope.set_transaction_name( - name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -class PyramidRequestExtractor(RequestExtractor): - def url(self) -> str: - return self.request.path_url - - def env(self) -> "Dict[str, str]": - return self.request.environ - - def cookies(self) -> "RequestCookies": - return self.request.cookies - - def raw_data(self) -> str: - return self.request.text - - def form(self) -> "Dict[str, str]": - return { - key: value - for key, value in self.request.POST.items() - if not getattr(value, "filename", None) - } - - def files(self) -> "Dict[str, _FieldStorageWithFile]": - return { - key: value - for key, value in self.request.POST.items() - if getattr(value, "filename", None) - } - - def size_of_file(self, postdata: "_FieldStorageWithFile") -> int: - file = postdata.file - try: - return os.fstat(file.fileno()).st_size - except Exception: - return 0 - - -def _make_event_processor( - weak_request: "Callable[[], Request]", integration: "PyramidIntegration" -) -> "EventProcessor": - def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - PyramidRequestExtractor(request).extract_into_event(event) - - if should_send_default_pii(): - with capture_internal_exceptions(): - user_info = event.setdefault("user", {}) - user_info.setdefault("id", authenticated_userid(request)) - - return event - - return pyramid_event_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py deleted file mode 100644 index aae68c4c10..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/pyreqwest.py +++ /dev/null @@ -1,197 +0,0 @@ -from contextlib import contextmanager -from typing import Any, Generator - -import sentry_sdk -from sentry_sdk import start_span -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import ( - add_http_request_source, - add_sentry_baggage_to_headers, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - logger, - parse_url, -) - -try: - from pyreqwest.client import ( # type: ignore[import-not-found] - ClientBuilder, - SyncClientBuilder, - ) - from pyreqwest.middleware import Next, SyncNext # type: ignore[import-not-found] - from pyreqwest.request import ( # type: ignore[import-not-found] - OneOffRequestBuilder, - Request, - SyncOneOffRequestBuilder, - ) - from pyreqwest.response import ( # type: ignore[import-not-found] - Response, - SyncResponse, - ) -except ImportError: - raise DidNotEnable("pyreqwest not installed or incompatible version installed") - - -class PyreqwestIntegration(Integration): - identifier = "pyreqwest" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _patch_pyreqwest() - - -def _patch_pyreqwest() -> None: - # Patch Client Builders - _patch_builder_method(ClientBuilder, "build", sentry_async_middleware) - _patch_builder_method(SyncClientBuilder, "build", sentry_sync_middleware) - - # Patch Request Builders - _patch_builder_method(OneOffRequestBuilder, "send", sentry_async_middleware) - _patch_builder_method(SyncOneOffRequestBuilder, "send", sentry_sync_middleware) - - -def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> None: - if not hasattr(cls, method_name): - return - - original_method = getattr(cls, method_name) - - def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - if not getattr(self, "_sentry_instrumented", False): - integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) - if integration is not None: - self.with_middleware(middleware) - try: - self._sentry_instrumented = True - except (TypeError, AttributeError): - # In case the instance itself is immutable or doesn't allow extra attributes - pass - return original_method(self, *args, **kwargs) - - setattr(cls, method_name, sentry_patched_method) - - -@contextmanager -def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(str(request.url), sanitize=False) - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": PyreqwestIntegration.origin, - SPANDATA.HTTP_REQUEST_METHOD: request.method, - }, - ) as span: - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - return - - with start_span( - op=OP.HTTP_CLIENT, - name=f"{request.method} {parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE}", - origin=PyreqwestIntegration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - - if should_propagate_trace(sentry_sdk.get_client(), str(request.url)): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format( - key=key, value=value, url=request.url - ) - ) - - if key == BAGGAGE_HEADER_NAME: - add_sentry_baggage_to_headers(request.headers, value) - else: - request.headers[key] = value - - yield span - - with capture_internal_exceptions(): - add_http_request_source(span) - - -async def sentry_async_middleware( - request: "Request", next_handler: "Next" -) -> "Response": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return await next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = await next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response - - -def sentry_sync_middleware( - request: "Request", next_handler: "SyncNext" -) -> "SyncResponse": - if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: - return next_handler.run(request) - - with _sentry_pyreqwest_span(request) as span: - response = next_handler.run(request) - if isinstance(span, StreamedSpan): - span.status = "error" if response.status >= 400 else "ok" - span.set_attribute( - SPANDATA.HTTP_STATUS_CODE, - response.status, - ) - else: - span.set_http_status(response.status) - - return response diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py deleted file mode 100644 index 6a5603d825..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/quart.py +++ /dev/null @@ -1,292 +0,0 @@ -import asyncio -import inspect -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Union - - from sentry_sdk._types import Event, EventProcessor - -try: - import quart_auth # type: ignore -except ImportError: - quart_auth = None - -try: - from quart import ( # type: ignore - Quart, - Request, - has_request_context, - has_websocket_context, - request, - websocket, - ) - from quart.signals import ( # type: ignore - got_background_exception, - got_request_exception, - got_websocket_exception, - request_started, - websocket_started, - ) -except ImportError: - raise DidNotEnable("Quart is not installed") -else: - # Quart 0.19 is based on Flask and hence no longer has a Scaffold - try: - from quart.scaffold import Scaffold # type: ignore - except ImportError: - from flask.sansio.scaffold import Scaffold # type: ignore - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class QuartIntegration(Integration): - identifier = "quart" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__(self, transaction_style: str = "endpoint") -> None: - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - - @staticmethod - def setup_once() -> None: - request_started.connect(_request_websocket_started) - websocket_started.connect(_request_websocket_started) - got_background_exception.connect(_capture_exception) - got_request_exception.connect(_capture_exception) - got_websocket_exception.connect(_capture_exception) - - patch_asgi_app() - patch_scaffold_route() - - -def patch_asgi_app() -> None: - old_app = Quart.__call__ - - async def sentry_patched_asgi_app( - self: "Any", scope: "Any", receive: "Any", send: "Any" - ) -> "Any": - if sentry_sdk.get_client().get_integration(QuartIntegration) is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - span_origin=QuartIntegration.origin, - asgi_version=3, - ) - return await middleware(scope, receive, send) - - Quart.__call__ = sentry_patched_asgi_app - - -def patch_scaffold_route() -> None: - # Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140 - if sys.version_info >= (3, 12): - iscoroutinefunction = inspect.iscoroutinefunction - else: - iscoroutinefunction = asyncio.iscoroutinefunction - - old_route = Scaffold.route - - def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any": - old_decorator = old_route(*args, **kwargs) - - def decorator(old_func: "Any") -> "Any": - if inspect.isfunction(old_func) and not iscoroutinefunction(old_func): - - @wraps(old_func) - @ensure_integration_enabled(QuartIntegration, old_func) - def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - span = get_current_span() - if span is not None and hasattr(span, "_segment"): - span._segment._update_active_thread() - else: - current_scope = sentry_sdk.get_current_scope() - if current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_func(*args, **kwargs) - - return old_decorator(_sentry_func) - - return old_decorator(old_func) - - return decorator - - Scaffold.route = _sentry_route - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Request" -) -> None: - try: - name_for_style = { - "url": request.url_rule.rule, - "endpoint": request.url_rule.endpoint, - } - - source = ( - SEGMENT_SOURCE_FOR_STYLE[transaction_style] - if has_span_streaming_enabled(sentry_sdk.get_client().options) - else TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - ) - - scope.set_transaction_name( - name=name_for_style[transaction_style], - source=source, - ) - except Exception: - pass - - -async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - if has_request_context(): - request_websocket = request._get_current_object() - if has_websocket_context(): - request_websocket = websocket._get_current_object() - - # Set the transaction name here, but rely on ASGI middleware - # to actually start the transaction - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request_websocket - ) - - scope = sentry_sdk.get_isolation_scope() - - if has_span_streaming_enabled(sentry_sdk.get_client().options): - current_span = get_current_span() - if type(current_span) is StreamedSpan: - segment = current_span._segment - - segment.set_attribute("http.request.method", request_websocket.method) - header_attributes: "dict[str, Any]" = {} - - for header, header_value in _filter_headers( - dict(request_websocket.headers), use_annotated_value=False - ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( - header_value - ) - - segment.set_attributes(header_attributes) - - if should_send_default_pii(): - segment.set_attribute("url.full", request_websocket.url) - segment.set_attribute( - "url.query", - request_websocket.query_string.decode("utf-8", errors="replace"), - ) - - user_properties = {} - if len(request_websocket.access_route) >= 1: - segment.set_attribute( - "client.address", request_websocket.access_route[0] - ) - user_properties["ip_address"] = request_websocket.access_route[0] - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_properties["id"] = current_user_id - - if user_properties: - existing_user_properties = scope._user or {} - scope.set_user({**existing_user_properties, **user_properties}) - - evt_processor = _make_request_event_processor(app, request_websocket, integration) - scope.add_event_processor(evt_processor) - - -def _make_request_event_processor( - app: "Quart", request: "Request", integration: "QuartIntegration" -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - # if the request is gone we are fine not logging the data from - # it. This might happen if the processor is pushed away to - # another thread. - if request is None: - return event - - with capture_internal_exceptions(): - # TODO: Figure out what to do with request body. Methods on request - # are async, but event processors are not. - - request_info = event.setdefault("request", {}) - request_info["url"] = request.url - request_info["query_string"] = request.query_string - request_info["method"] = request.method - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - if len(request.access_route) >= 1: - request_info["env"] = {"REMOTE_ADDR": request.access_route[0]} - - current_user_id = _get_current_user_id_from_quart() - if current_user_id: - user_info = event.setdefault("user", {}) - user_info["id"] = current_user_id - - return event - - return inner - - -async def _capture_exception( - sender: "Quart", exception: "Union[ValueError, BaseException]", **kwargs: "Any" -) -> None: - integration = sentry_sdk.get_client().get_integration(QuartIntegration) - if integration is None: - return - - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "quart", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_current_user_id_from_quart() -> "str | None": - if quart_auth is None: - return None - - if quart_auth.current_user is None: - return None - - try: - return quart_auth.current_user._auth_id - except Exception: - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py deleted file mode 100644 index f723a96f3c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/ray.py +++ /dev/null @@ -1,240 +0,0 @@ -import functools -import inspect -import sys - -import sentry_sdk -from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - event_from_exception, - logger, - package_version, - qualname_from_function, - reraise, -) - -try: - import ray # type: ignore[import-not-found] - from ray import remote -except ImportError: - raise DidNotEnable("Ray not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional - - from sentry_sdk.utils import ExcInfo - - -def _check_sentry_initialized() -> None: - if sentry_sdk.get_client().is_active(): - return - - logger.debug( - "[Tracing] Sentry not initialized in ray cluster worker, performance data will be discarded." - ) - - -def _insert_sentry_tracing_in_signature(func: "Callable[..., Any]") -> None: - # Patching new_func signature to add the _sentry_tracing parameter to it - # Ray later inspects the signature and finds the unexpected parameter otherwise - signature = inspect.signature(func) - params = list(signature.parameters.values()) - sentry_tracing_param = inspect.Parameter( - "_sentry_tracing", - kind=inspect.Parameter.KEYWORD_ONLY, - default=None, - ) - - # Keyword only arguments are penultimate if function has variadic keyword arguments - if params and params[-1].kind is inspect.Parameter.VAR_KEYWORD: - params.insert(-1, sentry_tracing_param) - else: - params.append(sentry_tracing_param) - - func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined] - - -def _patch_ray_remote() -> None: - old_remote = remote - - @functools.wraps(old_remote) - def new_remote( - f: "Optional[Callable[..., Any]]" = None, *args: "Any", **kwargs: "Any" - ) -> "Callable[..., Any]": - if inspect.isclass(f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(f, *args, **kwargs) - - def wrapper(user_f: "Callable[..., Any]") -> "Any": - if inspect.isclass(user_f): - # Ray Actors - # (https://docs.ray.io/en/latest/ray-core/actors.html) - # are not supported - # (Only Ray Tasks are supported) - return old_remote(*args, **kwargs)(user_f) - - @functools.wraps(user_f) - def new_func( - *f_args: "Any", - _sentry_tracing: "Optional[dict[str, Any]]" = None, - **f_kwargs: "Any", - ) -> "Any": - _check_sentry_initialized() - - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - sentry_sdk.traces.continue_trace(_sentry_tracing or {}) - - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RAY, - "sentry.origin": RayIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - }, - parent_span=None, - ): - try: - result = user_f(*f_args, **f_kwargs) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - transaction = sentry_sdk.continue_trace( - _sentry_tracing or {}, - op=OP.QUEUE_TASK_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - source=TransactionSource.TASK, - ) - - with sentry_sdk.start_transaction(transaction) as transaction: - try: - result = user_f(*f_args, **f_kwargs) - transaction.set_status(SPANSTATUS.OK) - except Exception: - transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - _insert_sentry_tracing_in_signature(new_func) - - if f: - rv = old_remote(new_func) - else: - rv = old_remote(*args, **kwargs)(new_func) - old_remote_method = rv.remote - - def _remote_method_with_header_propagation( - *args: "Any", **kwargs: "Any" - ) -> "Any": - """ - Ray Client - """ - span_streaming = has_span_streaming_enabled( - sentry_sdk.get_client().options - ) - if span_streaming: - function_name = qualname_from_function(user_f) - with sentry_sdk.traces.start_span( - name="unknown Ray task" - if function_name is None - else function_name, - attributes={ - "sentry.op": OP.QUEUE_SUBMIT_RAY, - "sentry.origin": RayIntegration.origin, - }, - ): - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - except Exception: - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - else: - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_RAY, - name=qualname_from_function(user_f), - origin=RayIntegration.origin, - ) as span: - tracing = { - k: v - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers() - } - try: - result = old_remote_method( - *args, **kwargs, _sentry_tracing=tracing - ) - span.set_status(SPANSTATUS.OK) - except Exception: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - - return result - - rv.remote = _remote_method_with_header_propagation - - return rv - - if f is not None: - return wrapper(f) - else: - return wrapper - - ray.remote = new_remote - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={ - "handled": False, - "type": RayIntegration.identifier, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - -class RayIntegration(Integration): - identifier = "ray" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = package_version("ray") - _check_minimum_version(RayIntegration, version) - - _patch_ray_remote() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py deleted file mode 100644 index 7095721ed2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE -from sentry_sdk.integrations.redis.rb import _patch_rb -from sentry_sdk.integrations.redis.redis import _patch_redis -from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster -from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -class RedisIntegration(Integration): - identifier = "redis" - - def __init__( - self, - max_data_size: "Optional[int]" = _DEFAULT_MAX_DATA_SIZE, - cache_prefixes: "Optional[list[str]]" = None, - ) -> None: - self.max_data_size = max_data_size - self.cache_prefixes = cache_prefixes if cache_prefixes is not None else [] - - if max_data_size is not None: - warnings.warn( - "The `max_data_size` parameter of `RedisIntegration` is " - "deprecated and will be removed in version 3.0 of sentry-sdk.", - DeprecationWarning, - stacklevel=2, - ) - - @staticmethod - def setup_once() -> None: - try: - from redis import StrictRedis, client - except ImportError: - raise DidNotEnable("Redis client not installed") - - _patch_redis(StrictRedis, client) - _patch_redis_cluster() - _patch_rb() - - try: - _patch_rediscluster() - except Exception: - logger.exception("Error occurred while patching `rediscluster` library") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py deleted file mode 100644 index 8fc3d0c3a9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_async_common.py +++ /dev/null @@ -1,177 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from redis.asyncio.client import Pipeline, StrictRedis - from redis.asyncio.cluster import ClusterPipeline, RedisCluster - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_async_pipeline( - pipeline_cls: "Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]]", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return await old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - try: - command_seq = self._execution_strategy._command_queue - except AttributeError: - if is_cluster: - command_seq = self._command_stack - else: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.is_transaction, - command_seq, - ) - - return await old_execute(self, *args, **kwargs) - - pipeline_cls.execute = _sentry_execute # type: ignore - - -def patch_redis_async_client( - cls: "Union[type[StrictRedis[Any]], type[RedisCluster[Any]]]", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - async def _sentry_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return await old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = await old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = _sentry_execute_command # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py deleted file mode 100644 index 58d686b099..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/_sync_common.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN -from sentry_sdk.integrations.redis.modules.caches import ( - _compile_cache_span_properties, - _set_cache_data, -) -from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties -from sentry_sdk.integrations.redis.utils import ( - _get_safe_command, - _set_client_data, - _set_pipeline_data, -) -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Optional, Union - - from sentry_sdk.traces import StreamedSpan - - -def patch_redis_pipeline( - pipeline_cls: "Any", - is_cluster: bool, - get_command_args_fn: "Any", - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - old_execute = pipeline_cls.execute - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RedisIntegration) is None: - return old_execute(self, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="redis.pipeline.execute", - attributes={ - "sentry.origin": SPAN_ORIGIN, - "sentry.op": OP.DB_REDIS, - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.DB_REDIS, - name="redis.pipeline.execute", - origin=SPAN_ORIGIN, - ) - - with span: - with capture_internal_exceptions(): - command_seq = None - try: - command_seq = self._execution_strategy.command_queue - except AttributeError: - command_seq = self.command_stack - - set_db_data_fn(span, self) - _set_pipeline_data( - span, - is_cluster, - get_command_args_fn, - False if is_cluster else self.transaction, - command_seq, - ) - - return old_execute(self, *args, **kwargs) - - pipeline_cls.execute = sentry_patched_execute - - -def patch_redis_client( - cls: "Any", - is_cluster: bool, - set_db_data_fn: "Callable[[Union[Span, StreamedSpan], Any], None]", -) -> None: - """ - This function can be used to instrument custom redis client classes or - subclasses. - """ - old_execute_command = cls.execute_command - - from sentry_sdk.integrations.redis import RedisIntegration - - def sentry_patched_execute_command( - self: "Any", name: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(RedisIntegration) - if integration is None: - return old_execute_command(self, name, *args, **kwargs) - - span_streaming = has_span_streaming_enabled(client.options) - - cache_properties = _compile_cache_span_properties( - name, - args, - kwargs, - integration, - ) - - additional_cache_span_attributes = {} - with capture_internal_exceptions(): - additional_cache_span_attributes[SPANDATA.DB_QUERY_TEXT] = ( - _get_safe_command(name, args) - ) - - cache_span: "Optional[Union[Span, StreamedSpan]]" = None - if cache_properties["is_cache_key"] and cache_properties["op"] is not None: - if span_streaming: - cache_span = sentry_sdk.traces.start_span( - name=cache_properties["description"], - attributes={ - "sentry.op": cache_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_cache_span_attributes, - }, - ) - else: - cache_span = sentry_sdk.start_span( - op=cache_properties["op"], - name=cache_properties["description"], - origin=SPAN_ORIGIN, - ) - cache_span.__enter__() - - db_properties = _compile_db_span_properties(integration, name, args) - - additional_db_span_attributes = {} - with capture_internal_exceptions(): - additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command( - name, args - ) - - db_span: "Union[Span, StreamedSpan]" - if span_streaming: - db_span = sentry_sdk.traces.start_span( - name=db_properties["description"], - attributes={ - "sentry.op": db_properties["op"], - "sentry.origin": SPAN_ORIGIN, - **additional_db_span_attributes, - }, - ) - else: - db_span = sentry_sdk.start_span( - op=db_properties["op"], - name=db_properties["description"], - origin=SPAN_ORIGIN, - ) - db_span.__enter__() - - set_db_data_fn(db_span, self) - _set_client_data(db_span, is_cluster, name, *args) - - value = old_execute_command(self, name, *args, **kwargs) - - db_span.__exit__(None, None, None) - - if cache_span: - _set_cache_data(cache_span, self, cache_properties, value) - cache_span.__exit__(None, None, None) - - return value - - cls.execute_command = sentry_patched_execute_command diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py deleted file mode 100644 index 0822c2c930..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/consts.py +++ /dev/null @@ -1,19 +0,0 @@ -SPAN_ORIGIN = "auto.db.redis" - -_SINGLE_KEY_COMMANDS = frozenset( - ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"], -) -_MULTI_KEY_COMMANDS = frozenset( - [ - "del", - "touch", - "unlink", - "mget", - ], -) -_COMMANDS_INCLUDING_SENSITIVE_DATA = [ - "auth", -] -_MAX_NUM_ARGS = 10 # Trim argument lists to this many values -_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values -_DEFAULT_MAX_DATA_SIZE = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py deleted file mode 100644 index 35f20fddd9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/caches.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Code used for the Caches module in Sentry -""" - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -GET_COMMANDS = ("get", "mget") -SET_COMMANDS = ("set", "setex") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _get_op(name: str) -> "Optional[str]": - op = None - if name.lower() in GET_COMMANDS: - op = OP.CACHE_GET - elif name.lower() in SET_COMMANDS: - op = OP.CACHE_PUT - - return op - - -def _compile_cache_span_properties( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> "dict[str, Any]": - key = _get_safe_key(redis_command, args, kwargs) - key_as_string = _key_as_string(key) - keys_as_string = key_as_string.split(", ") - - is_cache_key = False - for prefix in integration.cache_prefixes: - for kee in keys_as_string: - if kee.startswith(prefix): - is_cache_key = True - break - if is_cache_key: - break - - value = None - if redis_command.lower() in SET_COMMANDS: - value = args[-1] - - properties = { - "op": _get_op(redis_command), - "description": _get_cache_span_description( - redis_command, args, kwargs, integration - ), - "key": key, - "key_as_string": key_as_string, - "redis_command": redis_command.lower(), - "is_cache_key": is_cache_key, - "value": value, - } - - return properties - - -def _get_cache_span_description( - redis_command: str, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", - integration: "RedisIntegration", -) -> str: - description = _key_as_string(_get_safe_key(redis_command, args, kwargs)) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_cache_data( - span: "Union[Span, StreamedSpan]", - redis_client: "Any", - properties: "dict[str, Any]", - return_value: "Optional[Any]", -) -> None: - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - with capture_internal_exceptions(): - set_on_span(SPANDATA.CACHE_KEY, properties["key"]) - - if properties["redis_command"] in GET_COMMANDS: - if return_value is not None: - set_on_span(SPANDATA.CACHE_HIT, True) - size = ( - len(str(return_value).encode("utf-8")) - if not isinstance(return_value, bytes) - else len(return_value) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - else: - set_on_span(SPANDATA.CACHE_HIT, False) - - elif properties["redis_command"] in SET_COMMANDS: - if properties["value"] is not None: - size = ( - len(properties["value"].encode("utf-8")) - if not isinstance(properties["value"], bytes) - else len(properties["value"]) - ) - set_on_span(SPANDATA.CACHE_ITEM_SIZE, size) - - try: - connection_params = redis_client.connection_pool.connection_kwargs - except AttributeError: - # If it is a cluster, there is no connection_pool attribute so we - # need to get the default node from the cluster instance - default_node = redis_client.get_default_node() - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - - host = connection_params.get("host") - if host is not None: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, host) - - port = connection_params.get("port") - if port is not None: - set_on_span(SPANDATA.NETWORK_PEER_PORT, port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py deleted file mode 100644 index 69207bf6f6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/modules/queries.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Code used for the Queries module in Sentry -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations.redis.utils import _get_safe_command -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import Redis - - from sentry_sdk.integrations.redis import RedisIntegration - from sentry_sdk.tracing import Span - - -def _compile_db_span_properties( - integration: "RedisIntegration", redis_command: str, args: "tuple[Any, ...]" -) -> "dict[str, Any]": - description = _get_db_span_description(integration, redis_command, args) - - properties = { - "op": OP.DB_REDIS, - "description": description, - } - - return properties - - -def _get_db_span_description( - integration: "RedisIntegration", command_name: str, args: "tuple[Any, ...]" -) -> str: - description = command_name - - with capture_internal_exceptions(): - description = _get_safe_command(command_name, args) - - if integration.max_data_size and len(description) > integration.max_data_size: - description = description[: integration.max_data_size - len("...")] + "..." - - return description - - -def _set_db_data_on_span( - span: "Union[Span, StreamedSpan]", connection_params: "dict[str, Any]" -) -> None: - db = connection_params.get("db") - host = connection_params.get("host") - port = connection_params.get("port") - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, "redis") - span.set_attribute(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, str(db)) - - if host is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_attribute(SPANDATA.SERVER_PORT, port) - - else: - span.set_data(SPANDATA.DB_SYSTEM, "redis") - span.set_data(SPANDATA.DB_DRIVER_NAME, "redis-py") - - if db is not None: - span.set_data(SPANDATA.DB_NAME, str(db)) - - if host is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, host) - - if port is not None: - span.set_data(SPANDATA.SERVER_PORT, port) - - -def _set_db_data( - span: "Union[Span, StreamedSpan]", redis_instance: "Redis[Any]" -) -> None: - try: - _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs) - except AttributeError: - pass # connections_kwargs may be missing in some cases diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py deleted file mode 100644 index e2ce863fe8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/rb.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Instrumentation for Redis Blaster (rb) - -https://github.com/getsentry/rb -""" - -from sentry_sdk.integrations.redis._sync_common import patch_redis_client -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - - -def _patch_rb() -> None: - try: - import rb.clients # type: ignore - except ImportError: - pass - else: - patch_redis_client( - rb.clients.FanoutClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.MappingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_client( - rb.clients.RoutingClient, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py deleted file mode 100644 index e704c9bc6a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Instrumentation for Redis - -https://github.com/redis/redis-py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data - -if TYPE_CHECKING: - from typing import Any, Sequence - - -def _get_redis_command_args(command: "Any") -> "Sequence[Any]": - return command[0] - - -def _patch_redis(StrictRedis: "Any", client: "Any") -> None: # noqa: N803 - patch_redis_client( - StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_pipeline( - client.Pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - try: - strict_pipeline = client.StrictPipeline - except AttributeError: - pass - else: - patch_redis_pipeline( - strict_pipeline, - is_cluster=False, - get_command_args_fn=_get_redis_command_args, - set_db_data_fn=_set_db_data, - ) - - try: - import redis.asyncio - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - redis.asyncio.client.StrictRedis, - is_cluster=False, - set_db_data_fn=_set_db_data, - ) - patch_redis_async_pipeline( - redis.asyncio.client.Pipeline, - False, - _get_redis_command_args, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py deleted file mode 100644 index b6c95e6abd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_cluster.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Instrumentation for RedisCluster -This is part of the main redis-py client. - -https://github.com/redis/redis-py/blob/master/redis/cluster.py -""" - -from typing import TYPE_CHECKING - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command -from sentry_sdk.utils import capture_internal_exceptions - -if TYPE_CHECKING: - from typing import Any, Union - - from redis import RedisCluster - from redis.asyncio.cluster import ( - ClusterPipeline as AsyncClusterPipeline, - ) - from redis.asyncio.cluster import ( - RedisCluster as AsyncRedisCluster, - ) - - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.tracing import Span - - -def _set_async_cluster_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_instance: "AsyncRedisCluster[Any]", -) -> None: - default_node = async_redis_cluster_instance.get_default_node() - if default_node is not None and default_node.connection_kwargs is not None: - _set_db_data_on_span(span, default_node.connection_kwargs) - - -def _set_async_cluster_pipeline_db_data( - span: "Union[Span, StreamedSpan]", - async_redis_cluster_pipeline_instance: "AsyncClusterPipeline[Any]", -) -> None: - with capture_internal_exceptions(): - client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None) - if client is None: - # In older redis-py versions, the AsyncClusterPipeline had a `_client` - # attr but it is private so potentially problematic and mypy does not - # recognize it - see - # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386 - client = ( - async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined] - ) - - _set_async_cluster_db_data( - span, - client, - ) - - -def _set_cluster_db_data( - span: "Union[Span, StreamedSpan]", redis_cluster_instance: "RedisCluster[Any]" -) -> None: - default_node = redis_cluster_instance.get_default_node() - - if default_node is not None: - connection_params = { - "host": default_node.host, - "port": default_node.port, - } - _set_db_data_on_span(span, connection_params) - - -def _patch_redis_cluster() -> None: - """Patches the cluster module on redis SDK (as opposed to rediscluster library)""" - try: - from redis import RedisCluster, cluster - except ImportError: - pass - else: - patch_redis_client( - RedisCluster, - is_cluster=True, - set_db_data_fn=_set_cluster_db_data, - ) - patch_redis_pipeline( - cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_cluster_db_data, - ) - - try: - from redis.asyncio import cluster as async_cluster - except ImportError: - pass - else: - from sentry_sdk.integrations.redis._async_common import ( - patch_redis_async_client, - patch_redis_async_pipeline, - ) - - patch_redis_async_client( - async_cluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_async_cluster_db_data, - ) - patch_redis_async_pipeline( - async_cluster.ClusterPipeline, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_async_cluster_pipeline_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py deleted file mode 100644 index 3437aa1f2f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Instrumentation for redis-py-cluster -The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021). - -https://github.com/grokzen/redis-py-cluster -""" - -from sentry_sdk.integrations.redis._sync_common import ( - patch_redis_client, - patch_redis_pipeline, -) -from sentry_sdk.integrations.redis.modules.queries import _set_db_data -from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command - - -def _patch_rediscluster() -> None: - try: - import rediscluster # type: ignore - except ImportError: - return - - patch_redis_client( - rediscluster.RedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - - # up to v1.3.6, __version__ attribute is a tuple - # from v2.0.0, __version__ is a string and VERSION a tuple - version = getattr(rediscluster, "VERSION", rediscluster.__version__) - - # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0 - # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst - if (0, 2, 0) < version < (2, 0, 0): - pipeline_cls = rediscluster.pipeline.StrictClusterPipeline - patch_redis_client( - rediscluster.StrictRedisCluster, - is_cluster=True, - set_db_data_fn=_set_db_data, - ) - else: - pipeline_cls = rediscluster.pipeline.ClusterPipeline - - patch_redis_pipeline( - pipeline_cls, - is_cluster=True, - get_command_args_fn=_parse_rediscluster_command, - set_db_data_fn=_set_db_data, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py deleted file mode 100644 index 7e04df9c69..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/redis/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANDATA -from sentry_sdk.integrations.redis.consts import ( - _COMMANDS_INCLUDING_SENSITIVE_DATA, - _MAX_NUM_ARGS, - _MAX_NUM_COMMANDS, - _MULTI_KEY_COMMANDS, - _SINGLE_KEY_COMMANDS, -) -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - -if TYPE_CHECKING: - from typing import Any, Optional, Sequence, Union - - -def _get_safe_command(name: str, args: "Sequence[Any]") -> str: - command_parts = [name] - - name_low = name.lower() - send_default_pii = should_send_default_pii() - - for i, arg in enumerate(args): - if i > _MAX_NUM_ARGS: - break - - if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - continue - - arg_is_the_key = i == 0 - if arg_is_the_key: - command_parts.append(repr(arg)) - else: - if send_default_pii: - command_parts.append(repr(arg)) - else: - command_parts.append(SENSITIVE_DATA_SUBSTITUTE) - - command = " ".join(command_parts) - return command - - -def _safe_decode(key: "Any") -> str: - if isinstance(key, bytes): - try: - return key.decode() - except UnicodeDecodeError: - return "" - - return str(key) - - -def _key_as_string(key: "Any") -> str: - if isinstance(key, (dict, list, tuple)): - key = ", ".join(_safe_decode(x) for x in key) - elif isinstance(key, bytes): - key = _safe_decode(key) - elif key is None: - key = "" - else: - key = str(key) - - return key - - -def _get_safe_key( - method_name: str, - args: "Optional[tuple[Any, ...]]", - kwargs: "Optional[dict[str, Any]]", -) -> "Optional[tuple[str, ...]]": - """ - Gets the key (or keys) from the given method_name. - The method_name could be a redis command or a django caching command - """ - key = None - - if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS: - # for example redis "mget" - key = tuple(args) - - elif args is not None and len(args) >= 1: - # for example django "set_many/get_many" or redis "get" - if isinstance(args[0], (dict, list, tuple)): - key = tuple(args[0]) - else: - key = (args[0],) - - elif kwargs is not None and "key" in kwargs: - # this is a legacy case for older versions of Django - if isinstance(kwargs["key"], (list, tuple)): - if len(kwargs["key"]) > 0: - key = tuple(kwargs["key"]) - else: - if kwargs["key"] is not None: - key = (kwargs["key"],) - - return key - - -def _parse_rediscluster_command(command: "Any") -> "Sequence[Any]": - return command.args - - -def _set_pipeline_data( - span: "Union[Span, StreamedSpan]", - is_cluster: bool, - get_command_args_fn: "Any", - is_transaction: bool, - commands_seq: "Sequence[Any]", -) -> None: - # TODO: Remove this whole function when removing transaction based tracing - if isinstance(span, StreamedSpan): - return - - span.set_tag("redis.is_cluster", is_cluster) - span.set_tag("redis.transaction", is_transaction) - - commands = [] - for i, arg in enumerate(commands_seq): - if i >= _MAX_NUM_COMMANDS: - break - - command = get_command_args_fn(arg) - commands.append(_get_safe_command(command[0], command[1:])) - - span.set_data( - "redis.commands", - { - "count": len(commands_seq), - "first_ten": commands, - }, - ) - - -def _set_client_data( - span: "Union[Span, StreamedSpan]", is_cluster: bool, name: str, *args: "Any" -) -> None: - if isinstance(span, StreamedSpan): - if name: - span.set_attribute(SPANDATA.DB_OPERATION_NAME, name) - else: - span.set_tag("redis.is_cluster", is_cluster) - if name: - span.set_tag("redis.command", name) - span.set_tag(SPANDATA.DB_OPERATION, name) - - if name and args: - name_low = name.lower() - if (name_low in _SINGLE_KEY_COMMANDS) or ( - name_low in _MULTI_KEY_COMMANDS and len(args) == 1 - ): - if isinstance(span, StreamedSpan): - span.set_attribute("db.redis.key", args[0]) - else: - span.set_tag("redis.key", args[0]) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py deleted file mode 100644 index edd48a9f67..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rq.py +++ /dev/null @@ -1,225 +0,0 @@ -import functools -import weakref - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope, should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - event_from_exception, - format_timestamp, - parse_version, -) - -try: - from rq.job import JobStatus - from rq.queue import Queue - from rq.timeouts import JobTimeoutException - from rq.version import VERSION as RQ_VERSION - from rq.worker import Worker -except ImportError: - raise DidNotEnable("RQ not installed") - -try: - from rq.worker import BaseWorker - - if not hasattr(BaseWorker, "perform_job"): - BaseWorker = None -except ImportError: - BaseWorker = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable - - from rq.job import Job - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - -class RqIntegration(Integration): - identifier = "rq" - origin = f"auto.queue.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(RQ_VERSION) - _check_minimum_version(RqIntegration, version) - - # In rq 2.7.0+, SimpleWorker inherits from BaseWorker directly - # instead of Worker, so we need to patch BaseWorker to cover both. - # For older versions where BaseWorker doesn't exist or doesn't have - # perform_job, we patch Worker. - worker_cls = BaseWorker if BaseWorker is not None else Worker - - old_perform_job = worker_cls.perform_job - - @functools.wraps(old_perform_job) - def sentry_patched_perform_job( - self: "Any", job: "Job", *args: "Queue", **kwargs: "Any" - ) -> bool: - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_perform_job(self, job, *args, **kwargs) - - with sentry_sdk.new_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_event_processor(weakref.ref(job))) - - if has_span_streaming_enabled(client.options): - sentry_sdk.traces.continue_trace( - job.meta.get("_sentry_trace_headers") or {} - ) - - Scope.set_custom_sampling_context({"rq_job": job}) - - func_name = None - with capture_internal_exceptions(): - func_name = job.func_name - - with sentry_sdk.traces.start_span( - name="unknown RQ task" if func_name is None else func_name, - attributes={ - "sentry.op": OP.QUEUE_TASK_RQ, - "sentry.origin": RqIntegration.origin, - "sentry.span.source": SegmentSource.TASK, - SPANDATA.MESSAGING_MESSAGE_ID: job.id, - }, - parent_span=None, - ) as span: - if func_name is not None: - span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, func_name) - - rv = old_perform_job(self, job, *args, **kwargs) - else: - transaction = continue_trace( - job.meta.get("_sentry_trace_headers") or {}, - op=OP.QUEUE_TASK_RQ, - name="unknown RQ task", - source=TransactionSource.TASK, - origin=RqIntegration.origin, - ) - - with capture_internal_exceptions(): - transaction.name = job.func_name - - with sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"rq_job": job}, - ): - rv = old_perform_job(self, job, *args, **kwargs) - - if self.is_horse: - # We're inside of a forked process and RQ is - # about to call `os._exit`. Make sure that our - # events get sent out. - sentry_sdk.get_client().flush() - - return rv - - worker_cls.perform_job = sentry_patched_perform_job - - old_handle_exception = worker_cls.handle_exception - - def sentry_patched_handle_exception( - self: "Worker", job: "Any", *exc_info: "Any", **kwargs: "Any" - ) -> "Any": - retry = ( - hasattr(job, "retries_left") - and job.retries_left - and job.retries_left > 0 - ) - failed = job._status == JobStatus.FAILED or job.is_failed - if failed and not retry: - _capture_exception(exc_info) - - return old_handle_exception(self, job, *exc_info, **kwargs) - - worker_cls.handle_exception = sentry_patched_handle_exception - - old_enqueue_job = Queue.enqueue_job - - @functools.wraps(old_enqueue_job) - def sentry_patched_enqueue_job( - self: "Queue", job: "Any", **kwargs: "Any" - ) -> "Any": - client = sentry_sdk.get_client() - if client.get_integration(RqIntegration) is None: - return old_enqueue_job(self, job, **kwargs) - - scope = sentry_sdk.get_current_scope() - span = ( - scope.streamed_span - if has_span_streaming_enabled(client.options) - else scope.span - ) - if span is not None: - job.meta["_sentry_trace_headers"] = dict( - scope.iter_trace_propagation_headers() - ) - - return old_enqueue_job(self, job, **kwargs) - - Queue.enqueue_job = sentry_patched_enqueue_job - - ignore_logger("rq.worker") - - -def _make_event_processor(weak_job: "Callable[[], Job]") -> "EventProcessor": - def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - job = weak_job() - if job is not None: - with capture_internal_exceptions(): - extra = event.setdefault("extra", {}) - rq_job = { - "job_id": job.id, - "func": job.func_name, - "args": ( - job.args - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "kwargs": ( - job.kwargs - if should_send_default_pii() - else SENSITIVE_DATA_SUBSTITUTE - ), - "description": job.description, - } - - if job.enqueued_at: - rq_job["enqueued_at"] = format_timestamp(job.enqueued_at) - if job.started_at: - rq_job["started_at"] = format_timestamp(job.started_at) - - extra["rq-job"] = rq_job - - if "exc_info" in hint: - with capture_internal_exceptions(): - if issubclass(hint["exc_info"][0], JobTimeoutException): - event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name] - - return event - - return event_processor - - -def _capture_exception(exc_info: "ExcInfo", **kwargs: "Any") -> None: - client = sentry_sdk.get_client() - - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "rq", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py deleted file mode 100644 index 622e3c17af..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/rust_tracing.py +++ /dev/null @@ -1,300 +0,0 @@ -""" -This integration ingests tracing data from native extensions written in Rust. - -Using it requires additional setup on the Rust side to accept a -`RustTracingLayer` Python object and register it with the `tracing-subscriber` -using an adapter from the `pyo3-python-tracing-subscriber` crate. For example: -```rust -#[pyfunction] -pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { - tracing_subscriber::registry() - .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) - .init(); -} -``` - -Usage in Python would then look like: -``` -sentry_sdk.init( - dsn=sentry_dsn, - integrations=[ - RustTracingIntegration( - "demo_rust_extension", - demo_rust_extension.initialize_tracing, - event_type_mapping=event_type_mapping, - ) - ], -) -``` - -Each native extension requires its own integration. -""" - -import json -from enum import Enum, auto -from typing import Any, Callable, Dict, Optional, Union - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE - - -class RustTracingLevel(Enum): - Trace = "TRACE" - Debug = "DEBUG" - Info = "INFO" - Warn = "WARN" - Error = "ERROR" - - -class EventTypeMapping(Enum): - Ignore = auto() - Exc = auto() - Breadcrumb = auto() - Event = auto() - - -def tracing_level_to_sentry_level(level: str) -> "sentry_sdk._types.LogLevelStr": - level = RustTracingLevel(level) - if level in (RustTracingLevel.Trace, RustTracingLevel.Debug): - return "debug" - elif level == RustTracingLevel.Info: - return "info" - elif level == RustTracingLevel.Warn: - return "warning" - elif level == RustTracingLevel.Error: - return "error" - else: - # Better this than crashing - return "info" - - -def extract_contexts(event: "Dict[str, Any]") -> "Dict[str, Any]": - metadata = event.get("metadata", {}) - contexts = {} - - location = {} - for field in ["module_path", "file", "line"]: - if field in metadata: - location[field] = metadata[field] - if len(location) > 0: - contexts["rust_tracing_location"] = location - - fields = {} - for field in metadata.get("fields", []): - fields[field] = event.get(field) - if len(fields) > 0: - contexts["rust_tracing_fields"] = fields - - return contexts - - -def process_event(event: "Dict[str, Any]") -> None: - metadata = event.get("metadata", {}) - - logger = metadata.get("target") - level = tracing_level_to_sentry_level(metadata.get("level")) - message: "sentry_sdk._types.Any" = event.get("message") - contexts = extract_contexts(event) - - sentry_event: "sentry_sdk._types.Event" = { - "logger": logger, - "level": level, - "message": message, - "contexts": contexts, - } - - sentry_sdk.capture_event(sentry_event) - - -def process_exception(event: "Dict[str, Any]") -> None: - process_event(event) - - -def process_breadcrumb(event: "Dict[str, Any]") -> None: - level = tracing_level_to_sentry_level(event.get("metadata", {}).get("level")) - message = event.get("message") - - sentry_sdk.add_breadcrumb(level=level, message=message) - - -def default_span_filter(metadata: "Dict[str, Any]") -> bool: - return RustTracingLevel(metadata.get("level")) in ( - RustTracingLevel.Error, - RustTracingLevel.Warn, - RustTracingLevel.Info, - ) - - -def default_event_type_mapping(metadata: "Dict[str, Any]") -> "EventTypeMapping": - level = RustTracingLevel(metadata.get("level")) - if level == RustTracingLevel.Error: - return EventTypeMapping.Exc - elif level in (RustTracingLevel.Warn, RustTracingLevel.Info): - return EventTypeMapping.Breadcrumb - elif level in (RustTracingLevel.Debug, RustTracingLevel.Trace): - return EventTypeMapping.Ignore - else: - return EventTypeMapping.Ignore - - -class RustTracingLayer: - def __init__( - self, - origin: str, - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.origin = origin - self.event_type_mapping = event_type_mapping - self.span_filter = span_filter - self.include_tracing_fields = include_tracing_fields - - def _include_tracing_fields(self) -> bool: - """ - By default, the values of tracing fields are not included in case they - contain PII. A user may override that by passing `True` for the - `include_tracing_fields` keyword argument of this integration or by - setting `send_default_pii` to `True` in their Sentry client options. - """ - return ( - should_send_default_pii() - if self.include_tracing_fields is None - else self.include_tracing_fields - ) - - def on_event(self, event: str, sentry_span: "SentrySpan") -> None: - deserialized_event = json.loads(event) - metadata = deserialized_event.get("metadata", {}) - - event_type = self.event_type_mapping(metadata) - if event_type == EventTypeMapping.Ignore: - return - elif event_type == EventTypeMapping.Exc: - process_exception(deserialized_event) - elif event_type == EventTypeMapping.Breadcrumb: - process_breadcrumb(deserialized_event) - elif event_type == EventTypeMapping.Event: - process_event(deserialized_event) - - def on_new_span( - self, attrs: str, span_id: str - ) -> "Optional[Union[SentrySpan, StreamedSpan]]": - attrs = json.loads(attrs) - metadata = attrs.get("metadata", {}) - - if not self.span_filter(metadata): - return None - - module_path = metadata.get("module_path") - name = metadata.get("name") - message = attrs.get("message") - - if message is not None: - sentry_span_name = message - elif module_path is not None and name is not None: - sentry_span_name = f"{module_path}::{name}" # noqa: E231 - elif name is not None: - sentry_span_name = name - else: - sentry_span_name = "" - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - sentry_span = sentry_sdk.traces.start_span( - name=sentry_span_name, - attributes={ - "sentry.op": "function", - "sentry.origin": self.origin, - }, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_attribute(field, attrs.get(field)) - else: - sentry_span.set_attribute(field, SENSITIVE_DATA_SUBSTITUTE) - - return sentry_span - - sentry_span = sentry_sdk.start_span( - op="function", - name=sentry_span_name, - origin=self.origin, - ) - fields = metadata.get("fields", []) - for field in fields: - if self._include_tracing_fields(): - sentry_span.set_data(field, attrs.get(field)) - else: - sentry_span.set_data(field, SENSITIVE_DATA_SUBSTITUTE) - - sentry_span.__enter__() - return sentry_span - - def on_close(self, span_id: str, sentry_span: "SentrySpan") -> None: - if sentry_span is None: - return - - sentry_span.__exit__(None, None, None) - - def on_record( - self, span_id: str, values: str, sentry_span: "Union[SentrySpan, StreamedSpan]" - ) -> None: - if sentry_span is None: - return - - set_on_span = ( - sentry_span.set_attribute - if isinstance(sentry_span, StreamedSpan) - else sentry_span.set_data - ) - - deserialized_values = json.loads(values) - for key, value in deserialized_values.items(): - if self._include_tracing_fields(): - set_on_span(key, value) - else: - set_on_span(key, SENSITIVE_DATA_SUBSTITUTE) - - -class RustTracingIntegration(Integration): - """ - Ingests tracing data from a Rust native extension's `tracing` instrumentation. - - If a project uses more than one Rust native extension, each one will need - its own instance of `RustTracingIntegration` with an initializer function - specific to that extension. - - Since all of the setup for this integration requires instance-specific state - which is not available in `setup_once()`, setup instead happens in `__init__()`. - """ - - def __init__( - self, - identifier: str, - initializer: "Callable[[RustTracingLayer], None]", - event_type_mapping: """Callable[ - [Dict[str, Any]], EventTypeMapping - ]""" = default_event_type_mapping, - span_filter: "Callable[[Dict[str, Any]], bool]" = default_span_filter, - include_tracing_fields: "Optional[bool]" = None, - ): - self.identifier = identifier - origin = f"auto.function.rust_tracing.{identifier}" - self.tracing_layer = RustTracingLayer( - origin, event_type_mapping, span_filter, include_tracing_fields - ) - - initializer(self.tracing_layer) - - @staticmethod - def setup_once() -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py deleted file mode 100644 index 908fceb0cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sanic.py +++ /dev/null @@ -1,431 +0,0 @@ -import sys -import warnings -import weakref -from inspect import isawaitable -from typing import TYPE_CHECKING -from urllib.parse import urlsplit - -import sentry_sdk -from sentry_sdk import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - reraise, -) - -if TYPE_CHECKING: - from collections.abc import Container - from typing import Any, Callable, Dict, Optional, Union - - from sanic.request import Request, RequestParameters - from sanic.response import BaseHTTPResponse - from sanic.router import Route - - from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint - -try: - from sanic import Sanic - from sanic import __version__ as SANIC_VERSION - from sanic.exceptions import SanicException - from sanic.handlers import ErrorHandler - from sanic.router import Router -except ImportError: - raise DidNotEnable("Sanic not installed") - -old_error_handler_lookup = ErrorHandler.lookup -old_handle_request = Sanic.handle_request -old_router_get = Router.get - -try: - # This method was introduced in Sanic v21.9 - old_startup = Sanic._startup -except AttributeError: - pass - - -class SanicIntegration(Integration): - identifier = "sanic" - origin = f"auto.http.{identifier}" - version: "Optional[tuple[int, ...]]" = None - - def __init__( - self, unsampled_statuses: "Optional[Container[int]]" = frozenset({404}) - ) -> None: - """ - The unsampled_statuses parameter can be used to specify for which HTTP statuses the - transactions should not be sent to Sentry. By default, transactions are sent for all - HTTP statuses, except 404. Set unsampled_statuses to None to send transactions for all - HTTP statuses, including 404. - """ - self._unsampled_statuses = unsampled_statuses or set() - - @staticmethod - def setup_once() -> None: - SanicIntegration.version = parse_version(SANIC_VERSION) - _check_minimum_version(SanicIntegration, SanicIntegration.version) - - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - - if SANIC_VERSION.startswith("0.8."): - # Sanic 0.8 and older creates a logger named "root" and puts a - # stringified version of every exception in there (without exc_info), - # which our error deduplication can't detect. - # - # We explicitly check the version here because it is a very - # invasive step to ignore this logger and not necessary in newer - # versions at all. - # - # https://github.com/huge-success/sanic/issues/1332 - ignore_logger("root") - - if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): - _setup_legacy_sanic() - return - - _setup_sanic() - - -class SanicRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return dict(self.request.cookies) - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "RequestParameters": - return self.request.form - - def is_json(self) -> bool: - raise NotImplementedError() - - def json(self) -> "Optional[Any]": - return self.request.json - - def files(self) -> "RequestParameters": - return self.request.files - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) - - -def _setup_sanic() -> None: - Sanic._startup = _startup - ErrorHandler.lookup = _sentry_error_handler_lookup - - -def _setup_legacy_sanic() -> None: - Sanic.handle_request = _legacy_handle_request - Router.get = _legacy_router_get - ErrorHandler.lookup = _sentry_error_handler_lookup - - -async def _startup(self: "Sanic") -> None: - # This happens about as early in the lifecycle as possible, just after the - # Request object is created. The body has not yet been consumed. - self.signal("http.lifecycle.request")(_context_enter) - - # This happens after the handler is complete. In v21.9 this signal is not - # dispatched when there is an exception. Therefore we need to close out - # and call _context_exit from the custom exception handler as well. - # See https://github.com/sanic-org/sanic/issues/2297 - self.signal("http.lifecycle.response")(_context_exit) - - # This happens inside of request handling immediately after the route - # has been identified by the router. - self.signal("http.routing.after")(_set_transaction) - - # The above signals need to be declared before this can be called. - await old_startup(self) - - -async def _context_enter(request: "Request") -> None: - request.ctx._sentry_do_integration = ( - sentry_sdk.get_client().get_integration(SanicIntegration) is not None - ) - - if not request.ctx._sentry_do_integration: - return - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - weak_request = weakref.ref(request) - request.ctx._sentry_scope = sentry_sdk.isolation_scope() - scope = request.ctx._sentry_scope.__enter__() - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - if is_span_streaming_enabled: - integration = client.get_integration(SanicIntegration) - if ( - isinstance(integration, SanicIntegration) - and integration._unsampled_statuses - ): - warnings.warn( - "The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.", - stacklevel=2, - ) - - sentry_sdk.traces.continue_trace(dict(request.headers)) - scope.set_custom_sampling_context({"sanic_request": request}) - - if should_send_default_pii() and request.remote_addr: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) - - span = sentry_sdk.traces.start_span( - # Unless the request results in a 404 error, the name and source - # will get overwritten in _set_transaction - name=request.path, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": SanicIntegration.origin, - "sentry.span.source": SegmentSource.URL.value, - }, - parent_span=None, - ) - request.ctx._sentry_root_span = span - else: - transaction = continue_trace( - dict(request.headers), - op=OP.HTTP_SERVER, - # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction - name=request.path, - source=TransactionSource.URL, - origin=SanicIntegration.origin, - ) - request.ctx._sentry_root_span = sentry_sdk.start_transaction( - transaction - ).__enter__() - - -async def _context_exit( - request: "Request", response: "Optional[BaseHTTPResponse]" = None -) -> None: - with capture_internal_exceptions(): - if not request.ctx._sentry_do_integration: - return - - integration = sentry_sdk.get_client().get_integration(SanicIntegration) - - response_status = None if response is None else response.status - - # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception - # happens while trying to end the transaction, we still attempt to exit the hub. - with capture_internal_exceptions(): - span = request.ctx._sentry_root_span - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes(request).items(): - span.set_attribute(attr, value) - if response_status is not None: - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status) - span.status = "error" if response_status >= 400 else "ok" - - span.end() - - else: - span.set_http_status(response_status) - span.sampled &= ( - isinstance(integration, SanicIntegration) - and response_status not in integration._unsampled_statuses - ) - - span.__exit__(None, None, None) - - request.ctx._sentry_scope.__exit__(None, None, None) - - -async def _set_transaction(request: "Request", route: "Route", **_: "Any") -> None: - if request.ctx._sentry_do_integration: - with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - route_name = route.name.replace(request.app.name, "").strip(".") - scope.set_transaction_name(route_name, source=TransactionSource.COMPONENT) - - -def _sentry_error_handler_lookup( - self: "Any", exception: Exception, *args: "Any", **kwargs: "Any" -) -> "Optional[object]": - _capture_exception(exception) - old_error_handler = old_error_handler_lookup(self, exception, *args, **kwargs) - - if old_error_handler is None: - return None - - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return old_error_handler - - async def sentry_wrapped_error_handler( - request: "Request", exception: Exception - ) -> "Any": - try: - response = old_error_handler(request, exception) - if isawaitable(response): - response = await response - return response - except Exception: - # Report errors that occur in Sanic error handler. These - # exceptions will not even show up in Sanic's - # `sanic.exceptions` logger. - exc_info = sys.exc_info() - _capture_exception(exc_info) - reraise(*exc_info) - finally: - # As mentioned in previous comment in _startup, this can be removed - # after https://github.com/sanic-org/sanic/issues/2297 is resolved - if SanicIntegration.version and SanicIntegration.version == (21, 9): - await _context_exit(request) - - return sentry_wrapped_error_handler - - -async def _legacy_handle_request( - self: "Any", request: "Request", *args: "Any", **kwargs: "Any" -) -> "Any": - if sentry_sdk.get_client().get_integration(SanicIntegration) is None: - return await old_handle_request(self, request, *args, **kwargs) - - weak_request = weakref.ref(request) - - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - scope.add_event_processor(_make_request_processor(weak_request)) - - response = old_handle_request(self, request, *args, **kwargs) - if isawaitable(response): - response = await response - - return response - - -def _legacy_router_get(self: "Any", *args: "Union[Any, Request]") -> "Any": - rv = old_router_get(self, *args) - if sentry_sdk.get_client().get_integration(SanicIntegration) is not None: - with capture_internal_exceptions(): - scope = sentry_sdk.get_isolation_scope() - if SanicIntegration.version and SanicIntegration.version >= (21, 3): - # Sanic versions above and including 21.3 append the app name to the - # route name, and so we need to remove it from Route name so the - # transaction name is consistent across all versions - sanic_app_name = self.ctx.app.name - sanic_route = rv[0].name - - if sanic_route.startswith("%s." % sanic_app_name): - # We add a 1 to the len of the sanic_app_name because there is a dot - # that joins app name and the route name - # Format: app_name.route_name - sanic_route = sanic_route[len(sanic_app_name) + 1 :] - - scope.set_transaction_name( - sanic_route, source=TransactionSource.COMPONENT - ) - else: - scope.set_transaction_name( - rv[0].__name__, source=TransactionSource.COMPONENT - ) - - return rv - - -@ensure_integration_enabled(SanicIntegration) -def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None: - with capture_internal_exceptions(): - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "sanic", "handled": False}, - ) - - if hint and hasattr(hint["exc_info"][0], "quiet") and hint["exc_info"][0].quiet: - return - - sentry_sdk.capture_event(event, hint=hint) - - -def _get_request_attributes(request: "Request") -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from a Sanic request. - """ - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - urlparts = urlsplit(request.url) - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.url - attributes["url.path"] = urlparts.path - - if urlparts.query: - attributes[SPANDATA.HTTP_QUERY] = urlparts.query - - if urlparts.scheme: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme - - if should_send_default_pii() and request.remote_addr: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr - - return attributes - - -def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor": - def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]": - try: - if hint and issubclass(hint["exc_info"][0], SanicException): - return None - except KeyError: - pass - - request = weak_request() - if request is None: - return event - - with capture_internal_exceptions(): - extractor = SanicRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - urlparts = urlsplit(request.url) - - request_info["url"] = "%s://%s%s" % ( - urlparts.scheme, - urlparts.netloc, - urlparts.path, - ) - - request_info["query_string"] = urlparts.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_addr} - request_info["headers"] = _filter_headers(dict(request.headers)) - - return event - - return sanic_processor diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py deleted file mode 100644 index 16f91b28ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/serverless.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys -from functools import wraps -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import event_from_exception, reraise - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar, Union, overload - - F = TypeVar("F", bound=Callable[..., Any]) - -else: - - def overload(x: "F") -> "F": - return x - - -@overload -def serverless_function(f: "F", flush: bool = True) -> "F": - pass - - -@overload -def serverless_function(f: None = None, flush: bool = True) -> "Callable[[F], F]": # noqa: F811 - pass - - -def serverless_function( # noqa - f: "Optional[F]" = None, flush: bool = True -) -> "Union[F, Callable[[F], F]]": - def wrapper(f: "F") -> "F": - @wraps(f) - def inner(*args: "Any", **kwargs: "Any") -> "Any": - with sentry_sdk.isolation_scope() as scope: - scope.clear_breadcrumbs() - - try: - return f(*args, **kwargs) - except Exception: - _capture_and_reraise() - finally: - if flush: - sentry_sdk.flush() - - return inner # type: ignore - - if f is None: - return wrapper - else: - return wrapper(f) - - -def _capture_and_reraise() -> None: - exc_info = sys.exc_info() - client = sentry_sdk.get_client() - if client.is_active(): - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "serverless", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - reraise(*exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py deleted file mode 100644 index 775170fb9f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/socket.py +++ /dev/null @@ -1,142 +0,0 @@ -import socket - -import sentry_sdk -from sentry_sdk._types import MYPY -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.tracing_utils import has_span_streaming_enabled - -if MYPY: - from socket import AddressFamily, SocketKind - from typing import List, Optional, Tuple, Union - -__all__ = ["SocketIntegration"] - - -class SocketIntegration(Integration): - identifier = "socket" - origin = f"auto.socket.{identifier}" - - @staticmethod - def setup_once() -> None: - """ - patches two of the most used functions of socket: create_connection and getaddrinfo(dns resolver) - """ - _patch_create_connection() - _patch_getaddrinfo() - - -def _get_span_description( - host: "Union[bytes, str, None]", port: "Union[bytes, str, int, None]" -) -> str: - try: - host = host.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - try: - port = port.decode() # type: ignore - except (UnicodeDecodeError, AttributeError): - pass - - description = "%s:%s" % (host, port) # type: ignore - return description - - -def _patch_create_connection() -> None: - real_create_connection = socket.create_connection - - def create_connection( - address: "Tuple[Optional[str], int]", - timeout: "Optional[float]" = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore - source_address: "Optional[Tuple[Union[bytearray, bytes, str], int]]" = None, - ) -> "socket.socket": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_create_connection(address, timeout, source_address) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(address[0], address[1]), - attributes={ - "sentry.op": OP.SOCKET_CONNECTION, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if address[0] is not None: - span.set_attribute(SPANDATA.SERVER_ADDRESS, address[0]) - span.set_attribute(SPANDATA.SERVER_PORT, address[1]) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_CONNECTION, - name=_get_span_description(address[0], address[1]), - origin=SocketIntegration.origin, - ) as span: - span.set_data("address", address) - span.set_data("timeout", timeout) - span.set_data("source_address", source_address) - - return real_create_connection( - address=address, timeout=timeout, source_address=source_address - ) - - socket.create_connection = create_connection # type: ignore - - -def _patch_getaddrinfo() -> None: - real_getaddrinfo = socket.getaddrinfo - - def getaddrinfo( - host: "Union[bytes, str, None]", - port: "Union[bytes, str, int, None]", - family: int = 0, - type: int = 0, - proto: int = 0, - flags: int = 0, - ) -> "List[Tuple[AddressFamily, SocketKind, int, str, Union[Tuple[str, int], Tuple[str, int, int, int], Tuple[int, bytes]]]]": - client = sentry_sdk.get_client() - integration = client.get_integration(SocketIntegration) - if integration is None: - return real_getaddrinfo(host, port, family, type, proto, flags) - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=_get_span_description(host, port), - attributes={ - "sentry.op": OP.SOCKET_DNS, - "sentry.origin": SocketIntegration.origin, - }, - ) as span: - if isinstance(host, str): - span.set_attribute(SPANDATA.SERVER_ADDRESS, host) - elif isinstance(host, bytes): - span.set_attribute( - SPANDATA.SERVER_ADDRESS, host.decode(errors="replace") - ) - - if isinstance(port, int): - span.set_attribute(SPANDATA.SERVER_PORT, port) - elif port is not None: - try: - span.set_attribute(SPANDATA.SERVER_PORT, int(port)) - except (ValueError, TypeError): - pass - - return real_getaddrinfo(host, port, family, type, proto, flags) - else: - with sentry_sdk.start_span( - op=OP.SOCKET_DNS, - name=_get_span_description(host, port), - origin=SocketIntegration.origin, - ) as span: - span.set_data("host", host) - span.set_data("port", port) - - return real_getaddrinfo(host, port, family, type, proto, flags) - - socket.getaddrinfo = getaddrinfo diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py deleted file mode 100644 index 10d94163c5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from sentry_sdk.integrations.spark.spark_driver import SparkIntegration -from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration - -__all__ = ["SparkIntegration", "SparkWorkerIntegration"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py deleted file mode 100644 index a83532b6a6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_driver.py +++ /dev/null @@ -1,278 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled - -if TYPE_CHECKING: - from typing import Any, Optional - - from pyspark import SparkContext - - from sentry_sdk._types import Event, Hint - - -class SparkIntegration(Integration): - identifier = "spark" - - @staticmethod - def setup_once() -> None: - _setup_sentry_tracing() - - -def _set_app_properties() -> None: - """ - Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties. - This allows worker integration to have access to app_name and application_id. - """ - from pyspark import SparkContext - - spark_context = SparkContext._active_spark_context - if spark_context: - spark_context.setLocalProperty( - "sentry_app_name", - spark_context.appName, - ) - spark_context.setLocalProperty( - "sentry_application_id", - spark_context.applicationId, - ) - - -def _start_sentry_listener(sc: "SparkContext") -> None: - """ - Start java gateway server to add custom `SparkListener` - """ - from pyspark.java_gateway import ensure_callback_server_started - - gw = sc._gateway - ensure_callback_server_started(gw) - listener = SentryListener() - sc._jsc.sc().addSparkListener(listener) - - -def _add_event_processor(sc: "SparkContext") -> None: - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - if sentry_sdk.get_client().get_integration(SparkIntegration) is None: - return event - - if sc._active_spark_context is None: - return event - - event.setdefault("user", {}).setdefault("id", sc.sparkUser()) - - event.setdefault("tags", {}).setdefault( - "executor.id", sc._conf.get("spark.executor.id") - ) - event["tags"].setdefault( - "spark-submit.deployMode", - sc._conf.get("spark.submit.deployMode"), - ) - event["tags"].setdefault("driver.host", sc._conf.get("spark.driver.host")) - event["tags"].setdefault("driver.port", sc._conf.get("spark.driver.port")) - event["tags"].setdefault("spark_version", sc.version) - event["tags"].setdefault("app_name", sc.appName) - event["tags"].setdefault("application_id", sc.applicationId) - event["tags"].setdefault("master", sc.master) - event["tags"].setdefault("spark_home", sc.sparkHome) - - event.setdefault("extra", {}).setdefault("web_url", sc.uiWebUrl) - - return event - - -def _activate_integration(sc: "SparkContext") -> None: - _start_sentry_listener(sc) - _set_app_properties() - _add_event_processor(sc) - - -def _patch_spark_context_init() -> None: - from pyspark import SparkContext - - spark_context_init = SparkContext._do_init - - @ensure_integration_enabled(SparkIntegration, spark_context_init) - def _sentry_patched_spark_context_init( - self: "SparkContext", *args: "Any", **kwargs: "Any" - ) -> "Optional[Any]": - rv = spark_context_init(self, *args, **kwargs) - _activate_integration(self) - return rv - - SparkContext._do_init = _sentry_patched_spark_context_init - - -def _setup_sentry_tracing() -> None: - from pyspark import SparkContext - - if SparkContext._active_spark_context is not None: - _activate_integration(SparkContext._active_spark_context) - return - _patch_spark_context_init() - - -class SparkListener: - def onApplicationEnd(self, applicationEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onApplicationStart(self, applicationStart: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerAdded(self, blockManagerAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockManagerRemoved(self, blockManagerRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onBlockUpdated(self, blockUpdated: "Any") -> None: # noqa: N802,N803 - pass - - def onEnvironmentUpdate(self, environmentUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorAdded(self, executorAdded: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklisted(self, executorBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorBlacklistedForStage( # noqa: N802 - self, - executorBlacklistedForStage: "Any", # noqa: N803 - ) -> None: - pass - - def onExecutorMetricsUpdate(self, executorMetricsUpdate: "Any") -> None: # noqa: N802,N803 - pass - - def onExecutorRemoved(self, executorRemoved: "Any") -> None: # noqa: N802,N803 - pass - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklisted(self, nodeBlacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeBlacklistedForStage(self, nodeBlacklistedForStage: "Any") -> None: # noqa: N802,N803 - pass - - def onNodeUnblacklisted(self, nodeUnblacklisted: "Any") -> None: # noqa: N802,N803 - pass - - def onOtherEvent(self, event: "Any") -> None: # noqa: N802,N803 - pass - - def onSpeculativeTaskSubmitted(self, speculativeTask: "Any") -> None: # noqa: N802,N803 - pass - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - pass - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskEnd(self, taskEnd: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskGettingResult(self, taskGettingResult: "Any") -> None: # noqa: N802,N803 - pass - - def onTaskStart(self, taskStart: "Any") -> None: # noqa: N802,N803 - pass - - def onUnpersistRDD(self, unpersistRDD: "Any") -> None: # noqa: N802,N803 - pass - - class Java: - implements = ["org.apache.spark.scheduler.SparkListenerInterface"] - - -class SentryListener(SparkListener): - def _add_breadcrumb( - self, - level: str, - message: str, - data: "Optional[dict[str, Any]]" = None, - ) -> None: - sentry_sdk.get_isolation_scope().add_breadcrumb( - level=level, message=message, data=data - ) - - def onJobStart(self, jobStart: "Any") -> None: # noqa: N802,N803 - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - message = "Job {} Started".format(jobStart.jobId()) - self._add_breadcrumb(level="info", message=message) - _set_app_properties() - - def onJobEnd(self, jobEnd: "Any") -> None: # noqa: N802,N803 - level = "" - message = "" - data = {"result": jobEnd.jobResult().toString()} - - if jobEnd.jobResult().toString() == "JobSucceeded": - level = "info" - message = "Job {} Ended".format(jobEnd.jobId()) - else: - level = "warning" - message = "Job {} Failed".format(jobEnd.jobId()) - - self._add_breadcrumb(level=level, message=message, data=data) - - def onStageSubmitted(self, stageSubmitted: "Any") -> None: # noqa: N802,N803 - stage_info = stageSubmitted.stageInfo() - message = "Stage {} Submitted".format(stage_info.stageId()) - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - self._add_breadcrumb(level="info", message=message, data=data) - _set_app_properties() - - def onStageCompleted(self, stageCompleted: "Any") -> None: # noqa: N802,N803 - from py4j.protocol import Py4JJavaError # type: ignore - - stage_info = stageCompleted.stageInfo() - message = "" - level = "" - - data = {"name": stage_info.name()} - attempt_id = _get_attempt_id(stage_info) - if attempt_id is not None: - data["attemptId"] = attempt_id - - # Have to Try Except because stageInfo.failureReason() is typed with Scala Option - try: - data["reason"] = stage_info.failureReason().get() - message = "Stage {} Failed".format(stage_info.stageId()) - level = "warning" - except Py4JJavaError: - message = "Stage {} Completed".format(stage_info.stageId()) - level = "info" - - self._add_breadcrumb(level=level, message=message, data=data) - - -def _get_attempt_id(stage_info: "Any") -> "Optional[int]": - try: - return stage_info.attemptId() - except Exception: - pass - - try: - return stage_info.attemptNumber() - except Exception: - pass - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py deleted file mode 100644 index 5906472748..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/spark/spark_worker.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_hint_with_exc_info, - exc_info_from_error, - single_exception_from_error_tuple, - walk_exception_chain, -) - -if TYPE_CHECKING: - from typing import Any, Optional - - from sentry_sdk._types import Event, ExcInfo, Hint - - -class SparkWorkerIntegration(Integration): - identifier = "spark_worker" - - @staticmethod - def setup_once() -> None: - import pyspark.daemon as original_daemon - - original_daemon.worker_main = _sentry_worker_main - - -def _capture_exception(exc_info: "ExcInfo") -> None: - client = sentry_sdk.get_client() - - mechanism = {"type": "spark", "handled": False} - - exc_info = exc_info_from_error(exc_info) - - exc_type, exc_value, tb = exc_info - rv = [] - - # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - if exc_type not in (SystemExit, EOFError, ConnectionResetError): - rv.append( - single_exception_from_error_tuple( - exc_type, exc_value, tb, client.options, mechanism - ) - ) - - if rv: - rv.reverse() - hint = event_hint_with_exc_info(exc_info) - event: "Event" = {"level": "error", "exception": {"values": rv}} - - _tag_task_context() - - sentry_sdk.capture_event(event, hint=hint) - - -def _tag_task_context() -> None: - from pyspark.taskcontext import TaskContext - - scope = sentry_sdk.get_isolation_scope() - - @scope.add_event_processor - def process_event(event: "Event", hint: "Hint") -> "Optional[Event]": - with capture_internal_exceptions(): - integration = sentry_sdk.get_client().get_integration( - SparkWorkerIntegration - ) - task_context = TaskContext.get() - - if integration is None or task_context is None: - return event - - event.setdefault("tags", {}).setdefault( - "stageId", str(task_context.stageId()) - ) - event["tags"].setdefault("partitionId", str(task_context.partitionId())) - event["tags"].setdefault("attemptNumber", str(task_context.attemptNumber())) - event["tags"].setdefault("taskAttemptId", str(task_context.taskAttemptId())) - - if task_context._localProperties: - if "sentry_app_name" in task_context._localProperties: - event["tags"].setdefault( - "app_name", task_context._localProperties["sentry_app_name"] - ) - event["tags"].setdefault( - "application_id", - task_context._localProperties["sentry_application_id"], - ) - - if "callSite.short" in task_context._localProperties: - event.setdefault("extra", {}).setdefault( - "callSite", task_context._localProperties["callSite.short"] - ) - - return event - - -def _sentry_worker_main(*args: "Optional[Any]", **kwargs: "Optional[Any]") -> None: - import pyspark.worker as original_worker - - try: - original_worker.main(*args, **kwargs) - except SystemExit: - if sentry_sdk.get_client().get_integration(SparkWorkerIntegration) is not None: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - _capture_exception(exc_info) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py deleted file mode 100644 index 962fe4ee53..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sqlalchemy.py +++ /dev/null @@ -1,185 +0,0 @@ -from sentry_sdk.consts import SPANDATA, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - add_query_source, - record_sql_queries, -) -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - parse_version, -) - -try: - from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore - from sqlalchemy.engine import Engine # type: ignore - from sqlalchemy.event import listen # type: ignore -except ImportError: - raise DidNotEnable("SQLAlchemy not installed.") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, ContextManager, Optional, Union - - -class SqlalchemyIntegration(Integration): - identifier = "sqlalchemy" - origin = f"auto.db.{identifier}" - - @staticmethod - def setup_once() -> None: - version = parse_version(SQLALCHEMY_VERSION) - _check_minimum_version(SqlalchemyIntegration, version) - - listen(Engine, "before_cursor_execute", _before_cursor_execute) - listen(Engine, "after_cursor_execute", _after_cursor_execute) - listen(Engine, "handle_error", _handle_error) - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _before_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - executemany: bool, - *args: "Any", -) -> None: - ctx_mgr = record_sql_queries( - cursor, - statement, - parameters, - paramstyle=context and context.dialect and context.dialect.paramstyle or None, - executemany=executemany, - span_origin=SqlalchemyIntegration.origin, - ) - context._sentry_sql_span_manager = ctx_mgr - - span = ctx_mgr.__enter__() - - if span is not None: - _set_db_data(span, conn) - context._sentry_sql_span = span - - -@ensure_integration_enabled(SqlalchemyIntegration) -def _after_cursor_execute( - conn: "Any", - cursor: "Any", - statement: "Any", - parameters: "Any", - context: "Any", - *args: "Any", -) -> None: - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - context, "_sentry_sql_span_manager", None - ) - - # Record query source immediately before span is finished: accurate end timestamp and before the span is flushed. - span: "Optional[Union[Span, StreamedSpan]]" = getattr( - context, "_sentry_sql_span", None - ) - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_query_source(span) - - if ctx_mgr is not None: - context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - if isinstance(span, Span): - with capture_internal_exceptions(): - add_query_source(span) - - -def _handle_error(context: "Any", *args: "Any") -> None: - execution_context = context.execution_context - if execution_context is None: - return - - span: "Optional[Span]" = getattr(execution_context, "_sentry_sql_span", None) - - if span is not None: - if isinstance(span, StreamedSpan): - span.status = SpanStatus.ERROR - else: - span.set_status(SPANSTATUS.INTERNAL_ERROR) - - # _after_cursor_execute does not get called for crashing SQL stmts. Judging - # from SQLAlchemy codebase it does seem like any error coming into this - # handler is going to be fatal. - ctx_mgr: "Optional[ContextManager[Any]]" = getattr( - execution_context, "_sentry_sql_span_manager", None - ) - - if ctx_mgr is not None: - execution_context._sentry_sql_span_manager = None - ctx_mgr.__exit__(None, None, None) - - -# See: https://docs.sqlalchemy.org/en/20/dialects/index.html -def _get_db_system(name: str) -> "Optional[str]": - name = str(name) - - if "sqlite" in name: - return "sqlite" - - if "postgres" in name: - return "postgresql" - - if "mariadb" in name: - return "mariadb" - - if "mysql" in name: - return "mysql" - - if "oracle" in name: - return "oracle" - - return None - - -def _set_db_data(span: "Union[Span, StreamedSpan]", conn: "Any") -> None: - db_system = _get_db_system(conn.engine.name) - - if isinstance(span, StreamedSpan): - if db_system is not None: - span.set_attribute(SPANDATA.DB_SYSTEM_NAME, db_system) - else: - if db_system is not None: - span.set_data(SPANDATA.DB_SYSTEM, db_system) - - if isinstance(span, StreamedSpan): - set_on_span = span.set_attribute - else: - set_on_span = span.set_data - - try: - driver = conn.dialect.driver - if driver: - set_on_span(SPANDATA.DB_DRIVER_NAME, driver) - except Exception: - pass - - if conn.engine.url is None: - return - - db_name = conn.engine.url.database - if isinstance(span, StreamedSpan): - if db_name is not None: - span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) - else: - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) - - server_address = conn.engine.url.host - if server_address is not None: - set_on_span(SPANDATA.SERVER_ADDRESS, server_address) - - server_port = conn.engine.url.port - if server_port is not None: - set_on_span(SPANDATA.SERVER_PORT, server_port) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py deleted file mode 100644 index 3f9fbf4d8e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlette.py +++ /dev/null @@ -1,858 +0,0 @@ -import functools -import json -import sys -import warnings -from collections.abc import Set -from copy import deepcopy -from json import JSONDecodeError -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import ( - _DEFAULT_FAILED_REQUEST_STATUS_CODES, - DidNotEnable, - Integration, -) -from sentry_sdk.integrations._asgi_common import _RootPathInPath -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - HttpCodeRangeContainer, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan, get_current_span -from sentry_sdk.tracing import ( - SOURCE_FOR_STYLE, - TransactionSource, -) -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - parse_version, - transaction_from_function, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Awaitable, - Callable, - Container, - Dict, - Optional, - Tuple, - Union, - ) - - from sentry_sdk._types import Event, HttpStatusCodeRange -try: - import starlette - from starlette import __version__ as STARLETTE_VERSION - from starlette.applications import Starlette - from starlette.datastructures import ( - UploadFile, - ) - from starlette.middleware import Middleware - from starlette.middleware.authentication import ( - AuthenticationMiddleware, - ) - from starlette.requests import Request - from starlette.routing import Match - from starlette.types import ASGIApp, Receive, Send - from starlette.types import Scope as StarletteScope -except ImportError: - raise DidNotEnable("Starlette is not installed") - -try: - # Starlette 0.20 - from starlette.middleware.exceptions import ExceptionMiddleware -except ImportError: - # Startlette 0.19.1 - from starlette.exceptions import ExceptionMiddleware # type: ignore - -try: - # Optional dependency of Starlette to parse form data. - try: - # python-multipart 0.0.13 and later - import python_multipart as multipart - except ImportError: - # python-multipart 0.0.12 and earlier - import multipart # type: ignore -except ImportError: - multipart = None # type: ignore[assignment] - - -# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17 -if sys.version_info >= (3, 13): # pragma: no cover - from inspect import iscoroutinefunction -else: - from asyncio import iscoroutinefunction - - -_DEFAULT_TRANSACTION_NAME = "generic Starlette request" - -TRANSACTION_STYLE_VALUES = ("endpoint", "url") - - -class StarletteIntegration(Integration): - identifier = "starlette" - origin = f"auto.http.{identifier}" - - transaction_style = "" - - def __init__( - self, - transaction_style: str = "url", - failed_request_status_codes: "Union[Set[int], list[HttpStatusCodeRange], None]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES, - middleware_spans: bool = False, - http_methods_to_capture: "tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ): - if transaction_style not in TRANSACTION_STYLE_VALUES: - raise ValueError( - "Invalid value for transaction_style: %s (must be in %s)" - % (transaction_style, TRANSACTION_STYLE_VALUES) - ) - self.transaction_style = transaction_style - self.middleware_spans = middleware_spans - self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture)) - - if isinstance(failed_request_status_codes, Set): - self.failed_request_status_codes: "Container[int]" = ( - failed_request_status_codes - ) - else: - warnings.warn( - "Passing a list or None for failed_request_status_codes is deprecated. " - "Please pass a set of int instead.", - DeprecationWarning, - stacklevel=2, - ) - - if failed_request_status_codes is None: - self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES - else: - self.failed_request_status_codes = HttpCodeRangeContainer( - failed_request_status_codes - ) - - @staticmethod - def setup_once() -> None: - version = parse_version(STARLETTE_VERSION) - - if version is None: - raise DidNotEnable( - "Unparsable Starlette version: {}".format(STARLETTE_VERSION) - ) - - patch_middlewares() - # Starlette tolerates both starting with: - # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. - root_path_in_path = ( - _RootPathInPath.EITHER if version >= (0, 33) else _RootPathInPath.EXCLUDED - ) - patch_asgi_app(root_path_in_path=root_path_in_path) - patch_request_response() - - if version >= (0, 24): - patch_templates() - - -def _enable_span_for_middleware( - middleware_class: "Any", -) -> "Any": - old_call: "Callable[..., Awaitable[Any]]" = middleware_class.__call__ - - async def _create_span_call( - app: "Any", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - **kwargs: "Any", - ) -> None: - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await old_call(app, scope, receive, send, **kwargs) - - # Update transaction name with middleware name - name, source = _get_transaction_from_middleware(app, scope, integration) - - if name is not None: - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=source, - ) - - if not integration.middleware_spans: - return await old_call(app, scope, receive, send, **kwargs) - - middleware_name = app.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarletteIntegration.origin, - "middleware.name": middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarletteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlette.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(*args: "Any", **kwargs: "Any") -> "Any": - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLETTE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlette.middleware_name", middleware_name) - return await send(*args, **kwargs) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(app, scope, new_receive, new_send, **kwargs) - - not_yet_patched = old_call.__name__ not in [ - "_create_span_call", - "_sentry_authenticationmiddleware_call", - "_sentry_exceptionmiddleware_call", - ] - - if not_yet_patched: - middleware_class.__call__ = _create_span_call - - return middleware_class - - -def _serialize_request_body_data(data: "Any") -> str: - # data may be a JSON-serializable value, an AnnotatedValue, or a dict with AnnotatedValue values - def _default(value: "Any") -> "Any": - if isinstance(value, AnnotatedValue): - return value.value - return str(value) - - return json.dumps(data, default=_default) - - -@ensure_integration_enabled(StarletteIntegration) -def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: - event, hint = event_from_exception( - exception, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarletteIntegration.identifier, "handled": handled}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def patch_exception_middleware(middleware_class: "Any") -> None: - """ - Capture all exceptions in Starlette app and - also extract user information. - """ - old_middleware_init = middleware_class.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - - if not_yet_patched: - - def _sentry_middleware_init(self: "Any", *args: "Any", **kwargs: "Any") -> None: - old_middleware_init(self, *args, **kwargs) - - # Patch existing exception handlers - old_handlers = self._exception_handlers.copy() - - async def _sentry_patched_exception_handler( - self: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration( - StarletteIntegration - ) - - exp = args[0] - - if integration is not None: - is_http_server_error = ( - hasattr(exp, "status_code") - and isinstance(exp.status_code, int) - and exp.status_code in integration.failed_request_status_codes - ) - if is_http_server_error: - _capture_exception(exp, handled=True) - - # Find a matching handler - old_handler = None - for cls in type(exp).__mro__: - if cls in old_handlers: - old_handler = old_handlers[cls] - break - - if old_handler is None: - return - - if _is_async_callable(old_handler): - return await old_handler(self, *args, **kwargs) - else: - return old_handler(self, *args, **kwargs) - - for key in self._exception_handlers.keys(): - self._exception_handlers[key] = _sentry_patched_exception_handler - - middleware_class.__init__ = _sentry_middleware_init - - old_call = middleware_class.__call__ - - async def _sentry_exceptionmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - # Also add the user (that was eventually set by be Authentication middle - # that was called before this middleware). This is done because the authentication - # middleware sets the user in the scope and then (in the same function) - # calls this exception middelware. In case there is no exception (or no handler - # for the type of exception occuring) then the exception bubbles up and setting the - # user information into the sentry scope is done in auth middleware and the - # ASGI middleware will then send everything to Sentry and this is fine. - # But if there is an exception happening that the exception middleware here - # has a handler for, it will send the exception directly to Sentry, so we need - # the user information right now. - # This is why we do it here. - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_exceptionmiddleware_call - - -@ensure_integration_enabled(StarletteIntegration) -def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None: - """ - Extracts user information from the ASGI scope and - adds it to Sentry's scope. - """ - if "user" not in scope: - return - - if not should_send_default_pii(): - return - - user_info: "Dict[str, Any]" = {} - starlette_user = scope["user"] - - username = getattr(starlette_user, "username", None) - if username: - user_info.setdefault("username", starlette_user.username) - - user_id = getattr(starlette_user, "id", None) - if user_id: - user_info.setdefault("id", starlette_user.id) - - email = getattr(starlette_user, "email", None) - if email: - user_info.setdefault("email", starlette_user.email) - - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - -def patch_authentication_middleware(middleware_class: "Any") -> None: - """ - Add user information to Sentry scope. - """ - old_call = middleware_class.__call__ - - not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) - - if not_yet_patched: - - async def _sentry_authenticationmiddleware_call( - self: "Dict[str, Any]", - scope: "Dict[str, Any]", - receive: "Callable[[], Awaitable[Dict[str, Any]]]", - send: "Callable[[Dict[str, Any]], Awaitable[None]]", - ) -> None: - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_authenticationmiddleware_call - - -def patch_middlewares() -> None: - """ - Patches Starlettes `Middleware` class to record - spans for every middleware invoked. - """ - old_middleware_init = Middleware.__init__ - - not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - if not_yet_patched: - - def _sentry_middleware_init( - self: "Any", cls: "Any", *args: "Any", **kwargs: "Any" - ) -> None: - if cls == SentryAsgiMiddleware: - return old_middleware_init(self, cls, *args, **kwargs) - - span_enabled_cls = _enable_span_for_middleware(cls) - old_middleware_init(self, span_enabled_cls, *args, **kwargs) - - if cls == AuthenticationMiddleware: - patch_authentication_middleware(cls) - - if cls == ExceptionMiddleware: - patch_exception_middleware(cls) - - Middleware.__init__ = _sentry_middleware_init # type: ignore[method-assign] - - -def patch_asgi_app(root_path_in_path: "_RootPathInPath") -> None: - """ - Instrument Starlette ASGI app using the SentryAsgiMiddleware. - """ - old_app = Starlette.__call__ - - async def _sentry_patched_asgi_app( - self: "Starlette", scope: "StarletteScope", receive: "Receive", send: "Send" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StarletteIntegration) - if integration is None: - return await old_app(self, scope, receive, send) - - middleware = SentryAsgiMiddleware( - lambda *a, **kw: old_app(self, *a, **kw), - mechanism_type=StarletteIntegration.identifier, - transaction_style=integration.transaction_style, - span_origin=StarletteIntegration.origin, - http_methods_to_capture=( - integration.http_methods_to_capture - if integration - else DEFAULT_HTTP_METHODS_TO_CAPTURE - ), - asgi_version=3, - root_path_in_path=root_path_in_path, - ) - - return await middleware(scope, receive, send) - - Starlette.__call__ = _sentry_patched_asgi_app # type: ignore[method-assign] - - -# This was vendored in from Starlette to support Starlette 0.19.1 because -# this function was only introduced in 0.20.x -def _is_async_callable(obj: "Any") -> bool: - while isinstance(obj, functools.partial): - obj = obj.func - - return iscoroutinefunction(obj) or ( - callable(obj) and iscoroutinefunction(obj.__call__) # type: ignore[operator] - ) - - -def _get_cached_request_body_attribute( - client: "sentry_sdk.client.BaseClient", request: "Request" -) -> "Optional[str]": - """ - Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. - """ - if "content-length" not in request.headers: - return None - - try: - content_length = int(request.headers["content-length"]) - except ValueError: - return None - - if content_length and not request_body_within_bounds(client, content_length): - return OVER_SIZE_LIMIT_SUBSTITUTE - - if hasattr(request, "_json"): - return json.dumps(request._json) - - formdata_body = getattr(request, "_form", None) - if formdata_body is None: - return None - - form_data = {} - for key, val in formdata_body.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = val if not is_file else "[Unparsable]" - - return json.dumps(form_data) - - -async def _wrap_async_handler( - handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" -) -> "Any": - """ - Wraps an asynchronous handler function to attach request info to errors and the server segment span. - The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event - processor can still cause application hangs. - """ - client = sentry_sdk.get_client() - integration = client.get_integration(StarletteIntegration) - if integration is None: - return await handler(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), - integration.transaction_style, - request, - ) - - sentry_scope = sentry_sdk.get_isolation_scope() - extractor = StarletteRequestExtractor(request) - - info = await extractor.extract_request_info() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - # Add info from request to event - request_info = event.get("request", {}) - if info: - if "cookies" in info: - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - try: - return await handler(*args, **kwargs) - finally: - current_span = get_current_span() - - if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, - ) - - -def patch_request_response() -> None: - old_request_response = starlette.routing.request_response - - def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": - old_func = func - - is_coroutine = _is_async_callable(old_func) - if is_coroutine: - - async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": - return await _wrap_async_handler(old_func, *args, **kwargs) - - func = _sentry_async_func - - else: - - @functools.wraps(old_func) - def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - integration = client.get_integration(StarletteIntegration) - if integration is None: - return old_func(*args, **kwargs) - - current_scope = sentry_sdk.get_current_scope() - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming: - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - current_span._segment._update_active_thread() - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - request = args[0] - - _set_transaction_name_and_source( - sentry_scope, integration.transaction_style, request - ) - - extractor = StarletteRequestExtractor(request) - cookies = extractor.extract_cookies_from_request() - - def _make_request_event_processor( - req: "Any", integration: "Any" - ) -> "Callable[[Event, dict[str, Any]], Event]": - def event_processor( - event: "Event", hint: "dict[str, Any]" - ) -> "Event": - # Extract information from request - request_info = event.get("request", {}) - if cookies: - request_info["cookies"] = cookies - - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope._name = StarletteIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - return old_func(*args, **kwargs) - - func = _sentry_sync_func - - return old_request_response(func) - - starlette.routing.request_response = _sentry_request_response - - -def patch_templates() -> None: - # If markupsafe is not installed, then Jinja2 is not installed - # (markupsafe is a dependency of Jinja2) - # In this case we do not need to patch the Jinja2Templates class - try: - from markupsafe import Markup - except ImportError: - return # Nothing to do - - # https://github.com/Kludex/starlette/commit/96479daca2e4bd8157f68d914fd162aa94eff73a - try: - from starlette.templating import Jinja2Templates - except ImportError: - return - - old_jinja2templates_init = Jinja2Templates.__init__ - - not_yet_patched = "_sentry_jinja2templates_init" not in str( - old_jinja2templates_init - ) - - if not_yet_patched: - - def _sentry_jinja2templates_init( - self: "Jinja2Templates", *args: "Any", **kwargs: "Any" - ) -> None: - def add_sentry_trace_meta(request: "Request") -> "Dict[str, Any]": - trace_meta = Markup( - sentry_sdk.get_current_scope().trace_propagation_meta() - ) - return { - "sentry_trace_meta": trace_meta, - } - - kwargs.setdefault("context_processors", []) - - if add_sentry_trace_meta not in kwargs["context_processors"]: - kwargs["context_processors"].append(add_sentry_trace_meta) - - return old_jinja2templates_init(self, *args, **kwargs) - - Jinja2Templates.__init__ = _sentry_jinja2templates_init # type: ignore[method-assign] - - -class StarletteRequestExtractor: - """ - Extracts useful information from the Starlette request - (like form data or cookies) and adds it to the Sentry event. - """ - - def __init__(self: "StarletteRequestExtractor", request: "Request") -> None: - self.request = request - - def extract_cookies_from_request( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - cookies: "Optional[Dict[str, Any]]" = None - if should_send_default_pii(): - cookies = self.cookies() - - return cookies - - async def extract_request_info( - self: "StarletteRequestExtractor", - ) -> "Optional[Dict[str, Any]]": - client = sentry_sdk.get_client() - - request_info: "Dict[str, Any]" = {} - - with capture_internal_exceptions(): - # Add cookies - if should_send_default_pii(): - request_info["cookies"] = self.cookies() - - # If there is no body, just return the cookies - content_length = await self.content_length() - if not content_length: - return request_info - - # Add annotation if body is too big - if content_length and not request_body_within_bounds( - client, content_length - ): - request_info["data"] = AnnotatedValue.removed_because_over_size_limit() - return request_info - - # Add JSON body, if it is a JSON request - json = await self.json() - if json: - request_info["data"] = json - return request_info - - # Add form as key/value pairs, if request has form data - form = await self.form() - if form: - form_data = {} - for key, val in form.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = ( - val - if not is_file - else AnnotatedValue.removed_because_raw_data() - ) - - request_info["data"] = form_data - return request_info - - # Raw data, do not add body just an annotation - request_info["data"] = AnnotatedValue.removed_because_raw_data() - return request_info - - async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": - if "content-length" in self.request.headers: - return int(self.request.headers["content-length"]) - - return None - - def cookies(self: "StarletteRequestExtractor") -> "Dict[str, Any]": - return self.request.cookies - - async def form(self: "StarletteRequestExtractor") -> "Any": - if multipart is None: - return None - - # Parse the body first to get it cached, as Starlette does not cache form() as it - # does with body() and json() https://github.com/encode/starlette/discussions/1933 - # Calling `.form()` without calling `.body()` first will - # potentially break the users project. - await self.request.body() - - return await self.request.form() - - def is_json(self: "StarletteRequestExtractor") -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": - if not self.is_json(): - return None - try: - return await self.request.json() - except JSONDecodeError: - return None - - -def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]": - router = scope.get("router") - if not router: - return None - - for route in router.routes: - match = route.matches(scope) - if match[0] == Match.FULL: - try: - return route.path - except AttributeError: - # routes added via app.host() won't have a path attribute - return scope.get("path") - - return None - - -def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" -) -> None: - name = None - source = SOURCE_FOR_STYLE[transaction_style] - - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or None - - elif transaction_style == "url": - name = _transaction_name_from_router(request.scope) - - if name is None: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - scope.set_transaction_name(name, source=source) - - -def _get_transaction_from_middleware( - app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" -) -> "Tuple[Optional[str], Optional[str]]": - name = None - source = None - - if integration.transaction_style == "endpoint": - name = transaction_from_function(app.__class__) - source = TransactionSource.COMPONENT - elif integration.transaction_style == "url": - name = _transaction_name_from_router(asgi_scope) - source = TransactionSource.ROUTE - - return name, source diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py deleted file mode 100644 index 1eebd37e84..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/starlite.py +++ /dev/null @@ -1,314 +0,0 @@ -from copy import deepcopy - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from pydantic import BaseModel - from starlite import Request, Starlite, State # type: ignore - from starlite.handlers.base import BaseRouteHandler # type: ignore - from starlite.middleware import DefineMiddleware # type: ignore - from starlite.plugins.base import get_plugin_for_value # type: ignore - from starlite.routes.http import HTTPRoute # type: ignore - from starlite.utils import ( # type: ignore - ConnectionDataExtractor, - Ref, - is_async_callable, - ) -except ImportError: - raise DidNotEnable("Starlite is not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from starlite import MiddlewareProtocol - from starlite.types import ( # type: ignore - ASGIApp, - Hint, - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Send, - WebSocketReceiveMessage, - ) - from starlite.types import ( - Scope as StarliteScope, - ) - - from sentry_sdk._types import Event - - -_DEFAULT_TRANSACTION_NAME = "generic Starlite request" - - -class StarliteIntegration(Integration): - identifier = "starlite" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - patch_app_init() - patch_middlewares() - patch_http_route_handle() - - -class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): - def __init__( - self, app: "ASGIApp", span_origin: str = StarliteIntegration.origin - ) -> None: - super().__init__( - app=app, - unsafe_context_data=False, - transaction_style="endpoint", - mechanism_type="asgi", - span_origin=span_origin, - asgi_version=3, - ) - - -def patch_app_init() -> None: - """ - Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the - `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. - See: - - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception - - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ - """ - old__init__ = Starlite.__init__ - - @ensure_integration_enabled(StarliteIntegration, old__init__) - def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: - after_exception = kwargs.pop("after_exception", []) - kwargs.update( - after_exception=[ - exception_handler, - *( - after_exception - if isinstance(after_exception, list) - else [after_exception] - ), - ] - ) - - middleware = kwargs.get("middleware") or [] - kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] - old__init__(self, *args, **kwargs) - - Starlite.__init__ = injection_wrapper - - -def patch_middlewares() -> None: - old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware - - @ensure_integration_enabled(StarliteIntegration, old_resolve_middleware_stack) - def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]": - return [ - enable_span_for_middleware(middleware) - for middleware in old_resolve_middleware_stack(self) - ] - - BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper - - -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if ( - not hasattr(middleware, "__call__") # noqa: B004 - or middleware is SentryStarliteASGIMiddleware - ): - return middleware - - if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ - else: - old_call = middleware.__call__ - - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "StarliteScope", - receive: "Receive", - send: "Send", - ) -> None: - client = sentry_sdk.get_client() - if client.get_integration(StarliteIntegration) is None: - return await old_call(self, scope, receive, send) - - middleware_name = self.__class__.__name__ - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - def _start_middleware_span(op: str, name: str) -> "Any": - if is_span_streaming_enabled: - return sentry_sdk.traces.start_span( - name=name, - attributes={ - "sentry.op": op, - "sentry.origin": StarliteIntegration.origin, - SPANDATA.MIDDLEWARE_NAME: middleware_name, - }, - ) - return sentry_sdk.start_span( - op=op, - name=name, - origin=StarliteIntegration.origin, - ) - - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE, name=middleware_name - ) as middleware_span: - if not is_span_streaming_enabled: - middleware_span.set_tag("starlite.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await receive(*args, **kwargs) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_RECEIVE, - name=getattr(receive, "__qualname__", str(receive)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await receive(*args, **kwargs) - - receive_name = getattr(receive, "__name__", str(receive)) - receive_patched = receive_name == "_sentry_receive" - new_receive = _sentry_receive if not receive_patched else receive - - # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await send(message) - with _start_middleware_span( - op=OP.MIDDLEWARE_STARLITE_SEND, - name=getattr(send, "__qualname__", str(send)), - ) as span: - if not is_span_streaming_enabled: - span.set_tag("starlite.middleware_name", middleware_name) - return await send(message) - - send_name = getattr(send, "__name__", str(send)) - send_patched = send_name == "_sentry_send" - new_send = _sentry_send if not send_patched else send - - return await old_call(self, scope, new_receive, new_send) - - not_yet_patched = old_call.__name__ not in ["_create_span_call"] - - if not_yet_patched: - if isinstance(middleware, DefineMiddleware): - middleware.middleware.__call__ = _create_span_call - else: - middleware.__call__ = _create_span_call - - return middleware - - -def patch_http_route_handle() -> None: - old_handle = HTTPRoute.handle - - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: - return await old_handle(self, scope, receive, send) - - sentry_scope = sentry_sdk.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( - scope=scope, receive=receive, send=send - ) - extracted_request_data = ConnectionDataExtractor( - parse_body=True, parse_query=True - )(request) - body = extracted_request_data.pop("body") - - request_data = await body - - route_handler = scope.get("route_handler") - - func = None - if route_handler.name is not None: - name = route_handler.name - elif isinstance(route_handler.fn, Ref): - func = route_handler.fn.value - else: - func = route_handler.fn - if func is not None: - name = transaction_from_function(func) - - source = SOURCE_FOR_STYLE["endpoint"] - - if not name: - name = _DEFAULT_TRANSACTION_NAME - source = TransactionSource.ROUTE - - sentry_sdk.set_transaction_name(name, source) - sentry_scope.set_transaction_name(name, source) - - def event_processor(event: "Event", _: "Hint") -> "Event": - request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) - if should_send_default_pii(): - request_info["cookies"] = extracted_request_data["cookies"] - if request_data is not None: - request_info["data"] = request_data - - event["request"] = deepcopy(request_info) - return event - - sentry_scope._name = StarliteIntegration.identifier - sentry_scope.add_event_processor(event_processor) - - return await old_handle(self, scope, receive, send) - - HTTPRoute.handle = handle_wrapper - - -def retrieve_user_from_scope(scope: "StarliteScope") -> "Optional[dict[str, Any]]": - scope_user = scope.get("user") - if not scope_user: - return None - if isinstance(scope_user, dict): - return scope_user - if isinstance(scope_user, BaseModel): - return scope_user.dict() - if hasattr(scope_user, "asdict"): # dataclasses - return scope_user.asdict() - - plugin = get_plugin_for_value(scope_user) - if plugin and not is_async_callable(plugin.to_dict): - return plugin.to_dict(scope_user) - - return None - - -@ensure_integration_enabled(StarliteIntegration) -def exception_handler(exc: Exception, scope: "StarliteScope", _: "State") -> None: - user_info: "Optional[dict[str, Any]]" = None - if should_send_default_pii(): - user_info = retrieve_user_from_scope(scope) - if user_info and isinstance(user_info, dict): - sentry_scope = sentry_sdk.get_isolation_scope() - sentry_scope.set_user(user_info) - - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": StarliteIntegration.identifier, "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py deleted file mode 100644 index 746e09b36f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/statsig.py +++ /dev/null @@ -1,37 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.utils import parse_version - -try: - from statsig import statsig as statsig_module - from statsig.version import __version__ as STATSIG_VERSION -except ImportError: - raise DidNotEnable("statsig is not installed") - -if TYPE_CHECKING: - from statsig.statsig_user import StatsigUser - - -class StatsigIntegration(Integration): - identifier = "statsig" - - @staticmethod - def setup_once() -> None: - version = parse_version(STATSIG_VERSION) - _check_minimum_version(StatsigIntegration, version, "statsig") - - # Wrap and patch evaluation method(s) in the statsig module - old_check_gate = statsig_module.check_gate - - @wraps(old_check_gate) - def sentry_check_gate( - user: "StatsigUser", gate: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_check_gate(user, gate, *args, **kwargs) - add_feature_flag(gate, enabled) - return enabled - - statsig_module.check_gate = sentry_check_gate diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py deleted file mode 100644 index 82f30f2dda..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/stdlib.py +++ /dev/null @@ -1,404 +0,0 @@ -import os -import platform -import subprocess -import sys -from http.client import HTTPConnection, HTTPResponse -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import add_global_event_processor, should_send_default_pii -from sentry_sdk.traces import StreamedSpan -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import ( - EnvironHeaders, - add_http_request_source, - has_span_streaming_enabled, - should_propagate_trace, -) -from sentry_sdk.utils import ( - SENSITIVE_DATA_SUBSTITUTE, - capture_internal_exceptions, - ensure_integration_enabled, - is_sentry_url, - logger, - parse_url, - safe_repr, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Union - - from sentry_sdk._types import Event, Hint - - -_RUNTIME_CONTEXT: "dict[str, object]" = { - "name": platform.python_implementation(), - "version": "%s.%s.%s" % (sys.version_info[:3]), - "build": sys.version, -} - - -class StdlibIntegration(Integration): - identifier = "stdlib" - - @staticmethod - def setup_once() -> None: - _install_httplib() - _install_subprocess() - - @add_global_event_processor - def add_python_runtime_context( - event: "Event", hint: "Hint" - ) -> "Optional[Event]": - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is not None: - contexts = event.setdefault("contexts", {}) - if isinstance(contexts, dict) and "runtime" not in contexts: - contexts["runtime"] = _RUNTIME_CONTEXT - - return event - - -def _complete_span(span: "Union[Span, StreamedSpan]") -> None: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - add_http_request_source(span) - span.end() - else: - span.finish() - with capture_internal_exceptions(): - add_http_request_source(span) - - -def _install_httplib() -> None: - real_putrequest = HTTPConnection.putrequest - real_getresponse = HTTPConnection.getresponse - real_read = HTTPResponse.read - real_close = HTTPResponse.close - - def putrequest( - self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - default_port = self.default_port - - # proxies go through set_tunnel - tunnel_host = getattr(self, "_tunnel_host", None) - if tunnel_host: - host = tunnel_host - port = getattr(self, "_tunnel_port", default_port) - else: - host = self.host - port = self.port - - client = sentry_sdk.get_client() - if client.get_integration(StdlibIntegration) is None or is_sentry_url( - client, host - ): - return real_putrequest(self, method, url, *args, **kwargs) - - real_url = url - if real_url is None or not real_url.startswith(("http://", "https://")): - real_url = "%s://%s%s%s" % ( - default_port == 443 and "https" or "http", - host, - port != default_port and ":%s" % port or "", - url, - ) - - parsed_url = None - with capture_internal_exceptions(): - parsed_url = parse_url(real_url, sanitize=False) - - span_streaming = has_span_streaming_enabled(client.options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - attributes={ - "sentry.origin": "auto.http.stdlib.httplib", - "sentry.op": OP.HTTP_CLIENT, - SPANDATA.HTTP_REQUEST_METHOD: method, - }, - ) - - if parsed_url is not None and should_send_default_pii(): - span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment) - span.set_attribute(SPANDATA.URL_FULL, parsed_url.url) - span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query) - - set_on_span = span.set_attribute - - else: - span = sentry_sdk.start_span( - op=OP.HTTP_CLIENT, - name="%s %s" - % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE), - origin="auto.http.stdlib.httplib", - ) - - span.set_data(SPANDATA.HTTP_METHOD, method) - if parsed_url is not None: - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - - set_on_span = span.set_data - - # for proxies, these point to the proxy host/port - if tunnel_host: - set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host) - set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port) - - rv = real_putrequest(self, method, url, *args, **kwargs) - - if should_propagate_trace(client, real_url): - for ( - key, - value, - ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - logger.debug( - "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( - key=key, value=value, real_url=real_url - ) - ) - self.putheader(key, value) - - self._sentrysdk_span = span # type: ignore[attr-defined] - - return rv - - def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": - span = getattr(self, "_sentrysdk_span", None) - - if span is None: - return real_getresponse(self, *args, **kwargs) - - try: - rv = real_getresponse(self, *args, **kwargs) - except BaseException: - _complete_span(span) - raise - - if isinstance(span, StreamedSpan): - status_code = int(rv.status) - span.status = "error" if status_code >= 400 else "ok" - span.set_attribute("http.response.status_code", status_code) - else: - span.set_http_status(int(rv.status)) - span.set_data("reason", rv.reason) - - # getresponse doesn't include actually reading the response body. This - # is done in read(). So if the metadata/headers suggest there's a body to - # read, don't finish the span just yet, but save it for ending it later. - has_body = rv.chunked or (rv.length is not None and rv.length > 0) - if has_body: - rv._sentrysdk_span = span # type: ignore[attr-defined] - else: - _complete_span(span) - - return rv - - def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any": - try: - return real_read(self, *args, **kwargs) - finally: - span = getattr(self, "_sentrysdk_span", None) - # read() might be called multiple times to consume a single body, - # so we can't just end the span when read() is done. Instead, - # try to figure out whether the response body has been fully read. - if span and (self.fp is None or self.closed): - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - def close(self: "HTTPResponse") -> None: - # We patch close() as a best effort fallback in case the span is not - # ended yet in getresponse() or read(). - - try: - real_close(self) - finally: - span = getattr(self, "_sentrysdk_span", None) - if span is not None: - self._sentrysdk_span = None # type: ignore[attr-defined] - _complete_span(span) - - HTTPConnection.putrequest = putrequest # type: ignore[method-assign] - HTTPConnection.getresponse = getresponse # type: ignore[method-assign] - HTTPResponse.read = read # type: ignore[method-assign] - HTTPResponse.close = close # type: ignore[assignment,method-assign] - - -def _init_argument( - args: "List[Any]", - kwargs: "Dict[Any, Any]", - name: str, - position: int, - setdefault_callback: "Optional[Callable[[Any], Any]]" = None, -) -> "Any": - """ - given (*args, **kwargs) of a function call, retrieve (and optionally set a - default for) an argument by either name or position. - - This is useful for wrapping functions with complex type signatures and - extracting a few arguments without needing to redefine that function's - entire type signature. - """ - - if name in kwargs: - rv = kwargs[name] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - kwargs[name] = rv - elif position < len(args): - rv = args[position] - if setdefault_callback is not None: - rv = setdefault_callback(rv) - if rv is not None: - args[position] = rv - else: - rv = setdefault_callback and setdefault_callback(None) - if rv is not None: - kwargs[name] = rv - - return rv - - -def _install_subprocess() -> None: - old_popen_init = subprocess.Popen.__init__ - - @ensure_integration_enabled(StdlibIntegration, old_popen_init) - def sentry_patched_popen_init( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> None: - # Convert from tuple to list to be able to set values. - a = list(a) - - args = _init_argument(a, kw, "args", 0) or [] - cwd = _init_argument(a, kw, "cwd", 9) - - # if args is not a list or tuple (and e.g. some iterator instead), - # let's not use it at all. There are too many things that can go wrong - # when trying to collect an iterator into a list and setting that list - # into `a` again. - # - # Also invocations where `args` is not a sequence are not actually - # legal. They just happen to work under CPython. - description = None - - if isinstance(args, (list, tuple)) and len(args) < 100: - with capture_internal_exceptions(): - description = " ".join(map(str, args)) - - if description is None: - description = safe_repr(args) - - env = None - - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - span: "Union[Span, StreamedSpan]" - if span_streaming: - span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.op": OP.SUBPROCESS, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) - else: - span = sentry_sdk.start_span( - op=OP.SUBPROCESS, - name=description, - origin="auto.subprocess.stdlib.subprocess", - ) - - with span: - for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers( - span=span - ): - if env is None: - env = _init_argument( - a, - kw, - "env", - 10, - lambda x: dict(x if x is not None else os.environ), - ) - env["SUBPROCESS_" + k.upper().replace("-", "_")] = v - - if cwd and isinstance(span, Span): - span.set_data("subprocess.cwd", cwd) - - rv = old_popen_init(self, *a, **kw) - - if isinstance(span, StreamedSpan): - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - else: - span.set_tag("subprocess.pid", self.pid) - - return rv - - subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore - - old_popen_wait = subprocess.Popen.wait - - @ensure_integration_enabled(StdlibIntegration, old_popen_wait) - def sentry_patched_popen_wait( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_WAIT, - attributes={ - "sentry.op": OP.SUBPROCESS_WAIT, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_wait(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_WAIT, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_wait(self, *a, **kw) - - subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore - - old_popen_communicate = subprocess.Popen.communicate - - @ensure_integration_enabled(StdlibIntegration, old_popen_communicate) - def sentry_patched_popen_communicate( - self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any" - ) -> "Any": - span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) - if span_streaming: - with sentry_sdk.traces.start_span( - name=OP.SUBPROCESS_COMMUNICATE, - attributes={ - "sentry.op": OP.SUBPROCESS_COMMUNICATE, - "sentry.origin": "auto.subprocess.stdlib.subprocess", - }, - ) as span: - span.set_attribute(SPANDATA.PROCESS_PID, self.pid) - return old_popen_communicate(self, *a, **kw) - else: - with sentry_sdk.start_span( - op=OP.SUBPROCESS_COMMUNICATE, - origin="auto.subprocess.stdlib.subprocess", - ) as span: - span.set_tag("subprocess.pid", self.pid) - return old_popen_communicate(self, *a, **kw) - - subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore - - -def get_subprocess_traceparent_headers() -> "EnvironHeaders": - return EnvironHeaders(os.environ, prefix="SUBPROCESS_") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py deleted file mode 100644 index 5f00e8bf6d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/strawberry.py +++ /dev/null @@ -1,493 +0,0 @@ -import functools -import hashlib -import warnings -from inspect import isawaitable - -import sentry_sdk -from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import StreamedSpan, has_span_streaming_enabled -from sentry_sdk.utils import ( - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - package_version, -) - -try: - from functools import cached_property -except ImportError: - # The strawberry integration requires Python 3.8+. functools.cached_property - # was added in 3.8, so this check is technically not needed, but since this - # is an auto-enabling integration, we might get to executing this import in - # lower Python versions, so we need to deal with it. - raise DidNotEnable("strawberry-graphql integration requires Python 3.8 or newer") - -try: - from strawberry import Schema - from strawberry.extensions import SchemaExtension - from strawberry.extensions.tracing.utils import ( - should_skip_tracing as strawberry_should_skip_tracing, - ) - from strawberry.http import async_base_view, sync_base_view -except ImportError: - raise DidNotEnable("strawberry-graphql is not installed") - -try: - from strawberry.extensions.tracing import ( - SentryTracingExtension as StrawberrySentryAsyncExtension, - ) - from strawberry.extensions.tracing import ( - SentryTracingExtensionSync as StrawberrySentrySyncExtension, - ) -except ImportError: - StrawberrySentryAsyncExtension = None - StrawberrySentrySyncExtension = None - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, Generator, List, Optional - - from graphql import GraphQLError, GraphQLResolveInfo - from strawberry.http import GraphQLHTTPResponse - from strawberry.types import ExecutionContext - - from sentry_sdk._types import Event, EventProcessor - - -ignore_logger("strawberry.execution") - - -class StrawberryIntegration(Integration): - identifier = "strawberry" - origin = f"auto.graphql.{identifier}" - - def __init__(self, async_execution: "Optional[bool]" = None) -> None: - if async_execution not in (None, False, True): - raise ValueError( - 'Invalid value for async_execution: "{}" (must be bool)'.format( - async_execution - ) - ) - self.async_execution = async_execution - - @staticmethod - def setup_once() -> None: - version = package_version("strawberry-graphql") - _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") - - _patch_schema_init() - _patch_views() - - -def _patch_schema_init() -> None: - old_schema_init = Schema.__init__ - - @functools.wraps(old_schema_init) - def _sentry_patched_schema_init( - self: "Schema", *args: "Any", **kwargs: "Any" - ) -> None: - integration = sentry_sdk.get_client().get_integration(StrawberryIntegration) - if integration is None: - return old_schema_init(self, *args, **kwargs) - - extensions = kwargs.get("extensions") or [] - - should_use_async_extension: "Optional[bool]" = None - if integration.async_execution is not None: - should_use_async_extension = integration.async_execution - else: - # try to figure it out ourselves - should_use_async_extension = _guess_if_using_async(extensions) - - if should_use_async_extension is None: - warnings.warn( - "Assuming strawberry is running sync. If not, initialize the integration as StrawberryIntegration(async_execution=True).", - stacklevel=2, - ) - should_use_async_extension = False - - # remove the built in strawberry sentry extension, if present - extensions = [ - extension - for extension in extensions - if extension - not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension) - ] - - # add our extension - extensions = [ - SentryAsyncExtension if should_use_async_extension else SentrySyncExtension - ] + extensions - - kwargs["extensions"] = extensions - - return old_schema_init(self, *args, **kwargs) - - Schema.__init__ = _sentry_patched_schema_init # type: ignore[method-assign] - - -class SentryAsyncExtension(SchemaExtension): - def __init__( - self: "Any", - *, - execution_context: "Optional[ExecutionContext]" = None, - ) -> None: - if execution_context: - self.execution_context = execution_context - - @cached_property - def _resource_name(self) -> str: - query_hash = self.hash_query(self.execution_context.query) # type: ignore - - if self.execution_context.operation_name: - return "{}:{}".format(self.execution_context.operation_name, query_hash) - - return query_hash - - def hash_query(self, query: str) -> str: - return hashlib.md5(query.encode("utf-8")).hexdigest() - - def on_operation(self) -> "Generator[None, None, None]": - operation_name = self.execution_context.operation_name - - operation_type = "query" - op = OP.GRAPHQL_QUERY - - if self.execution_context.query is None: - self.execution_context.query = "" - - if self.execution_context.query.strip().startswith("mutation"): - operation_type = "mutation" - op = OP.GRAPHQL_MUTATION - elif self.execution_context.query.strip().startswith("subscription"): - operation_type = "subscription" - op = OP.GRAPHQL_SUBSCRIPTION - - description = operation_type - if operation_name: - description += " {}".format(operation_name) - - sentry_sdk.add_breadcrumb( - category="graphql.operation", - data={ - "operation_name": operation_name, - "operation_type": operation_type, - }, - ) - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_request_event_processor(self.execution_context) - scope.add_event_processor(event_processor) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - additional_attributes: "dict[str, Any]" = {} - - if should_send_default_pii(): - additional_attributes["graphql.document"] = self.execution_context.query - - if operation_name: - additional_attributes["graphql.operation.name"] = operation_name - - graphql_span = sentry_sdk.traces.start_span( - name=description, - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": op, - "graphql.operation.type": operation_type, - **additional_attributes, - }, - ) - else: - graphql_span = sentry_sdk.start_span( - op=op, - name=description, - origin=StrawberryIntegration.origin, - ) - graphql_span.__enter__() - - if type(graphql_span) is Span: - if should_send_default_pii(): - graphql_span.set_data("graphql.document", self.execution_context.query) - - graphql_span.set_data("graphql.operation.type", operation_type) - graphql_span.set_data("graphql.operation.name", operation_name) - # This attribute is being removed in streamed spans - graphql_span.set_data("graphql.resource_name", self._resource_name) - - yield - - if type(graphql_span) is StreamedSpan: - if self.execution_context.operation_name: - segment = graphql_span._segment - segment.set_attribute("sentry.span.source", SegmentSource.COMPONENT) - segment.set_attribute("sentry.op", op) - segment.name = self.execution_context.operation_name - elif isinstance(graphql_span, Span): - transaction = graphql_span.containing_transaction - if transaction and self.execution_context.operation_name: - transaction.name = self.execution_context.operation_name - transaction.source = TransactionSource.COMPONENT - transaction.op = op - - graphql_span.__exit__(None, None, None) - - def on_validate(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - validation_span = sentry_sdk.traces.start_span( - name="validation", - attributes={ - "sentry.op": OP.GRAPHQL_VALIDATE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - validation_span = sentry_sdk.start_span( - op=OP.GRAPHQL_VALIDATE, - name="validation", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during validation, we still need to close the span - try: - yield - finally: - if isinstance(validation_span, StreamedSpan): - validation_span.end() - else: - validation_span.finish() - - def on_parse(self) -> "Generator[None, None, None]": - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - if is_span_streaming_enabled: - parsing_span = sentry_sdk.traces.start_span( - name="parsing", - attributes={ - "sentry.op": OP.GRAPHQL_PARSE, - "sentry.origin": StrawberryIntegration.origin, - }, - ) - else: - parsing_span = sentry_sdk.start_span( - op=OP.GRAPHQL_PARSE, - name="parsing", - origin=StrawberryIntegration.origin, - ) - - # If an exception is raised during parsing, we still need to close the span - try: - yield - finally: - if isinstance(parsing_span, StreamedSpan): - parsing_span.end() - else: - parsing_span.finish() - - def should_skip_tracing( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - info: "GraphQLResolveInfo", - ) -> bool: - return strawberry_should_skip_tracing(_next, info) - - async def _resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - result = _next(root, info, *args, **kwargs) - - if isawaitable(result): - result = await result - - return result - - async def resolve( - self, - _next: "Callable[[Any, GraphQLResolveInfo, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return await self._resolve(_next, root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return await self._resolve(_next, root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return await self._resolve(_next, root, info, *args, **kwargs) - - -class SentrySyncExtension(SentryAsyncExtension): - def resolve( - self, - _next: "Callable[[Any, Any, Any, Any], Any]", - root: "Any", - info: "GraphQLResolveInfo", - *args: str, - **kwargs: "Any", - ) -> "Any": - if self.should_skip_tracing(_next, info): - return _next(root, info, *args, **kwargs) - - field_path = "{}.{}".format(info.parent_type, info.field_name) - - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - if is_span_streaming_enabled: - with sentry_sdk.traces.start_span( - name=f"resolving {field_path}", - attributes={ - "sentry.origin": StrawberryIntegration.origin, - "sentry.op": OP.GRAPHQL_RESOLVE, - }, - ): - return _next(root, info, *args, **kwargs) - - with sentry_sdk.start_span( - op=OP.GRAPHQL_RESOLVE, - name="resolving {}".format(field_path), - origin=StrawberryIntegration.origin, - ) as span: - span.set_data("graphql.field_name", info.field_name) - span.set_data("graphql.parent_type", info.parent_type.name) - span.set_data("graphql.field_path", field_path) - span.set_data("graphql.path", ".".join(map(str, info.path.as_list()))) - - return _next(root, info, *args, **kwargs) - - -def _patch_views() -> None: - old_async_view_handle_errors = async_base_view.AsyncBaseHTTPView._handle_errors - old_sync_view_handle_errors = sync_base_view.SyncBaseHTTPView._handle_errors - - def _sentry_patched_async_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_async_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - def _sentry_patched_sync_view_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - old_sync_view_handle_errors(self, errors, response_data) - _sentry_patched_handle_errors(self, errors, response_data) - - @ensure_integration_enabled(StrawberryIntegration) - def _sentry_patched_handle_errors( - self: "Any", errors: "List[GraphQLError]", response_data: "GraphQLHTTPResponse" - ) -> None: - if not errors: - return - - scope = sentry_sdk.get_isolation_scope() - event_processor = _make_response_event_processor(response_data) - scope.add_event_processor(event_processor) - - with capture_internal_exceptions(): - for error in errors: - event, hint = event_from_exception( - error, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": StrawberryIntegration.identifier, - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - - async_base_view.AsyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_async_view_handle_errors - ) - sync_base_view.SyncBaseHTTPView._handle_errors = ( # type: ignore[method-assign] - _sentry_patched_sync_view_handle_errors - ) - - -def _make_request_event_processor( - execution_context: "ExecutionContext", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - request_data = event.setdefault("request", {}) - request_data["api_target"] = "graphql" - - if not request_data.get("data"): - data: "dict[str, Any]" = {"query": execution_context.query} - if execution_context.variables: - data["variables"] = execution_context.variables - if execution_context.operation_name: - data["operationName"] = execution_context.operation_name - - request_data["data"] = data - - else: - try: - del event["request"]["data"] - except (KeyError, TypeError): - pass - - return event - - return inner - - -def _make_response_event_processor( - response_data: "GraphQLHTTPResponse", -) -> "EventProcessor": - def inner(event: "Event", hint: "dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - if should_send_default_pii(): - contexts = event.setdefault("contexts", {}) - contexts["response"] = {"data": response_data} - - return event - - return inner - - -def _guess_if_using_async(extensions: "List[SchemaExtension]") -> "Optional[bool]": - if StrawberrySentryAsyncExtension in extensions: - return True - elif StrawberrySentrySyncExtension in extensions: - return False - - return None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py deleted file mode 100644 index 4927c0e885..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/sys_exit.py +++ /dev/null @@ -1,65 +0,0 @@ -import functools -import sys - -import sentry_sdk -from sentry_sdk._types import TYPE_CHECKING -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import capture_internal_exceptions, event_from_exception - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import NoReturn, Union - - -class SysExitIntegration(Integration): - """Captures sys.exit calls and sends them as events to Sentry. - - By default, SystemExit exceptions are not captured by the SDK. Enabling this integration will capture SystemExit - exceptions generated by sys.exit calls and send them to Sentry. - - This integration, in its default configuration, only captures the sys.exit call if the exit code is a non-zero and - non-None value (unsuccessful exits). Pass `capture_successful_exits=True` to capture successful exits as well. - Note that the integration does not capture SystemExit exceptions raised outside a call to sys.exit. - """ - - identifier = "sys_exit" - - def __init__(self, *, capture_successful_exits: bool = False) -> None: - self._capture_successful_exits = capture_successful_exits - - @staticmethod - def setup_once() -> None: - SysExitIntegration._patch_sys_exit() - - @staticmethod - def _patch_sys_exit() -> None: - old_exit: "Callable[[Union[str, int, None]], NoReturn]" = sys.exit - - @functools.wraps(old_exit) - def sentry_patched_exit(__status: "Union[str, int, None]" = 0) -> "NoReturn": - # @ensure_integration_enabled ensures that this is non-None - integration = sentry_sdk.get_client().get_integration(SysExitIntegration) - if integration is None: - old_exit(__status) - - try: - old_exit(__status) - except SystemExit as e: - with capture_internal_exceptions(): - if integration._capture_successful_exits or __status not in ( - 0, - None, - ): - _capture_exception(e) - raise e - - sys.exit = sentry_patched_exit - - -def _capture_exception(exc: "SystemExit") -> None: - event, hint = event_from_exception( - exc, - client_options=sentry_sdk.get_client().options, - mechanism={"type": SysExitIntegration.identifier, "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py deleted file mode 100644 index f3ba046332..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/threading.py +++ /dev/null @@ -1,196 +0,0 @@ -import sys -import warnings -from concurrent.futures import Future, ThreadPoolExecutor -from functools import wraps -from threading import Thread, current_thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.scope import use_isolation_scope, use_scope -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import ExcInfo - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T", bound=Any) - - -class ThreadingIntegration(Integration): - identifier = "threading" - - def __init__( - self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True - ) -> None: - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - - self.propagate_scope = propagate_scope - - if propagate_hub is not None: - self.propagate_scope = propagate_hub - - @staticmethod - def setup_once() -> None: - old_start = Thread.start - - try: - from django import VERSION as django_version # noqa: N811 - except ImportError: - django_version = None - - try: - import channels # type: ignore[import-untyped] - - channels_version = channels.__version__ - except (ImportError, AttributeError): - channels_version = None - - is_async_emulated_with_threads = ( - sys.version_info < (3, 9) - and channels_version is not None - and channels_version < "4.0.0" - and django_version is not None - and django_version >= (3, 0) - and django_version < (4, 0) - ) - - @wraps(old_start) - def sentry_start(self: "Thread", *a: "Any", **kw: "Any") -> "Any": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return old_start(self, *a, **kw) - - if integration.propagate_scope: - if is_async_emulated_with_threads: - warnings.warn( - "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. " - "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) " - "Please either upgrade to Django channels 4.0+, use Django's async features " - "available in Django 3.1+ instead of Django channels, or upgrade to Python 3.9+.", - stacklevel=2, - ) - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - - else: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - # Patching instance methods in `start()` creates a reference cycle if - # done in a naive way. See - # https://github.com/getsentry/sentry-python/pull/434 - # - # In threading module, using current_thread API will access current thread instance - # without holding it to avoid a reference cycle in an easier way. - with capture_internal_exceptions(): - new_run = _wrap_run( - isolation_scope, - current_scope, - getattr(self.run, "__func__", self.run), - ) - self.run = new_run # type: ignore - - return old_start(self, *a, **kw) - - Thread.start = sentry_start # type: ignore - ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore - ThreadPoolExecutor.submit, is_async_emulated_with_threads - ) - - -def _wrap_run( - isolation_scope_to_use: "Optional[sentry_sdk.Scope]", - current_scope_to_use: "Optional[sentry_sdk.Scope]", - old_run_func: "F", -) -> "F": - @wraps(old_run_func) - def run(*a: "Any", **kw: "Any") -> "Any": - def _run_old_run_func() -> "Any": - try: - self = current_thread() - return old_run_func(self, *a[1:], **kw) - except Exception: - reraise(*_capture_exception()) - - if isolation_scope_to_use is not None and current_scope_to_use is not None: - with use_isolation_scope(isolation_scope_to_use): - with use_scope(current_scope_to_use): - return _run_old_run_func() - else: - return _run_old_run_func() - - return run # type: ignore - - -def _wrap_threadpool_executor_submit( - func: "Callable[..., Future[T]]", is_async_emulated_with_threads: bool -) -> "Callable[..., Future[T]]": - """ - Wrap submit call to propagate scopes on task submission. - """ - - @wraps(func) - def sentry_submit( - self: "ThreadPoolExecutor", - fn: "Callable[..., T]", - *args: "Any", - **kwargs: "Any", - ) -> "Future[T]": - integration = sentry_sdk.get_client().get_integration(ThreadingIntegration) - if integration is None: - return func(self, fn, *args, **kwargs) - - if integration.propagate_scope and is_async_emulated_with_threads: - isolation_scope = sentry_sdk.get_isolation_scope() - current_scope = sentry_sdk.get_current_scope() - elif integration.propagate_scope: - isolation_scope = sentry_sdk.get_isolation_scope().fork() - current_scope = sentry_sdk.get_current_scope().fork() - else: - isolation_scope = None - current_scope = None - - def wrapped_fn(*args: "Any", **kwargs: "Any") -> "Any": - if isolation_scope is not None and current_scope is not None: - with use_isolation_scope(isolation_scope): - with use_scope(current_scope): - return fn(*args, **kwargs) - - return fn(*args, **kwargs) - - return func(self, wrapped_fn, *args, **kwargs) - - return sentry_submit - - -def _capture_exception() -> "ExcInfo": - exc_info = sys.exc_info() - - client = sentry_sdk.get_client() - if client.get_integration(ThreadingIntegration) is not None: - event, hint = event_from_exception( - exc_info, - client_options=client.options, - mechanism={"type": "threading", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py deleted file mode 100644 index 859b0d0870..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/tornado.py +++ /dev/null @@ -1,320 +0,0 @@ -import contextlib -import weakref -from inspect import iscoroutinefunction - -import sentry_sdk -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.integrations._wsgi_common import ( - RequestExtractor, - _filter_headers, - _is_json_content_type, - request_body_within_bounds, -) -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - AnnotatedValue, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - transaction_from_function, -) - -try: - from tornado import version_info as TORNADO_VERSION - from tornado.gen import coroutine - from tornado.web import HTTPError, RequestHandler -except ImportError: - raise DidNotEnable("Tornado not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.tracing import Span - - -class TornadoIntegration(Integration): - identifier = "tornado" - origin = f"auto.http.{identifier}" - - @staticmethod - def setup_once() -> None: - _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - - ignore_logger("tornado.access") - - old_execute = RequestHandler._execute - - awaitable = iscoroutinefunction(old_execute) - - if awaitable: - # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) - # In that case our method should be a coroutine function too - async def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - return await old_execute(self, *args, **kwargs) - - else: - - @coroutine # type: ignore - def sentry_execute_request_handler( - self: "RequestHandler", *args: "Any", **kwargs: "Any" - ) -> "Any": - with _handle_request_impl(self): - result = yield from old_execute(self, *args, **kwargs) - return result - - RequestHandler._execute = sentry_execute_request_handler - - old_log_exception = RequestHandler.log_exception - - def sentry_log_exception( - self: "Any", - ty: type, - value: BaseException, - tb: "Any", - *args: "Any", - **kwargs: "Any", - ) -> "Optional[Any]": - _capture_exception(ty, value, tb) - return old_log_exception(self, ty, value, tb, *args, **kwargs) - - RequestHandler.log_exception = sentry_log_exception - - -_DEFAULT_ROOT_SPAN_NAME = "generic Tornado request" - - -@contextlib.contextmanager -def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": - integration = sentry_sdk.get_client().get_integration(TornadoIntegration) - - if integration is None: - yield - return - - weak_handler = weakref.ref(self) - client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) - - with sentry_sdk.isolation_scope() as scope: - headers = self.request.headers - - scope.clear_breadcrumbs() - processor = _make_event_processor(weak_handler) - scope.add_event_processor(processor) - - span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - - if is_span_streaming_enabled: - sentry_sdk.traces.continue_trace(dict(headers)) - scope.set_custom_sampling_context({"tornado_request": self.request}) - - if should_send_default_pii() and self.request.remote_ip: - scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_ROOT_SPAN_NAME, - attributes={ - "sentry.op": OP.HTTP_SERVER, - "sentry.origin": TornadoIntegration.origin, - "sentry.span.source": SegmentSource.ROUTE, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # Like with all other integrations, this is our - # fallback transaction in case there is no route. - # sentry_urldispatcher_resolve is responsible for - # setting a transaction name later. - name=_DEFAULT_ROOT_SPAN_NAME, - source=TransactionSource.ROUTE, - origin=TornadoIntegration.origin, - ) - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"tornado_request": self.request}, - ) - - with span_ctx as span: - try: - yield - finally: - if type(span) is StreamedSpan: - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - self.request - ).items(): - span.set_attribute(attr, value) - - with capture_internal_exceptions(): - method = getattr(self, self.request.method.lower(), None) - if method is not None: - span_name = transaction_from_function(method) - if span_name: - span.name = span_name - span.set_attribute( - "sentry.span.source", - SegmentSource.COMPONENT, - ) - - with capture_internal_exceptions(): - status_int = self.get_status() - span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) - span.status = "error" if status_int >= 400 else "ok" - - -def _get_request_attributes(request: "Any") -> "Dict[str, Any]": - attributes = {} # type: Dict[str, Any] - - if request.method: - attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() - - headers = _filter_headers(dict(request.headers), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value - - if should_send_default_pii(): - attributes[SPANDATA.URL_FULL] = request.full_url() - attributes["url.path"] = request.path - - if request.query: - attributes[SPANDATA.URL_QUERY] = request.query - - if request.protocol: - attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol - - if should_send_default_pii() and request.remote_ip: - attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip - - with capture_internal_exceptions(): - raw_data = _get_tornado_request_data(request) - body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data - if body_data is not None: - attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data - - return attributes - - -def _get_tornado_request_data( - request: "Any", -) -> "Union[Optional[str], AnnotatedValue]": - body = request.body - if not body: - return None - - if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): - return AnnotatedValue.substituted_because_over_size_limit() - - return body.decode("utf-8", "replace") - - -@ensure_integration_enabled(TornadoIntegration) -def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: - if isinstance(value, HTTPError): - return - - event, hint = event_from_exception( - (ty, value, tb), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "tornado", "handled": False}, - ) - - sentry_sdk.capture_event(event, hint=hint) - - -def _make_event_processor( - weak_handler: "Callable[[], RequestHandler]", -) -> "EventProcessor": - def tornado_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - handler = weak_handler() - if handler is None: - return event - - request = handler.request - - with capture_internal_exceptions(): - method = getattr(handler, handler.request.method.lower()) - event["transaction"] = transaction_from_function(method) or "" - event["transaction_info"] = {"source": TransactionSource.COMPONENT} - - with capture_internal_exceptions(): - extractor = TornadoRequestExtractor(request) - extractor.extract_into_event(event) - - request_info = event["request"] - - request_info["url"] = "%s://%s%s" % ( - request.protocol, - request.host, - request.path, - ) - - request_info["query_string"] = request.query - request_info["method"] = request.method - request_info["env"] = {"REMOTE_ADDR": request.remote_ip} - request_info["headers"] = _filter_headers(dict(request.headers)) - - if should_send_default_pii(): - try: - current_user = handler.current_user - except Exception: - current_user = None - - if current_user: - event.setdefault("user", {}).setdefault("is_authenticated", True) - - return event - - return tornado_processor - - -class TornadoRequestExtractor(RequestExtractor): - def content_length(self) -> int: - if self.request.body is None: - return 0 - return len(self.request.body) - - def cookies(self) -> "Dict[str, str]": - return {k: v.value for k, v in self.request.cookies.items()} - - def raw_data(self) -> bytes: - return self.request.body - - def form(self) -> "Dict[str, Any]": - return { - k: [v.decode("latin1", "replace") for v in vs] - for k, vs in self.request.body_arguments.items() - } - - def is_json(self) -> bool: - return _is_json_content_type(self.request.headers.get("content-type")) - - def files(self) -> "Dict[str, Any]": - return {k: v[0] for k, v in self.request.files.items() if v} - - def size_of_file(self, file: "Any") -> int: - return len(file.body or ()) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py deleted file mode 100644 index 0449a8f10c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/trytond.py +++ /dev/null @@ -1,52 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.utils import ensure_integration_enabled, event_from_exception - -try: - from trytond.exceptions import TrytonException # type: ignore - from trytond.wsgi import app # type: ignore -except ImportError: - raise DidNotEnable("Trytond is not installed.") - -# TODO: trytond-worker, trytond-cron and trytond-admin intergations - - -class TrytondWSGIIntegration(Integration): - identifier = "trytond_wsgi" - origin = f"auto.http.{identifier}" - - def __init__(self) -> None: - pass - - @staticmethod - def setup_once() -> None: - app.wsgi_app = SentryWsgiMiddleware( - app.wsgi_app, - span_origin=TrytondWSGIIntegration.origin, - ) - - @ensure_integration_enabled(TrytondWSGIIntegration) - def error_handler(e: Exception) -> None: - if isinstance(e, TrytonException): - return - else: - client = sentry_sdk.get_client() - event, hint = event_from_exception( - e, - client_options=client.options, - mechanism={"type": "trytond", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - # Expected error handlers signature was changed - # when the error_handler decorator was introduced - # in Tryton-5.4 - if hasattr(app, "error_handler"): - - @app.error_handler - def _(app, request, e): # type: ignore - error_handler(e) - - else: - app.error_handlers.append(error_handler) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py deleted file mode 100644 index 497f0539ec..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/typer.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Optional, Type - - Excepthook = Callable[ - [Type[BaseException], BaseException, Optional[TracebackType]], - Any, - ] - -try: - import typer - from typer.main import except_hook -except ImportError: - raise DidNotEnable("Typer not installed") - - -class TyperIntegration(Integration): - identifier = "typer" - - @staticmethod - def setup_once() -> None: - typer.main.except_hook = _make_excepthook(except_hook) # type: ignore - - -def _make_excepthook(old_excepthook: "Excepthook") -> "Excepthook": - def sentry_sdk_excepthook( - type_: "Type[BaseException]", - value: BaseException, - traceback: "Optional[TracebackType]", - ) -> None: - integration = sentry_sdk.get_client().get_integration(TyperIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_excepthook(type_, value, traceback) - - with capture_internal_exceptions(): - event, hint = event_from_exception( - (type_, value, traceback), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "typer", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_excepthook(type_, value, traceback) - - return sentry_sdk_excepthook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py deleted file mode 100644 index 0316f6b88a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unleash.py +++ /dev/null @@ -1,33 +0,0 @@ -from functools import wraps -from typing import Any - -from sentry_sdk.feature_flags import add_feature_flag -from sentry_sdk.integrations import DidNotEnable, Integration - -try: - from UnleashClient import UnleashClient -except ImportError: - raise DidNotEnable("UnleashClient is not installed") - - -class UnleashIntegration(Integration): - identifier = "unleash" - - @staticmethod - def setup_once() -> None: - # Wrap and patch evaluation methods (class methods) - old_is_enabled = UnleashClient.is_enabled - - @wraps(old_is_enabled) - def sentry_is_enabled( - self: "UnleashClient", feature: str, *args: "Any", **kwargs: "Any" - ) -> "Any": - enabled = old_is_enabled(self, feature, *args, **kwargs) - - # We have no way of knowing what type of unleash feature this is, so we have to treat - # it as a boolean / toggle feature. - add_feature_flag(feature, enabled) - - return enabled - - UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py deleted file mode 100644 index 2c7280a1f2..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/unraisablehook.py +++ /dev/null @@ -1,50 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.integrations import Integration -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, -) - -if TYPE_CHECKING: - from typing import Any, Callable - - -class UnraisablehookIntegration(Integration): - identifier = "unraisablehook" - - @staticmethod - def setup_once() -> None: - sys.unraisablehook = _make_unraisable(sys.unraisablehook) - - -def _make_unraisable( - old_unraisablehook: "Callable[[sys.UnraisableHookArgs], Any]", -) -> "Callable[[sys.UnraisableHookArgs], Any]": - def sentry_sdk_unraisablehook(unraisable: "sys.UnraisableHookArgs") -> None: - integration = sentry_sdk.get_client().get_integration(UnraisablehookIntegration) - - # Note: If we replace this with ensure_integration_enabled then - # we break the exceptiongroup backport; - # See: https://github.com/getsentry/sentry-python/issues/3097 - if integration is None: - return old_unraisablehook(unraisable) - - if unraisable.exc_value and unraisable.exc_traceback: - with capture_internal_exceptions(): - event, hint = event_from_exception( - ( - unraisable.exc_type, - unraisable.exc_value, - unraisable.exc_traceback, - ), - client_options=sentry_sdk.get_client().options, - mechanism={"type": "unraisablehook", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return old_unraisablehook(unraisable) - - return sentry_sdk_unraisablehook diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py deleted file mode 100644 index e776ed915a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/integrations/wsgi.py +++ /dev/null @@ -1,427 +0,0 @@ -import sys -from functools import partial -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._werkzeug import _get_headers, get_host -from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations._wsgi_common import ( - DEFAULT_HTTP_METHODS_TO_CAPTURE, - _filter_headers, - nullcontext, -) -from sentry_sdk.scope import Scope, should_send_default_pii, use_isolation_scope -from sentry_sdk.sessions import track_session -from sentry_sdk.traces import SegmentSource, StreamedSpan -from sentry_sdk.tracing import Span, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exceptions, - event_from_exception, - reraise, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Iterator, - Optional, - Protocol, - Tuple, - TypeVar, - Union, - ) - - from sentry_sdk._types import Event, EventProcessor - from sentry_sdk.utils import ExcInfo - - WsgiResponseIter = TypeVar("WsgiResponseIter") - WsgiResponseHeaders = TypeVar("WsgiResponseHeaders") - WsgiExcInfo = TypeVar("WsgiExcInfo") - - class StartResponse(Protocol): - def __call__( - self, - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, - ) -> "WsgiResponseIter": # type: ignore - pass - - -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") -_DEFAULT_TRANSACTION_NAME = "generic WSGI request" - - -def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str: - return s.encode("latin1").decode(charset, errors) - - -def get_request_url( - environ: "Dict[str, str]", use_x_forwarded_for: bool = False -) -> str: - """Return the absolute URL without query string for the given WSGI - environment.""" - script_name = environ.get("SCRIPT_NAME", "").rstrip("/") - path_info = environ.get("PATH_INFO", "").lstrip("/") - path = f"{script_name}/{path_info}" - - scheme = environ.get("wsgi.url_scheme") - if use_x_forwarded_for: - scheme = environ.get("HTTP_X_FORWARDED_PROTO", scheme) - - return "%s://%s/%s" % ( - scheme, - get_host(environ, use_x_forwarded_for), - wsgi_decoding_dance(path).lstrip("/"), - ) - - -class SentryWsgiMiddleware: - __slots__ = ( - "app", - "use_x_forwarded_for", - "span_origin", - "http_methods_to_capture", - ) - - def __init__( - self, - app: "Callable[[Dict[str, str], Callable[..., Any]], Any]", - use_x_forwarded_for: bool = False, - span_origin: str = "manual", - http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, - ) -> None: - self.app = app - self.use_x_forwarded_for = use_x_forwarded_for - self.span_origin = span_origin - self.http_methods_to_capture = http_methods_to_capture - - def __call__( - self, environ: "Dict[str, str]", start_response: "Callable[..., Any]" - ) -> "Any": - if _wsgi_middleware_applied.get(False): - return self.app(environ, start_response) - - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - - _wsgi_middleware_applied.set(True) - try: - with sentry_sdk.isolation_scope() as scope: - with track_session(scope, session_mode="request"): - with capture_internal_exceptions(): - scope.clear_breadcrumbs() - scope._name = "wsgi" - scope.add_event_processor( - _make_wsgi_event_processor( - environ, self.use_x_forwarded_for - ) - ) - - method = environ.get("REQUEST_METHOD", "").upper() - - span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None - if method in self.http_methods_to_capture: - if span_streaming: - sentry_sdk.traces.continue_trace( - dict(_get_headers(environ)) - ) - Scope.set_custom_sampling_context({"wsgi_environ": environ}) - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, client_ip - ) - - span_ctx = sentry_sdk.traces.start_span( - name=_DEFAULT_TRANSACTION_NAME, - attributes={ - "sentry.span.source": SegmentSource.ROUTE, - "sentry.origin": self.span_origin, - "sentry.op": OP.HTTP_SERVER, - }, - parent_span=None, - ) - else: - transaction = continue_trace( - environ, - op=OP.HTTP_SERVER, - name=_DEFAULT_TRANSACTION_NAME, - source=TransactionSource.ROUTE, - origin=self.span_origin, - ) - - span_ctx = sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"wsgi_environ": environ}, - ) - - span_ctx = span_ctx or nullcontext() - - with span_ctx as span: - if isinstance(span, StreamedSpan): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes( - environ, self.use_x_forwarded_for - ).items(): - span.set_attribute(attr, value) - - try: - response = self.app( - environ, - partial(_sentry_start_response, start_response, span), - ) - except BaseException: - reraise(*_capture_exception()) - finally: - _wsgi_middleware_applied.set(False) - - # Within the uWSGI subhandler, the use of the "offload" mechanism for file responses - # is determined by a pointer equality check on the response object - # (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278). - # - # If we were to return a _ScopedResponse, this would cause the check to always fail - # since it's checking the files are exactly the same. - # - # To avoid this and ensure that the offloading mechanism works as expected when it's - # enabled, we check if the response is a file-like object (determined by the presence - # of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so, - # it would've been used in handling the file in the response). - # - # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, - # so we want to make sure we don't interfere with those either. - # - # If all conditions are met, we return the original response object directly, - # allowing uWSGI to handle it as intended. - if ( - environ.get("wsgi.file_wrapper") - and getattr(response, "fileno", None) is not None - ): - return response - - return _ScopedResponse(scope, response) - - -def _sentry_start_response( - old_start_response: "StartResponse", - span: "Optional[Union[Span, StreamedSpan]]", - status: str, - response_headers: "WsgiResponseHeaders", - exc_info: "Optional[WsgiExcInfo]" = None, -) -> "WsgiResponseIter": # type: ignore[type-var] - with capture_internal_exceptions(): - status_int = int(status.split(" ", 1)[0]) - if span is not None: - if isinstance(span, StreamedSpan): - span.status = "error" if status_int >= 400 else "ok" - span.set_attribute("http.response.status_code", status_int) - else: - span.set_http_status(status_int) - - if exc_info is None: - # The Django Rest Framework WSGI test client, and likely other - # (incorrect) implementations, cannot deal with the exc_info argument - # if one is present. Avoid providing a third argument if not necessary. - return old_start_response(status, response_headers) - else: - return old_start_response(status, response_headers, exc_info) - - -def _get_environ(environ: "Dict[str, str]") -> "Iterator[Tuple[str, str]]": - """ - Returns our explicitly included environment variables we want to - capture (server name, port and remote addr if pii is enabled). - """ - keys = ["SERVER_NAME", "SERVER_PORT"] - if should_send_default_pii(): - # make debugging of proxy setup easier. Proxy headers are - # in headers. - keys += ["REMOTE_ADDR"] - - for key in keys: - if key in environ: - yield key, environ[key] - - -def get_client_ip(environ: "Dict[str, str]") -> "Optional[Any]": - """ - Infer the user IP address from various headers. This cannot be used in - security sensitive situations since the value may be forged from a client, - but it's good enough for the event payload. - """ - try: - return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() - except (KeyError, IndexError): - pass - - try: - return environ["HTTP_X_REAL_IP"] - except KeyError: - pass - - return environ.get("REMOTE_ADDR") - - -def _capture_exception() -> "ExcInfo": - """ - Captures the current exception and sends it to Sentry. - Returns the ExcInfo tuple to it can be reraised afterwards. - """ - exc_info = sys.exc_info() - e = exc_info[1] - - # SystemExit(0) is the only uncaught exception that is expected behavior - should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None) - if not should_skip_capture: - event, hint = event_from_exception( - exc_info, - client_options=sentry_sdk.get_client().options, - mechanism={"type": "wsgi", "handled": False}, - ) - sentry_sdk.capture_event(event, hint=hint) - - return exc_info - - -class _ScopedResponse: - """ - Users a separate scope for each response chunk. - - This will make WSGI apps more tolerant against: - - WSGI servers streaming responses from a different thread/from - different threads than the one that called start_response - - close() not being called - - WSGI servers streaming responses interleaved from the same thread - """ - - __slots__ = ("_response", "_scope") - - def __init__( - self, scope: "sentry_sdk.scope.Scope", response: "Iterator[bytes]" - ) -> None: - self._scope = scope - self._response = response - - def __iter__(self) -> "Iterator[bytes]": - iterator = iter(self._response) - - while True: - with use_isolation_scope(self._scope): - try: - chunk = next(iterator) - except StopIteration: - break - except BaseException: - reraise(*_capture_exception()) - - yield chunk - - def close(self) -> None: - with use_isolation_scope(self._scope): - try: - self._response.close() # type: ignore - except AttributeError: - pass - except BaseException: - reraise(*_capture_exception()) - - -def _make_wsgi_event_processor( - environ: "Dict[str, str]", use_x_forwarded_for: bool -) -> "EventProcessor": - # It's a bit unfortunate that we have to extract and parse the request data - # from the environ so eagerly, but there are a few good reasons for this. - # - # We might be in a situation where the scope never gets torn down - # properly. In that case we will have an unnecessary strong reference to - # all objects in the environ (some of which may take a lot of memory) when - # we're really just interested in a few of them. - # - # Keeping the environment around for longer than the request lifecycle is - # also not necessarily something uWSGI can deal with: - # https://github.com/unbit/uwsgi/issues/1950 - - client_ip = get_client_ip(environ) - request_url = get_request_url(environ, use_x_forwarded_for) - query_string = environ.get("QUERY_STRING") - method = environ.get("REQUEST_METHOD") - env = dict(_get_environ(environ)) - headers = _filter_headers(dict(_get_headers(environ))) - - def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": - with capture_internal_exceptions(): - # if the code below fails halfway through we at least have some data - request_info = event.setdefault("request", {}) - - if should_send_default_pii(): - user_info = event.setdefault("user", {}) - if client_ip: - user_info.setdefault("ip_address", client_ip) - - request_info["url"] = request_url - request_info["query_string"] = query_string - request_info["method"] = method - request_info["env"] = env - request_info["headers"] = headers - - return event - - return event_processor - - -def _get_request_attributes( - environ: "Dict[str, str]", - use_x_forwarded_for: bool = False, -) -> "Dict[str, Any]": - """ - Return span attributes related to the HTTP request from the WSGI environ. - """ - attributes: "dict[str, Any]" = {} - - method = environ.get("REQUEST_METHOD") - if method: - attributes["http.request.method"] = method.upper() - - headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False) - for header, value in headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value - - url_scheme = environ.get("wsgi.url_scheme") - if url_scheme: - attributes["network.protocol.name"] = url_scheme - - server_name = environ.get("SERVER_NAME") - if server_name: - attributes["server.address"] = server_name - - server_port = environ.get("SERVER_PORT") - if server_port: - try: - attributes["server.port"] = int(server_port) - except ValueError: - pass - - if should_send_default_pii(): - client_ip = get_client_ip(environ) - if client_ip: - attributes["client.address"] = client_ip - - query_string = environ.get("QUERY_STRING") - if query_string: - attributes["http.query"] = query_string - - path = environ.get("PATH_INFO", "") - if path: - attributes["url.path"] = path - - attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) - - return attributes diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py deleted file mode 100644 index d7f4425dfd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/logger.py +++ /dev/null @@ -1,88 +0,0 @@ -# NOTE: this is the logger sentry exposes to users, not some generic logger. -import functools -import time -from typing import TYPE_CHECKING, Any - -import sentry_sdk -from sentry_sdk.utils import capture_internal_exceptions, format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes - - -OTEL_RANGES = [ - # ((severity level range), severity text) - # https://opentelemetry.io/docs/specs/otel/logs/data-model - ((1, 4), "trace"), - ((5, 8), "debug"), - ((9, 12), "info"), - ((13, 16), "warn"), - ((17, 20), "error"), - ((21, 24), "fatal"), -] - - -class _dict_default_key(dict): # type: ignore[type-arg] - """dict that returns the key if missing.""" - - def __missing__(self, key: str) -> str: - return "{" + key + "}" - - -def _capture_log( - severity_text: str, severity_number: int, template: str, **kwargs: "Any" -) -> None: - body = template - - attributes: "Attributes" = {} - - if "attributes" in kwargs: - provided_attributes = kwargs.pop("attributes") or {} - for attribute, value in provided_attributes.items(): - attributes[attribute] = format_attribute(value) - - for k, v in kwargs.items(): - attributes[f"sentry.message.parameter.{k}"] = format_attribute(v) - - if kwargs: - # only attach template if there are parameters - attributes["sentry.message.template"] = format_attribute(template) - - with capture_internal_exceptions(): - body = template.format_map(_dict_default_key(kwargs)) - - sentry_sdk.get_current_scope()._capture_log( - { - "severity_text": severity_text, - "severity_number": severity_number, - "attributes": attributes, - "body": body, - "time_unix_nano": time.time_ns(), - "trace_id": None, - "span_id": None, - } - ) - - -trace = functools.partial(_capture_log, "trace", 1) -debug = functools.partial(_capture_log, "debug", 5) -info = functools.partial(_capture_log, "info", 9) -warning = functools.partial(_capture_log, "warn", 13) -error = functools.partial(_capture_log, "error", 17) -fatal = functools.partial(_capture_log, "fatal", 21) - - -def _otel_severity_text(otel_severity_number: int) -> str: - for (lower, upper), severity in OTEL_RANGES: - if lower <= otel_severity_number <= upper: - return severity - - return "default" - - -def _log_level_to_otel(level: int, mapping: "dict[Any, int]") -> "tuple[int, str]": - for py_level, otel_severity_number in sorted(mapping.items(), reverse=True): - if level >= py_level: - return otel_severity_number, _otel_severity_text(otel_severity_number) - - return 0, "default" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py deleted file mode 100644 index 27b7468c0d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/metrics.py +++ /dev/null @@ -1,62 +0,0 @@ -import time -from typing import TYPE_CHECKING, Any, Optional - -import sentry_sdk -from sentry_sdk.utils import format_attribute - -if TYPE_CHECKING: - from sentry_sdk._types import Attributes, Metric, MetricType - - -def _capture_metric( - name: str, - metric_type: "MetricType", - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[Attributes]" = None, -) -> None: - attrs: "Attributes" = {} - - if attributes: - for k, v in attributes.items(): - attrs[k] = format_attribute(v) - - metric: "Metric" = { - "timestamp": time.time(), - "trace_id": None, - "span_id": None, - "name": name, - "type": metric_type, - "value": float(value), - "unit": unit, - "attributes": attrs, - } - - sentry_sdk.get_current_scope()._capture_metric(metric) - - -def count( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "counter", value, unit, attributes) - - -def gauge( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "gauge", value, unit, attributes) - - -def distribution( - name: str, - value: float, - unit: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> None: - _capture_metric(name, "distribution", value, unit, attributes) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py deleted file mode 100644 index d2ba298c35..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/monitor.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import time -import weakref -from threading import Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Optional - - -MAX_DOWNSAMPLE_FACTOR = 10 - - -class Monitor: - """ - Performs health checks in a separate thread once every interval seconds - and updates the internal state. Other parts of the SDK only read this state - and act accordingly. - """ - - name = "sentry.monitor" - - _thread: "Optional[Thread]" - _thread_for_pid: "Optional[int]" - - def __init__( - self, transport: "sentry_sdk.transport.Transport", interval: float = 10 - ) -> None: - self.transport: "sentry_sdk.transport.Transport" = transport - self.interval: float = interval - - self._healthy = True - self._downsample_factor: int = 0 - self._running = True - self._reset_thread_state() - - # See https://github.com/getsentry/sentry-python/issues/6148. - # If os.fork() runs while another thread holds self._thread_lock, - # the child inherits the lock locked but the holding thread does - # not exist in the child, so the lock can never be released and - # _ensure_running deadlocks forever. Reinitialise the lock and - # cached thread/pid in the child so it starts clean regardless - # of inherited state. We bind via a WeakMethod so the - # permanently-registered fork handler does not pin this Monitor - # (and its Transport): register_at_fork has no unregister API. - # POSIX-only; Windows uses spawn. - if hasattr(os, "register_at_fork"): - weak_reset = weakref.WeakMethod(self._reset_thread_state) - - def _reset_in_child() -> None: - method = weak_reset() - if method is not None: - method() - - os.register_at_fork(after_in_child=_reset_in_child) - - def _reset_thread_state(self) -> None: - self._thread = None - self._thread_lock = Lock() - self._thread_for_pid = None - - def _ensure_running(self) -> None: - """ - Check that the monitor has an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - while self._running: - time.sleep(self.interval) - if self._running: - self.run() - - thread = Thread(name=self.name, target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self._running = False - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def run(self) -> None: - self.check_health() - self.set_downsample_factor() - - def set_downsample_factor(self) -> None: - if self._healthy: - if self._downsample_factor > 0: - logger.debug( - "[Monitor] health check positive, reverting to normal sampling" - ) - self._downsample_factor = 0 - else: - if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR: - self._downsample_factor += 1 - logger.debug( - "[Monitor] health check negative, downsampling with a factor of %d", - self._downsample_factor, - ) - - def check_health(self) -> None: - """ - Perform the actual health checks, - currently only checks if the transport is rate-limited. - TODO: augment in the future with more checks. - """ - self._healthy = self.transport.is_healthy() - - def is_healthy(self) -> bool: - self._ensure_running() - return self._healthy - - @property - def downsample_factor(self) -> int: - self._ensure_running() - return self._downsample_factor - - def kill(self) -> None: - self._running = False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py deleted file mode 100644 index d562405295..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -from sentry_sdk.profiler.continuous_profiler import ( - start_profile_session, - start_profiler, - stop_profile_session, - stop_profiler, -) -from sentry_sdk.profiler.transaction_profiler import ( - MAX_PROFILE_DURATION_NS, - PROFILE_MINIMUM_SAMPLES, - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - has_profiling_enabled, - setup_profiler, - teardown_profiler, -) -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - MAX_STACK_DEPTH, - extract_frame, - extract_stack, - frame_id, - get_frame_name, -) - -__all__ = [ - "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` - "start_profiler", - "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` - "stop_profiler", - # DEPRECATED: The following was re-exported for backwards compatibility. It - # will be removed from sentry_sdk.profiler in a future release. - "MAX_PROFILE_DURATION_NS", - "PROFILE_MINIMUM_SAMPLES", - "Profile", - "Scheduler", - "ThreadScheduler", - "GeventScheduler", - "has_profiling_enabled", - "setup_profiler", - "teardown_profiler", - "DEFAULT_SAMPLING_FREQUENCY", - "MAX_STACK_DEPTH", - "get_frame_name", - "extract_frame", - "extract_stack", - "frame_id", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py deleted file mode 100644 index ed525f52bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/continuous_profiler.py +++ /dev/null @@ -1,703 +0,0 @@ -import atexit -import os -import random -import sys -import threading -import time -import uuid -import warnings -from collections import deque -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.consts import VERSION -from sentry_sdk.envelope import Envelope -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - is_gevent, - logger, - now, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type, Union - - from typing_extensions import TypedDict - - from sentry_sdk._types import ContinuousProfilerMode, SDKInfo - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "timestamp": float, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - ThreadPool = None - - -_scheduler: "Optional[ContinuousScheduler]" = None - - -def setup_continuous_profiler( - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", -) -> bool: - global _scheduler - - already_initialized = _scheduler is not None - - if already_initialized: - logger.debug("[Profiling] Continuous Profiler is already setup") - teardown_continuous_profiler() - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventContinuousScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventContinuousScheduler.mode - else: - default_profiler_mode = ThreadContinuousScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - # TODO: deprecate this and just use the existing `profiler_mode` - experiments = options.get("_experiments", {}) - - profiler_mode = ( - experiments.get("continuous_profiling_mode") or default_profiler_mode - ) - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if profiler_mode == ThreadContinuousScheduler.mode: - _scheduler = ThreadContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - elif profiler_mode == GeventContinuousScheduler.mode: - _scheduler = GeventContinuousScheduler( - frequency, options, sdk_info, capture_func - ) - else: - raise ValueError("Unknown continuous profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up continuous profiler in {mode} mode".format( - mode=_scheduler.mode - ) - ) - - if not already_initialized: - atexit.register(teardown_continuous_profiler) - - return True - - -def is_profile_session_sampled() -> bool: - if _scheduler is None: - return False - return _scheduler.sampled - - -def try_autostart_continuous_profiler() -> None: - # TODO: deprecate this as it'll be replaced by the auto lifecycle option - - if _scheduler is None: - return - - if not _scheduler.is_auto_start_enabled(): - return - - _scheduler.manual_start() - - -def try_profile_lifecycle_trace_start() -> "Union[ContinuousProfile, None]": - if _scheduler is None: - return None - - return _scheduler.auto_start() - - -def start_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_start() - - -def start_profile_session() -> None: - warnings.warn( - "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - start_profiler() - - -def stop_profiler() -> None: - if _scheduler is None: - return - - _scheduler.manual_stop() - - -def stop_profile_session() -> None: - warnings.warn( - "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", - DeprecationWarning, - stacklevel=2, - ) - stop_profiler() - - -def teardown_continuous_profiler() -> None: - stop_profiler() - - global _scheduler - _scheduler = None - - -def get_profiler_id() -> "Union[str, None]": - if _scheduler is None: - return None - return _scheduler.profiler_id - - -def determine_profile_session_sampling_decision( - sample_rate: "Union[float, None]", -) -> bool: - # `None` is treated as `0.0` - if not sample_rate: - return False - - return random.random() < float(sample_rate) - - -class ContinuousProfile: - active: bool = True - - def stop(self) -> None: - self.active = False - - -class ContinuousScheduler: - mode: "ContinuousProfilerMode" = "unknown" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.interval = 1.0 / frequency - self.options = options - self.sdk_info = sdk_info - self.capture_func = capture_func - - self.lifecycle = self.options.get("profile_lifecycle") - profile_session_sample_rate = self.options.get("profile_session_sample_rate") - self.sampled = determine_profile_session_sampling_decision( - profile_session_sample_rate - ) - - self.sampler = self.make_sampler() - self.buffer: "Optional[ProfileBuffer]" = None - self.pid: "Optional[int]" = None - - self.running = False - self.soft_shutdown = False - - self.new_profiles: "Deque[ContinuousProfile]" = deque(maxlen=128) - self.active_profiles: "Set[ContinuousProfile]" = set() - - def is_auto_start_enabled(self) -> bool: - # Ensure that the scheduler only autostarts once per process. - # This is necessary because many web servers use forks to spawn - # additional processes. And the profiler is only spawned on the - # master process, then it often only profiles the main process - # and not the ones where the requests are being handled. - if self.pid == os.getpid(): - return False - - experiments = self.options.get("_experiments") - if not experiments: - return False - - return experiments.get("continuous_profiling_auto_start") - - def auto_start(self) -> "Union[ContinuousProfile, None]": - if not self.sampled: - return None - - if self.lifecycle != "trace": - return None - - logger.debug("[Profiling] Auto starting profiler") - - profile = ContinuousProfile() - - self.new_profiles.append(profile) - self.ensure_running() - - return profile - - def manual_start(self) -> None: - if not self.sampled: - return - - if self.lifecycle != "manual": - return - - self.ensure_running() - - def manual_stop(self) -> None: - if self.lifecycle != "manual": - return - - self.teardown() - - def ensure_running(self) -> None: - raise NotImplementedError - - def teardown(self) -> None: - raise NotImplementedError - - def pause(self) -> None: - raise NotImplementedError - - def reset_buffer(self) -> None: - self.buffer = ProfileBuffer( - self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func - ) - - @property - def profiler_id(self) -> "Union[str, None]": - if not self.running or self.buffer is None: - return None - return self.buffer.profiler_id - - def make_sampler(self) -> "Callable[..., bool]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - if self.lifecycle == "trace": - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - return True - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - inactive_profiles = [] - - for profile in self.active_profiles: - if not profile.active: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - else: - - def _sample_stack(*args: "Any", **kwargs: "Any") -> bool: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - - ts = now() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return False - - if self.buffer is not None: - self.buffer.write(ts, sample) - - return False - - return _sample_stack - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.soft_shutdown = self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # the soft shutdown happens here to give it a chance - # for the profiler to be reused - if self.soft_shutdown: - self.running = False - - # make sure to explicitly exit the profiler here or there might - # be multiple profilers at once - break - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - buffer = self.buffer - if buffer is not None: - buffer.flush() - - -class ThreadContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ContinuousProfilerMode" = "thread" - name = "sentry.profiler.ThreadContinuousScheduler" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[threading.Thread]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -class GeventContinuousScheduler(ContinuousScheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ContinuousProfilerMode" = "gevent" - - def __init__( - self, - frequency: int, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - capture_func: "Callable[[Envelope], None]", - ) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency, options, sdk_info, capture_func) - - self.thread: "Optional[_ThreadPool]" = None - self.lock = threading.Lock() - - def ensure_running(self) -> None: - self.soft_shutdown = False - - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # if the profiler thread is changing, - # we should create a new buffer along with it - self.reset_buffer() - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - - def teardown(self) -> None: - if self.running: - self.running = False - - if self.thread is not None: - self.thread.join() - self.thread = None - - -PROFILE_BUFFER_SECONDS = 60 - - -class ProfileBuffer: - def __init__( - self, - options: "Dict[str, Any]", - sdk_info: "SDKInfo", - buffer_size: int, - capture_func: "Callable[[Envelope], None]", - ) -> None: - self.options = options - self.sdk_info = sdk_info - self.buffer_size = buffer_size - self.capture_func = capture_func - - self.profiler_id = uuid.uuid4().hex - self.chunk = ProfileChunk() - - # Make sure to use the same clock to compute a sample's monotonic timestamp - # to ensure the timestamps are correctly aligned. - self.start_monotonic_time = now() - - # Make sure the start timestamp is defined only once per profiler id. - # This prevents issues with clock drift within a single profiler session. - # - # Subtracting the start_monotonic_time here to find a fixed starting position - # for relative monotonic timestamps for each sample. - self.start_timestamp = ( - datetime.now(timezone.utc).timestamp() - self.start_monotonic_time - ) - - def write(self, monotonic_time: float, sample: "ExtractedSample") -> None: - if self.should_flush(monotonic_time): - self.flush() - self.chunk = ProfileChunk() - self.start_monotonic_time = now() - - self.chunk.write(self.start_timestamp + monotonic_time, sample) - - def should_flush(self, monotonic_time: float) -> bool: - # If the delta between the new monotonic time and the start monotonic time - # exceeds the buffer size, it means we should flush the chunk - return monotonic_time - self.start_monotonic_time >= self.buffer_size - - def flush(self) -> None: - chunk = self.chunk.to_json(self.profiler_id, self.options, self.sdk_info) - envelope = Envelope() - envelope.add_profile_chunk(chunk) - self.capture_func(envelope) - - -class ProfileChunk: - def __init__(self) -> None: - self.chunk_id = uuid.uuid4().hex - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - def write(self, ts: float, sample: "ExtractedSample") -> None: - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "timestamp": ts, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def to_json( - self, profiler_id: str, options: "Dict[str, Any]", sdk_info: "SDKInfo" - ) -> "Dict[str, Any]": - profile = { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - }, - } - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - payload = { - "chunk_id": self.chunk_id, - "client_sdk": { - "name": sdk_info["name"], - "version": VERSION, - }, - "platform": "python", - "profile": profile, - "profiler_id": profiler_id, - "version": "2", - } - - for key in "release", "environment", "dist": - if options[key] is not None: - payload[key] = str(options[key]).strip() - - return payload diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py deleted file mode 100644 index fe65774c0b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/transaction_profiler.py +++ /dev/null @@ -1,802 +0,0 @@ -""" -This file is originally based on code from https://github.com/nylas/nylas-perftools, -which is published under the following license: - -The MIT License (MIT) - -Copyright (c) 2014 Nylas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import atexit -import os -import platform -import random -import sys -import threading -import time -import uuid -import warnings -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - capture_internal_exceptions, - get_current_thread_meta, - is_gevent, - is_valid_sample_rate, - logger, - nanosecond_time, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type - - from typing_extensions import TypedDict - - from sentry_sdk._types import Event, ProfilerMode, SamplingContext - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - ProcessedThreadMetadata, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "elapsed_since_start_ns": str, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - ProcessedProfile = TypedDict( - "ProcessedProfile", - { - "frames": List[ProcessedFrame], - "stacks": List[ProcessedStack], - "samples": List[ProcessedSample], - "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - - ThreadPool = None - - -_scheduler: "Optional[Scheduler]" = None - - -# The minimum number of unique samples that must exist in a profile to be -# considered valid. -PROFILE_MINIMUM_SAMPLES = 2 - - -def has_profiling_enabled(options: "Dict[str, Any]") -> bool: - profiles_sampler = options["profiles_sampler"] - if profiles_sampler is not None: - return True - - profiles_sample_rate = options["profiles_sample_rate"] - if profiles_sample_rate is not None and profiles_sample_rate > 0: - return True - - profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") - if profiles_sample_rate is not None: - logger.warning( - "_experiments['profiles_sample_rate'] is deprecated. " - "Please use the non-experimental profiles_sample_rate option " - "directly." - ) - if profiles_sample_rate > 0: - return True - - return False - - -def setup_profiler(options: "Dict[str, Any]") -> bool: - global _scheduler - - if _scheduler is not None: - logger.debug("[Profiling] Profiler is already setup") - return False - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventScheduler.mode - else: - default_profiler_mode = ThreadScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - profiler_mode = options.get("_experiments", {}).get("profiler_mode") - if profiler_mode is not None: - logger.warning( - "_experiments['profiler_mode'] is deprecated. Please use the " - "non-experimental profiler_mode option directly." - ) - profiler_mode = profiler_mode or default_profiler_mode - - if ( - profiler_mode == ThreadScheduler.mode - # for legacy reasons, we'll keep supporting sleep mode for this scheduler - or profiler_mode == "sleep" - ): - _scheduler = ThreadScheduler(frequency=frequency) - elif profiler_mode == GeventScheduler.mode: - _scheduler = GeventScheduler(frequency=frequency) - else: - raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) - ) - _scheduler.setup() - - atexit.register(teardown_profiler) - - return True - - -def teardown_profiler() -> None: - global _scheduler - - if _scheduler is not None: - _scheduler.teardown() - - _scheduler = None - - -MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds - - -class Profile: - def __init__( - self, - sampled: "Optional[bool]", - start_ns: int, - hub: "Optional[sentry_sdk.Hub]" = None, - scheduler: "Optional[Scheduler]" = None, - ) -> None: - self.scheduler = _scheduler if scheduler is None else scheduler - - self.event_id: str = uuid.uuid4().hex - - self.sampled: "Optional[bool]" = sampled - - # Various framework integrations are capable of overwriting the active thread id. - # If it is set to `None` at the end of the profile, we fall back to the default. - self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 - self.active_thread_id: "Optional[int]" = None - - try: - self.start_ns: int = start_ns - except AttributeError: - self.start_ns = 0 - - self.stop_ns: int = 0 - self.active: bool = False - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - self.unique_samples = 0 - - # Backwards compatibility with the old hub property - self._hub: "Optional[sentry_sdk.Hub]" = None - if hub is not None: - self._hub = hub - warnings.warn( - "The `hub` parameter is deprecated. Please do not use it.", - DeprecationWarning, - stacklevel=2, - ) - - def update_active_thread_id(self) -> None: - self.active_thread_id = get_current_thread_meta()[0] - logger.debug( - "[Profiling] updating active thread id to {tid}".format( - tid=self.active_thread_id - ) - ) - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the profile's sampling decision according to the following - precedence rules: - - 1. If the transaction to be profiled is not sampled, that decision - will be used, regardless of anything else. - - 2. Use `profiles_sample_rate` to decide. - """ - - # The corresponding transaction was not sampled, - # so don't generate a profile for it. - if not self.sampled: - logger.debug( - "[Profiling] Discarding profile because transaction is discarded." - ) - self.sampled = False - return - - # The profiler hasn't been properly initialized. - if self.scheduler is None: - logger.debug( - "[Profiling] Discarding profile because profiler was not started." - ) - self.sampled = False - return - - client = sentry_sdk.get_client() - if not client.is_active(): - self.sampled = False - return - - options = client.options - - if callable(options.get("profiles_sampler")): - sample_rate = options["profiles_sampler"](sampling_context) - elif options["profiles_sample_rate"] is not None: - sample_rate = options["profiles_sample_rate"] - else: - sample_rate = options["_experiments"].get("profiles_sample_rate") - - # The profiles_sample_rate option was not set, so profiling - # was never enabled. - if sample_rate is None: - logger.debug( - "[Profiling] Discarding profile because profiling was not enabled." - ) - self.sampled = False - return - - if not is_valid_sample_rate(sample_rate, source="Profiling"): - logger.warning( - "[Profiling] Discarding profile because of invalid sample rate." - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < float(sample_rate) - - if self.sampled: - logger.debug("[Profiling] Initializing profile") - else: - logger.debug( - "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( - sample_rate=float(sample_rate) - ) - ) - - def start(self) -> None: - if not self.sampled or self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Starting profile") - self.active = True - if not self.start_ns: - self.start_ns = nanosecond_time() - self.scheduler.start_profiling(self) - - def stop(self) -> None: - if not self.sampled or not self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Stopping profile") - self.active = False - self.stop_ns = nanosecond_time() - - def __enter__(self) -> "Profile": - scope = sentry_sdk.get_isolation_scope() - old_profile = scope.profile - scope.profile = self - - self._context_manager_state = (scope, old_profile) - - self.start() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - with capture_internal_exceptions(): - self.stop() - - scope, old_profile = self._context_manager_state - del self._context_manager_state - - scope.profile = old_profile - - def write(self, ts: int, sample: "ExtractedSample") -> None: - if not self.active: - return - - if ts < self.start_ns: - return - - offset = ts - self.start_ns - if offset > MAX_PROFILE_DURATION_NS: - self.stop() - return - - self.unique_samples += 1 - - elapsed_since_start_ns = str(offset) - - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "elapsed_since_start_ns": elapsed_since_start_ns, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def process(self) -> "ProcessedProfile": - # This collects the thread metadata at the end of a profile. Doing it - # this way means that any threads that terminate before the profile ends - # will not have any metadata associated with it. - thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - } - - return { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": thread_metadata, - } - - def to_json( - self, event_opt: "Event", options: "Dict[str, Any]" - ) -> "Dict[str, Any]": - profile = self.process() - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - return { - "environment": event_opt.get("environment"), - "event_id": self.event_id, - "platform": "python", - "profile": profile, - "release": event_opt.get("release", ""), - "timestamp": event_opt["start_timestamp"], - "version": "1", - "device": { - "architecture": platform.machine(), - }, - "os": { - "name": platform.system(), - "version": platform.release(), - }, - "runtime": { - "name": platform.python_implementation(), - "version": platform.python_version(), - }, - "transactions": [ - { - "id": event_opt["event_id"], - "name": event_opt["transaction"], - # we start the transaction before the profile and this is - # the transaction start time relative to the profile, so we - # hardcode it to 0 until we can start the profile before - "relative_start_ns": "0", - # use the duration of the profile instead of the transaction - # because we end the transaction after the profile - "relative_end_ns": str(self.stop_ns - self.start_ns), - "trace_id": event_opt["contexts"]["trace"]["trace_id"], - "active_thread_id": str( - self._default_active_thread_id - if self.active_thread_id is None - else self.active_thread_id - ), - } - ], - } - - def valid(self) -> bool: - client = sentry_sdk.get_client() - if not client.is_active(): - return False - - if not has_profiling_enabled(client.options): - return False - - if self.sampled is None or not self.sampled: - if client.transport: - client.transport.record_lost_event( - "sample_rate", data_category="profile" - ) - return False - - if self.unique_samples < PROFILE_MINIMUM_SAMPLES: - if client.transport: - client.transport.record_lost_event( - "insufficient_data", data_category="profile" - ) - logger.debug("[Profiling] Discarding profile because insufficient samples.") - return False - - return True - - @property - def hub(self) -> "Optional[sentry_sdk.Hub]": - warnings.warn( - "The `hub` attribute is deprecated. Please do not access it.", - DeprecationWarning, - stacklevel=2, - ) - return self._hub - - @hub.setter - def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: - warnings.warn( - "The `hub` attribute is deprecated. Please do not set it.", - DeprecationWarning, - stacklevel=2, - ) - self._hub = value - - -class Scheduler(ABC): - mode: "ProfilerMode" = "unknown" - - def __init__(self, frequency: int) -> None: - self.interval = 1.0 / frequency - - self.sampler = self.make_sampler() - - # cap the number of new profiles at any time so it does not grow infinitely - self.new_profiles: "Deque[Profile]" = deque(maxlen=128) - self.active_profiles: "Set[Profile]" = set() - - def __enter__(self) -> "Scheduler": - self.setup() - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self.teardown() - - @abstractmethod - def setup(self) -> None: - pass - - @abstractmethod - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - """ - Ensure the scheduler is running. By default, this method is a no-op. - The method should be overridden by any implementation for which it is - relevant. - """ - return None - - def start_profiling(self, profile: "Profile") -> None: - self.ensure_running() - self.new_profiles.append(profile) - - def make_sampler(self) -> "Callable[..., None]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - def _sample_stack(*args: "Any", **kwargs: "Any") -> None: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - # make sure to clear the cache if we're not profiling so we dont - # keep a reference to the last stack of frames around - return - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - now = nanosecond_time() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - - inactive_profiles = [] - - for profile in self.active_profiles: - if profile.active: - profile.write(now, sample) - else: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - return _sample_stack - - -class ThreadScheduler(Scheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ProfilerMode" = "thread" - name = "sentry.profiler.ThreadScheduler" - - def __init__(self, frequency: int) -> None: - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[threading.Thread]" = None - self.pid: "Optional[int]" = None - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - """ - Check that the profiler has an active thread to run in, and start one if - that's not the case. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self.running - will be False after running this function. - """ - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - -class GeventScheduler(Scheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ProfilerMode" = "gevent" - name = "sentry.profiler.GeventScheduler" - - def __init__(self, frequency: int) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[_ThreadPool]" = None - self.pid: "Optional[int]" = None - - # This intentionally uses the gevent patched threading.Lock. - # The lock will be required when first trying to start profiles - # as we need to spawn the profiler thread from the greenlets. - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py deleted file mode 100644 index e9f0fec07f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/profiler/utils.py +++ /dev/null @@ -1,186 +0,0 @@ -import os -from collections import deque -from typing import TYPE_CHECKING - -from sentry_sdk._compat import PY311 -from sentry_sdk.utils import filename_for_module - -if TYPE_CHECKING: - from types import FrameType - from typing import Deque, List, Optional, Sequence, Tuple - - from typing_extensions import TypedDict - - from sentry_sdk._lru_cache import LRUCache - - ThreadId = str - - ProcessedStack = List[int] - - ProcessedFrame = TypedDict( - "ProcessedFrame", - { - "abs_path": str, - "filename": Optional[str], - "function": str, - "lineno": int, - "module": Optional[str], - }, - ) - - ProcessedThreadMetadata = TypedDict( - "ProcessedThreadMetadata", - {"name": str}, - ) - - FrameId = Tuple[ - str, # abs_path - int, # lineno - str, # function - ] - FrameIds = Tuple[FrameId, ...] - - # The exact value of this id is not very meaningful. The purpose - # of this id is to give us a compact and unique identifier for a - # raw stack that can be used as a key to a dictionary so that it - # can be used during the sampled format generation. - StackId = Tuple[int, int] - - ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]] - ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]] - -# The default sampling frequency to use. This is set at 101 in order to -# mitigate the effects of lockstep sampling. -DEFAULT_SAMPLING_FREQUENCY = 101 - - -# We want to impose a stack depth limit so that samples aren't too large. -MAX_STACK_DEPTH = 128 - - -if PY311: - - def get_frame_name(frame: "FrameType") -> str: - return frame.f_code.co_qualname - -else: - - def get_frame_name(frame: "FrameType") -> str: - f_code = frame.f_code - co_varnames = f_code.co_varnames - - # co_name only contains the frame name. If the frame was a method, - # the class name will NOT be included. - name = f_code.co_name - - # if it was a method, we can get the class name by inspecting - # the f_locals for the `self` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `self` if its an instance method - co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals - ): - for cls in type(frame.f_locals["self"]).__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # if it was a class method, (decorated with `@classmethod`) - # we can get the class name by inspecting the f_locals for the `cls` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `cls` if its a class method - co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals - ): - for cls in frame.f_locals["cls"].__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # nothing we can do if it is a staticmethod (decorated with @staticmethod) - - # we've done all we can, time to give up and return what we have - return name - - -def frame_id(raw_frame: "FrameType") -> "FrameId": - return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) - - -def extract_frame(fid: "FrameId", raw_frame: "FrameType", cwd: str) -> "ProcessedFrame": - abs_path = raw_frame.f_code.co_filename - - try: - module = raw_frame.f_globals["__name__"] - except Exception: - module = None - - # namedtuples can be many times slower when initialing - # and accessing attribute so we opt to use a tuple here instead - return { - # This originally was `os.path.abspath(abs_path)` but that had - # a large performance overhead. - # - # According to docs, this is equivalent to - # `os.path.normpath(os.path.join(os.getcwd(), path))`. - # The `os.getcwd()` call is slow here, so we precompute it. - # - # Additionally, since we are using normalized path already, - # we skip calling `os.path.normpath` entirely. - "abs_path": os.path.join(cwd, abs_path), - "module": module, - "filename": filename_for_module(module, abs_path) or None, - "function": fid[2], - "lineno": raw_frame.f_lineno, - } - - -def extract_stack( - raw_frame: "Optional[FrameType]", - cache: "LRUCache", - cwd: str, - max_stack_depth: int = MAX_STACK_DEPTH, -) -> "ExtractedStack": - """ - Extracts the stack starting the specified frame. The extracted stack - assumes the specified frame is the top of the stack, and works back - to the bottom of the stack. - - In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, - only the first `MAX_STACK_DEPTH` frames will be returned. - """ - - raw_frames: "Deque[FrameType]" = deque(maxlen=max_stack_depth) - - while raw_frame is not None: - f_back = raw_frame.f_back - raw_frames.append(raw_frame) - raw_frame = f_back - - frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) - frames = [] - for i, fid in enumerate(frame_ids): - frame = cache.get(fid) - if frame is None: - frame = extract_frame(fid, raw_frames[i], cwd) - cache.set(fid, frame) - frames.append(frame) - - # Instead of mapping the stack into frame ids and hashing - # that as a tuple, we can directly hash the stack. - # This saves us from having to generate yet another list. - # Additionally, using the stack as the key directly is - # costly because the stack can be large, so we pre-hash - # the stack, and use the hash as the key as this will be - # needed a few times to improve performance. - # - # To Reduce the likelihood of hash collisions, we include - # the stack depth. This means that only stacks of the same - # depth can suffer from hash collisions. - stack_id = len(raw_frames), hash(frame_ids) - - return stack_id, frame_ids, frames diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py deleted file mode 100644 index 4fd22714cf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scope.py +++ /dev/null @@ -1,2185 +0,0 @@ -import os -import platform -import sys -import warnings -from collections import deque -from contextlib import contextmanager -from copy import copy, deepcopy -from datetime import datetime, timezone -from enum import Enum -from functools import wraps -from itertools import chain -from typing import TYPE_CHECKING, cast - -import sentry_sdk -from sentry_sdk._types import AnnotatedValue -from sentry_sdk.attachments import Attachment -from sentry_sdk.consts import ( - DEFAULT_MAX_BREADCRUMBS, - FALSE_VALUES, - INSTRUMENTER, - SPANDATA, -) -from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.profiler.transaction_profiler import Profile -from sentry_sdk.session import Session -from sentry_sdk.traces import ( - _DEFAULT_PARENT_SPAN, - NoOpStreamedSpan, - StreamedSpan, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.tracing_utils import ( - Baggage, - PropagationContext, - _make_sampling_decision, - has_span_streaming_enabled, - has_tracing_enabled, - is_ignored_span, -) -from sentry_sdk.utils import ( - ContextVar, - capture_internal_exception, - capture_internal_exceptions, - datetime_from_isoformat, - disable_capture_event, - event_from_exception, - exc_info_from_error, - format_attribute, - has_logs_enabled, - has_metrics_enabled, - logger, -) - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import ( - Any, - Callable, - Deque, - Dict, - Generator, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - ) - - from typing_extensions import Unpack - - import sentry_sdk - from sentry_sdk._types import ( - Attributes, - AttributeValue, - Breadcrumb, - BreadcrumbHint, - ErrorProcessor, - Event, - EventProcessor, - ExcInfo, - Hint, - Log, - LogLevelStr, - Metric, - SamplingContext, - Type, - ) - from sentry_sdk.tracing import TransactionKwargs - - P = ParamSpec("P") - R = TypeVar("R") - - F = TypeVar("F", bound=Callable[..., Any]) - T = TypeVar("T") - - -# Holds data that will be added to **all** events sent by this process. -# In case this is a http server (think web framework) with multiple users -# the data will be added to events of all users. -# Typically this is used for process wide data such as the release. -_global_scope: "Optional[Scope]" = None - -# Holds data for the active request. -# This is used to isolate data for different requests or users. -# The isolation scope is usually created by integrations, but may also -# be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) - -# Holds data for the active span. -# This can be used to manually add additional data to a span. -_current_scope = ContextVar("current_scope", default=None) - -global_event_processors: "List[EventProcessor]" = [] - -# A function returning a (trace_id, span_id) tuple -# from an external tracing source (such as otel) -_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None - - -class ScopeType(Enum): - CURRENT = "current" - ISOLATION = "isolation" - GLOBAL = "global" - MERGED = "merged" - - -class _ScopeManager: - def __init__(self, hub: "Optional[Any]" = None) -> None: - self._old_scopes: "List[Scope]" = [] - - def __enter__(self) -> "Scope": - isolation_scope = Scope.get_isolation_scope() - - self._old_scopes.append(isolation_scope) - - forked_scope = isolation_scope.fork() - _isolation_scope.set(forked_scope) - - return forked_scope - - def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: - old_scope = self._old_scopes.pop() - _isolation_scope.set(old_scope) - - -def add_global_event_processor(processor: "EventProcessor") -> None: - global_event_processors.append(processor) - - -def register_external_propagation_context( - fn: "Callable[[], Optional[Tuple[str, str]]]", -) -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = fn - - -def remove_external_propagation_context() -> None: - global _external_propagation_context_fn - _external_propagation_context_fn = None - - -def get_external_propagation_context() -> "Optional[Tuple[str, str]]": - return ( - _external_propagation_context_fn() if _external_propagation_context_fn else None - ) - - -def has_external_propagation_context() -> bool: - return _external_propagation_context_fn is not None - - -def _attr_setter(fn: "Any") -> "Any": - return property(fset=fn, doc=fn.__doc__) - - -def _disable_capture(fn: "F") -> "F": - @wraps(fn) - def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any": - if not self._should_capture: - return - try: - self._should_capture = False - return fn(self, *args, **kwargs) - finally: - self._should_capture = True - - return wrapper # type: ignore - - -class Scope: - """The scope holds extra information that should be sent with all - events that belong to it. - """ - - # NOTE: Even though it should not happen, the scope needs to not crash when - # accessed by multiple threads. It's fine if it's full of races, but those - # races should never make the user application crash. - # - # The same needs to hold for any accesses of the scope the SDK makes. - - __slots__ = ( - "_level", - "_name", - "_fingerprint", - # note that for legacy reasons, _transaction is the transaction *name*, - # not a Transaction object (the object is stored in _span) - "_transaction", - "_transaction_info", - "_user", - "_tags", - "_contexts", - "_extras", - "_breadcrumbs", - "_n_breadcrumbs_truncated", - "_gen_ai_original_message_count", - "_gen_ai_conversation_id", - "_event_processors", - "_error_processors", - "_should_capture", - "_span", - "_session", - "_attachments", - "_force_auto_session_tracking", - "_profile", - "_propagation_context", - "client", - "_type", - "_last_event_id", - "_flags", - "_attributes", - ) - - def __init__( - self, - ty: "Optional[ScopeType]" = None, - client: "Optional[sentry_sdk.Client]" = None, - ) -> None: - self._type = ty - - self._event_processors: "List[EventProcessor]" = [] - self._error_processors: "List[ErrorProcessor]" = [] - - self._name: "Optional[str]" = None - self._propagation_context: "Optional[PropagationContext]" = None - self._n_breadcrumbs_truncated: int = 0 - self._gen_ai_original_message_count: "Dict[str, int]" = {} - - self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() - - if client is not None: - self.set_client(client) - - self.clear() - - incoming_trace_information = self._load_trace_data_from_env() - self.generate_propagation_context(incoming_data=incoming_trace_information) - - def __copy__(self) -> "Scope": - """ - Returns a copy of this scope. - This also creates a copy of all referenced data structures. - """ - rv: "Scope" = object.__new__(self.__class__) - - rv._type = self._type - rv.client = self.client - rv._level = self._level - rv._name = self._name - rv._fingerprint = self._fingerprint - rv._transaction = self._transaction - rv._transaction_info = self._transaction_info.copy() - rv._user = self._user - - rv._tags = self._tags.copy() - rv._contexts = self._contexts.copy() - rv._extras = self._extras.copy() - - rv._breadcrumbs = copy(self._breadcrumbs) - rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated - rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy() - rv._event_processors = self._event_processors.copy() - rv._error_processors = self._error_processors.copy() - rv._propagation_context = self._propagation_context - - rv._should_capture = self._should_capture - rv._span = self._span - rv._session = self._session - rv._force_auto_session_tracking = self._force_auto_session_tracking - rv._attachments = self._attachments.copy() - - rv._profile = self._profile - - rv._last_event_id = self._last_event_id - - rv._flags = deepcopy(self._flags) - - rv._attributes = self._attributes.copy() - - rv._gen_ai_conversation_id = self._gen_ai_conversation_id - - return rv - - @classmethod - def get_current_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the current scope. - """ - current_scope = _current_scope.get() - if current_scope is None: - current_scope = Scope(ty=ScopeType.CURRENT) - _current_scope.set(current_scope) - - return current_scope - - @classmethod - def set_current_scope(cls, new_current_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new current scope overwriting the existing current scope. - :param new_current_scope: The scope to set as the new current scope. - """ - _current_scope.set(new_current_scope) - - @classmethod - def get_isolation_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the isolation scope. - """ - isolation_scope = _isolation_scope.get() - if isolation_scope is None: - isolation_scope = Scope(ty=ScopeType.ISOLATION) - _isolation_scope.set(isolation_scope) - - return isolation_scope - - @classmethod - def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None: - """ - .. versionadded:: 2.0.0 - - Sets the given scope as the new isolation scope overwriting the existing isolation scope. - :param new_isolation_scope: The scope to set as the new isolation scope. - """ - _isolation_scope.set(new_isolation_scope) - - @classmethod - def get_global_scope(cls) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns the global scope. - """ - global _global_scope - if _global_scope is None: - _global_scope = Scope(ty=ScopeType.GLOBAL) - - return _global_scope - - def set_global_attributes(self) -> None: - from sentry_sdk.client import SDK_INFO - - self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"]) - self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"]) - - self.set_attribute( - "process.runtime.name", - platform.python_implementation(), - ) - self.set_attribute( - "process.runtime.version", - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", - ) - - options = sentry_sdk.get_client().options - - server_name = options.get("server_name") - if server_name: - self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name) - - environment = options.get("environment") - if environment: - self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment) - - release = options.get("release") - if release: - self.set_attribute(SPANDATA.SENTRY_RELEASE, release) - - @classmethod - def last_event_id(cls) -> "Optional[str]": - """ - .. versionadded:: 2.2.0 - - Returns event ID of the event most recently captured by the isolation scope, or None if no event - has been captured. We do not consider events that are dropped, e.g. by a before_send hook. - Transactions also are not considered events in this context. - - The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry; - whether the event is sent depends on the transport. The event could be sent later or not at all. - Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or - various other reasons. - """ - return cls.get_isolation_scope()._last_event_id - - def _merge_scopes( - self, - additional_scope: "Optional[Scope]" = None, - additional_scope_kwargs: "Optional[Dict[str, Any]]" = None, - ) -> "Scope": - """ - Merges global, isolation and current scope into a new scope and - adds the given additional scope or additional scope kwargs to it. - """ - if additional_scope and additional_scope_kwargs: - raise TypeError("cannot provide scope and kwargs") - - final_scope = copy(_global_scope) if _global_scope is not None else Scope() - final_scope._type = ScopeType.MERGED - - isolation_scope = _isolation_scope.get() - if isolation_scope is not None: - final_scope.update_from_scope(isolation_scope) - - current_scope = _current_scope.get() - if current_scope is not None: - final_scope.update_from_scope(current_scope) - - if self != current_scope and self != isolation_scope: - final_scope.update_from_scope(self) - - if additional_scope is not None: - if callable(additional_scope): - additional_scope(final_scope) - else: - final_scope.update_from_scope(additional_scope) - - elif additional_scope_kwargs: - final_scope.update_from_kwargs(**additional_scope_kwargs) - - return final_scope - - @classmethod - def get_client(cls) -> "sentry_sdk.client.BaseClient": - """ - .. versionadded:: 2.0.0 - - Returns the currently used :py:class:`sentry_sdk.Client`. - This checks the current scope, the isolation scope and the global scope for a client. - If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. - """ - current_scope = _current_scope.get() - try: - client = current_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - isolation_scope = _isolation_scope.get() - try: - client = isolation_scope.client - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - try: - client = _global_scope.client # type: ignore - except AttributeError: - client = None - - if client is not None and client.is_active(): - return client - - return NonRecordingClient() - - def set_client( - self, client: "Optional[sentry_sdk.client.BaseClient]" = None - ) -> None: - """ - .. versionadded:: 2.0.0 - - Sets the client for this scope. - - :param client: The client to use in this scope. - If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`. - - """ - if client is not None: - self.client = client - # We need a client to set the initial global attributes on the global - # scope since they mostly come from client options, so populate them - # as soon as a client is set - sentry_sdk.get_global_scope().set_global_attributes() - else: - self.client = NonRecordingClient() - - def fork(self) -> "Scope": - """ - .. versionadded:: 2.0.0 - - Returns a fork of this scope. - """ - forked_scope = copy(self) - return forked_scope - - def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]": - """ - Load Sentry trace id and baggage from environment variables. - Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false". - """ - incoming_trace_information: "Optional[Dict[str, str]]" = None - - sentry_use_environment = ( - os.environ.get("SENTRY_USE_ENVIRONMENT") or "" - ).lower() - use_environment = sentry_use_environment not in FALSE_VALUES - if use_environment: - incoming_trace_information = {} - - if os.environ.get("SENTRY_TRACE"): - incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = ( - os.environ.get("SENTRY_TRACE") or "" - ) - - if os.environ.get("SENTRY_BAGGAGE"): - incoming_trace_information[BAGGAGE_HEADER_NAME] = ( - os.environ.get("SENTRY_BAGGAGE") or "" - ) - - return incoming_trace_information or None - - def set_new_propagation_context(self) -> None: - """ - Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one. - """ - self._propagation_context = PropagationContext() - - def generate_propagation_context( - self, incoming_data: "Optional[Dict[str, str]]" = None - ) -> None: - """ - Makes sure the propagation context is set on the scope. - If there is `incoming_data` overwrite existing propagation context. - If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing. - """ - if incoming_data is not None: - self._propagation_context = PropagationContext.from_incoming_data( - incoming_data - ) - - # TODO-neel this below is a BIG code smell but requires a bunch of other refactoring - if self._type != ScopeType.CURRENT: - if self._propagation_context is None: - self.set_new_propagation_context() - - def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]": - """ - Returns the Dynamic Sampling Context from the Propagation Context. - If not existing, creates a new one. - - Deprecated: Logic moved to PropagationContext, don't use directly. - """ - if self._propagation_context is None: - return None - - return self._propagation_context.dynamic_sampling_context - - def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": - """ - Returns the Sentry "sentry-trace" header (aka the traceparent) from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().to_traceparent() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return traceparent from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_traceparent() - elif not span_streaming and self.span is not None: - return self.span._to_traceparent() - - # else return traceparent from the propagation context - return self.get_active_propagation_context().to_traceparent() - - def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": - """ - Returns the Sentry "baggage" header containing trace information from the - currently active span or the scopes Propagation Context. - """ - client = self.get_client() - - if not has_tracing_enabled(client.options): - return self.get_active_propagation_context().get_baggage() - - span_streaming = has_span_streaming_enabled(client.options) - # If we have an active span, return baggage from there - if span_streaming and type(self.streamed_span) is StreamedSpan: - return self.streamed_span._to_baggage() - elif not span_streaming and self.span is not None: - return self.span._to_baggage() - - # else return baggage from the propagation context - return self.get_active_propagation_context().get_baggage() - - def get_trace_context(self) -> "Dict[str, Any]": - """ - Returns the Sentry "trace" context from the Propagation Context. - """ - if ( - has_tracing_enabled(self.get_client().options) - and self._span is not None - and not isinstance(self._span, (NoOpStreamedSpan, NoOpSpan)) - ): - return self._span._get_trace_context() - - # if we are tracing externally (otel), those values take precedence - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - trace_id, span_id = external_propagation_context - return {"trace_id": trace_id, "span_id": span_id} - - propagation_context = self.get_active_propagation_context() - - return { - "trace_id": propagation_context.trace_id, - "span_id": propagation_context.span_id, - "parent_span_id": propagation_context.parent_span_id, - "dynamic_sampling_context": propagation_context.dynamic_sampling_context, - } - - def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str: - """ - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - span = kwargs.pop("span", None) - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - meta = "" - - for name, content in self.iter_trace_propagation_headers(): - meta += f'' - - return meta - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context. - Deprecated: use PropagationContext.iter_headers instead. - """ - if self._propagation_context is not None: - yield from self._propagation_context.iter_headers() - - def iter_trace_propagation_headers( - self, *args: "Any", **kwargs: "Any" - ) -> "Generator[Tuple[str, str], None, None]": - """ - Return HTTP headers which allow propagation of trace data. - - If a span is given, the trace data will taken from the span. - If no span is given, the trace data is taken from the scope. - """ - client = self.get_client() - if not client.options.get("propagate_traces"): - warnings.warn( - "The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.", - DeprecationWarning, - stacklevel=2, - ) - return - - span = kwargs.pop("span", None) - if not span: - span_streaming = has_span_streaming_enabled(client.options) - span = self.streamed_span if span_streaming else self.span - - if ( - has_tracing_enabled(client.options) - and span is not None - and not isinstance(span, (NoOpStreamedSpan, NoOpSpan)) - ): - for header in span._iter_headers(): - yield header - elif has_external_propagation_context(): - # when we have an external_propagation_context (otlp) - # we leave outgoing propagation to the propagator - return - else: - for header in self.get_active_propagation_context().iter_headers(): - yield header - - def get_active_propagation_context(self) -> "PropagationContext": - if self._propagation_context is not None: - return self._propagation_context - - current_scope = self.get_current_scope() - if current_scope._propagation_context is not None: - return current_scope._propagation_context - - isolation_scope = self.get_isolation_scope() - # should actually never happen, but just in case someone calls scope.clear - if isolation_scope._propagation_context is None: - isolation_scope._propagation_context = PropagationContext() - return isolation_scope._propagation_context - - @classmethod - def set_custom_sampling_context( - cls, custom_sampling_context: "dict[str, Any]" - ) -> None: - cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context( - custom_sampling_context - ) - - def clear(self) -> None: - """Clears the entire scope.""" - self._level: "Optional[LogLevelStr]" = None - self._fingerprint: "Optional[List[str]]" = None - self._transaction: "Optional[str]" = None - self._transaction_info: "dict[str, str]" = {} - self._user: "Optional[Dict[str, Any]]" = None - - self._tags: "Dict[str, Any]" = {} - self._contexts: "Dict[str, Dict[str, Any]]" = {} - self._extras: "dict[str, Any]" = {} - self._attachments: "List[Attachment]" = [] - - self.clear_breadcrumbs() - self._should_capture: bool = True - - self._span: "Optional[Union[Span, StreamedSpan]]" = None - self._session: "Optional[Session]" = None - self._force_auto_session_tracking: "Optional[bool]" = None - - self._profile: "Optional[Profile]" = None - - self._propagation_context = None - - # self._last_event_id is only applicable to isolation scopes - self._last_event_id: "Optional[str]" = None - self._flags: "Optional[FlagBuffer]" = None - - self._attributes: "Attributes" = {} - - self._gen_ai_conversation_id: "Optional[str]" = None - - @_attr_setter - def level(self, value: "LogLevelStr") -> None: - """ - When set this overrides the level. - - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. - - :param value: The level to set. - """ - logger.warning( - "Deprecated: use .set_level() instead. This will be removed in the future." - ) - - self._level = value - - def set_level(self, value: "LogLevelStr") -> None: - """ - Sets the level for the scope. - - :param value: The level to set. - """ - self._level = value - - @_attr_setter - def fingerprint(self, value: "Optional[List[str]]") -> None: - """When set this overrides the default fingerprint.""" - self._fingerprint = value - - @property - def transaction(self) -> "Any": - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" - - # there is no span/transaction on the scope - if self._span is None: - return None - - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - # there is an orphan span on the scope - if self._span.containing_transaction is None: - return None - - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value: "Any") -> None: - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span: - if isinstance(self._span, StreamedSpan): - warnings.warn( - "Scope.transaction is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return None - - if self._span.containing_transaction: - self._span.containing_transaction.name = value - - def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: - """Set the transaction name and optionally the transaction source.""" - self._transaction = name - if self._span: - if isinstance(self._span, NoOpStreamedSpan): - return - - elif isinstance(self._span, StreamedSpan): - self._span._segment.name = name - if source: - self._span._segment.set_attribute( - "sentry.span.source", getattr(source, "value", source) - ) - - elif self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source - - if source: - self._transaction_info["source"] = source - - @_attr_setter - def user(self, value: "Optional[Dict[str, Any]]") -> None: - """When set a specific user is bound to the scope. Deprecated in favor of set_user.""" - warnings.warn( - "The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.", - DeprecationWarning, - stacklevel=2, - ) - self.set_user(value) - - def set_user(self, value: "Optional[Dict[str, Any]]") -> None: - """Sets a user for the scope.""" - self._user = value - - session = self.get_isolation_scope()._session - if session is not None: - session.update(user=value) - - @property - def span(self) -> "Optional[Span]": - """Get/set current tracing span or transaction.""" - return self._span if isinstance(self._span, Span) else None - - @span.setter - def span(self, span: "Optional[Span]") -> None: - self._span = span - # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source - - @property - def streamed_span(self) -> "Optional[StreamedSpan]": - """Get/set current tracing span.""" - return self._span if isinstance(self._span, StreamedSpan) else None - - @streamed_span.setter - def streamed_span(self, span: "Optional[StreamedSpan]") -> None: - self._span = span - - # Also set _transaction and _transaction_info in streaming mode as this - # is used for populating events and linking them to segments - if type(span) is StreamedSpan and span._is_segment(): - self._transaction = span.name - if span._attributes.get("sentry.span.source"): - self._transaction_info["source"] = str( - span._attributes["sentry.span.source"] - ) - - @property - def profile(self) -> "Optional[Profile]": - return self._profile - - @profile.setter - def profile(self, profile: "Optional[Profile]") -> None: - self._profile = profile - - def set_tag(self, key: str, value: "Any") -> None: - """ - Sets a tag for a key to a specific value. - - :param key: Key of the tag to set. - - :param value: Value of the tag to set. - """ - self._tags[key] = value - - def set_tags(self, tags: "Mapping[str, object]") -> None: - """Sets multiple tags at once. - - This method updates multiple tags at once. The tags are passed as a dictionary - or other mapping type. - - Calling this method is equivalent to calling `set_tag` on each key-value pair - in the mapping. If a tag key already exists in the scope, its value will be - updated. If the tag key does not exist in the scope, the key-value pair will - be added to the scope. - - This method only modifies tag keys in the `tags` mapping passed to the method. - `scope.set_tags({})` is, therefore, a no-op. - - :param tags: A mapping of tag keys to tag values to set. - """ - self._tags.update(tags) - - def remove_tag(self, key: str) -> None: - """ - Removes a specific tag. - - :param key: Key of the tag to remove. - """ - self._tags.pop(key, None) - - def set_context( - self, - key: str, - value: "Dict[str, Any]", - ) -> None: - """ - Binds a context at a certain key to a specific value. - """ - self._contexts[key] = value - - def remove_context( - self, - key: str, - ) -> None: - """Removes a context.""" - self._contexts.pop(key, None) - - def set_extra( - self, - key: str, - value: "Any", - ) -> None: - """Sets an extra key to a specific value.""" - self._extras[key] = value - - def remove_extra( - self, - key: str, - ) -> None: - """Removes a specific extra key.""" - self._extras.pop(key, None) - - def set_conversation_id(self, conversation_id: str) -> None: - """ - Sets the conversation ID for gen_ai spans. - - :param conversation_id: The conversation ID to set. - """ - self._gen_ai_conversation_id = conversation_id - - def get_conversation_id(self) -> "Optional[str]": - """ - Gets the conversation ID for gen_ai spans. - - :returns: The conversation ID, or None if not set. - """ - return self._gen_ai_conversation_id - - def remove_conversation_id(self) -> None: - """Removes the conversation ID.""" - self._gen_ai_conversation_id = None - - def clear_breadcrumbs(self) -> None: - """Clears breadcrumb buffer.""" - self._breadcrumbs: "Deque[Breadcrumb]" = deque() - self._n_breadcrumbs_truncated = 0 - - def add_attachment( - self, - bytes: "Union[None, bytes, Callable[[], bytes]]" = None, - filename: "Optional[str]" = None, - path: "Optional[str]" = None, - content_type: "Optional[str]" = None, - add_to_transactions: bool = False, - ) -> None: - """Adds an attachment to future events sent from this scope. - - The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor. - """ - self._attachments.append( - Attachment( - bytes=bytes, - path=path, - filename=filename, - content_type=content_type, - add_to_transactions=add_to_transactions, - ) - ) - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - client = self.get_client() - - if not client.is_active(): - logger.info("Dropped breadcrumb because no client bound") - return - - before_breadcrumb = client.options.get("before_breadcrumb") - max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS) - - crumb = dict(crumb or ()) - crumb.update(kwargs) - if not crumb: - return - - hint = dict(hint or ()) - - if crumb.get("timestamp") is None: - crumb["timestamp"] = datetime.now(timezone.utc) - if crumb.get("type") is None: - crumb["type"] = "default" - - if before_breadcrumb is not None: - new_crumb = before_breadcrumb(crumb, hint) - else: - new_crumb = crumb - - if new_crumb is not None: - self._breadcrumbs.append(new_crumb) - else: - logger.info("before breadcrumb dropped breadcrumb (%s)", crumb) - - while len(self._breadcrumbs) > max_breadcrumbs: - self._breadcrumbs.popleft() - self._n_breadcrumbs_truncated += 1 - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - try_autostart_continuous_profiler() - - custom_sampling_context = custom_sampling_context or {} - - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs: "TransactionKwargs" = kwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - # update the sample rate in the dsc - if transaction.sample_rate is not None: - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction._baggage: - transaction._baggage.sentry_items["sample_rate"] = str( - transaction.sample_rate - ) - - if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - - transaction._continuous_profile = try_profile_lifecycle_trace_start() - - # Typically, the profiler is set when the transaction is created. But when - # using the auto lifecycle, the profiler isn't running when the first - # transaction is started. So make sure we update the profiler id on it. - if transaction._continuous_profile is not None: - transaction.set_profiler_id(get_profiler_id()) - - # we don't bother to keep spans if we already know we're not going to - # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) - - return transaction - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - warnings.warn( - "Scope.start_span is not available in streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return NoOpSpan() - - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - client = self.get_client() - - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - # get current span or transaction - span = self.span or self.get_isolation_scope().span - if isinstance(span, StreamedSpan): - # make mypy happy - return NoOpSpan() - - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - kwargs["trace_id"] = propagation_context.trace_id - - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) - - return span - - def start_streamed_span( - self, - name: str, - attributes: "Optional[Attributes]", - parent_span: "Optional[StreamedSpan]", - active: bool, - ) -> "StreamedSpan": - # TODO: rename to start_span once we drop the old API - if isinstance(parent_span, NoOpStreamedSpan): - # parent_span is only set if the user explicitly set it - logger.debug( - "Ignored parent span provided. Span will be parented to the " - "currently active span instead." - ) - - if parent_span is _DEFAULT_PARENT_SPAN or isinstance( - parent_span, NoOpStreamedSpan - ): - parent_span = self.streamed_span - - # If no eligible parent_span was provided and there is no currently - # active span, this is a segment - if parent_span is None: - propagation_context = self.get_active_propagation_context() - - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - scope=self, - unsampled_reason="ignored", - ) - - sampled, sample_rate, sample_rand, outcome = _make_sampling_decision( - name, - attributes, - self, - ) - - if sample_rate is not None: - self._update_sample_rate(sample_rate) - - if sampled is False: - return NoOpStreamedSpan( - scope=self, - unsampled_reason=outcome, - ) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=None, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - parent_sampled=propagation_context.parent_sampled, - baggage=propagation_context.baggage, - sample_rand=sample_rand, - sample_rate=sample_rate, - ) - - # This is a child span; take propagation context from the parent span - with new_scope(): - if is_ignored_span(name, attributes): - return NoOpStreamedSpan( - unsampled_reason="ignored", - ) - - if isinstance(parent_span, NoOpStreamedSpan): - return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason) - - return StreamedSpan( - name=name, - attributes=attributes, - active=active, - scope=self, - segment=parent_span._segment, - trace_id=parent_span.trace_id, - parent_span_id=parent_span.span_id, - parent_sampled=parent_span.sampled, - ) - - def _update_sample_rate(self, sample_rate: float) -> None: - # If we had to adjust the sample rate when setting the sampling decision - # for a span, it needs to be updated in the propagation context too - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(sample_rate) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", - ) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - self.generate_propagation_context(environ_or_headers) - - # generate_propagation_context ensures that the propagation_context is not None. - propagation_context = cast(PropagationContext, self._propagation_context) - - optional_kwargs = {} - if name: - optional_kwargs["name"] = name - if source: - optional_kwargs["source"] = source - - return Transaction( - op=op, - origin=origin, - baggage=propagation_context.baggage, - parent_sampled=propagation_context.parent_sampled, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - same_process_as_parent=False, - **optional_kwargs, - ) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures an event. - - Merges given scope data and calls :py:meth:`sentry_sdk.client._Client.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - scope = self._merge_scopes(scope, scope_kwargs) - - event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) - - if event_id is not None and event.get("type") != "transaction": - self.get_isolation_scope()._last_event_id = event_id - - return event_id - - def _capture_log(self, log: "Optional[Log]") -> None: - if log is None: - return - - client = self.get_client() - if not has_logs_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}" - ) - - client._capture_log(log, scope=merged_scope) - - def _capture_metric(self, metric: "Optional[Metric]") -> None: - if metric is None: - return - - client = self.get_client() - if not has_metrics_enabled(client.options): - return - - merged_scope = self._merge_scopes() - - debug = client.options.get("debug", False) - if debug: - logger.debug( - f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}" - ) - - client._capture_metric(metric, scope=merged_scope) - - def _capture_span(self, span: "Optional[StreamedSpan]") -> None: - if span is None: - return - - client = self.get_client() - if not has_span_streaming_enabled(client.options): - return - - merged_scope = self._merge_scopes() - client._capture_span(span, scope=merged_scope) - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - Captures a message. - - :param message: The string to send as the message. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if level is None: - level = "info" - - event: "Event" = { - "message": message, - "level": level, - } - - return self.capture_event(event, scope=scope, **scope_kwargs) - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """Captures an exception. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - if disable_capture_event.get(False): - return None - - if error is not None: - exc_info = exc_info_from_error(error) - else: - exc_info = sys.exc_info() - - event, hint = event_from_exception( - exc_info, client_options=self.get_client().options - ) - - try: - return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs) - except Exception: - capture_internal_exception(sys.exc_info()) - - return None - - def start_session(self, *args: "Any", **kwargs: "Any") -> None: - """Starts a new session.""" - session_mode = kwargs.pop("session_mode", "application") - - self.end_session() - - client = self.get_client() - self._session = Session( - release=client.options.get("release"), - environment=client.options.get("environment"), - user=self._user, - session_mode=session_mode, - ) - - def end_session(self, *args: "Any", **kwargs: "Any") -> None: - """Ends the current session if there is one.""" - session = self._session - self._session = None - - if session is not None: - session.close() - self.get_client().capture_session(session) - - def stop_auto_session_tracking(self, *args: "Any", **kwargs: "Any") -> None: - """Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - self.end_session() - self._force_auto_session_tracking = False - - def resume_auto_session_tracking(self) -> None: - """Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - self._force_auto_session_tracking = None - - def add_event_processor( - self, - func: "EventProcessor", - ) -> None: - """Register a scope local event processor on the scope. - - :param func: This function behaves like `before_send.` - """ - if len(self._event_processors) > 20: - logger.warning( - "Too many event processors on scope! Clearing list to free up some memory: %r", - self._event_processors, - ) - del self._event_processors[:] - - self._event_processors.append(func) - - def add_error_processor( - self, - func: "ErrorProcessor", - cls: "Optional[Type[BaseException]]" = None, - ) -> None: - """Register a scope local error processor on the scope. - - :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument. - - :param cls: Optionally, only process exceptions of this type. - """ - if cls is not None: - cls_ = cls # For mypy. - real_func = func - - def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]": - try: - is_inst = isinstance(exc_info[1], cls_) - except Exception: - is_inst = False - if is_inst: - return real_func(event, exc_info) - return event - - self._error_processors.append(func) - - def _apply_level_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._level is not None: - event["level"] = self._level - - def _apply_breadcrumbs_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - event.setdefault("breadcrumbs", {}) - - # This check is just for mypy - - if not isinstance(event["breadcrumbs"], AnnotatedValue): - event["breadcrumbs"].setdefault("values", []) - event["breadcrumbs"]["values"].extend(self._breadcrumbs) - - # Attempt to sort timestamps - try: - if not isinstance(event["breadcrumbs"], AnnotatedValue): - for crumb in event["breadcrumbs"]["values"]: - if isinstance(crumb["timestamp"], str): - crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) - - event["breadcrumbs"]["values"].sort( - key=lambda crumb: crumb["timestamp"] - ) - except Exception as err: - logger.debug("Error when sorting breadcrumbs", exc_info=err) - pass - - def _apply_user_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("user") is None and self._user is not None: - event["user"] = self._user - - def _apply_transaction_name_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction") is None and self._transaction is not None: - event["transaction"] = self._transaction - - def _apply_transaction_info_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("transaction_info") is None and self._transaction_info is not None: - event["transaction_info"] = self._transaction_info - - def _apply_fingerprint_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if event.get("fingerprint") is None and self._fingerprint is not None: - event["fingerprint"] = self._fingerprint - - def _apply_extra_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._extras: - event.setdefault("extra", {}).update(self._extras) - - def _apply_tags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._tags: - event.setdefault("tags", {}).update(self._tags) - - def _apply_contexts_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - if self._contexts: - event.setdefault("contexts", {}).update(self._contexts) - - contexts = event.setdefault("contexts", {}) - - # Add "trace" context - if contexts.get("trace") is None: - contexts["trace"] = self.get_trace_context() - - def _apply_flags_to_event( - self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]" - ) -> None: - flags = self.flags.get() - if len(flags) > 0: - event.setdefault("contexts", {}).setdefault("flags", {}).update( - {"values": flags} - ) - - def _apply_scope_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - # TODO: turn Logs, Metrics into actual classes - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - for attribute, value in self._attributes.items(): - if attribute not in attributes: - attributes[attribute] = value - - def _apply_user_attributes_to_telemetry( - self, telemetry: "Union[Log, Metric, StreamedSpan]" - ) -> None: - if isinstance(telemetry, dict): - attributes = telemetry["attributes"] - else: - attributes = telemetry._attributes - - if not should_send_default_pii() or self._user is None: - return - - for attribute_name, user_attribute in ( - ("user.id", "id"), - ("user.name", "username"), - ("user.email", "email"), - ("user.ip_address", "ip_address"), - ): - if ( - user_attribute in self._user - and attribute_name not in attributes - and self._user[user_attribute] is not None - ): - attributes[attribute_name] = self._user[user_attribute] - - def _drop(self, cause: "Any", ty: str) -> "Optional[Any]": - logger.info("%s (%s) dropped event", ty, cause) - return None - - def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the error processors on the event and returns the modified event. - """ - exc_info = hint.get("exc_info") - if exc_info is not None: - error_processors = chain( - self.get_global_scope()._error_processors, - self.get_isolation_scope()._error_processors, - self.get_current_scope()._error_processors, - ) - - for error_processor in error_processors: - new_event = error_processor(event, exc_info) - if new_event is None: - return self._drop(error_processor, "error processor") - - event = new_event - - return event - - def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]": - """ - Runs the event processors on the event and returns the modified event. - """ - ty = event.get("type") - is_check_in = ty == "check_in" - - if not is_check_in: - # Get scopes without creating them to prevent infinite recursion - isolation_scope = _isolation_scope.get() - current_scope = _current_scope.get() - - event_processors = chain( - global_event_processors, - _global_scope and _global_scope._event_processors or [], - isolation_scope and isolation_scope._event_processors or [], - current_scope and current_scope._event_processors or [], - ) - - for event_processor in event_processors: - new_event = event - with capture_internal_exceptions(): - new_event = event_processor(event, hint) - if new_event is None: - return self._drop(event_processor, "event processor") - event = new_event - - return event - - @_disable_capture - def apply_to_event( - self, - event: "Event", - hint: "Hint", - options: "Optional[Dict[str, Any]]" = None, - ) -> "Optional[Event]": - """Applies the information contained on the scope to the given event.""" - ty = event.get("type") - is_transaction = ty == "transaction" - is_check_in = ty == "check_in" - - # put all attachments into the hint. This lets callbacks play around - # with attachments. We also later pull this out of the hint when we - # create the envelope. - attachments_to_send = hint.get("attachments") or [] - for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: - attachments_to_send.append(attachment) - hint["attachments"] = attachments_to_send - - self._apply_contexts_to_event(event, hint, options) - - if is_check_in: - # Check-ins only support the trace context, strip all others - event["contexts"] = { - "trace": event.setdefault("contexts", {}).get("trace", {}) - } - - if not is_check_in: - self._apply_level_to_event(event, hint, options) - self._apply_fingerprint_to_event(event, hint, options) - self._apply_user_to_event(event, hint, options) - self._apply_transaction_name_to_event(event, hint, options) - self._apply_transaction_info_to_event(event, hint, options) - self._apply_tags_to_event(event, hint, options) - self._apply_extra_to_event(event, hint, options) - - if not is_transaction and not is_check_in: - self._apply_breadcrumbs_to_event(event, hint, options) - self._apply_flags_to_event(event, hint, options) - - event = self.run_error_processors(event, hint) - if event is None: - return None - - event = self.run_event_processors(event, hint) - if event is None: - return None - - return event - - @_disable_capture - def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None: - # Attributes-based events and telemetry go through here (logs, metrics, - # spansV2) - if not isinstance(telemetry, StreamedSpan): - trace_context = self.get_trace_context() - trace_id = trace_context.get("trace_id") - if telemetry.get("trace_id") is None and trace_id is not None: - telemetry["trace_id"] = trace_id - - # span_id should only be populated if there's an active span. We can't - # use the trace_context here because it synthesizes a span_id if there - # isn't one - if telemetry.get("span_id") is None: - if self._span is not None and not isinstance( - self._span, (NoOpStreamedSpan, NoOpSpan) - ): - telemetry["span_id"] = self._span.span_id - else: - external_propagation_context = get_external_propagation_context() - if external_propagation_context: - _, span_id = external_propagation_context - if span_id is not None: - telemetry["span_id"] = span_id - - self._apply_scope_attributes_to_telemetry(telemetry) - self._apply_user_attributes_to_telemetry(telemetry) - - def update_from_scope(self, scope: "Scope") -> None: - """Update the scope with another scope's data.""" - if scope._level is not None: - self._level = scope._level - if scope._fingerprint is not None: - self._fingerprint = scope._fingerprint - if scope._transaction is not None: - self._transaction = scope._transaction - if scope._transaction_info is not None: - self._transaction_info.update(scope._transaction_info) - if scope._user is not None: - self._user = scope._user - if scope._tags: - self._tags.update(scope._tags) - if scope._contexts: - self._contexts.update(scope._contexts) - if scope._extras: - self._extras.update(scope._extras) - if scope._breadcrumbs: - self._breadcrumbs.extend(scope._breadcrumbs) - if scope._n_breadcrumbs_truncated: - self._n_breadcrumbs_truncated = ( - self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated - ) - if scope._gen_ai_original_message_count: - self._gen_ai_original_message_count.update( - scope._gen_ai_original_message_count - ) - if scope._gen_ai_conversation_id: - self._gen_ai_conversation_id = scope._gen_ai_conversation_id - if scope._span: - self._span = scope._span - if scope._attachments: - self._attachments.extend(scope._attachments) - if scope._profile: - self._profile = scope._profile - if scope._propagation_context: - self._propagation_context = scope._propagation_context - if scope._session: - self._session = scope._session - if scope._flags: - if not self._flags: - self._flags = deepcopy(scope._flags) - else: - for flag in scope._flags.get(): - self._flags.set(flag["flag"], flag["result"]) - if scope._attributes: - self._attributes.update(scope._attributes) - - def update_from_kwargs( - self, - user: "Optional[Any]" = None, - level: "Optional[LogLevelStr]" = None, - extras: "Optional[Dict[str, Any]]" = None, - contexts: "Optional[Dict[str, Dict[str, Any]]]" = None, - tags: "Optional[Dict[str, str]]" = None, - fingerprint: "Optional[List[str]]" = None, - attributes: "Optional[Attributes]" = None, - ) -> None: - """Update the scope's attributes.""" - if level is not None: - self._level = level - if user is not None: - self._user = user - if extras is not None: - self._extras.update(extras) - if contexts is not None: - self._contexts.update(contexts) - if tags is not None: - self._tags.update(tags) - if fingerprint is not None: - self._fingerprint = fingerprint - if attributes is not None: - self._attributes.update(attributes) - - def __repr__(self) -> str: - return "<%s id=%s name=%s type=%s>" % ( - self.__class__.__name__, - hex(id(self)), - self._name, - self._type, - ) - - @property - def flags(self) -> "FlagBuffer": - if self._flags is None: - max_flags = ( - self.get_client().options["_experiments"].get("max_flags") - or DEFAULT_FLAG_CAPACITY - ) - self._flags = FlagBuffer(capacity=max_flags) - return self._flags - - def set_attribute(self, attribute: str, value: "AttributeValue") -> None: - """ - Set an attribute on the scope. - - Any attributes-based telemetry (logs, metrics) captured while this scope - is active will inherit attributes set on the scope. - """ - self._attributes[attribute] = format_attribute(value) - - def remove_attribute(self, attribute: str) -> None: - """Remove an attribute if set on the scope. No-op if there is no such attribute.""" - try: - del self._attributes[attribute] - except KeyError: - pass - - -@contextmanager -def new_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current scope and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.new_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - new_scope = current_scope.fork() - token = _current_scope.set(new_scope) - - try: - yield new_scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_scope(scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `scope` and runs the wrapped code in it. - After the wrapped code is executed, the original scope is restored. - - Example Usage: - Suppose the variable `scope` contains a `Scope` object, which is not currently - the active scope. - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.use_scope(scope): - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # set given scope as current scope - token = _current_scope.set(scope) - - try: - yield scope - - finally: - try: - # restore original scope - _current_scope.reset(token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def isolation_scope() -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that forks the current isolation scope and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # fork isolation scope - isolation_scope = Scope.get_isolation_scope() - new_isolation_scope = isolation_scope.fork() - isolation_token = _isolation_scope.set(new_isolation_scope) - - try: - yield new_isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -@contextmanager -def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]": - """ - .. versionadded:: 2.0.0 - - Context manager that uses the given `isolation_scope` and runs the wrapped code in it. - The current scope is also forked to not bleed data into the existing current scope. - After the wrapped code is executed, the original scopes are restored. - - Example Usage: - - .. code-block:: python - - import sentry_sdk - - with sentry_sdk.isolation_scope() as scope: - scope.set_tag("color", "green") - sentry_sdk.capture_message("hello") # will include `color` tag. - - sentry_sdk.capture_message("hello, again") # will NOT include `color` tag. - - """ - # fork current scope - current_scope = Scope.get_current_scope() - forked_current_scope = current_scope.fork() - current_token = _current_scope.set(forked_current_scope) - - # set given scope as isolation scope - isolation_token = _isolation_scope.set(isolation_scope) - - try: - yield isolation_scope - - finally: - # restore original scopes - try: - _current_scope.reset(current_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - try: - _isolation_scope.reset(isolation_token) - except (LookupError, ValueError): - capture_internal_exception(sys.exc_info()) - - -def should_send_default_pii() -> bool: - """Shortcut for `Scope.get_client().should_send_default_pii()`.""" - return Scope.get_client().should_send_default_pii() - - -# Circular imports -from sentry_sdk.client import NonRecordingClient - -if TYPE_CHECKING: - import sentry_sdk.client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py deleted file mode 100644 index 6794491325..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/scrubber.py +++ /dev/null @@ -1,176 +0,0 @@ -from typing import TYPE_CHECKING, Dict, List, cast - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - iter_event_frames, -) - -if TYPE_CHECKING: - from typing import Optional - - from sentry_sdk._types import Event - - -DEFAULT_DENYLIST = [ - # stolen from relay - "password", - "passwd", - "secret", - "api_key", - "apikey", - "auth", - "credentials", - "mysql_pwd", - "privatekey", - "private_key", - "token", - "session", - # django - "csrftoken", - "sessionid", - # wsgi - "x_csrftoken", - "x_forwarded_for", - "set_cookie", - "cookie", - "authorization", - "proxy-authorization", - "x_api_key", - # other common names used in the wild - "aiohttp_session", # aiohttp - "connect.sid", # Express - "csrf_token", # Pyramid - "csrf", # (this is a cookie name used in accepted answers on stack overflow) - "_csrf", # Express - "_csrf_token", # Bottle - "PHPSESSID", # PHP - "_session", # Sanic - "symfony", # Symfony - "user_session", # Vue - "_xsrf", # Tornado - "XSRF-TOKEN", # Angular, Laravel -] - -DEFAULT_PII_DENYLIST = [ - "x_forwarded_for", - "x_real_ip", - "ip_address", - "remote_addr", -] - - -class EventScrubber: - def __init__( - self, - denylist: "Optional[List[str]]" = None, - recursive: bool = False, - send_default_pii: bool = False, - pii_denylist: "Optional[List[str]]" = None, - ) -> None: - """ - A scrubber that goes through the event payload and removes sensitive data configured through denylists. - - :param denylist: A security denylist that is always scrubbed, defaults to DEFAULT_DENYLIST. - :param recursive: Whether to scrub the event payload recursively, default False. - :param send_default_pii: Whether pii is sending is on, pii fields are not scrubbed. - :param pii_denylist: The denylist to use for scrubbing when pii is not sent, defaults to DEFAULT_PII_DENYLIST. - """ - self.denylist = DEFAULT_DENYLIST.copy() if denylist is None else denylist - - if not send_default_pii: - pii_denylist = ( - DEFAULT_PII_DENYLIST.copy() if pii_denylist is None else pii_denylist - ) - self.denylist += pii_denylist - - self.denylist = [x.lower() for x in self.denylist] - self.recursive = recursive - - def scrub_list(self, lst: object) -> None: - """ - If a list is passed to this method, the method recursively searches the list and any - nested lists for any dictionaries. The method calls scrub_dict on all dictionaries - it finds. - If the parameter passed to this method is not a list, the method does nothing. - """ - if not isinstance(lst, list): - return - - for v in lst: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_dict(self, d: object) -> None: - """ - If a dictionary is passed to this method, the method scrubs the dictionary of any - sensitive data. The method calls itself recursively on any nested dictionaries ( - including dictionaries nested in lists) if self.recursive is True. - This method does nothing if the parameter passed to it is not a dictionary. - """ - if not isinstance(d, dict): - return - - for k, v in d.items(): - # The cast is needed because mypy is not smart enough to figure out that k must be a - # string after the isinstance check. - if isinstance(k, str) and k.lower() in self.denylist: - d[k] = AnnotatedValue.substituted_because_contains_sensitive_data() - elif self.recursive: - self.scrub_dict(v) # no-op unless v is a dict - self.scrub_list(v) # no-op unless v is a list - - def scrub_request(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "request" in event: - if "headers" in event["request"]: - self.scrub_dict(event["request"]["headers"]) - if "cookies" in event["request"]: - self.scrub_dict(event["request"]["cookies"]) - if "data" in event["request"]: - self.scrub_dict(event["request"]["data"]) - - def scrub_extra(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "extra" in event: - self.scrub_dict(event["extra"]) - - def scrub_user(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "user" in event: - user = event["user"] - if "ip_address" in self.denylist and isinstance(user, dict): - user.pop("ip_address", None) - self.scrub_dict(user) - - def scrub_breadcrumbs(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "breadcrumbs" in event: - if ( - not isinstance(event["breadcrumbs"], AnnotatedValue) - and "values" in event["breadcrumbs"] - ): - for value in event["breadcrumbs"]["values"]: - if "data" in value: - self.scrub_dict(value["data"]) - - def scrub_frames(self, event: "Event") -> None: - with capture_internal_exceptions(): - for frame in iter_event_frames(event): - if "vars" in frame: - self.scrub_dict(frame["vars"]) - - def scrub_spans(self, event: "Event") -> None: - with capture_internal_exceptions(): - if "spans" in event: - for span in cast(List[Dict[str, object]], event["spans"]): - if "data" in span: - self.scrub_dict(span["data"]) - - def scrub_event(self, event: "Event") -> None: - self.scrub_request(event) - self.scrub_extra(event) - self.scrub_user(event) - self.scrub_breadcrumbs(event) - self.scrub_frames(event) - self.scrub_spans(event) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py deleted file mode 100644 index 6bf6f6e70b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/serializer.py +++ /dev/null @@ -1,418 +0,0 @@ -import math -import sys -from array import array -from collections.abc import Mapping -from datetime import datetime -from typing import TYPE_CHECKING - -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exception, - disable_capture_event, - format_timestamp, - safe_repr, - strip_string, -) - -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, ContextManager, Dict, List, Optional, Type, Union - - from sentry_sdk._types import NotImplementedType - - Span = Dict[str, Any] - - ReprProcessor = Callable[[Any, Dict[str, Any]], Union[NotImplementedType, str]] - Segment = Union[str, int] - - -# Bytes are technically not strings in Python 3, but we can serialize them -serializable_str_types = (str, bytes, bytearray, memoryview) - - -# Maximum length of JSON-serialized event payloads that can be safely sent -# before the server may reject the event due to its size. This is not intended -# to reflect actual values defined server-side, but rather only be an upper -# bound for events sent by the SDK. -# -# Can be overwritten if wanting to send more bytes, e.g. with a custom server. -# When changing this, keep in mind that events may be a little bit larger than -# this value due to attached metadata, so keep the number conservative. -MAX_EVENT_BYTES = 10**6 - -# Maximum depth and breadth of databags. Excess data will be trimmed. If -# max_request_body_size is "always", request bodies won't be trimmed. -MAX_DATABAG_DEPTH = 5 -MAX_DATABAG_BREADTH = 10 -CYCLE_MARKER = "" - - -global_repr_processors: "List[ReprProcessor]" = [] - - -def add_global_repr_processor(processor: "ReprProcessor") -> None: - global_repr_processors.append(processor) - - -sequence_types: "List[type]" = [tuple, list, set, frozenset, array] - - -def add_repr_sequence_type(ty: type) -> None: - sequence_types.append(ty) - - -class Memo: - __slots__ = ("_ids", "_objs") - - def __init__(self) -> None: - self._ids: "Dict[int, Any]" = {} - self._objs: "List[Any]" = [] - - def memoize(self, obj: "Any") -> "ContextManager[bool]": - self._objs.append(obj) - return self - - def __enter__(self) -> bool: - obj = self._objs[-1] - if id(obj) in self._ids: - return True - else: - self._ids[id(obj)] = obj - return False - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> None: - self._ids.pop(id(self._objs.pop()), None) - - -class _Serializer: - """Holds the state of a single serialize() call.""" - - __slots__ = ( - "memo", - "path", - "meta_stack", - "keep_request_bodies", - "max_value_length", - "is_vars", - "custom_repr", - ) - - def __init__( - self, - keep_request_bodies: bool, - max_value_length: "Optional[int]", - is_vars: bool, - custom_repr: "Optional[Callable[..., Optional[str]]]", - ) -> None: - self.memo = Memo() - self.path: "List[Segment]" = [] - self.meta_stack: "List[Dict[str, Any]]" = [] - self.keep_request_bodies = keep_request_bodies - self.max_value_length = max_value_length - self.is_vars = is_vars - self.custom_repr = custom_repr - - def _safe_repr_wrapper(self, value: "Any") -> str: - try: - repr_value = None - if self.custom_repr is not None: - repr_value = self.custom_repr(value) - return repr_value or safe_repr(value) - except Exception: - return safe_repr(value) - - def _annotate(self, **meta: "Any") -> None: - while len(self.meta_stack) <= len(self.path): - try: - segment = self.path[len(self.meta_stack) - 1] - node = self.meta_stack[-1].setdefault(str(segment), {}) - except IndexError: - node = {} - - self.meta_stack.append(node) - - self.meta_stack[-1].setdefault("", {}).update(meta) - - def _is_databag(self) -> "Optional[bool]": - """ - A databag is any value that we need to trim. - True for stuff like vars, request bodies, breadcrumbs and extra. - - :returns: `True` for "yes", `False` for :"no", `None` for "maybe soon". - """ - try: - if self.is_vars: - return True - - is_request_body = self._is_request_body() - if is_request_body in (True, None): - return is_request_body - - p0 = self.path[0] - if p0 == "breadcrumbs" and self.path[1] == "values": - self.path[2] - return True - - if p0 == "extra": - return True - - except IndexError: - return None - - return False - - def _is_span_attribute(self) -> "Optional[bool]": - try: - if self.path[0] == "spans" and self.path[2] == "data": - return True - except IndexError: - return None - - return False - - def _is_request_body(self) -> "Optional[bool]": - try: - if self.path[0] == "request" and self.path[1] == "data": - return True - except IndexError: - return None - - return False - - def _serialize_node( - self, - obj: "Any", - is_databag: "Optional[bool]" = None, - is_request_body: "Optional[bool]" = None, - should_repr_strings: "Optional[bool]" = None, - segment: "Optional[Segment]" = None, - remaining_breadth: "Optional[Union[int, float]]" = None, - remaining_depth: "Optional[Union[int, float]]" = None, - ) -> "Any": - if segment is not None: - self.path.append(segment) - - try: - with self.memo.memoize(obj) as result: - if result: - return CYCLE_MARKER - - return self._serialize_node_impl( - obj, - is_databag=is_databag, - is_request_body=is_request_body, - should_repr_strings=should_repr_strings, - remaining_depth=remaining_depth, - remaining_breadth=remaining_breadth, - ) - except BaseException: - capture_internal_exception(sys.exc_info()) - - if is_databag: - return "" - - return None - finally: - if segment is not None: - self.path.pop() - del self.meta_stack[len(self.path) + 1 :] - - def _flatten_annotated(self, obj: "Any") -> "Any": - if isinstance(obj, AnnotatedValue): - self._annotate(**obj.metadata) - obj = obj.value - return obj - - def _serialize_node_impl( - self, - obj: "Any", - is_databag: "Optional[bool]", - is_request_body: "Optional[bool]", - should_repr_strings: "Optional[bool]", - remaining_depth: "Optional[Union[float, int]]", - remaining_breadth: "Optional[Union[float, int]]", - ) -> "Any": - if isinstance(obj, AnnotatedValue): - should_repr_strings = False - if should_repr_strings is None: - should_repr_strings = self.is_vars - - if is_databag is None: - is_databag = self._is_databag() - - if is_request_body is None: - is_request_body = self._is_request_body() - - if is_databag: - if is_request_body and self.keep_request_bodies: - remaining_depth = float("inf") - remaining_breadth = float("inf") - else: - if remaining_depth is None: - remaining_depth = MAX_DATABAG_DEPTH - if remaining_breadth is None: - remaining_breadth = MAX_DATABAG_BREADTH - - obj = self._flatten_annotated(obj) - - if remaining_depth is not None and remaining_depth <= 0: - self._annotate(rem=[["!limit", "x"]]) - if is_databag: - return self._flatten_annotated( - strip_string( - self._safe_repr_wrapper(obj), max_length=self.max_value_length - ) - ) - return None - - is_span_attribute = self._is_span_attribute() - if (is_databag or is_span_attribute) and global_repr_processors: - hints = {"memo": self.memo, "remaining_depth": remaining_depth} - for processor in global_repr_processors: - result = processor(obj, hints) - if result is not NotImplemented: - return self._flatten_annotated(result) - - sentry_repr = getattr(type(obj), "__sentry_repr__", None) - - if obj is None or isinstance(obj, (bool, int, float)): - if should_repr_strings or ( - isinstance(obj, float) and (math.isinf(obj) or math.isnan(obj)) - ): - return self._safe_repr_wrapper(obj) - else: - return obj - - elif callable(sentry_repr): - return sentry_repr(obj) - - elif isinstance(obj, datetime): - return ( - str(format_timestamp(obj)) - if not should_repr_strings - else self._safe_repr_wrapper(obj) - ) - - elif isinstance(obj, Mapping): - # Create temporary copy here to avoid calling too much code that - # might mutate our dictionary while we're still iterating over it. - obj = dict(obj.items()) - - rv_dict: "Dict[str, Any]" = {} - i = 0 - - for k, v in obj.items(): - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) - break - - str_k = str(k) - v = self._serialize_node( - v, - segment=str_k, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - rv_dict[str_k] = v - i += 1 - - return rv_dict - - elif not isinstance(obj, serializable_str_types) and isinstance( - obj, tuple(sequence_types) - ): - rv_list = [] - - for i, v in enumerate(obj): # type: ignore[arg-type] - if remaining_breadth is not None and i >= remaining_breadth: - self._annotate(len=len(obj)) # type: ignore[arg-type] - break - - rv_list.append( - self._serialize_node( - v, - segment=i, - should_repr_strings=should_repr_strings, - is_databag=is_databag, - is_request_body=is_request_body, - remaining_depth=( - remaining_depth - 1 if remaining_depth is not None else None - ), - remaining_breadth=remaining_breadth, - ) - ) - - return rv_list - - if should_repr_strings: - obj = self._safe_repr_wrapper(obj) - else: - if isinstance(obj, bytes) or isinstance(obj, bytearray): - obj = obj.decode("utf-8", "replace") - - if not isinstance(obj, str): - obj = self._safe_repr_wrapper(obj) - - is_span_description = ( - len(self.path) == 3 - and self.path[0] == "spans" - and self.path[-1] == "description" - ) - if is_span_description: - return obj - - return self._flatten_annotated( - strip_string(obj, max_length=self.max_value_length) - ) - - -def serialize(event: "Dict[str, Any]", **kwargs: "Any") -> "Dict[str, Any]": - """ - A very smart serializer that takes a dict and emits a json-friendly dict. - Currently used for serializing the final Event and also prematurely while fetching the stack - local variables for each frame in a stacktrace. - - It works internally with 'databags' which are arbitrary data structures like Mapping, Sequence and Set. - The algorithm itself is a recursive graph walk down the data structures it encounters. - - It has the following responsibilities: - * Trimming databags and keeping them within MAX_DATABAG_BREADTH and MAX_DATABAG_DEPTH. - * Calling safe_repr() on objects appropriately to keep them informative and readable in the final payload. - * Annotating the payload with the _meta field whenever trimming happens. - - :param max_request_body_size: If set to "always", will never trim request bodies. - :param max_value_length: The max length to strip strings to, or None to disable string truncation. Defaults to None. - :param is_vars: If we're serializing vars early, we want to repr() things that are JSON-serializable to make their type more apparent. For example, it's useful to see the difference between a unicode-string and a bytestring when viewing a stacktrace. - :param custom_repr: A custom repr function that runs before safe_repr on the object to be serialized. If it returns None or throws internally, we will fallback to safe_repr. - - """ - serializer = _Serializer( - keep_request_bodies=kwargs.pop("max_request_body_size", None) == "always", - max_value_length=kwargs.pop("max_value_length", None), - is_vars=kwargs.pop("is_vars", False), - custom_repr=kwargs.pop("custom_repr", None), - ) - - disable_capture_event.set(True) - try: - serialized_event = serializer._serialize_node(event, **kwargs) - if ( - not serializer.is_vars - and serializer.meta_stack - and isinstance(serialized_event, dict) - ): - serialized_event["_meta"] = serializer.meta_stack[0] - - return serialized_event - finally: - disable_capture_event.set(False) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py deleted file mode 100644 index 3ffd071bbc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/session.py +++ /dev/null @@ -1,165 +0,0 @@ -import uuid -from datetime import datetime, timezone -from typing import TYPE_CHECKING - -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Union - - from sentry_sdk._types import SessionStatus - - -def _minute_trunc(ts: "datetime") -> "datetime": - return ts.replace(second=0, microsecond=0) - - -def _make_uuid( - val: "Union[str, uuid.UUID]", -) -> "uuid.UUID": - if isinstance(val, uuid.UUID): - return val - return uuid.UUID(val) - - -class Session: - def __init__( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - session_mode: str = "application", - ) -> None: - if sid is None: - sid = uuid.uuid4() - if started is None: - started = datetime.now(timezone.utc) - if status is None: - status = "ok" - self.status = status - self.did: "Optional[str]" = None - self.started = started - self.release: "Optional[str]" = None - self.environment: "Optional[str]" = None - self.duration: "Optional[float]" = None - self.user_agent: "Optional[str]" = None - self.ip_address: "Optional[str]" = None - self.session_mode: str = session_mode - self.errors = 0 - - self.update( - sid=sid, - did=did, - timestamp=timestamp, - duration=duration, - release=release, - environment=environment, - user_agent=user_agent, - ip_address=ip_address, - errors=errors, - user=user, - ) - - @property - def truncated_started(self) -> "datetime": - return _minute_trunc(self.started) - - def update( - self, - sid: "Optional[Union[str, uuid.UUID]]" = None, - did: "Optional[str]" = None, - timestamp: "Optional[datetime]" = None, - started: "Optional[datetime]" = None, - duration: "Optional[float]" = None, - status: "Optional[SessionStatus]" = None, - release: "Optional[str]" = None, - environment: "Optional[str]" = None, - user_agent: "Optional[str]" = None, - ip_address: "Optional[str]" = None, - errors: "Optional[int]" = None, - user: "Optional[Any]" = None, - ) -> None: - # If a user is supplied we pull some data form it - if user: - if ip_address is None: - ip_address = user.get("ip_address") - if did is None: - did = user.get("id") or user.get("email") or user.get("username") - - if sid is not None: - self.sid = _make_uuid(sid) - if did is not None: - self.did = str(did) - if timestamp is None: - timestamp = datetime.now(timezone.utc) - self.timestamp = timestamp - if started is not None: - self.started = started - if duration is not None: - self.duration = duration - if release is not None: - self.release = release - if environment is not None: - self.environment = environment - if ip_address is not None: - self.ip_address = ip_address - if user_agent is not None: - self.user_agent = user_agent - if errors is not None: - self.errors = errors - - if status is not None: - self.status = status - - def close( - self, - status: "Optional[SessionStatus]" = None, - ) -> "Any": - if status is None and self.status == "ok": - status = "exited" - if status is not None: - self.update(status=status) - - def get_json_attrs( - self, - with_user_info: "Optional[bool]" = True, - ) -> "Any": - attrs = {} - if self.release is not None: - attrs["release"] = self.release - if self.environment is not None: - attrs["environment"] = self.environment - if with_user_info: - if self.ip_address is not None: - attrs["ip_address"] = self.ip_address - if self.user_agent is not None: - attrs["user_agent"] = self.user_agent - return attrs - - def to_json(self) -> "Any": - rv: "Dict[str, Any]" = { - "sid": str(self.sid), - "init": True, - "started": format_timestamp(self.started), - "timestamp": format_timestamp(self.timestamp), - "status": self.status, - } - if self.errors: - rv["errors"] = self.errors - if self.did is not None: - rv["did"] = self.did - if self.duration is not None: - rv["duration"] = self.duration - attrs = self.get_json_attrs() - if attrs: - rv["attrs"] = attrs - return rv diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py deleted file mode 100644 index aabf874fcd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/sessions.py +++ /dev/null @@ -1,262 +0,0 @@ -import os -import warnings -from contextlib import contextmanager -from threading import Event, Lock, Thread -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.envelope import Envelope -from sentry_sdk.session import Session -from sentry_sdk.utils import format_timestamp - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, List, Optional, Union - - -def is_auto_session_tracking_enabled( - hub: "Optional[sentry_sdk.Hub]" = None, -) -> "Union[Any, bool, None]": - """DEPRECATED: Utility function to find out if session tracking is enabled.""" - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - - should_track = hub.scope._force_auto_session_tracking - - if should_track is None: - client_options = hub.client.options if hub.client else {} - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking( - hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: Use track_session instead - Starts and stops a session automatically around a block. - """ - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "Use track_session instead.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - should_track = is_auto_session_tracking_enabled(hub) - if should_track: - hub.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - hub.end_session() - - -def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: - """ - DEPRECATED: Utility function to find out if session tracking is enabled. - """ - - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - return _is_auto_session_tracking_enabled(scope) - - -def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: - """ - Utility function to find out if session tracking is enabled. - """ - - should_track = scope._force_auto_session_tracking - if should_track is None: - client_options = sentry_sdk.get_client().options - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking_scope( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: This function is a deprecated alias for track_session. - Starts and stops a session automatically around a block. - """ - - warnings.warn( - "This function is a deprecated alias for track_session and will be removed in the next major release.", - DeprecationWarning, - stacklevel=2, - ) - - with track_session(scope, session_mode=session_mode): - yield - - -@contextmanager -def track_session( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """ - Start a new session in the provided scope, assuming session tracking is enabled. - This is a no-op context manager if session tracking is not enabled. - """ - - should_track = _is_auto_session_tracking_enabled(scope) - if should_track: - scope.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - scope.end_session() - - -TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed") -MAX_ENVELOPE_ITEMS = 100 - - -def make_aggregate_envelope(aggregate_states: "Any", attrs: "Any") -> "Any": - return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())} - - -class SessionFlusher: - def __init__( - self, - capture_func: "Callable[[Envelope], None]", - flush_interval: int = 60, - ) -> None: - self.capture_func = capture_func - self.flush_interval = flush_interval - self.pending_sessions: "List[Any]" = [] - self.pending_aggregates: "Dict[Any, Any]" = {} - self._thread: "Optional[Thread]" = None - self._thread_lock = Lock() - self._aggregate_lock = Lock() - self._thread_for_pid: "Optional[int]" = None - self.__shutdown_requested = Event() - - def flush(self) -> None: - pending_sessions = self.pending_sessions - self.pending_sessions = [] - - with self._aggregate_lock: - pending_aggregates = self.pending_aggregates - self.pending_aggregates = {} - - envelope = Envelope() - for session in pending_sessions: - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_session(session) - - for attrs, states in pending_aggregates.items(): - if len(envelope.items) == MAX_ENVELOPE_ITEMS: - self.capture_func(envelope) - envelope = Envelope() - - envelope.add_sessions(make_aggregate_envelope(states, attrs)) - - if len(envelope.items) > 0: - self.capture_func(envelope) - - def _ensure_running(self) -> None: - """ - Check that we have an active thread to run in, or create one if not. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self._running - will be False after running this function. - """ - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - with self._thread_lock: - if self._thread_for_pid == os.getpid() and self._thread is not None: - return None - - def _thread() -> None: - running = True - while running: - running = not self.__shutdown_requested.wait(self.flush_interval) - self.flush() - - thread = Thread(target=_thread) - thread.daemon = True - try: - thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.__shutdown_requested.set() - return None - - self._thread = thread - self._thread_for_pid = os.getpid() - - return None - - def add_aggregate_session( - self, - session: "Session", - ) -> None: - # NOTE on `session.did`: - # the protocol can deal with buckets that have a distinct-id, however - # in practice we expect the python SDK to have an extremely high cardinality - # here, effectively making aggregation useless, therefore we do not - # aggregate per-did. - - # For this part we can get away with using the global interpreter lock - with self._aggregate_lock: - attrs = session.get_json_attrs(with_user_info=False) - primary_key = tuple(sorted(attrs.items())) - secondary_key = session.truncated_started # (, session.did) - states = self.pending_aggregates.setdefault(primary_key, {}) - state = states.setdefault(secondary_key, {}) - - if "started" not in state: - state["started"] = format_timestamp(session.truncated_started) - # if session.did is not None: - # state["did"] = session.did - if session.status == "crashed": - state["crashed"] = state.get("crashed", 0) + 1 - elif session.status == "abnormal": - state["abnormal"] = state.get("abnormal", 0) + 1 - elif session.errors > 0: - state["errored"] = state.get("errored", 0) + 1 - else: - state["exited"] = state.get("exited", 0) + 1 - - def add_session( - self, - session: "Session", - ) -> None: - if session.session_mode == "request": - self.add_aggregate_session(session) - else: - self.pending_sessions.append(session.to_json()) - self._ensure_running() - - def kill(self) -> None: - self.__shutdown_requested.set() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py deleted file mode 100644 index 2dcc86bc47..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/spotlight.py +++ /dev/null @@ -1,327 +0,0 @@ -import io -import logging -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from itertools import chain, product -from typing import TYPE_CHECKING - -import urllib3 - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Self - -from sentry_sdk.envelope import Envelope -from sentry_sdk.utils import ( - capture_internal_exceptions, - env_to_bool, -) -from sentry_sdk.utils import ( - logger as sentry_logger, -) - -logger = logging.getLogger("spotlight") - - -DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream" -DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware" - - -class SpotlightClient: - """ - A client for sending envelopes to Sentry Spotlight. - - Implements exponential backoff retry logic per the SDK spec: - - Logs error at least once when server is unreachable - - Does not log for every failed envelope - - Uses exponential backoff to avoid hammering an unavailable server - - Never blocks normal Sentry operation - """ - - # Exponential backoff settings - INITIAL_RETRY_DELAY = 1.0 # Start with 1 second - MAX_RETRY_DELAY = 60.0 # Max 60 seconds - - def __init__(self, url: str) -> None: - self.url = url - self.http = urllib3.PoolManager() - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time: float = 0.0 - - def capture_envelope(self, envelope: "Envelope") -> None: - # Check if we're in backoff period - skip sending to avoid blocking - if self._last_error_time > 0: - time_since_error = time.time() - self._last_error_time - if time_since_error < self._retry_delay: - # Still in backoff period, skip this envelope - return - - body = io.BytesIO() - envelope.serialize_into(body) - try: - req = self.http.request( - url=self.url, - body=body.getvalue(), - method="POST", - headers={ - "Content-Type": "application/x-sentry-envelope", - }, - ) - req.close() - # Success - reset backoff state - self._retry_delay = self.INITIAL_RETRY_DELAY - self._last_error_time = 0.0 - except Exception as e: - self._last_error_time = time.time() - - # Increase backoff delay exponentially first, so logged value matches actual wait - self._retry_delay = min(self._retry_delay * 2, self.MAX_RETRY_DELAY) - - # Log error once per backoff cycle (we skip sends during backoff, so only one failure per cycle) - sentry_logger.warning( - "Failed to send envelope to Spotlight at %s: %s. " - "Will retry after %.1f seconds.", - self.url, - e, - self._retry_delay, - ) - - -try: - from django.conf import settings - from django.http import HttpRequest, HttpResponse, HttpResponseServerError - from django.utils.deprecation import MiddlewareMixin - - SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js" - SPOTLIGHT_JS_SNIPPET_PATTERN = ( - "\n" - '\n' - ) - SPOTLIGHT_ERROR_PAGE_SNIPPET = ( - '\n' - '\n' - ) - CHARSET_PREFIX = "charset=" - BODY_TAG_NAME = "body" - BODY_CLOSE_TAG_POSSIBILITIES = tuple( - "".format("".join(chars)) - for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower())) - ) - - class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc] - _spotlight_script: "Optional[str]" = None - _spotlight_url: "Optional[str]" = None - - def __init__(self: "Self", get_response: "Callable[..., HttpResponse]") -> None: - super().__init__(get_response) - - import sentry_sdk.api - - self.sentry_sdk = sentry_sdk.api - - spotlight_client = self.sentry_sdk.get_client().spotlight - if spotlight_client is None: - sentry_logger.warning( - "Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware." - ) - return None - # Spotlight URL has a trailing `/stream` part at the end so split it off - self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../") - - @property - def spotlight_script(self: "Self") -> "Optional[str]": - if self._spotlight_url is not None and self._spotlight_script is None: - try: - spotlight_js_url = urllib.parse.urljoin( - self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH - ) - req = urllib.request.Request( - spotlight_js_url, - method="HEAD", - ) - urllib.request.urlopen(req) - self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format( - spotlight_url=self._spotlight_url, - spotlight_js_url=spotlight_js_url, - ) - except urllib.error.URLError as err: - sentry_logger.debug( - "Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.", - spotlight_js_url, - exc_info=err, - ) - - return self._spotlight_script - - def process_response( - self: "Self", _request: "HttpRequest", response: "HttpResponse" - ) -> "Optional[HttpResponse]": - content_type_header = tuple( - p.strip() - for p in response.headers.get("Content-Type", "").lower().split(";") - ) - content_type = content_type_header[0] - if len(content_type_header) > 1 and content_type_header[1].startswith( - CHARSET_PREFIX - ): - encoding = content_type_header[1][len(CHARSET_PREFIX) :] - else: - encoding = "utf-8" - - if ( - self.spotlight_script is not None - and not response.streaming - and content_type == "text/html" - ): - content_length = len(response.content) - injection = self.spotlight_script.encode(encoding) - injection_site = next( - ( - idx - for idx in ( - response.content.rfind(body_variant.encode(encoding)) - for body_variant in BODY_CLOSE_TAG_POSSIBILITIES - ) - if idx > -1 - ), - content_length, - ) - - # This approach works even when we don't have a `` tag - response.content = ( - response.content[:injection_site] - + injection - + response.content[injection_site:] - ) - - if response.has_header("Content-Length"): - response.headers["Content-Length"] = content_length + len(injection) - - return response - - def process_exception( - self: "Self", _request: "HttpRequest", exception: Exception - ) -> "Optional[HttpResponseServerError]": - if not settings.DEBUG or not self._spotlight_url: - return None - - try: - spotlight = ( - urllib.request.urlopen(self._spotlight_url).read().decode("utf-8") - ) - except urllib.error.URLError: - return None - else: - event_id = self.sentry_sdk.capture_exception(exception) - return HttpResponseServerError( - spotlight.replace( - "", - SPOTLIGHT_ERROR_PAGE_SNIPPET.format( - spotlight_url=self._spotlight_url, event_id=event_id - ), - ) - ) - -except ImportError: - settings = None - - -def _resolve_spotlight_url( - spotlight_config: "Any", sentry_logger: "Any" -) -> "Optional[str]": - """ - Resolve the Spotlight URL based on config and environment variable. - - Implements precedence rules per the SDK spec: - https://develop.sentry.dev/sdk/expected-features/spotlight/ - - Returns the resolved URL string, or None if Spotlight should be disabled. - """ - spotlight_env_value = os.environ.get("SENTRY_SPOTLIGHT") - - # Parse env var to determine if it's a boolean or URL - spotlight_from_env: "Optional[bool]" = None - spotlight_env_url: "Optional[str]" = None - if spotlight_env_value: - parsed = env_to_bool(spotlight_env_value, strict=True) - if parsed is None: - # It's a URL string - spotlight_from_env = True - spotlight_env_url = spotlight_env_value - else: - spotlight_from_env = parsed - - # Apply precedence rules per spec: - # https://develop.sentry.dev/sdk/expected-features/spotlight/#precedence-rules - if spotlight_config is False: - # Config explicitly disables spotlight - warn if env var was set - if spotlight_from_env: - sentry_logger.warning( - "Spotlight is disabled via spotlight=False config option, " - "ignoring SENTRY_SPOTLIGHT environment variable." - ) - return None - elif spotlight_config is True: - # Config enables spotlight with boolean true - # If env var has URL, use env var URL per spec - if spotlight_env_url: - return spotlight_env_url - else: - return DEFAULT_SPOTLIGHT_URL - elif isinstance(spotlight_config, str): - # Config has URL string - use config URL, warn if env var differs - if spotlight_env_value and spotlight_env_value != spotlight_config: - sentry_logger.warning( - "Spotlight URL from config (%s) takes precedence over " - "SENTRY_SPOTLIGHT environment variable (%s).", - spotlight_config, - spotlight_env_value, - ) - return spotlight_config - elif spotlight_config is None: - # No config - use env var - if spotlight_env_url: - return spotlight_env_url - elif spotlight_from_env: - return DEFAULT_SPOTLIGHT_URL - # else: stays None (disabled) - - return None - - -def setup_spotlight(options: "Dict[str, Any]") -> "Optional[SpotlightClient]": - url = _resolve_spotlight_url(options.get("spotlight"), sentry_logger) - - if url is None: - return None - - # Only set up logging handler when spotlight is actually enabled - _handler = logging.StreamHandler(sys.stderr) - _handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s")) - logger.addHandler(_handler) - logger.setLevel(logging.INFO) - - # Update options with resolved URL for consistency - options["spotlight"] = url - - with capture_internal_exceptions(): - if ( - settings is not None - and settings.DEBUG - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1")) - and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1")) - ): - middleware = settings.MIDDLEWARE - if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware: - settings.MIDDLEWARE = type(middleware)( - chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,)) - ) - logger.info("Enabled Spotlight integration for Django") - - client = SpotlightClient(url) - logger.info("Enabled Spotlight using sidecar at %s", url) - - return client diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py deleted file mode 100644 index 5ee7e8460b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/traces.py +++ /dev/null @@ -1,843 +0,0 @@ -""" -EXPERIMENTAL. Do not use in production. - -The API in this file is only meant to be used in span streaming mode. - -You can enable span streaming mode via -sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}). -""" - -import sys -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import SPANDATA -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) -from sentry_sdk.tracing_utils import Baggage -from sentry_sdk.utils import ( - capture_internal_exceptions, - format_attribute, - get_current_thread_meta, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Iterator, - Optional, - ParamSpec, - TypeVar, - Union, - overload, - ) - - from sentry_sdk._types import Attributes, AttributeValue, SpanJSON - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - - P = ParamSpec("P") - R = TypeVar("R") - - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -class SpanStatus(str, Enum): - OK = "ok" - ERROR = "error" - - def __str__(self) -> str: - return self.value - - -_VALID_SPAN_STATUSES = frozenset(e.value for e in SpanStatus) - - -# Segment source, see -# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource -class SegmentSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_SEGMENT_SOURCES = [ - SegmentSource.URL, -] - - -SOURCE_FOR_STYLE = { - "endpoint": SegmentSource.COMPONENT, - "function_name": SegmentSource.COMPONENT, - "handler_name": SegmentSource.COMPONENT, - "method_and_path_pattern": SegmentSource.ROUTE, - "path": SegmentSource.URL, - "route_name": SegmentSource.COMPONENT, - "route_pattern": SegmentSource.ROUTE, - "uri_template": SegmentSource.ROUTE, - "url": SegmentSource.ROUTE, -} - - -# Sentinel value for an unset parent_span to be able to distinguish it from -# a None set by the user -_DEFAULT_PARENT_SPAN = object() - - -def start_span( - name: str, - attributes: "Optional[Attributes]" = None, - parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment] - active: bool = True, -) -> "StreamedSpan": - """ - Start a span. - - EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span() - instead. - - The span's parent, unless provided explicitly via the `parent_span` argument, - will be the current active span, if any. If there is none, this span will - become the root of a new span tree. If you explicitly want this span to be - top-level without a parent, set `parent_span=None`. - - `start_span()` can either be used as context manager or you can use the span - object it returns and explicitly end it via `span.end()`. The following is - equivalent: - - ```python - import sentry_sdk - - with sentry_sdk.traces.start_span(name="My Span"): - # do something - - # The span automatically finishes once the `with` block is exited - ``` - - ```python - import sentry_sdk - - span = sentry_sdk.traces.start_span(name="My Span") - # do something - span.end() - ``` - - To continue a trace from another service, call - `sentry_sdk.traces.continue_trace()` prior to creating a top-level span. - - :param name: The name to identify this span by. - :type name: str - - :param attributes: Key-value attributes to set on the span from the start. - These will also be accessible in the traces sampler. - :type attributes: "Optional[Attributes]" - - :param parent_span: A span instance that the new span should consider its - parent. If not provided, the parent will be set to the currently active - span, if any. If set to `None`, this span will become a new root-level - span. - :type parent_span: "Optional[StreamedSpan]" - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to `False`. - :type active: bool - - :return: The span that has been started. - :rtype: StreamedSpan - """ - from sentry_sdk.tracing_utils import has_span_streaming_enabled - - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "sentry_sdk.start_transaction() and sentry_sdk.start_span() " - "instead.", - stacklevel=2, - ) - return NoOpStreamedSpan() - - return sentry_sdk.get_current_scope().start_streamed_span( - name, attributes, parent_span, active - ) - - -def continue_trace(incoming: "dict[str, Any]") -> None: - """ - Continue a trace from headers or environment variables. - - EXPERIMENTAL. Use sentry_sdk.continue_trace() instead. - - This function sets the propagation context on the scope. Any span started - in the updated scope will belong under the trace extracted from the - provided propagation headers or environment variables. - - continue_trace() doesn't start any spans on its own. Use the start_span() - API for that. - """ - # This is set both on the isolation and the current scope for compatibility - # reasons. Conceptually, it belongs on the isolation scope, and it also - # used to be set there in non-span-first mode. But in span first mode, we - # start spans on the current scope, regardless of type, like JS does, so we - # need to set the propagation context there. - sentry_sdk.get_isolation_scope().generate_propagation_context( - incoming, - ) - sentry_sdk.get_current_scope().generate_propagation_context( - incoming, - ) - - -def new_trace() -> None: - """ - Resets the propagation context, forcing a new trace. - - EXPERIMENTAL. - - This function sets the propagation context on the scope. Any span started - in the updated scope will start its own trace. - - new_trace() doesn't start any spans on its own. Use the start_span() API - for that. - """ - sentry_sdk.get_isolation_scope().set_new_propagation_context() - sentry_sdk.get_current_scope().set_new_propagation_context() - - -class StreamedSpan: - """ - A span holds timing information of a block of code. - - Spans can have multiple child spans, thus forming a span tree. - - This is the Span First span implementation that streams spans. The original - transaction-based span implementation lives in tracing.Span. - """ - - __slots__ = ( - "_name", - "_attributes", - "_active", - "_span_id", - "_trace_id", - "_parent_span_id", - "_segment", - "_parent_sampled", - "_start_timestamp", - "_start_timestamp_monotonic_ns", - "_end_timestamp", - "_status", - "_scope", - "_previous_span_on_scope", - "_baggage", - "_sample_rand", - "_sample_rate", - "_continuous_profile", - ) - - def __init__( - self, - *, - name: str, - attributes: "Optional[Attributes]" = None, - active: bool = True, - scope: "sentry_sdk.Scope", - segment: "Optional[StreamedSpan]" = None, - trace_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - sample_rate: "Optional[float]" = None, - sample_rand: "Optional[float]" = None, - ): - self._name: str = name - self._active: bool = active - self._attributes: "Attributes" = { - "sentry.origin": "manual", - "sentry.trace_lifecycle": "stream", - } - - if attributes: - for attribute, value in attributes.items(): - self.set_attribute(attribute, value) - - self._scope = scope - - self._segment = segment or self - - self._trace_id: "Optional[str]" = trace_id - self._parent_span_id = parent_span_id - self._parent_sampled = parent_sampled - self._baggage = baggage - self._sample_rand = sample_rand - self._sample_rate = sample_rate - - self._start_timestamp = datetime.now(timezone.utc) - self._end_timestamp: "Optional[datetime]" = None - - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - - self._span_id: "Optional[str]" = None - - self._status = SpanStatus.OK.value - - self._update_active_thread() - - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._start_profile() - self._set_profile_id(get_profiler_id()) - - self._set_segment_attributes() - - self._start() - - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__}(" - f"name={self._name}, " - f"trace_id={self.trace_id}, " - f"span_id={self.span_id}, " - f"parent_span_id={self._parent_span_id}, " - f"active={self._active})>" - ) - - def __enter__(self) -> "StreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore - return - - if value is not None and should_be_treated_as_error(ty, value): - self.status = SpanStatus.ERROR.value - - self._end() - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - """ - Finish this span and queue it for sending. - - :param end_timestamp: End timestamp to use instead of current time. - :type end_timestamp: "Optional[Union[float, datetime]]" - """ - self._end(end_timestamp) - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self.end(end_timestamp) - - def _start(self) -> None: - if self._active: - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._end_timestamp is not None: - # This span is already finished, ignore. - return - - # Stop the profiler - if self._is_segment() and self._continuous_profile is not None: - with capture_internal_exceptions(): - self._continuous_profile.stop() - - # Detach from scope - if self._active: - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - # Set attributes from the segment. These are set on span end on purpose - # so that we have the best chance to capture the segment's final name - # (since it might change during its lifetime) - self.set_attribute("sentry.segment.id", self._segment.span_id) - self.set_attribute("sentry.segment.name", self._segment.name) - - # Set the end timestamp - if end_timestamp is not None: - if isinstance(end_timestamp, (float, int)): - try: - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - except Exception: - pass - - if isinstance(end_timestamp, datetime): - self._end_timestamp = end_timestamp - else: - logger.debug( - "[Tracing] Failed to set end_timestamp. Using current time instead." - ) - - if self._end_timestamp is None: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self._end_timestamp = self._start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - - client = sentry_sdk.get_client() - if not client.is_active(): - return - - # Finally, queue the span for sending to Sentry - self._scope._capture_span(self) - - def get_attributes(self) -> "Attributes": - return self._attributes - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - self._attributes[key] = format_attribute(value) - - def set_attributes(self, attributes: "Attributes") -> None: - for key, value in attributes.items(): - self.set_attribute(key, value) - - def remove_attribute(self, key: str) -> None: - try: - del self._attributes[key] - except KeyError: - pass - - @property - def status(self) -> "str": - return self._status - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - if isinstance(status, Enum): - status = status.value - - if status not in _VALID_SPAN_STATUSES: - logger.debug( - f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"' - ) - return - - self._status = status - - @property - def name(self) -> str: - return self._name - - @name.setter - def name(self, name: str) -> None: - self._name = name - - @property - def active(self) -> bool: - return self._active - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @property - def sampled(self) -> "Optional[bool]": - return True - - @property - def start_timestamp(self) -> "Optional[datetime]": - return self._start_timestamp - - @property - def end_timestamp(self) -> "Optional[datetime]": - return self._end_timestamp - - def _is_segment(self) -> bool: - return self._segment is self - - def _update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - - if thread_id is not None: - self.set_attribute(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_attribute(SPANDATA.THREAD_NAME, thread_name) - - def _dynamic_sampling_context(self) -> "dict[str, str]": - return self._segment._get_baggage().dynamic_sampling_context() - - def _to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def _to_baggage(self) -> "Optional[Baggage]": - if self._segment: - return self._segment._get_baggage() - return None - - def _get_baggage(self) -> "Baggage": - """ - Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with - the segment. - - The first time a new baggage with Sentry items is made, it will be frozen. - """ - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_segment(self) - - return self._baggage - - def _iter_headers(self) -> "Iterator[tuple[str, str]]": - if not self._segment: - return - - yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent() - - baggage = self._segment._get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def _get_trace_context(self) -> "dict[str, Any]": - # Even if spans themselves are not event-based anymore, we need this - # to populate trace context on events - context: "dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self._parent_span_id, - "dynamic_sampling_context": self._dynamic_sampling_context(), - } - - if "sentry.op" in self._attributes: - context["op"] = self._attributes["sentry.op"] - if "sentry.origin" in self._attributes: - context["origin"] = self._attributes["sentry.origin"] - - return context - - def _set_profile_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_attribute("sentry.profiler_id", profiler_id) - - def _start_profile(self) -> None: - if not self._is_segment(): - return - - try_autostart_continuous_profiler() - - self._continuous_profile = try_profile_lifecycle_trace_start() - - def _set_segment_attributes(self) -> None: - if not self._is_segment(): - return - - client = sentry_sdk.get_client() - - self.set_attribute(SPANDATA.SENTRY_PLATFORM, "python") - self.set_attribute(SPANDATA.PROCESS_COMMAND_ARGS, sys.argv) - self.set_attribute( - SPANDATA.SENTRY_SDK_INTEGRATIONS, sorted(client.integrations.keys()) - ) - - if client.options.get("dist") and SPANDATA.SENTRY_DIST not in self._attributes: - self.set_attribute( - SPANDATA.SENTRY_DIST, str(client.options["dist"]).strip() - ) - - def _to_json(self) -> "SpanJSON": - res: "SpanJSON" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "name": self._name if self._name is not None else "", - "status": self._status, - "is_segment": self._is_segment(), - "start_timestamp": self._start_timestamp.timestamp(), - } - - if self._end_timestamp: - res["end_timestamp"] = self._end_timestamp.timestamp() - - if self._parent_span_id: - res["parent_span_id"] = self._parent_span_id - - res["attributes"] = {k: v for k, v in self._attributes.items()} - - return res - - -class NoOpStreamedSpan(StreamedSpan): - __slots__ = ( - "_finished", - "_unsampled_reason", - ) - - def __init__( - self, - unsampled_reason: "Optional[str]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - ) -> None: - self._scope = scope # type: ignore[assignment] - self._unsampled_reason = unsampled_reason - - self._finished = False - - self._start() - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}(sampled={self.sampled})>" - - def __enter__(self) -> "NoOpStreamedSpan": - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self._end() - - def _start(self) -> None: - if self._scope is None: - return - - old_span = self._scope.streamed_span - self._scope.streamed_span = self - self._previous_span_on_scope = old_span - - def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - if self._finished: - return - - if self._unsampled_reason is not None: - client = sentry_sdk.get_client() - if client.is_active() and client.transport: - logger.debug( - f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})" - ) - client.transport.record_lost_event( - reason=self._unsampled_reason, - data_category="span", - quantity=1, - ) - - if self._scope and hasattr(self, "_previous_span_on_scope"): - with capture_internal_exceptions(): - old_span = self._previous_span_on_scope - del self._previous_span_on_scope - self._scope.streamed_span = old_span - - self._finished = True - - def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - self._end() - - def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: - warnings.warn( - "span.finish() is deprecated. Use span.end() instead.", - stacklevel=2, - category=DeprecationWarning, - ) - - self._end() - - def get_attributes(self) -> "Attributes": - return {} - - def set_attribute(self, key: str, value: "AttributeValue") -> None: - pass - - def set_attributes(self, attributes: "Attributes") -> None: - pass - - def remove_attribute(self, key: str) -> None: - pass - - def _is_segment(self) -> bool: - return self._scope is not None - - @property - def status(self) -> "str": - return SpanStatus.OK.value - - @status.setter - def status(self, status: "Union[SpanStatus, str]") -> None: - pass - - @property - def name(self) -> str: - return "" - - @name.setter - def name(self, value: str) -> None: - pass - - @property - def active(self) -> bool: - return True - - @property - def span_id(self) -> str: - return "0000000000000000" - - @property - def trace_id(self) -> str: - return "00000000000000000000000000000000" - - @property - def sampled(self) -> "Optional[bool]": - return False - - @property - def start_timestamp(self) -> "Optional[datetime]": - return None - - @property - def end_timestamp(self) -> "Optional[datetime]": - return None - - -if TYPE_CHECKING: - - @overload - def trace( - func: "Callable[P, R]", - ) -> "Callable[P, R]": ... - - @overload - def trace( - func: None = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ... - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a span around a function call. - - EXPERIMENTAL. Use @sentry_sdk.trace instead. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param active: Controls whether spans started while this span is running - will automatically become its children. That's the default behavior. If - you want to create a span that shouldn't have any children (unless - provided explicitly via the `parent_span` argument), set this to False. - :type active: bool - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import ( - create_streaming_span_decorator, - ) - - decorator = create_streaming_span_decorator( - name=name, - attributes=attributes, - active=active, - ) - - if func: - return decorator(func) - else: - return decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[StreamedSpan]": - """ - Returns the currently active span on the scope if the span is a `StreamedSpan`, otherwise `None`. - - This function will only return a non-`None` value when the streaming trace lifecycle is enabled. - To enable the lifecycle, pass `_experiments={"trace_lifecycle": "stream"}` to `sentry.init()`. - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.streamed_span - return current_span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py deleted file mode 100644 index 1790e13fbf..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing.py +++ /dev/null @@ -1,1475 +0,0 @@ -import uuid -import warnings -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE -from sentry_sdk.profiler.continuous_profiler import get_profiler_id -from sentry_sdk.utils import ( - capture_internal_exceptions, - get_current_thread_meta, - is_valid_sample_rate, - logger, - nanosecond_time, - should_be_treated_as_error, -) - -if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping - from typing import ( - Any, - Dict, - Iterator, - List, - Optional, - ParamSpec, - Tuple, - TypeVar, - Union, - overload, - ) - - from typing_extensions import TypedDict, Unpack - - P = ParamSpec("P") - R = TypeVar("R") - - from sentry_sdk._types import ( - Event, - MeasurementUnit, - MeasurementValue, - SamplingContext, - ) - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - from sentry_sdk.profiler.transaction_profiler import Profile - - class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - - op: str - """ - The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - """ - - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - - hub: "Optional[sentry_sdk.Hub]" - """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" - - status: str - """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - - containing_transaction: "Optional[Transaction]" - """The transaction that this span belongs to.""" - - start_timestamp: "Optional[Union[datetime, float]]" - """ - The timestamp when the span started. If omitted, the current time - will be used. - """ - - scope: "sentry_sdk.Scope" - """The scope to use for this span. If not provided, we use the current scope.""" - - origin: str - """ - The origin of the span. - See https://develop.sentry.dev/sdk/performance/trace-origin/ - Default "manual". - """ - - name: str - """A string describing what operation is being performed within the span/transaction.""" - - class TransactionKwargs(SpanKwargs, total=False): - source: str - """ - A string describing the source of the transaction name. This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. - Default "custom". - """ - - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - - ProfileContext = TypedDict( - "ProfileContext", - { - "profiler_id": str, - }, - ) - -BAGGAGE_HEADER_NAME = "baggage" -SENTRY_TRACE_HEADER_NAME = "sentry-trace" - - -# Transaction source -# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations -class TransactionSource(str, Enum): - COMPONENT = "component" - CUSTOM = "custom" - ROUTE = "route" - TASK = "task" - URL = "url" - VIEW = "view" - - def __str__(self) -> str: - return self.value - - -# These are typically high cardinality and the server hates them -LOW_QUALITY_TRANSACTION_SOURCES = [ - TransactionSource.URL, -] - -SOURCE_FOR_STYLE = { - "endpoint": TransactionSource.COMPONENT, - "function_name": TransactionSource.COMPONENT, - "handler_name": TransactionSource.COMPONENT, - "method_and_path_pattern": TransactionSource.ROUTE, - "path": TransactionSource.URL, - "route_name": TransactionSource.COMPONENT, - "route_pattern": TransactionSource.ROUTE, - "uri_template": TransactionSource.ROUTE, - "url": TransactionSource.ROUTE, -} - - -def get_span_status_from_http_code(http_status_code: int) -> str: - """ - Returns the Sentry status corresponding to the given HTTP status code. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - if http_status_code < 400: - return SPANSTATUS.OK - - elif 400 <= http_status_code < 500: - if http_status_code == 403: - return SPANSTATUS.PERMISSION_DENIED - elif http_status_code == 404: - return SPANSTATUS.NOT_FOUND - elif http_status_code == 429: - return SPANSTATUS.RESOURCE_EXHAUSTED - elif http_status_code == 413: - return SPANSTATUS.FAILED_PRECONDITION - elif http_status_code == 401: - return SPANSTATUS.UNAUTHENTICATED - elif http_status_code == 409: - return SPANSTATUS.ALREADY_EXISTS - else: - return SPANSTATUS.INVALID_ARGUMENT - - elif 500 <= http_status_code < 600: - if http_status_code == 504: - return SPANSTATUS.DEADLINE_EXCEEDED - elif http_status_code == 501: - return SPANSTATUS.UNIMPLEMENTED - elif http_status_code == 503: - return SPANSTATUS.UNAVAILABLE - else: - return SPANSTATUS.INTERNAL_ERROR - - return SPANSTATUS.UNKNOWN_ERROR - - -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans", "dropped_spans") - - def __init__(self, maxlen: int) -> None: - # FIXME: this is `maxlen - 1` only to preserve historical behavior - # enforced by tests. - # Either this should be changed to `maxlen` or the JS SDK implementation - # should be changed to match a consistent interpretation of what maxlen - # limits: either transaction+spans or only child spans. - self.maxlen = maxlen - 1 - self.spans: "List[Span]" = [] - self.dropped_spans: int = 0 - - def add(self, span: "Span") -> None: - if len(self.spans) > self.maxlen: - span._span_recorder = None - self.dropped_spans += 1 - else: - self.spans.append(span) - - -class Span: - """A span holds timing information of a block of code. - Spans can have multiple child spans thus forming a span tree. - - :param trace_id: The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - :param span_id: The span ID of this span. If omitted, a new span ID will be generated. - :param parent_span_id: The span ID of the parent span, if applicable. - :param same_process_as_parent: Whether this span is in the same process as the parent span. - :param sampled: Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - :param op: The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - :param description: A description of what operation is being performed within the span. - - .. deprecated:: 2.15.0 - Please use the `name` parameter, instead. - :param name: A string describing what operation is being performed within the span. - :param hub: The hub to use for this span. - - .. deprecated:: 2.0.0 - Please use the `scope` parameter, instead. - :param status: The span's status. Possible values are listed at - https://develop.sentry.dev/sdk/event-payloads/span/ - :param containing_transaction: The transaction that this span belongs to. - :param start_timestamp: The timestamp when the span started. If omitted, the current time - will be used. - :param scope: The scope to use for this span. If not provided, we use the current scope. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "same_process_as_parent", - "sampled", - "op", - "description", - "_measurements", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "hub", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - "_flags", - "_flags_capacity", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - same_process_as_parent: bool = True, - sampled: "Optional[bool]" = None, - op: "Optional[str]" = None, - description: "Optional[str]" = None, - hub: "Optional[sentry_sdk.Hub]" = None, # deprecated - status: "Optional[str]" = None, - containing_transaction: "Optional[Transaction]" = None, - start_timestamp: "Optional[Union[datetime, float]]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - origin: str = "manual", - name: "Optional[str]" = None, - ) -> None: - self._trace_id = trace_id - self._span_id = span_id - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = name or description - self.status = status - self.hub = hub # backwards compatibility - self.scope = scope - self.origin = origin - self._measurements: "Dict[str, MeasurementValue]" = {} - self._tags: "MutableMapping[str, str]" = {} - self._data: "Dict[str, Any]" = {} - self._containing_transaction = containing_transaction - self._flags: "Dict[str, bool]" = {} - self._flags_capacity = 10 - - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - - if start_timestamp is None: - start_timestamp = datetime.now(timezone.utc) - elif isinstance(start_timestamp, float): - start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) - self.start_timestamp = start_timestamp - try: - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp: "Optional[datetime]" = None - - self._span_recorder: "Optional[_SpanRecorder]" = None - - self.update_active_thread() - self.set_profiler_id(get_profiler_id()) - - # TODO this should really live on the Transaction class rather than the Span - # class - def init_span_recorder(self, maxlen: int) -> None: - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - def __repr__(self) -> str: - return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.op, - self.description, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.origin, - ) - ) - - def __enter__(self) -> "Span": - scope = self.scope or sentry_sdk.get_current_scope() - old_span = scope.span - scope.span = self - self._context_manager_state = (scope, old_span) - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - with capture_internal_exceptions(): - scope, old_span = self._context_manager_state - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self) -> "Optional[Transaction]": - """The ``Transaction`` that this span belongs to. - The ``Transaction`` is the root of the span tree, - so one could also think of this ``Transaction`` as the "root span".""" - - # this is a getter rather than a regular attribute so that transactions - # can return `self` here instead (as a way to prevent them circularly - # referencing themselves) - return self._containing_transaction - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - Start a sub-span from the current span or transaction. - - Takes the same arguments as the initializer of :py:class:`Span`. The - trace id, sampling decision, transaction pointer, and span recorder are - inherited from the current span/transaction. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - - kwargs.setdefault("sampled", self.sampled) - - child = Span( - trace_id=self.trace_id, - parent_span_id=self.span_id, - containing_transaction=self.containing_transaction, - **kwargs, - ) - - span_recorder = ( - self.containing_transaction and self.containing_transaction._span_recorder - ) - if span_recorder: - span_recorder.add(child) - - return child - - @classmethod - def continue_from_environ( - cls, - environ: "Mapping[str, str]", - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a Transaction with the given params, then add in data pulled from - the ``sentry-trace`` and ``baggage`` headers from the environ (if any) - before returning the Transaction. - - This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` - in that it assumes header names in the form ``HTTP_HEADER_NAME`` - - such as you would get from a WSGI/ASGI environ - - rather than the form ``header-name``. - - :param environ: The ASGI/WSGI environ to pull information from. - """ - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers: "Mapping[str, str]", - *, - _sample_rand: "Optional[str]" = None, - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a transaction with the given params (including any data pulled from - the ``sentry-trace`` and ``baggage`` headers). - - :param headers: The dictionary with the HTTP headers to pull information from. - :param _sample_rand: If provided, we override the sample_rand value from the - incoming headers with this value. (internal use only) - """ - logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header( - headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand - ) - kwargs.update({BAGGAGE_HEADER_NAME: baggage}) - - sentrytrace_kwargs = extract_sentrytrace_data( - headers.get(SENTRY_TRACE_HEADER_NAME) - ) - - if sentrytrace_kwargs is not None: - kwargs.update(sentrytrace_kwargs) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. - If the span's containing transaction doesn't yet have a ``baggage`` value, - this will cause one to be generated and stored. - """ - if not self.containing_transaction: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - return - - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.containing_transaction.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - @classmethod - def from_traceparent( - cls, - traceparent: "Optional[str]", - **kwargs: "Any", - ) -> "Optional[Transaction]": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def to_baggage(self) -> "Optional[Baggage]": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with this ``Span``, if any. (Taken from the root of the span tree.) - """ - if self.containing_transaction: - return self.containing_transaction.get_baggage() - return None - - def set_tag(self, key: str, value: "Any") -> None: - self._tags[key] = value - - def set_data(self, key: str, value: "Any") -> None: - self._data[key] = value - - def update_data(self, data: "Dict[str, Any]") -> None: - self._data.update(data) - - def set_flag(self, flag: str, result: bool) -> None: - if len(self._flags) < self._flags_capacity: - self._flags[flag] = result - - def set_status(self, value: str) -> None: - self.status = value - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_thread( - self, thread_id: "Optional[int]", thread_name: "Optional[str]" - ) -> None: - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) - - def set_profiler_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status: int) -> None: - self.set_tag( - "http.status_code", str(http_status) - ) # TODO-neel remove in major, we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self) -> bool: - return self.status == "ok" - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ - if self.timestamp is not None: - # This span is already finished, ignore. - return None - - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp - else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - except AttributeError: - self.timestamp = datetime.now(timezone.utc) - - scope = scope or sentry_sdk.get_current_scope() - - # Copy conversation_id from scope to span data if this is an AI span - conversation_id = scope.get_conversation_id() - if conversation_id: - has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data - is_ai_span_op = self.op is not None and ( - self.op.startswith("ai.") or self.op.startswith("gen_ai.") - ) - if has_ai_op or is_ai_span_op: - self.set_data("gen_ai.conversation.id", conversation_id) - - maybe_create_breadcrumbs_from_span(scope, self) - - return None - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the span.""" - - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "same_process_as_parent": self.same_process_as_parent, - "op": self.op, - "description": self.description, - "start_timestamp": self.start_timestamp, - "timestamp": self.timestamp, - "origin": self.origin, - } - - if self.status: - rv["status"] = self.status - # TODO-neel remove redundant tag in major - self._tags["status"] = self.status - - if len(self._measurements) > 0: - rv["measurements"] = self._measurements - - tags = self._tags - if tags: - rv["tags"] = tags - - data = {} - data.update(self._flags) - data.update(self._data) - if data: - rv["data"] = data - - return rv - - def get_trace_context(self) -> "Any": - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "op": self.op, - "description": self.description, - "origin": self.origin, - } - if self.status: - rv["status"] = self.status - - if self.containing_transaction: - rv["dynamic_sampling_context"] = ( - self.containing_transaction.get_baggage().dynamic_sampling_context() - ) - - data = {} - - thread_id = self._data.get(SPANDATA.THREAD_ID) - if thread_id is not None: - data["thread.id"] = thread_id - - thread_name = self._data.get(SPANDATA.THREAD_NAME) - if thread_name is not None: - data["thread.name"] = thread_name - - if data: - rv["data"] = data - - return rv - - def get_profile_context(self) -> "Optional[ProfileContext]": - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - def update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -class Transaction(Span): - """The Transaction is the root element that holds all the spans - for Sentry performance instrumentation. - - :param name: Identifier of the transaction. - Will show up in the Sentry UI. - :param parent_sampled: Whether the parent transaction was sampled. - If True this transaction will be kept, if False it will be discarded. - :param baggage: The W3C baggage header value. - (see https://www.w3.org/TR/baggage/) - :param source: A string describing the source of the transaction name. - This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations - for more information. Default "custom". - :param kwargs: Additional arguments to be passed to the Span constructor. - See :py:class:`sentry_sdk.tracing.Span` for available arguments. - """ - - __slots__ = ( - "name", - "source", - "parent_sampled", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_measurements", - "_contexts", - "_profile", - "_continuous_profile", - "_baggage", - "_sample_rand", - ) - - def __init__( # type: ignore[misc] - self, - name: str = "", - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - source: str = TransactionSource.CUSTOM, - **kwargs: "Unpack[SpanKwargs]", - ) -> None: - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate: "Optional[float]" = None - self.parent_sampled = parent_sampled - self._measurements: "Dict[str, MeasurementValue]" = {} - self._contexts: "Dict[str, Any]" = {} - self._profile: "Optional[Profile]" = None - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._baggage = baggage - - baggage_sample_rand = ( - None if self._baggage is None else self._baggage._sample_rand() - ) - if baggage_sample_rand is not None: - self._sample_rand = baggage_sample_rand - else: - self._sample_rand = _generate_sample_rand(self.trace_id) - - def __repr__(self) -> str: - return ( - "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.name, - self.op, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.source, - self.origin, - ) - ) - - def _possibly_started(self) -> bool: - """Returns whether the transaction might have been started. - - If this returns False, we know that the transaction was not started - with sentry_sdk.start_transaction, and therefore the transaction will - be discarded. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self) -> "Transaction": - if not self._possibly_started(): - logger.debug( - "Transaction was entered without being started with sentry_sdk.start_transaction." - "The transaction will not be sent to Sentry. To fix, start the transaction by" - "passing it to sentry_sdk.start_transaction." - ) - - super().__enter__() - - if self._profile is not None: - self._profile.__enter__() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - - if self._continuous_profile is not None: - self._continuous_profile.stop() - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self) -> "Transaction": - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def _get_scope_from_finish_args( - self, - scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - ) -> "Optional[sentry_sdk.Scope]": - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - - def _get_log_representation(self) -> str: - return "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. - - :param scope: The Scope to use for this transaction. - If not provided, the current Scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args(scope, hub) - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be None - if self.sampled is False: - logger.debug("Discarding transaction because sampled = False") - else: - logger.debug( - "Discarding transaction because it was not started with sentry_sdk.start_transaction" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - if client.transport and has_tracing_enabled(client.options): - if client.monitor and client.monitor.downsample_factor > 0: - reason = "backpressure" - else: - reason = "sample_rate" - - client.transport.record_lost_event(reason, data_category="transaction") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) - if ( - status_code is not None - and status_code in client.options["trace_ignore_status_codes"] - ): - logger.debug( - "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( - transaction_description=self._get_log_representation(), - status_code=self._data[SPANDATA.HTTP_STATUS_CODE], - trace_ignore_status_codes=client.options[ - "trace_ignore_status_codes" - ], - ) - ) - if client.transport: - client.transport.record_lost_event( - "event_processor", data_category="transaction" - ) - - num_spans = len(self._span_recorder.spans) + 1 - client.transport.record_lost_event( - "event_processor", data_category="span", quantity=num_spans - ) - - self.sampled = False - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - if self.sampled is None: - logger.warning("Discarding transaction without sampling decision.") - - return None - - finished_spans = [] - has_gen_ai_span = False - if client.options.get("stream_gen_ai_spans", True): - for span in self._span_recorder.spans: - if span.timestamp is None: - continue - - if isinstance(span.op, str) and span.op.startswith("gen_ai."): - has_gen_ai_span = True - - finished_spans.append(span.to_json()) - else: - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - len_diff = len(self._span_recorder.spans) - len(finished_spans) - dropped_spans = len_diff + self._span_recorder.dropped_spans - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - self._span_recorder = None - - contexts = {} - contexts.update(self._contexts) - contexts.update({"trace": self.get_trace_context()}) - profile_context = self.get_profile_context() - if profile_context is not None: - contexts.update({"profile": profile_context}) - - event: "Event" = { - "type": "transaction", - "transaction": self.name, - "transaction_info": {"source": self.source}, - "contexts": contexts, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": finished_spans, - } - - if dropped_spans > 0: - event["_dropped_spans"] = dropped_spans - - if has_gen_ai_span: - event["_has_gen_ai_span"] = True - - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - - event["measurements"] = self._measurements - - return scope.capture_event(event) - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - """ - .. deprecated:: 2.28.0 - This function is deprecated and will be removed in the next major release. - """ - - warnings.warn( - "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.", - DeprecationWarning, - stacklevel=2, - ) - self._measurements[name] = {"value": value, "unit": unit} - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - """Sets a context. Transactions can have multiple contexts - and they should follow the format described in the "Contexts Interface" - documentation. - - :param key: The name of the context. - :param value: The information about the context. - """ - self._contexts[key] = value - - def set_http_status(self, http_status: int) -> None: - """Sets the status of the Transaction according to the given HTTP status. - - :param http_status: The HTTP status code.""" - super().set_http_status(http_status) - self.set_context("response", {"status_code": http_status}) - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the transaction.""" - rv = super().to_json() - - rv["name"] = self.name - rv["source"] = self.source - rv["sampled"] = self.sampled - - return rv - - def get_trace_context(self) -> "Any": - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self) -> "Baggage": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with the Transaction. - - The first time a new baggage with Sentry items is made, - it will be frozen.""" - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_transaction(self) - - return self._baggage - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the transaction's sampling decision, according to the following - precedence rules: - - 1. If a sampling decision is passed to `start_transaction` - (`start_transaction(name: "my transaction", sampled: True)`), that - decision will be used, regardless of anything else - - 2. If `traces_sampler` is defined, its decision will be used. It can - choose to keep or ignore any parent sampling decision, or use the - sampling context data to make its own decision or to choose a sample - rate for the transaction. - - 3. If `traces_sampler` is not defined, but there's a parent sampling - decision, the parent sampling decision will be used. - - 4. If `traces_sampler` is not defined and there's no parent sampling - decision, `traces_sample_rate` will be used. - """ - client = sentry_sdk.get_client() - - transaction_description = self._get_log_representation() - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) - # default inheritance behavior - else ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning( - "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( - transaction_description=transaction_description, - ) - ) - self.sampled = False - return - - self.sample_rate = float(sample_rate) - - if client.monitor: - self.sample_rate /= 2**client.monitor.downsample_factor - - # if the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - if not self.sample_rate: - logger.debug( - "[Tracing] Discarding {transaction_description} because {reason}".format( - transaction_description=transaction_description, - reason=( - "traces_sampler returned 0 or False" - if callable(client.options.get("traces_sampler")) - else "traces_sample_rate is set to 0" - ), - ) - ) - self.sampled = False - return - - # Now we roll the dice. - self.sampled = self._sample_rand < self.sample_rate - - if self.sampled: - logger.debug( - "[Tracing] Starting {transaction_description}".format( - transaction_description=transaction_description, - ) - ) - else: - logger.debug( - "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( - transaction_description=transaction_description, - sample_rate=self.sample_rate, - ) - ) - - # Private aliases matching StreamedSpan's private API - _get_baggage = get_baggage - _get_trace_context = get_trace_context - - -class NoOpSpan(Span): - def __repr__(self) -> str: - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self) -> "Optional[Transaction]": - return None - - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "NoOpSpan": - return NoOpSpan() - - def to_traceparent(self) -> str: - return "" - - def to_baggage(self) -> "Optional[Baggage]": - return None - - def get_baggage(self) -> "Optional[Baggage]": - return None - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - return iter(()) - - def set_tag(self, key: str, value: "Any") -> None: - pass - - def set_data(self, key: str, value: "Any") -> None: - pass - - def update_data(self, data: "Dict[str, Any]") -> None: - pass - - def set_status(self, value: str) -> None: - pass - - def set_http_status(self, http_status: int) -> None: - pass - - def is_success(self) -> bool: - return True - - def to_json(self) -> "Dict[str, Any]": - return {} - - def get_trace_context(self) -> "Any": - return {} - - def get_profile_context(self) -> "Any": - return {} - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, - ) -> "Optional[str]": - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ - pass - - def set_measurement( - self, name: str, value: float, unit: "MeasurementUnit" = "" - ) -> None: - pass - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - pass - - def init_span_recorder(self, maxlen: int) -> None: - pass - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - pass - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _get_baggage = get_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -if TYPE_CHECKING: - - @overload - def trace( - func: None = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": - # Handles: @trace() and @trace(op="custom") - pass - - @overload - def trace(func: "Callable[P, R]") -> "Callable[P, R]": - # Handles: @trace - pass - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a child span around a function call. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, a default operation will - be assigned based on the template. - :type op: str or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP, SPANTEMPLATE - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - op=OP.DB_QUERY, - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - - # With a custom template - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def calculate_interest_rate(amount, rate, years): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import create_span_decorator - - decorator = create_span_decorator( - op=op, - name=name, - attributes=attributes, - template=template, - ) - - if func: - return decorator(func) - else: - return decorator - - -# Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - _generate_sample_rand, - extract_sentrytrace_data, - has_tracing_enabled, - maybe_create_breadcrumbs_from_span, -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py deleted file mode 100644 index c2e34a795b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/tracing_utils.py +++ /dev/null @@ -1,1694 +0,0 @@ -import contextlib -import functools -import inspect -import os -import re -import sys -import uuid -import warnings -from collections.abc import Mapping, MutableMapping -from datetime import datetime, timedelta, timezone -from random import Random -from urllib.parse import quote, unquote - -try: - from re import Pattern -except ImportError: - # 3.6 - from typing import Pattern - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE -from sentry_sdk.utils import ( - _is_external_source, - _is_in_project_root, - _module_in_list, - capture_internal_exceptions, - filename_for_module, - is_sentry_url, - is_valid_sample_rate, - logger, - match_regex_list, - qualname_from_function, - safe_repr, - to_string, - try_convert, -) - -if TYPE_CHECKING: - from types import FrameType - from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union - - from sentry_sdk._types import Attributes - - -SENTRY_TRACE_REGEX = re.compile( - "^[ \t]*" # whitespace - "([0-9a-f]{32})?" # trace_id - "-?([0-9a-f]{16})?" # span_id - "-?([01])?" # sampled - "[ \t]*$" # whitespace -) - - -# This is a normal base64 regex, modified to reflect that fact that we strip the -# trailing = or == off -base64_stripped = ( - # any of the characters in the base64 "alphabet", in multiples of 4 - "([a-zA-Z0-9+/]{4})*" - # either nothing or 2 or 3 base64-alphabet characters (see - # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for - # why there's never only 1 extra character) - "([a-zA-Z0-9+/]{2,3})?" -) - - -class EnvironHeaders(Mapping): # type: ignore - def __init__( - self, - environ: "Mapping[str, str]", - prefix: str = "HTTP_", - ) -> None: - self.environ = environ - self.prefix = prefix - - def __getitem__(self, key: str) -> "Optional[Any]": - return self.environ[self.prefix + key.replace("-", "_").upper()] - - def __len__(self) -> int: - return sum(1 for _ in iter(self)) - - def __iter__(self) -> "Generator[str, None, None]": - for k in self.environ: - if not isinstance(k, str): - continue - - k = k.replace("-", "_").upper() - if not k.startswith(self.prefix): - continue - - yield k[len(self.prefix) :] - - -def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: - """ - Returns True if either traces_sample_rate or traces_sampler is - defined and enable_tracing is set and not false. - """ - if options is None: - return False - - return bool( - options.get("enable_tracing") is not False - and ( - options.get("traces_sample_rate") is not None - or options.get("traces_sampler") is not None - ) - ) - - -def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return False - - return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" - - -def should_truncate_gen_ai_input(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return True - - return not options.get( - "stream_gen_ai_spans", True - ) and not has_span_streaming_enabled(options) - - -@contextlib.contextmanager -def record_sql_queries( - cursor: "Any", - query: "Any", - params_list: "Any", - paramstyle: "Optional[str]", - executemany: bool, - record_cursor_repr: bool = False, - span_origin: str = "manual", - span_op_override_value: "Optional[str]" = None, -) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": - # TODO: Bring back capturing of params by default - client = sentry_sdk.get_client() - if client.options["_experiments"].get("record_sql_params", False): - if not params_list or params_list == [None]: - params_list = None - - if paramstyle == "pyformat": - paramstyle = "format" - else: - params_list = None - paramstyle = None - - query = _format_sql(cursor, query) - - data = {} - if params_list is not None: - data["db.params"] = params_list - if paramstyle is not None: - data["db.paramstyle"] = paramstyle - if executemany: - data["db.executemany"] = True - if record_cursor_repr and cursor is not None: - data["db.cursor"] = cursor - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message=query, category="query", data=data) - - if has_span_streaming_enabled(client.options): - additional_attributes = {} - if query is not None: - additional_attributes["db.query.text"] = query - - with sentry_sdk.traces.start_span( - name="" if query is None else query, - attributes={ - "sentry.origin": span_origin, - "sentry.op": span_op_override_value - if span_op_override_value - else OP.DB, - **additional_attributes, - }, - ) as span: - yield span - else: - with sentry_sdk.start_span( - op=span_op_override_value if span_op_override_value is not None else OP.DB, - name=query, - origin=span_origin, - ) as span: - for k, v in data.items(): - span.set_data(k, v) - yield span - - -def maybe_create_breadcrumbs_from_span( - scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" -) -> None: - if span.op == OP.DB_REDIS: - scope.add_breadcrumb( - message=span.description, type="redis", category="redis", data=span._tags - ) - - elif span.op == OP.HTTP_CLIENT: - level = None - status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) - if status_code: - if 500 <= status_code <= 599: - level = "error" - elif 400 <= status_code <= 499: - level = "warning" - - if level: - scope.add_breadcrumb( - type="http", category="httplib", data=span._data, level=level - ) - else: - scope.add_breadcrumb(type="http", category="httplib", data=span._data) - - elif span.op == "subprocess": - scope.add_breadcrumb( - type="subprocess", - category="subprocess", - message=span.description, - data=span._data, - ) - - -def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]": - try: - return frame.f_code.co_filename - except Exception: - return None - - -def _should_be_included( - is_sentry_sdk_frame: bool, - namespace: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", - abs_path: "Optional[str]", - project_root: "Optional[str]", -) -> bool: - # in_app_include takes precedence over in_app_exclude - should_be_included = _module_in_list(namespace, in_app_include) - should_be_excluded = _is_external_source(abs_path) or _module_in_list( - namespace, in_app_exclude - ) - return not is_sentry_sdk_frame and ( - should_be_included - or (_is_in_project_root(abs_path, project_root) and not should_be_excluded) - ) - - -def add_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", - project_root: "Optional[str]", - in_app_include: "Optional[list[str]]", - in_app_exclude: "Optional[list[str]]", -) -> None: - """ - Adds OTel compatible source code information to the span - """ - # Find the correct frame - frame: "Union[FrameType, None]" = sys._getframe() - while frame is not None: - abs_path = _get_frame_module_abs_path(frame) - - try: - namespace: "Optional[str]" = frame.f_globals.get("__name__") - except Exception: - namespace = None - - is_sentry_sdk_frame = namespace is not None and namespace.startswith( - "sentry_sdk." - ) - - should_be_included = _should_be_included( - is_sentry_sdk_frame=is_sentry_sdk_frame, - namespace=namespace, - in_app_include=in_app_include, - in_app_exclude=in_app_exclude, - abs_path=abs_path, - project_root=project_root, - ) - if should_be_included: - break - - frame = frame.f_back - else: - frame = None - - # Set the data - if frame is not None: - try: - lineno = frame.f_lineno - except Exception: - lineno = None - if lineno is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_LINENO, lineno) - else: - span.set_attribute("code.line.number", lineno) - - try: - namespace = frame.f_globals.get("__name__") - except Exception: - namespace = None - if namespace is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_NAMESPACE, namespace) - else: - span.set_attribute(SPANDATA.CODE_NAMESPACE, namespace) - - filepath = _get_frame_module_abs_path(frame) - if filepath is not None: - if namespace is not None: - in_app_path = filename_for_module(namespace, filepath) - elif project_root is not None and filepath.startswith(project_root): - in_app_path = filepath.replace(project_root, "").lstrip(os.sep) - else: - in_app_path = filepath - - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FILEPATH, in_app_path) - else: - if in_app_path is not None: - span.set_attribute("code.file.path", in_app_path) - - try: - code_function = frame.f_code.co_name - except Exception: - code_function = None - - if code_function is not None: - if isinstance(span, Span): - span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - else: - span.set_attribute(SPANDATA.CODE_FUNCTION, frame.f_code.co_name) - - -def add_query_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a database query span - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_query_source = client.options.get("enable_db_query_source", True) - if not should_add_query_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("db_query_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def add_http_request_source( - span: "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]", -) -> None: - """ - Adds OTel compatible source code information to a span for an outgoing HTTP request - """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if isinstance(span, Span): - # In the StreamedSpan case, we need to add the extra span information before - # the span finishes, so it's expected that this will be None. In the Span case, - # it should already be finished. - if span.timestamp is None: - return - - if span.start_timestamp is None: - return - - should_add_request_source = client.options.get("enable_http_request_source", True) - if not should_add_request_source: - return - - if isinstance(span, StreamedSpan): - end_timestamp = span.end_timestamp - else: - end_timestamp = span.timestamp - - end_timestamp = end_timestamp or datetime.now(timezone.utc) - - duration = end_timestamp - span.start_timestamp - threshold = client.options.get("http_request_source_threshold_ms", 0) - slow_query = duration / timedelta(milliseconds=1) > threshold - - if not slow_query: - return - - add_source( - span=span, - project_root=client.options["project_root"], - in_app_include=client.options.get("in_app_include"), - in_app_exclude=client.options.get("in_app_exclude"), - ) - - -def extract_sentrytrace_data( - header: "Optional[str]", -) -> "Optional[Dict[str, Union[str, bool, None]]]": - """ - Given a `sentry-trace` header string, return a dictionary of data. - """ - if not header: - return None - - if "," in header: - # Multiple headers may have been combined into one comma-separated value (RFC 7230 3.2.2); use the first non-empty one. - parts = [part.strip() for part in header.split(",")] - header = next((part for part in parts if part), "") - - if not header: - return None - - if header.startswith("00-") and header.endswith("-00"): - header = header[3:-3] - - match = SENTRY_TRACE_REGEX.match(header) - if not match: - return None - - trace_id, parent_span_id, sampled_str = match.groups() - parent_sampled = None - - if trace_id: - trace_id = "{:032x}".format(int(trace_id, 16)) - if parent_span_id: - parent_span_id = "{:016x}".format(int(parent_span_id, 16)) - if sampled_str: - parent_sampled = sampled_str != "0" - - return { - "trace_id": trace_id, - "parent_span_id": parent_span_id, - "parent_sampled": parent_sampled, - } - - -def _format_sql(cursor: "Any", sql: str) -> "Optional[str]": - real_sql = None - - # If we're using psycopg2, it could be that we're - # looking at a query that uses Composed objects. Use psycopg2's mogrify - # function to format the query. We lose per-parameter trimming but gain - # accuracy in formatting. - try: - if hasattr(cursor, "mogrify"): - real_sql = cursor.mogrify(sql) - if isinstance(real_sql, bytes): - real_sql = real_sql.decode(cursor.connection.encoding) - except Exception: - real_sql = None - - return real_sql or to_string(sql) - - -class PropagationContext: - """ - The PropagationContext represents the data of a trace in Sentry. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "parent_sampled", - "baggage", - "custom_sampling_context", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - parent_sampled: "Optional[bool]" = None, - dynamic_sampling_context: "Optional[Dict[str, str]]" = None, - baggage: "Optional[Baggage]" = None, - ) -> None: - self._trace_id = trace_id - """The trace id of the Sentry trace.""" - - self._span_id = span_id - """The span id of the currently executing span.""" - - self.parent_span_id = parent_span_id - """The id of the parent span that started this span. - The parent span could also be a span in an upstream service.""" - - self.parent_sampled = parent_sampled - """Boolean indicator if the parent span was sampled. - Important when the parent span originated in an upstream service, - because we want to sample the whole trace, or nothing from the trace.""" - - self.baggage = baggage - """Parsed baggage header that is used for dynamic sampling decisions.""" - - """DEPRECATED this only exists for backwards compat of constructor.""" - if baggage is None and dynamic_sampling_context is not None: - self.baggage = Baggage(dynamic_sampling_context) - - self.custom_sampling_context: "Optional[dict[str, Any]]" = None - - @classmethod - def from_incoming_data( - cls, incoming_data: "Dict[str, Any]" - ) -> "PropagationContext": - propagation_context = PropagationContext() - normalized_data = normalize_incoming_data(incoming_data) - - sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME) - sentrytrace_data = extract_sentrytrace_data(sentry_trace_header) - - # nothing to propagate if no sentry-trace - if sentrytrace_data is None: - return propagation_context - - baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME) - baggage = ( - Baggage.from_incoming_header(baggage_header) if baggage_header else None - ) - - if not _should_continue_trace(baggage): - return propagation_context - - propagation_context.update(sentrytrace_data) - if baggage: - propagation_context.baggage = baggage - - propagation_context._fill_sample_rand() - - return propagation_context - - @property - def trace_id(self) -> str: - """The trace id of the Sentry trace.""" - if not self._trace_id: - # New trace, don't fill in sample_rand - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - """The span id of the currently executed span.""" - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - @property - def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]": - return self.get_baggage().dynamic_sampling_context() - - def to_traceparent(self) -> str: - return f"{self.trace_id}-{self.span_id}" - - def get_baggage(self) -> "Baggage": - if self.baggage is None: - self.baggage = Baggage.populate_from_propagation_context(self) - return self.baggage - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers. - """ - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - def update(self, other_dict: "Dict[str, Any]") -> None: - """ - Updates the PropagationContext with data from the given dictionary. - """ - for key, value in other_dict.items(): - try: - setattr(self, key, value) - except AttributeError: - pass - - def _set_custom_sampling_context( - self, custom_sampling_context: "dict[str, Any]" - ) -> None: - self.custom_sampling_context = custom_sampling_context - - def __repr__(self) -> str: - return "".format( - self._trace_id, - self._span_id, - self.parent_span_id, - self.parent_sampled, - self.baggage, - ) - - def _fill_sample_rand(self) -> None: - """ - Ensure that there is a valid sample_rand value in the baggage. - - If there is a valid sample_rand value in the baggage, we keep it. - Otherwise, we generate a sample_rand value according to the following: - - - If we have a parent_sampled value and a sample_rate in the DSC, we compute - a sample_rand value randomly in the range: - - [0, sample_rate) if parent_sampled is True, - - or, in the range [sample_rate, 1) if parent_sampled is False. - - - If either parent_sampled or sample_rate is missing, we generate a random - value in the range [0, 1). - - The sample_rand is deterministically generated from the trace_id, if present. - - This function does nothing if there is no baggage. - """ - if self.baggage is None: - return - - sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand")) - if sample_rand is not None and 0 <= sample_rand < 1: - # sample_rand is present and valid, so don't overwrite it - return - - # Get the sample rate and compute the transformation that will map the random value - # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). - sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate")) - lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) - - try: - sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) - except ValueError: - # ValueError is raised if the interval is invalid, i.e. lower >= upper. - # lower >= upper might happen if the incoming trace's sampled flag - # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. - # We cannot generate a sensible sample_rand value in this case. - logger.debug( - f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " - f"and sample_rate={sample_rate}." - ) - return - - self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231 - - def _sample_rand(self) -> "Optional[str]": - """Convenience method to get the sample_rand value from the baggage.""" - if self.baggage is None: - return None - - return self.baggage.sentry_items.get("sample_rand") - - -class Baggage: - """ - The W3C Baggage header information (see https://www.w3.org/TR/baggage/). - - Before mutating a `Baggage` object, calling code must check that `mutable` is `True`. - Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but - it is the caller's responsibility to enforce this restriction. - """ - - __slots__ = ("sentry_items", "third_party_items", "mutable") - - SENTRY_PREFIX = "sentry-" - SENTRY_PREFIX_REGEX = re.compile("^sentry-") - - def __init__( - self, - sentry_items: "Dict[str, str]", - third_party_items: str = "", - mutable: bool = True, - ): - self.sentry_items = sentry_items - self.third_party_items = third_party_items - self.mutable = mutable - - @classmethod - def from_incoming_header( - cls, - header: "Optional[str]", - *, - _sample_rand: "Optional[str]" = None, - ) -> "Baggage": - """ - freeze if incoming header already has sentry baggage - """ - sentry_items = {} - third_party_items = "" - mutable = True - - if header: - for item in header.split(","): - if "=" not in item: - continue - - with capture_internal_exceptions(): - item = item.strip() - key, val = item.split("=", 1) - if Baggage.SENTRY_PREFIX_REGEX.match(key): - baggage_key = unquote(key.split("-")[1]) - sentry_items[baggage_key] = unquote(val) - mutable = False - else: - third_party_items += ("," if third_party_items else "") + item - - if _sample_rand is not None: - sentry_items["sample_rand"] = str(_sample_rand) - mutable = False - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]": - """ - Deprecated: use populate_from_propagation_context - """ - if scope._propagation_context is None: - return Baggage({}) - - return Baggage.populate_from_propagation_context(scope._propagation_context) - - @classmethod - def populate_from_propagation_context( - cls, propagation_context: "PropagationContext" - ) -> "Baggage": - sentry_items: "Dict[str, str]" = {} - third_party_items = "" - mutable = False - - client = sentry_sdk.get_client() - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options - - sentry_items["trace_id"] = propagation_context.trace_id - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if options.get("traces_sample_rate"): - sentry_items["sample_rate"] = str(options["traces_sample_rate"]) - - return Baggage(sentry_items, third_party_items, mutable) - - @classmethod - def populate_from_transaction( - cls, transaction: "sentry_sdk.tracing.Transaction" - ) -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - transaction.name - and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES - ): - sentry_items["transaction"] = transaction.name - - if transaction.sample_rate is not None: - sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction.sampled is not None: - sentry_items["sampled"] = "true" if transaction.sampled else "false" - - # there's an existing baggage but it was mutable, - # which is why we are creating this new baggage. - # However, if by chance the user put some sentry items in there, give them precedence. - if transaction._baggage and transaction._baggage.sentry_items: - sentry_items.update(transaction._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - @classmethod - def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = segment.trace_id - sentry_items["sample_rand"] = f"{segment._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - segment.get_attributes().get("sentry.span.source") - not in LOW_QUALITY_SEGMENT_SOURCES - ) and segment._name: - sentry_items["transaction"] = segment._name - - if segment._sample_rate is not None: - sentry_items["sample_rate"] = str(segment._sample_rate) - - if segment.sampled is not None: - sentry_items["sampled"] = "true" if segment.sampled else "false" - - # There's an existing baggage but it was mutable, which is why we are - # creating this new baggage. - # However, if by chance the user put some sentry items in there, give - # them precedence. - if segment._baggage and segment._baggage.sentry_items: - sentry_items.update(segment._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - - def freeze(self) -> None: - self.mutable = False - - def dynamic_sampling_context(self) -> "Dict[str, str]": - header = {} - - for key, item in self.sentry_items.items(): - header[key] = item - - return header - - def serialize(self, include_third_party: bool = False) -> str: - items = [] - - for key, val in self.sentry_items.items(): - with capture_internal_exceptions(): - item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val)) - items.append(item) - - if include_third_party: - items.append(self.third_party_items) - - return ",".join(items) - - @staticmethod - def strip_sentry_baggage(header: str) -> str: - """Remove Sentry baggage from the given header. - - Given a Baggage header, return a new Baggage header with all Sentry baggage items removed. - """ - return ",".join( - ( - item - for item in header.split(",") - if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip()) - ) - ) - - def _sample_rand(self) -> "Optional[float]": - """Convenience method to get the sample_rand value from the sentry_items. - - We validate the value and parse it as a float before returning it. The value is considered - valid if it is a float in the range [0, 1). - """ - sample_rand = try_convert(float, self.sentry_items.get("sample_rand")) - - if sample_rand is not None and 0.0 <= sample_rand < 1.0: - return sample_rand - - return None - - def __repr__(self) -> str: - return f'' - - -def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool: - """ - Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False. - """ - trace_propagation_targets = client.options["trace_propagation_targets"] - - if is_sentry_url(client, url): - return False - - return match_regex_list(url, trace_propagation_targets, substring_matching=True) - - -def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]": - """ - Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes. - """ - data = {} - for key, value in incoming_data.items(): - if key.startswith("HTTP_"): - key = key[5:] - - key = key.replace("_", "-").lower() - data[key] = value - - return data - - -def create_span_decorator( - op: "Optional[Union[str, OP]]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, -) -> "Any": - """ - Create a span decorator that can wrap both sync and async functions. - - :param op: The operation type for the span. - :type op: str or :py:class:`sentry_sdk.consts.OP` or None - :param name: The name of the span. - :type name: str or None - :param attributes: Additional attributes to set on the span. - :type attributes: dict or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - """ - from sentry_sdk.scope import should_send_default_pii - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return await f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return await f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = await f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - warnings.warn( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - DeprecationWarning, - stacklevel=2, - ) - return f(*args, **kwargs) - - span_op = op or _get_span_op(template) - function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - send_pii = should_send_default_pii() - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - - result = f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - - return result - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def create_streaming_span_decorator( - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - active: bool = True, -) -> "Any": - """ - Create a span creating decorator that can wrap both sync and async functions. - """ - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - result = await f(*args, **kwargs) - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - warnings.warn( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - stacklevel=2, - ) - - span_name = name or qualname_from_function(f) or "" - - with start_streaming_span( - name=span_name, attributes=attributes, active=active - ): - return f(*args, **kwargs) - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - -def _generate_sample_rand( - trace_id: "Optional[str]", - *, - interval: "tuple[float, float]" = (0.0, 1.0), -) -> float: - """Generate a sample_rand value from a trace ID. - - The generated value will be pseudorandomly chosen from the provided - interval. Specifically, given (lower, upper) = interval, the generated - value will be in the range [lower, upper). The value has 6-digit precision, - so when printing with .6f, the value will never be rounded up. - - The pseudorandom number generator is seeded with the trace ID. - """ - lower, upper = interval - if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly - raise ValueError("Invalid interval: lower must be less than upper") - - rng = Random(trace_id) - lower_scaled = int(lower * 1_000_000) - upper_scaled = int(upper * 1_000_000) - try: - sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled) - except ValueError: - # In some corner cases it might happen that the range is too small - # In that case, just take the lower bound - sample_rand_scaled = lower_scaled - - return sample_rand_scaled / 1_000_000 - - -def _sample_rand_range( - parent_sampled: "Optional[bool]", sample_rate: "Optional[float]" -) -> "tuple[float, float]": - """ - Compute the lower (inclusive) and upper (exclusive) bounds of the range of values - that a generated sample_rand value must fall into, given the parent_sampled and - sample_rate values. - """ - if parent_sampled is None or sample_rate is None: - return 0.0, 1.0 - elif parent_sampled is True: - return 0.0, sample_rate - else: # parent_sampled is False - return sample_rate, 1.0 - - -def _get_value(source: "Any", key: str) -> "Optional[Any]": - """ - Gets a value from a source object. The source can be a dict or an object. - It is checked for dictionary keys and object attributes. - """ - value = None - if isinstance(source, dict): - value = source.get(key) - else: - if hasattr(source, key): - try: - value = getattr(source, key) - except Exception: - value = None - return value - - -def _get_span_name( - template: "Union[str, SPANTEMPLATE]", - name: str, - kwargs: "Optional[dict[str, Any]]" = None, -) -> str: - """ - Get the name of the span based on the template and the name. - """ - span_name = name - - if template == SPANTEMPLATE.AI_CHAT: - model = None - if kwargs: - for key in ("model", "model_name"): - if kwargs.get(key) and isinstance(kwargs[key], str): - model = kwargs[key] - break - - span_name = f"chat {model}" if model else "chat" - - elif template == SPANTEMPLATE.AI_AGENT: - span_name = f"invoke_agent {name}" - - elif template == SPANTEMPLATE.AI_TOOL: - span_name = f"execute_tool {name}" - - return span_name - - -def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: - """ - Get the operation of the span based on the template. - """ - mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { - SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, - SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, - SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, - } - op = mapping.get(template, OP.FUNCTION) - - return str(op) - - -def _get_input_attributes( - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Get input attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - mapping = { - "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), - "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), - "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), - "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), - "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), - "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), - "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), - "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), - } - - def _set_from_key(key: str, value: "Any") -> None: - if key in mapping: - (attribute, data_type) = mapping[key] - if value is not None and isinstance(value, data_type): - attributes[attribute] = value - - for key, value in list(kwargs.items()): - if key == "prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "user", "content": value} - ) - continue - - if key == "system_prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "system", "content": value} - ) - continue - - _set_from_key(key, value) - - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) - - # Coerce to string - if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) - - return attributes - - -def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": - """ - Get usage attributes. - """ - attributes = {} - - def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: - for key in keys: - value = _get_value(usage, key) - if value is not None and isinstance(value, int): - attributes[attribute] = value - - _set_from_keys( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, - ("prompt_tokens", "input_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, - ("completion_tokens", "output_tokens"), - ) - _set_from_keys( - SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, - ("total_tokens",), - ) - - return attributes - - -def _get_output_attributes( - template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> "dict[str, Any]": - """ - Get output attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - with capture_internal_exceptions(): - # Usage from result, result.usage, and result.metadata.usage - usage_candidates = [result] - - usage = _get_value(result, "usage") - usage_candidates.append(usage) - - meta = _get_value(result, "metadata") - usage = _get_value(meta, "usage") - usage_candidates.append(usage) - - for usage_candidate in usage_candidates: - if usage_candidate is not None: - attributes.update(_get_usage_attributes(usage_candidate)) - - # Response model - model_name = _get_value(result, "model") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - model_name = _get_value(result, "model_name") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - # Tool output - if template == SPANTEMPLATE.AI_TOOL and send_pii: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) - - return attributes - - -def _set_input_attributes( - span: "Span", - template: "Union[str, SPANTEMPLATE]", - send_pii: bool, - name: str, - f: "Any", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> None: - """ - Set span input attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param f: The wrapped function. - :param args: The arguments to the wrapped function. - :param kwargs: The keyword arguments to the wrapped function. - """ - attributes: "dict[str, Any]" = {} - - if template == SPANTEMPLATE.AI_AGENT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: name, - } - elif template == SPANTEMPLATE.AI_CHAT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - } - elif template == SPANTEMPLATE.AI_TOOL: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: name, - } - - docstring = f.__doc__ - if docstring is not None: - attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - - attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.update_data(attributes or {}) - - -def _set_output_attributes( - span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any" -) -> None: - """ - Set span output attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param result: The result of the wrapped function. - """ - span.update_data(_get_output_attributes(template, send_pii, result) or {}) - - -def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: - """ - Check if we should continue the incoming trace according to the strict_trace_continuation spec. - https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation - """ - - client = sentry_sdk.get_client() - parsed_dsn = client.parsed_dsn - client_org_id = parsed_dsn.org_id if parsed_dsn else None - baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None - - if ( - client_org_id is not None - and baggage_org_id is not None - and client_org_id != baggage_org_id - ): - logger.debug( - f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - strict_trace_continuation: bool = client.options.get( - "strict_trace_continuation", False - ) - if strict_trace_continuation: - if (baggage_org_id is not None and client_org_id is None) or ( - baggage_org_id is None and client_org_id is not None - ): - logger.debug( - f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})" - ) - return False - - return True - - -def add_sentry_baggage_to_headers( - headers: "MutableMapping[str, str]", sentry_baggage: str -) -> None: - """Add the Sentry baggage to the headers. - - This function directly mutates the provided headers. The provided sentry_baggage - is appended to the existing baggage. If the baggage already contains Sentry items, - they are stripped out first. - """ - existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "") - stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage) - - separator = "," if len(stripped_existing_baggage) > 0 else "" - - headers[BAGGAGE_HEADER_NAME] = ( - stripped_existing_baggage + separator + sentry_baggage - ) - - -def _make_sampling_decision( - name: str, - attributes: "Optional[Attributes]", - scope: "sentry_sdk.Scope", -) -> "tuple[bool, Optional[float], Optional[float], Optional[str]]": - """ - Decide whether a span should be sampled. - - Returns a tuple with: - 1. the sampling decision - 2. the effective sample rate - 3. the sample rand - 4. the reason for not sampling the span, if unsampled - """ - client = sentry_sdk.get_client() - - if not has_tracing_enabled(client.options): - return False, None, None, None - - propagation_context = scope.get_active_propagation_context() - - sample_rand = None - if propagation_context.baggage is not None: - sample_rand = propagation_context.baggage._sample_rand() - if sample_rand is None: - sample_rand = _generate_sample_rand(propagation_context.trace_id) - - # If there's a traces_sampler, use that; otherwise use traces_sample_rate - traces_sampler_defined = callable(client.options.get("traces_sampler")) - if traces_sampler_defined: - sampling_context = { - "span_context": { - "name": name, - "trace_id": propagation_context.trace_id, - "parent_span_id": propagation_context.parent_span_id, - "parent_sampled": propagation_context.parent_sampled, - "attributes": dict(attributes) if attributes else {}, - }, - } - - if propagation_context.custom_sampling_context: - sampling_context.update(propagation_context.custom_sampling_context) - - sample_rate = client.options["traces_sampler"](sampling_context) - else: - if propagation_context.parent_sampled is not None: - sample_rate = propagation_context.parent_sampled - else: - sample_rate = client.options["traces_sample_rate"] - - # Validate whether the sample_rate we got is actually valid. Since - # traces_sampler is user-provided, it could return anything. - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning(f"[Tracing] Discarding {name} because of invalid sample rate.") - return False, None, None, "sample_rate" - - sample_rate = float(sample_rate) - if not sample_rate: - if traces_sampler_defined: - reason = "traces_sampler returned 0 or False" - else: - reason = "traces_sample_rate is set to 0" - - logger.debug(f"[Tracing] Discarding {name} because {reason}") - return False, 0.0, None, "sample_rate" - - # Adjust sample rate if we're under backpressure - sample_rate_before_backpressure = sample_rate - if client.monitor: - sample_rate /= 2**client.monitor.downsample_factor - - if not sample_rate: - logger.debug(f"[Tracing] Discarding {name} because backpressure") - return False, 0.0, None, "backpressure" - - # Make the actual decision - sampled = sample_rand < sample_rate - - if sampled: - logger.debug(f"[Tracing] Starting {name}") - outcome = None - - else: - # Determine why exactly the span will not be sampled. If we've lowered - # the effective sample_rate because of backpressure, check whether the - # span would've been sampled if backpressure wasn't active. If that's the - # case, backpressure is the actual reason, otherwise just pure sampling - # rate. - if ( - sample_rate_before_backpressure != sample_rate - and sample_rand < sample_rate_before_backpressure - ): - logger.debug(f"[Tracing] Discarding {name} because backpressure") - outcome = "backpressure" - - else: - logger.debug( - f"[Tracing] Discarding {name} because it's not included in the random sample (sampling rate = {sample_rate})" - ) - outcome = "sample_rate" - - return sampled, sample_rate, sample_rand, outcome - - -def is_ignored_span(name: str, attributes: "Optional[Attributes]") -> bool: - """Determine if a span fits one of the rules in ignore_spans.""" - client = sentry_sdk.get_client() - ignore_spans = (client.options.get("_experiments") or {}).get("ignore_spans") - - if not ignore_spans: - return False - - def _matches(rule: "Any", value: "Any") -> bool: - if isinstance(rule, Pattern): - if isinstance(value, str): - return bool(rule.fullmatch(value)) - else: - return False - - return rule == value - - for rule in ignore_spans: - if isinstance(rule, (str, Pattern)): - if _matches(rule, name): - return True - - elif isinstance(rule, dict) and ("name" in rule or "attributes" in rule): - name_matches = True - attributes_match = True - - if "name" in rule: - name_matches = _matches(rule["name"], name) - - if "attributes" in rule: - attributes = attributes or {} - - for attribute, value in rule["attributes"].items(): - if attribute not in attributes or not _matches( - value, attributes[attribute] - ): - attributes_match = False - break - - if name_matches and attributes_match: - return True - - return False - - -# Circular imports -from sentry_sdk.traces import ( - LOW_QUALITY_SEGMENT_SOURCES, - StreamedSpan, -) -from sentry_sdk.traces import ( - start_span as start_streaming_span, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - LOW_QUALITY_TRANSACTION_SOURCES, - SENTRY_TRACE_HEADER_NAME, - Span, -) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py deleted file mode 100644 index 2b71fee429..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/transport.py +++ /dev/null @@ -1,1255 +0,0 @@ -import asyncio -import gzip -import io -import json -import logging -import os -import socket -import ssl -import time -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from datetime import datetime, timedelta, timezone -from urllib.request import getproxies - -try: - import brotli # type: ignore -except ImportError: - brotli = None - -try: - import httpcore -except ImportError: - httpcore = None # type: ignore[assignment,unused-ignore] - -try: - import h2 # noqa: F401 - - HTTP2_ENABLED = httpcore is not None -except ImportError: - HTTP2_ENABLED = False - -try: - import anyio # noqa: F401 - - ASYNC_TRANSPORT_AVAILABLE = httpcore is not None -except ImportError: - ASYNC_TRANSPORT_AVAILABLE = False - -from typing import TYPE_CHECKING, Dict, List, cast - -import certifi -import urllib3 - -import sentry_sdk -from sentry_sdk.consts import EndpointType -from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.utils import ( - Dsn, - capture_internal_exceptions, - logger, - mark_sentry_task_internal, -) -from sentry_sdk.worker import AsyncWorker, BackgroundWorker, Worker - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - DefaultDict, - Iterable, - Mapping, - Optional, - Self, - Tuple, - Type, - Union, - ) - - from urllib3.poolmanager import PoolManager, ProxyManager - - from sentry_sdk._types import Event, EventDataCategory - -KEEP_ALIVE_SOCKET_OPTIONS = [] -for option in [ - (socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009 - (socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009 -]: - try: - KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2])) - except AttributeError: - # a specific option might not be available on specific systems, - # e.g. TCP_KEEPIDLE doesn't exist on macOS - pass - - -def _get_httpcore_header_value(response: "Any", header: str) -> "Optional[str]": - """Case-insensitive header lookup for httpcore-style responses.""" - header_lower = header.lower() - return next( - ( - val.decode("ascii") - for key, val in response.headers - if key.decode("ascii").lower() == header_lower - ), - None, - ) - - -class Transport(ABC): - """Baseclass for all transports. - - A transport is used to send an event to sentry. - """ - - parsed_dsn: "Optional[Dsn]" = None - - def __init__(self: "Self", options: "Optional[Dict[str, Any]]" = None) -> None: - self.options = options - if options and options["dsn"]: - self.parsed_dsn = Dsn(options["dsn"], options.get("org_id")) - else: - self.parsed_dsn = None - - def capture_event(self: "Self", event: "Event") -> None: - """ - DEPRECATED: Please use capture_envelope instead. - - This gets invoked with the event dictionary when an event should - be sent to sentry. - """ - - warnings.warn( - "capture_event is deprecated, please use capture_envelope instead!", - DeprecationWarning, - stacklevel=2, - ) - - envelope = Envelope() - envelope.add_event(event) - self.capture_envelope(envelope) - - @abstractmethod - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - """ - Send an envelope to Sentry. - - Envelopes are a data container format that can hold any type of data - submitted to Sentry. We use it to send all event data (including errors, - transactions, crons check-ins, etc.) to Sentry. - """ - pass - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Any]" = None, - ) -> None: - """ - Wait `timeout` seconds for the current events to be sent out. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def kill(self: "Self") -> None: - """ - Forcefully kills the transport. - - The default implementation is a no-op, since this method may only be relevant to some transports. - Subclasses should override this method if necessary. - """ - return None - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - """This increments a counter for event loss by reason and - data category by the given positive-int quantity (default 1). - - If an item is provided, the data category and quantity are - extracted from the item, and the values passed for - data_category and quantity are ignored. - - When recording a lost transaction via data_category="transaction", - the calling code should also record the lost spans via this method. - When recording lost spans, `quantity` should be set to the number - of contained spans, plus one for the transaction itself. When - passing an Item containing a transaction via the `item` parameter, - this method automatically records the lost spans. - """ - return None - - def is_healthy(self: "Self") -> bool: - return True - - -def _parse_rate_limits( - header: str, now: "Optional[datetime]" = None -) -> "Iterable[Tuple[Optional[EventDataCategory], datetime]]": - if now is None: - now = datetime.now(timezone.utc) - - for limit in header.split(","): - try: - parameters = limit.strip().split(":") - retry_after_val, categories = parameters[:2] - - retry_after = now + timedelta(seconds=int(retry_after_val)) - for category in categories and categories.split(";") or (None,): - yield category, retry_after # type: ignore - except (LookupError, ValueError): - continue - - -class HttpTransportCore(Transport): - """Shared base class for sync and async transports.""" - - TIMEOUT = 30 # seconds - - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - from sentry_sdk.consts import VERSION - - Transport.__init__(self, options) - assert self.parsed_dsn is not None - self.options: "Dict[str, Any]" = options - self._worker = self._create_worker(options) - self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) - self._disabled_until: "Dict[Optional[EventDataCategory], datetime]" = {} - # We only use this Retry() class for the `get_retry_after` method it exposes - self._retry = urllib3.util.Retry() - self._discarded_events: "DefaultDict[Tuple[EventDataCategory, str], int]" = ( - defaultdict(int) - ) - self._last_client_report_sent = time.time() - - self._pool = self._make_pool() - - # Backwards compatibility for deprecated `self.hub_class` attribute - self._hub_cls = sentry_sdk.Hub - - experiments = options.get("_experiments", {}) - compression_level = experiments.get( - "transport_compression_level", - experiments.get("transport_zlib_compression_level"), - ) - compression_algo = experiments.get( - "transport_compression_algo", - ( - "gzip" - # if only compression level is set, assume gzip for backwards compatibility - # if we don't have brotli available, fallback to gzip - if compression_level is not None or brotli is None - else "br" - ), - ) - - if compression_algo == "br" and brotli is None: - logger.warning( - "You asked for brotli compression without the Brotli module, falling back to gzip -9" - ) - compression_algo = "gzip" - compression_level = None - - if compression_algo not in ("br", "gzip"): - logger.warning( - "Unknown compression algo %s, disabling compression", compression_algo - ) - self._compression_level = 0 - self._compression_algo = None - else: - self._compression_algo = compression_algo - - if compression_level is not None: - self._compression_level = compression_level - elif self._compression_algo == "gzip": - self._compression_level = 9 - elif self._compression_algo == "br": - self._compression_level = 4 - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - raise NotImplementedError() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - if not self.options["send_client_reports"]: - return - - if item is not None: - data_category = item.data_category - quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). - - if data_category == "transaction": - # Also record the lost spans - event = item.get_transaction_event() or {} - - # +1 for the transaction itself - span_count = ( - len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 - ) - self.record_lost_event(reason, "span", quantity=span_count) - - elif data_category == "log_item" and item: - # Also record size of lost logs in bytes - bytes_size = len(item.get_bytes()) - self.record_lost_event(reason, "log_byte", quantity=bytes_size) - - elif data_category == "attachment": - # quantity of 0 is actually 1 as we do not want to count - # empty attachments as actually empty. - quantity = len(item.get_bytes()) or 1 - - elif data_category is None: - raise TypeError("data category not provided") - - self._discarded_events[data_category, reason] += quantity - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return response.headers.get(header) - - def _update_rate_limits( - self: "Self", response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]" - ) -> None: - # new sentries with more rate limit insights. We honor this header - # no matter of the status code to update our internal rate limits. - header = self._get_header_value(response, "x-sentry-rate-limits") - if header: - logger.warning("Rate-limited via x-sentry-rate-limits") - self._disabled_until.update(_parse_rate_limits(header)) - - # old sentries only communicate global rate limit hits via the - # retry-after header on 429. This header can also be emitted on new - # sentries if a proxy in front wants to globally slow things down. - elif response.status == 429: - logger.warning("Rate-limited via 429") - retry_after_value = self._get_header_value(response, "Retry-After") - retry_after = ( - self._retry.parse_retry_after(retry_after_value) - if retry_after_value is not None - else None - ) or 60 - self._disabled_until[None] = datetime.now(timezone.utc) + timedelta( - seconds=retry_after - ) - - def _handle_request_error( - self: "Self", - envelope: "Optional[Envelope]", - loss_reason: str = "network", - record_reason: str = "network_error", - ) -> None: - def record_loss(reason: str) -> None: - if envelope is None: - self.record_lost_event(reason, data_category="error") - else: - for item in envelope.items: - self.record_lost_event(reason, item=item) - - self.on_dropped_event(loss_reason) - record_loss(record_reason) - - def _handle_response( - self: "Self", - response: "Union[urllib3.BaseHTTPResponse, httpcore.Response]", - envelope: "Optional[Envelope]", - ) -> None: - self._update_rate_limits(response) - - if response.status == 413: - size_exceeded_message = ( - "HTTP 413: Event dropped due to exceeded envelope size limit" - ) - response_message = getattr( - response, "data", getattr(response, "content", None) - ) - if response_message is not None: - size_exceeded_message += f" (body: {response_message})" - - logger.error(size_exceeded_message) - self._handle_request_error( - envelope=envelope, loss_reason="status_413", record_reason="send_error" - ) - - elif response.status == 429: - # if we hit a 429. Something was rate limited but we already - # acted on this in `self._update_rate_limits`. Note that we - # do not want to record event loss here as we will have recorded - # an outcome in relay already. - self.on_dropped_event("status_429") - pass - - elif response.status >= 300 or response.status < 200: - logger.error( - "Unexpected status code: %s (body: %s)", - response.status, - getattr(response, "data", getattr(response, "content", None)), - ) - self._handle_request_error( - envelope=envelope, loss_reason="status_{}".format(response.status) - ) - - def _update_headers( - self: "Self", - headers: "Dict[str, str]", - ) -> None: - headers.update( - { - "User-Agent": str(self._auth.client), - "X-Sentry-Auth": str(self._auth.to_header()), - } - ) - - def on_dropped_event(self: "Self", _reason: str) -> None: - return None - - def _fetch_pending_client_report( - self: "Self", force: bool = False, interval: int = 60 - ) -> "Optional[Item]": - if not self.options["send_client_reports"]: - return None - - if not (force or self._last_client_report_sent < time.time() - interval): - return None - - discarded_events = self._discarded_events - self._discarded_events = defaultdict(int) - self._last_client_report_sent = time.time() - - if not discarded_events: - return None - - return Item( - PayloadRef( - json={ - "timestamp": time.time(), - "discarded_events": [ - {"reason": reason, "category": category, "quantity": quantity} - for ( - (category, reason), - quantity, - ) in discarded_events.items() - ], - } - ), - type="client_report", - ) - - def _check_disabled(self, category: str) -> bool: - def _disabled(bucket: "Any") -> bool: - ts = self._disabled_until.get(bucket) - return ts is not None and ts > datetime.now(timezone.utc) - - return _disabled(category) or _disabled(None) - - def _is_rate_limited(self: "Self") -> bool: - return any( - ts > datetime.now(timezone.utc) for ts in self._disabled_until.values() - ) - - def _is_worker_full(self: "Self") -> bool: - return self._worker.full() - - def is_healthy(self: "Self") -> bool: - return not (self._is_worker_full() or self._is_rate_limited()) - - def _prepare_envelope( - self: "Self", envelope: "Envelope" - ) -> "Optional[Tuple[Envelope, io.BytesIO, Dict[str, str]]]": - # remove all items from the envelope which are over quota - new_items = [] - for item in envelope.items: - if self._check_disabled(item.data_category): - if item.data_category in ("transaction", "error", "default", "statsd"): - self.on_dropped_event("self_rate_limits") - self.record_lost_event("ratelimit_backoff", item=item) - else: - new_items.append(item) - - # Since we're modifying the envelope here make a copy so that others - # that hold references do not see their envelope modified. - envelope = Envelope(headers=envelope.headers, items=new_items) - - if not envelope.items: - return None - - # since we're already in the business of sending out an envelope here - # check if we have one pending for the stats session envelopes so we - # can attach it to this enveloped scheduled for sending. This will - # currently typically attach the client report to the most recent - # session update. - client_report_item = self._fetch_pending_client_report(interval=30) - if client_report_item is not None: - envelope.items.append(client_report_item) - - content_encoding, body = self._serialize_envelope(envelope) - - assert self.parsed_dsn is not None - logger.debug( - "Sending envelope [%s] project:%s host:%s", - envelope.description, - self.parsed_dsn.project_id, - self.parsed_dsn.host, - ) - - headers: "Dict[str, str]" = { - "Content-Type": "application/x-sentry-envelope", - } - if content_encoding: - headers["Content-Encoding"] = content_encoding - - return envelope, body, headers - - def _serialize_envelope( - self: "Self", envelope: "Envelope" - ) -> "tuple[Optional[str], io.BytesIO]": - content_encoding = None - body = io.BytesIO() - if self._compression_level == 0 or self._compression_algo is None: - envelope.serialize_into(body) - else: - content_encoding = self._compression_algo - if self._compression_algo == "br" and brotli is not None: - body.write( - brotli.compress( - envelope.serialize(), quality=self._compression_level - ) - ) - else: # assume gzip as we sanitize the algo value in init - with gzip.GzipFile( - fileobj=body, mode="w", compresslevel=self._compression_level - ) as f: - envelope.serialize_into(f) - - return content_encoding, body - - def _get_httpcore_pool_options( - self: "Self", http2: bool = False - ) -> "Dict[str, Any]": - """Shared pool options for httpcore-based transports (Http2 and Async).""" - options: "Dict[str, Any]" = { - "http2": http2, - "retries": 3, - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - ssl_context = ssl.create_default_context() - ssl_context.load_verify_locations( - self.options["ca_certs"] - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE") - key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE") - if cert_file is not None: - ssl_context.load_cert_chain(cert_file, key_file) - - options["ssl_context"] = ssl_context - return options - - def _resolve_proxy(self: "Self") -> "Optional[str]": - """Resolve proxy URL from options and environment. Returns proxy URL or None.""" - if self.parsed_dsn is None: - return None - - no_proxy = self._in_no_proxy(self.parsed_dsn) - proxy = None - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - return proxy or None - - @property - def _timeout_extensions(self: "Self") -> "Dict[str, Any]": - return { - "timeout": { - "pool": self.TIMEOUT, - "connect": self.TIMEOUT, - "write": self.TIMEOUT, - "read": self.TIMEOUT, - } - } - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - raise NotImplementedError() - - def _in_no_proxy(self: "Self", parsed_dsn: "Dsn") -> bool: - no_proxy = getproxies().get("no") - if not no_proxy: - return False - for host in no_proxy.split(","): - host = host.strip() - if parsed_dsn.host.endswith(host) or parsed_dsn.netloc.endswith(host): - return True - return False - - def _make_pool( - self: "Self", - ) -> "Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool, httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - raise NotImplementedError() - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "Union[urllib3.BaseHTTPResponse, httpcore.Response]": - raise NotImplementedError() - - def kill(self: "Self") -> None: - logger.debug("Killing HTTP transport") - self._worker.kill() - - -# Keep BaseHttpTransport as an alias for backwards compatibility -# and for the sync transport implementation -class BaseHttpTransport(HttpTransportCore): - """The base HTTP transport (synchronous).""" - - def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - response.close() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return BackgroundWorker(queue_size=options["transport_queue_size"]) - - def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def capture_envelope( - self, - envelope: "Envelope", - ) -> None: - def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - self._send_envelope(envelope) - self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def flush( - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - self._worker.flush(timeout, callback) - - @staticmethod - def _warn_hub_cls() -> None: - """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" - warnings.warn( - "The `hub_cls` attribute is deprecated and will be removed in a future release.", - DeprecationWarning, - stacklevel=3, - ) - - @property - def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - return self._hub_cls - - @hub_cls.setter - def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - self._hub_cls = value - - -class HttpTransport(BaseHttpTransport): - if TYPE_CHECKING: - _pool: "Union[PoolManager, ProxyManager]" - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - num_pools = self.options.get("_experiments", {}).get("transport_num_pools") - options = { - "num_pools": 2 if num_pools is None else int(num_pools), - "cert_reqs": "CERT_REQUIRED", - "timeout": urllib3.Timeout(total=self.TIMEOUT), - } - - socket_options: "Optional[List[Tuple[int, int, int | bytes]]]" = None - - if self.options["socket_options"] is not None: - socket_options = self.options["socket_options"] - - if self.options["keep_alive"]: - if socket_options is None: - socket_options = [] - - used_options = {(o[0], o[1]) for o in socket_options} - for default_option in KEEP_ALIVE_SOCKET_OPTIONS: - if (default_option[0], default_option[1]) not in used_options: - socket_options.append(default_option) - - if socket_options is not None: - options["socket_options"] = socket_options - - options["ca_certs"] = ( - self.options["ca_certs"] # User-provided bundle from the SDK init - or os.environ.get("SSL_CERT_FILE") - or os.environ.get("REQUESTS_CA_BUNDLE") - or certifi.where() - ) - - options["cert_file"] = self.options["cert_file"] or os.environ.get( - "CLIENT_CERT_FILE" - ) - options["key_file"] = self.options["key_file"] or os.environ.get( - "CLIENT_KEY_FILE" - ) - - return options - - def _make_pool(self: "Self") -> "Union[PoolManager, ProxyManager]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = None - no_proxy = self._in_no_proxy(self.parsed_dsn) - - # try HTTPS first - https_proxy = self.options["https_proxy"] - if self.parsed_dsn.scheme == "https" and (https_proxy != ""): - proxy = https_proxy or (not no_proxy and getproxies().get("https")) - - # maybe fallback to HTTP proxy - http_proxy = self.options["http_proxy"] - if not proxy and (http_proxy != ""): - proxy = http_proxy or (not no_proxy and getproxies().get("http")) - - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - use_socks_proxy = True - try: - # Check if PySocks dependency is available - from urllib3.contrib.socks import SOCKSProxyManager - except ImportError: - use_socks_proxy = False - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.", - proxy, - ) - - if use_socks_proxy: - return SOCKSProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - else: - return urllib3.ProxyManager(proxy, **opts) - else: - return urllib3.PoolManager(**opts) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "urllib3.BaseHTTPResponse": - return self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - body=body, - headers=headers, - ) - - -class AsyncHttpTransport(HttpTransportCore): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - if not ASYNC_TRANSPORT_AVAILABLE: - raise RuntimeError( - "AsyncHttpTransport requires httpcore[asyncio]. " - "Install it with: pip install sentry-sdk[asyncio]" - ) - super().__init__(options) - # Requires event loop at init time - self.loop = asyncio.get_running_loop() - - def _create_worker(self: "Self", options: "Dict[str, Any]") -> "Worker": - return AsyncWorker(queue_size=options["transport_queue_size"]) - - def _get_header_value( - self: "Self", response: "Any", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - async def _send_envelope(self: "Self", envelope: "Envelope") -> None: - _prepared_envelope = self._prepare_envelope(envelope) - if _prepared_envelope is not None: - envelope, body, headers = _prepared_envelope - await self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) - return None - - async def _send_request( - self: "Self", - body: bytes, - headers: "Dict[str, str]", - endpoint_type: "EndpointType", - envelope: "Optional[Envelope]" = None, - ) -> None: - self._update_headers(headers) - try: - response = await self._request( - "POST", - endpoint_type, - body, - headers, - ) - except Exception: - self._handle_request_error(envelope=envelope, loss_reason="network") - raise - try: - self._handle_response(response=response, envelope=envelope) - finally: - await response.aclose() - - async def _request( # type: ignore[override] - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - return await self._pool.request( # type: ignore[misc,unused-ignore] - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - - async def _flush_client_reports(self: "Self", force: bool = False) -> None: - client_report = self._fetch_pending_client_report(force=force, interval=60) - if client_report is not None: - self.capture_envelope(Envelope(items=[client_report])) - - def _capture_envelope(self: "Self", envelope: "Envelope") -> None: - async def send_envelope_wrapper() -> None: - with capture_internal_exceptions(): - await self._send_envelope(envelope) - await self._flush_client_reports() - - if not self._worker.submit(send_envelope_wrapper): - self.on_dropped_event("full_queue") - for item in envelope.items: - self.record_lost_event("queue_overflow", item=item) - - def capture_envelope(self: "Self", envelope: "Envelope") -> None: - # Synchronous entry point - if self.loop and self.loop.is_running(): - self.loop.call_soon_threadsafe(self._capture_envelope, envelope) - else: - # The event loop is no longer running - logger.warning("Async Transport is not running in an event loop.") - self.on_dropped_event("internal_sdk_error") - for item in envelope.items: - self.record_lost_event("internal_sdk_error", item=item) - - def flush( # type: ignore[override] - self: "Self", - timeout: float, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> "Optional[asyncio.Task[None]]": - logger.debug("Flushing HTTP transport") - - if timeout > 0: - self._worker.submit(lambda: self._flush_client_reports(force=True)) - return self._worker.flush(timeout, callback) # type: ignore[func-returns-value] - return None - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=HTTP2_ENABLED - and self.parsed_dsn is not None - and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.AsyncSOCKSProxy, httpcore.AsyncHTTPProxy, httpcore.AsyncConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - socks_opts = opts.copy() - if "socket_options" in socks_opts: - socket_options = socks_opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.AsyncSOCKSProxy(proxy_url=proxy, **socks_opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.AsyncHTTPProxy(proxy_url=proxy, **opts) - - return httpcore.AsyncConnectionPool(**opts) - - def kill(self: "Self") -> "Optional[asyncio.Task[None]]": # type: ignore[override] - logger.debug("Killing HTTP transport") - self._worker.kill() - try: - # Return the pool cleanup task so caller can await it if needed - with mark_sentry_task_internal(): - return self.loop.create_task(self._pool.aclose()) # type: ignore[union-attr,unused-ignore] - except RuntimeError: - logger.warning("Event loop not running, aborting kill.") - return None - - -if not HTTP2_ENABLED: - # Sorry, no Http2Transport for you - class Http2Transport(HttpTransport): - def __init__(self: "Self", options: "Dict[str, Any]") -> None: - super().__init__(options) - logger.warning( - "You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport." - ) - -else: - - class Http2Transport(BaseHttpTransport): # type: ignore - """The HTTP2 transport based on httpcore.""" - - TIMEOUT = 15 - - if TYPE_CHECKING: - _pool: """Union[ - httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool - ]""" - - def _get_header_value( - self: "Self", response: "httpcore.Response", header: str - ) -> "Optional[str]": - return _get_httpcore_header_value(response, header) - - def _request( - self: "Self", - method: str, - endpoint_type: "EndpointType", - body: "Any", - headers: "Mapping[str, str]", - ) -> "httpcore.Response": - response = self._pool.request( - method, - self._auth.get_api_url(endpoint_type), - content=body, - headers=headers, # type: ignore[arg-type,unused-ignore] - extensions=self._timeout_extensions, - ) - return response - - def _get_pool_options(self: "Self") -> "Dict[str, Any]": - return self._get_httpcore_pool_options( - http2=self.parsed_dsn is not None and self.parsed_dsn.scheme == "https" - ) - - def _make_pool( - self: "Self", - ) -> "Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]": - if self.parsed_dsn is None: - raise ValueError("Cannot create HTTP-based transport without valid DSN") - - proxy = self._resolve_proxy() - opts = self._get_pool_options() - - if proxy: - proxy_headers = self.options["proxy_headers"] - if proxy_headers: - opts["proxy_headers"] = proxy_headers - - if proxy.startswith("socks"): - try: - if "socket_options" in opts: - socket_options = opts.pop("socket_options") - if socket_options: - logger.warning( - "You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options." - ) - return httpcore.SOCKSProxy(proxy_url=proxy, **opts) - except RuntimeError: - logger.warning( - "You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.", - proxy, - ) - else: - return httpcore.HTTPProxy(proxy_url=proxy, **opts) - - return httpcore.ConnectionPool(**opts) - - -class _EnvelopePrinterTransport(Transport): - """Wraps another transport, printing envelope contents to the SDK debug logger before sending.""" - - def __init__(self, transport: "Transport") -> None: - Transport.__init__(self, options=transport.options) - self._inner = transport - self.parsed_dsn = transport.parsed_dsn - - self.envelope_logger = logging.getLogger("sentry_sdk.envelopes") - self.envelope_logger.setLevel(logging.INFO) - self.envelope_logger.propagate = False - if not self.envelope_logger.handlers: - handler = logging.StreamHandler() - handler.setLevel(logging.INFO) - handler.setFormatter(logging.Formatter("%(message)s")) - self.envelope_logger.addHandler(handler) - - @property # type: ignore[misc] - def __class__(self) -> type: - return self._inner.__class__ - - def capture_envelope(self, envelope: "Envelope") -> None: - try: - self.envelope_logger.info("--- Sentry Envelope ---") - self.envelope_logger.info( - "Headers: %s", json.dumps(envelope.headers, indent=2, default=str) - ) - for item in envelope.items: - self.envelope_logger.info(" Item type: %s", item.type) - self.envelope_logger.info( - " Item headers: %s", - json.dumps(item.headers, indent=2, default=str), - ) - try: - payload = json.loads(item.get_bytes()) - self.envelope_logger.info( - " Payload:\n%s", - json.dumps(payload, indent=2, default=str), - ) - except (ValueError, TypeError): - self.envelope_logger.info( - " Payload: ", - len(item.get_bytes()), - ) - self.envelope_logger.info("--- End Envelope ---") - except Exception: - pass - - self._inner.capture_envelope(envelope) - - def flush( - self, - timeout: float, - callback: "Optional[Any]" = None, - ) -> "Any": - return self._inner.flush(timeout, callback) - - def kill(self) -> "Any": - return self._inner.kill() - - def record_lost_event( - self, - reason: str, - data_category: "Optional[EventDataCategory]" = None, - item: "Optional[Item]" = None, - *, - quantity: int = 1, - ) -> None: - self._inner.record_lost_event(reason, data_category, item, quantity=quantity) - - def is_healthy(self) -> bool: - return self._inner.is_healthy() - - def __getattr__(self, name: str) -> "Any": - return getattr(self._inner, name) - - -class _FunctionTransport(Transport): - """ - DEPRECATED: Users wishing to provide a custom transport should subclass - the Transport class, rather than providing a function. - """ - - def __init__( - self, - func: "Callable[[Event], None]", - ) -> None: - Transport.__init__(self) - self._func = func - - def capture_event( - self, - event: "Event", - ) -> None: - self._func(event) - return None - - def capture_envelope(self, envelope: "Envelope") -> None: - # Since function transports expect to be called with an event, we need - # to iterate over the envelope and call the function for each event, via - # the deprecated capture_event method. - event = envelope.get_event() - if event is not None: - self.capture_event(event) - - -def make_transport(options: "Dict[str, Any]") -> "Optional[Transport]": - ref_transport = options["transport"] - - use_http2_transport = options.get("_experiments", {}).get("transport_http2", False) - use_async_transport = options.get("_experiments", {}).get("transport_async", False) - async_integration = any( - integration.__class__.__name__ == "AsyncioIntegration" - for integration in options.get("integrations") or [] - ) - - # By default, we use the http transport class - transport_cls: "Type[Transport]" = ( - Http2Transport if use_http2_transport else HttpTransport - ) - - if use_async_transport and ASYNC_TRANSPORT_AVAILABLE: - try: - asyncio.get_running_loop() - if async_integration: - if use_http2_transport: - logger.warning( - "HTTP/2 transport is not supported with async transport. " - "Ignoring transport_http2 experiment." - ) - transport_cls = AsyncHttpTransport - else: - logger.warning( - "You tried to use AsyncHttpTransport but the AsyncioIntegration is not enabled. Falling back to sync transport." - ) - except RuntimeError: - # No event loop running, fall back to sync transport - logger.warning("No event loop running, falling back to sync transport.") - elif use_async_transport: - logger.warning( - "You tried to use AsyncHttpTransport but don't have httpcore[asyncio] installed. Falling back to sync transport." - ) - - transport: "Optional[Transport]" = None - - if isinstance(ref_transport, Transport): - transport = ref_transport - elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): - transport_cls = ref_transport - elif callable(ref_transport): - warnings.warn( - "Function transports are deprecated and will be removed in a future release." - "Please provide a Transport instance or subclass, instead.", - DeprecationWarning, - stacklevel=2, - ) - transport = _FunctionTransport(ref_transport) - - # if a transport class is given only instantiate it if the dsn is not - # empty or None - if transport is None and options["dsn"]: - transport = transport_cls(options) - - if transport is not None and os.environ.get( - "SENTRY_PRINT_ENVELOPES", "" - ).lower() in ("1", "true", "yes"): - transport = _EnvelopePrinterTransport(transport) - - return transport diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py deleted file mode 100644 index dff91e3719..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/types.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This module contains type definitions for the Sentry SDK's public API. -The types are re-exported from the internal module `sentry_sdk._types`. - -Disclaimer: Since types are a form of documentation, type definitions -may change in minor releases. Removing a type would be considered a -breaking change, and so we will only remove type definitions in major -releases. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - # Re-export types to make them available in the public API - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - EventDataCategory, - Hint, - Log, - Metric, - MonitorConfig, - SamplingContext, - ) -else: - from typing import Any - - # The lines below allow the types to be imported from outside `if TYPE_CHECKING` - # guards. The types in this module are only intended to be used for type hints. - Breadcrumb = Any - BreadcrumbHint = Any - Event = Any - EventDataCategory = Any - Hint = Any - Log = Any - MonitorConfig = Any - SamplingContext = Any - Metric = Any - - -__all__ = ( - "Breadcrumb", - "BreadcrumbHint", - "Event", - "EventDataCategory", - "Hint", - "Log", - "MonitorConfig", - "SamplingContext", - "Metric", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py deleted file mode 100644 index 0963015351..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/utils.py +++ /dev/null @@ -1,2178 +0,0 @@ -import base64 -import copy -import json -import linecache -import logging -import math -import os -import random -import re -import subprocess -import sys -import threading -import time -from collections import namedtuple -from contextlib import contextmanager -from datetime import datetime, timezone -from decimal import Decimal -from functools import partial, partialmethod, wraps -from numbers import Real -from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit - -try: - # Python 3.11 - from builtins import BaseExceptionGroup -except ImportError: - # Python 3.10 and below - BaseExceptionGroup = None # type: ignore - -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._compat import PY37 -from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE, Annotated, AnnotatedValue -from sentry_sdk.consts import ( - DEFAULT_ADD_FULL_STACK, - DEFAULT_MAX_STACK_FRAMES, - EndpointType, -) - -if TYPE_CHECKING: - from types import FrameType, TracebackType - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - Iterator, - List, - NoReturn, - Optional, - ParamSpec, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, - ) - - from gevent.hub import Hub - - from sentry_sdk._types import ( - AttributeValue, - Event, - ExcInfo, - Hint, - Log, - Metric, - SerializedAttributeValue, - SpanJSON, - ) - - P = ParamSpec("P") - R = TypeVar("R") - - -epoch = datetime(1970, 1, 1) - -# The logger is created here but initialized in the debug support module -logger = logging.getLogger("sentry_sdk.errors") - -_installed_modules = None - -BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") - -FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0")) -TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1")) - -MAX_STACK_FRAMES = 2000 -"""Maximum number of stack frames to send to Sentry. - -If we have more than this number of stack frames, we will stop processing -the stacktrace to avoid getting stuck in a long-lasting loop. This value -exceeds the default sys.getrecursionlimit() of 1000, so users will only -be affected by this limit if they have a custom recursion limit. -""" - - -def env_to_bool(value: "Any", *, strict: "Optional[bool]" = False) -> "bool | None": - """Casts an ENV variable value to boolean using the constants defined above. - In strict mode, it may return None if the value doesn't match any of the predefined values. - """ - normalized = str(value).lower() if value is not None else None - - if normalized in FALSY_ENV_VALUES: - return False - - if normalized in TRUTHY_ENV_VALUES: - return True - - return None if strict else bool(value) - - -def json_dumps(data: "Any") -> bytes: - """Serialize data into a compact JSON representation encoded as UTF-8.""" - return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") - - -def get_git_revision() -> "Optional[str]": - try: - with open(os.path.devnull, "w+") as null: - # prevent command prompt windows from popping up on windows - startupinfo = None - if sys.platform == "win32" or sys.platform == "cygwin": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - revision = ( - subprocess.Popen( - ["git", "rev-parse", "HEAD"], - startupinfo=startupinfo, - stdout=subprocess.PIPE, - stderr=null, - stdin=null, - ) - .communicate()[0] - .strip() - .decode("utf-8") - ) - except (OSError, IOError, FileNotFoundError): - return None - - return revision - - -def get_default_release() -> "Optional[str]": - """Try to guess a default release.""" - release = os.environ.get("SENTRY_RELEASE") - if release: - return release - - release = get_git_revision() - if release: - return release - - for var in ( - "HEROKU_BUILD_COMMIT", - "HEROKU_SLUG_COMMIT", # deprecated by Heroku, kept for backward compatibility - "SOURCE_VERSION", - "CODEBUILD_RESOLVED_SOURCE_VERSION", - "CIRCLE_SHA1", - "GAE_DEPLOYMENT_ID", - "K_REVISION", - ): - release = os.environ.get(var) - if release: - return release - return None - - -def get_sdk_name(installed_integrations: "List[str]") -> str: - """Return the SDK name including the name of the used web framework.""" - - # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier - # here because if django is not installed the integration is not accessible. - framework_integrations = [ - "django", - "flask", - "fastapi", - "bottle", - "falcon", - "quart", - "sanic", - "starlette", - "litestar", - "starlite", - "chalice", - "serverless", - "pyramid", - "tornado", - "aiohttp", - "aws_lambda", - "gcp", - "beam", - "asgi", - "wsgi", - ] - - for integration in framework_integrations: - if integration in installed_integrations: - return "sentry.python.{}".format(integration) - - return "sentry.python" - - -class CaptureInternalException: - __slots__ = () - - def __enter__(self) -> "ContextManager[Any]": - return self - - def __exit__( - self, - ty: "Optional[Type[BaseException]]", - value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - ) -> bool: - if ty is not None and value is not None: - capture_internal_exception((ty, value, tb)) - - return True - - -_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() - - -def capture_internal_exceptions() -> "ContextManager[Any]": - return _CAPTURE_INTERNAL_EXCEPTION - - -def capture_internal_exception(exc_info: "ExcInfo") -> None: - """ - Capture an exception that is likely caused by a bug in the SDK - itself. - - These exceptions do not end up in Sentry and are just logged instead. - """ - if sentry_sdk.get_client().is_active(): - logger.error("Internal error in sentry_sdk", exc_info=exc_info) - - -def to_timestamp(value: "datetime") -> float: - return (value - epoch).total_seconds() - - -def format_timestamp(value: "datetime") -> str: - """Formats a timestamp in RFC 3339 format. - - Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. - """ - utctime = value.astimezone(timezone.utc) - - # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for - # several years now), and isoformat is slightly different. - return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - -ISO_TZ_SEPARATORS = frozenset(("+", "-")) - - -def datetime_from_isoformat(value: str) -> "datetime": - try: - result = datetime.fromisoformat(value) - except (AttributeError, ValueError): - # py 3.6 - timestamp_format = ( - "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S" - ) - if value.endswith("Z"): - value = value[:-1] + "+0000" - - if value[-6] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - value = value[:-3] + value[-2:] - elif value[-5] in ISO_TZ_SEPARATORS: - timestamp_format += "%z" - - result = datetime.strptime(value, timestamp_format) - return result.astimezone(timezone.utc) - - -def event_hint_with_exc_info( - exc_info: "Optional[ExcInfo]" = None, -) -> "Dict[str, Optional[ExcInfo]]": - """Creates a hint with the exc info filled in.""" - if exc_info is None: - exc_info = sys.exc_info() - else: - exc_info = exc_info_from_error(exc_info) - if exc_info[0] is None: - exc_info = None - return {"exc_info": exc_info} - - -class BadDsn(ValueError): - """Raised on invalid DSNs.""" - - -class Dsn: - """Represents a DSN.""" - - ORG_ID_REGEX = re.compile(r"^o(\d+)\.") - - def __init__( - self, value: "Union[Dsn, str]", org_id: "Optional[str]" = None - ) -> None: - if isinstance(value, Dsn): - self.__dict__ = dict(value.__dict__) - return - parts = urlsplit(str(value)) - - if parts.scheme not in ("http", "https"): - raise BadDsn("Unsupported scheme %r" % parts.scheme) - self.scheme = parts.scheme - - if parts.hostname is None: - raise BadDsn("Missing hostname") - - self.host = parts.hostname - - if org_id is not None: - self.org_id: "Optional[str]" = org_id - else: - org_id_match = Dsn.ORG_ID_REGEX.match(self.host) - self.org_id = org_id_match.group(1) if org_id_match else None - - if parts.port is None: - self.port: int = self.scheme == "https" and 443 or 80 - else: - self.port = parts.port - - if not parts.username: - raise BadDsn("Missing public key") - - self.public_key = parts.username - self.secret_key = parts.password - - path = parts.path.rsplit("/", 1) - - try: - self.project_id = str(int(path.pop())) - except (ValueError, TypeError): - raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) - - self.path = "/".join(path) + "/" - - @property - def netloc(self) -> str: - """The netloc part of a DSN.""" - rv = self.host - if (self.scheme, self.port) not in (("http", 80), ("https", 443)): - rv = "%s:%s" % (rv, self.port) - return rv - - def to_auth(self, client: "Optional[Any]" = None) -> "Auth": - """Returns the auth info object for this dsn.""" - return Auth( - scheme=self.scheme, - host=self.netloc, - path=self.path, - project_id=self.project_id, - public_key=self.public_key, - secret_key=self.secret_key, - client=client, - ) - - def __str__(self) -> str: - return "%s://%s%s@%s%s%s" % ( - self.scheme, - self.public_key, - self.secret_key and "@" + self.secret_key or "", - self.netloc, - self.path, - self.project_id, - ) - - -class Auth: - """Helper object that represents the auth info.""" - - def __init__( - self, - scheme: str, - host: str, - project_id: str, - public_key: str, - secret_key: "Optional[str]" = None, - version: int = 7, - client: "Optional[Any]" = None, - path: str = "/", - ) -> None: - self.scheme = scheme - self.host = host - self.path = path - self.project_id = project_id - self.public_key = public_key - self.secret_key = secret_key - self.version = version - self.client = client - - def get_api_url( - self, - type: "EndpointType" = EndpointType.ENVELOPE, - ) -> str: - """Returns the API url for storing events.""" - return "%s://%s%sapi/%s/%s/" % ( - self.scheme, - self.host, - self.path, - self.project_id, - type.value, - ) - - def to_header(self) -> str: - """Returns the auth header a string.""" - rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] - if self.client is not None: - rv.append(("sentry_client", self.client)) - if self.secret_key is not None: - rv.append(("sentry_secret", self.secret_key)) - return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv) - - -def get_type_name(cls: "Optional[type]") -> "Optional[str]": - return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) - - -def get_type_module(cls: "Optional[type]") -> "Optional[str]": - mod = getattr(cls, "__module__", None) - if mod not in (None, "builtins", "__builtins__"): - return mod - return None - - -def should_hide_frame(frame: "FrameType") -> bool: - try: - mod = frame.f_globals["__name__"] - if mod.startswith("sentry_sdk."): - return True - except (AttributeError, KeyError): - pass - - for flag_name in "__traceback_hide__", "__tracebackhide__": - try: - if frame.f_locals[flag_name]: - return True - except Exception: - pass - - return False - - -def iter_stacks(tb: "Optional[TracebackType]") -> "Iterator[TracebackType]": - tb_: "Optional[TracebackType]" = tb - while tb_ is not None: - if not should_hide_frame(tb_.tb_frame): - yield tb_ - tb_ = tb_.tb_next - - -def get_lines_from_file( - filename: str, - lineno: int, - max_length: "Optional[int]" = None, - loader: "Optional[Any]" = None, - module: "Optional[str]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - context_lines = 5 - source = None - if loader is not None and hasattr(loader, "get_source"): - try: - source_str: "Optional[str]" = loader.get_source(module) - except (ImportError, IOError): - source_str = None - if source_str is not None: - source = source_str.splitlines() - - if source is None: - try: - source = linecache.getlines(filename) - except (OSError, IOError): - return [], None, [] - - if not source: - return [], None, [] - - lower_bound = max(0, lineno - context_lines) - upper_bound = min(lineno + 1 + context_lines, len(source)) - - try: - pre_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[lower_bound:lineno] - ] - context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length) - post_context = [ - strip_string(line.strip("\r\n"), max_length=max_length) - for line in source[(lineno + 1) : upper_bound] - ] - return pre_context, context_line, post_context - except IndexError: - # the file may have changed since it was loaded into memory - return [], None, [] - - -def get_source_context( - frame: "FrameType", - tb_lineno: "Optional[int]", - max_value_length: "Optional[int]" = None, -) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]": - try: - abs_path: "Optional[str]" = frame.f_code.co_filename - except Exception: - abs_path = None - try: - module = frame.f_globals["__name__"] - except Exception: - return [], None, [] - try: - loader = frame.f_globals["__loader__"] - except Exception: - loader = None - - if tb_lineno is not None and abs_path: - lineno = tb_lineno - 1 - return get_lines_from_file( - abs_path, lineno, max_value_length, loader=loader, module=module - ) - - return [], None, [] - - -def safe_str(value: "Any") -> str: - try: - return str(value) - except Exception: - return safe_repr(value) - - -def safe_repr(value: "Any") -> str: - try: - return repr(value) - except Exception: - return "" - - -def filename_for_module( - module: "Optional[str]", abs_path: "Optional[str]" -) -> "Optional[str]": - if not abs_path or not module: - return abs_path - - try: - if abs_path.endswith(".pyc"): - abs_path = abs_path[:-1] - - base_module = module.split(".", 1)[0] - if base_module == module: - return os.path.basename(abs_path) - - base_module_path = sys.modules[base_module].__file__ - if not base_module_path: - return abs_path - - return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( - os.sep - ) - except Exception: - return abs_path - - -def serialize_frame( - frame: "FrameType", - tb_lineno: "Optional[int]" = None, - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, - custom_repr: "Optional[Callable[..., Optional[str]]]" = None, -) -> "Dict[str, Any]": - f_code = getattr(frame, "f_code", None) - if not f_code: - abs_path = None - function = None - else: - abs_path = frame.f_code.co_filename - function = frame.f_code.co_name - try: - module = frame.f_globals["__name__"] - except Exception: - module = None - - if tb_lineno is None: - tb_lineno = frame.f_lineno - - try: - os_abs_path = os.path.abspath(abs_path) if abs_path else None - except Exception: - os_abs_path = None - - rv: "Dict[str, Any]" = { - "filename": filename_for_module(module, abs_path) or None, - "abs_path": os_abs_path, - "function": function or "", - "module": module, - "lineno": tb_lineno, - } - - if include_source_context: - rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context( - frame, tb_lineno, max_value_length - ) - - if include_local_variables: - from sentry_sdk.serializer import serialize - - rv["vars"] = serialize( - dict(frame.f_locals), is_vars=True, custom_repr=custom_repr - ) - - return rv - - -def current_stacktrace( - include_local_variables: bool = True, - include_source_context: bool = True, - max_value_length: "Optional[int]" = None, -) -> "Dict[str, Any]": - __tracebackhide__ = True - frames = [] - - f: "Optional[FrameType]" = sys._getframe() - while f is not None: - if not should_hide_frame(f): - frames.append( - serialize_frame( - f, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - ) - ) - f = f.f_back - - frames.reverse() - - return {"frames": frames} - - -def get_errno(exc_value: BaseException) -> "Optional[Any]": - return getattr(exc_value, "errno", None) - - -def get_error_message(exc_value: "Optional[BaseException]") -> str: - message: str = safe_str( - getattr(exc_value, "message", "") - or getattr(exc_value, "detail", "") - or safe_str(exc_value) - ) - - # __notes__ should be a list of strings when notes are added - # via add_note, but can be anything else if __notes__ is set - # directly. We only support strings in __notes__, since that - # is the correct use. - notes: object = getattr(exc_value, "__notes__", None) - if isinstance(notes, list) and len(notes) > 0: - message += "\n" + "\n".join(note for note in notes if isinstance(note, str)) - - return message - - -def single_exception_from_error_tuple( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: "Optional[int]" = None, - parent_id: "Optional[int]" = None, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "Dict[str, Any]": - """ - Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - """ - exception_value: "Dict[str, Any]" = {} - exception_value["mechanism"] = ( - mechanism.copy() if mechanism else {"type": "generic", "handled": True} - ) - if exception_id is not None: - exception_value["mechanism"]["exception_id"] = exception_id - - if exc_value is not None: - errno = get_errno(exc_value) - else: - errno = None - - if errno is not None: - exception_value["mechanism"].setdefault("meta", {}).setdefault( - "errno", {} - ).setdefault("number", errno) - - if source is not None: - exception_value["mechanism"]["source"] = source - - is_root_exception = exception_id == 0 - if not is_root_exception and parent_id is not None: - exception_value["mechanism"]["parent_id"] = parent_id - exception_value["mechanism"]["type"] = "chained" - - if is_root_exception and "type" not in exception_value["mechanism"]: - exception_value["mechanism"]["type"] = "generic" - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - if is_exception_group: - exception_value["mechanism"]["is_exception_group"] = True - - exception_value["module"] = get_type_module(exc_type) - exception_value["type"] = get_type_name(exc_type) - exception_value["value"] = get_error_message(exc_value) - - if client_options is None: - include_local_variables = True - include_source_context = True - max_value_length = None # fallback - custom_repr = None - else: - include_local_variables = client_options["include_local_variables"] - include_source_context = client_options["include_source_context"] - max_value_length = client_options["max_value_length"] - custom_repr = client_options.get("custom_repr") - - frames: "List[Dict[str, Any]]" = [ - serialize_frame( - tb.tb_frame, - tb_lineno=tb.tb_lineno, - include_local_variables=include_local_variables, - include_source_context=include_source_context, - max_value_length=max_value_length, - custom_repr=custom_repr, - ) - # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on - # processing a super-long stacktrace. - for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1)) - ] - - if len(frames) > MAX_STACK_FRAMES: - # If we have more frames than the limit, we remove the stacktrace completely. - # We don't trim the stacktrace here because we have not processed the whole - # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would - # intelligently trim by removing frames in the middle of the stacktrace, but - # since we don't have the whole stacktrace, we can't do that. Instead, we - # drop the entire stacktrace. - exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit( - value=None - ) - - elif frames: - if not full_stack: - new_frames = frames - else: - new_frames = merge_stack_frames(frames, full_stack, client_options) - - exception_value["stacktrace"] = {"frames": new_frames} - - return exception_value - - -HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") - -if HAS_CHAINED_EXCEPTIONS: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - exc_type, exc_value, tb = exc_info - - seen_exceptions = [] - seen_exception_ids: "Set[int]" = set() - - while ( - exc_type is not None - and exc_value is not None - and id(exc_value) not in seen_exception_ids - ): - yield exc_type, exc_value, tb - - # Avoid hashing random types we don't know anything - # about. Use the list to keep a ref so that the `id` is - # not used for another object. - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - if exc_value.__suppress_context__: - cause = exc_value.__cause__ - else: - cause = exc_value.__context__ - if cause is None: - break - exc_type = type(cause) - exc_value = cause - tb = getattr(cause, "__traceback__", None) - -else: - - def walk_exception_chain(exc_info: "ExcInfo") -> "Iterator[ExcInfo]": - yield exc_info - - -def exceptions_from_error( - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[TracebackType]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - exception_id: int = 0, - parent_id: int = 0, - source: "Optional[str]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, - seen_exceptions: "Optional[list[BaseException]]" = None, - seen_exception_ids: "Optional[Set[int]]" = None, -) -> "Tuple[int, List[Dict[str, Any]]]": - """ - Creates the list of exceptions. - This can include chained exceptions and exceptions from an ExceptionGroup. - - See the Exception Interface documentation for more details: - https://develop.sentry.dev/sdk/event-payloads/exception/ - - Args: - exception_id (int): - - Sequential counter for assigning ``mechanism.exception_id`` - to each processed exception. Is NOT the result of calling `id()` on the exception itself. - - parent_id (int): - - The ``mechanism.exception_id`` of the parent exception. - - Written into ``mechanism.parent_id`` in the event payload so Sentry can - reconstruct the exception tree. - - Not to be confused with ``seen_exception_ids``, which tracks Python ``id()`` - values for cycle detection. - """ - - if seen_exception_ids is None: - seen_exception_ids = set() - - if seen_exceptions is None: - seen_exceptions = [] - - if exc_value is not None and id(exc_value) in seen_exception_ids: - return (exception_id, []) - - if exc_value is not None: - seen_exceptions.append(exc_value) - seen_exception_ids.add(id(exc_value)) - - parent = single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source=source, - full_stack=full_stack, - ) - exceptions = [parent] - - parent_id = exception_id - exception_id += 1 - - should_supress_context = ( - hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore - ) - if should_supress_context: - # Add direct cause. - # The field `__cause__` is set when raised with the exception (using the `from` keyword). - exception_has_cause = ( - exc_value - and hasattr(exc_value, "__cause__") - and exc_value.__cause__ is not None - ) - if exception_has_cause: - cause = exc_value.__cause__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(cause), - exc_value=cause, - tb=getattr(cause, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__cause__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - else: - # Add indirect cause. - # The field `__context__` is assigned if another exception occurs while handling the exception. - exception_has_content = ( - exc_value - and hasattr(exc_value, "__context__") - and exc_value.__context__ is not None - ) - if exception_has_content: - context = exc_value.__context__ # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(context), - exc_value=context, - tb=getattr(context, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - source="__context__", - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - # Add exceptions from an ExceptionGroup. - is_exception_group = exc_value and hasattr(exc_value, "exceptions") - if is_exception_group: - for idx, e in enumerate(exc_value.exceptions): # type: ignore - (exception_id, child_exceptions) = exceptions_from_error( - exc_type=type(e), - exc_value=e, - tb=getattr(e, "__traceback__", None), - client_options=client_options, - mechanism=mechanism, - exception_id=exception_id, - parent_id=parent_id, - source="exceptions[%s]" % idx, - full_stack=full_stack, - seen_exceptions=seen_exceptions, - seen_exception_ids=seen_exception_ids, - ) - exceptions.extend(child_exceptions) - - return (exception_id, exceptions) - - -def exceptions_from_error_tuple( - exc_info: "ExcInfo", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, - full_stack: "Optional[list[dict[str, Any]]]" = None, -) -> "List[Dict[str, Any]]": - exc_type, exc_value, tb = exc_info - - is_exception_group = BaseExceptionGroup is not None and isinstance( - exc_value, BaseExceptionGroup - ) - - if is_exception_group: - (_, exceptions) = exceptions_from_error( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - exception_id=0, - parent_id=0, - full_stack=full_stack, - ) - - else: - exceptions = [] - for exc_type, exc_value, tb in walk_exception_chain(exc_info): - exceptions.append( - single_exception_from_error_tuple( - exc_type=exc_type, - exc_value=exc_value, - tb=tb, - client_options=client_options, - mechanism=mechanism, - full_stack=full_stack, - ) - ) - - exceptions.reverse() - - return exceptions - - -def to_string(value: str) -> str: - try: - return str(value) - except UnicodeDecodeError: - return repr(value)[1:-1] - - -def iter_event_stacktraces(event: "Event") -> "Iterator[Annotated[Dict[str, Any]]]": - if "stacktrace" in event: - yield event["stacktrace"] - if "threads" in event: - for thread in event["threads"].get("values") or (): - if "stacktrace" in thread: - yield thread["stacktrace"] - if "exception" in event: - for exception in event["exception"].get("values") or (): - if isinstance(exception, dict) and "stacktrace" in exception: - yield exception["stacktrace"] - - -def iter_event_frames(event: "Event") -> "Iterator[Dict[str, Any]]": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - for frame in stacktrace.get("frames") or (): - yield frame - - -def handle_in_app( - event: "Event", - in_app_exclude: "Optional[List[str]]" = None, - in_app_include: "Optional[List[str]]" = None, - project_root: "Optional[str]" = None, -) -> "Event": - for stacktrace in iter_event_stacktraces(event): - if isinstance(stacktrace, AnnotatedValue): - stacktrace = stacktrace.value or {} - - set_in_app_in_frames( - stacktrace.get("frames"), - in_app_exclude=in_app_exclude, - in_app_include=in_app_include, - project_root=project_root, - ) - - return event - - -def set_in_app_in_frames( - frames: "Any", - in_app_exclude: "Optional[List[str]]", - in_app_include: "Optional[List[str]]", - project_root: "Optional[str]" = None, -) -> "Optional[Any]": - if not frames: - return None - - for frame in frames: - # if frame has already been marked as in_app, skip it - current_in_app = frame.get("in_app") - if current_in_app is not None: - continue - - module = frame.get("module") - - # check if module in frame is in the list of modules to include - if _module_in_list(module, in_app_include): - frame["in_app"] = True - continue - - # check if module in frame is in the list of modules to exclude - if _module_in_list(module, in_app_exclude): - frame["in_app"] = False - continue - - # if frame has no abs_path, skip further checks - abs_path = frame.get("abs_path") - if abs_path is None: - continue - - if _is_external_source(abs_path): - frame["in_app"] = False - continue - - if _is_in_project_root(abs_path, project_root): - frame["in_app"] = True - continue - - return frames - - -def exc_info_from_error(error: "Union[BaseException, ExcInfo]") -> "ExcInfo": - if isinstance(error, tuple) and len(error) == 3: - exc_type, exc_value, tb = error - elif isinstance(error, BaseException): - tb = getattr(error, "__traceback__", None) - if tb is not None: - exc_type = type(error) - exc_value = error - else: - exc_type, exc_value, tb = sys.exc_info() - if exc_value is not error: - tb = None - exc_value = error - exc_type = type(error) - - else: - raise ValueError("Expected Exception object to report, got %s!" % type(error)) - - exc_info = (exc_type, exc_value, tb) - - if TYPE_CHECKING: - # This cast is safe because exc_type and exc_value are either both - # None or both not None. - exc_info = cast(ExcInfo, exc_info) - - return exc_info - - -def merge_stack_frames( - frames: "List[Dict[str, Any]]", - full_stack: "List[Dict[str, Any]]", - client_options: "Optional[Dict[str, Any]]", -) -> "List[Dict[str, Any]]": - """ - Add the missing frames from full_stack to frames and return the merged list. - """ - frame_ids = { - ( - frame["abs_path"], - frame["context_line"], - frame["lineno"], - frame["function"], - ) - for frame in frames - } - - new_frames = [ - stackframe - for stackframe in full_stack - if ( - stackframe["abs_path"], - stackframe["context_line"], - stackframe["lineno"], - stackframe["function"], - ) - not in frame_ids - ] - new_frames.extend(frames) - - # Limit the number of frames - max_stack_frames = ( - client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) - if client_options - else None - ) - if max_stack_frames is not None: - new_frames = new_frames[len(new_frames) - max_stack_frames :] - - return new_frames - - -def event_from_exception( - exc_info: "Union[BaseException, ExcInfo]", - client_options: "Optional[Dict[str, Any]]" = None, - mechanism: "Optional[Dict[str, Any]]" = None, -) -> "Tuple[Event, Dict[str, Any]]": - exc_info = exc_info_from_error(exc_info) - hint = event_hint_with_exc_info(exc_info) - - if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK): - full_stack = current_stacktrace( - include_local_variables=client_options["include_local_variables"], - max_value_length=client_options["max_value_length"], - )["frames"] - else: - full_stack = None - - return ( - { - "level": "error", - "exception": { - "values": exceptions_from_error_tuple( - exc_info, client_options, mechanism, full_stack - ) - }, - }, - hint, - ) - - -def _module_in_list(name: "Optional[str]", items: "Optional[List[str]]") -> bool: - if name is None: - return False - - if not items: - return False - - for item in items: - if item == name or name.startswith(item + "."): - return True - - return False - - -def _is_external_source(abs_path: "Optional[str]") -> bool: - # check if frame is in 'site-packages' or 'dist-packages' - if abs_path is None: - return False - - external_source = ( - re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None - ) - return external_source - - -def _is_in_project_root( - abs_path: "Optional[str]", project_root: "Optional[str]" -) -> bool: - if abs_path is None or project_root is None: - return False - - # check if path is in the project root - if abs_path.startswith(project_root): - return True - - return False - - -def _truncate_by_bytes(string: str, max_bytes: int) -> str: - """ - Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. - """ - truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore") - - return truncated + "..." - - -def _get_size_in_bytes(value: str) -> "Optional[int]": - try: - return len(value.encode("utf-8")) - except (UnicodeEncodeError, UnicodeDecodeError): - return None - - -def strip_string( - value: str, max_length: "Optional[int]" = None -) -> "Union[AnnotatedValue, str]": - if not value or max_length is None: - return value - - byte_size = _get_size_in_bytes(value) - text_size = len(value) - - if byte_size is not None and byte_size > max_length: - # truncate to max_length bytes, preserving code points - truncated_value = _truncate_by_bytes(value, max_length) - elif text_size is not None and text_size > max_length: - # fallback to truncating by string length - truncated_value = value[: max_length - 3] + "..." - else: - return value - - return AnnotatedValue( - value=truncated_value, - metadata={ - "len": byte_size or text_size, - "rem": [["!limit", "x", max_length - 3, max_length]], - }, - ) - - -def parse_version(version: str) -> "Optional[Tuple[int, ...]]": - """ - Parses a version string into a tuple of integers. - This uses the parsing loging from PEP 440: - https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions - """ - VERSION_PATTERN = r""" # noqa: N806 - v? - (?: - (?:(?P[0-9]+)!)? # epoch - (?P[0-9]+(?:\.[0-9]+)*) # release segment - (?P
                                          # pre-release
-                [-_\.]?
-                (?P(a|b|c|rc|alpha|beta|pre|preview))
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-            (?P                                         # post release
-                (?:-(?P[0-9]+))
-                |
-                (?:
-                    [-_\.]?
-                    (?Ppost|rev|r)
-                    [-_\.]?
-                    (?P[0-9]+)?
-                )
-            )?
-            (?P                                          # dev release
-                [-_\.]?
-                (?Pdev)
-                [-_\.]?
-                (?P[0-9]+)?
-            )?
-        )
-        (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
-    """
-
-    pattern = re.compile(
-        r"^\s*" + VERSION_PATTERN + r"\s*$",
-        re.VERBOSE | re.IGNORECASE,
-    )
-
-    try:
-        release = pattern.match(version).groupdict()["release"]  # type: ignore
-        release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
-    except (TypeError, ValueError, AttributeError):
-        return None
-
-    return release_tuple
-
-
-def _is_contextvars_broken() -> bool:
-    """
-    Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
-    """
-    try:
-        import gevent
-        from gevent.monkey import is_object_patched
-
-        # Get the MAJOR and MINOR version numbers of Gevent
-        version_tuple = tuple(
-            [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
-        )
-        if is_object_patched("threading", "local"):
-            # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
-            # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
-            # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
-            # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
-            # for contextvars, is able to patch both thread locals and contextvars, in
-            # that case, check if contextvars are effectively patched.
-            if (
-                # Gevent 20.9.0+
-                (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
-                # Gevent 20.5.0+ or Python < 3.7
-                or (is_object_patched("contextvars", "ContextVar"))
-            ):
-                return False
-
-            return True
-    except ImportError:
-        pass
-
-    try:
-        import greenlet
-        from eventlet.patcher import is_monkey_patched  # type: ignore
-
-        greenlet_version = parse_version(greenlet.__version__)
-
-        if greenlet_version is None:
-            logger.error(
-                "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
-            )
-            return False
-
-        if is_monkey_patched("thread") and greenlet_version < (0, 5):
-            return True
-    except ImportError:
-        pass
-
-    return False
-
-
-def _make_threadlocal_contextvars(local: type) -> type:
-    class ContextVar:
-        # Super-limited impl of ContextVar
-
-        def __init__(self, name: str, default: "Any" = None) -> None:
-            self._name = name
-            self._default = default
-            self._local = local()
-            self._original_local = local()
-
-        def get(self, default: "Any" = None) -> "Any":
-            return getattr(self._local, "value", default or self._default)
-
-        def set(self, value: "Any") -> "Any":
-            token = str(random.getrandbits(64))
-            original_value = self.get()
-            setattr(self._original_local, token, original_value)
-            self._local.value = value
-            return token
-
-        def reset(self, token: "Any") -> None:
-            self._local.value = getattr(self._original_local, token)
-            # delete the original value (this way it works in Python 3.6+)
-            del self._original_local.__dict__[token]
-
-    return ContextVar
-
-
-def _get_contextvars() -> "Tuple[bool, type]":
-    """
-    Figure out the "right" contextvars installation to use. Returns a
-    `contextvars.ContextVar`-like class with a limited API.
-
-    See https://docs.sentry.io/platforms/python/contextvars/ for more information.
-    """
-    if not _is_contextvars_broken():
-        # aiocontextvars is a PyPI package that ensures that the contextvars
-        # backport (also a PyPI package) works with asyncio under Python 3.6
-        #
-        # Import it if available.
-        if sys.version_info < (3, 7):
-            # `aiocontextvars` is absolutely required for functional
-            # contextvars on Python 3.6.
-            try:
-                from aiocontextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-        else:
-            # On Python 3.7 contextvars are functional.
-            try:
-                from contextvars import ContextVar
-
-                return True, ContextVar
-            except ImportError:
-                pass
-
-    # Fall back to basic thread-local usage.
-
-    from threading import local
-
-    return False, _make_threadlocal_contextvars(local)
-
-
-HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
-
-CONTEXTVARS_ERROR_MESSAGE = """
-
-With asyncio/ASGI applications, the Sentry SDK requires a functional
-installation of `contextvars` to avoid leaking scope/context data across
-requests.
-
-Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
-"""
-
-_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
-
-# These exceptions won't set the span status to error if they occur. Use
-# register_control_flow_exception to add to this list
-_control_flow_exception_classes: "set[type]" = set()
-
-
-def is_internal_task() -> bool:
-    return _is_sentry_internal_task.get()
-
-
-@contextmanager
-def mark_sentry_task_internal() -> "Generator[None, None, None]":
-    """Context manager to mark a task as Sentry internal."""
-    token = _is_sentry_internal_task.set(True)
-    try:
-        yield
-    finally:
-        _is_sentry_internal_task.reset(token)
-
-
-def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
-    func_qualname: "Optional[str]" = None
-
-    prefix, suffix = "", ""
-
-    if isinstance(func, partial) and hasattr(func.func, "__name__"):
-        prefix, suffix = "partial()"
-        func = func.func
-    else:
-        # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
-        # https://github.com/python/cpython/pull/16600
-        partial_method = getattr(func, "_partialmethod", None) or getattr(
-            func, "__partialmethod__", None
-        )
-        if isinstance(partial_method, partialmethod):
-            prefix, suffix = "partialmethod()"
-            func = partial_method.func
-
-    if hasattr(func, "__qualname__"):
-        func_qualname = func.__qualname__
-    elif hasattr(func, "__name__"):
-        func_qualname = func.__name__
-
-    if func_qualname is not None:
-        if hasattr(func, "__module__") and isinstance(func.__module__, str):
-            func_qualname = func.__module__ + "." + func_qualname
-        func_qualname = prefix + func_qualname + suffix
-
-    return func_qualname
-
-
-def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
-    return qualname_from_function(func)
-
-
-disable_capture_event = ContextVar("disable_capture_event")
-
-
-class ServerlessTimeoutWarning(Exception):  # noqa: N818
-    """Raised when a serverless method is about to reach its timeout."""
-
-    pass
-
-
-class TimeoutThread(threading.Thread):
-    """Creates a Thread which runs (sleeps) for a time duration equal to
-    waiting_time and raises a custom ServerlessTimeout exception.
-    """
-
-    def __init__(
-        self,
-        waiting_time: float,
-        configured_timeout: int,
-        isolation_scope: "Optional[sentry_sdk.Scope]" = None,
-        current_scope: "Optional[sentry_sdk.Scope]" = None,
-    ) -> None:
-        threading.Thread.__init__(self)
-        self.waiting_time = waiting_time
-        self.configured_timeout = configured_timeout
-
-        self.isolation_scope = isolation_scope
-        self.current_scope = current_scope
-
-        self._stop_event = threading.Event()
-
-    def stop(self) -> None:
-        self._stop_event.set()
-
-    def _capture_exception(self) -> "ExcInfo":
-        exc_info = sys.exc_info()
-
-        client = sentry_sdk.get_client()
-        event, hint = event_from_exception(
-            exc_info,
-            client_options=client.options,
-            mechanism={"type": "threading", "handled": False},
-        )
-        sentry_sdk.capture_event(event, hint=hint)
-
-        return exc_info
-
-    def run(self) -> None:
-        self._stop_event.wait(self.waiting_time)
-
-        if self._stop_event.is_set():
-            return
-
-        integer_configured_timeout = int(self.configured_timeout)
-
-        # Setting up the exact integer value of configured time(in seconds)
-        if integer_configured_timeout < self.configured_timeout:
-            integer_configured_timeout = integer_configured_timeout + 1
-
-        # Raising Exception after timeout duration is reached
-        if self.isolation_scope is not None and self.current_scope is not None:
-            with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
-                with sentry_sdk.scope.use_scope(self.current_scope):
-                    try:
-                        raise ServerlessTimeoutWarning(
-                            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                                integer_configured_timeout
-                            )
-                        )
-                    except Exception:
-                        reraise(*self._capture_exception())
-
-        raise ServerlessTimeoutWarning(
-            "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
-                integer_configured_timeout
-            )
-        )
-
-
-def to_base64(original: str) -> "Optional[str]":
-    """
-    Convert a string to base64, via UTF-8. Returns None on invalid input.
-    """
-    base64_string = None
-
-    try:
-        utf8_bytes = original.encode("UTF-8")
-        base64_bytes = base64.b64encode(utf8_bytes)
-        base64_string = base64_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
-
-    return base64_string
-
-
-def from_base64(base64_string: str) -> "Optional[str]":
-    """
-    Convert a string from base64, via UTF-8. Returns None on invalid input.
-    """
-    utf8_string = None
-
-    try:
-        only_valid_chars = BASE64_ALPHABET.match(base64_string)
-        assert only_valid_chars
-
-        base64_bytes = base64_string.encode("UTF-8")
-        utf8_bytes = base64.b64decode(base64_bytes)
-        utf8_string = utf8_bytes.decode("UTF-8")
-    except Exception as err:
-        logger.warning(
-            "Unable to decode {b64} from base64:".format(b64=base64_string), err
-        )
-
-    return utf8_string
-
-
-Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
-
-
-def sanitize_url(
-    url: str,
-    remove_authority: bool = True,
-    remove_query_values: bool = True,
-    split: bool = False,
-) -> "Union[str, Components]":
-    """
-    Removes the authority and query parameter values from a given URL.
-    """
-    parsed_url = urlsplit(url)
-    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
-
-    # strip username:password (netloc can be usr:pwd@example.com)
-    if remove_authority:
-        netloc_parts = parsed_url.netloc.split("@")
-        if len(netloc_parts) > 1:
-            netloc = "%s:%s@%s" % (
-                SENSITIVE_DATA_SUBSTITUTE,
-                SENSITIVE_DATA_SUBSTITUTE,
-                netloc_parts[-1],
-            )
-        else:
-            netloc = parsed_url.netloc
-    else:
-        netloc = parsed_url.netloc
-
-    # strip values from query string
-    if remove_query_values:
-        query_string = unquote(
-            urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
-        )
-    else:
-        query_string = parsed_url.query
-
-    components = Components(
-        scheme=parsed_url.scheme,
-        netloc=netloc,
-        query=query_string,
-        path=parsed_url.path,
-        fragment=parsed_url.fragment,
-    )
-
-    if split:
-        return components
-    else:
-        return urlunsplit(components)
-
-
-ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
-
-
-def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
-    """
-    Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
-    parameters will be sanitized to remove sensitive data. The autority (username and password)
-    in the URL will always be removed.
-    """
-    parsed_url = sanitize_url(
-        url, remove_authority=True, remove_query_values=sanitize, split=True
-    )
-
-    base_url = urlunsplit(
-        Components(
-            scheme=parsed_url.scheme,  # type: ignore
-            netloc=parsed_url.netloc,  # type: ignore
-            query="",
-            path=parsed_url.path,  # type: ignore
-            fragment="",
-        )
-    )
-
-    return ParsedUrl(
-        url=base_url,
-        query=parsed_url.query,  # type: ignore
-        fragment=parsed_url.fragment,  # type: ignore
-    )
-
-
-def is_valid_sample_rate(rate: "Any", source: str) -> bool:
-    """
-    Checks the given sample rate to make sure it is valid type and value (a
-    boolean or a number between 0 and 1, inclusive).
-    """
-
-    # both booleans and NaN are instances of Real, so a) checking for Real
-    # checks for the possibility of a boolean also, and b) we have to check
-    # separately for NaN and Decimal does not derive from Real so need to check that too
-    if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
-                source=source, rate=rate, type=type(rate)
-            )
-        )
-        return False
-
-    # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
-    rate = float(rate)
-    if rate < 0 or rate > 1:
-        logger.warning(
-            "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
-                source=source, rate=rate
-            )
-        )
-        return False
-
-    return True
-
-
-def match_regex_list(
-    item: str,
-    regex_list: "Optional[List[str]]" = None,
-    substring_matching: bool = False,
-) -> bool:
-    if regex_list is None:
-        return False
-
-    for item_matcher in regex_list:
-        if not substring_matching and item_matcher[-1] != "$":
-            item_matcher += "$"
-
-        matched = re.search(item_matcher, item)
-        if matched:
-            return True
-
-    return False
-
-
-def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
-    """
-    Determines whether the given URL matches the Sentry DSN.
-    """
-    return (
-        client is not None
-        and client.transport is not None
-        and client.transport.parsed_dsn is not None
-        and client.transport.parsed_dsn.netloc in url
-    )
-
-
-def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
-    try:
-        from importlib import metadata
-
-        yielded = set()
-        for dist in metadata.distributions():
-            name = dist.metadata.get("Name", None)  # type: ignore[attr-defined]
-            # `metadata` values may be `None`, see:
-            # https://github.com/python/cpython/issues/91216
-            # and
-            # https://github.com/python/importlib_metadata/issues/371
-            if name is not None:
-                normalized_name = _normalize_module_name(name)
-                if dist.version is not None and normalized_name not in yielded:
-                    yield normalized_name, dist.version
-                    yielded.add(normalized_name)
-
-    except ImportError:
-        # < py3.8
-        try:
-            import pkg_resources
-        except ImportError:
-            return
-
-        for info in pkg_resources.working_set:
-            yield _normalize_module_name(info.key), info.version
-
-
-def _normalize_module_name(name: str) -> str:
-    return name.lower()
-
-
-def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
-    # https://peps.python.org/pep-0503/#normalized-names
-    return re.sub(r"[-_.]+", "-", name)
-
-
-def _get_installed_modules() -> "Dict[str, str]":
-    global _installed_modules
-    if _installed_modules is None:
-        _installed_modules = dict(_generate_installed_modules())
-    return _installed_modules
-
-
-def package_version(package: str) -> "Optional[Tuple[int, ...]]":
-    normalized_package = _normalize_module_name(
-        _replace_hyphens_dots_and_underscores_with_dashes(package)
-    )
-
-    installed_packages = {
-        _replace_hyphens_dots_and_underscores_with_dashes(module): v
-        for module, v in _get_installed_modules().items()
-    }
-    version = installed_packages.get(normalized_package)
-    if version is None:
-        return None
-
-    return parse_version(version)
-
-
-def reraise(
-    tp: "Optional[Type[BaseException]]",
-    value: "Optional[BaseException]",
-    tb: "Optional[Any]" = None,
-) -> "NoReturn":
-    assert value is not None
-    if value.__traceback__ is not tb:
-        raise value.with_traceback(tb)
-    raise value
-
-
-def _no_op(*_a: "Any", **_k: "Any") -> None:
-    """No-op function for ensure_integration_enabled."""
-    pass
-
-
-if TYPE_CHECKING:
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-        original_function: "Callable[P, R]",
-    ) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
-
-    @overload
-    def ensure_integration_enabled(
-        integration: "type[sentry_sdk.integrations.Integration]",
-    ) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
-
-
-def ensure_integration_enabled(
-    integration: "type[sentry_sdk.integrations.Integration]",
-    original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
-) -> "Callable[[Callable[P, R]], Callable[P, R]]":
-    """
-    Ensures a given integration is enabled prior to calling a Sentry-patched function.
-
-    The function takes as its parameters the integration that must be enabled and the original
-    function that the SDK is patching. The function returns a function that takes the
-    decorated (Sentry-patched) function as its parameter, and returns a function that, when
-    called, checks whether the given integration is enabled. If the integration is enabled, the
-    function calls the decorated, Sentry-patched function. If the integration is not enabled,
-    the original function is called.
-
-    The function also takes care of preserving the original function's signature and docstring.
-
-    Example usage:
-
-    ```python
-    @ensure_integration_enabled(MyIntegration, my_function)
-    def patch_my_function():
-        with sentry_sdk.start_transaction(...):
-            return my_function()
-    ```
-    """
-    if TYPE_CHECKING:
-        # Type hint to ensure the default function has the right typing. The overloads
-        # ensure the default _no_op function is only used when R is None.
-        original_function = cast(Callable[P, R], original_function)
-
-    def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
-        def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
-            if sentry_sdk.get_client().get_integration(integration) is None:
-                return original_function(*args, **kwargs)
-
-            return sentry_patched_function(*args, **kwargs)
-
-        if original_function is _no_op:
-            return wraps(sentry_patched_function)(runner)
-
-        return wraps(original_function)(runner)
-
-    return patcher
-
-
-if PY37:
-
-    def nanosecond_time() -> int:
-        return time.perf_counter_ns()
-
-else:
-
-    def nanosecond_time() -> int:
-        return int(time.perf_counter() * 1e9)
-
-
-def now() -> float:
-    return time.perf_counter()
-
-
-try:
-    from gevent import get_hub as get_gevent_hub
-    from gevent.monkey import is_module_patched
-except ImportError:
-    # it's not great that the signatures are different, get_hub can't return None
-    # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
-    def get_gevent_hub() -> "Optional[Hub]":  # type: ignore[misc]
-        return None
-
-    def is_module_patched(mod_name: str) -> bool:
-        # unable to import from gevent means no modules have been patched
-        return False
-
-
-def is_gevent() -> bool:
-    return is_module_patched("threading") or is_module_patched("_thread")
-
-
-def get_current_thread_meta(
-    thread: "Optional[threading.Thread]" = None,
-) -> "Tuple[Optional[int], Optional[str]]":
-    """
-    Try to get the id of the current thread, with various fall backs.
-    """
-
-    # if a thread is specified, that takes priority
-    if thread is not None:
-        try:
-            thread_id = thread.ident
-            thread_name = thread.name
-            if thread_id is not None:
-                return thread_id, thread_name
-        except AttributeError:
-            pass
-
-    # if the app is using gevent, we should look at the gevent hub first
-    # as the id there differs from what the threading module reports
-    if is_gevent():
-        gevent_hub = get_gevent_hub()
-        if gevent_hub is not None:
-            try:
-                # this is undocumented, so wrap it in try except to be safe
-                return gevent_hub.thread_ident, None
-            except AttributeError:
-                pass
-
-    # use the current thread's id if possible
-    try:
-        thread = threading.current_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # if we can't get the current thread id, fall back to the main thread id
-    try:
-        thread = threading.main_thread()
-        thread_id = thread.ident
-        thread_name = thread.name
-        if thread_id is not None:
-            return thread_id, thread_name
-    except AttributeError:
-        pass
-
-    # we've tried everything, time to give up
-    return None, None
-
-
-def _register_control_flow_exception(
-    exc_type: "Union[type, list[type], tuple[type], set[type]]",
-) -> None:
-    if isinstance(exc_type, (list, tuple, set)):
-        _control_flow_exception_classes.update(exc_type)
-    else:
-        _control_flow_exception_classes.add(exc_type)
-
-
-def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
-    if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
-        # https://docs.python.org/3/library/exceptions.html#SystemExit
-        return False
-
-    if issubclass(ty, tuple(_control_flow_exception_classes)):
-        return False
-
-    return True
-
-
-if TYPE_CHECKING:
-    T = TypeVar("T")
-
-
-def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
-    """
-    Attempt to convert from an unknown type to a specific type, using the
-    given function. Return None if the conversion fails, i.e. if the function
-    raises an exception.
-    """
-    try:
-        if isinstance(value, convert_func):  # type: ignore
-            return value
-    except TypeError:
-        pass
-
-    try:
-        return convert_func(value)
-    except Exception:
-        return None
-
-
-def safe_serialize(data: "Any") -> str:
-    """Safely serialize to a readable string."""
-
-    def serialize_item(
-        item: "Any",
-    ) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
-        if callable(item):
-            try:
-                module = getattr(item, "__module__", None)
-                qualname = getattr(item, "__qualname__", None)
-                name = getattr(item, "__name__", "anonymous")
-
-                if module and qualname:
-                    full_path = f"{module}.{qualname}"
-                elif module and name:
-                    full_path = f"{module}.{name}"
-                else:
-                    full_path = name
-
-                return f""
-            except Exception:
-                return f""
-        elif isinstance(item, dict):
-            return {k: serialize_item(v) for k, v in item.items()}
-        elif isinstance(item, (list, tuple)):
-            return [serialize_item(x) for x in item]
-        elif hasattr(item, "__dict__"):
-            try:
-                attrs = {
-                    k: serialize_item(v)
-                    for k, v in vars(item).items()
-                    if not k.startswith("_")
-                }
-                return f"<{type(item).__name__} {attrs}>"
-            except Exception:
-                return repr(item)
-        else:
-            return item
-
-    try:
-        serialized = serialize_item(data)
-        return (
-            json.dumps(serialized, default=str)
-            if not isinstance(serialized, str)
-            else serialized
-        )
-    except Exception:
-        return str(data)
-
-
-def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(
-        options.get("enable_logs", False)
-        or options["_experiments"].get("enable_logs", False)
-    )
-
-
-def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return "data_collection" in options.get("_experiments", {})
-
-
-def get_before_send_log(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_log") or options["_experiments"].get(
-        "before_send_log"
-    )
-
-
-def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
-    if options is None:
-        return False
-
-    return bool(options.get("enable_metrics", True))
-
-
-def get_before_send_metric(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
-    if options is None:
-        return None
-
-    return options.get("before_send_metric") or options["_experiments"].get(
-        "before_send_metric"
-    )
-
-
-def get_before_send_span(
-    options: "Optional[dict[str, Any]]",
-) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
-    if options is None:
-        return None
-
-    return options["_experiments"].get("before_send_span")
-
-
-def format_attribute(val: "Any") -> "AttributeValue":
-    """
-    Turn unsupported attribute value types into an AttributeValue.
-
-    We do this as soon as a user-provided attribute is set, to prevent spans,
-    logs, metrics and similar from having live references to various objects.
-
-    Note: This is not the final attribute value format. Before they're sent,
-    they're serialized further into the actual format the protocol expects:
-    https://develop.sentry.dev/sdk/telemetry/attributes/
-    """
-    if isinstance(val, (bool, int, float, str)):
-        return val
-
-    if isinstance(val, (list, tuple)) and not val:
-        return []
-    elif isinstance(val, list):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return copy.deepcopy(val)
-    elif isinstance(val, tuple):
-        ty = type(val[0])
-        if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
-            return list(val)
-
-    return safe_repr(val)
-
-
-def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
-    """Serialize attribute value to the transport format."""
-    if isinstance(val, bool):
-        return {"value": val, "type": "boolean"}
-    if isinstance(val, int):
-        return {"value": val, "type": "integer"}
-    if isinstance(val, float):
-        return {"value": val, "type": "double"}
-    if isinstance(val, str):
-        return {"value": val, "type": "string"}
-
-    if isinstance(val, list):
-        if not val:
-            return {"value": [], "type": "array"}
-
-        # Only lists of elements of a single type are supported
-        ty = type(val[0])
-        if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
-            return {"value": val, "type": "array"}
-
-    # Coerce to string if we don't know what to do with the value. This should
-    # never happen as we pre-format early in format_attribute, but let's be safe.
-    return {"value": safe_repr(val), "type": "string"}
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py
deleted file mode 100644
index 5eb9b23130..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/sentry_sdk/worker.py
+++ /dev/null
@@ -1,308 +0,0 @@
-import asyncio
-import os
-import threading
-from abc import ABC, abstractmethod
-from time import sleep, time
-from typing import TYPE_CHECKING
-
-from sentry_sdk._queue import FullError, Queue
-from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
-from sentry_sdk.utils import logger, mark_sentry_task_internal
-
-if TYPE_CHECKING:
-    from typing import Any, Callable, Optional
-
-
-_TERMINATOR = object()
-
-
-class Worker(ABC):
-    """Base class for all workers."""
-
-    @property
-    @abstractmethod
-    def is_alive(self) -> bool:
-        """Whether the worker is alive and running."""
-        pass
-
-    @abstractmethod
-    def kill(self) -> None:
-        """Kill the worker. It will not process any more events."""
-        pass
-
-    def flush(
-        self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
-    ) -> None:
-        """Flush the worker, blocking until done or timeout is reached."""
-        return None
-
-    @abstractmethod
-    def full(self) -> bool:
-        """Whether the worker's queue is full."""
-        pass
-
-    @abstractmethod
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        """Schedule a callback. Returns True if queued, False if full."""
-        pass
-
-
-class BackgroundWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Queue" = Queue(queue_size)
-        self._lock = threading.Lock()
-        self._thread: "Optional[threading.Thread]" = None
-        self._thread_for_pid: "Optional[int]" = None
-
-    @property
-    def is_alive(self) -> bool:
-        if self._thread_for_pid != os.getpid():
-            return False
-        if not self._thread:
-            return False
-        return self._thread.is_alive()
-
-    def _ensure_thread(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    def _timed_queue_join(self, timeout: float) -> bool:
-        deadline = time() + timeout
-        queue = self._queue
-
-        queue.all_tasks_done.acquire()
-
-        try:
-            while queue.unfinished_tasks:
-                delay = deadline - time()
-                if delay <= 0:
-                    return False
-                queue.all_tasks_done.wait(timeout=delay)
-
-            return True
-        finally:
-            queue.all_tasks_done.release()
-
-    def start(self) -> None:
-        with self._lock:
-            if not self.is_alive:
-                self._thread = threading.Thread(
-                    target=self._target, name="sentry-sdk.BackgroundWorker"
-                )
-                self._thread.daemon = True
-                try:
-                    self._thread.start()
-                    self._thread_for_pid = os.getpid()
-                except RuntimeError:
-                    # At this point we can no longer start because the interpreter
-                    # is already shutting down.  Sadly at this point we can no longer
-                    # send out events.
-                    self._thread = None
-
-    def kill(self) -> None:
-        """
-        Kill worker thread. Returns immediately. Not useful for
-        waiting on shutdown for events, use `flush` for that.
-        """
-        logger.debug("background worker got kill request")
-        with self._lock:
-            if self._thread:
-                try:
-                    self._queue.put_nowait(_TERMINATOR)
-                except FullError:
-                    logger.debug("background worker queue full, kill failed")
-
-                self._thread = None
-                self._thread_for_pid = None
-
-    def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
-        logger.debug("background worker got flush request")
-        with self._lock:
-            if self.is_alive and timeout > 0.0:
-                self._wait_flush(timeout, callback)
-        logger.debug("background worker flushed")
-
-    def full(self) -> bool:
-        return self._queue.full()
-
-    def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
-        initial_timeout = min(0.1, timeout)
-        if not self._timed_queue_join(initial_timeout):
-            pending = self._queue.qsize() + 1
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            if not self._timed_queue_join(timeout - initial_timeout):
-                pending = self._queue.qsize() + 1
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_thread()
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except FullError:
-            return False
-
-    def _target(self) -> None:
-        while True:
-            callback = self._queue.get()
-            try:
-                if callback is _TERMINATOR:
-                    break
-                try:
-                    callback()
-                except Exception:
-                    logger.error("Failed processing job", exc_info=True)
-            finally:
-                self._queue.task_done()
-            sleep(0)
-
-
-class AsyncWorker(Worker):
-    def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
-        self._queue: "Optional[asyncio.Queue[Any]]" = None
-        self._queue_size = queue_size
-        self._task: "Optional[asyncio.Task[None]]" = None
-        # Event loop needs to remain in the same process
-        self._task_for_pid: "Optional[int]" = None
-        self._loop: "Optional[asyncio.AbstractEventLoop]" = None
-        # Track active callback tasks so they have a strong reference and can be cancelled on kill
-        self._active_tasks: "set[asyncio.Task[None]]" = set()
-
-    @property
-    def is_alive(self) -> bool:
-        if self._task_for_pid != os.getpid():
-            return False
-        if not self._task or not self._loop:
-            return False
-        return self._loop.is_running() and not self._task.done()
-
-    def kill(self) -> None:
-        if self._task:
-            # Cancel the main consumer task to prevent duplicate consumers
-            self._task.cancel()
-            # Also cancel any active callback tasks
-            # Avoid modifying the set while cancelling tasks
-            tasks_to_cancel = set(self._active_tasks)
-            for task in tasks_to_cancel:
-                task.cancel()
-            self._active_tasks.clear()
-            self._loop = None
-            self._task = None
-            self._task_for_pid = None
-
-    def start(self) -> None:
-        if not self.is_alive:
-            try:
-                self._loop = asyncio.get_running_loop()
-                # Always create a fresh queue on start to avoid stale items
-                self._queue = asyncio.Queue(maxsize=self._queue_size)
-                with mark_sentry_task_internal():
-                    self._task = self._loop.create_task(self._target())
-                self._task_for_pid = os.getpid()
-            except RuntimeError:
-                # There is no event loop running
-                logger.warning("No event loop running, async worker not started")
-                self._loop = None
-                self._task = None
-                self._task_for_pid = None
-
-    def full(self) -> bool:
-        if self._queue is None:
-            return True
-        return self._queue.full()
-
-    def _ensure_task(self) -> None:
-        if not self.is_alive:
-            self.start()
-
-    async def _wait_flush(
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> None:
-        if not self._loop or not self._loop.is_running() or self._queue is None:
-            return
-
-        initial_timeout = min(0.1, timeout)
-
-        # Timeout on the join
-        try:
-            await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
-        except asyncio.TimeoutError:
-            pending = self._queue.qsize() + len(self._active_tasks)
-            logger.debug("%d event(s) pending on flush", pending)
-            if callback is not None:
-                callback(pending, timeout)
-
-            try:
-                remaining_timeout = timeout - initial_timeout
-                await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
-            except asyncio.TimeoutError:
-                pending = self._queue.qsize() + len(self._active_tasks)
-                logger.error("flush timed out, dropped %s events", pending)
-
-    def flush(  # type: ignore[override]
-        self, timeout: float, callback: "Optional[Any]" = None
-    ) -> "Optional[asyncio.Task[None]]":
-        if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
-            with mark_sentry_task_internal():
-                return self._loop.create_task(self._wait_flush(timeout, callback))
-        return None
-
-    def submit(self, callback: "Callable[[], Any]") -> bool:
-        self._ensure_task()
-        if self._queue is None:
-            return False
-        try:
-            self._queue.put_nowait(callback)
-            return True
-        except asyncio.QueueFull:
-            return False
-
-    async def _target(self) -> None:
-        if self._queue is None:
-            return
-        try:
-            while True:
-                callback = await self._queue.get()
-                if callback is _TERMINATOR:
-                    self._queue.task_done()
-                    break
-                # Firing tasks instead of awaiting them allows for concurrent requests
-                with mark_sentry_task_internal():
-                    task = asyncio.create_task(self._process_callback(callback))
-                # Create a strong reference to the task so it can be cancelled on kill
-                # and does not get garbage collected while running
-                self._active_tasks.add(task)
-                # Capture queue ref at dispatch time so done callbacks use the
-                # correct queue even if kill()/start() replace self._queue.
-                queue_ref = self._queue
-                task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
-                # Yield to let the event loop run other tasks
-                await asyncio.sleep(0)
-        except asyncio.CancelledError:
-            pass  # Expected during kill()
-
-    async def _process_callback(self, callback: "Callable[[], Any]") -> None:
-        # Callback is an async coroutine, need to await it
-        await callback()
-
-    def _on_task_complete(
-        self,
-        task: "asyncio.Task[None]",
-        queue: "Optional[asyncio.Queue[Any]]" = None,
-    ) -> None:
-        try:
-            task.result()
-        except asyncio.CancelledError:
-            pass  # Task was cancelled, expected during shutdown
-        except Exception:
-            logger.error("Failed processing job", exc_info=True)
-        finally:
-            # Mark the task as done and remove it from the active tasks set
-            # Use the queue reference captured at dispatch time, not self._queue,
-            # to avoid calling task_done() on a different queue after kill()/start().
-            if queue is not None:
-                queue.task_done()
-            self._active_tasks.discard(task)
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER
deleted file mode 100644
index 5c69047b2e..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/INSTALLER
+++ /dev/null
@@ -1 +0,0 @@
-uv
\ No newline at end of file
diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA
deleted file mode 100644
index 9c7a4703f8..0000000000
--- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/METADATA
+++ /dev/null
@@ -1,163 +0,0 @@
-Metadata-Version: 2.4
-Name: urllib3
-Version: 2.7.0
-Summary: HTTP library with thread-safe connection pooling, file post, and more.
-Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
-Project-URL: Documentation, https://urllib3.readthedocs.io
-Project-URL: Code, https://github.com/urllib3/urllib3
-Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
-Author-email: Andrey Petrov 
-Maintainer-email: Seth Michael Larson , Quentin Pradet , Illia Volochii 
-License-Expression: MIT
-License-File: LICENSE.txt
-Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: 3.14
-Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.10
-Provides-Extra: brotli
-Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
-Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
-Provides-Extra: h2
-Requires-Dist: h2<5,>=4; extra == 'h2'
-Provides-Extra: socks
-Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
-Provides-Extra: zstd
-Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
-Description-Content-Type: text/markdown
-
-

- -![urllib3](https://github.com/urllib3/urllib3/raw/main/docs/_static/banner_github.svg) - -

- -

- PyPI Version - Python Versions - Join our Discord - Coverage Status - Build Status on GitHub - Documentation Status
- OpenSSF Scorecard - SLSA 3 - CII Best Practices -

- -urllib3 is a powerful, *user-friendly* HTTP client for Python. -urllib3 brings many critical features that are missing from the Python -standard libraries: - -- Thread safety. -- Connection pooling. -- Client-side SSL/TLS verification. -- File uploads with multipart encoding. -- Helpers for retrying requests and dealing with HTTP redirects. -- Support for gzip, deflate, brotli, and zstd encoding. -- Proxy support for HTTP and SOCKS. -- 100% test coverage. - -... and many more features, but most importantly: Our maintainers have a 15+ -year track record of maintaining urllib3 with the highest code standards and -attention to security and safety. - -[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses) -and you should too. - - -## Installing - -urllib3 can be installed with [pip](https://pip.pypa.io): - -```bash -$ python -m pip install urllib3 -``` - -Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3): - -```bash -$ git clone https://github.com/urllib3/urllib3.git -$ cd urllib3 -$ pip install . -``` - -## Getting Started - -urllib3 is easy to use: - -```python3 ->>> import urllib3 ->>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt") ->>> resp.status -200 ->>> resp.data -b"User-agent: *\nDisallow: /deny\n" -``` - -urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io). - - -## Community - -urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and -collaborating with other contributors. Drop by and say hello 👋 - - -## Contributing - -urllib3 happily accepts contributions. Please see our -[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html) -for some tips on getting started. - - -## Security Disclosures - -To report a security vulnerability, please use the -[Tidelift security contact](https://tidelift.com/security). -Tidelift will coordinate the fix and disclosure with maintainers. - - -## Maintainers - -Meet our maintainers since 2008: - -- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii) -- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson) -- [@pquentin](https://github.com/pquentin) (Quentin Pradet) -- [@theacodes](https://github.com/theacodes) (Thea Flowers) -- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro) -- [@lukasa](https://github.com/lukasa) (Cory Benfield) -- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco) -- [@shazow](https://github.com/shazow) (Andrey Petrov) - -👋 - - -## Sponsorship - -If your company benefits from this library, please consider [sponsoring its -development](https://urllib3.readthedocs.io/en/latest/sponsors.html). - - -## For Enterprise - -Professional support for urllib3 is available as part of the [Tidelift -Subscription][1]. Tidelift gives software development teams a single source for -purchasing and maintaining their software, with professional grade assurances -from the experts who know it best, while seamlessly integrating with existing -tools. - -[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD deleted file mode 100644 index 8c4f9c0b4b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/RECORD +++ /dev/null @@ -1,44 +0,0 @@ -urllib3-2.7.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 -urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852 -urllib3-2.7.0.dist-info/RECORD,, -urllib3-2.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 -urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093 -urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979 -urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580 -urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522 -urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931 -urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520 -urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786 -urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164 -urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836 -urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960 -urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677 -urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520 -urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566 -urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719 -urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734 -urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639 -urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945 -urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801 -urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388 -urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741 -urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578 -urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014 -urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895 -urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93 -urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219 -urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001 -urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444 -urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148 -urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363 -urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374 -urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577 -urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742 -urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479 -urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847 -urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346 -urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205 -urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146 -urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/REQUESTED b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL deleted file mode 100644 index b1b94fd58e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.29.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt deleted file mode 100644 index e6183d0276..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3-2.7.0.dist-info/licenses/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2008-2020 Andrey Petrov and contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py deleted file mode 100644 index 3fe782c8a4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/__init__.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more -""" - -from __future__ import annotations - -# Set default logging handler to avoid "No handler found" warnings. -import logging -import sys -import typing -import warnings -from logging import NullHandler - -from . import exceptions -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._version import __version__ -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .poolmanager import PoolManager, ProxyManager, proxy_from_url -from .response import BaseHTTPResponse, HTTPResponse -from .util.request import make_headers -from .util.retry import Retry -from .util.timeout import Timeout - -# Ensure that Python is compiled with OpenSSL 1.1.1+ -# If the 'ssl' module isn't available at all that's -# fine, we only care if the module is available. -try: - import ssl -except ImportError: - pass -else: - if not ssl.OPENSSL_VERSION.startswith("OpenSSL "): # Defensive: - warnings.warn( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/3020", - exceptions.NotOpenSSLWarning, - ) - elif ssl.OPENSSL_VERSION_INFO < (1, 1, 1): # Defensive: - raise ImportError( - "urllib3 v2 only supports OpenSSL 1.1.1+, currently " - f"the 'ssl' module is compiled with {ssl.OPENSSL_VERSION!r}. " - "See: https://github.com/urllib3/urllib3/issues/2168" - ) - -__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" -__license__ = "MIT" -__version__ = __version__ - -__all__ = ( - "HTTPConnectionPool", - "HTTPHeaderDict", - "HTTPSConnectionPool", - "PoolManager", - "ProxyManager", - "HTTPResponse", - "Retry", - "Timeout", - "add_stderr_logger", - "connection_from_url", - "disable_warnings", - "encode_multipart_formdata", - "make_headers", - "proxy_from_url", - "request", - "BaseHTTPResponse", -) - -logging.getLogger(__name__).addHandler(NullHandler()) - - -def add_stderr_logger( - level: int = logging.DEBUG, -) -> logging.StreamHandler[typing.TextIO]: - """ - Helper for quickly adding a StreamHandler to the logger. Useful for - debugging. - - Returns the handler after adding it. - """ - # This method needs to be in this __init__.py to get the __name__ correct - # even if urllib3 is vendored within another package. - logger = logging.getLogger(__name__) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) - logger.addHandler(handler) - logger.setLevel(level) - logger.debug("Added a stderr logging handler to logger: %s", __name__) - return handler - - -# ... Clean up. -del NullHandler - - -# All warning filters *must* be appended unless you're really certain that they -# shouldn't be: otherwise, it's very hard for users to use most Python -# mechanisms to silence them. -# SecurityWarning's always go off by default. -warnings.simplefilter("always", exceptions.SecurityWarning, append=True) -# InsecurePlatformWarning's don't vary between requests, so we keep it default. -warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) - - -def disable_warnings(category: type[Warning] = exceptions.HTTPWarning) -> None: - """ - Helper for quickly disabling all urllib3 warnings. - """ - warnings.simplefilter("ignore", category) - - -_DEFAULT_POOL = PoolManager() - - -def request( - method: str, - url: str, - *, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - preload_content: bool | None = True, - decode_content: bool | None = True, - redirect: bool | None = True, - retries: Retry | bool | int | None = None, - timeout: Timeout | float | int | None = 3, - json: typing.Any | None = None, -) -> BaseHTTPResponse: - """ - A convenience, top-level request method. It uses a module-global ``PoolManager`` instance. - Therefore, its side effects could be shared across dependencies relying on it. - To avoid side effects create a new ``PoolManager`` instance and use it instead. - The method does not accept low-level ``**urlopen_kw`` keyword arguments. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - - return _DEFAULT_POOL.request( - method, - url, - body=body, - fields=fields, - headers=headers, - preload_content=preload_content, - decode_content=decode_content, - redirect=redirect, - retries=retries, - timeout=timeout, - json=json, - ) - - -if sys.platform == "emscripten": - from .contrib.emscripten import inject_into_urllib3 # noqa: 401 - - inject_into_urllib3() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py deleted file mode 100644 index 992ec1657a..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_base_connection.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -import typing - -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from .util.url import Url - -_TYPE_BODY = typing.Union[ - bytes, typing.IO[typing.Any], typing.Iterable[bytes | str], str -] - - -class ProxyConfig(typing.NamedTuple): - ssl_context: ssl.SSLContext | None - use_forwarding_for_https: bool - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - - -class _ResponseOptions(typing.NamedTuple): - # TODO: Remove this in favor of a better - # HTTP request/response lifecycle tracking. - request_method: str - request_url: str - preload_content: bool - decode_content: bool - enforce_content_length: bool - - -if typing.TYPE_CHECKING: - import ssl - from typing import Protocol - - from .response import BaseHTTPResponse - - class BaseHTTPConnection(Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - host: str - port: int - timeout: None | ( - float - ) # Instance doesn't store _DEFAULT_TIMEOUT, must be resolved. - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool - proxy_is_verified: bool | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: ... - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: ... - - def connect(self) -> None: ... - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: ... - - def getresponse(self) -> BaseHTTPResponse: ... - - def close(self) -> None: ... - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - - class BaseHTTPSConnection(BaseHTTPConnection, Protocol): - default_port: typing.ClassVar[int] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - # Certificate verification methods - cert_reqs: int | str | None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None - ssl_context: ssl.SSLContext | None - - # Trusted CAs - ca_certs: str | None - ca_cert_dir: str | None - ca_cert_data: None | str | bytes - - # TLS version - ssl_minimum_version: int | None - ssl_maximum_version: int | None - ssl_version: int | str | None # Deprecated - - # Client certificates - cert_file: str | None - key_file: str | None - key_password: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: _TYPE_SOCKET_OPTIONS | None = ..., - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: ... diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py deleted file mode 100644 index ee9ca662b6..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_collections.py +++ /dev/null @@ -1,486 +0,0 @@ -from __future__ import annotations - -import typing -from collections import OrderedDict -from enum import Enum, auto -from threading import RLock - -if typing.TYPE_CHECKING: - # We can only import Protocol if TYPE_CHECKING because it's a development - # dependency, and is not available at runtime. - from typing import Protocol - - from typing_extensions import Self - - class HasGettableStringKeys(Protocol): - def keys(self) -> typing.Iterator[str]: ... - - def __getitem__(self, key: str) -> str: ... - - -__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] - - -# Key type -_KT = typing.TypeVar("_KT") -# Value type -_VT = typing.TypeVar("_VT") -# Default type -_DT = typing.TypeVar("_DT") - -ValidHTTPHeaderSource = typing.Union[ - "HTTPHeaderDict", - typing.Mapping[str, str], - typing.Iterable[tuple[str, str]], - "HasGettableStringKeys", -] - - -class _Sentinel(Enum): - not_passed = auto() - - -def ensure_can_construct_http_header_dict( - potential: object, -) -> ValidHTTPHeaderSource | None: - if isinstance(potential, HTTPHeaderDict): - return potential - elif isinstance(potential, typing.Mapping): - # Full runtime checking of the contents of a Mapping is expensive, so for the - # purposes of typechecking, we assume that any Mapping is the right shape. - return typing.cast(typing.Mapping[str, str], potential) - elif isinstance(potential, typing.Iterable): - # Similarly to Mapping, full runtime checking of the contents of an Iterable is - # expensive, so for the purposes of typechecking, we assume that any Iterable - # is the right shape. - return typing.cast(typing.Iterable[tuple[str, str]], potential) - elif hasattr(potential, "keys") and hasattr(potential, "__getitem__"): - return typing.cast("HasGettableStringKeys", potential) - else: - return None - - -class RecentlyUsedContainer(typing.Generic[_KT, _VT], typing.MutableMapping[_KT, _VT]): - """ - Provides a thread-safe dict-like container which maintains up to - ``maxsize`` keys while throwing away the least-recently-used keys beyond - ``maxsize``. - - :param maxsize: - Maximum number of recent elements to retain. - - :param dispose_func: - Every time an item is evicted from the container, - ``dispose_func(value)`` is called. Callback which will get called - """ - - _container: typing.OrderedDict[_KT, _VT] - _maxsize: int - dispose_func: typing.Callable[[_VT], None] | None - lock: RLock - - def __init__( - self, - maxsize: int = 10, - dispose_func: typing.Callable[[_VT], None] | None = None, - ) -> None: - super().__init__() - self._maxsize = maxsize - self.dispose_func = dispose_func - self._container = OrderedDict() - self.lock = RLock() - - def __getitem__(self, key: _KT) -> _VT: - # Re-insert the item, moving it to the end of the eviction line. - with self.lock: - item = self._container.pop(key) - self._container[key] = item - return item - - def __setitem__(self, key: _KT, value: _VT) -> None: - evicted_item = None - with self.lock: - # Possibly evict the existing value of 'key' - try: - # If the key exists, we'll overwrite it, which won't change the - # size of the pool. Because accessing a key should move it to - # the end of the eviction line, we pop it out first. - evicted_item = key, self._container.pop(key) - self._container[key] = value - except KeyError: - # When the key does not exist, we insert the value first so that - # evicting works in all cases, including when self._maxsize is 0 - self._container[key] = value - if len(self._container) > self._maxsize: - # If we didn't evict an existing value, and we've hit our maximum - # size, then we have to evict the least recently used item from - # the beginning of the container. - evicted_item = self._container.popitem(last=False) - - # After releasing the lock on the pool, dispose of any evicted value. - if evicted_item is not None and self.dispose_func: - _, evicted_value = evicted_item - self.dispose_func(evicted_value) - - def __delitem__(self, key: _KT) -> None: - with self.lock: - value = self._container.pop(key) - - if self.dispose_func: - self.dispose_func(value) - - def __len__(self) -> int: - with self.lock: - return len(self._container) - - def __iter__(self) -> typing.NoReturn: - raise NotImplementedError( - "Iteration over this class is unlikely to be threadsafe." - ) - - def clear(self) -> None: - with self.lock: - # Copy pointers to all values, then wipe the mapping - values = list(self._container.values()) - self._container.clear() - - if self.dispose_func: - for value in values: - self.dispose_func(value) - - def keys(self) -> set[_KT]: # type: ignore[override] - with self.lock: - return set(self._container.keys()) - - -class HTTPHeaderDictItemView(set[tuple[str, str]]): - """ - HTTPHeaderDict is unusual for a Mapping[str, str] in that it has two modes of - address. - - If we directly try to get an item with a particular name, we will get a string - back that is the concatenated version of all the values: - - >>> d['X-Header-Name'] - 'Value1, Value2, Value3' - - However, if we iterate over an HTTPHeaderDict's items, we will optionally combine - these values based on whether combine=True was called when building up the dictionary - - >>> d = HTTPHeaderDict({"A": "1", "B": "foo"}) - >>> d.add("A", "2", combine=True) - >>> d.add("B", "bar") - >>> list(d.items()) - [ - ('A', '1, 2'), - ('B', 'foo'), - ('B', 'bar'), - ] - - This class conforms to the interface required by the MutableMapping ABC while - also giving us the nonstandard iteration behavior we want; items with duplicate - keys, ordered by time of first insertion. - """ - - _headers: HTTPHeaderDict - - def __init__(self, headers: HTTPHeaderDict) -> None: - self._headers = headers - - def __len__(self) -> int: - return len(list(self._headers.iteritems())) - - def __iter__(self) -> typing.Iterator[tuple[str, str]]: - return self._headers.iteritems() - - def __contains__(self, item: object) -> bool: - if isinstance(item, tuple) and len(item) == 2: - passed_key, passed_val = item - if isinstance(passed_key, str) and isinstance(passed_val, str): - return self._headers._has_value_for_header(passed_key, passed_val) - return False - - -class HTTPHeaderDict(typing.MutableMapping[str, str]): - """ - :param headers: - An iterable of field-value pairs. Must not contain multiple field names - when compared case-insensitively. - - :param kwargs: - Additional field-value pairs to pass in to ``dict.update``. - - A ``dict`` like container for storing HTTP Headers. - - Field names are stored and compared case-insensitively in compliance with - RFC 7230. Iteration provides the first case-sensitive key seen for each - case-insensitive pair. - - Using ``__setitem__`` syntax overwrites fields that compare equal - case-insensitively in order to maintain ``dict``'s api. For fields that - compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` - in a loop. - - If multiple fields that are equal case-insensitively are passed to the - constructor or ``.update``, the behavior is undefined and some will be - lost. - - >>> headers = HTTPHeaderDict() - >>> headers.add('Set-Cookie', 'foo=bar') - >>> headers.add('set-cookie', 'baz=quxx') - >>> headers['content-length'] = '7' - >>> headers['SET-cookie'] - 'foo=bar, baz=quxx' - >>> headers['Content-Length'] - '7' - """ - - _container: typing.MutableMapping[str, list[str]] - - def __init__(self, headers: ValidHTTPHeaderSource | None = None, **kwargs: str): - super().__init__() - self._container = {} # 'dict' is insert-ordered - if headers is not None: - if isinstance(headers, HTTPHeaderDict): - self._copy_from(headers) - else: - self.extend(headers) - if kwargs: - self.extend(kwargs) - - def __setitem__(self, key: str, val: str) -> None: - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - self._container[key.lower()] = [key, val] - - def __getitem__(self, key: str) -> str: - if isinstance(key, bytes): - key = key.decode("latin-1") - val = self._container[key.lower()] - return ", ".join(val[1:]) - - def __delitem__(self, key: str) -> None: - if isinstance(key, bytes): - key = key.decode("latin-1") - del self._container[key.lower()] - - def __contains__(self, key: object) -> bool: - if isinstance(key, bytes): - key = key.decode("latin-1") - if isinstance(key, str): - return key.lower() in self._container - return False - - def setdefault(self, key: str, default: str = "") -> str: - return super().setdefault(key, default) - - def __eq__(self, other: object) -> bool: - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return False - else: - other_as_http_header_dict = type(self)(maybe_constructable) - - return {k.lower(): v for k, v in self.itermerged()} == { - k.lower(): v for k, v in other_as_http_header_dict.itermerged() - } - - def __ne__(self, other: object) -> bool: - return not self.__eq__(other) - - def __len__(self) -> int: - return len(self._container) - - def __iter__(self) -> typing.Iterator[str]: - # Only provide the originally cased names - for vals in self._container.values(): - yield vals[0] - - def discard(self, key: str) -> None: - try: - del self[key] - except KeyError: - pass - - def add(self, key: str, val: str, *, combine: bool = False) -> None: - """Adds a (name, value) pair, doesn't overwrite the value if it already - exists. - - If this is called with combine=True, instead of adding a new header value - as a distinct item during iteration, this will instead append the value to - any existing header value with a comma. If no existing header value exists - for the key, then the value will simply be added, ignoring the combine parameter. - - >>> headers = HTTPHeaderDict(foo='bar') - >>> headers.add('Foo', 'baz') - >>> headers['foo'] - 'bar, baz' - >>> list(headers.items()) - [('foo', 'bar'), ('foo', 'baz')] - >>> headers.add('foo', 'quz', combine=True) - >>> list(headers.items()) - [('foo', 'bar, baz, quz')] - """ - # avoid a bytes/str comparison by decoding before httplib - if isinstance(key, bytes): - key = key.decode("latin-1") - key_lower = key.lower() - new_vals = [key, val] - # Keep the common case aka no item present as fast as possible - vals = self._container.setdefault(key_lower, new_vals) - if new_vals is not vals: - # if there are values here, then there is at least the initial - # key/value pair - assert len(vals) >= 2 - if combine: - vals[-1] = vals[-1] + ", " + val - else: - vals.append(val) - - def extend(self, *args: ValidHTTPHeaderSource, **kwargs: str) -> None: - """Generic import function for any type of header-like object. - Adapted version of MutableMapping.update in order to insert items - with self.add instead of self.__setitem__ - """ - if len(args) > 1: - raise TypeError( - f"extend() takes at most 1 positional arguments ({len(args)} given)" - ) - other = args[0] if len(args) >= 1 else () - - if isinstance(other, HTTPHeaderDict): - for key, val in other.iteritems(): - self.add(key, val) - elif isinstance(other, typing.Mapping): - for key, val in other.items(): - self.add(key, val) - elif isinstance(other, typing.Iterable): - for key, value in other: - self.add(key, value) - elif hasattr(other, "keys") and hasattr(other, "__getitem__"): - # THIS IS NOT A TYPESAFE BRANCH - # In this branch, the object has a `keys` attr but is not a Mapping or any of - # the other types indicated in the method signature. We do some stuff with - # it as though it partially implements the Mapping interface, but we're not - # doing that stuff safely AT ALL. - for key in other.keys(): - self.add(key, other[key]) - - for key, value in kwargs.items(): - self.add(key, value) - - @typing.overload - def getlist(self, key: str) -> list[str]: ... - - @typing.overload - def getlist(self, key: str, default: _DT) -> list[str] | _DT: ... - - def getlist( - self, key: str, default: _Sentinel | _DT = _Sentinel.not_passed - ) -> list[str] | _DT: - """Returns a list of all the values for the named field. Returns an - empty list if the key doesn't exist.""" - if isinstance(key, bytes): - key = key.decode("latin-1") - try: - vals = self._container[key.lower()] - except KeyError: - if default is _Sentinel.not_passed: - # _DT is unbound; empty list is instance of List[str] - return [] - # _DT is bound; default is instance of _DT - return default - else: - # _DT may or may not be bound; vals[1:] is instance of List[str], which - # meets our external interface requirement of `Union[List[str], _DT]`. - return vals[1:] - - def _prepare_for_method_change(self) -> Self: - """ - Remove content-specific header fields before changing the request - method to GET or HEAD according to RFC 9110, Section 15.4. - """ - content_specific_headers = [ - "Content-Encoding", - "Content-Language", - "Content-Location", - "Content-Type", - "Content-Length", - "Digest", - "Last-Modified", - ] - for header in content_specific_headers: - self.discard(header) - return self - - # Backwards compatibility for httplib - getheaders = getlist - getallmatchingheaders = getlist - iget = getlist - - # Backwards compatibility for http.cookiejar - get_all = getlist - - def __repr__(self) -> str: - return f"{type(self).__name__}({dict(self.itermerged())})" - - def _copy_from(self, other: HTTPHeaderDict) -> None: - for key in other: - val = other.getlist(key) - self._container[key.lower()] = [key, *val] - - def copy(self) -> Self: - clone = type(self)() - clone._copy_from(self) - return clone - - def iteritems(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all header lines, including duplicate ones.""" - for key in self: - vals = self._container[key.lower()] - for val in vals[1:]: - yield vals[0], val - - def itermerged(self) -> typing.Iterator[tuple[str, str]]: - """Iterate over all headers, merging duplicate ones together.""" - for key in self: - val = self._container[key.lower()] - yield val[0], ", ".join(val[1:]) - - def items(self) -> HTTPHeaderDictItemView: # type: ignore[override] - return HTTPHeaderDictItemView(self) - - def _has_value_for_header(self, header_name: str, potential_value: str) -> bool: - if header_name in self: - return potential_value in self._container[header_name.lower()][1:] - return False - - def __ior__(self, other: object) -> HTTPHeaderDict: - # Supports extending a header dict in-place using operator |= - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - self.extend(maybe_constructable) - return self - - def __or__(self, other: object) -> Self: - # Supports merging header dicts using operator | - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = self.copy() - result.extend(maybe_constructable) - return result - - def __ror__(self, other: object) -> Self: - # Supports merging header dicts using operator | when other is on left side - # combining items with add instead of __setitem__ - maybe_constructable = ensure_can_construct_http_header_dict(other) - if maybe_constructable is None: - return NotImplemented - result = type(self)(maybe_constructable) - result.extend(self) - return result diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py deleted file mode 100644 index 297c271bf4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_request_methods.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -import json as _json -import typing -from urllib.parse import urlencode - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .filepost import _TYPE_FIELDS, encode_multipart_formdata -from .response import BaseHTTPResponse - -__all__ = ["RequestMethods"] - -_TYPE_ENCODE_URL_FIELDS = typing.Union[ - typing.Sequence[tuple[str, typing.Union[str, bytes]]], - typing.Mapping[str, typing.Union[str, bytes]], -] - - -class RequestMethods: - """ - Convenience mixin for classes who implement a :meth:`urlopen` method, such - as :class:`urllib3.HTTPConnectionPool` and - :class:`urllib3.PoolManager`. - - Provides behavior for making common types of HTTP request methods and - decides which type of request field encoding to use. - - Specifically, - - :meth:`.request_encode_url` is for sending requests whose fields are - encoded in the URL (such as GET, HEAD, DELETE). - - :meth:`.request_encode_body` is for sending requests whose fields are - encoded in the *body* of the request using multipart or www-form-urlencoded - (such as for POST, PUT, PATCH). - - :meth:`.request` is for making any kind of request, it will look up the - appropriate encoding format and use one of the above two methods to make - the request. - - Initializer parameters: - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - """ - - _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} - - def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: - self.headers = headers or {} - - def urlopen( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **kw: typing.Any, - ) -> BaseHTTPResponse: # Abstract - raise NotImplementedError( - "Classes extending RequestMethods must implement " - "their own ``urlopen`` method." - ) - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - json: typing.Any | None = None, - **urlopen_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the appropriate encoding of - ``fields`` based on the ``method`` used. - - This is a convenience method that requires the least amount of manual - effort. It can be used in most situations, while still having the - option to drop down to more specific methods when necessary, such as - :meth:`request_encode_url`, :meth:`request_encode_body`, - or even the lowest level :meth:`urlopen`. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param fields: - Data to encode and send in the URL or request body, depending on ``method``. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param json: - Data to encode and send as JSON with UTF-encoded in the request body. - The ``"Content-Type"`` header will be set to ``"application/json"`` - unless specified otherwise. - """ - method = method.upper() - - if json is not None and body is not None: - raise TypeError( - "request got values for both 'body' and 'json' parameters which are mutually exclusive" - ) - - if json is not None: - if headers is None: - headers = self.headers - - if not ("content-type" in map(str.lower, headers.keys())): - headers = HTTPHeaderDict(headers) - headers["Content-Type"] = "application/json" - - body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( - "utf-8" - ) - - if body is not None: - urlopen_kw["body"] = body - - if method in self._encode_url_methods: - return self.request_encode_url( - method, - url, - fields=fields, # type: ignore[arg-type] - headers=headers, - **urlopen_kw, - ) - else: - return self.request_encode_body( - method, url, fields=fields, headers=headers, **urlopen_kw - ) - - def request_encode_url( - self, - method: str, - url: str, - fields: _TYPE_ENCODE_URL_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the url. This is useful for request methods like GET, HEAD, DELETE, etc. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the URL. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": headers} - extra_kw.update(urlopen_kw) - - if fields: - url += "?" + urlencode(fields) - - return self.urlopen(method, url, **extra_kw) - - def request_encode_body( - self, - method: str, - url: str, - fields: _TYPE_FIELDS | None = None, - headers: typing.Mapping[str, str] | None = None, - encode_multipart: bool = True, - multipart_boundary: str | None = None, - **urlopen_kw: str, - ) -> BaseHTTPResponse: - """ - Make a request using :meth:`urlopen` with the ``fields`` encoded in - the body. This is useful for request methods like POST, PUT, PATCH, etc. - - When ``encode_multipart=True`` (default), then - :func:`urllib3.encode_multipart_formdata` is used to encode - the payload with the appropriate content type. Otherwise - :func:`urllib.parse.urlencode` is used with the - 'application/x-www-form-urlencoded' content type. - - Multipart encoding must be used when posting files, and it's reasonably - safe to use it in other times too. However, it may break request - signing, such as with OAuth. - - Supports an optional ``fields`` parameter of key/value strings AND - key/filetuple. A filetuple is a (filename, data, MIME type) tuple where - the MIME type is optional. For example:: - - fields = { - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), - 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - } - - When uploading a file, providing a filename (the first parameter of the - tuple) is optional but recommended to best mimic behavior of browsers. - - Note that if ``headers`` are supplied, the 'Content-Type' header will - be overwritten because it depends on the dynamic random boundary string - which is used to compose the body of the request. The random boundary - string can be explicitly set with the ``multipart_boundary`` parameter. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param fields: - Data to encode and send in the request body. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param encode_multipart: - If True, encode the ``fields`` using the multipart/form-data MIME - format. - - :param multipart_boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - if headers is None: - headers = self.headers - - extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} - body: bytes | str - - if fields: - if "body" in urlopen_kw: - raise TypeError( - "request got values for both 'fields' and 'body', can only specify one." - ) - - if encode_multipart: - body, content_type = encode_multipart_formdata( - fields, boundary=multipart_boundary - ) - else: - body, content_type = ( - urlencode(fields), # type: ignore[arg-type] - "application/x-www-form-urlencoded", - ) - - extra_kw["body"] = body - extra_kw["headers"].setdefault("Content-Type", content_type) - - extra_kw.update(urlopen_kw) - - return self.urlopen(method, url, **extra_kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py deleted file mode 100644 index bfed03985b..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/_version.py +++ /dev/null @@ -1,24 +0,0 @@ -# file generated by vcs-versioning -# don't change, don't track in version control -from __future__ import annotations - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -version: str -__version__: str -__version_tuple__: tuple[int | str, ...] -version_tuple: tuple[int | str, ...] -commit_id: str | None -__commit_id__: str | None - -__version__ = version = '2.7.0' -__version_tuple__ = version_tuple = (2, 7, 0) - -__commit_id__ = commit_id = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py deleted file mode 100644 index 84e1dab945..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connection.py +++ /dev/null @@ -1,1099 +0,0 @@ -from __future__ import annotations - -import datetime -import http.client -import logging -import os -import re -import socket -import sys -import threading -import typing -import warnings -from http.client import HTTPConnection as _HTTPConnection -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from .response import HTTPResponse - from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT - from .util.ssltransport import SSLTransport - -from ._collections import HTTPHeaderDict -from .http2 import probe as http2_probe -from .util.response import assert_header_parsing -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout -from .util.util import to_str -from .util.wait import wait_for_read - -try: # Compiled with SSL? - import ssl - - BaseSSLError = ssl.SSLError -except (ImportError, AttributeError): - ssl = None # type: ignore[assignment] - - class BaseSSLError(BaseException): # type: ignore[no-redef] - pass - - -from ._base_connection import _TYPE_BODY -from ._base_connection import ProxyConfig as ProxyConfig -from ._base_connection import _ResponseOptions as _ResponseOptions -from ._version import __version__ -from .exceptions import ( - ConnectTimeoutError, - HeaderParsingError, - NameResolutionError, - NewConnectionError, - ProxyError, - SystemTimeWarning, -) -from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_ -from .util.request import body_to_chunks -from .util.ssl_ import assert_fingerprint as _assert_fingerprint -from .util.ssl_ import ( - create_urllib3_context, - is_ipaddress, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .util.ssl_match_hostname import CertificateError, match_hostname -from .util.url import Url - -# Not a no-op, we're adding this to the namespace so it can be imported. -ConnectionError = ConnectionError -BrokenPipeError = BrokenPipeError - - -log = logging.getLogger(__name__) - -port_by_scheme = {"http": 80, "https": 443} - -# When it comes time to update this value as a part of regular maintenance -# (ie test_recent_date is failing) update it to ~6 months before the current date. -RECENT_DATE = datetime.date(2025, 1, 1) - -_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") - - -class HTTPConnection(_HTTPConnection): - """ - Based on :class:`http.client.HTTPConnection` but provides an extra constructor - backwards-compatibility layer between older and newer Pythons. - - Additional keyword parameters are used to configure attributes of the connection. - Accepted parameters include: - - - ``source_address``: Set the source address for the current connection. - - ``socket_options``: Set specific options on the underlying socket. If not specified, then - defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling - Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. - - For example, if you wish to enable TCP Keep Alive in addition to the defaults, - you might pass: - - .. code-block:: python - - HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ] - - Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). - """ - - default_port: typing.ClassVar[int] = port_by_scheme["http"] # type: ignore[misc] - - #: Disable Nagle's algorithm by default. - #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` - default_socket_options: typing.ClassVar[connection._TYPE_SOCKET_OPTIONS] = [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - ] - - #: Whether this connection verifies the host's certificate. - is_verified: bool = False - - #: Whether this proxy connection verified the proxy host's certificate. - # If no proxy is currently connected to the value will be ``None``. - proxy_is_verified: bool | None = None - - blocksize: int - source_address: tuple[str, int] | None - socket_options: connection._TYPE_SOCKET_OPTIONS | None - - _has_connected_to_proxy: bool - _response_options: _ResponseOptions | None - _tunnel_host: str | None - _tunnel_port: int | None - _tunnel_scheme: str | None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - super().__init__( - host=host, - port=port, - timeout=Timeout.resolve_default_timeout(timeout), - source_address=source_address, - blocksize=blocksize, - ) - self.socket_options = socket_options - self.proxy = proxy - self.proxy_config = proxy_config - - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host: str | None = None - self._tunnel_port: int | None = None - self._tunnel_scheme: str | None = None - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __repr__(self) -> str: - return f"<{self} at {id(self):#x}>" - - @property - def host(self) -> str: - """ - Getter method to remove any trailing dots that indicate the hostname is an FQDN. - - In general, SSL certificates don't include the trailing dot indicating a - fully-qualified domain name, and thus, they don't validate properly when - checked against a domain name that includes the dot. In addition, some - servers may not expect to receive the trailing dot when provided. - - However, the hostname with trailing dot is critical to DNS resolution; doing a - lookup with the trailing dot will properly only resolve the appropriate FQDN, - whereas a lookup without a trailing dot will search the system's search domain - list. Thus, it's important to keep the original host around for use only in - those cases where it's appropriate (i.e., when doing DNS lookup to establish the - actual TCP connection across which we're going to send HTTP requests). - """ - return self._dns_host.rstrip(".") - - @host.setter - def host(self, value: str) -> None: - """ - Setter for the `host` property. - - We assume that only urllib3 uses the _dns_host attribute; httplib itself - only uses `host`, and it seems reasonable that other libraries follow suit. - """ - self._dns_host = value - - def _new_conn(self) -> socket.socket: - """Establish a socket connection and set nodelay settings on it. - - :return: New socket connection. - """ - try: - sock = connection.create_connection( - (self._dns_host, self.port), - self.timeout, - source_address=self.source_address, - socket_options=self.socket_options, - ) - except socket.gaierror as e: - raise NameResolutionError(self.host, self, e) from e - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except OSError as e: - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - sys.audit("http.client.connect", self, self.host, self.port) - - return sock - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - if scheme not in ("http", "https"): - raise ValueError( - f"Invalid proxy scheme for tunneling: {scheme!r}, must be either 'http' or 'https'" - ) - super().set_tunnel(host, port=port, headers=headers) - self._tunnel_scheme = scheme - - if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)): - # Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3. - # When using connection_from_host, host will come without brackets. - def _wrap_ipv6(self, ip: bytes) -> bytes: - if b":" in ip and ip[0] != b"["[0]: - return b"[" + ip + b"]" - return ip - - if sys.version_info < (3, 11, 9): - # `_tunnel` copied from 3.11.13 backporting - # https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261 - # and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1 - def _tunnel(self) -> None: - _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] - connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - while True: - line = response.fp.readline(_MAXLINE + 1) - if len(line) > _MAXLINE: - raise http.client.LineTooLong("header line") - if not line: - # for sites which EOF without sending a trailer - break - if line in (b"\r\n", b"\n", b""): - break - - if self.debuglevel > 0: - print("header:", line.decode()) - finally: - response.close() - - elif (3, 12) <= sys.version_info < (3, 12, 3): - # `_tunnel` copied from 3.12.11 backporting - # https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8 - def _tunnel(self) -> None: # noqa: F811 - connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format] - self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr] - self._tunnel_port, - ) - headers = [connect] - for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined] - headers.append(f"{header}: {value}\r\n".encode("latin-1")) - headers.append(b"\r\n") - # Making a single send() call instead of one per line encourages - # the host OS to use a more optimal packet size instead of - # potentially emitting a series of small packets. - self.send(b"".join(headers)) - del headers - - response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined] - try: - (version, code, message) = response._read_status() # type: ignore[attr-defined] - - self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined] - - if self.debuglevel > 0: - for header in self._raw_proxy_headers: - print("header:", header.decode()) - - if code != http.HTTPStatus.OK: - self.close() - raise OSError( - f"Tunnel connection failed: {code} {message.strip()}" - ) - - finally: - response.close() - - def connect(self) -> None: - self.sock = self._new_conn() - if self._tunnel_host: - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - # TODO: Fix tunnel so it doesn't depend on self.sock state. - self._tunnel() - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - if self._has_connected_to_proxy: - self.proxy_is_verified = False - - @property - def is_closed(self) -> bool: - return self.sock is None - - @property - def is_connected(self) -> bool: - if self.sock is None: - return False - return not wait_for_read(self.sock, timeout=0.0) - - @property - def has_connected_to_proxy(self) -> bool: - return self._has_connected_to_proxy - - @property - def proxy_is_forwarding(self) -> bool: - """ - Return True if a forwarding proxy is configured, else return False - """ - return bool(self.proxy) and self._tunnel_host is None - - @property - def proxy_is_tunneling(self) -> bool: - """ - Return True if a tunneling proxy is configured, else return False - """ - return self._tunnel_host is not None - - def close(self) -> None: - try: - super().close() - finally: - # Reset all stateful properties so connection - # can be re-used without leaking prior configs. - self.sock = None - self.is_verified = False - self.proxy_is_verified = None - self._has_connected_to_proxy = False - self._response_options = None - self._tunnel_host = None - self._tunnel_port = None - self._tunnel_scheme = None - - def putrequest( - self, - method: str, - url: str, - skip_host: bool = False, - skip_accept_encoding: bool = False, - ) -> None: - """""" - # Empty docstring because the indentation of CPython's implementation - # is broken but we don't want this method in our documentation. - match = _CONTAINS_CONTROL_CHAR_RE.search(method) - if match: - raise ValueError( - f"Method cannot contain non-token characters {method!r} (found at least {match.group()!r})" - ) - - return super().putrequest( - method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding - ) - - def putheader(self, header: str, *values: str) -> None: # type: ignore[override] - """""" - if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): - super().putheader(header, *values) - elif to_str(header.lower()) not in SKIPPABLE_HEADERS: - skippable_headers = "', '".join( - [str.title(header) for header in sorted(SKIPPABLE_HEADERS)] - ) - raise ValueError( - f"urllib3.util.SKIP_HEADER only supports '{skippable_headers}'" - ) - - # `request` method's signature intentionally violates LSP. - # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - # Update the inner socket's timeout value to send the request. - # This only triggers if the connection is re-used. - if self.sock is not None: - self.sock.settimeout(self.timeout) - - # Store these values to be fed into the HTTPResponse - # object later. TODO: Remove this in favor of a real - # HTTP lifecycle mechanism. - - # We have to store these before we call .request() - # because sometimes we can still salvage a response - # off the wire even if we aren't able to completely - # send the request body. - self._response_options = _ResponseOptions( - request_method=method, - request_url=url, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - if headers is None: - headers = {} - header_keys = frozenset(to_str(k.lower()) for k in headers) - skip_accept_encoding = "accept-encoding" in header_keys - skip_host = "host" in header_keys - self.putrequest( - method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host - ) - - # Transform the body into an iterable of sendall()-able chunks - # and detect if an explicit Content-Length is doable. - chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize) - chunks = chunks_and_cl.chunks - content_length = chunks_and_cl.content_length - - # When chunked is explicit set to 'True' we respect that. - if chunked: - if "transfer-encoding" not in header_keys: - self.putheader("Transfer-Encoding", "chunked") - else: - # Detect whether a framing mechanism is already in use. If so - # we respect that value, otherwise we pick chunked vs content-length - # depending on the type of 'body'. - if "content-length" in header_keys: - chunked = False - elif "transfer-encoding" in header_keys: - chunked = True - - # Otherwise we go off the recommendation of 'body_to_chunks()'. - else: - chunked = False - if content_length is None: - if chunks is not None: - chunked = True - self.putheader("Transfer-Encoding", "chunked") - else: - self.putheader("Content-Length", str(content_length)) - - # Now that framing headers are out of the way we send all the other headers. - if "user-agent" not in header_keys: - self.putheader("User-Agent", _get_default_user_agent()) - for header, value in headers.items(): - self.putheader(header, value) - self.endheaders() - - # If we're given a body we start sending that in chunks. - if chunks is not None: - for chunk in chunks: - # Sending empty chunks isn't allowed for TE: chunked - # as it indicates the end of the body. - if not chunk: - continue - if isinstance(chunk, str): - chunk = chunk.encode("utf-8") - if chunked: - self.send(b"%x\r\n%b\r\n" % (len(chunk), chunk)) - else: - self.send(chunk) - - # Regardless of whether we have a body or not, if we're in - # chunked mode we want to send an explicit empty chunk. - if chunked: - self.send(b"0\r\n\r\n") - - def request_chunked( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - ) -> None: - """ - Alternative to the common request method, which sends the - body with chunked encoding and not as one block - """ - warnings.warn( - "HTTPConnection.request_chunked() is deprecated and will be removed " - "in urllib3 v3.0. Instead use HTTPConnection.request(..., chunked=True).", - category=FutureWarning, - stacklevel=2, - ) - self.request(method, url, body=body, headers=headers, chunked=True) - - def getresponse( # type: ignore[override] - self, - ) -> HTTPResponse: - """ - Get the response from the server. - - If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable. - - If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed. - """ - # Raise the same error as http.client.HTTPConnection - if self._response_options is None: - raise ResponseNotReady() - - # Reset this attribute for being used again. - resp_options = self._response_options - self._response_options = None - - # Since the connection's timeout value may have been updated - # we need to set the timeout on the socket. - self.sock.settimeout(self.timeout) - - # This is needed here to avoid circular import errors - from .response import HTTPResponse - - # Save a reference to the shutdown function before ownership is passed - # to httplib_response - # TODO should we implement it everywhere? - _shutdown = getattr(self.sock, "shutdown", None) - - # Get the response from http.client.HTTPConnection - httplib_response = super().getresponse() - - try: - assert_header_parsing(httplib_response.msg) - except (HeaderParsingError, TypeError) as hpe: - log.warning( - "Failed to parse headers (url=%s): %s", - _url_from_connection(self, resp_options.request_url), - hpe, - exc_info=True, - ) - - headers = HTTPHeaderDict(httplib_response.msg.items()) - - response = HTTPResponse( - body=httplib_response, - headers=headers, - status=httplib_response.status, - version=httplib_response.version, - version_string=getattr(self, "_http_vsn_str", "HTTP/?"), - reason=httplib_response.reason, - preload_content=resp_options.preload_content, - decode_content=resp_options.decode_content, - original_response=httplib_response, - enforce_content_length=resp_options.enforce_content_length, - request_method=resp_options.request_method, - request_url=resp_options.request_url, - sock_shutdown=_shutdown, - ) - return response - - -class HTTPSConnection(HTTPConnection): - """ - Many of the parameters to this constructor are passed to the underlying SSL - socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. - """ - - default_port = port_by_scheme["https"] # type: ignore[misc] - - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_fingerprint: str | None = None - _connect_callback: typing.Callable[..., None] | None = None - - def __init__( - self, - host: str, - port: int | None = None, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: None | ( - connection._TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - # cert_reqs depends on ssl_context so calculate last. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - self.cert_reqs = cert_reqs - self._connect_callback = None - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - """ - This method should only be called once, before the connection is used. - """ - warnings.warn( - "HTTPSConnection.set_cert() is deprecated and will be removed " - "in urllib3 v3.0. Instead provide the parameters to the " - "HTTPSConnection constructor.", - category=FutureWarning, - stacklevel=2, - ) - - # If cert_reqs is not provided we'll assume CERT_REQUIRED unless we also - # have an SSLContext object in which case we'll use its verify_mode. - if cert_reqs is None: - if self.ssl_context is not None: - cert_reqs = self.ssl_context.verify_mode - else: - cert_reqs = resolve_cert_reqs(None) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - def connect(self) -> None: - # Today we don't need to be doing this step before the /actual/ socket - # connection, however in the future we'll need to decide whether to - # create a new socket or re-use an existing "shared" socket as a part - # of the HTTP/2 handshake dance. - if self._tunnel_host is not None and self._tunnel_port is not None: - probe_http2_host = self._tunnel_host - probe_http2_port = self._tunnel_port - else: - probe_http2_host = self.host - probe_http2_port = self.port - - # Check if the target origin supports HTTP/2. - # If the value comes back as 'None' it means that the current thread - # is probing for HTTP/2 support. Otherwise, we're waiting for another - # probe to complete, or we get a value right away. - target_supports_http2: bool | None - if "h2" in ssl_.ALPN_PROTOCOLS: - target_supports_http2 = http2_probe.acquire_and_get( - host=probe_http2_host, port=probe_http2_port - ) - else: - # If HTTP/2 isn't going to be offered it doesn't matter if - # the target supports HTTP/2. Don't want to make a probe. - target_supports_http2 = False - - if self._connect_callback is not None: - self._connect_callback( - "before connect", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - try: - sock: socket.socket | ssl.SSLSocket - self.sock = sock = self._new_conn() - server_hostname: str = self.host - tls_in_tls = False - - # Do we need to establish a tunnel? - if self.proxy_is_tunneling: - # We're tunneling to an HTTPS origin so need to do TLS-in-TLS. - if self._tunnel_scheme == "https": - # _connect_tls_proxy will verify and assign proxy_is_verified - self.sock = sock = self._connect_tls_proxy(self.host, sock) - tls_in_tls = True - elif self._tunnel_scheme == "http": - self.proxy_is_verified = False - - # If we're tunneling it means we're connected to our proxy. - self._has_connected_to_proxy = True - - self._tunnel() - # Override the host with the one we're requesting data from. - server_hostname = typing.cast(str, self._tunnel_host) - - if self.server_hostname is not None: - server_hostname = self.server_hostname - - is_time_off = datetime.date.today() < RECENT_DATE - if is_time_off: - warnings.warn( - ( - f"System time is way off (before {RECENT_DATE}). This will probably " - "lead to SSL verification errors" - ), - SystemTimeWarning, - ) - - # Remove trailing '.' from fqdn hostnames to allow certificate validation - server_hostname_rm_dot = server_hostname.rstrip(".") - - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock=sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - server_hostname=server_hostname_rm_dot, - ssl_context=self.ssl_context, - tls_in_tls=tls_in_tls, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ) - self.sock = sock_and_verified.socket - - # If an error occurs during connection/handshake we may need to release - # our lock so another connection can probe the origin. - except BaseException: - if self._connect_callback is not None: - self._connect_callback( - "after connect failure", - thread_id=threading.get_ident(), - target_supports_http2=target_supports_http2, - ) - - if target_supports_http2 is None: - http2_probe.set_and_release( - host=probe_http2_host, port=probe_http2_port, supports_http2=None - ) - raise - - # If this connection doesn't know if the origin supports HTTP/2 - # we report back to the HTTP/2 probe our result. - if target_supports_http2 is None: - supports_http2 = sock_and_verified.socket.selected_alpn_protocol() == "h2" - http2_probe.set_and_release( - host=probe_http2_host, - port=probe_http2_port, - supports_http2=supports_http2, - ) - - # Forwarding proxies can never have a verified target since - # the proxy is the one doing the verification. Should instead - # use a CONNECT tunnel in order to verify the target. - # See: https://github.com/urllib3/urllib3/issues/3267. - if self.proxy_is_forwarding: - self.is_verified = False - else: - self.is_verified = sock_and_verified.is_verified - - # If there's a proxy to be connected to we are fully connected. - # This is set twice (once above and here) due to forwarding proxies - # not using tunnelling. - self._has_connected_to_proxy = bool(self.proxy) - - # Set `self.proxy_is_verified` unless it's already set while - # establishing a tunnel. - if self._has_connected_to_proxy and self.proxy_is_verified is None: - self.proxy_is_verified = sock_and_verified.is_verified - - def _connect_tls_proxy(self, hostname: str, sock: socket.socket) -> ssl.SSLSocket: - """ - Establish a TLS connection to the proxy using the provided SSL context. - """ - # `_connect_tls_proxy` is called when self._tunnel_host is truthy. - proxy_config = typing.cast(ProxyConfig, self.proxy_config) - ssl_context = proxy_config.ssl_context - sock_and_verified = _ssl_wrap_socket_and_match_hostname( - sock, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - ca_cert_data=self.ca_cert_data, - server_hostname=hostname, - ssl_context=ssl_context, - assert_hostname=proxy_config.assert_hostname, - assert_fingerprint=proxy_config.assert_fingerprint, - # Features that aren't implemented for proxies yet: - cert_file=None, - key_file=None, - key_password=None, - tls_in_tls=False, - ) - self.proxy_is_verified = sock_and_verified.is_verified - return sock_and_verified.socket # type: ignore[return-value] - - -class _WrappedAndVerifiedSocket(typing.NamedTuple): - """ - Wrapped socket and whether the connection is - verified after the TLS handshake - """ - - socket: ssl.SSLSocket | SSLTransport - is_verified: bool - - -def _ssl_wrap_socket_and_match_hostname( - sock: socket.socket, - *, - cert_reqs: None | str | int, - ssl_version: None | str | int, - ssl_minimum_version: int | None, - ssl_maximum_version: int | None, - cert_file: str | None, - key_file: str | None, - key_password: str | None, - ca_certs: str | None, - ca_cert_dir: str | None, - ca_cert_data: None | str | bytes, - assert_hostname: None | str | typing.Literal[False], - assert_fingerprint: str | None, - server_hostname: str | None, - ssl_context: ssl.SSLContext | None, - tls_in_tls: bool = False, -) -> _WrappedAndVerifiedSocket: - """Logic for constructing an SSLContext from all TLS parameters, passing - that down into ssl_wrap_socket, and then doing certificate verification - either via hostname or fingerprint. This function exists to guarantee - that both proxies and targets have the same behavior when connecting via TLS. - """ - default_ssl_context = False - if ssl_context is None: - default_ssl_context = True - context = create_urllib3_context( - ssl_version=resolve_ssl_version(ssl_version), - ssl_minimum_version=ssl_minimum_version, - ssl_maximum_version=ssl_maximum_version, - cert_reqs=resolve_cert_reqs(cert_reqs), - ) - else: - context = ssl_context - - context.verify_mode = resolve_cert_reqs(cert_reqs) - - # In some cases, we want to verify hostnames ourselves - if ( - # `ssl` can't verify fingerprints or alternate hostnames - assert_fingerprint - or assert_hostname - # assert_hostname can be set to False to disable hostname checking - or assert_hostname is False - # We still support OpenSSL 1.0.2, which prevents us from verifying - # hostnames easily: https://github.com/pyca/pyopenssl/pull/933 - or ssl_.IS_PYOPENSSL - or not ssl_.HAS_NEVER_CHECK_COMMON_NAME - ): - context.check_hostname = False - - # Try to load OS default certs if none are given. We need to do the hasattr() check - # for custom pyOpenSSL SSLContext objects because they don't support - # load_default_certs(). - if ( - not ca_certs - and not ca_cert_dir - and not ca_cert_data - and default_ssl_context - and hasattr(context, "load_default_certs") - ): - context.load_default_certs() - - # Ensure that IPv6 addresses are in the proper format and don't have a - # scope ID. Python's SSL module fails to recognize scoped IPv6 addresses - # and interprets them as DNS hostnames. - if server_hostname is not None: - normalized = server_hostname.strip("[]") - if "%" in normalized: - normalized = normalized[: normalized.rfind("%")] - if is_ipaddress(normalized): - server_hostname = normalized - - ssl_sock = ssl_wrap_socket( - sock=sock, - keyfile=key_file, - certfile=cert_file, - key_password=key_password, - ca_certs=ca_certs, - ca_cert_dir=ca_cert_dir, - ca_cert_data=ca_cert_data, - server_hostname=server_hostname, - ssl_context=context, - tls_in_tls=tls_in_tls, - ) - - try: - if assert_fingerprint: - _assert_fingerprint( - ssl_sock.getpeercert(binary_form=True), assert_fingerprint - ) - elif ( - context.verify_mode != ssl.CERT_NONE - and not context.check_hostname - and assert_hostname is not False - ): - cert: _TYPE_PEER_CERT_RET_DICT = ssl_sock.getpeercert() # type: ignore[assignment] - - # Need to signal to our match_hostname whether to use 'commonName' or not. - # If we're using our own constructed SSLContext we explicitly set 'False' - # because PyPy hard-codes 'True' from SSLContext.hostname_checks_common_name. - if default_ssl_context: - hostname_checks_common_name = False - else: - hostname_checks_common_name = ( - getattr(context, "hostname_checks_common_name", False) or False - ) - - _match_hostname( - cert, - assert_hostname or server_hostname, # type: ignore[arg-type] - hostname_checks_common_name, - ) - - return _WrappedAndVerifiedSocket( - socket=ssl_sock, - is_verified=context.verify_mode == ssl.CERT_REQUIRED - or bool(assert_fingerprint), - ) - except BaseException: - ssl_sock.close() - raise - - -def _match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - asserted_hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - # Our upstream implementation of ssl.match_hostname() - # only applies this normalization to IP addresses so it doesn't - # match DNS SANs so we do the same thing! - stripped_hostname = asserted_hostname.strip("[]") - if is_ipaddress(stripped_hostname): - asserted_hostname = stripped_hostname - - try: - match_hostname(cert, asserted_hostname, hostname_checks_common_name) - except CertificateError as e: - log.warning( - "Certificate did not match expected hostname: %s. Certificate: %s", - asserted_hostname, - cert, - ) - # Add cert to exception and reraise so client code can inspect - # the cert when catching the exception, if they want to - e._peer_cert = cert # type: ignore[attr-defined] - raise - - -def _wrap_proxy_error(err: Exception, proxy_scheme: str | None) -> ProxyError: - # Look for the phrase 'wrong version number', if found - # then we should warn the user that we're very sure that - # this proxy is HTTP-only and they have a configuration issue. - error_normalized = " ".join(re.split("[^a-z]", str(err).lower())) - is_likely_http_proxy = ( - "wrong version number" in error_normalized - or "unknown protocol" in error_normalized - or "record layer failure" in error_normalized - ) - http_proxy_warning = ( - ". Your proxy appears to only use HTTP and not HTTPS, " - "try changing your proxy URL to be HTTP. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#https-proxy-error-http-proxy" - ) - new_err = ProxyError( - f"Unable to connect to proxy" - f"{http_proxy_warning if is_likely_http_proxy and proxy_scheme == 'https' else ''}", - err, - ) - new_err.__cause__ = err - return new_err - - -def _get_default_user_agent() -> str: - return f"python-urllib3/{__version__}" - - -class DummyConnection: - """Used to detect a failed ConnectionCls import.""" - - -if not ssl: - HTTPSConnection = DummyConnection # type: ignore[misc, assignment] # noqa: F811 - - -VerifiedHTTPSConnection = HTTPSConnection - - -def _url_from_connection( - conn: HTTPConnection | HTTPSConnection, path: str | None = None -) -> str: - """Returns the URL from a given connection. This is mainly used for testing and logging.""" - - scheme = "https" if isinstance(conn, HTTPSConnection) else "http" - - return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py deleted file mode 100644 index 70fbc5e725..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/connectionpool.py +++ /dev/null @@ -1,1191 +0,0 @@ -from __future__ import annotations - -import errno -import logging -import queue -import sys -import typing -import warnings -import weakref -from socket import timeout as SocketTimeout -from types import TracebackType - -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from ._request_methods import RequestMethods -from .connection import ( - BaseSSLError, - BrokenPipeError, - DummyConnection, - HTTPConnection, - HTTPException, - HTTPSConnection, - ProxyConfig, - _wrap_proxy_error, -) -from .connection import port_by_scheme as port_by_scheme -from .exceptions import ( - ClosedPoolError, - EmptyPoolError, - FullPoolError, - HostChangedError, - InsecureRequestWarning, - LocationValueError, - MaxRetryError, - NewConnectionError, - ProtocolError, - ProxyError, - ReadTimeoutError, - SSLError, - TimeoutError, -) -from .response import BaseHTTPResponse -from .util.connection import is_connection_dropped -from .util.proxy import connection_requires_http_tunnel -from .util.request import _TYPE_BODY_POSITION, set_file_position -from .util.retry import Retry -from .util.ssl_match_hostname import CertificateError -from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout -from .util.url import Url, _encode_target -from .util.url import _normalize_host as normalize_host -from .util.url import parse_url -from .util.util import to_str - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - - from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - -_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] - - -# Pool objects -class ConnectionPool: - """ - Base class for all connection pools, such as - :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. - - .. note:: - ConnectionPool.urlopen() does not normalize or percent-encode target URIs - which is useful if your target server doesn't support percent-encoded - target URIs. - """ - - scheme: str | None = None - QueueCls = queue.LifoQueue - - def __init__(self, host: str, port: int | None = None) -> None: - if not host: - raise LocationValueError("No host specified.") - - self.host = _normalize_host(host, scheme=self.scheme) - self.port = port - - # This property uses 'normalize_host()' (not '_normalize_host()') - # to avoid removing square braces around IPv6 addresses. - # This value is sent to `HTTPConnection.set_tunnel()` if called - # because square braces are required for HTTP CONNECT tunneling. - self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() - - def __str__(self) -> str: - return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.close() - # Return False to re-raise any potential exceptions - return False - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - - -# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 -_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} - - -class HTTPConnectionPool(ConnectionPool, RequestMethods): - """ - Thread-safe connection pool for one host. - - :param host: - Host used for this HTTP Connection (e.g. "localhost"), passed into - :class:`http.client.HTTPConnection`. - - :param port: - Port used for this HTTP Connection (None is equivalent to 80), passed - into :class:`http.client.HTTPConnection`. - - :param timeout: - Socket timeout in seconds for each individual connection. This can - be a float or integer, which sets the timeout for the HTTP request, - or an instance of :class:`urllib3.util.Timeout` which gives you more - fine-grained control over request timeouts. After the constructor has - been parsed, this is always a `urllib3.util.Timeout` object. - - :param maxsize: - Number of connections to save that can be reused. More than 1 is useful - in multithreaded situations. If ``block`` is set to False, more - connections will be created but they will not be saved once they've - been used. - - :param block: - If set to True, no more than ``maxsize`` connections will be used at - a time. When no free connections are available, the call will block - until a connection has been released. This is a useful side effect for - particular multithreaded situations where one does not want to use more - than maxsize connections per host to prevent flooding. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param retries: - Retry configuration to use by default with requests in this pool. - - :param _proxy: - Parsed proxy URL, should not be used directly, instead, see - :class:`urllib3.ProxyManager` - - :param _proxy_headers: - A dictionary with proxy headers, should not be used directly, - instead, see :class:`urllib3.ProxyManager` - - :param \\**conn_kw: - Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, - :class:`urllib3.connection.HTTPSConnection` instances. - """ - - scheme = "http" - ConnectionCls: type[BaseHTTPConnection] | type[BaseHTTPSConnection] = HTTPConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - _proxy_config: ProxyConfig | None = None, - **conn_kw: typing.Any, - ): - ConnectionPool.__init__(self, host, port) - RequestMethods.__init__(self, headers) - - if not isinstance(timeout, Timeout): - timeout = Timeout.from_float(timeout) - - if retries is None: - retries = Retry.DEFAULT - - self.timeout = timeout - self.retries = retries - - self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) - self.block = block - - self.proxy = _proxy - self.proxy_headers = _proxy_headers or {} - self.proxy_config = _proxy_config - - # Fill the queue up so that doing get() on it will block properly - for _ in range(maxsize): - self.pool.put(None) - - # These are mostly for testing and debugging purposes. - self.num_connections = 0 - self.num_requests = 0 - self.conn_kw = conn_kw - - if self.proxy: - # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. - # Defaulting `socket_options` to an empty list avoids it defaulting to - # ``HTTPConnection.default_socket_options``. - self.conn_kw.setdefault("socket_options", []) - - self.conn_kw["proxy"] = self.proxy - self.conn_kw["proxy_config"] = self.proxy_config - - # Do not pass 'self' as callback to 'finalize'. - # Then the 'finalize' would keep an endless living (leak) to self. - # By just passing a reference to the pool allows the garbage collector - # to free self if nobody else has a reference to it. - pool = self.pool - - # Close all the HTTPConnections in the pool before the - # HTTPConnectionPool object is garbage collected. - weakref.finalize(self, _close_pool_connections, pool) - - def _new_conn(self) -> BaseHTTPConnection: - """ - Return a fresh :class:`HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTP connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "80", - ) - - conn = self.ConnectionCls( - host=self.host, - port=self.port, - timeout=self.timeout.connect_timeout, - **self.conn_kw, - ) - return conn - - def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: - """ - Get a connection. Will return a pooled connection if one is available. - - If no connections are available and :prop:`.block` is ``False``, then a - fresh connection is returned. - - :param timeout: - Seconds to wait before giving up and raising - :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and - :prop:`.block` is ``True``. - """ - conn = None - - if self.pool is None: - raise ClosedPoolError(self, "Pool is closed.") - - try: - conn = self.pool.get(block=self.block, timeout=timeout) - - except AttributeError: # self.pool is None - raise ClosedPoolError(self, "Pool is closed.") from None # Defensive: - - except queue.Empty: - if self.block: - raise EmptyPoolError( - self, - "Pool is empty and a new connection can't be opened due to blocking mode.", - ) from None - pass # Oh well, we'll create a new connection then - - # If this is a persistent connection, check if it got disconnected - if conn and is_connection_dropped(conn): - log.debug("Resetting dropped connection: %s", self.host) - conn.close() - - return conn or self._new_conn() - - def _put_conn(self, conn: BaseHTTPConnection | None) -> None: - """ - Put a connection back into the pool. - - :param conn: - Connection object for the current host and port as returned by - :meth:`._new_conn` or :meth:`._get_conn`. - - If the pool is already full, the connection is closed and discarded - because we exceeded maxsize. If connections are discarded frequently, - then maxsize should be increased. - - If the pool is closed, then the connection will be closed and discarded. - """ - if self.pool is not None: - try: - self.pool.put(conn, block=False) - return # Everything is dandy, done. - except AttributeError: - # self.pool is None. - pass - except queue.Full: - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - if self.block: - # This should never happen if you got the conn from self._get_conn - raise FullPoolError( - self, - "Pool reached maximum size and no more connections are allowed.", - ) from None - - log.warning( - "Connection pool is full, discarding connection: %s. Connection pool size: %s", - self.host, - self.pool.qsize(), - ) - - # Connection never got put back into the pool, close it. - if conn: - conn.close() - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - - def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: - # Nothing to do for HTTP connections. - pass - - def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: - """Helper that always returns a :class:`urllib3.util.Timeout`""" - if timeout is _DEFAULT_TIMEOUT: - return self.timeout.clone() - - if isinstance(timeout, Timeout): - return timeout.clone() - else: - # User passed us an int/float. This is for backwards compatibility, - # can be removed later - return Timeout.from_float(timeout) - - def _raise_timeout( - self, - err: BaseSSLError | OSError | SocketTimeout, - url: str, - timeout_value: _TYPE_TIMEOUT | None, - ) -> None: - """Is the error actually a timeout? Will raise a ReadTimeout or pass""" - - if isinstance(err, SocketTimeout): - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - # See the above comment about EAGAIN in Python 3. - if hasattr(err, "errno") and err.errno in _blocking_errnos: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={timeout_value})" - ) from err - - def _make_request( - self, - conn: BaseHTTPConnection, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | None = None, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - chunked: bool = False, - response_conn: BaseHTTPConnection | None = None, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> BaseHTTPResponse: - """ - Perform a request on a given urllib connection object taken from our - pool. - - :param conn: - a connection from one of our connection pools - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - Pass ``None`` to retry until you receive a response. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param response_conn: - Set this to ``None`` if you will handle releasing the connection or - set the connection to have the response release it. - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - self.num_requests += 1 - - timeout_obj = self._get_timeout(timeout) - timeout_obj.start_connect() - conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) - - try: - # Trigger any extra validation we need to do. - try: - self._validate_conn(conn) - except (SocketTimeout, BaseSSLError) as e: - self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) - raise - - # _validate_conn() starts the connection to an HTTPS proxy - # so we need to wrap errors with 'ProxyError' here too. - except ( - OSError, - NewConnectionError, - TimeoutError, - BaseSSLError, - CertificateError, - SSLError, - ) as e: - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - # If the connection didn't successfully connect to it's proxy - # then there - if isinstance( - new_e, (OSError, NewConnectionError, TimeoutError, SSLError) - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - raise new_e - - # conn.request() calls http.client.*.request, not the method in - # urllib3.request. It also calls makefile (recv) on the socket. - try: - conn.request( - method, - url, - body=body, - headers=headers, - chunked=chunked, - preload_content=preload_content, - decode_content=decode_content, - enforce_content_length=enforce_content_length, - ) - - # We are swallowing BrokenPipeError (errno.EPIPE) since the server is - # legitimately able to close the connection after sending a valid response. - # With this behaviour, the received response is still readable. - except BrokenPipeError: - pass - except OSError as e: - # MacOS/Linux - # EPROTOTYPE and ECONNRESET are needed on macOS - # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ - # Condition changed later to emit ECONNRESET instead of only EPROTOTYPE. - if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: - raise - - # Reset the timeout for the recv() on the socket - read_timeout = timeout_obj.read_timeout - - if not conn.is_closed: - # In Python 3 socket.py will catch EAGAIN and return None when you - # try and read into the file pointer created by http.client, which - # instead raises a BadStatusLine exception. Instead of catching - # the exception and assuming all BadStatusLine exceptions are read - # timeouts, check for a zero timeout before making the request. - if read_timeout == 0: - raise ReadTimeoutError( - self, url, f"Read timed out. (read timeout={read_timeout})" - ) - conn.timeout = read_timeout - - # Receive the response from the server - try: - response = conn.getresponse() - except (BaseSSLError, OSError) as e: - self._raise_timeout(err=e, url=url, timeout_value=read_timeout) - raise - - # Set properties that are used by the pooling layer. - response.retries = retries - response._connection = response_conn # type: ignore[attr-defined] - response._pool = self # type: ignore[attr-defined] - - log.debug( - '%s://%s:%s "%s %s %s" %s %s', - self.scheme, - self.host, - self.port, - method, - url, - response.version_string, - response.status, - response.length_remaining, - ) - - return response - - def close(self) -> None: - """ - Close all pooled connections and disable the pool. - """ - if self.pool is None: - return - # Disable access to the pool - old_pool, self.pool = self.pool, None - - # Close all the HTTPConnections in the pool. - _close_pool_connections(old_pool) - - def is_same_host(self, url: str) -> bool: - """ - Check if the given ``url`` is a member of the same host as this - connection pool. - """ - if url.startswith("/"): - return True - - # TODO: Add optional support for socket.gethostbyname checking. - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - if host is not None: - host = _normalize_host(host, scheme=scheme) - - # Use explicit default port for comparison when none is given - if self.port and not port: - port = port_by_scheme.get(scheme) - elif not self.port and port == port_by_scheme.get(scheme): - port = None - - return (scheme, host, port) == (self.scheme, self.host, self.port) - - def urlopen( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - redirect: bool = True, - assert_same_host: bool = True, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - pool_timeout: int | None = None, - release_conn: bool | None = None, - chunked: bool = False, - body_pos: _TYPE_BODY_POSITION | None = None, - preload_content: bool = True, - decode_content: bool = True, - **response_kw: typing.Any, - ) -> BaseHTTPResponse: - """ - Get a connection from the pool and perform an HTTP request. This is the - lowest level call for making a request, so you'll need to specify all - the raw details. - - .. note:: - - More commonly, it's appropriate to use a convenience method - such as :meth:`request`. - - .. note:: - - `release_conn` will only behave as expected if - `preload_content=False` because we want to make - `preload_content=False` the default behaviour someday soon without - breaking backwards compatibility. - - :param method: - HTTP request method (such as GET, POST, PUT, etc.) - - :param url: - The URL to perform the request on. - - :param body: - Data to send in the request body, either :class:`str`, :class:`bytes`, - an iterable of :class:`str`/:class:`bytes`, or a file-like object. - - :param headers: - Dictionary of custom headers to send, such as User-Agent, - If-None-Match, etc. If None, pool headers are used. If provided, - these headers completely replace any pool-specific headers. - - :param retries: - Configure the number of retries to allow before raising a - :class:`~urllib3.exceptions.MaxRetryError` exception. - - If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a - :class:`~urllib3.util.retry.Retry` object for fine-grained control - over different types of retries. - Pass an integer number to retry connection errors that many times, - but no other types of errors. Pass zero to never retry. - - If ``False``, then retries are disabled and any exception is raised - immediately. Also, instead of raising a MaxRetryError on redirects, - the redirect response will be returned. - - :type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. - - :param redirect: - If True, automatically handle redirects (status codes 301, 302, - 303, 307, 308). Each redirect counts as a retry. Disabling retries - will disable redirect, too. - - :param assert_same_host: - If ``True``, will make sure that the host of the pool requests is - consistent else will raise HostChangedError. When ``False``, you can - use the pool on an HTTP proxy and request foreign hosts. - - :param timeout: - If specified, overrides the default timeout for this one - request. It may be a float (in seconds) or an instance of - :class:`urllib3.util.Timeout`. - - :param pool_timeout: - If set and the pool is set to block=True, then this method will - block for ``pool_timeout`` seconds and raise EmptyPoolError if no - connection is available within the time period. - - :param bool preload_content: - If True, the response's body will be preloaded into memory. - - :param bool decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param release_conn: - If False, then the urlopen call will not release the connection - back into the pool once a response is received (but will release if - you read the entire contents of the response such as when - `preload_content=True`). This is useful if you're not preloading - the response's content immediately. You will need to call - ``r.release_conn()`` on the response ``r`` to return the connection - back into the pool. If None, it takes the value of ``preload_content`` - which defaults to ``True``. - - :param bool chunked: - If True, urllib3 will send the body using chunked transfer - encoding. Otherwise, urllib3 will send the body using the standard - content-length form. Defaults to False. - - :param int body_pos: - Position to seek to in file-like body in the event of a retry or - redirect. Typically this won't need to be set because urllib3 will - auto-populate the value when needed. - """ - # Ensure that the URL we're connecting to is properly encoded - if url.startswith("/"): - # URLs starting with / are inherently schemeless. - url = to_str(_encode_target(url)) - destination_scheme = None - else: - parsed_url = parse_url(url) - destination_scheme = parsed_url.scheme - url = to_str(parsed_url.url) - - if headers is None: - headers = self.headers - - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect, default=self.retries) - - if release_conn is None: - release_conn = preload_content - - # Check host - if assert_same_host and not self.is_same_host(url): - raise HostChangedError(self, url, retries) - - conn = None - - # Track whether `conn` needs to be released before - # returning/raising/recursing. Update this variable if necessary, and - # leave `release_conn` constant throughout the function. That way, if - # the function recurses, the original value of `release_conn` will be - # passed down into the recursive call, and its value will be respected. - # - # See issue #651 [1] for details. - # - # [1] - release_this_conn = release_conn - - http_tunnel_required = connection_requires_http_tunnel( - self.proxy, self.proxy_config, destination_scheme - ) - - # Merge the proxy headers. Only done when not using HTTP CONNECT. We - # have to copy the headers dict so we can safely change it without those - # changes being reflected in anyone else's copy. - if not http_tunnel_required: - headers = headers.copy() # type: ignore[attr-defined] - headers.update(self.proxy_headers) # type: ignore[union-attr] - - # Must keep the exception bound to a separate variable or else Python 3 - # complains about UnboundLocalError. - err = None - - # Keep track of whether we cleanly exited the except block. This - # ensures we do proper cleanup in finally. - clean_exit = False - - # Rewind body position, if needed. Record current position - # for future rewinds in the event of a redirect/retry. - body_pos = set_file_position(body, body_pos) - - try: - # Request a connection from the queue. - timeout_obj = self._get_timeout(timeout) - conn = self._get_conn(timeout=pool_timeout) - - conn.timeout = timeout_obj.connect_timeout # type: ignore[assignment] - - # Is this a closed/new connection that requires CONNECT tunnelling? - if self.proxy is not None and http_tunnel_required and conn.is_closed: - try: - self._prepare_proxy(conn) - except (BaseSSLError, OSError, SocketTimeout) as e: - self._raise_timeout( - err=e, url=self.proxy.url, timeout_value=conn.timeout - ) - raise - - # If we're going to release the connection in ``finally:``, then - # the response doesn't need to know about the connection. Otherwise - # it will also try to release it and we'll have a double-release - # mess. - response_conn = conn if not release_conn else None - - # Make the request on the HTTPConnection object - response = self._make_request( - conn, - method, - url, - timeout=timeout_obj, - body=body, - headers=headers, - chunked=chunked, - retries=retries, - response_conn=response_conn, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Everything went great! - clean_exit = True - - except EmptyPoolError: - # Didn't get a connection from the pool, no need to clean up - clean_exit = True - release_this_conn = False - raise - - except ( - TimeoutError, - HTTPException, - OSError, - ProtocolError, - BaseSSLError, - SSLError, - CertificateError, - ProxyError, - ) as e: - # Discard the connection for these exceptions. It will be - # replaced during the next _get_conn() call. - clean_exit = False - new_e: Exception = e - if isinstance(e, (BaseSSLError, CertificateError)): - new_e = SSLError(e) - if isinstance( - new_e, - ( - OSError, - NewConnectionError, - TimeoutError, - SSLError, - HTTPException, - ), - ) and (conn and conn.proxy and not conn.has_connected_to_proxy): - new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) - elif isinstance(new_e, (OSError, HTTPException)): - new_e = ProtocolError("Connection aborted.", new_e) - - retries = retries.increment( - method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] - ) - retries.sleep() - - # Keep track of the error for the retry warning. - err = e - - finally: - if not clean_exit: - # We hit some kind of exception, handled or otherwise. We need - # to throw the connection away unless explicitly told not to. - # Close the connection, set the variable to None, and make sure - # we put the None back in the pool to avoid leaking it. - if conn: - conn.close() - conn = None - release_this_conn = True - - if release_this_conn: - # Put the connection back to be reused. If the connection is - # expired then it will be None, which will get replaced with a - # fresh connection during _get_conn. - self._put_conn(conn) - - if not conn: - # Try again - log.warning( - "Retrying (%r) after connection broken by '%r': %s", retries, err, url - ) - return self.urlopen( - method, - url, - body, - headers, - retries, - redirect, - assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Handle redirect? - redirect_location = redirect and response.get_redirect_location() - if redirect_location: - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - body = None - headers = HTTPHeaderDict(headers)._prepare_for_method_change() - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # self.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not self.is_same_host( - redirect_location - ): - new_headers = headers.copy() # type: ignore[union-attr] - for header in headers: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - headers = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep_for_retry(response) - log.debug("Redirecting %s -> %s", url, redirect_location) - return self.urlopen( - method, - redirect_location, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - # Check if we should retry the HTTP response. - has_retry_after = bool(response.headers.get("Retry-After")) - if retries.is_retry(method, response.status, has_retry_after): - try: - retries = retries.increment(method, url, response=response, _pool=self) - except MaxRetryError: - if retries.raise_on_status: - response.drain_conn() - raise - return response - - response.drain_conn() - retries.sleep(response) - log.debug("Retry: %s", url) - return self.urlopen( - method, - url, - body, - headers, - retries=retries, - redirect=redirect, - assert_same_host=assert_same_host, - timeout=timeout, - pool_timeout=pool_timeout, - release_conn=release_conn, - chunked=chunked, - body_pos=body_pos, - preload_content=preload_content, - decode_content=decode_content, - **response_kw, - ) - - return response - - -class HTTPSConnectionPool(HTTPConnectionPool): - """ - Same as :class:`.HTTPConnectionPool`, but HTTPS. - - :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, - ``assert_hostname`` and ``host`` in this order to verify connections. - If ``assert_hostname`` is False, no verification is done. - - The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, - ``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` - is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade - the connection socket into an SSL socket. - """ - - scheme = "https" - ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection - - def __init__( - self, - host: str, - port: int | None = None, - timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, - maxsize: int = 1, - block: bool = False, - headers: typing.Mapping[str, str] | None = None, - retries: Retry | bool | int | None = None, - _proxy: Url | None = None, - _proxy_headers: typing.Mapping[str, str] | None = None, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - ssl_version: int | str | None = None, - ssl_minimum_version: ssl.TLSVersion | None = None, - ssl_maximum_version: ssl.TLSVersion | None = None, - assert_hostname: str | typing.Literal[False] | None = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - **conn_kw: typing.Any, - ) -> None: - super().__init__( - host, - port, - timeout, - maxsize, - block, - headers, - retries, - _proxy, - _proxy_headers, - **conn_kw, - ) - - self.key_file = key_file - self.cert_file = cert_file - self.cert_reqs = cert_reqs - self.key_password = key_password - self.ca_certs = ca_certs - self.ca_cert_dir = ca_cert_dir - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - - def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override] - """Establishes a tunnel connection through HTTP CONNECT.""" - if self.proxy and self.proxy.scheme == "https": - tunnel_scheme = "https" - else: - tunnel_scheme = "http" - - conn.set_tunnel( - scheme=tunnel_scheme, - host=self._tunnel_host, - port=self.port, - headers=self.proxy_headers, - ) - conn.connect() - - def _new_conn(self) -> BaseHTTPSConnection: - """ - Return a fresh :class:`urllib3.connection.HTTPConnection`. - """ - self.num_connections += 1 - log.debug( - "Starting new HTTPS connection (%d): %s:%s", - self.num_connections, - self.host, - self.port or "443", - ) - - if not self.ConnectionCls or self.ConnectionCls is DummyConnection: # type: ignore[comparison-overlap] - raise ImportError( - "Can't connect to HTTPS URL because the SSL module is not available." - ) - - actual_host: str = self.host - actual_port = self.port - if self.proxy is not None and self.proxy.host is not None: - actual_host = self.proxy.host - actual_port = self.proxy.port - - return self.ConnectionCls( - host=actual_host, - port=actual_port, - timeout=self.timeout.connect_timeout, - cert_file=self.cert_file, - key_file=self.key_file, - key_password=self.key_password, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint, - ssl_version=self.ssl_version, - ssl_minimum_version=self.ssl_minimum_version, - ssl_maximum_version=self.ssl_maximum_version, - **self.conn_kw, - ) - - def _validate_conn(self, conn: BaseHTTPConnection) -> None: - """ - Called right before a request is made, after the socket is created. - """ - super()._validate_conn(conn) - - # Force connect early to allow us to validate the connection. - if conn.is_closed: - conn.connect() - - # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791 - if not conn.is_verified and not conn.proxy_is_verified: - warnings.warn( - ( - f"Unverified HTTPS request is being made to host '{conn.host}'. " - "Adding certificate verification is strongly advised. See: " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#tls-warnings" - ), - InsecureRequestWarning, - ) - - -def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: - """ - Given a url, return an :class:`.ConnectionPool` instance of its host. - - This is a shortcut for not having to parse out the scheme, host, and port - of the url before creating an :class:`.ConnectionPool` instance. - - :param url: - Absolute URL string that must include the scheme. Port is optional. - - :param \\**kw: - Passes additional parameters to the constructor of the appropriate - :class:`.ConnectionPool`. Useful for specifying things like - timeout, maxsize, headers, etc. - - Example:: - - >>> conn = connection_from_url('http://google.com/') - >>> r = conn.request('GET', '/') - """ - scheme, _, host, port, *_ = parse_url(url) - scheme = scheme or "http" - port = port or port_by_scheme.get(scheme, 80) - if scheme == "https": - return HTTPSConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - else: - return HTTPConnectionPool(host, port=port, **kw) # type: ignore[arg-type] - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - """ - Normalize hosts for comparisons and use with sockets. - """ - - host = normalize_host(host, scheme) - - # httplib doesn't like it when we include brackets in IPv6 addresses - # Specifically, if we include brackets but also pass the port then - # httplib crazily doubles up the square brackets on the Host header. - # Instead, we need to make sure we never pass ``None`` as the port. - # However, for backward compatibility reasons we can't actually - # *assert* that. See http://bugs.python.org/issue28539 - if host and host.startswith("[") and host.endswith("]"): - host = host[1:-1] - return host - - -def _url_from_pool( - pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None -) -> str: - """Returns the URL from a given connection pool. This is mainly used for testing and logging.""" - return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url - - -def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: - """Drains a queue of connections and closes each one.""" - try: - while True: - conn = pool.get(block=False) - if conn: - conn.close() - except queue.Empty: - pass # Done. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py deleted file mode 100644 index e5b62b25e9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import urllib3.connection - -from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection - - -def inject_into_urllib3() -> None: - # override connection classes to use emscripten specific classes - # n.b. mypy complains about the overriding of classes below - # if it isn't ignored - HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection - HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection - urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment] - urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment] - urllib3.connection.VerifiedHTTPSConnection = EmscriptenHTTPSConnection # type: ignore[assignment] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py deleted file mode 100644 index 63f79dd3be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/connection.py +++ /dev/null @@ -1,260 +0,0 @@ -from __future__ import annotations - -import os -import typing - -# use http.client.HTTPException for consistency with non-emscripten -from http.client import HTTPException as HTTPException # noqa: F401 -from http.client import ResponseNotReady - -from ..._base_connection import _TYPE_BODY -from ...connection import HTTPConnection, ProxyConfig, port_by_scheme -from ...exceptions import TimeoutError -from ...response import BaseHTTPResponse -from ...util.connection import _TYPE_SOCKET_OPTIONS -from ...util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT -from ...util.url import Url -from .fetch import _RequestError, _TimeoutError, send_request, send_streaming_request -from .request import EmscriptenRequest -from .response import EmscriptenHttpResponseWrapper, EmscriptenResponse - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - - -class EmscriptenHTTPConnection: - default_port: typing.ClassVar[int] = port_by_scheme["http"] - default_socket_options: typing.ClassVar[_TYPE_SOCKET_OPTIONS] - - timeout: None | (float) - - host: str - port: int - blocksize: int - source_address: tuple[str, int] | None - socket_options: _TYPE_SOCKET_OPTIONS | None - - proxy: Url | None - proxy_config: ProxyConfig | None - - is_verified: bool = False - proxy_is_verified: bool | None = None - - response_class: type[BaseHTTPResponse] = EmscriptenHttpResponseWrapper - _response: EmscriptenResponse | None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 8192, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - ) -> None: - self.host = host - self.port = port - self.timeout = timeout if isinstance(timeout, float) else 0.0 - self.scheme = "http" - self._closed = True - self._response = None - # ignore these things because we don't - # have control over that stuff - self.proxy = None - self.proxy_config = None - self.blocksize = blocksize - self.source_address = None - self.socket_options = None - self.is_verified = False - - def set_tunnel( - self, - host: str, - port: int | None = 0, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - pass - - def connect(self) -> None: - pass - - def request( - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - # We know *at least* botocore is depending on the order of the - # first 3 parameters so to be safe we only mark the later ones - # as keyword-only to ensure we have space to extend. - *, - chunked: bool = False, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - ) -> None: - self._closed = False - if url.startswith("/"): - if self.port is not None: - port = f":{self.port}" - else: - port = "" - # no scheme / host / port included, make a full url - url = f"{self.scheme}://{self.host}{port}{url}" - request = EmscriptenRequest( - url=url, - method=method, - timeout=self.timeout if self.timeout else 0, - decode_content=decode_content, - ) - request.set_body(body) - if headers: - for k, v in headers.items(): - request.set_header(k, v) - self._response = None - try: - if not preload_content: - self._response = send_streaming_request(request) - if self._response is None: - self._response = send_request(request) - except _TimeoutError as e: - raise TimeoutError(e.message) from e - except _RequestError as e: - raise HTTPException(e.message) from e - - def getresponse(self) -> BaseHTTPResponse: - if self._response is not None: - return EmscriptenHttpResponseWrapper( - internal_response=self._response, - url=self._response.request.url, - connection=self, - ) - else: - raise ResponseNotReady() - - def close(self) -> None: - self._closed = True - self._response = None - - @property - def is_closed(self) -> bool: - """Whether the connection either is brand new or has been previously closed. - If this property is True then both ``is_connected`` and ``has_connected_to_proxy`` - properties must be False. - """ - return self._closed - - @property - def is_connected(self) -> bool: - """Whether the connection is actively connected to any origin (proxy or target)""" - return True - - @property - def has_connected_to_proxy(self) -> bool: - """Whether the connection has successfully connected to its proxy. - This returns False if no proxy is in use. Used to determine whether - errors are coming from the proxy layer or from tunnelling to the target origin. - """ - return False - - -class EmscriptenHTTPSConnection(EmscriptenHTTPConnection): - default_port = port_by_scheme["https"] - # all this is basically ignored, as browser handles https - cert_reqs: int | str | None = None - ca_certs: str | None = None - ca_cert_dir: str | None = None - ca_cert_data: None | str | bytes = None - cert_file: str | None - key_file: str | None - key_password: str | None - ssl_context: typing.Any | None - ssl_version: int | str | None = None - ssl_minimum_version: int | None = None - ssl_maximum_version: int | None = None - assert_hostname: None | str | typing.Literal[False] - assert_fingerprint: str | None = None - - def __init__( - self, - host: str, - port: int = 0, - *, - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - blocksize: int = 16384, - socket_options: ( - None | _TYPE_SOCKET_OPTIONS - ) = HTTPConnection.default_socket_options, - proxy: Url | None = None, - proxy_config: ProxyConfig | None = None, - cert_reqs: int | str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - server_hostname: str | None = None, - ssl_context: typing.Any | None = None, - ca_certs: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - ssl_version: int | str | None = None, # Deprecated - cert_file: str | None = None, - key_file: str | None = None, - key_password: str | None = None, - ) -> None: - super().__init__( - host, - port=port, - timeout=timeout, - source_address=source_address, - blocksize=blocksize, - socket_options=socket_options, - proxy=proxy, - proxy_config=proxy_config, - ) - self.scheme = "https" - - self.key_file = key_file - self.cert_file = cert_file - self.key_password = key_password - self.ssl_context = ssl_context - self.server_hostname = server_hostname - self.assert_hostname = assert_hostname - self.assert_fingerprint = assert_fingerprint - self.ssl_version = ssl_version - self.ssl_minimum_version = ssl_minimum_version - self.ssl_maximum_version = ssl_maximum_version - self.ca_certs = ca_certs and os.path.expanduser(ca_certs) - self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) - self.ca_cert_data = ca_cert_data - - self.cert_reqs = None - - # The browser will automatically verify all requests. - # We have no control over that setting. - self.is_verified = True - - def set_cert( - self, - key_file: str | None = None, - cert_file: str | None = None, - cert_reqs: int | str | None = None, - key_password: str | None = None, - ca_certs: str | None = None, - assert_hostname: None | str | typing.Literal[False] = None, - assert_fingerprint: str | None = None, - ca_cert_dir: str | None = None, - ca_cert_data: None | str | bytes = None, - ) -> None: - pass - - -# verify that this class implements BaseHTTP(s) connection correctly -if typing.TYPE_CHECKING: - _supports_http_protocol: BaseHTTPConnection = EmscriptenHTTPConnection("", 0) - _supports_https_protocol: BaseHTTPSConnection = EmscriptenHTTPSConnection("", 0) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js deleted file mode 100644 index faf141e1fa..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/emscripten_fetch_worker.js +++ /dev/null @@ -1,110 +0,0 @@ -let Status = { - SUCCESS_HEADER: -1, - SUCCESS_EOF: -2, - ERROR_TIMEOUT: -3, - ERROR_EXCEPTION: -4, -}; - -let connections = new Map(); -let nextConnectionID = 1; -const encoder = new TextEncoder(); - -self.addEventListener("message", async function (event) { - if (event.data.close) { - let connectionID = event.data.close; - connections.delete(connectionID); - return; - } else if (event.data.getMore) { - let connectionID = event.data.getMore; - let { curOffset, value, reader, intBuffer, byteBuffer } = - connections.get(connectionID); - // if we still have some in buffer, then just send it back straight away - if (!value || curOffset >= value.length) { - // read another buffer if required - try { - let readResponse = await reader.read(); - - if (readResponse.done) { - // read everything - clear connection and return - connections.delete(connectionID); - Atomics.store(intBuffer, 0, Status.SUCCESS_EOF); - Atomics.notify(intBuffer, 0); - // finished reading successfully - // return from event handler - return; - } - curOffset = 0; - connections.get(connectionID).value = readResponse.value; - value = readResponse.value; - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } - - // send as much buffer as we can - let curLen = value.length - curOffset; - if (curLen > byteBuffer.length) { - curLen = byteBuffer.length; - } - byteBuffer.set(value.subarray(curOffset, curOffset + curLen), 0); - - Atomics.store(intBuffer, 0, curLen); // store current length in bytes - Atomics.notify(intBuffer, 0); - curOffset += curLen; - connections.get(connectionID).curOffset = curOffset; - - return; - } else { - // start fetch - let connectionID = nextConnectionID; - nextConnectionID += 1; - const intBuffer = new Int32Array(event.data.buffer); - const byteBuffer = new Uint8Array(event.data.buffer, 8); - try { - const response = await fetch(event.data.url, event.data.fetchParams); - // return the headers first via textencoder - var headers = []; - for (const pair of response.headers.entries()) { - headers.push([pair[0], pair[1]]); - } - let headerObj = { - headers: headers, - status: response.status, - connectionID, - }; - const headerText = JSON.stringify(headerObj); - let headerBytes = encoder.encode(headerText); - let written = headerBytes.length; - byteBuffer.set(headerBytes); - intBuffer[1] = written; - // make a connection - connections.set(connectionID, { - reader: response.body.getReader(), - intBuffer: intBuffer, - byteBuffer: byteBuffer, - value: undefined, - curOffset: 0, - }); - // set header ready - Atomics.store(intBuffer, 0, Status.SUCCESS_HEADER); - Atomics.notify(intBuffer, 0); - // all fetching after this goes through a new postmessage call with getMore - // this allows for parallel requests - } catch (error) { - console.log("Request exception:", error); - let errorBytes = encoder.encode(error.message); - let written = errorBytes.length; - byteBuffer.set(errorBytes); - intBuffer[1] = written; - Atomics.store(intBuffer, 0, Status.ERROR_EXCEPTION); - Atomics.notify(intBuffer, 0); - } - } -}); -self.postMessage({ inited: true }); diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py deleted file mode 100644 index 612cfddc4c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/fetch.py +++ /dev/null @@ -1,726 +0,0 @@ -""" -Support for streaming http requests in emscripten. - -A few caveats - - -If your browser (or Node.js) has WebAssembly JavaScript Promise Integration enabled -https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md -*and* you launch pyodide using `pyodide.runPythonAsync`, this will fetch data using the -JavaScript asynchronous fetch api (wrapped via `pyodide.ffi.call_sync`). In this case -timeouts and streaming should just work. - -Otherwise, it uses a combination of XMLHttpRequest and a web-worker for streaming. - -This approach has several caveats: - -Firstly, you can't do streaming http in the main UI thread, because atomics.wait isn't allowed. -Streaming only works if you're running pyodide in a web worker. - -Secondly, this uses an extra web worker and SharedArrayBuffer to do the asynchronous fetch -operation, so it requires that you have crossOriginIsolation enabled, by serving over https -(or from localhost) with the two headers below set: - - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp - -You can tell if cross origin isolation is successfully enabled by looking at the global crossOriginIsolated variable in -JavaScript console. If it isn't, streaming requests will fallback to XMLHttpRequest, i.e. getting the whole -request into a buffer and then returning it. it shows a warning in the JavaScript console in this case. - -Finally, the webworker which does the streaming fetch is created on initial import, but will only be started once -control is returned to javascript. Call `await wait_for_streaming_ready()` to wait for streaming fetch. - -NB: in this code, there are a lot of JavaScript objects. They are named js_* -to make it clear what type of object they are. -""" - -from __future__ import annotations - -import io -import json -from email.parser import Parser -from importlib.resources import files -from typing import TYPE_CHECKING, Any - -import js # type: ignore[import-not-found] -from pyodide.ffi import ( # type: ignore[import-not-found] - JsArray, - JsException, - JsProxy, - to_js, -) - -if TYPE_CHECKING: - from typing_extensions import Buffer - -from .request import EmscriptenRequest -from .response import EmscriptenResponse - -""" -There are some headers that trigger unintended CORS preflight requests. -See also https://github.com/koenvo/pyodide-http/issues/22 -""" -HEADERS_TO_IGNORE = ("user-agent",) - -SUCCESS_HEADER = -1 -SUCCESS_EOF = -2 -ERROR_TIMEOUT = -3 -ERROR_EXCEPTION = -4 - - -class _RequestError(Exception): - def __init__( - self, - message: str | None = None, - *, - request: EmscriptenRequest | None = None, - response: EmscriptenResponse | None = None, - ): - self.request = request - self.response = response - self.message = message - super().__init__(self.message) - - -class _StreamingError(_RequestError): - pass - - -class _TimeoutError(_RequestError): - pass - - -def _obj_from_dict(dict_val: dict[str, Any]) -> JsProxy: - return to_js(dict_val, dict_converter=js.Object.fromEntries) - - -class _ReadStream(io.RawIOBase): - def __init__( - self, - int_buffer: JsArray, - byte_buffer: JsArray, - timeout: float, - worker: JsProxy, - connection_id: int, - request: EmscriptenRequest, - ): - self.int_buffer = int_buffer - self.byte_buffer = byte_buffer - self.read_pos = 0 - self.read_len = 0 - self.connection_id = connection_id - self.worker = worker - self.timeout = int(1000 * timeout) if timeout > 0 else None - self.is_live = True - self._is_closed = False - self.request: EmscriptenRequest | None = request - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.int_buffer = None - self.byte_buffer = None - self._is_closed = True - self.request = None - if self.is_live: - self.worker.postMessage(_obj_from_dict({"close": self.connection_id})) - self.is_live = False - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def readinto(self, byte_obj: Buffer) -> int: - if not self.int_buffer: - raise _StreamingError( - "No buffer for stream in _ReadStream.readinto", - request=self.request, - response=None, - ) - if self.read_len == 0: - # wait for the worker to send something - js.Atomics.store(self.int_buffer, 0, ERROR_TIMEOUT) - self.worker.postMessage(_obj_from_dict({"getMore": self.connection_id})) - if ( - js.Atomics.wait(self.int_buffer, 0, ERROR_TIMEOUT, self.timeout) - == "timed-out" - ): - raise _TimeoutError - data_len = self.int_buffer[0] - if data_len > 0: - self.read_len = data_len - self.read_pos = 0 - elif data_len == ERROR_EXCEPTION: - string_len = self.int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(self.byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", - request=self.request, - response=None, - ) - else: - # EOF, free the buffers and return zero - # and free the request - self.is_live = False - self.close() - return 0 - # copy from int32array to python bytes - ret_length = min(self.read_len, len(memoryview(byte_obj))) - subarray = self.byte_buffer.subarray( - self.read_pos, self.read_pos + ret_length - ).to_py() - memoryview(byte_obj)[0:ret_length] = subarray - self.read_len -= ret_length - self.read_pos += ret_length - return ret_length - - -class _StreamingFetcher: - def __init__(self) -> None: - # make web-worker and data buffer on startup - self.streaming_ready = False - streaming_worker_code = ( - files(__package__) - .joinpath("emscripten_fetch_worker.js") - .read_text(encoding="utf-8") - ) - js_data_blob = js.Blob.new( - to_js([streaming_worker_code], create_pyproxies=False), - _obj_from_dict({"type": "application/javascript"}), - ) - - def promise_resolver(js_resolve_fn: JsProxy, js_reject_fn: JsProxy) -> None: - def onMsg(e: JsProxy) -> None: - self.streaming_ready = True - js_resolve_fn(e) - - def onErr(e: JsProxy) -> None: - js_reject_fn(e) # Defensive: never happens in ci - - self.js_worker.onmessage = onMsg - self.js_worker.onerror = onErr - - js_data_url = js.URL.createObjectURL(js_data_blob) - self.js_worker = js.globalThis.Worker.new(js_data_url) - self.js_worker_ready_promise = js.globalThis.Promise.new(promise_resolver) - - def send(self, request: EmscriptenRequest) -> EmscriptenResponse: - headers = { - k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE - } - - body = request.body - fetch_data = {"headers": headers, "body": to_js(body), "method": request.method} - # start the request off in the worker - timeout = int(1000 * request.timeout) if request.timeout > 0 else None - js_shared_buffer = js.SharedArrayBuffer.new(1048576) - js_int_buffer = js.Int32Array.new(js_shared_buffer) - js_byte_buffer = js.Uint8Array.new(js_shared_buffer, 8) - - js.Atomics.store(js_int_buffer, 0, ERROR_TIMEOUT) - js.Atomics.notify(js_int_buffer, 0) - js_absolute_url = js.URL.new(request.url, js.location).href - self.js_worker.postMessage( - _obj_from_dict( - { - "buffer": js_shared_buffer, - "url": js_absolute_url, - "fetchParams": fetch_data, - } - ) - ) - # wait for the worker to send something - js.Atomics.wait(js_int_buffer, 0, ERROR_TIMEOUT, timeout) - if js_int_buffer[0] == ERROR_TIMEOUT: - raise _TimeoutError( - "Timeout connecting to streaming request", - request=request, - response=None, - ) - elif js_int_buffer[0] == SUCCESS_HEADER: - # got response - # header length is in second int of intBuffer - string_len = js_int_buffer[1] - # decode the rest to a JSON string - js_decoder = js.TextDecoder.new() - # this does a copy (the slice) because decode can't work on shared array - # for some silly reason - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - # get it as an object - response_obj = json.loads(json_str) - return EmscriptenResponse( - request=request, - status_code=response_obj["status"], - headers=response_obj["headers"], - body=_ReadStream( - js_int_buffer, - js_byte_buffer, - request.timeout, - self.js_worker, - response_obj["connectionID"], - request, - ), - ) - elif js_int_buffer[0] == ERROR_EXCEPTION: - string_len = js_int_buffer[1] - # decode the error string - js_decoder = js.TextDecoder.new() - json_str = js_decoder.decode(js_byte_buffer.slice(0, string_len)) - raise _StreamingError( - f"Exception thrown in fetch: {json_str}", request=request, response=None - ) - else: - raise _StreamingError( - f"Unknown status from worker in fetch: {js_int_buffer[0]}", - request=request, - response=None, - ) - - -class _JSPIReadStream(io.RawIOBase): - """ - A read stream that uses pyodide.ffi.run_sync to read from a JavaScript fetch - response. This requires support for WebAssembly JavaScript Promise Integration - in the containing browser, and for pyodide to be launched via runPythonAsync. - - :param js_read_stream: - The JavaScript stream reader - - :param timeout: - Timeout in seconds - - :param request: - The request we're handling - - :param response: - The response this stream relates to - - :param js_abort_controller: - A JavaScript AbortController object, used for timeouts - """ - - def __init__( - self, - js_read_stream: Any, - timeout: float, - request: EmscriptenRequest, - response: EmscriptenResponse, - js_abort_controller: Any, # JavaScript AbortController for timeouts - ): - self.js_read_stream = js_read_stream - self.timeout = timeout - self._is_closed = False - self._is_done = False - self.request: EmscriptenRequest | None = request - self.response: EmscriptenResponse | None = response - self.current_buffer = None - self.current_buffer_pos = 0 - self.js_abort_controller = js_abort_controller - - def __del__(self) -> None: - self.close() - - # this is compatible with _base_connection - def is_closed(self) -> bool: - return self._is_closed - - # for compatibility with RawIOBase - @property - def closed(self) -> bool: - return self.is_closed() - - def close(self) -> None: - if self.is_closed(): - return - self.read_len = 0 - self.read_pos = 0 - self.js_read_stream.cancel() - self.js_read_stream = None - self._is_closed = True - self._is_done = True - self.request = None - self.response = None - super().close() - - def readable(self) -> bool: - return True - - def writable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def _get_next_buffer(self) -> bool: - result_js = _run_sync_with_timeout( - self.js_read_stream.read(), - self.timeout, - self.js_abort_controller, - request=self.request, - response=self.response, - ) - if result_js.done: - self._is_done = True - return False - else: - self.current_buffer = result_js.value.to_py() - self.current_buffer_pos = 0 - return True - - def readinto(self, byte_obj: Buffer) -> int: - if self.current_buffer is None: - if not self._get_next_buffer() or self.current_buffer is None: - self.close() - return 0 - ret_length = min( - len(byte_obj), len(self.current_buffer) - self.current_buffer_pos - ) - byte_obj[0:ret_length] = self.current_buffer[ - self.current_buffer_pos : self.current_buffer_pos + ret_length - ] - self.current_buffer_pos += ret_length - if self.current_buffer_pos == len(self.current_buffer): - self.current_buffer = None - return ret_length - - -# check if we are in a worker or not -def is_in_browser_main_thread() -> bool: - return hasattr(js, "window") and hasattr(js, "self") and js.self == js.window - - -def is_cross_origin_isolated() -> bool: - return hasattr(js, "crossOriginIsolated") and js.crossOriginIsolated - - -def is_in_node() -> bool: - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - and hasattr(js.process.release, "name") - and js.process.release.name == "node" - ) - - -def is_worker_available() -> bool: - return hasattr(js, "Worker") and hasattr(js, "Blob") - - -_fetcher: _StreamingFetcher | None = None - -if is_worker_available() and ( - (is_cross_origin_isolated() and not is_in_browser_main_thread()) - and (not is_in_node()) -): - _fetcher = _StreamingFetcher() -else: - _fetcher = None - - -NODE_JSPI_ERROR = ( - "urllib3 only works in Node.js with pyodide.runPythonAsync" - " and requires the flag --experimental-wasm-stack-switching in " - " versions of node <24." -) - - -def send_streaming_request(request: EmscriptenRequest) -> EmscriptenResponse | None: - if has_jspi(): - return send_jspi_request(request, True) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - - if _fetcher and streaming_ready(): - return _fetcher.send(request) - else: - _show_streaming_warning() - return None - - -_SHOWN_TIMEOUT_WARNING = False - - -def _show_timeout_warning() -> None: - global _SHOWN_TIMEOUT_WARNING - if not _SHOWN_TIMEOUT_WARNING: - _SHOWN_TIMEOUT_WARNING = True - message = "Warning: Timeout is not available on main browser thread" - js.console.warn(message) - - -_SHOWN_STREAMING_WARNING = False - - -def _show_streaming_warning() -> None: - global _SHOWN_STREAMING_WARNING - if not _SHOWN_STREAMING_WARNING: - _SHOWN_STREAMING_WARNING = True - message = "Can't stream HTTP requests because: \n" - if not is_cross_origin_isolated(): - message += " Page is not cross-origin isolated\n" - if is_in_browser_main_thread(): - message += " Python is running in main browser thread\n" - if not is_worker_available(): - message += " Worker or Blob classes are not available in this environment." # Defensive: this is always False in browsers that we test in - if streaming_ready() is False: - message += """ Streaming fetch worker isn't ready. If you want to be sure that streaming fetch -is working, you need to call: 'await urllib3.contrib.emscripten.fetch.wait_for_streaming_ready()`""" - from js import console - - console.warn(message) - - -def send_request(request: EmscriptenRequest) -> EmscriptenResponse: - if has_jspi(): - return send_jspi_request(request, False) - elif is_in_node(): - raise _RequestError( - message=NODE_JSPI_ERROR, - request=request, - response=None, - ) - try: - js_xhr = js.XMLHttpRequest.new() - - if not is_in_browser_main_thread(): - js_xhr.responseType = "arraybuffer" - if request.timeout: - js_xhr.timeout = int(request.timeout * 1000) - else: - js_xhr.overrideMimeType("text/plain; charset=ISO-8859-15") - if request.timeout: - # timeout isn't available on the main thread - show a warning in console - # if it is set - _show_timeout_warning() - - js_xhr.open(request.method, request.url, False) - for name, value in request.headers.items(): - if name.lower() not in HEADERS_TO_IGNORE: - js_xhr.setRequestHeader(name, value) - - js_xhr.send(to_js(request.body)) - - headers = dict(Parser().parsestr(js_xhr.getAllResponseHeaders())) - - if not is_in_browser_main_thread(): - body = js_xhr.response.to_py().tobytes() - else: - body = js_xhr.response.encode("ISO-8859-15") - return EmscriptenResponse( - status_code=js_xhr.status, headers=headers, body=body, request=request - ) - except JsException as err: - if err.name == "TimeoutError": - raise _TimeoutError(err.message, request=request) - elif err.name == "NetworkError": - raise _RequestError(err.message, request=request) - else: - # general http error - raise _RequestError(err.message, request=request) - - -def send_jspi_request( - request: EmscriptenRequest, streaming: bool -) -> EmscriptenResponse: - """ - Send a request using WebAssembly JavaScript Promise Integration - to wrap the asynchronous JavaScript fetch api (experimental). - - :param request: - Request to send - - :param streaming: - Whether to stream the response - - :return: The response object - :rtype: EmscriptenResponse - """ - timeout = request.timeout - js_abort_controller = js.AbortController.new() - headers = {k: v for k, v in request.headers.items() if k not in HEADERS_TO_IGNORE} - req_body = request.body - fetch_data = { - "headers": headers, - "body": to_js(req_body), - "method": request.method, - "signal": js_abort_controller.signal, - } - # Node.js returns the whole response (unlike opaqueredirect in browsers), - # so urllib3 can set `redirect: manual` to control redirects itself. - # https://stackoverflow.com/a/78524615 - if _is_node_js(): - fetch_data["redirect"] = "manual" - # Call JavaScript fetch (async api, returns a promise) - fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) - # Now suspend WebAssembly until we resolve that promise - # or time out. - response_js = _run_sync_with_timeout( - fetcher_promise_js, - timeout, - js_abort_controller, - request=request, - response=None, - ) - headers = {} - header_iter = response_js.headers.entries() - while True: - iter_value_js = header_iter.next() - if getattr(iter_value_js, "done", False): - break - else: - headers[str(iter_value_js.value[0])] = str(iter_value_js.value[1]) - status_code = response_js.status - body: bytes | io.RawIOBase = b"" - - response = EmscriptenResponse( - status_code=status_code, headers=headers, body=b"", request=request - ) - if streaming: - # get via inputstream - if response_js.body is not None: - # get a reader from the fetch response - body_stream_js = response_js.body.getReader() - body = _JSPIReadStream( - body_stream_js, timeout, request, response, js_abort_controller - ) - else: - # get directly via arraybuffer - # n.b. this is another async JavaScript call. - body = _run_sync_with_timeout( - response_js.arrayBuffer(), - timeout, - js_abort_controller, - request=request, - response=response, - ).to_py() - response.body = body - return response - - -def _run_sync_with_timeout( - promise: Any, - timeout: float, - js_abort_controller: Any, - request: EmscriptenRequest | None, - response: EmscriptenResponse | None, -) -> Any: - """ - Await a JavaScript promise synchronously with a timeout which is implemented - via the AbortController - - :param promise: - Javascript promise to await - - :param timeout: - Timeout in seconds - - :param js_abort_controller: - A JavaScript AbortController object, used on timeout - - :param request: - The request being handled - - :param response: - The response being handled (if it exists yet) - - :raises _TimeoutError: If the request times out - :raises _RequestError: If the request raises a JavaScript exception - - :return: The result of awaiting the promise. - """ - timer_id = None - if timeout > 0: - timer_id = js.setTimeout( - js_abort_controller.abort.bind(js_abort_controller), int(timeout * 1000) - ) - try: - from pyodide.ffi import run_sync - - # run_sync here uses WebAssembly JavaScript Promise Integration to - # suspend python until the JavaScript promise resolves. - return run_sync(promise) - except JsException as err: - if err.name == "AbortError": - raise _TimeoutError( - message="Request timed out", request=request, response=response - ) - else: - raise _RequestError(message=err.message, request=request, response=response) - finally: - if timer_id is not None: - js.clearTimeout(timer_id) - - -def has_jspi() -> bool: - """ - Return true if jspi can be used. - - This requires both browser support and also WebAssembly - to be in the correct state - i.e. that the javascript - call into python was async not sync. - - :return: True if jspi can be used. - :rtype: bool - """ - try: - from pyodide.ffi import can_run_sync, run_sync # noqa: F401 - - return bool(can_run_sync()) - except ImportError: - return False - - -def _is_node_js() -> bool: - """ - Check if we are in Node.js. - - :return: True if we are in Node.js. - :rtype: bool - """ - return ( - hasattr(js, "process") - and hasattr(js.process, "release") - # According to the Node.js documentation, the release name is always "node". - and js.process.release.name == "node" - ) - - -def streaming_ready() -> bool | None: - if _fetcher: - return _fetcher.streaming_ready - else: - return None # no fetcher, return None to signify that - - -async def wait_for_streaming_ready() -> bool: - if _fetcher: - await _fetcher.js_worker_ready_promise - return True - else: - return False diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py deleted file mode 100644 index e692e692bd..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/request.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from ..._base_connection import _TYPE_BODY - - -@dataclass -class EmscriptenRequest: - method: str - url: str - params: dict[str, str] | None = None - body: _TYPE_BODY | None = None - headers: dict[str, str] = field(default_factory=dict) - timeout: float = 0 - decode_content: bool = True - - def set_header(self, name: str, value: str) -> None: - self.headers[name.capitalize()] = value - - def set_body(self, body: _TYPE_BODY | None) -> None: - self.body = body diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py deleted file mode 100644 index ec1e1dbe83..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/emscripten/response.py +++ /dev/null @@ -1,281 +0,0 @@ -from __future__ import annotations - -import json as _json -import logging -import typing -from contextlib import contextmanager -from dataclasses import dataclass -from http.client import HTTPException as HTTPException -from io import BytesIO, IOBase - -from ...exceptions import InvalidHeader, TimeoutError -from ...response import BaseHTTPResponse -from ...util.retry import Retry -from .request import EmscriptenRequest - -if typing.TYPE_CHECKING: - from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection - -log = logging.getLogger(__name__) - - -@dataclass -class EmscriptenResponse: - status_code: int - headers: dict[str, str] - body: IOBase | bytes - request: EmscriptenRequest - - -class EmscriptenHttpResponseWrapper(BaseHTTPResponse): - def __init__( - self, - internal_response: EmscriptenResponse, - url: str | None = None, - connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, - ): - self._pool = None # set by pool class - self._body = None - self._uncached_read_occurred = False - self._response = internal_response - self._url = url - self._connection = connection - self._closed = False - super().__init__( - headers=internal_response.headers, - status=internal_response.status_code, - request_url=url, - version=0, - version_string="HTTP/?", - reason="", - decode_content=True, - ) - self.length_remaining = self._init_length(self._response.request.method) - self.length_is_certain = False - - @property - def url(self) -> str | None: - return self._url - - @url.setter - def url(self, url: str | None) -> None: - self._url = url - - @property - def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: - return self._connection - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - while True: - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - else: - break - - def _init_length(self, request_method: str | None) -> int | None: - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Check for responses that shouldn't include a body - if ( - self.status in (204, 304) - or 100 <= self.status < 200 - or request_method == "HEAD" - ): - length = 0 - - return length - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, # ignored because browser decodes always - cache_content: bool = False, - ) -> bytes: - if ( - self._closed - or self._response is None - or (isinstance(self._response.body, IOBase) and self._response.body.closed) - ): - return b"" - - with self._error_catcher(): - # body has been preloaded as a string by XmlHttpRequest - if not isinstance(self._response.body, IOBase): - self.length_remaining = len(self._response.body) - self.length_is_certain = True - # wrap body in IOStream - self._response.body = BytesIO(self._response.body) - if amt is not None and amt >= 0: - # don't cache partial content - cache_content = False - data = self._response.body.read(amt) - self._uncached_read_occurred = True - else: # read all we can (and cache it) - data = self._response.body.read() - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - self._uncached_read_occurred = True - if self.length_remaining is not None: - self.length_remaining = max(self.length_remaining - len(data), 0) - if len(data) == 0 or ( - self.length_is_certain and self.length_remaining == 0 - ): - # definitely finished reading, close response stream - self._response.body.close() - return typing.cast(bytes, data) - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Generator[bytes]: - # chunked is handled by browser - while True: - bytes = self.read(amt, decode_content) - if not bytes: - break - yield bytes - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - self.close() - - @property - def data(self) -> bytes: - if self._body: - return self._body - else: - return self.read(cache_content=True) - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - def close(self) -> None: - if not self._closed: - if isinstance(self._response.body, IOBase): - self._response.body.close() - if self._connection: - self._connection.close() - self._connection = None - self._closed = True - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch Emscripten specific exceptions thrown by fetch.py, - instead re-raising urllib3 variants, so that low-level exceptions - are not leaked in the high-level api. - - On exit, release the connection back to the pool. - """ - from .fetch import _RequestError, _TimeoutError # avoid circular import - - clean_exit = False - - try: - yield - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - except _TimeoutError as e: - raise TimeoutError(str(e)) - except _RequestError as e: - raise HTTPException(str(e)) - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now - if ( - isinstance(self._response.body, IOBase) - and not self._response.body.closed - ): - self._response.body.close() - # release the connection back to the pool - self.release_conn() - else: - # If we have read everything from the response stream, - # return the connection back to the pool. - if ( - isinstance(self._response.body, IOBase) - and self._response.body.closed - ): - self.release_conn() diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py deleted file mode 100644 index f06b859992..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/pyopenssl.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Module for using pyOpenSSL as a TLS backend. This module was relevant before -the standard library ``ssl`` module supported SNI, but now that we've dropped -support for Python 2.7 all relevant Python versions support SNI so -**this module is no longer recommended**. - -This needs the following packages installed: - -* `pyOpenSSL`_ (tested with 19.0.0) -* `cryptography`_ (minimum 2.3, from pyopenssl) -* `idna`_ (minimum 2.1, from cryptography) - -However, pyOpenSSL depends on cryptography, so while we use all three directly here we -end up having relatively few packages required. - -You can install them with the following command: - -.. code-block:: bash - - $ python -m pip install pyopenssl cryptography idna - -To activate certificate checking, call -:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code -before you begin making HTTP requests. This can be done in a ``sitecustomize`` -module, or at any other time before your application begins using ``urllib3``, -like this: - -.. code-block:: python - - try: - import urllib3.contrib.pyopenssl - urllib3.contrib.pyopenssl.inject_into_urllib3() - except ImportError: - pass - -.. _pyopenssl: https://www.pyopenssl.org -.. _cryptography: https://cryptography.io -.. _idna: https://github.com/kjd/idna -""" - -from __future__ import annotations - -import OpenSSL.SSL # type: ignore[import-not-found] -from cryptography import x509 - -try: - from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] -except ImportError: - # UnsupportedExtension is gone in cryptography >= 2.1.0 - class UnsupportedExtension(Exception): # type: ignore[no-redef] - pass - - -import logging -import ssl -import typing -from io import BytesIO -from socket import socket as socket_cls - -from .. import util - -if typing.TYPE_CHECKING: - from OpenSSL.crypto import X509 # type: ignore[import-not-found] - - -__all__ = ["inject_into_urllib3", "extract_from_urllib3"] - -# Map from urllib3 to PyOpenSSL compatible parameter-values. -_openssl_versions: dict[int, int] = { - util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] - ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, -} - -if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD - -if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): - _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD - - -_stdlib_to_openssl_verify = { - ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, - ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, - ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER - + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, -} -_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} - -# The SSLvX values are the most likely to be missing in the future -# but we check them all just to be sure. -_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( - OpenSSL.SSL, "OP_NO_SSLv3", 0 -) -_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) -_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) -_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) -_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) - -_openssl_to_ssl_minimum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, - ssl.TLSVersion.TLSv1_3: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), - ssl.TLSVersion.MAXIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 - ), -} -_openssl_to_ssl_maximum_version: dict[int, int] = { - ssl.TLSVersion.MINIMUM_SUPPORTED: ( - _OP_NO_SSLv2_OR_SSLv3 - | _OP_NO_TLSv1 - | _OP_NO_TLSv1_1 - | _OP_NO_TLSv1_2 - | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1: ( - _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 - ), - ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, - ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, - ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, -} - -# OpenSSL will only write 16K at a time -SSL_WRITE_BLOCKSIZE = 16384 - -orig_util_SSLContext = util.ssl_.SSLContext - - -log = logging.getLogger(__name__) - - -def inject_into_urllib3() -> None: - "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." - - _validate_dependencies_met() - - util.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] - util.IS_PYOPENSSL = True - util.ssl_.IS_PYOPENSSL = True - - -def extract_from_urllib3() -> None: - "Undo monkey-patching by :func:`inject_into_urllib3`." - - util.SSLContext = orig_util_SSLContext - util.ssl_.SSLContext = orig_util_SSLContext - util.IS_PYOPENSSL = False - util.ssl_.IS_PYOPENSSL = False - - -def _validate_dependencies_met() -> None: - """ - Verifies that PyOpenSSL's package-level dependencies have been met. - Throws `ImportError` if they are not met. - """ - # Method added in `cryptography==1.1`; not available in older versions - from cryptography.x509.extensions import Extensions - - if getattr(Extensions, "get_extension_for_class", None) is None: - raise ImportError( - "'cryptography' module missing required functionality. " - "Try upgrading to v1.3.4 or newer." - ) - - # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 - # attribute is only present on those versions. - from OpenSSL.crypto import X509 - - x509 = X509() - if getattr(x509, "_x509", None) is None: - raise ImportError( - "'pyOpenSSL' module missing required functionality. " - "Try upgrading to v0.14 or newer." - ) - - -def _dnsname_to_stdlib(name: str) -> str | None: - """ - Converts a dNSName SubjectAlternativeName field to the form used by the - standard library on the given Python version. - - Cryptography produces a dNSName as a unicode string that was idna-decoded - from ASCII bytes. We need to idna-encode that string to get it back, and - then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib - uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). - - If the name cannot be idna-encoded then we return None signalling that - the name given should be skipped. - """ - - def idna_encode(name: str) -> bytes | None: - """ - Borrowed wholesale from the Python Cryptography Project. It turns out - that we can't just safely call `idna.encode`: it can explode for - wildcard names. This avoids that problem. - """ - import idna - - try: - for prefix in ["*.", "."]: - if name.startswith(prefix): - name = name[len(prefix) :] - return prefix.encode("ascii") + idna.encode(name) - return idna.encode(name) - except idna.core.IDNAError: - return None - - # Don't send IPv6 addresses through the IDNA encoder. - if ":" in name: - return name - - encoded_name = idna_encode(name) - if encoded_name is None: - return None - return encoded_name.decode("utf-8") - - -def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: - """ - Given an PyOpenSSL certificate, provides all the subject alternative names. - """ - cert = peer_cert.to_cryptography() - - # We want to find the SAN extension. Ask Cryptography to locate it (it's - # faster than looping in Python) - try: - ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value - except x509.ExtensionNotFound: - # No such extension, return the empty list. - return [] - except ( - x509.DuplicateExtension, - UnsupportedExtension, - x509.UnsupportedGeneralNameType, - UnicodeError, - ) as e: - # A problem has been found with the quality of the certificate. Assume - # no SAN field is present. - log.warning( - "A problem was encountered with the certificate that prevented " - "urllib3 from finding the SubjectAlternativeName field. This can " - "affect certificate validation. The error was %s", - e, - ) - return [] - - # We want to return dNSName and iPAddress fields. We need to cast the IPs - # back to strings because the match_hostname function wants them as - # strings. - # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 - # decoded. This is pretty frustrating, but that's what the standard library - # does with certificates, and so we need to attempt to do the same. - # We also want to skip over names which cannot be idna encoded. - names = [ - ("DNS", name) - for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) - if name is not None - ] - names.extend( - ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) - ) - - return names - - -class WrappedSocket: - """API-compatibility wrapper for Python OpenSSL's Connection-class.""" - - def __init__( - self, - connection: OpenSSL.SSL.Connection, - socket: socket_cls, - suppress_ragged_eofs: bool = True, - ) -> None: - self.connection = connection - self.socket = socket - self.suppress_ragged_eofs = suppress_ragged_eofs - self._io_refs = 0 - self._closed = False - - def fileno(self) -> int: - return self.socket.fileno() - - # Copy-pasted from Python 3.5 source code - def _decref_socketios(self) -> None: - if self._io_refs > 0: - self._io_refs -= 1 - if self._closed: - self.close() - - def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: - try: - data = self.connection.recv(*args, **kwargs) - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return b"" - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return b"" - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - else: - return data # type: ignore[no-any-return] - - def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: - try: - return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] - except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): - return 0 - else: - raise OSError(e.args[0], str(e)) from e - except OpenSSL.SSL.ZeroReturnError: - if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return 0 - else: - raise - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise TimeoutError("The read operation timed out") from e - else: - return self.recv_into(*args, **kwargs) - - # TLS 1.3 post-handshake authentication - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"read error: {e!r}") from e - - def settimeout(self, timeout: float) -> None: - return self.socket.settimeout(timeout) - - def _send_until_done(self, data: bytes) -> int: - while True: - try: - return self.connection.send(data) # type: ignore[no-any-return] - except OpenSSL.SSL.WantWriteError as e: - if not util.wait_for_write(self.socket, self.socket.gettimeout()): - raise TimeoutError() from e - continue - except OpenSSL.SSL.SysCallError as e: - raise OSError(e.args[0], str(e)) from e - - def sendall(self, data: bytes) -> None: - total_sent = 0 - while total_sent < len(data): - sent = self._send_until_done( - data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] - ) - total_sent += sent - - def shutdown(self, how: int) -> None: - try: - self.connection.shutdown() - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"shutdown error: {e!r}") from e - - def close(self) -> None: - self._closed = True - if self._io_refs <= 0: - self._real_close() - - def _real_close(self) -> None: - try: - return self.connection.close() # type: ignore[no-any-return] - except OpenSSL.SSL.Error: - return - - def getpeercert( - self, binary_form: bool = False - ) -> dict[str, list[typing.Any]] | None: - x509 = self.connection.get_peer_certificate() - - if not x509: - return x509 # type: ignore[no-any-return] - - if binary_form: - return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] - - return { - "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] - "subjectAltName": get_subj_alt_name(x509), - } - - def version(self) -> str: - return self.connection.get_protocol_version_name() # type: ignore[no-any-return] - - def selected_alpn_protocol(self) -> str | None: - alpn_proto = self.connection.get_alpn_proto_negotiated() - return alpn_proto.decode() if alpn_proto else None - - -WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] - - -class PyOpenSSLContext: - """ - I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible - for translating the interface of the standard library ``SSLContext`` object - to calls into PyOpenSSL. - """ - - def __init__(self, protocol: int) -> None: - self.protocol = _openssl_versions[protocol] - self._ctx = OpenSSL.SSL.Context(self.protocol) - self._options = 0 - self.check_hostname = False - self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED - self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED - self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST - - @property - def options(self) -> int: - return self._options - - @options.setter - def options(self, value: int) -> None: - self._options = value - self._set_ctx_options() - - @property - def verify_flags(self) -> int: - return self._verify_flags - - @verify_flags.setter - def verify_flags(self, value: int) -> None: - self._verify_flags = value - self._ctx.get_cert_store().set_flags(self._verify_flags) - - @property - def verify_mode(self) -> int: - return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] - - @verify_mode.setter - def verify_mode(self, value: ssl.VerifyMode) -> None: - self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) - - def set_default_verify_paths(self) -> None: - self._ctx.set_default_verify_paths() - - def set_ciphers(self, ciphers: bytes | str) -> None: - if isinstance(ciphers, str): - ciphers = ciphers.encode("utf-8") - self._ctx.set_cipher_list(ciphers) - - def load_verify_locations( - self, - cafile: str | None = None, - capath: str | None = None, - cadata: bytes | None = None, - ) -> None: - if cafile is not None: - cafile = cafile.encode("utf-8") # type: ignore[assignment] - if capath is not None: - capath = capath.encode("utf-8") # type: ignore[assignment] - try: - self._ctx.load_verify_locations(cafile, capath) - if cadata is not None: - self._ctx.load_verify_locations(BytesIO(cadata)) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e - - def load_cert_chain( - self, - certfile: str, - keyfile: str | None = None, - password: str | None = None, - ) -> None: - try: - self._ctx.use_certificate_chain_file(certfile) - if password is not None: - if not isinstance(password, bytes): - password = password.encode("utf-8") # type: ignore[assignment] - self._ctx.set_passwd_cb(lambda *_: password) - self._ctx.use_privatekey_file(keyfile or certfile) - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e - - def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: - protocols = [util.util.to_bytes(p, "ascii") for p in protocols] - return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] - - def wrap_socket( - self, - sock: socket_cls, - server_side: bool = False, - do_handshake_on_connect: bool = True, - suppress_ragged_eofs: bool = True, - server_hostname: bytes | str | None = None, - ) -> WrappedSocket: - cnx = OpenSSL.SSL.Connection(self._ctx, sock) - - # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 - if server_hostname and not util.ssl_.is_ipaddress(server_hostname): - if isinstance(server_hostname, str): - server_hostname = server_hostname.encode("utf-8") - cnx.set_tlsext_host_name(server_hostname) - - cnx.set_connect_state() - - while True: - try: - cnx.do_handshake() - except OpenSSL.SSL.WantReadError as e: - if not util.wait_for_read(sock, sock.gettimeout()): - raise TimeoutError("select timed out") from e - continue - except OpenSSL.SSL.Error as e: - raise ssl.SSLError(f"bad handshake: {e!r}") from e - break - - return WrappedSocket(cnx, sock) - - def _set_ctx_options(self) -> None: - self._ctx.set_options( - self._options - | _openssl_to_ssl_minimum_version[self._minimum_version] - | _openssl_to_ssl_maximum_version[self._maximum_version] - ) - - @property - def minimum_version(self) -> int: - return self._minimum_version - - @minimum_version.setter - def minimum_version(self, minimum_version: int) -> None: - self._minimum_version = minimum_version - self._set_ctx_options() - - @property - def maximum_version(self) -> int: - return self._maximum_version - - @maximum_version.setter - def maximum_version(self, maximum_version: int) -> None: - self._maximum_version = maximum_version - self._set_ctx_options() - - -def _verify_callback( - cnx: OpenSSL.SSL.Connection, - x509: X509, - err_no: int, - err_depth: int, - return_code: int, -) -> bool: - return err_no == 0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py deleted file mode 100644 index d37da8fc20..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/contrib/socks.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -This module contains provisional support for SOCKS proxies from within -urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and -SOCKS5. To enable its functionality, either install PySocks or install this -module with the ``socks`` extra. - -The SOCKS implementation supports the full range of urllib3 features. It also -supports the following SOCKS features: - -- SOCKS4A (``proxy_url='socks4a://...``) -- SOCKS4 (``proxy_url='socks4://...``) -- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) -- SOCKS5 with local DNS (``proxy_url='socks5://...``) -- Usernames and passwords for the SOCKS proxy - -.. note:: - It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in - your ``proxy_url`` to ensure that DNS resolution is done from the remote - server instead of client-side when connecting to a domain name. - -SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 -supports IPv4, IPv6, and domain names. - -When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` -will be sent as the ``userid`` section of the SOCKS request: - -.. code-block:: python - - proxy_url="socks4a://@proxy-host" - -When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion -of the ``proxy_url`` will be sent as the username/password to authenticate -with the proxy: - -.. code-block:: python - - proxy_url="socks5h://:@proxy-host" - -""" - -from __future__ import annotations - -try: - import socks # type: ignore[import-untyped] -except ImportError: - import warnings - - from ..exceptions import DependencyWarning - - warnings.warn( - ( - "SOCKS support in urllib3 requires the installation of optional " - "dependencies: specifically, PySocks. For more information, see " - "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" - ), - DependencyWarning, - ) - raise - -import typing -from socket import timeout as SocketTimeout - -from ..connection import HTTPConnection, HTTPSConnection -from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from ..exceptions import ConnectTimeoutError, NewConnectionError -from ..poolmanager import PoolManager -from ..util.url import parse_url - -try: - import ssl -except ImportError: - ssl = None # type: ignore[assignment] - - -class _TYPE_SOCKS_OPTIONS(typing.TypedDict): - socks_version: int - proxy_host: str | None - proxy_port: str | None - username: str | None - password: str | None - rdns: bool - - -class SOCKSConnection(HTTPConnection): - """ - A plain-text HTTP connection that connects via a SOCKS proxy. - """ - - def __init__( - self, - _socks_options: _TYPE_SOCKS_OPTIONS, - *args: typing.Any, - **kwargs: typing.Any, - ) -> None: - self._socks_options = _socks_options - super().__init__(*args, **kwargs) - - def _new_conn(self) -> socks.socksocket: - """ - Establish a new connection via the SOCKS proxy. - """ - extra_kw: dict[str, typing.Any] = {} - if self.source_address: - extra_kw["source_address"] = self.source_address - - if self.socket_options: - extra_kw["socket_options"] = self.socket_options - - try: - conn = socks.create_connection( - (self.host, self.port), - proxy_type=self._socks_options["socks_version"], - proxy_addr=self._socks_options["proxy_host"], - proxy_port=self._socks_options["proxy_port"], - proxy_username=self._socks_options["username"], - proxy_password=self._socks_options["password"], - proxy_rdns=self._socks_options["rdns"], - timeout=self.timeout, - **extra_kw, - ) - - except SocketTimeout as e: - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - - except socks.ProxyError as e: - # This is fragile as hell, but it seems to be the only way to raise - # useful errors here. - if e.socket_err: - error = e.socket_err - if isinstance(error, SocketTimeout): - raise ConnectTimeoutError( - self, - f"Connection to {self.host} timed out. (connect timeout={self.timeout})", - ) from e - else: - # Adding `from e` messes with coverage somehow, so it's omitted. - # See #2386. - raise NewConnectionError( - self, f"Failed to establish a new connection: {error}" - ) - else: # Defensive: see https://github.com/urllib3/urllib3/pull/3728#pullrequestreview-3816302703 - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - except OSError as e: # Defensive: PySocks should catch all these. - raise NewConnectionError( - self, f"Failed to establish a new connection: {e}" - ) from e - - return conn - - -# We don't need to duplicate the Verified/Unverified distinction from -# urllib3/connection.py here because the HTTPSConnection will already have been -# correctly set to either the Verified or Unverified form by that module. This -# means the SOCKSHTTPSConnection will automatically be the correct type. -class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): - pass - - -class SOCKSHTTPConnectionPool(HTTPConnectionPool): - ConnectionCls = SOCKSConnection - - -class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): - ConnectionCls = SOCKSHTTPSConnection - - -class SOCKSProxyManager(PoolManager): - """ - A version of the urllib3 ProxyManager that routes connections via the - defined SOCKS proxy. - """ - - pool_classes_by_scheme = { - "http": SOCKSHTTPConnectionPool, - "https": SOCKSHTTPSConnectionPool, - } - - def __init__( - self, - proxy_url: str, - username: str | None = None, - password: str | None = None, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ): - parsed = parse_url(proxy_url) - - if username is None and password is None and parsed.auth is not None: - split = parsed.auth.split(":") - if len(split) == 2: - username, password = split - if parsed.scheme == "socks5": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = False - elif parsed.scheme == "socks5h": - socks_version = socks.PROXY_TYPE_SOCKS5 - rdns = True - elif parsed.scheme == "socks4": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = False - elif parsed.scheme == "socks4a": - socks_version = socks.PROXY_TYPE_SOCKS4 - rdns = True - else: - raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") - - self.proxy_url = proxy_url - - socks_options = { - "socks_version": socks_version, - "proxy_host": parsed.host, - "proxy_port": parsed.port, - "username": username, - "password": password, - "rdns": rdns, - } - connection_pool_kw["_socks_options"] = socks_options - - super().__init__(num_pools, headers, **connection_pool_kw) - - self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py deleted file mode 100644 index 3d7e9b93d3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/exceptions.py +++ /dev/null @@ -1,335 +0,0 @@ -from __future__ import annotations - -import socket -import typing -import warnings -from email.errors import MessageDefect -from http.client import IncompleteRead as httplib_IncompleteRead - -if typing.TYPE_CHECKING: - from .connection import HTTPConnection - from .connectionpool import ConnectionPool - from .response import HTTPResponse - from .util.retry import Retry - -# Base Exceptions - - -class HTTPError(Exception): - """Base exception used by this module.""" - - -class HTTPWarning(Warning): - """Base warning used by this module.""" - - -_TYPE_REDUCE_RESULT = tuple[typing.Callable[..., object], tuple[object, ...]] - - -class PoolError(HTTPError): - """Base exception for errors caused within a pool.""" - - def __init__(self, pool: ConnectionPool, message: str) -> None: - self.pool = pool - self._message = message - super().__init__(f"{pool}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - -class RequestError(PoolError): - """Base exception for PoolErrors that have associated URLs.""" - - def __init__(self, pool: ConnectionPool, url: str | None, message: str) -> None: - self.url = url - super().__init__(pool, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self._message) - - -class SSLError(HTTPError): - """Raised when SSL certificate fails in an HTTPS connection.""" - - -class ProxyError(HTTPError): - """Raised when the connection to a proxy fails.""" - - # The original error is also available as __cause__. - original_error: Exception - - def __init__(self, message: str, error: Exception) -> None: - super().__init__(message, error) - self.original_error = error - - -class DecodeError(HTTPError): - """Raised when automatic decoding based on Content-Type fails.""" - - -class ProtocolError(HTTPError): - """Raised when something unexpected happens mid-request/response.""" - - -#: Renamed to ProtocolError but aliased for backwards compatibility. -ConnectionError = ProtocolError - - -# Leaf Exceptions - - -class MaxRetryError(RequestError): - """Raised when the maximum number of retries is exceeded. - - :param pool: The connection pool - :type pool: :class:`~urllib3.connectionpool.HTTPConnectionPool` - :param str url: The requested Url - :param reason: The underlying error - :type reason: :class:`Exception` - - """ - - def __init__( - self, pool: ConnectionPool, url: str | None, reason: Exception | None = None - ) -> None: - self.reason = reason - - message = f"Max retries exceeded with url: {url} (Caused by {reason!r})" - - super().__init__(pool, url, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self.url, self.reason) - - -class HostChangedError(RequestError): - """Raised when an existing pool gets a request for a foreign host.""" - - def __init__( - self, pool: ConnectionPool, url: str, retries: Retry | int = 3 - ) -> None: - message = f"Tried to open a foreign host with url: {url}" - super().__init__(pool, url, message) - self.retries = retries - - -class TimeoutStateError(HTTPError): - """Raised when passing an invalid state to a timeout""" - - -class TimeoutError(HTTPError): - """Raised when a socket timeout error occurs. - - Catching this error will catch both :exc:`ReadTimeoutErrors - ` and :exc:`ConnectTimeoutErrors `. - """ - - -class ReadTimeoutError(TimeoutError, RequestError): - """Raised when a socket timeout occurs while receiving data from a server""" - - -# This timeout error does not have a URL attached and needs to inherit from the -# base HTTPError -class ConnectTimeoutError(TimeoutError): - """Raised when a socket timeout occurs while connecting to a server""" - - -class NewConnectionError(ConnectTimeoutError, HTTPError): - """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" - - def __init__(self, conn: HTTPConnection, message: str) -> None: - self.conn = conn - self._message = message - super().__init__(f"{conn}: {message}") - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (None, self._message) - - @property - def pool(self) -> HTTPConnection: - warnings.warn( - "The 'pool' property is deprecated and will be removed " - "in urllib3 v3.0. Use 'conn' instead.", - FutureWarning, - stacklevel=2, - ) - - return self.conn - - -class NameResolutionError(NewConnectionError): - """Raised when host name resolution fails.""" - - def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): - message = f"Failed to resolve '{host}' ({reason})" - self._host = host - self._reason = reason - super().__init__(conn, message) - - def __reduce__(self) -> _TYPE_REDUCE_RESULT: - # For pickling purposes. - return self.__class__, (self._host, None, self._reason) - - -class EmptyPoolError(PoolError): - """Raised when a pool runs out of connections and no more are allowed.""" - - -class FullPoolError(PoolError): - """Raised when we try to add a connection to a full pool in blocking mode.""" - - -class ClosedPoolError(PoolError): - """Raised when a request enters a pool after the pool has been closed.""" - - -class LocationValueError(ValueError, HTTPError): - """Raised when there is something wrong with a given URL input.""" - - -class LocationParseError(LocationValueError): - """Raised when get_host or similar fails to parse the URL input.""" - - def __init__(self, location: str) -> None: - message = f"Failed to parse: {location}" - super().__init__(message) - - self.location = location - - -class URLSchemeUnknown(LocationValueError): - """Raised when a URL input has an unsupported scheme.""" - - def __init__(self, scheme: str): - message = f"Not supported URL scheme {scheme}" - super().__init__(message) - - self.scheme = scheme - - -class ResponseError(HTTPError): - """Used as a container for an error reason supplied in a MaxRetryError.""" - - GENERIC_ERROR = "too many error responses" - SPECIFIC_ERROR = "too many {status_code} error responses" - - -class SecurityWarning(HTTPWarning): - """Warned when performing security reducing actions""" - - -class InsecureRequestWarning(SecurityWarning): - """Warned when making an unverified HTTPS request.""" - - -class NotOpenSSLWarning(SecurityWarning): - """Warned when using unsupported SSL library""" - - -class SystemTimeWarning(SecurityWarning): - """Warned when system time is suspected to be wrong""" - - -class InsecurePlatformWarning(SecurityWarning): - """Warned when certain TLS/SSL configuration is not available on a platform.""" - - -class DependencyWarning(HTTPWarning): - """ - Warned when an attempt is made to import a module with missing optional - dependencies. - """ - - -class ResponseNotChunked(ProtocolError, ValueError): - """Response needs to be chunked in order to read it as chunks.""" - - -class BodyNotHttplibCompatible(HTTPError): - """ - Body should be :class:`http.client.HTTPResponse` like - (have an fp attribute which returns raw chunks) for read_chunked(). - """ - - -class IncompleteRead(HTTPError, httplib_IncompleteRead): - """ - Response length doesn't match expected Content-Length - - Subclass of :class:`http.client.IncompleteRead` to allow int value - for ``partial`` to avoid creating large objects on streamed reads. - """ - - partial: int # type: ignore[assignment] - expected: int - - def __init__(self, partial: int, expected: int) -> None: - self.partial = partial - self.expected = expected - - def __repr__(self) -> str: - return "IncompleteRead(%i bytes read, %i more expected)" % ( - self.partial, - self.expected, - ) - - -class InvalidChunkLength(HTTPError, httplib_IncompleteRead): - """Invalid chunk length in a chunked response.""" - - def __init__(self, response: HTTPResponse, length: bytes) -> None: - self.partial: int = response.tell() # type: ignore[assignment] - self.expected: int | None = response.length_remaining - self.response = response - self.length = length - - def __repr__(self) -> str: - return "InvalidChunkLength(got length %r, %i bytes read)" % ( - self.length, - self.partial, - ) - - -class InvalidHeader(HTTPError): - """The header provided was somehow invalid.""" - - -class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): - """ProxyManager does not support the supplied scheme""" - - # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. - - def __init__(self, scheme: str | None) -> None: - # 'localhost' is here because our URL parser parses - # localhost:8080 -> scheme=localhost, remove if we fix this. - if scheme == "localhost": - scheme = None - if scheme is None: - message = "Proxy URL had no scheme, should start with http:// or https://" - else: - message = f"Proxy URL had unsupported scheme {scheme}, should use http:// or https://" - super().__init__(message) - - -class ProxySchemeUnsupported(ValueError): - """Fetching HTTPS resources through HTTPS proxies is unsupported""" - - -class HeaderParsingError(HTTPError): - """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" - - def __init__( - self, defects: list[MessageDefect], unparsed_data: bytes | str | None - ) -> None: - message = f"{defects or 'Unknown'}, unparsed data: {unparsed_data!r}" - super().__init__(message) - - -class UnrewindableBodyError(HTTPError): - """urllib3 encountered an error when trying to rewind a body""" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py deleted file mode 100644 index fe68e17732..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/fields.py +++ /dev/null @@ -1,341 +0,0 @@ -from __future__ import annotations - -import email.utils -import mimetypes -import typing - -_TYPE_FIELD_VALUE = typing.Union[str, bytes] -_TYPE_FIELD_VALUE_TUPLE = typing.Union[ - _TYPE_FIELD_VALUE, - tuple[str, _TYPE_FIELD_VALUE], - tuple[str, _TYPE_FIELD_VALUE, str], -] - - -def guess_content_type( - filename: str | None, default: str = "application/octet-stream" -) -> str: - """ - Guess the "Content-Type" of a file. - - :param filename: - The filename to guess the "Content-Type" of using :mod:`mimetypes`. - :param default: - If no "Content-Type" can be guessed, default to `default`. - """ - if filename: - return mimetypes.guess_type(filename)[0] or default - return default - - -def format_header_param_rfc2231(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Helper function to format and quote a single header parameter using the - strategy defined in RFC 2231. - - Particularly useful for header parameters which might contain - non-ASCII values, like file names. This follows - `RFC 2388 Section 4.4 `_. - - :param name: - The name of the parameter, a string expected to be ASCII only. - :param value: - The value of the parameter, provided as ``bytes`` or `str``. - :returns: - An RFC-2231-formatted unicode string. - - .. deprecated:: 2.0.0 - Will be removed in urllib3 v3.0. This is not valid for - ``multipart/form-data`` header parameters. - """ - import warnings - - warnings.warn( - "'format_header_param_rfc2231' is insecure, deprecated and will be " - "removed in urllib3 v3.0. This is not valid for " - "multipart/form-data header parameters.", - FutureWarning, - stacklevel=2, - ) - - if isinstance(value, bytes): - value = value.decode("utf-8") - - if not any(ch in value for ch in '"\\\r\n'): - result = f'{name}="{value}"' - try: - result.encode("ascii") - except (UnicodeEncodeError, UnicodeDecodeError): - pass - else: - return result - - value = email.utils.encode_rfc2231(value, "utf-8") - value = f"{name}*={value}" - - return value - - -def format_multipart_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Format and quote a single multipart header parameter. - - This follows the `WHATWG HTML Standard`_ as of 2021/06/10, matching - the behavior of current browser and curl versions. Values are - assumed to be UTF-8. The ``\\n``, ``\\r``, and ``"`` characters are - percent encoded. - - .. _WHATWG HTML Standard: - https://html.spec.whatwg.org/multipage/ - form-control-infrastructure.html#multipart-form-data - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - :returns: - A string ``name="value"`` with the escaped value. - - .. versionchanged:: 2.0.0 - Matches the WHATWG HTML Standard as of 2021/06/10. Control - characters are no longer percent encoded. - - .. versionchanged:: 2.0.0 - Renamed from ``format_header_param_html5`` and - ``format_header_param``. The old names will be removed in - urllib3 v3.0. - """ - if isinstance(value, bytes): - value = value.decode("utf-8") - - # percent encode \n \r " - value = value.translate({10: "%0A", 13: "%0D", 34: "%22"}) - return f'{name}="{value}"' - - -def format_header_param_html5(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param_html5' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -def format_header_param(name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - .. deprecated:: 2.0.0 - Renamed to :func:`format_multipart_header_param`. Will be - removed in urllib3 v3.0. - """ - import warnings - - warnings.warn( - "'format_header_param' has been renamed to " - "'format_multipart_header_param'. The old name will be " - "removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - return format_multipart_header_param(name, value) - - -class RequestField: - """ - A data container for request body parameters. - - :param name: - The name of this request field. Must be unicode. - :param data: - The data/value body. - :param filename: - An optional filename of the request field. Must be unicode. - :param headers: - An optional dict-like object of headers to initially use for the field. - - .. versionchanged:: 2.0.0 - The ``header_formatter`` parameter is deprecated and will - be removed in urllib3 v3.0. - """ - - def __init__( - self, - name: str, - data: _TYPE_FIELD_VALUE, - filename: str | None = None, - headers: typing.Mapping[str, str] | None = None, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ): - self._name = name - self._filename = filename - self.data = data - self.headers: dict[str, str | None] = {} - if headers: - self.headers = dict(headers) - - if header_formatter is not None: - import warnings - - warnings.warn( - "The 'header_formatter' parameter is deprecated and " - "will be removed in urllib3 v3.0.", - FutureWarning, - stacklevel=2, - ) - self.header_formatter = header_formatter - else: - self.header_formatter = format_multipart_header_param - - @classmethod - def from_tuples( - cls, - fieldname: str, - value: _TYPE_FIELD_VALUE_TUPLE, - header_formatter: typing.Callable[[str, _TYPE_FIELD_VALUE], str] | None = None, - ) -> RequestField: - """ - A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. - - Supports constructing :class:`~urllib3.fields.RequestField` from - parameter of key/value strings AND key/filetuple. A filetuple is a - (filename, data, MIME type) tuple where the MIME type is optional. - For example:: - - 'foo': 'bar', - 'fakefile': ('foofile.txt', 'contents of foofile'), - 'realfile': ('barfile.txt', open('realfile').read()), - 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), - 'nonamefile': 'contents of nonamefile field', - - Field names and filenames must be unicode. - """ - filename: str | None - content_type: str | None - data: _TYPE_FIELD_VALUE - - if isinstance(value, tuple): - if len(value) == 3: - filename, data, content_type = value - else: - filename, data = value - content_type = guess_content_type(filename) - else: - filename = None - content_type = None - data = value - - request_param = cls( - fieldname, data, filename=filename, header_formatter=header_formatter - ) - request_param.make_multipart(content_type=content_type) - - return request_param - - def _render_part(self, name: str, value: _TYPE_FIELD_VALUE) -> str: - """ - Override this method to change how each multipart header - parameter is formatted. By default, this calls - :func:`format_multipart_header_param`. - - :param name: - The name of the parameter, an ASCII-only ``str``. - :param value: - The value of the parameter, a ``str`` or UTF-8 encoded - ``bytes``. - - :meta public: - """ - return self.header_formatter(name, value) - - def _render_parts( - self, - header_parts: ( - dict[str, _TYPE_FIELD_VALUE | None] - | typing.Sequence[tuple[str, _TYPE_FIELD_VALUE | None]] - ), - ) -> str: - """ - Helper function to format and quote a single header. - - Useful for single headers that are composed of multiple items. E.g., - 'Content-Disposition' fields. - - :param header_parts: - A sequence of (k, v) tuples or a :class:`dict` of (k, v) to format - as `k1="v1"; k2="v2"; ...`. - """ - iterable: typing.Iterable[tuple[str, _TYPE_FIELD_VALUE | None]] - - parts = [] - if isinstance(header_parts, dict): - iterable = header_parts.items() - else: - iterable = header_parts - - for name, value in iterable: - if value is not None: - parts.append(self._render_part(name, value)) - - return "; ".join(parts) - - def render_headers(self) -> str: - """ - Renders the headers for this request field. - """ - lines = [] - - sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] - for sort_key in sort_keys: - if self.headers.get(sort_key, False): - lines.append(f"{sort_key}: {self.headers[sort_key]}") - - for header_name, header_value in self.headers.items(): - if header_name not in sort_keys: - if header_value: - lines.append(f"{header_name}: {header_value}") - - lines.append("\r\n") - return "\r\n".join(lines) - - def make_multipart( - self, - content_disposition: str | None = None, - content_type: str | None = None, - content_location: str | None = None, - ) -> None: - """ - Makes this request field into a multipart request field. - - This method overrides "Content-Disposition", "Content-Type" and - "Content-Location" headers to the request parameter. - - :param content_disposition: - The 'Content-Disposition' of the request body. Defaults to 'form-data' - :param content_type: - The 'Content-Type' of the request body. - :param content_location: - The 'Content-Location' of the request body. - - """ - content_disposition = (content_disposition or "form-data") + "; ".join( - [ - "", - self._render_parts( - (("name", self._name), ("filename", self._filename)) - ), - ] - ) - - self.headers["Content-Disposition"] = content_disposition - self.headers["Content-Type"] = content_type - self.headers["Content-Location"] = content_location diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py deleted file mode 100644 index 14f70b05b4..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/filepost.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -import binascii -import codecs -import os -import typing -from io import BytesIO - -from .fields import _TYPE_FIELD_VALUE_TUPLE, RequestField - -writer = codecs.lookup("utf-8")[3] - -_TYPE_FIELDS_SEQUENCE = typing.Sequence[ - typing.Union[tuple[str, _TYPE_FIELD_VALUE_TUPLE], RequestField] -] -_TYPE_FIELDS = typing.Union[ - _TYPE_FIELDS_SEQUENCE, - typing.Mapping[str, _TYPE_FIELD_VALUE_TUPLE], -] - - -def choose_boundary() -> str: - """ - Our embarrassingly-simple replacement for mimetools.choose_boundary. - """ - return binascii.hexlify(os.urandom(16)).decode() - - -def iter_field_objects(fields: _TYPE_FIELDS) -> typing.Iterable[RequestField]: - """ - Iterate over fields. - - Supports list of (k, v) tuples and dicts, and lists of - :class:`~urllib3.fields.RequestField`. - - """ - iterable: typing.Iterable[RequestField | tuple[str, _TYPE_FIELD_VALUE_TUPLE]] - - if isinstance(fields, typing.Mapping): - iterable = fields.items() - else: - iterable = fields - - for field in iterable: - if isinstance(field, RequestField): - yield field - else: - yield RequestField.from_tuples(*field) - - -def encode_multipart_formdata( - fields: _TYPE_FIELDS, boundary: str | None = None -) -> tuple[bytes, str]: - """ - Encode a dictionary of ``fields`` using the multipart/form-data MIME format. - - :param fields: - Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). - Values are processed by :func:`urllib3.fields.RequestField.from_tuples`. - - :param boundary: - If not specified, then a random boundary will be generated using - :func:`urllib3.filepost.choose_boundary`. - """ - body = BytesIO() - if boundary is None: - boundary = choose_boundary() - - for field in iter_field_objects(fields): - body.write(f"--{boundary}\r\n".encode("latin-1")) - - writer(body).write(field.render_headers()) - data = field.data - - if isinstance(data, int): - data = str(data) # Backwards compatibility - - if isinstance(data, str): - writer(body).write(data) - else: - body.write(data) - - body.write(b"\r\n") - - body.write(f"--{boundary}--\r\n".encode("latin-1")) - - content_type = f"multipart/form-data; boundary={boundary}" - - return body.getvalue(), content_type diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py deleted file mode 100644 index 133e1d8f23..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from importlib.metadata import version - -__all__ = [ - "inject_into_urllib3", - "extract_from_urllib3", -] - -import typing - -orig_HTTPSConnection: typing.Any = None - - -def inject_into_urllib3() -> None: - # First check if h2 version is valid - h2_version = version("h2") - if not h2_version.startswith("4."): - raise ImportError( - "urllib3 v2 supports h2 version 4.x.x, currently " - f"the 'h2' module is compiled with {h2_version!r}. " - "See: https://github.com/urllib3/urllib3/issues/3290" - ) - - # Import here to avoid circular dependencies. - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - from .connection import HTTP2Connection - - global orig_HTTPSConnection - orig_HTTPSConnection = urllib3_connection.HTTPSConnection - - HTTPSConnectionPool.ConnectionCls = HTTP2Connection - urllib3_connection.HTTPSConnection = HTTP2Connection # type: ignore[misc] - - # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy. - urllib3_util.ALPN_PROTOCOLS = ["h2"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"] - - -def extract_from_urllib3() -> None: - from .. import connection as urllib3_connection - from .. import util as urllib3_util - from ..connectionpool import HTTPSConnectionPool - from ..util import ssl_ as urllib3_util_ssl - - HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection - urllib3_connection.HTTPSConnection = orig_HTTPSConnection # type: ignore[misc] - - urllib3_util.ALPN_PROTOCOLS = ["http/1.1"] - urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py deleted file mode 100644 index 0a026da0a8..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/connection.py +++ /dev/null @@ -1,356 +0,0 @@ -from __future__ import annotations - -import logging -import re -import threading -import types -import typing - -import h2.config -import h2.connection -import h2.events - -from .._base_connection import _TYPE_BODY -from .._collections import HTTPHeaderDict -from ..connection import HTTPSConnection, _get_default_user_agent -from ..exceptions import ConnectionError -from ..response import BaseHTTPResponse - -orig_HTTPSConnection = HTTPSConnection - -T = typing.TypeVar("T") - -log = logging.getLogger(__name__) - -RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$") -RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$") - - -def _is_legal_header_name(name: bytes) -> bool: - """ - "An implementation that validates fields according to the definitions in Sections - 5.1 and 5.5 of [HTTP] only needs an additional check that field names do not - include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - - `http.client._is_legal_header_name` does not validate the field name according to the - HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters. - - This does not allow for the `:` character in the header name, so should not - be used to validate pseudo-headers. - """ - return bool(RE_IS_LEGAL_HEADER_NAME.match(name)) - - -def _is_illegal_header_value(value: bytes) -> bool: - """ - "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed - (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field - value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB, - 0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity) - """ - return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value)) - - -class _LockedObject(typing.Generic[T]): - """ - A wrapper class that hides a specific object behind a lock. - The goal here is to provide a simple way to protect access to an object - that cannot safely be simultaneously accessed from multiple threads. The - intended use of this class is simple: take hold of it with a context - manager, which returns the protected object. - """ - - __slots__ = ( - "lock", - "_obj", - ) - - def __init__(self, obj: T): - self.lock = threading.RLock() - self._obj = obj - - def __enter__(self) -> T: - self.lock.acquire() - return self._obj - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> None: - self.lock.release() - - -class HTTP2Connection(HTTPSConnection): - def __init__( - self, host: str, port: int | None = None, **kwargs: typing.Any - ) -> None: - self._h2_conn = self._new_h2_conn() - self._h2_stream: int | None = None - self._headers: list[tuple[bytes, bytes]] = [] - - if "proxy" in kwargs or "proxy_config" in kwargs: # Defensive: - raise NotImplementedError("Proxies aren't supported with HTTP/2") - - super().__init__(host, port, **kwargs) - - if self._tunnel_host is not None: - raise NotImplementedError("Tunneling isn't supported with HTTP/2") - - def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]: - config = h2.config.H2Configuration(client_side=True) - return _LockedObject(h2.connection.H2Connection(config=config)) - - def connect(self) -> None: - super().connect() - with self._h2_conn as conn: - conn.initiate_connection() - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - def putrequest( # type: ignore[override] - self, - method: str, - url: str, - **kwargs: typing.Any, - ) -> None: - """putrequest - This deviates from the HTTPConnection method signature since we never need to override - sending accept-encoding headers or the host header. - """ - if "skip_host" in kwargs: - raise NotImplementedError("`skip_host` isn't supported") - if "skip_accept_encoding" in kwargs: - raise NotImplementedError("`skip_accept_encoding` isn't supported") - - self._request_url = url or "/" - self._validate_path(url) # type: ignore[attr-defined] - - if ":" in self.host: - authority = f"[{self.host}]:{self.port or 443}" - else: - authority = f"{self.host}:{self.port or 443}" - - self._headers.append((b":scheme", b"https")) - self._headers.append((b":method", method.encode())) - self._headers.append((b":authority", authority.encode())) - self._headers.append((b":path", url.encode())) - - with self._h2_conn as conn: - self._h2_stream = conn.get_next_available_stream_id() - - def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override] - # TODO SKIPPABLE_HEADERS from urllib3 are ignored. - header = header.encode() if isinstance(header, str) else header - header = header.lower() # A lot of upstream code uses capitalized headers. - if not _is_legal_header_name(header): - raise ValueError(f"Illegal header name {str(header)}") - - for value in values: - value = value.encode() if isinstance(value, str) else value - if _is_illegal_header_value(value): - raise ValueError(f"Illegal header value {str(value)}") - self._headers.append((header, value)) - - def endheaders(self, message_body: typing.Any = None) -> None: # type: ignore[override] - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - conn.send_headers( - stream_id=self._h2_stream, - headers=self._headers, - end_stream=(message_body is None), - ) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - self._headers = [] # Reset headers for the next request. - - def send(self, data: typing.Any) -> None: - """Send data to the server. - `data` can be: `str`, `bytes`, an iterable, or file-like objects - that support a .read() method. - """ - if self._h2_stream is None: - raise ConnectionError("Must call `putrequest` first.") - - with self._h2_conn as conn: - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - if hasattr(data, "read"): # file-like objects - while True: - chunk = data.read(self.blocksize) - if not chunk: - break - if isinstance(chunk, str): - chunk = chunk.encode() - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - return - - if isinstance(data, str): # str -> bytes - data = data.encode() - - try: - if isinstance(data, bytes): - conn.send_data(self._h2_stream, data, end_stream=True) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - else: - for chunk in data: - conn.send_data(self._h2_stream, chunk, end_stream=False) - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - conn.end_stream(self._h2_stream) - except TypeError: - raise TypeError( - "`data` should be str, bytes, iterable, or file. got %r" - % type(data) - ) - - def set_tunnel( - self, - host: str, - port: int | None = None, - headers: typing.Mapping[str, str] | None = None, - scheme: str = "http", - ) -> None: - raise NotImplementedError( - "HTTP/2 does not support setting up a tunnel through a proxy" - ) - - def getresponse( # type: ignore[override] - self, - ) -> HTTP2Response: - status = None - data = bytearray() - with self._h2_conn as conn: - end_stream = False - while not end_stream: - # TODO: Arbitrary read value. - if received_data := self.sock.recv(65535): - events = conn.receive_data(received_data) - for event in events: - if isinstance(event, h2.events.ResponseReceived): - headers = HTTPHeaderDict() - for header, value in event.headers: - if header == b":status": - status = int(value.decode()) - else: - headers.add( - header.decode("ascii"), value.decode("ascii") - ) - - elif isinstance(event, h2.events.DataReceived): - data += event.data - conn.acknowledge_received_data( - event.flow_controlled_length, event.stream_id - ) - - elif isinstance(event, h2.events.StreamEnded): - end_stream = True - - if data_to_send := conn.data_to_send(): - self.sock.sendall(data_to_send) - - assert status is not None - return HTTP2Response( - status=status, - headers=headers, - request_url=self._request_url, - data=bytes(data), - ) - - def request( # type: ignore[override] - self, - method: str, - url: str, - body: _TYPE_BODY | None = None, - headers: typing.Mapping[str, str] | None = None, - *, - preload_content: bool = True, - decode_content: bool = True, - enforce_content_length: bool = True, - **kwargs: typing.Any, - ) -> None: - """Send an HTTP/2 request""" - if "chunked" in kwargs: - # TODO this is often present from upstream. - # raise NotImplementedError("`chunked` isn't supported with HTTP/2") - pass - - if self.sock is not None: - self.sock.settimeout(self.timeout) - - self.putrequest(method, url) - - headers = headers or {} - for k, v in headers.items(): - if k.lower() == "transfer-encoding" and v == "chunked": - continue - else: - self.putheader(k, v) - - if b"user-agent" not in dict(self._headers): - self.putheader(b"user-agent", _get_default_user_agent()) - - if body: - self.endheaders(message_body=body) - self.send(body) - else: - self.endheaders() - - def close(self) -> None: - with self._h2_conn as conn: - try: - conn.close_connection() - if data := conn.data_to_send(): - self.sock.sendall(data) - except Exception: - pass - - # Reset all our HTTP/2 connection state. - self._h2_conn = self._new_h2_conn() - self._h2_stream = None - self._headers = [] - - super().close() - - -class HTTP2Response(BaseHTTPResponse): - # TODO: This is a woefully incomplete response object, but works for non-streaming. - def __init__( - self, - status: int, - headers: HTTPHeaderDict, - request_url: str, - data: bytes, - decode_content: bool = False, # TODO: support decoding - ) -> None: - super().__init__( - status=status, - headers=headers, - # Following CPython, we map HTTP versions to major * 10 + minor integers - version=20, - version_string="HTTP/2", - # No reason phrase in HTTP/2 - reason=None, - decode_content=decode_content, - request_url=request_url, - ) - self._data = data - self.length_remaining = 0 - - @property - def data(self) -> bytes: - return self._data - - def get_redirect_location(self) -> None: - return None - - def close(self) -> None: - pass diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py deleted file mode 100644 index 9ea900764f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/http2/probe.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import threading - - -class _HTTP2ProbeCache: - __slots__ = ( - "_lock", - "_cache_locks", - "_cache_values", - ) - - def __init__(self) -> None: - self._lock = threading.Lock() - self._cache_locks: dict[tuple[str, int], threading.RLock] = {} - self._cache_values: dict[tuple[str, int], bool | None] = {} - - def acquire_and_get(self, host: str, port: int) -> bool | None: - # By the end of this block we know that - # _cache_[values,locks] is available. - value = None - with self._lock: - key = (host, port) - try: - value = self._cache_values[key] - # If it's a known value we return right away. - if value is not None: - return value - except KeyError: - self._cache_locks[key] = threading.RLock() - self._cache_values[key] = None - - # If the value is unknown, we acquire the lock to signal - # to the requesting thread that the probe is in progress - # or that the current thread needs to return their findings. - key_lock = self._cache_locks[key] - key_lock.acquire() - try: - # If the by the time we get the lock the value has been - # updated we want to return the updated value. - value = self._cache_values[key] - - # In case an exception like KeyboardInterrupt is raised here. - except BaseException as e: # Defensive: - assert not isinstance(e, KeyError) # KeyError shouldn't be possible. - key_lock.release() - raise - - return value - - def set_and_release( - self, host: str, port: int, supports_http2: bool | None - ) -> None: - key = (host, port) - key_lock = self._cache_locks[key] - with key_lock: # Uses an RLock, so can be locked again from same thread. - if supports_http2 is None and self._cache_values[key] is not None: - raise ValueError( - "Cannot reset HTTP/2 support for origin after value has been set." - ) # Defensive: not expected in normal usage - - self._cache_values[key] = supports_http2 - key_lock.release() - - def _values(self) -> dict[tuple[str, int], bool | None]: - """This function is for testing purposes only. Gets the current state of the probe cache""" - with self._lock: - return {k: v for k, v in self._cache_values.items()} - - def _reset(self) -> None: - """This function is for testing purposes only. Reset the cache values""" - with self._lock: - self._cache_locks = {} - self._cache_values = {} - - -_HTTP2_PROBE_CACHE = _HTTP2ProbeCache() - -set_and_release = _HTTP2_PROBE_CACHE.set_and_release -acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get -_values = _HTTP2_PROBE_CACHE._values -_reset = _HTTP2_PROBE_CACHE._reset - -__all__ = [ - "set_and_release", - "acquire_and_get", -] diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py deleted file mode 100644 index 8f2c56745c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/poolmanager.py +++ /dev/null @@ -1,653 +0,0 @@ -from __future__ import annotations - -import functools -import logging -import typing -import warnings -from types import TracebackType -from urllib.parse import urljoin - -from ._collections import HTTPHeaderDict, RecentlyUsedContainer -from ._request_methods import RequestMethods -from .connection import ProxyConfig -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme -from .exceptions import ( - LocationValueError, - MaxRetryError, - ProxySchemeUnknown, - URLSchemeUnknown, -) -from .response import BaseHTTPResponse -from .util.connection import _TYPE_SOCKET_OPTIONS -from .util.proxy import connection_requires_http_tunnel -from .util.retry import Retry -from .util.timeout import Timeout -from .util.url import Url, parse_url - -if typing.TYPE_CHECKING: - import ssl - - from typing_extensions import Self - -__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] - - -log = logging.getLogger(__name__) - -SSL_KEYWORDS = ( - "key_file", - "cert_file", - "cert_reqs", - "ca_certs", - "ca_cert_data", - "ssl_version", - "ssl_minimum_version", - "ssl_maximum_version", - "ca_cert_dir", - "ssl_context", - "key_password", - "server_hostname", -) -# Default value for `blocksize` - a new parameter introduced to -# http.client.HTTPConnection & http.client.HTTPSConnection in Python 3.7 -_DEFAULT_BLOCKSIZE = 16384 - - -class PoolKey(typing.NamedTuple): - """ - All known keyword arguments that could be provided to the pool manager, its - pools, or the underlying connections. - - All custom key schemes should include the fields in this key at a minimum. - """ - - key_scheme: str - key_host: str - key_port: int | None - key_timeout: Timeout | float | int | None - key_retries: Retry | bool | int | None - key_block: bool | None - key_source_address: tuple[str, int] | None - key_key_file: str | None - key_key_password: str | None - key_cert_file: str | None - key_cert_reqs: str | None - key_ca_certs: str | None - key_ca_cert_data: str | bytes | None - key_ssl_version: int | str | None - key_ssl_minimum_version: ssl.TLSVersion | None - key_ssl_maximum_version: ssl.TLSVersion | None - key_ca_cert_dir: str | None - key_ssl_context: ssl.SSLContext | None - key_maxsize: int | None - key_headers: frozenset[tuple[str, str]] | None - key__proxy: Url | None - key__proxy_headers: frozenset[tuple[str, str]] | None - key__proxy_config: ProxyConfig | None - key_socket_options: _TYPE_SOCKET_OPTIONS | None - key__socks_options: frozenset[tuple[str, str]] | None - key_assert_hostname: bool | str | None - key_assert_fingerprint: str | None - key_server_hostname: str | None - key_blocksize: int | None - - -def _default_key_normalizer( - key_class: type[PoolKey], request_context: dict[str, typing.Any] -) -> PoolKey: - """ - Create a pool key out of a request context dictionary. - - According to RFC 3986, both the scheme and host are case-insensitive. - Therefore, this function normalizes both before constructing the pool - key for an HTTPS request. If you wish to change this behaviour, provide - alternate callables to ``key_fn_by_scheme``. - - :param key_class: - The class to use when constructing the key. This should be a namedtuple - with the ``scheme`` and ``host`` keys at a minimum. - :type key_class: namedtuple - :param request_context: - A dictionary-like object that contain the context for a request. - :type request_context: dict - - :return: A namedtuple that can be used as a connection pool key. - :rtype: PoolKey - """ - # Since we mutate the dictionary, make a copy first - context = request_context.copy() - context["scheme"] = context["scheme"].lower() - context["host"] = context["host"].lower() - - # These are both dictionaries and need to be transformed into frozensets - for key in ("headers", "_proxy_headers", "_socks_options"): - if key in context and context[key] is not None: - context[key] = frozenset(context[key].items()) - - # The socket_options key may be a list and needs to be transformed into a - # tuple. - socket_opts = context.get("socket_options") - if socket_opts is not None: - context["socket_options"] = tuple(socket_opts) - - # Map the kwargs to the names in the namedtuple - this is necessary since - # namedtuples can't have fields starting with '_'. - for key in list(context.keys()): - context["key_" + key] = context.pop(key) - - # Default to ``None`` for keys missing from the context - for field in key_class._fields: - if field not in context: - context[field] = None - - # Default key_blocksize to _DEFAULT_BLOCKSIZE if missing from the context - if context.get("key_blocksize") is None: - context["key_blocksize"] = _DEFAULT_BLOCKSIZE - - return key_class(**context) - - -#: A dictionary that maps a scheme to a callable that creates a pool key. -#: This can be used to alter the way pool keys are constructed, if desired. -#: Each PoolManager makes a copy of this dictionary so they can be configured -#: globally here, or individually on the instance. -key_fn_by_scheme = { - "http": functools.partial(_default_key_normalizer, PoolKey), - "https": functools.partial(_default_key_normalizer, PoolKey), -} - -pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} - - -class PoolManager(RequestMethods): - """ - Allows for arbitrary requests while transparently keeping track of - necessary connection pools for you. - - :param num_pools: - Number of connection pools to cache before discarding the least - recently used pool. - - :param headers: - Headers to include with all requests, unless other headers are given - explicitly. - - :param \\**connection_pool_kw: - Additional parameters are used to create fresh - :class:`urllib3.connectionpool.ConnectionPool` instances. - - Example: - - .. code-block:: python - - import urllib3 - - http = urllib3.PoolManager(num_pools=2) - - resp1 = http.request("GET", "https://google.com/") - resp2 = http.request("GET", "https://google.com/mail") - resp3 = http.request("GET", "https://yahoo.com/") - - print(len(http.pools)) - # 2 - - """ - - proxy: Url | None = None - proxy_config: ProxyConfig | None = None - - def __init__( - self, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - super().__init__(headers) - # PoolManager handles redirects itself in PoolManager.urlopen(). - # It always passes redirect=False to the underlying connection pool to - # suppress per-pool redirect handling. If the user supplied a non-Retry - # value (int/bool/etc) for retries and we let the pool normalize it - # while redirect=False, the resulting Retry object would have redirect - # handling disabled, which can interfere with PoolManager's own - # redirect logic. Normalize here so redirects remain governed solely by - # PoolManager logic. - if "retries" in connection_pool_kw: - retries = connection_pool_kw["retries"] - if not isinstance(retries, Retry): - retries = Retry.from_int(retries) - connection_pool_kw = connection_pool_kw.copy() - connection_pool_kw["retries"] = retries - self.connection_pool_kw = connection_pool_kw - - self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] - self.pools = RecentlyUsedContainer(num_pools) - - # Locally set the pool classes and keys so other PoolManagers can - # override them. - self.pool_classes_by_scheme = pool_classes_by_scheme - self.key_fn_by_scheme = key_fn_by_scheme.copy() - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> typing.Literal[False]: - self.clear() - # Return False to re-raise any potential exceptions - return False - - def _new_pool( - self, - scheme: str, - host: str, - port: int, - request_context: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and - any additional pool keyword arguments. - - If ``request_context`` is provided, it is provided as keyword arguments - to the pool class used. This method is used to actually create the - connection pools handed out by :meth:`connection_from_url` and - companion methods. It is intended to be overridden for customization. - """ - pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme] - if request_context is None: - request_context = self.connection_pool_kw.copy() - - # Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly - # set to 'None' in the request_context. - if request_context.get("blocksize") is None: - request_context["blocksize"] = _DEFAULT_BLOCKSIZE - - # Although the context has everything necessary to create the pool, - # this function has historically only used the scheme, host, and port - # in the positional args. When an API change is acceptable these can - # be removed. - for key in ("scheme", "host", "port"): - request_context.pop(key, None) - - if scheme == "http": - for kw in SSL_KEYWORDS: - request_context.pop(kw, None) - - return pool_cls(host, port, **request_context) - - def clear(self) -> None: - """ - Empty our store of pools and direct them all to close. - - This will not affect in-flight connections, but they will not be - re-used after completion. - """ - self.pools.clear() - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. - - If ``port`` isn't given, it will be derived from the ``scheme`` using - ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is - provided, it is merged with the instance's ``connection_pool_kw`` - variable and used to create the new connection pool, if one is - needed. - """ - - if not host: - raise LocationValueError("No host specified.") - - request_context = self._merge_pool_kwargs(pool_kwargs) - request_context["scheme"] = scheme or "http" - if not port: - port = port_by_scheme.get(request_context["scheme"].lower(), 80) - request_context["port"] = port - request_context["host"] = host - - return self.connection_from_context(request_context) - - def connection_from_context( - self, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. - - ``request_context`` must at least contain the ``scheme`` key and its - value must be a key in ``key_fn_by_scheme`` instance variable. - """ - if "strict" in request_context: - warnings.warn( - "The 'strict' parameter is no longer needed on Python 3+. " - "This will raise an error in urllib3 v3.0.", - FutureWarning, - ) - request_context.pop("strict") - - scheme = request_context["scheme"].lower() - pool_key_constructor = self.key_fn_by_scheme.get(scheme) - if not pool_key_constructor: - raise URLSchemeUnknown(scheme) - pool_key = pool_key_constructor(request_context) - - return self.connection_from_pool_key(pool_key, request_context=request_context) - - def connection_from_pool_key( - self, pool_key: PoolKey, request_context: dict[str, typing.Any] - ) -> HTTPConnectionPool: - """ - Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. - - ``pool_key`` should be a namedtuple that only contains immutable - objects. At a minimum it must have the ``scheme``, ``host``, and - ``port`` fields. - """ - with self.pools.lock: - # If the scheme, host, or port doesn't match existing open - # connections, open a new ConnectionPool. - pool = self.pools.get(pool_key) - if pool: - return pool - - # Make a fresh ConnectionPool of the desired type - scheme = request_context["scheme"] - host = request_context["host"] - port = request_context["port"] - pool = self._new_pool(scheme, host, port, request_context=request_context) - self.pools[pool_key] = pool - - return pool - - def connection_from_url( - self, url: str, pool_kwargs: dict[str, typing.Any] | None = None - ) -> HTTPConnectionPool: - """ - Similar to :func:`urllib3.connectionpool.connection_from_url`. - - If ``pool_kwargs`` is not provided and a new pool needs to be - constructed, ``self.connection_pool_kw`` is used to initialize - the :class:`urllib3.connectionpool.ConnectionPool`. If ``pool_kwargs`` - is provided, it is used instead. Note that if a new pool does not - need to be created for the request, the provided ``pool_kwargs`` are - not used. - """ - u = parse_url(url) - return self.connection_from_host( - u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs - ) - - def _merge_pool_kwargs( - self, override: dict[str, typing.Any] | None - ) -> dict[str, typing.Any]: - """ - Merge a dictionary of override values for self.connection_pool_kw. - - This does not modify self.connection_pool_kw and returns a new dict. - Any keys in the override dictionary with a value of ``None`` are - removed from the merged dictionary. - """ - base_pool_kwargs = self.connection_pool_kw.copy() - if override: - for key, value in override.items(): - if value is None: - try: - del base_pool_kwargs[key] - except KeyError: - pass - else: - base_pool_kwargs[key] = value - return base_pool_kwargs - - def _proxy_requires_url_absolute_form(self, parsed_url: Url) -> bool: - """ - Indicates if the proxy requires the complete destination URL in the - request. Normally this is only needed when not using an HTTP CONNECT - tunnel. - """ - if self.proxy is None: - return False - - return not connection_requires_http_tunnel( - self.proxy, self.proxy_config, parsed_url.scheme - ) - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - """ - Same as :meth:`urllib3.HTTPConnectionPool.urlopen` - with custom cross-host redirect logic and only sends the request-uri - portion of the ``url``. - - The given ``url`` parameter must be absolute, such that an appropriate - :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. - """ - u = parse_url(url) - - if u.scheme is None: - warnings.warn( - "URLs without a scheme (ie 'https://') are deprecated and will raise an error " - "in urllib3 v3.0. To avoid this FutureWarning ensure all URLs " - "start with 'https://' or 'http://'. Read more in this issue: " - "https://github.com/urllib3/urllib3/issues/2920", - category=FutureWarning, - stacklevel=2, - ) - - conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - - kw["assert_same_host"] = False - kw["redirect"] = False - - if "headers" not in kw: - kw["headers"] = self.headers - - if self._proxy_requires_url_absolute_form(u): - response = conn.urlopen(method, url, **kw) - else: - response = conn.urlopen(method, u.request_uri, **kw) - - redirect_location = redirect and response.get_redirect_location() - if not redirect_location: - return response - - # Support relative URLs for redirecting. - redirect_location = urljoin(url, redirect_location) - - if response.status == 303: - # Change the method according to RFC 9110, Section 15.4.4. - method = "GET" - # And lose the body not to transfer anything sensitive. - kw["body"] = None - kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() - - retries = kw.get("retries", response.retries) - if not isinstance(retries, Retry): - retries = Retry.from_int(retries, redirect=redirect) - - # Strip headers marked as unsafe to forward to the redirected location. - # Check remove_headers_on_redirect to avoid a potential network call within - # conn.is_same_host() which may use socket.gethostbyname() in the future. - if retries.remove_headers_on_redirect and not conn.is_same_host( - redirect_location - ): - new_headers = kw["headers"].copy() - for header in kw["headers"]: - if header.lower() in retries.remove_headers_on_redirect: - new_headers.pop(header, None) - kw["headers"] = new_headers - - try: - retries = retries.increment(method, url, response=response, _pool=conn) - except MaxRetryError: - if retries.raise_on_redirect: - response.drain_conn() - raise - return response - - kw["retries"] = retries - kw["redirect"] = redirect - - log.info("Redirecting %s -> %s", url, redirect_location) - - response.drain_conn() - return self.urlopen(method, redirect_location, **kw) - - -class ProxyManager(PoolManager): - """ - Behaves just like :class:`PoolManager`, but sends all requests through - the defined proxy, using the CONNECT method for HTTPS URLs. - - :param proxy_url: - The URL of the proxy to be used. - - :param proxy_headers: - A dictionary containing headers that will be sent to the proxy. In case - of HTTP they are being sent with each request, while in the - HTTPS/CONNECT case they are sent only once. Could be used for proxy - authentication. - - :param proxy_ssl_context: - The proxy SSL context is used to establish the TLS connection to the - proxy when using HTTPS proxies. - - :param use_forwarding_for_https: - (Defaults to False) If set to True will forward requests to the HTTPS - proxy to be made on behalf of the client instead of creating a TLS - tunnel via the CONNECT method. **Enabling this flag means that request - and response headers and content will be visible from the HTTPS proxy** - whereas tunneling keeps request and response headers and content - private. IP address, target hostname, SNI, and port are always visible - to an HTTPS proxy even when this flag is disabled. - - :param proxy_assert_hostname: - The hostname of the certificate to verify against. - - :param proxy_assert_fingerprint: - The fingerprint of the certificate to verify against. - - Example: - - .. code-block:: python - - import urllib3 - - proxy = urllib3.ProxyManager("https://localhost:3128/") - - resp1 = proxy.request("GET", "http://google.com/") - resp2 = proxy.request("GET", "http://httpbin.org/") - - # One pool was shared by both plain HTTP requests. - print(len(proxy.pools)) - # 1 - - resp3 = proxy.request("GET", "https://httpbin.org/") - resp4 = proxy.request("GET", "https://twitter.com/") - - # A separate pool was added for each HTTPS target. - print(len(proxy.pools)) - # 3 - - """ - - def __init__( - self, - proxy_url: str, - num_pools: int = 10, - headers: typing.Mapping[str, str] | None = None, - proxy_headers: typing.Mapping[str, str] | None = None, - proxy_ssl_context: ssl.SSLContext | None = None, - use_forwarding_for_https: bool = False, - proxy_assert_hostname: None | str | typing.Literal[False] = None, - proxy_assert_fingerprint: str | None = None, - **connection_pool_kw: typing.Any, - ) -> None: - if isinstance(proxy_url, HTTPConnectionPool): - str_proxy_url = f"{proxy_url.scheme}://{proxy_url.host}:{proxy_url.port}" - else: - str_proxy_url = proxy_url - proxy = parse_url(str_proxy_url) - - if proxy.scheme not in ("http", "https"): - raise ProxySchemeUnknown(proxy.scheme) - - if not proxy.port: - port = port_by_scheme.get(proxy.scheme, 80) - proxy = proxy._replace(port=port) - - self.proxy = proxy - self.proxy_headers = proxy_headers or {} - self.proxy_ssl_context = proxy_ssl_context - self.proxy_config = ProxyConfig( - proxy_ssl_context, - use_forwarding_for_https, - proxy_assert_hostname, - proxy_assert_fingerprint, - ) - - connection_pool_kw["_proxy"] = self.proxy - connection_pool_kw["_proxy_headers"] = self.proxy_headers - connection_pool_kw["_proxy_config"] = self.proxy_config - - super().__init__(num_pools, headers, **connection_pool_kw) - - def connection_from_host( - self, - host: str | None, - port: int | None = None, - scheme: str | None = "http", - pool_kwargs: dict[str, typing.Any] | None = None, - ) -> HTTPConnectionPool: - if scheme == "https": - return super().connection_from_host( - host, port, scheme, pool_kwargs=pool_kwargs - ) - - return super().connection_from_host( - self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs # type: ignore[union-attr] - ) - - def _set_proxy_headers( - self, url: str, headers: typing.Mapping[str, str] | None = None - ) -> typing.Mapping[str, str]: - """ - Sets headers needed by proxies: specifically, the Accept and Host - headers. Only sets headers not provided by the user. - """ - headers_ = {"Accept": "*/*"} - - netloc = parse_url(url).netloc - if netloc: - headers_["Host"] = netloc - - if headers: - headers_.update(headers) - return headers_ - - def urlopen( # type: ignore[override] - self, method: str, url: str, redirect: bool = True, **kw: typing.Any - ) -> BaseHTTPResponse: - "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." - u = parse_url(url) - if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): - # For connections using HTTP CONNECT, httplib sets the necessary - # headers on the CONNECT to the proxy. If we're not using CONNECT, - # we'll definitely need to set 'Host' at the very least. - headers = kw.get("headers", self.headers) - kw["headers"] = self._set_proxy_headers(url, headers) - - return super().urlopen(method, url, redirect=redirect, **kw) - - -def proxy_from_url(url: str, **kw: typing.Any) -> ProxyManager: - return ProxyManager(proxy_url=url, **kw) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed deleted file mode 100644 index 5f3ea3d919..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/py.typed +++ /dev/null @@ -1,2 +0,0 @@ -# Instruct type checkers to look for inline type annotations in this package. -# See PEP 561. diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py deleted file mode 100644 index e9246b75e3..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/response.py +++ /dev/null @@ -1,1493 +0,0 @@ -from __future__ import annotations - -import collections -import io -import json as _json -import logging -import socket -import sys -import typing -import warnings -import zlib -from contextlib import contextmanager -from http.client import HTTPMessage as _HttplibHTTPMessage -from http.client import HTTPResponse as _HttplibHTTPResponse -from socket import timeout as SocketTimeout - -if typing.TYPE_CHECKING: - from ._base_connection import BaseHTTPConnection - -try: - try: - import brotlicffi as brotli # type: ignore[import-not-found] - except ImportError: - import brotli # type: ignore[import-not-found] -except ImportError: - brotli = None - -from . import util -from ._base_connection import _TYPE_BODY -from ._collections import HTTPHeaderDict -from .connection import BaseSSLError, HTTPConnection, HTTPException -from .exceptions import ( - BodyNotHttplibCompatible, - DecodeError, - DependencyWarning, - HTTPError, - IncompleteRead, - InvalidChunkLength, - InvalidHeader, - ProtocolError, - ReadTimeoutError, - ResponseNotChunked, - SSLError, -) -from .util.response import is_fp_closed, is_response_to_head -from .util.retry import Retry - -if typing.TYPE_CHECKING: - from .connectionpool import HTTPConnectionPool - -log = logging.getLogger(__name__) - - -class ContentDecoder: - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - raise NotImplementedError() - - @property - def has_unconsumed_tail(self) -> bool: - raise NotImplementedError() - - def flush(self) -> bytes: - raise NotImplementedError() - - -class DeflateDecoder(ContentDecoder): - def __init__(self) -> None: - self._first_try = True - self._first_try_data = b"" - self._unfed_data = b"" - self._obj = zlib.decompressobj() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - data = self._unfed_data + data - self._unfed_data = b"" - if not data and not self._obj.unconsumed_tail: - return data - original_max_length = max_length - if original_max_length < 0: - max_length = 0 - elif original_max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unfed_data = data - return b"" - - # Subsequent calls always reuse `self._obj`. zlib requires - # passing the unconsumed tail if decompression is to continue. - if not self._first_try: - return self._obj.decompress( - self._obj.unconsumed_tail + data, max_length=max_length - ) - - # First call tries with RFC 1950 ZLIB format. - self._first_try_data += data - try: - decompressed = self._obj.decompress(data, max_length=max_length) - if decompressed: - self._first_try = False - self._first_try_data = b"" - return decompressed - # On failure, it falls back to RFC 1951 DEFLATE format. - except zlib.error: - self._first_try = False - self._obj = zlib.decompressobj(-zlib.MAX_WBITS) - try: - return self.decompress( - self._first_try_data, max_length=original_max_length - ) - finally: - self._first_try_data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unfed_data) or ( - bool(self._obj.unconsumed_tail) and not self._first_try - ) - - def flush(self) -> bytes: - return self._obj.flush() - - -class GzipDecoderState: - FIRST_MEMBER = 0 - OTHER_MEMBERS = 1 - SWALLOW_DATA = 2 - - -class GzipDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - self._state = GzipDecoderState.FIRST_MEMBER - self._unconsumed_tail = b"" - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - ret = bytearray() - if self._state == GzipDecoderState.SWALLOW_DATA: - return bytes(ret) - - if max_length == 0: - # We should not pass 0 to the zlib decompressor because 0 is - # the default value that will make zlib decompress without a - # length limit. - # Data should be stored for subsequent calls. - self._unconsumed_tail += data - return b"" - - # zlib requires passing the unconsumed tail to the subsequent - # call if decompression is to continue. - data = self._unconsumed_tail + data - if not data and self._obj.eof: - return bytes(ret) - - while True: - try: - ret += self._obj.decompress( - data, max_length=max(max_length - len(ret), 0) - ) - except zlib.error: - previous_state = self._state - # Ignore data after the first error - self._state = GzipDecoderState.SWALLOW_DATA - self._unconsumed_tail = b"" - if previous_state == GzipDecoderState.OTHER_MEMBERS: - # Allow trailing garbage acceptable in other gzip clients - return bytes(ret) - raise - - self._unconsumed_tail = data = ( - self._obj.unconsumed_tail or self._obj.unused_data - ) - if max_length > 0 and len(ret) >= max_length: - break - - if not data: - return bytes(ret) - # When the end of a gzip member is reached, a new decompressor - # must be created for unused (possibly future) data. - if self._obj.eof: - self._state = GzipDecoderState.OTHER_MEMBERS - self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) - - return bytes(ret) - - @property - def has_unconsumed_tail(self) -> bool: - return bool(self._unconsumed_tail) - - def flush(self) -> bytes: - return self._obj.flush() - - -if brotli is not None: - - class BrotliDecoder(ContentDecoder): - # Supports both 'brotlipy' and 'Brotli' packages - # since they share an import name. The top branches - # are for 'brotlipy' and bottom branches for 'Brotli' - def __init__(self) -> None: - self._obj = brotli.Decompressor() - if hasattr(self._obj, "decompress"): - setattr(self, "_decompress", self._obj.decompress) - else: - setattr(self, "_decompress", self._obj.process) - - # Requires Brotli >= 1.2.0 for `output_buffer_limit`. - def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes: - raise NotImplementedError() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - try: - if max_length > 0: - return self._decompress(data, output_buffer_limit=max_length) - else: - return self._decompress(data) - except TypeError: - # Fallback for Brotli/brotlicffi/brotlipy versions without - # the `output_buffer_limit` parameter. - warnings.warn( - "Brotli >= 1.2.0 is required to prevent decompression bombs.", - DependencyWarning, - ) - return self._decompress(data) - - @property - def has_unconsumed_tail(self) -> bool: - try: - return not self._obj.can_accept_more_data() - except AttributeError: - return False - - def flush(self) -> bytes: - if hasattr(self._obj, "flush"): - return self._obj.flush() # type: ignore[no-any-return] - return b"" - - -try: - if sys.version_info >= (3, 14): - from compression import zstd - else: - from backports import zstd -except ImportError: - HAS_ZSTD = False -else: - HAS_ZSTD = True - - class ZstdDecoder(ContentDecoder): - def __init__(self) -> None: - self._obj = zstd.ZstdDecompressor() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if not data and not self.has_unconsumed_tail: - return b"" - if self._obj.eof: - data = self._obj.unused_data + data - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress(data, max_length=max_length) - length = len(part) - data_parts = [part] - # Every loop iteration is supposed to read data from a separate frame. - # The loop breaks when: - # - enough data is read; - # - no more unused data is available; - # - end of the last read frame has not been reached (i.e., - # more data has to be fed). - while ( - self._obj.eof - and self._obj.unused_data - and (max_length < 0 or length < max_length) - ): - unused_data = self._obj.unused_data - if not self._obj.needs_input: - self._obj = zstd.ZstdDecompressor() - part = self._obj.decompress( - unused_data, - max_length=(max_length - length) if max_length > 0 else -1, - ) - if part_length := len(part): - data_parts.append(part) - length += part_length - elif self._obj.needs_input: - break - return b"".join(data_parts) - - @property - def has_unconsumed_tail(self) -> bool: - return not (self._obj.needs_input or self._obj.eof) or bool( - self._obj.unused_data - ) - - def flush(self) -> bytes: - if not self._obj.eof: - raise DecodeError("Zstandard data is incomplete") - return b"" - - -class MultiDecoder(ContentDecoder): - """ - From RFC7231: - If one or more encodings have been applied to a representation, the - sender that applied the encodings MUST generate a Content-Encoding - header field that lists the content codings in the order in which - they were applied. - """ - - # Maximum allowed number of chained HTTP encodings in the - # Content-Encoding header. - max_decode_links = 5 - - def __init__(self, modes: str) -> None: - encodings = [m.strip() for m in modes.split(",")] - if len(encodings) > self.max_decode_links: - raise DecodeError( - "Too many content encodings in the chain: " - f"{len(encodings)} > {self.max_decode_links}" - ) - self._decoders = [_get_decoder(e) for e in encodings] - - def flush(self) -> bytes: - return self._decoders[0].flush() - - def decompress(self, data: bytes, max_length: int = -1) -> bytes: - if max_length <= 0: - for d in reversed(self._decoders): - data = d.decompress(data) - return data - - ret = bytearray() - # Every while loop iteration goes through all decoders once. - # It exits when enough data is read or no more data can be read. - # It is possible that the while loop iteration does not produce - # any data because we retrieve up to `max_length` from every - # decoder, and the amount of bytes may be insufficient for the - # next decoder to produce enough/any output. - while True: - any_data = False - for d in reversed(self._decoders): - data = d.decompress(data, max_length=max_length - len(ret)) - if data: - any_data = True - # We should not break when no data is returned because - # next decoders may produce data even with empty input. - ret += data - if not any_data or len(ret) >= max_length: - return bytes(ret) - data = b"" - - @property - def has_unconsumed_tail(self) -> bool: - return any(d.has_unconsumed_tail for d in self._decoders) - - -def _get_decoder(mode: str) -> ContentDecoder: - if "," in mode: - return MultiDecoder(mode) - - # According to RFC 9110 section 8.4.1.3, recipients should - # consider x-gzip equivalent to gzip - if mode in ("gzip", "x-gzip"): - return GzipDecoder() - - if brotli is not None and mode == "br": - return BrotliDecoder() - - if HAS_ZSTD and mode == "zstd": - return ZstdDecoder() - - return DeflateDecoder() - - -class BytesQueueBuffer: - """Memory-efficient bytes buffer - - To return decoded data in read() and still follow the BufferedIOBase API, we need a - buffer to always return the correct amount of bytes. - - This buffer should be filled using calls to put() - - Our maximum memory usage is determined by the sum of the size of: - - * self.buffer, which contains the full data - * the largest chunk that we will copy in get() - """ - - def __init__(self) -> None: - self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque() - self._size: int = 0 - - def __len__(self) -> int: - return self._size - - def put(self, data: bytes) -> None: - self.buffer.append(data) - self._size += len(data) - - def get(self, n: int) -> bytes: - if n == 0: - return b"" - elif not self.buffer: - raise RuntimeError("buffer is empty") - elif n < 0: - raise ValueError("n should be > 0") - - if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes): - self._size -= n - return self.buffer.popleft() - - fetched = 0 - ret = io.BytesIO() - while fetched < n: - remaining = n - fetched - chunk = self.buffer.popleft() - chunk_length = len(chunk) - if remaining < chunk_length: - chunk = memoryview(chunk) - left_chunk, right_chunk = chunk[:remaining], chunk[remaining:] - ret.write(left_chunk) - self.buffer.appendleft(right_chunk) - self._size -= remaining - break - else: - ret.write(chunk) - self._size -= chunk_length - fetched += chunk_length - - if not self.buffer: - break - - return ret.getvalue() - - def get_all(self) -> bytes: - buffer = self.buffer - if not buffer: - assert self._size == 0 - return b"" - if len(buffer) == 1: - result = buffer.pop() - if isinstance(result, memoryview): - result = result.tobytes() - else: - ret = io.BytesIO() - ret.writelines(buffer.popleft() for _ in range(len(buffer))) - result = ret.getvalue() - self._size = 0 - return result - - -class BaseHTTPResponse(io.IOBase): - CONTENT_DECODERS = ["gzip", "x-gzip", "deflate"] - if brotli is not None: - CONTENT_DECODERS += ["br"] - if HAS_ZSTD: - CONTENT_DECODERS += ["zstd"] - REDIRECT_STATUSES = [301, 302, 303, 307, 308] - - DECODER_ERROR_CLASSES: tuple[type[Exception], ...] = (IOError, zlib.error) - if brotli is not None: - DECODER_ERROR_CLASSES += (brotli.error,) - - if HAS_ZSTD: - DECODER_ERROR_CLASSES += (zstd.ZstdError,) - - def __init__( - self, - *, - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int, - version: int, - version_string: str, - reason: str | None, - decode_content: bool, - request_url: str | None, - retries: Retry | None = None, - ) -> None: - if isinstance(headers, HTTPHeaderDict): - self.headers = headers - else: - self.headers = HTTPHeaderDict(headers) # type: ignore[arg-type] - self.status = status - self.version = version - self.version_string = version_string - self.reason = reason - self.decode_content = decode_content - self._has_decoded_content = False - self._request_url: str | None = request_url - self.retries = retries - - self.chunked = False - tr_enc = self.headers.get("transfer-encoding", "").lower() - # Don't incur the penalty of creating a list and then discarding it - encodings = (enc.strip() for enc in tr_enc.split(",")) - if "chunked" in encodings: - self.chunked = True - - self._decoder: ContentDecoder | None = None - self.length_remaining: int | None - - def get_redirect_location(self) -> str | None | typing.Literal[False]: - """ - Should we redirect and where to? - - :returns: Truthy redirect location string if we got a redirect status - code and valid location. ``None`` if redirect status and no - location. ``False`` if not a redirect status code. - """ - if self.status in self.REDIRECT_STATUSES: - return self.headers.get("location") - return False - - @property - def data(self) -> bytes: - raise NotImplementedError() - - def json(self) -> typing.Any: - """ - Deserializes the body of the HTTP response as a Python object. - - The body of the HTTP response must be encoded using UTF-8, as per - `RFC 8529 Section 8.1 `_. - - To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to - your custom decoder instead. - - If the body of the HTTP response is not decodable to UTF-8, a - `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a - valid JSON document, a `json.JSONDecodeError` will be raised. - - Read more :ref:`here `. - - :returns: The body of the HTTP response as a Python object. - """ - data = self.data.decode("utf-8") - return _json.loads(data) - - @property - def url(self) -> str | None: - raise NotImplementedError() - - @url.setter - def url(self, url: str | None) -> None: - raise NotImplementedError() - - @property - def connection(self) -> BaseHTTPConnection | None: - raise NotImplementedError() - - @property - def retries(self) -> Retry | None: - return self._retries - - @retries.setter - def retries(self, retries: Retry | None) -> None: - # Override the request_url if retries has a redirect location. - if retries is not None and retries.history: - self.url = retries.history[-1].redirect_location - self._retries = retries - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - raise NotImplementedError() - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - raise NotImplementedError() - - def read_chunked( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> typing.Iterator[bytes]: - raise NotImplementedError() - - def release_conn(self) -> None: - raise NotImplementedError() - - def drain_conn(self) -> None: - raise NotImplementedError() - - def shutdown(self) -> None: - raise NotImplementedError() - - def close(self) -> None: - raise NotImplementedError() - - def _init_decoder(self) -> None: - """ - Set-up the _decoder attribute if necessary. - """ - # Note: content-encoding value should be case-insensitive, per RFC 7230 - # Section 3.2 - content_encoding = self.headers.get("content-encoding", "").lower() - if self._decoder is None: - if content_encoding in self.CONTENT_DECODERS: - self._decoder = _get_decoder(content_encoding) - elif "," in content_encoding: - encodings = [ - e.strip() - for e in content_encoding.split(",") - if e.strip() in self.CONTENT_DECODERS - ] - if encodings: - self._decoder = _get_decoder(content_encoding) - - def _decode( - self, - data: bytes, - decode_content: bool | None, - flush_decoder: bool, - max_length: int | None = None, - ) -> bytes: - """ - Decode the data passed in and potentially flush the decoder. - """ - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - if max_length is None or flush_decoder: - max_length = -1 - - try: - if self._decoder: - data = self._decoder.decompress(data, max_length=max_length) - self._has_decoded_content = True - except self.DECODER_ERROR_CLASSES as e: - content_encoding = self.headers.get("content-encoding", "").lower() - raise DecodeError( - "Received response with content-encoding: %s, but " - "failed to decode it." % content_encoding, - e, - ) from e - if flush_decoder: - data += self._flush_decoder() - - return data - - def _flush_decoder(self) -> bytes: - """ - Flushes the decoder. Should only be called if the decoder is actually - being used. - """ - if self._decoder: - return self._decoder.decompress(b"") + self._decoder.flush() - return b"" - - # Compatibility methods for `io` module - def readinto(self, b: bytearray | memoryview[int]) -> int: - temp = self.read(len(b)) - if len(temp) == 0: - return 0 - else: - b[: len(temp)] = temp - return len(temp) - - # Methods used by dependent libraries - def getheaders(self) -> HTTPHeaderDict: - return self.headers - - def getheader(self, name: str, default: str | None = None) -> str | None: - return self.headers.get(name, default) - - # Compatibility method for http.cookiejar - def info(self) -> HTTPHeaderDict: - return self.headers - - def geturl(self) -> str | None: - return self.url - - -class HTTPResponse(BaseHTTPResponse): - """ - HTTP Response container. - - Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is - loaded and decoded on-demand when the ``data`` property is accessed. This - class is also compatible with the Python standard library's :mod:`io` - module, and can hence be treated as a readable object in the context of that - framework. - - Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: - - :param preload_content: - If True, the response's body will be preloaded during construction. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param original_response: - When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` - object, it's convenient to include the original for debug purposes. It's - otherwise unused. - - :param retries: - The retries contains the last :class:`~urllib3.util.retry.Retry` that - was used during the request. - - :param enforce_content_length: - Enforce content length checking. Body returned by server must match - value of Content-Length header, if present. Otherwise, raise error. - """ - - def __init__( - self, - body: _TYPE_BODY = "", - headers: typing.Mapping[str, str] | typing.Mapping[bytes, bytes] | None = None, - status: int = 0, - version: int = 0, - version_string: str = "HTTP/?", - reason: str | None = None, - preload_content: bool = True, - decode_content: bool = True, - original_response: _HttplibHTTPResponse | None = None, - pool: HTTPConnectionPool | None = None, - connection: HTTPConnection | None = None, - msg: _HttplibHTTPMessage | None = None, - retries: Retry | None = None, - enforce_content_length: bool = True, - request_method: str | None = None, - request_url: str | None = None, - auto_close: bool = True, - sock_shutdown: typing.Callable[[int], None] | None = None, - ) -> None: - super().__init__( - headers=headers, - status=status, - version=version, - version_string=version_string, - reason=reason, - decode_content=decode_content, - request_url=request_url, - retries=retries, - ) - - self.enforce_content_length = enforce_content_length - self.auto_close = auto_close - - self._body = None - self._uncached_read_occurred = False - self._fp: _HttplibHTTPResponse | None = None - self._original_response = original_response - self._fp_bytes_read = 0 - self.msg = msg - - if body and isinstance(body, (str, bytes)): - self._body = body - - self._pool = pool - self._connection = connection - - if hasattr(body, "read"): - self._fp = body # type: ignore[assignment] - self._sock_shutdown = sock_shutdown - - # Are we using the chunked-style of transfer encoding? - self.chunk_left: int | None = None - - # Determine length of response - self.length_remaining = self._init_length(request_method) - - # Used to return the correct amount of bytes for partial read()s - self._decoded_buffer = BytesQueueBuffer() - - # If requested, preload the body. - if preload_content and not self._body: - self._body = self.read(decode_content=decode_content) - - def release_conn(self) -> None: - if not self._pool or not self._connection: - return None - - self._pool._put_conn(self._connection) - self._connection = None - - def drain_conn(self) -> None: - """ - Read and discard any remaining HTTP response data in the response connection. - - Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. - """ - try: - self._raw_read() - except (HTTPError, OSError, BaseSSLError, HTTPException): - pass - if self._has_decoded_content: - # `_raw_read` skips decompression, so we should clean up the - # decoder to avoid keeping unnecessary data in memory. - self._decoded_buffer = BytesQueueBuffer() - self._decoder = None - - @property - def data(self) -> bytes: - # For backwards-compat with earlier urllib3 0.4 and earlier. - if self._body: - return self._body # type: ignore[return-value] - - if self._fp: - return self.read(cache_content=True) - - return None # type: ignore[return-value] - - @property - def connection(self) -> HTTPConnection | None: - return self._connection - - def isclosed(self) -> bool: - return is_fp_closed(self._fp) - - def tell(self) -> int: - """ - Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:`HTTPResponse.read` - if bytes are encoded on the wire (e.g, compressed). - """ - return self._fp_bytes_read - - def _init_length(self, request_method: str | None) -> int | None: - """ - Set initial length value for Response content if available. - """ - length: int | None - content_length: str | None = self.headers.get("content-length") - - if content_length is not None: - if self.chunked: - # This Response will fail with an IncompleteRead if it can't be - # received as chunked. This method falls back to attempt reading - # the response before raising an exception. - log.warning( - "Received response with both Content-Length and " - "Transfer-Encoding set. This is expressly forbidden " - "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " - "attempting to process response as Transfer-Encoding: " - "chunked." - ) - return None - - try: - # RFC 7230 section 3.3.2 specifies multiple content lengths can - # be sent in a single Content-Length header - # (e.g. Content-Length: 42, 42). This line ensures the values - # are all valid ints and that as long as the `set` length is 1, - # all values are the same. Otherwise, the header is invalid. - lengths = {int(val) for val in content_length.split(",")} - if len(lengths) > 1: - raise InvalidHeader( - "Content-Length contained multiple " - "unmatching values (%s)" % content_length - ) - length = lengths.pop() - except ValueError: - length = None - else: - if length < 0: - length = None - - else: # if content_length is None - length = None - - # Convert status to int for comparison - # In some cases, httplib returns a status of "_UNKNOWN" - try: - status = int(self.status) - except ValueError: - status = 0 - - # Check for responses that shouldn't include a body - if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": - length = 0 - - return length - - @contextmanager - def _error_catcher(self) -> typing.Generator[None]: - """ - Catch low-level python exceptions, instead re-raising urllib3 - variants, so that low-level exceptions are not leaked in the - high-level api. - - On exit, release the connection back to the pool. - """ - clean_exit = False - - try: - try: - yield - - except SocketTimeout as e: - # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but - # there is yet no clean way to get at it from this context. - raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] - - except BaseSSLError as e: - # SSL errors related to framing/MAC get wrapped and reraised here - raise SSLError(e) from e - - except IncompleteRead as e: - if ( - e.expected is not None - and e.partial is not None - and e.expected == -e.partial - ): - arg = "Response may not contain content." - else: - arg = f"Connection broken: {e!r}" - raise ProtocolError(arg, e) from e - - except (HTTPException, OSError) as e: - raise ProtocolError(f"Connection broken: {e!r}", e) from e - - # If no exception is thrown, we should avoid cleaning up - # unnecessarily. - clean_exit = True - finally: - # If we didn't terminate cleanly, we need to throw away our - # connection. - if not clean_exit: - # The response may not be closed but we're not going to use it - # anymore so close it now to ensure that the connection is - # released back to the pool. - if self._original_response: - self._original_response.close() - - # Closing the response may not actually be sufficient to close - # everything, so if we have a hold of the connection close that - # too. - if self._connection: - self._connection.close() - - # If we hold the original response but it's closed now, we should - # return the connection back to the pool. - if self._original_response and self._original_response.isclosed(): - self.release_conn() - - def _fp_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Read a response with the thought that reading the number of bytes - larger than can fit in a 32-bit int at a time via SSL in some - known cases leads to an overflow error that has to be prevented - if `amt` or `self.length_remaining` indicate that a problem may - happen. - - This happens to urllib3 injected with pyOpenSSL-backed SSL-support. - """ - assert self._fp - c_int_max = 2**31 - 1 - if ( - (amt and amt > c_int_max) - or ( - amt is None - and self.length_remaining - and self.length_remaining > c_int_max - ) - ) and util.IS_PYOPENSSL: - if read1: - return self._fp.read1(c_int_max) - buffer = io.BytesIO() - # Besides `max_chunk_amt` being a maximum chunk size, it - # affects memory overhead of reading a response by this - # method in CPython. - # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum - # chunk size that does not lead to an overflow error, but - # 256 MiB is a compromise. - max_chunk_amt = 2**28 - while amt is None or amt != 0: - if amt is not None: - chunk_amt = min(amt, max_chunk_amt) - amt -= chunk_amt - else: - chunk_amt = max_chunk_amt - data = self._fp.read(chunk_amt) - if not data: - break - buffer.write(data) - del data # to reduce peak memory usage by `max_chunk_amt`. - return buffer.getvalue() - elif read1: - return self._fp.read1(amt) if amt is not None else self._fp.read1() - else: - # StringIO doesn't like amt=None - return self._fp.read(amt) if amt is not None else self._fp.read() - - def _raw_read( - self, - amt: int | None = None, - *, - read1: bool = False, - ) -> bytes: - """ - Reads `amt` of bytes from the socket. - """ - if self._fp is None: - return None # type: ignore[return-value] - - fp_closed = getattr(self._fp, "closed", False) - - with self._error_catcher(): - data = self._fp_read(amt, read1=read1) if not fp_closed else b"" - if amt is not None and amt != 0 and not data: - # Platform-specific: Buggy versions of Python. - # Close the connection when no data is returned - # - # This is redundant to what httplib/http.client _should_ - # already do. However, versions of python released before - # December 15, 2012 (http://bugs.python.org/issue16298) do - # not properly close the connection in all cases. There is - # no harm in redundantly calling close. - self._fp.close() - if ( - self.enforce_content_length - and self.length_remaining is not None - and self.length_remaining != 0 - ): - # This is an edge case that httplib failed to cover due - # to concerns of backward compatibility. We're - # addressing it here to make sure IncompleteRead is - # raised during streaming, so all calls with incorrect - # Content-Length are caught. - raise IncompleteRead(self._fp_bytes_read, self.length_remaining) - elif read1 and ( - (amt != 0 and not data) or self.length_remaining == len(data) - ): - # All data has been read, but `self._fp.read1` in - # CPython 3.12 and older doesn't always close - # `http.client.HTTPResponse`, so we close it here. - # See https://github.com/python/cpython/issues/113199 - self._fp.close() - - if data: - self._fp_bytes_read += len(data) - if self.length_remaining is not None: - self.length_remaining -= len(data) - return data - - def read( - self, - amt: int | None = None, - decode_content: bool | None = None, - cache_content: bool = False, - ) -> bytes: - """ - Similar to :meth:`http.client.HTTPResponse.read`, but with two additional - parameters: ``decode_content`` and ``cache_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - - :param cache_content: - If True, will save the returned data such that the same result is - returned despite of the state of the underlying file object. This - is useful if you want the ``.data`` property to continue working - after having ``.read()`` the file object. (Overridden if ``amt`` is - set.) - """ - self._init_decoder() - if decode_content is None: - decode_content = self.decode_content - - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - elif amt is not None: - cache_content = False - - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and len(self._decoded_buffer) < amt - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) >= amt: - return self._decoded_buffer.get(amt) - - data = self._raw_read(amt) - if not cache_content: - self._uncached_read_occurred = True - - flush_decoder = amt is None or (amt != 0 and not data) - - if ( - not data - and len(self._decoded_buffer) == 0 - and not (self._decoder and self._decoder.has_unconsumed_tail) - ): - return data - - if amt is None: - data = self._decode(data, decode_content, flush_decoder) - # It's possible that there is buffered decoded data after a - # partial read. - if decode_content and len(self._decoded_buffer) > 0: - self._decoded_buffer.put(data) - data = self._decoded_buffer.get_all() - - if cache_content and not self._uncached_read_occurred: - self._body = data - else: - # do not waste memory on buffer when not decoding - if not decode_content: - if self._has_decoded_content: - raise RuntimeError( - "Calling read(decode_content=False) is not supported after " - "read(decode_content=True) was called." - ) - return data - - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - - while len(self._decoded_buffer) < amt and data: - # TODO make sure to initially read enough data to get past the headers - # For example, the GZ file header takes 10 bytes, we don't want to read - # it one byte at a time - data = self._raw_read(amt) - decoded_data = self._decode( - data, - decode_content, - flush_decoder, - max_length=amt - len(self._decoded_buffer), - ) - self._decoded_buffer.put(decoded_data) - data = self._decoded_buffer.get(amt) - - return data - - def read1( - self, - amt: int | None = None, - decode_content: bool | None = None, - ) -> bytes: - """ - Similar to ``http.client.HTTPResponse.read1`` and documented - in :meth:`io.BufferedReader.read1`, but with an additional parameter: - ``decode_content``. - - :param amt: - How much of the content to read. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if decode_content is None: - decode_content = self.decode_content - if amt and amt < 0: - # Negative numbers and `None` should be treated the same. - amt = None - # try and respond without going to the network - if self._has_decoded_content: - if not decode_content: - raise RuntimeError( - "Calling read1(decode_content=False) is not supported after " - "read1(decode_content=True) was called." - ) - if ( - self._decoder - and self._decoder.has_unconsumed_tail - and (amt is None or len(self._decoded_buffer) < amt) - ): - decoded_data = self._decode( - b"", - decode_content, - flush_decoder=False, - max_length=( - amt - len(self._decoded_buffer) if amt is not None else None - ), - ) - self._decoded_buffer.put(decoded_data) - if len(self._decoded_buffer) > 0: - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - if amt == 0: - return b"" - - # FIXME, this method's type doesn't say returning None is possible - data = self._raw_read(amt, read1=True) - self._uncached_read_occurred = True - if not decode_content or data is None: - return data - - self._init_decoder() - while True: - flush_decoder = not data - decoded_data = self._decode( - data, decode_content, flush_decoder, max_length=amt - ) - self._decoded_buffer.put(decoded_data) - if decoded_data or flush_decoder: - break - data = self._raw_read(8192, read1=True) - - if amt is None: - return self._decoded_buffer.get_all() - return self._decoded_buffer.get(amt) - - def stream( - self, amt: int | None = 2**16, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - A generator wrapper for the read() method. A call will block until - ``amt`` bytes have been read from the connection or until the - connection is closed. - - :param amt: - How much of the content to read. The generator will return up to - much data per iteration, but may return less. This is particularly - likely when using compressed data. However, the empty string will - never be returned. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - if amt == 0: - return - - if self.chunked and self.supports_chunked_reads(): - yield from self.read_chunked(amt, decode_content=decode_content) - else: - while ( - not is_fp_closed(self._fp) - or len(self._decoded_buffer) > 0 - or (self._decoder and self._decoder.has_unconsumed_tail) - ): - data = self.read(amt=amt, decode_content=decode_content) - - if data: - yield data - - # Overrides from io.IOBase - def readable(self) -> bool: - return True - - def shutdown(self) -> None: - if not self._sock_shutdown: - raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") - if self._connection is None: - raise RuntimeError( - "Cannot shutdown as connection has already been released to the pool" - ) - self._sock_shutdown(socket.SHUT_RD) - - def close(self) -> None: - self._sock_shutdown = None - - if not self.closed and self._fp: - self._fp.close() - - if self._connection: - self._connection.close() - - if not self.auto_close: - io.IOBase.close(self) - - @property - def closed(self) -> bool: - if not self.auto_close: - return io.IOBase.closed.__get__(self) # type: ignore[no-any-return] - elif self._fp is None: - return True - elif hasattr(self._fp, "isclosed"): - return self._fp.isclosed() - elif hasattr(self._fp, "closed"): - return self._fp.closed - else: - return True - - def fileno(self) -> int: - if self._fp is None: - raise OSError("HTTPResponse has no file to get a fileno from") - elif hasattr(self._fp, "fileno"): - return self._fp.fileno() - else: - raise OSError( - "The file-like object this HTTPResponse is wrapped " - "around has no file descriptor" - ) - - def flush(self) -> None: - if ( - self._fp is not None - and hasattr(self._fp, "flush") - and not getattr(self._fp, "closed", False) - ): - return self._fp.flush() - - def supports_chunked_reads(self) -> bool: - """ - Checks if the underlying file-like object looks like a - :class:`http.client.HTTPResponse` object. We do this by testing for - the fp attribute. If it is present we assume it returns raw chunks as - processed by read_chunked(). - """ - return hasattr(self._fp, "fp") - - def _update_chunk_length(self) -> None: - # First, we'll figure out length of a chunk and then - # we'll try to read it from socket. - if self.chunk_left is not None: - return None - line = self._fp.fp.readline() # type: ignore[union-attr] - line = line.split(b";", 1)[0] - try: - self.chunk_left = int(line, 16) - except ValueError: - self.close() - if line: - # Invalid chunked protocol response, abort. - raise InvalidChunkLength(self, line) from None - else: - # Truncated at start of next chunk - raise ProtocolError("Response ended prematurely") from None - - def _handle_chunk(self, amt: int | None) -> bytes: - returned_chunk = None - if amt is None: - chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - returned_chunk = chunk - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - elif self.chunk_left is not None and amt < self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self.chunk_left = self.chunk_left - amt - returned_chunk = value - elif amt == self.chunk_left: - value = self._fp._safe_read(amt) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - returned_chunk = value - else: # amt > self.chunk_left - returned_chunk = self._fp._safe_read(self.chunk_left) # type: ignore[union-attr] - self._fp._safe_read(2) # type: ignore[union-attr] # Toss the CRLF at the end of the chunk. - self.chunk_left = None - return returned_chunk # type: ignore[no-any-return] - - def read_chunked( - self, amt: int | None = None, decode_content: bool | None = None - ) -> typing.Generator[bytes]: - """ - Similar to :meth:`HTTPResponse.read`, but with an additional - parameter: ``decode_content``. - - :param amt: - How much of the content to read. If specified, caching is skipped - because it doesn't make sense to cache partial content as the full - response. - - :param decode_content: - If True, will attempt to decode the body based on the - 'content-encoding' header. - """ - self._init_decoder() - # FIXME: Rewrite this method and make it a class with a better structured logic. - if not self.chunked: - raise ResponseNotChunked( - "Response is not chunked. " - "Header 'transfer-encoding: chunked' is missing." - ) - if not self.supports_chunked_reads(): - raise BodyNotHttplibCompatible( - "Body should be http.client.HTTPResponse like. " - "It should have have an fp attribute which returns raw chunks." - ) - - with self._error_catcher(): - # Don't bother reading the body of a HEAD request. - if self._original_response and is_response_to_head(self._original_response): - self._original_response.close() - return None - - # If a response is already read and closed - # then return immediately. - if self._fp.fp is None: # type: ignore[union-attr] - return None - - if amt == 0: - return - elif amt and amt < 0: - # Negative numbers and `None` should be treated the same, - # but httplib handles only `None` correctly. - amt = None - - while True: - # First, check if any data is left in the decoder's buffer. - if self._decoder and self._decoder.has_unconsumed_tail: - chunk = b"" - else: - self._update_chunk_length() - self._uncached_read_occurred = True - if self.chunk_left == 0: - break - chunk = self._handle_chunk(amt) - decoded = self._decode( - chunk, - decode_content=decode_content, - flush_decoder=False, - max_length=amt, - ) - if decoded: - yield decoded - - if decode_content: - # On CPython and PyPy, we should never need to flush the - # decoder. However, on Jython we *might* need to, so - # lets defensively do it anyway. - decoded = self._flush_decoder() - if decoded: # Platform-specific: Jython. - yield decoded - - # Chunk content ends with \r\n: discard it. - while self._fp is not None: - line = self._fp.fp.readline() - if not line: - # Some sites may not end with '\r\n'. - break - if line == b"\r\n": - break - - # We read everything; close the "file". - if self._original_response: - self._original_response.close() - - @property - def url(self) -> str | None: - """ - Returns the URL that was the source of this response. - If the request that generated this response redirected, this method - will return the final redirect location. - """ - return self._request_url - - @url.setter - def url(self, url: str | None) -> None: - self._request_url = url - - def __iter__(self) -> typing.Iterator[bytes]: - buffer: list[bytes] = [] - for chunk in self.stream(decode_content=True): - if b"\n" in chunk: - chunks = chunk.split(b"\n") - yield b"".join(buffer) + chunks[0] + b"\n" - for x in chunks[1:-1]: - yield x + b"\n" - if chunks[-1]: - buffer = [chunks[-1]] - else: - buffer = [] - else: - buffer.append(chunk) - if buffer: - yield b"".join(buffer) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py deleted file mode 100644 index 534126033c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# For backwards compatibility, provide imports that used to be here. -from __future__ import annotations - -from .connection import is_connection_dropped -from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers -from .response import is_fp_closed -from .retry import Retry -from .ssl_ import ( - ALPN_PROTOCOLS, - IS_PYOPENSSL, - SSLContext, - assert_fingerprint, - create_urllib3_context, - resolve_cert_reqs, - resolve_ssl_version, - ssl_wrap_socket, -) -from .timeout import Timeout -from .url import Url, parse_url -from .wait import wait_for_read, wait_for_write - -__all__ = ( - "IS_PYOPENSSL", - "SSLContext", - "ALPN_PROTOCOLS", - "Retry", - "Timeout", - "Url", - "assert_fingerprint", - "create_urllib3_context", - "is_connection_dropped", - "is_fp_closed", - "parse_url", - "make_headers", - "resolve_cert_reqs", - "resolve_ssl_version", - "ssl_wrap_socket", - "wait_for_read", - "wait_for_write", - "SKIP_HEADER", - "SKIPPABLE_HEADERS", -) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py deleted file mode 100644 index f92519ee91..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/connection.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import annotations - -import socket -import typing - -from ..exceptions import LocationParseError -from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT - -_TYPE_SOCKET_OPTIONS = list[tuple[int, int, typing.Union[int, bytes]]] - -if typing.TYPE_CHECKING: - from .._base_connection import BaseHTTPConnection - - -def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific - """ - Returns True if the connection is dropped and should be closed. - :param conn: :class:`urllib3.connection.HTTPConnection` object. - """ - return not conn.is_connected - - -# This function is copied from socket.py in the Python 2.7 standard -# library test suite. Added to its signature is only `socket_options`. -# One additional modification is that we avoid binding to IPv6 servers -# discovered in DNS if the system doesn't have IPv6 functionality. -def create_connection( - address: tuple[str, int], - timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - source_address: tuple[str, int] | None = None, - socket_options: _TYPE_SOCKET_OPTIONS | None = None, -) -> socket.socket: - """Connect to *address* and return the socket object. - - Convenience function. Connect to *address* (a 2-tuple ``(host, - port)``) and return the socket object. Passing the optional - *timeout* parameter will set the timeout on the socket instance - before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`socket.getdefaulttimeout` - is used. If *source_address* is set it must be a tuple of (host, port) - for the socket to bind as a source address before making the connection. - An host of '' or port 0 tells the OS to use the default. - """ - - host, port = address - if host.startswith("["): - host = host.strip("[]") - err = None - - # Using the value from allowed_gai_family() in the context of getaddrinfo lets - # us select whether to work with IPv4 DNS records, IPv6 records, or both. - # The original create_connection function always returns all records. - family = allowed_gai_family() - - try: - host.encode("idna") - except UnicodeError: - raise LocationParseError(f"'{host}', label empty or too long") from None - - for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = None - try: - sock = socket.socket(af, socktype, proto) - - # If provided, set socket level options before connecting. - _set_socket_options(sock, socket_options) - - if timeout is not _DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(sa) - # Break explicitly a reference cycle - err = None - return sock - - except OSError as _: - err = _ - if sock is not None: - sock.close() - - if err is not None: - try: - raise err - finally: - # Break explicitly a reference cycle - err = None - else: - raise OSError("getaddrinfo returns an empty list") - - -def _set_socket_options( - sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None -) -> None: - if options is None: - return - - for opt in options: - sock.setsockopt(*opt) - - -def allowed_gai_family() -> socket.AddressFamily: - """This function is designed to work in the context of - getaddrinfo, where family=socket.AF_UNSPEC is the default and - will perform a DNS search for both IPv6 and IPv4 records.""" - - family = socket.AF_INET - if HAS_IPV6: - family = socket.AF_UNSPEC - return family - - -def _has_ipv6(host: str) -> bool: - """Returns True if the system can bind an IPv6 address.""" - sock = None - has_ipv6 = False - - if socket.has_ipv6: - # has_ipv6 returns true if cPython was compiled with IPv6 support. - # It does not tell us if the system has IPv6 support enabled. To - # determine that we must bind to an IPv6 address. - # https://github.com/urllib3/urllib3/pull/611 - # https://bugs.python.org/issue658327 - try: - sock = socket.socket(socket.AF_INET6) - sock.bind((host, 0)) - has_ipv6 = True - except Exception: - pass - - if sock: - sock.close() - return has_ipv6 - - -HAS_IPV6 = _has_ipv6("::1") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py deleted file mode 100644 index 908fc6621d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/proxy.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import typing - -from .url import Url - -if typing.TYPE_CHECKING: - from ..connection import ProxyConfig - - -def connection_requires_http_tunnel( - proxy_url: Url | None = None, - proxy_config: ProxyConfig | None = None, - destination_scheme: str | None = None, -) -> bool: - """ - Returns True if the connection requires an HTTP CONNECT through the proxy. - - :param URL proxy_url: - URL of the proxy. - :param ProxyConfig proxy_config: - Proxy configuration from poolmanager.py - :param str destination_scheme: - The scheme of the destination. (i.e https, http, etc) - """ - # If we're not using a proxy, no way to use a tunnel. - if proxy_url is None: - return False - - # HTTP destinations never require tunneling, we always forward. - if destination_scheme == "http": - return False - - # Support for forwarding with HTTPS proxies and HTTPS destinations. - if ( - proxy_url.scheme == "https" - and proxy_config - and proxy_config.use_forwarding_for_https - ): - return False - - # Otherwise always use a tunnel. - return True diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py deleted file mode 100644 index 6c2372ba7e..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/request.py +++ /dev/null @@ -1,263 +0,0 @@ -from __future__ import annotations - -import io -import sys -import typing -from base64 import b64encode -from enum import Enum - -from ..exceptions import UnrewindableBodyError -from .util import to_bytes - -if typing.TYPE_CHECKING: - from typing import Final - -# Pass as a value within ``headers`` to skip -# emitting some HTTP headers that are added automatically. -# The only headers that are supported are ``Accept-Encoding``, -# ``Host``, and ``User-Agent``. -SKIP_HEADER = "@@@SKIP_HEADER@@@" -SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) - -ACCEPT_ENCODING = "gzip,deflate" -try: - try: - import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 - except ImportError: - import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",br" - -try: - if sys.version_info >= (3, 14): - from compression import zstd as _unused_module_zstd # noqa: F401 - else: - from backports import zstd as _unused_module_zstd # noqa: F401 -except ImportError: - pass -else: - ACCEPT_ENCODING += ",zstd" - - -class _TYPE_FAILEDTELL(Enum): - token = 0 - - -_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token - -_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] - -# When sending a request with these methods we aren't expecting -# a body so don't need to set an explicit 'Content-Length: 0' -# The reason we do this in the negative instead of tracking methods -# which 'should' have a body is because unknown methods should be -# treated as if they were 'POST' which *does* expect a body. -_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} - - -def make_headers( - keep_alive: bool | None = None, - accept_encoding: bool | list[str] | str | None = None, - user_agent: str | None = None, - basic_auth: str | None = None, - proxy_basic_auth: str | None = None, - disable_cache: bool | None = None, -) -> dict[str, str]: - """ - Shortcuts for generating request headers. - - :param keep_alive: - If ``True``, adds 'connection: keep-alive' header. - - :param accept_encoding: - Can be a boolean, list, or string. - ``True`` translates to 'gzip,deflate'. If the dependencies for - Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or - Zstandard (the ``backports.zstd`` package for Python before 3.14) - algorithms are installed, then their encodings are - included in the string ('br' and 'zstd', respectively). - List will get joined by comma. - String will be used as provided. - - :param user_agent: - String representing the user-agent you want, such as - "python-urllib3/0.6" - - :param basic_auth: - Colon-separated username:password string for 'authorization: basic ...' - auth header. - - :param proxy_basic_auth: - Colon-separated username:password string for 'proxy-authorization: basic ...' - auth header. - - :param disable_cache: - If ``True``, adds 'cache-control: no-cache' header. - - Example: - - .. code-block:: python - - import urllib3 - - print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) - # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} - print(urllib3.util.make_headers(accept_encoding=True)) - # {'accept-encoding': 'gzip,deflate'} - """ - headers: dict[str, str] = {} - if accept_encoding: - if isinstance(accept_encoding, str): - pass - elif isinstance(accept_encoding, list): - accept_encoding = ",".join(accept_encoding) - else: - accept_encoding = ACCEPT_ENCODING - headers["accept-encoding"] = accept_encoding - - if user_agent: - headers["user-agent"] = user_agent - - if keep_alive: - headers["connection"] = "keep-alive" - - if basic_auth: - headers["authorization"] = ( - f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" - ) - - if proxy_basic_auth: - headers["proxy-authorization"] = ( - f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" - ) - - if disable_cache: - headers["cache-control"] = "no-cache" - - return headers - - -def set_file_position( - body: typing.Any, pos: _TYPE_BODY_POSITION | None -) -> _TYPE_BODY_POSITION | None: - """ - If a position is provided, move file to that point. - Otherwise, we'll attempt to record a position for future use. - """ - if pos is not None: - rewind_body(body, pos) - elif getattr(body, "tell", None) is not None: - try: - pos = body.tell() - except OSError: - # This differentiates from None, allowing us to catch - # a failed `tell()` later when trying to rewind the body. - pos = _FAILEDTELL - - return pos - - -def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: - """ - Attempt to rewind body to a certain position. - Primarily used for request redirects and retries. - - :param body: - File-like object that supports seek. - - :param int pos: - Position to seek to in file. - """ - body_seek = getattr(body, "seek", None) - if body_seek is not None and isinstance(body_pos, int): - try: - body_seek(body_pos) - except OSError as e: - raise UnrewindableBodyError( - "An error occurred when rewinding request body for redirect/retry." - ) from e - elif body_pos is _FAILEDTELL: - raise UnrewindableBodyError( - "Unable to record file position for rewinding " - "request body during a redirect/retry." - ) - else: - raise ValueError( - f"body_pos must be of type integer, instead it was {type(body_pos)}." - ) - - -class ChunksAndContentLength(typing.NamedTuple): - chunks: typing.Iterable[bytes] | None - content_length: int | None - - -def body_to_chunks( - body: typing.Any | None, method: str, blocksize: int -) -> ChunksAndContentLength: - """Takes the HTTP request method, body, and blocksize and - transforms them into an iterable of chunks to pass to - socket.sendall() and an optional 'Content-Length' header. - - A 'Content-Length' of 'None' indicates the length of the body - can't be determined so should use 'Transfer-Encoding: chunked' - for framing instead. - """ - - chunks: typing.Iterable[bytes] | None - content_length: int | None - - # No body, we need to make a recommendation on 'Content-Length' - # based on whether that request method is expected to have - # a body or not. - if body is None: - chunks = None - if method.upper() not in _METHODS_NOT_EXPECTING_BODY: - content_length = 0 - else: - content_length = None - - # Bytes or strings become bytes - elif isinstance(body, (str, bytes)): - chunks = (to_bytes(body),) - content_length = len(chunks[0]) - - # File-like object, TODO: use seek() and tell() for length? - elif hasattr(body, "read"): - - def chunk_readable() -> typing.Iterable[bytes]: - encode = isinstance(body, io.TextIOBase) - while True: - datablock = body.read(blocksize) - if not datablock: - break - if encode: - datablock = datablock.encode("utf-8") - yield datablock - - chunks = chunk_readable() - content_length = None - - # Otherwise we need to start checking via duck-typing. - else: - try: - # Check if the body implements the buffer API. - mv = memoryview(body) - except TypeError: - try: - # Check if the body is an iterable - chunks = iter(body) - content_length = None - except TypeError: - raise TypeError( - f"'body' must be a bytes-like object, file-like " - f"object, or iterable. Instead was {body!r}" - ) from None - else: - # Since it implements the buffer API can be passed directly to socket.sendall() - chunks = (body,) - content_length = mv.nbytes - - return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py deleted file mode 100644 index 0f4578696f..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/response.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import http.client as httplib -from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect - -from ..exceptions import HeaderParsingError - - -def is_fp_closed(obj: object) -> bool: - """ - Checks whether a given file-like object is closed. - - :param obj: - The file-like object to check. - """ - - try: - # Check `isclosed()` first, in case Python3 doesn't set `closed`. - # GH Issue #928 - return obj.isclosed() # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check via the official file-like-object way. - return obj.closed # type: ignore[no-any-return, attr-defined] - except AttributeError: - pass - - try: - # Check if the object is a container for another file-like object that - # gets released on exhaustion (e.g. HTTPResponse). - return obj.fp is None # type: ignore[attr-defined] - except AttributeError: - pass - - raise ValueError("Unable to determine whether fp is closed.") - - -def assert_header_parsing(headers: httplib.HTTPMessage) -> None: - """ - Asserts whether all headers have been successfully parsed. - Extracts encountered errors from the result of parsing headers. - - Only works on Python 3. - - :param http.client.HTTPMessage headers: Headers to verify. - - :raises urllib3.exceptions.HeaderParsingError: - If parsing errors are found. - """ - - # This will fail silently if we pass in the wrong kind of parameter. - # To make debugging easier add an explicit check. - if not isinstance(headers, httplib.HTTPMessage): - raise TypeError(f"expected httplib.Message, got {type(headers)}.") - - unparsed_data = None - - # get_payload is actually email.message.Message.get_payload; - # we're only interested in the result if it's not a multipart message - if not headers.is_multipart(): - payload = headers.get_payload() - - if isinstance(payload, (bytes, str)): - unparsed_data = payload - - # httplib is assuming a response body is available - # when parsing headers even when httplib only sends - # header data to parse_headers() This results in - # defects on multipart responses in particular. - # See: https://github.com/urllib3/urllib3/issues/800 - - # So we ignore the following defects: - # - StartBoundaryNotFoundDefect: - # The claimed start boundary was never found. - # - MultipartInvariantViolationDefect: - # A message claimed to be a multipart but no subparts were found. - defects = [ - defect - for defect in headers.defects - if not isinstance( - defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) - ) - ] - - if defects or unparsed_data: - raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) - - -def is_response_to_head(response: httplib.HTTPResponse) -> bool: - """ - Checks whether the request of a response has been a HEAD-request. - - :param http.client.HTTPResponse response: - Response to check if the originating request - used 'HEAD' as a method. - """ - # FIXME: Can we do this somehow without accessing private httplib _method? - method_str = response._method # type: str # type: ignore[attr-defined] - return method_str.upper() == "HEAD" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py deleted file mode 100644 index 7649898e1d..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/retry.py +++ /dev/null @@ -1,557 +0,0 @@ -from __future__ import annotations - -import email -import logging -import random -import re -import time -import typing -from itertools import takewhile -from types import TracebackType - -from ..exceptions import ( - ConnectTimeoutError, - InvalidHeader, - MaxRetryError, - ProtocolError, - ProxyError, - ReadTimeoutError, - ResponseError, -) -from .util import reraise - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from ..connectionpool import ConnectionPool - from ..response import BaseHTTPResponse - -log = logging.getLogger(__name__) - - -# Data structure for representing the metadata of requests that result in a retry. -class RequestHistory(typing.NamedTuple): - method: str | None - url: str | None - error: Exception | None - status: int | None - redirect_location: str | None - - -class Retry: - """Retry configuration. - - Each retry attempt will create a new Retry object with updated values, so - they can be safely reused. - - Retries can be defined as a default for a pool: - - .. code-block:: python - - retries = Retry(connect=5, read=2, redirect=5) - http = PoolManager(retries=retries) - response = http.request("GET", "https://example.com/") - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=Retry(10)) - - Retries can be disabled by passing ``False``: - - .. code-block:: python - - response = http.request("GET", "https://example.com/", retries=False) - - Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless - retries are disabled, in which case the causing exception will be raised. - - :param int total: - Total number of retries to allow. Takes precedence over other counts. - - Set to ``None`` to remove this constraint and fall back on other - counts. - - Set to ``0`` to fail on the first retry. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int connect: - How many connection-related errors to retry on. - - These are errors raised before the request is sent to the remote server, - which we assume has not triggered the server to process the request. - - Set to ``0`` to fail on the first retry of this type. - - :param int read: - How many times to retry on read errors. - - These errors are raised after the request was sent to the server, so the - request may have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - :param int redirect: - How many redirects to perform. Limit this to avoid infinite redirect - loops. - - A redirect is a HTTP response with a status code 301, 302, 303, 307 or - 308. - - Set to ``0`` to fail on the first retry of this type. - - Set to ``False`` to disable and imply ``raise_on_redirect=False``. - - :param int status: - How many times to retry on bad status codes. - - These are retries made on responses, where status code matches - ``status_forcelist``. - - Set to ``0`` to fail on the first retry of this type. - - :param int other: - How many times to retry on other errors. - - Other errors are errors that are not connect, read, redirect or status errors. - These errors might be raised after the request was sent to the server, so the - request might have side-effects. - - Set to ``0`` to fail on the first retry of this type. - - If ``total`` is not set, it's a good idea to set this to 0 to account - for unexpected edge cases and avoid infinite retry loops. - - :param Collection allowed_methods: - Set of uppercased HTTP method verbs that we should retry on. - - By default, we only retry on methods which are considered to be - idempotent (multiple requests with the same parameters end with the - same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. - - Set to a ``None`` value to retry on any verb. - - :param Collection status_forcelist: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``allowed_methods`` - and the response status code is in ``status_forcelist``. - - By default, this is disabled with ``None``. - - :param float backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). urllib3 will sleep for:: - - {backoff factor} * (2 ** ({number of previous retries})) - - seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: - - random.uniform(0, {backoff jitter}) - - seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will - sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever - be longer than `backoff_max`. - - By default, backoff is disabled (factor set to 0). - - :param float backoff_max: - The maximum backoff time (in seconds) between retry attempts. - This value caps the computed backoff from `backoff_factor`. - - :param float backoff_jitter: - Random jitter amount (in seconds) added to the computed backoff. - Jitter is sampled uniformly from `0` to `backoff_jitter`. - - :param bool raise_on_redirect: Whether, if the number of redirects is - exhausted, to raise a MaxRetryError, or to return a response with a - response code in the 3xx range. - - :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: - whether we should raise an exception, or return a response, - if status falls in ``status_forcelist`` range and retries have - been exhausted. - - :param tuple history: The history of the request encountered during - each call to :meth:`~Retry.increment`. The list is in the order - the requests occurred. Each list item is of class :class:`RequestHistory`. - - :param bool respect_retry_after_header: - Whether to respect Retry-After header on status codes defined as - :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. - - :param Collection remove_headers_on_redirect: - Sequence of headers to remove from the request when a response - indicating a redirect is returned before firing off the redirected - request. - - :param int retry_after_max: Number of seconds to allow as the maximum for - Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`. - Any Retry-After headers larger than this value will be limited to this - value. - """ - - #: Default methods to be used for ``allowed_methods`` - DEFAULT_ALLOWED_METHODS = frozenset( - ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] - ) - - #: Default status codes to be used for ``status_forcelist`` - RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - - #: Default headers to be used for ``remove_headers_on_redirect`` - DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( - ["Cookie", "Authorization", "Proxy-Authorization"] - ) - - #: Default maximum backoff time. - DEFAULT_BACKOFF_MAX = 120 - - # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries. - #: Default maximum allowed value for Retry-After headers in seconds - DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600 - - # Backward compatibility; assigned outside of the class. - DEFAULT: typing.ClassVar[Retry] - - def __init__( - self, - total: bool | int | None = 10, - connect: int | None = None, - read: int | None = None, - redirect: bool | int | None = None, - status: int | None = None, - other: int | None = None, - allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, - status_forcelist: typing.Collection[int] | None = None, - backoff_factor: float = 0, - backoff_max: float = DEFAULT_BACKOFF_MAX, - raise_on_redirect: bool = True, - raise_on_status: bool = True, - history: tuple[RequestHistory, ...] | None = None, - respect_retry_after_header: bool = True, - remove_headers_on_redirect: typing.Collection[ - str - ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, - backoff_jitter: float = 0.0, - retry_after_max: int = DEFAULT_RETRY_AFTER_MAX, - ) -> None: - self.total = total - self.connect = connect - self.read = read - self.status = status - self.other = other - - if redirect is False or total is False: - redirect = 0 - raise_on_redirect = False - - self.redirect = redirect - self.status_forcelist = status_forcelist or set() - self.allowed_methods = allowed_methods - self.backoff_factor = backoff_factor - self.backoff_max = backoff_max - self.retry_after_max = retry_after_max - self.raise_on_redirect = raise_on_redirect - self.raise_on_status = raise_on_status - self.history = history or () - self.respect_retry_after_header = respect_retry_after_header - self.remove_headers_on_redirect = frozenset( - h.lower() for h in remove_headers_on_redirect - ) - self.backoff_jitter = backoff_jitter - - def new(self, **kw: typing.Any) -> Self: - params = dict( - total=self.total, - connect=self.connect, - read=self.read, - redirect=self.redirect, - status=self.status, - other=self.other, - allowed_methods=self.allowed_methods, - status_forcelist=self.status_forcelist, - backoff_factor=self.backoff_factor, - backoff_max=self.backoff_max, - retry_after_max=self.retry_after_max, - raise_on_redirect=self.raise_on_redirect, - raise_on_status=self.raise_on_status, - history=self.history, - remove_headers_on_redirect=self.remove_headers_on_redirect, - respect_retry_after_header=self.respect_retry_after_header, - backoff_jitter=self.backoff_jitter, - ) - - params.update(kw) - return type(self)(**params) # type: ignore[arg-type] - - @classmethod - def from_int( - cls, - retries: Retry | bool | int | None, - redirect: bool | int | None = True, - default: Retry | bool | int | None = None, - ) -> Retry: - """Backwards-compatibility for the old retries format.""" - if retries is None: - retries = default if default is not None else cls.DEFAULT - - if isinstance(retries, Retry): - return retries - - redirect = bool(redirect) and None - new_retries = cls(retries, redirect=redirect) - log.debug("Converted retries value: %r -> %r", retries, new_retries) - return new_retries - - def get_backoff_time(self) -> float: - """Formula for computing the current backoff - - :rtype: float - """ - # We want to consider only the last consecutive errors sequence (Ignore redirects). - consecutive_errors_len = len( - list( - takewhile(lambda x: x.redirect_location is None, reversed(self.history)) - ) - ) - if consecutive_errors_len <= 1: - return 0 - - backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) - if self.backoff_jitter != 0.0: - backoff_value += random.random() * self.backoff_jitter - return float(max(0, min(self.backoff_max, backoff_value))) - - def parse_retry_after(self, retry_after: str) -> float: - seconds: float - # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 - if re.match(r"^\s*[0-9]+\s*$", retry_after): - seconds = int(retry_after) - else: - retry_date_tuple = email.utils.parsedate_tz(retry_after) - if retry_date_tuple is None: - raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") - - retry_date = email.utils.mktime_tz(retry_date_tuple) - seconds = retry_date - time.time() - - seconds = max(seconds, 0) - - # Check the seconds do not exceed the specified maximum - if seconds > self.retry_after_max: - seconds = self.retry_after_max - - return seconds - - def get_retry_after(self, response: BaseHTTPResponse) -> float | None: - """Get the value of Retry-After in seconds.""" - - retry_after = response.headers.get("Retry-After") - - if retry_after is None: - return None - - return self.parse_retry_after(retry_after) - - def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: - retry_after = self.get_retry_after(response) - if retry_after: - time.sleep(retry_after) - return True - - return False - - def _sleep_backoff(self) -> None: - backoff = self.get_backoff_time() - if backoff <= 0: - return - time.sleep(backoff) - - def sleep(self, response: BaseHTTPResponse | None = None) -> None: - """Sleep between retry attempts. - - This method will respect a server's ``Retry-After`` response header - and sleep the duration of the time requested. If that is not present, it - will use an exponential backoff. By default, the backoff factor is 0 and - this method will return immediately. - """ - - if self.respect_retry_after_header and response: - slept = self.sleep_for_retry(response) - if slept: - return - - self._sleep_backoff() - - def _is_connection_error(self, err: Exception) -> bool: - """Errors when we're fairly sure that the server did not receive the - request, so it should be safe to retry. - """ - if isinstance(err, ProxyError): - err = err.original_error - return isinstance(err, ConnectTimeoutError) - - def _is_read_error(self, err: Exception) -> bool: - """Errors that occur after the request has been started, so we should - assume that the server began processing it. - """ - return isinstance(err, (ReadTimeoutError, ProtocolError)) - - def _is_method_retryable(self, method: str) -> bool: - """Checks if a given HTTP method should be retried upon, depending if - it is included in the allowed_methods - """ - if self.allowed_methods and method.upper() not in self.allowed_methods: - return False - return True - - def is_retry( - self, method: str, status_code: int, has_retry_after: bool = False - ) -> bool: - """Is this method/status code retryable? (Based on allowlists and control - variables such as the number of total retries to allow, whether to - respect the Retry-After header, whether this header is present, and - whether the returned status code is on the list of status codes to - be retried upon on the presence of the aforementioned header) - """ - if not self._is_method_retryable(method): - return False - - if self.status_forcelist and status_code in self.status_forcelist: - return True - - return bool( - self.total - and self.respect_retry_after_header - and has_retry_after - and (status_code in self.RETRY_AFTER_STATUS_CODES) - ) - - def is_exhausted(self) -> bool: - """Are we out of retries?""" - retry_counts = [ - x - for x in ( - self.total, - self.connect, - self.read, - self.redirect, - self.status, - self.other, - ) - if x - ] - if not retry_counts: - return False - - return min(retry_counts) < 0 - - def increment( - self, - method: str | None = None, - url: str | None = None, - response: BaseHTTPResponse | None = None, - error: Exception | None = None, - _pool: ConnectionPool | None = None, - _stacktrace: TracebackType | None = None, - ) -> Self: - """Return a new Retry object with incremented retry counters. - - :param response: A response object, or None, if the server did not - return a response. - :type response: :class:`~urllib3.response.BaseHTTPResponse` - :param Exception error: An error encountered during the request, or - None if the response was received successfully. - - :return: A new ``Retry`` object. - """ - if self.total is False and error: - # Disabled, indicate to re-raise the error. - raise reraise(type(error), error, _stacktrace) - - total = self.total - if total is not None: - total -= 1 - - connect = self.connect - read = self.read - redirect = self.redirect - status_count = self.status - other = self.other - cause = "unknown" - status = None - redirect_location = None - - if error and self._is_connection_error(error): - # Connect retry? - if connect is False: - raise reraise(type(error), error, _stacktrace) - elif connect is not None: - connect -= 1 - - elif error and self._is_read_error(error): - # Read retry? - if read is False or method is None or not self._is_method_retryable(method): - raise reraise(type(error), error, _stacktrace) - elif read is not None: - read -= 1 - - elif error: - # Other retry? - if other is not None: - other -= 1 - - elif response and response.get_redirect_location(): - # Redirect retry? - if redirect is not None: - redirect -= 1 - cause = "too many redirects" - response_redirect_location = response.get_redirect_location() - if response_redirect_location: - redirect_location = response_redirect_location - status = response.status - - else: - # Incrementing because of a server error like a 500 in - # status_forcelist and the given method is in the allowed_methods - cause = ResponseError.GENERIC_ERROR - if response and response.status: - if status_count is not None: - status_count -= 1 - cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) - status = response.status - - history = self.history + ( - RequestHistory(method, url, error, status, redirect_location), - ) - - new_retry = self.new( - total=total, - connect=connect, - read=read, - redirect=redirect, - status=status_count, - other=other, - history=history, - ) - - if new_retry.is_exhausted(): - reason = error or ResponseError(cause) - raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] - - log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) - - return new_retry - - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(total={self.total}, connect={self.connect}, " - f"read={self.read}, redirect={self.redirect}, status={self.status})" - ) - - -# For backwards compatibility (equivalent to pre-v1.9): -Retry.DEFAULT = Retry(3) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py deleted file mode 100644 index e66549a76c..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_.py +++ /dev/null @@ -1,477 +0,0 @@ -from __future__ import annotations - -import hashlib -import hmac -import os -import socket -import sys -import typing -import warnings -from binascii import unhexlify - -from ..exceptions import ProxySchemeUnsupported, SSLError -from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE - -SSLContext = None -SSLTransport = None -HAS_NEVER_CHECK_COMMON_NAME = False -IS_PYOPENSSL = False -ALPN_PROTOCOLS = ["http/1.1"] - -_TYPE_VERSION_INFO = tuple[int, int, int, str, int] - -# Maps the length of a digest to a possible hash function producing this digest -HASHFUNC_MAP = { - length: getattr(hashlib, algorithm, None) - for length, algorithm in ((32, "md5"), (40, "sha1"), (64, "sha256")) -} - - -def _is_has_never_check_common_name_reliable( - openssl_version: str, -) -> bool: - # As of May 2023, all released versions of LibreSSL fail to reject certificates with - # only common names, see https://github.com/urllib3/urllib3/pull/3024 - is_openssl = openssl_version.startswith("OpenSSL ") - - return is_openssl - - -if typing.TYPE_CHECKING: - from ssl import VerifyMode - from typing import TypedDict - - from .ssltransport import SSLTransport as SSLTransportType - - class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): - subjectAltName: tuple[tuple[str, str], ...] - subject: tuple[tuple[tuple[str, str], ...], ...] - serialNumber: str - - -# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' -_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} - -try: # Do we have ssl at all? - import ssl - from ssl import ( # type: ignore[assignment] - CERT_REQUIRED, - HAS_NEVER_CHECK_COMMON_NAME, - OP_NO_COMPRESSION, - OP_NO_TICKET, - OPENSSL_VERSION, - PROTOCOL_TLS, - PROTOCOL_TLS_CLIENT, - VERIFY_X509_PARTIAL_CHAIN, - VERIFY_X509_STRICT, - OP_NO_SSLv2, - OP_NO_SSLv3, - SSLContext, - TLSVersion, - ) - - PROTOCOL_SSLv23 = PROTOCOL_TLS - - # Setting SSLContext.hostname_checks_common_name = False didn't work with - # LibreSSL, check details in the used function. - if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( - OPENSSL_VERSION, - ): # Defensive: - HAS_NEVER_CHECK_COMMON_NAME = False - - # Need to be careful here in case old TLS versions get - # removed in future 'ssl' module implementations. - for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): - try: - _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( - TLSVersion, attr - ) - except AttributeError: # Defensive: - continue - - from .ssltransport import SSLTransport # type: ignore[assignment] -except ImportError: - OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment, misc] - OP_NO_TICKET = 0x4000 # type: ignore[assignment, misc] - OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment, misc] - OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment, misc] - PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment, misc] - PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment, misc] - VERIFY_X509_PARTIAL_CHAIN = 0x80000 # type: ignore[assignment,misc] - VERIFY_X509_STRICT = 0x20 # type: ignore[assignment, misc] - - -_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] - - -def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: - """ - Checks if given fingerprint matches the supplied certificate. - - :param cert: - Certificate as bytes object. - :param fingerprint: - Fingerprint as string of hexdigits, can be interspersed by colons. - """ - - if cert is None: - raise SSLError("No certificate for the peer.") - - fingerprint = fingerprint.replace(":", "").lower() - digest_length = len(fingerprint) - if digest_length not in HASHFUNC_MAP: - raise SSLError(f"Fingerprint of invalid length: {fingerprint}") - hashfunc = HASHFUNC_MAP.get(digest_length) - if hashfunc is None: - raise SSLError( - f"Hash function implementation unavailable for fingerprint length: {digest_length}" - ) - - # We need encode() here for py32; works on py2 and p33. - fingerprint_bytes = unhexlify(fingerprint.encode()) - - cert_digest = hashfunc(cert).digest() - - if not hmac.compare_digest(cert_digest, fingerprint_bytes): - raise SSLError( - f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' - ) - - -def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: - """ - Resolves the argument to a numeric constant, which can be passed to - the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_REQUIRED`. - If given a string it is assumed to be the name of the constant in the - :mod:`ssl` module or its abbreviation. - (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. - If it's neither `None` nor a string we assume it is already the numeric - constant which can directly be passed to wrap_socket. - """ - if candidate is None: - return CERT_REQUIRED - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "CERT_" + candidate) - return res # type: ignore[no-any-return] - - return candidate # type: ignore[return-value] - - -def resolve_ssl_version(candidate: None | int | str) -> int: - """ - like resolve_cert_reqs - """ - if candidate is None: - return PROTOCOL_TLS - - if isinstance(candidate, str): - res = getattr(ssl, candidate, None) - if res is None: - res = getattr(ssl, "PROTOCOL_" + candidate) - return typing.cast(int, res) - - return candidate - - -def create_urllib3_context( - ssl_version: int | None = None, - cert_reqs: int | None = None, - options: int | None = None, - ciphers: str | None = None, - ssl_minimum_version: int | None = None, - ssl_maximum_version: int | None = None, - verify_flags: int | None = None, -) -> ssl.SSLContext: - """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. - - :param ssl_version: - The desired protocol version to use. This will default to - PROTOCOL_SSLv23 which will negotiate the highest protocol that both - the server and your installation of OpenSSL support. - - This parameter is deprecated instead use 'ssl_minimum_version'. - :param ssl_minimum_version: - The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - :param ssl_maximum_version: - The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. - Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the - default value. - :param cert_reqs: - Whether to require the certificate verification. This defaults to - ``ssl.CERT_REQUIRED``. - :param options: - Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, - ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. - :param ciphers: - Which cipher suites to allow the server to select. Defaults to either system configured - ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. - :param verify_flags: - The flags for certificate verification operations. These default to - ``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+. - :returns: - Constructed SSLContext object with specified options - :rtype: SSLContext - """ - if SSLContext is None: - raise TypeError("Can't create an SSLContext object without an ssl module") - - # This means 'ssl_version' was specified as an exact value. - if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): - # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' - # to avoid conflicts. - if ssl_minimum_version is not None or ssl_maximum_version is not None: - raise ValueError( - "Can't specify both 'ssl_version' and either " - "'ssl_minimum_version' or 'ssl_maximum_version'" - ) - - # 'ssl_version' is deprecated and will be removed in the future. - else: - # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. - ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MINIMUM_SUPPORTED - ) - ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( - ssl_version, TLSVersion.MAXIMUM_SUPPORTED - ) - - # This warning message is pushing users to use 'ssl_minimum_version' - # instead of both min/max. Best practice is to only set the minimum version and - # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' - warnings.warn( - "'ssl_version' option is deprecated and will be " - "removed in urllib3 v3.0. Instead use 'ssl_minimum_version'", - category=FutureWarning, - stacklevel=2, - ) - - context = SSLContext(PROTOCOL_TLS_CLIENT) - if ssl_minimum_version is not None: - context.minimum_version = ssl_minimum_version - else: # pyOpenSSL defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here - context.minimum_version = TLSVersion.TLSv1_2 - - if ssl_maximum_version is not None: - context.maximum_version = ssl_maximum_version - - # Unless we're given ciphers defer to either system ciphers in - # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. - if ciphers: - context.set_ciphers(ciphers) - - # Setting the default here, as we may have no ssl module on import - cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if options is None: - options = 0 - # SSLv2 is easily broken and is considered harmful and dangerous - options |= OP_NO_SSLv2 - # SSLv3 has several problems and is now dangerous - options |= OP_NO_SSLv3 - # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ - # (issue #309) - options |= OP_NO_COMPRESSION - # TLSv1.2 only. Unless set explicitly, do not request tickets. - # This may save some bandwidth on wire, and although the ticket is encrypted, - # there is a risk associated with it being on wire, - # if the server is not rotating its ticketing keys properly. - options |= OP_NO_TICKET - - context.options |= options - - if verify_flags is None: - verify_flags = 0 - # In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN - # and VERIFY_X509_STRICT so we do the same - if sys.version_info >= (3, 13): - verify_flags |= VERIFY_X509_PARTIAL_CHAIN - verify_flags |= VERIFY_X509_STRICT - - context.verify_flags |= verify_flags - - # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is - # necessary for conditional client cert authentication with TLS 1.3. - # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using - # an SSLContext created by pyOpenSSL. - if getattr(context, "post_handshake_auth", None) is not None: - context.post_handshake_auth = True - - # The order of the below lines setting verify_mode and check_hostname - # matter due to safe-guards SSLContext has to prevent an SSLContext with - # check_hostname=True, verify_mode=NONE/OPTIONAL. - # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own - # 'ssl.match_hostname()' implementation. - if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: - context.verify_mode = cert_reqs - context.check_hostname = True - else: - context.check_hostname = False - context.verify_mode = cert_reqs - - context.hostname_checks_common_name = False - - if "SSLKEYLOGFILE" in os.environ: - sslkeylogfile = os.path.expandvars(os.environ.get("SSLKEYLOGFILE")) - else: - sslkeylogfile = None - if sslkeylogfile: - context.keylog_filename = sslkeylogfile - - return context - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: typing.Literal[False] = ..., -) -> ssl.SSLSocket: ... - - -@typing.overload -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = ..., - certfile: str | None = ..., - cert_reqs: int | None = ..., - ca_certs: str | None = ..., - server_hostname: str | None = ..., - ssl_version: int | None = ..., - ciphers: str | None = ..., - ssl_context: ssl.SSLContext | None = ..., - ca_cert_dir: str | None = ..., - key_password: str | None = ..., - ca_cert_data: None | str | bytes = ..., - tls_in_tls: bool = ..., -) -> ssl.SSLSocket | SSLTransportType: ... - - -def ssl_wrap_socket( - sock: socket.socket, - keyfile: str | None = None, - certfile: str | None = None, - cert_reqs: int | None = None, - ca_certs: str | None = None, - server_hostname: str | None = None, - ssl_version: int | None = None, - ciphers: str | None = None, - ssl_context: ssl.SSLContext | None = None, - ca_cert_dir: str | None = None, - key_password: str | None = None, - ca_cert_data: None | str | bytes = None, - tls_in_tls: bool = False, -) -> ssl.SSLSocket | SSLTransportType: - """ - All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and - ca_cert_dir have the same meaning as they do when using - :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, - :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. - - :param server_hostname: - When SNI is supported, the expected hostname of the certificate - :param ssl_context: - A pre-made :class:`SSLContext` object. If none is provided, one will - be created using :func:`create_urllib3_context`. - :param ciphers: - A string of ciphers we wish the client to support. - :param ca_cert_dir: - A directory containing CA certificates in multiple separate files, as - supported by OpenSSL's -CApath flag or the capath argument to - SSLContext.load_verify_locations(). - :param key_password: - Optional password if the keyfile is encrypted. - :param ca_cert_data: - Optional string containing CA certificates in PEM format suitable for - passing as the cadata parameter to SSLContext.load_verify_locations() - :param tls_in_tls: - Use SSLTransport to wrap the existing socket. - """ - context = ssl_context - if context is None: - # Note: This branch of code and all the variables in it are only used in tests. - # We should consider deprecating and removing this code. - context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) - - if ca_certs or ca_cert_dir or ca_cert_data: - try: - context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) - except OSError as e: - raise SSLError(e) from e - - elif ssl_context is None and hasattr(context, "load_default_certs"): - # try to load OS default certs; works well on Windows. - context.load_default_certs() - - # Attempt to detect if we get the goofy behavior of the - # keyfile being encrypted and OpenSSL asking for the - # passphrase via the terminal and instead error out. - if keyfile and key_password is None and _is_key_file_encrypted(keyfile): - raise SSLError("Client private key is encrypted, password is required") - - if certfile: - if key_password is None: - context.load_cert_chain(certfile, keyfile) - else: - context.load_cert_chain(certfile, keyfile, key_password) - - context.set_alpn_protocols(ALPN_PROTOCOLS) - - ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) - return ssl_sock - - -def is_ipaddress(hostname: str | bytes) -> bool: - """Detects whether the hostname given is an IPv4 or IPv6 address. - Also detects IPv6 addresses with Zone IDs. - - :param str hostname: Hostname to examine. - :return: True if the hostname is an IP address, False otherwise. - """ - if isinstance(hostname, bytes): - # IDN A-label bytes are ASCII compatible. - hostname = hostname.decode("ascii") - return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) - - -def _is_key_file_encrypted(key_file: str) -> bool: - """Detects if a key file is encrypted or not.""" - with open(key_file) as f: - for line in f: - # Look for Proc-Type: 4,ENCRYPTED - if "ENCRYPTED" in line: - return True - - return False - - -def _ssl_wrap_socket_impl( - sock: socket.socket, - ssl_context: ssl.SSLContext, - tls_in_tls: bool, - server_hostname: str | None = None, -) -> ssl.SSLSocket | SSLTransportType: - if tls_in_tls: - if not SSLTransport: - # Import error, ssl is not available. - raise ProxySchemeUnsupported( - "TLS in TLS requires support for the 'ssl' module" - ) - - SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) - return SSLTransport(sock, ssl_context, server_hostname) - - return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py deleted file mode 100644 index 94994f25ae..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssl_match_hostname.py +++ /dev/null @@ -1,153 +0,0 @@ -"""The match_hostname() function from Python 3.5, essential when using SSL.""" - -# Note: This file is under the PSF license as the code comes from the python -# stdlib. http://docs.python.org/3/license.html -# It is modified to remove commonName support. - -from __future__ import annotations - -import ipaddress -import re -import typing -from ipaddress import IPv4Address, IPv6Address - -if typing.TYPE_CHECKING: - from .ssl_ import _TYPE_PEER_CERT_RET_DICT - -__version__ = "3.5.0.1" - - -class CertificateError(ValueError): - pass - - -def _dnsname_match( - dn: typing.Any, hostname: str, max_wildcards: int = 1 -) -> typing.Match[str] | None | bool: - """Matching according to RFC 6125, section 6.4.3 - - http://tools.ietf.org/html/rfc6125#section-6.4.3 - """ - pats = [] - if not dn: - return False - - # Ported from python3-syntax: - # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r".") - leftmost = parts[0] - remainder = parts[1:] - - wildcards = leftmost.count("*") - if wildcards > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survey of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn) - ) - - # speed up common case w/o wildcards - if not wildcards: - return bool(dn.lower() == hostname.lower()) - - # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. - if leftmost == "*": - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append("[^.]+") - elif leftmost.startswith("xn--") or hostname.startswith("xn--"): - # RFC 6125, section 6.4.3, subitem 3. - # The client SHOULD NOT attempt to match a presented identifier - # where the wildcard character is embedded within an A-label or - # U-label of an internationalized domain name. - pats.append(re.escape(leftmost)) - else: - # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) - - # add the remaining fragments, ignore any wildcards - for frag in remainder: - pats.append(re.escape(frag)) - - pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) - return pat.match(hostname) - - -def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: - """Exact matching of IP addresses. - - RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded - bytes of the IP address. An IP version 4 address is 4 octets, and an IP - version 6 address is 16 octets. [...] A reference identity of type IP-ID - matches if the address is identical to an iPAddress value of the - subjectAltName extension of the certificate." - """ - # OpenSSL may add a trailing newline to a subjectAltName's IP address - # Divergence from upstream: ipaddress can't handle byte str - ip = ipaddress.ip_address(ipname.rstrip()) - return bool(ip.packed == host_ip.packed) - - -def match_hostname( - cert: _TYPE_PEER_CERT_RET_DICT | None, - hostname: str, - hostname_checks_common_name: bool = False, -) -> None: - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError( - "empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED" - ) - - try: - host_ip = ipaddress.ip_address(hostname) - except ValueError: - # Not an IP address (common case) - host_ip = None - dnsnames = [] - san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) - key: str - value: str - for key, value in san: - if key == "DNS": - if host_ip is None and _dnsname_match(value, hostname): - return - dnsnames.append(value) - elif key == "IP Address": - if host_ip is not None and _ipaddress_match(value, host_ip): - return - dnsnames.append(value) - - # We only check 'commonName' if it's enabled and we're not verifying - # an IP address. IP addresses aren't valid within 'commonName'. - if hostname_checks_common_name and host_ip is None and not dnsnames: - for sub in cert.get("subject", ()): - for key, value in sub: - if key == "commonName": - if _dnsname_match(value, hostname): - return - dnsnames.append( - value - ) # Defensive: for older PyPy and OpenSSL versions - - if len(dnsnames) > 1: - raise CertificateError( - "hostname %r " - "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) - ) - elif len(dnsnames) == 1: - raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") - else: - raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py deleted file mode 100644 index 6d59bc3bce..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/ssltransport.py +++ /dev/null @@ -1,271 +0,0 @@ -from __future__ import annotations - -import io -import socket -import ssl -import typing - -from ..exceptions import ProxySchemeUnsupported - -if typing.TYPE_CHECKING: - from typing_extensions import Self - - from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT - - -_WriteBuffer = typing.Union[bytearray, memoryview] -_ReturnValue = typing.TypeVar("_ReturnValue") - -SSL_BLOCKSIZE = 16384 - - -class SSLTransport: - """ - The SSLTransport wraps an existing socket and establishes an SSL connection. - - Contrary to Python's implementation of SSLSocket, it allows you to chain - multiple TLS connections together. It's particularly useful if you need to - implement TLS within TLS. - - The class supports most of the socket API operations. - """ - - @staticmethod - def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: - """ - Raises a ProxySchemeUnsupported if the provided ssl_context can't be used - for TLS in TLS. - - The only requirement is that the ssl_context provides the 'wrap_bio' - methods. - """ - - if not hasattr(ssl_context, "wrap_bio"): - raise ProxySchemeUnsupported( - "TLS in TLS requires SSLContext.wrap_bio() which isn't " - "available on non-native SSLContext" - ) - - def __init__( - self, - socket: socket.socket, - ssl_context: ssl.SSLContext, - server_hostname: str | None = None, - suppress_ragged_eofs: bool = True, - ) -> None: - """ - Create an SSLTransport around socket using the provided ssl_context. - """ - self.incoming = ssl.MemoryBIO() - self.outgoing = ssl.MemoryBIO() - - self.suppress_ragged_eofs = suppress_ragged_eofs - self.socket = socket - - self.sslobj = ssl_context.wrap_bio( - self.incoming, self.outgoing, server_hostname=server_hostname - ) - - # Perform initial handshake. - self._ssl_io_loop(self.sslobj.do_handshake) - - def __enter__(self) -> Self: - return self - - def __exit__(self, *_: typing.Any) -> None: - self.close() - - def fileno(self) -> int: - return self.socket.fileno() - - def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: - return self._wrap_ssl_read(len, buffer) - - def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv") - return self._wrap_ssl_read(buflen) - - def recv_into( - self, - buffer: _WriteBuffer, - nbytes: int | None = None, - flags: int = 0, - ) -> None | int | bytes: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to recv_into") - if nbytes is None: - nbytes = len(buffer) - return self.read(nbytes, buffer) - - def sendall(self, data: bytes, flags: int = 0) -> None: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to sendall") - count = 0 - with memoryview(data) as view, view.cast("B") as byte_view: - amount = len(byte_view) - while count < amount: - v = self.send(byte_view[count:]) - count += v - - def send(self, data: bytes, flags: int = 0) -> int: - if flags != 0: - raise ValueError("non-zero flags not allowed in calls to send") - return self._ssl_io_loop(self.sslobj.write, data) - - def makefile( - self, - mode: str, - buffering: int | None = None, - *, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: - """ - Python's httpclient uses makefile and buffered io when reading HTTP - messages and we need to support it. - - This is unfortunately a copy and paste of socket.py makefile with small - changes to point to the socket directly. - """ - if not set(mode) <= {"r", "w", "b"}: - raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") - - writing = "w" in mode - reading = "r" in mode or not writing - assert reading or writing - binary = "b" in mode - rawmode = "" - if reading: - rawmode += "r" - if writing: - rawmode += "w" - raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] - self.socket._io_refs += 1 # type: ignore[attr-defined] - if buffering is None: - buffering = -1 - if buffering < 0: - buffering = io.DEFAULT_BUFFER_SIZE - if buffering == 0: - if not binary: - raise ValueError("unbuffered streams must be binary") - return raw - buffer: typing.BinaryIO - if reading and writing: - buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] - elif reading: - buffer = io.BufferedReader(raw, buffering) - else: - assert writing - buffer = io.BufferedWriter(raw, buffering) - if binary: - return buffer - text = io.TextIOWrapper(buffer, encoding, errors, newline) - text.mode = mode # type: ignore[misc] - return text - - def unwrap(self) -> None: - self._ssl_io_loop(self.sslobj.unwrap) - - def close(self) -> None: - self.socket.close() - - @typing.overload - def getpeercert( - self, binary_form: typing.Literal[False] = ... - ) -> _TYPE_PEER_CERT_RET_DICT | None: ... - - @typing.overload - def getpeercert(self, binary_form: typing.Literal[True]) -> bytes | None: ... - - def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: - return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] - - def version(self) -> str | None: - return self.sslobj.version() - - def cipher(self) -> tuple[str, str, int] | None: - return self.sslobj.cipher() - - def selected_alpn_protocol(self) -> str | None: - return self.sslobj.selected_alpn_protocol() - - def shared_ciphers(self) -> list[tuple[str, str, int]] | None: - return self.sslobj.shared_ciphers() - - def compression(self) -> str | None: - return self.sslobj.compression() - - def settimeout(self, value: float | None) -> None: - self.socket.settimeout(value) - - def gettimeout(self) -> float | None: - return self.socket.gettimeout() - - def _decref_socketios(self) -> None: - self.socket._decref_socketios() # type: ignore[attr-defined] - - def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: - try: - return self._ssl_io_loop(self.sslobj.read, len, buffer) - except ssl.SSLError as e: - if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: - return 0 # eof, return 0. - else: - raise - - # func is sslobj.do_handshake or sslobj.unwrap - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: ... - - # func is sslobj.write, arg1 is data - @typing.overload - def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: ... - - # func is sslobj.read, arg1 is len, arg2 is buffer - @typing.overload - def _ssl_io_loop( - self, - func: typing.Callable[[int, bytearray | None], bytes], - arg1: int, - arg2: bytearray | None, - ) -> bytes: ... - - def _ssl_io_loop( - self, - func: typing.Callable[..., _ReturnValue], - arg1: None | bytes | int = None, - arg2: bytearray | None = None, - ) -> _ReturnValue: - """Performs an I/O loop between incoming/outgoing and the socket.""" - should_loop = True - ret = None - - while should_loop: - errno = None - try: - if arg1 is None and arg2 is None: - ret = func() - elif arg2 is None: - ret = func(arg1) - else: - ret = func(arg1, arg2) - except ssl.SSLError as e: - if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): - # WANT_READ, and WANT_WRITE are expected, others are not. - raise e - errno = e.errno - - buf = self.outgoing.read() - self.socket.sendall(buf) - - if errno is None: - should_loop = False - elif errno == ssl.SSL_ERROR_WANT_READ: - buf = self.socket.recv(SSL_BLOCKSIZE) - if buf: - self.incoming.write(buf) - else: - self.incoming.write_eof() - return typing.cast(_ReturnValue, ret) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py deleted file mode 100644 index 4bb1be11d9..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/timeout.py +++ /dev/null @@ -1,275 +0,0 @@ -from __future__ import annotations - -import time -import typing -from enum import Enum -from socket import getdefaulttimeout - -from ..exceptions import TimeoutStateError - -if typing.TYPE_CHECKING: - from typing import Final - - -class _TYPE_DEFAULT(Enum): - # This value should never be passed to socket.settimeout() so for safety we use a -1. - # socket.settimout() raises a ValueError for negative values. - token = -1 - - -_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token - -_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] - - -class Timeout: - """Timeout configuration. - - Timeouts can be defined as a default for a pool: - - .. code-block:: python - - import urllib3 - - timeout = urllib3.util.Timeout(connect=2.0, read=7.0) - - http = urllib3.PoolManager(timeout=timeout) - - resp = http.request("GET", "https://example.com/") - - print(resp.status) - - Or per-request (which overrides the default for the pool): - - .. code-block:: python - - response = http.request("GET", "https://example.com/", timeout=Timeout(10)) - - Timeouts can be disabled by setting all the parameters to ``None``: - - .. code-block:: python - - no_timeout = Timeout(connect=None, read=None) - response = http.request("GET", "https://example.com/", timeout=no_timeout) - - - :param total: - This combines the connect and read timeouts into one; the read timeout - will be set to the time leftover from the connect attempt. In the - event that both a connect timeout and a total are specified, or a read - timeout and a total are specified, the shorter timeout will be applied. - - Defaults to None. - - :type total: int, float, or None - - :param connect: - The maximum amount of time (in seconds) to wait for a connection - attempt to a server to succeed. Omitting the parameter will default the - connect timeout to the system default, probably `the global default - timeout in socket.py - `_. - None will set an infinite timeout for connection attempts. - - :type connect: int, float, or None - - :param read: - The maximum amount of time (in seconds) to wait between consecutive - read operations for a response from the server. Omitting the parameter - will default the read timeout to the system default, probably `the - global default timeout in socket.py - `_. - None will set an infinite timeout. - - :type read: int, float, or None - - .. note:: - - Many factors can affect the total amount of time for urllib3 to return - an HTTP response. - - For example, Python's DNS resolver does not obey the timeout specified - on the socket. Other factors that can affect total request time include - high CPU load, high swap, the program running at a low priority level, - or other behaviors. - - In addition, the read and total timeouts only measure the time between - read operations on the socket connecting the client and the server, - not the total amount of time for the request to return a complete - response. For most requests, the timeout is raised because the server - has not sent the first byte in the specified time. This is not always - the case; if a server streams one byte every fifteen seconds, a timeout - of 20 seconds will not trigger, even though the request will take - several minutes to complete. - """ - - #: A sentinel object representing the default timeout value - DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT - - def __init__( - self, - total: _TYPE_TIMEOUT = None, - connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, - ) -> None: - self._connect = self._validate_timeout(connect, "connect") - self._read = self._validate_timeout(read, "read") - self.total = self._validate_timeout(total, "total") - self._start_connect: float | None = None - - def __repr__(self) -> str: - return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" - - # __str__ provided for backwards compatibility - __str__ = __repr__ - - @staticmethod - def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: - return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout - - @classmethod - def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: - """Check that a timeout attribute is valid. - - :param value: The timeout value to validate - :param name: The name of the timeout attribute to validate. This is - used to specify in error messages. - :return: The validated and casted version of the given value. - :raises ValueError: If it is a numeric value less than or equal to - zero, or the type is not an integer, float, or None. - """ - if value is None or value is _DEFAULT_TIMEOUT: - return value - - if isinstance(value, bool): - raise ValueError( - "Timeout cannot be a boolean value. It must " - "be an int, float or None." - ) - try: - float(value) - except (TypeError, ValueError): - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - try: - if value <= 0: - raise ValueError( - "Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than or equal to 0." % (name, value) - ) - except TypeError: - raise ValueError( - "Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value) - ) from None - - return value - - @classmethod - def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: - """Create a new Timeout from a legacy timeout value. - - The timeout value used by httplib.py sets the same timeout on the - connect(), and recv() socket requests. This creates a :class:`Timeout` - object that sets the individual timeouts to the ``timeout`` value - passed to this function. - - :param timeout: The legacy timeout value. - :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None - :return: Timeout object - :rtype: :class:`Timeout` - """ - return Timeout(read=timeout, connect=timeout) - - def clone(self) -> Timeout: - """Create a copy of the timeout object - - Timeout properties are stored per-pool but each request needs a fresh - Timeout object to ensure each one has its own start/stop configured. - - :return: a copy of the timeout object - :rtype: :class:`Timeout` - """ - # We can't use copy.deepcopy because that will also create a new object - # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to - # detect the user default. - return Timeout(connect=self._connect, read=self._read, total=self.total) - - def start_connect(self) -> float: - """Start the timeout clock, used during a connect() attempt - - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to start a timer that has been started already. - """ - if self._start_connect is not None: - raise TimeoutStateError("Timeout timer has already been started.") - self._start_connect = time.monotonic() - return self._start_connect - - def get_connect_duration(self) -> float: - """Gets the time elapsed since the call to :meth:`start_connect`. - - :return: Elapsed time in seconds. - :rtype: float - :raises urllib3.exceptions.TimeoutStateError: if you attempt - to get duration for a timer that hasn't been started. - """ - if self._start_connect is None: - raise TimeoutStateError( - "Can't get connect duration for timer that has not started." - ) - return time.monotonic() - self._start_connect - - @property - def connect_timeout(self) -> _TYPE_TIMEOUT: - """Get the value to use when setting a connection timeout. - - This will be a positive float or integer, the value None - (never timeout), or the default system timeout. - - :return: Connect timeout. - :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None - """ - if self.total is None: - return self._connect - - if self._connect is None or self._connect is _DEFAULT_TIMEOUT: - return self.total - - return min(self._connect, self.total) # type: ignore[type-var] - - @property - def read_timeout(self) -> float | None: - """Get the value for the read timeout. - - This assumes some time has elapsed in the connection timeout and - computes the read timeout appropriately. - - If self.total is set, the read timeout is dependent on the amount of - time taken by the connect timeout. If the connection time has not been - established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be - raised. - - :return: Value to use for the read timeout. - :rtype: int, float or None - :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` - has not yet been called on this object. - """ - if ( - self.total is not None - and self.total is not _DEFAULT_TIMEOUT - and self._read is not None - and self._read is not _DEFAULT_TIMEOUT - ): - # In case the connect timeout has not yet been established. - if self._start_connect is None: - return self._read - return max(0, min(self.total - self.get_connect_duration(), self._read)) - elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: - return max(0, self.total - self.get_connect_duration()) - else: - return self.resolve_default_timeout(self._read) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py deleted file mode 100644 index db057f17be..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/url.py +++ /dev/null @@ -1,469 +0,0 @@ -from __future__ import annotations - -import re -import typing - -from ..exceptions import LocationParseError -from .util import to_str - -# We only want to normalize urls with an HTTP(S) scheme. -# urllib3 infers URLs without a scheme (None) to be http. -_NORMALIZABLE_SCHEMES = ("http", "https", None) - -# Almost all of these patterns were derived from the -# 'rfc3986' module: https://github.com/python-hyper/rfc3986 -_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") -_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") -_URI_RE = re.compile( - r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" - r"(?://([^\\/?#]*))?" - r"([^?#]*)" - r"(?:\?([^#]*))?" - r"(?:#(.*))?$", - re.UNICODE | re.DOTALL, -) - -_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" -_HEX_PAT = "[0-9A-Fa-f]{1,4}" -_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) -_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} -_variations = [ - # 6( h16 ":" ) ls32 - "(?:%(hex)s:){6}%(ls32)s", - # "::" 5( h16 ":" ) ls32 - "::(?:%(hex)s:){5}%(ls32)s", - # [ h16 ] "::" 4( h16 ":" ) ls32 - "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", - # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 - "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", - # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 - "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", - # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 - "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", - # [ *4( h16 ":" ) h16 ] "::" ls32 - "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", - # [ *5( h16 ":" ) h16 ] "::" h16 - "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", - # [ *6( h16 ":" ) h16 ] "::" - "(?:(?:%(hex)s:){0,6}%(hex)s)?::", -] - -_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" -_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" -_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" -_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" -_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" -_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") - -_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") -_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") -_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") -_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") -_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") - -_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( - _REG_NAME_PAT, - _IPV4_PAT, - _IPV6_ADDRZ_PAT, -) -_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) - -_UNRESERVED_CHARS = set( - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" -) -_SUB_DELIM_CHARS = set("!$&'()*+,;=") -_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} -_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} -_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} - - -class Url( - typing.NamedTuple( - "Url", - [ - ("scheme", typing.Optional[str]), - ("auth", typing.Optional[str]), - ("host", typing.Optional[str]), - ("port", typing.Optional[int]), - ("path", typing.Optional[str]), - ("query", typing.Optional[str]), - ("fragment", typing.Optional[str]), - ], - ) -): - """ - Data structure for representing an HTTP URL. Used as a return value for - :func:`parse_url`. Both the scheme and host are normalized as they are - both case-insensitive according to RFC 3986. - """ - - def __new__( # type: ignore[no-untyped-def] - cls, - scheme: str | None = None, - auth: str | None = None, - host: str | None = None, - port: int | None = None, - path: str | None = None, - query: str | None = None, - fragment: str | None = None, - ): - if path and not path.startswith("/"): - path = "/" + path - if scheme is not None: - scheme = scheme.lower() - return super().__new__(cls, scheme, auth, host, port, path, query, fragment) - - @property - def hostname(self) -> str | None: - """For backwards-compatibility with urlparse. We're nice like that.""" - return self.host - - @property - def request_uri(self) -> str: - """Absolute path including the query string.""" - uri = self.path or "/" - - if self.query is not None: - uri += "?" + self.query - - return uri - - @property - def authority(self) -> str | None: - """ - Authority component as defined in RFC 3986 3.2. - This includes userinfo (auth), host and port. - - i.e. - userinfo@host:port - """ - userinfo = self.auth - netloc = self.netloc - if netloc is None or userinfo is None: - return netloc - else: - return f"{userinfo}@{netloc}" - - @property - def netloc(self) -> str | None: - """ - Network location including host and port. - - If you need the equivalent of urllib.parse's ``netloc``, - use the ``authority`` property instead. - """ - if self.host is None: - return None - if self.port: - return f"{self.host}:{self.port}" - return self.host - - @property - def url(self) -> str: - """ - Convert self into a url - - This function should more or less round-trip with :func:`.parse_url`. The - returned url may not be exactly the same as the url inputted to - :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls - with a blank port will have : removed). - - Example: - - .. code-block:: python - - import urllib3 - - U = urllib3.util.parse_url("https://google.com/mail/") - - print(U.url) - # "https://google.com/mail/" - - print( urllib3.util.Url("https", "username:password", - "host.com", 80, "/path", "query", "fragment" - ).url - ) - # "https://username:password@host.com:80/path?query#fragment" - """ - scheme, auth, host, port, path, query, fragment = self - url = "" - - # We use "is not None" we want things to happen with empty strings (or 0 port) - if scheme is not None: - url += scheme + "://" - if auth is not None: - url += auth + "@" - if host is not None: - url += host - if port is not None: - url += ":" + str(port) - if path is not None: - url += path - if query is not None: - url += "?" + query - if fragment is not None: - url += "#" + fragment - - return url - - def __str__(self) -> str: - return self.url - - -@typing.overload -def _encode_invalid_chars( - component: str, allowed_chars: typing.Container[str] -) -> str: # Abstract - ... - - -@typing.overload -def _encode_invalid_chars( - component: None, allowed_chars: typing.Container[str] -) -> None: # Abstract - ... - - -def _encode_invalid_chars( - component: str | None, allowed_chars: typing.Container[str] -) -> str | None: - """Percent-encodes a URI component without reapplying - onto an already percent-encoded component. - """ - if component is None: - return component - - component = to_str(component) - - # Normalize existing percent-encoded bytes. - # Try to see if the component we're encoding is already percent-encoded - # so we can skip all '%' characters but still encode all others. - component, percent_encodings = _PERCENT_RE.subn( - lambda match: match.group(0).upper(), component - ) - - uri_bytes = component.encode("utf-8", "surrogatepass") - is_percent_encoded = percent_encodings == uri_bytes.count(b"%") - encoded_component = bytearray() - - for i in range(0, len(uri_bytes)): - # Will return a single character bytestring - byte = uri_bytes[i : i + 1] - byte_ord = ord(byte) - if (is_percent_encoded and byte == b"%") or ( - byte_ord < 128 and byte.decode() in allowed_chars - ): - encoded_component += byte - continue - encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) - - return encoded_component.decode() - - -def _remove_path_dot_segments(path: str) -> str: - # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code - segments = path.split("/") # Turn the path into a list of segments - output = [] # Initialize the variable to use to store output - - for segment in segments: - # '.' is the current directory, so ignore it, it is superfluous - if segment == ".": - continue - # Anything other than '..', should be appended to the output - if segment != "..": - output.append(segment) - # In this case segment == '..', if we can, we should pop the last - # element - elif output: - output.pop() - - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if path.startswith("/") and (not output or output[0]): - output.insert(0, "") - - # If the path starts with '/.' or '/..' ensure we add one more empty - # string to add a trailing '/' - if path.endswith(("/.", "/..")): - output.append("") - - return "/".join(output) - - -@typing.overload -def _normalize_host(host: None, scheme: str | None) -> None: ... - - -@typing.overload -def _normalize_host(host: str, scheme: str | None) -> str: ... - - -def _normalize_host(host: str | None, scheme: str | None) -> str | None: - if host: - if scheme in _NORMALIZABLE_SCHEMES: - is_ipv6 = _IPV6_ADDRZ_RE.match(host) - if is_ipv6: - # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as - # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID - # separator as necessary to return a valid RFC 4007 scoped IP. - match = _ZONE_ID_RE.search(host) - if match: - start, end = match.span(1) - zone_id = host[start:end] - - if zone_id.startswith("%25") and zone_id != "%25": - zone_id = zone_id[3:] - else: - zone_id = zone_id[1:] - zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) - return f"{host[:start].lower()}%{zone_id}{host[end:]}" - else: - return host.lower() - elif not _IPV4_RE.match(host): - return to_str( - b".".join([_idna_encode(label) for label in host.split(".")]), - "ascii", - ) - return host - - -def _idna_encode(name: str) -> bytes: - if not name.isascii(): - try: - import idna - except ImportError: - raise LocationParseError( - "Unable to parse URL without the 'idna' module" - ) from None - - try: - return idna.encode(name.lower(), strict=True, std3_rules=True) - except idna.IDNAError: - raise LocationParseError( - f"Name '{name}' is not a valid IDNA label" - ) from None - - return name.lower().encode("ascii") - - -def _encode_target(target: str) -> str: - """Percent-encodes a request target so that there are no invalid characters - - Pre-condition for this function is that 'target' must start with '/'. - If that is the case then _TARGET_RE will always produce a match. - """ - match = _TARGET_RE.match(target) - if not match: # Defensive: - raise LocationParseError(f"{target!r} is not a valid request URI") - - path, query = match.groups() - encoded_target = _encode_invalid_chars(path, _PATH_CHARS) - if query is not None: - query = _encode_invalid_chars(query, _QUERY_CHARS) - encoded_target += "?" + query - return encoded_target - - -def parse_url(url: str) -> Url: - """ - Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is - performed to parse incomplete urls. Fields not provided will be None. - This parser is RFC 3986 and RFC 6874 compliant. - - The parser logic and helper functions are based heavily on - work done in the ``rfc3986`` module. - - :param str url: URL to parse into a :class:`.Url` namedtuple. - - Partly backwards-compatible with :mod:`urllib.parse`. - - Example: - - .. code-block:: python - - import urllib3 - - print( urllib3.util.parse_url('http://google.com/mail/')) - # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) - - print( urllib3.util.parse_url('google.com:80')) - # Url(scheme=None, host='google.com', port=80, path=None, ...) - - print( urllib3.util.parse_url('/foo?bar')) - # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) - """ - if not url: - # Empty - return Url() - - source_url = url - if not _SCHEME_RE.search(url): - url = "//" + url - - scheme: str | None - authority: str | None - auth: str | None - host: str | None - port: str | None - port_int: int | None - path: str | None - query: str | None - fragment: str | None - - try: - scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] - normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES - - if scheme: - scheme = scheme.lower() - - if authority: - auth, _, host_port = authority.rpartition("@") - auth = auth or None - host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] - if auth and normalize_uri: - auth = _encode_invalid_chars(auth, _USERINFO_CHARS) - if port == "": - port = None - else: - auth, host, port = None, None, None - - if port is not None: - port_int = int(port) - if not (0 <= port_int <= 65535): - raise LocationParseError(url) - else: - port_int = None - - host = _normalize_host(host, scheme) - - if normalize_uri and path: - path = _remove_path_dot_segments(path) - path = _encode_invalid_chars(path, _PATH_CHARS) - if normalize_uri and query: - query = _encode_invalid_chars(query, _QUERY_CHARS) - if normalize_uri and fragment: - fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) - - except (ValueError, AttributeError) as e: - raise LocationParseError(source_url) from e - - # For the sake of backwards compatibility we put empty - # string values for path if there are any defined values - # beyond the path in the URL. - # TODO: Remove this when we break backwards compatibility. - if not path: - if query is not None or fragment is not None: - path = "" - else: - path = None - - return Url( - scheme=scheme, - auth=auth, - host=host, - port=port_int, - path=path, - query=query, - fragment=fragment, - ) diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py deleted file mode 100644 index 35c77e4025..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/util.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import typing -from types import TracebackType - - -def to_bytes( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> bytes: - if isinstance(x, bytes): - return x - elif not isinstance(x, str): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.encode(encoding or "utf-8", errors=errors or "strict") - return x.encode() - - -def to_str( - x: str | bytes, encoding: str | None = None, errors: str | None = None -) -> str: - if isinstance(x, str): - return x - elif not isinstance(x, bytes): - raise TypeError(f"not expecting type {type(x).__name__}") - if encoding or errors: - return x.decode(encoding or "utf-8", errors=errors or "strict") - return x.decode() - - -def reraise( - tp: type[BaseException] | None, - value: BaseException, - tb: TracebackType | None = None, -) -> typing.NoReturn: - try: - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - finally: - value = None # type: ignore[assignment] - tb = None diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py deleted file mode 100644 index aeca0c7ad5..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSendDefaultPii/urllib3/util/wait.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import select -import socket -from functools import partial - -__all__ = ["wait_for_read", "wait_for_write"] - - -# How should we wait on sockets? -# -# There are two types of APIs you can use for waiting on sockets: the fancy -# modern stateful APIs like epoll/kqueue, and the older stateless APIs like -# select/poll. The stateful APIs are more efficient when you have a lots of -# sockets to keep track of, because you can set them up once and then use them -# lots of times. But we only ever want to wait on a single socket at a time -# and don't want to keep track of state, so the stateless APIs are actually -# more efficient. So we want to use select() or poll(). -# -# Now, how do we choose between select() and poll()? On traditional Unixes, -# select() has a strange calling convention that makes it slow, or fail -# altogether, for high-numbered file descriptors. The point of poll() is to fix -# that, so on Unixes, we prefer poll(). -# -# On Windows, there is no poll() (or at least Python doesn't provide a wrapper -# for it), but that's OK, because on Windows, select() doesn't have this -# strange calling convention; plain select() works fine. -# -# So: on Windows we use select(), and everywhere else we use poll(). We also -# fall back to select() in case poll() is somehow broken or missing. - - -def select_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - rcheck = [] - wcheck = [] - if read: - rcheck.append(sock) - if write: - wcheck.append(sock) - # When doing a non-blocking connect, most systems signal success by - # marking the socket writable. Windows, though, signals success by marked - # it as "exceptional". We paper over the difference by checking the write - # sockets for both conditions. (The stdlib selectors module does the same - # thing.) - fn = partial(select.select, rcheck, wcheck, wcheck) - rready, wready, xready = fn(timeout) - return bool(rready or wready or xready) - - -def poll_wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - if not read and not write: - raise RuntimeError("must specify at least one of read=True, write=True") - mask = 0 - if read: - mask |= select.POLLIN - if write: - mask |= select.POLLOUT - poll_obj = select.poll() - poll_obj.register(sock, mask) - - # For some reason, poll() takes timeout in milliseconds - def do_poll(t: float | None) -> list[tuple[int, int]]: - if t is not None: - t *= 1000 - return poll_obj.poll(t) - - return bool(do_poll(timeout)) - - -def _have_working_poll() -> bool: - # Apparently some systems have a select.poll that fails as soon as you try - # to use it, either due to strange configuration or broken monkeypatching - # from libraries like eventlet/greenlet. - try: - poll_obj = select.poll() - poll_obj.poll(0) - except (AttributeError, OSError): - return False - else: - return True - - -def wait_for_socket( - sock: socket.socket, - read: bool = False, - write: bool = False, - timeout: float | None = None, -) -> bool: - # We delay choosing which implementation to use until the first time we're - # called. We could do it at import time, but then we might make the wrong - # decision if someone goes wild with monkeypatching select.poll after - # we're imported. - global wait_for_socket - if _have_working_poll(): - wait_for_socket = poll_wait_for_socket - elif hasattr(select, "select"): - wait_for_socket = select_wait_for_socket - return wait_for_socket(sock, read, write, timeout) - - -def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for reading to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, read=True, timeout=timeout) - - -def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: - """Waits for writing to be available on a given socket. - Returns True if the socket is readable, or False if the timeout expired. - """ - return wait_for_socket(sock, write=True, timeout=timeout) From 951c408a4bdcc0c5113ae346a1690dee10ad85a0 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 9 Jul 2026 17:21:17 -0400 Subject: [PATCH 8/8] add test coverage within the wsgi test file --- tests/integrations/wsgi/test_wsgi.py | 250 +++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 3b684adb0d..cceb351ba7 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -858,6 +858,256 @@ def test_get_request_url_x_forwarded_proto(environ, use_x_forwarded_for, expecte assert get_request_url(environ, use_x_forwarded_for) == expected_url +@pytest.mark.parametrize("send_default_pii", [True, False]) +def test_request_headers_data_collection_default_redacts_sensitive( + sentry_init, crashing_app, capture_events, send_default_pii +): + """ + When ``data_collection`` is configured (even as ``None``, i.e. spec + defaults), the WSGI event processor routes request headers through the + data-collection filtering path. Sensitive headers are redacted regardless + of ``send_default_pii`` -- the value of that legacy option must not change + the outcome. + """ + sentry_init( + send_default_pii=send_default_pii, + _experiments={"data_collection": None}, + ) + app = SentryWsgiMiddleware(crashing_app) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get( + "/", + headers={ + "Authorization": "Bearer secret-token", + "X-Custom-Header": "passthrough", + }, + ) + + (event,) = events + headers = event["request"]["headers"] + + assert headers["Authorization"] == "[Filtered]" + assert headers["X-Custom-Header"] == "passthrough" + + +def test_request_headers_legacy_no_pii_redacts_sensitive( + sentry_init, crashing_app, capture_events +): + """ + With no ``data_collection`` configured, ``_filter_headers`` falls back to + the legacy ``send_default_pii`` behaviour. When PII is disabled, headers in + ``SENSITIVE_HEADERS`` are replaced with an ``AnnotatedValue`` (the default + ``use_annotated_value=True`` on the event-processor call site), which + serializes to an emptied value plus a ``_meta`` annotation. Non-sensitive + headers pass through untouched. + + ``X-Forwarded-For`` is used because it is in ``SENSITIVE_HEADERS`` but is + not scrubbed by the default ``EventScrubber``, so the substitution we are + asserting on can only come from ``_filter_headers``. + """ + sentry_init(send_default_pii=False) + app = SentryWsgiMiddleware(crashing_app) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get( + "/", + headers={ + "X-Forwarded-For": "1.2.3.4", + "X-Custom-Header": "passthrough", + }, + ) + + (event,) = events + + assert event["request"]["headers"]["X-Forwarded-For"] == "" + assert event["request"]["headers"]["X-Custom-Header"] == "passthrough" + + # The emptied value is accompanied by a `_meta` annotation marking it as + # removed, confirming the substitution came from the AnnotatedValue path. + assert event["_meta"]["request"]["headers"]["X-Forwarded-For"] == { + "": {"rem": [["!config", "x"]]} + } + + +def test_request_headers_data_collection_off_collects_no_headers( + sentry_init, crashing_app, capture_events +): + """ + With ``http_headers.request`` mode set to ``off``, no request headers are + collected at all -- the filtering returns an empty mapping. + """ + sentry_init( + _experiments={ + "data_collection": {"http_headers": {"request": {"mode": "off"}}} + }, + ) + app = SentryWsgiMiddleware(crashing_app) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get( + "/", + headers={ + "X-Forwarded-For": "1.2.3.4", + "X-Custom-Header": "passthrough", + }, + ) + + (event,) = events + + assert event["request"]["headers"] == {} + + +def test_request_headers_data_collection_allowlist_redacts_all_but_allowed_terms( + sentry_init, crashing_app, capture_events +): + """ + An ``allowlist`` allows through only headers matching a configured term + (partial, case-insensitive); every other header key is kept but its value + is redacted. + """ + sentry_init( + _experiments={ + "data_collection": { + "http_headers": {"request": {"mode": "allowlist", "terms": ["custom"]}} + } + }, + ) + app = SentryWsgiMiddleware(crashing_app) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get( + "/", + headers={ + "X-Forwarded-For": "1.2.3.4", + "X-Custom-Header": "passthrough", + }, + ) + + (event,) = events + headers = event["request"]["headers"] + + assert headers["X-Custom-Header"] == "passthrough" + assert headers["X-Forwarded-For"] == "[Filtered]" + assert headers["Host"] == "[Filtered]" + + +def test_request_headers_data_collection_denylist_redacts_only_matched_terms( + sentry_init, crashing_app, capture_events +): + """ + A ``denylist`` passes headers through by default, redacting only those + matching a configured term (partial, case-insensitive). + """ + sentry_init( + _experiments={ + "data_collection": { + "http_headers": {"request": {"mode": "denylist", "terms": ["custom"]}} + } + }, + ) + app = SentryWsgiMiddleware(crashing_app) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get( + "/", + headers={ + "X-Forwarded-For": "1.2.3.4", + "X-Custom-Header": "passthrough", + }, + ) + + (event,) = events + headers = event["request"]["headers"] + + assert headers["X-Custom-Header"] == "[Filtered]" + assert headers["X-Forwarded-For"] == "1.2.3.4" + assert headers["Host"] == "localhost" + + +def test_request_headers_data_collection_cookie_always_redacted( + sentry_init, crashing_app, capture_events +): + """ + The ``cookie``/``set-cookie`` headers are always redacted in the + data-collection path, even when explicitly allowlisted. A sibling header + (``custom``) that is also allowlisted passes through, isolating the + cookie override. + + The middleware is driven with an explicit environ because werkzeug's test + ``Client`` manages its own cookie jar and strips the ``Cookie`` header. + """ + sentry_init( + _experiments={ + "data_collection": { + "http_headers": { + "request": {"mode": "allowlist", "terms": ["cookie", "custom"]} + } + } + }, + ) + app = SentryWsgiMiddleware(crashing_app) + events = capture_events() + + environ = { + "REQUEST_METHOD": "GET", + "PATH_INFO": "/", + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "wsgi.url_scheme": "http", + "HTTP_COOKIE": "sessionid=secret", + "HTTP_X_CUSTOM_HEADER": "passthrough", + } + + with pytest.raises(ZeroDivisionError): + list(app(environ, lambda status, headers: None)) + + (event,) = events + headers = event["request"]["headers"] + + assert headers["Cookie"] == "[Filtered]" + assert headers["X-Custom-Header"] == "passthrough" + + +def test_request_headers_legacy_pii_passes_headers_through( + sentry_init, crashing_app, capture_events +): + """ + With no ``data_collection`` configured and ``send_default_pii`` enabled, + the legacy path returns all headers unchanged -- including those in + ``SENSITIVE_HEADERS``. + """ + sentry_init(send_default_pii=True) + app = SentryWsgiMiddleware(crashing_app) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get( + "/", + headers={ + "X-Forwarded-For": "1.2.3.4", + "X-Custom-Header": "passthrough", + }, + ) + + (event,) = events + headers = event["request"]["headers"] + + assert headers["X-Forwarded-For"] == "1.2.3.4" + assert headers["X-Custom-Header"] == "passthrough" + + @pytest.mark.parametrize("send_default_pii", [True, False]) def test_user_ip_address_on_all_spans(sentry_init, capture_items, send_default_pii): def dogpark(environ, start_response):